Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions pkg/analysis/passes/osvscanner/filter-gomod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package osvscanner

import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

"github.com/google/osv-scanner/v2/pkg/models"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"

"github.com/grafana/plugin-validator/pkg/logme"
)

const (
grafanaSDKModulePath = "github.com/grafana/grafana-plugin-sdk-go"
maxGoModSize = 2 << 20
)

type goModuleRequirement struct {
version string
indirect bool
}

var grafanaSDKGoModClient = &http.Client{Timeout: 10 * time.Second}

var fetchGrafanaSDKGoMod = func(version string) ([]byte, error) {
escapedVersion, err := module.EscapeVersion(version)
if err != nil {
return nil, fmt.Errorf("escape Grafana SDK version: %w", err)
}

url := "https://proxy.golang.org/" + grafanaSDKModulePath + "/@v/" + escapedVersion + ".mod"
response, err := grafanaSDKGoModClient.Get(url)
if err != nil {
return nil, fmt.Errorf("fetch Grafana SDK go.mod: %w", err)
}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetch Grafana SDK go.mod: unexpected status %s", response.Status)
}

content, err := io.ReadAll(io.LimitReader(response.Body, maxGoModSize+1))
if err != nil {
return nil, fmt.Errorf("read Grafana SDK go.mod: %w", err)
}
if len(content) > maxGoModSize {
return nil, fmt.Errorf("read Grafana SDK go.mod: response exceeds %d bytes", maxGoModSize)
}
return content, nil
}

func filterGoModResults(source models.VulnerabilityResults, goModPath string) models.VulnerabilityResults {
pluginContent, err := os.ReadFile(goModPath)
if err != nil {
logme.DebugFln("could not read plugin go.mod for OSV filtering: %s", err)
return source
}
pluginMod, err := modfile.Parse(goModPath, pluginContent, nil)
if err != nil {
logme.DebugFln("could not parse plugin go.mod for OSV filtering: %s", err)
return source
}

sdkVersion, ok := grafanaSDKVersion(pluginMod)
if !ok {
return source
}
sdkContent, err := fetchGrafanaSDKGoMod(sdkVersion)
if err != nil {
logme.DebugFln("could not load Grafana SDK go.mod for OSV filtering: %s", err)
return source
}
sdkMod, err := modfile.Parse("grafana-plugin-sdk-go.mod", sdkContent, nil)
if err != nil {
logme.DebugFln("could not parse Grafana SDK go.mod for OSV filtering: %s", err)
return source
}

pluginRequirements := goModuleRequirements(pluginMod)
sdkRequirements := goModuleRequirements(sdkMod)

filtered := source
filtered.Results = make([]models.PackageSource, 0, len(source.Results))
for _, result := range source.Results {
filteredResult := result
filteredResult.Packages = nil

for _, vulnerablePackage := range result.Packages {
name := vulnerablePackage.Package.Name
pluginRequirement, inPluginGoMod := pluginRequirements[name]
sdkRequirement, inSDKGoMod := sdkRequirements[name]

isSDKOwned := inPluginGoMod &&
pluginRequirement.indirect &&
inSDKGoMod &&
sameGoModuleVersion(pluginRequirement.version, vulnerablePackage.Package.Version) &&
sameGoModuleVersion(sdkRequirement.version, vulnerablePackage.Package.Version)
if isSDKOwned {
logme.DebugFln("excluded Grafana Go SDK dependency: %s@%s", name, vulnerablePackage.Package.Version)
continue
}
filteredResult.Packages = append(
filteredResult.Packages,
vulnerablePackage,
)
}
// drop result entries with no remaining findings so downstream can
// recognize a fully-filtered scan as passing (len(Results) == 0)
if len(filteredResult.Packages) > 0 {
filtered.Results = append(filtered.Results, filteredResult)
}
}
return filtered
}

func grafanaSDKVersion(file *modfile.File) (string, bool) {
for _, requirement := range file.Require {
if requirement.Mod.Path != grafanaSDKModulePath {
continue
}
version := requirement.Mod.Version
for _, replacement := range file.Replace {
if replacement.Old.Path != grafanaSDKModulePath ||
(replacement.Old.Version != "" && replacement.Old.Version != version) {
continue
}
if replacement.New.Path != grafanaSDKModulePath || replacement.New.Version == "" {
return "", false
}
version = replacement.New.Version
}
return version, version != ""
}
return "", false
}

func goModuleRequirements(file *modfile.File) map[string]goModuleRequirement {
requirements := make(map[string]goModuleRequirement, len(file.Require))
for _, requirement := range file.Require {
requirements[requirement.Mod.Path] = goModuleRequirement{
version: requirement.Mod.Version,
indirect: requirement.Indirect,
}
}
return requirements
}

func sameGoModuleVersion(left, right string) bool {
return strings.TrimPrefix(left, "v") == strings.TrimPrefix(right, "v")
}
3 changes: 1 addition & 2 deletions pkg/analysis/passes/osvscanner/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import (
)

func FilterOSVResults(source models.VulnerabilityResults, lockFile string) models.VulnerabilityResults {
// not filtering go.mod yet
if strings.HasSuffix(lockFile, "go.mod") {
return source
return filterGoModResults(source, lockFile)
}
var filtered models.VulnerabilityResults
// this expects a single result, with multiple packages since we are scanning a single file per-run
Expand Down
141 changes: 141 additions & 0 deletions pkg/analysis/passes/osvscanner/filter_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package osvscanner

import (
"errors"
"os"
"path/filepath"
"testing"

Expand Down Expand Up @@ -34,3 +36,142 @@ func TestFilterPackages(t *testing.T) {
require.Len(t, filteredResults.Results, 1)
require.Equal(t, "d3-color", filteredResults.Results[0].Source.Path)
}

func TestFilterGoModPackages(t *testing.T) {
goMod := `module example.com/plugin

go 1.22

require (
github.com/grafana/grafana-plugin-sdk-go v0.250.0
github.com/getkin/kin-openapi v0.124.0 // indirect
github.com/plugin/direct v1.0.0
github.com/shared/direct v1.2.0
github.com/unrelated/indirect v1.0.0 // indirect
github.com/versioned/indirect v1.1.0 // indirect
)
`
sdkGoMod := []byte(`module github.com/grafana/grafana-plugin-sdk-go

go 1.21

require (
github.com/getkin/kin-openapi v0.124.0
github.com/shared/direct v1.2.0 // indirect
github.com/versioned/indirect v1.0.0 // indirect
)
`)

goModPath := filepath.Join(t.TempDir(), "go.mod")
require.NoError(t, os.WriteFile(goModPath, []byte(goMod), 0o600))

actualFetch := fetchGrafanaSDKGoMod
t.Cleanup(func() { fetchGrafanaSDKGoMod = actualFetch })
fetchGrafanaSDKGoMod = func(version string) ([]byte, error) {
require.Equal(t, "v0.250.0", version)
return sdkGoMod, nil
}

source := vulnerabilityResults(
"github.com/getkin/kin-openapi", "v0.124.0",
"github.com/plugin/direct", "v1.0.0",
"github.com/shared/direct", "v1.2.0",
"github.com/unrelated/indirect", "v1.0.0",
"github.com/versioned/indirect", "v1.1.0",
)

filtered := FilterOSVResults(source, goModPath)

require.Equal(t, []string{
"github.com/plugin/direct",
"github.com/shared/direct",
"github.com/unrelated/indirect",
"github.com/versioned/indirect",
}, packageNames(filtered))
}

func TestFilterGoModPackagesAllFiltered(t *testing.T) {
goMod := `module example.com/plugin

go 1.22

require (
github.com/grafana/grafana-plugin-sdk-go v0.250.0
github.com/getkin/kin-openapi v0.124.0 // indirect
)
`
sdkGoMod := []byte(`module github.com/grafana/grafana-plugin-sdk-go

go 1.21

require (
github.com/getkin/kin-openapi v0.124.0
)
`)

goModPath := filepath.Join(t.TempDir(), "go.mod")
require.NoError(t, os.WriteFile(goModPath, []byte(goMod), 0o600))

actualFetch := fetchGrafanaSDKGoMod
t.Cleanup(func() { fetchGrafanaSDKGoMod = actualFetch })
fetchGrafanaSDKGoMod = func(version string) ([]byte, error) {
require.Equal(t, "v0.250.0", version)
return sdkGoMod, nil
}

source := vulnerabilityResults("github.com/getkin/kin-openapi", "v0.124.0")

filtered := FilterOSVResults(source, goModPath)

// every finding is SDK-owned, so the results slice must be empty so the
// downstream analyzer can report a clean pass (len(Results) == 0)
require.Empty(t, filtered.Results)
}

func TestFilterGoModPackagesFailsOpen(t *testing.T) {
goMod := `module example.com/plugin

go 1.22

require (
github.com/grafana/grafana-plugin-sdk-go v0.250.0
github.com/getkin/kin-openapi v0.124.0 // indirect
)
`
goModPath := filepath.Join(t.TempDir(), "go.mod")
require.NoError(t, os.WriteFile(goModPath, []byte(goMod), 0o600))

actualFetch := fetchGrafanaSDKGoMod
t.Cleanup(func() { fetchGrafanaSDKGoMod = actualFetch })
fetchGrafanaSDKGoMod = func(string) ([]byte, error) {
return nil, errors.New("module proxy unavailable")
}

source := vulnerabilityResults("github.com/getkin/kin-openapi", "v0.124.0")

require.Equal(t, source, FilterOSVResults(source, goModPath))
}

func vulnerabilityResults(packages ...string) models.VulnerabilityResults {
result := models.VulnerabilityResults{
Results: []models.PackageSource{{
Source: models.SourceInfo{Path: "go.mod", Type: "lockfile"},
}},
}
for i := 0; i < len(packages); i += 2 {
result.Results[0].Packages = append(result.Results[0].Packages, models.PackageVulns{
Package: models.PackageInfo{Name: packages[i], Version: packages[i+1]},
})
}
return result
}

func packageNames(result models.VulnerabilityResults) []string {
var names []string
for _, source := range result.Results {
for _, pkg := range source.Packages {
names = append(names, pkg.Package.Name)
}
}
return names
}