pmsec 0.4.1 → 0.5.1

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.
Files changed (3) hide show
  1. package/README.md +15 -16
  2. package/package.json +1 -1
  3. package/src/cli.mjs +48 -37
package/README.md CHANGED
@@ -1,23 +1,22 @@
1
1
  # pmsec (Node)
2
2
 
3
- Cross-platform CLI that inspects and applies install-time cooldown settings
4
- (npm `min-release-age`, pnpm `minimum-release-age`, yarn `npmMinimalAgeGate`,
5
- bun `minimumReleaseAge`, cargo `minimum-release-age`, mise `minimum_release_age`,
6
- uv `exclude-newer`) so freshly-published, possibly-malicious packages can't
7
- land in your machine within hours of upload.
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
5
+ package manager exposes — install cooldown, signature trust policy, lockfile
6
+ re-verification, build-script attestation, and more.
8
7
 
9
8
  ## Install
10
9
 
11
10
  ```bash
12
- npx pmsec check --min 7
13
- npx pmsec set 7
14
- npx pmsec unset
11
+ npx pmsec enable
12
+ npx pmsec check
13
+ npx pmsec disable
15
14
  ```
16
15
 
17
16
  ```bash
18
- uvx pmsec check --min 7
19
- uvx pmsec set 7
20
- uvx pmsec unset
17
+ uvx pmsec enable
18
+ uvx pmsec check
19
+ uvx pmsec disable
21
20
  ```
22
21
 
23
22
  If your environment already enforces cooldown (or routes through a proxy
@@ -28,18 +27,18 @@ npx --registry=https://registry.npmjs.org/ --min-release-age=0 pmsec check
28
27
  uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
29
28
  ```
30
29
 
31
- Zero-config install-time supply-chain hardening (cooldown + audit-level + trust-policy + …) across npm, pnpm, yarn, bun, cargo, mise, uv. Zero runtime dependencies, ESM, requires Node 22+.
30
+ Zero runtime dependencies, ESM, requires Node 22+.
32
31
 
33
32
  ## Commands
34
33
 
35
34
  | Command | Description |
36
35
  | --- | --- |
37
- | `pmsec check [--min N]` | Read each tool's config; exit 1 if any tool is below `N` days or unset. |
38
- | `pmsec set <DAYS>` | Write `DAYS`-day cooldown to every selected tool. Always proceeds; if the runtime is too old to honor the key, prints a `⚠` line under the success line. |
39
- | `pmsec unset` | Remove only the cooldown key from each config (other keys preserved). |
36
+ | `pmsec enable` | Write the hardening bundle (3-day cooldown + per-tool extras) to every selected tool's user config. |
37
+ | `pmsec check` | Read each tool's config; exit 1 if any row is missing or below the bundled value. |
38
+ | `pmsec disable` | Remove every key the bundle set; other keys in the file are preserved. |
40
39
  | `pmsec --version` | Print the installed pmsec version. |
41
40
 
42
- Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--json`.
41
+ Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--days N` (override the 3-day default), `--json`.
43
42
 
44
43
  When the target file is owned by another user (typical: `~/.npmrc` left
45
44
  root-owned by an old `sudo npm config set`), the write fails with `EACCES`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cli.mjs CHANGED
@@ -9,7 +9,9 @@ import * as mise from "./tools/mise.mjs";
9
9
  import * as uv from "./tools/uv.mjs";
10
10
 
11
11
  const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv];
12
- const DEFAULT_MIN = 7;
12
+ // Default cooldown for the hardening bundle. Override per-invocation with
13
+ // `--days N`; the default tracks the safest value we'd recommend.
14
+ const BUNDLE_DAYS = 3;
13
15
  export const VERSION = JSON.parse(
14
16
  readFileSync(new URL("../package.json", import.meta.url), "utf8")
15
17
  ).version;
@@ -17,45 +19,52 @@ export const VERSION = JSON.parse(
17
19
  const USAGE = `pmsec <command> [options]
18
20
 
19
21
  Zero-config install-time supply-chain hardening across npm, pnpm, yarn,
20
- bun, cargo, mise, uv. Each \`set\` writes the cooldown plus every safe-by-
21
- default key the tool exposes (audit-level, trust-policy, hardened mode,
22
- attestation re-verification, ...).
22
+ bun, cargo, mise, uv. \`set\` flips on every safe-by-default key each
23
+ tool exposes (cooldown, audit-level, trust-policy, hardened mode,
24
+ attestation re-verification, ...). No knobs.
23
25
 
24
26
  Commands:
25
- check Inspect hardening state (exit 1 if cooldown < --min or any extra unset)
26
- set <DAYS> Apply DAYS-day cooldown + the hardening bundle to all selected tools
27
- unset Remove the cooldown + hardening bundle from selected tools
27
+ enable Apply the hardening bundle to all selected tools
28
+ disable Remove the hardening bundle from selected tools
29
+ check Verify the bundle is in place (exit 1 if anything missing)
28
30
 
29
31
  Options:
30
32
  --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
31
- --min DAYS Minimum acceptable days for check (default ${DEFAULT_MIN})
33
+ --days N Override cooldown days (default 3)
32
34
  --json Emit JSON output
33
35
  -V, --version Show version
34
36
  -h, --help Show this help
35
37
 
36
38
  Examples:
37
- npx pmsec check --min 7
38
- npx pmsec set 7
39
- npx pmsec unset --tool npm
39
+ npx pmsec enable
40
+ npx pmsec enable --days 7
41
+ npx pmsec check
42
+ npx pmsec disable --tool npm
40
43
  `;
41
44
 
45
+ function parseDays(raw) {
46
+ const n = Number(raw);
47
+ if (!Number.isInteger(n) || n < 1) throw new Error(`--days must be a positive integer (got "${raw}")`);
48
+ return n;
49
+ }
50
+
42
51
  function parse(argv) {
43
- const opts = { command: null, days: null, min: DEFAULT_MIN, json: false, only: null, help: false, version: false };
52
+ const opts = { command: null, json: false, only: null, days: BUNDLE_DAYS, help: false, version: false };
44
53
  const positional = [];
45
54
  for (let i = 0; i < argv.length; i++) {
46
55
  const a = argv[i];
47
56
  if (a === "-h" || a === "--help") opts.help = true;
48
57
  else if (a === "-V" || a === "--version") opts.version = true;
49
58
  else if (a === "--json") opts.json = true;
50
- else if (a === "--min") opts.min = Number(argv[++i]);
51
- else if (a.startsWith("--min=")) opts.min = Number(a.slice(6));
52
59
  else if (a === "--tool") opts.only = argv[++i].split(",");
53
60
  else if (a.startsWith("--tool=")) opts.only = a.slice(7).split(",");
61
+ else if (a === "--days") opts.days = parseDays(argv[++i]);
62
+ else if (a.startsWith("--days=")) opts.days = parseDays(a.slice(7));
54
63
  else if (a.startsWith("-")) throw new Error(`unknown flag: ${a}`);
55
64
  else positional.push(a);
56
65
  }
57
66
  opts.command = positional[0] ?? null;
58
- if (opts.command === "set") opts.days = Number(positional[1]);
67
+ if (positional.length > 1) throw new Error(`unexpected argument: ${positional[1]}`);
59
68
  return opts;
60
69
  }
61
70
 
@@ -101,15 +110,15 @@ function renderHuman(rows, min) {
101
110
  return lines.join("\n") + "\n";
102
111
  }
103
112
 
104
- async function runCheck(targets, { min, json }, env, home, platform, out, err) {
113
+ async function runCheck(targets, { json, days }, env, home, platform, out, err) {
105
114
  const rows = await gatherStatus(targets, env, home, platform);
106
- const failingPrimary = rows.filter(r => r.days === null || r.days < min);
115
+ const failingPrimary = rows.filter(r => r.days === null || r.days < days);
107
116
  const failingExtras = rows.flatMap(r => r.extras.filter(e => !e.ok));
108
117
  const ok = failingPrimary.length === 0 && failingExtras.length === 0;
109
- if (json) out.write(JSON.stringify({ min, rows, ok }, null, 2) + "\n");
110
- else out.write(renderHuman(rows, min));
111
- if (failingPrimary.length) err.write(`pmsec: ${failingPrimary.length} tool(s) below ${min} days\n`);
112
- if (failingExtras.length) err.write(`pmsec: ${failingExtras.length} hardening setting(s) not at safe value\n`);
118
+ if (json) out.write(JSON.stringify({ bundleDays: days, rows, ok }, null, 2) + "\n");
119
+ else out.write(renderHuman(rows, days));
120
+ if (failingPrimary.length) err.write(`pmsec: ${failingPrimary.length} tool(s) below ${days} days — run \`pmsec enable\`\n`);
121
+ if (failingExtras.length) err.write(`pmsec: ${failingExtras.length} hardening setting(s) not at safe value — run \`pmsec enable\`\n`);
113
122
  return ok ? 0 : 1;
114
123
  }
115
124
 
@@ -125,11 +134,7 @@ function explainFsError(e, tool) {
125
134
  return `${tool}: ${e?.message ?? e}`;
126
135
  }
127
136
 
128
- async function runSet(targets, days, json, env, home, platform, out, err) {
129
- if (!Number.isInteger(days) || days <= 0) {
130
- err.write(`pmsec: set requires integer DAYS > 0\n`);
131
- return 2;
132
- }
137
+ async function runEnable(targets, json, days, env, home, platform, out, err) {
133
138
  const results = [];
134
139
  const failures = [];
135
140
  const warnings = [];
@@ -137,20 +142,26 @@ async function runSet(targets, days, json, env, home, platform, out, err) {
137
142
  const warn = preflightWarn(t);
138
143
  if (warn) warnings.push({ tool: t.name, warn });
139
144
  try {
140
- const r = await t.write(days, env, home, platform);
141
- results.push({ tool: t.name, path: r.path, days, ok: true, warn });
145
+ const current = await t.read(env, home, platform);
146
+ const currentDays = current.days ?? 0;
147
+ const effective = Math.max(currentDays, days);
148
+ const kept = currentDays >= days && currentDays > 0;
149
+ const r = await t.write(effective, env, home, platform);
150
+ results.push({ tool: t.name, path: r.path, days: effective, requested: days, kept, ok: true, warn });
142
151
  } catch (e) {
143
152
  failures.push({ tool: t.name, error: explainFsError(e, t.name) });
144
- results.push({ tool: t.name, path: e?.path ?? null, days, ok: false, error: explainFsError(e, t.name), warn });
153
+ results.push({ tool: t.name, path: e?.path ?? null, days, requested: days, kept: false, ok: false, error: explainFsError(e, t.name), warn });
145
154
  }
146
155
  }
147
- if (json) out.write(JSON.stringify({ set: days, results, warnings, ok: failures.length === 0 }, null, 2) + "\n");
156
+ if (json) out.write(JSON.stringify({ enabled: true, bundleDays: days, results, warnings, ok: failures.length === 0 }, null, 2) + "\n");
148
157
  else for (const r of results) {
149
158
  if (r.ok) {
159
+ const action = r.kept ? "keep " : "enable ";
160
+ const note = r.kept ? ` (kept existing ${r.days}d ≥ ${r.requested}d)` : "";
150
161
  const tail = r.warn ? `\n ⚠ ${r.warn}` : "";
151
- out.write(`set ${r.tool.padEnd(4)} ${r.days} days [${r.path}]${tail}\n`);
162
+ out.write(`${action} ${r.tool.padEnd(4)} [${r.path}]${note}${tail}\n`);
152
163
  } else {
153
- out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
164
+ out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
154
165
  }
155
166
  }
156
167
  for (const f of failures) err.write(`pmsec: ${f.error}\n`);
@@ -158,7 +169,7 @@ async function runSet(targets, days, json, env, home, platform, out, err) {
158
169
  return failures.length ? 1 : 0;
159
170
  }
160
171
 
161
- async function runUnset(targets, json, env, home, platform, out, err) {
172
+ async function runDisable(targets, json, env, home, platform, out, err) {
162
173
  const results = [];
163
174
  const failures = [];
164
175
  for (const t of targets) {
@@ -172,8 +183,8 @@ async function runUnset(targets, json, env, home, platform, out, err) {
172
183
  }
173
184
  if (json) out.write(JSON.stringify({ results, ok: failures.length === 0 }, null, 2) + "\n");
174
185
  else for (const r of results) {
175
- if (!r.ok) out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
176
- else out.write(`${r.removed ? "rm " : "skip"} ${r.tool.padEnd(4)} [${r.path}]\n`);
186
+ if (!r.ok) out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
187
+ else out.write(`${r.removed ? "disable " : "skip "} ${r.tool.padEnd(4)} [${r.path}]\n`);
177
188
  }
178
189
  for (const f of failures) err.write(`pmsec: ${f.error}\n`);
179
190
  return failures.length ? 1 : 0;
@@ -196,8 +207,8 @@ export async function run(argv, {
196
207
  catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
197
208
  try {
198
209
  if (opts.command === "check") return await runCheck(targets, opts, env, home, platform, out, err);
199
- if (opts.command === "set") return await runSet(targets, opts.days, opts.json, env, home, platform, out, err);
200
- if (opts.command === "unset") return await runUnset(targets, opts.json, env, home, platform, out, err);
210
+ if (opts.command === "enable") return await runEnable(targets, opts.json, opts.days, env, home, platform, out, err);
211
+ if (opts.command === "disable") return await runDisable(targets, opts.json, env, home, platform, out, err);
201
212
  err.write(`pmsec: unknown command "${opts.command}"\n`);
202
213
  return 2;
203
214
  } catch (e) {