mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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:
@@ -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!
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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 |
|
||||||
|
|||||||
@@ -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"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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,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)
|
||||||
}
|
}
|
||||||
|
|||||||
+171
-47
@@ -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
|
||||||
|
interaction := &guard.PackageManagerGuardInteraction{
|
||||||
SetStatus: ui.SetStatus,
|
SetStatus: ui.SetStatus,
|
||||||
ClearStatus: ui.ClearStatus,
|
ClearStatus: ui.ClearStatus,
|
||||||
ShowWarning: ui.ShowWarning,
|
ShowWarning: ui.ShowWarning,
|
||||||
GetConfirmationOnMalware: ui.GetConfirmationOnMalware,
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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:
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user