Skip to content
175 changes: 111 additions & 64 deletions policy/enforcer/policyenforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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,
),
}
}
96 changes: 96 additions & 0 deletions policy/enforcer/policyenforcer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
43 changes: 43 additions & 0 deletions policy/local/local_foreach_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading