@lateos/npm-scan 1.0.0 → 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 +864 -861
- package/backend/cra.js +113 -21
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +48 -3
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +109 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +78 -19
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +12 -3
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
|
+
|
|
3
|
+
const THRESHOLDS = {
|
|
4
|
+
flag_threshold: 80,
|
|
5
|
+
warn_threshold: 50,
|
|
6
|
+
new_package_days: 7,
|
|
7
|
+
unknown_depth_weight: 45,
|
|
8
|
+
typosquat_depth_weight: 50,
|
|
9
|
+
different_maintainer_weight: 35,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SUSPICIOUS_NAMES = /(?:plain-crypto|crypto-js|secure-crypto|crypto-lib|cryptography)/i;
|
|
13
|
+
|
|
14
|
+
function levenshtein(a, b) {
|
|
15
|
+
if (Math.abs(a.length - b.length) > 2) return 3;
|
|
16
|
+
const m = a.length,
|
|
17
|
+
n = b.length;
|
|
18
|
+
if (m === 0) return n;
|
|
19
|
+
if (n === 0) return m;
|
|
20
|
+
let prev = new Int32Array(n + 1);
|
|
21
|
+
let curr = new Int32Array(n + 1);
|
|
22
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
23
|
+
for (let i = 1; i <= m; i++) {
|
|
24
|
+
curr[0] = i;
|
|
25
|
+
let rowMin = curr[0];
|
|
26
|
+
for (let j = 1; j <= n; j++) {
|
|
27
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
28
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
29
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
30
|
+
}
|
|
31
|
+
if (rowMin > 2) return 3;
|
|
32
|
+
const tmp = prev;
|
|
33
|
+
prev = curr;
|
|
34
|
+
curr = tmp;
|
|
35
|
+
}
|
|
36
|
+
return prev[n];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isTyposquat(name, popularNames) {
|
|
40
|
+
for (const popular of popularNames) {
|
|
41
|
+
if (Math.abs(name.length - popular.length) > 2) continue;
|
|
42
|
+
const dist = levenshtein(name, popular);
|
|
43
|
+
if (dist <= 2 && name !== popular) return popular;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const POPULAR_PACKAGES = [
|
|
49
|
+
'crypto-js',
|
|
50
|
+
'crypto',
|
|
51
|
+
'bcrypt',
|
|
52
|
+
'jsonwebtoken',
|
|
53
|
+
'json5',
|
|
54
|
+
'lodash',
|
|
55
|
+
'axios',
|
|
56
|
+
'express',
|
|
57
|
+
'moment',
|
|
58
|
+
'chalk',
|
|
59
|
+
'react',
|
|
60
|
+
'vue',
|
|
61
|
+
'angular',
|
|
62
|
+
'next',
|
|
63
|
+
'nuxt',
|
|
64
|
+
'typescript',
|
|
65
|
+
'eslint',
|
|
66
|
+
'prettier',
|
|
67
|
+
'webpack',
|
|
68
|
+
'babel',
|
|
69
|
+
'mongoose',
|
|
70
|
+
'redis',
|
|
71
|
+
'mysql',
|
|
72
|
+
'postgres',
|
|
73
|
+
'passport',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function collectDependencies(pkgJson) {
|
|
77
|
+
const deps = {};
|
|
78
|
+
const allDeps = {
|
|
79
|
+
...(pkgJson?.dependencies || {}),
|
|
80
|
+
...(pkgJson?.devDependencies || {}),
|
|
81
|
+
};
|
|
82
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
83
|
+
deps[name] = { version, depth: 0, isDirect: true };
|
|
84
|
+
}
|
|
85
|
+
return deps;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function computeConfidence(findings) {
|
|
89
|
+
if (findings.length === 0) return 0;
|
|
90
|
+
const maxScore = Math.max(...findings.map((f) => f.weight));
|
|
91
|
+
let base = maxScore;
|
|
92
|
+
if (findings.length > 1) base += 15;
|
|
93
|
+
return Math.min(100, Math.max(0, base));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function severityLabel(score) {
|
|
97
|
+
if (score >= 80) return 'high';
|
|
98
|
+
if (score >= 60) return 'medium';
|
|
99
|
+
return 'low';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function confidenceLabel(score) {
|
|
103
|
+
if (score >= 80) return 'HIGH';
|
|
104
|
+
if (score >= 60) return 'MEDIUM';
|
|
105
|
+
return 'LOW';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const name = 'tier1-transitive-deps';
|
|
109
|
+
|
|
110
|
+
export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
|
|
111
|
+
const pkgName = pkgJson?.name;
|
|
112
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
113
|
+
|
|
114
|
+
const deps = collectDependencies(pkgJson);
|
|
115
|
+
const depNames = Object.keys(deps);
|
|
116
|
+
if (depNames.length === 0) return [];
|
|
117
|
+
|
|
118
|
+
const findings = [];
|
|
119
|
+
|
|
120
|
+
for (const [depName, depInfo] of Object.entries(deps)) {
|
|
121
|
+
let weight = 0;
|
|
122
|
+
const reasons = [];
|
|
123
|
+
|
|
124
|
+
if (SUSPICIOUS_NAMES.test(depName)) {
|
|
125
|
+
weight += 55;
|
|
126
|
+
reasons.push('suspicious_naming: matches known malicious pattern');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const typosquatTarget = isTyposquat(depName, POPULAR_PACKAGES);
|
|
130
|
+
if (typosquatTarget) {
|
|
131
|
+
weight += THRESHOLDS.typosquat_depth_weight;
|
|
132
|
+
reasons.push(`typosquat: "${depName}" similar to "${typosquatTarget}"`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (depInfo.isDirect) {
|
|
136
|
+
const depVersion = depInfo.version || '';
|
|
137
|
+
if (depVersion.includes('x') || depVersion === '*' || /^\d+\.\d+\.\d+$/.test(depVersion)) {
|
|
138
|
+
const parts = depVersion.split('.');
|
|
139
|
+
if (parts.length === 3) {
|
|
140
|
+
const major = parseInt(parts[0], 10);
|
|
141
|
+
if (major >= 99) {
|
|
142
|
+
weight += 55;
|
|
143
|
+
reasons.push(`version_anomaly: suspicious version ${depVersion}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (weight > 0) {
|
|
150
|
+
findings.push({
|
|
151
|
+
package: depName,
|
|
152
|
+
depth: depInfo.depth,
|
|
153
|
+
isDirect: depInfo.isDirect,
|
|
154
|
+
weight,
|
|
155
|
+
reasons,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (findings.length === 0) return [];
|
|
161
|
+
|
|
162
|
+
const confidenceScore = computeConfidence(findings);
|
|
163
|
+
if (confidenceScore < THRESHOLDS.warn_threshold) return [];
|
|
164
|
+
|
|
165
|
+
const topFindings = findings.sort((a, b) => b.weight - a.weight).slice(0, 5);
|
|
166
|
+
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
detector: 'tier1-transitive-deps',
|
|
170
|
+
id: 'TIER1-TRANSITIVE-DEPS',
|
|
171
|
+
severity: severityLabel(confidenceScore),
|
|
172
|
+
confidence: confidenceLabel(confidenceScore),
|
|
173
|
+
confidenceScore,
|
|
174
|
+
subtype: 'transitive_injection',
|
|
175
|
+
message: `${findings.length} suspicious transitive dependenc${findings.length > 1 ? 'ies' : 'y'} detected`,
|
|
176
|
+
evidence: topFindings.map((f) => `${f.package} (depth ${f.depth}): ${f.reasons.join('; ')}`),
|
|
177
|
+
locations: [{ file: 'package.json', line: 1, column: 1 }],
|
|
178
|
+
crossFiles: topFindings.map((f) => f.package),
|
|
179
|
+
reference: 'D12: Axios backdoor transitive injection',
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
|
@@ -3,38 +3,54 @@ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
|
3
3
|
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
const TOP_PACKAGES = require('../../src/config/top-5000.json');
|
|
6
|
-
const
|
|
6
|
+
const _TOP_SET = new Set(TOP_PACKAGES);
|
|
7
7
|
|
|
8
8
|
function levenshtein(a, b) {
|
|
9
|
-
if (Math.abs(a.length - b.length) > 2)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
if (Math.abs(a.length - b.length) > 2) {
|
|
10
|
+
return 3;
|
|
11
|
+
}
|
|
12
|
+
const m = a.length,
|
|
13
|
+
n = b.length;
|
|
14
|
+
if (m === 0) {
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
if (n === 0) {
|
|
18
|
+
return m;
|
|
19
|
+
}
|
|
13
20
|
let prev = new Int32Array(n + 1);
|
|
14
21
|
let curr = new Int32Array(n + 1);
|
|
15
|
-
for (let j = 0; j <= n; j++)
|
|
22
|
+
for (let j = 0; j <= n; j++) {
|
|
23
|
+
prev[j] = j;
|
|
24
|
+
}
|
|
16
25
|
for (let i = 1; i <= m; i++) {
|
|
17
26
|
curr[0] = i;
|
|
18
27
|
let rowMin = curr[0];
|
|
19
28
|
for (let j = 1; j <= n; j++) {
|
|
20
29
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
21
|
-
curr[j] = Math.min(
|
|
22
|
-
|
|
23
|
-
curr[j
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
if (curr[j] < rowMin) rowMin = curr[j];
|
|
30
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
31
|
+
if (curr[j] < rowMin) {
|
|
32
|
+
rowMin = curr[j];
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
|
-
if (rowMin > 2)
|
|
29
|
-
|
|
35
|
+
if (rowMin > 2) {
|
|
36
|
+
return 3;
|
|
37
|
+
}
|
|
38
|
+
const tmp = prev;
|
|
39
|
+
prev = curr;
|
|
40
|
+
curr = tmp;
|
|
30
41
|
}
|
|
31
42
|
return prev[n];
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
function jaroWinkler(a, b) {
|
|
35
|
-
if (a === b)
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
if (a === b) {
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
const m = a.length,
|
|
50
|
+
n = b.length;
|
|
51
|
+
if (m === 0 || n === 0) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
38
54
|
const matchDist = Math.floor(Math.max(m, n) / 2) - 1;
|
|
39
55
|
const aMatch = new Array(m).fill(false);
|
|
40
56
|
const bMatch = new Array(n).fill(false);
|
|
@@ -43,36 +59,53 @@ function jaroWinkler(a, b) {
|
|
|
43
59
|
const start = Math.max(0, i - matchDist);
|
|
44
60
|
const end = Math.min(n, i + matchDist + 1);
|
|
45
61
|
for (let j = start; j < end; j++) {
|
|
46
|
-
if (bMatch[j] || a[i] !== b[j])
|
|
62
|
+
if (bMatch[j] || a[i] !== b[j]) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
47
65
|
aMatch[i] = true;
|
|
48
66
|
bMatch[j] = true;
|
|
49
67
|
matches++;
|
|
50
68
|
break;
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
|
-
if (matches === 0)
|
|
54
|
-
|
|
71
|
+
if (matches === 0) {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
let t = 0,
|
|
75
|
+
k = 0;
|
|
55
76
|
for (let i = 0; i < m; i++) {
|
|
56
|
-
if (!aMatch[i])
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
if (!aMatch[i]) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
while (!bMatch[k]) {
|
|
81
|
+
k++;
|
|
82
|
+
}
|
|
83
|
+
if (a[i] !== b[k]) {
|
|
84
|
+
t++;
|
|
85
|
+
}
|
|
59
86
|
k++;
|
|
60
87
|
}
|
|
61
88
|
const jaro = (matches / m + matches / n + (matches - t / 2) / matches) / 3;
|
|
62
89
|
let prefix = 0;
|
|
63
90
|
const limit = Math.min(4, m, n);
|
|
64
91
|
for (let i = 0; i < limit; i++) {
|
|
65
|
-
if (a[i] === b[i])
|
|
66
|
-
|
|
92
|
+
if (a[i] === b[i]) {
|
|
93
|
+
prefix++;
|
|
94
|
+
} else {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
67
97
|
}
|
|
68
98
|
return jaro + prefix * 0.1 * (1 - jaro);
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
function
|
|
72
|
-
if (!s)
|
|
101
|
+
function _soundex(s) {
|
|
102
|
+
if (!s) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
73
105
|
s = s.toLowerCase();
|
|
74
106
|
const first = s[0];
|
|
75
|
-
const rest = s
|
|
107
|
+
const rest = s
|
|
108
|
+
.slice(1)
|
|
76
109
|
.replace(/[aeiouyhw]/g, '')
|
|
77
110
|
.replace(/[bfpv]/g, '1')
|
|
78
111
|
.replace(/[cgjkqsxz]/g, '2')
|
|
@@ -85,15 +118,22 @@ function soundex(s) {
|
|
|
85
118
|
}
|
|
86
119
|
|
|
87
120
|
function homoglyphScore(a, b) {
|
|
88
|
-
if (a.length !== b.length)
|
|
89
|
-
|
|
121
|
+
if (a.length !== b.length) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const map = { 0: 'o', 1: 'l', 3: 'e', 4: 'a', 5: 's', 6: 'g', 7: 't', 8: 'b', '@': 'a' };
|
|
90
125
|
let swaps = 0;
|
|
91
126
|
for (let i = 0; i < a.length; i++) {
|
|
92
|
-
if (a[i] === b[i])
|
|
127
|
+
if (a[i] === b[i]) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
93
130
|
const na = map[a[i]] || a[i];
|
|
94
131
|
const nb = map[b[i]] || b[i];
|
|
95
|
-
if (na === b[i] || a[i] === nb || na === nb)
|
|
96
|
-
|
|
132
|
+
if (na === b[i] || a[i] === nb || na === nb) {
|
|
133
|
+
swaps++;
|
|
134
|
+
} else {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
97
137
|
}
|
|
98
138
|
return swaps > 0 ? 1 - swaps / a.length : 0;
|
|
99
139
|
}
|
|
@@ -119,34 +159,52 @@ function computeConfidence(dist, phonetic, homoglyph, registryMeta) {
|
|
|
119
159
|
if (registryMeta) {
|
|
120
160
|
const age = registryMeta.age || 0;
|
|
121
161
|
const downloads = registryMeta.weeklyDownloads || 0;
|
|
122
|
-
if (age < 30)
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
if (age < 30) {
|
|
163
|
+
score += 15;
|
|
164
|
+
}
|
|
165
|
+
if (downloads < 1000) {
|
|
166
|
+
score += 10;
|
|
167
|
+
}
|
|
168
|
+
if (age > 365 && downloads > 100000) {
|
|
169
|
+
score -= 30;
|
|
170
|
+
}
|
|
125
171
|
}
|
|
126
172
|
score = Math.max(50, Math.min(100, score));
|
|
127
173
|
return { subtype, score };
|
|
128
174
|
}
|
|
129
175
|
|
|
130
176
|
function severityLabel(score) {
|
|
131
|
-
if (score >= 80)
|
|
132
|
-
|
|
177
|
+
if (score >= 80) {
|
|
178
|
+
return 'high';
|
|
179
|
+
}
|
|
180
|
+
if (score >= 60) {
|
|
181
|
+
return 'medium';
|
|
182
|
+
}
|
|
133
183
|
return 'low';
|
|
134
184
|
}
|
|
135
185
|
|
|
136
186
|
function confidenceLabel(score) {
|
|
137
|
-
if (score >= 80)
|
|
138
|
-
|
|
187
|
+
if (score >= 80) {
|
|
188
|
+
return 'HIGH';
|
|
189
|
+
}
|
|
190
|
+
if (score >= 60) {
|
|
191
|
+
return 'MEDIUM';
|
|
192
|
+
}
|
|
139
193
|
return 'LOW';
|
|
140
194
|
}
|
|
141
195
|
|
|
142
196
|
export const name = 'tier1-typosquat';
|
|
143
197
|
|
|
144
|
-
export async function scan(pkgJson, jsFiles, registryMeta,
|
|
198
|
+
export async function scan(pkgJson, jsFiles, registryMeta, _allFiles) {
|
|
145
199
|
const findings = [];
|
|
146
200
|
const pkgName = pkgJson?.name;
|
|
147
|
-
if (!pkgName)
|
|
201
|
+
if (!pkgName) {
|
|
202
|
+
return findings;
|
|
203
|
+
}
|
|
148
204
|
|
|
149
|
-
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
205
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
206
|
+
return findings;
|
|
207
|
+
}
|
|
150
208
|
|
|
151
209
|
const namesToCheck = [];
|
|
152
210
|
let scopedName = null;
|
|
@@ -167,27 +225,48 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
167
225
|
for (const checkName of namesToCheck) {
|
|
168
226
|
for (const target of TOP_PACKAGES) {
|
|
169
227
|
if (checkName === target) {
|
|
170
|
-
if (
|
|
228
|
+
if (
|
|
229
|
+
pkgName.startsWith('@') &&
|
|
230
|
+
checkName === scopedName &&
|
|
231
|
+
!KNOWN_REPUTABLE_PACKAGES.has(scopedName)
|
|
232
|
+
) {
|
|
171
233
|
let score = 80;
|
|
172
234
|
if (registryMeta) {
|
|
173
|
-
if ((registryMeta.age || 0) < 30)
|
|
174
|
-
|
|
235
|
+
if ((registryMeta.age || 0) < 30) {
|
|
236
|
+
score += 15;
|
|
237
|
+
}
|
|
238
|
+
if ((registryMeta.weeklyDownloads || 0) < 1000) {
|
|
239
|
+
score += 10;
|
|
240
|
+
}
|
|
175
241
|
}
|
|
176
242
|
score = Math.max(50, Math.min(100, score));
|
|
177
243
|
if (score > bestScore) {
|
|
178
244
|
bestScore = score;
|
|
179
|
-
best = {
|
|
245
|
+
best = {
|
|
246
|
+
target,
|
|
247
|
+
dist: 0,
|
|
248
|
+
phonetic: 1,
|
|
249
|
+
homoglyph: 0,
|
|
250
|
+
subtype: 'edit_distance_1',
|
|
251
|
+
isScopeSquat: true,
|
|
252
|
+
};
|
|
180
253
|
}
|
|
181
254
|
}
|
|
182
255
|
continue;
|
|
183
256
|
}
|
|
184
|
-
if (Math.abs(checkName.length - target.length) > 2)
|
|
257
|
+
if (Math.abs(checkName.length - target.length) > 2) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
185
260
|
const dist = levenshtein(checkName, target);
|
|
186
|
-
if (dist > 2)
|
|
261
|
+
if (dist > 2) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
187
264
|
const phonetic = jaroWinkler(checkName, target);
|
|
188
265
|
const homoglyph = homoglyphScore(checkName, target);
|
|
189
266
|
const conf = computeConfidence(dist, phonetic, homoglyph, registryMeta);
|
|
190
|
-
if (!conf || conf.score <= bestScore)
|
|
267
|
+
if (!conf || conf.score <= bestScore) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
191
270
|
bestScore = conf.score;
|
|
192
271
|
best = { target, dist, phonetic, homoglyph, subtype: conf.subtype, isScopeSquat: false };
|
|
193
272
|
}
|
|
@@ -3,11 +3,17 @@ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
|
3
3
|
const SENTINEL_PATTERNS = new Set(['99.99.99', '11.11.11', '10.10.10']);
|
|
4
4
|
|
|
5
5
|
function parseVersion(v) {
|
|
6
|
-
if (!v || typeof v !== 'string')
|
|
6
|
+
if (!v || typeof v !== 'string') {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
7
9
|
const parts = v.split('.');
|
|
8
|
-
if (parts.length !== 3)
|
|
10
|
+
if (parts.length !== 3) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
9
13
|
const [major, minor, patch] = parts.map(Number);
|
|
10
|
-
if (isNaN(major) || isNaN(minor) || isNaN(patch))
|
|
14
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
11
17
|
return { major, minor, patch, full: v };
|
|
12
18
|
}
|
|
13
19
|
|
|
@@ -17,19 +23,23 @@ function versionScore(v) {
|
|
|
17
23
|
|
|
18
24
|
function extractVersions(registryMeta) {
|
|
19
25
|
if (Array.isArray(registryMeta)) {
|
|
20
|
-
return registryMeta.map(v => parseVersion(v)).filter(Boolean);
|
|
26
|
+
return registryMeta.map((v) => parseVersion(v)).filter(Boolean);
|
|
21
27
|
}
|
|
22
28
|
if (registryMeta && typeof registryMeta === 'object') {
|
|
23
29
|
const versions = registryMeta.versions || registryMeta.time;
|
|
24
30
|
if (versions && typeof versions === 'object') {
|
|
25
|
-
return Object.keys(versions)
|
|
31
|
+
return Object.keys(versions)
|
|
32
|
+
.map((v) => parseVersion(v))
|
|
33
|
+
.filter(Boolean);
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
return [];
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
function computeStats(scores) {
|
|
32
|
-
if (scores.length < 2)
|
|
40
|
+
if (scores.length < 2) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
33
43
|
const mean = scores.reduce((s, v) => s + v, 0) / scores.length;
|
|
34
44
|
const variance = scores.reduce((s, v) => s + (v - mean) ** 2, 0) / scores.length;
|
|
35
45
|
const stddev = Math.sqrt(variance);
|
|
@@ -38,7 +48,9 @@ function computeStats(scores) {
|
|
|
38
48
|
|
|
39
49
|
export function analyzeAnomaly(packageName, versionStr, versionHistory) {
|
|
40
50
|
const current = parseVersion(versionStr);
|
|
41
|
-
if (!current)
|
|
51
|
+
if (!current) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
42
54
|
|
|
43
55
|
const historical = extractVersions(versionHistory);
|
|
44
56
|
const currentScore = versionScore(current);
|
|
@@ -79,14 +91,18 @@ export function analyzeAnomaly(packageName, versionStr, versionHistory) {
|
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
const stats = computeStats(recentScores);
|
|
82
|
-
if (!stats)
|
|
94
|
+
if (!stats) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
83
97
|
|
|
84
98
|
const zScore = stats.stddev > 0 ? (currentScore - stats.mean) / stats.stddev : 0;
|
|
85
|
-
const baselineMaxVer = historical.find(v => versionScore(v) === stats.max)?.full || 'unknown';
|
|
99
|
+
const baselineMaxVer = historical.find((v) => versionScore(v) === stats.max)?.full || 'unknown';
|
|
86
100
|
const baselineMeanVal = (stats.mean / 10000).toFixed(1);
|
|
87
101
|
const prevMaxMajor = Math.floor(stats.max / 10000);
|
|
88
|
-
const isNormalMajorBump =
|
|
89
|
-
|
|
102
|
+
const isNormalMajorBump =
|
|
103
|
+
current.major === prevMaxMajor + 1 && current.minor === 0 && current.patch === 0;
|
|
104
|
+
const isReasonableVersion =
|
|
105
|
+
current.major <= prevMaxMajor + 2 && current.major >= Math.floor(stats.min / 10000);
|
|
90
106
|
const ratio = stats.max > 0 ? currentScore / stats.max : 0;
|
|
91
107
|
|
|
92
108
|
let flagged = false;
|
|
@@ -126,7 +142,9 @@ export function analyzeAnomaly(packageName, versionStr, versionHistory) {
|
|
|
126
142
|
reason = `Version ${versionStr} has z-score ${zScore.toFixed(1)} and is outside expected version range`;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
if (!flagged)
|
|
145
|
+
if (!flagged) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
130
148
|
|
|
131
149
|
return {
|
|
132
150
|
flagged,
|
|
@@ -141,47 +159,65 @@ export function analyzeAnomaly(packageName, versionStr, versionHistory) {
|
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
function severityLabel(sc) {
|
|
144
|
-
if (sc >= 90)
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
if (sc >= 90) {
|
|
163
|
+
return 'critical';
|
|
164
|
+
}
|
|
165
|
+
if (sc >= 70) {
|
|
166
|
+
return 'high';
|
|
167
|
+
}
|
|
168
|
+
if (sc >= 50) {
|
|
169
|
+
return 'medium';
|
|
170
|
+
}
|
|
147
171
|
return 'low';
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
function confidenceLabel(sc) {
|
|
151
|
-
if (sc >= 80)
|
|
152
|
-
|
|
175
|
+
if (sc >= 80) {
|
|
176
|
+
return 'HIGH';
|
|
177
|
+
}
|
|
178
|
+
if (sc >= 60) {
|
|
179
|
+
return 'MEDIUM';
|
|
180
|
+
}
|
|
153
181
|
return 'LOW';
|
|
154
182
|
}
|
|
155
183
|
|
|
156
184
|
export const name = 'tier1-version-anomaly';
|
|
157
185
|
|
|
158
|
-
export async function scan(pkgJson, jsFiles, registryMeta,
|
|
186
|
+
export async function scan(pkgJson, jsFiles, registryMeta, _allFiles) {
|
|
159
187
|
const pkgName = pkgJson?.name;
|
|
160
188
|
const version = pkgJson?.version;
|
|
161
189
|
|
|
162
|
-
if (!pkgName || !version)
|
|
163
|
-
|
|
190
|
+
if (!pkgName || !version) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
164
196
|
|
|
165
197
|
const result = analyzeAnomaly(pkgName, version, registryMeta);
|
|
166
|
-
if (!result)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
198
|
+
if (!result) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
detector: 'tier1-version-anomaly',
|
|
205
|
+
id: 'TIER1-VERSION-ANOMALY',
|
|
206
|
+
severity: severityLabel(result.confidenceScore),
|
|
207
|
+
confidence: confidenceLabel(result.confidenceScore),
|
|
208
|
+
confidenceScore: result.confidenceScore,
|
|
209
|
+
subtype: result.attackPattern.toLowerCase(),
|
|
210
|
+
message: `Version anomaly detected in "${pkgName}": ${result.reason}`,
|
|
211
|
+
evidence: [
|
|
212
|
+
`version: ${version}`,
|
|
213
|
+
`baseline_max: ${result.baselineMax}`,
|
|
214
|
+
`baseline_mean: ${result.baselineMean}`,
|
|
215
|
+
`z_score: ${result.zScore ?? 'N/A'}`,
|
|
216
|
+
`attack_pattern: ${result.attackPattern}`,
|
|
217
|
+
],
|
|
218
|
+
crossFiles: [],
|
|
219
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
220
|
+
reference: '176-package dependency confusion campaign',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
187
223
|
}
|