mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
refactor: Analyzer to generalise
This commit is contained in:
+25
-13
@@ -3,7 +3,6 @@ package analyzer
|
||||
import (
|
||||
"context"
|
||||
|
||||
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
)
|
||||
|
||||
@@ -12,21 +11,34 @@ type Analyzer interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type MalysisResult struct {
|
||||
type Action int
|
||||
|
||||
const (
|
||||
ActionUnknown Action = iota
|
||||
ActionAllow
|
||||
ActionConfirm
|
||||
ActionBlock
|
||||
)
|
||||
|
||||
type PackageVersionAnalysisResult struct {
|
||||
PackageVersion *packagev1.PackageVersion
|
||||
|
||||
// Analyser specific analysis ID
|
||||
AnalysisID string
|
||||
Report *malysisv1.Report
|
||||
|
||||
// The action to take as recommended by the analyzer
|
||||
Action Action
|
||||
|
||||
// Summary of the analysis
|
||||
Summary string
|
||||
|
||||
// Analyzer specific data
|
||||
Data any
|
||||
}
|
||||
|
||||
func (m *MalysisResult) IsMalware() bool {
|
||||
return m.Report.GetInference().GetIsMalware()
|
||||
}
|
||||
|
||||
func (m *MalysisResult) Summary() string {
|
||||
return m.Report.GetInference().GetSummary()
|
||||
}
|
||||
|
||||
type MalysisAnalyzer interface {
|
||||
// Contract for implementing package version specific analyzers
|
||||
type PackageVersionAnalyzer interface {
|
||||
Analyzer
|
||||
|
||||
Analyze(ctx context.Context, packageVersion *packagev1.PackageVersion) (*MalysisResult, error)
|
||||
Analyze(ctx context.Context, packageVersion *packagev1.PackageVersion) (*PackageVersionAnalysisResult, error)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type malysisQueryAnalyzer struct {
|
||||
}
|
||||
|
||||
var _ Analyzer = &malysisQueryAnalyzer{}
|
||||
var _ MalysisAnalyzer = &malysisQueryAnalyzer{}
|
||||
var _ PackageVersionAnalyzer = &malysisQueryAnalyzer{}
|
||||
|
||||
func NewMalysisQueryAnalyzer(config MalysisQueryAnalyzerConfig) (*malysisQueryAnalyzer, error) {
|
||||
client, err := drygrpc.GrpcClient("pmg-malysis-query",
|
||||
@@ -41,7 +41,7 @@ func (a *malysisQueryAnalyzer) Name() string {
|
||||
}
|
||||
|
||||
func (a *malysisQueryAnalyzer) Analyze(ctx context.Context,
|
||||
packageVersion *packagev1.PackageVersion) (*MalysisResult, error) {
|
||||
packageVersion *packagev1.PackageVersion) (*PackageVersionAnalysisResult, error) {
|
||||
|
||||
res, err := a.client.QueryPackageAnalysis(ctx, &malysisv1.QueryPackageAnalysisRequest{
|
||||
Target: &malysisv1pb.PackageAnalysisTarget{
|
||||
@@ -52,7 +52,24 @@ func (a *malysisQueryAnalyzer) Analyze(ctx context.Context,
|
||||
return nil, fmt.Errorf("failed to query package analysis: %w", err)
|
||||
}
|
||||
|
||||
return &MalysisResult{
|
||||
Report: res.GetReport(),
|
||||
}, nil
|
||||
// By default, the analyzer allows the package version
|
||||
analysisResult := &PackageVersionAnalysisResult{
|
||||
PackageVersion: packageVersion,
|
||||
Action: ActionAllow,
|
||||
AnalysisID: res.GetAnalysisId(),
|
||||
Summary: res.GetReport().GetInference().GetSummary(),
|
||||
Data: res.GetReport(),
|
||||
}
|
||||
|
||||
// Mark the package version to be confirmed if it is malicious (not confirmed)
|
||||
if res.GetReport().GetInference().GetIsMalware() {
|
||||
analysisResult.Action = ActionConfirm
|
||||
}
|
||||
|
||||
// This is a confirmed malicious package, we must always block it
|
||||
if res.GetVerificationRecord().GetIsMalware() {
|
||||
analysisResult.Action = ActionBlock
|
||||
}
|
||||
|
||||
return analysisResult, nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ func executeCommonFlow(ctx context.Context, config config.Config, pm packagemana
|
||||
}
|
||||
|
||||
proxy, err := guard.NewPackageManagerGuard(guard.DefaultPackageManagerGuardConfig(),
|
||||
pm, packageResolver, []analyzer.MalysisAnalyzer{malysisQueryAnalyzer}, interaction)
|
||||
pm, packageResolver, []analyzer.PackageVersionAnalyzer{malysisQueryAnalyzer}, interaction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create package manager guard: %w", err)
|
||||
}
|
||||
|
||||
+18
-18
@@ -17,7 +17,7 @@ import (
|
||||
type PackageManagerGuardInteraction struct {
|
||||
SetStatus func(status string)
|
||||
ClearStatus func()
|
||||
GetConfirmationOnMalware func(malwarePackages []*packagev1.PackageVersion) (bool, error)
|
||||
GetConfirmationOnMalware func(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error)
|
||||
Block func() error
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
|
||||
type packageManagerGuard struct {
|
||||
config PackageManagerGuardConfig
|
||||
interaction PackageManagerGuardInteraction
|
||||
analyzers []analyzer.MalysisAnalyzer
|
||||
analyzers []analyzer.PackageVersionAnalyzer
|
||||
packageManager packagemanager.PackageManager
|
||||
packageResolver packagemanager.PackageResolver
|
||||
}
|
||||
@@ -46,7 +46,7 @@ type packageManagerGuard struct {
|
||||
func NewPackageManagerGuard(config PackageManagerGuardConfig,
|
||||
packageManager packagemanager.PackageManager,
|
||||
packageResolver packagemanager.PackageResolver,
|
||||
analyzers []analyzer.MalysisAnalyzer,
|
||||
analyzers []analyzer.PackageVersionAnalyzer,
|
||||
interaction PackageManagerGuardInteraction,
|
||||
) (*packageManagerGuard, error) {
|
||||
return &packageManagerGuard{
|
||||
@@ -118,15 +118,20 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("failed to analyze packages: %w", err)
|
||||
}
|
||||
|
||||
maliciousPackages := []*packagev1.PackageVersion{}
|
||||
confirmableMalwarePackages := []*analyzer.PackageVersionAnalysisResult{}
|
||||
for _, result := range analysisResults {
|
||||
if result.result.IsMalware() {
|
||||
maliciousPackages = append(maliciousPackages, result.pkg)
|
||||
if result.Action == analyzer.ActionBlock {
|
||||
_ = g.blockInstallation()
|
||||
return fmt.Errorf("malicious packages detected, installation aborted")
|
||||
}
|
||||
|
||||
if result.Action == analyzer.ActionConfirm {
|
||||
confirmableMalwarePackages = append(confirmableMalwarePackages, result)
|
||||
}
|
||||
}
|
||||
|
||||
if len(maliciousPackages) > 0 {
|
||||
confirmed, err := g.getConfirmationOnMalware(ctx, maliciousPackages)
|
||||
if len(confirmableMalwarePackages) > 0 {
|
||||
confirmed, err := g.getConfirmationOnMalware(ctx, confirmableMalwarePackages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get confirmation on malware: %w", err)
|
||||
}
|
||||
@@ -156,20 +161,15 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
type analyzePackageResult struct {
|
||||
pkg *packagev1.PackageVersion
|
||||
result *analyzer.MalysisResult
|
||||
}
|
||||
|
||||
func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
|
||||
packages []*packagev1.PackageVersion) ([]*analyzePackageResult, error) {
|
||||
packages []*packagev1.PackageVersion) ([]*analyzer.PackageVersionAnalysisResult, error) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, g.config.AnalysisTimeout)
|
||||
defer cancel()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
jobs := make(chan *packagev1.PackageVersion, len(packages))
|
||||
results := make(chan *analyzePackageResult, len(packages))
|
||||
results := make(chan *analyzer.PackageVersionAnalysisResult, len(packages))
|
||||
|
||||
for i := 0; i < g.config.MaxConcurrentAnalyzes; i++ {
|
||||
wg.Add(1)
|
||||
@@ -184,7 +184,7 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
|
||||
continue
|
||||
}
|
||||
|
||||
results <- &analyzePackageResult{pkg: pkg, result: analysisResult}
|
||||
results <- analysisResult
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -195,7 +195,7 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
analysisResults := []*analyzePackageResult{}
|
||||
analysisResults := []*analyzer.PackageVersionAnalysisResult{}
|
||||
go func() {
|
||||
for result := range results {
|
||||
analysisResults = append(analysisResults, result)
|
||||
@@ -218,7 +218,7 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
|
||||
return analysisResults, nil
|
||||
}
|
||||
|
||||
func (g *packageManagerGuard) getConfirmationOnMalware(ctx context.Context, malwarePackages []*packagev1.PackageVersion) (bool, error) {
|
||||
func (g *packageManagerGuard) getConfirmationOnMalware(ctx context.Context, malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
|
||||
if g.interaction.GetConfirmationOnMalware == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@ package ui
|
||||
|
||||
import "github.com/fatih/color"
|
||||
|
||||
type ColorFn func(format string, a ...interface{}) string
|
||||
|
||||
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
|
||||
Normal ColorFn
|
||||
Red ColorFn
|
||||
Yellow ColorFn
|
||||
Cyan ColorFn
|
||||
Green ColorFn
|
||||
}
|
||||
|
||||
var Colors = TerminalColors{
|
||||
Normal: color.New().SprintfFunc(),
|
||||
Red: color.New(color.FgRed, color.Bold).SprintfFunc(),
|
||||
Yellow: color.New(color.FgYellow).SprintfFunc(),
|
||||
Cyan: color.New(color.FgCyan).SprintfFunc(),
|
||||
|
||||
@@ -8,6 +8,14 @@ import (
|
||||
var spinnerChan chan bool
|
||||
|
||||
func StartSpinner(msg string) {
|
||||
StartSpinnerWithColor(msg, Colors.Normal)
|
||||
}
|
||||
|
||||
func StartSpinnerWithColor(msg string, c ColorFn) {
|
||||
if c == nil {
|
||||
c = Colors.Normal
|
||||
}
|
||||
|
||||
style := `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`
|
||||
frames := []rune(style)
|
||||
length := len(frames)
|
||||
@@ -24,7 +32,7 @@ func StartSpinner(msg string) {
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
fmt.Printf("\r%s ... %s", msg, string(frames[pos%length]))
|
||||
fmt.Printf("\r%s ... %s", c(msg), string(frames[pos%length]))
|
||||
pos += 1
|
||||
}
|
||||
}
|
||||
|
||||
+31
-9
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
)
|
||||
|
||||
// The UI is internal to PMG and opinionated for the CLI.
|
||||
@@ -40,7 +40,9 @@ func ClearStatus() {
|
||||
func Block() error {
|
||||
StopSpinner()
|
||||
|
||||
fmt.Println(Colors.Red("❌ Malicious packages detected, installation blocked!"))
|
||||
fmt.Println()
|
||||
fmt.Println(Colors.Red("❌ Malicious package blocked!"))
|
||||
|
||||
os.Exit(1)
|
||||
|
||||
return nil
|
||||
@@ -52,17 +54,19 @@ func SetStatus(status string) {
|
||||
}
|
||||
|
||||
StopSpinner()
|
||||
|
||||
fmt.Print("\r", Colors.Green(status), " ")
|
||||
StartSpinner(status)
|
||||
StartSpinnerWithColor(fmt.Sprintf("ℹ️ %s", status), Colors.Green)
|
||||
}
|
||||
|
||||
func GetConfirmationOnMalware(malwarePackages []*packagev1.PackageVersion) (bool, error) {
|
||||
func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
|
||||
StopSpinner()
|
||||
fmt.Println(Colors.Red("🚨 Malicious packages detected:"))
|
||||
fmt.Println(Colors.Red(fmt.Sprintf("🚨 Malicious packages detected: %d", len(malwarePackages))))
|
||||
fmt.Println()
|
||||
|
||||
for _, pkg := range malwarePackages {
|
||||
fmt.Println(" ⚠️ ", Colors.Red(fmt.Sprintf("%s@%s", pkg.Package.Name, pkg.Version)))
|
||||
for _, mp := range malwarePackages {
|
||||
fmt.Println("⚠️ ", Colors.Red(fmt.Sprintf("%s@%s", mp.PackageVersion.GetPackage().GetName(),
|
||||
mp.PackageVersion.GetVersion())))
|
||||
fmt.Println(Colors.Yellow(termWidthFormatText(mp.Summary, 60)))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
@@ -85,3 +89,21 @@ func GetConfirmationOnMalware(malwarePackages []*packagev1.PackageVersion) (bool
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Format the string to be maximum maxWidth. Use newlines to wrap the text.
|
||||
func termWidthFormatText(text string, maxWidth int) string {
|
||||
words := strings.Split(text, " ")
|
||||
lines := []string{}
|
||||
currentLine := ""
|
||||
|
||||
for _, word := range words {
|
||||
if len(currentLine)+len(word) > maxWidth {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = word
|
||||
} else {
|
||||
currentLine += " " + word
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user