vet/pkg/analyzer/malware.go
Abhisek Datta f6258fdc86
feat: Add support for malysis min confidence config (#429)
* feat: Add support for malysis min confidence config

* fix: Test case to use factory function
2025-03-26 14:07:40 +05:30

184 lines
5.8 KiB
Go

package analyzer
import (
"fmt"
"strings"
"sync"
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
"github.com/safedep/vet/gen/checks"
"github.com/safedep/vet/gen/filtersuite"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/models"
"github.com/safedep/vet/pkg/readers"
)
type MalwareAnalyzerConfig struct {
// Flag to trust automated analysis results without needing
// a verification record
TrustAutomatedAnalysis bool
// Fail fast on malware detection
FailFast bool
// Minimum confidence level for malicious package analysis result to fail fast
// Should be HIGH, MEDIUM or LOW
MinimumConfidence string
// Internally mapped confidence level as per protobuf spec
minimumConfidenceLevel malysisv1.Report_Evidence_Confidence
}
func DefaultMalwareAnalyzerConfig() MalwareAnalyzerConfig {
return MalwareAnalyzerConfig{
TrustAutomatedAnalysis: false,
FailFast: true,
}
}
type malwareAnalyzer struct {
config MalwareAnalyzerConfig
m sync.Mutex
}
var _ Analyzer = (*malwareAnalyzer)(nil)
func NewMalwareAnalyzer(config MalwareAnalyzerConfig) (*malwareAnalyzer, error) {
config.minimumConfidenceLevel = malysisv1.Report_Evidence_CONFIDENCE_HIGH
if config.MinimumConfidence != "" {
confidenceName := fmt.Sprintf("CONFIDENCE_%s", strings.ToUpper(config.MinimumConfidence))
conf, ok := malysisv1.Report_Evidence_Confidence_value[confidenceName]
if !ok {
return nil, fmt.Errorf("invalid minimum confidence level: %s", config.MinimumConfidence)
}
config.minimumConfidenceLevel = malysisv1.Report_Evidence_Confidence(conf)
}
return &malwareAnalyzer{
config: config,
}, nil
}
func (a *malwareAnalyzer) Name() string {
return "Malware Analyzer"
}
func (a *malwareAnalyzer) Analyze(manifest *models.PackageManifest,
handler AnalyzerEventHandler,
) error {
return readers.NewManifestModelReader(manifest).EnumPackages(func(pkg *models.Package) error {
err := a.applyMalwareDecision(pkg)
if err != nil {
return fmt.Errorf("MalwareAnalyzer: Failed to apply malware decision for package %s/%s/%s: %w",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion(), err)
}
if !pkg.IsMalware() && !pkg.IsSuspicious() {
return nil
}
var filterMsg string
if pkg.IsMalware() {
filterMsg = fmt.Sprintf("MalwareAnalyzer: Package %s/%s/%s is classified as malicious",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
} else {
filterMsg = fmt.Sprintf("MalwareAnalyzer: Package %s/%s/%s is classified as suspicious",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
}
// Trigger a policy violation event so that it gets recorded
// across reports that are tracking policy violations
err = handler(&AnalyzerEvent{
Type: ET_FilterExpressionMatched,
Source: a.Name(),
Manifest: manifest,
Package: pkg,
Message: filterMsg,
Filter: &filtersuite.Filter{
Name: "malware",
CheckType: checks.CheckType_CheckTypeMalware,
Summary: filterMsg,
Description: fmt.Sprintf("%s analyzed the package for possible malicious behavior", a.Name()),
References: []string{"https://docs.safedep.io/cloud/malware-analysis"},
Tags: []string{"malware-analysis"},
},
})
if err != nil {
logger.Errorf("MalwareAnalyzer: Failed to handle filter event for package %s/%s/%s: %v",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion(), err)
}
if a.config.FailFast && pkg.IsMalware() {
return handler(&AnalyzerEvent{
Type: ET_AnalyzerFailOnError,
Source: a.Name(),
Err: fmt.Errorf("Malware detected in package %s/%s/%s",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion()),
})
}
return nil
})
}
func (a *malwareAnalyzer) Finish() error {
return nil
}
// The malware analyzer makes a decision based on configuration and malware analysis results.
// The decision involves:
//
// - No action if the package is not classified as malware
// - Malware if a verfication record is available to confirm
// - Malware if `TrustAutomatedAnalysis` config is enabled and confidence is high
// - Suspicious for all other cases
func (a *malwareAnalyzer) applyMalwareDecision(pkg *models.Package) error {
ma := pkg.GetMalwareAnalysisResult()
if ma == nil || ma.Report == nil {
logger.Warnf("MalwareAnalyzer: No malware analysis result found for package %s/%s/%s",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
return nil
}
a.m.Lock()
defer a.m.Unlock()
report := ma.Report
if !report.GetInference().GetIsMalware() {
return nil
}
vr := ma.VerificationRecord
if vr != nil && vr.GetIsMalware() {
logger.Warnf("MalwareAnalyzer: Package %s/%s/%s is classified as malwars with verification record",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
ma.IsMalware = true
return nil
}
// By default we do not trust results without a verification record
// unless the config is set to trust automated analysis and a minimum confidence is set
if a.config.TrustAutomatedAnalysis && a.hasMinimumConfidence(report) {
logger.Warnf("MalwareAnalyzer: Package %s/%s/%s is classified as malware",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
ma.IsMalware = true
return nil
}
logger.Warnf("MalwareAnalyzer: Package %s/%s/%s is classified as suspicious due to low confidence",
pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
ma.IsSuspicious = true
return nil
}
func (a *malwareAnalyzer) hasMinimumConfidence(report *malysisv1.Report) bool {
confidence := report.GetInference().GetConfidence()
if confidence == malysisv1.Report_Evidence_CONFIDENCE_UNSPECIFIED {
return false
}
// Confidence is a protobuf enum, so we need to compare the integer values
return confidence <= a.config.minimumConfidenceLevel
}