diff --git a/.gitignore b/.gitignore index 7f6b81740c..48aea601a0 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,4 @@ dmypy.json .pyre/ .DS_Store +yarn.lock \ No newline at end of file diff --git a/community_images/common/templates/image_readme.j2 b/community_images/common/templates/image_readme.j2 index c1ba323c54..e42176ea68 100644 --- a/community_images/common/templates/image_readme.j2 +++ b/community_images/common/templates/image_readme.j2 @@ -152,8 +152,8 @@ Learn more about RapidFort's pioneering Software Attack Surface Management platf [slack-link]: https://join.slack.com/t/rapidfortcommunity/shared_invite/zt-1g3wy28lv-DaeGexTQ5IjfpbmYW7Rm_Q [rf-h-badge]: https://img.shields.io/static/v1?label=RapidFort&labelColor=333F48&message=hardened&color=50B4C4&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACcAAAAkCAYAAAAKNyObAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAHvSURBVHgB7ZjvTcMwEMUvEgNkhNuAjOAR2IAyQbsB2YAyQbsBYoKwQdjA3aAjHA514Xq1Hf9r6QeeFKVJ3tkv+cWOVYCAiKg124b82gZqe0+NNlsHJbLBxthg1o+RASetIEdTJxnBRvtUMCHgM6TIBtMZwY7SiQFfrhUsN+Ao/TJYR3WC5QY88/Nge6oXLBRwO+P/GcnNMZzZteBR0zQfogM0O4Q47Uz9TtSrUIHs71+paugw16Dn+qt5xJ/TD4viEcrE25tepaXPaHxP350GXtD10WwHQWjQxKhl7YUGRg/MuPaY9vxuzPFA+RpEW9rj0yCMbcCsmG9B+Xpk7YRo4RnjQEEttBiBtAefyI23BtoYpBrmRO6ZX0EZWo60c1yfaGBMOKRzdKVocYZO/NpuMss7E9cHitcc0gFS5Qig2LUUtCGkmmJwOsJJvLlokdWtfMFzAvLGctCOooYPtg2USoRQ7HwM2hXzIzuvKQenIxzHm4oWmZ9TKF1AnAR8sI2moB093nKcjoBvtnHFzoXQ8qeMDGcLtUW/i4NYtJ3jJhRcSnRYHMSg1Q5PD5cWHT4/ih0vIpDOf9QrhZtQLsWxlILT8AjXEol/iQRaiVTBX4pO57D6U0WJBFoFtyaLtuqLfwf19G62e7hFWbQKKuoLYovGDo9dW28AAAAASUVORK5CYII= -[metrics-link]: https://github.com/rapidfort/community-images/raw/main/community_images/ {{- github_location -}} /assets/metrics.webp -[cve-reduction-link]: https://github.com/rapidfort/community-images/raw/main/community_images/ {{- github_location -}} /assets/cve_reduction.webp +[metrics-link]: https://github.com/rapidfort/community-images/raw/main/community_images/ {{- github_location -}} /assets/metrics.svg +[cve-reduction-link]: https://github.com/rapidfort/community-images/raw/main/community_images/ {{- github_location -}} /assets/cve_reduction.svg [source-image-repo-link]: {{ source_image_repo_link }} [rf-dh-image-link]: https://hub.docker.com/r/{{- rf_docker_link }} diff --git a/report_shots/cvss-parser/cvss.js b/report_shots/cvss-parser/cvss.js new file mode 100644 index 0000000000..b130831738 --- /dev/null +++ b/report_shots/cvss-parser/cvss.js @@ -0,0 +1,33 @@ +const cvssParserV2 = require('./v2'); +const cvssParserV3 = require('./v3'); +function memoize(fn) { + const cache = new Map(); + return function(...args) { + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key); + } + const result = fn.apply(this, args); + cache.set(key, result); + return result; + }; +} + +const parse = (vector, version) => { + let parser; + switch (version){ + case 'V2': + parser = cvssParserV2; + break; + case 'V3': + parser = cvssParserV3; + break; + default: + parser = cvssParserV3; + break; + } + return parser.parse(vector) +}; +module.exports = { + parse:memoize(parse) +} \ No newline at end of file diff --git a/report_shots/cvss-parser/helpers.js b/report_shots/cvss-parser/helpers.js new file mode 100644 index 0000000000..6ed6ee92c7 --- /dev/null +++ b/report_shots/cvss-parser/helpers.js @@ -0,0 +1,22 @@ +function roundUpApprox(num, precision) { + precision = Math.pow(10, precision); + return Math.ceil(num * precision) / precision; +} +function roundUpExact(num) { + const int_input = Math.round(num * 100000); + if (int_input % 10000 === 0) { + return int_input / 100000; + } else { + return (Math.floor(int_input / 10000) + 1) / 10; + } +} +function roundUp (value) { + let rounded = Math.round(value * 100000); + return rounded % 10000 === 0 ? rounded / 100000.0 : (Math.floor(rounded / 10000) + 1) / 10.0; +} + +module.exports = { + roundUpApprox, + roundUpExact, + roundUp +}; \ No newline at end of file diff --git a/report_shots/cvss-parser/v2.js b/report_shots/cvss-parser/v2.js new file mode 100644 index 0000000000..9cdbb58609 --- /dev/null +++ b/report_shots/cvss-parser/v2.js @@ -0,0 +1,240 @@ +const metricWeights = { + // Base metrics + AV: { + title: 'Access Vector', + values: { + N: { weight: 1.0, title: "Network", default:true}, + A: { weight: 0.646, title: "Adjacent Network" }, + L: { weight: 0.395, title: "Local" } + } + }, + AC: { + title: 'Access Complexity', + values: { + H: { weight: 0.35, title: "High", default:true }, + M: { weight: 0.61, title: "Medium" }, + L: { weight: 0.71, title: "Low" } + } + }, + Au: { + title: 'Authentication', + values: { + M: { weight: 0.45, title: "Multiple", default:true }, + S: { weight: 0.56, title: "Single" }, + N: { weight: 0.704, title: "None" } + } + }, + C: { + title: 'Confidentiality Impact', + values: { + N: { weight: 0.0, title: "None" , default:true}, + P: { weight: 0.275, title: "Partial" }, + C: { weight: 0.660, title: "Complete" } + } + }, + I: { + title: 'Integrity Impact', + values: { + N: { weight: 0.0, title: "None", default:true }, + P: { weight: 0.275, title: "Partial" }, + C: { weight: 0.660, title: "Complete" } + } + }, + A: { + title: 'Availability Impact', + values: { + N: { weight: 0.0, title: "None", default:true }, + P: { weight: 0.275, title: "Partial" }, + C: { weight: 0.660, title: "Complete" } + } + }, + // Temporal metrics + E: { + title: 'Exploitability', + values: { + ND: { weight: 1.0, title: "Not Defined" , default:true}, + U: { weight: 0.85, title: "Unproven" }, + POC: { weight: 0.9, title: "Proof-of-Concept" }, + F: { weight: 0.95, title: "Functional" }, + H: { weight: 1.0, title: "High" } + } + }, + RL: { + title: 'Remediation Level', + values: { + ND: { weight: 1.0, title: "Not Defined", default:true }, + OF: { weight: 0.87, title: "Official Fix" }, + TF: { weight: 0.9, title: "Temporary Fix" }, + W: { weight: 0.95, title: "Workaround" }, + U: { weight: 1.0, title: "Unavailable" } + } + }, + RC: { + title: 'Report Confidence', + values: { + ND: { weight: 1.0, title: "Not Defined", default:true }, + UC: { weight: 0.9, title: "Uncorroborated" }, + UR: { weight: 0.95, title: "Uncorroborated" }, + C: { weight: 1.0, title: "Confirmed" } + } + }, + // Environmental metrics + CDP: { + title: 'Collateral Damage Potential', + values: { + ND: { weight: 0.0, title: "Not Defined", default:true }, + N: { weight: 0.0, title: "None" }, + L: { weight: 0.1, title: "Low" }, + LM: { weight: 0.3, title: "Low-Medium" }, + MH: { weight: 0.4, title: "Medium-High" }, + H: { weight: 0.5, title: "High" } + } + }, + TD: { + title: 'Target Distribution', + values: { + ND: { weight: 1.0, title: "Not Defined", default:true }, + N: { weight: 0.0, title: "None" }, + L: { weight: 0.25, title: "Low" }, + M: { weight: 0.75, title: "Medium" }, + H: { weight: 1.0, title: "High" } + } + }, + CR: { + title: 'Confidentiality Requirement', + values: { + ND: { weight: 1.0, title: "Not Defined", default:true }, + L: { weight: 0.5, title: "Low" }, + M: { weight: 1.0, title: "Medium" }, + H: { weight: 1.51, title: "High" } + } + }, + IR: { + title: 'Integrity Requirement', + values: { + ND: { weight: 1.0, title: "Not Defined", default:true }, + L: { weight: 0.5, title: "Low" }, + M: { weight: 1.0, title: "Medium" }, + H: { weight: 1.51, title: "High" } + } + }, + AR: { + title: 'Availability Requirement', + values: { + ND: { weight: 1.0, title: "Not Defined", default:true }, + L: { weight: 0.5, title: "Low" }, + M: { weight: 1.0, title: "Medium" }, + H: { weight: 1.51, title: "High" } + } + } +}; +const parse = (vector) => { + if (!vector) { + return false; + } + const metricsComponents = vector.split("/"); + const startIndex = metricsComponents[0].includes("CVSS2") ? 1 : 0; + const parsedMetrics = {}; + + for (let i = startIndex; i < metricsComponents.length; i++) { + const [metric, value] = metricsComponents[i].split(":"); + if (metricWeights[metric] && metricWeights[metric]?.values?.[value]) { + parsedMetrics[metric] = metricWeights[metric]?.values?.[value]; + parsedMetrics[metric].key = value + } else { + console.warn(`Unknown metric or value encountered for CVSSv2: ${metric}:${value}`); + } + } + + const cvss = { + metrics: parsedMetrics, + }; + + Object.keys(metricWeights).forEach((key) => { + cvss.metrics[key] = cvss.metrics[key] ? cvss.metrics[key] : { weight: 0, title: undefined, key:'X' }; + }); + + // Define functions to calculate various scores using cvss.metrics for CVSS v2 + function getImpactScore() { + return 10.41 * (1 - (1 - cvss.metrics.C.weight) * (1 - cvss.metrics.I.weight) * (1 - cvss.metrics.A.weight)); + } + + function getExploitabilityScore() { + return 20 * cvss.metrics.AV.weight * cvss.metrics.AC.weight * cvss.metrics.Au.weight; + } + + function getBaseScore() { + const impact = getImpactScore(); + const exploitability = getExploitabilityScore(); + const fImpactValue = impact === 0 ? 0 : 1.176; + const baseScore = ((0.6 * impact) + (0.4 * exploitability) - 1.5) * fImpactValue; + return roundUp(baseScore); // or use a different rounding function if needed + // return impact === 0 ? 0 : roundUp(Math.min((0.6 * impact) + (0.4 * exploitability) - 1.5, 10.0)); + } + + function getTemporalScore() { + const tempMetricKeys = ['E', 'RL', 'RC'] + const temporalScoreAvailable = tempMetricKeys.reduce((prev, cur)=> { + return cvss.metrics[cur].key !== 'X' || prev + }, false) + + if (!temporalScoreAvailable) { + return 0; + } + const E = (cvss.metrics.E.key !== 'X' ? cvss.metrics.E.weight : 1) ; + const RL = (cvss.metrics.RL.key !== 'X' ? cvss.metrics.RL.weight : 1); + const RC = (cvss.metrics.RC.key !== 'X' ? cvss.metrics.RC.weight : 1); + return roundUp(getBaseScore() * E * RL * RC); + } + + function getEnvironmentalScore() { + const environmentalMetrics = ['CR', 'IR', 'AR', 'CDP', 'TD']; + const allNotDefined = environmentalMetrics.every(metric => cvss.metrics[metric].key === 'ND'); + + if (allNotDefined) { + return null; // or however you wish to represent an undefined score + } else { + const temporalScoreAdjusted = getTemporalScore(true); // Assuming this function exists and can handle adjusted impact + const CDP = (cvss.metrics.CDP.key !== 'ND' ? cvss.metrics.CDP.weight : 1); + const TD = (cvss.metrics.TD.key !== 'ND' ? cvss.metrics.TD.weight : 1); + let rawEnvironmentalScore = ((temporalScoreAdjusted + (10 - temporalScoreAdjusted) * CDP) * TD); + // Round to one decimal place + rawEnvironmentalScore = roundUp(rawEnvironmentalScore); + // Ensure the score is not less than 0.0 + return Math.max(0.0, rawEnvironmentalScore); + } +} + const baseScore = roundUp(getBaseScore()); + const temporalScore = roundUp(getTemporalScore()); + const environmentalScore = roundUp(getEnvironmentalScore()) + let overallScore = baseScore; // Default to base score + + // If environmental score is not "NA" and not zero, use it + if (environmentalScore !== "NA" && environmentalScore !== 0) { + overallScore = environmentalScore; + } else if (temporalScore !== "NA" && temporalScore !== 0) { + overallScore = temporalScore; + } + return { + version: 'v2.0', + vector: vector, + metrics: cvss.metrics, + metricWeights:metricWeights, + impactScore: roundUp(getImpactScore()), + exploitabilityScore: roundUp(getExploitabilityScore()), + baseScore: baseScore, + temporalScore: temporalScore, + environmentalScore: environmentalScore, + overallScore:overallScore, + } +}; + +// Helper function to round up to one decimal place +function roundUp(number) { + return Math.round(number * 10) / 10; +} + +module.exports = { + metricWeights:metricWeights, + parse:parse, +}; \ No newline at end of file diff --git a/report_shots/cvss-parser/v3.js b/report_shots/cvss-parser/v3.js new file mode 100644 index 0000000000..fd3b99b50f --- /dev/null +++ b/report_shots/cvss-parser/v3.js @@ -0,0 +1,385 @@ +const { roundUp } = require('./helpers'); +const metricWeights = { + // Base Metric Group + AV: { + title: 'Attack Vector', + default:0.85, + values: { + N: { weight: 0.85, title: "Network" }, + A: { weight: 0.62, title: "Adjacent Network" }, + L: { weight: 0.55, title: "Local" }, + P: { weight: 0.2, title: "Physical" } + } + }, + AC: { + title: 'Attack Complexity', + values: { + L: { weight: 0.77, title: "Low" }, + H: { weight: 0.44, title: "High" } + } + }, + PR: { + title: 'Privileges Required', + values: { + N: { weight: 0.85, title: "None" }, + L: { weight: 0.62, title: "Low" }, + H: { weight: 0.27, title: "High" } + }, + valuesChanged: { + N: { weight: 0.85, title: "None" }, + L: { weight: 0.68, title: "Low" }, + H: { weight: 0.50, title: "High" }, + X: { weight: 0, title: "High" } + } + }, + UI: { + title: 'User Interaction', + values: { + N: { weight: 0.85, title: "None" }, + R: { weight: 0.62, title: "Required" } + } + }, + S: { + title: 'Scope', + values: { + U: { weight: 0, title: "Unchanged" }, + C: { weight: 0, title: "Changed" } + } + }, + C: { + title: 'Confidentiality Impact', + values: { + N: { weight: 0, title: "None" }, + L: { weight: 0.22, title: "Low" }, + H: { weight: 0.56, title: "High" } + } + }, + I: { + title: 'Integrity Impact', + default:0, + values: { + N: { weight: 0, title: "None" }, + L: { weight: 0.22, title: "Low" }, + H: { weight: 0.56, title: "High" } + } + }, + A: { + title: 'Availability Impact', + values: { + N: { weight: 0, title: "None" }, + L: { weight: 0.22, title: "Low" }, + H: { weight: 0.56, title: "High" } + } + }, + // Temporal Metric Group + E: { + title: 'Exploit Code Maturity', + values: { + X: { weight: 1, title: "Not Defined" }, + H: { weight: 1, title: "High" }, + F: { weight: 0.97, title: "Functional" }, + P: { weight: 0.94, title: "Proof-of-Concept" }, + U: { weight: 0.91, title: "Unproven" } + } + }, + RL: { + title: 'Remediation Level', + default:1, + values: { + X: { weight: 1, title: "Not Defined" }, + O: { weight: 0.95, title: "Official Fix" }, + T: { weight: 0.96, title: "Temporary Fix" }, + W: { weight: 0.97, title: "Workaround" }, + U: { weight: 1, title: "Unavailable" } + } + }, + RC: { + title: 'Report Confidence', + default:1, + values: { + X: { weight: 1, title: "Not Defined" }, + C: { weight: 1, title: "Confirmed" }, + R: { weight: 0.96, title: "Reasonable" }, + U: { weight: 0.92, title: "Unknown" } + } + }, + // Environmental Metric Group + CR: { + title: 'Confidentiality Requirement', + default:1, + values: { + X: { weight: 1, title: "Not Defined" }, + L: { weight: 0.5, title: "Low" }, + M: { weight: 1, title: "Medium" }, + H: { weight: 1.5, title: "High" } + } + }, + IR: { + title: 'Integrity Requirement', + default:1, + values: { + X: { weight: 1, title: "Not Defined" }, + L: { weight: 0.5, title: "Low" }, + M: { weight: 1, title: "Medium" }, + H: { weight: 1.5, title: "High" } + } + }, + AR: { + title: 'Availability Requirement', + default:1, + values: { + X: { weight: 1, title: "Not Defined" }, + L: { weight: 0.5, title: "Low" }, + M: { weight: 1, title: "Medium" }, + H: { weight: 1.5, title: "High" } + } + }, + // Modified Base Metrics + MAV: { + title: 'Modified Attack Vector', + default:0.85, + values: { + X: { weight: 0.85, title: "Not Defined" }, + N: { weight: 0.85, title: "Network" }, + A: { weight: 0.62, title: "Adjacent" }, + L: { weight: 0.55, title: "Local" }, + P: { weight: 0.2, title: "Physical" } + } + }, + MAC: { + title: 'Modified Attack Complexity', + default:0.77, + values: { + X: { weight: 0.77, title: "Not Defined"}, + L: { weight: 0.77, title: "Low" }, + H: { weight: 0.44, title: "High" } + } + }, + MPR: { + title: 'Modified Privileges Required', + default:0.85, + values: { + X: { weight: 0.85, title: "Not Defined" }, + N: { weight: 0.85, title: "None" }, + L: { weight: 0.62, title: "Low" }, + H: { weight: 0.27, title: "High" } + } + }, + MUI: { + title: 'Modified User Interaction', + default:0.85, + values: { + X: { weight: 0.85, title: "Not Defined" }, + N: { weight: 0.85, title: "None" }, + R: { weight: 0.62, title: "Required" } + } + }, + MS: { + title: 'Modified Scope', + default:6.42, + values: { + X: { weight: 6.42, title: "Not Defined" }, + U: { weight: 6.42, title: "Unchanged" }, + C: { weight: 7.52, title: "Changed" } + } + }, + MC: { + title: 'Modified Confidentiality', + default:0, + values: { + X: { weight: 0, title: "Not Defined" }, + N: { weight: 0, title: "None" }, + L: { weight: 0.22, title: "Low" }, + H: { weight: 0.56, title: "High" } + } + }, + MI: { + title: 'Modified Integrity', + default:0, + values: { + X: { weight: 0, title: "Not Defined" }, + N: { weight: 0, title: "None" }, + L: { weight: 0.22, title: "Low" }, + H: { weight: 0.56, title: "High" } + } + }, + MA: { + title: 'Modified Availability', + default:0, + values: { + X: { weight: 0, title: "Not Defined" }, + N: { weight: 0, title: "None" }, + L: { weight: 0.22, title: "Low" }, + H: { weight: 0.56, title: "High" } + } + } +}; + +const parse = (vector)=> { + if (!vector) { + return false; + } + + const metricsComponents = vector.split("/"); + const version = metricsComponents[0].includes("CVSS") ? metricsComponents[0].split(":")[1] : "Unknown"; + const startIndex = metricsComponents[0].includes("CVSS") ? 1 : 0; + + const parsedMetrics = {}; + for (let i = startIndex; i < metricsComponents.length; i++) { + const [metric, value] = metricsComponents[i].split(":"); + + if (metricWeights[metric] && metricWeights[metric]?.values?.[value]) { + parsedMetrics[metric] = {...metricWeights[metric].values?.[value]}; + parsedMetrics[metric].key = value + } + } + const cvss = { + metrics: parsedMetrics, + }; + // Adjust the PR weight if necessary + ['PR', 'MRP'].forEach((l)=> { + if (cvss.metrics?.[l] && cvss.metrics.S && cvss.metrics.S.key === 'C') { + // Adjust PR weight for 'Changed' scope + const prAdjustments = { 'N': 0.85, 'L': 0.68, 'H': 0.50 }; + const prValue = cvss.metrics[l].key; + if (prAdjustments[prValue]) { + cvss.metrics[l].weight = prAdjustments[prValue]; + } + } + }) + + // Ensure all required metrics are present + Object.keys(metricWeights).forEach((key) => { + cvss.metrics[key] = cvss.metrics[key] ? cvss.metrics[key] : { weight: 0, title: undefined, key:'X' }; + }); + + function getImpactScore() { + const ISCbase = 1 - (1 - cvss.metrics.C.weight) * (1 - cvss.metrics.I.weight) * (1 - cvss.metrics.A.weight); + return cvss.metrics.S.title === 'Changed' ? 7.52 * (ISCbase - 0.029) - 3.25 * Math.pow(ISCbase - 0.02, 15) : 6.42 * ISCbase; + } + + function getExploitabilityScore() { + return ( + 8.22 * + cvss.metrics.AV.weight * + cvss.metrics.AC.weight * + cvss.metrics.PR.weight * + cvss.metrics.UI.weight + ); + } + + + function getBaseScore() { + const ISC = getImpactScore(); + const ESC = getExploitabilityScore(); + + let baseScore = 0; + if (ISC > 0) { + if (cvss.metrics.S.title === 'Changed') + baseScore = Math.min(1.08 * (ISC + ESC), 10); + else + baseScore = Math.min(ISC + ESC, 10); + } + + return roundUp(baseScore); + } + + function getTemporalScore() { + const envMetricKeys = ['E', 'RL', 'RC'] + const temporalScoreAvailable = envMetricKeys.reduce((prev, cur)=> { + return cvss.metrics[cur].key !== 'X' || prev + }, false) + + if (!temporalScoreAvailable) { + return 0; + } + const E = (cvss.metrics.E.key !== 'X' ? cvss.metrics.E.weight : 1) ; + const RL = (cvss.metrics.RL.key !== 'X' ? cvss.metrics.RL.weight : 1); + const RC = (cvss.metrics.RC.key !== 'X' ? cvss.metrics.RC.weight : 1); + return roundUp(getBaseScore() * E * RL * RC); + } + + function getEnvironmentalScore() { + // First, handle 'X' values by setting them to the base score values + + const envMetricKeys = ['MC', 'MI', 'MA', 'MAV', 'MAC', 'MPR', 'MUI', 'CR', 'IR', 'AR'] + const environmentalScoreAvailable = envMetricKeys.reduce((prev, cur)=> { + return cvss.metrics[cur].key !== 'X' || prev + }, false) + + if (!environmentalScoreAvailable) { + return 0; + } + const MC = cvss.metrics.MC.key !== 'X' ? cvss.metrics.MC.weight : cvss.metrics.C.weight; + const MI = cvss.metrics.MI.key !== 'X' ? cvss.metrics.MI.weight : cvss.metrics.I.weight; + const MA = cvss.metrics.MA.key !== 'X' ? cvss.metrics.MA.weight : cvss.metrics.A.weight; + + const MAV = cvss.metrics.MAV.key !== 'X' ? cvss.metrics.MAV.weight : cvss.metrics.AV.weight; + const MAC = cvss.metrics.MAC.key !== 'X' ? cvss.metrics.MAC.weight : cvss.metrics.AC.weight; + const MPR = cvss.metrics.MPR.key !== 'X' ? cvss.metrics.MPR.weight : cvss.metrics.PR.weight; + const MUI = cvss.metrics.MUI.key !== 'X' ? cvss.metrics.MUI.weight : cvss.metrics.UI.weight; + + const CR = cvss.metrics.CR.key !== 'X' ? cvss.metrics.CR.weight : 1; + const IR = cvss.metrics.IR.key !== 'X' ? cvss.metrics.IR.weight : 1; + const AR = cvss.metrics.AR.key !== 'X' ? cvss.metrics.AR.weight : 1; + + // Now calculate the ISC Modified using the base or modified values as appropriate + const ISCmodified = Math.min(1 - (1 - MC * CR) * (1 - MI * IR) * (1 - MA * AR), 0.915); + + let mISC; + if (cvss.metrics.MS && cvss.metrics.MS.title === 'Changed') { + // Use the original formula for CVSS v3.0 + mISC = 7.52 * (ISCmodified - 0.029) - 3.25 * Math.pow(ISCmodified * 0.9731 - 0.02, 13); + } else { + mISC = 6.42 * ISCmodified; + } + + // Calculate Modified Exploitability Subscore + const mESC = 8.22 * MAV * MAC * MPR * MUI; + + let environmentalScore = 0; + if (mISC > 0) { + if (cvss.metrics.MS && cvss.metrics.MS.title === 'Changed') { + environmentalScore = roundUp(Math.min(1.08 * (mISC + mESC), 10)) * + (cvss.metrics.E.key !== 'X' ? cvss.metrics.E.weight : 1) * + (cvss.metrics.RL.key !== 'X' ? cvss.metrics.RL.weight : 1) * + (cvss.metrics.RC.key !== 'X' ? cvss.metrics.RC.weight : 1); + } else { + environmentalScore = roundUp(Math.min(mISC + mESC, 10)) * + (cvss.metrics.E.key !== 'X' ? cvss.metrics.E.weight : 1) * + (cvss.metrics.RL.key !== 'X' ? cvss.metrics.RL.weight : 1) * + (cvss.metrics.RC.key !== 'X' ? cvss.metrics.RC.weight : 1); + } + } + return roundUp(environmentalScore); + } + + const baseScore = roundUp(getBaseScore()); + const temporalScore = roundUp(getTemporalScore()); + const environmentalScore = roundUp(getEnvironmentalScore()) + let overallScore = baseScore; // Default to base score + + // If environmental score is not "NA" and not zero, use it + if (environmentalScore !== "NA" && environmentalScore !== 0) { + overallScore = environmentalScore; + } else if (temporalScore !== "NA" && temporalScore !== 0) { + overallScore = temporalScore; + } + return { + version: version, + vector: vector, + metrics: cvss.metrics, + impactScore: getImpactScore().toFixed(1), + metricWeights:metricWeights, + exploitabilityScore: getExploitabilityScore().toFixed(1), + baseScore: baseScore, + temporalScore: temporalScore, + environmentalScore: environmentalScore, + overallScore:overallScore, + }; +} + +module.exports = { + metricWeights:metricWeights, + parse:parse, +}; \ No newline at end of file diff --git a/report_shots/eslint.config.mjs b/report_shots/eslint.config.mjs new file mode 100644 index 0000000000..2eed6313a8 --- /dev/null +++ b/report_shots/eslint.config.mjs @@ -0,0 +1,27 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; + + + +export default [ + // Main configuration for JavaScript files + { + files: ["**/*.js"], + languageOptions: { + sourceType: "script", + globals: globals.node, + }, + rules: { + "no-unused-vars": "warn", // Explicitly set as warning + }, + }, + + // Ensure this part also has `no-unused-vars` set to "warn" + { + ...pluginJs.configs.recommended, + rules: { + ...pluginJs.configs.recommended.rules, + "no-unused-vars": "warn", // Override here as well + }, + }, +]; \ No newline at end of file diff --git a/report_shots/package.json b/report_shots/package.json index 0cce428000..f71f8a572b 100644 --- a/report_shots/package.json +++ b/report_shots/package.json @@ -1,6 +1,14 @@ { "dependencies": { + "@svgdotjs/svg.js": "^3.2.4", "js-yaml": "^4.1.0", - "puppeteer": "^14.1.1" + "sharp": "^0.33.5", + "svgdom": "^0.1.19", + "svgson": "^5.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "eslint": "^9.13.0", + "globals": "^15.11.0" } } diff --git a/report_shots/shots.js b/report_shots/shots.js index 5db4c70bab..5e455c5b23 100644 --- a/report_shots/shots.js +++ b/report_shots/shots.js @@ -1,46 +1,468 @@ -const puppeteer = require('puppeteer'); -const process = require('process'); const util = require('util'); const fsPromise = require('fs/promises'); const yaml = require('js-yaml') const fs = require('fs'); +const { parseJSON, parseCSVFormatV2, formatBytes } = require('./utils'); +const { convertVulnsData, vulnsColorScheme } = require('./vulnsParser'); +const sharp = require('sharp'); +const svgson = require('svgson'); +// Function to save SVG content to file +function saveSVGToFile(svgContent, imageSavePath) { + fs.writeFile(imageSavePath, svgContent, 'utf8', (err) => { + if (err) { + console.error('Error saving SVG file:', err); + } else { + console.log('SVG file successfully saved at:', imageSavePath); + } + }); +} + +// generate rect path with rounded top left and right corners +function createRoundedRectPath(x, y, width, height, radius) { + if (height < radius) { + radius = height; + } + return ` + M${x + radius},${y} + H${x + width - radius} + C${x + width},${y} ${x + width},${y} ${x + width},${y + radius} + V${y + height} + H${x} + V${y + radius} + C${x},${y} ${x},${y} ${x + radius},${y} + Z + `; +} + +const generateCharts = async (imageName, platform, imageSavePath) => { + const fetchDataRequest = async (path)=> { + let baseAPIUrl = '' + switch (platform) { + case 'pre-prod': + baseAPIUrl = 'https://frontrow-dev.rapidfort.io' + break; + case 'staging': + baseAPIUrl = 'https://frontrow.rapidfort.io' + break; + default: + baseAPIUrl = 'https://us01.rapidfort.com' + } + const result = await fetch(`${baseAPIUrl}${path}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + return await parseJSON(result); + } + + try { + // api/v1/community/imageinfo/?name=docker.io/library/redis + const jsonInfo = await fetchDataRequest(`/api/v1/community/imageinfo/?name=${imageName}`); + const imageInfoRaw = await fetchDataRequest(`/api/v1/community/images/?image_name=${imageName}`); + const imageInfo = parseCSVFormatV2({fields:imageInfoRaw?.fields, data:[imageInfoRaw?.image]})?.[0] + const vulns = await fetchDataRequest(jsonInfo?.vulns); + const vulnsHardened = await fetchDataRequest(jsonInfo?.vulns_hardened); + const {vulnsSeverityCount: vulnsHardenedSummary, hardenedVulnsFlags, } = convertVulnsData(vulnsHardened, true, true); + const {vulnsSeverityCount: vulnsOriginalSummary} = convertVulnsData(vulns, true, false, hardenedVulnsFlags); + + // generate SVGs + const vulnsSavingsChartSVG = await generateSavingsChart('Vulnerabilities', imageInfo.noVulns, imageInfo.noVulnsHardened, false); + const packagesSavingsChartSVG = await generateSavingsChart('Packages', imageInfo.noPkgs, imageInfo.noPkgsHardened, false); + const sizeSavingsChartSVG = await generateSavingsChart('Attack surface', imageInfo.origImageSize, imageInfo.hardenedImageSize, true); + const contextualSeverityChart = await generateContextualSeverityChart(vulnsOriginalSummary) + const vulnsBySeverityChart = await generateVulnsBySeverityChart(vulnsOriginalSummary.default, vulnsHardenedSummary.default); + + // save individual charts as svg + // saveSVGToFile(vulnsSavingsChartSVG, util.format('%s/savings_chart_vulns.svg', imageSavePath)); + // saveSVGToFile(packagesSavingsChartSVG, util.format('%s/savings_chart_pkgs.svg', imageSavePath)); + // saveSVGToFile(sizeSavingsChartSVG, util.format('%s/savings_chart_size.svg', imageSavePath)); + // saveSVGToFile(contextualSeverityChart, util.format('%s/contextual_severity_chart.svg', imageSavePath)); + // saveSVGToFile(vulnsBySeverityChart, util.format('%s/vulns_by_severity_histogram.svg', imageSavePath)); + generateReportViews(vulnsSavingsChartSVG, packagesSavingsChartSVG, sizeSavingsChartSVG, contextualSeverityChart, vulnsBySeverityChart, imageSavePath); + } catch (error) { + console.error(error); + } +} + +// Recursive function to find width and height in nested SVG tags +const findSVGDimensions = (node) => { + if (node.name === 'svg' && node.attributes.width && node.attributes.height) { + return { + width: parseFloat(node.attributes.width), + height: parseFloat(node.attributes.height), + }; + } + + for (const child of node.children || []) { + const dimensions = findSVGDimensions(child); + if (dimensions) return dimensions; + } + + return null; +}; + +const parseSVGDimensions = async (svgContent) => { + const svgJSON = await svgson.parse(svgContent); + const dimensions = findSVGDimensions(svgJSON); + return dimensions || { width: 0, height: 0 }; +}; + +const generateReportViews = async ( + vulnsSavingsChartSVG, + packagesSavingsChartSVG, + sizeSavingsChartSVG, + contextualSeverityChartSVG, + vulnsBySeverityChartSVG, + imageSavePath +) => { + const gap = 16; + let padding = 16; + + const svgFiles = [ + vulnsBySeverityChartSVG, + contextualSeverityChartSVG, + vulnsSavingsChartSVG, + packagesSavingsChartSVG, + sizeSavingsChartSVG, + ]; + // Extract and deduplicate style content + // Extract and deduplicate @font-face and other style content + const uniqueFontFaces = new Set(); + const otherStyles = new Set(); -async function takeShots(browser, imageSavePath, imageUrl, firstShot) { - const page = await browser.newPage(); + svgFiles.forEach((svgContent) => { + const styleMatch = svgContent.match(/]*>([\s\S]*?)<\/style>/); + if (styleMatch) { + const styleContent = styleMatch[1]; + + // Match and deduplicate @font-face + const fontFaceMatches = styleContent.match(/@font-face\s*{[^}]*}/g) || []; + fontFaceMatches.forEach(fontFace => uniqueFontFaces.add(fontFace)); - await page.setViewport({ - width: 5120, - height: 3840, - deviceScaleFactor: 1.25, + // Extract remaining styles + const nonFontFaceStyles = styleContent.replace(/@font-face\s*{[^}]*}/g, ""); + if (nonFontFaceStyles.trim()) { + otherStyles.add(nonFontFaceStyles.trim()); + } + } }); - await page.goto(imageUrl, { waitUntil: 'networkidle0' }); // wait until page load - await page.emulateMediaFeatures([{ - name: 'prefers-color-scheme', value: 'light' }]); + // Combine unique font faces and other styles + const combinedStyle = ` + + `; + + // Remove individual + + + + +20 +40 +0 + +Low + + + + +Medium + + + + +High + + + + +Critical + + + +Contextual severity + + diff --git a/report_shots/template_savings_chart.svg b/report_shots/template_savings_chart.svg new file mode 100644 index 0000000000..1411fca12d --- /dev/null +++ b/report_shots/template_savings_chart.svg @@ -0,0 +1,51 @@ + + + + + +Original image +53 +Hardened image +11 + + -42 + + + + + +Vulnerabilities +79% + + + + + + + + + + diff --git a/report_shots/template_vulns_by_severity.svg b/report_shots/template_vulns_by_severity.svg new file mode 100644 index 0000000000..a72897b005 --- /dev/null +++ b/report_shots/template_vulns_by_severity.svg @@ -0,0 +1,76 @@ + + + + +Vulnerabilities by severity +POC +Critical +High +Medium +Low + +Unknown +N/A + +0 +1 +1 +0 +1 +4 +28 + + + +0 +1 +1 +0 +1 +4 +28 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/report_shots/utils.js b/report_shots/utils.js new file mode 100644 index 0000000000..4aa2226e2f --- /dev/null +++ b/report_shots/utils.js @@ -0,0 +1,75 @@ +const parseJSON = (result) => { + return result.text().then(function(text) { + try { + return JSON.parse(text); + } catch { + const arr = text.split('\n'); + for (let i = 0; i < arr.length; i++) { + const line = arr[i] + try { + const lineJSON = JSON.parse(line); + if(typeof lineJSON === 'object') { + return lineJSON + } + } catch (error) { + console.log('error', error) + } + } + } + return text + }) +} + +function formatBytes (bytes, decimals = 2, forceUnit = null) { + if (bytes === 0) return '0 Bytes'; + + const k = 1000; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + let i; + if (forceUnit === null) { + i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k)); + } else { + i = sizes.indexOf(forceUnit); + } + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + + +function toCamelCase(str) { + return str + // First, lowercases the string to handle cases like 'Hello-World' + .toLowerCase() + // Then, replaces any non-alphanumeric character (or set of characters) followed by an alphanumeric character + .replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase()); +} + +const parseCSVFormatV2 = ({fields, data}, topLevel) => { + const reducer = (prev, cur, index) => { + if (Array.isArray(fields[index])) { + const [key, subheaders] = fields[index] + prev[toCamelCase(key)] = parseCSVFormatV2({fields:subheaders, data:cur}) + } else { + prev[toCamelCase(fields[index])] = cur + } + return prev + } + const mapper = (l) => { + return l?.reduce?.(reducer, {}) ?? {} + } + let result + if ((data?.length > 0 && Array.isArray(data[0]))) { + result = data?.map?.(mapper) + } else { + result = data?.reduce?.(reducer, {}) ?? topLevel ? [] : {} + } + return result +} + +module.exports = { + parseJSON, + parseCSVFormatV2, + formatBytes +}; diff --git a/report_shots/vulnsParser.js b/report_shots/vulnsParser.js new file mode 100644 index 0000000000..cee7519127 --- /dev/null +++ b/report_shots/vulnsParser.js @@ -0,0 +1,265 @@ +const { parse : parseCVSS } = require('./cvss-parser/cvss'); + + + +const vulnsColorScheme = { + exploited:'#C62A2F', + critical:'#DF1C41', + high:'#6E3FF3', + medium:'#F2AE40', + low:'#35B9E9', + unknown:'#8b8d98', + poc:'#C62A2F', + na:'#32D583', +} +const SEVERITY = { + CRITICAL: 'critical', + HIGH: 'high', + MEDIUM: 'medium', + LOW: 'low', + UNKNOWN: 'unknown' +} + + +const SEVERITY_DETAIL = { + [SEVERITY.CRITICAL]: { + id: SEVERITY.CRITICAL, + order: 0, + label: 'Critical', + color: vulnsColorScheme.critical + }, + [SEVERITY.HIGH]: { + id: SEVERITY.HIGH, + order: 1, + label: 'High', + color: vulnsColorScheme.high + }, + [SEVERITY.MEDIUM]: { + id: SEVERITY.MEDIUM, + order: 2, + label: 'Medium', + color: vulnsColorScheme.medium + }, + [SEVERITY.LOW]: { + id: SEVERITY.LOW, + order: 3, + label: 'Low', + color:vulnsColorScheme.low + }, + [SEVERITY.UNKNOWN]: { + id: SEVERITY.UNKNOWN, + order: 4, + label: 'Unknown', + color: vulnsColorScheme.unknown + }, +} + +const applyVectorModifiers = (vector, version, pocAvailable, execPath) => { + // Parse the vector string into an object + const vectorParams = vector.split('/').reduce((acc, param) => { + const [key, value] = param.split(':'); + acc[key] = value; + return acc; + }, {}); + + // Update the Exploit Code Maturity if pocAvailable is true + if (!pocAvailable) { + vectorParams.E = 'U'; + vectorParams.RL = version === 'V2' ? 'ND' : 'X'; + vectorParams.RC = version === 'V2' ? 'ND' : 'X'; + } + + // Update the Attack Complexity and User Interaction if execPath is true + if (!execPath && version === 'V3') { + vectorParams.MAC = 'H'; + vectorParams.MUI = 'R'; + } + + // Construct the updated vector string + const updatedVector = Object.entries(vectorParams) + .map(([key, value]) => `${key}:${value}`) + .join('/'); + + return updatedVector; +}; + + +function getSeverity(version, score) { + const ratings = { + 'V2': [ + { threshold: 0.0, label: 'UNKNOWN' }, + { threshold: 3.9, label: 'LOW' }, + { threshold: 6.9, label: 'MEDIUM' }, + { threshold: 10.0, label: 'HIGH' } + ], + 'V3': [ + { threshold: 0.0, label: 'UNKNOWN' }, + { threshold: 3.9, label: 'LOW' }, + { threshold: 6.9, label: 'MEDIUM' }, + { threshold: 8.9, label: 'HIGH' }, + { threshold: 10.0, label: 'CRITICAL' } + ] + }; + + const rating = ratings[version]; + if (!rating) return 'UNKNOWN'; + + for (let i = 0; i < rating.length; i++) { + if (score <= rating[i].threshold) { + return rating[i].label; + } + } +} + +const applyContextualCVSS = (item, type, imageHardened)=> { + const version = item[type]?.Version; + let vector = item[type].SeverityVector; + let score = item[type].SeverityScore; + let severity = item[type].Severity; + if (vector !== '-') { + vector = applyVectorModifiers(vector, version, item.RRS === 1, !imageHardened || item.hardened) + let parsedData = parseCVSS(vector, version); + score = parsedData; + const severity = getSeverity(version, parsedData.overallScore) ?? 'Unknown' + return { + Severity : (SEVERITY_DETAIL[severity.toLowerCase()]?.order ?? 0) > (SEVERITY_DETAIL[item[type]?.Severity.toLowerCase()]?.order) ? severity : item[type].Severity, + SeverityScore: parsedData.overallScore ?? score, + SeverityVector: vector, + Source: item[type].SeveritySource, + Version: version, + parsedData:parsedData, + } + } else { + return { + Severity: severity, + SeverityScore : score, + SeverityVector : vector, + Source: item[type].SeveritySource, + Version : version, + } + } +} + +const vulnsCountInfoObjTemplate = { + critical:0, + medium: 0, + high: 0, + low: 0, + unknown:0, + total : 0, + poc: 0, + na: 0 +} + +const updateSeverityInfo = (item, cvss, severity, severitySource, severityRef, imageHardened) => { + const nvdScale = cvss?.nvd?.V3Score ? 'V3' : cvss?.nvd?.V2Score ? 'V2' : severityRef?.nvd?.V3Severity ? 'V3' : severityRef?.nvd?.V2Severity ? 'V2' : '-'; + const defaultScale = cvss?.[severitySource]?.V3Score ? 'V3' : cvss?.[severitySource]?.V2Score ? 'V2' : severityRef?.[severitySource]?.V3Severity ? 'V3' : severityRef?.[severitySource]?.V2Severity ? 'V2' : '-';; + item.nvd = { + Severity : severityRef?.nvd?.[`${nvdScale}Severity`] ?? 'UNKNOWN', + SeverityScore : (parseFloat(cvss?.nvd?.[`${nvdScale}Score`]) > 0 ? cvss?.nvd?.[`${nvdScale}Score`] : '-') ?? '-', + SeverityVector : cvss?.nvd?.[`${nvdScale}Vector`] ?? '-', + Source:'NVD', + Version : nvdScale, + } + + item.default = { + Severity : severity ?? '', + } + item.default = { + Severity : severity, + Source:severitySource, + SeverityScore : (parseFloat(cvss?.[severitySource]?.[`${defaultScale}Score`]) > 0 ? cvss?.[severitySource]?.[`${defaultScale}Score`] : '-') ?? '-', + SeverityVector : cvss?.[severitySource]?.[`${defaultScale}Vector`] ?? '-', + Version : defaultScale, + } + + item.rfcvss = {} + + item.rfcvss_nvd = applyContextualCVSS(item, 'nvd', imageHardened) + if (severitySource.toLowerCase() === 'nvd') { + item.rfcvss_default = item.rfcvss_nvd + } else { + item.rfcvss_default = applyContextualCVSS(item, 'default', imageHardened) + } + + if (!cvss) { + if (cvss?.[severitySource]?.V3Score) { + item.default.Version = 'V3' + item.default.SeverityScore = cvss?.[severitySource]?.V3Score + } else if (cvss?.[severitySource]?.V2Score) { + item.default.Version = 'V2' + item.default.SeverityScore = cvss?.[severitySource]?.V2Score + } else { + item.default.Version = '-' + item.default.SeverityScore = '-' + } + } +} + +const convertVulnsData = (data, imageHardened, isHardened, flags) => { + let vulnsSeverityCount = {default: {...vulnsCountInfoObjTemplate}, nvd:{...vulnsCountInfoObjTemplate}, rfcvss_nvd:{...vulnsCountInfoObjTemplate}, rfcvss_default:{...vulnsCountInfoObjTemplate}} + const seenVulns = new Map(); + const seenNotApplicableVulns = new Map(); + const hardenedVulnsFlags = {} + let appKeyIndex = 0; + data?.forEach?.((cur, index) => { + cur?.Vulnerabilities?.forEach?.((item)=> { + let appKey = appKeyIndex++; + if ((cur.Class && cur.Class === 'os-pkgs') || (!cur.Class && index === 0)) { + appKey = cur.Type; + } + if (isHardened) { + item.hardened = true; + hardenedVulnsFlags[`i:${item.VulnerabilityID}|v${item.InstalledVersion}|p${item.PkgName}`] = true + } else { + item.hardened = flags[`i:${item.VulnerabilityID}|v${item.InstalledVersion}|p${item.PkgName}`] + } + item.applicable = item.RFJustification?.status === 'na' ? false : true; + item.Severity = (item.Severity) ? item.Severity : 'UNKNOWN' + updateSeverityInfo(item, item.CVSS, item.Severity, item.SeveritySource, item.SeverityRef, imageHardened) + const key = `${item.VulnerabilityID}_${item?.SourcePkgName ?? item?.PkgName}_${item.PkgSource}_${item.SourceVersion}_${appKey}`; + let isFirstOccurency = false; + if (item.applicable) { + if (!seenVulns.has(key)) { + isFirstOccurency = true + seenVulns.set(key, item); + } + } else { + if (!seenNotApplicableVulns.has(key)) { + isFirstOccurency = true + item.Packages = [item.PkgName] + seenNotApplicableVulns.set(key, item); + } + } + const advisories = ['default', 'rfcvss_default']; + // const advisories = ['default', 'rfcvss_default', 'nvd', 'rfcvss_nvd']; + advisories.forEach(advisory => { + const severitykey = item[advisory]?.Severity.toLowerCase() ?? 'unknown' + if (item.applicable) { + if (isFirstOccurency) { + vulnsSeverityCount[advisory][severitykey]++; + if (item.RRS >= 1) { + vulnsSeverityCount[advisory].poc++; + } + vulnsSeverityCount[advisory].total++ + } + } else { + if (isFirstOccurency) { + vulnsSeverityCount[advisory].na++; + } + } + }) + }) + }) + return { + hardenedVulnsFlags, + vulnsSeverityCount + } +} + + +module.exports = { + applyContextualCVSS, + convertVulnsData, + vulnsColorScheme, +}; \ No newline at end of file diff --git a/scripts/generate_screenshots.sh b/scripts/generate_screenshots.sh index b61b48605e..c7ab1dfd43 100755 --- a/scripts/generate_screenshots.sh +++ b/scripts/generate_screenshots.sh @@ -7,7 +7,7 @@ SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" sudo apt-get install -y curl -curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - +curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs