From 195cb270e8b1994c3b266c4ed9123d00aa349c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Groman?= <32076344+Matej4545@users.noreply.github.com> Date: Sun, 19 Mar 2023 12:29:30 +0100 Subject: [PATCH] Db version representation (#10) * Update Neo4J schema to contain info about vulnerable components * Update Neo4J schema to contain info about vulnerable components * FIltering works * New gitignore * WIP: Better import * remove file * Add CORS support * Iproved import flow * ImportForm - define name and version * WIP: Better import remove file Add CORS support Iproved import flow ImportForm - define name and version * Fix import API endpoint * WIP: Merge with filtering * Exclude local .env files * WIP: - upload shows progress bar - more resiliant upload form - upload gets its own snug id - project can be selected by URL - minor visual changes * Create DB contstraints * Introduce ProjectVersion type in DB * Update types * WIP: rewrite DBDataHelper * WIP: Rewrite import * Update types * WIP: continue with rewrite * WIP: Fix some bugs with submission * WIP: Can create components * WIP: probably broken * Working * Working import + vulnerabilities * WIP: working upload + visualization * Graph cleanup * FInalize import - progress bar * Colors improvement * Vulnerability detail improvement * Link Improvement * Different endpoint for FE /BE --------- Co-authored-by: Matej Groman --- .gitignore | 6 +- docker-compose.yml | 36 +- src/depvis-next/.dockerignore | 7 +- src/depvis-next/.env.production | 13 - src/depvis-next/.vscode/launch.json | 2 +- .../components/Details/ComponentDetails.tsx | 26 +- .../Details/VulnerabilityDetails.tsx | 65 ++- .../components/Dropdown/Dropdown.tsx | 37 +- .../components/Error/NoProjectFoundError.tsx | 11 + .../components/Graph/GraphConfig.tsx | 17 +- .../components/Graph/NoSSRGraph.tsx | 100 +---- .../components/GraphControl/GraphControl.tsx | 17 +- .../components/Import/ImportForm.tsx | 151 ++++--- .../components/Import/ImportResult.tsx | 79 ++-- src/depvis-next/components/Import/types.ts | 5 + .../components/Layout/GraphContainer.tsx | 20 +- .../components/Layout/Workspace.tsx | 141 ------ .../components/NavBar/MainNavBar.tsx | 9 +- .../components/Workspace/ProjectSelector.tsx | 100 +++++ .../Workspace/ProjectVersionSelector.tsx | 64 +++ .../components/Workspace/Workspace.tsx | 176 ++++++++ src/depvis-next/helpers/ApolloClientHelper.ts | 17 +- src/depvis-next/helpers/BatchHelper.ts | 32 +- src/depvis-next/helpers/DbDataHelper.ts | 213 +++++++--- src/depvis-next/helpers/DbDataProvider.ts | 308 ++++++++++++++ src/depvis-next/helpers/GraphHelper.ts | 125 ++++-- src/depvis-next/helpers/ImportSbomHelper.ts | 356 +++++++++++++--- src/depvis-next/helpers/QueueHelper.ts | 34 +- src/depvis-next/helpers/WorkspaceHelper.ts | 35 +- src/depvis-next/package-lock.json | 402 ++++++++++-------- src/depvis-next/package.json | 2 +- src/depvis-next/pages/api/graphql.ts | 52 ++- src/depvis-next/pages/api/import/index.ts | 129 ++++-- src/depvis-next/pages/api/import/status.ts | 51 ++- src/depvis-next/pages/api/vuln/[...purl].ts | 16 - src/depvis-next/pages/api/vuln/index.ts | 12 +- src/depvis-next/pages/index.tsx | 2 +- src/depvis-next/pages/toolbox.tsx | 38 +- src/depvis-next/pages/upload.tsx | 62 ++- src/depvis-next/queues/GetVulnQueue.ts | 36 +- src/depvis-next/queues/ImportQueue.ts | 51 ++- src/depvis-next/styles/custom.css | 4 + src/depvis-next/types/colorPalette.ts | 8 + src/depvis-next/types/component.ts | 23 +- src/depvis-next/types/gqlTypes.ts | 51 ++- src/depvis-next/types/project.ts | 29 +- src/depvis-next/types/vulnerability.ts | 4 +- 47 files changed, 2269 insertions(+), 905 deletions(-) delete mode 100644 src/depvis-next/.env.production create mode 100644 src/depvis-next/components/Error/NoProjectFoundError.tsx create mode 100644 src/depvis-next/components/Import/types.ts delete mode 100644 src/depvis-next/components/Layout/Workspace.tsx create mode 100644 src/depvis-next/components/Workspace/ProjectSelector.tsx create mode 100644 src/depvis-next/components/Workspace/ProjectVersionSelector.tsx create mode 100644 src/depvis-next/components/Workspace/Workspace.tsx create mode 100644 src/depvis-next/helpers/DbDataProvider.ts delete mode 100644 src/depvis-next/pages/api/vuln/[...purl].ts create mode 100644 src/depvis-next/types/colorPalette.ts diff --git a/.gitignore b/.gitignore index cd62e76..c6c16d4 100644 --- a/.gitignore +++ b/.gitignore @@ -73,11 +73,12 @@ web_modules/ .yarn-integrity # dotenv environment variable files -.env +.env* .env.development.local .env.test.local .env.production.local .env.local +.env* # parcel-bundler cache (https://parceljs.org/) .cache @@ -134,4 +135,5 @@ dist /runtime -*no_git* \ No newline at end of file +*no_git* + diff --git a/docker-compose.yml b/docker-compose.yml index 8993394..f2812dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ -version: '3' +version: "3" services: neo4j: - image: neo4j:4.4.7 + image: neo4j:latest ports: - 7474:7474 - 7687:7687 @@ -12,7 +12,7 @@ services: - ./runtime/plugins:/var/lib/neo4j/plugins restart: unless-stopped environment: - - NEO4J_AUTH=neo4j/test + - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD} - NEO4J_apoc_export_file_enabled=true - NEO4J_apoc_import_file_enabled=true - NEO4J_apoc_import_file_use__neo4j__config=true @@ -20,13 +20,29 @@ services: - NEO4J_dbms_security_procedures_unrestricted=apoc.*,algo.* redis-cache: - image: redis:alpine + image: redis:latest + command: redis-server --requirepass ${REDIS_PASSWORD} restart: unless-stopped - ports: - - 6379:6379 - command: redis-server --save 20 1 --loglevel notice volumes: - cache:/data + ports: + - 6379:6379 + links: + - redis-commander + + redis-commander: + image: rediscommander/redis-commander:latest + restart: unless-stopped + environment: + REDIS_HOSTS: redis-cache + REDIS_HOST: redis-cache + REDIS_PORT: redis-cache:6379 + REDIS_PASSWORD: ${REDIS_PASSWORD} + HTTP_USER: root + HTTP_PASSWORD: root + ports: + - 8081:8081 + depvis-next: depends_on: - neo4j @@ -34,8 +50,12 @@ services: build: ./src/depvis-next image: depvis-next:latest ports: - - 80:3000 + - 8123:3000 restart: always + environment: + NEO4J_PASSWORD: ${NEO4J_PASSWORD} + REDIS_PASSWORD: ${REDIS_PASSWORD} + NEXT_PUBLIC_SONATYPE_OSS_AUTH: ${SONATYPE_OSS_AUTH} volumes: cache: driver: local diff --git a/src/depvis-next/.dockerignore b/src/depvis-next/.dockerignore index 72e9aa4..bfe7df6 100644 --- a/src/depvis-next/.dockerignore +++ b/src/depvis-next/.dockerignore @@ -4,4 +4,9 @@ node_modules npm-debug.log README.md .next -.git \ No newline at end of file +.git + +.env.development.local +.env.test.local +.env.production.local +.env.local diff --git a/src/depvis-next/.env.production b/src/depvis-next/.env.production deleted file mode 100644 index 094f4e4..0000000 --- a/src/depvis-next/.env.production +++ /dev/null @@ -1,13 +0,0 @@ -# Sample .env file that can be used for deployment via docker -# Do not forget to change passwords to correspond with docker-compose file -NEO4J_USER=neo4j -NEO4J_PASSWORD= -NEO4J_HOST=neo4j://neo4j:7687 - -NEXT_PUBLIC_SONATYPE_OSS_AUTH= - -GQL_ALLOW_DEV_TOOLS=true - -REDIS_HOST=redis-cache -REDIS_PORT=6379 -REDIS_PASSWORD= \ No newline at end of file diff --git a/src/depvis-next/.vscode/launch.json b/src/depvis-next/.vscode/launch.json index 2065274..943b870 100644 --- a/src/depvis-next/.vscode/launch.json +++ b/src/depvis-next/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Next.js: debug server-side", "type": "node-terminal", "request": "launch", - "command": "npm run dev" + "command": "npm run dev --trace-warnings" }, { "name": "Next.js: debug client-side", diff --git a/src/depvis-next/components/Details/ComponentDetails.tsx b/src/depvis-next/components/Details/ComponentDetails.tsx index 836870d..a89fb49 100644 --- a/src/depvis-next/components/Details/ComponentDetails.tsx +++ b/src/depvis-next/components/Details/ComponentDetails.tsx @@ -1,12 +1,14 @@ -import { gql, useQuery } from '@apollo/client'; -import { Container } from 'react-bootstrap'; -import { GetComponentRepositoryURL } from '../../helpers/WorkspaceHelper'; -import Loading from '../Loading/Loading'; -import { DL, DLItem } from './DescriptionList'; +import { gql, useQuery } from "@apollo/client"; +import { Container } from "react-bootstrap"; +import { GetComponentRepositoryURL } from "../../helpers/WorkspaceHelper"; +import Loading from "../Loading/Loading"; +import { DL, DLItem } from "./DescriptionList"; const getComponentDetailsQuery = gql` query componentDetails($componentPurl: String, $projectId: ID) { - components(where: { purl: $componentPurl, project_SINGLE: { id: $projectId } }) { + components( + where: { purl: $componentPurl, projectVersion: { id: $projectId } } + ) { name purl author @@ -27,6 +29,7 @@ const ComponentDetails = (props) => { const renderLink = () => { const link = GetComponentRepositoryURL(data.components[0].purl); + if (!link) return; return ( {link} @@ -35,13 +38,15 @@ const ComponentDetails = (props) => { }; if (loading) return ; if (!data.components[0]) { - console.error('No data found when querying backend! Below is Apollo query result'); + console.error( + "No data found when querying backend! Below is Apollo query result" + ); console.error({ data: data, error: error }); return No data found!; } const component = data.components[0]; return ( - +

{component.name}

@@ -50,7 +55,10 @@ const ComponentDetails = (props) => { - + ( diff --git a/src/depvis-next/components/Details/VulnerabilityDetails.tsx b/src/depvis-next/components/Details/VulnerabilityDetails.tsx index 2b2f6dd..6ec8823 100644 --- a/src/depvis-next/components/Details/VulnerabilityDetails.tsx +++ b/src/depvis-next/components/Details/VulnerabilityDetails.tsx @@ -1,7 +1,10 @@ -import { gql, useQuery } from '@apollo/client'; -import { Badge, Container } from 'react-bootstrap'; -import Loading from '../Loading/Loading'; -import { DL, DLItem } from './DescriptionList'; +import { gql, useQuery } from "@apollo/client"; +import Link from "next/link"; +import { Badge, Container } from "react-bootstrap"; +import urlJoin from "url-join"; +import { vulnerabilityColorByCVSS } from "../../helpers/GraphHelper"; +import Loading from "../Loading/Loading"; +import { DL, DLItem } from "./DescriptionList"; const getVulnerabilityDetailsQuery = gql` query vulnerabilityDetails($vulnerabilityId: String) { @@ -38,31 +41,71 @@ const VulnerabilityDetails = (props) => { )); }; + const getCvssScoreLabel = (cvssScore) => { + if (cvssScore >= 9) return "Critical"; + if (cvssScore >= 7) return "High"; + if (cvssScore >= 4) return "Medium"; + return "Low"; + }; const renderCvss = (cvssScore) => { - return 5 ? 'danger' : 'warning'}>{cvssScore}; + return ( + + {cvssScore} - {getCvssScoreLabel(cvssScore)} + + ); + }; + + const renderCvssVectorAsLink = (cvssVector) => { + const cvssPortalUrl = "https://www.first.org/cvss/calculator/3.1"; + const url = `${cvssPortalUrl}#${cvssVector}`; + return ( +
+ {cvssVector} + + ); }; if (loading) return ; if (!data.vulnerabilities[0]) { - console.error('No data found when querying backend! Below is Apollo query result'); + console.error( + "No data found when querying backend! Below is Apollo query result" + ); console.error({ data: data, error: error }); return No data found!; } const vulnerability = data.vulnerabilities[0]; return ( - +

{vulnerability.name}

- - + + - + - +
); diff --git a/src/depvis-next/components/Dropdown/Dropdown.tsx b/src/depvis-next/components/Dropdown/Dropdown.tsx index 85b90b2..34671bf 100644 --- a/src/depvis-next/components/Dropdown/Dropdown.tsx +++ b/src/depvis-next/components/Dropdown/Dropdown.tsx @@ -1,24 +1,37 @@ -import { useState } from 'react'; -import { Container, Form } from 'react-bootstrap'; +import { useEffect, useState } from "react"; +import { Container, Form } from "react-bootstrap"; +export type DropdownItem = { + displayName: string; + id: string; +}; + +/** + * Dropdown component, renders set of options and fires a callback on change + * @param props one of title, onChange, defaultValue, options, disabled + * @returns + */ export default function Dropdown(props) { - const [selected, setSelected] = useState(props.default); + const { title, onChange, defaultValue, options, disabled } = props; + const [selectedId, setSelectedId] = useState(defaultValue); return (
- {props.title && {props.title}} + {title && {title}} { - props.onChange(e.target.value); - setSelected(e.target.value); + setSelectedId(e.target.value); + onChange(e.target.value); }} > - {props.options.map((v, i) => ( - - ))} + {options && + options.map((v: DropdownItem, i: number) => ( + + ))}
diff --git a/src/depvis-next/components/Error/NoProjectFoundError.tsx b/src/depvis-next/components/Error/NoProjectFoundError.tsx new file mode 100644 index 0000000..879120e --- /dev/null +++ b/src/depvis-next/components/Error/NoProjectFoundError.tsx @@ -0,0 +1,11 @@ +import Link from 'next/link'; +import { Container } from 'react-bootstrap'; + +export default function NoProjectFoundError(props) { + return ( + +

No project found!

+ Upload SBOM +
+ ); +} diff --git a/src/depvis-next/components/Graph/GraphConfig.tsx b/src/depvis-next/components/Graph/GraphConfig.tsx index f27e2fa..50f69b7 100644 --- a/src/depvis-next/components/Graph/GraphConfig.tsx +++ b/src/depvis-next/components/Graph/GraphConfig.tsx @@ -1,9 +1,10 @@ export type GraphConfig = { - zoomLevel: number - color: string | Function, - label: string | Function, - linkDirectionalArrowLength: number, - linkDirectionalRelPos: number - nodeVal: number | Function - linkLength: number -} \ No newline at end of file + zoomLevel: number; + color: string | Function; + label: string | Function; + linkDirectionalArrowLength: number; + linkDirectionalRelPos: number; + nodeVal: number | Function; + linkLength: number; + showOnlyVulnerable: Boolean; +}; diff --git a/src/depvis-next/components/Graph/NoSSRGraph.tsx b/src/depvis-next/components/Graph/NoSSRGraph.tsx index 5434b26..3875db5 100644 --- a/src/depvis-next/components/Graph/NoSSRGraph.tsx +++ b/src/depvis-next/components/Graph/NoSSRGraph.tsx @@ -1,6 +1,6 @@ -import { forceCollide, forceManyBody, forceX, forceY } from 'd3-force'; -import { useCallback, useEffect, useRef } from 'react'; -import ReactForceGraph2d, { ForceGraphMethods } from 'react-force-graph-2d'; +import { forceCollide, forceManyBody, forceX, forceY } from "d3-force"; +import { useEffect, useRef } from "react"; +import ReactForceGraph2d, { ForceGraphMethods } from "react-force-graph-2d"; export default function NoSSRGraph(props) { const graphRef = useRef(); @@ -9,97 +9,23 @@ export default function NoSSRGraph(props) { const r = graphRef.current; if (props.selectedNode) { console.log(props.selectedNode); - r.centerAt(props.selectedNode.x, props.selectedNode.y, 500); - const zoom_level = 50 / (props.selectedNode.dependsOnCount || 30); - graphRef.current.zoom(zoom_level, 500); + r.centerAt(props.selectedNode.x, props.selectedNode.y, 1000); } r.d3Force( - 'collide', + "collide", forceCollide(10) .radius((node) => node.size | 1) .strength(0.1) ); - r.d3Force('x', forceX()); - r.d3Force('y', forceY()); - r.d3Force('link') - .distance((link) => link.source.size + link.target.size + props.linkLength) + r.d3Force("x", forceX()); + r.d3Force("y", forceY()); + r.d3Force("link") + .distance( + (link) => link.source.size + link.target.size + props.linkLength + ) .strength(1); - r.d3Force('charge', forceManyBody().strength(-50)); + r.d3Force("charge", forceManyBody().strength(-50)); }, [props, graphRef]); - return ( - { - // ctx.fillStyle = color; - // const bckgDimensions = 1; - // bckgDimensions && ctx.fillRect(5, 5, 10, 10); - // }} - /> - ); + return ; } -// const my_map = (value, x1, y1, x2, y2) => -// ((value - x1) * (y2 - x2)) / (y1 - x1) + x2; - -// export default function NoSSRGraph(props) { -// const maxDepsCount = 262; -// const graphRef = useRef(); -// const f = new SonatypeOSSVulnProvider(); - -// useEffect(() => { -// const r = graphRef.current; -// r.zoomToFit(); -// // r.d3Force("collide", forceCollide(4)); -// // r.d3Force("x", forceX()); -// // r.d3Force("y", forceY()); -// // r.d3Force("link") -// // .distance( -// // (link) => -// // 1 * (link.source.dependsOnCount + link.target.dependsOnCount * 2) -// // ) -// // // .distance((link) => 1000) -// // .strength(1); -// // r.d3Force("charge", forceManyBody().strength(-70)); -// }, []); -// const handleClick = useCallback( -// (node) => { -// // Aim at node from outside it -// graphRef.current.centerAt(node.x, node.y, 500); -// let zoom_level = 3 - node.dependsOnCount / 100; -// if (zoom_level < 0.5) zoom_level = 0.5; -// graphRef.current.zoom(zoom_level, 500); -// console.log(node.neighbors); -// // f.fetchInfo(node.id); -// }, -// [graphRef] -// ); -// return ( -// "after"} -// nodeCanvasObject={(node, ctx, globalScale) => { -// ctx.canvas.width = 500; -// ctx.canvas.height = 500; -// // const label = node.id as string; //`${node.name} | ${node.dependsOnCount}`; -// // //const fontSize = my_map(node.dependsOnCount, 0, maxDepsCount, 5, 22); -// // ctx.font = "12px Sans-Serif"; //`${fontSize}px Sans-Serif`; -// // ctx.fillStyle = "#121212"; -// // ctx.textAlign = "center"; -// // ctx.textBaseline = "middle"; -// // ctx.fillText(label, node.x, node.y); -// }} -// {...props} -// linkDirectionalArrowLength={5} -// linkDirectionalArrowRelPos={3} -// nodeAutoColorBy={"id"} //nodeAutoColorBy={"__typename"} -// nodeLabel={"name"} -// // nodeVal={"dependsOnCount"} -// // backgroundColor={'#f0f0f0'} -// linkColor={"#ff0000"} -// // onNodeClick={(n) => { -// // handleClick(n); -// // }} -// /> -// ); -// } diff --git a/src/depvis-next/components/GraphControl/GraphControl.tsx b/src/depvis-next/components/GraphControl/GraphControl.tsx index bc5facd..0c0e842 100644 --- a/src/depvis-next/components/GraphControl/GraphControl.tsx +++ b/src/depvis-next/components/GraphControl/GraphControl.tsx @@ -10,17 +10,21 @@ const GraphControl = (props) => { useEffect(() => { props.onGraphConfigChange(graphConfig); + console.log(graphConfig); }, [graphConfig]); const handleNodeValToggle = (e) => { if (typeof graphConfig.nodeVal === 'function') { setGraphConfig({ ...graphConfig, nodeVal: 1 }); - console.log(graphConfig); } else { setGraphConfig({ ...graphConfig, nodeVal: getNodeValue }); } }; + const handleShowOnlyVulnerableToggle = (e) => { + setGraphConfig({ ...graphConfig, showOnlyVulnerable: !graphConfig.showOnlyVulnerable }); + }; + return (
Graph settings
@@ -35,6 +39,17 @@ const GraphControl = (props) => { checked={typeof graphConfig.nodeVal === 'function'} /> + + Show only affected components + { + handleShowOnlyVulnerableToggle(e); + }} + checked={graphConfig.showOnlyVulnerable as boolean} + /> + Length of links { diff --git a/src/depvis-next/components/Import/ImportForm.tsx b/src/depvis-next/components/Import/ImportForm.tsx index 3c3085e..3997cac 100644 --- a/src/depvis-next/components/Import/ImportForm.tsx +++ b/src/depvis-next/components/Import/ImportForm.tsx @@ -1,15 +1,18 @@ -import { useState } from "react"; -import { Alert, Button, Container, Form } from "react-bootstrap"; -import { ImportResult } from "./ImportResult"; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { Alert, Button, Container, Form, Row } from 'react-bootstrap'; +import { ImportResult } from './ImportResult'; +import { ImportFormData } from './types'; const allowedExtensionsRegex = /(\.json|\.xml)$/i; -const ImportForm = () => { - const [file, setFile] = useState(""); - const [preview, setPreview] = useState(""); - const [validated, setValidated] = useState(false); - const [isSubmitted, setIsSubmitted] = useState(false); - const [jobId, setJobId] = useState(null); +const ImportForm = (props) => { + const { onSubmitCallback } = props; + const [file, setFile] = useState(undefined); + const [preview, setPreview] = useState(''); + const [validated, setValidated] = useState(false); + const [projectName, setProjectName] = useState(''); + const [projectVersion, setProjectVersion] = useState('1.0.1'); const handleFiles = (e: any) => { const files = e.target.files; @@ -17,8 +20,9 @@ const ImportForm = () => { const file = files[0]; console.log(file); if (!allowedExtensionsRegex.exec(file.name)) { - alert("This extension is not allowed!"); - setFile(""); + setFile(undefined); + setValidated(true); + e.target.value = ''; return; } setFile(file); @@ -33,70 +37,83 @@ const ImportForm = () => { } setValidated(true); - console.log({ file: typeof file }); - const res = await fetch("/api/import", { - body: await file.text(), - headers: { "Content-Type": "application/xml" }, - method: "POST", - }); - const json = await res.json(); - setJobId(json.jobId); - setIsSubmitted(true); + const body: ImportFormData = { + projectName: projectName, + projectVersion: projectVersion, + sbom: await file.text(), + }; + await onSubmitCallback(body); }; const handlePreview = async (e: any) => { e.preventDefault(); - file && setPreview(await file.text()); + if (!file) return; + preview ? setPreview(undefined) : setPreview(await file.text()); }; - return isSubmitted ? ( - - ) : ( - - - - All data currently stored in DB will be overwritten. - - -
- - SBOM File - { - handleFiles(e); - }} - > - - Please select any XML / JSON file with SBOM. - - - - -
-
-
+ return ( + +
+ + Project name + { + setProjectName(e.target.value); + }} + value={projectName} + > + Please select any XML / JSON file with SBOM. + + + Project version + { + setProjectVersion(e.target.value); + }} + value={projectVersion} + > + Please select any XML / JSON file with SBOM. + + + SBOM File + { + handleFiles(e); + }} + > + Please select any XML / JSON file with SBOM. + + + +
{preview && ( - -

Preview - {file.name}

-
{preview}
-
+ + Contents of file {file.name} +
+
{preview}
+
)} -
+ ); }; diff --git a/src/depvis-next/components/Import/ImportResult.tsx b/src/depvis-next/components/Import/ImportResult.tsx index 44b1f29..9cca651 100644 --- a/src/depvis-next/components/Import/ImportResult.tsx +++ b/src/depvis-next/components/Import/ImportResult.tsx @@ -1,53 +1,66 @@ -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { Container, Spinner } from 'react-bootstrap'; - -const jobStatesRunning = ['active', 'waiting', 'paused', 'delayed']; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { Alert, Container, ProgressBar, Row, Spinner } from "react-bootstrap"; +import { ImportStatusReponse } from "../../pages/api/import/status"; +const fetchInterval = 500; export const ImportResult = (props) => { - const [status, setStatus] = useState(''); - const [isRunning, setIsRunning] = useState(true); - + const [response, setResponse] = useState(undefined); + const { jobId } = props; const router = useRouter(); const getJobStatus = async (id) => { - console.log({ id: id, ir: isRunning }); const res = await fetch(`/api/import/status?id=${id}`); - const json = await res.json(); - setStatus(json.status); - setIsRunning(jobStatesRunning.includes(json.status)); + setResponse(await res.json()); }; useEffect(() => { - if (!props.jobId || !isRunning) { + if (!jobId || (response && !response.continueQuery)) { return; } const interval = setInterval(() => { - getJobStatus(props.jobId); - }, 500); + getJobStatus(jobId); + }, fetchInterval); return () => clearInterval(interval); - }, [isRunning]); + }, [response]); - if (!isRunning && status == 'completed') { - router.push('/'); + if (!response) return <>; + if (response.status == "completed") { + const url = response.projectName + ? `/?projectName=${response.projectName}` + : "/"; + router.push(url); } + + const getPercent = () => { + if (!response.progress || !response.progress.percent) return 0; + return response.progress!.percent; + }; return ( - -

Your file was successfully submited and it will be parsed now!

-

This can take several minutes depending on the size of the input.

- {isRunning ? ( - - - Loading... - - + +

+ Importing project {response.projectName} +

+

+ {response.progress && response.progress.message} +

+

+ This can take several minutes depending on the size of the input. +

+ {response.continueQuery ? ( + ) : ( -

- - Completed (status {status}) Go to the main app! - -

+ + {response.status == "completed" ? ( + + Import completed! Redirecting to your graph... + + ) : ( + + {response.status} - {response.message} + + )} + )}
); diff --git a/src/depvis-next/components/Import/types.ts b/src/depvis-next/components/Import/types.ts new file mode 100644 index 0000000..8287735 --- /dev/null +++ b/src/depvis-next/components/Import/types.ts @@ -0,0 +1,5 @@ +export type ImportFormData = { + projectName: string; + projectVersion: string; + sbom: string; +}; diff --git a/src/depvis-next/components/Layout/GraphContainer.tsx b/src/depvis-next/components/Layout/GraphContainer.tsx index 380acc4..9f66a25 100644 --- a/src/depvis-next/components/Layout/GraphContainer.tsx +++ b/src/depvis-next/components/Layout/GraphContainer.tsx @@ -1,10 +1,10 @@ -import { useEffect, useRef, useState } from 'react'; -import { Col } from 'react-bootstrap'; -import { ForceGraphMethods } from 'react-force-graph-2d'; -import { getNodeColor, getNodeValue } from '../../helpers/GraphHelper'; -import { GraphConfig } from '../Graph/GraphConfig'; -import NoSSRGraphWrapper from '../Graph/NoSSRGraphWrapper'; -import Loading from '../Loading/Loading'; +import { useEffect, useRef, useState } from "react"; +import { Col } from "react-bootstrap"; +import { ForceGraphMethods } from "react-force-graph-2d"; +import { getNodeColor, getNodeValue } from "../../helpers/GraphHelper"; +import { GraphConfig } from "../Graph/GraphConfig"; +import NoSSRGraphWrapper from "../Graph/NoSSRGraphWrapper"; +import Loading from "../Loading/Loading"; const GraphContainer = (props) => { const [graphDimensions, setGraphDimensions] = useState({ width: 0, @@ -21,9 +21,9 @@ const GraphContainer = (props) => { useEffect(() => { setSize(); - window.addEventListener('resize', setSize); + window.addEventListener("resize", setSize); return () => { - window.removeEventListener('resize', setSize); + window.removeEventListener("resize", setSize); }; }, []); @@ -34,7 +34,7 @@ const GraphContainer = (props) => { {props.isLoading && } {!props.isLoading && ( 'after'} + nodeCanvasObjectMode={() => "after"} linkLength={graphConfig.linkLength} {...props} width={graphDimensions.width} diff --git a/src/depvis-next/components/Layout/Workspace.tsx b/src/depvis-next/components/Layout/Workspace.tsx deleted file mode 100644 index 4e51bba..0000000 --- a/src/depvis-next/components/Layout/Workspace.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useLazyQuery, useQuery } from '@apollo/client'; -import { useCallback, useEffect, useState } from 'react'; -import { Container, Row } from 'react-bootstrap'; -import { - formatData, - getAllComponentsQuery, - getNodeColor, - getNodeValue, - getProjectsQuery, -} from '../../helpers/GraphHelper'; -import ComponentDetails from '../Details/ComponentDetails'; -import Details from '../Details/Details'; -import VulnerabilityDetails from '../Details/VulnerabilityDetails'; -import Dropdown from '../Dropdown/Dropdown'; -import { GraphConfig } from '../Graph/GraphConfig'; -import GraphControl from '../GraphControl/GraphControl'; -import ImportForm from '../Import/ImportForm'; -import Loading from '../Loading/Loading'; -import Search from '../Search/Search'; -import GraphContainer from './GraphContainer'; -import Sidebar from './Sidebar'; - -const defaultGraphConfig: GraphConfig = { - zoomLevel: 1, - color: getNodeColor, - label: 'id', - linkDirectionalArrowLength: 5, - linkDirectionalRelPos: 0, - linkLength: 10, - nodeVal: getNodeValue, -}; - -const Workspace = () => { - const [node, setNode] = useState(undefined); - const [graphConfig, setGraphConfig] = useState(defaultGraphConfig); - const [graphData, setGraphData] = useState({ nodes: [], links: [] }); - const [selectedProject, setSelectedProject] = useState(); - const [getGraphData, { loading, error, data }] = useLazyQuery(getAllComponentsQuery); - const { data: projects, loading: projectsLoading } = useQuery(getProjectsQuery, { - onCompleted: (data) => { - if (data.projects.length > 0) { - setSelectedProject(data.projects[0].id); - } else { - setSelectedProject(undefined); - } - }, - }); - - useEffect(() => { - if (data) { - setGraphData(formatData(data)); - } - }, [data]); - useEffect(() => { - console.log(`Detected change, val ${selectedProject}`); - - if (selectedProject) { - console.log('Getting data'); - getGraphData({ variables: { projectId: selectedProject } }); - } - }, [selectedProject]); - - const handleNodeClick = (node) => { - setNode(node); - }; - - const handleVuln = async () => { - const res = await fetch('/api/vuln'); - console.log(res); - }; - - const handleSelectedSearchResult = (object) => { - setNode(object); - }; - - const handleNodeValToggle = (e) => { - if (typeof graphConfig.nodeVal === 'function') { - setGraphConfig({ ...graphConfig, nodeVal: 1 }); - console.log(graphConfig); - } else { - setGraphConfig({ ...graphConfig, nodeVal: getNodeValue }); - } - }; - - const paintRing = useCallback( - (currNode, ctx) => { - if (node) { - // add ring just for highlighted nodes - ctx.beginPath(); - ctx.arc(node.x, node.y, node.size | 1, 0, 2 * Math.PI, false); - ctx.fillStyle = currNode === node ? 'red' : 'orange'; - ctx.fill(); - } - }, - [node] - ); - - if (projectsLoading) return ; - if (!selectedProject) return ; - return ( - - - - setSelectedProject(e)} /> - - handleSelectedSearchResult(obj)} /> - { - setGraphConfig(val); - }} - onRefetchGraphClick={() => { - getGraphData({ - variables: { projectId: selectedProject }, - fetchPolicy: 'no-cache', - }); - }} - /> - {node && node.__typename === 'Component' && ( - - )} - {node && node.__typename === 'Vulnerability' && } - -
- - {!loading && ( - handleNodeClick(node)} - graphConfig={graphConfig} - /> - )} - - - ); -}; - -export default Workspace; diff --git a/src/depvis-next/components/NavBar/MainNavBar.tsx b/src/depvis-next/components/NavBar/MainNavBar.tsx index 265ef36..a2467bc 100644 --- a/src/depvis-next/components/NavBar/MainNavBar.tsx +++ b/src/depvis-next/components/NavBar/MainNavBar.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { useRouter } from "next/router"; import { Button } from "react-bootstrap"; import Container from "react-bootstrap/Container"; import Nav from "react-bootstrap/Nav"; @@ -7,6 +8,12 @@ import NavDropdown from "react-bootstrap/NavDropdown"; import { DeleteAllData } from "../../helpers/DbDataHelper"; import { gqlUrlPath } from "../../pages/api/graphql"; const MainNavbar = () => { + const router = useRouter(); + + const handleDeleteAllData = async () => { + await DeleteAllData(); + router.reload(); + }; return ( @@ -30,7 +37,7 @@ const MainNavbar = () => { className="mx-3" variant="danger" onClick={() => { - DeleteAllData(); + handleDeleteAllData(); }} > Delete all data in DB diff --git a/src/depvis-next/components/Workspace/ProjectSelector.tsx b/src/depvis-next/components/Workspace/ProjectSelector.tsx new file mode 100644 index 0000000..784418b --- /dev/null +++ b/src/depvis-next/components/Workspace/ProjectSelector.tsx @@ -0,0 +1,100 @@ +import { useQuery } from "@apollo/client"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { Button, Container } from "react-bootstrap"; +import { getProjectsQuery } from "../../helpers/GraphHelper"; +import Dropdown, { DropdownItem } from "../Dropdown/Dropdown"; + +type ProjectSelectorProps = { + onProjectVersionSelect?: (projectVersionId: DropdownItem) => void; +}; +const ProjectSelector = (props: ProjectSelectorProps) => { + const { onProjectVersionSelect } = props; + const [project, setProject] = useState(); + const [projectVersion, setProjectVersion] = useState(); + const router = useRouter(); + const { data: projects, loading: projectsLoading } = useQuery( + getProjectsQuery, + { + onCompleted: (data) => { + const project = selectProject(data.projects, router.query.projectName); + console.log("Selected project %s", project.id); + setProject({ id: project.id, displayName: project.name }); + const version = selectVersion(project, router.query.projectVersion); + console.log("Selected version %s", version.id); + + setProjectVersion(version); + }, + } + ); + + // useEffect(() => { + // if (projectVersion) { + // onProjectVersionSelect(projectVersion); + // } + // }, [projectVersion]); + const getProjectVersions = (project) => { + const projectObj = projects.projects.filter((p) => p.id === project.id)[0]; + return projectObj.versions.map((v) => { + return { id: v.id, displayName: v.version }; + }); + }; + + const handleProjectVersionSelect = (e: any) => { + console.log({ selectedVersion: e }); + onProjectVersionSelect && onProjectVersionSelect(e.id); + setProjectVersion(e); + }; + return ( + + {!projectsLoading && ( + <> + { + return { id: p.id, displayName: p.name }; + })} + onChange={(e) => { + setProjectVersion(undefined); + setProject(e); + }} + default={project} + /> + handleProjectVersionSelect(e)} + default={projectVersion} + /> + + )} + + ); +}; + +const selectProject = (projects, queryProjectName) => { + console.log({ projects: projects, query: queryProjectName }); + if ( + !queryProjectName || + projects.filter((p) => p.name === queryProjectName).length == 0 + ) + console.log("Fuck you"); + return projects[0]; + return projects.find((p) => p.name === queryProjectName); +}; + +const selectVersion = (project, queryProjectVersion) => { + if (project.versions.length == 0) { + console.log("Project %s does not have any versions", project.id); + return; + } + if ( + !queryProjectVersion || + project.versions.filter((v) => v.version === queryProjectVersion).length == + 0 + ) { + return project.versions[0]; + } + return project.versions.filter((v) => v.version === queryProjectVersion)[0]; +}; +export default ProjectSelector; diff --git a/src/depvis-next/components/Workspace/ProjectVersionSelector.tsx b/src/depvis-next/components/Workspace/ProjectVersionSelector.tsx new file mode 100644 index 0000000..d48d2a4 --- /dev/null +++ b/src/depvis-next/components/Workspace/ProjectVersionSelector.tsx @@ -0,0 +1,64 @@ +import { useQuery } from "@apollo/client"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { Container } from "react-bootstrap"; +import { getProjectVersionsQuery } from "../../helpers/GraphHelper"; +import Dropdown, { DropdownItem } from "../Dropdown/Dropdown"; +import NoProjectFoundError from "../Error/NoProjectFoundError"; + +type ProjectSelectorProps = { + onProjectVersionSelect?: (projectVersionId: string) => void; +}; +const ProjectVersionSelector = (props: ProjectSelectorProps) => { + const { onProjectVersionSelect } = props; + const [projectVersion, setProjectVersion] = useState(); + const router = useRouter(); + const { data: projects, loading: projectsLoading } = useQuery( + getProjectVersionsQuery, + { + onCompleted: (data) => { + setProjectVersion(data.projectVersions[0].id); + }, + } + ); + + useEffect(() => { + if (projectVersion) { + console.log("Fire"); + onProjectVersionSelect(projectVersion); + } + }, [projectVersion]); + + const handleProjectVersionSelect = (e: any) => { + setProjectVersion(e); + }; + + const transformDefault = () => { + const pv = projects.projectVersions.find((pv) => pv.id == projectVersion); + console.log({ projV: projectVersion, pv: pv }); + return { + id: projectVersion, + displayName: `${pv.name} v${pv.version}`, + }; + }; + + if (!projectsLoading && projects.projectVersions.length == 0) { + return ; + } + return ( + + {!projectsLoading && ( + { + return { id: p.id, displayName: `${p.project.name} v${p.version}` }; + })} + onChange={(e) => handleProjectVersionSelect(e)} + defaultValue={projectVersion} + /> + )} + + ); +}; + +export default ProjectVersionSelector; diff --git a/src/depvis-next/components/Workspace/Workspace.tsx b/src/depvis-next/components/Workspace/Workspace.tsx new file mode 100644 index 0000000..2252dcf --- /dev/null +++ b/src/depvis-next/components/Workspace/Workspace.tsx @@ -0,0 +1,176 @@ +import { useLazyQuery, useQuery } from "@apollo/client"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useCallback, useEffect, useState } from "react"; +import { Container, Row } from "react-bootstrap"; +import { + formatData, + getAllComponentsQuery, + getNodeColor, + getNodeValue, + getProjectsQuery, +} from "../../helpers/GraphHelper"; +import ComponentDetails from "../Details/ComponentDetails"; +import Details from "../Details/Details"; +import VulnerabilityDetails from "../Details/VulnerabilityDetails"; +import NoProjectFoundError from "../Error/NoProjectFoundError"; +import { GraphConfig } from "../Graph/GraphConfig"; +import GraphControl from "../GraphControl/GraphControl"; +import ImportForm from "../Import/ImportForm"; +import Loading from "../Loading/Loading"; +import Search from "../Search/Search"; +import GraphContainer from "../Layout/GraphContainer"; +import Sidebar from "../Layout/Sidebar"; +import ProjectSelector from "./ProjectSelector"; +import { DropdownItem } from "../Dropdown/Dropdown"; +import ProjectVersionSelector from "./ProjectVersionSelector"; +import { graphSelectedNode } from "../../types/colorPalette"; + +const defaultGraphConfig: GraphConfig = { + zoomLevel: 1, + color: getNodeColor, + label: "id", + linkDirectionalArrowLength: 5, + linkDirectionalRelPos: 0, + linkLength: 10, + nodeVal: getNodeValue, + showOnlyVulnerable: false, +}; + +const Workspace = () => { + const [node, setNode] = useState(undefined); + const [graphConfig, setGraphConfig] = + useState(defaultGraphConfig); + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); + const [selectedProjectVersion, setSelectedProjectVersion] = useState< + string | undefined + >(); + const [getGraphData, { loading, error, data }] = useLazyQuery( + getAllComponentsQuery + ); + + const router = useRouter(); + + useEffect(() => { + if (data) { + console.log(data); + setGraphData(formatData(data.projectVersions[0].allComponents)); + } + }, [data]); + useEffect(() => { + console.log(`Detected change, val ${selectedProjectVersion}`); + + if (selectedProjectVersion) { + console.log("Getting data"); + getGraphData({ + variables: { projectVersionId: selectedProjectVersion }, + pollInterval: 1000, + }); + } + }, [selectedProjectVersion]); + useEffect(() => { + handleShowOnlyVulnerableToggle(); + }, [graphConfig]); + + const handleNodeClick = (node) => { + setNode(node); + }; + + const handleShowOnlyVulnerableToggle = () => { + if (!data) return; + if (graphConfig.showOnlyVulnerable) { + setGraphData(formatData(data.projectVersions[0].allVulnerableComponents)); + } else { + setGraphData(formatData(data.projectVersions[0].allComponents)); + } + }; + + const handleSelectedSearchResult = (object) => { + setNode(object); + }; + + const handleNodeValToggle = (e) => { + if (typeof graphConfig.nodeVal === "function") { + setGraphConfig({ ...graphConfig, nodeVal: 1 }); + console.log(graphConfig); + } else { + setGraphConfig({ ...graphConfig, nodeVal: getNodeValue }); + } + }; + + const paintRing = useCallback( + (currNode, ctx) => { + ctx.beginPath(); + ctx.arc( + currNode.x, + currNode.y, + (Math.sqrt(currNode.size) * 4 + 1) | 1, + 0, + 2 * Math.PI, + false + ); + ctx.fillStyle = currNode === node ? graphSelectedNode : ""; + ctx.fill(); + }, + [node] + ); + + const handleProjectVersion = (projectVersion: string) => { + setSelectedProjectVersion(projectVersion); + }; + + if (!selectedProjectVersion) + return ( + + ); + return ( + + + + + handleSelectedSearchResult(obj)} + /> + { + setGraphConfig(val); + }} + onRefetchGraphClick={() => { + getGraphData({ + variables: { projectId: selectedProjectVersion }, + fetchPolicy: "no-cache", + }); + }} + /> + {node && node.__typename === "Component" && ( + + )} + {node && node.__typename === "Vulnerability" && ( + + )} + +
+ + {!loading && ( + handleNodeClick(node)} + graphConfig={graphConfig} + /> + )} + + + ); +}; + +export default Workspace; diff --git a/src/depvis-next/helpers/ApolloClientHelper.ts b/src/depvis-next/helpers/ApolloClientHelper.ts index 8078541..abf67ec 100644 --- a/src/depvis-next/helpers/ApolloClientHelper.ts +++ b/src/depvis-next/helpers/ApolloClientHelper.ts @@ -1,14 +1,25 @@ -import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'; -import { gqlUrlPath } from '../pages/api/graphql'; +import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; +import urlJoin from "url-join"; +import { gqlUrlPath } from "../pages/api/graphql"; /** * Function responsible for initialization of new Apollo Client used for GraphQL * @returns ApolloClient object */ export const createApolloClient = () => { - const uri = gqlUrlPath; + const serverUri = !(typeof window === "object") //decide if using browser or console + ? process.env.BACKEND_SERVER_URI + : process.env.NEXT_PUBLIC_SERVER_URI; + if (!serverUri) { + console.error("No server URI was provided, using default connection"); + } + const uri = urlJoin(serverUri || "http://localhost:3000", gqlUrlPath); + console.log(`Creating GQL Client (connection to ${uri})`); const link = new HttpLink({ uri: uri, + fetchOptions: { + mode: "cors", + }, }); return new ApolloClient({ link, diff --git a/src/depvis-next/helpers/BatchHelper.ts b/src/depvis-next/helpers/BatchHelper.ts index 90b2a53..2187aa7 100644 --- a/src/depvis-next/helpers/BatchHelper.ts +++ b/src/depvis-next/helpers/BatchHelper.ts @@ -5,18 +5,34 @@ * @param chunkSize How many items are used per one batch. Default is 10 * @returns Concatenated results of each function call. */ -export async function processBatch( + +export type processBatchOptions = { + fnArg2?: any; + updateProgressFn?: Function; + message?: any; + chunkSize: number; +}; +export async function processBatchAsync( inputList: any[], fn: Function, - chunkSize: number = 10 -) { + options: processBatchOptions +): Promise { if (!inputList) return; let res = []; - for (let i = 0; i < inputList.length; i += chunkSize) { - const chunk = inputList.slice(i, i + chunkSize); - const chunkRes = await fn(chunk); - res = res.concat(chunkRes); + const chunkSize = options.chunkSize || 10; + try { + for (let i = 0; i < inputList.length; i += chunkSize) { + if (options.updateProgressFn) { + options.updateProgressFn(i / inputList.length); + } + const chunk = inputList.slice(i, i + chunkSize); + const chunkRes = await fn(chunk, options.fnArg2); + res = res.concat(chunkRes); + } + } catch (error) { + console.error("While processing batch, error was encountered!"); + console.error(error); } console.log("Total items processed %d", res.length); - return res; + return res as T; } diff --git a/src/depvis-next/helpers/DbDataHelper.ts b/src/depvis-next/helpers/DbDataHelper.ts index 673d1d7..4e1cb43 100644 --- a/src/depvis-next/helpers/DbDataHelper.ts +++ b/src/depvis-next/helpers/DbDataHelper.ts @@ -1,9 +1,9 @@ -import { DocumentNode, gql } from '@apollo/client'; -import { randomBytes } from 'crypto'; -import { Component } from '../types/component'; -import { Project } from '../types/project'; -import { Vulnerability } from '../types/vulnerability'; -import { createApolloClient } from './ApolloClientHelper'; +import { DocumentNode, gql } from "@apollo/client"; +import { randomBytes } from "crypto"; +import { Component, ComponentDto } from "../types/component"; +import { Project } from "../types/project"; +import { Vulnerability } from "../types/vulnerability"; +import { createApolloClient } from "./ApolloClientHelper"; const chunkSize = 100; /** @@ -13,17 +13,29 @@ const chunkSize = 100; * @returns Data from successful query * @throws Error if there were some error during fetch */ -async function sendQuery(query: DocumentNode, variables?: Object) { +export async function sendGQLQuery(query: DocumentNode, variables?: Object) { + console.log( + `GQL Query: ${query} with variables ${await JSON.stringify(variables)}` + ); + console.dir({ query: query, variables: variables }); const client = createApolloClient(); const res = await client.query({ query: query, variables: variables }); if (res.errors) { throw Error(res.errors.toString()); } - return res.data; + return res; } -async function sendMutation(mutation: any, variables?: Object) { - console.log(`Sending mutation ${mutation} with variables ${await JSON.stringify(variables)}`); +export async function sendGQLMutation( + mutation: DocumentNode, + variables?: Object +) { + console.log( + `GQL Mutation: ${mutation} with variables ${await JSON.stringify( + variables + )}` + ); + console.dir({ mutation: mutation, variables: variables }); const client = createApolloClient(); const res = await client.mutate({ mutation: mutation, variables: variables }); if (res.errors) { @@ -33,21 +45,20 @@ async function sendMutation(mutation: any, variables?: Object) { return res; } -export async function ProjectExists(projectName: string) { - console.log(projectName); - +export async function TryGetProjectByName(projectName: string) { const query = gql` query Project($projectName: String!) { projects(where: { name_CONTAINS: $projectName }) { id name - version + versions { + version + } } } `; - const data = await sendQuery(query, { projectName: projectName }); - console.log(data); - return data.projects.length > 0; + const { data } = await sendGQLQuery(query, { projectName: projectName }); + return data.projects; } export async function ComponentExists(componentName: string) { @@ -60,11 +71,14 @@ export async function ComponentExists(componentName: string) { } } `; - const data = await sendQuery(query, { componentName: componentName }); + const { data } = await sendGQLQuery(query, { componentName: componentName }); return data.projects.length > 0; } -export async function CreateComponents(components: [Component?], projectId: string) { +export async function CreateComponents( + components: [Component?], + projectId: string +) { if (!components || components.length == 0) return; const mutation = gql` mutation CreateComponent($components: [ComponentCreateInput!]!) { @@ -82,7 +96,9 @@ export async function CreateComponents(components: [Component?], projectId: stri for (let i = 0; i < components.length; i += chunkSize) { const chunk = components.slice(i, i + chunkSize); const chunkWithProjectId = AddProjectToComponents(chunk, projectId); - const { data } = await sendMutation(mutation, { components: chunkWithProjectId }); + const { data } = await sendGQLMutation(mutation, { + components: chunkWithProjectId, + }); res.concat(data.components); } return res; @@ -90,9 +106,11 @@ export async function CreateComponents(components: [Component?], projectId: stri function AddProjectToComponents(components: Component[], projectId: string) { const res = components.map((c) => { - return { ...c, project: { connect: { where: { node: { id: projectId } } } } }; + return { + ...c, + project: { connect: { where: { node: { id: projectId } } } }, + }; }); - console.log(res); return res; } @@ -112,12 +130,12 @@ export async function CreateProject(project: Project) { } } `; - const { data } = await sendMutation(mutation, { project: [project] }); + const { data } = await sendGQLMutation(mutation, { project: [project] }); return data; } function generateName() { - return 'gql' + randomBytes(8).toString('hex'); + return "gql" + randomBytes(8).toString("hex"); } export async function GetVulnerability(vulnerabilityId) { @@ -129,48 +147,33 @@ export async function GetVulnerability(vulnerabilityId) { } } `; - const data = await sendQuery(query, { vulnerabilityId: vulnerabilityId }); + const { data } = await sendGQLQuery(query, { + vulnerabilityId: vulnerabilityId, + }); return data.vulnerabilities; } -export async function CreateUpdateVulnerability(purl: string, vulnerabilities: [Vulnerability?]) { - console.log('Creating vulnerability for package %s', purl); - if (!vulnerabilities || vulnerabilities.length == 0 || !purl) return; - const vulnExistsList = await Promise.all( - vulnerabilities.map(async (v) => { - const getVuln = await GetVulnerability(v.id); - return getVuln.length == 0; - }) - ); - const newVulnerabilities = vulnerabilities.filter((_v, index) => vulnExistsList[index]); - - if (newVulnerabilities.length == 0) { - console.log('All vulnerabilities for component %s already exists in DB', purl); - return; - } +export async function CreateVulnerability(vulnList: Vulnerability[]) { const mutation = gql` - mutation CreateVulnerability($purl: String, $vuln_array: [ComponentVulnerabilitiesCreateFieldInput!]) { - updateComponents(where: { purl: $purl }, update: { vulnerabilities: { create: $vuln_array } }) { + mutation CreateVulnerability($input: [VulnerabilityCreateInput!]!) { + createVulnerabilities(input: $input) { info { nodesCreated - relationshipsCreated } } } `; - //Tranform for mutation compatibility - const vulnArray = newVulnerabilities.map((v) => { - return { node: PrepareVulnAsGQL(v) }; + const input = vulnList.map((v) => { + return VulnToGQL(v); }); - const { data } = await sendMutation(mutation, { - purl: purl, - vuln_array: vulnArray, + const { data } = await sendGQLMutation(mutation, { + input: input, }); - console.log(data); + return data; } -function PrepareVulnAsGQL(vuln: Vulnerability) { +export function VulnToGQL(vuln: Vulnerability) { const refs = vuln.references ? { connectOrCreate: [ @@ -186,13 +189,21 @@ function PrepareVulnAsGQL(vuln: Vulnerability) { return { ...vuln, references: refs }; } -export async function UpdateProjectDependencies(projectId: string, components: [Component?]) { +export async function UpdateProjectDependencies( + projectId: string, + components: [Component?] +) { if (!projectId || components.length == 0) return; const mutation = gql` - mutation UpdateProjectDependencies($projectId: ID, $componentsPurl: [ComponentWhere!]!) { + mutation UpdateProjectDependencies( + $projectId: ID + $componentsPurl: [ComponentWhere!]! + ) { updateProjects( where: { id: $projectId } - update: { component: { connect: { where: { node: { OR: $componentsPurl } } } } } + update: { + component: { connect: { where: { node: { OR: $componentsPurl } } } } + } ) { __typename info { @@ -202,7 +213,7 @@ export async function UpdateProjectDependencies(projectId: string, components: [ } `; - const { data } = await sendMutation(mutation, { + const { data } = await sendGQLMutation(mutation, { projectId: projectId, componentsPurl: components.map((c) => { return { purl: c.purl }; @@ -210,23 +221,30 @@ export async function UpdateProjectDependencies(projectId: string, components: [ }); } -export async function UpdateComponentDependencies(dependencies, projectId: string) { +export async function UpdateComponentDependencies( + dependencies, + projectId: string +) { if (dependencies == null || dependencies.length == 0) return; for (let i = 0; i < dependencies.length; i += chunkSize) { //TODO: rewrite using variables const chunk = dependencies.slice(i, i + chunkSize); const chunkMutation = chunk .map((dependency) => { - return getComponentUpdateGQLQuery(dependency, dependency.dependsOn, generateName()); + return getComponentUpdateGQLQuery( + dependency, + dependency.dependsOn, + generateName() + ); }) - .join('\n'); + .join("\n"); const mutation = gql` mutation UpdateComponents ($projectId: ID){ ${chunkMutation} } `; - const { data } = await sendMutation(mutation, { projectId: projectId }); - console.log(data); + const { data } = await sendGQLMutation(mutation, { projectId: projectId }); + return data; } } export async function GetComponents() { @@ -237,7 +255,7 @@ export async function GetComponents() { } } `; - const data = await sendQuery(query); + const { data } = await sendGQLQuery(query); return data; } @@ -245,10 +263,14 @@ function getComponentWherePurlPart(array: [Component?]) { const res = array.map((c) => { return `{purl: \"${c.purl}\", project_SINGLE: {id: $projectId}}`; }); - return `[${res.join(',')}]`; + return `[${res.join(",")}]`; } -function getComponentUpdateGQLQuery(dependency, dependsOn, name = 'updateComponent') { +function getComponentUpdateGQLQuery( + dependency, + dependsOn, + name = "updateComponent" +) { const mutation_content = getComponentWherePurlPart(dependsOn); const mutation_part = `${name}: updateComponents( @@ -265,7 +287,6 @@ function getComponentUpdateGQLQuery(dependency, dependsOn, name = 'updateCompone relationshipsCreated } }`; - console.log(mutation_part); return mutation_part; } @@ -284,8 +305,68 @@ export async function DeleteAllData() { deleteVulnerabilities(where: {}) { nodesDeleted } + deleteProjectVersions(where: {}) { + nodesDeleted + } } `; - const { data } = await sendMutation(mutation); - console.log(data); + const { data } = await sendGQLMutation(mutation); + return data; +} + +export function AddProjectVersionConnectProject(projectId: string) { + return { connect: { where: { node: { id: projectId } } } }; +} + +export function CreateComponentsConnectProjectVersion( + components: [ComponentDto], + projectVersionId: string +) { + const ConnectProjectVersion = { + connect: { where: { node: { id: projectVersionId } } }, + }; + return components.map((c) => { + return { ...c, projectVersion: ConnectProjectVersion }; + }); +} + +export function AddComponentsConnectProjectVersion( + components: Component[], + projectVersionId: string +) { + return components.map((c) => { + return { + ...c, + projectVersion: { + connect: { where: { node: { id: projectVersionId } } }, + }, + }; + }); +} +export function BuildAddDependencyQuery( + dependencies: any[], + projectVersionId: string +) { + return dependencies.map((d) => { + if (!d.dependsOn) return; //No dependency + return { + where: { purl: d.purl, projectVersion: { id: projectVersionId } }, + connect: { + dependsOn: { + where: { + node: { + AND: getDependencyWherePurlPart(d.dependsOn, projectVersionId), + }, + }, + }, + }, + }; + }); +} + +function getDependencyWherePurlPart(dependsOn: any[], projectVersionId) { + const purls = dependsOn.map((d) => { + return d.purl; + }); + return { purl_IN: purls, projectVersion: { id: projectVersionId } }; } diff --git a/src/depvis-next/helpers/DbDataProvider.ts b/src/depvis-next/helpers/DbDataProvider.ts new file mode 100644 index 0000000..fb3fe4f --- /dev/null +++ b/src/depvis-next/helpers/DbDataProvider.ts @@ -0,0 +1,308 @@ +import { gql } from "@apollo/client"; +import { Component, Dependency } from "../types/component"; +import { Project, ProjectVersion, ProjectVersionDto } from "../types/project"; +import { Vulnerability } from "../types/vulnerability"; +import { + sendGQLQuery, + sendGQLMutation, + AddProjectVersionConnectProject, + BuildAddDependencyQuery, + AddComponentsConnectProjectVersion, + GetVulnerability, + CreateVulnerability as CreateVulnerabilities, +} from "./DbDataHelper"; +import { ProjectVersionInput } from "./ImportSbomHelper"; + +/** + * Function will create new project with optional first component + * @param project Project data + * @returns List containing new project object + */ +export async function CreateProject(project: Project): Promise { + if (!project) return; + const mutation = gql` + mutation CreateProject($project: [ProjectCreateInput!]!) { + createProjects(input: $project) { + projects { + id + name + versions { + id + version + } + } + } + } + `; + const { data } = await sendGQLMutation(mutation, { project: [project] }); + return data.createProjects.projects[0]; +} + +/** + * Get all projects that match provided name + * @param projectName Project name + * @returns List of projects that match given name + */ +export async function GetProjectByName( + projectName: string +): Promise { + const query = gql` + query Project($projectName: String!) { + projects(where: { name: $projectName }) { + id + name + versions { + id + version + } + } + } + `; + const { data } = await sendGQLQuery(query, { projectName: projectName }); + return data.projects; +} + +/** + * Get project by Id + * @param projectId Project Id + * @returns List of projects that match given Id + */ +export async function GetProjectById(projectId: string): Promise { + const query = gql` + query Project($projectName: String!) { + projects(where: { id: $projectId }) { + id + name + versions { + id + version + } + } + } + `; + const { data } = await sendGQLQuery(query, { projectId: projectId }); + return data.projects; +} + +/** + * Creates new version for a given project + * @param projectId Project ID + * @param version new version identificator + * @returns ID of the new project version + */ +export async function CreateProjectVersion( + projectId, + projectVersionInput: ProjectVersionInput +): Promise { + const { version, date } = projectVersionInput; + if (!projectId || !version) { + console.log( + "AddProjectComponent is missing some inputs! %s, %s", + projectId, + version + ); + return; + } + const mutation = gql` + mutation AddProjectVersion($projectVersion: ProjectVersionCreateInput!) { + createProjectVersions(input: [$projectVersion]) { + info { + nodesCreated + } + projectVersions { + id + } + } + } + `; + const projectVersion: ProjectVersionDto = { + version: version, + project: AddProjectVersionConnectProject(projectId), + date: new Date(date), + }; + const { data } = await sendGQLMutation(mutation, { + projectVersion: projectVersion, + }); + return data.createProjectVersions.projectVersions[0].id; +} + +/** + * Deletes project version with all its connected components, references and vulnerabilities + * @param projectVersionId ID of the project version + * @returns number of deleted nodes + */ +export async function DeleteProjectVersion( + projectVersionId: string +): Promise { + const mutation = gql` + mutation DeleteProjectVersion($projectVersionId: ID) { + deleteProjectVersions( + where: { id: $projectVersionId } + delete: { component: { where: {} } } + ) { + nodesDeleted + } + } + `; + const { data } = await sendGQLMutation(mutation, { + projectVersionId: projectVersionId, + }); + return data.nodesDeleted; +} + +/** + * Creates components in database and connects them with projectVersion + * @param components List of components + * @param projectVersionId ID of the project version + * @returns List of created components + */ +export async function CreateComponents( + components: Component[], + projectVersionId: string +) { + if (!components || components.length == 0) { + console.log("CreateComponents - No components provided!"); + return; + } + const mutation = gql` + mutation CreateComponents($components: [ComponentCreateInput!]!) { + createComponents(input: $components) { + info { + nodesCreated + } + components { + id + name + purl + } + } + } + `; + const componentsWithConnect = AddComponentsConnectProjectVersion( + components, + projectVersionId + ); + const { data } = await sendGQLMutation(mutation, { + components: componentsWithConnect, + }); + return data.createComponents.components; +} + +/** + * Adds dependency relationship for components and connects main component to project version + * @param dependencies List of dependencies + * @param projectVersionId ID of the project version + * @param mainComponentPurl purl of the main component that will be connected to projectVersion + * @returns number of dependencies created + */ +export async function updateComponentDependency( + dependencies: Dependency[], + projectVersionId: string, + mainComponentPurl: string, + progressUpdateFn: Function +) { + if (!dependencies || dependencies.length == 0) { + console.log("Updating dependencies - No dependencies provided!"); + return; + } + const mainComponentsMutation = gql` + mutation UpdateProjectVersion($projectVersionId: ID, $purl: String) { + updateProjectVersions( + where: { id: $projectVersionId } + connect: { + component: { + where: { + node: { purl: $purl, projectVersion: { id: $projectVersionId } } + } + } + } + ) { + info { + relationshipsCreated + } + } + } + `; + const mutation = gql` + mutation UpdateDependencies( + $where: ComponentWhere + $connect: ComponentConnectInput + ) { + name: updateComponents(where: $where, connect: $connect) { + info { + relationshipsCreated + } + } + } + `; + // Connect main component + const mainComponentRes = await sendGQLMutation(mainComponentsMutation, { + projectVersionId: projectVersionId, + purl: mainComponentPurl, + }); + const dependencyQueryList: any[] = BuildAddDependencyQuery( + dependencies, + projectVersionId + ); + let dependencyCount = 0; + for (let index = 0; index < dependencyQueryList.length; index++) { + const { data } = await sendGQLMutation(mutation, { + where: dependencyQueryList[index].where, + connect: dependencyQueryList[index].connect, + }); + dependencyCount += data.name.info.relationshipsCreated; + progressUpdateFn(index / dependencies.length); + } + console.log("Created %s relationships", dependencyCount); + return dependencyCount; +} + +export async function CreateUpdateVulnerability( + purl: string, + vulnerabilities: Vulnerability[] +) { + console.log("Creating vulnerability for package %s", purl); + if (!vulnerabilities || vulnerabilities.length == 0 || !purl) return; + + const vulnExistsList = await Promise.all( + vulnerabilities.map(async (v) => { + const getVuln = await GetVulnerability(v.id); + return getVuln.length == 0; + }) + ); + const newVulnerabilities = vulnerabilities.filter( + (_v, index) => vulnExistsList[index] + ); + const newVulnsUnique = newVulnerabilities.filter( + (v, index, arr) => arr.indexOf(v) == index + ); + //Create new vulnerabilities + await CreateVulnerabilities(newVulnsUnique); + + const mutation = gql` + mutation CreateVulnerability( + $purl: String + $vuln_array: [ComponentVulnerabilitiesConnectFieldInput!] + ) { + updateComponents( + where: { purl: $purl } + connect: { vulnerabilities: $vuln_array } + ) { + info { + nodesCreated + relationshipsCreated + } + } + } + `; + + //Connect vulnerabilities to component + const vulnArray = vulnerabilities.map((v) => { + return { where: { node: { id: v.id } } }; + }); + const { data } = await sendGQLMutation(mutation, { + purl: purl, + vuln_array: vulnArray, + }); + return data; +} diff --git a/src/depvis-next/helpers/GraphHelper.ts b/src/depvis-next/helpers/GraphHelper.ts index f3de6d1..2ed5bcf 100644 --- a/src/depvis-next/helpers/GraphHelper.ts +++ b/src/depvis-next/helpers/GraphHelper.ts @@ -1,11 +1,25 @@ -import { gql } from '@apollo/client'; +import { gql } from "@apollo/client"; +import { + graphExcludedNode, + graphNode, + graphUIGrey, + vulnerabilityCriticalColor, + vulnerabilityHighColor, + vulnerabilityLowColor, + vulnerabilityMediumColor, +} from "../types/colorPalette"; -export const formatData = (data) => { +/** + * Function responsible for transforming the data to format that can be visualized + * @param components List of components + * @returns Object containing list of nodes and links + */ +export const formatData = (components) => { const nodes = []; - const links = []; - if (!data || !data.projects || !data.projects[0].allComponents) return { nodes, links }; - const componentsCount = data.projects[0].allComponents.length / 20; - data.projects[0].allComponents.forEach((c) => { + let links = []; + if (!components) return { nodes, links }; + const componentsCount = components.length / 20; + components.forEach((c) => { nodes.push({ id: c.purl, name: c.name, @@ -19,6 +33,7 @@ export const formatData = (data) => { source: c.purl, target: d.purl, sourceDependsOnCount: c.dependsOnCount, + toVuln: false, }); }); } @@ -27,6 +42,7 @@ export const formatData = (data) => { links.push({ source: c.purl, target: v.id, + toVuln: true, }); nodes.push({ size: 1 + v.cvssScore, @@ -52,13 +68,39 @@ export const formatData = (data) => { }); } }); - console.log({ node: nodes, links: links }); + //Filter out links that are not connected + links = links.filter((l) => { + if (l.toVuln || nodes.find((n) => n.id === l.target)) return true; + }); return { nodes, links }; }; export const getAllComponentsQuery = gql` - query getProjectComponents($projectId: ID) { - projects(where: { id: $projectId }) { + query getProjectComponents($projectVersionId: ID) { + projectVersions(where: { id: $projectVersionId }) { + allVulnerableComponents { + id + name + version + __typename + purl + dependsOnCount + dependsOn { + purl + } + vulnerabilities { + __typename + id + cve + name + description + cvssScore + references { + __typename + url + } + } + } allComponents { id name @@ -90,26 +132,55 @@ export const getProjectsQuery = gql` projects { id name + versions { + id + version + } + } + } +`; + +export const getProjectVersionsQuery = gql` + { + projectVersions { + id version + project { + name + id + } } } `; -const componentColor = '#005f73'; -const vulnColor = '#ee9b00'; -const otherColor = '#001219'; -const severeVulnColor = '#bb3e03'; -const systemComponent = '#0f0f0f'; +// const componentColor = "#005f73"; +// const vulnColor = "#ee9b00"; +// const otherColor = "#001219"; +// const severeVulnColor = "#bb3e03"; +// const systemComponent = "#0f0f0f"; + +export const vulnerabilityColorByCVSS = (cvssScore: number) => { + if (cvssScore >= 9) return vulnerabilityCriticalColor; + if (cvssScore >= 7) return vulnerabilityHighColor; + if (cvssScore >= 4) return vulnerabilityMediumColor; + return vulnerabilityLowColor; +}; +const nodeExcludeRegex = new RegExp( + process.env.NEXT_PUBLIC_GRAPH_EXCLUDED_REGEX +); export const getNodeColor = (node) => { - if (!node) return otherColor; - if (node.__typename === 'Vulnerability') return node.cvssScore > 5 ? severeVulnColor : vulnColor; - if (node.selected) return '#6500ff'; - if (node.name && node.name.toLowerCase().includes('system')) return systemComponent; - if (node.__typename === 'Component') return componentColor; - return otherColor; + if (!node) return graphUIGrey; + if (node.__typename === "Vulnerability") + return vulnerabilityColorByCVSS(node.cvssScore); + if (node.name && nodeExcludeRegex.test(node.name)) return graphExcludedNode; + if (node.__typename === "Component") return graphNode; + return graphUIGrey; }; -const getNodeTier = (score: number, tresholds: { 1: 10; 0.75: 7; 0.5: 5; 0.25: 3; 0.1: 1 }) => { +const getNodeTier = ( + score: number, + tresholds: { 1: 10; 0.75: 7; 0.5: 5; 0.25: 3; 0.1: 1 } +) => { const limits = Object.keys(tresholds); limits.forEach((l) => { if (score < Number.parseFloat(l)) return tresholds[l]; @@ -127,13 +198,13 @@ export const getNodeValue = (node) => { */ function genGraphTree() { return { - nodes: [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }], + nodes: [{ id: "A" }, { id: "B" }, { id: "C" }, { id: "D" }], links: [ - { source: 'A', target: 'B' }, - { source: 'A', target: 'C' }, - { source: 'B', target: 'D' }, - { source: 'D', target: 'C' }, - { source: 'C', target: 'B' }, + { source: "A", target: "B" }, + { source: "A", target: "C" }, + { source: "B", target: "D" }, + { source: "D", target: "C" }, + { source: "C", target: "B" }, ], }; } diff --git a/src/depvis-next/helpers/ImportSbomHelper.ts b/src/depvis-next/helpers/ImportSbomHelper.ts index 67a0d66..e4e68e4 100644 --- a/src/depvis-next/helpers/ImportSbomHelper.ts +++ b/src/depvis-next/helpers/ImportSbomHelper.ts @@ -1,68 +1,151 @@ -import { Component } from '../types/component'; -import { Project } from '../types/project'; -import { VulnFetcherHandler } from '../vulnerability-mgmt/VulnFetcherHandler'; -import { processBatch } from './BatchHelper'; +import { Component, Dependency } from "../types/component"; +import { Project, ProjectVersion } from "../types/project"; +import { VulnFetcherHandler } from "../vulnerability-mgmt/VulnFetcherHandler"; +import { processBatchAsync } from "./BatchHelper"; import { - CreateComponents, CreateProject, - UpdateComponentDependencies, - UpdateProjectDependencies, -} from './DbDataHelper'; - -export async function ImportSbom(bom: any) { - // Prepare main component if exists - const mainComponentParsed = bom.metadata.component; - const mainComponent: Component | undefined = mainComponentParsed - ? { - type: mainComponentParsed.type, - name: mainComponentParsed.name, - purl: mainComponentParsed.purl, - version: mainComponentParsed.version, - author: mainComponentParsed.author, - publisher: mainComponentParsed.publisher, - } - : undefined; + CreateProjectVersion, + DeleteProjectVersion, + GetProjectById, + GetProjectByName, + CreateComponents, + updateComponentDependency, + CreateUpdateVulnerability, +} from "./DbDataProvider"; + +type ProjectInput = { + name?: string; + id?: string; +}; + +export type ProjectVersionInput = { + version: string; + date: string; +}; - // Prepare project - let project: Project = { - name: bom.metadata.component.name, - version: bom.metadata.component.version || 'n/a', - date: bom.metadata.timestamp || '1970-01-01', +const NotKnownPlaceholder = "n/a"; + +export async function ImportSbom( + bom: any, + projectInput: ProjectInput, + projectVersion: string, + updateProgressCallback +) { + const importInfo = { + project: undefined, + projectVersion: undefined, + createdComponents: [], + createdVulnerabilitiesIds: [], + currentPhase: ImportPhase.Init, + }; + + /** + * Simple wrapper function that is responsible for updating status for job worker + * @param percent Status in percent (0-100) + */ + const updateProgress = async (percent) => { + const obj = { + message: importInfo.currentPhase, + percent: CalculateProgress(importInfo.currentPhase, percent), + }; + console.log(obj); + await updateProgressCallback(obj); }; - // Prepare dependencies - let dependencies = GetDependencies(bom.dependencies.dependency); + try { + // Get project & project version + // Both project and project version will be created if it does not exist yet + importInfo.currentPhase = ImportPhase.CreateProject; + await updateProgress(0); + importInfo.project = await GetProject(projectInput); + const tmpProjectVersion = projectVersion + ? projectVersion + : bom.metadata.component + ? (bom.metadata.component.version as string) + : NotKnownPlaceholder; + const projectVersionInput: ProjectVersionInput = { + version: tmpProjectVersion, + date: bom.metadata.timestamp || Date.now().toLocaleString(), + }; + importInfo.projectVersion = await GetProjectVersionId( + importInfo.project, + projectVersionInput + ); + updateProgress(5); - // Currently there is no support for managing older projects - we first need to clear the DB - //await DeleteAllData(); - // Create all objects in DB - const projectResponse = await CreateProject(project); - const projectId = projectResponse.createProjects.projects[0].id; + // Prepare necessary objects - if it fails, no objects in DB are created yet + let dependencies = GetDependencies(bom.dependencies.dependency); + const mainComponent: Component = createMainComponent( + bom.metadata.component + ); + let components: Component[] = GetComponents(bom); + components.push(mainComponent); - // Prepare components - let components: [Component] = GetComponents(bom); - mainComponent && components.push(mainComponent); - console.log(components); + // Create components in DB + importInfo.currentPhase = ImportPhase.CreateComponents; - await CreateComponents(components, projectId); - await UpdateProjectDependencies(projectId, [mainComponent]); - await UpdateComponentDependencies(dependencies, projectId); + importInfo.createdComponents = await processBatchAsync( + components, + CreateComponents, + { + chunkSize: 5, + updateProgressFn: updateProgress, + fnArg2: importInfo.projectVersion, + } + ); - //Vulnerabilities - const purlList = components.map((c) => { - return c.purl; - }); - await processBatch(purlList, VulnFetcherHandler); + // Connect dependencies + importInfo.currentPhase = ImportPhase.ConnectDependencies; + const dependenciesResult = await updateComponentDependency( + dependencies, + importInfo.projectVersion, + mainComponent.purl, + updateProgress + ); + + // Find vulnerabilities + importInfo.currentPhase = ImportPhase.FindVulnerabilities; + const purlList = components.map((c) => { + return c.purl; + }); + const r = await processBatchAsync(purlList, VulnFetcherHandler, { + chunkSize: 10, + updateProgressFn: updateProgress, + }); + + // Create or connect vulnerabilities + r.forEach(async (component, index) => { + updateProgress(index / r.length); + if (component.vulnerabilities.length > 0) { + console.log( + "Creating %d vulns for %s", + component.vulnerabilities.length, + component.purl + ); + await CreateUpdateVulnerability( + component.purl, + component.vulnerabilities + ); + } + }); + importInfo.currentPhase = ImportPhase.Completed; + } catch (error) { + console.error("Recovery needed"); + console.error(error); + if (importInfo.projectVersion) { + await DeleteProjectVersion(importInfo.projectVersion); + } + } } function GetComponents(bom: any) { - let components = bom.components.component; + let components: any[] = bom.components.component; // Component data transformation components = components.map((c) => { return { type: c.type, name: c.name, purl: c.purl, - version: c.version, + version: `${c.version}`, author: c.author, publisher: c.publisher, }; @@ -70,9 +153,9 @@ function GetComponents(bom: any) { return components; } -function GetDependencies(dependencies: any) { +function GetDependencies(dependencies: any): Dependency[] { if (!dependencies) return; - const res = dependencies + const res: Dependency[] = dependencies .map((d) => { if (d.dependency != undefined) { if (!(d.dependency instanceof Array)) d.dependency = [d.dependency]; @@ -89,3 +172,176 @@ function GetDependencies(dependencies: any) { }); return res; } + +/** + * Function will find project with highest version number in format 1.2.3 + * Note: all project must have the same version length + * @param ProjectVersionList List of projects + * @returns The largest project + */ +export function getLatestProjectVersion( + ProjectVersionList: ProjectVersion[] +): ProjectVersion { + return ProjectVersionList.reduce((highestVersionObj, currentObj) => { + if ( + !highestVersionObj || + compareVersions(currentObj.version, highestVersionObj.version) > 0 + ) { + return currentObj; + } else { + return highestVersionObj; + } + }, undefined); +} + +/** + * Function will parse the versions from format '1.2.3' and compare them + * @param version1 version of project 1 + * @param version2 version of project 2 + * @returns 1 if version1 > version2, -1 if version1 < version2, 0 if they are equal + */ +export function compareVersions(version1: string, version2: string): number { + const arr1 = version1.split(".").map((num) => parseInt(num, 10)); + const arr2 = version2.split(".").map((num) => parseInt(num, 10)); + const num1 = parseInt(arr1.join(""), 10); + const num2 = parseInt(arr2.join(""), 10); + if (num1 > num2) { + return 1; + } else if (num1 < num2) { + return -1; + } + return 0; +} + +/** + * Function will return correct project. + * If the project exists, it will return corresponding object with additional info + * If the project does not exists, it will be created + * @param project Project Input data + * @returns Project + */ +async function GetProject(project: ProjectInput): Promise { + const { name, id } = project; + //We know exact ID + let existingProjects = []; + if (id) { + existingProjects = await GetProjectById(id); + } else { + existingProjects = await GetProjectByName(name); + } + // Project does not exist yet, so we need to create it first + if (existingProjects.length == 0) { + console.log("Creating new project with name %s", name); + const newProjectObj: Project = { name: name }; + const newProject = await CreateProject(newProjectObj); + console.log("Project created with id: %s", newProject!.id); + return newProject; + } + // Project exist, so we just return it + // Because there might be more projects, return the first one + const currentProject = existingProjects[0]; + if (existingProjects.length > 1) { + console.warn( + "Multiple project was found for the name %s\nReturning the first one with id %s", + name, + currentProject.id + ); + } + return currentProject; +} + +/** + * Function will return Id of correct Project Version. + * If the same version already exists, it will be removed, + * then a new version will be created. + * @param project Project object + * @param projectVersion Version represented as string, e.g. "1.0.0" + * @returns ProjectVersion Id + */ +async function GetProjectVersionId( + project: Project, + projectVersionInput: ProjectVersionInput +) { + const { version, date } = projectVersionInput; + if (!project || !version) { + throw Error("Invalid information - missing project or project version"); + } + + const existingProjectVersion = project.versions.find((pv) => { + pv.version === version; + }); + if (existingProjectVersion) { + console.log( + "Version %s for project %s already exists with id %s, it will be removed.", + version, + project.name, + existingProjectVersion.id + ); + await DeleteProjectVersion(existingProjectVersion.id); + } + const newVersionId = await CreateProjectVersion( + project.id, + projectVersionInput + ); + console.log( + "New version (%s) for project %s created with id %s", + projectVersionInput.version, + project.name, + newVersionId + ); + return newVersionId; +} + +function createMainComponent(inputComponent) { + if (!inputComponent) { + throw Error("No main component was provided!"); + } + const purl = + inputComponent.purl || `${inputComponent.name}@${inputComponent.version}`; + return { + type: inputComponent.type, + name: inputComponent.name, + purl: purl, + version: inputComponent.version, + author: inputComponent.author, + publisher: inputComponent.publisher, + }; +} + +enum ImportPhase { + CreateProject = "Creating project version", + CreateComponents = "Creating components", + ConnectDependencies = "Connecting dependencies", + FindVulnerabilities = "Looking for vulnerabilities", + CreateVulnerabilities = "Adding vulnerabilities", + Completed = "Import is completed", + Init = "Starting import", +} + +const CalculateProgress = (importPhase: ImportPhase, phasePercent: number) => { + switch (importPhase) { + case ImportPhase.CreateProject: + return CalculateProgressFn(0, 5, phasePercent); + case ImportPhase.CreateComponents: + return CalculateProgressFn(5, 40, phasePercent); + case ImportPhase.ConnectDependencies: + return CalculateProgressFn(40, 60, phasePercent); + case ImportPhase.FindVulnerabilities: + return CalculateProgressFn(60, 85, phasePercent); + case ImportPhase.CreateVulnerabilities: + return CalculateProgressFn(85, 100, phasePercent); + default: + return 0; + } +}; + +const CalculateProgressFn = ( + offset: number, + maximum: number, + percent: number +): number => { + const multiplier = percent > 100 ? 100 : percent; + const result = offset + (maximum - offset) * multiplier; + if (result < 0) return 0; + return Number.parseInt(result.toFixed(0)); +}; diff --git a/src/depvis-next/helpers/QueueHelper.ts b/src/depvis-next/helpers/QueueHelper.ts index 2878d0c..e804147 100644 --- a/src/depvis-next/helpers/QueueHelper.ts +++ b/src/depvis-next/helpers/QueueHelper.ts @@ -1,24 +1,18 @@ -const getKeys = async (q) => { - const multi = q.multi(); - multi.keys("*"); - const keys = await multi.exec(); - return keys[0][1]; -}; - -const filterQueueKeys = (q, keys) => { - const prefix = `${q.keyPrefix}:${q.name}`; - return keys.filter((k) => k.includes(prefix)); -}; +import { Queue, RedisOptions } from 'bullmq'; +import IORedis from 'ioredis'; -const deleteKeys = async (q, keys) => { - const multi = q.multi(); - keys.forEach((k) => multi.del(k)); - await multi.exec(); +export const emptyQueue = async (q: Queue) => { + q.drain(); }; -export const emptyQueue = async (q) => { - const keys = await getKeys(q); - const queueKeys = filterQueueKeys(q, keys); - await deleteKeys(q, queueKeys); - console.log(`Queue ${q.name} is empty`); +const REDIS_OPTIONS: RedisOptions = { + host: process.env.REDIS_HOST, + port: +process.env.REDIS_PORT!, + password: process.env.REDIS_PASSWORD, + enableOfflineQueue: false, + showFriendlyErrorStack: true, + enableReadyCheck: true, +} +export const defaultBullConfig = { + connection: new IORedis(REDIS_OPTIONS) }; diff --git a/src/depvis-next/helpers/WorkspaceHelper.ts b/src/depvis-next/helpers/WorkspaceHelper.ts index 78cc405..3455556 100644 --- a/src/depvis-next/helpers/WorkspaceHelper.ts +++ b/src/depvis-next/helpers/WorkspaceHelper.ts @@ -1,23 +1,26 @@ -import { PackageURL } from "packageurl-js"; +import { PackageURL } from 'packageurl-js'; import urlJoin from 'url-join'; - -const npmUrlBase = "https://www.npmjs.com/package/" -const nugetUrlBase = "https://www.nuget.org/packages/" -const pypiUrlBase = "https://pypi.org/project/" - +const npmUrlBase = 'https://www.npmjs.com/package/'; +const nugetUrlBase = 'https://www.nuget.org/packages/'; +const pypiUrlBase = 'https://pypi.org/project/'; export function GetComponentRepositoryURL(purl: string) { - const component = PackageURL.fromString(purl) + try { + const component = PackageURL.fromString(purl); switch (component.type) { - case "npm": - return urlJoin(npmUrlBase, component.name, component.version ? urlJoin("v", component.version) : "") - case "nuget": - return urlJoin(nugetUrlBase, component.name, component.version) - case "pypi": - return urlJoin(pypiUrlBase, component.name, component.version) - default: - return urlJoin("https://www.google.com/search?q=", purl) + case 'npm': + return urlJoin(npmUrlBase, component.name, component.version ? urlJoin('v', component.version) : ''); + case 'nuget': + return urlJoin(nugetUrlBase, component.name, component.version); + case 'pypi': + return urlJoin(pypiUrlBase, component.name, component.version); + default: + return urlJoin('https://www.google.com/search?q=', purl); } -} \ No newline at end of file + } catch { + console.log('Could not parse Package URL from %s', purl); + return; + } +} diff --git a/src/depvis-next/package-lock.json b/src/depvis-next/package-lock.json index 1fc6b57..b6322ad 100644 --- a/src/depvis-next/package-lock.json +++ b/src/depvis-next/package-lock.json @@ -12,7 +12,7 @@ "@neo4j/graphql": "^3.8.0", "apollo-server-micro": "^3.10.2", "bootstrap": "^5.2.1", - "bull": "^4.10.1", + "bullmq": "^3.6.2", "d3-force": "^3.0.0", "fast-xml-parser": "^4.0.10", "graphql": "^16.6.0", @@ -465,10 +465,70 @@ "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz", "integrity": "sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==" }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.0.tgz", + "integrity": "sha512-5qpnNHUyyEj9H3sm/4Um/bnx1lrQGhe8iqry/1d+cQYCRd/gzYA0YLeq0ezlk4hKx4vO+dsEsNyeowqRqslwQA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.0.tgz", + "integrity": "sha512-ZphTFFd6SFweNAMKD+QJCrWpgkjf4qBuHltiMkKkD6FFrB3NOTRVmetAGTkJ57pa+s6J0yCH06LujWB9rZe94g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.0.tgz", + "integrity": "sha512-ztKVV1dO/sSZyGse0PBCq3Pk1PkYjsA/dsEWE7lfrGoAK3i9HpS2o7XjGQ7V4va6nX+xPPOiuYpQwa4Bi6vlww==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.0.tgz", + "integrity": "sha512-NEX6hdSvP4BmVyegaIbrGxvHzHvTzzsPaxXCsUt0mbLbPpEftsvNwaEVKOowXnLoeuGeD4MaqSwL3BUK2elsUA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.0.tgz", + "integrity": "sha512-9uvdAkZMOPCY7SPRxZLW8XGqBOVNVEhqlgffenN8shA1XR9FWVsSM13nr/oHtNgXg6iVyML7RwWPyqUeThlwxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.2.0.tgz", - "integrity": "sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.0.tgz", + "integrity": "sha512-Wg0+9615kHKlr9iLVcG5I+/CHnf6w3x5UADRv8Ad16yA0Bu5l9eVOROjV7aHPG6uC8ZPFIVVaoSjDChD+Y0pzg==", "cpu": [ "x64" ], @@ -1468,8 +1528,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -1561,31 +1620,56 @@ "ieee754": "^1.2.1" } }, - "node_modules/bull": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/bull/-/bull-4.10.2.tgz", - "integrity": "sha512-xa65xtWjQsLqYU/eNaXxq9VRG8xd6qNsQEjR7yjYuae05xKrzbVMVj2QgrYsTMmSs/vsqJjHqHSRRiW1+IkGXQ==", + "node_modules/bullmq": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-3.6.2.tgz", + "integrity": "sha512-Nea7OWtdg6V47HP2vp0V2RUhG02KHCJE+VzbwpOFRONHS+1M6IehAVjEhfN3N036BNoPU776sMKCVxaegG0reA==", "dependencies": { - "cron-parser": "^4.2.1", - "debuglog": "^1.0.0", - "get-port": "^5.1.1", - "ioredis": "^5.0.0", + "cron-parser": "^4.6.0", + "glob": "^8.0.3", + "ioredis": "^5.3.0", "lodash": "^4.17.21", - "msgpackr": "^1.5.2", - "p-timeout": "^3.2.0", - "semver": "^7.3.2", - "uuid": "^8.3.0" + "msgpackr": "^1.6.2", + "semver": "^7.3.7", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bullmq/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bullmq/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/bull/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" + "node_modules/bullmq/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/bytes": { @@ -1729,11 +1813,11 @@ } }, "node_modules/cron-parser": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.0.tgz", - "integrity": "sha512-BdAELR+MCT2ZWsIBhZKDuUqIUCBjHHulPJnm53OfdRLA4EWBjva3R+KM5NeidJuGsNXdEcZkjC7SCnkW5rAFSA==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz", + "integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==", "dependencies": { - "luxon": "^3.1.0" + "luxon": "^3.2.1" }, "engines": { "node": ">=12.0.0" @@ -2000,14 +2084,6 @@ } } }, - "node_modules/debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", - "engines": { - "node": "*" - } - }, "node_modules/deep-equal": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.1.0.tgz", @@ -2847,8 +2923,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/function-bind": { "version": "1.1.1", @@ -2894,17 +2969,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -3229,7 +3293,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3271,14 +3334,14 @@ } }, "node_modules/ioredis": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.4.tgz", - "integrity": "sha512-qIpuAEt32lZJQ0XyrloCRdlEdUUNGG9i0UOk6zgzK6igyudNWqEBxfH6OlbnOOoBBvr1WB02mm8fR55CnikRng==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz", + "integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==", "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", - "denque": "^2.0.1", + "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", @@ -3855,32 +3918,32 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/msgpackr": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.8.1.tgz", - "integrity": "sha512-05fT4J8ZqjYlR4QcRDIhLCYKUOHXk7C/xa62GzMKj74l3up9k2QZ3LgFc6qWdsPHl91QA2WLWqWc8b8t7GLNNw==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.8.3.tgz", + "integrity": "sha512-m2JefwcKNzoHYXkH/5jzHRxAw7XLWsAdvu0FOJ+OLwwozwOV/J6UA62iLkfIMbg7G8+dIuRwgg6oz+QoQ4YkoA==", "optionalDependencies": { - "msgpackr-extract": "^2.2.0" + "msgpackr-extract": "^3.0.0" } }, "node_modules/msgpackr-extract": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-2.2.0.tgz", - "integrity": "sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.0.tgz", + "integrity": "sha512-oy6KCk1+X4Bn5m6Ycq5N1EWl9npqG/cLrE8ga8NX7ZqfqYUUBS08beCQaGq80fjbKBySur0E6x//yZjzNJDt3A==", "hasInstallScript": true, "optional": true, "dependencies": { - "node-gyp-build-optional-packages": "5.0.3" + "node-gyp-build-optional-packages": "5.0.7" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-linux-arm": "2.2.0", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-linux-x64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-win32-x64": "2.2.0" + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.0", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.0" } }, "node_modules/multiparty": { @@ -4024,9 +4087,9 @@ } }, "node_modules/node-gyp-build-optional-packages": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz", - "integrity": "sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", "optional": true, "bin": { "node-gyp-build-optional-packages": "bin.js", @@ -4155,7 +4218,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4186,14 +4248,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4224,17 +4278,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/packageurl-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.0.0.tgz", @@ -5338,8 +5381,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xss": { "version": "1.0.14", @@ -5707,10 +5749,40 @@ "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz", "integrity": "sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==" }, + "@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.0.tgz", + "integrity": "sha512-5qpnNHUyyEj9H3sm/4Um/bnx1lrQGhe8iqry/1d+cQYCRd/gzYA0YLeq0ezlk4hKx4vO+dsEsNyeowqRqslwQA==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.0.tgz", + "integrity": "sha512-ZphTFFd6SFweNAMKD+QJCrWpgkjf4qBuHltiMkKkD6FFrB3NOTRVmetAGTkJ57pa+s6J0yCH06LujWB9rZe94g==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.0.tgz", + "integrity": "sha512-ztKVV1dO/sSZyGse0PBCq3Pk1PkYjsA/dsEWE7lfrGoAK3i9HpS2o7XjGQ7V4va6nX+xPPOiuYpQwa4Bi6vlww==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.0.tgz", + "integrity": "sha512-NEX6hdSvP4BmVyegaIbrGxvHzHvTzzsPaxXCsUt0mbLbPpEftsvNwaEVKOowXnLoeuGeD4MaqSwL3BUK2elsUA==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.0.tgz", + "integrity": "sha512-9uvdAkZMOPCY7SPRxZLW8XGqBOVNVEhqlgffenN8shA1XR9FWVsSM13nr/oHtNgXg6iVyML7RwWPyqUeThlwxg==", + "optional": true + }, "@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.2.0.tgz", - "integrity": "sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.0.tgz", + "integrity": "sha512-Wg0+9615kHKlr9iLVcG5I+/CHnf6w3x5UADRv8Ad16yA0Bu5l9eVOROjV7aHPG6uC8ZPFIVVaoSjDChD+Y0pzg==", "optional": true }, "@neo4j/cypher-builder": { @@ -6399,8 +6471,7 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base64-js": { "version": "1.5.1", @@ -6445,26 +6516,48 @@ "ieee754": "^1.2.1" } }, - "bull": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/bull/-/bull-4.10.2.tgz", - "integrity": "sha512-xa65xtWjQsLqYU/eNaXxq9VRG8xd6qNsQEjR7yjYuae05xKrzbVMVj2QgrYsTMmSs/vsqJjHqHSRRiW1+IkGXQ==", + "bullmq": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-3.6.2.tgz", + "integrity": "sha512-Nea7OWtdg6V47HP2vp0V2RUhG02KHCJE+VzbwpOFRONHS+1M6IehAVjEhfN3N036BNoPU776sMKCVxaegG0reA==", "requires": { - "cron-parser": "^4.2.1", - "debuglog": "^1.0.0", - "get-port": "^5.1.1", - "ioredis": "^5.0.0", + "cron-parser": "^4.6.0", + "glob": "^8.0.3", + "ioredis": "^5.3.0", "lodash": "^4.17.21", - "msgpackr": "^1.5.2", - "p-timeout": "^3.2.0", - "semver": "^7.3.2", - "uuid": "^8.3.0" + "msgpackr": "^1.6.2", + "semver": "^7.3.7", + "tslib": "^2.0.0", + "uuid": "^9.0.0" }, "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } } } }, @@ -6564,11 +6657,11 @@ "dev": true }, "cron-parser": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.0.tgz", - "integrity": "sha512-BdAELR+MCT2ZWsIBhZKDuUqIUCBjHHulPJnm53OfdRLA4EWBjva3R+KM5NeidJuGsNXdEcZkjC7SCnkW5rAFSA==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz", + "integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==", "requires": { - "luxon": "^3.1.0" + "luxon": "^3.2.1" } }, "cross-spawn": { @@ -6764,11 +6857,6 @@ "ms": "2.1.2" } }, - "debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==" - }, "deep-equal": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.1.0.tgz", @@ -7415,8 +7503,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "function-bind": { "version": "1.1.1", @@ -7450,11 +7537,6 @@ "has-symbols": "^1.0.3" } }, - "get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==" - }, "get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -7671,7 +7753,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -7707,14 +7788,14 @@ } }, "ioredis": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.4.tgz", - "integrity": "sha512-qIpuAEt32lZJQ0XyrloCRdlEdUUNGG9i0UOk6zgzK6igyudNWqEBxfH6OlbnOOoBBvr1WB02mm8fR55CnikRng==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz", + "integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==", "requires": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", - "denque": "^2.0.1", + "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", @@ -8123,26 +8204,26 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "msgpackr": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.8.1.tgz", - "integrity": "sha512-05fT4J8ZqjYlR4QcRDIhLCYKUOHXk7C/xa62GzMKj74l3up9k2QZ3LgFc6qWdsPHl91QA2WLWqWc8b8t7GLNNw==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.8.3.tgz", + "integrity": "sha512-m2JefwcKNzoHYXkH/5jzHRxAw7XLWsAdvu0FOJ+OLwwozwOV/J6UA62iLkfIMbg7G8+dIuRwgg6oz+QoQ4YkoA==", "requires": { - "msgpackr-extract": "^2.2.0" + "msgpackr-extract": "^3.0.0" } }, "msgpackr-extract": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-2.2.0.tgz", - "integrity": "sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.0.tgz", + "integrity": "sha512-oy6KCk1+X4Bn5m6Ycq5N1EWl9npqG/cLrE8ga8NX7ZqfqYUUBS08beCQaGq80fjbKBySur0E6x//yZjzNJDt3A==", "optional": true, "requires": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-linux-arm": "2.2.0", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-linux-x64": "2.2.0", - "@msgpackr-extract/msgpackr-extract-win32-x64": "2.2.0", - "node-gyp-build-optional-packages": "5.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.0", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.0", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.0", + "node-gyp-build-optional-packages": "5.0.7" } }, "multiparty": { @@ -8240,9 +8321,9 @@ } }, "node-gyp-build-optional-packages": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz", - "integrity": "sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", "optional": true }, "object-assign": { @@ -8327,7 +8408,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -8355,11 +8435,6 @@ "word-wrap": "^1.2.3" } }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8378,14 +8453,6 @@ "p-limit": "^3.0.2" } }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "requires": { - "p-finally": "^1.0.0" - } - }, "packageurl-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.0.0.tgz", @@ -9158,8 +9225,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "xss": { "version": "1.0.14", diff --git a/src/depvis-next/package.json b/src/depvis-next/package.json index 3e3126f..b37e8a1 100644 --- a/src/depvis-next/package.json +++ b/src/depvis-next/package.json @@ -13,7 +13,7 @@ "@neo4j/graphql": "^3.8.0", "apollo-server-micro": "^3.10.2", "bootstrap": "^5.2.1", - "bull": "^4.10.1", + "bullmq": "^3.6.2", "d3-force": "^3.0.0", "fast-xml-parser": "^4.0.10", "graphql": "^16.6.0", diff --git a/src/depvis-next/pages/api/graphql.ts b/src/depvis-next/pages/api/graphql.ts index 79daa5c..980298e 100644 --- a/src/depvis-next/pages/api/graphql.ts +++ b/src/depvis-next/pages/api/graphql.ts @@ -1,23 +1,25 @@ -import { Neo4jGraphQL } from '@neo4j/graphql'; -import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'; -import { ApolloServer } from 'apollo-server-micro'; -import neo4j from 'neo4j-driver'; -import { env } from 'process'; -import { typeDefs } from '../../types/gqlTypes'; +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core"; +import { ApolloServer } from "apollo-server-micro"; +import neo4j from "neo4j-driver"; +import { env } from "process"; +import { typeDefs } from "../../types/gqlTypes"; -export const gqlUrlPath = '/api/graphql'; -const IsGQLDevToolsEnabled = env.GQL_ALLOW_DEV_TOOLS === 'true'; +export const gqlUrlPath = "/api/graphql"; +const IsGQLDevToolsEnabled = env.GQL_ALLOW_DEV_TOOLS === "true"; /** * Neo4j configuration settings * Use .env file to provide custom values! */ const NEO4J_CONFIG = { - neo4jHost: env.NEO4J_HOST || 'neo4j://localhost:7687', - neo4jUsername: env.NEO4J_USER || 'neo4j', - neo4jPassword: env.NEO4J_PASSWORD || '', + neo4jHost: env.NEO4J_HOST || "neo4j://localhost:7687", + neo4jUsername: env.NEO4J_USER || "neo4j", + neo4jPassword: env.NEO4J_PASSWORD || "", introspection: IsGQLDevToolsEnabled, - plugins: IsGQLDevToolsEnabled ? [ApolloServerPluginLandingPageGraphQLPlayground] : [], + plugins: IsGQLDevToolsEnabled + ? [ApolloServerPluginLandingPageGraphQLPlayground] + : [], }; const driver = neo4j.driver( @@ -25,10 +27,12 @@ const driver = neo4j.driver( neo4j.auth.basic(NEO4J_CONFIG.neo4jUsername, NEO4J_CONFIG.neo4jPassword) ); -export default async function handler(req, res) { +async function handler(req, res) { const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); + const schema = await neoSchema.getSchema(); + await neoSchema.assertIndexesAndConstraints({ options: { create: true } }); const apolloServer = new ApolloServer({ - schema: await neoSchema.getSchema(), + schema: schema, introspection: NEO4J_CONFIG.introspection, plugins: NEO4J_CONFIG.plugins, }); @@ -38,6 +42,26 @@ export default async function handler(req, res) { })(req, res); } +const allowCors = (fn) => async (req, res) => { + res.setHeader("Access-Control-Allow-Credentials", true); + const origin = process.env.CORS_ORIGIN || "http://localhost:3000"; + res.setHeader("Access-Control-Allow-Origin", origin); + res.setHeader( + "Access-Control-Allow-Methods", + "GET,OPTIONS,PATCH,DELETE,POST,PUT" + ); + res.setHeader( + "Access-Control-Allow-Headers", + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" + ); + if (req.method === "OPTIONS") { + res.status(200).end(); + return; + } + return await fn(req, res); +}; + +export default allowCors(handler); export const config = { api: { bodyParser: false, diff --git a/src/depvis-next/pages/api/import/index.ts b/src/depvis-next/pages/api/import/index.ts index 7fc1113..fb72ed5 100644 --- a/src/depvis-next/pages/api/import/index.ts +++ b/src/depvis-next/pages/api/import/index.ts @@ -1,68 +1,142 @@ -import Bull from 'bull'; -import { XMLParser } from 'fast-xml-parser'; -import { ImportSbom } from '../../../helpers/ImportSbomHelper'; -import { emptyQueue } from '../../../helpers/QueueHelper'; -import { GetVulnQueueName } from '../../../queues/GetVulnQueue'; -import { ImportQueueName } from '../../../queues/ImportQueue'; +import { Queue } from "bullmq"; +import { XMLParser } from "fast-xml-parser"; +import { GetProjectByName } from "../../../helpers/DbDataProvider"; +import { + compareVersions, + getLatestProjectVersion, + ImportSbom, +} from "../../../helpers/ImportSbomHelper"; +import { defaultBullConfig, emptyQueue } from "../../../helpers/QueueHelper"; +import { GetVulnQueueName } from "../../../queues/GetVulnQueue"; +import { + ImportQueueName, + ImportSbomJobData, +} from "../../../queues/ImportQueue"; export const config = { api: { bodyParser: { - sizeLimit: '10mb', + sizeLimit: "10mb", }, }, }; const XMLParserOptions = { ignoreAttributes: false, - attributeNamePrefix: '', + attributeNamePrefix: "", ignoreDeclaration: true, }; //Bull queue -const ImportQueue = new Bull(ImportQueueName); -const GetVulnQueue = new Bull(GetVulnQueueName); +const ImportQueue = new Queue(ImportQueueName, defaultBullConfig); +const GetVulnQueue = new Queue(GetVulnQueueName, defaultBullConfig); + type ImportResult = { isError: boolean; errorMessage?: string; jobId?: string; + sbom?: any; +}; + +type ImportRequestBody = { + projectName: string; + projectVersion: string; + sbom: string; }; export default async function handler(req, res) { try { - if (req.headers['content-type'] !== 'application/xml') { - res.status(500).json({ error: "Content-type must be 'application/xml'" }); + //Validate request + if (req.headers["content-type"] !== "application/json") { + res + .status(500) + .json({ error: "Content-type must be 'application/json'" }); return; } - //TODO: check if XML is even valid - const result = await parseXml(req.body); - return res.status(200).json(result); + const body: ImportRequestBody = req.body; + if (!validateImportRequestBody(body)) { + return res + .status(400) + .json({ isError: true, error: "Request body is malformed!" }); + } + console.log("Received following body: %s", JSON.stringify(body)); + + const projects = await GetProjectByName(body.projectName); + console.log(projects); + if (projects.length != 0 && projects[0].versions) { + const highestVersionProject = getLatestProjectVersion( + projects[0].versions + ); + if ( + compareVersions(body.projectVersion, highestVersionProject.version) != 1 + ) { + return res.status(400).json({ + isError: true, + error: `Project must have higher version than ${highestVersionProject.version}`, + }); + } + } + + // Parse sbom + const result = await parseXml(body.sbom); + if (result.isError) { + console.log("Import failed with %s", result.errorMessage); + return res.status(400).json(result); + } + + //Clear vuln queue + emptyQueue(GetVulnQueue); + console.log("Creating new job with following input: %s", body); + // Create new job + const job = await ImportQueue.add(body.projectName.toString(), { + sbom: result.sbom, + projectName: body.projectName, + projectVersion: body.projectVersion, + } as ImportSbomJobData); + + //Return response + const response: ImportResult = { jobId: job.id, isError: false }; + return res.status(200).json(response); } catch (err) { console.error(err); - return res.status(500).json({ error: 'failed to load data', content: err }); + return res.status(500).json({ error: "failed to load data", content: err }); } } +function validateImportRequestBody(body: ImportRequestBody) { + if (!body || !body.projectName || !body.projectVersion || !body.sbom) + return false; + return true; +} + // Function validates that object contains required properties function validateSbomXml(parsedXml): ImportResult { - const bom = parsedXml.bom; - if (!bom) + const sbom = parsedXml.bom; + if (!sbom) return { isError: true, errorMessage: "Validation failed - Missing 'bom' parameter in the file.", }; - if (!bom.metadata) + if (!sbom.metadata) return { isError: true, - errorMessage: "Validation failed - Missing 'metadata' parameter in the file.", + errorMessage: + "Validation failed - Missing 'metadata' parameter in the file.", }; - if (!bom.components) + if (!sbom.components) return { isError: true, - errorMessage: "Validation failed - Missing 'components' parameter in the file.", + errorMessage: + "Validation failed - Missing 'components' parameter in the file.", + }; + if (!sbom.dependencies) + return { + isError: true, + errorMessage: + "Validation failed - Missing 'dependencies' parameter in the file.", }; - return { isError: false }; + return { isError: false, sbom: sbom }; } // Function takes XML in plain text and transforms it into object @@ -70,12 +144,5 @@ async function parseXml(inputXml: string) { const parser = new XMLParser(XMLParserOptions); const xmlParsed = parser.parse(inputXml); - const validateResult = validateSbomXml(xmlParsed); - if (validateResult.isError) return validateResult; - - //Clear vuln queue - emptyQueue(GetVulnQueue); - const job = await ImportQueue.add({ bom: xmlParsed.bom }); - return { jobId: job.id }; - //return await ImportSbom(xmlParsed.bom); + return validateSbomXml(xmlParsed); } diff --git a/src/depvis-next/pages/api/import/status.ts b/src/depvis-next/pages/api/import/status.ts index 22633b1..5d41571 100644 --- a/src/depvis-next/pages/api/import/status.ts +++ b/src/depvis-next/pages/api/import/status.ts @@ -1,15 +1,46 @@ -import Bull from "bull"; -import { ImportQueueName } from "../../../queues/ImportQueue"; +import { Queue } from 'bullmq'; +import { defaultBullConfig } from '../../../helpers/QueueHelper'; +import { ImportQueueName } from '../../../queues/ImportQueue'; //Bull queue -const ImportQueue = new Bull(ImportQueueName); +const ImportQueue = new Queue(ImportQueueName, defaultBullConfig); +export type ImportStatusReponse = { + status: string; + progress?: any; + message: string; + continueQuery: boolean; + projectName?: string; +}; export default async function handler(req, res) { - if (req.method !== "GET") return res.status(405); - if (!("id" in req.query && !isNaN(req.query.id))) - return res.status(400).json({ - error: "'id' parameter is missing or its value is not a number", - }); - const result = await await ImportQueue.getJob(req.query.id); - return res.status(200).json({ status: await result.getState() }); + if (req.method !== 'GET') return res.status(405); + if (!('id' in req.query && !isNaN(req.query.id))) { + const response: ImportStatusReponse = { + status: 'Server Error', + message: 'The jobId was not provided or the value was null!', + continueQuery: false, + }; + return res.status(400).json(response); + } + try { + const result = await await ImportQueue.getJob(req.query.id); + const status = await result.getState(); + const projectName = result.data.projectName; + const response: ImportStatusReponse = { + status: status, + progress: result.progress, + message: '', + continueQuery: result.finishedOn === undefined, + projectName: projectName, + }; + return res.status(200).json(response); + } catch (err) { + console.error(err); + const response: ImportStatusReponse = { + status: 'Server Error', + message: 'There was an error while processing the request. See server log for more information.', + continueQuery: false, + }; + return res.status(500).json(response); + } } diff --git a/src/depvis-next/pages/api/vuln/[...purl].ts b/src/depvis-next/pages/api/vuln/[...purl].ts deleted file mode 100644 index 3ad4989..0000000 --- a/src/depvis-next/pages/api/vuln/[...purl].ts +++ /dev/null @@ -1,16 +0,0 @@ -import Bull from "bull"; -import { GetVulnQueueName } from "../../../queues/GetVulnQueue"; - -const VulnQueue = new Bull(GetVulnQueueName); - -export default async function handler(req, res) { - const { purl } = req.query; - const purl_j = purl.join("/"); - try { - const job = await VulnQueue.add({ purl: purl_j }); - return res.status(200).json({ jobId: job.id }); - } catch (error) { - console.log(error.message); - res.status(500).json({ error: error.message }); - } -} diff --git a/src/depvis-next/pages/api/vuln/index.ts b/src/depvis-next/pages/api/vuln/index.ts index 829738b..e693098 100644 --- a/src/depvis-next/pages/api/vuln/index.ts +++ b/src/depvis-next/pages/api/vuln/index.ts @@ -1,13 +1,13 @@ -import { processBatch } from "../../../helpers/BatchHelper"; -import { - CreateUpdateVulnerability, - GetComponents, -} from "../../../helpers/DbDataHelper"; +import { processBatchAsync } from "../../../helpers/BatchHelper"; +import { GetComponents } from "../../../helpers/DbDataHelper"; +import { CreateUpdateVulnerability } from "../../../helpers/DbDataProvider"; import { VulnFetcherHandler } from "../../../vulnerability-mgmt/VulnFetcherHandler"; export default async function handler(req, res) { const { components } = await GetComponents(); const purlList = components.map((c) => c.purl); - const r = await processBatch(purlList, VulnFetcherHandler, 100); + const r = await processBatchAsync(purlList, VulnFetcherHandler, { + chunkSize: 100, + }); //Use queue here r.forEach(async (component) => { diff --git a/src/depvis-next/pages/index.tsx b/src/depvis-next/pages/index.tsx index 4e91549..75f6d88 100644 --- a/src/depvis-next/pages/index.tsx +++ b/src/depvis-next/pages/index.tsx @@ -1,4 +1,4 @@ -import Workspace from '../components/Layout/Workspace'; +import Workspace from "../components/Workspace/Workspace"; const index = () => { return ; diff --git a/src/depvis-next/pages/toolbox.tsx b/src/depvis-next/pages/toolbox.tsx index e2622c4..4fb97ed 100644 --- a/src/depvis-next/pages/toolbox.tsx +++ b/src/depvis-next/pages/toolbox.tsx @@ -1,19 +1,36 @@ -import { PackageURL } from 'packageurl-js'; -import { useState } from 'react'; -import { Button, Col, Container, Row, Stack } from 'react-bootstrap'; -import { ParsePurl } from '../components/Toolbox/ParsePurl'; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { PackageURL } from "packageurl-js"; +import { useEffect, useState } from "react"; +import { + Button, + Col, + Container, + Form, + ProgressBar, + Row, + Stack, +} from "react-bootstrap"; +import { ParsePurl } from "../components/Toolbox/ParsePurl"; +import ProjectSelector from "../components/Workspace/ProjectSelector"; const Toolbox = () => { - const [purlString, setPurlString] = useState(''); - const [purlOutput, setPurlOutput] = useState(''); + const [purlString, setPurlString] = useState(""); + const [purlOutput, setPurlOutput] = useState(""); const handlePurl = async () => { setPurlOutput(await JSON.stringify(PackageURL.fromString(purlString))); console.log(purlOutput); }; const handleVuln = async () => { - const res = await fetch('/api/vuln'); + const res = await fetch("/api/vuln"); console.log(res); }; + + const router = useRouter(); + useEffect(() => { + console.log(router); + console.log(router.query); + }, [router]); return ( @@ -31,6 +48,13 @@ const Toolbox = () => { + Query param is {router.query.test} + + + + + + ); }; diff --git a/src/depvis-next/pages/upload.tsx b/src/depvis-next/pages/upload.tsx index 3c26929..5705b97 100644 --- a/src/depvis-next/pages/upload.tsx +++ b/src/depvis-next/pages/upload.tsx @@ -1,6 +1,64 @@ -import ImportForm from '../components/Import/ImportForm'; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { Alert, Container } from "react-bootstrap"; +import ImportForm from "../components/Import/ImportForm"; +import { ImportResult } from "../components/Import/ImportResult"; + +const ImportApiUrl = "/api/import"; const Upload = () => { - return ; + const router = useRouter(); + const [serverResponse, setServerResponse] = useState(); + + const handleSubmit = async (data) => { + const res = await fetch(ImportApiUrl, { + body: await JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + method: "POST", + }); + if (res.status == 200) { + const json = await res.json(); + router.push(`?jobId=${json.jobId}`, undefined, { shallow: true }); + } else { + setServerResponse(await res.json()); + } + }; + + if (router.query.jobId) { + return ; + } + return ( + + {serverResponse && ( + + Form can not be submitted - {serverResponse.error} + + )} + +

How to create SBOM for project

+
    +
  1. + Go to{" "} + + CycloneDX Tool Center + {" "} + and find a tool to generate SBOM according to your programming + language +
  2. +
  3. + Create the SBOM file according to the documentation for a given tool + - Choose XML as output file. +
  4. +
  5. Upload generated file here
  6. +
+
+ { + handleSubmit(data); + }} + /> +
+ ); }; export default Upload; diff --git a/src/depvis-next/queues/GetVulnQueue.ts b/src/depvis-next/queues/GetVulnQueue.ts index d85cbe2..076041c 100644 --- a/src/depvis-next/queues/GetVulnQueue.ts +++ b/src/depvis-next/queues/GetVulnQueue.ts @@ -1,21 +1,25 @@ -import Bull from 'bull'; -import { VulnFetcherHandler } from '../vulnerability-mgmt/VulnFetcherHandler'; -export const GetVulnQueueName = 'get-vuln-queue'; +import { Job, Worker } from "bullmq"; +import { CreateUpdateVulnerability } from "../helpers/DbDataProvider"; +import { defaultBullConfig } from "../helpers/QueueHelper"; +import { VulnFetcherHandler } from "../vulnerability-mgmt/VulnFetcherHandler"; +export const GetVulnQueueName = "get-vuln-queue"; -export const GetVulnQueueOptions: Bull.QueueOptions = { - redis: { - port: parseInt(process.env.REDIS_PORT, 10), - host: process.env.REDIS_HOST, - password: process.env.REDIS_PASSWORD, +const worker = new Worker( + GetVulnQueueName, + async (job) => { + console.log(`Processing Vuln ${job.data.purl}`); + const vulnerabilityList = await VulnFetcherHandler(job.data.purl); + if (vulnerabilityList.length == 0) return; + return await CreateUpdateVulnerability(job.data.purl, vulnerabilityList); }, - limiter: { max: 3, duration: 1000 }, -}; + defaultBullConfig +); -const GetVulnQueue = new Bull(GetVulnQueueName, GetVulnQueueOptions); +worker.on("failed", (job, error) => { + console.log("Job %d failed with error %s", job.id, error.message); + console.error(error); +}); -GetVulnQueue.process(async (job) => { - console.log(`Processing Vuln ${job.data.purl}`); - const vulnerabilityList = await VulnFetcherHandler(job.data.purl); - // if (vulnerabilitiesList.length == 0) return; - //return await CreateUpdateVulnerability(job.data.purl, vulnerabilitiesList); +worker.on("completed", (job) => { + console.log("Job %d completed successfully!", job.id); }); diff --git a/src/depvis-next/queues/ImportQueue.ts b/src/depvis-next/queues/ImportQueue.ts index b798e1e..bb7976e 100644 --- a/src/depvis-next/queues/ImportQueue.ts +++ b/src/depvis-next/queues/ImportQueue.ts @@ -1,25 +1,38 @@ -import Bull from 'bull'; -import { ImportSbom } from '../helpers/ImportSbomHelper'; +import { Worker } from "bullmq"; +import { ImportSbom } from "../helpers/ImportSbomHelper"; +import { defaultBullConfig } from "../helpers/QueueHelper"; -export const ImportQueueName = 'import-queue'; +export const ImportQueueName = "import-queue"; -export const ImportQueueOptions: Bull.QueueOptions = { - redis: { - port: parseInt(process.env.REDIS_PORT, 10), - host: process.env.REDIS_HOST, - password: process.env.REDIS_PASSWORD, - }, - limiter: { max: 2, duration: 3600 }, +export type ImportSbomJobData = { + projectName: string; + projectVersion: string; + sbom: string; }; -console.log(ImportQueueOptions); -const ImportQueue = new Bull(ImportQueueName, ImportQueueOptions); - -ImportQueue.process(async (job) => { - try { - const res = ImportSbom(job.data.bom); +const worker = new Worker( + ImportQueueName, + async (job) => { + const updateProgress = async (input) => { + await job.updateProgress(input); + }; + const data = job.data; + const res = await ImportSbom( + data.sbom, + { name: data.projectName }, + data.projectVersion, + updateProgress + ); return res; - } catch (e) { - console.error(e); - } + }, + defaultBullConfig +); + +worker.on("failed", (job, error) => { + console.log("Job %d failed with error %s", job.id, error.message); + console.error(error); +}); + +worker.on("completed", (job) => { + console.log("Job %d completed successfully!", job.id); }); diff --git a/src/depvis-next/styles/custom.css b/src/depvis-next/styles/custom.css index f633830..64214e9 100644 --- a/src/depvis-next/styles/custom.css +++ b/src/depvis-next/styles/custom.css @@ -32,3 +32,7 @@ overflow: hidden; white-space: nowrap; } + +.import-preview { + max-height: 25rem; +} diff --git a/src/depvis-next/types/colorPalette.ts b/src/depvis-next/types/colorPalette.ts new file mode 100644 index 0000000..531b564 --- /dev/null +++ b/src/depvis-next/types/colorPalette.ts @@ -0,0 +1,8 @@ +export const vulnerabilityCriticalColor = "#931621"; +export const vulnerabilityHighColor = "#D45113"; +export const vulnerabilityMediumColor = "#FB8500"; +export const vulnerabilityLowColor = "#FFB703"; +export const graphSelectedNode = "#00ABE7"; +export const graphNode = "#054A91"; +export const graphExcludedNode = "#81A4CD"; +export const graphUIGrey = "#423E3B"; diff --git a/src/depvis-next/types/component.ts b/src/depvis-next/types/component.ts index 0e3740d..331401d 100644 --- a/src/depvis-next/types/component.ts +++ b/src/depvis-next/types/component.ts @@ -1,6 +1,6 @@ -import { Project } from './project'; -import { Reference } from './reference'; -import { Vulnerability } from './vulnerability'; +import { Project } from "./project"; +import { Reference } from "./reference"; +import { Vulnerability } from "./vulnerability"; export type Component = { /** @@ -38,17 +38,28 @@ export type Component = { /** * List of all components that are direct dependencies for given component */ - dependsOn?: [Component]; + dependsOn?: Component[]; /** * References to external resources */ - references?: [Reference]; + references?: Reference[]; /** * List of vulnerabilities for given component */ - vulnerabilities?: [Vulnerability]; + vulnerabilities?: Vulnerability[]; /** * Reference to a project that contains given component */ project?: Project; }; + +export type ComponentDto = Component & { + projectVersion: any; +}; + +export type Dependency = { + purl: string; + dependsOn: { + purl: string; + }[]; +}; diff --git a/src/depvis-next/types/gqlTypes.ts b/src/depvis-next/types/gqlTypes.ts index 76f8918..4ce53cf 100644 --- a/src/depvis-next/types/gqlTypes.ts +++ b/src/depvis-next/types/gqlTypes.ts @@ -1,4 +1,4 @@ -import { gql } from 'apollo-server-micro'; +import { gql } from "apollo-server-micro"; /** * Type definition for GraphQL server @@ -7,7 +7,8 @@ import { gql } from 'apollo-server-micro'; export const typeDefs = gql` type Component { id: ID @id - project: [Project!]! @relationship(type: "BELONGS_TO", direction: OUT) + projectVersion: ProjectVersion + @relationship(type: "BELONGS_TO", direction: OUT) purl: String name: String type: String @@ -23,38 +24,70 @@ export const typeDefs = gql` """ ) dependsOn: [Component!]! @relationship(type: "DEPENDS_ON", direction: OUT) - references: [Reference!]! @relationship(type: "HAS_REFERENCE", direction: OUT) - vulnerabilities: [Vulnerability!]! @relationship(type: "HAS_VULNERABILITY", direction: OUT) + references: [Reference!]! + @relationship(type: "HAS_REFERENCE", direction: OUT) + vulnerabilities: [Vulnerability!]! + @relationship(type: "HAS_VULNERABILITY", direction: OUT) + indirectVulnCount: Int + @cypher( + statement: """ + MATCH (v:Vulnerability) + CALL apoc.algo.dijkstra(this,v, "DEPENDS_ON>|HAS_VULNERABILITY>", "count",1) + YIELD weight + RETURN count(weight) + """ + ) } type Vulnerability { id: String @unique - cve: String @unique - ghsa: String @unique + cve: String + ghsa: String name: String description: String affectedVersions: String cvssScore: Float cvssVector: String cwe: String - references: [Reference!]! @relationship(type: "HAS_REFERENCE", direction: OUT) + references: [Reference!]! + @relationship(type: "HAS_REFERENCE", direction: OUT) + affectedComponents: [Component!]! + @relationship(type: "HAS_VULNERABILITY", direction: IN) } type Project { id: ID @id - name: String + name: String @unique + versions: [ProjectVersion!]! + @relationship(type: "HAS_VERSION", direction: OUT) + } + + type ProjectVersion { + id: ID @id version: String allComponents: [Component!]! @cypher( statement: """ - MATCH (this)-->(c)-[:DEPENDS_ON*]->(c2) + MATCH (this)-->(c)-[:DEPENDS_ON*0..]->(c2) WITH collect(c) + collect(c2) as all UNWIND all as single RETURN distinct single """ ) + allVulnerableComponents: [Component!]! + @cypher( + statement: """ + MATCH a=(v:Vulnerability)<-[:HAS_VULNERABILITY]-(c1:Component)<-[:DEPENDS_ON*0..]-(c2)<-[:DEPENDS_ON]-(this) + WITH NODES(a) AS nodes + UNWIND nodes AS n + WITH n + WHERE 'Component' IN LABELS(n) + RETURN distinct n; + """ + ) component: [Component!]! @relationship(type: "DEPENDS_ON", direction: OUT) date: Date + project: Project! @relationship(type: "HAS_VERSION", direction: IN) } type Reference { diff --git a/src/depvis-next/types/project.ts b/src/depvis-next/types/project.ts index bc49162..14d8187 100644 --- a/src/depvis-next/types/project.ts +++ b/src/depvis-next/types/project.ts @@ -1,4 +1,4 @@ -import { Component } from "./component"; +import { Component } from './component'; export type Project = { /** @@ -9,6 +9,17 @@ export type Project = { * Friendly name */ name: string; + /** + * List of versions related to a given project + */ + versions?: ProjectVersion[]; +}; + +type ProjectVersionBase = { + /** + * Project Version Id + */ + id?: string; /** * Project version */ @@ -16,9 +27,23 @@ export type Project = { /** * List of all components in a given project */ - components?: [Component]; + components?: Component[]; /** * Date when SBOM was created for a given project */ date?: Date; }; + +export type ProjectVersion = ProjectVersionBase & { + /** + * Represents to which project a given version belongs + */ + project: Project; +}; + +export type ProjectVersionDto = ProjectVersionBase & { + /** + * Represents graphql connection + */ + project: any; +}; diff --git a/src/depvis-next/types/vulnerability.ts b/src/depvis-next/types/vulnerability.ts index f14870c..4d4674f 100644 --- a/src/depvis-next/types/vulnerability.ts +++ b/src/depvis-next/types/vulnerability.ts @@ -1,4 +1,4 @@ -import { Reference } from "./reference"; +import { Reference } from './reference'; export type Vulnerability = { /** @@ -40,5 +40,5 @@ export type Vulnerability = { /** * References to additional resources */ - references?: [Reference]; + references?: Reference[]; };