feat: Refactor PMG to Maintain Separation of Concerns and Clean Architecture (#19)

* feat: Add separate package manager and resolver

* fix: Npm dependency resolver

* feat: Add analyzer for malysis query

* feat: Add package manager guard as the orchestrator

* feat: Add PMG to orchestrate installation

* Add concurrent scan execution

* Introduce package manager interaction abstraction

* feat: Add UI port for guard

* Remove refactored source files

* Update README

* fix: CI script for multi-arch build

* ci: goreleaser CI fix

* fix: npm command parser to extract package names

* feat: Introduce global config primitive

* fix: Close results channel for clean goroutine exit

* ci: Add container image releaser

* test: Improve test for npm resolver

* refactor: Analyzer to generalise

* Improve UI with additional info

* fix: Goreleaser config

* fix: npm resolver bug

* fix: Fail when command exec workflow fails

* fix: Bug with transitive dependency resolution

* fix: Synchronize common data update in dependency resolver

* chore: Improve log handling

* docs: Update README

* fix: UI text wrapping

* fix: UI handling bugs

* feat: Use concurrent dependency resolver
This commit is contained in:
Abhisek Datta
2025-05-15 16:50:59 +05:30
committed by GitHub
parent 8b46964c7a
commit e86b6ef056
47 changed files with 2562 additions and 1788 deletions
+47
View File
@@ -0,0 +1,47 @@
package analyzer
import (
"context"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
)
// A base interface for all analyzers
type Analyzer interface {
Name() string
}
type Action int
const (
ActionUnknown Action = iota
ActionAllow
ActionConfirm
ActionBlock
)
type PackageVersionAnalysisResult struct {
PackageVersion *packagev1.PackageVersion
// Analyser specific analysis ID
AnalysisID string
// Reference URL for the analysis
ReferenceURL string
// The action to take as recommended by the analyzer
Action Action
// Summary of the analysis
Summary string
// Analyzer specific data
Data any
}
// Contract for implementing package version specific analyzers
type PackageVersionAnalyzer interface {
Analyzer
Analyze(ctx context.Context, packageVersion *packagev1.PackageVersion) (*PackageVersionAnalysisResult, error)
}
+80
View File
@@ -0,0 +1,80 @@
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"
"google.golang.org/grpc"
)
type MalysisQueryAnalyzerConfig struct{}
type malysisQueryAnalyzer struct {
client malysisv1grpc.MalwareAnalysisServiceClient
Config MalysisQueryAnalyzerConfig
}
var _ Analyzer = &malysisQueryAnalyzer{}
var _ PackageVersionAnalyzer = &malysisQueryAnalyzer{}
func NewMalysisQueryAnalyzer(config MalysisQueryAnalyzerConfig) (*malysisQueryAnalyzer, error) {
client, err := drygrpc.GrpcClient("pmg-malysis-query",
"community-api.safedep.io", "443", "", 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
}
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(),
}
// Mark the package version to be confirmed if it is malicious (not confirmed)
if res.GetReport().GetInference().GetIsMalware() {
analysisResult.Action = ActionConfirm
}
// This is a confirmed malicious package, we must always block it
if res.GetVerificationRecord().GetIsMalware() {
analysisResult.Action = ActionBlock
}
return analysisResult, nil
}
func malysisReportUrl(analysisId string) string {
return fmt.Sprintf("https://platform.safedep.io/community/malysis/%s", analysisId)
}