* refactor(flows): extract SetupCACertificate for reuse
Move the CA load/generate/merge logic out of proxyFlow into an exported
flows.SetupCACertificate so the persistent proxy server can reuse it.
* feat(proxy): add persistent proxy server with start/stop/env/status
Introduces 'pmg proxy' commands backed by internal/proxyserver: a long-lived
MITM proxy that intercepts package managers via env vars (no shims). Supports
--daemon (Unix), --state, --port; generic 'env' output that skips cert vars
when the CA is OS-trusted; opt-in 'stop --fail-on-violation' (fail-closed on
crash) with a synchronous cloud event flush; and the malysis analysis cache.
* feat(action): add server-mode for persistent proxy
When server-mode=true the action starts the proxy daemon and injects proxy
env vars into the job instead of installing shims.
* test(proxy): add persistent proxy server E2E workflow
* docs(readme): document persistent proxy server mode
* fix(proxy): create cache dir before writing state file and daemon log
On a fresh CI runner the cache directory does not exist yet; os.OpenFile and
os.WriteFile do not create parent dirs, so 'pmg proxy start --daemon' failed
with 'no such file or directory'. MkdirAll the parent before writing.
* docs: add persistent proxy server architecture doc
* refactor proxyserver
* fix(proxy): always emit cert env vars instead of skipping on OS-trust status
npm/pip/yarn/requests trust the MITM CA inconsistently across tools, versions,
and configs; many still use bundled CA stores. Always emitting the cert-path
env vars is the conservative choice that works regardless, and is harmless for
tools that read the OS store (they ignore the vars). Skipping them when a
system CA exists would silently break any tool still on a bundled store.
* refactor(proxy): drop redundant audit init in daemon; rely on main.go
main.go's PersistentPreRun already initializes the audit pipeline for every
command (including the daemon's re-exec'd child) and closes it at process exit.
Re-initializing in proxyserver.Run created a second auditor and a second
cloud-sync WAL connection, orphaning the first. Removing it makes the daemon
consistent with the normal proxy flow, which never self-initializes audit.
* fix(proxy): bypass proxy env when flushing events to cloud on stop
pmg proxy stop inherits HTTP(S)_PROXY (injected by 'pmg proxy env') pointing at
the PMG proxy it just shut down. The cloud sync gRPC client honored those vars
and routed api.safedep.io through the dead proxy, failing with 'connection
refused' so no events were delivered. Clear the proxy env vars before the sync
so PMG's own cloud traffic goes direct.
* chore(proxy): address review feedback
- configurable bind host via proxy.server.listen_host (default loopback)
- proxy commands use ui.ErrorExit instead of returning errors to cobra
- rename errcode to ProxyPolicyViolation (covers malware + cooldown)
- share cloud sync via audit.DrainToCloud (de-dup with cmd/cloud/sync)
- centralize proxy CA bundle path in certmanager
- docs: persistent proxy cert trust + bind address
* fix(proxy): show real message on fail-on-violation error
stopExitError set only WithMsg, but ui.ErrorExit renders HumanError, so the
framed error showed 'no human-readable message available'. Set both from one
string, and emit the framed error before the stdout summary so the blocked
count is stated once.
* fix(proxy): flush cloud events from the daemon, not stop
The stop process inherits HTTP_PROXY (from 'pmg proxy env'), so its cloud
client routed api.safedep.io through the already-stopped proxy and failed with
connection refused. Move the flush into the daemon's shutdown, which has no
proxy env (it started before env injection) and dials SafeDep directly.
- daemon flushes on shutdown via audit.DrainToCloud and records the result in
the state file; stop surfaces it (on both success and fail-on-violation
paths) since the daemon's own logs aren't visible to stop
- coordinate stop's wait with the daemon shutdown budget; on timeout, error
out without reading stale state or deleting the file (fail-closed)
- persist blocked count before the flush so the gate stays correct if the
flush hangs or the daemon is killed mid-flush
- remove now-redundant cloud_flush.go
* disable auto-sync for proxy cmds
* feat(proxy): periodic cloud sync + move proxy env vars to packagemanager
- daemon runs a periodic cloud-sync ticker so the shutdown flush stays small;
the run total is reported by stop, and shutdown timeouts are coordinated
- move EnvVarForProxy from config to packagemanager (it is package-manager
knowledge); the shared function now builds the proxy URL and NO_PROXY itself,
removing the duplicated construction in the per-command and persistent paths
- relocate the #319 yarn and #339 IPv6 regression tests alongside the function
- enable cloud sync in the persistent-proxy E2E workflow and fix the stale
internal/proxystate path filter
* refactor(proxy): rename cloudFlushLockTimeout to cloudFlushLockWait
Consistent timeout naming: *LockWait is the lock-acquire bound, *Timeout is
the sync-RPC bound. Previously the final-flush pair was cloudFlushLockTimeout
vs cloudFlushTimeout — two lookalike names for different operations.
* refactor(proxy): extract cloudFlush and trim duplicate shutdown comments
The shutdown's final-flush block is now a cloudFlush helper, symmetric with
startCloudSyncLoop (one-shot vs loop). Removed the triplicated ticker/lock
contention comments, keeping the contract on the function doc and one-line
pointers at the call sites.
* docs: update persistent proxy cloud sync to daemon-owned model
The daemon now owns cloud delivery (periodic sync while serving + final flush
on shutdown); stop signals it, waits, and reports the result. Rewrite the Cloud
event sync section, fix stop attributions, add the cloud_sync state field, and
update the sequence diagram.
* docs: move Usage section up below How it works
Put the copy-paste recipes near the top so users find them before the internals.
* refactor(proxy): address PR review feedback
- configurable bind host/port via --host/--port flags + config (listen_host,
listen_port), bound directly to config fields per PMG's flag pattern
- daemon log path via --log-file and readiness timeout in ProxyDaemonConfig;
Daemonize no longer owns path policy (caller validates, fails fast)
- gate periodic cloud sync on auto_sync; suppress detached background sync for
proxy commands instead of flipping the flag
- pmg proxy env --export emits shell-quoted lines for eval (spaces survive)
- extract shared flows.BuildCachedMalysisAnalyzer, dropping the analyzer+cache
duplication between proxy flow and proxy server
- add internal/proxyserver/doc.go documenting the package + boundary vs flows
- E2E: assert malicious installs are blocked (drop continue-on-error)
- docs: trim Commands/State-file to user contracts; refresh bind address
* refactor(proxy): proactive alignment fixes from whole-PR review
- gate the shutdown cloud flush on auto_sync too, matching the periodic ticker
(auto_sync consistently controls all daemon-driven cloud delivery)
- ResolveStatePath takes cacheDir instead of *RuntimeConfig, keeping state.go
free of config dependency
- drop the empty-host comment in listenAddr; keep the loopback guard so a blank
host never silently binds all interfaces
* fix: Decouple localdb with malysis analyser construction
* fix: Persist global args before proxy server daemon exec
* fix: GitHub Action for cloud auto-sync in server mode
---------
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
PMG was forcing Accept: application/vnd.pypi.simple.v1+json on all
Simple API requests regardless of client capability. Older pip versions
(< 22.3) that only understand text/html would reject the JSON response,
breaking installs entirely.
Now checks the client's original Accept header before applying cooldown.
If PEP 691 is not supported, the request passes through unchanged with
a warning log recommending pip upgrade.
* feat: add doctor check runner core types and logic
* feat: add doctor checks for config, binary, directory, and aliases
* feat: add doctor checks for sandbox and security features
* feat: add protection verification check using test malicious packages
* feat: add summarized package manager availability check
* feat: add pmg setup doctor command with compact output
* feat: add PATH shim verification to doctor command
* fix: improve doctor command UX and alias detection
- Capitalize all check messages for consistent output
- Dim passing checks, color warn/fail for visual clarity
- Silence empty Cobra error output on doctor failure
- Remove redundant pmg binary check (self-evident)
- Fix alias IsInstalled to skip commented-out source lines
- Improve protection failure message
* refactor: remove package manager availability check from doctor
* fix: handle os.RemoveAll error in doctor protection check
* refactor: use table layout for setup doctor, extract shared table renderer
Move renderTable, truncate, and visibleWidth helpers from cmd/sandbox
to internal/ui so both sandbox and setup doctor share them. Rewrite
setup doctor output to use the same table structure as sandbox doctor.
Fix VisibleWidth to count runes instead of bytes for correct alignment
with multi-byte UTF-8 characters.
* docs: add pmg setup doctor to README, remove manual verification step
* refactor: use constants for check names, rename and inline doctor helpers
Address PR review comments: extract check name constants, rename
CheckConfigFile to CheckFileExists and CheckDirectoryWritable to
CheckDirectoryExists for reusability, inline trivial wrappers
(CheckSandbox, CheckSecurityFeature, CheckProxyMode), and add
fix hints for all checks with correct config keys.
* refactor: inline simple doctor checks into command layer
* fix: skip protection check when aliases and shims are inactive
Protection checks now fail immediately when shell aliases and shims
are both inactive, instead of falsely passing by running through the
pmg binary directly. Also clean up summary messages to remove
redundant fix hints and truncated paths.
* feat: Add support for background sync
* refactor: Maintain single source of truth for command defn
* fix: Code review fixes
* fix: Code review fixes
* docs: Add corner case inline doc
- Remove top banner and add a tagline under the H1 so the value
proposition sits above the fold; move the demo GIF above the badges.
- Reframe "How PMG Works" as defense in depth with three explicit
layers (Threat Intelligence, Policy/Cooldown, Sandbox) and promote
Dependency Cooldown out of the Features table.
- De-duplicate install instructions between Quick Start and Installation,
and drop the all-"Yes" Status column from the supported package
managers table.
- Update GitHub Actions snippet to actions/setup-node@v6 and add a
comment recommending SHA pinning for third-party Actions.
- Add Mini Shai-Hulud (300+ npm packages compromised) to the recent
malicious-package examples.
- Tighten prose: remove filler adverbs, passive voice, em-dashes, and
marketing fluff; split semicolon-joined clauses into separate
sentences.
* feat: add GitHub Action for one-step PMG setup in CI
Composite action at repo root that downloads PMG (with SHA-256 verification
against the upstream checksums.txt), runs `pmg setup install`, and wires
shims onto $GITHUB_PATH so subsequent `npm install` / `pip install` calls
are transparently analyzed.
Defaults are conservative: malware blocking + dependency cooldown + proxy
mode (matching PMG's own defaults). Sandbox is opt-in because enabling
Landlock/Bubblewrap on ubuntu-latest requires relaxing AppArmor
user-namespace restrictions.
Cloud sync uses the documented SAFEDEP_API_KEY / SAFEDEP_TENANT_ID env-var
fallback so we skip the keychain codepath that has no usable backend in
headless CI. When cloud is enabled and no endpoint-id is supplied, the
action sets PMG_CLOUD_ENDPOINT_ID=github-actions/${GITHUB_REPOSITORY} so
events aggregate per repository instead of per ephemeral runner hostname.
Closes#248.
https://claude.ai/code/session_01ARb8ZiBiJjvhWjchBXraAh
* fix(action): drop github.repository template from input description
Action manifest validation rejected the action.yml because the endpoint-id
input description contained ${{ github.repository }} — template expressions
aren't evaluated in input description text and trip the validator with
"Unrecognized named-value: 'github'". This caused every job using uses: ./
to fail before any step ran.
Also switch the config-file e2e job to verify the staged file directly
instead of calling `pmg config get`, which is not in the v0.13.0 release
that "latest" resolves to today.
https://claude.ai/code/session_01ARb8ZiBiJjvhWjchBXraAh
* fix(action): address review comments on PR #263
- Drop opinionated defaults on PMG_* toggle inputs. All defaults are now
empty strings, and the action only exports PMG_* env vars when the
caller explicitly sets the input. Without this, defaults like
PMG_PARANOID=false silently shadowed config-file overrides because env
vars beat config.yml in Viper precedence.
- Verify cached PMG against upstream checksums.txt on every cache hit.
The cached tarball is stored alongside the binary and re-hashed against
the freshly-fetched checksums.txt; on drift, the cache entry is evicted
and re-downloaded.
- Export PMG_* env vars BEFORE running `pmg setup install` so settings
like disable-telemetry actually apply during setup, not just to
subsequent package-manager calls.
- Add `|| true` to the grep that extracts the expected checksum so
set -e doesn't kill the script before the friendly error message fires
when no checksum entry is found.
- Pin third-party actions (actions/checkout, actions/setup-node) to
commit SHAs to match the repo's supply-chain hardening convention.
- Fix the malicious-package E2E test capturing tee's exit code instead
of npm's; redirect to a file and check the actual command exit code.
- Add an E2E job that asserts PMG_PARANOID is unset when only
config-file is provided — regression guard for the precedence fix.
https://claude.ai/code/session_01ARb8ZiBiJjvhWjchBXraAh
* ci(action-e2e): scope sandbox tests to action setup, not PMG runtime
The landlock job was running `npm install express` with no explicit
sandbox profile and the default profile blocks something npm needs
(PMG's own e2e uses `--sandbox-profile npm-restrictive` to make this
viable). Bubblewrap happened to pass, but verifying the default sandbox
profile is permissive enough for arbitrary package installs is PMG's
e2e responsibility — this workflow's job is to assert the action wires
sandbox config correctly.
Switch both drivers to a matrix and verify only what the action owns:
PMG_SANDBOX_* env vars propagated, pmg binary runs, bwrap is installed
when requested, AppArmor user-ns restriction relaxed.
https://claude.ai/code/session_01ARb8ZiBiJjvhWjchBXraAh
* ci(action-e2e): bump setup-node to 24
Node 20 reached end-of-life and setup-node now warns on it. Match the
version pinned by publish-npm.yml (the repo's newest workflow). Updated
the README and docs/github-action.md quick-start examples to match.
https://claude.ai/code/session_01ARb8ZiBiJjvhWjchBXraAh
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
* update README.md
* ci: Remove paths-ignore filters from all workflows
Removes docs/ and *.md path exclusions so required checks always run,
preventing PRs from being blocked when only documentation files change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: Add config merging design spec for #114
Defines the merge-during-setup-install approach for keeping user
configs up to date with new template keys while preserving all
existing values, comments, and formatting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: Add implementation plan for config merging
TDD-based plan with 6 tasks: dependency setup, failing tests,
core merge implementation, integration test, WriteTemplateConfig
integration, and full verification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Merge template config into existing user config during setup install
Instead of skipping when a config file exists, WriteTemplateConfig() now
merges missing keys from the embedded template into the user's config
using YAML AST manipulation. Preserves all user values, comments, and
formatting. Only adds keys present in the template but absent in the
user's config.
Closes#114
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: Graceful error handling for config loading and setup commands
Replace panics in loadViperConfig with error returns so the app falls
back to defaults instead of crashing on malformed config files. Add
SilenceUsage to setup install/remove commands so runtime errors don't
dump the full usage text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add test cmd in readme
* update copy text
* refactor: Address review feedback on config merging
- Rename existing/template to dest/source for generic util naming
- Remove unnecessary code comments (Rule N references, obvious comments)
- Add AGENTS.md with dev guide and code style rules, symlink CLAUDE.md
- Add BenchmarkMergeYAML (~46μs/op on M4 Pro)
- Remove stale design spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* update `MergeYAML` to use from dry/utils
* update AGENTS.md
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Add support for bubblewrap sandbox
* fix: Glob pattern expansion limit for linux
* fix: Bug in glob pattern expansion for bwrap
* fix: README on trust
* fix: Multiple bubblewrap translator fix
* test: Add E2E for linux sandbox
* fix: Refactor bwrap sandbox to use common dangerous files
* fix: Path test case
* fix: Non-existent path handling bug
* refactor: Misc cleanup
* fix: Avoid bind mount for non-existentent deny protection
* fix: Off by one bug in path depth handling
* ci: Disable AppArmor on GHA runner
* fix: Disable apparmor userns restrictions
* define contract for package executors
* introduce npx executor
* add npx and pnpx cmd support
* fix typo
* rm PackageExecutor and depend on PackageManager interface
* add support for PTY to handle parent-child process interaction
* refactor PTY handling in proxy flow
* enforce interactiveSession interface check
* close reader explicitly and clean npm version for pkg executors
* rm interaction from interceptors
* add docs and wait for outputRouter before exit
* add support for non interactive TTY for proxy mode
* add support for CI env var check for non interactive tty proxy mode
* update readme to include npx, pnpx support
* Update internal/flows/proxy_flow.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
* update ptyx lib
* fix docs typo
---------
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* feat: Add experimental proxy based npm interceptor
* refactor: Analysis cache
* ci: Add E2E for npm proxy
* fix: Handle dry-run in proxy flow
* fix: Handle special case for scope package name
* fix: Misc fixes
* fix: Code review fixes
* fix: Code review fixes
* refactor: Reusable code into base registry interceptor
* Pause npm process during user confirmation (#90)
* pause npm process when prompting user for confirmation
* disable progress bar
* fix logging and close chan on return
* update use of deprecated field
* refactor: Separation of concerns for handling process state
* fix: Safe permission for cert file
* fix: Handle nil check for interaction hook
* fix: Add test for base registry
* Fix goreleaser for windows build (#93)
* introduce platform specific process control
* rename common.go to common_flow.go
* feat: Add support for pause resume on windows
* fix: Code review fixes
* test: Add confirmation handler tests
---------
Co-authored-by: Sahil Bansal <bansalsahil315@gmail.com>
* Add support for yarn package manager
- Added yarn command handler in cmd/npm/yarn.go
- Added DefaultYarnPackageManagerConfig() in packagemanager/npm.go
- Added yarn dev flag support (--dev/-D) in ParseCommand()
- Added yarn analytics tracking in internal/analytics/event.go
- Registered yarn command in main.go
- Added yarn to alias setup in internal/alias/alias.go
- Added comprehensive test suite in packagemanager/yarn_test.go
- All tests passing (12 yarn tests + existing tests)
Resolves#30
Signed-off-by: shrvansudhakara <shrvansudhakara@gmail.com>
* docs: update README with yarn package manager support
Updated documentation to reflect yarn support addition:
- Changed yarn status from '🚧 Planned' to '✅ Active' in package managers table
- Added 'pmg yarn add <package>' command example
- Added 'yarn add <package-name>' to command list
- Added 'yarn install # Uses yarn.lock' to lockfile examples
This documentation update corresponds to the yarn implementation in the previous commit.
Related to #30
Signed-off-by: shrvansudhakara <shrvansudhakara@gmail.com>
* fix: address review feedback for yarn support
- Add support for bare 'yarn' command as manifest install
- Validate yarn install does not accept package names
- Add YarnExtractor for yarn.lock file support
Changes:
- packagemanager/npm.go: Added bare yarn command handling and validation
- packagemanager/yarn_test.go: Added test for bare yarn command
- extractor/ecosystems.go: Added Yarn PackageManagerName and extractor
- extractor/npm.go: Added YarnExtractor implementation
- go.mod, go.sum: Updated dependencies
Addresses review feedback from @Sahilb315
- Bare yarn command now triggers manifest install per docs
- yarn install <package> is now properly rejected
- yarn.lock extraction support added
Related to #30
Signed-off-by: shrvansudhakara <shrvansudhakara@gmail.com>
* chore: tidy go.mod and go.sum to remove unused dependencies
Signed-off-by: shrvansudhakara <shrvansudhakara@gmail.com>
* fix: add yarn.lock extractor mapping
- Add yarn.lock case in getExtractorForFile switch
- Import yarnlock extractor from osv-scalibr
- Enables lockfile parsing for manifest-based installations
Tested both package-based and manifest-based installations successfully.
Addresses review feedback from @Sahilb315
Signed-off-by: shrvansudhakara <shrvansudhakara@gmail.com>
* test: add validation test for yarn install with package name
- Add test case to verify 'yarn install <package>' is rejected
- Ensures invalid syntax returns no install targets
- Confirms IsManifestInstall is false for invalid commands
Addresses review feedback from @Sahilb315
Signed-off-by: shrvansudhakara <shrvansudhakara@gmail.com>
---------
Signed-off-by: shrvansudhakara <shrvansudhakara@gmail.com>
* follow proper consistent naming in pypi packagemanager
* follow proper consistent naming in pypi packagemanager - 2
* feat: add specialized command parsers to handle pip and uv command formats
* add uv support & modify extractor to be more robust
* add uv alias
* refactor var name & add error handling
* update readme & add support for `uv pip sync` cmd