From 57a5bef00ee095d1c5bf19af29c5debf7611595f Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 27 Oct 2024 23:17:46 +1100 Subject: [PATCH] fix: artifact UI and cvss --- .repo/scratchad.sql | 4 +- functions/v1/artifact/files.js | 10 +- functions/v1/issue/[uuid].js | 56 +++--- .../DefaultLayoutWithVerticalNav.vue | 4 +- src/pages/Artifacts.vue | 170 ++++++++---------- src/pages/Issue.vue | 106 ++++++++--- src/utils.js | 44 ++++- 7 files changed, 228 insertions(+), 166 deletions(-) diff --git a/.repo/scratchad.sql b/.repo/scratchad.sql index 3eef548..3210739 100644 --- a/.repo/scratchad.sql +++ b/.repo/scratchad.sql @@ -8,5 +8,5 @@ -- ALTER TABLE Finding DROP COLUMN malicious; -- ALTER TABLE Finding ADD malicious INTEGER; SELECT * -FROM CVEMetadata -WHERE vectorString IS NOT NULL; +FROM Finding +WHERE cvssVectorString IS NOT NULL; diff --git a/functions/v1/artifact/files.js b/functions/v1/artifact/files.js index baf911f..edad70f 100644 --- a/functions/v1/artifact/files.js +++ b/functions/v1/artifact/files.js @@ -129,10 +129,10 @@ export async function onRequestGet(context) { }) const artifacts = result.map(artifact => { - artifact.spdx = artifact.spdx.sort((a, b) => a.createdAt - b.createdAt)?.pop() - artifact.sarif = artifact.sarif.sort((a, b) => a.createdAt - b.createdAt)?.pop() - artifact.cdx = artifact.cdx.sort((a, b) => a.createdAt - b.createdAt)?.pop() - const vex = artifact.vex.sort((a, b) => a.lastObserved - b.lastObserved)?.pop() + artifact.spdx = artifact.spdx.sort((a, b) => b.createdAt - a.createdAt)?.pop() + artifact.sarif = artifact.sarif.sort((a, b) => b.createdAt - a.createdAt)?.pop() + artifact.cdx = artifact.cdx.sort((a, b) => b.createdAt - a.createdAt)?.pop() + const vex = artifact.vex.sort((a, b) => b.lastObserved - a.lastObserved)?.pop() if (vex?.finding) { vex.repoName = vex?.finding?.repoName vex.source = vex?.finding?.source @@ -140,7 +140,7 @@ export async function onRequestGet(context) { delete vex.finding } artifact.vex = vex - artifact.downloadLink = artifact.downloadLinks.sort((a, b) => a.id - b.id)?.pop() + artifact.downloadLink = artifact.downloadLinks.sort((a, b) => b.id - a.id)?.pop() delete artifact.downloadLinks return artifact }) diff --git a/functions/v1/issue/[uuid].js b/functions/v1/issue/[uuid].js index cbf34de..265e744 100644 --- a/functions/v1/issue/[uuid].js +++ b/functions/v1/issue/[uuid].js @@ -73,6 +73,7 @@ export async function onRequestGet(context) { const cveId = finding.detectionTitle.startsWith('CVE-') ? finding.detectionTitle : osvData?.aliases?.filter(a => a.startsWith('CVE-')).pop() let cvssVector + let cvssScore let cvelistv5 let cve if (cveId) { @@ -89,13 +90,13 @@ export async function onRequestGet(context) { cveMetadata, containers: { cna, adp } } = cvelistv5 - const cveCvss = findVectorString(cna.metrics) - if (cveCvss.startsWith('CVSS:4/')) { - cvssVector = new CVSS40(cveCvss) - } else if (cveCvss.startsWith('CVSS:3.1/')) { - cvssVector = new CVSS31(cveCvss) - } else if (cveCvss.startsWith('CVSS:3/')) { - cvssVector = new CVSS30(cveCvss) + const cvssVector = findVectorString(cna.metrics) + if (cvssVector.startsWith('CVSS:4.0/')) { + cvssScore = new CVSS40(cvssVector).Score().toString() + } else if (cvssVector.startsWith('CVSS:3.1/')) { + cvssScore = new CVSS31(cvssVector).BaseScore().toString() + } else if (cvssVector.startsWith('CVSS:3.0/')) { + cvssScore = new CVSS30(cvssVector).BaseScore().toString() } if (cna?.timeline) { finding.timelineJSON = JSON.stringify(cna.timeline.map(i => convertIsoDatesToTimestamps(i))) @@ -170,10 +171,15 @@ export async function onRequestGet(context) { epssScore = parseFloat(scores.epss) epssPercentile = parseFloat(scores.percentile) } - const cvss4 = osvData?.severity?.filter(i => i.score.startsWith('CVSS:4/'))?.pop() - const cvss31 = osvData?.severity?.filter(i => i.score.startsWith('CVSS:3.1/'))?.pop() - const cvss3 = osvData?.severity?.filter(i => i.score.startsWith('CVSS:3/'))?.pop() - cvssVector = !!cvss4 ? new CVSS40(cvss4.score) : !!cvss31 ? new CVSS31(cvss31.score) : cvss3 ? new CVSS30(cvss3.score) : null + const cvss = {} + if (!cvssVector) { + cvss.v4 = osvData?.severity?.filter(i => i.score.startsWith('CVSS:4/'))?.pop() + cvss.v31 = osvData?.severity?.filter(i => i.score.startsWith('CVSS:3.1/'))?.pop() + cvss.v3 = osvData?.severity?.filter(i => i.score.startsWith('CVSS:3/'))?.pop() + cvssVector = !!cvss.v4 ? cvss.v4.score : !!cvss.v31 ? cvss.v31.score : cvss.v3 ? cvss.v3.score : null + const vector = !!cvss.v4 ? new CVSS40(cvss.v4.score) : !!cvss.v31 ? new CVSS31(cvss.v31.score) : cvss.v3 ? new CVSS30(cvss.v3.score) : null + cvssScore = !!cvss.v4 ? vector.Score().toString() : !!cvss.v31 ? vector.BaseScore().toString() : cvss.v3 ? vector.BaseScore().toString() : null + } // Decision // Methodology // Exploitation @@ -186,9 +192,10 @@ export async function onRequestGet(context) { let { analysisState = 'in_triage', triageAutomated = 0, triagedAt = null, seenAt = null } = finding?.triage || {} if ( (cvssVector && ( - ['E:U', 'E:P', 'E:F', 'E:H'].some(substring => cvss3?.score?.includes(substring)) || - ['E:A', 'E:P', 'E:U'].some(substring => cvss4?.score?.includes(substring)) - )) || epssPercentile > 0.2 + ['E:U', 'E:P', 'E:F', 'E:H'].some(substring => cvss.v3?.score?.includes(substring)) || + ['E:U', 'E:P', 'E:F', 'E:H'].some(substring => cvss.v31?.score?.includes(substring)) || + ['E:A', 'E:P', 'E:U'].some(substring => cvss.v4?.score?.includes(substring)) + )) || epssPercentile > 0.27 ) { analysisState = 'exploitable' triageAutomated = 1 @@ -199,21 +206,18 @@ export async function onRequestGet(context) { if (seen === 1) { seenAt = new Date().getTime() } - let vexExist = true - if (!finding.triage.some(t => t.analysisState === analysisState)) { - vexExist = false - finding.triage.push({ - analysisState, - findingUuid: uuid, - createdAt: new Date().getTime(), - lastObserved: new Date().getTime(), - }) - } + const vexExist = finding.triage.some(t => t.analysisState === analysisState).length !== 0 let vexData = finding.triage.filter(t => t.analysisState === analysisState).pop() + if (!vexExist) { + vexData.analysisState = analysisState + vexData.findingUuid = finding.uuid + vexData.createdAt = new Date().getTime() + vexData.lastObserved = new Date().getTime() + } vexData.triageAutomated = triageAutomated vexData.triagedAt = triagedAt - vexData.cvssVector = !!cvss4 ? cvss4.score : !!cvss31 ? cvss31.score : cvss3 ? cvss3.score : null - vexData.cvssScore = !!cvss4 ? cvssVector.Score().toString() : !!cvss31 ? cvssVector.BaseScore().toString() : cvss3 ? cvssVector.BaseScore().toString() : null + vexData.cvssVector = cvssVector + vexData.cvssScore = cvssScore if (epssPercentile) { vexData.epssPercentile = epssPercentile.toString() } diff --git a/src/layouts/components/DefaultLayoutWithVerticalNav.vue b/src/layouts/components/DefaultLayoutWithVerticalNav.vue index db78ada..91abcdd 100644 --- a/src/layouts/components/DefaultLayoutWithVerticalNav.vue +++ b/src/layouts/components/DefaultLayoutWithVerticalNav.vue @@ -103,11 +103,11 @@ watch(NotificationsStore, () => { icon: 'fluent-mdl2:set-action', to: '/triage', }" /> - + }" /> --> !!a).join('@') - } else if (bomFormat === "SPDX") { - file.title = [artifact.spdx.name, artifact.spdx.version].filter(a => !!a).join('@') - } - group = addFileToSourceSubgroup(group, file) - break + if (artifact.vex?.analysisResponse) { + analysis = `${analysis}, ${VexAnalysisResponse[artifact.vex.analysisResponse]}` } } - } - for (const group of state.artifacts) { - if (group.children.length === 0) { - group.children.push({ isEmpty: true }) + const file = { contentType, ext, lastModified: artifact.date, uuid, source, url, analysis, findingTitle, dependencies, repoName, results, versionInfo } + file.key = crypto.randomUUID() + group.key = crypto.randomUUID() + if (group.title === "SARIF" && contentType?.includes("sarif")) { + file.title = analysisKey || `${uuid}.${ext}` + group = addFileToSourceSubgroup(group, file) + break + } else if (group.title === "VEX" && type === "VEX") { + file.title = artifact.vex.findingTitle + group = addFileToSourceSubgroup(group, file) + break + } else if (group.title === bomFormat) { + if (bomFormat === "CycloneDX") { + file.title = [artifact.cdx.name, artifact.cdx.version].filter(a => !!a).join('@') + } else if (bomFormat === "SPDX") { + file.title = [artifact.spdx.name, artifact.spdx.version].filter(a => !!a).join('@') + } + group = addFileToSourceSubgroup(group, file) + break } } } - } else if (typeof data === "string" && !isJSON(data)) { - break - } else if (data?.error?.message) { - state.loading = false - state.error = data.error.message - return - } else if (["Expired", "Revoked", "Forbidden"].includes(data?.result)) { - state.loading = false - state.info = data.result - setTimeout(() => router.push('/logout'), 2000) - return } else { break } @@ -144,6 +127,12 @@ class Controller { skip += pageSize } } + for (const group of state.artifacts) { + if (group.children.length === 0) { + group.children.push({ isEmpty: true }) + } + } + state.loading = false } catch (e) { console.error(e) @@ -321,12 +310,7 @@ const files = ref({ }) function addFileToSourceSubgroup(group, file) { if (!file?.source) { - if (group.children.length === 1) { - const child = group.children.pop() - if (child.isEmpty) { - group.children = [] - } - } + group.children = group.children.filter(item => !item?.isEmpty) group.children.push(file) return group } @@ -338,12 +322,7 @@ function addFileToSourceSubgroup(group, file) { } if (!sourceSubgroup.children.some(f => f.uuid === file.uuid)) { delete file.source - if (sourceSubgroup.children.length === 1) { - const child = sourceSubgroup.children.pop() - if (child.isEmpty) { - sourceSubgroup.children = [] - } - } + sourceSubgroup.children = sourceSubgroup.children.filter(item => !item?.isEmpty) sourceSubgroup.children.push(file) } @@ -401,13 +380,11 @@ function updateArtifactsFromFiles(files) { newFile.results = fileData.resultsCount || 0 // Assuming SARIF has a resultsCount } + // Remove the 'isEmpty' property if it exists + uploadObject.children = uploadObject.children.filter(item => !item?.isEmpty) + // Add the new file to the upload object's children uploadObject.children.push(newFile) - - // Remove the 'isEmpty' property if it exists - if (targetArtifact.children[0].isEmpty) { - delete targetArtifact.children[0].isEmpty - } } } @@ -581,7 +558,8 @@ function updateArtifactsFromFiles(files) { import { useMemberStore } from '@/stores/member'; -import { Client } from '@/utils'; +import { Client, round } from '@/utils'; import IconVulnetix from '@images/IconVulnetix.vue'; import { reactive } from 'vue'; import { useRoute } from 'vue-router'; @@ -17,15 +17,18 @@ const initialState = { loading: false, issue: {}, } + const state = reactive({ ...initialState, }) + const clearAlerts = () => { state.error = '' state.warning = '' state.success = '' state.info = '' } + class Controller { fetchIssue = async (uuid) => { clearAlerts() @@ -34,6 +37,7 @@ class Controller { const { data } = await client.get(`/issue/${uuid}`) if (data?.finding) { state.finding = data.finding + state.finding.vex = data.finding.triage.sort((a, b) => b.lastObserved - a.lastObserved)?.pop() } state.loading = false } catch (e) { @@ -50,11 +54,19 @@ const formatDate = (timestamp) => { } const getSeverityColor = (score) => { - if (!score) return 'grey' - if (score < 4) return 'green' - if (score < 7) return 'yellow' - if (score < 9) return 'orange' - return 'red' + if (!score) return '#808080' + if (score < 4) return '#008000' + if (score < 7) return 'ffff00' + if (score < 9) return '#ffa500' + return '#ff0000' +} + +const getSeverity = (score) => { + if (!score) return 'Informational' + if (score < 4) return 'Low' + if (score < 7) return 'Medium' + if (score < 9) return 'High' + return 'Critical' } const getSourceIcon = (url) => { @@ -71,9 +83,10 @@ onBeforeRouteUpdate(async (to, from) => { state.issue = await controller.fetchIssue(to.params.uuid) } }) + -