@lateos/npm-scan 1.0.0 → 1.1.1
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.de.md +3 -98
- package/README.fr.md +3 -98
- package/README.ja.md +3 -98
- package/README.md +2 -122
- package/README.zh.md +3 -98
- 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
|
@@ -1,68 +1,100 @@
|
|
|
1
1
|
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
2
|
|
|
3
3
|
const INTERNAL_SUFFIX_RE = /\.(?:internal|local|corp|intra|priv|lan)(?:[.:/]|$)/i;
|
|
4
|
-
const CORPORATE_RE =
|
|
5
|
-
|
|
4
|
+
const CORPORATE_RE =
|
|
5
|
+
/(?:github-ent|jira-ent|github\.enterprise|internal-gitlab|gitlab\.internal|jenkins\.internal|confluence\.internal)/i;
|
|
6
|
+
const PRIVATE_IP_RE =
|
|
7
|
+
/^(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})/;
|
|
6
8
|
|
|
7
9
|
function extractDomain(url) {
|
|
8
10
|
try {
|
|
9
11
|
const u = new URL(url);
|
|
10
12
|
return u.hostname;
|
|
11
13
|
} catch {
|
|
12
|
-
const m = url.match(/^(?:https?:\/\/)?([
|
|
14
|
+
const m = url.match(/^(?:https?:\/\/)?([^/\s:]+)/);
|
|
13
15
|
return m ? m[1] : null;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function isInternalUrl(url) {
|
|
18
|
-
if (!url)
|
|
20
|
+
if (!url) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
19
23
|
const domain = extractDomain(url);
|
|
20
|
-
if (!domain)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
+
if (!domain) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (INTERNAL_SUFFIX_RE.test(domain)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (CORPORATE_RE.test(domain)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (PRIVATE_IP_RE.test(domain)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
24
36
|
return false;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
function parseSemver(version) {
|
|
28
|
-
if (!version)
|
|
40
|
+
if (!version) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
29
43
|
const parts = version.replace(/^[~^]/, '').split('.');
|
|
30
44
|
const m = parseInt(parts[0], 10);
|
|
31
45
|
const n = parseInt(parts[1], 10);
|
|
32
46
|
const p = parseInt(parts[2], 10);
|
|
33
|
-
if (isNaN(m) || isNaN(n) || isNaN(p))
|
|
47
|
+
if (isNaN(m) || isNaN(n) || isNaN(p)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
34
50
|
return { major: m, minor: n, patch: p };
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
function detectSemverInflation(currentVer, registryMeta) {
|
|
38
|
-
if (!currentVer || !registryMeta)
|
|
54
|
+
if (!currentVer || !registryMeta) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
39
57
|
|
|
40
58
|
const age = registryMeta.age;
|
|
41
|
-
if (age !== undefined && age < 7)
|
|
59
|
+
if (age !== undefined && age < 7) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
42
62
|
|
|
43
63
|
const previousVer = registryMeta.previousVersion || null;
|
|
44
|
-
if (!previousVer)
|
|
64
|
+
if (!previousVer) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
45
67
|
|
|
46
68
|
const cur = parseSemver(currentVer);
|
|
47
69
|
const prev = parseSemver(previousVer);
|
|
48
|
-
if (!cur || !prev)
|
|
70
|
+
if (!cur || !prev) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
49
73
|
|
|
50
74
|
const majorJump = cur.major - prev.major;
|
|
51
75
|
const minorJump = cur.minor - prev.minor;
|
|
52
76
|
const patchJump = cur.patch - prev.patch;
|
|
53
77
|
|
|
54
|
-
if (majorJump > 10)
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
if (majorJump > 10) {
|
|
79
|
+
return { type: 'major', from: previousVer, to: currentVer, jump: majorJump };
|
|
80
|
+
}
|
|
81
|
+
if (minorJump > 20) {
|
|
82
|
+
return { type: 'minor', from: previousVer, to: currentVer, jump: minorJump };
|
|
83
|
+
}
|
|
84
|
+
if (patchJump > 50) {
|
|
85
|
+
return { type: 'patch', from: previousVer, to: currentVer, jump: patchJump };
|
|
86
|
+
}
|
|
57
87
|
|
|
58
88
|
return null;
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
export const name = 'tier1-metadata-spoof';
|
|
62
92
|
|
|
63
|
-
export async function scan(pkgJson, jsFiles, registryMeta,
|
|
93
|
+
export async function scan(pkgJson, jsFiles, registryMeta, _allFiles) {
|
|
64
94
|
const pkgName = pkgJson?.name;
|
|
65
|
-
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
95
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
66
98
|
|
|
67
99
|
const fieldUrls = [];
|
|
68
100
|
|
|
@@ -87,7 +119,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
87
119
|
if (pkgJson.funding) {
|
|
88
120
|
const arr = Array.isArray(pkgJson.funding) ? pkgJson.funding : [pkgJson.funding];
|
|
89
121
|
for (let i = 0; i < arr.length; i++) {
|
|
90
|
-
if (arr[i] && arr[i].url)
|
|
122
|
+
if (arr[i] && arr[i].url) {
|
|
123
|
+
addField(`funding[${i}].url`, arr[i].url);
|
|
124
|
+
}
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
|
|
@@ -95,13 +129,15 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
95
129
|
addField('author.url', pkgJson.author.url);
|
|
96
130
|
}
|
|
97
131
|
|
|
98
|
-
const internalFields = fieldUrls.filter(f => isInternalUrl(f.value));
|
|
132
|
+
const internalFields = fieldUrls.filter((f) => isInternalUrl(f.value));
|
|
99
133
|
const hasInternalUrls = internalFields.length > 0;
|
|
100
134
|
|
|
101
135
|
const currentVersion = pkgJson?.version;
|
|
102
136
|
const semverInflation = detectSemverInflation(currentVersion, registryMeta);
|
|
103
137
|
|
|
104
|
-
if (!hasInternalUrls && !semverInflation)
|
|
138
|
+
if (!hasInternalUrls && !semverInflation) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
105
141
|
|
|
106
142
|
let baseScore = 0;
|
|
107
143
|
let subtype = '';
|
|
@@ -117,10 +153,14 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
117
153
|
const domain = extractDomain(f.value);
|
|
118
154
|
evidence.push(`url: ${f.field} = ${f.value}`);
|
|
119
155
|
|
|
120
|
-
let pattern
|
|
121
|
-
if (PRIVATE_IP_RE.test(domain))
|
|
122
|
-
|
|
123
|
-
else
|
|
156
|
+
let pattern;
|
|
157
|
+
if (PRIVATE_IP_RE.test(domain)) {
|
|
158
|
+
pattern = 'private IP';
|
|
159
|
+
} else if (CORPORATE_RE.test(domain)) {
|
|
160
|
+
pattern = 'corporate domain';
|
|
161
|
+
} else {
|
|
162
|
+
pattern = 'internal domain';
|
|
163
|
+
}
|
|
124
164
|
evidence.push(`pattern: ${domain} (${pattern})`);
|
|
125
165
|
|
|
126
166
|
locations.push({ field: f.field, value: f.value });
|
|
@@ -140,7 +180,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
140
180
|
if (hasInternalUrls) {
|
|
141
181
|
baseScore = Math.round(baseScore * 1.3);
|
|
142
182
|
evidence.push(semverMsg);
|
|
143
|
-
evidence.push(
|
|
183
|
+
evidence.push(
|
|
184
|
+
`${semverInflation.type} version jump (${semverInflation.jump}) without changelog`
|
|
185
|
+
);
|
|
144
186
|
locations.push({ field: 'version', old: semverInflation.from, new: semverInflation.to });
|
|
145
187
|
primaryMessage += ' + unjustified semver jump';
|
|
146
188
|
} else {
|
|
@@ -155,26 +197,34 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
155
197
|
const confidenceScore = Math.max(50, Math.min(90, baseScore));
|
|
156
198
|
|
|
157
199
|
function severityLabel(sc) {
|
|
158
|
-
if (sc >= 70)
|
|
200
|
+
if (sc >= 70) {
|
|
201
|
+
return 'high';
|
|
202
|
+
}
|
|
159
203
|
return 'medium';
|
|
160
204
|
}
|
|
161
205
|
|
|
162
206
|
function confidenceLabel(sc) {
|
|
163
|
-
if (sc >= 80)
|
|
164
|
-
|
|
207
|
+
if (sc >= 80) {
|
|
208
|
+
return 'HIGH';
|
|
209
|
+
}
|
|
210
|
+
if (sc >= 60) {
|
|
211
|
+
return 'MEDIUM';
|
|
212
|
+
}
|
|
165
213
|
return 'LOW';
|
|
166
214
|
}
|
|
167
215
|
|
|
168
|
-
return [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
detector: 'tier1-metadata-spoof',
|
|
219
|
+
id: 'TIER1-METADATA-SPOOF',
|
|
220
|
+
severity: severityLabel(confidenceScore),
|
|
221
|
+
confidence: confidenceLabel(confidenceScore),
|
|
222
|
+
confidenceScore,
|
|
223
|
+
subtype,
|
|
224
|
+
message: primaryMessage,
|
|
225
|
+
evidence,
|
|
226
|
+
locations,
|
|
227
|
+
reference: 'Campaign 1',
|
|
228
|
+
},
|
|
229
|
+
];
|
|
180
230
|
}
|
|
@@ -1,34 +1,51 @@
|
|
|
1
1
|
const SCAN_HOOKS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
2
2
|
|
|
3
|
-
const REMOTE_FETCH_RE =
|
|
3
|
+
const REMOTE_FETCH_RE =
|
|
4
|
+
/\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
|
|
4
5
|
const BINARY_EXEC_RE = /\b(?:execFile|execFileSync|execSync|exec|spawnSync|spawn)\s*\(/;
|
|
5
6
|
const DETACHED_RE = /detached\s*:\s*true/;
|
|
6
7
|
|
|
7
8
|
function severityLabel(score) {
|
|
8
|
-
if (score >= 95)
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
if (score >= 95) {
|
|
10
|
+
return 'critical';
|
|
11
|
+
}
|
|
12
|
+
if (score >= 80) {
|
|
13
|
+
return 'high';
|
|
14
|
+
}
|
|
15
|
+
if (score >= 60) {
|
|
16
|
+
return 'medium';
|
|
17
|
+
}
|
|
11
18
|
return 'low';
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
function confidenceLabel(score) {
|
|
15
|
-
if (score >= 95)
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
if (score >= 95) {
|
|
23
|
+
return 'CRITICAL';
|
|
24
|
+
}
|
|
25
|
+
if (score >= 80) {
|
|
26
|
+
return 'HIGH';
|
|
27
|
+
}
|
|
28
|
+
if (score >= 60) {
|
|
29
|
+
return 'MEDIUM';
|
|
30
|
+
}
|
|
18
31
|
return 'LOW';
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
export const name = 'tier1-multistage-postinstall';
|
|
22
35
|
|
|
23
|
-
export async function scan(pkgJson,
|
|
36
|
+
export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
|
|
24
37
|
const scripts = pkgJson?.scripts;
|
|
25
|
-
if (!scripts || typeof scripts !== 'object')
|
|
38
|
+
if (!scripts || typeof scripts !== 'object') {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
26
41
|
|
|
27
42
|
const findings = [];
|
|
28
43
|
|
|
29
44
|
for (const hookName of SCAN_HOOKS) {
|
|
30
45
|
const content = scripts[hookName];
|
|
31
|
-
if (!content || typeof content !== 'string')
|
|
46
|
+
if (!content || typeof content !== 'string') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
32
49
|
|
|
33
50
|
const hasRemoteFetch = REMOTE_FETCH_RE.test(content);
|
|
34
51
|
const hasBinaryExec = BINARY_EXEC_RE.test(content);
|
|
@@ -37,7 +54,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
37
54
|
const signalA = hasRemoteFetch && hasBinaryExec;
|
|
38
55
|
const signalB = hasDetached;
|
|
39
56
|
|
|
40
|
-
if (!signalA && !signalB)
|
|
57
|
+
if (!signalA && !signalB) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
41
60
|
|
|
42
61
|
let confidenceScore;
|
|
43
62
|
let subtype;
|
|
@@ -54,9 +73,15 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
const evidence = [`hook: ${hookName}`];
|
|
57
|
-
if (hasRemoteFetch)
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
if (hasRemoteFetch) {
|
|
77
|
+
evidence.push('pattern: remote fetch call');
|
|
78
|
+
}
|
|
79
|
+
if (hasBinaryExec) {
|
|
80
|
+
evidence.push('pattern: binary execution call');
|
|
81
|
+
}
|
|
82
|
+
if (hasDetached) {
|
|
83
|
+
evidence.push('pattern: detached background process');
|
|
84
|
+
}
|
|
60
85
|
|
|
61
86
|
findings.push({
|
|
62
87
|
detector: 'tier1-multistage-postinstall',
|
|
@@ -67,11 +92,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
67
92
|
subtype,
|
|
68
93
|
message: `Multi-stage install hook detected in "${hookName}" — ${subtype}`,
|
|
69
94
|
evidence,
|
|
70
|
-
locations: [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
locations: [
|
|
96
|
+
{
|
|
97
|
+
file: 'package.json',
|
|
98
|
+
field: `scripts.${hookName}`,
|
|
99
|
+
value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
75
102
|
crossFiles: [],
|
|
76
103
|
reference: 'Sonatype-2026-003429',
|
|
77
104
|
});
|
|
@@ -6,8 +6,10 @@ const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
|
6
6
|
const ENTROPY_THRESHOLD = 5.3;
|
|
7
7
|
const PAYLOAD_SIZE_THRESHOLD = 100000;
|
|
8
8
|
|
|
9
|
-
function analyze(code,
|
|
10
|
-
if (!code || code.length < 20)
|
|
9
|
+
function analyze(code, _label) {
|
|
10
|
+
if (!code || code.length < 20) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
11
13
|
|
|
12
14
|
const entropy = shannonEntropy(code);
|
|
13
15
|
const patterns = detectPatterns(code);
|
|
@@ -15,7 +17,7 @@ function analyze(code, label) {
|
|
|
15
17
|
const payloadSize = code.length;
|
|
16
18
|
|
|
17
19
|
let score = 0;
|
|
18
|
-
|
|
20
|
+
const flags = [];
|
|
19
21
|
|
|
20
22
|
if (entropy > ENTROPY_THRESHOLD) {
|
|
21
23
|
score += 35;
|
|
@@ -43,7 +45,9 @@ function analyze(code, label) {
|
|
|
43
45
|
|
|
44
46
|
if (payloadSize > PAYLOAD_SIZE_THRESHOLD) {
|
|
45
47
|
score += 15;
|
|
46
|
-
flags.push(
|
|
48
|
+
flags.push(
|
|
49
|
+
`Payload size ${payloadSize} bytes exceeds ${PAYLOAD_SIZE_THRESHOLD} byte threshold`
|
|
50
|
+
);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
if (patterns.includes('XOR_CIPHER') && patterns.length >= 2) {
|
|
@@ -58,7 +62,9 @@ function analyze(code, label) {
|
|
|
58
62
|
|
|
59
63
|
score = Math.max(0, Math.min(100, score));
|
|
60
64
|
|
|
61
|
-
if (score < 40)
|
|
65
|
+
if (score < 40) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
62
68
|
|
|
63
69
|
return {
|
|
64
70
|
flagged: true,
|
|
@@ -73,31 +79,47 @@ function analyze(code, label) {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
function severityLabel(sc) {
|
|
76
|
-
if (sc >= 90)
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
if (sc >= 90) {
|
|
83
|
+
return 'critical';
|
|
84
|
+
}
|
|
85
|
+
if (sc >= 70) {
|
|
86
|
+
return 'high';
|
|
87
|
+
}
|
|
88
|
+
if (sc >= 50) {
|
|
89
|
+
return 'medium';
|
|
90
|
+
}
|
|
79
91
|
return 'low';
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
function confidenceLabel(sc) {
|
|
83
|
-
if (sc >= 80)
|
|
84
|
-
|
|
95
|
+
if (sc >= 80) {
|
|
96
|
+
return 'HIGH';
|
|
97
|
+
}
|
|
98
|
+
if (sc >= 60) {
|
|
99
|
+
return 'MEDIUM';
|
|
100
|
+
}
|
|
85
101
|
return 'LOW';
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
export const name = 'tier1-obfuscation-heuristics';
|
|
89
105
|
|
|
90
|
-
export async function scan(pkgJson, jsFiles,
|
|
106
|
+
export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
|
|
91
107
|
const pkgName = pkgJson?.name;
|
|
92
|
-
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
108
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
93
111
|
|
|
94
112
|
const findings = [];
|
|
95
113
|
const scripts = pkgJson?.scripts || {};
|
|
96
114
|
|
|
97
115
|
for (const [hookName, scriptContent] of Object.entries(scripts)) {
|
|
98
|
-
if (!LIFECYCLE_SCRIPTS.includes(hookName))
|
|
116
|
+
if (!LIFECYCLE_SCRIPTS.includes(hookName)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
99
119
|
const result = analyze(scriptContent, hookName);
|
|
100
|
-
if (!result)
|
|
120
|
+
if (!result) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
101
123
|
|
|
102
124
|
findings.push({
|
|
103
125
|
detector: 'tier1-obfuscation-heuristics',
|
|
@@ -123,12 +145,18 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
123
145
|
|
|
124
146
|
for (const f of jsFiles || []) {
|
|
125
147
|
const content = f.content || '';
|
|
126
|
-
if (content.length < 100)
|
|
148
|
+
if (content.length < 100) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
127
151
|
|
|
128
152
|
const result = analyze(content, f.path || 'unknown.js');
|
|
129
|
-
if (!result)
|
|
153
|
+
if (!result) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
130
156
|
|
|
131
|
-
if (result.confidenceScore < 50)
|
|
157
|
+
if (result.confidenceScore < 50) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
132
160
|
|
|
133
161
|
findings.push({
|
|
134
162
|
detector: 'tier1-obfuscation-heuristics',
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
|
+
|
|
3
|
+
const THRESHOLDS = {
|
|
4
|
+
flag_threshold: 75,
|
|
5
|
+
warn_threshold: 60,
|
|
6
|
+
burst_window_minutes: 60,
|
|
7
|
+
min_packages_burst: 3,
|
|
8
|
+
identical_payload_weight: 40,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function parseTimeStamps(registryMeta) {
|
|
12
|
+
const timeData = registryMeta?.time;
|
|
13
|
+
if (!timeData || typeof timeData !== 'object') return [];
|
|
14
|
+
return Object.entries(timeData)
|
|
15
|
+
.map(([ver, ts]) => ({
|
|
16
|
+
version: ver,
|
|
17
|
+
time: new Date(ts).getTime(),
|
|
18
|
+
}))
|
|
19
|
+
.filter((e) => !isNaN(e.time))
|
|
20
|
+
.sort((a, b) => a.time - b.time);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findBursts(entries, windowMs) {
|
|
24
|
+
const bursts = [];
|
|
25
|
+
for (let i = 0; i < entries.length; i++) {
|
|
26
|
+
const windowEnd = entries[i].time + windowMs;
|
|
27
|
+
const group = [];
|
|
28
|
+
for (let j = i; j < entries.length && entries[j].time <= windowEnd; j++) {
|
|
29
|
+
group.push(entries[j]);
|
|
30
|
+
}
|
|
31
|
+
if (group.length >= 3) {
|
|
32
|
+
bursts.push({
|
|
33
|
+
startVersion: group[0].version,
|
|
34
|
+
endVersion: group[group.length - 1].version,
|
|
35
|
+
count: group.length,
|
|
36
|
+
windowMinutes: windowMs / 60000,
|
|
37
|
+
versions: group.map((e) => e.version),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return bursts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function computeConfidence(bursts, findings) {
|
|
45
|
+
let base = 40;
|
|
46
|
+
if (bursts.length > 0) {
|
|
47
|
+
base += 20 + Math.min(bursts[0].count * 5, 25);
|
|
48
|
+
}
|
|
49
|
+
if (findings.length > 0) {
|
|
50
|
+
base += THRESHOLDS.identical_payload_weight;
|
|
51
|
+
}
|
|
52
|
+
return Math.min(100, Math.max(0, base));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function severityLabel(score) {
|
|
56
|
+
if (score >= 80) return 'high';
|
|
57
|
+
if (score >= 60) return 'medium';
|
|
58
|
+
return 'low';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function confidenceLabel(score) {
|
|
62
|
+
if (score >= 80) return 'HIGH';
|
|
63
|
+
if (score >= 60) return 'MEDIUM';
|
|
64
|
+
return 'LOW';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const name = 'tier1-self-propagation';
|
|
68
|
+
|
|
69
|
+
export async function scan(pkgJson, _jsFiles, registryMeta, _allFiles) {
|
|
70
|
+
const pkgName = pkgJson?.name;
|
|
71
|
+
if (!pkgName) return [];
|
|
72
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
73
|
+
|
|
74
|
+
const entries = parseTimeStamps(registryMeta);
|
|
75
|
+
if (entries.length < 3) return [];
|
|
76
|
+
|
|
77
|
+
const windowMs = (THRESHOLDS.burst_window_minutes || 60) * 60 * 1000;
|
|
78
|
+
const bursts = findBursts(entries, windowMs);
|
|
79
|
+
if (bursts.length === 0) return [];
|
|
80
|
+
|
|
81
|
+
const burst = bursts[0];
|
|
82
|
+
const confidenceScore = computeConfidence(bursts, []);
|
|
83
|
+
if (confidenceScore < THRESHOLDS.warn_threshold) return [];
|
|
84
|
+
|
|
85
|
+
const relatedPackages = [];
|
|
86
|
+
const maintainer =
|
|
87
|
+
registryMeta?.maintainer || registryMeta?.versions?.[pkgJson.version]?._npmUser?.name;
|
|
88
|
+
const namespaces = registryMeta?.namespacePackages || [];
|
|
89
|
+
if (namespaces.length > 0) {
|
|
90
|
+
for (const np of namespaces) {
|
|
91
|
+
if (np !== pkgName) relatedPackages.push(np);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
detector: 'tier1-self-propagation',
|
|
98
|
+
id: 'TIER1-SELF-PROPAGATION',
|
|
99
|
+
severity: severityLabel(confidenceScore),
|
|
100
|
+
confidence: confidenceLabel(confidenceScore),
|
|
101
|
+
confidenceScore,
|
|
102
|
+
subtype: 'self_propagation_burst',
|
|
103
|
+
message: `Self-propagation burst detected: ${burst.count} versions in ${burst.windowMinutes} minutes`,
|
|
104
|
+
evidence: [
|
|
105
|
+
`burst: ${burst.count} versions in ${burst.windowMinutes}min`,
|
|
106
|
+
`window: ${burst.startVersion} -> ${burst.endVersion}`,
|
|
107
|
+
`related_packages: ${relatedPackages.length}`,
|
|
108
|
+
`maintainer: ${maintainer || 'unknown'}`,
|
|
109
|
+
],
|
|
110
|
+
locations: [{ file: 'package.json', line: 1, column: 1 }],
|
|
111
|
+
crossFiles: relatedPackages.slice(0, 10),
|
|
112
|
+
reference: 'D10: @redhat-cloud-services Miasma self-propagation',
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|