@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.
Files changed (110) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +199 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -669
  6. package/README.fr.md +707 -668
  7. package/README.ja.md +704 -665
  8. package/README.md +826 -801
  9. package/README.zh.md +708 -669
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +24 -0
  26. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +24 -0
  27. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +90 -0
  28. package/backend/detectors/axios-poisoning/index.js +94 -0
  29. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  30. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  31. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  32. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  33. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  34. package/backend/detectors/hf-impersonation/index.js +396 -396
  35. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  36. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  37. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  38. package/backend/detectors/index.js +75 -38
  39. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  40. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  41. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  42. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  43. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  44. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  45. package/backend/detectors/megalodon/index.js +80 -80
  46. package/backend/detectors/megalodon/types.js +9 -9
  47. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  48. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  49. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  50. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  51. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  52. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  53. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  54. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  55. package/backend/detectors/msh-supplement/d1-obfuscation.js +18 -0
  56. package/backend/detectors/msh-supplement/d2-persistence.js +47 -0
  57. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +35 -0
  58. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +33 -0
  59. package/backend/detectors/msh-supplement/index.js +107 -0
  60. package/backend/detectors/tier1-binary-embed.js +219 -0
  61. package/backend/detectors/tier1-infostealer.js +280 -0
  62. package/backend/detectors/tier1-lifecycle-hook.js +176 -0
  63. package/backend/detectors/tier1-metadata-spoof.js +180 -0
  64. package/backend/detectors/tier1-typosquat.js +219 -0
  65. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +77 -0
  66. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +37 -0
  67. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +66 -0
  68. package/backend/detectors/typosquat-vpmdhaj/index.js +98 -0
  69. package/backend/fetch.js +175 -175
  70. package/backend/index.js +4 -4
  71. package/backend/license.js +89 -89
  72. package/backend/lockfile.js +379 -379
  73. package/backend/pdf.js +245 -245
  74. package/backend/policy.js +193 -176
  75. package/backend/provenance.js +79 -0
  76. package/backend/report.js +254 -254
  77. package/backend/sbom.js +66 -66
  78. package/backend/siem/cef.js +32 -32
  79. package/backend/siem/ecs.js +40 -40
  80. package/backend/siem/index.js +18 -18
  81. package/backend/siem/qradar.js +56 -56
  82. package/backend/siem/sentinel.js +27 -27
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  94. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  95. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  96. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  97. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  98. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  99. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  100. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  101. package/deploy/helm/npm-scan/values.yaml +102 -102
  102. package/package.json +57 -57
  103. package/scripts/download-corpus.js +30 -30
  104. package/scripts/gen-mal-corpus.js +34 -34
  105. package/scripts/generate-campaign-fixtures.js +170 -0
  106. package/src/config/top-5000.json +87 -0
  107. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  108. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  109. package/test/fixtures/lockfiles/yarn.lock +103 -103
  110. 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 };