pmsec 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ npx --registry=https://registry.npmjs.org/ --min-release-age=0 pmsec check
28
28
  uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
29
29
  ```
30
30
 
31
- Zero runtime dependencies, ESM, requires Node 20+.
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+.
32
32
 
33
33
  ## Commands
34
34
 
@@ -37,13 +37,14 @@ Zero runtime dependencies, ESM, requires Node 20+.
37
37
  | `pmsec check [--min N]` | Read each tool's config; exit 1 if any tool is below `N` days or unset. |
38
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
39
  | `pmsec unset` | Remove only the cooldown key from each config (other keys preserved). |
40
+ | `pmsec --version` | Print the installed pmsec version. |
40
41
 
41
42
  Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--json`.
42
43
 
43
44
  When the target file is owned by another user (typical: `~/.npmrc` left
44
- root-owned by an old `sudo npm config set`), pmsec auto-runs
45
- `sudo chown $(id -u):$(id -g) <path>` and retries the write. You'll be
46
- prompted for your password once; subsequent runs need no escalation.
45
+ root-owned by an old `sudo npm config set`), the write fails with `EACCES`
46
+ and pmsec prints the exact `chown` command needed to restore ownership —
47
+ re-run after applying it. pmsec never escalates privileges itself.
47
48
 
48
49
  See the [project README](https://github.com/HikaruEgashira/pmsec) for the
49
50
  full table of keys, units, paths, and environment overrides.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Inspect and apply install-time cooldown (min-release-age / exclude-newer) for npm 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",
@@ -17,7 +17,7 @@
17
17
  "type": "module",
18
18
  "license": "MIT",
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=22"
21
21
  },
22
22
  "bin": {
23
23
  "pmsec": "./bin/cli.mjs"
@@ -36,6 +36,11 @@
36
36
  "min-release-age",
37
37
  "exclude-newer",
38
38
  "npm",
39
+ "pnpm",
40
+ "yarn",
41
+ "bun",
42
+ "cargo",
43
+ "mise",
39
44
  "uv",
40
45
  "pmsec"
41
46
  ]
package/src/cli.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
2
3
  import * as npm from "./tools/npm.mjs";
3
4
  import * as pnpm from "./tools/pnpm.mjs";
4
5
  import * as yarn from "./tools/yarn.mjs";
@@ -9,18 +10,27 @@ import * as uv from "./tools/uv.mjs";
9
10
 
10
11
  const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv];
11
12
  const DEFAULT_MIN = 7;
13
+ export const VERSION = JSON.parse(
14
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
15
+ ).version;
12
16
 
13
17
  const USAGE = `pmsec <command> [options]
14
18
 
19
+ 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, ...).
23
+
15
24
  Commands:
16
- check Inspect cooldown settings (exit 1 if any tool below --min)
17
- set <DAYS> Apply DAYS-day cooldown to all selected tools
18
- unset Remove cooldown settings from selected tools
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
19
28
 
20
29
  Options:
21
30
  --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
22
31
  --min DAYS Minimum acceptable days for check (default ${DEFAULT_MIN})
23
32
  --json Emit JSON output
33
+ -V, --version Show version
24
34
  -h, --help Show this help
25
35
 
26
36
  Examples:
@@ -30,11 +40,12 @@ Examples:
30
40
  `;
31
41
 
32
42
  function parse(argv) {
33
- const opts = { command: null, days: null, min: DEFAULT_MIN, json: false, only: null, help: false };
43
+ const opts = { command: null, days: null, min: DEFAULT_MIN, json: false, only: null, help: false, version: false };
34
44
  const positional = [];
35
45
  for (let i = 0; i < argv.length; i++) {
36
46
  const a = argv[i];
37
47
  if (a === "-h" || a === "--help") opts.help = true;
48
+ else if (a === "-V" || a === "--version") opts.version = true;
38
49
  else if (a === "--json") opts.json = true;
39
50
  else if (a === "--min") opts.min = Number(argv[++i]);
40
51
  else if (a.startsWith("--min=")) opts.min = Number(a.slice(6));
@@ -65,7 +76,12 @@ function preflightWarn(t) {
65
76
  async function gatherStatus(targets, env, home, platform) {
66
77
  return Promise.all(targets.map(async t => {
67
78
  const r = await t.read(env, home, platform);
68
- return { tool: t.name, key: t.key, path: r.path, configured: r.configured, days: r.days, warn: preflightWarn(t) };
79
+ return {
80
+ tool: t.name, key: t.key, path: r.path,
81
+ configured: r.configured, days: r.days,
82
+ extras: r.extras ?? [],
83
+ warn: preflightWarn(t)
84
+ };
69
85
  }));
70
86
  }
71
87
 
@@ -76,17 +92,25 @@ function renderHuman(rows, min) {
76
92
  const value = r.configured ?? "(unset)";
77
93
  const tail = r.warn ? `\n ⚠ ${r.warn}` : "";
78
94
  lines.push(`${status} ${r.tool.padEnd(4)} ${r.key} = ${value} [${r.path}]${tail}`);
95
+ for (const e of r.extras) {
96
+ const exStatus = e.configured === null ? "MISSING" : e.ok ? "OK " : "STALE ";
97
+ const exValue = e.configured ?? "(unset)";
98
+ lines.push(`${exStatus} ${r.tool.padEnd(4)} ${e.key} = ${exValue} [${r.path}]`);
99
+ }
79
100
  }
80
101
  return lines.join("\n") + "\n";
81
102
  }
82
103
 
83
104
  async function runCheck(targets, { min, json }, env, home, platform, out, err) {
84
105
  const rows = await gatherStatus(targets, env, home, platform);
85
- const failing = rows.filter(r => r.days === null || r.days < min);
86
- if (json) out.write(JSON.stringify({ min, rows, ok: failing.length === 0 }, null, 2) + "\n");
106
+ const failingPrimary = rows.filter(r => r.days === null || r.days < min);
107
+ const failingExtras = rows.flatMap(r => r.extras.filter(e => !e.ok));
108
+ const ok = failingPrimary.length === 0 && failingExtras.length === 0;
109
+ if (json) out.write(JSON.stringify({ min, rows, ok }, null, 2) + "\n");
87
110
  else out.write(renderHuman(rows, min));
88
- if (failing.length) err.write(`pmsec: ${failing.length} tool(s) below ${min} days\n`);
89
- return failing.length ? 1 : 0;
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`);
113
+ return ok ? 0 : 1;
90
114
  }
91
115
 
92
116
  function shQuote(s) { return s ? `'${String(s).replace(/'/g, `'\\''`)}'` : ""; }
@@ -102,7 +126,10 @@ function explainFsError(e, tool) {
102
126
  }
103
127
 
104
128
  async function runSet(targets, days, json, env, home, platform, out, err) {
105
- if (!Number.isInteger(days) || days <= 0) throw new Error(`set requires integer DAYS > 0`);
129
+ if (!Number.isInteger(days) || days <= 0) {
130
+ err.write(`pmsec: set requires integer DAYS > 0\n`);
131
+ return 2;
132
+ }
106
133
  const results = [];
107
134
  const failures = [];
108
135
  const warnings = [];
@@ -162,6 +189,7 @@ export async function run(argv, {
162
189
  let opts;
163
190
  try { opts = parse(argv); }
164
191
  catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
192
+ if (opts.version) { out.write(`pmsec ${VERSION}\n`); return 0; }
165
193
  if (opts.help || !opts.command) { out.write(USAGE); return opts.help ? 0 : 2; }
166
194
  let targets;
167
195
  try { targets = selectTools(opts.only); }
package/src/tools/bun.mjs CHANGED
@@ -8,6 +8,7 @@ export const key = "minimumReleaseAge";
8
8
  export const section = "install";
9
9
  export const docs = "https://bun.com/docs/runtime/bunfig#install";
10
10
  export const minBin = [1, 3, 0];
11
+ export const extras = [];
11
12
 
12
13
  export function path(env, home) { return bunConfigPath(env, home); }
13
14
 
@@ -23,7 +24,7 @@ export async function read(env, home) {
23
24
  const raw = await readSafe(p);
24
25
  const value = readKey(raw, key, { section });
25
26
  const seconds = value === null ? null : Number(value);
26
- return { path: p, configured: value, days: seconds === null || Number.isNaN(seconds) ? null : Math.floor(seconds / 86400) };
27
+ return { path: p, configured: value, days: seconds === null || Number.isNaN(seconds) ? null : Math.floor(seconds / 86400), extras: [] };
27
28
  }
28
29
 
29
30
  export async function write(days, env, home) {
@@ -6,6 +6,7 @@ export const name = "cargo";
6
6
  export const key = "minimum-release-age";
7
7
  export const section = "install";
8
8
  export const docs = "https://rust-lang.github.io/rfcs/3801-package-cooldown.html";
9
+ export const extras = [];
9
10
 
10
11
  export function path(env, home) { return cargoConfigPath(env, home); }
11
12
 
@@ -21,7 +22,7 @@ export async function read(env, home) {
21
22
  const p = path(env, home);
22
23
  const raw = await readSafe(p);
23
24
  const value = readKey(raw, key, { section });
24
- return { path: p, configured: value, days: parseDays(value) };
25
+ return { path: p, configured: value, days: parseDays(value), extras: [] };
25
26
  }
26
27
 
27
28
  export async function write(days, env, home) {
@@ -1,12 +1,16 @@
1
1
  import { miseConfigPath } 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 { detectVersion, gte } from "../util/version.mjs";
5
6
  export const name = "mise";
6
7
  export const key = "minimum_release_age";
7
8
  export const section = "settings";
8
9
  export const docs = "https://mise.jdx.dev/configuration/settings.html#minimum_release_age";
9
10
  export const minBin = [2026, 4, 22];
11
+ export const extras = [
12
+ { key: "paranoid", expected: "true", line: "paranoid = true", section }
13
+ ];
10
14
 
11
15
  export function path(env, home, platform) { return miseConfigPath(env, home, platform); }
12
16
 
@@ -33,13 +37,17 @@ export async function read(env, home, platform) {
33
37
  const p = path(env, home, platform);
34
38
  const raw = await readSafe(p);
35
39
  const value = readKey(raw, key, { section });
36
- return { path: p, configured: value, days: parseDays(value) };
40
+ return {
41
+ path: p, configured: value, days: parseDays(value),
42
+ extras: readExtras(raw, extras)
43
+ };
37
44
  }
38
45
 
39
46
  export async function write(days, env, home, platform) {
40
47
  const p = path(env, home, platform);
41
48
  const before = await readSafe(p);
42
- const after = setKey(before, key, `${key} = "${days}d"`, { section });
49
+ let after = setKey(before, key, `${key} = "${days}d"`, { section });
50
+ after = applyExtras(after, extras);
43
51
  await writeAtomic(p, after);
44
52
  return { path: p, before, after };
45
53
  }
@@ -47,7 +55,9 @@ export async function write(days, env, home, platform) {
47
55
  export async function unset(env, home, platform) {
48
56
  const p = path(env, home, platform);
49
57
  const before = await readSafe(p);
50
- const { text: after, removed } = removeKey(before, key, { section });
51
- if (removed) await writeAtomic(p, after);
58
+ const cooldown = removeKey(before, key, { section });
59
+ const ex = removeExtras(cooldown.text, extras);
60
+ const removed = cooldown.removed || ex.removed;
61
+ if (removed) await writeAtomic(p, ex.text);
52
62
  return { path: p, removed };
53
63
  }
package/src/tools/npm.mjs CHANGED
@@ -1,12 +1,16 @@
1
1
  import { npmrcPath } 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 { detectVersion, gte } from "../util/version.mjs";
5
6
 
6
7
  export const name = "npm";
7
8
  export const key = "min-release-age";
8
9
  export const docs = "https://docs.npmjs.com/cli/v11/using-npm/config#min-release-age";
9
10
  export const minBin = [11, 10, 0];
11
+ export const extras = [
12
+ { key: "audit-level", expected: "high", line: "audit-level=high" }
13
+ ];
10
14
 
11
15
  export function path(env, home) { return npmrcPath(env, home); }
12
16
 
@@ -21,13 +25,18 @@ export async function read(env, home) {
21
25
  const p = path(env, home);
22
26
  const raw = await readSafe(p);
23
27
  const value = readKey(raw, key);
24
- return { path: p, configured: value, days: value === null ? null : Number(value) };
28
+ return {
29
+ path: p, configured: value,
30
+ days: value === null ? null : Number(value),
31
+ extras: readExtras(raw, extras)
32
+ };
25
33
  }
26
34
 
27
35
  export async function write(days, env, home) {
28
36
  const p = path(env, home);
29
37
  const before = await readSafe(p);
30
- const after = setKey(before, key, `${key}=${days}`);
38
+ let after = setKey(before, key, `${key}=${days}`);
39
+ after = applyExtras(after, extras);
31
40
  await writeAtomic(p, after);
32
41
  return { path: p, before, after };
33
42
  }
@@ -35,7 +44,9 @@ export async function write(days, env, home) {
35
44
  export async function unset(env, home) {
36
45
  const p = path(env, home);
37
46
  const before = await readSafe(p);
38
- const { text: after, removed } = removeKey(before, key);
39
- if (removed) await writeAtomic(p, after);
47
+ const cooldown = removeKey(before, key);
48
+ const ex = removeExtras(cooldown.text, extras);
49
+ const removed = cooldown.removed || ex.removed;
50
+ if (removed) await writeAtomic(p, ex.text);
40
51
  return { path: p, removed };
41
52
  }
@@ -1,12 +1,17 @@
1
1
  import { npmrcPath } 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 { detectVersion, gte } from "../util/version.mjs";
5
6
 
6
7
  export const name = "pnpm";
7
8
  export const key = "minimum-release-age";
8
9
  export const docs = "https://pnpm.io/settings#minimumreleaseage";
9
10
  export const minBin = [10, 6, 0];
11
+ export const extras = [
12
+ { key: "trust-policy", expected: "no-downgrade", line: "trust-policy=no-downgrade" },
13
+ { key: "block-exotic-subdeps", expected: "true", line: "block-exotic-subdeps=true" }
14
+ ];
10
15
 
11
16
  export function path(env, home) { return npmrcPath(env, home); }
12
17
 
@@ -22,13 +27,18 @@ export async function read(env, home) {
22
27
  const raw = await readSafe(p);
23
28
  const value = readKey(raw, key);
24
29
  const minutes = value === null ? null : Number(value);
25
- return { path: p, configured: value, days: minutes === null || Number.isNaN(minutes) ? null : Math.floor(minutes / (60 * 24)) };
30
+ return {
31
+ path: p, configured: value,
32
+ days: minutes === null || Number.isNaN(minutes) ? null : Math.floor(minutes / (60 * 24)),
33
+ extras: readExtras(raw, extras)
34
+ };
26
35
  }
27
36
 
28
37
  export async function write(days, env, home) {
29
38
  const p = path(env, home);
30
39
  const before = await readSafe(p);
31
- const after = setKey(before, key, `${key}=${days * 24 * 60}`);
40
+ let after = setKey(before, key, `${key}=${days * 24 * 60}`);
41
+ after = applyExtras(after, extras);
32
42
  await writeAtomic(p, after);
33
43
  return { path: p, before, after };
34
44
  }
@@ -36,7 +46,9 @@ export async function write(days, env, home) {
36
46
  export async function unset(env, home) {
37
47
  const p = path(env, home);
38
48
  const before = await readSafe(p);
39
- const { text: after, removed } = removeKey(before, key);
40
- if (removed) await writeAtomic(p, after);
49
+ const cooldown = removeKey(before, key);
50
+ const ex = removeExtras(cooldown.text, extras);
51
+ const removed = cooldown.removed || ex.removed;
52
+ if (removed) await writeAtomic(p, ex.text);
41
53
  return { path: p, removed };
42
54
  }
package/src/tools/uv.mjs CHANGED
@@ -7,6 +7,7 @@ export const name = "uv";
7
7
  export const key = "exclude-newer";
8
8
  export const docs = "https://docs.astral.sh/uv/reference/settings/#exclude-newer";
9
9
  export const minBin = [0, 9, 17];
10
+ export const extras = [];
10
11
 
11
12
  export function path(env, home, platform) { return uvConfigPath(env, home, platform); }
12
13
 
@@ -29,7 +30,7 @@ export async function read(env, home, platform) {
29
30
  const p = path(env, home, platform);
30
31
  const raw = await readSafe(p);
31
32
  const value = readKey(raw, key);
32
- return { path: p, configured: value, days: parseDays(value) };
33
+ return { path: p, configured: value, days: parseDays(value), extras: [] };
33
34
  }
34
35
 
35
36
  export async function write(days, env, home, platform) {
@@ -1,6 +1,7 @@
1
1
  import { yarnrcPath } 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 { detectVersion, gte } from "../util/version.mjs";
5
6
 
6
7
  export const name = "yarn";
@@ -8,6 +9,9 @@ export const key = "npmMinimalAgeGate";
8
9
  export const docs = "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate";
9
10
  export const minBin = [4, 10, 0];
10
11
  const SEP = ":";
12
+ export const extras = [
13
+ { key: "enableHardenedMode", expected: "true", line: "enableHardenedMode: true", sep: SEP }
14
+ ];
11
15
 
12
16
  export function path(env, home) { return yarnrcPath(env, home); }
13
17
 
@@ -30,13 +34,17 @@ export async function read(env, home) {
30
34
  const p = path(env, home);
31
35
  const raw = await readSafe(p);
32
36
  const value = readKey(raw, key, { sep: SEP });
33
- return { path: p, configured: value, days: parseDays(value) };
37
+ return {
38
+ path: p, configured: value, days: parseDays(value),
39
+ extras: readExtras(raw, extras)
40
+ };
34
41
  }
35
42
 
36
43
  export async function write(days, env, home) {
37
44
  const p = path(env, home);
38
45
  const before = await readSafe(p);
39
- const after = setKey(before, key, `${key}: "${days}d"`, { sep: SEP });
46
+ let after = setKey(before, key, `${key}: "${days}d"`, { sep: SEP });
47
+ after = applyExtras(after, extras);
40
48
  await writeAtomic(p, after);
41
49
  return { path: p, before, after };
42
50
  }
@@ -44,7 +52,9 @@ export async function write(days, env, home) {
44
52
  export async function unset(env, home) {
45
53
  const p = path(env, home);
46
54
  const before = await readSafe(p);
47
- const { text: after, removed } = removeKey(before, key, { sep: SEP });
48
- if (removed) await writeAtomic(p, after);
55
+ const cooldown = removeKey(before, key, { sep: SEP });
56
+ const ex = removeExtras(cooldown.text, extras);
57
+ const removed = cooldown.removed || ex.removed;
58
+ if (removed) await writeAtomic(p, ex.text);
49
59
  return { path: p, removed };
50
60
  }
@@ -0,0 +1,32 @@
1
+ // Per-tool "extras" are fixed-value hardening keys written alongside the main
2
+ // cooldown key on `set`, removed on `unset`, and validated on `check`.
3
+ // Each entry: { key, expected, line, sep?, section? } — `line` is the verbatim
4
+ // text to write (handles per-format quoting/typing); `expected` is the bare
5
+ // value compared against readKey output.
6
+ import { readKey, setKey, removeKey } from "./lines.mjs";
7
+
8
+ export function readExtras(raw, extras) {
9
+ return extras.map(e => {
10
+ const cur = readKey(raw, e.key, { sep: e.sep, section: e.section });
11
+ return { key: e.key, configured: cur, expected: e.expected, ok: cur === e.expected };
12
+ });
13
+ }
14
+
15
+ export function applyExtras(text, extras) {
16
+ let out = text;
17
+ for (const e of extras) {
18
+ out = setKey(out, e.key, e.line, { sep: e.sep, section: e.section });
19
+ }
20
+ return out;
21
+ }
22
+
23
+ export function removeExtras(text, extras) {
24
+ let out = text;
25
+ let removed = false;
26
+ for (const e of extras) {
27
+ const r = removeKey(out, e.key, { sep: e.sep, section: e.section });
28
+ if (r.removed) removed = true;
29
+ out = r.text;
30
+ }
31
+ return { text: out, removed };
32
+ }