diff --git a/README.md b/README.md
index e5c1305..0d8706b 100644
--- a/README.md
+++ b/README.md
@@ -24,11 +24,11 @@ matches the source code they reviewed, eliminating the risk of tampered or malic
## PMG in Action
-
+
## TL;DR
-Install `pmg` using Homebrew:
+Install `pmg` using your favorite package manager:
```shell
# MacOS/Linux with Homebrew
@@ -63,6 +63,7 @@ uv pip install
- Malicious package identification using [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) with realtime threat detection
- Deep dependency analysis and transitive dependency resolution
- Fast and efficient package verification
+- Defense in depth using OS native sandboxing
- Seamless integration with existing package managers
- Automated shell integration with cross-shell support
- Package installation tracking and event logging
diff --git a/docs/demo/.gitignore b/docs/demo/.gitignore
new file mode 100644
index 0000000..e8625c1
--- /dev/null
+++ b/docs/demo/.gitignore
@@ -0,0 +1,3 @@
+package.json
+package-lock.json
+node_modules
diff --git a/docs/demo/build.sh b/docs/demo/build.sh
new file mode 100755
index 0000000..0fe168e
--- /dev/null
+++ b/docs/demo/build.sh
@@ -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
\ No newline at end of file
diff --git a/docs/demo/pmg-intro.gif b/docs/demo/pmg-intro.gif
new file mode 100644
index 0000000..c139db2
Binary files /dev/null and b/docs/demo/pmg-intro.gif differ
diff --git a/docs/demo/pmg-intro.tape b/docs/demo/pmg-intro.tape
new file mode 100644
index 0000000..6f21dae
--- /dev/null
+++ b/docs/demo/pmg-intro.tape
@@ -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
+
diff --git a/docs/demo/pmg-sandbox.gif b/docs/demo/pmg-sandbox.gif
new file mode 100644
index 0000000..3c2e1de
Binary files /dev/null and b/docs/demo/pmg-sandbox.gif differ
diff --git a/docs/demo/pmg-sandbox.tape b/docs/demo/pmg-sandbox.tape
new file mode 100644
index 0000000..b3ac812
--- /dev/null
+++ b/docs/demo/pmg-sandbox.tape
@@ -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
+
diff --git a/guard/guard.go b/guard/guard.go
index 5b7810d..2eabe59 100644
--- a/guard/guard.go
+++ b/guard/guard.go
@@ -19,6 +19,7 @@ import (
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager"
"github.com/safedep/pmg/sandbox/executor"
+ "github.com/safedep/pmg/usefulerror"
)
type PackageManagerGuardInteraction struct {
@@ -254,7 +255,21 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
}()
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
diff --git a/internal/flows/proxy_flow.go b/internal/flows/proxy_flow.go
index d2faaa8..71e6148 100644
--- a/internal/flows/proxy_flow.go
+++ b/internal/flows/proxy_flow.go
@@ -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: Command would be: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args)
- ui.SetStatus("Running in dry-run mode (proxy mode)")
- ui.ClearStatus()
return nil
}
@@ -102,7 +100,7 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
SetStatus: ui.SetStatus,
ClearStatus: ui.ClearStatus,
ShowWarning: ui.ShowWarning,
- Block: ui.Block,
+ Block: ui.BlockNoExit,
}
// Create ecosystem-specific interceptor using factory
@@ -307,7 +305,7 @@ func (f *proxyFlow) executeWithProxyForNonInteractiveTTY(
err = cmd.Run()
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 {
- return fmt.Errorf("failed to wait for session: %w", sessionError)
+ return f.handlePackageManagerExecutionError(sessionError)
}
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)
+}
diff --git a/internal/ui/error.go b/internal/ui/error.go
index 24aea37..12dc76c 100644
--- a/internal/ui/error.go
+++ b/internal/ui/error.go
@@ -33,13 +33,9 @@ func ErrorExit(err error) {
}
// 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) {
- // Line 1: Error code + 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." {
fmt.Printf(" %s %s\n", Colors.Dim("→"), Colors.Dim(hint))
}
diff --git a/internal/ui/error_convert.go b/internal/ui/error_convert.go
index 2385e32..4e80d4e 100644
--- a/internal/ui/error_convert.go
+++ b/internal/ui/error_convert.go
@@ -76,7 +76,6 @@ var errorMatchers = []errorMatcher{
WithCode(usefulerror.ErrCodeLifecycle).
WithHumanError(fmt.Sprintf("Command failed with exit code %d", exitCode)).
WithHelp("Check command output above").
- WithAdditionalHelp("Run with PMG_DEBUG=true for more details").
Wrap(err)
},
},
diff --git a/internal/ui/ui.go b/internal/ui/ui.go
index 156c556..702b909 100644
--- a/internal/ui/ui.go
+++ b/internal/ui/ui.go
@@ -54,6 +54,14 @@ func ClearStatus() {
}
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()
fmt.Println()
@@ -64,7 +72,10 @@ func Block(config *BlockConfig) error {
}
fmt.Println()
- os.Exit(1)
+
+ if exit {
+ os.Exit(1)
+ }
return nil
}
diff --git a/packagemanager/errors.go b/packagemanager/errors.go
index d7f5f32..0e21465 100644
--- a/packagemanager/errors.go
+++ b/packagemanager/errors.go
@@ -4,44 +4,51 @@ import (
"github.com/safedep/pmg/usefulerror"
)
+const (
+ errDependencyResolutionFailed = "DependencyResolutionFailed"
+ errPackageParseFailed = "PackageParseFailed"
+ errPackageAuthorNotFound = "PackageAuthorNotFound"
+ errGitHubRateLimitExceeded = "GitHubRateLimitExceeded"
+)
+
var (
ErrPackageNotFound = usefulerror.Useful().
- WithCode("package_not_found").
+ WithCode(usefulerror.ErrCodeNotFound).
WithHumanError("The requested package could not be found.").
WithHelp("Please check the package name and try again.")
ErrFailedToFetchPackage = usefulerror.Useful().
- WithCode("fetch_failed").
+ WithCode(usefulerror.ErrCodeNetwork).
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")
ErrFailedToResolveVersion = usefulerror.Useful().
- WithCode("resolve_version_failed").
+ WithCode(usefulerror.ErrCodeNetwork).
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")
ErrFailedToResolveDependencies = usefulerror.Useful().
- WithCode("resolve_failed").
+ WithCode(errDependencyResolutionFailed).
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")
ErrFailedToParsePackage = usefulerror.Useful().
- WithCode("parse_failed").
+ WithCode(errPackageParseFailed).
WithHumanError("The package data could not be processed.").
WithHelp("The package may be corrupted or in an unsupported format.").
Msg("failed to parse package")
ErrAuthorNotFound = usefulerror.Useful().
- WithCode("author_not_found").
+ WithCode(errPackageAuthorNotFound).
WithHumanError("The package author information could not be found.").
WithHelp("This may be due to incomplete package metadata or network issues.").
Msg("author not found")
ErrGitHubRateLimitExceeded = usefulerror.Useful().
- WithCode("github_rate_limit").
+ WithCode(errGitHubRateLimitExceeded).
WithHumanError("GitHub API rate limit has been exceeded.").
WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit.").
Msg("github api rate limit exceeded")
diff --git a/proxy/interceptors/base_registry.go b/proxy/interceptors/base_registry.go
index 19f3407..337c8fc 100644
--- a/proxy/interceptors/base_registry.go
+++ b/proxy/interceptors/base_registry.go
@@ -10,7 +10,6 @@ import (
"github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config"
- "github.com/safedep/pmg/guard"
"github.com/safedep/pmg/internal/eventlog"
"github.com/safedep/pmg/proxy"
)
@@ -21,7 +20,6 @@ type baseRegistryInterceptor struct {
analyzer analyzer.PackageVersionAnalyzer
cache AnalysisCache
confirmationChan chan *ConfirmationRequest
- interaction guard.PackageManagerGuardInteraction
}
var _ proxy.Interceptor = (*baseRegistryInterceptor)(nil)
diff --git a/proxy/interceptors/base_registry_test.go b/proxy/interceptors/base_registry_test.go
index 57bd818..44e8d01 100644
--- a/proxy/interceptors/base_registry_test.go
+++ b/proxy/interceptors/base_registry_test.go
@@ -8,7 +8,6 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/analyzer"
- "github.com/safedep/pmg/guard"
"github.com/safedep/pmg/proxy"
"github.com/stretchr/testify/assert"
)
@@ -119,11 +118,9 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
confirmationChan := make(chan *ConfirmationRequest, 1)
- interaction := guard.PackageManagerGuardInteraction{}
base := &baseRegistryInterceptor{
confirmationChan: confirmationChan,
- interaction: interaction,
}
parsedURL, _ := url.Parse("https://registry.npmjs.org/test")
diff --git a/usefulerror/codes.go b/usefulerror/codes.go
index 37bd80f..36e5f20 100644
--- a/usefulerror/codes.go
+++ b/usefulerror/codes.go
@@ -4,13 +4,14 @@ package usefulerror
// 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.
const (
- ErrCodeInvalidArgument = "InvalidArgument"
- ErrCodePermissionDenied = "PermissionDenied"
- ErrCodeNotFound = "NotFound"
- ErrCodeTimeout = "Timeout"
- ErrCodeCanceled = "Canceled"
- ErrCodeUnexpectedEOF = "UnexpectedEOF"
- ErrCodeUnknown = "Unknown"
- ErrCodeLifecycle = "Lifecycle"
- ErrCodeNetwork = "Network"
+ ErrCodeInvalidArgument = "InvalidArgument"
+ ErrCodePermissionDenied = "PermissionDenied"
+ ErrCodeNotFound = "NotFound"
+ ErrCodeTimeout = "Timeout"
+ ErrCodeCanceled = "Canceled"
+ ErrCodeUnexpectedEOF = "UnexpectedEOF"
+ ErrCodeUnknown = "Unknown"
+ ErrCodeLifecycle = "Lifecycle"
+ ErrCodeNetwork = "Network"
+ ErrCodePackageManagerExecutionFailed = "PackageManagerExecutionFailed"
)