Files
pmg/proxy/interceptors/npm_registry.go
31f23fd065 Add support for package executors and support for PTY handling (#100)
* define contract for package executors

* introduce npx executor

* add npx and pnpx cmd support

* fix typo

* rm PackageExecutor and depend on PackageManager interface

* add support for PTY to handle parent-child process interaction

* refactor PTY handling in proxy flow

* enforce interactiveSession interface check

* close reader explicitly and clean npm version for pkg executors

* rm interaction from interceptors

* add docs and wait for outputRouter before exit

* add support for non interactive TTY for proxy mode

* add support for CI env var check for non interactive tty proxy mode

* update readme to include npx, pnpx support

* Update internal/flows/proxy_flow.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* update ptyx lib

* fix docs typo

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-09 22:03:42 +05:30

89 lines
2.8 KiB
Go

package interceptors
import (
"strings"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/proxy"
)
var (
npmRegistryDomains = []string{
"registry.npmjs.org",
"registry.yarnpkg.com",
}
)
// NpmRegistryInterceptor intercepts NPM registry requests and analyzes packages for malware
// It embeds baseRegistryInterceptor to reuse ecosystem agnostic functionality
type NpmRegistryInterceptor struct {
baseRegistryInterceptor
}
var _ proxy.Interceptor = (*NpmRegistryInterceptor)(nil)
// NewNpmRegistryInterceptor creates a new NPM registry interceptor
func NewNpmRegistryInterceptor(
analyzer analyzer.PackageVersionAnalyzer,
cache AnalysisCache,
confirmationChan chan *ConfirmationRequest,
) *NpmRegistryInterceptor {
return &NpmRegistryInterceptor{
baseRegistryInterceptor: baseRegistryInterceptor{
analyzer: analyzer,
cache: cache,
confirmationChan: confirmationChan,
},
}
}
// Name returns the interceptor name for logging
func (i *NpmRegistryInterceptor) Name() string {
return "npm-registry-interceptor"
}
// ShouldIntercept determines if this interceptor should handle the given request
func (i *NpmRegistryInterceptor) ShouldIntercept(ctx *proxy.RequestContext) bool {
for _, domain := range npmRegistryDomains {
if ctx.Hostname == domain || strings.HasSuffix(ctx.Hostname, "."+domain) {
return true
}
}
return false
}
// HandleRequest processes the request and returns response action
// We take a fail-open approach here, allowing requests that we can't parse the package information from the URL.
func (i *NpmRegistryInterceptor) HandleRequest(ctx *proxy.RequestContext) (*proxy.InterceptorResponse, error) {
log.Debugf("[%s] Handling NPM registry request: %s", ctx.RequestID, ctx.URL.Path)
pkgInfo, err := parseNpmRegistryURL(ctx.URL.Path)
if err != nil {
log.Warnf("[%s] Failed to parse NPM registry URL %s: %v", ctx.RequestID, ctx.URL.Path, err)
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
}
// Only analyze tarball downloads (these have a specific version)
// Metadata requests (without version) are allowed through
if !pkgInfo.IsTarball {
log.Debugf("[%s] Skipping analysis for metadata request: %s", ctx.RequestID, pkgInfo.Name)
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
}
result, err := i.baseRegistryInterceptor.analyzePackage(
ctx,
packagev1.Ecosystem_ECOSYSTEM_NPM,
pkgInfo.Name,
pkgInfo.Version,
)
if err != nil {
log.Errorf("[%s] Failed to analyze package %s@%s: %v", ctx.RequestID, pkgInfo.Name, pkgInfo.Version, err)
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
}
return i.baseRegistryInterceptor.handleAnalysisResult(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, pkgInfo.Name, pkgInfo.Version, result)
}