Skip to content

Commit

Permalink
feat: add support for bun.lock (#379)
Browse files Browse the repository at this point in the history
As part of improving ergonomics, [Bun has introduced a new text-based lockfile](https://bun.sh/blog/bun-lock-text-lockfile) which this adds support for extracting.

While the blog post claims that it's "JSON with comments" (aka `JSONC`), it's actually "JSON with comments _and_ trailing commas" (aka [`JWCC`](https://nigeltao.github.io/blog/2021/json-with-commas-comments.html) or [`HuJSON`](https://github.com/tailscale/hujson)) - since the Go standard library only supports parsing standard JSON, I've had to bring in a third-party library to handle parsing, which is designed to leverage the existing standard library by instead handling "standardizing" the input into valid boring JSON.

~Aside from the general question of if this library is acceptable to use here, it also seems to require Go 1.23 resulting in the addition of a `toolchain` line in `go.mod` which I've never managed to quite figure out what the right thing to do with - overall, I'd like someone from Google to confirm what library they'd prefer we use here.~ (I've since switched to using a library that requires Go 1.16)

I have also specified the testdata fixtures as JSON5 as that's the most appropriate format supported by both VSCode and IntelliJ/GoLand, though that technically supports more features like single quotes (which it actually seems like `bun` does not mind if you use in your lockfile, though it'll always use double quotes itself) - personally I think that's fine, but don't mind renaming the files to be `.hujson` if folks would prefer.

Resolves google/osv-scanner#1405

Closes #379

COPYBARA_INTEGRATE_REVIEW=#379 from ackama:bun/support 0771c5a
PiperOrigin-RevId: 720546722
  • Loading branch information
G-Rath authored and copybara-github committed Jan 28, 2025
1 parent 34aef7c commit abf6098
Show file tree
Hide file tree
Showing 25 changed files with 1,345 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/supported_inventory_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ SCALIBR supports extracting software package information from a variety of OS an
* Lockfiles: pom.xml, gradle.lockfile, verification-metadata.xml
* Javascript
* Installed NPM packages (package.json)
* Lockfiles: package-lock.json, yarn.lock, pnpm-lock.yaml
* Lockfiles: package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock
* ObjectiveC
* Podfile.lock
* PHP:
Expand Down
148 changes: 148 additions & 0 deletions extractor/filesystem/language/javascript/bunlock/bunlock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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 bunlock extracts bun.lock files
package bunlock

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"path/filepath"
"strings"

"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/osv"
"github.com/google/osv-scalibr/plugin"
"github.com/google/osv-scalibr/purl"
"github.com/tidwall/jsonc/pkg/jsonc"

Check failure on line 32 in extractor/filesystem/language/javascript/bunlock/bunlock.go

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest)

no required module provides package github.com/tidwall/jsonc/pkg/jsonc; to add it:

Check failure on line 32 in extractor/filesystem/language/javascript/bunlock/bunlock.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest)

no required module provides package github.com/tidwall/jsonc/pkg/jsonc; to add it:

Check failure on line 32 in extractor/filesystem/language/javascript/bunlock/bunlock.go

View workflow job for this annotation

GitHub Actions / tests (windows-latest)

no required module provides package github.com/tidwall/jsonc/pkg/jsonc; to add it:
)

type bunLockfile struct {
Version int `json:"lockfileVersion"`
Packages map[string][]any `json:"packages"`
}

// Extractor extracts npm packages from bun.lock files.
type Extractor struct{}

// Name of the extractor.
func (e Extractor) Name() string { return "javascript/bunlock" }

// Version of the extractor.
func (e Extractor) Version() int { return 0 }

// Requirements of the extractor.
func (e Extractor) Requirements() *plugin.Capabilities {
return &plugin.Capabilities{}
}

// FileRequired returns true if the specified file matches bun lockfile patterns.
func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
return filepath.Base(api.Path()) == "bun.lock"
}

// structurePackageDetails returns the name, version, and commit of a package
// specified as a tuple in a bun.lock
func structurePackageDetails(pkg []any) (string, string, string, error) {
if len(pkg) == 0 {
return "", "", "", fmt.Errorf("empty package tuple")
}

str, ok := pkg[0].(string)

if !ok {
return "", "", "", fmt.Errorf("first element of package tuple is not a string")
}

str, isScoped := strings.CutPrefix(str, "@")
name, version, _ := strings.Cut(str, "@")

if isScoped {
name = "@" + name
}

version, commit, _ := strings.Cut(version, "#")

// bun.lock does not track both the commit and version,
// so if we have a commit then we don't have a version
if commit != "" {
version = ""
}

// file dependencies do not have a semantic version recorded
if strings.HasPrefix(version, "file:") {
version = ""
}

return name, version, commit, nil
}

// Extract extracts packages from bun.lock files passed through the scan input.
func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) {
var parsedLockfile *bunLockfile

b, err := io.ReadAll(input.Reader)

if err != nil {
return nil, fmt.Errorf("could not extract from %q: %w", input.Path, err)
}

if err := json.Unmarshal(jsonc.ToJSON(b), &parsedLockfile); err != nil {
return nil, fmt.Errorf("could not extract from %q: %w", input.Path, err)
}

inventories := make([]*extractor.Inventory, 0, len(parsedLockfile.Packages))

var errs []error

for key, pkg := range parsedLockfile.Packages {
name, version, commit, err := structurePackageDetails(pkg)

if err != nil {
errs = append(errs, fmt.Errorf("could not extract '%s' from %q: %w", key, input.Path, err))

continue
}

inventories = append(inventories, &extractor.Inventory{
Name: name,
Version: version,
SourceCode: &extractor.SourceCodeIdentifier{
Commit: commit,
},
Metadata: osv.DepGroupMetadata{
DepGroupVals: []string{},
},
Locations: []string{input.Path},
})
}

return inventories, errors.Join(errs...)
}

// ToPURL converts an inventory created by this extractor into a PURL.
func (e Extractor) ToPURL(i *extractor.Inventory) *purl.PackageURL {
return &purl.PackageURL{
Type: purl.TypeNPM,
Name: strings.ToLower(i.Name),
Version: i.Version,
}
}

// Ecosystem returns the OSV ecosystem ('npm') of the software extracted by this extractor.
func (e Extractor) Ecosystem(_ *extractor.Inventory) string { return "npm" }
Loading

0 comments on commit abf6098

Please sign in to comment.