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): add package-lock.json graph parsing #479

Open
wants to merge 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -28,43 +28,14 @@ import (
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/internal/commitextractor"
"github.com/google/osv-scalibr/extractor/filesystem/osv"
"github.com/google/osv-scalibr/internal/dependencyfile/packagelockjson"
"github.com/google/osv-scalibr/plugin"
"github.com/google/osv-scalibr/purl"
"github.com/google/osv-scalibr/stats"

"golang.org/x/exp/maps"
)

type npmLockDependency struct {
// For an aliased package, Version is like "npm:[name]@[version]"
Version string `json:"version"`
Dependencies map[string]npmLockDependency `json:"dependencies,omitempty"`

Dev bool `json:"dev,omitempty"`
Optional bool `json:"optional,omitempty"`
}

type npmLockPackage struct {
// For an aliased package, Name is the real package name
Name string `json:"name"`
Version string `json:"version"`
Resolved string `json:"resolved"`

Dev bool `json:"dev,omitempty"`
DevOptional bool `json:"devOptional,omitempty"`
Optional bool `json:"optional,omitempty"`

Link bool `json:"link,omitempty"`
}

type npmLockfile struct {
Version int `json:"lockfileVersion"`
// npm v1- lockfiles use "dependencies"
Dependencies map[string]npmLockDependency `json:"dependencies,omitempty"`
// npm v2+ lockfiles use "packages"
Packages map[string]npmLockPackage `json:"packages,omitempty"`
}

type packageDetails struct {
Name string
Version string
Expand Down Expand Up @@ -105,21 +76,7 @@ func (pdm npmPackageDetailsMap) add(key string, details packageDetails) {
pdm[key] = details
}

func (dep npmLockDependency) depGroups() []string {
if dep.Dev && dep.Optional {
return []string{"dev", "optional"}
}
if dep.Dev {
return []string{"dev"}
}
if dep.Optional {
return []string{"optional"}
}

return nil
}

func parseNpmLockDependencies(dependencies map[string]npmLockDependency) map[string]packageDetails {
func parseNpmLockDependencies(dependencies map[string]packagelockjson.Dependency) map[string]packageDetails {
details := npmPackageDetailsMap{}

for name, detail := range dependencies {
Expand Down Expand Up @@ -162,7 +119,7 @@ func parseNpmLockDependencies(dependencies map[string]npmLockDependency) map[str
Name: name,
Version: finalVersion,
Commit: commit,
DepGroups: detail.depGroups(),
DepGroups: detail.DepGroups(),
})
}

Expand All @@ -180,21 +137,7 @@ func extractNpmPackageName(name string) string {
return pkgName
}

func (pkg npmLockPackage) depGroups() []string {
if pkg.Dev {
return []string{"dev"}
}
if pkg.Optional {
return []string{"optional"}
}
if pkg.DevOptional {
return []string{"dev", "optional"}
}

return nil
}

func parseNpmLockPackages(packages map[string]npmLockPackage) map[string]packageDetails {
func parseNpmLockPackages(packages map[string]packagelockjson.Package) map[string]packageDetails {
details := npmPackageDetailsMap{}

for namePath, detail := range packages {
Expand All @@ -221,14 +164,14 @@ func parseNpmLockPackages(packages map[string]npmLockPackage) map[string]package
Name: finalName,
Version: detail.Version,
Commit: commit,
DepGroups: detail.depGroups(),
DepGroups: detail.DepGroups(),
})
}

return details
}

func parseNpmLock(lockfile npmLockfile) map[string]packageDetails {
func parseNpmLock(lockfile packagelockjson.LockFile) map[string]packageDetails {
if lockfile.Packages != nil {
return parseNpmLockPackages(lockfile.Packages)
}
Expand Down Expand Up @@ -341,7 +284,7 @@ func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]
}

func (e Extractor) extractPkgLock(_ context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) {
var parsedLockfile *npmLockfile
var parsedLockfile *packagelockjson.LockFile

err := json.NewDecoder(input.Reader).Decode(&parsedLockfile)

Expand Down
90 changes: 90 additions & 0 deletions internal/dependencyfile/packagelockjson/packagelockjson.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 packagelockjson provides the structures for npm's package-lock.json lockfile format.
package packagelockjson

// Lockfile is the npm package-lock.json lockfile.
type LockFile struct {
Version int `json:"lockfileVersion"`
// npm v1- lockfiles use "dependencies"
Dependencies map[string]Dependency `json:"dependencies,omitempty"`
// npm v2+ lockfiles use "packages"
Packages map[string]Package `json:"packages,omitempty"`
}

// Dependency is the representation of an installed dependency in lockfileVersion 1
type Dependency struct {
// For an aliased package, Version is like "npm:[name]@[version]"
Version string `json:"version"`

Dev bool `json:"dev,omitempty"`
Optional bool `json:"optional,omitempty"`

Requires map[string]string `json:"requires,omitempty"`
Dependencies map[string]Dependency `json:"dependencies,omitempty"`
}

// DepGroups returns the list of groups this dependency belongs to.
// May be empty, or one or both of "dev", "optional".
func (dep Dependency) DepGroups() []string {
if dep.Dev && dep.Optional {
return []string{"dev", "optional"}
}
if dep.Dev {
return []string{"dev"}
}
if dep.Optional {
return []string{"optional"}
}

return nil
}

// Package is the representation of an installed dependency in lockfileVersion 2+
type Package struct {
// For an aliased package, Name is the real package name
Name string `json:"name,omitempty"`
Version string `json:"version"`
Resolved string `json:"resolved"`
Link bool `json:"link,omitempty"`

Dev bool `json:"dev,omitempty"`
DevOptional bool `json:"devOptional,omitempty"`
Optional bool `json:"optional,omitempty"`

Dependencies map[string]string `json:"dependencies,omitempty"`
DevDependencies map[string]string `json:"devDependencies,omitempty"`
OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"`
PeerDependencies map[string]string `json:"peerDependencies,omitempty"`
PeerDependenciesMeta map[string]struct {
Optional bool `json:"optional,omitempty"`
} `json:"peerDependenciesMeta,omitempty"`
}

// DepGroups returns the list of groups this package belongs to.
// May be empty, or one or both of "dev", "optional".
func (pkg Package) DepGroups() []string {
if pkg.Dev {
return []string{"dev"}
}
if pkg.Optional {
return []string{"optional"}
}
if pkg.DevOptional {
return []string{"dev", "optional"}
}

return nil
}
28 changes: 28 additions & 0 deletions internal/guidedremediation/lockfile/lockfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 lockfile provides methods for parsing and writing lockfiles.
package lockfile

import (
"deps.dev/util/resolve"
scalibrfs "github.com/google/osv-scalibr/fs"
)

// ReadWriter is the interface for parsing and applying remediation patches to a lockfile.
type ReadWriter interface {
System() resolve.System
Read(path string, fsys scalibrfs.FS) (*resolve.Graph, error)
// TODO(#454): Write()
}
Loading
Loading