Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions database/models/vulnevent_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func (event *VulnEvent) GetArbitraryJSONData() map[string]any {
return event.arbitraryJSONData
}

// adds additional arbitrary JSON data
func (event *VulnEvent) SetArbitraryJSONData(data map[string]any) {
event.arbitraryJSONData = data
// parse the additional data
Expand Down
74 changes: 54 additions & 20 deletions services/csaf_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ import (
"golang.org/x/mod/semver"
)

// maybe we should move this to csaf dto?
const ProductIDDelimiter string = "|"

type csafService struct {
client http.Client
}
Expand Down Expand Up @@ -926,9 +929,9 @@ func generateProductTree(asset models.Asset, assetVersionRepository shared.Asset

func artifactNameAndComponentPurlToProductID(artifactName, assetVersionName, componentPurl string) gocsaf.ProductID {
if assetVersionName == "" {
return gocsaf.ProductID(fmt.Sprintf("%s|%s", artifactName, componentPurl))
return gocsaf.ProductID(fmt.Sprintf("%s%s%s", strings.TrimSpace(artifactName), ProductIDDelimiter, componentPurl))
}
return gocsaf.ProductID(fmt.Sprintf("%s@%s|%s", artifactName, assetVersionName, componentPurl))
return gocsaf.ProductID(fmt.Sprintf("%s@%s%s%s", strings.TrimSpace(artifactName), assetVersionName, ProductIDDelimiter, componentPurl))
}

// generates the vulnerability object for a specific asset at a certain timeStamp in time
Expand Down Expand Up @@ -1143,12 +1146,16 @@ func stateToString(state dtos.VulnState) string {
func generateTrackingObject(asset models.Asset, vulns []models.DependencyVuln) (gocsaf.Tracking, error) {
tracking := gocsaf.Tracking{}
allEvents := make([]vulnEventWithVuln, 0)

for _, vuln := range vulns {
for _, event := range vuln.Events {
allEvents = append(allEvents, vulnEventWithVuln{
VulnEvent: event,
Vuln: vuln,
})
// only handle relevant vuln events
if isVulnEventCSAFRelevant(event) {
allEvents = append(allEvents, vulnEventWithVuln{
VulnEvent: event,
Vuln: vuln,
})
}
}
}

Expand Down Expand Up @@ -1206,9 +1213,9 @@ func buildRevisionHistory(asset models.Asset, vulnEvents []vulnEventWithVuln) ([
Date: utils.Ptr(event.VulnEvent.CreatedAt.Format(time.RFC3339)),
}
revisionObject.Number = utils.Ptr(gocsaf.RevisionNumber(strconv.Itoa(version + 1)))
summary, err := generateSummaryForEvent(event.Vuln, event.VulnEvent)
if err != nil {
continue
summary := generateSummaryForEvent(event.Vuln, event.VulnEvent)
if summary == "" {
slog.Error("could not generate summary for revision entry")
}
revisionObject.Summary = &summary
revisions = append(revisions, &revisionObject)
Expand All @@ -1218,26 +1225,45 @@ func buildRevisionHistory(asset models.Asset, vulnEvents []vulnEventWithVuln) ([
return revisions, nil
}

func generateSummaryForEvent(vuln models.DependencyVuln, event models.VulnEvent) (string, error) {
artifactNames := make([]string, 0)
for _, artifact := range vuln.Artifacts {
artifactNames = append(artifactNames, fmt.Sprintf("%s@%s", artifact.ArtifactName, artifact.AssetVersionName))
// generates a textual summary of the given event, returns an empty summary if the event is not relevant for the revision
func generateSummaryForEvent(vuln models.DependencyVuln, event models.VulnEvent) string {
// set of all unique affected artifacts for this vuln event
uniqueArtifacts := make([]string, 0)

// retrieve artifact information from arbitrary json data from keys artifactNames and scannerID
jsonData := event.GetArbitraryJSONData()
artifact, ok := jsonData["artifactNames"].(string)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will break. Relying on untyped logic so much makes it hard to maintain. I am pretty sure, we are not setting it consistently...

if ok {
uniqueArtifacts = append(uniqueArtifacts, artifact)
}
artifact, ok = jsonData["scannerID"].(string)
if ok {
if !slices.Contains(uniqueArtifacts, artifact) {
uniqueArtifacts = append(uniqueArtifacts, artifact)
}
}

// then build the ProductID for each of the found artifacts
for i := range uniqueArtifacts {
uniqueArtifacts[i] = string(artifactNameAndComponentPurlToProductID(artifact, vuln.AssetVersionName, *vuln.ComponentPurl))
}
artifactNameString := strings.Join(normalize.SortStringsSlice(artifactNames), ", ")

productIDsString := strings.Join(normalize.SortStringsSlice(uniqueArtifacts), ", ")

switch event.Type {
case dtos.EventTypeDetected:
return fmt.Sprintf("Detected vulnerability %s in package %s (%s).", *vuln.CVEID, *vuln.ComponentPurl, artifactNameString), nil
return fmt.Sprintf("Detected vulnerability %s in product %s.", *vuln.CVEID, productIDsString)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually i preferred the way it was before...

case dtos.EventTypeReopened:
return fmt.Sprintf("Reopened vulnerability %s in package %s (%s).", *vuln.CVEID, *vuln.ComponentPurl, artifactNameString), nil
return fmt.Sprintf("Reopened vulnerability %s in product %s.", *vuln.CVEID, productIDsString)
case dtos.EventTypeFixed:
return fmt.Sprintf("Fixed vulnerability %s in package %s (%s).", *vuln.CVEID, *vuln.ComponentPurl, artifactNameString), nil
return fmt.Sprintf("Fixed vulnerability %s in product %s.", *vuln.CVEID, productIDsString)
case dtos.EventTypeAccepted:
return fmt.Sprintf("Accepted vulnerability %s in package %s (%s).", *vuln.CVEID, *vuln.ComponentPurl, artifactNameString), nil
return fmt.Sprintf("Accepted vulnerability %s in product %s.", *vuln.CVEID, productIDsString)
case dtos.EventTypeFalsePositive:
return fmt.Sprintf("Marked vulnerability %s as false positive in package %s (%s).", *vuln.CVEID, *vuln.ComponentPurl, artifactNameString), nil
return fmt.Sprintf("Marked vulnerability %s as false positive in product %s.", *vuln.CVEID, productIDsString)
default:
return "", fmt.Errorf("unknown event type: %s (%s)", event.Type, artifactNameString)
slog.Error(fmt.Sprintf("Cannot create summary for event with type: %s, should have been filtered out already!", event.Type))
return ""
}
}

Expand Down Expand Up @@ -1282,3 +1308,11 @@ func SignCSAFReport(csafJSON []byte) ([]byte, error) {
}
return signature, nil
}

func isVulnEventCSAFRelevant(event models.VulnEvent) bool {
return event.Type == dtos.EventTypeDetected ||
event.Type == dtos.EventTypeReopened ||
event.Type == dtos.EventTypeFixed ||
event.Type == dtos.EventTypeAccepted ||
event.Type == dtos.EventTypeFalsePositive
}