From b0db3e222bbdb0faf1dddd5626671262024a6621 Mon Sep 17 00:00:00 2001 From: Assaf Attias <49212512+attiasas@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:01:04 +0200 Subject: [PATCH] Audit - Sarif output, show default location for license violation (#1030) --- xray/utils/resultstable.go | 1 + xray/utils/resultwriter.go | 78 +++++++++----- xray/utils/resultwriter_test.go | 184 ++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 27 deletions(-) diff --git a/xray/utils/resultstable.go b/xray/utils/resultstable.go index 0586bf680..43277480d 100644 --- a/xray/utils/resultstable.go +++ b/xray/utils/resultstable.go @@ -801,6 +801,7 @@ func simplifyViolations(scanViolations []services.Violation, multipleRoots bool) continue } uniqueViolations[packageKey] = &services.Violation{ + Summary: violation.Summary, Severity: violation.Severity, ViolationType: violation.ViolationType, Components: map[string]services.Component{vulnerableComponentId: violation.Components[vulnerableComponentId]}, diff --git a/xray/utils/resultwriter.go b/xray/utils/resultwriter.go index 590573ea9..13c258ecd 100644 --- a/xray/utils/resultwriter.go +++ b/xray/utils/resultwriter.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/owenrumney/go-sarif/v2/sarif" + "golang.org/x/exp/slices" ) type OutputFormat string @@ -117,15 +118,7 @@ func (rw *ResultsWriter) PrintScanResults() error { case Json: return PrintJson(rw.results.GetScaScansXrayResults()) case Sarif: - sarifReport, err := GenereateSarifReportFromResults(rw.results, rw.isMultipleRoots, rw.includeLicenses) - if err != nil { - return err - } - sarifFile, err := ConvertSarifReportToString(sarifReport) - if err != nil { - return err - } - log.Output(sarifFile) + return PrintSarif(rw.results, rw.isMultipleRoots, rw.includeLicenses) } return nil } @@ -175,12 +168,12 @@ func printMessage(message string) { log.Output("💬" + message) } -func GenereateSarifReportFromResults(results *Results, isMultipleRoots, includeLicenses bool) (report *sarif.Report, err error) { +func GenereateSarifReportFromResults(results *Results, isMultipleRoots, includeLicenses bool, allowedLicenses []string) (report *sarif.Report, err error) { report, err = NewReport() if err != nil { return } - xrayRun, err := convertXrayResponsesToSarifRun(results, isMultipleRoots, includeLicenses) + xrayRun, err := convertXrayResponsesToSarifRun(results, isMultipleRoots, includeLicenses, allowedLicenses) if err != nil { return } @@ -202,14 +195,14 @@ func ConvertSarifReportToString(report *sarif.Report) (sarifStr string, err erro return clientUtils.IndentJson(out), nil } -func convertXrayResponsesToSarifRun(results *Results, isMultipleRoots, includeLicenses bool) (run *sarif.Run, err error) { - xrayJson, err := convertXrayScanToSimpleJson(results, isMultipleRoots, includeLicenses, true) +func convertXrayResponsesToSarifRun(results *Results, isMultipleRoots, includeLicenses bool, allowedLicenses []string) (run *sarif.Run, err error) { + xrayJson, err := ConvertXrayScanToSimpleJson(results, isMultipleRoots, includeLicenses, true, allowedLicenses) if err != nil { return } xrayRun := sarif.NewRunWithInformationURI("JFrog Xray SCA", BaseDocumentationURL+"sca") xrayRun.Tool.Driver.Version = &results.XrayVersion - if len(xrayJson.Vulnerabilities) > 0 || len(xrayJson.SecurityViolations) > 0 { + if len(xrayJson.Vulnerabilities) > 0 || len(xrayJson.SecurityViolations) > 0 || len(xrayJson.LicensesViolations) > 0 { if err = extractXrayIssuesToSarifRun(xrayRun, xrayJson); err != nil { return } @@ -283,7 +276,7 @@ func addXrayLicenseViolationToSarifRun(license formats.LicenseRow, run *sarif.Ru getXrayLicenseSarifHeadline(license.ImpactedDependencyName, license.ImpactedDependencyVersion, license.LicenseKey), getLicenseViolationMarkdown(license.ImpactedDependencyName, license.ImpactedDependencyVersion, license.LicenseKey, formattedDirectDependencies), license.Components, - nil, + getXrayIssueLocation(""), run, ) return @@ -329,10 +322,14 @@ func getXrayIssueLocationIfValidExists(tech coreutils.Technology, run *sarif.Run if err != nil { return } - if strings.TrimSpace(descriptorPath) == "" { - descriptorPath = "Package-Descriptor" + return getXrayIssueLocation(descriptorPath), nil +} + +func getXrayIssueLocation(filePath string) *sarif.Location { + if strings.TrimSpace(filePath) == "" { + filePath = "Package-Descriptor" } - return sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + descriptorPath))), nil + return sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + filePath))) } func addXrayRule(ruleId, ruleDescription, maxCveScore, summary, markdownDescription string, run *sarif.Run) { @@ -351,7 +348,7 @@ func addXrayRule(ruleId, ruleDescription, maxCveScore, summary, markdownDescript }) } -func convertXrayScanToSimpleJson(results *Results, isMultipleRoots, includeLicenses, simplifiedOutput bool) (formats.SimpleJsonResults, error) { +func ConvertXrayScanToSimpleJson(results *Results, isMultipleRoots, includeLicenses, simplifiedOutput bool, allowedLicenses []string) (formats.SimpleJsonResults, error) { violations, vulnerabilities, licenses := SplitScanResults(results.ScaResults) jsonTable := formats.SimpleJsonResults{} if len(vulnerabilities) > 0 { @@ -361,6 +358,16 @@ func convertXrayScanToSimpleJson(results *Results, isMultipleRoots, includeLicen } jsonTable.Vulnerabilities = vulJsonTable } + if includeLicenses || len(allowedLicenses) > 0 { + licJsonTable, err := PrepareLicenses(licenses) + if err != nil { + return formats.SimpleJsonResults{}, err + } + if includeLicenses { + jsonTable.Licenses = licJsonTable + } + jsonTable.LicensesViolations = GetViolatedLicenses(allowedLicenses, licJsonTable) + } if len(violations) > 0 { secViolationsJsonTable, licViolationsJsonTable, opRiskViolationsJsonTable, err := PrepareViolations(violations, results, isMultipleRoots, simplifiedOutput) if err != nil { @@ -370,19 +377,23 @@ func convertXrayScanToSimpleJson(results *Results, isMultipleRoots, includeLicen jsonTable.LicensesViolations = licViolationsJsonTable jsonTable.OperationalRiskViolations = opRiskViolationsJsonTable } - if includeLicenses { - licJsonTable, err := PrepareLicenses(licenses) - if err != nil { - return formats.SimpleJsonResults{}, err + return jsonTable, nil +} + +func GetViolatedLicenses(allowedLicenses []string, licenses []formats.LicenseRow) (violatedLicenses []formats.LicenseRow) { + if len(allowedLicenses) == 0 { + return + } + for _, license := range licenses { + if !slices.Contains(allowedLicenses, license.LicenseKey) { + violatedLicenses = append(violatedLicenses, license) } - jsonTable.Licenses = licJsonTable } - - return jsonTable, nil + return } func (rw *ResultsWriter) convertScanToSimpleJson() (formats.SimpleJsonResults, error) { - jsonTable, err := convertXrayScanToSimpleJson(rw.results, rw.isMultipleRoots, rw.includeLicenses, false) + jsonTable, err := ConvertXrayScanToSimpleJson(rw.results, rw.isMultipleRoots, rw.includeLicenses, false, nil) if err != nil { return formats.SimpleJsonResults{}, err } @@ -534,6 +545,19 @@ func PrintJson(output interface{}) error { return nil } +func PrintSarif(results *Results, isMultipleRoots, includeLicenses bool) error { + sarifReport, err := GenereateSarifReportFromResults(results, isMultipleRoots, includeLicenses, nil) + if err != nil { + return err + } + sarifFile, err := ConvertSarifReportToString(sarifReport) + if err != nil { + return err + } + log.Output(sarifFile) + return nil +} + func CheckIfFailBuild(results []services.ScanResponse) bool { for _, result := range results { for _, violation := range result.Violations { diff --git a/xray/utils/resultwriter_test.go b/xray/utils/resultwriter_test.go index 387aa4f64..7c2b738e5 100644 --- a/xray/utils/resultwriter_test.go +++ b/xray/utils/resultwriter_test.go @@ -8,6 +8,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-cli-core/v2/xray/formats" + "github.com/jfrog/jfrog-client-go/xray/services" "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" ) @@ -193,3 +194,186 @@ func TestGetXrayIssueLocationIfValidExists(t *testing.T) { }) } } + +func TestConvertXrayScanToSimpleJson(t *testing.T) { + vulnerabilities := []services.Vulnerability{ + { + IssueId: "XRAY-1", + Summary: "summary-1", + Severity: "high", + Components: map[string]services.Component{"component-A": {}, "component-B": {}}, + }, + { + IssueId: "XRAY-2", + Summary: "summary-2", + Severity: "low", + Components: map[string]services.Component{"component-B": {}}, + }, + } + expectedVulnerabilities := []formats.VulnerabilityOrViolationRow{ + { + Summary: "summary-1", + IssueId: "XRAY-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "high"}, + ImpactedDependencyName: "component-A", + }, + }, + { + Summary: "summary-1", + IssueId: "XRAY-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "high"}, + ImpactedDependencyName: "component-B", + }, + }, + { + Summary: "summary-2", + IssueId: "XRAY-2", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "low"}, + ImpactedDependencyName: "component-B", + }, + }, + } + + violations := []services.Violation{ + { + IssueId: "XRAY-1", + Summary: "summary-1", + Severity: "high", + WatchName: "watch-1", + ViolationType: "security", + Components: map[string]services.Component{"component-A": {}, "component-B": {}}, + }, + { + IssueId: "XRAY-2", + Summary: "summary-2", + Severity: "low", + WatchName: "watch-1", + ViolationType: "license", + LicenseKey: "license-1", + Components: map[string]services.Component{"component-B": {}}, + }, + } + expectedSecViolations := []formats.VulnerabilityOrViolationRow{ + { + Summary: "summary-1", + IssueId: "XRAY-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "high"}, + ImpactedDependencyName: "component-A", + }, + }, + { + Summary: "summary-1", + IssueId: "XRAY-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "high"}, + ImpactedDependencyName: "component-B", + }, + }, + } + expectedLicViolations := []formats.LicenseRow{ + { + LicenseKey: "license-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "low"}, + ImpactedDependencyName: "component-B", + }, + }, + } + + licenses := []services.License{ + { + Key: "license-1", + Name: "license-1-name", + Components: map[string]services.Component{"component-A": {}, "component-B": {}}, + }, + { + Key: "license-2", + Name: "license-2-name", + Components: map[string]services.Component{"component-B": {}}, + }, + } + expectedLicenses := []formats.LicenseRow{ + { + LicenseKey: "license-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-A"}, + }, + { + LicenseKey: "license-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-B"}, + }, + { + LicenseKey: "license-2", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-B"}, + }, + } + + testCases := []struct { + name string + result services.ScanResponse + includeLicenses bool + allowedLicenses []string + expectedOutput formats.SimpleJsonResults + }{ + { + name: "Vulnerabilities only", + includeLicenses: false, + allowedLicenses: nil, + result: services.ScanResponse{Vulnerabilities: vulnerabilities, Licenses: licenses}, + expectedOutput: formats.SimpleJsonResults{Vulnerabilities: expectedVulnerabilities}, + }, + { + name: "Vulnerabilities with licenses", + includeLicenses: true, + allowedLicenses: nil, + result: services.ScanResponse{Vulnerabilities: vulnerabilities, Licenses: licenses}, + expectedOutput: formats.SimpleJsonResults{Vulnerabilities: expectedVulnerabilities, Licenses: expectedLicenses}, + }, + { + name: "Vulnerabilities only - with allowed licenses", + includeLicenses: false, + allowedLicenses: []string{"license-1"}, + result: services.ScanResponse{Vulnerabilities: vulnerabilities, Licenses: licenses}, + expectedOutput: formats.SimpleJsonResults{ + Vulnerabilities: expectedVulnerabilities, + LicensesViolations: []formats.LicenseRow{ + { + LicenseKey: "license-2", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-B"}, + }, + }, + }, + }, + { + name: "Violations only", + includeLicenses: false, + allowedLicenses: nil, + result: services.ScanResponse{Violations: violations, Licenses: licenses}, + expectedOutput: formats.SimpleJsonResults{SecurityViolations: expectedSecViolations, LicensesViolations: expectedLicViolations}, + }, + { + name: "Violations - override allowed licenses", + includeLicenses: false, + allowedLicenses: []string{"license-1"}, + result: services.ScanResponse{Violations: violations, Licenses: licenses}, + expectedOutput: formats.SimpleJsonResults{SecurityViolations: expectedSecViolations, LicensesViolations: expectedLicViolations}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + results := NewAuditResults() + results.ScaResults = append(results.ScaResults, ScaScanResult{XrayResults: []services.ScanResponse{tc.result}}) + output, err := ConvertXrayScanToSimpleJson(results, false, tc.includeLicenses, true, tc.allowedLicenses) + if assert.NoError(t, err) { + assert.ElementsMatch(t, tc.expectedOutput.Vulnerabilities, output.Vulnerabilities) + assert.ElementsMatch(t, tc.expectedOutput.SecurityViolations, output.SecurityViolations) + assert.ElementsMatch(t, tc.expectedOutput.LicensesViolations, output.LicensesViolations) + assert.ElementsMatch(t, tc.expectedOutput.Licenses, output.Licenses) + assert.ElementsMatch(t, tc.expectedOutput.OperationalRiskViolations, output.OperationalRiskViolations) + } + }) + } +}