pmsec 0.11.0 → 0.13.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 +2 -2
- package/package.json +2 -2
- package/src/cli.mjs +9 -8
- package/src/tools/bundler.mjs +46 -0
- package/src/tools/npm.mjs +3 -1
- package/src/tools/yarn.mjs +2 -1
- package/src/util/paths.mjs +9 -0
- package/src/util/version.mjs +3 -2
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
|
|
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.
|
|
3
|
+
"version": "0.13.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
|
|
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
|
|
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
|
|
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
|
|
139
|
-
// "pmsec ran but wrote to nowhere" (root's $HOME) or "wrote a file no one
|
|
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;
|
|
@@ -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
|
+
}
|
package/src/tools/npm.mjs
CHANGED
|
@@ -9,7 +9,9 @@ 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" }
|
|
13
15
|
];
|
|
14
16
|
|
|
15
17
|
export function path(ctx) { return npmrcPath(ctx.env, ctx.home); }
|
package/src/tools/yarn.mjs
CHANGED
|
@@ -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); }
|
package/src/util/paths.mjs
CHANGED
|
@@ -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") {
|
package/src/util/version.mjs
CHANGED
|
@@ -34,8 +34,9 @@ export function gte(v, target) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function buildPreflight(name, minBin, suffix) {
|
|
37
|
-
|
|
38
|
-
|
|
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,
|