muaddib-scanner 2.4.6 → 2.4.7
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 +1 -1
- package/src/response/playbooks.js +15 -0
- package/src/rules/index.js +38 -0
- package/src/scanner/ast-detectors.js +160 -9
- package/src/scanner/ast.js +18 -1
package/package.json
CHANGED
|
@@ -387,6 +387,21 @@ const PLAYBOOKS = {
|
|
|
387
387
|
'Domaine C2 ou d\'exfiltration detecte dans le code source. Ces domaines (oastify.com, webhook.site, ngrok.io, etc.) ' +
|
|
388
388
|
'sont utilises pour recevoir des donnees volees ou relayer des commandes. Verifier si le package a une raison ' +
|
|
389
389
|
'legitime d\'utiliser ce domaine. Bloquer les connexions sortantes vers ce domaine.',
|
|
390
|
+
|
|
391
|
+
fetch_decrypt_exec:
|
|
392
|
+
'CRITIQUE: Chaine steganographique detectee. Le code telecharge un fichier distant, le dechiffre via crypto, ' +
|
|
393
|
+
'puis execute le resultat via eval/Function. Pattern buildrunner-dev: payload malveillant cache dans une image PNG. ' +
|
|
394
|
+
'Isoler immediatement. Analyser le payload dechiffre. Supprimer le package.',
|
|
395
|
+
|
|
396
|
+
download_exec_binary:
|
|
397
|
+
'CRITIQUE: Pattern download-execute detecte. Le code telecharge un binaire, le rend executable (chmod 755), ' +
|
|
398
|
+
'puis l\'execute via execSync. Dropper deguise en compilation native addon (NeoShadow pattern). ' +
|
|
399
|
+
'Bloquer les telechargements. Verifier les binaires ecrits sur disque. Supprimer le package.',
|
|
400
|
+
|
|
401
|
+
ide_persistence:
|
|
402
|
+
'Persistence IDE detectee. Le code ecrit dans tasks.json ou la configuration VS Code avec execution automatique ' +
|
|
403
|
+
'a l\'ouverture du dossier (runOn: folderOpen, reveal: silent). Pattern FAMOUS CHOLLIMA / StegaBin. ' +
|
|
404
|
+
'Verifier ~/.config/Code/User/tasks.json et supprimer les taches inconnues.',
|
|
390
405
|
};
|
|
391
406
|
|
|
392
407
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -1103,6 +1103,44 @@ const RULES = {
|
|
|
1103
1103
|
],
|
|
1104
1104
|
mitre: 'T1071.001'
|
|
1105
1105
|
},
|
|
1106
|
+
|
|
1107
|
+
fetch_decrypt_exec: {
|
|
1108
|
+
id: 'MUADDIB-AST-033',
|
|
1109
|
+
name: 'Steganographic Payload Chain',
|
|
1110
|
+
severity: 'CRITICAL',
|
|
1111
|
+
confidence: 'high',
|
|
1112
|
+
description: 'Chaine steganographique: fetch distant + dechiffrement crypto + execution dynamique (eval/Function). Pattern buildrunner-dev: payload cache dans une image, dechiffre a runtime, puis execute.',
|
|
1113
|
+
references: [
|
|
1114
|
+
'https://attack.mitre.org/techniques/T1027/003/',
|
|
1115
|
+
'https://attack.mitre.org/techniques/T1140/'
|
|
1116
|
+
],
|
|
1117
|
+
mitre: 'T1027.003'
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1120
|
+
download_exec_binary: {
|
|
1121
|
+
id: 'MUADDIB-AST-034',
|
|
1122
|
+
name: 'Download-Execute Binary Pattern',
|
|
1123
|
+
severity: 'CRITICAL',
|
|
1124
|
+
confidence: 'high',
|
|
1125
|
+
description: 'Pattern download-execute: telechargement distant + chmod executable + execSync dans le meme fichier. Dropper binaire deguise en compilation native addon (NeoShadow pattern).',
|
|
1126
|
+
references: [
|
|
1127
|
+
'https://attack.mitre.org/techniques/T1105/',
|
|
1128
|
+
'https://attack.mitre.org/techniques/T1059/'
|
|
1129
|
+
],
|
|
1130
|
+
mitre: 'T1105'
|
|
1131
|
+
},
|
|
1132
|
+
|
|
1133
|
+
ide_persistence: {
|
|
1134
|
+
id: 'MUADDIB-AST-035',
|
|
1135
|
+
name: 'IDE Task Persistence',
|
|
1136
|
+
severity: 'HIGH',
|
|
1137
|
+
confidence: 'high',
|
|
1138
|
+
description: 'Persistence IDE: ecriture dans tasks.json ou Code/User/ avec execution automatique a l\'ouverture du dossier (runOn: folderOpen). Pattern FAMOUS CHOLLIMA / StegaBin pour persistance VS Code.',
|
|
1139
|
+
references: [
|
|
1140
|
+
'https://attack.mitre.org/techniques/T1546/'
|
|
1141
|
+
],
|
|
1142
|
+
mitre: 'T1546'
|
|
1143
|
+
},
|
|
1106
1144
|
};
|
|
1107
1145
|
|
|
1108
1146
|
function getRule(type) {
|
|
@@ -154,6 +154,35 @@ function extractStringValue(node) {
|
|
|
154
154
|
return null;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Recursively resolve BinaryExpression with '+' operator to reconstruct
|
|
159
|
+
* concatenated strings like '.gi' + 't' → '.git' or 'ho' + 'oks' → 'hooks'.
|
|
160
|
+
* Returns null if any part is non-literal.
|
|
161
|
+
*/
|
|
162
|
+
function resolveStringConcat(node) {
|
|
163
|
+
if (!node) return null;
|
|
164
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
|
|
165
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
166
|
+
return node.quasis.map(q => q.value.raw).join('');
|
|
167
|
+
}
|
|
168
|
+
if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
169
|
+
const left = resolveStringConcat(node.left);
|
|
170
|
+
const right = resolveStringConcat(node.right);
|
|
171
|
+
if (left !== null && right !== null) return left + right;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract string value from a node, including BinaryExpression resolution.
|
|
178
|
+
* Falls back to extractStringValue if concat resolution fails.
|
|
179
|
+
*/
|
|
180
|
+
function extractStringValueDeep(node) {
|
|
181
|
+
const concat = resolveStringConcat(node);
|
|
182
|
+
if (concat !== null) return concat;
|
|
183
|
+
return extractStringValue(node);
|
|
184
|
+
}
|
|
185
|
+
|
|
157
186
|
/**
|
|
158
187
|
* Returns true if all arguments of a call/new expression are string literals.
|
|
159
188
|
* Used to distinguish safe patterns like eval('1+2') or Function('return this')
|
|
@@ -276,7 +305,7 @@ function handleVariableDeclarator(node, ctx) {
|
|
|
276
305
|
const prop = node.init.callee.property;
|
|
277
306
|
if (obj?.type === 'Identifier' && obj.name === 'path' &&
|
|
278
307
|
prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
279
|
-
const joinArgs = node.init.arguments.map(a =>
|
|
308
|
+
const joinArgs = node.init.arguments.map(a => extractStringValueDeep(a) || '').join('/');
|
|
280
309
|
if (/\.github[\\/\/]workflows/i.test(joinArgs) || /\.github[\\/\/]actions/i.test(joinArgs)) {
|
|
281
310
|
ctx.workflowPathVars.add(node.id.name);
|
|
282
311
|
}
|
|
@@ -284,6 +313,32 @@ function handleVariableDeclarator(node, ctx) {
|
|
|
284
313
|
else if (node.init.arguments.some(a => a.type === 'Identifier' && ctx.workflowPathVars.has(a.name))) {
|
|
285
314
|
ctx.workflowPathVars.add(node.id.name);
|
|
286
315
|
}
|
|
316
|
+
// Track path.join that resolves to .git/hooks (concat fragments included)
|
|
317
|
+
if (/\.git[\\/\/]hooks/i.test(joinArgs) ||
|
|
318
|
+
(GIT_HOOKS.some(h => joinArgs.includes(h)) && joinArgs.includes('.git'))) {
|
|
319
|
+
ctx.gitHooksPathVars.set(node.id.name, joinArgs);
|
|
320
|
+
}
|
|
321
|
+
// Propagate: path.join(gitHooksPathVar, ...) inherits tracking
|
|
322
|
+
else if (node.init.arguments.some(a => a.type === 'Identifier' && ctx.gitHooksPathVars.has(a.name))) {
|
|
323
|
+
const parentPath = node.init.arguments.map(a => {
|
|
324
|
+
if (a.type === 'Identifier' && ctx.gitHooksPathVars.has(a.name)) return ctx.gitHooksPathVars.get(a.name);
|
|
325
|
+
return extractStringValueDeep(a) || '';
|
|
326
|
+
}).join('/');
|
|
327
|
+
ctx.gitHooksPathVars.set(node.id.name, parentPath);
|
|
328
|
+
}
|
|
329
|
+
// Track path.join that resolves to IDE config paths (.claude/, .cursor/, etc.)
|
|
330
|
+
const joinLower = joinArgs.toLowerCase();
|
|
331
|
+
if (MCP_CONFIG_PATHS.some(p => joinLower.includes(p.toLowerCase()))) {
|
|
332
|
+
ctx.ideConfigPathVars.set(node.id.name, joinArgs);
|
|
333
|
+
}
|
|
334
|
+
// Propagate: path.join(ideConfigPathVar, ...) inherits tracking
|
|
335
|
+
else if (node.init.arguments.some(a => a.type === 'Identifier' && ctx.ideConfigPathVars.has(a.name))) {
|
|
336
|
+
const parentPath = node.init.arguments.map(a => {
|
|
337
|
+
if (a.type === 'Identifier' && ctx.ideConfigPathVars.has(a.name)) return ctx.ideConfigPathVars.get(a.name);
|
|
338
|
+
return extractStringValueDeep(a) || '';
|
|
339
|
+
}).join('/');
|
|
340
|
+
ctx.ideConfigPathVars.set(node.id.name, parentPath);
|
|
341
|
+
}
|
|
287
342
|
}
|
|
288
343
|
}
|
|
289
344
|
}
|
|
@@ -336,6 +391,11 @@ function handleCallExpression(node, ctx) {
|
|
|
336
391
|
file: ctx.relFile
|
|
337
392
|
});
|
|
338
393
|
}
|
|
394
|
+
// Wave 4: detect require() of .node binary files (native addon camouflage)
|
|
395
|
+
const reqStr = extractStringValueDeep(arg);
|
|
396
|
+
if (reqStr && /\.node\s*$/.test(reqStr)) {
|
|
397
|
+
ctx.hasRequireNodeFile = true;
|
|
398
|
+
}
|
|
339
399
|
}
|
|
340
400
|
|
|
341
401
|
// Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
|
|
@@ -345,6 +405,8 @@ function handleCallExpression(node, ctx) {
|
|
|
345
405
|
(node.callee.property.name === 'exec' || node.callee.property.name === 'execSync')
|
|
346
406
|
? node.callee.property.name : null;
|
|
347
407
|
if ((execName || memberExec) && node.arguments.length > 0) {
|
|
408
|
+
// Wave 4: track any execSync/exec call for compound detection
|
|
409
|
+
ctx.hasExecSyncCall = true;
|
|
348
410
|
const arg = node.arguments[0];
|
|
349
411
|
let cmdStr = null;
|
|
350
412
|
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
@@ -560,13 +622,18 @@ function handleCallExpression(node, ctx) {
|
|
|
560
622
|
const mcpWriteMethod = node.callee.property.name;
|
|
561
623
|
if (['writeFileSync', 'writeFile'].includes(mcpWriteMethod) && node.arguments.length >= 2) {
|
|
562
624
|
const mcpPathArg = node.arguments[0];
|
|
563
|
-
const mcpPathStr =
|
|
564
|
-
// Also check path.join() calls
|
|
625
|
+
const mcpPathStr = extractStringValueDeep(mcpPathArg);
|
|
626
|
+
// Also check path.join() calls — resolve concat fragments in each argument
|
|
565
627
|
let mcpJoinedPath = '';
|
|
566
628
|
if (mcpPathArg?.type === 'CallExpression' && mcpPathArg.arguments) {
|
|
567
|
-
mcpJoinedPath = mcpPathArg.arguments.map(a =>
|
|
629
|
+
mcpJoinedPath = mcpPathArg.arguments.map(a => extractStringValueDeep(a) || '').join('/');
|
|
630
|
+
}
|
|
631
|
+
// Also check if path arg is a variable tracked as MCP/IDE config path
|
|
632
|
+
let mcpVarPath = '';
|
|
633
|
+
if (mcpPathArg?.type === 'Identifier' && ctx.ideConfigPathVars.has(mcpPathArg.name)) {
|
|
634
|
+
mcpVarPath = ctx.ideConfigPathVars.get(mcpPathArg.name);
|
|
568
635
|
}
|
|
569
|
-
const mcpCheckPath = (mcpPathStr || mcpJoinedPath).toLowerCase();
|
|
636
|
+
const mcpCheckPath = (mcpPathStr || mcpJoinedPath || mcpVarPath).toLowerCase();
|
|
570
637
|
const isMcpPath = MCP_CONFIG_PATHS.some(p => mcpCheckPath.includes(p.toLowerCase()));
|
|
571
638
|
if (isMcpPath) {
|
|
572
639
|
// Check content argument for MCP-related patterns
|
|
@@ -592,14 +659,19 @@ function handleCallExpression(node, ctx) {
|
|
|
592
659
|
const gitWriteMethod = node.callee.property.name;
|
|
593
660
|
if (['writeFileSync', 'writeFile'].includes(gitWriteMethod) && node.arguments.length >= 1) {
|
|
594
661
|
const gitPathArg = node.arguments[0];
|
|
595
|
-
const gitPathStr =
|
|
662
|
+
const gitPathStr = extractStringValueDeep(gitPathArg);
|
|
596
663
|
let gitJoinedPath = '';
|
|
597
664
|
if (gitPathArg?.type === 'CallExpression' && gitPathArg.arguments) {
|
|
598
|
-
gitJoinedPath = gitPathArg.arguments.map(a =>
|
|
665
|
+
gitJoinedPath = gitPathArg.arguments.map(a => extractStringValueDeep(a) || '').join('/');
|
|
599
666
|
}
|
|
600
|
-
|
|
667
|
+
// Also check if path arg is a variable tracked as git hooks path
|
|
668
|
+
let gitVarPath = '';
|
|
669
|
+
if (gitPathArg?.type === 'Identifier' && ctx.gitHooksPathVars.has(gitPathArg.name)) {
|
|
670
|
+
gitVarPath = ctx.gitHooksPathVars.get(gitPathArg.name);
|
|
671
|
+
}
|
|
672
|
+
const gitCheckPath = gitPathStr || gitJoinedPath || gitVarPath;
|
|
601
673
|
if (/\.git[\\/]hooks[\\/]/i.test(gitCheckPath) ||
|
|
602
|
-
GIT_HOOKS.some(h => gitCheckPath.includes(h) && gitCheckPath.includes('.git'))) {
|
|
674
|
+
GIT_HOOKS.some(h => gitCheckPath.includes(h) && (gitCheckPath.includes('.git') || gitCheckPath.includes('hooks')))) {
|
|
603
675
|
ctx.threats.push({
|
|
604
676
|
type: 'git_hooks_injection',
|
|
605
677
|
severity: 'HIGH',
|
|
@@ -633,6 +705,43 @@ function handleCallExpression(node, ctx) {
|
|
|
633
705
|
}
|
|
634
706
|
}
|
|
635
707
|
|
|
708
|
+
// Wave 4: IDE persistence — writeFileSync to VS Code tasks.json or Code/User/ paths
|
|
709
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
710
|
+
const ideWriteMethod = node.callee.property.name;
|
|
711
|
+
if (['writeFileSync', 'writeFile'].includes(ideWriteMethod) && node.arguments.length >= 1) {
|
|
712
|
+
const idePathArg = node.arguments[0];
|
|
713
|
+
const idePathStr = extractStringValueDeep(idePathArg);
|
|
714
|
+
let ideJoinedPath = '';
|
|
715
|
+
if (idePathArg?.type === 'CallExpression' && idePathArg.arguments) {
|
|
716
|
+
ideJoinedPath = idePathArg.arguments.map(a => extractStringValueDeep(a) || '').join('/');
|
|
717
|
+
}
|
|
718
|
+
let ideVarPath = '';
|
|
719
|
+
if (idePathArg?.type === 'Identifier' && ctx.ideConfigPathVars.has(idePathArg.name)) {
|
|
720
|
+
ideVarPath = ctx.ideConfigPathVars.get(idePathArg.name);
|
|
721
|
+
}
|
|
722
|
+
const ideCheckPath = (idePathStr || ideJoinedPath || ideVarPath).toLowerCase();
|
|
723
|
+
if ((ideCheckPath.includes('tasks.json') || ideCheckPath.includes('code/user/') || ideCheckPath.includes('.vscode/')) &&
|
|
724
|
+
!ctx.hasIdePersistenceWrite) {
|
|
725
|
+
// Check content for task runner persistence patterns (runOn, folderOpen)
|
|
726
|
+
const ideContentArg = node.arguments[1];
|
|
727
|
+
const ideContentStr = extractStringValue(ideContentArg);
|
|
728
|
+
const hasPersistencePattern = ideContentStr
|
|
729
|
+
? /runOn|folderOpen|reveal.*silent/.test(ideContentStr)
|
|
730
|
+
: true; // dynamic content targeting IDE task paths = suspicious
|
|
731
|
+
if (hasPersistencePattern) {
|
|
732
|
+
ctx.hasIdePersistenceWrite = true;
|
|
733
|
+
ctx.threats.push({
|
|
734
|
+
type: 'ide_persistence',
|
|
735
|
+
severity: 'HIGH',
|
|
736
|
+
message: `IDE persistence: ${ideWriteMethod}() writes to IDE task configuration (${ideCheckPath}). Auto-execution on folder open.`,
|
|
737
|
+
file: ctx.relFile
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
|
|
636
745
|
// Detect fs.chmodSync with executable permissions (deferred to postWalk for compound check)
|
|
637
746
|
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
638
747
|
const chmodMethod = node.callee.property.name;
|
|
@@ -926,6 +1035,7 @@ function handleNewExpression(node, ctx) {
|
|
|
926
1035
|
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
927
1036
|
// Skip string literal args — zero-risk globalThis polyfills used by every bundler
|
|
928
1037
|
if (!hasOnlyStringLiteralArgs(node)) {
|
|
1038
|
+
ctx.hasDynamicExec = true;
|
|
929
1039
|
ctx.threats.push({
|
|
930
1040
|
type: 'dangerous_call_function',
|
|
931
1041
|
severity: 'MEDIUM',
|
|
@@ -1222,6 +1332,47 @@ function handlePostWalk(ctx) {
|
|
|
1222
1332
|
file: ctx.relFile
|
|
1223
1333
|
});
|
|
1224
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
// Wave 4: Remote fetch + crypto decrypt + dynamic eval = steganographic payload chain
|
|
1337
|
+
if (ctx.hasRemoteFetch && ctx.hasCryptoDecipher && ctx.hasDynamicExec) {
|
|
1338
|
+
ctx.threats.push({
|
|
1339
|
+
type: 'fetch_decrypt_exec',
|
|
1340
|
+
severity: 'CRITICAL',
|
|
1341
|
+
message: 'Steganographic payload chain: remote fetch + crypto decryption + dynamic execution. No legitimate package uses this pattern.',
|
|
1342
|
+
file: ctx.relFile
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Wave 4: Download-execute-cleanup — https download + chmod executable + execSync + unlink
|
|
1347
|
+
if (ctx.hasRemoteFetch && ctx.hasChmodExecutable && ctx.hasExecSyncCall) {
|
|
1348
|
+
ctx.threats.push({
|
|
1349
|
+
type: 'download_exec_binary',
|
|
1350
|
+
severity: 'CRITICAL',
|
|
1351
|
+
message: 'Download-execute pattern: remote fetch + chmod executable + execSync in same file. Binary dropper camouflaged as native addon build.',
|
|
1352
|
+
file: ctx.relFile
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Wave 4: IDE persistence via content co-occurrence — tasks.json + runOn + writeFileSync
|
|
1357
|
+
if (!ctx.hasIdePersistenceWrite && ctx.hasTasksJsonInContent && ctx.hasRunOnInContent && ctx.hasWriteFileSyncInContent) {
|
|
1358
|
+
ctx.hasIdePersistenceWrite = true;
|
|
1359
|
+
ctx.threats.push({
|
|
1360
|
+
type: 'ide_persistence',
|
|
1361
|
+
severity: 'HIGH',
|
|
1362
|
+
message: 'IDE persistence: writes tasks.json with auto-execution trigger (runOn/folderOpen). VS Code task persistence technique.',
|
|
1363
|
+
file: ctx.relFile
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Wave 4: MCP content keywords in file with writeFileSync = MCP injection signal
|
|
1368
|
+
if (ctx.hasMcpContentKeywords && !ctx.threats.some(t => t.type === 'mcp_config_injection')) {
|
|
1369
|
+
ctx.threats.push({
|
|
1370
|
+
type: 'mcp_config_injection',
|
|
1371
|
+
severity: 'CRITICAL',
|
|
1372
|
+
message: 'MCP config injection: code contains MCP server configuration keywords (mcpServers/mcp.json/claude_desktop_config) with filesystem writes. AI toolchain poisoning.',
|
|
1373
|
+
file: ctx.relFile
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1225
1376
|
}
|
|
1226
1377
|
|
|
1227
1378
|
module.exports = {
|
package/src/scanner/ast.js
CHANGED
|
@@ -94,7 +94,24 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
94
94
|
hasBase64Encode: /\.toString\s*\(\s*['"]base64(url)?['"]\s*\)/.test(content),
|
|
95
95
|
hasDnsLoop: false, // set when dns call inside loop context detected
|
|
96
96
|
// SANDWORM_MODE P2: LLM API key harvesting
|
|
97
|
-
llmApiKeyCount: 0
|
|
97
|
+
llmApiKeyCount: 0,
|
|
98
|
+
// Wave 4: path variable tracking for git hooks and IDE config injection
|
|
99
|
+
gitHooksPathVars: new Map(),
|
|
100
|
+
ideConfigPathVars: new Map(),
|
|
101
|
+
// Wave 4: compound detection — fetch + decrypt + eval chain
|
|
102
|
+
hasRemoteFetch: /\bhttps?\.(get|request)\b/.test(content) || /\bfetch\s*\(/.test(content),
|
|
103
|
+
hasCryptoDecipher: /\bcreateDecipher(iv)?\s*\(/.test(content),
|
|
104
|
+
// Wave 4: native addon camouflage signals
|
|
105
|
+
hasRequireNodeFile: false,
|
|
106
|
+
hasExecSyncCall: false,
|
|
107
|
+
// Wave 4: IDE persistence (VS Code tasks.json, Code/User/ paths)
|
|
108
|
+
hasIdePersistenceWrite: false,
|
|
109
|
+
hasTasksJsonInContent: /\btasks\.json\b/.test(content),
|
|
110
|
+
hasRunOnInContent: /\brunOn\b|\bfolderOpen\b/.test(content),
|
|
111
|
+
hasWriteFileSyncInContent: /\bwriteFileSync\b|\bwriteFile\s*\(/.test(content),
|
|
112
|
+
// Wave 4: MCP content keyword detection (must also have writeFileSync in same file)
|
|
113
|
+
hasMcpContentKeywords: (/\bmcpServers\b/.test(content) || /\bmcp\.json\b/.test(content) || /\bclaude_desktop_config\b/.test(content)) &&
|
|
114
|
+
/\bwriteFileSync\b|\bwriteFile\s*\(/.test(content)
|
|
98
115
|
};
|
|
99
116
|
|
|
100
117
|
walk.simple(ast, {
|