From a6cb60a757d16cb9501bb1127b8ecb8137d2037d Mon Sep 17 00:00:00 2001 From: Sahil Bansal Date: Tue, 10 Feb 2026 22:22:40 +0530 Subject: [PATCH] Treat suspicious as malicious in paranoid mode (#156) * paranoid mode blocks suspicious packages * enable sandbox for paranoid flag * rm sandbox enabling for paranoid mode * update docs --- README.md | 11 +++ analyzer/malysis_query.go | 7 ++ analyzer/malysis_query_test.go | 169 +++++++++++++++++++++++++++++++++ config/config.template.yml | 4 +- 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 analyzer/malysis_query_test.go diff --git a/README.md b/README.md index 95cb959..52e7758 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,17 @@ This will: ## Usage +
+Paranoid Mode + +Use the `--paranoid` flag to treat suspicous (unverified) packages as malicious packages. + +```bash +pmg --paranoid npm install +``` + +
+
Silent Mode diff --git a/analyzer/malysis_query.go b/analyzer/malysis_query.go index 457e268..6df94fc 100644 --- a/analyzer/malysis_query.go +++ b/analyzer/malysis_query.go @@ -10,6 +10,7 @@ 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/pmg/config" "google.golang.org/grpc" ) @@ -62,9 +63,15 @@ func (a *malysisQueryAnalyzer) Analyze(ctx context.Context, Data: res.GetReport(), } + cfg := config.Get() // Mark the package version to be confirmed if it is malicious (not confirmed) if res.GetReport().GetInference().GetIsMalware() { analysisResult.Action = ActionConfirm + + // Treat suspicious package as malicious when `--paranoid` flag is set to true + if cfg.Config.Paranoid { + analysisResult.Action = ActionBlock + } } // This is a confirmed malicious package, we must always block it diff --git a/analyzer/malysis_query_test.go b/analyzer/malysis_query_test.go new file mode 100644 index 0000000..1749e63 --- /dev/null +++ b/analyzer/malysis_query_test.go @@ -0,0 +1,169 @@ +package analyzer + +import ( + "context" + "testing" + + malysisv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1" + 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" + "github.com/safedep/pmg/config" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" +) + +// stubMalwareAnalysisServiceClient is a minimal stub implementing the Malysis gRPC client interface, +// returning a preconfigured response for testing. +type stubMalwareAnalysisServiceClient struct { + resp *malysisv1.QueryPackageAnalysisResponse + err error +} + +func (s *stubMalwareAnalysisServiceClient) QueryPackageAnalysis(ctx context.Context, req *malysisv1.QueryPackageAnalysisRequest, opts ...grpc.CallOption) (*malysisv1.QueryPackageAnalysisResponse, error) { + return s.resp, s.err +} + +// helper to make a basic PackageVersion for tests +func makePkgVersion(name, version string) *packagev1.PackageVersion { + return &packagev1.PackageVersion{ + Package: &packagev1.Package{ + Name: name, + }, + Version: version, + } +} + +func TestMalysisQueryAnalyzer_DefaultAllowWhenNotMalicious(t *testing.T) { + resp := &malysisv1.QueryPackageAnalysisResponse{ + AnalysisId: "analysis-1", + Report: &malysisv1pb.Report{ + Inference: &malysisv1pb.Report_Inference{ + IsMalware: false, + Summary: "No indicators of compromise", + }, + }, + VerificationRecord: &malysisv1pb.VerificationRecord{ + IsMalware: false, + }, + } + an := &malysisQueryAnalyzer{ + client: &stubMalwareAnalysisServiceClient{resp: resp}, + } + + pv := makePkgVersion("safe-pkg", "1.0.0") + result, err := an.Analyze(context.Background(), pv) + assert.NoError(t, err) + assert.Equal(t, ActionAllow, result.Action) + assert.Equal(t, "analysis-1", result.AnalysisID) + assert.Equal(t, pv, result.PackageVersion) + assert.NotEmpty(t, result.ReferenceURL) + assert.Equal(t, "No indicators of compromise", result.Summary) +} + +func TestMalysisQueryAnalyzer_ConfirmOnSuspiciousWhenNotParanoid(t *testing.T) { + // Ensure paranoid is disabled + cfg := config.Get() + origParanoid := cfg.Config.Paranoid + cfg.Config.Paranoid = false + defer func() { cfg.Config.Paranoid = origParanoid }() + + // Setup: inference says suspicious/malicious (unverified) + resp := &malysisv1.QueryPackageAnalysisResponse{ + AnalysisId: "analysis-2", + Report: &malysisv1pb.Report{ + Inference: &malysisv1pb.Report_Inference{ + IsMalware: true, + Summary: "Suspicious patterns detected", + }, + }, + VerificationRecord: &malysisv1pb.VerificationRecord{ + IsMalware: false, + }, + } + an := &malysisQueryAnalyzer{ + client: &stubMalwareAnalysisServiceClient{resp: resp}, + } + + pv := makePkgVersion("suspicious-pkg", "2.0.0") + result, err := an.Analyze(context.Background(), pv) + assert.NoError(t, err) + assert.Equal(t, ActionConfirm, result.Action) + assert.Equal(t, "analysis-2", result.AnalysisID) +} + +func TestMalysisQueryAnalyzer_BlockOnSuspiciousWhenParanoid(t *testing.T) { + // Enable paranoid mode + cfg := config.Get() + origParanoid := cfg.Config.Paranoid + cfg.Config.Paranoid = true + defer func() { cfg.Config.Paranoid = origParanoid }() + + // Setup: inference says suspicious/malicious (unverified) + resp := &malysisv1.QueryPackageAnalysisResponse{ + AnalysisId: "analysis-3", + Report: &malysisv1pb.Report{ + Inference: &malysisv1pb.Report_Inference{ + IsMalware: true, + Summary: "Suspicious patterns detected", + }, + }, + VerificationRecord: &malysisv1pb.VerificationRecord{ + IsMalware: false, + }, + } + an := &malysisQueryAnalyzer{ + client: &stubMalwareAnalysisServiceClient{resp: resp}, + } + + pv := makePkgVersion("suspicious-pkg", "3.0.0") + result, err := an.Analyze(context.Background(), pv) + assert.NoError(t, err) + assert.Equal(t, ActionBlock, result.Action, "Paranoid mode should block suspicious packages") +} + +func TestMalysisQueryAnalyzer_AlwaysBlockOnVerifiedMalware(t *testing.T) { + // Paranoid on/off should not matter + cfg := config.Get() + origParanoid := cfg.Config.Paranoid + cfg.Config.Paranoid = false + defer func() { cfg.Config.Paranoid = origParanoid }() + + resp := &malysisv1.QueryPackageAnalysisResponse{ + AnalysisId: "analysis-4", + Report: &malysisv1pb.Report{ + Inference: &malysisv1pb.Report_Inference{ + IsMalware: false, + Summary: "Inference not malicious, but verification is", + }, + }, + VerificationRecord: &malysisv1pb.VerificationRecord{ + IsMalware: true, + }, + } + an := &malysisQueryAnalyzer{ + client: &stubMalwareAnalysisServiceClient{resp: resp}, + } + + pv := makePkgVersion("verified-malware", "9.9.9") + result, err := an.Analyze(context.Background(), pv) + assert.NoError(t, err) + assert.Equal(t, ActionBlock, result.Action, "Verified malware must be blocked always") +} + +// 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 + return nil, nil +} +func (s *stubMalwareAnalysisServiceClient) GetAnalysisReport(ctx context.Context, req *malysisv1.GetAnalysisReportRequest, opts ...grpc.CallOption) (*malysisv1.GetAnalysisReportResponse, error) { + // Not used in these tests + return nil, nil +} +func (s *stubMalwareAnalysisServiceClient) InternalAnalyzePackage(ctx context.Context, req *malysisv1.InternalAnalyzePackageRequest, opts ...grpc.CallOption) (*malysisv1.InternalAnalyzePackageResponse, error) { + // Not used in these tests + return nil, nil +} +func (s *stubMalwareAnalysisServiceClient) ListPackageAnalysisRecords(ctx context.Context, req *malysisv1.ListPackageAnalysisRecordsRequest, opts ...grpc.CallOption) (*malysisv1.ListPackageAnalysisRecordsResponse, error) { + // Not used in these tests + return nil, nil +} diff --git a/config/config.template.yml b/config/config.template.yml index 289f794..a43dc0e 100644 --- a/config/config.template.yml +++ b/config/config.template.yml @@ -10,8 +10,8 @@ transitive_depth: 5 # Include dev dependencies in the dependency graph. Default is false. include_dev_dependencies: false -# Enable paranoid mode. In paranoid mode, PMG will use SafeDep Cloud credentials -# to scan unknown packages for malware. This is slow and should be used with caution. +# Enable paranoid mode. In paranoid mode, PMG will treat suspicious packages +# as malicious packages paranoid: false # Skip event logging. Default is false.