@mitre/hdf-converters 3.6.0-rc.2 → 3.6.0-rc.4

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
@@ -1,8 +1,150 @@
1
1
  import { n as detectFormat, r as detectConverter, t as registerAllFingerprints } from "./register-all-CDfYvwQF.js";
2
2
  import { flattenOverlays } from "@mitre/hdf-parsers";
3
- import { buildCsv, buildXml, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, governingStatusOverride, impactToSeverity, isUnratedSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, severityToImpact, severityToImpactWithAliases, sha256, stripHtml, stripHtml as stripHTML, trimUtcFraction, worstStatus } from "@mitre/hdf-utilities";
4
- import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact as severityToImpact$1 } from "@mitre/hdf-schema";
3
+ import { DEFAULT_MAX_INPUT_SIZE, buildCsv, buildXml, canonicalJson, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, encodeBase64Utf8, formatJsonNumber, formatTimestamp, formatTimestampSeconds, governingStatusOverride, impactToSeverity, isUnratedSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, severityToImpact, severityToImpactWithAliases, sha256, stripHtml, stripHtml as stripHTML, trimUtcFraction, validateInputSize, worstStatus, xmlSafeText } from "@mitre/hdf-utilities";
4
+ import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, affectedPackageSchema, amendmentsSchema, bomSchema, commonSchema, comparisonSchema, componentSchema, createDescription, createMinimalBaseline, createRequirement, createResult, cvssSchema, dataFlowSchema, epssSchema, eventsSchema, extensionsSchema, hdfAmendmentsSchema, hdfBaselineSchema, hdfComparisonSchema, hdfEvidencePackageSchema, hdfPlanSchema, hdfRequirementChangeEventSchema, hdfResultsSchema, hdfSystemSchema, kevSchema, parameterSchema, planSchema, platformSchema, resultSchema, runnerSchema, severityToImpact as severityToImpact$1, statisticsSchema, systemSchema, targetSchema } from "@mitre/hdf-schema";
5
5
  import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_CONFIG_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getHipcheckNistControls, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
6
+ import Ajv from "ajv";
7
+ import addFormats from "ajv-formats";
8
+ //#region ../hdf-validators/dist/index.js
9
+ /**
10
+ * Create and configure Ajv instance with all HDF schemas
11
+ */
12
+ function createValidator() {
13
+ const ajv = new Ajv({
14
+ allErrors: true,
15
+ verbose: true,
16
+ strict: false,
17
+ validateFormats: true,
18
+ validateSchema: false
19
+ });
20
+ addFormats(ajv);
21
+ const registeredDateTime = ajv.formats["date-time"];
22
+ ajv.addFormat("date-time", {
23
+ type: "string",
24
+ validate: (v) => /^\d{4}-\d{2}-\d{2}[Tt]/.test(v) && registeredDateTime.validate(v)
25
+ });
26
+ ajv.addSchema(commonSchema);
27
+ ajv.addSchema(platformSchema);
28
+ ajv.addSchema(resultSchema);
29
+ ajv.addSchema(runnerSchema);
30
+ ajv.addSchema(statisticsSchema);
31
+ ajv.addSchema(targetSchema);
32
+ ajv.addSchema(parameterSchema);
33
+ ajv.addSchema(cvssSchema);
34
+ ajv.addSchema(epssSchema);
35
+ ajv.addSchema(kevSchema);
36
+ ajv.addSchema(affectedPackageSchema);
37
+ ajv.addSchema(bomSchema);
38
+ ajv.addSchema(amendmentsSchema);
39
+ ajv.addSchema(extensionsSchema);
40
+ ajv.addSchema(systemSchema);
41
+ ajv.addSchema(planSchema);
42
+ ajv.addSchema(comparisonSchema);
43
+ ajv.addSchema(eventsSchema);
44
+ ajv.addSchema(componentSchema);
45
+ ajv.addSchema(dataFlowSchema);
46
+ ajv.addSchema(hdfResultsSchema);
47
+ ajv.addSchema(hdfBaselineSchema);
48
+ ajv.addSchema(hdfComparisonSchema);
49
+ ajv.addSchema(hdfSystemSchema);
50
+ ajv.addSchema(hdfPlanSchema);
51
+ ajv.addSchema(hdfAmendmentsSchema);
52
+ ajv.addSchema(hdfRequirementChangeEventSchema);
53
+ ajv.addSchema(hdfEvidencePackageSchema);
54
+ return ajv;
55
+ }
56
+ const ajv = createValidator();
57
+ let resultsValidator = null;
58
+ function getResultsValidator() {
59
+ if (!resultsValidator) resultsValidator = ajv.compile(hdfResultsSchema);
60
+ return resultsValidator;
61
+ }
62
+ /**
63
+ * Convert Ajv errors to ValidationError format
64
+ */
65
+ function formatErrors(errors) {
66
+ if (!errors || errors.length === 0) return [];
67
+ return errors.map((err) => {
68
+ let field = err.instancePath.replace(/^\//, "").replace(/\//g, ".");
69
+ if (!field && err.schemaPath) {
70
+ const pathParts = err.schemaPath.split("/");
71
+ field = pathParts[pathParts.length - 1] || "(root)";
72
+ }
73
+ let message = err.message || "validation failed";
74
+ if (err.params) {
75
+ if ("missingProperty" in err.params) {
76
+ field = field ? `${field}.${err.params.missingProperty}` : err.params.missingProperty;
77
+ message = "is required";
78
+ } else if ("additionalProperty" in err.params) {
79
+ field = field ? `${field}.${err.params.additionalProperty}` : err.params.additionalProperty;
80
+ message = "is not allowed";
81
+ } else if ("limit" in err.params) message = `${message} (limit: ${err.params.limit})`;
82
+ }
83
+ return {
84
+ field: field || "(root)",
85
+ keyword: err.keyword,
86
+ ...err.keyword === "format" && err.params && "format" in err.params ? { format: String(err.params.format) } : {},
87
+ message,
88
+ value: err.data
89
+ };
90
+ });
91
+ }
92
+ /**
93
+ * Create ValidationResult from validator output
94
+ */
95
+ function createResult$1(validator, data) {
96
+ const valid = validator(data);
97
+ const errors = formatErrors(validator.errors);
98
+ return {
99
+ valid: valid === true,
100
+ errors,
101
+ getErrorMessage() {
102
+ if (this.valid) return "";
103
+ return this.errors.map((e) => {
104
+ if (e.field === "(root)") return e.message;
105
+ return `${e.field}: ${e.message}`;
106
+ }).join("; ");
107
+ }
108
+ };
109
+ }
110
+ /**
111
+ * Validate HDF Results document against schema
112
+ */
113
+ function validateResults(data) {
114
+ return createResult$1(getResultsValidator(), data);
115
+ }
116
+ //#endregion
117
+ //#region shared/typescript/amendmentchain.ts
118
+ /**
119
+ * Stamps `previousChecksum` across amendment overrides in document order,
120
+ * linking each to the one before it and leaving the first unlinked.
121
+ *
122
+ * Every route that authors amendments calls this, so whether a document carries
123
+ * tamper-evidence does not depend on which one wrote it. The checksum is the
124
+ * canonical JSON form shared with the Go converters and with `hdf amend verify`,
125
+ * so a chain written here verifies everywhere.
126
+ *
127
+ * The override is put through serializeHdf first, so what gets hashed is the
128
+ * shape the document will actually carry. The TS converters hold
129
+ * `appliedAt`/`expiresAt` as `Date` objects, which stringify with a `.000`
130
+ * fraction that trimmed-UTC HDF output never carries; hashing them raw would
131
+ * make every TS-written chain disagree with its own serialized document, and
132
+ * with Go.
133
+ *
134
+ * Mutates the array in place, mirroring the Go helper.
135
+ */
136
+ async function chainOverrides(overrides) {
137
+ let previous;
138
+ for (const override of overrides) {
139
+ if (previous === void 0) delete override.previousChecksum;
140
+ else override.previousChecksum = previous;
141
+ previous = {
142
+ algorithm: HashAlgorithm.Sha256,
143
+ value: await sha256(canonicalJson(JSON.parse(serializeHdf(override, 0))))
144
+ };
145
+ }
146
+ }
147
+ //#endregion
6
148
  //#region shared/typescript/converterutil.ts
7
149
  /**
8
150
  * Shared utilities for TypeScript HDF converters.
@@ -145,7 +287,7 @@ function mapCWEToNIST(cweIDs, fallback) {
145
287
  }
146
288
  return controls.size > 0 ? [...controls].sort() : fallback;
147
289
  }
148
- /** Matches CWE identifiers like "CWE-79", "CWE 89", "cwe22". */
290
+ /** Matches CWE identifiers like "CWE-79", "CWE 89", "cwe22"; group 1 is the number. */
149
291
  const CWE_PATTERN$1 = /CWE[- ]?(\d+)/gi;
150
292
  /**
151
293
  * Extract all numeric CWE IDs from text.
@@ -161,23 +303,44 @@ function extractCWEIDs(text) {
161
303
  ids.sort();
162
304
  return ids;
163
305
  }
164
- /** Default maximum input size for converters (50MB) */
165
- const DEFAULT_MAX_INPUT_SIZE = 50 * 1024 * 1024;
166
306
  /**
167
- * Validates that input string doesn't exceed maximum allowed size.
168
- *
169
- * Note: string.length gives char count, not bytes. For multi-byte chars this
170
- * underestimates. This is acceptable as a coarse safety check — exact byte
171
- * counting would require TextEncoder.
172
- *
173
- * @param input - Raw input string to validate
174
- * @param converterName - Name of the converter (used in error message)
175
- * @param maxSize - Maximum allowed character count (defaults to DEFAULT_MAX_INPUT_SIZE)
176
- * @throws Error if input exceeds maxSize
307
+ * Return the first candidate whose content is not empty or whitespace-only,
308
+ * as-is (the emptiness test trims, the returned value does not). Returns '' when
309
+ * every candidate is empty or whitespace. The caller supplies its own documented
310
+ * fallback as the last candidate so the substituted value is meaningful per
311
+ * field — this helper never invents a placeholder like "N/A". Use it where a
312
+ * target schema requires a non-empty (often minLength:1) string and a source
313
+ * value may be absent; it is a selection helper, not a normalizer, and never
314
+ * decides to OMIT a field (that stays the caller's call). Go parity: FirstNonEmpty.
315
+ */
316
+ function firstNonEmpty(...candidates) {
317
+ for (const c of candidates) if (c != null && c.trim() !== "") return c;
318
+ return "";
319
+ }
320
+ /**
321
+ * The converter-facing face of the @mitre/hdf-utilities size guard: same
322
+ * limit, same UTF-8 byte-length measure as Go's ValidateJSONSize, with the
323
+ * converter name prefixed onto the error. A non-positive maxSize means "use
324
+ * the default" (an explicitly-passed 0 never rejects all non-empty input).
325
+ */
326
+ function validateInputSize$1(input, converterName, maxSize = DEFAULT_MAX_INPUT_SIZE) {
327
+ const limit = maxSize > 0 ? maxSize : DEFAULT_MAX_INPUT_SIZE;
328
+ try {
329
+ validateInputSize(input, limit);
330
+ } catch (err) {
331
+ throw new Error(`${converterName}: ${err instanceof Error ? err.message : String(err)}`);
332
+ }
333
+ }
334
+ /** The schema's Severity enum keyed by its lowercase token. */
335
+ const HDF_SEVERITIES = new Map(Object.values(Severity).map((s) => [s, s]));
336
+ /**
337
+ * Map a raw severity token onto the schema's Severity enum, case-insensitively.
338
+ * The one place the enum vocabulary is spelled out for converters that must not
339
+ * cast an off-vocabulary source value into the typed field. Whitespace is not
340
+ * trimmed; callers own that normalization. Go parity: shared.ParseSeverity.
177
341
  */
178
- function validateInputSize(input, converterName, maxSize = DEFAULT_MAX_INPUT_SIZE) {
179
- if (maxSize <= 0) maxSize = DEFAULT_MAX_INPUT_SIZE;
180
- if (input.length > maxSize) throw new Error(`${converterName}: input exceeds maximum allowed size of ${maxSize} characters`);
342
+ function parseSeverity(s) {
343
+ return HDF_SEVERITIES.get(s.toLowerCase());
181
344
  }
182
345
  /**
183
346
  * Parse an HDF document. The single ingest point for every HDF-consuming
@@ -202,6 +365,15 @@ function requireHdfResults(input, converterName) {
202
365
  return requireHdfStructure(input, converterName, "baselines");
203
366
  }
204
367
  /**
368
+ * The HDF Amendments counterpart of requireHdfResults, keyed on the top-level
369
+ * `overrides` array instead of `baselines`.
370
+ */
371
+ function requireHdfAmendments(input, converterName) {
372
+ const r = requireHdfStructure(input, converterName, "overrides");
373
+ if (r.items.length === 0) throw new Error(`${converterName}: invalid HDF structure: missing overrides field`);
374
+ return r;
375
+ }
376
+ /**
205
377
  * Reject malformed HDF input (empty, oversized, non-JSON, top-level array/null,
206
378
  * or a missing/wrong-typed structural key) before a converter zero-fills it into
207
379
  * a typed document. A parsed JSON `null` (or array, or primitive) is NOT an HDF
@@ -210,12 +382,16 @@ function requireHdfResults(input, converterName) {
210
382
  */
211
383
  function requireHdfStructure(input, converterName, field) {
212
384
  if (input.length === 0) throw new Error(`${converterName}: empty input`);
213
- validateInputSize(input, converterName);
385
+ validateInputSize$1(input, converterName);
214
386
  const parsed = parseHdf(input);
215
387
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${converterName}: invalid HDF structure: missing ${field} field`);
216
388
  const doc = parsed;
217
389
  const items = doc[field];
218
390
  if (!Array.isArray(items)) throw new Error(`${converterName}: invalid HDF structure: missing ${field} field`);
391
+ if (field === "baselines") {
392
+ const violations = typeViolations(validateResults(doc));
393
+ if (violations.length > 0) throw new Error(`${converterName}: input is not valid HDF: ${violations.slice(0, 5).join("; ")}`);
394
+ }
219
395
  return {
220
396
  doc,
221
397
  items
@@ -298,7 +474,7 @@ function ecosystemFromPurlType(type) {
298
474
  * - purl alone
299
475
  * - cpe alone
300
476
  */
301
- function buildAffectedPackage$2(opts) {
477
+ function buildAffectedPackage$1(opts) {
302
478
  const pkg = {};
303
479
  if (opts.purl) pkg.purl = opts.purl;
304
480
  if (opts.cpe) pkg.cpe = opts.cpe;
@@ -484,6 +660,36 @@ function deriveVerificationMethod(code) {
484
660
  if (code === void 0 || code === null || code === "") return void 0;
485
661
  return VerificationMethodEnum.Automated;
486
662
  }
663
+ /**
664
+ * Renames the HDF severity bands into the OSCAL risk characterization facet
665
+ * vocabulary (medium is 'moderate' there, informational is 'info'). Returns ''
666
+ * for anything outside the HDF vocabulary so callers omit the facet rather than
667
+ * emit a value OSCAL does not define. Go peer: shared.OSCALSeverityFromHDF.
668
+ */
669
+ const OSCAL_SEVERITY_BY_HDF_BAND = {
670
+ critical: "critical",
671
+ high: "high",
672
+ medium: "moderate",
673
+ low: "low",
674
+ informational: "info"
675
+ };
676
+ function oscalSeverityFromHdf(severity) {
677
+ const band = (severity ?? "").toLowerCase();
678
+ return Object.prototype.hasOwnProperty.call(OSCAL_SEVERITY_BY_HDF_BAND, band) ? OSCAL_SEVERITY_BY_HDF_BAND[band] : "";
679
+ }
680
+ /**
681
+ * The expiresAt an override takes when the source tool records no expiration:
682
+ * one calendar year after appliedAt, computed in UTC. A calendar year (not 365
683
+ * days) keeps the date on the same month/day across leap years, and doing the
684
+ * rollover in UTC keeps the result off the host timezone. Go peer:
685
+ * shared.DefaultOverrideExpiry.
686
+ */
687
+ function defaultOverrideExpiry(appliedAt) {
688
+ const expiresAt = /* @__PURE__ */ new Date();
689
+ expiresAt.setTime(appliedAt.getTime());
690
+ expiresAt.setUTCFullYear(expiresAt.getUTCFullYear() + 1);
691
+ return expiresAt;
692
+ }
487
693
  function buildNoFindingsRequirement(id, codeDesc, startTime) {
488
694
  return {
489
695
  id,
@@ -511,6 +717,24 @@ const UNRATED_SEVERITY_VALUE = "unrated";
511
717
  function markUnratedSeverity(tags, severity) {
512
718
  if (isUnratedSeverity(severity)) tags[UNRATED_SEVERITY_TAG] = UNRATED_SEVERITY_VALUE;
513
719
  }
720
+ /**
721
+ * The schema violations Go's typed decode would also reject.
722
+ *
723
+ * encoding/json refuses exactly two things: a JSON value whose type does not fit
724
+ * the target field, and a string that will not parse into a time.Time. Every
725
+ * other schema rule — required, enum, minItems, uuid and uri formats — decodes
726
+ * cleanly into the generated structs, so rejecting it here would put the two
727
+ * languages back out of step in the opposite direction.
728
+ *
729
+ * null is exempt for the same reason. Go unmarshals a JSON null into a slice,
730
+ * map or pointer as nil and into anything else as a no-op, never as an error, so
731
+ * a null cvss entry or a null results array reaches the converter there and must
732
+ * reach it here too — several parity goldens pin exactly that.
733
+ */
734
+ function typeViolations(result) {
735
+ if (result.valid) return [];
736
+ return (result.errors ?? []).filter((e) => e.value !== null).filter((e) => e.keyword === "type" || e.keyword === "format" && e.format === "date-time").map((e) => `${e.field ?? ""} ${e.message ?? ""}`.trim());
737
+ }
514
738
  //#endregion
515
739
  //#region converters/legacyhdf-to-hdf/typescript/converter.ts
516
740
  /**
@@ -523,22 +747,13 @@ function markUnratedSeverity(tags, severity) {
523
747
  * - Results: snake_case → camelCase for all fields
524
748
  * - Overlay flattening: merge overlay/wrapper baselines so every requirement has results
525
749
  */
526
- /** Valid severity values per HDF v2.0 schema (matches the Go converter). */
527
- const VALID_SEVERITIES$1 = /* @__PURE__ */ new Set([
528
- "critical",
529
- "high",
530
- "medium",
531
- "low",
532
- "informational"
533
- ]);
534
750
  /**
535
751
  * Convert tags.severity string to a valid severity value.
536
752
  * Returns null if the value is not a recognized severity.
537
753
  */
538
754
  function tagSeverityToSeverity(tagSeverity) {
539
755
  if (typeof tagSeverity !== "string") return null;
540
- const normalized = tagSeverity.toLowerCase().trim();
541
- return VALID_SEVERITIES$1.has(normalized) ? normalized : null;
756
+ return parseSeverity(tagSeverity.trim()) ?? null;
542
757
  }
543
758
  /**
544
759
  * Normalize status values from v1.0 to v2.0 format.
@@ -856,7 +1071,7 @@ function documentTimestamp(v1Data) {
856
1071
  return latest ? formatTimestamp(latest) : void 0;
857
1072
  }
858
1073
  function convertLegacyHdf(v1Data, converterVersion = "1.0.0") {
859
- validateInputSize(JSON.stringify(v1Data), "legacyhdf-to-hdf");
1074
+ validateInputSize$1(JSON.stringify(v1Data), "legacyhdf-to-hdf");
860
1075
  const v2 = {
861
1076
  baselines: (v1Data.profiles || []).map(convertProfile),
862
1077
  statistics: v1Data.statistics || {}
@@ -1327,7 +1542,7 @@ function buildBaseline$1(name, findings, resultsChecksum) {
1327
1542
  /** Converts an ASFF document to HDF Results JSON. */
1328
1543
  async function convertAsffToHdf(input, converterVersion = "1.0.0") {
1329
1544
  if (!input || input.trim().length === 0) throw new Error("asff: empty input");
1330
- validateInputSize(input, "asff");
1545
+ validateInputSize$1(input, "asff");
1331
1546
  const findings = parseFindings$1(input);
1332
1547
  const resultsChecksum = await inputChecksum(input);
1333
1548
  const order = [];
@@ -1376,7 +1591,7 @@ const SARIF_ALIASES = {
1376
1591
  note: .3
1377
1592
  };
1378
1593
  async function convertSarifToHdf(input, converterVersion = "1.0.0") {
1379
- validateInputSize(input, "sarif");
1594
+ validateInputSize$1(input, "sarif");
1380
1595
  const resultsChecksum = await inputChecksum(input);
1381
1596
  const sarif = parseJSON(input);
1382
1597
  if (!sarif || typeof sarif !== "object") throw new Error("Invalid SARIF structure: not a valid JSON object");
@@ -1514,7 +1729,7 @@ function packageFromSarifProperties(props) {
1514
1729
  if (props.purl) ecosystem = ecosystemFromPurlType(parsePurl(props.purl)?.type);
1515
1730
  else if (props.ecosystem) ecosystem = ecosystemFromPurlType(props.ecosystem);
1516
1731
  else if (name && version) ecosystem = Ecosystem.Generic;
1517
- return buildAffectedPackage$2({
1732
+ return buildAffectedPackage$1({
1518
1733
  name,
1519
1734
  version,
1520
1735
  ecosystem,
@@ -1650,9 +1865,7 @@ function buildSuppressionOverride(result, timestamp) {
1650
1865
  const isFalsePositive = accepted.some((s) => justificationIndicatesFalsePositive(s.justification));
1651
1866
  const type = isFalsePositive ? OverrideType.FalsePositive : OverrideType.Waiver;
1652
1867
  const effective = isFalsePositive ? ResultStatus.NotApplicable : ResultStatus.Passed;
1653
- const expiresAt = /* @__PURE__ */ new Date();
1654
- expiresAt.setTime(timestamp.getTime());
1655
- expiresAt.setUTCFullYear(expiresAt.getUTCFullYear() + 1);
1868
+ const expiresAt = defaultOverrideExpiry(timestamp);
1656
1869
  return {
1657
1870
  override: {
1658
1871
  type,
@@ -1725,11 +1938,10 @@ function buildTags$4(result, rule, resolvedLevel, cweIds, nistControls, cciContr
1725
1938
  function extractSourceLocation$1(location) {
1726
1939
  const uri = location.physicalLocation?.artifactLocation?.uri;
1727
1940
  const line = location.physicalLocation?.region?.startLine;
1728
- if (!uri || !line) return;
1729
- return {
1730
- ref: uri,
1731
- line
1732
- };
1941
+ const sourceLocation = {};
1942
+ if (uri) sourceLocation.ref = uri;
1943
+ if (line) sourceLocation.line = line;
1944
+ return sourceLocation.ref !== void 0 || sourceLocation.line !== void 0 ? sourceLocation : void 0;
1733
1945
  }
1734
1946
  function createHDFResult(location, status, backtrace, suppressionMessage) {
1735
1947
  const uri = location.physicalLocation?.artifactLocation?.uri || "";
@@ -1769,7 +1981,7 @@ const ARRAY_TAGS$2 = [
1769
1981
  */
1770
1982
  async function convertJunitToHdf(input, converterVersion = "1.0.0") {
1771
1983
  if (!input || !input.trim()) throw new Error("Empty input");
1772
- validateInputSize(input, "junit");
1984
+ validateInputSize$1(input, "junit");
1773
1985
  const { suites, name } = parseJUnitXML(input);
1774
1986
  const scanTime = resolveScanTime(suites);
1775
1987
  const requirements = buildRequirements(suites, scanTime);
@@ -1790,7 +2002,7 @@ function hostComponents(suites) {
1790
2002
  const hosts = [];
1791
2003
  const seen = /* @__PURE__ */ new Set();
1792
2004
  for (const suite of suites) {
1793
- const hostname = suite.hostname?.trim();
2005
+ const hostname = suite["@_hostname"]?.trim();
1794
2006
  if (!hostname || seen.has(hostname)) continue;
1795
2007
  seen.add(hostname);
1796
2008
  hosts.push({
@@ -1802,22 +2014,31 @@ function hostComponents(suites) {
1802
2014
  return hosts;
1803
2015
  }
1804
2016
  function resolveScanTime(suites) {
1805
- for (const suite of suites) if (suite.timestamp) {
1806
- const parsed = parseTimestamp(suite.timestamp);
2017
+ for (const suite of suites) if (suite["@_timestamp"]) {
2018
+ const parsed = parseTimestamp(suite["@_timestamp"]);
1807
2019
  if (parsed) return parsed;
1808
2020
  }
1809
2021
  return /* @__PURE__ */ new Date();
1810
2022
  }
1811
2023
  function parseJUnitXML(input) {
1812
- const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
1813
- if (parsed.testsuites) return {
1814
- suites: parsed.testsuites.testsuite ?? [],
1815
- name: parsed.testsuites.name ? decodeXmlEntities$6(parsed.testsuites.name) : "JUnit Test Results"
1816
- };
2024
+ const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2, { attributeNamePrefix: "@_" });
2025
+ if (parsed.testsuites) {
2026
+ const suites = [...parsed.testsuites.testsuite ?? []];
2027
+ const name = parsed.testsuites["@_name"] ? decodeXmlEntities$6(parsed.testsuites["@_name"]) : "JUnit Test Results";
2028
+ const looseCases = parsed.testsuites.testcase ?? [];
2029
+ if (looseCases.length > 0) suites.push({
2030
+ "@_name": name,
2031
+ testcase: looseCases
2032
+ });
2033
+ return {
2034
+ suites,
2035
+ name
2036
+ };
2037
+ }
1817
2038
  if (parsed.testsuite) {
1818
2039
  const suite = parsed.testsuite;
1819
2040
  const suites = Array.isArray(suite) ? suite : [suite];
1820
- const suiteName = suites[0]?.name;
2041
+ const suiteName = suites[0]?.["@_name"];
1821
2042
  return {
1822
2043
  suites,
1823
2044
  name: suiteName ? decodeXmlEntities$6(suiteName) : "JUnit Test Results"
@@ -1848,14 +2069,14 @@ function testCaseToRequirement(tc, scanTime) {
1848
2069
  startTime: scanTime
1849
2070
  };
1850
2071
  if (message !== void 0) result.message = message;
1851
- if (tc.time) {
1852
- const parsed = parseFloat(tc.time);
2072
+ if (tc["@_time"]) {
2073
+ const parsed = parseFloat(tc["@_time"]);
1853
2074
  if (!isNaN(parsed)) result.runTime = parsed;
1854
2075
  }
1855
- const name = decodeXmlEntities$6(tc.name);
2076
+ const name = decodeXmlEntities$6(tc["@_name"]);
1856
2077
  const descriptions = [{
1857
2078
  label: "default",
1858
- data: `JUnit test: ${name} in ${tc.classname ? decodeXmlEntities$6(tc.classname) : "unknown"}`
2079
+ data: `JUnit test: ${name} in ${tc["@_classname"] ? decodeXmlEntities$6(tc["@_classname"]) : "unknown"}`
1859
2080
  }];
1860
2081
  const { systemOut, systemErr } = collectSystemStreams(tc);
1861
2082
  if (systemOut) descriptions.push({
@@ -1894,19 +2115,19 @@ function collectSystemStreams(tc) {
1894
2115
  };
1895
2116
  }
1896
2117
  function buildID(tc) {
1897
- if (tc.classname) return `${decodeXmlEntities$6(tc.classname)}.${decodeXmlEntities$6(tc.name)}`;
1898
- return decodeXmlEntities$6(tc.name);
2118
+ if (tc["@_classname"]) return `${decodeXmlEntities$6(tc["@_classname"])}.${decodeXmlEntities$6(tc["@_name"])}`;
2119
+ return decodeXmlEntities$6(tc["@_name"]);
1899
2120
  }
1900
2121
  function resolveStatus(tc) {
1901
2122
  if (tc.failure) {
1902
- const msg = buildFailureMessage(tc.failure.message ?? "", tc.failure.type ?? "", tc.failure["#text"] ?? "");
2123
+ const msg = buildFailureMessage(tc.failure["@_message"] ?? "", tc.failure["@_type"] ?? "", tc.failure["#text"] ?? "");
1903
2124
  return {
1904
2125
  status: ResultStatus.Failed,
1905
2126
  message: msg
1906
2127
  };
1907
2128
  }
1908
2129
  if (tc.error) {
1909
- const msg = buildFailureMessage(tc.error.message ?? "", tc.error.type ?? "", tc.error["#text"] ?? "");
2130
+ const msg = buildFailureMessage(tc.error["@_message"] ?? "", tc.error["@_type"] ?? "", tc.error["#text"] ?? "");
1910
2131
  return {
1911
2132
  status: ResultStatus.Error,
1912
2133
  message: msg
@@ -1914,9 +2135,9 @@ function resolveStatus(tc) {
1914
2135
  }
1915
2136
  if (tc.skipped !== void 0) {
1916
2137
  const skipped = typeof tc.skipped === "object" ? tc.skipped : null;
1917
- if (skipped?.message) return {
2138
+ if (skipped?.["@_message"]) return {
1918
2139
  status: ResultStatus.NotReviewed,
1919
- message: `Skipped: ${decodeXmlEntities$6(skipped.message)}`
2140
+ message: `Skipped: ${decodeXmlEntities$6(skipped["@_message"])}`
1920
2141
  };
1921
2142
  return {
1922
2143
  status: ResultStatus.NotReviewed,
@@ -1933,12 +2154,12 @@ function buildFailureMessage(message, typeName, body) {
1933
2154
  return result;
1934
2155
  }
1935
2156
  function buildCodeDesc$10(tc) {
1936
- if (tc.classname) return `${decodeXmlEntities$6(tc.classname)} :: ${decodeXmlEntities$6(tc.name)}`;
1937
- return decodeXmlEntities$6(tc.name);
2157
+ if (tc["@_classname"]) return `${decodeXmlEntities$6(tc["@_classname"])} :: ${decodeXmlEntities$6(tc["@_name"])}`;
2158
+ return decodeXmlEntities$6(tc["@_name"]);
1938
2159
  }
1939
2160
  function noFindingsTarget(baselineName, suites) {
1940
2161
  if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
1941
- for (const s of suites) if (s.name) return decodeXmlEntities$6(s.name);
2162
+ for (const s of suites) if (s["@_name"]) return decodeXmlEntities$6(s["@_name"]);
1942
2163
  return "JUnit test suite";
1943
2164
  }
1944
2165
  //#endregion
@@ -2036,7 +2257,7 @@ function parseCpeVersion(testSystem) {
2036
2257
  }
2037
2258
  async function convertXccdfResultsToHdf(input, converterVersion = "1.0.0") {
2038
2259
  if (!input || !input.trim()) throw new Error("Empty input");
2039
- validateInputSize(input, "xccdf-results");
2260
+ validateInputSize$1(input, "xccdf-results");
2040
2261
  const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1, { trimValues: false });
2041
2262
  const arfParsed = parsed;
2042
2263
  if (arfParsed["asset-report-collection"]) return convertArfCollection(arfParsed["asset-report-collection"], input, converterVersion);
@@ -2960,13 +3181,6 @@ function cklbTitle(cl) {
2960
3181
  //#endregion
2961
3182
  //#region shared/typescript/checklist/to-hdf.ts
2962
3183
  const CONVERTER_VERSION = "1.0.0";
2963
- const VALID_SEVERITIES = /* @__PURE__ */ new Set([
2964
- "critical",
2965
- "high",
2966
- "medium",
2967
- "low",
2968
- "informational"
2969
- ]);
2970
3184
  /**
2971
3185
  * Map the format-neutral Checklist model to an HDF Results object.
2972
3186
  * controlType is derived per-Vuln from CCI->NIST; verificationMethod and
@@ -3038,7 +3252,8 @@ function vulnToRequirement(v, scanTime) {
3038
3252
  if (v.legacyIDs && v.legacyIDs.length) tags["legacy_ids"] = v.legacyIDs;
3039
3253
  if (v.extra && Object.keys(v.extra).length > 0) tags["cklMetadata"] = { ...v.extra };
3040
3254
  const req = createRequirement(v.vulnNum, v.ruleTitle ?? v.vulnNum, descriptions, impact, [result], { tags });
3041
- if (VALID_SEVERITIES.has(severity)) req.severity = severity;
3255
+ const hdfSeverity = parseSeverity(severity);
3256
+ if (hdfSeverity !== void 0) req.severity = hdfSeverity;
3042
3257
  const controlType = deriveControlTypeFromTags(nistTags);
3043
3258
  if (controlType !== void 0) req.controlType = controlType;
3044
3259
  return req;
@@ -3105,9 +3320,9 @@ function stamp(v) {
3105
3320
  /** Maps a requirement onto the canonical effective-status input shape. */
3106
3321
  function requirementStatusInput(req) {
3107
3322
  return {
3108
- impact: req.impact,
3109
- resultStatuses: (req.results ?? []).map((r) => String(r.status)),
3110
- overrides: (req.statusOverrides ?? []).map((o) => ({
3323
+ impact: req.impact ?? 0,
3324
+ resultStatuses: (req.results ?? []).filter((r) => r != null).map((r) => String(r.status)),
3325
+ overrides: (req.statusOverrides ?? []).filter((o) => o != null).map((o) => ({
3111
3326
  status: o.status ? String(o.status) : void 0,
3112
3327
  appliedAt: stamp(o.appliedAt),
3113
3328
  expiresAt: stamp(o.expiresAt)
@@ -3127,8 +3342,8 @@ function requirementEffectiveStatus(req) {
3127
3342
  * fields are synthesized best-effort so any HDF yields a valid checklist.
3128
3343
  */
3129
3344
  function hdfToChecklist(input) {
3130
- const hdf = parseHdf(input);
3131
- if (!hdf || !Array.isArray(hdf.baselines) || hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
3345
+ const hdf = requireHdfResults(input, "hdf to checklist").doc;
3346
+ if (hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
3132
3347
  const ext = hdf.extensions ?? {};
3133
3348
  const format = strVal(ext, "checklistFormat") || "ckl";
3134
3349
  const cklbVersion = strVal(ext, "cklbVersion");
@@ -3255,7 +3470,7 @@ function formatOverride(o) {
3255
3470
  }
3256
3471
  function overrideSeverity(req) {
3257
3472
  for (const o of req.statusOverrides ?? []) if (o.impact) return {
3258
- severity: qualSeverityFromImpact(o.impact.value),
3473
+ severity: cklSeverityFromImpact(o.impact.value) || "low",
3259
3474
  justification: o.reason ?? ""
3260
3475
  };
3261
3476
  return {
@@ -3263,20 +3478,20 @@ function overrideSeverity(req) {
3263
3478
  justification: ""
3264
3479
  };
3265
3480
  }
3266
- function qualSeverityFromImpact(impact) {
3267
- if (impact >= .7) return "high";
3268
- if (impact >= .4) return "medium";
3269
- return "low";
3481
+ function cklSeverityFromImpact(impact) {
3482
+ switch (impactToSeverity(impact)) {
3483
+ case "critical":
3484
+ case "high": return "high";
3485
+ case "medium": return "medium";
3486
+ case "low": return "low";
3487
+ default: return "";
3488
+ }
3270
3489
  }
3271
3490
  function resolveSeverity(req, tags) {
3272
3491
  const tagSev = strVal(tags, "severity");
3273
3492
  if (tagSev) return tagSev;
3274
3493
  if (req.severity) return String(req.severity).toLowerCase();
3275
- const i = req.impact ?? 0;
3276
- if (i >= .7) return "high";
3277
- if (i >= .4) return "medium";
3278
- if (i > 0) return "low";
3279
- return "";
3494
+ return cklSeverityFromImpact(req.impact ?? 0);
3280
3495
  }
3281
3496
  function resolveCcis(tags) {
3282
3497
  const direct = strSlice(tags, "cci");
@@ -3326,7 +3541,7 @@ function strSlice(m, key) {
3326
3541
  * skill Step 4d).
3327
3542
  */
3328
3543
  async function convertCklToHdf(input) {
3329
- validateInputSize(input, "ckl-to-hdf");
3544
+ validateInputSize$1(input, "ckl-to-hdf");
3330
3545
  const resultsChecksum = await inputChecksum(input);
3331
3546
  const checklist = parseCkl(input);
3332
3547
  return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "ckl-to-hdf"), null, 2);
@@ -3343,7 +3558,7 @@ async function convertCklToHdf(input) {
3343
3558
  * skill Step 4d).
3344
3559
  */
3345
3560
  async function convertCklbToHdf(input) {
3346
- validateInputSize(input, "cklb-to-hdf");
3561
+ validateInputSize$1(input, "cklb-to-hdf");
3347
3562
  const resultsChecksum = await inputChecksum(input);
3348
3563
  const checklist = parseCklb(input);
3349
3564
  return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "cklb-to-hdf"), null, 2);
@@ -3572,7 +3787,7 @@ function buildRequirement$18(query, startTime) {
3572
3787
  * @returns HDF JSON string
3573
3788
  */
3574
3789
  async function convertKicsToHdf(input, converterVersion = "1.0.0") {
3575
- validateInputSize(input, "kics");
3790
+ validateInputSize$1(input, "kics");
3576
3791
  if (!input || input.trim().length === 0) throw new Error("kics: empty input");
3577
3792
  const report = parseJSON(input);
3578
3793
  if (!Array.isArray(report.queries) || typeof report.kics_version !== "string") throw new Error("kics: input does not look like a KICS report");
@@ -3635,10 +3850,11 @@ function lineOf(position) {
3635
3850
  }
3636
3851
  /**
3637
3852
  * Semgrep emits CWEs in prose form -- 'CWE-89: Improper Neutralization of ...'
3638
- * -- while the mapping keys on the bare number.
3853
+ * -- while the mapping keys on the bare number. Read entry by entry so source
3854
+ * order is kept, matching the Go twin.
3639
3855
  */
3640
3856
  function extractCweIds(metadata) {
3641
- return normalizeToArray(metadata.cwe).map((entry) => /CWE-(\d+)/i.exec(entry)?.[1]).filter((id) => id !== void 0).map((id) => Number.parseInt(id, 10)).filter((id) => Number.isFinite(id));
3857
+ return normalizeToArray(metadata.cwe).flatMap((entry) => extractCWEIDs(entry)).map((id) => Number.parseInt(id, 10)).filter((id) => Number.isFinite(id));
3642
3858
  }
3643
3859
  /**
3644
3860
  * The parsed CWE ids in the CWE-N catalog form the schema's cwe[] field asks
@@ -3658,6 +3874,15 @@ function impactFor(result) {
3658
3874
  return severityToImpactWithAliases(severity, SEMGREP_SEVERITY_ALIASES, DEFAULT_IMPACT);
3659
3875
  }
3660
3876
  /**
3877
+ * Publishes the band the impact already implies, so the two fields cannot
3878
+ * disagree. An absent or redacted severity stays unset: the unrated tag is what
3879
+ * tells a consumer the 0.5 was a default, and a "medium" here would hide it.
3880
+ */
3881
+ function severityFor(result) {
3882
+ if (!isPresent(result.extra?.severity)) return;
3883
+ return impactToSeverity(impactFor(result));
3884
+ }
3885
+ /**
3661
3886
  * Semgrep rule ids are dotted paths whose final segment is the rule name. The
3662
3887
  * JSON output carries no human-readable rule title anywhere -- unlike the SARIF
3663
3888
  * output, whose rule objects have `name` and `shortDescription` -- so one is
@@ -3794,6 +4019,8 @@ function buildRequirement$17(checkId, results, startTime) {
3794
4019
  ...refs.length > 0 ? { refs } : {},
3795
4020
  ...sourceLocation ? { sourceLocation } : {}
3796
4021
  });
4022
+ const severity = severityFor(representative);
4023
+ if (severity !== void 0) requirement.severity = severity;
3797
4024
  requirement.verificationMethod = VerificationMethodEnum.Automated;
3798
4025
  const controlType = deriveControlTypeFromTags(tags.nist);
3799
4026
  if (controlType !== void 0) requirement.controlType = controlType;
@@ -3870,7 +4097,7 @@ function buildCoverageRequirement(report, ruleCount, startTime) {
3870
4097
  * @returns HDF JSON string
3871
4098
  */
3872
4099
  async function convertSemgrepToHdf(input, converterVersion = "1.0.0") {
3873
- validateInputSize(input, "semgrep");
4100
+ validateInputSize$1(input, "semgrep");
3874
4101
  if (!input || input.trim().length === 0) throw new Error("semgrep: empty input");
3875
4102
  registerAllFingerprints();
3876
4103
  const detected = detectConverter(input);
@@ -4106,7 +4333,7 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
4106
4333
  const version = rep.version;
4107
4334
  if (name && version) {
4108
4335
  const ecosystem = ecosystemFromSnykPackageManager(packageManager);
4109
- const pkg = buildAffectedPackage$2({
4336
+ const pkg = buildAffectedPackage$1({
4110
4337
  name,
4111
4338
  version,
4112
4339
  ecosystem,
@@ -4153,7 +4380,7 @@ function convertSingleProject(report, resultsChecksum, scanTime) {
4153
4380
  */
4154
4381
  async function convertSnykToHdf(input, converterVersion = "1.0.0") {
4155
4382
  if (!input || input.trim().length === 0) throw new Error("snyk: empty input");
4156
- validateInputSize(input, "snyk");
4383
+ validateInputSize$1(input, "snyk");
4157
4384
  registerAllFingerprints();
4158
4385
  const detected = detectConverter(input);
4159
4386
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
@@ -4261,31 +4488,27 @@ function buildCvssEntries$7(vuln) {
4261
4488
  }
4262
4489
  function mapGrypeTypeToEcosystem(grypeType) {
4263
4490
  switch ((grypeType ?? "").toLowerCase()) {
4264
- case "rpm": return Ecosystem.RPM;
4265
- case "deb": return Ecosystem.Deb;
4266
- case "npm": return Ecosystem.Npm;
4267
4491
  case "python": return Ecosystem.Pypi;
4268
- case "gem": return Ecosystem.Gem;
4269
4492
  case "go-module": return Ecosystem.Go;
4270
4493
  case "java-archive":
4271
4494
  case "jenkins-plugin": return Ecosystem.Maven;
4272
4495
  case "dotnet": return Ecosystem.Nuget;
4273
4496
  case "rust-crate": return Ecosystem.Cargo;
4274
- default: return Ecosystem.Generic;
4497
+ default: return ecosystemFromPurlType(grypeType);
4275
4498
  }
4276
4499
  }
4277
4500
  function buildAffectedPackages(match) {
4278
4501
  const artifact = match.artifact;
4279
- const pkg = {
4502
+ const fix = match.vulnerability.fix;
4503
+ const pkg = buildAffectedPackage$1({
4280
4504
  name: artifact.name,
4281
4505
  version: artifact.version,
4282
- ecosystem: mapGrypeTypeToEcosystem(artifact.type)
4283
- };
4284
- if (artifact.cpes && artifact.cpes.length > 0 && artifact.cpes[0]) pkg.cpe = artifact.cpes[0];
4285
- if (artifact.purl) pkg.purl = artifact.purl;
4286
- const fix = match.vulnerability.fix;
4287
- if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
4288
- return [pkg];
4506
+ ecosystem: mapGrypeTypeToEcosystem(artifact.type),
4507
+ cpe: artifact.cpes?.[0],
4508
+ purl: artifact.purl,
4509
+ fixedInVersion: fix?.state === "fixed" ? fix.versions?.[0] : void 0
4510
+ });
4511
+ return pkg ? [pkg] : [];
4289
4512
  }
4290
4513
  const CWE_ID_PATTERN$2 = /^CWE-[1-9]\d*$/;
4291
4514
  function extractCwe(raw) {
@@ -4398,7 +4621,7 @@ function buildComponent$2(report, targetName) {
4398
4621
  return component;
4399
4622
  }
4400
4623
  async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
4401
- validateInputSize(input, "grype");
4624
+ validateInputSize$1(input, "grype");
4402
4625
  const resultsChecksum = await inputChecksum(input);
4403
4626
  const grypeData = parseJSON(input);
4404
4627
  const requirements = [];
@@ -4472,9 +4695,7 @@ function buildWaiverOverride(ar) {
4472
4695
  let reason = ar.decision_details || ar.name || "";
4473
4696
  if (!reason) reason = "Risk accepted in DefectDojo";
4474
4697
  const appliedAt = (ar.created ? parseTimestamp(ar.created) : null) ?? /* @__PURE__ */ new Date();
4475
- const oneYearOut = /* @__PURE__ */ new Date();
4476
- oneYearOut.setTime(appliedAt.getTime());
4477
- oneYearOut.setFullYear(oneYearOut.getFullYear() + 1);
4698
+ const oneYearOut = defaultOverrideExpiry(appliedAt);
4478
4699
  const expiresAt = (ar.expiration_date ? parseTimestamp(ar.expiration_date) : null) ?? oneYearOut;
4479
4700
  return {
4480
4701
  type: OverrideType.Waiver,
@@ -4506,9 +4727,7 @@ function falsePositiveReviewer(f) {
4506
4727
  function buildFalsePositiveOverride(f) {
4507
4728
  const reason = f.mitigation || "Marked as false positive in DefectDojo";
4508
4729
  const appliedAt = (f.mitigated ? parseTimestamp(f.mitigated) : null) ?? parseFindingDate(f.date) ?? /* @__PURE__ */ new Date();
4509
- const oneYearOut = /* @__PURE__ */ new Date();
4510
- oneYearOut.setTime(appliedAt.getTime());
4511
- oneYearOut.setUTCFullYear(oneYearOut.getUTCFullYear() + 1);
4730
+ const oneYearOut = defaultOverrideExpiry(appliedAt);
4512
4731
  return {
4513
4732
  type: OverrideType.FalsePositive,
4514
4733
  status: ResultStatus.NotApplicable,
@@ -4641,7 +4860,7 @@ function scannerName(f) {
4641
4860
  return f.related_fields?.test?.test_type?.name || "DefectDojo";
4642
4861
  }
4643
4862
  async function convertDefectDojoToHdf(input, converterVersion = "0.1.0") {
4644
- validateInputSize(input, "defectdojo");
4863
+ validateInputSize$1(input, "defectdojo");
4645
4864
  const resultsChecksum = await inputChecksum(input);
4646
4865
  const parsed = parseJSON(input);
4647
4866
  const findings = Array.isArray(parsed) ? parsed : parsed.results ?? [];
@@ -4720,7 +4939,7 @@ function decodeEntitiesDeep(value) {
4720
4939
  * Convert Nessus XML scan results to HDF format
4721
4940
  */
4722
4941
  async function convertNessusToHdf(nessusXml, converterVersion = "1.0.0") {
4723
- validateInputSize(nessusXml, "nessus");
4942
+ validateInputSize$1(nessusXml, "nessus");
4724
4943
  const resultsChecksum = await inputChecksum(nessusXml);
4725
4944
  const parsed = decodeEntitiesDeep(parseXmlWithArrays(nessusXml, [
4726
4945
  "preference",
@@ -5181,7 +5400,7 @@ function foldNistControls(cweIds, owaspIds) {
5181
5400
  * @returns HDF JSON string
5182
5401
  */
5183
5402
  async function convertSonarqubeToHdf(input, converterVersion = "1.0.0") {
5184
- validateInputSize(input, "sonarqube");
5403
+ validateInputSize$1(input, "sonarqube");
5185
5404
  const resultsChecksum = await inputChecksum(input);
5186
5405
  const sonarData = parseJSON(input);
5187
5406
  if (!sonarData || typeof sonarData !== "object") throw new Error("Invalid SonarQube structure: not a valid JSON object");
@@ -5519,7 +5738,7 @@ function buildRequirement$15(rule) {
5519
5738
  * @returns HDF JSON string
5520
5739
  */
5521
5740
  async function convertAwsConfigToHdf(input, converterVersion = "1.0.0") {
5522
- validateInputSize(input, "aws-config");
5741
+ validateInputSize$1(input, "aws-config");
5523
5742
  const resultsChecksum = await inputChecksum(input);
5524
5743
  const data = parseJSON(input);
5525
5744
  if (!data || typeof data !== "object") throw new Error("Invalid AWS Config export: not a valid JSON object");
@@ -5657,7 +5876,7 @@ function parseInput(input) {
5657
5876
  * @returns HDF JSON string
5658
5877
  */
5659
5878
  async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
5660
- validateInputSize(input, "checkov");
5879
+ validateInputSize$1(input, "checkov");
5661
5880
  registerAllFingerprints();
5662
5881
  const detected = detectConverter(input);
5663
5882
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
@@ -5842,7 +6061,7 @@ function buildGosecExtensions(report) {
5842
6061
  * @returns HDF JSON string
5843
6062
  */
5844
6063
  async function convertGosecToHdf(input, converterVersion = "1.0.0") {
5845
- validateInputSize(input, "gosec");
6064
+ validateInputSize$1(input, "gosec");
5846
6065
  registerAllFingerprints();
5847
6066
  const detected = detectConverter(input);
5848
6067
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
@@ -5916,7 +6135,7 @@ function convertVulnToRequirement(vuln) {
5916
6135
  return req;
5917
6136
  }
5918
6137
  async function convertNiktoToHdf(input, converterVersion = "unknown") {
5919
- validateInputSize(input, "nikto");
6138
+ validateInputSize$1(input, "nikto");
5920
6139
  const resultsChecksum = await inputChecksum(input);
5921
6140
  const niktoData = parseJSON(input);
5922
6141
  const vulnGroups = /* @__PURE__ */ new Map();
@@ -6116,7 +6335,7 @@ function parseZapTimestamp(s) {
6116
6335
  return parseTimestamp(s) ?? void 0;
6117
6336
  }
6118
6337
  async function convertZapToHdf(input, converterVersion = "1.0.0") {
6119
- validateInputSize(input, "zap");
6338
+ validateInputSize$1(input, "zap");
6120
6339
  registerAllFingerprints();
6121
6340
  const detected = detectConverter(input);
6122
6341
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
@@ -6655,7 +6874,7 @@ function buildBom(parts) {
6655
6874
  * oversized or undetectable input.
6656
6875
  */
6657
6876
  function parseBom(input) {
6658
- validateInputSize(input, CONVERTER);
6877
+ validateInputSize$1(input, CONVERTER);
6659
6878
  const obj = parseJSON(input);
6660
6879
  const detected = detectFormat(obj);
6661
6880
  if (!detected) throw new Error("bom-parser: could not detect a supported BOM format (expected CycloneDX or SPDX JSON)");
@@ -7273,9 +7492,7 @@ function analysisOverride(vuln, fallback) {
7273
7492
  default: return;
7274
7493
  }
7275
7494
  const appliedAt = analysisAppliedAt(vuln, fallback);
7276
- const expiresAt = /* @__PURE__ */ new Date();
7277
- expiresAt.setTime(appliedAt.getTime());
7278
- expiresAt.setUTCFullYear(expiresAt.getUTCFullYear() + 1);
7495
+ const expiresAt = defaultOverrideExpiry(appliedAt);
7279
7496
  const override = {
7280
7497
  type: disposition,
7281
7498
  status: effectiveStatus,
@@ -7303,7 +7520,7 @@ function analysisOverride(vuln, fallback) {
7303
7520
  */
7304
7521
  async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
7305
7522
  if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
7306
- validateInputSize(input, "cyclonedx");
7523
+ validateInputSize$1(input, "cyclonedx");
7307
7524
  const bom = parseJSON(input);
7308
7525
  if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
7309
7526
  if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
@@ -7413,10 +7630,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
7413
7630
  * @returns CSV string with sanitized output
7414
7631
  */
7415
7632
  function convertHdfToCsv(input) {
7416
- validateInputSize(input, "hdf-to-csv");
7417
- const hdf = parseHdf(input);
7418
- if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
7419
- if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
7633
+ const { doc: hdfDoc } = requireHdfResults(input, "hdf-to-csv");
7634
+ const hdf = hdfDoc;
7420
7635
  const rows = [];
7421
7636
  const components = hdf.components || [];
7422
7637
  const targetList = components.length > 0 ? components.map((t) => ({
@@ -7430,35 +7645,64 @@ function convertHdfToCsv(input) {
7430
7645
  fqdn: "",
7431
7646
  ipAddress: ""
7432
7647
  }];
7433
- for (const baseline of hdf.baselines) for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
7648
+ for (const baseline of hdf.baselines) {
7649
+ if (!Array.isArray(baseline.requirements) || baseline.requirements.length === 0) throw new Error(`hdf-to-csv: baseline "${text(baseline.name)}" has no requirements`);
7650
+ for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
7651
+ }
7434
7652
  return buildCsv(rows, { sanitize: true });
7435
7653
  }
7436
7654
  /**
7655
+ * A schema-required number as a number. Go decodes an absent one to the zero
7656
+ * value and cannot tell it apart from a real zero — impact 0 legitimately means
7657
+ * Not Applicable — so distinguishing them would need a schema validation pass
7658
+ * the converters deliberately do not do. Rendering 0 here matches what Go emits,
7659
+ * where reading .toFixed off an absent value threw a raw TypeError instead.
7660
+ */
7661
+ function numeric(value) {
7662
+ return typeof value === "number" ? value : 0;
7663
+ }
7664
+ /**
7665
+ * A schema-required string as text. Nested-invalid input reaches the converter
7666
+ * with these absent, and an absent value reached sanitizeCsvValue's String() as
7667
+ * the literal "undefined" — where Go, formatting a zero-value string, wrote
7668
+ * nothing. Every schema-required STRING cell goes through here; the numeric
7669
+ * ones go through numeric(), which serves the same purpose for a different type.
7670
+ */
7671
+ function text(value) {
7672
+ return typeof value === "string" ? value : "";
7673
+ }
7674
+ /** The requirement id as text; also names the requirement in the no-results error. */
7675
+ function requirementIdText$1(id) {
7676
+ return text(id);
7677
+ }
7678
+ /**
7437
7679
  * Create a CSV row from baseline, requirement, and target data
7438
7680
  */
7439
7681
  function createRow(baseline, requirement, target) {
7440
- const description = requirement.descriptions.find((d) => d.label === "default")?.data || "";
7682
+ const [firstResult] = Array.isArray(requirement.results) ? requirement.results : [];
7683
+ if (!firstResult) throw new Error(`hdf-to-csv: requirement "${requirementIdText$1(requirement.id)}" has no results`);
7684
+ if (!Array.isArray(requirement.descriptions) || requirement.descriptions.length === 0) throw new Error(`hdf-to-csv: requirement "${requirementIdText$1(requirement.id)}" has no descriptions`);
7685
+ const description = descriptionByLabel(requirement, "default");
7441
7686
  const check = descriptionByLabel(requirement, "check");
7442
7687
  const fix = descriptionByLabel(requirement, "fix");
7443
7688
  const rationale = descriptionByLabel(requirement, "rationale");
7444
7689
  const code = requirement.code ?? "";
7445
7690
  const references = flattenRefs(requirement.refs);
7446
7691
  const severity = getSeverity(requirement);
7447
- const firstResult = requirement.results[0];
7448
- const status = firstResult?.status || "";
7449
- const message = firstResult?.message || "";
7692
+ const status = firstResult.status;
7693
+ const message = firstResult.message ?? "";
7450
7694
  const nistControls = extractArrayFromTags(requirement.tags, "nist");
7451
7695
  const cciControls = extractArrayFromTags(requirement.tags, "cci");
7452
7696
  const effectiveStatus = requirementEffectiveStatus(requirement);
7453
- const effectiveImpact = (requirement.effectiveImpact ?? requirement.impact).toFixed(1);
7697
+ const effectiveImpact = numeric(requirement.effectiveImpact ?? requirement.impact).toFixed(2);
7454
7698
  const disposition = requirement.disposition ? String(requirement.disposition) : "";
7455
7699
  return {
7456
- "Baseline ID": baseline.name,
7700
+ "Baseline ID": text(baseline.name),
7457
7701
  "Baseline Version": baseline.version || "",
7458
7702
  "Baseline Title": baseline.title || "",
7459
- "Target ID": target.name,
7460
- "Target Type": target.type,
7461
- "Requirement ID": requirement.id,
7703
+ "Target ID": text(target.name),
7704
+ "Target Type": text(target.type),
7705
+ "Requirement ID": requirementIdText$1(requirement.id),
7462
7706
  "Requirement Title": requirement.title || "",
7463
7707
  "Description": description,
7464
7708
  "Check": check,
@@ -7467,8 +7711,8 @@ function createRow(baseline, requirement, target) {
7467
7711
  "Code": code,
7468
7712
  "References": references,
7469
7713
  "Severity": severity,
7470
- "Impact": requirement.impact.toFixed(1),
7471
- "Status": String(status),
7714
+ "Impact": numeric(requirement.impact).toFixed(2),
7715
+ "Status": text(status),
7472
7716
  "NIST Controls": nistControls,
7473
7717
  "CCI Controls": cciControls,
7474
7718
  "Control Type": requirement.controlType ?? "",
@@ -7482,8 +7726,8 @@ function createRow(baseline, requirement, target) {
7482
7726
  "Applied By": joinOverrides(requirement.statusOverrides, (o) => o.appliedBy?.identifier ?? ""),
7483
7727
  "Expires At": joinOverrides(requirement.statusOverrides, (o) => formatExpires(o.expiresAt)),
7484
7728
  "CVSS": cvssScores(requirement.cvss),
7485
- "CWE": Array.isArray(requirement.cwe) ? requirement.cwe.join("; ") : "",
7486
- "EPSS": requirement.epss ? requirement.epss.score.toFixed(5) : "",
7729
+ "CWE": (requirement.cwe ?? []).join("; "),
7730
+ "EPSS": requirement.epss ? numeric(requirement.epss.score).toFixed(5) : "",
7487
7731
  "KEV": requirement.kev ? requirement.kev.inKev ? "true" : "false" : "",
7488
7732
  "Target FQDN": target.fqdn,
7489
7733
  "Target IP": target.ipAddress
@@ -7497,6 +7741,7 @@ function joinOverrides(overrides, pick) {
7497
7741
  if (!Array.isArray(overrides)) return "";
7498
7742
  const out = [];
7499
7743
  for (const o of overrides) {
7744
+ if (o === null || o === void 0) continue;
7500
7745
  const v = pick(o);
7501
7746
  if (v) out.push(v);
7502
7747
  }
@@ -7512,14 +7757,17 @@ function formatExpires(value) {
7512
7757
  }
7513
7758
  /**
7514
7759
  * Render each CVSS entry's score (computed when present, else base) to one
7515
- * decimal, joined with '; ' to preserve multi-CVE findings.
7760
+ * decimal — the precision the CVSS spec defines and the source carries — joined
7761
+ * with '; ' to preserve multi-CVE findings. The Go peer rounds through a shared
7762
+ * helper matching toFixed, so a .x5 score renders the same digits in both.
7516
7763
  */
7517
7764
  function cvssScores(entries) {
7518
7765
  if (!Array.isArray(entries)) return "";
7519
7766
  const out = [];
7520
7767
  for (const c of entries) {
7768
+ if (c === null || c === void 0) continue;
7521
7769
  const score = c.computedScore ?? c.baseScore;
7522
- if (score !== void 0) out.push(score.toFixed(1));
7770
+ if (typeof score === "number") out.push(score.toFixed(1));
7523
7771
  }
7524
7772
  return out.join("; ");
7525
7773
  }
@@ -7531,13 +7779,10 @@ function getSeverity(requirement) {
7531
7779
  if ("severity" in requirement.tags) {
7532
7780
  const sev = requirement.tags.severity;
7533
7781
  if (typeof sev === "string") return sev;
7534
- if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
7782
+ if (Array.isArray(sev) && typeof sev[0] === "string") return sev[0];
7535
7783
  }
7536
7784
  }
7537
- const impact = requirement.impact;
7538
- if (impact >= .7) return "high";
7539
- if (impact >= .4) return "medium";
7540
- return "low";
7785
+ return impactToSeverity(requirement.impact);
7541
7786
  }
7542
7787
  /**
7543
7788
  * Extract array values from tags object and join with semicolons
@@ -7545,18 +7790,20 @@ function getSeverity(requirement) {
7545
7790
  function extractArrayFromTags(tags, key) {
7546
7791
  if (!tags || typeof tags !== "object") return "";
7547
7792
  const value = tags[key];
7548
- if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
7793
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string").join("; ");
7549
7794
  return "";
7550
7795
  }
7551
7796
  /**
7552
7797
  * Get the data of the first description matching a label. Empty when absent.
7553
7798
  */
7554
7799
  function descriptionByLabel(requirement, label) {
7555
- return requirement.descriptions.find((d) => d.label === label)?.data || "";
7800
+ return requirement.descriptions.find((d) => d !== null && d !== void 0 && d.label === label)?.data || "";
7556
7801
  }
7557
7802
  /**
7558
7803
  * Flatten a requirement's refs to one string: each Reference rendered as its
7559
- * url/uri (or a string `ref`); array-form refs are skipped. Joined with '; ' to
7804
+ * url/uri (or a string `ref`); array-form refs are skipped here, though a document
7805
+ * containing one never reaches this function in Go, whose typed decode rejects
7806
+ * it outright — a reject-versus-degrade split tracked on its own card. Joined with '; ' to
7560
7807
  * match the NIST/CCI column convention.
7561
7808
  */
7562
7809
  function flattenRefs(refs) {
@@ -8234,7 +8481,7 @@ function buildEvidences(req) {
8234
8481
  return out;
8235
8482
  }
8236
8483
  function floatMetric(f) {
8237
- return String(f);
8484
+ return formatJsonNumber(f);
8238
8485
  }
8239
8486
  //#endregion
8240
8487
  //#region converters/hdf-to-asff/typescript/converter.ts
@@ -8263,7 +8510,7 @@ const MAX_DESCRIPTION = 1024;
8263
8510
  const EPOCH_SENTINEL = "1970-01-01T00:00:00Z";
8264
8511
  function convertHdfToAsff(input, converterVersion = "0.1.0") {
8265
8512
  const name = "hdf-to-asff";
8266
- validateInputSize(input, name);
8513
+ validateInputSize$1(input, name);
8267
8514
  const doc = parseHdf(input);
8268
8515
  const baselines = asArr(doc.baselines);
8269
8516
  if (!baselines) throw new Error(`${name}: invalid HDF structure: missing baselines field`);
@@ -8539,7 +8786,7 @@ function convertGroups(groups) {
8539
8786
  * @returns HDF Results JSON string
8540
8787
  */
8541
8788
  async function convertSplunkToHdf(input, converterVersion = "1.0.0") {
8542
- validateInputSize(input, "splunk");
8789
+ validateInputSize$1(input, "splunk");
8543
8790
  const resultsChecksum = await inputChecksum(input);
8544
8791
  const events = parseJSON(input);
8545
8792
  if (!Array.isArray(events) || events.length === 0) throw new Error("No Splunk events found in input");
@@ -8655,9 +8902,24 @@ function convertStatistics(stats) {
8655
8902
  * The document is walked as a plain JSON tree and every key is emitted in
8656
8903
  * source-JSON order, so the output can never silently lag a schema addition the
8657
8904
  * way the previous hand-maintained struct mirror did (it dropped ~30 post-v3.2
8658
- * fields). JSON.parse preserves object key insertion order, and the Go converter
8659
- * walks the same normalized JSON in the same order, so the two languages emit
8660
- * output that is identical after the shared XML golden normalization.
8905
+ * fields). The Go converter walks the same normalized JSON in the same order and
8906
+ * emits identical output after the shared XML golden normalization for every
8907
+ * shape this repo's fixtures and parity tests cover.
8908
+ *
8909
+ * Equality is NOT guaranteed in general. The largest seam is that JSON.parse
8910
+ * discards information before this builder ever runs, so anything it drops is
8911
+ * unreachable from the serializer; shared/xml-divergence-cases.json is the
8912
+ * census of THAT seam, read and pinned by both languages -- a duplicate key
8913
+ * keeps only the last value, an integer-like key is hoisted ahead of its
8914
+ * siblings, and a number too large for a double arrives as Infinity with its
8915
+ * token text gone. Closing any of them means parsing untrusted input with a
8916
+ * parser that preserves order, duplicates and the number token, a worse trade.
8917
+ *
8918
+ * That table is NOT the whole list, and at least one divergence has a different
8919
+ * cause: the underlying builder caps nesting depth where the Go peer does not,
8920
+ * so a deep enough document converts there and throws here (pinned by the
8921
+ * nesting-depth tests in both languages). A shape outside the fixture corpus
8922
+ * needs checking against the peer rather than assuming it is covered.
8661
8923
  */
8662
8924
  /**
8663
8925
  * Maps a container (plural) key to the element name emitted for each of its
@@ -8688,13 +8950,33 @@ function singularFor(key) {
8688
8950
  return PLURAL_TO_SINGULAR[key] ?? "item";
8689
8951
  }
8690
8952
  /**
8691
- * Wrap a primitive so fast-xml-parser renders it as element text rather than an
8692
- * attribute.
8953
+ * Encode a JSON key to a legal XML element name, reporting whether it had to
8954
+ * change. HDF leaves object keys unconstrained — tags carry vendor-namespaced
8955
+ * keys like "sonarqube/hash", and additionalProperties lets a converter park
8956
+ * native fields anywhere — while XML constrains element names to Name, so an
8957
+ * unencoded key produced a document no parser could read.
8958
+ *
8959
+ * The kept set is ASCII — [A-Za-z0-9._-] — not the far wider set XML Name
8960
+ * allows, so the two languages agree by construction rather than by way of two
8961
+ * Unicode tables that can drift apart. It costs nothing here: no tag key in this
8962
+ * package's converter fixtures is non-ASCII. ':' is deliberately not kept —
8963
+ * it is a legal Name character but would invent an undeclared namespace prefix.
8964
+ *
8965
+ * Mirrored by xmlElementName in the Go converter and pinned to a shared table.
8693
8966
  */
8694
- function wrapScalar(value) {
8695
- return { "#text": value };
8967
+ function xmlElementName(key) {
8968
+ const out = [...key].map((ch) => /[A-Za-z0-9.\-_]/.test(ch) ? ch : "_").join("");
8969
+ const [first = ""] = [...out];
8970
+ const name = /[A-Za-z_]/.test(first) ? out : `_${out}`;
8971
+ return [name, name !== key];
8696
8972
  }
8697
8973
  /**
8974
+ * The attribute holding the original key of a rewritten element. The prefix is
8975
+ * what separates an attribute from a child element in the builder; it cannot
8976
+ * collide with a child, because any key spelled this way is itself rewritten.
8977
+ */
8978
+ const NAME_ATTR = "@_name";
8979
+ /**
8698
8980
  * True for a JSON scalar (string, number, bool) or a null/undefined placeholder.
8699
8981
  * Nested objects and arrays are not scalar, which selects the wrapped
8700
8982
  * object-array rendering.
@@ -8702,37 +8984,48 @@ function wrapScalar(value) {
8702
8984
  function isScalar(value) {
8703
8985
  return value === null || value === void 0 || typeof value !== "object";
8704
8986
  }
8987
+ function present(value) {
8988
+ return value !== null && value !== void 0;
8989
+ }
8705
8990
  /**
8706
- * Convert an object-array item into its fast-xml-parser representation. Items
8707
- * are objects (the object-array rule), but a scalar in a heterogeneous array is
8708
- * wrapped so it renders as <singular>text</singular>, matching the Go walker.
8991
+ * Render one JSON key as the sibling nodes it becomes, mirroring the Go
8992
+ * writeValue case for case. A list rather than a single node because a scalar
8993
+ * array repeats its key unwrapped, and because two keys can encode to one
8994
+ * element name — preserveOrder keeps each at its own position, which a plain
8995
+ * object keyed by element name could not.
8709
8996
  */
8710
- function toXmlValue(value) {
8711
- if (value === null || value === void 0) return;
8712
- if (Array.isArray(value)) return { item: value.filter((v) => v !== null && v !== void 0).map(toXmlValue) };
8713
- if (typeof value === "object") return buildObject(value);
8714
- return wrapScalar(value);
8997
+ function nodesFor(key, value) {
8998
+ const [name, rewritten] = xmlElementName(key);
8999
+ const attrs = rewritten ? { ":@": { [NAME_ATTR]: xmlSafeText(key) } } : {};
9000
+ if (Array.isArray(value)) {
9001
+ if (value.length === 0) return [{
9002
+ [name]: [],
9003
+ ...attrs
9004
+ }];
9005
+ if (value.every(isScalar)) return value.filter(present).flatMap((item) => nodesFor(key, item));
9006
+ const child = singularFor(key);
9007
+ return [{
9008
+ [name]: value.filter(present).flatMap((item) => nodesFor(child, item)),
9009
+ ...attrs
9010
+ }];
9011
+ }
9012
+ if (typeof value === "object") return [{
9013
+ [name]: buildNodes(value),
9014
+ ...attrs
9015
+ }];
9016
+ const text = typeof value === "string" ? xmlSafeText(value) : typeof value === "number" ? formatJsonNumber(value) : value;
9017
+ return [{
9018
+ [name]: [{ "#text": text }],
9019
+ ...attrs
9020
+ }];
8715
9021
  }
8716
9022
  /**
8717
- * Walk a JSON object into a fast-xml-parser-ready structure, emitting keys in
8718
- * source order. Null-valued keys are omitted (parity with the old omitempty
8719
- * semantics). A scalar array repeats its key unwrapped (<nist>AU-12</nist>); an
8720
- * object array becomes a wrapper whose per-item child name comes from the
8721
- * plural->singular map; an empty array becomes an empty wrapper element.
9023
+ * Walk a JSON object into fast-xml-parser preserveOrder nodes, emitting keys in
9024
+ * source order. Null-valued keys are omitted, matching the old omitempty
9025
+ * semantics and the Go peer.
8722
9026
  */
8723
- function buildObject(obj) {
8724
- const out = {};
8725
- for (const [key, value] of Object.entries(obj)) {
8726
- if (value === null || value === void 0) continue;
8727
- if (Array.isArray(value)) {
8728
- const items = value.filter((v) => v !== null && v !== void 0);
8729
- if (value.length === 0) out[key] = {};
8730
- else if (value.every(isScalar)) out[key] = items.map((v) => wrapScalar(v));
8731
- else out[key] = { [singularFor(key)]: items.map(toXmlValue) };
8732
- } else if (typeof value === "object") out[key] = buildObject(value);
8733
- else out[key] = wrapScalar(value);
8734
- }
8735
- return out;
9027
+ function buildNodes(obj) {
9028
+ return Object.entries(obj).flatMap(([key, value]) => present(value) ? nodesFor(key, value) : []);
8736
9029
  }
8737
9030
  /**
8738
9031
  * Convert HDF Results JSON to XML.
@@ -8741,11 +9034,14 @@ function buildObject(obj) {
8741
9034
  * @returns XML string
8742
9035
  */
8743
9036
  function convertHdfToXml(input) {
8744
- validateInputSize(input, "hdf-to-xml");
9037
+ validateInputSize$1(input, "hdf-to-xml");
8745
9038
  const hdf = parseHdf(input);
8746
9039
  if (!hdf || typeof hdf !== "object" || Array.isArray(hdf) || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
8747
9040
  if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
8748
- return buildXml({ HdfResults: buildObject(hdf) });
9041
+ return `${buildXml([{ HdfResults: buildNodes(hdf) }], {
9042
+ preserveOrder: true,
9043
+ attributeNamePrefix: "@_"
9044
+ }).trim()}\n`;
8749
9045
  }
8750
9046
  //#endregion
8751
9047
  //#region converters/gitlab-to-hdf/typescript/converter.ts
@@ -8868,7 +9164,7 @@ function buildCodeDesc$5(scanType, location) {
8868
9164
  }
8869
9165
  }
8870
9166
  async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
8871
- validateInputSize(input, "gitlab");
9167
+ validateInputSize$1(input, "gitlab");
8872
9168
  const resultsChecksum = await inputChecksum(input);
8873
9169
  const report = parseJSON(input);
8874
9170
  const scanType = report.scan?.type ?? "sast";
@@ -8970,25 +9266,14 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
8970
9266
  const BASELINE_NAME = "Trivy Scan";
8971
9267
  const STATIC_CCI = nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
8972
9268
  const CWE_ID_PATTERN$1 = /^CWE-[1-9]\d*$/;
8973
- const IMPACT_MAP = {
8974
- critical: .9,
8975
- high: .7,
8976
- medium: .5,
8977
- low: .3,
8978
- unknown: .5,
8979
- none: 0,
8980
- info: 0
8981
- };
8982
- function severityToImpact$2(severity) {
8983
- return IMPACT_MAP[(severity || "").toLowerCase()] ?? .5;
8984
- }
9269
+ const SEVERITY_ALIASES = { critical: .9 };
8985
9270
  /**
8986
9271
  * Route Trivy output to HDF Results: native Trivy JSON (SchemaVersion 2) is
8987
9272
  * parsed here; SARIF / CycloneDX / ASFF / GitLab are delegated to their
8988
9273
  * converters. TypeScript peer of shared/go/../trivy-to-hdf converter.go.
8989
9274
  */
8990
9275
  async function convertTrivyToHdf(input, converterVersion = "1.0.0") {
8991
- validateInputSize(input, "trivy");
9276
+ validateInputSize$1(input, "trivy");
8992
9277
  if (!input?.trim()) throw new Error("trivy: empty input");
8993
9278
  const probe = parseJSON(input);
8994
9279
  if (typeof probe !== "object" || probe === null || Array.isArray(probe)) throw new Error("trivy: input is not a JSON object");
@@ -9031,7 +9316,7 @@ async function convertNative(input, converterVersion) {
9031
9316
  function convertVuln(v, res, startTime) {
9032
9317
  const descriptions = [{
9033
9318
  label: "default",
9034
- data: firstNonEmpty(v.Description, v.Title, v.VulnerabilityID)
9319
+ data: firstNonEmpty(v.Description ?? "", v.Title ?? "", v.VulnerabilityID ?? "")
9035
9320
  }];
9036
9321
  if (v.FixedVersion) descriptions.push({
9037
9322
  label: "fix",
@@ -9049,7 +9334,7 @@ function convertVuln(v, res, startTime) {
9049
9334
  id: `Trivy/${v.VulnerabilityID ?? ""}`,
9050
9335
  title: `Trivy found ${v.VulnerabilityID ?? ""} in ${pkgLabel(v.PkgName, v.InstalledVersion)}`,
9051
9336
  descriptions,
9052
- impact: severityToImpact$2(v.Severity ?? ""),
9337
+ impact: severityToImpactWithAliases(v.Severity, SEVERITY_ALIASES, .5),
9053
9338
  tags,
9054
9339
  cwe: filterCwes(v.CweIDs),
9055
9340
  cvss: buildCvssEntries$4(v.CVSS),
@@ -9061,18 +9346,18 @@ function convertVuln(v, res, startTime) {
9061
9346
  status: ResultStatus.Failed,
9062
9347
  codeDesc: buildVulnCodeDesc(v, res),
9063
9348
  startTime,
9064
- message: `Severity: ${firstNonEmpty(v.Severity, "UNKNOWN")}`
9349
+ message: `Severity: ${firstNonEmpty(v.Severity ?? "", "UNKNOWN")}`
9065
9350
  }]
9066
9351
  };
9067
- const ap = buildAffectedPackage$1(v);
9068
- if (ap.purl) req.affectedPackages = [ap];
9352
+ const ap = buildTrivyAffectedPackage(v);
9353
+ if (ap) req.affectedPackages = [ap];
9069
9354
  if (v.PkgPath) req.sourceLocation = { ref: v.PkgPath };
9070
9355
  return req;
9071
9356
  }
9072
9357
  function convertMisconf(m, res, startTime) {
9073
9358
  const descriptions = [{
9074
9359
  label: "default",
9075
- data: firstNonEmpty(m.Description, m.Message, m.Title)
9360
+ data: firstNonEmpty(m.Description ?? "", m.Message ?? "", m.Title ?? "")
9076
9361
  }];
9077
9362
  if (m.Resolution) descriptions.push({
9078
9363
  label: "fix",
@@ -9084,10 +9369,10 @@ function convertMisconf(m, res, startTime) {
9084
9369
  });
9085
9370
  markUnratedSeverity(tags, m.Severity);
9086
9371
  const req = {
9087
- id: `Trivy/${firstNonEmpty(m.ID, m.AVDID)}`,
9372
+ id: `Trivy/${firstNonEmpty(m.ID ?? "", m.AVDID ?? "")}`,
9088
9373
  title: m.Title,
9089
9374
  descriptions,
9090
- impact: severityToImpact$2(m.Severity ?? ""),
9375
+ impact: severityToImpactWithAliases(m.Severity, SEVERITY_ALIASES, .5),
9091
9376
  tags,
9092
9377
  refs: buildRefs$5([m.PrimaryURL, ...m.References ?? []]),
9093
9378
  code: JSON.stringify(m, null, 2),
@@ -9095,7 +9380,7 @@ function convertMisconf(m, res, startTime) {
9095
9380
  verificationMethod: VerificationMethodEnum.Automated,
9096
9381
  results: [{
9097
9382
  status: misconfStatus(m.Status),
9098
- codeDesc: firstNonEmpty(m.Message, m.Title),
9383
+ codeDesc: firstNonEmpty(m.Message ?? "", m.Title ?? ""),
9099
9384
  startTime
9100
9385
  }]
9101
9386
  };
@@ -9117,9 +9402,9 @@ function convertSecret(s, res, startTime) {
9117
9402
  title: s.Title,
9118
9403
  descriptions: [{
9119
9404
  label: "default",
9120
- data: `${firstNonEmpty(s.Title, s.RuleID)} detected in ${res.Target ?? ""} (value redacted by Trivy).`
9405
+ data: `${firstNonEmpty(s.Title ?? "", s.RuleID ?? "")} detected in ${res.Target ?? ""} (value redacted by Trivy).`
9121
9406
  }],
9122
- impact: severityToImpact$2(s.Severity ?? ""),
9407
+ impact: severityToImpactWithAliases(s.Severity, SEVERITY_ALIASES, .5),
9123
9408
  tags,
9124
9409
  code: JSON.stringify(s, null, 2),
9125
9410
  controlType: deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS),
@@ -9151,7 +9436,7 @@ function convertLicense(l, res, startTime) {
9151
9436
  label: "default",
9152
9437
  data: `Package ${l.PkgName ?? ""} uses the ${l.Name ?? ""} license (category: ${l.Category ?? ""}).`
9153
9438
  }],
9154
- impact: severityToImpact$2(l.Severity ?? ""),
9439
+ impact: severityToImpactWithAliases(l.Severity, SEVERITY_ALIASES, .5),
9155
9440
  tags,
9156
9441
  refs: buildRefs$5([l.Link]),
9157
9442
  code: JSON.stringify(l, null, 2),
@@ -9232,33 +9517,19 @@ function buildCvssEntries$4(m) {
9232
9517
  }
9233
9518
  return entries.length > 0 ? entries : void 0;
9234
9519
  }
9235
- const PURL_ECOSYSTEMS = {
9236
- deb: Ecosystem.Deb,
9237
- rpm: Ecosystem.RPM,
9238
- maven: Ecosystem.Maven,
9239
- npm: Ecosystem.Npm,
9240
- pypi: Ecosystem.Pypi,
9241
- gem: Ecosystem.Gem,
9242
- cargo: Ecosystem.Cargo,
9243
- golang: Ecosystem.Go,
9244
- nuget: Ecosystem.Nuget
9245
- };
9246
- function buildAffectedPackage$1(v) {
9247
- const ap = {};
9248
- if (v.PkgName) ap.name = v.PkgName;
9249
- if (v.InstalledVersion) ap.version = v.InstalledVersion;
9250
- if (v.FixedVersion) ap.fixedInVersion = v.FixedVersion;
9520
+ function buildTrivyAffectedPackage(v) {
9251
9521
  const purl = v.PkgIdentifier?.PURL;
9252
- if (purl) {
9253
- ap.purl = purl;
9254
- const eco = ecosystemFromPurl(purl);
9255
- if (eco) ap.ecosystem = eco;
9256
- }
9257
- return ap;
9522
+ return buildAffectedPackage$1({
9523
+ name: v.PkgName,
9524
+ version: v.InstalledVersion,
9525
+ fixedInVersion: v.FixedVersion,
9526
+ purl,
9527
+ ecosystem: purl ? ecosystemFromPurl(purl) : void 0
9528
+ });
9258
9529
  }
9259
9530
  function ecosystemFromPurl(purl) {
9260
- const type = (purl.replace(/^pkg:/, "").split(/[/@?]/)[0] ?? "").toLowerCase();
9261
- return PURL_ECOSYSTEMS[type];
9531
+ const eco = ecosystemFromPurlType(purl.replace(/^pkg:/, "").split(/[/@?]/)[0] ?? "");
9532
+ return eco === Ecosystem.Generic ? void 0 : eco;
9262
9533
  }
9263
9534
  function buildRefs$5(urls) {
9264
9535
  const seen = /* @__PURE__ */ new Set();
@@ -9285,10 +9556,6 @@ function digestPart(ref) {
9285
9556
  const at = ref.lastIndexOf("@");
9286
9557
  return at >= 0 ? ref.slice(at + 1) : ref;
9287
9558
  }
9288
- function firstNonEmpty(...vals) {
9289
- for (const v of vals) if (v) return v;
9290
- return "";
9291
- }
9292
9559
  function putIf(m, key, val) {
9293
9560
  if (val) m[key] = val;
9294
9561
  }
@@ -9448,7 +9715,7 @@ function buildRequirement$12(reqID, findings) {
9448
9715
  * @returns HDF JSON string
9449
9716
  */
9450
9717
  async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
9451
- validateInputSize(input, "trufflehog");
9718
+ validateInputSize$1(input, "trufflehog");
9452
9719
  const findings = !input || input.trim().length === 0 ? [] : parseFindings(input);
9453
9720
  const resultsChecksum = await inputChecksum(input);
9454
9721
  const limitedFindings = limitArrayWithWarning(findings, "finding");
@@ -9581,7 +9848,7 @@ function investigateReason(reason) {
9581
9848
  * @returns HDF JSON string
9582
9849
  */
9583
9850
  async function convertHipcheckToHdf(input, converterVersion = "1.0.0") {
9584
- validateInputSize(input, "hipcheck");
9851
+ validateInputSize$1(input, "hipcheck");
9585
9852
  if (!input || input.trim().length === 0) throw new Error("hipcheck: empty input");
9586
9853
  const report = parseJSON(input);
9587
9854
  if (!report.hipcheck_version && !report.repo_name) throw new Error("hipcheck: input does not look like a Hipcheck report");
@@ -9704,7 +9971,7 @@ function buildIssueCode(issue) {
9704
9971
  */
9705
9972
  async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
9706
9973
  if (!input || input.trim().length === 0) throw new Error("burpsuite: empty input");
9707
- validateInputSize(input, "burpsuite");
9974
+ validateInputSize$1(input, "burpsuite");
9708
9975
  const resultsChecksum = await inputChecksum(input);
9709
9976
  const parsed = parseXmlWithArrays(input, ["issue"]);
9710
9977
  if (!parsed.issues) throw new Error("burpsuite: invalid XML — missing <issues> root element");
@@ -10010,7 +10277,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
10010
10277
  */
10011
10278
  async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
10012
10279
  if (!input || input.trim().length === 0) throw new Error("dbprotect: empty input");
10013
- validateInputSize(input, "dbprotect");
10280
+ validateInputSize$1(input, "dbprotect");
10014
10281
  const resultsChecksum = await inputChecksum(input);
10015
10282
  const parsed = parseXmlWithArrays(input, [
10016
10283
  "item",
@@ -10093,15 +10360,15 @@ function buildCvss$1(vuln) {
10093
10360
  source: vuln.cve ?? vuln.id
10094
10361
  });
10095
10362
  }
10096
- const CWE_REGEX = /cwe[-_]?(\d+)/gi;
10097
10363
  /**
10098
- * Extracts canonical CWE-N identifiers from a free-form string.
10364
+ * Extracts canonical CWE-N identifiers from a free-form string, deduplicated
10365
+ * in source order.
10099
10366
  */
10100
10367
  function parseCwes(raw) {
10101
10368
  if (!raw) return [];
10102
10369
  const out = [];
10103
10370
  const seen = /* @__PURE__ */ new Set();
10104
- for (const m of raw.matchAll(CWE_REGEX)) {
10371
+ for (const m of raw.matchAll(CWE_PATTERN$1)) {
10105
10372
  const id = `CWE-${m[1]}`;
10106
10373
  if (seen.has(id)) continue;
10107
10374
  seen.add(id);
@@ -10128,7 +10395,9 @@ function debDistro(distro) {
10128
10395
  }
10129
10396
  /**
10130
10397
  * Maps a Twistlock package type plus the result's distro string to a schema
10131
- * Ecosystem value. Defaults to 'generic' for unknown types.
10398
+ * Ecosystem value. Only the Twistlock spellings that differ from the PURL type
10399
+ * vocabulary are listed; the rest defer to the shared resolver, which falls
10400
+ * back to 'generic' for unknown types.
10132
10401
  */
10133
10402
  function resolveEcosystem(packageType, distro) {
10134
10403
  const t = (packageType ?? "").toLowerCase();
@@ -10138,19 +10407,10 @@ function resolveEcosystem(packageType, distro) {
10138
10407
  if (rhelDistro(d)) return Ecosystem.RPM;
10139
10408
  if (debDistro(d)) return Ecosystem.Deb;
10140
10409
  return Ecosystem.Generic;
10141
- case "rpm": return Ecosystem.RPM;
10142
- case "deb": return Ecosystem.Deb;
10143
- case "jar":
10144
- case "maven": return Ecosystem.Maven;
10145
- case "python":
10146
- case "pypi": return Ecosystem.Pypi;
10147
- case "nodejs":
10148
- case "npm": return Ecosystem.Npm;
10149
- case "gem": return Ecosystem.Gem;
10150
- case "nuget": return Ecosystem.Nuget;
10151
- case "go": return Ecosystem.Go;
10152
- case "cargo": return Ecosystem.Cargo;
10153
- default: return Ecosystem.Generic;
10410
+ case "jar": return Ecosystem.Maven;
10411
+ case "python": return Ecosystem.Pypi;
10412
+ case "nodejs": return Ecosystem.Npm;
10413
+ default: return ecosystemFromPurlType(t);
10154
10414
  }
10155
10415
  }
10156
10416
  const FIX_VERSION_REGEX = /\d+(?:\.\d+)+[A-Za-z0-9._+\-]*/;
@@ -10165,20 +10425,18 @@ function extractFixedInVersion(vuln) {
10165
10425
  return m ? m[0] : "";
10166
10426
  }
10167
10427
  /**
10168
- * Builds an AffectedPackage entry from per-vulnerability fields. Returns
10169
- * undefined when packageName or packageVersion are missing (both required).
10428
+ * Builds an AffectedPackage entry from per-vulnerability fields. Twistlock
10429
+ * carries no purl or cpe, so the shared builder returns undefined unless the
10430
+ * name + version + ecosystem triple is complete.
10170
10431
  */
10171
10432
  function buildAffectedPackage(vuln, packageTypes, distro) {
10172
- if (!vuln.packageName || !vuln.packageVersion) return void 0;
10173
- const pkgType = vuln.packageType ?? packageTypes.get(vuln.packageName);
10174
- const pkg = {
10433
+ const pkgType = vuln.packageType ?? packageTypes.get(vuln.packageName ?? "");
10434
+ return buildAffectedPackage$1({
10175
10435
  name: vuln.packageName,
10176
10436
  version: vuln.packageVersion,
10177
- ecosystem: resolveEcosystem(pkgType, distro)
10178
- };
10179
- const fixed = extractFixedInVersion(vuln);
10180
- if (fixed) pkg.fixedInVersion = fixed;
10181
- return pkg;
10437
+ ecosystem: resolveEcosystem(pkgType, distro),
10438
+ fixedInVersion: extractFixedInVersion(vuln)
10439
+ });
10182
10440
  }
10183
10441
  /**
10184
10442
  * Indexes packageName → packageType from the result-level packages array.
@@ -10308,7 +10566,7 @@ function convertSingleResult(result, resultsChecksum) {
10308
10566
  */
10309
10567
  async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
10310
10568
  if (!input || input.trim().length === 0) throw new Error("twistlock: empty input");
10311
- validateInputSize(input, "twistlock");
10569
+ validateInputSize$1(input, "twistlock");
10312
10570
  const resultsChecksum = await inputChecksum(input);
10313
10571
  const parsed = parseJSON(input);
10314
10572
  if (!parsed || typeof parsed !== "object") throw new Error("twistlock: invalid JSON");
@@ -10515,7 +10773,7 @@ function buildAffectedPackageFromComponent(c) {
10515
10773
  let ecosystem;
10516
10774
  if (c.purl) ecosystem = ecosystemFromPurlType(parsePurl(c.purl)?.type);
10517
10775
  else if (c.name && c.version) ecosystem = ecosystemFromPurlType(void 0);
10518
- return buildAffectedPackage$2({
10776
+ return buildAffectedPackage$1({
10519
10777
  name: c.name,
10520
10778
  version: c.version,
10521
10779
  ecosystem,
@@ -10532,7 +10790,7 @@ function buildAffectedPackageFromComponent(c) {
10532
10790
  */
10533
10791
  async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
10534
10792
  if (!input || input.trim().length === 0) throw new Error("deptrack: empty input");
10535
- validateInputSize(input, "deptrack");
10793
+ validateInputSize$1(input, "deptrack");
10536
10794
  const resultsChecksum = await inputChecksum(input);
10537
10795
  const parsed = parseJSON(input);
10538
10796
  if (!parsed || typeof parsed !== "object") throw new Error("deptrack: invalid JSON");
@@ -10787,7 +11045,7 @@ function buildAffectedPackageFromEntry(entry) {
10787
11045
  ecosystem = ecosystemFromXraySource(parsed.scheme);
10788
11046
  }
10789
11047
  const fixed = entry.component_versions?.fixed_versions?.[0];
10790
- return buildAffectedPackage$2({
11048
+ return buildAffectedPackage$1({
10791
11049
  name,
10792
11050
  version,
10793
11051
  ecosystem: ecosystem ?? (name ? Ecosystem.Generic : void 0),
@@ -10802,7 +11060,7 @@ function buildAffectedPackageFromEntry(entry) {
10802
11060
  */
10803
11061
  async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
10804
11062
  if (!input || input.trim().length === 0) throw new Error("jfrog-xray: empty input");
10805
- validateInputSize(input, "jfrog-xray");
11063
+ validateInputSize$1(input, "jfrog-xray");
10806
11064
  const resultsChecksum = await inputChecksum(input);
10807
11065
  const parsed = parseJSON(input);
10808
11066
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.data)) throw new Error("jfrog-xray: invalid JSON structure");
@@ -11019,7 +11277,7 @@ function buildRequirement$6(vuln, scanTime, ml) {
11019
11277
  const refs = buildRefs$3(vuln);
11020
11278
  if (refs) req.refs = refs;
11021
11279
  const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
11022
- const pkg = buildAffectedPackage$2({
11280
+ const pkg = buildAffectedPackage$1({
11023
11281
  name: vuln.package_name,
11024
11282
  version: vuln.package_version,
11025
11283
  ecosystem: vuln.package_name && vuln.package_version ? Ecosystem.Generic : void 0,
@@ -11104,7 +11362,7 @@ function buildComponent(report) {
11104
11362
  */
11105
11363
  async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
11106
11364
  if (!input || input.trim().length === 0) throw new Error("neuvector: empty input");
11107
- validateInputSize(input, "neuvector");
11365
+ validateInputSize$1(input, "neuvector");
11108
11366
  const scan = parseJSON(input);
11109
11367
  if (!scan || typeof scan !== "object" || !("report" in scan)) throw new Error("neuvector: invalid JSON structure");
11110
11368
  const resultsChecksum = await inputChecksum(input);
@@ -11346,7 +11604,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
11346
11604
  */
11347
11605
  async function convertFortifyToHdf(input, converterVersion = "1.0.0") {
11348
11606
  if (!input || input.trim().length === 0) throw new Error("fortify: empty input");
11349
- validateInputSize(input, "fortify");
11607
+ validateInputSize$1(input, "fortify");
11350
11608
  const resultsChecksum = await inputChecksum(input);
11351
11609
  const parsed = parseXml(input, { stopNodes: ["FVDL.Description.Abstract", "FVDL.Description.Explanation"] });
11352
11610
  if (!parsed.FVDL) throw new Error("fortify: invalid FVDL — missing <FVDL> root element");
@@ -11592,7 +11850,7 @@ function buildAffectedPackageFromRecord(rec) {
11592
11850
  const version = rec["Package Version"];
11593
11851
  if (!name || !version) return void 0;
11594
11852
  const fixMatch = rec["Fix Status"] ? FIX_VERSION_PATTERN.exec(rec["Fix Status"]) : null;
11595
- return buildAffectedPackage$2({
11853
+ return buildAffectedPackage$1({
11596
11854
  name,
11597
11855
  version,
11598
11856
  ecosystem: ecosystemFromDistro(rec.Distro),
@@ -11629,7 +11887,7 @@ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
11629
11887
  */
11630
11888
  async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
11631
11889
  if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
11632
- validateInputSize(input, "prisma");
11890
+ validateInputSize$1(input, "prisma");
11633
11891
  const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
11634
11892
  for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
11635
11893
  const records = parseCsv(input);
@@ -11731,12 +11989,13 @@ function baseScoreFromBlock(block) {
11731
11989
  */
11732
11990
  function buildNetsparkerCvss(classification) {
11733
11991
  const out = [];
11734
- for (const block of [classification?.cvss, classification?.cvss31]) {
11992
+ const blocks = [[classification?.cvss, Version.The30], [classification?.cvss31, Version.The31]];
11993
+ for (const [block, elementVersion] of blocks) {
11735
11994
  const baseScore = baseScoreFromBlock(block);
11736
11995
  const vector = block?.vector ?? "";
11737
11996
  if (!vector && baseScore === void 0) continue;
11738
11997
  out.push(buildCvss$3({
11739
- version: cvssVersionFromVector(vector),
11998
+ version: cvssVersionFromVector(vector, elementVersion),
11740
11999
  baseScore,
11741
12000
  baseVector: vector
11742
12001
  }));
@@ -11857,7 +12116,7 @@ function buildRequirement$3(vuln, initiated) {
11857
12116
  */
11858
12117
  async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
11859
12118
  if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
11860
- validateInputSize(input, "netsparker");
12119
+ validateInputSize$1(input, "netsparker");
11861
12120
  const resultsChecksum = await inputChecksum(input);
11862
12121
  const parsed = parseXmlWithArrays(input, [
11863
12122
  "vulnerability",
@@ -12018,7 +12277,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
12018
12277
  */
12019
12278
  async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
12020
12279
  if (!input || input.trim().length === 0) throw new Error("scoutsuite: empty input");
12021
- validateInputSize(input, "scoutsuite");
12280
+ validateInputSize$1(input, "scoutsuite");
12022
12281
  const jsonStr = stripJSPrefix(input);
12023
12282
  const resultsChecksum = await inputChecksum(jsonStr);
12024
12283
  const report = parseJSON(jsonStr);
@@ -12230,7 +12489,7 @@ function buildScannerBaseline(scannerName, results, shaMap, resultsChecksum) {
12230
12489
  */
12231
12490
  async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
12232
12491
  if (!input || input.trim().length === 0) throw new Error("conveyor: empty input");
12233
- validateInputSize(input, "conveyor");
12492
+ validateInputSize$1(input, "conveyor");
12234
12493
  const data = parseJSON(input);
12235
12494
  if (!data || typeof data !== "object") throw new Error("conveyor: invalid JSON");
12236
12495
  if (!data.api_response) throw new Error("conveyor: missing api_response field");
@@ -12702,7 +12961,7 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
12702
12961
  */
12703
12962
  async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
12704
12963
  if (!input || input.trim().length === 0) throw new Error("veracode: empty input");
12705
- validateInputSize(input, "veracode");
12964
+ validateInputSize$1(input, "veracode");
12706
12965
  const parsed = parseXml(input, {
12707
12966
  attributeNamePrefix: "@_",
12708
12967
  trimValues: false
@@ -12858,7 +13117,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
12858
13117
  */
12859
13118
  async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
12860
13119
  if (!input || input.trim().length === 0) throw new Error("msft-secure-score: empty input");
12861
- validateInputSize(input, "msft-secure-score");
13120
+ validateInputSize$1(input, "msft-secure-score");
12862
13121
  const combined = parseJSON(input);
12863
13122
  if (!combined || typeof combined !== "object") throw new Error("msft-secure-score: invalid JSON");
12864
13123
  if (!combined.secureScore?.value) throw new Error("msft-secure-score: missing secureScore.value");
@@ -12905,7 +13164,7 @@ async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
12905
13164
  * the output with MSDO-specific metadata.
12906
13165
  */
12907
13166
  async function convertMsftDefenderDevopsToHdf(input, converterVersion = "1.0.0") {
12908
- validateInputSize(input, "msft-defender-devops");
13167
+ validateInputSize$1(input, "msft-defender-devops");
12909
13168
  const raw = parseJSON(input);
12910
13169
  if (!raw || !Array.isArray(raw.runs)) throw new Error("Invalid MSDO SARIF structure: missing or invalid runs field");
12911
13170
  const { components, runEnrichments } = extractEnrichments(raw);
@@ -13070,7 +13329,7 @@ function buildResultFromAssessment(a, scanTime) {
13070
13329
  * @returns HDF JSON string
13071
13330
  */
13072
13331
  async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0") {
13073
- validateInputSize(input, "msft-defender-cloud");
13332
+ validateInputSize$1(input, "msft-defender-cloud");
13074
13333
  const scanTime = /* @__PURE__ */ new Date();
13075
13334
  const resultsChecksum = await inputChecksum(input);
13076
13335
  const raw = parseJSON(input);
@@ -13180,9 +13439,7 @@ function buildTriageOverride(alert, startTime) {
13180
13439
  default: return null;
13181
13440
  }
13182
13441
  const appliedAt = (alert.resolvedDateTime ? parseTimestamp(alert.resolvedDateTime) : null) ?? (alert.lastUpdateDateTime ? parseTimestamp(alert.lastUpdateDateTime) : null) ?? startTime;
13183
- const expiresAt = /* @__PURE__ */ new Date();
13184
- expiresAt.setTime(appliedAt.getTime());
13185
- expiresAt.setUTCFullYear(expiresAt.getUTCFullYear() + 1);
13442
+ const expiresAt = defaultOverrideExpiry(appliedAt);
13186
13443
  return {
13187
13444
  override: {
13188
13445
  type: disposition,
@@ -13356,7 +13613,7 @@ function alertToRequirement(alert, scanTime) {
13356
13613
  * @returns HDF JSON string
13357
13614
  */
13358
13615
  async function convertMsftDefenderEndpointToHdf(input, converterVersion = "1.0.0") {
13359
- validateInputSize(input, "msft-defender-endpoint");
13616
+ validateInputSize$1(input, "msft-defender-endpoint");
13360
13617
  const resultsChecksum = await inputChecksum(input);
13361
13618
  const response = parseJSON(input);
13362
13619
  if (!response || typeof response !== "object") throw new Error("Invalid Microsoft Defender for Endpoint structure: not a valid JSON object");
@@ -13408,7 +13665,7 @@ const BUILD_OPTIONS = {
13408
13665
  * @returns XCCDF 1.2 XML string
13409
13666
  */
13410
13667
  function convertHdfToXccdf(input) {
13411
- validateInputSize(input, "hdf-to-xccdf");
13668
+ validateInputSize$1(input, "hdf-to-xccdf");
13412
13669
  const hdfData = parseHdf(input);
13413
13670
  if (!hdfData || typeof hdfData !== "object" || !("baselines" in hdfData)) throw new Error("Invalid HDF structure: missing baselines field");
13414
13671
  if (!Array.isArray(hdfData.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
@@ -13418,12 +13675,14 @@ function convertHdfToXccdf(input) {
13418
13675
  function wrap(value) {
13419
13676
  return { "#text": value };
13420
13677
  }
13421
- /** Map HDF impact (0.0-1.0) to XCCDF severity string. */
13678
+ /** Map HDF impact (0.0-1.0) to XCCDF severity via the shared band mapper. The
13679
+ * XCCDF vocabulary has no critical band, so critical folds into high, and its
13680
+ * zero band is spelled 'info'. */
13422
13681
  function impactToSeverity$2(impact) {
13423
- if (impact >= .7) return "high";
13424
- if (impact >= .4) return "medium";
13425
- if (impact >= .1) return "low";
13426
- return "info";
13682
+ const sev = impactToSeverity(impact);
13683
+ if (sev === "critical") return "high";
13684
+ if (sev === "informational") return "info";
13685
+ return sev;
13427
13686
  }
13428
13687
  /** Map HDF result status to XCCDF result value. */
13429
13688
  function hdfStatusToXccdf(status) {
@@ -13441,7 +13700,44 @@ function findDescription(descriptions, label) {
13441
13700
  }
13442
13701
  /** Replace characters not valid in XCCDF IDs with underscores. */
13443
13702
  function sanitizeXccdfId(id) {
13444
- return id.replace(/[^a-zA-Z0-9_.\-]/g, "_");
13703
+ return id.replace(/[^a-zA-Z0-9_.\-]/gu, "_");
13704
+ }
13705
+ /**
13706
+ * Supply the trailing name segment that benchmarkIdType and profileIdType
13707
+ * require via their .+ — a baseline with an empty name is valid HDF but
13708
+ * produced "xccdf_hdf_benchmark_", which the XSD rejects.
13709
+ */
13710
+ function xccdfNamePart(name) {
13711
+ return name === "" ? "unnamed" : name;
13712
+ }
13713
+ /**
13714
+ * Render an HDF tags.gid as a groupIdType: an NCName that must also match
13715
+ * xccdf_[^_]+_group_.+ (xccdf_1.2.xsd:821). HDF constrains gid not at all, and
13716
+ * most real data does not satisfy it — 783 of the 1216 distinct gid values in
13717
+ * this package's converter fixtures are bare STIG ids like "V-257777" — so copying one
13718
+ * through produced XSD-invalid output from valid HDF.
13719
+ *
13720
+ * A gid that already conforms is a genuine XCCDF group id (STIG content carries
13721
+ * "xccdf_mil.disa.stig_group_V-204393") and is passed through; anything else is
13722
+ * sanitized and namespaced under this converter, matching what the benchmark,
13723
+ * profile, and rule ids already do.
13724
+ *
13725
+ * Mirrored by xccdfGroupID in the Go converter. Exported so the shared table
13726
+ * can be asserted against the encoder directly, including the empty gid the
13727
+ * converter itself skips.
13728
+ */
13729
+ function xccdfGroupId(gid) {
13730
+ if (isXccdfGroupId(gid)) return gid;
13731
+ return sanitizeXccdfId("xccdf_hdf_group_" + (gid === "" ? "unnamed" : gid));
13732
+ }
13733
+ /**
13734
+ * Whether a value already satisfies groupIdType. The character test is
13735
+ * ASCII-only, deliberately narrower than NCName: a conforming non-ASCII id is
13736
+ * re-encoded rather than trusted, which errs toward valid output and keeps the
13737
+ * check identical to the Go peer's.
13738
+ */
13739
+ function isXccdfGroupId(id) {
13740
+ return /^xccdf_[^_]+_group_.+$/.test(id) && /^[A-Za-z0-9._-]+$/.test(id);
13445
13741
  }
13446
13742
  /** Read a string-valued tag, or undefined if absent/non-string. */
13447
13743
  function tagString(tags, key) {
@@ -13466,12 +13762,22 @@ function toolTestSystem(tool) {
13466
13762
  const name = tool.name ? cpeField(tool.name) : "tool";
13467
13763
  return `cpe:/a:${name}:${name}:${cpeField(tool.version)}`;
13468
13764
  }
13765
+ /**
13766
+ * The requirement id as text for id construction. HDF makes id required, but
13767
+ * nested-invalid input reaches the converter and concatenating an absent id
13768
+ * wrote the literal string "undefined" into the emitted identifier — where Go,
13769
+ * concatenating a zero-value string, wrote nothing. Both satisfied the XCCDF
13770
+ * pattern, so no schema gate caught the divergence.
13771
+ */
13772
+ function requirementIdText(id) {
13773
+ return typeof id === "string" ? id : "";
13774
+ }
13469
13775
  /** Build the Benchmark XML object from HDF data. */
13470
13776
  function buildBenchmarkObj(hdfData) {
13471
13777
  const empty = !hdfData.baselines || hdfData.baselines.length === 0;
13472
13778
  const benchmark = {
13473
13779
  [`${ATTR}xmlns`]: "http://checklists.nist.gov/xccdf/1.2",
13474
- [`${ATTR}id`]: empty ? "xccdf_hdf_benchmark_exported" : sanitizeXccdfId("xccdf_hdf_benchmark_" + hdfData.baselines[0].name),
13780
+ [`${ATTR}id`]: empty ? "xccdf_hdf_benchmark_exported" : sanitizeXccdfId("xccdf_hdf_benchmark_" + xccdfNamePart(hdfData.baselines[0].name)),
13475
13781
  [`${ATTR}resolved`]: "1",
13476
13782
  status: wrap("incomplete")
13477
13783
  };
@@ -13485,7 +13791,7 @@ function buildBenchmarkObj(hdfData) {
13485
13791
  if (baseline.summary) benchmark.description = wrap(baseline.summary);
13486
13792
  benchmark.version = wrap(baseline.version ?? "1.0");
13487
13793
  benchmark.Profile = {
13488
- [`${ATTR}id`]: sanitizeXccdfId("xccdf_hdf_profile_" + baseline.name),
13794
+ [`${ATTR}id`]: sanitizeXccdfId("xccdf_hdf_profile_" + xccdfNamePart(baseline.name)),
13489
13795
  title: wrap(baseline.title ?? baseline.name)
13490
13796
  };
13491
13797
  const groups = [];
@@ -13502,7 +13808,7 @@ function buildBenchmarkObj(hdfData) {
13502
13808
  if (idx === void 0) {
13503
13809
  idx = groups.length;
13504
13810
  groupIndex.set(gid, idx);
13505
- const group = { [`${ATTR}id`]: gid };
13811
+ const group = { [`${ATTR}id`]: xccdfGroupId(gid) };
13506
13812
  const gtitle = tagString(req.tags, "gtitle");
13507
13813
  if (gtitle) group.title = wrap(gtitle);
13508
13814
  group.Rule = [];
@@ -13512,12 +13818,13 @@ function buildBenchmarkObj(hdfData) {
13512
13818
  }
13513
13819
  if (groups.length > 0) benchmark.Group = groups;
13514
13820
  if (flatRules.length > 0) benchmark.Rule = flatRules;
13515
- benchmark.TestResult = buildTestResultObj(hdfData, baseline);
13821
+ const testResult = buildTestResultObj(hdfData, baseline);
13822
+ if (testResult) benchmark.TestResult = testResult;
13516
13823
  return benchmark;
13517
13824
  }
13518
13825
  /** Build an XCCDF Rule object from an HDF EvaluatedRequirement. */
13519
13826
  function buildRuleObj(req) {
13520
- const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
13827
+ const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + requirementIdText(req.id) + "_rule");
13521
13828
  const rule = {
13522
13829
  [`${ATTR}id`]: ruleId,
13523
13830
  [`${ATTR}severity`]: impactToSeverity$2(req.impact),
@@ -13566,27 +13873,74 @@ function buildRuleObj(req) {
13566
13873
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
13567
13874
  "check-content": wrap(checkContent)
13568
13875
  });
13569
- if (req.code) checks.push({
13876
+ const code = firstNonEmpty(req.code ?? "");
13877
+ if (code !== "") checks.push({
13570
13878
  [`${ATTR}system`]: "http://inspec.io/",
13571
- "check-content": wrap(req.code)
13879
+ "check-content": wrap(code)
13572
13880
  });
13573
13881
  if (checks.length > 0) rule.check = checks;
13574
13882
  return rule;
13575
13883
  }
13576
- /** Build the XCCDF TestResult object. */
13884
+ /**
13885
+ * The earliest and latest result time in a baseline.
13886
+ *
13887
+ * XCCDF makes TestResult/@end-time required while HDF's top-level timestamp is
13888
+ * optional, so a document without one still has to carry a scan window. It is
13889
+ * derived from the results rather than the wall clock, which would break both
13890
+ * determinism and the golden comparison. A TestResult is only built when a
13891
+ * baseline exists, and the HDF schema puts minItems 1 on both requirements and
13892
+ * results with startTime required on each, so a window is always available
13893
+ * here; undefined is returned anyway rather than assuming it.
13894
+ *
13895
+ * Times are emitted through formatTimestamp, HDF's canonical form, which is
13896
+ * documented as byte-identical to the RFC3339Nano string the Go converter
13897
+ * emits for the same instant.
13898
+ *
13899
+ * Mirrored by resultTimeWindow in the Go converter.
13900
+ */
13901
+ function resultTimeWindow(baseline) {
13902
+ let first;
13903
+ let last;
13904
+ for (const req of baseline.requirements) for (const res of req.results) {
13905
+ const parsed = parseTimestamp(typeof res.startTime === "string" ? res.startTime : res.startTime.toISOString());
13906
+ if (!parsed) continue;
13907
+ const at = parsed.getTime();
13908
+ const canonical = formatTimestamp(parsed);
13909
+ if (!first || at < first.at) first = {
13910
+ at,
13911
+ text: canonical
13912
+ };
13913
+ if (!last || at > last.at) last = {
13914
+ at,
13915
+ text: canonical
13916
+ };
13917
+ }
13918
+ return first && last ? {
13919
+ first: first.text,
13920
+ last: last.text
13921
+ } : void 0;
13922
+ }
13923
+ /** Build the XCCDF TestResult object, or undefined when it cannot carry a time. */
13577
13924
  function buildTestResultObj(hdfData, baseline) {
13578
13925
  const testResult = {
13579
13926
  [`${ATTR}id`]: "xccdf_hdf_testresult_1",
13580
13927
  title: wrap("HDF Assessment Results")
13581
13928
  };
13582
13929
  if (hdfData.timestamp) {
13583
- const ts = typeof hdfData.timestamp === "string" ? hdfData.timestamp : hdfData.timestamp.toISOString();
13930
+ const raw = typeof hdfData.timestamp === "string" ? hdfData.timestamp : hdfData.timestamp.toISOString();
13931
+ const parsedTs = parseTimestamp(raw);
13932
+ const ts = parsedTs ? formatTimestamp(parsedTs) : raw;
13584
13933
  testResult[`${ATTR}start-time`] = ts;
13585
13934
  let endTime = ts;
13586
13935
  const duration = hdfData.statistics?.duration;
13587
13936
  const start = parseTimestamp(ts);
13588
13937
  if (start && typeof duration === "number" && duration > 0) endTime = formatTimestamp(new Date(start.getTime() + duration * 1e3));
13589
13938
  testResult[`${ATTR}end-time`] = endTime;
13939
+ } else {
13940
+ const window = resultTimeWindow(baseline);
13941
+ if (!window) return;
13942
+ testResult[`${ATTR}start-time`] = window.first;
13943
+ testResult[`${ATTR}end-time`] = window.last;
13590
13944
  }
13591
13945
  const testSystem = toolTestSystem(hdfData.tool);
13592
13946
  if (testSystem) testResult[`${ATTR}test-system`] = testSystem;
@@ -13599,7 +13953,7 @@ function buildTestResultObj(hdfData, baseline) {
13599
13953
  let passed = 0;
13600
13954
  let scorable = 0;
13601
13955
  for (const req of baseline.requirements) {
13602
- const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
13956
+ const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + requirementIdText(req.id) + "_rule");
13603
13957
  const stigId = tagString(req.tags, "stig_id");
13604
13958
  const statusInput = requirementStatusInput(req);
13605
13959
  const resolved = computeEffectiveStatus(statusInput);
@@ -13611,7 +13965,9 @@ function buildTestResultObj(hdfData, baseline) {
13611
13965
  scorable++;
13612
13966
  } else if (status === "fail") scorable++;
13613
13967
  const rr = { [`${ATTR}idref`]: ruleIdRef };
13614
- rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
13968
+ const rawStart = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
13969
+ const parsedStart = parseTimestamp(rawStart);
13970
+ rr[`${ATTR}time`] = parsedStart ? formatTimestamp(parsedStart) : rawStart;
13615
13971
  if (stigId) rr[`${ATTR}version`] = stigId;
13616
13972
  rr.result = wrap(status);
13617
13973
  if (result.message) rr.message = wrap(result.message);
@@ -13633,6 +13989,11 @@ function buildTestResultObj(hdfData, baseline) {
13633
13989
  }
13634
13990
  //#endregion
13635
13991
  //#region converters/oscal-to-hdf/typescript/shared.ts
13992
+ /**
13993
+ * Shared helper functions for OSCAL-to-HDF converters.
13994
+ *
13995
+ * Mirrors the Go helpers in converters/oscal-to-hdf/go/shared.go.
13996
+ */
13636
13997
  const controlEnhancementRe = /^([a-z]{2}-\d+)\.(\d+)$/;
13637
13998
  const objectiveIDRe = /^([a-z]{2}-\d+(?:\.\d+)?)/;
13638
13999
  /**
@@ -13717,6 +14078,12 @@ function flattenPartsByName(parts, name) {
13717
14078
  }
13718
14079
  return pieces.join("\n").trim();
13719
14080
  }
14081
+ /** The one OSCAL spelling the standard severity map does not know; every other
14082
+ * facet value is standard vocabulary. */
14083
+ const RISK_FACET_SEVERITY_ALIASES = { moderate: .5 };
14084
+ /** Sentinel for "this facet's value is not a severity" — impact is 0.0-1.0, so
14085
+ * a negative can never be a real reading. */
14086
+ const UNRECOGNIZED_FACET_IMPACT = -1;
13720
14087
  /**
13721
14088
  * Extracts impact/severity from risk characterization facets.
13722
14089
  * Returns a normalized 0.0-1.0 impact value. Falls back to defaultImpact.
@@ -13725,15 +14092,10 @@ function extractRiskSeverity(characterizations, defaultImpact) {
13725
14092
  if (!characterizations) return defaultImpact;
13726
14093
  for (const c of characterizations) {
13727
14094
  if (!c.facets) continue;
13728
- for (const f of c.facets) if (f.name === "impact" || f.name === "risk" || f.name === "likelihood") switch (f.value.toLowerCase()) {
13729
- case "critical": return .9;
13730
- case "high": return .7;
13731
- case "moderate":
13732
- case "medium": return .5;
13733
- case "low": return .3;
13734
- case "info":
13735
- case "informational":
13736
- case "none": return 0;
14095
+ for (const f of c.facets) {
14096
+ if (f.name !== "impact" && f.name !== "risk" && f.name !== "likelihood") continue;
14097
+ const impact = severityToImpactWithAliases(f.value, RISK_FACET_SEVERITY_ALIASES, UNRECOGNIZED_FACET_IMPACT);
14098
+ if (impact >= 0) return impact;
13737
14099
  }
13738
14100
  }
13739
14101
  return defaultImpact;
@@ -13764,11 +14126,7 @@ function nistTagToControlId(tag) {
13764
14126
  * This is the reverse of extractRiskSeverity.
13765
14127
  */
13766
14128
  function impactToSeverity$1(impact) {
13767
- if (impact >= .9) return "critical";
13768
- if (impact >= .7) return "high";
13769
- if (impact >= .4) return "moderate";
13770
- if (impact >= .1) return "low";
13771
- return "info";
14129
+ return oscalSeverityFromHdf(impactToSeverity(impact));
13772
14130
  }
13773
14131
  /**
13774
14132
  * Maps an HDF status string to an OSCAL risk status string.
@@ -13866,15 +14224,9 @@ function oscalString(s) {
13866
14224
  * @returns OSCAL SAR JSON string
13867
14225
  */
13868
14226
  async function convertHdfToOscalSar(input) {
13869
- validateInputSize(input, "hdf-to-oscal-sar");
13870
- if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-sar: empty input");
13871
- let hdfResults;
13872
- try {
13873
- hdfResults = parseHdf(input);
13874
- } catch {
13875
- throw new Error("hdf-to-oscal-sar: failed to parse HDF JSON");
13876
- }
13877
- if (!hdfResults || typeof hdfResults !== "object" || !("baselines" in hdfResults)) throw new Error("hdf-to-oscal-sar: invalid HDF structure: missing baselines field");
14227
+ const { doc: hdfResultsDoc } = requireHdfResults(input, "hdf-to-oscal-sar");
14228
+ const hdfResults = hdfResultsDoc;
14229
+ if (hdfResults.baselines.length === 0) throw new Error("hdf-to-oscal-sar: cannot represent an assessment with no evaluated baselines as OSCAL Assessment Results, which requires at least one result");
13878
14230
  const doc = buildOSCALDocument(hdfResults);
13879
14231
  return JSON.stringify(doc, null, 2);
13880
14232
  }
@@ -13976,7 +14328,7 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
13976
14328
  const includeControls = [];
13977
14329
  const seenControl = /* @__PURE__ */ new Set();
13978
14330
  for (const req of baseline.requirements) {
13979
- const nistId = nistTagToControlId(req.id);
14331
+ const nistId = nistTagToControlId(req.id ?? "");
13980
14332
  if (nistId === "") continue;
13981
14333
  const { finding, observation, risk, resource } = requirementToFindingSet(req, timestamp, toolActorUuid, subjects);
13982
14334
  findings.push(finding);
@@ -13998,9 +14350,9 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
13998
14350
  start: assessmentStart(baseline, timestamp),
13999
14351
  ...resultProps.length > 0 ? { props: resultProps } : {},
14000
14352
  "reviewed-controls": { "control-selections": [controlSelection] },
14001
- findings,
14002
- observations,
14003
- risks
14353
+ ...findings.length > 0 ? { findings } : {},
14354
+ ...observations.length > 0 ? { observations } : {},
14355
+ ...risks.length > 0 ? { risks } : {}
14004
14356
  },
14005
14357
  resources
14006
14358
  };
@@ -14010,10 +14362,17 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
14010
14362
  * component's UUID (componentId when present, otherwise a fresh one) identifies
14011
14363
  * the subject; the HDF component type is a valid OSCAL subject type token and
14012
14364
  * its name becomes the subject title.
14365
+ *
14366
+ * A component with no type is skipped rather than given one. OSCAL requires both
14367
+ * subject-uuid and type on a subject-reference, so the type cannot simply be
14368
+ * omitted, and hdf-results defines no default component type to fall back on.
14369
+ * String(c.type) turned an absent type into the literal "undefined" — a valid
14370
+ * OSCAL token, so a schema check could not see it, asserting a component type
14371
+ * the source never stated. Mirrors the Go peer.
14013
14372
  */
14014
14373
  function buildSubjects(components) {
14015
14374
  if (!Array.isArray(components) || components.length === 0) return [];
14016
- return components.map((c) => ({
14375
+ return components.filter((c) => oscalString(c.type ?? "") !== "").map((c) => ({
14017
14376
  "subject-uuid": c.componentId && c.componentId !== "" ? c.componentId : crypto.randomUUID(),
14018
14377
  type: String(c.type),
14019
14378
  title: c.name
@@ -14023,7 +14382,7 @@ function buildSubjects(components) {
14023
14382
  * Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
14024
14383
  */
14025
14384
  function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14026
- const controlID = oscalToken(nistTagToControlId(req.id));
14385
+ const controlID = oscalToken(nistTagToControlId(req.id ?? ""));
14027
14386
  const results = req.results ?? [];
14028
14387
  const descriptions = req.descriptions ?? [];
14029
14388
  const { state, reason } = effectiveState(req);
@@ -14105,7 +14464,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14105
14464
  uuid: resourceUuid,
14106
14465
  title: `Check source code for ${req.id}`,
14107
14466
  base64: {
14108
- value: Buffer.from(req.code, "utf-8").toString("base64"),
14467
+ value: encodeBase64Utf8(req.code),
14109
14468
  "media-type": "text/plain"
14110
14469
  }
14111
14470
  };
@@ -14287,14 +14646,7 @@ function sourceLocationText(loc) {
14287
14646
  }
14288
14647
  /** Maps an explicit HDF severity to the OSCAL risk facet value vocabulary. */
14289
14648
  function severityToFacetValue(s) {
14290
- switch (s) {
14291
- case "critical": return "critical";
14292
- case "high": return "high";
14293
- case "medium": return "moderate";
14294
- case "low": return "low";
14295
- case "informational": return "info";
14296
- default: return "";
14297
- }
14649
+ return oscalSeverityFromHdf(s);
14298
14650
  }
14299
14651
  /**
14300
14652
  * Turns the requirement's fix description and any governing risk-acceptance
@@ -14380,17 +14732,27 @@ function riskStatusFromState(state) {
14380
14732
  * @returns OSCAL POA&M JSON string
14381
14733
  */
14382
14734
  async function convertHdfToOscalPoam(input) {
14383
- validateInputSize(input, "hdf-to-oscal-poam");
14384
- if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-poam: empty input");
14385
- let amendments;
14386
- try {
14387
- amendments = parseHdf(input);
14388
- } catch {
14389
- throw new Error("hdf-to-oscal-poam: failed to parse JSON");
14390
- }
14391
- const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendments) };
14735
+ const { doc: amendmentsDoc } = requireHdfAmendments(input, "hdf-to-oscal-poam");
14736
+ const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendmentsDoc) };
14392
14737
  return JSON.stringify(doc, null, 2);
14393
14738
  }
14739
+ /**
14740
+ * Picks the document title, which OSCAL requires on metadata. The HDF name is
14741
+ * schema-required, so the fallbacks only matter for a document that slipped
14742
+ * through some other producer's validation.
14743
+ */
14744
+ function poamTitle(a) {
14745
+ return firstNonEmpty(a.name, a.amendmentId ?? "", "HDF Amendments");
14746
+ }
14747
+ /**
14748
+ * Supplies the text OSCAL requires for a risk's description and statement. HDF
14749
+ * puts no minLength on reason, so an override can legitimately carry none; the
14750
+ * fallback states that absence rather than inventing an impact assessment the
14751
+ * source never made.
14752
+ */
14753
+ function riskRationale(override) {
14754
+ return firstNonEmpty(override.reason, `No rationale was recorded for the ${String(override.type)} override applied to ${override.requirementId}.`);
14755
+ }
14394
14756
  /** HDF dates arrive as strings from JSON.parse but are typed as Date. */
14395
14757
  function toDate(value) {
14396
14758
  if (value === void 0) return void 0;
@@ -14408,14 +14770,15 @@ var PartyRegistry = class {
14408
14770
  this.byId = /* @__PURE__ */ new Map();
14409
14771
  }
14410
14772
  getOrAdd(id) {
14411
- const existing = this.byId.get(id.identifier);
14773
+ const name = oscalString(id.identifier);
14774
+ const existing = this.byId.get(name);
14412
14775
  if (existing) return existing.uuid;
14413
14776
  const party = {
14414
14777
  uuid: crypto.randomUUID(),
14415
14778
  type: "person",
14416
- name: id.identifier
14779
+ ...name === "" ? {} : { name }
14417
14780
  };
14418
- this.byId.set(id.identifier, party);
14781
+ this.byId.set(name, party);
14419
14782
  return party.uuid;
14420
14783
  }
14421
14784
  list() {
@@ -14465,7 +14828,7 @@ function amendmentsToPOAM(amendments) {
14465
14828
  observations.push(...itemObs);
14466
14829
  }
14467
14830
  const metadata = {
14468
- title: amendments.name,
14831
+ title: poamTitle(amendments),
14469
14832
  "last-modified": latestAppliedAt(amendments.overrides),
14470
14833
  version: amendmentsVersion(amendments),
14471
14834
  "oscal-version": OSCAL_VERSION
@@ -14483,9 +14846,9 @@ function amendmentsToPOAM(amendments) {
14483
14846
  uuid: crypto.randomUUID(),
14484
14847
  metadata,
14485
14848
  "import-ssp": importSSP,
14486
- risks,
14487
14849
  "poam-items": poamItems
14488
14850
  };
14851
+ if (risks.length > 0) poam.risks = risks;
14489
14852
  if (observations.length > 0) poam.observations = observations;
14490
14853
  const resources = externalRefResources(amendments.overrides);
14491
14854
  if (resources.length > 0) poam["back-matter"] = { resources };
@@ -14498,10 +14861,9 @@ function amendmentsToPOAM(amendments) {
14498
14861
  function overrideToPOAMItem(override, parties) {
14499
14862
  const riskUUID = crypto.randomUUID();
14500
14863
  const riskStatus = override.status ? hdfStatusToOscalRiskStatus(String(override.status)) : "open";
14501
- const riskProps = [{
14502
- name: "impacted-control-id",
14503
- value: nistTagToControlId(override.requirementId)
14504
- }];
14864
+ const controlID = nistTagToControlId(override.requirementId);
14865
+ const riskProps = [];
14866
+ pushStringProp(riskProps, "impacted-control-id", controlID);
14505
14867
  if (override.type) riskProps.push({
14506
14868
  name: "override-type",
14507
14869
  value: String(override.type)
@@ -14510,18 +14872,9 @@ function overrideToPOAMItem(override, parties) {
14510
14872
  name: "impact-override",
14511
14873
  value: String(override.impact.value)
14512
14874
  });
14513
- if (override.justification) riskProps.push({
14514
- name: "justification",
14515
- value: String(override.justification)
14516
- });
14517
- if (override.baselineRef) riskProps.push({
14518
- name: "baseline-ref",
14519
- value: override.baselineRef
14520
- });
14521
- if (override.componentRef) riskProps.push({
14522
- name: "component-ref",
14523
- value: override.componentRef
14524
- });
14875
+ if (override.justification) pushStringProp(riskProps, "justification", String(override.justification));
14876
+ if (override.baselineRef) pushStringProp(riskProps, "baseline-ref", override.baselineRef);
14877
+ if (override.componentRef) pushStringProp(riskProps, "component-ref", override.componentRef);
14525
14878
  const remediations = [];
14526
14879
  if (override.milestones) for (const ms of override.milestones) {
14527
14880
  const msProps = [];
@@ -14583,11 +14936,12 @@ function overrideToPOAMItem(override, parties) {
14583
14936
  itemObs.push(evidenceObservation(ev, obsUUID, collected));
14584
14937
  relatedObs.push({ "observation-uuid": obsUUID });
14585
14938
  }
14939
+ const rationale = riskRationale(override);
14586
14940
  const risk = {
14587
14941
  uuid: riskUUID,
14588
14942
  title: override.requirementId,
14589
- description: override.reason,
14590
- statement: override.reason,
14943
+ description: rationale,
14944
+ statement: rationale,
14591
14945
  status: riskStatus,
14592
14946
  deadline,
14593
14947
  props: riskProps,
@@ -14622,9 +14976,22 @@ function latestAppliedAt(overrides) {
14622
14976
  }
14623
14977
  return formatTimestampSeconds(latest ?? /* @__PURE__ */ new Date());
14624
14978
  }
14979
+ /**
14980
+ * Add a property whose value OSCAL types as StringDatatype, trimming it and
14981
+ * omitting the property entirely when nothing survives. A prop with an empty
14982
+ * value carries no more than an absent one and is schema-invalid. Mirrors
14983
+ * appendStringProp in the Go peer.
14984
+ */
14985
+ function pushStringProp(props, name, value) {
14986
+ const trimmed = oscalString(value ?? "");
14987
+ if (trimmed !== "") props.push({
14988
+ name,
14989
+ value: trimmed
14990
+ });
14991
+ }
14625
14992
  /** Sources metadata.version from the amendments document, defaulting when omitted. */
14626
14993
  function amendmentsVersion(a) {
14627
- return a.version && a.version !== "" ? a.version : "1.0.0";
14994
+ return oscalString(a.version ?? "") || "1.0.0";
14628
14995
  }
14629
14996
  /**
14630
14997
  * Carries document identifiers and labels that have no first-class OSCAL home.
@@ -14632,17 +14999,33 @@ function amendmentsVersion(a) {
14632
14999
  */
14633
15000
  function metadataProps(a) {
14634
15001
  const props = [];
14635
- if (a.amendmentId) props.push({
14636
- name: "amendment-id",
14637
- value: a.amendmentId
14638
- });
14639
- if (a.labels) for (const k of Object.keys(a.labels).sort()) props.push({
14640
- name: k,
14641
- value: a.labels[k],
14642
- class: "amendment-label"
14643
- });
15002
+ if (a.amendmentId) pushStringProp(props, "amendment-id", a.amendmentId);
15003
+ if (a.labels) for (const [key, value] of Object.entries(a.labels).sort(([x], [y]) => x < y ? -1 : x > y ? 1 : 0)) {
15004
+ const prop = labelProp(key, value);
15005
+ if (prop.value !== "") props.push(prop);
15006
+ }
14644
15007
  return props;
14645
15008
  }
15009
+ /**
15010
+ * Render one amendments label as a property. OSCAL types prop/@name as
15011
+ * TokenDatatype while HDF puts no constraint on label keys, so the key is
15012
+ * encoded and the source key kept in remarks when it had to change. Every label
15013
+ * key in this package's converter fixtures is token-shaped today, so this guards a shape real
15014
+ * data has not yet produced — but Kubernetes and OCI label keys are namespaced
15015
+ * with '/', which HDF permits and OSCAL rejects.
15016
+ *
15017
+ * Mirrored by labelProp in the Go converter.
15018
+ */
15019
+ function labelProp(key, value) {
15020
+ const name = oscalToken(key) === "" ? "_" : oscalToken(key);
15021
+ const prop = {
15022
+ name,
15023
+ value: oscalString(value),
15024
+ class: "amendment-label"
15025
+ };
15026
+ if (key !== "" && name !== key) prop.remarks = key;
15027
+ return prop;
15028
+ }
14646
15029
  /** Picks the collection timestamp for evidence observations. */
14647
15030
  function observationCollected(override) {
14648
15031
  return formatTimestampSeconds(toDate(override.appliedAt) ?? /* @__PURE__ */ new Date());
@@ -14656,14 +15039,8 @@ function evidenceObservation(ev, uuid, defaultCollected) {
14656
15039
  if (ev.type === "url") re.href = ev.data;
14657
15040
  else if (ev.data) re.remarks = ev.data;
14658
15041
  const props = [];
14659
- if (ev.mimeType) props.push({
14660
- name: "mime-type",
14661
- value: ev.mimeType
14662
- });
14663
- if (ev.capturedBy && ev.capturedBy.identifier) props.push({
14664
- name: "captured-by",
14665
- value: ev.capturedBy.identifier
14666
- });
15042
+ if (ev.mimeType) pushStringProp(props, "mime-type", ev.mimeType);
15043
+ if (ev.capturedBy && ev.capturedBy.identifier) pushStringProp(props, "captured-by", ev.capturedBy.identifier);
14667
15044
  return {
14668
15045
  uuid,
14669
15046
  description: desc,
@@ -14886,7 +15263,7 @@ function verdictLabels(a) {
14886
15263
  }
14887
15264
  async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
14888
15265
  if (!input?.trim()) throw new Error("Empty input");
14889
- validateInputSize(input, "ionchannel");
15266
+ validateInputSize$1(input, "ionchannel");
14890
15267
  const analysis = parseJSON(input);
14891
15268
  if (!Array.isArray(analysis.scan_summaries)) throw new Error(`Ion Channel scan_summaries invalid summary data (expecting array, got ${typeof analysis.scan_summaries})`);
14892
15269
  let allDeps = [];
@@ -15248,12 +15625,8 @@ function supplierEvidence(sourceURI, description) {
15248
15625
  *
15249
15626
  * Spec: https://github.com/openvex/spec
15250
15627
  */
15251
- /** One year in milliseconds. VEX statements are re-evaluated as new info
15252
- * arrives; one year is a defensive default consistent with the
15253
- * no-permanent-amendment rule on Standalone_Override. */
15254
- const DEFAULT_EXPIRY_HORIZON_MS$3 = 365 * 24 * 60 * 60 * 1e3;
15255
15628
  async function convertOpenVexToHdf(input, converterVersion = "1.0.0") {
15256
- validateInputSize(input, "openvex-to-hdf");
15629
+ validateInputSize$1(input, "openvex-to-hdf");
15257
15630
  const doc = parseJSON(input);
15258
15631
  const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
15259
15632
  const overrides = [];
@@ -15262,8 +15635,10 @@ async function convertOpenVexToHdf(input, converterVersion = "1.0.0") {
15262
15635
  if (override) overrides.push(override);
15263
15636
  }
15264
15637
  if (overrides.length === 0) throw new Error("openvex-to-hdf: VEX document contains no actionable statements (all 'affected' or 'under_investigation'); no amendment to write");
15638
+ const name = doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements";
15639
+ await chainOverrides(overrides);
15265
15640
  return {
15266
- name: doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements",
15641
+ name,
15267
15642
  description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
15268
15643
  overrides,
15269
15644
  appliedBy: identityFor$2(doc.author, doc.role),
@@ -15283,7 +15658,7 @@ function statementToOverride(stmt, doc, docTime) {
15283
15658
  if (!requirementId) return void 0;
15284
15659
  const stmtTime = (stmt.timestamp ? parseTimestamp(stmt.timestamp) : null) ?? docTime;
15285
15660
  const author = stmt.author ?? doc.author ?? "";
15286
- const expiresAt = new Date(stmtTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$3);
15661
+ const expiresAt = defaultOverrideExpiry(stmtTime);
15287
15662
  const override = {
15288
15663
  type: target.overrideType,
15289
15664
  requirementId,
@@ -15343,9 +15718,8 @@ function truncateId(id) {
15343
15718
  *
15344
15719
  * Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
15345
15720
  */
15346
- const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
15347
15721
  async function convertCsafVexToHdf(input, converterVersion = "1.0.0") {
15348
- validateInputSize(input, "csaf-vex-to-hdf");
15722
+ validateInputSize$1(input, "csaf-vex-to-hdf");
15349
15723
  const doc = parseJSON(input);
15350
15724
  if (doc.document?.category !== "csaf_vex") throw new Error(`csaf-vex-to-hdf: document.category is ${JSON.stringify(doc.document?.category)}; only 'csaf_vex' is supported`);
15351
15725
  const dm = doc.document;
@@ -15357,6 +15731,7 @@ async function convertCsafVexToHdf(input, converterVersion = "1.0.0") {
15357
15731
  for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
15358
15732
  if (overrides.length === 0) throw new Error("csaf-vex-to-hdf: CSAF VEX document contains no actionable statements (only affected/under_investigation/recommended); no amendment to write");
15359
15733
  const publisherName = publisher.name ?? "";
15734
+ await chainOverrides(overrides);
15360
15735
  return {
15361
15736
  name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
15362
15737
  description: `Imported VEX advisory ${tracking.id ?? ""}`,
@@ -15389,7 +15764,7 @@ function vulnerabilityToOverrides(vuln, publisher, tracking, docTime, productLoo
15389
15764
  function buildOverride(vuln, publisher, tracking, docTime, canonical, products, productLookup) {
15390
15765
  const target = importTargetFor(canonical);
15391
15766
  if (!target) return void 0;
15392
- const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$2);
15767
+ const expiresAt = defaultOverrideExpiry(docTime);
15393
15768
  const override = {
15394
15769
  type: target.overrideType,
15395
15770
  requirementId: vuln.cve,
@@ -15560,9 +15935,8 @@ function overlap(ids, scope) {
15560
15935
  *
15561
15936
  * Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
15562
15937
  */
15563
- const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
15564
15938
  async function convertCyclonedxVexToHdf(input, converterVersion = "1.0.0") {
15565
- validateInputSize(input, "cyclonedx-vex-to-hdf");
15939
+ validateInputSize$1(input, "cyclonedx-vex-to-hdf");
15566
15940
  const bom = parseJSON(input);
15567
15941
  if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
15568
15942
  const docTime = (bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) : null) ?? /* @__PURE__ */ new Date();
@@ -15575,9 +15949,12 @@ async function convertCyclonedxVexToHdf(input, converterVersion = "1.0.0") {
15575
15949
  }
15576
15950
  if (overrides.length === 0) throw new Error("cyclonedx-vex-to-hdf: BOM contains no actionable VEX statements (only exploitable/in_triage or no analysis); no amendment to write");
15577
15951
  const publisherName = publisher.identifier;
15952
+ const name = publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements";
15953
+ const description = bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX";
15954
+ await chainOverrides(overrides);
15578
15955
  return {
15579
- name: publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements",
15580
- description: bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX",
15956
+ name,
15957
+ description,
15581
15958
  overrides,
15582
15959
  appliedBy: publisher,
15583
15960
  generator: {
@@ -15594,7 +15971,7 @@ function vulnerabilityToOverride(v, productLookup, docTime, publisher) {
15594
15971
  const target = importTargetFor(canonical);
15595
15972
  if (!target) return void 0;
15596
15973
  const affectedPackages = affectedPackagesForVuln(v, productLookup);
15597
- const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$1);
15974
+ const expiresAt = defaultOverrideExpiry(docTime);
15598
15975
  const override = {
15599
15976
  type: target.overrideType,
15600
15977
  requirementId: v.id,
@@ -15728,8 +16105,6 @@ function publisherIdentityOrDefault(bom) {
15728
16105
  *
15729
16106
  * Spec: https://spdx.github.io/spdx-spec/v3.0.1/ (security profile)
15730
16107
  */
15731
- /** One year in milliseconds — the override expiresAt horizon. */
15732
- const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
15733
16108
  /** Fallback system identity when no supplier agent resolves. */
15734
16109
  const DEFAULT_APPLIED_BY = "spdx-vex-import";
15735
16110
  /** The four SPDX-3 VEX assessment relationship subtypes -> canonical status. */
@@ -15740,7 +16115,7 @@ const VEX_STATUS_BY_TYPE = {
15740
16115
  security_VexUnderInvestigationVulnAssessmentRelationship: VexStatus.UnderInvestigation
15741
16116
  };
15742
16117
  async function convertSpdxVexToHdf(input, converterVersion = "1.0.0") {
15743
- validateInputSize(input, "spdx-vex-to-hdf");
16118
+ validateInputSize$1(input, "spdx-vex-to-hdf");
15744
16119
  const graph = parseJSON(input)["@graph"] ?? [];
15745
16120
  if (graph.length === 0) throw new Error("spdx-vex-to-hdf: document has no @graph elements");
15746
16121
  const idx = buildIndex(graph);
@@ -15752,8 +16127,10 @@ async function convertSpdxVexToHdf(input, converterVersion = "1.0.0") {
15752
16127
  }
15753
16128
  if (overrides.length === 0) throw new Error("spdx-vex-to-hdf: SPDX document contains no actionable VEX statements (only affected/under_investigation); no amendment to write");
15754
16129
  const appliedBy = documentIdentity(graph, idx);
16130
+ const name = appliedBy.identifier && appliedBy.identifier !== DEFAULT_APPLIED_BY ? `SPDX VEX statements from ${appliedBy.identifier}` : "SPDX VEX statements";
16131
+ await chainOverrides(overrides);
15755
16132
  return {
15756
- name: appliedBy.identifier && appliedBy.identifier !== DEFAULT_APPLIED_BY ? `SPDX VEX statements from ${appliedBy.identifier}` : "SPDX VEX statements",
16133
+ name,
15757
16134
  description: "Imported SPDX 3.0.1 security-profile VEX statements",
15758
16135
  overrides,
15759
16136
  appliedBy,
@@ -15800,7 +16177,7 @@ function relationshipToOverride(rel, idx) {
15800
16177
  const requirementId = cveIdentifier(vuln);
15801
16178
  if (!requirementId) return void 0;
15802
16179
  const appliedAt = createdAt(rel.creationInfo, idx) ?? createdAt(vuln?.creationInfo, idx) ?? /* @__PURE__ */ new Date();
15803
- const expiresAt = new Date(appliedAt.getTime() + DEFAULT_EXPIRY_HORIZON_MS);
16180
+ const expiresAt = defaultOverrideExpiry(appliedAt);
15804
16181
  const override = {
15805
16182
  type: target.overrideType,
15806
16183
  requirementId,
@@ -15897,16 +16274,11 @@ function buildCvss(rel, cve) {
15897
16274
  return cvss;
15898
16275
  }
15899
16276
  function cvssVersion(relType, vector) {
15900
- if (vector?.startsWith("CVSS:")) {
15901
- const rest = vector.slice(5);
15902
- const slash = rest.indexOf("/");
15903
- if (slash > 0) switch (rest.slice(0, slash)) {
15904
- case "2.0": return Version.The20;
15905
- case "3.0": return Version.The30;
15906
- case "3.1": return Version.The31;
15907
- case "4.0": return Version.The40;
15908
- }
15909
- }
16277
+ return cvssVersionFromVector(vector, cvssVersionFromRelType(relType));
16278
+ }
16279
+ /** Fallback when the vector carries no recognized prefix: the relationship
16280
+ * subtype names the CVSS generation. */
16281
+ function cvssVersionFromRelType(relType) {
15910
16282
  if ((relType ?? "").includes("CvssV2")) return Version.The20;
15911
16283
  if ((relType ?? "").includes("CvssV4")) return Version.The40;
15912
16284
  return Version.The31;
@@ -15965,7 +16337,7 @@ function isCvssType(type) {
15965
16337
  const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
15966
16338
  const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
15967
16339
  const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
15968
- const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
16340
+ const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
15969
16341
  /** Map an HDF Cvss block to a CSAF score entry (cvss_v2/v3/v4 by version). */
15970
16342
  function buildCsafScore(cvss, products) {
15971
16343
  const inner = {
@@ -15981,7 +16353,7 @@ function buildCsafScore(cvss, products) {
15981
16353
  };
15982
16354
  }
15983
16355
  function convertHdfToCsafVex(input, converterVersion) {
15984
- validateInputSize(input, "hdf-to-csaf-vex");
16356
+ validateInputSize$1(input, "hdf-to-csaf-vex");
15985
16357
  const amendments = parseHdf(input);
15986
16358
  const groups = groupByCVE(amendments.overrides ?? []);
15987
16359
  const registry = /* @__PURE__ */ new Map();
@@ -16000,8 +16372,8 @@ function convertHdfToCsafVex(input, converterVersion) {
16000
16372
  const doc = buildDocument(amendments, converterVersion);
16001
16373
  doc.vulnerabilities = vulnerabilities;
16002
16374
  doc.product_tree.full_product_names = names.length > 0 ? names : [{
16003
- name: DEFAULT_PRODUCT_ID$2,
16004
- product_id: DEFAULT_PRODUCT_ID$2
16375
+ name: DEFAULT_PRODUCT_ID$1,
16376
+ product_id: DEFAULT_PRODUCT_ID$1
16005
16377
  }];
16006
16378
  return JSON.stringify(doc, null, 2);
16007
16379
  }
@@ -16054,8 +16426,8 @@ function productEntriesFor(o) {
16054
16426
  }));
16055
16427
  }
16056
16428
  return [{
16057
- name: DEFAULT_PRODUCT_ID$2,
16058
- product_id: DEFAULT_PRODUCT_ID$2
16429
+ name: DEFAULT_PRODUCT_ID$1,
16430
+ product_id: DEFAULT_PRODUCT_ID$1
16059
16431
  }];
16060
16432
  }
16061
16433
  /** product_tree entries for the synthesized fixed-version products. */
@@ -16094,18 +16466,36 @@ function productIDsFor$1(o) {
16094
16466
  const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
16095
16467
  if (parts.length > 0) return parts;
16096
16468
  }
16097
- return [DEFAULT_PRODUCT_ID$2];
16469
+ return [DEFAULT_PRODUCT_ID$1];
16098
16470
  }
16099
16471
  function stripProductsLine$1(reason) {
16100
16472
  return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
16101
16473
  }
16102
16474
  /**
16475
+ * Fill CSAF references[].summary, which is required with minLength 1 while the
16476
+ * HDF description backing it is optional. The URL is the documented fallback:
16477
+ * the only text a reference always carries, where a placeholder would assert a
16478
+ * description the source never gave.
16479
+ */
16480
+ function referenceSummary(description, url) {
16481
+ return firstNonEmpty(description, url);
16482
+ }
16483
+ /**
16484
+ * The override reason with the machine-written 'Products:' line removed, or ''
16485
+ * when no prose remains. Both sinks it feeds — threats[].details and
16486
+ * notes[].text — are minLength 1 in CSAF, so an empty result means the element
16487
+ * is omitted, never emitted blank.
16488
+ */
16489
+ function reasonProse(reason) {
16490
+ return firstNonEmpty(stripProductsLine$1(reason ?? ""));
16491
+ }
16492
+ /**
16103
16493
  * Surface the override reason prose as a CSAF description note. Used on the
16104
16494
  * not_affected/fixed paths, which otherwise drop reason (the affected path
16105
16495
  * keeps it as threats[impact]).
16106
16496
  */
16107
16497
  function appendReasonNote(v, reason) {
16108
- const text = stripProductsLine$1(reason ?? "");
16498
+ const text = reasonProse(reason);
16109
16499
  if (!text) return;
16110
16500
  v.notes = v.notes ?? [];
16111
16501
  v.notes.push({
@@ -16182,11 +16572,12 @@ function buildVulnerability(group) {
16182
16572
  emitted = true;
16183
16573
  } else if (canonical === VexStatus.Affected) {
16184
16574
  status.known_affected = (status.known_affected ?? []).concat(pids);
16185
- if (o.reason) {
16575
+ const details = reasonProse(o.reason);
16576
+ if (details) {
16186
16577
  v.threats = v.threats ?? [];
16187
16578
  v.threats.push({
16188
16579
  category: "impact",
16189
- details: stripProductsLine$1(o.reason),
16580
+ details,
16190
16581
  product_ids: pids
16191
16582
  });
16192
16583
  }
@@ -16208,7 +16599,7 @@ function buildVulnerability(group) {
16208
16599
  v.references = v.references ?? [];
16209
16600
  v.references.push({
16210
16601
  category: "external",
16211
- summary: e.description ?? "",
16602
+ summary: referenceSummary(e.description, e.data),
16212
16603
  url: e.data
16213
16604
  });
16214
16605
  }
@@ -16217,7 +16608,7 @@ function buildVulnerability(group) {
16217
16608
  v.references = v.references ?? [];
16218
16609
  v.references.push({
16219
16610
  category: "external",
16220
- summary: r.description ?? "",
16611
+ summary: referenceSummary(r.description, r.href),
16221
16612
  url: r.href
16222
16613
  });
16223
16614
  }
@@ -16239,7 +16630,7 @@ function buildVulnerability(group) {
16239
16630
  return {
16240
16631
  cve: v.cve,
16241
16632
  ...v.notes && { notes: v.notes },
16242
- product_status: status,
16633
+ ...Object.keys(status).length > 0 && { product_status: status },
16243
16634
  ...v.flags && { flags: v.flags },
16244
16635
  ...v.threats && { threats: v.threats },
16245
16636
  ...v.remediations && { remediations: v.remediations },
@@ -16319,9 +16710,8 @@ const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
16319
16710
  const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
16320
16711
  const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
16321
16712
  const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
16322
- const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
16323
16713
  async function convertHdfToOpenVex(input, converterVersion) {
16324
- validateInputSize(input, "hdf-to-openvex");
16714
+ validateInputSize$1(input, "hdf-to-openvex");
16325
16715
  const amendments = parseHdf(input);
16326
16716
  const author = amendments.appliedBy?.identifier || "HDF Amendments Export";
16327
16717
  const role = amendments.appliedBy?.description;
@@ -16361,19 +16751,12 @@ function overrideToStatement(o, docAuthor) {
16361
16751
  if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
16362
16752
  const notAffected = canonical === VexStatus.NotAffected;
16363
16753
  const justification = notAffected && o.justification ? String(o.justification) : "";
16364
- const reason = stripProductsLine(o.reason ?? "");
16365
- const impact = notAffected ? reason : "";
16754
+ const reason = firstNonEmpty(stripProductsLine(o.reason ?? ""));
16755
+ const impact = notAffected ? firstNonEmpty(reason, justification ? "" : noImpactRationaleRecorded(o)) : "";
16366
16756
  let action = "";
16367
- let reasonEmitted = notAffected && impact !== "";
16368
- if (canonical === VexStatus.Fixed) action = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
16369
- else if (canonical === VexStatus.Affected) {
16370
- action = firstMilestoneAction(o);
16371
- if (!action) {
16372
- action = reason;
16373
- reasonEmitted = reason !== "";
16374
- }
16375
- }
16376
- const statusNotes = buildStatusNotes(o, reason, reasonEmitted, docAuthor);
16757
+ if (canonical === VexStatus.Fixed) action = firstNonEmpty(firstMilestoneAction(o), "Fix applied; consumer re-scan confirmed clean.");
16758
+ else if (canonical === VexStatus.Affected) action = firstNonEmpty(firstMilestoneAction(o), reason, noRemediationRecorded(o));
16759
+ const statusNotes = buildStatusNotes(o, reason, reason !== "" && (impact === reason || action === reason), docAuthor);
16377
16760
  return {
16378
16761
  vulnerability: {
16379
16762
  "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`,
@@ -16420,12 +16803,26 @@ function productsFor(o) {
16420
16803
  const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
16421
16804
  if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
16422
16805
  }
16423
- if (ids.length === 0) ids = [DEFAULT_PRODUCT_ID$1];
16806
+ if (ids.length === 0) return void 0;
16424
16807
  return ids.map((id) => ({ "@id": id }));
16425
16808
  }
16809
+ /**
16810
+ * States the absence OpenVEX has no other way to express: action_statement is
16811
+ * required on an affected statement, and a source override may carry neither a
16812
+ * milestone nor a reason to fill it.
16813
+ */
16814
+ function noRemediationRecorded(o) {
16815
+ return `No remediation action was recorded for the ${String(o.type)} override applied to ${o.requirementId}.`;
16816
+ }
16817
+ /**
16818
+ * The not_affected peer of noRemediationRecorded, for an override carrying
16819
+ * neither a justification enum nor a reason.
16820
+ */
16821
+ function noImpactRationaleRecorded(o) {
16822
+ return `No impact rationale was recorded for the ${String(o.type)} override applied to ${o.requirementId}.`;
16823
+ }
16426
16824
  function firstMilestoneAction(o) {
16427
- for (const m of o.milestones ?? []) if (m.description) return m.description;
16428
- return "";
16825
+ return firstNonEmpty(...(o.milestones ?? []).map((m) => m.description));
16429
16826
  }
16430
16827
  function allMilestonesCompleted$1(o) {
16431
16828
  if (!o.milestones || o.milestones.length === 0) return false;
@@ -16512,7 +16909,7 @@ function buildCdxRating(cvss) {
16512
16909
  return rating;
16513
16910
  }
16514
16911
  async function convertHdfToCyclonedxVex(input, converterVersion) {
16515
- validateInputSize(input, "hdf-to-cyclonedx-vex");
16912
+ validateInputSize$1(input, "hdf-to-cyclonedx-vex");
16516
16913
  const amendments = parseHdf(input);
16517
16914
  const componentRegistry = /* @__PURE__ */ new Map();
16518
16915
  const vulnerabilities = [];
@@ -16712,7 +17109,7 @@ function detectOscalDocumentType(input) {
16712
17109
  * @returns HDF Baseline JSON string
16713
17110
  */
16714
17111
  async function convertOscalCatalogToHdf(input) {
16715
- validateInputSize(input, "oscal-catalog");
17112
+ validateInputSize$1(input, "oscal-catalog");
16716
17113
  if (!input || input.trim().length === 0) throw new Error("empty input");
16717
17114
  const doc = parseJSON(input);
16718
17115
  if (!doc.catalog) throw new Error("oscal-catalog: input is not a catalog document (root key is not 'catalog')");
@@ -16833,7 +17230,7 @@ function catalogBaselineName(catalog) {
16833
17230
  * @returns HDF Baseline JSON string
16834
17231
  */
16835
17232
  async function convertOscalComponentToHdf(input) {
16836
- validateInputSize(input, "oscal-component-definition");
17233
+ validateInputSize$1(input, "oscal-component-definition");
16837
17234
  if (!input || input.trim().length === 0) throw new Error("empty input");
16838
17235
  const doc = parseJSON(input);
16839
17236
  if (!doc["component-definition"]) throw new Error("oscal-component-definition: input is not a component-definition document (root key is not 'component-definition')");
@@ -16902,7 +17299,7 @@ function componentBaselineName(comp, compDef) {
16902
17299
  * @returns HDF Amendments JSON string
16903
17300
  */
16904
17301
  async function convertOscalPoamToHdf(input) {
16905
- validateInputSize(input, "oscal-poam");
17302
+ validateInputSize$1(input, "oscal-poam");
16906
17303
  if (!input || input.trim().length === 0) throw new Error("empty input");
16907
17304
  const doc = parseJSON(input);
16908
17305
  if (!doc["plan-of-action-and-milestones"]) throw new Error("oscal-poam: input is not a plan-of-action-and-milestones document (root key is not 'plan-of-action-and-milestones')");
@@ -16914,6 +17311,7 @@ async function convertOscalPoamToHdf(input) {
16914
17311
  for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
16915
17312
  const systemRef = poam["import-ssp"]?.href || void 0;
16916
17313
  const appliedBy = extractAppliedBy(poam.metadata);
17314
+ await chainOverrides(overrides);
16917
17315
  return serializeHdf({
16918
17316
  name: toKebabCase(poam.metadata.title, "oscal-poam"),
16919
17317
  overrides,
@@ -16973,27 +17371,27 @@ function poamItemStatus(item, riskMap) {
16973
17371
  const risk = riskMap.get(riskUuid);
16974
17372
  if (risk) {
16975
17373
  const status = oscalStatusToHdf(risk.status);
16976
- if (status === "passed") return "passed";
16977
- if (status === "failed") return "failed";
17374
+ if (status === "passed") return ResultStatus.Passed;
17375
+ if (status === "failed") return ResultStatus.Failed;
16978
17376
  }
16979
17377
  }
16980
17378
  }
16981
- return "failed";
17379
+ return ResultStatus.Failed;
16982
17380
  }
16983
17381
  function poamItemAppliedBy(poam) {
16984
17382
  const rps = poam.metadata["responsible-parties"];
16985
17383
  if (rps) {
16986
17384
  for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
16987
- type: "simple",
17385
+ type: IdentityType.Simple,
16988
17386
  identifier: rp["party-uuids"][0]
16989
17387
  };
16990
17388
  if (rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
16991
- type: "simple",
17389
+ type: IdentityType.Simple,
16992
17390
  identifier: rps[0]["party-uuids"][0]
16993
17391
  };
16994
17392
  }
16995
17393
  return {
16996
- type: "system",
17394
+ type: IdentityType.System,
16997
17395
  identifier: "oscal-poam-converter"
16998
17396
  };
16999
17397
  }
@@ -17029,7 +17427,7 @@ function extractMilestones(item, riskMap) {
17029
17427
  milestones.push({
17030
17428
  description: task.description ? `${task.title}: ${task.description}` : task.title,
17031
17429
  estimatedCompletion,
17032
- status: "pending"
17430
+ status: MilestoneStatus.Pending
17033
17431
  });
17034
17432
  }
17035
17433
  }
@@ -17040,7 +17438,7 @@ function extractAppliedBy(meta) {
17040
17438
  const rps = meta["responsible-parties"];
17041
17439
  if (rps) {
17042
17440
  for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
17043
- type: "simple",
17441
+ type: IdentityType.Simple,
17044
17442
  identifier: rp["party-uuids"][0]
17045
17443
  };
17046
17444
  }
@@ -17059,7 +17457,7 @@ function extractAppliedBy(meta) {
17059
17457
  * @returns HDF Plan JSON string
17060
17458
  */
17061
17459
  async function convertOscalSapToHdf(input) {
17062
- validateInputSize(input, "oscal-assessment-plan");
17460
+ validateInputSize$1(input, "oscal-assessment-plan");
17063
17461
  if (!input || input.trim().length === 0) throw new Error("empty input");
17064
17462
  const doc = parseJSON(input);
17065
17463
  if (!doc["assessment-plan"]) throw new Error("oscal-assessment-plan: input is not an assessment-plan document (root key is not 'assessment-plan')");
@@ -17189,7 +17587,7 @@ function buildPlanDescription(ap) {
17189
17587
  * @returns HDF Results JSON string
17190
17588
  */
17191
17589
  async function convertOscalSarToHdf(input) {
17192
- validateInputSize(input, "oscal-assessment-results");
17590
+ validateInputSize$1(input, "oscal-assessment-results");
17193
17591
  if (!input || input.trim().length === 0) throw new Error("empty input");
17194
17592
  const doc = parseJSON(input);
17195
17593
  if (!doc["assessment-results"]) throw new Error("oscal-assessment-results: input is not an assessment-results document (root key is not 'assessment-results')");
@@ -17498,7 +17896,7 @@ function sarBaselineName(result, sar) {
17498
17896
  * @returns HDF System JSON string
17499
17897
  */
17500
17898
  async function convertOscalSspToHdf(input) {
17501
- validateInputSize(input, "oscal-ssp");
17899
+ validateInputSize$1(input, "oscal-ssp");
17502
17900
  if (!input || input.trim().length === 0) throw new Error("empty input");
17503
17901
  const doc = parseJSON(input);
17504
17902
  if (!doc["system-security-plan"]) throw new Error("oscal-ssp: input is not a system-security-plan document (root key is not 'system-security-plan')");
@@ -17675,7 +18073,7 @@ function mapOSCALComponentType(oscalType) {
17675
18073
  * @returns HDF Baseline JSON string
17676
18074
  */
17677
18075
  async function convertOscalProfileToHdf(profileInput, catalogInput) {
17678
- validateInputSize(profileInput, "oscal-profile");
18076
+ validateInputSize$1(profileInput, "oscal-profile");
17679
18077
  if (!profileInput || profileInput.trim().length === 0) throw new Error("empty profile input");
17680
18078
  if (!catalogInput || catalogInput.trim().length === 0) throw new Error("empty catalog input");
17681
18079
  const profileDoc = parseJSON(profileInput);
@@ -17872,8 +18270,8 @@ function toRfc3339(d) {
17872
18270
  * TypeScript peer of shared/go/enrich_stix.go (kept at parity).
17873
18271
  */
17874
18272
  function enrichStix(resultsInput, bundleInput, opts) {
17875
- validateInputSize(resultsInput, "enrich-stix", opts?.maxSize);
17876
- validateInputSize(bundleInput, "enrich-stix", opts?.maxSize);
18273
+ validateInputSize$1(resultsInput, "enrich-stix", opts?.maxSize);
18274
+ validateInputSize$1(bundleInput, "enrich-stix", opts?.maxSize);
17877
18275
  if (!resultsInput) throw new Error("enrich-stix: empty results input");
17878
18276
  const bundle = parseStixBundle(bundleInput);
17879
18277
  let parsed;