diff --git a/.gitignore b/.gitignore index fda9ea0..1c180c6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,11 @@ bin/ tmp/ **/*.log.dccache +# Vim swap files +*.swp +*.swo +*~ + # Development cache and temporary files .dccache *.log @@ -60,3 +65,4 @@ tmp/ *_improvements.md *_notes.md *_todo.md +TODO.md diff --git a/.swo b/.swo deleted file mode 100644 index aa5028a..0000000 Binary files a/.swo and /dev/null differ diff --git a/CLAUDE.md b/CLAUDE.md index 1680c74..b5033bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,13 +162,17 @@ For detailed technical architecture, design patterns, and implementation guideli **Cross-Package Manager Compatibility**: SysPkg normalizes package states for consistent behavior across different package managers. For example, APT's "config-files" state (packages removed but with configuration files remaining) is normalized to "available" status to match the semantics used by other package managers like YUM and Snap. +## Current Session Todos + +@TODO.md + ## Project Improvement Roadmap *Note: To-do list consolidated 2025-05-30 - removed duplicates, feature creep items, and over-engineering. Focused on core security, testing, and platform support.* ### 🔴 High Priority (Security & Critical Bugs) - 15 items -1. **Fix command injection vulnerability** - validate/sanitize package names before exec.Command -2. **Implement input validation helper function** for package names and arguments +1. **Fix command injection vulnerability** ✅ - validate/sanitize package names before exec.Command (PR #25) +2. **Implement input validation helper function** ✅ for package names and arguments (PR #25) 3. **Fix resource leaks** in error handling paths 4. **Add security scanning with Snyk** to CI/CD pipeline 5. **Review and merge PR #12** - fix GetPackageManager("") panic bug ✅ @@ -179,64 +183,42 @@ For detailed technical architecture, design patterns, and implementation guideli 10. **Implement CommandBuilder interface (Issue #20)** - Replace direct exec.Command calls with testable CommandBuilder pattern 11. **Add exit code documentation** ✅ - Created comprehensive exit code docs for all package managers -### ✅ CRITICAL INVESTIGATION COMPLETED -**Investigation Results: No design flaw found - tests are correct** -8. **RESOLVED: Tests are correctly testing parser functions** ✅ - `behavior_test.go` tests `ParseFindOutput()` directly, not `Find()` method -9. **RESOLVED: ParseFindOutput() vs Find() distinction clarified** ✅: - - `ParseFindOutput()`: Pure parser, returns all packages as "available" (YUM limitation) - - `Find()`: Enhanced method that adds rpm -q status detection for accurate results -10. **RESOLVED: CommandRunner interface verified** ✅ - Correctly implemented in `enhancePackagesWithStatus()` -11. **RESOLVED: Test execution paths confirmed** ✅ - Tests correctly test parsers, not enhanced methods -12. **RESOLVED: Fixtures validated as authentic** ✅ - Real YUM output that correctly lacks status info -13. **RESOLVED: Interface implementation verified** ✅ - All methods properly implemented and registered -14. **RESOLVED: Created integration tests** ✅ - Added `yum_integration_test.go` demonstrating three-layer testing approach - -### 🟡 Medium Priority (Code Quality & Testing) - 8 items -**Testing:** -- **Create integration tests** ✅ - Added `yum_integration_test.go` with three-layer testing approach -- **Document testing strategy** ✅ - Added comprehensive testing documentation to CONTRIBUTING.md -- **Implement CommandRunner dependency injection (Issue #20)** 🚧 - YUM partially implemented, architecture decision updated to Option C (CommandBuilder) -- Add unit tests for snap package manager -- Add unit tests for flatpak package manager -- **APT fixture cleanup and behavior testing** ✅ - Reduced 16→7 fixtures, full test coverage -- **Cross-platform parsing robustness** ✅ - CRLF/whitespace handling, regex optimization -- **YUM fixture analysis and cleanup** ✅ **COMPLETED** (Issue #16) - Following APT pattern: - - ✅ Analyzed YUM fixtures to determine what's needed for comprehensive testing - - ✅ Removed redundant/duplicate files (search-vim-rockylinux.txt) - - ✅ Standardized fixture naming convention (rocky8 vs rockylinux inconsistency) - - ✅ Renamed info-vim-rockylinux.txt → info-vim-installed-rocky8.txt for clarity - - ✅ Added missing edge case fixtures (empty results, not found, clean, refresh) - - ✅ Created comprehensive behavior_test.go following APT fixture pattern - - ✅ Converted YUM tests from inline data to fixture-based tests - - ✅ Verified fixture compatibility and completeness with all tests passing -- **YUM operations implementation** ✅ **COMPLETED** - Comprehensive YUM package manager: - - ✅ Implemented all missing operations: Install, Delete, ListUpgradable, Upgrade, UpgradeAll, AutoRemove - - ✅ Added complete parser functions for all operation outputs - - ✅ Created comprehensive behavior tests covering all operations and edge cases - - ✅ Generated real fixtures using Rocky Linux Docker for authentic test data - - ✅ Documented YUM-specific behaviors and cross-package manager compatibility - - ✅ **YUM Find() status detection** - Implemented rpm -q integration for accurate installation status - - ✅ **Cross-package manager API consistency** - YUM Find() now matches APT behavior exactly - - ✅ All tests passing with 100% security scan clearance - -**Documentation:** -- **API and behavior documentation** ✅ - Enhanced interface docs, status normalization, cross-PM compatibility -- **Error handling best practices** ✅ - Fixed ignored errors in documentation examples -- **Accuracy improvements** ✅ - Fixed misleading comments about status handling -- **YUM documentation updates** ✅ - Updated all outdated behavior comments to reflect Find() status detection capabilities - -**Code Improvements:** -- Implement context support for cancellation and timeouts -- Create custom error types for better error handling -- Extract common parsing logic to shared utilities (DRY principle) -- Replace magic strings/numbers with named constants -- **Fix APT multi-arch package parsing** (Issue #15) - cosmetic fix for empty package names +### ✅ COMPLETED INVESTIGATIONS (Collapsed) +
+Critical investigation results - tests are correctly implemented -**Removed from roadmap (2025-05-30):** -- ~~Structured logging~~ (over-engineering for project scope) -- ~~Progress indicators~~ (feature creep for CLI/library) -- ~~Architecture diagrams~~ (low ROI for library project) -- ~~TODO comment fixes~~ (covered by security improvements) +**Investigation Results: No design flaw found - tests are correct** +- ✅ **Parser vs enhanced method distinction clarified** +- ✅ **CommandRunner interface verified** +- ✅ **Test execution paths confirmed** +- ✅ **Fixtures validated as authentic** +- ✅ **Integration tests created** +
+ +### 🟡 Medium Priority & Completed Items (Collapsed) +
+Code quality, testing achievements, and removed items + +**Completed Testing Work:** +- ✅ YUM comprehensive implementation (Issue #16) +- ✅ APT fixture cleanup and behavior testing +- ✅ Integration tests and testing strategy documentation +- ✅ Cross-platform parsing robustness + +**Completed Documentation:** +- ✅ API and behavior documentation enhanced +- ✅ Error handling best practices documented +- ✅ YUM documentation updates + +**Remaining Code Improvements:** +- Context support for cancellation and timeouts +- Custom error types for better error handling +- Extract common parsing logic (DRY principle) +- Replace magic strings/numbers with constants + +**Items Removed from Roadmap (2025-05-30):** +- ~~Structured logging, progress indicators, architecture diagrams~~ (over-engineering/feature creep) +
### 🟢 Low Priority (Platform Support) - 3 items **New Package Managers:** diff --git a/manager/apt/apt.go b/manager/apt/apt.go index 534e81d..a336f45 100644 --- a/manager/apt/apt.go +++ b/manager/apt/apt.go @@ -86,6 +86,11 @@ func (a *PackageManager) GetPackageManager() string { // Install installs the provided packages using the apt package manager. func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + args := append([]string{"install", ArgsFixBroken}, pkgs...) if opts == nil { @@ -125,6 +130,11 @@ func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manage // Delete removes the provided packages using the apt package manager. func (a *PackageManager) Delete(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + // args := append([]string{"remove", ArgsFixBroken, ArgsPurge, ArgsAutoRemove}, pkgs...) args := append([]string{"remove", ArgsFixBroken, ArgsAutoRemove}, pkgs...) if opts == nil { @@ -192,6 +202,11 @@ func (a *PackageManager) Refresh(opts *manager.Options) error { // Find searches for packages matching the provided keywords using the apt package manager. func (a *PackageManager) Find(keywords []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate keywords to prevent command injection + if err := manager.ValidatePackageNames(keywords); err != nil { + return nil, err + } + args := append([]string{"search"}, keywords...) cmd := exec.Command("apt", args...) cmd.Env = append(os.Environ(), ENV_NonInteractive...) @@ -229,6 +244,13 @@ func (a *PackageManager) ListUpgradable(opts *manager.Options) ([]manager.Packag // Upgrade upgrades the provided packages using the apt package manager. func (a *PackageManager) Upgrade(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if len(pkgs) > 0 { + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + } + args := []string{"upgrade"} if len(pkgs) > 0 { args = append(args, pkgs...) @@ -307,6 +329,11 @@ func (a *PackageManager) Clean(opts *manager.Options) error { // GetPackageInfo retrieves package information for the specified package using the apt package manager. func (a *PackageManager) GetPackageInfo(pkg string, opts *manager.Options) (manager.PackageInfo, error) { + // Validate package name to prevent command injection + if err := manager.ValidatePackageName(pkg); err != nil { + return manager.PackageInfo{}, err + } + cmd := exec.Command("apt-cache", "show", pkg) cmd.Env = ENV_NonInteractive out, err := cmd.Output() diff --git a/manager/apt/utils.go b/manager/apt/utils.go index e0adb71..3c3b8f5 100644 --- a/manager/apt/utils.go +++ b/manager/apt/utils.go @@ -311,6 +311,11 @@ func logDebugPackages(packages map[string]manager.PackageInfo, opts *manager.Opt // runDpkgQuery executes dpkg-query command and handles errors appropriately func runDpkgQuery(packageNames []string, opts *manager.Options) ([]byte, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(packageNames); err != nil { + return nil, err + } + args := []string{"-W", "--showformat", "${binary:Package} ${Status} ${Version}\n"} args = append(args, packageNames...) cmd := exec.Command("dpkg-query", args...) diff --git a/manager/flatpak/flatpak.go b/manager/flatpak/flatpak.go index 2fc54c4..f3aefe4 100644 --- a/manager/flatpak/flatpak.go +++ b/manager/flatpak/flatpak.go @@ -61,6 +61,11 @@ func (a *PackageManager) GetPackageManager() string { // Install installs the given packages using Flatpak with the provided options. func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + args := append([]string{"install", ArgsFixBroken, ArgsUpsert, ArgsVerbose}, pkgs...) if opts == nil { @@ -104,6 +109,11 @@ func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manage // Delete removes the given packages using Flatpak with the provided options. func (a *PackageManager) Delete(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + args := append([]string{"uninstall", ArgsFixBroken, ArgsVerbose}, pkgs...) if opts == nil { @@ -154,6 +164,11 @@ func (a *PackageManager) Refresh(opts *manager.Options) error { // Find searches for packages matching the given keywords using Flatpak with the provided options. func (a *PackageManager) Find(keywords []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate keywords to prevent command injection + if err := manager.ValidatePackageNames(keywords); err != nil { + return nil, err + } + args := append([]string{"search", ArgsVerbose}, keywords...) if opts == nil { @@ -248,6 +263,11 @@ func (a *PackageManager) UpgradeAll(opts *manager.Options) ([]manager.PackageInf // GetPackageInfo retrieves package information for a single package using Flatpak with the provided options. func (a *PackageManager) GetPackageInfo(pkg string, opts *manager.Options) (manager.PackageInfo, error) { + // Validate package name to prevent command injection + if err := manager.ValidatePackageName(pkg); err != nil { + return manager.PackageInfo{}, err + } + cmd := exec.Command(pm, "info", pkg) cmd.Env = ENV_NonInteractive out, err := cmd.Output() diff --git a/manager/security.go b/manager/security.go new file mode 100644 index 0000000..78d45a0 --- /dev/null +++ b/manager/security.go @@ -0,0 +1,83 @@ +// Package manager provides security utilities for package manager operations +package manager + +import ( + "errors" + "regexp" +) + +// packageNameRegex defines the allowed pattern for package names +// This pattern allows: +// - Letters (a-z, A-Z) +// - Numbers (0-9) +// - Dash/hyphen (-) +// - Underscore (_) +// - Period/dot (.) +// - Plus sign (+) +// - Colon (:) for architecture specifiers (e.g., package:amd64) +// - Forward slash (/) for repository specifiers (e.g., repo/package) +// The pattern requires at least one valid character +var packageNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-_.+:/]+$`) + +// ErrInvalidPackageName is returned when a package name contains invalid characters +var ErrInvalidPackageName = errors.New("invalid package name: contains potentially dangerous characters") + +// ValidatePackageName validates that a package name only contains safe characters +// to prevent command injection attacks. +// +// Valid package names may contain: +// - Alphanumeric characters (a-z, A-Z, 0-9) +// - Dash/hyphen (-) +// - Underscore (_) +// - Period/dot (.) +// - Plus sign (+) +// - Colon (:) for architecture specifiers +// - Forward slash (/) for repository specifiers +// +// This function rejects any package names containing: +// - Shell metacharacters (;, |, &, $, `, \, ", ', <, >, (, ), {, }, [, ], *, ?, ~) +// - Whitespace characters +// - Control characters +// - Null bytes +// +// Example valid names: +// - "vim" +// - "libssl1.1" +// - "gcc-9-base" +// - "python3.8" +// - "package:amd64" +// - "repo/package" +// +// Example invalid names: +// - "package; rm -rf /" +// - "package && malicious-command" +// - "package`evil`" +// - "package$(bad)" +func ValidatePackageName(name string) error { + // Check for empty string + if name == "" { + return errors.New("package name cannot be empty") + } + + // Check length limit (most package managers have reasonable limits) + if len(name) > 255 { + return errors.New("package name too long (max 255 characters)") + } + + // Validate against regex pattern + if !packageNameRegex.MatchString(name) { + return ErrInvalidPackageName + } + + return nil +} + +// ValidatePackageNames validates multiple package names +func ValidatePackageNames(names []string) error { + for _, name := range names { + if err := ValidatePackageName(name); err != nil { + return err + } + } + return nil +} diff --git a/manager/security_test.go b/manager/security_test.go new file mode 100644 index 0000000..d9d7007 --- /dev/null +++ b/manager/security_test.go @@ -0,0 +1,240 @@ +package manager + +import ( + "strings" + "testing" +) + +func TestValidatePackageName(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + errMsg string + }{ + // Valid package names + { + name: "simple package name", + input: "vim", + wantErr: false, + }, + { + name: "package with version", + input: "python3.8", + wantErr: false, + }, + { + name: "package with dash", + input: "gcc-9-base", + wantErr: false, + }, + { + name: "package with underscore", + input: "libc6_dev", + wantErr: false, + }, + { + name: "package with plus", + input: "g++", + wantErr: false, + }, + { + name: "package with architecture", + input: "libc6:amd64", + wantErr: false, + }, + { + name: "package with repo", + input: "ppa/package-name", + wantErr: false, + }, + { + name: "complex valid name", + input: "lib32stdc++-9-dev:i386", + wantErr: false, + }, + + // Invalid package names - command injection attempts + { + name: "semicolon injection", + input: "package; rm -rf /", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "pipe injection", + input: "package | cat /etc/passwd", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "ampersand injection", + input: "package && malicious-command", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "backtick injection", + input: "package`evil`", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "dollar sign injection", + input: "package$(bad)", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "redirect injection", + input: "package > /etc/passwd", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "single quote injection", + input: "package'; drop table users; --", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "double quote injection", + input: `package"; rm -rf /; "`, + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "backslash injection", + input: `package\nmalicious`, + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "space injection", + input: "package name with spaces", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "tab injection", + input: "package\tmalicious", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "newline injection", + input: "package\nmalicious", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "null byte injection", + input: "package\x00malicious", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "parenthesis injection", + input: "package(malicious)", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "bracket injection", + input: "package[malicious]", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "curly brace injection", + input: "package{malicious}", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "asterisk injection", + input: "package*", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "question mark injection", + input: "package?", + wantErr: true, + errMsg: "invalid package name", + }, + { + name: "tilde injection", + input: "~package", + wantErr: true, + errMsg: "invalid package name", + }, + + // Edge cases + { + name: "empty string", + input: "", + wantErr: true, + errMsg: "empty", + }, + { + name: "very long name", + input: string(make([]byte, 256)), + wantErr: true, + errMsg: "too long", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidatePackageName(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ValidatePackageName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + return + } + if err != nil && tt.errMsg != "" { + if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("ValidatePackageName(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg) + } + } + }) + } +} + +func TestValidatePackageNames(t *testing.T) { + tests := []struct { + name string + input []string + wantErr bool + }{ + { + name: "all valid names", + input: []string{"vim", "git", "python3.8"}, + wantErr: false, + }, + { + name: "one invalid name", + input: []string{"vim", "git; rm -rf /", "python3.8"}, + wantErr: true, + }, + { + name: "empty slice", + input: []string{}, + wantErr: false, + }, + { + name: "empty string in slice", + input: []string{"vim", "", "git"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidatePackageNames(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ValidatePackageNames(%v) error = %v, wantErr %v", tt.input, err, tt.wantErr) + } + }) + } +} diff --git a/manager/snap/snap.go b/manager/snap/snap.go index 43ceff6..c21c122 100644 --- a/manager/snap/snap.go +++ b/manager/snap/snap.go @@ -56,6 +56,11 @@ func (a *PackageManager) GetPackageManager() string { // Install installs the specified packages using the snap package manager with the provided options. func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + args := append([]string{"install", ArgsFixBroken}, pkgs...) if opts == nil { @@ -102,6 +107,11 @@ func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manage // Delete removes the specified packages using the snap package manager with the provided options. func (a *PackageManager) Delete(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + args := append([]string{"remove", ArgsFixBroken}, pkgs...) if opts == nil { @@ -153,6 +163,11 @@ func (a *PackageManager) Refresh(opts *manager.Options) error { // Find searches for packages matching the provided keywords using the snap package manager. func (a *PackageManager) Find(keywords []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate keywords to prevent command injection + if err := manager.ValidatePackageNames(keywords); err != nil { + return nil, err + } + args := append([]string{"search"}, keywords...) cmd := exec.Command("snap", args...) cmd.Env = ENV_NonInteractive @@ -189,6 +204,13 @@ func (a *PackageManager) ListUpgradable(opts *manager.Options) ([]manager.Packag // Upgrade upgrades the specified packages using the snap package manager with the provided options. func (a *PackageManager) Upgrade(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if len(pkgs) > 0 { + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + } + args := []string{"refresh"} if len(pkgs) > 0 { args = append(args, pkgs...) @@ -244,6 +266,11 @@ func (a *PackageManager) UpgradeAll(opts *manager.Options) ([]manager.PackageInf // GetPackageInfo retrieves information about the specified package using the snap package manager. func (a *PackageManager) GetPackageInfo(pkg string, opts *manager.Options) (manager.PackageInfo, error) { + // Validate package name to prevent command injection + if err := manager.ValidatePackageName(pkg); err != nil { + return manager.PackageInfo{}, err + } + cmd := exec.Command("snap", "info", pkg) cmd.Env = ENV_NonInteractive out, err := cmd.Output() diff --git a/manager/yum/utils.go b/manager/yum/utils.go index 979dd0b..15db729 100644 --- a/manager/yum/utils.go +++ b/manager/yum/utils.go @@ -462,6 +462,11 @@ func ParseAutoRemoveOutput(msg string, opts *manager.Options) []manager.PackageI // checkRpmInstallationStatus uses rpm -q to check which packages are installed // Returns a map of installed package names to their PackageInfo using the provided CommandRunner func checkRpmInstallationStatus(packageNames []string, runner manager.CommandRunner) (map[string]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(packageNames); err != nil { + return nil, err + } + installedPackages := make(map[string]manager.PackageInfo) // Check if rpm command is available by trying to run rpm --version diff --git a/manager/yum/yum.go b/manager/yum/yum.go index 91c13c9..1d6557e 100644 --- a/manager/yum/yum.go +++ b/manager/yum/yum.go @@ -121,6 +121,11 @@ func (a *PackageManager) GetPackageManager() string { // - Uses -y flag to automatically answer yes to prompts // - Respects DryRun, Interactive, and Verbose options func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + if opts == nil { opts = &manager.Options{ DryRun: false, @@ -173,6 +178,11 @@ func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manage // - Uses -y flag to automatically answer yes to prompts // - Respects DryRun, Interactive, and Verbose options func (a *PackageManager) Delete(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + if opts == nil { opts = &manager.Options{ DryRun: false, @@ -267,6 +277,11 @@ func (a *PackageManager) Refresh(opts *manager.Options) error { // - Version: Installed version (if installed) or empty // - NewVersion: Available version from repositories func (a *PackageManager) Find(keywords []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate keywords to prevent command injection + if err := manager.ValidatePackageNames(keywords); err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), readTimeout) defer cancel() @@ -336,6 +351,13 @@ func (a *PackageManager) ListUpgradable(opts *manager.Options) ([]manager.Packag // Upgrade upgrades the specified packages using the yum package manager. // Returns PackageInfo for each successfully upgraded package with new version information. func (a *PackageManager) Upgrade(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + // Validate package names to prevent command injection + if len(pkgs) > 0 { + if err := manager.ValidatePackageNames(pkgs); err != nil { + return nil, err + } + } + if opts == nil { opts = &manager.Options{ DryRun: false, @@ -470,6 +492,11 @@ func (a *PackageManager) Clean(opts *manager.Options) error { // PackageStatusAvailable if under "Available Packages" section // - PackageManager: "yum" func (a *PackageManager) GetPackageInfo(pkg string, opts *manager.Options) (manager.PackageInfo, error) { + // Validate package name to prevent command injection + if err := manager.ValidatePackageName(pkg); err != nil { + return manager.PackageInfo{}, err + } + ctx, cancel := context.WithTimeout(context.Background(), readTimeout) defer cancel()