mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add UI port for guard
This commit is contained in:
+9
-1
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/guard"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"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)
|
||||
}
|
||||
|
||||
interaction := guard.PackageManagerGuardInteraction{
|
||||
SetStatus: ui.SetStatus,
|
||||
ClearStatus: ui.ClearStatus,
|
||||
GetConfirmationOnMalware: ui.GetConfirmationOnMalware,
|
||||
Block: ui.Block,
|
||||
}
|
||||
|
||||
proxy, err := guard.NewPackageManagerGuard(guard.DefaultPackageManagerGuardConfig(),
|
||||
pm, packageResolver, []analyzer.MalysisAnalyzer{malysisQueryAnalyzer})
|
||||
pm, packageResolver, []analyzer.MalysisAnalyzer{malysisQueryAnalyzer}, interaction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create package manager guard: %w", err)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ require (
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1
|
||||
github.com/fatih/color v1.18.0
|
||||
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/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
|
||||
@@ -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/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-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/go.mod h1:DN+59m5kB1QSOpdstCk9rfLTMCPtT/ZLMP5rwuix/j8=
|
||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
|
||||
+26
-2
@@ -16,7 +16,9 @@ import (
|
||||
|
||||
type PackageManagerGuardInteraction struct {
|
||||
SetStatus func(status string)
|
||||
ClearStatus func()
|
||||
GetConfirmationOnMalware func(malwarePackages []*packagev1.PackageVersion) (bool, error)
|
||||
Block func() error
|
||||
}
|
||||
|
||||
type PackageManagerGuardConfig struct {
|
||||
@@ -44,8 +46,11 @@ type packageManagerGuard struct {
|
||||
func NewPackageManagerGuard(config PackageManagerGuardConfig,
|
||||
packageManager packagemanager.PackageManager,
|
||||
packageResolver packagemanager.PackageResolver,
|
||||
analyzers []analyzer.MalysisAnalyzer) (*packageManagerGuard, error) {
|
||||
analyzers []analyzer.MalysisAnalyzer,
|
||||
interaction PackageManagerGuardInteraction,
|
||||
) (*packageManagerGuard, error) {
|
||||
return &packageManagerGuard{
|
||||
interaction: interaction,
|
||||
analyzers: analyzers,
|
||||
packageManager: packageManager,
|
||||
packageResolver: packageResolver,
|
||||
@@ -127,12 +132,14 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
_ = g.blockInstallation()
|
||||
return fmt.Errorf("malicious packages detected, installation aborted")
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("No malicious packages found, continuing execution")
|
||||
|
||||
g.clearStatus()
|
||||
return g.continueExecution(ctx, parsedCommand)
|
||||
}
|
||||
|
||||
@@ -172,7 +179,8 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
|
||||
for _, analyzer := range g.analyzers {
|
||||
analysisResult, err := analyzer.Analyze(ctx, pkg)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -224,3 +232,19 @@ func (g *packageManagerGuard) setStatus(status string) {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user