pmsec 0.4.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +14 -15
  2. package/package.json +2 -2
  3. package/src/cli.mjs +41 -31
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
@@ -34,12 +33,12 @@ Zero runtime dependencies, ESM, requires Node 22+.
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,10 +1,10 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Inspect and apply install-time cooldown 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, and uv.",
8
8
  "homepage": "https://github.com/HikaruEgashira/pmsec#readme",
9
9
  "repository": {
10
10
  "type": "git",
package/src/cli.mjs CHANGED
@@ -9,48 +9,62 @@ 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;
16
18
 
17
19
  const USAGE = `pmsec <command> [options]
18
20
 
21
+ Zero-config install-time supply-chain hardening across npm, pnpm, yarn,
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.
25
+
19
26
  Commands:
20
- check Inspect cooldown settings (exit 1 if any tool below --min)
21
- set <DAYS> Apply DAYS-day cooldown to all selected tools
22
- unset Remove cooldown settings 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)
23
30
 
24
31
  Options:
25
32
  --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
26
- --min DAYS Minimum acceptable days for check (default ${DEFAULT_MIN})
33
+ --days N Override cooldown days (default 3)
27
34
  --json Emit JSON output
28
35
  -V, --version Show version
29
36
  -h, --help Show this help
30
37
 
31
38
  Examples:
32
- npx pmsec check --min 7
33
- npx pmsec set 7
34
- 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
35
43
  `;
36
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
+
37
51
  function parse(argv) {
38
- 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 };
39
53
  const positional = [];
40
54
  for (let i = 0; i < argv.length; i++) {
41
55
  const a = argv[i];
42
56
  if (a === "-h" || a === "--help") opts.help = true;
43
57
  else if (a === "-V" || a === "--version") opts.version = true;
44
58
  else if (a === "--json") opts.json = true;
45
- else if (a === "--min") opts.min = Number(argv[++i]);
46
- else if (a.startsWith("--min=")) opts.min = Number(a.slice(6));
47
59
  else if (a === "--tool") opts.only = argv[++i].split(",");
48
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));
49
63
  else if (a.startsWith("-")) throw new Error(`unknown flag: ${a}`);
50
64
  else positional.push(a);
51
65
  }
52
66
  opts.command = positional[0] ?? null;
53
- if (opts.command === "set") opts.days = Number(positional[1]);
67
+ if (positional.length > 1) throw new Error(`unexpected argument: ${positional[1]}`);
54
68
  return opts;
55
69
  }
56
70
 
@@ -96,15 +110,15 @@ function renderHuman(rows, min) {
96
110
  return lines.join("\n") + "\n";
97
111
  }
98
112
 
99
- async function runCheck(targets, { min, json }, env, home, platform, out, err) {
113
+ async function runCheck(targets, { json, days }, env, home, platform, out, err) {
100
114
  const rows = await gatherStatus(targets, env, home, platform);
101
- const failingPrimary = rows.filter(r => r.days === null || r.days < min);
115
+ const failingPrimary = rows.filter(r => r.days === null || r.days < days);
102
116
  const failingExtras = rows.flatMap(r => r.extras.filter(e => !e.ok));
103
117
  const ok = failingPrimary.length === 0 && failingExtras.length === 0;
104
- if (json) out.write(JSON.stringify({ min, rows, ok }, null, 2) + "\n");
105
- else out.write(renderHuman(rows, min));
106
- if (failingPrimary.length) err.write(`pmsec: ${failingPrimary.length} tool(s) below ${min} days\n`);
107
- 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`);
108
122
  return ok ? 0 : 1;
109
123
  }
110
124
 
@@ -120,11 +134,7 @@ function explainFsError(e, tool) {
120
134
  return `${tool}: ${e?.message ?? e}`;
121
135
  }
122
136
 
123
- async function runSet(targets, days, json, env, home, platform, out, err) {
124
- if (!Number.isInteger(days) || days <= 0) {
125
- err.write(`pmsec: set requires integer DAYS > 0\n`);
126
- return 2;
127
- }
137
+ async function runEnable(targets, json, days, env, home, platform, out, err) {
128
138
  const results = [];
129
139
  const failures = [];
130
140
  const warnings = [];
@@ -139,13 +149,13 @@ async function runSet(targets, days, json, env, home, platform, out, err) {
139
149
  results.push({ tool: t.name, path: e?.path ?? null, days, ok: false, error: explainFsError(e, t.name), warn });
140
150
  }
141
151
  }
142
- if (json) out.write(JSON.stringify({ set: days, results, warnings, ok: failures.length === 0 }, null, 2) + "\n");
152
+ if (json) out.write(JSON.stringify({ enabled: true, bundleDays: days, results, warnings, ok: failures.length === 0 }, null, 2) + "\n");
143
153
  else for (const r of results) {
144
154
  if (r.ok) {
145
155
  const tail = r.warn ? `\n ⚠ ${r.warn}` : "";
146
- out.write(`set ${r.tool.padEnd(4)} ${r.days} days [${r.path}]${tail}\n`);
156
+ out.write(`enable ${r.tool.padEnd(4)} [${r.path}]${tail}\n`);
147
157
  } else {
148
- out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
158
+ out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
149
159
  }
150
160
  }
151
161
  for (const f of failures) err.write(`pmsec: ${f.error}\n`);
@@ -153,7 +163,7 @@ async function runSet(targets, days, json, env, home, platform, out, err) {
153
163
  return failures.length ? 1 : 0;
154
164
  }
155
165
 
156
- async function runUnset(targets, json, env, home, platform, out, err) {
166
+ async function runDisable(targets, json, env, home, platform, out, err) {
157
167
  const results = [];
158
168
  const failures = [];
159
169
  for (const t of targets) {
@@ -167,8 +177,8 @@ async function runUnset(targets, json, env, home, platform, out, err) {
167
177
  }
168
178
  if (json) out.write(JSON.stringify({ results, ok: failures.length === 0 }, null, 2) + "\n");
169
179
  else for (const r of results) {
170
- if (!r.ok) out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
171
- else out.write(`${r.removed ? "rm " : "skip"} ${r.tool.padEnd(4)} [${r.path}]\n`);
180
+ if (!r.ok) out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
181
+ else out.write(`${r.removed ? "disable " : "skip "} ${r.tool.padEnd(4)} [${r.path}]\n`);
172
182
  }
173
183
  for (const f of failures) err.write(`pmsec: ${f.error}\n`);
174
184
  return failures.length ? 1 : 0;
@@ -191,8 +201,8 @@ export async function run(argv, {
191
201
  catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
192
202
  try {
193
203
  if (opts.command === "check") return await runCheck(targets, opts, env, home, platform, out, err);
194
- if (opts.command === "set") return await runSet(targets, opts.days, opts.json, env, home, platform, out, err);
195
- if (opts.command === "unset") return await runUnset(targets, opts.json, env, home, platform, out, err);
204
+ if (opts.command === "enable") return await runEnable(targets, opts.json, opts.days, env, home, platform, out, err);
205
+ if (opts.command === "disable") return await runDisable(targets, opts.json, env, home, platform, out, err);
196
206
  err.write(`pmsec: unknown command "${opts.command}"\n`);
197
207
  return 2;
198
208
  } catch (e) {