feat: Add support for malysis min confidence config (#429)

* feat: Add support for malysis min confidence config

* fix: Test case to use factory function
This commit is contained in:
Abhisek Datta 2025-03-26 14:07:40 +05:30 committed by GitHub
parent 635baeb86e
commit f6258fdc86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 185 additions and 66 deletions

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"strings"
"sync"
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
@ -19,6 +20,13 @@ type MalwareAnalyzerConfig struct {
// 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 {
@ -36,6 +44,17 @@ type malwareAnalyzer struct {
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
@ -46,7 +65,8 @@ func (a *malwareAnalyzer) Name() string {
}
func (a *malwareAnalyzer) Analyze(manifest *models.PackageManifest,
handler AnalyzerEventHandler) error {
handler AnalyzerEventHandler,
) error {
return readers.NewManifestModelReader(manifest).EnumPackages(func(pkg *models.Package) error {
err := a.applyMalwareDecision(pkg)
if err != nil {
@ -84,7 +104,6 @@ func (a *malwareAnalyzer) Analyze(manifest *models.PackageManifest,
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)
@ -138,16 +157,27 @@ func (a *malwareAnalyzer) applyMalwareDecision(pkg *models.Package) error {
return nil
}
if a.config.TrustAutomatedAnalysis &&
report.GetInference().GetConfidence() == malysisv1.Report_Evidence_CONFIDENCE_HIGH {
// 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 malware with low confidence",
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
}

View File

@ -8,6 +8,89 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewMalwareAnalyzerHasMinimumConfidence(t *testing.T) {
cases := []struct {
name string
config MalwareAnalyzerConfig
expectedConfLevel malysisv1.Report_Evidence_Confidence
reports []struct {
confidence malysisv1.Report_Evidence_Confidence
expected bool
}
wantError bool
}{
{
name: "when minimum confidence is not set",
config: MalwareAnalyzerConfig{},
expectedConfLevel: malysisv1.Report_Evidence_CONFIDENCE_HIGH,
reports: []struct {
confidence malysisv1.Report_Evidence_Confidence
expected bool
}{
{malysisv1.Report_Evidence_CONFIDENCE_MEDIUM, false},
{malysisv1.Report_Evidence_CONFIDENCE_UNSPECIFIED, false},
{malysisv1.Report_Evidence_CONFIDENCE_LOW, false},
{malysisv1.Report_Evidence_CONFIDENCE_HIGH, true},
},
},
{
name: "when minimum confidence is set to HIGH",
config: MalwareAnalyzerConfig{MinimumConfidence: "HIGH"},
expectedConfLevel: malysisv1.Report_Evidence_CONFIDENCE_HIGH,
reports: []struct {
confidence malysisv1.Report_Evidence_Confidence
expected bool
}{
{malysisv1.Report_Evidence_CONFIDENCE_UNSPECIFIED, false},
{malysisv1.Report_Evidence_CONFIDENCE_LOW, false},
{malysisv1.Report_Evidence_CONFIDENCE_MEDIUM, false},
{malysisv1.Report_Evidence_CONFIDENCE_HIGH, true},
},
},
{
name: "when minimum confidence is set to MEDIUM",
config: MalwareAnalyzerConfig{MinimumConfidence: "MEDIUM"},
expectedConfLevel: malysisv1.Report_Evidence_CONFIDENCE_MEDIUM,
reports: []struct {
confidence malysisv1.Report_Evidence_Confidence
expected bool
}{
{malysisv1.Report_Evidence_CONFIDENCE_LOW, false},
{malysisv1.Report_Evidence_CONFIDENCE_UNSPECIFIED, false},
{malysisv1.Report_Evidence_CONFIDENCE_MEDIUM, true},
{malysisv1.Report_Evidence_CONFIDENCE_HIGH, true},
},
},
{
name: "when invalid minimum confidence is set",
config: MalwareAnalyzerConfig{MinimumConfidence: "INVALID"},
wantError: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
analyzer, err := NewMalwareAnalyzer(tc.config)
if tc.wantError {
assert.Error(t, err)
assert.Nil(t, analyzer)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.expectedConfLevel, analyzer.config.minimumConfidenceLevel)
for _, report := range tc.reports {
assert.Equal(t, report.expected, analyzer.hasMinimumConfidence(&malysisv1.Report{
Inference: &malysisv1.Report_Inference{
Confidence: report.confidence,
},
}))
}
})
}
}
func TestMalwareAnalyzerDecision(t *testing.T) {
pkgDetail := models.NewPackageDetail(models.EcosystemNpm, "test", "1.0.0")
pkgManifest := models.NewPackageManifestFromLocal("test", models.EcosystemNpm)
@ -26,7 +109,7 @@ func TestMalwareAnalyzerDecision(t *testing.T) {
PackageDetails: pkgDetail,
},
assert: func(pkg *models.Package) {
assert.Nil(t, pkg.GetMalwareAnalysisResult())
assert.Nil(t, pkg.GetMalwareAnalysisResult(), "should not have malware analysis result")
},
},
{
@ -47,7 +130,7 @@ func TestMalwareAnalyzerDecision(t *testing.T) {
},
},
assert: func(pkg *models.Package) {
assert.True(t, pkg.GetMalwareAnalysisResult().IsMalware)
assert.True(t, pkg.GetMalwareAnalysisResult().IsMalware, "should be malware")
},
},
{
@ -65,8 +148,8 @@ func TestMalwareAnalyzerDecision(t *testing.T) {
},
},
assert: func(pkg *models.Package) {
assert.False(t, pkg.GetMalwareAnalysisResult().IsMalware)
assert.True(t, pkg.GetMalwareAnalysisResult().IsSuspicious)
assert.False(t, pkg.GetMalwareAnalysisResult().IsMalware, "should not be malware")
assert.True(t, pkg.GetMalwareAnalysisResult().IsSuspicious, "should be suspicious")
},
},
{
@ -85,7 +168,7 @@ func TestMalwareAnalyzerDecision(t *testing.T) {
},
},
assert: func(pkg *models.Package) {
assert.True(t, pkg.GetMalwareAnalysisResult().IsMalware)
assert.True(t, pkg.GetMalwareAnalysisResult().IsMalware, "should be malware")
},
},
{
@ -104,16 +187,18 @@ func TestMalwareAnalyzerDecision(t *testing.T) {
},
},
assert: func(pkg *models.Package) {
assert.False(t, pkg.GetMalwareAnalysisResult().IsMalware)
assert.True(t, pkg.GetMalwareAnalysisResult().IsSuspicious)
assert.False(t, pkg.GetMalwareAnalysisResult().IsMalware, "should not be malware")
assert.True(t, pkg.GetMalwareAnalysisResult().IsSuspicious, "should be suspicious")
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
a := &malwareAnalyzer{config: tc.config}
err := a.applyMalwareDecision(tc.pkg)
a, err := NewMalwareAnalyzer(tc.config)
assert.NoError(t, err)
err = a.applyMalwareDecision(tc.pkg)
assert.NoError(t, err)
tc.assert(tc.pkg)
})

View File

@ -76,6 +76,7 @@ var (
scannerExperimental bool
malwareAnalyzerTrustToolResult bool
malwareAnalysisTimeout time.Duration
malwareAnalysisMinimumConfidence string
)
func newScanCommand() *cobra.Command {
@ -189,6 +190,8 @@ func newScanCommand() *cobra.Command {
"Trust malicious package analysis tool result without verification record")
cmd.Flags().DurationVarP(&malwareAnalysisTimeout, "malware-analysis-timeout", "", 5*time.Minute,
"Timeout for malicious package analysis")
cmd.Flags().StringVarP(&malwareAnalysisMinimumConfidence, "malware-analysis-min-confidence", "", "HIGH",
"Minimum confidence level for malicious package analysis result to fail fast")
// Add validations that should trigger a fail fast condition
cmd.PreRun = func(cmd *cobra.Command, args []string) {
@ -381,6 +384,7 @@ func internalStartScan() error {
if enrichMalware {
config := analyzer.DefaultMalwareAnalyzerConfig()
config.TrustAutomatedAnalysis = malwareAnalyzerTrustToolResult
config.MinimumConfidence = malwareAnalysisMinimumConfidence
config.FailFast = failFast
task, err := analyzer.NewMalwareAnalyzer(config)