Skip to content

Commit 898cae8

Browse files
authored
Remidiation static sca (jfrog#590)
1 parent 6dbc032 commit 898cae8

File tree

6 files changed

+767
-9
lines changed

6 files changed

+767
-9
lines changed

go.mod

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ module github.com/jfrog/jfrog-cli-security
22

33
go 1.24.6
44

5+
// TODO: update xray-scan lib to latest version that supports CycloneDX v0.9.3 (not yet released)
6+
replace github.com/CycloneDX/cyclonedx-go => github.com/CycloneDX/cyclonedx-go v0.9.2
7+
58
require (
69
github.com/CycloneDX/cyclonedx-go v0.9.3
710
github.com/beevik/etree v1.4.0
@@ -17,7 +20,7 @@ require (
1720
github.com/jfrog/jfrog-apps-config v1.0.1
1821
github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251021143342-49bab7f38cec
1922
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251023084247-a56afca52451
20-
github.com/jfrog/jfrog-client-go v1.55.1-0.20251030113529-d87ecf28ffb6
23+
github.com/jfrog/jfrog-client-go v1.55.1-0.20251103081126-15edfe03d6e5
2124
github.com/magiconair/properties v1.8.10
2225
github.com/owenrumney/go-sarif/v3 v3.2.3
2326
github.com/package-url/packageurl-go v0.1.3
@@ -124,13 +127,12 @@ require (
124127
gopkg.in/warnings.v0 v0.1.2 // indirect
125128
)
126129

127-
// attiasas:retry_build_scan
128-
replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20251030094108-376296f968cc
130+
// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go master
129131

130132
// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master
131133

132-
//replace github.com/jfrog/jfrog-cli-artifactory => github.com/fluxxBot/jfrog-cli-artifactory v0.0.0-20251017061455-6a03988302bf
134+
//replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory main
133135

134-
// replace github.com/jfrog/build-info-go => github.com/attiasas/build-info-go dev
136+
// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go dev
135137

136138
// replace github.com/jfrog/froggit-go => github.com/jfrog/froggit-go master

go.sum

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
44
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
55
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
66
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
7-
github.com/CycloneDX/cyclonedx-go v0.9.3 h1:Pyk/lwavPz7AaZNvugKFkdWOm93MzaIyWmBwmBo3aUI=
8-
github.com/CycloneDX/cyclonedx-go v0.9.3/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg=
7+
github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo=
8+
github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg=
99
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
1010
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
1111
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
@@ -21,6 +21,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
2121
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
2222
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
2323
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
24+
github.com/attiasas/jfrog-client-go v0.0.0-20251027091559-b3d20c0c4c64 h1:Cn41pu2bfTjWq18sm/GgLJCkSZUtgKZM6CeIjuUmw7E=
25+
github.com/attiasas/jfrog-client-go v0.0.0-20251027091559-b3d20c0c4c64/go.mod h1:7E859kSn1CaF9A2LH/zAXNPO6bGViNoJ0yqEJYwCu0E=
2426
github.com/attiasas/jfrog-client-go v0.0.0-20251030094108-376296f968cc h1:AJQueJft8TFJ7s46cr8saEFGfcJEDAxUJ/rhkCnWsP0=
2527
github.com/attiasas/jfrog-client-go v0.0.0-20251030094108-376296f968cc/go.mod h1:wsMEtoyAu/1bARUHxFdmgz83g96ml7ZWcFioIPiuz/U=
2628
github.com/beevik/etree v1.4.0 h1:oz1UedHRepuY3p4N5OjE0nK1WLCqtzHf25bxplKOHLs=
@@ -142,6 +144,8 @@ github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251021143342-49bab7f38cec h1:i
142144
github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251021143342-49bab7f38cec/go.mod h1:JE/35+kU8cBET4I4iuNcVBvhm8SF64DAmGgtHRzf5Do=
143145
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251023084247-a56afca52451 h1:Q0PY8VSOVsfvXzKiUnn+Rv7Ynf901QW6Wn1CbWpHBD0=
144146
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251023084247-a56afca52451/go.mod h1:UOeOwEEmRIi57cRwghN5OBVoqkJieYQQfLpeqw8Yv38=
147+
github.com/jfrog/jfrog-client-go v1.55.1-0.20251103081126-15edfe03d6e5 h1:vjWXoDEkxpTkO9qRiqeFaB2jNpWWtTcINQ8eZ4RbO5o=
148+
github.com/jfrog/jfrog-client-go v1.55.1-0.20251103081126-15edfe03d6e5/go.mod h1:wsMEtoyAu/1bARUHxFdmgz83g96ml7ZWcFioIPiuz/U=
145149
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
146150
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
147151
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=

sca/scan/enrich/runner.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212

1313
"github.com/jfrog/jfrog-cli-security/sca/scan"
1414
"github.com/jfrog/jfrog-cli-security/utils/catalog"
15+
"github.com/jfrog/jfrog-cli-security/utils/xray"
16+
"github.com/jfrog/jfrog-cli-security/utils/xray/remediation"
1517
)
1618

1719
type EnrichScanStrategy struct {
@@ -58,6 +60,20 @@ func (ess *EnrichScanStrategy) SbomEnrichTask(target *cyclonedx.BOM) (enriched *
5860
return nil, []services.Violation{}, fmt.Errorf("failed to create catalog service manager: %w", err)
5961
}
6062
enriched, err = catalogManager.Enrich(target)
63+
if err != nil {
64+
return nil, []services.Violation{}, fmt.Errorf("failed to enrich SBOM: %w", err)
65+
}
66+
log.Debug("SBOM enrichment completed successfully")
67+
// Fixed versions are not returned from the enrich API, next we need to enrich with remediation API.
68+
xrayManager, err := xray.CreateXrayServiceManager(ess.serverDetails, xray.WithScopedProjectKey(ess.projectKey))
69+
if err != nil {
70+
return nil, []services.Violation{}, fmt.Errorf("failed to create Xray service manager: %w", err)
71+
}
72+
err = remediation.AttachFixedVersionsToVulnerabilities(xrayManager, enriched)
73+
if err != nil {
74+
return nil, []services.Violation{}, fmt.Errorf("failed to attach fixed versions to vulnerabilities: %w", err)
75+
}
76+
log.Debug("SBOM remediation enrichment completed successfully")
6177
return
6278
}
6379

utils/formats/cdxutils/cyclonedxutils.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,20 +450,42 @@ func CreateScaImpactedAffects(impactedPackageComponent cyclonedx.Component, fixe
450450
Range: &[]cyclonedx.AffectedVersions{},
451451
}
452452
// Affected version
453-
*affect.Range = append(*affect.Range, cyclonedx.AffectedVersions{
453+
AppendAffectedVersionsIfNotExists(&affect, cyclonedx.AffectedVersions{
454454
Version: impactedPackageVersion,
455455
Status: cyclonedx.VulnerabilityStatusAffected,
456456
})
457457
// Fixed versions
458458
for _, fixedVersion := range fixedVersions {
459-
*affect.Range = append(*affect.Range, cyclonedx.AffectedVersions{
459+
AppendAffectedVersionsIfNotExists(&affect, cyclonedx.AffectedVersions{
460460
Version: fixedVersion,
461461
Status: cyclonedx.VulnerabilityStatusNotAffected,
462462
})
463463
}
464464
return
465465
}
466466

467+
func AppendAffectedVersionsIfNotExists(affect *cyclonedx.Affects, affectedVersions ...cyclonedx.AffectedVersions) {
468+
if affect.Range == nil {
469+
affect.Range = &[]cyclonedx.AffectedVersions{}
470+
}
471+
// Validate that the affected version does not already exist in the affected versions
472+
for _, newAffectedVersion := range affectedVersions {
473+
if newAffectedVersion.Version == "" {
474+
continue
475+
}
476+
exists := false
477+
for _, existingAffectedVersion := range *affect.Range {
478+
if existingAffectedVersion.Version == newAffectedVersion.Version {
479+
exists = true
480+
break
481+
}
482+
}
483+
if !exists {
484+
*affect.Range = append(*affect.Range, newAffectedVersion)
485+
}
486+
}
487+
}
488+
467489
type CdxVulnerabilityParams struct {
468490
Ref string
469491
ID string
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package remediation
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/CycloneDX/cyclonedx-go"
7+
8+
"github.com/jfrog/jfrog-client-go/utils/log"
9+
"github.com/jfrog/jfrog-client-go/xray"
10+
"github.com/jfrog/jfrog-client-go/xray/services/utils"
11+
12+
"github.com/jfrog/jfrog-cli-security/utils/formats/cdxutils"
13+
)
14+
15+
func AttachFixedVersionsToVulnerabilities(xrayManager *xray.XrayServicesManager, bom *cyclonedx.BOM) error {
16+
if bom.Vulnerabilities == nil || len(*bom.Vulnerabilities) == 0 {
17+
log.Debug("No vulnerabilities found in the SBOM, skipping attaching fixed versions")
18+
return nil
19+
}
20+
// Remediate the CVE by forcing the transitive dependency to a specific fix-version
21+
remediationOptions, err := xrayManager.RemediationByCve(bom)
22+
if err != nil {
23+
return fmt.Errorf("failed to get remediation options from Xray: %w", err)
24+
}
25+
log.Verbose(fmt.Sprintf("Remediation options received from Xray: %+v", remediationOptions))
26+
for _, vulnerability := range *bom.Vulnerabilities {
27+
matchVulnerabilityToRemediationOptions(bom, &vulnerability, remediationOptions)
28+
}
29+
return nil
30+
}
31+
32+
func matchVulnerabilityToRemediationOptions(bom *cyclonedx.BOM, vulnerability *cyclonedx.Vulnerability, remediationOptions utils.CveRemediationResponse) {
33+
if vulnerability.Affects == nil || len(*vulnerability.Affects) == 0 {
34+
log.Debug("No affected components found for vulnerability " + vulnerability.ID + ", skipping attaching fixed versions")
35+
return
36+
}
37+
if cveRemediationOptions, found := remediationOptions[vulnerability.ID]; found {
38+
for i, affect := range *vulnerability.Affects {
39+
// Lets find the remediation for this specific component
40+
affectComponent := cdxutils.SearchComponentByRef(bom.Components, affect.Ref)
41+
if affectComponent == nil {
42+
log.Debug("Affected component " + affect.Ref + " not found in BOM components, skipping attaching fixed versions for vulnerability " + vulnerability.ID)
43+
continue
44+
}
45+
// Convert remediation steps to fixed versions affected versions
46+
for _, step := range getAffectComponentCveRemediationStepsByFixedVersion(vulnerability.ID, *affectComponent, cveRemediationOptions) {
47+
cdxutils.AppendAffectedVersionsIfNotExists(&affect, cyclonedx.AffectedVersions{
48+
Version: step.UpgradeTo.Version,
49+
Status: cyclonedx.VulnerabilityStatusNotAffected,
50+
})
51+
}
52+
(*vulnerability.Affects)[i] = affect
53+
}
54+
} else {
55+
log.Debug("No remediation options found for vulnerability " + vulnerability.ID)
56+
}
57+
}
58+
59+
func getAffectComponentCveRemediationStepsByFixedVersion(cve string, component cyclonedx.Component, cveRemediationOptions []utils.Option) (steps []utils.OptionStep) {
60+
for _, cveRemediationOption := range cveRemediationOptions {
61+
if cveRemediationOption.Type != utils.InLock {
62+
// We only want InLock remediation type (forcing the actual component to a specific fix version)
63+
continue
64+
}
65+
for _, step := range cveRemediationOption.Steps {
66+
if step.StepType == utils.NoFixVersion {
67+
log.Debug(fmt.Sprintf("No fix version available for component '%s' in vulnerability '%s'", component.Name, cve))
68+
continue
69+
} else if step.StepType == utils.PackageNotFound {
70+
log.Debug(fmt.Sprintf("Component '%s' not found in catalog for vulnerability '%s'", component.Name, cve))
71+
continue
72+
}
73+
// We only want FixVersion step type
74+
if step.StepType == utils.FixVersion && step.PkgVersion.Name == component.Name && step.PkgVersion.Version == component.Version {
75+
steps = append(steps, step)
76+
}
77+
}
78+
}
79+
if len(steps) == 0 {
80+
log.Debug(fmt.Sprintf("No remediation steps by forcing fixed version found for component '%s' in vulnerability '%s'", component.Name, cve))
81+
}
82+
return
83+
}

0 commit comments

Comments
 (0)