mirror of
https://github.com/anchore/syft.git
synced 2026-06-10 06:18:24 +02:00
feat: add support for Bun lockfile (#4625)
--------- Signed-off-by: Yoonho Hann <hnnynh125@gmail.com> Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
parent
63232bf725
commit
b08d3c2970
5
go.mod
5
go.mod
@ -101,7 +101,10 @@ require (
|
||||
modernc.org/sqlite v1.51.0
|
||||
)
|
||||
|
||||
require github.com/pb33f/ordered-map/v2 v2.3.1
|
||||
require (
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@ -924,6 +924,8 @@ github.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w=
|
||||
github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88=
|
||||
github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI=
|
||||
github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
|
||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
|
||||
|
||||
@ -3,12 +3,13 @@ package internal
|
||||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "16.1.3"
|
||||
JSONSchemaVersion = "16.1.4"
|
||||
|
||||
// Changelog
|
||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
||||
// 16.1.1 - correct elf package osCpe field according to the document of systemd (also add appCpe field)
|
||||
// 16.1.2 - placeholder for 16.1.2 changelog
|
||||
// 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata
|
||||
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
|
||||
|
||||
)
|
||||
|
||||
@ -11,6 +11,7 @@ func AllTypes() []any {
|
||||
pkg.ApkDBEntry{},
|
||||
pkg.BinarySignature{},
|
||||
pkg.BitnamiSBOMEntry{},
|
||||
pkg.BunLockEntry{},
|
||||
pkg.CocoaPodfileLockEntry{},
|
||||
pkg.ConanV1LockEntry{},
|
||||
pkg.ConanV2LockEntry{},
|
||||
|
||||
@ -96,6 +96,7 @@ var jsonTypes = makeJSONTypes(
|
||||
jsonNames(pkg.NpmPackageLockEntry{}, "javascript-npm-package-lock-entry", "NpmPackageLockJsonMetadata"),
|
||||
jsonNames(pkg.YarnLockEntry{}, "javascript-yarn-lock-entry", "YarnLockJsonMetadata"),
|
||||
jsonNames(pkg.PnpmLockEntry{}, "javascript-pnpm-lock-entry"),
|
||||
jsonNames(pkg.BunLockEntry{}, "javascript-bun-lock-entry"),
|
||||
jsonNames(pkg.PEBinary{}, "pe-binary"),
|
||||
jsonNames(pkg.PhpComposerLockEntry{}, "php-composer-lock-entry", "PhpComposerJsonMetadata"),
|
||||
jsonNamesWithoutLookup(pkg.PhpComposerInstalledEntry{}, "php-composer-installed-entry", "PhpComposerJsonMetadata"), // the legacy value is split into two types, where the other is preferred
|
||||
|
||||
4306
schema/json/schema-16.1.4.json
Normal file
4306
schema/json/schema-16.1.4.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.1.3/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.1.4/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@ -1918,6 +1918,61 @@
|
||||
"type": "object",
|
||||
"description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation."
|
||||
},
|
||||
"JavascriptBunLockEntry": {
|
||||
"properties": {
|
||||
"integrity": {
|
||||
"type": "string",
|
||||
"description": "Integrity is Subresource Integrity hash for verification (SRI format)"
|
||||
},
|
||||
"dependencies": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "Dependencies is a map of runtime dependencies and their version specifiers"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "OptionalDependencies is a map of optional dependencies and their version specifiers"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "PeerDependencies is a map of peer dependencies and their version specifiers"
|
||||
},
|
||||
"bin": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "Bin is a map of binary names to the paths they are installed to"
|
||||
},
|
||||
"os": {
|
||||
"type": "string",
|
||||
"description": "OS is the operating system constraint for the package (e.g. \"darwin\")"
|
||||
},
|
||||
"cpu": {
|
||||
"type": "string",
|
||||
"description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"integrity",
|
||||
"dependencies",
|
||||
"optionalDependencies",
|
||||
"peerDependencies",
|
||||
"bin",
|
||||
"os",
|
||||
"cpu"
|
||||
],
|
||||
"description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file"
|
||||
},
|
||||
"JavascriptNpmPackage": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@ -2657,6 +2712,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/JavaJvmInstallation"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/JavascriptBunLockEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/JavascriptNpmPackage"
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
completionTester := packagemetadata.NewCompletionTester(t,
|
||||
pkg.BinarySignature{},
|
||||
pkg.BitnamiSBOMEntry{},
|
||||
pkg.BunLockEntry{},
|
||||
pkg.CocoaPodfileLockEntry{},
|
||||
pkg.ConanV1LockEntry{},
|
||||
pkg.ConanV2LockEntry{}, // the field Username might be the username of either the package originator or the supplier (unclear currently)
|
||||
|
||||
@ -29,6 +29,38 @@ catalogers:
|
||||
- npm
|
||||
- package
|
||||
parsers: # AUTO-GENERATED structure
|
||||
- function: parseBunLock
|
||||
detector: # AUTO-GENERATED
|
||||
method: glob # AUTO-GENERATED
|
||||
criteria: # AUTO-GENERATED
|
||||
- '**/bun.lock'
|
||||
metadata_types: # AUTO-GENERATED
|
||||
- pkg.BunLockEntry
|
||||
package_types: # AUTO-GENERATED
|
||||
- npm
|
||||
json_schema_types: # AUTO-GENERATED
|
||||
- JavascriptBunLockEntry
|
||||
capabilities: # MANUAL - preserved across regeneration
|
||||
- name: license
|
||||
default: false
|
||||
- name: dependency.depth
|
||||
default:
|
||||
- direct
|
||||
- indirect
|
||||
- name: dependency.edges
|
||||
default: ""
|
||||
- name: dependency.kinds
|
||||
default:
|
||||
- runtime
|
||||
- dev
|
||||
- name: package_manager.files.listing
|
||||
default: false
|
||||
- name: package_manager.files.digests
|
||||
default: false
|
||||
- name: package_manager.package_integrity_hash
|
||||
default: true
|
||||
evidence:
|
||||
- BunLockEntry.Integrity
|
||||
- function: parsePnpmLock
|
||||
detector: # AUTO-GENERATED
|
||||
method: glob # AUTO-GENERATED
|
||||
|
||||
@ -19,8 +19,10 @@ func NewLockCataloger(cfg CatalogerConfig) pkg.Cataloger {
|
||||
yarnLockAdapter := newGenericYarnLockAdapter(cfg)
|
||||
packageLockAdapter := newGenericPackageLockAdapter(cfg)
|
||||
pnpmLockAdapter := newGenericPnpmLockAdapter(cfg)
|
||||
bunLockAdapter := newGenericBunLockAdapter(cfg)
|
||||
return generic.NewCataloger("javascript-lock-cataloger").
|
||||
WithParserByGlobs(packageLockAdapter.parsePackageLock, "**/package-lock.json").
|
||||
WithParserByGlobs(yarnLockAdapter.parseYarnLock, "**/yarn.lock").
|
||||
WithParserByGlobs(pnpmLockAdapter.parsePnpmLock, "**/pnpm-lock.yaml")
|
||||
WithParserByGlobs(pnpmLockAdapter.parsePnpmLock, "**/pnpm-lock.yaml").
|
||||
WithParserByGlobs(bunLockAdapter.parseBunLock, "**/bun.lock")
|
||||
}
|
||||
|
||||
@ -176,6 +176,7 @@ func Test_LockCataloger_Globs(t *testing.T) {
|
||||
"src/package-lock.json",
|
||||
"src/pnpm-lock.yaml",
|
||||
"src/yarn.lock",
|
||||
"src/bun.lock",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -83,3 +83,28 @@ func yarnLockDependencySpecifier(p pkg.Package) dependency.Specification {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func bunLockDependencySpecifier(p pkg.Package) dependency.Specification {
|
||||
meta, ok := p.Metadata.(pkg.BunLockEntry)
|
||||
if !ok {
|
||||
log.Tracef("cataloger failed to extract bun lock metadata for package %+v", p.Name)
|
||||
return dependency.Specification{}
|
||||
}
|
||||
|
||||
provides := []string{p.Name}
|
||||
|
||||
var requires []string
|
||||
|
||||
for name := range meta.Dependencies {
|
||||
requires = append(requires, name)
|
||||
}
|
||||
for name := range meta.OptionalDependencies {
|
||||
requires = append(requires, name)
|
||||
}
|
||||
return dependency.Specification{
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: provides,
|
||||
Requires: requires,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,6 +220,43 @@ func newYarnLockPackage(ctx context.Context, cfg CatalogerConfig, resolver file.
|
||||
)
|
||||
}
|
||||
|
||||
func newBunPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, integrity string, metadata bunPackageMetadata) pkg.Package {
|
||||
var licenseSet pkg.LicenseSet
|
||||
|
||||
if cfg.SearchRemoteLicenses {
|
||||
license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version)
|
||||
if err == nil && license != "" {
|
||||
licenseSet = pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, license)...)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("unable to extract licenses from javascript bun.lock for package %s:%s: %+v", name, version, err)
|
||||
}
|
||||
}
|
||||
return finalizeLockPkg(
|
||||
ctx,
|
||||
resolver,
|
||||
location,
|
||||
pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Licenses: licenseSet,
|
||||
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
PURL: packageURL(name, version),
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: integrity,
|
||||
Dependencies: metadata.Dependencies,
|
||||
OptionalDependencies: metadata.OptionalDependencies,
|
||||
PeerDependencies: metadata.PeerDependencies,
|
||||
Bin: metadata.Bin,
|
||||
OS: metadata.OS,
|
||||
CPU: metadata.CPU,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func formatNpmRegistryURL(baseURL, packageName, version string) (requestURL string, err error) {
|
||||
urlPath := []string{packageName, version}
|
||||
requestURL, err = url.JoinPath(baseURL, urlPath...)
|
||||
|
||||
295
syft/pkg/cataloger/javascript/parse_bun_lock.go
Normal file
295
syft/pkg/cataloger/javascript/parse_bun_lock.go
Normal file
@ -0,0 +1,295 @@
|
||||
package javascript
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
|
||||
)
|
||||
|
||||
// bunPackage holds the name, version, and metadata extracted from a single lockfile entry.
|
||||
type bunPackage struct {
|
||||
Name string
|
||||
Version string
|
||||
Integrity string
|
||||
Metadata bunPackageMetadata
|
||||
}
|
||||
|
||||
// bunPackageMetadata is the metadata object (the third element) of a bun.lock package tuple.
|
||||
type bunPackageMetadata struct {
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
OptionalDependencies map[string]string `json:"optionalDependencies"`
|
||||
PeerDependencies map[string]string `json:"peerDependencies"`
|
||||
Bin map[string]string `json:"bin"`
|
||||
OS string `json:"os"`
|
||||
CPU string `json:"cpu"`
|
||||
}
|
||||
|
||||
// bunLockfile represents the structure of a bun.lock file (JSONC format).
|
||||
type bunLockfile struct {
|
||||
LockfileVersion int `json:"lockfileVersion"`
|
||||
ConfigVersion int `json:"configVersion"`
|
||||
Workspaces map[string]bunWorkspace `json:"workspaces"`
|
||||
Packages map[string]json.RawMessage `json:"packages"`
|
||||
}
|
||||
|
||||
type bunWorkspace struct {
|
||||
Name string `json:"name"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
DevDependencies map[string]string `json:"devDependencies"`
|
||||
}
|
||||
|
||||
type genericBunLockAdapter struct {
|
||||
cfg CatalogerConfig
|
||||
}
|
||||
|
||||
func newGenericBunLockAdapter(cfg CatalogerConfig) genericBunLockAdapter {
|
||||
return genericBunLockAdapter{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// parseBunLock is the main parser function for bun.lock files.
|
||||
func (a genericBunLockAdapter) parseBunLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
data, err := io.ReadAll(reader) //nolint:gocritic // bun.lock is JSONC; the full document must be buffered for hujson.Standardize before unmarshalling
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load bun.lock file: %w", err)
|
||||
}
|
||||
|
||||
// bun.lock is JSONC (JSON with comments and trailing commas), not strict JSON, so it must
|
||||
// be standardized before it can be unmarshalled. See https://bun.sh/blog/bun-lock-text-lockfile
|
||||
data, err = hujson.Standardize(data)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to standardize bun.lock file: %w", err)
|
||||
}
|
||||
|
||||
var lockfile bunLockfile
|
||||
if err := json.Unmarshal(data, &lockfile); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse bun.lock file: %w", err)
|
||||
}
|
||||
|
||||
log.WithFields("lockfileVersion", lockfile.LockfileVersion, "configVersion", lockfile.ConfigVersion).Trace("parsed bun.lock metadata")
|
||||
|
||||
bunPkgs := parseBunLockPackages(lockfile)
|
||||
|
||||
// Collect dev dependencies from all workspaces
|
||||
devDeps := make(map[string]bool)
|
||||
for _, workspace := range lockfile.Workspaces {
|
||||
for devDepName := range workspace.DevDependencies {
|
||||
devDeps[devDepName] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Determine dev-only packages
|
||||
prodDeps := make(map[string]string)
|
||||
for _, workspace := range lockfile.Workspaces {
|
||||
maps.Copy(prodDeps, workspace.Dependencies)
|
||||
}
|
||||
|
||||
devOnlyPkgs := findDevOnlyBunPkgs(bunPkgs, prodDeps, devDeps)
|
||||
|
||||
packages := make([]pkg.Package, 0, len(bunPkgs))
|
||||
for _, p := range bunPkgs {
|
||||
if devOnlyPkgs[p.Name] && !a.cfg.IncludeDevDependencies {
|
||||
continue
|
||||
}
|
||||
packages = append(packages, newBunPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version, p.Integrity, p.Metadata))
|
||||
}
|
||||
|
||||
pkg.Sort(packages)
|
||||
|
||||
return packages, dependency.Resolve(bunLockDependencySpecifier, packages), unknown.IfEmptyf(packages, "unable to determine packages")
|
||||
}
|
||||
|
||||
func parseBunLockPackages(lockfile bunLockfile) []bunPackage {
|
||||
packages := make([]bunPackage, 0, len(lockfile.Packages))
|
||||
|
||||
for pkgName, pkgData := range lockfile.Packages {
|
||||
var pkgArray []json.RawMessage
|
||||
if err := json.Unmarshal(pkgData, &pkgArray); err != nil {
|
||||
log.WithFields("package", pkgName, "error", err).Trace("unable to parse bun.lock package entry")
|
||||
continue
|
||||
}
|
||||
if len(pkgArray) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract identifier (name@version) from first element
|
||||
var identifier string
|
||||
if err := json.Unmarshal(pkgArray[0], &identifier); err != nil {
|
||||
log.WithFields("package", pkgName, "error", err).Trace("unable to parse bun.lock package identifier")
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse identifier to handle scoped packages (@scope/name@version)
|
||||
name, version, ok := parseBunPackageIdentifier(identifier)
|
||||
if !ok {
|
||||
log.WithFields("identifier", identifier).Trace("unable to parse bun.lock package identifier format")
|
||||
continue
|
||||
}
|
||||
|
||||
// root, workspace, link, and file entries are local first-party packages rather than
|
||||
// resolved third-party dependencies, so they are not cataloged from the lockfile.
|
||||
if isLocalBunPackage(version) {
|
||||
continue
|
||||
}
|
||||
|
||||
// tuple length and the positions of the metadata object and integrity hash vary by
|
||||
// source (registry, git, tarball), locate by type
|
||||
metadata, integrity := extractBunPackageFields(pkgName, pkgArray[1:])
|
||||
|
||||
packages = append(packages, bunPackage{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Integrity: integrity,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
// extractBunPackageFields locates the metadata object and integrity hash within the trailing
|
||||
// elements of a bun.lock package tuple. Their positions vary by source: registry entries are
|
||||
// [identifier, registry, {metadata}, integrity]
|
||||
func extractBunPackageFields(pkgName string, elements []json.RawMessage) (bunPackageMetadata, string) {
|
||||
var metadata bunPackageMetadata
|
||||
var integrity string
|
||||
|
||||
for _, raw := range elements {
|
||||
value := bytes.TrimSpace(raw)
|
||||
if len(value) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch value[0] {
|
||||
case '{':
|
||||
if err := json.Unmarshal(raw, &metadata); err != nil {
|
||||
log.WithFields("package", pkgName, "error", err).Trace("unable to parse bun.lock package metadata")
|
||||
}
|
||||
case '"':
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil && isIntegrityHash(s) {
|
||||
integrity = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata, integrity
|
||||
}
|
||||
|
||||
// isIntegrityHash reports whether s is a Subresource Integrity hash (SRI format), which lets it
|
||||
// be distinguished from the other string fields in a tuple
|
||||
func isIntegrityHash(s string) bool {
|
||||
for _, prefix := range []string{"sha512-", "sha384-", "sha256-", "sha1-"} {
|
||||
if strings.HasPrefix(s, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isLocalBunPackage reports whether a package version refers to a local first-party package
|
||||
// (the root project, a workspace, a symlink, or a folder) rather than a resolved third-party
|
||||
// dependency. These correspond to the root/workspace/link/file resolution forms in bun.lock.
|
||||
func isLocalBunPackage(version string) bool {
|
||||
for _, prefix := range []string{"root:", "workspace:", "link:", "file:"} {
|
||||
if strings.HasPrefix(version, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseBunPackageIdentifier extracts the package name and version from a bun.lock identifier.
|
||||
func parseBunPackageIdentifier(identifier string) (name, version string, ok bool) {
|
||||
// Find the last @ symbol which separates name from version
|
||||
lastAtIndex := strings.LastIndex(identifier, "@")
|
||||
if lastAtIndex == -1 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Handle scoped packages (@scope/package@version)
|
||||
if strings.HasPrefix(identifier, "@") && lastAtIndex > 0 {
|
||||
name = identifier[:lastAtIndex]
|
||||
version = identifier[lastAtIndex+1:]
|
||||
return name, version, true
|
||||
}
|
||||
|
||||
// Handle non-scoped packages (package@version)
|
||||
name = identifier[:lastAtIndex]
|
||||
version = identifier[lastAtIndex+1:]
|
||||
return name, version, true
|
||||
}
|
||||
|
||||
func findDevOnlyBunPkgs(bunPkgs []bunPackage, prodDeps map[string]string, devDeps map[string]bool) map[string]bool {
|
||||
// Build a simplified dependency graph
|
||||
depGraph := make(map[string][]string)
|
||||
for _, p := range bunPkgs {
|
||||
var deps []string
|
||||
for depName := range p.Metadata.Dependencies {
|
||||
deps = append(deps, depName)
|
||||
}
|
||||
for depName := range p.Metadata.OptionalDependencies {
|
||||
deps = append(deps, depName)
|
||||
}
|
||||
depGraph[p.Name] = deps
|
||||
}
|
||||
|
||||
// Find all packages reachable from production dependencies
|
||||
prodReachable := make(map[string]bool)
|
||||
var visitProd func(string)
|
||||
visitProd = func(name string) {
|
||||
if prodReachable[name] {
|
||||
return
|
||||
}
|
||||
prodReachable[name] = true
|
||||
for _, dep := range depGraph[name] {
|
||||
visitProd(dep)
|
||||
}
|
||||
}
|
||||
|
||||
for prodDep := range prodDeps {
|
||||
visitProd(prodDep)
|
||||
}
|
||||
|
||||
// Find all packages reachable from dev dependencies
|
||||
devReachable := make(map[string]bool)
|
||||
var visitDev func(string)
|
||||
visitDev = func(name string) {
|
||||
if devReachable[name] {
|
||||
return
|
||||
}
|
||||
devReachable[name] = true
|
||||
for _, dep := range depGraph[name] {
|
||||
visitDev(dep)
|
||||
}
|
||||
}
|
||||
|
||||
for devDep := range devDeps {
|
||||
visitDev(devDep)
|
||||
}
|
||||
|
||||
// Packages that are dev-only are those reachable from dev but not from prod
|
||||
devOnly := make(map[string]bool)
|
||||
for name := range devReachable {
|
||||
if !prodReachable[name] {
|
||||
devOnly[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
return devOnly
|
||||
}
|
||||
476
syft/pkg/cataloger/javascript/parse_bun_lock_test.go
Normal file
476
syft/pkg/cataloger/javascript/parse_bun_lock_test.go
Normal file
@ -0,0 +1,476 @@
|
||||
package javascript
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func TestParseBunLock(t *testing.T) {
|
||||
fixture := "test-fixtures/bun/bun.lock"
|
||||
|
||||
locationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
{
|
||||
Name: "@img/sharp-darwin-arm64",
|
||||
Version: "0.33.5",
|
||||
PURL: "pkg:npm/%40img/sharp-darwin-arm64@0.33.5",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
OS: "darwin",
|
||||
CPU: "arm64",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "@img/sharp-linux-x64",
|
||||
Version: "0.33.5",
|
||||
PURL: "pkg:npm/%40img/sharp-linux-x64@0.33.5",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
OS: "linux",
|
||||
CPU: "x64",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "axios",
|
||||
Version: "1.6.0",
|
||||
PURL: "pkg:npm/axios@1.6.0",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
|
||||
Dependencies: map[string]string{
|
||||
"follow-redirects": "^1.15.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "color",
|
||||
Version: "4.2.3",
|
||||
PURL: "pkg:npm/color@4.2.3",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "eslint",
|
||||
Version: "9.0.0",
|
||||
PURL: "pkg:npm/eslint@9.0.0",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-IMryZ5SudxzQvuod6rUdxH8KRx8BKHkTXpHWe3BJ9Qef3PbG9v9vjSB0STcKOVjTvPnG1+9T5e4xfzZ4wKdqiA==",
|
||||
Dependencies: map[string]string{
|
||||
"eslint-visitor-keys": "^4.0.0",
|
||||
},
|
||||
Bin: map[string]string{
|
||||
"eslint": "bin/eslint.js",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "eslint-visitor-keys",
|
||||
Version: "4.0.0",
|
||||
PURL: "pkg:npm/eslint-visitor-keys@4.0.0",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "follow-redirects",
|
||||
Version: "1.15.3",
|
||||
PURL: "pkg:npm/follow-redirects@1.15.3",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
PeerDependencies: map[string]string{
|
||||
"debug": "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "lodash",
|
||||
Version: "4.17.21",
|
||||
PURL: "pkg:npm/lodash@4.17.21",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "sharp",
|
||||
Version: "0.33.5",
|
||||
PURL: "pkg:npm/sharp@0.33.5",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgxt/HmijnBghFeqCkXp3e+MH2fYF5o6qzrNJJ2k9s4bsZX0QBUCbFjr8VdLAIww==",
|
||||
Dependencies: map[string]string{
|
||||
"color": "^4.2.3",
|
||||
},
|
||||
OptionalDependencies: map[string]string{
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "typescript",
|
||||
Version: "5.0.0",
|
||||
PURL: "pkg:npm/typescript@5.0.0",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-w5c493tkwFQLjKSYw0JvvVbqJ9ZM2SXoWBv/wqHYqWN/jP9bGilMKFbAKkpRHfaLxPn6A3K/fmJ6LD8B0EO9oA==",
|
||||
Bin: map[string]string{
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Relationships are automatically generated by dependency.Resolve
|
||||
// We explicitly define expected relationships for packages with dependencies
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
// sharp depends on color
|
||||
{
|
||||
From: expectedPkgs[3], // color
|
||||
To: expectedPkgs[8], // sharp
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
// sharp optionally depends on @img/sharp-darwin-arm64
|
||||
{
|
||||
From: expectedPkgs[0], // @img/sharp-darwin-arm64
|
||||
To: expectedPkgs[8], // sharp
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
// sharp optionally depends on @img/sharp-linux-x64
|
||||
{
|
||||
From: expectedPkgs[1], // @img/sharp-linux-x64
|
||||
To: expectedPkgs[8], // sharp
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
// axios depends on follow-redirects
|
||||
{
|
||||
From: expectedPkgs[6], // follow-redirects
|
||||
To: expectedPkgs[2], // axios
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
// eslint depends on eslint-visitor-keys
|
||||
{
|
||||
From: expectedPkgs[5], // eslint-visitor-keys
|
||||
To: expectedPkgs[4], // eslint
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
|
||||
adapter := newGenericBunLockAdapter(CatalogerConfig{IncludeDevDependencies: true})
|
||||
pkgtest.TestFileParser(t, fixture, adapter.parseBunLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParseBunLock_ExcludeDevDependencies(t *testing.T) {
|
||||
fixture := "test-fixtures/bun/bun.lock"
|
||||
|
||||
locationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
{
|
||||
Name: "@img/sharp-darwin-arm64",
|
||||
Version: "0.33.5",
|
||||
PURL: "pkg:npm/%40img/sharp-darwin-arm64@0.33.5",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
OS: "darwin",
|
||||
CPU: "arm64",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "@img/sharp-linux-x64",
|
||||
Version: "0.33.5",
|
||||
PURL: "pkg:npm/%40img/sharp-linux-x64@0.33.5",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
OS: "linux",
|
||||
CPU: "x64",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "axios",
|
||||
Version: "1.6.0",
|
||||
PURL: "pkg:npm/axios@1.6.0",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
|
||||
Dependencies: map[string]string{
|
||||
"follow-redirects": "^1.15.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "color",
|
||||
Version: "4.2.3",
|
||||
PURL: "pkg:npm/color@4.2.3",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "follow-redirects",
|
||||
Version: "1.15.3",
|
||||
PURL: "pkg:npm/follow-redirects@1.15.3",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
PeerDependencies: map[string]string{
|
||||
"debug": "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "lodash",
|
||||
Version: "4.17.21",
|
||||
PURL: "pkg:npm/lodash@4.17.21",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "sharp",
|
||||
Version: "0.33.5",
|
||||
PURL: "pkg:npm/sharp@0.33.5",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.BunLockEntry{
|
||||
Integrity: "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgxt/HmijnBghFeqCkXp3e+MH2fYF5o6qzrNJJ2k9s4bsZX0QBUCbFjr8VdLAIww==",
|
||||
Dependencies: map[string]string{
|
||||
"color": "^4.2.3",
|
||||
},
|
||||
OptionalDependencies: map[string]string{
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// When excluding dev dependencies, we don't have eslint and typescript,
|
||||
// so we only have relationships for axios and sharp
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
// sharp depends on color
|
||||
{
|
||||
From: expectedPkgs[3], // color
|
||||
To: expectedPkgs[6], // sharp (index 6, not 7)
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
// sharp optionally depends on @img/sharp-darwin-arm64
|
||||
{
|
||||
From: expectedPkgs[0], // @img/sharp-darwin-arm64
|
||||
To: expectedPkgs[6], // sharp
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
// sharp optionally depends on @img/sharp-linux-x64
|
||||
{
|
||||
From: expectedPkgs[1], // @img/sharp-linux-x64
|
||||
To: expectedPkgs[6], // sharp
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
// axios depends on follow-redirects
|
||||
{
|
||||
From: expectedPkgs[4], // follow-redirects
|
||||
To: expectedPkgs[2], // axios
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
|
||||
adapter := newGenericBunLockAdapter(CatalogerConfig{IncludeDevDependencies: false})
|
||||
pkgtest.TestFileParser(t, fixture, adapter.parseBunLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
// TestParseBunLock_Fixtures parses each bun.lock fixture and asserts the set of cataloged
|
||||
// packages (and, where given, specific integrity hashes). It covers the real-world formats
|
||||
// bun emits:
|
||||
// - strict JSON
|
||||
// - JSONC with trailing commas (see https://bun.sh/blog/bun-lock-text-lockfile)
|
||||
// - variable-length package tuples whose field positions differ by source:
|
||||
// - root (2 elements): [identifier, {bin, binDir}], e.g. ["my-monorepo@root:", {...}]
|
||||
// - workspace (1 element): only the identifier, e.g. ["@my/util@workspace:packages/util"]
|
||||
// - github without integrity (3 elements): [identifier, {metadata}, resolved]
|
||||
// - github with integrity (4 elements): [identifier, {metadata}, resolved, integrity]
|
||||
// - registry (4 elements): [identifier, registry, {metadata}, integrity]
|
||||
//
|
||||
// The github and registry forms are both four elements but place the metadata object and
|
||||
// integrity hash at different indices, so they must be located by type rather than position.
|
||||
func TestParseBunLock_Fixtures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
includeDevDependencies bool
|
||||
wantNames []string
|
||||
wantIntegrity map[string]string // package name -> expected integrity, checked when set
|
||||
}{
|
||||
{
|
||||
name: "strict json",
|
||||
fixture: "test-fixtures/bun/bun.lock",
|
||||
includeDevDependencies: true,
|
||||
wantNames: []string{
|
||||
"@img/sharp-darwin-arm64",
|
||||
"@img/sharp-linux-x64",
|
||||
"axios",
|
||||
"color",
|
||||
"eslint",
|
||||
"eslint-visitor-keys",
|
||||
"follow-redirects",
|
||||
"lodash",
|
||||
"sharp",
|
||||
"typescript",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "jsonc with trailing commas",
|
||||
fixture: "test-fixtures/bun-trailing-comma/bun.lock",
|
||||
includeDevDependencies: true,
|
||||
wantNames: []string{"axios", "follow-redirects", "lodash"},
|
||||
},
|
||||
{
|
||||
name: "variable length tuples",
|
||||
fixture: "test-fixtures/bun-variable-tuples/bun.lock",
|
||||
includeDevDependencies: true,
|
||||
// the workspace package (@my/util) is a local first-party package, not a resolved
|
||||
// third-party dependency, so it is not cataloged from the lockfile.
|
||||
wantNames: []string{"axios", "follow-redirects", "ghostty-web", "tracestrings"},
|
||||
// the github tuple places its integrity hash at index 3 with the metadata object at
|
||||
// index 1, so a position-based parser (expecting metadata at index 2) would miss it.
|
||||
wantIntegrity: map[string]string{
|
||||
"ghostty-web": "sha512-nLx3R2hPwQvmL42LbiaQvbJpPZAXjzUtgU23G2LaKMRuA2mdXHdLQ5Hfw0PmxsohbqO/GhKOnTMcRrlLKS81+g==",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := os.Open(tt.fixture)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = f.Close() })
|
||||
|
||||
adapter := newGenericBunLockAdapter(CatalogerConfig{IncludeDevDependencies: tt.includeDevDependencies})
|
||||
pkgs, _, err := adapter.parseBunLock(
|
||||
context.Background(),
|
||||
nil,
|
||||
nil,
|
||||
file.NewLocationReadCloser(file.NewLocation(tt.fixture), f),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
byName := make(map[string]pkg.Package)
|
||||
var names []string
|
||||
for _, p := range pkgs {
|
||||
byName[p.Name] = p
|
||||
names = append(names, p.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, tt.wantNames, names)
|
||||
|
||||
for name, integrity := range tt.wantIntegrity {
|
||||
require.Contains(t, byName, name)
|
||||
meta, ok := byName[name].Metadata.(pkg.BunLockEntry)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, integrity, meta.Integrity)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBunPackageIdentifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
identifier string
|
||||
wantName string
|
||||
wantVer string
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
name: "simple package",
|
||||
identifier: "lodash@4.17.21",
|
||||
wantName: "lodash",
|
||||
wantVer: "4.17.21",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "scoped package",
|
||||
identifier: "@babel/core@7.24.0",
|
||||
wantName: "@babel/core",
|
||||
wantVer: "7.24.0",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "no version",
|
||||
identifier: "lodash",
|
||||
wantName: "",
|
||||
wantVer: "",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "scoped package with multiple @",
|
||||
identifier: "@org/pkg@1.0.0",
|
||||
wantName: "@org/pkg",
|
||||
wantVer: "1.0.0",
|
||||
wantOK: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotName, gotVer, gotOK := parseBunPackageIdentifier(tt.identifier)
|
||||
if gotName != tt.wantName || gotVer != tt.wantVer || gotOK != tt.wantOK {
|
||||
t.Errorf("parseBunPackageIdentifier(%q) = (%q, %q, %v), want (%q, %q, %v)",
|
||||
tt.identifier, gotName, gotVer, gotOK, tt.wantName, tt.wantVer, tt.wantOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "test-project",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"axios": ["axios@1.6.0", "", { "dependencies": { "follow-redirects": "^1.15.0" } }, "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.3", "", {}, "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "my-monorepo",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"ghostty-web": "github:rcarmo/ghostty-web#6c1c75b",
|
||||
"tracestrings": "github:oven-sh/bun.report#912ca63",
|
||||
},
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@my/util",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"my-monorepo": ["my-monorepo@root:", { "bin": { "my-cli": "bin/cli.js" } }],
|
||||
|
||||
"@my/util": ["@my/util@workspace:packages/util"],
|
||||
|
||||
"axios": ["axios@1.6.0", "", { "dependencies": { "follow-redirects": "^1.15.0" } }, "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.3", "", {}, "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="],
|
||||
|
||||
"ghostty-web": ["ghostty-web@github:rcarmo/ghostty-web#6c1c75b", {}, "rcarmo-ghostty-web-6c1c75b", "sha512-nLx3R2hPwQvmL42LbiaQvbJpPZAXjzUtgU23G2LaKMRuA2mdXHdLQ5Hfw0PmxsohbqO/GhKOnTMcRrlLKS81+g=="],
|
||||
|
||||
"tracestrings": ["tracestrings@github:oven-sh/bun.report#912ca63", {}, "oven-sh-bun.report-912ca63"],
|
||||
}
|
||||
}
|
||||
114
syft/pkg/cataloger/javascript/test-fixtures/bun/bun.lock
Normal file
114
syft/pkg/cataloger/javascript/test-fixtures/bun/bun.lock
Normal file
@ -0,0 +1,114 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "test-project",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"axios": "^1.6.0",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"eslint": "^9.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages": {
|
||||
"@img/sharp-darwin-arm64": [
|
||||
"@img/sharp-darwin-arm64@0.33.5",
|
||||
"",
|
||||
{
|
||||
"os": "darwin",
|
||||
"cpu": "arm64"
|
||||
},
|
||||
"sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="
|
||||
],
|
||||
"@img/sharp-linux-x64": [
|
||||
"@img/sharp-linux-x64@0.33.5",
|
||||
"",
|
||||
{
|
||||
"os": "linux",
|
||||
"cpu": "x64"
|
||||
},
|
||||
"sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="
|
||||
],
|
||||
"axios": [
|
||||
"axios@1.6.0",
|
||||
"",
|
||||
{
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0"
|
||||
}
|
||||
},
|
||||
"sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg=="
|
||||
],
|
||||
"color": [
|
||||
"color@4.2.3",
|
||||
"",
|
||||
{},
|
||||
"sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="
|
||||
],
|
||||
"eslint": [
|
||||
"eslint@9.0.0",
|
||||
"",
|
||||
{
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"eslint": "bin/eslint.js"
|
||||
}
|
||||
},
|
||||
"sha512-IMryZ5SudxzQvuod6rUdxH8KRx8BKHkTXpHWe3BJ9Qef3PbG9v9vjSB0STcKOVjTvPnG1+9T5e4xfzZ4wKdqiA=="
|
||||
],
|
||||
"eslint-visitor-keys": [
|
||||
"eslint-visitor-keys@4.0.0",
|
||||
"",
|
||||
{},
|
||||
"sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw=="
|
||||
],
|
||||
"follow-redirects": [
|
||||
"follow-redirects@1.15.3",
|
||||
"",
|
||||
{
|
||||
"peerDependencies": {
|
||||
"debug": "*"
|
||||
}
|
||||
},
|
||||
"sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="
|
||||
],
|
||||
"lodash": [
|
||||
"lodash@4.17.21",
|
||||
"",
|
||||
{},
|
||||
"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
],
|
||||
"sharp": [
|
||||
"sharp@0.33.5",
|
||||
"",
|
||||
{
|
||||
"dependencies": {
|
||||
"color": "^4.2.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5"
|
||||
}
|
||||
},
|
||||
"sha512-haPVm1EkS9pgvHrQ/F3Xy+hgxt/HmijnBghFeqCkXp3e+MH2fYF5o6qzrNJJ2k9s4bsZX0QBUCbFjr8VdLAIww=="
|
||||
],
|
||||
"typescript": [
|
||||
"typescript@5.0.0",
|
||||
"",
|
||||
{
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
}
|
||||
},
|
||||
"sha512-w5c493tkwFQLjKSYw0JvvVbqJ9ZM2SXoWBv/wqHYqWN/jP9bGilMKFbAKkpRHfaLxPn6A3K/fmJ6LD8B0EO9oA=="
|
||||
]
|
||||
}
|
||||
}
|
||||
0
syft/pkg/cataloger/javascript/testdata/glob-paths/src/bun.lock
vendored
Normal file
0
syft/pkg/cataloger/javascript/testdata/glob-paths/src/bun.lock
vendored
Normal file
@ -62,3 +62,27 @@ type PnpmLockEntry struct {
|
||||
// Dependencies is a map of dependencies and their versions
|
||||
Dependencies map[string]string `mapstructure:"dependencies" json:"dependencies"`
|
||||
}
|
||||
|
||||
// BunLockEntry represents a single entry in the "packages" section of a bun.lock file
|
||||
type BunLockEntry struct {
|
||||
// Integrity is Subresource Integrity hash for verification (SRI format)
|
||||
Integrity string `mapstructure:"integrity" json:"integrity"`
|
||||
|
||||
// Dependencies is a map of runtime dependencies and their version specifiers
|
||||
Dependencies map[string]string `mapstructure:"dependencies" json:"dependencies"`
|
||||
|
||||
// OptionalDependencies is a map of optional dependencies and their version specifiers
|
||||
OptionalDependencies map[string]string `mapstructure:"optionalDependencies" json:"optionalDependencies"`
|
||||
|
||||
// PeerDependencies is a map of peer dependencies and their version specifiers
|
||||
PeerDependencies map[string]string `mapstructure:"peerDependencies" json:"peerDependencies"`
|
||||
|
||||
// Bin is a map of binary names to the paths they are installed to
|
||||
Bin map[string]string `mapstructure:"bin" json:"bin"`
|
||||
|
||||
// OS is the operating system constraint for the package (e.g. "darwin")
|
||||
OS string `mapstructure:"os" json:"os"`
|
||||
|
||||
// CPU is the CPU architecture constraint for the package (e.g. "arm64")
|
||||
CPU string `mapstructure:"cpu" json:"cpu"`
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user