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"
|
echo "SUCCESS: lodash installed through proxy"
|
||||||
cd .. && rm -rf benign-test
|
cd .. && rm -rf benign-test
|
||||||
|
|
||||||
- name: Malicious package is blocked
|
- name: Malicious package is blocked (npm exits non-zero)
|
||||||
run: |
|
run: |
|
||||||
mkdir malicious-test && cd malicious-test
|
mkdir malicious-test && cd malicious-test
|
||||||
npm init -y
|
npm init -y
|
||||||
if npm --no-cache --prefer-online install safedep-test-pkg@0.1.3; then
|
npm --no-cache --prefer-online install safedep-test-pkg@0.1.3 && exit 1 || true
|
||||||
echo "ERROR: safedep-test-pkg was not blocked!"
|
test ! -d node_modules/safedep-test-pkg
|
||||||
exit 1
|
echo "SUCCESS: npm correctly failed to install safedep-test-pkg"
|
||||||
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"
|
|
||||||
cd .. && rm -rf malicious-test
|
cd .. && rm -rf malicious-test
|
||||||
|
|
||||||
- name: pip installs through proxy
|
- name: pip installs through proxy
|
||||||
@@ -97,6 +91,10 @@ jobs:
|
|||||||
python -c "import requests; print('pip ok:', requests.__version__)"
|
python -c "import requests; print('pip ok:', requests.__version__)"
|
||||||
deactivate && rm -rf venv
|
deactivate && rm -rf venv
|
||||||
|
|
||||||
- name: Stop proxy
|
- name: Stop proxy (exits 1 when packages were blocked)
|
||||||
if: always()
|
run: |
|
||||||
run: pmg proxy stop || true
|
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
|
<-sigCh
|
||||||
|
|
||||||
close(confirmationChan)
|
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)
|
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
+38
-2
@@ -4,12 +4,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/safedep/dry/usefulerror"
|
||||||
"github.com/safedep/pmg/config"
|
"github.com/safedep/pmg/config"
|
||||||
|
"github.com/safedep/pmg/errcodes"
|
||||||
"github.com/safedep/pmg/internal/proxystate"
|
"github.com/safedep/pmg/internal/proxystate"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
stopPollInterval = 200 * time.Millisecond
|
||||||
|
stopPollTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func newStopCommand() *cobra.Command {
|
func newStopCommand() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "stop",
|
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)
|
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 {
|
// Wait for the process to exit so we can read the final blocked count it writes.
|
||||||
return fmt.Errorf("write stop message: %w", err)
|
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
|
return nil
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const (
|
|||||||
CertTrustStore = "CertTrustStore"
|
CertTrustStore = "CertTrustStore"
|
||||||
UnsupportedPlatform = "UnsupportedPlatform"
|
UnsupportedPlatform = "UnsupportedPlatform"
|
||||||
|
|
||||||
|
// Proxy error codes.
|
||||||
|
ProxyPackagesBlocked = "ProxyPackagesBlocked"
|
||||||
|
|
||||||
// Unknown mirrors the default code that dry/usefulerror returns for errors
|
// Unknown mirrors the default code that dry/usefulerror returns for errors
|
||||||
// created without an explicit code, so unset and explicitly-unknown errors
|
// created without an explicit code, so unset and explicitly-unknown errors
|
||||||
// classify identically (e.g. the bug-report hint in ui.ErrorExit).
|
// classify identically (e.g. the bug-report hint in ui.ErrorExit).
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import (
|
|||||||
const stateFileName = "proxy-state.json"
|
const stateFileName = "proxy-state.json"
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
CACertPath string `json:"ca_cert_path"`
|
CACertPath string `json:"ca_cert_path"`
|
||||||
|
BlockedCount int `json:"blocked_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func StatePath(configDir string) string {
|
func StatePath(configDir string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user