supply-chain-guard 5.20.2 → 5.21.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 +1 -1
- package/dist/cli.js +1 -1
- package/dist/dependency-risk-analyzer.d.ts +16 -0
- package/dist/dependency-risk-analyzer.d.ts.map +1 -1
- package/dist/dependency-risk-analyzer.js +228 -38
- package/dist/dependency-risk-analyzer.js.map +1 -1
- package/dist/install-guard.d.ts +10 -1
- package/dist/install-guard.d.ts.map +1 -1
- package/dist/install-guard.js +83 -13
- package/dist/install-guard.js.map +1 -1
- package/dist/ioc-blocklist.d.ts.map +1 -1
- package/dist/ioc-blocklist.js +41 -0
- package/dist/ioc-blocklist.js.map +1 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +19 -0
- package/dist/patterns.js.map +1 -1
- package/dist/policy-engine.d.ts.map +1 -1
- package/dist/policy-engine.js +11 -0
- package/dist/policy-engine.js.map +1 -1
- package/dist/reporter.js +5 -5
- package/dist/scanner.js +26 -12
- package/dist/scanner.js.map +1 -1
- package/dist/threat-intel.d.ts.map +1 -1
- package/dist/threat-intel.js +295 -0
- package/dist/threat-intel.js.map +1 -1
- package/package.json +1 -1
- package/policy-schema.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ const program = new commander_1.Command();
|
|
|
22
22
|
program
|
|
23
23
|
.name("supply-chain-guard")
|
|
24
24
|
.description("Open-source supply-chain security scanner. Detects GlassWorm and similar malware campaigns in npm packages, PyPI packages, code repos, VS Code extensions, and project dependencies.")
|
|
25
|
-
.version("5.
|
|
25
|
+
.version("5.21.0");
|
|
26
26
|
// ── scan command ────────────────────────────────────────────────────
|
|
27
27
|
program
|
|
28
28
|
.command("scan")
|
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
* Checks package names against popular packages to detect mimicry.
|
|
6
6
|
*/
|
|
7
7
|
import type { Finding } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve an npm alias spec to the package that actually gets installed.
|
|
10
|
+
*
|
|
11
|
+
* `"utils": "npm:chalk-tempalte@1.0.0"` installs chalk-tempalte and calls it
|
|
12
|
+
* `utils` in the tree. The KEY is a local label chosen by whoever wrote the
|
|
13
|
+
* manifest; the TARGET is the real registry package. Checking the key is
|
|
14
|
+
* therefore checking attacker-controlled text, which is how a known-malicious
|
|
15
|
+
* package used to scan completely clean: every consumer here read the key.
|
|
16
|
+
*
|
|
17
|
+
* Returns null for anything that is not an `npm:` alias, so callers can fall
|
|
18
|
+
* back to the literal name.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveNpmAlias(spec: string | undefined): {
|
|
21
|
+
name: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
} | null;
|
|
8
24
|
/**
|
|
9
25
|
* Calculate Levenshtein distance between two strings.
|
|
10
26
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dependency-risk-analyzer.d.ts","sourceRoot":"","sources":["../src/dependency-risk-analyzer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"dependency-risk-analyzer.d.ts","sourceRoot":"","sources":["../src/dependency-risk-analyzer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA8D1C;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsB3C;AAeD;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAsBxD;AAmDD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,YAAY,EAAE,MAAM,GACnB,OAAO,EAAE,CA8JX"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Checks package names against popular packages to detect mimicry.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.resolveNpmAlias = resolveNpmAlias;
|
|
9
10
|
exports.levenshtein = levenshtein;
|
|
10
11
|
exports.analyzeDependencyRisks = analyzeDependencyRisks;
|
|
11
12
|
/** Top popular npm packages (targets for typosquatting) */
|
|
@@ -29,6 +30,76 @@ const POPULAR_PACKAGES = [
|
|
|
29
30
|
];
|
|
30
31
|
/** Packages that are known-safe even when close in name to popular packages */
|
|
31
32
|
const POPULAR_PACKAGES_SET = new Set(POPULAR_PACKAGES);
|
|
33
|
+
/**
|
|
34
|
+
* Legitimate, widely-used packages that sit one edit from an entry in
|
|
35
|
+
* POPULAR_PACKAGES and must never be reported as typosquats.
|
|
36
|
+
*
|
|
37
|
+
* This is an ADDITIONAL exemption, not a replacement for POPULAR_PACKAGES_SET:
|
|
38
|
+
* entries here are NOT typosquat targets. The two lists are separate because a
|
|
39
|
+
* name can need exempting without being worth defending: putting a name in
|
|
40
|
+
* POPULAR_PACKAGES also makes every name one edit from it a candidate squat, so
|
|
41
|
+
* the target list has to stay a deliberate choice rather than a dumping ground
|
|
42
|
+
* for whatever tripped the rule this week.
|
|
43
|
+
*
|
|
44
|
+
* Scope note: this set gates exactly one rule (TYPOSQUAT_LEVENSHTEIN, at the
|
|
45
|
+
* single call site below). It is NOT a malware bypass - feed IOC matches, the
|
|
46
|
+
* known-bad-version blocklist and every scanner pattern run on separate paths
|
|
47
|
+
* that never consult it. Do not move this check earlier in install-guard's
|
|
48
|
+
* checkSpec, where it would start gating real malware verdicts.
|
|
49
|
+
*
|
|
50
|
+
* Every entry was verified against the npm registry on 2026-07-29 as a real,
|
|
51
|
+
* maintained package with an identifiable owner - the same evidentiary bar
|
|
52
|
+
* src/patterns.ts applies before blocking any bare name.
|
|
53
|
+
*/
|
|
54
|
+
const TYPOSQUAT_ALLOWLIST = new Set([
|
|
55
|
+
"pathe", // unjs, universal path utils (also a transitive dep of this repo)
|
|
56
|
+
"color", // Qix-/color; NOT a squat of the sabotaged "colors"
|
|
57
|
+
"colord", // omgovich, color manipulation
|
|
58
|
+
"mysql", // the original mysqljs driver; "mysql2" is the successor, both real
|
|
59
|
+
"nuxt", // the Nuxt framework
|
|
60
|
+
"ulid", // ULID id generator; near "uuid" by coincidence
|
|
61
|
+
"preact", // 3kb React-compatible virtual DOM
|
|
62
|
+
"gaxios", // googleapis HTTP client
|
|
63
|
+
"enquirer", // interactive CLI prompts
|
|
64
|
+
"ttypescript", // TypeScript wrapper supporting custom transformers
|
|
65
|
+
]);
|
|
66
|
+
/** Registry name shape, used to validate an alias target. */
|
|
67
|
+
const ALIAS_NAME_RE = /^(@[a-z0-9][a-z0-9-._~]*\/)?[a-z0-9-._~][a-z0-9-._~]*$/i;
|
|
68
|
+
/**
|
|
69
|
+
* Resolve an npm alias spec to the package that actually gets installed.
|
|
70
|
+
*
|
|
71
|
+
* `"utils": "npm:chalk-tempalte@1.0.0"` installs chalk-tempalte and calls it
|
|
72
|
+
* `utils` in the tree. The KEY is a local label chosen by whoever wrote the
|
|
73
|
+
* manifest; the TARGET is the real registry package. Checking the key is
|
|
74
|
+
* therefore checking attacker-controlled text, which is how a known-malicious
|
|
75
|
+
* package used to scan completely clean: every consumer here read the key.
|
|
76
|
+
*
|
|
77
|
+
* Returns null for anything that is not an `npm:` alias, so callers can fall
|
|
78
|
+
* back to the literal name.
|
|
79
|
+
*/
|
|
80
|
+
function resolveNpmAlias(spec) {
|
|
81
|
+
if (typeof spec !== "string")
|
|
82
|
+
return null;
|
|
83
|
+
const trimmed = spec.trim();
|
|
84
|
+
if (!trimmed.startsWith("npm:"))
|
|
85
|
+
return null;
|
|
86
|
+
const target = trimmed.slice(4);
|
|
87
|
+
if (target.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
// Same split rule as a plain spec: the LAST "@" separates the version, and
|
|
90
|
+
// index 0 is a scope marker rather than a separator.
|
|
91
|
+
const at = target.lastIndexOf("@");
|
|
92
|
+
const name = at > 0 ? target.substring(0, at) : target;
|
|
93
|
+
const version = at > 0 ? target.substring(at + 1) : undefined;
|
|
94
|
+
if (!ALIAS_NAME_RE.test(name))
|
|
95
|
+
return null;
|
|
96
|
+
// A nested protocol ("npm:foo@git:...") is not a plain registry pin. Keep the
|
|
97
|
+
// resolved NAME, which is still checkable, and drop only the version.
|
|
98
|
+
if (version !== undefined && (version.length === 0 || version.includes(":"))) {
|
|
99
|
+
return { name };
|
|
100
|
+
}
|
|
101
|
+
return version === undefined ? { name } : { name, version };
|
|
102
|
+
}
|
|
32
103
|
/** Patterns that suggest internal/private package names */
|
|
33
104
|
const INTERNAL_PATTERNS = [
|
|
34
105
|
/^@[^/]+\/internal-/,
|
|
@@ -60,13 +131,63 @@ function levenshtein(a, b) {
|
|
|
60
131
|
}
|
|
61
132
|
return dp[m][n];
|
|
62
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Optimal String Alignment distance (Damerau-Levenshtein restricted to adjacent
|
|
136
|
+
* transpositions), used ONLY by the typosquat heuristic below.
|
|
137
|
+
*
|
|
138
|
+
* Deliberately separate from the exported `levenshtein`, which is public API
|
|
139
|
+
* (re-exported from src/index.ts) and pinned to exact values by its own tests.
|
|
140
|
+
*
|
|
141
|
+
* Why the typosquat rule needs transposition awareness: the realistic squat is a
|
|
142
|
+
* swapped pair of adjacent characters, which plain Levenshtein scores as 2 edits
|
|
143
|
+
* (two substitutions) rather than 1. The curated squats this rule catches that
|
|
144
|
+
* are transpositions - "rimarf" -> rimraf, "yarsg" -> yargs, "lodahs" -> lodash,
|
|
145
|
+
* "axois" -> axios, "raect" -> react - would all be lost at a plain 1-edit
|
|
146
|
+
* ceiling. Scoring them as 1 is what allows the ceiling to drop from 2 to 1
|
|
147
|
+
* without losing them.
|
|
148
|
+
*
|
|
149
|
+
* Note this rule only ever sees squats of names in POPULAR_PACKAGES. Curated
|
|
150
|
+
* malicious names whose target is NOT in that list (cyrpto, hardhta, naniod,
|
|
151
|
+
* suport-color) are matched by exact name in patterns.ts and the feed, never
|
|
152
|
+
* here - so this heuristic's job is unpublished future squats, not the corpus
|
|
153
|
+
* already curated.
|
|
154
|
+
*/
|
|
155
|
+
function osaDistance(a, b) {
|
|
156
|
+
const m = a.length;
|
|
157
|
+
const n = b.length;
|
|
158
|
+
const dp = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0));
|
|
159
|
+
for (let i = 0; i <= m; i++)
|
|
160
|
+
dp[i][0] = i;
|
|
161
|
+
for (let j = 0; j <= n; j++)
|
|
162
|
+
dp[0][j] = j;
|
|
163
|
+
for (let i = 1; i <= m; i++) {
|
|
164
|
+
for (let j = 1; j <= n; j++) {
|
|
165
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
166
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
167
|
+
// Adjacent transposition counts as a single edit.
|
|
168
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
169
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return dp[m][n];
|
|
174
|
+
}
|
|
63
175
|
/**
|
|
64
176
|
* Analyze dependencies for typosquatting and confusion risks.
|
|
65
177
|
*/
|
|
66
178
|
function analyzeDependencyRisks(dependencies, relativePath) {
|
|
67
179
|
const findings = [];
|
|
68
|
-
|
|
69
|
-
|
|
180
|
+
// Every rule below judges the package that is actually INSTALLED. For an npm
|
|
181
|
+
// alias ("utils": "npm:lodahs@1.0.0") that is the target, not the manifest
|
|
182
|
+
// key, which is arbitrary text chosen by whoever wrote the file.
|
|
183
|
+
const entries = Object.keys(dependencies).map((key) => {
|
|
184
|
+
const alias = resolveNpmAlias(dependencies[key]);
|
|
185
|
+
return { key, name: alias ? alias.name : key, aliased: alias !== null };
|
|
186
|
+
});
|
|
187
|
+
/** Sorted "a b" keys, so one pair yields one finding rather than one per side. */
|
|
188
|
+
const reportedPairs = new Set();
|
|
189
|
+
for (const { key, name, aliased } of entries) {
|
|
190
|
+
const via = aliased ? ` (installed via the npm alias "${key}")` : "";
|
|
70
191
|
// Skip scoped packages for Levenshtein (handled separately)
|
|
71
192
|
if (name.startsWith("@")) {
|
|
72
193
|
// Check internal name patterns on public registry
|
|
@@ -74,7 +195,7 @@ function analyzeDependencyRisks(dependencies, relativePath) {
|
|
|
74
195
|
if (pattern.test(name)) {
|
|
75
196
|
findings.push({
|
|
76
197
|
rule: "DEP_INTERNAL_NAME_PUBLIC",
|
|
77
|
-
description: `Dependency "${name}" looks like an internal package name. If this is on a public registry, it may be a dependency confusion attack.`,
|
|
198
|
+
description: `Dependency "${name}" looks like an internal package name${via}. If this is on a public registry, it may be a dependency confusion attack.`,
|
|
78
199
|
severity: "critical",
|
|
79
200
|
file: relativePath,
|
|
80
201
|
confidence: 0.7,
|
|
@@ -86,53 +207,122 @@ function analyzeDependencyRisks(dependencies, relativePath) {
|
|
|
86
207
|
}
|
|
87
208
|
continue;
|
|
88
209
|
}
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
210
|
+
// Typosquat check against popular packages.
|
|
211
|
+
//
|
|
212
|
+
// The ceiling is ONE transposition-aware edit. It used to be two plain
|
|
213
|
+
// Levenshtein edits, which flagged 321 measured real packages (acorn, preact,
|
|
214
|
+
// cypress, redux, viem, knex, globby, jose...) against 2 true positives that
|
|
215
|
+
// patterns.ts and the feed already catch by exact name - a ~1:160 signal ratio,
|
|
216
|
+
// and it got WORSE the more popular the package was (2.37% of packages above
|
|
217
|
+
// 10M weekly downloads). Two edits against a 4-character name is simply not
|
|
218
|
+
// evidence of anything. Transposition awareness (see osaDistance) is what makes
|
|
219
|
+
// the tightening free: every real squat in this repo's threat data is an
|
|
220
|
+
// adjacent swap, so all of them still score 1 and are still caught.
|
|
221
|
+
//
|
|
222
|
+
// No first-character predicate is used. It would cut a further ~24 false
|
|
223
|
+
// positives, but this repo curates "1odash" and "l0dash" in patterns.ts as
|
|
224
|
+
// canonical squats, and both change character zero - a leading-homoglyph rule
|
|
225
|
+
// would make this heuristic structurally unable to generalize them. It is also
|
|
226
|
+
// published source, so it would be a one-character documented bypass.
|
|
227
|
+
//
|
|
228
|
+
// Skip if the name is itself a known popular/safe package - prevents false
|
|
229
|
+
// positives where two legitimate popular packages are close in name (next/jest).
|
|
230
|
+
if (!POPULAR_PACKAGES_SET.has(name) &&
|
|
231
|
+
!TYPOSQUAT_ALLOWLIST.has(name) &&
|
|
232
|
+
name.length >= 4) {
|
|
233
|
+
// At a ceiling of 1 every hit is the same distance, so there is no closer
|
|
234
|
+
// match to search for and the first hit is reported. (Under the old 2-edit
|
|
235
|
+
// ceiling this loop could report a distance-2 target while a distance-1 one
|
|
236
|
+
// sat later in the array.) If the ceiling is ever raised, this must go back
|
|
237
|
+
// to tracking the minimum before reporting.
|
|
238
|
+
let best;
|
|
93
239
|
for (const popular of POPULAR_PACKAGES) {
|
|
94
240
|
if (name === popular)
|
|
95
241
|
continue; // Exact match = legitimate
|
|
96
|
-
// Skip
|
|
97
|
-
|
|
242
|
+
// Skip short popular targets. The floor is 5, not 4: measured over 29,687
|
|
243
|
+
// real published npm names, a floor of 4 produced 57 false positives and a
|
|
244
|
+
// floor of 5 produces 29, with the curated true-positive set unchanged. The
|
|
245
|
+
// four-letter targets are the damaging ones because almost any short name is
|
|
246
|
+
// one edit from them - "path" alone accounted for 11 (upath, mpath, xpath,
|
|
247
|
+
// paths, ...) and "jest" for 5 (test, nest, rest, ...). Floor 6 is too far:
|
|
248
|
+
// it loses yarsg, axois, raect and chlak.
|
|
249
|
+
if (popular.length < 5)
|
|
98
250
|
continue;
|
|
99
|
-
if (Math.abs(name.length - popular.length) >
|
|
251
|
+
if (Math.abs(name.length - popular.length) > 1)
|
|
100
252
|
continue; // Quick skip
|
|
101
|
-
const dist =
|
|
102
|
-
if (dist > 0 && dist <=
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
description: `Dependency "${name}" is ${dist} edit(s) away from popular package "${popular}". Likely a typosquat.`,
|
|
106
|
-
severity: "high",
|
|
107
|
-
file: relativePath,
|
|
108
|
-
confidence: dist === 1 ? 0.85 : 0.65,
|
|
109
|
-
category: "supply-chain",
|
|
110
|
-
recommendation: `Did you mean "${popular}"? Typosquatting replaces popular packages with malicious copies.`,
|
|
111
|
-
});
|
|
112
|
-
break; // One match per dep is enough
|
|
253
|
+
const dist = osaDistance(name, popular);
|
|
254
|
+
if (dist > 0 && dist <= 1 && (!best || dist < best.dist)) {
|
|
255
|
+
best = { popular, dist };
|
|
256
|
+
break; // Distance 1 is the tightest possible hit; nothing can beat it.
|
|
113
257
|
}
|
|
114
258
|
}
|
|
115
|
-
|
|
116
|
-
// Check if name is similar to another direct dependency
|
|
117
|
-
for (const otherName of depNames) {
|
|
118
|
-
if (name === otherName)
|
|
119
|
-
continue;
|
|
120
|
-
if (name.startsWith("@") || otherName.startsWith("@"))
|
|
121
|
-
continue;
|
|
122
|
-
if (Math.abs(name.length - otherName.length) > 2)
|
|
123
|
-
continue;
|
|
124
|
-
const dist = levenshtein(name, otherName);
|
|
125
|
-
if (dist > 0 && dist <= 1) {
|
|
259
|
+
if (best) {
|
|
126
260
|
findings.push({
|
|
127
|
-
rule: "
|
|
128
|
-
|
|
261
|
+
rule: "TYPOSQUAT_LEVENSHTEIN",
|
|
262
|
+
// The dependency name MUST stay the first quoted token: policy-engine.ts
|
|
263
|
+
// extracts it with /"([^"]+)"/ to honour user `allowlist.packages` rules.
|
|
264
|
+
description: `Dependency "${name}"${via} is ${best.dist} edit(s) away from popular package "${best.popular}". Likely a typosquat.`,
|
|
129
265
|
severity: "high",
|
|
130
266
|
file: relativePath,
|
|
131
|
-
confidence: 0.
|
|
267
|
+
confidence: 0.85,
|
|
132
268
|
category: "supply-chain",
|
|
133
|
-
recommendation: `
|
|
269
|
+
recommendation: `Did you mean "${best.popular}"? Typosquatting replaces popular packages with malicious copies.`,
|
|
134
270
|
});
|
|
135
|
-
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Check if name is similar to another direct dependency.
|
|
274
|
+
//
|
|
275
|
+
// Measured over 939 real manifests, this rule fired on 5.0% of them (8.1% of
|
|
276
|
+
// real repository roots) and every colliding pair was a legitimate co-install:
|
|
277
|
+
// preact+react, vue+vuex, path+pathe, color+colors, mysql+mysql2, uuid+ulid,
|
|
278
|
+
// eslint+tslint. Four guards below cut that to under 1% while INCREASING
|
|
279
|
+
// recall, because switching to osaDistance starts catching the transposition
|
|
280
|
+
// squats (lodahs+lodash, axois+axios, raect+react, yarsg+yargs) that the rule
|
|
281
|
+
// was silently missing next to their own target.
|
|
282
|
+
//
|
|
283
|
+
// The known-good test is ONE-SIDED on purpose. The outer loop visits both
|
|
284
|
+
// members of a pair, so testing only the reported side keeps the case this
|
|
285
|
+
// rule exists for: {expres, express} still reports, because the express-side
|
|
286
|
+
// pass is dropped while the expres-side pass survives. Testing otherName too
|
|
287
|
+
// would suppress exactly the squat-next-to-its-target shape (measured: it
|
|
288
|
+
// drops 11 of 14 true positives).
|
|
289
|
+
//
|
|
290
|
+
// Guards are scoped to this block rather than the outer loop so the two
|
|
291
|
+
// typosquat rules stay independently editable.
|
|
292
|
+
const skipPairRule = name.length < 4 || POPULAR_PACKAGES_SET.has(name) || TYPOSQUAT_ALLOWLIST.has(name);
|
|
293
|
+
if (!skipPairRule) {
|
|
294
|
+
for (const other of entries) {
|
|
295
|
+
const otherName = other.name;
|
|
296
|
+
if (name === otherName)
|
|
297
|
+
continue;
|
|
298
|
+
if (name.startsWith("@") || otherName.startsWith("@"))
|
|
299
|
+
continue;
|
|
300
|
+
// Same length floor as the sibling rule: below 4 characters almost any
|
|
301
|
+
// name is one edit from another (ws/wss, np/nx, fs/fse, qs/q).
|
|
302
|
+
if (otherName.length < 4)
|
|
303
|
+
continue;
|
|
304
|
+
if (Math.abs(name.length - otherName.length) > 1)
|
|
305
|
+
continue;
|
|
306
|
+
const dist = osaDistance(name, otherName);
|
|
307
|
+
if (dist > 0 && dist <= 1) {
|
|
308
|
+
const pairKey = [name, otherName].sort().join(" ");
|
|
309
|
+
if (reportedPairs.has(pairKey))
|
|
310
|
+
break;
|
|
311
|
+
reportedPairs.add(pairKey);
|
|
312
|
+
const otherVia = other.aliased ? ` (installed via the npm alias "${other.key}")` : "";
|
|
313
|
+
findings.push({
|
|
314
|
+
rule: "TYPOSQUAT_SIMILAR_TO_DEP",
|
|
315
|
+
// "name" leads and is guaranteed to be the non-allowlisted side, so
|
|
316
|
+
// the suspicious package is named first.
|
|
317
|
+
description: `Dependencies "${name}"${via} and "${otherName}"${otherVia} differ by only ${dist} character(s). One may be a typosquat of the other.`,
|
|
318
|
+
severity: "high",
|
|
319
|
+
file: relativePath,
|
|
320
|
+
confidence: 0.7,
|
|
321
|
+
category: "supply-chain",
|
|
322
|
+
recommendation: `Review both "${name}" and "${otherName}". Only one should be in your dependencies.`,
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
136
326
|
}
|
|
137
327
|
}
|
|
138
328
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dependency-risk-analyzer.js","sourceRoot":"","sources":["../src/dependency-risk-analyzer.ts"],"names":[],"mappings":";AAAA;;;;;GAKG
|
|
1
|
+
{"version":3,"file":"dependency-risk-analyzer.js","sourceRoot":"","sources":["../src/dependency-risk-analyzer.ts"],"names":[],"mappings":"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"}
|
package/dist/install-guard.d.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* No network calls are made by this module.
|
|
12
12
|
*/
|
|
13
13
|
import { type FeedIOC } from "./threat-intel.js";
|
|
14
|
+
import { resolveNpmAlias } from "./dependency-risk-analyzer.js";
|
|
14
15
|
import type { Finding } from "./types.js";
|
|
16
|
+
export { resolveNpmAlias };
|
|
15
17
|
export declare const SUPPORTED_MANAGERS: readonly ["npm", "pnpm", "yarn", "bun"];
|
|
16
18
|
export type PackageManager = (typeof SUPPORTED_MANAGERS)[number];
|
|
17
19
|
/**
|
|
@@ -50,7 +52,12 @@ export declare function extractInstallSpecs(manager: PackageManager, args: strin
|
|
|
50
52
|
};
|
|
51
53
|
export interface InstallGuardVerdict {
|
|
52
54
|
spec: InstallPackageSpec;
|
|
55
|
+
/** Every finding, blocking or not. Nothing is dropped. */
|
|
53
56
|
findings: Finding[];
|
|
57
|
+
/** Findings that carry blocking authority */
|
|
58
|
+
blocking: Finding[];
|
|
59
|
+
/** Heuristic findings that are reported but do not block */
|
|
60
|
+
warnings: Finding[];
|
|
54
61
|
}
|
|
55
62
|
export interface InstallCommandAnalysis {
|
|
56
63
|
manager: PackageManager;
|
|
@@ -61,8 +68,10 @@ export interface InstallCommandAnalysis {
|
|
|
61
68
|
specs: InstallPackageSpec[];
|
|
62
69
|
/** One verdict per spec; findings empty = clean */
|
|
63
70
|
verdicts: InstallGuardVerdict[];
|
|
64
|
-
/** True when at least one spec has
|
|
71
|
+
/** True when at least one spec has a BLOCKING finding */
|
|
65
72
|
blocked: boolean;
|
|
73
|
+
/** True when at least one spec has a warn-only finding */
|
|
74
|
+
warned: boolean;
|
|
66
75
|
}
|
|
67
76
|
/**
|
|
68
77
|
* npm package IOCs in the feed carry no ecosystem prefix (only ruby:/composer:/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-guard.d.ts","sourceRoot":"","sources":["../src/install-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAoC,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"install-guard.d.ts","sourceRoot":"","sources":["../src/install-guard.ts"],"names":[],"mappings":"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"}
|
package/dist/install-guard.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* No network calls are made by this module.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.SUPPORTED_MANAGERS = void 0;
|
|
15
|
+
exports.SUPPORTED_MANAGERS = exports.resolveNpmAlias = void 0;
|
|
16
16
|
exports.resolveManagerBinary = resolveManagerBinary;
|
|
17
17
|
exports.parseSpecToken = parseSpecToken;
|
|
18
18
|
exports.extractInstallSpecs = extractInstallSpecs;
|
|
@@ -25,6 +25,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
25
25
|
const threat_intel_js_1 = require("./threat-intel.js");
|
|
26
26
|
const ioc_blocklist_js_1 = require("./ioc-blocklist.js");
|
|
27
27
|
const dependency_risk_analyzer_js_1 = require("./dependency-risk-analyzer.js");
|
|
28
|
+
Object.defineProperty(exports, "resolveNpmAlias", { enumerable: true, get: function () { return dependency_risk_analyzer_js_1.resolveNpmAlias; } });
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Managers and install verbs
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
@@ -79,6 +80,24 @@ const REGISTRY_NAME_RE = /^(@[a-z0-9][a-z0-9-._~]*\/)?[a-z0-9-._~][a-z0-9-._~]*$
|
|
|
79
80
|
function parseSpecToken(token) {
|
|
80
81
|
if (token.length === 0 || token.startsWith("-"))
|
|
81
82
|
return null;
|
|
83
|
+
// Alias form FIRST: "utils@npm:chalk-tempalte@1.0.0". This must be detected
|
|
84
|
+
// before the version split, because lastIndexOf("@") would land on the alias
|
|
85
|
+
// target's own version and leave an unparseable name. Previously that made the
|
|
86
|
+
// whole spec fail REGISTRY_NAME_RE and get dropped, so `npm install
|
|
87
|
+
// x@npm:<malware>` was never checked at all - a complete bypass of the guard.
|
|
88
|
+
// The installed package is the alias TARGET, so resolve and check that.
|
|
89
|
+
const aliasAt = token.indexOf("@npm:");
|
|
90
|
+
if (aliasAt > 0) {
|
|
91
|
+
const label = token.substring(0, aliasAt);
|
|
92
|
+
if (!REGISTRY_NAME_RE.test(label))
|
|
93
|
+
return null;
|
|
94
|
+
const alias = (0, dependency_risk_analyzer_js_1.resolveNpmAlias)(token.substring(aliasAt + 1));
|
|
95
|
+
if (!alias)
|
|
96
|
+
return null;
|
|
97
|
+
return alias.version === undefined
|
|
98
|
+
? { raw: token, name: alias.name }
|
|
99
|
+
: { raw: token, name: alias.name, version: alias.version };
|
|
100
|
+
}
|
|
82
101
|
// Split "name@version" / "@scope/name@version" at the LAST "@";
|
|
83
102
|
// index 0 is the scope marker, not a version separator.
|
|
84
103
|
const at = token.lastIndexOf("@");
|
|
@@ -86,8 +105,8 @@ function parseSpecToken(token) {
|
|
|
86
105
|
const version = at > 0 ? token.substring(at + 1) : undefined;
|
|
87
106
|
if (!REGISTRY_NAME_RE.test(name))
|
|
88
107
|
return null;
|
|
89
|
-
//
|
|
90
|
-
//
|
|
108
|
+
// Other protocol versions ("foo@git:...") are not plain registry pins; skip
|
|
109
|
+
// rather than mis-attribute a version.
|
|
91
110
|
if (version !== undefined && (version.length === 0 || version.includes(":")))
|
|
92
111
|
return null;
|
|
93
112
|
return version === undefined ? { raw: token, name } : { raw: token, name, version };
|
|
@@ -148,6 +167,31 @@ function extractInstallSpecs(manager, args) {
|
|
|
148
167
|
}
|
|
149
168
|
return { verb, installVerb: true, specs };
|
|
150
169
|
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Offline analysis
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/**
|
|
174
|
+
* Rules that are REPORTED but never block an install.
|
|
175
|
+
*
|
|
176
|
+
* A deny-list, not an allow-list, on purpose: a new exact-match rule added to
|
|
177
|
+
* checkSpec keeps blocking authority automatically, and only a rule someone
|
|
178
|
+
* deliberately lists here loses it. Given that a false negative silently lowers
|
|
179
|
+
* the bar everywhere this runs, the safe default has to be "blocks".
|
|
180
|
+
*
|
|
181
|
+
* Only DEP_INTERNAL_NAME_PUBLIC is demoted. It fires on the NAME SHAPE of a
|
|
182
|
+
* scoped package (any @scope/*-utils, -api, -core, -service, -shared, -common,
|
|
183
|
+
* -lib), which matched 1.7% of all real scoped packages in a registry sweep -
|
|
184
|
+
* including @babel/helper-plugin-utils, @vue/compiler-core and
|
|
185
|
+
* @tanstack/query-core - and blocked every one of them at critical.
|
|
186
|
+
*
|
|
187
|
+
* TYPOSQUAT_LEVENSHTEIN is deliberately NOT demoted, even though it is also a
|
|
188
|
+
* heuristic. checkSpec never consults src/patterns.ts, so for curated squats
|
|
189
|
+
* whose target is popular (lodahs, crossenv, cros-env, 1odash, l0dash, expresss,
|
|
190
|
+
* nodemai1er, reqeust, axois, raect) this rule is the guard's ONLY view of them:
|
|
191
|
+
* demoting it was measured to lose 10 real blocks. Wiring the curated names into
|
|
192
|
+
* checkSpec is the prerequisite for ever revisiting that.
|
|
193
|
+
*/
|
|
194
|
+
const WARN_ONLY_RULES = new Set(["DEP_INTERNAL_NAME_PUBLIC"]);
|
|
151
195
|
/**
|
|
152
196
|
* npm package IOCs in the feed carry no ecosystem prefix (only ruby:/composer:/
|
|
153
197
|
* nuget:/go:/jenkins: entries do - see matchPackageIOC). Same companion
|
|
@@ -214,12 +258,21 @@ function analyzeInstallCommand(manager, args, feed) {
|
|
|
214
258
|
}
|
|
215
259
|
const { verb, installVerb, specs } = extractInstallSpecs(manager, args);
|
|
216
260
|
if (!installVerb || specs.length === 0) {
|
|
217
|
-
return { manager, verb, installVerb, specs, verdicts: [], blocked: false };
|
|
261
|
+
return { manager, verb, installVerb, specs, verdicts: [], blocked: false, warned: false };
|
|
218
262
|
}
|
|
219
263
|
const entries = feed ?? (0, threat_intel_js_1.loadThreatIntel)();
|
|
220
|
-
const verdicts = specs.map((spec) =>
|
|
221
|
-
|
|
222
|
-
|
|
264
|
+
const verdicts = specs.map((spec) => {
|
|
265
|
+
const findings = checkSpec(spec, entries);
|
|
266
|
+
return {
|
|
267
|
+
spec,
|
|
268
|
+
findings,
|
|
269
|
+
blocking: findings.filter((f) => !WARN_ONLY_RULES.has(f.rule)),
|
|
270
|
+
warnings: findings.filter((f) => WARN_ONLY_RULES.has(f.rule)),
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
const blocked = verdicts.some((v) => v.blocking.length > 0);
|
|
274
|
+
const warned = verdicts.some((v) => v.warnings.length > 0);
|
|
275
|
+
return { manager, verb, installVerb, specs, verdicts, blocked, warned };
|
|
223
276
|
}
|
|
224
277
|
// cmd.exe metacharacters that must be ^-escaped (cross-spawn's meta set).
|
|
225
278
|
const CMD_META_RE = /([()\][%!^"`<>&|;, *?])/g;
|
|
@@ -266,15 +319,20 @@ const defaultSpawn = (command, args, options) => {
|
|
|
266
319
|
const result = (0, node_child_process_1.spawnSync)(command, args, options);
|
|
267
320
|
return { status: result.status, error: result.error };
|
|
268
321
|
};
|
|
269
|
-
function printVerdicts(analysis, log) {
|
|
270
|
-
const hits = analysis.verdicts.filter((v) => v.
|
|
271
|
-
|
|
322
|
+
function printVerdicts(analysis, log, kind = "blocking") {
|
|
323
|
+
const hits = analysis.verdicts.filter((v) => v[kind].length > 0);
|
|
324
|
+
if (hits.length === 0)
|
|
325
|
+
return;
|
|
326
|
+
const total = hits.reduce((n, v) => n + v[kind].length, 0);
|
|
327
|
+
const label = kind === "blocking"
|
|
328
|
+
? `${total} finding(s)`
|
|
329
|
+
: `${total} warning(s) (reported, not blocking)`;
|
|
272
330
|
log("");
|
|
273
|
-
log(` Install guard: ${
|
|
331
|
+
log(` Install guard: ${label} in "${analysis.manager} ${analysis.verb}" command:`);
|
|
274
332
|
for (const verdict of hits) {
|
|
275
333
|
log("");
|
|
276
334
|
log(` Package: ${verdict.spec.raw}`);
|
|
277
|
-
for (const finding of verdict
|
|
335
|
+
for (const finding of verdict[kind]) {
|
|
278
336
|
log(` [${finding.severity.toUpperCase()}] ${finding.rule}: ${finding.description}`);
|
|
279
337
|
log(` ${finding.recommendation}`);
|
|
280
338
|
}
|
|
@@ -296,7 +354,9 @@ function runInstallGuard(manager, managerArgs, options = {}) {
|
|
|
296
354
|
const log = options.log ?? ((line) => console.log(line));
|
|
297
355
|
const analysis = analyzeInstallCommand(manager, managerArgs, options.feed);
|
|
298
356
|
if (analysis.blocked) {
|
|
299
|
-
printVerdicts(analysis, log);
|
|
357
|
+
printVerdicts(analysis, log, "blocking");
|
|
358
|
+
// Warn-only findings on a blocked command are still worth showing.
|
|
359
|
+
printVerdicts(analysis, log, "warnings");
|
|
300
360
|
if (options.dryRun) {
|
|
301
361
|
log(" Dry run: package manager not invoked.");
|
|
302
362
|
return 2;
|
|
@@ -307,6 +367,16 @@ function runInstallGuard(manager, managerArgs, options = {}) {
|
|
|
307
367
|
}
|
|
308
368
|
log(" WARNING: --force is set - proceeding DESPITE the findings above. The packages listed are known-bad or suspicious.");
|
|
309
369
|
}
|
|
370
|
+
else if (analysis.warned) {
|
|
371
|
+
// Heuristic-only findings: report in full, then let the install proceed.
|
|
372
|
+
// These used to exit 2, which made a name-shape guess as authoritative as a
|
|
373
|
+
// threat-feed malware match.
|
|
374
|
+
printVerdicts(analysis, log, "warnings");
|
|
375
|
+
if (options.dryRun) {
|
|
376
|
+
log(" Dry run: nothing known-bad, warnings above are advisory only.");
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
310
380
|
else if (options.dryRun) {
|
|
311
381
|
log(` Install guard: no known-bad packages in "${[analysis.manager, ...managerArgs].join(" ")}" (dry run, nothing executed).`);
|
|
312
382
|
return 0;
|