mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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.
131 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|