Files
pmg/guard/guard_test.go
T
20c854e473 feat: Config Persistence & API (#83)
* 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>
2026-01-01 12:33:52 +05:30

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)
})
}
}