introduce malysis interceptor

This commit is contained in:
Sahilb315
2025-12-31 13:48:08 +05:30
parent 2bc500f817
commit 204fb44a8e
2 changed files with 170 additions and 1 deletions
+8 -1
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
"github.com/safedep/pmg/proxy/certmanager" "github.com/safedep/pmg/proxy/certmanager"
) )
@@ -49,12 +50,18 @@ func main() {
log.Fatalf("Failed to create cert manager: %v", err) 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 // Create proxy with certificate manager and logging interceptor
proxyConfig := &proxy.ProxyConfig{ proxyConfig := &proxy.ProxyConfig{
ListenAddr: listenAddr, ListenAddr: listenAddr,
CertManager: certMgr, CertManager: certMgr,
EnableMITM: true, EnableMITM: true,
Interceptors: []proxy.Interceptor{newLoggingInterceptor()}, Interceptors: []proxy.Interceptor{newMalysisInterceptor(malysisQueryAnalyzer)},
ConnectTimeout: connectTimeout, ConnectTimeout: connectTimeout,
RequestTimeout: requestTimeout, RequestTimeout: requestTimeout,
} }
+162
View File
@@ -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
}
}