Files
pmg/config/config.template.yml
T
365deb1897 feat: Add proxy_install_only config to restrict proxy to download commands (#222)
* feat: Add proxy_install_only config to restrict proxy to download commands

Introduces proxy_install_only (default: false) which, when enabled,
skips the proxy for package manager commands that do not download
packages (e.g. npm ls, pip list), avoiding unnecessary MITM overhead.

- Add ProxyInstallOnly to Config and config template
- Add IsKnownDownloadCommand / MayDownloadPackages to ParsedCommand
- Add DownloadCommands to npm and pypi PM configs covering update,
  ci, audit, dlx, exec, x, download, run and equivalents per PM
- Extract shared runner.Execute used by both proxy flow and guard
- Proxy flow short-circuits to runner.Execute for non-download commands
  when proxy_install_only=true

* refactor: Inject CommandExecutor into guard to fix dependency direction

guard depended on internal/runner, which inverted the intended layer
hierarchy. Now guard defines a CommandExecutor function type and accepts
it as a constructor argument. internal/flows (the composition root)
creates the executor closure wrapping runner.Execute and injects it,
keeping guard free of internal/ dependencies.

* refactor: Invert proxy_install_only logic to use known non-download commands

Replace the DownloadCommands allowlist (opt-in, fail-open) with a
NonDownloadCommands denylist (opt-out, fail-safe). The proxy now runs
for all commands except those explicitly known to not download packages.
Unknown or future package manager subcommands default to running with
the proxy.

Includes script runners (run, start, test, stop, restart) that can spin
up local servers — setting proxy env vars on these breaks them without
providing any security benefit. Also covers removal commands and local
operations that never contact the registry.

* fix: Support PMG_* env vars regardless of config file state

AutomaticEnv only resolves env vars for keys Viper already knows about
via AllKeys(). When a key is absent from the config file (commented out,
new key added after last setup, or no config file at all), Viper had no
knowledge of it and silently skipped the env var.

Fix by registering all Config struct fields as Viper defaults via
reflection (using mapstructure tags) before reading the config file.
This ensures PMG_* env vars work in all cases.

Precedence: cobra flags > env vars > config file > defaults.
SetDefault is used (not Set) so env vars and config file can still
override the Go defaults freely.

Tests added covering all precedence levels including the key-absent-
from-config-file case that was the original bug report.

* fix: Only check first non-flag arg against NonDownloadCommands

Scanning all args caused false proxy bypasses when package names or
script arguments matched a NonDownloadCommands entry. For example:
- npm exec test → "test" matched, proxy incorrectly skipped
- npm update config → "config" matched, proxy skipped
- npm publish --tag version → "version" matched, proxy skipped

Fix by checking only the first non-flag argument (the subcommand).
If it is not in NonDownloadCommands we break immediately, so trailing
args never influence the classification. Applied to all four parsers:
npm, pip/pip3, uv, and poetry.

Regression tests added for the false positive cases.

* refactor: Replace reflection-based Viper defaults with embedded template

Load the embedded config template as the Viper base so all keys are
registered upfront, enabling PMG_* env vars to work regardless of
whether a key exists in the user's config file.

* fix: Restore trusted_packages template entry and revert DefaultConfig change

* docs: Document environment variable overrides for config keys

* update npm test cmd

* refactor: extract shared non-download command detection helper

Replaces duplicated first-non-flag-arg detection loops in npm.go and
pypi.go (pip + poetry parsers) with a shared isFirstNonFlagArgInList
helper in packagemanager.go.

https://claude.ai/code/session_01AHaKF3vc2Haj9tK3jgUBAs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 01:13:30 +05:30

161 lines
6.3 KiB
YAML

# PMG configuration template. Customize this file as needed.
# https://github.com/safedep/pmg
# Enable transitive dependency resolution. Default is true.
transitive: true
# Maximum depth of transitive dependencies to resolve. Default is 5.
transitive_depth: 5
# Include dev dependencies in the dependency graph. Default is false.
include_dev_dependencies: false
# UI verbosity level. Valid values: silent, normal, verbose. Default is normal.
# silent: PMG is hidden from the user except for errors and malicious package detection
# normal: Show minimal status updates
# verbose: Show verbose status updates and detailed information
verbosity: normal
# Enable paranoid mode. In paranoid mode, PMG will treat suspicious packages
# as malicious packages
paranoid: false
# Skip event logging. Default is false.
# When skip_event_logging is false, all events will be logged to file. These events are useful for audit
# trail and incident response on systems using PMG. Set this config to true to disable event logging.
skip_event_logging: false
# Event log retention days. Default is 7.
# This is the number of days to retain event logs.
event_log_retention_days: 7
# Proxy mode. Default is true.
# When enabled, PMG uses a proxy-based interception approach instead of the
# default guard-based analysis. The proxy intercepts package manager requests in real-time
# and analyzes packages as they are downloaded. Proxy mode may not work in all environments,
# and can be disabled to fall back to the guard-based analysis.
proxy_mode: true
# Restrict proxy to install commands only. Default is false.
# When false, the proxy intercepts all package manager commands (e.g., npm install, npm ls).
# When true, non-install commands bypass the proxy and execute directly, which can improve
# performance for commands that don't download packages (e.g., npm ls, pip list, npm outdated).
proxy_install_only: false
# Trusted packages are packages that are trusted by the user and will be ignored by the security guardrails.
# This is useful for packages that are known to be safe and are used in the application.
# Example:
# - purl: pkg:npm/@safedep/pmg
# reason: "PMG is a trusted package for PMG"
#
# When a package is specified without an explicit version, all versions of the package will be trusted.
# This feature should be used with caution and should be used for minimal set of packages.
#
# When a package is specified with an explicit version, only that version will be trusted.
#
# The purl is the package identifier and the reason is the reason for trusting the package.
# PURL specification: https://github.com/package-url/purl-spec
trusted_packages:
- purl: pkg:npm/@safedep/pmg
reason: "PMG is a trusted package for PMG"
# Sandbox configuration (EXPERIMENTAL)
# When enabled, package managers run in sandbox environments with restricted
# filesystem, network, and process execution access. This provides defense-in-depth
# protection against malicious install scripts and supply chain attacks.
#
# When sandboxing is enabled for a package manager, policy violations will block execution
# (no "warn-only" mode). Sandboxing itself can be disabled globally or per package manager.
#
# Currently supported platforms:
# - macOS (using Seatbelt sandbox-exec)
# - Linux (using Bubblewrap with namespace isolation)
# - Windows (planned)
#
# Platform-specific limitations:
# - Linux: Filesystem permissions use coarse-grained bind mounts. Glob patterns (e.g., *.txt)
# are expanded at policy translation time, but entire directories may be mounted rather than
# individual matching files. This is less precise than macOS regex-based filtering.
# - macOS: Network filtering is limited (all-or-nothing for most policies).
sandbox:
# Enable sandbox mode (opt-in, default: false for backward compatibility)
enabled: false
# Controls scope of sandbox enforcement:
# - true: sandbox applies to all package manager commands
# - false (default): sandbox only applies to install commands, others run unrestricted
# Requires 'enabled: true' and per-PM policies to be active. May break workflows expecting unrestricted commands.
enforce_always: false
# Policy templates define policy profiles by name and path.
# They can be used to override a built-in profile or create a custom profile.
# Note: Custom profiles loaded via policy_templates can inherit from built-in
# profiles using the 'inherits' field in the YAML (e.g., inherits: npm-restrictive).
# Inheritance allows you to extend a base profile with additional permissions.
policy_templates:
# Name for the template. Can be used to override a built-in profile or create a custom profile.
# Path is the path to the template file.
# Relative path can be used to reference a template file in the config directory (example: ./npm-restrictive.yml)
npm-restrictive-override:
path: ./profiles/npm-restrictive.yml
# Per-package-manager sandbox policies
# Each package manager can have its own policy to account for unique security characteristics
policies:
# npm ecosystem. npm-restrictive is a built-in profile.
npm:
enabled: true
profile: npm-restrictive # Built-in profile, template name, or path to custom YAML
pnpm:
enabled: true
profile: pnpm-restrictive
npx:
enabled: true
profile: npx
pnpx:
enabled: true
profile: npx
yarn:
enabled: true
profile: npm-restrictive
bun:
enabled: true
profile: npm-restrictive
# PyPI ecosystem. pypi-restrictive is a built-in profile.
pip:
enabled: true
profile: pypi-restrictive
pip3:
enabled: true
profile: pypi-restrictive
poetry:
enabled: true
profile: pypi-restrictive
uv:
enabled: true
profile: pypi-restrictive
# Dependency cooldown blocks installation of package versions published within
# a configurable time window.
dependency_cooldown:
enabled: true
days: 5
# 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.
cloud:
enabled: false
# Endpoint ID is not required. By default, it falls back to the machine's hostname.
# Set it only if you want to explicitly override the identifier for this endpoint.
# endpoint_id: "my-machine"