@lateos/npm-scan 0.18.2 → 1.0.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +265 -233
  2. package/LICENSING.md +19 -19
  3. package/README.de.md +708 -708
  4. package/README.fr.md +707 -707
  5. package/README.ja.md +704 -704
  6. package/README.md +861 -826
  7. package/README.zh.md +708 -708
  8. package/VALIDATION.md +92 -0
  9. package/backend/cra.js +68 -68
  10. package/backend/db/pg-schema.sql +155 -0
  11. package/backend/db/schema.sql +32 -32
  12. package/backend/db.js +88 -88
  13. package/backend/detectors/atk-001-lifecycle.js +17 -17
  14. package/backend/detectors/atk-002-obfusc.js +261 -261
  15. package/backend/detectors/atk-003-creds.js +13 -13
  16. package/backend/detectors/atk-004-persist.js +13 -13
  17. package/backend/detectors/atk-005-exfil.js +13 -13
  18. package/backend/detectors/atk-006-depconf.js +14 -14
  19. package/backend/detectors/atk-007-typosquat.js +34 -34
  20. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  21. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  22. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  23. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  24. package/backend/detectors/config/thresholds.js +66 -0
  25. package/backend/detectors/config/whitelist.json +74 -0
  26. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  27. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  28. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  29. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  30. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  31. package/backend/detectors/hf-impersonation/index.js +396 -396
  32. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  33. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  34. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  35. package/backend/detectors/index.js +87 -81
  36. package/backend/detectors/lib/ast-patterns.js +21 -0
  37. package/backend/detectors/lib/entropy-analyzer.js +24 -0
  38. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  39. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  40. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  41. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  42. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  43. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  44. package/backend/detectors/megalodon/index.js +80 -80
  45. package/backend/detectors/megalodon/types.js +9 -9
  46. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  47. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  48. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  49. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  50. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  51. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  52. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  53. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  54. package/backend/detectors/tier1-binary-embed.js +34 -5
  55. package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
  56. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  57. package/backend/detectors/tier1-version-anomaly.js +187 -0
  58. package/backend/detectors.test.js +88 -0
  59. package/backend/fetch.js +175 -175
  60. package/backend/index.js +4 -4
  61. package/backend/license.js +89 -89
  62. package/backend/lockfile.js +379 -379
  63. package/backend/pdf.js +245 -245
  64. package/backend/policy.js +193 -193
  65. package/backend/report.js +254 -254
  66. package/backend/sbom.js +66 -66
  67. package/backend/scripts/analyze-false-positives.js +146 -0
  68. package/backend/scripts/analyze-validation.js +151 -0
  69. package/backend/scripts/detect-false-positives.js +93 -0
  70. package/backend/scripts/fetch-top-packages.js +129 -0
  71. package/backend/scripts/validate-detectors.js +142 -0
  72. package/backend/siem/cef.js +32 -32
  73. package/backend/siem/ecs.js +40 -40
  74. package/backend/siem/index.js +18 -18
  75. package/backend/siem/qradar.js +56 -56
  76. package/backend/siem/sentinel.js +27 -27
  77. package/backend/tests-d5-enhanced.test.js +46 -0
  78. package/backend/tests-d6-version-anomaly.test.js +58 -0
  79. package/backend/tests-d6.test.js +116 -0
  80. package/backend/tests-d6c.test.js +106 -0
  81. package/backend/tests-d7-obfuscation.test.js +91 -0
  82. package/backend/tests.test.js +898 -0
  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/package.json +74 -57
  94. package/.dockerignore +0 -20
  95. package/.husky/pre-commit +0 -1
  96. package/SECURITY.md +0 -73
  97. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  98. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  99. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  100. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  101. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  102. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  103. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  104. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  105. package/deploy/helm/npm-scan/values.yaml +0 -103
  106. package/scripts/download-corpus.js +0 -30
  107. package/scripts/gen-mal-corpus.js +0 -35
  108. package/scripts/generate-campaign-fixtures.js +0 -170
  109. package/src/config/top-5000.json +0 -87
  110. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  111. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  112. package/test/fixtures/lockfiles/yarn.lock +0 -104
  113. package/test/fixtures/mock-data.js +0 -69
@@ -1,116 +1,116 @@
1
- const ACTIVATION_RISK_MATRIX = {
2
- '*': { base: 'critical', label: 'Wildcard (all files)' },
3
- 'onStartupFinished': { base: 'high', label: 'Startup finished' },
4
- 'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
5
- 'workspaceContains': { base: 'high', label: 'Workspace contains' },
6
- 'onCommand:*': { base: 'low', label: 'Any command' },
7
- };
8
-
9
- const DEFAULT_BASE_RISK = 'medium';
10
-
11
- const ESCALATION_KEYWORDS = [
12
- 'npx', 'bun', 'curl', 'wget', 'fetch(',
13
- 'exec(', 'spawn(', 'execSync', 'spawnSync',
14
- 'child_process', 'shell: true', 'detached: true',
15
- ];
16
-
17
- const BUNDLED_BUN_PATTERN = /bun|runtime/;
18
-
19
- const SIZE_DELTA_THRESHOLD = 400 * 1024;
20
-
21
- const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
22
-
23
- export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
24
- const signals = [];
25
-
26
- const activationEvents = extensionManifest?.activationEvents || [];
27
- if (activationEvents.length === 0 && extensionManifest?.main) {
28
- return { triggered: false, signals: [], riskLevel: null, why: [] };
29
- }
30
-
31
- let maxBaseRisk = 0;
32
- const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
33
- const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
34
-
35
- let worstEvent = null;
36
- const why = [];
37
-
38
- for (const event of activationEvents) {
39
- const risk = ACTIVATION_RISK_MATRIX[event];
40
- if (risk) {
41
- const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
42
- if (baseIdx > maxBaseRisk) {
43
- maxBaseRisk = baseIdx;
44
- worstEvent = event;
45
- }
46
- } else if (event.includes('*') && event !== 'onCommand:*') {
47
- const baseIdx = riskValues['high'];
48
- if (baseIdx > maxBaseRisk) {
49
- maxBaseRisk = baseIdx;
50
- worstEvent = event;
51
- }
52
- }
53
- }
54
-
55
- const contributes = extensionManifest?.contributes || {};
56
- const commands = contributes?.commands || [];
57
- const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
58
-
59
- const bundledDeps = extensionManifest?.bundledDependencies || [];
60
- const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
61
-
62
- const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
63
- const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
64
-
65
- const activationEventsStr = activationEvents.join(' ');
66
- const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
67
-
68
- let escalateToCritical = false;
69
-
70
- if (hasShellKeyword || hasBunBundled || hasShellInActivationContext) {
71
- escalateToCritical = true;
72
- why.push('HIGH activation event + shell/execution keywords');
73
- }
74
-
75
- if (versionHistory.length >= 2) {
76
- const sizes = versionHistory
77
- .filter(v => v.assetSize)
78
- .map(v => v.assetSize)
79
- .sort((a, b) => b - a);
80
-
81
- if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
82
- escalateToCritical = true;
83
- why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
84
- }
85
- }
86
-
87
- const priorActivationEvents = priorVersions
88
- .filter(v => v.activationEvents)
89
- .flatMap(v => v.activationEvents);
90
-
91
- if (priorActivationEvents.length > 0) {
92
- const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
93
- if (newEvents.length > 0) {
94
- why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
95
- if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
96
- escalateToCritical = true;
97
- }
98
- }
99
- }
100
-
101
- let riskLevel = maxBaseRisk > 0 ? riskLabels[maxBaseRisk] : null;
102
- if (escalateToCritical && riskValues[riskLevel] <= riskValues['high']) {
103
- riskLevel = 'critical';
104
- }
105
-
106
- if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
107
-
108
- signals.push({
109
- type: 'ACTIVATION_EVENT_RISK',
110
- activationEvents,
111
- riskLevel,
112
- why,
113
- });
114
-
115
- return { triggered: true, signals, riskLevel, why };
116
- }
1
+ const ACTIVATION_RISK_MATRIX = {
2
+ '*': { base: 'critical', label: 'Wildcard (all files)' },
3
+ 'onStartupFinished': { base: 'high', label: 'Startup finished' },
4
+ 'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
5
+ 'workspaceContains': { base: 'high', label: 'Workspace contains' },
6
+ 'onCommand:*': { base: 'low', label: 'Any command' },
7
+ };
8
+
9
+ const DEFAULT_BASE_RISK = 'medium';
10
+
11
+ const ESCALATION_KEYWORDS = [
12
+ 'npx', 'bun', 'curl', 'wget', 'fetch(',
13
+ 'exec(', 'spawn(', 'execSync', 'spawnSync',
14
+ 'child_process', 'shell: true', 'detached: true',
15
+ ];
16
+
17
+ const BUNDLED_BUN_PATTERN = /bun|runtime/;
18
+
19
+ const SIZE_DELTA_THRESHOLD = 400 * 1024;
20
+
21
+ const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
22
+
23
+ export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
24
+ const signals = [];
25
+
26
+ const activationEvents = extensionManifest?.activationEvents || [];
27
+ if (activationEvents.length === 0 && extensionManifest?.main) {
28
+ return { triggered: false, signals: [], riskLevel: null, why: [] };
29
+ }
30
+
31
+ let maxBaseRisk = 0;
32
+ const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
33
+ const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
34
+
35
+ let worstEvent = null;
36
+ const why = [];
37
+
38
+ for (const event of activationEvents) {
39
+ const risk = ACTIVATION_RISK_MATRIX[event];
40
+ if (risk) {
41
+ const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
42
+ if (baseIdx > maxBaseRisk) {
43
+ maxBaseRisk = baseIdx;
44
+ worstEvent = event;
45
+ }
46
+ } else if (event.includes('*') && event !== 'onCommand:*') {
47
+ const baseIdx = riskValues['high'];
48
+ if (baseIdx > maxBaseRisk) {
49
+ maxBaseRisk = baseIdx;
50
+ worstEvent = event;
51
+ }
52
+ }
53
+ }
54
+
55
+ const contributes = extensionManifest?.contributes || {};
56
+ const commands = contributes?.commands || [];
57
+ const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
58
+
59
+ const bundledDeps = extensionManifest?.bundledDependencies || [];
60
+ const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
61
+
62
+ const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
63
+ const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
64
+
65
+ const activationEventsStr = activationEvents.join(' ');
66
+ const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
67
+
68
+ let escalateToCritical = false;
69
+
70
+ if (hasShellKeyword || hasBunBundled || hasShellInActivationContext) {
71
+ escalateToCritical = true;
72
+ why.push('HIGH activation event + shell/execution keywords');
73
+ }
74
+
75
+ if (versionHistory.length >= 2) {
76
+ const sizes = versionHistory
77
+ .filter(v => v.assetSize)
78
+ .map(v => v.assetSize)
79
+ .sort((a, b) => b - a);
80
+
81
+ if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
82
+ escalateToCritical = true;
83
+ why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
84
+ }
85
+ }
86
+
87
+ const priorActivationEvents = priorVersions
88
+ .filter(v => v.activationEvents)
89
+ .flatMap(v => v.activationEvents);
90
+
91
+ if (priorActivationEvents.length > 0) {
92
+ const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
93
+ if (newEvents.length > 0) {
94
+ why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
95
+ if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
96
+ escalateToCritical = true;
97
+ }
98
+ }
99
+ }
100
+
101
+ let riskLevel = maxBaseRisk > 0 ? riskLabels[maxBaseRisk] : null;
102
+ if (escalateToCritical && riskValues[riskLevel] <= riskValues['high']) {
103
+ riskLevel = 'critical';
104
+ }
105
+
106
+ if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
107
+
108
+ signals.push({
109
+ type: 'ACTIVATION_EVENT_RISK',
110
+ activationEvents,
111
+ riskLevel,
112
+ why,
113
+ });
114
+
115
+ return { triggered: true, signals, riskLevel, why };
116
+ }
@@ -1,52 +1,52 @@
1
- export async function checkBurstPublish(versionHistory, config = {}) {
2
- const windowMinutes = config.burstWindowMinutes ?? 30;
3
- const threshold = config.burstVersionThreshold ?? 2;
4
- const hotPullMinutes = config.hotPullMinutes ?? 20;
5
-
6
- const entries = versionHistory
7
- .filter(v => v.publishedAt)
8
- .map(v => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
9
- .filter(e => !Number.isNaN(e.time))
10
- .sort((a, b) => a.time - b.time);
11
-
12
- if (entries.length < threshold) return { triggered: false };
13
-
14
- const windowMs = windowMinutes * 60 * 1000;
15
- let burstFound = false;
16
- let burstWindowStart = null;
17
- let burstWindowEnd = null;
18
- let burstVersionCount = 0;
19
- let burstVersions = [];
20
-
21
- for (let i = 0; i < entries.length; i++) {
22
- const start = entries[i].time;
23
- const end = start + windowMs;
24
- const inWindow = entries.filter(e => e.time >= start && e.time <= end);
25
-
26
- if (inWindow.length >= threshold) {
27
- burstFound = true;
28
- burstWindowStart = new Date(start).toISOString();
29
- burstWindowEnd = new Date(end).toISOString();
30
- burstVersionCount = inWindow.length;
31
- burstVersions = inWindow.map(e => e.version);
32
- break;
33
- }
34
- }
35
-
36
- let hotPullDetected = false;
37
- for (let i = 1; i < entries.length; i++) {
38
- const gapMinutes = (entries[i].time - entries[i - 1].time) / (1000 * 60);
39
- if (gapMinutes > 0 && gapMinutes < hotPullMinutes) {
40
- hotPullDetected = true;
41
- break;
42
- }
43
- }
44
-
45
- return {
46
- triggered: burstFound || hotPullDetected,
47
- burstWindow: burstFound
48
- ? { start: burstWindowStart, end: burstWindowEnd, versionCount: burstVersionCount, versions: burstVersions }
49
- : null,
50
- hotPullDetected,
51
- };
52
- }
1
+ export async function checkBurstPublish(versionHistory, config = {}) {
2
+ const windowMinutes = config.burstWindowMinutes ?? 30;
3
+ const threshold = config.burstVersionThreshold ?? 2;
4
+ const hotPullMinutes = config.hotPullMinutes ?? 20;
5
+
6
+ const entries = versionHistory
7
+ .filter(v => v.publishedAt)
8
+ .map(v => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
9
+ .filter(e => !Number.isNaN(e.time))
10
+ .sort((a, b) => a.time - b.time);
11
+
12
+ if (entries.length < threshold) return { triggered: false };
13
+
14
+ const windowMs = windowMinutes * 60 * 1000;
15
+ let burstFound = false;
16
+ let burstWindowStart = null;
17
+ let burstWindowEnd = null;
18
+ let burstVersionCount = 0;
19
+ let burstVersions = [];
20
+
21
+ for (let i = 0; i < entries.length; i++) {
22
+ const start = entries[i].time;
23
+ const end = start + windowMs;
24
+ const inWindow = entries.filter(e => e.time >= start && e.time <= end);
25
+
26
+ if (inWindow.length >= threshold) {
27
+ burstFound = true;
28
+ burstWindowStart = new Date(start).toISOString();
29
+ burstWindowEnd = new Date(end).toISOString();
30
+ burstVersionCount = inWindow.length;
31
+ burstVersions = inWindow.map(e => e.version);
32
+ break;
33
+ }
34
+ }
35
+
36
+ let hotPullDetected = false;
37
+ for (let i = 1; i < entries.length; i++) {
38
+ const gapMinutes = (entries[i].time - entries[i - 1].time) / (1000 * 60);
39
+ if (gapMinutes > 0 && gapMinutes < hotPullMinutes) {
40
+ hotPullDetected = true;
41
+ break;
42
+ }
43
+ }
44
+
45
+ return {
46
+ triggered: burstFound || hotPullDetected,
47
+ burstWindow: burstFound
48
+ ? { start: burstWindowStart, end: burstWindowEnd, versionCount: burstVersionCount, versions: burstVersions }
49
+ : null,
50
+ hotPullDetected,
51
+ };
52
+ }
@@ -1,88 +1,88 @@
1
- const CREDENTIAL_FILE_PATTERNS = [
2
- /~\/\.npmrc/,
3
- /~\/\.gitconfig/,
4
- /~\/\.aws\/credentials/,
5
- /~\/\.ssh\/id_\w+/,
6
- /~\/\.vault-token/,
7
- /~\/\.claude\/settings\.json/,
8
- /~\/Library\/Application\s+Support\/1Password\//,
9
- /\/etc\/vault\/token/,
10
- /\/proc\/\*\/mem/,
11
- /\$GITHUB_ENV/,
12
- /\$GITHUB_TOKEN/,
13
- /\$NPM_TOKEN/,
14
- /\$NODE_AUTH_TOKEN/,
15
- /GH_TOKEN/,
16
- ];
17
-
18
- const EXFIL_CHANNEL_PATTERNS = [
19
- /(?:[a-z0-9_-]{40,})\.[a-z0-9_-]+\.(?:com|io|org|net|app|dev|xyz)(?:\/[^\s"')\]]{0,50})?/i,
20
- /\/gists\b.*authorization/i,
21
- /\/repos\/[^/]+\/[^/]+\/git\/refs/i,
22
- /AES-256-GCM/,
23
- /RSA\/(?:PKCS|OAEP)/,
24
- ];
25
-
26
- const ANTI_ANALYSIS_PATTERNS = [
27
- { pattern: /os\.cpus\(\)\.length\s*<\s*4/, label: 'CPU core count check (< 4)' },
28
- { pattern: /Intl\.DateTimeFormat.*(?:timeZone|locale)/, label: 'Timezone/locale check' },
29
- { pattern: /Intl\.DateTimeFormat.*\b(?:ru|rus|kz|by|cn|cns)\b/i, label: 'CIS/locale filtering' },
30
- { pattern: /\bspawn\(\s*[^,]+,\s*\{[^}]*detached:\s*true\s*\}/, label: 'Detached process spawn' },
31
- { pattern: /\bBUN_INSTALL\b/, label: 'BUN_INSTALL env reference' },
32
- { pattern: /~\/\.bun\/bin\/bun/, label: 'Bun binary path' },
33
- { pattern: /\bBun\.file\(/, label: 'Bun.file() API' },
34
- { pattern: /\bBun\.serve\(/, label: 'Bun.serve() API' },
35
- ];
36
-
37
- function truncateSnippet(str, maxLen = 200) {
38
- if (!str || str.length <= maxLen) return str || '';
39
- return str.slice(0, maxLen) + '...';
40
- }
41
-
42
- export async function checkExfilPattern(extensionFiles = []) {
43
- const signals = [];
44
- const exfilPatterns = [];
45
- const antiAnalysisTechniques = [];
46
-
47
- for (const file of extensionFiles) {
48
- const content = typeof file.content === 'string' ? file.content : '';
49
- if (!content) continue;
50
- const path = file.path || '';
51
-
52
- for (const cp of CREDENTIAL_FILE_PATTERNS) {
53
- const match = content.match(cp);
54
- if (match) {
55
- const snippet = truncateSnippet(match[0]);
56
- if (!exfilPatterns.some(e => e.includes(snippet))) {
57
- exfilPatterns.push(`${path}: ${snippet}`);
58
- signals.push({ type: 'CREDENTIAL_FILE_TARGET', pattern: cp.source, file: path });
59
- }
60
- }
61
- }
62
-
63
- for (const ep of EXFIL_CHANNEL_PATTERNS) {
64
- const match = content.match(ep);
65
- if (match) {
66
- const snippet = truncateSnippet(match[0]);
67
- exfilPatterns.push(`${path}: ${snippet}`);
68
- signals.push({ type: 'EXFIL_CHANNEL', pattern: ep.source, file: path });
69
- }
70
- }
71
-
72
- for (const ap of ANTI_ANALYSIS_PATTERNS) {
73
- if (ap.pattern.test(content)) {
74
- if (!antiAnalysisTechniques.includes(ap.label)) {
75
- antiAnalysisTechniques.push(ap.label);
76
- signals.push({ type: 'ANTI_ANALYSIS', technique: ap.label, file: path });
77
- }
78
- }
79
- }
80
- }
81
-
82
- return {
83
- triggered: signals.length > 0,
84
- signals,
85
- exfilPatterns,
86
- antiAnalysisTechniques,
87
- };
88
- }
1
+ const CREDENTIAL_FILE_PATTERNS = [
2
+ /~\/\.npmrc/,
3
+ /~\/\.gitconfig/,
4
+ /~\/\.aws\/credentials/,
5
+ /~\/\.ssh\/id_\w+/,
6
+ /~\/\.vault-token/,
7
+ /~\/\.claude\/settings\.json/,
8
+ /~\/Library\/Application\s+Support\/1Password\//,
9
+ /\/etc\/vault\/token/,
10
+ /\/proc\/\*\/mem/,
11
+ /\$GITHUB_ENV/,
12
+ /\$GITHUB_TOKEN/,
13
+ /\$NPM_TOKEN/,
14
+ /\$NODE_AUTH_TOKEN/,
15
+ /GH_TOKEN/,
16
+ ];
17
+
18
+ const EXFIL_CHANNEL_PATTERNS = [
19
+ /(?:[a-z0-9_-]{40,})\.[a-z0-9_-]+\.(?:com|io|org|net|app|dev|xyz)(?:\/[^\s"')\]]{0,50})?/i,
20
+ /\/gists\b.*authorization/i,
21
+ /\/repos\/[^/]+\/[^/]+\/git\/refs/i,
22
+ /AES-256-GCM/,
23
+ /RSA\/(?:PKCS|OAEP)/,
24
+ ];
25
+
26
+ const ANTI_ANALYSIS_PATTERNS = [
27
+ { pattern: /os\.cpus\(\)\.length\s*<\s*4/, label: 'CPU core count check (< 4)' },
28
+ { pattern: /Intl\.DateTimeFormat.*(?:timeZone|locale)/, label: 'Timezone/locale check' },
29
+ { pattern: /Intl\.DateTimeFormat.*\b(?:ru|rus|kz|by|cn|cns)\b/i, label: 'CIS/locale filtering' },
30
+ { pattern: /\bspawn\(\s*[^,]+,\s*\{[^}]*detached:\s*true\s*\}/, label: 'Detached process spawn' },
31
+ { pattern: /\bBUN_INSTALL\b/, label: 'BUN_INSTALL env reference' },
32
+ { pattern: /~\/\.bun\/bin\/bun/, label: 'Bun binary path' },
33
+ { pattern: /\bBun\.file\(/, label: 'Bun.file() API' },
34
+ { pattern: /\bBun\.serve\(/, label: 'Bun.serve() API' },
35
+ ];
36
+
37
+ function truncateSnippet(str, maxLen = 200) {
38
+ if (!str || str.length <= maxLen) return str || '';
39
+ return str.slice(0, maxLen) + '...';
40
+ }
41
+
42
+ export async function checkExfilPattern(extensionFiles = []) {
43
+ const signals = [];
44
+ const exfilPatterns = [];
45
+ const antiAnalysisTechniques = [];
46
+
47
+ for (const file of extensionFiles) {
48
+ const content = typeof file.content === 'string' ? file.content : '';
49
+ if (!content) continue;
50
+ const path = file.path || '';
51
+
52
+ for (const cp of CREDENTIAL_FILE_PATTERNS) {
53
+ const match = content.match(cp);
54
+ if (match) {
55
+ const snippet = truncateSnippet(match[0]);
56
+ if (!exfilPatterns.some(e => e.includes(snippet))) {
57
+ exfilPatterns.push(`${path}: ${snippet}`);
58
+ signals.push({ type: 'CREDENTIAL_FILE_TARGET', pattern: cp.source, file: path });
59
+ }
60
+ }
61
+ }
62
+
63
+ for (const ep of EXFIL_CHANNEL_PATTERNS) {
64
+ const match = content.match(ep);
65
+ if (match) {
66
+ const snippet = truncateSnippet(match[0]);
67
+ exfilPatterns.push(`${path}: ${snippet}`);
68
+ signals.push({ type: 'EXFIL_CHANNEL', pattern: ep.source, file: path });
69
+ }
70
+ }
71
+
72
+ for (const ap of ANTI_ANALYSIS_PATTERNS) {
73
+ if (ap.pattern.test(content)) {
74
+ if (!antiAnalysisTechniques.includes(ap.label)) {
75
+ antiAnalysisTechniques.push(ap.label);
76
+ signals.push({ type: 'ANTI_ANALYSIS', technique: ap.label, file: path });
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ return {
83
+ triggered: signals.length > 0,
84
+ signals,
85
+ exfilPatterns,
86
+ antiAnalysisTechniques,
87
+ };
88
+ }