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.
- package/README.md +15 -16
- package/package.json +1 -1
- package/src/cli.mjs +48 -37
package/README.md
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
# pmsec (Node)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
13
|
-
npx pmsec
|
|
14
|
-
npx pmsec
|
|
11
|
+
npx pmsec enable
|
|
12
|
+
npx pmsec check
|
|
13
|
+
npx pmsec disable
|
|
15
14
|
```
|
|
16
15
|
|
|
17
16
|
```bash
|
|
18
|
-
uvx pmsec
|
|
19
|
-
uvx pmsec
|
|
20
|
-
uvx pmsec
|
|
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
|
|
30
|
+
Zero runtime dependencies, ESM, requires Node 22+.
|
|
32
31
|
|
|
33
32
|
## Commands
|
|
34
33
|
|
|
35
34
|
| Command | Description |
|
|
36
35
|
| --- | --- |
|
|
37
|
-
| `pmsec
|
|
38
|
-
| `pmsec
|
|
39
|
-
| `pmsec
|
|
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
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
|
-
|
|
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.
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
--
|
|
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
|
|
38
|
-
npx pmsec
|
|
39
|
-
npx pmsec
|
|
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,
|
|
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 (
|
|
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, {
|
|
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 <
|
|
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({
|
|
110
|
-
else out.write(renderHuman(rows,
|
|
111
|
-
if (failingPrimary.length) err.write(`pmsec: ${failingPrimary.length} tool(s) below ${
|
|
112
|
-
if (failingExtras.length) err.write(`pmsec: ${failingExtras.length} hardening setting(s) not at safe value
|
|
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
|
|
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
|
|
141
|
-
|
|
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({
|
|
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(
|
|
162
|
+
out.write(`${action} ${r.tool.padEnd(4)} [${r.path}]${note}${tail}\n`);
|
|
152
163
|
} else {
|
|
153
|
-
out.write(`FAIL
|
|
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
|
|
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
|
|
176
|
-
else out.write(`${r.removed ? "
|
|
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 === "
|
|
200
|
-
if (opts.command === "
|
|
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) {
|