mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
chore: README update demo and Error Fix (#126)
* docs: Update README with demo gif * fix: Proxy remove dependency on interaction * fix: Update demo gif width * Update docs/demo/pmg-intro.tape Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> * fix: PMG demo --------- Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -24,11 +24,11 @@ matches the source code they reviewed, eliminating the risk of tampered or malic
|
|||||||
|
|
||||||
## PMG in Action
|
## PMG in Action
|
||||||
|
|
||||||
<img src="./docs/assets/pmg-intro.png" width="600" alt="pmg in action">
|
<img src="./docs/demo/pmg-intro.gif" width="800" alt="pmg in action">
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
Install `pmg` using Homebrew:
|
Install `pmg` using your favorite package manager:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# MacOS/Linux with Homebrew
|
# MacOS/Linux with Homebrew
|
||||||
@@ -63,6 +63,7 @@ uv pip install <package-name>
|
|||||||
- Malicious package identification using [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) with realtime threat detection
|
- Malicious package identification using [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) with realtime threat detection
|
||||||
- Deep dependency analysis and transitive dependency resolution
|
- Deep dependency analysis and transitive dependency resolution
|
||||||
- Fast and efficient package verification
|
- Fast and efficient package verification
|
||||||
|
- Defense in depth using OS native sandboxing
|
||||||
- Seamless integration with existing package managers
|
- Seamless integration with existing package managers
|
||||||
- Automated shell integration with cross-shell support
|
- Automated shell integration with cross-shell support
|
||||||
- Package installation tracking and event logging
|
- Package installation tracking and event logging
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
node_modules
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if vhs is installed
|
||||||
|
if ! command -v vhs &> /dev/null; then
|
||||||
|
echo "vhs could not be found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch to the script directory
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Enumerate all .tape files in the current directory
|
||||||
|
for file in *.tape; do
|
||||||
|
vhs "$file"
|
||||||
|
done
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
Output pmg-intro.gif
|
||||||
|
|
||||||
|
Set FontSize 28
|
||||||
|
Set Width 1400
|
||||||
|
Set Height 1000
|
||||||
|
|
||||||
|
Set WindowBar Colorful
|
||||||
|
|
||||||
|
Type "pmg setup install"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Type "source ~/.pmg.rc"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Type "npm install safedep-test-pkg"
|
||||||
|
Enter
|
||||||
|
Sleep 5s
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
Output pmg-sandbox.gif
|
||||||
|
|
||||||
|
Set FontSize 28
|
||||||
|
Set Width 1400
|
||||||
|
Set Height 1000
|
||||||
|
|
||||||
|
Set WindowBar Colorful
|
||||||
|
|
||||||
|
Hide
|
||||||
|
Type "export CI=true"
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Type "clear"
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 0.5s
|
||||||
|
Show
|
||||||
|
|
||||||
|
Type "pmg setup install"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Type "source ~/.pmg.rc"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Type `npm exec -- \` Enter
|
||||||
|
Type `node -e "require('child_process').execSync('curl')" \` Enter
|
||||||
|
Type ` 2>&1 | head -n 8` Enter
|
||||||
|
|
||||||
|
Sleep 5s
|
||||||
|
|
||||||
+16
-1
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/safedep/pmg/internal/ui"
|
"github.com/safedep/pmg/internal/ui"
|
||||||
"github.com/safedep/pmg/packagemanager"
|
"github.com/safedep/pmg/packagemanager"
|
||||||
"github.com/safedep/pmg/sandbox/executor"
|
"github.com/safedep/pmg/sandbox/executor"
|
||||||
|
"github.com/safedep/pmg/usefulerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PackageManagerGuardInteraction struct {
|
type PackageManagerGuardInteraction struct {
|
||||||
@@ -254,7 +255,21 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if result.ShouldRun() {
|
if result.ShouldRun() {
|
||||||
return cmd.Run()
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
humanError := "Failed to execute package manager command"
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
humanError = fmt.Sprintf("Package manager command exited with code: %d", exitErr.ExitCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
return usefulerror.Useful().
|
||||||
|
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
|
||||||
|
WithHumanError(humanError).
|
||||||
|
WithHelp("Check the package manager command and its arguments").
|
||||||
|
Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
|
|||||||
log.Infof("Dry-run mode: Would execute %s with experimental proxy protection", f.pm.Name())
|
log.Infof("Dry-run mode: Would execute %s with experimental proxy protection", f.pm.Name())
|
||||||
log.Infof("Dry-run mode: Command would be: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args)
|
log.Infof("Dry-run mode: Command would be: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args)
|
||||||
|
|
||||||
ui.SetStatus("Running in dry-run mode (proxy mode)")
|
|
||||||
ui.ClearStatus()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +100,7 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
|
|||||||
SetStatus: ui.SetStatus,
|
SetStatus: ui.SetStatus,
|
||||||
ClearStatus: ui.ClearStatus,
|
ClearStatus: ui.ClearStatus,
|
||||||
ShowWarning: ui.ShowWarning,
|
ShowWarning: ui.ShowWarning,
|
||||||
Block: ui.Block,
|
Block: ui.BlockNoExit,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ecosystem-specific interceptor using factory
|
// Create ecosystem-specific interceptor using factory
|
||||||
@@ -307,7 +305,7 @@ func (f *proxyFlow) executeWithProxyForNonInteractiveTTY(
|
|||||||
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to execute %s: %w", f.pm.Name(), err)
|
return f.handlePackageManagerExecutionError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,8 +452,32 @@ func (f *proxyFlow) executeWithProxy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sessionError != nil {
|
if sessionError != nil {
|
||||||
return fmt.Errorf("failed to wait for session: %w", sessionError)
|
return f.handlePackageManagerExecutionError(sessionError)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *proxyFlow) handlePackageManagerExecutionError(err error) error {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return usefulerror.Useful().
|
||||||
|
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
|
||||||
|
WithHumanError(fmt.Sprintf("Package manager command exited with code: %d", exitErr.ExitCode())).
|
||||||
|
WithHelp("Check the package manager command and its arguments").
|
||||||
|
Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionError, ok := err.(*pty.ExitError); ok {
|
||||||
|
return usefulerror.Useful().
|
||||||
|
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
|
||||||
|
WithHumanError(fmt.Sprintf("Package manager command exited with code: %d", sessionError.Code)).
|
||||||
|
WithHelp("Check the package manager command and its arguments").
|
||||||
|
Wrap(sessionError.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return usefulerror.Useful().
|
||||||
|
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
|
||||||
|
WithHumanError("Failed to execute package manager command").
|
||||||
|
WithHelp("Check the package manager command and its arguments").
|
||||||
|
Wrap(err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,13 +33,9 @@ func ErrorExit(err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// printMinimalError prints error in minimal two-line format:
|
// printMinimalError prints error in minimal two-line format:
|
||||||
// Line 1: Error code (red background) + message (red)
|
|
||||||
// Line 2: Actionable hint with arrow prefix (dimmed)
|
|
||||||
func printMinimalError(code, message, hint string) {
|
func printMinimalError(code, message, hint string) {
|
||||||
// Line 1: Error code + message
|
|
||||||
fmt.Printf("%s %s\n", Colors.ErrorCode(" %s ", code), Colors.Red(message))
|
fmt.Printf("%s %s\n", Colors.ErrorCode(" %s ", code), Colors.Red(message))
|
||||||
|
|
||||||
// Line 2: Actionable hint with arrow (only if meaningful)
|
|
||||||
if hint != "" && hint != "No additional help is available for this error." {
|
if hint != "" && hint != "No additional help is available for this error." {
|
||||||
fmt.Printf(" %s %s\n", Colors.Dim("→"), Colors.Dim(hint))
|
fmt.Printf(" %s %s\n", Colors.Dim("→"), Colors.Dim(hint))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ var errorMatchers = []errorMatcher{
|
|||||||
WithCode(usefulerror.ErrCodeLifecycle).
|
WithCode(usefulerror.ErrCodeLifecycle).
|
||||||
WithHumanError(fmt.Sprintf("Command failed with exit code %d", exitCode)).
|
WithHumanError(fmt.Sprintf("Command failed with exit code %d", exitCode)).
|
||||||
WithHelp("Check command output above").
|
WithHelp("Check command output above").
|
||||||
WithAdditionalHelp("Run with PMG_DEBUG=true for more details").
|
|
||||||
Wrap(err)
|
Wrap(err)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-1
@@ -54,6 +54,14 @@ func ClearStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Block(config *BlockConfig) error {
|
func Block(config *BlockConfig) error {
|
||||||
|
return blockWithExit(config, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BlockNoExit(config *BlockConfig) error {
|
||||||
|
return blockWithExit(config, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func blockWithExit(config *BlockConfig, exit bool) error {
|
||||||
StopSpinner()
|
StopSpinner()
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -64,7 +72,10 @@ func Block(config *BlockConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
os.Exit(1)
|
|
||||||
|
if exit {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-10
@@ -4,44 +4,51 @@ import (
|
|||||||
"github.com/safedep/pmg/usefulerror"
|
"github.com/safedep/pmg/usefulerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errDependencyResolutionFailed = "DependencyResolutionFailed"
|
||||||
|
errPackageParseFailed = "PackageParseFailed"
|
||||||
|
errPackageAuthorNotFound = "PackageAuthorNotFound"
|
||||||
|
errGitHubRateLimitExceeded = "GitHubRateLimitExceeded"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrPackageNotFound = usefulerror.Useful().
|
ErrPackageNotFound = usefulerror.Useful().
|
||||||
WithCode("package_not_found").
|
WithCode(usefulerror.ErrCodeNotFound).
|
||||||
WithHumanError("The requested package could not be found.").
|
WithHumanError("The requested package could not be found.").
|
||||||
WithHelp("Please check the package name and try again.")
|
WithHelp("Please check the package name and try again.")
|
||||||
|
|
||||||
ErrFailedToFetchPackage = usefulerror.Useful().
|
ErrFailedToFetchPackage = usefulerror.Useful().
|
||||||
WithCode("fetch_failed").
|
WithCode(usefulerror.ErrCodeNetwork).
|
||||||
WithHumanError("Failed to retrieve the requested package.").
|
WithHumanError("Failed to retrieve the requested package.").
|
||||||
WithHelp("Check your network connection and try again. If the problem persists, the package repository may be temporarily unavailable.").
|
WithHelp("Check your network connection and try again.").
|
||||||
Msg("failed to fetch package")
|
Msg("failed to fetch package")
|
||||||
|
|
||||||
ErrFailedToResolveVersion = usefulerror.Useful().
|
ErrFailedToResolveVersion = usefulerror.Useful().
|
||||||
WithCode("resolve_version_failed").
|
WithCode(usefulerror.ErrCodeNetwork).
|
||||||
WithHumanError("Failed to resolve the requested package version.").
|
WithHumanError("Failed to resolve the requested package version.").
|
||||||
WithHelp("Check your network connection and try again. If the problem persists, the package repository may be temporarily unavailable.").
|
WithHelp("Check your network connection and try again.").
|
||||||
Msg("failed to resolve package version")
|
Msg("failed to resolve package version")
|
||||||
|
|
||||||
ErrFailedToResolveDependencies = usefulerror.Useful().
|
ErrFailedToResolveDependencies = usefulerror.Useful().
|
||||||
WithCode("resolve_failed").
|
WithCode(errDependencyResolutionFailed).
|
||||||
WithHumanError("Failed to resolve dependencies.").
|
WithHumanError("Failed to resolve dependencies.").
|
||||||
WithHelp("Check your network connection and try again. If the problem persists, the package repository may be temporarily unavailable.").
|
WithHelp("Check your network connection and try again.").
|
||||||
Msg("failed to resolve dependencies")
|
Msg("failed to resolve dependencies")
|
||||||
|
|
||||||
ErrFailedToParsePackage = usefulerror.Useful().
|
ErrFailedToParsePackage = usefulerror.Useful().
|
||||||
WithCode("parse_failed").
|
WithCode(errPackageParseFailed).
|
||||||
WithHumanError("The package data could not be processed.").
|
WithHumanError("The package data could not be processed.").
|
||||||
WithHelp("The package may be corrupted or in an unsupported format.").
|
WithHelp("The package may be corrupted or in an unsupported format.").
|
||||||
Msg("failed to parse package")
|
Msg("failed to parse package")
|
||||||
|
|
||||||
ErrAuthorNotFound = usefulerror.Useful().
|
ErrAuthorNotFound = usefulerror.Useful().
|
||||||
WithCode("author_not_found").
|
WithCode(errPackageAuthorNotFound).
|
||||||
WithHumanError("The package author information could not be found.").
|
WithHumanError("The package author information could not be found.").
|
||||||
WithHelp("This may be due to incomplete package metadata or network issues.").
|
WithHelp("This may be due to incomplete package metadata or network issues.").
|
||||||
Msg("author not found")
|
Msg("author not found")
|
||||||
|
|
||||||
ErrGitHubRateLimitExceeded = usefulerror.Useful().
|
ErrGitHubRateLimitExceeded = usefulerror.Useful().
|
||||||
WithCode("github_rate_limit").
|
WithCode(errGitHubRateLimitExceeded).
|
||||||
WithHumanError("GitHub API rate limit has been exceeded.").
|
WithHumanError("GitHub API rate limit has been exceeded.").
|
||||||
WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit.").
|
WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit.").
|
||||||
Msg("github api rate limit exceeded")
|
Msg("github api rate limit exceeded")
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/safedep/dry/log"
|
"github.com/safedep/dry/log"
|
||||||
"github.com/safedep/pmg/analyzer"
|
"github.com/safedep/pmg/analyzer"
|
||||||
"github.com/safedep/pmg/config"
|
"github.com/safedep/pmg/config"
|
||||||
"github.com/safedep/pmg/guard"
|
|
||||||
"github.com/safedep/pmg/internal/eventlog"
|
"github.com/safedep/pmg/internal/eventlog"
|
||||||
"github.com/safedep/pmg/proxy"
|
"github.com/safedep/pmg/proxy"
|
||||||
)
|
)
|
||||||
@@ -21,7 +20,6 @@ type baseRegistryInterceptor struct {
|
|||||||
analyzer analyzer.PackageVersionAnalyzer
|
analyzer analyzer.PackageVersionAnalyzer
|
||||||
cache AnalysisCache
|
cache AnalysisCache
|
||||||
confirmationChan chan *ConfirmationRequest
|
confirmationChan chan *ConfirmationRequest
|
||||||
interaction guard.PackageManagerGuardInteraction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ proxy.Interceptor = (*baseRegistryInterceptor)(nil)
|
var _ proxy.Interceptor = (*baseRegistryInterceptor)(nil)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||||
"github.com/safedep/pmg/analyzer"
|
"github.com/safedep/pmg/analyzer"
|
||||||
"github.com/safedep/pmg/guard"
|
|
||||||
"github.com/safedep/pmg/proxy"
|
"github.com/safedep/pmg/proxy"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -119,11 +118,9 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
confirmationChan := make(chan *ConfirmationRequest, 1)
|
confirmationChan := make(chan *ConfirmationRequest, 1)
|
||||||
interaction := guard.PackageManagerGuardInteraction{}
|
|
||||||
|
|
||||||
base := &baseRegistryInterceptor{
|
base := &baseRegistryInterceptor{
|
||||||
confirmationChan: confirmationChan,
|
confirmationChan: confirmationChan,
|
||||||
interaction: interaction,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedURL, _ := url.Parse("https://registry.npmjs.org/test")
|
parsedURL, _ := url.Parse("https://registry.npmjs.org/test")
|
||||||
|
|||||||
+10
-9
@@ -4,13 +4,14 @@ package usefulerror
|
|||||||
// We will use a human friendly format for the error codes and not align with posix error codes.
|
// We will use a human friendly format for the error codes and not align with posix error codes.
|
||||||
// Keep this minimal. Reuse first before adding new ones.
|
// Keep this minimal. Reuse first before adding new ones.
|
||||||
const (
|
const (
|
||||||
ErrCodeInvalidArgument = "InvalidArgument"
|
ErrCodeInvalidArgument = "InvalidArgument"
|
||||||
ErrCodePermissionDenied = "PermissionDenied"
|
ErrCodePermissionDenied = "PermissionDenied"
|
||||||
ErrCodeNotFound = "NotFound"
|
ErrCodeNotFound = "NotFound"
|
||||||
ErrCodeTimeout = "Timeout"
|
ErrCodeTimeout = "Timeout"
|
||||||
ErrCodeCanceled = "Canceled"
|
ErrCodeCanceled = "Canceled"
|
||||||
ErrCodeUnexpectedEOF = "UnexpectedEOF"
|
ErrCodeUnexpectedEOF = "UnexpectedEOF"
|
||||||
ErrCodeUnknown = "Unknown"
|
ErrCodeUnknown = "Unknown"
|
||||||
ErrCodeLifecycle = "Lifecycle"
|
ErrCodeLifecycle = "Lifecycle"
|
||||||
ErrCodeNetwork = "Network"
|
ErrCodeNetwork = "Network"
|
||||||
|
ErrCodePackageManagerExecutionFailed = "PackageManagerExecutionFailed"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user