Skip to content

Commit 42202f6

Browse files
authored
New transitive decorator (#954)
* new transitive decorator * small fixes * aggregated ca status * delete redundant line * go mod update * (Direct) instead of (D) * Vulnerable Dependencies on the left * cosmetic changes * deleting redundant comments * test fix * fixed tests * after code review * updated getDependencyPathDetailsContent loop * removed details tag loop as it was unnecessary
1 parent fb3f977 commit 42202f6

File tree

2 files changed

+176
-54
lines changed

2 files changed

+176
-54
lines changed

scanrepository/scanrepository_test.go

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"github.com/jfrog/froggit-go/vcsclient"
1919
"github.com/jfrog/froggit-go/vcsutils"
2020
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
21-
"github.com/jfrog/jfrog-cli-security/tests/validations"
2221
"github.com/jfrog/jfrog-cli-security/utils/formats"
2322
"github.com/jfrog/jfrog-cli-security/utils/results"
2423
"github.com/jfrog/jfrog-cli-security/utils/techutils"
@@ -489,42 +488,41 @@ func TestCreateVulnerabilitiesMap(t *testing.T) {
489488
{
490489
name: "Scan results with vulnerabilities and no violations",
491490
scanResults: &results.SecurityCommandResults{
492-
ResultContext: results.ResultContext{IncludeVulnerabilities: true},
491+
ResultsMetaData: results.ResultsMetaData{ResultContext: results.ResultContext{IncludeVulnerabilities: true}},
493492
Targets: []*results.TargetResults{{
494493
ScanTarget: results.ScanTarget{Target: "target1"},
495494
ScaResults: &results.ScaScanResults{
496-
DeprecatedXrayResults: validations.NewMockScaResults(
497-
services.ScanResponse{
498-
Vulnerabilities: []services.Vulnerability{
499-
{
500-
Cves: []services.Cve{
501-
{Id: "CVE-2023-1234", CvssV3Score: "9.1"},
502-
{Id: "CVE-2023-4321", CvssV3Score: "8.9"},
503-
},
504-
Severity: "Critical",
505-
Components: map[string]services.Component{
506-
"vuln1": {
507-
FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"},
508-
ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}}},
509-
},
510-
},
495+
DeprecatedXrayResults: []services.ScanResponse{{
496+
Vulnerabilities: []services.Vulnerability{
497+
{
498+
Cves: []services.Cve{
499+
{Id: "CVE-2023-1234", CvssV3Score: "9.1"},
500+
{Id: "CVE-2023-4321", CvssV3Score: "8.9"},
511501
},
512-
{
513-
Cves: []services.Cve{
514-
{Id: "CVE-2022-1234", CvssV3Score: "7.1"},
515-
{Id: "CVE-2022-4321", CvssV3Score: "7.9"},
502+
Severity: "Critical",
503+
Components: map[string]services.Component{
504+
"vuln1": {
505+
FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"},
506+
ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}}},
516507
},
517-
Severity: "High",
518-
Components: map[string]services.Component{
519-
"vuln2": {
520-
FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"},
521-
ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}, {ComponentId: "vuln2"}}},
522-
},
508+
},
509+
},
510+
{
511+
Cves: []services.Cve{
512+
{Id: "CVE-2022-1234", CvssV3Score: "7.1"},
513+
{Id: "CVE-2022-4321", CvssV3Score: "7.9"},
514+
},
515+
Severity: "High",
516+
Components: map[string]services.Component{
517+
"vuln2": {
518+
FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"},
519+
ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}, {ComponentId: "vuln2"}}},
523520
},
524521
},
525522
},
526523
},
527-
),
524+
},
525+
},
528526
},
529527
JasResults: &results.JasScansResults{},
530528
}},
@@ -544,12 +542,12 @@ func TestCreateVulnerabilitiesMap(t *testing.T) {
544542
{
545543
name: "Scan results with violations and no vulnerabilities",
546544
scanResults: &results.SecurityCommandResults{
547-
ResultContext: results.ResultContext{IncludeVulnerabilities: true, Watches: []string{"w1"}},
545+
ResultsMetaData: results.ResultsMetaData{ResultContext: results.ResultContext{IncludeVulnerabilities: true}},
548546
Targets: []*results.TargetResults{{
549547
ScanTarget: results.ScanTarget{Target: "target1"},
550548
ScaResults: &results.ScaScanResults{
551-
DeprecatedXrayResults: validations.NewMockScaResults(
552-
services.ScanResponse{
549+
DeprecatedXrayResults: []services.ScanResponse{
550+
{
553551
Violations: []services.Violation{
554552
{
555553
ViolationType: "security",
@@ -583,7 +581,7 @@ func TestCreateVulnerabilitiesMap(t *testing.T) {
583581
},
584582
},
585583
},
586-
),
584+
},
587585
},
588586
JasResults: &results.JasScansResults{},
589587
}},

utils/outputwriter/outputcontent.go

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ func GetVulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolatio
433433
if len(vulnerabilities) == 0 {
434434
return []string{}
435435
}
436-
content = append(content, writer.MarkInCenter(getVulnerabilitiesSummaryTable(vulnerabilities, writer)))
436+
content = append(content, getVulnerabilitiesSummaryTable(vulnerabilities, writer))
437437
content = append(content, getScaSecurityIssueDetailsContent(vulnerabilities, false, writer)...)
438438
return ConvertContentToComments(content, writer, getDecoratorWithScaVulnerabilitiesTitle(writer))
439439
}
@@ -454,24 +454,17 @@ func getVulnerabilitiesSummaryTable(vulnerabilities []formats.VulnerabilityOrVio
454454
if writer.IsShowingCaColumn() {
455455
columns = append(columns, "Contextual Analysis")
456456
}
457-
columns = append(columns, "Direct Dependencies", "Impacted Dependency", "Fixed Versions")
457+
columns = append(columns, "Dependency Path")
458458
table := NewMarkdownTable(columns...).SetDelimiter(writer.Separator())
459-
if _, ok := writer.(*SimplifiedOutput); ok {
460-
// The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row.
461-
// It means that the first row will show the full details, and the following rows will show only the direct dependency.
462-
// It makes it easier to read the table and less crowded with text in a single cell that could be potentially large.
463-
table.GetColumnInfo("Direct Dependencies").ColumnType = MultiRowColumn
464-
}
459+
table.GetColumnInfo("Dependency Path").Centered = false
465460
// Construct rows
466461
for _, vulnerability := range vulnerabilities {
467462
row := []CellData{{writer.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)}, getCveIdsCellData(vulnerability.Cves, vulnerability.IssueId)}
468463
if writer.IsShowingCaColumn() {
469464
row = append(row, NewCellData(vulnerability.Applicable))
470465
}
471466
row = append(row,
472-
getDirectDependenciesCellData(vulnerability.Components),
473-
NewCellData(fmt.Sprintf("%s %s", vulnerability.ImpactedDependencyName, vulnerability.ImpactedDependencyVersion)),
474-
NewCellData(vulnerability.FixedVersions...),
467+
getDependencyPathCellData(vulnerability.ImpactPaths, writer),
475468
)
476469
table.AddRowWithCellData(row...)
477470
}
@@ -605,6 +598,83 @@ func getCveIdsCellData(cveRows []formats.CveRow, issueId string) (ids CellData)
605598
return
606599
}
607600

601+
func getFinalApplicabilityStatus(cves []formats.CveRow) string {
602+
if len(cves) == 0 {
603+
return ""
604+
}
605+
606+
statuses := []jasutils.ApplicabilityStatus{}
607+
for _, cve := range cves {
608+
if cve.Applicability != nil && cve.Applicability.Status != "" {
609+
status := jasutils.ConvertToApplicabilityStatus(cve.Applicability.Status)
610+
if status != "" {
611+
statuses = append(statuses, status)
612+
}
613+
}
614+
}
615+
if len(statuses) == 0 {
616+
return ""
617+
}
618+
return results.GetFinalApplicabilityStatus(statuses).String()
619+
}
620+
621+
func getDependencyPathCellData(impactPaths [][]formats.ComponentRow, writer OutputWriter) CellData {
622+
if len(impactPaths) == 0 {
623+
return NewCellData()
624+
}
625+
626+
// key: "name:version"
627+
directDeps := make(map[string]formats.ComponentRow)
628+
transitiveDeps := make(map[string]formats.ComponentRow)
629+
630+
// Extract dependencies from all impact paths
631+
for _, path := range impactPaths {
632+
if len(path) == 2 {
633+
direct := path[1]
634+
key := fmt.Sprintf("%s:%s", direct.Name, direct.Version)
635+
directDeps[key] = direct
636+
637+
} else if len(path) > 2 {
638+
transitive := path[len(path)-1]
639+
key := fmt.Sprintf("%s:%s", transitive.Name, transitive.Version)
640+
transitiveDeps[key] = transitive
641+
}
642+
}
643+
644+
var parts []string
645+
if len(directDeps) > 0 {
646+
directList := make([]string, 0, len(directDeps))
647+
for _, dep := range directDeps {
648+
directList = append(directList, results.GetDependencyId(dep.Name, dep.Version))
649+
}
650+
sort.Strings(directList)
651+
directCount := len(directList)
652+
directContent := strings.Join(directList, writer.Separator())
653+
directSummary := fmt.Sprintf("%d Direct", directCount)
654+
directSection := writer.MarkAsDetails(directSummary, 0, directContent)
655+
parts = append(parts, directSection)
656+
}
657+
658+
if len(transitiveDeps) > 0 {
659+
transitiveList := make([]string, 0, len(transitiveDeps))
660+
for _, dep := range transitiveDeps {
661+
transitiveList = append(transitiveList, results.GetDependencyId(dep.Name, dep.Version))
662+
}
663+
sort.Strings(transitiveList)
664+
transitiveCount := len(transitiveList)
665+
transitiveContent := strings.Join(transitiveList, writer.Separator())
666+
transitiveSummary := fmt.Sprintf("%d Transitive", transitiveCount)
667+
transitiveSection := writer.MarkAsDetails(transitiveSummary, 0, transitiveContent)
668+
parts = append(parts, transitiveSection)
669+
}
670+
671+
if len(parts) == 0 {
672+
return NewCellData()
673+
}
674+
content := strings.Join(parts, "")
675+
return NewCellData(content)
676+
}
677+
608678
func getScaSecurityIssueDetailsContent(issues []formats.VulnerabilityOrViolationRow, violations bool, writer OutputWriter) (content []string) {
609679
issuesWithDetails := getIssuesWithDetails(issues)
610680
if len(issuesWithDetails) == 0 {
@@ -652,16 +722,68 @@ func getComponentIssueIdentifier(key, compName, version, watch string) (id strin
652722
return strings.Join(parts, " ")
653723
}
654724

725+
func getDependencyPathDetailsContent(impactPaths [][]formats.ComponentRow, fixedVersions []string, writer OutputWriter) string {
726+
if len(impactPaths) == 0 {
727+
return ""
728+
}
729+
730+
type packageInfo struct {
731+
component formats.ComponentRow
732+
isDirect bool
733+
}
734+
packages := make(map[string]packageInfo) // key: "name:version"
735+
736+
for _, path := range impactPaths {
737+
if len(path) == 2 {
738+
direct := path[1]
739+
key := fmt.Sprintf("%s:%s", direct.Name, direct.Version)
740+
packages[key] = packageInfo{component: direct, isDirect: true}
741+
} else if len(path) > 2 {
742+
transitive := path[len(path)-1]
743+
key := fmt.Sprintf("%s:%s", transitive.Name, transitive.Version)
744+
packages[key] = packageInfo{component: transitive, isDirect: true}
745+
}
746+
}
747+
748+
if len(packages) == 0 {
749+
return ""
750+
}
751+
752+
var directEntries []string
753+
var transitiveEntries []string
754+
755+
for _, pkgInfo := range packages {
756+
depType := "(Transitive)" // Transitive
757+
if pkgInfo.isDirect {
758+
depType = "(Direct)" // Direct
759+
}
760+
761+
packageSummary := fmt.Sprintf("%s: %s %s", pkgInfo.component.Name, pkgInfo.component.Version, depType)
762+
763+
var packageContentParts []string
764+
if len(fixedVersions) > 0 {
765+
packageContentParts = append(packageContentParts, fmt.Sprintf("Fix Version: %s", fixedVersions[0]))
766+
}
767+
packageContent := strings.Join(packageContentParts, writer.Separator())
768+
packageEntry := writer.MarkAsDetails(packageSummary, 0, packageContent)
769+
770+
if pkgInfo.isDirect {
771+
directEntries = append(directEntries, packageEntry)
772+
} else {
773+
transitiveEntries = append(transitiveEntries, packageEntry)
774+
}
775+
}
776+
sort.Strings(directEntries)
777+
sort.Strings(transitiveEntries)
778+
allEntries := append(directEntries, transitiveEntries...)
779+
780+
return strings.Join(allEntries, "")
781+
}
782+
655783
func getScaSecurityIssueDetails(issue formats.VulnerabilityOrViolationRow, violations bool, writer OutputWriter) (content string) {
656784
var contentBuilder strings.Builder
657-
// Title
658785
WriteNewLine(&contentBuilder)
659786
WriteContent(&contentBuilder, writer.MarkAsTitle(fmt.Sprintf("%s Details", getIssueType(violations)), 3))
660-
// Details Table
661-
directComponent := []string{}
662-
for _, component := range issue.ImpactedDependencyDetails.Components {
663-
directComponent = append(directComponent, results.GetDependencyId(component.Name, component.Version))
664-
}
665787
noHeaderTable := NewNoHeaderMarkdownTable(2, false)
666788
if len(issue.Policies) > 0 {
667789
noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Policies:")), NewCellData(issue.Policies...))
@@ -673,18 +795,20 @@ func getScaSecurityIssueDetails(issue formats.VulnerabilityOrViolationRow, viola
673795
severity := severityutils.Severity(issue.JfrogResearchInformation.Severity)
674796
noHeaderTable.AddRow(MarkAsBold("Jfrog Research Severity:"), fmt.Sprintf("%s %s", writer.SeverityIcon(severity), severity.String()))
675797
}
676-
if issue.Applicable != "" {
677-
noHeaderTable.AddRow(MarkAsBold("Contextual Analysis:"), issue.Applicable)
798+
applicableStatus := getFinalApplicabilityStatus(issue.Cves)
799+
if applicableStatus != "" {
800+
noHeaderTable.AddRow(MarkAsBold("Contextual Analysis:"), applicableStatus)
678801
}
679-
noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Direct Dependencies:")), NewCellData(directComponent...))
680-
noHeaderTable.AddRow(MarkAsBold("Impacted Dependency:"), results.GetDependencyId(issue.ImpactedDependencyName, issue.ImpactedDependencyVersion))
681-
noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Fixed Versions:")), NewCellData(issue.FixedVersions...))
682802

683803
cvss := []string{}
684804
for _, cve := range issue.Cves {
685805
cvss = append(cvss, cve.CvssV3)
686806
}
687807
noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("CVSS V3:")), NewCellData(cvss...))
808+
dependencyPathDetails := getDependencyPathDetailsContent(issue.ImpactPaths, issue.FixedVersions, writer)
809+
if dependencyPathDetails != "" {
810+
noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Dependency Path:")), NewCellData(dependencyPathDetails))
811+
}
688812
WriteContent(&contentBuilder, noHeaderTable.Build())
689813

690814
// Summary

0 commit comments

Comments
 (0)