From c486834c2e0681b99e494d8a4a656e4d4f7bb00a Mon Sep 17 00:00:00 2001 From: Sahil Bansal Date: Mon, 28 Jul 2025 17:23:32 +0530 Subject: [PATCH] feat/#55 bun support (#56) * add support for bun package manager * update readme --- README.md | 19 +++++++++++- cmd/npm/bun.go | 60 +++++++++++++++++++++++++++++++++++++ extractor/ecosystems.go | 2 ++ extractor/npm.go | 56 ++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ internal/analytics/event.go | 5 ++++ main.go | 1 + packagemanager/npm.go | 9 +++++- 9 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 cmd/npm/bun.go diff --git a/README.md b/README.md index f7565e8..6195a71 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ pnpm add - [Usage](#usage) - [Recommended: Automated Setup](#recommended) - [Alternative: Manual Commands](#alternative) + - [Lockfile Installation](#lockfile) - [Silent Mode](#silent-mode) - [Dry Run](#dry-run) - [Verbose Mode](#verbose-mode) @@ -87,10 +88,11 @@ PMG supports the following package managers: | --------------- | --------- | --------------------------- | | `npm` | ✅ Active | `pmg npm install ` | | `pnpm` | ✅ Active | `pmg pnpm add ` | +| `bun` | ✅ Active | `pmg bun add ` | | `pip` | ✅ Active | `pmg pip install ` | +| `uv` | 🚧 Planned | | | `yarn` | 🚧 Planned | | | `poetry` | 🚧 Planned | | -| `uv` | 🚧 Planned | | > Want us to support your favorite package manager? [Open an issue](https://github.com/safedep/pmg/issues) and let us know! @@ -164,6 +166,7 @@ After setup, use your package managers normally: ```bash npm install pnpm add +bun add pip install ``` @@ -173,9 +176,23 @@ You can also run PMG manually without aliases: ```bash pmg npm install pmg pnpm add +pmg bun add pmg pip install ``` +### Lockfile Installation +PMG seamlessly protects lockfile-based installations: + +```bash +npm install # Uses package-lock.json +pnpm install # Uses pnpm-lock.yaml +bun install # Uses bun.lock + +pip install -r requirements.txt # Uses requirements file +``` + +PMG scans the exact package versions specified in lockfiles and blocks installation if malicious packages are detected. + ### Silent Mode Use the `--silent` flag to run PMG in silent mode: diff --git a/cmd/npm/bun.go b/cmd/npm/bun.go new file mode 100644 index 0000000..451fcfd --- /dev/null +++ b/cmd/npm/bun.go @@ -0,0 +1,60 @@ +package npm + +import ( + "context" + "fmt" + + "github.com/safedep/dry/log" + "github.com/safedep/pmg/config" + "github.com/safedep/pmg/internal/analytics" + "github.com/safedep/pmg/internal/flows" + "github.com/safedep/pmg/internal/ui" + "github.com/safedep/pmg/packagemanager" + "github.com/spf13/cobra" +) + +func NewBunCommand() *cobra.Command { + return &cobra.Command{ + Use: "bun [action] [package]", + Short: "Guard bun package manager", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + err := executeBunFlow(cmd.Context(), args) + if err != nil { + log.Errorf("Failed to execute bun flow: %s", err) + } + + return nil + }, + } +} + +func executeBunFlow(ctx context.Context, args []string) error { + analytics.TrackCommandBun() + packageManager, err := packagemanager.NewNpmPackageManager(packagemanager.DefaultBunPackageManagerConfig()) + if err != nil { + ui.Fatalf("Failed to create bun package manager proxy: %s", err) + } + + config, err := config.FromContext(ctx) + if err != nil { + ui.Fatalf("Failed to get config: %s", err) + } + + parsedCommand, err := packageManager.ParseCommand(args) + if err != nil { + return fmt.Errorf("failed to parse command: %w", err) + } + + packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig() + packageResolverConfig.IncludeTransitiveDependencies = config.Transitive + packageResolverConfig.TransitiveDepth = config.TransitiveDepth + packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies + + packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig) + if err != nil { + ui.Fatalf("Failed to create dependency resolver: %s", err) + } + + return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) +} diff --git a/extractor/ecosystems.go b/extractor/ecosystems.go index 7fb3430..70e61ab 100644 --- a/extractor/ecosystems.go +++ b/extractor/ecosystems.go @@ -25,6 +25,7 @@ const ( Npm PackageManagerName = "npm" Pnpm PackageManagerName = "pnpm" Pip PackageManagerName = "pip" + Bun PackageManagerName = "bun" ) type ExtractorManager struct { @@ -37,6 +38,7 @@ func NewExtractorManager() *ExtractorManager { Npm: &NpmExtractor{}, Pnpm: &PnpmExtractor{}, Pip: &PipExtractor{}, + Bun: &BunExtractor{}, }, } } diff --git a/extractor/npm.go b/extractor/npm.go index 48acb82..3f0da9a 100644 --- a/extractor/npm.go +++ b/extractor/npm.go @@ -7,6 +7,7 @@ import ( packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/bunlock" "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson" "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/pnpmlock" "github.com/google/osv-scalibr/fs" @@ -123,3 +124,58 @@ func parsePnpmLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersio return packages, nil } + +type BunExtractor struct{} + +func (n *BunExtractor) GetSupportedFiles() []string { + return []string{"bun.lock"} +} + +func (n *BunExtractor) GetEcosystem() packagev1.Ecosystem { + return packagev1.Ecosystem_ECOSYSTEM_NPM +} + +func (n *BunExtractor) GetPackageManager() PackageManagerName { + return Bun +} + +func (n *BunExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) { + return parseBunPackageLockFile(lockfilePath, scanDir) +} + +func parseBunPackageLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) { + bunlockExtractor := bunlock.New() + + file, err := os.Open(lockfilePath) + if err != nil { + return nil, fmt.Errorf("failed to open lockfile: %s", err) + } + defer file.Close() + + inputConfig := &filesystem.ScanInput{ + FS: fs.DirFS(scanDir), + Path: lockfilePath, + Reader: file, + } + + inventory, err := bunlockExtractor.Extract(context.Background(), inputConfig) + if err != nil { + return nil, fmt.Errorf("failed to extract packages: %s", err) + } + + var packages []*packagev1.PackageVersion + + for _, invPkg := range inventory.Packages { + pkg := &packagev1.PackageVersion{ + Package: &packagev1.Package{ + Name: invPkg.Name, + Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM, + }, + Version: invPkg.Version, + } + + packages = append(packages, pkg) + } + + return packages, nil +} diff --git a/go.mod b/go.mod index 9dd68f9..d1e7c29 100644 --- a/go.mod +++ b/go.mod @@ -180,6 +180,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tdakkota/asciicheck v0.4.1 // indirect github.com/tetafro/godot v1.5.0 // indirect + github.com/tidwall/jsonc v0.3.2 // indirect github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect github.com/timonwong/loggercheck v0.10.1 // indirect github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect diff --git a/go.sum b/go.sum index 8a45df5..014a2c8 100644 --- a/go.sum +++ b/go.sum @@ -448,6 +448,8 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc= +github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg= github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= diff --git a/internal/analytics/event.go b/internal/analytics/event.go index d0db7c9..dddc4a5 100644 --- a/internal/analytics/event.go +++ b/internal/analytics/event.go @@ -3,6 +3,7 @@ package analytics const ( eventRun = "pmg_command_run" eventCommandNpm = "pmg_command_npm" + eventCommandBun = "pmg_command_bun" eventCommandPnpm = "pmg_command_pnpm" eventCommandPip = "pmg_command_pip" @@ -19,6 +20,10 @@ func TrackCommandNpm() { TrackEvent(eventCommandNpm) } +func TrackCommandBun() { + TrackEvent(eventCommandBun) +} + func TrackCommandPnpm() { TrackEvent(eventCommandPnpm) } diff --git a/main.go b/main.go index 720b0ad..956157a 100644 --- a/main.go +++ b/main.go @@ -90,6 +90,7 @@ func main() { cmd.AddCommand(npm.NewNpmCommand()) cmd.AddCommand(npm.NewPnpmCommand()) + cmd.AddCommand(npm.NewBunCommand()) cmd.AddCommand(pypi.NewPipCommand()) cmd.AddCommand(version.NewVersionCommand()) cmd.AddCommand(setup.NewSetupCommand()) diff --git a/packagemanager/npm.go b/packagemanager/npm.go index bff49a1..91dd3c4 100644 --- a/packagemanager/npm.go +++ b/packagemanager/npm.go @@ -27,6 +27,13 @@ func DefaultPnpmPackageManagerConfig() NpmPackageManagerConfig { } } +func DefaultBunPackageManagerConfig() NpmPackageManagerConfig { + return NpmPackageManagerConfig{ + InstallCommands: []string{"install", "i", "add"}, + CommandName: "bun", + } +} + type npmPackageManager struct { Config NpmPackageManagerConfig } @@ -48,7 +55,7 @@ func (npm *npmPackageManager) Ecosystem() packagev1.Ecosystem { } func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error) { - if len(args) > 0 && (args[0] == "npm" || args[0] == "pnpm") { + if len(args) > 0 && (args[0] == "npm" || args[0] == "pnpm" || args[0] == "bun") { args = args[1:] }