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:
Abhisek Datta
2026-04-10 19:07:15 +05:30
committed by GitHub
parent e72ff6aeaf
commit 0355a5d4fd
19 changed files with 872 additions and 284 deletions
+217
View File
@@ -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
}
+231
View File
@@ -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)
}
+127
View File
@@ -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++
}
+59
View File
@@ -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
}
+59
View File
@@ -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))
}
}
+36
View File
@@ -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)
})
}
}
+10
View File
@@ -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
}
+3 -155
View File
@@ -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 {
+21 -9
View File
@@ -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)