mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add audit system with sink based dispatcher (#211)
* chore: Dependency update * feat: Add audit system with eventlog as a sink * fix: Linter fixes * fix: Code review fixes
This commit is contained in:
@@ -35,6 +35,12 @@ type PackageVersionAnalysisResult struct {
|
||||
// Summary of the analysis
|
||||
Summary string
|
||||
|
||||
// Whether the package is flagged as malware by inference
|
||||
IsMalware bool
|
||||
|
||||
// Whether the malware verdict has been verified (confirmed by a human or verification system)
|
||||
IsVerified bool
|
||||
|
||||
// Analyzer specific data
|
||||
Data any
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ func (a *malysisQueryAnalyzer) Analyze(ctx context.Context,
|
||||
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
|
||||
@@ -76,6 +77,8 @@ func (a *malysisQueryAnalyzer) Analyze(ctx context.Context,
|
||||
|
||||
// This is a confirmed malicious package, we must always block it
|
||||
if res.GetVerificationRecord().GetIsMalware() {
|
||||
analysisResult.IsMalware = true
|
||||
analysisResult.IsVerified = true
|
||||
analysisResult.Action = ActionBlock
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,11 @@ func (s *stubMalwareAnalysisServiceClient) InternalAnalyzePackage(ctx context.Co
|
||||
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
|
||||
}
|
||||
func (s *stubMalwareAnalysisServiceClient) InternalAgenticAnalyzePackage(ctx context.Context, req *malysisv1.InternalAgenticAnalyzePackageRequest, opts ...grpc.CallOption) (*malysisv1.InternalAgenticAnalyzePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubMalwareAnalysisServiceClient) InternalPublishDomainEvent(ctx context.Context, req *malysisv1.InternalPublishDomainEventRequest, opts ...grpc.CallOption) (*malysisv1.InternalPublishDomainEventResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ module github.com/safedep/pmg
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1
|
||||
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/elazarl/goproxy v1.8.1
|
||||
github.com/fatih/color v1.18.0
|
||||
@@ -12,7 +12,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.7
|
||||
github.com/posthog/posthog-go v1.5.12
|
||||
github.com/safedep/dry v0.0.0-20260331131405-bd4c66ef7083
|
||||
github.com/safedep/dry v0.0.0-20260409102613-51a5bb88eb14
|
||||
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12
|
||||
github.com/sony/gobreaker/v2 v2.4.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
@@ -20,13 +20,13 @@ require (
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.39.0
|
||||
google.golang.org/grpc v1.75.0
|
||||
golang.org/x/term v0.40.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/caarlos0/env/v11 v11.3.1 // indirect
|
||||
@@ -69,13 +69,13 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
|
||||
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2 h1:txDywYkqsXvtA/sDSMcwMjC8XTHnqlyc+3aIFMzRVeQ=
|
||||
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2/go.mod h1:fdc8Y27iNGRno4W5CMBFBOQlG75NJ3M9q0GZRvY/TVA=
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1 h1:4sM5O5dx0yUucJ1trjZ8Cm9IGX2loEc4cUyh3Xy+5eU=
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1/go.mod h1:uR95GqsnNCRn6cTyRBte6uMJMm0rEBRxTGpakKCNL9I=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1 h1:NXwdBG3BiC6xWH4iG3csbT+JHF9u1jl5ThDhumCnKnk=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1 h1:OKKSMXf1k7NZ1qcp5gfpTw0F3QGVM/X6ghkRs9GtrIA=
|
||||
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1/go.mod h1:k1tkPvr2SWI5hWfY/fF9XgIBh1sZOMPMPl/bUH3L0wE=
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1 h1:x5a6h/YeT2cqgxqmBGOCKlifMEMb4VRJ3eOgQkNvJrs=
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1/go.mod h1:I8E+sZXJNqzWBtSlRGCoiEorLSRiix50h2R/66aBzME=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
@@ -16,14 +16,16 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -36,12 +38,12 @@ github.com/elazarl/goproxy v1.8.1/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZ
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -150,8 +152,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/safedep/dry v0.0.0-20260331131405-bd4c66ef7083 h1:c2Nm7SrGYDAz5M3VSZPvovoK55Y4gT0zf8rX1eHqlsY=
|
||||
github.com/safedep/dry v0.0.0-20260331131405-bd4c66ef7083/go.mod h1:wehXuIfB9rwLygByruLsQG/71xCHIzUPQz6ikYa8G3E=
|
||||
github.com/safedep/dry v0.0.0-20260409102613-51a5bb88eb14 h1:ajMAtCe1asSa4jz8Vl65CNtzTgWTAaHdeWRu0lDn8p4=
|
||||
github.com/safedep/dry v0.0.0-20260409102613-51a5bb88eb14/go.mod h1:JOiCF9w4plbcMS6XnLNpDaSoUEabdscP+eFk+kROHrs=
|
||||
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12 h1:NzARvPtncPbVI8a8Z0JKpJ7XJCSPpsRstV7wAgEtmOU=
|
||||
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12/go.mod h1:fyt+PACz6dtEoqsnE0BPPv/lHpuBG/8zkDqeIVcyRY4=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
@@ -186,18 +188,18 @@ github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
|
||||
github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -213,8 +215,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -222,8 +224,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -232,8 +234,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -249,14 +251,14 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -270,24 +272,24 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
+6
-37
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/extractor"
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/packagemanager"
|
||||
"github.com/safedep/pmg/sandbox/executor"
|
||||
@@ -122,7 +122,7 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedComm
|
||||
|
||||
// Log the installation start
|
||||
if g.packageManager != nil {
|
||||
eventlog.LogInstallStarted(g.packageManager.Name(), args)
|
||||
audit.LogInstallStarted(g.packageManager.Name(), args)
|
||||
}
|
||||
|
||||
if g.config.InsecureInstallation {
|
||||
@@ -245,12 +245,7 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedComm
|
||||
// Log successful installation allowance
|
||||
if len(parsedCommand.InstallTargets) > 0 {
|
||||
for _, target := range parsedCommand.InstallTargets {
|
||||
eventlog.LogInstallAllowed(
|
||||
target.PackageVersion.GetPackage().GetName(),
|
||||
target.PackageVersion.GetVersion(),
|
||||
target.PackageVersion.GetPackage().GetEcosystem().String(),
|
||||
len(packagesToAnalyze),
|
||||
)
|
||||
audit.LogInstallAllowed(target.PackageVersion, len(packagesToAnalyze))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,13 +530,7 @@ func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, pa
|
||||
|
||||
// Log successful installation allowance for manifest-based installations
|
||||
if len(packages) > 0 {
|
||||
firstPkg := packages[0]
|
||||
eventlog.LogInstallAllowed(
|
||||
firstPkg.GetPackage().GetName(),
|
||||
firstPkg.GetVersion(),
|
||||
firstPkg.GetPackage().GetEcosystem().String(),
|
||||
len(packagesToAnalyze),
|
||||
)
|
||||
audit.LogInstallAllowed(packages[0], len(packagesToAnalyze))
|
||||
}
|
||||
|
||||
g.clearStatus()
|
||||
@@ -554,29 +543,9 @@ func (g *packageManagerGuard) logMalwareDetection(result *analyzer.PackageVersio
|
||||
return
|
||||
}
|
||||
|
||||
pkg := result.PackageVersion.GetPackage()
|
||||
if pkg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
details := map[string]interface{}{
|
||||
"analysis_id": result.AnalysisID,
|
||||
"reference_url": result.ReferenceURL,
|
||||
}
|
||||
|
||||
if blocked {
|
||||
eventlog.LogMalwareBlocked(
|
||||
pkg.GetName(),
|
||||
result.PackageVersion.GetVersion(),
|
||||
pkg.GetEcosystem().String(),
|
||||
result.Summary,
|
||||
details,
|
||||
)
|
||||
audit.LogMalwareBlocked(result.PackageVersion, result.Summary, result.AnalysisID, result.ReferenceURL, result.IsMalware, result.IsVerified)
|
||||
} else {
|
||||
eventlog.LogMalwareConfirmed(
|
||||
pkg.GetName(),
|
||||
result.PackageVersion.GetVersion(),
|
||||
pkg.GetEcosystem().String(),
|
||||
)
|
||||
audit.LogMalwareConfirmed(result.PackageVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
)
|
||||
|
||||
var global *auditor
|
||||
|
||||
func setGlobal(a *auditor) {
|
||||
global = a
|
||||
}
|
||||
|
||||
func resetGlobal() {
|
||||
global = nil
|
||||
}
|
||||
|
||||
func Initialize() error {
|
||||
a := newAuditor(newEventlogSink())
|
||||
global = a
|
||||
return nil
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
if global == nil {
|
||||
return nil
|
||||
}
|
||||
return global.close()
|
||||
}
|
||||
|
||||
func logEvent(event AuditEvent) {
|
||||
if global == nil {
|
||||
return
|
||||
}
|
||||
global.dispatch(context.Background(), event)
|
||||
}
|
||||
|
||||
func pkgName(pv *packagev1.PackageVersion) string {
|
||||
if pv != nil {
|
||||
if pkg := pv.GetPackage(); pkg != nil {
|
||||
return pkg.GetName()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func pkgVersion(pv *packagev1.PackageVersion) string {
|
||||
if pv != nil {
|
||||
return pv.GetVersion()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func pkgEcosystem(pv *packagev1.PackageVersion) string {
|
||||
if pv != nil {
|
||||
if pkg := pv.GetPackage(); pkg != nil {
|
||||
return pkg.GetEcosystem().String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LogMalwareBlocked records that a package was blocked due to malware detection.
|
||||
func LogMalwareBlocked(pv *packagev1.PackageVersion, reason, analysisID, referenceURL string, isMalware, isVerified bool) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeMalwareBlocked,
|
||||
Message: fmt.Sprintf("Blocked installation of malicious package: %s@%s", pkgName(pv), pkgVersion(pv)),
|
||||
PackageVersion: pv,
|
||||
AnalysisID: analysisID,
|
||||
IsMalware: isMalware,
|
||||
IsVerified: isVerified,
|
||||
Details: map[string]interface{}{
|
||||
"reason": reason,
|
||||
"analysis_id": analysisID,
|
||||
"reference_url": referenceURL,
|
||||
},
|
||||
})
|
||||
|
||||
if global != nil {
|
||||
global.recordBlocked()
|
||||
}
|
||||
}
|
||||
|
||||
// LogMalwareConfirmed records that the user confirmed installation of a flagged package.
|
||||
func LogMalwareConfirmed(pv *packagev1.PackageVersion) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeMalwareConfirmed,
|
||||
Message: fmt.Sprintf("User confirmed installation of flagged package: %s@%s", pkgName(pv), pkgVersion(pv)),
|
||||
PackageVersion: pv,
|
||||
})
|
||||
|
||||
if global != nil {
|
||||
global.recordConfirmed()
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallAllowed records that a package passed security checks and installation was permitted.
|
||||
func LogInstallAllowed(pv *packagev1.PackageVersion, packageCount int) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeInstallAllowed,
|
||||
Message: fmt.Sprintf("Installation allowed for %s@%s (%d packages analyzed)", pkgName(pv), pkgVersion(pv), packageCount),
|
||||
PackageVersion: pv,
|
||||
Details: map[string]interface{}{
|
||||
"packages_analyzed": packageCount,
|
||||
},
|
||||
PackageCount: packageCount,
|
||||
})
|
||||
|
||||
if global != nil {
|
||||
global.recordAllowed()
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallTrustedAllowed records that a trusted package skipped security analysis.
|
||||
func LogInstallTrustedAllowed(pv *packagev1.PackageVersion) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeInstallTrustedAllowed,
|
||||
Message: fmt.Sprintf("Installation allowed for trusted package: %s@%s", pkgName(pv), pkgVersion(pv)),
|
||||
PackageVersion: pv,
|
||||
})
|
||||
|
||||
if global != nil {
|
||||
global.recordTrustedSkipped()
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallInsecureBypass records that a package bypassed security analysis due to insecure mode.
|
||||
func LogInstallInsecureBypass(pv *packagev1.PackageVersion) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeInstallInsecureBypass,
|
||||
Message: fmt.Sprintf("Installation bypassed analysis due to insecure installation mode: %s@%s", pkgName(pv), pkgVersion(pv)),
|
||||
PackageVersion: pv,
|
||||
})
|
||||
|
||||
if global != nil {
|
||||
global.recordInsecureBypassed()
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallStarted records the start of a package installation session.
|
||||
func LogInstallStarted(packageManager string, args []string) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeInstallStarted,
|
||||
Message: fmt.Sprintf("Starting package installation with %s", packageManager),
|
||||
Details: map[string]interface{}{
|
||||
"package_manager": packageManager,
|
||||
"arguments": args,
|
||||
},
|
||||
PackageManager: packageManager,
|
||||
Args: args,
|
||||
})
|
||||
|
||||
if global != nil {
|
||||
global.startSession(packageManager, args)
|
||||
}
|
||||
}
|
||||
|
||||
// LogProxyHostObserved records an outbound host observed by the proxy that is not a known registry.
|
||||
func LogProxyHostObserved(hostname, method, reason string, details map[string]interface{}) {
|
||||
base := map[string]interface{}{
|
||||
"hostname": hostname,
|
||||
"method": method,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeProxyHostObserved,
|
||||
Message: fmt.Sprintf("Proxy observed outbound host: %s", hostname),
|
||||
Details: mergeDetails(base, details),
|
||||
Hostname: hostname,
|
||||
Method: method,
|
||||
Reason: reason,
|
||||
})
|
||||
}
|
||||
|
||||
// LogSandboxOverride records that runtime sandbox policy overrides were applied.
|
||||
func LogSandboxOverride(sandboxProfile string, overrides []map[string]string) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeSandboxOverride,
|
||||
Message: fmt.Sprintf("Sandbox runtime overrides applied (%d rules)", len(overrides)),
|
||||
Details: map[string]interface{}{
|
||||
"sandbox_profile": sandboxProfile,
|
||||
"sandbox_runtime_overrides": overrides,
|
||||
},
|
||||
ProfileName: sandboxProfile,
|
||||
Overrides: overrides,
|
||||
})
|
||||
}
|
||||
|
||||
// LogError records a significant error during PMG operation.
|
||||
func LogError(message string, err error) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeError,
|
||||
Message: message,
|
||||
Error: err,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
event.Details = map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
logEvent(event)
|
||||
}
|
||||
|
||||
func mergeDetails(base, extra map[string]interface{}) map[string]interface{} {
|
||||
if base == nil {
|
||||
base = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range extra {
|
||||
base[k] = v
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockSink struct {
|
||||
mu sync.Mutex
|
||||
events []AuditEvent
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (m *mockSink) Handle(_ context.Context, event AuditEvent) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.events = append(m.events, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockSink) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockSink) getEvents() []AuditEvent {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cp := make([]AuditEvent, len(m.events))
|
||||
copy(cp, m.events)
|
||||
return cp
|
||||
}
|
||||
|
||||
func testPackageVersion(name, version, ecosystem string) *packagev1.PackageVersion {
|
||||
eco := packagev1.Ecosystem_ECOSYSTEM_UNSPECIFIED
|
||||
switch ecosystem {
|
||||
case "npm":
|
||||
eco = packagev1.Ecosystem_ECOSYSTEM_NPM
|
||||
case "pypi":
|
||||
eco = packagev1.Ecosystem_ECOSYSTEM_PYPI
|
||||
}
|
||||
return &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Name: name,
|
||||
Ecosystem: eco,
|
||||
},
|
||||
Version: version,
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditorDispatchesToAllSinks(t *testing.T) {
|
||||
s1 := &mockSink{}
|
||||
s2 := &mockSink{}
|
||||
a := newAuditor(s1, s2)
|
||||
|
||||
event := AuditEvent{Type: EventTypeMalwareBlocked, Message: "test"}
|
||||
a.dispatch(context.Background(), event)
|
||||
|
||||
assert.Len(t, s1.getEvents(), 1)
|
||||
assert.Len(t, s2.getEvents(), 1)
|
||||
assert.Equal(t, EventTypeMalwareBlocked, s1.getEvents()[0].Type)
|
||||
assert.Equal(t, EventTypeMalwareBlocked, s2.getEvents()[0].Type)
|
||||
}
|
||||
|
||||
func TestAuditorSetsTimestamp(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
|
||||
before := time.Now()
|
||||
a.dispatch(context.Background(), AuditEvent{Type: EventTypeError})
|
||||
after := time.Now()
|
||||
|
||||
events := s.getEvents()
|
||||
require.Len(t, events, 1)
|
||||
assert.False(t, events[0].Timestamp.IsZero())
|
||||
assert.True(t, !events[0].Timestamp.Before(before))
|
||||
assert.True(t, !events[0].Timestamp.After(after))
|
||||
}
|
||||
|
||||
func TestAuditorCloseClosesAllSinks(t *testing.T) {
|
||||
s1 := &mockSink{}
|
||||
s2 := &mockSink{}
|
||||
a := newAuditor(s1, s2)
|
||||
|
||||
err := a.close()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, s1.closed)
|
||||
assert.True(t, s2.closed)
|
||||
}
|
||||
|
||||
func TestAuditorSessionTracking(t *testing.T) {
|
||||
a := newAuditor()
|
||||
|
||||
// No session yet — record calls are no-ops
|
||||
a.recordAllowed()
|
||||
assert.Nil(t, a.getSession())
|
||||
|
||||
a.startSession("npm", []string{"install", "lodash"})
|
||||
s := a.getSession()
|
||||
require.NotNil(t, s)
|
||||
assert.Equal(t, "npm", s.packageManager)
|
||||
assert.Equal(t, []string{"install", "lodash"}, s.args)
|
||||
|
||||
a.recordAllowed()
|
||||
a.recordBlocked()
|
||||
a.recordConfirmed()
|
||||
a.recordTrustedSkipped()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
assert.Equal(t, uint32(3), s.totalAnalyzed) // confirmed doesn't count — LogInstallAllowed does
|
||||
assert.Equal(t, uint32(1), s.allowedCount)
|
||||
assert.Equal(t, uint32(1), s.blockedCount)
|
||||
assert.Equal(t, uint32(1), s.confirmedCount)
|
||||
assert.Equal(t, uint32(1), s.trustedSkipped)
|
||||
}
|
||||
|
||||
func TestPublicAPIDispatchesToSinks(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
setGlobal(a)
|
||||
defer resetGlobal()
|
||||
|
||||
pv := testPackageVersion("evil", "1.0.0", "npm")
|
||||
LogMalwareBlocked(pv, "malware", "analysis-1", "https://ref", true, false)
|
||||
|
||||
events := s.getEvents()
|
||||
require.Len(t, events, 1)
|
||||
assert.Equal(t, EventTypeMalwareBlocked, events[0].Type)
|
||||
assert.Equal(t, pv, events[0].PackageVersion)
|
||||
assert.Equal(t, "malware", events[0].Details["reason"])
|
||||
assert.Equal(t, "analysis-1", events[0].AnalysisID)
|
||||
assert.Equal(t, true, events[0].IsMalware)
|
||||
}
|
||||
|
||||
func TestPublicAPISilentWhenNotInitialized(t *testing.T) {
|
||||
resetGlobal()
|
||||
|
||||
// None of these should panic
|
||||
LogMalwareBlocked(nil, "reason", "", "", false, false)
|
||||
LogMalwareConfirmed(nil)
|
||||
LogInstallAllowed(nil, 5)
|
||||
LogInstallTrustedAllowed(nil)
|
||||
LogInstallInsecureBypass(nil)
|
||||
LogInstallStarted("npm", []string{"install"})
|
||||
LogProxyHostObserved("host", "GET", "reason", nil)
|
||||
LogSandboxOverride("profile", nil)
|
||||
LogError("err", nil)
|
||||
}
|
||||
|
||||
func TestLogInstallStartedInitializesSession(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
setGlobal(a)
|
||||
defer resetGlobal()
|
||||
|
||||
LogInstallStarted("pip", []string{"install", "requests"})
|
||||
|
||||
sess := a.getSession()
|
||||
require.NotNil(t, sess)
|
||||
assert.Equal(t, "pip", sess.packageManager)
|
||||
assert.Equal(t, []string{"install", "requests"}, sess.args)
|
||||
}
|
||||
|
||||
func TestLogInstallAllowedIncrementsSession(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
setGlobal(a)
|
||||
defer resetGlobal()
|
||||
|
||||
a.startSession("npm", nil)
|
||||
LogInstallAllowed(testPackageVersion("pkg", "1.0", "npm"), 3)
|
||||
|
||||
sess := a.getSession()
|
||||
require.NotNil(t, sess)
|
||||
assert.Equal(t, uint32(1), sess.allowedCount)
|
||||
assert.Equal(t, uint32(1), sess.totalAnalyzed)
|
||||
}
|
||||
|
||||
func TestLogMalwareBlockedIncrementsSession(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
setGlobal(a)
|
||||
defer resetGlobal()
|
||||
|
||||
a.startSession("npm", nil)
|
||||
LogMalwareBlocked(testPackageVersion("evil", "1.0", "npm"), "bad", "", "", true, false)
|
||||
|
||||
sess := a.getSession()
|
||||
require.NotNil(t, sess)
|
||||
assert.Equal(t, uint32(1), sess.blockedCount)
|
||||
assert.Equal(t, uint32(1), sess.totalAnalyzed)
|
||||
}
|
||||
|
||||
func TestLogMalwareConfirmedIncrementsSession(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
setGlobal(a)
|
||||
defer resetGlobal()
|
||||
|
||||
a.startSession("npm", nil)
|
||||
LogMalwareConfirmed(testPackageVersion("pkg", "1.0", "npm"))
|
||||
|
||||
sess := a.getSession()
|
||||
require.NotNil(t, sess)
|
||||
assert.Equal(t, uint32(1), sess.confirmedCount)
|
||||
assert.Equal(t, uint32(0), sess.totalAnalyzed) // confirmed doesn't increment — LogInstallAllowed does
|
||||
}
|
||||
|
||||
func TestLogInstallTrustedAllowedIncrementsSession(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
setGlobal(a)
|
||||
defer resetGlobal()
|
||||
|
||||
a.startSession("npm", nil)
|
||||
LogInstallTrustedAllowed(testPackageVersion("pkg", "1.0", "npm"))
|
||||
|
||||
sess := a.getSession()
|
||||
require.NotNil(t, sess)
|
||||
assert.Equal(t, uint32(1), sess.trustedSkipped)
|
||||
assert.Equal(t, uint32(1), sess.totalAnalyzed)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
)
|
||||
|
||||
type session struct {
|
||||
mu sync.Mutex
|
||||
startTime time.Time
|
||||
packageManager string
|
||||
args []string
|
||||
totalAnalyzed uint32
|
||||
allowedCount uint32
|
||||
blockedCount uint32
|
||||
confirmedCount uint32
|
||||
trustedSkipped uint32
|
||||
insecureBypassed uint32
|
||||
}
|
||||
|
||||
type auditor struct {
|
||||
sinks []Sink
|
||||
session *session
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newAuditor(sinks ...Sink) *auditor {
|
||||
return &auditor{sinks: sinks}
|
||||
}
|
||||
|
||||
func (a *auditor) dispatch(ctx context.Context, event AuditEvent) {
|
||||
if event.Timestamp.IsZero() {
|
||||
event.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
for _, s := range a.sinks {
|
||||
if err := s.Handle(ctx, event); err != nil {
|
||||
log.Warnf("audit sink error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *auditor) close() error {
|
||||
var firstErr error
|
||||
for _, s := range a.sinks {
|
||||
if err := s.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (a *auditor) startSession(packageManager string, args []string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.session = &session{
|
||||
startTime: time.Now(),
|
||||
packageManager: packageManager,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *auditor) getSession() *session {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.session
|
||||
}
|
||||
|
||||
func (a *auditor) recordAllowed() {
|
||||
s := a.getSession()
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.allowedCount++
|
||||
s.totalAnalyzed++
|
||||
}
|
||||
|
||||
func (a *auditor) recordBlocked() {
|
||||
s := a.getSession()
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.blockedCount++
|
||||
s.totalAnalyzed++
|
||||
}
|
||||
|
||||
// recordConfirmed tracks that a user confirmed a suspicious package. It does not
|
||||
// increment totalAnalyzed because the subsequent LogInstallAllowed call for the
|
||||
// same package already does that.
|
||||
func (a *auditor) recordConfirmed() {
|
||||
s := a.getSession()
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.confirmedCount++
|
||||
}
|
||||
|
||||
func (a *auditor) recordTrustedSkipped() {
|
||||
s := a.getSession()
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.trustedSkipped++
|
||||
s.totalAnalyzed++
|
||||
}
|
||||
|
||||
func (a *auditor) recordInsecureBypassed() {
|
||||
s := a.getSession()
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.insecureBypassed++
|
||||
s.totalAnalyzed++
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeMalwareBlocked EventType = "malware_blocked"
|
||||
EventTypeMalwareConfirmed EventType = "malware_confirmed"
|
||||
EventTypeInstallAllowed EventType = "install_allowed"
|
||||
EventTypeInstallTrustedAllowed EventType = "install_trusted_allowed"
|
||||
EventTypeInstallStarted EventType = "install_started"
|
||||
EventTypeDependencyResolved EventType = "dependency_resolved"
|
||||
EventTypeInstallInsecureBypass EventType = "install_insecure_bypass"
|
||||
EventTypeProxyHostObserved EventType = "proxy_host_observed"
|
||||
EventTypeSandboxOverride EventType = "sandbox_override"
|
||||
EventTypeError EventType = "error"
|
||||
EventTypeSessionComplete EventType = "session_complete"
|
||||
)
|
||||
|
||||
// AuditEvent is the canonical audit event for PMG. It carries all data any
|
||||
// sink might need. Fields are zero-valued when not applicable to the event type.
|
||||
type AuditEvent struct {
|
||||
Type EventType
|
||||
Timestamp time.Time
|
||||
Message string
|
||||
|
||||
// Typed package version — populated for package-level events.
|
||||
PackageVersion *packagev1.PackageVersion
|
||||
|
||||
// Analysis context
|
||||
AnalysisID string
|
||||
IsMalware bool
|
||||
IsVerified bool
|
||||
|
||||
// Freeform details for backward-compatible eventlog output.
|
||||
Details map[string]interface{}
|
||||
|
||||
// Install context
|
||||
PackageManager string
|
||||
Args []string
|
||||
PackageCount int
|
||||
|
||||
// Sandbox context
|
||||
ProfileName string
|
||||
Overrides []map[string]string
|
||||
|
||||
// Proxy context
|
||||
Hostname string
|
||||
Method string
|
||||
Reason string
|
||||
|
||||
// Error context
|
||||
Error error
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
)
|
||||
|
||||
type eventlogSink struct{}
|
||||
|
||||
func newEventlogSink() *eventlogSink {
|
||||
return &eventlogSink{}
|
||||
}
|
||||
|
||||
func (s *eventlogSink) Handle(_ context.Context, event AuditEvent) error {
|
||||
elEvent := eventlog.Event{
|
||||
Timestamp: event.Timestamp,
|
||||
EventType: mapEventType(event.Type),
|
||||
Message: event.Message,
|
||||
PackageName: pkgName(event.PackageVersion),
|
||||
Version: pkgVersion(event.PackageVersion),
|
||||
Ecosystem: pkgEcosystem(event.PackageVersion),
|
||||
Details: event.Details,
|
||||
}
|
||||
return eventlog.LogEvent(elEvent)
|
||||
}
|
||||
|
||||
func (s *eventlogSink) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapEventType(t EventType) eventlog.EventType {
|
||||
switch t {
|
||||
case EventTypeMalwareBlocked:
|
||||
return eventlog.EventTypeMalwareBlocked
|
||||
case EventTypeMalwareConfirmed:
|
||||
return eventlog.EventTypeMalwareConfirmed
|
||||
case EventTypeInstallAllowed:
|
||||
return eventlog.EventTypeInstallAllowed
|
||||
case EventTypeInstallTrustedAllowed:
|
||||
return eventlog.EventTypeInstallTrustedAllowed
|
||||
case EventTypeInstallStarted:
|
||||
return eventlog.EventTypeInstallStarted
|
||||
case EventTypeDependencyResolved:
|
||||
return eventlog.EventTypeDependencyResolved
|
||||
case EventTypeInstallInsecureBypass:
|
||||
return eventlog.EventTypeInstallInsecureBypass
|
||||
case EventTypeProxyHostObserved:
|
||||
return eventlog.EventTypeProxyHostObserved
|
||||
case EventTypeSandboxOverride:
|
||||
return eventlog.EventTypeSandboxOverride
|
||||
case EventTypeError:
|
||||
return eventlog.EventTypeError
|
||||
case EventTypeSessionComplete:
|
||||
return eventlog.EventType("session_complete")
|
||||
default:
|
||||
return eventlog.EventType(string(t))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEventlogSinkTranslatesAllEventTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input EventType
|
||||
expected eventlog.EventType
|
||||
}{
|
||||
{"malware_blocked", EventTypeMalwareBlocked, eventlog.EventTypeMalwareBlocked},
|
||||
{"malware_confirmed", EventTypeMalwareConfirmed, eventlog.EventTypeMalwareConfirmed},
|
||||
{"install_allowed", EventTypeInstallAllowed, eventlog.EventTypeInstallAllowed},
|
||||
{"install_trusted_allowed", EventTypeInstallTrustedAllowed, eventlog.EventTypeInstallTrustedAllowed},
|
||||
{"install_started", EventTypeInstallStarted, eventlog.EventTypeInstallStarted},
|
||||
{"dependency_resolved", EventTypeDependencyResolved, eventlog.EventTypeDependencyResolved},
|
||||
{"install_insecure_bypass", EventTypeInstallInsecureBypass, eventlog.EventTypeInstallInsecureBypass},
|
||||
{"proxy_host_observed", EventTypeProxyHostObserved, eventlog.EventTypeProxyHostObserved},
|
||||
{"sandbox_override", EventTypeSandboxOverride, eventlog.EventTypeSandboxOverride},
|
||||
{"error", EventTypeError, eventlog.EventTypeError},
|
||||
{"session_complete", EventTypeSessionComplete, eventlog.EventType("session_complete")},
|
||||
{"unknown_type", EventType("custom_event"), eventlog.EventType("custom_event")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mapEventType(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package audit
|
||||
|
||||
import "context"
|
||||
|
||||
// Sink processes audit events. Implementations decide which event types they
|
||||
// care about. Handle must not block the caller for I/O-heavy operations.
|
||||
type Sink interface {
|
||||
Handle(ctx context.Context, event AuditEvent) error
|
||||
Close() error
|
||||
}
|
||||
@@ -126,7 +126,9 @@ func InitializeWithDir(logDir string) error {
|
||||
func reinitializeForTest(logDir string) error {
|
||||
// Close existing logger if any
|
||||
if globalLogger != nil {
|
||||
globalLogger.Close()
|
||||
if err := globalLogger.Close(); err != nil {
|
||||
log.Warnf("failed to close existing logger: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset once
|
||||
@@ -281,8 +283,6 @@ func (l *fileWithRotationLogger) IsActive() bool {
|
||||
return l.active
|
||||
}
|
||||
|
||||
// Global logging functions
|
||||
|
||||
// LogEvent logs an event using the global logger
|
||||
func LogEvent(event Event) error {
|
||||
// If logger is not initialized or not active, silently fail
|
||||
@@ -293,158 +293,6 @@ func LogEvent(event Event) error {
|
||||
return globalLogger.Log(event)
|
||||
}
|
||||
|
||||
// LogMalwareBlocked logs when malware is blocked
|
||||
func LogMalwareBlocked(packageName, version, ecosystem, reason string, details map[string]interface{}) {
|
||||
event := Event{
|
||||
EventType: EventTypeMalwareBlocked,
|
||||
Message: fmt.Sprintf("Blocked installation of malicious package: %s@%s", packageName, version),
|
||||
PackageName: packageName,
|
||||
Version: version,
|
||||
Ecosystem: ecosystem,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
if details == nil {
|
||||
event.Details = make(map[string]interface{})
|
||||
}
|
||||
|
||||
event.Details["reason"] = reason
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log malware blocked event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogMalwareConfirmed logs when user confirms installation despite warning
|
||||
func LogMalwareConfirmed(packageName, version, ecosystem string) {
|
||||
event := Event{
|
||||
EventType: EventTypeMalwareConfirmed,
|
||||
Message: fmt.Sprintf("User confirmed installation of flagged package: %s@%s", packageName, version),
|
||||
PackageName: packageName,
|
||||
Version: version,
|
||||
Ecosystem: ecosystem,
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log malware confirmed event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallAllowed logs when an installation is allowed
|
||||
func LogInstallAllowed(packageName, version, ecosystem string, packageCount int) {
|
||||
event := Event{
|
||||
EventType: EventTypeInstallAllowed,
|
||||
Message: fmt.Sprintf("Installation allowed for %s@%s (%d packages analyzed)", packageName, version, packageCount),
|
||||
PackageName: packageName,
|
||||
Version: version,
|
||||
Ecosystem: ecosystem,
|
||||
Details: map[string]interface{}{
|
||||
"packages_analyzed": packageCount,
|
||||
},
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log install allowed event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallTrustedAllowed logs when an installation is allowed for a trusted package
|
||||
func LogInstallTrustedAllowed(packageName, version, ecosystem string) {
|
||||
event := Event{
|
||||
EventType: EventTypeInstallTrustedAllowed,
|
||||
Message: fmt.Sprintf("Installation allowed for trusted package: %s@%s", packageName, version),
|
||||
PackageName: packageName,
|
||||
Version: version,
|
||||
Ecosystem: ecosystem,
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log install trusted allowed event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallInsecureBypass logs when an installation skips analysis due to insecure installation mode.
|
||||
func LogInstallInsecureBypass(packageName, version, ecosystem string) {
|
||||
event := Event{
|
||||
EventType: EventTypeInstallInsecureBypass,
|
||||
Message: fmt.Sprintf("Installation bypassed analysis due to insecure installation mode: %s@%s", packageName, version),
|
||||
PackageName: packageName,
|
||||
Version: version,
|
||||
Ecosystem: ecosystem,
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log install insecure bypass event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInstallStarted logs when an installation starts
|
||||
func LogInstallStarted(packageManager string, args []string) {
|
||||
event := Event{
|
||||
EventType: EventTypeInstallStarted,
|
||||
Message: fmt.Sprintf("Starting package installation with %s", packageManager),
|
||||
Details: map[string]interface{}{
|
||||
"package_manager": packageManager,
|
||||
"arguments": args,
|
||||
},
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log install started event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogProxyHostObserved logs when proxy mode observes outbound traffic to a host.
|
||||
func LogProxyHostObserved(hostname, method, reason string, details map[string]interface{}) {
|
||||
event := Event{
|
||||
EventType: EventTypeProxyHostObserved,
|
||||
Message: fmt.Sprintf("Proxy observed outbound host: %s", hostname),
|
||||
Details: map[string]interface{}{
|
||||
"hostname": hostname,
|
||||
"method": method,
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range details {
|
||||
event.Details[k] = v
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log proxy host observed event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogSandboxOverrides logs when runtime sandbox allow overrides are applied.
|
||||
func LogSandboxOverrides(sandboxProfile string, overrides []map[string]string) {
|
||||
event := Event{
|
||||
EventType: EventTypeSandboxOverride,
|
||||
Message: fmt.Sprintf("Sandbox runtime overrides applied (%d rules)", len(overrides)),
|
||||
Details: map[string]interface{}{
|
||||
"sandbox_profile": sandboxProfile,
|
||||
"sandbox_runtime_overrides": overrides,
|
||||
},
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log sandbox override event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError logs an error event
|
||||
func LogError(message string, err error) {
|
||||
event := Event{
|
||||
EventType: EventTypeError,
|
||||
Message: message,
|
||||
Details: map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := LogEvent(event); err != nil {
|
||||
log.Warnf("failed to log error event: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the global logger
|
||||
func Close() error {
|
||||
if globalLogger != nil {
|
||||
|
||||
@@ -85,12 +85,10 @@ func TestLogEvent(t *testing.T) {
|
||||
assert.Equal(t, "evil-package", loggedEvent.PackageName)
|
||||
}
|
||||
|
||||
func TestLogMalwareBlocked(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
func TestLogEventMalwareBlocked(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, ".pmg", "logs")
|
||||
|
||||
// Initialize logger
|
||||
err := reinitializeForTest(logDir)
|
||||
assert.NoError(t, err, "Failed to initialize logger")
|
||||
defer func() {
|
||||
@@ -98,10 +96,16 @@ func TestLogMalwareBlocked(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
// Log malware blocked event
|
||||
LogMalwareBlocked("malicious-pkg", "2.0.0", "pypi", "Contains known malware", nil)
|
||||
err = LogEvent(Event{
|
||||
EventType: EventTypeMalwareBlocked,
|
||||
Message: "Blocked installation of malicious package: malicious-pkg@2.0.0",
|
||||
PackageName: "malicious-pkg",
|
||||
Version: "2.0.0",
|
||||
Ecosystem: "pypi",
|
||||
Details: map[string]interface{}{"reason": "Contains known malware"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read and verify
|
||||
logFilePath := filepath.Join(logDir, time.Now().Format("20060102")+"-pmg.log")
|
||||
data, err := os.ReadFile(logFilePath)
|
||||
assert.NoError(t, err, "Failed to read log file")
|
||||
@@ -115,7 +119,7 @@ func TestLogMalwareBlocked(t *testing.T) {
|
||||
assert.Equal(t, "pypi", event.Ecosystem)
|
||||
}
|
||||
|
||||
func TestLogProxyHostObserved(t *testing.T) {
|
||||
func TestLogEventProxyHostObserved(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, ".pmg", "logs")
|
||||
|
||||
@@ -126,9 +130,17 @@ func TestLogProxyHostObserved(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
LogProxyHostObserved("example.com", "CONNECT", "connect_tunnel_no_interceptor", map[string]interface{}{
|
||||
"request_id": "abc123",
|
||||
err = LogEvent(Event{
|
||||
EventType: EventTypeProxyHostObserved,
|
||||
Message: "Proxy observed outbound host: example.com",
|
||||
Details: map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
"method": "CONNECT",
|
||||
"reason": "connect_tunnel_no_interceptor",
|
||||
"request_id": "abc123",
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
logFilePath := filepath.Join(logDir, time.Now().Format("20060102")+"-pmg.log")
|
||||
data, err := os.ReadFile(logFilePath)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/safedep/pmg/cmd/version"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/analytics"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
appVersion "github.com/safedep/pmg/internal/version"
|
||||
@@ -84,6 +85,10 @@ func main() {
|
||||
ui.Fatalf("failed to initialize event logging: %v", eventlogErr)
|
||||
}
|
||||
|
||||
if err := audit.Initialize(); err != nil {
|
||||
log.Warnf("failed to initialize audit system: %v", err)
|
||||
}
|
||||
|
||||
config.FinalizeDependencyCooldownOverride()
|
||||
|
||||
// Parse and validate --sandbox-allow flags after all flags are resolved
|
||||
@@ -131,6 +136,11 @@ func main() {
|
||||
|
||||
defer analytics.Close()
|
||||
defer eventlog.Close()
|
||||
defer func() {
|
||||
if err := audit.Close(); err != nil {
|
||||
log.Warnf("failed to close audit system: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
analytics.TrackCommandRun()
|
||||
analytics.TrackCI()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package interceptors
|
||||
|
||||
import (
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/proxy"
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ func (i *AuditLoggerInterceptor) HandleRequest(ctx *proxy.RequestContext) (*prox
|
||||
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
||||
}
|
||||
|
||||
eventlog.LogProxyHostObserved(ctx.Hostname, ctx.Method, "audit_logger_interceptor", map[string]interface{}{
|
||||
audit.LogProxyHostObserved(ctx.Hostname, ctx.Method, "audit_logger_interceptor", map[string]interface{}{
|
||||
"request_id": ctx.RequestID,
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/proxy"
|
||||
gobreaker "github.com/sony/gobreaker/v2"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -82,7 +82,7 @@ func (b *baseRegistryInterceptor) analyzePackage(
|
||||
if cfg := config.Get(); cfg.InsecureInstallation {
|
||||
log.Debugf("[%s] Skipping insecure installation", ctx.RequestID)
|
||||
|
||||
eventlog.LogInstallInsecureBypass(packageName, packageVersion, ecosystem.String())
|
||||
audit.LogInstallInsecureBypass(pkgVersion)
|
||||
|
||||
return &analyzer.PackageVersionAnalysisResult{
|
||||
PackageVersion: pkgVersion,
|
||||
@@ -94,7 +94,7 @@ func (b *baseRegistryInterceptor) analyzePackage(
|
||||
log.Debugf("[%s] Skipping trusted package: %s/%s@%s",
|
||||
ctx.RequestID, ecosystem.String(), packageName, packageVersion)
|
||||
|
||||
eventlog.LogInstallTrustedAllowed(packageName, packageVersion, ecosystem.String())
|
||||
audit.LogInstallTrustedAllowed(pkgVersion)
|
||||
|
||||
return &analyzer.PackageVersionAnalysisResult{
|
||||
PackageVersion: pkgVersion,
|
||||
@@ -153,10 +153,7 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
case analyzer.ActionBlock:
|
||||
log.Warnf("[%s] Blocking malicious package %s@%s", ctx.RequestID, packageName, packageVersion)
|
||||
|
||||
eventlog.LogMalwareBlocked(packageName, packageVersion, ecosystem.String(), result.Summary, map[string]interface{}{
|
||||
"analysis_id": result.AnalysisID,
|
||||
"reference_url": result.ReferenceURL,
|
||||
})
|
||||
audit.LogMalwareBlocked(result.PackageVersion, result.Summary, result.AnalysisID, result.ReferenceURL, result.IsMalware, result.IsVerified)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordBlocked(result)
|
||||
@@ -195,10 +192,7 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
if !confirmed {
|
||||
log.Infof("[%s] User declined installation of suspicious package %s/%s@%s", ctx.RequestID, ecosystem.String(), packageName, packageVersion)
|
||||
|
||||
eventlog.LogMalwareBlocked(packageName, packageVersion, ecosystem.String(), result.Summary, map[string]interface{}{
|
||||
"analysis_id": result.AnalysisID,
|
||||
"reference_url": result.ReferenceURL,
|
||||
})
|
||||
audit.LogMalwareBlocked(result.PackageVersion, result.Summary, result.AnalysisID, result.ReferenceURL, result.IsMalware, result.IsVerified)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordUserCancelled(result)
|
||||
@@ -217,8 +211,8 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
}, nil
|
||||
}
|
||||
|
||||
eventlog.LogMalwareConfirmed(packageName, packageVersion, ecosystem.String())
|
||||
eventlog.LogInstallAllowed(packageName, packageVersion, ecosystem.String(), 1)
|
||||
audit.LogMalwareConfirmed(result.PackageVersion)
|
||||
audit.LogInstallAllowed(result.PackageVersion, 1)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordConfirmed(result)
|
||||
@@ -228,7 +222,7 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
||||
|
||||
case analyzer.ActionAllow:
|
||||
eventlog.LogInstallAllowed(packageName, packageVersion, ecosystem.String(), 1)
|
||||
audit.LogInstallAllowed(result.PackageVersion, 1)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordAllowed(result)
|
||||
@@ -238,7 +232,7 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
||||
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
||||
|
||||
default:
|
||||
eventlog.LogInstallAllowed(packageName, packageVersion, ecosystem.String(), 1)
|
||||
audit.LogInstallAllowed(result.PackageVersion, 1)
|
||||
|
||||
if b.statsCollector != nil {
|
||||
b.statsCollector.RecordAllowed(result)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/dry/utils"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/eventlog"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/sandbox"
|
||||
"github.com/safedep/pmg/sandbox/platform"
|
||||
"github.com/safedep/pmg/usefulerror"
|
||||
@@ -119,7 +119,7 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
|
||||
// Apply runtime --sandbox-allow overrides to the policy before execution
|
||||
if len(cfg.SandboxAllowOverrides) > 0 {
|
||||
applyRuntimeOverrides(policy, cfg.SandboxAllowOverrides)
|
||||
logSandboxOverridesToEventLog(policy.Name, cfg.SandboxAllowOverrides)
|
||||
logSandboxOverrides(policy.Name, cfg.SandboxAllowOverrides)
|
||||
}
|
||||
|
||||
if !policy.AppliesToPackageManager(pmName) {
|
||||
@@ -209,8 +209,8 @@ func removeExactMatch(slice []string, value string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// logSandboxOverridesToEventLog records sandbox allow overrides in the audit event log.
|
||||
func logSandboxOverridesToEventLog(profileName string, overrides []config.SandboxAllowOverride) {
|
||||
// logSandboxOverrides records sandbox allow overrides in the audit event log.
|
||||
func logSandboxOverrides(profileName string, overrides []config.SandboxAllowOverride) {
|
||||
entries := make([]map[string]string, 0, len(overrides))
|
||||
for _, o := range overrides {
|
||||
entries = append(entries, map[string]string{
|
||||
@@ -219,5 +219,5 @@ func logSandboxOverridesToEventLog(profileName string, overrides []config.Sandbo
|
||||
})
|
||||
}
|
||||
|
||||
eventlog.LogSandboxOverrides(profileName, entries)
|
||||
audit.LogSandboxOverride(profileName, entries)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user