mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
introduce malysis interceptor
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/proxy"
|
||||
"github.com/safedep/pmg/proxy/certmanager"
|
||||
)
|
||||
@@ -49,12 +50,18 @@ func main() {
|
||||
log.Fatalf("Failed to create cert manager: %v", err)
|
||||
}
|
||||
|
||||
malysisQueryAnalyzer, err := analyzer.NewMalysisQueryAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
|
||||
if err != nil {
|
||||
fmt.Println("failed to initialise malysis query analyser: ", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Create proxy with certificate manager and logging interceptor
|
||||
proxyConfig := &proxy.ProxyConfig{
|
||||
ListenAddr: listenAddr,
|
||||
CertManager: certMgr,
|
||||
EnableMITM: true,
|
||||
Interceptors: []proxy.Interceptor{newLoggingInterceptor()},
|
||||
Interceptors: []proxy.Interceptor{newMalysisInterceptor(malysisQueryAnalyzer)},
|
||||
ConnectTimeout: connectTimeout,
|
||||
RequestTimeout: requestTimeout,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user