Skip to content

Commit

Permalink
Dependencies (#75)
Browse files Browse the repository at this point in the history
* feat: dependency list
* feat: add middleware to do authentication implicitly
* feat: improved findings page details
* feat: Add syntax highlight
* feat: Add foldable text component
* feat: Add string output sanitisation
* feat: Add  markdown support
* feat: import repos and branches from any page
* feat: git branches

---------

Co-authored-by: Christopher Langton <[email protected]>
  • Loading branch information
0x73746F66 and chrisdlangton authored Nov 25, 2024
1 parent f9eb76a commit 725af44
Show file tree
Hide file tree
Showing 60 changed files with 2,242 additions and 1,828 deletions.
26 changes: 16 additions & 10 deletions .repo/scratchad.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
-- SELECT *
-- FROM Triage
-- WHERE findingUuid = "c00f2661-a1fc-4a72-83af-5894580ee510";
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";
--
-- DELETE FROM Finding;
-- DELETE FROM Triage;
-- DELETE FROM GitRepo;
-- DELETE FROM SARIFInfo;
-- DELETE FROM SarifResults;
-- DELETE FROM CycloneDXInfo;
-- DELETE FROM SPDXInfo;
-- DELETE FROM IntegrationUsageLog;
-- DELETE FROM Link;
-- DELETE FROM Artifact;
-- DELETE FROM Dependency;
--
SELECT *
FROM `Triage`
WHERE remediation IS NOT NULL;
167 changes: 167 additions & 0 deletions functions/_middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
AuthResult,
Client,
ensureStrReqBody,
hexStringToUint8Array,
isJSON,
unauthenticatedRoutes,
} from "@/utils";
import { PrismaD1 } from '@prisma/adapter-d1';
import { PrismaClient } from '@prisma/client';

export const errorHandling = async 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 {
return await next()
} catch (err) {
console.error(err.message, err.stack)
return new Response(err.message, { status: 500 })
}
}

// Respond to OPTIONS method
export const onRequestOptions = async () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Max-Age': '86400',
},
})
}

export const setup = async context => {
const { request, env, data, next } = context
const adapter = new PrismaD1(env.d1db)
const { searchParams } = new URL(request.url)
data.searchParams = searchParams
if (["POST", "PUT", "DELETE", "PATCH"].includes(request.method.toUpperCase())) {
data.body = await ensureStrReqBody(request)
if (isJSON(data.body)) {
data.json = JSON.parse(data.body)
}
}
const clientOptions = {
adapter,
transactionOptions: {
maxWait: 1500, // default: 2000
timeout: 2000, // default: 5000
},
}
data.logger = () => { }
if (env.LOGGER === "DEBUG") {
data.logger = console.log
clientOptions.log = [
{
emit: "event",
level: "query",
},
]
}
data.prisma = new PrismaClient(clientOptions)
data.prisma.$on("query", async e => {
data.logger(`${e.query} ${e.params}`)
})

return await next()
}

export const authentication = async context => {
const { request, next, data } = context
const url = new URL(request.url)
if (request.cf.botManagement.verifiedBot || request.cf.botManagement.score <= 60) {
return new Response(JSON.stringify({ ok: false, error: { message: AuthResult.FORBIDDEN } }), { status: 403 })
}
const authRequired =
!unauthenticatedRoutes.static.includes(url.pathname) &&
!unauthenticatedRoutes.prefixes.map(i => url.pathname.startsWith(i)).includes(true)

if (!authRequired || !url.pathname.startsWith('/api/')) {
return await next()
}
const method = request.method.toUpperCase()
const path = url.pathname + url.search
const body = ['GET', 'DELETE'].includes(method.toUpperCase()) ? '' : await ensureStrReqBody(request)
// Retrieve signature and timestamp from headers
const signature = request.headers.get('authorization')?.replace('HMAC ', '')
const timestampStr = request.headers.get('x-timestamp')
const kid = request.headers.get('x-vulnetix-kid')

if (!signature || !timestampStr || !kid) {
return new Response(JSON.stringify({ ok: false, error: { message: AuthResult.FORBIDDEN } }), { status: 403 })
}

// Convert timestamp from string to integer
const timestamp = parseInt(timestampStr, 10)
if (isNaN(timestamp)) {
data.logger('Invalid timestamp format', timestamp)
return new Response(JSON.stringify({ ok: false, error: { message: AuthResult.FORBIDDEN } }), { status: 403 })
}
// Validate timestamp (you may want to add a check to ensure the request isn't too old)
const currentTimestamp = new Date().getTime()
if (Math.abs(currentTimestamp - timestamp) > 3e+5) { // e.g., allow a 5-minute skew
data.logger('expired, skew', timestamp)
return new Response(JSON.stringify({ ok: false, error: { message: AuthResult.EXPIRED } }), { status: 401 })
}
// Retrieve the session key from the database using Prisma
const session = await data.prisma.Session.findFirstOrThrow({
where: { kid }
})
if (!session.expiry || session.expiry <= new Date().getTime()) {
data.logger('expired', timestamp)
return new Response(JSON.stringify({ ok: false, error: { message: AuthResult.EXPIRED } }), { status: 401 })
}
const secretKeyBytes = new TextEncoder().encode(session.secret)
const payloadBytes = Client.makePayload({
method,
path,
kid,
timestamp,
body: encodeURIComponent(body)
})
const key = await crypto.subtle.importKey(
"raw",
secretKeyBytes,
{ name: "HMAC", hash: "SHA-512" },
false,
["verify"]
)

const signatureBytes = hexStringToUint8Array(signature)
const isValid = await crypto.subtle.verify("HMAC", key, signatureBytes, payloadBytes)

if (!isValid) {
data.logger('Invalid signature', signature)
return new Response(JSON.stringify({ ok: false, error: { message: AuthResult.FORBIDDEN } }), { status: 401 })
}
data.session = session

return await next()
}

// Set CORS to all /api responses
const dynamicHeaders = async 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
const response = await next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Max-Age', '86400')
return response
}

export const onRequest = [errorHandling, setup, authentication, dynamicHeaders]
20 changes: 0 additions & 20 deletions functions/_middleware.ts

This file was deleted.

27 changes: 6 additions & 21 deletions functions/api/[analysisState]/issues.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AuthResult, Server } from "@/utils";
import { PrismaD1 } from '@prisma/adapter-d1';
import { PrismaClient } from '@prisma/client';
import { AuthResult } from "@/utils";

export async function onRequestGet(context) {
const {
Expand All @@ -12,25 +10,12 @@ export async function onRequestGet(context) {
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 analysisState = searchParams.get('analysisState') || 'in_triage'
const take = parseInt(searchParams.get('take'), 10) || 50
const skip = parseInt(searchParams.get('skip'), 10) || 0
const findings = await prisma.Finding.findMany({
const analysisState = data.searchParams.get('analysisState') || 'in_triage'
const take = parseInt(data.searchParams.get('take'), 10) || 50
const skip = parseInt(data.searchParams.get('skip'), 10) || 0
const findings = await data.prisma.Finding.findMany({
where: {
orgId: verificationResult.session.orgId,
orgId: data.session.orgId,
AND: {
triage: {
every: { analysisState }
Expand Down
34 changes: 9 additions & 25 deletions functions/api/[source]/integration.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AuthResult, Server, ensureStrReqBody } from "@/utils";
import { PrismaD1 } from '@prisma/adapter-d1';
import { PrismaClient } from '@prisma/client';
import { AuthResult } from "@/utils";

export async function onRequestPost(context) {
const {
Expand All @@ -15,45 +13,31 @@ export async function onRequestPost(context) {
if (!['osv', 'first', 'vulncheck', 'github', 'mitre-cve'].includes((params?.source || '').toLowerCase())) {
return Response.json({ ok: false, error: { message: `Invalid log source` }, results: [] })
}
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 bodyStr = await ensureStrReqBody(request)
const data = JSON.parse(bodyStr)
const where = {
orgId: verificationResult.session.orgId,
orgId: data.session.orgId,
AND: { name: params.source },
}
const original = await prisma.IntegrationConfig.findFirst({ where })
const original = await data.prisma.IntegrationConfig.findFirst({ where })
if (original === null) {
const info = await prisma.IntegrationConfig.create({
const info = await data.prisma.IntegrationConfig.create({
data: {
orgId: verificationResult.session.orgId,
orgId: data.session.orgId,
name: params.source,
created: new Date().getTime(),
suspend: data?.suspend === undefined ? 0 : (data.suspend ? 1 : 0),
suspend: data.json?.suspend === undefined ? 0 : (data.json.suspend ? 1 : 0),
}
})
return Response.json({ ok: true, info })
}
if ((data?.suspend ? 1 : 0) === original.suspend) {
if ((data.json?.suspend ? 1 : 0) === original.suspend) {
return Response.json({ ok: false, result: 'No Change' })
}
const info = await prisma.IntegrationConfig.update({
const info = await data.prisma.IntegrationConfig.update({
where: {
uuid: original.uuid
},
data: {
suspend: data?.suspend === undefined ? original.suspend : (data.suspend ? 1 : 0),
suspend: data.json?.suspend === undefined ? original.suspend : (data.json.suspend ? 1 : 0),
}
})
return Response.json({ ok: true, info })
Expand Down
25 changes: 5 additions & 20 deletions functions/api/[source]/log.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AuthResult, Server } from "@/utils";
import { PrismaD1 } from '@prisma/adapter-d1';
import { PrismaClient } from '@prisma/client';
import { AuthResult } from "@/utils";

export async function onRequestGet(context) {
const {
Expand All @@ -15,24 +13,11 @@ export async function onRequestGet(context) {
if (!['osv', 'first', 'vulncheck', 'github', 'mitre-cve'].includes((params?.source || '').toLowerCase())) {
return Response.json({ ok: false, error: { message: `Invalid log source` }, results: [] })
}
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, results: [] })
}
const { searchParams } = new URL(request.url)
const take = parseInt(searchParams.get('take'), 10) || 50
const skip = parseInt(searchParams.get('skip'), 10) || 0
let results = await prisma.IntegrationUsageLog.findMany({
const take = parseInt(data.searchParams.get('take'), 10) || 50
const skip = parseInt(data.searchParams.get('skip'), 10) || 0
let results = await data.prisma.IntegrationUsageLog.findMany({
where: {
orgId: verificationResult.session.orgId,
orgId: data.session.orgId,
source: params?.source,
},
take,
Expand Down
20 changes: 3 additions & 17 deletions functions/api/analytics.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AuthResult, Server } from "@/utils";
import { PrismaD1 } from '@prisma/adapter-d1';
import { PrismaClient } from '@prisma/client';
import { AuthResult } from "@/utils";
import { ActionCISA, ActionFIRST } from "ssvc";

export async function onRequestGet(context) {
Expand All @@ -13,22 +11,10 @@ export async function onRequestGet(context) {
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 findings = await prisma.Finding.findMany({
const findings = await data.prisma.Finding.findMany({
where: {
orgId: verificationResult.session.orgId,
orgId: data.session.orgId,
},
include: {
triage: true,
Expand Down
Loading

0 comments on commit 725af44

Please sign in to comment.