@mitre/hdf-utilities 3.2.0 → 3.3.0

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/dist/index.js CHANGED
@@ -41,7 +41,890 @@ function impactToSeverity(impact) {
41
41
  if (impact > 0) return "low";
42
42
  return "informational";
43
43
  }
44
+ /**
45
+ * Map a raw CVSS base score (0.0–10.0) to a FIRST qualitative severity band.
46
+ *
47
+ * Bands per FIRST CVSS v3.x / v4.0 spec:
48
+ * none = 0.0
49
+ * low = 0.1 – 3.9
50
+ * medium = 4.0 – 6.9
51
+ * high = 7.0 – 8.9
52
+ * critical = 9.0 – 10.0
53
+ *
54
+ * Out-of-range inputs are clamped: scores < 0.1 → "none" (band floor),
55
+ * scores > 10.0 → "critical". This matches scanner behavior of treating
56
+ * malformed scores as informational/critical extremes rather than throwing.
57
+ *
58
+ * @param score - CVSS base score
59
+ * @returns FIRST severity band label
60
+ */
61
+ function cvssScoreToSeverity(score) {
62
+ if (!Number.isFinite(score) || score < .1) return "none";
63
+ if (score < 4) return "low";
64
+ if (score < 7) return "medium";
65
+ if (score < 9) return "high";
66
+ return "critical";
67
+ }
68
+ //#endregion
69
+ //#region src/cvss/index.ts
70
+ /**
71
+ * Allowed values for each metric, keyed by CVSS major version.
72
+ *
73
+ * Sourced from FIRST specifications (linked above). Only metrics present
74
+ * in a given version's grammar appear in that version's map; unknown metrics
75
+ * encountered during validation are ignored (forward-compat).
76
+ */
77
+ const METRIC_VALUES = {
78
+ "2.0": {
79
+ AV: [
80
+ "L",
81
+ "A",
82
+ "N"
83
+ ],
84
+ AC: [
85
+ "H",
86
+ "M",
87
+ "L"
88
+ ],
89
+ Au: [
90
+ "M",
91
+ "S",
92
+ "N"
93
+ ],
94
+ C: [
95
+ "N",
96
+ "P",
97
+ "C"
98
+ ],
99
+ I: [
100
+ "N",
101
+ "P",
102
+ "C"
103
+ ],
104
+ A: [
105
+ "N",
106
+ "P",
107
+ "C"
108
+ ],
109
+ E: [
110
+ "U",
111
+ "POC",
112
+ "F",
113
+ "H",
114
+ "ND"
115
+ ],
116
+ RL: [
117
+ "OF",
118
+ "TF",
119
+ "W",
120
+ "U",
121
+ "ND"
122
+ ],
123
+ RC: [
124
+ "UC",
125
+ "UR",
126
+ "C",
127
+ "ND"
128
+ ],
129
+ CDP: [
130
+ "N",
131
+ "L",
132
+ "LM",
133
+ "MH",
134
+ "H",
135
+ "ND"
136
+ ],
137
+ TD: [
138
+ "N",
139
+ "L",
140
+ "M",
141
+ "H",
142
+ "ND"
143
+ ],
144
+ CR: [
145
+ "L",
146
+ "M",
147
+ "H",
148
+ "ND"
149
+ ],
150
+ IR: [
151
+ "L",
152
+ "M",
153
+ "H",
154
+ "ND"
155
+ ],
156
+ AR: [
157
+ "L",
158
+ "M",
159
+ "H",
160
+ "ND"
161
+ ]
162
+ },
163
+ "3.0": {
164
+ AV: [
165
+ "N",
166
+ "A",
167
+ "L",
168
+ "P"
169
+ ],
170
+ AC: ["L", "H"],
171
+ PR: [
172
+ "N",
173
+ "L",
174
+ "H"
175
+ ],
176
+ UI: ["N", "R"],
177
+ S: ["U", "C"],
178
+ C: [
179
+ "N",
180
+ "L",
181
+ "H"
182
+ ],
183
+ I: [
184
+ "N",
185
+ "L",
186
+ "H"
187
+ ],
188
+ A: [
189
+ "N",
190
+ "L",
191
+ "H"
192
+ ],
193
+ E: [
194
+ "X",
195
+ "U",
196
+ "P",
197
+ "F",
198
+ "H"
199
+ ],
200
+ RL: [
201
+ "X",
202
+ "O",
203
+ "T",
204
+ "W",
205
+ "U"
206
+ ],
207
+ RC: [
208
+ "X",
209
+ "U",
210
+ "R",
211
+ "C"
212
+ ],
213
+ CR: [
214
+ "X",
215
+ "L",
216
+ "M",
217
+ "H"
218
+ ],
219
+ IR: [
220
+ "X",
221
+ "L",
222
+ "M",
223
+ "H"
224
+ ],
225
+ AR: [
226
+ "X",
227
+ "L",
228
+ "M",
229
+ "H"
230
+ ],
231
+ MAV: [
232
+ "X",
233
+ "N",
234
+ "A",
235
+ "L",
236
+ "P"
237
+ ],
238
+ MAC: [
239
+ "X",
240
+ "L",
241
+ "H"
242
+ ],
243
+ MPR: [
244
+ "X",
245
+ "N",
246
+ "L",
247
+ "H"
248
+ ],
249
+ MUI: [
250
+ "X",
251
+ "N",
252
+ "R"
253
+ ],
254
+ MS: [
255
+ "X",
256
+ "U",
257
+ "C"
258
+ ],
259
+ MC: [
260
+ "X",
261
+ "N",
262
+ "L",
263
+ "H"
264
+ ],
265
+ MI: [
266
+ "X",
267
+ "N",
268
+ "L",
269
+ "H"
270
+ ],
271
+ MA: [
272
+ "X",
273
+ "N",
274
+ "L",
275
+ "H"
276
+ ]
277
+ },
278
+ "3.1": {
279
+ AV: [
280
+ "N",
281
+ "A",
282
+ "L",
283
+ "P"
284
+ ],
285
+ AC: ["L", "H"],
286
+ PR: [
287
+ "N",
288
+ "L",
289
+ "H"
290
+ ],
291
+ UI: ["N", "R"],
292
+ S: ["U", "C"],
293
+ C: [
294
+ "N",
295
+ "L",
296
+ "H"
297
+ ],
298
+ I: [
299
+ "N",
300
+ "L",
301
+ "H"
302
+ ],
303
+ A: [
304
+ "N",
305
+ "L",
306
+ "H"
307
+ ],
308
+ E: [
309
+ "X",
310
+ "U",
311
+ "P",
312
+ "F",
313
+ "H"
314
+ ],
315
+ RL: [
316
+ "X",
317
+ "O",
318
+ "T",
319
+ "W",
320
+ "U"
321
+ ],
322
+ RC: [
323
+ "X",
324
+ "U",
325
+ "R",
326
+ "C"
327
+ ],
328
+ CR: [
329
+ "X",
330
+ "L",
331
+ "M",
332
+ "H"
333
+ ],
334
+ IR: [
335
+ "X",
336
+ "L",
337
+ "M",
338
+ "H"
339
+ ],
340
+ AR: [
341
+ "X",
342
+ "L",
343
+ "M",
344
+ "H"
345
+ ],
346
+ MAV: [
347
+ "X",
348
+ "N",
349
+ "A",
350
+ "L",
351
+ "P"
352
+ ],
353
+ MAC: [
354
+ "X",
355
+ "L",
356
+ "H"
357
+ ],
358
+ MPR: [
359
+ "X",
360
+ "N",
361
+ "L",
362
+ "H"
363
+ ],
364
+ MUI: [
365
+ "X",
366
+ "N",
367
+ "R"
368
+ ],
369
+ MS: [
370
+ "X",
371
+ "U",
372
+ "C"
373
+ ],
374
+ MC: [
375
+ "X",
376
+ "N",
377
+ "L",
378
+ "H"
379
+ ],
380
+ MI: [
381
+ "X",
382
+ "N",
383
+ "L",
384
+ "H"
385
+ ],
386
+ MA: [
387
+ "X",
388
+ "N",
389
+ "L",
390
+ "H"
391
+ ]
392
+ },
393
+ "4.0": {
394
+ AV: [
395
+ "N",
396
+ "A",
397
+ "L",
398
+ "P"
399
+ ],
400
+ AC: ["L", "H"],
401
+ AT: ["N", "P"],
402
+ PR: [
403
+ "N",
404
+ "L",
405
+ "H"
406
+ ],
407
+ UI: [
408
+ "N",
409
+ "P",
410
+ "A"
411
+ ],
412
+ VC: [
413
+ "H",
414
+ "L",
415
+ "N"
416
+ ],
417
+ VI: [
418
+ "H",
419
+ "L",
420
+ "N"
421
+ ],
422
+ VA: [
423
+ "H",
424
+ "L",
425
+ "N"
426
+ ],
427
+ SC: [
428
+ "H",
429
+ "L",
430
+ "N"
431
+ ],
432
+ SI: [
433
+ "H",
434
+ "L",
435
+ "N"
436
+ ],
437
+ SA: [
438
+ "H",
439
+ "L",
440
+ "N"
441
+ ],
442
+ E: [
443
+ "X",
444
+ "A",
445
+ "P",
446
+ "U"
447
+ ],
448
+ MAV: [
449
+ "X",
450
+ "N",
451
+ "A",
452
+ "L",
453
+ "P"
454
+ ],
455
+ MAC: [
456
+ "X",
457
+ "L",
458
+ "H"
459
+ ],
460
+ MAT: [
461
+ "X",
462
+ "N",
463
+ "P"
464
+ ],
465
+ MPR: [
466
+ "X",
467
+ "N",
468
+ "L",
469
+ "H"
470
+ ],
471
+ MUI: [
472
+ "X",
473
+ "N",
474
+ "P",
475
+ "A"
476
+ ],
477
+ MVC: [
478
+ "X",
479
+ "H",
480
+ "L",
481
+ "N"
482
+ ],
483
+ MVI: [
484
+ "X",
485
+ "H",
486
+ "L",
487
+ "N"
488
+ ],
489
+ MVA: [
490
+ "X",
491
+ "H",
492
+ "L",
493
+ "N"
494
+ ],
495
+ MSC: [
496
+ "X",
497
+ "H",
498
+ "L",
499
+ "N"
500
+ ],
501
+ MSI: [
502
+ "X",
503
+ "S",
504
+ "H",
505
+ "L",
506
+ "N"
507
+ ],
508
+ MSA: [
509
+ "X",
510
+ "S",
511
+ "H",
512
+ "L",
513
+ "N"
514
+ ],
515
+ CR: [
516
+ "X",
517
+ "H",
518
+ "M",
519
+ "L"
520
+ ],
521
+ IR: [
522
+ "X",
523
+ "H",
524
+ "M",
525
+ "L"
526
+ ],
527
+ AR: [
528
+ "X",
529
+ "H",
530
+ "M",
531
+ "L"
532
+ ],
533
+ S: [
534
+ "X",
535
+ "N",
536
+ "P"
537
+ ],
538
+ AU: [
539
+ "X",
540
+ "N",
541
+ "Y"
542
+ ],
543
+ R: [
544
+ "X",
545
+ "A",
546
+ "U",
547
+ "I"
548
+ ],
549
+ V: [
550
+ "X",
551
+ "D",
552
+ "C"
553
+ ],
554
+ RE: [
555
+ "X",
556
+ "L",
557
+ "M",
558
+ "H"
559
+ ],
560
+ U: [
561
+ "X",
562
+ "Clear",
563
+ "Green",
564
+ "Amber",
565
+ "Red"
566
+ ]
567
+ }
568
+ };
569
+ const REQUIRED_METRICS = {
570
+ "2.0": [
571
+ "AV",
572
+ "AC",
573
+ "Au",
574
+ "C",
575
+ "I",
576
+ "A"
577
+ ],
578
+ "3.0": [
579
+ "AV",
580
+ "AC",
581
+ "PR",
582
+ "UI",
583
+ "S",
584
+ "C",
585
+ "I",
586
+ "A"
587
+ ],
588
+ "3.1": [
589
+ "AV",
590
+ "AC",
591
+ "PR",
592
+ "UI",
593
+ "S",
594
+ "C",
595
+ "I",
596
+ "A"
597
+ ],
598
+ "4.0": [
599
+ "AV",
600
+ "AC",
601
+ "AT",
602
+ "PR",
603
+ "UI",
604
+ "VC",
605
+ "VI",
606
+ "VA",
607
+ "SC",
608
+ "SI",
609
+ "SA"
610
+ ]
611
+ };
612
+ /**
613
+ * Parse a CVSS vector string into version + metric map.
614
+ *
615
+ * Version detection rule: a leading `CVSS:X.Y` segment yields version "X.Y".
616
+ * Otherwise (legacy v2 vectors), version is "2.0". Malformed input yields
617
+ * "unknown" with an empty metric map; no exceptions are thrown.
618
+ *
619
+ * @param vector - CVSS vector string (e.g. "CVSS:3.1/AV:N/AC:L/...")
620
+ * @returns Parsed version + metric map
621
+ */
622
+ function parseCvssVector(vector) {
623
+ const metrics = /* @__PURE__ */ new Map();
624
+ if (typeof vector !== "string" || vector.length === 0) return {
625
+ version: "unknown",
626
+ metrics
627
+ };
628
+ const segments = vector.split("/").filter((s) => s.length > 0);
629
+ if (segments.length === 0) return {
630
+ version: "unknown",
631
+ metrics
632
+ };
633
+ let version = "2.0";
634
+ let start = 0;
635
+ const first = segments[0];
636
+ if (first === void 0) return {
637
+ version: "unknown",
638
+ metrics
639
+ };
640
+ if (first.startsWith("CVSS:")) {
641
+ version = first.slice(5);
642
+ start = 1;
643
+ } else if (!first.includes(":")) return {
644
+ version: "unknown",
645
+ metrics
646
+ };
647
+ for (let i = start; i < segments.length; i++) {
648
+ const seg = segments[i];
649
+ if (seg === void 0) continue;
650
+ const colon = seg.indexOf(":");
651
+ if (colon <= 0 || colon === seg.length - 1) continue;
652
+ const key = seg.slice(0, colon);
653
+ const value = seg.slice(colon + 1);
654
+ metrics.set(key, value);
655
+ }
656
+ return {
657
+ version,
658
+ metrics
659
+ };
660
+ }
661
+ /**
662
+ * Validate a CVSS vector string against the FIRST grammar for its version.
663
+ *
664
+ * Checks:
665
+ * - Version is one of the supported set (2.0, 3.0, 3.1, 4.0).
666
+ * - All required base metrics for the version are present.
667
+ * - All known metric values are within the allowed enum.
668
+ *
669
+ * Unknown metric keys are tolerated (forward-compat with future minor versions
670
+ * or scanner-specific extensions). To check a v2 vector without the CVSS:
671
+ * prefix, callers may pass version="2.0" explicitly; otherwise the parser
672
+ * defaults to "2.0" on prefix absence.
673
+ *
674
+ * @param vector - CVSS vector string
675
+ * @param version - Optional explicit version override (else inferred)
676
+ * @returns `{ valid, errors[] }` — never throws
677
+ */
678
+ function validateCvssVector(vector, version) {
679
+ const errors = [];
680
+ if (typeof vector !== "string" || vector.length === 0) return {
681
+ valid: false,
682
+ errors: ["vector is empty"]
683
+ };
684
+ const parsed = parseCvssVector(vector);
685
+ const v = version ?? parsed.version;
686
+ const grammar = METRIC_VALUES[v];
687
+ const required = REQUIRED_METRICS[v];
688
+ if (!grammar || !required) {
689
+ errors.push(`unsupported CVSS version: ${v}`);
690
+ return {
691
+ valid: false,
692
+ errors
693
+ };
694
+ }
695
+ for (const req of required) if (!parsed.metrics.has(req)) errors.push(`missing required metric: ${req}`);
696
+ for (const [key, value] of parsed.metrics.entries()) {
697
+ const allowed = grammar[key];
698
+ if (!allowed) continue;
699
+ if (!allowed.includes(value)) errors.push(`invalid value for metric ${key}: ${value} (allowed: ${allowed.join(",")})`);
700
+ }
701
+ return {
702
+ valid: errors.length === 0,
703
+ errors
704
+ };
705
+ }
706
+ //#endregion
707
+ //#region src/cpe/index.ts
708
+ const CPE_PREFIX = "cpe:2.3:";
709
+ const EXPECTED_TOTAL_FIELDS = 13;
710
+ const PRODUCT_FIELD_COUNT = 11;
711
+ const VALID_PARTS = new Set([
712
+ "a",
713
+ "o",
714
+ "h",
715
+ "*"
716
+ ]);
717
+ /**
718
+ * Split a CPE 2.3 body on unescaped `:` separators.
719
+ *
720
+ * Per CPE 2.3 spec section 6.1.2.4, `:` and `\` inside a field must be
721
+ * escaped as `\:` and `\\`. We respect those escapes during the split and
722
+ * unescape the result.
723
+ */
724
+ function splitOnUnescapedColons(body) {
725
+ const result = [];
726
+ let current = "";
727
+ for (let i = 0; i < body.length; i++) {
728
+ const ch = body[i];
729
+ if (ch === "\\" && i + 1 < body.length) {
730
+ const next = body[i + 1];
731
+ if (next === ":" || next === "\\") {
732
+ current += next;
733
+ i++;
734
+ continue;
735
+ }
736
+ current += ch;
737
+ continue;
738
+ }
739
+ if (ch === ":") {
740
+ result.push(current);
741
+ current = "";
742
+ continue;
743
+ }
744
+ current += ch;
745
+ }
746
+ result.push(current);
747
+ return result;
748
+ }
749
+ /**
750
+ * Parse a CPE 2.3 URI.
751
+ *
752
+ * Accept-and-warn semantics:
753
+ * - Input missing the `cpe:2.3:` prefix → returns `null`.
754
+ * - Input with the prefix but fewer than 11 product fields → fields are
755
+ * padded with `"*"` and a `truncated:` warning is added.
756
+ * - Input with more than 11 product fields → extras are dropped and an
757
+ * `"extra fields ignored"` warning is added.
758
+ * - Unknown `part` value → kept as-is in the result, `"unknown part: X"`
759
+ * warning is added.
760
+ * - `\:` and `\\` escapes inside fields are honored during splitting and
761
+ * unescaped in the returned field values.
762
+ *
763
+ * @param cpeUri - CPE 2.3 URI string
764
+ * @returns Parsed CPE with warnings, or `null` if input lacks the prefix
765
+ *
766
+ * @example
767
+ * ```typescript
768
+ * parseCpe('cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*');
769
+ * // { part: 'a', vendor: 'openssl', product: 'openssl', version: '1.1.1k', ... warnings: [] }
770
+ *
771
+ * parseCpe('cpe:2.3:a:openssl:openssl:1.1.1k');
772
+ * // truncated input → padded with '*', warnings: ['truncated: expected 13 colon-separated fields, got 6']
773
+ *
774
+ * parseCpe('openssl:1.1.1k'); // null — no prefix
775
+ * ```
776
+ */
777
+ function parseCpe(cpeUri) {
778
+ if (!cpeUri.startsWith(CPE_PREFIX)) return null;
779
+ const warnings = [];
780
+ const body = cpeUri.slice(8);
781
+ const isBarePrefix = body.length === 0;
782
+ const bodyParts = isBarePrefix ? [] : splitOnUnescapedColons(body);
783
+ const totalFields = isBarePrefix ? 2 : 2 + bodyParts.length;
784
+ if (bodyParts.length > PRODUCT_FIELD_COUNT) warnings.push("extra fields ignored");
785
+ else if (bodyParts.length < PRODUCT_FIELD_COUNT) warnings.push(`truncated: expected ${EXPECTED_TOTAL_FIELDS} colon-separated fields, got ${totalFields}`);
786
+ const padValue = isBarePrefix ? "" : "*";
787
+ const fields = bodyParts.slice(0, PRODUCT_FIELD_COUNT);
788
+ while (fields.length < PRODUCT_FIELD_COUNT) fields.push(padValue);
789
+ const [part = "", vendor = "", product = "", version = "", update = "", edition = "", language = "", swEdition = "", targetSw = "", targetHw = "", other = ""] = fields;
790
+ if (!VALID_PARTS.has(part)) warnings.push(`unknown part: ${part}`);
791
+ return {
792
+ raw: cpeUri,
793
+ part,
794
+ vendor,
795
+ product,
796
+ version,
797
+ update,
798
+ edition,
799
+ language,
800
+ swEdition,
801
+ targetSw,
802
+ targetHw,
803
+ other,
804
+ warnings
805
+ };
806
+ }
807
+ //#endregion
808
+ //#region src/purl/index.ts
809
+ const PURL_PREFIX = "pkg:";
810
+ /**
811
+ * Parse a Package URL string.
812
+ *
813
+ * Returns `null` only when the input is missing the mandatory `pkg:` prefix
814
+ * or the type segment. Other deviations are surfaced via `warnings` on the
815
+ * returned object.
816
+ *
817
+ * @param purlStr - PURL string, e.g. `pkg:npm/lodash@4.17.21`
818
+ * @returns Parsed PURL or `null` if the input is unrecoverably malformed
819
+ *
820
+ * @example
821
+ * ```typescript
822
+ * const r = parsePurl('pkg:rpm/redhat/openssl@1.1.1k-7.el8_4?arch=x86_64');
823
+ * r?.type; // "rpm"
824
+ * r?.namespace; // "redhat"
825
+ * r?.name; // "openssl"
826
+ * r?.qualifiers.get('arch'); // "x86_64"
827
+ * ```
828
+ */
829
+ function parsePurl(purlStr) {
830
+ if (!purlStr || !purlStr.startsWith(PURL_PREFIX)) return null;
831
+ const raw = purlStr;
832
+ const warnings = [];
833
+ let remainder = purlStr.slice(4);
834
+ while (remainder.startsWith("/")) remainder = remainder.slice(1);
835
+ if (remainder.length === 0) return null;
836
+ let subpath = null;
837
+ const hashIdx = remainder.indexOf("#");
838
+ if (hashIdx !== -1) {
839
+ subpath = remainder.slice(hashIdx + 1);
840
+ remainder = remainder.slice(0, hashIdx);
841
+ if (subpath.length === 0) subpath = null;
842
+ }
843
+ const qualifiers = /* @__PURE__ */ new Map();
844
+ const qIdx = remainder.indexOf("?");
845
+ if (qIdx !== -1) {
846
+ const qStr = remainder.slice(qIdx + 1);
847
+ remainder = remainder.slice(0, qIdx);
848
+ parseQualifiers(qStr, qualifiers, warnings);
849
+ }
850
+ while (remainder.endsWith("/")) remainder = remainder.slice(0, -1);
851
+ const firstSlash = remainder.indexOf("/");
852
+ let type;
853
+ let pathPart;
854
+ if (firstSlash === -1) {
855
+ type = remainder;
856
+ pathPart = "";
857
+ } else {
858
+ type = remainder.slice(0, firstSlash);
859
+ pathPart = remainder.slice(firstSlash + 1);
860
+ }
861
+ if (type.length === 0) return null;
862
+ type = type.toLowerCase();
863
+ let version = null;
864
+ const lastAt = pathPart.lastIndexOf("@");
865
+ let nameAndNs = pathPart;
866
+ if (lastAt !== -1) {
867
+ version = pathPart.slice(lastAt + 1);
868
+ nameAndNs = pathPart.slice(0, lastAt);
869
+ version = safeDecode(version);
870
+ }
871
+ let namespace = null;
872
+ let name;
873
+ const lastSlash = nameAndNs.lastIndexOf("/");
874
+ if (lastSlash === -1) name = nameAndNs;
875
+ else {
876
+ namespace = nameAndNs.slice(0, lastSlash);
877
+ name = nameAndNs.slice(lastSlash + 1);
878
+ }
879
+ if (namespace !== null) namespace = namespace.split("/").map(safeDecode).join("/");
880
+ name = safeDecode(name);
881
+ if (name.length === 0) warnings.push("PURL is missing the name segment");
882
+ if (namespace !== null && namespace.length === 0) namespace = null;
883
+ if (version !== null && version.length === 0) version = null;
884
+ return {
885
+ raw,
886
+ type,
887
+ namespace,
888
+ name,
889
+ version,
890
+ qualifiers,
891
+ subpath,
892
+ warnings
893
+ };
894
+ }
895
+ /**
896
+ * Parse the `?key=value&key=value` qualifier string. Empty segments are
897
+ * skipped silently; segments with no `=` are recorded with empty value and
898
+ * a warning. Values are URL-decoded.
899
+ */
900
+ function parseQualifiers(qStr, out, warnings) {
901
+ const parts = qStr.split("&");
902
+ for (const part of parts) {
903
+ if (part.length === 0) continue;
904
+ const eq = part.indexOf("=");
905
+ if (eq === -1) {
906
+ out.set(part, "");
907
+ warnings.push(`qualifier "${part}" has no value`);
908
+ continue;
909
+ }
910
+ const key = part.slice(0, eq);
911
+ const value = part.slice(eq + 1);
912
+ out.set(key, safeDecode(value));
913
+ }
914
+ }
915
+ /**
916
+ * decodeURIComponent that returns the original string on failure rather than
917
+ * throwing. PURLs in the wild occasionally contain stray `%` characters that
918
+ * aren't legitimate percent-encoding.
919
+ */
920
+ function safeDecode(s) {
921
+ try {
922
+ return decodeURIComponent(s);
923
+ } catch {
924
+ return s;
925
+ }
926
+ }
44
927
  //#endregion
45
- export { buildCsv, buildCsvArray, buildXml, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, generateHash, hashObject, impactToSeverity, isValidCsv, isValidJSON, isValidXml, parseCsv, parseCsvArray, parseJSON, parseTimestamp, parseXml, parseXmlWithArrays, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, sha256, sha512, stringifyJSON, stripHtml, verifyHash };
928
+ export { buildCsv, buildCsvArray, buildXml, cvssScoreToSeverity, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, generateHash, hashObject, impactToSeverity, isValidCsv, isValidJSON, isValidXml, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, sha256, sha512, stringifyJSON, stripHtml, validateCvssVector, verifyHash };
46
929
 
47
930
  //# sourceMappingURL=index.js.map