@lateos/npm-scan 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +864 -861
  2. package/backend/cra.js +113 -21
  3. package/backend/db.js +18 -10
  4. package/backend/detectors/atk-001-lifecycle.js +5 -5
  5. package/backend/detectors/atk-002-obfusc.js +126 -47
  6. package/backend/detectors/atk-003-creds.js +8 -4
  7. package/backend/detectors/atk-004-persist.js +3 -3
  8. package/backend/detectors/atk-005-exfil.js +8 -4
  9. package/backend/detectors/atk-006-depconf.js +3 -3
  10. package/backend/detectors/atk-007-typosquat.js +64 -10
  11. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  12. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  13. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  14. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  15. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  16. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  17. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  18. package/backend/detectors/axios-poisoning/index.js +77 -60
  19. package/backend/detectors/config/thresholds.js +48 -3
  20. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  21. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  22. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  23. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  24. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  25. package/backend/detectors/hf-impersonation/index.js +94 -31
  26. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  27. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  28. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  29. package/backend/detectors/index.js +181 -34
  30. package/backend/detectors/lib/ast-patterns.js +4 -1
  31. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  32. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  33. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  34. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  35. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  36. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  37. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  38. package/backend/detectors/megalodon/index.js +35 -25
  39. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  40. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  41. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  42. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  43. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  44. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  45. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  46. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  47. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  48. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  49. package/backend/detectors/msh-supplement/index.js +78 -63
  50. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  51. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  52. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  53. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  54. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  55. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  56. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  57. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  58. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  59. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  60. package/backend/detectors/tier1-binary-embed.js +109 -41
  61. package/backend/detectors/tier1-cloud-imds.js +57 -37
  62. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  63. package/backend/detectors/tier1-infostealer.js +121 -68
  64. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  65. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  66. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  67. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  68. package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
  69. package/backend/detectors/tier1-self-propagation.js +115 -0
  70. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  71. package/backend/detectors/tier1-transitive-deps.js +182 -0
  72. package/backend/detectors/tier1-typosquat.js +129 -50
  73. package/backend/detectors/tier1-version-anomaly.js +77 -41
  74. package/backend/detectors/tier1-version-confusion.js +79 -59
  75. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  76. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  77. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  78. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  79. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  80. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  81. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  82. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  83. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  84. package/backend/detectors/trapdoor/index.js +19 -14
  85. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  86. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  87. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  88. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  89. package/backend/detectors.test.js +78 -19
  90. package/backend/fetch.js +37 -29
  91. package/backend/index.js +1 -1
  92. package/backend/license.js +20 -4
  93. package/backend/lockfile.js +60 -36
  94. package/backend/pdf.js +107 -28
  95. package/backend/policy.js +183 -56
  96. package/backend/provenance.js +28 -3
  97. package/backend/report.js +136 -70
  98. package/backend/sbom.js +33 -27
  99. package/backend/scripts/analyze-false-positives.js +14 -8
  100. package/backend/scripts/analyze-validation.js +27 -21
  101. package/backend/scripts/detect-false-positives.js +20 -10
  102. package/backend/scripts/fetch-top-packages.js +197 -49
  103. package/backend/scripts/validate-d10-d13.js +103 -0
  104. package/backend/scripts/validate-detectors.js +26 -17
  105. package/backend/siem/cef.js +23 -21
  106. package/backend/siem/ecs.js +3 -3
  107. package/backend/siem/index.js +1 -1
  108. package/backend/siem/qradar.js +3 -3
  109. package/backend/siem/sentinel.js +2 -2
  110. package/backend/tests-d5-enhanced.test.js +13 -12
  111. package/backend/tests-d6-version-anomaly.test.js +17 -8
  112. package/backend/tests-d6.test.js +24 -14
  113. package/backend/tests-d6c.test.js +27 -14
  114. package/backend/tests-d7-obfuscation.test.js +9 -12
  115. package/backend/tests.test.js +182 -83
  116. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  117. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  118. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  119. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  120. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  121. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  122. package/backend/vsix-scan/index.js +97 -41
  123. package/backend/vsix-scan/marketplace-client.js +29 -13
  124. package/cli/cli.js +154 -64
  125. package/package.json +12 -3
@@ -1,68 +1,100 @@
1
1
  import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
2
 
3
3
  const INTERNAL_SUFFIX_RE = /\.(?:internal|local|corp|intra|priv|lan)(?:[.:/]|$)/i;
4
- const CORPORATE_RE = /(?:github-ent|jira-ent|github\.enterprise|internal-gitlab|gitlab\.internal|jenkins\.internal|confluence\.internal)/i;
5
- const PRIVATE_IP_RE = /^(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})/;
4
+ const CORPORATE_RE =
5
+ /(?:github-ent|jira-ent|github\.enterprise|internal-gitlab|gitlab\.internal|jenkins\.internal|confluence\.internal)/i;
6
+ const PRIVATE_IP_RE =
7
+ /^(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})/;
6
8
 
7
9
  function extractDomain(url) {
8
10
  try {
9
11
  const u = new URL(url);
10
12
  return u.hostname;
11
13
  } catch {
12
- const m = url.match(/^(?:https?:\/\/)?([^\/\s:]+)/);
14
+ const m = url.match(/^(?:https?:\/\/)?([^/\s:]+)/);
13
15
  return m ? m[1] : null;
14
16
  }
15
17
  }
16
18
 
17
19
  function isInternalUrl(url) {
18
- if (!url) return false;
20
+ if (!url) {
21
+ return false;
22
+ }
19
23
  const domain = extractDomain(url);
20
- if (!domain) return false;
21
- if (INTERNAL_SUFFIX_RE.test(domain)) return true;
22
- if (CORPORATE_RE.test(domain)) return true;
23
- if (PRIVATE_IP_RE.test(domain)) return true;
24
+ if (!domain) {
25
+ return false;
26
+ }
27
+ if (INTERNAL_SUFFIX_RE.test(domain)) {
28
+ return true;
29
+ }
30
+ if (CORPORATE_RE.test(domain)) {
31
+ return true;
32
+ }
33
+ if (PRIVATE_IP_RE.test(domain)) {
34
+ return true;
35
+ }
24
36
  return false;
25
37
  }
26
38
 
27
39
  function parseSemver(version) {
28
- if (!version) return null;
40
+ if (!version) {
41
+ return null;
42
+ }
29
43
  const parts = version.replace(/^[~^]/, '').split('.');
30
44
  const m = parseInt(parts[0], 10);
31
45
  const n = parseInt(parts[1], 10);
32
46
  const p = parseInt(parts[2], 10);
33
- if (isNaN(m) || isNaN(n) || isNaN(p)) return null;
47
+ if (isNaN(m) || isNaN(n) || isNaN(p)) {
48
+ return null;
49
+ }
34
50
  return { major: m, minor: n, patch: p };
35
51
  }
36
52
 
37
53
  function detectSemverInflation(currentVer, registryMeta) {
38
- if (!currentVer || !registryMeta) return null;
54
+ if (!currentVer || !registryMeta) {
55
+ return null;
56
+ }
39
57
 
40
58
  const age = registryMeta.age;
41
- if (age !== undefined && age < 7) return null;
59
+ if (age !== undefined && age < 7) {
60
+ return null;
61
+ }
42
62
 
43
63
  const previousVer = registryMeta.previousVersion || null;
44
- if (!previousVer) return null;
64
+ if (!previousVer) {
65
+ return null;
66
+ }
45
67
 
46
68
  const cur = parseSemver(currentVer);
47
69
  const prev = parseSemver(previousVer);
48
- if (!cur || !prev) return null;
70
+ if (!cur || !prev) {
71
+ return null;
72
+ }
49
73
 
50
74
  const majorJump = cur.major - prev.major;
51
75
  const minorJump = cur.minor - prev.minor;
52
76
  const patchJump = cur.patch - prev.patch;
53
77
 
54
- if (majorJump > 10) return { type: 'major', from: previousVer, to: currentVer, jump: majorJump };
55
- if (minorJump > 20) return { type: 'minor', from: previousVer, to: currentVer, jump: minorJump };
56
- if (patchJump > 50) return { type: 'patch', from: previousVer, to: currentVer, jump: patchJump };
78
+ if (majorJump > 10) {
79
+ return { type: 'major', from: previousVer, to: currentVer, jump: majorJump };
80
+ }
81
+ if (minorJump > 20) {
82
+ return { type: 'minor', from: previousVer, to: currentVer, jump: minorJump };
83
+ }
84
+ if (patchJump > 50) {
85
+ return { type: 'patch', from: previousVer, to: currentVer, jump: patchJump };
86
+ }
57
87
 
58
88
  return null;
59
89
  }
60
90
 
61
91
  export const name = 'tier1-metadata-spoof';
62
92
 
63
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
93
+ export async function scan(pkgJson, jsFiles, registryMeta, _allFiles) {
64
94
  const pkgName = pkgJson?.name;
65
- if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
95
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
96
+ return [];
97
+ }
66
98
 
67
99
  const fieldUrls = [];
68
100
 
@@ -87,7 +119,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
87
119
  if (pkgJson.funding) {
88
120
  const arr = Array.isArray(pkgJson.funding) ? pkgJson.funding : [pkgJson.funding];
89
121
  for (let i = 0; i < arr.length; i++) {
90
- if (arr[i] && arr[i].url) addField(`funding[${i}].url`, arr[i].url);
122
+ if (arr[i] && arr[i].url) {
123
+ addField(`funding[${i}].url`, arr[i].url);
124
+ }
91
125
  }
92
126
  }
93
127
 
@@ -95,13 +129,15 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
95
129
  addField('author.url', pkgJson.author.url);
96
130
  }
97
131
 
98
- const internalFields = fieldUrls.filter(f => isInternalUrl(f.value));
132
+ const internalFields = fieldUrls.filter((f) => isInternalUrl(f.value));
99
133
  const hasInternalUrls = internalFields.length > 0;
100
134
 
101
135
  const currentVersion = pkgJson?.version;
102
136
  const semverInflation = detectSemverInflation(currentVersion, registryMeta);
103
137
 
104
- if (!hasInternalUrls && !semverInflation) return [];
138
+ if (!hasInternalUrls && !semverInflation) {
139
+ return [];
140
+ }
105
141
 
106
142
  let baseScore = 0;
107
143
  let subtype = '';
@@ -117,10 +153,14 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
117
153
  const domain = extractDomain(f.value);
118
154
  evidence.push(`url: ${f.field} = ${f.value}`);
119
155
 
120
- let pattern = '';
121
- if (PRIVATE_IP_RE.test(domain)) pattern = 'private IP';
122
- else if (CORPORATE_RE.test(domain)) pattern = 'corporate domain';
123
- else pattern = 'internal domain';
156
+ let pattern;
157
+ if (PRIVATE_IP_RE.test(domain)) {
158
+ pattern = 'private IP';
159
+ } else if (CORPORATE_RE.test(domain)) {
160
+ pattern = 'corporate domain';
161
+ } else {
162
+ pattern = 'internal domain';
163
+ }
124
164
  evidence.push(`pattern: ${domain} (${pattern})`);
125
165
 
126
166
  locations.push({ field: f.field, value: f.value });
@@ -140,7 +180,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
140
180
  if (hasInternalUrls) {
141
181
  baseScore = Math.round(baseScore * 1.3);
142
182
  evidence.push(semverMsg);
143
- evidence.push(`${semverInflation.type} version jump (${semverInflation.jump}) without changelog`);
183
+ evidence.push(
184
+ `${semverInflation.type} version jump (${semverInflation.jump}) without changelog`
185
+ );
144
186
  locations.push({ field: 'version', old: semverInflation.from, new: semverInflation.to });
145
187
  primaryMessage += ' + unjustified semver jump';
146
188
  } else {
@@ -155,26 +197,34 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
155
197
  const confidenceScore = Math.max(50, Math.min(90, baseScore));
156
198
 
157
199
  function severityLabel(sc) {
158
- if (sc >= 70) return 'high';
200
+ if (sc >= 70) {
201
+ return 'high';
202
+ }
159
203
  return 'medium';
160
204
  }
161
205
 
162
206
  function confidenceLabel(sc) {
163
- if (sc >= 80) return 'HIGH';
164
- if (sc >= 60) return 'MEDIUM';
207
+ if (sc >= 80) {
208
+ return 'HIGH';
209
+ }
210
+ if (sc >= 60) {
211
+ return 'MEDIUM';
212
+ }
165
213
  return 'LOW';
166
214
  }
167
215
 
168
- return [{
169
- detector: 'tier1-metadata-spoof',
170
- id: 'TIER1-METADATA-SPOOF',
171
- severity: severityLabel(confidenceScore),
172
- confidence: confidenceLabel(confidenceScore),
173
- confidenceScore,
174
- subtype,
175
- message: primaryMessage,
176
- evidence,
177
- locations,
178
- reference: 'Campaign 1',
179
- }];
216
+ return [
217
+ {
218
+ detector: 'tier1-metadata-spoof',
219
+ id: 'TIER1-METADATA-SPOOF',
220
+ severity: severityLabel(confidenceScore),
221
+ confidence: confidenceLabel(confidenceScore),
222
+ confidenceScore,
223
+ subtype,
224
+ message: primaryMessage,
225
+ evidence,
226
+ locations,
227
+ reference: 'Campaign 1',
228
+ },
229
+ ];
180
230
  }
@@ -1,34 +1,51 @@
1
1
  const SCAN_HOOKS = ['preinstall', 'install', 'postinstall', 'prepare'];
2
2
 
3
- const REMOTE_FETCH_RE = /\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
3
+ const REMOTE_FETCH_RE =
4
+ /\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
4
5
  const BINARY_EXEC_RE = /\b(?:execFile|execFileSync|execSync|exec|spawnSync|spawn)\s*\(/;
5
6
  const DETACHED_RE = /detached\s*:\s*true/;
6
7
 
7
8
  function severityLabel(score) {
8
- if (score >= 95) return 'critical';
9
- if (score >= 80) return 'high';
10
- if (score >= 60) return 'medium';
9
+ if (score >= 95) {
10
+ return 'critical';
11
+ }
12
+ if (score >= 80) {
13
+ return 'high';
14
+ }
15
+ if (score >= 60) {
16
+ return 'medium';
17
+ }
11
18
  return 'low';
12
19
  }
13
20
 
14
21
  function confidenceLabel(score) {
15
- if (score >= 95) return 'CRITICAL';
16
- if (score >= 80) return 'HIGH';
17
- if (score >= 60) return 'MEDIUM';
22
+ if (score >= 95) {
23
+ return 'CRITICAL';
24
+ }
25
+ if (score >= 80) {
26
+ return 'HIGH';
27
+ }
28
+ if (score >= 60) {
29
+ return 'MEDIUM';
30
+ }
18
31
  return 'LOW';
19
32
  }
20
33
 
21
34
  export const name = 'tier1-multistage-postinstall';
22
35
 
23
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
36
+ export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
24
37
  const scripts = pkgJson?.scripts;
25
- if (!scripts || typeof scripts !== 'object') return [];
38
+ if (!scripts || typeof scripts !== 'object') {
39
+ return [];
40
+ }
26
41
 
27
42
  const findings = [];
28
43
 
29
44
  for (const hookName of SCAN_HOOKS) {
30
45
  const content = scripts[hookName];
31
- if (!content || typeof content !== 'string') continue;
46
+ if (!content || typeof content !== 'string') {
47
+ continue;
48
+ }
32
49
 
33
50
  const hasRemoteFetch = REMOTE_FETCH_RE.test(content);
34
51
  const hasBinaryExec = BINARY_EXEC_RE.test(content);
@@ -37,7 +54,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
37
54
  const signalA = hasRemoteFetch && hasBinaryExec;
38
55
  const signalB = hasDetached;
39
56
 
40
- if (!signalA && !signalB) continue;
57
+ if (!signalA && !signalB) {
58
+ continue;
59
+ }
41
60
 
42
61
  let confidenceScore;
43
62
  let subtype;
@@ -54,9 +73,15 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
54
73
  }
55
74
 
56
75
  const evidence = [`hook: ${hookName}`];
57
- if (hasRemoteFetch) evidence.push('pattern: remote fetch call');
58
- if (hasBinaryExec) evidence.push('pattern: binary execution call');
59
- if (hasDetached) evidence.push('pattern: detached background process');
76
+ if (hasRemoteFetch) {
77
+ evidence.push('pattern: remote fetch call');
78
+ }
79
+ if (hasBinaryExec) {
80
+ evidence.push('pattern: binary execution call');
81
+ }
82
+ if (hasDetached) {
83
+ evidence.push('pattern: detached background process');
84
+ }
60
85
 
61
86
  findings.push({
62
87
  detector: 'tier1-multistage-postinstall',
@@ -67,11 +92,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
67
92
  subtype,
68
93
  message: `Multi-stage install hook detected in "${hookName}" — ${subtype}`,
69
94
  evidence,
70
- locations: [{
71
- file: 'package.json',
72
- field: `scripts.${hookName}`,
73
- value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
74
- }],
95
+ locations: [
96
+ {
97
+ file: 'package.json',
98
+ field: `scripts.${hookName}`,
99
+ value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
100
+ },
101
+ ],
75
102
  crossFiles: [],
76
103
  reference: 'Sonatype-2026-003429',
77
104
  });
@@ -6,8 +6,10 @@ const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
6
6
  const ENTROPY_THRESHOLD = 5.3;
7
7
  const PAYLOAD_SIZE_THRESHOLD = 100000;
8
8
 
9
- function analyze(code, label) {
10
- if (!code || code.length < 20) return null;
9
+ function analyze(code, _label) {
10
+ if (!code || code.length < 20) {
11
+ return null;
12
+ }
11
13
 
12
14
  const entropy = shannonEntropy(code);
13
15
  const patterns = detectPatterns(code);
@@ -15,7 +17,7 @@ function analyze(code, label) {
15
17
  const payloadSize = code.length;
16
18
 
17
19
  let score = 0;
18
- let flags = [];
20
+ const flags = [];
19
21
 
20
22
  if (entropy > ENTROPY_THRESHOLD) {
21
23
  score += 35;
@@ -43,7 +45,9 @@ function analyze(code, label) {
43
45
 
44
46
  if (payloadSize > PAYLOAD_SIZE_THRESHOLD) {
45
47
  score += 15;
46
- flags.push(`Payload size ${payloadSize} bytes exceeds ${PAYLOAD_SIZE_THRESHOLD} byte threshold`);
48
+ flags.push(
49
+ `Payload size ${payloadSize} bytes exceeds ${PAYLOAD_SIZE_THRESHOLD} byte threshold`
50
+ );
47
51
  }
48
52
 
49
53
  if (patterns.includes('XOR_CIPHER') && patterns.length >= 2) {
@@ -58,7 +62,9 @@ function analyze(code, label) {
58
62
 
59
63
  score = Math.max(0, Math.min(100, score));
60
64
 
61
- if (score < 40) return null;
65
+ if (score < 40) {
66
+ return null;
67
+ }
62
68
 
63
69
  return {
64
70
  flagged: true,
@@ -73,31 +79,47 @@ function analyze(code, label) {
73
79
  }
74
80
 
75
81
  function severityLabel(sc) {
76
- if (sc >= 90) return 'critical';
77
- if (sc >= 70) return 'high';
78
- if (sc >= 50) return 'medium';
82
+ if (sc >= 90) {
83
+ return 'critical';
84
+ }
85
+ if (sc >= 70) {
86
+ return 'high';
87
+ }
88
+ if (sc >= 50) {
89
+ return 'medium';
90
+ }
79
91
  return 'low';
80
92
  }
81
93
 
82
94
  function confidenceLabel(sc) {
83
- if (sc >= 80) return 'HIGH';
84
- if (sc >= 60) return 'MEDIUM';
95
+ if (sc >= 80) {
96
+ return 'HIGH';
97
+ }
98
+ if (sc >= 60) {
99
+ return 'MEDIUM';
100
+ }
85
101
  return 'LOW';
86
102
  }
87
103
 
88
104
  export const name = 'tier1-obfuscation-heuristics';
89
105
 
90
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
106
+ export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
91
107
  const pkgName = pkgJson?.name;
92
- if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
108
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
109
+ return [];
110
+ }
93
111
 
94
112
  const findings = [];
95
113
  const scripts = pkgJson?.scripts || {};
96
114
 
97
115
  for (const [hookName, scriptContent] of Object.entries(scripts)) {
98
- if (!LIFECYCLE_SCRIPTS.includes(hookName)) continue;
116
+ if (!LIFECYCLE_SCRIPTS.includes(hookName)) {
117
+ continue;
118
+ }
99
119
  const result = analyze(scriptContent, hookName);
100
- if (!result) continue;
120
+ if (!result) {
121
+ continue;
122
+ }
101
123
 
102
124
  findings.push({
103
125
  detector: 'tier1-obfuscation-heuristics',
@@ -123,12 +145,18 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
123
145
 
124
146
  for (const f of jsFiles || []) {
125
147
  const content = f.content || '';
126
- if (content.length < 100) continue;
148
+ if (content.length < 100) {
149
+ continue;
150
+ }
127
151
 
128
152
  const result = analyze(content, f.path || 'unknown.js');
129
- if (!result) continue;
153
+ if (!result) {
154
+ continue;
155
+ }
130
156
 
131
- if (result.confidenceScore < 50) continue;
157
+ if (result.confidenceScore < 50) {
158
+ continue;
159
+ }
132
160
 
133
161
  findings.push({
134
162
  detector: 'tier1-obfuscation-heuristics',
@@ -0,0 +1,115 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+
3
+ const THRESHOLDS = {
4
+ flag_threshold: 75,
5
+ warn_threshold: 60,
6
+ burst_window_minutes: 60,
7
+ min_packages_burst: 3,
8
+ identical_payload_weight: 40,
9
+ };
10
+
11
+ function parseTimeStamps(registryMeta) {
12
+ const timeData = registryMeta?.time;
13
+ if (!timeData || typeof timeData !== 'object') return [];
14
+ return Object.entries(timeData)
15
+ .map(([ver, ts]) => ({
16
+ version: ver,
17
+ time: new Date(ts).getTime(),
18
+ }))
19
+ .filter((e) => !isNaN(e.time))
20
+ .sort((a, b) => a.time - b.time);
21
+ }
22
+
23
+ function findBursts(entries, windowMs) {
24
+ const bursts = [];
25
+ for (let i = 0; i < entries.length; i++) {
26
+ const windowEnd = entries[i].time + windowMs;
27
+ const group = [];
28
+ for (let j = i; j < entries.length && entries[j].time <= windowEnd; j++) {
29
+ group.push(entries[j]);
30
+ }
31
+ if (group.length >= 3) {
32
+ bursts.push({
33
+ startVersion: group[0].version,
34
+ endVersion: group[group.length - 1].version,
35
+ count: group.length,
36
+ windowMinutes: windowMs / 60000,
37
+ versions: group.map((e) => e.version),
38
+ });
39
+ }
40
+ }
41
+ return bursts;
42
+ }
43
+
44
+ function computeConfidence(bursts, findings) {
45
+ let base = 40;
46
+ if (bursts.length > 0) {
47
+ base += 20 + Math.min(bursts[0].count * 5, 25);
48
+ }
49
+ if (findings.length > 0) {
50
+ base += THRESHOLDS.identical_payload_weight;
51
+ }
52
+ return Math.min(100, Math.max(0, base));
53
+ }
54
+
55
+ function severityLabel(score) {
56
+ if (score >= 80) return 'high';
57
+ if (score >= 60) return 'medium';
58
+ return 'low';
59
+ }
60
+
61
+ function confidenceLabel(score) {
62
+ if (score >= 80) return 'HIGH';
63
+ if (score >= 60) return 'MEDIUM';
64
+ return 'LOW';
65
+ }
66
+
67
+ export const name = 'tier1-self-propagation';
68
+
69
+ export async function scan(pkgJson, _jsFiles, registryMeta, _allFiles) {
70
+ const pkgName = pkgJson?.name;
71
+ if (!pkgName) return [];
72
+ if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
73
+
74
+ const entries = parseTimeStamps(registryMeta);
75
+ if (entries.length < 3) return [];
76
+
77
+ const windowMs = (THRESHOLDS.burst_window_minutes || 60) * 60 * 1000;
78
+ const bursts = findBursts(entries, windowMs);
79
+ if (bursts.length === 0) return [];
80
+
81
+ const burst = bursts[0];
82
+ const confidenceScore = computeConfidence(bursts, []);
83
+ if (confidenceScore < THRESHOLDS.warn_threshold) return [];
84
+
85
+ const relatedPackages = [];
86
+ const maintainer =
87
+ registryMeta?.maintainer || registryMeta?.versions?.[pkgJson.version]?._npmUser?.name;
88
+ const namespaces = registryMeta?.namespacePackages || [];
89
+ if (namespaces.length > 0) {
90
+ for (const np of namespaces) {
91
+ if (np !== pkgName) relatedPackages.push(np);
92
+ }
93
+ }
94
+
95
+ return [
96
+ {
97
+ detector: 'tier1-self-propagation',
98
+ id: 'TIER1-SELF-PROPAGATION',
99
+ severity: severityLabel(confidenceScore),
100
+ confidence: confidenceLabel(confidenceScore),
101
+ confidenceScore,
102
+ subtype: 'self_propagation_burst',
103
+ message: `Self-propagation burst detected: ${burst.count} versions in ${burst.windowMinutes} minutes`,
104
+ evidence: [
105
+ `burst: ${burst.count} versions in ${burst.windowMinutes}min`,
106
+ `window: ${burst.startVersion} -> ${burst.endVersion}`,
107
+ `related_packages: ${relatedPackages.length}`,
108
+ `maintainer: ${maintainer || 'unknown'}`,
109
+ ],
110
+ locations: [{ file: 'package.json', line: 1, column: 1 }],
111
+ crossFiles: relatedPackages.slice(0, 10),
112
+ reference: 'D10: @redhat-cloud-services Miasma self-propagation',
113
+ },
114
+ ];
115
+ }
@@ -7,6 +7,6 @@
7
7
 
8
8
  export const name = 'tier1-slsa-attestation';
9
9
 
10
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
10
+ export async function scan(_pkgJson, _jsFiles, _registryMeta, _allFiles) {
11
11
  return [];
12
12
  }