mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat(proxy): report blocked packages on stop, exit 1 if any were blocked
pmg proxy stop now waits for the proxy process to exit, reads the final blocked count written to the state file on shutdown, and exits non-zero when one or more packages were blocked during the session. This gives CI a clear failure signal from the proxy itself, separate from the package manager's own exit code.
This commit is contained in:
@@ -75,19 +75,13 @@ jobs:
|
||||
echo "SUCCESS: lodash installed through proxy"
|
||||
cd .. && rm -rf benign-test
|
||||
|
||||
- name: Malicious package is blocked
|
||||
- name: Malicious package is blocked (npm exits non-zero)
|
||||
run: |
|
||||
mkdir malicious-test && cd malicious-test
|
||||
npm init -y
|
||||
if npm --no-cache --prefer-online install safedep-test-pkg@0.1.3; then
|
||||
echo "ERROR: safedep-test-pkg was not blocked!"
|
||||
exit 1
|
||||
fi
|
||||
if [ -d "node_modules/safedep-test-pkg" ]; then
|
||||
echo "ERROR: safedep-test-pkg found in node_modules!"
|
||||
exit 1
|
||||
fi
|
||||
echo "SUCCESS: safedep-test-pkg blocked by persistent proxy"
|
||||
npm --no-cache --prefer-online install safedep-test-pkg@0.1.3 && exit 1 || true
|
||||
test ! -d node_modules/safedep-test-pkg
|
||||
echo "SUCCESS: npm correctly failed to install safedep-test-pkg"
|
||||
cd .. && rm -rf malicious-test
|
||||
|
||||
- name: pip installs through proxy
|
||||
@@ -97,6 +91,10 @@ jobs:
|
||||
python -c "import requests; print('pip ok:', requests.__version__)"
|
||||
deactivate && rm -rf venv
|
||||
|
||||
- name: Stop proxy
|
||||
if: always()
|
||||
run: pmg proxy stop || true
|
||||
- name: Stop proxy (exits 1 when packages were blocked)
|
||||
run: |
|
||||
if pmg proxy stop; then
|
||||
echo "ERROR: pmg proxy stop should have exited non-zero (packages were blocked)"
|
||||
exit 1
|
||||
fi
|
||||
echo "SUCCESS: pmg proxy stop correctly reported blocked packages"
|
||||
|
||||
+7
-1
@@ -106,7 +106,13 @@ func runStart(_ *cobra.Command, _ []string) error {
|
||||
<-sigCh
|
||||
|
||||
close(confirmationChan)
|
||||
_ = proxystate.Remove(statePath)
|
||||
|
||||
// Write final blocked count before exiting so `pmg proxy stop` can read it.
|
||||
// stop is responsible for removing the state file.
|
||||
state.BlockedCount = stats.GetStats().BlockedCount
|
||||
if werr := proxystate.Write(statePath, state); werr != nil {
|
||||
log.Warnf("failed to write final proxy state: %v", werr)
|
||||
}
|
||||
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
+38
-2
@@ -4,12 +4,20 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/usefulerror"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/errcodes"
|
||||
"github.com/safedep/pmg/internal/proxystate"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
stopPollInterval = 200 * time.Millisecond
|
||||
stopPollTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
func newStopCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop",
|
||||
@@ -41,8 +49,36 @@ func runStop(_ *cobra.Command, _ []string) error {
|
||||
return fmt.Errorf("send SIGTERM to proxy (pid %d): %w", state.PID, err)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(os.Stdout, "Sent SIGTERM to PMG proxy (pid %d, addr %s)\n", state.PID, state.Addr); err != nil {
|
||||
return fmt.Errorf("write stop message: %w", err)
|
||||
// Wait for the process to exit so we can read the final blocked count it writes.
|
||||
deadline := time.Now().Add(stopPollTimeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if !state.IsRunning() {
|
||||
break
|
||||
}
|
||||
time.Sleep(stopPollInterval)
|
||||
}
|
||||
|
||||
// Read the final state the proxy wrote on shutdown (has BlockedCount).
|
||||
final, rerr := proxystate.Read(statePath)
|
||||
_ = proxystate.Remove(statePath)
|
||||
|
||||
if rerr != nil {
|
||||
// Proxy exited but didn't write final state (e.g. crash). Treat as clean.
|
||||
if _, werr := fmt.Fprintf(os.Stdout, "PMG proxy (pid %d) stopped\n", state.PID); werr != nil {
|
||||
return werr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, werr := fmt.Fprintf(os.Stdout, "PMG proxy stopped — analyzed packages, %d blocked\n", final.BlockedCount); werr != nil {
|
||||
return werr
|
||||
}
|
||||
|
||||
if final.BlockedCount > 0 {
|
||||
return usefulerror.NewUsefulError().
|
||||
WithCode(errcodes.ProxyPackagesBlocked).
|
||||
WithMsg(fmt.Sprintf("%d package(s) were blocked by the proxy", final.BlockedCount)).
|
||||
WithHelp("Review the proxy logs for details on blocked packages")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -24,6 +24,9 @@ const (
|
||||
CertTrustStore = "CertTrustStore"
|
||||
UnsupportedPlatform = "UnsupportedPlatform"
|
||||
|
||||
// Proxy error codes.
|
||||
ProxyPackagesBlocked = "ProxyPackagesBlocked"
|
||||
|
||||
// Unknown mirrors the default code that dry/usefulerror returns for errors
|
||||
// created without an explicit code, so unset and explicitly-unknown errors
|
||||
// classify identically (e.g. the bug-report hint in ui.ErrorExit).
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
const stateFileName = "proxy-state.json"
|
||||
|
||||
type State struct {
|
||||
PID int `json:"pid"`
|
||||
Addr string `json:"addr"`
|
||||
CACertPath string `json:"ca_cert_path"`
|
||||
PID int `json:"pid"`
|
||||
Addr string `json:"addr"`
|
||||
CACertPath string `json:"ca_cert_path"`
|
||||
BlockedCount int `json:"blocked_count"`
|
||||
}
|
||||
|
||||
func StatePath(configDir string) string {
|
||||
|
||||
Reference in New Issue
Block a user