diff --git a/.repo/scratchad.sql b/.repo/scratchad.sql index 8a94713..58de372 100644 --- a/.repo/scratchad.sql +++ b/.repo/scratchad.sql @@ -10,6 +10,13 @@ -- SELECT * -- FROM Triage -- WHERE findingUuid = "c00f2661-a1fc-4a72-83af-5894580ee510"; -SELECT * -FROM Finding -WHERE affectedFunctions IS NOT NULL; +SELECT A."accessToken", + A."avatarUrl", + A."created", + A."expires", + A."installationId", + A."login", + A."memberEmail", + B."orgId" +FROM "GitHubApp" A + INNER JOIN "Member" B ON A."memberEmail" = B."email"; diff --git a/functions/api/[analysisState]/issues.js b/functions/api/[analysisState]/issues.js index 13002f8..9b72985 100644 --- a/functions/api/[analysisState]/issues.js +++ b/functions/api/[analysisState]/issues.js @@ -37,9 +37,6 @@ export async function onRequestGet(context) { } }, }, - omit: { - memberEmail: true, - }, include: { triage: true, spdx: { diff --git a/functions/api/analytics.js b/functions/api/analytics.js index 742667c..7e3dfaa 100644 --- a/functions/api/analytics.js +++ b/functions/api/analytics.js @@ -30,9 +30,6 @@ export async function onRequestGet(context) { where: { orgId: verificationResult.session.orgId, }, - omit: { - memberEmail: true, - }, include: { triage: true, spdx: { diff --git a/functions/api/archive.js b/functions/api/archive.js index f6eb13d..05b2575 100644 --- a/functions/api/archive.js +++ b/functions/api/archive.js @@ -36,9 +36,6 @@ export async function onRequestGet(context) { } }, }, - omit: { - memberEmail: true, - }, include: { triage: { orderBy: { diff --git a/functions/api/cdx.js b/functions/api/cdx.js index cc8e8dd..6df2986 100644 --- a/functions/api/cdx.js +++ b/functions/api/cdx.js @@ -1,3 +1,4 @@ +import { parseCycloneDXComponents } from "@/finding"; import { AuthResult, ensureStrReqBody, hex, isCDX, OSV, saveArtifact, Server } from "@/utils"; import { PrismaD1 } from '@prisma/adapter-d1'; import { PrismaClient } from '@prisma/client'; @@ -31,9 +32,6 @@ export async function onRequestGet(context) { where: { orgId: verificationResult.session.orgId, }, - omit: { - memberEmail: true, - }, include: { repo: true, artifact: { @@ -100,8 +98,29 @@ export async function onRequestPost(context) { orgId: verificationResult.session.orgId, } }) - - const artifactUuid = originalCdx?.artifactUuid || cdx.serialNumber.startsWith('urn:uuid:') ? cdx.serialNumber.substring(9) : crypto.randomUUID() + const artifactUuid = originalCdx?.artifactUuid || (cdx?.serialNumber?.startsWith('urn:uuid:') ? cdx.serialNumber.substring(9) : crypto.randomUUID()) + if (!cdx?.serialNumber) { + cdx.serialNumber = `urn:uuid:${artifactUuid}` + } + const dependencies = [] + for (const dep of parseCycloneDXComponents(cdx)) { + const info = await prisma.Dependency.upsert({ + where: { + cdx_dep: { + cdxId, + name: dep.name, + version: dep.version, + } + }, + update: { + license: dep.license, + dependsOnUuid: dep.dependsOnUuid + }, + create: { ...dep, cdxId } + }) + dependencies.push({ ...dep, cdxId }) + console.log(`Dependency ${dep.name}@${dep.version}`, info) + } const cdxStr = JSON.stringify(cdx) const artifact = await saveArtifact(prisma, env.r2artifacts, cdxStr, artifactUuid, `cyclonedx`) const cdxData = { @@ -109,19 +128,17 @@ export async function onRequestPost(context) { artifactUuid, source: 'upload', orgId: verificationResult.session.orgId, - memberEmail: verificationResult.session.memberEmail, cdxVersion: cdx.specVersion, serialNumber: cdx.serialNumber, name: cdx.metadata?.component?.name, version: cdx.metadata?.component?.version, - createdAt: (new Date(cdx.metadata.timestamp)).getTime(), + createdAt: cdx.metadata?.timestamp ? new Date(cdx.metadata.timestamp).getTime() : new Date().getTime(), toolName: cdx.metadata.tools.map(t => `${t?.vendor} ${t?.name} ${t?.version}`.trim()).join(', '), - externalReferencesCount: cdx.metadata.component?.externalReferences?.length || 0, } const info = await prisma.CycloneDXInfo.upsert({ where: { cdxId, - memberEmail: verificationResult.session.memberEmail, + orgId: verificationResult.session.orgId, }, update: { createdAt: cdxData.createdAt, @@ -130,6 +147,7 @@ export async function onRequestPost(context) { create: cdxData }) console.log(`/upload/cdx ${cdxId} kid=${verificationResult.session.kid}`, info) + cdxData.dependencies = dependencies files.push(cdxData) const osvQueries = cdx.components.map(component => { @@ -155,7 +173,6 @@ export async function onRequestPost(context) { const findingData = { findingId, orgId: verificationResult.session.orgId, - memberEmail: verificationResult.session.memberEmail, source: 'osv.dev', category: 'sca', createdAt: (new Date()).getTime(), diff --git a/functions/api/exploitable.js b/functions/api/exploitable.js index b99cef3..e0885e6 100644 --- a/functions/api/exploitable.js +++ b/functions/api/exploitable.js @@ -32,9 +32,6 @@ export async function onRequestGet(context) { orgId: verificationResult.session.orgId, triage: { every: { analysisState: 'exploitable' } } }, - omit: { - memberEmail: true, - }, include: { triage: { orderBy: { diff --git a/functions/api/github/[installation_id]/uninstall.js b/functions/api/github/[installation_id]/uninstall.js index 4ab341e..6002811 100644 --- a/functions/api/github/[installation_id]/uninstall.js +++ b/functions/api/github/[installation_id]/uninstall.js @@ -26,7 +26,7 @@ export async function onRequestGet(context) { try { const where = { - memberEmail: verificationResult.session.memberEmail, + orgId: verificationResult.session.orgId, installationId: parseInt(params.installation_id, 10), } const app = await prisma.GitHubApp.findUniqueOrThrow({ where }) diff --git a/functions/api/github/repos.js b/functions/api/github/repos.js index 8de44c9..097b54c 100644 --- a/functions/api/github/repos.js +++ b/functions/api/github/repos.js @@ -36,7 +36,7 @@ export async function onRequestGet(context) { const gitRepos = [] const installs = await prisma.GitHubApp.findMany({ where: { - memberEmail: verificationResult.session.memberEmail, + orgId: verificationResult.session.orgId, AND: { expires: { gte: (new Date()).getTime(), } } }, }) @@ -53,14 +53,14 @@ export async function onRequestGet(context) { await prisma.GitHubApp.update({ where: { installationId: parseInt(app.installationId, 10), - AND: { memberEmail: app.memberEmail, }, + AND: { orgId: app.orgId, }, }, data: app, }) continue } delete app.accessToken - delete app.memberEmail + return Response.json({ error, app }) } for (const repo of content) { @@ -107,7 +107,6 @@ const store = async (prisma, session, repo) => { pushedAt: (new Date(repo.pushed_at)).getTime(), defaultBranch: repo.default_branch, ownerId: repo.owner.id, - memberEmail: session.memberEmail, licenseSpdxId: repo.license?.spdx_id || '', licenseName: repo.license?.name || '', fork: repo.fork ? 1 : 0, diff --git a/functions/api/github/repos/[org]/[repo]/sarif.js b/functions/api/github/repos/[org]/[repo]/sarif.js index 5aa16c6..7afefe2 100644 --- a/functions/api/github/repos/[org]/[repo]/sarif.js +++ b/functions/api/github/repos/[org]/[repo]/sarif.js @@ -35,7 +35,7 @@ export async function onRequestGet(context) { const errors = [] const githubApps = await prisma.GitHubApp.findMany({ where: { - memberEmail: verificationResult.session.memberEmail, + orgId: verificationResult.session.orgId, }, }) const repoName = `${params.org}/${params.repo}` @@ -54,14 +54,13 @@ export async function onRequestGet(context) { await prisma.GitHubApp.update({ where: { installationId: parseInt(app.installationId, 10), - AND: { memberEmail: app.memberEmail, }, + AND: { orgId: app.orgId }, }, data: app, }) continue } delete app.accessToken - delete app.memberEmail errors.push({ error, app }) continue } @@ -116,7 +115,6 @@ const process = async (prisma, session, data, fullName) => { fullName, source: 'GitHub', orgId: session.orgId, - memberEmail: session.memberEmail, commitSha: data.report.commit_sha, ref: data.report.ref, createdAt: (new Date(data.report.created_at)).getTime(), @@ -215,7 +213,6 @@ const process = async (prisma, session, data, fullName) => { reportId: data.report.id.toString(), artifactUuid: sarifId, fullName, - memberEmail: session.memberEmail, commitSha: data.report.commit_sha, ref: data.report.commit_sha, createdAt: (new Date(data.report.created_at)).getTime(), diff --git a/functions/api/github/repos/[org]/[repo]/spdx.js b/functions/api/github/repos/[org]/[repo]/spdx.js index f93fab9..0a818a5 100644 --- a/functions/api/github/repos/[org]/[repo]/spdx.js +++ b/functions/api/github/repos/[org]/[repo]/spdx.js @@ -1,3 +1,4 @@ +import { createPurlFromUrl, parsePackageRef, parseSPDXComponents } from "@/finding"; import { GitHub, hex, isSPDX, OSV, saveArtifact, Server } from "@/utils"; import { PrismaD1 } from '@prisma/adapter-d1'; import { PrismaClient } from '@prisma/client'; @@ -39,7 +40,7 @@ export async function onRequestGet(context) { const githubApps = await prisma.GitHubApp.findMany({ where: { - memberEmail: verificationResult.session.memberEmail, + orgId: verificationResult.session.orgId, }, }) for (const app of githubApps) { @@ -55,14 +56,13 @@ export async function onRequestGet(context) { await prisma.GitHubApp.update({ where: { installationId: parseInt(app.installationId, 10), - AND: { memberEmail: app.memberEmail, }, + AND: { orgId: app.orgId }, }, data: app, }) continue } delete app.accessToken - delete app.memberEmail errors.push({ error, app }) continue } @@ -88,6 +88,26 @@ export async function onRequestGet(context) { } const findingIds = await process(prisma, verificationResult.session, repoName, spdx, spdxId, originalSpdx?.artifactUuid || artifact?.uuid) findings = [...findings, ...findingIds] + const dependencies = [] + for (const dep of parseSPDXComponents(spdx)) { + const info = await prisma.Dependency.upsert({ + where: { + spdx_dep: { + spdxId, + name: dep.name, + version: dep.version, + } + }, + update: { + license: dep.license, + dependsOnUuid: dep.dependsOnUuid + }, + create: { ...dep, spdxId } + }) + dependencies.push({ ...dep, spdxId }) + console.log(`Dependency ${dep.name}@${dep.version}`, info) + } + spdx.dependencies = dependencies files.push({ spdx, errors }) } const memberKeys = await prisma.MemberKey.findMany({ @@ -142,7 +162,6 @@ const process = async (prisma, session, repoName, spdx, spdxId, artifactUuid) => artifactUuid, source: 'GitHub', orgId: session.orgId, - memberEmail: session.memberEmail, repoName, spdxVersion: spdx.spdxVersion, dataLicense: spdx.dataLicense, @@ -168,13 +187,21 @@ const process = async (prisma, session, repoName, spdx, spdxId, artifactUuid) => console.log(`/github/repos/spdx ${repoName} kid=${session.kid}`, info) const osvQueries = spdx.packages.flatMap(pkg => { - if (!pkg?.externalRefs) { return } + const { version } = parsePackageRef(pkg.SPDXID, pkg.name) + if (!pkg?.externalRefs && pkg?.downloadLocation) { + return [{ + purl: createPurlFromUrl(pkg.downloadLocation, pkg.name, pkg?.versionInfo ? pkg.versionInfo : version), + name: pkg.name, + version: pkg?.versionInfo ? pkg.versionInfo : version, + license: pkg?.licenseConcluded || pkg?.licenseDeclared, + }] + } return pkg.externalRefs .filter(ref => ref?.referenceType === 'purl') .map(ref => ({ purl: ref.referenceLocator, name: pkg.name, - version: pkg?.versionInfo, + version: pkg?.versionInfo ? pkg.versionInfo : version, license: pkg?.licenseConcluded || pkg?.licenseDeclared, })) }).filter(q => q?.purl) @@ -192,7 +219,6 @@ const process = async (prisma, session, repoName, spdx, spdxId, artifactUuid) => const findingData = { findingId, orgId: session.orgId, - memberEmail: session.memberEmail, repoName, source: 'osv.dev', category: 'sca', diff --git a/functions/api/github/repos/cached.js b/functions/api/github/repos/cached.js index 7b5c9fc..7e0fc93 100644 --- a/functions/api/github/repos/cached.js +++ b/functions/api/github/repos/cached.js @@ -30,9 +30,6 @@ export async function onRequestGet(context) { where: { orgId: verificationResult.session.orgId, }, - omit: { - memberEmail: true, - }, take, skip, orderBy: { diff --git a/functions/api/issue/[uuid].js b/functions/api/issue/[uuid].js index 343bd31..b95c3a5 100644 --- a/functions/api/issue/[uuid].js +++ b/functions/api/issue/[uuid].js @@ -7,9 +7,9 @@ import { VexAnalysisResponse, VexAnalysisState } from "@/utils"; +import { CVSS30, CVSS31, CVSS40 } from '@pandatix/js-cvss'; import { PrismaD1 } from '@prisma/adapter-d1'; import { PrismaClient } from '@prisma/client'; -import { CVSS30, CVSS31, CVSS40 } from '@pandatix/js-cvss'; export async function onRequestPost(context) { const { @@ -163,14 +163,12 @@ export async function onRequestGet(context) { uuid, AND: { orgId: verificationResult.session.orgId } }, - omit: { - memberEmail: true, - }, include: { triage: true, spdx: { include: { repo: true, + dependencies: true, artifact: { include: { downloadLinks: true, @@ -181,6 +179,7 @@ export async function onRequestGet(context) { cdx: { include: { repo: true, + dependencies: true, artifact: { include: { downloadLinks: true, diff --git a/functions/api/login/github/[code].js b/functions/api/login/github/[code].js index ffbaec2..16bdb96 100644 --- a/functions/api/login/github/[code].js +++ b/functions/api/login/github/[code].js @@ -143,7 +143,7 @@ export async function onRequestGet(context) { }) console.log(`/github/install session kid=${token}`, sessionInfo) const githubApp = await prisma.GitHubApp.findFirst({ - where: { memberEmail: response.member.email }, + where: { orgId: response.member.orgId }, }) let installationId = githubApp?.installationId if (!installationId) { @@ -170,7 +170,7 @@ export async function onRequestGet(context) { }, create: { installationId: parseInt(installationId, 10), - memberEmail: response.member.email, + orgId: response.member.orgId, accessToken: oauthData.access_token, login: content.login, created, diff --git a/functions/api/new-issues.js b/functions/api/new-issues.js deleted file mode 100644 index 59aecf0..0000000 --- a/functions/api/new-issues.js +++ /dev/null @@ -1,75 +0,0 @@ -import { AuthResult, Server } from "@/utils"; -import { PrismaD1 } from '@prisma/adapter-d1'; -import { PrismaClient } from '@prisma/client'; - -export async function onRequestGet(context) { - const { - request, // same as existing Worker API - env, // same as existing Worker API - params, // if filename includes [id] or [[path]] - waitUntil, // same as ctx.waitUntil in existing Worker API - next, // used for middleware or to fetch assets - data, // arbitrary space for passing data between middlewares - } = context - try { - const adapter = new PrismaD1(env.d1db) - const prisma = new PrismaClient({ - adapter, - transactionOptions: { - maxWait: 1500, // default: 2000 - timeout: 2000, // default: 5000 - }, - }) - const verificationResult = await (new Server(request, prisma)).authenticate() - if (!verificationResult.isValid) { - return Response.json({ ok: false, result: verificationResult.message }) - } - const { searchParams } = new URL(request.url) - const take = parseInt(searchParams.get('take'), 10) || 50 - const skip = parseInt(searchParams.get('skip'), 10) || 0 - const findings = await prisma.Finding.findMany({ - where: { - orgId: verificationResult.session.orgId, - AND: { - triage: { - is: { seen: 0, analysisState: 'in_triage', } - } - }, - }, - omit: { - memberEmail: true, - }, - include: { - triage: true, - spdx: { - include: { - repo: true - } - }, - cdx: { - include: { - repo: true - } - }, - }, - take, - skip, - orderBy: { - createdAt: 'asc', - } - }) - - return Response.json({ - ok: true, findings: findings.map(finding => { - finding.references = JSON.parse(finding.referencesJSON) - delete finding.referencesJSON - finding.aliases = JSON.parse(finding.aliases) - finding.cwes = JSON.parse(finding.cwes) - return finding - }) - }) - } catch (err) { - console.error(err) - return Response.json({ ok: false, error: { message: err }, result: AuthResult.REVOKED }) - } -} diff --git a/functions/api/next-issue.js b/functions/api/next-issue.js index adb02ee..8b86726 100644 --- a/functions/api/next-issue.js +++ b/functions/api/next-issue.js @@ -33,7 +33,7 @@ export async function onRequestGet(context) { orgId: verificationResult.session.orgId, AND: { triage: { - is: { analysisState: 'in_triage' } + every: { analysisState: 'in_triage' } } }, } @@ -42,14 +42,16 @@ export async function onRequestGet(context) { if (findingCount > 0) { finding = await prisma.Finding.findFirst({ where, - omit: { - memberEmail: true, - }, include: { - triage: true, + triage: { + orderBy: { + lastObserved: 'desc', // newest first + } + }, spdx: { include: { repo: true, + dependencies: true, artifact: { include: { downloadLinks: true, @@ -60,6 +62,7 @@ export async function onRequestGet(context) { cdx: { include: { repo: true, + dependencies: true, artifact: { include: { downloadLinks: true, diff --git a/functions/api/org/integrations.js b/functions/api/org/integrations.js index 85f954a..ca3aaa4 100644 --- a/functions/api/org/integrations.js +++ b/functions/api/org/integrations.js @@ -25,10 +25,9 @@ export async function onRequestGet(context) { } const githubApps = await prisma.GitHubApp.findMany({ where: { - memberEmail: verificationResult.session.memberEmail, + orgId: verificationResult.session.orgId, }, omit: { - memberEmail: true, accessToken: true, }, }) diff --git a/functions/api/sarif.js b/functions/api/sarif.js index 950771d..8286b3b 100644 --- a/functions/api/sarif.js +++ b/functions/api/sarif.js @@ -31,9 +31,6 @@ export async function onRequestGet(context) { where: { orgId: verificationResult.session.orgId, }, - omit: { - memberEmail: true, - }, include: { results: true, artifact: { @@ -102,7 +99,6 @@ export async function onRequestPost(context) { reportId, source: 'upload', orgId: verificationResult.session.orgId, - memberEmail: verificationResult.session.memberEmail, createdAt, resultsCount: run.results.length, rulesCount: run.tool.driver.rules.length, diff --git a/functions/api/spdx.js b/functions/api/spdx.js index a0dceb2..631aa19 100644 --- a/functions/api/spdx.js +++ b/functions/api/spdx.js @@ -1,3 +1,4 @@ +import { parseSPDXComponents } from "@/finding"; import { AuthResult, OSV, Server, ensureStrReqBody, hex, isSPDX, saveArtifact } from "@/utils"; import { PrismaD1 } from '@prisma/adapter-d1'; import { PrismaClient } from '@prisma/client'; @@ -126,12 +127,30 @@ export async function onRequestPost(context) { const spdxStr = JSON.stringify(spdx) const artifact = await saveArtifact(prisma, env.r2artifacts, spdxStr, crypto.randomUUID(), `spdx`) const artifactUuid = originalSpdx?.artifactUuid || artifact?.uuid + const dependencies = [] + for (const dep of parseSPDXComponents(spdx)) { + const info = await prisma.Dependency.upsert({ + where: { + spdx_dep: { + spdxId, + name: dep.name, + version: dep.version, + } + }, + update: { + license: dep.license, + dependsOnUuid: dep.dependsOnUuid + }, + create: { ...dep, spdxId } + }) + dependencies.push({ ...dep, spdxId }) + console.log(`Dependency ${dep.name}@${dep.version}`, info) + } const spdxData = { spdxId, artifactUuid, source: 'upload', orgId: verificationResult.session.orgId, - memberEmail: verificationResult.session.memberEmail, repoName: '', spdxVersion: spdx.spdxVersion, dataLicense: spdx.dataLicense, @@ -154,6 +173,7 @@ export async function onRequestPost(context) { create: spdxData }) console.log(`/github/repos/spdx ${spdxId} kid=${verificationResult.session.kid}`, info) + spdxData.dependencies = dependencies files.push(spdxData) const osvQueries = spdx.packages.flatMap(pkg => { @@ -181,7 +201,6 @@ export async function onRequestPost(context) { const findingData = { findingId, orgId: verificationResult.session.orgId, - memberEmail: verificationResult.session.memberEmail, source: 'osv.dev', category: 'sca', createdAt: (new Date()).getTime(), diff --git a/functions/api/unresolved.js b/functions/api/unresolved.js index 0a7203d..6fa265b 100644 --- a/functions/api/unresolved.js +++ b/functions/api/unresolved.js @@ -34,9 +34,6 @@ export async function onRequestGet(context) { triage: { every: { analysisState: { in: ['exploitable', 'in_triage'] } } } }, }, - omit: { - memberEmail: true, - }, include: { triage: { orderBy: { diff --git a/migrations/0018_dependencies.sql b/migrations/0018_dependencies.sql index f9c8856..18efbd1 100644 --- a/migrations/0018_dependencies.sql +++ b/migrations/0018_dependencies.sql @@ -14,10 +14,336 @@ CREATE TABLE "Dependency" ( "spdxId" TEXT, "cdxId" TEXT ); +CREATE UNIQUE INDEX "Dependency_name_version_spdxId_key" ON "Dependency"("name", "version", "spdxId"); +CREATE UNIQUE INDEX "Dependency_name_version_cdxId_key" ON "Dependency"("name", "version", "cdxId"); +CREATE TABLE "new_CycloneDXInfo" ( + "cdxId" TEXT NOT NULL PRIMARY KEY, + "source" TEXT NOT NULL, + "orgId" TEXT NOT NULL, + "repoName" TEXT, + "artifactUuid" TEXT NOT NULL, + "cdxVersion" TEXT NOT NULL, + "serialNumber" TEXT, + "name" TEXT, + "version" TEXT, + "createdAt" INTEGER NOT NULL, + "toolName" TEXT +); +INSERT INTO "new_CycloneDXInfo" ( + "artifactUuid", + "cdxId", + "cdxVersion", + "createdAt", + "name", + "orgId", + "repoName", + "serialNumber", + "source", + "toolName", + "version" + ) +SELECT "artifactUuid", + "cdxId", + "cdxVersion", + "createdAt", + "name", + "orgId", + "repoName", + "serialNumber", + "source", + "toolName", + "version" +FROM "CycloneDXInfo"; +DROP TABLE "CycloneDXInfo"; +ALTER TABLE "new_CycloneDXInfo" + RENAME TO "CycloneDXInfo"; +CREATE TABLE "new_Finding" ( + "uuid" TEXT NOT NULL PRIMARY KEY, + "findingId" TEXT NOT NULL, + "orgId" TEXT NOT NULL, + "repoName" TEXT, + "source" TEXT NOT NULL, + "category" TEXT NOT NULL, + "createdAt" INTEGER NOT NULL, + "modifiedAt" INTEGER NOT NULL, + "publishedAt" INTEGER, + "detectionTitle" TEXT NOT NULL, + "detectionDescription" TEXT, + "purl" TEXT, + "cpe" TEXT, + "databaseReviewed" INTEGER, + "aliases" TEXT, + "cwes" TEXT, + "packageName" TEXT NOT NULL, + "packageVersion" TEXT, + "packageLicense" TEXT, + "vendor" TEXT, + "product" TEXT, + "packageEcosystem" TEXT, + "customCvssVector" TEXT, + "customCvssScore" TEXT, + "advisoryUrl" TEXT, + "exploitsJSON" TEXT, + "knownExploitsJSON" TEXT, + "cisaDateAdded" INTEGER, + "knownRansomwareCampaignUse" TEXT, + "fixVersion" TEXT, + "fixAutomatable" INTEGER, + "vulnerableVersionRange" TEXT, + "affectedFunctions" TEXT, + "malicious" INTEGER, + "abandoned" INTEGER, + "squattedPackage" TEXT, + "referencesJSON" TEXT, + "timelineJSON" TEXT, + "confidenceScore" TEXT, + "confidenceLevel" TEXT, + "confidenceRationaleJSON" TEXT, + "spdxId" TEXT, + "cdxId" TEXT +); +INSERT INTO "new_Finding" ( + "abandoned", + "advisoryUrl", + "affectedFunctions", + "aliases", + "category", + "cdxId", + "cisaDateAdded", + "confidenceLevel", + "confidenceRationaleJSON", + "confidenceScore", + "cpe", + "createdAt", + "customCvssScore", + "customCvssVector", + "cwes", + "databaseReviewed", + "detectionDescription", + "detectionTitle", + "exploitsJSON", + "findingId", + "fixAutomatable", + "fixVersion", + "knownExploitsJSON", + "knownRansomwareCampaignUse", + "malicious", + "modifiedAt", + "orgId", + "packageEcosystem", + "packageLicense", + "packageName", + "packageVersion", + "product", + "publishedAt", + "purl", + "referencesJSON", + "repoName", + "source", + "spdxId", + "squattedPackage", + "timelineJSON", + "uuid", + "vendor", + "vulnerableVersionRange" + ) +SELECT "abandoned", + "advisoryUrl", + "affectedFunctions", + "aliases", + "category", + "cdxId", + "cisaDateAdded", + "confidenceLevel", + "confidenceRationaleJSON", + "confidenceScore", + "cpe", + "createdAt", + "customCvssScore", + "customCvssVector", + "cwes", + "databaseReviewed", + "detectionDescription", + "detectionTitle", + "exploitsJSON", + "findingId", + "fixAutomatable", + "fixVersion", + "knownExploitsJSON", + "knownRansomwareCampaignUse", + "malicious", + "modifiedAt", + "orgId", + "packageEcosystem", + "packageLicense", + "packageName", + "packageVersion", + "product", + "publishedAt", + "purl", + "referencesJSON", + "repoName", + "source", + "spdxId", + "squattedPackage", + "timelineJSON", + "uuid", + "vendor", + "vulnerableVersionRange" +FROM "Finding"; +DROP TABLE "Finding"; +ALTER TABLE "new_Finding" + RENAME TO "Finding"; +CREATE TABLE "new_GitHubApp" ( + "installationId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "orgId" TEXT NOT NULL, + "memberEmail" TEXT NOT NULL, + "accessToken" TEXT NOT NULL, + "login" TEXT, + "created" INTEGER NOT NULL, + "expires" INTEGER, + "avatarUrl" TEXT +); +INSERT INTO "new_GitHubApp" ( + "accessToken", + "avatarUrl", + "created", + "expires", + "installationId", + "login", + "memberEmail", + "orgId" + ) +SELECT "accessToken", + "avatarUrl", + "created", + "expires", + "installationId", + "login", + "memberEmail", + ( + SELECT "orgId" + FROM "Member" + WHERE "email" = memberEmail + LIMIT 1 + ) +FROM "GitHubApp"; +DROP TABLE "GitHubApp"; +ALTER TABLE "new_GitHubApp" + RENAME TO "GitHubApp"; +CREATE TABLE "new_GitRepo" ( + "fullName" TEXT NOT NULL, + "orgId" TEXT NOT NULL, + "ghid" INTEGER, + "source" TEXT NOT NULL, + "createdAt" INTEGER NOT NULL, + "updatedAt" INTEGER NOT NULL, + "pushedAt" INTEGER NOT NULL, + "defaultBranch" TEXT NOT NULL, + "ownerId" INTEGER NOT NULL, + "licenseSpdxId" TEXT, + "licenseName" TEXT, + "fork" INTEGER NOT NULL DEFAULT 0, + "template" INTEGER NOT NULL DEFAULT 0, + "archived" INTEGER NOT NULL DEFAULT 0, + "visibility" TEXT NOT NULL, + "avatarUrl" TEXT, + PRIMARY KEY ("fullName", "orgId") +); +INSERT INTO "new_GitRepo" ( + "archived", + "avatarUrl", + "createdAt", + "defaultBranch", + "fork", + "fullName", + "ghid", + "licenseName", + "licenseSpdxId", + "orgId", + "ownerId", + "pushedAt", + "source", + "template", + "updatedAt", + "visibility" + ) +SELECT "archived", + "avatarUrl", + "createdAt", + "defaultBranch", + "fork", + "fullName", + "ghid", + "licenseName", + "licenseSpdxId", + "orgId", + "ownerId", + "pushedAt", + "source", + "template", + "updatedAt", + "visibility" +FROM "GitRepo"; +DROP TABLE "GitRepo"; +ALTER TABLE "new_GitRepo" + RENAME TO "GitRepo"; +CREATE TABLE "new_SARIFInfo" ( + "reportId" TEXT NOT NULL PRIMARY KEY, + "sarifId" TEXT NOT NULL, + "fullName" TEXT, + "orgId" TEXT NOT NULL, + "source" TEXT NOT NULL, + "artifactUuid" TEXT NOT NULL, + "commitSha" TEXT, + "ref" TEXT, + "createdAt" INTEGER NOT NULL, + "resultsCount" INTEGER NOT NULL, + "rulesCount" INTEGER NOT NULL, + "toolName" TEXT, + "toolVersion" TEXT, + "analysisKey" TEXT, + "warning" TEXT +); +INSERT INTO "new_SARIFInfo" ( + "analysisKey", + "artifactUuid", + "commitSha", + "createdAt", + "fullName", + "orgId", + "ref", + "reportId", + "resultsCount", + "rulesCount", + "sarifId", + "source", + "toolName", + "toolVersion", + "warning" + ) +SELECT "analysisKey", + "artifactUuid", + "commitSha", + "createdAt", + "fullName", + "orgId", + "ref", + "reportId", + "resultsCount", + "rulesCount", + "sarifId", + "source", + "toolName", + "toolVersion", + "warning" +FROM "SARIFInfo"; +DROP TABLE "SARIFInfo"; +ALTER TABLE "new_SARIFInfo" + RENAME TO "SARIFInfo"; CREATE TABLE "new_SPDXInfo" ( "spdxId" TEXT NOT NULL PRIMARY KEY, "source" TEXT NOT NULL, - "memberEmail" TEXT NOT NULL, "orgId" TEXT NOT NULL, "repoName" TEXT, "artifactUuid" TEXT NOT NULL, @@ -38,14 +364,14 @@ INSERT INTO "new_SPDXInfo" ( "dataLicense", "documentDescribes", "documentNamespace", - "memberEmail", "name", "orgId", "repoName", "source", "spdxId", "spdxVersion", - "toolName" + "toolName", + "version" ) SELECT "artifactUuid", "comment", @@ -53,16 +379,15 @@ SELECT "artifactUuid", "dataLicense", "documentDescribes", "documentNamespace", - "memberEmail", "name", "orgId", "repoName", "source", "spdxId", "spdxVersion", - "toolName" + "toolName", + "version" FROM "SPDXInfo"; DROP TABLE "SPDXInfo"; ALTER TABLE "new_SPDXInfo" RENAME TO "SPDXInfo"; -ALTER TABLE CycloneDXInfo DROP COLUMN componentsCount; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6512234..9704157 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,6 +33,7 @@ model Org { sarif SARIFInfo[] cdx CycloneDXInfo[] spdx SPDXInfo[] + GitHubApp GitHubApp[] } model IntegrationConfig { @@ -59,16 +60,11 @@ model Member { alertOverdue Int @default(0) alertFindings Int @default(0) alertType Int @default(0) - sarif SARIFInfo[] github_apps GitHubApp[] - repos GitRepo[] member_keys MemberKey[] sessions Session[] - spdx SPDXInfo[] integration_usage_log IntegrationUsageLog[] - findings Finding[] triage_activity Triage[] - cdx CycloneDXInfo[] } model MemberKey { @@ -95,6 +91,8 @@ model GitHubPAT { model GitHubApp { installationId Int @id + orgId String + org Org @relation(fields: [orgId], references: [uuid]) memberEmail String member Member @relation(fields: [memberEmail], references: [email]) accessToken String @@ -115,8 +113,6 @@ model GitRepo { pushedAt Int defaultBranch String ownerId Int - memberEmail String - member Member @relation(fields: [memberEmail], references: [email]) licenseSpdxId String? licenseName String? fork Int @default(0) @@ -140,8 +136,6 @@ model SARIFInfo { orgId String org Org @relation(fields: [orgId], references: [uuid]) source String - memberEmail String - member Member @relation(fields: [memberEmail], references: [email]) artifactUuid String artifact Artifact @relation(fields: [artifactUuid], references: [uuid]) commitSha String? @@ -174,32 +168,27 @@ model SarifResults { } model CycloneDXInfo { - cdxId String @id - source String - memberEmail String - member Member @relation(fields: [memberEmail], references: [email]) - orgId String - org Org @relation(fields: [orgId], references: [uuid]) - repoName String? - repo GitRepo? @relation(fields: [repoName, orgId], references: [fullName, orgId]) - artifactUuid String - artifact Artifact @relation(fields: [artifactUuid], references: [uuid]) - cdxVersion String - serialNumber String? - name String? - version String? - createdAt Int - toolName String? - externalReferencesCount Int - findings Finding[] - dependencies Dependency[] + cdxId String @id + source String + orgId String + org Org @relation(fields: [orgId], references: [uuid]) + repoName String? + repo GitRepo? @relation(fields: [repoName, orgId], references: [fullName, orgId]) + artifactUuid String + artifact Artifact @relation(fields: [artifactUuid], references: [uuid]) + cdxVersion String + serialNumber String? + name String? + version String? + createdAt Int + toolName String? + findings Finding[] + dependencies Dependency[] } model SPDXInfo { spdxId String @id source String - memberEmail String - member Member @relation(fields: [memberEmail], references: [email]) orgId String org Org @relation(fields: [orgId], references: [uuid]) repoName String? @@ -238,6 +227,9 @@ model Dependency { spdx SPDXInfo? @relation(fields: [spdxId], references: [spdxId]) cdxId String? cdx CycloneDXInfo? @relation(fields: [cdxId], references: [cdxId]) + + @@unique([name, version, spdxId], name: "spdx_dep") + @@unique([name, version, cdxId], name: "cdx_dep") } model IntegrationUsageLog { @@ -258,10 +250,8 @@ model Finding { findingId String orgId String org Org @relation(fields: [orgId], references: [uuid]) - memberEmail String - member Member @relation(fields: [memberEmail], references: [email]) repoName String? - repo GitRepo? @relation(fields: [repoName, memberEmail], references: [fullName, orgId]) + repo GitRepo? @relation(fields: [repoName, orgId], references: [fullName, orgId]) source String category String createdAt Int diff --git a/src/components/DependencyGraph.vue b/src/components/DependencyGraph.vue new file mode 100644 index 0000000..b59169e --- /dev/null +++ b/src/components/DependencyGraph.vue @@ -0,0 +1,43 @@ + + diff --git a/src/components/Finding.vue b/src/components/Finding.vue index bd713ed..e1fcbd3 100644 --- a/src/components/Finding.vue +++ b/src/components/Finding.vue @@ -13,7 +13,6 @@ import { CVSS31, CVSS40 } from '@pandatix/js-cvss'; import VCodeBlock from '@wdns/vue-code-block'; import { onMounted } from 'vue'; import { useTheme } from 'vuetify'; -import { VAlert } from 'vuetify/lib/components/index.mjs'; const { meta_x, meta_v } = useMagicKeys() onMounted(() => init()) @@ -688,1963 +687,1903 @@ watch([ class="pa-0" v-if="props.finding" > - - - - - -
- {{ props.finding.detectionTitle }} + + + +
+ {{ props.finding.detectionTitle }} + + {{ VexAnalysisState[props.currentTriage.analysisState] }} + + + {{ VexAnalysisResponse[props.currentTriage.analysisResponse] }} + + + {{ VexAnalysisJustification[props.currentTriage.analysisJustification] }} + +
+
+ + + + + + + + + + + + + + + + + + + + + *indicates required + field + + + + + + + + + + + + + +
+
+ + + + + + + + + + Package: {{ packageString }} + + + + + + + {{ props.finding.vendor }} {{ props.finding.product }} + + + + + + + CPE: {{ props.finding.cpe }} + + + + + + +
- {{ VexAnalysisState[props.currentTriage.analysisState] }} + {{ alias }} + - {{ VexAnalysisResponse[props.currentTriage.analysisResponse] }} + {{ cwe }} + - {{ VexAnalysisJustification[props.currentTriage.analysisJustification] }} + Malicious Package +
-
- - + + {{ alias }} + + + + + + + Download CycloneDX Artifact + {{ [props.finding.cdx.name, + props.finding.cdx?.version].filter(a => !!a).join('@') }} + - + + + Download SPDX Artifact + {{ [props.finding.spdx.name, + props.finding.spdx?.version].filter(a => !!a).join('@') }} + - - - - - - *indicates required - field - - - - - - - - - - - - - -
- - - - - - - - + - - Package: {{ packageString }} - + + Open {{ props.finding.repoSource }} in a new tab + + {{ props.finding.repoName }} - + + - - {{ props.finding.vendor }} {{ props.finding.product }} - + Open NVD Calculator in a new tab + + {{ cvssVectorString }} - + + - - CPE: {{ props.finding.cpe }} - + Exploit Prediction Scoring System - - - -
- - {{ alias }} - - - - {{ cwe }} - - - - Malicious Package - - -
-
- - - + + + + + + - - - - - - {{ [props.finding.cdx.name, props.finding.cdx?.version].filter(a => - !!a).join('@') }} - - - - - - - - - Purl: {{ props.finding.purl }} - - diff --git a/src/finding.js b/src/finding.js index c22a273..6788738 100644 --- a/src/finding.js +++ b/src/finding.js @@ -822,3 +822,212 @@ export const evaluateAdvisoryConfidence = advisory => { evaluations, }; } + +// Helper function to create PURL from download location +export function createPurlFromUrl(location, name, version) { + if (!location || location === 'NOASSERTION') return null + + try { + const url = new URL(location.replace(/^git\+/, '')) + const [owner, repo] = url.pathname.split('/').filter(Boolean) + + // If it's a GitHub URL + if (url.hostname === 'github.com') { + return `pkg:github/${owner}/${repo}@${version}` + } + + // For git URLs without specific host + if (location.startsWith('git://')) { + return `pkg:git/${owner}/${repo}@${version}` + } + + // Generic URL + return `pkg:generic/${name}@${version}?download_url=${encodeURIComponent(location)}` + + } catch (e) { + // If URL parsing fails, return null + return null + } +} + +// Helper function to extract ecosystem from purl +export function getEcosystemFromPurl(purl) { + if (!purl) return null + const match = purl.match(/pkg:([^/]+)/) + return match ? match[1] : null +} + +// Helper function to extract license from SPDX package +export function extractLicense(pkg) { + if (pkg?.licenseDeclared) { + return pkg.licenseDeclared + } + if (pkg?.licenseInfoFromFiles && pkg.licenseInfoFromFiles.length > 0) { + return pkg.licenseInfoFromFiles.join(',') + } + + return null +} + +// Helper function to extract name and version from SPDXID +export function parsePackageRef(spdxId, name) { + // Format is typically "SPDXRef-name-version" + const match = spdxId.match(/SPDXRef-(.+)-([0-9].+)$/) + if (!match) return { + name, + version: spdxId.replace(`SPDXRef-${name}-`, '') + } + + return { + name: match[1], + version: match[2] + } +} + +export function parseSPDXComponents(spdxJson) { + const components = new Map() + const relationships = [] + const packageEcosystem = 'generic' // TODO: There may be a SPDX solution + + // First pass: Create all component records + spdxJson.packages.forEach(pkg => { + const parsedRef = parsePackageRef(pkg.SPDXID); + if (!parsedRef) return; + + const { name, version } = parsedRef; + const license = extractLicense(pkg) + + components.set(pkg.SPDXID, { + uuid: crypto.randomUUID(), + name, + version, + license, + packageEcosystem, + isTransitive: 1, // Default all to transitive + isDirect: 0, // We'll update direct deps later + }) + }) + + // Second pass: Process relationships + if (spdxJson.relationships) { + spdxJson.relationships.forEach(rel => { + // Only process DEPENDS_ON relationships + if (rel.relationshipType !== 'DEPENDS_ON') return; + + const sourceComp = components.get(rel.spdxElementId) + const targetComp = components.get(rel.relatedSpdxElement) + + if (!sourceComp || !targetComp) return; + + // If this is a dependency of the root package, mark as direct + if (sourceComp.name === spdxJson.name) { + targetComp.isDirect = 1 + targetComp.isTransitive = 0 + } + + relationships.push({ + uuid: crypto.randomUUID(), + dependsOnUuid: targetComp.uuid, + name: targetComp.name, + version: targetComp.version, + license: targetComp.license, + packageEcosystem: targetComp.packageEcosystem, + isTransitive: targetComp.isTransitive, + isDirect: targetComp.isDirect, + }) + }) + } + + return relationships +} + +export function parseCycloneDXComponents(cdxJson) { + const components = new Map() + const dependencies = [] + const rootPackage = cdxJson.metadata.component + + // First pass: Create all component records + cdxJson.components.forEach(component => { + if (!component?.['bom-ref']) { + console.log('bom-ref missing', component) + return; + } + + const packageEcosystem = getEcosystemFromPurl(component.purl) + const name = component?.group ? [component.group, component.name].join('/') : component.name + const license = component.licenses.filter(l => l?.license?.id).map(l => l.license.id).join(',') + + components.set(component['bom-ref'], { + uuid: crypto.randomUUID(), + name, + version: component.version, + license, + packageEcosystem, + isTransitive: 1, // Default all to transitive + isDirect: 0, // We'll update direct deps later + }) + }) + + // Add root package if not already present + const rootRef = rootPackage['bom-ref'] + if (!components.has(rootRef)) { + components.set(rootRef, { + uuid: crypto.randomUUID(), + name: rootPackage.name, + version: rootPackage.version, + license: null, + packageEcosystem: getEcosystemFromPurl(rootPackage.purl), + isTransitive: 0, + isDirect: 0, + }) + } + + // Second pass: Process dependencies + cdxJson.dependencies.forEach(dep => { + const parentRef = dep.ref + const parentComponent = components.get(parentRef) + + if (!parentComponent) return; + + // If this is the root package's dependencies, mark them as direct + if (parentRef === rootRef) { + dep.dependsOn?.forEach(childRef => { + const childComponent = components.get(childRef) + if (childComponent) { + childComponent.isDirect = 1 + childComponent.isTransitive = 0 + + dependencies.push({ + uuid: crypto.randomUUID(), + dependsOnUuid: childComponent.uuid, + name: childComponent.name, + version: childComponent.version, + license: childComponent.license, + packageEcosystem: childComponent.packageEcosystem, + isTransitive: 0, + isDirect: 1, + }) + } + }) + } else { + // Process transitive dependencies + dep.dependsOn?.forEach(childRef => { + const childComponent = components.get(childRef); + if (childComponent) { + dependencies.push({ + uuid: crypto.randomUUID(), + dependsOnUuid: childComponent.uuid, + name: childComponent.name, + version: childComponent.version, + license: childComponent.license, + packageEcosystem: childComponent.packageEcosystem, + isTransitive: 1, + isDirect: 0, + }) + } + }) + } + }) + + return dependencies +} diff --git a/src/pages/Artifacts.vue b/src/pages/Artifacts.vue index 6f5d62a..17816c3 100644 --- a/src/pages/Artifacts.vue +++ b/src/pages/Artifacts.vue @@ -154,17 +154,24 @@ class Controller { if (isSARIF(json)) { sarif.push(json) } - } catch (e) { } + } catch (e) { + console.log(json, e) + } try { if (isSPDX(json)) { spdx.push(json) } - } catch (e) { } + } catch (e) { + console.log(json, e) + } try { if (isCDX(json)) { cdx.push(json) } - } catch (e) { } + } catch (e) { + console.log(json, e) + // state.uploadError = typeof e === "string" ? e : `${e.code} ${e.message}` + } } } let success = false @@ -492,7 +499,7 @@ function updateArtifactsFromFiles(files) {