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/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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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