Skip to content

Commit fb06e74

Browse files
paulrosca-snykCalamarBicefalo
authored andcommitted
feat: compute pin advice in --json output
1 parent 02fe1ed commit fb06e74

File tree

5 files changed

+131
-4
lines changed

5 files changed

+131
-4
lines changed

internal/legacy/definitions/legacy-json.tsp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,16 @@ model RemediationUpgradeInfo {
200200
vulns: string[];
201201
}
202202

203+
model PinRemediation {
204+
isTransitive: boolean;
205+
vulns: string[];
206+
upgradeTo: string;
207+
}
208+
203209
model Remediation {
204210
ignore: Record<string>;
205211
patch: Record<string>;
206-
pin: Record<string>;
212+
pin: Record<PinRemediation>;
207213
unresolved: Vulnerability[];
208214
upgrade: Record<RemediationUpgradeInfo>;
209215
}

internal/legacy/definitions/oapi.gen.go

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/legacy/definitions/spec.yaml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,21 @@ components:
306306
type: string
307307
version:
308308
type: string
309+
PinRemediation:
310+
type: object
311+
required:
312+
- isTransitive
313+
- vulns
314+
- upgradeTo
315+
properties:
316+
isTransitive:
317+
type: boolean
318+
vulns:
319+
type: array
320+
items:
321+
type: string
322+
upgradeTo:
323+
type: string
309324
Reachability:
310325
type: string
311326
enum:
@@ -341,7 +356,7 @@ components:
341356
pin:
342357
type: object
343358
additionalProperties:
344-
type: string
359+
$ref: '#/components/schemas/PinRemediation'
345360
unresolved:
346361
type: array
347362
items:

internal/legacy/transform/remediation.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,98 @@
11
package transform
22

33
import (
4+
"errors"
45
"fmt"
6+
"slices"
57
"strings"
68

79
"github.com/rs/zerolog"
810
"github.com/snyk/go-application-framework/pkg/apiclients/testapi"
911

1012
"github.com/snyk/cli-extension-os-flows/internal/legacy/definitions"
13+
"github.com/snyk/cli-extension-os-flows/internal/semver/shared"
1114
"github.com/snyk/cli-extension-os-flows/internal/util"
1215
)
1316

17+
func CalculatePin(vulns []definitions.Vulnerability, semver shared.Runtime) (map[string]definitions.PinRemediation, error) {
18+
pin := make(map[string]definitions.PinRemediation)
19+
for _, vuln := range vulns {
20+
if len(vuln.From) < 2 {
21+
continue
22+
}
23+
24+
key := fmt.Sprintf("%s@%s", vuln.Name, vuln.Version)
25+
26+
var currentUpgradeToVersion string
27+
vulnPin, pinExistsForVuln := pin[key]
28+
if pinExistsForVuln {
29+
currentUpgradeToVersion = vulnPin.UpgradeTo[len(vuln.Name)+1:]
30+
}
31+
32+
if vuln.FixedIn == nil || len(*vuln.FixedIn) == 0 {
33+
continue
34+
}
35+
36+
var sortErr error
37+
slices.SortStableFunc(*vuln.FixedIn, func(a, b string) int {
38+
comp, err := semver.Compare(a, b)
39+
if err != nil {
40+
sortErr = errors.Join(sortErr, err)
41+
}
42+
return comp
43+
})
44+
if sortErr != nil {
45+
return nil, fmt.Errorf("failed to sort fixedIn values: %w", sortErr)
46+
}
47+
48+
isTransitive := len(vuln.From) > 2
49+
50+
var newVersion string
51+
for _, fixVersion := range *vuln.FixedIn {
52+
comp, err := semver.Compare(fixVersion, vuln.Version)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
if comp > 0 {
58+
newVersion = fixVersion
59+
break
60+
}
61+
}
62+
63+
if newVersion == "" {
64+
continue
65+
}
66+
67+
if !pinExistsForVuln {
68+
pin[key] = definitions.PinRemediation{
69+
UpgradeTo: fmt.Sprintf("%s@%s", vuln.Name, newVersion),
70+
Vulns: []string{vuln.Id},
71+
IsTransitive: isTransitive,
72+
}
73+
} else {
74+
vulnPin.Vulns = append(vulnPin.Vulns, vuln.Id)
75+
76+
v, err := semver.Compare(newVersion, currentUpgradeToVersion)
77+
if err != nil {
78+
return nil, err
79+
}
80+
if v > 0 {
81+
vulnPin.UpgradeTo = fmt.Sprintf("%s@%s", vuln.Name, newVersion)
82+
}
83+
84+
if !isTransitive {
85+
vulnPin.IsTransitive = false
86+
}
87+
88+
// vulnPin is a copy of the value from the map,
89+
// so we need to store it back after modifying it
90+
pin[key] = vulnPin
91+
}
92+
}
93+
return pin, nil
94+
}
95+
1496
func getNameFromStringPath(path string) string {
1597
lastIndx := strings.LastIndex(path, "@")
1698
return path[:lastIndx]

internal/legacy/transform/transform.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/snyk/cli-extension-os-flows/internal/errors"
1010
"github.com/snyk/cli-extension-os-flows/internal/legacy/definitions"
11+
"github.com/snyk/cli-extension-os-flows/internal/semver"
1112
"github.com/snyk/cli-extension-os-flows/internal/util"
1213
)
1314

@@ -447,6 +448,22 @@ func ConvertSnykSchemaFindingsToLegacy(params *SnykSchemaToLegacyParams) (*defin
447448
}
448449
}
449450

451+
if semver, err := semver.GetSemver(params.PackageManager); err == nil {
452+
pin, err := CalculatePin(res.Vulnerabilities, semver)
453+
if err != nil {
454+
return nil, params.ErrFactory.NewLegacyJSONTransformerError(fmt.Errorf("calculating pin remediation: %w", err))
455+
}
456+
res.Remediation = &definitions.Remediation{Pin: pin}
457+
} else {
458+
// This will be the case for the sbom test as long
459+
// as long as we don't get the package manager back from the server
460+
params.
461+
Logger.
462+
Warn().
463+
Str("package manager", params.PackageManager).
464+
Msg("skipping remediation computation as no semver runtime could be retrieved for the package manager")
465+
}
466+
450467
return &res, nil
451468
}
452469

0 commit comments

Comments
 (0)