Skip to content

Commit

Permalink
feat(issueVariant): allow for creation of issue variant with only a s…
Browse files Browse the repository at this point in the history
…everity rating (#362)

* feat(issueVariant): added optional rating field to severity input

* feat(issueVariant): added comments and check for when rating doesn't match vector

* fix(tests): removed severity input from tests where not needed

* feat(issueVariant): changed iv creation to just use vector when both rating and vector are passed

* feat(issueVariant): changed update handler for iv to overwrite existing vector if passed and added test

* adding test

---------

Co-authored-by: dustindemmerle <[email protected]>
Co-authored-by: Michael Reimsbach <[email protected]>
  • Loading branch information
3 people committed Nov 25, 2024
1 parent abd965d commit 1e07244
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 7 deletions.
11 changes: 9 additions & 2 deletions internal/api/graphql/graph/model/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
1 change: 1 addition & 0 deletions internal/api/graphql/graph/schema/common.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type Severity {

input SeverityInput {
vector: String
rating: SeverityValues
}

type FilterItem {
Expand Down
29 changes: 26 additions & 3 deletions internal/database/mariadb/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,14 +362,21 @@ 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),
IssueRepository: repository,
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),
Expand Down Expand Up @@ -403,14 +410,22 @@ 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),
IssueRepository: &rep,
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),
Expand All @@ -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),
Expand All @@ -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),
Expand Down
3 changes: 2 additions & 1 deletion internal/database/mariadb/issue_variant.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
80 changes: 79 additions & 1 deletion internal/e2e/issue_variant_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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))
})
})
})
})
Expand Down Expand Up @@ -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())
}
})
})
})
})
Expand Down
24 changes: 24 additions & 0 deletions internal/entity/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 1e07244

Please sign in to comment.