muaddib-scanner 2.11.36 → 2.11.38

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/bin/muaddib.js CHANGED
@@ -44,6 +44,7 @@ let target = '.';
44
44
  let jsonOutput = false;
45
45
  let htmlOutput = null;
46
46
  let sarifOutput = null;
47
+ let cyclonedxOutput = null;
47
48
  let explainMode = false;
48
49
  let failLevel = 'high';
49
50
  let webhookUrl = null;
@@ -56,6 +57,7 @@ let temporalPublishMode = false;
56
57
  let temporalMaintainerMode = false;
57
58
  let temporalFullMode = false;
58
59
  let breakdownMode = false;
60
+ let domainFilter = null; // P0a: --domain malware,author → filter output by risk domain
59
61
  let noDeobfuscate = false;
60
62
  let noModuleGraph = false;
61
63
  let noReachability = false;
@@ -87,6 +89,16 @@ for (let i = 0; i < options.length; i++) {
87
89
  }
88
90
  sarifOutput = sarifPath;
89
91
  i++;
92
+ } else if (options[i] === '--cyclonedx') {
93
+ // P1b: CycloneDX 1.5 SBOM export (https://cyclonedx.org)
94
+ const bomPath = options[i + 1] || 'muaddib-bom.cdx.json';
95
+ // CLI-001: Block path traversal
96
+ if (bomPath.includes('..')) {
97
+ console.error('[ERROR] --cyclonedx path must not contain path traversal (..)');
98
+ process.exit(1);
99
+ }
100
+ cyclonedxOutput = bomPath;
101
+ i++;
90
102
  } else if (options[i] === '--explain') {
91
103
  explainMode = true;
92
104
  } else if (options[i] === '--fail-on') {
@@ -146,6 +158,24 @@ for (let i = 0; i < options.length; i++) {
146
158
  temporalMaintainerMode = true;
147
159
  } else if (options[i] === '--breakdown') {
148
160
  breakdownMode = true;
161
+ } else if (options[i] === '--domain') {
162
+ // P0a: comma-separated list of risk domains to DISPLAY (filter only, not
163
+ // a score modifier). Valid values: malware, author, engineering,
164
+ // vulnerability, license, unknown. Example: --domain malware,author
165
+ const val = options[i + 1];
166
+ if (!val || val.startsWith('-')) {
167
+ console.error('[ERROR] --domain requires a comma-separated list (e.g. malware,author)');
168
+ process.exit(1);
169
+ }
170
+ const VALID = new Set(['malware', 'author', 'engineering', 'vulnerability', 'license', 'unknown']);
171
+ const parts = val.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
172
+ const invalid = parts.filter(p => !VALID.has(p));
173
+ if (invalid.length > 0) {
174
+ console.error('[ERROR] --domain invalid value(s): ' + invalid.join(',') + ' (valid: ' + Array.from(VALID).join('|') + ')');
175
+ process.exit(1);
176
+ }
177
+ domainFilter = parts;
178
+ i++;
149
179
  } else if (options[i] === '--no-deobfuscate') {
150
180
  noDeobfuscate = true;
151
181
  } else if (options[i] === '--no-module-graph') {
@@ -254,6 +284,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
254
284
  json: jsonOutput,
255
285
  html: htmlOutput,
256
286
  sarif: sarifOutput,
287
+ cyclonedx: cyclonedxOutput,
257
288
  explain: explainMode,
258
289
  failLevel: failLevel,
259
290
  webhook: webhookUrl,
@@ -265,6 +296,7 @@ if (command === 'version' || command === '--version' || command === '-v') {
265
296
  exclude: excludeDirs,
266
297
  entropyThreshold: entropyThreshold,
267
298
  breakdown: breakdownMode,
299
+ domainFilter: domainFilter,
268
300
  noDeobfuscate: noDeobfuscate,
269
301
  noModuleGraph: noModuleGraph,
270
302
  noReachability: noReachability,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.11.36",
3
+ "version": "2.11.38",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-05-24T21:02:11.478Z",
3
+ "timestamp": "2026-05-24T22:20:18.999Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -14,6 +14,7 @@
14
14
  "rule_id": "MUADDIB-AST-074",
15
15
  "rule_name": "String Mutation Obfuscation",
16
16
  "confidence": "high",
17
+ "domain": "malware",
17
18
  "references": [
18
19
  "https://attack.mitre.org/techniques/T1027/",
19
20
  "https://attack.mitre.org/techniques/T1140/"
@@ -34,6 +35,7 @@
34
35
  "rule_id": "MUADDIB-AST-074",
35
36
  "rule_name": "String Mutation Obfuscation",
36
37
  "confidence": "high",
38
+ "domain": "malware",
37
39
  "references": [
38
40
  "https://attack.mitre.org/techniques/T1027/",
39
41
  "https://attack.mitre.org/techniques/T1140/"
@@ -54,6 +56,7 @@
54
56
  "rule_id": "MUADDIB-AST-074",
55
57
  "rule_name": "String Mutation Obfuscation",
56
58
  "confidence": "high",
59
+ "domain": "malware",
57
60
  "references": [
58
61
  "https://attack.mitre.org/techniques/T1027/",
59
62
  "https://attack.mitre.org/techniques/T1140/"
@@ -74,6 +77,7 @@
74
77
  "rule_id": "MUADDIB-AST-074",
75
78
  "rule_name": "String Mutation Obfuscation",
76
79
  "confidence": "high",
80
+ "domain": "malware",
77
81
  "references": [
78
82
  "https://attack.mitre.org/techniques/T1027/",
79
83
  "https://attack.mitre.org/techniques/T1140/"
@@ -94,6 +98,7 @@
94
98
  "rule_id": "MUADDIB-AST-074",
95
99
  "rule_name": "String Mutation Obfuscation",
96
100
  "confidence": "high",
101
+ "domain": "malware",
97
102
  "references": [
98
103
  "https://attack.mitre.org/techniques/T1027/",
99
104
  "https://attack.mitre.org/techniques/T1140/"
@@ -114,6 +119,7 @@
114
119
  "rule_id": "MUADDIB-AST-074",
115
120
  "rule_name": "String Mutation Obfuscation",
116
121
  "confidence": "high",
122
+ "domain": "malware",
117
123
  "references": [
118
124
  "https://attack.mitre.org/techniques/T1027/",
119
125
  "https://attack.mitre.org/techniques/T1140/"
@@ -134,6 +140,7 @@
134
140
  "rule_id": "MUADDIB-AST-074",
135
141
  "rule_name": "String Mutation Obfuscation",
136
142
  "confidence": "high",
143
+ "domain": "malware",
137
144
  "references": [
138
145
  "https://attack.mitre.org/techniques/T1027/",
139
146
  "https://attack.mitre.org/techniques/T1140/"
@@ -154,6 +161,7 @@
154
161
  "rule_id": "MUADDIB-AST-075",
155
162
  "rule_name": "Module Internals Hijack",
156
163
  "confidence": "high",
164
+ "domain": "malware",
157
165
  "references": [
158
166
  "https://nodejs.org/api/modules.html",
159
167
  "https://attack.mitre.org/techniques/T1574.006/"
@@ -174,6 +182,7 @@
174
182
  "rule_id": "MUADDIB-AST-006",
175
183
  "rule_name": "Dynamic Require with Concatenation",
176
184
  "confidence": "high",
185
+ "domain": "malware",
177
186
  "references": [
178
187
  "https://attack.mitre.org/techniques/T1027/"
179
188
  ],
@@ -193,6 +202,7 @@
193
202
  "rule_id": "MUADDIB-AST-002",
194
203
  "rule_name": "Sensitive Environment Variable Access",
195
204
  "confidence": "high",
205
+ "domain": "malware",
196
206
  "references": [
197
207
  "https://blog.phylum.io/shai-hulud-npm-worm",
198
208
  "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions"
@@ -213,6 +223,7 @@
213
223
  "rule_id": "MUADDIB-AST-006",
214
224
  "rule_name": "Dynamic Require with Concatenation",
215
225
  "confidence": "high",
226
+ "domain": "malware",
216
227
  "references": [
217
228
  "https://attack.mitre.org/techniques/T1027/"
218
229
  ],
@@ -232,6 +243,7 @@
232
243
  "rule_id": "MUADDIB-AST-006",
233
244
  "rule_name": "Dynamic Require with Concatenation",
234
245
  "confidence": "high",
246
+ "domain": "malware",
235
247
  "references": [
236
248
  "https://attack.mitre.org/techniques/T1027/"
237
249
  ],
@@ -251,6 +263,7 @@
251
263
  "rule_id": "MUADDIB-AST-019",
252
264
  "rule_name": "Require Cache Poisoning",
253
265
  "confidence": "high",
266
+ "domain": "malware",
254
267
  "references": [
255
268
  "https://attack.mitre.org/techniques/T1574/006/"
256
269
  ],
@@ -270,6 +283,7 @@
270
283
  "rule_id": "MUADDIB-AST-006",
271
284
  "rule_name": "Dynamic Require with Concatenation",
272
285
  "confidence": "high",
286
+ "domain": "malware",
273
287
  "references": [
274
288
  "https://attack.mitre.org/techniques/T1027/"
275
289
  ],
@@ -289,6 +303,7 @@
289
303
  "rule_id": "MUADDIB-AST-008",
290
304
  "rule_name": "Dynamic import() of Dangerous Module",
291
305
  "confidence": "high",
306
+ "domain": "malware",
292
307
  "references": [
293
308
  "https://attack.mitre.org/techniques/T1027/"
294
309
  ],
@@ -308,6 +323,7 @@
308
323
  "rule_id": "MUADDIB-AST-005",
309
324
  "rule_name": "new Function() Constructor",
310
325
  "confidence": "high",
326
+ "domain": "vulnerability",
311
327
  "references": [
312
328
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function"
313
329
  ],
@@ -327,6 +343,7 @@
327
343
  "rule_id": "MUADDIB-AST-074",
328
344
  "rule_name": "String Mutation Obfuscation",
329
345
  "confidence": "high",
346
+ "domain": "malware",
330
347
  "references": [
331
348
  "https://attack.mitre.org/techniques/T1027/",
332
349
  "https://attack.mitre.org/techniques/T1140/"
@@ -347,6 +364,7 @@
347
364
  "rule_id": "MUADDIB-AST-074",
348
365
  "rule_name": "String Mutation Obfuscation",
349
366
  "confidence": "high",
367
+ "domain": "malware",
350
368
  "references": [
351
369
  "https://attack.mitre.org/techniques/T1027/",
352
370
  "https://attack.mitre.org/techniques/T1140/"
@@ -367,6 +385,7 @@
367
385
  "rule_id": "MUADDIB-AST-008",
368
386
  "rule_name": "Dynamic import() of Dangerous Module",
369
387
  "confidence": "high",
388
+ "domain": "malware",
370
389
  "references": [
371
390
  "https://attack.mitre.org/techniques/T1027/"
372
391
  ],
@@ -386,6 +405,7 @@
386
405
  "rule_id": "MUADDIB-AST-019",
387
406
  "rule_name": "Require Cache Poisoning",
388
407
  "confidence": "high",
408
+ "domain": "malware",
389
409
  "references": [
390
410
  "https://attack.mitre.org/techniques/T1574/006/"
391
411
  ],
@@ -405,6 +425,7 @@
405
425
  "rule_id": "MUADDIB-AST-008",
406
426
  "rule_name": "Dynamic import() of Dangerous Module",
407
427
  "confidence": "high",
428
+ "domain": "malware",
408
429
  "references": [
409
430
  "https://attack.mitre.org/techniques/T1027/"
410
431
  ],
@@ -424,6 +445,7 @@
424
445
  "rule_id": "MUADDIB-AST-008",
425
446
  "rule_name": "Dynamic import() of Dangerous Module",
426
447
  "confidence": "high",
448
+ "domain": "malware",
427
449
  "references": [
428
450
  "https://attack.mitre.org/techniques/T1027/"
429
451
  ],
@@ -443,6 +465,7 @@
443
465
  "rule_id": "MUADDIB-AST-070",
444
466
  "rule_name": "Shared Memory IPC",
445
467
  "confidence": "medium",
468
+ "domain": "malware",
446
469
  "references": [
447
470
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer",
448
471
  "https://attack.mitre.org/techniques/T1559/"
@@ -465,6 +488,7 @@
465
488
  "rule_id": "MUADDIB-AST-007",
466
489
  "rule_name": "Dangerous Shell Command Execution",
467
490
  "confidence": "high",
491
+ "domain": "malware",
468
492
  "references": [
469
493
  "https://owasp.org/www-community/attacks/Command_Injection"
470
494
  ],
@@ -486,6 +510,7 @@
486
510
  "rule_id": "MUADDIB-AST-007",
487
511
  "rule_name": "Dangerous Shell Command Execution",
488
512
  "confidence": "high",
513
+ "domain": "malware",
489
514
  "references": [
490
515
  "https://owasp.org/www-community/attacks/Command_Injection"
491
516
  ],
@@ -507,6 +532,7 @@
507
532
  "rule_id": "MUADDIB-AST-007",
508
533
  "rule_name": "Dangerous Shell Command Execution",
509
534
  "confidence": "high",
535
+ "domain": "malware",
510
536
  "references": [
511
537
  "https://owasp.org/www-community/attacks/Command_Injection"
512
538
  ],
@@ -528,6 +554,7 @@
528
554
  "rule_id": "MUADDIB-AST-007",
529
555
  "rule_name": "Dangerous Shell Command Execution",
530
556
  "confidence": "high",
557
+ "domain": "malware",
531
558
  "references": [
532
559
  "https://owasp.org/www-community/attacks/Command_Injection"
533
560
  ],
@@ -549,6 +576,7 @@
549
576
  "rule_id": "MUADDIB-AST-007",
550
577
  "rule_name": "Dangerous Shell Command Execution",
551
578
  "confidence": "high",
579
+ "domain": "malware",
552
580
  "references": [
553
581
  "https://owasp.org/www-community/attacks/Command_Injection"
554
582
  ],
@@ -570,6 +598,7 @@
570
598
  "rule_id": "MUADDIB-AST-007",
571
599
  "rule_name": "Dangerous Shell Command Execution",
572
600
  "confidence": "high",
601
+ "domain": "malware",
573
602
  "references": [
574
603
  "https://owasp.org/www-community/attacks/Command_Injection"
575
604
  ],
@@ -591,6 +620,7 @@
591
620
  "rule_id": "MUADDIB-AST-007",
592
621
  "rule_name": "Dangerous Shell Command Execution",
593
622
  "confidence": "high",
623
+ "domain": "malware",
594
624
  "references": [
595
625
  "https://owasp.org/www-community/attacks/Command_Injection"
596
626
  ],
@@ -612,6 +642,7 @@
612
642
  "rule_id": "MUADDIB-AST-007",
613
643
  "rule_name": "Dangerous Shell Command Execution",
614
644
  "confidence": "high",
645
+ "domain": "malware",
615
646
  "references": [
616
647
  "https://owasp.org/www-community/attacks/Command_Injection"
617
648
  ],
@@ -633,6 +664,7 @@
633
664
  "rule_id": "MUADDIB-AST-007",
634
665
  "rule_name": "Dangerous Shell Command Execution",
635
666
  "confidence": "high",
667
+ "domain": "malware",
636
668
  "references": [
637
669
  "https://owasp.org/www-community/attacks/Command_Injection"
638
670
  ],
@@ -654,6 +686,7 @@
654
686
  "rule_id": "MUADDIB-AST-007",
655
687
  "rule_name": "Dangerous Shell Command Execution",
656
688
  "confidence": "high",
689
+ "domain": "malware",
657
690
  "references": [
658
691
  "https://owasp.org/www-community/attacks/Command_Injection"
659
692
  ],
@@ -675,6 +708,7 @@
675
708
  "rule_id": "MUADDIB-AST-007",
676
709
  "rule_name": "Dangerous Shell Command Execution",
677
710
  "confidence": "high",
711
+ "domain": "malware",
678
712
  "references": [
679
713
  "https://owasp.org/www-community/attacks/Command_Injection"
680
714
  ],
@@ -696,6 +730,7 @@
696
730
  "rule_id": "MUADDIB-AST-007",
697
731
  "rule_name": "Dangerous Shell Command Execution",
698
732
  "confidence": "high",
733
+ "domain": "malware",
699
734
  "references": [
700
735
  "https://owasp.org/www-community/attacks/Command_Injection"
701
736
  ],
@@ -717,6 +752,7 @@
717
752
  "rule_id": "MUADDIB-AST-007",
718
753
  "rule_name": "Dangerous Shell Command Execution",
719
754
  "confidence": "high",
755
+ "domain": "malware",
720
756
  "references": [
721
757
  "https://owasp.org/www-community/attacks/Command_Injection"
722
758
  ],
@@ -738,6 +774,7 @@
738
774
  "rule_id": "MUADDIB-AST-007",
739
775
  "rule_name": "Dangerous Shell Command Execution",
740
776
  "confidence": "high",
777
+ "domain": "malware",
741
778
  "references": [
742
779
  "https://owasp.org/www-community/attacks/Command_Injection"
743
780
  ],
@@ -759,6 +796,7 @@
759
796
  "rule_id": "MUADDIB-AST-007",
760
797
  "rule_name": "Dangerous Shell Command Execution",
761
798
  "confidence": "high",
799
+ "domain": "malware",
762
800
  "references": [
763
801
  "https://owasp.org/www-community/attacks/Command_Injection"
764
802
  ],
@@ -780,6 +818,7 @@
780
818
  "rule_id": "MUADDIB-AST-007",
781
819
  "rule_name": "Dangerous Shell Command Execution",
782
820
  "confidence": "high",
821
+ "domain": "malware",
783
822
  "references": [
784
823
  "https://owasp.org/www-community/attacks/Command_Injection"
785
824
  ],
@@ -801,6 +840,7 @@
801
840
  "rule_id": "MUADDIB-AST-007",
802
841
  "rule_name": "Dangerous Shell Command Execution",
803
842
  "confidence": "high",
843
+ "domain": "malware",
804
844
  "references": [
805
845
  "https://owasp.org/www-community/attacks/Command_Injection"
806
846
  ],
@@ -822,6 +862,7 @@
822
862
  "rule_id": "MUADDIB-AST-007",
823
863
  "rule_name": "Dangerous Shell Command Execution",
824
864
  "confidence": "high",
865
+ "domain": "malware",
825
866
  "references": [
826
867
  "https://owasp.org/www-community/attacks/Command_Injection"
827
868
  ],
@@ -847,6 +888,7 @@
847
888
  "rule_id": "MUADDIB-ENTROPY-001",
848
889
  "rule_name": "High Entropy String",
849
890
  "confidence": "medium",
891
+ "domain": "malware",
850
892
  "references": [
851
893
  "https://attack.mitre.org/techniques/T1027/"
852
894
  ],
@@ -872,6 +914,7 @@
872
914
  "rule_id": "MUADDIB-ENTROPY-001",
873
915
  "rule_name": "High Entropy String",
874
916
  "confidence": "medium",
917
+ "domain": "malware",
875
918
  "references": [
876
919
  "https://attack.mitre.org/techniques/T1027/"
877
920
  ],
@@ -897,6 +940,7 @@
897
940
  "rule_id": "MUADDIB-ENTROPY-001",
898
941
  "rule_name": "High Entropy String",
899
942
  "confidence": "medium",
943
+ "domain": "malware",
900
944
  "references": [
901
945
  "https://attack.mitre.org/techniques/T1027/"
902
946
  ],
@@ -922,6 +966,7 @@
922
966
  "rule_id": "MUADDIB-ENTROPY-001",
923
967
  "rule_name": "High Entropy String",
924
968
  "confidence": "medium",
969
+ "domain": "malware",
925
970
  "references": [
926
971
  "https://attack.mitre.org/techniques/T1027/"
927
972
  ],
@@ -947,6 +992,7 @@
947
992
  "rule_id": "MUADDIB-ENTROPY-001",
948
993
  "rule_name": "High Entropy String",
949
994
  "confidence": "medium",
995
+ "domain": "malware",
950
996
  "references": [
951
997
  "https://attack.mitre.org/techniques/T1027/"
952
998
  ],
@@ -972,6 +1018,7 @@
972
1018
  "rule_id": "MUADDIB-ENTROPY-001",
973
1019
  "rule_name": "High Entropy String",
974
1020
  "confidence": "medium",
1021
+ "domain": "malware",
975
1022
  "references": [
976
1023
  "https://attack.mitre.org/techniques/T1027/"
977
1024
  ],
@@ -997,6 +1044,7 @@
997
1044
  "rule_id": "MUADDIB-ENTROPY-001",
998
1045
  "rule_name": "High Entropy String",
999
1046
  "confidence": "medium",
1047
+ "domain": "malware",
1000
1048
  "references": [
1001
1049
  "https://attack.mitre.org/techniques/T1027/"
1002
1050
  ],
@@ -1022,6 +1070,7 @@
1022
1070
  "rule_id": "MUADDIB-ENTROPY-001",
1023
1071
  "rule_name": "High Entropy String",
1024
1072
  "confidence": "medium",
1073
+ "domain": "malware",
1025
1074
  "references": [
1026
1075
  "https://attack.mitre.org/techniques/T1027/"
1027
1076
  ],
@@ -1047,6 +1096,7 @@
1047
1096
  "rule_id": "MUADDIB-ENTROPY-001",
1048
1097
  "rule_name": "High Entropy String",
1049
1098
  "confidence": "medium",
1099
+ "domain": "malware",
1050
1100
  "references": [
1051
1101
  "https://attack.mitre.org/techniques/T1027/"
1052
1102
  ],
@@ -0,0 +1,204 @@
1
+ // P1b — CycloneDX 1.5 SBOM export.
2
+ //
3
+ // Maps muaddib scan results to CycloneDX vulnerabilities affecting the scanned
4
+ // package (root component). Consumed by SBOM-oriented pipelines: Dependency-
5
+ // Track, Anchore, Snyk, Trivy, GitHub Security, etc.
6
+ //
7
+ // Spec : https://cyclonedx.org/docs/1.5/json/
8
+ // Why 1.5 and not 1.6 : v1.5 has universal consumer support; v1.6 adds
9
+ // features (mldata, evidence) we don't use.
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+ const { getRuleDomain } = require('../rules/index.js');
15
+
16
+ const SPEC_VERSION = '1.5';
17
+ const TOOL_NAME = 'muaddib';
18
+ const TOOL_VENDOR = 'muaddib';
19
+ const TOOL_URL = 'https://github.com/DNSZLSK/muad-dib';
20
+ const ROOT_BOM_REF = 'scanned-package';
21
+
22
+ const _muaddibVersion = (() => {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')).version;
25
+ } catch {
26
+ return '0.0.0';
27
+ }
28
+ })();
29
+
30
+ /**
31
+ * Map muaddib severity (uppercase) → CycloneDX severity (lowercase).
32
+ * Unknown values fall to "info" (CycloneDX's mildest level).
33
+ */
34
+ function severityToCycloneDX(severity) {
35
+ switch (severity) {
36
+ case 'CRITICAL': return 'critical';
37
+ case 'HIGH': return 'high';
38
+ case 'MEDIUM': return 'medium';
39
+ case 'LOW': return 'low';
40
+ default: return 'info';
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Build a package URL (purl) for the scanned component.
46
+ * https://github.com/package-url/purl-spec
47
+ * - npm scoped: pkg:npm/%40scope%2Fname@version
48
+ * - npm unscoped: pkg:npm/name@version
49
+ * - generic fall: pkg:generic/dirname@0.0.0
50
+ *
51
+ * When `hasPackageJson` is true we assume npm. PyPI / other ecosystems would
52
+ * need a separate detector (out of scope for v1).
53
+ */
54
+ function buildPurl(name, version, hasPackageJson) {
55
+ const safeVersion = version || '0.0.0';
56
+ if (!hasPackageJson) {
57
+ return 'pkg:generic/' + encodeURIComponent(name || 'unknown') + '@' + encodeURIComponent(safeVersion);
58
+ }
59
+ if (name && name.startsWith('@') && name.includes('/')) {
60
+ const slashIdx = name.indexOf('/');
61
+ const scope = name.slice(0, slashIdx);
62
+ const rest = name.slice(slashIdx + 1);
63
+ return 'pkg:npm/' + encodeURIComponent(scope) + '/' + encodeURIComponent(rest) + '@' + encodeURIComponent(safeVersion);
64
+ }
65
+ return 'pkg:npm/' + encodeURIComponent(name || 'unknown') + '@' + encodeURIComponent(safeVersion);
66
+ }
67
+
68
+ /**
69
+ * Read the scanned target's package.json (if present) to derive root identity.
70
+ * Returns { name, version, hasPackageJson }.
71
+ */
72
+ function resolveRootComponent(targetPath) {
73
+ let name = null;
74
+ let version = null;
75
+ let hasPackageJson = false;
76
+ if (targetPath && typeof targetPath === 'string') {
77
+ try {
78
+ const pkgPath = path.join(targetPath, 'package.json');
79
+ if (fs.existsSync(pkgPath)) {
80
+ const data = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
81
+ if (typeof data.name === 'string' && data.name.length > 0) name = data.name;
82
+ if (typeof data.version === 'string' && data.version.length > 0) version = data.version;
83
+ hasPackageJson = true;
84
+ }
85
+ } catch {
86
+ // ignore — fallback to dirname below
87
+ }
88
+ }
89
+ if (!name) {
90
+ // Last-resort fallback: dirname of the target
91
+ try {
92
+ name = targetPath ? path.basename(path.resolve(targetPath)) : 'unknown';
93
+ } catch {
94
+ name = 'unknown';
95
+ }
96
+ }
97
+ if (!version) version = '0.0.0';
98
+ return { name, version, hasPackageJson };
99
+ }
100
+
101
+ /**
102
+ * Build the properties array for a vulnerability — exposes muaddib-specific
103
+ * data (risk_domain, confidence, mitre, type, file, line) using a namespaced
104
+ * key convention so consumers can filter/group on them without collisions.
105
+ */
106
+ function vulnerabilityProperties(threat) {
107
+ const props = [];
108
+ const domain = threat.domain || getRuleDomain(threat.type);
109
+ if (domain) props.push({ name: 'muaddib:risk_domain', value: String(domain) });
110
+ if (threat.type) props.push({ name: 'muaddib:type', value: String(threat.type) });
111
+ if (threat.confidence) props.push({ name: 'muaddib:confidence', value: String(threat.confidence) });
112
+ if (threat.mitre) props.push({ name: 'muaddib:mitre', value: String(threat.mitre) });
113
+ if (threat.file) props.push({ name: 'muaddib:file', value: String(threat.file) });
114
+ if (threat.line) props.push({ name: 'muaddib:line', value: String(threat.line) });
115
+ if (typeof threat.points === 'number') props.push({ name: 'muaddib:points', value: String(threat.points) });
116
+ return props;
117
+ }
118
+
119
+ function vulnerabilityFromThreat(threat, idx) {
120
+ const sev = severityToCycloneDX(threat.severity);
121
+ const score = (typeof threat.points === 'number') ? threat.points : 0;
122
+ return {
123
+ 'bom-ref': 'muaddib-vuln-' + idx,
124
+ id: threat.rule_id || threat.type || ('MUADDIB-UNK-' + idx),
125
+ source: { name: 'MUADDIB', url: TOOL_URL },
126
+ description: threat.message || (threat.type || 'muaddib threat'),
127
+ ratings: [
128
+ {
129
+ source: { name: 'MUADDIB' },
130
+ severity: sev,
131
+ method: 'other',
132
+ score,
133
+ vector: 'muaddib-confidence:' + (threat.confidence || 'medium')
134
+ }
135
+ ],
136
+ affects: [{ ref: ROOT_BOM_REF }],
137
+ properties: vulnerabilityProperties(threat)
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Generate a CycloneDX 1.5 BOM document from a muaddib scan result.
143
+ * @param {object} results - scan result object (must contain `threats`, `target`, optionally `timestamp`)
144
+ * @returns {object} CycloneDX BOM ready to JSON.stringify
145
+ */
146
+ function generateCycloneDX(results) {
147
+ const target = (results && results.target) || '.';
148
+ const timestamp = (results && results.timestamp) || new Date().toISOString();
149
+ const root = resolveRootComponent(target);
150
+ const purl = buildPurl(root.name, root.version, root.hasPackageJson);
151
+
152
+ const threats = Array.isArray(results && results.threats) ? results.threats : [];
153
+ const vulnerabilities = threats.map((t, i) => vulnerabilityFromThreat(t, i + 1));
154
+
155
+ return {
156
+ bomFormat: 'CycloneDX',
157
+ specVersion: SPEC_VERSION,
158
+ serialNumber: 'urn:uuid:' + crypto.randomUUID(),
159
+ version: 1,
160
+ metadata: {
161
+ timestamp,
162
+ tools: [
163
+ {
164
+ vendor: TOOL_VENDOR,
165
+ name: TOOL_NAME,
166
+ version: _muaddibVersion
167
+ }
168
+ ],
169
+ component: {
170
+ 'bom-ref': ROOT_BOM_REF,
171
+ type: 'library',
172
+ name: root.name,
173
+ version: root.version,
174
+ purl
175
+ }
176
+ },
177
+ vulnerabilities
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Write the generated BOM to disk. Mirrors saveSARIF semantics.
183
+ */
184
+ function saveCycloneDX(results, outputPath) {
185
+ if (!outputPath || typeof outputPath !== 'string') {
186
+ throw new Error('Invalid output path for CycloneDX report');
187
+ }
188
+ const bom = generateCycloneDX(results);
189
+ try {
190
+ fs.writeFileSync(outputPath, JSON.stringify(bom, null, 2));
191
+ } catch (e) {
192
+ throw new Error('Failed to write CycloneDX report to ' + outputPath + ': ' + e.message);
193
+ }
194
+ return outputPath;
195
+ }
196
+
197
+ module.exports = {
198
+ generateCycloneDX,
199
+ saveCycloneDX,
200
+ severityToCycloneDX,
201
+ buildPurl,
202
+ resolveRootComponent,
203
+ SPEC_VERSION
204
+ };