Add support for package executors and support for PTY handling (#100)

* define contract for package executors

* introduce npx executor

* add npx and pnpx cmd support

* fix typo

* rm PackageExecutor and depend on PackageManager interface

* add support for PTY to handle parent-child process interaction

* refactor PTY handling in proxy flow

* enforce interactiveSession interface check

* close reader explicitly and clean npm version for pkg executors

* rm interaction from interceptors

* add docs and wait for outputRouter before exit

* add support for non interactive TTY for proxy mode

* add support for CI env var check for non interactive tty proxy mode

* update readme to include npx, pnpx support

* Update internal/flows/proxy_flow.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* update ptyx lib

* fix docs typo

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sahil Bansal
2026-01-09 22:03:42 +05:30
committed by GitHub
co-authored by Copilot
parent a373b5b243
commit 31f23fd065
21 changed files with 1009 additions and 96 deletions
+2
View File
@@ -84,6 +84,8 @@ PMG supports the following package managers:
| `pip` | ✅ Active | `pmg pip install <package>` | | `pip` | ✅ Active | `pmg pip install <package>` |
| `uv` | ✅ Active | `pmg uv add <package>` or `pmg uv pip install <package>` | | `uv` | ✅ Active | `pmg uv add <package>` or `pmg uv pip install <package>` |
| `poetry` | ✅ Active | `pmg poetry add <package>` | | `poetry` | ✅ Active | `pmg poetry add <package>` |
| `npx` | ✅ Active | `pmg npx <package> <action>` |
| `pnpx` | ✅ Active | `pmg pnpx <package> <action>` |
> Want us to support your favorite package manager? [Open an issue](https://github.com/safedep/pmg/issues) and let us know! > Want us to support your favorite package manager? [Open an issue](https://github.com/safedep/pmg/issues) and let us know!
+59
View File
@@ -0,0 +1,59 @@
package executors
import (
"context"
"fmt"
"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 NewNpxCommand() *cobra.Command {
return &cobra.Command{
Use: "npx [package] [action]",
Short: "Guard npx package executor",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
err := executeNpxFlow(cmd.Context(), args)
if err != nil {
ui.ErrorExit(err)
}
return nil
},
}
}
func executeNpxFlow(ctx context.Context, args []string) error {
analytics.TrackCommandNpx()
packageExecutor, err := packagemanager.NewNpmPackageExecutor(packagemanager.DefaultNpxPackageExecutorConfig())
if err != nil {
return fmt.Errorf("failed to create npx package executor proxy: %w", err)
}
config := config.Get()
parsedCommand, err := packageExecutor.ParseCommand(args)
if err != nil {
return fmt.Errorf("failed to parse command: %w", err)
}
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
if err != nil {
return fmt.Errorf("failed to create dependency resolver: %w", err)
}
if config.Config.ExperimentalProxyMode {
return flows.ProxyFlow(packageExecutor, packageResolver).Run(ctx, args, parsedCommand)
}
return flows.Common(packageExecutor, packageResolver).Run(ctx, args, parsedCommand)
}
+59
View File
@@ -0,0 +1,59 @@
package executors
import (
"context"
"fmt"
"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 NewPnpxCommand() *cobra.Command {
return &cobra.Command{
Use: "pnpx [package] [action]",
Short: "Guard pnpx package executor",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
err := executePnpxFlow(cmd.Context(), args)
if err != nil {
ui.ErrorExit(err)
}
return nil
},
}
}
func executePnpxFlow(ctx context.Context, args []string) error {
analytics.TrackCommandPnpx()
packageExecutor, err := packagemanager.NewNpmPackageExecutor(packagemanager.DefaultPnpxPackageExecutorConfig())
if err != nil {
return fmt.Errorf("failed to create pnpx package executor proxy: %w", err)
}
config := config.Get()
parsedCommand, err := packageExecutor.ParseCommand(args)
if err != nil {
return fmt.Errorf("failed to parse command: %w", err)
}
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
if err != nil {
return fmt.Errorf("failed to create dependency resolver: %w", err)
}
if config.Config.ExperimentalProxyMode {
return flows.ProxyFlow(packageExecutor, packageResolver).Run(ctx, args, parsedCommand)
}
return flows.Common(packageExecutor, packageResolver).Run(ctx, args, parsedCommand)
}
+6 -6
View File
@@ -3,9 +3,9 @@
PMG supports an experimental proxy based interception as an alternative to the current optimistic dependency resolution. When enabled via `--experimental-proxy-mode` flag: PMG supports an experimental proxy based interception as an alternative to the current optimistic dependency resolution. When enabled via `--experimental-proxy-mode` flag:
- PMG starts a micro-proxy server on a random localhost port - PMG starts a micro-proxy server on a random localhost port
- Run `npm` and other supported package managers configured to use the proxy - Runs `npm` and other supported package managers configured to use the proxy
- Intercept package registry requests and analyze packages as they are downloaded - Intercepts package registry requests and analyzes packages as they are downloaded
- Block malicious packages and allow trusted packages to be installed - Blocks malicious packages and allows trusted packages to be installed
## Usage ## Usage
@@ -26,10 +26,10 @@ experimental_proxy_mode: true
| Package Manager | Status | | Package Manager | Status |
| --------------- | --------- | | --------------- | --------- |
| `npm` | ✅ Active | | `npm` | ✅ Active |
| `npx` | 🕒 Planned | | `npx` | ✅ Active |
| `yarn` | 🕒 Planned | | `pnpx` | ✅ Active |
| `pnpm` | 🕒 Planned | | `pnpm` | 🕒 Planned |
| `pnpx` | 🕒 Planned | | `yarn` | 🕒 Planned |
| `bun` | 🕒 Planned | | `bun` | 🕒 Planned |
| `pip` | 🕒 Planned | | `pip` | 🕒 Planned |
| `uv` | 🕒 Planned | | `uv` | 🕒 Planned |
+1 -2
View File
@@ -11,9 +11,8 @@ If you don't have a `config.yml` file, you can create one by running `pmg setup
```yaml ```yaml
trusted_packages: trusted_packages:
- purl: pkg:npm/safedep/pmg - purl: pkg:npm/@safedep/pmg
reason: "All versions of PMG are trusted" reason: "All versions of PMG are trusted"
- purl: pkg:npm/express@4.18.0 - purl: pkg:npm/express@4.18.0
reason: "Version 4.18.0 of Express is a trusted package" reason: "Version 4.18.0 of Express is a trusted package"
``` ```
+7 -5
View File
@@ -4,24 +4,28 @@ go 1.25.1
tool github.com/golangci/golangci-lint/cmd/golangci-lint tool github.com/golangci/golangci-lint/cmd/golangci-lint
replace github.com/KennethanCeyer/ptyx v0.2.0 => github.com/safedep/ptyx v0.2.0
require ( require (
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2 buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1 buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1
github.com/KennethanCeyer/ptyx v0.2.0
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/elazarl/goproxy v1.7.2 github.com/elazarl/goproxy v1.7.2
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/google/osv-scalibr v0.2.1 github.com/google/osv-scalibr v0.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/mitchellh/mapstructure v1.5.0
github.com/posthog/posthog-go v1.5.12 github.com/posthog/posthog-go v1.5.12
github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175 github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sys v0.35.0
golang.org/x/term v0.34.0
google.golang.org/grpc v1.72.0 google.golang.org/grpc v1.72.0
google.golang.org/protobuf v1.36.6
) )
require ( require (
@@ -207,19 +211,17 @@ require (
go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.26.0 // indirect golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.35.0 // indirect golang.org/x/tools v0.35.0 // indirect
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
+15 -6
View File
@@ -84,6 +84,8 @@ github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd
github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
@@ -102,7 +104,12 @@ github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVo
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -308,8 +315,6 @@ github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY=
github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -343,6 +348,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -385,6 +392,8 @@ github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9f
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175 h1:TxAI6m/v01CL+kwIYE3RZsuxu01pbuGy3wOi3WyBT1E= github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175 h1:TxAI6m/v01CL+kwIYE3RZsuxu01pbuGy3wOi3WyBT1E=
github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175/go.mod h1:Mdqx/Q2DhAcN38XiUNTGCC5MktofYDQW9Az7YWGEF0s= github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175/go.mod h1:Mdqx/Q2DhAcN38XiUNTGCC5MktofYDQW9Az7YWGEF0s=
github.com/safedep/ptyx v0.2.0 h1:4M3YlVVB25ze0ANnY8JgOznhMFjiGMQP/pSDbIHclik=
github.com/safedep/ptyx v0.2.0/go.mod h1:aLBSWDiEko9wd9zJj0hE8LiXpbLXM1t5QxuRWF22U4c=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
@@ -612,8 +621,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -622,8 +631,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+19
View File
@@ -3,6 +3,7 @@ package guard
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"slices" "slices"
@@ -36,6 +37,24 @@ type PackageManagerGuardInteraction struct {
// packages are passed as arguments. These are the packages that were detected as malicious. // packages are passed as arguments. These are the packages that were detected as malicious.
// Client code must perform the necessary error handling and termination of the process. // Client code must perform the necessary error handling and termination of the process.
Block func(config *ui.BlockConfig) error Block func(config *ui.BlockConfig) error
// inputReader is the reader to use for user input during confirmations.
// If nil, os.Stdin is used. This is set via SetInput to allow PTY input routing.
inputReader io.Reader
}
// SetInput sets the input reader for user confirmations.
// This allows the PTY switchboard to route input to the prompt during confirmations.
func (i *PackageManagerGuardInteraction) SetInput(r io.Reader) {
i.inputReader = r
}
// Reader returns the configured input reader, or os.Stdin if none is set.
func (i *PackageManagerGuardInteraction) Reader() io.Reader {
if i.inputReader != nil {
return i.inputReader
}
return os.Stdin
} }
type PackageManagerGuardConfig struct { type PackageManagerGuardConfig struct {
+11
View File
@@ -11,6 +11,9 @@ const (
eventCommandUv = "pmg_command_uv" eventCommandUv = "pmg_command_uv"
eventCommandPoetry = "pmg_command_poetry" eventCommandPoetry = "pmg_command_poetry"
eventCommandNpx = "pmg_command_npx"
eventCommandPnpx = "pmg_command_pnpx"
eventPmgGenerateEnvDocker = "pmg_command_generate_env_docker" eventPmgGenerateEnvDocker = "pmg_command_generate_env_docker"
eventPmgGenerateEnvGitHubActions = "pmg_command_generate_env_github_actions" eventPmgGenerateEnvGitHubActions = "pmg_command_generate_env_github_actions"
eventPmgGenerateEnvGitLabCI = "pmg_command_generate_env_gitlab_ci" eventPmgGenerateEnvGitLabCI = "pmg_command_generate_env_gitlab_ci"
@@ -24,6 +27,14 @@ func TrackCommandNpm() {
TrackEvent(eventCommandNpm) TrackEvent(eventCommandNpm)
} }
func TrackCommandNpx() {
TrackEvent(eventCommandNpx)
}
func TrackCommandPnpx() {
TrackEvent(eventCommandPnpx)
}
func TrackCommandBun() { func TrackCommandBun() {
TrackEvent(eventCommandBun) TrackEvent(eventCommandBun)
} }
+175 -51
View File
@@ -2,16 +2,20 @@ package flows
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config" "github.com/safedep/pmg/config"
"github.com/safedep/pmg/guard" "github.com/safedep/pmg/guard"
"github.com/safedep/pmg/internal/pty"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager" "github.com/safedep/pmg/packagemanager"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
@@ -34,6 +38,15 @@ func ProxyFlow(pm packagemanager.PackageManager, packageResolver packagemanager.
// Run executes the proxy-based flow // Run executes the proxy-based flow
func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) error { func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) error {
// Get the ecosystem from the package manager
ecosystem := f.pm.Ecosystem()
// Check if proxy mode is supported for this ecosystem
if !interceptors.IsSupported(ecosystem) {
return fmt.Errorf("proxy mode is not supported for %s", ecosystem.String())
}
cfg := config.Get() cfg := config.Get()
// Check if dry-run mode is enabled // Check if dry-run mode is enabled
@@ -82,24 +95,16 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
defer close(confirmationChan) defer close(confirmationChan)
// Create interaction callbacks for user prompts // Create interaction callbacks for user prompts
interaction := guard.PackageManagerGuardInteraction{ // Note: We use a pointer so we can later inject the input reader via SetInput
SetStatus: ui.SetStatus, interaction := &guard.PackageManagerGuardInteraction{
ClearStatus: ui.ClearStatus, SetStatus: ui.SetStatus,
ShowWarning: ui.ShowWarning, ClearStatus: ui.ClearStatus,
GetConfirmationOnMalware: ui.GetConfirmationOnMalware, ShowWarning: ui.ShowWarning,
Block: ui.Block, Block: ui.Block,
}
// Get the ecosystem from the package manager
ecosystem := f.pm.Ecosystem()
// Check if proxy mode is supported for this ecosystem
if !interceptors.IsSupported(ecosystem) {
return fmt.Errorf("proxy mode is not supported for %s", ecosystem.String())
} }
// Create ecosystem-specific interceptor using factory // Create ecosystem-specific interceptor using factory
factory := interceptors.NewInterceptorFactory(malysisAnalyzer, cache, confirmationChan, interaction) factory := interceptors.NewInterceptorFactory(malysisAnalyzer, cache, confirmationChan)
interceptor, err := factory.CreateInterceptor(ecosystem) interceptor, err := factory.CreateInterceptor(ecosystem)
if err != nil { if err != nil {
return fmt.Errorf("failed to create interceptor for %s: %w", ecosystem.String(), err) return fmt.Errorf("failed to create interceptor for %s: %w", ecosystem.String(), err)
@@ -127,8 +132,15 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
log.Infof("Proxy server started on %s", proxyAddr) log.Infof("Proxy server started on %s", proxyAddr)
log.Infof("Running %s with proxy protection enabled", f.pm.Name()) log.Infof("Running %s with proxy protection enabled", f.pm.Name())
proxyEnv := f.setupEnvForProxy(proxyAddr, caCertPath)
if !pty.IsInteractiveTerminal() {
// Execute the package manager command with proxy environment variables for non PTY or non-interactive TTY
return f.executeWithProxyForNonInteractiveTTY(ctx, parsedCmd, proxyEnv, confirmationChan, interaction)
}
// Execute the package manager command with proxy environment variables // Execute the package manager command with proxy environment variables
return f.executeWithProxy(ctx, parsedCmd, proxyAddr, caCertPath, confirmationChan, interaction) return f.executeWithProxy(ctx, parsedCmd, proxyEnv, confirmationChan, interaction)
} }
// setupCACertificate generates or loads a CA certificate for MITM // setupCACertificate generates or loads a CA certificate for MITM
@@ -211,21 +223,11 @@ func (f *proxyFlow) createAndStartProxyServer(
return proxyServer, proxyAddr, nil return proxyServer, proxyAddr, nil
} }
// executeWithProxy executes the package manager command with proxy environment variables func (f *proxyFlow) setupEnvForProxy(proxyAddr, caCertPath string) []string {
func (f *proxyFlow) executeWithProxy(ctx context.Context, parsedCmd *packagemanager.ParsedCommand,
proxyAddr, caCertPath string, confirmationChan chan *interceptors.ConfirmationRequest,
interaction guard.PackageManagerGuardInteraction,
) error {
// Build proxy URL
proxyURL := fmt.Sprintf("http://%s", proxyAddr) proxyURL := fmt.Sprintf("http://%s", proxyAddr)
// Create command env := os.Environ()
cmd := exec.CommandContext(ctx, parsedCmd.Command.Exe, parsedCmd.Command.Args...) env = append(env,
// Set proxy environment variables. This is what tells the executed command to use the proxy for communication.
// However, every package manager has its nuances and may require additional environment variables to be set.
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("HTTP_PROXY=%s", proxyURL), fmt.Sprintf("HTTP_PROXY=%s", proxyURL),
fmt.Sprintf("HTTPS_PROXY=%s", proxyURL), fmt.Sprintf("HTTPS_PROXY=%s", proxyURL),
fmt.Sprintf("NODE_EXTRA_CA_CERTS=%s", caCertPath), fmt.Sprintf("NODE_EXTRA_CA_CERTS=%s", caCertPath),
@@ -237,32 +239,35 @@ func (f *proxyFlow) executeWithProxy(ctx context.Context, parsedCmd *packagemana
fmt.Sprintf("PIP_PROXY=%s", proxyURL), fmt.Sprintf("PIP_PROXY=%s", proxyURL),
) )
return env
}
// executeWithProxyForNonInteractiveTTY runs the command without PTY (for CI/non-interactive environments)
func (f *proxyFlow) executeWithProxyForNonInteractiveTTY(
ctx context.Context,
parsedCmd *packagemanager.ParsedCommand,
env []string,
confirmationChan chan *interceptors.ConfirmationRequest,
interaction *guard.PackageManagerGuardInteraction,
) error {
log.Debugf("Executing proxy for non interactive TTY")
// For non-interactive terminals, we enforce suspicious packages as malicious
interaction.GetConfirmationOnMalware = func(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
return false, nil
}
cmd := exec.CommandContext(ctx, parsedCmd.Command.Exe, parsedCmd.Command.Args...)
cmd.Env = append(env, "CI=true")
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
log.Debugf("Executing command: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args) go interceptors.HandleConfirmationRequests(
log.Debugf("Proxy environment: HTTP_PROXY=%s, HTTPS_PROXY=%s, NODE_EXTRA_CA_CERTS=%s", proxyURL, proxyURL, caCertPath) confirmationChan,
interaction,
// Start confirmation handler in goroutine. Use confirmation hooks to pause and resume the executed nil,
// process to prevent stdout and stderr from being mixed up. Pause / resume is on a best effort basis. )
// We do not consider it a critical error if pause / resume fails.
go interceptors.HandleConfirmationRequests(confirmationChan, interaction, &interceptors.ConfirmationHook{
BeforeInteraction: func([]*analyzer.PackageVersionAnalysisResult) error {
if err := platformPauseProcess(cmd); err != nil {
log.Warnf("Failed to pause process for user interaction: %v", err)
}
return nil
},
AfterInteraction: func([]*analyzer.PackageVersionAnalysisResult, bool) error {
if err := platformResumeProcess(cmd); err != nil {
log.Warnf("Failed to resume process after user interaction: %v", err)
}
return nil
},
})
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
@@ -276,3 +281,122 @@ func (f *proxyFlow) executeWithProxy(ctx context.Context, parsedCmd *packagemana
log.Debugf("Command completed successfully") log.Debugf("Command completed successfully")
return nil return nil
} }
// executeWithProxy executes the package manager command with proxy environment variables.
func (f *proxyFlow) executeWithProxy(
ctx context.Context,
parsedCmd *packagemanager.ParsedCommand,
env []string,
confirmationChan chan *interceptors.ConfirmationRequest,
interaction *guard.PackageManagerGuardInteraction,
) error {
log.Debugf("Executing proxy for interactive TTY")
// Set the confirmation handler to use the interaction's reader
// This allows PTY input routing during proxy mode
interaction.GetConfirmationOnMalware = func(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
return ui.GetConfirmationOnMalwareWithReader(malwarePackages, interaction.Reader())
}
sessionConfig := pty.NewSessionConfig(parsedCmd.Command.Exe, parsedCmd.Command.Args, env)
sess, err := pty.NewSession(ctx, sessionConfig)
if err != nil {
return fmt.Errorf("failed to create pty session: %w", err)
}
defer sess.Close()
outputRouter, err := pty.NewOutputRouter(os.Stdout)
if err != nil {
return fmt.Errorf("failed to create output router: %w", err)
}
var wg sync.WaitGroup
wg.Go(func() {
io.Copy(outputRouter, sess.PtyReader())
})
inputRouter, err := pty.NewInputRouter(sess.PtyWriter())
if err != nil {
return fmt.Errorf("failed to create input router: %w", err)
}
promptReader, promptWriter := io.Pipe()
defer func() {
promptWriter.Close()
promptReader.Close()
}()
// Note: This goroutine cannot be cleanly cancelled because os.Stdin.Read() is
// a blocking syscall that doesn't support timeouts or cancellation. This is a
// known limitation. The goroutine will exit when the process terminates, which
// is acceptable for a CLI tool. For long-running servers, stdin reading should
// be handled differently.
go inputRouter.ReadLoop(os.Stdin)
go interceptors.HandleConfirmationRequests(
confirmationChan,
interaction,
&interceptors.ConfirmationHook{
BeforeInteraction: func(_ []*analyzer.PackageVersionAnalysisResult) error {
// Pause printing the child output
outputRouter.Pause()
// Restore "Cooked" mode so user can type normally with echo
if err := sess.SetCookedMode(); err != nil {
return fmt.Errorf("failed to set cooked mode: %w", err)
}
// Force cursor visible (ANSI escape sequence)
fmt.Fprint(os.Stdout, "\033[?25h")
// Switch Input: Route keystrokes to the Prompt Pipe
inputRouter.RouteToPrompt(promptWriter)
// Inject the Reader into the Interaction for the confirmation prompt
interaction.SetInput(promptReader)
return nil
},
AfterInteraction: func(_ []*analyzer.PackageVersionAnalysisResult, _ bool) error {
// Switch input back to PTY
inputRouter.RouteToPTY()
// Restore "Raw" mode for the PTY
if err := sess.SetRawMode(); err != nil {
return fmt.Errorf("failed to set raw mode: %w", err)
}
// Clear the interaction input (back to default)
interaction.SetInput(nil)
// Flush buffered output and resume live output
outputRouter.Resume()
return nil
},
},
)
err = sess.Wait()
// Wait for the routers to copy all the remaining data
wg.Wait()
if err != nil {
var exitErr *pty.ExitError
if errors.As(err, &exitErr) {
// Close writer and reader
promptWriter.Close()
promptReader.Close()
// Close the session
sess.Close()
os.Exit(exitErr.Code)
}
return err
}
return nil
}
+115
View File
@@ -0,0 +1,115 @@
package pty
import (
"bytes"
"io"
"sync"
"sync/atomic"
)
// OutputRouter manages buffered vs live output.
type OutputRouter struct {
mu sync.Mutex
stdout io.Writer
buffer bytes.Buffer
buffering bool
}
func NewOutputRouter(out io.Writer) (*OutputRouter, error) {
return &OutputRouter{
stdout: out,
}, nil
}
func (r *OutputRouter) Write(p []byte) (n int, err error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.buffering {
// We are in "Prompt Mode", so save this output for later.
// If we printed it now, it would mess up the confirmation prompt.
return r.buffer.Write(p)
}
// Normal mode: just print it to stdout.
return r.stdout.Write(p)
}
// Pause starts buffering output. Call this before showing a confirmation prompt.
func (r *OutputRouter) Pause() {
r.mu.Lock()
defer r.mu.Unlock()
r.buffering = true
}
// Resume stops buffering, flushes any buffered output, and resumes live output.
// Call this after the confirmation prompt is complete.
func (r *OutputRouter) Resume() {
r.mu.Lock()
defer r.mu.Unlock()
// Flush any buffered output
if r.buffer.Len() > 0 {
_, _ = io.Copy(r.stdout, &r.buffer)
r.buffer.Reset()
}
r.buffering = false
}
// writerDest wraps io.Writer for use with atomic.Pointer
// (atomic.Value panics on nil interface stores)
type writerDest struct {
w io.Writer
}
// InputRouter manages routing stdin to either PTY or a prompt pipe.
// Only ONE goroutine should call ReadLoop().
type InputRouter struct {
dest atomic.Pointer[writerDest]
defaultDst io.Writer // PTY writer
}
func NewInputRouter(ptyWriter io.Writer) (*InputRouter, error) {
return &InputRouter{
defaultDst: ptyWriter,
}, nil
}
// ReadLoop continuously reads from src and routes data to the current destination.
//
// IMPORTANT: Only ONE goroutine should call ReadLoop() because:
// 1. Multiple readers on the same source (e.g., stdin) cause data splitting -
// one goroutine might read "hel" while another reads "lo\n"
// 2. Concurrent routing decisions create race conditions on the destination
// 3. User input becomes unpredictably interleaved between readers
//
// This function blocks until src returns an error (e.g., EOF).
func (r *InputRouter) ReadLoop(src io.Reader) {
buf := make([]byte, 1024)
for {
nr, err := src.Read(buf)
if err != nil {
return
}
// Check where to route the data
if dest := r.dest.Load(); dest != nil {
// Send confirmation prompt response to the pipe. (PMG)
_, _ = dest.w.Write(buf[:nr])
} else {
// Send response to the child PTY.
_, _ = r.defaultDst.Write(buf[:nr])
}
}
}
// RouteToPrompt switches input to go to the given writer (prompt pipe)
func (r *InputRouter) RouteToPrompt(w io.Writer) {
r.dest.Store(&writerDest{w: w})
}
// RouteToPTY switches input back to the PTY (default)
func (r *InputRouter) RouteToPTY() {
r.dest.Store(nil)
}
+175
View File
@@ -0,0 +1,175 @@
package pty
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/KennethanCeyer/ptyx"
"golang.org/x/term"
)
// InteractiveSession manages a PTY-based command execution with
// support for input/output routing and terminal mode switching.
type InteractiveSession interface {
// PtyWriter returns the writer to send input to the child process
PtyWriter() io.Writer
// PtyReader returns the reader to receive output from the child process
PtyReader() io.Reader
// SetRawMode puts terminal in raw mode (for PTY passthrough)
SetRawMode() error
// SetCookedMode restores normal terminal mode (for prompts)
SetCookedMode() error
// Wait blocks until the child process exits
// Returns ExitError if process exited with non-zero code
Wait() error
// Close cleans up resources (PTY, terminal state)
Close() error
}
// IsInteractiveTerminal returns true if stdin is a real terminal (TTY).
// Returns false in CI environments (when the "CI" env var set to "true"),
// when input is piped, or in non-interactive shells.
func IsInteractiveTerminal() bool {
if ci := os.Getenv("CI"); ci != "" && strings.ToLower(ci) == "true" {
return false
}
return term.IsTerminal(int(os.Stdin.Fd()))
}
var _ InteractiveSession = &session{}
type session struct {
console ptyx.Console
spawn ptyx.Session
oldState ptyx.RawState // Saved terminal state for restoration
}
// SessionConfig holds options for creating a session
type SessionConfig struct {
Command string
Args []string
Env []string
}
func NewSessionConfig(cmd string, args, env []string) SessionConfig {
return SessionConfig{
Command: cmd,
Args: args,
Env: env,
}
}
// NewSession creates a new interactive PTY session.
// The terminal is put into raw mode automatically.
func NewSession(ctx context.Context, cfg SessionConfig) (InteractiveSession, error) {
if cfg.Command == "" {
return nil, fmt.Errorf("pty session requires command")
}
// 1. Create console
c, err := ptyx.NewConsole()
if err != nil {
return nil, fmt.Errorf("failed to create console: %w", err)
}
c.EnableVT()
// 2. Set raw mode, save old state
oldState, err := c.MakeRaw()
if err != nil {
c.Close()
return nil, fmt.Errorf("failed to set raw mode: %w", err)
}
// 3. Get terminal size
cols, rows := c.Size()
// 4. Spawn the process
s, err := ptyx.Spawn(ctx, ptyx.SpawnOpts{
Prog: cfg.Command,
Args: cfg.Args,
Cols: cols,
Rows: rows,
Env: cfg.Env,
})
if err != nil {
c.Restore(oldState)
c.Close()
return nil, fmt.Errorf("failed to spawn: %w", err)
}
return &session{
console: c,
spawn: s,
oldState: oldState,
}, nil
}
func (s *session) PtyWriter() io.Writer { return s.spawn.PtyWriter() }
func (s *session) PtyReader() io.Reader { return s.spawn.PtyReader() }
func (s *session) SetRawMode() error {
_, err := s.console.MakeRaw()
return err
}
func (s *session) SetCookedMode() error {
return s.console.Restore(s.oldState)
}
func (s *session) Wait() error {
err := s.spawn.Wait()
if err != nil {
if exitErr, ok := err.(*ptyx.ExitError); ok {
return &ExitError{Code: exitErr.ExitCode, Err: err}
}
return &ExitError{Code: -1, Err: err}
}
return nil
}
func (s *session) Close() error {
// Always restore terminal state
if s.oldState != nil {
_ = s.console.Restore(s.oldState)
}
if s.spawn != nil {
_ = s.spawn.Close()
}
if s.console != nil {
_ = s.console.Close()
}
return nil
}
// ExitError is returned when the child process exits with non-zero code
type ExitError struct {
Code int
Err error // Underlying error from ptyx
}
func (e *ExitError) Error() string {
if e.Code != 0 {
return fmt.Sprintf("process exited with code %d", e.Code)
}
if e.Err != nil {
return e.Err.Error()
}
return "unknown process error"
}
// Unwrap allows errors.Is and errors.As to work
func (e *ExitError) Unwrap() error {
return e.Err
}
+21 -11
View File
@@ -1,7 +1,9 @@
package ui package ui
import ( import (
"bufio"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
@@ -76,7 +78,15 @@ func SetStatus(status string) {
StartSpinnerWithColor(fmt.Sprintf("️ %s", status), Colors.Green) StartSpinnerWithColor(fmt.Sprintf("️ %s", status), Colors.Green)
} }
// GetConfirmationOnMalware prompts the user to confirm installation of suspicious packages.
// It reads from os.Stdin. Use GetConfirmationOnMalwareWithReader for custom input sources.
func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) { func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
return GetConfirmationOnMalwareWithReader(malwarePackages, os.Stdin)
}
// GetConfirmationOnMalwareWithReader prompts the user to confirm installation of suspicious packages.
// It reads from the provided reader, allowing for PTY input routing during proxy mode.
func GetConfirmationOnMalwareWithReader(malwarePackages []*analyzer.PackageVersionAnalysisResult, reader io.Reader) (bool, error) {
StopSpinner() StopSpinner()
fmt.Println() fmt.Println()
@@ -87,19 +97,19 @@ func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysis
fmt.Println() fmt.Println()
fmt.Print(Colors.Yellow("Do you want to continue with the installation? (y/N) ")) fmt.Print(Colors.Yellow("Do you want to continue with the installation? (y/N) "))
var response string // Use Scanner on the provided reader to support PTY input routing
scanner := bufio.NewScanner(reader)
// We don't care about the error here because we will return false if scanner.Scan() {
// if the user doesn't provide a valid response response := strings.ToLower(strings.TrimSpace(scanner.Text()))
_, _ = fmt.Scanln(&response) if response == "y" || response == "yes" || (len(response) > 0 && response[0] == 'y') {
return true, nil
if len(response) == 0 { }
return false, nil
} }
response = strings.ToLower(response) // Check for scanner errors, but don't treat them as fatal
if response == "y" || response == "yes" || response[0] == 'y' { if err := scanner.Err(); err != nil {
return true, nil // On EOF or interrupted read, just return false (deny)
return false, nil
} }
return false, nil return false, nil
+3
View File
@@ -5,6 +5,7 @@ import (
"os" "os"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/cmd/executors"
"github.com/safedep/pmg/cmd/npm" "github.com/safedep/pmg/cmd/npm"
"github.com/safedep/pmg/cmd/pypi" "github.com/safedep/pmg/cmd/pypi"
"github.com/safedep/pmg/cmd/setup" "github.com/safedep/pmg/cmd/setup"
@@ -94,6 +95,8 @@ func main() {
cmd.AddCommand(npm.NewPnpmCommand()) cmd.AddCommand(npm.NewPnpmCommand())
cmd.AddCommand(npm.NewBunCommand()) cmd.AddCommand(npm.NewBunCommand())
cmd.AddCommand(npm.NewYarnCommand()) cmd.AddCommand(npm.NewYarnCommand())
cmd.AddCommand(executors.NewNpxCommand())
cmd.AddCommand(executors.NewPnpxCommand())
cmd.AddCommand(pypi.NewPipCommand()) cmd.AddCommand(pypi.NewPipCommand())
cmd.AddCommand(pypi.NewPip3Command()) cmd.AddCommand(pypi.NewPip3Command())
cmd.AddCommand(pypi.NewUvCommand()) cmd.AddCommand(pypi.NewUvCommand())
+123
View File
@@ -0,0 +1,123 @@
package packagemanager
import (
"io"
"slices"
"strings"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/spf13/pflag"
)
type NpmPackageExecutorConfig struct {
CommandName string
}
func DefaultNpxPackageExecutorConfig() NpmPackageExecutorConfig {
return NpmPackageExecutorConfig{
CommandName: "npx",
}
}
func DefaultPnpxPackageExecutorConfig() NpmPackageExecutorConfig {
return NpmPackageExecutorConfig{
CommandName: "pnpx",
}
}
type npmPackageExecutor struct {
Config NpmPackageExecutorConfig
}
func NewNpmPackageExecutor(config NpmPackageExecutorConfig) (*npmPackageExecutor, error) {
return &npmPackageExecutor{
Config: config,
}, nil
}
var _ PackageManager = &npmPackageExecutor{}
func (n *npmPackageExecutor) Name() string {
return n.Config.CommandName
}
func (n *npmPackageExecutor) Ecosystem() packagev1.Ecosystem {
return packagev1.Ecosystem_ECOSYSTEM_NPM
}
func (n *npmPackageExecutor) ParseCommand(args []string) (*ParsedCommand, error) {
if len(args) > 0 && (args[0] == "npx" || args[0] == "pnpx") {
args = args[1:]
}
command := Command{Exe: n.Config.CommandName, Args: args}
if len(args) < 1 {
return &ParsedCommand{
Command: command,
}, nil
}
flagSet := pflag.NewFlagSet(n.Config.CommandName, pflag.ContinueOnError)
flagSet.SetOutput(io.Discard)
flagSet.ParseErrorsAllowlist.UnknownFlags = true
var packages []string
switch n.Config.CommandName {
case "npx":
flagSet.StringArrayVarP(&packages, "package", "p", []string{}, "Package List")
case "pnpx":
flagSet.StringArrayVar(&packages, "package", []string{}, "Package List")
}
err := flagSet.Parse(args)
if err != nil {
return &ParsedCommand{Command: command}, nil
}
for _, arg := range flagSet.Args() {
// Append the scoped package
if strings.HasPrefix(arg, "@") && !slices.Contains(packages, arg) {
packages = append(packages, arg)
}
}
// Unlike npx, pnpx does not separate package and binary;
// the first arg is always an install target.
if n.Config.CommandName == "pnpx" && len(flagSet.Args()) > 0 {
pkg := flagSet.Args()[0]
if !slices.Contains(packages, pkg) {
packages = append(packages, pkg)
}
}
var installTargets []*PackageInstallTarget
for _, pkg := range packages {
packageName, version, err := npmParsePackageInfo(pkg)
if err != nil {
return nil, ErrFailedToParsePackage.Wrap(err)
}
if version != "" {
version = npmCleanVersion(version)
}
installTarget := &PackageInstallTarget{
PackageVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
Name: packageName,
},
Version: version,
},
}
installTargets = append(installTargets, installTarget)
}
return &ParsedCommand{
Command: command,
InstallTargets: installTargets,
}, nil
}
+211
View File
@@ -0,0 +1,211 @@
package packagemanager
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNpxExecutorParseCommand(t *testing.T) {
cases := []struct {
name string
command string
assert func(t *testing.T, parsed *ParsedCommand, err error)
}{
{
name: "bare npx invocation",
command: "npx",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.NotNil(t, parsed)
assert.Equal(t, 0, len(parsed.InstallTargets))
},
},
{
name: "scoped package via -p flag",
command: "npx -p @types/node",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 1, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "scoped package with version",
command: "npx @types/node@1.2.3",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 1, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Equal(t, "1.2.3", parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "non-package npx command",
command: "npx create-react-app my-app",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 0, len(parsed.InstallTargets))
assert.Equal(t, []string{"create-react-app", "my-app"}, parsed.Command.Args)
},
},
{
name: "single package using -p flag npx command with binary",
command: "npx -p tsx my-app",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 1, len(parsed.InstallTargets))
assert.Equal(t, "tsx", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "single package using --package flag npx command with binary",
command: "npx --package=tsx my-app",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 1, len(parsed.InstallTargets))
assert.Equal(t, "tsx", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "multiple scoped packages via flags and args",
command: "npx -p @types/node @react@2.0.0",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 2, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
assert.Equal(t, "@react", parsed.InstallTargets[1].PackageVersion.Package.Name)
assert.Equal(t, "2.0.0", parsed.InstallTargets[1].PackageVersion.Version)
},
},
{
name: "multiple packages via flags and scoped",
command: "npx @types/node -p react@2.0.0",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 2, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[1].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[1].PackageVersion.Version)
assert.Equal(t, "react", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Equal(t, "2.0.0", parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "multiple packages via flags and args",
command: "npx -p node -p react@2.0.0",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 2, len(parsed.InstallTargets))
assert.Equal(t, "node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
assert.Equal(t, "react", parsed.InstallTargets[1].PackageVersion.Package.Name)
assert.Equal(t, "2.0.0", parsed.InstallTargets[1].PackageVersion.Version)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
exec := &npmPackageExecutor{Config: DefaultNpxPackageExecutorConfig()}
parsed, err := exec.ParseCommand(strings.Split(tc.command, " "))
tc.assert(t, parsed, err)
})
}
}
func TestPnpxExecutorParseCommand(t *testing.T) {
cases := []struct {
name string
command string
assert func(t *testing.T, parsed *ParsedCommand, err error)
}{
{
name: "bare pnpx invocation",
command: "pnpx",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.NotNil(t, parsed)
assert.Equal(t, 0, len(parsed.InstallTargets))
},
},
{
name: "scoped package via package flag",
command: "pnpx --package=@types/node",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 1, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "scoped package with version",
command: "pnpx @types/node@1.2.3",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 1, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Equal(t, "1.2.3", parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "single package command",
command: "pnpx tsx my-app",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 1, len(parsed.InstallTargets))
assert.Equal(t, "tsx", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "multiple scoped packages via flags and args",
command: "pnpx --package @types/node @react@2.0.0",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 2, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
assert.Equal(t, "@react", parsed.InstallTargets[1].PackageVersion.Package.Name)
assert.Equal(t, "2.0.0", parsed.InstallTargets[1].PackageVersion.Version)
},
},
{
name: "multiple packages via flags and scoped",
command: "pnpx @types/node --package react@2.0.0",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 2, len(parsed.InstallTargets))
assert.Equal(t, "@types/node", parsed.InstallTargets[1].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[1].PackageVersion.Version)
assert.Equal(t, "react", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Equal(t, "2.0.0", parsed.InstallTargets[0].PackageVersion.Version)
},
},
{
name: "multiple packages via flags and args",
command: "pnpx --package node --package react@2.0.0",
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
assert.NoError(t, err)
assert.Equal(t, 2, len(parsed.InstallTargets))
assert.Equal(t, "node", parsed.InstallTargets[0].PackageVersion.Package.Name)
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
assert.Equal(t, "react", parsed.InstallTargets[1].PackageVersion.Package.Name)
assert.Equal(t, "2.0.0", parsed.InstallTargets[1].PackageVersion.Version)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
exec := &npmPackageExecutor{Config: DefaultPnpxPackageExecutorConfig()}
parsed, err := exec.ParseCommand(strings.Split(tc.command, " "))
tc.assert(t, parsed, err)
})
}
}
+3 -3
View File
@@ -132,7 +132,7 @@ func (p *pipCommandParser) ParseCommand(args []string) (*ParsedCommand, error) {
flagSet := pflag.NewFlagSet(p.config.CommandName, pflag.ContinueOnError) flagSet := pflag.NewFlagSet(p.config.CommandName, pflag.ContinueOnError)
flagSet.SetOutput(io.Discard) flagSet.SetOutput(io.Discard)
flagSet.ParseErrorsWhitelist.UnknownFlags = true flagSet.ParseErrorsAllowlist.UnknownFlags = true
// Define flags // Define flags
var requirementFiles []string var requirementFiles []string
@@ -252,7 +252,7 @@ func (u *uvCommandParser) ParseCommand(args []string) (*ParsedCommand, error) {
// Set up flag parsing // Set up flag parsing
flagSet := pflag.NewFlagSet("uv", pflag.ContinueOnError) flagSet := pflag.NewFlagSet("uv", pflag.ContinueOnError)
flagSet.SetOutput(io.Discard) flagSet.SetOutput(io.Discard)
flagSet.ParseErrorsWhitelist.UnknownFlags = true flagSet.ParseErrorsAllowlist.UnknownFlags = true
var manifestFiles []string var manifestFiles []string
@@ -349,7 +349,7 @@ func (p *poetryCommandParser) ParseCommand(args []string) (*ParsedCommand, error
// Set up flag parsing // Set up flag parsing
flagSet := pflag.NewFlagSet("poetry", pflag.ContinueOnError) flagSet := pflag.NewFlagSet("poetry", pflag.ContinueOnError)
flagSet.ParseErrorsWhitelist.UnknownFlags = true flagSet.ParseErrorsAllowlist.UnknownFlags = true
flagSet.SetOutput(io.Discard) flagSet.SetOutput(io.Discard)
err := flagSet.Parse(installArgs) err := flagSet.Parse(installArgs)
+1 -1
View File
@@ -31,7 +31,7 @@ type ConfirmationHook struct {
// //
// The function will exit when the confirmation channel is closed. // The function will exit when the confirmation channel is closed.
func HandleConfirmationRequests(confirmationChan chan *ConfirmationRequest, func HandleConfirmationRequests(confirmationChan chan *ConfirmationRequest,
interaction guard.PackageManagerGuardInteraction, hooks *ConfirmationHook) { interaction *guard.PackageManagerGuardInteraction, hooks *ConfirmationHook) {
if hooks == nil { if hooks == nil {
hooks = &ConfirmationHook{} hooks = &ConfirmationHook{}
} }
+2 -2
View File
@@ -118,7 +118,7 @@ func TestHandleConfirmationRequests(t *testing.T) {
} }
confirmationChan := make(chan *ConfirmationRequest, 1) confirmationChan := make(chan *ConfirmationRequest, 1)
go HandleConfirmationRequests(confirmationChan, interaction, hooks) go HandleConfirmationRequests(confirmationChan, &interaction, hooks)
pkgVersion := mockPackageVersion("test-package", "1.0.0") pkgVersion := mockPackageVersion("test-package", "1.0.0")
analysisResult := mockAnalysisResult() analysisResult := mockAnalysisResult()
@@ -159,7 +159,7 @@ func TestHandleConfirmationRequests_MultipleSequential(t *testing.T) {
} }
confirmationChan := make(chan *ConfirmationRequest, 3) confirmationChan := make(chan *ConfirmationRequest, 3)
go HandleConfirmationRequests(confirmationChan, interaction, nil) go HandleConfirmationRequests(confirmationChan, &interaction, nil)
pkgVersion1 := mockPackageVersion("package-1", "1.0.0") pkgVersion1 := mockPackageVersion("package-1", "1.0.0")
analysisResult1 := mockAnalysisResult() analysisResult1 := mockAnalysisResult()
-5
View File
@@ -5,7 +5,6 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/guard"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
) )
@@ -14,7 +13,6 @@ type InterceptorFactory struct {
analyzer analyzer.PackageVersionAnalyzer analyzer analyzer.PackageVersionAnalyzer
cache AnalysisCache cache AnalysisCache
confirmationChan chan *ConfirmationRequest confirmationChan chan *ConfirmationRequest
interaction guard.PackageManagerGuardInteraction
} }
// NewInterceptorFactory creates a new interceptor factory with shared dependencies // NewInterceptorFactory creates a new interceptor factory with shared dependencies
@@ -22,13 +20,11 @@ func NewInterceptorFactory(
analyzer analyzer.PackageVersionAnalyzer, analyzer analyzer.PackageVersionAnalyzer,
cache AnalysisCache, cache AnalysisCache,
confirmationChan chan *ConfirmationRequest, confirmationChan chan *ConfirmationRequest,
interaction guard.PackageManagerGuardInteraction,
) *InterceptorFactory { ) *InterceptorFactory {
return &InterceptorFactory{ return &InterceptorFactory{
analyzer: analyzer, analyzer: analyzer,
cache: cache, cache: cache,
confirmationChan: confirmationChan, confirmationChan: confirmationChan,
interaction: interaction,
} }
} }
@@ -41,7 +37,6 @@ func (f *InterceptorFactory) CreateInterceptor(ecosystem packagev1.Ecosystem) (p
f.analyzer, f.analyzer,
f.cache, f.cache,
f.confirmationChan, f.confirmationChan,
f.interaction,
), nil ), nil
default: default:
-3
View File
@@ -6,7 +6,6 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/guard"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
) )
@@ -30,14 +29,12 @@ func NewNpmRegistryInterceptor(
analyzer analyzer.PackageVersionAnalyzer, analyzer analyzer.PackageVersionAnalyzer,
cache AnalysisCache, cache AnalysisCache,
confirmationChan chan *ConfirmationRequest, confirmationChan chan *ConfirmationRequest,
interaction guard.PackageManagerGuardInteraction,
) *NpmRegistryInterceptor { ) *NpmRegistryInterceptor {
return &NpmRegistryInterceptor{ return &NpmRegistryInterceptor{
baseRegistryInterceptor: baseRegistryInterceptor{ baseRegistryInterceptor: baseRegistryInterceptor{
analyzer: analyzer, analyzer: analyzer,
cache: cache, cache: cache,
confirmationChan: confirmationChan, confirmationChan: confirmationChan,
interaction: interaction,
}, },
} }
} }