From 55d6eff2f904862beff3673e0f7757ab448aefd3 Mon Sep 17 00:00:00 2001 From: Oleg-RapidFort Date: Tue, 5 Nov 2024 21:42:58 +0200 Subject: [PATCH] - added generating readme's with using new graphics (#890) Signed-off-by: oleg_wyndrick --- .../common/templates/image_readme.j2 | 51 ++-- contrib/full_report.svg | 52 ++++ contrib/logo_light.svg | 14 + contrib/view_details.svg | 35 +++ contrib/view_dockerhub.svg | 45 +++ contrib/view_github.svg | 36 +++ contrib/zero_cve_images_link.svg | 244 +++++++++++++++ report_shots/package.json | 3 +- report_shots/shots.js | 280 +++++++++++++++++- .../template_original_vs_hardened.svg | 53 ++++ report_shots/template_savings_chart.svg | 1 + report_shots/template_savings_view.svg | 77 +++++ report_shots/template_vulns_count.svg | 90 ++++++ report_shots/utils.js | 27 +- 14 files changed, 976 insertions(+), 32 deletions(-) create mode 100644 contrib/full_report.svg create mode 100644 contrib/logo_light.svg create mode 100644 contrib/view_details.svg create mode 100644 contrib/view_dockerhub.svg create mode 100644 contrib/view_github.svg create mode 100644 contrib/zero_cve_images_link.svg create mode 100644 report_shots/template_original_vs_hardened.svg create mode 100644 report_shots/template_savings_view.svg create mode 100644 report_shots/template_vulns_count.svg diff --git a/community_images/common/templates/image_readme.j2 b/community_images/common/templates/image_readme.j2 index e42176ea68..b85771227d 100644 --- a/community_images/common/templates/image_readme.j2 +++ b/community_images/common/templates/image_readme.j2 @@ -1,18 +1,20 @@ -RapidFort +RapidFort
+ [![rf-h][rf-h-badge]][rf-view-report-button] [![DH Image][dh-rf-badge]][rf-dh-image-link] [![Slack][slack-badge]][slack-link] [![FOSSA Status][fossa-badge]][fossa-link] -Near Zero CVE images available at hub.rapidfort.com/repositories. + +Zero cve images + +
-⚠️ CRITICAL NOTICE
-As of 7/2024 community-images will be gated. Please register for free at www.rapidfort.com to access these images # RapidFort hardened image for {{ official_name }} @@ -24,26 +26,33 @@ RapidFort has optimized and hardened this {{ official_name }} container image. T This optimized image is functionally equivalent to [{{- source_image_provider }} {{ official_name -}}][source-image-repo-link] image but more secure with a significantly smaller software attack surface. -Every day, RapidFort automatically optimizes and hardens a growing bank of Docker Hub’s most important container images. Check out our [entire library](https://hub.docker.com/u/rapidfort) of secured container images. -
-[Get the full report here or click on the image below][rf-view-report-link] +

+ + Vulnerabilities by severity + + + Original vs. this image + +

-[![Metrics][metrics-link]][rf-image-metrics-link] -

Vulnerabilities: Original vs. Hardened - -

- -[![CVE Reduction][cve-reduction-link]][rf-image-cve-reduction-link] +[![Savings][savings-link]][rf-image-savings-link] -View Report +View Report

+Every day, RapidFort automatically optimizes and hardens a growing bank of Docker Hub’s most important container images. + +Check out our [entire library of secured container images.](https://hub.docker.com/u/rapidfort) +
+ +[Get the full report here or click on the image below][rf-view-report-link] + ## What is {{ official_name }}? > {{ what_is_text }} @@ -69,7 +78,7 @@ docker run -e RF_ACCESS_TOKEN="your_access_token" image_name The runtime instructions for this hardened container image are the same as the official release. Follow the instructions provided with the [{{- source_image_provider }} {{ official_name -}}][source-image-repo-link]. -View Detailed Instructions +View Detailed Instructions

@@ -89,7 +98,7 @@ RapidFort is the pioneering Software Attack Surface Management (SASM) platform i Vulnerability reports for RapidFort's hardened images are updated daily to include newly discovered vulnerabilities and fixes. -View on GitHub +View on GitHub

@@ -123,7 +132,7 @@ If you find this project useful, please star this repo just like many [amazing p ## Have questions? -[![RapidFort](https://raw.githubusercontent.com/rapidfort/community-images/main/contrib/github_logo_footer.png)][rf-rapidfort-footer-logo-link] +[![RapidFort](https://raw.githubusercontent.com/rapidfort/community-images/main/contrib/logo_light.svg)][rf-rapidfort-footer-logo-link] Learn more about RapidFort's pioneering Software Attack Surface Management platform at [RapidFort.com][rf-link]. @@ -142,9 +151,13 @@ Learn more about RapidFort's pioneering Software Attack Surface Management platf [rf-rapidfort-footer-logo-link]: {{ report_url -}}?utm_source=github&utm_medium=ci_view_report&utm_campaign=sep_01_sprint&utm_term={{- name -}}&utm_content=rapidfort_footer_logo [rf-view-report-button]: {{ report_url -}}?utm_source=github&utm_medium=ci_view_report&utm_campaign=sep_01_sprint&utm_term={{- name -}}&utm_content=view_report_button [rf-view-report-link]: {{ report_url -}}?utm_source=github&utm_medium=ci_view_report&utm_campaign=sep_01_sprint&utm_term={{- name -}}&utm_content=view_report_link + [rf-image-metrics-link]: {{ report_url -}}?utm_source=github&utm_medium=ci_view_report&utm_campaign=sep_01_sprint&utm_term={{- name -}}&utm_content=image_metrics_link [rf-image-cve-reduction-link]: {{ report_url -}}?utm_source=github&utm_medium=ci_view_report&utm_campaign=sep_01_sprint&utm_term={{- name -}}&utm_content=image_cve_reduction_link +[rf-image-savings-link]: {{ report_url -}}?utm_source=github&utm_medium=ci_view_report&utm_campaign=sep_01_sprint&utm_term={{- name -}}&utm_content=image_savings_link +[rf-image-vulns-link]: {{ report_url -}}?utm_source=github&utm_medium=ci_view_report&utm_campaign=sep_01_sprint&utm_term={{- name -}}&utm_content=vulns_link + [dh-img-size-badge]: https://img.shields.io/docker/image-size/ {{- rf_docker_link -}} ?logo=docker&logoColor=white&sort=semver [dh-img-pulls-badge]: https://img.shields.io/docker/pulls/ {{- rf_docker_link -}} ?logo=docker&logoColor=white @@ -152,8 +165,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.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 }} + +[savings-link]: https://github.com/rapidfort/community-images/raw/main/community_images/ {{- github_location -}} /assets/savings.svg diff --git a/contrib/full_report.svg b/contrib/full_report.svg new file mode 100644 index 0000000000..258461aee1 --- /dev/null +++ b/contrib/full_report.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contrib/logo_light.svg b/contrib/logo_light.svg new file mode 100644 index 0000000000..7af307a9ec --- /dev/null +++ b/contrib/logo_light.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/contrib/view_details.svg b/contrib/view_details.svg new file mode 100644 index 0000000000..8c80b0ee65 --- /dev/null +++ b/contrib/view_details.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contrib/view_dockerhub.svg b/contrib/view_dockerhub.svg new file mode 100644 index 0000000000..d8efd93178 --- /dev/null +++ b/contrib/view_dockerhub.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contrib/view_github.svg b/contrib/view_github.svg new file mode 100644 index 0000000000..67f9c737c9 --- /dev/null +++ b/contrib/view_github.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contrib/zero_cve_images_link.svg b/contrib/zero_cve_images_link.svg new file mode 100644 index 0000000000..655fc26f18 --- /dev/null +++ b/contrib/zero_cve_images_link.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/report_shots/package.json b/report_shots/package.json index f71f8a572b..0464bbdf0f 100644 --- a/report_shots/package.json +++ b/report_shots/package.json @@ -4,7 +4,8 @@ "js-yaml": "^4.1.0", "sharp": "^0.33.5", "svgdom": "^0.1.19", - "svgson": "^5.3.1" + "svgson": "^5.3.1", + "xmldom": "^0.6.0" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/report_shots/shots.js b/report_shots/shots.js index 7a34a9b033..3812b80d44 100644 --- a/report_shots/shots.js +++ b/report_shots/shots.js @@ -2,7 +2,7 @@ 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 { parseJSON, parseCSVFormatV2, formatBytes, capitalizeFirstLetter, formatSizeString } = require('./utils'); const { convertVulnsData, vulnsColorScheme } = require('./vulnsParser'); const sharp = require('sharp'); const svgson = require('svgson'); @@ -66,20 +66,55 @@ const generateCharts = async (imageName, platform, imageSavePath) => { const {vulnsSeverityCount: vulnsHardenedSummary, hardenedVulnsFlags, } = convertVulnsData(vulnsHardened, true, true); const {vulnsSeverityCount: vulnsOriginalSummary} = convertVulnsData(vulns, true, false, hardenedVulnsFlags); + Object.keys(vulnsOriginalSummary.default).forEach((severity) => { + vulnsOriginalSummary.default[severity] = vulnsOriginalSummary.default[severity] * 100000; + }); + // 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); + // 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); + const {width, svg:vulnsCountChartSVG} = await generateVulnsCountChart(vulnsOriginalSummary.default); + const vulnsOriginalHardenedChartSVG = await generateVulnsOriginalHardenedChart(width, vulnsOriginalSummary.default, vulnsHardenedSummary.default); + + saveSVGToFile(vulnsCountChartSVG, util.format('%s/vulns_count_chart.svg', imageSavePath)); + saveSVGToFile(vulnsOriginalHardenedChartSVG, util.format('%s/original_vs_hardened_vulns_chart.svg', imageSavePath)); + const savingsSVG = await generateSavingsCardsCompound([ + { + type:'vulns', + title:'Vulnerabilities', + original: imageInfo.noVulns, + hardened:imageInfo.noVulnsHardened, + isSize:false, + }, + { + type:'packages', + title:'Packages', + original: imageInfo.noPkgs, + hardened:imageInfo.noPkgsHardened, + isSize:false, + }, + { + type:'size', + title:'Size', + original: imageInfo.origImageSize, + hardened:imageInfo.hardenedImageSize, + isSize:true, + } + ]); + saveSVGToFile(savingsSVG, util.format('%s/savings.svg', imageSavePath)); + // 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); + // generateReportViews(vulnsSavingsChartSVG, packagesSavingsChartSVG, sizeSavingsChartSVG, contextualSeverityChart, vulnsBySeverityChart, imageSavePath); + } catch (error) { console.error(error); } @@ -213,22 +248,45 @@ const generateReportViews = async ( }; + + async function readSVGTemplate (filename) { // reading svg template return await fsPromise.readFile(`./${filename}.svg`, { encoding: 'utf8' }); } // helper function for initiating svg modifying -async function prepareSVG (filename) { +async function prepareSVG(filename) { const { SVG, registerWindow } = await import('@svgdotjs/svg.js'); const { createSVGWindow } = await import('svgdom'); const svgTemplate = await readSVGTemplate(filename); + + // Extract attributes from the tag in the template + const svgTagMatch = svgTemplate.match(/]+)>/); + const attributes = {}; + if (svgTagMatch) { + const attrString = svgTagMatch[1]; + attrString.replace(/(\w+)=["']([^"']*)["']/g, (match, name, value) => { + attributes[name] = value; + }); + } + + // Remove the outer tags from the template content + const innerContent = svgTemplate.replace(/]*>|<\/svg>/g, ''); + + // Set up a window and document const window = createSVGWindow(); const document = window.document; registerWindow(window, document); + + // Initialize SVG and add only the inner content const draw = SVG(document.documentElement); - draw.svg(svgTemplate); - return draw + draw.svg(innerContent); // Add only the inner content to `draw` + + // Apply extracted attributes to the new element + Object.keys(attributes).forEach(attr => draw.attr(attr, attributes[attr])); + + return draw; } // helper function for updating text in tspan of text element @@ -465,6 +523,207 @@ async function generateContextualSeverityChart(vulnsOriginalSummary) { // return svg as string return draw.svg(); } +/** + * Draws a chart in an SVG based on the provided data. + * + * @param {Object} draw - SVG.js object. + * @param {string} groupId - ID of the group where the chart will be added. + * @param {number} maxWidth - Maximum width of the chart. + * @param {Object} vulnsCount - Object with vulnerability counts by severity. + * @param {number} total - Total value for filling the chart to 100%. + */ +function drawVulnsChart(draw, groupId, maxWidth, vulnsCount, total) { + const severities = ['critical', 'high', 'medium', 'low', 'unknown']; + + const minWidth = 5; + const spacing = 2; + + // Calculate initial widths for each severity level + let initialWidths = severities.map(severity => ({ + severity, + color: vulnsColorScheme[severity], + value: vulnsCount[severity], + width: (vulnsCount[severity] / total) * (maxWidth - spacing * (severities.length - 1)) + })); + + // Adjust widths to meet minimum width requirements and calculate the excess + let totalExcess = 0; + initialWidths.forEach(item => { + if (item.width < minWidth) { + totalExcess += minWidth - item.width; + item.width = minWidth; + } + }); + + + // Proportionally distribute the remaining width + let remainingRects = initialWidths.filter(item => item.width > minWidth); + let remainingWidth = maxWidth - initialWidths.reduce((acc, item) => acc + item.width, 0) - (spacing * (severities.length - 1)); + + if (remainingWidth > 0) { + remainingRects.forEach(item => { + item.width += (item.width / remainingRects.reduce((acc, r) => acc + r.width, 0)) * remainingWidth; + }); + } + + // Find the group by ID and clear any existing elements + const group = draw.findOne(`#${groupId}`); + group.clear(); + + // Draw rectangles + let currentX = 0; + initialWidths.forEach(({ color, width }) => { + group.rect(width, 8) + .attr({ x: currentX, y: 0, fill: color }); + currentX += width + spacing; + }); +} + + +/** + * + * @param {*} vulnsCount + * @returns + */ +async function generateVulnsCountChart(vulnsCount) { + // Preparing data for chart + const severities = ['critical', 'high', 'medium', 'low', 'unknown']; + const draw = await prepareSVG('template_vulns_count'); + + const margin = 10; + const initialSize = 92; + const maxLegendWidth = severities.reduce((prev, severity)=> { + // Calculate width of reduction_value text for setting it centered + const reductionText = draw.text(`${capitalizeFirstLetter(severity)}: ${vulnsCount[severity]}`).font({ size: 12, family: 'InterMedium' }); + const reductionWidth = reductionText.bbox().width; + reductionText.remove(); // remove text after measure + return Math.max(reductionWidth + 14 + margin, prev) + }, 0) + + const offsetMultiplier = [0, 1, 2, 0, 1] + severities.forEach((severity, index) => { + updateText(draw, `#text_${severity}`, vulnsCount[severity]); + const legendGroup = draw.findOne(`#legend_${severity}`); + legendGroup.attr('transform', `translate(${(maxLegendWidth - initialSize) * offsetMultiplier[index]}, 0)`) + }) + const initialContentWidth = 316; + // Prepare SVG + const contentWidth = Math.max(maxLegendWidth * 3, 316); + draw.findOne("#vulns_count_mask").attr('width', contentWidth) + draw.findOne("#external-link").attr('transform', `translate(${contentWidth - initialContentWidth}, 0)`) + draw.findOne("#vulns_count_mask_rect").attr('width', contentWidth) + console.log('contentWidth', contentWidth) + drawVulnsChart(draw, 'vulns_count_view', contentWidth, vulnsCount, vulnsCount.total); + // Update the total count text + updateText(draw, '#vulns_total', vulnsCount.total); + + draw.width(contentWidth + 48); + const currentViewBox = draw.attr('viewBox') || '0 0 100 100'; + const [x, y, , originalHeight] = currentViewBox.split(' ').map(Number); + const newWidth = contentWidth + 48; + const updatedViewBox = `${x} ${y} ${newWidth} ${originalHeight}`; + draw.attr('viewBox', updatedViewBox); + + // return svg as string + return {width:newWidth, svg: draw.svg()} +} + + +/** + * + * @param {*} vulnsCount + * @returns + */ +async function generateVulnsOriginalHardenedChart(cardWidth, original, hardened) { + // Preparing data for chart + const severities = ['critical', 'high', 'medium', 'low', 'unknown']; + const data = { + original: original, + hardened: hardened + } + const draw = await prepareSVG('template_original_vs_hardened'); + const originalValueText = draw.text(`${original.total}`).font({ size: 14, family: 'InterMedium' }); + const originalValueTextWidth = originalValueText.bbox().width; + originalValueText.remove(); // remove text after measure + const initialContentWidth = 365; + const overflowWidth = parseInt(cardWidth) - (337 + originalValueTextWidth); + console.log('originalValueTextWidth', originalValueTextWidth) + console.log('overflowWidth', overflowWidth) + console.log('cardWidth', cardWidth) + draw.findOne("#external-link").attr('transform', `translate(${cardWidth - initialContentWidth}, 0)`) + draw.findOne(`#original_value_tspan`).attr('x', cardWidth - originalValueTextWidth - 24) + draw.findOne(`#original_mask`).attr('width', cardWidth - originalValueTextWidth - 24 - 10 - 106) + draw.findOne(`#original_mask_rect`).attr('width', cardWidth - originalValueTextWidth - 24 - 10 - 106) + + Object.entries(data).forEach(([key, vulnsCount]) => { + if (original.total === 0) { + return + } + let contentWidth = cardWidth - originalValueTextWidth - 24 - 10 - 106; + if (key === 'hardened') { + const minWidth = severities.reduce((prev, severity) => { + return prev + (vulnsCount[severity] > 0 ? 7 : 0) + }, 0) + contentWidth = Math.max(vulnsCount.total / original.total * 197, minWidth); + draw.findOne(`#hardened_mask`).attr('width', contentWidth) + draw.findOne(`#hardened_mask_rect`).attr('width', contentWidth) + draw.findOne(`#hardened_value_tspan`).attr('x', 106 + contentWidth + 10) + } + drawVulnsChart(draw, `${key}_count_view`, contentWidth, vulnsCount, vulnsCount.total); + updateText(draw, `#${key}_value`, vulnsCount.total); + }) + + draw.width(cardWidth); + const currentViewBox = draw.attr('viewBox'); + const [x, y, , originalHeight] = currentViewBox.split(' ').map(Number); + const updatedViewBox = `${x} ${y} ${cardWidth} ${originalHeight}`; + draw.attr('viewBox', updatedViewBox); + + return draw.svg() +} + +// Generating chart savings chart (vulns, packages, size) +/** + * + * @param {String} title chart title + * @param {Number} original original value + * @param {Number} hardened hardened value + * @param {Boolean} isSize if size then need to show values with metrics suffix + * @returns + */ +async function generateSavingsCardsCompound(savingsData) { + const draw = await prepareSVG('template_savings_view') + savingsData.forEach(async ({type, title, original, hardened, isSize}) => { + const percentage = (1 - (hardened / original)) * 100; + if (isSize) { + original = formatSizeString(formatBytes(original, 2)); + const unit = original.split(' ')[1]; + hardened = formatSizeString(formatBytes(hardened, 2, unit)); + } + + // update text fields in svg + updateText(draw, `#${type}_title`, title); + updateText(draw, `#${type}_original_value`, original.toString()); + updateText(draw, `#${type}_hardened_value`, hardened.toString()); + updateText(draw, `#${type}_percentage`, `${Math.round(-percentage)}%`); + + // calculate dash array and dash offset for progress line in svg + const calculateDasharray = (r) => Math.PI * r * 2; + const calculateDashoffset = (percentageShown, circumference) => ((100 - percentageShown) / 100) * circumference; + const dashArray = calculateDasharray(33.5); + const dashOffset = calculateDashoffset(100 - percentage, dashArray); + // set value for circle as progress bar + const progressCircle = draw.findOne(`#${type}_progress`); + + progressCircle.attr({ + 'stroke-dasharray': dashArray, + 'stroke-dashoffset': dashOffset, + }); + + // return back SVG as string + }); + return draw.svg(); +} async function main() { const imgListPath = process.argv[2] @@ -474,7 +733,6 @@ async function main() { const imgListArray = imgList.split("\n"); for await (const imagePath of imgListArray) { - console.log("image name=", imagePath); try { let imageYmlPath = fs.realpathSync(util.format('../community_images/%s/image.yml', imagePath)); diff --git a/report_shots/template_original_vs_hardened.svg b/report_shots/template_original_vs_hardened.svg new file mode 100644 index 0000000000..3b6ead3f6e --- /dev/null +++ b/report_shots/template_original_vs_hardened.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + Original vs. this image + + + This image + 24 + + + + + + + + + Original + 699 + + + + + + + + + diff --git a/report_shots/template_savings_chart.svg b/report_shots/template_savings_chart.svg index 1416271f2d..8a6893c513 100644 --- a/report_shots/template_savings_chart.svg +++ b/report_shots/template_savings_chart.svg @@ -8,6 +8,7 @@ } #progress { transform: rotate(-90deg); + transform-origin: 272.425px 69.988px; } @font-face { font-family: 'InterMedium'; diff --git a/report_shots/template_savings_view.svg b/report_shots/template_savings_view.svg new file mode 100644 index 0000000000..86feecad50 --- /dev/null +++ b/report_shots/template_savings_view.svg @@ -0,0 +1,77 @@ + + + + + + + Savings + + + + + + +Vulnerabilities + + +-78% +This image +Original +174 +699 + + +Packages + + +-78% +This image +Original +174 +699 + + +Size + + +-78% +This image +Original +174 +699 + + + + diff --git a/report_shots/template_vulns_count.svg b/report_shots/template_vulns_count.svg new file mode 100644 index 0000000000..261ee7bb39 --- /dev/null +++ b/report_shots/template_vulns_count.svg @@ -0,0 +1,90 @@ + + + + + + +Vulnerabilities + + + + + + + 699 + + + + + + + + + + + + + +Critical: +6 + + + + + + + +High: +57 + + + + + + + +Medium: +102 + + + + + + + +Low: +529 + + + + + + + +Unknown: +5 + + + + + + diff --git a/report_shots/utils.js b/report_shots/utils.js index 4aa2226e2f..b65faa2fde 100644 --- a/report_shots/utils.js +++ b/report_shots/utils.js @@ -67,9 +67,34 @@ const parseCSVFormatV2 = ({fields, data}, topLevel) => { } return result } + +const capitalizeFirstLetter = (string) => { + if (!string) { + return string + } + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function formatSizeString(sizeString) { + const [size, unit] = sizeString.split(' '); + let number = parseFloat(size); + + // Determine the number of significant digits and format accordingly + const digitCount = Math.floor(Math.log10(number)) + 1; + + // Format based on the number of digits + const formattedSize = digitCount >= 3 + ? Math.floor(number) // Show as an integer if 3 or more digits + : parseFloat(number.toPrecision(3)); // Show up to 3 significant digits + + // Return the formatted size with the unit (no space before the unit) + return `${formattedSize}${unit}`; +} module.exports = { parseJSON, parseCSVFormatV2, - formatBytes + formatBytes, + capitalizeFirstLetter, + formatSizeString };