package ci import ( "github.com/google/osv-scanner/v2/internal/grouper" "github.com/google/osv-scanner/v2/internal/output" "github.com/google/osv-scanner/v2/pkg/models" ) // DiffVulnerabilityResults will return any new vulnerabilities that are in `newRes` // which is not present in `oldRes`, but not the reverse. func DiffVulnerabilityResults(oldRes, newRes models.VulnerabilityResults) models.VulnerabilityResults { result := models.VulnerabilityResults{} // Initialize caches for quick lookup sourceToIndex, packageToIndex, vulnToIndex := initializeCaches(oldRes) for _, ps := range newRes.Results { sourceIdx, sourceExists := sourceToIndex[ps.Source] if !sourceExists { // Newly introduced source, so all results for this source are going to be new, add everything for this source result.Results = append(result.Results, ps) continue } // Otherwise the old source used to exist, so we need to find the difference in the packages result.Results = append(result.Results, models.PackageSource{ Source: ps.Source, }) resultPS := &result.Results[len(result.Results)-1] for _, pv := range ps.Packages { pkgIdx, packageExists := packageToIndex[sourceIdx][pv.Package] if !packageExists { // Newly introduced package, so all results for this package are going to be new, add everything for this package resultPS.Packages = append(resultPS.Packages, pv) continue } // Otherwise the old package used to exist, so we need to find the difference in the vulnerabilities // Only copy over packages as vulns and groups might change resultPS.Packages = append(resultPS.Packages, models.PackageVulns{ Package: pv.Package, }) resultPV := &resultPS.Packages[len(resultPS.Packages)-1] for _, v := range pv.Vulnerabilities { if !vulnToIndex[sourceIdx][pkgIdx][v.GetId()] { // Vulnerability is new, add it to the results resultPV.Vulnerabilities = append(resultPV.Vulnerabilities, v) continue } } if len(resultPV.Vulnerabilities) == 0 { // No vulns, so we can remove the PackageVulns entry entirely, and skip grouping resultPS.Packages = resultPS.Packages[:len(resultPS.Packages)-1] continue } // Rebuild the groups lost in the previous step groups := grouper.Group(grouper.ConvertVulnerabilityToIDAliases(resultPV.Vulnerabilities)) for i, group := range groups { groups[i].MaxSeverity = output.MaxSeverity(group, *resultPV) } resultPV.Groups = groups } if len(resultPS.Packages) == 0 { // No packages, so we can remove the PackageSource entry entirely result.Results = result.Results[:len(result.Results)-1] continue } } return result } // initializeCaches sets up maps for quick lookup of sources, packages, and vulnerabilities by their indices. func initializeCaches(oldRes models.VulnerabilityResults) (map[models.SourceInfo]int, []map[models.PackageInfo]int, [][]map[string]bool) { sourceToIndex := make(map[models.SourceInfo]int, len(oldRes.Results)) // The index in the array corresponds to a source index, a query would look like packageToIndex[sourceIndex][packageInfo] packageToIndex := make([]map[models.PackageInfo]int, len(oldRes.Results)) // The first index in the array corresponds to a source index, and the second index corresponds to a package index // a query would look like vulnToIndex[sourceIndex][packageIndex][vulnID] vulnToIndex := make([][]map[string]bool, len(oldRes.Results)) // Populate index maps for sources, packages, and vulnerabilities for sourceIndex, vulnResult := range oldRes.Results { sourceToIndex[oldRes.Results[sourceIndex].Source] = sourceIndex if vulnToIndex[sourceIndex] == nil { vulnToIndex[sourceIndex] = make([]map[string]bool, len(vulnResult.Packages)) } for packageIndex, pkg := range vulnResult.Packages { if packageToIndex[sourceIndex] == nil { packageToIndex[sourceIndex] = make(map[models.PackageInfo]int, len(vulnResult.Packages)) } packageToIndex[sourceIndex][pkg.Package] = packageIndex if vulnToIndex[sourceIndex][packageIndex] == nil { vulnToIndex[sourceIndex][packageIndex] = make(map[string]bool, len(pkg.Vulnerabilities)) } for _, vuln := range pkg.Vulnerabilities { vulnToIndex[sourceIndex][packageIndex][vuln.GetId()] = true // Mark the vulnerability as present } } } return sourceToIndex, packageToIndex, vulnToIndex } // DiffVulnerabilityResultsByOccurrences will return the occurrence of each vulnerability that are in `newRes` // which is not present in `oldRes`, but not the reverse. This calculates the difference by vulnerability ID, // while ignoring the source of the vulnerability. // // This prevents us reporting "new" vulnerabilities in a PR when a previously vulnerable file is being moved. func DiffVulnerabilityResultsByOccurrences(oldRes, newRes models.VulnerabilityResults) map[string]int { oldResFlat := oldRes.Flatten() newResFlat := newRes.Flatten() oldResMap := map[string]int{} newResMap := map[string]int{} for _, vf := range oldResFlat { oldResMap[vf.Vulnerability.GetId()] += 1 } for _, vf := range newResFlat { newResMap[vf.Vulnerability.GetId()] += 1 } for k, oldVulnCount := range oldResMap { // If the new result has fewer vulnerabilities than the old result remove the entry from the new result. // `map`'s default value is 0 when empty, and delete also works fine when the entry is empty if newResMap[k] <= oldVulnCount { delete(newResMap, k) } } return newResMap }