From b6c013ed67c9753b291e594a5cac36cdac59c44a Mon Sep 17 00:00:00 2001 From: Michael Kedar Date: Thu, 11 Apr 2024 11:20:31 +1000 Subject: [PATCH] GR: resolve tests & mock client (#909) Some tests checking dependency resolution works as expected and correctly finds the vulnerable dependency chains. Includes a new `MockResolutionClient` so I can test guided resolution on toy data without relying on the live OSV / deps.dev APIs. --- go.mod | 8 +- go.sum | 16 +- .../__snapshots__/resolve_test.snap | 522 ++++++++++++++++++ .../clienttest/mock_resolution_client.go | 89 +++ .../resolution/fixtures/basic-universe.yaml | 69 +++ .../resolution/fixtures/complex-universe.yaml | 61 ++ .../resolution/fixtures/diamond-universe.yaml | 27 + internal/resolution/resolve_test.go | 249 +++++++++ internal/resolution/testmain_test.go | 16 + 9 files changed, 1045 insertions(+), 12 deletions(-) create mode 100755 internal/resolution/__snapshots__/resolve_test.snap create mode 100644 internal/resolution/clienttest/mock_resolution_client.go create mode 100644 internal/resolution/fixtures/basic-universe.yaml create mode 100644 internal/resolution/fixtures/complex-universe.yaml create mode 100644 internal/resolution/fixtures/diamond-universe.yaml create mode 100644 internal/resolution/resolve_test.go create mode 100644 internal/resolution/testmain_test.go diff --git a/go.mod b/go.mod index 5c739f5883..55e584e1c7 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/google/osv-scanner go 1.21.8 require ( - deps.dev/api/v3 v3.0.0-20240408233705-d32937cb3c73 - deps.dev/util/maven v0.0.0-20240408233705-d32937cb3c73 - deps.dev/util/resolve v0.0.0-20240408233705-d32937cb3c73 - deps.dev/util/semver v0.0.0-20240408233705-d32937cb3c73 + deps.dev/api/v3 v3.0.0-20240410004301-2c48bd578133 + deps.dev/util/maven v0.0.0-20240410004301-2c48bd578133 + deps.dev/util/resolve v0.0.0-20240410004301-2c48bd578133 + deps.dev/util/semver v0.0.0-20240410004301-2c48bd578133 github.com/BurntSushi/toml v1.3.2 github.com/CycloneDX/cyclonedx-go v0.8.0 github.com/charmbracelet/bubbles v0.18.0 diff --git a/go.sum b/go.sum index 4fc14ec888..3105355248 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -deps.dev/api/v3 v3.0.0-20240408233705-d32937cb3c73 h1:EkRr4/wSWtl8d2+PyuXsPNCUZr0avlULXXqW54s1tck= -deps.dev/api/v3 v3.0.0-20240408233705-d32937cb3c73/go.mod h1:k3RHZwAw7ijqoXmVDvcO7ikeTwTC4jtmhCDathV+IKE= -deps.dev/util/maven v0.0.0-20240408233705-d32937cb3c73 h1:H2SKkJ/f6wbFbDV751Y4QoKwYtndcncTju2z5nceaRg= -deps.dev/util/maven v0.0.0-20240408233705-d32937cb3c73/go.mod h1:SBW3EribdkZYk6zxi5oVn/ZECvi4ixb7EGgEWfSimNk= -deps.dev/util/resolve v0.0.0-20240408233705-d32937cb3c73 h1:UPGf40y60F4vTopLzuvv97KaEjB9E4VplJnyfrhtV+Y= -deps.dev/util/resolve v0.0.0-20240408233705-d32937cb3c73/go.mod h1:8JnoxYaxXYJ0gJ9RbNPFgCZFDO/TMNFrHTcRkGApBV0= -deps.dev/util/semver v0.0.0-20240408233705-d32937cb3c73 h1:gj+40U8zWZ/rPmxrbaXEJI1td0tqbBCyrG4GwFhyC5U= -deps.dev/util/semver v0.0.0-20240408233705-d32937cb3c73/go.mod h1:jkcH+k02gWHBiZ7G4OnUOkSZ6WDq54Pt5DrOA8FN8Uo= +deps.dev/api/v3 v3.0.0-20240410004301-2c48bd578133 h1:RZj09g++MGd4JkSmFqU5g3KJiuY2LB5LW2vgOVsyYp4= +deps.dev/api/v3 v3.0.0-20240410004301-2c48bd578133/go.mod h1:k3RHZwAw7ijqoXmVDvcO7ikeTwTC4jtmhCDathV+IKE= +deps.dev/util/maven v0.0.0-20240410004301-2c48bd578133 h1:QHLkms89+9CAsK9XajnsBUdeDQ/bx9VOemJZD1e6/Dg= +deps.dev/util/maven v0.0.0-20240410004301-2c48bd578133/go.mod h1:SBW3EribdkZYk6zxi5oVn/ZECvi4ixb7EGgEWfSimNk= +deps.dev/util/resolve v0.0.0-20240410004301-2c48bd578133 h1:jnYj12u4w8JUwT5Aom+zSJd1fm3YPkVYGG3wWNT0W80= +deps.dev/util/resolve v0.0.0-20240410004301-2c48bd578133/go.mod h1:8JnoxYaxXYJ0gJ9RbNPFgCZFDO/TMNFrHTcRkGApBV0= +deps.dev/util/semver v0.0.0-20240410004301-2c48bd578133 h1:ezuCAD47LNWxNRZ9KtDYWfOx7OtdEG+bQ4a6uN6pIiM= +deps.dev/util/semver v0.0.0-20240410004301-2c48bd578133/go.mod h1:jkcH+k02gWHBiZ7G4OnUOkSZ6WDq54Pt5DrOA8FN8Uo= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M= diff --git a/internal/resolution/__snapshots__/resolve_test.snap b/internal/resolution/__snapshots__/resolve_test.snap new file mode 100755 index 0000000000..cf4b7406c8 --- /dev/null +++ b/internal/resolution/__snapshots__/resolve_test.snap @@ -0,0 +1,522 @@ + +[TestResolve/complex - 1] +complex 9.9.9 +├─ reg|KnownAs="chuck"|Selector="" | alice@^1.0.0 1.0.1 +│ └─ $1@^2.0.0 +├─ 1: reg|Selector="" | bob@2.2.2 2.2.2 +└─ reg|Selector="" | dave@~3.3.3 3.3.3 + ├─ $1@^2.2.2 + └─ reg|KnownAs="duck"|Selector="" | chuck@^2.0.0 2.0.0 + └─ $1@^2.0.1 + +--- + +[TestResolve/complex - 2] +[ + { + "ID": "CMPLX-0000-0000", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + }, + { + "ID": "CMPLX-1000-0000", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 2, + "Requirement": "2.2.2", + "Type": {} + } + ], + [ + { + "From": 1, + "To": 2, + "Requirement": "^2.0.0", + "Type": {} + }, + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ], + [ + { + "From": 3, + "To": 2, + "Requirement": "^2.2.2", + "Type": {} + }, + { + "From": 0, + "To": 3, + "Requirement": "~3.3.3", + "Type": {} + } + ], + [ + { + "From": 4, + "To": 2, + "Requirement": "^2.0.1", + "Type": {} + }, + { + "From": 3, + "To": 4, + "Requirement": "^2.0.0", + "Type": {} + }, + { + "From": 0, + "To": 3, + "Requirement": "~3.3.3", + "Type": {} + } + ] + ], + "NonProblemChains": [] + }, + { + "ID": "CMPLX-2000-0000", + "DevOnly": true, + "ProblemChains": [ + [ + { + "From": 3, + "To": 4, + "Requirement": "^2.0.0", + "Type": {} + }, + { + "From": 0, + "To": 3, + "Requirement": "~3.3.3", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- + +[TestResolve/diamond - 1] +diamond 1.0.0 +├─ reg|Selector="" | pkg@^1.0.0 1.0.0 +│ └─ 1: reg|Selector="" | bad@^1.0.0 1.1.1 +└─ reg|Selector="" | dep-one@^1.0.0 1.0.0 + ├─ $1@^1.0.0 + └─ reg|Selector="" | dep-two@^1.0.0 1.0.0 + └─ $1@^1.0.0 + +--- + +[TestResolve/diamond - 2] +[ + { + "ID": "DIA-000-000", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 1, + "To": 3, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ], + [ + { + "From": 2, + "To": 3, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 2, + "Requirement": "^1.0.0", + "Type": {} + } + ], + [ + { + "From": 4, + "To": 3, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 2, + "To": 4, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 2, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- + +[TestResolve/different-pkgs - 1] +different-pkgs 3.0.0 +├─ reg|Selector="" | bad2@^1.0.0 1.0.0 +└─ reg|Selector="" | dependency@^1.0.0 1.0.0 + └─ reg|Selector="" | bad@^1.0.0 1.1.1 + +--- + +[TestResolve/different-pkgs - 2] +[ + { + "ID": "OSV-000-000", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 2, + "To": 3, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 2, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + }, + { + "ID": "OSV-000-001", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ], + [ + { + "From": 2, + "To": 3, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 2, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- + +[TestResolve/direct - 1] +direct 1.0.0 +└─ reg|Selector="" | bad@^2.0.0 2.2.2 + +--- + +[TestResolve/direct - 2] +[ + { + "ID": "OSV-000-001", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 1, + "Requirement": "^2.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- + +[TestResolve/duplicates - 1] +duplicates 1.1.1 +├─ reg|KnownAs="bad-aliased"|Selector="" | bad@^1.0.0 1.1.1 +├─ reg|Selector="" | dependency@^2.0.0 2.0.0 +│ └─ reg|Selector="" | bad@^2.0.0 2.2.2 +└─ reg|KnownAs="dependency-v1"|Selector="" | dependency@^1.0.0 1.0.0 + └─ reg|Selector="" | bad@^1.0.0 1.1.1 + +--- + +[TestResolve/duplicates - 2] +[ + { + "ID": "OSV-000-000", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ], + [ + { + "From": 3, + "To": 5, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 3, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + }, + { + "ID": "OSV-000-001", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ], + [ + { + "From": 2, + "To": 4, + "Requirement": "^2.0.0", + "Type": {} + }, + { + "From": 0, + "To": 2, + "Requirement": "^2.0.0", + "Type": {} + } + ], + [ + { + "From": 3, + "To": 5, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 3, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- + +[TestResolve/existing - 1] +existing 1.0.0 +└─ opt|Selector="" | dependency@^2.0.0 2.0.0 + └─ reg|Selector="" | bad@^2.0.0 2.2.2 + +--- + +[TestResolve/existing - 2] +[ + { + "ID": "OSV-000-001", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 1, + "To": 2, + "Requirement": "^2.0.0", + "Type": {} + }, + { + "From": 0, + "To": 1, + "Requirement": "^2.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- + +[TestResolve/non-problem - 1] +non-problem 1.0.0 +├─ 1: reg|Selector="" | bad@^1.0.0 1.1.1 +└─ reg|Selector="" | dependency@^3.0.0 3.0.0 + └─ $1@* + +--- + +[TestResolve/non-problem - 2] +[ + { + "ID": "OSV-000-000", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [ + [ + { + "From": 2, + "To": 1, + "Requirement": "*", + "Type": {} + }, + { + "From": 0, + "To": 2, + "Requirement": "^3.0.0", + "Type": {} + } + ] + ] + }, + { + "ID": "OSV-000-001", + "DevOnly": false, + "ProblemChains": [ + [ + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ], + [ + { + "From": 2, + "To": 1, + "Requirement": "*", + "Type": {} + }, + { + "From": 0, + "To": 2, + "Requirement": "^3.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- + +[TestResolve/simple - 1] +simple 1.0.0 +└─ reg|Selector="" | dependency@^1.0.0 1.0.0 + └─ reg|Selector="" | bad@^1.0.0 1.1.1 + +--- + +[TestResolve/simple - 2] +[ + { + "ID": "OSV-000-000", + "DevOnly": true, + "ProblemChains": [ + [ + { + "From": 1, + "To": 2, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + }, + { + "ID": "OSV-000-001", + "DevOnly": true, + "ProblemChains": [ + [ + { + "From": 1, + "To": 2, + "Requirement": "^1.0.0", + "Type": {} + }, + { + "From": 0, + "To": 1, + "Requirement": "^1.0.0", + "Type": {} + } + ] + ], + "NonProblemChains": [] + } +] +--- diff --git a/internal/resolution/clienttest/mock_resolution_client.go b/internal/resolution/clienttest/mock_resolution_client.go new file mode 100644 index 0000000000..208879c459 --- /dev/null +++ b/internal/resolution/clienttest/mock_resolution_client.go @@ -0,0 +1,89 @@ +package clienttest + +import ( + "context" + "os" + "strings" + "testing" + + "deps.dev/util/resolve" + "deps.dev/util/resolve/schema" + "github.com/google/osv-scanner/internal/resolution/client" + "github.com/google/osv-scanner/internal/resolution/util" + "github.com/google/osv-scanner/internal/utility/vulns" + "github.com/google/osv-scanner/pkg/models" + "gopkg.in/yaml.v3" +) + +type resolutionUniverse struct { + System string `yaml:"system"` + Schema string `yaml:"schema"` + Vulns []models.Vulnerability `yaml:"vulns"` +} + +type mockVulnerabilityClient []models.Vulnerability + +func (mvc mockVulnerabilityClient) FindVulns(g *resolve.Graph) ([]models.Vulnerabilities, error) { + result := make([]models.Vulnerabilities, len(g.Nodes)) + for i, n := range g.Nodes { + if i == 0 { + continue // skip root node + } + for _, v := range mvc { + if vulns.IsAffected(v, util.VKToPackageDetails(n.Version)) { + result[i] = append(result[i], v) + } + } + } + + return result, nil +} + +type mockDependencyClient struct { + *resolve.LocalClient +} + +func (mdc mockDependencyClient) LoadCache(string) error { return nil } +func (mdc mockDependencyClient) WriteCache(string) error { return nil } +func (mdc mockDependencyClient) PreFetch(context.Context, []resolve.RequirementVersion, string) {} + +func NewMockResolutionClient(t *testing.T, universeYAML string) client.ResolutionClient { + t.Helper() + f, err := os.Open(universeYAML) + if err != nil { + t.Fatalf("failed opening mock universe: %v", err) + } + defer f.Close() + dec := yaml.NewDecoder(f) + + var universe resolutionUniverse + if err := dec.Decode(&universe); err != nil { + t.Fatalf("failed decoding mock universe: %v", err) + } + + cl := client.ResolutionClient{ + VulnerabilityClient: mockVulnerabilityClient(universe.Vulns), + } + + var sys resolve.System + switch strings.ToLower(universe.System) { + case "npm": + sys = resolve.NPM + case "maven": + sys = resolve.Maven + default: + t.Fatalf("unknown ecosystem in universe: %s", universe.System) + } + + // schema needs a strict tab indentation, which is awkward to do within the YAML. + // Replace double space from yaml with single tab + universe.Schema = strings.ReplaceAll(universe.Schema, " ", "\t") + sch, err := schema.New(universe.Schema, sys) + if err != nil { + t.Fatalf("failed parsing schema: %v", err) + } + + cl.DependencyClient = mockDependencyClient{sch.NewClient()} + + return cl +} diff --git a/internal/resolution/fixtures/basic-universe.yaml b/internal/resolution/fixtures/basic-universe.yaml new file mode 100644 index 0000000000..a1ecf43c3e --- /dev/null +++ b/internal/resolution/fixtures/basic-universe.yaml @@ -0,0 +1,69 @@ +system: npm +schema: | + dependency + 0.0.1 + 1.0.0 + bad@^1.0.0 + 2.0.0 + bad@^2.0.0 + 3.0.0 + bad@* + bad + 1.0.0 + 1.1.1 + 2.0.0 + 2.2.2 + bad2 + 1.0.0 + existing + 1.0.0 + bad@^1.0.0 + bad2@^1.0.0 + dependency@^0.0.1 + +vulns: + - id: OSV-000-000 + affected: + - package: + ecosystem: npm + name: bad + ranges: + - type: SEMVER + events: + - introduced: '0' + - fixed: '2.0.0' + - id: OSV-000-001 + affected: + - package: + ecosystem: npm + name: bad + ranges: + - type: SEMVER + events: + - introduced: '0' + - package: + ecosystem: npm + name: bad2 + ranges: + - type: SEMVER + events: + - introduced: '0' + - id: OSV-000-002 + affected: + - package: + ecosystem: npm + name: dependency + ranges: + - type: SEMVER + events: + - introduced: '0' + - fixed: '1.0.0' + - id: OSV-000-003 + affected: + - package: + ecosystem: npm + name: existing + ranges: + - type: SEMVER + events: + - introduced: '0' diff --git a/internal/resolution/fixtures/complex-universe.yaml b/internal/resolution/fixtures/complex-universe.yaml new file mode 100644 index 0000000000..8e2b429bb2 --- /dev/null +++ b/internal/resolution/fixtures/complex-universe.yaml @@ -0,0 +1,61 @@ +system: npm +schema: | + alice + 1.0.0 + 1.0.1 + bob@^2.0.0 + bob + 2.0.0 + 2.0.1 + 2.2.2 + chuck + 2.0.0 + bob@^2.0.1 + dave + 3.3.3 + bob@^2.2.2 + KnownAs duck|chuck@^2.0.0 + +vulns: + - id: CMPLX-0000-0000 + affected: + - package: + ecosystem: npm + name: alice + ranges: + - type: SEMVER + events: + - introduced: '1.0.1' + - fixed: '2.0.0' + - id: CMPLX-1000-0000 + affected: + - package: + ecosystem: npm + name: bob + ranges: + - type: SEMVER + events: + - introduced: '0' + - id: CMPLX-2000-0000 + affected: + - package: + ecosystem: npm + name: chuck + ranges: + - type: SEMVER + events: + - introduced: '0' + - fixed: '1.0.1' + - introduced: '2.0.0' + - fixed: '3.0.0' + - id: CMPLX-3000-0000 + affected: + - package: + ecosystem: npm + name: dave + ranges: + - type: SEMVER + events: + - introduced: '0' + - fixed: '3.3.2' + - introduced: '3.3.4' diff --git a/internal/resolution/fixtures/diamond-universe.yaml b/internal/resolution/fixtures/diamond-universe.yaml new file mode 100644 index 0000000000..eb9494c2f6 --- /dev/null +++ b/internal/resolution/fixtures/diamond-universe.yaml @@ -0,0 +1,27 @@ +system: npm +schema: | + pkg + 1.0.0 + bad@^1.0.0 + dep-one + 1.0.0 + bad@^1.0.0 + dep-two@^1.0.0 + dep-two + 1.0.0 + bad@^1.0.0 + bad + 1.0.0 + 1.1.1 + +vulns: + - id: DIA-000-000 + affected: + - package: + ecosystem: npm + name: bad + ranges: + - type: SEMVER + events: + - introduced: '0' + - fixed: '2.0.0' diff --git a/internal/resolution/resolve_test.go b/internal/resolution/resolve_test.go new file mode 100644 index 0000000000..deb3d83f44 --- /dev/null +++ b/internal/resolution/resolve_test.go @@ -0,0 +1,249 @@ +package resolution_test + +import ( + "cmp" + "context" + "slices" + "testing" + + "deps.dev/util/resolve" + "deps.dev/util/resolve/dep" + "github.com/google/osv-scanner/internal/resolution" + "github.com/google/osv-scanner/internal/resolution/clienttest" + "github.com/google/osv-scanner/internal/resolution/manifest" + "github.com/google/osv-scanner/internal/testutility" +) + +func checkResult(t *testing.T, result *resolution.ResolutionResult) { + t.Helper() + snap := testutility.NewSnapshot() + snap.MatchText(t, result.Graph.String()) + + type minimalVuln struct { + ID string + DevOnly bool + ProblemChains [][]resolve.Edge + NonProblemChains [][]resolve.Edge + } + + minVulns := make([]minimalVuln, len(result.Vulns)) + for i, v := range result.Vulns { + minVulns[i] = minimalVuln{ + ID: v.Vulnerability.ID, + DevOnly: v.DevOnly, + ProblemChains: make([][]resolve.Edge, len(v.ProblemChains)), + NonProblemChains: make([][]resolve.Edge, len(v.NonProblemChains)), + } + for j, c := range v.ProblemChains { + minVulns[i].ProblemChains[j] = c.Edges + } + for j, c := range v.NonProblemChains { + minVulns[i].NonProblemChains[j] = c.Edges + } + } + slices.SortFunc(minVulns, func(a, b minimalVuln) int { + return cmp.Compare(a.ID, b.ID) + }) + snap.MatchJSON(t, minVulns) +} + +func TestResolve(t *testing.T) { + t.Parallel() + + aliasType := func(knownAs string) dep.Type { + t.Helper() + typ := dep.NewType() + typ.AddAttr(dep.KnownAs, knownAs) + + return typ + } + + type requirement struct { + name string + version string + typ dep.Type + groups []string + } + tests := []struct { + name string + version string + system resolve.System + universe string + requirements []requirement + }{ + { + name: "simple", // simple root -> dependency -> vuln + version: "1.0.0", + system: resolve.NPM, + universe: "./fixtures/basic-universe.yaml", + requirements: []requirement{ + { + name: "dependency", + version: "^1.0.0", + groups: []string{"dev"}, + }, + }, + }, + { + name: "direct", // vulnerability in direct dependency + version: "1.0.0", + system: resolve.NPM, + universe: "./fixtures/basic-universe.yaml", + requirements: []requirement{ + { + name: "bad", + version: "^2.0.0", + }, + }, + }, + { + name: "duplicates", // same package with vulns included multiple times + version: "1.1.1", + system: resolve.NPM, + universe: "./fixtures/basic-universe.yaml", + requirements: []requirement{ + { + name: "bad", + version: "^1.0.0", + typ: aliasType("bad-aliased"), + }, + { + name: "dependency", + version: "^2.0.0", + groups: []string{"dev"}, + }, + { + name: "dependency", + version: "^1.0.0", + typ: aliasType("dependency-v1"), + }, + }, + }, + { + name: "different-pkgs", // same vuln in two different packages + version: "3.0.0", + system: resolve.NPM, + universe: "./fixtures/basic-universe.yaml", + requirements: []requirement{ + { + name: "bad2", + version: "^1.0.0", + }, + { + name: "dependency", + version: "^1.0.0", + }, + }, + }, + { + name: "existing", // manifest package/version exists in universe already + version: "1.0.0", + system: resolve.NPM, + universe: "./fixtures/basic-universe.yaml", + requirements: []requirement{ + { + name: "dependency", + version: "^2.0.0", + typ: dep.NewType(dep.Opt), + }, + }, + }, + { + name: "non-problem", // non-problem chains + version: "1.0.0", + system: resolve.NPM, + universe: "./fixtures/basic-universe.yaml", + requirements: []requirement{ + { + name: "bad", + version: "^1.0.0", + }, + { + name: "dependency", + version: "^3.0.0", + }, + }, + }, + { + name: "diamond", // diamond dependency on vulnerable pkg + version: "1.0.0", + system: resolve.NPM, + universe: "./fixtures/diamond-universe.yaml", + requirements: []requirement{ + { + name: "pkg", + version: "^1.0.0", + }, + { + name: "dep-one", + version: "^1.0.0", + groups: []string{"dev"}, + }, + }, + }, + { + name: "complex", // more complex graph/vulnerability structure + version: "9.9.9", + system: resolve.NPM, + universe: "./fixtures/complex-universe.yaml", + requirements: []requirement{ + { + name: "alice", + version: "^1.0.0", + typ: aliasType("chuck"), + }, + { + name: "bob", + version: "2.2.2", + }, + { + name: "dave", + version: "~3.3.3", + groups: []string{"dev"}, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cl := clienttest.NewMockResolutionClient(t, tt.universe) + var m manifest.Manifest + m.Root = resolve.Version{ + VersionKey: resolve.VersionKey{ + PackageKey: resolve.PackageKey{ + Name: tt.name, + System: tt.system, + }, + Version: tt.version, + VersionType: resolve.Concrete, + }, + } + m.Groups = make(map[resolve.PackageKey][]string) + m.Requirements = make([]resolve.RequirementVersion, len(tt.requirements)) + for i, req := range tt.requirements { + pk := resolve.PackageKey{ + Name: req.name, + System: tt.system, + } + m.Requirements[i] = resolve.RequirementVersion{ + VersionKey: resolve.VersionKey{ + PackageKey: pk, + Version: req.version, + VersionType: resolve.Requirement, + }, + Type: req.typ, + } + m.Groups[pk] = req.groups + } + + res, err := resolution.Resolve(context.Background(), cl, m) + if err != nil { + t.Fatalf("error resolving: %v", err) + } + checkResult(t, res) + }) + } +} diff --git a/internal/resolution/testmain_test.go b/internal/resolution/testmain_test.go new file mode 100644 index 0000000000..0f6d1b756e --- /dev/null +++ b/internal/resolution/testmain_test.go @@ -0,0 +1,16 @@ +package resolution_test + +import ( + "os" + "testing" + + "github.com/google/osv-scanner/internal/testutility" +) + +func TestMain(m *testing.M) { + code := m.Run() + + testutility.CleanSnapshots(m) + + os.Exit(code) +}