@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,147 +1,147 @@
1
- import { MegalodonSignal } from './types.js';
2
- import yaml from 'js-yaml';
3
-
4
- const C2_EXFIL_RE = /curl\s+.*?https?:\/\/(?!github\.com|githubusercontent\.com|raw\.githubusercontent\.com)[^\s'"]+/i;
5
- const SECRETS_REF_RE = /\$\{\{?\s*secrets\.\w+/;
6
- const B64_DECODE_CHAIN_RE = /base64\s+-d\s*[|>]\s*(ba)?sh/;
7
-
8
- function isWorkflowFile(f) {
9
- const p = f.path.replace(/\\/g, '/');
10
- return /\.github\/workflows\/.+\.(yml|yaml)$/i.test(p);
11
- }
12
-
13
- function countExecutableLines(text) {
14
- return text.split('\n').filter(l => l.trim() && !l.trim().startsWith('#')).length;
15
- }
16
-
17
- function extractRunBlocks(parsed) {
18
- const runs = [];
19
- if (!parsed || typeof parsed !== 'object') return runs;
20
-
21
- const walk = (obj) => {
22
- if (!obj || typeof obj !== 'object') return;
23
- if (Array.isArray(obj)) { obj.forEach(walk); return; }
24
- for (const [k, v] of Object.entries(obj)) {
25
- if (k === 'run' && typeof v === 'string') {
26
- runs.push(v);
27
- }
28
- if (k === 'env' && typeof v === 'object' && v !== null) {
29
- runs.push({ _env: v });
30
- }
31
- walk(v);
32
- }
33
- };
34
- walk(parsed);
35
- return runs;
36
- }
37
-
38
- function extractRunBlocksRaw(text) {
39
- const runs = [];
40
- const runMatch = text.match(/run:\s*[|>]\s*\n(\s{2,}.*(?:\n\s{2,}.*)*)/g);
41
- if (runMatch) runs.push(...runMatch.map(m => m.replace(/^run:\s*[|>]\s*\n/, '')));
42
-
43
- const inlineRe = /run:\s*['"](.+?)['"]\s*$/gm;
44
- let m;
45
- while ((m = inlineRe.exec(text)) !== null) runs.push(m[1]);
46
-
47
- const envRe = /env:\s*\n((?:\s{2,}\w+:\s*.+\n?)*)/g;
48
- let em;
49
- while ((em = envRe.exec(text)) !== null) runs.push({ _env: em[1] });
50
- return runs;
51
- }
52
-
53
- function runInStepHasBoth(step, signal) {
54
- const runVal = step.run;
55
- const envVals = step.env ? Object.values(step.env).filter(v => typeof v === 'string').join(' ') : '';
56
- const combined = typeof runVal === 'string' ? `${runVal} ${envVals}` : '';
57
-
58
- if (signal === 'exfil') {
59
- return C2_EXFIL_RE.test(combined) && SECRETS_REF_RE.test(combined);
60
- }
61
- if (signal === 'decode') {
62
- return B64_DECODE_CHAIN_RE.test(combined);
63
- }
64
- return false;
65
- }
66
-
67
- export async function scan(allFiles) {
68
- const evidence = [];
69
- const workflowFiles = allFiles.filter(isWorkflowFile);
70
-
71
- for (const f of workflowFiles) {
72
- if (f.content.length > 512 * 1024) continue;
73
-
74
- let parsed = null;
75
- let parseError = null;
76
- try {
77
- parsed = yaml.load(f.content);
78
- } catch (e) {
79
- parseError = e;
80
- }
81
-
82
- const rawRunBlocks = parsed ? extractRunBlocks(parsed) : extractRunBlocksRaw(f.content);
83
- const runStrings = rawRunBlocks.filter(r => typeof r === 'string');
84
- const envBlocks = rawRunBlocks.filter(r => typeof r === 'object' && r._env);
85
-
86
- let exfilTriggered = false;
87
- let decodeTriggered = false;
88
-
89
- for (const runStr of runStrings) {
90
- if (!exfilTriggered && C2_EXFIL_RE.test(runStr) && SECRETS_REF_RE.test(runStr)) {
91
- exfilTriggered = true;
92
- evidence.push({
93
- signal: MegalodonSignal.WORKFLOW_C2_EXFIL,
94
- file: f.path,
95
- excerpt: runStr.slice(0, 120),
96
- detail: 'C2 outbound call co-occurs with credentials reference in run block',
97
- });
98
- }
99
-
100
- if (!decodeTriggered && B64_DECODE_CHAIN_RE.test(runStr)) {
101
- decodeTriggered = true;
102
- evidence.push({
103
- signal: MegalodonSignal.WORKFLOW_DECODE_CHAIN,
104
- file: f.path,
105
- excerpt: runStr.slice(0, 120),
106
- detail: 'Base64 decode pipe to shell — obfuscated payload execution',
107
- });
108
- }
109
- }
110
-
111
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
112
- const steps = parsed.jobs ? Object.values(parsed.jobs).flatMap(j => j.steps || []) : [];
113
- for (const step of steps) {
114
- if (!exfilTriggered && runInStepHasBoth(step, 'exfil')) {
115
- exfilTriggered = true;
116
- const runVal = step.run || '';
117
- evidence.push({
118
- signal: MegalodonSignal.WORKFLOW_C2_EXFIL,
119
- file: f.path,
120
- excerpt: runVal.slice(0, 120),
121
- detail: 'C2 outbound call co-occurs with secrets reference in same step',
122
- });
123
- }
124
- if (!decodeTriggered && runInStepHasBoth(step, 'decode')) {
125
- decodeTriggered = true;
126
- const runVal = step.run || '';
127
- evidence.push({
128
- signal: MegalodonSignal.WORKFLOW_DECODE_CHAIN,
129
- file: f.path,
130
- excerpt: runVal.slice(0, 120),
131
- detail: 'Base64 decode pipe to shell — obfuscated payload execution',
132
- });
133
- }
134
- }
135
- }
136
-
137
- const lineCount = countExecutableLines(f.content);
138
- if ((exfilTriggered || decodeTriggered) && lineCount >= 100 && lineCount <= 120) {
139
- const found = evidence.find(e => e.signal === MegalodonSignal.WORKFLOW_C2_EXFIL || e.signal === MegalodonSignal.WORKFLOW_DECODE_CHAIN);
140
- if (found) {
141
- found.detail += ` | Matches ${lineCount}-line Megalodon payload footprint`;
142
- }
143
- }
144
- }
145
-
146
- return evidence;
147
- }
1
+ import { MegalodonSignal } from './types.js';
2
+ import yaml from 'js-yaml';
3
+
4
+ const C2_EXFIL_RE = /curl\s+.*?https?:\/\/(?!github\.com|githubusercontent\.com|raw\.githubusercontent\.com)[^\s'"]+/i;
5
+ const SECRETS_REF_RE = /\$\{\{?\s*secrets\.\w+/;
6
+ const B64_DECODE_CHAIN_RE = /base64\s+-d\s*[|>]\s*(ba)?sh/;
7
+
8
+ function isWorkflowFile(f) {
9
+ const p = f.path.replace(/\\/g, '/');
10
+ return /\.github\/workflows\/.+\.(yml|yaml)$/i.test(p);
11
+ }
12
+
13
+ function countExecutableLines(text) {
14
+ return text.split('\n').filter(l => l.trim() && !l.trim().startsWith('#')).length;
15
+ }
16
+
17
+ function extractRunBlocks(parsed) {
18
+ const runs = [];
19
+ if (!parsed || typeof parsed !== 'object') return runs;
20
+
21
+ const walk = (obj) => {
22
+ if (!obj || typeof obj !== 'object') return;
23
+ if (Array.isArray(obj)) { obj.forEach(walk); return; }
24
+ for (const [k, v] of Object.entries(obj)) {
25
+ if (k === 'run' && typeof v === 'string') {
26
+ runs.push(v);
27
+ }
28
+ if (k === 'env' && typeof v === 'object' && v !== null) {
29
+ runs.push({ _env: v });
30
+ }
31
+ walk(v);
32
+ }
33
+ };
34
+ walk(parsed);
35
+ return runs;
36
+ }
37
+
38
+ function extractRunBlocksRaw(text) {
39
+ const runs = [];
40
+ const runMatch = text.match(/run:\s*[|>]\s*\n(\s{2,}.*(?:\n\s{2,}.*)*)/g);
41
+ if (runMatch) runs.push(...runMatch.map(m => m.replace(/^run:\s*[|>]\s*\n/, '')));
42
+
43
+ const inlineRe = /run:\s*['"](.+?)['"]\s*$/gm;
44
+ let m;
45
+ while ((m = inlineRe.exec(text)) !== null) runs.push(m[1]);
46
+
47
+ const envRe = /env:\s*\n((?:\s{2,}\w+:\s*.+\n?)*)/g;
48
+ let em;
49
+ while ((em = envRe.exec(text)) !== null) runs.push({ _env: em[1] });
50
+ return runs;
51
+ }
52
+
53
+ function runInStepHasBoth(step, signal) {
54
+ const runVal = step.run;
55
+ const envVals = step.env ? Object.values(step.env).filter(v => typeof v === 'string').join(' ') : '';
56
+ const combined = typeof runVal === 'string' ? `${runVal} ${envVals}` : '';
57
+
58
+ if (signal === 'exfil') {
59
+ return C2_EXFIL_RE.test(combined) && SECRETS_REF_RE.test(combined);
60
+ }
61
+ if (signal === 'decode') {
62
+ return B64_DECODE_CHAIN_RE.test(combined);
63
+ }
64
+ return false;
65
+ }
66
+
67
+ export async function scan(allFiles) {
68
+ const evidence = [];
69
+ const workflowFiles = allFiles.filter(isWorkflowFile);
70
+
71
+ for (const f of workflowFiles) {
72
+ if (f.content.length > 512 * 1024) continue;
73
+
74
+ let parsed = null;
75
+ let parseError = null;
76
+ try {
77
+ parsed = yaml.load(f.content);
78
+ } catch (e) {
79
+ parseError = e;
80
+ }
81
+
82
+ const rawRunBlocks = parsed ? extractRunBlocks(parsed) : extractRunBlocksRaw(f.content);
83
+ const runStrings = rawRunBlocks.filter(r => typeof r === 'string');
84
+ const envBlocks = rawRunBlocks.filter(r => typeof r === 'object' && r._env);
85
+
86
+ let exfilTriggered = false;
87
+ let decodeTriggered = false;
88
+
89
+ for (const runStr of runStrings) {
90
+ if (!exfilTriggered && C2_EXFIL_RE.test(runStr) && SECRETS_REF_RE.test(runStr)) {
91
+ exfilTriggered = true;
92
+ evidence.push({
93
+ signal: MegalodonSignal.WORKFLOW_C2_EXFIL,
94
+ file: f.path,
95
+ excerpt: runStr.slice(0, 120),
96
+ detail: 'C2 outbound call co-occurs with credentials reference in run block',
97
+ });
98
+ }
99
+
100
+ if (!decodeTriggered && B64_DECODE_CHAIN_RE.test(runStr)) {
101
+ decodeTriggered = true;
102
+ evidence.push({
103
+ signal: MegalodonSignal.WORKFLOW_DECODE_CHAIN,
104
+ file: f.path,
105
+ excerpt: runStr.slice(0, 120),
106
+ detail: 'Base64 decode pipe to shell — obfuscated payload execution',
107
+ });
108
+ }
109
+ }
110
+
111
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
112
+ const steps = parsed.jobs ? Object.values(parsed.jobs).flatMap(j => j.steps || []) : [];
113
+ for (const step of steps) {
114
+ if (!exfilTriggered && runInStepHasBoth(step, 'exfil')) {
115
+ exfilTriggered = true;
116
+ const runVal = step.run || '';
117
+ evidence.push({
118
+ signal: MegalodonSignal.WORKFLOW_C2_EXFIL,
119
+ file: f.path,
120
+ excerpt: runVal.slice(0, 120),
121
+ detail: 'C2 outbound call co-occurs with secrets reference in same step',
122
+ });
123
+ }
124
+ if (!decodeTriggered && runInStepHasBoth(step, 'decode')) {
125
+ decodeTriggered = true;
126
+ const runVal = step.run || '';
127
+ evidence.push({
128
+ signal: MegalodonSignal.WORKFLOW_DECODE_CHAIN,
129
+ file: f.path,
130
+ excerpt: runVal.slice(0, 120),
131
+ detail: 'Base64 decode pipe to shell — obfuscated payload execution',
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ const lineCount = countExecutableLines(f.content);
138
+ if ((exfilTriggered || decodeTriggered) && lineCount >= 100 && lineCount <= 120) {
139
+ const found = evidence.find(e => e.signal === MegalodonSignal.WORKFLOW_C2_EXFIL || e.signal === MegalodonSignal.WORKFLOW_DECODE_CHAIN);
140
+ if (found) {
141
+ found.detail += ` | Matches ${lineCount}-line Megalodon payload footprint`;
142
+ }
143
+ }
144
+ }
145
+
146
+ return evidence;
147
+ }
@@ -1,61 +1,61 @@
1
- import { MegalodonSignal } from './types.js';
2
-
3
- const CRED_PATTERNS = [
4
- { pattern: /\bAWS_(SECRET_ACCESS_KEY|ACCESS_KEY_ID|SESSION_TOKEN)\b/, label: 'AWS credential' },
5
- { pattern: /\bGOOGLE_APPLICATION_CREDENTIALS\b/, label: 'GCP credential' },
6
- { pattern: /\bAZURE_(CLIENT_SECRET|TENANT_ID|CLIENT_ID|SUBSCRIPTION_ID)\b/, label: 'Azure credential' },
7
- { pattern: /\bGH_(TOKEN|PAT)\b/, label: 'GitHub PAT' },
8
- { pattern: /\bGITHUB_TOKEN\b/, label: 'GitHub token' },
9
- { pattern: /\bNPM_TOKEN\b/, label: 'npm token' },
10
- { pattern: /\bDISCORD_TOKEN\b/, label: 'Discord token' },
11
- { pattern: /\bSLACK_TOKEN\b/, label: 'Slack token' },
12
- { pattern: /\bSTRIPE_(SECRET|PUBLISHABLE)_KEY\b/, label: 'Stripe key' },
13
- { pattern: /\bTWILIO_(ACCOUNT_SID|AUTH_TOKEN)\b/, label: 'Twilio credential' },
14
- { pattern: /\bDB_(USERNAME|PASSWORD|URL)\b/, label: 'Database credential' },
15
- { pattern: /\bMONGO_(URI|URL|CONNECTION)\b/, label: 'MongoDB connection' },
16
- ];
17
-
18
- const OUTBOUND_NET_RE = /curl\s+|wget\s+|fetch\s*\(|https?\.request\s*\(|http\.request\s*\(|got\s*\(|axios\s*\.|request\s*\(|node-fetch|\.post\s*\(|\.get\s*\(/i;
19
-
20
- const TARGET_EXTENSIONS = ['.sh', '.bash', '.yml', '.yaml', '.js'];
21
-
22
- function isTargetFile(f) {
23
- const ext = f.path.slice(f.path.lastIndexOf('.')).toLowerCase();
24
- return TARGET_EXTENSIONS.includes(ext);
25
- }
26
-
27
- export async function scan(allFiles) {
28
- const evidence = [];
29
- const targetFiles = allFiles.filter(isTargetFile);
30
-
31
- for (const f of targetFiles) {
32
- const content = f.content;
33
- let score = 0;
34
- const matched = [];
35
-
36
- for (const cp of CRED_PATTERNS) {
37
- const re = new RegExp(cp.pattern.source, 'gi');
38
- let m;
39
- while ((m = re.exec(content)) !== null) {
40
- if (!matched.some(ex => ex.label === cp.label)) {
41
- matched.push({ label: cp.label, match: m[0] });
42
- }
43
- score += 3;
44
- }
45
- }
46
-
47
- if (score > 0) {
48
- const hasNetwork = OUTBOUND_NET_RE.test(content);
49
- if (hasNetwork) {
50
- evidence.push({
51
- signal: MegalodonSignal.CREDENTIAL_HARVEST,
52
- file: f.path,
53
- excerpt: matched.map(m => m.label).join(', ').slice(0, 120),
54
- detail: `Credential env vars (${matched.map(m => m.label).join(', ')}) co-occur with outbound network call (score: ${score})`,
55
- });
56
- }
57
- }
58
- }
59
-
60
- return evidence;
61
- }
1
+ import { MegalodonSignal } from './types.js';
2
+
3
+ const CRED_PATTERNS = [
4
+ { pattern: /\bAWS_(SECRET_ACCESS_KEY|ACCESS_KEY_ID|SESSION_TOKEN)\b/, label: 'AWS credential' },
5
+ { pattern: /\bGOOGLE_APPLICATION_CREDENTIALS\b/, label: 'GCP credential' },
6
+ { pattern: /\bAZURE_(CLIENT_SECRET|TENANT_ID|CLIENT_ID|SUBSCRIPTION_ID)\b/, label: 'Azure credential' },
7
+ { pattern: /\bGH_(TOKEN|PAT)\b/, label: 'GitHub PAT' },
8
+ { pattern: /\bGITHUB_TOKEN\b/, label: 'GitHub token' },
9
+ { pattern: /\bNPM_TOKEN\b/, label: 'npm token' },
10
+ { pattern: /\bDISCORD_TOKEN\b/, label: 'Discord token' },
11
+ { pattern: /\bSLACK_TOKEN\b/, label: 'Slack token' },
12
+ { pattern: /\bSTRIPE_(SECRET|PUBLISHABLE)_KEY\b/, label: 'Stripe key' },
13
+ { pattern: /\bTWILIO_(ACCOUNT_SID|AUTH_TOKEN)\b/, label: 'Twilio credential' },
14
+ { pattern: /\bDB_(USERNAME|PASSWORD|URL)\b/, label: 'Database credential' },
15
+ { pattern: /\bMONGO_(URI|URL|CONNECTION)\b/, label: 'MongoDB connection' },
16
+ ];
17
+
18
+ const OUTBOUND_NET_RE = /curl\s+|wget\s+|fetch\s*\(|https?\.request\s*\(|http\.request\s*\(|got\s*\(|axios\s*\.|request\s*\(|node-fetch|\.post\s*\(|\.get\s*\(/i;
19
+
20
+ const TARGET_EXTENSIONS = ['.sh', '.bash', '.yml', '.yaml', '.js'];
21
+
22
+ function isTargetFile(f) {
23
+ const ext = f.path.slice(f.path.lastIndexOf('.')).toLowerCase();
24
+ return TARGET_EXTENSIONS.includes(ext);
25
+ }
26
+
27
+ export async function scan(allFiles) {
28
+ const evidence = [];
29
+ const targetFiles = allFiles.filter(isTargetFile);
30
+
31
+ for (const f of targetFiles) {
32
+ const content = f.content;
33
+ let score = 0;
34
+ const matched = [];
35
+
36
+ for (const cp of CRED_PATTERNS) {
37
+ const re = new RegExp(cp.pattern.source, 'gi');
38
+ let m;
39
+ while ((m = re.exec(content)) !== null) {
40
+ if (!matched.some(ex => ex.label === cp.label)) {
41
+ matched.push({ label: cp.label, match: m[0] });
42
+ }
43
+ score += 3;
44
+ }
45
+ }
46
+
47
+ if (score > 0) {
48
+ const hasNetwork = OUTBOUND_NET_RE.test(content);
49
+ if (hasNetwork) {
50
+ evidence.push({
51
+ signal: MegalodonSignal.CREDENTIAL_HARVEST,
52
+ file: f.path,
53
+ excerpt: matched.map(m => m.label).join(', ').slice(0, 120),
54
+ detail: `Credential env vars (${matched.map(m => m.label).join(', ')}) co-occur with outbound network call (score: ${score})`,
55
+ });
56
+ }
57
+ }
58
+ }
59
+
60
+ return evidence;
61
+ }
@@ -1,67 +1,67 @@
1
- import { MegalodonSignal } from './types.js';
2
-
3
- export function detectVelocitySpike(times, windowHours = 6, threshold = 3) {
4
- const filtered = {};
5
- for (const [v, t] of Object.entries(times)) {
6
- if (v === 'created' || v === 'modified') continue;
7
- filtered[v] = t;
8
- }
9
-
10
- const entries = Object.entries(filtered)
11
- .filter(([, t]) => t)
12
- .map(([v, t]) => [v, new Date(t).getTime()])
13
- .filter(([, ts]) => !Number.isNaN(ts))
14
- .sort((a, b) => a[1] - b[1]);
15
-
16
- if (entries.length === 0) {
17
- return { triggered: false, versionsInWindow: [], windowStartISO: null };
18
- }
19
-
20
- const windowMs = windowHours * 3_600_000;
21
-
22
- for (let i = 0; i < entries.length; i++) {
23
- const windowStart = entries[i][1];
24
- const windowEnd = windowStart + windowMs;
25
- const inWindow = [];
26
-
27
- for (let j = i; j < entries.length; j++) {
28
- if (entries[j][1] <= windowEnd) {
29
- inWindow.push(entries[j][0]);
30
- } else {
31
- break;
32
- }
33
- }
34
-
35
- if (inWindow.length >= threshold) {
36
- let display = inWindow.slice(0, 10);
37
- let suffix = '';
38
- if (inWindow.length > 10) {
39
- suffix = ` +${inWindow.length - 10} more`;
40
- }
41
- return {
42
- triggered: true,
43
- versionsInWindow: display.join(', ') + suffix,
44
- windowStartISO: new Date(windowStart).toISOString(),
45
- _allVersions: inWindow,
46
- };
47
- }
48
- }
49
-
50
- return { triggered: false, versionsInWindow: [], windowStartISO: null };
51
- }
52
-
53
- export async function scan(registryMeta) {
54
- const times = registryMeta?.time || {};
55
- const result = detectVelocitySpike(times);
56
-
57
- if (!result.triggered) return [];
58
-
59
- return [{
60
- signal: MegalodonSignal.PUBLISH_VELOCITY,
61
- file: 'registry.npmjs.org',
62
- excerpt: result.versionsInWindow,
63
- detail: `Version publish velocity spike: ${result.versionsInWindow} versions in window starting ${result.windowStartISO}`,
64
- _windowStartISO: result.windowStartISO,
65
- _allVersions: result._allVersions,
66
- }];
67
- }
1
+ import { MegalodonSignal } from './types.js';
2
+
3
+ export function detectVelocitySpike(times, windowHours = 6, threshold = 3) {
4
+ const filtered = {};
5
+ for (const [v, t] of Object.entries(times)) {
6
+ if (v === 'created' || v === 'modified') continue;
7
+ filtered[v] = t;
8
+ }
9
+
10
+ const entries = Object.entries(filtered)
11
+ .filter(([, t]) => t)
12
+ .map(([v, t]) => [v, new Date(t).getTime()])
13
+ .filter(([, ts]) => !Number.isNaN(ts))
14
+ .sort((a, b) => a[1] - b[1]);
15
+
16
+ if (entries.length === 0) {
17
+ return { triggered: false, versionsInWindow: [], windowStartISO: null };
18
+ }
19
+
20
+ const windowMs = windowHours * 3_600_000;
21
+
22
+ for (let i = 0; i < entries.length; i++) {
23
+ const windowStart = entries[i][1];
24
+ const windowEnd = windowStart + windowMs;
25
+ const inWindow = [];
26
+
27
+ for (let j = i; j < entries.length; j++) {
28
+ if (entries[j][1] <= windowEnd) {
29
+ inWindow.push(entries[j][0]);
30
+ } else {
31
+ break;
32
+ }
33
+ }
34
+
35
+ if (inWindow.length >= threshold) {
36
+ let display = inWindow.slice(0, 10);
37
+ let suffix = '';
38
+ if (inWindow.length > 10) {
39
+ suffix = ` +${inWindow.length - 10} more`;
40
+ }
41
+ return {
42
+ triggered: true,
43
+ versionsInWindow: display.join(', ') + suffix,
44
+ windowStartISO: new Date(windowStart).toISOString(),
45
+ _allVersions: inWindow,
46
+ };
47
+ }
48
+ }
49
+
50
+ return { triggered: false, versionsInWindow: [], windowStartISO: null };
51
+ }
52
+
53
+ export async function scan(registryMeta) {
54
+ const times = registryMeta?.time || {};
55
+ const result = detectVelocitySpike(times);
56
+
57
+ if (!result.triggered) return [];
58
+
59
+ return [{
60
+ signal: MegalodonSignal.PUBLISH_VELOCITY,
61
+ file: 'registry.npmjs.org',
62
+ excerpt: result.versionsInWindow,
63
+ detail: `Version publish velocity spike: ${result.versionsInWindow} versions in window starting ${result.windowStartISO}`,
64
+ _windowStartISO: result.windowStartISO,
65
+ _allVersions: result._allVersions,
66
+ }];
67
+ }