scan-compromised 1.0.16 → 1.0.18
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/package.json +1 -1
- package/scripts/index.js +83 -23
package/package.json
CHANGED
package/scripts/index.js
CHANGED
|
@@ -1,24 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import
|
|
3
|
+
import { join, isAbsolute, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import util from "util";
|
|
6
|
+
import CONFIG from "../config/config.js";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} = CONFIG;
|
|
8
|
+
// Create a debug logger for the "threats" namespace
|
|
9
|
+
const debug = util.debuglog("threats");
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const { threatsFile } = CONFIG;
|
|
15
|
+
if (!threatsFile) throw new Error("Set threatsFile in config");
|
|
16
|
+
|
|
17
|
+
debug("CONFIG.threatsFile = %s", threatsFile);
|
|
18
|
+
|
|
19
|
+
let resolvedThreatsPath;
|
|
20
|
+
|
|
21
|
+
if (isAbsolute(threatsFile)) {
|
|
22
|
+
debug("Path is absolute");
|
|
23
|
+
resolvedThreatsPath = threatsFile;
|
|
24
|
+
} else {
|
|
25
|
+
debug("Path is relative");
|
|
26
|
+
|
|
27
|
+
const localPath = join(process.cwd(), threatsFile);
|
|
28
|
+
debug("Checking local path: %s -> %s", localPath, existsSync(localPath));
|
|
29
|
+
|
|
30
|
+
const cliPath = join(__dirname, "..", threatsFile);
|
|
31
|
+
debug("Checking CLI path: %s -> %s", cliPath, existsSync(cliPath));
|
|
32
|
+
|
|
33
|
+
if (existsSync(localPath)) {
|
|
34
|
+
resolvedThreatsPath = localPath;
|
|
35
|
+
} else if (existsSync(cliPath)) {
|
|
36
|
+
resolvedThreatsPath = cliPath;
|
|
16
37
|
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!resolvedThreatsPath || !existsSync(resolvedThreatsPath)) {
|
|
41
|
+
console.error(`❌ ${threatsFile} not found in project or CLI directory.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
debug("Using threats file: %s", resolvedThreatsPath);
|
|
46
|
+
|
|
47
|
+
function loadThreats() {
|
|
48
|
+
debug("Loading threats from %s", resolvedThreatsPath);
|
|
17
49
|
try {
|
|
18
|
-
const raw = readFileSync(
|
|
19
|
-
|
|
50
|
+
const raw = readFileSync(resolvedThreatsPath, "utf8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
debug("Loaded %d packages from threats file", Object.keys(parsed).length);
|
|
53
|
+
return parsed;
|
|
20
54
|
} catch (err) {
|
|
21
|
-
console.error(
|
|
55
|
+
console.error(`❌ Failed to parse ${resolvedThreatsPath}:`, err.message);
|
|
22
56
|
process.exit(1);
|
|
23
57
|
}
|
|
24
58
|
}
|
|
@@ -37,21 +71,24 @@ function escRe(s) {
|
|
|
37
71
|
}
|
|
38
72
|
|
|
39
73
|
function getCompromisedMap() {
|
|
74
|
+
debug("Building compromised map");
|
|
40
75
|
const map = new Map();
|
|
41
76
|
for (const [pkg, versions] of Object.entries(compromised)) {
|
|
42
77
|
map.set(pkg, new Set(versions));
|
|
43
78
|
}
|
|
79
|
+
debug("Compromised map contains %d packages", map.size);
|
|
44
80
|
return map;
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
function recordFinding(findings, type, file, pkg, version, where) {
|
|
84
|
+
debug("Recording finding: %s %s@%s in %s (%s)", type, pkg, version, file, where);
|
|
48
85
|
findings.push({ type, file, pkg, version, where });
|
|
49
86
|
}
|
|
50
87
|
|
|
51
88
|
// -------- Scanners --------
|
|
52
89
|
|
|
53
|
-
// package.json
|
|
54
90
|
function scanPackageJson(content, bad) {
|
|
91
|
+
debug("Scanning package.json");
|
|
55
92
|
const findings = [];
|
|
56
93
|
try {
|
|
57
94
|
const json = JSON.parse(content);
|
|
@@ -61,6 +98,7 @@ function scanPackageJson(content, bad) {
|
|
|
61
98
|
...(json.peerDependencies || {}),
|
|
62
99
|
...(json.optionalDependencies || {})
|
|
63
100
|
};
|
|
101
|
+
debug("Found %d dependencies in package.json", Object.keys(allDeps).length);
|
|
64
102
|
for (const [pkg, range] of Object.entries(allDeps)) {
|
|
65
103
|
if (!bad.has(pkg)) continue;
|
|
66
104
|
let type = "warn";
|
|
@@ -72,16 +110,18 @@ function scanPackageJson(content, bad) {
|
|
|
72
110
|
}
|
|
73
111
|
recordFinding(findings, type, "package.json", pkg, range, "declared dependency");
|
|
74
112
|
}
|
|
75
|
-
} catch {
|
|
113
|
+
} catch (err) {
|
|
114
|
+
debug("Error parsing package.json: %s", err.message);
|
|
115
|
+
}
|
|
116
|
+
debug("package.json scan complete: %d findings", findings.length);
|
|
76
117
|
return findings;
|
|
77
118
|
}
|
|
78
119
|
|
|
79
|
-
// package-lock.json
|
|
80
120
|
function scanPackageLock(content, bad) {
|
|
121
|
+
debug("Scanning package-lock.json");
|
|
81
122
|
const findings = [];
|
|
82
123
|
try {
|
|
83
124
|
const lock = JSON.parse(content);
|
|
84
|
-
const stack = [];
|
|
85
125
|
|
|
86
126
|
function visitDeps(obj, pathArr = []) {
|
|
87
127
|
if (!obj) return;
|
|
@@ -97,6 +137,7 @@ function scanPackageLock(content, bad) {
|
|
|
97
137
|
}
|
|
98
138
|
|
|
99
139
|
if (lock.packages && typeof lock.packages === "object") {
|
|
140
|
+
debug("Detected npm v7+ lockfile format");
|
|
100
141
|
for (const [pkgPath, meta] of Object.entries(lock.packages)) {
|
|
101
142
|
if (!meta || !meta.version) continue;
|
|
102
143
|
const name = meta.name || (pkgPath.includes("node_modules/") ? pkgPath.split("node_modules/").pop() : null);
|
|
@@ -107,14 +148,18 @@ function scanPackageLock(content, bad) {
|
|
|
107
148
|
}
|
|
108
149
|
}
|
|
109
150
|
} else {
|
|
151
|
+
debug("Detected npm v6 lockfile format");
|
|
110
152
|
visitDeps(lock, []);
|
|
111
153
|
}
|
|
112
|
-
} catch {
|
|
154
|
+
} catch (err) {
|
|
155
|
+
debug("Error parsing package-lock.json: %s", err.message);
|
|
156
|
+
}
|
|
157
|
+
debug("package-lock.json scan complete: %d findings", findings.length);
|
|
113
158
|
return findings;
|
|
114
159
|
}
|
|
115
160
|
|
|
116
|
-
// yarn.lock v1
|
|
117
161
|
function scanYarnLockV1(content, bad) {
|
|
162
|
+
debug("Scanning yarn.lock v1");
|
|
118
163
|
const findings = [];
|
|
119
164
|
const entries = content.split(/\n{2,}/g);
|
|
120
165
|
for (const entry of entries) {
|
|
@@ -142,11 +187,12 @@ function scanYarnLockV1(content, bad) {
|
|
|
142
187
|
}
|
|
143
188
|
}
|
|
144
189
|
}
|
|
190
|
+
debug("yarn.lock v1 scan complete: %d findings", findings.length);
|
|
145
191
|
return findings;
|
|
146
192
|
}
|
|
147
193
|
|
|
148
|
-
// Yarn Berry (v2+)
|
|
149
194
|
function scanYarnBerryLock(content, bad) {
|
|
195
|
+
debug("Scanning Yarn Berry lockfile");
|
|
150
196
|
const findings = [];
|
|
151
197
|
const blockRe = /^("?([^"\n]+)"?):\n((?: {2}.+\n)+)/gm;
|
|
152
198
|
let m;
|
|
@@ -168,11 +214,12 @@ function scanYarnBerryLock(content, bad) {
|
|
|
168
214
|
recordFinding(findings, type, "yarn.lock", name, version, key);
|
|
169
215
|
}
|
|
170
216
|
}
|
|
217
|
+
debug("Yarn Berry scan complete: %d findings", findings.length);
|
|
171
218
|
return findings;
|
|
172
219
|
}
|
|
173
220
|
|
|
174
|
-
// pnpm-lock.yaml
|
|
175
221
|
function scanPnpmLock(content, bad) {
|
|
222
|
+
debug("Scanning pnpm-lock.yaml");
|
|
176
223
|
const findings = [];
|
|
177
224
|
const re = /^\s*\/?(@?[^@\s/][^@:\s/]*\/?[^@:\s/]*)@([0-9][^:\s]+):/gm;
|
|
178
225
|
let m;
|
|
@@ -184,18 +231,25 @@ function scanPnpmLock(content, bad) {
|
|
|
184
231
|
recordFinding(findings, type, "pnpm-lock.yaml", name, version, `${name}@${version}`);
|
|
185
232
|
}
|
|
186
233
|
}
|
|
234
|
+
debug("pnpm-lock.yaml scan complete: %d findings", findings.length);
|
|
187
235
|
return findings;
|
|
188
236
|
}
|
|
189
237
|
|
|
190
238
|
// -------- Runner --------
|
|
191
239
|
(function main() {
|
|
240
|
+
debug("Starting main scan");
|
|
192
241
|
const bad = getCompromisedMap();
|
|
193
242
|
const allFindings = [];
|
|
194
243
|
|
|
195
244
|
for (const file of filesToCheck) {
|
|
245
|
+
debug("Checking for %s", file);
|
|
196
246
|
const filePath = join(process.cwd(), file);
|
|
197
|
-
if (!existsSync(filePath))
|
|
247
|
+
if (!existsSync(filePath)) {
|
|
248
|
+
debug("Skipping %s (not found)", file);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
198
251
|
const content = readFileSync(filePath, "utf8");
|
|
252
|
+
debug("Found %s, size %d bytes", file, content.length);
|
|
199
253
|
|
|
200
254
|
if (file === "package.json") {
|
|
201
255
|
allFindings.push(...scanPackageJson(content, bad));
|
|
@@ -210,9 +264,13 @@ function scanPnpmLock(content, bad) {
|
|
|
210
264
|
}
|
|
211
265
|
}
|
|
212
266
|
|
|
267
|
+
debug("Total findings collected: %d", allFindings.length);
|
|
268
|
+
|
|
213
269
|
const badFindings = allFindings.filter(f => f.type === "bad");
|
|
214
270
|
const warnFindings = allFindings.filter(f => f.type === "warn");
|
|
215
271
|
|
|
272
|
+
debug("Bad findings: %d, Warn findings: %d", badFindings.length, warnFindings.length);
|
|
273
|
+
|
|
216
274
|
warnFindings.forEach(f =>
|
|
217
275
|
console.log(`⚠️ WARNING: ${f.pkg}@${f.version} in ${f.file} (${f.where}) — package was targeted in past attack, but version is not flagged as malicious`)
|
|
218
276
|
);
|
|
@@ -224,6 +282,8 @@ function scanPnpmLock(content, bad) {
|
|
|
224
282
|
if (badFindings.length === 0) {
|
|
225
283
|
console.log("✅ No known malicious versions detected.");
|
|
226
284
|
} else {
|
|
285
|
+
debug("Exiting with code 1 due to bad findings");
|
|
227
286
|
process.exit(1);
|
|
228
287
|
}
|
|
229
|
-
|
|
288
|
+
debug("Scan complete");
|
|
289
|
+
})();
|