Add comprehensive event logging system (#82)

* Add comprehensive event logging system with OS-specific location and rotation

Features:
- Event logging for security-relevant events (malware detection, installations)
- OS-specific default log locations (~/.pmg/logs/ on macOS/Linux, %LOCALAPPDATA%\pmg\logs\ on Windows)
- Automatic 7-day log rotation with daily log files (YYYYMMDD-pmg.log format)
- Support for custom log files via --log flag
- Thread-safe JSON logging with zero external dependencies

Implementation:
- New internal/eventlog package with comprehensive logging functionality
- Integration with guard.go to log malware detections and blocks
- Integration with main.go for initialization and cleanup
- Log file naming: YYYYMMDD-pmg.log (e.g., 20251216-pmg.log)
- Fail-safe design - PMG continues if logging fails

Event Types:
- malware_blocked: Malicious package blocked from installation
- malware_confirmed: User proceeded with flagged package
- install_allowed: Clean package installation allowed
- install_started: Package manager command initiated
- error: Error events

Testing:
- Comprehensive test suite with 6 passing tests
- Verified with real malware detection (e.g., @postman/tunnel-agent)
- Works with all package managers (npm, pip, etc.)

Technical Details:
- Thread-safe with mutex protection
- JSON format for easy parsing
- Automatic cleanup of logs >7 days old
- Background cleanup goroutine
- Uses only Go standard library (encoding/json, os, path/filepath, sync, time)

* Add update command support and improve event logging robustness

Features:
- Add support for npm/pnpm/bun/yarn update/upgrade/ci commands
- These commands now scan packages for malware before updating
- Closes security gap where update commands bypassed PMG protection

Improvements:
- Make event logging more defensive (graceful failure when not initialized)
- Add nil check for packageManager in guard to prevent test failures
- Add comprehensive tests for update commands

Testing:
- All 33+ unit tests passing
- Integration tests verified with real malware detection
- Tested with npm update, npm ci, npm upgrade, pnpm update, yarn upgrade

Files changed:
- packagemanager/npm.go: Added update/upgrade/ci to InstallCommands
- packagemanager/npm_test.go: Added 4 new test cases for update commands
- guard/guard.go: Added nil check for packageManager
- internal/eventlog/eventlog.go: Made logging more defensive

* Address review feedback: use log.Warnf instead of silently failing

Replace silent error handling in cleanupOldLogs with log.Warnf
to avoid completely swallowing errors when reading log directory.

Fixes reviewer feedback from abhisek.

* Remove update/upgrade command support, keep logging improvements

- Remove update/upgrade/ci commands from InstallCommands for npm, pnpm, bun, yarn
- Remove special handling for update/upgrade/ci commands in ParseCommand
- Remove update command test cases and restore original test
- Preserve logging improvements (nil check in guard.go, defensive check in eventlog.go)

All tests passing.
This commit is contained in:
EapolSniper
2025-12-31 16:05:31 +05:30
committed by GitHub
parent 2bc500f817
commit 698bd3dd13
4 changed files with 698 additions and 2 deletions
+80
View File
@@ -13,6 +13,7 @@ import (
"github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/extractor"
"github.com/safedep/pmg/internal/eventlog"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager"
)
@@ -79,6 +80,11 @@ func NewPackageManagerGuard(config PackageManagerGuardConfig,
func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedCommand *packagemanager.ParsedCommand) error {
log.Debugf("Running package manager guard with args: %v", args)
// Log the installation start
if g.packageManager != nil {
eventlog.LogInstallStarted(g.packageManager.Name(), args)
}
if g.config.InsecureInstallation {
log.Debugf("Bypassing block for unconfirmed malicious packages due to PMG_INSECURE_INSTALLATION")
@@ -150,6 +156,7 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedComm
for _, result := range analysisResults {
if result.Action == analyzer.ActionBlock {
blockConfig.MalwarePackages = append(blockConfig.MalwarePackages, result)
g.logMalwareDetection(result, true)
return g.blockInstallation(blockConfig)
}
@@ -167,12 +174,32 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedComm
if !confirmed {
blockConfig.ShowReference = false
blockConfig.MalwarePackages = confirmableMalwarePackages
for _, pkg := range confirmableMalwarePackages {
g.logMalwareDetection(pkg, true)
}
return g.blockInstallation(blockConfig)
}
// User confirmed installation despite warning
for _, pkg := range confirmableMalwarePackages {
g.logMalwareDetection(pkg, false)
}
}
log.Debugf("No malicious packages found, continuing execution")
// 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),
)
}
}
g.clearStatus()
return g.continueExecution(ctx, parsedCommand)
}
@@ -388,12 +415,65 @@ func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, pa
if !confirmed {
blockConfig.ShowReference = false
blockConfig.MalwarePackages = confirmableMalwarePackages
for _, pkg := range confirmableMalwarePackages {
g.logMalwareDetection(pkg, true)
}
return g.blockInstallation(blockConfig)
}
// User confirmed installation despite warning
for _, pkg := range confirmableMalwarePackages {
g.logMalwareDetection(pkg, false)
}
}
log.Debugf("No malicious packages found in manifest files, continuing execution")
// 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),
)
}
g.clearStatus()
return g.continueExecution(ctx, parsedCommand)
}
// logMalwareDetection logs malware detection events
func (g *packageManagerGuard) logMalwareDetection(result *analyzer.PackageVersionAnalysisResult, blocked bool) {
if result == nil || result.PackageVersion == nil {
return
}
pkg := result.PackageVersion.GetPackage()
if pkg == nil {
return
}
details := map[string]interface{}{
"analysis_id": result.AnalysisID,
"reference_url": result.ReferenceURL,
"summary": result.Summary,
}
if blocked {
eventlog.LogMalwareBlocked(
pkg.GetName(),
result.PackageVersion.GetVersion(),
pkg.GetEcosystem().String(),
result.Summary,
details,
)
} else {
eventlog.LogMalwareConfirmed(
pkg.GetName(),
result.PackageVersion.GetVersion(),
pkg.GetEcosystem().String(),
)
}
}