mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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:
co-authored by
Copilot
Sahilb315
parent
698bd3dd13
commit
20c854e473
@@ -66,12 +66,23 @@ jobs:
|
||||
- name: Add pmg to 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
|
||||
run: |
|
||||
echo "Testing NPM single package installation..."
|
||||
mkdir npm-test && cd npm-test
|
||||
pmg npm init -y
|
||||
pmg npm install express
|
||||
pmg npm install express@5.2.1
|
||||
pmg npm install lodash@4.17.21
|
||||
|
||||
# Verification: npm added packages present and manifest updated
|
||||
@@ -95,7 +106,7 @@ jobs:
|
||||
echo "Testing PNPM single package installation..."
|
||||
mkdir pnpm-test && cd pnpm-test
|
||||
pmg pnpm init
|
||||
pmg pnpm add express
|
||||
pmg pnpm add express@5.2.1
|
||||
pmg pnpm add lodash@4.17.21
|
||||
|
||||
# Verification: pnpm packages installed and lockfile created
|
||||
@@ -118,7 +129,7 @@ jobs:
|
||||
echo "Testing Bun single package installation..."
|
||||
mkdir bun-test && cd bun-test
|
||||
pmg bun init -y
|
||||
pmg bun add express
|
||||
pmg bun add express@5.2.1
|
||||
pmg bun add lodash@4.17.21
|
||||
|
||||
# Verification: bun packages installed and lockfile created
|
||||
@@ -145,7 +156,7 @@ jobs:
|
||||
|
||||
mkdir yarn-test && cd yarn-test
|
||||
pmg yarn init -y
|
||||
pmg yarn add express
|
||||
pmg yarn add express@5.2.1
|
||||
pmg yarn add lodash@4.17.21
|
||||
|
||||
# Verification: yarn packages installed and lockfile created
|
||||
@@ -168,8 +179,8 @@ jobs:
|
||||
echo "Testing Pip single package installation..."
|
||||
mkdir pip-test && cd pip-test
|
||||
python -m venv venv && source venv/bin/activate
|
||||
pmg pip install requests
|
||||
pmg pip install numpy==1.24.0
|
||||
pmg pip install requests==2.32.4
|
||||
pmg pip install numpy==2.3.5
|
||||
pmg pip freeze > requirements.txt
|
||||
|
||||
# Verification: requirements.txt contains expected packages
|
||||
@@ -191,8 +202,8 @@ jobs:
|
||||
echo "Testing Pip3 single package installation..."
|
||||
mkdir pip3-test && cd pip3-test
|
||||
python -m venv venv && source venv/bin/activate
|
||||
pmg pip3 install requests
|
||||
pmg pip3 install numpy==1.24.0
|
||||
pmg pip3 install requests==2.32.4
|
||||
pmg pip3 install numpy==2.3.5
|
||||
pmg pip3 freeze > requirements.txt
|
||||
|
||||
# Verification: requirements.txt contains expected packages
|
||||
@@ -214,8 +225,8 @@ jobs:
|
||||
echo "Testing UV single package installation..."
|
||||
mkdir uv-test && cd uv-test
|
||||
pmg uv init --no-readme
|
||||
pmg uv add requests
|
||||
pmg uv add numpy
|
||||
pmg uv add requests==2.32.4
|
||||
pmg uv add numpy==2.3.5
|
||||
|
||||
# Verification: pyproject.toml lists expected dependencies
|
||||
test -f pyproject.toml
|
||||
@@ -247,8 +258,8 @@ jobs:
|
||||
echo "Testing Poetry single package installation..."
|
||||
mkdir poetry-test && cd poetry-test
|
||||
pmg poetry init --name poetry-test --no-interaction --quiet
|
||||
pmg poetry add requests
|
||||
pmg poetry add numpy
|
||||
pmg poetry add requests==2.32.4
|
||||
pmg poetry add numpy==2.3.5
|
||||
|
||||
# Verification: pyproject.toml dependencies updated
|
||||
test -f pyproject.toml
|
||||
|
||||
+5
-9
@@ -35,25 +35,21 @@ func executeBunFlow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create bun package manager proxy: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
config := config.Get()
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse command: %w", err)
|
||||
}
|
||||
|
||||
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
|
||||
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
|
||||
if err != nil {
|
||||
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
@@ -35,25 +35,21 @@ func executeNpmFlow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create npm package manager proxy: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
config := config.Get()
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse command: %w", err)
|
||||
}
|
||||
|
||||
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
|
||||
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
|
||||
if err != nil {
|
||||
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
@@ -35,10 +35,7 @@ func executePnpmFlow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create pnpm package manager proxy: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
config := config.Get()
|
||||
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
@@ -46,14 +43,14 @@ func executePnpmFlow(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
|
||||
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
|
||||
if err != nil {
|
||||
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
@@ -35,25 +35,21 @@ func executeYarnFlow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create yarn package manager proxy: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
config := config.Get()
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse command: %w", err)
|
||||
}
|
||||
|
||||
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
|
||||
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
|
||||
if err != nil {
|
||||
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
@@ -35,11 +35,7 @@ func executePipFlow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create pip package manager proxy: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
config := config.Get()
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
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
|
||||
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
|
||||
|
||||
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 flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand)
|
||||
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||
}
|
||||
|
||||
+5
-9
@@ -35,11 +35,7 @@ func executePip3Flow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create pip3 package manager proxy: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
config := config.Get()
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
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
|
||||
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
|
||||
|
||||
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 flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand)
|
||||
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||
}
|
||||
|
||||
+5
-9
@@ -35,20 +35,16 @@ func executePoetryFlow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create poetry package manager: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
config := config.Get()
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse command: %w", err)
|
||||
}
|
||||
|
||||
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
|
||||
|
||||
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 flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand)
|
||||
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||
}
|
||||
|
||||
+5
-9
@@ -35,20 +35,16 @@ func executeUvFlow(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to create uv package manager: %w", err)
|
||||
}
|
||||
|
||||
config, err := config.FromContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
config := config.Get()
|
||||
parsedCommand, err := packageManager.ParseCommand(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse command: %w", err)
|
||||
}
|
||||
|
||||
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
|
||||
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
|
||||
|
||||
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 flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand)
|
||||
return flows.Common(packageManager, packageResolver).Run(ctx, args, parsedCommand)
|
||||
}
|
||||
|
||||
+37
-12
@@ -2,18 +2,24 @@ package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/alias"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
setupRemoveConfigFile = false
|
||||
)
|
||||
|
||||
func NewSetupCommand() *cobra.Command {
|
||||
setupCmd := &cobra.Command{
|
||||
Use: "setup",
|
||||
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 {
|
||||
return cmd.Help()
|
||||
},
|
||||
@@ -28,38 +34,57 @@ func NewSetupCommand() *cobra.Command {
|
||||
func NewInstallCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install PMG aliases for package managers (npm, pnpm, pip)",
|
||||
Long: "Creates ~/.pmg.rc with package manager aliases and sources it in your shell config files (.bashrc, .zshrc, config.fish)",
|
||||
Short: "Setup PMG config and aliases for package managers (npm, pnpm, pip, and more)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
|
||||
|
||||
config := alias.DefaultConfig()
|
||||
rcFileManager, err := alias.NewDefaultRcFileManager(config.RcFileName)
|
||||
cfg := alias.DefaultConfig()
|
||||
rcFileManager, err := alias.NewDefaultRcFileManager(cfg.RcFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to create alias manager: %w", err)
|
||||
}
|
||||
|
||||
aliasManager := alias.New(config, rcFileManager)
|
||||
return aliasManager.Install()
|
||||
aliasManager := alias.New(cfg, rcFileManager)
|
||||
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 {
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Removes pmg aliases from the user's shell config file.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
|
||||
|
||||
config := alias.DefaultConfig()
|
||||
rcFileManager, err := alias.NewDefaultRcFileManager(config.RcFileName)
|
||||
// We remove the config file only if explicitly asked to do so.
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
aliasManager := alias.New(config, rcFileManager)
|
||||
aliasManager := alias.New(cfg, rcFileManager)
|
||||
return aliasManager.Remove()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&setupRemoveConfigFile, "config-file", false, "Remove the config file")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -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
@@ -1,41 +1,246 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
type configKey struct{}
|
||||
type contextValue struct {
|
||||
Config Config
|
||||
const (
|
||||
// Environment variable key for the insecure installation flag
|
||||
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
|
||||
type Config struct {
|
||||
Transitive bool
|
||||
TransitiveDepth int
|
||||
IncludeDevDependencies bool
|
||||
Paranoid bool
|
||||
// TrustedPackage is a package that is trusted by the user and will be ignored by the security guardrails.
|
||||
type TrustedPackage struct {
|
||||
Purl string `mapstructure:"purl"`
|
||||
Reason string `mapstructure:"reason"`
|
||||
}
|
||||
|
||||
// DryRun to check for packages for risks.
|
||||
// Do not actually execute any commands.
|
||||
// RuntimeConfig is the configuration that is used at runtime. It contains static configuration
|
||||
// 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
|
||||
|
||||
// InsecureInstallation allows bypassing install blocking on malicious packages
|
||||
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
|
||||
func (c Config) Inject(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, configKey{}, &contextValue{Config: c})
|
||||
// ConfigFilePath returns the path to the config file.
|
||||
func (r *RuntimeConfig) ConfigFilePath() string {
|
||||
return r.configFilePath
|
||||
}
|
||||
|
||||
// Extract config from context
|
||||
func FromContext(ctx context.Context) (Config, error) {
|
||||
c, ok := ctx.Value(configKey{}).(*contextValue)
|
||||
if !ok {
|
||||
return Config{}, fmt.Errorf("config not found in context")
|
||||
// EventLogDir returns the path to the event log directory.
|
||||
func (r *RuntimeConfig) EventLogDir() string {
|
||||
return r.eventLogDir
|
||||
}
|
||||
|
||||
// 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 c.Config, nil
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -13,12 +13,15 @@ require (
|
||||
github.com/google/osv-scalibr v0.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
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/safedep/dry v0.0.0-20250514080944-bb77f30c7175
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
google.golang.org/grpc v1.72.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -81,7 +84,7 @@ require (
|
||||
github.com/go-toolsmith/astp 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-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/gobwas/glob v0.2.3 // 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/ryancurrah/gomodguard v1.3.5 // 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/santhosh-tekuri/jsonschema/v6 v6.0.1 // 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/tenv v1.12.1 // 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/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.2.0 // 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/multierr v1.11.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/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/term v0.33.0 // indirect
|
||||
golang.org/x/text v0.28.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/protobuf v1.36.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
@@ -223,4 +227,5 @@ require (
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
mvdan.cc/gofumpt v0.7.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/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-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
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/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
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.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.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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-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/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/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/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
|
||||
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/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/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
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/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
|
||||
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/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM=
|
||||
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.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
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/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
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/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
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/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
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/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
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/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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.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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
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-20180826012351-8a410e7b638d/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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
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.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
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/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=
|
||||
@@ -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.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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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.12.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.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
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.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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.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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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.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.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
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-20191011141410-1b5146add898/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/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
|
||||
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
@@ -10,8 +10,10 @@ import (
|
||||
"time"
|
||||
|
||||
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/pmg/analyzer"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/extractor"
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
@@ -43,6 +45,7 @@ type PackageManagerGuardConfig struct {
|
||||
AnalysisTimeout time.Duration
|
||||
DryRun bool
|
||||
InsecureInstallation bool
|
||||
TrustedPackages []config.TrustedPackage
|
||||
}
|
||||
|
||||
func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
||||
@@ -52,9 +55,45 @@ func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
||||
AnalysisTimeout: 5 * time.Minute,
|
||||
DryRun: 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 {
|
||||
config PackageManagerGuardConfig
|
||||
interaction PackageManagerGuardInteraction
|
||||
@@ -226,7 +265,6 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
|
||||
|
||||
func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
|
||||
packages []*packagev1.PackageVersion) ([]*analyzer.PackageVersionAnalysisResult, error) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, g.config.AnalysisTimeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -255,8 +293,16 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
|
||||
|
||||
// Queue all packages for analysis
|
||||
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
|
||||
}
|
||||
|
||||
close(jobs)
|
||||
|
||||
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))
|
||||
|
||||
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
|
||||
if g.config.ResolveDependencies && slices.Contains(parsedCommand.ManifestFiles, "requirements.txt") {
|
||||
|
||||
g.setStatus(fmt.Sprintf("Resolving dependencies for %d package(s)", len(packages)))
|
||||
|
||||
for _, pkg := range packages {
|
||||
@@ -458,7 +503,6 @@ func (g *packageManagerGuard) logMalwareDetection(result *analyzer.PackageVersio
|
||||
details := map[string]interface{}{
|
||||
"analysis_id": result.AnalysisID,
|
||||
"reference_url": result.ReferenceURL,
|
||||
"summary": result.Summary,
|
||||
}
|
||||
|
||||
if blocked {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/packagemanager"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -268,3 +269,262 @@ func TestGuardInsecureInstallation(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ func (a *AliasManager) removeSourceLinesFromShells() error {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tempPath := tempFile.Name()
|
||||
|
||||
// Write filtered content
|
||||
@@ -222,7 +223,7 @@ func (a *AliasManager) removeSourceLinesFromShells() error {
|
||||
// Replace original file
|
||||
os.Chmod(tempPath, info.Mode())
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ type Shell interface {
|
||||
var commentForRemovingShellSource = "# remove aliases by running `pmg setup remove` or deleting the line"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -6,23 +6,23 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/config"
|
||||
)
|
||||
|
||||
// EventType represents the type of event being logged
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeMalwareBlocked EventType = "malware_blocked"
|
||||
EventTypeMalwareConfirmed EventType = "malware_confirmed"
|
||||
EventTypeInstallAllowed EventType = "install_allowed"
|
||||
EventTypeInstallStarted EventType = "install_started"
|
||||
EventTypeMalwareBlocked EventType = "malware_blocked"
|
||||
EventTypeMalwareConfirmed EventType = "malware_confirmed"
|
||||
EventTypeInstallAllowed EventType = "install_allowed"
|
||||
EventTypeInstallStarted EventType = "install_started"
|
||||
EventTypeDependencyResolved EventType = "dependency_resolved"
|
||||
EventTypeError EventType = "error"
|
||||
EventTypeError EventType = "error"
|
||||
)
|
||||
|
||||
// Event represents a security event
|
||||
@@ -36,73 +36,84 @@ type Event struct {
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// Logger represents an event logger
|
||||
type Logger struct {
|
||||
// Logger defines the contract for implementing event loggers.
|
||||
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
|
||||
writer io.Writer
|
||||
mu sync.Mutex
|
||||
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 (
|
||||
globalLogger *Logger
|
||||
globalLogger Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetDefaultLogDir returns the default log directory based on the OS
|
||||
func GetDefaultLogDir() (string, error) {
|
||||
var baseDir string
|
||||
|
||||
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)
|
||||
}
|
||||
return config.Get().EventLogDir(), nil
|
||||
}
|
||||
|
||||
// Initialize sets up the global event logger with the default log directory
|
||||
func Initialize() error {
|
||||
if config.Get().Config.SkipEventLogging {
|
||||
return nil
|
||||
}
|
||||
|
||||
logDir, err := GetDefaultLogDir()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get default log directory: %w", err)
|
||||
}
|
||||
|
||||
return InitializeWithDir(logDir)
|
||||
}
|
||||
|
||||
// InitializeWithFile sets up the global event logger with a specific file path
|
||||
func InitializeWithFile(filePath string) error {
|
||||
if config.Get().Config.SkipEventLogging {
|
||||
return nil
|
||||
}
|
||||
|
||||
var initErr error
|
||||
once.Do(func() {
|
||||
globalLogger = &Logger{}
|
||||
initErr = globalLogger.initWithFile(filePath)
|
||||
fwrl := &fileWithRotationLogger{}
|
||||
initErr = fwrl.initWithFile(filePath)
|
||||
globalLogger = fwrl
|
||||
})
|
||||
|
||||
return initErr
|
||||
}
|
||||
|
||||
// InitializeWithDir sets up the global event logger with a custom log directory
|
||||
func InitializeWithDir(logDir string) error {
|
||||
if config.Get().Config.SkipEventLogging {
|
||||
return nil
|
||||
}
|
||||
|
||||
var initErr error
|
||||
once.Do(func() {
|
||||
globalLogger = &Logger{}
|
||||
initErr = globalLogger.init(logDir)
|
||||
fwrl := &fileWithRotationLogger{}
|
||||
initErr = fwrl.init(logDir)
|
||||
globalLogger = fwrl
|
||||
})
|
||||
|
||||
return initErr
|
||||
}
|
||||
|
||||
@@ -122,7 +133,7 @@ func reinitializeForTest(logDir string) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
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
|
||||
func (l *Logger) initWithFile(filePath string) error {
|
||||
func (l *fileWithRotationLogger) initWithFile(filePath string) error {
|
||||
// Create directory if it doesn't exist
|
||||
dir := filepath.Dir(filePath)
|
||||
if dir != "" && dir != "." {
|
||||
@@ -172,8 +183,8 @@ func (l *Logger) initWithFile(filePath string) error {
|
||||
}
|
||||
|
||||
// cleanupOldLogs removes log files older than 7 days
|
||||
func (l *Logger) cleanupOldLogs(logDir string) {
|
||||
cutoff := time.Now().AddDate(0, 0, -7)
|
||||
func (l *fileWithRotationLogger) cleanupOldLogs(logDir string) {
|
||||
cutoff := time.Now().AddDate(0, 0, -1*config.Get().Config.EventLogRetentionDays)
|
||||
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
@@ -196,17 +207,20 @@ func (l *Logger) cleanupOldLogs(logDir string) {
|
||||
filePath := filepath.Join(logDir, name)
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get info for log file: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
func (l *Logger) Log(event Event) error {
|
||||
func (l *fileWithRotationLogger) Log(event Event) error {
|
||||
if !l.active {
|
||||
return nil
|
||||
}
|
||||
@@ -240,7 +254,7 @@ func (l *Logger) Log(event Event) error {
|
||||
}
|
||||
|
||||
// Close closes the logger
|
||||
func (l *Logger) Close() error {
|
||||
func (l *fileWithRotationLogger) Close() error {
|
||||
if !l.active {
|
||||
return nil
|
||||
}
|
||||
@@ -255,14 +269,20 @@ func (l *Logger) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsActive returns whether the logger is active
|
||||
func (l *fileWithRotationLogger) IsActive() bool {
|
||||
return l.active
|
||||
}
|
||||
|
||||
// Global logging functions
|
||||
|
||||
// LogEvent logs an event using the global logger
|
||||
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 globalLogger.Log(event)
|
||||
}
|
||||
|
||||
@@ -276,9 +296,11 @@ func LogMalwareBlocked(packageName, version, ecosystem, reason string, details m
|
||||
Ecosystem: ecosystem,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
if details == nil {
|
||||
event.Details = make(map[string]interface{})
|
||||
}
|
||||
|
||||
event.Details["reason"] = reason
|
||||
LogEvent(event)
|
||||
}
|
||||
@@ -292,6 +314,7 @@ func LogMalwareConfirmed(packageName, version, ecosystem string) {
|
||||
Version: version,
|
||||
Ecosystem: ecosystem,
|
||||
}
|
||||
|
||||
LogEvent(event)
|
||||
}
|
||||
|
||||
@@ -307,6 +330,7 @@ func LogInstallAllowed(packageName, version, ecosystem string, packageCount int)
|
||||
"packages_analyzed": packageCount,
|
||||
},
|
||||
}
|
||||
|
||||
LogEvent(event)
|
||||
}
|
||||
|
||||
@@ -320,6 +344,7 @@ func LogInstallStarted(packageManager string, args []string) {
|
||||
"arguments": args,
|
||||
},
|
||||
}
|
||||
|
||||
LogEvent(event)
|
||||
}
|
||||
|
||||
@@ -332,6 +357,7 @@ func LogError(message string, err error) {
|
||||
"error": err.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
LogEvent(event)
|
||||
}
|
||||
|
||||
@@ -340,11 +366,6 @@ func Close() error {
|
||||
if globalLogger != nil {
|
||||
return globalLogger.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsInitialized returns whether the global logger is initialized
|
||||
func IsInitialized() bool {
|
||||
return globalLogger != nil && globalLogger.active
|
||||
}
|
||||
|
||||
|
||||
@@ -7,29 +7,22 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetDefaultLogDir(t *testing.T) {
|
||||
logDir, err := GetDefaultLogDir()
|
||||
if err != nil {
|
||||
t.Fatalf("GetDefaultLogDir() failed: %v", err)
|
||||
}
|
||||
assert.NoError(t, err, "failed to get default log directory")
|
||||
|
||||
if logDir == "" {
|
||||
t.Error("Expected non-empty log directory")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
assert.NotEmpty(t, logDir, "log directory should not be empty")
|
||||
assert.Contains(t, logDir, "safedep/pmg/logs")
|
||||
}
|
||||
|
||||
func TestLoggerInitialization(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, ".pmg", "logs")
|
||||
logDir := filepath.Join(tmpDir, "pmg", "logs")
|
||||
|
||||
// Initialize logger
|
||||
err := InitializeWithDir(logDir)
|
||||
@@ -231,11 +224,12 @@ func TestCleanupOldLogs(t *testing.T) {
|
||||
}
|
||||
|
||||
// Initialize logger (which triggers cleanup)
|
||||
logger := &Logger{}
|
||||
logger := &fileWithRotationLogger{}
|
||||
err = logger.init(logDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize logger: %v", err)
|
||||
}
|
||||
|
||||
defer logger.Close()
|
||||
|
||||
// Give cleanup goroutine time to run
|
||||
@@ -251,4 +245,3 @@ func TestCleanupOldLogs(t *testing.T) {
|
||||
t.Error("Recent log file should still exist")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,24 +14,23 @@ import (
|
||||
type commonFlow struct {
|
||||
pm packagemanager.PackageManager
|
||||
packageResolver packagemanager.PackageResolver
|
||||
config config.Config
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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{
|
||||
pm: pm,
|
||||
packageResolver: pkgResolver,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) error {
|
||||
var analyzers []analyzer.PackageVersionAnalyzer
|
||||
config := config.Get()
|
||||
|
||||
if f.config.Paranoid {
|
||||
if config.Config.Paranoid {
|
||||
malysisActiveScanAnalyzer, err := analyzer.NewMalysisActiveScanAnalyzer(analyzer.DefaultMalysisActiveScanAnalyzerConfig())
|
||||
if err != nil {
|
||||
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.DryRun = f.config.DryRun
|
||||
guardConfig.InsecureInstallation = f.config.InsecureInstallation
|
||||
guardConfig.DryRun = config.DryRun
|
||||
guardConfig.InsecureInstallation = config.InsecureInstallation
|
||||
guardConfig.TrustedPackages = config.Config.TrustedPackages
|
||||
|
||||
proxy, err := guard.NewPackageManagerGuard(guardConfig, f.pm, f.packageResolver, analyzers, interaction)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/cmd/npm"
|
||||
@@ -19,11 +18,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
debug bool
|
||||
silent bool
|
||||
verbose bool
|
||||
logFile string
|
||||
globalConfig config.Config
|
||||
debug bool
|
||||
silent bool
|
||||
verbose bool
|
||||
logFile string
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -49,8 +47,7 @@ func main() {
|
||||
}
|
||||
|
||||
if silent && verbose {
|
||||
fmt.Println("pmg: --silent and --verbose cannot be used together")
|
||||
os.Exit(1)
|
||||
ui.Fatalf("pmg: --silent and --verbose cannot be used together")
|
||||
}
|
||||
|
||||
if silent {
|
||||
@@ -59,25 +56,21 @@ func main() {
|
||||
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")
|
||||
|
||||
// Initialize event logging (silently fail if it can't be initialized)
|
||||
var eventlogErr error
|
||||
if logFile != "" {
|
||||
// If a custom log file is specified, use it for event logging too
|
||||
eventlogErr = eventlog.InitializeWithFile(logFile)
|
||||
} else {
|
||||
// Otherwise use the default log directory
|
||||
eventlogErr = eventlog.Initialize()
|
||||
}
|
||||
|
||||
log.InitZapLogger("pmg", "cli")
|
||||
|
||||
// Initialize event logging (silently fail if it can't be initialized)
|
||||
if logFile != "" {
|
||||
// If a custom log file is specified, use it for event logging too
|
||||
_ = eventlog.InitializeWithFile(logFile)
|
||||
} else {
|
||||
// Otherwise use the default log directory
|
||||
_ = 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 {
|
||||
if len(args) == 0 {
|
||||
@@ -92,13 +85,10 @@ func main() {
|
||||
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(&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,
|
||||
"Maximum depth of transitive dependencies to resolve")
|
||||
cmd.PersistentFlags().BoolVar(&globalConfig.IncludeDevDependencies, "include-dev-dependencies", false,
|
||||
"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)")
|
||||
|
||||
// Apply config flags to the command. This allows for overriding the configuration at runtime
|
||||
// using the command line.
|
||||
config.ApplyCobraFlags(cmd)
|
||||
|
||||
cmd.AddCommand(npm.NewNpmCommand())
|
||||
cmd.AddCommand(npm.NewPnpmCommand())
|
||||
|
||||
Reference in New Issue
Block a user