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)
}
})
+
-
+
{
cols="12"
md="6"
>
+ {{ state.finding.detectionTitle }}
- CVSS Score
- {{ state.finding.cvssScore || 'N/A'
- }}
+ Aliases
+ {{ alias }}
+ CVSS Score
+
+
+ {{ state.finding.vex.cvssScore }}
+
+
+ {{ state.finding.vex.cvssScore }}
+
+
+ {{ state.finding.vex.cvssScore }}
+
+
+ {{ state.finding.vex.cvssScore }}
+
+
+ {{ getSeverity(state.finding.vex.cvssScore) }}
+
+
+
+
EPSS Score
- {{ state.finding.epssScore || 'N/A'
- }}
+
+ {{ state.finding.vex.epssScore }} ({{
+ round(parseFloat(state.finding.vex.epssPercentile)) }}%)
+
Fix Version
- {{ state.finding.fixVersion || 'N/A'
- }}
+ {{ state.finding.fixVersion || 'N/A' }}
-
+
Vulnerable Version Range
- {{ state.finding.vulnerableVersionRange || 'Unknown'
- }}
-
-
- Malicious
- {{ state.finding.malicious ? 'Yes' : 'No'
- }}
+ {{ state.finding.vulnerableVersionRange || 'Unknown' }}
+
- CVE
- {{ state.finding.cve || 'N/A' }}
+ Known Malicious
+ {{ state.finding.malicious ? 'Yes' : 'No' }}
Published At
- {{ formatDate(state.finding.publishedAt)
- }}
+ {{ formatDate(state.finding.publishedAt) }}
diff --git a/src/utils.js b/src/utils.js
index 6497d7a..58a534f 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -817,11 +817,7 @@ export class GitHub {
return { url, error: { message: e.message, lineno, colno } }
}
}
- async fetchSARIF(url) {
- const githubIntegration = await prisma.IntegrationConfig.findFirst({ where: { orgId, AND: { name: `github` } } })
- if (!!githubIntegration?.suspend) {
- throw new Error('GitHub Integration is Disabled')
- }
+ async __FetchSARIF(url) {
try {
const headers = Object.assign(this.headers, { 'Accept': 'application/sarif+json' })
const response = await fetch(url, { headers })
@@ -831,8 +827,11 @@ export class GitHub {
console.error(respText)
console.error(`GitHub error! status: ${response.status} ${response.statusText}`)
}
- const tokenExpiry = response.headers.get('GitHub-Authentication-Token-Expiration')
- const content = JSON.parse(respText)
+ let content = respText
+ if (isJSON(respText)) {
+ content = JSON.parse(respText)
+ }
+ const tokenExpiry = (new Date(response.headers.get('GitHub-Authentication-Token-Expiration'))).getTime()
return { ok: response.ok, status: response.status, statusText: response.statusText, tokenExpiry, error: { message: content?.message }, content, url }
} catch (e) {
const [, lineno, colno] = e.stack.match(/(\d+):(\d+)/)
@@ -841,6 +840,33 @@ export class GitHub {
return { url, error: { message: e.message, lineno, colno } }
}
}
+ async fetchSARIF(prisma, orgId, memberEmail, url) {
+ const githubIntegration = await prisma.IntegrationConfig.findFirst({ where: { orgId, AND: { name: `github` } } })
+ if (!!githubIntegration?.suspend) {
+ throw new Error('GitHub Integration is Disabled')
+ }
+ try {
+ const response = await this.__FetchSARIF(url)
+ const createLog0 = await prisma.IntegrationUsageLog.create({
+ data: {
+ memberEmail,
+ orgId,
+ source: 'github',
+ request: JSON.stringify({ method: "GET", url }).trim(),
+ response: JSON.stringify({ body: convertIsoDatesToTimestamps(response.content), tokenExpiry: response.tokenExpiry }).trim(),
+ statusCode: response?.status || 500,
+ createdAt: new Date().getTime(),
+ }
+ })
+ // console.log(`GitHub.getRepoSarif()`, createLog0)
+ return response
+ } catch (e) {
+ const [, lineno, colno] = e.stack.match(/(\d+):(\d+)/)
+ console.error(`line ${lineno}, col ${colno} ${e.message}`, e.stack)
+
+ return { url, error: { message: e.message, lineno, colno } }
+ }
+ }
async getRepoSarif(prisma, orgId, memberEmail, full_name) {
// https://docs.github.com/en/rest/code-scanning/code-scanning?apiVersion=2022-11-28#list-code-scanning-analyses-for-a-repository
const githubIntegration = await prisma.IntegrationConfig.findFirst({ where: { orgId, AND: { name: `github` } } })
@@ -866,14 +892,14 @@ export class GitHub {
createdAt: new Date().getTime(),
}
})
- console.log(`GitHub.getRepoSarif()`, createLog0)
+ // console.log(`GitHub.getRepoSarif()`, createLog0)
if (!data?.ok) {
return data
}
for (const report of data.content) {
const sarifUrl = `${this.baseUrl}/repos/${full_name}/code-scanning/analyses/${report.id}`
console.log(`github.getRepoSarif(${full_name}) ${sarifUrl}`)
- const sarifData = await this.fetchSARIF(sarifUrl)
+ const sarifData = await this.__FetchSARIF(sarifUrl)
const createLog = await prisma.IntegrationUsageLog.create({
data: {
memberEmail,