feat(analyzer): analysis cache contract (MalysisCache interface + config) (#334)

This commit is contained in:
dmdhrumilmistry
2026-06-17 15:07:47 +05:30
committed by GitHub
parent 61230fbcd7
commit baf637be97
4 changed files with 174 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
package analyzer
import (
"context"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
)
// MalysisCache is the contract for a caching layer over package malware-analysis
// verdicts. It lets a repeat analysis of the same package version be served
// without a fresh backend round-trip, which is the dominant cost when
// re-installing an already-screened dependency graph.
//
// Implementations are injected by the caller and may be backed by any store
// (filesystem, sqlite, in-memory, ...). An implementation owns its own
// expiry/TTL policy and must be safe for concurrent use by multiple goroutines.
//
// The cache stores and returns whatever verdicts it is given; the decision of
// which verdicts are safe to cache (e.g. only ALLOW) belongs to the caller, not
// the cache.
type MalysisCache interface {
// Get returns the cached analysis result for the given package version.
// The boolean is false on a miss, which includes an absent or expired
// entry. A non-nil error indicates a backend failure; callers should treat
// it as a miss and fall back to a fresh analysis rather than failing the
// operation.
Get(ctx context.Context, pkg *packagev1.PackageVersion) (*PackageVersionAnalysisResult, bool, error)
// Set stores an analysis result for the given package version. Caching is
// best-effort: a non-nil error means the verdict could not be persisted and
// callers should continue without failing.
Set(ctx context.Context, pkg *packagev1.PackageVersion, result *PackageVersionAnalysisResult) error
}
+58
View File
@@ -97,11 +97,47 @@ type Config struct {
DependencyCooldown DependencyCooldownConfig `mapstructure:"dependency_cooldown"`
// AnalysisCache configures the optional cross-run cache of malware-analysis
// verdicts, so repeat installs of an already-screened dependency graph skip
// the per-package analysis round-trip.
AnalysisCache AnalysisCacheConfig `mapstructure:"analysis_cache"`
Cloud CloudConfig `mapstructure:"cloud"`
Proxy ProxyConfig `mapstructure:"proxy"`
}
// AnalysisCacheConfig is the umbrella for per-analyzer cross-run caches. Caching
// is analyzer-specific — each analyzer decides what is safe to cache — so config
// is nested per analyzer rather than shared. Today only the Malysis (malware)
// analyzer has a cache; future analyzers can add their own sub-config here.
type AnalysisCacheConfig struct {
// Malysis configures the cross-run cache for the Malysis malware analyzer.
Malysis MalysisCacheConfig `mapstructure:"malysis"`
}
// MalysisCacheConfig configures a persistent, cross-run cache of package
// malware-analysis verdicts produced by the Malysis analyzer.
//
// By default PMG keeps an in-memory analysis cache that lives only for the
// duration of a single invocation, so every install re-screens the whole
// resolved graph against the analysis backend. When Enabled, clean (ALLOW)
// verdicts are additionally persisted on disk and reused across runs, which
// makes repeat installs of an unchanged graph fast.
//
// Security trade-off: a version that was clean when first screened but is later
// flagged as malicious is served from cache (and thus allowed) until its entry
// expires; TTL bounds that exposure window. Only ALLOW verdicts are cached —
// suspicious, malicious, and tenant-excluded verdicts are always re-evaluated.
// Disabled by default.
type MalysisCacheConfig struct {
Enabled bool `mapstructure:"enabled"`
// TTL is how long a cached verdict remains valid. A non-positive TTL
// disables persistence (entries are always treated as a miss).
TTL time.Duration `mapstructure:"ttl"`
}
// CloudConfig configures audit event sync to SafeDep Cloud.
type CloudConfig struct {
Enabled bool `mapstructure:"enabled"`
@@ -268,6 +304,7 @@ type RuntimeConfig struct {
sandboxProfileDir string
sandboxOverlayDir string
sandboxViolationCacheDir string
cacheDir string
viper *viper.Viper
}
@@ -342,6 +379,15 @@ func (r *RuntimeConfig) SandboxViolationCacheDir() string {
return r.sandboxViolationCacheDir
}
// CacheDir returns the path to the PMG cache root directory. This follows the
// platform cache convention (XDG cache dir on Linux, ~/Library/Caches on macOS,
// %LOCALAPPDATA% on Windows) and is overridable via PMG_CACHE_DIR. Caching
// layers (e.g. the analysis cache) should store regenerable data here rather
// than under the config directory.
func (r *RuntimeConfig) CacheDir() string {
return r.cacheDir
}
func (r *RuntimeConfig) IsProxyModeEnabled() bool {
return r.Config.Proxy.Enabled
}
@@ -396,6 +442,12 @@ func DefaultConfig() RuntimeConfig {
Enabled: true,
Days: 5,
},
AnalysisCache: AnalysisCacheConfig{
Malysis: MalysisCacheConfig{
Enabled: false,
TTL: 24 * time.Hour,
},
},
Cloud: CloudConfig{
Enabled: false,
AutoSync: CloudAutoSyncConfig{
@@ -472,6 +524,11 @@ func initConfig() {
panic(fmt.Errorf("failed to get sandbox overlay directory: %w", err))
}
cacheRootDir, err := cacheDir()
if err != nil {
panic(fmt.Errorf("failed to get cache directory: %w", err))
}
globalConfig.configDir = configDir
globalConfig.configFilePath = activeConfigPath
globalConfig.userConfigFilePath = userConfigPath
@@ -479,6 +536,7 @@ func initConfig() {
globalConfig.sandboxProfileDir = sandboxProfileDir
globalConfig.sandboxOverlayDir = sandboxOverlayDir
globalConfig.sandboxViolationCacheDir = sandboxViolationCacheDir
globalConfig.cacheDir = cacheRootDir
// A globally managed config enforces lockdown only when it opts in via
// global_lockdown, read straight from the file so it cannot be flipped by
+17
View File
@@ -182,6 +182,23 @@ dependency_cooldown:
# reason: "Pin a specific just-published build"
skip: []
# Persistent analysis cache (opt-in). Caching is analyzer-specific, so config is
# nested per analyzer; today only the Malysis (malware) analyzer has a cache.
#
# By default PMG re-screens the whole resolved graph against the analysis
# backend on every install. When enabled, clean (ALLOW) verdicts are cached on
# disk and reused across runs, so repeat installs of an unchanged graph are
# fast. Only ALLOW verdicts are cached — suspicious, malicious, and
# tenant-excluded verdicts are always re-evaluated.
#
# Trade-off: a version that was clean when first screened but is later flagged
# malicious is served from cache until its entry expires; `ttl` bounds that
# window. Keep `ttl` short if you prefer freshness over speed.
analysis_cache:
malysis:
enabled: false
ttl: 24h
# Cloud sync configuration.
# When enabled, PMG audit events are synced to SafeDep Cloud for centralized visibility.
# Requires SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables for authentication.
+66
View File
@@ -0,0 +1,66 @@
# Analysis Cache
PMG screens every package in the resolved dependency graph against the malware
analysis backend. By default this screening cache is **in-memory and per-run**:
it is empty at the start of each `install`, so every install re-screens the whole
graph — even when the package store is warm and nothing changed. For large or
frequently re-installed graphs this dominates wall-clock time.
The persistent analysis cache stores clean verdicts across runs and reuses them,
so a repeat install of an unchanged graph skips the per-package analysis
round-trip.
> **Status:** this page describes the analysis-cache contract and configuration.
> The caching layer is defined as the `analyzer.MalysisCache` interface so it can
> be backed by different stores; a concrete persistent implementation (sqlite)
> lands in a follow-up. Until then, `enabled` is inert.
## How It Works
- The cache is modeled as the `analyzer.MalysisCache` interface — a pluggable
contract the analyzer layer reads through. A concrete backend (e.g. sqlite) is
injected by the caller, so the abstraction is decoupled from any single store.
- Persistent verdicts live under the platform **cache** directory (`config.CacheDir()`
— XDG cache dir on Linux, `~/Library/Caches` on macOS, `%LOCALAPPDATA%` on
Windows; overridable via `PMG_CACHE_DIR`), not the config directory, since they
are regenerable.
- **Only clean (`ALLOW`) verdicts are cached.** Suspicious, malicious, and
tenant-excluded verdicts are never persisted and are always re-evaluated.
- Each entry expires after `ttl`.
## Configuration
Disabled by default. Caching is analyzer-specific, so it is configured per
analyzer under `analysis_cache`; today only the Malysis (malware) analyzer has a
cache. Enable it in `config.yml`:
```yaml
analysis_cache:
malysis:
enabled: true
ttl: 24h
```
- `enabled` — turn the persistent cache on/off.
- `ttl` — how long a cached verdict stays valid (Go duration, e.g. `30m`, `24h`,
`168h`). A non-positive `ttl` disables persistence (every lookup is a miss),
making the cache behave like the default in-memory one.
## Security Trade-off
Caching a verdict means trusting it for up to `ttl` without re-checking. A
package version that was clean when first screened but is **later flagged as
malicious** will be served from cache — and therefore allowed — until its entry
expires. `ttl` bounds that exposure window.
Because only `ALLOW` verdicts are cached, a package that is currently flagged is
never cached and is always re-evaluated. Choose `ttl` to balance install speed
against how quickly you want newly-published malware verdicts to take effect. If
in doubt, keep the cache disabled (the default) or use a short `ttl`.
## Requirements
The analysis cache applies to [proxy mode](proxy-mode.md). It is independent of
[dependency cooldown](dependency-cooldown.md): cooldown decides which *versions*
are eligible to install, while the analysis cache remembers malware verdicts for
versions that were already screened.