muaddib-scanner 2.10.21 → 2.10.23

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/README.md CHANGED
@@ -30,7 +30,7 @@
30
30
 
31
31
  npm and PyPI supply-chain attacks are exploding. Shai-Hulud compromised 25K+ repos in 2025. Existing tools detect threats but don't help you respond.
32
32
 
33
- MUAD'DIB combines **14 parallel scanners** (162 detection rules), a **deobfuscation engine**, **inter-module dataflow analysis**, **compound scoring**, **ML classifiers** (XGBoost), and Docker sandbox to detect known threats and suspicious behavioral patterns in npm and PyPI packages.
33
+ MUAD'DIB combines **14 parallel scanners** (176 detection rules), a **deobfuscation engine**, **inter-module dataflow analysis**, **compound scoring**, **ML classifiers** (XGBoost), and Docker sandbox to detect known threats and suspicious behavioral patterns in npm and PyPI packages.
34
34
 
35
35
  ---
36
36
 
@@ -136,7 +136,7 @@ Ultra-strict detection with lower tolerance. Detects any network access, subproc
136
136
  muaddib scan . --webhook "https://discord.com/api/webhooks/..."
137
137
  ```
138
138
 
139
- Strict filtering (v2.1.2): alerts only for IOC matches, sandbox-confirmed threats, or canary token exfiltration. Priority triage (v2.10.5): P1 (red, IOC/sandbox/canary), P2 (orange, high-score/compounds), P3 (yellow, rest).
139
+ Strict filtering (v2.1.2): alerts only for IOC matches, sandbox-confirmed threats, or canary token exfiltration. Priority triage (v2.10.21): P1 (red, IOC/sandbox/canary), P2 (orange, high-score/compounds), P3 (yellow, rest).
140
140
 
141
141
  ### Behavioral anomaly detection (v2.0)
142
142
 
@@ -195,9 +195,9 @@ muaddib replay # Ground truth validation (46/49 TPR)
195
195
  | GitHub Actions | Shai-Hulud backdoor detection |
196
196
  | Hash Scanner | Known malicious file hashes |
197
197
 
198
- ### 162 detection rules
198
+ ### 176 detection rules
199
199
 
200
- All rules are mapped to MITRE ATT&CK techniques. See [SECURITY.md](SECURITY.md#detection-rules-v2105) for the complete rules reference.
200
+ All rules are mapped to MITRE ATT&CK techniques. See [SECURITY.md](SECURITY.md#detection-rules-v21021) for the complete rules reference.
201
201
 
202
202
  ### Detected campaigns
203
203
 
@@ -271,7 +271,7 @@ With pre-commit framework:
271
271
  ```yaml
272
272
  repos:
273
273
  - repo: https://github.com/DNSZLSK/muad-dib
274
- rev: v2.6.6
274
+ rev: v2.10.21
275
275
  hooks:
276
276
  - id: muaddib-scan
277
277
  ```
@@ -284,11 +284,11 @@ repos:
284
284
  |--------|--------|---------|
285
285
  | **Wild TPR** (Datadog 17K) | **92.8%** (13,538/14,587 in-scope) | 17,922 packages. 3,335 skipped (no JS). By category: compromised_lib 97.8%, malicious_intent 92.1% |
286
286
  | **TPR** (Ground Truth) | **93.9%** (46/49) | 51 real attacks. 3 out-of-scope: browser-only |
287
- | **FPR** (Benign curated) | **11.0%** (58/529) | 529 npm packages, real source via `npm pack` |
287
+ | **FPR** (Benign curated) | **10.6%** (56/529) | 529 npm packages, real source via `npm pack` |
288
288
  | **FPR** (Benign random) | **7.5%** (15/200) | 200 random npm packages, stratified sampling |
289
- | **ADR** (Adversarial + Holdout) | **96.3%** (103/107) | 67 adversarial + 40 holdout (107 available on disk), global threshold=20 |
289
+ | **ADR** (Adversarial + Holdout) | **94.0%** (101/107) | 67 adversarial + 40 holdout (107 available on disk), global threshold=20 |
290
290
 
291
- **2643 tests** across 57 files. **162 rules** (157 RULES + 5 PARANOID).
291
+ **2793 tests** across 57 files. **176 rules** (171 RULES + 5 PARANOID).
292
292
 
293
293
  > **Methodology caveats:**
294
294
  > - TPR measured on 49 Node.js attack samples (3 browser-only excluded from 51 total)
@@ -329,11 +329,11 @@ npm test
329
329
 
330
330
  ### Testing
331
331
 
332
- - **2643 tests** across 57 modular test files
332
+ - **2793 tests** across 57 modular test files
333
333
  - **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
334
334
  - **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
335
335
  - **Ground truth validation** - 51 real-world attacks (93.9% TPR)
336
- - **False positive validation** - 11.0% FPR on 529 curated npm packages, 7.5% on 200 random
336
+ - **False positive validation** - 10.6% FPR on 529 curated npm packages, 7.5% on 200 random
337
337
 
338
338
  ---
339
339
 
@@ -351,7 +351,7 @@ npm test
351
351
  - [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) - Experimental protocol, holdout scores
352
352
  - [Threat Model](docs/threat-model.md) - What MUAD'DIB detects and doesn't detect
353
353
  - [Adversarial Evaluation](ADVERSARIAL.md) - Red team samples and ADR results
354
- - [Security Policy](SECURITY.md) - Detection rules reference (162 rules)
354
+ - [Security Policy](SECURITY.md) - Detection rules reference (176 rules)
355
355
  - [Security Audit](docs/SECURITY_AUDIT.md) - Bypass validation report
356
356
  - [FP Analysis](docs/EVALUATION.md) - Historical false positive analysis
357
357
 
package/bin/muaddib.js CHANGED
@@ -1,4 +1,26 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // Auto-respawn with memory flags for evaluate command (OOM prevention)
4
+ if (process.argv[2] === 'evaluate') {
5
+ const hasMaxOld = process.execArgv.some(a => a.includes('--max-old-space-size'));
6
+ const hasGC = process.execArgv.some(a => a === '--expose-gc');
7
+ if (!hasMaxOld || !hasGC) {
8
+ const { execFileSync } = require('child_process');
9
+ const flags = [];
10
+ if (!hasMaxOld) flags.push('--max-old-space-size=8192');
11
+ if (!hasGC) flags.push('--expose-gc');
12
+ try {
13
+ execFileSync(process.execPath, [...flags, __filename, ...process.argv.slice(2)], {
14
+ stdio: 'inherit',
15
+ env: process.env
16
+ });
17
+ process.exit(0);
18
+ } catch (e) {
19
+ process.exit(e.status || 1);
20
+ }
21
+ }
22
+ }
23
+
2
24
  const { execFile } = require('child_process');
3
25
  const { run } = require('../src/index.js');
4
26
  const { updateIOCs } = require('../src/ioc/updater.js');
@@ -13,6 +35,9 @@ const args = process.argv.slice(2);
13
35
  const command = args[0];
14
36
  const options = args.slice(1);
15
37
 
38
+ // Helper: detect --help / -h in options
39
+ const wantHelp = options.includes('--help') || options.includes('-h');
40
+
16
41
  // Parse options
17
42
  let target = '.';
18
43
  let jsonOutput = false;
@@ -394,19 +419,45 @@ const helpText = `
394
419
  muaddib diff <ref> [path] Compare threats with a previous version
395
420
  muaddib install <pkg> [options] Safe install (scan before install)
396
421
  muaddib watch [path] Watch in real-time
422
+ muaddib sandbox <pkg> [options] Analyze in isolated Docker container
423
+ muaddib sandbox-report <pkg> Sandbox + detailed network report
424
+ muaddib evaluate [options] Run TPR/FPR/ADR evaluation suite
425
+ muaddib monitor [options] Start real-time npm/PyPI monitor
397
426
  muaddib daemon [options] Start daemon
398
427
  muaddib init-hooks [options] Setup git pre-commit hooks
399
428
  muaddib remove-hooks [path] Remove MUAD'DIB git hooks
400
429
  muaddib update Update IOCs
401
430
  muaddib scrape Scrape new IOCs
402
- muaddib sandbox <pkg> [--strict] [--no-canary] Analyze in isolated Docker container
403
- muaddib sandbox-report <pkg> Sandbox + detailed network report
431
+ muaddib feed [options] Threat feed (JSON)
432
+ muaddib serve [options] Threat feed HTTP server
433
+ muaddib detections [options] View monitor detections
434
+ muaddib stats [options] View scan statistics
435
+ muaddib replay [options] Replay ground-truth attacks
404
436
  muaddib version Show version
405
437
 
406
- Replay Options:
407
- --verbose Show detailed findings per attack
408
- --json Machine-readable JSON output
409
- GT-NNN Replay single attack by ID
438
+ Scan Options:
439
+ --json JSON output
440
+ --html [file] HTML report (default: muaddib-report.html)
441
+ --sarif [file] SARIF report (default: muaddib-results.sarif)
442
+ --explain Detailed explanations with MITRE references
443
+ --breakdown Show score breakdown by threat
444
+ --fail-on [level] Exit code threshold (critical|high|medium|low, default: high)
445
+ --webhook [url] Discord/Slack webhook (HTTPS only)
446
+ --paranoid Ultra-strict detection mode
447
+ --exclude [dir] Exclude directory from scan (repeatable)
448
+ --config [file] Custom config file (.muaddibrc.json format)
449
+ --entropy-threshold [n] Custom entropy threshold (0-8, default: 5.5)
450
+ --no-deobfuscate Disable deobfuscation pre-processing
451
+ --no-module-graph Disable cross-file dataflow analysis
452
+ --no-reachability Disable entry-point reachability analysis
453
+ --auto-sandbox Auto-trigger sandbox when static scan score >= 20 (requires Docker)
454
+
455
+ Temporal Options (scan):
456
+ --temporal Detect sudden lifecycle script changes
457
+ --temporal-ast Detect sudden dangerous API additions via AST diff
458
+ --temporal-publish Detect publish frequency anomalies
459
+ --temporal-maintainer Detect maintainer changes
460
+ --temporal-full All temporal analyses combined
410
461
 
411
462
  Diff Examples:
412
463
  muaddib diff HEAD~1 Compare with previous commit
@@ -414,40 +465,303 @@ const helpText = `
414
465
  muaddib diff main Compare with branch
415
466
  muaddib diff abc1234 ./myproject Compare specific commit
416
467
 
468
+ Install Options:
469
+ --save-dev, -D Install as dev dependency
470
+ -g, --global Install globally
471
+ --force Force install despite threats
472
+
473
+ Sandbox Options:
474
+ --local Analyze a local path instead of npm package
475
+ --strict Block non-essential network access
476
+ --no-canary Disable honey token injection
477
+
417
478
  Init-hooks Options:
418
479
  --type [auto|husky|pre-commit|git] Hook system (default: auto)
419
480
  --mode [scan|diff] scan=all threats, diff=new only
420
481
 
482
+ Evaluate Options:
483
+ --json JSON output
484
+ --benign-limit [n] Limit benign packages to scan
485
+ --refresh-benign Re-download benign packages
486
+
487
+ Feed/Serve Options:
488
+ --limit [n] Limit feed entries (default: 50)
489
+ --severity [level] Filter by severity (CRITICAL|HIGH|MEDIUM|LOW)
490
+ --since [date] Filter detections after date (ISO 8601)
491
+ --port [n] HTTP server port (default: 3000)
492
+
493
+ Monitor Options:
494
+ --verbose Show detailed output
495
+ --temporal --test <pkg> Test temporal analysis on a single package
496
+ --temporal-ast --test <pkg> Test AST diff on a single package
497
+
498
+ Detections Options:
499
+ --json JSON output
500
+ --stats Show aggregated detection statistics
501
+
502
+ Stats Options:
503
+ --json JSON output
504
+ --daily Show daily breakdown (last 7 days)
505
+
506
+ Replay Options:
507
+ --verbose Show detailed findings per attack
508
+ --json Machine-readable JSON output
509
+ GT-NNN Replay single attack by ID
510
+ `;
511
+
512
+ // Per-command help texts for subcommand --help
513
+ const commandHelp = {
514
+ scan: `
515
+ Usage: muaddib scan [path] [options]
516
+
517
+ Scan a project for supply-chain threats.
518
+
519
+ Arguments:
520
+ path Target directory to scan (default: .)
521
+
421
522
  Options:
422
523
  --json JSON output
423
- --html [file] HTML report
424
- --sarif [file] SARIF report (GitHub Security)
425
- --explain Detailed explanations
524
+ --html [file] HTML report (default: muaddib-report.html)
525
+ --sarif [file] SARIF report (default: muaddib-results.sarif)
526
+ --explain Detailed explanations with MITRE references
426
527
  --breakdown Show score breakdown by threat
427
- --fail-on [level] Fail level (critical|high|medium|low)
428
- --webhook [url] Discord/Slack webhook
429
- --paranoid Ultra-strict mode
430
- --temporal Detect sudden lifecycle script changes (network requests per package)
431
- --temporal-ast Detect sudden dangerous API additions via AST diff (downloads tarballs)
432
- --temporal-publish Detect publish frequency anomalies (bursts, dormant spikes)
433
- --temporal-maintainer Detect maintainer changes (new maintainer, account takeover)
434
- --temporal-full All temporal analyses (lifecycle + AST + publish + maintainer)
435
- --auto-sandbox Auto-trigger sandbox when static scan score >= 20 (requires Docker)
436
- --no-canary Disable honey token injection in sandbox
528
+ --fail-on [level] Exit code threshold (critical|high|medium|low, default: high)
529
+ --webhook [url] Discord/Slack webhook (HTTPS only)
530
+ --paranoid Ultra-strict detection mode
531
+ --exclude [dir] Exclude directory from scan (repeatable)
532
+ --config [file] Custom config file (.muaddibrc.json format)
533
+ --entropy-threshold [n] Custom entropy threshold (0-8, default: 5.5)
437
534
  --no-deobfuscate Disable deobfuscation pre-processing
438
535
  --no-module-graph Disable cross-file dataflow analysis
439
536
  --no-reachability Disable entry-point reachability analysis
440
- --exclude [dir] Exclude directory from scan (repeatable)
441
- --limit [n] Limit feed entries (default: 50)
442
- --severity [level] Filter by severity (CRITICAL|HIGH|MEDIUM|LOW)
443
- --since [date] Filter detections after date (ISO 8601)
444
- --port [n] HTTP server port (default: 3000, serve only)
445
- --entropy-threshold [n] Custom string-level entropy threshold (default: 5.5)
446
- --config [file] Custom config file (.muaddibrc.json format)
537
+ --auto-sandbox Auto-trigger sandbox when static scan score >= 20 (requires Docker)
538
+ --temporal Detect sudden lifecycle script changes
539
+ --temporal-ast Detect sudden dangerous API additions
540
+ --temporal-publish Detect publish frequency anomalies
541
+ --temporal-maintainer Detect maintainer changes
542
+ --temporal-full All temporal analyses combined
543
+
544
+ Examples:
545
+ muaddib scan .
546
+ muaddib scan ./my-project --explain --paranoid
547
+ muaddib scan . --json > results.json
548
+ muaddib scan . --html report.html --explain
549
+ `,
550
+ diff: `
551
+ Usage: muaddib diff <ref> [path] [options]
552
+
553
+ Compare threats between two versions of a project.
554
+ Without <ref>, shows available tags and recent commits.
555
+
556
+ Arguments:
557
+ ref Commit hash, tag, or branch to compare with
558
+ path Target directory (default: .)
559
+
560
+ Options:
561
+ --json JSON output
562
+ --explain Detailed explanations
563
+ --fail-on [level] Exit code threshold (critical|high|medium|low)
564
+ --paranoid Ultra-strict detection mode
565
+
566
+ Examples:
567
+ muaddib diff HEAD~1
568
+ muaddib diff v1.2.0
569
+ muaddib diff main ./myproject
570
+ `,
571
+ install: `
572
+ Usage: muaddib install <package> [<package>...] [options]
573
+
574
+ Scan packages for threats before installing them.
575
+
576
+ Options:
447
577
  --save-dev, -D Install as dev dependency
448
578
  -g, --global Install globally
449
- --force Force install despite threats
450
- `;
579
+ --force Force install despite detected threats
580
+
581
+ Examples:
582
+ muaddib install lodash
583
+ muaddib install express morgan --save-dev
584
+ muaddib install -g typescript
585
+ `,
586
+ sandbox: `
587
+ Usage: muaddib sandbox <package-name|path> [options]
588
+
589
+ Run dynamic analysis in an isolated Docker container.
590
+ Requires Docker to be installed and running.
591
+
592
+ Options:
593
+ --local Analyze a local path instead of npm package
594
+ --strict Block non-essential network access
595
+ --no-canary Disable honey token injection
596
+
597
+ Examples:
598
+ muaddib sandbox suspicious-pkg
599
+ muaddib sandbox ./local-pkg --local --strict
600
+ `,
601
+ 'sandbox-report': `
602
+ Usage: muaddib sandbox-report <package-name|path> [options]
603
+
604
+ Run sandbox analysis with detailed network traffic report.
605
+ Same options as 'muaddib sandbox'.
606
+ `,
607
+ evaluate: `
608
+ Usage: muaddib evaluate [options]
609
+
610
+ Run the full TPR/FPR/ADR evaluation suite against ground truth,
611
+ benign packages, and adversarial samples.
612
+
613
+ Options:
614
+ --json JSON output
615
+ --benign-limit [n] Limit benign packages to scan
616
+ --refresh-benign Re-download benign packages
617
+
618
+ Examples:
619
+ muaddib evaluate
620
+ muaddib evaluate --json > eval-results.json
621
+ `,
622
+ monitor: `
623
+ Usage: muaddib monitor [options]
624
+
625
+ Start real-time monitoring of npm/PyPI registries for
626
+ newly published malicious packages.
627
+
628
+ Options:
629
+ --verbose Show detailed output
630
+ --temporal --test <pkg> Test temporal analysis on a single package
631
+ --temporal-ast --test <pkg> Test AST diff on a single package
632
+
633
+ Examples:
634
+ muaddib monitor
635
+ muaddib monitor --verbose
636
+ muaddib monitor --temporal --test lodash
637
+ `,
638
+ feed: `
639
+ Usage: muaddib feed [options]
640
+
641
+ Output the threat detection feed as JSON.
642
+
643
+ Options:
644
+ --limit [n] Limit entries (default: 50)
645
+ --severity [level] Filter by severity (CRITICAL|HIGH|MEDIUM|LOW)
646
+ --since [date] Filter after date (ISO 8601)
647
+
648
+ Examples:
649
+ muaddib feed
650
+ muaddib feed --severity CRITICAL --limit 10
651
+ `,
652
+ serve: `
653
+ Usage: muaddib serve [options]
654
+
655
+ Start an HTTP server serving the threat feed.
656
+
657
+ Options:
658
+ --port [n] Server port (default: 3000)
659
+
660
+ Examples:
661
+ muaddib serve
662
+ muaddib serve --port 8080
663
+ `,
664
+ detections: `
665
+ Usage: muaddib detections [options]
666
+
667
+ View detections recorded by the monitor.
668
+
669
+ Options:
670
+ --json Full JSON output
671
+ --stats Show aggregated detection statistics
672
+
673
+ Examples:
674
+ muaddib detections
675
+ muaddib detections --stats
676
+ muaddib detections --json > detections.json
677
+ `,
678
+ stats: `
679
+ Usage: muaddib stats [options]
680
+
681
+ View scan statistics from the monitor.
682
+
683
+ Options:
684
+ --json JSON output
685
+ --daily Show daily breakdown (last 7 days)
686
+
687
+ Examples:
688
+ muaddib stats
689
+ muaddib stats --daily
690
+ `,
691
+ replay: `
692
+ Usage: muaddib replay [options] [GT-NNN]
693
+
694
+ Replay ground-truth attack samples to verify detection.
695
+
696
+ Options:
697
+ --verbose, -v Show detailed findings per attack
698
+ --json Machine-readable JSON output
699
+ GT-NNN Replay a single attack by ID
700
+
701
+ Examples:
702
+ muaddib replay
703
+ muaddib replay --verbose
704
+ muaddib replay GT-001
705
+ `,
706
+ 'init-hooks': `
707
+ Usage: muaddib init-hooks [options]
708
+
709
+ Setup git pre-commit hooks for automatic scanning.
710
+
711
+ Options:
712
+ --type [auto|husky|pre-commit|git] Hook system (default: auto)
713
+ --mode [scan|diff] scan=all threats, diff=new only
714
+
715
+ Examples:
716
+ muaddib init-hooks
717
+ muaddib init-hooks --type husky --mode diff
718
+ `,
719
+ 'remove-hooks': `
720
+ Usage: muaddib remove-hooks [path]
721
+
722
+ Remove MUAD'DIB git hooks from a project.
723
+ `,
724
+ watch: `
725
+ Usage: muaddib watch [path]
726
+
727
+ Watch a project directory and re-scan on file changes.
728
+
729
+ Examples:
730
+ muaddib watch
731
+ muaddib watch ./my-project
732
+ `,
733
+ daemon: `
734
+ Usage: muaddib daemon [options]
735
+
736
+ Start the MUAD'DIB daemon for background monitoring.
737
+
738
+ Options:
739
+ --webhook [url] Discord/Slack webhook URL
740
+
741
+ Examples:
742
+ muaddib daemon
743
+ muaddib daemon --webhook https://discord.com/api/webhooks/...
744
+ `,
745
+ report: `
746
+ Usage: muaddib report --now | --status
747
+
748
+ Send or check status of daily monitor reports.
749
+
750
+ Options:
751
+ --now Send report immediately
752
+ --status Show report status
753
+ `,
754
+ };
755
+
756
+ // Show per-command help or global help
757
+ function showHelp(cmd) {
758
+ if (cmd && commandHelp[cmd]) {
759
+ console.log(commandHelp[cmd]);
760
+ } else {
761
+ console.log(helpText);
762
+ }
763
+ process.exit(0);
764
+ }
451
765
 
452
766
  // Main
453
767
  if (command === 'version' || command === '--version' || command === '-v') {
@@ -456,18 +770,14 @@ if (command === 'version' || command === '--version' || command === '-v') {
456
770
  process.exit(0);
457
771
  } else if (!command || command === '--help' || command === '-h') {
458
772
  if (command === '--help' || command === '-h') {
459
- console.log(helpText);
460
- process.exit(0);
773
+ showHelp();
461
774
  }
462
775
  interactiveMenu().catch(err => {
463
776
  console.error('[ERROR]', err.message);
464
777
  process.exit(1);
465
778
  });
466
779
  } else if (command === 'scan') {
467
- if (options.includes('--help') || options.includes('-h')) {
468
- console.log(helpText);
469
- process.exit(0);
470
- }
780
+ if (wantHelp) showHelp('scan');
471
781
  run(target, {
472
782
  json: jsonOutput,
473
783
  html: htmlOutput,
@@ -495,6 +805,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
495
805
  process.exit(1);
496
806
  });
497
807
  } else if (command === 'feed') {
808
+ if (wantHelp) showHelp('feed');
498
809
  const { getFeed } = require('../src/threat-feed.js');
499
810
  const feedOpts = {};
500
811
  if (feedLimit) feedOpts.limit = feedLimit;
@@ -504,10 +815,12 @@ if (command === 'version' || command === '--version' || command === '-v') {
504
815
  console.log(JSON.stringify(result, null, 2));
505
816
  process.exit(0);
506
817
  } else if (command === 'serve') {
818
+ if (wantHelp) showHelp('serve');
507
819
  const { startServer } = require('../src/serve.js');
508
820
  startServer({ port: servePort || 3000 });
509
821
  // Server runs indefinitely — no process.exit
510
822
  } else if (command === 'watch') {
823
+ if (wantHelp) showHelp('watch');
511
824
  watch(target);
512
825
  } else if (command === 'update') {
513
826
  updateIOCs().then(() => {
@@ -525,6 +838,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
525
838
  process.exit(1);
526
839
  });
527
840
  } else if (command === 'monitor') {
841
+ if (wantHelp) showHelp('monitor');
528
842
  const testPkg = options.filter(o => !o.startsWith('-'));
529
843
  const isTemporal = options.includes('--temporal');
530
844
  const isTemporalAst = options.includes('--temporal-ast');
@@ -614,9 +928,11 @@ if (command === 'version' || command === '--version' || command === '-v') {
614
928
  });
615
929
  }
616
930
  } else if (command === 'daemon') {
931
+ if (wantHelp) showHelp('daemon');
617
932
  const { startDaemon } = require('../src/daemon.js');
618
933
  startDaemon({ webhook: webhookUrl });
619
934
  } else if (command === 'install' || command === 'i') {
935
+ if (wantHelp) showHelp('install');
620
936
  const packages = options.filter(o => !o.startsWith('-'));
621
937
  const isDev = options.includes('--save-dev') || options.includes('-D');
622
938
  const isGlobal = options.includes('-g') || options.includes('--global');
@@ -637,13 +953,14 @@ if (command === 'version' || command === '--version' || command === '-v') {
637
953
  process.exit(1);
638
954
  });
639
955
  } else if (command === 'sandbox') {
956
+ if (wantHelp) showHelp('sandbox');
640
957
  const sandboxOpts = options.filter(o => !o.startsWith('-'));
641
958
  const packageName = sandboxOpts[0];
642
959
  const strict = options.includes('--strict');
643
960
  const canary = !options.includes('--no-canary');
644
961
  const local = options.includes('--local');
645
962
  if (!packageName) {
646
- console.log('Usage: muaddib sandbox <package-name|path> [--local] [--strict] [--no-canary]');
963
+ console.log(commandHelp['sandbox']);
647
964
  process.exit(1);
648
965
  }
649
966
 
@@ -657,13 +974,14 @@ if (command === 'version' || command === '--version' || command === '-v') {
657
974
  process.exit(1);
658
975
  });
659
976
  } else if (command === 'sandbox-report') {
977
+ if (wantHelp) showHelp('sandbox-report');
660
978
  const sandboxOpts = options.filter(o => !o.startsWith('-'));
661
979
  const packageName = sandboxOpts[0];
662
980
  const strict = options.includes('--strict');
663
981
  const canary = !options.includes('--no-canary');
664
982
  const local = options.includes('--local');
665
983
  if (!packageName) {
666
- console.log('Usage: muaddib sandbox-report <package-name|path> [--local] [--strict] [--no-canary]');
984
+ console.log(commandHelp['sandbox-report']);
667
985
  process.exit(1);
668
986
  }
669
987
 
@@ -680,6 +998,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
680
998
  process.exit(1);
681
999
  });
682
1000
  } else if (command === 'diff') {
1001
+ if (wantHelp) showHelp('diff');
683
1002
  // Parse diff arguments: muaddib diff <ref> [path] [options]
684
1003
  const diffArgs = options.filter(o => !o.startsWith('-'));
685
1004
  const baseRef = diffArgs[0];
@@ -702,6 +1021,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
702
1021
  process.exit(1);
703
1022
  });
704
1023
  } else if (command === 'detections') {
1024
+ if (wantHelp) showHelp('detections');
705
1025
  const { loadDetections, getDetectionStats } = require('../src/monitor.js');
706
1026
  const wantStats = options.includes('--stats');
707
1027
  const wantJson = options.includes('--json');
@@ -753,6 +1073,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
753
1073
  console.log('');
754
1074
  process.exit(0);
755
1075
  } else if (command === 'stats') {
1076
+ if (wantHelp) showHelp('stats');
756
1077
  const { loadScanStats } = require('../src/monitor.js');
757
1078
  const wantDaily = options.includes('--daily');
758
1079
  const wantJson = options.includes('--json');
@@ -796,6 +1117,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
796
1117
  console.log('');
797
1118
  process.exit(0);
798
1119
  } else if (command === 'evaluate') {
1120
+ if (wantHelp) showHelp('evaluate');
799
1121
  const { evaluate } = require('../src/commands/evaluate.js');
800
1122
  const evalOpts = { json: jsonOutput };
801
1123
  for (let i = 0; i < options.length; i++) {
@@ -813,6 +1135,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
813
1135
  process.exit(1);
814
1136
  });
815
1137
  } else if (command === 'init-hooks') {
1138
+ if (wantHelp) showHelp('init-hooks');
816
1139
  // Parse init-hooks arguments
817
1140
  let hookType = 'auto';
818
1141
  let hookMode = 'scan';
@@ -834,6 +1157,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
834
1157
  process.exit(1);
835
1158
  });
836
1159
  } else if (command === 'remove-hooks') {
1160
+ if (wantHelp) showHelp('remove-hooks');
837
1161
  removeHooks(target).then(success => {
838
1162
  process.exit(success ? 0 : 1);
839
1163
  }).catch(err => {
@@ -841,6 +1165,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
841
1165
  process.exit(1);
842
1166
  });
843
1167
  } else if (command === 'replay' || command === 'ground-truth') {
1168
+ if (wantHelp) showHelp('replay');
844
1169
  const { replay } = require('../tests/ground-truth/replay.js');
845
1170
  const replayOpts = {};
846
1171
  for (const o of options) {
@@ -858,7 +1183,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
858
1183
  process.exit(1);
859
1184
  });
860
1185
  } else if (command === 'report') {
861
- // Hidden/internal — not in --help
1186
+ if (wantHelp) showHelp('report');
862
1187
  if (options.includes('--now')) {
863
1188
  const { sendReportNow } = require('../src/monitor.js');
864
1189
  sendReportNow().then(result => {
@@ -889,8 +1214,9 @@ if (command === 'version' || command === '--version' || command === '-v') {
889
1214
  process.exit(1);
890
1215
  }
891
1216
  } else if (command === 'help') {
892
- console.log(helpText);
893
- process.exit(0);
1217
+ // muaddib help <command> — show per-command help
1218
+ const helpCmd = options.filter(o => !o.startsWith('-'))[0];
1219
+ showHelp(helpCmd);
894
1220
  } else {
895
1221
  console.log(`Unknown command: ${String(command).replace(/[\x00-\x1f\x7f-\x9f]/g, '')}`);
896
1222
  console.log('Type "muaddib help" to see available commands.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.21",
3
+ "version": "2.10.23",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {