Skip to content

Commit

Permalink
Implemented the REST API (#1452)
Browse files Browse the repository at this point in the history
* 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
nathannaveen authored Nov 15, 2023
1 parent 565483d commit 1e5a333
Show file tree
Hide file tree
Showing 17 changed files with 2,546 additions and 11 deletions.
1 change: 1 addition & 0 deletions .github/scripts/excluded_from_copyright
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
./cmd/guacrest/docs/docs.go
./internal/testing/mocks/backend.go
./internal/testing/mocks/documentparser.go
./internal/testing/mocks/scorecard.go
Expand Down
16 changes: 16 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions cmd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions cmd/guacrest/README.md
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`
236 changes: 236 additions & 0 deletions cmd/guacrest/bad.go
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
}
Loading

0 comments on commit 1e5a333

Please sign in to comment.