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)
|
- [Dry Run](#dry-run)
|
||||||
- [Verbose Mode](#verbose-mode)
|
- [Verbose Mode](#verbose-mode)
|
||||||
- [Debugging](#debugging)
|
- [Debugging](#debugging)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
- [🤝 Contributing](#-contributing)
|
- [🤝 Contributing](#-contributing)
|
||||||
- [🚫 Limitations](#-limitations)
|
- [🚫 Limitations](#-limitations)
|
||||||
|
|
||||||
@@ -214,6 +215,19 @@ Store the debug logs in a file:
|
|||||||
pmg --debug --log /tmp/debug.json npm install <package-name>
|
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
|
## 🤝 Contributing
|
||||||
|
|
||||||
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
|
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ type Config struct {
|
|||||||
// DryRun to check for packages for risks.
|
// DryRun to check for packages for risks.
|
||||||
// Do not actually execute any commands.
|
// Do not actually execute any commands.
|
||||||
DryRun bool
|
DryRun bool
|
||||||
|
|
||||||
|
// InsecureInstallation allows bypassing install blocking on malicious packages
|
||||||
|
InsecureInstallation bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject config into context while protecting against context poisoning
|
// 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 is called to clear the status of the guard in the UI
|
||||||
ClearStatus func()
|
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 is called to get the confirmation of the user on the malware packages
|
||||||
GetConfirmationOnMalware func(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error)
|
GetConfirmationOnMalware func(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error)
|
||||||
|
|
||||||
@@ -38,6 +41,7 @@ type PackageManagerGuardConfig struct {
|
|||||||
MaxConcurrentAnalyzes int
|
MaxConcurrentAnalyzes int
|
||||||
AnalysisTimeout time.Duration
|
AnalysisTimeout time.Duration
|
||||||
DryRun bool
|
DryRun bool
|
||||||
|
InsecureInstallation bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
||||||
@@ -46,6 +50,7 @@ func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
|||||||
MaxConcurrentAnalyzes: 10,
|
MaxConcurrentAnalyzes: 10,
|
||||||
AnalysisTimeout: 5 * time.Minute,
|
AnalysisTimeout: 5 * time.Minute,
|
||||||
DryRun: false,
|
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 {
|
func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedCommand *packagemanager.ParsedCommand) error {
|
||||||
log.Debugf("Running package manager guard with args: %v", args)
|
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() {
|
if !parsedCommand.HasInstallTarget() {
|
||||||
// Check if this is a manifest-based installation
|
// Check if this is a manifest-based installation
|
||||||
if parsedCommand.ShouldExtractFromManifest() {
|
if parsedCommand.ShouldExtractFromManifest() {
|
||||||
@@ -283,6 +294,14 @@ func (g *packageManagerGuard) clearStatus() {
|
|||||||
g.interaction.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 {
|
func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, parsedCommand *packagemanager.ParsedCommand) error {
|
||||||
extractorConfig := extractor.NewDefaultExtractorConfig()
|
extractorConfig := extractor.NewDefaultExtractorConfig()
|
||||||
extractorConfig.ExtractorPackageManager = extractor.PackageManagerName(g.packageManager.Name())
|
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"
|
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/internal/ui"
|
||||||
|
"github.com/safedep/pmg/packagemanager"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +18,9 @@ func TestGuardConcurrentlyAnalyzePackagesMalwareQueryService(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pg, err := NewPackageManagerGuard(DefaultPackageManagerGuardConfig(), nil, nil,
|
pg, err := NewPackageManagerGuard(DefaultPackageManagerGuardConfig(), nil, nil,
|
||||||
[]analyzer.PackageVersionAnalyzer{mq}, PackageManagerGuardInteraction{})
|
[]analyzer.PackageVersionAnalyzer{mq}, PackageManagerGuardInteraction{
|
||||||
|
ShowWarning: func(message string) {},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create pg: %v", err)
|
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)
|
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{
|
interaction := guard.PackageManagerGuardInteraction{
|
||||||
SetStatus: ui.SetStatus,
|
SetStatus: ui.SetStatus,
|
||||||
ClearStatus: ui.ClearStatus,
|
ClearStatus: ui.ClearStatus,
|
||||||
|
ShowWarning: ui.ShowWarning,
|
||||||
GetConfirmationOnMalware: ui.GetConfirmationOnMalware,
|
GetConfirmationOnMalware: ui.GetConfirmationOnMalware,
|
||||||
Block: ui.Block,
|
Block: ui.Block,
|
||||||
}
|
}
|
||||||
|
|
||||||
guardConfig := guard.DefaultPackageManagerGuardConfig()
|
guardConfig := guard.DefaultPackageManagerGuardConfig()
|
||||||
guardConfig.DryRun = f.config.DryRun
|
guardConfig.DryRun = f.config.DryRun
|
||||||
|
guardConfig.InsecureInstallation = f.config.InsecureInstallation
|
||||||
|
|
||||||
proxy, err := guard.NewPackageManagerGuard(guardConfig, f.pm, f.packageResolver, analyzers, interaction)
|
proxy, err := guard.NewPackageManagerGuard(guardConfig, f.pm, f.packageResolver, analyzers, interaction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysis
|
|||||||
return false, nil
|
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{}) {
|
func Fatalf(msg string, args ...interface{}) {
|
||||||
ClearStatus()
|
ClearStatus()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/safedep/dry/log"
|
"github.com/safedep/dry/log"
|
||||||
"github.com/safedep/pmg/cmd/npm"
|
"github.com/safedep/pmg/cmd/npm"
|
||||||
@@ -56,6 +57,13 @@ func main() {
|
|||||||
ui.SetVerbosityLevel(ui.VerbosityLevelVerbose)
|
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")
|
log.InitZapLogger("pmg", "cli")
|
||||||
cmd.SetContext(globalConfig.Inject(cmd.Context()))
|
cmd.SetContext(globalConfig.Inject(cmd.Context()))
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user