mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* 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.
1064 lines
40 KiB
YAML
1064 lines
40 KiB
YAML
name: PMG E2E Tests
|
|
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
push:
|
|
branches:
|
|
- main
|
|
workflow_dispatch:
|
|
|
|
# PRs share a concurrency group (cancel/serialize); Pushes use unique groups to avoid cancellation/queuing
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ (github.event_name == 'pull_request' && github.ref) || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
e2e-test:
|
|
name: PMG E2E Tests - All Package Managers
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest]
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
steps:
|
|
- name: Checkout Source
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Enable corepack
|
|
run: corepack enable
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Setup PNPM
|
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
|
|
|
- name: Setup Bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
with:
|
|
bun-version: latest
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Setup uv
|
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
|
|
- name: Install Poetry
|
|
uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a
|
|
|
|
- name: Build PMG
|
|
run: make
|
|
|
|
- name: Add pmg to PATH
|
|
run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
|
|
|
|
- name: Setup PMG
|
|
run: pmg setup install
|
|
|
|
- name: Test PMG - Config File is Created
|
|
run: |
|
|
test -f $HOME/.config/safedep/pmg/config.yml
|
|
|
|
- name: Test PMG Aliases and Shims are Installed
|
|
run: |
|
|
test -f $HOME/.pmg.rc
|
|
test -d $HOME/.pmg/bin
|
|
for shim in npm pip pip3 pnpm bun uv uvx yarn poetry npx pnpx; do
|
|
test -x $HOME/.pmg/bin/$shim || { echo "Missing shim: $shim"; exit 1; }
|
|
done
|
|
|
|
- name: Test NPM - Single Package & Manifest
|
|
run: |
|
|
echo "Testing NPM single package installation..."
|
|
mkdir npm-test && cd npm-test
|
|
pmg npm init -y
|
|
pmg npm install express@5.2.1
|
|
pmg npm install lodash@4.17.21
|
|
|
|
# Verification: npm added packages present and manifest updated
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
grep -q '"express"' package.json
|
|
grep -q '"lodash"' package.json
|
|
|
|
echo "Testing NPM manifest installation..."
|
|
rm -rf node_modules package-lock.json
|
|
pmg npm install
|
|
|
|
# Verification: npm lockfile and installed modules exist after manifest install
|
|
test -f package-lock.json
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
cd .. && rm -rf npm-test
|
|
|
|
- name: Test NPM - Proxy Mode
|
|
run: |
|
|
echo "Testing NPM with proxy-based interception..."
|
|
mkdir npm-proxy-test && cd npm-proxy-test
|
|
pmg npm init -y
|
|
|
|
echo "Testing proxy mode single package installation..."
|
|
pmg npm install express@5.2.1
|
|
pmg npm install lodash@4.17.21
|
|
|
|
# Verification: packages installed via proxy mode
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
grep -q '"express"' package.json
|
|
grep -q '"lodash"' package.json
|
|
|
|
echo "Testing proxy mode manifest installation..."
|
|
rm -rf node_modules package-lock.json
|
|
pmg npm install
|
|
|
|
# Verification: manifest install via proxy mode works
|
|
test -f package-lock.json
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
|
|
echo "Testing proxy mode with scoped package..."
|
|
pmg npm install @types/node@18.0.0
|
|
|
|
# Verification: scoped package installed via proxy
|
|
test -d node_modules/@types
|
|
test -d node_modules/@types/node
|
|
grep -q '"@types/node"' package.json
|
|
|
|
echo "Testing proxy mode dry-run (should not create files)..."
|
|
rm -rf node_modules package-lock.json
|
|
pmg --dry-run npm install
|
|
|
|
# Verification: dry-run doesn't create files even in proxy mode
|
|
test ! -d node_modules
|
|
test ! -f package-lock.json
|
|
|
|
cd .. && rm -rf npm-proxy-test
|
|
|
|
- name: Test PyPI - Proxy Mode
|
|
run: |
|
|
echo "Testing PyPI package managers with proxy-based interception..."
|
|
mkdir pypi-proxy-test && cd pypi-proxy-test
|
|
|
|
echo "Setting up Python virtual environment for pip and pip3 tests..."
|
|
python -m venv venv && source venv/bin/activate
|
|
python --version
|
|
pip --version
|
|
|
|
echo "Testing pip single package installation via proxy mode..."
|
|
pmg pip install requests==2.32.4
|
|
pmg pip install numpy==2.3.5
|
|
|
|
# Verification: packages installed and importable
|
|
python -c "import requests, numpy; print('pip ok:', requests.__version__, numpy.__version__)"
|
|
|
|
echo "Testing pip manifest installation via proxy mode..."
|
|
pmg pip freeze > requirements.txt
|
|
pmg pip uninstall -y requests numpy
|
|
pmg pip install -r <(grep -v '^PMG:' requirements.txt)
|
|
python -c "import requests, numpy; print('pip manifest ok:', requests.__version__, numpy.__version__)"
|
|
deactivate
|
|
|
|
echo "Setting up Python virtual environment for pip3 tests..."
|
|
python -m venv venv3 && source venv3/bin/activate
|
|
python --version
|
|
pip3 --version
|
|
|
|
echo "Testing pip3 single package installation via proxy mode..."
|
|
pmg pip3 install requests==2.32.4
|
|
pmg pip3 install numpy==2.3.5
|
|
|
|
# Verification: packages installed and importable
|
|
python -c "import requests, numpy; print('pip3 ok:', requests.__version__, numpy.__version__)"
|
|
|
|
echo "Testing pip3 manifest installation via proxy mode..."
|
|
pmg pip3 freeze > requirements3.txt
|
|
pmg pip3 uninstall -y requests numpy
|
|
pmg pip3 install -r <(grep -v '^PMG:' requirements3.txt)
|
|
python -c "import requests, numpy; print('pip3 manifest ok:', requests.__version__, numpy.__version__)"
|
|
deactivate
|
|
|
|
echo "Testing uv add and uv pip install via proxy mode..."
|
|
mkdir uv-proxy && cd uv-proxy
|
|
pmg uv init --no-readme
|
|
pmg uv add requests==2.32.4
|
|
pmg uv add numpy==2.3.5
|
|
|
|
# Verification: pyproject.toml lists expected dependencies
|
|
test -f pyproject.toml
|
|
grep -q 'requests' pyproject.toml
|
|
grep -q 'numpy' pyproject.toml
|
|
|
|
echo "Sync environment and verify installations..."
|
|
pmg uv sync
|
|
pmg uv pip show requests >/dev/null
|
|
pmg uv pip show numpy >/dev/null
|
|
|
|
echo "Testing uv pip install from requirements via proxy mode..."
|
|
pmg uv pip freeze > requirements.txt
|
|
pmg uv pip install -r <(grep -v '^PMG:' requirements.txt)
|
|
pmg uv pip show requests >/dev/null
|
|
pmg uv pip show numpy >/dev/null
|
|
cd ..
|
|
|
|
cd .. && rm -rf pypi-proxy-test
|
|
|
|
- name: Test PyPI - httpx NO_PROXY Regression (#339)
|
|
run: |
|
|
echo "Testing that proxy-mode NO_PROXY does not crash Python httpx (#339)..."
|
|
HTTPX_TESTDIR=$(mktemp -d) && cd "$HTTPX_TESTDIR"
|
|
pmg uv init --no-readme
|
|
pmg uv add httpx==0.28.1
|
|
|
|
# Run under proxy mode (default). PMG injects NO_PROXY into the child env;
|
|
# the bracketed [::1] it used to inject made urllib/httpx raise
|
|
# "InvalidURL: Invalid port: ':1]'" when constructing a client.
|
|
pmg uv run python -c '
|
|
import os, httpx
|
|
np = os.environ.get("NO_PROXY") or os.environ.get("no_proxy") or ""
|
|
assert "::1" in np, "proxy-mode NO_PROXY not set, test is not exercising the fix: %r" % np
|
|
assert "[::1]" not in np, "NO_PROXY still contains bracketed loopback: %r" % np
|
|
httpx.Client().close()
|
|
print("httpx ok; NO_PROXY=" + np)
|
|
'
|
|
|
|
cd - && rm -rf "$HTTPX_TESTDIR"
|
|
|
|
- name: Test Go Modules - Proxy Mode (Experimental)
|
|
run: |
|
|
echo "Testing experimental Go module support via proxy mode..."
|
|
GO_TESTDIR=$(mktemp -d) && cd "$GO_TESTDIR"
|
|
go mod init example.com/pmg-go-e2e
|
|
cat > main.go <<'EOF'
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func main() { fmt.Println(uuid.NewString()) }
|
|
EOF
|
|
|
|
echo "Testing pmg go get with a pinned version..."
|
|
pmg go get github.com/google/uuid@v1.6.0
|
|
|
|
# Verification: module resolved and checksummed through the proxy
|
|
grep -q 'github.com/google/uuid v1.6.0' go.mod
|
|
grep -q 'github.com/google/uuid v1.6.0' go.sum
|
|
|
|
echo "Testing pmg go mod tidy..."
|
|
rm go.sum
|
|
pmg go mod tidy
|
|
|
|
# Verification: go.sum regenerated via proxied module fetches
|
|
grep -q 'github.com/google/uuid v1.6.0' go.sum
|
|
|
|
echo "Testing pmg go run..."
|
|
pmg go run . | grep -Eq '^[0-9a-f-]{36}$'
|
|
|
|
cd - && rm -rf "$GO_TESTDIR"
|
|
|
|
# Regression: PMG injects HTTP(S)_PROXY into its child, and `go test`
|
|
# inherits it. The hermetic proxye2e harness must not route its
|
|
# in-process proxy traffic through the outer PMG proxy.
|
|
- name: Test Go - Hermetic Proxy Tests Under PMG
|
|
run: pmg go test -count=1 -run 'TestProxyFlow_Go' ./test/proxye2e/
|
|
|
|
- name: Test PNPM - Single Package & Manifest
|
|
run: |
|
|
echo "Testing PNPM single package installation..."
|
|
PNPM_TESTDIR=$(mktemp -d) && cd "$PNPM_TESTDIR"
|
|
# Avoid `pnpm init`: it writes devEngines.packageManager with
|
|
# onFail:download, and the following `pnpm add` then crashes with
|
|
# "Cannot use 'in' operator to search for 'integrity' in undefined".
|
|
npm init -y
|
|
pmg pnpm add express@5.2.1
|
|
pmg pnpm add lodash@4.17.21
|
|
|
|
# Verification: pnpm packages installed and lockfile created
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
test -f pnpm-lock.yaml
|
|
|
|
echo "Testing PNPM manifest installation..."
|
|
rm -rf node_modules pnpm-lock.yaml
|
|
pmg pnpm install
|
|
|
|
# Verification: pnpm lockfile and modules exist after manifest install
|
|
test -f pnpm-lock.yaml
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
cd - && rm -rf "$PNPM_TESTDIR"
|
|
|
|
- name: Test Bun - Single Package & Manifest
|
|
run: |
|
|
echo "Testing Bun single package installation..."
|
|
mkdir bun-test && cd bun-test
|
|
pmg bun init -y
|
|
pmg bun add express@5.2.1
|
|
pmg bun add lodash@4.17.21
|
|
|
|
# Verification: bun packages installed and lockfile created
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
test -f bun.lock
|
|
|
|
echo "Testing Bun manifest installation..."
|
|
rm -rf node_modules bun.lock
|
|
pmg bun install
|
|
|
|
# Verification: bun lockfile and modules exist after manifest install
|
|
test -f bun.lock
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
|
|
echo "Testing Bun frozen manifest installation with bun ci..."
|
|
rm -rf node_modules
|
|
pmg bun ci
|
|
|
|
# Verification: bun lockfile and modules exist after frozen manifest install
|
|
test -f bun.lock
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
cd .. && rm -rf bun-test
|
|
|
|
# Runs before the classic-yarn step so corepack's Berry shim stays the `yarn`
|
|
# on PATH (the later `npm install -g yarn@1.22.22` would shadow it).
|
|
- name: Test safedep-test-pkg is Blocked using Yarn (Berry) Proxy mode
|
|
env:
|
|
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
|
|
YARN_NODE_LINKER: node-modules
|
|
YARN_ENABLE_HARDENED_MODE: "0"
|
|
# CI=true would enable immutable installs and fail before the proxy can block.
|
|
YARN_ENABLE_IMMUTABLE_INSTALLS: "false"
|
|
run: |
|
|
echo "Testing that safedep-test-pkg is blocked with Yarn Berry in proxy mode..."
|
|
YARN_BERRY_TESTDIR=$(mktemp -d) && cd "$YARN_BERRY_TESTDIR"
|
|
npm init -y
|
|
|
|
# Pin Yarn Berry without running an install (corepack use would trigger one).
|
|
npm pkg set packageManager=yarn@4.16.0
|
|
yarn --version
|
|
|
|
# A non-zero exit alone is not enough — yarn could fail for unrelated
|
|
# reasons (setup, network, CA trust) and falsely pass. Require PMG's
|
|
# malware block signal in the output.
|
|
if output=$(pmg yarn add safedep-test-pkg@0.1.3 2>&1); then
|
|
echo "$output"
|
|
echo "ERROR: safedep-test-pkg was not blocked with Yarn Berry!"
|
|
exit 1
|
|
fi
|
|
echo "$output"
|
|
if ! echo "$output" | grep -q "Malicious package blocked"; then
|
|
echo "ERROR: yarn add failed, but not due to a PMG malware block"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: safedep-test-pkg correctly blocked with Yarn Berry"
|
|
|
|
if [ -d "node_modules/safedep-test-pkg" ]; then
|
|
echo "ERROR: safedep-test-pkg found in node_modules!"
|
|
exit 1
|
|
else
|
|
echo "SUCCESS: safedep-test-pkg not present in node_modules"
|
|
fi
|
|
cd - && rm -rf "$YARN_BERRY_TESTDIR"
|
|
|
|
- name: Test Yarn - Single Package & Manifest
|
|
run: |
|
|
echo "Testing Yarn single package installation..."
|
|
export YARN_ENABLE_HARDENED_MODE=0
|
|
npm install -g yarn@1.22.22
|
|
yarn --version
|
|
|
|
YARN_TESTDIR=$(mktemp -d) && cd "$YARN_TESTDIR"
|
|
pmg yarn init -y
|
|
pmg yarn add express@5.2.1
|
|
pmg yarn add lodash@4.17.21
|
|
|
|
# Verification: yarn packages installed and lockfile created
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
test -f yarn.lock
|
|
|
|
echo "Testing Yarn manifest installation..."
|
|
rm -rf node_modules yarn.lock
|
|
pmg yarn install
|
|
|
|
# Verification: yarn lockfile and modules exist after manifest install
|
|
test -f yarn.lock
|
|
test -d node_modules/express
|
|
test -d node_modules/lodash
|
|
cd - && rm -rf "$YARN_TESTDIR"
|
|
|
|
- name: Test NPX - Package Execution
|
|
run: |
|
|
echo "Testing NPX package execution..."
|
|
mkdir npx-test && cd npx-test
|
|
|
|
echo "Testing npx with a simple package..."
|
|
pmg npx cowsay@1.6.0 "Hello from pmg npx" | tee npx-output.txt
|
|
|
|
# Verification: cowsay output contains our message
|
|
grep -q "Hello from pmg npx" npx-output.txt
|
|
|
|
echo "Testing npx with --package flag..."
|
|
pmg npx --package cowsay@1.6.0 -- cowsay "Hello with package flag" | tee npx-pkg-output.txt
|
|
|
|
# Verification: package flag execution produces expected output
|
|
grep -q "Hello with package flag" npx-pkg-output.txt
|
|
|
|
echo "Testing npx dry-run mode..."
|
|
pmg --dry-run npx cowsay@1.6.0 "This should not execute" | tee npx-dry-output.txt
|
|
|
|
# Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^ should not appear)
|
|
! grep -q '\^__\^' npx-dry-output.txt
|
|
|
|
cd .. && rm -rf npx-test
|
|
|
|
- name: Test PNPX - Package Execution
|
|
run: |
|
|
echo "Testing PNPX package execution..."
|
|
PNPX_TESTDIR=$(mktemp -d) && cd "$PNPX_TESTDIR"
|
|
|
|
echo "Testing pnpx with a simple package..."
|
|
pmg pnpx cowsay@1.6.0 "Hello from pmg pnpx" | tee pnpx-output.txt
|
|
|
|
# Verification: cowsay output contains our message
|
|
grep -q "Hello from pmg pnpx" pnpx-output.txt
|
|
|
|
echo "Testing pnpx with --package flag..."
|
|
pmg pnpx --package cowsay@1.6.0 -- cowsay "Hello with package flag" | tee pnpx-pkg-output.txt
|
|
|
|
# Verification: package flag execution produces expected output
|
|
grep -q "Hello with package flag" pnpx-pkg-output.txt
|
|
|
|
echo "Testing pnpx dry-run mode..."
|
|
pmg --dry-run pnpx cowsay@1.6.0 "This should not execute" | tee pnpx-dry-output.txt
|
|
|
|
# Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^ should not appear)
|
|
! grep -q '\^__\^' pnpx-dry-output.txt
|
|
|
|
cd - && rm -rf "$PNPX_TESTDIR"
|
|
|
|
- name: Test UVX - Package Execution
|
|
run: |
|
|
echo "Testing UVX package execution (uv tool run)..."
|
|
UVX_TESTDIR=$(mktemp -d) && cd "$UVX_TESTDIR"
|
|
|
|
echo "Testing uvx with a simple tool in proxy mode (default)..."
|
|
pmg uvx ruff@0.6.9 --version | tee uvx-output.txt
|
|
# Verification: pinned version is resolved and executed via the proxy
|
|
grep -q "0.6.9" uvx-output.txt
|
|
|
|
echo "Testing uvx version pin via @ syntax..."
|
|
pmg uvx ruff@0.6.9 --version | tee uvx-pin-output.txt
|
|
grep -q "0.6.9" uvx-pin-output.txt
|
|
|
|
echo "Testing uvx with --from (command name differs from package)..."
|
|
pmg uvx --from cowsay cowsay -t "Hello from pmg uvx" | tee uvx-from-output.txt
|
|
# Verification: cowsay output contains our message and cow art
|
|
grep -q "Hello from pmg uvx" uvx-from-output.txt
|
|
grep -q '\^__\^' uvx-from-output.txt
|
|
|
|
echo "Testing uvx dry-run mode..."
|
|
pmg --dry-run uvx --from cowsay cowsay -t "This should not execute" | tee uvx-dry-output.txt
|
|
# Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^)
|
|
! grep -q '\^__\^' uvx-dry-output.txt
|
|
|
|
cd - && rm -rf "$UVX_TESTDIR"
|
|
|
|
- name: Test Pip - Single Package & Manifest
|
|
run: |
|
|
echo "Testing Pip single package installation..."
|
|
mkdir pip-test && cd pip-test
|
|
python -m venv venv && source venv/bin/activate
|
|
pmg pip install requests==2.32.4
|
|
pmg pip install numpy==2.3.5
|
|
pmg pip freeze > requirements.txt
|
|
|
|
# Verification: requirements.txt contains expected packages
|
|
test -s requirements.txt
|
|
grep -E '^requests==' requirements.txt
|
|
grep -E '^numpy==' requirements.txt
|
|
|
|
echo "Testing Pip manifest installation..."
|
|
pmg pip uninstall -y requests numpy
|
|
pmg pip install -r requirements.txt
|
|
|
|
# Verification: imported packages are available in the environment
|
|
python -c "import requests, numpy; print(requests.__version__); print(numpy.__version__)"
|
|
deactivate
|
|
cd .. && rm -rf pip-test
|
|
|
|
- name: Test Pip3 - Single Package & Manifest
|
|
run: |
|
|
echo "Testing Pip3 single package installation..."
|
|
mkdir pip3-test && cd pip3-test
|
|
python -m venv venv && source venv/bin/activate
|
|
pmg pip3 install requests==2.32.4
|
|
pmg pip3 install numpy==2.3.5
|
|
pmg pip3 freeze > requirements.txt
|
|
|
|
# Verification: requirements.txt contains expected packages
|
|
test -s requirements.txt
|
|
grep -E '^requests==' requirements.txt
|
|
grep -E '^numpy==' requirements.txt
|
|
|
|
echo "Testing Pip3 manifest installation..."
|
|
pmg pip3 uninstall -y requests numpy
|
|
pmg pip3 install -r requirements.txt
|
|
|
|
# Verification: imported packages are available in the environment
|
|
python -c "import requests, numpy; print(requests.__version__); print(numpy.__version__)"
|
|
deactivate
|
|
cd .. && rm -rf pip3-test
|
|
|
|
- name: Test UV - Single Package & Manifest
|
|
run: |
|
|
echo "Testing UV single package installation..."
|
|
mkdir uv-test && cd uv-test
|
|
pmg uv init --no-readme
|
|
pmg uv add requests==2.32.4
|
|
pmg uv add numpy==2.3.5
|
|
|
|
# Verification: pyproject.toml lists expected dependencies
|
|
test -f pyproject.toml
|
|
grep -q 'requests' pyproject.toml
|
|
grep -q 'numpy' pyproject.toml
|
|
|
|
echo "Testing UV manifest installation..."
|
|
rm -rf .venv uv.lock
|
|
pmg uv sync
|
|
|
|
# Verification: uv lockfile and virtualenv created; packages present
|
|
test -d .venv
|
|
test -f uv.lock
|
|
pmg uv pip show requests >/dev/null
|
|
pmg uv pip show numpy >/dev/null
|
|
|
|
echo "Testing UV pip commands..."
|
|
pmg uv pip freeze > requirements.txt
|
|
pmg uv pip install -r requirements.txt
|
|
pmg uv pip sync requirements.txt
|
|
|
|
# Verification: uv pip can show installed packages after requirements sync
|
|
pmg uv pip show requests >/dev/null
|
|
pmg uv pip show numpy >/dev/null
|
|
cd .. && rm -rf uv-test
|
|
|
|
- name: Test Poetry - Single Package & Manifest
|
|
run: |
|
|
echo "Testing Poetry single package installation..."
|
|
mkdir poetry-test && cd poetry-test
|
|
pmg poetry init --name poetry-test --no-interaction --quiet
|
|
pmg poetry add requests==2.32.4
|
|
pmg poetry add numpy==2.3.5
|
|
|
|
# Verification: pyproject.toml dependencies updated
|
|
test -f pyproject.toml
|
|
grep -q 'requests' pyproject.toml
|
|
grep -q 'numpy' pyproject.toml
|
|
|
|
echo "Testing Poetry manifest installation..."
|
|
rm -rf .venv poetry.lock
|
|
pmg poetry install --no-root
|
|
cd .. && rm -rf poetry-test
|
|
|
|
- name: Test Malicious Package Detection
|
|
run: |
|
|
echo "Testing malicious package detection..."
|
|
mkdir malicious-test && cd malicious-test
|
|
pmg npm init -y
|
|
if pmg npm install nyc-config@10.0.0; then
|
|
echo "ERROR: malicious package was not blocked!"
|
|
exit 1
|
|
fi
|
|
echo "Malicious package correctly blocked"
|
|
cd .. && rm -rf malicious-test
|
|
|
|
- name: Test safedep-test-pkg is Blocked using Proxy mode
|
|
run: |
|
|
echo "Testing that safedep-test-pkg is blocked..."
|
|
mkdir safedep-test-pkg-test && cd safedep-test-pkg-test
|
|
pmg npm init -y
|
|
# Attempt to install safedep-test-pkg - should fail
|
|
if pmg npm --no-cache --prefer-online i safedep-test-pkg@0.1.3; then
|
|
echo "ERROR: safedep-test-pkg was not blocked!"
|
|
exit 1
|
|
else
|
|
echo "SUCCESS: safedep-test-pkg correctly blocked"
|
|
fi
|
|
# Verify package is not installed locally
|
|
if [ -d "node_modules/safedep-test-pkg" ]; then
|
|
echo "ERROR: safedep-test-pkg found in node_modules!"
|
|
exit 1
|
|
else
|
|
echo "SUCCESS: safedep-test-pkg not present in node_modules"
|
|
fi
|
|
cd .. && rm -rf safedep-test-pkg-test
|
|
|
|
- name: Test PMG Modes
|
|
run: |
|
|
echo "Testing different PMG modes..."
|
|
mkdir pmg-modes-test && cd pmg-modes-test
|
|
pmg npm init -y
|
|
# Mode: --dry-run should not create node_modules or lockfiles
|
|
pmg --dry-run npm install express
|
|
# Verification: no files created during dry-run
|
|
test ! -d node_modules
|
|
test ! -f package-lock.json
|
|
|
|
# Mode: --silent should install without noisy output
|
|
pmg --silent npm install express
|
|
# Verification: package installed
|
|
test -d node_modules/express
|
|
# Clean and test --verbose installation
|
|
rm -rf node_modules package-lock.json
|
|
pmg --verbose npm install express
|
|
# Verification: package installed
|
|
test -d node_modules/express
|
|
|
|
# Clean and test --debug with log output
|
|
rm -rf node_modules package-lock.json
|
|
pmg --debug --log debug.json npm install express
|
|
# Verification: debug log written
|
|
test -f debug.json
|
|
|
|
# Mode: --paranoid may require cloud credentials; run non-blocking with dry-run
|
|
pmg --paranoid --dry-run npm install express || true
|
|
|
|
# Removed proxy opt-outs must fail loudly, not silently proxy
|
|
if PMG_PROXY_ENABLED=false pmg --dry-run npm install express; then
|
|
echo "ERROR: PMG_PROXY_ENABLED=false should fail loudly"
|
|
exit 1
|
|
fi
|
|
# Legacy PMG_PROXY_MODE only selected guard mode when the config has
|
|
# no proxy: section; with the standard (template) config it was inert
|
|
# and must keep working.
|
|
PMG_PROXY_MODE=false pmg --dry-run npm install express
|
|
if PMG_CONFIG_DIR=$(mktemp -d) PMG_PROXY_MODE=false pmg --dry-run npm install express; then
|
|
echo "ERROR: PMG_PROXY_MODE=false without a proxy: section should fail loudly"
|
|
exit 1
|
|
fi
|
|
if pmg --proxy-mode=false --dry-run npm install express; then
|
|
echo "ERROR: --proxy-mode should be an unknown flag"
|
|
exit 1
|
|
fi
|
|
cd .. && rm -rf pmg-modes-test
|
|
|
|
sandbox-e2e-macos:
|
|
name: Sandbox E2E - macOS
|
|
runs-on: macos-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Checkout Source
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Enable corepack
|
|
run: corepack enable
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Setup PNPM
|
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
|
|
|
- name: Build PMG
|
|
run: make
|
|
|
|
- name: Add pmg to PATH
|
|
run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
|
|
|
|
- name: Setup PMG
|
|
run: pmg setup install
|
|
|
|
- name: Create Test Directories for Sandbox Permissions Tests
|
|
run: mkdir -p ~/.aws ~/.gcloud ~/.kube ~/.ssh
|
|
|
|
- name: Create Test Files for Sandbox Permissions Tests
|
|
run: |
|
|
touch ~/.aws/credentials
|
|
touch ~/.gcloud/credentials.json
|
|
touch ~/.kube/config
|
|
touch ~/.ssh/id_rsa
|
|
touch ./.env
|
|
|
|
- name: Run Sandbox E2E Test
|
|
env:
|
|
E2E_ENV_SEEDED: "1"
|
|
GITHUB_TOKEN: pmg-e2e-canary
|
|
gh_token: pmg-e2e-canary
|
|
AWS_SECRET_ACCESS_KEY: pmg-e2e-canary
|
|
OP_SERVICE_ACCOUNT_TOKEN: pmg-e2e-canary
|
|
CLOUDFLARE_API_TOKEN: pmg-e2e-canary
|
|
TWINE_PASSWORD: pmg-e2e-canary
|
|
NPM_TOKEN: pmg-e2e-keep
|
|
NODE_AUTH_TOKEN: pmg-e2e-keep
|
|
run: pmg --sandbox --sandbox-enforce npm exec -- node test/sandbox-e2e.js
|
|
|
|
- name: Run Package Manager E2E Test
|
|
run: pmg --sandbox --sandbox-enforce npm exec -- node test/pm-e2e.js
|
|
|
|
sandbox-e2e-linux:
|
|
name: Sandbox E2E - Linux (Bubblewrap)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
PMG_SANDBOX_DRIVER: bubblewrap
|
|
steps:
|
|
- name: Checkout Source
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Enable corepack
|
|
run: corepack enable
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Setup PNPM
|
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
|
|
|
- name: Install Bubblewrap
|
|
run: sudo apt-get update && sudo apt-get install -y bubblewrap
|
|
|
|
- name: Verify Bubblewrap Installation
|
|
run: bwrap --version
|
|
|
|
- name: Build PMG
|
|
run: make
|
|
|
|
- name: Add pmg to PATH
|
|
run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
|
|
|
|
- name: Setup PMG
|
|
run: pmg setup install
|
|
|
|
- name: Create Test Directories for Sandbox Permissions Tests
|
|
run: mkdir -p ~/.aws ~/.gcloud ~/.kube ~/.ssh ~/.gnupg ~/.docker
|
|
|
|
- name: Create Test Files for Sandbox Permissions Tests
|
|
run: |
|
|
touch ~/.aws/credentials
|
|
touch ~/.gcloud/credentials.json
|
|
touch ~/.kube/config
|
|
touch ~/.ssh/id_rsa
|
|
touch ~/.gnupg/pubring.kbx
|
|
touch ~/.docker/config.json
|
|
touch ./.env
|
|
|
|
- name: Disable AppArmor for Bubblewrap
|
|
run: |
|
|
sudo systemctl stop apparmor
|
|
sudo systemctl disable apparmor
|
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
|
|
- name: Run Sandbox E2E Test
|
|
env:
|
|
E2E_ENV_SEEDED: "1"
|
|
GITHUB_TOKEN: pmg-e2e-canary
|
|
gh_token: pmg-e2e-canary
|
|
AWS_SECRET_ACCESS_KEY: pmg-e2e-canary
|
|
OP_SERVICE_ACCOUNT_TOKEN: pmg-e2e-canary
|
|
CLOUDFLARE_API_TOKEN: pmg-e2e-canary
|
|
TWINE_PASSWORD: pmg-e2e-canary
|
|
NPM_TOKEN: pmg-e2e-keep
|
|
NODE_AUTH_TOKEN: pmg-e2e-keep
|
|
run: pmg --sandbox --sandbox-enforce npm exec -- node test/sandbox-e2e.js
|
|
|
|
- name: Run Package Manager E2E Test
|
|
run: pmg --sandbox --sandbox-enforce npm exec -- node test/pm-e2e.js
|
|
|
|
sandbox-e2e-linux-landlock:
|
|
name: Sandbox E2E - Linux (Landlock)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
PMG_SANDBOX_DRIVER: landlock
|
|
PMG_LANDLOCK_E2E: "1"
|
|
# Redirect npm's cache into /tmp so it sits outside any pre-existing
|
|
# state in /home/runner/.npm (which setup-node / the runner image may
|
|
# have populated with state the sandbox policy doesn't account for).
|
|
# The npm-restrictive profile already grants /tmp/** read+write.
|
|
npm_config_cache: /tmp/npm-cache
|
|
steps:
|
|
- name: Checkout Source
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Enable corepack
|
|
run: corepack enable
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Setup PNPM
|
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
|
|
|
- name: Build PMG
|
|
run: make
|
|
|
|
- name: Add pmg to PATH
|
|
run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
|
|
|
|
- name: Setup PMG
|
|
run: pmg setup install
|
|
|
|
- name: Create Test Directories for Sandbox Permissions Tests
|
|
run: mkdir -p ~/.aws ~/.gcloud ~/.kube ~/.ssh ~/.gnupg ~/.docker
|
|
|
|
- name: Create Test Files for Sandbox Permissions Tests
|
|
run: |
|
|
touch ~/.aws/credentials
|
|
touch ~/.gcloud/credentials.json
|
|
touch ~/.kube/config
|
|
touch ~/.ssh/id_rsa
|
|
touch ~/.gnupg/pubring.kbx
|
|
touch ~/.docker/config.json
|
|
touch ./.env
|
|
|
|
- name: Disable AppArmor for User Namespaces
|
|
run: |
|
|
sudo systemctl stop apparmor
|
|
sudo systemctl disable apparmor
|
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
|
|
- name: Verify Landlock Available
|
|
run: |
|
|
if [ ! -d /sys/kernel/security/landlock ] && ! grep -q landlock /proc/kallsyms 2>/dev/null; then
|
|
echo "Landlock not detected by sysfs probe (continuing — driver will fail loudly if unavailable)"
|
|
fi
|
|
uname -a
|
|
|
|
- name: Run Landlock Helper E2E Tests (Go)
|
|
run: go test -count=1 -v -run TestLandlockHelper ./sandbox/platform/...
|
|
|
|
# The npm leaf profile's npm_config_* env allow is what keeps the
|
|
# job-level npm_config_cache redirect alive.
|
|
- name: Run Sandbox E2E Test
|
|
env:
|
|
E2E_ENV_SEEDED: "1"
|
|
GITHUB_TOKEN: pmg-e2e-canary
|
|
gh_token: pmg-e2e-canary
|
|
AWS_SECRET_ACCESS_KEY: pmg-e2e-canary
|
|
OP_SERVICE_ACCOUNT_TOKEN: pmg-e2e-canary
|
|
CLOUDFLARE_API_TOKEN: pmg-e2e-canary
|
|
TWINE_PASSWORD: pmg-e2e-canary
|
|
NPM_TOKEN: pmg-e2e-keep
|
|
NODE_AUTH_TOKEN: pmg-e2e-keep
|
|
run: pmg --sandbox --sandbox-enforce npm exec -- node test/sandbox-e2e.js
|
|
|
|
- name: Run Package Manager E2E Test
|
|
run: pmg --sandbox --sandbox-enforce npm exec -- node test/pm-e2e.js
|
|
|
|
# Linux system-wide install: root install, ENV PATH (Docker-style), non-root user,
|
|
# managed config, and remove. Profile.d login shells are covered by sourcing the snippet.
|
|
e2e-system-install:
|
|
name: PMG E2E - System Install (Linux)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
steps:
|
|
- name: Checkout Source
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Build PMG
|
|
run: make
|
|
|
|
- name: Reject private PMG binary for system install
|
|
run: |
|
|
sudo mkdir -p /root/pmg-private
|
|
sudo cp bin/pmg /root/pmg-private/pmg
|
|
sudo chmod 700 /root/pmg-private /root/pmg-private/pmg
|
|
if sudo /root/pmg-private/pmg setup install --system; then
|
|
echo "ERROR: system install accepted a non-world-executable binary"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: private binary rejected"
|
|
|
|
- name: Reject user-owned PMG binary for system install
|
|
run: |
|
|
mkdir -p "$HOME/pmg-user-writable"
|
|
cp bin/pmg "$HOME/pmg-user-writable/pmg"
|
|
chmod 755 "$HOME/pmg-user-writable/pmg"
|
|
if sudo "$HOME/pmg-user-writable/pmg" setup install --system; then
|
|
echo "ERROR: system install accepted a user-owned binary"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: user-owned binary rejected"
|
|
|
|
- name: Reject unreachable PMG binary for system install
|
|
run: |
|
|
# Binary is 0755 and root-owned, but sits under a 0700 dir: other
|
|
# users cannot traverse to it, so every shim would exit 127.
|
|
sudo mkdir -p /root/pmg-unreachable
|
|
sudo install -m 755 bin/pmg /root/pmg-unreachable/pmg
|
|
sudo chmod 700 /root/pmg-unreachable
|
|
if sudo /root/pmg-unreachable/pmg setup install --system; then
|
|
echo "ERROR: system install accepted a binary under a non-searchable directory"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: unreachable binary rejected"
|
|
|
|
- name: Install PMG system-wide
|
|
run: |
|
|
# GitHub runners ship /usr/local/bin world-writable; system install
|
|
# (correctly) requires a root-owned, non-world-writable dir for the
|
|
# shared binary. Normalize to the standard production perms first.
|
|
ls -ld /usr/local/bin
|
|
sudo chown root:root /usr/local/bin
|
|
sudo chmod 755 /usr/local/bin
|
|
sudo install -m 755 bin/pmg /usr/local/bin/pmg
|
|
sudo pmg setup install --system
|
|
|
|
- name: Verify system install artifacts
|
|
run: |
|
|
test -f /etc/safedep/pmg/config.yml
|
|
test -f /etc/profile.d/pmg.sh
|
|
grep -q '/usr/local/lib/pmg/bin' /etc/profile.d/pmg.sh
|
|
for shim in npm pip pip3 pipx pnpm bun uv uvx yarn poetry npx pnpx; do
|
|
test -x "/usr/local/lib/pmg/bin/$shim" || { echo "Missing shim: $shim"; exit 1; }
|
|
done
|
|
|
|
- name: Root runs keep per-user state out of the invoking user's home
|
|
run: |
|
|
# GitHub runner sudo preserves HOME. Every sudo pmg run above used to
|
|
# create root-owned ~/.config/safedep for the runner user, which
|
|
# fail-closes all their later pmg/npm runs. Must run before any
|
|
# non-root pmg invocation legitimately creates that directory.
|
|
sudo sh -c 'echo "sudo sees HOME=$HOME"'
|
|
if [ -e "$HOME/.config/safedep" ]; then
|
|
echo "ERROR: root-created state leaked into $HOME/.config/safedep"
|
|
ls -laR "$HOME/.config/safedep"
|
|
exit 1
|
|
fi
|
|
sudo test -d /root/.config/safedep/pmg/logs
|
|
echo "SUCCESS: root state stayed under /root"
|
|
|
|
- name: PATH and profile.d activate shims
|
|
run: |
|
|
# Docker-style: non-login shells need PATH (or source profile.d)
|
|
export PATH="/usr/local/lib/pmg/bin:$PATH"
|
|
which npm | grep -q '/usr/local/lib/pmg/bin/npm'
|
|
source /etc/profile.d/pmg.sh
|
|
which npm | grep -q '/usr/local/lib/pmg/bin/npm'
|
|
|
|
- name: Managed config refuses CLI mutation
|
|
run: |
|
|
# Assert the refusal reason: a permission-denied brick (poisoned home)
|
|
# would also make config set fail and mask a regression.
|
|
if out=$(pmg config set dependency_cooldown.days 7 2>&1); then
|
|
echo "ERROR: config set should fail under system config"
|
|
exit 1
|
|
fi
|
|
echo "$out" | grep -qi 'globally managed' || { echo "ERROR: failed for the wrong reason:"; echo "$out"; exit 1; }
|
|
if out=$(sudo pmg config set dependency_cooldown.days 7 2>&1); then
|
|
echo "ERROR: config set should fail under system config even as root"
|
|
exit 1
|
|
fi
|
|
echo "$out" | grep -qi 'globally managed' || { echo "ERROR: root run failed for the wrong reason:"; echo "$out"; exit 1; }
|
|
echo "SUCCESS: managed config is locked"
|
|
|
|
- name: Doctor reports system install state
|
|
run: |
|
|
export PATH="/usr/local/lib/pmg/bin:$PATH"
|
|
out=$(pmg setup doctor 2>&1 || true)
|
|
echo "$out"
|
|
echo "$out" | grep -q 'No aliases (system install)'
|
|
echo "$out" | grep -Eq 'Package managers resolve to System shim directory|System shim directory is in PATH'
|
|
|
|
- name: Non-root user interception via system shims
|
|
run: |
|
|
sudo useradd -m pmgtest || true
|
|
# Pass runner PATH so setup-node's npm remains visible after FilterPMGFromPath.
|
|
sudo -u pmgtest env "PATH=/usr/local/lib/pmg/bin:$PATH" HOME=/home/pmgtest bash -lc '
|
|
set -euo pipefail
|
|
# GH runners export XDG_CONFIG_HOME=/home/runner/.config and it
|
|
# leaks through sudo -u, so pmg would resolve the runner user
|
|
# config dir and fail on its runner-owned log file.
|
|
export XDG_CONFIG_HOME="$HOME/.config"
|
|
echo "HOME=$HOME XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
|
|
which npm | grep -q /usr/local/lib/pmg/bin/npm
|
|
mkdir -p "$HOME/sys-e2e" && cd "$HOME/sys-e2e"
|
|
npm init -y
|
|
if npm install --no-cache --prefer-online safedep-test-pkg@0.1.3; then
|
|
echo "ERROR: safedep-test-pkg was not blocked for non-root user"
|
|
exit 1
|
|
fi
|
|
if [ -d node_modules/safedep-test-pkg ]; then
|
|
echo "ERROR: safedep-test-pkg present in node_modules"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: non-root user blocked malicious package via system shims"
|
|
'
|
|
|
|
- name: Remove system install
|
|
run: |
|
|
sudo pmg setup remove --system --config-file
|
|
test ! -e /etc/profile.d/pmg.sh
|
|
test ! -e /etc/safedep/pmg/config.yml
|
|
test ! -d /usr/local/lib/pmg/bin
|
|
echo "SUCCESS: system install removed"
|