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

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 { buildCsv, buildXml, canonicalJson, 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, 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.
@@ -161,6 +303,20 @@ function extractCWEIDs(text) {
161
303
  ids.sort();
162
304
  return ids;
163
305
  }
306
+ /**
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
+ }
164
320
  /** Default maximum input size for converters (50MB) */
165
321
  const DEFAULT_MAX_INPUT_SIZE = 50 * 1024 * 1024;
166
322
  /**
@@ -202,6 +358,15 @@ function requireHdfResults(input, converterName) {
202
358
  return requireHdfStructure(input, converterName, "baselines");
203
359
  }
204
360
  /**
361
+ * The HDF Amendments counterpart of requireHdfResults, keyed on the top-level
362
+ * `overrides` array instead of `baselines`.
363
+ */
364
+ function requireHdfAmendments(input, converterName) {
365
+ const r = requireHdfStructure(input, converterName, "overrides");
366
+ if (r.items.length === 0) throw new Error(`${converterName}: invalid HDF structure: missing overrides field`);
367
+ return r;
368
+ }
369
+ /**
205
370
  * Reject malformed HDF input (empty, oversized, non-JSON, top-level array/null,
206
371
  * or a missing/wrong-typed structural key) before a converter zero-fills it into
207
372
  * a typed document. A parsed JSON `null` (or array, or primitive) is NOT an HDF
@@ -216,6 +381,10 @@ function requireHdfStructure(input, converterName, field) {
216
381
  const doc = parsed;
217
382
  const items = doc[field];
218
383
  if (!Array.isArray(items)) throw new Error(`${converterName}: invalid HDF structure: missing ${field} field`);
384
+ if (field === "baselines") {
385
+ const violations = typeViolations(validateResults(doc));
386
+ if (violations.length > 0) throw new Error(`${converterName}: input is not valid HDF: ${violations.slice(0, 5).join("; ")}`);
387
+ }
219
388
  return {
220
389
  doc,
221
390
  items
@@ -511,6 +680,24 @@ const UNRATED_SEVERITY_VALUE = "unrated";
511
680
  function markUnratedSeverity(tags, severity) {
512
681
  if (isUnratedSeverity(severity)) tags[UNRATED_SEVERITY_TAG] = UNRATED_SEVERITY_VALUE;
513
682
  }
683
+ /**
684
+ * The schema violations Go's typed decode would also reject.
685
+ *
686
+ * encoding/json refuses exactly two things: a JSON value whose type does not fit
687
+ * the target field, and a string that will not parse into a time.Time. Every
688
+ * other schema rule — required, enum, minItems, uuid and uri formats — decodes
689
+ * cleanly into the generated structs, so rejecting it here would put the two
690
+ * languages back out of step in the opposite direction.
691
+ *
692
+ * null is exempt for the same reason. Go unmarshals a JSON null into a slice,
693
+ * map or pointer as nil and into anything else as a no-op, never as an error, so
694
+ * a null cvss entry or a null results array reaches the converter there and must
695
+ * reach it here too — several parity goldens pin exactly that.
696
+ */
697
+ function typeViolations(result) {
698
+ if (result.valid) return [];
699
+ 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());
700
+ }
514
701
  //#endregion
515
702
  //#region converters/legacyhdf-to-hdf/typescript/converter.ts
516
703
  /**
@@ -1725,11 +1912,10 @@ function buildTags$4(result, rule, resolvedLevel, cweIds, nistControls, cciContr
1725
1912
  function extractSourceLocation$1(location) {
1726
1913
  const uri = location.physicalLocation?.artifactLocation?.uri;
1727
1914
  const line = location.physicalLocation?.region?.startLine;
1728
- if (!uri || !line) return;
1729
- return {
1730
- ref: uri,
1731
- line
1732
- };
1915
+ const sourceLocation = {};
1916
+ if (uri) sourceLocation.ref = uri;
1917
+ if (line) sourceLocation.line = line;
1918
+ return sourceLocation.ref !== void 0 || sourceLocation.line !== void 0 ? sourceLocation : void 0;
1733
1919
  }
1734
1920
  function createHDFResult(location, status, backtrace, suppressionMessage) {
1735
1921
  const uri = location.physicalLocation?.artifactLocation?.uri || "";
@@ -1790,7 +1976,7 @@ function hostComponents(suites) {
1790
1976
  const hosts = [];
1791
1977
  const seen = /* @__PURE__ */ new Set();
1792
1978
  for (const suite of suites) {
1793
- const hostname = suite.hostname?.trim();
1979
+ const hostname = suite["@_hostname"]?.trim();
1794
1980
  if (!hostname || seen.has(hostname)) continue;
1795
1981
  seen.add(hostname);
1796
1982
  hosts.push({
@@ -1802,22 +1988,31 @@ function hostComponents(suites) {
1802
1988
  return hosts;
1803
1989
  }
1804
1990
  function resolveScanTime(suites) {
1805
- for (const suite of suites) if (suite.timestamp) {
1806
- const parsed = parseTimestamp(suite.timestamp);
1991
+ for (const suite of suites) if (suite["@_timestamp"]) {
1992
+ const parsed = parseTimestamp(suite["@_timestamp"]);
1807
1993
  if (parsed) return parsed;
1808
1994
  }
1809
1995
  return /* @__PURE__ */ new Date();
1810
1996
  }
1811
1997
  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
- };
1998
+ const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2, { attributeNamePrefix: "@_" });
1999
+ if (parsed.testsuites) {
2000
+ const suites = [...parsed.testsuites.testsuite ?? []];
2001
+ const name = parsed.testsuites["@_name"] ? decodeXmlEntities$6(parsed.testsuites["@_name"]) : "JUnit Test Results";
2002
+ const looseCases = parsed.testsuites.testcase ?? [];
2003
+ if (looseCases.length > 0) suites.push({
2004
+ "@_name": name,
2005
+ testcase: looseCases
2006
+ });
2007
+ return {
2008
+ suites,
2009
+ name
2010
+ };
2011
+ }
1817
2012
  if (parsed.testsuite) {
1818
2013
  const suite = parsed.testsuite;
1819
2014
  const suites = Array.isArray(suite) ? suite : [suite];
1820
- const suiteName = suites[0]?.name;
2015
+ const suiteName = suites[0]?.["@_name"];
1821
2016
  return {
1822
2017
  suites,
1823
2018
  name: suiteName ? decodeXmlEntities$6(suiteName) : "JUnit Test Results"
@@ -1848,14 +2043,14 @@ function testCaseToRequirement(tc, scanTime) {
1848
2043
  startTime: scanTime
1849
2044
  };
1850
2045
  if (message !== void 0) result.message = message;
1851
- if (tc.time) {
1852
- const parsed = parseFloat(tc.time);
2046
+ if (tc["@_time"]) {
2047
+ const parsed = parseFloat(tc["@_time"]);
1853
2048
  if (!isNaN(parsed)) result.runTime = parsed;
1854
2049
  }
1855
- const name = decodeXmlEntities$6(tc.name);
2050
+ const name = decodeXmlEntities$6(tc["@_name"]);
1856
2051
  const descriptions = [{
1857
2052
  label: "default",
1858
- data: `JUnit test: ${name} in ${tc.classname ? decodeXmlEntities$6(tc.classname) : "unknown"}`
2053
+ data: `JUnit test: ${name} in ${tc["@_classname"] ? decodeXmlEntities$6(tc["@_classname"]) : "unknown"}`
1859
2054
  }];
1860
2055
  const { systemOut, systemErr } = collectSystemStreams(tc);
1861
2056
  if (systemOut) descriptions.push({
@@ -1894,19 +2089,19 @@ function collectSystemStreams(tc) {
1894
2089
  };
1895
2090
  }
1896
2091
  function buildID(tc) {
1897
- if (tc.classname) return `${decodeXmlEntities$6(tc.classname)}.${decodeXmlEntities$6(tc.name)}`;
1898
- return decodeXmlEntities$6(tc.name);
2092
+ if (tc["@_classname"]) return `${decodeXmlEntities$6(tc["@_classname"])}.${decodeXmlEntities$6(tc["@_name"])}`;
2093
+ return decodeXmlEntities$6(tc["@_name"]);
1899
2094
  }
1900
2095
  function resolveStatus(tc) {
1901
2096
  if (tc.failure) {
1902
- const msg = buildFailureMessage(tc.failure.message ?? "", tc.failure.type ?? "", tc.failure["#text"] ?? "");
2097
+ const msg = buildFailureMessage(tc.failure["@_message"] ?? "", tc.failure["@_type"] ?? "", tc.failure["#text"] ?? "");
1903
2098
  return {
1904
2099
  status: ResultStatus.Failed,
1905
2100
  message: msg
1906
2101
  };
1907
2102
  }
1908
2103
  if (tc.error) {
1909
- const msg = buildFailureMessage(tc.error.message ?? "", tc.error.type ?? "", tc.error["#text"] ?? "");
2104
+ const msg = buildFailureMessage(tc.error["@_message"] ?? "", tc.error["@_type"] ?? "", tc.error["#text"] ?? "");
1910
2105
  return {
1911
2106
  status: ResultStatus.Error,
1912
2107
  message: msg
@@ -1914,9 +2109,9 @@ function resolveStatus(tc) {
1914
2109
  }
1915
2110
  if (tc.skipped !== void 0) {
1916
2111
  const skipped = typeof tc.skipped === "object" ? tc.skipped : null;
1917
- if (skipped?.message) return {
2112
+ if (skipped?.["@_message"]) return {
1918
2113
  status: ResultStatus.NotReviewed,
1919
- message: `Skipped: ${decodeXmlEntities$6(skipped.message)}`
2114
+ message: `Skipped: ${decodeXmlEntities$6(skipped["@_message"])}`
1920
2115
  };
1921
2116
  return {
1922
2117
  status: ResultStatus.NotReviewed,
@@ -1933,12 +2128,12 @@ function buildFailureMessage(message, typeName, body) {
1933
2128
  return result;
1934
2129
  }
1935
2130
  function buildCodeDesc$10(tc) {
1936
- if (tc.classname) return `${decodeXmlEntities$6(tc.classname)} :: ${decodeXmlEntities$6(tc.name)}`;
1937
- return decodeXmlEntities$6(tc.name);
2131
+ if (tc["@_classname"]) return `${decodeXmlEntities$6(tc["@_classname"])} :: ${decodeXmlEntities$6(tc["@_name"])}`;
2132
+ return decodeXmlEntities$6(tc["@_name"]);
1938
2133
  }
1939
2134
  function noFindingsTarget(baselineName, suites) {
1940
2135
  if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
1941
- for (const s of suites) if (s.name) return decodeXmlEntities$6(s.name);
2136
+ for (const s of suites) if (s["@_name"]) return decodeXmlEntities$6(s["@_name"]);
1942
2137
  return "JUnit test suite";
1943
2138
  }
1944
2139
  //#endregion
@@ -3105,9 +3300,9 @@ function stamp(v) {
3105
3300
  /** Maps a requirement onto the canonical effective-status input shape. */
3106
3301
  function requirementStatusInput(req) {
3107
3302
  return {
3108
- impact: req.impact,
3109
- resultStatuses: (req.results ?? []).map((r) => String(r.status)),
3110
- overrides: (req.statusOverrides ?? []).map((o) => ({
3303
+ impact: req.impact ?? 0,
3304
+ resultStatuses: (req.results ?? []).filter((r) => r != null).map((r) => String(r.status)),
3305
+ overrides: (req.statusOverrides ?? []).filter((o) => o != null).map((o) => ({
3111
3306
  status: o.status ? String(o.status) : void 0,
3112
3307
  appliedAt: stamp(o.appliedAt),
3113
3308
  expiresAt: stamp(o.expiresAt)
@@ -7413,10 +7608,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
7413
7608
  * @returns CSV string with sanitized output
7414
7609
  */
7415
7610
  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");
7611
+ const { doc: hdfDoc } = requireHdfResults(input, "hdf-to-csv");
7612
+ const hdf = hdfDoc;
7420
7613
  const rows = [];
7421
7614
  const components = hdf.components || [];
7422
7615
  const targetList = components.length > 0 ? components.map((t) => ({
@@ -7430,35 +7623,64 @@ function convertHdfToCsv(input) {
7430
7623
  fqdn: "",
7431
7624
  ipAddress: ""
7432
7625
  }];
7433
- for (const baseline of hdf.baselines) for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
7626
+ for (const baseline of hdf.baselines) {
7627
+ if (!Array.isArray(baseline.requirements) || baseline.requirements.length === 0) throw new Error(`hdf-to-csv: baseline "${text(baseline.name)}" has no requirements`);
7628
+ for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
7629
+ }
7434
7630
  return buildCsv(rows, { sanitize: true });
7435
7631
  }
7436
7632
  /**
7633
+ * A schema-required number as a number. Go decodes an absent one to the zero
7634
+ * value and cannot tell it apart from a real zero — impact 0 legitimately means
7635
+ * Not Applicable — so distinguishing them would need a schema validation pass
7636
+ * the converters deliberately do not do. Rendering 0 here matches what Go emits,
7637
+ * where reading .toFixed off an absent value threw a raw TypeError instead.
7638
+ */
7639
+ function numeric(value) {
7640
+ return typeof value === "number" ? value : 0;
7641
+ }
7642
+ /**
7643
+ * A schema-required string as text. Nested-invalid input reaches the converter
7644
+ * with these absent, and an absent value reached sanitizeCsvValue's String() as
7645
+ * the literal "undefined" — where Go, formatting a zero-value string, wrote
7646
+ * nothing. Every schema-required STRING cell goes through here; the numeric
7647
+ * ones go through numeric(), which serves the same purpose for a different type.
7648
+ */
7649
+ function text(value) {
7650
+ return typeof value === "string" ? value : "";
7651
+ }
7652
+ /** The requirement id as text; also names the requirement in the no-results error. */
7653
+ function requirementIdText$1(id) {
7654
+ return text(id);
7655
+ }
7656
+ /**
7437
7657
  * Create a CSV row from baseline, requirement, and target data
7438
7658
  */
7439
7659
  function createRow(baseline, requirement, target) {
7440
- const description = requirement.descriptions.find((d) => d.label === "default")?.data || "";
7660
+ const [firstResult] = Array.isArray(requirement.results) ? requirement.results : [];
7661
+ if (!firstResult) throw new Error(`hdf-to-csv: requirement "${requirementIdText$1(requirement.id)}" has no results`);
7662
+ if (!Array.isArray(requirement.descriptions) || requirement.descriptions.length === 0) throw new Error(`hdf-to-csv: requirement "${requirementIdText$1(requirement.id)}" has no descriptions`);
7663
+ const description = descriptionByLabel(requirement, "default");
7441
7664
  const check = descriptionByLabel(requirement, "check");
7442
7665
  const fix = descriptionByLabel(requirement, "fix");
7443
7666
  const rationale = descriptionByLabel(requirement, "rationale");
7444
7667
  const code = requirement.code ?? "";
7445
7668
  const references = flattenRefs(requirement.refs);
7446
7669
  const severity = getSeverity(requirement);
7447
- const firstResult = requirement.results[0];
7448
- const status = firstResult?.status || "";
7449
- const message = firstResult?.message || "";
7670
+ const status = firstResult.status;
7671
+ const message = firstResult.message ?? "";
7450
7672
  const nistControls = extractArrayFromTags(requirement.tags, "nist");
7451
7673
  const cciControls = extractArrayFromTags(requirement.tags, "cci");
7452
7674
  const effectiveStatus = requirementEffectiveStatus(requirement);
7453
- const effectiveImpact = (requirement.effectiveImpact ?? requirement.impact).toFixed(1);
7675
+ const effectiveImpact = numeric(requirement.effectiveImpact ?? requirement.impact).toFixed(2);
7454
7676
  const disposition = requirement.disposition ? String(requirement.disposition) : "";
7455
7677
  return {
7456
- "Baseline ID": baseline.name,
7678
+ "Baseline ID": text(baseline.name),
7457
7679
  "Baseline Version": baseline.version || "",
7458
7680
  "Baseline Title": baseline.title || "",
7459
- "Target ID": target.name,
7460
- "Target Type": target.type,
7461
- "Requirement ID": requirement.id,
7681
+ "Target ID": text(target.name),
7682
+ "Target Type": text(target.type),
7683
+ "Requirement ID": requirementIdText$1(requirement.id),
7462
7684
  "Requirement Title": requirement.title || "",
7463
7685
  "Description": description,
7464
7686
  "Check": check,
@@ -7467,8 +7689,8 @@ function createRow(baseline, requirement, target) {
7467
7689
  "Code": code,
7468
7690
  "References": references,
7469
7691
  "Severity": severity,
7470
- "Impact": requirement.impact.toFixed(1),
7471
- "Status": String(status),
7692
+ "Impact": numeric(requirement.impact).toFixed(2),
7693
+ "Status": text(status),
7472
7694
  "NIST Controls": nistControls,
7473
7695
  "CCI Controls": cciControls,
7474
7696
  "Control Type": requirement.controlType ?? "",
@@ -7482,8 +7704,8 @@ function createRow(baseline, requirement, target) {
7482
7704
  "Applied By": joinOverrides(requirement.statusOverrides, (o) => o.appliedBy?.identifier ?? ""),
7483
7705
  "Expires At": joinOverrides(requirement.statusOverrides, (o) => formatExpires(o.expiresAt)),
7484
7706
  "CVSS": cvssScores(requirement.cvss),
7485
- "CWE": Array.isArray(requirement.cwe) ? requirement.cwe.join("; ") : "",
7486
- "EPSS": requirement.epss ? requirement.epss.score.toFixed(5) : "",
7707
+ "CWE": (requirement.cwe ?? []).join("; "),
7708
+ "EPSS": requirement.epss ? numeric(requirement.epss.score).toFixed(5) : "",
7487
7709
  "KEV": requirement.kev ? requirement.kev.inKev ? "true" : "false" : "",
7488
7710
  "Target FQDN": target.fqdn,
7489
7711
  "Target IP": target.ipAddress
@@ -7497,6 +7719,7 @@ function joinOverrides(overrides, pick) {
7497
7719
  if (!Array.isArray(overrides)) return "";
7498
7720
  const out = [];
7499
7721
  for (const o of overrides) {
7722
+ if (o === null || o === void 0) continue;
7500
7723
  const v = pick(o);
7501
7724
  if (v) out.push(v);
7502
7725
  }
@@ -7512,14 +7735,17 @@ function formatExpires(value) {
7512
7735
  }
7513
7736
  /**
7514
7737
  * Render each CVSS entry's score (computed when present, else base) to one
7515
- * decimal, joined with '; ' to preserve multi-CVE findings.
7738
+ * decimal — the precision the CVSS spec defines and the source carries — joined
7739
+ * with '; ' to preserve multi-CVE findings. The Go peer rounds through a shared
7740
+ * helper matching toFixed, so a .x5 score renders the same digits in both.
7516
7741
  */
7517
7742
  function cvssScores(entries) {
7518
7743
  if (!Array.isArray(entries)) return "";
7519
7744
  const out = [];
7520
7745
  for (const c of entries) {
7746
+ if (c === null || c === void 0) continue;
7521
7747
  const score = c.computedScore ?? c.baseScore;
7522
- if (score !== void 0) out.push(score.toFixed(1));
7748
+ if (typeof score === "number") out.push(score.toFixed(1));
7523
7749
  }
7524
7750
  return out.join("; ");
7525
7751
  }
@@ -7531,7 +7757,7 @@ function getSeverity(requirement) {
7531
7757
  if ("severity" in requirement.tags) {
7532
7758
  const sev = requirement.tags.severity;
7533
7759
  if (typeof sev === "string") return sev;
7534
- if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
7760
+ if (Array.isArray(sev) && typeof sev[0] === "string") return sev[0];
7535
7761
  }
7536
7762
  }
7537
7763
  const impact = requirement.impact;
@@ -7545,18 +7771,20 @@ function getSeverity(requirement) {
7545
7771
  function extractArrayFromTags(tags, key) {
7546
7772
  if (!tags || typeof tags !== "object") return "";
7547
7773
  const value = tags[key];
7548
- if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
7774
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string").join("; ");
7549
7775
  return "";
7550
7776
  }
7551
7777
  /**
7552
7778
  * Get the data of the first description matching a label. Empty when absent.
7553
7779
  */
7554
7780
  function descriptionByLabel(requirement, label) {
7555
- return requirement.descriptions.find((d) => d.label === label)?.data || "";
7781
+ return requirement.descriptions.find((d) => d !== null && d !== void 0 && d.label === label)?.data || "";
7556
7782
  }
7557
7783
  /**
7558
7784
  * 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
7785
+ * url/uri (or a string `ref`); array-form refs are skipped here, though a document
7786
+ * containing one never reaches this function in Go, whose typed decode rejects
7787
+ * it outright — a reject-versus-degrade split tracked on its own card. Joined with '; ' to
7560
7788
  * match the NIST/CCI column convention.
7561
7789
  */
7562
7790
  function flattenRefs(refs) {
@@ -8655,9 +8883,20 @@ function convertStatistics(stats) {
8655
8883
  * The document is walked as a plain JSON tree and every key is emitted in
8656
8884
  * source-JSON order, so the output can never silently lag a schema addition the
8657
8885
  * 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.
8886
+ * fields). The Go converter walks the same normalized JSON in the same order, so
8887
+ * the two languages emit output that is identical after the shared XML golden
8888
+ * normalization for every shape this repo's fixtures and parity tests cover.
8889
+ *
8890
+ * Equality after that normalization is NOT guaranteed in general, and the reason
8891
+ * is structural rather than a fixed list of cases: JSON.parse loses duplicate
8892
+ * keys and hoists
8893
+ * array-index keys before this builder ever runs, V8's number-to-string forms
8894
+ * differ from Go's strconv at the extremes, and Go's encoding/xml sanitizes every
8895
+ * XML-illegal rune to U+FFFD where this builder emits it verbatim. This builder
8896
+ * also caps nesting depth, which the Go peer does not. Anything landing in those
8897
+ * seams can differ; known instances are tracked as cards under the
8898
+ * exporter-conformance epic. Assume a shape outside the fixture corpus needs
8899
+ * checking against the peer rather than that it is covered.
8661
8900
  */
8662
8901
  /**
8663
8902
  * Maps a container (plural) key to the element name emitted for each of its
@@ -8688,13 +8927,33 @@ function singularFor(key) {
8688
8927
  return PLURAL_TO_SINGULAR[key] ?? "item";
8689
8928
  }
8690
8929
  /**
8691
- * Wrap a primitive so fast-xml-parser renders it as element text rather than an
8692
- * attribute.
8930
+ * Encode a JSON key to a legal XML element name, reporting whether it had to
8931
+ * change. HDF leaves object keys unconstrained — tags carry vendor-namespaced
8932
+ * keys like "sonarqube/hash", and additionalProperties lets a converter park
8933
+ * native fields anywhere — while XML constrains element names to Name, so an
8934
+ * unencoded key produced a document no parser could read.
8935
+ *
8936
+ * The kept set is ASCII — [A-Za-z0-9._-] — not the far wider set XML Name
8937
+ * allows, so the two languages agree by construction rather than by way of two
8938
+ * Unicode tables that can drift apart. It costs nothing here: no tag key in this
8939
+ * package's converter fixtures is non-ASCII. ':' is deliberately not kept —
8940
+ * it is a legal Name character but would invent an undeclared namespace prefix.
8941
+ *
8942
+ * Mirrored by xmlElementName in the Go converter and pinned to a shared table.
8693
8943
  */
8694
- function wrapScalar(value) {
8695
- return { "#text": value };
8944
+ function xmlElementName(key) {
8945
+ const out = [...key].map((ch) => /[A-Za-z0-9.\-_]/.test(ch) ? ch : "_").join("");
8946
+ const [first = ""] = [...out];
8947
+ const name = /[A-Za-z_]/.test(first) ? out : `_${out}`;
8948
+ return [name, name !== key];
8696
8949
  }
8697
8950
  /**
8951
+ * The attribute holding the original key of a rewritten element. The prefix is
8952
+ * what separates an attribute from a child element in the builder; it cannot
8953
+ * collide with a child, because any key spelled this way is itself rewritten.
8954
+ */
8955
+ const NAME_ATTR = "@_name";
8956
+ /**
8698
8957
  * True for a JSON scalar (string, number, bool) or a null/undefined placeholder.
8699
8958
  * Nested objects and arrays are not scalar, which selects the wrapped
8700
8959
  * object-array rendering.
@@ -8702,37 +8961,48 @@ function wrapScalar(value) {
8702
8961
  function isScalar(value) {
8703
8962
  return value === null || value === void 0 || typeof value !== "object";
8704
8963
  }
8964
+ function present(value) {
8965
+ return value !== null && value !== void 0;
8966
+ }
8705
8967
  /**
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.
8968
+ * Render one JSON key as the sibling nodes it becomes, mirroring the Go
8969
+ * writeValue case for case. A list rather than a single node because a scalar
8970
+ * array repeats its key unwrapped, and because two keys can encode to one
8971
+ * element name — preserveOrder keeps each at its own position, which a plain
8972
+ * object keyed by element name could not.
8709
8973
  */
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);
8974
+ function nodesFor(key, value) {
8975
+ const [name, rewritten] = xmlElementName(key);
8976
+ const attrs = rewritten ? { ":@": { [NAME_ATTR]: xmlSafeText(key) } } : {};
8977
+ if (Array.isArray(value)) {
8978
+ if (value.length === 0) return [{
8979
+ [name]: [],
8980
+ ...attrs
8981
+ }];
8982
+ if (value.every(isScalar)) return value.filter(present).flatMap((item) => nodesFor(key, item));
8983
+ const child = singularFor(key);
8984
+ return [{
8985
+ [name]: value.filter(present).flatMap((item) => nodesFor(child, item)),
8986
+ ...attrs
8987
+ }];
8988
+ }
8989
+ if (typeof value === "object") return [{
8990
+ [name]: buildNodes(value),
8991
+ ...attrs
8992
+ }];
8993
+ const text = typeof value === "string" ? xmlSafeText(value) : value;
8994
+ return [{
8995
+ [name]: [{ "#text": text }],
8996
+ ...attrs
8997
+ }];
8715
8998
  }
8716
8999
  /**
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.
9000
+ * Walk a JSON object into fast-xml-parser preserveOrder nodes, emitting keys in
9001
+ * source order. Null-valued keys are omitted, matching the old omitempty
9002
+ * semantics and the Go peer.
8722
9003
  */
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;
9004
+ function buildNodes(obj) {
9005
+ return Object.entries(obj).flatMap(([key, value]) => present(value) ? nodesFor(key, value) : []);
8736
9006
  }
8737
9007
  /**
8738
9008
  * Convert HDF Results JSON to XML.
@@ -8745,7 +9015,10 @@ function convertHdfToXml(input) {
8745
9015
  const hdf = parseHdf(input);
8746
9016
  if (!hdf || typeof hdf !== "object" || Array.isArray(hdf) || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
8747
9017
  if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
8748
- return buildXml({ HdfResults: buildObject(hdf) });
9018
+ return `${buildXml([{ HdfResults: buildNodes(hdf) }], {
9019
+ preserveOrder: true,
9020
+ attributeNamePrefix: "@_"
9021
+ }).trim()}\n`;
8749
9022
  }
8750
9023
  //#endregion
8751
9024
  //#region converters/gitlab-to-hdf/typescript/converter.ts
@@ -8970,18 +9243,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
8970
9243
  const BASELINE_NAME = "Trivy Scan";
8971
9244
  const STATIC_CCI = nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
8972
9245
  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
- }
9246
+ const SEVERITY_ALIASES = { critical: .9 };
8985
9247
  /**
8986
9248
  * Route Trivy output to HDF Results: native Trivy JSON (SchemaVersion 2) is
8987
9249
  * parsed here; SARIF / CycloneDX / ASFF / GitLab are delegated to their
@@ -9031,7 +9293,7 @@ async function convertNative(input, converterVersion) {
9031
9293
  function convertVuln(v, res, startTime) {
9032
9294
  const descriptions = [{
9033
9295
  label: "default",
9034
- data: firstNonEmpty(v.Description, v.Title, v.VulnerabilityID)
9296
+ data: firstNonEmpty(v.Description ?? "", v.Title ?? "", v.VulnerabilityID ?? "")
9035
9297
  }];
9036
9298
  if (v.FixedVersion) descriptions.push({
9037
9299
  label: "fix",
@@ -9049,7 +9311,7 @@ function convertVuln(v, res, startTime) {
9049
9311
  id: `Trivy/${v.VulnerabilityID ?? ""}`,
9050
9312
  title: `Trivy found ${v.VulnerabilityID ?? ""} in ${pkgLabel(v.PkgName, v.InstalledVersion)}`,
9051
9313
  descriptions,
9052
- impact: severityToImpact$2(v.Severity ?? ""),
9314
+ impact: severityToImpactWithAliases(v.Severity, SEVERITY_ALIASES, .5),
9053
9315
  tags,
9054
9316
  cwe: filterCwes(v.CweIDs),
9055
9317
  cvss: buildCvssEntries$4(v.CVSS),
@@ -9061,7 +9323,7 @@ function convertVuln(v, res, startTime) {
9061
9323
  status: ResultStatus.Failed,
9062
9324
  codeDesc: buildVulnCodeDesc(v, res),
9063
9325
  startTime,
9064
- message: `Severity: ${firstNonEmpty(v.Severity, "UNKNOWN")}`
9326
+ message: `Severity: ${firstNonEmpty(v.Severity ?? "", "UNKNOWN")}`
9065
9327
  }]
9066
9328
  };
9067
9329
  const ap = buildAffectedPackage$1(v);
@@ -9072,7 +9334,7 @@ function convertVuln(v, res, startTime) {
9072
9334
  function convertMisconf(m, res, startTime) {
9073
9335
  const descriptions = [{
9074
9336
  label: "default",
9075
- data: firstNonEmpty(m.Description, m.Message, m.Title)
9337
+ data: firstNonEmpty(m.Description ?? "", m.Message ?? "", m.Title ?? "")
9076
9338
  }];
9077
9339
  if (m.Resolution) descriptions.push({
9078
9340
  label: "fix",
@@ -9084,10 +9346,10 @@ function convertMisconf(m, res, startTime) {
9084
9346
  });
9085
9347
  markUnratedSeverity(tags, m.Severity);
9086
9348
  const req = {
9087
- id: `Trivy/${firstNonEmpty(m.ID, m.AVDID)}`,
9349
+ id: `Trivy/${firstNonEmpty(m.ID ?? "", m.AVDID ?? "")}`,
9088
9350
  title: m.Title,
9089
9351
  descriptions,
9090
- impact: severityToImpact$2(m.Severity ?? ""),
9352
+ impact: severityToImpactWithAliases(m.Severity, SEVERITY_ALIASES, .5),
9091
9353
  tags,
9092
9354
  refs: buildRefs$5([m.PrimaryURL, ...m.References ?? []]),
9093
9355
  code: JSON.stringify(m, null, 2),
@@ -9095,7 +9357,7 @@ function convertMisconf(m, res, startTime) {
9095
9357
  verificationMethod: VerificationMethodEnum.Automated,
9096
9358
  results: [{
9097
9359
  status: misconfStatus(m.Status),
9098
- codeDesc: firstNonEmpty(m.Message, m.Title),
9360
+ codeDesc: firstNonEmpty(m.Message ?? "", m.Title ?? ""),
9099
9361
  startTime
9100
9362
  }]
9101
9363
  };
@@ -9117,9 +9379,9 @@ function convertSecret(s, res, startTime) {
9117
9379
  title: s.Title,
9118
9380
  descriptions: [{
9119
9381
  label: "default",
9120
- data: `${firstNonEmpty(s.Title, s.RuleID)} detected in ${res.Target ?? ""} (value redacted by Trivy).`
9382
+ data: `${firstNonEmpty(s.Title ?? "", s.RuleID ?? "")} detected in ${res.Target ?? ""} (value redacted by Trivy).`
9121
9383
  }],
9122
- impact: severityToImpact$2(s.Severity ?? ""),
9384
+ impact: severityToImpactWithAliases(s.Severity, SEVERITY_ALIASES, .5),
9123
9385
  tags,
9124
9386
  code: JSON.stringify(s, null, 2),
9125
9387
  controlType: deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS),
@@ -9151,7 +9413,7 @@ function convertLicense(l, res, startTime) {
9151
9413
  label: "default",
9152
9414
  data: `Package ${l.PkgName ?? ""} uses the ${l.Name ?? ""} license (category: ${l.Category ?? ""}).`
9153
9415
  }],
9154
- impact: severityToImpact$2(l.Severity ?? ""),
9416
+ impact: severityToImpactWithAliases(l.Severity, SEVERITY_ALIASES, .5),
9155
9417
  tags,
9156
9418
  refs: buildRefs$5([l.Link]),
9157
9419
  code: JSON.stringify(l, null, 2),
@@ -9232,17 +9494,6 @@ function buildCvssEntries$4(m) {
9232
9494
  }
9233
9495
  return entries.length > 0 ? entries : void 0;
9234
9496
  }
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
9497
  function buildAffectedPackage$1(v) {
9247
9498
  const ap = {};
9248
9499
  if (v.PkgName) ap.name = v.PkgName;
@@ -9257,8 +9508,8 @@ function buildAffectedPackage$1(v) {
9257
9508
  return ap;
9258
9509
  }
9259
9510
  function ecosystemFromPurl(purl) {
9260
- const type = (purl.replace(/^pkg:/, "").split(/[/@?]/)[0] ?? "").toLowerCase();
9261
- return PURL_ECOSYSTEMS[type];
9511
+ const eco = ecosystemFromPurlType(purl.replace(/^pkg:/, "").split(/[/@?]/)[0] ?? "");
9512
+ return eco === Ecosystem.Generic ? void 0 : eco;
9262
9513
  }
9263
9514
  function buildRefs$5(urls) {
9264
9515
  const seen = /* @__PURE__ */ new Set();
@@ -9285,10 +9536,6 @@ function digestPart(ref) {
9285
9536
  const at = ref.lastIndexOf("@");
9286
9537
  return at >= 0 ? ref.slice(at + 1) : ref;
9287
9538
  }
9288
- function firstNonEmpty(...vals) {
9289
- for (const v of vals) if (v) return v;
9290
- return "";
9291
- }
9292
9539
  function putIf(m, key, val) {
9293
9540
  if (val) m[key] = val;
9294
9541
  }
@@ -13441,7 +13688,44 @@ function findDescription(descriptions, label) {
13441
13688
  }
13442
13689
  /** Replace characters not valid in XCCDF IDs with underscores. */
13443
13690
  function sanitizeXccdfId(id) {
13444
- return id.replace(/[^a-zA-Z0-9_.\-]/g, "_");
13691
+ return id.replace(/[^a-zA-Z0-9_.\-]/gu, "_");
13692
+ }
13693
+ /**
13694
+ * Supply the trailing name segment that benchmarkIdType and profileIdType
13695
+ * require via their .+ — a baseline with an empty name is valid HDF but
13696
+ * produced "xccdf_hdf_benchmark_", which the XSD rejects.
13697
+ */
13698
+ function xccdfNamePart(name) {
13699
+ return name === "" ? "unnamed" : name;
13700
+ }
13701
+ /**
13702
+ * Render an HDF tags.gid as a groupIdType: an NCName that must also match
13703
+ * xccdf_[^_]+_group_.+ (xccdf_1.2.xsd:821). HDF constrains gid not at all, and
13704
+ * most real data does not satisfy it — 783 of the 1216 distinct gid values in
13705
+ * this package's converter fixtures are bare STIG ids like "V-257777" — so copying one
13706
+ * through produced XSD-invalid output from valid HDF.
13707
+ *
13708
+ * A gid that already conforms is a genuine XCCDF group id (STIG content carries
13709
+ * "xccdf_mil.disa.stig_group_V-204393") and is passed through; anything else is
13710
+ * sanitized and namespaced under this converter, matching what the benchmark,
13711
+ * profile, and rule ids already do.
13712
+ *
13713
+ * Mirrored by xccdfGroupID in the Go converter. Exported so the shared table
13714
+ * can be asserted against the encoder directly, including the empty gid the
13715
+ * converter itself skips.
13716
+ */
13717
+ function xccdfGroupId(gid) {
13718
+ if (isXccdfGroupId(gid)) return gid;
13719
+ return sanitizeXccdfId("xccdf_hdf_group_" + (gid === "" ? "unnamed" : gid));
13720
+ }
13721
+ /**
13722
+ * Whether a value already satisfies groupIdType. The character test is
13723
+ * ASCII-only, deliberately narrower than NCName: a conforming non-ASCII id is
13724
+ * re-encoded rather than trusted, which errs toward valid output and keeps the
13725
+ * check identical to the Go peer's.
13726
+ */
13727
+ function isXccdfGroupId(id) {
13728
+ return /^xccdf_[^_]+_group_.+$/.test(id) && /^[A-Za-z0-9._-]+$/.test(id);
13445
13729
  }
13446
13730
  /** Read a string-valued tag, or undefined if absent/non-string. */
13447
13731
  function tagString(tags, key) {
@@ -13466,12 +13750,22 @@ function toolTestSystem(tool) {
13466
13750
  const name = tool.name ? cpeField(tool.name) : "tool";
13467
13751
  return `cpe:/a:${name}:${name}:${cpeField(tool.version)}`;
13468
13752
  }
13753
+ /**
13754
+ * The requirement id as text for id construction. HDF makes id required, but
13755
+ * nested-invalid input reaches the converter and concatenating an absent id
13756
+ * wrote the literal string "undefined" into the emitted identifier — where Go,
13757
+ * concatenating a zero-value string, wrote nothing. Both satisfied the XCCDF
13758
+ * pattern, so no schema gate caught the divergence.
13759
+ */
13760
+ function requirementIdText(id) {
13761
+ return typeof id === "string" ? id : "";
13762
+ }
13469
13763
  /** Build the Benchmark XML object from HDF data. */
13470
13764
  function buildBenchmarkObj(hdfData) {
13471
13765
  const empty = !hdfData.baselines || hdfData.baselines.length === 0;
13472
13766
  const benchmark = {
13473
13767
  [`${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),
13768
+ [`${ATTR}id`]: empty ? "xccdf_hdf_benchmark_exported" : sanitizeXccdfId("xccdf_hdf_benchmark_" + xccdfNamePart(hdfData.baselines[0].name)),
13475
13769
  [`${ATTR}resolved`]: "1",
13476
13770
  status: wrap("incomplete")
13477
13771
  };
@@ -13485,7 +13779,7 @@ function buildBenchmarkObj(hdfData) {
13485
13779
  if (baseline.summary) benchmark.description = wrap(baseline.summary);
13486
13780
  benchmark.version = wrap(baseline.version ?? "1.0");
13487
13781
  benchmark.Profile = {
13488
- [`${ATTR}id`]: sanitizeXccdfId("xccdf_hdf_profile_" + baseline.name),
13782
+ [`${ATTR}id`]: sanitizeXccdfId("xccdf_hdf_profile_" + xccdfNamePart(baseline.name)),
13489
13783
  title: wrap(baseline.title ?? baseline.name)
13490
13784
  };
13491
13785
  const groups = [];
@@ -13502,7 +13796,7 @@ function buildBenchmarkObj(hdfData) {
13502
13796
  if (idx === void 0) {
13503
13797
  idx = groups.length;
13504
13798
  groupIndex.set(gid, idx);
13505
- const group = { [`${ATTR}id`]: gid };
13799
+ const group = { [`${ATTR}id`]: xccdfGroupId(gid) };
13506
13800
  const gtitle = tagString(req.tags, "gtitle");
13507
13801
  if (gtitle) group.title = wrap(gtitle);
13508
13802
  group.Rule = [];
@@ -13512,12 +13806,13 @@ function buildBenchmarkObj(hdfData) {
13512
13806
  }
13513
13807
  if (groups.length > 0) benchmark.Group = groups;
13514
13808
  if (flatRules.length > 0) benchmark.Rule = flatRules;
13515
- benchmark.TestResult = buildTestResultObj(hdfData, baseline);
13809
+ const testResult = buildTestResultObj(hdfData, baseline);
13810
+ if (testResult) benchmark.TestResult = testResult;
13516
13811
  return benchmark;
13517
13812
  }
13518
13813
  /** Build an XCCDF Rule object from an HDF EvaluatedRequirement. */
13519
13814
  function buildRuleObj(req) {
13520
- const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
13815
+ const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + requirementIdText(req.id) + "_rule");
13521
13816
  const rule = {
13522
13817
  [`${ATTR}id`]: ruleId,
13523
13818
  [`${ATTR}severity`]: impactToSeverity$2(req.impact),
@@ -13566,27 +13861,74 @@ function buildRuleObj(req) {
13566
13861
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
13567
13862
  "check-content": wrap(checkContent)
13568
13863
  });
13569
- if (req.code) checks.push({
13864
+ const code = firstNonEmpty(req.code ?? "");
13865
+ if (code !== "") checks.push({
13570
13866
  [`${ATTR}system`]: "http://inspec.io/",
13571
- "check-content": wrap(req.code)
13867
+ "check-content": wrap(code)
13572
13868
  });
13573
13869
  if (checks.length > 0) rule.check = checks;
13574
13870
  return rule;
13575
13871
  }
13576
- /** Build the XCCDF TestResult object. */
13872
+ /**
13873
+ * The earliest and latest result time in a baseline.
13874
+ *
13875
+ * XCCDF makes TestResult/@end-time required while HDF's top-level timestamp is
13876
+ * optional, so a document without one still has to carry a scan window. It is
13877
+ * derived from the results rather than the wall clock, which would break both
13878
+ * determinism and the golden comparison. A TestResult is only built when a
13879
+ * baseline exists, and the HDF schema puts minItems 1 on both requirements and
13880
+ * results with startTime required on each, so a window is always available
13881
+ * here; undefined is returned anyway rather than assuming it.
13882
+ *
13883
+ * Times are emitted through formatTimestamp, HDF's canonical form, which is
13884
+ * documented as byte-identical to the RFC3339Nano string the Go converter
13885
+ * emits for the same instant.
13886
+ *
13887
+ * Mirrored by resultTimeWindow in the Go converter.
13888
+ */
13889
+ function resultTimeWindow(baseline) {
13890
+ let first;
13891
+ let last;
13892
+ for (const req of baseline.requirements) for (const res of req.results) {
13893
+ const parsed = parseTimestamp(typeof res.startTime === "string" ? res.startTime : res.startTime.toISOString());
13894
+ if (!parsed) continue;
13895
+ const at = parsed.getTime();
13896
+ const canonical = formatTimestamp(parsed);
13897
+ if (!first || at < first.at) first = {
13898
+ at,
13899
+ text: canonical
13900
+ };
13901
+ if (!last || at > last.at) last = {
13902
+ at,
13903
+ text: canonical
13904
+ };
13905
+ }
13906
+ return first && last ? {
13907
+ first: first.text,
13908
+ last: last.text
13909
+ } : void 0;
13910
+ }
13911
+ /** Build the XCCDF TestResult object, or undefined when it cannot carry a time. */
13577
13912
  function buildTestResultObj(hdfData, baseline) {
13578
13913
  const testResult = {
13579
13914
  [`${ATTR}id`]: "xccdf_hdf_testresult_1",
13580
13915
  title: wrap("HDF Assessment Results")
13581
13916
  };
13582
13917
  if (hdfData.timestamp) {
13583
- const ts = typeof hdfData.timestamp === "string" ? hdfData.timestamp : hdfData.timestamp.toISOString();
13918
+ const raw = typeof hdfData.timestamp === "string" ? hdfData.timestamp : hdfData.timestamp.toISOString();
13919
+ const parsedTs = parseTimestamp(raw);
13920
+ const ts = parsedTs ? formatTimestamp(parsedTs) : raw;
13584
13921
  testResult[`${ATTR}start-time`] = ts;
13585
13922
  let endTime = ts;
13586
13923
  const duration = hdfData.statistics?.duration;
13587
13924
  const start = parseTimestamp(ts);
13588
13925
  if (start && typeof duration === "number" && duration > 0) endTime = formatTimestamp(new Date(start.getTime() + duration * 1e3));
13589
13926
  testResult[`${ATTR}end-time`] = endTime;
13927
+ } else {
13928
+ const window = resultTimeWindow(baseline);
13929
+ if (!window) return;
13930
+ testResult[`${ATTR}start-time`] = window.first;
13931
+ testResult[`${ATTR}end-time`] = window.last;
13590
13932
  }
13591
13933
  const testSystem = toolTestSystem(hdfData.tool);
13592
13934
  if (testSystem) testResult[`${ATTR}test-system`] = testSystem;
@@ -13599,7 +13941,7 @@ function buildTestResultObj(hdfData, baseline) {
13599
13941
  let passed = 0;
13600
13942
  let scorable = 0;
13601
13943
  for (const req of baseline.requirements) {
13602
- const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
13944
+ const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + requirementIdText(req.id) + "_rule");
13603
13945
  const stigId = tagString(req.tags, "stig_id");
13604
13946
  const statusInput = requirementStatusInput(req);
13605
13947
  const resolved = computeEffectiveStatus(statusInput);
@@ -13611,7 +13953,9 @@ function buildTestResultObj(hdfData, baseline) {
13611
13953
  scorable++;
13612
13954
  } else if (status === "fail") scorable++;
13613
13955
  const rr = { [`${ATTR}idref`]: ruleIdRef };
13614
- rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
13956
+ const rawStart = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
13957
+ const parsedStart = parseTimestamp(rawStart);
13958
+ rr[`${ATTR}time`] = parsedStart ? formatTimestamp(parsedStart) : rawStart;
13615
13959
  if (stigId) rr[`${ATTR}version`] = stigId;
13616
13960
  rr.result = wrap(status);
13617
13961
  if (result.message) rr.message = wrap(result.message);
@@ -13866,15 +14210,9 @@ function oscalString(s) {
13866
14210
  * @returns OSCAL SAR JSON string
13867
14211
  */
13868
14212
  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");
14213
+ const { doc: hdfResultsDoc } = requireHdfResults(input, "hdf-to-oscal-sar");
14214
+ const hdfResults = hdfResultsDoc;
14215
+ 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
14216
  const doc = buildOSCALDocument(hdfResults);
13879
14217
  return JSON.stringify(doc, null, 2);
13880
14218
  }
@@ -13976,7 +14314,7 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
13976
14314
  const includeControls = [];
13977
14315
  const seenControl = /* @__PURE__ */ new Set();
13978
14316
  for (const req of baseline.requirements) {
13979
- const nistId = nistTagToControlId(req.id);
14317
+ const nistId = nistTagToControlId(req.id ?? "");
13980
14318
  if (nistId === "") continue;
13981
14319
  const { finding, observation, risk, resource } = requirementToFindingSet(req, timestamp, toolActorUuid, subjects);
13982
14320
  findings.push(finding);
@@ -13998,9 +14336,9 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
13998
14336
  start: assessmentStart(baseline, timestamp),
13999
14337
  ...resultProps.length > 0 ? { props: resultProps } : {},
14000
14338
  "reviewed-controls": { "control-selections": [controlSelection] },
14001
- findings,
14002
- observations,
14003
- risks
14339
+ ...findings.length > 0 ? { findings } : {},
14340
+ ...observations.length > 0 ? { observations } : {},
14341
+ ...risks.length > 0 ? { risks } : {}
14004
14342
  },
14005
14343
  resources
14006
14344
  };
@@ -14010,10 +14348,17 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
14010
14348
  * component's UUID (componentId when present, otherwise a fresh one) identifies
14011
14349
  * the subject; the HDF component type is a valid OSCAL subject type token and
14012
14350
  * its name becomes the subject title.
14351
+ *
14352
+ * A component with no type is skipped rather than given one. OSCAL requires both
14353
+ * subject-uuid and type on a subject-reference, so the type cannot simply be
14354
+ * omitted, and hdf-results defines no default component type to fall back on.
14355
+ * String(c.type) turned an absent type into the literal "undefined" — a valid
14356
+ * OSCAL token, so a schema check could not see it, asserting a component type
14357
+ * the source never stated. Mirrors the Go peer.
14013
14358
  */
14014
14359
  function buildSubjects(components) {
14015
14360
  if (!Array.isArray(components) || components.length === 0) return [];
14016
- return components.map((c) => ({
14361
+ return components.filter((c) => oscalString(c.type ?? "") !== "").map((c) => ({
14017
14362
  "subject-uuid": c.componentId && c.componentId !== "" ? c.componentId : crypto.randomUUID(),
14018
14363
  type: String(c.type),
14019
14364
  title: c.name
@@ -14023,7 +14368,7 @@ function buildSubjects(components) {
14023
14368
  * Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
14024
14369
  */
14025
14370
  function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14026
- const controlID = oscalToken(nistTagToControlId(req.id));
14371
+ const controlID = oscalToken(nistTagToControlId(req.id ?? ""));
14027
14372
  const results = req.results ?? [];
14028
14373
  const descriptions = req.descriptions ?? [];
14029
14374
  const { state, reason } = effectiveState(req);
@@ -14380,17 +14725,27 @@ function riskStatusFromState(state) {
14380
14725
  * @returns OSCAL POA&M JSON string
14381
14726
  */
14382
14727
  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) };
14728
+ const { doc: amendmentsDoc } = requireHdfAmendments(input, "hdf-to-oscal-poam");
14729
+ const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendmentsDoc) };
14392
14730
  return JSON.stringify(doc, null, 2);
14393
14731
  }
14732
+ /**
14733
+ * Picks the document title, which OSCAL requires on metadata. The HDF name is
14734
+ * schema-required, so the fallbacks only matter for a document that slipped
14735
+ * through some other producer's validation.
14736
+ */
14737
+ function poamTitle(a) {
14738
+ return firstNonEmpty(a.name, a.amendmentId ?? "", "HDF Amendments");
14739
+ }
14740
+ /**
14741
+ * Supplies the text OSCAL requires for a risk's description and statement. HDF
14742
+ * puts no minLength on reason, so an override can legitimately carry none; the
14743
+ * fallback states that absence rather than inventing an impact assessment the
14744
+ * source never made.
14745
+ */
14746
+ function riskRationale(override) {
14747
+ return firstNonEmpty(override.reason, `No rationale was recorded for the ${String(override.type)} override applied to ${override.requirementId}.`);
14748
+ }
14394
14749
  /** HDF dates arrive as strings from JSON.parse but are typed as Date. */
14395
14750
  function toDate(value) {
14396
14751
  if (value === void 0) return void 0;
@@ -14408,14 +14763,15 @@ var PartyRegistry = class {
14408
14763
  this.byId = /* @__PURE__ */ new Map();
14409
14764
  }
14410
14765
  getOrAdd(id) {
14411
- const existing = this.byId.get(id.identifier);
14766
+ const name = oscalString(id.identifier);
14767
+ const existing = this.byId.get(name);
14412
14768
  if (existing) return existing.uuid;
14413
14769
  const party = {
14414
14770
  uuid: crypto.randomUUID(),
14415
14771
  type: "person",
14416
- name: id.identifier
14772
+ ...name === "" ? {} : { name }
14417
14773
  };
14418
- this.byId.set(id.identifier, party);
14774
+ this.byId.set(name, party);
14419
14775
  return party.uuid;
14420
14776
  }
14421
14777
  list() {
@@ -14465,7 +14821,7 @@ function amendmentsToPOAM(amendments) {
14465
14821
  observations.push(...itemObs);
14466
14822
  }
14467
14823
  const metadata = {
14468
- title: amendments.name,
14824
+ title: poamTitle(amendments),
14469
14825
  "last-modified": latestAppliedAt(amendments.overrides),
14470
14826
  version: amendmentsVersion(amendments),
14471
14827
  "oscal-version": OSCAL_VERSION
@@ -14483,9 +14839,9 @@ function amendmentsToPOAM(amendments) {
14483
14839
  uuid: crypto.randomUUID(),
14484
14840
  metadata,
14485
14841
  "import-ssp": importSSP,
14486
- risks,
14487
14842
  "poam-items": poamItems
14488
14843
  };
14844
+ if (risks.length > 0) poam.risks = risks;
14489
14845
  if (observations.length > 0) poam.observations = observations;
14490
14846
  const resources = externalRefResources(amendments.overrides);
14491
14847
  if (resources.length > 0) poam["back-matter"] = { resources };
@@ -14498,10 +14854,9 @@ function amendmentsToPOAM(amendments) {
14498
14854
  function overrideToPOAMItem(override, parties) {
14499
14855
  const riskUUID = crypto.randomUUID();
14500
14856
  const riskStatus = override.status ? hdfStatusToOscalRiskStatus(String(override.status)) : "open";
14501
- const riskProps = [{
14502
- name: "impacted-control-id",
14503
- value: nistTagToControlId(override.requirementId)
14504
- }];
14857
+ const controlID = nistTagToControlId(override.requirementId);
14858
+ const riskProps = [];
14859
+ pushStringProp(riskProps, "impacted-control-id", controlID);
14505
14860
  if (override.type) riskProps.push({
14506
14861
  name: "override-type",
14507
14862
  value: String(override.type)
@@ -14510,18 +14865,9 @@ function overrideToPOAMItem(override, parties) {
14510
14865
  name: "impact-override",
14511
14866
  value: String(override.impact.value)
14512
14867
  });
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
- });
14868
+ if (override.justification) pushStringProp(riskProps, "justification", String(override.justification));
14869
+ if (override.baselineRef) pushStringProp(riskProps, "baseline-ref", override.baselineRef);
14870
+ if (override.componentRef) pushStringProp(riskProps, "component-ref", override.componentRef);
14525
14871
  const remediations = [];
14526
14872
  if (override.milestones) for (const ms of override.milestones) {
14527
14873
  const msProps = [];
@@ -14583,11 +14929,12 @@ function overrideToPOAMItem(override, parties) {
14583
14929
  itemObs.push(evidenceObservation(ev, obsUUID, collected));
14584
14930
  relatedObs.push({ "observation-uuid": obsUUID });
14585
14931
  }
14932
+ const rationale = riskRationale(override);
14586
14933
  const risk = {
14587
14934
  uuid: riskUUID,
14588
14935
  title: override.requirementId,
14589
- description: override.reason,
14590
- statement: override.reason,
14936
+ description: rationale,
14937
+ statement: rationale,
14591
14938
  status: riskStatus,
14592
14939
  deadline,
14593
14940
  props: riskProps,
@@ -14622,9 +14969,22 @@ function latestAppliedAt(overrides) {
14622
14969
  }
14623
14970
  return formatTimestampSeconds(latest ?? /* @__PURE__ */ new Date());
14624
14971
  }
14972
+ /**
14973
+ * Add a property whose value OSCAL types as StringDatatype, trimming it and
14974
+ * omitting the property entirely when nothing survives. A prop with an empty
14975
+ * value carries no more than an absent one and is schema-invalid. Mirrors
14976
+ * appendStringProp in the Go peer.
14977
+ */
14978
+ function pushStringProp(props, name, value) {
14979
+ const trimmed = oscalString(value ?? "");
14980
+ if (trimmed !== "") props.push({
14981
+ name,
14982
+ value: trimmed
14983
+ });
14984
+ }
14625
14985
  /** Sources metadata.version from the amendments document, defaulting when omitted. */
14626
14986
  function amendmentsVersion(a) {
14627
- return a.version && a.version !== "" ? a.version : "1.0.0";
14987
+ return oscalString(a.version ?? "") || "1.0.0";
14628
14988
  }
14629
14989
  /**
14630
14990
  * Carries document identifiers and labels that have no first-class OSCAL home.
@@ -14632,17 +14992,33 @@ function amendmentsVersion(a) {
14632
14992
  */
14633
14993
  function metadataProps(a) {
14634
14994
  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
- });
14995
+ if (a.amendmentId) pushStringProp(props, "amendment-id", a.amendmentId);
14996
+ if (a.labels) for (const [key, value] of Object.entries(a.labels).sort(([x], [y]) => x < y ? -1 : x > y ? 1 : 0)) {
14997
+ const prop = labelProp(key, value);
14998
+ if (prop.value !== "") props.push(prop);
14999
+ }
14644
15000
  return props;
14645
15001
  }
15002
+ /**
15003
+ * Render one amendments label as a property. OSCAL types prop/@name as
15004
+ * TokenDatatype while HDF puts no constraint on label keys, so the key is
15005
+ * encoded and the source key kept in remarks when it had to change. Every label
15006
+ * key in this package's converter fixtures is token-shaped today, so this guards a shape real
15007
+ * data has not yet produced — but Kubernetes and OCI label keys are namespaced
15008
+ * with '/', which HDF permits and OSCAL rejects.
15009
+ *
15010
+ * Mirrored by labelProp in the Go converter.
15011
+ */
15012
+ function labelProp(key, value) {
15013
+ const name = oscalToken(key) === "" ? "_" : oscalToken(key);
15014
+ const prop = {
15015
+ name,
15016
+ value: oscalString(value),
15017
+ class: "amendment-label"
15018
+ };
15019
+ if (key !== "" && name !== key) prop.remarks = key;
15020
+ return prop;
15021
+ }
14646
15022
  /** Picks the collection timestamp for evidence observations. */
14647
15023
  function observationCollected(override) {
14648
15024
  return formatTimestampSeconds(toDate(override.appliedAt) ?? /* @__PURE__ */ new Date());
@@ -14656,14 +15032,8 @@ function evidenceObservation(ev, uuid, defaultCollected) {
14656
15032
  if (ev.type === "url") re.href = ev.data;
14657
15033
  else if (ev.data) re.remarks = ev.data;
14658
15034
  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
- });
15035
+ if (ev.mimeType) pushStringProp(props, "mime-type", ev.mimeType);
15036
+ if (ev.capturedBy && ev.capturedBy.identifier) pushStringProp(props, "captured-by", ev.capturedBy.identifier);
14667
15037
  return {
14668
15038
  uuid,
14669
15039
  description: desc,
@@ -15262,8 +15632,10 @@ async function convertOpenVexToHdf(input, converterVersion = "1.0.0") {
15262
15632
  if (override) overrides.push(override);
15263
15633
  }
15264
15634
  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");
15635
+ const name = doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements";
15636
+ await chainOverrides(overrides);
15265
15637
  return {
15266
- name: doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements",
15638
+ name,
15267
15639
  description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
15268
15640
  overrides,
15269
15641
  appliedBy: identityFor$2(doc.author, doc.role),
@@ -15357,6 +15729,7 @@ async function convertCsafVexToHdf(input, converterVersion = "1.0.0") {
15357
15729
  for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
15358
15730
  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
15731
  const publisherName = publisher.name ?? "";
15732
+ await chainOverrides(overrides);
15360
15733
  return {
15361
15734
  name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
15362
15735
  description: `Imported VEX advisory ${tracking.id ?? ""}`,
@@ -15575,9 +15948,12 @@ async function convertCyclonedxVexToHdf(input, converterVersion = "1.0.0") {
15575
15948
  }
15576
15949
  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
15950
  const publisherName = publisher.identifier;
15951
+ const name = publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements";
15952
+ const description = bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX";
15953
+ await chainOverrides(overrides);
15578
15954
  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",
15955
+ name,
15956
+ description,
15581
15957
  overrides,
15582
15958
  appliedBy: publisher,
15583
15959
  generator: {
@@ -15752,8 +16128,10 @@ async function convertSpdxVexToHdf(input, converterVersion = "1.0.0") {
15752
16128
  }
15753
16129
  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
16130
  const appliedBy = documentIdentity(graph, idx);
16131
+ const name = appliedBy.identifier && appliedBy.identifier !== DEFAULT_APPLIED_BY ? `SPDX VEX statements from ${appliedBy.identifier}` : "SPDX VEX statements";
16132
+ await chainOverrides(overrides);
15755
16133
  return {
15756
- name: appliedBy.identifier && appliedBy.identifier !== DEFAULT_APPLIED_BY ? `SPDX VEX statements from ${appliedBy.identifier}` : "SPDX VEX statements",
16134
+ name,
15757
16135
  description: "Imported SPDX 3.0.1 security-profile VEX statements",
15758
16136
  overrides,
15759
16137
  appliedBy,
@@ -15965,7 +16343,7 @@ function isCvssType(type) {
15965
16343
  const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
15966
16344
  const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
15967
16345
  const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
15968
- const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
16346
+ const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
15969
16347
  /** Map an HDF Cvss block to a CSAF score entry (cvss_v2/v3/v4 by version). */
15970
16348
  function buildCsafScore(cvss, products) {
15971
16349
  const inner = {
@@ -16000,8 +16378,8 @@ function convertHdfToCsafVex(input, converterVersion) {
16000
16378
  const doc = buildDocument(amendments, converterVersion);
16001
16379
  doc.vulnerabilities = vulnerabilities;
16002
16380
  doc.product_tree.full_product_names = names.length > 0 ? names : [{
16003
- name: DEFAULT_PRODUCT_ID$2,
16004
- product_id: DEFAULT_PRODUCT_ID$2
16381
+ name: DEFAULT_PRODUCT_ID$1,
16382
+ product_id: DEFAULT_PRODUCT_ID$1
16005
16383
  }];
16006
16384
  return JSON.stringify(doc, null, 2);
16007
16385
  }
@@ -16054,8 +16432,8 @@ function productEntriesFor(o) {
16054
16432
  }));
16055
16433
  }
16056
16434
  return [{
16057
- name: DEFAULT_PRODUCT_ID$2,
16058
- product_id: DEFAULT_PRODUCT_ID$2
16435
+ name: DEFAULT_PRODUCT_ID$1,
16436
+ product_id: DEFAULT_PRODUCT_ID$1
16059
16437
  }];
16060
16438
  }
16061
16439
  /** product_tree entries for the synthesized fixed-version products. */
@@ -16094,18 +16472,36 @@ function productIDsFor$1(o) {
16094
16472
  const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
16095
16473
  if (parts.length > 0) return parts;
16096
16474
  }
16097
- return [DEFAULT_PRODUCT_ID$2];
16475
+ return [DEFAULT_PRODUCT_ID$1];
16098
16476
  }
16099
16477
  function stripProductsLine$1(reason) {
16100
16478
  return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
16101
16479
  }
16102
16480
  /**
16481
+ * Fill CSAF references[].summary, which is required with minLength 1 while the
16482
+ * HDF description backing it is optional. The URL is the documented fallback:
16483
+ * the only text a reference always carries, where a placeholder would assert a
16484
+ * description the source never gave.
16485
+ */
16486
+ function referenceSummary(description, url) {
16487
+ return firstNonEmpty(description, url);
16488
+ }
16489
+ /**
16490
+ * The override reason with the machine-written 'Products:' line removed, or ''
16491
+ * when no prose remains. Both sinks it feeds — threats[].details and
16492
+ * notes[].text — are minLength 1 in CSAF, so an empty result means the element
16493
+ * is omitted, never emitted blank.
16494
+ */
16495
+ function reasonProse(reason) {
16496
+ return firstNonEmpty(stripProductsLine$1(reason ?? ""));
16497
+ }
16498
+ /**
16103
16499
  * Surface the override reason prose as a CSAF description note. Used on the
16104
16500
  * not_affected/fixed paths, which otherwise drop reason (the affected path
16105
16501
  * keeps it as threats[impact]).
16106
16502
  */
16107
16503
  function appendReasonNote(v, reason) {
16108
- const text = stripProductsLine$1(reason ?? "");
16504
+ const text = reasonProse(reason);
16109
16505
  if (!text) return;
16110
16506
  v.notes = v.notes ?? [];
16111
16507
  v.notes.push({
@@ -16182,11 +16578,12 @@ function buildVulnerability(group) {
16182
16578
  emitted = true;
16183
16579
  } else if (canonical === VexStatus.Affected) {
16184
16580
  status.known_affected = (status.known_affected ?? []).concat(pids);
16185
- if (o.reason) {
16581
+ const details = reasonProse(o.reason);
16582
+ if (details) {
16186
16583
  v.threats = v.threats ?? [];
16187
16584
  v.threats.push({
16188
16585
  category: "impact",
16189
- details: stripProductsLine$1(o.reason),
16586
+ details,
16190
16587
  product_ids: pids
16191
16588
  });
16192
16589
  }
@@ -16208,7 +16605,7 @@ function buildVulnerability(group) {
16208
16605
  v.references = v.references ?? [];
16209
16606
  v.references.push({
16210
16607
  category: "external",
16211
- summary: e.description ?? "",
16608
+ summary: referenceSummary(e.description, e.data),
16212
16609
  url: e.data
16213
16610
  });
16214
16611
  }
@@ -16217,7 +16614,7 @@ function buildVulnerability(group) {
16217
16614
  v.references = v.references ?? [];
16218
16615
  v.references.push({
16219
16616
  category: "external",
16220
- summary: r.description ?? "",
16617
+ summary: referenceSummary(r.description, r.href),
16221
16618
  url: r.href
16222
16619
  });
16223
16620
  }
@@ -16239,7 +16636,7 @@ function buildVulnerability(group) {
16239
16636
  return {
16240
16637
  cve: v.cve,
16241
16638
  ...v.notes && { notes: v.notes },
16242
- product_status: status,
16639
+ ...Object.keys(status).length > 0 && { product_status: status },
16243
16640
  ...v.flags && { flags: v.flags },
16244
16641
  ...v.threats && { threats: v.threats },
16245
16642
  ...v.remediations && { remediations: v.remediations },
@@ -16319,7 +16716,6 @@ const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
16319
16716
  const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
16320
16717
  const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
16321
16718
  const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
16322
- const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
16323
16719
  async function convertHdfToOpenVex(input, converterVersion) {
16324
16720
  validateInputSize(input, "hdf-to-openvex");
16325
16721
  const amendments = parseHdf(input);
@@ -16361,19 +16757,12 @@ function overrideToStatement(o, docAuthor) {
16361
16757
  if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
16362
16758
  const notAffected = canonical === VexStatus.NotAffected;
16363
16759
  const justification = notAffected && o.justification ? String(o.justification) : "";
16364
- const reason = stripProductsLine(o.reason ?? "");
16365
- const impact = notAffected ? reason : "";
16760
+ const reason = firstNonEmpty(stripProductsLine(o.reason ?? ""));
16761
+ const impact = notAffected ? firstNonEmpty(reason, justification ? "" : noImpactRationaleRecorded(o)) : "";
16366
16762
  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);
16763
+ if (canonical === VexStatus.Fixed) action = firstNonEmpty(firstMilestoneAction(o), "Fix applied; consumer re-scan confirmed clean.");
16764
+ else if (canonical === VexStatus.Affected) action = firstNonEmpty(firstMilestoneAction(o), reason, noRemediationRecorded(o));
16765
+ const statusNotes = buildStatusNotes(o, reason, reason !== "" && (impact === reason || action === reason), docAuthor);
16377
16766
  return {
16378
16767
  vulnerability: {
16379
16768
  "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`,
@@ -16420,12 +16809,26 @@ function productsFor(o) {
16420
16809
  const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
16421
16810
  if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
16422
16811
  }
16423
- if (ids.length === 0) ids = [DEFAULT_PRODUCT_ID$1];
16812
+ if (ids.length === 0) return void 0;
16424
16813
  return ids.map((id) => ({ "@id": id }));
16425
16814
  }
16815
+ /**
16816
+ * States the absence OpenVEX has no other way to express: action_statement is
16817
+ * required on an affected statement, and a source override may carry neither a
16818
+ * milestone nor a reason to fill it.
16819
+ */
16820
+ function noRemediationRecorded(o) {
16821
+ return `No remediation action was recorded for the ${String(o.type)} override applied to ${o.requirementId}.`;
16822
+ }
16823
+ /**
16824
+ * The not_affected peer of noRemediationRecorded, for an override carrying
16825
+ * neither a justification enum nor a reason.
16826
+ */
16827
+ function noImpactRationaleRecorded(o) {
16828
+ return `No impact rationale was recorded for the ${String(o.type)} override applied to ${o.requirementId}.`;
16829
+ }
16426
16830
  function firstMilestoneAction(o) {
16427
- for (const m of o.milestones ?? []) if (m.description) return m.description;
16428
- return "";
16831
+ return firstNonEmpty(...(o.milestones ?? []).map((m) => m.description));
16429
16832
  }
16430
16833
  function allMilestonesCompleted$1(o) {
16431
16834
  if (!o.milestones || o.milestones.length === 0) return false;
@@ -16914,6 +17317,7 @@ async function convertOscalPoamToHdf(input) {
16914
17317
  for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
16915
17318
  const systemRef = poam["import-ssp"]?.href || void 0;
16916
17319
  const appliedBy = extractAppliedBy(poam.metadata);
17320
+ await chainOverrides(overrides);
16917
17321
  return serializeHdf({
16918
17322
  name: toKebabCase(poam.metadata.title, "oscal-poam"),
16919
17323
  overrides,