diff --git a/.storybook/mocks/mocks.ts b/.storybook/mocks/mocks.ts index 0030a4e9..fb1b7dca 100644 --- a/.storybook/mocks/mocks.ts +++ b/.storybook/mocks/mocks.ts @@ -65,8 +65,7 @@ export function createTask(): Task { `::group::${eventName}\n` + `::${status[Math.floor(faker.number.int({ min: 0, max: 1 }) * status.length)]}:: Job '${jobName}'\n` + `::step-start::${stepName}\n` + - `::${ - status[Math.floor(faker.number.int({ min: 0, max: 1 }) * status.length)] + `::${status[Math.floor(faker.number.int({ min: 0, max: 1 }) * status.length)] }:: Job '${jobName}' step '${stepName}'\n` + `::step-end::${stepName}::${duration}\n` + `${generateLogMessage()}\n` + @@ -143,12 +142,14 @@ export const Problem = (args: any) => { const links = `https://security-tracker.debian.org/tracker/${vuln_id}`; const severityScore = `0.${faker.number.int({ min: 1, max: 9 })}`; const data = JSON.stringify({ id: `${faker.number.int({ min: 1, max: 100 })}` }, null, '\t'); + const service = faker.helpers.arrayElement(['cli', 'service1']); return { identifier: vuln_id, severity: args.hasOwnProperty('severity') ? args.severity : severity, source: args.hasOwnProperty('source') ? args.source : source, severityScore: severityScore, associatedPackage: associatedPackage, + service: service, description, links, data, diff --git a/src/components/Problems/index.js b/src/components/Problems/index.js index 42c5f870..9812a67c 100644 --- a/src/components/Problems/index.js +++ b/src/components/Problems/index.js @@ -12,8 +12,32 @@ const getOptionsFromProblems = (problems, key) => { return [...uniqueOptions]; }; +// reduceProblemsDuplicatedByService will take a list of problems and remove duplicates +// Duplicates are any items with the same source and identifier (multiple sources could report the same cve, but perhaps with different data) +// it appends the duplicated service names. +const reduceProblemsDuplicatedByService = problems => { + let reduceProblemsByService = new Map(); + + for (const c of problems) { + const { identifier, source, service } = c; + const id = `${identifier}-${source}`; + + if (reduceProblemsByService.has(id)) { + let prob = Object.assign({}, reduceProblemsByService.get(id)); + prob.service = `${prob.service}, ${c.service}`; + reduceProblemsByService.set(id, prob); + } else { + reduceProblemsByService.set(id, c); + } + } + return Array.from(reduceProblemsByService.values()); +} + const Problems = ({ problems }) => { - const { sortedItems, requestSort, getClassNamesFor } = useSortableProblemsData(problems); + + const reducedProblems = reduceProblemsDuplicatedByService(problems); + + const { sortedItems, requestSort, getClassNamesFor } = useSortableProblemsData(reducedProblems); const [severitySelected, setSeverity] = useState([]); const [sourceSelected, setSource] = useState([]); const [servicesSelected, setService] = useState([]); @@ -26,6 +50,7 @@ const Problems = ({ problems }) => { const sources = getOptionsFromProblems(problems, 'source'); const services = getOptionsFromProblems(problems, 'service'); + // Handlers const handleSort = key => requestSort(key); @@ -70,40 +95,40 @@ const Problems = ({ problems }) => { const matchesSeveritySelector = item => { return severitySelected.length > 0 ? Object.keys(item).some(key => { - if (item[key] !== null) { - return severitySelected.indexOf(item['severity'].toString()) > -1; - } - }) + if (item[key] !== null) { + return severitySelected.indexOf(item['severity'].toString()) > -1; + } + }) : true; }; const matchesSourceSelector = item => { return sourceSelected.length > 0 ? Object.keys(item).some(key => { - if (item[key] !== null) { - return sourceSelected.indexOf(item['source'].toString()) > -1; - } - }) + if (item[key] !== null) { + return sourceSelected.indexOf(item['source'].toString()) > -1; + } + }) : true; }; const matchesServiceSelector = item => { return servicesSelected.length > 0 ? Object.keys(item).some(key => { - if (item[key] !== null) { - return servicesSelected.indexOf(item['service'].toString()) > -1; - } - }) + if (item[key] !== null) { + return servicesSelected.indexOf(item['service'].toString()) > -1; + } + }) : true; }; const matchesTextFilter = item => { return problemTerm != null || problemTerm !== '' ? Object.keys(item).some(key => { - if (item[key] !== null) { - return item[key].toString().toLowerCase().includes(problemTerm.toLowerCase()); - } - }) + if (item[key] !== null) { + return item[key].toString().toLowerCase().includes(problemTerm.toLowerCase()); + } + }) : true; }; @@ -116,6 +141,7 @@ const Problems = ({ problems }) => { ); }; + useEffect(() => { let stats = { critical: sortedItems.filter(p => p.severity === 'CRITICAL').length, diff --git a/src/stories/problems.stories.tsx b/src/stories/problems.stories.tsx index b2ce0c74..9831ae34 100644 --- a/src/stories/problems.stories.tsx +++ b/src/stories/problems.stories.tsx @@ -54,6 +54,39 @@ export const Default: Story = { }, }; +const duplicateProblemsAcrossServices = [ + { ...problemData[0], service: "cli" }, + { ...problemData[0], service: "php-nginx" }, + { ...problemData[1], service: "cli" }, + { ...problemData[1], service: "node" }, + { ...problemData[1], service: "service" }, +]; + +export const DuplicateData: Story = { + args: { + router: { + query: fakeQueryParams, + }, + }, + parameters: { + msw: { + handlers: [ + graphql.query('getEnvironment', (_, res, ctx) => { + return res( + ctx.delay(), + ctx.data({ + environment: { + ...generateEnvironments(), + problems: duplicateProblemsAcrossServices, + }, + }) + ); + }), + ], + }, + }, +}; + export const Loading: Story = { parameters: { msw: { @@ -65,4 +98,7 @@ export const Loading: Story = { }, }, }; + + + export default meta;