├── .env ├── .eslintrc.json ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── apollo └── client.ts ├── app ├── layout.tsx ├── page.tsx └── themeContext.tsx ├── codegen.ts ├── components ├── breadcrumb.tsx ├── graph │ ├── ForceGraph2D.tsx │ ├── ForceGraph2DWrapper.tsx │ ├── Graph.tsx │ └── types.ts ├── highlightToggles.tsx ├── known │ ├── knownInfo.tsx │ ├── knownQueries.ts │ └── vulnResults.tsx ├── layout │ ├── footer.tsx │ ├── header.tsx │ └── navlinks.tsx ├── navigationButton.tsx ├── nodeInfo │ └── nodeInfo.tsx ├── packages │ ├── certifyBadSelect.tsx │ ├── packageGenericSelector.tsx │ ├── packageNameSelect.tsx │ ├── packageNamespaceSelect.tsx │ ├── packageSelector.tsx │ ├── packageTypeSelect.tsx │ ├── packageVersionSelect.tsx │ └── toggleSwitch.tsx ├── queryvuln │ ├── certifyVulnQuery.ts │ └── queryVuln.tsx └── tooltip.tsx ├── gql ├── __generated__ │ ├── fragment-masking.ts │ ├── gql.ts │ ├── graphql.ts │ └── index.ts └── types │ └── nodeFragment.ts ├── hooks ├── useBreadcrumbNavigation.ts ├── useDimensions.ts ├── useGraphData.ts └── usePackageData.ts ├── main.d.ts ├── next.config.js ├── package.json ├── pages └── api │ └── version.ts ├── postcss.config.js ├── public ├── favicon.ico ├── github.png ├── images │ └── icons │ │ ├── close-button.svg │ │ ├── copy-clipboard.svg │ │ └── guac-logo.svg ├── next.svg ├── thirteen.svg └── vercel.svg ├── src └── config.js ├── store ├── packageDataContext.tsx └── vulnResultsContext.tsx ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── utils ├── ggraph.tsx └── graph_queries.ts └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![visualizer-animation-gif](https://github.com/guacsec/guac-visualizer/assets/68356865/06128619-8a69-4f52-9265-941c48b0be50) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } 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 | export default function Home() { 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 | 59 | 60 | 61 |
62 | {packageLoading ? ( 63 |
Loading package types...
64 | ) : packageError ? ( 65 |
Error loading package types!
66 | ) : ( 67 |
68 | 73 |
74 | 75 |
76 |
77 | )} 78 | 79 |
80 | {/* TODO: Fix highlighter, until then keep it commented */} 81 | {/*
82 |

83 | Tip: Use click 84 | and scroll to adjust graph.
85 | Right clicking a node displays more information. 86 |

87 |

Highlight Nodes

88 |
89 |
90 | 94 |
95 | 96 |
97 |
*/} 98 | 99 |
100 | 114 | {graphData.nodes.length !== 0 && 115 | graphData.links.length !== 0 && ( 116 | 125 | )} 126 | item.label)} 128 | handleNodeClick={handleBreadcrumbClick} 129 | currentIndex={currentIndex} 130 | /> 131 |
132 | 133 |
134 |
135 |
136 |
137 |
138 |
139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 |
  1. 36 | {index !== 0 && ( 37 | 38 | )} 39 | 49 |
  2. 50 | ); 51 | })} 52 |
53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 |
      337 | {occurrences 338 | .filter((occurrence) => { 339 | const hash = `${occurrence.artifact.algorithm}:${occurrence.artifact.digest}`; 340 | return hash.includes(searchTerm); 341 | }) 342 | .map((occurrence, index) => ( 343 |
      347 |

      348 | {occurrence.subject.namespaces[0].names[0].name} 349 |

      350 |

      351 | {occurrence.artifact.algorithm}: 352 | {occurrence.artifact.digest} 353 |

      354 | 366 | {toggleSLSA[index] && 367 | slsaIndex === index && 368 | slsas[index] && ( 369 |
      370 |

      371 | Build Type:{" "} 372 | {slsas[index][0]?.slsa?.buildType} 373 |

      374 |

      375 | Built By:{" "} 376 | {slsas[index][0]?.slsa?.builtBy.uri} 377 |

      378 |

      379 | 380 | Download location: 381 | {" "} 382 | {slsas[index][0]?.slsa?.origin} 383 |

      384 |

      385 | 386 | SLSA Version: 387 | {" "} 388 | {slsas[index][0]?.slsa?.slsaVersion} 389 |

      390 |
      391 |
        392 |

        393 | Predicate{" "} 394 |

        395 | {slsas[index][0]?.slsa?.slsaPredicate.map( 396 | (predicate, i) => ( 397 |
      • 401 | 402 | {predicate.key} : 403 | 404 | 405 | {predicate.value} 406 | 407 |
      • 408 | ) 409 | )} 410 |
      411 |
      412 |
      413 | )} 414 |
      415 | ))} 416 |
    417 |
    418 | ))} 419 |
    420 | 421 | {vulnResults && } 422 |
    423 | ); 424 | }; 425 | 426 | export default KnownInfo; 427 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 |
    18 |

    19 | We'd love to hear how we can make the visualizer more useful.{" "} 20 | 25 | Send feedback here! 26 | 27 |

    28 |
    29 |
    31 | Served by GUAC GraphQL {versionData.guacgql} 32 |
    33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /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 | GUAC Logo 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 | -------------------------------------------------------------------------------- /components/layout/navlinks.tsx: -------------------------------------------------------------------------------- 1 | export default function NavigationLinks() { 2 | return ( 3 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | ) => { 29 | onSelect(newValue.value); 30 | }} 31 | isDisabled={disabled} 32 | {...rest} 33 | /> 34 |
    35 | ); 36 | }; 37 | 38 | export default PackageGenericSelector; 39 | -------------------------------------------------------------------------------- /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/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/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 |
    56 |
    57 | 64 |
    65 |
    66 | 75 |
    76 |
    77 | 87 |
    88 |
    89 | 98 |
    99 |
    100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /gql/__generated__/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | // FIXME: Instead of disabling this, we should fix the problem. See 11 | // https://github.com/guacsec/guac-visualizer/issues/98 12 | experimental: { 13 | missingSuspenseWithCSRBailout: false, 14 | }, 15 | async rewrites() { 16 | return [ 17 | { 18 | source: config.GUACGQL_PROXY_PATH, 19 | destination: config.GUACGQL_SERVER_QUERY_URL.toString(), 20 | }, 21 | ]; 22 | }, 23 | }; 24 | 25 | module.exports = nextConfig; 26 | -------------------------------------------------------------------------------- /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 | "got": "^14.4.3", 36 | "graphql": "^16.8.1", 37 | "graphql-codegen-apollo-next-ssr": "^1.7.4", 38 | "jerrypick": "^1.1.1", 39 | "js-yaml": "^4.1.0", 40 | "lodash": "^4.17.21", 41 | "next": "14.2.26", 42 | "next-transpile-modules": "^10.0.0", 43 | "postcss": "^8.4.31", 44 | "react": "^18.2.0", 45 | "react-cytoscapejs": "^2.0.0", 46 | "react-dom": "^18.2.0", 47 | "react-force-graph-2d": "^1.23.15", 48 | "react-router-dom": "^6.11.0", 49 | "react-select": "^5.7.2", 50 | "react-toggle-button": "^2.2.0", 51 | "swr": "^2.1.0", 52 | "tailwindcss": "^3.3.1", 53 | "typescript": "^4.9.4", 54 | "uuid": "^9.0.0" 55 | }, 56 | "devDependencies": { 57 | "@types/got": "^9.6.12", 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/lodash": "^4.14.196", 66 | "@types/uuid": "^9.0.1", 67 | "cosmos-over-cytoscape": "^1.0.3", 68 | "react-multi-select-component": "^4.3.4", 69 | "styled-components": "^5.3.9" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guacsec/guac-visualizer/b95ccc5facd1d100e760d036f01d8bf805643fc1/public/favicon.ico -------------------------------------------------------------------------------- /public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guacsec/guac-visualizer/b95ccc5facd1d100e760d036f01d8bf805643fc1/public/github.png -------------------------------------------------------------------------------- /public/images/icons/close-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/icons/copy-clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | copy 5 | 6 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------