Files
pmg/analyzer/malysis_query.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

162 lines
5.5 KiB
Go

package analyzer
import (
"context"
"fmt"
"net/http"
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/malysis/v1/malysisv1grpc"
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"
drygrpc "github.com/safedep/dry/adapters/grpc"
"github.com/safedep/dry/cloud"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/config"
"google.golang.org/grpc"
)
const (
communityMalysisHost = "community-api.safedep.io"
communityMalysisPort = "443"
)
type MalysisQueryAnalyzerConfig struct{}
type malysisQueryAnalyzer struct {
client malysisv1grpc.MalwareAnalysisServiceClient
Config MalysisQueryAnalyzerConfig
// honorExclusions enables honoring tenant-specific malicious package
// exclusions returned by authenticated queries. Exclusions are never
// returned for unauthenticated (community) queries.
honorExclusions bool
}
var _ Analyzer = &malysisQueryAnalyzer{}
var _ PackageVersionAnalyzer = &malysisQueryAnalyzer{}
// NewMalysisQueryAnalyzer creates an unauthenticated analyzer that queries the
// SafeDep community malware analysis service.
func NewMalysisQueryAnalyzer(config MalysisQueryAnalyzerConfig) (*malysisQueryAnalyzer, error) {
client, err := drygrpc.GrpcClient("pmg-malysis-query",
communityMalysisHost, communityMalysisPort, "", http.Header{}, []grpc.DialOption{})
if err != nil {
return nil, fmt.Errorf("failed to create gRPC client: %w", err)
}
return &malysisQueryAnalyzer{
client: malysisv1grpc.NewMalwareAnalysisServiceClient(client),
Config: config,
}, nil
}
// NewMalysisAuthenticatedQueryAnalyzer creates an analyzer that queries the
// authenticated SafeDep Cloud malware analysis service (api.safedep.io) using
// the provided API key credentials. The analysis behavior is identical to the
// community analyzer except that it additionally honors tenant-specific
// malicious package exclusions returned in the response.
func NewMalysisAuthenticatedQueryAnalyzer(config MalysisQueryAnalyzerConfig,
creds *cloud.Credentials) (*malysisQueryAnalyzer, error) {
cloudClient, err := cloud.NewDataPlaneClient("pmg-malysis-query", creds)
if err != nil {
return nil, fmt.Errorf("failed to create authenticated gRPC client: %w", err)
}
return &malysisQueryAnalyzer{
client: malysisv1grpc.NewMalwareAnalysisServiceClient(cloudClient.Connection()),
Config: config,
honorExclusions: true,
}, nil
}
func (a *malysisQueryAnalyzer) Name() string {
return "malysis-query"
}
func (a *malysisQueryAnalyzer) Analyze(ctx context.Context,
packageVersion *packagev1.PackageVersion) (*PackageVersionAnalysisResult, error) {
res, err := a.client.QueryPackageAnalysis(ctx, &malysisv1.QueryPackageAnalysisRequest{
Target: &malysisv1pb.PackageAnalysisTarget{
PackageVersion: packageVersion,
},
})
if err != nil {
return nil, fmt.Errorf("failed to query package analysis: %w", err)
}
// By default, the analyzer allows the package version
analysisResult := &PackageVersionAnalysisResult{
PackageVersion: packageVersion,
ReferenceURL: malysisReportUrl(res.GetAnalysisId()),
Action: ActionAllow,
AnalysisID: res.GetAnalysisId(),
Summary: res.GetReport().GetInference().GetSummary(),
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.IsMalware = true
analysisResult.Action = ActionConfirm
// Treat suspicious package as malicious when `--paranoid` flag is set to true
if cfg.Config.Paranoid {
analysisResult.Action = ActionBlock
}
}
// A confirmed malicious package is blocked here, unless a tenant exclusion
// downgrades it to allow in applyExclusion below.
if res.GetVerificationRecord().GetIsMalware() {
analysisResult.IsMalware = true
analysisResult.IsVerified = true
analysisResult.Action = ActionBlock
}
// Honor tenant-specific exclusion as an opt-in trust signal. This is only
// applied for authenticated queries and only when the package was actually
// flagged. The exclusion in the response is scoped by the server to the
// exact package version we queried, so it is an exact match by construction.
a.applyExclusion(analysisResult, res)
return analysisResult, nil
}
// applyExclusion downgrades a flagged package to ActionAllow when the
// authenticated response carries a tenant-specific malicious package exclusion.
// The exclusion is honored only when the package was flagged as malware, so it
// never weakens the verdict for packages that were already allowed.
func (a *malysisQueryAnalyzer) applyExclusion(result *PackageVersionAnalysisResult,
res *malysisv1.QueryPackageAnalysisResponse) {
if !a.honorExclusions {
return
}
exclusion := res.GetMaliciousPackageExclusion()
if exclusion == nil || exclusion.GetExclusionId() == "" {
return
}
if !result.IsMalware {
return
}
log.Debugf("Honoring tenant exclusion %q for package %s@%s: %s",
exclusion.GetExclusionId(),
result.PackageVersion.GetPackage().GetName(),
result.PackageVersion.GetVersion(),
exclusion.GetReason())
result.IsExcluded = true
result.ExclusionID = exclusion.GetExclusionId()
result.ExclusionReason = exclusion.GetReason()
result.Action = ActionAllow
}
func malysisReportUrl(analysisId string) string {
return fmt.Sprintf("https://app.safedep.io/community/malysis/%s", analysisId)
}