mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add post-exec reporting support (#134)
* feat: Add post install reporting support * fix: UI report handling * fix: Duplicate reporting * fix: Show warning on insecure bypass * fix: Proxy event log insecure skip installation * fix: Proxy event log insecure skip installation * fix: Common definition for infer outcome
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
type baseRegistryInterceptor struct {
|
||||
analyzer analyzer.PackageVersionAnalyzer
|
||||
cache AnalysisCache
|
||||
statsCollector *AnalysisStatsCollector
|
||||
confirmationChan chan *ConfirmationRequest
|
||||
}
|
||||
|
||||
@@ -56,6 +57,17 @@ func (b *baseRegistryInterceptor) analyzePackage(
|
||||
Version: packageVersion,
|
||||
}
|
||||
|
||||
if cfg := config.Get(); cfg.InsecureInstallation {
|
||||
log.Debugf("[%s] Skipping insecure installation", ctx.RequestID)
|
||||
|
||||
eventlog.LogInstallInsecureBypass(packageName, packageVersion, ecosystem.String())
|
||||
|
||||
return &analyzer.PackageVersionAnalysisResult{
|
||||
PackageVersion: pkgVersion,
|
||||
Action: analyzer.ActionAllow,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if config.IsTrustedPackage(pkgVersion) {
|
||||
log.Debugf("[%s] Skipping trusted package: %s/%s@%s",
|
||||
ctx.RequestID, ecosystem.String(), packageName, packageVersion)
|
||||
@@ -108,6 +120,10 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
"reference_url": result.ReferenceURL,
|
||||
})
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordBlocked(result)
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Malicious package blocked: %s/%s@%s\n\nReason: %s\n\nReference: %s",
|
||||
ecosystem.String(),
|
||||
packageName, packageVersion,
|
||||
@@ -126,6 +142,11 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
confirmed, err := b.requestUserConfirmation(ctx, result)
|
||||
if err != nil {
|
||||
log.Errorf("[%s] Failed to get user confirmation: %v", ctx.RequestID, err)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordBlocked(result)
|
||||
}
|
||||
|
||||
return &proxy.InterceptorResponse{
|
||||
Action: proxy.ActionBlock,
|
||||
BlockCode: http.StatusForbidden,
|
||||
@@ -141,6 +162,10 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
"reference_url": result.ReferenceURL,
|
||||
})
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordUserCancelled(result)
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Installation blocked by user: %s/%s@%s\n\nReason: %s\n\nReference: %s",
|
||||
ecosystem.String(),
|
||||
packageName, packageVersion,
|
||||
@@ -157,18 +182,30 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
eventlog.LogMalwareConfirmed(packageName, packageVersion, ecosystem.String())
|
||||
eventlog.LogInstallAllowed(packageName, packageVersion, ecosystem.String(), 1)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordConfirmed(result)
|
||||
}
|
||||
|
||||
log.Infof("[%s] User confirmed installation of suspicious package %s/%s@%s", ctx.RequestID, ecosystem.String(), packageName, packageVersion)
|
||||
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
||||
|
||||
case analyzer.ActionAllow:
|
||||
eventlog.LogInstallAllowed(packageName, packageVersion, ecosystem.String(), 1)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordAllowed(result)
|
||||
}
|
||||
|
||||
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:
|
||||
eventlog.LogInstallAllowed(packageName, packageVersion, ecosystem.String(), 1)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordAllowed(result)
|
||||
}
|
||||
|
||||
log.Warnf("[%s] Unknown analysis action %d for package %s/%s@%s, allowing by default", ctx.RequestID, result.Action, ecosystem.String(), packageName, packageVersion)
|
||||
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
type InterceptorFactory struct {
|
||||
analyzer analyzer.PackageVersionAnalyzer
|
||||
cache AnalysisCache
|
||||
statsCollector *AnalysisStatsCollector
|
||||
confirmationChan chan *ConfirmationRequest
|
||||
}
|
||||
|
||||
@@ -19,11 +20,13 @@ type InterceptorFactory struct {
|
||||
func NewInterceptorFactory(
|
||||
analyzer analyzer.PackageVersionAnalyzer,
|
||||
cache AnalysisCache,
|
||||
statsCollector *AnalysisStatsCollector,
|
||||
confirmationChan chan *ConfirmationRequest,
|
||||
) *InterceptorFactory {
|
||||
return &InterceptorFactory{
|
||||
analyzer: analyzer,
|
||||
cache: cache,
|
||||
statsCollector: statsCollector,
|
||||
confirmationChan: confirmationChan,
|
||||
}
|
||||
}
|
||||
@@ -36,6 +39,7 @@ func (f *InterceptorFactory) CreateInterceptor(ecosystem packagev1.Ecosystem) (p
|
||||
return NewNpmRegistryInterceptor(
|
||||
f.analyzer,
|
||||
f.cache,
|
||||
f.statsCollector,
|
||||
f.confirmationChan,
|
||||
), nil
|
||||
|
||||
|
||||
@@ -44,12 +44,14 @@ var _ proxy.Interceptor = (*NpmRegistryInterceptor)(nil)
|
||||
func NewNpmRegistryInterceptor(
|
||||
analyzer analyzer.PackageVersionAnalyzer,
|
||||
cache AnalysisCache,
|
||||
statsCollector *AnalysisStatsCollector,
|
||||
confirmationChan chan *ConfirmationRequest,
|
||||
) *NpmRegistryInterceptor {
|
||||
return &NpmRegistryInterceptor{
|
||||
baseRegistryInterceptor: baseRegistryInterceptor{
|
||||
analyzer: analyzer,
|
||||
cache: cache,
|
||||
statsCollector: statsCollector,
|
||||
confirmationChan: confirmationChan,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package interceptors
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
)
|
||||
|
||||
// AnalysisStats contains aggregated statistics from analysis results
|
||||
type AnalysisStats struct {
|
||||
TotalAnalyzed int
|
||||
AllowedCount int
|
||||
ConfirmedCount int
|
||||
BlockedCount int
|
||||
UserCancelledCount int
|
||||
}
|
||||
|
||||
// AnalysisStatsCollector tracks analysis statistics during proxy execution.
|
||||
// It is separate from the cache to allow different cache implementations
|
||||
// without coupling them to reporting concerns.
|
||||
type AnalysisStatsCollector struct {
|
||||
mu sync.RWMutex
|
||||
stats AnalysisStats
|
||||
blockedPackages []*analyzer.PackageVersionAnalysisResult
|
||||
confirmedPackages []*analyzer.PackageVersionAnalysisResult
|
||||
}
|
||||
|
||||
// NewAnalysisStatsCollector creates a new stats collector
|
||||
func NewAnalysisStatsCollector() *AnalysisStatsCollector {
|
||||
return &AnalysisStatsCollector{}
|
||||
}
|
||||
|
||||
// RecordAllowed records a package that was allowed (safe)
|
||||
func (c *AnalysisStatsCollector) RecordAllowed(result *analyzer.PackageVersionAnalysisResult) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.stats.TotalAnalyzed++
|
||||
c.stats.AllowedCount++
|
||||
}
|
||||
|
||||
// RecordBlocked records a package that was automatically blocked (ActionBlock)
|
||||
func (c *AnalysisStatsCollector) RecordBlocked(result *analyzer.PackageVersionAnalysisResult) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.stats.TotalAnalyzed++
|
||||
c.stats.BlockedCount++
|
||||
c.blockedPackages = append(c.blockedPackages, result)
|
||||
}
|
||||
|
||||
// RecordUserCancelled records a package that was blocked because user declined confirmation (ActionConfirm declined)
|
||||
func (c *AnalysisStatsCollector) RecordUserCancelled(result *analyzer.PackageVersionAnalysisResult) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.stats.TotalAnalyzed++
|
||||
c.stats.UserCancelledCount++
|
||||
|
||||
// User cancelled packages are counted as blocked as well
|
||||
c.stats.BlockedCount++
|
||||
|
||||
c.blockedPackages = append(c.blockedPackages, result)
|
||||
}
|
||||
|
||||
// RecordConfirmed records a package where user confirmed installation despite warning
|
||||
func (c *AnalysisStatsCollector) RecordConfirmed(result *analyzer.PackageVersionAnalysisResult) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.stats.TotalAnalyzed++
|
||||
c.stats.ConfirmedCount++
|
||||
c.confirmedPackages = append(c.confirmedPackages, result)
|
||||
}
|
||||
|
||||
// GetStats returns the current statistics
|
||||
func (c *AnalysisStatsCollector) GetStats() AnalysisStats {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return c.stats
|
||||
}
|
||||
|
||||
// GetBlockedPackages returns all blocked packages
|
||||
func (c *AnalysisStatsCollector) GetBlockedPackages() []*analyzer.PackageVersionAnalysisResult {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
result := make([]*analyzer.PackageVersionAnalysisResult, len(c.blockedPackages))
|
||||
copy(result, c.blockedPackages)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetConfirmedPackages returns all confirmed packages
|
||||
func (c *AnalysisStatsCollector) GetConfirmedPackages() []*analyzer.PackageVersionAnalysisResult {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
result := make([]*analyzer.PackageVersionAnalysisResult, len(c.confirmedPackages))
|
||||
copy(result, c.confirmedPackages)
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user