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): Manifest parsing #471

Merged
merged 5 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
22 changes: 7 additions & 15 deletions internal/guidedremediation/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ 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"
scalibrfs "github.com/google/osv-scalibr/fs"
)

// Manifest is the interface for the representation of a manifest file needed for dependency resolution.
Expand All @@ -27,7 +26,7 @@ type Manifest interface {
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
Groups() map[RequirementKey][]string // Dependency groups that the direct requirements belong to
LocalManifests() []Manifest // Manifests of local packages
EcosystemSpecific() any // Any ecosystem-specific information needed

Expand All @@ -38,16 +37,9 @@ type Manifest interface {
// 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
}
// ReadWriter is the interface for parsing and applying remediation patches to a manifest file.
type ReadWriter interface {
System() resolve.System
Read(path string, fsys scalibrfs.FS) (Manifest, error)
// TODO(#454): Write()
}
320 changes: 320 additions & 0 deletions internal/guidedremediation/manifest/maven/pomxml.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@
package maven

import (
"context"
"fmt"
"maps"
"slices"

"deps.dev/util/maven"
"deps.dev/util/resolve"
"deps.dev/util/resolve/dep"
"github.com/google/osv-scalibr/clients/datasource"
"github.com/google/osv-scalibr/extractor/filesystem"
scalibrfs "github.com/google/osv-scalibr/fs"
"github.com/google/osv-scalibr/internal/guidedremediation/manifest"
"github.com/google/osv-scalibr/internal/mavenutil"
)

// RequirementKey is a comparable type that uniquely identifies a package dependency in a manifest.
Expand Down Expand Up @@ -63,3 +73,313 @@ type DependencyWithOrigin struct {
maven.Dependency
Origin string // Origin indicates where the dependency comes from
}

type mavenManifest struct {
filePath string
root resolve.Version
requirements []resolve.RequirementVersion
groups map[manifest.RequirementKey][]string
specific ManifestSpecific
}

// FilePath returns the path to the manifest file.
func (m *mavenManifest) FilePath() string {
return m.filePath
}

// Root returns the Version representing this package.
func (m *mavenManifest) Root() resolve.Version {
return m.root
}

// System returns the ecosystem of this manifest.
func (m *mavenManifest) System() resolve.System {
return resolve.Maven
}

// Requirements returns all direct requirements (including dev).
func (m *mavenManifest) Requirements() []resolve.RequirementVersion {
return m.requirements
}

// Groups returns the dependency groups that the direct requirements belong to.
func (m *mavenManifest) Groups() map[manifest.RequirementKey][]string {
return m.groups
}

// LocalManifests returns Manifests of any local packages.
func (m *mavenManifest) LocalManifests() []manifest.Manifest {
return nil
}

// EcosystemSpecific returns any ecosystem-specific information for this manifest.
func (m *mavenManifest) EcosystemSpecific() any {
return m.specific
}

// Clone returns a copy of this manifest that is safe to modify.
func (m *mavenManifest) Clone() manifest.Manifest {
clone := &mavenManifest{
filePath: m.filePath,
root: m.root,
requirements: slices.Clone(m.requirements),
groups: maps.Clone(m.groups),
specific: ManifestSpecific{
Parent: m.specific.Parent,
Properties: slices.Clone(m.specific.Properties),
OriginalRequirements: slices.Clone(m.specific.OriginalRequirements),
RequirementsForUpdates: slices.Clone(m.specific.RequirementsForUpdates),
Repositories: slices.Clone(m.specific.Repositories),
},
}
clone.root.AttrSet = m.root.AttrSet.Clone()

return clone
}

type readWriter struct {
*datasource.MavenRegistryAPIClient
}

// GetReadWriter returns a ReadWriter for pom.xml manifest files.
func GetReadWriter(registry string) (manifest.ReadWriter, error) {
client, err := datasource.NewMavenRegistryAPIClient(datasource.MavenRegistry{URL: registry, ReleasesEnabled: true})
if err != nil {
return nil, err
}
return readWriter{MavenRegistryAPIClient: client}, nil
}

// System returns the ecosystem of this ReadWriter.
func (r readWriter) System() resolve.System {
return resolve.Maven
}

// Read parses the manifest from the given file.
func (r readWriter) Read(path string, fsys scalibrfs.FS) (manifest.Manifest, error) {
ctx := context.Background()
f, err := fsys.Open(path)
if err != nil {
return nil, err
}
defer f.Close()

var project maven.Project
if err := datasource.NewMavenDecoder(f).Decode(&project); err != nil {
return nil, fmt.Errorf("failed to unmarshal project: %w", err)
}
properties := buildPropertiesWithOrigins(project, "")
origRequirements := buildOriginalRequirements(project, "")

var reqsForUpdates []resolve.RequirementVersion
if project.Parent.GroupID != "" && project.Parent.ArtifactID != "" {
reqsForUpdates = append(reqsForUpdates, resolve.RequirementVersion{
VersionKey: resolve.VersionKey{
PackageKey: resolve.PackageKey{
System: resolve.Maven,
Name: project.Parent.ProjectKey.Name(),
},
// Parent version is a concrete version, but we model parent as dependency here.
VersionType: resolve.Requirement,
Version: string(project.Parent.Version),
},
Type: resolve.MavenDepType(maven.Dependency{Type: "pom"}, mavenutil.OriginParent),
})
}

// Empty JDK and ActivationOS indicates merging the default profiles.
if err := project.MergeProfiles("", maven.ActivationOS{}); err != nil {
return nil, fmt.Errorf("failed to merge profiles: %w", err)
}

// TODO: there may be properties in repo.Releases.Enabled and repo.Snapshots.Enabled
for _, repo := range project.Repositories {
if err := r.MavenRegistryAPIClient.AddRegistry(datasource.MavenRegistry{
URL: string(repo.URL),
ID: string(repo.ID),
ReleasesEnabled: repo.Releases.Enabled.Boolean(),
SnapshotsEnabled: repo.Snapshots.Enabled.Boolean(),
}); err != nil {
return nil, fmt.Errorf("failed to add registry %s: %w", repo.URL, err)
}
}

// Merging parents data by parsing local parent pom.xml or fetching from upstream.
if err := mavenutil.MergeParents(ctx, &filesystem.ScanInput{FS: fsys, Path: path}, r.MavenRegistryAPIClient, &project, project.Parent, 1, true); err != nil {
return nil, fmt.Errorf("failed to merge parents: %w", err)
}

// For dependency management imports, the dependencies that imports
// dependencies from other projects will be replaced by the imported
// dependencies, so add them to requirements first.
for _, dep := range project.DependencyManagement.Dependencies {
if dep.Scope == "import" && dep.Type == "pom" {
reqsForUpdates = append(reqsForUpdates, makeRequirementVersion(dep, mavenutil.OriginManagement))
}
}

// Process the dependencies:
// - dedupe dependencies and dependency management
// - import dependency management
// - fill in missing dependency version requirement
project.ProcessDependencies(func(groupID, artifactID, version maven.String) (maven.DependencyManagement, error) {
return mavenutil.GetDependencyManagement(ctx, r.MavenRegistryAPIClient, groupID, artifactID, version)
})

groups := make(map[manifest.RequirementKey][]string)
requirements := addRequirements([]resolve.RequirementVersion{}, groups, project.Dependencies, "")
requirements = addRequirements(requirements, groups, project.DependencyManagement.Dependencies, mavenutil.OriginManagement)

// Requirements may not appear in the dependency graph but needs to be updated.
for _, profile := range project.Profiles {
reqsForUpdates = addRequirements(reqsForUpdates, groups, profile.Dependencies, "")
reqsForUpdates = addRequirements(reqsForUpdates, groups, profile.DependencyManagement.Dependencies, mavenutil.OriginManagement)
}
for _, plugin := range project.Build.PluginManagement.Plugins {
reqsForUpdates = addRequirements(reqsForUpdates, groups, plugin.Dependencies, "")
}

return &mavenManifest{
filePath: path,
root: resolve.Version{
VersionKey: resolve.VersionKey{
PackageKey: resolve.PackageKey{
System: resolve.Maven,
Name: project.ProjectKey.Name(),
},
VersionType: resolve.Concrete,
Version: string(project.Version),
},
},
requirements: requirements,
groups: groups,
specific: ManifestSpecific{
Parent: project.Parent,
Properties: properties,
OriginalRequirements: origRequirements,
RequirementsForUpdates: reqsForUpdates,
Repositories: project.Repositories,
},
}, nil
}

func addRequirements(reqs []resolve.RequirementVersion, groups map[manifest.RequirementKey][]string, deps []maven.Dependency, origin string) []resolve.RequirementVersion {
for _, d := range deps {
reqVer := makeRequirementVersion(d, origin)
reqs = append(reqs, reqVer)
if d.Scope != "" {
reqKey := MakeRequirementKey(reqVer)
groups[reqKey] = append(groups[reqKey], string(d.Scope))
}
}

return reqs
}

func buildPropertiesWithOrigins(project maven.Project, originPrefix string) []PropertyWithOrigin {
count := len(project.Properties.Properties)
for _, prof := range project.Profiles {
count += len(prof.Properties.Properties)
}
properties := make([]PropertyWithOrigin, 0, count)
for _, prop := range project.Properties.Properties {
properties = append(properties, PropertyWithOrigin{Property: prop})
}
for _, profile := range project.Profiles {
for _, prop := range profile.Properties.Properties {
properties = append(properties, PropertyWithOrigin{
Property: prop,
Origin: mavenOrigin(originPrefix, mavenutil.OriginProfile, string(profile.ID)),
})
}
}

return properties
}

func buildOriginalRequirements(project maven.Project, originPrefix string) []DependencyWithOrigin {
var dependencies []DependencyWithOrigin //nolint:prealloc
if project.Parent.GroupID != "" && project.Parent.ArtifactID != "" {
dependencies = append(dependencies, DependencyWithOrigin{
Dependency: maven.Dependency{
GroupID: project.Parent.GroupID,
ArtifactID: project.Parent.ArtifactID,
Version: project.Parent.Version,
Type: "pom",
},
Origin: mavenOrigin(originPrefix, mavenutil.OriginParent),
})
}
for _, d := range project.Dependencies {
dependencies = append(dependencies, DependencyWithOrigin{Dependency: d, Origin: originPrefix})
}
for _, d := range project.DependencyManagement.Dependencies {
dependencies = append(dependencies, DependencyWithOrigin{
Dependency: d,
Origin: mavenOrigin(originPrefix, mavenutil.OriginManagement),
})
}
for _, prof := range project.Profiles {
for _, d := range prof.Dependencies {
dependencies = append(dependencies, DependencyWithOrigin{
Dependency: d,
Origin: mavenOrigin(originPrefix, mavenutil.OriginProfile, string(prof.ID)),
})
}
for _, d := range prof.DependencyManagement.Dependencies {
dependencies = append(dependencies, DependencyWithOrigin{
Dependency: d,
Origin: mavenOrigin(originPrefix, mavenutil.OriginProfile, string(prof.ID), mavenutil.OriginManagement),
})
}
}
for _, plugin := range project.Build.PluginManagement.Plugins {
for _, d := range plugin.Dependencies {
dependencies = append(dependencies, DependencyWithOrigin{
Dependency: d,
Origin: mavenOrigin(originPrefix, mavenutil.OriginPlugin, plugin.ProjectKey.Name()),
})
}
}

return dependencies
}

// For dependencies in profiles and plugins, we use origin to indicate where they are from.
// The origin is in the format prefix@identifier[@postfix] (where @ is the separator):
// - prefix indicates it is from profile or plugin
// - identifier to locate the profile/plugin which is profile ID or plugin name
// - (optional) suffix indicates if this is a dependency management
func makeRequirementVersion(dep maven.Dependency, origin string) resolve.RequirementVersion {
// Treat test & optional dependencies as regular dependencies to force the resolver to resolve them.
if dep.Scope == "test" {
dep.Scope = ""
}
dep.Optional = ""

return resolve.RequirementVersion{
VersionKey: resolve.VersionKey{
PackageKey: resolve.PackageKey{
System: resolve.Maven,
Name: dep.Name(),
},
VersionType: resolve.Requirement,
Version: string(dep.Version),
},
Type: resolve.MavenDepType(dep, origin),
}
}

func mavenOrigin(list ...string) string {
result := ""
for _, str := range list {
if result != "" && str != "" {
result += "@"
}
if str != "" {
result += str
}
}

return result
}
Loading
Loading