Files
pmg/internal/flows/common_flow.go
T
f6e1d9e733 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>
2026-06-01 15:32:22 +05:30

138 lines
4.5 KiB
Go

package flows
import (
"context"
"fmt"
"os"
"time"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/guard"
"github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/internal/runner"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager"
)
type commonFlow struct {
pm packagemanager.PackageManager
packageResolver packagemanager.PackageResolver
}
// Creates a common flow of execution for all package managers. This should work for most
// of the cases unless a package manager has its own unique requirements. Configuration
// should be passed through the context (Global Config)
func Common(pm packagemanager.PackageManager, pkgResolver packagemanager.PackageResolver) *commonFlow {
return &commonFlow{
pm: pm,
packageResolver: pkgResolver,
}
}
func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) error {
var analyzers []analyzer.PackageVersionAnalyzer
// Configure sandbox based on command type and enforcement policy
config.ConfigureSandbox(parsedCmd.IsInstallationCommand() || parsedCmd.MayDownloadPackages())
cfg := config.Get()
// Initialize report data at the start
reportData := ui.NewReportData()
reportData.PackageManagerName = f.pm.Name()
reportData.FlowType = ui.FlowTypeGuard
reportData.DryRun = cfg.DryRun
reportData.InsecureMode = cfg.InsecureInstallation
reportData.TransitiveEnabled = cfg.Config.Transitive
reportData.ParanoidMode = cfg.Config.Paranoid
reportData.SandboxEnabled = cfg.Config.Sandbox.Enabled
if cfg.Config.Sandbox.Enabled {
if policyRef, exists := cfg.Config.Sandbox.Policies[f.pm.Name()]; exists {
reportData.SandboxProfile = policyRef.Profile
}
}
if cfg.SandboxProfileOverride != "" {
reportData.SandboxProfile = cfg.SandboxProfileOverride
}
startTime := time.Now()
malysisQueryAnalyzer, err := analyzer.NewMalysisAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
if err != nil {
return fmt.Errorf("failed to create malware analyzer: %w", err)
}
analyzers = append(analyzers, malysisQueryAnalyzer)
interaction := guard.PackageManagerGuardInteraction{
SetStatus: ui.SetStatus,
ClearStatus: ui.ClearStatus,
ShowWarning: ui.ShowWarning,
GetConfirmationOnMalware: ui.GetConfirmationOnMalware,
Block: ui.BlockNoExit,
}
guardConfig := guard.DefaultPackageManagerGuardConfig()
guardConfig.DryRun = cfg.DryRun
guardConfig.InsecureInstallation = cfg.InsecureInstallation
pmName := f.pm.Name()
executor := func(ctx context.Context, pc *packagemanager.ParsedCommand) error {
return runner.Execute(ctx, pc, pmName, cfg.DryRun)
}
guardManager, err := guard.NewPackageManagerGuard(guardConfig, f.pm, f.packageResolver, analyzers, interaction, executor)
if err != nil {
return fmt.Errorf("failed to create package manager guard: %s", err)
}
guardResult, err := guardManager.Run(ctx, args, parsedCmd)
// Populate report data from guard result
reportData.StartTime = startTime
if guardResult != nil {
reportData.TotalAnalyzed = guardResult.TotalAnalyzed
reportData.TrustedSkipped = guardResult.TrustedSkipped
reportData.AllowedCount = guardResult.AllowedCount
reportData.ConfirmedCount = guardResult.ConfirmedCount
reportData.BlockedCount = guardResult.BlockedCount
reportData.BlockedPackages = guardResult.BlockedPackages
reportData.ConfirmedPackages = guardResult.ConfirmedPackages
}
// Infer outcome from data and config using shared inference logic
blockedCount := 0
userCancelledCount := 0
if guardResult != nil {
blockedCount = guardResult.BlockedCount
// In guard flow, if user cancelled, all blocked packages are due to user cancellation
// (guard returns immediately on ActionBlock, so we can't have both types)
if guardResult.WasUserCancelled {
userCancelledCount = guardResult.BlockedCount
}
}
reportData.Outcome = inferOutcome(cfg.InsecureInstallation, cfg.DryRun, blockedCount, userCancelledCount, err)
// Session complete is called here (not deferred) because guard.Run() calls
// LogInstallStarted internally, and all paths after guard.Run() reach this point.
audit.LogSessionComplete(audit.Outcome(reportData.Outcome.String()), audit.FlowTypeGuard)
// Show the report
ui.Report(reportData)
// Exit after report for blocked/cancelled outcomes
if reportData.Outcome == ui.OutcomeBlocked || reportData.Outcome == ui.OutcomeUserCancelled {
os.Exit(1)
}
if err != nil {
return fmt.Errorf("failed to run package manager guard: %w", err)
}
return nil
}