From d769f3878c154b462b042d3da5382fc6660d6536 Mon Sep 17 00:00:00 2001 From: nickviola Date: Thu, 27 Jun 2024 14:09:09 -0500 Subject: [PATCH] Add 508 compliance with aria description and patters to pie charts --- frontend/src/pages/Risk/Risk.tsx | 12 +- .../src/pages/Risk/VulnerabilityPieChart.tsx | 588 ++++++++++++------ frontend/src/pages/Risk/utils.ts | 24 +- 3 files changed, 429 insertions(+), 195 deletions(-) diff --git a/frontend/src/pages/Risk/Risk.tsx b/frontend/src/pages/Risk/Risk.tsx index d26cda36..e490a31b 100644 --- a/frontend/src/pages/Risk/Risk.tsx +++ b/frontend/src/pages/Risk/Risk.tsx @@ -6,7 +6,12 @@ import TopVulnerablePorts from './TopVulnerablePorts'; import TopVulnerableDomains from './TopVulnerableDomains'; import VulnerabilityPieChart from './VulnerabilityPieChart'; import * as RiskStyles from './style'; -import { getSeverityColor, offsets, severities } from './utils'; +import { + getSeverityColor, + getServicesColor, + offsets, + severities +} from './utils'; import { useAuthContext } from 'context'; import { geoCentroid } from 'd3-geo'; import { @@ -75,7 +80,7 @@ const Risk: React.FC = (props) => { const geoStateUrl = 'https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json'; - const allColors = ['rgb(0, 111, 162)', 'rgb(0, 185, 227)']; + // const allColors = ['rgb(0, 111, 162)', 'rgb(0, 185, 227)']; const fetchStats = useCallback( async (orgId?: string) => { @@ -272,7 +277,8 @@ const Risk: React.FC = (props) => { )} diff --git a/frontend/src/pages/Risk/VulnerabilityPieChart.tsx b/frontend/src/pages/Risk/VulnerabilityPieChart.tsx index cc39f517..fde01ccf 100644 --- a/frontend/src/pages/Risk/VulnerabilityPieChart.tsx +++ b/frontend/src/pages/Risk/VulnerabilityPieChart.tsx @@ -1,16 +1,291 @@ -import React from 'react'; +// import React from 'react'; +// import { useHistory } from 'react-router-dom'; +// import { ResponsivePie } from '@nivo/pie'; +// import { animated } from '@react-spring/web'; +// import { Paper } from '@mui/material'; +// import { Point } from './Risk'; +// import * as RiskStyles from './style'; + +// const patterns = [ +// { id: 'dots', type: 'patternDots', size: 3, padding: 2, stagger: false }, +// { +// id: 'squares', +// type: 'patternSquares', +// size: 3, +// padding: 3, +// stagger: false +// }, +// { +// id: 'lines', +// type: 'patternLines', +// rotation: -36, +// lineWidth: 2, +// spacing: 10 +// }, +// { +// id: 'dots-alternate', +// type: 'patternDots', +// size: 2, +// padding: 6, +// stagger: true +// }, +// { +// id: 'lines-alternate', +// type: 'patternLines', +// rotation: 25, +// lineWidth: 6, +// spacing: 12 +// }, +// { +// id: 'big-squares', +// type: 'patternSquares', +// size: 4, +// padding: 1, +// stagger: true +// } +// // Add more patterns if needed +// ]; + +// const generatePatternsAndFills = (data: any[]) => { +// return data.map((point: { id: any }, index: number) => { +// const pattern = patterns[index % patterns.length]; +// const patternId = `${pattern.id}-${point.id}`; +// return { +// pattern: { +// ...pattern, +// background: 'inherit', +// color: 'rgba(0, 0, 0, 0.25)', +// id: patternId +// }, +// fill: { +// match: { id: point.id }, +// id: patternId +// } +// }; +// }); +// }; + +// const VulnerabilityPieChart = (props: { +// title: string; +// data: Point[]; +// colors: any; +// type: string; +// }) => { +// const history = useHistory(); +// const { title, data, colors, type } = props; +// const { cardRoot, cardSmall, header, chartSmall } = RiskStyles.classesRisk; + +// const ariaLabel = +// `${title} pie chart. ` + +// data +// .map( +// (point) => +// `${point.id}: ${point.value} (${( +// (point.value / data.reduce((acc, curr) => acc + curr.value, 0)) * +// 100 +// ).toFixed(2)}%)` +// ) +// .join(', '); +// console.log('Data: ', data); +// const patternsAndFills = generatePatternsAndFills(data); +// console.log('Paterns and fills: ', patternsAndFills); +// const defs = patternsAndFills.map((pf: { pattern: any }) => pf.pattern); +// console.log('Defs: ', defs); +// const fill = patternsAndFills.map((pf: any) => pf.fill); +// console.log('Fill: ', fill); + +// return ( +// +//
+//
+//

{title}

+//
+//
+// { +// if (type === 'vulns') { +// history.push(`/inventory/vulnerabilities?severity=${event.id}`); +// } +// }} +// arcLinkLabelsSkipAngle={10} +// arcLinkLabel={(datum) => +// `${datum.id.toLocaleString().toUpperCase()}` +// } +// arcLinkLabelsThickness={2} +// arcLinkLabelsColor={{ from: 'color' }} +// arcLinkLabelsTextColor={{ +// from: 'color', +// modifiers: [['darker', 3]] +// }} +// arcLabelsSkipAngle={20} +// arcLabelsRadiusOffset={0.55} +// arcLabelsTextColor={{ +// from: 'color', +// modifiers: [['darker', 3]] +// }} +// arcLinkLabelsOffset={2} +// arcLabelsComponent={({ datum, label, style }) => ( +// +// +// +// +// {label} +// +// +// )} +// defs={defs} +// fill={fill} +// legends={[ +// { +// anchor: 'bottom', +// direction: 'row', +// justify: false, +// translateX: 0, +// translateY: 20, +// itemsSpacing: 0, +// itemWidth: 50, +// itemHeight: 10, +// itemTextColor: '#999', +// itemDirection: 'bottom-to-top', +// itemOpacity: 1, +// symbolSize: 18, +// symbolShape: 'circle', +// effects: [ +// { +// on: 'hover', +// style: { +// itemTextColor: '#000' +// } +// } +// ] +// } +// ]} +// layers={['arcs', 'arcLabels', 'arcLinkLabels']} +// /> +//
+//
+//
+// ); +// }; + +// export default VulnerabilityPieChart; +import React, { useEffect, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { - ResponsivePie, - ComputedDatum, - PieTooltipProps, - PieCustomLayerProps, - ResponsivePieCanvas -} from '@nivo/pie'; -import { Paper } from '@mui/material'; +import { ResponsivePie } from '@nivo/pie'; +import { animated } from '@react-spring/web'; +import { Paper, Tooltip } from '@mui/material'; import { Point } from './Risk'; import * as RiskStyles from './style'; -import { arc } from 'd3-shape'; + +const patterns = [ + { id: 'dots', type: 'patternDots', size: 3, padding: 2, stagger: false }, + { + id: 'squares', + type: 'patternSquares', + size: 3, + padding: 3, + stagger: false + }, + { + id: 'lines', + type: 'patternLines', + rotation: -36, + lineWidth: 2, + spacing: 10 + }, + { + id: 'dots-alternate', + type: 'patternDots', + size: 2, + padding: 6, + stagger: true + }, + { + id: 'lines-alternate', + type: 'patternLines', + rotation: 25, + lineWidth: 6, + spacing: 12 + }, + { + id: 'big-squares', + type: 'patternSquares', + size: 4, + padding: 1, + stagger: true + } +]; + +const generatePatternsAndFills = (data: any[]) => { + return data.map((point: { id: any }, index: number) => { + const pattern = patterns[index % patterns.length]; + const patternId = `${pattern.id}-${point.id}`; + return { + pattern: { + ...pattern, + background: 'inherit', + color: 'rgba(0, 0, 0, 0.25)', + id: patternId + }, + fill: { + match: { id: point.id }, + id: patternId + } + }; + }); +}; const VulnerabilityPieChart = (props: { title: string; @@ -21,59 +296,27 @@ const VulnerabilityPieChart = (props: { const history = useHistory(); const { title, data, colors, type } = props; const { cardRoot, cardSmall, header, chartSmall } = RiskStyles.classesRisk; + const [ariaLabel, setAriaLabel] = useState(''); + const ariaLiveRef = useRef(null); - // console.log('VulnerabilityPieChart props:', props); // Debugging - - // const getTooltip = ({ datum }: PieTooltipProps) => { - // const totalValue = data.reduce((acc, curr) => acc + curr.value, 0); - // const percentage = ((datum.value / totalValue) * 100).toFixed(2); - // return ( - //
- // - // {datum.id}: {datum.value} ({percentage}%) - // - //
- // ); - // }; + useEffect(() => { + const label = + `${title} pie chart. ` + + data + .map( + (point) => + `${point.id}: ${point.value} (${( + (point.value / data.reduce((acc, curr) => acc + curr.value, 0)) * + 100 + ).toFixed(2)}%)` + ) + .join(', '); + setAriaLabel(label); + }, [title, data]); - // const customLayer = (props: PieCustomLayerProps) => { - // const pieArc = arc() - // .innerRadius(0) - // .outerRadius(Math.min(props.centerX, props.centerY)); - - // return ( - // - // {props.dataWithArc.map((arcDatum) => { - // const centroid = pieArc.centroid(arcDatum.arc); - // const percentage = ( - // (arcDatum.value / data.reduce((acc, curr) => acc + curr.value, 0)) * - // 100 - // ).toFixed(2); - // return ( - // - // ); - // })} - // - // ); - // }; - - const ariaLabel = - `${title} pie chart. ` + - data - .map( - (point) => - `${point.id}: ${point.value} (${( - (point.value / data.reduce((acc, curr) => acc + curr.value, 0)) * - 100 - ).toFixed(2)}%)` - ) - .join(', '); + const patternsAndFills = generatePatternsAndFills(data); + const defs = patternsAndFills.map((pf: { pattern: any }) => pf.pattern); + const fill = patternsAndFills.map((pf: any) => pf.fill); return ( @@ -81,9 +324,21 @@ const VulnerabilityPieChart = (props: {

{title}

-
- {/*
*/} - +
+ {ariaLabel} +
+ { @@ -103,140 +362,99 @@ const VulnerabilityPieChart = (props: { history.push(`/inventory/vulnerabilities?severity=${event.id}`); } }} - // arcLabel={(datum) => `${datum.id} (${datum.value})`} - // arcLinkLabel={(datum) => `${datum.id}: ${datum.value}`} - arcLabel={(datum) => `(${datum.value})`} - arcLabelsSkipAngle={10} - arcLabelsTextColor={{ - from: 'color', - modifiers: [['darker', 3]] - }} arcLinkLabelsSkipAngle={10} - arcLinkLabel={(datum) => `${datum.id}`} - arcLinkLabelsThickness={3} + arcLinkLabel={(datum) => + `${datum.id.toLocaleString().toUpperCase()}` + } + arcLinkLabelsThickness={2} arcLinkLabelsColor={{ from: 'color' }} arcLinkLabelsTextColor={{ from: 'color', modifiers: [['darker', 3]] }} - defs={[ - { - id: 'dots', - type: 'patternDots', - background: 'inherit', - color: 'rgba(255, 255, 255, 0.3)', - size: 4, - padding: 1, - stagger: true - }, - { - id: 'lines', - type: 'patternLines', - background: 'inherit', - color: 'rgba(255, 255, 255, 0.3)', - rotation: -45, - lineWidth: 6, - spacing: 10 - } - ]} - fill={[ - { - match: { - id: 'High' - }, - id: 'dots' - }, - { - match: { - id: 'Medium' - }, - id: 'dots' - }, - { - match: { - id: 'Low' - }, - id: 'dots' - }, - { - match: { - id: 'Critical' - }, - id: 'dots' - }, - { - match: { - id: 'scala' - }, - id: 'lines' - }, - { - match: { - id: 'lisp' - }, - id: 'lines' - }, - { - match: { - id: 'elixir' - }, - id: 'lines' - }, - { - match: { - id: 'javascript' - }, - id: 'lines' - } - ]} - // legends={[ - // { - // anchor: 'right', - // direction: 'column', - // justify: false, - // translateX: 60, - // translateY: 56, - // itemsSpacing: 6, - // itemWidth: 100, - // itemHeight: 18, - // itemTextColor: '#999', - // itemDirection: 'left-to-right', - // itemOpacity: 1, - // symbolSize: 18, - // symbolShape: 'circle', - // effects: [ - // { - // on: 'hover', - // style: { - // itemTextColor: '#000' - // } - // } - // ] - // } - // ]} + arcLabelsSkipAngle={20} + arcLabelsRadiusOffset={0.55} + arcLabelsTextColor={{ + from: 'color', + modifiers: [['darker', 3]] + }} + arcLinkLabelsOffset={2} + arcLabelsComponent={({ datum, label, style }) => ( + + + + + + {label} + + + + )} + defs={defs} + fill={fill} legends={[ { - anchor: 'bottom-left', - direction: 'column', + anchor: 'bottom', + direction: 'row', justify: false, translateX: 0, - translateY: 0, - itemWidth: 25, - itemHeight: 20, + translateY: 20, itemsSpacing: 0, - symbolSize: 20, - itemDirection: 'left-to-right' + itemWidth: 50, + itemHeight: 10, + itemTextColor: '#999', + itemDirection: 'bottom-to-top', + itemOpacity: 1, + symbolSize: 18, + symbolShape: 'circle', + effects: [ + { + on: 'hover', + style: { + itemTextColor: '#000' + } + } + ] } ]} - layers={[ - 'arcs', - 'arcLabels', - 'arcLinkLabels', - 'legends' - //customLayer - ]} + layers={['arcs', 'arcLabels', 'arcLinkLabels']} /> - {/*
*/}
diff --git a/frontend/src/pages/Risk/utils.ts b/frontend/src/pages/Risk/utils.ts index 2c204c0a..62b19e27 100644 --- a/frontend/src/pages/Risk/utils.ts +++ b/frontend/src/pages/Risk/utils.ts @@ -6,17 +6,17 @@ export const getSingleColor = () => { }; export const getSeverityColor = ({ id }: { id: string }) => { if (id === 'null' || id === '') return '#EFF1F5'; - else if (id === 'Low') return '#F8DFE2'; - else if (id === 'Medium') return '#F2938C'; - else if (id === 'High') return '#B51D09'; + else if (id === 'Low') return '#ffe100'; + else if (id === 'Medium') return '#f66e1f'; + else if (id === 'High') return '#ed240e'; else return '#540C03'; }; export const getCVSSColor = (score: number) => { if (!score || score === 0) return ['#EFF1F5', 'NONE']; - else if (0.1 <= score && score <= 3.9) return ['#99cc33', 'LOW']; - else if (4 <= score && score <= 6.9) return ['#ffcc00', 'MEDIUM']; - else if (7 <= score && score <= 8.9) return ['#ff9966', 'HIGH']; - else if (9 <= score && score <= 10) return ['#ff6254', 'CRITICAL']; + else if (0.1 <= score && score <= 3.9) return ['#ffe100', 'LOW']; + else if (4 <= score && score <= 6.9) return ['#fd913f', 'MEDIUM']; + else if (7 <= score && score <= 8.9) return ['#f13a25', 'HIGH']; + else if (9 <= score && score <= 10) return ['#ab0225', 'CRITICAL']; else return '#540C03'; }; export const offsets: any = { @@ -42,4 +42,14 @@ export const severities: VulnSeverities[] = [ { label: 'Low', sevList: ['Low'] } ]; +export const getServicesColor = ({ id }: { id: string }) => { + if (id === 'null' || id === '') return '#EFF1F5'; + else if (id === 'http') return '#2cb9ff'; + else if (id === 'https') return '#0e8cd6'; + else if (id === 'rdp') return '#0063b4'; + else if (id === 'ftp') return '#2a6ebb'; + else if (id === 'ssh') return '#5792ff'; + else return '#540C03'; +}; + export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));