mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
0deb8fa4ff07185bec05eab4d2ecffdb12b273e4
269
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0deb8fa4ff |
docs: container traffic reaches the proxy via the bridge address
Measured. Binding the proxy to the Docker bridge instead of loopback is enough to make container traffic reach it, and needs no code change in either pmg or the agent. The agent reads the address from the proxy state file, so the redirect target follows. 127.0.0.1 container connection failed, never reached the proxy 172.17.0.1 curl exit 60, reached the proxy and refused the certificate Exit 60 is a certificate error, so TCP completed and TLS began. Host traffic was unaffected in the same run. CA trust inside the container stays unsolved and is probably unsolvable from the host, since a container has its own trust store and injecting into it requires whoever starts the container to mount it. For containers the guarantee therefore degrades to fail closed. Only the default bridge is covered. Compose and custom networks use other gateways, which needs bpf_get_netns_cookie to select a target per namespace. A non loopback bind also exposes the proxy, so it wants a firewall rule outside an isolated runner. SETUP.md gains an optional Docker section with the commands and the expected exit code, and Current Limits is corrected. |
||
|
|
20ea125002 |
docs(ebpf-poc): fix setup guide errors found by walking it
Went through every step against a clean machine. Six problems: Step 4 still claimed the setup command uses npm to write certificate config. That stopped being true when npm handling was removed. npm is now needed only for the test. Step 5 did not say where to clone, while later steps assume ~/pmg. Step 7 told the reader to verify the build after the command block had already copied the binary. The block now builds, verifies, then installs. Step 9 pre-created the proxy state directory. The proxy creates it itself through os.MkdirAll, at 0700 rather than the 0755 the manual command produced. Steps 13 and 14 showed hardcoded uids 999 and 1001. They are assigned by the system and differed on the next machine, which read as a mismatch. Step 15 was wrong in a way that hid a real gap. On a dual stack machine curl prefers IPv6, the hook is connect4 only, so the connection bypassed enforcement entirely and returned the real package. The step now uses curl -4 and explains why. Current Limits records the measurement. |
||
|
|
9b29a95e03 |
feat(ebpf-poc): trust the CA machine wide instead of configuring npm
v1 owns one trust mechanism: the system trust store. Every user and every program that reads it gets the proxy CA with no configuration. Programs that carry their own certificate list are documented rather than managed, because each keeps that list in a different place and format. ca install now calls truststore.Install with system scope and verifies the result. ca remove untrusts before deleting, since a CA left trusted after its files are gone is worse than one never installed: nothing points at it, but whoever holds the old key can still intercept every user. Only the public certificate reaches the trust store. The private key stays 0600 under the proxy user, which is the entire security boundary here. Deletes npm_ca.go and the npm half of ca.go: the per user cafile handling, the previous value save and restore, the read back verification, and the --npm-user and --npm-bin flags. Verified on Ubuntu with no per tool configuration anywhere: curl without --cacert, npm without cafile, and a malicious package still blocked with 403. Removal confirmed to leave zero matches in ca-certificates.crt. |
||
|
|
e9ce5a27f2 |
docs(ebpf-poc): require build-essential in setup
A fresh Ubuntu machine can have gcc without the C library headers. PMG builds runtime/cgo, so the build fails with 'fatal error: errno.h: No such file or directory'. Also adds a check that the build succeeded before installing the binary, since a failed build leaves the previous file in place. |
||
|
|
472a3e859a |
docs(ebpf-poc): setup guide and CA trust findings
SETUP.md walks a fresh Linux machine through the POC end to end, in short numbered steps. The design doc gains two measured sections. CA Trust records which clients honour the system trust store and which ship their own roots, along with the config file settings that work without environment variables. Container Reachability records that the hook does reach into containers but the redirect target does not, because loopback is network namespaced. |
||
|
|
d987512ccf |
feat(ebpf-poc): manage the proxy CA and npm trust
Redirecting a package manager into the proxy is only half the job. The client then has to trust the proxy's certificate, and npm will not, because Node from nodejs.org compiles its own roots into the binary and ignores the system trust store. Adds `pmgwatch ca` with install, status and remove. install persists a CA keypair through PMG's own certmanager, so the certificate survives a proxy restart. Without a persisted CA the daemon generates an ephemeral one on every start and trust has to be redone each time. The private key is left owned by the proxy user at 0600, and a separate public bundle is written for npm, so npm never needs access to the key. npm is configured through `npm config set cafile --location=user`, run as the target user. Environment variables that could shadow that config are stripped from the child so the read-back verification is meaningful. The previous cafile is recorded and restored on remove. install refuses to run while the proxy is up, since the daemon reads the certificate at startup, and refuses to change options against existing state so a half-migrated setup cannot happen. |
||
|
|
60f2565145 | feat(ebpf-poc): derive proxy target and exemption from state | ||
|
|
93b8abf390 | docs(design): record the interception implementation steps | ||
|
|
a4b99e40e7 |
feat(ebpf-poc): redirect eligible connections to the proxy
Turns the recorded ACTION_REDIRECT decision into an actual rewrite of user_ip4 and user_port, so a connection that would have reached the registry directly lands on the configured target instead. The target address is copied out of the map as is. It is already held in network byte order, the same layout as ctx->user_ip4, so converting it again silently corrupts the destination: 127.0.0.1 becomes 1.0.0.127, which is routable, never answers, and shows up as a two minute hang rather than an error. Only the port is converted, since it is kept in host order for the userspace side. Verified against a dummy listener. curl to an external HTTPS host is logged as REDIRECT against its original destination, and the TLS ClientHello arrives on the local target. |
||
|
|
05a484ea5e | implement decision logic | ||
|
|
70e2d34a48 |
docs(design): Linux network enforcement via eBPF
Design notes for making package registry traffic on Linux unable to avoid the PMG proxy, scoped to CI runners and Linux servers. Covers the cgroup connect hook and why the socket layer was chosen, why redirect is preferred over deny, the proxy self traffic exemption and cgroup escape analysis, when the destination name becomes knowable and why SNI is the authoritative source, and findings from the network visibility POC. linux-network-enforcement.md is a cleaner rewrite of the same material. Both are kept for now. |
||
|
|
82ddbc70af |
feat(proxy): accept transparent redirected connections
The proxy only accepted explicit clients, which announce their destination with a CONNECT request. A client whose connection is redirected at the kernel level speaks TLS immediately instead, so http.Server tries to parse a TLS record as an HTTP request line and drops the connection. Demultiplex on the first byte of an accepted connection. A TLS handshake record (0x16) cannot begin an HTTP method, so it separates the two cleanly. Redirected connections have their destination recovered from the ClientHello SNI and are served by synthesising the CONNECT the client never sent, which keeps the MITM decision, cert generation and interceptor chain on the existing code path. Redirected connections deliberately bypass http.Server. It issues a background read while a handler runs, which consumes the first byte of the replayed ClientHello and corrupts the handshake. The CONNECT response is also suppressed, since a client mid handshake expects a ServerHello and would read those bytes as a malformed TLS record. Off by default. Enabled with `pmg proxy start --transparent` or proxy.server.transparent, and only useful alongside a redirect mechanism such as an eBPF connect rewrite. |
||
|
|
b8e12c27ae | fix main.go | ||
|
|
da6e10d2ad | add protocol field | ||
|
|
3e1ece3257 | visibility setup with ebpf | ||
|
|
94781d6bda |
Remove guard mode: proxy interception is now the only flow (#386)
* refactor: remove guard mode execution paths and guard-only packages Guard (non-proxy) mode is removed; all package-manager commands now always run the proxy flow. Removes the guard engine, the common flow, the extractor package, the npm/pypi dependency resolvers and the PackageResolver plumbing that only guard mode consumed. The guard package retains only PackageManagerGuardInteraction, which the proxy flow and confirmation interceptors reuse for user prompts. Proxy behavior is unchanged. * refactor: remove proxy opt-out surfaces, guard references in config, action and docs Removes Config.ProxyMode, ProxyConfig.Enabled, IsProxyModeEnabled, the proxy_mode legacy fallback, PMG_PROXY_ENABLED handling and the --proxy-mode / --include-dev-dependencies flags. Proxy interception can no longer be disabled. Also removes the proxy-mode input from the GitHub Action, the proxy-mode doctor check and setup info row, updates the E2E workflow to stop passing --proxy-mode=false, and sweeps guard-mode wording from docs and the config template. The legacy proxy_install_only flat key and PMG_PROXY_INSTALL_ONLY env var remain supported. audit.FlowTypeGuard is kept so previously recorded audit events still translate for cloud sync. * feat: fail loudly when a removed proxy opt-out is still configured A leftover proxy.enabled: false / proxy_mode: false config key or PMG_PROXY_ENABLED=false / PMG_PROXY_MODE=false env var previously meant guard mode; silently ignoring it would switch those users to proxy interception without notice. PMG now exits with an actionable error naming the exact source. Precedence mirrors the old resolution order: env (ignored under lockdown) > proxy.enabled > legacy proxy_mode. The pmg config subtree is exempt so the config file can still be fixed with pmg config edit/set. The GitHub Action's proxy-mode input is kept as a tombstone that fails the action when set to false and warns otherwise. * refactor: extract flows.RunProxy and address review findings Collapses the identical parse-then-run body duplicated across the 12 package manager commands into flows.RunProxy. Documents the cache-hit / offline analysis trade-off versus the removed guard manifest path, fixes a stale non-proxy label in the E2E workflow and a stale guard reference in the uvx parser comment. * fix(config): mirror old proxy opt-out precedence exactly PMG_PROXY_MODE only ever took effect through the legacy fallback, which was gated on the presence of a proxy: key in the config file (even a null one). Promoting it to the top env tier caused two inversions: a stale PMG_PROXY_MODE=false hard-failed configs that resolved to proxy mode, and PMG_PROXY_MODE=true silently overrode an explicit proxy.enabled: false file opt-out. The check now resolves in the old order: PMG_PROXY_ENABLED > proxy: section (presence gates the legacy tier) > PMG_PROXY_MODE > flat proxy_mode. parseOptOutBool also accepts numeric values (0 = false) to match viper's WeaklyTypedInput/cast.ToBool coercion, so proxy.enabled: 0 and proxy_mode: 0 are detected as opt-outs. * refactor: move package manager interaction out of guard * refactor: trim package manager interaction * fix(config): normalize config keys viper-style in proxy opt-out check Viper resolved config file keys case-insensitively and expanded dotted keys, so spellings like Proxy:, Enabled:, a literal proxy.enabled key or Proxy_Mode selected guard mode before the removal. The opt-out check now lowercases keys recursively and nests dotted keys before matching, so those existing opt-outs fail loudly instead of being silently ignored. * refactor: remove inert transitive controls, dead parser state and guard audit variant transitive / transitive_depth lost their only consumers with the dependency resolvers; remove the config fields, flags, template and doc entries, and the report/audit plumbing that misreported transitive analysis as enabled. Remove write-only parser state (PackageInstallTarget.Extras, ParsedCommand.ManifestFiles, ShouldExtractFromManifest); IsManifestInstall stays as it feeds sandbox gating via IsInstallationCommand. Remove audit.FlowTypeGuard and its cloud mapping; guard events recorded by pre-removal versions in an unsynced WAL translate to UNSPECIFIED. * fix: address review findings on the opt-out wiring and cleanups Move the removed-opt-out rejection from the CLI PersistentPreRun into proxyFlow.Run: the check now fires exactly for package-manager runs, so non-install commands (pmg setup remove, doctor, config, version) stay usable to fix or remove an opted-out installation, and future commands inherit or avoid the check by construction instead of by exemption list. Also: make the e2e malicious-package assertion actually fail the job when an install is not blocked, route pmg go through flows.RunProxy, and drop the dead extras return from pypiParsePackageInfo (extras are still stripped from package names). * fix(config): make the removed opt-out check faithful to the old resolution The gate that silenced the legacy proxy_mode surfaces matched the raw proxy key case-sensitively in the old code, while values resolved viper-style (case-insensitive, dotted keys); applying each semantic where the old code did fixes both divergences: a case-variant Proxy: section no longer hides a flat proxy_mode: false opt-out, and a dotted proxy.enabled: false overridden by proxy_mode: true no longer errors. Replace the generic key-tree normalization with two targeted lookups (the check only ever resolves proxy.enabled and proxy_mode), which also makes colliding spellings resolve deterministically. Coerce legacy-tier values cast.ToBool-style so PMG_PROXY_MODE=off style opt-outs are detected, log the config read error instead of swallowing it, and shorten the error to a one-line statement with the specific remedy in the help text. Add lockdown coverage (env inert both directions) and a repeated-run determinism test. * fix(config): fall back to defaults for unrecognized proxy opt-out values The old loader swallowed viper errors and ran on defaults, so values like proxy.enabled: yes or PMG_PROXY_ENABLED=banana silently discarded the whole config and defaulted to proxy. Treat them the same way now: unrecognized values mean the default (proxy on) instead of a hard error, and the doc comment no longer claims the old loader failed loudly. Only values that actually meant guard mode fail. Also check the removed opt-out before the CA trust check in pmg go, restoring the old error precedence: a config problem must not steer the user into an unnecessary OS trust store change. * fix(e2e): PMG_PROXY_MODE assertion must match the legacy gate semantics The runner's setup step writes the template config, which has a proxy: section — and with one present the legacy PMG_PROXY_MODE was always inert, so expecting a loud failure there asserts pre-fidelity-fix behavior. Assert both sides instead: inert (command succeeds) with the standard config, loud failure against an empty config dir where the legacy fallback actually applied. * refactor(config): collapse parseOptOutBool to ParseBool over the string form YAML hands us typed values (bool, int), so route them through fmt.Sprintf %v and strconv.ParseBool instead of a per-type switch. Identical behavior for every recognized value; numbers other than 0/1 now read as no opinion instead of cast.ToBool's nonzero-true, which no real config relies on. |
||
|
|
3031665ac7 |
fix(cloud): actionable errors when sync credentials are missing or rejected (#388)
`pmg cloud sync` without usable credentials surfaced misleading errors:
missing credentials fell into the generic network fallback ("check your
network connectivity") and server-rejected credentials (gateway 403)
showed a bare "Permission denied" with no remediation.
Map dry's cloud.ErrMissingCredentials to a new CloudCredentialsNotFound
code pointing at 'pmg cloud login' and the SAFEDEP_API_KEY /
SAFEDEP_TENANT_ID environment variables, and rebrand authentication and
authorization failures with the same credential guidance. Entitlement,
quota and server errors still pass through unchanged.
|
||
|
|
ee684a29a9 |
feat(sandbox): presets — additive workload allowance bundles (#387)
* feat(sandbox): introduce presets - additive workload allowance bundles Presets are named, additive-only bundles of sandbox allowances for a specific workload (git hooks tooling, Astro/Vite/Next.js dev servers). They solve the per-workload tuning friction from #384 without weakening the default posture: no built-in profile references a preset, presets cannot carry deny rules or profile booleans (strict YAML decoding), and mandatory denies still win everywhere except the existing exact-match suppression. - Preset schema with metadata (author, labels) and schema_version gating - Registry over ordered sources (embedded builtin, user dir); builtin wins name collisions; source abstraction is the extension point for a future hosted registry and SafeDep cloud sync - Official presets: git, astro, vite, nextjs (with threat notes) - Overlay/runtime integration: pmg sandbox allow preset=<name> and --sandbox-allow preset=<name>, stored by reference, resolved at apply time, missing presets warn (fail closed) instead of aborting - Profile integration: presets: [...] list resolved after inherits - CLI: pmg sandbox preset list (metadata filters, --json), show (prints YAML with threat notes), lint - Docs: user guide (docs/sandbox-presets.md) and design spec Closes #384 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * fix(sandbox): address review findings on presets - Presets never modify deny lists: a profile authored deny now survives a preset allowing the same path (deny-beats-allow keeps it enforced). Regression test added. - Profile inspection commands (show, diff, lint) construct the profile registry with the user-aware preset registry so they agree with runtime resolution of custom profiles referencing user presets. - Handle stderr write error when warning about unresolvable presets. - Compute preset show underline from the uncolored header. - Use path.Join for embed.FS reads (slash-separated on all platforms). - Clarify in docs that lint-staged/astro are examples of preset workloads. - Drop the design spec from the PR per review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * fix(sandbox): harden preset precedence against authored denies Addresses external security review findings on the preset mechanism: - Bubblewrap: a mandatory write-denied path listed in allow_read lost its protection when a later writable parent bind covered it (bwrap last mount wins) - exactly the git preset shape (allow_read .git/config + allow_write .git/**). The mandatory deny now re-binds the path read-only after all writable mounts instead of being skipped. Regression test asserts mount ordering. Landlock and Seatbelt were unaffected (tests added for the same policy shape on Landlock). - Environment: ScrubEnv is allow-wins, so a preset environment allowance could override a profile-authored deny. Preset env allowances overlapping an authored deny pattern are now dropped at application time (conservative bidirectional glob overlap, fail closed). Surviving entries still opt out of built-in credential scrubbing as intended. - Network: removed allow_outbound from the preset schema. Both platform translators are all-or-nothing for outbound (one allow rule means blanket network access), so a preset outbound entry would silently change network posture far beyond what its YAML conveys. Strict decoding rejects the key. - Added a dual-path expansion equivalence test (profile presets: field vs overlay/--sandbox-allow) and documented the precedence guarantees in docs/sandbox-presets.md. Explicit --sandbox-allow and pmg sandbox allow overrides keep their existing semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * docs(sandbox): document env and preset allowances in allow command and overlay docs pmg sandbox allow help, the --sandbox-allow flag usage, and the project overlay docs enumerated only read/write/exec/net types. Add env and preset to all of them, with an overlay example for persisting an env allowance and a note on why env entries are not auto-promoted by --last. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * chore(sandbox): trim preset code comments to corner cases and minimal godocs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * fix(sandbox): exact glob intersection for preset env deny overlap The bidirectional literal-text heuristic missed overlapping globs with different literal structure: preset allow AWS_*_KEY and authored deny AWS_SECRET_* both match AWS_SECRET_ACCESS_KEY but neither pattern matches the other's text, so the allowance merged and allow-wins scrubbing exposed the variable. EnvPatternsOverlap now computes exact intersection non-emptiness for the name glob dialect (case-insensitive, '*' any sequence, '?' single char) via memoized DP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * fix(sandbox): preset env allowances are exact names, not globs Glob-vs-glob intersection is a losing game: every dialect extension (character classes today) silently reopens the deny-bypass hole. Restricting preset environment allowances to literal variable names makes the authored-deny precedence check exact by construction: each deny pattern is evaluated against the concrete name with the same matcher ScrubEnv uses at runtime, so the decision cannot diverge from enforcement regardless of deny dialect. Removes the glob intersection machinery. Profile and --sandbox-allow env globs are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * fix(sandbox): reject mandatory-deny targets in preset paths Preset validation relied on IsSensitiveProjectTarget, which covers fewer files than util.DANGEROUS_FILES. A preset naming .git-credentials, .pgpass, .docker/config.json or .config/gh exactly would exact-match suppress the mandatory deny; .git/config in allow_write would suppress the write protection. Preset paths are now checked against DANGEROUS_FILES (single source of truth), .git/hooks is rejected in any direction, and .git/config is rejected for write/exec while read stays allowed for git repo discovery. Docs state the two deliberate opt-outs precisely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * feat(sandbox): preset init and edit commands for community authoring pmg sandbox preset init scaffolds a valid user preset (metadata flags, threat-note template, starter rule) and refuses built-in names since builtins win resolution. pmg sandbox preset edit opens the file via the shared editor package and validates the result, warning when a user preset is shadowed by a built-in. Docs lead with the scaffolded flow and spell out builtin-vs-community provenance in preset list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn * refactor(sandbox): move mandatory-target matching into util Preset path validation re-encoded knowledge util already owns: the dangerous-files comparison and hardcoded .git/config and .git/hooks strings. util now exports GitConfigPath, GitHooksPath (also used by GetMandatoryDenyPatterns), PathCoveredBy and DangerousFileMatch, and preset validation consumes them so the mandatory deny policy has a single definition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PETCfE4crLcmodosz12qRn --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
695a1d739d |
feat: Add support for sandbox edit command (#385)
* feat: Add support for sandbox edit command * docs: Update sandbox docs * fix(editor): address review — neutral failure wording, skip sh-script tests on Windows Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): avoid pnpm init in pm-e2e — devEngines.packageManager crashes subsequent pnpm add Same workaround and rationale as the PNPM job in pmg-e2e.yml. Latest pnpm (unpinned pnpm/action-setup) writes devEngines.packageManager with onFail:download on init, and the next add fails with "Cannot use 'in' operator to search for 'integrity' in undefined". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bafd3c0654 | Update README to include GHCR installation detail (#383) | ||
|
|
6cd8851679 |
fix(container): cross-compile per target platform so linux/arm64 ships an arm64 binary (#382)
The build stage ran natively on $BUILDPLATFORM and make never received a target arch, so both manifest variants shipped the same amd64 binary (#379). Pass TARGETOS/TARGETARCH into the build with CGO_ENABLED=0 and verify each platform variant runs 'pmg version' after push. Fixes #379 Claude-Session: https://claude.ai/code/session_01E8yrxjqVw7454zByb8kxvD Co-authored-by: Claude <noreply@anthropic.com>v0.24.2 |
||
|
|
d6709e2fd7 |
feat(audit): record cloud events without credentials via EventEmitterClient (#381)
Switch the audit cloudSink from the authenticated SyncClientBundle to dry's new emit-only endpointsync.EventEmitterClient. Normal command invocations now persist events to the local cloud-sync WAL without resolving credentials or opening a data-plane connection; delivery still happens only through the authenticated sync paths (pmg cloud sync, sync-background auto-sync, and the proxy daemon drain), which are unchanged. Previously, running with cloud.enabled but no credentials skipped the cloud sink entirely, so no events were recorded. Those events are now staged in the WAL and sync once the user authenticates. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>v0.24.1 |
||
|
|
46803f8e70 |
fix(doctor): stop npm protection passing when the real package manager is absent (#378)
`pmg setup doctor` reported npm protection as OK on a machine with no npm installed. Two compounding defects: 1. The availability gate used a plain `exec.LookPath`, which resolves the PMG shim on PATH rather than the real binary, so the "skip" branch never fired. It now uses `shim.ResolveRealBinary` (PATH with shim dirs stripped), matching the runner, so a missing real binary correctly yields WARN "not available". 2. The result was inferred purely from a non-zero exit, so PackageManagerNotFound (exit 127) and other failures were misread as a successful block. The check now requires PMG's block headline in the captured output before reporting PASS; other non-zero exits report WARN with the error surfaced. The block headline is extracted into `ui.MalwareBlockedHeadline` so the doctor's marker stays in sync with what PMG prints across its block-output sites.v0.24.0 |
||
|
|
dc0e3202cc |
feat: Linux system-wide setup (pmg setup install --system) (#377)
* feat: add Linux system-wide setup
Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: keep local design documents untracked
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden and simplify Linux system install
Tighten shim detection, profile repair, and install ordering while
trimming over-specific doctor/info hints from the system-install path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: clarify system-install doctor alias and shim path checks
Use UserBinDir for PATH checks and pass aliases as not required under
system install without treating that as active interception.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: tighten event-log soft-fail warning prefix
Prefix the warning with [pmg] and drop the redundant continuing clause.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: add Linux system-install e2e and pin pnpm for add flake
Cover root system setup, PATH/profile.d, managed config, non-root
interception, and remove. Pin pnpm 11.10.0 on the package-manager e2e
job after an integrity crash on pnpm add.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: bump packageManager to pnpm 11.10.0 for e2e
Align package.json with the pnpm version we want in CI so action-setup
stops erroring on a version mismatch after the e2e integrity flake.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: use npm init for pnpm e2e to avoid integrity crash
pnpm 11.x `pnpm init` still writes onFail:download; `pnpm add` then
fails after PMG analysis even on 11.10.0. Seed the temp package with
npm init instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: revert packageManager pin to pnpm 11.1.3
The e2e integrity crash is avoided by npm init; the 11.10.0 bump is
no longer needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden system-install review findings
Require root-owned, non-group/other-writable pmg for --system install;
allow remove without that validation. Doctor checks npm resolution for
PATH precedence, uses ImpliesInterception instead of message matching,
and documents version-manager shadowing. Pass profile bin dir from the
shim manager and note that system config ignores per-user files.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden doctor PATH checks and attribute cloud events by OS user
Doctor now verifies every installed package manager against the shim
directory, and system-install validation only requires a safe parent
directory. Cloud sync records username/uid on invocation context for
multi-user hosts sharing one endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address system-install review findings
- shim: make system executable resolution injectable so tests pass under
umask 002; skip the root-owner test when running as root
- doctor: treat resolution into either the system or per-user shim dir as
intercepted, and collapse the shim-in-PATH check to a single call site
- setup: make remove (both --system and per-user) best-effort with
errors.Join so one failed step no longer strands the other artifact
- shim: allow a group-writable install parent dir (Debian/Ubuntu ship
/usr/local/bin as root:staff 2775) while still rejecting world-writable
and non-root-owned parents
- audit: attribute cloud events to SUDO_USER when running under sudo
- docs: drop the soft-fail event-logging claim (hard-fail is retained)
* ci: normalize /usr/local/bin perms before system-install e2e
The GitHub ubuntu-latest runner ships /usr/local/bin world-writable so
tooling can install without sudo. System install correctly refuses a
world-writable dir for the shared binary (any local user could replace
it and hijack every user's npm/pip). No FHS-compliant distro or Docker
image ships it world-writable — it is always root:root 0755 or
root:staff 2775 — so this normalizes only the anomalous CI runner back
to standard perms and still exercises the real /usr/local/bin path.
* fix: actionable remedy for root-created per-user config dir
A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su
without -) creates the invoking user's ~/.config/safedep as root-owned,
and event-log init then fail-closes every later non-root command.
Make that state self-solvable:
- event-log init permission errors exit with a usefulerror naming the
likely cause and the chown fix instead of a bare fatal
- pmg setup doctor probes event-log dir writability and reports the
same fix via a new per-result Fix override
- document the mechanism and remedy in system-install.md, along with
the binary ownership requirements for --system
- consolidate this branch's doctor tests into doctor_test.go
* fix: resolve per-user paths from root's own home when running as root
Path resolution trusted HOME (and XDG_*), which sudo and su can preserve
from the invoking user (GitHub runners, sudo -E, su without -). Any pmg
run as root then created root-owned ~/.config/safedep inside that user's
home, and event-log init fail-closed every later non-root pmg/npm/pip
run for them. System install made sudo pmg the documented flow, turning
this latent bug into the happy path.
When euid is 0, configDir and cacheDir now resolve from root's passwd
home instead of the environment, so root state lands under /root and
user homes are never touched. PMG_CONFIG_DIR/PMG_CACHE_DIR still win,
non-root resolution is unchanged, and Windows is unaffected (no euid).
Event-log init stays fatal on failure; sudo-run package events are
attributed via SUDO_USER and synced by the exit auto-sync as usual.
E2E: GitHub runners preserve HOME under sudo, so assert that no sudo
pmg run leaks state into the runner's home, and that the managed-config
refusal fails for the documented reason rather than a permission brick.
* fix: triage the unwritable config dir remedy by cause
The chown hint is only correct when another account created files
inside the current user's own home. When a leaked HOME or
XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub
runners), following it would chown that user's directory and brick
their pmg instead. Classify the failure against the passwd home,
which the leaked environment cannot influence, and prescribe:
- dir inside own home: restore ownership with chown
- dir outside own home: fix the leaked environment, never chown
- explicit PMG_CONFIG_DIR: make it writable
Used by both the fatal event-log error and the doctor check, and the
docs troubleshooting now carries the same two-case triage.
* ci: pin XDG_CONFIG_HOME for the cross-user e2e step; terse doctor fix
GitHub runners export XDG_CONFIG_HOME=/home/runner/.config and it leaks
through sudo -u, so the pmgtest pmg resolved the runner user's config
dir and fail-closed on its runner-owned log file (run 29289868727 shows
the triaged error catching exactly this). Set it inside the login shell
so it wins regardless of how the leak is delivered.
The remedy now returns a full-help and doctor-table pair from a single
triage, and drops the do-not-chown tail from the leak message.
* fix: adapt event-log error to the two-value remedy signature
Belongs with the previous commit; it was left unstaged and
|
||
|
|
d3e656edcd | feat(sandbox): enable network_via_proxy_only for the Go ecosystem profile (#375) v0.23.0 | ||
|
|
5e0670266e | test(sandbox): darwin end-to-end lockdown enforcement test (#374) | ||
|
|
ed614812f5 | feat(sandbox): actionable violation message for lockdown network denials (#373) | ||
|
|
3ac83a436d |
feat(sandbox): Seatbelt lockdown translation — deny-all outbound, allow loopback proxy port (#372)
* feat(sandbox): Seatbelt lockdown translation confines outbound to the PMG proxy Under network_via_proxy_only the Seatbelt profile now denies all network-outbound (with a target=direct violation marker) and allows only the loopback proxy port. SBPL is last-match-wins, so the broad deny is emitted first, specific allows after, and the allow_network_bind rules last — keeping loopback-to-loopback dev traffic working under lockdown. allow_direct_dns re-opens the /var/run/mDNSResponder unix socket that the deny otherwise covers. Replaces the temporary fail-closed rejection in the Seatbelt driver with the real translation; non-lockdown profiles translate byte-identically to before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS * test(sandbox): assert deny marker presence before ordering comparison Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS * feat(sandbox): render lockdown profiles without a proxy as deny-only with runtime note pmg sandbox profile show renders profiles for debugging and must not fail on lockdown profiles. Without a running proxy the translator keeps the broad deny (rendered profile stays fail-closed, never looser than runtime) and documents the runtime-only proxy-port allow in an SBPL comment instead of fabricating a port. Execution is unaffected: the driver validates the proxy address before translating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
5131c3f641 |
feat(sandbox): ExecutionContext plumbing and fail-closed lockdown contract (#371)
* feat(sandbox): ExecutionContext plumbing and fail-closed lockdown contract
Network lockdown needs the PMG proxy's address, which is only known at
spawn time. Thread an ExecutionContext from the proxy flow through the
runner and executor into every sandbox driver, and enforce the
network_via_proxy_only fail-closed contract: lockdown without a running
loopback proxy, or on a driver that cannot enforce it, is a hard error —
never a silent fallback to unrestricted network.
- sandbox.ExecutionContext{ProxyAddr} + 4-arg Sandbox.Execute
- sandbox.ValidateLockdown validates the proxy address (loopback only)
with usefulerror code SandboxRequiresProxy
- Seatbelt validates lockdown before translation (translation itself
lands next); bubblewrap and landlock reject lockdown as unsupported
until Linux enforcement is implemented
- executor.WithExecutionContext, runner.ExecuteOptions.SandboxProxyAddr,
proxy flow passes the live proxy address
- ApplySandbox also validates centrally before invoking the driver
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
* fix(sandbox): require numeric in-range proxy port in ValidateLockdown
The validated port string is embedded into generated sandbox profiles,
so service names, zero, and out-of-range ports are refused.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
* fix(sandbox): fail closed on Seatbelt lockdown until translation lands
A lockdown policy that passed proxy validation would silently receive
the pre-lockdown network rules from the translator. Reject it until the
lockdown profile translation is implemented, keeping the window between
plumbing and enforcement fail-closed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
* chore: review feedback — drop redundant comment, simplify stub help text
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
22d6eabb6b |
feat(sandbox): add network_via_proxy_only and allow_direct_dns policy fields (#370)
* feat(sandbox): add network_via_proxy_only and allow_direct_dns policy fields Config surface for network lockdown: network_via_proxy_only confines a sandboxed package manager's outbound network to the PMG proxy; allow_direct_dns is its escape hatch re-opening direct DNS. Both follow the existing pointer-bool inheritance pattern in MergeWithParent. Lint warns when allow_direct_dns is set without network_via_proxy_only, where it has no effect. The fields are declared and inherited but unread; enforcement lands with the ExecutionContext plumbing and Seatbelt lockdown translation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS * fix(sandbox): deep-copy new pointer flags in profile resolution expandPolicyPaths re-points the older pointer booleans so callers cannot corrupt the registry-cached policy; the new NetworkViaProxyOnly and AllowDirectDNS fields need the same isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e6b5157a2a |
ci: add macOS test job (#366)
* ci: add macOS test job Darwin-tagged tests (Seatbelt translator, diagnostics, upcoming network lockdown tests) never ran in CI: the test matrix covered ubuntu and windows only. The M0 sandbox lockdown milestone is macOS-first, so macOS coverage must exist before enforcement work lands. Also establishes the convention that darwin integration tests requiring sandbox-exec must fail (not skip) when running in CI, so missing tooling cannot silently hide security tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS * test(sandbox): make Seatbelt render golden test machine-independent The golden file baked the authoring machine's HOME, CWD and TMPDIR parent into the expected profile, so TestRenderSeatbelt_Golden could only pass on that machine. It never ran in CI before the macOS job exposed it. Normalize CWD, home and TMPDIR parents (raw and query-escaped forms) to stable placeholders before comparison, longest path first so prefix overlaps survive, and rewrite the golden accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS * ci: make setup-go cache explicit in macOS job Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
31e51b10dd |
refactor: move proxy block message rendering to presentation layer (#364)
* refactor: move proxy block message rendering to presentation layer * refactor: introduce ui.ProxyPresenter with injected advisory source * feat: friendly ecosystem labels in proxy block messages * test: cover ecosystemLabel derivation * fix: address review feedback on block context assertions and empty reference line |
||
|
|
2f1c348a06 |
fix(cloud): surface real backend errors from pmg cloud sync (#365)
* fix(cloud): surface real backend errors from pmg cloud sync runSync wrapped every DrainToCloud failure as a network error, masking the actual cause — an entitlement failure surfaced as "check your network connectivity", which made backend issues very hard to diagnose. Classify the error first (usefulerror gRPC converters map backend statuses to authentication, entitlement, quota and server errors) and pass it through. The network-flavored message remains only as the fallback when nothing can classify the error. Bump safedep/dry to pick up nested-Any ErrorInfo extraction so entitlement classification also works against control-tower versions that re-wrap status details. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHdvpFXb2shyMzUDyR8QKA * chore: bump safedep/dry to latest branch commit Picks up the review follow-up in dry#128 (skip unmarshalling unknown detail types when unwrapping Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHdvpFXb2shyMzUDyR8QKA * chore: bump safedep/dry to post-merge main Replaces the dry#128 branch pseudo-version with the squashed main commit now that the nested-Any ErrorInfo fix has merged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHdvpFXb2shyMzUDyR8QKA --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2d938ea381 |
feat: advisory message appended to block output (#362)
* docs(specs): add custom block messages and package blocklist spec * docs(specs): add custom block messages and package blocklist implementation plan * feat(config): add blocked_packages list and custom block messages * feat(audit): add package_blocklist_blocked event and blocklist model * feat(proxy): block blocklisted packages in the policy gate before analysis * feat(guard): block blocklisted packages before trust skip and analysis * feat(ui): render blocklist blocks and custom messages, fix silent-mode block output * feat(proxy): append custom messages to malware and go-cooldown block bodies * test(proxye2e): cover blocklist enforcement and custom block messages * docs(specs): remove spec and plan documents * refactor: drop guard-flow blocklist enforcement and trim docs Guard mode is being deprecated; the blocklist is enforced in proxy mode only. Remove the trusted_packages mirroring references outside the docs. * refactor(config): consolidate blocklist and block message under top-level block section Replace dependency_cooldown.message, malware.message and blocked_packages with a single block section: block.message is appended to every block output regardless of which control blocked, and block.packages is the package blocklist. * fix(ui): render block.message as info note with clean spacing * fix(ui): indent wrapped continuation lines in block reasons and messages * update config template * refactor(config): replace block section with top-level advisory_message Remove the package blocklist (will be implemented as part of policies in the future) and replace block.message with an optional top-level advisory_message appended to every block output. * chore(config): move advisory_message near top-level scalar configs in template |
||
|
|
c601e17cdc |
fix: Sandbox profile for Go ecosystem (#361)
* fix: Sandbox profile for Go ecosystem * fix: Sandbox violations for network bindv0.22.1 |
||
|
|
02965143d0 |
fix: fail fast when package manager is not installed (#360)
* fix: fail fast with clear error when package manager is not installed When PMG shims intercept a package manager that isn't installed (e.g. a clean JAMF-provisioned laptop where PMG is set up before dev tooling), real-binary resolution failed with a generic "unknown" error and a bug-report link, making it look like PMG itself had crashed. Introduce a typed BinaryNotFoundError that exits with code 127 (standard "command not found") and maps to a new PackageManagerNotFound error code with an actionable message instead of the Unknown classification. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: improve missing package manager help message Use a concise, dynamic install hint instead of explaining PMG's PATH forwarding internals. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: mention PATH in missing package manager help text Covers the common case where a package manager is installed but its bin directory is not on PATH yet (e.g. after curl | bash install). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>v0.22.0 |
||
|
|
b19473945b |
Add experimental Go module proxy support (#358)
* feat: add experimental Go module support via pmg go Adds Go modules as a proxy-guarded ecosystem, opt-in only: the command runs solely when invoked explicitly as `pmg go ...` and is deliberately excluded from setup aliases and PATH shims so existing users are unaffected. - packagemanager: goPackageManager with fail-safe command classification (vet/fix excluded from non-download since they can fetch on a cold cache) and pinned-version extraction where only canonical semver counts as explicit. - GOPROXY normalization (fail-closed): effective GOPROXY read via `go env` (honors go env -w), rebuilt comma-joined with `direct` dropped so a 403 block is terminal and nothing silently falls back to unanalyzed VCS fetches. GOPRIVATE/GONOPROXY surface a warning; GOINSECURE is cleared. Contributed to the proxy flow through a new ProxyRoutingProvider hook (extra child env + dynamic MITM hosts). - Go interceptor with dynamic host matching from the user's effective GOPROXY via InterceptorContext.GoProxyHosts. Malware analysis runs on .zip only (the sole endpoint that delivers code); .info/.mod/@latest/ list pass through; /sumdb/ traffic and sum.golang.org are never touched so checksum-db verification stays intact; golang.org/toolchain is allowed on Go's own checksum verification. - Dependency cooldown: publish time captured from .info responses (body unmodified), in-window .zip blocked with 403; fails open for cooldown only when the publish time was never observed. - Cert gate: on macOS/Windows `pmg go` fails fast with actionable guidance unless the persisted PMG CA is OS-trusted (Go ignores SSL_CERT_FILE there); Linux works via the injected bundle. - proxye2e: GOPROXY-protocol mock registry, Go driver and 10 hermetic cases (allow/block/confirm, case-escaped paths, cooldown block and fail-open, toolchain, sumdb passthrough). Verified end-to-end on Linux: `pmg go get github.com/google/uuid@v1.6.0` MITMs proxy.golang.org, analyzes the decoded module at the .zip fetch, and go.sum verification succeeds through the tunneled checksum db. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xuhBeTVpfU4SdqVaarvuK * fix(go): address review findings on experimental Go support - Drop fmt/clean from NonDownloadCommands: both load packages via go list and can download modules on a cold cache, which would bypass the proxy under install_only. - Support GOPROXY entries with a base path (e.g. corp Athens/JFrog at https://corp/goproxy): the interceptor now receives host -> base URL and strips the path prefix before parsing module URLs, so verdicts and cooldown key on the real module path. - Default unschemed GOPROXY entries to https, matching go's own behavior, so corp mirrors configured as bare hosts are intercepted instead of silently unanalyzed. - Memoize the final verdict per module zip: go re-requests a failed zip during go get's load phase, which double-recorded stats (the report showed the same blocked module twice) and would have re-prompted on Confirm verdicts. - Fetch .info out-of-band on a cooldown cache miss: go serves .info from its local module cache on any machine that used go before PMG, which silently disabled cooldown. Failure of the side-fetch still fails open for cooldown only. - Move the noop package resolver into packagemanager. Verified live: cold-cache cooldown block now records once; warm-cache rerun is blocked via the side-fetch instead of failing open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xuhBeTVpfU4SdqVaarvuK * docs: collapse Go proxy-mode details by default Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xuhBeTVpfU4SdqVaarvuK --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
648adcbda4 |
feat: add uvx (uv tool run) package executor (#357)
* feat(uvx): add uvx (uv tool run) package executor Adds support for `uvx`, implemented as a PyPI Executor alongside pipx. uvx is an alias for `uv tool run`: it installs a tool into an ephemeral environment and runs it, so it has no install/list subcommand and the first positional argument (or --from) is the package to audit. Parsing highlights: - --from overrides the positional command as the package to audit - --with packages are audited as additional environment dependencies - name@version shorthand (ruff@0.3.0, ruff@latest) is normalized - flag parsing stops at the tool name so the tool's own flags are not misread as uvx options; uvx's value/boolean flags are registered so none greedily consume the package positional - VCS/URL/local-path specs are skipped for registry auditing Wires up command registration, analytics, shell alias/shim, cloud audit mapping, a dedicated `uvx` sandbox profile (UV_*/PIP_* env, uv cache and tool dirs), config policy, docs, unit tests and an E2E workflow step. Closes #326 https://claude.ai/code/session_011hyLxq7oWJX5Dp4tCEfG19 * chore(uvx): align docs and base profile with uvx support Incorporates the low-risk, non-parser improvements from the community PR #345 (author non-responsive) into our implementation: - list uvx (and the previously-missing pipx) as PyPI managers in the pypi-restrictive base profile package_managers and its README, so the base profile applies directly when selected via --sandbox-profile - document uvx in docs/github-action.md and docs/proxy-mode.md - add version / IsExplicitVersion assertions to the uvx parser tests Our pflag-based parser is kept as-is: unlike #345 it audits --with packages and handles all uvx short flags (e.g. -w), both of which the community PR misses. * fix(uvx): skip interpreter requests; use require in tests Addresses review feedback on PR #357: - uvx interpreter requests (`uvx python`, `uvx python@3.12`, `uvx pypy`, ...) launch an isolated interpreter rather than installing a PyPI tool. Treating the positional as a package made the guard flow resolve/analyze pkg:pypi/python (and python==3.12), which could wrongly block or fail a valid invocation. Skip these for the positional; --with packages on the same command are still audited. - Use require.NoError / require.Len for fatal assertions in the uvx tests, matching the repo's testing convention, so a failure stops the subtest before a nil dereference instead of panicking. * docs(uvx): document fail-open and --with-requirements trade-offs Record the two deliberate parsing decisions raised in review as in-code trade-off comments (no behavior change): - unknown flags are tolerated (fail open), consistent with the other executors; the residual gap only affects non-proxy guard mode since the default proxy flow intercepts every registry download. - --with-requirements / --with-editable values are consumed but not expanded into audit targets; expanding them needs manifest-extractor and guard changes, tracked as follow-up. Proxy mode still covers them. * docs(uvx): drop --with-requirements limitation note Per maintainer review: guard mode is being deprecated and auditing the contents of an existing requirements file is a scanner's responsibility, not PMG's. Remove the "known limitation / follow-up" note; the flags stay registered only so their values are not mistaken for the tool positional. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0fb6faa951 |
add runner hostname to CI invocation context (#356)
* add runner hostname to CI invocation context * update testv0.21.3 |
||
|
|
f9ca26d112 |
Add Trendshift badge to README (#355)
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com> |
||
|
|
2a7ecdb556 |
ci: add E2E for pmg action server-mode (#354)
* ci: add E2E for pmg action server-mode Exercises the safedep/pmg action with server-mode enabled: the action starts the proxy daemon and injects HTTP_PROXY + CA env vars, then bare npm/pip installs are intercepted. Asserts benign installs pass, malicious npm/pip installs are blocked, and 'pmg proxy stop --fail-on-violation' fails the job on the blocks. Scoped via paths to action.yml and the proxy server sources so it only runs when relevant. Complements persistent-proxy-e2e.yml, which covers the branch's proxy internals via raw pmg proxy commands. * ci: extract shared proxy E2E assertions into composite action Both the action server-mode E2E and the persistent-proxy E2E run the same benign/malicious install assertions. Factor them into a local composite action (.github/actions/proxy-e2e-assertions) so the assertions live in one place and both workflows exercise the composite itself. Proxy teardown (pmg proxy stop --fail-on-violation) stays inline in each workflow since the setup/teardown differs per binary-under-test (released binary via the action vs. branch build via make). |
||
|
|
7b13b6bfb3 | docs: Update README GHA example (#353) v0.21.2 | ||
|
|
5ac0f275b8 |
feat(proxy): emit session summary on persistent proxy shutdown (#352)
* feat(proxy): emit session summary on persistent proxy shutdown The persistent proxy daemon never ran the per-invocation flow that calls LogSessionComplete, so no session summary reached the cloud for server-mode runs. Emit one from the daemon's aggregate stats collector on shutdown, before the final cloud flush, so it is delivered with the run's events and carries the CI/invocation context. Extract the event emission into a shared LogSessionSummary(SessionData); both the per-invocation flow and the daemon now use it. The daemon serves every package manager, so the summary carries no single package manager. * chore(action): shorten default cloud endpoint id prefix to gha/v1 v0.21.1 |
||
|
|
c47776db27 |
feat(proxy): persistent proxy server mode (#351)
* 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>v0.21.0 |
||
|
|
7ee4187d50 | refactor: Event log use audit event as SSOT (#350) | ||
|
|
25dd12d7a4 |
test: Add proxy e2e test (#348)
* test: Add proxy e2e test * fix: Code review fixes * test: Add dependency cooldown skip list test casev0.20.0 |
||
|
|
d360e75897 |
feat: Add malysis cache implementation with proxy flow integration (#346)
* feat: Add malysis cache implementation with proxy flow integration * fix: Code review fixes |
||
|
|
327c9c7068 |
feat(cooldown): respect trusted_packages in dependency cooldown (#342)
* feat(cooldown): respect trusted_packages in dependency cooldown Trusted packages are now treated as a superset waiver that bypasses every PMG control (malware analysis, cooldown, and any future controls). A globally trusted package is automatically exempt from the cooldown window and no longer needs a duplicate entry in dependency_cooldown.skip. The skip list remains the narrower, cooldown-only waiver for packages that must bypass the cooldown wait but still be malware-scanned. * refactor(cooldown): tag skip reason and audit-log skipped packages Address review feedback on #342: - Restore cooldownSkip to a pure single-list function (SRP); the merge into trusted_packages now happens in a separate mergeCooldownSkip step, driven by the exported CooldownSkip wrapper. - Extend CooldownSkipInfo with a CooldownSkipReason (TrustedPackage / CooldownSkipList) on both SkipAll and per-version entries, so callers can tell apart the broad waiver from the cooldown-only one. When both lists match the same package, trusted_packages wins. - Add audit.LogCooldownSkipped and emit it from the npm and PyPI interceptors on the SkipAll path, alongside the existing info log, carrying the source list as the reason. * refactor(cooldown): inline list merge, audit per-version exemptions Address further review feedback: - Drop the separate mergeCooldownSkip helper; cooldownSkip now writes into a shared *CooldownSkipInfo and is called twice from CooldownSkip (cooldown skip list first, trusted_packages on top so trusted entries override the reason on overlap). - Audit log every exemption, not just SkipAll: a new auditCooldownSkip helper in proxy/interceptors/cooldown.go emits one event per match (package-wide or per-version), each tagged with its source list. LogCooldownSkipped gains a version argument for the per-version case. - Cover the trusted_packages reason path in TestCooldownSkip. * fix(cooldown): avoid double-auditing trusted package exemptions auditCooldownSkip now only emits EventTypeCooldownSkipped for entries that came from dependency_cooldown.skip. Trusted-package exemptions already get an EventTypeInstallTrustedAllowed event at tarball-download time (proxy/interceptors/base_registry.go), so emitting a cooldown event for them too would double-count the same waiver. * emit trusted and cooldown skip events to cloud * fix tests * refactor(cooldown): return value from collectCooldownSkip, short-circuit on trusted SkipAll Address PR review feedback: - Rename cooldownSkip to collectCooldownSkip and return CooldownSkipInfo instead of mutating an input pointer. - Add mergeCooldownSkip to combine per-list results with trusted_packages taking precedence on overlap. - CooldownSkip now consults trusted_packages first and returns immediately on a package-wide trusted exemption (DC skip list cannot add anything). - Extend tests to cover disjoint pinned entries across both lists and the case where DC version-less subsumes a trusted pinned entry. * fix(audit): address cooldown review feedback * fix(cooldown): audit cooldown skips at download time with concrete version Backend rejects PackageVersion messages without a version, and audit logs should reflect the runtime fact (a specific version was skipped) rather than the config rule. Move the audit emission from metadata-request handling to download-request handling, where the concrete version is known, and require version in LogCooldownSkipped. * chore(audit): drop dead scope assignment in LogCooldownSkipped * refactor(cooldown): move skip-list logic into cooldown handlers Registry interceptors no longer compute CooldownSkip or branch on SkipAll; they just call HandleMetadataRequest. The npm and pypi cooldown handlers own the skip lookup, the package-wide exemption short-circuit, and (for pypi) the canonical-name denormalization. Also align LogCooldownSkipped with other LogXxx signatures by taking *packagev1.PackageVersion. * fix: Simplify audit logging for dependency cooldown skip * refactor: Simplify cooldown handling and maintain separation of concepts for trusted and DC skip packages * fix: Code review fixes * fix: Emit cooldown skipped audit event ONLY when an in-window version is skipped --------- Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com> |
||
|
|
c17b941ac3 |
fix: expand ${CWD}/${HOME}/${TMPDIR} in --sandbox-allow path overrides (#344)
The runtime --sandbox-allow CLI override path never expanded the supported
sandbox variables (${CWD}, ${HOME}, ${TMPDIR}), so a value like
write='${CWD}/**' was treated as a literal path segment and the allow rule
never matched. Profile-loaded sandbox paths already expand these via
sandbox/util.ExpandVariables.
Expand the variables in resolveToAbsolute, the shared chokepoint for
read/write/exec overrides, before resolving to an absolute path. Glob
characters are preserved through expansion and filepath.Clean.
Fixes #257
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
|
||
|
|
55f3f2a252 | fix: Decouple cloud sync from telemetry (#343) |