mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: add CloudSinkEnvResolver interface with default implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add GitHub Actions environment resolver for cloud sink Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: populate invocation context with CI environment on cloud events Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address lint errors in cloud sink tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use getter-based CloudSinkCIResolver with nil-when-no-CI Rename to CloudSinkCIResolver with focused CI concern. Factory returns nil when no CI is detected, removing the need for IsCI() and a default resolver. Leaves room for a separate agent resolver in the future. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add CI metadata support using updated API SDK Update SDK to include SetMetadata on EndpointCIContext. Add Metadata() to CloudSinkCIResolver interface and GitHub Actions implementation (workflow, job, run_attempt, server_url). Wire metadata into buildInvocationContext. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: address review comments on CI resolver - Inject CloudSinkCIResolver as dependency into newCloudSink for testability - Check both GITHUB_ACTIONS and GITHUB_RUN_ID for GHA environment detection - Make factory and constructor package-private (newCloudSinkCIResolver, newGithubActionsCIResolver) - Attach invocation context only to session complete events, not every event Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: fail fast on os.Getwd error instead of swallowing it Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
297 lines
7.9 KiB
Go
297 lines
7.9 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
|
"github.com/safedep/dry/log"
|
|
"github.com/safedep/pmg/config"
|
|
"github.com/safedep/pmg/internal/analytics"
|
|
)
|
|
|
|
var global *auditor
|
|
|
|
func setGlobal(a *auditor) {
|
|
global = a
|
|
}
|
|
|
|
func resetGlobal() {
|
|
global = nil
|
|
}
|
|
|
|
// Initialize sets up the audit system with an eventlog sink and, when enabled,
|
|
// a cloud sync sink.
|
|
func Initialize(cfg *config.RuntimeConfig) error {
|
|
var sinks []Sink
|
|
sinks = append(sinks, newEventlogSink())
|
|
|
|
if cfg.Config.Cloud.Enabled && !analytics.IsDisabled() {
|
|
cs, err := newCloudSink(cfg, newCloudSinkCIResolver())
|
|
if err != nil {
|
|
log.Warnf("Cloud sync initialization failed: %v", err)
|
|
} else {
|
|
sinks = append(sinks, cs)
|
|
}
|
|
}
|
|
|
|
if cfg.Config.Cloud.Enabled && analytics.IsDisabled() {
|
|
log.Warnf("Cloud sync is disabled because telemetry is disabled")
|
|
}
|
|
|
|
setGlobal(newAuditor(sinks...))
|
|
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, analysisID string, isMalware, isVerified bool) {
|
|
logEvent(AuditEvent{
|
|
Type: EventTypeMalwareConfirmed,
|
|
Message: fmt.Sprintf("User confirmed installation of flagged package: %s@%s", pkgName(pv), pkgVersion(pv)),
|
|
PackageVersion: pv,
|
|
AnalysisID: analysisID,
|
|
IsMalware: isMalware,
|
|
IsVerified: isVerified,
|
|
})
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
// LogDependencyCooldown records that a package was blocked by the dependency cooldown policy.
|
|
func LogDependencyCooldown(pv *packagev1.PackageVersion, publishDate time.Time, cooldownDays, daysAgo, daysLeft int) {
|
|
logEvent(AuditEvent{
|
|
Type: EventTypeDependencyCooldown,
|
|
Message: fmt.Sprintf("Package blocked by cooldown policy: %s@%s (published %d days ago, %d days remaining)", pkgName(pv), pkgVersion(pv), daysAgo, daysLeft),
|
|
PackageVersion: pv,
|
|
PublishDate: publishDate,
|
|
CooldownDays: cooldownDays,
|
|
DaysAgo: daysAgo,
|
|
DaysLeft: daysLeft,
|
|
})
|
|
|
|
if global != nil {
|
|
global.recordCooldownBlocked()
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// LogSessionComplete records the end of a PMG invocation with aggregate session stats.
|
|
func LogSessionComplete(outcome Outcome, flowType FlowType) {
|
|
if global == nil {
|
|
return
|
|
}
|
|
|
|
s := global.getSession()
|
|
if s == nil {
|
|
return
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
cfg := config.Get()
|
|
|
|
logEvent(AuditEvent{
|
|
Type: EventTypeSessionComplete,
|
|
Message: fmt.Sprintf("Session complete: %s", outcome),
|
|
SessionData: &SessionData{
|
|
PackageManager: s.packageManager,
|
|
FlowType: flowType,
|
|
Outcome: outcome,
|
|
TotalAnalyzed: s.totalAnalyzed,
|
|
AllowedCount: s.allowedCount,
|
|
BlockedCount: s.blockedCount,
|
|
ConfirmedCount: s.confirmedCount,
|
|
TrustedSkipped: s.trustedSkipped,
|
|
InsecureBypassed: s.insecureBypassed,
|
|
CooldownBlockedCount: s.cooldownBlockedCount,
|
|
Duration: time.Since(s.startTime),
|
|
SandboxEnabled: cfg.Config.Sandbox.Enabled,
|
|
ParanoidMode: cfg.Config.Paranoid,
|
|
TransitiveEnabled: cfg.Config.Transitive,
|
|
},
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|