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>
This commit is contained in:
Abhisek Datta
2026-01-01 12:33:52 +05:30
committed by GitHub
co-authored by Copilot Sahilb315
parent 698bd3dd13
commit 20c854e473
25 changed files with 970 additions and 282 deletions
+23 -12
View File
@@ -66,12 +66,23 @@ jobs:
- name: Add pmg to PATH - name: Add pmg to PATH
run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
- name: Setup PMG
run: pmg setup install
- name: Test PMG - Config File is Created
run: |
test -f $HOME/.config/safedep/pmg/config.yml
- name: Test pmg.rc File is Created
run: |
test -f $HOME/.pmg.rc
- name: Test NPM - Single Package & Manifest - name: Test NPM - Single Package & Manifest
run: | run: |
echo "Testing NPM single package installation..." echo "Testing NPM single package installation..."
mkdir npm-test && cd npm-test mkdir npm-test && cd npm-test
pmg npm init -y pmg npm init -y
pmg npm install express pmg npm install express@5.2.1
pmg npm install lodash@4.17.21 pmg npm install lodash@4.17.21
# Verification: npm added packages present and manifest updated # Verification: npm added packages present and manifest updated
@@ -95,7 +106,7 @@ jobs:
echo "Testing PNPM single package installation..." echo "Testing PNPM single package installation..."
mkdir pnpm-test && cd pnpm-test mkdir pnpm-test && cd pnpm-test
pmg pnpm init pmg pnpm init
pmg pnpm add express pmg pnpm add express@5.2.1
pmg pnpm add lodash@4.17.21 pmg pnpm add lodash@4.17.21
# Verification: pnpm packages installed and lockfile created # Verification: pnpm packages installed and lockfile created
@@ -118,7 +129,7 @@ jobs:
echo "Testing Bun single package installation..." echo "Testing Bun single package installation..."
mkdir bun-test && cd bun-test mkdir bun-test && cd bun-test
pmg bun init -y pmg bun init -y
pmg bun add express pmg bun add express@5.2.1
pmg bun add lodash@4.17.21 pmg bun add lodash@4.17.21
# Verification: bun packages installed and lockfile created # Verification: bun packages installed and lockfile created
@@ -145,7 +156,7 @@ jobs:
mkdir yarn-test && cd yarn-test mkdir yarn-test && cd yarn-test
pmg yarn init -y pmg yarn init -y
pmg yarn add express pmg yarn add express@5.2.1
pmg yarn add lodash@4.17.21 pmg yarn add lodash@4.17.21
# Verification: yarn packages installed and lockfile created # Verification: yarn packages installed and lockfile created
@@ -168,8 +179,8 @@ jobs:
echo "Testing Pip single package installation..." echo "Testing Pip single package installation..."
mkdir pip-test && cd pip-test mkdir pip-test && cd pip-test
python -m venv venv && source venv/bin/activate python -m venv venv && source venv/bin/activate
pmg pip install requests pmg pip install requests==2.32.4
pmg pip install numpy==1.24.0 pmg pip install numpy==2.3.5
pmg pip freeze > requirements.txt pmg pip freeze > requirements.txt
# Verification: requirements.txt contains expected packages # Verification: requirements.txt contains expected packages
@@ -191,8 +202,8 @@ jobs:
echo "Testing Pip3 single package installation..." echo "Testing Pip3 single package installation..."
mkdir pip3-test && cd pip3-test mkdir pip3-test && cd pip3-test
python -m venv venv && source venv/bin/activate python -m venv venv && source venv/bin/activate
pmg pip3 install requests pmg pip3 install requests==2.32.4
pmg pip3 install numpy==1.24.0 pmg pip3 install numpy==2.3.5
pmg pip3 freeze > requirements.txt pmg pip3 freeze > requirements.txt
# Verification: requirements.txt contains expected packages # Verification: requirements.txt contains expected packages
@@ -214,8 +225,8 @@ jobs:
echo "Testing UV single package installation..." echo "Testing UV single package installation..."
mkdir uv-test && cd uv-test mkdir uv-test && cd uv-test
pmg uv init --no-readme pmg uv init --no-readme
pmg uv add requests pmg uv add requests==2.32.4
pmg uv add numpy pmg uv add numpy==2.3.5
# Verification: pyproject.toml lists expected dependencies # Verification: pyproject.toml lists expected dependencies
test -f pyproject.toml test -f pyproject.toml
@@ -247,8 +258,8 @@ jobs:
echo "Testing Poetry single package installation..." echo "Testing Poetry single package installation..."
mkdir poetry-test && cd poetry-test mkdir poetry-test && cd poetry-test
pmg poetry init --name poetry-test --no-interaction --quiet pmg poetry init --name poetry-test --no-interaction --quiet
pmg poetry add requests pmg poetry add requests==2.32.4
pmg poetry add numpy pmg poetry add numpy==2.3.5
# Verification: pyproject.toml dependencies updated # Verification: pyproject.toml dependencies updated
test -f pyproject.toml test -f pyproject.toml
+5 -9
View File
@@ -35,25 +35,21 @@ func executeBunFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create bun package manager proxy: %w", err) return fmt.Errorf("failed to create bun package manager proxy: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse command: %w", err) return fmt.Errorf("failed to parse command: %w", err)
} }
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
if err != nil { if err != nil {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+5 -9
View File
@@ -35,25 +35,21 @@ func executeNpmFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create npm package manager proxy: %w", err) return fmt.Errorf("failed to create npm package manager proxy: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse command: %w", err) return fmt.Errorf("failed to parse command: %w", err)
} }
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
if err != nil { if err != nil {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+5 -8
View File
@@ -35,10 +35,7 @@ func executePnpmFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create pnpm package manager proxy: %w", err) return fmt.Errorf("failed to create pnpm package manager proxy: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
@@ -46,14 +43,14 @@ func executePnpmFlow(ctx context.Context, args []string) error {
} }
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
if err != nil { if err != nil {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+5 -9
View File
@@ -35,25 +35,21 @@ func executeYarnFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create yarn package manager proxy: %w", err) return fmt.Errorf("failed to create yarn package manager proxy: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse command: %w", err) return fmt.Errorf("failed to parse command: %w", err)
} }
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
if err != nil { if err != nil {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+5 -9
View File
@@ -35,11 +35,7 @@ func executePipFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create pip package manager proxy: %w", err) return fmt.Errorf("failed to create pip package manager proxy: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse command: %w", err) return fmt.Errorf("failed to parse command: %w", err)
@@ -47,9 +43,9 @@ func executePipFlow(ctx context.Context, args []string) error {
// Parse the args right here // Parse the args right here
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig)
@@ -57,5 +53,5 @@ func executePipFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+5 -9
View File
@@ -35,11 +35,7 @@ func executePip3Flow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create pip3 package manager proxy: %w", err) return fmt.Errorf("failed to create pip3 package manager proxy: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse command: %w", err) return fmt.Errorf("failed to parse command: %w", err)
@@ -47,9 +43,9 @@ func executePip3Flow(ctx context.Context, args []string) error {
// Parse the args right here // Parse the args right here
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig)
@@ -57,5 +53,5 @@ func executePip3Flow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+5 -9
View File
@@ -35,20 +35,16 @@ func executePoetryFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create poetry package manager: %w", err) return fmt.Errorf("failed to create poetry package manager: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse command: %w", err) return fmt.Errorf("failed to parse command: %w", err)
} }
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig)
@@ -56,5 +52,5 @@ func executePoetryFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+5 -9
View File
@@ -35,20 +35,16 @@ func executeUvFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create uv package manager: %w", err) return fmt.Errorf("failed to create uv package manager: %w", err)
} }
config, err := config.FromContext(ctx) config := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
parsedCommand, err := packageManager.ParseCommand(args) parsedCommand, err := packageManager.ParseCommand(args)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse command: %w", err) return fmt.Errorf("failed to parse command: %w", err)
} }
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig() packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig) packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig)
@@ -56,5 +52,5 @@ func executeUvFlow(ctx context.Context, args []string) error {
return fmt.Errorf("failed to create dependency resolver: %w", err) return fmt.Errorf("failed to create dependency resolver: %w", err)
} }
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand) return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
} }
+37 -12
View File
@@ -2,18 +2,24 @@ package setup
import ( import (
"fmt" "fmt"
"os"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/alias" "github.com/safedep/pmg/internal/alias"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/internal/version" "github.com/safedep/pmg/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var (
setupRemoveConfigFile = false
)
func NewSetupCommand() *cobra.Command { func NewSetupCommand() *cobra.Command {
setupCmd := &cobra.Command{ setupCmd := &cobra.Command{
Use: "setup", Use: "setup",
Short: "Manage PMG shell aliases and integration", Short: "Manage PMG shell aliases and integration",
Long: "Setup and manage PMG shell aliases that allow you to use 'npm', 'pnpm', 'pip' commands through PMG's security wrapper.", Long: "Setup and manage PMG config, shell aliases that allow you to use package manager commands with security guardrails.",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help() return cmd.Help()
}, },
@@ -28,38 +34,57 @@ func NewSetupCommand() *cobra.Command {
func NewInstallCommand() *cobra.Command { func NewInstallCommand() *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "install", Use: "install",
Short: "Install PMG aliases for package managers (npm, pnpm, pip)", Short: "Setup PMG config and aliases for package managers (npm, pnpm, pip, and more)",
Long: "Creates ~/.pmg.rc with package manager aliases and sources it in your shell config files (.bashrc, .zshrc, config.fish)",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit)) fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
config := alias.DefaultConfig() cfg := alias.DefaultConfig()
rcFileManager, err := alias.NewDefaultRcFileManager(config.RcFileName) rcFileManager, err := alias.NewDefaultRcFileManager(cfg.RcFileName)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create alias manager: %w", err)
} }
aliasManager := alias.New(config, rcFileManager) aliasManager := alias.New(cfg, rcFileManager)
return aliasManager.Install() err = aliasManager.Install()
if err != nil {
return fmt.Errorf("failed to install aliases: %w", err)
}
if err := config.WriteTemplateConfig(); err != nil {
return fmt.Errorf("failed to write template config: %w", err)
}
return nil
}, },
} }
} }
func NewRemoveCommand() *cobra.Command { func NewRemoveCommand() *cobra.Command {
return &cobra.Command{ cmd := &cobra.Command{
Use: "remove", Use: "remove",
Short: "Removes pmg aliases from the user's shell config file.", Short: "Removes pmg aliases from the user's shell config file.",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit)) fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
config := alias.DefaultConfig() // We remove the config file only if explicitly asked to do so.
rcFileManager, err := alias.NewDefaultRcFileManager(config.RcFileName) if setupRemoveConfigFile {
config := config.Get()
if err := os.Remove(config.ConfigFilePath()); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove config file %q: %w", config.ConfigFilePath(), err)
}
}
cfg := alias.DefaultConfig()
rcFileManager, err := alias.NewDefaultRcFileManager(cfg.RcFileName)
if err != nil { if err != nil {
return err return err
} }
aliasManager := alias.New(config, rcFileManager) aliasManager := alias.New(cfg, rcFileManager)
return aliasManager.Remove() return aliasManager.Remove()
}, },
} }
cmd.Flags().BoolVar(&setupRemoveConfigFile, "config-file", false, "Remove the config file")
return cmd
} }
+22
View File
@@ -0,0 +1,22 @@
package config
import "github.com/spf13/cobra"
// ApplyCobraFlags applies the cobra flags to the command.
// These flags are local concern of the config package. This helper function is used
// to bind them to the Cobra. The default values are taken from the global configuration,
// allowing for overriding the configuration at runtime.
func ApplyCobraFlags(cmd *cobra.Command) {
cmd.PersistentFlags().BoolVar(&globalConfig.Config.Transitive, "transitive",
globalConfig.Config.Transitive, "Resolve transitive dependencies")
cmd.PersistentFlags().IntVar(&globalConfig.Config.TransitiveDepth, "transitive-depth",
globalConfig.Config.TransitiveDepth, "Maximum depth of transitive dependencies to resolve")
cmd.PersistentFlags().BoolVar(&globalConfig.Config.IncludeDevDependencies, "include-dev-dependencies",
globalConfig.Config.IncludeDevDependencies, "Include dev dependencies in the dependency graph (slows down resolution)")
cmd.PersistentFlags().BoolVar(&globalConfig.DryRun, "dry-run",
globalConfig.DryRun, "Dry run skips execution of package manager")
cmd.PersistentFlags().BoolVar(&globalConfig.Config.Paranoid, "paranoid",
globalConfig.Config.Paranoid, "Perform active scanning of unknown packages (slow)")
cmd.PersistentFlags().BoolVar(&globalConfig.Config.SkipEventLogging, "skip-event-log",
globalConfig.Config.SkipEventLogging, "Skip event logging")
}
+226 -21
View File
@@ -1,41 +1,246 @@
package config package config
import ( import (
"context"
"fmt" "fmt"
"os"
"path/filepath"
"runtime"
"strconv"
_ "embed"
) )
type configKey struct{} const (
type contextValue struct { // Environment variable key for the insecure installation flag
Config Config PMG_INSECURE_INSTALLATION_ENV_KEY = "PMG_INSECURE_INSTALLATION"
// Allow overriding the config path from the environment
CONFIG_DIR_ENV_KEY = "PMG_CONFIG_DIR"
// Config path is computed as the user config directory + the default relative path
// when not overridden by the environment variable
CONFIG_DEFAULT_HOME_RELATIVE_PATH = "safedep/pmg"
// Default log directory is relative to the config directory.
CONFIG_DEFAULT_LOG_DIR = "logs"
// Config file name.
// Important: The config file path and the schema should be backward compatible. In case of breaking config
// changes, we must introduce a new file name and a migration path.
CONFIG_FILE_NAME = "config.yml"
)
//go:embed config.template.yml
var templateConfig string
// Config is the global configuration for PMG that can be persisted or loaded from a given source.
// Here we only define the configuration that can be persisted or loaded from a given source and
// not those that we believe should not be persisted (eg. insecure installation, etc.)
type Config struct {
Transitive bool `mapstructure:"transitive"`
TransitiveDepth int `mapstructure:"transitive_depth"`
IncludeDevDependencies bool `mapstructure:"include_dev_dependencies"`
// Paranoid mode enables active scanning of unknown packages for malware.
Paranoid bool `mapstructure:"paranoid"`
// TrustedPackages allows for trusting a suspicious package and ignoring the suspicious behaviour for the package in future installations
TrustedPackages []TrustedPackage `mapstructure:"trusted_packages"`
// SkipEventLogging allows for skipping event logging.
SkipEventLogging bool `mapstructure:"skip_event_logging"`
// EventLogRetentionDays is the number of days to retain event logs.
EventLogRetentionDays int `mapstructure:"event_log_retention_days"`
} }
// Global configuration // TrustedPackage is a package that is trusted by the user and will be ignored by the security guardrails.
type Config struct { type TrustedPackage struct {
Transitive bool Purl string `mapstructure:"purl"`
TransitiveDepth int Reason string `mapstructure:"reason"`
IncludeDevDependencies bool }
Paranoid bool
// DryRun to check for packages for risks. // RuntimeConfig is the configuration that is used at runtime. It contains static configuration
// Do not actually execute any commands. // that can be loaded from a source and, if allowed, overridden by the user at runtime.
type RuntimeConfig struct {
Config Config
// DryRun enables dry-run mode for the package manager, where actual execution of commands is skipped.
DryRun bool DryRun bool
// InsecureInstallation allows bypassing install blocking on malicious packages // InsecureInstallation allows bypassing install blocking on malicious packages
InsecureInstallation bool InsecureInstallation bool
// Internal config values computed at runtime and must be accessed via. API
configDir string
configFilePath string
eventLogDir string
} }
// Inject config into context while protecting against context poisoning // ConfigFilePath returns the path to the config file.
func (c Config) Inject(ctx context.Context) context.Context { func (r *RuntimeConfig) ConfigFilePath() string {
return context.WithValue(ctx, configKey{}, &contextValue{Config: c}) return r.configFilePath
} }
// Extract config from context // EventLogDir returns the path to the event log directory.
func FromContext(ctx context.Context) (Config, error) { func (r *RuntimeConfig) EventLogDir() string {
c, ok := ctx.Value(configKey{}).(*contextValue) return r.eventLogDir
if !ok {
return Config{}, fmt.Errorf("config not found in context")
} }
return c.Config, nil // DefaultConfig is a fail safe contract for the runtime configuration.
// The config package return an appropriate RuntimeConfig based on the environment and the configuration.
func DefaultConfig() RuntimeConfig {
// Backward compatibility for the insecure installation flag before config was introduced.
insecureInstallation := false
if val := os.Getenv(PMG_INSECURE_INSTALLATION_ENV_KEY); val != "" {
if boolVal, err := strconv.ParseBool(val); err == nil {
insecureInstallation = boolVal
}
}
return RuntimeConfig{
Config: Config{
Transitive: true,
TransitiveDepth: 5,
IncludeDevDependencies: false,
Paranoid: false,
EventLogRetentionDays: 7,
SkipEventLogging: false,
TrustedPackages: []TrustedPackage{},
},
DryRun: false,
InsecureInstallation: insecureInstallation,
}
}
// globalConfig is the global configuration for PMG.
// It is initialized in the init function and can be overridden by a repository.
var globalConfig *RuntimeConfig
func init() {
initConfig()
}
// initConfig should be idempotent and can be called multiple times.
// This is required for testing purposes.
func initConfig() {
defaultConfig := DefaultConfig()
globalConfig = &defaultConfig
configDir, err := configDir()
if err != nil {
panic(fmt.Errorf("failed to get config directory: %w", err))
}
configFilePath, err := configFilePath()
if err != nil {
panic(fmt.Errorf("failed to get config file path: %w", err))
}
eventLogDir, err := eventLogDir()
if err != nil {
panic(fmt.Errorf("failed to get event log directory: %w", err))
}
globalConfig.configDir = configDir
globalConfig.configFilePath = configFilePath
globalConfig.eventLogDir = eventLogDir
loadConfig()
}
// loadConfig loads the configuration from the config file.
// This is where we determine the source of config and use the appropriate loader.
// Right now we only support loading from a config file using Viper. All loader
// functions should be safe with reasonable defaults and panic only in case of system errors.
func loadConfig() {
loadViperConfig()
}
// configDir computes the path to the config directory.
func configDir() (string, error) {
dir := os.Getenv(CONFIG_DIR_ENV_KEY)
if dir != "" {
return dir, nil
}
userConfigDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("failed to retrieve user config directory: %w", err)
}
return filepath.Join(userConfigDir, CONFIG_DEFAULT_HOME_RELATIVE_PATH), nil
}
// configFilePath computes the path to the config file.
func configFilePath() (string, error) {
configDir, err := configDir()
if err != nil {
return "", fmt.Errorf("failed to get config directory: %w", err)
}
return filepath.Join(configDir, CONFIG_FILE_NAME), nil
}
// eventLogDir computes the path to the event log directory.
func eventLogDir() (string, error) {
// For rationale on why different directory for Windows, see:
// https://github.com/safedep/pmg/pull/82#discussion_r2636746036
switch runtime.GOOS {
case "windows":
// Windows: %LOCALAPPDATA%\safedep\pmg\logs or %USERPROFILE%\safedep\pmg\logs
baseDir := os.Getenv("LOCALAPPDATA")
if baseDir == "" {
baseDir = os.Getenv("USERPROFILE")
if baseDir == "" {
return "", fmt.Errorf("could not determine Windows user directory for event log storage")
}
}
return filepath.Join(baseDir, CONFIG_DEFAULT_HOME_RELATIVE_PATH, CONFIG_DEFAULT_LOG_DIR), nil
case "darwin", "linux":
configDir, err := configDir()
if err != nil {
return "", fmt.Errorf("failed to get config directory: %w", err)
}
return filepath.Join(configDir, CONFIG_DEFAULT_LOG_DIR), nil
default:
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
}
// Get returns the global configuration.
// This is the public API for the configuration package. This package should guarantee
// that this function will never return nil.
func Get() *RuntimeConfig {
return globalConfig
}
// WriteTemplateConfig writes the template configuration file to disk if it doesn't already exist.
func WriteTemplateConfig() error {
configDir, err := configDir()
if err != nil {
return fmt.Errorf("failed to get config directory: %w", err)
}
if err := os.MkdirAll(configDir, 0o755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
configFilePath, err := configFilePath()
if err != nil {
return fmt.Errorf("failed to get config file path: %w", err)
}
// Do not overwrite the config file if it already exists
if _, err := os.Stat(configFilePath); err == nil {
return nil
}
if err := os.WriteFile(configFilePath, []byte(templateConfig), 0o644); err != nil {
return fmt.Errorf("failed to write template config: %w", err)
}
return nil
} }
+42
View File
@@ -0,0 +1,42 @@
# PMG configuration template. Customize this file as needed.
# https://github.com/safedep/pmg
# Enable transitive dependency resolution. Default is true.
transitive: true
# Maximum depth of transitive dependencies to resolve. Default is 5.
transitive_depth: 5
# Include dev dependencies in the dependency graph. Default is false.
include_dev_dependencies: false
# Enable paranoid mode. In paranoid mode, PMG will use SafeDep Cloud credentials
# to scan unknown packages for malware. This is slow and should be used with caution.
paranoid: false
# Skip event logging. Default is false.
# When event logging is enabled, all events will be logged to file. These events are useful for audit
# trail and incident response on systems using PMG. Set this config to true to skip event logging.
skip_event_logging: false
# Event log retention days. Default is 7.
# This is the number of days to retain event logs.
event_log_retention_days: 7
# Trusted packages are packages that are trusted by the user and will be ignored by the security guardrails.
# This is useful for packages that are known to be safe and are used in the application.
# Example:
# - purl: pkg:npm/@safedep/pmg
# reason: "PMG is a trusted package for PMG"
#
# When a package is specified without an explicit version, all versions of the package will be trusted.
# This feature should be used with caution and should be used for minimal set of packages.
#
# When a package is specified with an explicit version, only that version will be trusted.
#
# The purl is the package identifier and the reason is the reason for trusting the package.
# PURL specification: https://github.com/package-url/purl-spec
trusted_packages:
- purl: pkg:npm/@safedep/pmg
reason: "PMG is a trusted package for PMG"
+45
View File
@@ -0,0 +1,45 @@
package config
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfigIsNeverNil(t *testing.T) {
config := Get()
assert.NotNil(t, config)
}
func TestConfigHasDefaultValues(t *testing.T) {
t.Run("with non-existent config directory", func(t *testing.T) {
t.Setenv("PMG_CONFIG_DIR", "/tmp/pmg-test/random-does-not-exist")
initConfig()
config := Get()
assert.Equal(t, true, config.Config.Transitive)
assert.Equal(t, 5, config.Config.TransitiveDepth)
assert.Equal(t, false, config.Config.IncludeDevDependencies)
assert.Equal(t, false, config.Config.Paranoid)
assert.Equal(t, []TrustedPackage{}, config.Config.TrustedPackages)
assert.Equal(t, "/tmp/pmg-test/random-does-not-exist", config.configDir)
assert.Equal(t, "/tmp/pmg-test/random-does-not-exist/config.yml", config.configFilePath)
})
t.Run("when no config directory is set", func(t *testing.T) {
t.Setenv("PMG_CONFIG_DIR", "")
initConfig()
config := Get()
userConfigDir, err := os.UserConfigDir()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, filepath.Join(userConfigDir, "safedep/pmg"), config.configDir)
assert.Equal(t, filepath.Join(userConfigDir, "safedep/pmg/config.yml"), config.configFilePath)
})
}
+43
View File
@@ -0,0 +1,43 @@
package config
import (
"fmt"
"os"
"strings"
"github.com/spf13/viper"
)
// loadViperConfig loads the configuration using Viper if available.
// This function will panic for system errors since it is part of the init path.
func loadViperConfig() {
configPath, err := configFilePath()
if err != nil {
panic(fmt.Errorf("failed to get config file path: %w", err))
}
// Check if config file exists before attempting to load
// If it doesn't exist, we use the default configuration (see config.go)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return
}
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
v.SetEnvPrefix("PMG")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
if err := v.ReadInConfig(); err != nil {
panic(fmt.Errorf("failed to read config file %s: %w", configPath, err))
}
var loadedConfig Config
if err := v.Unmarshal(&loadedConfig); err != nil {
panic(fmt.Errorf("failed to unmarshal config: %w", err))
}
globalConfig.Config = loadedConfig
}
+21 -16
View File
@@ -13,12 +13,15 @@ require (
github.com/google/osv-scalibr v0.2.1 github.com/google/osv-scalibr v0.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/mitchellh/mapstructure v1.5.0
github.com/posthog/posthog-go v1.5.12 github.com/posthog/posthog-go v1.5.12
github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175 github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6 github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.10.0 github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
google.golang.org/grpc v1.72.0 google.golang.org/grpc v1.72.0
google.golang.org/protobuf v1.36.6
) )
require ( require (
@@ -81,7 +84,7 @@ require (
github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.12.1 // indirect
@@ -160,7 +163,7 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/ryancurrah/gomodguard v1.3.5 // indirect github.com/ryancurrah/gomodguard v1.3.5 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
@@ -170,11 +173,10 @@ require (
github.com/sivchari/containedctx v1.0.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/tenv v1.12.1 // indirect github.com/sivchari/tenv v1.12.1 // indirect
github.com/sonatard/noctx v0.1.0 // indirect github.com/sonatard/noctx v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.14.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
@@ -205,17 +207,19 @@ require (
go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.24.0 // indirect golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.13.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.31.0 // indirect golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.32.0 // indirect golang.org/x/tools v0.35.0 // indirect
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -223,4 +227,5 @@ require (
honnef.co/go/tools v0.6.1 // indirect honnef.co/go/tools v0.6.1 // indirect
mvdan.cc/gofumpt v0.7.0 // indirect mvdan.cc/gofumpt v0.7.0 // indirect
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
) )
+41 -29
View File
@@ -158,8 +158,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -196,6 +196,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o= github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o=
@@ -307,6 +308,8 @@ github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY=
github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -382,8 +385,8 @@ github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9f
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175 h1:TxAI6m/v01CL+kwIYE3RZsuxu01pbuGy3wOi3WyBT1E= github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175 h1:TxAI6m/v01CL+kwIYE3RZsuxu01pbuGy3wOi3WyBT1E=
github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175/go.mod h1:Mdqx/Q2DhAcN38XiUNTGCC5MktofYDQW9Az7YWGEF0s= github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175/go.mod h1:Mdqx/Q2DhAcN38XiUNTGCC5MktofYDQW9Az7YWGEF0s=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
@@ -405,21 +408,22 @@ github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY=
github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw=
github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM=
github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4=
@@ -438,8 +442,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
@@ -517,6 +521,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -545,8 +551,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -566,8 +572,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -580,8 +586,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -606,8 +612,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -616,8 +622,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -628,8 +634,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -655,8 +661,12 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -701,3 +711,5 @@ mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+50 -6
View File
@@ -10,8 +10,10 @@ import (
"time" "time"
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/dry/api/pb"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/extractor" "github.com/safedep/pmg/extractor"
"github.com/safedep/pmg/internal/eventlog" "github.com/safedep/pmg/internal/eventlog"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
@@ -43,6 +45,7 @@ type PackageManagerGuardConfig struct {
AnalysisTimeout time.Duration AnalysisTimeout time.Duration
DryRun bool DryRun bool
InsecureInstallation bool InsecureInstallation bool
TrustedPackages []config.TrustedPackage
} }
func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig { func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
@@ -52,9 +55,45 @@ func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
AnalysisTimeout: 5 * time.Minute, AnalysisTimeout: 5 * time.Minute,
DryRun: false, DryRun: false,
InsecureInstallation: false, InsecureInstallation: false,
TrustedPackages: []config.TrustedPackage{},
} }
} }
func (c *PackageManagerGuardConfig) IsTrustedPackageVersion(pkgVersion *packagev1.PackageVersion) bool {
if pkgVersion == nil {
return false
}
trustedPkgs := c.TrustedPackages
if len(trustedPkgs) == 0 {
return false
}
for _, v := range trustedPkgs {
purlTrustedPackageVersion, err := pb.NewPurlPackageVersion(v.Purl)
if err != nil {
log.Warnf("failed to parse trusted package version: %s: %v", v.Purl, err)
continue
}
if purlTrustedPackageVersion.Version() != "" && purlTrustedPackageVersion.Version() != pkgVersion.GetVersion() {
continue
}
if purlTrustedPackageVersion.Name() != pkgVersion.GetPackage().GetName() {
continue
}
if purlTrustedPackageVersion.Ecosystem() != pkgVersion.GetPackage().GetEcosystem() {
continue
}
return true
}
return false
}
type packageManagerGuard struct { type packageManagerGuard struct {
config PackageManagerGuardConfig config PackageManagerGuardConfig
interaction PackageManagerGuardInteraction interaction PackageManagerGuardInteraction
@@ -226,7 +265,6 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context, func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
packages []*packagev1.PackageVersion) ([]*analyzer.PackageVersionAnalysisResult, error) { packages []*packagev1.PackageVersion) ([]*analyzer.PackageVersionAnalysisResult, error) {
ctx, cancel := context.WithTimeout(ctx, g.config.AnalysisTimeout) ctx, cancel := context.WithTimeout(ctx, g.config.AnalysisTimeout)
defer cancel() defer cancel()
@@ -255,8 +293,16 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
// Queue all packages for analysis // Queue all packages for analysis
for _, pkg := range packages { for _, pkg := range packages {
if g.config.IsTrustedPackageVersion(pkg) {
log.Debugf("Skipping trusted package: %s/%s@%s",
pkg.GetPackage().GetEcosystem(), pkg.GetPackage().GetName(), pkg.GetVersion())
continue
}
jobs <- pkg jobs <- pkg
} }
close(jobs) close(jobs)
analysisResults := []*analyzer.PackageVersionAnalysisResult{} analysisResults := []*analyzer.PackageVersionAnalysisResult{}
@@ -351,13 +397,12 @@ func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, pa
log.Debugf("Extracted %d packages from manifest files", len(packages)) log.Debugf("Extracted %d packages from manifest files", len(packages))
packagesToAnalyze := []*packagev1.PackageVersion{} packagesToAnalyze := []*packagev1.PackageVersion{}
for _, pkg := range packages {
packagesToAnalyze = append(packagesToAnalyze, pkg) // Add all packages to analyze that are extracted from manifest files
} packagesToAnalyze = append(packagesToAnalyze, packages...)
// Only resolve dependencies for requirements.txt because other lockfiles dependencies are already resolved // Only resolve dependencies for requirements.txt because other lockfiles dependencies are already resolved
if g.config.ResolveDependencies && slices.Contains(parsedCommand.ManifestFiles, "requirements.txt") { if g.config.ResolveDependencies && slices.Contains(parsedCommand.ManifestFiles, "requirements.txt") {
g.setStatus(fmt.Sprintf("Resolving dependencies for %d package(s)", len(packages))) g.setStatus(fmt.Sprintf("Resolving dependencies for %d package(s)", len(packages)))
for _, pkg := range packages { for _, pkg := range packages {
@@ -458,7 +503,6 @@ func (g *packageManagerGuard) logMalwareDetection(result *analyzer.PackageVersio
details := map[string]interface{}{ details := map[string]interface{}{
"analysis_id": result.AnalysisID, "analysis_id": result.AnalysisID,
"reference_url": result.ReferenceURL, "reference_url": result.ReferenceURL,
"summary": result.Summary,
} }
if blocked { if blocked {
+260
View File
@@ -6,6 +6,7 @@ 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/config"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager" "github.com/safedep/pmg/packagemanager"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -268,3 +269,262 @@ func TestGuardInsecureInstallation(t *testing.T) {
assert.False(t, config.InsecureInstallation, "InsecureInstallation should default 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)
})
}
}
+2 -1
View File
@@ -198,6 +198,7 @@ func (a *AliasManager) removeSourceLinesFromShells() error {
if err != nil { if err != nil {
continue continue
} }
tempPath := tempFile.Name() tempPath := tempFile.Name()
// Write filtered content // Write filtered content
@@ -222,7 +223,7 @@ func (a *AliasManager) removeSourceLinesFromShells() error {
// Replace original file // Replace original file
os.Chmod(tempPath, info.Mode()) os.Chmod(tempPath, info.Mode())
if err := os.Rename(tempPath, configPath); err != nil { if err := os.Rename(tempPath, configPath); err != nil {
os.Remove(tempPath) // cleanup on failure os.Remove(tempPath)
log.Warnf("Warning: failed to update %s: %s", configPath, err) log.Warnf("Warning: failed to update %s: %s", configPath, err)
} }
} }
+1 -1
View File
@@ -11,5 +11,5 @@ type Shell interface {
var commentForRemovingShellSource = "# remove aliases by running `pmg setup remove` or deleting the line" var commentForRemovingShellSource = "# remove aliases by running `pmg setup remove` or deleting the line"
func defaultShellSource(rcPath string) string { func defaultShellSource(rcPath string) string {
return fmt.Sprintf("%s \n[ -f %s ] && source %s # PMG source aliases\n", commentForRemovingShellSource, rcPath, rcPath) return fmt.Sprintf("%s \n[ -f '%s' ] && source '%s' # PMG source aliases\n", commentForRemovingShellSource, rcPath, rcPath)
} }
+68 -47
View File
@@ -6,11 +6,11 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"sync" "sync"
"time" "time"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/config"
) )
// EventType represents the type of event being logged // EventType represents the type of event being logged
@@ -36,73 +36,84 @@ type Event struct {
Details map[string]interface{} `json:"details,omitempty"` Details map[string]interface{} `json:"details,omitempty"`
} }
// Logger represents an event logger // Logger defines the contract for implementing event loggers.
type Logger struct { type Logger interface {
// Log writes an event to the log file
Log(event Event) error
// Close closes the logger
Close() error
// IsActive returns whether the logger is active
IsActive() bool
}
// fileWithRotationLogger represents an event logger that writes to a file and rotates the
// file when it reaches a certain age
type fileWithRotationLogger struct {
file *os.File file *os.File
writer io.Writer writer io.Writer
mu sync.Mutex mu sync.Mutex
active bool active bool
} }
// fileWithRotationLogger implements the Logger interface. This is the default logger
// that will be used. Future enhancements will introduce additional and optional loggers.
var _ Logger = &fileWithRotationLogger{}
var ( var (
globalLogger *Logger globalLogger Logger
once sync.Once once sync.Once
) )
// GetDefaultLogDir returns the default log directory based on the OS // GetDefaultLogDir returns the default log directory based on the OS
func GetDefaultLogDir() (string, error) { func GetDefaultLogDir() (string, error) {
var baseDir string return config.Get().EventLogDir(), nil
switch runtime.GOOS {
case "windows":
// Windows: %LOCALAPPDATA%\pmg\logs or %USERPROFILE%\.pmg\logs
baseDir = os.Getenv("LOCALAPPDATA")
if baseDir == "" {
baseDir = os.Getenv("USERPROFILE")
if baseDir == "" {
return "", fmt.Errorf("could not determine Windows user directory")
}
return filepath.Join(baseDir, ".pmg", "logs"), nil
}
return filepath.Join(baseDir, "pmg", "logs"), nil
case "darwin", "linux":
// macOS and Linux: ~/.pmg/logs
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("could not determine home directory: %w", err)
}
return filepath.Join(homeDir, ".pmg", "logs"), nil
default:
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
} }
// Initialize sets up the global event logger with the default log directory // Initialize sets up the global event logger with the default log directory
func Initialize() error { func Initialize() error {
if config.Get().Config.SkipEventLogging {
return nil
}
logDir, err := GetDefaultLogDir() logDir, err := GetDefaultLogDir()
if err != nil { if err != nil {
return err return fmt.Errorf("failed to get default log directory: %w", err)
} }
return InitializeWithDir(logDir) return InitializeWithDir(logDir)
} }
// InitializeWithFile sets up the global event logger with a specific file path // InitializeWithFile sets up the global event logger with a specific file path
func InitializeWithFile(filePath string) error { func InitializeWithFile(filePath string) error {
if config.Get().Config.SkipEventLogging {
return nil
}
var initErr error var initErr error
once.Do(func() { once.Do(func() {
globalLogger = &Logger{} fwrl := &fileWithRotationLogger{}
initErr = globalLogger.initWithFile(filePath) initErr = fwrl.initWithFile(filePath)
globalLogger = fwrl
}) })
return initErr return initErr
} }
// InitializeWithDir sets up the global event logger with a custom log directory // InitializeWithDir sets up the global event logger with a custom log directory
func InitializeWithDir(logDir string) error { func InitializeWithDir(logDir string) error {
if config.Get().Config.SkipEventLogging {
return nil
}
var initErr error var initErr error
once.Do(func() { once.Do(func() {
globalLogger = &Logger{} fwrl := &fileWithRotationLogger{}
initErr = globalLogger.init(logDir) initErr = fwrl.init(logDir)
globalLogger = fwrl
}) })
return initErr return initErr
} }
@@ -122,7 +133,7 @@ func reinitializeForTest(logDir string) error {
} }
// init initializes the logger with the specified directory // init initializes the logger with the specified directory
func (l *Logger) init(logDir string) error { func (l *fileWithRotationLogger) init(logDir string) error {
// Create log directory if it doesn't exist // Create log directory if it doesn't exist
if err := os.MkdirAll(logDir, 0755); err != nil { if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %w", err) return fmt.Errorf("failed to create log directory: %w", err)
@@ -148,7 +159,7 @@ func (l *Logger) init(logDir string) error {
} }
// initWithFile initializes the logger with a specific file path // initWithFile initializes the logger with a specific file path
func (l *Logger) initWithFile(filePath string) error { func (l *fileWithRotationLogger) initWithFile(filePath string) error {
// Create directory if it doesn't exist // Create directory if it doesn't exist
dir := filepath.Dir(filePath) dir := filepath.Dir(filePath)
if dir != "" && dir != "." { if dir != "" && dir != "." {
@@ -172,8 +183,8 @@ func (l *Logger) initWithFile(filePath string) error {
} }
// cleanupOldLogs removes log files older than 7 days // cleanupOldLogs removes log files older than 7 days
func (l *Logger) cleanupOldLogs(logDir string) { func (l *fileWithRotationLogger) cleanupOldLogs(logDir string) {
cutoff := time.Now().AddDate(0, 0, -7) cutoff := time.Now().AddDate(0, 0, -1*config.Get().Config.EventLogRetentionDays)
entries, err := os.ReadDir(logDir) entries, err := os.ReadDir(logDir)
if err != nil { if err != nil {
@@ -196,17 +207,20 @@ func (l *Logger) cleanupOldLogs(logDir string) {
filePath := filepath.Join(logDir, name) filePath := filepath.Join(logDir, name)
info, err := entry.Info() info, err := entry.Info()
if err != nil { if err != nil {
log.Warnf("Failed to get info for log file: %v", err)
continue continue
} }
if info.ModTime().Before(cutoff) { if info.ModTime().Before(cutoff) {
os.Remove(filePath) if err := os.Remove(filePath); err != nil {
log.Warnf("Failed to remove old log file: %v", err)
}
} }
} }
} }
// Log writes an event to the log file // Log writes an event to the log file
func (l *Logger) Log(event Event) error { func (l *fileWithRotationLogger) Log(event Event) error {
if !l.active { if !l.active {
return nil return nil
} }
@@ -240,7 +254,7 @@ func (l *Logger) Log(event Event) error {
} }
// Close closes the logger // Close closes the logger
func (l *Logger) Close() error { func (l *fileWithRotationLogger) Close() error {
if !l.active { if !l.active {
return nil return nil
} }
@@ -255,14 +269,20 @@ func (l *Logger) Close() error {
return nil return nil
} }
// IsActive returns whether the logger is active
func (l *fileWithRotationLogger) IsActive() bool {
return l.active
}
// Global logging functions // Global logging functions
// LogEvent logs an event using the global logger // LogEvent logs an event using the global logger
func LogEvent(event Event) error { func LogEvent(event Event) error {
if globalLogger == nil || !globalLogger.active {
// If logger is not initialized or not active, silently fail // If logger is not initialized or not active, silently fail
if globalLogger == nil || !globalLogger.IsActive() {
return nil return nil
} }
return globalLogger.Log(event) return globalLogger.Log(event)
} }
@@ -276,9 +296,11 @@ func LogMalwareBlocked(packageName, version, ecosystem, reason string, details m
Ecosystem: ecosystem, Ecosystem: ecosystem,
Details: details, Details: details,
} }
if details == nil { if details == nil {
event.Details = make(map[string]interface{}) event.Details = make(map[string]interface{})
} }
event.Details["reason"] = reason event.Details["reason"] = reason
LogEvent(event) LogEvent(event)
} }
@@ -292,6 +314,7 @@ func LogMalwareConfirmed(packageName, version, ecosystem string) {
Version: version, Version: version,
Ecosystem: ecosystem, Ecosystem: ecosystem,
} }
LogEvent(event) LogEvent(event)
} }
@@ -307,6 +330,7 @@ func LogInstallAllowed(packageName, version, ecosystem string, packageCount int)
"packages_analyzed": packageCount, "packages_analyzed": packageCount,
}, },
} }
LogEvent(event) LogEvent(event)
} }
@@ -320,6 +344,7 @@ func LogInstallStarted(packageManager string, args []string) {
"arguments": args, "arguments": args,
}, },
} }
LogEvent(event) LogEvent(event)
} }
@@ -332,6 +357,7 @@ func LogError(message string, err error) {
"error": err.Error(), "error": err.Error(),
}, },
} }
LogEvent(event) LogEvent(event)
} }
@@ -340,11 +366,6 @@ func Close() error {
if globalLogger != nil { if globalLogger != nil {
return globalLogger.Close() return globalLogger.Close()
} }
return nil return nil
} }
// IsInitialized returns whether the global logger is initialized
func IsInitialized() bool {
return globalLogger != nil && globalLogger.active
}
+8 -15
View File
@@ -7,29 +7,22 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
func TestGetDefaultLogDir(t *testing.T) { func TestGetDefaultLogDir(t *testing.T) {
logDir, err := GetDefaultLogDir() logDir, err := GetDefaultLogDir()
if err != nil { assert.NoError(t, err, "failed to get default log directory")
t.Fatalf("GetDefaultLogDir() failed: %v", err)
}
if logDir == "" { assert.NotEmpty(t, logDir, "log directory should not be empty")
t.Error("Expected non-empty log directory") assert.Contains(t, logDir, "safedep/pmg/logs")
}
// Check that it contains expected path components
expectedDir := ".pmg"
if filepath.Base(filepath.Dir(logDir)) != expectedDir && filepath.Base(filepath.Dir(filepath.Dir(logDir))) != expectedDir {
t.Errorf("Expected log directory to contain %s, got: %s", expectedDir, logDir)
}
} }
func TestLoggerInitialization(t *testing.T) { func TestLoggerInitialization(t *testing.T) {
// Create a temporary directory for testing // Create a temporary directory for testing
tmpDir := t.TempDir() tmpDir := t.TempDir()
logDir := filepath.Join(tmpDir, ".pmg", "logs") logDir := filepath.Join(tmpDir, "pmg", "logs")
// Initialize logger // Initialize logger
err := InitializeWithDir(logDir) err := InitializeWithDir(logDir)
@@ -231,11 +224,12 @@ func TestCleanupOldLogs(t *testing.T) {
} }
// Initialize logger (which triggers cleanup) // Initialize logger (which triggers cleanup)
logger := &Logger{} logger := &fileWithRotationLogger{}
err = logger.init(logDir) err = logger.init(logDir)
if err != nil { if err != nil {
t.Fatalf("Failed to initialize logger: %v", err) t.Fatalf("Failed to initialize logger: %v", err)
} }
defer logger.Close() defer logger.Close()
// Give cleanup goroutine time to run // Give cleanup goroutine time to run
@@ -251,4 +245,3 @@ func TestCleanupOldLogs(t *testing.T) {
t.Error("Recent log file should still exist") t.Error("Recent log file should still exist")
} }
} }
+6 -6
View File
@@ -14,24 +14,23 @@ import (
type commonFlow struct { type commonFlow struct {
pm packagemanager.PackageManager pm packagemanager.PackageManager
packageResolver packagemanager.PackageResolver packageResolver packagemanager.PackageResolver
config config.Config
} }
// Creates a common flow of execution for all package managers. This should work for most // Creates a common flow of execution for all package managers. This should work for most
// of the cases unless a package manager has its own unique requirements. Configuration // of the cases unless a package manager has its own unique requirements. Configuration
// should be passed through the context (Global Config) // should be passed through the context (Global Config)
func Common(pm packagemanager.PackageManager, pkgResolver packagemanager.PackageResolver, config config.Config) *commonFlow { func Common(pm packagemanager.PackageManager, pkgResolver packagemanager.PackageResolver) *commonFlow {
return &commonFlow{ return &commonFlow{
pm: pm, pm: pm,
packageResolver: pkgResolver, packageResolver: pkgResolver,
config: config,
} }
} }
func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) error { func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) error {
var analyzers []analyzer.PackageVersionAnalyzer var analyzers []analyzer.PackageVersionAnalyzer
config := config.Get()
if f.config.Paranoid { if config.Config.Paranoid {
malysisActiveScanAnalyzer, err := analyzer.NewMalysisActiveScanAnalyzer(analyzer.DefaultMalysisActiveScanAnalyzerConfig()) malysisActiveScanAnalyzer, err := analyzer.NewMalysisActiveScanAnalyzer(analyzer.DefaultMalysisActiveScanAnalyzerConfig())
if err != nil { if err != nil {
return fmt.Errorf("failed to create malware analyzer: %s", err) return fmt.Errorf("failed to create malware analyzer: %s", err)
@@ -56,8 +55,9 @@ func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagem
} }
guardConfig := guard.DefaultPackageManagerGuardConfig() guardConfig := guard.DefaultPackageManagerGuardConfig()
guardConfig.DryRun = f.config.DryRun guardConfig.DryRun = config.DryRun
guardConfig.InsecureInstallation = f.config.InsecureInstallation guardConfig.InsecureInstallation = config.InsecureInstallation
guardConfig.TrustedPackages = config.Config.TrustedPackages
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 {
+11 -21
View File
@@ -3,7 +3,6 @@ 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"
@@ -23,7 +22,6 @@ var (
silent bool silent bool
verbose bool verbose bool
logFile string logFile string
globalConfig config.Config
) )
func main() { func main() {
@@ -49,8 +47,7 @@ func main() {
} }
if silent && verbose { if silent && verbose {
fmt.Println("pmg: --silent and --verbose cannot be used together") ui.Fatalf("pmg: --silent and --verbose cannot be used together")
os.Exit(1)
} }
if silent { if silent {
@@ -59,25 +56,21 @@ 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")
// Initialize event logging (silently fail if it can't be initialized) // Initialize event logging (silently fail if it can't be initialized)
var eventlogErr error
if logFile != "" { if logFile != "" {
// If a custom log file is specified, use it for event logging too // If a custom log file is specified, use it for event logging too
_ = eventlog.InitializeWithFile(logFile) eventlogErr = eventlog.InitializeWithFile(logFile)
} else { } else {
// Otherwise use the default log directory // Otherwise use the default log directory
_ = eventlog.Initialize() eventlogErr = eventlog.Initialize()
} }
cmd.SetContext(globalConfig.Inject(cmd.Context())) if eventlogErr != nil {
ui.Fatalf("failed to initialize event logging: %v", eventlogErr)
}
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
@@ -92,13 +85,10 @@ func main() {
cmd.PersistentFlags().BoolVar(&silent, "silent", false, "Silent mode for invisible experience") cmd.PersistentFlags().BoolVar(&silent, "silent", false, "Silent mode for invisible experience")
cmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Verbose mode for more information") cmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Verbose mode for more information")
cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging (defaults to stdout)") cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging (defaults to stdout)")
cmd.PersistentFlags().BoolVar(&globalConfig.Transitive, "transitive", true, "Resolve transitive dependencies")
cmd.PersistentFlags().IntVar(&globalConfig.TransitiveDepth, "transitive-depth", 5, // Apply config flags to the command. This allows for overriding the configuration at runtime
"Maximum depth of transitive dependencies to resolve") // using the command line.
cmd.PersistentFlags().BoolVar(&globalConfig.IncludeDevDependencies, "include-dev-dependencies", false, config.ApplyCobraFlags(cmd)
"Include dev dependencies in the dependency graph (slows down resolution)")
cmd.PersistentFlags().BoolVar(&globalConfig.DryRun, "dry-run", false, "Dry run skips execution of package manager")
cmd.PersistentFlags().BoolVar(&globalConfig.Paranoid, "paranoid", false, "Perform active scanning of unknown packages (slow)")
cmd.AddCommand(npm.NewNpmCommand()) cmd.AddCommand(npm.NewNpmCommand())
cmd.AddCommand(npm.NewPnpmCommand()) cmd.AddCommand(npm.NewPnpmCommand())