muaddib-scanner 2.11.156 → 2.11.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.11.156",
3
+ "version": "2.11.158",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-07-03T11:48:38.678Z",
3
+ "timestamp": "2026-07-06T12:54:44.482Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -78,7 +78,11 @@ const HIGH_CONFIDENCE_MALICE_TYPES = new Set([
78
78
  // Phantom Gyp 2026-06: binding.gyp command-substitution = install-time RCE, quasi-never legit in benign packages
79
79
  'gyp_command_exec',
80
80
  // Phantom Gyp compound (Phase 1b): configure-time <!(node x.js) × independently-malicious invoked file
81
- 'gyp_phantom_exec'
81
+ 'gyp_phantom_exec',
82
+ // electron_app_injection (MUADDIB-AST-097): install hook overwrites a THIRD-PARTY Electron app's
83
+ // core/.asar with injected code (payload-in-write coupling ⇒ FP≈0). Bypasses MT-1 cap like the
84
+ // other install-time RCE/injection markers above (node_modules_write, systemd_persistence).
85
+ 'electron_app_injection'
82
86
  ]);
83
87
 
84
88
  // Lifecycle compound types that indicate real malicious intent beyond a simple postinstall
@@ -270,11 +270,30 @@ function buildBurstPreAlertEmbed(name, count, ecosystem = 'npm') {
270
270
  };
271
271
  }
272
272
 
273
+ /**
274
+ * Burst pre-alert Discord toggle — OFF by default. Measured 2026-07: the burst
275
+ * heads-up fires ~700×/day and is anti-correlated with real kills/incidents, so it is
276
+ * pure #alerts noise. This gates ONLY the Discord POST in sendBurstPreAlert(): the
277
+ * burst versions are still queued + scanned (queue.js), and the `[MONITOR] BURST
278
+ * PRE-ALERT` console.log + the `stats.burstPreAlerts` counter (used by the daily
279
+ * summary) are emitted BEFORE the send at the call site, so they are unaffected.
280
+ * Opt back in with MUADDIB_BURST_PREALERT_WEBHOOK=1 (also accepts true/yes/on).
281
+ * Read at call time so a restart re-toggles it without a code change (and tests can flip it).
282
+ */
283
+ function burstPreAlertWebhookEnabled() {
284
+ const v = (process.env.MUADDIB_BURST_PREALERT_WEBHOOK || '').trim().toLowerCase();
285
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
286
+ }
287
+
273
288
  /**
274
289
  * Layer 1c: Send a burst pre-alert webhook. Fire-and-forget; callers dedupe per
275
- * name/window so a burst pings once, not once per version.
290
+ * name/window so a burst pings once, not once per version. Discord POST is muted by
291
+ * default — see burstPreAlertWebhookEnabled(). Scoped to this sender only; the shared
292
+ * sendWebhook() and every other alert type (IOC/campaign pre-alerts, scan results,
293
+ * DEGRADED, daily report) are untouched.
276
294
  */
277
295
  async function sendBurstPreAlert(name, count, ecosystem = 'npm') {
296
+ if (!burstPreAlertWebhookEnabled()) return;
278
297
  const url = getWebhookUrl();
279
298
  if (!url) return;
280
299
  await sendWebhook(url, buildBurstPreAlertEmbed(name, count, ecosystem), { rawPayload: true });
@@ -1739,6 +1758,7 @@ module.exports = {
1739
1758
  sendCampaignPreAlert,
1740
1759
  buildBurstPreAlertEmbed,
1741
1760
  sendBurstPreAlert,
1761
+ burstPreAlertWebhookEnabled,
1742
1762
  matchVersionedIOC,
1743
1763
  computeRiskLevel,
1744
1764
  computeRiskScore,
@@ -668,6 +668,15 @@ const PLAYBOOKS = {
668
668
  'ou modifie git config init.templateDir pour la persistence globale. ' +
669
669
  'Verifier .git/hooks/ et git config --global --list. Supprimer les hooks non reconnus.',
670
670
 
671
+ electron_app_injection:
672
+ 'CRITIQUE: Injection dans une application Electron tierce. Le package localise une app Electron installee ' +
673
+ '(Discord, Atomic Wallet, Slack, ...) via os.homedir()+.asar/discord_desktop_core+existsSync, puis ecrase ' +
674
+ 'son code (index.js du core module / app.asar) avec un payload injecte (hook webContents.debugger, override ' +
675
+ 'XMLHttpRequest.prototype.send, BrowserWindow) — le payload s\'execute avec l\'identite de l\'app cible pour ' +
676
+ 'intercepter credentials/MFA ou detourner des adresses crypto. Verifier l\'integrite de l\'app ciblee ' +
677
+ '(reinstaller depuis la source officielle), rechercher le code injecte dans son core/.asar, revoquer tout ' +
678
+ 'credential saisi dans l\'app compromise, et supprimer le package. Considerer le poste comme compromis si le hook a tourne.',
679
+
671
680
  env_harvesting_dynamic:
672
681
  'Collecte dynamique de variables d\'environnement via Object.entries/keys/values(process.env) ' +
673
682
  'avec filtrage par patterns sensibles. Technique de vol de credentials a grande echelle. ' +
@@ -1155,6 +1155,20 @@ const RULES = {
1155
1155
  ],
1156
1156
  mitre: 'T1497'
1157
1157
  },
1158
+ electron_app_injection: {
1159
+ id: 'MUADDIB-AST-097',
1160
+ name: 'Electron App Injection',
1161
+ severity: 'CRITICAL',
1162
+ confidence: 'high',
1163
+ domain: 'malware',
1164
+ description: 'Un hook d\'installation localise une application Electron tierce deja installee chez la victime (Discord, Atomic Wallet, Slack, ...) — via os.homedir() + un chemin de signature Electron (.asar / discord_desktop_core / module core) + une sonde fs.existsSync — puis ecrase son code avec une charge utile JavaScript injectee, de sorte que le payload s\'execute avec l\'identite et les permissions de l\'application cible. Discriminant AST (vs proxy file-level): l\'ecriture (writeFileSync/appendFileSync) ecrit un CONTENU qui EST du code Electron injecte (BrowserWindow / webContents.debugger / *.prototype.X= / require(\'electron\')). Une app Electron legitime ecrit sa propre config JSON, jamais du code hookant les internals d\'une autre app. CRITICAL quand corrobore par le contexte de decouverte foreign-app; HIGH pour une ecriture nue ciblant un fichier .asar/core sous le home (GT-036 discord-electron-inject, GT-044 atomic-wallet-patch).',
1165
+ references: [
1166
+ 'https://research.jfrog.com/post/duer-js-malicious-package/',
1167
+ 'https://thehackernews.com/2025/04/malicious-npm-package-targets-atomic.html',
1168
+ 'https://attack.mitre.org/techniques/T1554/'
1169
+ ],
1170
+ mitre: 'T1554'
1171
+ },
1158
1172
  detached_process: {
1159
1173
  id: 'MUADDIB-AST-012',
1160
1174
  name: 'Detached Background Process',
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ // MUADDIB-AST-097 — electron_app_injection shared detection helpers.
4
+ //
5
+ // Threat model: an install-time hook LOCATES a third-party Electron desktop app already
6
+ // installed on the victim (Discord, Atomic Wallet, Slack, ...) by resolving the user's home
7
+ // directory, matching an Electron signature path (.asar / *_desktop_core / an electron core
8
+ // module), and probing it with fs.existsSync — then OVERWRITES that app's code with an injected
9
+ // JavaScript payload, so the payload runs with the target app's identity and permissions.
10
+ // GT-036 (discord-electron-inject): overwrites Discord's discord_desktop_core/index.js with a
11
+ // webContents.debugger network hook that exfiltrates login/MFA responses to bada-stealer.com.
12
+ // GT-044 (atomic-wallet-patch): patches Atomic Wallet's app.asar bundle with an
13
+ // XMLHttpRequest.prototype.send hook that swaps crypto recipient addresses.
14
+ //
15
+ // The DISCRIMINATOR (why this beats a file-level co-occurrence proxy): the malice is not that a
16
+ // file *mentions* BrowserWindow/.asar — a legitimate Electron app's own main process mentions all
17
+ // of those. The malice is that a filesystem WRITE writes CONTENT that IS injected Electron code
18
+ // into a FOREIGN app. So we couple, at AST level:
19
+ // (1) the write's CONTENT resolves to injected-Electron-code markers (the payload), AND
20
+ // (2) module context proves foreign-app discovery: os.homedir() + Electron-sig path + existsSync.
21
+ // A legit Electron app writes JSON *config* (settings.json), never source that hooks another app's
22
+ // internals — so coupling the payload to the write is what separates injection from an app writing
23
+ // its own settings, WITHOUT the evadable "is this file itself an Electron main?" heuristic.
24
+ //
25
+ // Consumed by: handle-call-expression.js (write-sink + fact collection), handle-variable-declarator.js
26
+ // & handle-assignment-expression.js (injected-code variable tracking), handle-post-walk.js (emit).
27
+
28
+ const { extractStringValueDeep } = require('./helpers.js');
29
+
30
+ // An Electron *target path* signature: an installed app's packed archive or a native core module.
31
+ // app.asar / core.asar / *.asar(.unpacked) — Electron ASAR archive
32
+ // discord_desktop_core / betterdiscord — Discord's swappable core module (well-known target)
33
+ // modules/<x>_core / *_desktop_core — Electron "<app>_core" native module convention
34
+ const ELECTRON_SIG_RE = /\.asar\b|discord_desktop_core|betterdiscord|[\\/]modules[\\/][A-Za-z0-9_.-]*_core\b|_desktop_core\b/i;
35
+
36
+ // Injected-Electron-code markers — tokens that make written CONTENT an injection payload, not config.
37
+ // HOOK markers (interception / tamper): near-zero in benign written content.
38
+ const ELECTRON_HOOK_MARKERS =
39
+ /\bwebContents\b|\bdebugger\s*\.\s*(?:attach|on|sendCommand)\b|\bNetwork\s*\.\s*(?:enable|responseReceived|getResponseBody)\b|\bipcRenderer\b|\.\s*prototype\s*\.\s*[A-Za-z_$][\w$]*\s*=/;
40
+ // APP markers (electron surface): boilerplate-ish, weaker alone but a payload signal in write content.
41
+ const ELECTRON_APP_MARKERS =
42
+ /\bBrowserWindow\b|require\s*\(\s*['"]electron['"]\s*\)|module\s*\.\s*exports\s*=\s*require\s*\(/;
43
+ // A write's content is a payload when it carries a HOOK marker OR an APP marker.
44
+ const ELECTRON_PAYLOAD_MARKERS = new RegExp(ELECTRON_HOOK_MARKERS.source + '|' + ELECTRON_APP_MARKERS.source);
45
+
46
+ const HOME_PATH_STR_RE = /[\\/]home[\\/]|[\\/]Users[\\/]|appdata|localappdata|userprofile|[\\/]\.config[\\/]|^~[\\/]/i;
47
+
48
+ // Best-effort: collect all string-literal fragments reachable from a node subtree (array
49
+ // elements, `.join()`/`.concat()` receivers+args, `+` concatenation, template quasis, conditional
50
+ // branches). Bounded in depth and total length. Used to test whether a write's CONTENT argument
51
+ // (or a variable initializer) carries injected-Electron-code markers even when it is built as
52
+ // `[ ...lines ].join("\n")` or `"...hook..." + fileContent`.
53
+ function collectCodeString(node, depth, acc) {
54
+ if (!node || depth > 6 || acc.budget <= 0) return acc.s;
55
+ switch (node.type) {
56
+ case 'Literal':
57
+ if (typeof node.value === 'string') { acc.s += node.value + '\n'; acc.budget -= node.value.length + 1; }
58
+ break;
59
+ case 'TemplateLiteral':
60
+ for (const q of node.quasis || []) { const raw = q.value.raw; acc.s += raw + '\n'; acc.budget -= raw.length + 1; }
61
+ for (const e of node.expressions || []) collectCodeString(e, depth + 1, acc);
62
+ break;
63
+ case 'BinaryExpression':
64
+ if (node.operator === '+') { collectCodeString(node.left, depth + 1, acc); collectCodeString(node.right, depth + 1, acc); }
65
+ break;
66
+ case 'ArrayExpression':
67
+ for (const el of node.elements || []) collectCodeString(el, depth + 1, acc);
68
+ break;
69
+ case 'CallExpression': {
70
+ const m = node.callee && node.callee.type === 'MemberExpression' && node.callee.property
71
+ ? node.callee.property.name : null;
72
+ if (m === 'join' || m === 'concat') {
73
+ collectCodeString(node.callee.object, depth + 1, acc);
74
+ for (const a of node.arguments || []) collectCodeString(a, depth + 1, acc);
75
+ }
76
+ break;
77
+ }
78
+ case 'ConditionalExpression':
79
+ collectCodeString(node.consequent, depth + 1, acc);
80
+ collectCodeString(node.alternate, depth + 1, acc);
81
+ break;
82
+ default:
83
+ break;
84
+ }
85
+ return acc.s;
86
+ }
87
+
88
+ function resolveCodeString(node) {
89
+ return collectCodeString(node, 0, { s: '', budget: 20000 });
90
+ }
91
+
92
+ // Does this subtree reference a variable already known to hold injected Electron code?
93
+ // Handles `hook + "\n" + fileContent` (BinaryExpression), templates, and array joins.
94
+ function referencesInjectedVar(node, ctx, depth) {
95
+ if (!node || depth > 6 || !ctx.injectedCodeVars) return false;
96
+ switch (node.type) {
97
+ case 'Identifier':
98
+ return ctx.injectedCodeVars.has(node.name);
99
+ case 'BinaryExpression':
100
+ return node.operator === '+' &&
101
+ (referencesInjectedVar(node.left, ctx, depth + 1) || referencesInjectedVar(node.right, ctx, depth + 1));
102
+ case 'TemplateLiteral':
103
+ return (node.expressions || []).some(e => referencesInjectedVar(e, ctx, depth + 1));
104
+ case 'ConditionalExpression':
105
+ return referencesInjectedVar(node.consequent, ctx, depth + 1) || referencesInjectedVar(node.alternate, ctx, depth + 1);
106
+ case 'CallExpression': {
107
+ const m = node.callee && node.callee.type === 'MemberExpression' && node.callee.property
108
+ ? node.callee.property.name : null;
109
+ if (m === 'join' || m === 'concat') {
110
+ return referencesInjectedVar(node.callee.object, ctx, depth + 1) ||
111
+ (node.arguments || []).some(a => referencesInjectedVar(a, ctx, depth + 1));
112
+ }
113
+ return false;
114
+ }
115
+ default:
116
+ return false;
117
+ }
118
+ }
119
+
120
+ // True if a node (a variable initializer or a write's content arg) carries an injected payload:
121
+ // it references a tracked injected-code variable, or it statically resolves to code with markers.
122
+ function carriesInjectedPayload(node, ctx) {
123
+ if (!node) return false;
124
+ if (node.type === 'Identifier') return !!(ctx.injectedCodeVars && ctx.injectedCodeVars.has(node.name));
125
+ if (referencesInjectedVar(node, ctx, 0)) return true;
126
+ return ELECTRON_PAYLOAD_MARKERS.test(resolveCodeString(node));
127
+ }
128
+
129
+ // An argument that roots a path in the user's HOME (where a victim's apps are installed):
130
+ // os.homedir() | process.env.{HOME,APPDATA,LOCALAPPDATA,USERPROFILE,XDG_CONFIG_HOME}
131
+ // | "~"/"~/..." | absolute "/home/..", "/Users/..", "C:\Users\.." literal
132
+ function isHomeRootArg(node) {
133
+ if (!node) return false;
134
+ if (node.type === 'CallExpression' && node.callee && node.callee.type === 'MemberExpression' &&
135
+ node.callee.object && node.callee.object.type === 'Identifier' && node.callee.object.name === 'os' &&
136
+ node.callee.property && node.callee.property.type === 'Identifier' && node.callee.property.name === 'homedir') {
137
+ return true;
138
+ }
139
+ if (node.type === 'MemberExpression' &&
140
+ node.object && node.object.type === 'MemberExpression' &&
141
+ node.object.object && node.object.object.type === 'Identifier' && node.object.object.name === 'process' &&
142
+ node.object.property && node.object.property.type === 'Identifier' && node.object.property.name === 'env' &&
143
+ node.property && node.property.type === 'Identifier' &&
144
+ ['HOME', 'APPDATA', 'LOCALAPPDATA', 'USERPROFILE', 'XDG_CONFIG_HOME'].includes(node.property.name)) {
145
+ return true;
146
+ }
147
+ if (node.type === 'Literal' && typeof node.value === 'string') {
148
+ if (/^~(?:[\\/]|$)/.test(node.value)) return true;
149
+ if (/^\/home\/|^\/Users\/|:[\\/]Users[\\/]/i.test(node.value)) return true;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ function _scanPathArgForSig(arg, ctx) {
155
+ if (!arg) return;
156
+ const s = extractStringValueDeep(arg);
157
+ if (s && ELECTRON_SIG_RE.test(s)) ctx.hasElectronAppSig = true;
158
+ if (arg.type === 'CallExpression' && arg.callee && arg.callee.type === 'MemberExpression' &&
159
+ arg.callee.object && arg.callee.object.type === 'Identifier' && arg.callee.object.name === 'path' &&
160
+ arg.callee.property && arg.callee.property.type === 'Identifier' &&
161
+ (arg.callee.property.name === 'join' || arg.callee.property.name === 'resolve')) {
162
+ const joined = (arg.arguments || []).map(a => extractStringValueDeep(a) || '').join('/');
163
+ if (ELECTRON_SIG_RE.test(joined)) ctx.hasElectronAppSig = true;
164
+ if ((arg.arguments || []).some(isHomeRootArg)) ctx.hasHomedirResolve = true;
165
+ }
166
+ }
167
+
168
+ // Collect the foreign-app-discovery corroboration facts from a CallExpression:
169
+ // os.homedir() → hasHomedirResolve
170
+ // fs.existsSync/accessSync/statSync/lstatSync → hasFsExistsProbe (+ sig/home from the probed path)
171
+ // path.join/path.resolve → hasElectronAppSig / hasHomedirResolve
172
+ // app.whenReady/app.getPath/.loadURL/.loadFile → hasOwnElectronMain (the file IS an Electron main;
173
+ // gates the HIGH tier so a real app's self-writes
174
+ // are not flagged as tampering).
175
+ function scanElectronFacts(node, ctx) {
176
+ const callee = node.callee;
177
+ if (!callee || callee.type !== 'MemberExpression' || !callee.property || callee.property.type !== 'Identifier') return;
178
+ const prop = callee.property.name;
179
+ const objName = callee.object && callee.object.type === 'Identifier' ? callee.object.name : null;
180
+
181
+ if (objName === 'os' && prop === 'homedir') ctx.hasHomedirResolve = true;
182
+
183
+ if (prop === 'existsSync' || prop === 'accessSync' || prop === 'statSync' || prop === 'lstatSync') {
184
+ ctx.hasFsExistsProbe = true;
185
+ _scanPathArgForSig(node.arguments && node.arguments[0], ctx);
186
+ }
187
+
188
+ if (objName === 'path' && (prop === 'join' || prop === 'resolve')) {
189
+ const joined = (node.arguments || []).map(a => extractStringValueDeep(a) || '').join('/');
190
+ if (ELECTRON_SIG_RE.test(joined)) ctx.hasElectronAppSig = true;
191
+ if ((node.arguments || []).some(isHomeRootArg)) ctx.hasHomedirResolve = true;
192
+ }
193
+
194
+ if ((prop === 'whenReady' && (objName === 'app' || objName === 'electron')) ||
195
+ (prop === 'getPath' && objName === 'app') ||
196
+ prop === 'loadURL' || prop === 'loadFile') {
197
+ ctx.hasOwnElectronMain = true;
198
+ }
199
+ }
200
+
201
+ // For the HIGH tier: does a write TARGET resolve to a home-rooted Electron file? Handles an inline
202
+ // path.join/path.resolve, a bare literal, or an Identifier bound to a tracked path.join result
203
+ // (ctx.electronTargetVars — the common `const target = path.join(os.homedir(), ..., 'app.asar')`).
204
+ function resolveElectronTarget(node, ctx) {
205
+ if (node && node.type === 'Identifier' && ctx && ctx.electronTargetVars && ctx.electronTargetVars.has(node.name)) {
206
+ return ctx.electronTargetVars.get(node.name);
207
+ }
208
+ let pathStr = '';
209
+ let home = false;
210
+ if (node && node.type === 'CallExpression' && node.callee && node.callee.type === 'MemberExpression' &&
211
+ node.callee.object && node.callee.object.type === 'Identifier' && node.callee.object.name === 'path' &&
212
+ node.callee.property && node.callee.property.type === 'Identifier' &&
213
+ (node.callee.property.name === 'join' || node.callee.property.name === 'resolve')) {
214
+ pathStr = (node.arguments || []).map(a => extractStringValueDeep(a) || '').join('/');
215
+ if ((node.arguments || []).some(isHomeRootArg)) home = true;
216
+ } else {
217
+ pathStr = extractStringValueDeep(node) || '';
218
+ }
219
+ if (HOME_PATH_STR_RE.test(pathStr)) home = true;
220
+ return { sig: ELECTRON_SIG_RE.test(pathStr), home, pathStr: pathStr.slice(0, 120) };
221
+ }
222
+
223
+ module.exports = {
224
+ ELECTRON_SIG_RE,
225
+ ELECTRON_PAYLOAD_MARKERS,
226
+ ELECTRON_HOOK_MARKERS,
227
+ resolveCodeString,
228
+ carriesInjectedPayload,
229
+ referencesInjectedVar,
230
+ isHomeRootArg,
231
+ scanElectronFacts,
232
+ resolveElectronTarget
233
+ };
@@ -11,10 +11,18 @@ const {
11
11
  resolveStringConcat,
12
12
  extractStringValueDeep
13
13
  } = require('./helpers.js');
14
+ const { carriesInjectedPayload } = require('./electron-injection.js');
14
15
 
15
16
  function handleAssignmentExpression(node, ctx) {
16
17
  // Variable reassignment: x += 'process' or x = x + 'process'
17
18
  if (node.left?.type === 'Identifier') {
19
+ // MUADDIB-AST-097: propagate injected-Electron-code taint across reassignment. Covers
20
+ // `content = hook + "\n" + content` where `hook` is a payload array-join, so the subsequent
21
+ // writeFileSync(target, content) is recognised as writing an injected payload.
22
+ if ((node.operator === '=' || node.operator === '+=') &&
23
+ carriesInjectedPayload(node.right, ctx)) {
24
+ ctx.injectedCodeVars.add(node.left.name);
25
+ }
18
26
  if (node.operator === '+=' && ctx.stringVarValues.has(node.left.name)) {
19
27
  const rightVal = extractStringValueDeep(node.right);
20
28
  if (rightVal !== null) {
@@ -96,6 +96,11 @@ const {
96
96
  const { countInvisibleUnicode } = require('../../shared/unicode-invisibles.js');
97
97
  const { classifyMcpWrite } = require('./mcp-write-classifier.js');
98
98
  const { isShadowEnabled, recordShadowDivergence } = require('../../shared/shadow.js');
99
+ const {
100
+ carriesInjectedPayload,
101
+ scanElectronFacts,
102
+ resolveElectronTarget
103
+ } = require('./electron-injection.js');
99
104
 
100
105
  /**
101
106
  * SHADOW 3-tier classification for mcp_config_injection emissions (R5 + R5b).
@@ -172,6 +177,11 @@ function _isUserLevelPathArg(node, depth = 0) {
172
177
  function handleCallExpression(node, ctx) {
173
178
  const callName = getCallName(node);
174
179
 
180
+ // MUADDIB-AST-097: collect foreign-Electron-app-discovery facts (os.homedir / existsSync /
181
+ // .asar-electron-core path.join / own-app markers). Correlated with the payload-write in
182
+ // handlePostWalk to emit electron_app_injection.
183
+ scanElectronFacts(node, ctx);
184
+
175
185
  // Detect require() with non-literal argument (obfuscation)
176
186
  if (callName === 'require' && node.arguments.length > 0) {
177
187
  const arg = node.arguments[0];
@@ -967,6 +977,28 @@ function handleCallExpression(node, ctx) {
967
977
  }
968
978
 
969
979
 
980
+ // MUADDIB-AST-097: electron_app_injection — a filesystem write whose CONTENT resolves to
981
+ // injected Electron code (BrowserWindow / webContents-debugger hook / prototype override /
982
+ // require('electron')). The payload-in-write coupling is the AST discriminator vs a legit
983
+ // Electron app writing JSON config. The foreign-app-discovery corroboration (os.homedir +
984
+ // .asar/electron-core sig + existsSync) and the final verdict are assembled in handlePostWalk.
985
+ // A home-rooted .asar/electron-core *target* write (payload not statically resolvable) is
986
+ // recorded separately for the HIGH tier.
987
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
988
+ const eaiMethod = node.callee.property.name;
989
+ if (['writeFileSync', 'writeFile', 'appendFileSync', 'appendFile'].includes(eaiMethod) &&
990
+ node.arguments.length >= 2) {
991
+ if (carriesInjectedPayload(node.arguments[1], ctx)) {
992
+ ctx.electronPayloadWrites.push({ method: eaiMethod });
993
+ } else {
994
+ const tgt = resolveElectronTarget(node.arguments[0], ctx);
995
+ if (tgt.sig && tgt.home) {
996
+ ctx.electronAsarTargetWrites.push({ method: eaiMethod, pathStr: tgt.pathStr });
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+
970
1002
  // Detect fs.chmodSync with executable permissions (deferred to postWalk for compound check)
971
1003
  if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
972
1004
  const chmodMethod = node.callee.property.name;
@@ -640,6 +640,39 @@ function handlePostWalk(ctx) {
640
640
  });
641
641
  }
642
642
  }
643
+
644
+ // MUADDIB-AST-097: electron_app_injection — an install hook overwrites a THIRD-PARTY Electron
645
+ // app's core/.asar with injected code (GT-036 Discord debugger-hook stealer → bada-stealer.com;
646
+ // GT-044 Atomic Wallet XMLHttpRequest.prototype.send address-swap). The discriminating AST
647
+ // coupling (assembled here from facts collected during the walk): a writeFileSync whose CONTENT
648
+ // resolves to injected Electron code (electronPayloadWrites), corroborated by the foreign-app
649
+ // discovery context — os.homedir() + a .asar/electron-core signature path + an existsSync probe.
650
+ // A legit Electron app writes JSON *config*, never source that hooks another app's internals, so
651
+ // the payload-in-write coupling is what separates injection from an app writing its own settings
652
+ // (no need for the evadable "is this file itself an Electron main?" exclusion). dist/bundled
653
+ // files are suppressed: an app's own minified main naturally carries these tokens.
654
+ const _eaiDist = /^(?:dist|build|out|output|vendor)[/\\]/i.test(ctx.relFile) ||
655
+ /\.(?:bundle|min)\.js$/i.test(ctx.relFile);
656
+ if (!_eaiDist) {
657
+ const foreignAppDiscovery = ctx.hasHomedirResolve && ctx.hasElectronAppSig && ctx.hasFsExistsProbe;
658
+ if ((ctx.electronPayloadWrites?.length || 0) > 0 && foreignAppDiscovery) {
659
+ const w = ctx.electronPayloadWrites[0];
660
+ ctx.threats.push({
661
+ type: 'electron_app_injection',
662
+ severity: 'CRITICAL',
663
+ message: `Electron app injection: ${w.method}() overwrites a foreign Electron app — located via os.homedir()+.asar/electron-core+existsSync — with injected code (BrowserWindow/webContents-debugger/prototype-override/require('electron')). Third-party desktop-app code injection (Discord/Atomic-Wallet class): the payload runs with the target app's identity.`,
664
+ file: ctx.relFile
665
+ });
666
+ } else if ((ctx.electronAsarTargetWrites?.length || 0) > 0 && !ctx.hasOwnElectronMain) {
667
+ const w = ctx.electronAsarTargetWrites[0];
668
+ ctx.threats.push({
669
+ type: 'electron_app_injection',
670
+ severity: 'HIGH',
671
+ message: `Electron app tampering: ${w.method}() overwrites a user-home Electron app file "${w.pathStr}" (.asar/electron-core). Writing into an installed desktop app is an injection/patching technique; the injected payload was not statically resolvable.`,
672
+ file: ctx.relFile
673
+ });
674
+ }
675
+ }
643
676
  }
644
677
 
645
678
 
@@ -16,6 +16,7 @@ const {
16
16
  extractStringValueDeep,
17
17
  isStaticValue
18
18
  } = require('./helpers.js');
19
+ const { carriesInjectedPayload, resolveElectronTarget } = require('./electron-injection.js');
19
20
 
20
21
  function handleVariableDeclarator(node, ctx) {
21
22
  if (node.id?.type === 'Identifier') {
@@ -24,6 +25,24 @@ function handleVariableDeclarator(node, ctx) {
24
25
  ctx.staticAssignments.add(node.id.name);
25
26
  }
26
27
 
28
+ // MUADDIB-AST-097: track variables whose value is injected Electron code (the write payload).
29
+ // Covers `var injectedPayload = [ "...BrowserWindow...", ... ].join("\n")` and any concat /
30
+ // template / literal carrying payload markers. Consumed by the write-sink check + post-walk.
31
+ if (node.init && carriesInjectedPayload(node.init, ctx)) {
32
+ ctx.injectedCodeVars.add(node.id.name);
33
+ }
34
+
35
+ // MUADDIB-AST-097: track variables bound to a path.join/resolve that builds an Electron app
36
+ // file path (for the HIGH-tier bare-.asar-target write, where the write target is a variable,
37
+ // e.g. `const target = path.join(os.homedir(), '.config', 'discord', 'app.asar')`).
38
+ if (node.init && node.init.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression' &&
39
+ node.init.callee.object?.type === 'Identifier' && node.init.callee.object.name === 'path' &&
40
+ node.init.callee.property?.type === 'Identifier' &&
41
+ (node.init.callee.property.name === 'join' || node.init.callee.property.name === 'resolve')) {
42
+ const eaiTgt = resolveElectronTarget(node.init);
43
+ if (eaiTgt.sig) ctx.electronTargetVars.set(node.id.name, eaiTgt);
44
+ }
45
+
27
46
  // AST-018 alias capture (@longzy): `var env = process.env` — record the alias so a later
28
47
  // `env[String.fromCharCode(...)]` access resolves back to process.env in the member handler.
29
48
  if (ctx.envAliases && node.init?.type === 'MemberExpression' && !node.init.computed &&
@@ -158,6 +158,16 @@ function analyzeFile(content, filePath, basePath) {
158
158
  objectPropertyMap: new Map(), // B5: objName → Map<propName, stringValue>
159
159
  concatValues: new Map(), // B2: varName → { value, operands } for concat strings with ≥3 operands
160
160
  stringVarValues: new Map(), // Variable reassignment tracking: varName → string value
161
+ // MUADDIB-AST-097 electron_app_injection: injected-code variable taint + write-sink records +
162
+ // foreign-Electron-app-discovery facts (os.homedir + .asar/electron-core sig + existsSync probe).
163
+ injectedCodeVars: new Set(), // vars whose value is injected Electron code (write payload)
164
+ electronTargetVars: new Map(), // varName → {sig,home,pathStr} for a path.join Electron target
165
+ electronPayloadWrites: [], // writeFile* calls whose CONTENT is an injected payload
166
+ electronAsarTargetWrites: [], // writeFile* calls whose TARGET is a home-rooted .asar/core file
167
+ hasHomedirResolve: false, // os.homedir() / process.env.{HOME,APPDATA,...} resolved
168
+ hasElectronAppSig: false, // a .asar / *_desktop_core / electron-core path constructed
169
+ hasFsExistsProbe: false, // fs.existsSync/accessSync/statSync (probe the victim install)
170
+ hasOwnElectronMain: false, // file IS an Electron main (app.whenReady/loadURL/...) — gates HIGH
161
171
  hasFromCharCode: content.includes('fromCharCode'),
162
172
  // Anti-analysis / sandbox evasion (2026, @longzy DPRK). Structural "detonation-gate wall"
163
173
  // magnitude + host recon + a charcode-hidden analyzer-honeytoken reference. Aggregated in