-
Notifications
You must be signed in to change notification settings - Fork 27
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} | ||
} |
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 | ||
} |
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. | ||
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, | ||
} | ||
} |
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should be able to do this soon There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
npm