muaddib-scanner 2.2.19 → 2.2.22
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/.gitattributes +18 -0
- package/README.fr.md +819 -825
- package/README.md +822 -822
- package/bin/muaddib.js +868 -849
- package/docker/Dockerfile +19 -18
- package/docker/sandbox-runner.sh +313 -292
- package/package.json +61 -61
- package/src/commands/evaluate.js +484 -484
- package/src/diff.js +411 -411
- package/src/hooks-init.js +264 -258
- package/src/index.js +439 -437
- package/src/ioc/scraper.js +1293 -1293
- package/src/monitor.js +1817 -1764
- package/src/sandbox.js +631 -620
- package/src/scanner/ast-detectors.js +946 -933
- package/src/scanner/ast.js +96 -96
- package/src/scanner/github-actions.js +96 -96
- package/src/scanner/module-graph.js +2 -2
- package/src/scanner/obfuscation.js +128 -128
- package/src/scanner/shell.js +48 -48
- package/src/shared/download.js +181 -171
- package/src/webhook.js +413 -353
package/bin/muaddib.js
CHANGED
|
@@ -1,850 +1,869 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const { exec } = require('child_process');
|
|
3
|
-
const { run } = require('../src/index.js');
|
|
4
|
-
const { updateIOCs } = require('../src/ioc/updater.js');
|
|
5
|
-
const { watch } = require('../src/watch.js');
|
|
6
|
-
const { startDaemon } = require('../src/daemon.js');
|
|
7
|
-
const { runScraper } = require('../src/ioc/scraper.js');
|
|
8
|
-
const { safeInstall } = require('../src/safe-install.js');
|
|
9
|
-
const { buildSandboxImage, runSandbox, generateNetworkReport } = require('../src/sandbox.js');
|
|
10
|
-
const { diff, showRefs } = require('../src/diff.js');
|
|
11
|
-
const { initHooks, removeHooks } = require('../src/hooks-init.js');
|
|
12
|
-
|
|
13
|
-
const args = process.argv.slice(2);
|
|
14
|
-
const command = args[0];
|
|
15
|
-
const options = args.slice(1);
|
|
16
|
-
|
|
17
|
-
// Parse options
|
|
18
|
-
let target = '.';
|
|
19
|
-
let jsonOutput = false;
|
|
20
|
-
let htmlOutput = null;
|
|
21
|
-
let sarifOutput = null;
|
|
22
|
-
let explainMode = false;
|
|
23
|
-
let failLevel = 'high';
|
|
24
|
-
let webhookUrl = null;
|
|
25
|
-
let paranoidMode = false;
|
|
26
|
-
let excludeDirs = [];
|
|
27
|
-
let entropyThreshold = null;
|
|
28
|
-
let temporalMode = false;
|
|
29
|
-
let temporalAstMode = false;
|
|
30
|
-
let temporalPublishMode = false;
|
|
31
|
-
let temporalMaintainerMode = false;
|
|
32
|
-
let temporalFullMode = false;
|
|
33
|
-
let breakdownMode = false;
|
|
34
|
-
let noDeobfuscate = false;
|
|
35
|
-
let noModuleGraph = false;
|
|
36
|
-
let feedLimit = null;
|
|
37
|
-
let feedSeverity = null;
|
|
38
|
-
let feedSince = null;
|
|
39
|
-
let servePort = null;
|
|
40
|
-
|
|
41
|
-
for (let i = 0; i < options.length; i++) {
|
|
42
|
-
if (options[i] === '--json') {
|
|
43
|
-
jsonOutput = true;
|
|
44
|
-
} else if (options[i] === '--html') {
|
|
45
|
-
const htmlPath = options[i + 1] || 'muaddib-report.html';
|
|
46
|
-
// CLI-001: Block path traversal
|
|
47
|
-
if (htmlPath.includes('..')) {
|
|
48
|
-
console.error('[ERROR] --html path must not contain path traversal (..)');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
htmlOutput = htmlPath;
|
|
52
|
-
i++;
|
|
53
|
-
} else if (options[i] === '--sarif') {
|
|
54
|
-
const sarifPath = options[i + 1] || 'muaddib-results.sarif';
|
|
55
|
-
// CLI-001: Block path traversal
|
|
56
|
-
if (sarifPath.includes('..')) {
|
|
57
|
-
console.error('[ERROR] --sarif path must not contain path traversal (..)');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
sarifOutput = sarifPath;
|
|
61
|
-
i++;
|
|
62
|
-
} else if (options[i] === '--explain') {
|
|
63
|
-
explainMode = true;
|
|
64
|
-
} else if (options[i] === '--fail-on') {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} else if (options[i] === '--
|
|
110
|
-
|
|
111
|
-
} else if (options[i] === '--temporal-
|
|
112
|
-
|
|
113
|
-
} else if (options[i] === '--
|
|
114
|
-
|
|
115
|
-
} else if (options[i] === '--
|
|
116
|
-
|
|
117
|
-
} else if (options[i] === '--
|
|
118
|
-
|
|
119
|
-
} else if (options[i] === '--
|
|
120
|
-
|
|
121
|
-
} else if (options[i] === '--
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} else if (options[i] === '--
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
i++;
|
|
133
|
-
} else if (options[i] === '--
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
if (!
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{ name: '
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
console.log(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
muaddib
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
muaddib
|
|
381
|
-
muaddib
|
|
382
|
-
muaddib
|
|
383
|
-
muaddib
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
--
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
--
|
|
399
|
-
--
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
--
|
|
403
|
-
--
|
|
404
|
-
--
|
|
405
|
-
--
|
|
406
|
-
--
|
|
407
|
-
--
|
|
408
|
-
--
|
|
409
|
-
--
|
|
410
|
-
--
|
|
411
|
-
--
|
|
412
|
-
--
|
|
413
|
-
-
|
|
414
|
-
--
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
console.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
console.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
console.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
process.exit(1);
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
} else if (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
.
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
console.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
let
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (options[i] === '--
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const { run } = require('../src/index.js');
|
|
4
|
+
const { updateIOCs } = require('../src/ioc/updater.js');
|
|
5
|
+
const { watch } = require('../src/watch.js');
|
|
6
|
+
const { startDaemon } = require('../src/daemon.js');
|
|
7
|
+
const { runScraper } = require('../src/ioc/scraper.js');
|
|
8
|
+
const { safeInstall } = require('../src/safe-install.js');
|
|
9
|
+
const { buildSandboxImage, runSandbox, generateNetworkReport } = require('../src/sandbox.js');
|
|
10
|
+
const { diff, showRefs } = require('../src/diff.js');
|
|
11
|
+
const { initHooks, removeHooks } = require('../src/hooks-init.js');
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0];
|
|
15
|
+
const options = args.slice(1);
|
|
16
|
+
|
|
17
|
+
// Parse options
|
|
18
|
+
let target = '.';
|
|
19
|
+
let jsonOutput = false;
|
|
20
|
+
let htmlOutput = null;
|
|
21
|
+
let sarifOutput = null;
|
|
22
|
+
let explainMode = false;
|
|
23
|
+
let failLevel = 'high';
|
|
24
|
+
let webhookUrl = null;
|
|
25
|
+
let paranoidMode = false;
|
|
26
|
+
let excludeDirs = [];
|
|
27
|
+
let entropyThreshold = null;
|
|
28
|
+
let temporalMode = false;
|
|
29
|
+
let temporalAstMode = false;
|
|
30
|
+
let temporalPublishMode = false;
|
|
31
|
+
let temporalMaintainerMode = false;
|
|
32
|
+
let temporalFullMode = false;
|
|
33
|
+
let breakdownMode = false;
|
|
34
|
+
let noDeobfuscate = false;
|
|
35
|
+
let noModuleGraph = false;
|
|
36
|
+
let feedLimit = null;
|
|
37
|
+
let feedSeverity = null;
|
|
38
|
+
let feedSince = null;
|
|
39
|
+
let servePort = null;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < options.length; i++) {
|
|
42
|
+
if (options[i] === '--json') {
|
|
43
|
+
jsonOutput = true;
|
|
44
|
+
} else if (options[i] === '--html') {
|
|
45
|
+
const htmlPath = options[i + 1] || 'muaddib-report.html';
|
|
46
|
+
// CLI-001: Block path traversal
|
|
47
|
+
if (htmlPath.includes('..')) {
|
|
48
|
+
console.error('[ERROR] --html path must not contain path traversal (..)');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
htmlOutput = htmlPath;
|
|
52
|
+
i++;
|
|
53
|
+
} else if (options[i] === '--sarif') {
|
|
54
|
+
const sarifPath = options[i + 1] || 'muaddib-results.sarif';
|
|
55
|
+
// CLI-001: Block path traversal
|
|
56
|
+
if (sarifPath.includes('..')) {
|
|
57
|
+
console.error('[ERROR] --sarif path must not contain path traversal (..)');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
sarifOutput = sarifPath;
|
|
61
|
+
i++;
|
|
62
|
+
} else if (options[i] === '--explain') {
|
|
63
|
+
explainMode = true;
|
|
64
|
+
} else if (options[i] === '--fail-on') {
|
|
65
|
+
const val = (options[i + 1] || 'high').toLowerCase();
|
|
66
|
+
const validLevels = ['critical', 'high', 'medium', 'low'];
|
|
67
|
+
if (!validLevels.includes(val)) {
|
|
68
|
+
console.error(`[ERROR] --fail-on must be one of: ${validLevels.join(', ')} (got: "${val}")`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
failLevel = val;
|
|
72
|
+
i++;
|
|
73
|
+
} else if (options[i] === '--webhook') {
|
|
74
|
+
const rawUrl = options[i + 1];
|
|
75
|
+
// CLI-002: Validate webhook URL (HTTPS only, no private IPs)
|
|
76
|
+
if (rawUrl) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = new URL(rawUrl);
|
|
79
|
+
if (parsed.protocol !== 'https:') {
|
|
80
|
+
console.error('[ERROR] --webhook URL must use HTTPS');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const host = parsed.hostname.toLowerCase();
|
|
84
|
+
if (/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|0\.|169\.254\.|localhost$|::1$)/.test(host)) {
|
|
85
|
+
console.error('[ERROR] --webhook URL must not point to a private/local address');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
console.error('[ERROR] --webhook URL is invalid');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
webhookUrl = rawUrl;
|
|
94
|
+
i++;
|
|
95
|
+
} else if (options[i] === '--exclude') {
|
|
96
|
+
if (options[i + 1] && !options[i + 1].startsWith('-')) {
|
|
97
|
+
excludeDirs.push(options[i + 1]);
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
} else if (options[i] === '--entropy-threshold') {
|
|
101
|
+
const val = parseFloat(options[i + 1]);
|
|
102
|
+
if (!isNaN(val) && val > 0 && val <= 8) {
|
|
103
|
+
entropyThreshold = val;
|
|
104
|
+
} else {
|
|
105
|
+
console.error('[ERROR] --entropy-threshold must be a number between 0 and 8');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
i++;
|
|
109
|
+
} else if (options[i] === '--paranoid') {
|
|
110
|
+
paranoidMode = true;
|
|
111
|
+
} else if (options[i] === '--temporal-full') {
|
|
112
|
+
temporalFullMode = true;
|
|
113
|
+
} else if (options[i] === '--temporal-ast') {
|
|
114
|
+
temporalAstMode = true;
|
|
115
|
+
} else if (options[i] === '--temporal-publish') {
|
|
116
|
+
temporalPublishMode = true;
|
|
117
|
+
} else if (options[i] === '--temporal-maintainer') {
|
|
118
|
+
temporalMaintainerMode = true;
|
|
119
|
+
} else if (options[i] === '--breakdown') {
|
|
120
|
+
breakdownMode = true;
|
|
121
|
+
} else if (options[i] === '--no-deobfuscate') {
|
|
122
|
+
noDeobfuscate = true;
|
|
123
|
+
} else if (options[i] === '--no-module-graph') {
|
|
124
|
+
noModuleGraph = true;
|
|
125
|
+
} else if (options[i] === '--temporal') {
|
|
126
|
+
temporalMode = true;
|
|
127
|
+
} else if (options[i] === '--limit') {
|
|
128
|
+
const val = parseInt(options[i + 1], 10);
|
|
129
|
+
if (!isNaN(val) && val > 0) {
|
|
130
|
+
feedLimit = val;
|
|
131
|
+
}
|
|
132
|
+
i++;
|
|
133
|
+
} else if (options[i] === '--severity') {
|
|
134
|
+
feedSeverity = options[i + 1] || null;
|
|
135
|
+
i++;
|
|
136
|
+
} else if (options[i] === '--since') {
|
|
137
|
+
feedSince = options[i + 1] || null;
|
|
138
|
+
i++;
|
|
139
|
+
} else if (options[i] === '--port') {
|
|
140
|
+
const val = parseInt(options[i + 1], 10);
|
|
141
|
+
if (!isNaN(val) && val >= 1 && val <= 65535) {
|
|
142
|
+
servePort = val;
|
|
143
|
+
} else {
|
|
144
|
+
console.error('[ERROR] --port must be a number between 1 and 65535');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
i++;
|
|
148
|
+
} else if (options[i] === '--strict') {
|
|
149
|
+
// Sandbox strict mode flag (parsed here, used by sandbox commands)
|
|
150
|
+
} else if (options[i] === '--no-canary') {
|
|
151
|
+
// Sandbox canary disable flag (parsed here, used by sandbox commands)
|
|
152
|
+
} else if (!options[i].startsWith('-')) {
|
|
153
|
+
target = options[i];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Version check (truly non-blocking, skip for machine-readable output)
|
|
158
|
+
if (!jsonOutput && !sarifOutput && command !== 'feed' && command !== 'serve') {
|
|
159
|
+
try {
|
|
160
|
+
const currentVersion = require('../package.json').version;
|
|
161
|
+
exec('npm view muaddib-scanner version', { timeout: 5000 }, (err, stdout) => {
|
|
162
|
+
if (err) return; // No network or npm unavailable
|
|
163
|
+
const latest = (stdout || '').toString().trim();
|
|
164
|
+
if (!latest || latest === currentVersion) return;
|
|
165
|
+
// Semver comparison: only notify if remote is strictly newer
|
|
166
|
+
const parse = v => v.split('.').map(Number);
|
|
167
|
+
const [cM, cm, cp] = parse(currentVersion);
|
|
168
|
+
const [lM, lm, lp] = parse(latest);
|
|
169
|
+
const isNewer = lM > cM || (lM === cM && (lm > cm || (lm === cm && lp > cp)));
|
|
170
|
+
if (isNewer) {
|
|
171
|
+
console.log(`\n[UPDATE] New version available: ${currentVersion} -> ${latest}`);
|
|
172
|
+
console.log(` Run: npm install -g muaddib-scanner@latest\n`);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
// Skip silently
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Interactive menu
|
|
181
|
+
async function interactiveMenu() {
|
|
182
|
+
const { select, input, confirm } = await import('@inquirer/prompts');
|
|
183
|
+
|
|
184
|
+
console.log(`
|
|
185
|
+
╔═══════════════════════════════════════════════╗
|
|
186
|
+
║ MUAD'DIB - npm & PyPI Supply Chain Hunter ║
|
|
187
|
+
║ "The worms must die." ║
|
|
188
|
+
╚═══════════════════════════════════════════════╝
|
|
189
|
+
`);
|
|
190
|
+
|
|
191
|
+
const action = await select({
|
|
192
|
+
message: 'What do you want to do?',
|
|
193
|
+
choices: [
|
|
194
|
+
{ name: 'Scan a project', value: 'scan' },
|
|
195
|
+
{ name: 'Scan with paranoid mode', value: 'scan-paranoid' },
|
|
196
|
+
{ name: 'Compare with previous version (diff)', value: 'diff' },
|
|
197
|
+
{ name: 'Install packages (safe)', value: 'install' },
|
|
198
|
+
{ name: 'Watch a project (real-time)', value: 'watch' },
|
|
199
|
+
{ name: 'Start daemon', value: 'daemon' },
|
|
200
|
+
{ name: 'Setup git hooks', value: 'init-hooks' },
|
|
201
|
+
{ name: 'Update IOCs', value: 'update' },
|
|
202
|
+
{ name: 'Scrape new IOCs', value: 'scrape' },
|
|
203
|
+
{ name: 'Sandbox analysis', value: 'sandbox' },
|
|
204
|
+
{ name: 'Threat feed (JSON)', value: 'feed' },
|
|
205
|
+
{ name: 'Threat feed server', value: 'serve' },
|
|
206
|
+
{ name: 'Quit', value: 'quit' }
|
|
207
|
+
]
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (action === 'quit') {
|
|
211
|
+
console.log('Bye!');
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (action === 'scan' || action === 'scan-paranoid') {
|
|
216
|
+
const path = await input({
|
|
217
|
+
message: 'Project path:',
|
|
218
|
+
default: '.'
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const outputFormat = await select({
|
|
222
|
+
message: 'Output format:',
|
|
223
|
+
choices: [
|
|
224
|
+
{ name: 'Console (default)', value: 'console' },
|
|
225
|
+
{ name: 'JSON', value: 'json' },
|
|
226
|
+
{ name: 'HTML', value: 'html' },
|
|
227
|
+
{ name: 'SARIF (GitHub Security)', value: 'sarif' }
|
|
228
|
+
]
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const opts = {
|
|
232
|
+
json: outputFormat === 'json',
|
|
233
|
+
html: outputFormat === 'html' ? 'muaddib-report.html' : null,
|
|
234
|
+
sarif: outputFormat === 'sarif' ? 'muaddib-results.sarif' : null,
|
|
235
|
+
explain: true,
|
|
236
|
+
failLevel: 'high',
|
|
237
|
+
paranoid: action === 'scan-paranoid'
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const exitCode = await run(path, opts);
|
|
241
|
+
process.exit(exitCode);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (action === 'install') {
|
|
245
|
+
const pkgInput = await input({
|
|
246
|
+
message: 'Package(s) to install (space-separated):'
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const packages = pkgInput.split(' ').filter(p => p.trim());
|
|
250
|
+
if (packages.length === 0) {
|
|
251
|
+
console.log('No packages specified.');
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const result = await safeInstall(packages, {});
|
|
256
|
+
process.exit(result.blocked ? 1 : 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (action === 'watch') {
|
|
260
|
+
const path = await input({
|
|
261
|
+
message: 'Project path:',
|
|
262
|
+
default: '.'
|
|
263
|
+
});
|
|
264
|
+
watch(path);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (action === 'daemon') {
|
|
268
|
+
const useWebhook = await confirm({
|
|
269
|
+
message: 'Configure Discord/Slack webhook?',
|
|
270
|
+
default: false
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
let webhook = null;
|
|
274
|
+
if (useWebhook) {
|
|
275
|
+
webhook = await input({
|
|
276
|
+
message: 'Webhook URL:'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
startDaemon({ webhook });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (action === 'update') {
|
|
283
|
+
await updateIOCs();
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (action === 'scrape') {
|
|
288
|
+
const result = await runScraper();
|
|
289
|
+
console.log(`[OK] ${result.added} new IOCs (total: ${result.total})`);
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (action === 'sandbox') {
|
|
294
|
+
const packageName = await input({
|
|
295
|
+
message: 'Package name to analyze:'
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (!packageName.trim()) {
|
|
299
|
+
console.log('No package specified.');
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const useStrict = await confirm({
|
|
304
|
+
message: 'Enable strict mode? (blocks non-essential network)',
|
|
305
|
+
default: false
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await buildSandboxImage();
|
|
309
|
+
const results = await runSandbox(packageName.trim(), { strict: useStrict });
|
|
310
|
+
if (results.raw_report) {
|
|
311
|
+
console.log(generateNetworkReport(results.raw_report));
|
|
312
|
+
}
|
|
313
|
+
process.exit(results.suspicious ? 1 : 0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (action === 'feed') {
|
|
317
|
+
const { getFeed } = require('../src/threat-feed.js');
|
|
318
|
+
const result = getFeed();
|
|
319
|
+
console.log(JSON.stringify(result, null, 2));
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (action === 'serve') {
|
|
324
|
+
const { startServer } = require('../src/serve.js');
|
|
325
|
+
startServer({ port: 3000 });
|
|
326
|
+
// Server runs indefinitely
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (action === 'diff') {
|
|
330
|
+
const baseRef = await input({
|
|
331
|
+
message: 'Compare with (commit/tag/branch):',
|
|
332
|
+
default: 'HEAD~1'
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const projectPath = await input({
|
|
336
|
+
message: 'Project path:',
|
|
337
|
+
default: '.'
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const exitCode = await diff(projectPath, baseRef, { explain: true });
|
|
341
|
+
process.exit(exitCode);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (action === 'init-hooks') {
|
|
345
|
+
const hookMode = await select({
|
|
346
|
+
message: 'Hook mode:',
|
|
347
|
+
choices: [
|
|
348
|
+
{ name: 'Scan all threats', value: 'scan' },
|
|
349
|
+
{ name: 'Diff only (block only NEW threats)', value: 'diff' }
|
|
350
|
+
]
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const hookType = await select({
|
|
354
|
+
message: 'Hook system:',
|
|
355
|
+
choices: [
|
|
356
|
+
{ name: 'Auto-detect', value: 'auto' },
|
|
357
|
+
{ name: 'Husky', value: 'husky' },
|
|
358
|
+
{ name: 'pre-commit framework', value: 'pre-commit' },
|
|
359
|
+
{ name: 'Native git hooks', value: 'git' }
|
|
360
|
+
]
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
await initHooks('.', { type: hookType, mode: hookMode });
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const helpText = `
|
|
369
|
+
MUAD'DIB - npm & PyPI Supply Chain Threat Hunter
|
|
370
|
+
|
|
371
|
+
Usage:
|
|
372
|
+
muaddib Interactive mode
|
|
373
|
+
muaddib scan [path] [options] Scan a project
|
|
374
|
+
muaddib diff <ref> [path] Compare threats with a previous version
|
|
375
|
+
muaddib install <pkg> [options] Safe install (scan before install)
|
|
376
|
+
muaddib watch [path] Watch in real-time
|
|
377
|
+
muaddib daemon [options] Start daemon
|
|
378
|
+
muaddib init-hooks [options] Setup git pre-commit hooks
|
|
379
|
+
muaddib remove-hooks [path] Remove MUAD'DIB git hooks
|
|
380
|
+
muaddib update Update IOCs
|
|
381
|
+
muaddib scrape Scrape new IOCs
|
|
382
|
+
muaddib sandbox <pkg> [--strict] [--no-canary] Analyze in isolated Docker container
|
|
383
|
+
muaddib sandbox-report <pkg> Sandbox + detailed network report
|
|
384
|
+
muaddib version Show version
|
|
385
|
+
|
|
386
|
+
Replay Options:
|
|
387
|
+
--verbose Show detailed findings per attack
|
|
388
|
+
--json Machine-readable JSON output
|
|
389
|
+
GT-NNN Replay single attack by ID
|
|
390
|
+
|
|
391
|
+
Diff Examples:
|
|
392
|
+
muaddib diff HEAD~1 Compare with previous commit
|
|
393
|
+
muaddib diff v1.2.0 Compare with tag
|
|
394
|
+
muaddib diff main Compare with branch
|
|
395
|
+
muaddib diff abc1234 ./myproject Compare specific commit
|
|
396
|
+
|
|
397
|
+
Init-hooks Options:
|
|
398
|
+
--type [auto|husky|pre-commit|git] Hook system (default: auto)
|
|
399
|
+
--mode [scan|diff] scan=all threats, diff=new only
|
|
400
|
+
|
|
401
|
+
Options:
|
|
402
|
+
--json JSON output
|
|
403
|
+
--html [file] HTML report
|
|
404
|
+
--sarif [file] SARIF report (GitHub Security)
|
|
405
|
+
--explain Detailed explanations
|
|
406
|
+
--breakdown Show score breakdown by threat
|
|
407
|
+
--fail-on [level] Fail level (critical|high|medium|low)
|
|
408
|
+
--webhook [url] Discord/Slack webhook
|
|
409
|
+
--paranoid Ultra-strict mode
|
|
410
|
+
--temporal Detect sudden lifecycle script changes (network requests per package)
|
|
411
|
+
--temporal-ast Detect sudden dangerous API additions via AST diff (downloads tarballs)
|
|
412
|
+
--temporal-publish Detect publish frequency anomalies (bursts, dormant spikes)
|
|
413
|
+
--temporal-maintainer Detect maintainer changes (new maintainer, account takeover)
|
|
414
|
+
--temporal-full All temporal analyses (lifecycle + AST + publish + maintainer)
|
|
415
|
+
--no-canary Disable honey token injection in sandbox
|
|
416
|
+
--no-deobfuscate Disable deobfuscation pre-processing
|
|
417
|
+
--no-module-graph Disable cross-file dataflow analysis
|
|
418
|
+
--exclude [dir] Exclude directory from scan (repeatable)
|
|
419
|
+
--limit [n] Limit feed entries (default: 50)
|
|
420
|
+
--severity [level] Filter by severity (CRITICAL|HIGH|MEDIUM|LOW)
|
|
421
|
+
--since [date] Filter detections after date (ISO 8601)
|
|
422
|
+
--port [n] HTTP server port (default: 3000, serve only)
|
|
423
|
+
--entropy-threshold [n] Custom string-level entropy threshold (default: 5.5)
|
|
424
|
+
--save-dev, -D Install as dev dependency
|
|
425
|
+
-g, --global Install globally
|
|
426
|
+
--force Force install despite threats
|
|
427
|
+
`;
|
|
428
|
+
|
|
429
|
+
// Main
|
|
430
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
431
|
+
const pkg = require('../package.json');
|
|
432
|
+
console.log(`muaddib-scanner v${pkg.version}`);
|
|
433
|
+
process.exit(0);
|
|
434
|
+
} else if (!command || command === '--help' || command === '-h') {
|
|
435
|
+
if (command === '--help' || command === '-h') {
|
|
436
|
+
console.log(helpText);
|
|
437
|
+
process.exit(0);
|
|
438
|
+
}
|
|
439
|
+
interactiveMenu().catch(err => {
|
|
440
|
+
console.error('[ERROR]', err.message);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
});
|
|
443
|
+
} else if (command === 'scan') {
|
|
444
|
+
if (options.includes('--help') || options.includes('-h')) {
|
|
445
|
+
console.log(helpText);
|
|
446
|
+
process.exit(0);
|
|
447
|
+
}
|
|
448
|
+
run(target, {
|
|
449
|
+
json: jsonOutput,
|
|
450
|
+
html: htmlOutput,
|
|
451
|
+
sarif: sarifOutput,
|
|
452
|
+
explain: explainMode,
|
|
453
|
+
failLevel: failLevel,
|
|
454
|
+
webhook: webhookUrl,
|
|
455
|
+
paranoid: paranoidMode,
|
|
456
|
+
temporal: temporalMode || temporalFullMode,
|
|
457
|
+
temporalAst: temporalAstMode || temporalFullMode,
|
|
458
|
+
temporalPublish: temporalPublishMode || temporalFullMode,
|
|
459
|
+
temporalMaintainer: temporalMaintainerMode || temporalFullMode,
|
|
460
|
+
exclude: excludeDirs,
|
|
461
|
+
entropyThreshold: entropyThreshold,
|
|
462
|
+
breakdown: breakdownMode,
|
|
463
|
+
noDeobfuscate: noDeobfuscate,
|
|
464
|
+
noModuleGraph: noModuleGraph
|
|
465
|
+
}).then(exitCode => {
|
|
466
|
+
process.exit(exitCode);
|
|
467
|
+
}).catch(err => {
|
|
468
|
+
console.error('[ERROR]', err.message);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
});
|
|
471
|
+
} else if (command === 'feed') {
|
|
472
|
+
const { getFeed } = require('../src/threat-feed.js');
|
|
473
|
+
const feedOpts = {};
|
|
474
|
+
if (feedLimit) feedOpts.limit = feedLimit;
|
|
475
|
+
if (feedSeverity) feedOpts.severity = feedSeverity;
|
|
476
|
+
if (feedSince) feedOpts.since = feedSince;
|
|
477
|
+
const result = getFeed(feedOpts);
|
|
478
|
+
console.log(JSON.stringify(result, null, 2));
|
|
479
|
+
process.exit(0);
|
|
480
|
+
} else if (command === 'serve') {
|
|
481
|
+
const { startServer } = require('../src/serve.js');
|
|
482
|
+
startServer({ port: servePort || 3000 });
|
|
483
|
+
// Server runs indefinitely — no process.exit
|
|
484
|
+
} else if (command === 'watch') {
|
|
485
|
+
watch(target);
|
|
486
|
+
} else if (command === 'update') {
|
|
487
|
+
updateIOCs().then(() => {
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}).catch(err => {
|
|
490
|
+
console.error('[ERROR]', err.message);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
});
|
|
493
|
+
} else if (command === 'scrape') {
|
|
494
|
+
runScraper().then(result => {
|
|
495
|
+
console.log(`[OK] ${result.added} new IOCs (total: ${result.total})`);
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}).catch(err => {
|
|
498
|
+
console.error('[ERROR]', err.message);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
});
|
|
501
|
+
} else if (command === 'monitor') {
|
|
502
|
+
const testPkg = options.filter(o => !o.startsWith('-'));
|
|
503
|
+
const isTemporal = options.includes('--temporal');
|
|
504
|
+
const isTemporalAst = options.includes('--temporal-ast');
|
|
505
|
+
const isTest = options.includes('--test');
|
|
506
|
+
|
|
507
|
+
if (isTemporalAst && isTest) {
|
|
508
|
+
const actualPkg = options.filter(o => !o.startsWith('-')).pop();
|
|
509
|
+
if (!actualPkg) {
|
|
510
|
+
console.log('Usage: muaddib monitor --temporal-ast --test <package-name>');
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
const { detectSuddenAstChanges } = require('../src/temporal-ast-diff.js');
|
|
514
|
+
console.log(`[TEMPORAL-AST] Analyzing ${actualPkg}...\n`);
|
|
515
|
+
detectSuddenAstChanges(actualPkg).then(result => {
|
|
516
|
+
console.log(`Package: ${result.packageName}`);
|
|
517
|
+
console.log(`Latest version: ${result.latestVersion || 'N/A'}`);
|
|
518
|
+
console.log(`Previous version: ${result.previousVersion || 'N/A'}`);
|
|
519
|
+
console.log(`Suspicious: ${result.suspicious ? 'YES' : 'NO'}`);
|
|
520
|
+
if (result.metadata.latestPublishedAt) {
|
|
521
|
+
console.log(`Published: ${result.metadata.latestPublishedAt}`);
|
|
522
|
+
}
|
|
523
|
+
if (result.findings.length > 0) {
|
|
524
|
+
console.log(`\nFindings:`);
|
|
525
|
+
for (const f of result.findings) {
|
|
526
|
+
console.log(` [${f.severity}] ${f.pattern}: ${f.description}`);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
console.log(`\nNo dangerous API changes detected between the last two versions.`);
|
|
530
|
+
}
|
|
531
|
+
process.exit(result.suspicious ? 1 : 0);
|
|
532
|
+
}).catch(err => {
|
|
533
|
+
console.error(`[ERROR] ${err.message}`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
});
|
|
536
|
+
} else if (isTemporal && isTest && testPkg.length > 0) {
|
|
537
|
+
const { detectSuddenLifecycleChange } = require('../src/temporal-analysis.js');
|
|
538
|
+
const pkgName = testPkg[testPkg.indexOf('--test') !== -1 ? testPkg.length - 1 : 0] || testPkg[0];
|
|
539
|
+
// Find the package name: it's the non-flag argument
|
|
540
|
+
const actualPkg = options.filter(o => !o.startsWith('-')).pop();
|
|
541
|
+
if (!actualPkg) {
|
|
542
|
+
console.log('Usage: muaddib monitor --temporal --test <package-name>');
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
console.log(`[TEMPORAL] Analyzing ${actualPkg}...\n`);
|
|
546
|
+
detectSuddenLifecycleChange(actualPkg).then(result => {
|
|
547
|
+
console.log(`Package: ${result.packageName}`);
|
|
548
|
+
console.log(`Latest version: ${result.latestVersion || 'N/A'}`);
|
|
549
|
+
console.log(`Previous version: ${result.previousVersion || 'N/A'}`);
|
|
550
|
+
console.log(`Suspicious: ${result.suspicious ? 'YES' : 'NO'}`);
|
|
551
|
+
if (result.metadata.note) {
|
|
552
|
+
console.log(`Note: ${result.metadata.note}`);
|
|
553
|
+
}
|
|
554
|
+
if (result.metadata.latestPublishedAt) {
|
|
555
|
+
console.log(`Published: ${result.metadata.latestPublishedAt}`);
|
|
556
|
+
}
|
|
557
|
+
if (result.metadata.maintainers && result.metadata.maintainers.length > 0) {
|
|
558
|
+
const names = result.metadata.maintainers.map(m => m.name || m.email).join(', ');
|
|
559
|
+
console.log(`Maintainers: ${names}`);
|
|
560
|
+
}
|
|
561
|
+
if (result.findings.length > 0) {
|
|
562
|
+
console.log(`\nFindings:`);
|
|
563
|
+
for (const f of result.findings) {
|
|
564
|
+
const action = f.type === 'lifecycle_added' ? 'ADDED' : f.type === 'lifecycle_modified' ? 'MODIFIED' : 'REMOVED';
|
|
565
|
+
const value = f.type === 'lifecycle_modified' ? f.newValue : f.value;
|
|
566
|
+
console.log(` [${f.severity}] ${f.script} script ${action}: "${value}"`);
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
console.log(`\nNo lifecycle script changes detected between the last two versions.`);
|
|
570
|
+
}
|
|
571
|
+
process.exit(result.suspicious ? 1 : 0);
|
|
572
|
+
}).catch(err => {
|
|
573
|
+
console.error(`[ERROR] ${err.message}`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
});
|
|
576
|
+
} else if (isTemporal && isTest) {
|
|
577
|
+
console.log('Usage: muaddib monitor --temporal --test <package-name>');
|
|
578
|
+
process.exit(1);
|
|
579
|
+
} else {
|
|
580
|
+
// Start full monitor
|
|
581
|
+
const { startMonitor } = require('../src/monitor.js');
|
|
582
|
+
const monitorOpts = {
|
|
583
|
+
verbose: options.includes('--verbose')
|
|
584
|
+
};
|
|
585
|
+
startMonitor(monitorOpts).catch(err => {
|
|
586
|
+
console.error('[ERROR]', err.message);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
} else if (command === 'daemon') {
|
|
591
|
+
startDaemon({ webhook: webhookUrl });
|
|
592
|
+
} else if (command === 'install' || command === 'i') {
|
|
593
|
+
const packages = options.filter(o => !o.startsWith('-'));
|
|
594
|
+
const isDev = options.includes('--save-dev') || options.includes('-D');
|
|
595
|
+
const isGlobal = options.includes('-g') || options.includes('--global');
|
|
596
|
+
const force = options.includes('--force');
|
|
597
|
+
|
|
598
|
+
if (packages.length === 0) {
|
|
599
|
+
console.log('Usage: muaddib install <package> [<package>...] [--save-dev] [-g] [--force]');
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
safeInstall(packages, { isDev, isGlobal, force }).then(result => {
|
|
604
|
+
if (result.blocked && !force) {
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
process.exit(0);
|
|
608
|
+
}).catch(err => {
|
|
609
|
+
console.error('[ERROR]', err.message);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
});
|
|
612
|
+
} else if (command === 'sandbox') {
|
|
613
|
+
const sandboxOpts = options.filter(o => !o.startsWith('-'));
|
|
614
|
+
const packageName = sandboxOpts[0];
|
|
615
|
+
const strict = options.includes('--strict');
|
|
616
|
+
const canary = !options.includes('--no-canary');
|
|
617
|
+
if (!packageName) {
|
|
618
|
+
console.log('Usage: muaddib sandbox <package-name> [--strict] [--no-canary]');
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
buildSandboxImage()
|
|
623
|
+
.then(() => runSandbox(packageName, { strict, canary }))
|
|
624
|
+
.then((results) => {
|
|
625
|
+
process.exit(results.suspicious ? 1 : 0);
|
|
626
|
+
})
|
|
627
|
+
.catch((err) => {
|
|
628
|
+
console.error('[ERROR]', err.message);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
});
|
|
631
|
+
} else if (command === 'sandbox-report') {
|
|
632
|
+
const sandboxOpts = options.filter(o => !o.startsWith('-'));
|
|
633
|
+
const packageName = sandboxOpts[0];
|
|
634
|
+
const strict = options.includes('--strict');
|
|
635
|
+
const canary = !options.includes('--no-canary');
|
|
636
|
+
if (!packageName) {
|
|
637
|
+
console.log('Usage: muaddib sandbox-report <package-name> [--strict] [--no-canary]');
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
buildSandboxImage()
|
|
642
|
+
.then(() => runSandbox(packageName, { strict, canary }))
|
|
643
|
+
.then((results) => {
|
|
644
|
+
if (results.raw_report) {
|
|
645
|
+
console.log(generateNetworkReport(results.raw_report));
|
|
646
|
+
}
|
|
647
|
+
process.exit(results.suspicious ? 1 : 0);
|
|
648
|
+
})
|
|
649
|
+
.catch((err) => {
|
|
650
|
+
console.error('[ERROR]', err.message);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
});
|
|
653
|
+
} else if (command === 'diff') {
|
|
654
|
+
// Parse diff arguments: muaddib diff <ref> [path] [options]
|
|
655
|
+
const diffArgs = options.filter(o => !o.startsWith('-'));
|
|
656
|
+
const baseRef = diffArgs[0];
|
|
657
|
+
const diffTarget = diffArgs[1] || '.';
|
|
658
|
+
|
|
659
|
+
if (!baseRef) {
|
|
660
|
+
showRefs('.');
|
|
661
|
+
process.exit(0);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
diff(diffTarget, baseRef, {
|
|
665
|
+
json: jsonOutput,
|
|
666
|
+
explain: explainMode,
|
|
667
|
+
failLevel: failLevel,
|
|
668
|
+
paranoid: paranoidMode
|
|
669
|
+
}).then(exitCode => {
|
|
670
|
+
process.exit(exitCode);
|
|
671
|
+
}).catch(err => {
|
|
672
|
+
console.error('[ERROR]', err.message);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
});
|
|
675
|
+
} else if (command === 'detections') {
|
|
676
|
+
const { loadDetections, getDetectionStats } = require('../src/monitor.js');
|
|
677
|
+
const wantStats = options.includes('--stats');
|
|
678
|
+
const wantJson = options.includes('--json');
|
|
679
|
+
|
|
680
|
+
if (wantJson) {
|
|
681
|
+
const data = loadDetections();
|
|
682
|
+
console.log(JSON.stringify(data, null, 2));
|
|
683
|
+
process.exit(0);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (wantStats) {
|
|
687
|
+
const s = getDetectionStats();
|
|
688
|
+
console.log('\n MUAD\'DIB Detection Stats\n');
|
|
689
|
+
console.log(` Total detections: ${s.total}`);
|
|
690
|
+
if (Object.keys(s.bySeverity).length > 0) {
|
|
691
|
+
console.log(' By severity:');
|
|
692
|
+
for (const [sev, count] of Object.entries(s.bySeverity)) {
|
|
693
|
+
console.log(` ${sev}: ${count}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (Object.keys(s.byEcosystem).length > 0) {
|
|
697
|
+
console.log(' By ecosystem:');
|
|
698
|
+
for (const [eco, count] of Object.entries(s.byEcosystem)) {
|
|
699
|
+
console.log(` ${eco}: ${count}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (s.leadTime) {
|
|
703
|
+
console.log(` Lead time (hours): avg=${s.leadTime.avg.toFixed(1)}, min=${s.leadTime.min.toFixed(1)}, max=${s.leadTime.max.toFixed(1)} (${s.leadTime.count} entries)`);
|
|
704
|
+
} else {
|
|
705
|
+
console.log(' Lead time: no advisory data yet');
|
|
706
|
+
}
|
|
707
|
+
console.log('');
|
|
708
|
+
process.exit(0);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Default: list recent detections
|
|
712
|
+
const data = loadDetections();
|
|
713
|
+
const recent = data.detections.slice(-20).reverse();
|
|
714
|
+
if (recent.length === 0) {
|
|
715
|
+
console.log('\n No detections recorded yet.\n');
|
|
716
|
+
process.exit(0);
|
|
717
|
+
}
|
|
718
|
+
console.log(`\n MUAD'DIB Recent Detections (${recent.length} of ${data.detections.length})\n`);
|
|
719
|
+
for (const d of recent) {
|
|
720
|
+
const lead = d.lead_time_hours != null ? ` | lead: ${d.lead_time_hours.toFixed(1)}h` : '';
|
|
721
|
+
console.log(` [${d.severity}] ${d.ecosystem}/${d.package}@${d.version} — ${d.first_seen_at}${lead}`);
|
|
722
|
+
console.log(` findings: ${d.findings.join(', ')}`);
|
|
723
|
+
}
|
|
724
|
+
console.log('');
|
|
725
|
+
process.exit(0);
|
|
726
|
+
} else if (command === 'stats') {
|
|
727
|
+
const { loadScanStats } = require('../src/monitor.js');
|
|
728
|
+
const wantDaily = options.includes('--daily');
|
|
729
|
+
const wantJson = options.includes('--json');
|
|
730
|
+
|
|
731
|
+
const data = loadScanStats();
|
|
732
|
+
|
|
733
|
+
if (wantJson) {
|
|
734
|
+
console.log(JSON.stringify(data, null, 2));
|
|
735
|
+
process.exit(0);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (wantDaily) {
|
|
739
|
+
const last7 = data.daily.slice(-7);
|
|
740
|
+
console.log('\n MUAD\'DIB Scan Stats — Daily Breakdown\n');
|
|
741
|
+
if (last7.length === 0) {
|
|
742
|
+
console.log(' No daily data recorded yet.\n');
|
|
743
|
+
process.exit(0);
|
|
744
|
+
}
|
|
745
|
+
console.log(' Date Scanned Clean Suspect FP Confirmed FP Rate');
|
|
746
|
+
console.log(' ' + '-'.repeat(60));
|
|
747
|
+
for (const d of last7) {
|
|
748
|
+
const fpRate = (d.fp_rate * 100).toFixed(1) + '%';
|
|
749
|
+
console.log(` ${d.date} ${String(d.scanned).padStart(5)} ${String(d.clean).padStart(5)} ${String(d.suspect).padStart(7)} ${String(d.false_positive).padStart(2)} ${String(d.confirmed).padStart(9)} ${fpRate.padStart(7)}`);
|
|
750
|
+
}
|
|
751
|
+
console.log('');
|
|
752
|
+
process.exit(0);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Default: global stats
|
|
756
|
+
const s = data.stats;
|
|
757
|
+
const globalDenom = s.false_positive + s.confirmed_malicious;
|
|
758
|
+
const globalFpRate = globalDenom > 0 ? ((s.false_positive / globalDenom) * 100).toFixed(1) + '%' : 'N/A';
|
|
759
|
+
|
|
760
|
+
console.log('\n MUAD\'DIB Scan Stats\n');
|
|
761
|
+
console.log(` Total scanned: ${s.total_scanned}`);
|
|
762
|
+
console.log(` Clean: ${s.clean}`);
|
|
763
|
+
console.log(` Suspect: ${s.suspect}`);
|
|
764
|
+
console.log(` False positives: ${s.false_positive}`);
|
|
765
|
+
console.log(` Confirmed malicious: ${s.confirmed_malicious}`);
|
|
766
|
+
console.log(` FP rate: ${globalFpRate}`);
|
|
767
|
+
console.log('');
|
|
768
|
+
process.exit(0);
|
|
769
|
+
} else if (command === 'evaluate') {
|
|
770
|
+
const { evaluate } = require('../src/commands/evaluate.js');
|
|
771
|
+
const evalOpts = { json: jsonOutput };
|
|
772
|
+
for (let i = 0; i < options.length; i++) {
|
|
773
|
+
if (options[i] === '--benign-limit' && options[i + 1]) {
|
|
774
|
+
evalOpts.benignLimit = parseInt(options[i + 1], 10);
|
|
775
|
+
i++;
|
|
776
|
+
} else if (options[i] === '--refresh-benign') {
|
|
777
|
+
evalOpts.refreshBenign = true;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
evaluate(evalOpts).then(() => {
|
|
781
|
+
process.exit(0);
|
|
782
|
+
}).catch(err => {
|
|
783
|
+
console.error('[ERROR]', err.message);
|
|
784
|
+
process.exit(1);
|
|
785
|
+
});
|
|
786
|
+
} else if (command === 'init-hooks') {
|
|
787
|
+
// Parse init-hooks arguments
|
|
788
|
+
let hookType = 'auto';
|
|
789
|
+
let hookMode = 'scan';
|
|
790
|
+
|
|
791
|
+
for (let i = 0; i < options.length; i++) {
|
|
792
|
+
if (options[i] === '--type' && options[i + 1]) {
|
|
793
|
+
hookType = options[i + 1];
|
|
794
|
+
i++;
|
|
795
|
+
} else if (options[i] === '--mode' && options[i + 1]) {
|
|
796
|
+
hookMode = options[i + 1];
|
|
797
|
+
i++;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
initHooks(target, { type: hookType, mode: hookMode }).then(success => {
|
|
802
|
+
process.exit(success ? 0 : 1);
|
|
803
|
+
}).catch(err => {
|
|
804
|
+
console.error('[ERROR]', err.message);
|
|
805
|
+
process.exit(1);
|
|
806
|
+
});
|
|
807
|
+
} else if (command === 'remove-hooks') {
|
|
808
|
+
removeHooks(target).then(success => {
|
|
809
|
+
process.exit(success ? 0 : 1);
|
|
810
|
+
}).catch(err => {
|
|
811
|
+
console.error('[ERROR]', err.message);
|
|
812
|
+
process.exit(1);
|
|
813
|
+
});
|
|
814
|
+
} else if (command === 'replay' || command === 'ground-truth') {
|
|
815
|
+
const { replay } = require('../tests/ground-truth/replay.js');
|
|
816
|
+
const replayOpts = {};
|
|
817
|
+
for (const o of options) {
|
|
818
|
+
if (o === '--verbose' || o === '-v') replayOpts.verbose = true;
|
|
819
|
+
else if (o === '--json') replayOpts.json = true;
|
|
820
|
+
else if (o.startsWith('GT-')) replayOpts.filterId = o;
|
|
821
|
+
}
|
|
822
|
+
replay(replayOpts).then(result => {
|
|
823
|
+
if (!replayOpts.json) {
|
|
824
|
+
process.exit(result.missed > 0 ? 1 : 0);
|
|
825
|
+
}
|
|
826
|
+
process.exit(0);
|
|
827
|
+
}).catch(err => {
|
|
828
|
+
console.error('[ERROR]', err.message);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
});
|
|
831
|
+
} else if (command === 'report') {
|
|
832
|
+
// Hidden/internal — not in --help
|
|
833
|
+
if (options.includes('--now')) {
|
|
834
|
+
const { sendReportNow } = require('../src/monitor.js');
|
|
835
|
+
sendReportNow().then(result => {
|
|
836
|
+
const color = process.stdout.isTTY;
|
|
837
|
+
if (result.sent) {
|
|
838
|
+
const check = color ? '\x1b[32m\u2713\x1b[0m' : '\u2713';
|
|
839
|
+
console.log(`\n ${check} ${result.message}\n`);
|
|
840
|
+
} else {
|
|
841
|
+
const warn = color ? '\x1b[33m!\x1b[0m' : '!';
|
|
842
|
+
console.log(`\n ${warn} ${result.message}\n`);
|
|
843
|
+
}
|
|
844
|
+
process.exit(result.sent ? 0 : 1);
|
|
845
|
+
}).catch(err => {
|
|
846
|
+
console.error('[ERROR]', err.message);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
});
|
|
849
|
+
} else if (options.includes('--status')) {
|
|
850
|
+
const { getReportStatus } = require('../src/monitor.js');
|
|
851
|
+
const status = getReportStatus();
|
|
852
|
+
console.log('\n MUAD\'DIB Report Status\n');
|
|
853
|
+
console.log(` Last report sent: ${status.lastDailyReportDate || 'Never'}`);
|
|
854
|
+
console.log(` Packages scanned since: ${status.scannedSince}`);
|
|
855
|
+
console.log(` Next scheduled report: ${status.nextReport}`);
|
|
856
|
+
console.log('');
|
|
857
|
+
process.exit(0);
|
|
858
|
+
} else {
|
|
859
|
+
console.log('Usage: muaddib report --now | --status');
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
} else if (command === 'help') {
|
|
863
|
+
console.log(helpText);
|
|
864
|
+
process.exit(0);
|
|
865
|
+
} else {
|
|
866
|
+
console.log(`Unknown command: ${String(command).replace(/[\x00-\x1f\x7f-\x9f]/g, '')}`);
|
|
867
|
+
console.log('Type "muaddib help" to see available commands.');
|
|
868
|
+
process.exit(1);
|
|
850
869
|
}
|