From 1e5a333697740afe0563eeefc3b11a2e0e1a8b26 Mon Sep 17 00:00:00 2001 From: Nathan Naveen <42319948+nathannaveen@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:46:54 -0600 Subject: [PATCH] Implemented the REST API (#1452) * Implemented the REST API * Fixes https://github.com/guacsec/guac/issues/1326 * Implemented the REST API for Known * Fixed docker-compose down When running `make stop-service` I was getting: ``` make stop-service docker compose down service "oci-collector" depends on undefined service guac-graphql: invalid compose project make: *** [stop-service] Error 15 ``` This is because the guac-graphql was removed from, 8336525f9e8ed9b7c34de0dd60cc66b9c300925a. Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Added comments to vuln to improve readability Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Basic REST API for Bad Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Updated to include visualizer url Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Updated docs Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Included Tests for Bad Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Updated based on comment Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Updated Makefile Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Ignored other operating systems for goreleaser Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Included Swagger docs Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Fixed fmt Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> --------- Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> --- .github/scripts/excluded_from_copyright | 1 + .goreleaser.yaml | 16 + cmd/README.md | 9 + cmd/guacrest/README.md | 25 ++ cmd/guacrest/bad.go | 236 ++++++++++ cmd/guacrest/bad_test.go | 95 ++++ cmd/guacrest/design.md | 55 +++ cmd/guacrest/docs/docs.go | 259 +++++++++++ cmd/guacrest/docs/swagger.json | 230 ++++++++++ cmd/guacrest/docs/swagger.yaml | 150 +++++++ cmd/guacrest/known.go | 366 +++++++++++++++ cmd/guacrest/known_test.go | 221 +++++++++ cmd/guacrest/main.go | 88 ++++ cmd/guacrest/vulnerability.go | 565 ++++++++++++++++++++++++ cmd/guacrest/vulnerability_test.go | 122 +++++ go.mod | 30 +- go.sum | 89 +++- 17 files changed, 2546 insertions(+), 11 deletions(-) create mode 100644 cmd/guacrest/README.md create mode 100644 cmd/guacrest/bad.go create mode 100644 cmd/guacrest/bad_test.go create mode 100644 cmd/guacrest/design.md create mode 100644 cmd/guacrest/docs/docs.go create mode 100644 cmd/guacrest/docs/swagger.json create mode 100644 cmd/guacrest/docs/swagger.yaml create mode 100644 cmd/guacrest/known.go create mode 100644 cmd/guacrest/known_test.go create mode 100644 cmd/guacrest/main.go create mode 100644 cmd/guacrest/vulnerability.go create mode 100644 cmd/guacrest/vulnerability_test.go diff --git a/.github/scripts/excluded_from_copyright b/.github/scripts/excluded_from_copyright index 95c131c85e..988a925673 100644 --- a/.github/scripts/excluded_from_copyright +++ b/.github/scripts/excluded_from_copyright @@ -1,3 +1,4 @@ +./cmd/guacrest/docs/docs.go ./internal/testing/mocks/backend.go ./internal/testing/mocks/documentparser.go ./internal/testing/mocks/scorecard.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 27c485116b..9066c50129 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -141,6 +141,22 @@ builds: goarch: arm64 - goos: windows goarch: arm + - main: ./cmd/guacrest + id: guacrest + binary: guacrest-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goarch: + - amd64 + - arm64 + - arm + ignore: + - goos: windows + goarch: arm64 + - goos: windows + goarch: arm universal_binaries: - replace: true diff --git a/cmd/README.md b/cmd/README.md index 5d7530253f..ee1a8057cb 100644 --- a/cmd/README.md +++ b/cmd/README.md @@ -61,6 +61,15 @@ services: - polling options - flag to toggle retrieving deps +**guacrest** + +**The guacrest is currently an EXPERIMENTAL Feature!** + +- what it does: runs a REST API server +- options: + - listening port + - gql endpoint + ## Collectors and Certifiers These appear both in `guacone` and in `guaccollect`. The difference is that diff --git a/cmd/guacrest/README.md b/cmd/guacrest/README.md new file mode 100644 index 0000000000..8945526fb4 --- /dev/null +++ b/cmd/guacrest/README.md @@ -0,0 +1,25 @@ +# REST API Documentation + +## The guacrest is currently an EXPERIMENTAL Feature! + +## Implementation: + +* Using gin-gonic gin framework for building REST API + +## Available HTTP Methods: + +* **GET** pURL - Fetches a known item using a given pURL. The pURL is a mandatory parameter. + * **Success Response**: + * If the pURL is valid and the known item is found, the server responds with HTTP status code `200` and includes the known item in the response body. + * **Error Responses**: + * If the pURL is invalid, the server responds with HTTP status code `400` (Bad Request). + * If the known item is not found for the provided pURL, the server responds with HTTP status code `404` (Not Found). + * For any other server errors, the server responds with HTTP status code `500` (Internal Server Error). + +## Endpoints: + +- `/known/package/*hash` +- `/known/source/*vcs` +- `/known/artifact/*artifact` +- `/vuln/*purl` +- `/bad` diff --git a/cmd/guacrest/bad.go b/cmd/guacrest/bad.go new file mode 100644 index 0000000000..1f92d90f11 --- /dev/null +++ b/cmd/guacrest/bad.go @@ -0,0 +1,236 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Khan/genqlient/graphql" + "github.com/gin-gonic/gin" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" +) + +// badHandler is a function that returns a gin.HandlerFunc. It handles requests to the /bad endpoint. +// This comment is for Swagger documentation +// @Summary Vulnerability handler +// @Description Handles the vulnerability based on the context +// @Tags Vulnerabilities +// @Accept json +// @Produce json +// @Param purl path string true "PURL" +// @Success 200 {object} Response +// @Failure 400 {object} HTTPError +// @Failure 404 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /vuln/{purl} [get] +func badHandler(ctx context.Context) func(c *gin.Context) { + return func(c *gin.Context) { + graphqlEndpoint, searchDepth, err := parseBadQueryParameters(c) + + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, fmt.Sprintf("error parsing query parameters: %v", err)}) + return + } + + httpClient := &http.Client{Timeout: httpTimeout} + gqlclient := graphql.NewClient(graphqlEndpoint, httpClient) + + certifyBadResponse, err := model.CertifyBads(ctx, gqlclient, model.CertifyBadSpec{}) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying for package: %v", err)}) + return + } + + // Iterate over the bad certifications. + for _, certifyBad := range certifyBadResponse.CertifyBad { + // Handle the different types of subjects. + switch subject := certifyBad.Subject.(type) { + case *model.AllCertifyBadSubjectPackage: + var path []string + + var pkgVersions []model.AllPkgTreeNamespacesPackageNamespaceNamesPackageNameVersionsPackageVersion + if len(subject.Namespaces[0].Names[0].Versions) == 0 { + pkgFilter := &model.PkgSpec{ + Type: &subject.Type, + Namespace: &subject.Namespaces[0].Namespace, + Name: &subject.Namespaces[0].Names[0].Name, + } + pkgResponse, err := model.Packages(ctx, gqlclient, *pkgFilter) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying for package: %v", err)}) + return + } + if len(pkgResponse.Packages) != 1 { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, "failed to located package based on package from certifyBad"}) + return + } + pkgVersions = pkgResponse.Packages[0].Namespaces[0].Names[0].Versions + } else { + pkgVersions = subject.Namespaces[0].Names[0].Versions + } + + pkgPath, err := searchDependencyPackagesReverse(ctx, gqlclient, "", pkgVersions[0].Id, searchDepth) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error searching dependency packages match: %v", err)}) + return + } + + if len(pkgPath) > 0 { + for _, version := range pkgVersions { + path = append([]string{certifyBad.Id, + version.Id, + subject.Namespaces[0].Names[0].Id, subject.Namespaces[0].Id, + subject.Id}, pkgPath...) + } + + response := Response{ + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(path), `,`)), + } + c.IndentedJSON(http.StatusOK, response) + } else { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "No paths to bad package found!\n"}) + } + case *model.AllCertifyBadSubjectSource: + var path []string + srcFilter := &model.SourceSpec{ + Type: &subject.Type, + Namespace: &subject.Namespaces[0].Namespace, + Name: &subject.Namespaces[0].Names[0].Name, + Tag: subject.Namespaces[0].Names[0].Tag, + Commit: subject.Namespaces[0].Names[0].Commit, + } + srcResponse, err := model.Sources(ctx, gqlclient, *srcFilter) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying for sources: %v", err)}) + return + } + if len(srcResponse.Sources) != 1 { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, "failed to located sources based on vcs"}) + return + } + + neighborResponse, err := model.Neighbors(ctx, gqlclient, srcResponse.Sources[0].Namespaces[0].Names[0].Id, []model.Edge{model.EdgeSourceHasSourceAt, model.EdgeSourceIsOccurrence}) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying neighbors: %v", err)}) + return + } + for _, neighbor := range neighborResponse.Neighbors { + switch v := neighbor.(type) { + case *model.NeighborsNeighborsHasSourceAt: + if len(v.Package.Namespaces[0].Names[0].Versions) > 0 { + path = append(path, v.Id, v.Package.Namespaces[0].Names[0].Versions[0].Id, v.Package.Namespaces[0].Names[0].Id, v.Package.Namespaces[0].Id, v.Package.Id) + } else { + path = append(path, v.Id, v.Package.Namespaces[0].Names[0].Id, v.Package.Namespaces[0].Id, v.Package.Id) + } + case *model.NeighborsNeighborsIsOccurrence: + path = append(path, v.Id, v.Artifact.Id) + default: + continue + } + } + + if len(path) > 0 { + fullCertifyBadPath := append([]string{certifyBad.Id, + subject.Namespaces[0].Names[0].Id, + subject.Namespaces[0].Id, subject.Id}, path...) + path = append(path, fullCertifyBadPath...) + response := Response{ + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(path), `,`)), + } + c.IndentedJSON(http.StatusOK, response) + } else { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "No paths to bad source found!\n"}) + } + + case *model.AllCertifyBadSubjectArtifact: + var path []string + artifactFilter := &model.ArtifactSpec{ + Algorithm: &subject.Algorithm, + Digest: &subject.Digest, + } + + artifactResponse, err := model.Artifacts(ctx, gqlclient, *artifactFilter) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying for artifacts: %v", err)}) + return + } + if len(artifactResponse.Artifacts) != 1 { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, "failed to located artifacts based on (algorithm:digest)"}) + return + } + neighborResponse, err := model.Neighbors(ctx, gqlclient, artifactResponse.Artifacts[0].Id, []model.Edge{model.EdgeArtifactHashEqual, model.EdgeArtifactIsOccurrence}) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying neighbors: %v", err)}) + return + } + for _, neighbor := range neighborResponse.Neighbors { + switch v := neighbor.(type) { + case *model.NeighborsNeighborsHashEqual: + path = append(path, v.Id) + case *model.NeighborsNeighborsIsOccurrence: + switch occurrenceSubject := v.Subject.(type) { + case *model.AllIsOccurrencesTreeSubjectPackage: + path = append(path, v.Id, occurrenceSubject.Namespaces[0].Names[0].Versions[0].Id, occurrenceSubject.Namespaces[0].Names[0].Id, occurrenceSubject.Namespaces[0].Id, occurrenceSubject.Id) + case *model.AllIsOccurrencesTreeSubjectSource: + path = append(path, v.Id, occurrenceSubject.Namespaces[0].Names[0].Id, occurrenceSubject.Namespaces[0].Id, occurrenceSubject.Id) + } + default: + continue + } + } + + if len(path) > 0 { + path = append(path, append([]string{certifyBad.Id, subject.Id}, path...)...) + response := Response{ + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(path), `,`)), + } + c.IndentedJSON(http.StatusOK, response) + } else { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "No paths to bad artifact found!\n"}) + } + } + } + } +} + +// parseBadQueryParameters is a helper function that parses the query parameters from a request. +func parseBadQueryParameters(c *gin.Context) (string, int, error) { + graphqlEndpoint := c.Query("gql_addr") + + if graphqlEndpoint == "" { + graphqlEndpoint = gqlDefaultServerURL + } + + var searchDepth int + var err error + + // Parse the search depth from the query parameters. + searchDepthString := c.Query("search_depth") + if searchDepthString != "" { + searchDepth, err = strconv.Atoi(searchDepthString) + if err != nil && searchDepthString != "" { + // If the search depth is not an integer, return an error. + return "", 0, errors.New("invalid search depth") + } + } + + return graphqlEndpoint, searchDepth, nil +} diff --git a/cmd/guacrest/bad_test.go b/cmd/guacrest/bad_test.go new file mode 100644 index 0000000000..1aa69a7c9f --- /dev/null +++ b/cmd/guacrest/bad_test.go @@ -0,0 +1,95 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package main + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestBadHandler(t *testing.T) { + type args struct { + gqlAddr string + searchDepth string + } + tests := []struct { + name string + args args + wantStatusCode int + wantBody string + }{ + { + name: "default", + args: args{ + gqlAddr: "http://localhost:8080/query", + searchDepth: "1", + }, + wantStatusCode: 200, + }, + { + name: "invalid search depth", + args: args{ + gqlAddr: "http://localhost:8080/query", + searchDepth: "invalid", + }, + wantStatusCode: 400, + }, + } + + r := gin.Default() + ctx := context.Background() + + r.GET("/bad", badHandler(ctx)) + + ts := httptest.NewServer(r) + defer ts.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "/bad?gql_addr="+tt.args.gqlAddr+"&search_depth="+tt.args.searchDepth, nil) + w := httptest.NewRecorder() + + r.ServeHTTP(w, req) + + resp, err := http.Get(ts.URL + "/bad?gql_addr=" + tt.args.gqlAddr + "&search_depth=" + tt.args.searchDepth) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + tt.wantBody = string(body) + + if diff := cmp.Diff(tt.wantStatusCode, w.Code); diff != "" { + t.Errorf("code mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.wantBody, w.Body.String(), cmpopts.SortSlices(func(x, y string) bool { return x < y })); diff != "" { + t.Errorf("body mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/cmd/guacrest/design.md b/cmd/guacrest/design.md new file mode 100644 index 0000000000..ce8193f49b --- /dev/null +++ b/cmd/guacrest/design.md @@ -0,0 +1,55 @@ +# Experimental REST Feature Design Document + +***This is a work-in-progress document.*** +## Introduction + +The Experimental REST service is a new addition to the GUAC project. It aims to provide a RESTful API interface for querying data from GUAC. This interface complements the existing GraphQL interface, providing an enhanced way for users to query the GUAC data. + +## Goals + +1. **Provide a RESTful API interface**: The primary goal of this feature is to provide a RESTful API interface that provides a simplified response to complex questions. + +2. **Support advanced queries**: The REST API contains advanced queries. These queries require complex client-side filtering, type checking, iteration, or path finding using the existing GraphQL interface. This interface contains all that logic in simple REST API. + +3. **Ensure compatibility with existing systems**: The REST API should adhere to REST conventions and best practices so that it is intuitive to consumers and is compatible with existing systems and tools that use RESTful APIs. + +## Proposed Design + +The REST API will be implemented using the Gin web framework in Go. The API will provide endpoints for querying packages, dependencies, and other related data. + +### Why Gin? + +Gin is a high-performance web framework for Go that provides robust features for building web applications. It is efficient, lean, and fully compatible with the `net/http` library, making it an excellent choice for implementing a REST API. Furthermore, Gin has a large and active community, ensuring it is well-maintained and up-to-date with the latest best practices in web development. + +### Why not the standard HTTP router? + +While the standard HTTP router in Go is powerful and flexible, it lacks some of the features and conveniences that Gin provides. For instance, Gin provides middleware support, better error handling, and routing capabilities that are more advanced than the standard HTTP router. Gin is known for its fast routing and request handling. It uses a radix tree for routing, which can sometimes be faster than the standard library's regular expression-based routing. + +### Endpoints + +The following endpoints will be provided: + +- `/known/package/*hash`: This endpoint will retrieve information about a known package based on its hash. The response will include details about the package and its neighbors. +- `/known/source/*vcs`: This endpoint will retrieve information about a known source based on its VCS. The response will include details about the source and its neighbors. +- `/known/artifact/*artifact`: This endpoint will retrieve information about a known artifact based on the artifact provided. The response will include details about the artifact and its neighbors. +- `/vuln/*purl`: This endpoint will retrieve information about a vulnerability based on its purl. The response will include details about the vulnerability and its neighbors. +- `/bad`: This endpoint will handle bad requests. + +### Data Models + +The data returned by the REST API will be structured according to the following models: + +- `Neighbors`: This model will represent the neighbors of a package. It will include fields for the neighbor's hash, scorecards, occurrences, and other relevant details. + +### Error Handling + +The REST API will return appropriate HTTP status codes in case of errors. + +### Dependencies + +- `graphql`: This library will interact with the GUAC database. It will allow the API to perform advanced queries and retrieve detailed information about packages and their dependencies. + +## References + +- Experimental-REST issue on GitHub: [https://github.com/guacsec/guac/issues/1326](https://github.com/guacsec/guac/issues/1326) +- Gin web framework: [https://github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) \ No newline at end of file diff --git a/cmd/guacrest/docs/docs.go b/cmd/guacrest/docs/docs.go new file mode 100644 index 0000000000..4dc981d79d --- /dev/null +++ b/cmd/guacrest/docs/docs.go @@ -0,0 +1,259 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/known/artifact/{artifact}": { + "get": { + "description": "Handles the known artifact based on the artifact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Known" + ], + "summary": "Known artifact handler for artifact", + "parameters": [ + { + "type": "string", + "description": "Artifact", + "name": "artifact", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + }, + "/known/package/{hash}": { + "get": { + "description": "Handles the known package based on the hash", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Known" + ], + "summary": "Known package handler for hash", + "parameters": [ + { + "type": "string", + "description": "Hash", + "name": "hash", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + }, + "/known/source/{vcs}": { + "get": { + "description": "Handles the known source based on the VCS", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Known" + ], + "summary": "Known source handler for VCS", + "parameters": [ + { + "type": "string", + "description": "VCS", + "name": "vcs", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + }, + "/vuln/{purl}": { + "get": { + "description": "Handles the vulnerability based on the context", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Vulnerabilities" + ], + "summary": "Vulnerability handler", + "parameters": [ + { + "type": "string", + "description": "PURL", + "name": "purl", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + } + }, + "definitions": { + "main.HTTPError": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + }, + "main.Response": { + "type": "object", + "properties": { + "NeighborsData": {}, + "Visualizer url": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/cmd/guacrest/docs/swagger.json b/cmd/guacrest/docs/swagger.json new file mode 100644 index 0000000000..56b4de9ccd --- /dev/null +++ b/cmd/guacrest/docs/swagger.json @@ -0,0 +1,230 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/known/artifact/{artifact}": { + "get": { + "description": "Handles the known artifact based on the artifact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Known" + ], + "summary": "Known artifact handler for artifact", + "parameters": [ + { + "type": "string", + "description": "Artifact", + "name": "artifact", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + }, + "/known/package/{hash}": { + "get": { + "description": "Handles the known package based on the hash", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Known" + ], + "summary": "Known package handler for hash", + "parameters": [ + { + "type": "string", + "description": "Hash", + "name": "hash", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + }, + "/known/source/{vcs}": { + "get": { + "description": "Handles the known source based on the VCS", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Known" + ], + "summary": "Known source handler for VCS", + "parameters": [ + { + "type": "string", + "description": "VCS", + "name": "vcs", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + }, + "/vuln/{purl}": { + "get": { + "description": "Handles the vulnerability based on the context", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Vulnerabilities" + ], + "summary": "Vulnerability handler", + "parameters": [ + { + "type": "string", + "description": "PURL", + "name": "purl", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.HTTPError" + } + } + } + } + } + }, + "definitions": { + "main.HTTPError": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + }, + "main.Response": { + "type": "object", + "properties": { + "NeighborsData": {}, + "Visualizer url": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/cmd/guacrest/docs/swagger.yaml b/cmd/guacrest/docs/swagger.yaml new file mode 100644 index 0000000000..74919eef7b --- /dev/null +++ b/cmd/guacrest/docs/swagger.yaml @@ -0,0 +1,150 @@ +definitions: + main.HTTPError: + properties: + code: + type: integer + message: + type: string + type: object + main.Response: + properties: + NeighborsData: {} + Visualizer url: + type: string + type: object +info: + contact: {} +paths: + /known/artifact/{artifact}: + get: + consumes: + - application/json + description: Handles the known artifact based on the artifact + parameters: + - description: Artifact + in: path + name: artifact + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/main.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/main.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.HTTPError' + summary: Known artifact handler for artifact + tags: + - Known + /known/package/{hash}: + get: + consumes: + - application/json + description: Handles the known package based on the hash + parameters: + - description: Hash + in: path + name: hash + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/main.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/main.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.HTTPError' + summary: Known package handler for hash + tags: + - Known + /known/source/{vcs}: + get: + consumes: + - application/json + description: Handles the known source based on the VCS + parameters: + - description: VCS + in: path + name: vcs + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/main.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/main.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.HTTPError' + summary: Known source handler for VCS + tags: + - Known + /vuln/{purl}: + get: + consumes: + - application/json + description: Handles the vulnerability based on the context + parameters: + - description: PURL + in: path + name: purl + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/main.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/main.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.HTTPError' + summary: Vulnerability handler + tags: + - Vulnerabilities +swagger: "2.0" diff --git a/cmd/guacrest/known.go b/cmd/guacrest/known.go new file mode 100644 index 0000000000..71d0cfb536 --- /dev/null +++ b/cmd/guacrest/known.go @@ -0,0 +1,366 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/guacsec/guac/internal/testing/ptrfrom" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" + "github.com/guacsec/guac/pkg/assembler/helpers" + + "github.com/Khan/genqlient/graphql" + "github.com/gin-gonic/gin" +) + +// This comment is for Swagger documentation +// @Summary Known artifact handler for artifact +// @Description Handles the known artifact based on the artifact +// @Tags Known +// @Accept json +// @Produce json +// @Param artifact path string true "Artifact" +// @Success 200 {object} Response +// @Failure 400 {object} HTTPError +// @Failure 404 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /known/artifact/{artifact} [get] +func artifactHandlerForArtifact(ctx context.Context) func(c *gin.Context) { + return func(c *gin.Context) { + graphqlEndpoint, err := parseKnownQueryParameters(c) + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, fmt.Sprintf("error parsing query parameters: %v", err)}) + return + } + + httpClient := &http.Client{Timeout: httpTimeout} + gqlclient := graphql.NewClient(graphqlEndpoint, httpClient) + + artifact := strings.TrimLeft(c.Param("artifact"), "/") // Retrieve and trim the artifact from the URL parameter + + split := strings.Split(artifact, ":") + if len(split) != 2 { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, "failed to parse artifact. Needs to be in algorithm:digest form"}) + return + } + artifactFilter := &model.ArtifactSpec{ + Algorithm: ptrfrom.String(strings.ToLower(split[0])), + Digest: ptrfrom.String(strings.ToLower(split[1])), + } + + artifactResponse, err := model.Artifacts(ctx, gqlclient, *artifactFilter) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying for artifacts: %v", err)}) + return + } + if len(artifactResponse.Artifacts) != 1 { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, "failed to located artifacts based on (algorithm:digest)"}) + return + } + artifactNeighbors, neighborsPath, err := queryKnownNeighbors(ctx, gqlclient, artifactResponse.Artifacts[0].Id) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying for artifact neighbors: %v", err)}) + return + } + + path := append([]string{artifactResponse.Artifacts[0].Id}, neighborsPath...) + + response := Response{ + NeighborsData: artifactNeighbors, + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(path), ",")), + } + c.IndentedJSON(200, response) + } +} + +// This comment is for Swagger documentation +// @Summary Known source handler for VCS +// @Description Handles the known source based on the VCS +// @Tags Known +// @Accept json +// @Produce json +// @Param vcs path string true "VCS" +// @Success 200 {object} Response +// @Failure 400 {object} HTTPError +// @Failure 404 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /known/source/{vcs} [get] +func sourceHandlerForVCS(ctx context.Context) func(c *gin.Context) { + return func(c *gin.Context) { + graphqlEndpoint, err := parseKnownQueryParameters(c) + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, fmt.Sprintf("error parsing query parameters: %v", err)}) + return + } + + httpClient := &http.Client{Timeout: httpTimeout} + gqlclient := graphql.NewClient(graphqlEndpoint, httpClient) + + vcs := strings.TrimLeft(c.Param("vcs"), "/") // Retrieve and trim the vcs from the URL parameter + + srcInput, err := helpers.VcsToSrc(vcs) + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, "invalid vcs"}) + return + } + + srcFilter := &model.SourceSpec{ + Type: &srcInput.Type, + Namespace: &srcInput.Namespace, + Name: &srcInput.Name, + Tag: srcInput.Tag, + Commit: srcInput.Commit, + } + + srcResponse, err := model.Sources(ctx, gqlclient, *srcFilter) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("Error querying source: %v", err)}) + return + } + + if len(srcResponse.Sources) != 1 { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "No source found for the given vcs"}) + return + } + + sourceNeighbors, neighborsPath, err := queryKnownNeighbors(ctx, gqlclient, srcResponse.Sources[0].Namespaces[0].Names[0].Id) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("Error querying for source Neighbors: %v", err)}) + return + } + + path := append([]string{srcResponse.Sources[0].Namespaces[0].Names[0].Id, + srcResponse.Sources[0].Namespaces[0].Id, srcResponse.Sources[0].Id}, neighborsPath...) + + response := Response{ + NeighborsData: sourceNeighbors, + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(path), ",")), + } + c.IndentedJSON(200, response) + } +} + +// This comment is for Swagger documentation +// @Summary Known package handler for hash +// @Description Handles the known package based on the hash +// @Tags Known +// @Accept json +// @Produce json +// @Param hash path string true "Hash" +// @Success 200 {object} Response +// @Failure 400 {object} HTTPError +// @Failure 404 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /known/package/{hash} [get] +func packageHandlerForHash(ctx context.Context) func(c *gin.Context) { + return func(c *gin.Context) { + graphqlEndpoint, err := parseKnownQueryParameters(c) + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, fmt.Sprintf("error parsing query parameters: %v", err)}) + return + } + + httpClient := &http.Client{Timeout: httpTimeout} + gqlclient := graphql.NewClient(graphqlEndpoint, httpClient) + + hash := strings.TrimLeft(c.Param("hash"), "/") // Retrieve and trim the hash from the URL parameter + + // Convert package URL to package input + pkgInput, err := helpers.PurlToPkg(hash) + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, "invalid hash"}) + return + } + + pkgFilter := createPackageFilter(pkgInput) + + // Query for the package using the package filter + pkgResponse, err := model.Packages(ctx, gqlclient, *pkgFilter) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("Error querying package: %v", err)}) + return + } + + if len(pkgResponse.Packages) != 1 { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "No package found for the given hash"}) + return + } + + // Query for the package's neighbors + res, path, err := queryNeighborsForPackage(ctx, gqlclient, pkgResponse.Packages[0]) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("Error querying Neighbors: %v", err)}) + return + } + + // Convert []*string to []string + var pathStrings []string + for _, s := range path { + if s != nil { + pathStrings = append(pathStrings, *s) + } + } + + response := Response{ + NeighborsData: res, + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(pathStrings), ",")), + } + c.IndentedJSON(200, response) + } +} + +func parseKnownQueryParameters(c *gin.Context) (string, error) { + graphqlEndpoint := c.Query("gql_addr") + + if graphqlEndpoint == "" { + graphqlEndpoint = gqlDefaultServerURL + } + + return graphqlEndpoint, nil +} + +// createPackageFilter generates a package filter from a given package input. +// It constructs a package qualifier filter from the qualifiers in the package input, +// and returns a package specification with the type, namespace, name, version, subpath, +// and qualifiers from the package input. +func createPackageFilter(pkgInput *model.PkgInputSpec) *model.PkgSpec { + pkgQualifierFilter := []model.PackageQualifierSpec{} + for _, qualifier := range pkgInput.Qualifiers { + pkgQualifierFilter = append(pkgQualifierFilter, model.PackageQualifierSpec{ + Key: qualifier.Key, + Value: &qualifier.Value, + }) + } + + return &model.PkgSpec{ + Type: &pkgInput.Type, + Namespace: pkgInput.Namespace, + Name: &pkgInput.Name, + Version: pkgInput.Version, + Subpath: pkgInput.Subpath, + Qualifiers: pkgQualifierFilter, + } +} + +// queryNeighborsForPackage is a function that queries for the neighbors of a given package. +// It takes in a context, a graphql client, and a package model. +// It returns a slice of pointers to Neighbors and an error. +func queryNeighborsForPackage(ctx context.Context, gqlclient graphql.Client, pkg model.PackagesPackagesPackage) ([]*Neighbors, []*string, error) { + var res []*Neighbors + var path []*string + + // Query for the package's name neighbors + pkgNameNeighbors, neighborsPath, err := queryKnownNeighbors(ctx, gqlclient, pkg.Namespaces[0].Names[0].Id) + if err != nil { + return nil, nil, err + } + + res = append(res, pkgNameNeighbors) + + path = append(path, &pkg.Namespaces[0].Names[0].Id, + &pkg.Namespaces[0].Id, &pkg.Id) + + for _, neighborPath := range neighborsPath { + path = append(path, &neighborPath) + } + + // Query for the package's version neighbors + pkgNameNeighbors, _, err = queryKnownNeighbors(ctx, gqlclient, pkg.Namespaces[0].Names[0].Versions[0].Id) + if err != nil { + return nil, nil, err + } + + res = append(res, pkgNameNeighbors) + + path = append(path, []*string{&pkg.Namespaces[0].Names[0].Versions[0].Id, + &pkg.Namespaces[0].Names[0].Id, &pkg.Namespaces[0].Id, &pkg.Id}...) + + for _, neighborPath := range neighborsPath { + path = append(path, &neighborPath) + } + + return res, path, nil +} + +type Neighbors struct { + HashEquals []*model.NeighborsNeighborsHashEqual `json:",omitempty"` + Scorecards []*model.NeighborsNeighborsCertifyScorecard `json:",omitempty"` + Occurrences []*model.NeighborsNeighborsIsOccurrence `json:",omitempty"` + HasSrcAt []*model.NeighborsNeighborsHasSourceAt `json:",omitempty"` + HasSBOMs []*model.NeighborsNeighborsHasSBOM `json:",omitempty"` + HasSLSAs []*model.NeighborsNeighborsHasSLSA `json:",omitempty"` + CertifyVulns []*model.NeighborsNeighborsCertifyVuln `json:",omitempty"` + VexLinks []*model.NeighborsNeighborsCertifyVEXStatement `json:",omitempty"` + BadLinks []*model.NeighborsNeighborsCertifyBad `json:",omitempty"` + GoodLinks []*model.NeighborsNeighborsCertifyGood `json:",omitempty"` + PkgEquals []*model.NeighborsNeighborsPkgEqual `json:",omitempty"` +} + +// queryKnownNeighbors is a function that queries for the neighbors of a given subject. +// It takes in a context, a graphql client, and a subject query ID. +// It returns a Neighbors struct and an error. +func queryKnownNeighbors(ctx context.Context, gqlclient graphql.Client, subjectQueryID string) (*Neighbors, []string, error) { + collectedNeighbors := &Neighbors{} + var path []string + // Query for neighbors using the subject query ID + neighborResponse, err := model.Neighbors(ctx, gqlclient, subjectQueryID, []model.Edge{}) + if err != nil { + return nil, nil, fmt.Errorf("error querying Neighbors: %v", err) + } + for _, neighbor := range neighborResponse.Neighbors { + switch v := neighbor.(type) { + case *model.NeighborsNeighborsCertifyVuln: + collectedNeighbors.CertifyVulns = append(collectedNeighbors.CertifyVulns, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsCertifyBad: + collectedNeighbors.BadLinks = append(collectedNeighbors.BadLinks, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsCertifyGood: + collectedNeighbors.GoodLinks = append(collectedNeighbors.GoodLinks, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsCertifyScorecard: + collectedNeighbors.Scorecards = append(collectedNeighbors.Scorecards, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsCertifyVEXStatement: + collectedNeighbors.VexLinks = append(collectedNeighbors.VexLinks, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsHasSBOM: + collectedNeighbors.HasSBOMs = append(collectedNeighbors.HasSBOMs, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsHasSLSA: + collectedNeighbors.HasSLSAs = append(collectedNeighbors.HasSLSAs, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsHasSourceAt: + collectedNeighbors.HasSrcAt = append(collectedNeighbors.HasSrcAt, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsHashEqual: + collectedNeighbors.HashEquals = append(collectedNeighbors.HashEquals, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsIsOccurrence: + collectedNeighbors.Occurrences = append(collectedNeighbors.Occurrences, v) + path = append(path, v.Id) + case *model.NeighborsNeighborsPkgEqual: + collectedNeighbors.PkgEquals = append(collectedNeighbors.PkgEquals, v) + path = append(path, v.Id) + default: + continue + } + } + return collectedNeighbors, path, nil +} diff --git a/cmd/guacrest/known_test.go b/cmd/guacrest/known_test.go new file mode 100644 index 0000000000..f70b27aa0c --- /dev/null +++ b/cmd/guacrest/known_test.go @@ -0,0 +1,221 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package main + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/go-cmp/cmp" +) + +func TestGetPackage(t *testing.T) { + type args struct { + url string + hash string + } + tests := []struct { + name string + args args + wantStatusCode int + wantBody string + }{ + { + name: "default", + args: args{ + url: "/known/package/pkg:golang/github.com/prometheus/client_golang@v1.11.1", + hash: "pkg:golang/github.com/prometheus/client_golang@v1.11.1", + }, + wantStatusCode: 200, + }, + { + name: "invalid hash", + args: args{ + url: "/known/package/invalid", + hash: "invalid", + }, + wantStatusCode: 400, + }, + { + name: "non-existent package", + args: args{ + url: "/known/package/pkg:golang/github.com/nonexistent/package@v1.0.0", + hash: "pkg:golang/github.com/nonexistent/package@v1.0.0", + }, + wantStatusCode: 404, + }, + } + + r := gin.Default() + ctx := context.Background() + + r.GET("/known/package/*hash", packageHandlerForHash(ctx)) + + ts := httptest.NewServer(r) + defer ts.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", tt.args.url, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + resp, err := http.Get(ts.URL + tt.args.url) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + tt.wantBody = string(body) + + if diff := cmp.Diff(tt.wantStatusCode, w.Code); diff != "" { + t.Errorf("code mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.wantBody, w.Body.String()); diff != "" { + t.Errorf("body mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestGetSource(t *testing.T) { + type args struct { + url string + vcs string + } + tests := []struct { + name string + args args + wantStatusCode int + }{ + { + name: "Valid VCS", + args: args{ + url: "/known/source/git+https://github.com/prometheus/client_golang", + vcs: "git+https://github.com/prometheus/client_golang", + }, + wantStatusCode: 200, + }, + { + name: "Invalid VCS", + args: args{ + url: "/known/source/invalid", + vcs: "invalid", + }, + wantStatusCode: 400, + }, + { + name: "Non-existent VCS", + args: args{ + url: "/known/source/git+https://github.com/nonexistent/vcs", + vcs: "git+https://github.com/nonexistent/vcs", + }, + wantStatusCode: 404, + }, + } + + r := gin.Default() + ctx := context.Background() + + r.GET("/known/source/*vcs", sourceHandlerForVCS(ctx)) + + ts := httptest.NewServer(r) + defer ts.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", tt.args.url, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + resp, err := http.Get(ts.URL + tt.args.url) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if diff := cmp.Diff(tt.wantStatusCode, w.Code); diff != "" { + t.Errorf("code mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestGetArtifact(t *testing.T) { + type args struct { + url string + artifact string + } + tests := []struct { + name string + args args + wantStatusCode int + wantBody string + }{ + { + name: "default", + args: args{ + url: "/known/artifact/sha256:625fe537a4c1657bd613be44f7882a8883c13c3b72919cfdbd02d2eb4dbf677b", + artifact: "sha256:625fe537a4c1657bd613be44f7882a8883c13c3b72919cfdbd02d2eb4dbf677b", + }, + wantStatusCode: 200, + wantBody: "ok", + }, + } + + r := gin.Default() + ctx := context.Background() + + r.GET("/known/artifact/*artifact", artifactHandlerForArtifact(ctx)) + + ts := httptest.NewServer(r) + defer ts.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", tt.args.url, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + resp, err := http.Get(ts.URL + tt.args.url) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + tt.wantBody = string(body) + + if diff := cmp.Diff(tt.wantStatusCode, w.Code); diff != "" { + t.Errorf("code mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.wantBody, w.Body.String()); diff != "" { + t.Errorf("body mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/cmd/guacrest/main.go b/cmd/guacrest/main.go new file mode 100644 index 0000000000..b7d2fecdde --- /dev/null +++ b/cmd/guacrest/main.go @@ -0,0 +1,88 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/gin-gonic/gin" +) + +type Response struct { + NeighborsData interface{} `json:"NeighborsData"` + VisualizerURL string `json:"Visualizer url"` +} + +type HTTPError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +const ( + gqlDefaultServerURL = "http://localhost:8080/query" + httpTimeout = 10 * time.Second + guacType = "guac" + noVulnType = "novuln" +) + +// This comment is for Swagger documentation +// Using https://github.com/swaggo/swag to generate the Swagger docs +// Use the command `swag init -g **/**/*.go` to generate docs because there are variables from outside the package guacrest. +// @title GUAC API +// @version 1.0 +// @description This is the GUAC API server. +// @termsOfService http://www.apache.org/licenses/LICENSE-2.0 +// @contact.name GUAC Support +// @contact.url http://www.guac.com/support +// @contact.email support@guac.com +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost:9000 +// @BasePath / +func main() { + if os.Getenv("GUAC_EXPERIMENTAL") != "true" { + log.Fatalf("GUAC_EXPERIMENTAL is not set to true. Exiting.") + } + + r := gin.Default() + ctx := context.Background() + + r.GET("/known/package/*hash", packageHandlerForHash(ctx)) + r.GET("/known/source/*vcs", sourceHandlerForVCS(ctx)) + r.GET("/known/artifact/*artifact", artifactHandlerForArtifact(ctx)) + r.GET("/vuln/*purl", vulnerabilityHandler(ctx)) + r.GET("/bad", badHandler(ctx)) + + if err := r.Run(":9000"); err != nil { + log.Fatalf("Failed to run server: %v", err) + } +} + +func removeDuplicateValuesFromPath(path []string) []string { + keys := make(map[string]bool) + var list []string + + for _, entry := range path { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list +} diff --git a/cmd/guacrest/vulnerability.go b/cmd/guacrest/vulnerability.go new file mode 100644 index 0000000000..0423af7cde --- /dev/null +++ b/cmd/guacrest/vulnerability.go @@ -0,0 +1,565 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/guacsec/guac/pkg/assembler/helpers" + + "github.com/Khan/genqlient/graphql" + "github.com/gin-gonic/gin" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" + "github.com/guacsec/guac/pkg/misc/depversion" +) + +// vulnerabilityHandler is a function that handles the vulnerability route for the REST API. +// It is responsible for parsing the query parameters from the request, creating a GraphQL client, +// and handling the vulnerability ID query or the no vulnerability ID query based on the parsed parameters. +// It returns a function that takes a gin.Context and performs the necessary operations to handle the request. +// This comment is for Swagger documentation +// @Summary Vulnerability handler +// @Description Handles the vulnerability based on the context +// @Tags Vulnerabilities +// @Accept json +// @Produce json +// @Param purl path string true "PURL" +// @Success 200 {object} Response +// @Failure 400 {object} HTTPError +// @Failure 404 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /vuln/{purl} [get] +func vulnerabilityHandler(ctx context.Context) func(c *gin.Context) { + return func(c *gin.Context) { + vulnID, graphqlEndpoint, searchDepth, pathsToReturn, err := parseQueryParameters(c) + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, fmt.Sprintf("error parsing query parameters: %v", err)}) + return + } + + httpClient := &http.Client{Timeout: httpTimeout} + gqlclient := graphql.NewClient(graphqlEndpoint, httpClient) + + purl := strings.TrimLeft(c.Param("purl"), "/") // Retrieve and trim the purl from the URL parameter + + // Convert package URL to package input + pkgInput, err := helpers.PurlToPkg(purl) + if err != nil { + c.JSON(http.StatusBadRequest, HTTPError{http.StatusBadRequest, "invalid purl"}) + return + } + + pkgFilter := createPackageFilter(pkgInput) + + // Query for the package + pkgResponse, err := model.Packages(ctx, gqlclient, *pkgFilter) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying package: %v", err)}) + return + } + + if len(pkgResponse.Packages) != 1 { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "no package found for the given hash"}) + return + } + + if vulnID != "" { + handleVulnerabilityIDQuery(ctx, c, gqlclient, vulnID, pkgResponse, searchDepth, pathsToReturn) + } else { + handleNoVulnerabilityIDQuery(ctx, c, gqlclient, pkgResponse, searchDepth) + } + } +} + +// parseQueryParameters is a function that parses the query parameters from the request. +// It extracts the vulnerability ID, search depth, and number of paths to return from the request. +// It returns the parsed parameters and an error if any of the parameters are invalid. +func parseQueryParameters(c *gin.Context) (string, string, int, int, error) { + vulnID := c.Query("vuln_id") + + graphqlEndpoint := c.Query("gql_addr") + + if graphqlEndpoint == "" { + graphqlEndpoint = gqlDefaultServerURL + } + + searchDepthString := c.Query("search_depth") + if searchDepthString == "" { + return "", "", 0, 0, errors.New("empty search depth") + } + searchDepth, err := strconv.Atoi(searchDepthString) + if err != nil && searchDepthString != "" { + return "", "", 0, 0, errors.New("invalid search depth") + } + + pathsToReturnString := c.Query("num_path") + if vulnID != "" && pathsToReturnString == "" { + return "", "", 0, 0, errors.New("empty number of paths even though vuln_id is given") + } + pathsToReturn, err := strconv.Atoi(pathsToReturnString) + if err != nil && pathsToReturnString != "" { + return "", "", 0, 0, errors.New("invalid number of paths") + } + + return vulnID, graphqlEndpoint, searchDepth, pathsToReturn, nil +} + +// handleVulnerabilityIDQuery is a function that handles the vulnerability ID query. +// It queries for vulnerabilities based on the vulnerability ID and handles the response. +// It takes a context, a gin.Context, a GraphQL client, a vulnerability ID, a package response, a search depth, and a number of paths to return. +// It does not return anything, but it does output a gin context in the form of a string. +func handleVulnerabilityIDQuery(ctx context.Context, c *gin.Context, gqlclient graphql.Client, vulnID string, pkgResponse *model.PackagesResponse, searchDepth, pathsToReturn int) { + vulnResponse, err := model.Vulnerabilities(ctx, gqlclient, model.VulnerabilitySpec{VulnerabilityID: &vulnID}) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying for vulnerabilities: %v", err)}) + return + } + + var path []string + + if len(vulnResponse.Vulnerabilities) > 0 { + path, err = QueryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse, vulnResponse.Vulnerabilities, model.EdgeVulnerabilityCertifyVuln, searchDepth, pathsToReturn) + + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying vulnerabilities via node neighbors: %v", err)}) + return + } + } + + if len(path) > 0 { + response := Response{ + NeighborsData: vulnResponse.Vulnerabilities, + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(path), `,`)), + } + c.IndentedJSON(200, response) + } else { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "no path to vulnerability ID found"}) + return + } +} + +// handleNoVulnerabilityIDQuery is a function that handles the case where no vulnerability ID is provided in the query. +// It queries for vulnerabilities based on the package response and the search depth. +// It takes a context, a gin.Context, a GraphQL client, a package response, and a search depth. +// It does not return anything, but it does output a gin context in the form of a string. +func handleNoVulnerabilityIDQuery(ctx context.Context, c *gin.Context, gqlclient graphql.Client, pkgResponse *model.PackagesResponse, searchDepth int) { + path, res := []string{}, []*Neighbors{} + + if pkgResponse.Packages[0].Type != guacType { + vulnPath, neighbors, err := queryVulnsViaPackageNeighbors(ctx, gqlclient, pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[0].Id, []model.Edge{model.EdgePackageCertifyVuln, model.EdgePackageCertifyVexStatement}) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error querying package neighbors: %v", err)}) + return + } + + path = append(path, vulnPath...) + res = append(res, neighbors...) + } + + depVulnPath, depVulnNeighbors, err := searchDependencyPackages(ctx, gqlclient, pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[0].Id, searchDepth) + if err != nil { + c.JSON(http.StatusInternalServerError, HTTPError{http.StatusInternalServerError, fmt.Sprintf("error searching dependency packages: %v", err)}) + } + path = append(path, depVulnPath...) + res = append(res, depVulnNeighbors...) + + if len(path) > 0 { + path = append([]string{pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[0].Id, + pkgResponse.Packages[0].Namespaces[0].Names[0].Id, pkgResponse.Packages[0].Namespaces[0].Id, + pkgResponse.Packages[0].Id}, path...) + + response := Response{ + NeighborsData: res, + VisualizerURL: fmt.Sprintf("http://localhost:3000/?path=%v", strings.Join(removeDuplicateValuesFromPath(path), `,`)), + } + c.IndentedJSON(200, response) + } else { + c.JSON(http.StatusNotFound, HTTPError{http.StatusNotFound, "no path to vulnerabilities found"}) + } +} + +// QueryVulnsViaVulnNodeNeighbors is a function that queries for vulnerabilities via node neighbors. +// This function traverses through the graph with searchDependencyPackagesReverse. +// It takes a context, a GraphQL client, a package response, a list of vulnerabilities, an edge type, a search depth, and a number of paths to return. +// It returns a list of paths to the vulnerabilities and an error if the query fails. +func QueryVulnsViaVulnNodeNeighbors(ctx context.Context, gqlclient graphql.Client, topPkgResponse *model.PackagesResponse, vulnerabilitiesResponses []model.VulnerabilitiesVulnerabilitiesVulnerability, edgeType model.Edge, depth, pathsToReturn int) ([]string, error) { + type vulnNeighbor struct { + node model.NeighborsNeighborsNode + id string + } + + var path []string + vulnNodeNeighborResponses := []vulnNeighbor{} + + for _, vulnerabilitiesResponse := range vulnerabilitiesResponses { + for _, vulnerabilityNodeID := range vulnerabilitiesResponse.VulnerabilityIDs { + vulnNodeNeighborResponse, err := model.Neighbors(ctx, gqlclient, vulnerabilityNodeID.Id, []model.Edge{edgeType}) + + if err != nil { + return nil, fmt.Errorf("error querying neighbor for vulnerability: %w", err) + } + + for _, neighbor := range vulnNodeNeighborResponse.Neighbors { + vulnNodeNeighborResponses = append(vulnNodeNeighborResponses, vulnNeighbor{neighbor, vulnerabilityNodeID.Id}) + } + } + } + + certifyVulnFound := false + numberOfPaths := 0 + for _, neighbor := range vulnNodeNeighborResponses { + if certifyVuln, ok := neighbor.node.(*model.NeighborsNeighborsCertifyVuln); ok { + certifyVulnFound = true + pkgPath, err := searchDependencyPackagesReverse(ctx, gqlclient, topPkgResponse.Packages[0].Namespaces[0].Names[0].Versions[0].Id, certifyVuln.Package.Namespaces[0].Names[0].Versions[0].Id, depth) + if err != nil { + return nil, fmt.Errorf("error searching dependency packages match: %w", err) + } + if len(pkgPath) > 0 { + fullVulnPath := append([]string{neighbor.id, certifyVuln.Id, + certifyVuln.Package.Namespaces[0].Names[0].Versions[0].Id, + certifyVuln.Package.Namespaces[0].Names[0].Id, certifyVuln.Package.Namespaces[0].Id, + certifyVuln.Package.Id}, pkgPath...) + path = append(path, fullVulnPath...) + numberOfPaths += 1 + } + if pathsToReturn != 0 && numberOfPaths == pathsToReturn { + return path, nil + } + } + } + if !certifyVulnFound { + return nil, fmt.Errorf("error certify vulnerability node not found, incomplete data. Please ensure certifier has run") + } + return path, nil +} + +// queryVulnsViaPackageNeighbors queries vulnerabilities via package neighbors. +// It takes in a context, a graphql client, a package version ID and a slice of edge types. +// It returns a slice of strings, a slice of pointers to Neighbors and an error. +// The function queries the neighbors for vulnerabilities and checks if a certify vulnerability node is found. +// If not, it returns an error. If yes, it appends the certify vulnerability to the result slice and the vulnerability IDs to the path slice. +// It also checks for certify VEX statements and appends them to the result slice and their IDs to the path slice. +// The function returns the path slice, the result slice and nil if no errors occurred. +func queryVulnsViaPackageNeighbors(ctx context.Context, gqlclient graphql.Client, pkgVersionID string, edgeTypes []model.Edge) ([]string, []*Neighbors, error) { + var path []string + var res []*Neighbors + pkgVersionNeighborResponse, err := model.Neighbors(ctx, gqlclient, pkgVersionID, edgeTypes) + if err != nil { + return nil, nil, fmt.Errorf("error querying neighbor for vulnerability: %w", err) + } + certifyVulnFound := false + for _, neighbor := range pkgVersionNeighborResponse.Neighbors { + if certifyVuln, ok := neighbor.(*model.NeighborsNeighborsCertifyVuln); ok { + certifyVulnFound = true + if certifyVuln.Vulnerability.Type != noVulnType { + res = append(res, &Neighbors{CertifyVulns: []*model.NeighborsNeighborsCertifyVuln{certifyVuln}}) + for _, vuln := range certifyVuln.Vulnerability.VulnerabilityIDs { + path = append(path, []string{vuln.Id, certifyVuln.Id, + certifyVuln.Package.Namespaces[0].Names[0].Versions[0].Id, + certifyVuln.Package.Namespaces[0].Names[0].Id, certifyVuln.Package.Namespaces[0].Id, + certifyVuln.Package.Id}...) + } + } + } + + if certifyVex, ok := neighbor.(*model.NeighborsNeighborsCertifyVEXStatement); ok { + res = append(res, &Neighbors{VexLinks: []*model.NeighborsNeighborsCertifyVEXStatement{certifyVex}}) + for _, vuln := range certifyVex.Vulnerability.VulnerabilityIDs { + path = append(path, certifyVex.Id, vuln.Id) + } + path = append(path, vexSubjectIds(certifyVex.Subject)...) + } + + } + if !certifyVulnFound { + return nil, nil, fmt.Errorf("error certify vulnerability node not found, incomplete data. Please ensure certifier has run") + } + return path, res, nil +} + +// vexSubjectIds returns a slice of strings containing the IDs of the subject of a VEX statement. +// It takes in a model.AllCertifyVEXStatementSubjectPackageOrArtifact and checks its type. +// If it's a model.AllCertifyVEXStatementSubjectArtifact, it returns a slice containing its ID. +// If it's a model.AllCertifyVEXStatementSubjectPackage, it returns a slice containing its ID and the IDs of its namespaces, names and versions. +// If it's neither, it returns an empty slice. +func vexSubjectIds(s model.AllCertifyVEXStatementSubjectPackageOrArtifact) []string { + switch v := s.(type) { + case *model.AllCertifyVEXStatementSubjectArtifact: + return []string{v.Id} + case *model.AllCertifyVEXStatementSubjectPackage: + return []string{ + v.Id, + v.Namespaces[0].Id, + v.Namespaces[0].Names[0].Id, + v.Namespaces[0].Names[0].Versions[0].Id} + default: + return []string{} + } +} + +// searchDependencyPackages is a function that performs a breadth-first search (BFS) on the dependency graph. +// It starts from a given package (topPkgID) and traverses the graph in the direction from dependencies to dependents. +// The function stops when it reaches a specified depth (maxLength). +// The function returns a path from the topPkgID to the deepest package. +// The path is represented as a slice of strings, where each string is an ID of a package in the path. +// The function also collects the table rows of all visited packages in the res slice. +// The function uses a queue to implement the BFS algorithm and a map (nodeMap) to keep track of visited packages and their properties. +func searchDependencyPackages(ctx context.Context, gqlclient graphql.Client, topPkgID string, maxLength int) ([]string, []*Neighbors, error) { + var path []string + var res []*Neighbors + queue := make([]string, 0) // the queue of nodes in bfs + type dfsNode struct { + expanded bool // true once all node neighbors are added to queue + parent string + depth int + } + nodeMap := map[string]dfsNode{} + + nodeMap[topPkgID] = dfsNode{} + queue = append(queue, topPkgID) + + for len(queue) > 0 { + now := queue[0] + queue = queue[1:] + nowNode := nodeMap[now] + + // Stop if the maximum depth is reached + if maxLength != 0 && nowNode.depth >= maxLength { + break + } + + // Query the neighbors of the current node + isDependencyNeighborResponses, err := model.Neighbors(ctx, gqlclient, now, []model.Edge{model.EdgePackageIsDependency}) + if err != nil { + return nil, nil, fmt.Errorf("failed getting package parent:%w", err) + } + for _, neighbor := range isDependencyNeighborResponses.Neighbors { + if isDependency, ok := neighbor.(*model.NeighborsNeighborsIsDependency); ok { + if isDependency.DependencyPackage.Type == guacType { + continue + } + + depPkgFilter := &model.PkgSpec{ + Type: &isDependency.DependencyPackage.Type, + Namespace: &isDependency.DependencyPackage.Namespaces[0].Namespace, + Name: &isDependency.DependencyPackage.Namespaces[0].Names[0].Name, + } + + depPkgResponse, err := model.Packages(ctx, gqlclient, *depPkgFilter) + if err != nil { + return nil, nil, fmt.Errorf("error querying for dependent package: %w", err) + } + + // Create a map and a slice for the versions of the dependency package + depPkgVersionsMap := map[string]string{} + depPkgVersions := []string{} + for _, depPkgVersion := range depPkgResponse.Packages[0].Namespaces[0].Names[0].Versions { + depPkgVersions = append(depPkgVersions, depPkgVersion.Version) + depPkgVersionsMap[depPkgVersion.Version] = depPkgVersion.Id + } + + // Determine which versions of the dependency package match the version range + matchingDepPkgVersions, err := depversion.WhichVersionMatches(depPkgVersions, isDependency.VersionRange) + if err != nil { + // TODO(jeffmendoza): depversion is not handling all/new possible + // version ranges from deps.dev. Continue here to report possible + // vulns even if some paths cannot be followed. + matchingDepPkgVersions = nil + //return nil, nil, fmt.Errorf("error determining dependent version matches: %w", err) + } + + for matchingDepPkgVersion := range matchingDepPkgVersions { + matchingDepPkgVersionID := depPkgVersionsMap[matchingDepPkgVersion] + vulnPath, foundVulnTableRow, err := queryVulnsViaPackageNeighbors(ctx, gqlclient, matchingDepPkgVersionID, []model.Edge{model.EdgePackageCertifyVuln, model.EdgePackageCertifyVexStatement}) + if err != nil { + return nil, nil, fmt.Errorf("error querying neighbor: %w", err) + } + // If vulnerabilities are found, add them to the path and result slices + if len(vulnPath) > 0 { + path = append(path, isDependency.Id, matchingDepPkgVersionID, + depPkgResponse.Packages[0].Namespaces[0].Names[0].Id, depPkgResponse.Packages[0].Namespaces[0].Id, + depPkgResponse.Packages[0].Id) + path = append(path, vulnPath...) + res = append(res, foundVulnTableRow...) + } + + // Check if the matching version has been visited before + dfsN, seen := nodeMap[matchingDepPkgVersionID] + if !seen { + dfsN = dfsNode{ + parent: now, + depth: nowNode.depth + 1, + } + nodeMap[matchingDepPkgVersionID] = dfsN + } + // If the matching version hasn't been expanded yet, add it to the queue + if !dfsN.expanded { + queue = append(queue, matchingDepPkgVersionID) + } + } + } + } + + // Mark the current node as expanded + nowNode.expanded = true + nodeMap[now] = nowNode + } + + return path, res, nil +} + +// searchDependencyPackagesReverse performs a breadth-first search (BFS) on the dependency graph. +// It starts from a given package (searchPkgID) and traverses the graph in reverse direction (from dependents to dependencies). +// The function stops either when it reaches a specified package (topPkgID) or when it reaches a specified depth (maxLength). +// The function returns a path from the searchPkgID to the topPkgID (or to the deepest package if topPkgID is not specified). +// The path is represented as a slice of strings, where each string is an ID of a package in the path. +// If the function cannot find a path to the topPkgID, it returns an error. +// The function also collects the IDs of all visited packages in the collectedIDs slice. +// The function uses a queue to implement the BFS algorithm and a map (nodeMap) to keep track of visited packages and their properties. +func searchDependencyPackagesReverse(ctx context.Context, gqlclient graphql.Client, topPkgID string, searchPkgID string, maxLength int) ([]string, error) { + var path []string + var collectedIDs []string + queue := make([]string, 0) // the queue of nodes in bfs + type dfsNode struct { + expanded bool // true once all node neighbors are added to queue + parent string + isDependency *model.NeighborsNeighborsIsDependency + depth int + } + nodeMap := map[string]dfsNode{} + + // Add the starting package to the nodeMap and queue + nodeMap[searchPkgID] = dfsNode{} + queue = append(queue, searchPkgID) + collectedIDs = append(collectedIDs, searchPkgID) + + found := false + for len(queue) > 0 { + now := queue[0] + queue = queue[1:] + nowNode := nodeMap[now] + + // If the current package is the topPkgID, set found to true and break the loop + if topPkgID != "" { + if now == topPkgID { + found = true + break + } + } + + // If a maxLength is specified and the depth of the current package is greater or equal to maxLength, break the loop + if maxLength != 0 && nowNode.depth >= maxLength { + break + } + + pkgNameNeighborResponses, err := model.Neighbors(ctx, gqlclient, now, []model.Edge{}) + if err != nil { + return nil, fmt.Errorf("failed getting package parent:%v", err) + } + + for _, neighbor := range pkgNameNeighborResponses.Neighbors { + if pkgName, ok := neighbor.(*model.NeighborsNeighborsPackage); ok { + // If the package has no namespaces, skip it + if len(pkgName.Namespaces) == 0 { + continue + } + isDependencyNeighborResponses, err := model.Neighbors(ctx, gqlclient, pkgName.Namespaces[0].Names[0].Id, []model.Edge{model.EdgePackageIsDependency}) + if err != nil { + return nil, fmt.Errorf("failed getting package parent:%v", err) + } + + // Iterate over the dependencies of the package + for _, neighbor := range isDependencyNeighborResponses.Neighbors { + if isDependency, ok := neighbor.(*model.NeighborsNeighborsIsDependency); ok && now != isDependency.Package.Namespaces[0].Names[0].Versions[0].Id { + dfsN, seen := nodeMap[isDependency.Package.Namespaces[0].Names[0].Versions[0].Id] + if !seen { + dfsN = dfsNode{ + parent: now, + isDependency: isDependency, + depth: nowNode.depth + 1, + } + nodeMap[isDependency.Package.Namespaces[0].Names[0].Versions[0].Id] = dfsN + } + + // If the dependency has not been expanded, add it to the queue + if !dfsN.expanded { + queue = append(queue, isDependency.Package.Namespaces[0].Names[0].Versions[0].Id) + collectedIDs = append(collectedIDs, isDependency.Package.Namespaces[0].Names[0].Versions[0].Id) + } + } + } + } + + // If the neighbor is a dependency and it's not the current package, add it to the nodeMap and queue + if isDependency, ok := neighbor.(*model.NeighborsNeighborsIsDependency); ok && now != isDependency.Package.Namespaces[0].Names[0].Versions[0].Id { + dfsN, seen := nodeMap[isDependency.Package.Namespaces[0].Names[0].Versions[0].Id] + if !seen { + dfsN = dfsNode{ + parent: now, + isDependency: isDependency, + depth: nowNode.depth + 1, + } + nodeMap[isDependency.Package.Namespaces[0].Names[0].Versions[0].Id] = dfsN + } + if !dfsN.expanded { + queue = append(queue, isDependency.Package.Namespaces[0].Names[0].Versions[0].Id) + collectedIDs = append(collectedIDs, isDependency.Package.Namespaces[0].Names[0].Versions[0].Id) + } + } + + } + + // Mark the current node as expanded + nowNode.expanded = true + nodeMap[now] = nowNode + } + + if topPkgID != "" && !found { + return nil, fmt.Errorf("no path found up to specified length") + } + + var now string + if topPkgID != "" { + // If a topPkgID is specified, build the path from topPkgID to searchPkgID + now = topPkgID + for now != searchPkgID { + path = append(path, nodeMap[now].isDependency.Id, nodeMap[now].isDependency.DependencyPackage.Namespaces[0].Names[0].Id, + nodeMap[now].isDependency.DependencyPackage.Namespaces[0].Id, nodeMap[now].isDependency.DependencyPackage.Id, + nodeMap[now].isDependency.Package.Namespaces[0].Names[0].Versions[0].Id, + nodeMap[now].isDependency.Package.Namespaces[0].Names[0].Id, nodeMap[now].isDependency.Package.Namespaces[0].Id, + nodeMap[now].isDependency.Package.Id) + now = nodeMap[now].parent + } + return path, nil + } else { + // If a topPkgID is not specified, build the path from the deepest package to searchPkgID + for i := len(collectedIDs) - 1; i >= 0; i-- { + if nodeMap[collectedIDs[i]].isDependency != nil { + path = append(path, nodeMap[collectedIDs[i]].isDependency.Id, nodeMap[collectedIDs[i]].isDependency.DependencyPackage.Namespaces[0].Names[0].Id, + nodeMap[collectedIDs[i]].isDependency.DependencyPackage.Namespaces[0].Id, nodeMap[collectedIDs[i]].isDependency.DependencyPackage.Id, + nodeMap[collectedIDs[i]].isDependency.Package.Namespaces[0].Names[0].Versions[0].Id, + nodeMap[collectedIDs[i]].isDependency.Package.Namespaces[0].Names[0].Id, nodeMap[collectedIDs[i]].isDependency.Package.Namespaces[0].Id, + nodeMap[collectedIDs[i]].isDependency.Package.Id) + } + } + return path, nil + } +} diff --git a/cmd/guacrest/vulnerability_test.go b/cmd/guacrest/vulnerability_test.go new file mode 100644 index 0000000000..3fff7452d5 --- /dev/null +++ b/cmd/guacrest/vulnerability_test.go @@ -0,0 +1,122 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package main + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestVulnerabilityHandler(t *testing.T) { + type args struct { + vulnID string + searchDepth string + numPath string + purl string + } + tests := []struct { + name string + args args + wantStatusCode int + wantBody string + }{ + { + name: "default", + args: args{ + vulnID: "ghsa-7rjr-3q55-vv33", + searchDepth: "1", + numPath: "1", + purl: "pkg:guac/spdx/ghcr.io/guacsec/vul-image-latest", + }, + wantStatusCode: 200, + }, + { + name: "invalid search depth", + args: args{ + vulnID: "", + searchDepth: "invalid", + numPath: "", + purl: "pkg:guac/spdx/ghcr.io/guacsec/vul-image-latest", + }, + wantStatusCode: 400, + }, + { + name: "invalid purl", + args: args{ + vulnID: "", + searchDepth: "1", + numPath: "", + purl: "invalid", + }, + wantStatusCode: 400, + }, + { + name: "invalid number of paths", + args: args{ + vulnID: "ghsa-7rjr-3q55-vv33", + searchDepth: "1", + numPath: "invalid", + purl: "pkg:guac/spdx/ghcr.io/guacsec/vul-image-latest", + }, + wantStatusCode: 400, + }, + } + + r := gin.Default() + ctx := context.Background() + + r.GET("/vuln/*purl", vulnerabilityHandler(ctx)) + + ts := httptest.NewServer(r) + defer ts.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "/vuln/"+url.PathEscape(tt.args.purl)+"?vuln_id="+tt.args.vulnID+"&search_depth="+tt.args.searchDepth+"&num_path="+tt.args.numPath, nil) + w := httptest.NewRecorder() + + r.ServeHTTP(w, req) + + resp, err := http.Get(ts.URL + "/vuln/" + tt.args.purl + "?vuln_id=" + tt.args.vulnID + "&search_depth=" + tt.args.searchDepth + "&num_path=" + tt.args.numPath) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + tt.wantBody = string(body) + + if diff := cmp.Diff(tt.wantStatusCode, w.Code); diff != "" { + t.Errorf("code mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.wantBody, w.Body.String(), cmpopts.SortSlices(func(x, y string) bool { return x < y })); diff != "" { + t.Errorf("body mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/go.mod b/go.mod index 2d42bb689f..bdd1fca12c 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.15.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.18.0 // indirect golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.5.0 golang.org/x/sys v0.14.0 // indirect @@ -51,6 +51,7 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect dario.cat/mergo v1.0.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect @@ -80,8 +81,10 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bombsimon/logrusr/v2 v2.0.1 // indirect github.com/bradleyfalzon/ghinstallation/v2 v2.8.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect github.com/caarlos0/env/v6 v6.10.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect @@ -97,12 +100,22 @@ require ( github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.15.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goark/errs v1.3.2 // indirect github.com/goark/go-cvss v1.6.6 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/google/go-containerregistry v0.16.1 // indirect @@ -123,10 +136,14 @@ require ( github.com/ianlancetaylor/demangle v0.0.0-20231023195312-e2daf7ba7156 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf // indirect github.com/logrusorgru/aurora/v3 v3.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -168,6 +185,8 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect github.com/theupdateframework/go-tuf v0.5.2 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/urfave/cli/v2 v2.25.7 // indirect github.com/vbatts/tar-split v0.11.3 // indirect @@ -179,6 +198,7 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zclconf/go-cty v1.10.0 // indirect gocloud.dev v0.34.0 // indirect + golang.org/x/arch v0.3.0 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/term v0.14.0 // indirect golang.org/x/time v0.4.0 // indirect @@ -206,9 +226,10 @@ require ( github.com/aws/aws-sdk-go v1.46.2 github.com/aws/aws-sdk-go-v2 v1.22.2 github.com/aws/aws-sdk-go-v2/config v1.19.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.42.1 - github.com/aws/aws-sdk-go-v2/service/sqs v1.26.0 - github.com/fsnotify/fsnotify v1.7.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.38.1 + github.com/aws/aws-sdk-go-v2/service/sqs v1.24.1 + github.com/fsnotify/fsnotify v1.6.0 + github.com/gin-gonic/gin v1.9.1 github.com/go-git/go-git/v5 v5.10.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/uuid v4.4.0+incompatible @@ -238,6 +259,7 @@ require ( github.com/spdx/tools-golang v0.5.3 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 + github.com/swaggo/swag v1.16.2 github.com/vektah/gqlparser/v2 v2.5.10 golang.org/x/exp v0.0.0-20231006140011-7918f672742d gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index fb80ac920a..62f3e0cbb4 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk= github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= @@ -109,9 +111,11 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.46.2 h1:XZbOmjtN1VCfEtQq7QNFsbxIqO+bB+bRhiOBjp6AzWc= github.com/aws/aws-sdk-go v1.46.2/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.20.0/go.mod h1:uWOr0m0jDsiWw8nnXiqZ+YG6LdvAlGYDLLf2NmHZoy4= github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= github.com/aws/aws-sdk-go-v2 v1.22.2 h1:lV0U8fnhAnPz8YcdmZVV60+tr6CakHzqA6P8T46ExJI= github.com/aws/aws-sdk-go-v2 v1.22.2/go.mod h1:Kd0OJtkW3Q0M0lUWGszapWjEvrXDzRW+D21JNsroB+c= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.11/go.mod h1:va22++AdXht4ccO3kH2SHkHHYvZ2G9Utz+CXKmm2CaU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.0 h1:hHgLiIrTRtddC0AKcJr5s7i/hLgcpTt+q/FKxf1Zayk= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.0/go.mod h1:w4I/v3NOWgD+qvs1NPEwhd++1h3XPHFaVxasfY6HlYQ= github.com/aws/aws-sdk-go-v2/config v1.19.1 h1:oe3vqcGftyk40icfLymhhhNysAwk0NfiwkDi2GTPMXs= @@ -122,35 +126,43 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVEN github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.76 h1:DJ1kHj0GI9BbX+XhF0kHxlzOVjcncmDUXmCvXdbfdAE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.76/go.mod h1:/AZCdswMSgwpB2yMSFfY5H4pVeBLnCuPehdmO/r3xSM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37/go.mod h1:Pdn4j43v49Kk6+82spO3Tu5gSeQXRsxo56ePPQAvFiA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.2 h1:AaQsr5vvGR7rmeSWBtTCcw16tT9r51mWijuCQhzLnq8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.2/go.mod h1:o1IiRn7CWocIFTXJjGKJDOwxv1ibL53NpcvcqGWyRBA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31/go.mod h1:fTJDMe8LOFYtqiFFFeHA+SVMAwqLhoq0kcInYoLa9Js= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.2 h1:UZx8SXZ0YtzRiALzYAWcjb9Y9hZUR7MBKaBQ5ouOjPs= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.2/go.mod h1:ipuRpcSaklmxR6C39G187TpBAO132gUfleTGccUPs8c= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.0/go.mod h1:EhC/83j8/hL/UB1WmExo3gkElaja/KlmZM/gl1rTfjM= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.2 h1:pyVrNAf7Hwz0u39dLKN5t+n0+K/3rMYKuiOoIum3AsU= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.2/go.mod h1:mydrfOb9uiOYCxuCPR8YHQNQyGQwUQ7gPMZGBKbH8NY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.12/go.mod h1:fUTHpOXqRQpXvEpDPSa3zxCc2fnpW6YnBoba+eQr+Bg= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.0 h1:CJxo7ZBbaIzmXfV3hjcx36n9V87gJsIUPJflwqEHl3Q= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.0/go.mod h1:yjVfjuY4nD1EW9i387Kau+I6V5cBA5YnC/mWNopjZrI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.32/go.mod h1:QmMEM7es84EUkbYWcpnkx8i5EW2uERPfrTFeOch128Y= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.2 h1:f2LhPofnjcdOQKRtumKjMvIHkfSQ8aH/rwKUDEQ/SB4= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.2/go.mod h1:q+xX0H4OfuWDuBy7y/LDi4v8IBOWuF+vtp8Z6ex+lw4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.31/go.mod h1:3+lloe3sZuBQw1aBc5MyndvodzQlyqCZ7x1QPDHaWP4= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.2 h1:h7j73yuAVVjic8pqswh+L/7r2IHP43QwRyOu6zcCDDE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.2/go.mod h1:H07AHdK5LSy8F7EJUQhoxyiCNkePoHj2D8P2yGTWafo= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.0/go.mod h1:FWNzS4+zcWAP05IF7TDYTY1ysZAzIvogxWaDT9p8fsA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.2 h1:gbIaOzpXixUpoPK+js/bCBK1QBDXM22SigsnzGZio0U= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.2/go.mod h1:p+S7RNbdGN8qgHDSg2SCQJ9FeMAmvcETQiVpeGhYnNM= -github.com/aws/aws-sdk-go-v2/service/s3 v1.42.1 h1:o6MCcX1rJW8Y3g+hvg2xpjF6JR6DftuYhfl3Nc1WV9Q= -github.com/aws/aws-sdk-go-v2/service/s3 v1.42.1/go.mod h1:UDtxEWbREX6y4KREapT+jjtjoH0TiVSS6f5nfaY1UaM= -github.com/aws/aws-sdk-go-v2/service/sqs v1.26.0 h1:21QmEZkOnaJ4SPRFhhN+8MV5ewb0j1lxTg+RPp0mUeE= -github.com/aws/aws-sdk-go-v2/service/sqs v1.26.0/go.mod h1:E02a07/HTyJEHFpp+WMRh33xuNVdsd8WCbLlODeT4lU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.38.1 h1:mTgFVlfQT8gikc5+/HwD8UL9jnUro5MGv8n/VEYF12I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.38.1/go.mod h1:6SOWLiobcZZshbmECRTADIRYliPL0etqFSigauQEeT0= +github.com/aws/aws-sdk-go-v2/service/sqs v1.24.1 h1:KbGaxApdPOT2ZWqJiQY5ApnpNhUGbGTjYiKAidlFwp8= +github.com/aws/aws-sdk-go-v2/service/sqs v1.24.1/go.mod h1:+phkm4aFvcM4jbsDRGoZ+mD8MMvksHF459Xpy5Z90f0= github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.14.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.16.0 h1:gJZEH/Fqh+RsvlJ1Zt4tVAtV6bKkp3cC+R6FCZMNzik= github.com/aws/smithy-go v1.16.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= @@ -164,11 +176,17 @@ github.com/bradleyfalzon/ghinstallation/v2 v2.8.0/go.mod h1:fmPmvCiBWhJla3zDv9ZT github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -241,10 +259,16 @@ github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsouza/fake-gcs-server v1.47.6 h1:/d/879q/Os9Zc5gyV3QVLfZoajN1KcWucf2zYCFeFxs= github.com/fsouza/fake-gcs-server v1.47.6/go.mod h1:ApSXKexpG1BUXJ4f2tNCxvhTKwCPFqFLBDW2UNQDODE= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -265,6 +289,29 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -277,6 +324,8 @@ github.com/goark/go-cvss v1.6.6 h1:WJFuIWqmAw1Ilb9USv0vuX+nYzOWJp8lIujseJ/y3sU= github.com/goark/go-cvss v1.6.6/go.mod h1:H3qbfUSUlV7XtA3EwWNunvXz6OySwWHOuO+R6ZPMQPI= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -443,6 +492,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -454,6 +505,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -469,6 +521,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf h1:ndns1qx/5dL43g16EQkPV/i8+b3l5bYQwLeoSBe7tS8= github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf/go.mod h1:aGkAgvWY/IUcVFfuly53REpfv5edu25oij+qHRFaraA= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -477,6 +531,11 @@ github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvz github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= @@ -527,6 +586,7 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/neo4j/neo4j-go-driver/v4 v4.4.7 h1:6D0DPI7VOVF6zB8eubY1lav7RI7dZ2mytnr3fj369Ow= github.com/neo4j/neo4j-go-driver/v4 v4.4.7/go.mod h1:NexOfrm4c317FVjekrhVV8pHBXgtMG5P6GeweJWCyo4= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -656,17 +716,24 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= github.com/theupdateframework/go-tuf v0.5.2 h1:habfDzTmpbzBLIFGWa2ZpVhYvFBoK0C1onC3a4zuPRA= github.com/theupdateframework/go-tuf v0.5.2/go.mod h1:SyMV5kg5n4uEclsyxXJZI2UxPFJNDc4Y+r7wv+MlvTA= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= @@ -728,6 +795,9 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= gocloud.dev v0.34.0 h1:LzlQY+4l2cMtuNfwT2ht4+fiXwWf/NmPTnXUlLmGif4= gocloud.dev v0.34.0/go.mod h1:psKOachbnvY3DAOPbsFVmLIErwsbWPUG2H5i65D38vE= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -826,8 +896,9 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -912,6 +983,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1128,6 +1200,7 @@ gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1146,6 +1219,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1161,6 +1235,7 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/release-utils v0.7.3 h1:6pS8x6c5RmdUgR9qcg1LO6hjUzuE4Yo9TGZ3DemrZdM=