-
Notifications
You must be signed in to change notification settings - Fork 174
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implemented the REST API * Fixes #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, 8336525. Signed-off-by: nathannaveen <[email protected]> * Added comments to vuln to improve readability Signed-off-by: nathannaveen <[email protected]> * Basic REST API for Bad Signed-off-by: nathannaveen <[email protected]> * Updated to include visualizer url Signed-off-by: nathannaveen <[email protected]> * Updated docs Signed-off-by: nathannaveen <[email protected]> * Included Tests for Bad Signed-off-by: nathannaveen <[email protected]> * Updated based on comment Signed-off-by: nathannaveen <[email protected]> * Updated Makefile Signed-off-by: nathannaveen <[email protected]> * Ignored other operating systems for goreleaser Signed-off-by: nathannaveen <[email protected]> * Included Swagger docs Signed-off-by: nathannaveen <[email protected]> * Fixed fmt Signed-off-by: nathannaveen <[email protected]> --------- Signed-off-by: nathannaveen <[email protected]>
- Loading branch information
1 parent
565483d
commit 1e5a333
Showing
17 changed files
with
2,546 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.