From 36d8213a94097d585e0d2659d3899e31671811fa Mon Sep 17 00:00:00 2001 From: nathannaveen <42319948+nathannaveen@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:21:26 -0500 Subject: [PATCH] Searching in Latest SBOM Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> --- pkg/guacrest/client/client.go | 34 ++++-- pkg/guacrest/client/models.go | 1 + pkg/guacrest/generated/models.go | 1 + pkg/guacrest/generated/server.go | 44 ++++--- pkg/guacrest/generated/spec.go | 55 ++++----- pkg/guacrest/helpers/getPackageInfo.go | 86 +++++++++++--- pkg/guacrest/helpers/sbom.go | 151 +++++++++++++++++++++++++ pkg/guacrest/openapi.yaml | 13 ++- pkg/guacrest/server/server.go | 65 ++++++++--- 9 files changed, 365 insertions(+), 85 deletions(-) create mode 100644 pkg/guacrest/helpers/sbom.go diff --git a/pkg/guacrest/client/client.go b/pkg/guacrest/client/client.go index a6e8c1d8212..87f544a3c79 100644 --- a/pkg/guacrest/client/client.go +++ b/pkg/guacrest/client/client.go @@ -98,7 +98,7 @@ type ClientInterface interface { RetrieveDependencies(ctx context.Context, params *RetrieveDependenciesParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetPackageInfo request - GetPackageInfo(ctx context.Context, purl string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*http.Response, error) + GetPackageInfo(ctx context.Context, purlOrArtifact string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) AnalyzeDependencies(ctx context.Context, params *AnalyzeDependenciesParams, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -137,8 +137,8 @@ func (c *Client) RetrieveDependencies(ctx context.Context, params *RetrieveDepen return c.Client.Do(req) } -func (c *Client) GetPackageInfo(ctx context.Context, purl string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetPackageInfoRequest(c.Server, purl, params) +func (c *Client) GetPackageInfo(ctx context.Context, purlOrArtifact string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetPackageInfoRequest(c.Server, purlOrArtifact, params) if err != nil { return nil, err } @@ -335,12 +335,12 @@ func NewRetrieveDependenciesRequest(server string, params *RetrieveDependenciesP } // NewGetPackageInfoRequest generates requests for GetPackageInfo -func NewGetPackageInfoRequest(server string, purl string, params *GetPackageInfoParams) (*http.Request, error) { +func NewGetPackageInfoRequest(server string, purlOrArtifact string, params *GetPackageInfoParams) (*http.Request, error) { var err error var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "purl", runtime.ParamLocationPath, purl) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "purlOrArtifact", runtime.ParamLocationPath, purlOrArtifact) if err != nil { return nil, err } @@ -350,7 +350,7 @@ func NewGetPackageInfoRequest(server string, purl string, params *GetPackageInfo return nil, err } - operationPath := fmt.Sprintf("/v1/purl/%s", pathParam0) + operationPath := fmt.Sprintf("/v1/package/%s", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -395,6 +395,22 @@ func NewGetPackageInfoRequest(server string, purl string, params *GetPackageInfo } + if params.LatestSbom != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "latestSbom", runtime.ParamLocationQuery, *params.LatestSbom); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() } @@ -459,7 +475,7 @@ type ClientWithResponsesInterface interface { RetrieveDependenciesWithResponse(ctx context.Context, params *RetrieveDependenciesParams, reqEditors ...RequestEditorFn) (*RetrieveDependenciesResponse, error) // GetPackageInfoWithResponse request - GetPackageInfoWithResponse(ctx context.Context, purl string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*GetPackageInfoResponse, error) + GetPackageInfoWithResponse(ctx context.Context, purlOrArtifact string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*GetPackageInfoResponse, error) } type AnalyzeDependenciesResponse struct { @@ -586,8 +602,8 @@ func (c *ClientWithResponses) RetrieveDependenciesWithResponse(ctx context.Conte } // GetPackageInfoWithResponse request returning *GetPackageInfoResponse -func (c *ClientWithResponses) GetPackageInfoWithResponse(ctx context.Context, purl string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*GetPackageInfoResponse, error) { - rsp, err := c.GetPackageInfo(ctx, purl, params, reqEditors...) +func (c *ClientWithResponses) GetPackageInfoWithResponse(ctx context.Context, purlOrArtifact string, params *GetPackageInfoParams, reqEditors ...RequestEditorFn) (*GetPackageInfoResponse, error) { + rsp, err := c.GetPackageInfo(ctx, purlOrArtifact, params, reqEditors...) if err != nil { return nil, err } diff --git a/pkg/guacrest/client/models.go b/pkg/guacrest/client/models.go index 041916df560..f2884214b7b 100644 --- a/pkg/guacrest/client/models.go +++ b/pkg/guacrest/client/models.go @@ -141,6 +141,7 @@ type RetrieveDependenciesParamsLinkCondition string type GetPackageInfoParams struct { Vulns *bool `form:"vulns,omitempty" json:"vulns,omitempty"` Dependencies *bool `form:"dependencies,omitempty" json:"dependencies,omitempty"` + LatestSbom *bool `form:"latestSbom,omitempty" json:"latestSbom,omitempty"` } // Getter for additional properties for PackageInfoResponse. Returns the specified diff --git a/pkg/guacrest/generated/models.go b/pkg/guacrest/generated/models.go index 9f8e3f240c6..1504fbde41e 100644 --- a/pkg/guacrest/generated/models.go +++ b/pkg/guacrest/generated/models.go @@ -141,6 +141,7 @@ type RetrieveDependenciesParamsLinkCondition string type GetPackageInfoParams struct { Vulns *bool `form:"vulns,omitempty" json:"vulns,omitempty"` Dependencies *bool `form:"dependencies,omitempty" json:"dependencies,omitempty"` + LatestSbom *bool `form:"latestSbom,omitempty" json:"latestSbom,omitempty"` } // Getter for additional properties for PackageInfoResponse. Returns the specified diff --git a/pkg/guacrest/generated/server.go b/pkg/guacrest/generated/server.go index 3745aaa498f..74d264cef57 100644 --- a/pkg/guacrest/generated/server.go +++ b/pkg/guacrest/generated/server.go @@ -25,9 +25,9 @@ type ServerInterface interface { // Retrieve transitive dependencies // (GET /query/dependencies) RetrieveDependencies(w http.ResponseWriter, r *http.Request, params RetrieveDependenciesParams) - // Get package information - // (GET /v1/purl/{purl}) - GetPackageInfo(w http.ResponseWriter, r *http.Request, purl string, params GetPackageInfoParams) + // Get package or artifact information + // (GET /v1/package/{purlOrArtifact}) + GetPackageInfo(w http.ResponseWriter, r *http.Request, purlOrArtifact string, params GetPackageInfoParams) } // Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. @@ -52,9 +52,9 @@ func (_ Unimplemented) RetrieveDependencies(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusNotImplemented) } -// Get package information -// (GET /v1/purl/{purl}) -func (_ Unimplemented) GetPackageInfo(w http.ResponseWriter, r *http.Request, purl string, params GetPackageInfoParams) { +// Get package or artifact information +// (GET /v1/package/{purlOrArtifact}) +func (_ Unimplemented) GetPackageInfo(w http.ResponseWriter, r *http.Request, purlOrArtifact string, params GetPackageInfoParams) { w.WriteHeader(http.StatusNotImplemented) } @@ -183,12 +183,12 @@ func (siw *ServerInterfaceWrapper) GetPackageInfo(w http.ResponseWriter, r *http var err error - // ------------- Path parameter "purl" ------------- - var purl string + // ------------- Path parameter "purlOrArtifact" ------------- + var purlOrArtifact string - err = runtime.BindStyledParameterWithOptions("simple", "purl", chi.URLParam(r, "purl"), &purl, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "purlOrArtifact", chi.URLParam(r, "purlOrArtifact"), &purlOrArtifact, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "purl", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "purlOrArtifact", Err: err}) return } @@ -211,8 +211,16 @@ func (siw *ServerInterfaceWrapper) GetPackageInfo(w http.ResponseWriter, r *http return } + // ------------- Optional query parameter "latestSbom" ------------- + + err = runtime.BindQueryParameter("form", true, false, "latestSbom", r.URL.Query(), ¶ms.LatestSbom) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "latestSbom", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetPackageInfo(w, r, purl, params) + siw.Handler.GetPackageInfo(w, r, purlOrArtifact, params) })) for _, middleware := range siw.HandlerMiddlewares { @@ -345,7 +353,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Get(options.BaseURL+"/query/dependencies", wrapper.RetrieveDependencies) }) r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/v1/purl/{purl}", wrapper.GetPackageInfo) + r.Get(options.BaseURL+"/v1/package/{purlOrArtifact}", wrapper.GetPackageInfo) }) return r @@ -481,8 +489,8 @@ func (response RetrieveDependencies502JSONResponse) VisitRetrieveDependenciesRes } type GetPackageInfoRequestObject struct { - Purl string `json:"purl"` - Params GetPackageInfoParams + PurlOrArtifact string `json:"purlOrArtifact"` + Params GetPackageInfoParams } type GetPackageInfoResponseObject interface { @@ -531,8 +539,8 @@ type StrictServerInterface interface { // Retrieve transitive dependencies // (GET /query/dependencies) RetrieveDependencies(ctx context.Context, request RetrieveDependenciesRequestObject) (RetrieveDependenciesResponseObject, error) - // Get package information - // (GET /v1/purl/{purl}) + // Get package or artifact information + // (GET /v1/package/{purlOrArtifact}) GetPackageInfo(ctx context.Context, request GetPackageInfoRequestObject) (GetPackageInfoResponseObject, error) } @@ -642,10 +650,10 @@ func (sh *strictHandler) RetrieveDependencies(w http.ResponseWriter, r *http.Req } // GetPackageInfo operation middleware -func (sh *strictHandler) GetPackageInfo(w http.ResponseWriter, r *http.Request, purl string, params GetPackageInfoParams) { +func (sh *strictHandler) GetPackageInfo(w http.ResponseWriter, r *http.Request, purlOrArtifact string, params GetPackageInfoParams) { var request GetPackageInfoRequestObject - request.Purl = purl + request.PurlOrArtifact = purlOrArtifact request.Params = params handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { diff --git a/pkg/guacrest/generated/spec.go b/pkg/guacrest/generated/spec.go index 6d83997d0f1..c281f4dea0e 100644 --- a/pkg/guacrest/generated/spec.go +++ b/pkg/guacrest/generated/spec.go @@ -18,33 +18,34 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "UnGr3vwrAAD//1kT8b3NFwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/guacrest/helpers/getPackageInfo.go b/pkg/guacrest/helpers/getPackageInfo.go index 63d2f84bdfb..02c1f4ba557 100644 --- a/pkg/guacrest/helpers/getPackageInfo.go +++ b/pkg/guacrest/helpers/getPackageInfo.go @@ -13,6 +13,7 @@ type QueryType struct { Vulns *bool Dependencies *bool Licenses *bool + LatestSBOM *bool } func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput *model.PkgInputSpec, shouldQuery QueryType) (*gen.PackageInfoResponseJSONResponse, error) { @@ -40,12 +41,14 @@ func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput * } var purls []string + var packageIds []string for _, pkg := range pkgs.Packages { for _, namespace := range pkg.Namespaces { for _, n := range namespace.Names { for _, v := range n.Versions { purls = append(purls, v.Purl) + packageIds = append(packageIds, v.Id) } } } @@ -53,9 +56,25 @@ func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput * response.Packages = purls + searchSoftware := false + + latestSbom := &model.AllHasSBOMTree{} + + // If the LatestSBOM query is specified then all other queries should be for the latest SBOM + if shouldQuery.LatestSBOM != nil && *shouldQuery.LatestSBOM { + if len(packageIds) > 1 { + return nil, fmt.Errorf("cant find latest SBOM when more than one package found for given purl") + } + latestSbom, err = LatestSBOMForAGivenId(ctx, gqlClient, packageIds[0]) + if err != nil { + return nil, err + } + searchSoftware = true + } + if shouldQuery.Vulns != nil && *shouldQuery.Vulns { logger.Infof("Searching for vulnerabilities in package %s", pkgInput.Name) - vulnerabilities, err := searchAttachedVulns(ctx, gqlClient, pkgSpec) + vulnerabilities, err := searchAttachedVulns(ctx, gqlClient, pkgSpec, searchSoftware, *latestSbom) if err != nil { return nil, err } @@ -66,7 +85,7 @@ func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput * var dependencies []gen.PackageInfo - deps, err := searchDependencies(ctx, gqlClient, pkgSpec) + deps, err := searchDependencies(ctx, gqlClient, pkgSpec, searchSoftware, *latestSbom) if err != nil { return nil, err } @@ -93,11 +112,11 @@ func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput * // // The function performs a breadth-first search starting from the given package, // collecting vulnerabilities for each package and its dependencies. -func searchAttachedVulns(ctx context.Context, gqlClient graphql.Client, pkgSpec model.PkgSpec) ([]gen.Vulnerability, error) { +func searchAttachedVulns(ctx context.Context, gqlClient graphql.Client, pkgSpec model.PkgSpec, searchSoftware bool, startSBOM model.AllHasSBOMTree) ([]gen.Vulnerability, error) { logger := logging.FromContext(ctx) var vulnerabilities []gen.Vulnerability - pkgs, err := searchDependencies(ctx, gqlClient, pkgSpec) + pkgs, err := searchDependencies(ctx, gqlClient, pkgSpec, searchSoftware, startSBOM) if err != nil { return nil, fmt.Errorf("error searching dependencies: %w", err) } @@ -151,7 +170,9 @@ func searchAttachedVulns(ctx context.Context, gqlClient graphql.Client, pkgSpec return vulnerabilities, nil } -func searchDependencies(ctx context.Context, gqlClient graphql.Client, pkgSpec model.PkgSpec) (map[string]string, error) { +func searchDependencies(ctx context.Context, gqlClient graphql.Client, pkgSpec model.PkgSpec, searchSoftware bool, startSBOM model.AllHasSBOMTree) (map[string]string, error) { + logger := logging.FromContext(ctx) + dependencies := make(map[string]string) pkgs, err := model.Packages(ctx, gqlClient, pkgSpec) @@ -163,6 +184,8 @@ func searchDependencies(ctx context.Context, gqlClient graphql.Client, pkgSpec m dependencies[pkg.Namespaces[0].Names[0].Versions[0].Id] = pkg.Namespaces[0].Names[0].Versions[0].Purl } + doneFirst := false + queue := []model.PkgSpec{pkgSpec} for len(queue) > 0 { @@ -175,21 +198,56 @@ func searchDependencies(ctx context.Context, gqlClient graphql.Client, pkgSpec m }, }) - //isDeps, err := model.Dependencies(ctx, gqlClient, model.IsDependencySpec{ - // DependencyPackage: &pop, - //}) + if !doneFirst && searchSoftware { + hasSboms = &model.HasSBOMsResponse{ + HasSBOM: []model.HasSBOMsHasSBOM{ + { + startSBOM, + }, + }, + } + } + + doneFirst = true + if err != nil { return nil, fmt.Errorf("error fetching hasSboms from package spec %+v: %w", pop, err) } - for _, hasSbom := range hasSboms.HasSBOM { - for _, dep := range hasSbom.IncludedDependencies { - dependencies[dep.Package.Namespaces[0].Names[0].Versions[0].Id] = dep.Package.Namespaces[0].Names[0].Versions[0].Purl - queue = append(queue, model.PkgSpec{ - Id: &dep.Package.Namespaces[0].Names[0].Versions[0].Id, - }) + if !searchSoftware { + for _, hasSbom := range hasSboms.HasSBOM { + for _, dep := range hasSbom.IncludedDependencies { + dependencies[dep.Package.Namespaces[0].Names[0].Versions[0].Id] = dep.Package.Namespaces[0].Names[0].Versions[0].Purl + queue = append(queue, model.PkgSpec{ + Id: &dep.Package.Namespaces[0].Names[0].Versions[0].Id, + }) + } + } + } else { + for _, hasSbom := range hasSboms.HasSBOM { + for _, software := range hasSbom.IncludedSoftware { + switch s := software.(type) { + case *model.AllHasSBOMTreeIncludedSoftwarePackage: + dependencies[s.Namespaces[0].Names[0].Versions[0].Id] = s.Namespaces[0].Names[0].Versions[0].Purl + queue = append(queue, model.PkgSpec{ + Id: &s.Namespaces[0].Names[0].Versions[0].Id, + }) + case *model.AllHasSBOMTreeIncludedSoftwareArtifact: + // convert artifact to pkg, then use the pkg id to get the vulnerability + pkg, err := getPkgFromArtifact(gqlClient, s.Id) + if err != nil { + return nil, fmt.Errorf("failed to get package attached to artifact %s: %w", s.Id, err) + } + dependencies[pkg.Namespaces[0].Names[0].Versions[0].Id] = pkg.Namespaces[0].Names[0].Versions[0].Purl + queue = append(queue, model.PkgSpec{ + Id: &pkg.Namespaces[0].Names[0].Versions[0].Id, + }) + } + } } } + + logger.Infof("Dependencies: %+v", dependencies) } return dependencies, nil diff --git a/pkg/guacrest/helpers/sbom.go b/pkg/guacrest/helpers/sbom.go new file mode 100644 index 00000000000..38279406ec7 --- /dev/null +++ b/pkg/guacrest/helpers/sbom.go @@ -0,0 +1,151 @@ +// +// Copyright 2024 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 helpers + +import ( + "context" + "fmt" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" + "strings" + + "github.com/Khan/genqlient/graphql" + "github.com/Masterminds/semver" + "github.com/guacsec/guac/pkg/logging" +) + +func LatestSBOMForAGivenId(ctx context.Context, client graphql.Client, id string) (*model.AllHasSBOMTree, error) { + logger := logging.FromContext(ctx) + + // Define the spec to filter SBOMs by the package version level ID + spec := model.HasSBOMSpec{ + Subject: &model.PackageOrArtifactSpec{ + Package: &model.PkgSpec{ + Id: &id, + }, + }, + } + + // Query for SBOMs as a package + sboms, err := model.HasSBOMs(ctx, client, spec) + if err != nil { + logger.Errorw("Failed to query SBOMs for package", "id", id, "error", err) + return nil, err + } + + // If no SBOMs found, try querying as an artifact + if len(sboms.HasSBOM) == 0 { + spec.Subject = &model.PackageOrArtifactSpec{ + Artifact: &model.ArtifactSpec{ + Id: &id, + }, + } + sboms, err = model.HasSBOMs(ctx, client, spec) + if err != nil { + logger.Errorw("Failed to query SBOMs for artifact", "id", id, "error", err) + return nil, err + } + } + + if len(sboms.HasSBOM) == 0 { + logger.Errorf("Failed to find any SBOMs with id: %v", id) + return nil, fmt.Errorf("error getting sboms, no sboms with id %v found", id) + } + + // Find the latest SBOM + latestSBOM := sboms.HasSBOM[0] + for _, sbom := range sboms.HasSBOM[1:] { + if compare(&sbom.AllHasSBOMTree, &latestSBOM.AllHasSBOMTree, client) { + latestSBOM = sbom + } + } + + return &latestSBOM.AllHasSBOMTree, nil +} + +func compare(a *model.AllHasSBOMTree, b *model.AllHasSBOMTree, gqlClient graphql.Client) bool { + logger := logging.FromContext(context.Background()) + + aVersion, err := findSubjectBasedOnType(a, gqlClient) + if err != nil { + return false + } + + bVersion, err := findSubjectBasedOnType(b, gqlClient) + if err != nil { + return false + } + + if (aVersion == "" && bVersion != "") || (aVersion != "" && bVersion == "") { + return aVersion != "" + } + + if strings.HasPrefix(aVersion, "sha256") || aVersion == "" || + strings.HasPrefix(bVersion, "sha256") || bVersion == "" || aVersion == bVersion { + return a.KnownSince.After(b.KnownSince) + } + + parsedAVersion, err := semver.NewVersion(aVersion) + if err != nil { + logger.Warnw("Could not parse version, fallback to time", "version", aVersion, "error", err) + return a.KnownSince.After(b.KnownSince) + } + parsedBVersion, err := semver.NewVersion(bVersion) + if err != nil { + logger.Warnw("Could not parse version, fallback to time", "version", bVersion, "error", err) + return a.KnownSince.After(b.KnownSince) + } + + return parsedAVersion.Compare(parsedBVersion) > 0 +} + +func findSubjectBasedOnType(a *model.AllHasSBOMTree, gqlClient graphql.Client) (string, error) { + var version string + switch subject := a.Subject.(type) { + case *model.AllHasSBOMTreeSubjectArtifact: + // Get the package attached to the artifact via an isOccurrence node + pkg, err := getPkgFromArtifact(gqlClient, subject.Id) + if err != nil { + return "", fmt.Errorf("could not find package for subject: %s, with err: %v", subject.Id, err) + } + version = pkg.Namespaces[0].Names[0].Versions[0].Version + case *model.AllHasSBOMTreeSubjectPackage: + version = subject.Namespaces[0].Names[0].Versions[0].Version + default: + return "", fmt.Errorf("Unknown subject type") + } + return version, nil +} + +func getPkgFromArtifact(gqlClient graphql.Client, id string) (*model.AllPkgTree, error) { + rsp, err := model.Occurrences(context.Background(), gqlClient, model.IsOccurrenceSpec{ + Artifact: &model.ArtifactSpec{ + Id: &id, + }, + }) + if err != nil { + return nil, fmt.Errorf("error getting occurrences from artifact %s: %v", id, err) + } + for i := range rsp.GetIsOccurrence() { + if *rsp.GetIsOccurrence()[i].GetSubject().GetTypename() == "Package" { + p, ok := rsp.GetIsOccurrence()[i].GetSubject().(*model.AllIsOccurrencesTreeSubjectPackage) + if !ok { + return nil, fmt.Errorf("could not convert package %s to type *model.AllIsOccurrencesTreeSubjectPackage", id) + } + return &p.AllPkgTree, nil + } + } + return nil, nil +} diff --git a/pkg/guacrest/openapi.yaml b/pkg/guacrest/openapi.yaml index 42cafc50e37..eed6a6049f1 100644 --- a/pkg/guacrest/openapi.yaml +++ b/pkg/guacrest/openapi.yaml @@ -99,15 +99,15 @@ paths: $ref: "#/components/responses/InternalServerError" "502": $ref: "#/components/responses/BadGateway" - "/v1/purl/{purl}": + "/v1/package/{purlOrArtifact}": get: - summary: Get package information + summary: Get package or artifact information operationId: getPackageInfo parameters: - - name: purl + - name: purlOrArtifact in: path required: true - description: URL-encoded Package URL (PURL) + description: URL-encoded Package URL (PURL) or artifact identifier schema: type: string - name: vulns @@ -120,6 +120,11 @@ paths: required: false schema: type: boolean + - name: latestSbom + in: query + required: false + schema: + type: boolean responses: "200": $ref: "#/components/responses/PackageInfoResponse" diff --git a/pkg/guacrest/server/server.go b/pkg/guacrest/server/server.go index 0122d8d2f5f..40b2d31707d 100644 --- a/pkg/guacrest/server/server.go +++ b/pkg/guacrest/server/server.go @@ -18,6 +18,7 @@ package server import ( "context" "fmt" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" helpers2 "github.com/guacsec/guac/pkg/assembler/helpers" "github.com/guacsec/guac/pkg/guacrest/helpers" "net/http" @@ -105,33 +106,71 @@ func (s *DefaultServer) AnalyzeDependencies(ctx context.Context, request gen.Ana } func (s *DefaultServer) GetPackageInfo(ctx context.Context, request gen.GetPackageInfoRequestObject) (gen.GetPackageInfoResponseObject, error) { - decodedPurl, err := url.QueryUnescape(request.Purl) + logger := logging.FromContext(ctx) + + decodedPurlOrArtifact, err := url.QueryUnescape(request.PurlOrArtifact) if err != nil { + logger.Infof("error decoding purl or artifact: %v", err) return gen.GetPackageInfo400JSONResponse{ BadRequestJSONResponse: gen.BadRequestJSONResponse{ - Message: fmt.Sprintf("Invalid PURL: %v", err), + Message: fmt.Sprintf("Invalid identifier: %v", err), }, }, nil } - // Add the "pkg:" prefix if not present - if !strings.HasPrefix(decodedPurl, "pkg:") { - decodedPurl = "pkg:" + decodedPurl - } + // Determine if the identifier is a PURL or an artifact + var pkgInput *model.PkgInputSpec - pkgInput, err := helpers2.PurlToPkg(decodedPurl) - if err != nil { - return gen.GetPackageInfo400JSONResponse{ - BadRequestJSONResponse: gen.BadRequestJSONResponse{ - Message: fmt.Sprintf("Failed to parse PURL: %v", err), + if strings.HasPrefix(decodedPurlOrArtifact, "pkg:") { + pkgInput, err = helpers2.PurlToPkg(decodedPurlOrArtifact) + if err != nil { + return gen.GetPackageInfo400JSONResponse{ + BadRequestJSONResponse: gen.BadRequestJSONResponse{ + Message: fmt.Sprintf("Failed to parse PURL: %v", err), + }, + }, nil + } + } else { + splitString := strings.Split(decodedPurlOrArtifact, ":") + + if len(splitString) != 2 { + return gen.GetPackageInfo400JSONResponse{ + BadRequestJSONResponse: gen.BadRequestJSONResponse{ + Message: fmt.Sprintf("Invalid identifier, artifact should be comprised of :: %v", decodedPurlOrArtifact), + }, + }, nil + } + + occurrence, err := model.Occurrences(ctx, s.gqlClient, model.IsOccurrenceSpec{ + Artifact: &model.ArtifactSpec{ + Algorithm: &splitString[0], + Digest: &splitString[1], }, - }, nil + }) + if err != nil { + return gen.GetPackageInfo400JSONResponse{ + BadRequestJSONResponse: gen.BadRequestJSONResponse{ + Message: fmt.Sprintf("Invalid identifier, artifact should be comprised of :: %v", decodedPurlOrArtifact), + }, + }, nil + } + + pkg := occurrence.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage).AllPkgTree + + pkgInput = &model.PkgInputSpec{ + Type: pkg.Type, + Namespace: &pkg.Namespaces[0].Namespace, + Name: pkg.Namespaces[0].Names[0].Name, + Version: &pkg.Namespaces[0].Names[0].Versions[0].Version, + Subpath: &pkg.Namespaces[0].Names[0].Versions[0].Subpath, + } } - // whatToSearch states what type of query we want to run for the given package + // whatToSearch states what type of query we want to run for the given package or artifact whatToSearch := helpers.QueryType{ Vulns: request.Params.Vulns, Dependencies: request.Params.Dependencies, + LatestSBOM: request.Params.LatestSbom, } packageResponse, err := helpers.GetInfoForPackage(ctx, s.gqlClient, pkgInput, whatToSearch)