mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* introduce a persistent config * add tests and refactor config creation * update config handling and add support for removing config * add support to skip suspicious pkgs marked as trusted * add support for config dir Env & unexport functions * small fixes * add assert for dir * fix tests * fix shell source line & trusted pkgs parsing * fix flag inconsistency * update config to read on each invocation and create if does not exist * fix flags value being overridden * remove redundant func call * modify trusted pkg check to be config bound * modify RemoveConfig to rm files & not dir. add tests for paths.go * add versions for package for e2e * modify tests to reset config * fix: Simplify config persistence * fix: Misc comments * fix: Misc fix * fix: Do not overwrite config file if exists * fix: Do not overwrite config file if exists * fix: Config cobra command should override and not replace * fix: Create dir before writing config template * fix: Create dir before writing config template * fix: Misc refactoring * test: Add test for is trusted package version * Update cmd/setup/setup.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> * Update config/config.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> * fix: Remove unused constant in config * fix: Resolve conflict with event logger * docs: Add doc for eventlogger.Logger interface * test: Add E2E for config file creation * fix: Code review fixes --------- Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> Co-authored-by: Sahilb315 <bansalsahil315@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
531 lines
15 KiB
Go
531 lines
15 KiB
Go
package guard
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
|
"github.com/safedep/pmg/analyzer"
|
|
"github.com/safedep/pmg/config"
|
|
"github.com/safedep/pmg/internal/ui"
|
|
"github.com/safedep/pmg/packagemanager"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestGuardConcurrentlyAnalyzePackagesMalwareQueryService(t *testing.T) {
|
|
mq, err := analyzer.NewMalysisQueryAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
|
|
if err != nil {
|
|
t.Fatalf("failed to create mq: %v", err)
|
|
}
|
|
|
|
pg, err := NewPackageManagerGuard(DefaultPackageManagerGuardConfig(), nil, nil,
|
|
[]analyzer.PackageVersionAnalyzer{mq}, PackageManagerGuardInteraction{
|
|
ShowWarning: func(message string) {},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to create pg: %v", err)
|
|
}
|
|
|
|
t.Run("should resolve a single known malicious package version", func(t *testing.T) {
|
|
r, err := pg.concurrentAnalyzePackages(context.Background(), []*packagev1.PackageVersion{
|
|
{
|
|
Package: &packagev1.Package{
|
|
Name: "nyc-config",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "10.0.0",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to analyze packages: %v", err)
|
|
}
|
|
|
|
assert.Equal(t, 1, len(r))
|
|
assert.Equal(t, "nyc-config", r[0].PackageVersion.GetPackage().GetName())
|
|
assert.Equal(t, "10.0.0", r[0].PackageVersion.GetVersion())
|
|
assert.Equal(t, packagev1.Ecosystem_ECOSYSTEM_NPM, r[0].PackageVersion.GetPackage().GetEcosystem())
|
|
assert.NotEmpty(t, r[0].ReferenceURL)
|
|
assert.NotEmpty(t, r[0].Summary)
|
|
assert.NotNil(t, r[0].Data)
|
|
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")
|
|
})
|
|
}
|
|
|
|
func TestGuardIsTrustedPackageVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
trustedPackages []config.TrustedPackage
|
|
pkgVersion *packagev1.PackageVersion
|
|
want bool
|
|
}{
|
|
{
|
|
name: "nil package version returns false",
|
|
trustedPackages: []config.TrustedPackage{},
|
|
pkgVersion: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "empty trusted packages list returns false",
|
|
trustedPackages: []config.TrustedPackage{},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "exact match with version returns true",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/express@4.18.0",
|
|
Reason: "trusted by team",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "match without version in trusted package returns true",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/express",
|
|
Reason: "all versions trusted",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "version mismatch returns false",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/express@4.17.0",
|
|
Reason: "old version trusted",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "name mismatch returns false",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/react@18.0.0",
|
|
Reason: "trusted package",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "ecosystem mismatch returns false",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:pypi/requests@2.28.0",
|
|
Reason: "trusted package",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "requests",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "2.28.0",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "pypi package exact match returns true",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:pypi/requests@2.28.0",
|
|
Reason: "trusted http library",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "requests",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_PYPI,
|
|
},
|
|
Version: "2.28.0",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "multiple trusted packages finds correct match",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/lodash@4.17.21",
|
|
Reason: "utility library",
|
|
},
|
|
{
|
|
Purl: "pkg:npm/express@4.18.0",
|
|
Reason: "web framework",
|
|
},
|
|
{
|
|
Purl: "pkg:pypi/requests@2.28.0",
|
|
Reason: "http library",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "multiple trusted packages no match returns false",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/lodash@4.17.21",
|
|
Reason: "utility library",
|
|
},
|
|
{
|
|
Purl: "pkg:npm/react@18.0.0",
|
|
Reason: "ui library",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "invalid purl in trusted packages skips and returns false",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "invalid-purl-format",
|
|
Reason: "malformed",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "invalid purl skipped but valid match found",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "invalid-purl-format",
|
|
Reason: "malformed",
|
|
},
|
|
{
|
|
Purl: "pkg:npm/express@4.18.0",
|
|
Reason: "valid trusted package",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "4.18.0",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "package version without version field matches versionless trusted package",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/express",
|
|
Reason: "all versions trusted",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "package version without version field does not match versioned trusted package",
|
|
trustedPackages: []config.TrustedPackage{
|
|
{
|
|
Purl: "pkg:npm/express@4.18.0",
|
|
Reason: "specific version trusted",
|
|
},
|
|
},
|
|
pkgVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Name: "express",
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
},
|
|
Version: "",
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := PackageManagerGuardConfig{
|
|
TrustedPackages: tt.trustedPackages,
|
|
}
|
|
|
|
got := config.IsTrustedPackageVersion(tt.pkgVersion)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|