mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: authenticated Malysis analyzer with tenant exclusion support (#313)
* feat: authenticated Malysis analyzer with tenant exclusion support When SafeDep Cloud credentials are available (keychain or environment), PMG now uses an authenticated malware analysis query against api.safedep.io instead of the unauthenticated community endpoint (community-api.safedep.io). The API key and tenant ID are supplied via the gRPC connection. The authenticated response may carry a tenant-specific malicious package exclusion. This is honored as an opt-in trust signal: a flagged package is downgraded to allow only when a concrete exclusion (non-empty ID) is present for the exact package version queried. Exclusions are never honored for community queries and never weaken the verdict for packages that were not flagged. Allowed-by-exclusion packages are surfaced as a warning so the trust decision is never silent. Changes are additive; non-authenticated usage is unchanged. Credential resolution is extracted into internal/cloudauth and reused by both the analyzer factory and the existing cloud sync client. * fix: surface tenant exclusions in proxy mode; clarify comments - Warn when proxy interceptor allows a flagged package due to a tenant exclusion, matching the guard flow so the trust decision is not silent. - Remove stray doc comment above warnIfExcluded. - Clarify that a verified-malware verdict can be downgraded by an exclusion in applyExclusion. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,14 @@ type PackageVersionAnalysisResult struct {
|
||||
// Whether the malware verdict has been verified (confirmed by a human or verification system)
|
||||
IsVerified bool
|
||||
|
||||
// Whether a tenant-specific exclusion caused this package to be trusted
|
||||
// despite a malware verdict. Only set by authenticated analyzers.
|
||||
IsExcluded bool
|
||||
|
||||
// The tenant-specific exclusion that trusted this package, when IsExcluded is true
|
||||
ExclusionID string
|
||||
ExclusionReason string
|
||||
|
||||
// Analyzer specific data
|
||||
Data any
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/internal/cloudauth"
|
||||
)
|
||||
|
||||
// NewMalysisAnalyzer creates the malysis query analyzer best suited for the
|
||||
// current environment. When SafeDep Cloud credentials are available (via
|
||||
// keychain or environment), it returns an authenticated analyzer that queries
|
||||
// api.safedep.io and honors tenant-specific package exclusions. Otherwise it
|
||||
// falls back to the unauthenticated community analyzer.
|
||||
func NewMalysisAnalyzer(config MalysisQueryAnalyzerConfig) (PackageVersionAnalyzer, error) {
|
||||
creds, closeResolver, err := cloudauth.ResolveCredentials()
|
||||
if err != nil {
|
||||
log.Debugf("SafeDep Cloud credentials unavailable, using community malysis analyzer: %v", err)
|
||||
return NewMalysisQueryAnalyzer(config)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := closeResolver(); closeErr != nil {
|
||||
log.Warnf("failed to close credential resolver: %v", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Debugf("SafeDep Cloud credentials found, using authenticated malysis analyzer")
|
||||
return NewMalysisAuthenticatedQueryAnalyzer(config, creds)
|
||||
}
|
||||
@@ -10,23 +10,37 @@ import (
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/malysis/v1"
|
||||
drygrpc "github.com/safedep/dry/adapters/grpc"
|
||||
"github.com/safedep/dry/cloud"
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/config"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
communityMalysisHost = "community-api.safedep.io"
|
||||
communityMalysisPort = "443"
|
||||
)
|
||||
|
||||
type MalysisQueryAnalyzerConfig struct{}
|
||||
|
||||
type malysisQueryAnalyzer struct {
|
||||
client malysisv1grpc.MalwareAnalysisServiceClient
|
||||
Config MalysisQueryAnalyzerConfig
|
||||
|
||||
// honorExclusions enables honoring tenant-specific malicious package
|
||||
// exclusions returned by authenticated queries. Exclusions are never
|
||||
// returned for unauthenticated (community) queries.
|
||||
honorExclusions bool
|
||||
}
|
||||
|
||||
var _ Analyzer = &malysisQueryAnalyzer{}
|
||||
var _ PackageVersionAnalyzer = &malysisQueryAnalyzer{}
|
||||
|
||||
// NewMalysisQueryAnalyzer creates an unauthenticated analyzer that queries the
|
||||
// SafeDep community malware analysis service.
|
||||
func NewMalysisQueryAnalyzer(config MalysisQueryAnalyzerConfig) (*malysisQueryAnalyzer, error) {
|
||||
client, err := drygrpc.GrpcClient("pmg-malysis-query",
|
||||
"community-api.safedep.io", "443", "", http.Header{}, []grpc.DialOption{})
|
||||
communityMalysisHost, communityMalysisPort, "", http.Header{}, []grpc.DialOption{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gRPC client: %w", err)
|
||||
}
|
||||
@@ -37,6 +51,25 @@ func NewMalysisQueryAnalyzer(config MalysisQueryAnalyzerConfig) (*malysisQueryAn
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewMalysisAuthenticatedQueryAnalyzer creates an analyzer that queries the
|
||||
// authenticated SafeDep Cloud malware analysis service (api.safedep.io) using
|
||||
// the provided API key credentials. The analysis behavior is identical to the
|
||||
// community analyzer except that it additionally honors tenant-specific
|
||||
// malicious package exclusions returned in the response.
|
||||
func NewMalysisAuthenticatedQueryAnalyzer(config MalysisQueryAnalyzerConfig,
|
||||
creds *cloud.Credentials) (*malysisQueryAnalyzer, error) {
|
||||
cloudClient, err := cloud.NewDataPlaneClient("pmg-malysis-query", creds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create authenticated gRPC client: %w", err)
|
||||
}
|
||||
|
||||
return &malysisQueryAnalyzer{
|
||||
client: malysisv1grpc.NewMalwareAnalysisServiceClient(cloudClient.Connection()),
|
||||
Config: config,
|
||||
honorExclusions: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *malysisQueryAnalyzer) Name() string {
|
||||
return "malysis-query"
|
||||
}
|
||||
@@ -75,16 +108,54 @@ func (a *malysisQueryAnalyzer) Analyze(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
// This is a confirmed malicious package, we must always block it
|
||||
// A confirmed malicious package is blocked here, unless a tenant exclusion
|
||||
// downgrades it to allow in applyExclusion below.
|
||||
if res.GetVerificationRecord().GetIsMalware() {
|
||||
analysisResult.IsMalware = true
|
||||
analysisResult.IsVerified = true
|
||||
analysisResult.Action = ActionBlock
|
||||
}
|
||||
|
||||
// Honor tenant-specific exclusion as an opt-in trust signal. This is only
|
||||
// applied for authenticated queries and only when the package was actually
|
||||
// flagged. The exclusion in the response is scoped by the server to the
|
||||
// exact package version we queried, so it is an exact match by construction.
|
||||
a.applyExclusion(analysisResult, res)
|
||||
|
||||
return analysisResult, nil
|
||||
}
|
||||
|
||||
// applyExclusion downgrades a flagged package to ActionAllow when the
|
||||
// authenticated response carries a tenant-specific malicious package exclusion.
|
||||
// The exclusion is honored only when the package was flagged as malware, so it
|
||||
// never weakens the verdict for packages that were already allowed.
|
||||
func (a *malysisQueryAnalyzer) applyExclusion(result *PackageVersionAnalysisResult,
|
||||
res *malysisv1.QueryPackageAnalysisResponse) {
|
||||
if !a.honorExclusions {
|
||||
return
|
||||
}
|
||||
|
||||
exclusion := res.GetMaliciousPackageExclusion()
|
||||
if exclusion == nil || exclusion.GetExclusionId() == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if !result.IsMalware {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Honoring tenant exclusion %q for package %s@%s: %s",
|
||||
exclusion.GetExclusionId(),
|
||||
result.PackageVersion.GetPackage().GetName(),
|
||||
result.PackageVersion.GetVersion(),
|
||||
exclusion.GetReason())
|
||||
|
||||
result.IsExcluded = true
|
||||
result.ExclusionID = exclusion.GetExclusionId()
|
||||
result.ExclusionReason = exclusion.GetReason()
|
||||
result.Action = ActionAllow
|
||||
}
|
||||
|
||||
func malysisReportUrl(analysisId string) string {
|
||||
return fmt.Sprintf("https://app.safedep.io/community/malysis/%s", analysisId)
|
||||
}
|
||||
|
||||
@@ -150,6 +150,127 @@ func TestMalysisQueryAnalyzer_AlwaysBlockOnVerifiedMalware(t *testing.T) {
|
||||
assert.Equal(t, ActionBlock, result.Action, "Verified malware must be blocked always")
|
||||
}
|
||||
|
||||
func TestMalysisQueryAnalyzer_HonorsExclusionForFlaggedPackage(t *testing.T) {
|
||||
cfg := config.Get()
|
||||
origParanoid := cfg.Config.Paranoid
|
||||
cfg.Config.Paranoid = false
|
||||
defer func() { cfg.Config.Paranoid = origParanoid }()
|
||||
|
||||
// Verified malware that would normally be blocked, but the tenant has an
|
||||
// exclusion trusting this exact package version.
|
||||
resp := &malysisv1.QueryPackageAnalysisResponse{
|
||||
AnalysisId: "analysis-excl",
|
||||
Report: &malysisv1pb.Report{
|
||||
Inference: &malysisv1pb.Report_Inference{
|
||||
IsMalware: true,
|
||||
Summary: "Suspicious patterns detected",
|
||||
},
|
||||
},
|
||||
VerificationRecord: &malysisv1pb.VerificationRecord{
|
||||
IsMalware: true,
|
||||
},
|
||||
MaliciousPackageExclusion: &malysisv1.QueryPackageAnalysisResponse_MaliciousPackageExclusion{
|
||||
ExclusionId: "excl-1",
|
||||
Reason: "Reviewed and trusted internally",
|
||||
},
|
||||
}
|
||||
|
||||
an := &malysisQueryAnalyzer{
|
||||
client: &stubMalwareAnalysisServiceClient{resp: resp},
|
||||
honorExclusions: true,
|
||||
}
|
||||
|
||||
pv := makePkgVersion("trusted-internal-pkg", "1.2.3")
|
||||
result, err := an.Analyze(context.Background(), pv)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ActionAllow, result.Action, "Exclusion should downgrade a flagged package to allow")
|
||||
assert.True(t, result.IsExcluded)
|
||||
assert.Equal(t, "excl-1", result.ExclusionID)
|
||||
assert.Equal(t, "Reviewed and trusted internally", result.ExclusionReason)
|
||||
// Inference flags are retained for reporting/audit
|
||||
assert.True(t, result.IsMalware)
|
||||
assert.True(t, result.IsVerified)
|
||||
}
|
||||
|
||||
func TestMalysisQueryAnalyzer_IgnoresExclusionWhenNotEnabled(t *testing.T) {
|
||||
// Community analyzer must never honor exclusions even if one is present.
|
||||
resp := &malysisv1.QueryPackageAnalysisResponse{
|
||||
AnalysisId: "analysis-excl-2",
|
||||
Report: &malysisv1pb.Report{
|
||||
Inference: &malysisv1pb.Report_Inference{IsMalware: true},
|
||||
},
|
||||
VerificationRecord: &malysisv1pb.VerificationRecord{IsMalware: true},
|
||||
MaliciousPackageExclusion: &malysisv1.QueryPackageAnalysisResponse_MaliciousPackageExclusion{
|
||||
ExclusionId: "excl-2",
|
||||
Reason: "Should be ignored",
|
||||
},
|
||||
}
|
||||
|
||||
an := &malysisQueryAnalyzer{
|
||||
client: &stubMalwareAnalysisServiceClient{resp: resp},
|
||||
honorExclusions: false,
|
||||
}
|
||||
|
||||
pv := makePkgVersion("verified-malware", "9.9.9")
|
||||
result, err := an.Analyze(context.Background(), pv)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ActionBlock, result.Action, "Exclusions must be ignored when not enabled")
|
||||
assert.False(t, result.IsExcluded)
|
||||
}
|
||||
|
||||
func TestMalysisQueryAnalyzer_IgnoresEmptyExclusionId(t *testing.T) {
|
||||
// An exclusion with no ID is not a concrete, exact match and must be ignored.
|
||||
resp := &malysisv1.QueryPackageAnalysisResponse{
|
||||
AnalysisId: "analysis-excl-3",
|
||||
Report: &malysisv1pb.Report{
|
||||
Inference: &malysisv1pb.Report_Inference{IsMalware: true},
|
||||
},
|
||||
VerificationRecord: &malysisv1pb.VerificationRecord{IsMalware: true},
|
||||
MaliciousPackageExclusion: &malysisv1.QueryPackageAnalysisResponse_MaliciousPackageExclusion{
|
||||
ExclusionId: "",
|
||||
Reason: "No id",
|
||||
},
|
||||
}
|
||||
|
||||
an := &malysisQueryAnalyzer{
|
||||
client: &stubMalwareAnalysisServiceClient{resp: resp},
|
||||
honorExclusions: true,
|
||||
}
|
||||
|
||||
pv := makePkgVersion("verified-malware", "9.9.9")
|
||||
result, err := an.Analyze(context.Background(), pv)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ActionBlock, result.Action)
|
||||
assert.False(t, result.IsExcluded)
|
||||
}
|
||||
|
||||
func TestMalysisQueryAnalyzer_ExclusionDoesNotAffectCleanPackage(t *testing.T) {
|
||||
// A non-malware package with a spurious exclusion stays allowed and is not
|
||||
// marked as excluded (nothing to trust).
|
||||
resp := &malysisv1.QueryPackageAnalysisResponse{
|
||||
AnalysisId: "analysis-excl-4",
|
||||
Report: &malysisv1pb.Report{
|
||||
Inference: &malysisv1pb.Report_Inference{IsMalware: false},
|
||||
},
|
||||
VerificationRecord: &malysisv1pb.VerificationRecord{IsMalware: false},
|
||||
MaliciousPackageExclusion: &malysisv1.QueryPackageAnalysisResponse_MaliciousPackageExclusion{
|
||||
ExclusionId: "excl-4",
|
||||
Reason: "Stale exclusion",
|
||||
},
|
||||
}
|
||||
|
||||
an := &malysisQueryAnalyzer{
|
||||
client: &stubMalwareAnalysisServiceClient{resp: resp},
|
||||
honorExclusions: true,
|
||||
}
|
||||
|
||||
pv := makePkgVersion("clean-pkg", "1.0.0")
|
||||
result, err := an.Analyze(context.Background(), pv)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ActionAllow, result.Action)
|
||||
assert.False(t, result.IsExcluded)
|
||||
}
|
||||
|
||||
// Implement the full client interface surface expected by malysisv1grpc.MalwareAnalysisServiceClient
|
||||
func (s *stubMalwareAnalysisServiceClient) AnalyzePackage(ctx context.Context, req *malysisv1.AnalyzePackageRequest, opts ...grpc.CallOption) (*malysisv1.AnalyzePackageResponse, error) {
|
||||
// Not used in these tests; return a nil response with no error
|
||||
|
||||
+15
-1
@@ -215,6 +215,7 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedComm
|
||||
confirmableMalwarePackages = append(confirmableMalwarePackages, analysisResult)
|
||||
} else {
|
||||
result.AllowedCount++
|
||||
g.warnIfExcluded(analysisResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +454,7 @@ func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, pa
|
||||
confirmableMalwarePackages = append(confirmableMalwarePackages, analysisResult)
|
||||
} else {
|
||||
result.AllowedCount++
|
||||
g.warnIfExcluded(analysisResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +499,19 @@ func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, pa
|
||||
return result, g.continueExecution(ctx, parsedCommand)
|
||||
}
|
||||
|
||||
// logMalwareDetection logs malware detection events
|
||||
// warnIfExcluded surfaces a security-relevant notice when a flagged package was
|
||||
// allowed only because of a tenant-specific exclusion, so it is never silently
|
||||
// trusted.
|
||||
func (g *packageManagerGuard) warnIfExcluded(result *analyzer.PackageVersionAnalysisResult) {
|
||||
if result == nil || !result.IsExcluded || result.PackageVersion == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pkg := result.PackageVersion
|
||||
g.showWarning(fmt.Sprintf("Allowing flagged package %s@%s due to tenant exclusion (%s)",
|
||||
pkg.GetPackage().GetName(), pkg.GetVersion(), result.ExclusionReason))
|
||||
}
|
||||
|
||||
func (g *packageManagerGuard) logMalwareDetection(result *analyzer.PackageVersionAnalysisResult, blocked bool) {
|
||||
if result == nil || result.PackageVersion == nil {
|
||||
return
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
"github.com/safedep/dry/cloud/endpointsync"
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/cloudauth"
|
||||
appVersion "github.com/safedep/pmg/internal/version"
|
||||
)
|
||||
|
||||
// SyncClientBundle holds a SyncClient and its underlying cloud client.
|
||||
// Callers must call Close() when done.
|
||||
type SyncClientBundle struct {
|
||||
syncClient *endpointsync.SyncClient
|
||||
cloudClient *cloud.Client
|
||||
keychainResolver cloud.CloseableCredentialResolver
|
||||
syncClient *endpointsync.SyncClient
|
||||
cloudClient *cloud.Client
|
||||
}
|
||||
|
||||
// Sync delivers pending events from the WAL to SafeDep Cloud.
|
||||
@@ -37,61 +37,26 @@ func (b *SyncClientBundle) Close() error {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if b.keychainResolver != nil {
|
||||
if err := b.keychainResolver.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// NewSyncClientBundle creates an authenticated SyncClient connected to SafeDep Cloud.
|
||||
func NewSyncClientBundle(cfg *config.RuntimeConfig) (*SyncClientBundle, error) {
|
||||
// Build credential resolver chain: keychain first, env fallback.
|
||||
var resolvers []cloud.CredentialResolver
|
||||
var keychainResolver cloud.CloseableCredentialResolver
|
||||
|
||||
keychainResolver, err := cloud.NewKeychainCredentialResolver(cloud.CredentialTypeAPIKey)
|
||||
// Resolve credentials via keychain-first, env fallback chain. The keychain
|
||||
// resolver can be closed immediately because the data plane client extracts
|
||||
// the credential values when it is created.
|
||||
creds, closeResolver, err := cloudauth.ResolveCredentials()
|
||||
if err != nil {
|
||||
log.Debugf("Keychain credential resolver not available, skipping: %v", err)
|
||||
} else {
|
||||
resolvers = append(resolvers, keychainResolver)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
envResolver, err := cloud.NewEnvCredentialResolver()
|
||||
if err != nil {
|
||||
log.Debugf("Env credential resolver not available, skipping: %v", err)
|
||||
} else {
|
||||
resolvers = append(resolvers, envResolver)
|
||||
}
|
||||
|
||||
if len(resolvers) == 0 {
|
||||
if keychainResolver != nil {
|
||||
if closeErr := keychainResolver.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close keychain resolver: %v", closeErr)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := closeResolver(); closeErr != nil {
|
||||
log.Warnf("failed to close keychain resolver: %v", closeErr)
|
||||
}
|
||||
return nil, fmt.Errorf("no credential resolvers available")
|
||||
}
|
||||
|
||||
chain := cloud.NewChainCredentialResolver(resolvers...)
|
||||
creds, err := chain.Resolve()
|
||||
if err != nil {
|
||||
if keychainResolver != nil {
|
||||
if closeErr := keychainResolver.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close keychain resolver: %v", closeErr)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to resolve cloud credentials: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
cloudClient, err := cloud.NewDataPlaneClient("pmg", creds)
|
||||
if err != nil {
|
||||
if keychainResolver != nil {
|
||||
if closeErr := keychainResolver.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close keychain resolver: %v", closeErr)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create data plane client: %w", err)
|
||||
}
|
||||
|
||||
@@ -115,17 +80,11 @@ func NewSyncClientBundle(cfg *config.RuntimeConfig) (*SyncClientBundle, error) {
|
||||
if closeErr := cloudClient.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close cloud client after sync client init failure: %v", closeErr)
|
||||
}
|
||||
if keychainResolver != nil {
|
||||
if closeErr := keychainResolver.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close keychain resolver: %v", closeErr)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create sync client: %w", err)
|
||||
}
|
||||
|
||||
return &SyncClientBundle{
|
||||
syncClient: syncClient,
|
||||
cloudClient: cloudClient,
|
||||
keychainResolver: keychainResolver,
|
||||
syncClient: syncClient,
|
||||
cloudClient: cloudClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Package cloudauth provides shared helpers for resolving SafeDep Cloud
|
||||
// credentials from the local keychain or environment. It centralizes the
|
||||
// credential resolver chain used by both audit sync and authenticated
|
||||
// analyzers so the resolution behavior stays consistent.
|
||||
package cloudauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/safedep/dry/cloud"
|
||||
"github.com/safedep/dry/log"
|
||||
)
|
||||
|
||||
// ResolveCredentials resolves SafeDep Cloud API key credentials using a
|
||||
// keychain-first, environment-fallback chain. It returns the resolved
|
||||
// credentials and a close function that releases keychain resources. The
|
||||
// close function is always non-nil and safe to call regardless of the error.
|
||||
//
|
||||
// An error is returned when no credentials are available, which callers can
|
||||
// use to decide whether authenticated cloud features should be enabled.
|
||||
func ResolveCredentials() (*cloud.Credentials, func() error, error) {
|
||||
var resolvers []cloud.CredentialResolver
|
||||
var keychainResolver cloud.CloseableCredentialResolver
|
||||
|
||||
keychainResolver, err := cloud.NewKeychainCredentialResolver(cloud.CredentialTypeAPIKey)
|
||||
if err != nil {
|
||||
log.Debugf("Keychain credential resolver not available, skipping: %v", err)
|
||||
} else {
|
||||
resolvers = append(resolvers, keychainResolver)
|
||||
}
|
||||
|
||||
envResolver, err := cloud.NewEnvCredentialResolver()
|
||||
if err != nil {
|
||||
log.Debugf("Env credential resolver not available, skipping: %v", err)
|
||||
} else {
|
||||
resolvers = append(resolvers, envResolver)
|
||||
}
|
||||
|
||||
closeFn := func() error {
|
||||
if keychainResolver != nil {
|
||||
return keychainResolver.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(resolvers) == 0 {
|
||||
if err := closeFn(); err != nil {
|
||||
log.Warnf("failed to close keychain resolver: %v", err)
|
||||
}
|
||||
return nil, func() error { return nil }, fmt.Errorf("no credential resolvers available")
|
||||
}
|
||||
|
||||
chain := cloud.NewChainCredentialResolver(resolvers...)
|
||||
creds, err := chain.Resolve()
|
||||
if err != nil {
|
||||
if closeErr := closeFn(); closeErr != nil {
|
||||
log.Warnf("failed to close keychain resolver: %v", closeErr)
|
||||
}
|
||||
return nil, func() error { return nil }, fmt.Errorf("failed to resolve cloud credentials: %w", err)
|
||||
}
|
||||
|
||||
return creds, closeFn, nil
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagem
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
malysisQueryAnalyzer, err := analyzer.NewMalysisQueryAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
|
||||
malysisQueryAnalyzer, err := analyzer.NewMalysisAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create malware analyzer: %w", err)
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ func (f *proxyFlow) createCertificateManager(caCert *certmanager.Certificate) (c
|
||||
// createAnalyzer creates the malysis query analyzer
|
||||
func (f *proxyFlow) createAnalyzer() (analyzer.PackageVersionAnalyzer, error) {
|
||||
log.Debugf("Creating malysis query analyzer")
|
||||
return analyzer.NewMalysisQueryAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
|
||||
return analyzer.NewMalysisAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
|
||||
}
|
||||
|
||||
// createAndStartProxyServer creates and starts the proxy server with the given interceptor
|
||||
|
||||
@@ -229,7 +229,14 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
b.statsCollector.RecordAllowed(result)
|
||||
}
|
||||
|
||||
log.Debugf("[%s] Package %s/%s@%s is safe, allowing request", ctx.RequestID, ecosystem.String(), packageName, packageVersion)
|
||||
// A flagged package allowed only because of a tenant-specific exclusion is
|
||||
// a security-relevant trust decision; surface it so it is never silent.
|
||||
if result.IsExcluded {
|
||||
log.Warnf("[%s] Allowing flagged package %s/%s@%s due to tenant exclusion (%s)",
|
||||
ctx.RequestID, ecosystem.String(), packageName, packageVersion, result.ExclusionReason)
|
||||
} else {
|
||||
log.Debugf("[%s] Package %s/%s@%s is safe, allowing request", ctx.RequestID, ecosystem.String(), packageName, packageVersion)
|
||||
}
|
||||
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user