mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
Merge branch 'main' into feat/experimental-sandbox-support
Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>
This commit is contained in:
@@ -217,6 +217,56 @@ jobs:
|
|||||||
test -d node_modules/lodash
|
test -d node_modules/lodash
|
||||||
cd .. && rm -rf yarn-test
|
cd .. && rm -rf yarn-test
|
||||||
|
|
||||||
|
- name: Test NPX - Package Execution
|
||||||
|
run: |
|
||||||
|
echo "Testing NPX package execution..."
|
||||||
|
mkdir npx-test && cd npx-test
|
||||||
|
|
||||||
|
echo "Testing npx with a simple package..."
|
||||||
|
pmg npx cowsay@1.6.0 "Hello from pmg npx" | tee npx-output.txt
|
||||||
|
|
||||||
|
# Verification: cowsay output contains our message
|
||||||
|
grep -q "Hello from pmg npx" npx-output.txt
|
||||||
|
|
||||||
|
echo "Testing npx with --package flag..."
|
||||||
|
pmg npx --package cowsay@1.6.0 -- cowsay "Hello with package flag" | tee npx-pkg-output.txt
|
||||||
|
|
||||||
|
# Verification: package flag execution produces expected output
|
||||||
|
grep -q "Hello with package flag" npx-pkg-output.txt
|
||||||
|
|
||||||
|
echo "Testing npx dry-run mode..."
|
||||||
|
pmg --dry-run npx cowsay@1.6.0 "This should not execute" | tee npx-dry-output.txt
|
||||||
|
|
||||||
|
# Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^ should not appear)
|
||||||
|
! grep -q '\^__\^' npx-dry-output.txt
|
||||||
|
|
||||||
|
cd .. && rm -rf npx-test
|
||||||
|
|
||||||
|
- name: Test PNPX - Package Execution
|
||||||
|
run: |
|
||||||
|
echo "Testing PNPX package execution..."
|
||||||
|
mkdir pnpx-test && cd pnpx-test
|
||||||
|
|
||||||
|
echo "Testing pnpx with a simple package..."
|
||||||
|
pmg pnpx cowsay@1.6.0 "Hello from pmg pnpx" | tee pnpx-output.txt
|
||||||
|
|
||||||
|
# Verification: cowsay output contains our message
|
||||||
|
grep -q "Hello from pmg pnpx" pnpx-output.txt
|
||||||
|
|
||||||
|
echo "Testing pnpx with --package flag..."
|
||||||
|
pmg pnpx --package cowsay@1.6.0 -- cowsay "Hello with package flag" | tee pnpx-pkg-output.txt
|
||||||
|
|
||||||
|
# Verification: package flag execution produces expected output
|
||||||
|
grep -q "Hello with package flag" pnpx-pkg-output.txt
|
||||||
|
|
||||||
|
echo "Testing pnpx dry-run mode..."
|
||||||
|
pmg --dry-run pnpx cowsay@1.6.0 "This should not execute" | tee pnpx-dry-output.txt
|
||||||
|
|
||||||
|
# Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^ should not appear)
|
||||||
|
! grep -q '\^__\^' pnpx-dry-output.txt
|
||||||
|
|
||||||
|
cd .. && rm -rf pnpx-test
|
||||||
|
|
||||||
- name: Test Pip - Single Package & Manifest
|
- name: Test Pip - Single Package & Manifest
|
||||||
run: |
|
run: |
|
||||||
echo "Testing Pip single package installation..."
|
echo "Testing Pip single package installation..."
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ Set up `pmg` to protect your development environment from malicious packages:
|
|||||||
pmg setup install
|
pmg setup install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Note:** Make sure to restart your terminal or source your shell's config file.
|
||||||
|
|
||||||
Continue using your favorite package manager as usual:
|
Continue using your favorite package manager as usual:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -70,6 +72,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!
|
||||||
|
|
||||||
@@ -108,11 +112,19 @@ Set up PMG to intercept package manager commands:
|
|||||||
pmg setup install
|
pmg setup install
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will:
|
<details>
|
||||||
|
<summary>Custom config directory</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PMG_CONFIG_DIR=/path/to/config pmg setup install
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
The setup command will:
|
||||||
|
|
||||||
- Create a `~/.pmg.rc` file containing package manager aliases
|
- Create a `~/.pmg.rc` file containing package manager aliases
|
||||||
- Automatically add a source line to your shell configuration files
|
- Automatically add a source line to your shell configuration files
|
||||||
- Supports bash, zsh and fish shell
|
- Create a default config file. See [config template](config/config.template.yml)
|
||||||
|
|
||||||
> **Note**: After running `pmg setup install`, restart your terminal or run `source ~/.zshrc` (or your shell's config file) to activate the aliases.
|
> **Note**: After running `pmg setup install`, restart your terminal or run `source ~/.zshrc` (or your shell's config file) to activate the aliases.
|
||||||
|
|
||||||
@@ -209,6 +221,11 @@ pmg npm install <package-name>
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## Advanced
|
||||||
|
|
||||||
|
- [Trusted Packages](docs/trusted-packages.md)
|
||||||
|
- [Experimental Proxy Mode](docs/proxy-mode.md)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
|
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
@@ -239,28 +256,6 @@ from non-PyPI locations.
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Windows support for experimental proxy mode</summary>
|
|
||||||
|
|
||||||
PMG's experimental proxy mode (enabled with `--experimental-proxy-mode` flag) is supported on Windows
|
|
||||||
with the following considerations:
|
|
||||||
|
|
||||||
**Process Suspension**: PMG uses the Windows NT API (NtSuspendProcess/NtResumeProcess) to pause the
|
|
||||||
package manager during security prompts. This prevents console output from being mixed with interactive
|
|
||||||
prompts. While this API is widely used by tools like Process Explorer and is stable, it has some known
|
|
||||||
limitations:
|
|
||||||
|
|
||||||
- **Race condition**: Threads created during suspension are not suspended (very unlikely for brief suspensions)
|
|
||||||
- **Permission requirements**: Requires Windows Vista or later (all currently supported Windows versions)
|
|
||||||
- **Antivirus detection**: Some antivirus software may flag the use of NtSuspendProcess as suspicious behavior,
|
|
||||||
which is a false positive (PMG only uses it to pause its own child processes during user interaction)
|
|
||||||
|
|
||||||
**Fallback Behavior**: If process suspension fails (e.g., due to insufficient permissions), PMG continues
|
|
||||||
with the security prompt but console output from the package manager may be mixed with the prompt text.
|
|
||||||
This doesn't affect security functionality.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Telemetry
|
## Telemetry
|
||||||
|
|
||||||
`pmg` collects anonymous telemetry to help us understand how it is used and
|
`pmg` collects anonymous telemetry to help us understand how it is used and
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -51,5 +51,9 @@ func executeBunFlow(ctx context.Context, args []string) error {
|
|||||||
return fmt.Errorf("failed to create dependency resolver: %w", err)
|
return fmt.Errorf("failed to create dependency resolver: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Config.ExperimentalProxyMode {
|
||||||
|
return flows.ProxyFlow(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,5 +52,9 @@ func executePnpmFlow(ctx context.Context, args []string) error {
|
|||||||
return fmt.Errorf("failed to create dependency resolver: %w", err)
|
return fmt.Errorf("failed to create dependency resolver: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Config.ExperimentalProxyMode {
|
||||||
|
return flows.ProxyFlow(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,5 +51,9 @@ func executeYarnFlow(ctx context.Context, args []string) error {
|
|||||||
return fmt.Errorf("failed to create dependency resolver: %w", err)
|
return fmt.Errorf("failed to create dependency resolver: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Config.ExperimentalProxyMode {
|
||||||
|
return flows.ProxyFlow(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/safedep/pmg/config"
|
||||||
|
"github.com/safedep/pmg/internal/alias"
|
||||||
|
"github.com/safedep/pmg/internal/ui"
|
||||||
|
"github.com/safedep/pmg/internal/version"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewInfoCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "info",
|
||||||
|
Short: "Show information about PMG setup and configuration.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
err := executeSetupInfo()
|
||||||
|
if err != nil {
|
||||||
|
ui.ErrorExit(fmt.Errorf("failed to execute setup info: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeSetupInfo() error {
|
||||||
|
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
|
||||||
|
|
||||||
|
// Configuration section
|
||||||
|
cfg := config.Get()
|
||||||
|
configEntries := make(map[string]string)
|
||||||
|
configEntries["Config File"] = cfg.ConfigFilePath()
|
||||||
|
configEntries["Proxy Mode"] = strconv.FormatBool(cfg.Config.ExperimentalProxyMode)
|
||||||
|
ui.PrintInfoSection("Configuration", configEntries)
|
||||||
|
|
||||||
|
// Shell Integration section
|
||||||
|
aliasCfg := alias.DefaultConfig()
|
||||||
|
rcFileManager, err := alias.NewDefaultRcFileManager(aliasCfg.RcFileName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create alias manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasManager := alias.New(aliasCfg, rcFileManager)
|
||||||
|
isInstalled, err := aliasManager.IsInstalled()
|
||||||
|
if err != nil {
|
||||||
|
isInstalled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
shellEntries := make(map[string]string)
|
||||||
|
shell, err := alias.DetectShell()
|
||||||
|
if err != nil {
|
||||||
|
shell = "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
shellEntries["Detected Shell"] = shell
|
||||||
|
shellEntries["Alias Installed"] = strconv.FormatBool(isInstalled)
|
||||||
|
ui.PrintInfoSection("Shell Integration", shellEntries)
|
||||||
|
|
||||||
|
// Security section
|
||||||
|
securityEntries := make(map[string]string)
|
||||||
|
trustedPackages := cfg.Config.TrustedPackages
|
||||||
|
trustedPackagesCount := len(trustedPackages)
|
||||||
|
|
||||||
|
if trustedPackagesCount > 3 {
|
||||||
|
purls := []string{}
|
||||||
|
for _, p := range trustedPackages[0:3] {
|
||||||
|
purls = append(purls, p.Purl)
|
||||||
|
}
|
||||||
|
|
||||||
|
trustedPackagesValue := fmt.Sprintf("%v ...and %d more", purls, trustedPackagesCount-3)
|
||||||
|
securityEntries["Trusted Packages"] = trustedPackagesValue
|
||||||
|
} else if trustedPackagesCount > 0 {
|
||||||
|
purls := []string{}
|
||||||
|
for _, p := range trustedPackages {
|
||||||
|
purls = append(purls, p.Purl)
|
||||||
|
}
|
||||||
|
|
||||||
|
securityEntries["Trusted Packages"] = fmt.Sprintf("%v", purls)
|
||||||
|
} else {
|
||||||
|
securityEntries["Trusted Packages"] = "None"
|
||||||
|
}
|
||||||
|
|
||||||
|
securityEntries["Event Logging"] = strconv.FormatBool(!cfg.Config.SkipEventLogging)
|
||||||
|
securityEntries["Event Log Directory"] = cfg.EventLogDir()
|
||||||
|
|
||||||
|
ui.PrintInfoSection("Security", securityEntries)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ func NewSetupCommand() *cobra.Command {
|
|||||||
|
|
||||||
setupCmd.AddCommand(NewInstallCommand())
|
setupCmd.AddCommand(NewInstallCommand())
|
||||||
setupCmd.AddCommand(NewRemoveCommand())
|
setupCmd.AddCommand(NewRemoveCommand())
|
||||||
|
setupCmd.AddCommand(NewInfoCommand())
|
||||||
|
|
||||||
return setupCmd
|
return setupCmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Proxy Mode
|
||||||
|
|
||||||
|
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
|
||||||
|
- Runs `npm` and other supported package managers configured to use the proxy
|
||||||
|
- Intercepts package registry requests and analyzes packages as they are downloaded
|
||||||
|
- Blocks malicious packages and allows trusted packages to be installed
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pmg --experimental-proxy-mode npm install lodash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To permanently enable proxy mode, add the following to your `config.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
experimental_proxy_mode: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Package Managers
|
||||||
|
|
||||||
|
| Package Manager | Status |
|
||||||
|
| --------------- | --------- |
|
||||||
|
| `npm` | ✅ Active |
|
||||||
|
| `npx` | ✅ Active |
|
||||||
|
| `pnpm` | ✅ Active |
|
||||||
|
| `pnpx` | ✅ Active |
|
||||||
|
| `bun` | ✅ Active |
|
||||||
|
| `yarn` | ✅ Active |
|
||||||
|
| `pip` | 🕒 Planned |
|
||||||
|
| `uv` | 🕒 Planned |
|
||||||
|
| `poetry` | 🕒 Planned |
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Trusted Packages
|
||||||
|
|
||||||
|
`pmg` allows you to trust a package. Trusted packages are not scanned and always allowed to be installed.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Trusted packages are configured in the `config.yml` file. See [config template](../config/config.template.yml) for the configuration schema.
|
||||||
|
If you don't have a `config.yml` file, you can create one by running `pmg setup install`.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
trusted_packages:
|
||||||
|
- purl: pkg:npm/@safedep/pmg
|
||||||
|
reason: "All versions of PMG are trusted"
|
||||||
|
- purl: pkg:npm/express@4.18.0
|
||||||
|
reason: "Version 4.18.0 of Express is a trusted package"
|
||||||
|
```
|
||||||
@@ -4,9 +4,12 @@ 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
|
||||||
@@ -19,9 +22,9 @@ require (
|
|||||||
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
|
||||||
golang.org/x/sys v0.34.0
|
go.yaml.in/yaml/v3 v3.0.4
|
||||||
|
golang.org/x/term v0.34.0
|
||||||
google.golang.org/grpc v1.72.0
|
google.golang.org/grpc v1.72.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -207,13 +210,12 @@ 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/term v0.33.0 // indirect
|
golang.org/x/sys v0.35.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
|
||||||
|
|||||||
@@ -392,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=
|
||||||
@@ -619,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=
|
||||||
@@ -629,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"
|
||||||
@@ -37,6 +38,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 {
|
||||||
|
|||||||
+29
-1
@@ -95,7 +95,7 @@ func DefaultConfig() AliasConfig {
|
|||||||
|
|
||||||
return AliasConfig{
|
return AliasConfig{
|
||||||
RcFileName: ".pmg.rc",
|
RcFileName: ".pmg.rc",
|
||||||
PackageManagers: []string{"npm", "pip", "pip3", "pnpm", "bun", "uv", "yarn", "poetry"},
|
PackageManagers: []string{"npm", "pip", "pip3", "pnpm", "bun", "uv", "yarn", "poetry", "npx", "pnpx"},
|
||||||
Shells: shells,
|
Shells: shells,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,6 +142,34 @@ func (a *AliasManager) Remove() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsInstalled checks if the PMG aliases are sourced in any of the shell config files.
|
||||||
|
func (a *AliasManager) IsInstalled() (bool, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, shell := range a.config.Shells {
|
||||||
|
configPath := filepath.Join(homeDir, shell.Path())
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Warning: could not read %s (%s)", shell.Name(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(string(data), a.config.RcFileName) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildAliases creates the alias strings for all configured package managers.
|
// buildAliases creates the alias strings for all configured package managers.
|
||||||
func (a *AliasManager) buildAliases() []string {
|
func (a *AliasManager) buildAliases() []string {
|
||||||
aliases := make([]string, 0, len(a.config.PackageManagers))
|
aliases := make([]string, 0, len(a.config.PackageManagers))
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package alias
|
package alias
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Shell interface {
|
type Shell interface {
|
||||||
Source(rcPath string) string
|
Source(rcPath string) string
|
||||||
@@ -13,3 +17,16 @@ var commentForRemovingShellSource = "# remove aliases by running `pmg setup remo
|
|||||||
func defaultShellSource(rcPath string) string {
|
func defaultShellSource(rcPath string) string {
|
||||||
return fmt.Sprintf("%s \n[ -f '%s' ] && source '%s' # PMG source aliases\n", commentForRemovingShellSource, rcPath, rcPath)
|
return fmt.Sprintf("%s \n[ -f '%s' ] && source '%s' # PMG source aliases\n", commentForRemovingShellSource, rcPath, rcPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DetectShell attempts to detect the current shell from the SHELL environment variable.
|
||||||
|
func DetectShell() (string, error) {
|
||||||
|
shellEnv := os.Getenv("SHELL")
|
||||||
|
if shellEnv == "" {
|
||||||
|
return "", fmt.Errorf("SHELL environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(shellEnv, "/")
|
||||||
|
shellName := parts[len(parts)-1]
|
||||||
|
|
||||||
|
return shellName, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package alias
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetectShell(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
shellEnvValue string
|
||||||
|
want string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bash full path",
|
||||||
|
shellEnvValue: "/bin/bash",
|
||||||
|
want: "bash",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zsh full path",
|
||||||
|
shellEnvValue: "/bin/zsh",
|
||||||
|
want: "zsh",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bash only name",
|
||||||
|
shellEnvValue: "bash",
|
||||||
|
want: "bash",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when shell env is not set",
|
||||||
|
shellEnvValue: "",
|
||||||
|
want: "",
|
||||||
|
wantErr: fmt.Errorf("SHELL environment variable not set"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Setenv("SHELL", tc.shellEnvValue)
|
||||||
|
got, err := DetectShell()
|
||||||
|
|
||||||
|
if tc.wantErr != nil {
|
||||||
|
assert.ErrorContains(t, err, tc.wantErr.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package flows
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func platformPauseProcess(cmd *exec.Cmd) error {
|
|
||||||
if cmd == nil || cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Process.Signal(syscall.SIGSTOP); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func platformResumeProcess(cmd *exec.Cmd) error {
|
|
||||||
if cmd == nil || cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Process.Signal(syscall.SIGCONT); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package flows
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
modntdll = windows.NewLazySystemDLL("ntdll.dll")
|
|
||||||
procNtSuspendProcess = modntdll.NewProc("NtSuspendProcess")
|
|
||||||
procNtResumeProcess = modntdll.NewProc("NtResumeProcess")
|
|
||||||
)
|
|
||||||
|
|
||||||
// platformPauseProcess suspends the process using Windows NT API.
|
|
||||||
//
|
|
||||||
// Known limitations:
|
|
||||||
// - Race condition: threads created during suspension are not suspended
|
|
||||||
// - Remote thread injection still possible (very rare)
|
|
||||||
//
|
|
||||||
// For PMG's use case (brief suspension during user prompts), these limitations
|
|
||||||
// are acceptable.
|
|
||||||
//
|
|
||||||
// References:
|
|
||||||
// - gopsutil: https://github.com/shirou/gopsutil/blob/master/process/process_windows.go
|
|
||||||
// - Analysis: https://github.com/diversenok/Suspending-Techniques
|
|
||||||
func platformPauseProcess(cmd *exec.Cmd) error {
|
|
||||||
if cmd == nil || cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
handle, err := windows.OpenProcess(
|
|
||||||
windows.PROCESS_SUSPEND_RESUME,
|
|
||||||
false,
|
|
||||||
uint32(cmd.Process.Pid),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open process for suspension: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer windows.CloseHandle(handle)
|
|
||||||
|
|
||||||
r1, _, _ := procNtSuspendProcess.Call(uintptr(handle))
|
|
||||||
if r1 != 0 {
|
|
||||||
return fmt.Errorf("NtSuspendProcess failed with NTSTATUS=0x%.8X", r1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// platformResumeProcess resumes a suspended process using Windows NT API.
|
|
||||||
func platformResumeProcess(cmd *exec.Cmd) error {
|
|
||||||
if cmd == nil || cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
handle, err := windows.OpenProcess(
|
|
||||||
windows.PROCESS_SUSPEND_RESUME,
|
|
||||||
false,
|
|
||||||
uint32(cmd.Process.Pid),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open process for resumption: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer windows.CloseHandle(handle)
|
|
||||||
|
|
||||||
r1, _, _ := procNtResumeProcess.Call(uintptr(handle))
|
|
||||||
if r1 != 0 {
|
|
||||||
return fmt.Errorf("NtResumeProcess failed with NTSTATUS=0x%.8X", r1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package flows
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPlatformPauseResumeProcess(t *testing.T) {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Windows-only test")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("ping", "127.0.0.1", "-n", "60")
|
|
||||||
err := cmd.Start()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
defer cmd.Process.Kill()
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
err = platformPauseProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
err = platformResumeProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlatformPauseProcessNil(t *testing.T) {
|
|
||||||
err := platformPauseProcess(nil)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlatformResumeProcessNil(t *testing.T) {
|
|
||||||
err := platformResumeProcess(nil)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlatformPauseProcessNilProcess(t *testing.T) {
|
|
||||||
cmd := exec.Command("ping", "127.0.0.1")
|
|
||||||
err := platformPauseProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlatformResumeProcessNilProcess(t *testing.T) {
|
|
||||||
cmd := exec.Command("ping", "127.0.0.1")
|
|
||||||
|
|
||||||
err := platformResumeProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlatformPauseProcessExited(t *testing.T) {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Windows-only test")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("cmd", "/c", "exit 0")
|
|
||||||
err := cmd.Start()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
defer cmd.Process.Kill()
|
|
||||||
|
|
||||||
err = platformPauseProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlatformResumeProcessExited(t *testing.T) {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Windows-only test")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("cmd", "/c", "exit 0")
|
|
||||||
err := cmd.Start()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
defer cmd.Process.Kill()
|
|
||||||
|
|
||||||
err = platformResumeProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlatformPauseResumeMultipleTimes(t *testing.T) {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Windows-only test")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("ping", "127.0.0.1", "-n", "60")
|
|
||||||
err := cmd.Start()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
defer cmd.Process.Kill()
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
err = platformPauseProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
err = platformResumeProcess(cmd)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+175
-59
@@ -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"
|
||||||
@@ -35,6 +39,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
|
||||||
@@ -83,24 +96,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)
|
||||||
@@ -128,8 +133,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
|
||||||
@@ -212,21 +224,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),
|
||||||
@@ -238,40 +240,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
|
||||||
|
|
||||||
pmName := f.pm.Name()
|
go interceptors.HandleConfirmationRequests(
|
||||||
result, err := executor.ApplySandbox(ctx, cmd, pmName)
|
confirmationChan,
|
||||||
if err != nil {
|
interaction,
|
||||||
return fmt.Errorf("failed to apply sandbox: %w", err)
|
nil,
|
||||||
}
|
)
|
||||||
|
|
||||||
defer result.Close()
|
|
||||||
|
|
||||||
log.Debugf("Executing command: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args)
|
|
||||||
log.Debugf("Proxy environment: HTTP_PROXY=%s, HTTPS_PROXY=%s, NODE_EXTRA_CA_CERTS=%s", proxyURL, proxyURL, caCertPath)
|
|
||||||
|
|
||||||
// Start confirmation handler in goroutine. Use confirmation hooks to pause and resume the executed
|
|
||||||
// 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
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Only run the command if the sandbox didn't already execute it
|
// Only run the command if the sandbox didn't already execute it
|
||||||
if result.ShouldRun() {
|
if result.ShouldRun() {
|
||||||
@@ -288,3 +285,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
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
brandPinkRed = color.RGB(219, 39, 119).Add(color.Bold).SprintFunc() // #DB2777 Brand Pink
|
brandPinkRed = color.RGB(219, 39, 119).Add(color.Bold).SprintFunc() // #DB2777 Brand Pink
|
||||||
whiteDim = color.New(color.Faint).SprintFunc()
|
whiteDim = color.New(color.Faint).SprintFunc()
|
||||||
whiteBold = color.New(color.Bold).SprintFunc()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GeneratePMGBanner(version, commit string) string {
|
func GeneratePMGBanner(version, commit string) string {
|
||||||
@@ -21,8 +20,8 @@ func GeneratePMGBanner(version, commit string) string {
|
|||||||
commit = commit[:6]
|
commit = commit[:6]
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s \t%s: %s %s: %s\n\n", brandPinkRed(pmgASCIIText),
|
return fmt.Sprintf("%s %s: %s %s: %s\n\n", brandPinkRed(pmgASCIIText),
|
||||||
whiteDim("version"), whiteBold(version),
|
whiteDim("version"), Colors.Bold(version),
|
||||||
whiteDim("commit"), whiteBold(commit),
|
whiteDim("commit"), Colors.Bold(commit),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type TerminalColors struct {
|
|||||||
Yellow ColorFn
|
Yellow ColorFn
|
||||||
Cyan ColorFn
|
Cyan ColorFn
|
||||||
Green ColorFn
|
Green ColorFn
|
||||||
|
Bold ColorFn
|
||||||
}
|
}
|
||||||
|
|
||||||
var Colors = TerminalColors{
|
var Colors = TerminalColors{
|
||||||
@@ -18,4 +19,5 @@ var Colors = TerminalColors{
|
|||||||
Yellow: color.New(color.FgYellow).SprintfFunc(),
|
Yellow: color.New(color.FgYellow).SprintfFunc(),
|
||||||
Cyan: color.New(color.FgCyan).SprintfFunc(),
|
Cyan: color.New(color.FgCyan).SprintfFunc(),
|
||||||
Green: color.New(color.FgGreen).SprintfFunc(),
|
Green: color.New(color.FgGreen).SprintfFunc(),
|
||||||
|
Bold: color.New(color.Bold).SprintfFunc(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintInfoSection prints a formatted block of key-value information.
|
||||||
|
func PrintInfoSection(title string, entries map[string]string) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(Colors.Cyan(title))
|
||||||
|
fmt.Println(Colors.Normal("--------------------"))
|
||||||
|
|
||||||
|
// Sort keys for consistent output
|
||||||
|
keys := make([]string, 0, len(entries))
|
||||||
|
for k := range entries {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
fmt.Printf("%-25s: %s\n", Colors.Bold(k), entries[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
+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,125 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For both npx and pnpx, the first positional argument is typically
|
||||||
|
// the package to execute (e.g., `npx cowsay@1.6.0` or `pnpx cowsay@1.6.0`).
|
||||||
|
// However, if -p/--package flags are provided, the first positional arg
|
||||||
|
// is the binary to run, not the package (e.g., `npx -p typescript tsc`).
|
||||||
|
if len(flagSet.Args()) > 0 && len(packages) == 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,234 @@
|
|||||||
|
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: "package as first positional arg",
|
||||||
|
command: "npx create-react-app 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, "create-react-app", parsed.InstallTargets[0].PackageVersion.Package.Name)
|
||||||
|
assert.Equal(t, []string{"create-react-app", "my-app"}, parsed.Command.Args)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package with version as first positional arg",
|
||||||
|
command: "npx cowsay@1.6.0 hello",
|
||||||
|
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(parsed.InstallTargets))
|
||||||
|
assert.Equal(t, "cowsay", parsed.InstallTargets[0].PackageVersion.Package.Name)
|
||||||
|
assert.Equal(t, "1.6.0", parsed.InstallTargets[0].PackageVersion.Version)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package via -p flag with different binary",
|
||||||
|
command: "npx -p typescript tsc --version",
|
||||||
|
assert: func(t *testing.T, parsed *ParsedCommand, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(parsed.InstallTargets))
|
||||||
|
assert.Equal(t, "typescript", parsed.InstallTargets[0].PackageVersion.Package.Name)
|
||||||
|
assert.Empty(t, parsed.InstallTargets[0].PackageVersion.Version)
|
||||||
|
// tsc is the binary, not a package
|
||||||
|
assert.Equal(t, []string{"-p", "typescript", "tsc", "--version"}, 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