pmsec 0.11.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # pmsec (Node)
2
2
 
3
3
  Zero-config install-time supply-chain hardening for npm, pnpm, yarn, bun,
4
- cargo, mise, and uv. One command flips on every safe-by-default knob each
4
+ cargo, mise, uv, and bundler. One command flips on every safe-by-default knob each
5
5
  package manager exposes — install cooldown, signature trust policy, lockfile
6
6
  re-verification, build-script attestation, and more.
7
7
 
@@ -38,7 +38,7 @@ Zero runtime dependencies, ESM, requires Node 22+.
38
38
  | `pmsec --disable` | Remove every key the bundle set; other keys in the file are preserved. |
39
39
  | `pmsec --version` | Print the installed pmsec version. |
40
40
 
41
- Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--days N` (override the 1-day default), `--force` (overwrite stricter existing cooldowns; default is monotonic), `--json`. `--check` and `--disable` are mutually exclusive.
41
+ Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv,bundler`, `--days N` (override the 1-day default), `--force` (overwrite stricter existing cooldowns; default is monotonic), `--json`. `--check` and `--disable` are mutually exclusive.
42
42
 
43
43
  When the target file is owned by another user (typical: `~/.npmrc` left
44
44
  root-owned by an old `sudo npm config set`), the write fails with `EACCES`
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "0.11.0",
3
+ "version": "0.14.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Zero-config install-time supply-chain hardening for npm, pnpm, yarn, bun, cargo, mise, and uv.",
7
+ "description": "Zero-config install-time supply-chain hardening for npm, pnpm, yarn, bun, cargo, mise, uv, and bundler.",
8
8
  "homepage": "https://github.com/HikaruEgashira/pmsec#readme",
9
9
  "repository": {
10
10
  "type": "git",
package/src/cli.mjs CHANGED
@@ -9,8 +9,9 @@ import * as bun from "./tools/bun.mjs";
9
9
  import * as cargo from "./tools/cargo.mjs";
10
10
  import * as mise from "./tools/mise.mjs";
11
11
  import * as uv from "./tools/uv.mjs";
12
+ import * as bundler from "./tools/bundler.mjs";
12
13
 
13
- const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv];
14
+ const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv, bundler];
14
15
  // Default cooldown for the hardening bundle. Override per-invocation with
15
16
  // `--days N`; the default tracks the safest value we'd recommend.
16
17
  const BUNDLE_DAYS = 1;
@@ -21,15 +22,15 @@ export const VERSION = JSON.parse(
21
22
  const USAGE = `pmsec [options]
22
23
 
23
24
  Zero-config install-time supply-chain hardening across npm, pnpm, yarn,
24
- bun, cargo, mise, uv. Default action enables every safe-by-default key
25
- each tool exposes (cooldown, audit-level, trust-policy, hardened mode,
25
+ bun, cargo, mise, uv, bundler. Default action enables every safe-by-default
26
+ key each tool exposes (cooldown, audit-level, trust-policy, hardened mode,
26
27
  attestation re-verification, ...). No knobs.
27
28
 
28
29
  Options:
29
30
  --check Verify the bundle is in place (exit 1 if anything missing)
30
31
  --disable Remove the hardening bundle from selected tools
31
- --doctor Diagnose effective paths/owner/uid (read-only; for MDM debugging)
32
- --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
32
+ --doctor Diagnose effective paths/owner/uid (read-only; for unattended-deployment debugging)
33
+ --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv,bundler)
33
34
  --days N Override cooldown days (default 1)
34
35
  --force Overwrite stricter existing cooldowns (otherwise enable is monotonic)
35
36
  --json Emit JSON output
@@ -135,9 +136,9 @@ async function runCheck(targets, { json, days }, ctx, out, err) {
135
136
 
136
137
  // `pmsec doctor` runs read-only and reports the same path resolution that
137
138
  // enable/check/disable would do, plus identity (uid/euid) and parent-dir
138
- // writability — the smallest set of facts an MDM operator needs to diagnose
139
- // "pmsec ran but wrote to nowhere" (root's $HOME) or "wrote a file no one can
140
- // read" (chown failed under SIP/SELinux). Never mutates the filesystem.
139
+ // writability — the smallest set of facts an operator needs to diagnose
140
+ // "pmsec ran but wrote to nowhere" (root's $HOME) or "wrote a file no one
141
+ // can read" (chown failed under SIP/SELinux). Never mutates the filesystem.
141
142
  function probePath(p, uid) {
142
143
  const parent = dirname(p);
143
144
  let exists = false, writable = false, parentExists = false, owner = null;
package/src/tools/bun.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { bunConfigPath } from "../util/paths.mjs";
2
2
  import { readSafe, writeAtomic } from "../util/io.mjs";
3
3
  import { readKey, setKey, removeKey } from "../util/lines.mjs";
4
+ import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
4
5
  import { buildPreflight } from "../util/version.mjs";
5
6
 
6
7
  export const name = "bun";
@@ -8,7 +9,9 @@ export const key = "minimumReleaseAge";
8
9
  export const section = "install";
9
10
  export const docs = "https://bun.com/docs/runtime/bunfig#install";
10
11
  export const minBin = [1, 3, 0];
11
- export const extras = [];
12
+ export const extras = [
13
+ { key: "ignoreScripts", expected: "true", line: "ignoreScripts = true", section },
14
+ ];
12
15
 
13
16
  export function path(ctx) { return bunConfigPath(ctx.env, ctx.home); }
14
17
 
@@ -20,12 +23,17 @@ export async function read(ctx) {
20
23
  const raw = await readSafe(p);
21
24
  const value = readKey(raw, key, { section });
22
25
  const seconds = value === null ? null : Number(value);
23
- return { path: p, configured: value, days: seconds === null || Number.isNaN(seconds) ? null : Math.floor(seconds / 86400), extras: [] };
26
+ return {
27
+ path: p, configured: value,
28
+ days: seconds === null || Number.isNaN(seconds) ? null : Math.floor(seconds / 86400),
29
+ extras: readExtras(raw, extras),
30
+ };
24
31
  }
25
32
 
26
33
  export async function write(days, ctx) {
27
34
  const p = path(ctx);
28
- const text = setKey(await readSafe(p), key, `${key} = ${days * 86400}`, { section });
35
+ let text = setKey(await readSafe(p), key, `${key} = ${days * 86400}`, { section });
36
+ text = applyExtras(text, extras);
29
37
  await writeAtomic(p, text);
30
38
  return { path: p };
31
39
  }
@@ -33,7 +41,9 @@ export async function write(days, ctx) {
33
41
  export async function unset(ctx) {
34
42
  const p = path(ctx);
35
43
  const before = await readSafe(p);
36
- const { text: after, removed } = removeKey(before, key, { section });
37
- if (removed) await writeAtomic(p, after);
44
+ const cooldown = removeKey(before, key, { section });
45
+ const ex = removeExtras(cooldown.text, extras);
46
+ const removed = cooldown.removed || ex.removed;
47
+ if (removed) await writeAtomic(p, ex.text);
38
48
  return { path: p, removed };
39
49
  }
@@ -0,0 +1,46 @@
1
+ import { bundleConfigPath } from "../util/paths.mjs";
2
+ import { readSafe, writeAtomic } from "../util/io.mjs";
3
+ import { readKey, setKey, removeKey } from "../util/lines.mjs";
4
+ import { buildPreflight } from "../util/version.mjs";
5
+
6
+ export const name = "bundler";
7
+ export const key = "BUNDLE_COOLDOWN";
8
+ export const docs = "https://bundler.io/man/bundle-config.1.html";
9
+ export const minBin = [4, 0, 13];
10
+ export const extras = [];
11
+ const SEP = ":";
12
+
13
+ export function path(ctx) { return bundleConfigPath(ctx.env, ctx.home); }
14
+
15
+ export const preflight = buildPreflight(name, minBin,
16
+ "BUNDLE_COOLDOWN is silently ignored. Upgrade bundler (v4.0.13+) to enforce the cooldown.");
17
+
18
+ // bundler stores the cooldown as a plain integer number of days, quoted in the
19
+ // YAML config (e.g. `BUNDLE_COOLDOWN: "7"`). Accept it with or without quotes.
20
+ function parseDays(value) {
21
+ if (value === null) return null;
22
+ const m = value.match(/^"?\s*(\d+)\s*"?$/);
23
+ return m ? Number(m[1]) : null;
24
+ }
25
+
26
+ export async function read(ctx) {
27
+ const p = path(ctx);
28
+ const raw = await readSafe(p);
29
+ const value = readKey(raw, key, { sep: SEP });
30
+ return { path: p, configured: value, days: parseDays(value), extras: [] };
31
+ }
32
+
33
+ export async function write(days, ctx) {
34
+ const p = path(ctx);
35
+ const text = setKey(await readSafe(p), key, `${key}: "${days}"`, { sep: SEP });
36
+ await writeAtomic(p, text);
37
+ return { path: p };
38
+ }
39
+
40
+ export async function unset(ctx) {
41
+ const p = path(ctx);
42
+ const before = await readSafe(p);
43
+ const { text: after, removed } = removeKey(before, key, { sep: SEP });
44
+ if (removed) await writeAtomic(p, after);
45
+ return { path: p, removed };
46
+ }
@@ -9,7 +9,8 @@ export const section = "settings";
9
9
  export const docs = "https://mise.jdx.dev/configuration/settings.html#minimum_release_age";
10
10
  export const minBin = [2026, 4, 22];
11
11
  export const extras = [
12
- { key: "paranoid", expected: "true", line: "paranoid = true", section }
12
+ { key: "paranoid", expected: "true", line: "paranoid = true", section },
13
+ { key: "gpg_verify", expected: "true", line: "gpg_verify = true", section },
13
14
  ];
14
15
 
15
16
  export function path(ctx) { return miseConfigPath(ctx.env, ctx.home, ctx.platform); }
package/src/tools/npm.mjs CHANGED
@@ -9,7 +9,11 @@ export const key = "min-release-age";
9
9
  export const docs = "https://docs.npmjs.com/cli/v11/using-npm/config#min-release-age";
10
10
  export const minBin = [11, 10, 0];
11
11
  export const extras = [
12
- { key: "audit-level", expected: "high", line: "audit-level=high" }
12
+ { key: "audit-level", expected: "high", line: "audit-level=high" },
13
+ { key: "allow-git", expected: "root", line: "allow-git=root" },
14
+ { key: "allow-remote", expected: "root", line: "allow-remote=root" },
15
+ { key: "allow-file", expected: "root", line: "allow-file=root" },
16
+ { key: "allow-directory", expected: "root", line: "allow-directory=root" },
13
17
  ];
14
18
 
15
19
  export function path(ctx) { return npmrcPath(ctx.env, ctx.home); }
package/src/tools/uv.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  import { uvConfigPath } from "../util/paths.mjs";
2
2
  import { readSafe, writeAtomic } from "../util/io.mjs";
3
3
  import { readKey, setKey, removeKey } from "../util/lines.mjs";
4
+ import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
4
5
  import { buildPreflight } from "../util/version.mjs";
5
6
 
6
7
  export const name = "uv";
7
8
  export const key = "exclude-newer";
8
9
  export const docs = "https://docs.astral.sh/uv/reference/settings/#exclude-newer";
9
10
  export const minBin = [0, 9, 17];
10
- export const extras = [];
11
+ export const extras = [
12
+ { key: "index-strategy", expected: '"first-index"', line: 'index-strategy = "first-index"' },
13
+ ];
11
14
 
12
15
  export function path(ctx) { return uvConfigPath(ctx.env, ctx.home, ctx.platform); }
13
16
 
@@ -26,12 +29,13 @@ export async function read(ctx) {
26
29
  const p = path(ctx);
27
30
  const raw = await readSafe(p);
28
31
  const value = readKey(raw, key);
29
- return { path: p, configured: value, days: parseDays(value), extras: [] };
32
+ return { path: p, configured: value, days: parseDays(value), extras: readExtras(raw, extras) };
30
33
  }
31
34
 
32
35
  export async function write(days, ctx) {
33
36
  const p = path(ctx);
34
- const text = setKey(await readSafe(p), key, `${key} = "${days} days"`);
37
+ let text = setKey(await readSafe(p), key, `${key} = "${days} days"`);
38
+ text = applyExtras(text, extras);
35
39
  await writeAtomic(p, text);
36
40
  return { path: p };
37
41
  }
@@ -39,7 +43,9 @@ export async function write(days, ctx) {
39
43
  export async function unset(ctx) {
40
44
  const p = path(ctx);
41
45
  const before = await readSafe(p);
42
- const { text: after, removed } = removeKey(before, key);
43
- if (removed) await writeAtomic(p, after);
46
+ const cooldown = removeKey(before, key);
47
+ const ex = removeExtras(cooldown.text, extras);
48
+ const removed = cooldown.removed || ex.removed;
49
+ if (removed) await writeAtomic(p, ex.text);
44
50
  return { path: p, removed };
45
51
  }
@@ -10,7 +10,8 @@ export const docs = "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate"
10
10
  export const minBin = [4, 10, 0];
11
11
  const SEP = ":";
12
12
  export const extras = [
13
- { key: "enableHardenedMode", expected: "true", line: "enableHardenedMode: true", sep: SEP }
13
+ { key: "enableHardenedMode", expected: "true", line: "enableHardenedMode: true", sep: SEP },
14
+ { key: "enableScripts", expected: "false", line: "enableScripts: false", sep: SEP }
14
15
  ];
15
16
 
16
17
  export function path(ctx) { return yarnrcPath(ctx.env, ctx.home); }
@@ -42,6 +42,15 @@ export function cargoConfigPath(env = process.env, home = homedir()) {
42
42
  return join(home, ".cargo", "config.toml");
43
43
  }
44
44
 
45
+ // Bundler's global config. BUNDLE_USER_CONFIG points at the file directly;
46
+ // BUNDLE_USER_HOME points at the home dir (config lives at <home>/config).
47
+ // Both default to ~/.bundle, matching bundler's own resolution order.
48
+ export function bundleConfigPath(env = process.env, home = homedir()) {
49
+ if (env.BUNDLE_USER_CONFIG) return env.BUNDLE_USER_CONFIG;
50
+ if (env.BUNDLE_USER_HOME) return join(env.BUNDLE_USER_HOME, "config");
51
+ return join(home, ".bundle", "config");
52
+ }
53
+
45
54
  export function miseConfigPath(env = process.env, home = homedir(), platform = process.platform) {
46
55
  if (env.MISE_GLOBAL_CONFIG_FILE) return env.MISE_GLOBAL_CONFIG_FILE;
47
56
  if (platform === "win32") {
@@ -34,8 +34,9 @@ export function gte(v, target) {
34
34
  }
35
35
 
36
36
  export function buildPreflight(name, minBin, suffix) {
37
- return () => {
38
- const v = detectVersion(name);
37
+ const overrideKey = `PMSEC_${name.toUpperCase()}_VERSION`;
38
+ return (ctx) => {
39
+ const v = detectVersion(name, ["--version"], { env: ctx?.env, overrideKey });
39
40
  if (v === null) return { ok: true, message: null };
40
41
  if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
41
42
  return { ok: true, warn: true, version: v.raw,