Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: guided remediation dependency resolution #432

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
10 changes: 6 additions & 4 deletions extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import (

// Extractor extracts Maven packages with transitive dependency resolution.
type Extractor struct {
client.DependencyClient
resolve.Client
*datasource.MavenRegistryAPIClient
}

Expand Down Expand Up @@ -98,12 +98,14 @@ func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]
for i, reg := range registries {
clientRegs[i] = reg
}
if err := e.DependencyClient.AddRegistries(clientRegs); err != nil {
return nil, err
if cl, ok := e.Client.(client.ClientWithRegistries); ok {
if err := cl.AddRegistries(clientRegs); err != nil {
return nil, err
}
}
}

overrideClient := client.NewOverrideClient(e.DependencyClient)
overrideClient := client.NewOverrideClient(e.Client)
resolver := mavenresolve.NewResolver(overrideClient)

// Resolve the dependencies.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func TestExtractor_Extract(t *testing.T) {
t.Run(tt.Name, func(t *testing.T) {
resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml")
extr := pomxmlnet.Extractor{
DependencyClient: resolutionClient,
Client: resolutionClient,
MavenRegistryAPIClient: &datasource.MavenRegistryAPIClient{},
}

Expand Down Expand Up @@ -351,7 +351,7 @@ func TestExtractor_Extract_WithMockServer(t *testing.T) {

resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml")
extr := pomxmlnet.Extractor{
DependencyClient: resolutionClient,
Client: resolutionClient,
MavenRegistryAPIClient: apiClient,
}

Expand Down
53 changes: 53 additions & 0 deletions internal/guidedremediation/manifest/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package manifest provides methods for parsing and writing manifest files.
package manifest

import (
"deps.dev/util/resolve"
"github.com/google/osv-scalibr/internal/guidedremediation/manifest/maven"
"github.com/google/osv-scalibr/internal/guidedremediation/manifest/npm"
)

// Manifest is the interface for the representation of a manifest file needed for dependency resolution.
type Manifest interface {
FilePath() string // Path to the manifest file
Root() resolve.Version // Version representing this package
System() resolve.System // The System of this manifest
Requirements() []resolve.RequirementVersion // All direct requirements, including dev
Groups() map[RequirementKey][]string // Dependency groups that the imports belong to
LocalManifests() []Manifest // Manifests of local packages
EcosystemSpecific() any // Any ecosystem-specific information needed

Clone() Manifest // Clone the manifest
}

// RequirementKey is a comparable type that uniquely identifies a package dependency in a manifest.
// It does not include the version specification.
type RequirementKey any

// MakeRequirementKey constructs an ecosystem-specific RequirementKey from the given RequirementVersion.
func MakeRequirementKey(requirement resolve.RequirementVersion) RequirementKey {
switch requirement.System {
case resolve.NPM:
return npm.MakeRequirementKey(requirement)
case resolve.Maven:
return maven.MakeRequirementKey(requirement)
case resolve.UnknownSystem:
fallthrough
default:
return requirement.PackageKey
}
}
65 changes: 65 additions & 0 deletions internal/guidedremediation/manifest/maven/pomxml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package maven provides the manifest parsing and writing for the Maven pom.xml format.
package maven

import (
"deps.dev/util/maven"
"deps.dev/util/resolve"
"deps.dev/util/resolve/dep"
)

// RequirementKey is a comparable type that uniquely identifies a package dependency in a manifest.
type RequirementKey struct {
resolve.PackageKey
ArtifactType string
Classifier string
}

var _ map[RequirementKey]interface{}

// MakeRequirementKey constructs a maven RequirementKey from the given RequirementVersion.
func MakeRequirementKey(requirement resolve.RequirementVersion) RequirementKey {
// Maven dependencies must have unique groupId:artifactId:type:classifier.
artifactType, _ := requirement.Type.GetAttr(dep.MavenArtifactType)
classifier, _ := requirement.Type.GetAttr(dep.MavenClassifier)

return RequirementKey{
PackageKey: requirement.PackageKey,
ArtifactType: artifactType,
Classifier: classifier,
}
}

// ManifestSpecific is ecosystem-specific information needed for the pom.xml manifest.
type ManifestSpecific struct {
Parent maven.Parent
Properties []PropertyWithOrigin // Properties from the base project
OriginalRequirements []DependencyWithOrigin // Dependencies from the base project
RequirementsForUpdates []resolve.RequirementVersion // Requirements that we only need for updates
Repositories []maven.Repository
}

// PropertyWithOrigin is a maven property with the origin where it comes from.
type PropertyWithOrigin struct {
maven.Property
Origin string // Origin indicates where the property comes from
}

// DependencyWithOrigin is a maven dependency with the origin where it comes from.
type DependencyWithOrigin struct {
maven.Dependency
Origin string // Origin indicates where the dependency comes from
}
41 changes: 41 additions & 0 deletions internal/guidedremediation/manifest/npm/packagejson.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package maven provides the manifest parsing and writing for the npm package.json format.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm

package npm

import (
"deps.dev/util/resolve"
"deps.dev/util/resolve/dep"
)

// RequirementKey is a comparable type that uniquely identifies a package dependency in a manifest.
type RequirementKey struct {
resolve.PackageKey
KnownAs string
}

var _ map[RequirementKey]interface{}

// MakeRequirementKey constructs an npm RequirementKey from the given RequirementVersion.
func MakeRequirementKey(requirement resolve.RequirementVersion) RequirementKey {
// Npm requirements are the uniquely identified by the key in the dependencies fields (which ends up being the path in node_modules)
// Declaring a dependency in multiple places (dependencies, devDependencies, optionalDependencies) only installs it once at one version.
// Aliases & non-registry dependencies are keyed on their 'KnownAs' attribute.
knownAs, _ := requirement.Type.GetAttr(dep.KnownAs)
return RequirementKey{
PackageKey: requirement.PackageKey,
KnownAs: knownAs,
}
}
53 changes: 53 additions & 0 deletions internal/guidedremediation/matcher/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package matcher provides the interface for the vulnerability matcher used by guided remediation.
package matcher

import (
"context"

"github.com/google/osv-scalibr/extractor"
)

// Temporarily internal while migration is in progress.
// Will need to be moved to publicly accessible location once external interface is created.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a TODO here and link to a github issue about moving guided remediation


// VulnerabilityMatcher interface provides functionality get a list of affecting vulnerabilities for each package in an inventory.
type VulnerabilityMatcher interface {
MatchVulnerabilities(ctx context.Context, invs []*extractor.Inventory) ([][]*OSVRecord, error)
}

// OSVRecord is a representation of an OSV record.
// TODO: replace with https://github.com/ossf/osv-schema/pull/333
type OSVRecord struct {
Comment on lines +32 to +34
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should be able to do this soon

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internally we have a policy that all TODOs need to have a bug attached - can you create a github issue for moving Guided Remediation and refer to it here?

ID string `yaml:"id"`
Affected []struct {
Package struct {
Ecosystem string `yaml:"ecosystem,omitempty"`
Name string `yaml:"name,omitempty"`
} `yaml:"package,omitempty"`
Ranges []struct {
Type string `yaml:"type,omitempty"`
Events []OSVEvent `yaml:"events,omitempty"`
} `yaml:"ranges,omitempty"`
Versions []string `yaml:"versions,omitempty"`
} `yaml:"affected,omitempty"`
}

type OSVEvent struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment

Introduced string `yaml:"introduced,omitempty"`
Fixed string `yaml:"fixed,omitempty"`
LastAffected string `yaml:"last_affected,omitempty"`
}
133 changes: 133 additions & 0 deletions internal/guidedremediation/matchertest/mock_vulnerability_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package matchertest provides mock matcher for testing.
package matchertest

import (
"context"
"os"
"slices"
"testing"

"deps.dev/util/resolve"
"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/internal/guidedremediation/matcher"
"gopkg.in/yaml.v3"
)

type mockVulnerabilityMatcher []*matcher.OSVRecord

func (mvc mockVulnerabilityMatcher) MatchVulnerabilities(ctx context.Context, invs []*extractor.Inventory) ([][]*matcher.OSVRecord, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment

result := make([][]*matcher.OSVRecord, len(invs))
for i, inv := range invs {
for _, vuln := range mvc {
if vulnAffectsInv(vuln, inv) {
result[i] = append(result[i], vuln)
}
}
}
return result, nil
}

type mockVulns struct {
Vulns []*matcher.OSVRecord `yaml:"vulns"`
}

// NewMockVulnerabilityMatcher creates a mock vulnerability matcher for testing.
// It loads vulnerability data from a YAML file specified by vulnsYAML.
func NewMockVulnerabilityMatcher(t *testing.T, vulnsYAML string) mockVulnerabilityMatcher {
t.Helper()
f, err := os.Open(vulnsYAML)
if err != nil {
t.Fatalf("failed opening mock vulns: %v", err)
}
defer f.Close()
dec := yaml.NewDecoder(f)

var vulns mockVulns
if err := dec.Decode(&vulns); err != nil {
t.Fatalf("failed decoding mock vulns: %v", err)
}
return mockVulnerabilityMatcher(vulns.Vulns)
}

// TODO: similar logic will need to be used elsewhere in guided remediation.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add issue

func vulnAffectsInv(vuln *matcher.OSVRecord, inv *extractor.Inventory) bool {
resolveSys, ok := inv.Metadata.(resolve.System)
if !ok {
return false
}
sys := resolveSys.Semver()
for _, affected := range vuln.Affected {
if affected.Package.Ecosystem != inv.Ecosystem() ||
affected.Package.Name != inv.Name {
continue
}
if slices.Contains(affected.Versions, inv.Version) {
return true
}
for _, r := range affected.Ranges {
if r.Type != "ECOSYSTEM" &&
!(r.Type == "SEMVER" && affected.Package.Ecosystem == "npm") {
continue
}
events := slices.Clone(r.Events)
eventVersion := func(e matcher.OSVEvent) string {
if e.Introduced != "" {
return e.Introduced
}
if e.Fixed != "" {
return e.Fixed
}
return e.LastAffected
}
slices.SortFunc(events, func(a, b matcher.OSVEvent) int {
aVer := eventVersion(a)
bVer := eventVersion(b)
if aVer == "0" {
if bVer == "0" {
return 0
}
return -1
}
if bVer == "0" {
return 1
}
// sys.Compare on strings is expensive, should consider precomputing sys.Parse
return sys.Compare(aVer, bVer)
})
idx, exact := slices.BinarySearchFunc(events, inv.Version, func(e matcher.OSVEvent, v string) int {
eVer := eventVersion(e)
if eVer == "0" {
return -1
}
return sys.Compare(eVer, v)
})
if exact {
e := events[idx]
// Version is exactly on a range-inclusive event
if e.Introduced != "" || e.LastAffected != "" {
return true
}
} else {
// Version is between events, only match if previous event is Introduced
if idx != 0 && events[idx-1].Introduced != "" {
return true
}
}
}
}
return false
}
Loading