mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: add suppport for bypassing the blocking behavior of malicious packages (#53)
* feat: add suppport for bypassing the blocking behavior of malicious packages * feat: add InsecureInstallation config to bypass malware scanning with tests * ui: introduce ShowWarning interaction method * guard test fix
This commit is contained in:
@@ -67,6 +67,7 @@ pnpm add <package-name>
|
||||
- [Dry Run](#dry-run)
|
||||
- [Verbose Mode](#verbose-mode)
|
||||
- [Debugging](#debugging)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [🤝 Contributing](#-contributing)
|
||||
- [🚫 Limitations](#-limitations)
|
||||
|
||||
@@ -214,6 +215,19 @@ Store the debug logs in a file:
|
||||
pmg --debug --log /tmp/debug.json npm install <package-name>
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### PMG_INSECURE_INSTALLATION
|
||||
|
||||
Allows bypassing the blocking behavior when malicious packages are detected during installation.
|
||||
|
||||
> ⚠️ **Warning**: This is a security feature bypass. Use with extreme caution and only when you understand the risks.
|
||||
|
||||
```bash
|
||||
export PMG_INSECURE_INSTALLATION=true
|
||||
pmg npm install <package-name>
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
@@ -20,6 +20,9 @@ type Config struct {
|
||||
// DryRun to check for packages for risks.
|
||||
// Do not actually execute any commands.
|
||||
DryRun bool
|
||||
|
||||
// InsecureInstallation allows bypassing install blocking on malicious packages
|
||||
InsecureInstallation bool
|
||||
}
|
||||
|
||||
// Inject config into context while protecting against context poisoning
|
||||
|
||||
@@ -24,6 +24,9 @@ type PackageManagerGuardInteraction struct {
|
||||
// ClearStatus is called to clear the status of the guard in the UI
|
||||
ClearStatus func()
|
||||
|
||||
// ShowWarning is called to show a warning message to the user
|
||||
ShowWarning func(message string)
|
||||
|
||||
// GetConfirmationOnMalware is called to get the confirmation of the user on the malware packages
|
||||
GetConfirmationOnMalware func(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error)
|
||||
|
||||
@@ -38,6 +41,7 @@ type PackageManagerGuardConfig struct {
|
||||
MaxConcurrentAnalyzes int
|
||||
AnalysisTimeout time.Duration
|
||||
DryRun bool
|
||||
InsecureInstallation bool
|
||||
}
|
||||
|
||||
func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
||||
@@ -46,6 +50,7 @@ func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
||||
MaxConcurrentAnalyzes: 10,
|
||||
AnalysisTimeout: 5 * time.Minute,
|
||||
DryRun: false,
|
||||
InsecureInstallation: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +80,12 @@ func NewPackageManagerGuard(config PackageManagerGuardConfig,
|
||||
func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedCommand *packagemanager.ParsedCommand) error {
|
||||
log.Debugf("Running package manager guard with args: %v", args)
|
||||
|
||||
if g.config.InsecureInstallation {
|
||||
log.Debugf("Bypassing block for unconfirmed malicious packages due to PMG_INSECURE_INSTALLATION")
|
||||
g.showWarning("⚠️ WARNING: INSECURE INSTALLATION MODE - Malware protection bypassed!")
|
||||
return g.continueExecution(ctx, parsedCommand)
|
||||
}
|
||||
|
||||
if !parsedCommand.HasInstallTarget() {
|
||||
// Check if this is a manifest-based installation
|
||||
if parsedCommand.ShouldExtractFromManifest() {
|
||||
@@ -283,6 +294,14 @@ func (g *packageManagerGuard) clearStatus() {
|
||||
g.interaction.ClearStatus()
|
||||
}
|
||||
|
||||
func (g *packageManagerGuard) showWarning(message string) {
|
||||
if g.interaction.ShowWarning == nil {
|
||||
return
|
||||
}
|
||||
|
||||
g.interaction.ShowWarning(message)
|
||||
}
|
||||
|
||||
func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, parsedCommand *packagemanager.ParsedCommand) error {
|
||||
extractorConfig := extractor.NewDefaultExtractorConfig()
|
||||
extractorConfig.ExtractorPackageManager = extractor.PackageManagerName(g.packageManager.Name())
|
||||
|
||||
+224
-1
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/packagemanager"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -16,7 +18,9 @@ func TestGuardConcurrentlyAnalyzePackagesMalwareQueryService(t *testing.T) {
|
||||
}
|
||||
|
||||
pg, err := NewPackageManagerGuard(DefaultPackageManagerGuardConfig(), nil, nil,
|
||||
[]analyzer.PackageVersionAnalyzer{mq}, PackageManagerGuardInteraction{})
|
||||
[]analyzer.PackageVersionAnalyzer{mq}, PackageManagerGuardInteraction{
|
||||
ShowWarning: func(message string) {},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pg: %v", err)
|
||||
}
|
||||
@@ -45,3 +49,222 @@ func TestGuardConcurrentlyAnalyzePackagesMalwareQueryService(t *testing.T) {
|
||||
assert.Equal(t, analyzer.ActionBlock, r[0].Action)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGuardInsecureInstallation(t *testing.T) {
|
||||
mq, err := analyzer.NewMalysisQueryAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create mq: %v", err)
|
||||
}
|
||||
|
||||
t.Run("should bypass malware blocking when InsecureInstallation is enabled", func(t *testing.T) {
|
||||
// Create guard with InsecureInstallation enabled
|
||||
config := DefaultPackageManagerGuardConfig()
|
||||
config.InsecureInstallation = true
|
||||
config.DryRun = true // Enable dry run to avoid actual command execution
|
||||
config.ResolveDependencies = false // Disable dependency resolution to avoid nil pointer issues
|
||||
|
||||
blockCalled := false
|
||||
warningCalled := false
|
||||
var warningMessage string
|
||||
|
||||
interaction := PackageManagerGuardInteraction{
|
||||
ShowWarning: func(message string) {
|
||||
warningCalled = true
|
||||
warningMessage = message
|
||||
},
|
||||
Block: func(config *ui.BlockConfig) error {
|
||||
blockCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
pg, err := NewPackageManagerGuard(config, nil, nil,
|
||||
[]analyzer.PackageVersionAnalyzer{mq}, interaction)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pg: %v", err)
|
||||
}
|
||||
|
||||
// Create a parsed command with a known malicious package
|
||||
parsedCommand := &packagemanager.ParsedCommand{
|
||||
Command: packagemanager.Command{
|
||||
Exe: "npm",
|
||||
Args: []string{"install", "nyc-config@10.0.0"},
|
||||
},
|
||||
InstallTargets: []*packagemanager.PackageInstallTarget{
|
||||
{
|
||||
PackageVersion: &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Name: "nyc-config",
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||
},
|
||||
Version: "10.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = pg.Run(context.Background(), []string{"npm", "install", "nyc-config@10.0.0"}, parsedCommand)
|
||||
|
||||
// With dry run enabled, we expect no error even though we're bypassing execution
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Block should not be called because InsecureInstallation bypasses the analysis
|
||||
assert.False(t, blockCalled, "Block should not be called when InsecureInstallation is enabled")
|
||||
|
||||
// Warning should be called to inform user about insecure installation
|
||||
assert.True(t, warningCalled, "Warning should be called when InsecureInstallation is enabled")
|
||||
assert.Contains(t, warningMessage, "INSECURE INSTALLATION MODE", "Warning message should mention insecure installation")
|
||||
})
|
||||
|
||||
t.Run("should block malware when InsecureInstallation is disabled", func(t *testing.T) {
|
||||
// Create guard with InsecureInstallation disabled (default)
|
||||
config := DefaultPackageManagerGuardConfig()
|
||||
config.InsecureInstallation = false
|
||||
config.DryRun = true
|
||||
config.ResolveDependencies = false // Disable dependency resolution to avoid nil pointer issues
|
||||
|
||||
blockCalled := false
|
||||
var blockedPackages []*analyzer.PackageVersionAnalysisResult
|
||||
|
||||
interaction := PackageManagerGuardInteraction{
|
||||
ShowWarning: func(message string) {},
|
||||
Block: func(config *ui.BlockConfig) error {
|
||||
blockCalled = true
|
||||
blockedPackages = config.MalwarePackages
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
pg, err := NewPackageManagerGuard(config, nil, nil,
|
||||
[]analyzer.PackageVersionAnalyzer{mq}, interaction)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pg: %v", err)
|
||||
}
|
||||
|
||||
// Create a parsed command with a known malicious package
|
||||
parsedCommand := &packagemanager.ParsedCommand{
|
||||
Command: packagemanager.Command{
|
||||
Exe: "npm",
|
||||
Args: []string{"install", "nyc-config@10.0.0"},
|
||||
},
|
||||
InstallTargets: []*packagemanager.PackageInstallTarget{
|
||||
{
|
||||
PackageVersion: &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Name: "nyc-config",
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||
},
|
||||
Version: "10.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = pg.Run(context.Background(), []string{"npm", "install", "nyc-config@10.0.0"}, parsedCommand)
|
||||
|
||||
// We expect no error from the guard itself (blocking is handled via the Block callback)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Block should be called because InsecureInstallation is disabled
|
||||
assert.True(t, blockCalled, "Block should be called when InsecureInstallation is disabled")
|
||||
|
||||
// Verify that the malicious package was detected and blocked
|
||||
assert.NotEmpty(t, blockedPackages, "Blocked packages should not be empty")
|
||||
if len(blockedPackages) > 0 {
|
||||
assert.Equal(t, "nyc-config", blockedPackages[0].PackageVersion.GetPackage().GetName())
|
||||
assert.Equal(t, "10.0.0", blockedPackages[0].PackageVersion.GetVersion())
|
||||
assert.Equal(t, analyzer.ActionBlock, blockedPackages[0].Action)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should continue execution for commands without install targets when InsecureInstallation is enabled", func(t *testing.T) {
|
||||
// Create guard with InsecureInstallation enabled
|
||||
config := DefaultPackageManagerGuardConfig()
|
||||
config.InsecureInstallation = true
|
||||
config.DryRun = true
|
||||
config.ResolveDependencies = false // Disable dependency resolution to avoid nil pointer issues
|
||||
|
||||
blockCalled := false
|
||||
|
||||
interaction := PackageManagerGuardInteraction{
|
||||
ShowWarning: func(message string) {},
|
||||
Block: func(config *ui.BlockConfig) error {
|
||||
blockCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
pg, err := NewPackageManagerGuard(config, nil, nil,
|
||||
[]analyzer.PackageVersionAnalyzer{mq}, interaction)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pg: %v", err)
|
||||
}
|
||||
|
||||
// Create a parsed command without install targets (e.g., npm list)
|
||||
parsedCommand := &packagemanager.ParsedCommand{
|
||||
Command: packagemanager.Command{
|
||||
Exe: "npm",
|
||||
Args: []string{"list"},
|
||||
},
|
||||
InstallTargets: []*packagemanager.PackageInstallTarget{}, // No install targets
|
||||
}
|
||||
|
||||
err = pg.Run(context.Background(), []string{"npm", "list"}, parsedCommand)
|
||||
|
||||
// Should not error since there are no install targets to analyze
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Block should not be called since there are no packages to analyze
|
||||
assert.False(t, blockCalled, "Block should not be called when there are no install targets")
|
||||
})
|
||||
|
||||
t.Run("should handle manifest-based installation when InsecureInstallation is enabled", func(t *testing.T) {
|
||||
// Create guard with InsecureInstallation enabled
|
||||
config := DefaultPackageManagerGuardConfig()
|
||||
config.InsecureInstallation = true
|
||||
config.DryRun = true
|
||||
config.ResolveDependencies = false // Disable dependency resolution to avoid nil pointer issues
|
||||
|
||||
blockCalled := false
|
||||
|
||||
interaction := PackageManagerGuardInteraction{
|
||||
ShowWarning: func(message string) {},
|
||||
Block: func(config *ui.BlockConfig) error {
|
||||
blockCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
pg, err := NewPackageManagerGuard(config, nil, nil,
|
||||
[]analyzer.PackageVersionAnalyzer{mq}, interaction)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pg: %v", err)
|
||||
}
|
||||
|
||||
// Create a parsed command for manifest-based installation
|
||||
parsedCommand := &packagemanager.ParsedCommand{
|
||||
Command: packagemanager.Command{
|
||||
Exe: "npm",
|
||||
Args: []string{"install"},
|
||||
},
|
||||
InstallTargets: []*packagemanager.PackageInstallTarget{}, // No direct install targets
|
||||
IsManifestInstall: true,
|
||||
ManifestFiles: []string{"package.json"},
|
||||
}
|
||||
|
||||
err = pg.Run(context.Background(), []string{"npm", "install"}, parsedCommand)
|
||||
|
||||
// Should not error and should bypass malware checking
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Block should not be called because InsecureInstallation bypasses analysis
|
||||
assert.False(t, blockCalled, "Block should not be called when InsecureInstallation is enabled for manifest installation")
|
||||
})
|
||||
|
||||
t.Run("should verify InsecureInstallation defaults to false", func(t *testing.T) {
|
||||
config := DefaultPackageManagerGuardConfig()
|
||||
|
||||
// Verify that InsecureInstallation defaults to false
|
||||
assert.False(t, config.InsecureInstallation, "InsecureInstallation should default to false")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,12 +49,14 @@ func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagem
|
||||
interaction := guard.PackageManagerGuardInteraction{
|
||||
SetStatus: ui.SetStatus,
|
||||
ClearStatus: ui.ClearStatus,
|
||||
ShowWarning: ui.ShowWarning,
|
||||
GetConfirmationOnMalware: ui.GetConfirmationOnMalware,
|
||||
Block: ui.Block,
|
||||
}
|
||||
|
||||
guardConfig := guard.DefaultPackageManagerGuardConfig()
|
||||
guardConfig.DryRun = f.config.DryRun
|
||||
guardConfig.InsecureInstallation = f.config.InsecureInstallation
|
||||
|
||||
proxy, err := guard.NewPackageManagerGuard(guardConfig, f.pm, f.packageResolver, analyzers, interaction)
|
||||
if err != nil {
|
||||
|
||||
@@ -105,6 +105,11 @@ func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysis
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func ShowWarning(message string) {
|
||||
// Print colored warning to stderr immediately - it won't be cleared by other output
|
||||
fmt.Fprintf(os.Stderr, "%s\n", Colors.Red(message))
|
||||
}
|
||||
|
||||
func Fatalf(msg string, args ...interface{}) {
|
||||
ClearStatus()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/cmd/npm"
|
||||
@@ -56,6 +57,13 @@ func main() {
|
||||
ui.SetVerbosityLevel(ui.VerbosityLevelVerbose)
|
||||
}
|
||||
|
||||
// Check for PMG_INSECURE_INSTALLATION environment variable
|
||||
if val := os.Getenv("PMG_INSECURE_INSTALLATION"); val != "" {
|
||||
if boolVal, err := strconv.ParseBool(val); err == nil {
|
||||
globalConfig.InsecureInstallation = boolVal
|
||||
}
|
||||
}
|
||||
|
||||
log.InitZapLogger("pmg", "cli")
|
||||
cmd.SetContext(globalConfig.Inject(cmd.Context()))
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user