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