Skip to content

Commit e12fba3

Browse files
authored
osv-scanner: fix reports of vulnerabilities when multiple versions of @grafana packages are installed (#419)
* osv-scanner: fix multiple versions of a same grafana package reporting vulnerabilities * simplify code * replace express by body-parser * Merge main * restore files * simplify lock file * reduce lock file to a min * fix string
1 parent a49848f commit e12fba3

5 files changed

Lines changed: 5599 additions & 25 deletions

File tree

pkg/analysis/passes/osvscanner/cache-grafana-packages.go

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,10 @@ import (
66
"github.com/grafana/plugin-validator/pkg/analysis/passes/osvscanner/lockfile"
77
)
88

9-
func packageExists(name string, items []lockfile.PackageDetails) bool {
10-
for _, aPackage := range items {
11-
if aPackage.Name == name {
12-
return true
13-
}
14-
}
15-
return false
16-
}
17-
18-
func IncludedByGrafanaPackage(packageName string, cache []lockfile.PackageFlattened) (bool, string) {
9+
func IncludedByGrafanaPackage(
10+
packageName string,
11+
cache []lockfile.PackageFlattened,
12+
) (bool, string) {
1913
for _, item := range cache {
2014
for _, dependency := range item.Dependencies {
2115
if dependency.Package.Name == packageName {
@@ -26,24 +20,105 @@ func IncludedByGrafanaPackage(packageName string, cache []lockfile.PackageFlatte
2620
return false, ""
2721
}
2822

29-
func CacheGrafanaPackages(allPackages []lockfile.PackageDetails) ([]lockfile.PackageFlattened, error) {
23+
func CacheGrafanaPackages(
24+
allPackages []lockfile.PackageDetails,
25+
) ([]lockfile.PackageFlattened, error) {
3026
cache := make([]lockfile.PackageFlattened, 0)
31-
for grafanaPackage := range GrafanaPackages {
32-
// check if the package is in the list parsed
33-
if packageExists(grafanaPackage, allPackages) {
34-
// get dependencies of grafanaPackage
35-
expanded, err := lockfile.ExpandPackage(grafanaPackage, allPackages)
36-
if err != nil {
37-
// failed to parse
38-
return nil, err
27+
processedNames := make(map[string]bool)
28+
29+
for _, pkg := range allPackages {
30+
if GrafanaPackages[pkg.Name] && !processedNames[pkg.Name] {
31+
processedNames[pkg.Name] = true
32+
33+
allVersions := make([]lockfile.PackageDetails, 0)
34+
for _, p := range allPackages {
35+
if p.Name == pkg.Name {
36+
allVersions = append(allVersions, p)
37+
}
38+
}
39+
40+
// should never happen
41+
if len(allVersions) == 0 {
42+
continue
43+
}
44+
45+
// create a custom flattened package
46+
// with all dependencies for all versions of this package
47+
merged := lockfile.PackageFlattened{
48+
Name: pkg.Name,
49+
Version: allVersions[0].Version,
50+
}
51+
52+
for _, version := range allVersions {
53+
for _, dep := range version.Dependencies {
54+
// do not add them twice
55+
if !dependencyExists(dep.Name, merged.Dependencies) {
56+
state := lockfile.DependencyState{
57+
Package: dep,
58+
Processed: false,
59+
}
60+
merged.Dependencies = append(merged.Dependencies, state)
61+
}
62+
}
3963
}
40-
cache = append(cache, *expanded)
64+
65+
// find all transitive dependencies of this package
66+
// aka deps of deps of deps of deps to the infinite
67+
expandDependenciesRecursively(&merged, allPackages)
68+
69+
cache = append(cache, merged)
4170
}
4271
}
43-
// sort cache
72+
4473
sort.SliceStable(cache, func(i, j int) bool {
4574
return cache[i].Name < cache[j].Name
4675
})
4776

4877
return cache, nil
4978
}
79+
80+
func expandDependenciesRecursively(
81+
topLevel *lockfile.PackageFlattened,
82+
allPackages []lockfile.PackageDetails,
83+
) {
84+
for i := 0; i < len(topLevel.Dependencies); i++ {
85+
if topLevel.Dependencies[i].Processed {
86+
continue
87+
}
88+
89+
topLevel.Dependencies[i].Processed = true
90+
depName := topLevel.Dependencies[i].Package.Name
91+
92+
var depPackage *lockfile.PackageDetails
93+
for j := range allPackages {
94+
if allPackages[j].Name == depName {
95+
depPackage = &allPackages[j]
96+
break
97+
}
98+
}
99+
100+
if depPackage == nil {
101+
continue
102+
}
103+
104+
for _, subDep := range depPackage.Dependencies {
105+
// do not add them twice
106+
if !dependencyExists(subDep.Name, topLevel.Dependencies) {
107+
state := lockfile.DependencyState{
108+
Package: subDep,
109+
Processed: false,
110+
}
111+
topLevel.Dependencies = append(topLevel.Dependencies, state)
112+
}
113+
}
114+
}
115+
}
116+
117+
func dependencyExists(depName string, dependencies []lockfile.DependencyState) bool {
118+
for _, dep := range dependencies {
119+
if dep.Package.Name == depName {
120+
return true
121+
}
122+
}
123+
return false
124+
}

pkg/analysis/passes/osvscanner/cache-grafana-packages_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,100 @@ func TestCacheHitMiss(t *testing.T) {
3232
cacheMiss, _ := IncludedByGrafanaPackage("notmoment", cachedPackages)
3333
require.False(t, cacheMiss)
3434
}
35+
36+
func TestCacheGrafanaPackagesMultipleVersionsNPM(t *testing.T) {
37+
t.Parallel()
38+
39+
aLockfile := filepath.Join("testdata", "node", "multi-version-npm", "package-lock.json")
40+
packages, err := lockfile.ParseNpmLock(aLockfile)
41+
require.NoError(t, err)
42+
43+
cachedPackages, err := CacheGrafanaPackages(packages)
44+
require.NoError(t, err)
45+
46+
// This test verifies the fix: when multiple versions of @grafana/data exist (9.5.1 with dompurify and 9.3.8 without), both versions should be cached so dompurify is found
47+
var foundGrafanaData bool
48+
var hasDompurify bool
49+
for _, pkg := range cachedPackages {
50+
if pkg.Name == "@grafana/data" {
51+
foundGrafanaData = true
52+
for _, dep := range pkg.Dependencies {
53+
if dep.Package.Name == "dompurify" {
54+
hasDompurify = true
55+
break
56+
}
57+
}
58+
break
59+
}
60+
}
61+
62+
require.True(t, foundGrafanaData, "@grafana/data should be in the cache")
63+
require.True(t, hasDompurify, "dompurify should be in @grafana/data dependencies")
64+
65+
cacheHit, includedBy := IncludedByGrafanaPackage("dompurify", cachedPackages)
66+
require.True(t, cacheHit, "dompurify should be found in the cache")
67+
require.Equal(t, "@grafana/data", includedBy, "dompurify should be included by @grafana/data")
68+
}
69+
70+
func TestCacheGrafanaPackagesSingleVersionNPM(t *testing.T) {
71+
t.Parallel()
72+
73+
aLockfile := filepath.Join("testdata", "node", "critical-npm", "package-lock.json")
74+
packages, err := lockfile.ParseNpmLock(aLockfile)
75+
require.NoError(t, err)
76+
77+
cachedPackages, err := CacheGrafanaPackages(packages)
78+
require.NoError(t, err)
79+
80+
require.NotEmpty(t, cachedPackages, "cache should have entries")
81+
82+
cacheHit, includedBy := IncludedByGrafanaPackage("moment", cachedPackages)
83+
require.True(t, cacheHit, "moment should be found in the cache")
84+
require.Equal(t, "@grafana/data", includedBy, "moment should be included by @grafana/data")
85+
}
86+
87+
func TestCacheGrafanaPackagesYarnLock(t *testing.T) {
88+
t.Parallel()
89+
90+
aLockfile := filepath.Join("testdata", "node", "circular-yarn", "yarn.lock")
91+
packages, err := lockfile.ParseYarnLock(aLockfile)
92+
require.NoError(t, err)
93+
94+
cachedPackages, err := CacheGrafanaPackages(packages)
95+
require.NoError(t, err)
96+
97+
grafanaPackageCount := 0
98+
grafanaPackages := make(map[string]bool)
99+
for _, pkg := range cachedPackages {
100+
if pkg.Name == "@grafana/data" || pkg.Name == "@grafana/runtime" || pkg.Name == "@grafana/toolkit" || pkg.Name == "@grafana/ui" {
101+
grafanaPackageCount++
102+
grafanaPackages[pkg.Name] = true
103+
}
104+
}
105+
106+
require.GreaterOrEqual(t, grafanaPackageCount, 2, "should have at least 2 different @grafana/* packages in cache")
107+
require.True(t, grafanaPackages["@grafana/data"], "@grafana/data should be in cache")
108+
require.True(t, grafanaPackages["@grafana/runtime"], "@grafana/runtime should be in cache")
109+
110+
cacheHit, includedBy := IncludedByGrafanaPackage("moment", cachedPackages)
111+
require.True(t, cacheHit, "moment should be found in the cache")
112+
require.Equal(t, "@grafana/data", includedBy, "moment should be included by @grafana/data")
113+
}
114+
115+
func TestNonGrafanaDependencyVulnerabilitiesAreNotFiltered(t *testing.T) {
116+
t.Parallel()
117+
118+
aLockfile := filepath.Join("testdata", "node", "multi-version-npm", "package-lock.json")
119+
packages, err := lockfile.ParseNpmLock(aLockfile)
120+
require.NoError(t, err)
121+
122+
cachedPackages, err := CacheGrafanaPackages(packages)
123+
require.NoError(t, err)
124+
125+
bodyParserInCache, _ := IncludedByGrafanaPackage("body-parser", cachedPackages)
126+
require.False(t, bodyParserInCache, "body-parser should NOT be included by any Grafana package")
127+
128+
dompurifyInCache, includedByDompurify := IncludedByGrafanaPackage("dompurify", cachedPackages)
129+
require.True(t, dompurifyInCache, "dompurify should be included by a Grafana package")
130+
require.Equal(t, "@grafana/data", includedByDompurify, "dompurify should be included by @grafana/data")
131+
}

pkg/analysis/passes/osvscanner/osvscanner.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ func run(pass *analysis.Pass) (interface{}, error) {
9797
scanningPerformed = true
9898
data, err := doScanInternal(lockFile)
9999
if err != nil {
100-
logme.DebugFln("osv-scanner returned error (vulnerabilities found): %s", err.Error())
100+
logme.DebugFln(
101+
"osv-scanner returned error (vulnerabilities found): %s",
102+
err.Error(),
103+
)
101104
}
102105

103106
filteredResults := FilterOSVResults(data, lockFile)
@@ -138,19 +141,20 @@ func run(pass *analysis.Pass) (interface{}, error) {
138141
messagesReported[message] = true
139142
switch severity {
140143
case SeverityCritical:
144+
logme.DebugFln("osv-scanner detected a critical severity issue: %s", message)
141145
pass.ReportResult(
142146
pass.AnalyzerName,
143147
osvScannerCriticalSeverityDetected,
144148
"osv-scanner detected a critical severity issue",
145149
message)
146150
criticalSeverityCount++
147151
case SeverityHigh:
152+
logme.DebugFln("osv-scanner detected a high severity issue: %s", message)
148153
pass.ReportResult(
149154
pass.AnalyzerName,
150155
osvScannerHighSeverityDetected,
151156
"osv-scanner detected a high severity issue",
152-
message,
153-
)
157+
message)
154158
highSeverityCount++
155159
}
156160
}

pkg/analysis/passes/osvscanner/osvscanner_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package osvscanner
22

33
import (
44
"path/filepath"
5+
"strings"
56
"testing"
67

78
"github.com/google/osv-scanner/v2/pkg/models"
@@ -111,7 +112,7 @@ func TestOSVScannerAsLibrary(t *testing.T) {
111112
require.NoError(t, err)
112113
require.Len(t, interceptor.Diagnostics, 4)
113114

114-
// this results in three issues: 1 individual critical, 1 critical summary, 1 individual high, 1 high summary
115+
// this results in four issues: 1 individual critical, 1 critical summary, 1 individual high, 1 high summary
115116
messages := []string{
116117
"osv-scanner detected a critical severity issue",
117118
"osv-scanner detected critical severity issues",
@@ -154,3 +155,31 @@ func TestOSVScannerAsLibraryInvalidLockfile(t *testing.T) {
154155
require.NoError(t, err)
155156
require.Len(t, interceptor.Diagnostics, 0)
156157
}
158+
159+
func TestOSVScannerMultiVersionNPM(t *testing.T) {
160+
var interceptor testpassinterceptor.TestPassInterceptor
161+
pass := &analysis.Pass{
162+
RootDir: filepath.Join("./"),
163+
ResultOf: map[*analysis.Analyzer]interface{}{
164+
archive.Analyzer: filepath.Join("testdata", "node", "multi-version-npm"),
165+
sourcecode.Analyzer: filepath.Join("testdata", "node", "multi-version-npm"),
166+
},
167+
Report: interceptor.ReportInterceptor(),
168+
}
169+
170+
_, err := Analyzer.Run(pass)
171+
require.NoError(t, err)
172+
173+
titles := interceptor.GetTitles()
174+
require.Contains(t, titles, "osv-scanner detected high severity issues")
175+
176+
details := interceptor.GetDetails()
177+
hasBodyParserVulnerability := false
178+
for _, detail := range details {
179+
if strings.Contains(detail, "body-parser") && strings.Contains(detail, "SEVERITY: HIGH") {
180+
hasBodyParserVulnerability = true
181+
break
182+
}
183+
}
184+
require.True(t, hasBodyParserVulnerability, "body-parser high severity vulnerability should be reported")
185+
}

0 commit comments

Comments
 (0)