@cdxoo/npm-lockdown-proxy 0.0.1 → 0.0.2
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 +20 -8
- package/package.json +13 -3
- package/proxy.js +12 -12
- package/whitelist-from-lockfile.js +102 -0
package/README.md
CHANGED
|
@@ -4,27 +4,39 @@ A minimal npm registry proxy that blocks any package (or version) not on a white
|
|
|
4
4
|
|
|
5
5
|
## AI Disclosure
|
|
6
6
|
|
|
7
|
-
This stuff was vibe coded with claude (pronounced "KLORT!!")
|
|
7
|
+
This stuff was vibe coded with claude (pronounced "KLORT!!"). I hope I never have to actually mantain this...
|
|
8
8
|
|
|
9
9
|
## Run
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# env var defaults are PORT=4873 WHITELIST=whitelist.json
|
|
13
|
+
npx @cdxoo/npm-lockdown-proxy
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
# or
|
|
16
|
+
|
|
17
|
+
npm install -g @cdxoo/npm-lockdown-proxy
|
|
18
|
+
npm-lockdown-proxy
|
|
19
|
+
npm-lockdown-proxy-create-whitelist-form-lockfile some-package-lock.json [--merge]
|
|
20
|
+
|
|
21
|
+
```
|
|
19
22
|
|
|
20
23
|
## Use
|
|
21
24
|
|
|
22
25
|
```sh
|
|
23
26
|
npm install <pkg> --registry http://localhost:4873
|
|
24
|
-
# or
|
|
27
|
+
# or
|
|
28
|
+
echo "registry=http://localhost:4873" >> my-project/.npmrc # or ~/.npmrc
|
|
29
|
+
# or
|
|
25
30
|
npm config set registry http://localhost:4873
|
|
26
31
|
```
|
|
27
32
|
|
|
33
|
+
## Server Env Vars
|
|
34
|
+
|
|
35
|
+
| Env var | Default | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `PORT` | `4873` | Port to listen on |
|
|
38
|
+
| `WHITELIST` | `whitelist.json` | Path to whitelist file |
|
|
39
|
+
|
|
28
40
|
## Whitelist format
|
|
29
41
|
|
|
30
42
|
`whitelist.json` is an object. The value controls which versions are allowed:
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdxoo/npm-lockdown-proxy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Minimal npm registry proxy with package/version whitelisting",
|
|
5
5
|
"bin": {
|
|
6
|
-
"npm-lockdown-proxy": "./proxy.js"
|
|
6
|
+
"npm-lockdown-proxy": "./proxy.js",
|
|
7
|
+
"npm-lockdown-proxy-whitelist-from-lockfile": "./whitelist-from-lockfile.js"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
9
10
|
"proxy.js"
|
|
@@ -24,5 +25,14 @@
|
|
|
24
25
|
"publishConfig": {
|
|
25
26
|
"access": "public"
|
|
26
27
|
},
|
|
27
|
-
"dependencies": {}
|
|
28
|
+
"dependencies": {},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"npm",
|
|
31
|
+
"registry",
|
|
32
|
+
"proxy",
|
|
33
|
+
"whitelist",
|
|
34
|
+
"lockdown",
|
|
35
|
+
"security",
|
|
36
|
+
"supply-chain"
|
|
37
|
+
]
|
|
28
38
|
}
|
package/proxy.js
CHANGED
|
@@ -15,19 +15,19 @@ function loadWhitelist() {
|
|
|
15
15
|
try {
|
|
16
16
|
raw = JSON.parse(fs.readFileSync(WHITELIST_FILE, 'utf8'));
|
|
17
17
|
} catch {
|
|
18
|
-
console.warn(`WARNING: could not load whitelist from '${WHITELIST_FILE}'
|
|
18
|
+
console.warn(`WARNING: could not load whitelist from '${WHITELIST_FILE}' - all packages will be blocked`);
|
|
19
19
|
console.warn(` Set the WHITELIST env var or create a whitelist.json in the working directory.`);
|
|
20
|
-
return
|
|
20
|
+
return new Map();
|
|
21
21
|
}
|
|
22
|
-
// Normalise each entry to
|
|
23
|
-
const wl =
|
|
22
|
+
// Normalise each entry to a Set of allowed versions, or '*' for any.
|
|
23
|
+
const wl = new Map();
|
|
24
24
|
for (const [pkg, versions] of Object.entries(raw)) {
|
|
25
25
|
if (versions === '*') {
|
|
26
|
-
wl
|
|
26
|
+
wl.set(pkg, '*');
|
|
27
27
|
} else if (Array.isArray(versions)) {
|
|
28
|
-
wl
|
|
28
|
+
wl.set(pkg, new Set(versions));
|
|
29
29
|
} else {
|
|
30
|
-
wl
|
|
30
|
+
wl.set(pkg, new Set([versions]));
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
return wl;
|
|
@@ -83,15 +83,15 @@ const server = http.createServer((req, res) => {
|
|
|
83
83
|
const { pkg, version } = parseRequest(req.url.split('?')[0]);
|
|
84
84
|
|
|
85
85
|
if (pkg !== null) {
|
|
86
|
-
if (!
|
|
86
|
+
if (!whitelist.has(pkg)) {
|
|
87
87
|
console.log(`BLOCKED ${req.method} ${req.url} - '${pkg}' not whitelisted`);
|
|
88
88
|
return deny(res, `Package '${pkg}' is not on the whitelist`);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const allowed = whitelist
|
|
92
|
-
if (version !== null && allowed !== '*' && !allowed.
|
|
91
|
+
const allowed = whitelist.get(pkg);
|
|
92
|
+
if (version !== null && allowed !== '*' && !allowed.has(version)) {
|
|
93
93
|
console.log(`BLOCKED ${req.method} ${req.url} - '${pkg}@${version}' not an allowed version`);
|
|
94
|
-
return deny(res, `Version '${version}' of '${pkg}' is not on the whitelist (allowed: ${allowed.join(', ')})`);
|
|
94
|
+
return deny(res, `Version '${version}' of '${pkg}' is not on the whitelist (allowed: ${[...allowed].join(', ')})`);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -122,5 +122,5 @@ const server = http.createServer((req, res) => {
|
|
|
122
122
|
|
|
123
123
|
server.listen(PORT, () => {
|
|
124
124
|
console.log(`npm proxy -> ${UPSTREAM} on http://localhost:${PORT}`);
|
|
125
|
-
console.log(`whitelist ${WHITELIST_FILE} (${
|
|
125
|
+
console.log(`whitelist ${WHITELIST_FILE} (${whitelist.size} packages)`);
|
|
126
126
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2).filter(a => a !== '--merge');
|
|
7
|
+
const merge = process.argv.includes('--merge');
|
|
8
|
+
const lockfile = args[0];
|
|
9
|
+
const output = args[1] || 'whitelist.json';
|
|
10
|
+
|
|
11
|
+
if (!lockfile) {
|
|
12
|
+
console.error('usage: npm-lockdown-proxy-whitelist-from-lockfile <package-lock.json> [whitelist.json] [--merge]');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const lock = JSON.parse(fs.readFileSync(lockfile, 'utf8'));
|
|
17
|
+
|
|
18
|
+
if (lock.lockfileVersion < 2) {
|
|
19
|
+
console.error('error: lockfile version 2 or higher required (npm 7+)');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function confirm(prompt) {
|
|
24
|
+
process.stdout.write(prompt);
|
|
25
|
+
const fd = fs.openSync('/dev/tty', 'r');
|
|
26
|
+
const buf = Buffer.alloc(1);
|
|
27
|
+
let answer = '';
|
|
28
|
+
while (true) {
|
|
29
|
+
fs.readSync(fd, buf, 0, 1);
|
|
30
|
+
if (buf[0] === 0x0a) break; // newline
|
|
31
|
+
answer += buf.toString();
|
|
32
|
+
}
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
return answer.trim().toLowerCase() === 'y';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Map<name, Set<version>>
|
|
38
|
+
function parseLockfile(lock) {
|
|
39
|
+
const pkgs = new Map();
|
|
40
|
+
for (const [key, pkg] of Object.entries(lock.packages || {})) {
|
|
41
|
+
if (key === '') continue; // root package
|
|
42
|
+
if (pkg.link) continue; // workspace symlinks
|
|
43
|
+
const name = key.replace(/^.*node_modules\//, '');
|
|
44
|
+
process.stdout.write(`\rparsing: ${name.padEnd(60)}`);
|
|
45
|
+
const versions = pkgs.get(name) ?? new Set();
|
|
46
|
+
versions.add(pkg.version);
|
|
47
|
+
pkgs.set(name, versions);
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write('\r' + ' '.repeat(70) + '\r'); // clear line
|
|
50
|
+
return pkgs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Map<name, Set<version> | '*'>
|
|
54
|
+
function loadWhitelist(file) {
|
|
55
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
56
|
+
const wl = new Map();
|
|
57
|
+
for (const [name, versions] of Object.entries(raw)) {
|
|
58
|
+
wl.set(name, versions === '*' ? '*' : new Set(versions));
|
|
59
|
+
}
|
|
60
|
+
return wl;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serialize(wl) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [name, versions] of [...wl.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
66
|
+
out[name] = versions === '*' ? '*' : [...versions];
|
|
67
|
+
}
|
|
68
|
+
return JSON.stringify(out, null, 2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parsed = parseLockfile(lock);
|
|
72
|
+
|
|
73
|
+
if (merge) {
|
|
74
|
+
if (!fs.existsSync(output)) {
|
|
75
|
+
console.error(`error: '${output}' does not exist, cannot merge`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if (!confirm(`merge into '${output}'? [y/N] `)) {
|
|
79
|
+
console.log('aborted');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
const existing = loadWhitelist(output);
|
|
83
|
+
for (const [name, versions] of parsed) {
|
|
84
|
+
if (!existing.has(name)) {
|
|
85
|
+
existing.set(name, versions);
|
|
86
|
+
} else if (existing.get(name) !== '*') {
|
|
87
|
+
const merged = existing.get(name);
|
|
88
|
+
for (const v of versions) merged.add(v);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
fs.writeFileSync(output, serialize(existing));
|
|
92
|
+
console.log(`merged ${parsed.size} packages into ${output} (${existing.size} total)`);
|
|
93
|
+
} else {
|
|
94
|
+
if (fs.existsSync(output)) {
|
|
95
|
+
if (!confirm(`'${output}' already exists, overwrite? [y/N] `)) {
|
|
96
|
+
console.log('aborted');
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
fs.writeFileSync(output, serialize(parsed));
|
|
101
|
+
console.log(`wrote ${parsed.size} packages to ${output}`);
|
|
102
|
+
}
|