diff --git a/internal/testing/backend/certifyVuln_test.go b/internal/testing/backend/certifyVuln_test.go index 4c992552d7..124487a884 100644 --- a/internal/testing/backend/certifyVuln_test.go +++ b/internal/testing/backend/certifyVuln_test.go @@ -26,6 +26,7 @@ import ( "github.com/guacsec/guac/internal/testing/ptrfrom" "github.com/guacsec/guac/internal/testing/testdata" "github.com/guacsec/guac/pkg/assembler/graphql/model" + "github.com/stretchr/testify/assert" ) var vmd1 = &model.ScanMetadata{ @@ -1514,3 +1515,131 @@ func TestIngestCertifyVulns(t *testing.T) { }) } } + +func TestDeleteCertifyVuln(t *testing.T) { + ctx := context.Background() + b := setupTest(t) + type call struct { + Pkgs []*model.IDorPkgInput + Vulns []*model.IDorVulnerabilityInput + CertifyVulns []*model.ScanMetadataInput + } + tests := []struct { + InPkg []*model.PkgInputSpec + Name string + InVuln []*model.VulnerabilityInputSpec + Calls []call + ExpVuln []*model.CertifyVuln + Query *model.CertifyVulnSpec + ExpIngestErr bool + ExpQueryErr bool + }{ + { + Name: "HappyPath", + InVuln: []*model.VulnerabilityInputSpec{testdata.C1, testdata.C2}, + InPkg: []*model.PkgInputSpec{testdata.P1, testdata.P2}, + Calls: []call{ + { + Pkgs: []*model.IDorPkgInput{{PackageInput: testdata.P2}, {PackageInput: testdata.P1}}, + Vulns: []*model.IDorVulnerabilityInput{{VulnerabilityInput: testdata.C1}, {VulnerabilityInput: testdata.C2}}, + CertifyVulns: []*model.ScanMetadataInput{ + { + Collector: "test collector", + Origin: "test origin", + ScannerVersion: "v1.0.0", + ScannerURI: "test scanner uri", + DbVersion: "2023.01.01", + DbURI: "test db uri", + TimeScanned: testdata.T1, + }, + { + Collector: "test collector", + Origin: "test origin", + ScannerVersion: "v1.0.0", + ScannerURI: "test scanner uri", + DbVersion: "2023.01.01", + DbURI: "test db uri", + TimeScanned: testdata.T1, + }, + }, + }, + }, + Query: &model.CertifyVulnSpec{ + Collector: ptrfrom.String("test collector"), + }, + ExpVuln: []*model.CertifyVuln{ + { + ID: "1", + Package: testdata.P2out, + Vulnerability: &model.Vulnerability{ + Type: "cve", + VulnerabilityIDs: []*model.VulnerabilityID{testdata.C1out}, + }, + Metadata: vmd1, + }, + { + ID: "10", + Package: testdata.P1out, + Vulnerability: &model.Vulnerability{ + Type: "cve", + VulnerabilityIDs: []*model.VulnerabilityID{testdata.C2out}, + }, + Metadata: vmd1, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + for _, v := range test.InVuln { + if _, err := b.IngestVulnerability(ctx, model.IDorVulnerabilityInput{VulnerabilityInput: v}); err != nil { + t.Fatalf("Could not ingest vulnerabilities: %a", err) + } + } + for _, p := range test.InPkg { + if _, err := b.IngestPackage(ctx, model.IDorPkgInput{PackageInput: p}); err != nil { + t.Fatalf("Could not ingest packages: %v", err) + } + } + for _, o := range test.Calls { + _, err := b.IngestCertifyVulns(ctx, o.Pkgs, o.Vulns, o.CertifyVulns) + if (err != nil) != test.ExpIngestErr { + t.Fatalf("did not get expected ingest error, want: %v, got: %v", test.ExpIngestErr, err) + } + if err != nil { + return + } + + } + got, err := b.CertifyVulnList(ctx, *test.Query, nil, nil) + if (err != nil) != test.ExpQueryErr { + t.Fatalf("did not get expected query error, want: %v, got: %v", test.ExpQueryErr, err) + } + if err != nil { + return + } + var returnedObjects []*model.CertifyVuln + if got != nil { + for _, obj := range got.Edges { + returnedObjects = append(returnedObjects, obj.Node) + } + } + if diff := cmp.Diff(test.ExpVuln, returnedObjects, commonOpts); diff != "" { + t.Errorf("Unexpected results. (-want +got):\n%s", diff) + } + deleted, err := b.Delete(ctx, returnedObjects[0].ID) + if err != nil { + t.Fatalf("did not get expected query error, want: %v, got: %v", test.ExpQueryErr, err) + } + assert.True(t, deleted) + secondgot, err := b.CertifyVulnList(ctx, *test.Query, nil, nil) + if (err != nil) != test.ExpQueryErr { + t.Fatalf("did not get expected query error, want: %v, got: %v", test.ExpQueryErr, err) + } + if err != nil { + return + } + assert.True(t, len(secondgot.Edges) == 1) + }) + } +} diff --git a/internal/testing/backend/main_test.go b/internal/testing/backend/main_test.go index 725b1f6e84..95078ffbef 100644 --- a/internal/testing/backend/main_test.go +++ b/internal/testing/backend/main_test.go @@ -93,8 +93,9 @@ var skipMatrix = map[string]map[string]bool{ // redis order issues "TestVEX": {arango: true, redis: true, tikv: true}, // redis order issues - "TestVEXBulkIngest": {arango: true, redis: true}, - "TestFindSoftware": {redis: true, arango: true}, + "TestVEXBulkIngest": {arango: true, redis: true}, + "TestFindSoftware": {redis: true, arango: true}, + "TestDeleteCertifyVuln": {arango: true, memmap: true, redis: true, tikv: true}, } type backend interface { diff --git a/pkg/assembler/backends/arangodb/path.go b/pkg/assembler/backends/arangodb/path.go index 057bcb3fd0..a230972336 100644 --- a/pkg/assembler/backends/arangodb/path.go +++ b/pkg/assembler/backends/arangodb/path.go @@ -358,6 +358,6 @@ func (c *arangoClient) Nodes(ctx context.Context, nodeIDs []string) ([]model.Nod return rv, nil } -func (c *demoClient) Delete(ctx context.Context, node string) (bool, error) { +func (c *arangoClient) Delete(ctx context.Context, node string) (bool, error) { panic(fmt.Errorf("not implemented: Delete")) } diff --git a/pkg/assembler/backends/ent/backend/certifyVuln.go b/pkg/assembler/backends/ent/backend/certifyVuln.go index b0849965ae..4b86ad7ac4 100644 --- a/pkg/assembler/backends/ent/backend/certifyVuln.go +++ b/pkg/assembler/backends/ent/backend/certifyVuln.go @@ -40,6 +40,21 @@ func bulkCertifyVulnGlobalID(ids []string) []string { return toGlobalIDs(certifyvuln.Table, ids) } +func (b *EntBackend) DeleteCertifyVuln(ctx context.Context, certifyVulnID uuid.UUID) (bool, error) { + _, txErr := WithinTX(ctx, b.client, func(ctx context.Context) (*string, error) { + tx := ent.TxFromContext(ctx) + + if err := tx.CertifyVuln.DeleteOneID(certifyVulnID).Exec(ctx); err != nil { + return nil, errors.Wrap(err, "failed to delete certifyVuln with error") + } + return nil, nil + }) + if txErr != nil { + return false, txErr + } + return true, nil +} + func certifyVulnConflictColumns() []string { return []string{ certifyvuln.FieldPackageID, diff --git a/pkg/assembler/backends/ent/backend/neighbors.go b/pkg/assembler/backends/ent/backend/neighbors.go index c8ee0e80c0..9ad2207b26 100644 --- a/pkg/assembler/backends/ent/backend/neighbors.go +++ b/pkg/assembler/backends/ent/backend/neighbors.go @@ -50,7 +50,234 @@ import ( ) func (b *EntBackend) Delete(ctx context.Context, node string) (bool, error) { - panic(fmt.Errorf("not implemented: Delete")) + foundGlobalID := fromGlobalID(node) + if foundGlobalID.nodeType == "" { + return false, fmt.Errorf("failed to parse globalID %s. Missing Node Type", node) + } + // return uuid if valid, else error + nodeID, err := uuid.Parse(foundGlobalID.id) + if err != nil { + return false, fmt.Errorf("uuid conversion from string failed with error: %w", err) + } + + switch foundGlobalID.nodeType { + // case artifact.Table: + // artifacts, err := b.Artifacts(ctx, &model.ArtifactSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for Artifacts via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(artifacts) != 1 { + // return false, fmt.Errorf("ID returned multiple Artifacts nodes %s", nodeID.String()) + // } + // return artifacts[0], nil + // case packageversion.Table: + // pv, err := b.client.PackageVersion.Query(). + // Where(packageversion.ID(nodeID)). + // WithName(func(q *ent.PackageNameQuery) {}). + // Only(ctx) + // if err != nil { + // return false, err + // } + // return toModelPackage(backReferencePackageVersion(pv)), nil + // case packagename.Table: + // pn, err := b.client.PackageName.Query(). + // Where(packagename.ID(nodeID)). + // WithVersions(). + // Only(ctx) + // if err != nil { + // return false, err + // } + // return toModelPackage(backReferencePackageName(pn)), nil + // case sourcename.Table: + // sources, err := b.Sources(ctx, &model.SourceSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for Sources via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(sources) != 1 { + // return false, fmt.Errorf("ID returned multiple Sources nodes %s", nodeID.String()) + // } + // return sources[0], nil + // case builder.Table: + // builders, err := b.Builders(ctx, &model.BuilderSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for Builders via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(builders) != 1 { + // return false, fmt.Errorf("ID returned multiple Builders nodes %s", nodeID.String()) + // } + // return builders[0], nil + // case license.Table: + // licenses, err := b.Licenses(ctx, &model.LicenseSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for Licenses via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(licenses) != 1 { + // return false, fmt.Errorf("ID returned multiple Licenses nodes %s", nodeID.String()) + // } + // return licenses[0], nil + // case vulnerabilityid.Table: + // vulnerabilities, err := b.Vulnerabilities(ctx, &model.VulnerabilitySpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for Vulnerabilities via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(vulnerabilities) != 1 { + // return false, fmt.Errorf("ID returned multiple Vulnerabilities nodes %s", nodeID.String()) + // } + // return vulnerabilities[0], nil + // case certifyBadString: + // certs, err := b.CertifyBad(ctx, &model.CertifyBadSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for CertifyBad via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(certs) != 1 { + // return false, fmt.Errorf("ID returned multiple CertifyBad nodes %s", nodeID.String()) + // } + // return certs[0], nil + // case certifyGoodString: + // certs, err := b.CertifyGood(ctx, &model.CertifyGoodSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for CertifyGood via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(certs) != 1 { + // return false, fmt.Errorf("ID returned multiple CertifyGood nodes %s", nodeID.String()) + // } + // return certs[0], nil + // case certifylegal.Table: + // legals, err := b.CertifyLegal(ctx, &model.CertifyLegalSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for CertifyLegal via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(legals) != 1 { + // return false, fmt.Errorf("ID returned multiple CertifyLegal nodes %s", nodeID.String()) + // } + // return legals[0], nil + // case certifyscorecard.Table: + // scores, err := b.Scorecards(ctx, &model.CertifyScorecardSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for scorecard via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(scores) != 1 { + // return false, fmt.Errorf("ID returned multiple scorecard nodes %s", nodeID.String()) + // } + // return scores[0], nil + // case certifyvex.Table: + // vexs, err := b.CertifyVEXStatement(ctx, &model.CertifyVEXStatementSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for CertifyVEXStatement via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(vexs) != 1 { + // return false, fmt.Errorf("ID returned multiple CertifyVEXStatement nodes %s", nodeID.String()) + // } + // return vexs[0], nil + case certifyvuln.Table: + deleted, err := b.DeleteCertifyVuln(ctx, nodeID) + if err != nil { + return false, fmt.Errorf("failed to delete CertifyVuln via ID: %s, with error: %w", nodeID.String(), err) + } + return deleted, nil + // case hashequal.Table: + // hes, err := b.HashEqual(ctx, &model.HashEqualSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for HashEqual via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(hes) != 1 { + // return false, fmt.Errorf("ID returned multiple HashEqual nodes %s", nodeID.String()) + // } + // return hes[0], nil + // case hasmetadata.Table: + // hms, err := b.HasMetadata(ctx, &model.HasMetadataSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for HasMetadata via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(hms) != 1 { + // return false, fmt.Errorf("ID returned multiple HasMetadata nodes %s", nodeID.String()) + // } + // return hms[0], nil + // case billofmaterials.Table: + // hbs, err := b.HasSBOM(ctx, &model.HasSBOMSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for HasSBOM via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(hbs) != 1 { + // return false, fmt.Errorf("ID returned multiple HasSBOM nodes %s", nodeID.String()) + // } + // return hbs[0], nil + // case slsaattestation.Table: + // slsas, err := b.HasSlsa(ctx, &model.HasSLSASpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for HasSlsa via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(slsas) != 1 { + // return false, fmt.Errorf("ID returned multiple HasSlsa nodes %s", nodeID.String()) + // } + // return slsas[0], nil + // case hassourceat.Table: + // hsas, err := b.HasSourceAt(ctx, &model.HasSourceAtSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for HasSourceAt via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(hsas) != 1 { + // return false, fmt.Errorf("ID returned multiple HasSourceAt nodes %s", nodeID.String()) + // } + // return hsas[0], nil + // case dependency.Table: + // deps, err := b.IsDependency(ctx, &model.IsDependencySpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for IsDependency via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(deps) != 1 { + // return false, fmt.Errorf("ID returned multiple IsDependency nodes %s", nodeID.String()) + // } + // return deps[0], nil + // case occurrence.Table: + // occurs, err := b.IsOccurrence(ctx, &model.IsOccurrenceSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for IsOccurrence via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(occurs) != 1 { + // return false, fmt.Errorf("ID returned multiple IsOccurrence nodes %s", nodeID.String()) + // } + // return occurs[0], nil + // case pkgequal.Table: + // pes, err := b.PkgEqual(ctx, &model.PkgEqualSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for PkgEqual via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(pes) != 1 { + // return false, fmt.Errorf("ID returned multiple PkgEqual nodes %s", nodeID.String()) + // } + // return pes[0], nil + // case pointofcontact.Table: + // pocs, err := b.PointOfContact(ctx, &model.PointOfContactSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for PointOfContact via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(pocs) != 1 { + // return false, fmt.Errorf("ID returned multiple PointOfContact nodes %s", nodeID.String()) + // } + // return pocs[0], nil + // case vulnequal.Table: + // ves, err := b.VulnEqual(ctx, &model.VulnEqualSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for VulnEqual via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(ves) != 1 { + // return false, fmt.Errorf("ID returned multiple VulnEqual nodes %s", nodeID.String()) + // } + // return ves[0], nil + // case vulnerabilitymetadata.Table: + // vms, err := b.VulnerabilityMetadata(ctx, &model.VulnerabilityMetadataSpec{ID: ptrfrom.String(nodeID.String())}) + // if err != nil { + // return false, fmt.Errorf("failed to query for VulnerabilityMetadata via ID: %s, with error: %w", nodeID.String(), err) + // } + // if len(vms) != 1 { + // return false, fmt.Errorf("ID returned multiple VulnerabilityMetadata nodes %s", nodeID.String()) + // } + // return vms[0], nil + default: + log.Printf("Unknown node type: %s", foundGlobalID.nodeType) + } + return false, nil } func (b *EntBackend) Path(ctx context.Context, subject string, target string, maxPathLength int, usingOnly []model.Edge) ([]model.Node, error) { diff --git a/pkg/assembler/backends/ent/schema/certifyvuln.go b/pkg/assembler/backends/ent/schema/certifyvuln.go index 89f73ab910..d8dc075125 100644 --- a/pkg/assembler/backends/ent/schema/certifyvuln.go +++ b/pkg/assembler/backends/ent/schema/certifyvuln.go @@ -51,6 +51,8 @@ func (CertifyVuln) Fields() []ent.Field { // Edges of the Vulnerability. func (CertifyVuln) Edges() []ent.Edge { return []ent.Edge{ + // edge.To("vulnerability", VulnerabilityID.Type).Unique().Field("vulnerability_id").Required().Annotations(entsql.OnDelete(entsql.Cascade)), + // edge.To("package", PackageVersion.Type).Unique().Field("package_id").Required().Annotations(entsql.OnDelete(entsql.Cascade)), edge.To("vulnerability", VulnerabilityID.Type).Unique().Field("vulnerability_id").Required(), edge.To("package", PackageVersion.Type).Unique().Field("package_id").Required(), } diff --git a/pkg/assembler/clients/generated/operations.go b/pkg/assembler/clients/generated/operations.go index ed6f160a63..1d50ad4852 100644 --- a/pkg/assembler/clients/generated/operations.go +++ b/pkg/assembler/clients/generated/operations.go @@ -9141,6 +9141,15 @@ const ( ComparatorLessEqual Comparator = "LESS_EQUAL" ) +// DeleteResponse is returned by Delete on success. +type DeleteResponse struct { + // Delete node with ID and all associated relationships + Delete bool `json:"delete"` +} + +// GetDelete returns DeleteResponse.Delete, and is useful for accessing the field via an interface. +func (v *DeleteResponse) GetDelete() bool { return v.Delete } + // DependenciesIsDependency includes the requested fields of the GraphQL type IsDependency. // The GraphQL type's documentation follows. // @@ -29711,6 +29720,14 @@ func (v *__CertifyVulnListInput) GetAfter() *string { return v.After } // GetFirst returns __CertifyVulnListInput.First, and is useful for accessing the field via an interface. func (v *__CertifyVulnListInput) GetFirst() *int { return v.First } +// __DeleteInput is used internally by genqlient +type __DeleteInput struct { + NodeID string `json:"nodeID"` +} + +// GetNodeID returns __DeleteInput.NodeID, and is useful for accessing the field via an interface. +func (v *__DeleteInput) GetNodeID() string { return v.NodeID } + // __DependenciesInput is used internally by genqlient type __DependenciesInput struct { Filter IsDependencySpec `json:"filter"` @@ -32275,6 +32292,39 @@ func CertifyVulnList( return &data_, err_ } +// The query or mutation executed by Delete. +const Delete_Operation = ` +mutation Delete ($nodeID: ID!) { + delete(node: $nodeID) +} +` + +func Delete( + ctx_ context.Context, + client_ graphql.Client, + nodeID string, +) (*DeleteResponse, error) { + req_ := &graphql.Request{ + OpName: "Delete", + Query: Delete_Operation, + Variables: &__DeleteInput{ + NodeID: nodeID, + }, + } + var err_ error + + var data_ DeleteResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + // The query or mutation executed by Dependencies. const Dependencies_Operation = ` query Dependencies ($filter: IsDependencySpec!) { diff --git a/pkg/assembler/clients/operations/delete.graphql b/pkg/assembler/clients/operations/delete.graphql new file mode 100644 index 0000000000..82a01ff156 --- /dev/null +++ b/pkg/assembler/clients/operations/delete.graphql @@ -0,0 +1,22 @@ +# +# 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. + +# NOTE: This is experimental and might change in the future! + +# Delete nodes based on ID (and all associated edges) + +mutation Delete($nodeID: ID!) { + delete(node: $nodeID) +} \ No newline at end of file diff --git a/pkg/certifier/components/root_package/root_package.go b/pkg/certifier/components/root_package/root_package.go index f758586a57..5d6d687680 100644 --- a/pkg/certifier/components/root_package/root_package.go +++ b/pkg/certifier/components/root_package/root_package.go @@ -169,6 +169,12 @@ func (p *packageQuery) getPackageNodes(ctx context.Context, nodeChan chan<- *Pac if math.Abs(difference.Hours()) < float64(p.daysSinceLastScan*24) { certifyVulnFound = true } + if math.Abs(difference.Hours()) > float64(p.daysSinceLastScan*24) { + _, err := generated.Delete(ctx, p.client, vulns.Id) + if err != nil { + return fmt.Errorf("failed to delete certifyVuln node with ID: %s, with error: %w", vulns.Id, err) + } + } } else { certifyVulnFound = true break