Files
pmg/examples/proxy/malysis_interceptor.go
T

163 lines
4.6 KiB
Go

package main
import (
"context"
"fmt"
"net/url"
"path"
"regexp"
"slices"
"strings"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/proxy"
)
type malysisInterceptor struct {
domains []string
malysisQueryAnalyzer analyzer.PackageVersionAnalyzer
}
func newMalysisInterceptor(analyzer analyzer.PackageVersionAnalyzer) *malysisInterceptor {
return &malysisInterceptor{
malysisQueryAnalyzer: analyzer,
domains: []string{
"registry.npmjs.org",
"registry.yarnpkg.com",
"pypi.org",
"files.pythonhosted.org",
},
}
}
func (m *malysisInterceptor) Name() string {
return "logging-interceptor"
}
func (m *malysisInterceptor) ShouldIntercept(ctx *proxy.RequestContext) bool {
return slices.Contains(m.domains, ctx.Hostname)
}
// parseNpmTarballURL parses tarball URLs such as
//
// https://registry.npmjs.org/send/-/send-1.2.1.tgz
//
// and returns a PackageVersion with Ecosystem_NPM, Name and Version set.
//
// The parser:
// - extracts the filename from the URL path
// - strips common extensions (.tgz, .tar.gz)
// - finds a semver-like suffix in the filename and treats it as the version
// - derives the package name as the filename without the version suffix
// - attempts to detect scoped package names from earlier path segments (e.g. @scope/pkg)
func parseNpmTarballURL(raw string) (*packagev1.PackageVersion, error) {
u, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("failed to parse url: %w", err)
}
filename := path.Base(u.Path) // e.g. send-1.2.1.tgz
name := filename
if strings.HasSuffix(name, ".tgz") {
name = strings.TrimSuffix(name, ".tgz")
} else if strings.HasSuffix(name, ".tar.gz") {
name = strings.TrimSuffix(name, ".tar.gz")
}
// Capture semver-like version at the end of the filename.
// Examples matched: 1.2.3, 1.2.3-beta.1, 1.2.3+meta, 1.2.3-beta.1+meta
re := regexp.MustCompile(`-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]*)?)$`)
m := re.FindStringSubmatch(name)
if len(m) < 2 {
return nil, fmt.Errorf("version not found in filename %s", filename)
}
version := m[1]
pkgName := strings.TrimSuffix(name, "-"+version)
// Try to detect scoped package name from the path segments, e.g.
// /@scope%2Fpkg/-/pkg-1.0.0.tgz -> segment "@scope%2Fpkg"
// decode any percent-encoded segments and prefer a decoded value that contains "/"
seg := strings.Split(u.Path, "/")
for _, s := range seg {
if strings.Contains(s, "%2F") || strings.Contains(s, "%2f") || strings.HasPrefix(s, "@") {
dec, err := url.PathUnescape(s)
if err == nil && strings.Contains(dec, "/") {
// dec is like "@scope/pkg"
pkgName = dec
break
}
if err == nil && strings.HasPrefix(dec, "@") {
// fallback: keep the decoded @scope if present
pkgName = dec
break
}
}
}
pv := &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: pkgName,
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: version,
}
return pv, nil
}
func (m *malysisInterceptor) HandleRequest(ctx *proxy.RequestContext) (*proxy.InterceptorResponse, error) {
url := ctx.URL.String()
isTarball := strings.Contains(url, "tgz")
if !isTarball {
return &proxy.InterceptorResponse{
Action: proxy.ActionAllow,
}, nil
}
// Parse the tarball URL into a PackageVersion and attach it to the request context
pv, err := parseNpmTarballURL(url)
if err != nil {
fmt.Printf("Failed to parse npm tarball [%s] with error: %s", url, err.Error())
return &proxy.InterceptorResponse{
Action: proxy.ActionAllow,
}, nil
}
result, err := m.malysisQueryAnalyzer.Analyze(context.Background(), pv)
if err != nil {
fmt.Printf("failed to analyse package %s with error %s:", pv.Package.Name, err.Error())
return &proxy.InterceptorResponse{
Action: proxy.ActionAllow,
}, nil
}
switch result.Action {
case analyzer.ActionAllow:
fmt.Printf("Package %s@%s -> action: ALLOW\n", pv.Package.Name, pv.Version)
return &proxy.InterceptorResponse{
Action: proxy.ActionAllow,
}, nil
case analyzer.ActionConfirm:
fmt.Printf("Package %s@%s -> action: CONFIRM (mapping to BLOCK)\n", pv.Package.Name, pv.Version)
return &proxy.InterceptorResponse{
Action: proxy.ActionBlock,
}, nil
case analyzer.ActionBlock:
fmt.Printf("Package %s@%s -> action: BLOCK\n", pv.Package.Name, pv.Version)
return &proxy.InterceptorResponse{
Action: proxy.ActionBlock,
}, nil
default:
fmt.Printf("Package %s@%s -> action: UNKNOWN (default to ALLOW)\n", pv.Package.Name, pv.Version)
return &proxy.InterceptorResponse{
Action: proxy.ActionAllow,
}, nil
}
}