Files
pmg/internal/ui/ui.go
T
Abhisek DattaandGitHub e86b6ef056 feat: Refactor PMG to Maintain Separation of Concerns and Clean Architecture (#19)
* feat: Add separate package manager and resolver

* fix: Npm dependency resolver

* feat: Add analyzer for malysis query

* feat: Add package manager guard as the orchestrator

* feat: Add PMG to orchestrate installation

* Add concurrent scan execution

* Introduce package manager interaction abstraction

* feat: Add UI port for guard

* Remove refactored source files

* Update README

* fix: CI script for multi-arch build

* ci: goreleaser CI fix

* fix: npm command parser to extract package names

* feat: Introduce global config primitive

* fix: Close results channel for clean goroutine exit

* ci: Add container image releaser

* test: Improve test for npm resolver

* refactor: Analyzer to generalise

* Improve UI with additional info

* fix: Goreleaser config

* fix: npm resolver bug

* fix: Fail when command exec workflow fails

* fix: Bug with transitive dependency resolution

* fix: Synchronize common data update in dependency resolver

* chore: Improve log handling

* docs: Update README

* fix: UI text wrapping

* fix: UI handling bugs

* feat: Use concurrent dependency resolver
2025-05-15 16:50:59 +05:30

151 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ui
import (
"fmt"
"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
)
var verbosityLevel VerbosityLevel = VerbosityLevelNormal
func SetVerbosityLevel(level VerbosityLevel) {
verbosityLevel = level
}
func ClearStatus() {
StopSpinner()
fmt.Print("\r")
}
func Block(malwarePackages ...*analyzer.PackageVersionAnalysisResult) error {
StopSpinner()
fmt.Println()
fmt.Println(Colors.Red("❌ Malicious package blocked!"))
printMaliciousPackagesList(malwarePackages)
fmt.Println()
os.Exit(1)
return nil
}
func SetStatus(status string) {
if verbosityLevel == VerbosityLevelSilent {
return
}
StopSpinner()
StartSpinnerWithColor(fmt.Sprintf("️ %s", status), Colors.Green)
}
func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
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) "))
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
}
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)))
if mp.ReferenceURL != "" {
fmt.Println()
fmt.Println(Colors.Yellow(fmt.Sprintf("Reference: %s", mp.ReferenceURL)))
}
}
}
}
// 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")
}