From 1844298f72edb383b398138979653684b38f1c2b Mon Sep 17 00:00:00 2001 From: Sahilb315 Date: Wed, 30 Apr 2025 02:26:19 +0530 Subject: [PATCH] feat: add pnpm support & introduce pkg manager wrap for npm --- cmd/ecosystems/npm.go | 158 -------------------------------------- cmd/npm/npm.go | 49 ++++++++++++ cmd/npm/pnpm.go | 49 ++++++++++++ main.go | 5 +- pkg/common/utils/utils.go | 1 + pkg/registry/factory.go | 3 +- pkg/wrapper/npm_base.go | 154 +++++++++++++++++++++++++++++++++++++ 7 files changed, 258 insertions(+), 161 deletions(-) delete mode 100644 cmd/ecosystems/npm.go create mode 100644 cmd/npm/npm.go create mode 100644 cmd/npm/pnpm.go create mode 100644 pkg/wrapper/npm_base.go diff --git a/cmd/ecosystems/npm.go b/cmd/ecosystems/npm.go deleted file mode 100644 index f1147e4..0000000 --- a/cmd/ecosystems/npm.go +++ /dev/null @@ -1,158 +0,0 @@ -package ecosystems - -import ( - "context" - _ "embed" - "fmt" - "os" - "time" - - "github.com/safedep/dry/log" - "github.com/safedep/pmg/internal/ui" - "github.com/safedep/pmg/pkg/analyser" - "github.com/safedep/pmg/pkg/common/utils" - "github.com/safedep/pmg/pkg/models" - "github.com/safedep/pmg/pkg/registry" - vetUtils "github.com/safedep/vet/pkg/common/utils" - "github.com/spf13/cobra" -) - -var ( - packageName string - action string -) - -func NewNpmCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "npm [action] [package]", - Short: "Scan packages from npm registry", - DisableFlagParsing: true, - RunE: func(cmd *cobra.Command, args []string) error { - npmPath, err := utils.GetExecutablePath("npm") - if err != nil { - fmt.Fprintf(os.Stderr, "npm not found: %v\n", err) - return err - } - - // Check if it's an install command that needs scanning - if len(args) >= 2 && utils.IsInstallCommand("npm", args[0]) { - // Verify required environment variables before proceeding - if err := utils.ValidateEnvVars(); err != nil { - return err - } - - action = args[0] - packageName = args[1] - - if err := wrapNpm(); err != nil { - os.Exit(1) - } - return nil - } - - // For non-install commands, pass through to npm - if err := utils.ExecCmd(npmPath, args, []string{}); err != nil { - os.Exit(1) - } - os.Exit(0) - return nil - }, - } - return cmd -} - -func wrapNpm() error { - ui.StartProgressWriter() - var progressTracker ui.ProgressTracker - - progressTracker = ui.TrackProgress(fmt.Sprintf("Scanning %s ", packageName), 1) - if packageName == "" { - return fmt.Errorf("package name cannot be empty") - } - - // Setup context with timeout for API calls - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) - defer cancel() - - factory := registry.NewFetcherFactory(10 * time.Second) - - // Get an NPM fetcher - npmFetcher, err := factory.CreateFetcher(registry.RegistryNPM) - if err != nil { - return err - } - name, version, err := utils.ParsePackageInfo(packageName) - - // If version is empty, get the latest version - if version == "" { - log.Infof("No version specified for %s, fetching latest version...", name) - version, err = npmFetcher.(*registry.NpmFetcher).ResolveVersion(ctx, name, version) - if err != nil { - return err - } - log.Infof("Latest version of %s is %s", name, version) - // Update packageName with resolved version for npm installation - packageName = fmt.Sprintf("%s@%s", name, version) - } - ui.IncrementProgress(progressTracker, 1) - - deps, err := npmFetcher.GetFlattenedDependencies(ctx, name, version) - ui.IncrementProgress(progressTracker, 1) - if err != nil { - return err - } - ui.IncrementTrackerTotal(progressTracker, int64(len(deps))) - client, err := analyser.GetMalwareAnalysisClient() - if err != nil { - return fmt.Errorf("error while creating a malware analysis client: %w", err) - } - pkgAnalyser := analyser.New(client, ctx) - - pkgAnalyser.ProgressTracker = progressTracker - handler := pkgAnalyser.Handler() - - // Create work queue with appropriate buffer size and concurrency - queue := vetUtils.NewWorkQueue[models.Package](100, 10, handler) - queue.Start() - defer queue.Stop() - - // Add packages to the queue - for _, dep := range deps { - name, version, err := utils.ParsePackageInfo(dep) - if err != nil { - log.Errorf("Error while parsing info of package %s", name) - continue - } - queue.Add(models.Package{ - Name: name, - Version: version, - }) - } - - // Wait for all analysis to complete - queue.Wait() - ui.MarkTrackerAsDone(progressTracker) - ui.StopProgressWriter() - - // Get the npm PATH and continue with installation - npmPath, err := utils.GetExecutablePath("npm") - if err != nil { - return fmt.Errorf("npm not found: %w", err) - } - - if len(pkgAnalyser.MaliciousPkgs) > 0 { - if !utils.ConfirmInstallation(pkgAnalyser.MaliciousPkgs) { - log.Infof("Installation canceled due to security concerns") - return nil - } - log.Warnf("Continuing installation despite security warnings...") - } - - cmdArgs := []string{action, packageName} - if err = utils.ExecCmd(npmPath, cmdArgs, []string{}); err != nil { - return fmt.Errorf("failed to execute npm command: %w", err) - } - - log.Infof("Successfully installed %s", packageName) - return nil -} diff --git a/cmd/npm/npm.go b/cmd/npm/npm.go new file mode 100644 index 0000000..8f8d582 --- /dev/null +++ b/cmd/npm/npm.go @@ -0,0 +1,49 @@ +package npm + +import ( + _ "embed" + "fmt" + "os" + + "github.com/safedep/pmg/pkg/common/utils" + "github.com/safedep/pmg/pkg/registry" + "github.com/safedep/pmg/pkg/wrapper" + "github.com/spf13/cobra" +) + +func NewNpmCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "npm [action] [package]", + Short: "Scan packages from npm registry", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + execPath, err := utils.GetExecutablePath(string(registry.RegistryNPM)) + if err != nil { + fmt.Fprintf(os.Stderr, "npm not found: %v\n", err) + return err + } + + if len(args) >= 2 && utils.IsInstallCommand(string(registry.RegistryNPM), args[0]) { + if err := utils.ValidateEnvVars(); err != nil { + return err + } + + pmw := wrapper.NewPackageManagerWrapper(registry.RegistryNPM) + pmw.Action = args[0] + pmw.PackageName = args[1] + + if err := pmw.Wrap(); err != nil { + os.Exit(1) + } + return nil + } + + if err := utils.ExecCmd(execPath, args, []string{}); err != nil { + os.Exit(1) + } + os.Exit(0) + return nil + }, + } + return cmd +} diff --git a/cmd/npm/pnpm.go b/cmd/npm/pnpm.go new file mode 100644 index 0000000..6ffe6a9 --- /dev/null +++ b/cmd/npm/pnpm.go @@ -0,0 +1,49 @@ +package npm + +import ( + _ "embed" + "fmt" + "os" + + "github.com/safedep/pmg/pkg/common/utils" + "github.com/safedep/pmg/pkg/registry" + "github.com/safedep/pmg/pkg/wrapper" + "github.com/spf13/cobra" +) + +func NewPnpmCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "pnpm [action] [package]", + Short: "Scan packages from npm registry", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + execPath, err := utils.GetExecutablePath(string(registry.RegistryPNPM)) + if err != nil { + fmt.Fprintf(os.Stderr, "pnpm not found: %v\n", err) + return err + } + + if len(args) >= 2 && utils.IsInstallCommand(string(registry.RegistryPNPM), args[0]) { + if err := utils.ValidateEnvVars(); err != nil { + return err + } + + pmw := wrapper.NewPackageManagerWrapper(registry.RegistryPNPM) + pmw.Action = args[0] + pmw.PackageName = args[1] + + if err := pmw.Wrap(); err != nil { + os.Exit(1) + } + return nil + } + + if err := utils.ExecCmd(execPath, args, []string{}); err != nil { + os.Exit(1) + } + os.Exit(0) + return nil + }, + } + return cmd +} diff --git a/main.go b/main.go index 28123e0..a85baf7 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ import ( "os" "github.com/safedep/dry/log" - "github.com/safedep/pmg/cmd/ecosystems" + "github.com/safedep/pmg/cmd/npm" "github.com/spf13/cobra" ) @@ -30,7 +30,8 @@ func main() { cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") - cmd.AddCommand(ecosystems.NewNpmCommand()) + cmd.AddCommand(npm.NewNpmCommand()) + cmd.AddCommand(npm.NewPnpmCommand()) if err := cmd.Execute(); err != nil { os.Exit(1) diff --git a/pkg/common/utils/utils.go b/pkg/common/utils/utils.go index 34f7af1..51e13e3 100644 --- a/pkg/common/utils/utils.go +++ b/pkg/common/utils/utils.go @@ -42,6 +42,7 @@ func IsInstallCommand(pkgManager, cmd string) bool { "add": true, }, "pnpm": { + "add": true, "install": true, "i": true, }, diff --git a/pkg/registry/factory.go b/pkg/registry/factory.go index 284eb98..2125dab 100644 --- a/pkg/registry/factory.go +++ b/pkg/registry/factory.go @@ -10,6 +10,7 @@ type RegistryType string const ( RegistryNPM RegistryType = "npm" + RegistryPNPM RegistryType = "pnpm" RegistryPyPI RegistryType = "pypi" RegistryGo RegistryType = "go" ) @@ -29,7 +30,7 @@ func NewFetcherFactory(timeout time.Duration) *FetcherFactory { // CreateFetcher returns a fetcher for the specified registry type func (ff *FetcherFactory) CreateFetcher(registryType RegistryType) (Fetcher, error) { switch registryType { - case RegistryNPM: + case RegistryNPM, RegistryPNPM: return NewNpmFetcher(ff.timeout), nil default: return nil, fmt.Errorf("unsupported registry type: %s", registryType) diff --git a/pkg/wrapper/npm_base.go b/pkg/wrapper/npm_base.go new file mode 100644 index 0000000..db96919 --- /dev/null +++ b/pkg/wrapper/npm_base.go @@ -0,0 +1,154 @@ +package wrapper + +import ( + "context" + "fmt" + "time" + + "github.com/safedep/dry/log" + "github.com/safedep/pmg/internal/ui" + "github.com/safedep/pmg/pkg/analyser" + "github.com/safedep/pmg/pkg/common/utils" + "github.com/safedep/pmg/pkg/models" + "github.com/safedep/pmg/pkg/registry" + vetUtils "github.com/safedep/vet/pkg/common/utils" +) + +type PackageManagerWrapper struct { + RegistryType registry.RegistryType + Action string + PackageName string +} + +func NewPackageManagerWrapper(registryType registry.RegistryType) *PackageManagerWrapper { + return &PackageManagerWrapper{ + RegistryType: registryType, + } +} + +func (pmw *PackageManagerWrapper) Wrap() error { + ui.StartProgressWriter() + + progressTracker := ui.TrackProgress(fmt.Sprintf("Scanning %s ", pmw.PackageName), 1) + if pmw.PackageName == "" { + return fmt.Errorf("package name cannot be empty") + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + if err := pmw.scanAndInstall(ctx, progressTracker); err != nil { + return err + } + + log.Infof("Successfully installed %s", pmw.PackageName) + return nil +} + +func (pmw *PackageManagerWrapper) scanAndInstall(ctx context.Context, progressTracker ui.ProgressTracker) error { + factory := registry.NewFetcherFactory(10 * time.Second) + fetcher, err := factory.CreateFetcher(pmw.RegistryType) + if err != nil { + return err + } + + name, version, err := utils.ParsePackageInfo(pmw.PackageName) + if err != nil { + return err + } + + if version == "" { + version, err = pmw.resolveLatestVersion(ctx, fetcher, name) + if err != nil { + return err + } + pmw.PackageName = fmt.Sprintf("%s@%s", name, version) + } + + deps, err := pmw.getDependencies(ctx, fetcher, name, version, progressTracker) + if err != nil { + return err + } + + if err := pmw.analyzeDependencies(ctx, deps, progressTracker); err != nil { + return err + } + + return pmw.executeInstallation() +} + +func (pmw *PackageManagerWrapper) resolveLatestVersion(ctx context.Context, fetcher registry.Fetcher, name string) (string, error) { + log.Infof("No version specified for %s, fetching latest version...", name) + version, err := fetcher.(*registry.NpmFetcher).ResolveVersion(ctx, name, "") + if err != nil { + return "", err + } + log.Infof("Latest version of %s is %s", name, version) + return version, nil +} + +func (pmw *PackageManagerWrapper) getDependencies(ctx context.Context, fetcher registry.Fetcher, name, version string, progressTracker ui.ProgressTracker) ([]string, error) { + ui.IncrementProgress(progressTracker, 1) + deps, err := fetcher.GetFlattenedDependencies(ctx, name, version) + ui.IncrementProgress(progressTracker, 1) + if err != nil { + return nil, err + } + ui.IncrementTrackerTotal(progressTracker, int64(len(deps))) + return deps, nil +} + +func (pmw *PackageManagerWrapper) analyzeDependencies(ctx context.Context, deps []string, progressTracker ui.ProgressTracker) error { + client, err := analyser.GetMalwareAnalysisClient() + if err != nil { + return fmt.Errorf("error while creating a malware analysis client: %w", err) + } + + pkgAnalyser := analyser.New(client, ctx) + pkgAnalyser.ProgressTracker = progressTracker + handler := pkgAnalyser.Handler() + + queue := vetUtils.NewWorkQueue[models.Package](100, 10, handler) + queue.Start() + defer queue.Stop() + + for _, dep := range deps { + name, version, err := utils.ParsePackageInfo(dep) + if err != nil { + log.Errorf("Error while parsing info of package %s", name) + continue + } + queue.Add(models.Package{ + Name: name, + Version: version, + }) + } + + queue.Wait() + ui.MarkTrackerAsDone(progressTracker) + ui.StopProgressWriter() + + if len(pkgAnalyser.MaliciousPkgs) > 0 { + if !utils.ConfirmInstallation(pkgAnalyser.MaliciousPkgs) { + log.Infof("Installation canceled due to security concerns") + return fmt.Errorf("installation canceled") + } + log.Warnf("Continuing installation despite security warnings...") + } + + return nil +} + +func (pmw *PackageManagerWrapper) executeInstallation() error { + execPath, err := utils.GetExecutablePath(string(pmw.RegistryType)) + if err != nil { + return fmt.Errorf("%s not found: %w", pmw.RegistryType, err) + } + + cmdArgs := []string{pmw.Action, pmw.PackageName} + if err = utils.ExecCmd(execPath, cmdArgs, []string{}); err != nil { + return fmt.Errorf("failed to execute %s command: %w", pmw.RegistryType, err) + } + + return nil +}