fix: Proxy flow should respect trusted packages (#96)

* fix: Handle trusted packages in proxy flow

* perf: Pre-parse trusted PURLs

* fix: Code review fixes

* fix: Remove unused config
This commit is contained in:
Abhisek Datta
2026-01-08 00:15:02 +05:30
committed by GitHub
parent 028e78aed8
commit 1c319eba0e
7 changed files with 373 additions and 304 deletions
+14
View File
@@ -8,6 +8,9 @@ import (
"strconv" "strconv"
_ "embed" _ "embed"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log"
) )
const ( const (
@@ -62,6 +65,13 @@ type Config struct {
type TrustedPackage struct { type TrustedPackage struct {
Purl string `mapstructure:"purl"` Purl string `mapstructure:"purl"`
Reason string `mapstructure:"reason"` Reason string `mapstructure:"reason"`
// Pre-parsed PURL components (not serialized, computed at load time)
// These fields avoid repeated PURL parsing on every IsTrustedPackage() call
parsed bool
ecosystem packagev1.Ecosystem
name string
version string
} }
// RuntimeConfig is the configuration that is used at runtime. It contains static configuration // RuntimeConfig is the configuration that is used at runtime. It contains static configuration
@@ -152,6 +162,10 @@ func initConfig() {
globalConfig.eventLogDir = eventLogDir globalConfig.eventLogDir = eventLogDir
loadConfig() loadConfig()
if err := preprocessTrustedPackages(&globalConfig.Config); err != nil {
log.Warnf("Failed to preprocess trusted packages: %v", err)
}
} }
// loadConfig loads the configuration from the config file. // loadConfig loads the configuration from the config file.
+74
View File
@@ -0,0 +1,74 @@
package config
import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/api/pb"
"github.com/safedep/dry/log"
)
// IsTrustedPackage checks if a package version is trusted based on global configuration.
// This is the primary API that should be used by guard and proxy flows.
// It returns true if the package is in the trusted packages list, false otherwise.
func IsTrustedPackage(pkgVersion *packagev1.PackageVersion) bool {
return isTrustedPackageVersion(Get().Config.TrustedPackages, pkgVersion)
}
// preprocessTrustedPackages pre-parses all PURL strings in trusted packages.
// This is called once during config load to avoid repeated parsing during
// trusted package checks. Invalid PURLs are logged but not fatal.
func preprocessTrustedPackages(cfg *Config) error {
for i := range cfg.TrustedPackages {
tp := &cfg.TrustedPackages[i]
parsedPurl, err := pb.NewPurlPackageVersion(tp.Purl)
if err != nil {
log.Warnf("Failed to parse trusted package PURL: %s: %v", tp.Purl, err)
tp.parsed = false
continue
}
tp.parsed = true
tp.ecosystem = parsedPurl.Ecosystem()
tp.name = parsedPurl.Name()
tp.version = parsedPurl.Version()
}
return nil
}
// isTrustedPackageVersion checks if a package version is in the trusted packages list.
//
// It matches based on ecosystem, package name, and optionally version.
// If the trusted package PURL doesn't specify a version, all versions of that package are trusted.
// Returns false if pkgVersion is nil or if trustedPackages is empty.
func isTrustedPackageVersion(trustedPackages []TrustedPackage, pkgVersion *packagev1.PackageVersion) bool {
if pkgVersion == nil {
return false
}
if len(trustedPackages) == 0 {
return false
}
for _, v := range trustedPackages {
if !v.parsed {
continue
}
if v.ecosystem != pkgVersion.GetPackage().GetEcosystem() {
continue
}
if v.name != pkgVersion.GetPackage().GetName() {
continue
}
if v.version != "" && v.version != pkgVersion.GetVersion() {
continue
}
return true
}
return false
}
+267
View File
@@ -0,0 +1,267 @@
package config
import (
"testing"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/stretchr/testify/assert"
)
func TestIsTrustedPackageVersion(t *testing.T) {
tests := []struct {
name string
trustedPackages []TrustedPackage
pkgVersion *packagev1.PackageVersion
want bool
}{
{
name: "nil package version returns false",
trustedPackages: []TrustedPackage{},
pkgVersion: nil,
want: false,
},
{
name: "empty trusted packages list returns false",
trustedPackages: []TrustedPackage{},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "exact match with version returns true",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/express@4.18.0",
Reason: "trusted by team",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "match without version in trusted package returns true",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/express",
Reason: "all versions trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "version mismatch returns false",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/express@4.17.0",
Reason: "old version trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "name mismatch returns false",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/react@18.0.0",
Reason: "trusted package",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "ecosystem mismatch returns false",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:pypi/requests@2.28.0",
Reason: "trusted package",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "requests",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "2.28.0",
},
want: false,
},
{
name: "pypi package exact match returns true",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:pypi/requests@2.28.0",
Reason: "trusted http library",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "requests",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_PYPI,
},
Version: "2.28.0",
},
want: true,
},
{
name: "multiple trusted packages finds correct match",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/lodash@4.17.21",
Reason: "utility library",
},
{
Purl: "pkg:npm/express@4.18.0",
Reason: "web framework",
},
{
Purl: "pkg:pypi/requests@2.28.0",
Reason: "http library",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "multiple trusted packages no match returns false",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/lodash@4.17.21",
Reason: "utility library",
},
{
Purl: "pkg:npm/react@18.0.0",
Reason: "ui library",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "invalid purl in trusted packages skips and returns false",
trustedPackages: []TrustedPackage{
{
Purl: "invalid-purl-format",
Reason: "malformed",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "invalid purl skipped but valid match found",
trustedPackages: []TrustedPackage{
{
Purl: "invalid-purl-format",
Reason: "malformed",
},
{
Purl: "pkg:npm/express@4.18.0",
Reason: "valid trusted package",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "package version without version field matches versionless trusted package",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/express",
Reason: "all versions trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "",
},
want: true,
},
{
name: "package version without version field does not match versioned trusted package",
trustedPackages: []TrustedPackage{
{
Purl: "pkg:npm/express@4.18.0",
Reason: "specific version trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Pre-process trusted packages to populate pre-parsed fields
cfg := &Config{TrustedPackages: tt.trustedPackages}
_ = preprocessTrustedPackages(cfg)
got := isTrustedPackageVersion(cfg.TrustedPackages, tt.pkgVersion)
assert.Equal(t, tt.want, got)
})
}
}
+1 -39
View File
@@ -10,7 +10,6 @@ import (
"time" "time"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/api/pb"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config" "github.com/safedep/pmg/config"
@@ -45,7 +44,6 @@ type PackageManagerGuardConfig struct {
AnalysisTimeout time.Duration AnalysisTimeout time.Duration
DryRun bool DryRun bool
InsecureInstallation bool InsecureInstallation bool
TrustedPackages []config.TrustedPackage
} }
func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig { func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
@@ -55,45 +53,9 @@ func DefaultPackageManagerGuardConfig() PackageManagerGuardConfig {
AnalysisTimeout: 5 * time.Minute, AnalysisTimeout: 5 * time.Minute,
DryRun: false, DryRun: false,
InsecureInstallation: false, InsecureInstallation: false,
TrustedPackages: []config.TrustedPackage{},
} }
} }
func (c *PackageManagerGuardConfig) IsTrustedPackageVersion(pkgVersion *packagev1.PackageVersion) bool {
if pkgVersion == nil {
return false
}
trustedPkgs := c.TrustedPackages
if len(trustedPkgs) == 0 {
return false
}
for _, v := range trustedPkgs {
purlTrustedPackageVersion, err := pb.NewPurlPackageVersion(v.Purl)
if err != nil {
log.Warnf("failed to parse trusted package version: %s: %v", v.Purl, err)
continue
}
if purlTrustedPackageVersion.Version() != "" && purlTrustedPackageVersion.Version() != pkgVersion.GetVersion() {
continue
}
if purlTrustedPackageVersion.Name() != pkgVersion.GetPackage().GetName() {
continue
}
if purlTrustedPackageVersion.Ecosystem() != pkgVersion.GetPackage().GetEcosystem() {
continue
}
return true
}
return false
}
type packageManagerGuard struct { type packageManagerGuard struct {
config PackageManagerGuardConfig config PackageManagerGuardConfig
interaction PackageManagerGuardInteraction interaction PackageManagerGuardInteraction
@@ -293,7 +255,7 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
// Queue all packages for analysis // Queue all packages for analysis
for _, pkg := range packages { for _, pkg := range packages {
if g.config.IsTrustedPackageVersion(pkg) { if config.IsTrustedPackage(pkg) {
log.Debugf("Skipping trusted package: %s/%s@%s", log.Debugf("Skipping trusted package: %s/%s@%s",
pkg.GetPackage().GetEcosystem(), pkg.GetPackage().GetName(), pkg.GetVersion()) pkg.GetPackage().GetEcosystem(), pkg.GetPackage().GetName(), pkg.GetVersion())
-259
View File
@@ -6,7 +6,6 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager" "github.com/safedep/pmg/packagemanager"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -270,261 +269,3 @@ func TestGuardInsecureInstallation(t *testing.T) {
}) })
} }
func TestGuardIsTrustedPackageVersion(t *testing.T) {
tests := []struct {
name string
trustedPackages []config.TrustedPackage
pkgVersion *packagev1.PackageVersion
want bool
}{
{
name: "nil package version returns false",
trustedPackages: []config.TrustedPackage{},
pkgVersion: nil,
want: false,
},
{
name: "empty trusted packages list returns false",
trustedPackages: []config.TrustedPackage{},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "exact match with version returns true",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/express@4.18.0",
Reason: "trusted by team",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "match without version in trusted package returns true",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/express",
Reason: "all versions trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "version mismatch returns false",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/express@4.17.0",
Reason: "old version trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "name mismatch returns false",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/react@18.0.0",
Reason: "trusted package",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "ecosystem mismatch returns false",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:pypi/requests@2.28.0",
Reason: "trusted package",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "requests",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "2.28.0",
},
want: false,
},
{
name: "pypi package exact match returns true",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:pypi/requests@2.28.0",
Reason: "trusted http library",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "requests",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_PYPI,
},
Version: "2.28.0",
},
want: true,
},
{
name: "multiple trusted packages finds correct match",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/lodash@4.17.21",
Reason: "utility library",
},
{
Purl: "pkg:npm/express@4.18.0",
Reason: "web framework",
},
{
Purl: "pkg:pypi/requests@2.28.0",
Reason: "http library",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "multiple trusted packages no match returns false",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/lodash@4.17.21",
Reason: "utility library",
},
{
Purl: "pkg:npm/react@18.0.0",
Reason: "ui library",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "invalid purl in trusted packages skips and returns false",
trustedPackages: []config.TrustedPackage{
{
Purl: "invalid-purl-format",
Reason: "malformed",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: false,
},
{
name: "invalid purl skipped but valid match found",
trustedPackages: []config.TrustedPackage{
{
Purl: "invalid-purl-format",
Reason: "malformed",
},
{
Purl: "pkg:npm/express@4.18.0",
Reason: "valid trusted package",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "4.18.0",
},
want: true,
},
{
name: "package version without version field matches versionless trusted package",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/express",
Reason: "all versions trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "",
},
want: true,
},
{
name: "package version without version field does not match versioned trusted package",
trustedPackages: []config.TrustedPackage{
{
Purl: "pkg:npm/express@4.18.0",
Reason: "specific version trusted",
},
},
pkgVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: "express",
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: "",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := PackageManagerGuardConfig{
TrustedPackages: tt.trustedPackages,
}
got := config.IsTrustedPackageVersion(tt.pkgVersion)
assert.Equal(t, tt.want, got)
})
}
}
-1
View File
@@ -57,7 +57,6 @@ func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagem
guardConfig := guard.DefaultPackageManagerGuardConfig() guardConfig := guard.DefaultPackageManagerGuardConfig()
guardConfig.DryRun = config.DryRun guardConfig.DryRun = config.DryRun
guardConfig.InsecureInstallation = config.InsecureInstallation guardConfig.InsecureInstallation = config.InsecureInstallation
guardConfig.TrustedPackages = config.Config.TrustedPackages
proxy, err := guard.NewPackageManagerGuard(guardConfig, f.pm, f.packageResolver, analyzers, interaction) proxy, err := guard.NewPackageManagerGuard(guardConfig, f.pm, f.packageResolver, analyzers, interaction)
if err != nil { if err != nil {
+17 -5
View File
@@ -9,6 +9,7 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/guard" "github.com/safedep/pmg/guard"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
) )
@@ -47,11 +48,7 @@ func (b *baseRegistryInterceptor) analyzePackage(
packageName string, packageName string,
packageVersion string, packageVersion string,
) (*analyzer.PackageVersionAnalysisResult, error) { ) (*analyzer.PackageVersionAnalysisResult, error) {
if cached, ok := b.cache.Get(ecosystem.String(), packageName, packageVersion); ok { // Check if package is trusted before analyzing
log.Debugf("[%s] Using cached analysis result for %s@%s", ctx.RequestID, packageName, packageVersion)
return cached, nil
}
pkgVersion := &packagev1.PackageVersion{ pkgVersion := &packagev1.PackageVersion{
Package: &packagev1.Package{ Package: &packagev1.Package{
Ecosystem: ecosystem, Ecosystem: ecosystem,
@@ -60,6 +57,21 @@ func (b *baseRegistryInterceptor) analyzePackage(
Version: packageVersion, Version: packageVersion,
} }
if config.IsTrustedPackage(pkgVersion) {
log.Debugf("[%s] Skipping trusted package: %s/%s@%s",
ctx.RequestID, ecosystem.String(), packageName, packageVersion)
return &analyzer.PackageVersionAnalysisResult{
PackageVersion: pkgVersion,
Action: analyzer.ActionAllow,
}, nil
}
if cached, ok := b.cache.Get(ecosystem.String(), packageName, packageVersion); ok {
log.Debugf("[%s] Using cached analysis result for %s@%s", ctx.RequestID, packageName, packageVersion)
return cached, nil
}
log.Debugf("[%s] Analyzing package %s@%s", ctx.RequestID, packageName, packageVersion) log.Debugf("[%s] Analyzing package %s@%s", ctx.RequestID, packageName, packageVersion)
analysisCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) analysisCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)