feat: Add UI port for guard

This commit is contained in:
abhisek
2025-05-14 14:01:20 +05:30
parent 9aa2d97cd5
commit 723b6142bc
7 changed files with 187 additions and 4 deletions
+9 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/guard" "github.com/safedep/pmg/guard"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager" "github.com/safedep/pmg/packagemanager"
) )
@@ -20,8 +21,15 @@ func executeCommonFlow(pm packagemanager.PackageManager, args []string) error {
return fmt.Errorf("failed to create malysis query analyzer: %w", err) return fmt.Errorf("failed to create malysis query analyzer: %w", err)
} }
interaction := guard.PackageManagerGuardInteraction{
SetStatus: ui.SetStatus,
ClearStatus: ui.ClearStatus,
GetConfirmationOnMalware: ui.GetConfirmationOnMalware,
Block: ui.Block,
}
proxy, err := guard.NewPackageManagerGuard(guard.DefaultPackageManagerGuardConfig(), proxy, err := guard.NewPackageManagerGuard(guard.DefaultPackageManagerGuardConfig(),
pm, packageResolver, []analyzer.MalysisAnalyzer{malysisQueryAnalyzer}) pm, packageResolver, []analyzer.MalysisAnalyzer{malysisQueryAnalyzer}, interaction)
if err != nil { if err != nil {
return fmt.Errorf("failed to create package manager guard: %w", err) return fmt.Errorf("failed to create package manager guard: %w", err)
} }
+1 -1
View File
@@ -9,7 +9,7 @@ require (
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1 buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/safedep/dry v0.0.0-20250512123505-23dcce9fe1af github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175
github.com/safedep/vet v1.10.1 github.com/safedep/vet v1.10.1
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
+2
View File
@@ -511,6 +511,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-20250512123505-23dcce9fe1af h1:3Pomh9zoEIwvzWLBAc5ikJ8kPT53O2APD06HmrlE6A4= github.com/safedep/dry v0.0.0-20250512123505-23dcce9fe1af h1:3Pomh9zoEIwvzWLBAc5ikJ8kPT53O2APD06HmrlE6A4=
github.com/safedep/dry v0.0.0-20250512123505-23dcce9fe1af/go.mod h1:Mdqx/Q2DhAcN38XiUNTGCC5MktofYDQW9Az7YWGEF0s= github.com/safedep/dry v0.0.0-20250512123505-23dcce9fe1af/go.mod h1:Mdqx/Q2DhAcN38XiUNTGCC5MktofYDQW9Az7YWGEF0s=
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/vet v1.10.1 h1:L8yG3X/t3I9fDVICc0DZ+zGweoLhDeyFIK6ubcNiXUc= github.com/safedep/vet v1.10.1 h1:L8yG3X/t3I9fDVICc0DZ+zGweoLhDeyFIK6ubcNiXUc=
github.com/safedep/vet v1.10.1/go.mod h1:DN+59m5kB1QSOpdstCk9rfLTMCPtT/ZLMP5rwuix/j8= github.com/safedep/vet v1.10.1/go.mod h1:DN+59m5kB1QSOpdstCk9rfLTMCPtT/ZLMP5rwuix/j8=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+26 -2
View File
@@ -16,7 +16,9 @@ import (
type PackageManagerGuardInteraction struct { type PackageManagerGuardInteraction struct {
SetStatus func(status string) SetStatus func(status string)
ClearStatus func()
GetConfirmationOnMalware func(malwarePackages []*packagev1.PackageVersion) (bool, error) GetConfirmationOnMalware func(malwarePackages []*packagev1.PackageVersion) (bool, error)
Block func() error
} }
type PackageManagerGuardConfig struct { type PackageManagerGuardConfig struct {
@@ -44,8 +46,11 @@ type packageManagerGuard struct {
func NewPackageManagerGuard(config PackageManagerGuardConfig, func NewPackageManagerGuard(config PackageManagerGuardConfig,
packageManager packagemanager.PackageManager, packageManager packagemanager.PackageManager,
packageResolver packagemanager.PackageResolver, packageResolver packagemanager.PackageResolver,
analyzers []analyzer.MalysisAnalyzer) (*packageManagerGuard, error) { analyzers []analyzer.MalysisAnalyzer,
interaction PackageManagerGuardInteraction,
) (*packageManagerGuard, error) {
return &packageManagerGuard{ return &packageManagerGuard{
interaction: interaction,
analyzers: analyzers, analyzers: analyzers,
packageManager: packageManager, packageManager: packageManager,
packageResolver: packageResolver, packageResolver: packageResolver,
@@ -127,12 +132,14 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string) error {
} }
if !confirmed { if !confirmed {
_ = g.blockInstallation()
return fmt.Errorf("malicious packages detected, installation aborted") return fmt.Errorf("malicious packages detected, installation aborted")
} }
} }
log.Debugf("No malicious packages found, continuing execution") log.Debugf("No malicious packages found, continuing execution")
g.clearStatus()
return g.continueExecution(ctx, parsedCommand) return g.continueExecution(ctx, parsedCommand)
} }
@@ -172,7 +179,8 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
for _, analyzer := range g.analyzers { for _, analyzer := range g.analyzers {
analysisResult, err := analyzer.Analyze(ctx, pkg) analysisResult, err := analyzer.Analyze(ctx, pkg)
if err != nil { if err != nil {
log.Errorf("failed to analyze package: %w", err) // This is not an error because we may not have results for all packages
log.Debugf("failed to analyze package: %v", err)
continue continue
} }
@@ -224,3 +232,19 @@ func (g *packageManagerGuard) setStatus(status string) {
g.interaction.SetStatus(status) g.interaction.SetStatus(status)
} }
func (g *packageManagerGuard) blockInstallation() error {
if g.interaction.Block == nil {
return nil
}
return g.interaction.Block()
}
func (g *packageManagerGuard) clearStatus() {
if g.interaction.ClearStatus == nil {
return
}
g.interaction.ClearStatus()
}
+17
View File
@@ -0,0 +1,17 @@
package ui
import "github.com/fatih/color"
type TerminalColors struct {
Red func(format string, a ...interface{}) string
Yellow func(format string, a ...interface{}) string
Cyan func(format string, a ...interface{}) string
Green func(format string, a ...interface{}) string
}
var Colors = TerminalColors{
Red: color.New(color.FgRed, color.Bold).SprintfFunc(),
Yellow: color.New(color.FgYellow).SprintfFunc(),
Cyan: color.New(color.FgCyan).SprintfFunc(),
Green: color.New(color.FgGreen).SprintfFunc(),
}
+45
View File
@@ -0,0 +1,45 @@
package ui
import (
"fmt"
"time"
)
var spinnerChan chan bool
func StartSpinner(msg string) {
style := `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`
frames := []rune(style)
length := len(frames)
spinnerChan = make(chan bool)
ticker := time.NewTicker(100 * time.Millisecond)
go func() {
pos := 0
for {
select {
case <-spinnerChan:
ticker.Stop()
return
case <-ticker.C:
fmt.Printf("\r%s ... %s", msg, string(frames[pos%length]))
pos += 1
}
}
}()
}
func StopSpinner() {
// Gracefully handle the case where the spinner is already stopped
// and the channel is closed, yet client code calls StopSpinner() again.
defer func() {
_ = recover()
}()
close(spinnerChan)
fmt.Printf("\r")
fmt.Println()
}
+87
View File
@@ -0,0 +1,87 @@
package ui
import (
"fmt"
"os"
"strings"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
)
// The UI is internal to PMG and opinionated for the CLI.
// It is not intended to be used outside of PMG.
type VerbosityLevel int
const (
// PMG is hidden from the user except for errors
// and when malicious packages are detected
VerbosityLevelSilent VerbosityLevel = iota
// Show minimal status updates
VerbosityLevelNormal
// Show verbose status updates and information including
// information about malicious packages
VerbosityLevelVerbose
)
var verbosityLevel VerbosityLevel = VerbosityLevelNormal
func SetVerbosityLevel(level VerbosityLevel) {
verbosityLevel = level
}
func ClearStatus() {
StopSpinner()
fmt.Print("\r")
}
func Block() error {
StopSpinner()
fmt.Println(Colors.Red("❌ Malicious packages detected, installation blocked!"))
os.Exit(1)
return nil
}
func SetStatus(status string) {
if verbosityLevel == VerbosityLevelSilent {
return
}
StopSpinner()
fmt.Print("\r", Colors.Green(status), " ")
StartSpinner(status)
}
func GetConfirmationOnMalware(malwarePackages []*packagev1.PackageVersion) (bool, error) {
StopSpinner()
fmt.Println(Colors.Red("🚨 Malicious packages detected:"))
for _, pkg := range malwarePackages {
fmt.Println(" ⚠️ ", Colors.Red(fmt.Sprintf("%s@%s", pkg.Package.Name, pkg.Version)))
}
fmt.Println()
fmt.Print(Colors.Yellow("Do you want to continue with the installation? (y/N) "))
var response string
// We don't care about the error here because we will return false
// if the user doesn't provide a valid response
_, _ = fmt.Scanln(&response)
if len(response) == 0 {
return false, nil
}
response = strings.ToLower(response)
if response == "y" || response == "yes" || response[0] == 'y' {
return true, nil
}
return false, nil
}