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:
Sahil Bansal
2025-07-02 19:09:27 +05:30
committed by GitHub
parent 0e17378d3e
commit ca752edf79
7 changed files with 275 additions and 1 deletions
+14
View File
@@ -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)
+3
View File
@@ -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
+19
View File
@@ -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
View File
@@ -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")
})
}
+2
View File
@@ -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 {
+5
View File
@@ -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()
+8
View File
@@ -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()))
},