Files
pmg/cmd/proxy/start.go
T
Sahilb315 ea2c7e8242 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.
2026-06-23 17:07:07 +05:30

131 lines
4.1 KiB
Go

package proxy
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/flows"
"github.com/safedep/pmg/internal/proxystate"
pmgproxy "github.com/safedep/pmg/proxy"
"github.com/safedep/pmg/proxy/certmanager"
"github.com/safedep/pmg/proxy/interceptors"
"github.com/spf13/cobra"
)
func newStartCommand() *cobra.Command {
return &cobra.Command{
Use: "start",
Short: "Start the persistent PMG proxy server (runs in foreground)",
RunE: runStart,
}
}
func runStart(_ *cobra.Command, _ []string) error {
cfg := config.Get()
statePath := proxystate.StatePath(cfg.ConfigDir())
if existing, err := proxystate.Read(statePath); err == nil && existing.IsRunning() {
return fmt.Errorf("proxy already running (pid %d, addr %s) — run 'pmg proxy stop' first", existing.PID, existing.Addr)
}
caCertPath := filepath.Join(cfg.ConfigDir(), "proxy-ca.pem")
caCert, _, err := flows.SetupCACertificate(cfg.ConfigDir(), caCertPath)
if err != nil {
return fmt.Errorf("setup CA certificate: %w", err)
}
certMgr, err := certmanager.NewCertificateManagerWithCA(caCert, certmanager.DefaultCertManagerConfig())
if err != nil {
return fmt.Errorf("create certificate manager: %w", err)
}
malysisAnalyzer, err := analyzer.NewMalysisAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
if err != nil {
return fmt.Errorf("create analyzer: %w", err)
}
cache := interceptors.NewInMemoryAnalysisCache()
stats := interceptors.NewAnalysisStatsCollector()
confirmationChan := make(chan *interceptors.ConfirmationRequest, 100)
go autoBlockConfirmations(confirmationChan)
factory := interceptors.NewInterceptorFactory(
malysisAnalyzer, cache, stats, confirmationChan, interceptors.InterceptorContext{},
)
var interceptorList []pmgproxy.Interceptor
for _, eco := range interceptors.SupportedEcosystems() {
i, ferr := factory.CreateInterceptor(eco)
if ferr != nil {
return fmt.Errorf("create interceptor for %s: %w", eco.String(), ferr)
}
interceptorList = append(interceptorList, i)
}
interceptorList = append(interceptorList, interceptors.NewAuditLoggerInterceptor())
proxyConfig := pmgproxy.DefaultProxyConfig()
proxyConfig.CertManager = certMgr
proxyConfig.Interceptors = interceptorList
server, err := pmgproxy.NewProxyServer(proxyConfig)
if err != nil {
return fmt.Errorf("create proxy server: %w", err)
}
if err := server.Start(); err != nil {
return fmt.Errorf("start proxy server: %w", err)
}
state := proxystate.State{
PID: os.Getpid(),
Addr: server.Address(),
CACertPath: caCertPath,
}
if err := proxystate.Write(statePath, state); err != nil {
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Stop(stopCtx)
return fmt.Errorf("write proxy state: %w", err)
}
log.Infof("PMG persistent proxy running on %s (pid %d)", state.Addr, state.PID)
if _, err := fmt.Fprintf(os.Stderr, "PMG proxy running on %s\nRun: eval $(pmg proxy env) # or: pmg proxy env --gha\n", state.Addr); err != nil {
log.Warnf("failed to write startup message: %v", err)
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
<-sigCh
close(confirmationChan)
// 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()
return server.Stop(stopCtx)
}
// autoBlockConfirmations drains the confirmation channel and always denies,
// appropriate for non-interactive CI/CD environments.
func autoBlockConfirmations(ch chan *interceptors.ConfirmationRequest) {
for req := range ch {
log.Warnf("Persistent proxy: auto-blocking suspicious package %s", req.PackageVersion.GetPackage().GetName())
req.ResponseChan <- false
close(req.ResponseChan)
}
}