@lateos/npm-scan 0.18.3 → 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/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- 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 +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- 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 +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- 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 +138 -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 +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- 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 +147 -0
- 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 +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- 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 +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- 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 +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
|
@@ -1,12 +1,43 @@
|
|
|
1
|
-
const DIST_BUILD_PATTERNS = [
|
|
2
|
-
|
|
1
|
+
const DIST_BUILD_PATTERNS = [
|
|
2
|
+
/\/dist\//,
|
|
3
|
+
/\/build\//,
|
|
4
|
+
/\/bundle/,
|
|
5
|
+
/\/min\//,
|
|
6
|
+
/\.min\.js$/,
|
|
7
|
+
/\.bundled?\.js$/,
|
|
8
|
+
];
|
|
9
|
+
const TEST_FIXTURE_PATTERNS = [
|
|
10
|
+
/\/test\//,
|
|
11
|
+
/\/tests\//,
|
|
12
|
+
/\/__tests__\//,
|
|
13
|
+
/\/spec\//,
|
|
14
|
+
/\.test\.js$/,
|
|
15
|
+
/\.spec\.js$/,
|
|
16
|
+
/fixtures?/,
|
|
17
|
+
];
|
|
3
18
|
const KNOWN_SAFE_DOMAINS = [
|
|
4
|
-
'registry.npmjs.org',
|
|
5
|
-
'
|
|
6
|
-
'
|
|
19
|
+
'registry.npmjs.org',
|
|
20
|
+
'cdn.jsdelivr.net',
|
|
21
|
+
'unpkg.com',
|
|
22
|
+
'cdn.skypack.dev',
|
|
23
|
+
'esm.sh',
|
|
24
|
+
'deno.land',
|
|
25
|
+
'raw.githubusercontent.com',
|
|
26
|
+
'github.com',
|
|
27
|
+
'npmjs.com',
|
|
28
|
+
'nodejs.org',
|
|
29
|
+
'v8.dev',
|
|
30
|
+
'typescriptlang.org',
|
|
7
31
|
];
|
|
8
32
|
|
|
9
|
-
const LIFECYCLE_SCRIPT_NAMES = [
|
|
33
|
+
const LIFECYCLE_SCRIPT_NAMES = [
|
|
34
|
+
'install',
|
|
35
|
+
'postinstall',
|
|
36
|
+
'preinstall',
|
|
37
|
+
'prepare',
|
|
38
|
+
'prepack',
|
|
39
|
+
'postpack',
|
|
40
|
+
];
|
|
10
41
|
|
|
11
42
|
function extractUrlDomain(code) {
|
|
12
43
|
const urlMatch = code.match(/https?:\/\/([^/'"\s]+)/);
|
|
@@ -14,22 +45,26 @@ function extractUrlDomain(code) {
|
|
|
14
45
|
}
|
|
15
46
|
|
|
16
47
|
function isDistOrBuild(filePath) {
|
|
17
|
-
return DIST_BUILD_PATTERNS.some(p => p.test(filePath));
|
|
48
|
+
return DIST_BUILD_PATTERNS.some((p) => p.test(filePath));
|
|
18
49
|
}
|
|
19
50
|
|
|
20
51
|
function isTestOrFixture(filePath) {
|
|
21
|
-
return TEST_FIXTURE_PATTERNS.some(p => p.test(filePath));
|
|
52
|
+
return TEST_FIXTURE_PATTERNS.some((p) => p.test(filePath));
|
|
22
53
|
}
|
|
23
54
|
|
|
24
55
|
function isKnownSafeDomain(domain) {
|
|
25
|
-
if (!domain)
|
|
26
|
-
|
|
56
|
+
if (!domain) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return KNOWN_SAFE_DOMAINS.some((safe) => domain === safe || domain.endsWith('.' + safe));
|
|
27
60
|
}
|
|
28
61
|
|
|
29
62
|
function locateLine(code, pattern) {
|
|
30
63
|
const lines = code.split('\n');
|
|
31
64
|
for (let i = 0; i < lines.length; i++) {
|
|
32
|
-
if (pattern.test(lines[i]))
|
|
65
|
+
if (pattern.test(lines[i])) {
|
|
66
|
+
return i + 1;
|
|
67
|
+
}
|
|
33
68
|
}
|
|
34
69
|
return null;
|
|
35
70
|
}
|
|
@@ -40,65 +75,96 @@ function decodePreview(code) {
|
|
|
40
75
|
try {
|
|
41
76
|
const decoded = atob(b64Match[1]);
|
|
42
77
|
return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
|
|
43
|
-
} catch {
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore decode errors */
|
|
80
|
+
}
|
|
44
81
|
}
|
|
45
|
-
|
|
82
|
+
|
|
46
83
|
const hexMatch = code.match(/Buffer\.from\(['"]([0-9a-fA-F]+)['"],\s*['"]hex['"]\)/);
|
|
47
84
|
if (hexMatch) {
|
|
48
85
|
try {
|
|
49
86
|
const decoded = Buffer.from(hexMatch[1], 'hex').toString();
|
|
50
87
|
return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
|
|
51
|
-
} catch {
|
|
88
|
+
} catch {
|
|
89
|
+
/* ignore decode errors */
|
|
90
|
+
}
|
|
52
91
|
}
|
|
53
|
-
|
|
92
|
+
|
|
54
93
|
const btoaMatch = code.match(/btoa\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
|
|
55
94
|
if (btoaMatch) {
|
|
56
95
|
try {
|
|
57
96
|
const decoded = atob(btoaMatch[1]);
|
|
58
97
|
return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
|
|
59
|
-
} catch {
|
|
98
|
+
} catch {
|
|
99
|
+
/* ignore decode errors */
|
|
100
|
+
}
|
|
60
101
|
}
|
|
61
|
-
|
|
102
|
+
|
|
62
103
|
return null;
|
|
63
104
|
}
|
|
64
105
|
|
|
65
106
|
function detectEncodingType(code) {
|
|
66
|
-
if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code))
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (/
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code)) {
|
|
108
|
+
return 'hex';
|
|
109
|
+
}
|
|
110
|
+
if (/atob\(/.test(code)) {
|
|
111
|
+
return 'base64';
|
|
112
|
+
}
|
|
113
|
+
if (/btoa\(/.test(code)) {
|
|
114
|
+
return 'base64';
|
|
115
|
+
}
|
|
116
|
+
if (/Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code)) {
|
|
117
|
+
return 'base64';
|
|
118
|
+
}
|
|
119
|
+
if (/String\.fromCharCode\(/.test(code)) {
|
|
120
|
+
return 'charcode';
|
|
121
|
+
}
|
|
122
|
+
if (/btoa\(.*btoa\(|atob\(.*atob\(/.test(code)) {
|
|
123
|
+
return 'double-base64';
|
|
124
|
+
}
|
|
72
125
|
return 'unknown';
|
|
73
126
|
}
|
|
74
127
|
|
|
75
128
|
function isFileInLifecycleScript(filePath, pkgJson) {
|
|
76
|
-
if (!pkgJson?.scripts)
|
|
77
|
-
|
|
129
|
+
if (!pkgJson?.scripts) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
const scripts = pkgJson.scripts;
|
|
79
134
|
const fileName = filePath.split('/').pop();
|
|
80
|
-
const normalizedPath = filePath
|
|
81
|
-
|
|
135
|
+
const normalizedPath = filePath
|
|
136
|
+
.replace(/^node_modules\//, '')
|
|
137
|
+
.replace(/^dist\//, '')
|
|
138
|
+
.replace(/^build\//, '');
|
|
139
|
+
|
|
82
140
|
for (const scriptName of LIFECYCLE_SCRIPT_NAMES) {
|
|
83
141
|
const scriptValue = scripts[scriptName];
|
|
84
|
-
if (!scriptValue)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (scriptValue.includes(
|
|
89
|
-
|
|
142
|
+
if (!scriptValue) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (scriptValue.includes(filePath)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (scriptValue.includes(fileName)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (scriptValue.includes(normalizedPath)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
90
156
|
const scriptFileMatch = scriptValue.match(/[^\s'"]+\.js$/);
|
|
91
|
-
if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0]))
|
|
157
|
+
if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
92
160
|
}
|
|
93
|
-
|
|
161
|
+
|
|
94
162
|
return false;
|
|
95
163
|
}
|
|
96
164
|
|
|
97
165
|
function isLikelyLifecycleFileName(filePath) {
|
|
98
166
|
const name = filePath.split('/').pop().replace(/\.js$/, '');
|
|
99
|
-
return LIFECYCLE_SCRIPT_NAMES.includes(name) ||
|
|
100
|
-
name === 'setup' ||
|
|
101
|
-
name === 'install-helper';
|
|
167
|
+
return LIFECYCLE_SCRIPT_NAMES.includes(name) || name === 'setup' || name === 'install-helper';
|
|
102
168
|
}
|
|
103
169
|
|
|
104
170
|
function createEvidence(code, filePath, pattern, pkgJson) {
|
|
@@ -106,8 +172,9 @@ function createEvidence(code, filePath, pattern, pkgJson) {
|
|
|
106
172
|
const line = locateLine(code, pattern);
|
|
107
173
|
const decodedPreview = decodePreview(code);
|
|
108
174
|
const destinationHost = extractUrlDomain(code);
|
|
109
|
-
const lifecycleHook =
|
|
110
|
-
|
|
175
|
+
const lifecycleHook =
|
|
176
|
+
isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
|
|
177
|
+
|
|
111
178
|
return {
|
|
112
179
|
file: filePath,
|
|
113
180
|
line: line,
|
|
@@ -121,7 +188,7 @@ function createEvidence(code, filePath, pattern, pkgJson) {
|
|
|
121
188
|
export async function scan(pkgJson, files = []) {
|
|
122
189
|
const findings = [];
|
|
123
190
|
const pkgName = pkgJson?.name || '';
|
|
124
|
-
const
|
|
191
|
+
const _selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
|
|
125
192
|
|
|
126
193
|
for (const f of files) {
|
|
127
194
|
const code = f.content;
|
|
@@ -137,15 +204,23 @@ export async function scan(pkgJson, files = []) {
|
|
|
137
204
|
if (hasEval) {
|
|
138
205
|
const hexDecode = /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"]/.test(code);
|
|
139
206
|
const b64Decode = /atob\(|Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code);
|
|
140
|
-
const b64UrlDecode =
|
|
207
|
+
const b64UrlDecode =
|
|
208
|
+
/try\s*\{[^}]*atob\s*\(/s.test(code) || /btoa\(.*\)\s*[^;]*\.replace\(/s.test(code);
|
|
141
209
|
|
|
142
210
|
if (hexDecode || b64Decode || b64UrlDecode) {
|
|
143
|
-
const evidence = createEvidence(
|
|
211
|
+
const evidence = createEvidence(
|
|
212
|
+
code,
|
|
213
|
+
filePath,
|
|
214
|
+
/eval\(|new Function\(|\bFunction\('/,
|
|
215
|
+
pkgJson
|
|
216
|
+
);
|
|
144
217
|
findings.push({
|
|
145
218
|
id: 'ATK-002',
|
|
146
219
|
severity: 'medium',
|
|
147
220
|
title: 'Obfuscated payload',
|
|
148
|
-
description: hexDecode
|
|
221
|
+
description: hexDecode
|
|
222
|
+
? 'Eval with hex-decoded payload'
|
|
223
|
+
: 'Eval with base64-decoded payload',
|
|
149
224
|
evidence: evidence,
|
|
150
225
|
context: {
|
|
151
226
|
file_path: filePath,
|
|
@@ -184,8 +259,12 @@ export async function scan(pkgJson, files = []) {
|
|
|
184
259
|
}
|
|
185
260
|
}
|
|
186
261
|
|
|
187
|
-
if (
|
|
188
|
-
|
|
262
|
+
if (
|
|
263
|
+
/atob\(|Buffer\.from/.test(code) &&
|
|
264
|
+
/url|fetch|curl|http\.request|https\.request/.test(code)
|
|
265
|
+
) {
|
|
266
|
+
const isNetworkObfusc =
|
|
267
|
+
/atob\(.*(https?:\/\/|\\x|http).*\)/s.test(code) ||
|
|
189
268
|
/Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"].*fetch\(|fetch\(.*atob\(/s.test(code);
|
|
190
269
|
if (isNetworkObfusc) {
|
|
191
270
|
const evidence = createEvidence(code, filePath, /atob\(|Buffer\.from/, pkgJson);
|
|
@@ -259,4 +338,4 @@ export async function scan(pkgJson, files = []) {
|
|
|
259
338
|
}
|
|
260
339
|
|
|
261
340
|
return findings;
|
|
262
|
-
}
|
|
341
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
4
|
-
if (
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
|
+
if (
|
|
5
|
+
/process\.env\.(NPM_TOKEN|GIT_TOKEN|AWS_SECRET|AWS_ACCESS|SSH_KEY)|\.npmrc|\.ssh\/id_rsa|readFile.*\.ssh/.test(
|
|
6
|
+
code
|
|
7
|
+
)
|
|
8
|
+
) {
|
|
5
9
|
findings.push({
|
|
6
10
|
id: 'ATK-003',
|
|
7
11
|
severity: 'high',
|
|
8
12
|
title: 'Credential harvesting',
|
|
9
13
|
description: 'Env vars or .npmrc/SSH key access',
|
|
10
|
-
evidence: 'credential pattern match'
|
|
14
|
+
evidence: 'credential pattern match',
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
return findings;
|
|
14
|
-
}
|
|
18
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
4
|
if (/mkdir.*(\.vscode|\.claude|\.cursor)/.test(code)) {
|
|
5
5
|
findings.push({
|
|
6
6
|
id: 'ATK-004',
|
|
7
7
|
severity: 'high',
|
|
8
8
|
title: 'Persistence via editor configs',
|
|
9
9
|
description: 'Creates .vscode/.claude/.cursor dirs',
|
|
10
|
-
evidence: 'mkdir pattern match'
|
|
10
|
+
evidence: 'mkdir pattern match',
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
13
|
return findings;
|
|
14
|
-
}
|
|
14
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
4
|
-
if (
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
|
+
if (
|
|
5
|
+
/curl.*(-d|--data|--data-binary)|github\.com\/.*keys|pastebin|dns\.resolve.*\.com|exfil/.test(
|
|
6
|
+
code.toLowerCase()
|
|
7
|
+
)
|
|
8
|
+
) {
|
|
5
9
|
findings.push({
|
|
6
10
|
id: 'ATK-005',
|
|
7
11
|
severity: 'critical',
|
|
8
12
|
title: 'Network exfiltration',
|
|
9
13
|
description: 'Suspicious network calls: curl data exfil, pastebin, dns tunneling',
|
|
10
|
-
evidence: 'network exfil pattern'
|
|
14
|
+
evidence: 'network exfil pattern',
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
return findings;
|
|
14
|
-
}
|
|
18
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
export async function scan(pkgJson) {
|
|
2
2
|
const findings = [];
|
|
3
3
|
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
4
|
-
const squat = Object.keys(deps).filter(d => /squat|confus|typo/i.test(d.toLowerCase()));
|
|
4
|
+
const squat = Object.keys(deps).filter((d) => /squat|confus|typo/i.test(d.toLowerCase()));
|
|
5
5
|
if (squat.length) {
|
|
6
6
|
findings.push({
|
|
7
7
|
id: 'ATK-006',
|
|
8
8
|
severity: 'medium',
|
|
9
9
|
title: 'Dependency confusion',
|
|
10
10
|
description: 'Suspicious dependency names',
|
|
11
|
-
evidence: squat.join(', ')
|
|
11
|
+
evidence: squat.join(', '),
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
return findings;
|
|
15
|
-
}
|
|
15
|
+
}
|
|
@@ -1,12 +1,62 @@
|
|
|
1
|
-
const TOP_PKGS = [
|
|
1
|
+
const TOP_PKGS = [
|
|
2
|
+
'lodash',
|
|
3
|
+
'react',
|
|
4
|
+
'express',
|
|
5
|
+
'axios',
|
|
6
|
+
'chalk',
|
|
7
|
+
'vue',
|
|
8
|
+
'typescript',
|
|
9
|
+
'moment',
|
|
10
|
+
'uuid',
|
|
11
|
+
'commander',
|
|
12
|
+
'debug',
|
|
13
|
+
'semver',
|
|
14
|
+
'underscore',
|
|
15
|
+
'request',
|
|
16
|
+
'async',
|
|
17
|
+
'cheerio',
|
|
18
|
+
'bluebird',
|
|
19
|
+
'jest',
|
|
20
|
+
'mocha',
|
|
21
|
+
'dotenv',
|
|
22
|
+
'glob',
|
|
23
|
+
'minimist',
|
|
24
|
+
'body-parser',
|
|
25
|
+
'cors',
|
|
26
|
+
'helmet',
|
|
27
|
+
'jsonwebtoken',
|
|
28
|
+
'socket.io',
|
|
29
|
+
'redis',
|
|
30
|
+
'mongoose',
|
|
31
|
+
'sequelize',
|
|
32
|
+
'pg',
|
|
33
|
+
'passport',
|
|
34
|
+
'nodemailer',
|
|
35
|
+
'multer',
|
|
36
|
+
'bcrypt',
|
|
37
|
+
'winston',
|
|
38
|
+
'luxon',
|
|
39
|
+
'dayjs',
|
|
40
|
+
'rxjs',
|
|
41
|
+
'redux',
|
|
42
|
+
];
|
|
2
43
|
|
|
3
44
|
function levenshtein(a, b) {
|
|
4
|
-
const m = a.length,
|
|
45
|
+
const m = a.length,
|
|
46
|
+
n = b.length;
|
|
5
47
|
const d = Array.from({ length: m + 1 }, (_, i) => [i]);
|
|
6
|
-
for (let j = 0; j <= n; j++)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
48
|
+
for (let j = 0; j <= n; j++) {
|
|
49
|
+
d[0][j] = j;
|
|
50
|
+
}
|
|
51
|
+
for (let i = 1; i <= m; i++) {
|
|
52
|
+
for (let j = 1; j <= n; j++) {
|
|
53
|
+
d[i][j] = Math.min(
|
|
54
|
+
d[i - 1][j] + 1,
|
|
55
|
+
d[i][j - 1] + 1,
|
|
56
|
+
d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
10
60
|
return d[m][n];
|
|
11
61
|
}
|
|
12
62
|
|
|
@@ -14,9 +64,13 @@ export async function scan(pkgJson) {
|
|
|
14
64
|
const findings = [];
|
|
15
65
|
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
16
66
|
const names = Object.keys(deps);
|
|
17
|
-
if (names.length === 0)
|
|
67
|
+
if (names.length === 0) {
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
18
70
|
for (const d of names) {
|
|
19
|
-
if (d.length < 4)
|
|
71
|
+
if (d.length < 4) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
20
74
|
for (const top of TOP_PKGS) {
|
|
21
75
|
const dist = levenshtein(d, top);
|
|
22
76
|
if (dist > 0 && dist <= 2 && d !== top) {
|
|
@@ -25,11 +79,11 @@ export async function scan(pkgJson) {
|
|
|
25
79
|
severity: 'low',
|
|
26
80
|
title: 'Typosquatting suspect',
|
|
27
81
|
description: `"${d}" is edit-distance ${dist} from "${top}"`,
|
|
28
|
-
evidence: d
|
|
82
|
+
evidence: d,
|
|
29
83
|
});
|
|
30
84
|
break;
|
|
31
85
|
}
|
|
32
86
|
}
|
|
33
87
|
}
|
|
34
88
|
return findings;
|
|
35
|
-
}
|
|
89
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
3
|
const repo = pkgJson.repository || {};
|
|
4
|
-
const repoUrl = typeof repo === 'string' ? repo :
|
|
4
|
+
const repoUrl = typeof repo === 'string' ? repo : repo.url || '';
|
|
5
5
|
const pkgName = (pkgJson.name || '').toLowerCase();
|
|
6
6
|
|
|
7
7
|
const knownRepos = {
|
|
@@ -29,7 +29,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
if (repoUrl && repoUrl.includes('github.com')) {
|
|
32
|
-
const repoMatch = repoUrl.match(/github\.com[
|
|
32
|
+
const repoMatch = repoUrl.match(/github\.com[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
33
33
|
if (repoMatch) {
|
|
34
34
|
const ghRepo = repoMatch[1].toLowerCase();
|
|
35
35
|
const ghName = ghRepo.split('/')[1];
|
|
@@ -45,7 +45,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
45
45
|
severity: 'high',
|
|
46
46
|
title: 'Tarball tampering suspect',
|
|
47
47
|
description: `Repository "${ghRepo}" does not match expected "${expectedRepo}" for package "${pkgName}"`,
|
|
48
|
-
evidence: `repo: ${ghRepo}, expected: ${expectedRepo}
|
|
48
|
+
evidence: `repo: ${ghRepo}, expected: ${expectedRepo}`,
|
|
49
49
|
});
|
|
50
50
|
} else {
|
|
51
51
|
const orgExpected = knownRepos[shortName];
|
|
@@ -57,7 +57,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
57
57
|
severity: 'medium',
|
|
58
58
|
title: 'Tarball tampering suspect',
|
|
59
59
|
description: `Repository "${ghRepo}" is a different repo under a different org (legitimate: ${expectedRepo})`,
|
|
60
|
-
evidence: `org mismatch: ${ghOrg} vs ${expectedOrg}
|
|
60
|
+
evidence: `org mismatch: ${ghOrg} vs ${expectedOrg}`,
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -66,7 +66,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const code = files.map(f => f.content).join('\n');
|
|
69
|
+
const code = files.map((f) => f.content).join('\n');
|
|
70
70
|
const embeddedIntros = code.match(/\/\/\s*Source:\s*(https?:\/\/[^\s]+)/gi);
|
|
71
71
|
if (embeddedIntros && repoUrl) {
|
|
72
72
|
for (const intro of embeddedIntros) {
|
|
@@ -78,7 +78,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
78
78
|
severity: 'medium',
|
|
79
79
|
title: 'Tarball tampering suspect',
|
|
80
80
|
description: 'Source URL in file does not match declared repository',
|
|
81
|
-
evidence: srcUrl
|
|
81
|
+
evidence: srcUrl,
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
} catch {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
4
|
|
|
5
5
|
const ciPatterns = [
|
|
6
6
|
{ pattern: /process\.env\.CI\b/, label: 'CI env check' },
|
|
7
|
-
{
|
|
7
|
+
{
|
|
8
|
+
pattern: /process\.env\.(TRAVIS|CIRCLECI|GITHUB_ACTIONS|JENKINS|GITLAB_CI|CODEBUILD)/,
|
|
9
|
+
label: 'CI platform check',
|
|
10
|
+
},
|
|
8
11
|
{ pattern: /\bisCI\b/, label: 'isCI utility check' },
|
|
9
12
|
];
|
|
10
13
|
|
|
@@ -15,7 +18,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
15
18
|
severity: 'high',
|
|
16
19
|
title: 'Conditional trigger (CI/production env)',
|
|
17
20
|
description: `Package checks for CI or production environment: ${label}`,
|
|
18
|
-
evidence: 'conditional trigger detected'
|
|
21
|
+
evidence: 'conditional trigger detected',
|
|
19
22
|
});
|
|
20
23
|
break;
|
|
21
24
|
}
|
|
@@ -24,7 +27,8 @@ export async function scan(pkgJson, files = []) {
|
|
|
24
27
|
const suspiciousCode = /\beval\(|atob\(|btoa\(|new Function\(|child_process\b|\.exec\(|spawn\(/;
|
|
25
28
|
const suspiciousNetwork = /\.fetch\(|http\.request\(|https\.request\(|dns\.lookup\(/;
|
|
26
29
|
const suspiciousEnv = /process\.env\.(?!NODE_ENV)[A-Z_]{4,}/;
|
|
27
|
-
const hasSuspicious =
|
|
30
|
+
const hasSuspicious =
|
|
31
|
+
suspiciousCode.test(code) || suspiciousNetwork.test(code) || suspiciousEnv.test(code);
|
|
28
32
|
|
|
29
33
|
const timePatterns = [
|
|
30
34
|
{
|
|
@@ -52,7 +56,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
52
56
|
severity: hasSuspicious ? 'high' : 'medium',
|
|
53
57
|
title: 'Conditional trigger (time-based)',
|
|
54
58
|
description: `Package uses ${label}`,
|
|
55
|
-
evidence: `${label}${hasSuspicious ? ' — elevated (suspicious context: eval/network/exec detected)' : ''}
|
|
59
|
+
evidence: `${label}${hasSuspicious ? ' — elevated (suspicious context: eval/network/exec detected)' : ''}`,
|
|
56
60
|
});
|
|
57
61
|
break;
|
|
58
62
|
}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
4
|
|
|
5
5
|
const highPatterns = [
|
|
6
6
|
{ pattern: /\bdebugger\s*;?(\s*\/\/|\s*$|\)|\])/m, label: 'debugger statement' },
|
|
7
|
-
{
|
|
8
|
-
|
|
7
|
+
{
|
|
8
|
+
pattern: /process\.argv.*['"]--inspect['"]|process\.argv.*\binspect\b(?!.*argv)/,
|
|
9
|
+
label: 'inspect/debug flag detection',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
pattern: /hostname.*(?:docker|sandbox|container|vmware|vbox)/i,
|
|
13
|
+
label: 'anti-sandbox hostname check',
|
|
14
|
+
},
|
|
9
15
|
{ pattern: /detect.*(?:sandbox|debugger|analysis|virtual)/i, label: 'explicit evasion probe' },
|
|
10
|
-
{
|
|
16
|
+
{
|
|
17
|
+
pattern: /e\.stack\b.*(?:sandbox|docker|container|vmware)/i,
|
|
18
|
+
label: 'stack trace sandbox probe',
|
|
19
|
+
},
|
|
11
20
|
];
|
|
12
21
|
|
|
13
22
|
for (const { pattern, label } of highPatterns) {
|
|
@@ -17,35 +26,41 @@ export async function scan(pkgJson, files = []) {
|
|
|
17
26
|
severity: 'high',
|
|
18
27
|
title: 'Sandbox evasion / anti-analysis',
|
|
19
28
|
description: `Package performs anti-analysis behavior: ${label}`,
|
|
20
|
-
evidence: 'evasion pattern detected'
|
|
29
|
+
evidence: 'evasion pattern detected',
|
|
21
30
|
});
|
|
22
31
|
break;
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
if (findings.length === 0) {
|
|
27
|
-
const multiApi = [
|
|
36
|
+
const multiApi = [
|
|
37
|
+
'process.pid',
|
|
38
|
+
'process.ppid',
|
|
39
|
+
'os.hostname',
|
|
40
|
+
'os.cpus',
|
|
41
|
+
'process.arch',
|
|
42
|
+
].filter((api) => code.includes(api));
|
|
28
43
|
if (multiApi.length >= 3) {
|
|
29
44
|
findings.push({
|
|
30
45
|
id: 'ATK-010',
|
|
31
46
|
severity: 'medium',
|
|
32
47
|
title: 'Sandbox evasion / anti-analysis',
|
|
33
48
|
description: 'Multiple system fingerprinting APIs detected',
|
|
34
|
-
evidence: `${multiApi.length} fingerprinting APIs: ${multiApi.join(', ')}
|
|
49
|
+
evidence: `${multiApi.length} fingerprinting APIs: ${multiApi.join(', ')}`,
|
|
35
50
|
});
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
const multiStack = ['Error().stack', 'new Error().stack'].filter(s => code.includes(s));
|
|
54
|
+
const multiStack = ['Error().stack', 'new Error().stack'].filter((s) => code.includes(s));
|
|
40
55
|
if (multiStack.length > 0 && /atob|eval|execSync|spawn|child_process/.test(code)) {
|
|
41
56
|
findings.push({
|
|
42
57
|
id: 'ATK-010',
|
|
43
58
|
severity: 'medium',
|
|
44
59
|
title: 'Sandbox evasion / anti-analysis',
|
|
45
60
|
description: 'Stack trace capture combined with code execution',
|
|
46
|
-
evidence: 'stack trace + execution'
|
|
61
|
+
evidence: 'stack trace + execution',
|
|
47
62
|
});
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
return findings;
|
|
51
|
-
}
|
|
66
|
+
}
|