supply-scan 1.0.2 → 1.1.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 +31 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +627 -383
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/rules/axios-2026.json +43 -11
- package/rules/chalk-debug-2025.json +72 -23
- package/rules/coa-rc-2021.json +32 -7
- package/rules/colors-faker-2022.json +12 -4
- package/rules/eslint-scope-2018.json +15 -5
- package/rules/event-stream-2018.json +14 -5
- package/rules/glassworm-2026.json +27 -7
- package/rules/lottie-player-2024.json +11 -3
- package/rules/node-ipc-2022.json +29 -8
- package/rules/shai-hulud-2025.json +27 -8
- package/rules/solana-web3-2024.json +13 -4
- package/rules/ua-parser-js-2021.json +24 -6
package/dist/index.js
CHANGED
|
@@ -2,14 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { dirname, join as
|
|
5
|
+
import { dirname, join as join5 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/args.ts
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const opts = {
|
|
11
|
+
rules: [],
|
|
12
|
+
all: false,
|
|
13
|
+
list: false,
|
|
14
|
+
path: null,
|
|
15
|
+
ci: false,
|
|
16
|
+
help: false,
|
|
17
|
+
version: false
|
|
18
|
+
};
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const arg = argv[i];
|
|
21
|
+
switch (arg) {
|
|
22
|
+
case "--all":
|
|
23
|
+
case "-a":
|
|
24
|
+
opts.all = true;
|
|
25
|
+
break;
|
|
26
|
+
case "--list":
|
|
27
|
+
case "-l":
|
|
28
|
+
opts.list = true;
|
|
29
|
+
break;
|
|
30
|
+
case "--ci":
|
|
31
|
+
opts.ci = true;
|
|
32
|
+
opts.all = true;
|
|
33
|
+
break;
|
|
34
|
+
case "--help":
|
|
35
|
+
case "-h":
|
|
36
|
+
opts.help = true;
|
|
37
|
+
break;
|
|
38
|
+
case "--version":
|
|
39
|
+
case "-v":
|
|
40
|
+
opts.version = true;
|
|
41
|
+
break;
|
|
42
|
+
case "--rule":
|
|
43
|
+
case "-r":
|
|
44
|
+
if (i + 1 < argv.length) {
|
|
45
|
+
opts.rules.push(argv[++i]);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "--path":
|
|
49
|
+
case "-p":
|
|
50
|
+
if (i + 1 < argv.length) {
|
|
51
|
+
opts.path = resolve(argv[++i]);
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return opts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/rules.ts
|
|
60
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
61
|
+
import { join as join2 } from "path";
|
|
6
62
|
|
|
7
63
|
// src/utils.ts
|
|
8
64
|
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
9
|
-
import { join
|
|
65
|
+
import { join } from "path";
|
|
10
66
|
import { platform, homedir } from "os";
|
|
11
|
-
|
|
12
|
-
|
|
67
|
+
function escapeRegex(s) {
|
|
68
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
69
|
+
}
|
|
13
70
|
function getOS() {
|
|
14
71
|
return platform();
|
|
15
72
|
}
|
|
@@ -45,6 +102,15 @@ function expandPath(p) {
|
|
|
45
102
|
}
|
|
46
103
|
return result2;
|
|
47
104
|
}
|
|
105
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
106
|
+
"node_modules",
|
|
107
|
+
".git",
|
|
108
|
+
"bower_components",
|
|
109
|
+
".cache",
|
|
110
|
+
".Trash",
|
|
111
|
+
"Library",
|
|
112
|
+
".npm"
|
|
113
|
+
]);
|
|
48
114
|
function findProjects(baseDirs, maxDepth = 8) {
|
|
49
115
|
const projects = /* @__PURE__ */ new Set();
|
|
50
116
|
function walk(dir, depth) {
|
|
@@ -58,16 +124,7 @@ function findProjects(baseDirs, maxDepth = 8) {
|
|
|
58
124
|
}
|
|
59
125
|
continue;
|
|
60
126
|
}
|
|
61
|
-
|
|
62
|
-
"node_modules",
|
|
63
|
-
".git",
|
|
64
|
-
"bower_components",
|
|
65
|
-
".cache",
|
|
66
|
-
".Trash",
|
|
67
|
-
"Library",
|
|
68
|
-
".npm"
|
|
69
|
-
];
|
|
70
|
-
if (skip.includes(entry.name)) continue;
|
|
127
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
71
128
|
walk(join(dir, entry.name), depth + 1);
|
|
72
129
|
}
|
|
73
130
|
} catch {
|
|
@@ -108,14 +165,54 @@ function getCommonProjectDirs() {
|
|
|
108
165
|
];
|
|
109
166
|
return candidates.map((d) => join(home, d)).filter((d) => existsSync(d));
|
|
110
167
|
}
|
|
168
|
+
function fileExists(path) {
|
|
169
|
+
try {
|
|
170
|
+
return statSync(path).isFile();
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function dirExists(path) {
|
|
176
|
+
try {
|
|
177
|
+
return statSync(path).isDirectory();
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/rules.ts
|
|
184
|
+
function decodeB64(s) {
|
|
185
|
+
return Buffer.from(s, "base64").toString("utf-8");
|
|
186
|
+
}
|
|
187
|
+
function decodeArray(arr) {
|
|
188
|
+
return arr?.map(decodeB64);
|
|
189
|
+
}
|
|
190
|
+
function decodeRecord(rec) {
|
|
191
|
+
if (!rec) return void 0;
|
|
192
|
+
const result2 = {};
|
|
193
|
+
for (const [key, vals] of Object.entries(rec)) {
|
|
194
|
+
result2[key] = vals.map(decodeB64);
|
|
195
|
+
}
|
|
196
|
+
return result2;
|
|
197
|
+
}
|
|
198
|
+
function decodeRule(rule) {
|
|
199
|
+
if (!rule.encoded || !rule.ioc) return rule;
|
|
200
|
+
const ioc = rule.ioc;
|
|
201
|
+
ioc.domains = decodeArray(ioc.domains);
|
|
202
|
+
ioc.ips = decodeArray(ioc.ips);
|
|
203
|
+
ioc.processes = decodeArray(ioc.processes);
|
|
204
|
+
ioc.strings = decodeArray(ioc.strings);
|
|
205
|
+
ioc.files = decodeRecord(ioc.files);
|
|
206
|
+
return rule;
|
|
207
|
+
}
|
|
111
208
|
function loadRules(rulesDir) {
|
|
112
209
|
const rules = [];
|
|
113
210
|
try {
|
|
114
|
-
const files =
|
|
211
|
+
const files = readdirSync2(rulesDir).filter((f) => f.endsWith(".json"));
|
|
115
212
|
for (const file of files) {
|
|
116
|
-
const rule = readJSON(
|
|
213
|
+
const rule = readJSON(join2(rulesDir, file));
|
|
117
214
|
if (rule && rule.id && rule.packages) {
|
|
118
|
-
rules.push(rule);
|
|
215
|
+
rules.push(decodeRule(rule));
|
|
119
216
|
}
|
|
120
217
|
}
|
|
121
218
|
} catch {
|
|
@@ -123,62 +220,9 @@ function loadRules(rulesDir) {
|
|
|
123
220
|
rules.sort((a, b) => b.date.localeCompare(a.date));
|
|
124
221
|
return rules;
|
|
125
222
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
all: false,
|
|
130
|
-
list: false,
|
|
131
|
-
path: null,
|
|
132
|
-
ci: false,
|
|
133
|
-
help: false,
|
|
134
|
-
version: false
|
|
135
|
-
};
|
|
136
|
-
for (let i = 0; i < argv.length; i++) {
|
|
137
|
-
const arg = argv[i];
|
|
138
|
-
switch (arg) {
|
|
139
|
-
case "--all":
|
|
140
|
-
case "-a":
|
|
141
|
-
opts.all = true;
|
|
142
|
-
break;
|
|
143
|
-
case "--list":
|
|
144
|
-
case "-l":
|
|
145
|
-
opts.list = true;
|
|
146
|
-
break;
|
|
147
|
-
case "--ci":
|
|
148
|
-
opts.ci = true;
|
|
149
|
-
opts.all = true;
|
|
150
|
-
break;
|
|
151
|
-
case "--help":
|
|
152
|
-
case "-h":
|
|
153
|
-
opts.help = true;
|
|
154
|
-
break;
|
|
155
|
-
case "--version":
|
|
156
|
-
case "-v":
|
|
157
|
-
opts.version = true;
|
|
158
|
-
break;
|
|
159
|
-
case "--rule":
|
|
160
|
-
case "-r":
|
|
161
|
-
if (i + 1 < argv.length) {
|
|
162
|
-
opts.rules.push(argv[++i]);
|
|
163
|
-
}
|
|
164
|
-
break;
|
|
165
|
-
case "--path":
|
|
166
|
-
case "-p":
|
|
167
|
-
if (i + 1 < argv.length) {
|
|
168
|
-
opts.path = resolve(argv[++i]);
|
|
169
|
-
}
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return opts;
|
|
174
|
-
}
|
|
175
|
-
function runCommand(cmd) {
|
|
176
|
-
try {
|
|
177
|
-
return execSync(cmd, { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
178
|
-
} catch {
|
|
179
|
-
return "";
|
|
180
|
-
}
|
|
181
|
-
}
|
|
223
|
+
|
|
224
|
+
// src/prompt.ts
|
|
225
|
+
import { createInterface } from "readline";
|
|
182
226
|
function prompt(question) {
|
|
183
227
|
const rl = createInterface({
|
|
184
228
|
input: process.stdin,
|
|
@@ -191,221 +235,378 @@ function prompt(question) {
|
|
|
191
235
|
});
|
|
192
236
|
});
|
|
193
237
|
}
|
|
194
|
-
function fileExists(path) {
|
|
195
|
-
try {
|
|
196
|
-
return statSync(path).isFile();
|
|
197
|
-
} catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function dirExists(path) {
|
|
202
|
-
try {
|
|
203
|
-
return statSync(path).isDirectory();
|
|
204
|
-
} catch {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
238
|
|
|
209
239
|
// src/ui.ts
|
|
240
|
+
var isTTY = !!process.stdout.isTTY;
|
|
241
|
+
var noColor = !!process.env.NO_COLOR || process.env.TERM === "dumb";
|
|
242
|
+
var hasTruecolor = isTTY && !noColor && (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit" || !!process.env.WT_SESSION || process.env.TERM_PROGRAM === "vscode" || process.env.TERM_PROGRAM === "iTerm.app");
|
|
243
|
+
var hasColor = isTTY && !noColor;
|
|
210
244
|
var ESC = "\x1B";
|
|
245
|
+
function fg(r, g, b) {
|
|
246
|
+
if (hasTruecolor) return `${ESC}[38;2;${r};${g};${b}m`;
|
|
247
|
+
if (hasColor) return `${ESC}[36m`;
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
function bg(r, g, b) {
|
|
251
|
+
if (hasTruecolor) return `${ESC}[48;2;${r};${g};${b}m`;
|
|
252
|
+
if (hasColor) return `${ESC}[44m`;
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
211
255
|
var c = {
|
|
212
|
-
red: `${ESC}[1;31m
|
|
213
|
-
green: `${ESC}[1;32m
|
|
214
|
-
yellow: `${ESC}[1;33m
|
|
215
|
-
cyan: `${ESC}[1;36m
|
|
216
|
-
white: `${ESC}[1;37m
|
|
217
|
-
dim: `${ESC}[2m
|
|
218
|
-
bold: `${ESC}[1m
|
|
219
|
-
reset: `${ESC}[0m
|
|
220
|
-
bgRed: `${ESC}[41m
|
|
221
|
-
bgGreen: `${ESC}[42m
|
|
222
|
-
bgYellow: `${ESC}[43m
|
|
223
|
-
bgBlue: `${ESC}[44m`
|
|
256
|
+
red: hasColor ? `${ESC}[1;31m` : "",
|
|
257
|
+
green: hasColor ? `${ESC}[1;32m` : "",
|
|
258
|
+
yellow: hasColor ? `${ESC}[1;33m` : "",
|
|
259
|
+
cyan: hasColor ? `${ESC}[1;36m` : "",
|
|
260
|
+
white: hasColor ? `${ESC}[1;37m` : "",
|
|
261
|
+
dim: hasColor ? `${ESC}[2m` : "",
|
|
262
|
+
bold: hasColor ? `${ESC}[1m` : "",
|
|
263
|
+
reset: hasColor ? `${ESC}[0m` : "",
|
|
264
|
+
bgRed: hasColor ? `${ESC}[41m` : "",
|
|
265
|
+
bgGreen: hasColor ? `${ESC}[42m` : "",
|
|
266
|
+
bgYellow: hasColor ? `${ESC}[43m` : "",
|
|
267
|
+
bgBlue: hasColor ? `${ESC}[44m` : "",
|
|
268
|
+
reverse: hasColor ? `${ESC}[7m` : ""
|
|
224
269
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
shield: "\u{1F6E1}\uFE0F ",
|
|
232
|
-
skull: "\u{1F480}",
|
|
233
|
-
clean: "\u2728",
|
|
234
|
-
gear: "\u2699\uFE0F ",
|
|
235
|
-
pkg: "\u{1F4E6}",
|
|
236
|
-
lock: "\u{1F512}",
|
|
237
|
-
globe: "\u{1F310}",
|
|
238
|
-
folder: "\u{1F4C1}"
|
|
239
|
-
};
|
|
240
|
-
function banner(ruleCount) {
|
|
241
|
-
const w = process.stdout.write.bind(process.stdout);
|
|
242
|
-
w("\n");
|
|
243
|
-
w(`${c.cyan} ___ _ _ ___ ___ _ _ _ ___ ___ _ _ _ ${c.reset}
|
|
244
|
-
`);
|
|
245
|
-
w(`${c.cyan} / __| | | | _ \\ _ \\ | | | | | / __|/ __| /_\\ | \\| |${c.reset}
|
|
246
|
-
`);
|
|
247
|
-
w(`${c.cyan} \\__ \\ |_| | _/ _/ |_| |_| |_| (__| (__ / _ \\| .\` |${c.reset}
|
|
248
|
-
`);
|
|
249
|
-
w(`${c.cyan} |___/\\___/|_| |_| \\__, |_____|\\___|\\___|_/ \\_\\_|\\_|${c.reset}
|
|
250
|
-
`);
|
|
251
|
-
w(`${c.cyan} |___/ ${c.dim}v1.0.0${c.reset}
|
|
252
|
-
`);
|
|
253
|
-
w("\n");
|
|
254
|
-
w(` ${c.white}Universal npm Supply Chain Attack Scanner${c.reset}
|
|
255
|
-
`);
|
|
256
|
-
w(` ${c.dim}Detects ${ruleCount} known attacks | Zero dependencies${c.reset}
|
|
257
|
-
`);
|
|
258
|
-
w("\n");
|
|
259
|
-
divider();
|
|
270
|
+
function termW() {
|
|
271
|
+
return process.stdout.columns || 80;
|
|
272
|
+
}
|
|
273
|
+
var W = () => Math.min(termW(), 70);
|
|
274
|
+
function line(char = "\u2500") {
|
|
275
|
+
return `${c.dim}${char.repeat(W())}${c.reset}`;
|
|
260
276
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
277
|
+
function severityBadge(sev) {
|
|
278
|
+
const label = sev.toUpperCase();
|
|
279
|
+
if (!hasColor) return `[${label}]`;
|
|
280
|
+
const colors = {
|
|
281
|
+
critical: [220, 38, 38],
|
|
282
|
+
high: [234, 179, 8],
|
|
283
|
+
medium: [59, 130, 246],
|
|
284
|
+
low: [107, 114, 128]
|
|
285
|
+
};
|
|
286
|
+
const col = colors[sev] || [107, 114, 128];
|
|
287
|
+
return `${bg(col[0], col[1], col[2])}${c.white} ${label} ${c.reset}`;
|
|
288
|
+
}
|
|
289
|
+
function badge(type) {
|
|
290
|
+
if (!hasColor) return `[${type.toUpperCase()}]`;
|
|
291
|
+
const badges = {
|
|
292
|
+
pass: `${bg(34, 197, 94)}${c.white} PASS ${c.reset}`,
|
|
293
|
+
fail: `${bg(239, 68, 68)}${c.white} FAIL ${c.reset}`,
|
|
294
|
+
warn: `${bg(234, 179, 8)}${ESC}[30m WARN ${c.reset}`,
|
|
295
|
+
info: `${bg(107, 114, 128)}${c.white} INFO ${c.reset}`
|
|
296
|
+
};
|
|
297
|
+
return badges[type];
|
|
298
|
+
}
|
|
299
|
+
function banner(ruleCount, version = "0.0.0") {
|
|
300
|
+
const art = [
|
|
301
|
+
" \u250C\u2500\u2510\u252C \u252C\u250C\u2500\u2510\u250C\u2500\u2510\u252C \u252C \u252C \u250C\u2500\u2510\u250C\u2500\u2510\u250C\u2500\u2510\u250C\u2510\u250C",
|
|
302
|
+
" \u2514\u2500\u2510\u2502 \u2502\u251C\u2500\u2518\u251C\u2500\u2518\u2502 \u2514\u252C\u2518 \u2514\u2500\u2510\u2502 \u251C\u2500\u2524\u2502\u2502\u2502",
|
|
303
|
+
" \u2514\u2500\u2518\u2514\u2500\u2518\u2534 \u2534 \u2534\u2500\u2518 \u2534 \u2514\u2500\u2518\u2514\u2500\u2518\u2534 \u2534\u2518\u2514\u2518"
|
|
304
|
+
];
|
|
305
|
+
const vBadge = `${bg(59, 130, 246)}${c.white} v${version} ${c.reset}`;
|
|
306
|
+
console.log("");
|
|
307
|
+
for (let i = 0; i < art.length; i++) {
|
|
308
|
+
const t = art.length > 1 ? i / (art.length - 1) : 0;
|
|
309
|
+
const r = Math.round(0 + 120 * t);
|
|
310
|
+
const g = Math.round(200 - 120 * t);
|
|
311
|
+
const b = Math.round(255);
|
|
312
|
+
console.log(`${fg(r, g, b)}${art[i]}${c.reset}`);
|
|
313
|
+
}
|
|
314
|
+
console.log("");
|
|
315
|
+
console.log(` ${c.white}Universal npm Supply Chain Attack Scanner${c.reset} ${vBadge}`);
|
|
316
|
+
console.log(` ${c.dim}Detects ${ruleCount} known attacks \xB7 Zero dependencies${c.reset}`);
|
|
317
|
+
console.log("");
|
|
318
|
+
console.log(line());
|
|
263
319
|
}
|
|
264
320
|
function sectionHeader(num, total, title, icon) {
|
|
321
|
+
const w = W();
|
|
322
|
+
const step = `${bg(59, 130, 246)}${c.white} ${num}/${total} ${c.reset}`;
|
|
323
|
+
const text = ` ${icon} ${step} ${c.white}${c.bold}${title}${c.reset} `;
|
|
324
|
+
const textLen = 4 + 4 + title.length + 4;
|
|
325
|
+
const fill = Math.max(w - textLen - 2, 0);
|
|
265
326
|
console.log("");
|
|
266
|
-
console.log(`${c.
|
|
267
|
-
divider();
|
|
327
|
+
console.log(`${c.dim}\u2500\u2500${c.reset}${text}${c.dim}${"\u2500".repeat(fill)}${c.reset}`);
|
|
268
328
|
}
|
|
269
329
|
function result(type, msg) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
fail: sym.fail,
|
|
273
|
-
warn: sym.warn,
|
|
274
|
-
info: sym.info
|
|
275
|
-
};
|
|
276
|
-
const colors = {
|
|
277
|
-
pass: c.green,
|
|
278
|
-
fail: `${c.red}${c.bold}`,
|
|
279
|
-
warn: c.yellow,
|
|
280
|
-
info: c.dim
|
|
281
|
-
};
|
|
282
|
-
console.log(` ${icons[type]} ${colors[type]}${msg}${c.reset}`);
|
|
330
|
+
const color = type === "fail" ? c.red : type === "warn" ? c.yellow : type === "pass" ? c.green : c.dim;
|
|
331
|
+
console.log(` ${badge(type)} ${color}${msg}${c.reset}`);
|
|
283
332
|
}
|
|
284
333
|
function resultDetail(msg) {
|
|
285
|
-
console.log(`
|
|
334
|
+
console.log(` ${c.dim}\u21B3${c.reset} ${c.red}${msg}${c.reset}`);
|
|
335
|
+
}
|
|
336
|
+
function hideCursor() {
|
|
337
|
+
if (isTTY) process.stdout.write(`${ESC}[?25l`);
|
|
286
338
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
339
|
+
function showCursor() {
|
|
340
|
+
if (isTTY) process.stdout.write(`${ESC}[?25h`);
|
|
341
|
+
}
|
|
342
|
+
function teardownStdin(listener) {
|
|
343
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
344
|
+
process.stdin.pause();
|
|
345
|
+
process.stdin.removeListener("data", listener);
|
|
346
|
+
showCursor();
|
|
347
|
+
}
|
|
348
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
349
|
+
var SPIN_COLORS = FRAMES.map((_, i) => {
|
|
350
|
+
const r = Math.round(80 + 40 * Math.sin(i * 0.6));
|
|
351
|
+
const g = Math.round(180 + 20 * Math.cos(i * 0.6));
|
|
352
|
+
return fg(r, g, 255);
|
|
353
|
+
});
|
|
354
|
+
var spinTimer = null;
|
|
355
|
+
var spinIdx = 0;
|
|
290
356
|
function spinnerStart(msg) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
357
|
+
spinIdx = 0;
|
|
358
|
+
hideCursor();
|
|
359
|
+
spinTimer = setInterval(() => {
|
|
360
|
+
const i = spinIdx % FRAMES.length;
|
|
361
|
+
process.stdout.write(`\r${ESC}[2K ${SPIN_COLORS[i]}${FRAMES[i]}${c.reset} ${c.dim}${msg}${c.reset}`);
|
|
362
|
+
spinIdx++;
|
|
296
363
|
}, 80);
|
|
297
364
|
}
|
|
298
365
|
function spinnerStop() {
|
|
299
|
-
if (
|
|
300
|
-
clearInterval(
|
|
301
|
-
|
|
302
|
-
process.stdout.write(
|
|
366
|
+
if (spinTimer) {
|
|
367
|
+
clearInterval(spinTimer);
|
|
368
|
+
spinTimer = null;
|
|
369
|
+
process.stdout.write(`\r${ESC}[2K`);
|
|
370
|
+
showCursor();
|
|
303
371
|
}
|
|
304
372
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
373
|
+
function info(msg) {
|
|
374
|
+
console.log(` ${c.dim}${msg}${c.reset}`);
|
|
375
|
+
}
|
|
376
|
+
function success(msg) {
|
|
377
|
+
console.log(` ${c.green}\u2713${c.reset} ${msg}`);
|
|
378
|
+
}
|
|
311
379
|
function printRuleList(rules) {
|
|
312
380
|
console.log("");
|
|
313
|
-
console.log(` ${c.white}Available
|
|
381
|
+
console.log(` ${c.white}${c.bold}Available rules (${rules.length}):${c.reset}`);
|
|
314
382
|
console.log("");
|
|
315
383
|
for (const rule of rules) {
|
|
316
|
-
const sc = severityColors[rule.severity] || c.dim;
|
|
317
384
|
const pkgCount = Object.keys(rule.packages.compromised).length + Object.keys(rule.packages.malicious).length;
|
|
318
|
-
console.log(
|
|
319
|
-
|
|
320
|
-
);
|
|
321
|
-
console.log(` ${c.dim}${rule.description}${c.reset}`);
|
|
385
|
+
console.log(` ${severityBadge(rule.severity)} ${c.white}${rule.id}${c.reset} ${c.dim}\xB7 ${rule.date} \xB7 ${pkgCount} pkgs${c.reset}`);
|
|
386
|
+
console.log(` ${c.dim}${rule.description}${c.reset}`);
|
|
322
387
|
}
|
|
323
388
|
console.log("");
|
|
324
389
|
}
|
|
325
390
|
function printSummary(summary) {
|
|
326
391
|
console.log("");
|
|
327
|
-
console.log(
|
|
328
|
-
console.log(
|
|
329
|
-
console.log(
|
|
392
|
+
console.log(line("\u2550"));
|
|
393
|
+
console.log("");
|
|
394
|
+
console.log(` ${c.dim}Projects scanned${c.reset} ${c.white}${c.bold}${summary.projectsScanned}${c.reset}`);
|
|
395
|
+
console.log(` ${c.dim}Rules checked${c.reset} ${c.white}${c.bold}${summary.rulesChecked}${c.reset}`);
|
|
330
396
|
console.log("");
|
|
331
|
-
console.log(` ${c.
|
|
332
|
-
console.log(` ${c.dim}Rules checked:${c.reset} ${c.white}${summary.rulesChecked}${c.reset}`);
|
|
333
|
-
console.log(` ${c.dim}Total checks:${c.reset} ${c.white}${summary.totalChecks}${c.reset}`);
|
|
334
|
-
console.log(` ${c.green}Passed:${c.reset} ${c.green}${summary.passed}${c.reset}`);
|
|
335
|
-
console.log(` ${c.yellow}Warnings:${c.reset} ${c.yellow}${summary.warnings}${c.reset}`);
|
|
336
|
-
console.log(` ${c.red}Failed:${c.reset} ${c.red}${summary.failed}${c.reset}`);
|
|
397
|
+
console.log(` ${badge("pass")} ${c.green}${summary.passed}${c.reset} ${badge("warn")} ${c.yellow}${summary.warnings}${c.reset} ${badge("fail")} ${c.red}${summary.failed}${c.reset}`);
|
|
337
398
|
console.log("");
|
|
338
|
-
divider();
|
|
339
399
|
if (summary.failed > 0) {
|
|
400
|
+
console.log(` ${bg(220, 38, 38)}${c.white}${c.bold} \u26D4 COMPROMISE DETECTED \u26D4 ${c.reset}`);
|
|
340
401
|
console.log("");
|
|
341
|
-
console.log(` ${c.
|
|
342
|
-
console.log("");
|
|
343
|
-
console.log(` ${c.red}${c.bold}Immediate Actions Required:${c.reset}`);
|
|
344
|
-
console.log("");
|
|
402
|
+
console.log(` ${c.red}${c.bold}Immediate Actions:${c.reset}`);
|
|
345
403
|
console.log(` ${c.red}1.${c.reset} Disconnect from the network`);
|
|
346
|
-
console.log(` ${c.red}2.${c.reset} Rotate ALL credentials, tokens, API keys
|
|
347
|
-
console.log(` ${c.red}3.${c.reset}
|
|
348
|
-
console.log(` ${c.red}4.${c.reset}
|
|
349
|
-
console.log(` ${c.red}5.${c.reset} Review system for backdoors and persistence
|
|
350
|
-
console.log(` ${c.red}6.${c.reset} Consider full system wipe if RAT/worm was executed`);
|
|
351
|
-
console.log("");
|
|
404
|
+
console.log(` ${c.red}2.${c.reset} Rotate ALL credentials, tokens, API keys`);
|
|
405
|
+
console.log(` ${c.red}3.${c.reset} ${c.white}npm install <pkg>@latest${c.reset} for compromised packages`);
|
|
406
|
+
console.log(` ${c.red}4.${c.reset} ${c.white}npm cache clean --force${c.reset}`);
|
|
407
|
+
console.log(` ${c.red}5.${c.reset} Review system for backdoors and persistence`);
|
|
352
408
|
if (summary.compromisedProjects.length > 0) {
|
|
353
|
-
console.log(` ${c.red}${c.bold}Compromised Projects:${c.reset}`);
|
|
354
|
-
for (const proj of summary.compromisedProjects) {
|
|
355
|
-
console.log(` ${c.red}${sym.skull} ${proj}${c.reset}`);
|
|
356
|
-
}
|
|
357
409
|
console.log("");
|
|
410
|
+
console.log(` ${c.red}${c.bold}Compromised:${c.reset}`);
|
|
411
|
+
for (const p of summary.compromisedProjects) {
|
|
412
|
+
console.log(` ${c.red} \u2192 ${p}${c.reset}`);
|
|
413
|
+
}
|
|
358
414
|
}
|
|
359
|
-
const
|
|
415
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
360
416
|
for (const r of summary.results.filter((r2) => r2.type === "fail")) {
|
|
361
|
-
const arr =
|
|
417
|
+
const arr = byRule.get(r.rule) || [];
|
|
362
418
|
arr.push(r);
|
|
363
|
-
|
|
419
|
+
byRule.set(r.rule, arr);
|
|
364
420
|
}
|
|
365
|
-
for (const [rule, results] of
|
|
421
|
+
for (const [rule, results] of byRule) {
|
|
422
|
+
console.log("");
|
|
366
423
|
console.log(` ${c.red}${c.bold}${rule}:${c.reset}`);
|
|
367
424
|
for (const r of results) {
|
|
368
|
-
console.log(`
|
|
369
|
-
if (r.details) console.log(`
|
|
425
|
+
console.log(` ${c.red}\u2192 ${r.message}${c.reset}`);
|
|
426
|
+
if (r.details) console.log(` ${c.dim}${r.details}${c.reset}`);
|
|
370
427
|
}
|
|
371
|
-
console.log("");
|
|
372
428
|
}
|
|
373
429
|
} else if (summary.warnings > 0) {
|
|
430
|
+
console.log(` ${bg(234, 179, 8)}${ESC}[30m${c.bold} \u26A0 WARNINGS \u2014 REVIEW RECOMMENDED \u26A0 ${c.reset}`);
|
|
374
431
|
console.log("");
|
|
375
|
-
console.log(` ${c.
|
|
376
|
-
console.log("");
|
|
377
|
-
console.log(` ${c.yellow}Some items need manual review. Check the warnings above.${c.reset}`);
|
|
378
|
-
console.log("");
|
|
432
|
+
console.log(` ${c.yellow}Some items need manual review.${c.reset}`);
|
|
379
433
|
} else {
|
|
434
|
+
console.log(` ${bg(34, 197, 94)}${c.white}${c.bold} \u2713 ALL CLEAR ${c.reset}`);
|
|
380
435
|
console.log("");
|
|
381
|
-
console.log(` ${c.
|
|
382
|
-
console.log("");
|
|
383
|
-
console.log(` ${c.green}Your system appears clean from all known supply chain attacks.${c.reset}`);
|
|
436
|
+
console.log(` ${c.green}No supply chain compromises detected.${c.reset}`);
|
|
384
437
|
console.log("");
|
|
385
|
-
console.log(` ${c.dim}
|
|
386
|
-
console.log(` ${c.cyan}\u203A${c.reset} Use lockfiles and verify
|
|
438
|
+
console.log(` ${c.dim}Tips:${c.reset}`);
|
|
439
|
+
console.log(` ${c.cyan}\u203A${c.reset} Use lockfiles and verify integrity`);
|
|
387
440
|
console.log(` ${c.cyan}\u203A${c.reset} Pin dependencies to exact versions`);
|
|
388
|
-
console.log(` ${c.cyan}\u203A${c.reset} Enable npm audit in
|
|
389
|
-
console.log(` ${c.cyan}\u203A${c.reset} Monitor
|
|
390
|
-
console.log("");
|
|
441
|
+
console.log(` ${c.cyan}\u203A${c.reset} Enable npm audit in CI/CD`);
|
|
442
|
+
console.log(` ${c.cyan}\u203A${c.reset} Monitor SLSA provenance on critical packages`);
|
|
391
443
|
}
|
|
392
|
-
divider();
|
|
393
444
|
console.log("");
|
|
394
|
-
console.log(
|
|
445
|
+
console.log(line("\u2550"));
|
|
446
|
+
console.log(` ${c.dim}${(/* @__PURE__ */ new Date()).toISOString()}${c.reset}`);
|
|
395
447
|
console.log(` ${c.dim}https://github.com/AsyrafHussin/supply-scan${c.reset}`);
|
|
396
448
|
console.log("");
|
|
397
449
|
}
|
|
450
|
+
async function interactiveMultiSelect(opts) {
|
|
451
|
+
if (!process.stdin.isTTY) return opts.items.map((i) => i.value);
|
|
452
|
+
const items = opts.items;
|
|
453
|
+
const hasAll = !!opts.allOption;
|
|
454
|
+
const totalRows = hasAll ? items.length + 1 : items.length;
|
|
455
|
+
let cursor = 0;
|
|
456
|
+
const selected = /* @__PURE__ */ new Set();
|
|
457
|
+
let allSelected = true;
|
|
458
|
+
const headerLines = 3;
|
|
459
|
+
const totalLines = headerLines + totalRows;
|
|
460
|
+
function render(initial = false) {
|
|
461
|
+
if (!initial) process.stdout.write(`${ESC}[${totalLines}A`);
|
|
462
|
+
process.stdout.write(`${ESC}[2K
|
|
463
|
+
`);
|
|
464
|
+
process.stdout.write(`${ESC}[2K ${c.white}${c.bold}${opts.message}${c.reset}
|
|
465
|
+
`);
|
|
466
|
+
process.stdout.write(`${ESC}[2K ${c.dim}\u2191\u2193 navigate \xB7 space toggle \xB7 enter confirm${c.reset}
|
|
467
|
+
`);
|
|
468
|
+
if (hasAll) {
|
|
469
|
+
const active = cursor === 0;
|
|
470
|
+
const icon = allSelected ? `${c.green}\u25C9${c.reset}` : `${c.dim}\u25CB${c.reset}`;
|
|
471
|
+
const ptr = active ? `${c.cyan}\u276F${c.reset}` : " ";
|
|
472
|
+
const lbl = active ? `${c.green}${c.bold}${opts.allOption.label}${c.reset}` : `${c.dim}${opts.allOption.label}${c.reset}`;
|
|
473
|
+
process.stdout.write(`${ESC}[2K ${ptr} ${icon} ${lbl}
|
|
474
|
+
`);
|
|
475
|
+
}
|
|
476
|
+
for (let i = 0; i < items.length; i++) {
|
|
477
|
+
const idx = hasAll ? i + 1 : i;
|
|
478
|
+
const active = cursor === idx;
|
|
479
|
+
const sel = allSelected || selected.has(i);
|
|
480
|
+
const icon = sel ? `${c.green}\u25C9${c.reset}` : `${c.dim}\u25CB${c.reset}`;
|
|
481
|
+
const ptr = active ? `${c.cyan}\u276F${c.reset}` : " ";
|
|
482
|
+
const hint = items[i].hint ? ` ${c.dim}${items[i].hint}${c.reset}` : "";
|
|
483
|
+
const lbl = active ? `${c.white}${items[i].label}${c.reset}` : `${c.dim}${items[i].label}${c.reset}`;
|
|
484
|
+
process.stdout.write(`${ESC}[2K ${ptr} ${icon} ${lbl}${hint}
|
|
485
|
+
`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return new Promise((resolve2) => {
|
|
489
|
+
hideCursor();
|
|
490
|
+
render(true);
|
|
491
|
+
process.stdin.setRawMode(true);
|
|
492
|
+
process.stdin.resume();
|
|
493
|
+
const maxCursor = hasAll ? items.length : items.length - 1;
|
|
494
|
+
const onData = (data) => {
|
|
495
|
+
const key = data.toString();
|
|
496
|
+
if (key === "\x1B[A") {
|
|
497
|
+
cursor = cursor <= 0 ? maxCursor : cursor - 1;
|
|
498
|
+
render();
|
|
499
|
+
} else if (key === "\x1B[B") {
|
|
500
|
+
cursor = cursor >= maxCursor ? 0 : cursor + 1;
|
|
501
|
+
render();
|
|
502
|
+
} else if (key === " ") {
|
|
503
|
+
if (hasAll && cursor === 0) {
|
|
504
|
+
allSelected = !allSelected;
|
|
505
|
+
selected.clear();
|
|
506
|
+
} else {
|
|
507
|
+
const ii = hasAll ? cursor - 1 : cursor;
|
|
508
|
+
allSelected = false;
|
|
509
|
+
if (selected.has(ii)) selected.delete(ii);
|
|
510
|
+
else selected.add(ii);
|
|
511
|
+
if (selected.size === items.length) {
|
|
512
|
+
allSelected = true;
|
|
513
|
+
selected.clear();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
render();
|
|
517
|
+
} else if (key === "\r") {
|
|
518
|
+
teardownStdin(onData);
|
|
519
|
+
if (allSelected || selected.size === 0) {
|
|
520
|
+
resolve2(items.map((i) => i.value));
|
|
521
|
+
} else {
|
|
522
|
+
resolve2([...selected].map((i) => items[i].value));
|
|
523
|
+
}
|
|
524
|
+
} else if (key === "") {
|
|
525
|
+
teardownStdin(onData);
|
|
526
|
+
process.exit(130);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
process.stdin.on("data", onData);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
async function interactiveSingleSelect(opts) {
|
|
533
|
+
if (!process.stdin.isTTY) return opts.items[0].value;
|
|
534
|
+
const items = opts.items;
|
|
535
|
+
let cursor = 0;
|
|
536
|
+
const headerLines = 3;
|
|
537
|
+
const totalLines = headerLines + items.length;
|
|
538
|
+
function render(initial = false) {
|
|
539
|
+
if (!initial) process.stdout.write(`${ESC}[${totalLines}A`);
|
|
540
|
+
process.stdout.write(`${ESC}[2K
|
|
541
|
+
`);
|
|
542
|
+
process.stdout.write(`${ESC}[2K ${c.white}${c.bold}${opts.message}${c.reset}
|
|
543
|
+
`);
|
|
544
|
+
process.stdout.write(`${ESC}[2K ${c.dim}\u2191\u2193 navigate \xB7 enter select${c.reset}
|
|
545
|
+
`);
|
|
546
|
+
for (let i = 0; i < items.length; i++) {
|
|
547
|
+
const active = cursor === i;
|
|
548
|
+
const ptr = active ? `${c.cyan}\u276F${c.reset}` : " ";
|
|
549
|
+
const hint = items[i].hint ? ` ${c.dim}${items[i].hint}${c.reset}` : "";
|
|
550
|
+
const lbl = active ? `${c.white}${c.bold}${items[i].label}${c.reset}` : `${c.dim}${items[i].label}${c.reset}`;
|
|
551
|
+
process.stdout.write(`${ESC}[2K ${ptr} ${lbl}${hint}
|
|
552
|
+
`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return new Promise((resolve2) => {
|
|
556
|
+
hideCursor();
|
|
557
|
+
render(true);
|
|
558
|
+
process.stdin.setRawMode(true);
|
|
559
|
+
process.stdin.resume();
|
|
560
|
+
const onData = (data) => {
|
|
561
|
+
const key = data.toString();
|
|
562
|
+
if (key === "\x1B[A") {
|
|
563
|
+
cursor = cursor <= 0 ? items.length - 1 : cursor - 1;
|
|
564
|
+
render();
|
|
565
|
+
} else if (key === "\x1B[B") {
|
|
566
|
+
cursor = cursor >= items.length - 1 ? 0 : cursor + 1;
|
|
567
|
+
render();
|
|
568
|
+
} else if (key === "\r") {
|
|
569
|
+
teardownStdin(onData);
|
|
570
|
+
resolve2(items[cursor].value);
|
|
571
|
+
} else if (key === "") {
|
|
572
|
+
teardownStdin(onData);
|
|
573
|
+
process.exit(130);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
process.stdin.on("data", onData);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
398
579
|
|
|
399
580
|
// src/checks/packages.ts
|
|
400
|
-
import { join as
|
|
401
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
581
|
+
import { join as join3 } from "path";
|
|
582
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync3 } from "fs";
|
|
583
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
584
|
+
function getMaliciousPkgRegex(pkg) {
|
|
585
|
+
let re = regexCache.get(pkg);
|
|
586
|
+
if (!re) {
|
|
587
|
+
re = new RegExp(`["'/]${escapeRegex(pkg)}["'/@:]`);
|
|
588
|
+
regexCache.set(pkg, re);
|
|
589
|
+
}
|
|
590
|
+
return re;
|
|
591
|
+
}
|
|
402
592
|
function checkPackages(rules, projectDirs) {
|
|
403
593
|
const results = [];
|
|
594
|
+
for (const rule of rules) {
|
|
595
|
+
for (const pkg of Object.keys(rule.packages.malicious)) {
|
|
596
|
+
getMaliciousPkgRegex(pkg);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
404
599
|
for (const dir of projectDirs) {
|
|
405
600
|
const lockfileCache = readLockfiles(dir);
|
|
601
|
+
const versionCache = /* @__PURE__ */ new Map();
|
|
406
602
|
for (const rule of rules) {
|
|
407
603
|
for (const [pkg, badVersions] of Object.entries(rule.packages.compromised)) {
|
|
408
|
-
const
|
|
604
|
+
const pkgJsonPath = join3(dir, "node_modules", pkg, "package.json");
|
|
605
|
+
let version = versionCache.get(pkgJsonPath);
|
|
606
|
+
if (version === void 0) {
|
|
607
|
+
version = getPkgVersion(pkgJsonPath);
|
|
608
|
+
versionCache.set(pkgJsonPath, version);
|
|
609
|
+
}
|
|
409
610
|
if (version && isVersionCompromised(version, badVersions)) {
|
|
410
611
|
results.push({
|
|
411
612
|
type: "fail",
|
|
@@ -417,9 +618,14 @@ function checkPackages(rules, projectDirs) {
|
|
|
417
618
|
}
|
|
418
619
|
}
|
|
419
620
|
for (const [pkg] of Object.entries(rule.packages.malicious)) {
|
|
420
|
-
const pkgDir =
|
|
621
|
+
const pkgDir = join3(dir, "node_modules", pkg);
|
|
421
622
|
if (dirExists(pkgDir)) {
|
|
422
|
-
const
|
|
623
|
+
const pkgJsonPath = join3(pkgDir, "package.json");
|
|
624
|
+
let version = versionCache.get(pkgJsonPath);
|
|
625
|
+
if (version === void 0) {
|
|
626
|
+
version = getPkgVersion(pkgJsonPath);
|
|
627
|
+
versionCache.set(pkgJsonPath, version);
|
|
628
|
+
}
|
|
423
629
|
results.push({
|
|
424
630
|
type: "fail",
|
|
425
631
|
rule: rule.id,
|
|
@@ -434,21 +640,20 @@ function checkPackages(rules, projectDirs) {
|
|
|
434
640
|
}
|
|
435
641
|
return results;
|
|
436
642
|
}
|
|
643
|
+
var LOCKFILE_NAMES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb"];
|
|
437
644
|
function readLockfiles(dir) {
|
|
438
645
|
const cache = /* @__PURE__ */ new Map();
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
];
|
|
448
|
-
for (const name of lockfiles) {
|
|
646
|
+
let dirEntries;
|
|
647
|
+
try {
|
|
648
|
+
dirEntries = new Set(readdirSync3(dir));
|
|
649
|
+
} catch {
|
|
650
|
+
return cache;
|
|
651
|
+
}
|
|
652
|
+
for (const name of LOCKFILE_NAMES) {
|
|
653
|
+
if (!dirEntries.has(name)) continue;
|
|
449
654
|
try {
|
|
450
655
|
const encoding = name === "bun.lockb" ? "latin1" : "utf-8";
|
|
451
|
-
cache.set(name, readFileSync2(
|
|
656
|
+
cache.set(name, readFileSync2(join3(dir, name), encoding));
|
|
452
657
|
} catch {
|
|
453
658
|
}
|
|
454
659
|
}
|
|
@@ -457,7 +662,7 @@ function readLockfiles(dir) {
|
|
|
457
662
|
function checkLockfilesForRule(lockfileCache, dir, rule, results) {
|
|
458
663
|
for (const [lockfile, content] of lockfileCache) {
|
|
459
664
|
for (const pkg of Object.keys(rule.packages.malicious)) {
|
|
460
|
-
if (
|
|
665
|
+
if (getMaliciousPkgRegex(pkg).test(content)) {
|
|
461
666
|
results.push({
|
|
462
667
|
type: "fail",
|
|
463
668
|
rule: rule.id,
|
|
@@ -471,11 +676,8 @@ function checkLockfilesForRule(lockfileCache, dir, rule, results) {
|
|
|
471
676
|
for (const ver of versions) {
|
|
472
677
|
const tied = [
|
|
473
678
|
`"${pkg}": "${ver}"`,
|
|
474
|
-
// package-lock.json / bun.lock
|
|
475
679
|
`${pkg}@${ver}`,
|
|
476
|
-
// yarn.lock / pnpm-lock.yaml
|
|
477
680
|
`"${pkg}","${ver}"`
|
|
478
|
-
// bun.lockb binary format
|
|
479
681
|
];
|
|
480
682
|
if (tied.some((p) => content.includes(p))) {
|
|
481
683
|
results.push({
|
|
@@ -516,47 +718,70 @@ function checkFiles(rules) {
|
|
|
516
718
|
|
|
517
719
|
// src/checks/network.ts
|
|
518
720
|
import { readFileSync as readFileSync3 } from "fs";
|
|
721
|
+
|
|
722
|
+
// src/shell.ts
|
|
723
|
+
import { execSync, execFileSync } from "child_process";
|
|
724
|
+
var TIMEOUT = 1e4;
|
|
725
|
+
function runCommand(cmd) {
|
|
726
|
+
try {
|
|
727
|
+
return execSync(cmd, { encoding: "utf-8", timeout: TIMEOUT, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
728
|
+
} catch {
|
|
729
|
+
return "";
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function runSafe(cmd, args) {
|
|
733
|
+
try {
|
|
734
|
+
return execFileSync(cmd, args, { encoding: "utf-8", timeout: TIMEOUT, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
735
|
+
} catch {
|
|
736
|
+
return "";
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/checks/network.ts
|
|
519
741
|
function checkNetwork(rules) {
|
|
520
742
|
const results = [];
|
|
521
743
|
const os = getOS();
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
const ruleByIOC = /* @__PURE__ */ new Map();
|
|
744
|
+
const rulesByIP = /* @__PURE__ */ new Map();
|
|
745
|
+
const rulesByDomain = /* @__PURE__ */ new Map();
|
|
525
746
|
for (const rule of rules) {
|
|
526
747
|
for (const ip of rule.ioc.ips ?? []) {
|
|
527
|
-
|
|
528
|
-
|
|
748
|
+
const arr = rulesByIP.get(ip) || [];
|
|
749
|
+
arr.push(rule.id);
|
|
750
|
+
rulesByIP.set(ip, arr);
|
|
529
751
|
}
|
|
530
752
|
for (const domain of rule.ioc.domains ?? []) {
|
|
531
|
-
|
|
532
|
-
|
|
753
|
+
const arr = rulesByDomain.get(domain) || [];
|
|
754
|
+
arr.push(rule.id);
|
|
755
|
+
rulesByDomain.set(domain, arr);
|
|
533
756
|
}
|
|
534
757
|
}
|
|
535
|
-
if (
|
|
536
|
-
const
|
|
537
|
-
for (const ip of
|
|
538
|
-
|
|
758
|
+
if (rulesByIP.size === 0 && rulesByDomain.size === 0) return results;
|
|
759
|
+
const connectionLines = getActiveConnections(os).split("\n");
|
|
760
|
+
for (const [ip, ruleIds] of rulesByIP) {
|
|
761
|
+
const pattern = new RegExp(`(?:^|[\\s:])${escapeRegex(ip)}(?:[\\s:]|$)`);
|
|
762
|
+
if (connectionLines.some((line2) => pattern.test(line2))) {
|
|
539
763
|
results.push({
|
|
540
764
|
type: "fail",
|
|
541
|
-
rule:
|
|
765
|
+
rule: ruleIds[0],
|
|
542
766
|
check: "network",
|
|
543
767
|
message: `Active connection to C2 IP: ${ip}`,
|
|
544
|
-
details: "Disconnect from network immediately
|
|
768
|
+
details: `Rules: ${ruleIds.join(", ")} \u2014 Disconnect from network immediately!`
|
|
545
769
|
});
|
|
546
770
|
}
|
|
547
771
|
}
|
|
548
|
-
for (const domain of
|
|
549
|
-
|
|
772
|
+
for (const [domain, ruleIds] of rulesByDomain) {
|
|
773
|
+
const pattern = new RegExp(`(?:^|[\\s:])${escapeRegex(domain)}(?:[\\s:.]|$)`);
|
|
774
|
+
if (connectionLines.some((line2) => pattern.test(line2))) {
|
|
550
775
|
results.push({
|
|
551
776
|
type: "fail",
|
|
552
|
-
rule:
|
|
777
|
+
rule: ruleIds[0],
|
|
553
778
|
check: "network",
|
|
554
779
|
message: `Active connection to C2 domain: ${domain}`,
|
|
555
|
-
details: "Disconnect from network immediately
|
|
780
|
+
details: `Rules: ${ruleIds.join(", ")} \u2014 Disconnect from network immediately!`
|
|
556
781
|
});
|
|
557
782
|
}
|
|
558
783
|
}
|
|
559
|
-
checkHostsFile(
|
|
784
|
+
checkHostsFile(os, rulesByIP, rulesByDomain, results);
|
|
560
785
|
return results;
|
|
561
786
|
}
|
|
562
787
|
function getActiveConnections(os) {
|
|
@@ -567,26 +792,29 @@ function getActiveConnections(os) {
|
|
|
567
792
|
}
|
|
568
793
|
return "";
|
|
569
794
|
}
|
|
570
|
-
function checkHostsFile(ips, domains,
|
|
795
|
+
function checkHostsFile(os, ips, domains, results) {
|
|
796
|
+
const hostsPath = os === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts";
|
|
571
797
|
try {
|
|
572
|
-
const
|
|
573
|
-
for (const ip of ips) {
|
|
574
|
-
|
|
798
|
+
const hostsLines = readFileSync3(hostsPath, "utf-8").split("\n");
|
|
799
|
+
for (const [ip, ruleIds] of ips) {
|
|
800
|
+
const pattern = new RegExp(`(?:^|\\s)${escapeRegex(ip)}(?:\\s|$)`);
|
|
801
|
+
if (hostsLines.some((line2) => pattern.test(line2))) {
|
|
575
802
|
results.push({
|
|
576
803
|
type: "warn",
|
|
577
|
-
rule:
|
|
804
|
+
rule: ruleIds[0],
|
|
578
805
|
check: "network",
|
|
579
|
-
message: `C2 IP ${ip} found in
|
|
806
|
+
message: `C2 IP ${ip} found in hosts file`
|
|
580
807
|
});
|
|
581
808
|
}
|
|
582
809
|
}
|
|
583
|
-
for (const domain of domains) {
|
|
584
|
-
|
|
810
|
+
for (const [domain, ruleIds] of domains) {
|
|
811
|
+
const pattern = new RegExp(`(?:^|\\s)${escapeRegex(domain)}(?:\\s|$)`);
|
|
812
|
+
if (hostsLines.some((line2) => pattern.test(line2))) {
|
|
585
813
|
results.push({
|
|
586
814
|
type: "warn",
|
|
587
|
-
rule:
|
|
815
|
+
rule: ruleIds[0],
|
|
588
816
|
check: "network",
|
|
589
|
-
message: `C2 domain ${domain} found in
|
|
817
|
+
message: `C2 domain ${domain} found in hosts file`
|
|
590
818
|
});
|
|
591
819
|
}
|
|
592
820
|
}
|
|
@@ -598,23 +826,26 @@ function checkHostsFile(ips, domains, ruleByIOC, results) {
|
|
|
598
826
|
function checkProcesses(rules) {
|
|
599
827
|
const results = [];
|
|
600
828
|
const os = getOS();
|
|
601
|
-
const
|
|
829
|
+
const rulesByProcess = /* @__PURE__ */ new Map();
|
|
602
830
|
for (const rule of rules) {
|
|
603
831
|
for (const proc of rule.ioc.processes ?? []) {
|
|
604
|
-
|
|
832
|
+
const arr = rulesByProcess.get(proc) || [];
|
|
833
|
+
arr.push(rule.id);
|
|
834
|
+
rulesByProcess.set(proc, arr);
|
|
605
835
|
}
|
|
606
836
|
}
|
|
607
|
-
if (
|
|
608
|
-
const
|
|
609
|
-
if (
|
|
610
|
-
for (const [pattern,
|
|
611
|
-
|
|
837
|
+
if (rulesByProcess.size === 0) return results;
|
|
838
|
+
const processLines = getProcessList(os).split("\n");
|
|
839
|
+
if (processLines.length === 0) return results;
|
|
840
|
+
for (const [pattern, ruleIds] of rulesByProcess) {
|
|
841
|
+
const regex = new RegExp(`(?:^|[\\s/])${escapeRegex(pattern)}(?:\\s|$)`);
|
|
842
|
+
if (processLines.some((line2) => regex.test(line2))) {
|
|
612
843
|
results.push({
|
|
613
844
|
type: "fail",
|
|
614
|
-
rule:
|
|
845
|
+
rule: ruleIds[0],
|
|
615
846
|
check: "processes",
|
|
616
847
|
message: `Suspicious process running: ${pattern}`,
|
|
617
|
-
details: "
|
|
848
|
+
details: `Rules: ${ruleIds.join(", ")} \u2014 May indicate active compromise`
|
|
618
849
|
});
|
|
619
850
|
}
|
|
620
851
|
}
|
|
@@ -652,12 +883,10 @@ function checkMacOSPersistence(rules, results) {
|
|
|
652
883
|
const existingDirs = launchDirs.filter(dirExists);
|
|
653
884
|
if (existingDirs.length === 0 || searchStrings.size === 0) return;
|
|
654
885
|
const pattern = Array.from(searchStrings.keys()).join("|");
|
|
655
|
-
const hits =
|
|
656
|
-
`grep -rlE "${pattern}" ${existingDirs.map((d) => `"${d}"`).join(" ")} 2>/dev/null`
|
|
657
|
-
);
|
|
886
|
+
const hits = runSafe("grep", ["-rlE", pattern, ...existingDirs]);
|
|
658
887
|
if (hits) {
|
|
659
888
|
for (const file of hits.split("\n").filter(Boolean)) {
|
|
660
|
-
const content =
|
|
889
|
+
const content = runSafe("cat", [file]);
|
|
661
890
|
let matchedRule = "unknown";
|
|
662
891
|
for (const [s, ruleId] of searchStrings) {
|
|
663
892
|
if (content.includes(s)) {
|
|
@@ -677,12 +906,12 @@ function checkMacOSPersistence(rules, results) {
|
|
|
677
906
|
}
|
|
678
907
|
|
|
679
908
|
// src/checks/cache.ts
|
|
680
|
-
import { join as
|
|
909
|
+
import { join as join4 } from "path";
|
|
681
910
|
import { homedir as homedir2 } from "os";
|
|
682
911
|
function detectCaches() {
|
|
683
912
|
const home = homedir2();
|
|
684
913
|
const caches = [];
|
|
685
|
-
const npmCache = runCommand("npm config get cache") ||
|
|
914
|
+
const npmCache = runCommand("npm config get cache") || join4(home, ".npm");
|
|
686
915
|
if (dirExists(npmCache)) {
|
|
687
916
|
caches.push({ name: "npm", dir: npmCache, cleanCmd: "npm cache clean --force" });
|
|
688
917
|
}
|
|
@@ -694,11 +923,11 @@ function detectCaches() {
|
|
|
694
923
|
if (yarnCache && dirExists(yarnCache)) {
|
|
695
924
|
caches.push({ name: "yarn", dir: yarnCache, cleanCmd: "yarn cache clean" });
|
|
696
925
|
}
|
|
697
|
-
const yarnBerryCache =
|
|
926
|
+
const yarnBerryCache = join4(process.cwd(), ".yarn", "cache");
|
|
698
927
|
if (dirExists(yarnBerryCache)) {
|
|
699
928
|
caches.push({ name: "yarn-berry", dir: yarnBerryCache, cleanCmd: "yarn cache clean --all" });
|
|
700
929
|
}
|
|
701
|
-
const bunCache =
|
|
930
|
+
const bunCache = join4(home, ".bun", "install", "cache");
|
|
702
931
|
if (dirExists(bunCache)) {
|
|
703
932
|
caches.push({ name: "bun", dir: bunCache, cleanCmd: "bun pm cache rm" });
|
|
704
933
|
}
|
|
@@ -726,48 +955,40 @@ function checkCache(rules) {
|
|
|
726
955
|
}
|
|
727
956
|
return results;
|
|
728
957
|
}
|
|
729
|
-
function
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
type: "fail",
|
|
742
|
-
rule: ruleId,
|
|
743
|
-
check: "cache",
|
|
744
|
-
message: `Malicious package "${name}" found in ${cache.name} cache`,
|
|
745
|
-
details: `Run: ${cache.cleanCmd}`
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
}
|
|
958
|
+
function findInCache(cacheDir, names, type) {
|
|
959
|
+
const hits = /* @__PURE__ */ new Map();
|
|
960
|
+
if (names.size === 0) return hits;
|
|
961
|
+
const nameExprs = Array.from(names.keys()).flatMap(
|
|
962
|
+
(n, i) => i === 0 ? ["-name", n] : ["-o", "-name", n]
|
|
963
|
+
);
|
|
964
|
+
const found = runSafe("find", [cacheDir, "-maxdepth", "4", "-type", type, "(", ...nameExprs, ")"]);
|
|
965
|
+
if (found) {
|
|
966
|
+
for (const line2 of found.split("\n").filter(Boolean)) {
|
|
967
|
+
const name = line2.split("/").pop() || "";
|
|
968
|
+
const ruleId = names.get(name);
|
|
969
|
+
if (ruleId) hits.set(name, ruleId);
|
|
749
970
|
}
|
|
750
971
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
}
|
|
972
|
+
return hits;
|
|
973
|
+
}
|
|
974
|
+
function scanCacheDir(cache, maliciousPkgs, compromisedTarballs, results) {
|
|
975
|
+
for (const [name, ruleId] of findInCache(cache.dir, maliciousPkgs, "d")) {
|
|
976
|
+
results.push({
|
|
977
|
+
type: "fail",
|
|
978
|
+
rule: ruleId,
|
|
979
|
+
check: "cache",
|
|
980
|
+
message: `Malicious package "${name}" found in ${cache.name} cache`,
|
|
981
|
+
details: `Run: ${cache.cleanCmd}`
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
for (const [name, ruleId] of findInCache(cache.dir, compromisedTarballs, "f")) {
|
|
985
|
+
results.push({
|
|
986
|
+
type: "warn",
|
|
987
|
+
rule: ruleId,
|
|
988
|
+
check: "cache",
|
|
989
|
+
message: `Compromised tarball ${name} in ${cache.name} cache`,
|
|
990
|
+
details: `Run: ${cache.cleanCmd}`
|
|
991
|
+
});
|
|
771
992
|
}
|
|
772
993
|
}
|
|
773
994
|
|
|
@@ -830,19 +1051,27 @@ function displayResults(results, passMessage) {
|
|
|
830
1051
|
}
|
|
831
1052
|
function buildSummary(results, projectsScanned, rulesChecked, totalChecks) {
|
|
832
1053
|
const compromisedProjects = /* @__PURE__ */ new Set();
|
|
1054
|
+
let failCount = 0;
|
|
1055
|
+
let warnCount = 0;
|
|
1056
|
+
let passCount = 0;
|
|
833
1057
|
for (const r of results) {
|
|
834
|
-
if (r.type === "fail"
|
|
835
|
-
|
|
836
|
-
if (
|
|
1058
|
+
if (r.type === "fail") {
|
|
1059
|
+
failCount++;
|
|
1060
|
+
if (r.details) {
|
|
1061
|
+
const match = r.details.match(/— (.+)$/);
|
|
1062
|
+
if (match) compromisedProjects.add(match[1]);
|
|
1063
|
+
}
|
|
1064
|
+
} else if (r.type === "warn") {
|
|
1065
|
+
warnCount++;
|
|
1066
|
+
} else if (r.type === "pass") {
|
|
1067
|
+
passCount++;
|
|
837
1068
|
}
|
|
838
1069
|
}
|
|
839
|
-
const failCount = results.filter((r) => r.type === "fail").length;
|
|
840
|
-
const warnCount = results.filter((r) => r.type === "warn").length;
|
|
841
1070
|
return {
|
|
842
1071
|
projectsScanned,
|
|
843
1072
|
rulesChecked,
|
|
844
1073
|
totalChecks: results.length || totalChecks,
|
|
845
|
-
passed: results.length === 0 ? totalChecks :
|
|
1074
|
+
passed: results.length === 0 ? totalChecks : passCount,
|
|
846
1075
|
failed: failCount,
|
|
847
1076
|
warnings: warnCount,
|
|
848
1077
|
results,
|
|
@@ -853,8 +1082,9 @@ function buildSummary(results, projectsScanned, rulesChecked, totalChecks) {
|
|
|
853
1082
|
// src/index.ts
|
|
854
1083
|
var __filename = fileURLToPath(import.meta.url);
|
|
855
1084
|
var __dirname = dirname(__filename);
|
|
856
|
-
var RULES_DIR =
|
|
857
|
-
var
|
|
1085
|
+
var RULES_DIR = join5(__dirname, "..", "rules");
|
|
1086
|
+
var PKG_JSON_PATH = join5(__dirname, "..", "package.json");
|
|
1087
|
+
var VERSION = readJSON(PKG_JSON_PATH)?.version ?? "0.0.0";
|
|
858
1088
|
var HELP_TEXT = `
|
|
859
1089
|
${c.white}supply-scan${c.reset} \u2014 Universal npm supply chain attack scanner
|
|
860
1090
|
|
|
@@ -888,53 +1118,49 @@ async function run(argv) {
|
|
|
888
1118
|
const opts = parseArgs(argv);
|
|
889
1119
|
if (opts.version) {
|
|
890
1120
|
console.log(VERSION);
|
|
891
|
-
|
|
1121
|
+
return 0;
|
|
892
1122
|
}
|
|
893
1123
|
if (opts.help) {
|
|
894
1124
|
console.log(HELP_TEXT);
|
|
895
|
-
|
|
1125
|
+
return 0;
|
|
896
1126
|
}
|
|
897
1127
|
const allRules = loadRules(RULES_DIR);
|
|
898
1128
|
if (allRules.length === 0) {
|
|
899
1129
|
console.error(`${c.red}No rules found in ${RULES_DIR}${c.reset}`);
|
|
900
|
-
|
|
1130
|
+
return 1;
|
|
901
1131
|
}
|
|
902
1132
|
if (opts.list) {
|
|
903
|
-
banner(allRules.length);
|
|
1133
|
+
banner(allRules.length, VERSION);
|
|
904
1134
|
printRuleList(allRules);
|
|
905
|
-
|
|
1135
|
+
return 0;
|
|
906
1136
|
}
|
|
907
1137
|
let selectedRules;
|
|
908
1138
|
if (opts.rules.length > 0) {
|
|
909
1139
|
selectedRules = allRules.filter((r) => opts.rules.includes(r.id));
|
|
910
1140
|
if (selectedRules.length === 0) {
|
|
911
1141
|
console.error(`${c.red}No matching rules found. Use --list to see available rules.${c.reset}`);
|
|
912
|
-
|
|
1142
|
+
return 1;
|
|
913
1143
|
}
|
|
914
1144
|
} else if (opts.all || opts.ci) {
|
|
915
1145
|
selectedRules = allRules;
|
|
916
1146
|
} else {
|
|
917
|
-
banner(allRules.length);
|
|
1147
|
+
banner(allRules.length, VERSION);
|
|
918
1148
|
selectedRules = await interactiveRuleSelection(allRules);
|
|
919
1149
|
}
|
|
920
1150
|
if (!opts.ci && (opts.all || opts.rules.length > 0)) {
|
|
921
|
-
banner(selectedRules.length);
|
|
1151
|
+
banner(selectedRules.length, VERSION);
|
|
922
1152
|
}
|
|
923
1153
|
let projectDirs;
|
|
924
1154
|
if (opts.path) {
|
|
925
|
-
|
|
926
|
-
projectDirs = findProjects([opts.path]);
|
|
927
|
-
if (!opts.ci) spinnerStop();
|
|
1155
|
+
projectDirs = discoverProjects([opts.path], opts.ci);
|
|
928
1156
|
} else if (opts.ci || opts.all) {
|
|
929
|
-
|
|
930
|
-
projectDirs = findProjects([process.cwd()]);
|
|
931
|
-
if (!opts.ci) spinnerStop();
|
|
1157
|
+
projectDirs = discoverProjects([process.cwd()], opts.ci);
|
|
932
1158
|
} else {
|
|
933
1159
|
projectDirs = await interactivePathSelection();
|
|
934
1160
|
}
|
|
935
1161
|
if (!opts.ci) {
|
|
936
|
-
console.log(
|
|
937
|
-
|
|
1162
|
+
console.log("");
|
|
1163
|
+
info(`Scanning ${projectDirs.length} projects with ${selectedRules.length} rules...`);
|
|
938
1164
|
}
|
|
939
1165
|
const summary = await scan({
|
|
940
1166
|
rules: selectedRules,
|
|
@@ -945,55 +1171,70 @@ async function run(argv) {
|
|
|
945
1171
|
printSummary(summary);
|
|
946
1172
|
}
|
|
947
1173
|
if (summary.failed > 0) {
|
|
948
|
-
|
|
1174
|
+
return 1;
|
|
949
1175
|
} else if (summary.warnings > 0) {
|
|
950
|
-
|
|
1176
|
+
return 2;
|
|
951
1177
|
} else {
|
|
952
1178
|
if (opts.ci) {
|
|
953
1179
|
console.log("OK");
|
|
954
1180
|
}
|
|
955
|
-
|
|
1181
|
+
return 0;
|
|
956
1182
|
}
|
|
957
1183
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const r = rules[i];
|
|
965
|
-
const sc = severityColors[r.severity] || c.dim;
|
|
966
|
-
console.log(` ${c.cyan}${i + 1}${c.reset}) ${r.name} ${sc}(${r.severity})${c.reset}`);
|
|
1184
|
+
function discoverProjects(dirs, ci) {
|
|
1185
|
+
if (!ci) spinnerStart("Searching for npm projects...");
|
|
1186
|
+
const projects = findProjects(dirs);
|
|
1187
|
+
if (!ci) {
|
|
1188
|
+
spinnerStop();
|
|
1189
|
+
success(`Found ${projects.length} projects`);
|
|
967
1190
|
}
|
|
1191
|
+
return projects;
|
|
1192
|
+
}
|
|
1193
|
+
async function interactiveRuleSelection(rules) {
|
|
968
1194
|
console.log("");
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1195
|
+
const selected = await interactiveMultiSelect({
|
|
1196
|
+
message: "Select attacks to scan:",
|
|
1197
|
+
items: rules.map((r) => ({
|
|
1198
|
+
label: r.name,
|
|
1199
|
+
value: r,
|
|
1200
|
+
hint: `${r.severity} ${r.date}`
|
|
1201
|
+
})),
|
|
1202
|
+
allOption: { label: `All attacks (${rules.length} rules)` }
|
|
1203
|
+
});
|
|
975
1204
|
return selected.length > 0 ? selected : rules;
|
|
976
1205
|
}
|
|
977
1206
|
async function interactivePathSelection() {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1207
|
+
const choice = await interactiveSingleSelect({
|
|
1208
|
+
message: "Where should I scan?",
|
|
1209
|
+
items: [
|
|
1210
|
+
{ label: "Current directory", value: "cwd", hint: process.cwd() },
|
|
1211
|
+
{ label: "Common project directories", value: "common", hint: "Desktop, Projects, dev..." },
|
|
1212
|
+
{ label: "Entire home directory", value: "home", hint: "(slower)" },
|
|
1213
|
+
{ label: "Enter a custom path", value: "custom" }
|
|
1214
|
+
]
|
|
1215
|
+
});
|
|
986
1216
|
let baseDirs;
|
|
987
1217
|
switch (choice) {
|
|
988
|
-
case "
|
|
1218
|
+
case "common":
|
|
989
1219
|
baseDirs = getCommonProjectDirs();
|
|
990
1220
|
if (baseDirs.length === 0) {
|
|
991
|
-
|
|
1221
|
+
info("No common directories found, scanning current directory...");
|
|
992
1222
|
baseDirs = [process.cwd()];
|
|
1223
|
+
} else {
|
|
1224
|
+
info(`Scanning ${baseDirs.length} directories:`);
|
|
1225
|
+
for (const d of baseDirs.slice(0, 5)) {
|
|
1226
|
+
console.log(` ${c.dim}${d}${c.reset}`);
|
|
1227
|
+
}
|
|
1228
|
+
if (baseDirs.length > 5) console.log(` ${c.dim}...and ${baseDirs.length - 5} more${c.reset}`);
|
|
993
1229
|
}
|
|
994
1230
|
break;
|
|
995
|
-
case "
|
|
996
|
-
|
|
1231
|
+
case "home":
|
|
1232
|
+
baseDirs = [process.env.HOME || "/"];
|
|
1233
|
+
info(`Scanning home directory (this may take a while)...`);
|
|
1234
|
+
break;
|
|
1235
|
+
case "custom": {
|
|
1236
|
+
const customPath = await prompt(`
|
|
1237
|
+
${c.cyan}Enter path:${c.reset} `);
|
|
997
1238
|
baseDirs = [customPath.replace(/^~/, process.env.HOME || "")];
|
|
998
1239
|
break;
|
|
999
1240
|
}
|
|
@@ -1004,12 +1245,15 @@ async function interactivePathSelection() {
|
|
|
1004
1245
|
spinnerStart("Searching for npm projects...");
|
|
1005
1246
|
const projects = findProjects(baseDirs);
|
|
1006
1247
|
spinnerStop();
|
|
1007
|
-
|
|
1248
|
+
success(`Found ${projects.length} npm projects`);
|
|
1008
1249
|
return projects;
|
|
1009
1250
|
}
|
|
1010
|
-
var isCLI = process.argv[1]?.
|
|
1251
|
+
var isCLI = process.argv[1]?.endsWith("supply-scan") || process.argv[1]?.endsWith("dist/index.js") || process.argv[1]?.includes("/supply-scan/");
|
|
1011
1252
|
if (isCLI) {
|
|
1012
|
-
run(process.argv.slice(2))
|
|
1253
|
+
run(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
|
|
1254
|
+
console.error(`${c.red}Error: ${err instanceof Error ? err.message : err}${c.reset}`);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
});
|
|
1013
1257
|
}
|
|
1014
1258
|
export {
|
|
1015
1259
|
run
|