diff --git a/policy/enforcer/policyenforcer.go b/policy/enforcer/policyenforcer.go index 43c68cb61..6970c02d7 100644 --- a/policy/enforcer/policyenforcer.go +++ b/policy/enforcer/policyenforcer.go @@ -193,11 +193,8 @@ func convertToScaViolation(cmdResults *results.SecurityCommandResults, impactedC scaViolation = violationutils.ScaViolation{ Violation: convertToBasicViolation(getScaViolationType(violation), violation), } - affectedComponent, scaViolation.DirectComponents, scaViolation.ImpactPaths = locateBomComponentInfo(cmdResults, impactedComponentXrayId, violation) - if affectedComponent == nil { - return - } - scaViolation.ImpactedComponent = *affectedComponent + scaViolation.ImpactedComponent, scaViolation.DirectComponents, scaViolation.ImpactPaths = locateBomComponentInfo(cmdResults, impactedComponentXrayId, violation) + affectedComponent = scaViolation.ImpactedComponent return } @@ -251,25 +248,33 @@ func locateBomComponentInfo(cmdResults *results.SecurityCommandResults, impacted return } -func locateBomVulnerabilityInfo(cmdResults *results.SecurityCommandResults, issueId string, impactedComponent cyclonedx.Component) (relevantVulnerability *cyclonedx.Vulnerability, contextualAnalysis *formats.Applicability) { +// locateBomVulnerabilityInfo finds a CycloneDX vulnerability in scan results by issue/CVE id. +// When impactedComponent is nil, only vulnerabilities with empty Affects are matched. +// If the BOM lists Affects but Xray omits InfectedComponentIds, conversion still fails (returns nil). +func locateBomVulnerabilityInfo(cmdResults *results.SecurityCommandResults, issueId string, impactedComponent *cyclonedx.Component) (relevantVulnerability *cyclonedx.Vulnerability, contextualAnalysis *formats.Applicability) { for _, target := range cmdResults.Targets { if target.ScaResults == nil || target.ScaResults.Sbom == nil || target.ScaResults.Sbom.Vulnerabilities == nil { continue } for _, vulnerability := range *target.ScaResults.Sbom.Vulnerabilities { - if vulnerability.ID != issueId || vulnerability.Affects == nil || len(*vulnerability.Affects) == 0 { + if vulnerability.ID != issueId { continue } - for _, affected := range *vulnerability.Affects { - if affected.Ref == impactedComponent.BOMRef { - // Found the relevant component in a vulnerability - relevantVulnerability = &vulnerability - contextualAnalysis = results.GetCveApplicabilityField(vulnerability.BOMRef, target.JasResults.GetApplicabilityScanResults()) - break + if impactedComponent != nil && vulnerability.Affects != nil { + for _, affected := range *vulnerability.Affects { + if affected.Ref == impactedComponent.BOMRef { + // Found the relevant component in a vulnerability + relevantVulnerability = &vulnerability + break + } } + } else if vulnerability.Affects == nil || len(*vulnerability.Affects) == 0 { + // No impacted component, use the first vulnerability that matches the issue ID + relevantVulnerability = &vulnerability } if relevantVulnerability != nil { // Found the relevant vulnerability, no need to continue searching + contextualAnalysis = results.GetCveApplicabilityField(vulnerability.BOMRef, target.JasResults.GetApplicabilityScanResults()) break } } @@ -397,78 +402,120 @@ func convertToBasicViolation(violationType violationutils.ViolationIssueType, vi } func convertToCveViolations(cmdResults *results.SecurityCommandResults, violation services.XrayViolation) (cveViolations []violationutils.CveViolation) { - for _, infectedComponentXrayId := range violation.InfectedComponentIds { - if infectedComponentXrayId == "" { - log.Warn(fmt.Sprintf("Skipping CVE violation with empty infected component ID for violation ID %s", violation.Id)) + for _, cve := range violation.Cves { + if cve.Id == "" { + log.Warn(fmt.Sprintf("Skipping CVE violation with empty CVE ID for violation ID %s", violation.Id)) continue } - affectedComponent, scaViolation := convertToScaViolation(cmdResults, infectedComponentXrayId, violation) - if affectedComponent == nil { - log.Warn(fmt.Sprintf("Skipping CVE violation with no located affected component for violation ID %s and infected component ID %s", violation.Id, infectedComponentXrayId)) - continue - } - for _, cve := range violation.Cves { - if cve.Id == "" { - log.Warn(fmt.Sprintf("Skipping CVE violation with empty CVE ID for violation ID %s", violation.Id)) + actualInfectedComponentIds := getInfectedComponentIds(cmdResults, violation) + if len(actualInfectedComponentIds) == 0 { + // No affected components, create a violation without an affected component + cveViolation := createCveViolation(cmdResults, "", cve.Id, violation) + if cveViolation == nil { + log.Warn(fmt.Sprintf("CVE violation with no located affected components for violation ID %s", violation.Id)) continue } - vulnerability, contextualAnalysis := locateBomVulnerabilityInfo(cmdResults, cve.Id, *affectedComponent) - if vulnerability == nil { - log.Warn(fmt.Sprintf("Skipping CVE violation with no located vulnerability for CVE ID %s, violation ID %s and infected component ID %s", cve.Id, violation.Id, infectedComponentXrayId)) + cveViolations = append(cveViolations, *cveViolation) + } + // Create a violation for each affected component + for _, infectedComponentXrayId := range actualInfectedComponentIds { + cveViolation := createCveViolation(cmdResults, infectedComponentXrayId, cve.Id, violation) + if cveViolation == nil { + log.Warn(fmt.Sprintf("CVE violation with no located affected components for violation ID %s", violation.Id)) continue } - cveViolation := violationutils.CveViolation{ - ScaViolation: scaViolation, - CveVulnerability: *vulnerability, - ContextualAnalysis: contextualAnalysis, - FixedVersions: cdxutils.ConvertToAffectedVersions(*affectedComponent, violation.FixVersions), - JfrogResearchInformation: results.ConvertJfrogResearchInformation(violation.JfrogResearchInformation), - } - cveViolations = append(cveViolations, cveViolation) + cveViolations = append(cveViolations, *cveViolation) } } return cveViolations } -func convertToLicenseViolations(cmdResults *results.SecurityCommandResults, violation services.XrayViolation) (licenseViolations []violationutils.LicenseViolation) { +func getInfectedComponentIds(cmdResults *results.SecurityCommandResults, violation services.XrayViolation) []string { + actualInfectedComponentIds := []string{} for _, infectedComponentXrayId := range violation.InfectedComponentIds { if infectedComponentXrayId == "" { - log.Verbose(fmt.Sprintf("Skipping license violation with empty infected component ID for violation ID %s", violation.Id)) + log.Warn(fmt.Sprintf("Skipping violation with empty infected component ID for violation ID %s", violation.Id)) continue } - _, scaViolation := convertToScaViolation(cmdResults, infectedComponentXrayId, violation) - licenseViolation := violationutils.LicenseViolation{ - ScaViolation: scaViolation, - LicenseKey: violation.IssueId, - LicenseName: violation.Description, + if affectedComponent, _, _ := locateBomComponentInfo(cmdResults, infectedComponentXrayId, violation); affectedComponent == nil { + log.Warn(fmt.Sprintf("Skipping violation with no located affected component for violation ID %s and infected component ID %s", violation.Id, infectedComponentXrayId)) + continue } - licenseViolations = append(licenseViolations, licenseViolation) + actualInfectedComponentIds = append(actualInfectedComponentIds, infectedComponentXrayId) + } + return actualInfectedComponentIds +} + +func createCveViolation(cmdResults *results.SecurityCommandResults, impactedComponentXrayId, cveId string, violation services.XrayViolation) *violationutils.CveViolation { + affectedComponent, scaViolation := convertToScaViolation(cmdResults, impactedComponentXrayId, violation) + vulnerability, contextualAnalysis := locateBomVulnerabilityInfo(cmdResults, cveId, affectedComponent) + if vulnerability == nil { + log.Warn(fmt.Sprintf("Skipping CVE violation with no located vulnerability for CVE ID %s, violation ID %s and infected component ID %s", cveId, violation.Id, impactedComponentXrayId)) + return nil + } + var fixedVersions *[]cyclonedx.AffectedVersions + if affectedComponent != nil { + fixedVersions = cdxutils.ConvertToAffectedVersions(*affectedComponent, violation.FixVersions) + } + cveViolation := violationutils.CveViolation{ + ScaViolation: scaViolation, + CveVulnerability: *vulnerability, + ContextualAnalysis: contextualAnalysis, + FixedVersions: fixedVersions, + JfrogResearchInformation: results.ConvertJfrogResearchInformation(violation.JfrogResearchInformation), + } + return &cveViolation +} + +func convertToLicenseViolations(cmdResults *results.SecurityCommandResults, violation services.XrayViolation) (licenseViolations []violationutils.LicenseViolation) { + if violation.IssueId == "" { + log.Warn(fmt.Sprintf("Skipping license violation with empty issue ID for violation ID %s", violation.Id)) + } + actualInfectedComponentIds := getInfectedComponentIds(cmdResults, violation) + if len(actualInfectedComponentIds) == 0 { + // No affected components, create a violation without an affected component + licenseViolations = append(licenseViolations, createLicenseViolation(cmdResults, "", violation)) + } + for _, infectedComponentXrayId := range actualInfectedComponentIds { + licenseViolations = append(licenseViolations, createLicenseViolation(cmdResults, infectedComponentXrayId, violation)) } return licenseViolations } -func convertToOpRiskViolations(cmdResults *results.SecurityCommandResults, violation services.XrayViolation) (opRiskViolations []violationutils.OperationalRiskViolation) { - for _, infectedComponentXrayId := range violation.InfectedComponentIds { - if infectedComponentXrayId == "" { - log.Verbose(fmt.Sprintf("Skipping operational risk violation with empty infected component ID for violation ID %s", violation.Id)) - continue - } - _, scaViolation := convertToScaViolation(cmdResults, infectedComponentXrayId, violation) - opRiskViolation := violationutils.OperationalRiskViolation{ - ScaViolation: scaViolation, - OperationalRiskViolationReadableData: violationutils.GetOperationalRiskViolationReadableData( - violation.OperationalRisk.RiskReason, - violation.OperationalRisk.IsEol, - violation.OperationalRisk.EolMessage, - violation.OperationalRisk.Cadence, - violation.OperationalRisk.Commits, - violation.OperationalRisk.Committers, - violation.OperationalRisk.LatestVersion, - violation.OperationalRisk.NewerVersions, - ), - } - opRiskViolations = append(opRiskViolations, opRiskViolation) +func createLicenseViolation(cmdResults *results.SecurityCommandResults, impactedComponentXrayId string, violation services.XrayViolation) violationutils.LicenseViolation { + _, scaViolation := convertToScaViolation(cmdResults, impactedComponentXrayId, violation) + return violationutils.LicenseViolation{ + ScaViolation: scaViolation, + LicenseKey: violation.IssueId, + LicenseName: violation.Description, } +} +func convertToOpRiskViolations(cmdResults *results.SecurityCommandResults, violation services.XrayViolation) (opRiskViolations []violationutils.OperationalRiskViolation) { + actualInfectedComponentIds := getInfectedComponentIds(cmdResults, violation) + if len(actualInfectedComponentIds) == 0 { + // No affected components, create a violation without an affected component + opRiskViolations = append(opRiskViolations, createOpRiskViolation(cmdResults, "", violation)) + } + for _, infectedComponentXrayId := range actualInfectedComponentIds { + opRiskViolations = append(opRiskViolations, createOpRiskViolation(cmdResults, infectedComponentXrayId, violation)) + } return opRiskViolations } + +func createOpRiskViolation(cmdResults *results.SecurityCommandResults, impactedComponentXrayId string, violation services.XrayViolation) violationutils.OperationalRiskViolation { + _, scaViolation := convertToScaViolation(cmdResults, impactedComponentXrayId, violation) + return violationutils.OperationalRiskViolation{ + ScaViolation: scaViolation, + OperationalRiskViolationReadableData: violationutils.GetOperationalRiskViolationReadableData( + violation.OperationalRisk.RiskReason, + violation.OperationalRisk.IsEol, + violation.OperationalRisk.EolMessage, + violation.OperationalRisk.Cadence, + violation.OperationalRisk.Commits, + violation.OperationalRisk.Committers, + violation.OperationalRisk.LatestVersion, + violation.OperationalRisk.NewerVersions, + ), + } +} diff --git a/policy/enforcer/policyenforcer_test.go b/policy/enforcer/policyenforcer_test.go new file mode 100644 index 000000000..d8aa0f0e4 --- /dev/null +++ b/policy/enforcer/policyenforcer_test.go @@ -0,0 +1,96 @@ +package enforcer + +import ( + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-client-go/xray/services" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" +) + +func TestConvertToCveViolations_withoutImpactedComponent(t *testing.T) { + cveId := "CVE-2024-no-component" + bom := &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{}, + Vulnerabilities: &[]cyclonedx.Vulnerability{{ + ID: cveId, + BOMRef: "vuln-ref-no-affects", + }}, + } + cmdResults := results.NewCommandResults(utils.SourceCode) + target := cmdResults.NewScanResults(results.ScanTarget{Target: "target"}) + target.SetSbom(bom) + + xrayViolation := services.XrayViolation{ + Id: "violation-1", + Type: xrayUtils.SecurityViolation, + Severity: "High", + Cves: []services.CveDetails{{Id: cveId}}, + InfectedComponentIds: []string{}, + } + + got := convertToCveViolations(cmdResults, xrayViolation) + require.Len(t, got, 1) + assert.Nil(t, got[0].ImpactedComponent) + assert.Equal(t, cveId, got[0].CveVulnerability.ID) +} + +func TestConvertToCveViolations_skippedWhenBomHasAffectsButNoComponentId(t *testing.T) { + cveId := "CVE-2024-mismatch" + componentRef := "pkg:golang/example@1.0.0" + bom := &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{{ + BOMRef: componentRef, + PackageURL: componentRef, + Type: cyclonedx.ComponentTypeLibrary, + }}, + Vulnerabilities: &[]cyclonedx.Vulnerability{{ + ID: cveId, + BOMRef: "vuln-with-affects", + Affects: &[]cyclonedx.Affects{{ + Ref: componentRef, + }}, + }}, + } + cmdResults := results.NewCommandResults(utils.SourceCode) + target := cmdResults.NewScanResults(results.ScanTarget{Target: "target"}) + target.SetSbom(bom) + + xrayViolation := services.XrayViolation{ + Id: "violation-2", + Type: xrayUtils.SecurityViolation, + Severity: "High", + Cves: []services.CveDetails{{Id: cveId}}, + InfectedComponentIds: []string{}, + } + + got := convertToCveViolations(cmdResults, xrayViolation) + assert.Empty(t, got, "BOM vulnerability has Affects but Xray sent no component ids — createCveViolation returns nil") +} + +func TestConvertToLicenseViolations_withoutImpactedComponent(t *testing.T) { + cmdResults := results.NewCommandResults(utils.SourceCode) + cmdResults.NewScanResults(results.ScanTarget{Target: "target"}).SetSbom(&cyclonedx.BOM{ + Components: &[]cyclonedx.Component{}, + Vulnerabilities: &[]cyclonedx.Vulnerability{}, + }) + + xrayViolation := services.XrayViolation{ + Id: "license-vio-1", + Type: xrayUtils.LicenseViolation, + Severity: "Medium", + IssueId: "GPL-3.0", + Description: "GPL license issue", + InfectedComponentIds: []string{}, + } + + got := convertToLicenseViolations(cmdResults, xrayViolation) + require.Len(t, got, 1) + assert.Nil(t, got[0].ImpactedComponent) + assert.Equal(t, "GPL-3.0", got[0].LicenseKey) +} diff --git a/policy/local/local_foreach_test.go b/policy/local/local_foreach_test.go new file mode 100644 index 000000000..d51232fa7 --- /dev/null +++ b/policy/local/local_foreach_test.go @@ -0,0 +1,43 @@ +package local + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/formats/violationutils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-client-go/xray/services" +) + +func TestForEachScanGraphViolation_emptyComponents(t *testing.T) { + violation := services.Violation{ + IssueId: "XRAY-iter-empty", + Severity: "Low", + WatchName: "watch", + ViolationType: violationutils.ScaViolationTypeSecurity.String(), + Cves: []services.Cve{{Id: "CVE-iter-empty"}}, + Components: map[string]services.Component{}, + } + var securityCalls int + _, _, err := ForEachScanGraphViolation( + results.ScanTarget{Target: "."}, + []string{}, + []services.Violation{violation}, + false, + nil, + func(_ services.Violation, _ []formats.CveRow, _ jasutils.ApplicabilityStatus, _ severityutils.Severity, impactedPackagesId string, _ []string, _ []formats.ComponentRow, _ [][]formats.ComponentRow) error { + securityCalls++ + assert.Empty(t, impactedPackagesId) + return nil + }, + nil, + nil, + ) + require.NoError(t, err) + assert.Equal(t, 1, securityCalls) +} diff --git a/policy/local/localconvertor.go b/policy/local/localconvertor.go index 874c79dc2..d1c4b569d 100644 --- a/policy/local/localconvertor.go +++ b/policy/local/localconvertor.go @@ -219,7 +219,7 @@ func getScaViolationType(violation services.Violation) violationutils.ViolationI return "" } -func convertToScaViolation(violation services.Violation, severity severityutils.Severity, affectedComponent cyclonedx.Component, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) violationutils.ScaViolation { +func convertToScaViolation(violation services.Violation, severity severityutils.Severity, affectedComponent *cyclonedx.Component, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) violationutils.ScaViolation { return violationutils.ScaViolation{ Violation: convertToBasicViolation(violation, severity), ImpactedComponent: affectedComponent, @@ -231,8 +231,13 @@ func convertToScaViolation(violation services.Violation, severity severityutils. func convertScaSecurityViolationToPolicyViolation(convertedViolations *violationutils.Violations) ParseScanGraphViolationFunc { xrayService := results.GetXrayService() return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesId string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) (err error) { - // Create the CycloneDX component for the impacted package - affectedComponent := results.CreateScaComponentFromXrayCompId(impactedPackagesId) + var affectedComponent *cyclonedx.Component + var fixedVersions *[]cyclonedx.AffectedVersions + if impactedPackagesId != "" { + // Create the CycloneDX component for the impacted package + component := results.CreateScaComponentFromXrayCompId(impactedPackagesId) + affectedComponent = &component + } // Extract the vulnerability CVE's information and create the SCA vulnerability for each cveIds, applicability, cwes, ratings := results.ExtractIssuesInfoForCdx(violation.IssueId, cves, severity, applicabilityStatus, xrayService) extendedInformation := "" @@ -250,15 +255,18 @@ func convertScaSecurityViolationToPolicyViolation(convertedViolations *violation References: violation.References, Service: xrayService, }) - // Attach the affected impacted library component to the vulnerability - cdxutils.AttachComponentAffects(&vulnerability, affectedComponent, func(affectedComponent cyclonedx.Component) cyclonedx.Affects { - return cdxutils.CreateScaImpactedAffects(affectedComponent, fixedVersion) - }) + if affectedComponent != nil { + // Attach the affected impacted library component to the vulnerability + cdxutils.AttachComponentAffects(&vulnerability, *affectedComponent, func(affectedComponent cyclonedx.Component) cyclonedx.Affects { + return cdxutils.CreateScaImpactedAffects(affectedComponent, fixedVersion) + }) + fixedVersions = cdxutils.ConvertToAffectedVersions(*affectedComponent, fixedVersion) + } convertedViolations.Sca = append(convertedViolations.Sca, violationutils.CveViolation{ ScaViolation: convertToScaViolation(violation, severity, affectedComponent, directComponents, impactPaths), CveVulnerability: vulnerability, ContextualAnalysis: applicability[i], - FixedVersions: cdxutils.ConvertToAffectedVersions(affectedComponent, fixedVersion), + FixedVersions: fixedVersions, JfrogResearchInformation: results.ConvertJfrogResearchInformation(violation.ExtendedInformation), }) } @@ -268,8 +276,12 @@ func convertScaSecurityViolationToPolicyViolation(convertedViolations *violation func convertScaLicenseViolationToPolicyViolation(convertedViolations *violationutils.Violations) ParseScanGraphViolationFunc { return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesId string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) (err error) { - // Create the CycloneDX component for the impacted package - affectedComponent := results.CreateScaComponentFromXrayCompId(impactedPackagesId) + var affectedComponent *cyclonedx.Component + if impactedPackagesId != "" { + // Create the CycloneDX component for the impacted package + component := results.CreateScaComponentFromXrayCompId(impactedPackagesId) + affectedComponent = &component + } // Add the license violation convertedViolations.License = append(convertedViolations.License, violationutils.LicenseViolation{ ScaViolation: convertToScaViolation(violation, severity, affectedComponent, directComponents, impactPaths), @@ -282,8 +294,12 @@ func convertScaLicenseViolationToPolicyViolation(convertedViolations *violationu func convertOperationalRiskViolationToPolicyViolation(convertedViolations *violationutils.Violations) ParseScanGraphViolationFunc { return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesId string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) (err error) { - // Create the CycloneDX component for the impacted package - affectedComponent := results.CreateScaComponentFromXrayCompId(impactedPackagesId) + var affectedComponent *cyclonedx.Component + if impactedPackagesId != "" { + // Create the CycloneDX component for the impacted package + component := results.CreateScaComponentFromXrayCompId(impactedPackagesId) + affectedComponent = &component + } // Add the operational risk violation convertedViolations.OpRisk = append(convertedViolations.OpRisk, violationutils.OperationalRiskViolation{ ScaViolation: convertToScaViolation(violation, severity, affectedComponent, directComponents, impactPaths), @@ -331,6 +347,13 @@ func ForEachScanGraphViolation(target results.ScanTarget, descriptors []string, // No handler was provided for security violations continue } + if len(impactedPackagesIds) == 0 { + // Security violation without any impacted packages, we pass an empty string as the impacted package ID + if e := securityHandler(violation, cves, applicabilityStatus, severity, "", []string{}, []formats.ComponentRow{}, [][]formats.ComponentRow{}); e != nil { + err = errors.Join(err, e) + continue + } + } for compIndex := 0; compIndex < len(impactedPackagesIds); compIndex++ { if e := securityHandler(violation, cves, applicabilityStatus, severity, impactedPackagesIds[compIndex], fixedVersions[compIndex], directComponents[compIndex], impactPaths[compIndex]); e != nil { err = errors.Join(err, e) @@ -342,6 +365,13 @@ func ForEachScanGraphViolation(target results.ScanTarget, descriptors []string, // No handler was provided for license violations continue } + if len(impactedPackagesIds) == 0 { + // License violation without any impacted packages, we pass an empty string as the impacted package ID + if e := licenseHandler(violation, cves, applicabilityStatus, severity, "", []string{}, []formats.ComponentRow{}, [][]formats.ComponentRow{}); e != nil { + err = errors.Join(err, e) + continue + } + } for compIndex := range impactedPackagesIds { if impactedPackagesName, _, _ := techutils.SplitComponentId(impactedPackagesIds[compIndex]); impactedPackagesName == "root" { // No Need to output 'root' as impacted package for license since we add this as the root node for the scan @@ -357,6 +387,13 @@ func ForEachScanGraphViolation(target results.ScanTarget, descriptors []string, // No handler was provided for operational risk violations continue } + if len(impactedPackagesIds) == 0 { + // Operational risk violation without any impacted packages, we pass an empty string as the impacted package ID + if e := operationalRiskHandler(violation, cves, applicabilityStatus, severity, "", []string{}, []formats.ComponentRow{}, [][]formats.ComponentRow{}); e != nil { + err = errors.Join(err, e) + continue + } + } for compIndex := range impactedPackagesIds { if e := operationalRiskHandler(violation, cves, applicabilityStatus, severity, impactedPackagesIds[compIndex], fixedVersions[compIndex], directComponents[compIndex], impactPaths[compIndex]); e != nil { err = errors.Join(err, e) diff --git a/policy/local/localconvertor_test.go b/policy/local/localconvertor_test.go index fdb2890c9..a18cce803 100644 --- a/policy/local/localconvertor_test.go +++ b/policy/local/localconvertor_test.go @@ -7,6 +7,7 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/jfrog/jfrog-client-go/xray/services" @@ -190,6 +191,28 @@ func TestGenerateViolations(t *testing.T) { } } +func TestGenerateViolations_componentLessSecurityViolation(t *testing.T) { + input := createTestResultsWithViolations(false, []services.Violation{ + { + IssueId: "XRAY-no-comp", + Summary: "summary-no-comp", + Severity: "High", + WatchName: "watch-name", + ViolationType: "security", + Cves: []services.Cve{{Id: "CVE-2024-nocomp", CvssV3Score: "7.0", CvssV3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}}, + Components: map[string]services.Component{}, + }, + }) + converted, err := NewDeprecatedViolationGenerator().GenerateViolations(input) + require.NoError(t, err) + require.Len(t, converted.Sca, 1) + assert.Nil(t, converted.Sca[0].ImpactedComponent) + assert.Equal(t, "XRAY-no-comp", converted.Sca[0].ViolationId) + assert.Equal(t, "CVE-2024-nocomp", converted.Sca[0].CveVulnerability.BOMRef) + assert.Equal(t, "XRAY-no-comp", converted.Sca[0].CveVulnerability.ID) + assert.Nil(t, converted.Sca[0].CveVulnerability.Affects) +} + func createTestResultsWithViolations(entitledForJas bool, violations []services.Violation, applicableRuns ...*sarif.Run) *results.SecurityCommandResults { cmdResults := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(entitledForJas) target := cmdResults.NewScanResults(results.ScanTarget{Target: "target"}) @@ -208,7 +231,7 @@ func createScaTestViolation(id, component string, vioType violationutils.Violati Severity: severity, Watch: watch, }, - ImpactedComponent: cyclonedx.Component{ + ImpactedComponent: &cyclonedx.Component{ BOMRef: fmt.Sprintf("pkg:generic/%s", component), PackageURL: fmt.Sprintf("pkg:generic/%s", component), Type: cyclonedx.ComponentTypeLibrary, @@ -232,7 +255,7 @@ func createCdxVulnerabilityFull(ref, id, description string, cweList []int, comp if otherScore < 5.0 { severity = "low" } - return cyclonedx.Vulnerability{ + vuln := cyclonedx.Vulnerability{ BOMRef: ref, ID: id, Source: source, @@ -252,11 +275,14 @@ func createCdxVulnerabilityFull(ref, id, description string, cweList []int, comp Method: "other", }, }, - Affects: &[]cyclonedx.Affects{ - { - Ref: fmt.Sprintf("pkg:generic/%s", component), - Range: &[]cyclonedx.AffectedVersions{}, - }, - }, } + if component == "" { + vuln.Affects = nil + } else { + vuln.Affects = &[]cyclonedx.Affects{{ + Ref: fmt.Sprintf("pkg:generic/%s", component), + Range: &[]cyclonedx.AffectedVersions{}, + }} + } + return vuln } diff --git a/utils/formats/violationutils/violations.go b/utils/formats/violationutils/violations.go index eaa67c1c2..58fad0e1f 100644 --- a/utils/formats/violationutils/violations.go +++ b/utils/formats/violationutils/violations.go @@ -206,7 +206,7 @@ type JasViolation struct { type ScaViolation struct { Violation - ImpactedComponent cyclonedx.Component `json:"impacted_component"` + ImpactedComponent *cyclonedx.Component `json:"impacted_component,omitempty"` DirectComponents []formats.ComponentRow `json:"direct_components,omitempty"` ImpactPaths [][]formats.ComponentRow `json:"impact_paths,omitempty"` } diff --git a/utils/results/common.go b/utils/results/common.go index 67d9ce1e7..163ff8323 100644 --- a/utils/results/common.go +++ b/utils/results/common.go @@ -54,7 +54,7 @@ type ParseScanGraphVulnerabilityFunc func(vulnerability services.Vulnerability, type ParseLicenseFunc func(license services.License, impactedPackagesId string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error type ParseJasIssueFunc func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) error type ParseSbomComponentFunc func(component cyclonedx.Component, relatedDependencies *cyclonedx.Dependency, relation cdxutils.ComponentRelation) error -type ParseBomScaVulnerabilityFunc func(vulnerability cyclonedx.Vulnerability, component cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) error +type ParseBomScaVulnerabilityFunc func(vulnerability cyclonedx.Vulnerability, component *cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) error // Allows to iterate over the provided SARIF runs and call the provided handler for each issue to process it. func ForEachJasIssue(runs []*sarif.Run, entitledForJas bool, handler ParseJasIssueFunc) (err error) { @@ -110,6 +110,13 @@ func ForEachScanGraphVulnerability(target ScanTarget, descriptors []string, vuln err = errors.Join(err, e) continue } + if len(impactedPackagesIds) == 0 { + // Vulnerability without any impacted packages, we pass an empty string as the impacted package ID + if e := handler(vulnerability, cves, applicabilityStatus, severity, "", []string{}, []formats.ComponentRow{}, [][]formats.ComponentRow{}); e != nil { + err = errors.Join(err, e) + continue + } + } for compIndex := 0; compIndex < len(impactedPackagesIds); compIndex++ { if e := handler(vulnerability, cves, applicabilityStatus, severity, impactedPackagesIds[compIndex], fixedVersions[compIndex], directComponents[compIndex], impactPaths[compIndex]); e != nil { err = errors.Join(err, e) @@ -142,25 +149,26 @@ func ForEachScaBomVulnerability(_ ScanTarget, bom *cyclonedx.BOM, entitledForJas return nil } for _, vulnerability := range *bom.Vulnerabilities { - if vulnerability.Affects == nil || len(*vulnerability.Affects) == 0 { - // If there are no affected components, we skip the vulnerability. - log.Debug(fmt.Sprintf("Skipping vulnerability %s as it has no affected components", vulnerability.BOMRef)) - continue - } + severity := cdxRatingToSeverity(vulnerability.Ratings) // Check the CA status of the vulnerability var applicability *formats.Applicability if entitledForJas && len(applicabilityRuns) > 0 { applicability = GetCveApplicabilityField(vulnerability.BOMRef, applicabilityRuns) } + if vulnerability.Affects == nil || len(*vulnerability.Affects) == 0 { + // No affected components, pass nil as the affected component and the applicability + if e := handler(vulnerability, nil, nil, applicability, severity); e != nil { + err = errors.Join(err, e) + } + continue + } // Get the related components for the vulnerability for _, affectedComponent := range *vulnerability.Affects { relatedComponent := cdxutils.SearchComponentByRef(bom.Components, affectedComponent.Ref) if relatedComponent == nil { - log.Verbose(fmt.Sprintf("Skipping vulnerability %s as it has no related component with BOMRef %s", vulnerability.BOMRef, affectedComponent.Ref)) - continue + log.Warn(fmt.Sprintf("Vulnerability %s references component BOMRef %s that was not found in the BOM; reporting without impacted component", vulnerability.BOMRef, affectedComponent.Ref)) } - // Pass the vulnerability to the handler with its related information - if e := handler(vulnerability, *relatedComponent, GetFixedVersions(affectedComponent), applicability, cdxRatingToSeverity(vulnerability.Ratings)); e != nil { + if e := handler(vulnerability, relatedComponent, GetFixedVersions(affectedComponent), applicability, severity); e != nil { err = errors.Join(err, e) continue } @@ -216,6 +224,13 @@ func ForEachLicense(target ScanTarget, licenses []services.License, handler Pars err = errors.Join(err, e) continue } + if len(impactedPackagesIds) == 0 { + // License without any impacted packages, we pass an empty string as the impacted package ID + if e := handler(license, "", []formats.ComponentRow{}, [][]formats.ComponentRow{}); e != nil { + err = errors.Join(err, e) + continue + } + } for compIndex := range impactedPackagesIds { if e := handler(license, impactedPackagesIds[compIndex], directComponents[compIndex], impactPaths[compIndex]); e != nil { err = errors.Join(err, e) @@ -654,6 +669,10 @@ func GetDependencyId(depName, version string) string { } func GetScaIssueId(depName, version, issueId string) string { + if depName == "" && version == "" { + depName = "unknown" + version = "unknown" + } return fmt.Sprintf("%s_%s_%s", issueId, depName, version) } @@ -1277,6 +1296,11 @@ func ScanResponseToSbom(destination *cyclonedx.BOM, scanResponse services.ScanRe func ParseScanGraphLicenseToSbom(destination *cyclonedx.BOM) ParseLicenseFunc { return func(license services.License, impactedPackagesId string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + if impactedPackagesId == "" { + // License without any impacted packages, we skip for now + log.Warn(fmt.Sprintf("License %s without any impacted component, skip attaching it to the SBOM", license.Key)) + return nil + } // Add the license related component if it is not already existing affectedComponent := GetOrCreateScaComponent(destination, impactedPackagesId) // Attach the license to the component @@ -1298,8 +1322,11 @@ func ParseScanGraphVulnerabilityToSbom(destination *cyclonedx.BOM) ParseScanGrap // Prepare the information needed to create the SCA vulnerability xrayService := GetXrayService() return func(vulnerability services.Vulnerability, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesId string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { - // Add the vulnerability related component if it is not already existing - affectedComponent := GetOrCreateScaComponent(destination, impactedPackagesId) + var affectedComponent *cyclonedx.Component + if impactedPackagesId != "" { + // Add the vulnerability related component if it is not already existing + affectedComponent = GetOrCreateScaComponent(destination, impactedPackagesId) + } // Extract the vulnerability CVE's information and create the SCA vulnerability for each cveIds, applicability, cwes, ratings := ExtractIssuesInfoForCdx(vulnerability.IssueId, cves, severity, applicabilityStatus, xrayService) extendedInformation := "" @@ -1318,10 +1345,12 @@ func ParseScanGraphVulnerabilityToSbom(destination *cyclonedx.BOM) ParseScanGrap Service: xrayService, } vulnerability := cdxutils.GetOrCreateScaIssue(destination, params) - // Attach the affected impacted library component to the vulnerability - cdxutils.AttachComponentAffects(vulnerability, *affectedComponent, func(affectedComponent cyclonedx.Component) cyclonedx.Affects { - return cdxutils.CreateScaImpactedAffects(affectedComponent, fixedVersion) - }) + if affectedComponent != nil { + // Attach the affected impacted library component to the vulnerability + cdxutils.AttachComponentAffects(vulnerability, *affectedComponent, func(affectedComponent cyclonedx.Component) cyclonedx.Affects { + return cdxutils.CreateScaImpactedAffects(affectedComponent, fixedVersion) + }) + } // Attach JAS information to the vulnerability AttachApplicabilityToVulnerability(destination, vulnerability, applicability[i]) } diff --git a/utils/results/common_test.go b/utils/results/common_test.go index e06262e3d..7ee011960 100644 --- a/utils/results/common_test.go +++ b/utils/results/common_test.go @@ -3222,3 +3222,152 @@ func TestSplitComponents(t *testing.T) { }) } } + +func TestForEachScaBomVulnerability(t *testing.T) { + validComponent := cyclonedx.Component{ + BOMRef: "pkg:golang/example@1.0.0", + PackageURL: "pkg:golang/example@1.0.0", + Type: cyclonedx.ComponentTypeLibrary, + Name: "example", + Version: "1.0.0", + } + bomComponents := []cyclonedx.Component{validComponent} + + t.Run("no affects invokes handler once with nil component", func(t *testing.T) { + bom := &cyclonedx.BOM{ + Components: &bomComponents, + Vulnerabilities: &[]cyclonedx.Vulnerability{{ID: "CVE-2024-0001", BOMRef: "vuln-no-affects"}}, + } + var callCount int + var lastComp *cyclonedx.Component + err := ForEachScaBomVulnerability(ScanTarget{}, bom, false, nil, + func(_ cyclonedx.Vulnerability, comp *cyclonedx.Component, _ *[]cyclonedx.AffectedVersions, _ *formats.Applicability, _ severityutils.Severity) error { + callCount++ + lastComp = comp + return nil + }) + require.NoError(t, err) + assert.Equal(t, 1, callCount) + assert.Nil(t, lastComp) + }) + + t.Run("resolved affect invokes handler once with component", func(t *testing.T) { + bom := &cyclonedx.BOM{ + Components: &bomComponents, + Vulnerabilities: &[]cyclonedx.Vulnerability{{ + ID: "CVE-2024-0002", + BOMRef: "vuln-with-affect", + Affects: &[]cyclonedx.Affects{{ + Ref: validComponent.BOMRef, + }}, + }}, + } + var callCount int + err := ForEachScaBomVulnerability(ScanTarget{}, bom, false, nil, + func(_ cyclonedx.Vulnerability, comp *cyclonedx.Component, _ *[]cyclonedx.AffectedVersions, _ *formats.Applicability, _ severityutils.Severity) error { + callCount++ + require.NotNil(t, comp) + assert.Equal(t, validComponent.BOMRef, comp.BOMRef) + return nil + }) + require.NoError(t, err) + assert.Equal(t, 1, callCount) + }) + + t.Run("unresolved affect ref still invokes handler with nil component", func(t *testing.T) { + bom := &cyclonedx.BOM{ + Components: &bomComponents, + Vulnerabilities: &[]cyclonedx.Vulnerability{{ + ID: "CVE-2024-0003", + BOMRef: "vuln-mixed-affects", + Affects: &[]cyclonedx.Affects{ + {Ref: "pkg:golang/missing@9.9.9"}, + {Ref: validComponent.BOMRef}, + }, + }}, + } + var nilComponentCalls int + var resolvedCalls int + err := ForEachScaBomVulnerability(ScanTarget{}, bom, false, nil, + func(_ cyclonedx.Vulnerability, comp *cyclonedx.Component, _ *[]cyclonedx.AffectedVersions, _ *formats.Applicability, _ severityutils.Severity) error { + if comp == nil { + nilComponentCalls++ + } else { + resolvedCalls++ + assert.Equal(t, validComponent.BOMRef, comp.BOMRef) + } + return nil + }) + require.NoError(t, err) + assert.Equal(t, 1, nilComponentCalls) + assert.Equal(t, 1, resolvedCalls) + }) + + t.Run("only unresolved affects invokes handler per affect with nil component", func(t *testing.T) { + bom := &cyclonedx.BOM{ + Components: &bomComponents, + Vulnerabilities: &[]cyclonedx.Vulnerability{{ + ID: "CVE-2024-0004", + BOMRef: "vuln-broken-affects", + Affects: &[]cyclonedx.Affects{ + {Ref: "pkg:golang/missing-a@1.0.0"}, + {Ref: "pkg:golang/missing-b@2.0.0"}, + }, + }}, + } + var callCount int + err := ForEachScaBomVulnerability(ScanTarget{}, bom, false, nil, + func(_ cyclonedx.Vulnerability, comp *cyclonedx.Component, _ *[]cyclonedx.AffectedVersions, _ *formats.Applicability, _ severityutils.Severity) error { + callCount++ + assert.Nil(t, comp) + return nil + }) + require.NoError(t, err) + assert.Equal(t, 2, callCount) + }) +} + +func TestForEachScanGraphVulnerability_emptyImpactedPackages(t *testing.T) { + vuln := services.Vulnerability{ + IssueId: "XRAY-empty-comp", + Severity: "High", + Cves: []services.Cve{{Id: "CVE-2024-empty"}}, + Components: map[string]services.Component{}, + } + var calls int + var lastPkgId string + err := ForEachScanGraphVulnerability( + ScanTarget{Target: "."}, + []string{}, + []services.Vulnerability{vuln}, + false, + nil, + func(_ services.Vulnerability, _ []formats.CveRow, _ jasutils.ApplicabilityStatus, _ severityutils.Severity, impactedPackagesId string, _ []string, _ []formats.ComponentRow, _ [][]formats.ComponentRow) error { + calls++ + lastPkgId = impactedPackagesId + return nil + }, + ) + require.NoError(t, err) + assert.Equal(t, 1, calls) + assert.Empty(t, lastPkgId) +} + +func TestForEachLicense_emptyImpactedPackages(t *testing.T) { + license := services.License{ + Key: "GPL-3.0", + Components: map[string]services.Component{}, + } + var calls int + err := ForEachLicense( + ScanTarget{Target: "."}, + []services.License{license}, + func(_ services.License, impactedPackagesId string, _ []formats.ComponentRow, _ [][]formats.ComponentRow) error { + calls++ + assert.Empty(t, impactedPackagesId) + return nil + }, + ) + require.NoError(t, err) + assert.Equal(t, 1, calls) +} diff --git a/utils/results/conversion/convertor_test.go b/utils/results/conversion/convertor_test.go index d08950c85..2c117ad57 100644 --- a/utils/results/conversion/convertor_test.go +++ b/utils/results/conversion/convertor_test.go @@ -422,7 +422,7 @@ func getAuditTestResults(unique bool) (*results.SecurityCommandResults, validati ViolationId: "XRAY-609848", Severity: severityutils.Unknown, }, - ImpactedComponent: cyclonedx.Component{ + ImpactedComponent: &cyclonedx.Component{ BOMRef: "pkg:npm/async@3.2.4", PackageURL: "pkg:npm/async@3.2.4", }, @@ -462,7 +462,7 @@ func getAuditTestResults(unique bool) (*results.SecurityCommandResults, validati ViolationId: "98yhnmju7654rfvbnj", Severity: severityutils.Medium, }, - ImpactedComponent: cyclonedx.Component{ + ImpactedComponent: &cyclonedx.Component{ BOMRef: "pkg:npm/lodash@4.17.0", PackageURL: "pkg:npm/lodash@4.17.0", }, @@ -503,7 +503,7 @@ func getAuditTestResults(unique bool) (*results.SecurityCommandResults, validati ViolationId: "12ee2e134edqwe234", Severity: severityutils.High, }, - ImpactedComponent: cyclonedx.Component{ + ImpactedComponent: &cyclonedx.Component{ BOMRef: "pkg:npm/lodash@4.17.0", PackageURL: "pkg:npm/lodash@4.17.0", }, @@ -751,7 +751,7 @@ func getDockerScanTestResults(unique bool) (*results.SecurityCommandResults, val ViolationId: "XRAY-632747", Severity: severityutils.Unknown, }, - ImpactedComponent: cyclonedx.Component{ + ImpactedComponent: &cyclonedx.Component{ BOMRef: "pkg:deb/debian/bookworm/libssl3@3.0.13-1~deb12u1", PackageURL: "pkg:deb/debian/bookworm/libssl3@3.0.13-1~deb12u1", }, diff --git a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go index 4d47c2771..5a2422080 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -142,9 +142,11 @@ func (cdc *CmdResultsCycloneDxConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, } cdc.addJasService(applicableScan) return results.ForEachScaBomVulnerability(cdc.currentTarget, enrichedSbom, cdc.entitledForJas, results.CollectRuns(applicableScan...), - func(vulnToParse cyclonedx.Vulnerability, compToParse cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { - // Add the vulnerability related component if it is not already existing - cdc.getOrCreateScaComponent(compToParse) + func(vulnToParse cyclonedx.Vulnerability, compToParse *cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { + if compToParse != nil { + // Add the vulnerability related component if it is not already existing + cdc.getOrCreateScaComponent(*compToParse) + } // Add the vulnerability to the BOM if it is not already existing vulnerability := cdc.getOrCreateScaIssue(vulnToParse) // Attach JAS information to the vulnerability diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index 923a0e5a2..9abebb7ca 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -236,7 +236,10 @@ func (sc *CmdResultsSarifConverter) ParseViolations(violationsScanResults violat err = errors.Join(err, e) continue } - compName, compVersion, _ := techutils.SplitPackageURL(cveViolation.ImpactedComponent.PackageURL) + var compName, compVersion string + if cveViolation.ImpactedComponent != nil { + compName, compVersion, _ = techutils.SplitPackageURL(cveViolation.ImpactedComponent.PackageURL) + } createAndAddScaIssue(scaParseParams{ CmdType: sc.currentCmdType, IssueId: cveViolation.CveVulnerability.ID, @@ -258,7 +261,12 @@ func (sc *CmdResultsSarifConverter) ParseViolations(violationsScanResults violat } // License violations for _, licenseViolation := range violationsScanResults.License { - compName, compVersion, _ := techutils.SplitPackageURL(licenseViolation.ImpactedComponent.PackageURL) + var compName, compVersion string + summary := licenseViolation.LicenseKey + if licenseViolation.ImpactedComponent != nil { + compName, compVersion, _ = techutils.SplitPackageURL(licenseViolation.ImpactedComponent.PackageURL) + summary = getLicenseViolationSummary(compName, compVersion, licenseViolation.LicenseKey) + } markdownDescription, e := getScaLicenseViolationMarkdown(compName, compVersion, licenseViolation.LicenseKey, licenseViolation.DirectComponents) if e != nil { err = errors.Join(err, e) @@ -267,7 +275,7 @@ func (sc *CmdResultsSarifConverter) ParseViolations(violationsScanResults violat createAndAddScaIssue(scaParseParams{ CmdType: sc.currentCmdType, IssueId: licenseViolation.LicenseKey, - Summary: getLicenseViolationSummary(compName, compVersion, licenseViolation.LicenseKey), + Summary: summary, Violation: &licenseViolation.Violation, MarkdownDescription: markdownDescription, SeverityScore: fmt.Sprintf("%.1f", severityutils.GetSeverityScore(licenseViolation.Severity, jasutils.Applicable)), @@ -349,14 +357,19 @@ func (sc *CmdResultsSarifConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, appli func addCdxScaVulnerability(cmdType utils.CommandType, enrichedSbom *cyclonedx.BOM, sarifResults *[]*sarif.Result, rules *map[string]*sarif.ReportingDescriptor) results.ParseBomScaVulnerabilityFunc { bomIndex := cdxutils.NewBOMIndex(enrichedSbom, true) - return func(vulnerability cyclonedx.Vulnerability, component cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { - impactPaths := results.BuildImpactPath(component, bomIndex) - directDependencies := results.ExtractComponentDirectComponentsInBOM(bomIndex, component, impactPaths) + return func(vulnerability cyclonedx.Vulnerability, component *cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { + var impactPaths [][]formats.ComponentRow + var directDependencies []formats.ComponentRow + var compName, compVersion string + if component != nil { + impactPaths = results.BuildImpactPath(*component, bomIndex) + directDependencies = results.ExtractComponentDirectComponentsInBOM(bomIndex, *component, impactPaths) + compName, compVersion, _ = techutils.SplitPackageURL(component.PackageURL) + } applicabilityStatus, maxCveScore, cves, fixedVersions, markdownDescription, e := prepareCdxInfoForSarif(vulnerability, severity, applicability, directDependencies, fixedVersion) if e != nil { return } - compName, compVersion, _ := techutils.SplitPackageURL(component.PackageURL) createAndAddScaIssue(scaParseParams{ CmdType: cmdType, IssueId: vulnerability.ID, @@ -589,6 +602,20 @@ func parseScaToSarifFormat(params scaParseParams) (sarifResults []*sarif.Result, params.Summary, params.MarkdownDescription, ) + if len(params.DirectComponents) == 0 && params.ImpactedPackagesName == "" && params.ImpactedPackagesVersion == "" { + log.Debug(fmt.Sprintf("Issue %s without any components, adding a result with the issue id only without any location", issueId)) + // Issue without any components, lets add a result with the issue id only + issueResult := sarif.NewRuleResult(cveImpactedComponentRuleId). + WithMessage(sarif.NewTextMessage(params.GenerateTitleFunc("unknown", "unknown", issueId, watch))). + WithLevel(level.String()) + // Add properties + issueResult = appendScaVulnerabilityPropertiesToSarifResult(issueResult, params.ApplicabilityStatus, params.FixedVersions, params.AddFixedVersionProperty) + if isViolation { + issueResult = appendViolationContextToSarifResult(issueResult, *params.Violation) + } + sarifResults = append(sarifResults, issueResult) + return + } for _, directDependency := range params.DirectComponents { // Create result for each direct dependency issueResult := sarif.NewRuleResult(cveImpactedComponentRuleId). diff --git a/utils/results/conversion/sarifparser/sarifparser_test.go b/utils/results/conversion/sarifparser/sarifparser_test.go index 01a61d3a7..26cf25a7d 100644 --- a/utils/results/conversion/sarifparser/sarifparser_test.go +++ b/utils/results/conversion/sarifparser/sarifparser_test.go @@ -15,8 +15,10 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetComponentSarifLocation(t *testing.T) { @@ -64,6 +66,26 @@ func TestGetComponentSarifLocation(t *testing.T) { } } +func TestParseScaToSarifFormat_componentLessIssue(t *testing.T) { + params := scaParseParams{ + CmdType: utils.SourceCode, + IssueId: "CVE-2024-sarif-nocomp", + Summary: "test summary", + Severity: severityutils.High, + SeverityScore: "7.0", + ImpactedPackagesName: "", + ImpactedPackagesVersion: "", + DirectComponents: nil, + GenerateTitleFunc: getScaVulnerabilitySarifHeadline, + } + sarifResults, rule := parseScaToSarifFormat(params) + require.NotNil(t, rule) + require.Len(t, sarifResults, 1) + assert.Empty(t, sarifResults[0].Locations) + expectedRuleId := results.GetScaIssueId("unknown", "unknown", "CVE-2024-sarif-nocomp") + assert.Equal(t, expectedRuleId, *sarifResults[0].RuleID) +} + func TestGetVulnerabilityOrViolationSarifHeadline(t *testing.T) { assert.Equal(t, "[CVE-2022-1234] loadsh 1.4.1", getScaVulnerabilitySarifHeadline("loadsh", "1.4.1", "CVE-2022-1234", "")) assert.NotEqual(t, "[CVE-2022-1234] comp 1.4.1", getScaVulnerabilitySarifHeadline("comp", "1.2.1", "CVE-2022-1234", "")) diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index 84280b41c..246919201 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -131,8 +131,13 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, } bomIndex := cdxutils.NewBOMIndex(enrichedSbom, true) return results.ForEachScaBomVulnerability(sjc.currentTarget, enrichedSbom, sjc.entitledForJas, results.CollectRuns(applicableScan...), - func(vulnerability cyclonedx.Vulnerability, component cyclonedx.Component, fixedVersions *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { - impactPaths := results.BuildImpactPath(component, bomIndex) + func(vulnerability cyclonedx.Vulnerability, component *cyclonedx.Component, fixedVersions *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { + var impactPaths [][]formats.ComponentRow + var directComponents []formats.ComponentRow + if component != nil { + impactPaths = results.BuildImpactPath(*component, bomIndex) + directComponents = results.ExtractComponentDirectComponentsInBOM(bomIndex, *component, impactPaths) + } // Convert the CycloneDX vulnerability to a simple JSON vulnerability row sjc.current.Vulnerabilities = append(sjc.current.Vulnerabilities, sjc.createVulnerabilityOrViolationRowFromCdx( vulnerability.ID, @@ -141,7 +146,7 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, applicability, vulnerability, component, - results.ExtractComponentDirectComponentsInBOM(bomIndex, component, impactPaths), + directComponents, impactPaths, fixedVersions, // TODO: implement JfrogResearchInformation conversion @@ -223,12 +228,15 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseViolations(violationsScanResults return nil } -func (sjc *CmdResultsSimpleJsonConverter) createVulnerabilityOrViolationRowFromCdx(issueId, summary string, severity severityutils.Severity, contextualAnalysis *formats.Applicability, vulnerability cyclonedx.Vulnerability, component cyclonedx.Component, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow, fixedVersions *[]cyclonedx.AffectedVersions, jfrogResearch *formats.JfrogResearchInformation) formats.VulnerabilityOrViolationRow { +func (sjc *CmdResultsSimpleJsonConverter) createVulnerabilityOrViolationRowFromCdx(issueId, summary string, severity severityutils.Severity, contextualAnalysis *formats.Applicability, vulnerability cyclonedx.Vulnerability, component *cyclonedx.Component, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow, fixedVersions *[]cyclonedx.AffectedVersions, jfrogResearch *formats.JfrogResearchInformation) formats.VulnerabilityOrViolationRow { applicabilityStatus := jasutils.NotScanned if contextualAnalysis != nil { applicabilityStatus = jasutils.ConvertToApplicabilityStatus(contextualAnalysis.Status) } - compName, compVersion, compType := techutils.SplitPackageURL(component.PackageURL) + var compName, compVersion, compType string + if component != nil { + compName, compVersion, compType = techutils.SplitPackageURL(component.PackageURL) + } return formats.VulnerabilityOrViolationRow{ IssueId: issueId, Summary: summary, @@ -263,8 +271,11 @@ func toReferences(vulnerability cyclonedx.Vulnerability) (references []string) { return } -func (sjc *CmdResultsSimpleJsonConverter) createLicenseViolationRow(licenseKey, licenseName string, severity severityutils.Severity, component cyclonedx.Component, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow, violationContext formats.ViolationContext) formats.LicenseViolationRow { - compName, compVersion, compType := techutils.SplitPackageURL(component.PackageURL) +func (sjc *CmdResultsSimpleJsonConverter) createLicenseViolationRow(licenseKey, licenseName string, severity severityutils.Severity, component *cyclonedx.Component, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow, violationContext formats.ViolationContext) formats.LicenseViolationRow { + var compName, compVersion, compType string + if component != nil { + compName, compVersion, compType = techutils.SplitPackageURL(component.PackageURL) + } return formats.LicenseViolationRow{ ViolationContext: violationContext, LicenseRow: formats.LicenseRow{ @@ -283,7 +294,10 @@ func (sjc *CmdResultsSimpleJsonConverter) createLicenseViolationRow(licenseKey, } func (sjc *CmdResultsSimpleJsonConverter) createOpRiskViolationRow(opRiskViolation violationutils.OperationalRiskViolation) formats.OperationalRiskViolationRow { - compName, compVersion, compType := techutils.SplitPackageURL(opRiskViolation.ImpactedComponent.PackageURL) + var compName, compVersion, compType string + if opRiskViolation.ImpactedComponent != nil { + compName, compVersion, compType = techutils.SplitPackageURL(opRiskViolation.ImpactedComponent.PackageURL) + } return formats.OperationalRiskViolationRow{ ViolationContext: convertToViolationContext(opRiskViolation.Violation), ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ diff --git a/utils/results/conversion/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go index a8b4ac9e9..9fd9ec4c0 100644 --- a/utils/results/conversion/summaryparser/summaryparser.go +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -198,7 +198,7 @@ func (sc *CmdResultsSummaryConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, app func (sc *CmdResultsSummaryConverter) getBomScaVulnerabilityHandler() results.ParseBomScaVulnerabilityFunc { parsed := datastructures.MakeSet[string]() - return func(vulnerability cyclonedx.Vulnerability, _ cyclonedx.Component, _ *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (err error) { + return func(vulnerability cyclonedx.Vulnerability, _ *cyclonedx.Component, _ *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (err error) { if parsed.Exists(vulnerability.BOMRef) { return }