├── .eslintrc.json
├── gql
├── __generated__
│ ├── index.ts
│ └── fragment-masking.ts
└── types
│ └── nodeFragment.ts
├── public
├── github.png
├── favicon.ico
├── vercel.svg
├── thirteen.svg
├── images
│ └── icons
│ │ ├── close-button.svg
│ │ └── copy-clipboard.svg
└── next.svg
├── postcss.config.js
├── main.d.ts
├── components
├── graph
│ ├── ForceGraph2DWrapper.tsx
│ ├── types.ts
│ ├── ForceGraph2D.tsx
│ └── Graph.tsx
├── nodeInfo
│ └── nodeInfo.tsx
├── packages
│ ├── toggleSwitch.tsx
│ ├── packageGenericSelector.tsx
│ ├── certifyBadSelect.tsx
│ ├── packageNamespaceSelect.tsx
│ ├── packageTypeSelect.tsx
│ ├── packageNameSelect.tsx
│ ├── packageSelector.tsx
│ └── packageVersionSelect.tsx
├── layout
│ ├── navlinks.tsx
│ ├── footer.tsx
│ └── header.tsx
├── queryvuln
│ ├── certifyVulnQuery.ts
│ └── queryVuln.tsx
├── known
│ ├── vulnResults.tsx
│ ├── knownQueries.ts
│ └── knownInfo.tsx
├── highlightToggles.tsx
├── breadcrumb.tsx
├── navigationButton.tsx
└── tooltip.tsx
├── .env
├── next.config.js
├── tailwind.config.js
├── .gitignore
├── src
└── config.js
├── codegen.ts
├── app
├── layout.tsx
├── themeContext.tsx
└── page.tsx
├── store
├── vulnResultsContext.tsx
└── packageDataContext.tsx
├── hooks
├── useDimensions.ts
├── usePackageData.ts
├── useGraphData.ts
└── useBreadcrumbNavigation.ts
├── tsconfig.json
├── README.md
├── pages
└── api
│ └── version.ts
├── utils
├── graph_queries.ts
└── ggraph.tsx
├── styles
└── globals.css
├── apollo
└── client.ts
├── package.json
├── .github
└── workflows
│ ├── ci.yaml
│ └── release.yaml
└── LICENSE
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/gql/__generated__/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./fragment-masking";
2 | export * from "./gql";
--------------------------------------------------------------------------------
/public/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guacsec/guac-visualizer/HEAD/public/github.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guacsec/guac-visualizer/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/main.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'cytoscape-spread';
2 | declare module 'cytoscape-cose-bilkent';
3 | interface HTMLCanvasElement {
4 | __zoom: {
5 | x: number;
6 | y: number;
7 | };
8 | }
--------------------------------------------------------------------------------
/components/graph/ForceGraph2DWrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import dynamic from "next/dynamic";
4 | const ForceGraph2D = dynamic(() => import("./ForceGraph2D"), {
5 | ssr: false,
6 | });
7 |
8 | export default ForceGraph2D;
9 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Reminder: These are the defaults for the application.
2 | # Please, use a `.env.local` file for you local config
3 | # See also: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
4 |
5 | NEXT_PUBLIC_GUACGQL_SERVER_URL=http://localhost:8080
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const config = require("./src/config");
2 |
3 | /** @type {import('next').NextConfig} */
4 | const nextConfig = {
5 | reactStrictMode: true,
6 | transpilePackages: ["react-cytoscapejs"],
7 | typescript: {
8 | ignoreBuildErrors: true,
9 | },
10 | async rewrites() {
11 | return [
12 | {
13 | source: config.GUACGQL_PROXY_PATH,
14 | destination: config.GUACGQL_SERVER_QUERY_URL.toString(),
15 | },
16 | ];
17 | },
18 | };
19 |
20 | module.exports = nextConfig;
21 |
--------------------------------------------------------------------------------
/components/nodeInfo/nodeInfo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import KnownInfo from "../known/knownInfo";
3 |
4 | const NodeInfo = () => {
5 | return (
6 |
7 |
Package Information
8 |
Fetch more information about this package
9 |
10 |
11 | );
12 | };
13 |
14 | export default NodeInfo;
15 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx}',
5 | './components/**/*.{js,ts,jsx,tsx}',
6 | './app/**/*.{js,ts,jsx,tsx}',
7 | ],
8 | darkMode: 'class',
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # vscode
39 | .vscode/
40 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | function getGuacQlServerUrl() {
2 | let url = process.env.NEXT_PUBLIC_GUACGQL_SERVER_URL;
3 | url = url.trim();
4 | while (url.length > 0 && url.endsWith("/")) {
5 | url = url.substring(0, url.length - 1);
6 | }
7 | console.log("Using NEXT_PUBLIC_GUACGQL_SERVER_URL=" + url);
8 | return url;
9 | }
10 |
11 | const GUACGQL_SERVER_URL = getGuacQlServerUrl();
12 |
13 | module.exports = {
14 | GUACGQL_SERVER_QUERY_URL: new URL(GUACGQL_SERVER_URL + "/query"),
15 | GUACGQL_SERVER_VERSION_URL: new URL(GUACGQL_SERVER_URL + "/version"),
16 | GUACGQL_PROXY_PATH: "/api/graphql",
17 | };
18 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/packages/toggleSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export const Toggle = ({
4 | label,
5 | toggled,
6 | onClick,
7 | }: {
8 | label: string;
9 | toggled: boolean;
10 | onClick: (toggled: boolean) => void;
11 | }) => {
12 | const [isToggled, toggle] = useState(toggled);
13 |
14 | const callback = () => {
15 | toggle(!isToggled);
16 | onClick(!isToggled);
17 | };
18 |
19 | return (
20 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/codegen.ts:
--------------------------------------------------------------------------------
1 |
2 | import type { CodegenConfig } from '@graphql-codegen/cli';
3 |
4 | const config: CodegenConfig = {
5 | // TODO (mlieberman85): Below should change to some API endpoint with the schema or to soem place to download the schema.
6 | // For now this is just the relative path to my local clone of guac.
7 | schema: [
8 | '../guac/pkg/assembler/graphql/schema/*.graphql',
9 | ],
10 | documents: ['../guac/pkg/assembler/clients/operations/*.graphql'],
11 | generates: {
12 | './gql/__generated__/': {
13 | preset: 'client',
14 | plugins: [
15 | ],
16 | presetConfig: {
17 | gqlTagName: 'gql',
18 | },
19 | }
20 | },
21 | };
22 |
23 | export default config;
24 |
--------------------------------------------------------------------------------
/components/graph/types.ts:
--------------------------------------------------------------------------------
1 | import { LinkObject, NodeObject } from "react-force-graph-2d";
2 |
3 | export type GraphDataWithMetadata = {
4 | nodes: NodeWithMetadataObject[];
5 | links: LinkWithMetadataObject[];
6 | };
7 |
8 | export type NodeMetadata = {
9 | type: string; // TODO: [lillichoung] Consolidate all the node types into a enum
10 | label: string;
11 | };
12 |
13 | export type LinkMetadata = {
14 | label: string;
15 | };
16 |
17 | export type NodeWithMetadataObject = NodeObject & NodeMetadata;
18 | export type LinkWithMetadataObject = LinkObject & LinkMetadata;
19 |
20 | /* From react-force-graph-2d.d.ts */
21 | type Accessor = Out | string | ((obj: In) => Out);
22 | export type NodeAccessor = Accessor;
23 | export type CanvasCustomRenderFn = (
24 | obj: T,
25 | canvasContext: CanvasRenderingContext2D,
26 | globalScale: number
27 | ) => void;
28 |
--------------------------------------------------------------------------------
/components/layout/navlinks.tsx:
--------------------------------------------------------------------------------
1 | export default function NavigationLinks() {
2 | return (
3 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css";
2 | import Footer from "@/components/layout/footer";
3 | import Header from "@/components/layout/header";
4 | import { GuacVizThemeContextProvider } from "@/app/themeContext";
5 |
6 | export const metadata = {
7 | title: "GUAC Visualizer",
8 | description:
9 | "GUAC Visualizer is an experimental utility that can be used to visualized data loaded from GUAC.",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 | {children}
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/components/queryvuln/certifyVulnQuery.ts:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 |
3 | export const CERTIFY_VULN_QUERY = gql`
4 | query CertifyVuln($filter: CertifyVulnSpec!) {
5 | CertifyVuln(certifyVulnSpec: $filter) {
6 | id
7 | package {
8 | id
9 | type
10 | namespaces {
11 | id
12 | namespace
13 | names {
14 | id
15 | name
16 | versions {
17 | id
18 | version
19 | qualifiers {
20 | key
21 | value
22 | }
23 | subpath
24 | }
25 | }
26 | }
27 | }
28 | vulnerability {
29 | id
30 | type
31 | vulnerabilityIDs {
32 | id
33 | vulnerabilityID
34 | }
35 | }
36 | metadata {
37 | dbUri
38 | dbVersion
39 | scannerUri
40 | scannerVersion
41 | timeScanned
42 | }
43 | }
44 | }
45 | `;
46 |
--------------------------------------------------------------------------------
/components/packages/packageGenericSelector.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Select from "react-select";
3 |
4 | export type PackageSelectorOption = {
5 | label: string;
6 | value: T;
7 | };
8 |
9 | const PackageGenericSelector = ({
10 | label,
11 | options,
12 | onSelect,
13 | className = "",
14 | disabled = false,
15 | ...rest
16 | }: {
17 | label: string;
18 | options: PackageSelectorOption[];
19 | onSelect: (value: T) => void;
20 | className?: string;
21 | disabled?: boolean;
22 | }) => {
23 | return (
24 |
25 | {label &&
{label}
}
26 |
35 | );
36 | };
37 |
38 | export default PackageGenericSelector;
39 |
--------------------------------------------------------------------------------
/store/vulnResultsContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, createContext, useContext, useState } from "react";
2 |
3 | type VulnResultsContextType = {
4 | vulnResults: string[] | null;
5 | setVulnResults: React.Dispatch>;
6 | };
7 |
8 | const VulnResultsContext = createContext(
9 | undefined
10 | );
11 |
12 | type VulnResultsProviderProps = {
13 | children: ReactNode;
14 | };
15 |
16 | export const VulnResultsProvider: React.FC = ({
17 | children,
18 | }) => {
19 | const [vulnResults, setVulnResults] = useState(null);
20 |
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | export const useVulnResults = () => {
29 | const context = useContext(VulnResultsContext);
30 | if (context === undefined) {
31 | throw new Error("useResults must be used within a VulnResultsProvider");
32 | }
33 | return context;
34 | };
35 |
--------------------------------------------------------------------------------
/hooks/useDimensions.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { debounce } from "lodash";
3 |
4 | export function useDimensions() {
5 | const [dimensions, setDimensions] = useState({ width: 3000, height: 1000 });
6 |
7 | useEffect(() => {
8 | setDimensions({
9 | width: window.innerWidth,
10 | height: window.innerHeight,
11 | });
12 |
13 | const handleResize = debounce(() => {
14 | setDimensions({
15 | width: window.innerWidth,
16 | height: window.innerHeight,
17 | });
18 | }, 300);
19 |
20 | window.addEventListener("resize", handleResize);
21 | return () => {
22 | window.removeEventListener("resize", handleResize);
23 | };
24 | }, []);
25 |
26 | let containerWidth = dimensions.width * 0.5;
27 | let containerHeight = dimensions.height * 0.6;
28 |
29 | if (dimensions.width <= 640) {
30 | containerWidth = dimensions.width * 0.9;
31 | containerHeight = dimensions.height * 0.5;
32 | }
33 |
34 | return { containerWidth, containerHeight };
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowSyntheticDefaultImports": true,
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "strictNullChecks": false,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noEmit": true,
16 | "esModuleInterop": true,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "jsx": "preserve",
22 | "incremental": true,
23 | "baseUrl": ".",
24 | "paths": {
25 | "@/*": [
26 | "./*"
27 | ]
28 | },
29 | "plugins": [
30 | {
31 | "name": "next"
32 | }
33 | ]
34 | },
35 | "include": [
36 | "next-env.d.ts",
37 | "main.d.ts",
38 | "**/*.ts",
39 | "**/*.tsx",
40 | ".next/types/**/*.ts",
41 | "next.config.js",
42 | "src/config.js"
43 | ],
44 | "exclude": [
45 | "node_modules"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/gql/types/nodeFragment.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AllArtifactTreeFragment,
3 | AllBuilderTreeFragment,
4 | AllCertifyBadFragment,
5 | AllCertifyGoodFragment,
6 | AllCertifyScorecardFragment,
7 | AllCertifyVexStatementFragment,
8 | AllHashEqualTreeFragment,
9 | AllHasSbomTreeFragment,
10 | AllSlsaTreeFragment,
11 | AllHasSourceAtFragment,
12 | AllIsDependencyTreeFragment,
13 | AllIsOccurrencesTreeFragment,
14 | AllCertifyVulnFragment,
15 | AllPkgEqualFragment,
16 | AllPkgTreeFragment,
17 | AllSourceTreeFragment,
18 | VulnerabilityId,
19 | } from "@/gql/__generated__/graphql";
20 |
21 | export type NodeFragment =
22 | | AllArtifactTreeFragment
23 | | AllBuilderTreeFragment
24 | | AllCertifyBadFragment
25 | | AllCertifyGoodFragment
26 | | AllCertifyScorecardFragment
27 | | AllCertifyVexStatementFragment
28 | | VulnerabilityId
29 | | AllHasSbomTreeFragment
30 | | AllSlsaTreeFragment
31 | | AllHasSourceAtFragment
32 | | AllHashEqualTreeFragment
33 | | AllIsDependencyTreeFragment
34 | | AllIsOccurrencesTreeFragment
35 | | AllCertifyVulnFragment
36 | | AllPkgTreeFragment
37 | | AllPkgEqualFragment
38 | | AllSourceTreeFragment;
39 |
--------------------------------------------------------------------------------
/components/layout/footer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {useEffect, useState} from "react";
4 |
5 | export default function Footer() {
6 | const [versionData, setData] = useState({guacgql: "", guacVisualizer: ""})
7 | useEffect(() => {
8 | fetch('/api/version')
9 | .then((res) => res.json())
10 | .then((data) => {
11 | setData(data);
12 | })
13 | }, []);
14 |
15 | return (
16 | <>
17 |
29 |
31 | Served by GUAC GraphQL {versionData.guacgql}
32 |
33 | >
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/public/images/icons/close-button.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/known/vulnResults.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const VulnResults: React.FC<{ results: any }> = ({ results }) => {
4 | return (
5 |
6 | {results.map((node: any) => {
7 | return (
8 |
9 |
10 | Name: {" "}
11 | {node.package.namespaces[0].names[0].name}
12 |
13 |
14 | Version:{" "}
15 | {node.package.namespaces[0].names[0].versions[0].version}
16 |
17 |
18 | Type:
19 | {node.package.type}
20 |
21 |
22 | Vulnerability ID:
23 | {node.vulnerability.vulnerabilityIDs[0].vulnerabilityID}
24 |
25 |
26 | Last scanned on{" "}
27 | {new Date(node.metadata.timeScanned).toLocaleString()}
28 |
29 |
30 | );
31 | })}
32 |
33 | );
34 | };
35 |
36 | export default VulnResults;
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GUAC Visualizer
2 |
3 | The GUAC Visualizer is an experimental utility that can be used to interact with
4 | the GUAC services. It acts as a way to visualize the software supply chain graph
5 | as well as a means to explore the supply chain, and prototype policies.
6 |
7 | Since the GUAC Visualizer is **still in an early experimental stage**, it is likely
8 | that there may be some unexpected behavior or usage problems. For a more robust
9 | use of GUAC, we recommend using the
10 | [GraphQL interface directly](https://github.com/guacsec/guac/blob/main/demo/GraphQL.md).
11 |
12 | ## Get Started
13 |
14 | To get started with GUAC visualizer, look at the guide on [our official docsite](https://docs.guac.sh/guac-visualizer/).
15 |
16 | Once started, using the visualizer might look something like this:
17 |
18 | 
19 |
20 | ## Looking for GUAC?
21 |
22 | If you are instead looking for the main GUAC repo, you may find it [here](https://github.com/guacsec/guac).
23 |
24 | ## Give Feedback
25 |
26 | We'd love to hear how we can make the visualizer more useful.
27 | See the [GUAC Community page](https://guac.sh/community) for contact information.
28 |
29 | ## License
30 |
31 | GUAC Visualizer is released under the [Apache License 2.0](LICENSE).
32 |
--------------------------------------------------------------------------------
/hooks/usePackageData.ts:
--------------------------------------------------------------------------------
1 | import client from "@/apollo/client";
2 | import { INITIAL_PACKAGE_NAMESPACES } from "@/components/packages/packageSelector";
3 | import { PackageTypesDocument } from "@/gql/__generated__/graphql";
4 | import { useEffect, useState } from "react";
5 |
6 | export function usePackageData() {
7 | const [packageTypes, setPackageTypes] = useState(INITIAL_PACKAGE_NAMESPACES);
8 | const [packageLoading, setPackageLoading] = useState(true);
9 | const [packageError, setPackageError] = useState(null);
10 |
11 | useEffect(() => {
12 | setPackageLoading(true);
13 | client
14 | .query({
15 | query: PackageTypesDocument,
16 | variables: { filter: {} },
17 | })
18 | .then((res) => {
19 | let packageData = res.data.packages;
20 | let sortablePackageData = [...(packageData ?? [])];
21 | const types = sortablePackageData
22 | .sort((a, b) => a.type.localeCompare(b.type))
23 | .map((t) => ({ label: t.type, value: t.type }));
24 | setPackageTypes(types);
25 | setPackageLoading(false);
26 | })
27 | .catch((error) => {
28 | console.error("Error fetching package types:", error);
29 | setPackageError(error);
30 | setPackageLoading(false);
31 | });
32 | }, []);
33 |
34 | return {
35 | packageTypes,
36 | packageLoading,
37 | packageError,
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/public/images/icons/copy-clipboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/pages/api/version.ts:
--------------------------------------------------------------------------------
1 | import type {NextApiRequest, NextApiResponse} from 'next'
2 | import got from 'got'
3 | import * as console from "node:console";
4 | import {GUACGQL_SERVER_VERSION_URL} from "@/src/config";
5 | import packageJson from "@/package.json";
6 |
7 | export const config = {
8 | api: {
9 | // Enable `externalResolver` option in Next.js
10 | externalResolver: true,
11 | bodyParser: false,
12 | },
13 | }
14 |
15 | export default async function provideVersionInformation(req: NextApiRequest, res: NextApiResponse) {
16 | const options = {
17 | timeout: {
18 | connect: 5 // assuming the backend should be able to establish connection in 5 seconds
19 | },
20 | responseType: "buffer",
21 | headers: {
22 | "User-Agent": "guac-visualizer-v" + packageJson.version,
23 | "Accept": "application/json",
24 | }
25 | };
26 | let guacgqlVersion: string = "n/a"
27 | let guacVisualizerVersion: string = "n/a";
28 | try {
29 | const guacgqlResponse = await got.get(GUACGQL_SERVER_VERSION_URL, options);
30 | guacgqlVersion = guacgqlResponse.body.toString();
31 | } catch (error) {
32 | console.log("provideVersionInformation() -> guacgql server error: " + error + ", url: " + GUACGQL_SERVER_VERSION_URL + ", code: " + error.code + ", message:" + error.response?.body);
33 | }
34 | return res.status(200).json({
35 | "guacVisualizer": guacVisualizerVersion,
36 | "guacgql": guacgqlVersion,
37 | });
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/components/highlightToggles.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Toggle } from "@/components/packages/toggleSwitch";
3 |
4 | interface HighlightTogglesProps {
5 | highlights: {
6 | artifact: boolean;
7 | vuln: boolean;
8 | sbom: boolean;
9 | builder: boolean;
10 | };
11 | setHighlights: React.Dispatch<
12 | React.SetStateAction<{
13 | artifact: boolean;
14 | vuln: boolean;
15 | sbom: boolean;
16 | builder: boolean;
17 | }>
18 | >;
19 | }
20 |
21 | export const HighlightToggles: React.FC = ({
22 | highlights,
23 | setHighlights,
24 | }) => {
25 | return (
26 |
27 |
31 | setHighlights((prev) => ({ ...prev, artifact: !prev.artifact }))
32 | }
33 | />
34 | setHighlights((prev) => ({ ...prev, vuln: !prev.vuln }))}
38 | />
39 | setHighlights((prev) => ({ ...prev, sbom: !prev.sbom }))}
43 | />
44 |
48 | setHighlights((prev) => ({ ...prev, builder: !prev.builder }))
49 | }
50 | />
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/utils/graph_queries.ts:
--------------------------------------------------------------------------------
1 | import client from "@/apollo/client";
2 | import {
3 | NeighborsDocument,
4 | NodeDocument,
5 | } from "@/gql/__generated__/graphql";
6 | import { GuacGraphData } from "@/utils/ggraph";
7 | import { GraphDataWithMetadata } from "@/components/graph/types";
8 |
9 | export async function fetchNeighbors(id: string) {
10 | const res = await client.query({
11 | query: NeighborsDocument,
12 | variables: {
13 | node: id,
14 | usingOnly: [],
15 | },
16 | });
17 |
18 | return res.data.neighbors;
19 | }
20 |
21 | export async function GetNodeById(id: string) {
22 | const res = await client.query({
23 | query: NodeDocument,
24 | variables: {
25 | node: id
26 | },
27 | });
28 | return res.data;
29 | }
30 |
31 | export function parseAndFilterGraph(
32 | graphData: GraphDataWithMetadata,
33 | parsedNode: GuacGraphData
34 | ) {
35 | const uniqueNodeIds = new Set(graphData.nodes.map((node) => node.id));
36 | const uniqueLinkKeys = new Set(
37 | graphData.links.map((link) => `${link.source}-${link.target}-${link.label}`)
38 | );
39 | const linkKey = (link: any) => `${link.source}-${link.target}-${link.label}`;
40 |
41 | const uniqueNodes = parsedNode.nodes.filter(
42 | (node) => !uniqueNodeIds.has(node.data.id)
43 | );
44 |
45 | const uniqueEdges = parsedNode.edges.filter(
46 | (edge) => !uniqueLinkKeys.has(linkKey(edge.data.id))
47 | );
48 |
49 | uniqueNodes.forEach((n) => {
50 | graphData.nodes.push(n.data);
51 | });
52 |
53 | uniqueEdges.forEach((n) => {
54 | graphData.links.push(n.data);
55 | });
56 | }
57 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --backgroundcp-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
29 | label {
30 | position: relative;
31 | display: inline-block;
32 | width: 60px;
33 | height: 30px;
34 | }
35 |
36 | input[type="checkbox"] {
37 | opacity: 0;
38 | width: 0;
39 | height: 0;
40 | }
41 |
42 | .toggle_span {
43 | position: absolute;
44 | cursor: pointer;
45 | top: 0;
46 | left: 0;
47 | right: 0;
48 | bottom: 0;
49 | background: #2c3e50;
50 | transition: 0.3s;
51 | border-radius: 30px;
52 | }
53 |
54 | .toggle_span:before {
55 | position: absolute;
56 | content: "";
57 | height: 25px;
58 | left: 3px;
59 | width: 25px;
60 | bottom: 2.6px;
61 | background-color: #fff;
62 | border-radius: 50%;
63 | transition: 0.3s;
64 | }
65 |
66 | .red-bold-text {
67 | color: red;
68 | font-weight: bold;
69 | }
70 |
71 | input:checked + span {
72 | background-color: #00c853;
73 | }
74 |
75 | input:checked + span:before {
76 | transform: translateX(29px);
77 | }
78 |
79 | strong {
80 | position: absolute;
81 | left: 100%;
82 | width: max-content;
83 | line-height: 30px;
84 | margin-left: 10px;
85 | cursor: pointer;
86 | }
87 |
--------------------------------------------------------------------------------
/apollo/client.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import {
3 | ApolloClient,
4 | InMemoryCache,
5 | NormalizedCacheObject,
6 | } from "@apollo/client";
7 | import merge from "deepmerge";
8 | import { GUACGQL_PROXY_PATH } from "@/src/config";
9 |
10 | let apolloClient: ApolloClient;
11 |
12 | function createApolloClient() {
13 | return new ApolloClient({
14 | ssrMode: typeof window === "undefined",
15 | // link: createIsomorphLink(),
16 | uri: GUACGQL_PROXY_PATH,
17 | credentials: "same-origin",
18 | cache: new InMemoryCache(),
19 | defaultOptions: {
20 | watchQuery: {
21 | fetchPolicy: "no-cache",
22 | },
23 | query: {
24 | fetchPolicy: "no-cache",
25 | },
26 | },
27 | });
28 | }
29 |
30 | const client = createApolloClient();
31 |
32 | export function initializeApollo(initialState: NormalizedCacheObject = null) {
33 | const _apolloClient = apolloClient ?? createApolloClient();
34 |
35 | if (initialState) {
36 | // Get existing cache, loaded during client side data fetching
37 | const existingCache = _apolloClient.extract();
38 |
39 | // Merge the existing cache into data passed from getStaticProps/getServerSideProps
40 | const data = merge(initialState, existingCache);
41 |
42 | // Restore the cache with the merged data
43 | _apolloClient.cache.restore(data);
44 | }
45 |
46 | if (typeof window === "undefined") return _apolloClient;
47 |
48 | if (!apolloClient) apolloClient = _apolloClient;
49 |
50 | return _apolloClient;
51 | }
52 |
53 | export function useApollo(initialState: NormalizedCacheObject) {
54 | const store = useMemo(() => initializeApollo(initialState), [initialState]);
55 | return store;
56 | }
57 |
58 | export default client;
59 |
--------------------------------------------------------------------------------
/components/packages/certifyBadSelect.tsx:
--------------------------------------------------------------------------------
1 | import client from "@/apollo/client";
2 | import {
3 | NodeDocument,
4 | Node as gqlNode,
5 | CertifyBad,
6 | } from "@/gql/__generated__/graphql";
7 | import React from "react";
8 | import Select from "react-select";
9 |
10 | const CertifyBadSelect = ({
11 | label,
12 | options,
13 | setGraphDataFunc,
14 | ...rest
15 | }: {
16 | label: string;
17 | options: CertifyBad[];
18 | setGraphDataFunc: (data: gqlNode[]) => void;
19 | }) => {
20 | const onSelectCertifyBad = (event: { value: CertifyBad }) => {
21 | let nodeId = "";
22 | const sub = event.value.subject;
23 | switch (sub.__typename) {
24 | case "Source":
25 | nodeId = sub.namespaces[0].names[0].id;
26 | break;
27 | case "Package":
28 | const name = sub.namespaces[0].names[0];
29 | nodeId =
30 | name.versions != undefined && name.versions.length > 0
31 | ? name.versions[0].id
32 | : name.id;
33 | break;
34 | case "Artifact":
35 | nodeId = sub.id;
36 | break;
37 | }
38 | client
39 | .query({
40 | query: NodeDocument,
41 | fetchPolicy: "no-cache",
42 | variables: {
43 | node: nodeId.toString(),
44 | },
45 | })
46 | .then((res) => {
47 | const node = res.data.node as gqlNode;
48 | setGraphDataFunc([node]);
49 | });
50 | };
51 |
52 | // change width of Select
53 | return (
54 |
55 | {label && }
56 |
64 | );
65 | };
66 |
67 | export default CertifyBadSelect;
68 |
--------------------------------------------------------------------------------
/components/packages/packageNamespaceSelect.tsx:
--------------------------------------------------------------------------------
1 | import client from "@/apollo/client";
2 | import { PackageNamesDocument } from "@/gql/__generated__/graphql";
3 | import PackageGenericSelector, {
4 | PackageSelectorOption,
5 | } from "@/components/packages/packageGenericSelector";
6 |
7 | const PackageNamespaceSelect = ({
8 | label,
9 | options,
10 | setPackageNamespaceFunc,
11 | setPackageNamesFunc,
12 | packageType,
13 | resetNamespaceFunc,
14 | ...rest
15 | }: {
16 | label: string;
17 | options: PackageSelectorOption[];
18 | setPackageNamespaceFunc: (value: string) => void;
19 | setPackageNamesFunc: (value: PackageSelectorOption[]) => void;
20 | packageType: string;
21 | resetNamespaceFunc: () => void;
22 | disabled?: boolean;
23 | }) => {
24 | const onSelectPackageNamespace = (value: string) => {
25 | resetNamespaceFunc();
26 | setPackageNamespaceFunc(value);
27 |
28 | const packageNameQuery = client.query({
29 | query: PackageNamesDocument,
30 | variables: {
31 | filter: {
32 | namespace: value,
33 | type: packageType,
34 | },
35 | },
36 | });
37 |
38 | packageNameQuery.then((res) => {
39 | const sortablePackageNames = [
40 | ...(res.data.packages[0].namespaces[0].names ?? []),
41 | ];
42 |
43 | setPackageNamesFunc(
44 | sortablePackageNames
45 | .sort((a, b) => a.name.localeCompare(b.name))
46 | .map((t) => ({ label: t.name, value: t.name }))
47 | );
48 | });
49 | };
50 |
51 | return (
52 |
58 | );
59 | };
60 |
61 | export default PackageNamespaceSelect;
62 |
--------------------------------------------------------------------------------
/components/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ChevronDoubleRightIcon } from "@heroicons/react/24/solid";
3 |
4 | interface BreadcrumbProps {
5 | breadcrumb: string[];
6 | handleNodeClick: (nodeIndex: number) => void;
7 | currentIndex: number;
8 | }
9 |
10 | export const Breadcrumb: React.FC = ({
11 | breadcrumb,
12 | handleNodeClick,
13 | currentIndex,
14 | }) => {
15 | if (breadcrumb.length === 0) {
16 | return null;
17 | }
18 |
19 | return (
20 |
21 |
25 | {breadcrumb.map((label, index) => {
26 | const maxLabelLength = 25;
27 | let truncatedLabel =
28 | label.length > maxLabelLength
29 | ? `${label.substr(0, maxLabelLength)}...`
30 | : label;
31 |
32 | const isActive = index === currentIndex;
33 |
34 | return (
35 | -
36 | {index !== 0 && (
37 |
38 | )}
39 |
49 |
50 | );
51 | })}
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/store/packageDataContext.tsx:
--------------------------------------------------------------------------------
1 | import { Package } from "@/gql/__generated__/graphql";
2 | import { createContext, useContext, useState, ReactNode } from "react";
3 |
4 | type PackageDataContextType = {
5 | pkgContext: any;
6 | setPkgContext: (pkg: any) => void;
7 | pkgID: string;
8 | setPkgID: (pkg: string) => void;
9 | packageName: string;
10 | setPackageName: React.Dispatch>;
11 | pkgType: string;
12 | setPkgType: (pkg: string) => void;
13 | pkgVersion: string;
14 | setPkgVersion: (pkg: string) => void;
15 | };
16 |
17 | const PackageDataContext = createContext(
18 | undefined
19 | );
20 |
21 | export const usePackageData = () => {
22 | const context = useContext(PackageDataContext);
23 | if (!context) {
24 | throw new Error("usePackageData must be used within a Provider");
25 | }
26 | return context;
27 | };
28 |
29 | type PackageDataProviderProps = {
30 | children: ReactNode;
31 | };
32 |
33 | export const PackageDataProvider: React.FC = ({
34 | children,
35 | }) => {
36 | const [pkgContext, setPkgContext] = useState();
37 | const [pkgID, setPkgID] = useState("");
38 | const [packageName, setPackageName] = useState("");
39 | const [pkgType, setPkgType] = useState("");
40 | const [pkgVersion, setPkgVersion] = useState("");
41 |
42 | return (
43 |
57 | {children}
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/components/packages/packageTypeSelect.tsx:
--------------------------------------------------------------------------------
1 | import client from "@/apollo/client";
2 | import { PackageNamespacesDocument } from "@/gql/__generated__/graphql";
3 | import React, { Dispatch, SetStateAction } from "react";
4 | import PackageGenericSelector, {
5 | PackageSelectorOption,
6 | } from "@/components/packages/packageGenericSelector";
7 |
8 | const PackageTypeSelect = ({
9 | label,
10 | options,
11 | setPackageTypeFunc,
12 | setPackageNamespacesFunc,
13 | resetTypeFunc,
14 | disabled,
15 | ...rest
16 | }: {
17 | label: string;
18 | options: PackageSelectorOption[];
19 | setPackageTypeFunc: Dispatch>;
20 | setPackageNamespacesFunc: Dispatch<
21 | SetStateAction[]>
22 | >;
23 | resetTypeFunc: () => void;
24 | disabled?: boolean;
25 | }) => {
26 | const onSelectPackageType = (value: string) => {
27 | resetTypeFunc();
28 | setPackageTypeFunc(value);
29 | const packageNamespacesQuery = client.query({
30 | query: PackageNamespacesDocument,
31 | variables: {
32 | filter: {
33 | type: value,
34 | },
35 | },
36 | });
37 | packageNamespacesQuery.then((res) => {
38 | const sortablePackageNamespaces = [
39 | ...(res.data.packages[0].namespaces ?? []),
40 | ];
41 | setPackageNamespacesFunc(
42 | sortablePackageNamespaces
43 | .sort((a, b) => a.namespace.localeCompare(b.namespace))
44 | .map((t) => ({ label: t.namespace || "\u00A0", value: t.namespace }))
45 | );
46 | });
47 | };
48 |
49 | return (
50 |
57 | );
58 | };
59 |
60 | export default PackageTypeSelect;
61 |
--------------------------------------------------------------------------------
/app/themeContext.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createContext, ReactElement, useEffect, useState } from "react";
4 |
5 | const GuacVizThemeContext = createContext({
6 | isDarkTheme: true,
7 | toggleThemeHandler: () => {},
8 | });
9 |
10 | interface ThemePropsInterface {
11 | children?: JSX.Element | Array;
12 | }
13 |
14 | export function GuacVizThemeContextProvider(
15 | props: ThemePropsInterface
16 | ): ReactElement {
17 | const [isDarkTheme, setIsDarkTheme] = useState(true);
18 | useEffect(() => initialThemeHandler());
19 |
20 | function isLocalStorageEmpty(): boolean {
21 | return !localStorage.getItem("isDarkTheme");
22 | }
23 |
24 | function initialThemeHandler(): void {
25 | if (isLocalStorageEmpty()) {
26 | localStorage.setItem("isDarkTheme", `true`);
27 | document!.querySelector("body")!.classList.add("dark");
28 | setIsDarkTheme(true);
29 | } else {
30 | const isDarkTheme: boolean = JSON.parse(
31 | localStorage.getItem("isDarkTheme")!
32 | );
33 | isDarkTheme && document!.querySelector("body")!.classList.add("dark");
34 | setIsDarkTheme(() => {
35 | return isDarkTheme;
36 | });
37 | }
38 | }
39 |
40 | function toggleThemeHandler(): void {
41 | const isDarkTheme: boolean = JSON.parse(
42 | localStorage.getItem("isDarkTheme")!
43 | );
44 | setIsDarkTheme(!isDarkTheme);
45 | toggleDarkClassToBody();
46 | setValueToLocalStorage();
47 | }
48 |
49 | function toggleDarkClassToBody(): void {
50 | document!.querySelector("body")!.classList.toggle("dark");
51 | }
52 |
53 | function setValueToLocalStorage(): void {
54 | localStorage.setItem("isDarkTheme", `${!isDarkTheme}`);
55 | }
56 |
57 | return (
58 |
61 | {props.children}
62 |
63 | );
64 | }
65 |
66 | export default GuacVizThemeContext;
67 |
--------------------------------------------------------------------------------
/gql/__generated__/fragment-masking.ts:
--------------------------------------------------------------------------------
1 | import { ResultOf, TypedDocumentNode as DocumentNode, } from '@graphql-typed-document-node/core';
2 |
3 |
4 | export type FragmentType> = TDocumentType extends DocumentNode<
5 | infer TType,
6 | any
7 | >
8 | ? TType extends { ' $fragmentName'?: infer TKey }
9 | ? TKey extends string
10 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } }
11 | : never
12 | : never
13 | : never;
14 |
15 | // return non-nullable if `fragmentType` is non-nullable
16 | export function useFragment(
17 | _documentNode: DocumentNode,
18 | fragmentType: FragmentType>
19 | ): TType;
20 | // return nullable if `fragmentType` is nullable
21 | export function useFragment(
22 | _documentNode: DocumentNode,
23 | fragmentType: FragmentType> | null | undefined
24 | ): TType | null | undefined;
25 | // return array of non-nullable if `fragmentType` is array of non-nullable
26 | export function useFragment(
27 | _documentNode: DocumentNode,
28 | fragmentType: ReadonlyArray>>
29 | ): ReadonlyArray;
30 | // return array of nullable if `fragmentType` is array of nullable
31 | export function useFragment(
32 | _documentNode: DocumentNode,
33 | fragmentType: ReadonlyArray>> | null | undefined
34 | ): ReadonlyArray | null | undefined;
35 | export function useFragment(
36 | _documentNode: DocumentNode,
37 | fragmentType: FragmentType> | ReadonlyArray>> | null | undefined
38 | ): TType | ReadonlyArray | null | undefined {
39 | return fragmentType as any;
40 | }
41 |
42 |
43 | export function makeFragmentData<
44 | F extends DocumentNode,
45 | FT extends ResultOf
46 | >(data: FT, _fragment: F): FragmentType {
47 | return data as FragmentType;
48 | }
--------------------------------------------------------------------------------
/components/navigationButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface NavigationButtonsProps {
4 | backStack: any[];
5 | breadcrumb: any[];
6 | currentIndex: number;
7 | handleBackClick: () => void;
8 | handleForwardClick: () => void;
9 | reset: () => void;
10 | userInteractedWithPath: boolean;
11 | }
12 |
13 | export const NavigationButtons: React.FC = ({
14 | backStack,
15 | breadcrumb,
16 | currentIndex,
17 | handleBackClick,
18 | handleForwardClick,
19 | reset,
20 | userInteractedWithPath,
21 | }) => {
22 | return (
23 |
24 |
37 |
54 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/components/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 |
4 | type TooltipProps = {
5 | style: React.CSSProperties;
6 | content: JSX.Element[];
7 | plainText: string;
8 | onClose: () => void;
9 | onCopy: (event: React.MouseEvent) => void;
10 | };
11 |
12 | const Tooltip: React.FC = ({
13 | style,
14 | content,
15 | onClose,
16 | onCopy,
17 | }) => {
18 | return (
19 |
32 |
33 |
50 |
67 |
{content}
68 |
69 |
70 | );
71 | };
72 |
73 | export default Tooltip;
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "engines": {
3 | "node": "20.*.*"
4 | },
5 | "name": "guac-visualizer",
6 | "version": "0.0.0-development",
7 | "license": "Apache-2.0",
8 | "private": true,
9 | "scripts": {
10 | "dev": "next dev",
11 | "build": "next build",
12 | "start": "next start",
13 | "lint": "next lint",
14 | "compile": "graphql-codegen",
15 | "watch": "graphql-codegen -w"
16 | },
17 | "dependencies": {
18 | "3d-force-graph": "^1.71.2",
19 | "@apollo/client": "^3.8.4",
20 | "@apollo/react-hooks": "^4.0.0",
21 | "@floating-ui/dom": "^1.2.6",
22 | "@heroicons/react": "^2.0.18",
23 | "@textea/json-viewer": "^2.14.1",
24 | "@types/node": "^20.14.8",
25 | "@types/react": "18.0.27",
26 | "@types/react-cytoscapejs": "^1.2.2",
27 | "@types/react-dom": "18.0.10",
28 | "autoprefixer": "^10.4.14",
29 | "deepmerge": "^4.3.0",
30 | "eslint": "8.32.0",
31 | "eslint-config-next": "^13.5.3",
32 | "eslint-plugin-import": "^2.26.0",
33 | "eslint-plugin-jsx-a11y": "^6.5.1",
34 | "eslint-plugin-react": "^7.25.3",
35 | "form-data": "2.5.5",
36 | "got": "^14.4.3",
37 | "graphql": "^16.8.1",
38 | "graphql-codegen-apollo-next-ssr": "^1.7.4",
39 | "jerrypick": "^1.1.1",
40 | "js-yaml": "^4.1.1",
41 | "lodash": "^4.17.21",
42 | "next": "15.4.10",
43 | "next-transpile-modules": "^10.0.0",
44 | "postcss": "^8.4.31",
45 | "react": "^18.2.0",
46 | "react-cytoscapejs": "^2.0.0",
47 | "react-dom": "^18.2.0",
48 | "react-force-graph-2d": "^1.23.15",
49 | "react-router-dom": "^6.11.0",
50 | "react-select": "^5.7.2",
51 | "react-toggle-button": "^2.2.0",
52 | "swr": "^2.1.0",
53 | "tailwindcss": "^3.3.1",
54 | "typescript": "^4.9.4",
55 | "uuid": "^9.0.0"
56 | },
57 | "devDependencies": {
58 | "@graphql-codegen/cli": "^3.2.2",
59 | "@graphql-codegen/client-preset": "^2.1.1",
60 | "@graphql-codegen/typescript": "^3.0.2",
61 | "@graphql-codegen/typescript-operations": "^3.0.2",
62 | "@graphql-codegen/typescript-react-apollo": "^3.3.7",
63 | "@graphql-codegen/typescript-resolvers": "^3.1.1",
64 | "@types/cytoscape": "3.19.9",
65 | "@types/got": "^9.6.12",
66 | "@types/lodash": "^4.14.196",
67 | "@types/uuid": "^9.0.1",
68 | "cosmos-over-cytoscape": "^1.0.3",
69 | "react-multi-select-component": "^4.3.4",
70 | "styled-components": "^5.3.9"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2024 The GUAC Authors.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | name: ci
16 |
17 | on:
18 | pull_request:
19 | branches:
20 | - main
21 | types:
22 | - opened
23 | - synchronize
24 | - reopened
25 |
26 | permissions:
27 | actions: read
28 | contents: read
29 |
30 | jobs:
31 | build:
32 | name: Build image
33 | runs-on: ubuntu-latest
34 | outputs:
35 | image: ${{ env.IMAGE_URI }}
36 | digest: ${{ steps.build_image.outputs.IMAGE_DIGEST }}
37 | env:
38 | IMAGE_URI: ghcr.io/${{ github.repository }}
39 | BUILDER: paketobuildpacks/builder-jammy-base
40 | BUILDPACK: paketo-buildpacks/nodejs
41 | steps:
42 | - name: Checkout code
43 | uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # tag=v3
44 | with:
45 | persist-credentials: false
46 | - name: Login to GitHub Container Registry
47 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
48 | with:
49 | registry: ghcr.io
50 | username: ${{ github.actor }}
51 | password: ${{ secrets.GITHUB_TOKEN }}
52 | - name: Setup pack
53 | uses: buildpacks/github-actions/setup-pack@7fc3d673350db0fff960cc94a3b9b80e5b663ae2 # v5.0.0
54 | - name: Install cosign
55 | uses: sigstore/cosign-installer@v3.6.0 # main
56 | with:
57 | cosign-release: 'v2.4.0'
58 | - name: Install crane
59 | uses: imjasonh/setup-crane@5146f708a817ea23476677995bf2133943b9be0b # v0.1
60 | - name: Build image
61 | id: build_image
62 | run: |
63 | #!/usr/bin/env bash
64 | set -euo pipefail
65 | pack build --env NODE_ENV=production ${IMAGE_URI}:ci${{ github.run_id }} --builder ${BUILDER} --buildpack ${BUILDPACK}
66 | echo "IMAGE_DIGEST=$(crane digest ${IMAGE_URI}:ci${{ github.run_id }})" >> $GITHUB_OUTPUT
67 |
--------------------------------------------------------------------------------
/components/packages/packageNameSelect.tsx:
--------------------------------------------------------------------------------
1 | import client from "@/apollo/client";
2 | import {
3 | PackageVersionsDocument,
4 | PackageQualifier,
5 | } from "@/gql/__generated__/graphql";
6 | import React, { Dispatch, SetStateAction } from "react";
7 | import PackageGenericSelector, {
8 | PackageSelectorOption,
9 | } from "@/components/packages/packageGenericSelector";
10 |
11 | export type VersionQueryVersion = {
12 | __typename?: "PackageVersion";
13 | version: string;
14 | qualifiers: {
15 | __typename?: "PackageQualifier";
16 | key: string;
17 | value: string;
18 | }[];
19 | };
20 |
21 | const PackageNameSelect = ({
22 | label,
23 | options,
24 | setPackageNameFunc,
25 | setPackageVersionsFunc,
26 | packageType,
27 | packageNamespace,
28 | resetNameFunc,
29 | ...rest
30 | }: {
31 | label: string;
32 | options: PackageSelectorOption[];
33 | setPackageNameFunc: Dispatch>;
34 | setPackageVersionsFunc: Dispatch<
35 | SetStateAction[]>
36 | >;
37 | packageType: string;
38 | packageNamespace: string;
39 | resetNameFunc: () => void;
40 | disabled?: boolean;
41 | }) => {
42 | function toVersionString(v: VersionQueryVersion): string {
43 | return (
44 | v.version +
45 | JSON.stringify(
46 | v.qualifiers.map((l: PackageQualifier) => l.key + "=" + l.value)
47 | )
48 | );
49 | }
50 |
51 | const onSelectPackageName = (value: string) => {
52 | resetNameFunc();
53 | setPackageNameFunc(value);
54 |
55 | const packageVersionQuery = client.query({
56 | query: PackageVersionsDocument,
57 | variables: {
58 | filter: {
59 | name: value,
60 | type: packageType,
61 | namespace: packageNamespace,
62 | },
63 | },
64 | });
65 |
66 | packageVersionQuery.then((res) => {
67 | const sortablePackageVersions = [
68 | ...(res.data.packages[0].namespaces[0].names[0].versions ?? []),
69 | ].map((v) => {
70 | return { label: toVersionString(v), value: v };
71 | });
72 | setPackageVersionsFunc(
73 | sortablePackageVersions
74 | .sort((a, b) => a.label.localeCompare(b.label))
75 | .map((t) => ({ label: t.label, value: t.value }))
76 | );
77 | });
78 | };
79 |
80 | return (
81 |
87 | );
88 | };
89 |
90 | export default PackageNameSelect;
91 |
--------------------------------------------------------------------------------
/components/queryvuln/queryVuln.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useApolloClient } from "@apollo/client";
3 | import { CERTIFY_VULN_QUERY } from "./certifyVulnQuery";
4 | import { useRouter } from "next/navigation";
5 | import { ArrowRightCircleIcon } from "@heroicons/react/24/solid";
6 | import { useVulnResults } from "@/store/vulnResultsContext";
7 |
8 | const QueryCertifyVuln: React.FC = () => {
9 | const [vulnerabilityID, setVulnerabilityID] = useState("");
10 | const [results, setResults] = useState(null);
11 | const [searched, setSearched] = useState(false);
12 | const client = useApolloClient();
13 | const router = useRouter();
14 | const { setVulnResults } = useVulnResults();
15 |
16 | // triggers a GraphQL query based on the user input, updates the results state, and navigates to a URL with its corresponding id
17 | const handleVulnSearch = async () => {
18 | if (!vulnerabilityID) return;
19 | setSearched(true);
20 | const { data } = await client.query({
21 | query: CERTIFY_VULN_QUERY,
22 | variables: {
23 | filter: { vulnerability: { vulnerabilityID } },
24 | },
25 | });
26 |
27 | if (data.CertifyVuln && data.CertifyVuln.length > 0) {
28 | setResults(data.CertifyVuln);
29 | setVulnResults(data.CertifyVuln);
30 |
31 | const firstResultId = data.CertifyVuln[0].id;
32 | router.push(`/?path=${firstResultId}`);
33 | } else {
34 | setResults([]);
35 | setVulnResults([]);
36 | }
37 | setVulnerabilityID("");
38 | };
39 |
40 | return (
41 |
42 |
Query vulnerability
43 |
setVulnerabilityID(e.target.value)}
47 | placeholder="Enter vuln ID here..."
48 | />
49 |
55 | {results ? (
56 |
57 | {results.map((node) => {
58 | return (
59 |
60 |
61 | Name: {" "}
62 | {node.package.namespaces[0].names[0].name}
63 |
64 |
65 | Version:{" "}
66 | {node.package.namespaces[0].names[0].versions[0].version}
67 |
68 |
69 | Type:
70 | {node.package.type}
71 |
72 |
73 | Vulnerability ID:
74 | {node.vulnerability.vulnerabilityIDs[0].vulnerabilityID}
75 |
76 |
77 | Last scanned on{" "}
78 | {new Date(node.metadata.timeScanned).toLocaleString()}
79 |
80 |
81 | );
82 | })}
83 |
84 | ) : (
85 | searched && (
86 |
No results found.
87 | )
88 | )}
89 |
90 | );
91 | };
92 |
93 | export default QueryCertifyVuln;
94 |
--------------------------------------------------------------------------------
/components/packages/packageSelector.tsx:
--------------------------------------------------------------------------------
1 | import PackageTypeSelect from "@/components/packages/packageTypeSelect";
2 | import PackageNamespaceSelect from "@/components/packages/packageNamespaceSelect";
3 | import PackageNameSelect, {
4 | VersionQueryVersion,
5 | } from "@/components/packages/packageNameSelect";
6 | import PackageVersionSelect from "@/components/packages/packageVersionSelect";
7 | import { useState } from "react";
8 | import { PackageSelectorOption } from "@/components/packages/packageGenericSelector";
9 | import { GraphDataWithMetadata } from "@/components/graph/types";
10 |
11 | export const INITIAL_PACKAGE_NAMESPACES: PackageSelectorOption[] = [
12 | { label: "loading...", value: "loading" },
13 | ];
14 |
15 | export default function PackageSelector({
16 | packageTypes,
17 | setGraphData,
18 | resetTypeFunc,
19 | }: {
20 | packageTypes: PackageSelectorOption[];
21 | setGraphData: (data: GraphDataWithMetadata) => void;
22 | resetTypeFunc?: () => void;
23 | }) {
24 | const [packageType, setPackageType] = useState(null);
25 | const [packageNamespace, setPackageNamespace] = useState(null);
26 | const [packageName, setPackageName] = useState(null);
27 | const [packageNamespaces, setPackageNamespaces] = useState(
28 | INITIAL_PACKAGE_NAMESPACES
29 | );
30 | const [packageNames, setPackageNames] = useState(INITIAL_PACKAGE_NAMESPACES);
31 | const [packageVersions, setPackageVersions] =
32 | useState[]>(null);
33 |
34 | const resetNamespace = () => {
35 | setPackageNames(INITIAL_PACKAGE_NAMESPACES);
36 | setPackageName(null);
37 | resetName();
38 | };
39 |
40 | const resetName = () => {
41 | setPackageVersions(null);
42 | };
43 |
44 | const resetType = () => {
45 | setPackageNamespaces(INITIAL_PACKAGE_NAMESPACES);
46 | setPackageNamespace(null);
47 | resetNamespace();
48 | resetTypeFunc();
49 | };
50 |
51 | return (
52 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/components/layout/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useContext, useState } from "react";
4 | import Image from "next/image";
5 | import { MoonIcon, SunIcon, Bars3Icon } from "@heroicons/react/24/solid";
6 | import GuacVizThemeContext from "@/app/themeContext";
7 | import Link from "next/link";
8 | import packageJson from "../../package.json";
9 |
10 | export default function Header() {
11 | const { isDarkTheme, toggleThemeHandler } = useContext(GuacVizThemeContext);
12 | const [isMenuOpen, setIsMenuOpen] = useState(false);
13 |
14 | function NavigationLinks() {
15 | return (
16 |
38 | );
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
54 | *Experimental
55 |
56 | GUAC Visualizer{" "}
57 |
58 | v{packageJson.version}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
80 |
81 |
82 |
83 |
90 |
91 |
92 | {isMenuOpen && (
93 |
94 |
95 |
96 | )}
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/hooks/useGraphData.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { useRouter, useSearchParams } from "next/navigation";
5 | import { GraphDataWithMetadata } from "@/components/graph/types";
6 | import { NodeFragment } from "@/gql/types/nodeFragment";
7 | import {
8 | fetchNeighbors,
9 | GetNodeById,
10 | parseAndFilterGraph,
11 | } from "@/utils/graph_queries";
12 | import { ParseNode } from "@/utils/ggraph";
13 |
14 | export function useGraphData() {
15 | const searchParams = useSearchParams();
16 |
17 | const [graphData, setGraphData] = useState({
18 | nodes: [],
19 | links: [],
20 | });
21 | const [initialGraphData, setInitialGraphData] =
22 | useState(null);
23 | const [renderedInitialGraph, setRenderedInitialGraph] = useState(false);
24 |
25 | const [breadcrumbs, setBreadcrumbs] = useState([]);
26 |
27 | const addBreadcrumb = (nodeId: string) => {
28 | setBreadcrumbs([...breadcrumbs, nodeId]);
29 | };
30 |
31 | const removeBreadcrumbsFromIndex = (index: number) => {
32 | setBreadcrumbs(breadcrumbs.slice(0, index + 1));
33 | };
34 |
35 | // fetch and parse node information by IDs
36 | const fetchAndParseNodes = (nodeIds: string[]) =>
37 | Promise.all(nodeIds.map((nodeId) => GetNodeById(nodeId))).then((nodes) =>
38 | nodes.map((node) => ParseNode(node.node as NodeFragment))
39 | );
40 |
41 | // generate graph data from parsed nodes
42 | const generateGraphDataFromNodes = (parsedNodes: any[]) => {
43 | let graphData: GraphDataWithMetadata = { nodes: [], links: [] };
44 | parsedNodes.forEach((parsedNode) =>
45 | parseAndFilterGraph(graphData, parsedNode)
46 | );
47 | return graphData;
48 | };
49 |
50 | // fetch neighbor nodes and set new graph data
51 | const fetchAndSetGraphData = async (id: string | number) => {
52 | try {
53 | const res = await fetchNeighbors(id.toString());
54 | const newGraphData: GraphDataWithMetadata = { nodes: [], links: [] };
55 | res.forEach((n) => {
56 | let node = n as NodeFragment;
57 | parseAndFilterGraph(newGraphData, ParseNode(node));
58 | });
59 | setGraphData(newGraphData);
60 | } catch (error) {
61 | console.error(error);
62 | }
63 | };
64 |
65 | // load graph data based on an array of node IDs
66 | const loadGraphData = async (nodeIds: string[]) => {
67 | try {
68 | const parsedNodes = await fetchAndParseNodes(nodeIds);
69 | const newGraphData = await generateGraphDataFromNodes(parsedNodes);
70 | setGraphData(newGraphData);
71 | setRenderedInitialGraph(true);
72 | } catch (error) {
73 | console.error(error);
74 | }
75 | };
76 |
77 | // set the initial graph data and the current graph data
78 | const setGraphDataWithInitial = (data: GraphDataWithMetadata) => {
79 | setGraphData(data);
80 | if (!initialGraphData) {
81 | setInitialGraphData(data);
82 | }
83 | };
84 |
85 | // fetch and update data based on query parameters
86 | const fetchDataFromQueryParams = async () => {
87 | try {
88 | const myQuery = searchParams.get("path");
89 |
90 | if (myQuery) {
91 | const nodeIds = myQuery.split(",");
92 | const parsedNodes = await fetchAndParseNodes(nodeIds);
93 |
94 | if (parsedNodes.length > 0) {
95 | const graphData = generateGraphDataFromNodes(parsedNodes);
96 |
97 | setGraphData(graphData);
98 | setInitialGraphData(graphData);
99 | } else {
100 | setGraphData({ nodes: [], links: [] });
101 | }
102 | }
103 | } catch (error) {
104 | console.error("An error occurred:", error);
105 | }
106 | };
107 |
108 | useEffect(() => {
109 | fetchDataFromQueryParams();
110 | }, [searchParams]);
111 |
112 | return {
113 | graphData,
114 | setGraphData,
115 | initialGraphData,
116 | renderedInitialGraph,
117 | fetchAndSetGraphData,
118 | loadGraphData,
119 | setGraphDataWithInitial,
120 | breadcrumbs,
121 | addBreadcrumb,
122 | removeBreadcrumbsFromIndex,
123 | };
124 | }
125 |
--------------------------------------------------------------------------------
/components/known/knownQueries.ts:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 |
3 | // VULN QUERY
4 | export const GET_VULNS = gql`
5 | fragment allCertifyVulnTree on CertifyVuln {
6 | id
7 | package {
8 | id
9 | type
10 | namespaces {
11 | id
12 | namespace
13 | names {
14 | id
15 | name
16 | versions {
17 | id
18 | version
19 | qualifiers {
20 | key
21 | value
22 | }
23 | subpath
24 | }
25 | }
26 | }
27 | }
28 | vulnerability {
29 | id
30 | type
31 | vulnerabilityIDs {
32 | id
33 | vulnerabilityID
34 | }
35 | }
36 | metadata {
37 | dbUri
38 | dbVersion
39 | scannerUri
40 | scannerVersion
41 | timeScanned
42 | origin
43 | collector
44 | }
45 | }
46 |
47 | query CertifyVuln($pkgVersion: String!) {
48 | CertifyVuln(certifyVulnSpec: { package: { version: $pkgVersion } }) {
49 | ...allCertifyVulnTree
50 | }
51 | }
52 | `;
53 |
54 | // SBOM QUERY
55 | export const GET_SBOMS = gql`
56 | query HasSBOM($name: String!, $pkgID: ID!) {
57 | HasSBOM(
58 | hasSBOMSpec: { subject: { package: { name: $name, id: $pkgID } } }
59 | ) {
60 | ...allHasSBOMTree
61 | }
62 | }
63 |
64 | fragment allHasSBOMTree on HasSBOM {
65 | id
66 | subject {
67 | __typename
68 | ... on Package {
69 | id
70 | type
71 | namespaces {
72 | id
73 | namespace
74 | names {
75 | id
76 | name
77 | versions {
78 | id
79 | version
80 | qualifiers {
81 | key
82 | value
83 | }
84 | subpath
85 | }
86 | }
87 | }
88 | }
89 |
90 | ... on Artifact {
91 | id
92 | algorithm
93 | digest
94 | }
95 | }
96 | uri
97 | algorithm
98 | digest
99 | downloadLocation
100 | origin
101 | collector
102 | }
103 | `;
104 |
105 | // OCCURENCES + SLSA
106 | export const GET_OCCURRENCES_BY_VERSION = gql`
107 | fragment allIsOccurrencesTree on IsOccurrence {
108 | id
109 | subject {
110 | __typename
111 | ... on Package {
112 | id
113 | type
114 | namespaces {
115 | id
116 | namespace
117 | names {
118 | id
119 | name
120 | versions {
121 | id
122 | version
123 | qualifiers {
124 | key
125 | value
126 | }
127 | subpath
128 | }
129 | }
130 | }
131 | }
132 | ... on Source {
133 | id
134 | type
135 | namespaces {
136 | id
137 | namespace
138 | names {
139 | id
140 | name
141 | tag
142 | commit
143 | }
144 | }
145 | }
146 | }
147 | artifact {
148 | id
149 | algorithm
150 | digest
151 | }
152 | justification
153 | origin
154 | collector
155 | }
156 |
157 | query IsOccurrenceByVersion($pkgName: String!) {
158 | IsOccurrence(
159 | isOccurrenceSpec: { subject: { package: { name: $pkgName } } }
160 | ) {
161 | ...allIsOccurrencesTree
162 | }
163 | }
164 | `;
165 |
166 | export const GET_SLSAS = gql`
167 | fragment allHasSLSATree on HasSLSA {
168 | id
169 | subject {
170 | id
171 | algorithm
172 | digest
173 | }
174 | slsa {
175 | builtFrom {
176 | id
177 | algorithm
178 | digest
179 | }
180 | builtBy {
181 | id
182 | uri
183 | }
184 | buildType
185 | slsaPredicate {
186 | key
187 | value
188 | }
189 | slsaVersion
190 | startedOn
191 | finishedOn
192 | origin
193 | collector
194 | }
195 | }
196 |
197 | query HasSLSA($algorithm: String!, $digest: String!) {
198 | HasSLSA(
199 | hasSLSASpec: { subject: { algorithm: $algorithm, digest: $digest } }
200 | ) {
201 | ...allHasSLSATree
202 | }
203 | }
204 | `;
205 |
--------------------------------------------------------------------------------
/components/packages/packageVersionSelect.tsx:
--------------------------------------------------------------------------------
1 | import client from "@/apollo/client";
2 | import { parseAndFilterGraph } from "@/utils/graph_queries";
3 | import {
4 | AllPkgTreeFragment,
5 | NeighborsDocument,
6 | PackagesDocument,
7 | } from "@/gql/__generated__/graphql";
8 | import React from "react";
9 | import { ParseNode } from "@/utils/ggraph";
10 | import PackageGenericSelector, {
11 | PackageSelectorOption,
12 | } from "@/components/packages/packageGenericSelector";
13 | import { VersionQueryVersion } from "@/components/packages/packageNameSelect";
14 | import { GraphDataWithMetadata } from "@/components/graph/types";
15 | import { usePackageData } from "@/store/packageDataContext";
16 |
17 | type PackageNamespaceQuerySpec = {
18 | type: string;
19 | name: string;
20 | namespace: string;
21 | version: string;
22 | qualifiers: PackageNamespaceQueryQualifier[];
23 | matchOnlyEmptyQualifiers: boolean;
24 | };
25 |
26 | type PackageNamespaceQueryQualifier = {
27 | __typename?: "PackageQualifier";
28 | key: string;
29 | value: string;
30 | };
31 |
32 | const PackageVersionSelect = ({
33 | label,
34 | options,
35 | setGraphDataFunc,
36 | packageType,
37 | packageNamespace,
38 | packageName,
39 | ...rest
40 | }: {
41 | label: string;
42 | options: PackageSelectorOption[];
43 | setGraphDataFunc: (data: GraphDataWithMetadata) => void;
44 | packageType: string;
45 | packageNamespace: string;
46 | packageName: string;
47 | disabled?: boolean;
48 | }) => {
49 | const { setPkgContext, setPkgID, setPackageName, setPkgType, setPkgVersion } =
50 | usePackageData();
51 | const onSelectPackageVersion = (option: VersionQueryVersion) => {
52 | let specVersion;
53 | let specQualifiers: {
54 | __typename?: "PackageQualifier";
55 | key: string;
56 | value: string;
57 | }[];
58 | let specMatchOnlyEmptyQualifiers = false;
59 |
60 | if (option.version != "") {
61 | specVersion = option.version;
62 | }
63 |
64 | if (option.qualifiers.length > 0) {
65 | specQualifiers = option.qualifiers;
66 | } else {
67 | specMatchOnlyEmptyQualifiers = true;
68 | }
69 |
70 | let spec: PackageNamespaceQuerySpec = {
71 | type: packageType,
72 | name: packageName,
73 | namespace: packageNamespace,
74 | version: specVersion,
75 | qualifiers: specQualifiers,
76 | matchOnlyEmptyQualifiers: specMatchOnlyEmptyQualifiers,
77 | };
78 |
79 | const packageNamespacesQuery = client.query({
80 | query: PackagesDocument,
81 | variables: {
82 | filter: spec,
83 | },
84 | fetchPolicy: "no-cache",
85 | });
86 |
87 | packageNamespacesQuery.then((res) => {
88 | const pkg = res.data.packages[0] as AllPkgTreeFragment;
89 | const pkgID = pkg.namespaces[0].names[0].versions[0].id;
90 | const pkgName = pkg.namespaces[0].names[0].name;
91 | const pkgType = pkg.type;
92 | const pkgVersion = pkg.namespaces[0].names[0].versions[0].version;
93 |
94 | setPkgContext(pkg);
95 | setPkgID(pkgID);
96 | setPackageName(pkgName);
97 | setPkgType(pkgType);
98 | setPkgVersion(pkgVersion);
99 |
100 | const graphData: GraphDataWithMetadata = { nodes: [], links: [] };
101 | const parsedNode = ParseNode(pkg);
102 |
103 | parseAndFilterGraph(graphData, parsedNode);
104 | client
105 | .query({
106 | query: NeighborsDocument,
107 | variables: {
108 | node: pkg.id,
109 | usingOnly: [],
110 | },
111 | })
112 | .then((r) => processGraphData(r.data.neighbors, graphData));
113 | });
114 | };
115 |
116 | const processGraphData = (
117 | packages: any[],
118 | graphData: GraphDataWithMetadata
119 | ) => {
120 | let currentGraphData = graphData;
121 | packages.forEach((e) => {
122 | const parsedGraphData = ParseNode(e);
123 | parseAndFilterGraph(currentGraphData, parsedGraphData);
124 | });
125 | console.log("set graph data from selector", graphData);
126 | setGraphDataFunc(currentGraphData);
127 | };
128 |
129 | if (!options) {
130 | options = [];
131 | }
132 |
133 | return (
134 |
140 | );
141 | };
142 |
143 | export default PackageVersionSelect;
144 |
--------------------------------------------------------------------------------
/hooks/useBreadcrumbNavigation.ts:
--------------------------------------------------------------------------------
1 | import { GraphDataWithMetadata } from "@/components/graph/types";
2 | import { useState, useCallback, useEffect } from "react";
3 |
4 | /*
5 | custom hook for managing breadcrumb navigation within a graph visualization.
6 | This hook tracks the user's path through the graph, allowing them to navigate
7 | backward and forward, and interact with nodes in the navigation trail.
8 | */
9 |
10 | export const useBreadcrumbNavigation = (
11 | fetchAndSetGraphData: (id: string) => void,
12 | initialGraphData: GraphDataWithMetadata,
13 | setGraphData: (data: GraphDataWithMetadata) => void
14 | ) => {
15 | const [breadcrumb, setBreadcrumb] = useState([]);
16 | const [backStack, setBackStack] = useState([]);
17 | const [forwardStack, setForwardStack] = useState([]);
18 | const [currentNode, setCurrentNode] = useState(null);
19 | const [currentIndex, setCurrentIndex] = useState(0);
20 | const [userInteractedWithPath, setUserInteractedWithPath] = useState(false);
21 | const [firstNode, setFirstNode] = useState(null);
22 | const [currentNodeId, setCurrentNodeId] = useState(null);
23 |
24 | const handleBreadcrumbClick = (nodeIndex: number) => {
25 | const newBackStack = breadcrumb.slice(0, nodeIndex);
26 | const newForwardStack = breadcrumb.slice(nodeIndex + 1);
27 |
28 | setBackStack(newBackStack);
29 | setForwardStack(newForwardStack);
30 | setCurrentNode(breadcrumb[nodeIndex]);
31 | setCurrentIndex(nodeIndex);
32 |
33 | fetchAndSetGraphData(breadcrumb[nodeIndex].id);
34 | };
35 |
36 | // triggered when a node is clicked in the graph visualization.
37 | // updates the breadcrumb trail, sets the current node, and fetches data for the clicked node.
38 | const handleNodeClick = useCallback(
39 | (node) => {
40 | setUserInteractedWithPath(true);
41 |
42 | if (currentNode) {
43 | setBackStack((prevBackStack) => [...prevBackStack, currentNode]);
44 | }
45 | setForwardStack([]);
46 |
47 | setCurrentNode(node);
48 |
49 | let nodeName = node.label || "Unnamed Node";
50 | const count = breadcrumb.filter(
51 | (item) => item.label.split("[")[0] === nodeName
52 | ).length;
53 |
54 | if (count > 0) {
55 | nodeName = `${nodeName}[${count + 1}]`;
56 | }
57 |
58 | setBreadcrumb((prevBreadcrumb) => {
59 | const newBreadcrumb = [
60 | ...prevBreadcrumb.slice(0, currentIndex + 1),
61 | { label: nodeName, id: node.id },
62 | ];
63 | setCurrentIndex(newBreadcrumb.length - 1);
64 | return newBreadcrumb;
65 | });
66 |
67 | fetchAndSetGraphData(node.id);
68 | },
69 | [currentNode, breadcrumb, firstNode]
70 | );
71 |
72 | const handleBackClick = () => {
73 | if (currentIndex === 0 || backStack.length === 0) return;
74 |
75 | const newForwardStack = [currentNode, ...forwardStack];
76 | const newBackStack = [...backStack];
77 | const lastNode = newBackStack.pop();
78 |
79 | setCurrentNode(lastNode);
80 | setCurrentIndex(currentIndex - 1);
81 | setForwardStack(newForwardStack);
82 | setBackStack(newBackStack);
83 |
84 | fetchAndSetGraphData(lastNode.id);
85 | };
86 |
87 | const handleForwardClick = () => {
88 | if (currentIndex >= breadcrumb.length - 1 || forwardStack.length === 0)
89 | return;
90 |
91 | const newForwardStack = [...forwardStack];
92 | const nextNode = newForwardStack.shift();
93 | const newBackStack = [...backStack, currentNode];
94 |
95 | setCurrentNode(nextNode);
96 | setCurrentIndex(currentIndex + 1);
97 | setBackStack(newBackStack);
98 | setForwardStack(newForwardStack);
99 |
100 | fetchAndSetGraphData(nextNode.id);
101 | };
102 |
103 | const reset = () => {
104 | if (initialGraphData) {
105 | setGraphData(initialGraphData);
106 | setBackStack([]);
107 | setForwardStack([]);
108 | setCurrentNode(null);
109 | setFirstNode(null);
110 | setBreadcrumb([]);
111 | setUserInteractedWithPath(false);
112 | }
113 | };
114 |
115 | useEffect(() => {
116 | if (currentNodeId !== null) {
117 | fetchAndSetGraphData(currentNodeId);
118 | }
119 | }, [currentNodeId]);
120 |
121 | return {
122 | breadcrumb,
123 | backStack,
124 | forwardStack,
125 | currentNode,
126 | currentIndex,
127 | userInteractedWithPath,
128 | handleBreadcrumbClick,
129 | handleNodeClick,
130 | handleBackClick,
131 | handleForwardClick,
132 | reset,
133 | };
134 | };
135 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 The GUAC Authors.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | name: release-guac-visualizer-image
16 |
17 | on:
18 | push:
19 | tags:
20 | - 'v*'
21 |
22 | permissions:
23 | actions: read # for detecting the Github Actions environment.
24 | packages: read # To publish container images to GHCR
25 |
26 | jobs:
27 | build-n-release:
28 | name: Build and publish image
29 | runs-on: ubuntu-latest
30 | outputs:
31 | image: ${{ env.IMAGE_URI }}
32 | digest: ${{ steps.build_n_publish_image.outputs.IMAGE_DIGEST }}
33 | env:
34 | IMAGE_URI: ghcr.io/${{ github.repository }}
35 | BUILDER: paketobuildpacks/builder-jammy-base
36 | BUILDPACK: paketo-buildpacks/nodejs
37 | BUILDPACK_SBOM_OUTPUT_DIR: sbom-output-dir
38 | BUILDPACK_SPDX_SBOM: "launch/paketo-buildpacks_yarn-install/launch-modules/sbom.spdx.json"
39 | permissions:
40 | packages: write # To publish container images to GHCR
41 | id-token: write # For sigstore to use our OIDC token for signing
42 | steps:
43 | - name: Checkout code
44 | uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # tag=v3
45 | with:
46 | persist-credentials: false
47 | - name: Login to GitHub Container Registry
48 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
49 | with:
50 | registry: ghcr.io
51 | username: ${{ github.actor }}
52 | password: ${{ secrets.GITHUB_TOKEN }}
53 | - name: Setup pack
54 | uses: buildpacks/github-actions/setup-pack@7fc3d673350db0fff960cc94a3b9b80e5b663ae2 # v5.0.0
55 | - name: Install cosign
56 | uses: sigstore/cosign-installer@v3.6.0 # main
57 | with:
58 | cosign-release: 'v2.4.0'
59 | - name: Install crane
60 | uses: imjasonh/setup-crane@5146f708a817ea23476677995bf2133943b9be0b # v0.1
61 | - name: Update version string to match git tag
62 | run: |
63 | #!/usr/bin/env bash
64 | set -euo pipefail
65 | npm version --no-git-tag-version $(git describe --tags --abbrev=0)
66 | - name: Build and publish image
67 | id: build_n_publish_image
68 | run: |
69 | #!/usr/bin/env bash
70 | set -euo pipefail
71 | pack build --env NODE_ENV=production ${IMAGE_URI}:${GITHUB_REF_NAME} --builder ${BUILDER} --buildpack ${BUILDPACK} --publish --sbom-output-dir ${BUILDPACK_SBOM_OUTPUT_DIR}
72 | echo "IMAGE_DIGEST=$(crane digest ${IMAGE_URI}:${GITHUB_REF_NAME})" >> $GITHUB_OUTPUT
73 | - name: Sign and verify image
74 | run: |
75 | #!/usr/bin/env bash
76 | set -euo pipefail
77 | cosign sign -a git_sha=$GITHUB_SHA ${IMAGE_URI_DIGEST} --yes
78 | cosign attach sbom --sbom ${BUILDPACK_SBOM_OUTPUT_DIR}/${BUILDPACK_SPDX_SBOM} ${IMAGE_URI_DIGEST}
79 | cosign sign -a git_sha=$GITHUB_SHA --attachment sbom ${IMAGE_URI_DIGEST} --yes
80 | cosign verify ${IMAGE_URI_DIGEST} --certificate-oidc-issuer ${GITHUB_ACITONS_OIDC_ISSUER} --certificate-identity ${COSIGN_KEYLESS_SIGNING_CERT_SUBJECT}
81 | shell: bash
82 | env:
83 | IMAGE_URI_DIGEST: ${{ env.IMAGE_URI }}@${{ steps.build_n_publish_image.outputs.IMAGE_DIGEST }}
84 | GITHUB_ACITONS_OIDC_ISSUER: https://token.actions.githubusercontent.com
85 | COSIGN_KEYLESS_SIGNING_CERT_SUBJECT: https://github.com/${{ github.repository }}/.github/workflows/release.yaml@${{ github.ref }}
86 |
87 | provenance-container:
88 | name: generate provenance for container
89 | needs: [build-n-release]
90 | if: startsWith(github.ref, 'refs/tags/')
91 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 # must use semver here
92 | permissions:
93 | actions: read
94 | packages: write
95 | id-token: write # needed for signing the images with GitHub OIDC Token **not production ready**
96 | with:
97 | image: ${{ needs.build-n-release.outputs.image }}
98 | digest: ${{ needs.build-n-release.outputs.digest }}
99 | registry-username: ${{ github.actor }}
100 | secrets:
101 | registry-password: ${{ secrets.GITHUB_TOKEN }}
102 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, Suspense } from "react";
4 | import Graph from "@/components/graph/Graph";
5 | // import { HighlightToggles } from "@/components/highlightToggles";
6 | import { useGraphData } from "@/hooks/useGraphData";
7 | import { usePackageData } from "@/hooks/usePackageData";
8 | import PackageSelector from "@/components/packages/packageSelector";
9 | import { Breadcrumb } from "@/components/breadcrumb";
10 | import { NavigationButtons } from "@/components/navigationButton";
11 | import { useBreadcrumbNavigation } from "@/hooks/useBreadcrumbNavigation";
12 | import QueryVuln from "@/components/queryvuln/queryVuln";
13 | import { ApolloProvider } from "@apollo/client";
14 | import client from "@/apollo/client";
15 | import { useDimensions } from "@/hooks/useDimensions";
16 | import { PackageDataProvider } from "@/store/packageDataContext";
17 | import NodeInfo from "@/components/nodeInfo/nodeInfo";
18 | import { VulnResultsProvider } from "@/store/vulnResultsContext";
19 |
20 | function HomeContent() {
21 | const [highlights, setHighlights] = useState({
22 | artifact: false,
23 | vuln: false,
24 | sbom: false,
25 | builder: false,
26 | });
27 |
28 | const {
29 | graphData,
30 | setGraphData,
31 | initialGraphData,
32 | fetchAndSetGraphData,
33 | setGraphDataWithInitial,
34 | } = useGraphData();
35 |
36 | const {
37 | breadcrumb,
38 | backStack,
39 | currentIndex,
40 | userInteractedWithPath,
41 | handleBreadcrumbClick,
42 | handleNodeClick,
43 | handleBackClick,
44 | handleForwardClick,
45 | reset,
46 | } = useBreadcrumbNavigation(
47 | fetchAndSetGraphData,
48 | initialGraphData,
49 | setGraphData
50 | );
51 |
52 | const { packageTypes, packageLoading, packageError } = usePackageData();
53 |
54 | const { containerWidth, containerHeight } = useDimensions();
55 |
56 | return (
57 |
58 | {packageLoading ? (
59 | Loading package types...
60 | ) : packageError ? (
61 | Error loading package types!
62 | ) : (
63 |
73 | )}
74 |
75 |
76 | {/* TODO: Fix highlighter, until then keep it commented */}
77 | {/*
78 |
79 | Tip: Use click
80 | and scroll to adjust graph.
81 | Right clicking a node displays more information.
82 |
83 |
Highlight Nodes
84 |
93 |
*/}
94 |
95 |
96 |
110 | {graphData.nodes.length !== 0 &&
111 | graphData.links.length !== 0 && (
112 |
121 | )}
122 | item.label)}
124 | handleNodeClick={handleBreadcrumbClick}
125 | currentIndex={currentIndex}
126 | />
127 |
128 |
129 |
130 |
131 | );
132 | }
133 |
134 | // Loading fallback component
135 | function LoadingFallback() {
136 | return (
137 |
140 | );
141 | }
142 |
143 | export default function Home() {
144 | return (
145 |
146 |
147 |
148 |
149 | }>
150 |
151 |
152 |
153 |
154 |
155 |
156 | );
157 | }
--------------------------------------------------------------------------------
/components/graph/ForceGraph2D.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRef, useState } from "react";
4 | import ForceGraph, {
5 | LinkObject,
6 | ForceGraphMethods,
7 | GraphData,
8 | NodeObject,
9 | } from "react-force-graph-2d";
10 | import { CanvasCustomRenderFn, NodeAccessor } from "@/components/graph/types";
11 | import { cloneDeep } from "lodash";
12 | import Tooltip from "@/components/tooltip";
13 |
14 | type ForceGraph2DWrapperProps = {
15 | graphData: GraphData;
16 | nodeAutoColorBy?: string;
17 | linkDirectionalArrowLength: number;
18 | linkDirectionalArrowRelPos: number;
19 | onNodeClick?: (node: NodeObject, event: MouseEvent) => void;
20 | nodeLabel: NodeAccessor;
21 | linkLabel?: string | ((link: LinkObject) => string);
22 | linkDirectionalParticles: number;
23 | linkSource?: string;
24 | linkTarget?: string;
25 | selectedNode?: any;
26 | dataFetcher?: (id: string | number) => void;
27 | nodeCanvasObject: CanvasCustomRenderFn;
28 | nodeCanvasObjectMode?: string | ((obj: NodeObject) => any);
29 | onNodeDragEnd?: (
30 | node: NodeObject,
31 | translate: { x: number; y: number }
32 | ) => void;
33 | bgdColor?: string;
34 | d3AlphaDecay: number;
35 | d3VelocityDecay: number;
36 | };
37 |
38 | type ResponsiveProps = {
39 | width?: number;
40 | height?: number;
41 | };
42 |
43 | const ForceGraph2D: React.FC = ({
44 | graphData,
45 | nodeAutoColorBy,
46 | linkDirectionalArrowLength,
47 | linkDirectionalArrowRelPos,
48 | onNodeClick,
49 | nodeLabel,
50 | linkLabel,
51 | linkDirectionalParticles,
52 | linkSource,
53 | linkTarget,
54 | selectedNode,
55 | nodeCanvasObject,
56 | nodeCanvasObjectMode,
57 | onNodeDragEnd,
58 | dataFetcher,
59 | width,
60 | height,
61 | bgdColor,
62 | d3AlphaDecay,
63 | d3VelocityDecay,
64 | }) => {
65 | const [tooltipStyle, setTooltipStyle] = useState({
66 | display: "none",
67 | top: 0,
68 | left: 0,
69 | });
70 | const [tooltipContent, setTooltipContent] = useState([]);
71 | const [tooltipPlainText, setTooltipPlainText] = useState("");
72 | const [copyTooltipStyle, setCopyTooltipStyle] = useState({
73 | display: "none",
74 | top: 0,
75 | left: 0,
76 | });
77 |
78 | const fgRef = useRef();
79 |
80 | if (selectedNode) {
81 | const sn = graphData.nodes.find((node) => node.id === selectedNode.value);
82 | if (sn && fgRef.current) {
83 | }
84 | }
85 |
86 | const buildTooltipContent = (
87 | obj: any,
88 | parentKey = "",
89 | level = 0,
90 | cache: Map = new Map()
91 | ): [JSX.Element[], string[]] => {
92 | if (cache.has(obj)) {
93 | return cache.get(obj)!;
94 | }
95 |
96 | let content: JSX.Element[] = [];
97 | let plainTextContent: string[] = [];
98 | let filteredKeys = ["__typename", "__indexColor", "index", "expanded"];
99 |
100 | for (const [key, value] of Object.entries(obj)) {
101 | if (filteredKeys.includes(key)) continue;
102 | let newKey = parentKey ? `${parentKey}_${key}` : key;
103 | if (typeof value !== "object" || value === null) {
104 | let specialFormat =
105 | newKey === "package_namespaces_0_names_0_name" ? "red-bold-text" : "";
106 | content.push(
107 |
108 | {`- ${newKey}: `}
109 | {String(value)}
110 |
111 | );
112 | plainTextContent.push(
113 | `${" ".repeat(level)}- ${newKey}: ${String(value)}`
114 | );
115 | } else if (Array.isArray(value)) {
116 | for (let index = 0; index < value.length; index++) {
117 | const [jsx, text] = buildTooltipContent(
118 | value[index],
119 | `${newKey}_${index}`,
120 | level + 1,
121 | cache
122 | );
123 | content.push(...jsx);
124 | plainTextContent.push(...text);
125 | }
126 | } else {
127 | const [jsx, text] = buildTooltipContent(
128 | value,
129 | newKey,
130 | level + 1,
131 | cache
132 | );
133 | content.push(...jsx);
134 | plainTextContent.push(...text);
135 | }
136 | }
137 |
138 | cache.set(obj, [content, plainTextContent]);
139 | return [content, plainTextContent];
140 | };
141 |
142 | const copyToClipboard = (event: React.MouseEvent) => {
143 | navigator.clipboard.writeText(tooltipPlainText);
144 | setCopyTooltipStyle({
145 | display: "block",
146 | top: event.clientY,
147 | left: event.clientX,
148 | });
149 | setTimeout(
150 | () => setCopyTooltipStyle({ display: "none", top: 0, left: 0 }),
151 | 1000
152 | );
153 | };
154 |
155 | return (
156 |
157 |
162 | setTooltipStyle((prev) => ({ ...prev, display: "none" }))
163 | }
164 | onCopy={copyToClipboard}
165 | />
166 |
175 | Copied to clipboard!
176 |
177 | {
188 | event.preventDefault();
189 | let [content, plainText] = buildTooltipContent(cloneDeep(node));
190 | setTooltipStyle({
191 | display: "block",
192 | top: event.clientY,
193 | left: event.clientX,
194 | });
195 | setTooltipContent(content);
196 | setTooltipPlainText(plainText.join("\n"));
197 | }}
198 | nodeLabel={nodeLabel}
199 | linkLabel={linkLabel}
200 | nodeId="id"
201 | linkDirectionalParticles={linkDirectionalParticles}
202 | linkDirectionalParticleWidth={10.5}
203 | linkSource={linkSource}
204 | linkTarget={linkTarget}
205 | linkWidth={3}
206 | cooldownTicks={100}
207 | cooldownTime={15000}
208 | nodeCanvasObjectMode={nodeCanvasObjectMode}
209 | nodeCanvasObject={nodeCanvasObject}
210 | onNodeDragEnd={onNodeDragEnd}
211 | d3AlphaDecay={d3AlphaDecay}
212 | d3VelocityDecay={d3VelocityDecay}
213 | />
214 |
215 | );
216 | };
217 |
218 | export default ForceGraph2D;
219 |
--------------------------------------------------------------------------------
/components/graph/Graph.tsx:
--------------------------------------------------------------------------------
1 | import ForceGraph2D from "@/components/graph/ForceGraph2DWrapper";
2 | import {
3 | GraphDataWithMetadata,
4 | NodeMetadata,
5 | NodeWithMetadataObject,
6 | } from "@/components/graph/types";
7 | import { GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
8 |
9 | const getMetadataFromGraphData = (graphData: {
10 | nodes: NodeWithMetadataObject[];
11 | links: LinkObject[];
12 | }): {
13 | metadata: { [id: string | number]: NodeMetadata };
14 | graphData: GraphData;
15 | } => {
16 | const metadataObj: { [id: string | number]: NodeMetadata } = {};
17 |
18 | graphData.nodes.forEach((node: NodeWithMetadataObject) => {
19 | metadataObj[node.id] = {
20 | type: node.type,
21 | label: node.label,
22 | };
23 | });
24 |
25 | return { metadata: metadataObj, graphData: graphData };
26 | };
27 |
28 | export default function Graph({
29 | graphData,
30 | options,
31 | containerOptions,
32 | onNodeClick,
33 | }: {
34 | graphData: GraphDataWithMetadata;
35 | options: {
36 | highlightArtifact: boolean;
37 | highlightVuln: boolean;
38 | highlightSbom: boolean;
39 | highlightBuilder: boolean;
40 | };
41 | containerOptions: {
42 | width: number;
43 | height: number;
44 | };
45 | onNodeClick: (node: any) => void;
46 | }) {
47 | const bgColor = "#e7e5e4";
48 |
49 | const { metadata } = getMetadataFromGraphData(graphData);
50 |
51 | const nodeLabelFromNodeObject = (node: NodeObject) => {
52 | return metadata[node.id]?.label;
53 | };
54 |
55 | const nodeTypeFromNodeObject = (node: NodeObject) => {
56 | return metadata[node.id]?.type;
57 | };
58 |
59 | const nodeCanvasObject = (
60 | node: NodeObject,
61 | ctx: CanvasRenderingContext2D
62 | ) => {
63 | const shapeSize = 10;
64 | const nodeType = nodeTypeFromNodeObject(node);
65 | const applyRedFillAndOutline =
66 | (options.highlightArtifact && nodeType === "Artifact") ||
67 | (options.highlightVuln && nodeType === "VulnerabilityID") ||
68 | (options.highlightSbom && nodeType === "IsDependency") ||
69 | (options.highlightBuilder && nodeType === "PackageType");
70 |
71 | switch (nodeType) {
72 | case "PackageType":
73 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "teal";
74 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
75 | ctx.shadowBlur = 3;
76 | ctx.fillRect(node.x - 6, node.y - 4, 12, 8);
77 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
78 | ctx.shadowBlur = 3;
79 | break;
80 | case "PackageName":
81 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "#7D0541";
82 | ctx.beginPath();
83 | ctx.arc(node.x, node.y, 6, 0, 2 * Math.PI, false);
84 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
85 | ctx.shadowBlur = 3;
86 | ctx.fill();
87 | break;
88 | case "PackageVersion":
89 | var side = 10;
90 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
91 | ctx.shadowBlur = 3;
92 | ctx.fillRect(node.x - side / 2, node.y - side / 2, side, side);
93 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
94 | ctx.shadowBlur = 3;
95 | ctx.fill();
96 | break;
97 | case "PackageNamespace":
98 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "DarkOrchid";
99 | ctx.beginPath();
100 | ctx.arc(node.x, node.y, 6, 0, 2 * Math.PI, false);
101 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
102 | ctx.shadowBlur = 3;
103 | ctx.fill();
104 | break;
105 | case "IsOccurrence":
106 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "Goldenrod";
107 | ctx.beginPath();
108 | ctx.fillRect(node.x - side / 2, node.y - side / 2, side, side);
109 | ctx.arc(node.x, node.y, 6, 0, 2 * Math.PI, false);
110 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
111 | ctx.shadowBlur = 3;
112 | ctx.fill();
113 | break;
114 | case "HasSlsa":
115 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "DarkTurquoise";
116 | ctx.beginPath();
117 | ctx.arc(node.x, node.y, 6, 0, 2 * Math.PI, false);
118 | ctx.shadowColor = "rgba(0, 0, 0, 0.1)";
119 | ctx.shadowBlur = 2;
120 | ctx.fill();
121 | break;
122 | case "Builder":
123 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "RebeccaPurple";
124 | ctx.beginPath();
125 | ctx.arc(node.x, node.y, 6, 0, 2 * Math.PI, false);
126 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
127 | ctx.shadowBlur = 3;
128 | ctx.fill();
129 | break;
130 | case "Vulnerability":
131 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "Brown";
132 | ctx.beginPath();
133 | ctx.moveTo(node.x, node.y - 6);
134 | ctx.lineTo(node.x - 6, node.y + 6);
135 | ctx.lineTo(node.x + 6, node.y + 6);
136 | ctx.closePath();
137 | ctx.fill();
138 | break;
139 | case "VulnerabilityID":
140 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "darkorange";
141 | ctx.beginPath();
142 | ctx.arc(node.x, node.y, 6, 0, 2 * Math.PI, false);
143 | ctx.fill();
144 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
145 | ctx.shadowBlur = 3;
146 | break;
147 | case "VulnEqual":
148 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "DarkMagenta";
149 | ctx.beginPath();
150 | ctx.arc(node.x, node.y, 6, 0, 2 * Math.PI, false);
151 | ctx.lineTo(node.x, node.y);
152 | ctx.closePath();
153 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
154 | ctx.shadowBlur = 3;
155 | ctx.fill();
156 | break;
157 | case "CertifyLegal":
158 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "DarkOliveGreen";
159 | ctx.beginPath();
160 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
161 | ctx.stroke();
162 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
163 | ctx.shadowBlur = 3;
164 | ctx.fill();
165 | break;
166 | case "License":
167 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "DarkOliveGreen";
168 | ctx.beginPath();
169 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
170 | ctx.stroke();
171 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
172 | ctx.shadowBlur = 3;
173 | ctx.fill();
174 | break;
175 | case "IsDependency":
176 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "hotpink";
177 | ctx.beginPath();
178 | ctx.moveTo(node.x, node.y - shapeSize / 2);
179 | ctx.lineTo(node.x - shapeSize / 2, node.y + shapeSize / 2);
180 | ctx.lineTo(node.x + shapeSize / 2, node.y + shapeSize / 2);
181 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
182 | ctx.shadowBlur = 3;
183 | ctx.fill();
184 | break;
185 | case "CertifyVuln":
186 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "tomato";
187 | const sideLength =
188 | shapeSize / Math.sqrt(3.5 - 1.5 * Math.cos(Math.PI / 4));
189 | ctx.beginPath();
190 | ctx.moveTo(node.x + sideLength, node.y);
191 | ctx.lineTo(node.x + sideLength / 2, node.y - sideLength / 2);
192 | ctx.lineTo(node.x - sideLength / 2, node.y - sideLength / 2);
193 | ctx.lineTo(node.x - sideLength, node.y);
194 | ctx.lineTo(node.x - sideLength / 2, node.y + sideLength / 2);
195 | ctx.lineTo(node.x + sideLength / 2, node.y + sideLength / 2);
196 | ctx.closePath();
197 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
198 | ctx.shadowBlur = 3;
199 | ctx.fill();
200 | break;
201 | case "NoVuln":
202 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "green";
203 | ctx.beginPath();
204 | ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
205 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
206 | ctx.shadowBlur = 3;
207 | ctx.fill();
208 | break;
209 | case "Artifact":
210 | ctx.fillStyle = "Coral";
211 | ctx.beginPath();
212 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
213 | ctx.stroke();
214 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
215 | ctx.shadowBlur = 3;
216 | ctx.fill();
217 | break;
218 | case "HasSourceAt":
219 | ctx.fillStyle = "#C4AEAD";
220 | ctx.beginPath();
221 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
222 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
223 | ctx.shadowBlur = 3;
224 | ctx.fill();
225 | break;
226 | case "SourceType":
227 | ctx.fillStyle = "#997070";
228 | ctx.beginPath();
229 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
230 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
231 | ctx.shadowBlur = 3;
232 | ctx.fill();
233 | break;
234 | case "SourceName":
235 | ctx.strokeStyle = "red";
236 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "SandyBrown";
237 | ctx.beginPath();
238 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
239 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
240 | ctx.shadowBlur = 3;
241 | ctx.fill();
242 | break;
243 | case "SourceNamespace":
244 | ctx.fillStyle = "#C48189";
245 | ctx.beginPath();
246 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
247 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
248 | ctx.shadowBlur = 3;
249 | ctx.fill();
250 | break;
251 | case "CertifyScorecard":
252 | ctx.fillStyle = "#7E354D";
253 | ctx.beginPath();
254 | ctx.arc(node.x, node.y, shapeSize / 2, 0, 2 * Math.PI);
255 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
256 | ctx.shadowBlur = 3;
257 | ctx.fill();
258 | break;
259 | default:
260 | ctx.fillStyle = applyRedFillAndOutline ? "red" : "blue";
261 | ctx.beginPath();
262 | ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
263 | ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
264 | ctx.shadowBlur = 3;
265 | ctx.fill();
266 | break;
267 | }
268 | ctx.fillText(nodeLabelFromNodeObject(node), node.x, node.y + 13);
269 | };
270 |
271 | return (
272 | {
274 | onNodeClick(node);
275 | }}
276 | bgdColor={bgColor}
277 | graphData={graphData}
278 | nodeLabel={nodeLabelFromNodeObject}
279 | linkDirectionalArrowLength={9}
280 | linkDirectionalArrowRelPos={3}
281 | linkDirectionalParticles={0}
282 | width={containerOptions.width}
283 | height={containerOptions.height}
284 | onNodeDragEnd={(node) => {
285 | node.fx = node.x;
286 | node.fy = node.y;
287 | }}
288 | nodeCanvasObject={nodeCanvasObject}
289 | d3AlphaDecay={0.000288}
290 | d3VelocityDecay={0.5}
291 | />
292 | );
293 | }
294 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2022 The GUAC Authors
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/components/known/knownInfo.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@apollo/client";
2 | import React, { useEffect, useState } from "react";
3 | import { usePackageData } from "@/store/packageDataContext";
4 | import {
5 | ExclamationCircleIcon,
6 | FolderOpenIcon,
7 | Square3Stack3DIcon,
8 | TrashIcon,
9 | } from "@heroicons/react/24/solid";
10 | import { useRouter } from "next/navigation";
11 | import {
12 | GET_OCCURRENCES_BY_VERSION,
13 | GET_SBOMS,
14 | GET_SLSAS,
15 | GET_VULNS,
16 | } from "./knownQueries";
17 | import { useVulnResults } from "@/store/vulnResultsContext";
18 | import VulnResults from "./vulnResults";
19 | import { CertifyVuln } from "@/gql/__generated__/graphql";
20 |
21 | const KnownInfo = () => {
22 | const router = useRouter();
23 | const { vulnResults, setVulnResults } = useVulnResults();
24 |
25 | const { pkgID, packageName, pkgVersion } = usePackageData();
26 |
27 | const [sboms, setSboms] = useState([]);
28 | const [vulns, setVulns] = useState([]);
29 | const [slsas, setSLSAs] = useState([]);
30 | const [occurrences, setOccurrences] = useState([]);
31 | const [toggleSLSA, setToggleSLSA] = useState([]);
32 |
33 | const [searchTerm, setSearchTerm] = useState("");
34 |
35 | const [handleSbomClicked, setHandleSbomClicked] = useState(false);
36 | const [handleVulnClicked, setHandleVulnClicked] = useState(false);
37 | const [slsaIndex, setSLSAIndex] = useState(null);
38 | const [handleOccurrencesClicked, setHandleOccurrencesClicked] =
39 | useState(false);
40 |
41 | // VULN
42 | const {
43 | loading: vulnsLoading,
44 | error: vulnsError,
45 | refetch: vulnsRefetch,
46 | } = useQuery(GET_VULNS, {
47 | variables: { pkgVersion },
48 |
49 | skip: true,
50 | });
51 |
52 | const fetchVulns = async () => {
53 | let pathWithIDs = "";
54 | resetState();
55 | setHandleVulnClicked(true);
56 | if (pkgVersion !== "") {
57 | const { data } = await vulnsRefetch({ pkgVersion });
58 | if (
59 | data?.CertifyVuln &&
60 | Array.isArray(data.CertifyVuln) &&
61 | data.CertifyVuln.length > 0 &&
62 | data.CertifyVuln.some(
63 | (vuln: CertifyVuln) => vuln.vulnerability.type !== "novuln"
64 | )
65 | ) {
66 | setVulns(data.CertifyVuln);
67 | for (let vuln of data.CertifyVuln) {
68 | pathWithIDs += `${vuln.id},`;
69 | }
70 | router.push(`/?path=${pathWithIDs?.slice(0, pathWithIDs.length - 1)}`);
71 | } else {
72 | console.error("Unexpected data structure:", data);
73 | setVulns([]);
74 | }
75 | }
76 | };
77 |
78 | // do not capture "novuln" vals
79 | const filteredVulns = vulns.filter(
80 | (vuln) => vuln?.vulnerability?.type !== "novuln"
81 | );
82 |
83 | // SBOMs
84 | const {
85 | loading: sbomLoading,
86 | error: sbomError,
87 | refetch: sbomRefetch,
88 | } = useQuery(GET_SBOMS, {
89 | variables: { name: packageName, pkgID },
90 |
91 | skip: true,
92 | });
93 |
94 | const fetchSBOMs = async () => {
95 | resetState();
96 | setHandleSbomClicked(true);
97 | if (packageName) {
98 | const { data } = await sbomRefetch({ name: packageName, pkgID });
99 | setSboms(data?.HasSBOM || []);
100 | const sbomResultID = data?.HasSBOM[0]?.id;
101 |
102 | if (sbomResultID) {
103 | router.push(`/?path=${sbomResultID}`);
104 | }
105 | } else {
106 | return;
107 | }
108 | };
109 |
110 | // OCCURENCES AND SLSA
111 | const { refetch: occurrenceRefetch } = useQuery(GET_OCCURRENCES_BY_VERSION, {
112 | skip: true,
113 | });
114 |
115 | const {
116 | loading: slsaLoading,
117 | error: slsaError,
118 | refetch: slsaRefetch,
119 | } = useQuery(GET_SLSAS, {
120 | skip: true,
121 | });
122 |
123 | const fetchOccurrences = async () => {
124 | resetState();
125 | setHandleOccurrencesClicked(true);
126 | const { data } = await occurrenceRefetch({ pkgName: packageName });
127 |
128 | if (data?.IsOccurrence) {
129 | setOccurrences(data.IsOccurrence);
130 | }
131 | };
132 |
133 | const fetchSLSAForArtifact = async (
134 | algorithm: string,
135 | digest: string,
136 | index: number
137 | ) => {
138 | const { data } = await slsaRefetch({ algorithm, digest });
139 | const newSLSA = data?.HasSLSA || [];
140 | setSLSAs((prevSLSAs) => {
141 | const updatedSLSAs = { ...prevSLSAs };
142 | updatedSLSAs[index] = newSLSA;
143 | return updatedSLSAs;
144 | });
145 | setSLSAIndex(index);
146 | router.push(`/?path=${newSLSA[0].id},${occurrences[index]?.id}`);
147 | };
148 |
149 | const handleToggleSLSA = (index) => {
150 | setToggleSLSA((prevToggle) => ({
151 | ...prevToggle,
152 | [index]: !prevToggle[index],
153 | }));
154 | };
155 |
156 | const handleGetSLSA = async (
157 | algorithm: string,
158 | digest: string,
159 | index: number
160 | ) => {
161 | if (toggleSLSA[index]) {
162 | handleToggleSLSA(index);
163 | } else {
164 | await fetchSLSAForArtifact(algorithm, digest, index);
165 | handleToggleSLSA(index);
166 | }
167 | };
168 |
169 | // reset state
170 | function resetState() {
171 | setSboms([]);
172 | setVulns([]);
173 | setSLSAs([]);
174 | setToggleSLSA([]);
175 | setVulnResults([]);
176 | setHandleSbomClicked(false);
177 | setHandleVulnClicked(false);
178 | setHandleOccurrencesClicked(false);
179 | }
180 |
181 | // For VULN loading and error
182 | const VULNLoadingElement = vulnsLoading ? (
183 | Loading vulnerabilities...
184 | ) : null;
185 | const VULNErrorElement = vulnsError ? (
186 | Error: {vulnsError.message}
187 | ) : null;
188 |
189 | // For SBOMs loading and error
190 | const SBOMLoadingElement = sbomLoading ? Loading SBOMs...
: null;
191 | const SBOMErrorElement = sbomError ? (
192 | Error: {sbomError.message}
193 | ) : null;
194 |
195 | // For SLSA loading and error
196 | const SLSALoadingElement = slsaLoading ? Loading SLSAs...
: null;
197 | const SLSAErrorElement = slsaError ? (
198 | Error: {slsaError.message}
199 | ) : null;
200 |
201 | // trigger for resetting
202 | // useEffect(() => {
203 | // resetState();
204 | // }, [pkgID, packageName, pkgVersion]);
205 |
206 | return (
207 |
208 |
209 | {VULNLoadingElement}
210 | {VULNErrorElement}
211 | {SBOMLoadingElement}
212 | {SBOMErrorElement}
213 | {SLSALoadingElement}
214 | {SLSAErrorElement}
215 |
229 |
240 |
251 |
264 |
265 |
266 | {handleSbomClicked &&
267 | (sboms.length === 0 ? (
268 |
Didn't find SBOMs
269 | ) : (
270 |
271 | {sboms.map((sbom) => (
272 | -
273 | {sbom.__typename === "HasSBOM" && (
274 |
275 |
276 | Package {sbom.subject?.namespaces[0]?.names[0]?.name}{" "}
277 | has an SBOM located in this location:{" "}
278 |
279 |
{sbom.downloadLocation}
280 |
281 | )}
282 |
283 | ))}
284 |
285 | ))}
286 |
287 |
288 |
289 | {handleVulnClicked &&
290 | (filteredVulns.length === 0 ? (
291 |
Didn't find vulns
292 | ) : (
293 |
294 | {filteredVulns.map((vuln, index) => (
295 | -
296 |
297 |
298 | Name:{" "}
299 | {vuln?.package?.namespaces?.[0]?.names?.[0]?.name ||
300 | "N/A"}
301 |
302 |
303 | Version:{" "}
304 | {vuln?.package?.namespaces?.[0]?.names?.[0]?.versions?.[0]
305 | ?.version || "N/A"}
306 |
307 |
Type: {vuln?.vulnerability?.type || "N/A"}
308 |
309 | Vulnerability ID:{" "}
310 | {vuln?.vulnerability?.vulnerabilityIDs?.[0]
311 | ?.vulnerabilityID || "N/A"}
312 |
313 |
Last scanned: {vuln?.metadata?.timeScanned || "N/A"}
314 |
315 |
316 |
317 | ))}
318 |
319 | ))}
320 |
321 |
322 | {handleOccurrencesClicked &&
323 | (occurrences.length === 0 ? (
324 |
No SLSA attestations found
325 | ) : (
326 |
327 |
Search by artifact
328 |
setSearchTerm(e.target.value)}
334 | />
335 |
336 |
417 |
418 | ))}
419 |
420 |
421 | {vulnResults &&
}
422 |
423 | );
424 | };
425 |
426 | export default KnownInfo;
427 |
--------------------------------------------------------------------------------
/utils/ggraph.tsx:
--------------------------------------------------------------------------------
1 | import { NodeFragment } from "@/gql/types/nodeFragment";
2 | import {
3 | IsDependency,
4 | Source,
5 | Package,
6 | PackageNamespace,
7 | PackageName,
8 | PackageVersion,
9 | PkgEqual,
10 | CertifyGood,
11 | Node as gqlNode,
12 | SourceNamespace,
13 | SourceName,
14 | Artifact,
15 | IsOccurrence,
16 | Builder,
17 | CertifyVexStatement,
18 | HashEqual,
19 | CertifyBad,
20 | CertifyScorecard,
21 | CertifyVuln,
22 | HasSourceAt,
23 | HasSbom,
24 | HasSlsa,
25 | Vulnerability,
26 | VulnerabilityMetadata,
27 | VulnEqual,
28 | License,
29 | CertifyLegal,
30 | } from "@/gql/__generated__/graphql";
31 |
32 | export type Node = {
33 | data: {
34 | id: string;
35 | label: string;
36 | type: string;
37 | expanded?: string;
38 | data?: Object;
39 | };
40 | };
41 |
42 | export type Edge = {
43 | data: {
44 | source: string;
45 | target: string;
46 | label: string;
47 | id?: string;
48 | };
49 | };
50 |
51 | export type GuacGraphData = {
52 | nodes: Node[];
53 | edges: Edge[];
54 | };
55 |
56 | export function ParseNode(
57 | n: gqlNode | NodeFragment
58 | ): GuacGraphData | undefined {
59 | let gd: GuacGraphData;
60 | let target: Node | undefined;
61 |
62 | const typeName = n.__typename;
63 | switch (typeName) {
64 | // SW trees
65 | case "Package":
66 | [gd, target] = parsePackage(n);
67 | break;
68 | case "Source":
69 | [gd, target] = parseSource(n);
70 | break;
71 | case "Artifact":
72 | [gd, target] = parseArtifact(n);
73 | break;
74 | case "Builder":
75 | [gd, target] = parseBuilder(n);
76 | break;
77 |
78 | // Evidence trees
79 | case "IsDependency":
80 | [gd, target] = parseIsDependency(n as IsDependency);
81 | break;
82 | case "PkgEqual":
83 | [gd, target] = parsePkgEqual(n as PkgEqual);
84 | break;
85 | case "IsOccurrence":
86 | [gd, target] = parseIsOccurrence(n as IsOccurrence);
87 | break;
88 | case "CertifyVEXStatement":
89 | [gd, target] = parseCertifyVexStatement(n as CertifyVexStatement);
90 | break;
91 | case "HashEqual":
92 | [gd, target] = parseHashEqual(n as HashEqual);
93 | break;
94 | case "CertifyBad":
95 | [gd, target] = parseCertifyBad(n as CertifyBad);
96 | break;
97 | case "CertifyGood":
98 | [gd, target] = parseCertifyGood(n as CertifyGood);
99 | break;
100 | case "CertifyLegal":
101 | [gd, target] = parseCertifyLegal(n as CertifyLegal);
102 | break;
103 | case "License":
104 | [gd, target] = parseLicense(n as License);
105 | break;
106 | case "VulnEqual":
107 | [gd, target] = parseVulnEqual(n as VulnEqual);
108 | break;
109 | case "CertifyScorecard":
110 | [gd, target] = parseCertifyScorecard(n as CertifyScorecard);
111 | break;
112 | case "CertifyVuln":
113 | [gd, target] = parseCertifyVuln(n as CertifyVuln);
114 | break;
115 | case "HasSourceAt":
116 | [gd, target] = parseHasSourceAt(n as HasSourceAt);
117 | break;
118 | case "HasSBOM":
119 | [gd, target] = parseHasSbom(n as HasSbom);
120 | break;
121 | case "HasSLSA":
122 | [gd, target] = parseHasSlsa(n as HasSlsa);
123 | break;
124 | case "Vulnerability": // Add this case
125 | [gd, target] = parseVulnerability(n); // You'll need to implement this function
126 | break;
127 |
128 | default:
129 | // not handled
130 | console.log("unhandled node type:", typeName);
131 | return undefined;
132 | }
133 |
134 | gd.edges.forEach((e) => {
135 | e.data.id = e.data.source + "->" + e.data.target;
136 | });
137 |
138 | return gd;
139 | }
140 |
141 | // parse* returns a set of GraphData that consists of the nodes and edges to create a subgraph
142 | // it also returns the node which is the main node to link to of the subgraph
143 | export function parsePackage(n: Package): [GuacGraphData, Node | undefined] {
144 | let nodes: Node[] = [];
145 | let edges: Edge[] = [];
146 | // for each check if its the leaf, and if its the leaf that's where the edge goes
147 | let target: Node | undefined = undefined;
148 |
149 | const typ: Package = n;
150 | nodes = [
151 | ...nodes,
152 | { data: { id: typ.id, label: typ.type, type: "PackageType" } },
153 | ];
154 | if (typ.namespaces.length == 0) {
155 | target = nodes.at(-1);
156 | }
157 |
158 | typ.namespaces.forEach((ns: PackageNamespace) => {
159 | nodes = [
160 | ...nodes,
161 | { data: { id: ns.id, label: ns.namespace, type: "PackageNamespace" } },
162 | ];
163 |
164 | const edgesAfter = [
165 | ...edges,
166 | { data: { source: typ.id, target: ns.id, label: "pkgNs" } },
167 | ];
168 |
169 | edges = edgesAfter;
170 | if (ns.names.length == 0) {
171 | target = nodes.at(-1);
172 | }
173 |
174 | ns.names.forEach((name: PackageName) => {
175 | nodes = [
176 | ...nodes,
177 | {
178 | data: { id: name.id, label: `pkg:${name.name}`, type: "PackageName" },
179 | },
180 | ];
181 |
182 | edges = [
183 | ...edges,
184 | { data: { source: ns.id, target: name.id, label: "pkgName" } },
185 | ];
186 | if (name.versions.length == 0) {
187 | target = nodes.at(-1);
188 | }
189 | if (name.versions.length !== 0) {
190 | name.versions.forEach((version: PackageVersion) => {
191 | nodes = [
192 | ...nodes,
193 | {
194 | data: {
195 | ...version,
196 | id: version.id,
197 | label: version.version,
198 | type: "PackageVersion",
199 | },
200 | },
201 | ];
202 | edges = [
203 | ...edges,
204 | {
205 | data: {
206 | source: name.id,
207 | target: version.id,
208 | label: "pkgVersion",
209 | },
210 | },
211 | ];
212 | target = nodes.at(-1);
213 | });
214 | }
215 | });
216 | });
217 | return [{ nodes: nodes, edges: edges }, target];
218 | }
219 |
220 | export function parseSource(n: Source): [GuacGraphData, Node | undefined] {
221 | let nodes: Node[] = [];
222 | let edges: Edge[] = [];
223 | // for each check if its the leaf, and if its the leaf that's where the edge goes
224 | let target: Node | undefined = undefined;
225 |
226 | const typ: Source = n;
227 | nodes = [
228 | ...nodes,
229 | { data: { id: typ.id, label: typ.type, type: "SourceType" } },
230 | ];
231 | if (typ.namespaces.length == 0) {
232 | target = nodes.at(-1);
233 | }
234 |
235 | typ.namespaces.forEach((ns: SourceNamespace) => {
236 | nodes = [
237 | ...nodes,
238 | { data: { id: ns.id, label: ns.namespace, type: "SourceNamespace" } },
239 | ];
240 | edges = [
241 | ...edges,
242 | { data: { source: typ.id, target: ns.id, label: "srcNs" } },
243 | ];
244 | if (ns.names.length == 0) {
245 | target = nodes.at(-1);
246 | }
247 |
248 | ns.names.forEach((name: SourceName) => {
249 | nodes = [
250 | ...nodes,
251 | {
252 | data: { ...name, id: name.id, label: name.name, type: "SourceName" },
253 | },
254 | ];
255 | edges = [
256 | ...edges,
257 | { data: { source: ns.id, target: name.id, label: "srcName" } },
258 | ];
259 | target = nodes.at(-1);
260 | });
261 | });
262 |
263 | return [{ nodes: nodes, edges: edges }, target];
264 | }
265 |
266 | export function parseArtifact(n: Artifact): [GuacGraphData, Node | undefined] {
267 | let nodes: Node[] = [];
268 | let target: Node | undefined = undefined;
269 |
270 | nodes = [
271 | ...nodes,
272 | {
273 | data: {
274 | ...n,
275 | id: n.id,
276 | label: n.algorithm + ":" + n.digest,
277 | type: "Artifact",
278 | },
279 | },
280 | ];
281 | target = nodes.at(-1);
282 |
283 | return [{ nodes: nodes, edges: [] }, target];
284 | }
285 |
286 | export function parseBuilder(n: Builder): [GuacGraphData, Node | undefined] {
287 | let nodes: Node[] = [];
288 | let target: Node | undefined = undefined;
289 |
290 | nodes = [
291 | ...nodes,
292 | { data: { ...n, id: n.id, label: n.uri, type: "Builder" } },
293 | ];
294 | target = nodes.at(-1);
295 |
296 | return [{ nodes: nodes, edges: [] }, target];
297 | }
298 |
299 | export function parseVulnerability(
300 | n: Vulnerability
301 | ): [GuacGraphData, Node | undefined] {
302 | let nodes: Node[] = [];
303 | let edges: Edge[] = [];
304 | let target: Node | undefined = undefined;
305 |
306 | if (n.type !== "novuln") {
307 | nodes = [
308 | ...nodes,
309 | { data: { id: n.id, label: n.type, type: "Vulnerability" } },
310 | ];
311 |
312 | n.vulnerabilityIDs.forEach((vulnID) => {
313 | nodes = [
314 | ...nodes,
315 | {
316 | data: {
317 | id: vulnID.id,
318 | label: vulnID.vulnerabilityID,
319 | type: "VulnerabilityID",
320 | },
321 | },
322 | ];
323 | edges = [
324 | ...edges,
325 | { data: { source: n.id, target: vulnID.id, label: "has_vuln_id" } },
326 | ];
327 | });
328 |
329 | target = nodes.at(-1);
330 | }
331 |
332 | return [{ nodes: nodes, edges: edges }, target];
333 | }
334 |
335 | export function parseCertifyVuln(
336 | n: CertifyVuln
337 | ): [GuacGraphData, Node | undefined] {
338 | let nodes: Node[] = [];
339 | let edges: Edge[] = [];
340 | let target: Node | undefined = undefined;
341 |
342 | // if there's a valid vulnID and vulnerability type is not "NoVuln", i.e., there's no vulnerabilites to match nodes with, don't show CertifyVuln node
343 | if (
344 | n.vulnerability.vulnerabilityIDs[0].vulnerabilityID &&
345 | n.vulnerability.type !== "NoVuln"
346 | ) {
347 | nodes.push({
348 | data: {
349 | ...n,
350 | id: n.id,
351 | label: "CertifyVuln",
352 | type: "CertifyVuln",
353 | expanded: "true",
354 | },
355 | });
356 |
357 | n.vulnerability.vulnerabilityIDs.forEach((vulnID) => {
358 | nodes.push({
359 | data: {
360 | id: vulnID.id,
361 | label: vulnID.vulnerabilityID,
362 | type: "VulnerabilityID",
363 | },
364 | });
365 | edges.push({
366 | data: { source: n.id, target: vulnID.id, label: "has_vuln_id" },
367 | });
368 | });
369 |
370 | target = nodes.at(-1);
371 |
372 | let [gd, t] = parsePackage(n.package);
373 | nodes = [...nodes, ...gd.nodes];
374 | edges = [...edges, ...gd.edges];
375 |
376 | if (t != undefined) {
377 | edges.push({
378 | data: { source: n.id, target: t.data.id, label: "subject" },
379 | });
380 | }
381 |
382 | if (n.vulnerability.type === "Vulnerability") {
383 | [gd, t] = parseVulnerability(n.vulnerability);
384 | nodes = [...nodes, ...gd.nodes];
385 | edges = [...edges, ...gd.edges];
386 | if (t != undefined) {
387 | edges.push({
388 | data: { source: n.id, target: t.data.id, label: "vulnerability" },
389 | });
390 | }
391 | }
392 | }
393 |
394 | return [{ nodes: nodes, edges: edges }, target];
395 | }
396 |
397 | export function parseCertifyVexStatement(
398 | n: CertifyVexStatement
399 | ): [GuacGraphData, Node | undefined] {
400 | let nodes: Node[] = [];
401 | let edges: Edge[] = [];
402 | let target: Node | undefined = undefined;
403 |
404 | nodes = [
405 | ...nodes,
406 | {
407 | data: {
408 | ...n,
409 | id: n.id,
410 | label: "CertifyVexStatement",
411 | type: "CertifyVexStatement",
412 | expanded: "true",
413 | },
414 | },
415 | ];
416 | target = nodes.at(-1);
417 |
418 | let gd: GuacGraphData;
419 | let t: Node | undefined;
420 |
421 | if (n.subject.__typename == "Artifact") {
422 | [gd, t] = parseArtifact(n.subject);
423 | nodes = [...nodes, ...gd.nodes];
424 | edges = [...edges, ...gd.edges];
425 |
426 | if (t != undefined) {
427 | edges = [
428 | ...edges,
429 | { data: { source: n.id, target: t.data.id, label: "subject" } },
430 | ];
431 | }
432 | } else if (n.subject.__typename == "Package") {
433 | [gd, t] = parsePackage(n.subject);
434 | nodes = [...nodes, ...gd.nodes];
435 | edges = [...edges, ...gd.edges];
436 |
437 | if (t != undefined) {
438 | edges = [
439 | ...edges,
440 | { data: { source: n.id, target: t.data.id, label: "subject" } },
441 | ];
442 | }
443 | }
444 |
445 | return [{ nodes: nodes, edges: edges }, target];
446 | }
447 |
448 | export function parseHashEqual(
449 | n: HashEqual
450 | ): [GuacGraphData, Node | undefined] {
451 | let nodes: Node[] = [];
452 | let edges: Edge[] = [];
453 | // for each check if its the leaf, and if its the leaf that's where the edge goes
454 | let target: Node | undefined = undefined;
455 |
456 | nodes = [
457 | ...nodes,
458 | {
459 | data: {
460 | ...n,
461 | id: n.id,
462 | label: "HashEqual",
463 | type: "HashEqual",
464 | expanded: "true",
465 | },
466 | },
467 | ];
468 | target = nodes.at(-1);
469 |
470 | n.artifacts.forEach((m) => {
471 | let [gd, t] = parseArtifact(m);
472 | nodes = [...nodes, ...gd.nodes];
473 | edges = [...edges, ...gd.edges];
474 |
475 | if (t != undefined) {
476 | edges = [
477 | ...edges,
478 | { data: { source: n.id, target: t.data.id, label: "artEqual" } },
479 | ];
480 | }
481 | });
482 |
483 | return [{ nodes: nodes, edges: edges }, target];
484 | }
485 |
486 | export function parseCertifyBad(
487 | n: CertifyBad
488 | ): [GuacGraphData, Node | undefined] {
489 | let nodes: Node[] = [];
490 | let edges: Edge[] = [];
491 | let target: Node | undefined = undefined;
492 |
493 | nodes = [
494 | ...nodes,
495 | {
496 | data: {
497 | ...n,
498 | id: n.id,
499 | label: "CertifyBad",
500 | type: "CertifyBad",
501 | expanded: "true",
502 | },
503 | },
504 | ];
505 | target = nodes.at(-1);
506 |
507 | let gd: GuacGraphData;
508 | let t: Node | undefined;
509 |
510 | if (n.subject.__typename == "Artifact") {
511 | [gd, t] = parseArtifact(n.subject);
512 | nodes = [...nodes, ...gd.nodes];
513 | edges = [...edges, ...gd.edges];
514 | if (t != undefined) {
515 | edges = [
516 | ...edges,
517 | { data: { source: n.id, target: t.data.id, label: "is_bad" } },
518 | ];
519 | }
520 | } else if (n.subject.__typename == "Source") {
521 | [gd, t] = parseSource(n.subject);
522 | nodes = [...nodes, ...gd.nodes];
523 | edges = [...edges, ...gd.edges];
524 | if (t != undefined) {
525 | edges = [
526 | ...edges,
527 | { data: { source: n.id, target: t.data.id, label: "is_bad" } },
528 | ];
529 | }
530 | } else if (n.subject.__typename == "Package") {
531 | [gd, t] = parsePackage(n.subject);
532 | nodes = [...nodes, ...gd.nodes];
533 | edges = [...edges, ...gd.edges];
534 | if (t != undefined) {
535 | edges = [
536 | ...edges,
537 | { data: { source: n.id, target: t.data.id, label: "is_bad" } },
538 | ];
539 | }
540 | }
541 |
542 | return [{ nodes: nodes, edges: edges }, target];
543 | }
544 |
545 | export function parseCertifyGood(
546 | n: CertifyGood
547 | ): [GuacGraphData, Node | undefined] {
548 | let nodes: Node[] = [];
549 | let edges: Edge[] = [];
550 | let target: Node | undefined = undefined;
551 |
552 | nodes = [
553 | ...nodes,
554 | {
555 | data: {
556 | ...n,
557 | id: n.id,
558 | label: "CertifyGood",
559 | type: "CertifyGood",
560 | expanded: "true",
561 | },
562 | },
563 | ];
564 | target = nodes.at(-1);
565 |
566 | let gd: GuacGraphData;
567 | let t: Node | undefined;
568 |
569 | if (n.subject.__typename == "Artifact") {
570 | [gd, t] = parseArtifact(n.subject);
571 | nodes = [...nodes, ...gd.nodes];
572 | edges = [...edges, ...gd.edges];
573 | if (t != undefined) {
574 | edges = [
575 | ...edges,
576 | { data: { source: n.id, target: t.data.id, label: "is_good" } },
577 | ];
578 | }
579 | } else if (n.subject.__typename == "Source") {
580 | [gd, t] = parseSource(n.subject);
581 | nodes = [...nodes, ...gd.nodes];
582 | edges = [...edges, ...gd.edges];
583 | if (t != undefined) {
584 | edges = [
585 | ...edges,
586 | { data: { source: n.id, target: t.data.id, label: "is_good" } },
587 | ];
588 | }
589 | } else if (n.subject.__typename == "Package") {
590 | [gd, t] = parsePackage(n.subject);
591 | nodes = [...nodes, ...gd.nodes];
592 | edges = [...edges, ...gd.edges];
593 | if (t != undefined) {
594 | edges = [
595 | ...edges,
596 | { data: { source: n.id, target: t.data.id, label: "is_good" } },
597 | ];
598 | }
599 | }
600 |
601 | return [{ nodes: nodes, edges: edges }, target];
602 | }
603 |
604 | export function parseCertifyScorecard(
605 | n: CertifyScorecard
606 | ): [GuacGraphData, Node | undefined] {
607 | let nodes: Node[] = [];
608 | let edges: Edge[] = [];
609 | let target: Node | undefined = undefined;
610 |
611 | nodes = [
612 | ...nodes,
613 | {
614 | data: {
615 | ...n,
616 | id: n.id,
617 | label: "CertifyScorecard",
618 | type: "CertifyScorecard",
619 | expanded: "true",
620 | },
621 | },
622 | ];
623 | target = nodes.at(-1);
624 |
625 | let [gd, t] = parseSource(n.source);
626 | nodes = [...nodes, ...gd.nodes];
627 | edges = [...edges, ...gd.edges];
628 | if (t != undefined) {
629 | edges = [
630 | ...edges,
631 | { data: { source: n.id, target: t.data.id, label: "has_scorecard" } },
632 | ];
633 | }
634 |
635 | return [{ nodes: nodes, edges: edges }, target];
636 | }
637 |
638 | export function parseHasSourceAt(
639 | n: HasSourceAt
640 | ): [GuacGraphData, Node | undefined] {
641 | let nodes: Node[] = [];
642 | let edges: Edge[] = [];
643 | let target: Node | undefined = undefined;
644 |
645 | nodes = [
646 | ...nodes,
647 | {
648 | data: {
649 | ...n,
650 | id: n.id,
651 | label: "HasSourceAt",
652 | type: "HasSourceAt",
653 | expanded: "true",
654 | },
655 | },
656 | ];
657 | target = nodes.at(-1);
658 |
659 | let [gd, t] = parsePackage(n.package);
660 | nodes = [...nodes, ...gd.nodes];
661 | edges = [...edges, ...gd.edges];
662 | if (t != undefined) {
663 | edges = [
664 | ...edges,
665 | { data: { source: t.data.id, target: n.id, label: "subject" } },
666 | ];
667 | }
668 |
669 | [gd, t] = parseSource(n.source);
670 | nodes = [...nodes, ...gd.nodes];
671 | edges = [...edges, ...gd.edges];
672 | if (t != undefined) {
673 | edges = [
674 | ...edges,
675 | { data: { source: n.id, target: t.data.id, label: "has_source" } },
676 | ];
677 | }
678 |
679 | return [{ nodes: nodes, edges: edges }, target];
680 | }
681 |
682 | export function parseHasSbom(n: HasSbom): [GuacGraphData, Node | undefined] {
683 | let nodes: Node[] = [];
684 | let edges: Edge[] = [];
685 | let target: Node | undefined = undefined;
686 |
687 | nodes = [
688 | ...nodes,
689 | {
690 | data: {
691 | ...n,
692 | id: n.id,
693 | label: "HasSbom",
694 | type: "HasSbom",
695 | expanded: "true",
696 | },
697 | },
698 | ];
699 | target = nodes.at(-1);
700 |
701 | let gd: GuacGraphData;
702 | let t: Node | undefined;
703 |
704 | if (n.subject.__typename == "Source") {
705 | [gd, t] = parseSource(n.subject);
706 | nodes = [...nodes, ...gd.nodes];
707 | edges = [...edges, ...gd.edges];
708 | if (t != undefined) {
709 | edges = [
710 | ...edges,
711 | {
712 | data: { source: n.id, target: t.data.id, label: "vulnerability" },
713 | },
714 | ];
715 | }
716 | } else if (n.subject.__typename == "Package") {
717 | [gd, t] = parsePackage(n.subject);
718 | nodes = [...nodes, ...gd.nodes];
719 | edges = [...edges, ...gd.edges];
720 | if (t != undefined) {
721 | edges = [
722 | ...edges,
723 | {
724 | data: { source: n.id, target: t.data.id, label: "vulnerability" },
725 | },
726 | ];
727 | }
728 | }
729 |
730 | return [{ nodes: nodes, edges: edges }, target];
731 | }
732 |
733 | export function parseHasSlsa(n: HasSlsa): [GuacGraphData, Node | undefined] {
734 | let nodes: Node[] = [];
735 | let edges: Edge[] = [];
736 | let target: Node | undefined = undefined;
737 |
738 | nodes = [
739 | ...nodes,
740 | {
741 | data: {
742 | ...n,
743 | id: n.id,
744 | label: "HasSlsa",
745 | type: "HasSlsa",
746 | expanded: "true",
747 | },
748 | },
749 | ];
750 | target = nodes.at(-1);
751 |
752 | let gd: GuacGraphData;
753 | let t: Node | undefined;
754 |
755 | [gd, t] = parseArtifact(n.subject);
756 | nodes = [...nodes, ...gd.nodes];
757 | edges = [...edges, ...gd.edges];
758 | if (t != undefined) {
759 | edges = [
760 | ...edges,
761 | { data: { source: n.id, target: t.data.id, label: "subject" } },
762 | ];
763 | }
764 |
765 | // TODO This should not be the case, change graphql and change this
766 | if (!n.slsa) {
767 | return [gd, t];
768 | }
769 |
770 | [gd, t] = parseBuilder(n.slsa.builtBy);
771 | nodes = [...nodes, ...gd.nodes];
772 | edges = [...edges, ...gd.edges];
773 | if (t != undefined) {
774 | edges = [
775 | ...edges,
776 | { data: { source: n.id, target: t.data.id, label: "built_by" } },
777 | ];
778 | }
779 |
780 | n.slsa.builtFrom.forEach((m) => {
781 | [gd, t] = parseArtifact(m);
782 | nodes = [...nodes, ...gd.nodes];
783 | edges = [...edges, ...gd.edges];
784 | if (t != undefined) {
785 | edges = [
786 | ...edges,
787 | { data: { source: n.id, target: t.data.id, label: "build_from" } },
788 | ];
789 | }
790 | });
791 |
792 | return [{ nodes: nodes, edges: edges }, target];
793 | }
794 |
795 | // parse* returns a set of GraphData that consists of the nodes and edges to create a subgraph
796 | // it also returns the node which is the main node to link to of the subgraph
797 | export function parseIsDependency(
798 | n: IsDependency
799 | ): [GuacGraphData, Node | undefined] {
800 | let nodes: Node[] = [];
801 | let edges: Edge[] = [];
802 | // for each check if its the leaf, and if its the leaf that's where the edge goes
803 | let target: Node | undefined = undefined;
804 |
805 | nodes = [
806 | ...nodes,
807 | {
808 | data: {
809 | ...n,
810 | id: n.id,
811 | label: "depends on",
812 | type: "IsDependency",
813 | expanded: "true",
814 | },
815 | },
816 | ];
817 | target = nodes.at(-1);
818 |
819 | let [gd, t] = parsePackage(n.package);
820 | nodes = [...nodes, ...gd.nodes];
821 | edges = [...edges, ...gd.edges];
822 |
823 | if (t != undefined) {
824 | edges = [
825 | ...edges,
826 | {
827 | data: {
828 | source: t.data.id,
829 | target: n.id,
830 | label: "IsDependency_subject",
831 | },
832 | },
833 | ];
834 | }
835 |
836 | [gd, t] = parsePackage(n.dependencyPackage);
837 | nodes = [...nodes, ...gd.nodes];
838 | edges = [...edges, ...gd.edges];
839 |
840 | if (t != undefined) {
841 | edges = [
842 | ...edges,
843 | { data: { source: n.id, target: t.data.id, label: "depends_on" } },
844 | ];
845 | }
846 |
847 | return [{ nodes: nodes, edges: edges }, target];
848 | }
849 |
850 | export function parsePkgEqual(n: PkgEqual): [GuacGraphData, Node | undefined] {
851 | let nodes: Node[] = [];
852 | let edges: Edge[] = [];
853 | // for each check if its the leaf, and if its the leaf that's where the edge goes
854 | let target: Node | undefined = undefined;
855 |
856 | nodes = [
857 | ...nodes,
858 | {
859 | data: {
860 | ...n,
861 | id: n.id,
862 | label: "PkgEqual",
863 | type: "PkgEqual",
864 | expanded: "true",
865 | },
866 | },
867 | ];
868 | target = nodes.at(-1);
869 |
870 | n.packages.forEach((m) => {
871 | let [gd, t] = parsePackage(m);
872 | nodes = [...nodes, ...gd.nodes];
873 | edges = [...edges, ...gd.edges];
874 |
875 | if (t != undefined) {
876 | edges = [
877 | ...edges,
878 | { data: { source: n.id, target: t.data.id, label: "pkgEqual" } },
879 | ];
880 | }
881 | });
882 |
883 | return [{ nodes: nodes, edges: edges }, target];
884 | }
885 |
886 | export function parseIsOccurrence(
887 | n: IsOccurrence
888 | ): [GuacGraphData, Node | undefined] {
889 | let nodes: Node[] = [];
890 | let edges: Edge[] = [];
891 | // for each check if its the leaf, and if its the leaf that's where the edge goes
892 | let target: Node | undefined = undefined;
893 |
894 | nodes = [
895 | ...nodes,
896 | {
897 | data: {
898 | ...n,
899 | id: n.id,
900 | label: "Occur",
901 | type: "IsOccurrence",
902 | expanded: "true",
903 | },
904 | },
905 | ];
906 | target = nodes.at(-1);
907 |
908 | let gd: GuacGraphData;
909 | let t: Node | undefined;
910 | if (n.subject.__typename == "Package") {
911 | [gd, t] = parsePackage(n.subject);
912 | nodes = [...nodes, ...gd.nodes];
913 | edges = [...edges, ...gd.edges];
914 |
915 | if (t != undefined) {
916 | edges = [
917 | ...edges,
918 | { data: { source: n.id, target: t.data.id, label: "subject" } },
919 | ];
920 | }
921 | } else if (n.subject.__typename == "Source") {
922 | [gd, t] = parseSource(n.subject);
923 | nodes = [...nodes, ...gd.nodes];
924 | edges = [...edges, ...gd.edges];
925 |
926 | if (t != undefined) {
927 | edges = [
928 | ...edges,
929 | { data: { source: n.id, target: t.data.id, label: "subject" } },
930 | ];
931 | }
932 | }
933 |
934 | [gd, t] = parseArtifact(n.artifact);
935 | nodes = [...nodes, ...gd.nodes];
936 | edges = [...edges, ...gd.edges];
937 |
938 | if (t != undefined) {
939 | edges = [
940 | ...edges,
941 | { data: { source: n.id, target: t.data.id, label: "is_occurrence" } },
942 | ];
943 | }
944 |
945 | return [{ nodes: nodes, edges: edges }, target];
946 | }
947 |
948 | export function parseLicense(n: License): [GuacGraphData, Node | undefined] {
949 | let nodes: Node[] = [];
950 | let edges: Edge[] = [];
951 | let target: Node | undefined = undefined;
952 |
953 | nodes = [
954 | ...nodes,
955 | {
956 | data: {
957 | id: n.id,
958 | label: `License-${n.id}`,
959 | type: "License",
960 | },
961 | },
962 | ];
963 |
964 | target = nodes.at(-1);
965 | return [{ nodes: nodes, edges: edges }, target];
966 | }
967 |
968 | export function parseVulnEqual(
969 | n: VulnEqual
970 | ): [GuacGraphData, Node | undefined] {
971 | let nodes: Node[] = [];
972 | let edges: Edge[] = [];
973 | let target: Node | undefined = undefined;
974 |
975 | nodes = [
976 | ...nodes,
977 | {
978 | data: {
979 | ...n,
980 | id: n.id,
981 | label: "VulnEq",
982 | type: "VulnEqual",
983 | expanded: "true",
984 | },
985 | },
986 | ];
987 | target = nodes.at(-1);
988 |
989 | let gd: GuacGraphData;
990 | let t: Node | undefined;
991 |
992 | if (n.vulnerabilities) {
993 | n.vulnerabilities.forEach((vulnerability) => {
994 | [gd, t] = parseVulnerability(vulnerability);
995 | nodes = [...nodes, ...gd.nodes];
996 | edges = [...edges, ...gd.edges];
997 |
998 | if (t != undefined) {
999 | edges = [
1000 | ...edges,
1001 | { data: { source: n.id, target: t.data.id, label: "is_vuln_equal" } },
1002 | ];
1003 | }
1004 | });
1005 | }
1006 |
1007 | return [{ nodes: nodes, edges: edges }, target];
1008 | }
1009 |
1010 | export function parseVulnerabilityMetadata(
1011 | n: VulnerabilityMetadata
1012 | ): [GuacGraphData, Node | undefined] {
1013 | let nodes: Node[] = [];
1014 | let edges: Edge[] = [];
1015 | let target = undefined;
1016 |
1017 | nodes = [
1018 | ...nodes,
1019 | {
1020 | data: {
1021 | id: n.id,
1022 | label: n.origin,
1023 | type: "VulnerabilityMetadata",
1024 | },
1025 | },
1026 | ];
1027 |
1028 | target = nodes.at(-1);
1029 | return [{ nodes: nodes, edges: edges }, target];
1030 | }
1031 |
1032 | export function parseCertifyLegal(
1033 | n: CertifyLegal
1034 | ): [GuacGraphData, Node | undefined] {
1035 | let nodes: Node[] = [];
1036 | let edges: Edge[] = [];
1037 | let target = undefined;
1038 |
1039 | nodes = [
1040 | ...nodes,
1041 | {
1042 | data: {
1043 | id: n.id,
1044 | label: `CertifyLegal-${n.id} ${n.origin}`,
1045 | type: "CertifyLegal",
1046 | },
1047 | },
1048 | ];
1049 |
1050 | target = nodes.at(-1);
1051 | return [{ nodes: nodes, edges: edges }, target];
1052 | }
1053 |
--------------------------------------------------------------------------------