Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
112ee5f
feat: add sfw input to wrap vp install with Socket Firewall Free
fengmk2 May 26, 2026
fafe07a
test(ci): add test-sfw-blocks-malicious job using lodahs canary
fengmk2 May 26, 2026
c4df421
ci: limit test-sfw to ubuntu-latest, document sfw rustls TLS limitation
fengmk2 May 26, 2026
f5f2b16
feat(sfw): fall back to plain vp install on non-Linux with a warning
fengmk2 May 26, 2026
b3e3a58
chore: point sfw non-Linux warning at setup-vp tracker issue
fengmk2 May 26, 2026
4d71ec2
docs: point README sfw fallback note at setup-vp#73 tracker
fengmk2 May 26, 2026
b0bece4
docs(ci): collapse sfw-free issue references to setup-vp#73 tracker
fengmk2 May 26, 2026
f0618a4
docs: point isSfwSupported comment at setup-vp#73 tracker
fengmk2 May 26, 2026
d2ed525
test(ci): add test-sfw-package-managers covering pnpm/npm/yarn/bun
fengmk2 May 26, 2026
f13d0ba
test(ci): fix yarn job + promote bun to required
fengmk2 May 26, 2026
9fbb519
test(ci): force Yarn nodeLinker=node-modules so verify step is uniform
fengmk2 May 26, 2026
e6024b3
test(ci): merge test-sfw and test-sfw-package-managers into one matrix
fengmk2 May 26, 2026
aa92f44
ci+docs: address code-review findings on the matrix-merge diff
fengmk2 May 26, 2026
381ef2b
ci+src: handle PR #72 review comments
fengmk2 May 26, 2026
2e0144f
ci: only test latest vp release in test-sfw matrix
fengmk2 May 26, 2026
65e179d
ci: also drop alpha from test-sfw-alpine and test-sfw-blocks-malicious
fengmk2 May 26, 2026
b6a100f
ci: drop single-value version axis from sfw jobs entirely
fengmk2 May 26, 2026
c69ca6f
ci+src: fix code-review findings on commits since aa92f44
fengmk2 May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,194 @@ jobs:
- name: Verify vp exec works
run: vp exec node -e "console.log('vp exec works in Alpine')"

test-sfw:
# On Linux: sfw wraps vp install end-to-end. Verify across all package
# managers vp auto-detects via lockfile (pnpm/npm/yarn/bun).
# On macOS / Windows: sfw is temporarily unsupported; the action emits a
# warning and falls back to plain `vp install` with no sfw binary
# downloaded. We verify the fallback once per non-Linux OS using the
# default package manager (pnpm) — the PM diversity matrix is Linux-only
# because the sfw wrap is Linux-only.
# Tracking: https://github.com/voidzero-dev/setup-vp/issues/73
# vp version is left at the action default (`latest`) — sfw is decoupled
# from vp's release channel. Other test jobs (test-cache-*,
# test-node-version, etc.) still cover the alpha channel for vp itself.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
package-manager: [pnpm, npm, yarn, bun]
exclude:
# Non-Linux only needs one fallback check per OS — sfw isn't
# invoked there, so PM diversity adds no coverage.
- { os: macos-latest, package-manager: npm }
- { os: macos-latest, package-manager: yarn }
- { os: macos-latest, package-manager: bun }
- { os: windows-latest, package-manager: npm }
- { os: windows-latest, package-manager: yarn }
- { os: windows-latest, package-manager: bun }
runs-on: ${{ matrix.os }}
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2

- name: Create test project for ${{ matrix.package-manager }}
shell: bash
run: |
case "${{ matrix.package-manager }}" in
pnpm) LOCKFILE=pnpm-lock.yaml; CONTENTS='' ;;
npm) LOCKFILE=package-lock.json; CONTENTS='{"name":"test-project","lockfileVersion":3}' ;;
yarn) LOCKFILE=yarn.lock; CONTENTS='' ;;
bun) LOCKFILE=bun.lock; CONTENTS='' ;;
*) echo "Unsupported package-manager: ${{ matrix.package-manager }}" >&2; exit 1 ;;
esac
mkdir -p test-project
cd test-project
echo '{"name":"test-project","private":true,"dependencies":{"is-odd":"^3.0.1"}}' > package.json
printf '%s' "$CONTENTS" > "$LOCKFILE"

- name: Configure Yarn .yarnrc.yml (Linux + yarn only)
if: matrix.package-manager == 'yarn' && runner.os == 'Linux'
# nodeLinker=node-modules: Yarn Berry defaults to Plug'n'Play, which
# makes plain `require()` from a non-yarn-wrapped node process fail.
# enableImmutableInstalls=false: Yarn Berry auto-enables immutable
# installs under CI, which makes the bootstrap from an empty
# yarn.lock fail with YN0028. Setting it here (instead of via the
# YARN_ENABLE_IMMUTABLE_INSTALLS env var) survives any future
# env-sanitization vp might apply to spawned subprocesses.
shell: bash
run: |
{
echo "nodeLinker: node-modules"
echo "enableImmutableInstalls: false"
} > test-project/.yarnrc.yml

- name: Setup Vite+ with sfw + ${{ matrix.package-manager }}
uses: ./
with:
sfw: true
run-install: |
- cwd: test-project
cache: false

- name: Verify sfw is on PATH (Linux only)
if: runner.os == 'Linux'
run: sfw --version

- name: Verify sfw fallback on non-Linux (action did not install sfw)
if: runner.os != 'Linux'
shell: bash
# Check the exact path getSfwBinDir() would have created rather than
# `command -v sfw`, so a runner image that happens to ship sfw
# globally (or a leftover from a prior self-hosted job) doesn't
# false-fail this assertion.
run: |
if [ -e "$RUNNER_TEMP/sfw-bin/sfw" ] || [ -e "$RUNNER_TEMP/sfw-bin/sfw.exe" ]; then
echo "ERROR: expected the action to skip the sfw download on ${{ runner.os }}, but $RUNNER_TEMP/sfw-bin/sfw[.exe] exists"
exit 1
fi
echo "OK: action did not install sfw; fallback to plain vp install confirmed"

- name: Verify dependency installed via ${{ matrix.package-manager }}
working-directory: test-project
run: vp exec node -e "console.log(require('is-odd')(3))"

test-sfw-alpine:
# vp version is left at the action default (`latest`) — sfw's musl asset
# selection is decoupled from vp's release channel.
# NOTE: if this job is later re-matrixed (alpha+latest, multiple alpine
# versions, etc.), restore `strategy: { fail-fast: false }` so a flake in
# one shard doesn't cancel the others.
runs-on: ubuntu-latest
container:
image: alpine:3.23
steps:
- name: Install Alpine dependencies
run: apk add --no-cache bash curl gcompat libstdc++

- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2

- name: Create test project with a real dependency
run: |
mkdir -p test-project
cd test-project
echo '{"name":"test-project","private":true,"dependencies":{"is-odd":"^3.0.1"}}' > package.json

- name: Setup Vite+ with sfw (musl)
uses: ./
with:
sfw: true
run-install: |
- cwd: test-project
cache: false

- name: Verify sfw is on PATH (musl)
run: sfw --version

- name: Verify dependency installed under sfw (musl)
working-directory: test-project
run: vp exec node -e "console.log(require('is-odd')(3))"

test-sfw-blocks-malicious:
# Verifies sfw actually intercepts a known-malicious package, not just
# that it wraps the install. Uses `lodahs` (lodash typosquat), the same
# canary SocketDev's own workflows use:
# https://github.com/SocketDev/bun-security-scanner/blob/main/.github/workflows/test.yml
# If this job ever stops blocking, either sfw is misconfigured or the
# canary itself has been delisted — swap it for another Socket-flagged
# package from https://socket.dev/blog/category/threat-research.
# vp version is left at the action default (`latest`) — sfw block behavior
# is decoupled from vp's release channel.
# NOTE: if this job is later re-matrixed, restore
# `strategy: { fail-fast: false }` so a flake in one shard doesn't cancel
# the others.
runs-on: ubuntu-latest
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2

- name: Create test project with a benign dependency
shell: bash
run: |
mkdir -p test-project
cd test-project
echo '{"name":"test-project","private":true,"dependencies":{"is-odd":"^3.0.1"}}' > package.json

- name: Setup Vite+ with sfw and install benign dep
uses: ./
with:
sfw: true
run-install: |
- cwd: test-project
cache: false

- name: Assert sfw blocks malicious package (lodahs typosquat of lodash)
shell: bash
working-directory: test-project
# Exit code alone isn't sufficient: a non-zero exit from npm 404,
# network blip, or vp crash would also produce a false positive. We
# also require the literal sfw block-line for lodahs in the combined
# output so an unrelated failure doesn't get reported as "sfw blocked
# it". The block-line format observed in CI is:
# " - blocked npm package: name: lodahs; version: ...; reason: ..."
# The banner "Protected by Socket Firewall" and the "=== Socket
# Firewall ===" header are emitted on EVERY sfw invocation, so neither
# of those is a usable marker — use the unique "blocked npm package:
# name: lodahs" line instead.
run: |
set +e
OUTPUT=$(sfw vp install lodahs 2>&1)
CODE=$?
set -e
printf '%s\n' "$OUTPUT"
if [ "$CODE" -eq 0 ]; then
echo "::error::sfw failed to block lodahs — install exited 0"
exit 1
fi
if ! printf '%s' "$OUTPUT" | grep -qF -- "blocked npm package: name: lodahs"; then
echo "::error::sfw vp install exited $CODE but the lodahs block-line was not in the output — likely failed for a non-sfw reason (canary delisted, network blip, vp crash, or sfw output format changed). Swap the canary if Socket has delisted lodahs, or update the marker grep if sfw's block-line format changed."
exit 1
fi
echo "OK: sfw blocked lodahs (exit $CODE, block-line found)"

Comment thread
fengmk2 marked this conversation as resolved.
build:
runs-on: ubuntu-latest
steps:
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ GitHub Action to set up [Vite+](https://viteplus.dev) (`vp`) with dependency cac
- Optionally set up a specific Node.js version via `vp env use`
- Cache project dependencies with auto-detection of lock files
- Optionally run `vp install` after setup
- Optionally wrap `vp install` with [Socket Firewall Free (`sfw`)](https://docs.socket.dev/docs/socket-firewall-free) to block malicious dependencies
- Support for all major package managers (npm, pnpm, yarn, bun)

## Usage
Expand Down Expand Up @@ -135,6 +136,24 @@ steps:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

### With Socket Firewall Free (sfw)

Set `sfw: true` to wrap `vp install` with [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free). The action downloads the matching `sfw` binary from the upstream [releases](https://github.com/SocketDev/sfw-free/releases) (auto-detected per OS/arch, with musl support on Alpine) and runs `sfw vp install …` so the underlying npm / pnpm / yarn fetches are inspected before packages are installed:

```yaml
steps:
- uses: actions/checkout@v6
- uses: voidzero-dev/setup-vp@v1
with:
sfw: true
run-install: true
```

`sfw` is only applied when `run-install` is enabled; other `vp` commands (e.g. `vp env use`, `vp --version`) run unwrapped.

> [!IMPORTANT]
> **Linux-only for now.** `sfw` ships a self-signed CA whose certificate has an empty Extended Key Usage extension. Strict TLS stacks like rustls (used by `vp`) reject it as `UnknownIssuer`, so `vp install` fails the TLS handshake on macOS / Windows. To keep `sfw: true` safe to set unconditionally in cross-platform workflows, the action **falls back to plain `vp install` with a warning on non-Linux platforms** — it does not download the `sfw` binary there. The platform check will be relaxed once the upstream work tracked in [voidzero-dev/setup-vp#73](https://github.com/voidzero-dev/setup-vp/issues/73) lands.

### Alpine Container

Alpine Linux uses musl libc instead of glibc. Install compatibility packages before using the action:
Expand Down Expand Up @@ -178,6 +197,7 @@ jobs:
| `node-version-file` | Path to file containing Node.js version (`.nvmrc`, `.node-version`, `.tool-versions`, `package.json`) | No | |
| `working-directory` | Project directory used for relative paths, lockfile auto-detection, environment checks, and default install | No | Workspace root |
| `run-install` | Run `vp install` after setup. Accepts boolean or YAML object with `cwd`/`args` | No | `true` |
| `sfw` | Wrap `vp install` with [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free) (`sfw`) | No | `false` |
| `cache` | Enable caching of project dependencies | No | `false` |
| `cache-dependency-path` | Path to lock file for cache key generation | No | Auto-detected |
| `registry-url` | Optional registry to set up for auth. Sets the registry in `.npmrc` and reads auth from `NODE_AUTH_TOKEN` | No | |
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ inputs:
description: "Run `vp install` after setup. Accepts boolean or YAML object with cwd/args."
required: false
default: "true"
sfw:
description: "Wrap `vp install` with Socket Firewall Free (sfw) to block malicious dependency fetches. Currently Linux-only: on macOS/Windows the action emits a warning and falls back to plain `vp install` (no sfw binary downloaded). Tracking: https://github.com/voidzero-dev/setup-vp/issues/73. See also https://docs.socket.dev/docs/socket-firewall-free."
required: false
default: "false"
node-version:
description: "Node.js version to install via `vp env use`. Defaults to Node.js latest LTS version."
required: false
Expand Down
124 changes: 62 additions & 62 deletions dist/index.mjs

Large diffs are not rendered by default.

33 changes: 31 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { saveState, getState, setFailed, info, setOutput, warning } from "@actio
import { exec, getExecOutput } from "@actions/exec";
import { getInputs } from "./inputs.js";
import { installVitePlus } from "./install-viteplus.js";
import { installSfw, isMuslLinux, isSfwSupported } from "./install-sfw.js";
import { runViteInstall } from "./run-install.js";
import { restoreCache } from "./cache-restore.js";
import { saveCache } from "./cache-save.js";
Expand Down Expand Up @@ -43,9 +44,37 @@ async function runMain(inputs: Inputs): Promise<void> {
await restoreCache(inputs);
}

// Step 6: Run vp install if requested
// Step 6: Install Socket Firewall Free if requested (must run before vp install).
// sfw is currently only supported on Linux on architectures with a published
// sfw asset (see isSfwSupported); other combinations fall back to plain
// `vp install`. Whenever `sfw: true` is set but sfw won't actually be
// invoked, emit a clear log message so the no-op is visible.
let effectiveSfw = inputs.sfw;
if (inputs.sfw) {
const env = `process.platform=${process.platform}, process.arch=${process.arch}, musl=${isMuslLinux()}`;
const supported = isSfwSupported();
const needsInstall = inputs.runInstall.length > 0;
if (!supported && needsInstall) {
warning(
`sfw is temporarily not supported on this runner (${env}); falling back to plain \`vp install\`. Track upstream: https://github.com/voidzero-dev/setup-vp/issues/73`,
);
effectiveSfw = false;
} else if (!supported && !needsInstall) {
info(
`sfw was requested but is not supported on this runner (${env}); no sfw binary will be downloaded. Track upstream: https://github.com/voidzero-dev/setup-vp/issues/73`,
);
effectiveSfw = false;
} else if (supported && !needsInstall) {
info("sfw was requested but `run-install` is disabled; no sfw binary will be downloaded.");
}
}
Comment thread
fengmk2 marked this conversation as resolved.
if (effectiveSfw && inputs.runInstall.length > 0) {
await installSfw();
}
Comment thread
fengmk2 marked this conversation as resolved.

// Step 7: Run vp install if requested
if (inputs.runInstall.length > 0) {
await runViteInstall(inputs);
await runViteInstall({ ...inputs, sfw: effectiveSfw });
}

// Print version info at the end
Expand Down
13 changes: 13 additions & 0 deletions src/inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe("getInputs", () => {
nodeVersionFile: undefined,
workingDirectory: undefined,
runInstall: [],
sfw: false,
cache: false,
cacheDependencyPath: undefined,
});
Expand Down Expand Up @@ -106,6 +107,18 @@ describe("getInputs", () => {
expect(inputs.cache).toBe(true);
});

it("should parse sfw input", () => {
vi.mocked(getInput).mockReturnValue("");
vi.mocked(getBooleanInput).mockImplementation((name) => {
if (name === "sfw") return true;
return false;
});

const inputs = getInputs();

expect(inputs.sfw).toBe(true);
});

it("should parse node-version-file input", () => {
vi.mocked(getInput).mockImplementation((name) => {
if (name === "node-version-file") return ".nvmrc";
Expand Down
1 change: 1 addition & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function getInputs(): Inputs {
nodeVersionFile: getInput("node-version-file") || undefined,
workingDirectory: getInput("working-directory") || undefined,
runInstall: parseRunInstall(getInput("run-install")),
sfw: getBooleanInput("sfw"),
cache: getBooleanInput("cache"),
cacheDependencyPath: getInput("cache-dependency-path") || undefined,
registryUrl: getInput("registry-url") || undefined,
Expand Down
66 changes: 66 additions & 0 deletions src/install-sfw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect } from "vite-plus/test";
import { getSfwAssetName, isSfwSupported } from "./install-sfw.js";

describe("getSfwAssetName", () => {
it("returns macOS arm64 asset", () => {
expect(getSfwAssetName("darwin", "arm64", false)).toBe("sfw-free-macos-arm64");
});

it("returns macOS x64 asset", () => {
expect(getSfwAssetName("darwin", "x64", false)).toBe("sfw-free-macos-x86_64");
});

it("ignores isMusl on darwin", () => {
expect(getSfwAssetName("darwin", "arm64", true)).toBe("sfw-free-macos-arm64");
expect(getSfwAssetName("darwin", "x64", true)).toBe("sfw-free-macos-x86_64");
});

it("returns Linux glibc arm64 asset", () => {
expect(getSfwAssetName("linux", "arm64", false)).toBe("sfw-free-linux-arm64");
});

it("returns Linux glibc x64 asset", () => {
expect(getSfwAssetName("linux", "x64", false)).toBe("sfw-free-linux-x86_64");
});

it("returns Linux musl arm64 asset", () => {
expect(getSfwAssetName("linux", "arm64", true)).toBe("sfw-free-musl-linux-arm64");
});

it("returns Linux musl x64 asset", () => {
expect(getSfwAssetName("linux", "x64", true)).toBe("sfw-free-musl-linux-x86_64");
});

it("returns Windows arm64 asset", () => {
expect(getSfwAssetName("win32", "arm64", false)).toBe("sfw-free-windows-arm64.exe");
});

it("returns Windows x64 asset", () => {
expect(getSfwAssetName("win32", "x64", false)).toBe("sfw-free-windows-x86_64.exe");
});

it("ignores isMusl on win32", () => {
expect(getSfwAssetName("win32", "x64", true)).toBe("sfw-free-windows-x86_64.exe");
});

it("throws on unsupported platform", () => {
expect(() => getSfwAssetName("freebsd" as NodeJS.Platform, "x64", false)).toThrow(
/freebsd\/x64/,
);
});

it("throws on unsupported arch", () => {
expect(() => getSfwAssetName("linux", "ia32", false)).toThrow(/linux\/ia32/);
});

it("includes libc in error message for unsupported Linux arch", () => {
expect(() => getSfwAssetName("linux", "ia32", true)).toThrow(/musl/);
expect(() => getSfwAssetName("linux", "ia32", false)).toThrow(/glibc/);
});
});

describe("isSfwSupported", () => {
it("returns true on Linux, false elsewhere (matches current platform)", () => {
expect(isSfwSupported()).toBe(process.platform === "linux");
Comment on lines +63 to +64
});
});
Loading
Loading