Skip to content

Commit cc37bd7

Browse files
wip
1 parent 40d59d1 commit cc37bd7

File tree

5 files changed

+812
-357
lines changed

5 files changed

+812
-357
lines changed

internal/remediation/model.go

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,36 @@ package remediation
22

33
// Input
44

5+
/**
6+
* Finding represents a vulnerability for a given package & version.
7+
*
8+
* If a finding is introduced through multiple paths, a single finding with several `DependencyPath` entries must be constructed.
9+
*/
510
type Finding struct {
6-
Package Package
7-
Severity Severity
8-
Title string
9-
DependencyPath DependencyPath
10-
11-
// Snyk vuln ID
12-
VulnId string
11+
Package Package
12+
Vulnerability Vulnerability
13+
DependencyPaths []DependencyPath
1314

1415
// All versions containing a fix for this problem
15-
InitiallyFixedInVersions []string
16-
Fix Fix
16+
FixedInVersions []string
17+
Fix Fix
18+
PackageManager PackageManager
1719
}
20+
21+
type PackageManager string
22+
23+
type Outcome string
1824
type Fix interface {
1925
Outcome() Outcome
2026
}
2127

2228
type PinFix struct {
2329
outcome Outcome
24-
pinAction PinAction
30+
PinAction PinAction
31+
}
32+
33+
type PinAction struct {
34+
Package Package
2535
}
2636

2737
func (pf PinFix) Outcome() Outcome {
@@ -31,22 +41,39 @@ func (pf PinFix) Outcome() Outcome {
3141
func NewPinFix(outcome Outcome, action PinAction) Fix {
3242
return PinFix{
3343
outcome: outcome,
34-
pinAction: action,
44+
PinAction: action,
3545
}
3646
}
3747

38-
type Outcome string
48+
type UpgradeFix struct {
49+
outcome Outcome
50+
UpgradeAction UpgradeAction
51+
}
52+
53+
type UpgradeAction struct {
54+
PackageName string
55+
UpgradePaths []DependencyPath
56+
}
57+
58+
type VulnID string
59+
60+
func (uf UpgradeFix) Outcome() Outcome {
61+
return uf.outcome
62+
}
63+
64+
func NewUpgradeFix(outcome Outcome, action UpgradeAction) Fix {
65+
return UpgradeFix{
66+
outcome: outcome,
67+
UpgradeAction: action,
68+
}
69+
}
3970

4071
const (
4172
FullyResolved Outcome = "fully-resolved"
4273
PartiallyResolved Outcome = "partially-resolved"
4374
Unresolved Outcome = "unresolved"
4475
)
4576

46-
type PinAction struct {
47-
Package Package
48-
}
49-
5077
// Output
5178

5279
/*
@@ -69,18 +96,26 @@ type Summary struct {
6996
a chain reaction that will bump it nevertheless.
7097
*/
7198
Upgrades []Upgrade
99+
100+
/**
101+
Unresolved models all vulns that do not have a resolution known to Snyk. Either because
102+
there is no known fix for the vulnerable package, or because there's no viable upgrade
103+
through direct dependencies to bump it.
104+
*/
105+
Unresolved []VulnerabilityInPackage
72106
}
73107

74108
type Upgrade struct {
75109
From Package
76110
To Package
77111

78-
Fixes []*VulnerabilityInPackage
112+
Fixes []VulnerabilityInPackage
79113
}
80114

81115
type VulnerabilityInPackage struct {
82116
VulnerablePackage Package
83117
Vulnerability Vulnerability
118+
FixedInVersions []string
84119
IntroducedThrough []DependencyPath
85120
}
86121

@@ -89,8 +124,9 @@ type Package struct {
89124
Name string
90125
Version string
91126
}
127+
92128
type Vulnerability struct {
93-
ID string
129+
ID VulnID
94130
Name string
95131
Severity Severity
96132
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package remediation
2+
3+
import (
4+
"github.com/snyk/cli-extension-os-flows/internal/semver"
5+
)
6+
7+
func FixesToRemediationSummary(findings []Finding) (Summary, error) {
8+
pins, err := calculatePins(findings)
9+
if err != nil {
10+
return Summary{}, err
11+
}
12+
upgrades, unresolved, err := calculateUpgrades(findings)
13+
if err != nil {
14+
return Summary{}, err
15+
}
16+
return Summary{Pins: pins, Upgrades: upgrades, Unresolved: unresolved}, err
17+
}
18+
19+
func calculateUpgrades(findings []Finding) (upgrades []Upgrade, unresolved []VulnerabilityInPackage, err error) {
20+
for _, finding := range findings {
21+
switch fix := finding.Fix.(type) {
22+
case UpgradeFix:
23+
matchingPaths := getMatchingPaths(finding, fix)
24+
25+
upgradablePaths := matchingPaths.upgradablePaths
26+
for i, _ := range upgradablePaths.upgradePath {
27+
vulnerabilityInPackage := newVulnerabilityInPackage(finding)
28+
vulnerabilityInPackage.IntroducedThrough = upgradablePaths.dependencyPath
29+
30+
upgrades = append(upgrades, Upgrade{
31+
From: upgradablePaths.dependencyPath[i][1],
32+
To: upgradablePaths.upgradePath[i][0],
33+
Fixes: []VulnerabilityInPackage{vulnerabilityInPackage},
34+
})
35+
}
36+
37+
if len(matchingPaths.nonUpgradablePaths) > 0 {
38+
vulnerabilityInPackage := newVulnerabilityInPackage(finding)
39+
vulnerabilityInPackage.IntroducedThrough = matchingPaths.nonUpgradablePaths
40+
unresolved = append(unresolved, vulnerabilityInPackage)
41+
}
42+
default:
43+
continue
44+
}
45+
}
46+
return upgrades, unresolved, nil
47+
}
48+
49+
/*
50+
*
51+
Returns equally ordered arrays of original dependencyPaths and their upgrade counterparts.
52+
53+
For those upgrades/dep paths that don't have a counterpart, no entries will be found.
54+
*/
55+
type paths struct {
56+
upgradablePaths upgradablePathss
57+
nonUpgradablePaths []DependencyPath
58+
}
59+
60+
type upgradablePathss struct {
61+
dependencyPath []DependencyPath
62+
upgradePath []DependencyPath
63+
}
64+
65+
func getMatchingPaths(finding Finding, fix UpgradeFix) paths {
66+
var upgradePaths []DependencyPath
67+
var dependencyPaths []DependencyPath
68+
var nonUpgradablePaths []DependencyPath
69+
70+
for _, depPath := range finding.DependencyPaths {
71+
var found = false
72+
for _, upath := range fix.UpgradeAction.UpgradePaths {
73+
if !isMatchingUpgradePath(upath, depPath) {
74+
continue
75+
}
76+
found = true
77+
upgradePaths = append(upgradePaths, upath)
78+
dependencyPaths = append(dependencyPaths, depPath)
79+
break
80+
}
81+
if !found {
82+
nonUpgradablePaths = append(nonUpgradablePaths, depPath)
83+
}
84+
}
85+
return paths{
86+
upgradablePaths: upgradablePathss{
87+
dependencyPath: dependencyPaths,
88+
upgradePath: upgradePaths,
89+
},
90+
nonUpgradablePaths: nonUpgradablePaths,
91+
}
92+
}
93+
94+
func calculatePins(findings []Finding) ([]Upgrade, error) {
95+
var output []Upgrade
96+
var pinMap = make(map[string]*[]*Upgrade)
97+
for _, finding := range findings {
98+
switch finding.Fix.(type) {
99+
case PinFix:
100+
vulnerablePackage := finding.Package
101+
key := finding.Package.Name
102+
103+
// We'll be grouping by package name
104+
upgrade, upgradeExists := pinMap[key]
105+
106+
if upgradeExists {
107+
highestVersion, err := getMaxVersion(finding.PackageManager, (*upgrade)[0].To.Version, finding.Fix.(PinFix).PinAction.Package.Version)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
// As we process individual pins, we'll get the highest version of a given package across vulns & package version
113+
// Goal is to obtain a single pin that fixes as many problems as possible
114+
versionAlreadyExists := false
115+
for _, u := range *upgrade {
116+
u.To.Version = highestVersion
117+
if u.From.Version == finding.Package.Version {
118+
u.Fixes = append(u.Fixes, newVulnerabilityInPackage(finding))
119+
versionAlreadyExists = true
120+
break
121+
}
122+
}
123+
if !versionAlreadyExists {
124+
*upgrade = append(*upgrade, &Upgrade{
125+
From: vulnerablePackage,
126+
To: finding.Fix.(PinFix).PinAction.Package,
127+
Fixes: []VulnerabilityInPackage{newVulnerabilityInPackage(finding)},
128+
})
129+
}
130+
} else {
131+
pinMap[key] = &[]*Upgrade{{
132+
From: vulnerablePackage,
133+
To: finding.Fix.(PinFix).PinAction.Package,
134+
Fixes: []VulnerabilityInPackage{newVulnerabilityInPackage(finding)},
135+
}}
136+
}
137+
138+
default:
139+
continue
140+
}
141+
}
142+
143+
for _, versionGroupedPins := range pinMap {
144+
for _, pin := range *versionGroupedPins {
145+
output = append(output, *pin)
146+
}
147+
}
148+
return output, nil
149+
}
150+
151+
func newVulnerabilityInPackage(finding Finding) VulnerabilityInPackage {
152+
return VulnerabilityInPackage{
153+
FixedInVersions: finding.FixedInVersions,
154+
VulnerablePackage: finding.Package,
155+
Vulnerability: finding.Vulnerability,
156+
IntroducedThrough: finding.DependencyPaths,
157+
}
158+
}
159+
160+
func isMatchingUpgradePath(upath DependencyPath, depPath DependencyPath) bool {
161+
if len(depPath) != len(upath)+1 {
162+
return false
163+
}
164+
for i, uDep := range upath {
165+
if uDep.Name != depPath[i+1].Name {
166+
return false
167+
}
168+
}
169+
return true
170+
}
171+
172+
func getMaxVersion(packageManager PackageManager, v1 string, v2 string) (string, error) {
173+
semverResolver, err := semver.GetSemver(string(packageManager))
174+
if err != nil {
175+
return "", err
176+
}
177+
var version string
178+
compare, err := semverResolver.Compare(v1, v2)
179+
if err != nil {
180+
return "", err
181+
}
182+
if compare >= 0 {
183+
version = v1
184+
} else {
185+
version = v2
186+
}
187+
return version, nil
188+
}

0 commit comments

Comments
 (0)