-
Notifications
You must be signed in to change notification settings - Fork 99
PR Decorator Fix - Same CVEs should be aggregated in the same record #1067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3_er
Are you sure you want to change the base?
Changes from all commits
7d3e789
97cefd4
e901bb9
539ffda
6dd17ac
0ebb33d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package packageupdaters | |
| import ( | ||
| "fmt" | ||
| "io/fs" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "regexp" | ||
|
|
@@ -135,6 +136,26 @@ func BuildPackageWithVersionRegex(impactedName, impactedVersion, dependencyLineF | |
| return regexp.MustCompile(regexpCompleteFormat) | ||
| } | ||
|
|
||
| func getAbsolutePathUnderWd(path string) (string, error) { | ||
| absPath, err := filepath.Abs(filepath.Clean(path)) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to resolve path %q: %w", path, err) | ||
| } | ||
| wd, err := os.Getwd() | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to get working directory: %w", err) | ||
| } | ||
| wdAbs, err := filepath.Abs(wd) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Getwd already returns absolute path so calling filepath.Abs is redundant |
||
| if err != nil { | ||
| return "", fmt.Errorf("failed to resolve working directory: %w", err) | ||
| } | ||
| rel, err := filepath.Rel(wdAbs, absPath) | ||
| if err != nil || strings.HasPrefix(rel, "..") || rel == ".." { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. strings.HasPrefix(rel, "..") already covers the case of "rel == ".."" so you dont need the ' rel == ".." ' |
||
| return "", fmt.Errorf("path %q is outside project directory", path) | ||
| } | ||
| return absPath, nil | ||
| } | ||
|
|
||
| func GetVulnerabilityLocations(vulnDetails *utils.VulnerabilityDetails, namesFilters []string, ignoreFilters []string) []string { | ||
| pathsSet := datastructures.MakeSet[string]() | ||
| for _, component := range vulnDetails.Components { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -155,7 +155,12 @@ func (npm *NpmPackageUpdater) RegenerateLockfile(vulnDetails *utils.Vulnerabilit | |
|
|
||
| if err = npm.regenerateLockFileWithRetry(); err != nil { | ||
| log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) | ||
| if rollbackErr := os.WriteFile(descriptorPath, backupContent, 0644); rollbackErr != nil { | ||
| safePath, pathErr := getAbsolutePathUnderWd(descriptorPath) | ||
| if pathErr != nil { | ||
| return fmt.Errorf("failed to rollback descriptor: %w (original error: %v)", pathErr, err) | ||
| } | ||
| // #nosec G703 -- path validated under working directory by getAbsolutePathUnderWd | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if rollbackErr := os.WriteFile(safePath, backupContent, 0644); rollbackErr != nil { | ||
| return fmt.Errorf("failed to rollback descriptor after lock file regeneration failure: %w (original error: %v)", rollbackErr, err) | ||
| } | ||
| return err | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -287,8 +287,9 @@ func getSecurityViolationsContent(issues issues.ScansIssuesCollection, writer Ou | |
| if len(issues.ScaViolations) == 0 { | ||
| return []string{} | ||
| } | ||
| content = append(content, getSecurityViolationsSummaryTable(issues.ScaViolations, writer)) | ||
| content = append(content, getScaSecurityIssueDetailsContent(issues.ScaViolations, true, writer)...) | ||
| aggregated := aggregateVulnerabilitiesOrViolationsByCve(issues.ScaViolations) | ||
| content = append(content, getSecurityViolationsSummaryTable(aggregated, writer)) | ||
| content = append(content, getScaSecurityIssueDetailsContent(aggregated, true, writer)...) | ||
| return ConvertContentToComments(content, writer, getDecoratorWithSecurityViolationTitle(writer)) | ||
| } | ||
|
|
||
|
|
@@ -422,12 +423,78 @@ func getScaLicenseViolationDetails(violation formats.LicenseViolationRow, writer | |
|
|
||
| // Sca Vulnerabilities | ||
|
|
||
| func vulnerabilitySummaryKey(v formats.VulnerabilityOrViolationRow) string { | ||
| ids := make([]string, 0, len(v.Cves)) | ||
| for _, cve := range v.Cves { | ||
| ids = append(ids, cve.Id) | ||
| } | ||
| sort.Strings(ids) | ||
| if len(ids) == 0 { | ||
| return v.IssueId | ||
| } | ||
| return strings.Join(ids, ",") | ||
| } | ||
|
|
||
| func uniqueStrings(s []string) []string { | ||
| seen := make(map[string]struct{}, len(s)) | ||
| out := make([]string, 0, len(s)) | ||
| for _, v := range s { | ||
| if _, ok := seen[v]; ok { | ||
| continue | ||
| } | ||
| seen[v] = struct{}{} | ||
| out = append(out, v) | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func aggregateVulnerabilitiesOrViolationsByCve(vulnerabilities []formats.VulnerabilityOrViolationRow) []formats.VulnerabilityOrViolationRow { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The violations aggregation is drops important data when two rows for
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another issue with the violation aggregation is when two violations rows for the same CVE have different impacted dependency versions (e.g., lib:1.5.15 and lib:1.5.1), the merged row will only show lib:1.5.15 in the "Impacted Dependency" column. The second version is silently dropped — it won't appear in the table at all (unlike vulnerabilities where ImpactPaths carries that info implicitly). The Components field (used for "Direct Dependencies" via getDirectDependenciesCellData) has the same problem — it's also only kept from the first occurrence in the aggregation merge step. |
||
| if len(vulnerabilities) == 0 { | ||
| return vulnerabilities | ||
| } | ||
| byKey := make(map[string]*formats.VulnerabilityOrViolationRow) | ||
| for i := range vulnerabilities { | ||
| v := &vulnerabilities[i] | ||
| key := vulnerabilitySummaryKey(*v) | ||
| if existing, ok := byKey[key]; ok { | ||
| existing.ImpactPaths = append(existing.ImpactPaths, v.ImpactPaths...) | ||
| if len(v.FixedVersions) > 0 { | ||
| existing.FixedVersions = uniqueStrings(append(existing.FixedVersions, v.FixedVersions...)) | ||
| } | ||
| } else { | ||
| agg := *v | ||
orto17 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| agg.ImpactPaths = append([][]formats.ComponentRow(nil), v.ImpactPaths...) | ||
| agg.FixedVersions = append([]string(nil), v.FixedVersions...) | ||
| heapCopy := new(formats.VulnerabilityOrViolationRow) | ||
| *heapCopy = agg | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do you need this second copy? this is similar to the copy made here 'agg := *v' |
||
| byKey[key] = heapCopy | ||
| } | ||
| } | ||
| result := make([]formats.VulnerabilityOrViolationRow, 0, len(byKey)) | ||
| for _, v := range byKey { | ||
| result = append(result, *v) | ||
| } | ||
| // Preserve relative order by first occurrence | ||
| order := make(map[string]int) | ||
| for i, v := range vulnerabilities { | ||
| key := vulnerabilitySummaryKey(v) | ||
| if _, ok := order[key]; !ok { | ||
| order[key] = i | ||
| } | ||
| } | ||
| sort.Slice(result, func(i, j int) bool { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The order-preservation pass re-iterates vulnerabilities to build the |
||
| return order[vulnerabilitySummaryKey(result[i])] < order[vulnerabilitySummaryKey(result[j])] | ||
| }) | ||
| return result | ||
| } | ||
|
|
||
| func GetVulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) (content []string) { | ||
| if len(vulnerabilities) == 0 { | ||
| return []string{} | ||
| } | ||
| content = append(content, getVulnerabilitiesSummaryTable(vulnerabilities, writer)) | ||
| content = append(content, getScaSecurityIssueDetailsContent(vulnerabilities, false, writer)...) | ||
| aggregated := aggregateVulnerabilitiesOrViolationsByCve(vulnerabilities) | ||
| content = append(content, getVulnerabilitiesSummaryTable(aggregated, writer)) | ||
| content = append(content, getScaSecurityIssueDetailsContent(aggregated, false, writer)...) | ||
| return ConvertContentToComments(content, writer, getDecoratorWithScaVulnerabilitiesTitle(writer)) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add unit test