2025-05-15 16:50:59 +05:30
|
|
|
|
package ui
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-01-09 22:03:42 +05:30
|
|
|
|
"bufio"
|
2025-05-15 16:50:59 +05:30
|
|
|
|
"fmt"
|
2026-01-09 22:03:42 +05:30
|
|
|
|
"io"
|
2025-05-15 16:50:59 +05:30
|
|
|
|
"os"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/safedep/pmg/analyzer"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-06-24 12:44:12 +05:30
|
|
|
|
type BlockConfig struct {
|
|
|
|
|
|
// ShowReference determines whether to show detailed information for suspicious packages.
|
|
|
|
|
|
// If false, the details are omitted to avoid repeating information already shown to the user.
|
|
|
|
|
|
ShowReference bool
|
|
|
|
|
|
|
|
|
|
|
|
MalwarePackages []*analyzer.PackageVersionAnalysisResult
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewDefaultBlockConfig() *BlockConfig {
|
|
|
|
|
|
return &BlockConfig{
|
|
|
|
|
|
ShowReference: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-15 16:50:59 +05:30
|
|
|
|
var verbosityLevel VerbosityLevel = VerbosityLevelNormal
|
|
|
|
|
|
|
|
|
|
|
|
func SetVerbosityLevel(level VerbosityLevel) {
|
|
|
|
|
|
verbosityLevel = level
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func ClearStatus() {
|
|
|
|
|
|
StopSpinner()
|
|
|
|
|
|
fmt.Print("\r")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 12:44:12 +05:30
|
|
|
|
func Block(config *BlockConfig) error {
|
2026-01-18 16:00:37 +05:30
|
|
|
|
return blockWithExit(config, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func BlockNoExit(config *BlockConfig) error {
|
|
|
|
|
|
return blockWithExit(config, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func blockWithExit(config *BlockConfig, exit bool) error {
|
2025-05-15 16:50:59 +05:30
|
|
|
|
StopSpinner()
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
|
fmt.Println(Colors.Red("❌ Malicious package blocked!"))
|
|
|
|
|
|
|
2025-06-24 12:44:12 +05:30
|
|
|
|
if config.ShowReference {
|
|
|
|
|
|
printMaliciousPackagesList(config.MalwarePackages)
|
|
|
|
|
|
}
|
2025-05-15 16:50:59 +05:30
|
|
|
|
|
|
|
|
|
|
fmt.Println()
|
2026-01-18 16:00:37 +05:30
|
|
|
|
|
|
|
|
|
|
if exit {
|
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
|
}
|
2025-05-15 16:50:59 +05:30
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func SetStatus(status string) {
|
|
|
|
|
|
if verbosityLevel == VerbosityLevelSilent {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
StopSpinner()
|
|
|
|
|
|
StartSpinnerWithColor(fmt.Sprintf("ℹ️ %s", status), Colors.Green)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 22:03:42 +05:30
|
|
|
|
// GetConfirmationOnMalware prompts the user to confirm installation of suspicious packages.
|
|
|
|
|
|
// It reads from os.Stdin. Use GetConfirmationOnMalwareWithReader for custom input sources.
|
2025-05-15 16:50:59 +05:30
|
|
|
|
func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
|
2026-01-09 22:03:42 +05:30
|
|
|
|
return GetConfirmationOnMalwareWithReader(malwarePackages, os.Stdin)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetConfirmationOnMalwareWithReader prompts the user to confirm installation of suspicious packages.
|
|
|
|
|
|
// It reads from the provided reader, allowing for PTY input routing during proxy mode.
|
|
|
|
|
|
func GetConfirmationOnMalwareWithReader(malwarePackages []*analyzer.PackageVersionAnalysisResult, reader io.Reader) (bool, error) {
|
2025-05-15 16:50:59 +05:30
|
|
|
|
StopSpinner()
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
|
fmt.Println(Colors.Red(fmt.Sprintf("🚨 Suspicious package(s) detected: %d", len(malwarePackages))))
|
|
|
|
|
|
|
|
|
|
|
|
printMaliciousPackagesList(malwarePackages)
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
|
fmt.Print(Colors.Yellow("Do you want to continue with the installation? (y/N) "))
|
|
|
|
|
|
|
2026-01-09 22:03:42 +05:30
|
|
|
|
// Use Scanner on the provided reader to support PTY input routing
|
|
|
|
|
|
scanner := bufio.NewScanner(reader)
|
|
|
|
|
|
if scanner.Scan() {
|
|
|
|
|
|
response := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
|
|
|
|
|
if response == "y" || response == "yes" || (len(response) > 0 && response[0] == 'y') {
|
|
|
|
|
|
return true, nil
|
|
|
|
|
|
}
|
2025-05-15 16:50:59 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 22:03:42 +05:30
|
|
|
|
// Check for scanner errors, but don't treat them as fatal
|
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
|
|
// On EOF or interrupted read, just return false (deny)
|
|
|
|
|
|
return false, nil
|
2025-05-15 16:50:59 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-02 19:09:27 +05:30
|
|
|
|
func ShowWarning(message string) {
|
|
|
|
|
|
// Print colored warning to stderr immediately - it won't be cleared by other output
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s\n", Colors.Red(message))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-15 16:50:59 +05:30
|
|
|
|
func Fatalf(msg string, args ...interface{}) {
|
|
|
|
|
|
ClearStatus()
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Println(Colors.Red(fmt.Sprintf(msg, args...)))
|
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func printMaliciousPackagesList(malwarePackages []*analyzer.PackageVersionAnalysisResult) {
|
|
|
|
|
|
for _, mp := range malwarePackages {
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
|
fmt.Println("⚠️ ", Colors.Red(fmt.Sprintf("%s@%s", mp.PackageVersion.GetPackage().GetName(),
|
|
|
|
|
|
mp.PackageVersion.GetVersion())))
|
|
|
|
|
|
|
|
|
|
|
|
if verbosityLevel == VerbosityLevelVerbose {
|
|
|
|
|
|
fmt.Println(Colors.Yellow(termWidthFormatText(mp.Summary, 80)))
|
2025-05-15 18:03:46 +05:30
|
|
|
|
}
|
2025-05-15 16:50:59 +05:30
|
|
|
|
|
2025-05-15 18:03:46 +05:30
|
|
|
|
if mp.ReferenceURL != "" {
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
|
fmt.Println(Colors.Yellow(fmt.Sprintf("Reference: %s", mp.ReferenceURL)))
|
2025-05-15 16:50:59 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Format the string to be maximum maxWidth. Use newlines to wrap the text.
|
|
|
|
|
|
func termWidthFormatText(text string, maxWidth int) string {
|
|
|
|
|
|
// Replace all newlines with spaces so that we can split the text into words
|
|
|
|
|
|
// This is to ensure that we don't split the text at the newlines
|
|
|
|
|
|
text = strings.ReplaceAll(text, "\n", " ")
|
|
|
|
|
|
|
|
|
|
|
|
words := strings.Split(text, " ")
|
|
|
|
|
|
lines := []string{}
|
|
|
|
|
|
currentLine := ""
|
|
|
|
|
|
|
|
|
|
|
|
for i, word := range words {
|
|
|
|
|
|
// Skip empty words that might result from multiple spaces
|
|
|
|
|
|
if word == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if i == 0 {
|
|
|
|
|
|
// First word doesn't need a leading space
|
|
|
|
|
|
currentLine = word
|
|
|
|
|
|
} else if len(currentLine)+len(word)+1 > maxWidth {
|
|
|
|
|
|
// +1 for the space we would add
|
|
|
|
|
|
lines = append(lines, currentLine)
|
|
|
|
|
|
currentLine = word
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentLine += " " + word
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Don't forget to add the last line
|
|
|
|
|
|
if currentLine != "" {
|
|
|
|
|
|
lines = append(lines, currentLine)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return strings.Join(lines, "\n")
|
|
|
|
|
|
}
|