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