diff --git a/internal/api/graphql/graph/model/models.go b/internal/api/graphql/graph/model/models.go index 7089bd5f..404a89a5 100644 --- a/internal/api/graphql/graph/model/models.go +++ b/internal/api/graphql/graph/model/models.go @@ -80,7 +80,7 @@ func NewPage(p *entity.Page) *Page { func NewSeverity(sev entity.Severity) *Severity { severity, _ := SeverityValue(sev.Value) - if severity == "unknown" { + if severity == "unknown" || sev.Cvss == (entity.Cvss{}) { return &Severity{ Value: &severity, Score: &sev.Score, @@ -157,9 +157,16 @@ func NewSeverity(sev entity.Severity) *Severity { } func NewSeverityEntity(severity *SeverityInput) entity.Severity { - if severity == nil || severity.Vector == nil { + if severity == nil || (severity.Rating == nil && severity.Vector == nil) { + // no severity information was passed return entity.Severity{} } + if (severity.Vector == nil || *severity.Vector == "") && severity.Rating != nil { + // only rating was passed + return entity.NewSeverityFromRating(entity.SeverityValues(*severity.Rating)) + } + // both rating and vector or only vector was passed + // either way, use the vector as the primary source of information return entity.NewSeverity(*severity.Vector) } diff --git a/internal/api/graphql/graph/queryCollection/issueVariant/createWithRating.graphql b/internal/api/graphql/graph/queryCollection/issueVariant/createWithRating.graphql new file mode 100644 index 00000000..52915f47 --- /dev/null +++ b/internal/api/graphql/graph/queryCollection/issueVariant/createWithRating.graphql @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation ($input: IssueVariantInput!) { + createIssueVariant ( + input: $input + ) { + id + secondaryName + description + severity { + value + score + } + issueRepositoryId + issueId + } +} \ No newline at end of file diff --git a/internal/api/graphql/graph/schema/common.graphqls b/internal/api/graphql/graph/schema/common.graphqls index ee15d2b7..11b632ae 100644 --- a/internal/api/graphql/graph/schema/common.graphqls +++ b/internal/api/graphql/graph/schema/common.graphqls @@ -98,6 +98,7 @@ type Severity { input SeverityInput { vector: String + rating: SeverityValues } type FilterItem { diff --git a/internal/database/mariadb/entity.go b/internal/database/mariadb/entity.go index 0f9f78e8..0d14e775 100644 --- a/internal/database/mariadb/entity.go +++ b/internal/database/mariadb/entity.go @@ -362,6 +362,13 @@ type IssueVariantRow struct { } func (ivr *IssueVariantRow) AsIssueVariant(repository *entity.IssueRepository) entity.IssueVariant { + var severity entity.Severity + if ivr.Vector.String == "" { + severity = entity.NewSeverityFromRating(entity.SeverityValues(ivr.Rating.String)) + } else { + severity = entity.NewSeverity(GetStringValue(ivr.Vector)) + } + return entity.IssueVariant{ Id: GetInt64Value(ivr.Id), IssueRepositoryId: GetInt64Value(ivr.IssueRepositoryId), @@ -369,7 +376,7 @@ func (ivr *IssueVariantRow) AsIssueVariant(repository *entity.IssueRepository) e SecondaryName: GetStringValue(ivr.SecondaryName), IssueId: GetInt64Value(ivr.IssueId), Issue: nil, - Severity: entity.NewSeverity(GetStringValue(ivr.Vector)), + Severity: severity, Description: GetStringValue(ivr.Description), Metadata: entity.Metadata{ CreatedAt: GetTimeValue(ivr.CreatedAt), @@ -403,6 +410,14 @@ type IssueVariantWithRepository struct { func (ivwr *IssueVariantWithRepository) AsIssueVariantEntry() entity.IssueVariant { rep := ivwr.IssueRepositoryRow.AsIssueRepository() + + var severity entity.Severity + if ivwr.Vector.String == "" { + severity = entity.NewSeverityFromRating(entity.SeverityValues(ivwr.Rating.String)) + } else { + severity = entity.NewSeverity(GetStringValue(ivwr.Vector)) + } + return entity.IssueVariant{ Id: GetInt64Value(ivwr.IssueVariantRow.Id), IssueRepositoryId: GetInt64Value(ivwr.IssueRepositoryId), @@ -410,7 +425,7 @@ func (ivwr *IssueVariantWithRepository) AsIssueVariantEntry() entity.IssueVarian SecondaryName: GetStringValue(ivwr.IssueVariantRow.SecondaryName), IssueId: GetInt64Value(ivwr.IssueId), Issue: nil, - Severity: entity.NewSeverity(GetStringValue(ivwr.Vector)), + Severity: severity, Description: GetStringValue(ivwr.Description), Metadata: entity.Metadata{ CreatedAt: GetTimeValue(ivwr.IssueVariantRow.CreatedAt), @@ -429,6 +444,14 @@ type ServiceIssueVariantRow struct { func (siv *ServiceIssueVariantRow) AsServiceIssueVariantEntry() entity.ServiceIssueVariant { rep := siv.IssueRepositoryRow.AsIssueRepository() + + var severity entity.Severity + if siv.Vector.String == "" { + severity = entity.NewSeverityFromRating(entity.SeverityValues(siv.Rating.String)) + } else { + severity = entity.NewSeverity(GetStringValue(siv.Vector)) + } + return entity.ServiceIssueVariant{ IssueVariant: entity.IssueVariant{ Id: GetInt64Value(siv.IssueVariantRow.Id), @@ -437,7 +460,7 @@ func (siv *ServiceIssueVariantRow) AsServiceIssueVariantEntry() entity.ServiceIs SecondaryName: GetStringValue(siv.IssueVariantRow.SecondaryName), IssueId: GetInt64Value(siv.IssueId), Issue: nil, - Severity: entity.NewSeverity(GetStringValue(siv.Vector)), + Severity: severity, Description: GetStringValue(siv.Description), Metadata: entity.Metadata{ CreatedAt: GetTimeValue(siv.IssueVariantRow.CreatedAt), diff --git a/internal/database/mariadb/issue_variant.go b/internal/database/mariadb/issue_variant.go index 1cac98b3..8fa4841b 100644 --- a/internal/database/mariadb/issue_variant.go +++ b/internal/database/mariadb/issue_variant.go @@ -81,7 +81,8 @@ func (s *SqlDatabase) getIssueVariantUpdateFields(issueVariant *entity.IssueVari if issueVariant.SecondaryName != "" { fl = append(fl, "issuevariant_secondary_name = :issuevariant_secondary_name") } - if issueVariant.Severity.Cvss.Vector != "" { + // if rating but not vector is passed, we need to include the vector in the update in order to overwrite any existing vector + if issueVariant.Severity.Cvss.Vector != "" || (issueVariant.Severity.Value != "" && issueVariant.Severity.Cvss.Vector == "") { fl = append(fl, "issuevariant_vector = :issuevariant_vector") } if issueVariant.Severity.Value != "" { diff --git a/internal/e2e/issue_variant_query_test.go b/internal/e2e/issue_variant_query_test.go index 5ba4f242..b9d178b7 100644 --- a/internal/e2e/issue_variant_query_test.go +++ b/internal/e2e/issue_variant_query_test.go @@ -224,7 +224,7 @@ var _ = Describe("Creating IssueVariant via API", Label("e2e", "IssueVariants"), }) Context("and a mutation query is performed", Label("create.graphql"), func() { - It("creates new issueVariant", func() { + It("creates new issueVariant with Vector", func() { // create a queryCollection (safe to share across requests) client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.Port)) @@ -261,6 +261,43 @@ var _ = Describe("Creating IssueVariant via API", Label("e2e", "IssueVariants"), Expect(*respData.IssueVariant.IssueID).To(Equal(fmt.Sprintf("%d", issueVariant.IssueId))) Expect(*respData.IssueVariant.Severity.Cvss.Vector).To(Equal(issueVariant.Severity.Cvss.Vector)) }) + It("creates new issueVariant with Rating", func() { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.Port)) + + //@todo may need to make this more fault proof?! What if the test is executed from the root dir? does it still work? + b, err := os.ReadFile("../api/graphql/graph/queryCollection/issueVariant/createWithRating.graphql") + + Expect(err).To(BeNil()) + str := string(b) + req := graphql.NewRequest(str) + + req.Var("input", map[string]interface{}{ + "secondaryName": issueVariant.SecondaryName, + "description": issueVariant.Description, + "issueRepositoryId": fmt.Sprintf("%d", issueVariant.IssueRepositoryId), + "issueId": fmt.Sprintf("%d", issueVariant.IssueId), + "severity": map[string]string{ + "rating": issueVariant.Severity.Value, + }, + }) + + req.Header.Set("Cache-Control", "no-cache") + ctx := context.Background() + + var respData struct { + IssueVariant model.IssueVariant `json:"createIssueVariant"` + } + if err := util2.RequestWithBackoff(func() error { return client.Run(ctx, req, &respData) }); err != nil { + logrus.WithError(err).WithField("request", req).Fatalln("Error while unmarshaling") + } + + Expect(*respData.IssueVariant.SecondaryName).To(Equal(issueVariant.SecondaryName)) + Expect(*respData.IssueVariant.Description).To(Equal(issueVariant.Description)) + Expect(*respData.IssueVariant.IssueRepositoryID).To(Equal(fmt.Sprintf("%d", issueVariant.IssueRepositoryId))) + Expect(*respData.IssueVariant.IssueID).To(Equal(fmt.Sprintf("%d", issueVariant.IssueId))) + Expect(string(*respData.IssueVariant.Severity.Value)).To(Equal(issueVariant.Severity.Value)) + }) }) }) }) @@ -329,6 +366,47 @@ var _ = Describe("Updating issueVariant via API", Label("e2e", "IssueVariants"), Expect(*respData.IssueVariant.SecondaryName).To(Equal(issueVariant.SecondaryName)) }) + It("updates issueVariant severity with rating", func() { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.Port)) + + //@todo may need to make this more fault proof?! What if the test is executed from the root dir? does it still work? + b, err := os.ReadFile("../api/graphql/graph/queryCollection/issueVariant/update.graphql") + + Expect(err).To(BeNil()) + str := string(b) + req := graphql.NewRequest(str) + + newRating := model.SeverityValuesLow + + ir := seedCollection.IssueRepositoryRows[0].AsIssueRepository() + issueVariant := seedCollection.IssueVariantRows[0].AsIssueVariant(&ir) + + issueVariant.Severity.Value = string(newRating) + + req.Var("id", fmt.Sprintf("%d", issueVariant.Id)) + req.Var("input", map[string]interface{}{ + "severity": model.SeverityInput{ + Rating: &newRating, + }, + }) + + req.Header.Set("Cache-Control", "no-cache") + ctx := context.Background() + + var respData struct { + IssueVariant model.IssueVariant `json:"updateIssueVariant"` + } + + if err := util2.RequestWithBackoff(func() error { return client.Run(ctx, req, &respData) }); err != nil { + logrus.WithError(err).WithField("request", req).Fatalln("Error while unmarshaling") + } + + Expect(string(*respData.IssueVariant.Severity.Value)).To(Equal(issueVariant.Severity.Value)) + if respData.IssueVariant.Severity.Cvss != nil && respData.IssueVariant.Severity.Cvss.Vector != nil { + Expect(string(*respData.IssueVariant.Severity.Cvss.Vector)).To(BeEmpty()) + } + }) }) }) }) diff --git a/internal/entity/common.go b/internal/entity/common.go index f0d26d0f..dd1ecafc 100644 --- a/internal/entity/common.go +++ b/internal/entity/common.go @@ -160,6 +160,30 @@ type Cursor struct { Limit int } +func NewSeverityFromRating(rating SeverityValues) Severity { + // These values are based on the CVSS v3.1 specification + // https://www.first.org/cvss/v3.1/specification-document#Qualitative-Severity-Rating-Scale + // https://nvd.nist.gov/vuln-metrics/cvss + // They are the lower bounds of the CVSS Score ranges that correlate to each given Rating + score := 0.0 + switch rating { + case SeverityValuesLow: + score = 0.1 + case SeverityValuesMedium: + score = 4.0 + case SeverityValuesHigh: + score = 7.0 + case SeverityValuesCritical: + score = 9.0 + } + + return Severity{ + Value: string(rating), + Score: score, + Cvss: Cvss{}, + } +} + func NewSeverity(url string) Severity { ev, err := metric.NewEnvironmental().Decode(url)