@lateos/npm-scan 0.16.0 → 0.16.5
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/.dockerignore +20 -20
- package/.husky/pre-commit +1 -1
- package/CHANGELOG.md +199 -199
- package/LICENSING.md +19 -19
- package/README.de.md +708 -669
- package/README.fr.md +707 -668
- package/README.ja.md +704 -665
- package/README.md +826 -801
- package/README.zh.md +708 -669
- package/SECURITY.md +72 -72
- package/backend/cra.js +68 -68
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +24 -0
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +24 -0
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +90 -0
- package/backend/detectors/axios-poisoning/index.js +94 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +75 -38
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/msh-supplement/d1-obfuscation.js +18 -0
- package/backend/detectors/msh-supplement/d2-persistence.js +47 -0
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +35 -0
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +33 -0
- package/backend/detectors/msh-supplement/index.js +107 -0
- package/backend/detectors/tier1-binary-embed.js +219 -0
- package/backend/detectors/tier1-infostealer.js +280 -0
- package/backend/detectors/tier1-lifecycle-hook.js +176 -0
- package/backend/detectors/tier1-metadata-spoof.js +180 -0
- package/backend/detectors/tier1-typosquat.js +219 -0
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +77 -0
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +37 -0
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +66 -0
- package/backend/detectors/typosquat-vpmdhaj/index.js +98 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -176
- package/backend/provenance.js +79 -0
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/deploy/helm/npm-scan/Chart.yaml +21 -21
- package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
- package/deploy/helm/npm-scan/templates/api.yaml +93 -93
- package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
- package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
- package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
- package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
- package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
- package/deploy/helm/npm-scan/values.yaml +102 -102
- package/package.json +57 -57
- package/scripts/download-corpus.js +30 -30
- package/scripts/gen-mal-corpus.js +34 -34
- package/scripts/generate-campaign-fixtures.js +170 -0
- package/src/config/top-5000.json +87 -0
- package/test/fixtures/lockfiles/npm-lock.json +68 -68
- package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
- package/test/fixtures/lockfiles/yarn.lock +103 -103
- package/test/fixtures/mock-data.js +69 -69
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
const TOP_PKGS = ['lodash', 'react', 'express', 'axios', 'chalk', 'vue', 'typescript', 'moment', 'uuid', 'commander', 'debug', 'semver', 'underscore', 'request', 'async', 'cheerio', 'bluebird', 'jest', 'mocha', 'dotenv', 'glob', 'minimist', 'body-parser', 'cors', 'helmet', 'jsonwebtoken', 'socket.io', 'redis', 'mongoose', 'sequelize', 'pg', 'passport', 'nodemailer', 'multer', 'bcrypt', 'winston', 'luxon', 'dayjs', 'rxjs', 'redux'];
|
|
2
|
-
|
|
3
|
-
function levenshtein(a, b) {
|
|
4
|
-
const m = a.length, n = b.length;
|
|
5
|
-
const d = Array.from({ length: m + 1 }, (_, i) => [i]);
|
|
6
|
-
for (let j = 0; j <= n; j++) d[0][j] = j;
|
|
7
|
-
for (let i = 1; i <= m; i++)
|
|
8
|
-
for (let j = 1; j <= n; j++)
|
|
9
|
-
d[i][j] = Math.min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+(a[i-1]===b[j-1]?0:1));
|
|
10
|
-
return d[m][n];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function scan(pkgJson) {
|
|
14
|
-
const findings = [];
|
|
15
|
-
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
16
|
-
const names = Object.keys(deps);
|
|
17
|
-
if (names.length === 0) return findings;
|
|
18
|
-
for (const d of names) {
|
|
19
|
-
if (d.length < 4) continue;
|
|
20
|
-
for (const top of TOP_PKGS) {
|
|
21
|
-
const dist = levenshtein(d, top);
|
|
22
|
-
if (dist > 0 && dist <= 2 && d !== top) {
|
|
23
|
-
findings.push({
|
|
24
|
-
id: 'ATK-007',
|
|
25
|
-
severity: 'low',
|
|
26
|
-
title: 'Typosquatting suspect',
|
|
27
|
-
description: `"${d}" is edit-distance ${dist} from "${top}"`,
|
|
28
|
-
evidence: d
|
|
29
|
-
});
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return findings;
|
|
1
|
+
const TOP_PKGS = ['lodash', 'react', 'express', 'axios', 'chalk', 'vue', 'typescript', 'moment', 'uuid', 'commander', 'debug', 'semver', 'underscore', 'request', 'async', 'cheerio', 'bluebird', 'jest', 'mocha', 'dotenv', 'glob', 'minimist', 'body-parser', 'cors', 'helmet', 'jsonwebtoken', 'socket.io', 'redis', 'mongoose', 'sequelize', 'pg', 'passport', 'nodemailer', 'multer', 'bcrypt', 'winston', 'luxon', 'dayjs', 'rxjs', 'redux'];
|
|
2
|
+
|
|
3
|
+
function levenshtein(a, b) {
|
|
4
|
+
const m = a.length, n = b.length;
|
|
5
|
+
const d = Array.from({ length: m + 1 }, (_, i) => [i]);
|
|
6
|
+
for (let j = 0; j <= n; j++) d[0][j] = j;
|
|
7
|
+
for (let i = 1; i <= m; i++)
|
|
8
|
+
for (let j = 1; j <= n; j++)
|
|
9
|
+
d[i][j] = Math.min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+(a[i-1]===b[j-1]?0:1));
|
|
10
|
+
return d[m][n];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function scan(pkgJson) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
16
|
+
const names = Object.keys(deps);
|
|
17
|
+
if (names.length === 0) return findings;
|
|
18
|
+
for (const d of names) {
|
|
19
|
+
if (d.length < 4) continue;
|
|
20
|
+
for (const top of TOP_PKGS) {
|
|
21
|
+
const dist = levenshtein(d, top);
|
|
22
|
+
if (dist > 0 && dist <= 2 && d !== top) {
|
|
23
|
+
findings.push({
|
|
24
|
+
id: 'ATK-007',
|
|
25
|
+
severity: 'low',
|
|
26
|
+
title: 'Typosquatting suspect',
|
|
27
|
+
description: `"${d}" is edit-distance ${dist} from "${top}"`,
|
|
28
|
+
evidence: d
|
|
29
|
+
});
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
35
|
}
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
export async function scan(pkgJson, files = []) {
|
|
2
|
-
const findings = [];
|
|
3
|
-
const repo = pkgJson.repository || {};
|
|
4
|
-
const repoUrl = typeof repo === 'string' ? repo : (repo.url || '');
|
|
5
|
-
const pkgName = (pkgJson.name || '').toLowerCase();
|
|
6
|
-
|
|
7
|
-
const knownRepos = {
|
|
8
|
-
lodash: 'lodash/lodash',
|
|
9
|
-
chalk: 'chalk/chalk',
|
|
10
|
-
react: 'facebook/react',
|
|
11
|
-
axios: 'axios/axios',
|
|
12
|
-
express: 'expressjs/express',
|
|
13
|
-
vue: 'vuejs/core',
|
|
14
|
-
typescript: 'microsoft/typescript',
|
|
15
|
-
moment: 'moment/moment',
|
|
16
|
-
uuid: 'uuidjs/uuid',
|
|
17
|
-
commander: 'tj/commander.js',
|
|
18
|
-
debug: 'debug-js/debug',
|
|
19
|
-
semver: 'npm/node-semver',
|
|
20
|
-
underscore: 'jashkenas/underscore',
|
|
21
|
-
request: 'request/request',
|
|
22
|
-
async: 'caolan/async',
|
|
23
|
-
cheerio: 'cheeriojs/cheerio',
|
|
24
|
-
bluebird: 'petkaantonov/bluebird',
|
|
25
|
-
jest: 'jestjs/jest',
|
|
26
|
-
mocha: 'mochajs/mocha',
|
|
27
|
-
dotenv: 'motdotla/dotenv',
|
|
28
|
-
glob: 'isaacs/node-glob',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
if (repoUrl && repoUrl.includes('github.com')) {
|
|
32
|
-
const repoMatch = repoUrl.match(/github\.com[\/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
33
|
-
if (repoMatch) {
|
|
34
|
-
const ghRepo = repoMatch[1].toLowerCase();
|
|
35
|
-
const ghName = ghRepo.split('/')[1];
|
|
36
|
-
const ghOrg = ghRepo.split('/')[0];
|
|
37
|
-
const shortName = pkgName.split('/').pop();
|
|
38
|
-
|
|
39
|
-
if (ghName !== shortName) {
|
|
40
|
-
const expectedRepo = knownRepos[pkgName] || knownRepos[shortName];
|
|
41
|
-
|
|
42
|
-
if (expectedRepo && expectedRepo !== ghRepo) {
|
|
43
|
-
findings.push({
|
|
44
|
-
id: 'ATK-008',
|
|
45
|
-
severity: 'high',
|
|
46
|
-
title: 'Tarball tampering suspect',
|
|
47
|
-
description: `Repository "${ghRepo}" does not match expected "${expectedRepo}" for package "${pkgName}"`,
|
|
48
|
-
evidence: `repo: ${ghRepo}, expected: ${expectedRepo}`
|
|
49
|
-
});
|
|
50
|
-
} else {
|
|
51
|
-
const orgExpected = knownRepos[shortName];
|
|
52
|
-
if (orgExpected) {
|
|
53
|
-
const expectedOrg = orgExpected.split('/')[0];
|
|
54
|
-
if (ghOrg !== expectedOrg) {
|
|
55
|
-
findings.push({
|
|
56
|
-
id: 'ATK-008',
|
|
57
|
-
severity: 'medium',
|
|
58
|
-
title: 'Tarball tampering suspect',
|
|
59
|
-
description: `Repository "${ghRepo}" is a different repo under a different org (legitimate: ${expectedRepo})`,
|
|
60
|
-
evidence: `org mismatch: ${ghOrg} vs ${expectedOrg}`
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const code = files.map(f => f.content).join('\n');
|
|
70
|
-
const embeddedIntros = code.match(/\/\/\s*Source:\s*(https?:\/\/[^\s]+)/gi);
|
|
71
|
-
if (embeddedIntros && repoUrl) {
|
|
72
|
-
for (const intro of embeddedIntros) {
|
|
73
|
-
const srcUrl = intro.replace(/\/\/\s*Source:\s*/i, '').trim();
|
|
74
|
-
try {
|
|
75
|
-
if (!repoUrl.includes(new URL(srcUrl).hostname)) {
|
|
76
|
-
findings.push({
|
|
77
|
-
id: 'ATK-008',
|
|
78
|
-
severity: 'medium',
|
|
79
|
-
title: 'Tarball tampering suspect',
|
|
80
|
-
description: 'Source URL in file does not match declared repository',
|
|
81
|
-
evidence: srcUrl
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// ignore malformed URLs
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return findings;
|
|
91
|
-
}
|
|
1
|
+
export async function scan(pkgJson, files = []) {
|
|
2
|
+
const findings = [];
|
|
3
|
+
const repo = pkgJson.repository || {};
|
|
4
|
+
const repoUrl = typeof repo === 'string' ? repo : (repo.url || '');
|
|
5
|
+
const pkgName = (pkgJson.name || '').toLowerCase();
|
|
6
|
+
|
|
7
|
+
const knownRepos = {
|
|
8
|
+
lodash: 'lodash/lodash',
|
|
9
|
+
chalk: 'chalk/chalk',
|
|
10
|
+
react: 'facebook/react',
|
|
11
|
+
axios: 'axios/axios',
|
|
12
|
+
express: 'expressjs/express',
|
|
13
|
+
vue: 'vuejs/core',
|
|
14
|
+
typescript: 'microsoft/typescript',
|
|
15
|
+
moment: 'moment/moment',
|
|
16
|
+
uuid: 'uuidjs/uuid',
|
|
17
|
+
commander: 'tj/commander.js',
|
|
18
|
+
debug: 'debug-js/debug',
|
|
19
|
+
semver: 'npm/node-semver',
|
|
20
|
+
underscore: 'jashkenas/underscore',
|
|
21
|
+
request: 'request/request',
|
|
22
|
+
async: 'caolan/async',
|
|
23
|
+
cheerio: 'cheeriojs/cheerio',
|
|
24
|
+
bluebird: 'petkaantonov/bluebird',
|
|
25
|
+
jest: 'jestjs/jest',
|
|
26
|
+
mocha: 'mochajs/mocha',
|
|
27
|
+
dotenv: 'motdotla/dotenv',
|
|
28
|
+
glob: 'isaacs/node-glob',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (repoUrl && repoUrl.includes('github.com')) {
|
|
32
|
+
const repoMatch = repoUrl.match(/github\.com[\/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
33
|
+
if (repoMatch) {
|
|
34
|
+
const ghRepo = repoMatch[1].toLowerCase();
|
|
35
|
+
const ghName = ghRepo.split('/')[1];
|
|
36
|
+
const ghOrg = ghRepo.split('/')[0];
|
|
37
|
+
const shortName = pkgName.split('/').pop();
|
|
38
|
+
|
|
39
|
+
if (ghName !== shortName) {
|
|
40
|
+
const expectedRepo = knownRepos[pkgName] || knownRepos[shortName];
|
|
41
|
+
|
|
42
|
+
if (expectedRepo && expectedRepo !== ghRepo) {
|
|
43
|
+
findings.push({
|
|
44
|
+
id: 'ATK-008',
|
|
45
|
+
severity: 'high',
|
|
46
|
+
title: 'Tarball tampering suspect',
|
|
47
|
+
description: `Repository "${ghRepo}" does not match expected "${expectedRepo}" for package "${pkgName}"`,
|
|
48
|
+
evidence: `repo: ${ghRepo}, expected: ${expectedRepo}`
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
const orgExpected = knownRepos[shortName];
|
|
52
|
+
if (orgExpected) {
|
|
53
|
+
const expectedOrg = orgExpected.split('/')[0];
|
|
54
|
+
if (ghOrg !== expectedOrg) {
|
|
55
|
+
findings.push({
|
|
56
|
+
id: 'ATK-008',
|
|
57
|
+
severity: 'medium',
|
|
58
|
+
title: 'Tarball tampering suspect',
|
|
59
|
+
description: `Repository "${ghRepo}" is a different repo under a different org (legitimate: ${expectedRepo})`,
|
|
60
|
+
evidence: `org mismatch: ${ghOrg} vs ${expectedOrg}`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const code = files.map(f => f.content).join('\n');
|
|
70
|
+
const embeddedIntros = code.match(/\/\/\s*Source:\s*(https?:\/\/[^\s]+)/gi);
|
|
71
|
+
if (embeddedIntros && repoUrl) {
|
|
72
|
+
for (const intro of embeddedIntros) {
|
|
73
|
+
const srcUrl = intro.replace(/\/\/\s*Source:\s*/i, '').trim();
|
|
74
|
+
try {
|
|
75
|
+
if (!repoUrl.includes(new URL(srcUrl).hostname)) {
|
|
76
|
+
findings.push({
|
|
77
|
+
id: 'ATK-008',
|
|
78
|
+
severity: 'medium',
|
|
79
|
+
title: 'Tarball tampering suspect',
|
|
80
|
+
description: 'Source URL in file does not match declared repository',
|
|
81
|
+
evidence: srcUrl
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore malformed URLs
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return findings;
|
|
91
|
+
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
export async function scan(pkgJson, files = []) {
|
|
2
|
-
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
4
|
-
|
|
5
|
-
const ciPatterns = [
|
|
6
|
-
{ pattern: /process\.env\.CI\b/, label: 'CI env check' },
|
|
7
|
-
{ pattern: /process\.env\.(TRAVIS|CIRCLECI|GITHUB_ACTIONS|JENKINS|GITLAB_CI|CODEBUILD)/, label: 'CI platform check' },
|
|
8
|
-
{ pattern: /\bisCI\b/, label: 'isCI utility check' },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
for (const { pattern, label } of ciPatterns) {
|
|
12
|
-
if (pattern.test(code)) {
|
|
13
|
-
findings.push({
|
|
14
|
-
id: 'ATK-009',
|
|
15
|
-
severity: 'high',
|
|
16
|
-
title: 'Conditional trigger (CI/production env)',
|
|
17
|
-
description: `Package checks for CI or production environment: ${label}`,
|
|
18
|
-
evidence: 'conditional trigger detected'
|
|
19
|
-
});
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const suspiciousCode = /\beval\(|atob\(|btoa\(|new Function\(|child_process\b|\.exec\(|spawn\(/;
|
|
25
|
-
const suspiciousNetwork = /\.fetch\(|http\.request\(|https\.request\(|dns\.lookup\(/;
|
|
26
|
-
const suspiciousEnv = /process\.env\.(?!NODE_ENV)[A-Z_]{4,}/;
|
|
27
|
-
const hasSuspicious = suspiciousCode.test(code) || suspiciousNetwork.test(code) || suspiciousEnv.test(code);
|
|
28
|
-
|
|
29
|
-
const timePatterns = [
|
|
30
|
-
{
|
|
31
|
-
pattern: /new Date\(\)\s*[><=!]+\s*new Date\(['"]\d{4}/,
|
|
32
|
-
label: 'time-based activation',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
pattern: /\bDate\.now\(\)\s*[><=!]+.*(?:eval|fetch|exec|write|crypto|env\.CI)/i,
|
|
36
|
-
label: 'timestamp check with suspicious behavior',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
pattern: /\bsetTimeout\s*\([^)]*,\s*(?!0\b)[1-9]\d{3,}/,
|
|
40
|
-
label: 'long-delay execution (>1000ms)',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
pattern: /\bDate\(\)\b.*(?:exec|eval|fetch|write|crypto)/i,
|
|
44
|
-
label: 'date check with suspicious behavior',
|
|
45
|
-
},
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
for (const { pattern, label } of timePatterns) {
|
|
49
|
-
if (pattern.test(code)) {
|
|
50
|
-
findings.push({
|
|
51
|
-
id: 'ATK-009',
|
|
52
|
-
severity: hasSuspicious ? 'high' : 'medium',
|
|
53
|
-
title: 'Conditional trigger (time-based)',
|
|
54
|
-
description: `Package uses ${label}`,
|
|
55
|
-
evidence: `${label}${hasSuspicious ? ' — elevated (suspicious context: eval/network/exec detected)' : ''}`
|
|
56
|
-
});
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return findings;
|
|
62
|
-
}
|
|
1
|
+
export async function scan(pkgJson, files = []) {
|
|
2
|
+
const findings = [];
|
|
3
|
+
const code = files.map(f => f.content).join('\n');
|
|
4
|
+
|
|
5
|
+
const ciPatterns = [
|
|
6
|
+
{ pattern: /process\.env\.CI\b/, label: 'CI env check' },
|
|
7
|
+
{ pattern: /process\.env\.(TRAVIS|CIRCLECI|GITHUB_ACTIONS|JENKINS|GITLAB_CI|CODEBUILD)/, label: 'CI platform check' },
|
|
8
|
+
{ pattern: /\bisCI\b/, label: 'isCI utility check' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
for (const { pattern, label } of ciPatterns) {
|
|
12
|
+
if (pattern.test(code)) {
|
|
13
|
+
findings.push({
|
|
14
|
+
id: 'ATK-009',
|
|
15
|
+
severity: 'high',
|
|
16
|
+
title: 'Conditional trigger (CI/production env)',
|
|
17
|
+
description: `Package checks for CI or production environment: ${label}`,
|
|
18
|
+
evidence: 'conditional trigger detected'
|
|
19
|
+
});
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const suspiciousCode = /\beval\(|atob\(|btoa\(|new Function\(|child_process\b|\.exec\(|spawn\(/;
|
|
25
|
+
const suspiciousNetwork = /\.fetch\(|http\.request\(|https\.request\(|dns\.lookup\(/;
|
|
26
|
+
const suspiciousEnv = /process\.env\.(?!NODE_ENV)[A-Z_]{4,}/;
|
|
27
|
+
const hasSuspicious = suspiciousCode.test(code) || suspiciousNetwork.test(code) || suspiciousEnv.test(code);
|
|
28
|
+
|
|
29
|
+
const timePatterns = [
|
|
30
|
+
{
|
|
31
|
+
pattern: /new Date\(\)\s*[><=!]+\s*new Date\(['"]\d{4}/,
|
|
32
|
+
label: 'time-based activation',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern: /\bDate\.now\(\)\s*[><=!]+.*(?:eval|fetch|exec|write|crypto|env\.CI)/i,
|
|
36
|
+
label: 'timestamp check with suspicious behavior',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /\bsetTimeout\s*\([^)]*,\s*(?!0\b)[1-9]\d{3,}/,
|
|
40
|
+
label: 'long-delay execution (>1000ms)',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /\bDate\(\)\b.*(?:exec|eval|fetch|write|crypto)/i,
|
|
44
|
+
label: 'date check with suspicious behavior',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
for (const { pattern, label } of timePatterns) {
|
|
49
|
+
if (pattern.test(code)) {
|
|
50
|
+
findings.push({
|
|
51
|
+
id: 'ATK-009',
|
|
52
|
+
severity: hasSuspicious ? 'high' : 'medium',
|
|
53
|
+
title: 'Conditional trigger (time-based)',
|
|
54
|
+
description: `Package uses ${label}`,
|
|
55
|
+
evidence: `${label}${hasSuspicious ? ' — elevated (suspicious context: eval/network/exec detected)' : ''}`
|
|
56
|
+
});
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return findings;
|
|
62
|
+
}
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
export async function scan(pkgJson, files = []) {
|
|
2
|
-
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
4
|
-
|
|
5
|
-
const highPatterns = [
|
|
6
|
-
{ pattern: /\bdebugger\s*;?(\s*\/\/|\s*$|\)|\])/m, label: 'debugger statement' },
|
|
7
|
-
{ pattern: /process\.argv.*['"]--inspect['"]|process\.argv.*\binspect\b(?!.*argv)/, label: 'inspect/debug flag detection' },
|
|
8
|
-
{ pattern: /hostname.*(?:docker|sandbox|container|vmware|vbox)/i, label: 'anti-sandbox hostname check' },
|
|
9
|
-
{ pattern: /detect.*(?:sandbox|debugger|analysis|virtual)/i, label: 'explicit evasion probe' },
|
|
10
|
-
{ pattern: /e\.stack\b.*(?:sandbox|docker|container|vmware)/i, label: 'stack trace sandbox probe' },
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
for (const { pattern, label } of highPatterns) {
|
|
14
|
-
if (pattern.test(code)) {
|
|
15
|
-
findings.push({
|
|
16
|
-
id: 'ATK-010',
|
|
17
|
-
severity: 'high',
|
|
18
|
-
title: 'Sandbox evasion / anti-analysis',
|
|
19
|
-
description: `Package performs anti-analysis behavior: ${label}`,
|
|
20
|
-
evidence: 'evasion pattern detected'
|
|
21
|
-
});
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (findings.length === 0) {
|
|
27
|
-
const multiApi = ['process.pid', 'process.ppid', 'os.hostname', 'os.cpus', 'process.arch'].filter(api => code.includes(api));
|
|
28
|
-
if (multiApi.length >= 3) {
|
|
29
|
-
findings.push({
|
|
30
|
-
id: 'ATK-010',
|
|
31
|
-
severity: 'medium',
|
|
32
|
-
title: 'Sandbox evasion / anti-analysis',
|
|
33
|
-
description: 'Multiple system fingerprinting APIs detected',
|
|
34
|
-
evidence: `${multiApi.length} fingerprinting APIs: ${multiApi.join(', ')}`
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const multiStack = ['Error().stack', 'new Error().stack'].filter(s => code.includes(s));
|
|
40
|
-
if (multiStack.length > 0 && /atob|eval|execSync|spawn|child_process/.test(code)) {
|
|
41
|
-
findings.push({
|
|
42
|
-
id: 'ATK-010',
|
|
43
|
-
severity: 'medium',
|
|
44
|
-
title: 'Sandbox evasion / anti-analysis',
|
|
45
|
-
description: 'Stack trace capture combined with code execution',
|
|
46
|
-
evidence: 'stack trace + execution'
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return findings;
|
|
1
|
+
export async function scan(pkgJson, files = []) {
|
|
2
|
+
const findings = [];
|
|
3
|
+
const code = files.map(f => f.content).join('\n');
|
|
4
|
+
|
|
5
|
+
const highPatterns = [
|
|
6
|
+
{ pattern: /\bdebugger\s*;?(\s*\/\/|\s*$|\)|\])/m, label: 'debugger statement' },
|
|
7
|
+
{ pattern: /process\.argv.*['"]--inspect['"]|process\.argv.*\binspect\b(?!.*argv)/, label: 'inspect/debug flag detection' },
|
|
8
|
+
{ pattern: /hostname.*(?:docker|sandbox|container|vmware|vbox)/i, label: 'anti-sandbox hostname check' },
|
|
9
|
+
{ pattern: /detect.*(?:sandbox|debugger|analysis|virtual)/i, label: 'explicit evasion probe' },
|
|
10
|
+
{ pattern: /e\.stack\b.*(?:sandbox|docker|container|vmware)/i, label: 'stack trace sandbox probe' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
for (const { pattern, label } of highPatterns) {
|
|
14
|
+
if (pattern.test(code)) {
|
|
15
|
+
findings.push({
|
|
16
|
+
id: 'ATK-010',
|
|
17
|
+
severity: 'high',
|
|
18
|
+
title: 'Sandbox evasion / anti-analysis',
|
|
19
|
+
description: `Package performs anti-analysis behavior: ${label}`,
|
|
20
|
+
evidence: 'evasion pattern detected'
|
|
21
|
+
});
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (findings.length === 0) {
|
|
27
|
+
const multiApi = ['process.pid', 'process.ppid', 'os.hostname', 'os.cpus', 'process.arch'].filter(api => code.includes(api));
|
|
28
|
+
if (multiApi.length >= 3) {
|
|
29
|
+
findings.push({
|
|
30
|
+
id: 'ATK-010',
|
|
31
|
+
severity: 'medium',
|
|
32
|
+
title: 'Sandbox evasion / anti-analysis',
|
|
33
|
+
description: 'Multiple system fingerprinting APIs detected',
|
|
34
|
+
evidence: `${multiApi.length} fingerprinting APIs: ${multiApi.join(', ')}`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const multiStack = ['Error().stack', 'new Error().stack'].filter(s => code.includes(s));
|
|
40
|
+
if (multiStack.length > 0 && /atob|eval|execSync|spawn|child_process/.test(code)) {
|
|
41
|
+
findings.push({
|
|
42
|
+
id: 'ATK-010',
|
|
43
|
+
severity: 'medium',
|
|
44
|
+
title: 'Sandbox evasion / anti-analysis',
|
|
45
|
+
description: 'Stack trace capture combined with code execution',
|
|
46
|
+
evidence: 'stack trace + execution'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return findings;
|
|
51
51
|
}
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
export async function scan(pkgJson, files = []) {
|
|
2
|
-
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
4
|
-
|
|
5
|
-
const highPatterns = [
|
|
6
|
-
{
|
|
7
|
-
pattern: /(?:exec|execSync|spawn)\s*\([^)]*npm\s+(?:install|link)\s*\./i,
|
|
8
|
-
label: 'programmatic self-propagation via npm install/link'
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
pattern: /fs\.(?:writeFile|writeFileSync|copyFile|copyFileSync)\s*\([^)]*(?:node_modules\/(?!\.)[^/]+).*(?:index\.js|main\.js|package\.json)/i,
|
|
12
|
-
label: 'direct file write to peer node_modules'
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*package\.json[^)]*["']scripts["']/i,
|
|
16
|
-
label: 'package.json script injection in another package'
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*\.\.\/[^)]*package\.json/i,
|
|
20
|
-
label: 'writes modified package.json to sibling package'
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
pattern: /(?:exec|execSync|spawn)\s*\([^)]*(?:\.\.\/|process\.env\.INIT_CWD).*npm\s+install/i,
|
|
24
|
-
label: 'cross-directory npm install propagation'
|
|
25
|
-
},
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
for (const { pattern, label } of highPatterns) {
|
|
29
|
-
if (pattern.test(code)) {
|
|
30
|
-
findings.push({
|
|
31
|
-
id: 'ATK-011',
|
|
32
|
-
severity: 'high',
|
|
33
|
-
title: 'Transitive propagation (worm)',
|
|
34
|
-
description: `Package attempts lateral worm-style spread: ${label}`,
|
|
35
|
-
evidence: 'transitive propagation pattern detected'
|
|
36
|
-
});
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (findings.length === 0) {
|
|
42
|
-
const mediumPatterns = [
|
|
43
|
-
{
|
|
44
|
-
pattern: /process\.env\.npm_package_name/,
|
|
45
|
-
label: 'reads own package name from env (self-awareness indicator)'
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
pattern: /fs\.symlink(?:Sync)?\s*\([^)]*node_modules/,
|
|
49
|
-
label: 'creates symlinks in node_modules (worm spreading mechanism)'
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
pattern: /fs\.(?:mkdir|mkdirSync)\s*\([^)]*\.\.\/[^)]*node_modules/,
|
|
53
|
-
label: 'creates directories in parent node_modules'
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
pattern: /__dirname.*\.\.\/[^/]+\/node_modules.*require\(/,
|
|
57
|
-
label: 'dynamic parent-node_modules require for lateral spread'
|
|
58
|
-
},
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
for (const { pattern, label } of mediumPatterns) {
|
|
62
|
-
if (pattern.test(code)) {
|
|
63
|
-
findings.push({
|
|
64
|
-
id: 'ATK-011',
|
|
65
|
-
severity: 'medium',
|
|
66
|
-
title: 'Transitive propagation (worm)',
|
|
67
|
-
description: label,
|
|
68
|
-
evidence: 'potential propagation indicator'
|
|
69
|
-
});
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return findings;
|
|
76
|
-
}
|
|
1
|
+
export async function scan(pkgJson, files = []) {
|
|
2
|
+
const findings = [];
|
|
3
|
+
const code = files.map(f => f.content).join('\n');
|
|
4
|
+
|
|
5
|
+
const highPatterns = [
|
|
6
|
+
{
|
|
7
|
+
pattern: /(?:exec|execSync|spawn)\s*\([^)]*npm\s+(?:install|link)\s*\./i,
|
|
8
|
+
label: 'programmatic self-propagation via npm install/link'
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
pattern: /fs\.(?:writeFile|writeFileSync|copyFile|copyFileSync)\s*\([^)]*(?:node_modules\/(?!\.)[^/]+).*(?:index\.js|main\.js|package\.json)/i,
|
|
12
|
+
label: 'direct file write to peer node_modules'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*package\.json[^)]*["']scripts["']/i,
|
|
16
|
+
label: 'package.json script injection in another package'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*\.\.\/[^)]*package\.json/i,
|
|
20
|
+
label: 'writes modified package.json to sibling package'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /(?:exec|execSync|spawn)\s*\([^)]*(?:\.\.\/|process\.env\.INIT_CWD).*npm\s+install/i,
|
|
24
|
+
label: 'cross-directory npm install propagation'
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const { pattern, label } of highPatterns) {
|
|
29
|
+
if (pattern.test(code)) {
|
|
30
|
+
findings.push({
|
|
31
|
+
id: 'ATK-011',
|
|
32
|
+
severity: 'high',
|
|
33
|
+
title: 'Transitive propagation (worm)',
|
|
34
|
+
description: `Package attempts lateral worm-style spread: ${label}`,
|
|
35
|
+
evidence: 'transitive propagation pattern detected'
|
|
36
|
+
});
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (findings.length === 0) {
|
|
42
|
+
const mediumPatterns = [
|
|
43
|
+
{
|
|
44
|
+
pattern: /process\.env\.npm_package_name/,
|
|
45
|
+
label: 'reads own package name from env (self-awareness indicator)'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: /fs\.symlink(?:Sync)?\s*\([^)]*node_modules/,
|
|
49
|
+
label: 'creates symlinks in node_modules (worm spreading mechanism)'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
pattern: /fs\.(?:mkdir|mkdirSync)\s*\([^)]*\.\.\/[^)]*node_modules/,
|
|
53
|
+
label: 'creates directories in parent node_modules'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pattern: /__dirname.*\.\.\/[^/]+\/node_modules.*require\(/,
|
|
57
|
+
label: 'dynamic parent-node_modules require for lateral spread'
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const { pattern, label } of mediumPatterns) {
|
|
62
|
+
if (pattern.test(code)) {
|
|
63
|
+
findings.push({
|
|
64
|
+
id: 'ATK-011',
|
|
65
|
+
severity: 'medium',
|
|
66
|
+
title: 'Transitive propagation (worm)',
|
|
67
|
+
description: label,
|
|
68
|
+
evidence: 'potential propagation indicator'
|
|
69
|
+
});
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return findings;
|
|
76
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const BLOCKED_VERSIONS = new Map([
|
|
2
|
+
['axios', ['1.14.1', '0.30.4']],
|
|
3
|
+
]);
|
|
4
|
+
|
|
5
|
+
export function scanVersionBlocklist(pkgJson) {
|
|
6
|
+
const pkgName = pkgJson?.name || '';
|
|
7
|
+
const pkgVersion = pkgJson?.version || '';
|
|
8
|
+
|
|
9
|
+
const blocked = BLOCKED_VERSIONS.get(pkgName);
|
|
10
|
+
if (!blocked) return { triggered: false, stopCondition: false, matchedVersion: null };
|
|
11
|
+
|
|
12
|
+
if (blocked.includes(pkgVersion)) {
|
|
13
|
+
return {
|
|
14
|
+
triggered: true,
|
|
15
|
+
stopCondition: true,
|
|
16
|
+
matchedVersion: pkgVersion,
|
|
17
|
+
reason: `Known compromised version in registry poisoning campaign`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { triggered: false, stopCondition: false, matchedVersion: null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { BLOCKED_VERSIONS };
|