+
+
+
+ The current screen size is not supported. Use a larger display to access the CVE table.
+
+
+
+ setActiveTabKey(key)}
+ items={tabs}
+ destroyInactiveTabPane={false}
+ type="card"
+ />
+
+
+
+ );
+}
diff --git a/src/components/CveReportsTable/index.ts b/src/components/CveReportsTable/index.ts
new file mode 100644
index 0000000000..0cfd2630e3
--- /dev/null
+++ b/src/components/CveReportsTable/index.ts
@@ -0,0 +1,3 @@
+import CveReportsTable from "./CveReportsTable";
+
+export default CveReportsTable;
diff --git a/utils/cves/index.js b/utils/cves/index.js
new file mode 100644
index 0000000000..43d6b10281
--- /dev/null
+++ b/utils/cves/index.js
@@ -0,0 +1,283 @@
+const { api, callRateLimitAPI } = require("./requests");
+const { existsSync, mkdirSync } = require("node:fs");
+const { logger } = require("@docusaurus/logger");
+const fs = require("fs").promises;
+const path = require("path");
+const { formatDateCveDetails } = require("../helpers/date");
+const { escapeMDXSpecialChars } = require("../helpers/string");
+const { generateMarkdownTable } = require("../helpers/affected-table");
+const { generateRevisionHistory } = require("../helpers/revision-history");
+
+async function getSecurityBulletins(payload) {
+ try {
+ return await callRateLimitAPI(() => api.post(`https://dso.teams.spectrocloud.com/v1/advisories`, payload));
+ } catch (error) {
+ logger.error(error);
+ logger.error("Error:", error.response ? error.response.data || error.response.status : error.message);
+ }
+}
+
+async function generateCVEs() {
+ let GlobalCVEData = {};
+
+ const securityBulletins = new Map();
+ const dirname = path.join(".docusaurus", "security-bulletins", "default");
+ const filename = path.join(dirname, "data.json");
+
+ if (process.env.DISABLE_SECURITY_INTEGRATIONS === "true") {
+ logger.info("Security integrations are disabled. Skipping generation of security bulletins.");
+ if (!existsSync(dirname) || !existsSync(filename)) {
+ // Write the security bulletins data to a JSON file
+ mkdirSync(dirname, { recursive: true });
+ await fs.writeFile(filename, JSON.stringify({}, null, 2));
+ }
+ return;
+ }
+
+ if (existsSync(dirname) && existsSync(filename)) {
+ logger.info("Security bulletins JSON file already exists. Skipping fetching.");
+ GlobalCVEData = JSON.parse(await fs.readFile(filename, "utf-8"));
+ } else {
+ logger.info("Fetching security bulletins...");
+
+ try {
+ const palette = await getSecurityBulletins({
+ filters: [
+ {
+ field: "metadata.nistSeverity",
+ operator: "in",
+ options: ["CRITICAL", "HIGH"],
+ },
+ {
+ field: "spec.impact.impactedProducts.palette",
+ operator: "ex",
+ },
+ {
+ field: "spec.impact.impactedDeployments.connected",
+ operator: "ex",
+ },
+ ],
+ });
+ const paletteAirgap = await getSecurityBulletins({
+ filters: [
+ {
+ field: "metadata.nistSeverity",
+ operator: "in",
+ options: ["CRITICAL", "HIGH"],
+ },
+ {
+ field: "spec.impact.impactedProducts.palette",
+ operator: "ex",
+ },
+ {
+ field: "spec.impact.impactedDeployments.airgap",
+ operator: "ex",
+ },
+ ],
+ });
+ const vertex = await getSecurityBulletins({
+ filters: [
+ {
+ field: "metadata.nistSeverity",
+ operator: "in",
+ options: ["CRITICAL", "HIGH"],
+ },
+ {
+ field: "spec.impact.impactedProducts.vertex",
+ operator: "ex",
+ },
+ {
+ field: "spec.impact.impactedDeployments.connected",
+ operator: "ex",
+ },
+ ],
+ });
+ const vertexAirgap = await getSecurityBulletins({
+ filters: [
+ {
+ field: "metadata.nistSeverity",
+ operator: "in",
+ options: ["CRITICAL", "HIGH"],
+ },
+ {
+ field: "spec.impact.impactedProducts.vertex",
+ operator: "ex",
+ },
+ {
+ field: "spec.impact.impactedDeployments.airgap",
+ operator: "ex",
+ },
+ ],
+ });
+
+ securityBulletins.set("palette", palette);
+ securityBulletins.set("paletteAirgap", paletteAirgap);
+ securityBulletins.set("vertex", vertex);
+ securityBulletins.set("vertexAirgap", vertexAirgap);
+
+ // const plainObject = Object.fromEntries(securityBulletins);
+ const plainObject = Object.fromEntries(
+ Array.from(securityBulletins.entries()).map(([key, value]) => [key, value.data])
+ );
+ GlobalCVEData = plainObject;
+
+ // Write the security bulletins data to a JSON file
+ mkdirSync(dirname, { recursive: true });
+ await fs.writeFile(filename, JSON.stringify(GlobalCVEData, null, 2));
+
+ logger.info("Finished fetching security bulletins data.");
+ } catch (error) {
+ logger.error(error);
+ logger.error("Error:", error.response ? error.response.status : error.message);
+ }
+ }
+
+ await generateMarkdownForCVEs(GlobalCVEData);
+}
+
+async function generateMarkdownForCVEs(GlobalCVEData) {
+ const allCVEs = Object.values(GlobalCVEData).reduce((acc, curr) => acc.concat(curr), []);
+
+ // 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 markdownPromises = allCVEs.map((item) =>
+ createCveMarkdown(item, cveImpactMap[item.metadata.cve], "docs/docs-content/security-bulletins/reports/")
+ );
+
+ const results = await Promise.all(markdownPromises);
+
+ const failedFiles = results.filter((result) => !result.success);
+
+ if (failedFiles.length > 0) {
+ logger.error("Failed to generate the following markdown files:");
+ failedFiles.forEach((failure) => {
+ logger.error(`File: ${failure.file}, Error: ${failure.error.message}`);
+ });
+ }
+
+ logger.success("All security bulletin markdown files generated.");
+}
+
+function createCveMarkdown(item, cveImpactData, location) {
+ const upperCaseCve = item.metadata.cve.toUpperCase();
+ const revisions = item.spec.revision;
+ const uid = item.metadata.uid.toLowerCase();
+
+ // Generate a table of impacted products
+ let table = generateMarkdownTable(cveImpactData);
+ let revisionHistory = generateRevisionHistory(revisions);
+
+ const content = `---
+sidebar_label: "${upperCaseCve}"
+title: "${upperCaseCve}"
+description: "Lifecycle of ${upperCaseCve}"
+sidebar_class_name: "hide-from-sidebar"
+hide_table_of_contents: false
+toc_max_heading_level: 2
+tags: ["security", "cve"]
+---
+
+## CVE Details
+
+[${upperCaseCve}](https://nvd.nist.gov/vuln/detail/${upperCaseCve})
+
+## Initial Publication
+
+${formatDateCveDetails(item.metadata.advCreatedTimestamp)}
+
+## Last Update
+
+${formatDateCveDetails(item.metadata.advLastModifiedTimestamp)}
+
+${item.spec.assessment?.thirdParty?.dependentPackage != "" ? `## Third Party Dependency \n\n${item.spec.assessment.thirdParty.dependentPackage}` : "This CVE does not have a third party dependency."}
+
+
+## NIST CVE Summary
+
+${escapeMDXSpecialChars(item.metadata.summary)}
+
+## CVE Severity
+
+${item.metadata.cvssScore}
+
+## Our Official Summary
+
+${item.spec.assessment.justification ? escapeMDXSpecialChars(item.spec.assessment.justification) : "Investigation is ongoing to determine how this vulnerability affects our products."}
+
+## Status
+
+${item.status.status}
+
+## Affected Products & Versions
+
+${item.spec.impact.isImpacting ? table : "This CVE is non-impacting as the impacting symbol and/or function is not used in the product"}
+
+
+## Revision History
+
+${revisionHistory ? revisionHistory : "No revision history available."}
+`;
+
+ const filePath = path.join(location, `${uid}.md`);
+
+ // Return a promise and include the CVE or file path in the error log
+ return fs
+ .writeFile(filePath, content)
+ .then(() => ({
+ success: true,
+ file: filePath,
+ }))
+ .catch((err) => {
+ console.error(`Error writing file for ${upperCaseCve} at ${filePath}:`, err);
+ return {
+ success: false,
+ file: filePath,
+ error: err,
+ };
+ });
+}
+
+// Call the main function to generate CVEs
+generateCVEs();
diff --git a/utils/cves/requests.js b/utils/cves/requests.js
new file mode 100644
index 0000000000..1d240728ef
--- /dev/null
+++ b/utils/cves/requests.js
@@ -0,0 +1,52 @@
+const axios = require("axios");
+const axiosRetry = require("axios-retry").default;
+const { pRateLimit } = require("p-ratelimit");
+require("dotenv").config();
+
+const SECURITY_BULLETIN_URL = "https://dso.teams.spectrocloud.com";
+
+// Ensure that the authentication token is available in the environment
+const authToken = process.env.DSO_AUTH_TOKEN;
+if (!authToken) {
+ throw new Error("DSO_AUTH_TOKEN must be set in the environment to use this plugin.");
+}
+
+const api = axios.create({
+ baseURL: SECURITY_BULLETIN_URL,
+ timeout: 120000, // 2 minutes timeout
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Basic " + authToken, // Use the environment variable for auth token
+ },
+});
+
+// Set up rate limiting using pRateLimit
+const limit = pRateLimit({
+ interval: 2000, // 2 seconds
+ rate: 10, // 10 API calls per interval
+ concurrency: 1, // no more than 1 running at once
+});
+
+axiosRetry(api, {
+ retries: 3, // Retry up to 3 times
+ retryDelay: axiosRetry.exponentialDelay, // Exponential backoff starting with 1 second
+ retryCondition(error) {
+ // Retry based on status codes
+ switch (error.response?.status) {
+ case 500:
+ case 404:
+ case 501:
+ case 429:
+ return true;
+ default:
+ return false;
+ }
+ },
+});
+
+// Function to handle API calls with rate limiting
+function callRateLimitAPI(delayedApiCall) {
+ return limit(delayedApiCall);
+}
+
+module.exports = { api, callRateLimitAPI };
diff --git a/utils/helpers/affected-table.js b/utils/helpers/affected-table.js
new file mode 100644
index 0000000000..da23a3d27b
--- /dev/null
+++ b/utils/helpers/affected-table.js
@@ -0,0 +1,48 @@
+const semver = require("semver");
+
+function generateMarkdownTable(cveImpactMap) {
+ if (!cveImpactMap || typeof cveImpactMap !== "object") {
+ throw new Error("Invalid input: cveImpactMap must be an object.");
+ }
+
+ const impactData = {
+ "Palette Enterprise": cveImpactMap.impactsPaletteEnterprise,
+ "Palette Enterprise Airgap": cveImpactMap.impactsPaletteEnterpriseAirgap,
+ VerteX: cveImpactMap.impactsVerteX,
+ "VerteX Airgap": cveImpactMap.impactsVerteXAirgap,
+ };
+
+ const allProductsFalse = Object.values(impactData).every((value) => value === false);
+ if (allProductsFalse) {
+ 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)) {
+ throw new Error("Error: Data inconsistency - Products impacted but no versions provided.");
+ }
+
+ // Create the header row with the specified order
+ 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);
+
+ 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",
+ ].join(" | ");
+ return row + " |";
+ })
+ .join("\n");
+
+ return header + separator + rows;
+}
+
+module.exports = { generateMarkdownTable };
diff --git a/utils/helpers/affected-table.test.js b/utils/helpers/affected-table.test.js
new file mode 100644
index 0000000000..5cda941769
--- /dev/null
+++ b/utils/helpers/affected-table.test.js
@@ -0,0 +1,47 @@
+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,
+ };
+
+ 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 |`;
+
+ expect(generateMarkdownTable(cveImpactMap).replace(/\s+/g, "")).toBe(expectedTable.replace(/\s+/g, ""));
+ });
+
+ 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,
+ };
+
+ 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,
+ };
+
+ expect(() => generateMarkdownTable(cveImpactMap)).toThrow(
+ "Error: Data inconsistency - Products impacted but no versions provided."
+ );
+ });
+});
diff --git a/utils/helpers/date.js b/utils/helpers/date.js
new file mode 100644
index 0000000000..d49b8dcb84
--- /dev/null
+++ b/utils/helpers/date.js
@@ -0,0 +1,23 @@
+function getTodayFormattedDate() {
+ const options = { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" };
+ const formattedDate = new Date().toLocaleDateString("en-CA", options);
+ return formattedDate;
+}
+
+function formatDateCveDetails(isoString) {
+ const date = new Date(isoString);
+
+ // Check if the date is valid
+ if (isNaN(date.getTime())) {
+ console.warn(`Invalid date string: ${isoString}`);
+ return "N/A"; // or an appropriate placeholder for invalid dates
+ }
+
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0"); // Pad month to 2 digits
+ const day = String(date.getUTCDate()).padStart(2, "0"); // Pad day to 2 digits
+ const year = date.getUTCFullYear();
+
+ return `${month}/${day}/${year}`;
+}
+
+module.exports = { getTodayFormattedDate, formatDateCveDetails };
diff --git a/utils/helpers/dates.test.js b/utils/helpers/dates.test.js
new file mode 100644
index 0000000000..d06e062284
--- /dev/null
+++ b/utils/helpers/dates.test.js
@@ -0,0 +1,51 @@
+const { getTodayFormattedDate, formatDateCveDetails } = require("./date");
+
+describe("getTodayFormattedDate", () => {
+ it("should return today's date formatted as YYYY-MM-DD in America/Los_Angeles timezone", () => {
+ const options = { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" };
+ const expectedDate = new Date().toLocaleDateString("en-CA", options);
+
+ expect(getTodayFormattedDate()).toBe(expectedDate);
+ });
+
+ it("should return the date in YYYY-MM-DD format", () => {
+ const formattedDate = getTodayFormattedDate();
+ expect(formattedDate).toMatch(/^\d{4}-\d{2}-\d{2}$/); // Check for correct format
+ });
+});
+
+describe("formatDateCveDetails", () => {
+ it("should format ISO string date to MM/DD/YYYY with zero-padded month and day", () => {
+ const isoString = "2023-09-05T00:00:00Z";
+ const formattedDate = formatDateCveDetails(isoString);
+
+ expect(formattedDate).toBe("09/05/2023");
+ });
+
+ it("should handle leap years correctly", () => {
+ const isoString = "2024-02-29T00:00:00Z";
+ const formattedDate = formatDateCveDetails(isoString);
+
+ expect(formattedDate).toBe("02/29/2024");
+ });
+
+ it("should return the correct date even with different time zones in the input", () => {
+ const isoString = "2023-09-20T15:00:00Z"; // Time zone is UTC but should still give the same day in UTC
+ const formattedDate = formatDateCveDetails(isoString);
+
+ expect(formattedDate).toBe("09/20/2023");
+ });
+
+ it("should return 'N/A' for an invalid date string", () => {
+ const invalidDate = "invalid-date";
+ const formattedDate = formatDateCveDetails(invalidDate);
+
+ expect(formattedDate).toBe("N/A");
+ });
+
+ it("should return 'N/A' for undefined input", () => {
+ const formattedDate = formatDateCveDetails(undefined);
+
+ expect(formattedDate).toBe("N/A");
+ });
+});
diff --git a/utils/helpers/revision-history.js b/utils/helpers/revision-history.js
new file mode 100644
index 0000000000..29f9a36647
--- /dev/null
+++ b/utils/helpers/revision-history.js
@@ -0,0 +1,104 @@
+const { formatDateCveDetails } = require("./date");
+
+/**
+ * Generates a markdown table for revision history, sorted by newest entries first
+ * @param {Array