From fa57306972f6fb10ec23e5fc9233df0f9bf59a2a Mon Sep 17 00:00:00 2001 From: nathannaveen <42319948+nathannaveen@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:21:28 -0500 Subject: [PATCH] Updated Design and Included Tests Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> --- pkg/guacrest/client/client.go | 36 +--- pkg/guacrest/client/models.go | 15 +- pkg/guacrest/generated/models.go | 15 +- pkg/guacrest/generated/server.go | 22 +-- pkg/guacrest/generated/spec.go | 57 +++--- pkg/guacrest/helpers/getPackageInfo.go | 26 ++- pkg/guacrest/helpers/getPackageInfo_test.go | 165 +++++++++++++++++ pkg/guacrest/helpers/sbom.go | 71 ++++---- pkg/guacrest/helpers/sbom_test.go | 189 ++++++++++++++++++++ pkg/guacrest/openapi.yaml | 23 ++- pkg/guacrest/server/server.go | 34 +++- 11 files changed, 499 insertions(+), 154 deletions(-) create mode 100644 pkg/guacrest/helpers/getPackageInfo_test.go create mode 100644 pkg/guacrest/helpers/sbom_test.go diff --git a/pkg/guacrest/client/client.go b/pkg/guacrest/client/client.go index 87f544a3c79..bc08e083744 100644 --- a/pkg/guacrest/client/client.go +++ b/pkg/guacrest/client/client.go @@ -363,41 +363,9 @@ func NewGetPackageInfoRequest(server string, purlOrArtifact string, params *GetP if params != nil { queryValues := queryURL.Query() - if params.Vulns != nil { + if params.Query != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "vulns", runtime.ParamLocationQuery, *params.Vulns); 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) - } - } - } - - } - - if params.Dependencies != nil { - - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "dependencies", runtime.ParamLocationQuery, *params.Dependencies); 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) - } - } - } - - } - - if params.LatestSbom != nil { - - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "latestSbom", runtime.ParamLocationQuery, *params.LatestSbom); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", false, "query", runtime.ParamLocationQuery, *params.Query); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err diff --git a/pkg/guacrest/client/models.go b/pkg/guacrest/client/models.go index f2884214b7b..b18d308d55f 100644 --- a/pkg/guacrest/client/models.go +++ b/pkg/guacrest/client/models.go @@ -9,6 +9,13 @@ import ( "time" ) +// Defines values for QueryType. +const ( + Dependencies QueryType = "dependencies" + LatestSbom QueryType = "latestSbom" + Vulns QueryType = "vulns" +) + // Defines values for AnalyzeDependenciesParamsSort. const ( Frequency AnalyzeDependenciesParamsSort = "frequency" @@ -38,6 +45,9 @@ type PaginationInfo struct { // Purl defines model for Purl. type Purl = string +// QueryType defines model for QueryType. +type QueryType string + // ScanMetadata defines model for ScanMetadata. type ScanMetadata struct { Collector *string `json:"collector,omitempty"` @@ -139,9 +149,8 @@ type RetrieveDependenciesParamsLinkCondition string // GetPackageInfoParams defines parameters for GetPackageInfo. 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"` + // Query Comma-separated list of additional information to include in the response + Query *[]QueryType `form:"query,omitempty" json:"query,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 1504fbde41e..dbb5cd10378 100644 --- a/pkg/guacrest/generated/models.go +++ b/pkg/guacrest/generated/models.go @@ -9,6 +9,13 @@ import ( "time" ) +// Defines values for QueryType. +const ( + Dependencies QueryType = "dependencies" + LatestSbom QueryType = "latestSbom" + Vulns QueryType = "vulns" +) + // Defines values for AnalyzeDependenciesParamsSort. const ( Frequency AnalyzeDependenciesParamsSort = "frequency" @@ -38,6 +45,9 @@ type PaginationInfo struct { // Purl defines model for Purl. type Purl = string +// QueryType defines model for QueryType. +type QueryType string + // ScanMetadata defines model for ScanMetadata. type ScanMetadata struct { Collector *string `json:"collector,omitempty"` @@ -139,9 +149,8 @@ type RetrieveDependenciesParamsLinkCondition string // GetPackageInfoParams defines parameters for GetPackageInfo. 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"` + // Query Comma-separated list of additional information to include in the response + Query *[]QueryType `form:"query,omitempty" json:"query,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 74d264cef57..ff5f84328a7 100644 --- a/pkg/guacrest/generated/server.go +++ b/pkg/guacrest/generated/server.go @@ -195,27 +195,11 @@ func (siw *ServerInterfaceWrapper) GetPackageInfo(w http.ResponseWriter, r *http // Parameter object where we will unmarshal all parameters from the context var params GetPackageInfoParams - // ------------- Optional query parameter "vulns" ------------- + // ------------- Optional query parameter "query" ------------- - err = runtime.BindQueryParameter("form", true, false, "vulns", r.URL.Query(), ¶ms.Vulns) + err = runtime.BindQueryParameter("form", false, false, "query", r.URL.Query(), ¶ms.Query) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "vulns", Err: err}) - return - } - - // ------------- Optional query parameter "dependencies" ------------- - - err = runtime.BindQueryParameter("form", true, false, "dependencies", r.URL.Query(), ¶ms.Dependencies) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "dependencies", Err: err}) - 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}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "query", Err: err}) return } diff --git a/pkg/guacrest/generated/spec.go b/pkg/guacrest/generated/spec.go index c281f4dea0e..c6e7e498996 100644 --- a/pkg/guacrest/generated/spec.go +++ b/pkg/guacrest/generated/spec.go @@ -18,34 +18,35 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "UnGr3vwrAAD//1kT8b3NFwAA", + "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", } // 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 02c1f4ba557..1f2a488efde 100644 --- a/pkg/guacrest/helpers/getPackageInfo.go +++ b/pkg/guacrest/helpers/getPackageInfo.go @@ -10,10 +10,9 @@ import ( ) type QueryType struct { - Vulns *bool - Dependencies *bool - Licenses *bool - LatestSBOM *bool + Vulns bool + Dependencies bool + LatestSBOM bool } func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput *model.PkgInputSpec, shouldQuery QueryType) (*gen.PackageInfoResponseJSONResponse, error) { @@ -25,13 +24,13 @@ func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput * Name: &pkgInput.Name, } - if *pkgInput.Namespace != "" { + if pkgInput.Namespace != nil && *pkgInput.Namespace != "" { pkgSpec.Namespace = pkgInput.Namespace } - if *pkgInput.Version != "" { + if pkgInput.Version != nil && *pkgInput.Version != "" { pkgSpec.Version = pkgInput.Version } - if *pkgInput.Subpath != "" { + if pkgInput.Subpath != nil && *pkgInput.Subpath != "" { pkgSpec.Subpath = pkgInput.Subpath } @@ -61,18 +60,15 @@ func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput * 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 shouldQuery.LatestSBOM { + latestSbom, err = LatestSBOMFromID(ctx, gqlClient, packageIds) if err != nil { return nil, err } searchSoftware = true } - if shouldQuery.Vulns != nil && *shouldQuery.Vulns { + if shouldQuery.Vulns { logger.Infof("Searching for vulnerabilities in package %s", pkgInput.Name) vulnerabilities, err := searchAttachedVulns(ctx, gqlClient, pkgSpec, searchSoftware, *latestSbom) if err != nil { @@ -80,7 +76,7 @@ func GetInfoForPackage(ctx context.Context, gqlClient graphql.Client, pkgInput * } response.Vulnerabilities = &vulnerabilities } - if shouldQuery.Dependencies != nil && *shouldQuery.Dependencies { + if shouldQuery.Dependencies { logger.Infof("Searching for dependencies in package %s", pkgInput.Name) var dependencies []gen.PackageInfo @@ -202,7 +198,7 @@ func searchDependencies(ctx context.Context, gqlClient graphql.Client, pkgSpec m hasSboms = &model.HasSBOMsResponse{ HasSBOM: []model.HasSBOMsHasSBOM{ { - startSBOM, + AllHasSBOMTree: startSBOM, }, }, } diff --git a/pkg/guacrest/helpers/getPackageInfo_test.go b/pkg/guacrest/helpers/getPackageInfo_test.go new file mode 100644 index 00000000000..76e96934949 --- /dev/null +++ b/pkg/guacrest/helpers/getPackageInfo_test.go @@ -0,0 +1,165 @@ +// +// 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. + +//go:build integration + +package helpers + +import ( + "context" + "log" + "sort" + "testing" + "time" + + clients "github.com/guacsec/guac/internal/testing/graphqlClients" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" + + "github.com/google/go-cmp/cmp" +) + +func TestGetInfoForPackage_Integration(t *testing.T) { + ctx := context.Background() + gqlClient := clients.SetupTest(t) + + ns := "test-namespace-1" + version := "v1.0.0" + + // Prepare the package input specification + pkgInput := model.IDorPkgInput{ + PackageInput: &model.PkgInputSpec{ + Type: "golang", + Namespace: &ns, + Name: "test-name-1", + Version: &version, + }, + } + + // Ingest the package with our exact input specification + _, err := model.IngestPackage(ctx, gqlClient, pkgInput) + if err != nil { + log.Fatalf("Failed to ingest package: %v", err) + } + + ns2 := "test-namespace-2" + version2 := "v2.0.0" + + // Prepare the package input specification + pkgInput2 := model.IDorPkgInput{ + PackageInput: &model.PkgInputSpec{ + Type: "golang", + Namespace: &ns2, + Name: "test-name-2", + Version: &version2, + }, + } + + // Ingest the package with our exact input specification + pkg2, err := model.IngestPackage(ctx, gqlClient, pkgInput2) + if err != nil { + log.Fatalf("Failed to ingest package: %v", err) + } + + depId, err := model.IngestIsDependency(ctx, gqlClient, pkgInput2, pkgInput, model.IsDependencyInputSpec{ + DependencyType: model.DependencyTypeDirect, + }) + if err != nil { + log.Fatalf("Failed to ingest dependency: %v", err) + } + + hasSBOMInput := model.HasSBOMInputSpec{ + KnownSince: time.Now(), + } + + x := model.HasSBOMIncludesInputSpec{ + Packages: []string{pkg2.IngestPackage.PackageVersionID}, + Artifacts: []string{}, + Dependencies: []string{depId.IngestDependency}, + Occurrences: []string{}, + } + + _, err = model.IngestHasSBOMPkg(ctx, gqlClient, pkgInput, hasSBOMInput, x) + if err != nil { + log.Fatalf("Failed to ingest HasSBOM: %v", err) + } + + hasSBOMInput2 := model.HasSBOMInputSpec{ + KnownSince: time.Now(), + } + + x2 := model.HasSBOMIncludesInputSpec{ + Packages: []string{}, + Artifacts: []string{}, + Dependencies: []string{}, + Occurrences: []string{}, + } + + _, err = model.IngestHasSBOMPkg(ctx, gqlClient, pkgInput2, hasSBOMInput2, x2) + if err != nil { + log.Fatalf("Failed to ingest HasSBOM: %v", err) + } + + // Prepare the vulnerability input specification + vulnInput := model.IDorVulnerabilityInput{ + VulnerabilityInput: &model.VulnerabilityInputSpec{ + Type: "CVE", + VulnerabilityID: "CVE-2023-1234", + }, + } + + // Ingest the vulnerability + _, err = model.IngestVulnerability(ctx, gqlClient, vulnInput) + if err != nil { + log.Fatalf("Failed to ingest vulnerability: %v", err) + } + + // Prepare the scan metadata input specification + scanMetadataInput := model.ScanMetadataInput{ + TimeScanned: time.Now(), + DbUri: "http://example.com/db", + DbVersion: "2023.01.01", + ScannerUri: "http://example.com/scanner", + ScannerVersion: "v1.0.0", + Origin: "example-origin", + Collector: "example-collector", + DocumentRef: "example-document-ref", + } + + // Link the vulnerability to the package with certification + _, err = model.IngestCertifyVulnPkg(ctx, gqlClient, pkgInput2, vulnInput, scanMetadataInput) + if err != nil { + log.Fatalf("Failed to link vulnerability to package: %v", err) + } + + resp, err := GetInfoForPackage(ctx, gqlClient, pkgInput.PackageInput, QueryType{ + Vulns: true, + Dependencies: true, + }) + if err != nil { + t.Fatalf("Failed to get info for package: %v", err) + } + + if diff := cmp.Diff("cve-2023-1234", (*resp.Vulnerabilities)[0].Vulnerability.VulnerabilityIDs[0]); diff != "" { + t.Errorf("Vulnerability ID mismatch (-want +got):\n%s", diff) + } + + sort.Slice(*resp.Dependencies, func(i, j int) bool { + return (*resp.Dependencies)[i] < (*resp.Dependencies)[j] + }) + + if diff := cmp.Diff("pkg:golang/test-namespace-1/test-name-1@v1.0.0", (*resp.Dependencies)[0]); diff != "" { + t.Errorf("Dependency mismatch (-want +got):\n%s", diff) + } +} diff --git a/pkg/guacrest/helpers/sbom.go b/pkg/guacrest/helpers/sbom.go index 38279406ec7..bdbd3656be4 100644 --- a/pkg/guacrest/helpers/sbom.go +++ b/pkg/guacrest/helpers/sbom.go @@ -26,49 +26,52 @@ import ( "github.com/guacsec/guac/pkg/logging" ) -func LatestSBOMForAGivenId(ctx context.Context, client graphql.Client, id string) (*model.AllHasSBOMTree, error) { +func LatestSBOMFromID(ctx context.Context, client graphql.Client, IDs []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 - } + latestSBOM := model.HasSBOMsHasSBOM{} - // If no SBOMs found, try querying as an artifact - if len(sboms.HasSBOM) == 0 { - spec.Subject = &model.PackageOrArtifactSpec{ - Artifact: &model.ArtifactSpec{ - Id: &id, + for _, ID := range IDs { + // Define the spec to filter SBOMs by the package version level ID + spec := model.HasSBOMSpec{ + Subject: &model.PackageOrArtifactSpec{ + Package: &model.PkgSpec{ + Id: &ID, + }, }, } - sboms, err = model.HasSBOMs(ctx, client, spec) + + // Query for SBOMs as a package + sboms, err := model.HasSBOMs(ctx, client, spec) if err != nil { - logger.Errorw("Failed to query SBOMs for artifact", "id", id, "error", err) + logger.Errorw("Failed to query SBOMs for package", "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) - } + // 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 + } + } - // Find the latest SBOM - latestSBOM := sboms.HasSBOM[0] - for _, sbom := range sboms.HasSBOM[1:] { - if compare(&sbom.AllHasSBOMTree, &latestSBOM.AllHasSBOMTree, client) { - latestSBOM = sbom + 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 + for _, sbom := range sboms.HasSBOM { + if latestSBOM.Id == "" || compare(&sbom.AllHasSBOMTree, &latestSBOM.AllHasSBOMTree, client) { + latestSBOM = sbom + } } } @@ -85,7 +88,7 @@ func compare(a *model.AllHasSBOMTree, b *model.AllHasSBOMTree, gqlClient graphql bVersion, err := findSubjectBasedOnType(b, gqlClient) if err != nil { - return false + return true } if (aVersion == "" && bVersion != "") || (aVersion != "" && bVersion == "") { diff --git a/pkg/guacrest/helpers/sbom_test.go b/pkg/guacrest/helpers/sbom_test.go new file mode 100644 index 00000000000..7cd9b8247c0 --- /dev/null +++ b/pkg/guacrest/helpers/sbom_test.go @@ -0,0 +1,189 @@ +// +// 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. + +//go:build integration + +package helpers + +import ( + "context" + "testing" + "time" + + clients "github.com/guacsec/guac/internal/testing/graphqlClients" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" + + "github.com/stretchr/testify/assert" +) + +type compareSbomTestData struct { + pkgVersion string + knownSince time.Time +} + +func TestLatestSBOMFromID_Integration(t *testing.T) { + startTime := time.Now() + + tests := []struct { + name string + pkgType string + pkgNamespace string + pkgName string + compareData []compareSbomTestData + knownSince []time.Time + expectedIndex int // Index of the expected latest SBOM + }{ + { + name: "Latest version SBOM", + pkgType: "test-type", + pkgNamespace: "", + pkgName: "test-package", + compareData: []compareSbomTestData{ + { + pkgVersion: "v1.0.0", + knownSince: startTime, + }, + { + pkgVersion: "v1.1.0", + knownSince: startTime, + }, + { + pkgVersion: "v1.2.0", + knownSince: startTime, + }, + }, + expectedIndex: 2, + }, + { + name: "Latest SBOM by time (same version)", + pkgType: "test-type", + pkgNamespace: "", + pkgName: "test-name-same-versions", + compareData: []compareSbomTestData{ + { + pkgVersion: "1.0.0", + knownSince: startTime, + }, + { + pkgVersion: "1.0.0", + knownSince: startTime.Add(time.Hour), + }, + { + pkgVersion: "1.0.0", + knownSince: startTime.Add(2 * time.Hour), + }, + }, + expectedIndex: 2, + }, + { + name: "Latest SBOM by time (empty versions)", + pkgType: "test-type", + pkgNamespace: "test-namespace", + pkgName: "test-name-empty-version", + compareData: []compareSbomTestData{ + { + pkgVersion: "", + knownSince: startTime, + }, + { + pkgVersion: "", + knownSince: startTime.Add(time.Hour), + }, + { + pkgVersion: "", + knownSince: startTime.Add(2 * time.Hour), + }, + }, + expectedIndex: 2, + }, + { + name: "Latest SBOM by time (sha256 versions)", + pkgType: "test-type", + pkgNamespace: "test-namespace", + pkgName: "test-name-sha256", + compareData: []compareSbomTestData{ + { + pkgVersion: "sha256:123", + knownSince: startTime, + }, + { + pkgVersion: "sha256:789", + knownSince: startTime.Add(2 * time.Hour), + }, + { + pkgVersion: "sha256:456", + knownSince: startTime.Add(time.Hour), + }, + }, + expectedIndex: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + gqlClient := clients.SetupTest(t) + + sbomIDMap := make(map[string]int) + var pkgIds []string + + // Ingest packages and SBOMs + for i, sbomTestData := range tt.compareData { + pkgInput := model.PkgInputSpec{ + Type: tt.pkgType, + Namespace: &tt.pkgNamespace, + Name: tt.pkgName, + Version: &sbomTestData.pkgVersion, + } + + pkg, err := model.IngestPackage(ctx, gqlClient, model.IDorPkgInput{PackageInput: &pkgInput}) + assert.NoError(t, err) + pkgIds = append(pkgIds, pkg.IngestPackage.PackageVersionID) + + sbomInput := model.HasSBOMInputSpec{ + KnownSince: sbomTestData.knownSince, + } + + sbomResult, err := model.IngestHasSBOMPkg(ctx, gqlClient, model.IDorPkgInput{PackageVersionID: &pkg.IngestPackage.PackageVersionID}, sbomInput, model.HasSBOMIncludesInputSpec{ + Packages: []string{}, + Artifacts: []string{}, + Occurrences: []string{}, + Dependencies: []string{}, + }) + assert.NoError(t, err) + + // Store the ID of the ingested SBOM with its index in the array + sbomIDMap[sbomResult.IngestHasSBOM] = i + } + + // Retrieve the latest SBOM + latestPkg, err := LatestSBOMFromID(ctx, gqlClient, pkgIds) + assert.NoError(t, err) + assert.NotNil(t, latestPkg) + + // Check if the retrieved SBOM is the expected one by comparing the index + actualIndex, exists := sbomIDMap[latestPkg.Id] + assert.True(t, exists, "The returned SBOM ID does not exist in the map") + assert.Equal(t, tt.expectedIndex, actualIndex, "The index of the latest SBOM does not match the expected index") + + // Additional checks to ensure the content is correct + pkgSubject, ok := latestPkg.Subject.(*model.AllHasSBOMTreeSubjectPackage) + if !ok { + t.Fatalf("Unexpected subject type: %T", latestPkg.Subject) + } + assert.Equal(t, tt.compareData[tt.expectedIndex].pkgVersion, pkgSubject.Namespaces[0].Names[0].Versions[0].Version) + }) + } +} diff --git a/pkg/guacrest/openapi.yaml b/pkg/guacrest/openapi.yaml index eed6a6049f1..d9cf8cce376 100644 --- a/pkg/guacrest/openapi.yaml +++ b/pkg/guacrest/openapi.yaml @@ -110,21 +110,16 @@ paths: description: URL-encoded Package URL (PURL) or artifact identifier schema: type: string - - name: vulns + - name: query in: query required: false + description: Comma-separated list of additional information to include in the response schema: - type: boolean - - name: dependencies - in: query - required: false - schema: - type: boolean - - name: latestSbom - in: query - required: false - schema: - type: boolean + type: array + items: + $ref: '#/components/schemas/QueryType' + style: form + explode: false responses: "200": $ref: "#/components/responses/PackageInfoResponse" @@ -226,6 +221,10 @@ components: type: string collector: type: string + QueryType: + type: string + enum: [ vulns, dependencies, latestSbom ] + x-enum-varnames: [ Vulns, Dependencies, LatestSbom ] responses: # for code 200 PurlList: diff --git a/pkg/guacrest/server/server.go b/pkg/guacrest/server/server.go index 40b2d31707d..7b6d59429bc 100644 --- a/pkg/guacrest/server/server.go +++ b/pkg/guacrest/server/server.go @@ -155,7 +155,16 @@ func (s *DefaultServer) GetPackageInfo(ctx context.Context, request gen.GetPacka }, nil } - pkg := occurrence.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage).AllPkgTree + convertedOccurrence, ok := occurrence.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) + if !ok { + return gen.GetPackageInfo500JSONResponse{ + InternalServerErrorJSONResponse: gen.InternalServerErrorJSONResponse{ + Message: fmt.Sprintf("Error converting issoccurrence to model.IsOccurrencesTreeSubjectPackage: %v", decodedPurlOrArtifact), + }, + }, nil + } + + pkg := convertedOccurrence.AllPkgTree pkgInput = &model.PkgInputSpec{ Type: pkg.Type, @@ -167,15 +176,28 @@ func (s *DefaultServer) GetPackageInfo(ctx context.Context, request gen.GetPacka } // 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, + var whatToSearch helpers.QueryType + + if request.Params.Query != nil { + for _, queryParam := range *request.Params.Query { + switch queryParam { + case gen.Dependencies: + whatToSearch.Dependencies = true + case gen.Vulns: + whatToSearch.Vulns = true + case gen.LatestSbom: + whatToSearch.LatestSBOM = true + } + } } packageResponse, err := helpers.GetInfoForPackage(ctx, s.gqlClient, pkgInput, whatToSearch) if err != nil { - return nil, err + return gen.GetPackageInfo400JSONResponse{ + BadRequestJSONResponse: gen.BadRequestJSONResponse{ + Message: fmt.Sprintf("error getting package info: %v", err), + }, + }, nil } response := gen.GetPackageInfo200JSONResponse{