@mitre/hdf-converters 3.3.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as detectConverter, t as registerAllFingerprints } from "./register-all-DxZN6IJ4.js";
2
2
  import { flattenOverlays } from "@mitre/hdf-parsers";
3
- import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML, trimUtcFraction } from "@mitre/hdf-utilities";
4
- import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact } from "@mitre/hdf-schema";
3
+ import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, severityToImpact, sha256, stripHtml as stripHTML, trimUtcFraction } from "@mitre/hdf-utilities";
4
+ import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact as severityToImpact$1 } from "@mitre/hdf-schema";
5
5
  import { DEFAULT_COMPONENT_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, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
6
6
  //#region shared/typescript/converterutil.ts
7
7
  /**
@@ -153,6 +153,34 @@ function validateInputSize(input, converterName, maxSize = DEFAULT_MAX_INPUT_SIZ
153
153
  if (input.length > maxSize) throw new Error(`${converterName}: input exceeds maximum allowed size of ${maxSize} characters`);
154
154
  }
155
155
  /**
156
+ * Parse an HDF document. The single ingest point for every HDF-consuming
157
+ * converter: real HDF carries zone-less timestamps (InSpec emits startTime with
158
+ * no offset), so the raw JSON is normalized to canonical trimmed-UTC RFC3339
159
+ * before parsing — otherwise the non-canonical value is laundered into the
160
+ * converter's output (and Go, whose schema types decode date-time into
161
+ * time.Time, rejects the document outright).
162
+ */
163
+ function parseHdf(input) {
164
+ return parseJSON(normalizeHdfTimestamps(input));
165
+ }
166
+ /**
167
+ * Read a timestamp field off a parsed HDF document.
168
+ *
169
+ * The generated schema types these as `Date` (startTime, appliedAt, ...), but
170
+ * JSON.parse does not revive Dates, so at runtime they are strings. Reaching for
171
+ * `new Date(value)` to bridge that gap reads a zone-less timestamp as host-local,
172
+ * where Go reads it as UTC — so the same document converts differently depending
173
+ * on the machine. parseTimestamp reads it as UTC, matching Go.
174
+ *
175
+ * Returns null when the field is absent or unparseable; callers decide the
176
+ * fallback, since a missing time is not the same as the epoch.
177
+ */
178
+ function hdfTime(value) {
179
+ if (value instanceof Date) return value;
180
+ if (value === void 0 || value === null || value === "") return null;
181
+ return parseTimestamp(String(value));
182
+ }
183
+ /**
156
184
  * Normalise a value that may be a single item, an array, undefined, or null
157
185
  * into a guaranteed array.
158
186
  *
@@ -505,6 +533,7 @@ function convertResult(v1Result) {
505
533
  if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
506
534
  v2Result.startTime = legacyStartTime(v1Result.start_time);
507
535
  if (v1Result.message !== void 0) v2Result.message = v1Result.message;
536
+ else if (v1Result.skip_message !== void 0) v2Result.message = v1Result.skip_message;
508
537
  if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
509
538
  if (v1Result.backtrace !== void 0) v2Result.backtrace = v1Result.backtrace;
510
539
  if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
@@ -512,6 +541,59 @@ function convertResult(v1Result) {
512
541
  return v2Result;
513
542
  }
514
543
  /**
544
+ * Convert a v1 ref value (a string or an array of objects) to the v2
545
+ * Reference.ref union value. Returns undefined when there is no content (e.g.
546
+ * an empty array), so callers can drop empty references — matching the Go
547
+ * converter's toRef.
548
+ */
549
+ function toRefValue(raw) {
550
+ if (typeof raw === "string") return raw === "" ? void 0 : raw;
551
+ if (Array.isArray(raw)) {
552
+ const maps = raw.filter((e) => !!e && typeof e === "object" && !Array.isArray(e));
553
+ return maps.length === 0 ? void 0 : maps;
554
+ }
555
+ }
556
+ /**
557
+ * Convert a single v1 refs[] element (a bare string or an object with
558
+ * ref/url/uri) to a v2 Reference object. Returns null when the element carries
559
+ * no usable content. Key order (ref, url, uri) matches the Go Reference struct.
560
+ */
561
+ function convertRef(raw) {
562
+ if (typeof raw === "string") return raw === "" ? null : { ref: raw };
563
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
564
+ const m = raw;
565
+ const out = {};
566
+ let has = false;
567
+ const refVal = toRefValue(m.ref);
568
+ if (refVal !== void 0) {
569
+ out.ref = refVal;
570
+ has = true;
571
+ }
572
+ if (typeof m.url === "string" && m.url !== "") {
573
+ out.url = m.url;
574
+ has = true;
575
+ }
576
+ if (typeof m.uri === "string" && m.uri !== "") {
577
+ out.uri = m.uri;
578
+ has = true;
579
+ }
580
+ return has ? out : null;
581
+ }
582
+ return null;
583
+ }
584
+ /**
585
+ * Map v1 control-level refs to v2 requirement refs, dropping empty/contentless
586
+ * entries. Returns undefined when nothing maps.
587
+ */
588
+ function convertRefs(refs) {
589
+ const out = [];
590
+ for (const raw of refs) {
591
+ const ref = convertRef(raw);
592
+ if (ref) out.push(ref);
593
+ }
594
+ return out.length ? out : void 0;
595
+ }
596
+ /**
515
597
  * Convert v1.0 control to v2.0 requirement.
516
598
  * Transforms field names and structure.
517
599
  */
@@ -524,6 +606,10 @@ function convertControl(v1Control) {
524
606
  if (v1Control.descriptions !== void 0) v2Req.descriptions = v1Control.descriptions;
525
607
  if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
526
608
  if (v1Control.code !== void 0) v2Req.code = v1Control.code;
609
+ if (Array.isArray(v1Control.refs)) {
610
+ const refs = convertRefs(v1Control.refs);
611
+ if (refs) v2Req.refs = refs;
612
+ }
527
613
  if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
528
614
  if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
529
615
  if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
@@ -590,6 +676,26 @@ function convertDependency(v1Dep) {
590
676
  return v2Dep;
591
677
  }
592
678
  /**
679
+ * Map v1 profile `supports` entries (InSpec hyphenated keys) to v2
680
+ * SupportedPlatform objects. Entries that map no recognized key are dropped.
681
+ * Key order (platform, platformFamily, platformName, release) matches the Go
682
+ * SupportedPlatform struct. Returns undefined when nothing maps.
683
+ */
684
+ function convertSupports(supports) {
685
+ const out = [];
686
+ for (const raw of supports) {
687
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
688
+ const s = raw;
689
+ const sp = {};
690
+ if (typeof s.platform === "string" && s.platform !== "") sp.platform = s.platform;
691
+ if (typeof s["platform-family"] === "string" && s["platform-family"] !== "") sp.platformFamily = s["platform-family"];
692
+ if (typeof s["platform-name"] === "string" && s["platform-name"] !== "") sp.platformName = s["platform-name"];
693
+ if (typeof s.release === "string" && s.release !== "") sp.release = s.release;
694
+ if (Object.keys(sp).length > 0) out.push(sp);
695
+ }
696
+ return out.length ? out : void 0;
697
+ }
698
+ /**
593
699
  * Convert v1.0 profile to v2.0 baseline.
594
700
  * Transforms field names and nested structures.
595
701
  */
@@ -602,7 +708,10 @@ function convertProfile(v1Profile) {
602
708
  if (v1Profile.license !== void 0) v2Baseline.license = v1Profile.license;
603
709
  if (v1Profile.copyright !== void 0) v2Baseline.copyright = v1Profile.copyright;
604
710
  if (v1Profile.copyright_email !== void 0) v2Baseline.copyrightEmail = v1Profile.copyright_email;
605
- if (v1Profile.supports?.length) v2Baseline.supports = v1Profile.supports;
711
+ if (v1Profile.supports?.length) {
712
+ const supports = convertSupports(v1Profile.supports);
713
+ if (supports) v2Baseline.supports = supports;
714
+ }
606
715
  if (v1Profile.attributes?.length) v2Baseline.inputs = v1Profile.attributes;
607
716
  if (v1Profile.status !== void 0) v2Baseline.status = v1Profile.status;
608
717
  if (v1Profile.sha256) v2Baseline.integrity = {
@@ -652,7 +761,7 @@ function convertV1ToV2(v1Data) {
652
761
  type: "host",
653
762
  name: v1Data.platform.name
654
763
  };
655
- if (v1Data.platform.target_id) {
764
+ if (v1Data.platform.target_id || v1Data.platform.release !== void 0) {
656
765
  component.osName = v1Data.platform.name;
657
766
  if (v1Data.platform.release !== void 0) component.osVersion = v1Data.platform.release;
658
767
  }
@@ -695,7 +804,7 @@ const IMPACT_MAPPING$9 = {
695
804
  warning: .5,
696
805
  note: .3
697
806
  };
698
- async function convertSarifToHdf(input) {
807
+ async function convertSarifToHdf(input, converterVersion = "1.0.0") {
699
808
  validateInputSize(input, "sarif");
700
809
  const resultsChecksum = await inputChecksum(input);
701
810
  const sarif = parseJSON(input);
@@ -708,12 +817,11 @@ async function convertSarifToHdf(input) {
708
817
  const timestamp = /* @__PURE__ */ new Date();
709
818
  return buildHdfResults({
710
819
  generatorName: "sarif-to-hdf",
711
- converterVersion: "1.0.0",
820
+ converterVersion,
712
821
  toolName: firstDriver?.name,
713
822
  toolVersion: firstDriver?.version,
714
823
  toolFormat: "SARIF",
715
824
  baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
716
- components: [],
717
825
  timestamp
718
826
  });
719
827
  }
@@ -837,9 +945,9 @@ function convertSarifResultToHDFResults(result) {
837
945
  const locationlessResult = {
838
946
  status,
839
947
  codeDesc: "No source location",
840
- startTime: /* @__PURE__ */ new Date(),
841
- backtrace
948
+ startTime: /* @__PURE__ */ new Date()
842
949
  };
950
+ if (backtrace.length > 0) locationlessResult.backtrace = backtrace;
843
951
  if (suppressionJustification) locationlessResult.message = `Suppressed: ${suppressionJustification}`;
844
952
  results.push(locationlessResult);
845
953
  }
@@ -891,28 +999,17 @@ function extractCweFromRule(rule) {
891
999
  if (cweIds.length > 0) return cweIds;
892
1000
  }
893
1001
  if (rule.properties?.tags && Array.isArray(rule.properties.tags)) {
894
- const cweTagPattern = /^CWE-\d+$/;
895
1002
  const cweIds = [];
896
- for (const tag of rule.properties.tags) if (typeof tag === "string" && cweTagPattern.test(tag)) cweIds.push(tag);
1003
+ for (const tag of rule.properties.tags) {
1004
+ if (typeof tag !== "string") continue;
1005
+ cweIds.push(...extractCWEIDs(tag).map((id) => `CWE-${id}`));
1006
+ }
897
1007
  if (cweIds.length > 0) return cweIds;
898
1008
  }
899
1009
  return [];
900
1010
  }
901
1011
  function extractCweIds(text) {
902
- const matches = text.match(/\(([^)]+)\)/g);
903
- if (!matches) return [];
904
- const cweIds = [];
905
- for (const match of matches) {
906
- const content = match.slice(1, -1);
907
- if (content.includes("CWE-")) {
908
- const parts = content.split(/,\s*|!\//);
909
- for (const part of parts) {
910
- const trimmed = part.trim();
911
- if (trimmed.startsWith("CWE-")) cweIds.push(trimmed);
912
- }
913
- }
914
- }
915
- return cweIds;
1012
+ return extractCWEIDs(text).map((id) => `CWE-${id}`);
916
1013
  }
917
1014
  function mapKindToStatus(kind) {
918
1015
  switch (kind) {
@@ -1008,21 +1105,31 @@ function createHDFResult(location, status, backtrace, suppressionMessage) {
1008
1105
  const snippet = location.physicalLocation?.region?.snippet?.text;
1009
1106
  let codeDesc = `URL : ${uri} LINE : ${line} COLUMN : ${column}`;
1010
1107
  if (snippet) codeDesc = `${codeDesc}\n${snippet}`;
1011
- return createResult(status, suppressionMessage ? `Suppressed: ${suppressionMessage}` : "", {
1108
+ const result = createResult(status, void 0, {
1012
1109
  codeDesc,
1013
- startTime: /* @__PURE__ */ new Date(),
1014
- backtrace
1110
+ startTime: /* @__PURE__ */ new Date()
1015
1111
  });
1112
+ delete result.message;
1113
+ if (suppressionMessage) result.message = `Suppressed: ${suppressionMessage}`;
1114
+ if (backtrace.length > 0) result.backtrace = backtrace;
1115
+ return result;
1016
1116
  }
1017
1117
  //#endregion
1018
1118
  //#region converters/junit-to-hdf/typescript/converter.ts
1019
1119
  const DEFAULT_NIST = ["SA-11"];
1020
- const CONVERTER_VERSION$2 = "1.0.0";
1120
+ /**
1121
+ * The shared XML parser runs with `processEntities: false` (XXE defense), so
1122
+ * character references reach us undecoded. Surefire escapes quotes and angle
1123
+ * brackets in failure messages, so decode them here.
1124
+ */
1125
+ function decodeXmlEntities$5(s) {
1126
+ return s.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10))).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
1127
+ }
1021
1128
  const ARRAY_TAGS$2 = ["testsuite", "testcase"];
1022
1129
  /**
1023
1130
  * Converts JUnit XML test results to HDF format.
1024
1131
  */
1025
- async function convertJunitToHdf(input) {
1132
+ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
1026
1133
  if (!input || !input.trim()) throw new Error("Empty input");
1027
1134
  validateInputSize(input, "junit");
1028
1135
  const { suites, name } = parseJUnitXML(input);
@@ -1031,7 +1138,7 @@ async function convertJunitToHdf(input) {
1031
1138
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
1032
1139
  return buildHdfResults({
1033
1140
  generatorName: "junit-to-hdf",
1034
- converterVersion: CONVERTER_VERSION$2,
1141
+ converterVersion,
1035
1142
  toolName: "JUnit XML",
1036
1143
  toolFormat: "XML",
1037
1144
  baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
@@ -1053,14 +1160,15 @@ function parseJUnitXML(input) {
1053
1160
  const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
1054
1161
  if (parsed.testsuites) return {
1055
1162
  suites: parsed.testsuites.testsuite ?? [],
1056
- name: parsed.testsuites.name || "JUnit Test Results"
1163
+ name: parsed.testsuites.name ? decodeXmlEntities$5(parsed.testsuites.name) : "JUnit Test Results"
1057
1164
  };
1058
1165
  if (parsed.testsuite) {
1059
1166
  const suite = parsed.testsuite;
1060
1167
  const suites = Array.isArray(suite) ? suite : [suite];
1168
+ const suiteName = suites[0]?.name;
1061
1169
  return {
1062
1170
  suites,
1063
- name: suites[0]?.name || "JUnit Test Results"
1171
+ name: suiteName ? decodeXmlEntities$5(suiteName) : "JUnit Test Results"
1064
1172
  };
1065
1173
  }
1066
1174
  throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
@@ -1082,28 +1190,29 @@ function buildRequirements(suites, scanTime) {
1082
1190
  function testCaseToRequirement(tc, scanTime) {
1083
1191
  const id = buildID(tc);
1084
1192
  const { status, message } = resolveStatus(tc);
1085
- const resultOptions = {
1193
+ const result = {
1194
+ status,
1086
1195
  codeDesc: buildCodeDesc$9(tc),
1087
1196
  startTime: scanTime
1088
1197
  };
1198
+ if (message !== void 0) result.message = message;
1089
1199
  if (tc.time) {
1090
1200
  const parsed = parseFloat(tc.time);
1091
- if (!isNaN(parsed)) resultOptions.runTime = parsed;
1201
+ if (!isNaN(parsed)) result.runTime = parsed;
1092
1202
  }
1093
- const result = createResult(status, message ?? "", resultOptions);
1094
- const descriptions = [{
1203
+ const name = decodeXmlEntities$5(tc.name);
1204
+ const req = createRequirement(id, name, [{
1095
1205
  label: "default",
1096
- data: `JUnit test: ${tc.name} in ${tc.classname || "unknown"}`
1097
- }];
1098
- const req = createRequirement(id, tc.name, descriptions, .5, [result], { tags: { nist: DEFAULT_NIST } });
1206
+ data: `JUnit test: ${name} in ${tc.classname ? decodeXmlEntities$5(tc.classname) : "unknown"}`
1207
+ }], .5, [result], { tags: { nist: DEFAULT_NIST } });
1099
1208
  const controlType = deriveControlTypeFromTags(DEFAULT_NIST);
1100
1209
  if (controlType !== void 0) req.controlType = controlType;
1101
1210
  req.verificationMethod = VerificationMethodEnum.Automated;
1102
1211
  return req;
1103
1212
  }
1104
1213
  function buildID(tc) {
1105
- if (tc.classname) return `${tc.classname}.${tc.name}`;
1106
- return tc.name;
1214
+ if (tc.classname) return `${decodeXmlEntities$5(tc.classname)}.${decodeXmlEntities$5(tc.name)}`;
1215
+ return decodeXmlEntities$5(tc.name);
1107
1216
  }
1108
1217
  function resolveStatus(tc) {
1109
1218
  if (tc.failure) {
@@ -1124,7 +1233,7 @@ function resolveStatus(tc) {
1124
1233
  const skipped = typeof tc.skipped === "object" ? tc.skipped : null;
1125
1234
  if (skipped?.message) return {
1126
1235
  status: ResultStatus.NotReviewed,
1127
- message: `Skipped: ${skipped.message}`
1236
+ message: `Skipped: ${decodeXmlEntities$5(skipped.message)}`
1128
1237
  };
1129
1238
  return {
1130
1239
  status: ResultStatus.NotReviewed,
@@ -1135,24 +1244,40 @@ function resolveStatus(tc) {
1135
1244
  }
1136
1245
  function buildFailureMessage(message, typeName, body) {
1137
1246
  let result = "";
1138
- if (typeName) result = `${typeName}: `;
1139
- result += message;
1140
- if (body) result += "\n" + body;
1247
+ if (typeName) result = `${decodeXmlEntities$5(typeName)}: `;
1248
+ result += decodeXmlEntities$5(message);
1249
+ if (body) result += "\n" + decodeXmlEntities$5(body);
1141
1250
  return result;
1142
1251
  }
1143
1252
  function buildCodeDesc$9(tc) {
1144
- if (tc.classname) return `${tc.classname} :: ${tc.name}`;
1145
- return tc.name;
1253
+ if (tc.classname) return `${decodeXmlEntities$5(tc.classname)} :: ${decodeXmlEntities$5(tc.name)}`;
1254
+ return decodeXmlEntities$5(tc.name);
1146
1255
  }
1147
1256
  function noFindingsTarget(baselineName, suites) {
1148
1257
  if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
1149
- for (const s of suites) if (s.name) return s.name;
1258
+ for (const s of suites) if (s.name) return decodeXmlEntities$5(s.name);
1150
1259
  return "JUnit test suite";
1151
1260
  }
1152
1261
  //#endregion
1153
1262
  //#region converters/xccdf-results-to-hdf/typescript/converter.ts
1154
- const CONVERTER_VERSION$1 = "1.0.0";
1155
- const CCI_SYSTEM = "http://cyber.mil/cci";
1263
+ /**
1264
+ * Walk the Group tree depth-first, collecting every rule at any depth. Rules may
1265
+ * sit directly on a Group that also has nested Groups.
1266
+ */
1267
+ function flattenGroups(groups) {
1268
+ const out = [];
1269
+ for (const group of groups) {
1270
+ for (const rule of group.Rule ?? []) {
1271
+ if (!rule.id) continue;
1272
+ out.push({
1273
+ rule,
1274
+ group
1275
+ });
1276
+ }
1277
+ out.push(...flattenGroups(group.Group ?? []));
1278
+ }
1279
+ return out;
1280
+ }
1156
1281
  /** Tags that must always be parsed as arrays even if only one element exists. */
1157
1282
  const ARRAY_TAGS$1 = [
1158
1283
  "Group",
@@ -1201,18 +1326,18 @@ function parseStartTime(raw) {
1201
1326
  }
1202
1327
  return /* @__PURE__ */ new Date();
1203
1328
  }
1204
- async function convertXccdfResultsToHdf(input) {
1329
+ async function convertXccdfResultsToHdf(input, converterVersion = "1.0.0") {
1205
1330
  if (!input || !input.trim()) throw new Error("Empty input");
1206
1331
  validateInputSize(input, "xccdf-results");
1207
- const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1);
1332
+ const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1, { trimValues: false });
1208
1333
  const arfParsed = parsed;
1209
- if (arfParsed["asset-report-collection"]) return convertArfCollection(arfParsed["asset-report-collection"], input);
1334
+ if (arfParsed["asset-report-collection"]) return convertArfCollection(arfParsed["asset-report-collection"], input, converterVersion);
1210
1335
  const benchmark = parsed.Benchmark;
1211
1336
  if (!benchmark) throw new Error("Input is not an XCCDF document: expected <Benchmark> root element");
1212
1337
  if (!benchmark.TestResult) throw new Error("Input has no TestResult elements — this is a benchmark. Use 'xccdf-benchmark' or 'xccdf' instead");
1213
- return convertBenchmarkResultsToHdf(benchmark, input);
1338
+ return convertBenchmarkResultsToHdf(benchmark, input, converterVersion);
1214
1339
  }
1215
- async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1340
+ async function convertBenchmarkResultsToHdf(benchmark, rawInput, converterVersion) {
1216
1341
  const testResult = benchmark.TestResult;
1217
1342
  const ruleIndex = buildRuleIndex(benchmark);
1218
1343
  const ruleResults = testResult["rule-result"] ?? [];
@@ -1226,32 +1351,35 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1226
1351
  requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1227
1352
  }
1228
1353
  const resultsChecksum = await inputChecksum(rawInput);
1229
- const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
1230
- const components = buildTargets(testResult);
1231
- const timestamp = scanTime;
1232
- let durationSeconds;
1233
- if (testResult["start-time"] && testResult["end-time"]) {
1234
- const start = parseTimestamp(testResult["start-time"])?.getTime();
1235
- const end = parseTimestamp(testResult["end-time"])?.getTime();
1236
- if (start !== void 0 && end !== void 0 && end >= start) durationSeconds = (end - start) / 1e3;
1237
- }
1238
- const hdf = {
1354
+ const baselineName = extractText(benchmark.title) || benchmark.id || "";
1355
+ const baseline = createMinimalBaseline(baselineName, requirements, { resultsChecksum });
1356
+ baseline.title = baselineName;
1357
+ baseline.version = extractVersion$1(benchmark.version);
1358
+ baseline.status = "loaded";
1359
+ baseline.summary = stripHTML(extractText(benchmark.description));
1360
+ return serializeHdf({
1239
1361
  baselines: [baseline],
1240
1362
  generator: {
1241
1363
  name: "xccdf-results-to-hdf",
1242
- version: CONVERTER_VERSION$1
1364
+ version: converterVersion
1243
1365
  },
1244
1366
  tool: {
1245
- name: "XCCDF Results",
1246
- format: "XML"
1367
+ name: "XCCDF",
1368
+ format: "XCCDF"
1247
1369
  },
1248
- components,
1249
- timestamp
1250
- };
1251
- if (durationSeconds !== void 0) hdf.statistics = { duration: durationSeconds };
1252
- return serializeHdf(hdf);
1370
+ components: buildTargets(testResult),
1371
+ timestamp: scanTime,
1372
+ statistics: { duration: calculateDuration(testResult) }
1373
+ });
1253
1374
  }
1254
- async function convertArfCollection(arc, rawInput) {
1375
+ /** Seconds between the TestResult start-time and end-time; 0 when unavailable. */
1376
+ function calculateDuration(testResult) {
1377
+ const start = testResult["start-time"] ? parseTimestamp(testResult["start-time"])?.getTime() : void 0;
1378
+ const end = testResult["end-time"] ? parseTimestamp(testResult["end-time"])?.getTime() : void 0;
1379
+ if (start === void 0 || end === void 0 || end < start) return 0;
1380
+ return (end - start) / 1e3;
1381
+ }
1382
+ async function convertArfCollection(arc, rawInput, converterVersion) {
1255
1383
  const resultsChecksum = await inputChecksum(rawInput);
1256
1384
  const benchmark = findBenchmarkInArf(arc);
1257
1385
  const ruleIndex = benchmark ? buildRuleIndex(benchmark) : /* @__PURE__ */ new Map();
@@ -1266,16 +1394,9 @@ async function convertArfCollection(arc, rawInput) {
1266
1394
  for (const report of arc.reports?.report ?? []) {
1267
1395
  const testResult = report.content?.TestResult;
1268
1396
  if (!testResult?.id) continue;
1269
- if (testResult["start-time"] && !firstTimestamp) {
1270
- const t = parseTimestamp(testResult["start-time"]);
1271
- if (t) firstTimestamp = t;
1272
- }
1273
- if (testResult["start-time"] && testResult["end-time"]) {
1274
- const start = parseTimestamp(testResult["start-time"])?.getTime();
1275
- const end = parseTimestamp(testResult["end-time"])?.getTime();
1276
- if (start !== void 0 && end !== void 0 && end >= start) totalDuration += (end - start) / 1e3;
1277
- }
1278
1397
  const scanTime = parseStartTime(testResult["start-time"]);
1398
+ if (!firstTimestamp) firstTimestamp = scanTime;
1399
+ totalDuration += calculateDuration(testResult);
1279
1400
  const ruleResults = testResult["rule-result"] ?? [];
1280
1401
  const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
1281
1402
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -1286,9 +1407,15 @@ async function convertArfCollection(arc, rawInput) {
1286
1407
  requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1287
1408
  }
1288
1409
  let baselineName = "";
1289
- if (benchmark) baselineName = extractText(benchmark.title) || "";
1290
- if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "ARF Report";
1410
+ if (benchmark) baselineName = extractText(benchmark.title) || benchmark.id || "";
1411
+ if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "";
1291
1412
  const baseline = createMinimalBaseline(baselineName, requirements, { resultsChecksum });
1413
+ baseline.title = baselineName;
1414
+ baseline.status = "loaded";
1415
+ if (benchmark) {
1416
+ baseline.version = extractVersion$1(benchmark.version);
1417
+ baseline.summary = stripHTML(extractText(benchmark.description));
1418
+ }
1292
1419
  baselines.push(baseline);
1293
1420
  const reportTargets = buildTargets(testResult);
1294
1421
  const target = reportTargets[0];
@@ -1302,21 +1429,20 @@ async function convertArfCollection(arc, rawInput) {
1302
1429
  components.push(...reportTargets);
1303
1430
  }
1304
1431
  if (baselines.length === 0) throw new Error("ARF document contains no XCCDF TestResult reports");
1305
- const hdf = {
1432
+ return serializeHdf({
1306
1433
  baselines,
1307
1434
  generator: {
1308
1435
  name: "xccdf-results-to-hdf",
1309
- version: CONVERTER_VERSION$1
1436
+ version: converterVersion
1310
1437
  },
1311
1438
  tool: {
1312
1439
  name: "ARF",
1313
1440
  format: "ARF"
1314
1441
  },
1315
1442
  components,
1316
- timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
1317
- };
1318
- if (totalDuration > 0) hdf.statistics = { duration: totalDuration };
1319
- return serializeHdf(hdf);
1443
+ timestamp: firstTimestamp ?? /* @__PURE__ */ new Date(),
1444
+ statistics: { duration: totalDuration }
1445
+ });
1320
1446
  }
1321
1447
  /**
1322
1448
  * Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
@@ -1362,11 +1488,7 @@ function buildRuleIndex(benchmark) {
1362
1488
  const index = /* @__PURE__ */ new Map();
1363
1489
  const topRules = benchmark.Rule ?? [];
1364
1490
  for (const rule of topRules) if (rule.id) index.set(rule.id, rule);
1365
- const groups = benchmark.Group ?? [];
1366
- for (const group of groups) {
1367
- const rules = group.Rule ?? [];
1368
- for (const rule of rules) if (rule.id) index.set(rule.id, rule);
1369
- }
1491
+ for (const { rule } of flattenGroups(benchmark.Group ?? [])) index.set(rule.id, rule);
1370
1492
  return index;
1371
1493
  }
1372
1494
  /**
@@ -1375,39 +1497,29 @@ function buildRuleIndex(benchmark) {
1375
1497
  function ruleResultToRequirement(rr, ruleIndex, scanTime) {
1376
1498
  const ruleId = rr.idref ?? "";
1377
1499
  const ruleDef = ruleIndex.get(ruleId);
1378
- const id = extractVersion$1(ruleDef?.version) || ruleId;
1379
- const title = extractText(ruleDef?.title) || id;
1380
- const severity = rr.severity ?? ruleDef?.severity ?? "";
1381
- const impact = severity ? severityToImpact(severity) : .5;
1382
- const descriptions = [];
1383
- const rawDesc = extractText(ruleDef?.description);
1384
- if (rawDesc) descriptions.push({
1500
+ const id = ruleDef?.id ? extractRuleID(ruleDef.id) : ruleId;
1501
+ const title = extractText(ruleDef?.title) || ruleId;
1502
+ const severity = (rr.severity || ruleDef?.severity || "").toLowerCase();
1503
+ const impact = severity ? severityToImpact$1(severity) : .5;
1504
+ const descriptions = [{
1385
1505
  label: "default",
1386
- data: extractVulnDiscussion(rawDesc)
1387
- });
1506
+ data: stripHTML(extractVulnDiscussion(extractText(ruleDef?.description)))
1507
+ }];
1388
1508
  const fixtext = extractFixtext(ruleDef?.fixtext);
1389
1509
  if (fixtext) descriptions.push({
1390
1510
  label: "fix",
1391
- data: fixtext
1511
+ data: stripHTML(fixtext)
1392
1512
  });
1393
- if (descriptions.length === 0) descriptions.push({
1394
- label: "default",
1395
- data: ""
1396
- });
1397
- const status = STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed;
1513
+ const xccdfResult = (rr.result ?? "").trim().toLowerCase();
1514
+ const status = STATUS_MAP[xccdfResult] ?? ResultStatus.Error;
1398
1515
  const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
1399
- const result = createResult(status, "", {
1400
- codeDesc: `XCCDF rule ${id}`,
1516
+ const result = {
1517
+ status,
1518
+ codeDesc: `XCCDF rule ${ruleId}`,
1401
1519
  startTime: perRuleTime ?? scanTime
1402
- });
1403
- const tags = {};
1404
- let nistTags = [];
1405
- const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
1406
- if (cciIds.length > 0) {
1407
- tags["cci"] = cciIds;
1408
- nistTags = [...new Set(cciIds.flatMap((cci) => getCCINistMappings(cci) ?? []))];
1409
- if (nistTags.length > 0) tags["nist"] = nistTags;
1410
- }
1520
+ };
1521
+ const tags = buildCciNistTags(extractCCIs([...rr.ident ?? [], ...ruleDef?.ident ?? []]));
1522
+ const nistTags = tags["nist"];
1411
1523
  const req = createRequirement(id, title, descriptions, impact, [result], { tags });
1412
1524
  const controlType = deriveControlTypeFromTags(nistTags);
1413
1525
  if (controlType !== void 0) req.controlType = controlType;
@@ -1438,19 +1550,29 @@ function buildTargets(testResult) {
1438
1550
  const addresses = testResult["target-address"] ?? [];
1439
1551
  const target = {
1440
1552
  name: targetName,
1441
- type: TargetType.Host,
1442
- labels: {}
1553
+ type: TargetType.Host
1443
1554
  };
1444
1555
  if (addresses.length > 0) target.ipAddress = addresses[0];
1445
1556
  return [target];
1446
1557
  }
1447
- /**
1448
- * Extract plain text from a field that may be a string or {#text: string}.
1449
- */
1558
+ const NAMED_ENTITIES$4 = {
1559
+ lt: "<",
1560
+ gt: ">",
1561
+ quot: "\"",
1562
+ apos: "'",
1563
+ amp: "&"
1564
+ };
1565
+ function decodeXmlEntities$4(s) {
1566
+ return s.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|(lt|gt|quot|apos|amp));/g, (match, dec, hex, name) => {
1567
+ if (dec !== void 0) return String.fromCodePoint(Number.parseInt(dec, 10));
1568
+ if (hex !== void 0) return String.fromCodePoint(Number.parseInt(hex, 16));
1569
+ return name !== void 0 ? NAMED_ENTITIES$4[name] ?? match : match;
1570
+ });
1571
+ }
1450
1572
  function extractText(field) {
1451
1573
  if (field === void 0 || field === null) return "";
1452
- if (typeof field === "string") return field;
1453
- return field["#text"] ?? "";
1574
+ if (typeof field === "string") return decodeXmlEntities$4(field);
1575
+ return decodeXmlEntities$4(field["#text"] ?? "");
1454
1576
  }
1455
1577
  /**
1456
1578
  * Extract version string from a version field.
@@ -1465,8 +1587,8 @@ function extractVersion$1(field) {
1465
1587
  */
1466
1588
  function extractFixtext(field) {
1467
1589
  if (field === void 0 || field === null) return "";
1468
- if (typeof field === "string") return field;
1469
- return field["#text"] ?? "";
1590
+ if (typeof field === "string") return decodeXmlEntities$4(field);
1591
+ return decodeXmlEntities$4(field["#text"] ?? "");
1470
1592
  }
1471
1593
  /**
1472
1594
  * Extract the VulnDiscussion text from an XCCDF description that contains
@@ -1475,16 +1597,38 @@ function extractFixtext(field) {
1475
1597
  */
1476
1598
  function extractVulnDiscussion(description) {
1477
1599
  const match = description.match(/<VulnDiscussion>([\s\S]*?)<\/VulnDiscussion>/);
1478
- if (match) return match[1].trim();
1479
- const entityMatch = description.match(/&lt;VulnDiscussion&gt;([\s\S]*?)&lt;\/VulnDiscussion&gt;/);
1480
- if (entityMatch) return entityMatch[1].trim();
1481
- return description;
1600
+ return match ? match[1] : description;
1482
1601
  }
1483
1602
  /**
1484
- * Extract CCI identifiers from ident elements (system="http://cyber.mil/cci").
1603
+ * Extract CCI identifiers from ident elements, deduplicated in first-seen order.
1485
1604
  */
1486
1605
  function extractCCIs(idents) {
1487
- return idents.filter((i) => i.system === CCI_SYSTEM).map((i) => i["#text"] ?? "").filter((v) => v.length > 0);
1606
+ const ccis = idents.filter((i) => (i.system ?? "").toLowerCase().includes("cci")).map((i) => i["#text"] ?? "").filter((v) => v.length > 0);
1607
+ return [...new Set(ccis)];
1608
+ }
1609
+ /**
1610
+ * Build the cci/nist tag pair. `nist` is always present (empty when there are no
1611
+ * CCIs) and sorted, matching Go's cci.CCIToNIST.
1612
+ */
1613
+ function buildCciNistTags(cciIds) {
1614
+ if (cciIds.length === 0) return { nist: [] };
1615
+ return {
1616
+ cci: cciIds,
1617
+ nist: [...new Set(cciIds.flatMap((c) => getCCINistMappings(c) ?? []))].sort()
1618
+ };
1619
+ }
1620
+ /**
1621
+ * Extract the vulnerability ID from an XCCDF Rule ID:
1622
+ * "SV-254238r991589_rule" and "xccdf_mil.disa.stig_rule_SV-204393r603261_rule"
1623
+ * both yield "SV-254238"/"SV-204393". Non-SV IDs pass through unchanged.
1624
+ */
1625
+ function extractRuleID(ruleID) {
1626
+ const svIdx = ruleID.toUpperCase().indexOf("SV-");
1627
+ if (svIdx < 0) return ruleID;
1628
+ const digits = ruleID.slice(svIdx + 3);
1629
+ const revIdx = digits.indexOf("r");
1630
+ if (revIdx > 0) return `SV-${digits.slice(0, revIdx)}`;
1631
+ return `SV-${digits}`;
1488
1632
  }
1489
1633
  //#endregion
1490
1634
  //#region shared/typescript/checklist/status.ts
@@ -1672,6 +1816,8 @@ function cklVulnToModel(v) {
1672
1816
  status: parseStatus(v.STATUS),
1673
1817
  findingDetails: str(v.FINDING_DETAILS),
1674
1818
  comments: str(v.COMMENTS),
1819
+ severityOverride: str(v.SEVERITY_OVERRIDE),
1820
+ severityJustification: str(v.SEVERITY_JUSTIFICATION),
1675
1821
  extra
1676
1822
  };
1677
1823
  }
@@ -1760,7 +1906,9 @@ function modelVulnToCkl(v) {
1760
1906
  STIG_DATA: stigData,
1761
1907
  STATUS: statusToCkl(v.status),
1762
1908
  FINDING_DETAILS: v.findingDetails ?? "",
1763
- COMMENTS: v.comments ?? ""
1909
+ COMMENTS: v.comments ?? "",
1910
+ SEVERITY_OVERRIDE: v.severityOverride ?? "",
1911
+ SEVERITY_JUSTIFICATION: v.severityJustification ?? ""
1764
1912
  };
1765
1913
  }
1766
1914
  //#endregion
@@ -1929,7 +2077,7 @@ function stigToBaseline(s, resultsChecksum, scanTime) {
1929
2077
  }
1930
2078
  function vulnToRequirement(v, scanTime) {
1931
2079
  const severity = (v.severity ?? "").toLowerCase();
1932
- const impact = severity ? severityToImpact(severity) : .5;
2080
+ const impact = severity ? severityToImpact$1(severity) : .5;
1933
2081
  const descriptions = [{
1934
2082
  label: "default",
1935
2083
  data: stripHTML(v.vulnDiscuss ?? "")
@@ -1942,7 +2090,7 @@ function vulnToRequirement(v, scanTime) {
1942
2090
  label: "fix",
1943
2091
  data: stripHTML(v.fixText)
1944
2092
  });
1945
- const message = [v.findingDetails, v.comments].map((s) => (s ?? "").trim()).filter(Boolean).join("\n\n");
2093
+ const message = (v.findingDetails ?? "").trim();
1946
2094
  const result = createResult(statusToHdf(v.status), message, {
1947
2095
  codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
1948
2096
  startTime: scanTime
@@ -1954,12 +2102,13 @@ function vulnToRequirement(v, scanTime) {
1954
2102
  nistTags = [...new Set(v.ccis.flatMap((c) => getCCINistMappings(c) ?? []))].sort();
1955
2103
  tags["nist"] = nistTags;
1956
2104
  } else tags["nist"] = [];
1957
- setIf(tags, "rid", v.ruleID);
1958
- setIf(tags, "stig_id", v.ruleVer);
1959
- setIf(tags, "gtitle", v.groupTitle);
1960
- setIf(tags, "group_id", v.groupID);
1961
- setIf(tags, "weight", v.weight);
1962
- setIf(tags, "severity", severity);
2105
+ setIf$1(tags, "rid", v.ruleID);
2106
+ setIf$1(tags, "stig_id", v.ruleVer);
2107
+ setIf$1(tags, "gtitle", v.groupTitle);
2108
+ setIf$1(tags, "group_id", v.groupID);
2109
+ setIf$1(tags, "weight", v.weight);
2110
+ setIf$1(tags, "comments", v.comments);
2111
+ setIf$1(tags, "severity", severity);
1963
2112
  if (v.legacyIDs && v.legacyIDs.length) tags["legacy_ids"] = v.legacyIDs;
1964
2113
  if (v.extra && Object.keys(v.extra).length > 0) tags["cklMetadata"] = { ...v.extra };
1965
2114
  const req = createRequirement(v.vulnNum, v.ruleTitle ?? v.vulnNum, descriptions, impact, [result], { tags });
@@ -1974,6 +2123,7 @@ function assetToComponent(a) {
1974
2123
  name: a.hostName || a.hostFQDN || a.hostIP || "",
1975
2124
  type: TargetType.Host
1976
2125
  };
2126
+ if (a.hostName) c.hostname = a.hostName;
1977
2127
  if (a.hostIP) c.ipAddress = a.hostIP;
1978
2128
  if (a.hostFQDN) c.fqdn = a.hostFQDN;
1979
2129
  if (a.hostMAC) c.macAddress = a.hostMAC;
@@ -1983,30 +2133,30 @@ function rootExtensions(cl) {
1983
2133
  const ext = { checklistFormat: cl.format || "ckl" };
1984
2134
  if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
1985
2135
  const ax = {};
1986
- setIf(ax, "role", cl.asset.role);
1987
- setIf(ax, "assetType", cl.asset.assetType);
1988
- setIf(ax, "marking", cl.asset.marking);
1989
- setIf(ax, "targetKey", cl.asset.targetKey);
1990
- setIf(ax, "techArea", cl.asset.techArea);
1991
- setIf(ax, "targetComment", cl.asset.targetComment);
1992
- setIf(ax, "webDbSite", cl.asset.webDBSite);
1993
- setIf(ax, "webDbInstance", cl.asset.webDBInstance);
1994
- setIf(ax, "classification", cl.asset.classification);
2136
+ setIf$1(ax, "role", cl.asset.role);
2137
+ setIf$1(ax, "assetType", cl.asset.assetType);
2138
+ setIf$1(ax, "marking", cl.asset.marking);
2139
+ setIf$1(ax, "targetKey", cl.asset.targetKey);
2140
+ setIf$1(ax, "techArea", cl.asset.techArea);
2141
+ setIf$1(ax, "targetComment", cl.asset.targetComment);
2142
+ setIf$1(ax, "webDbSite", cl.asset.webDBSite);
2143
+ setIf$1(ax, "webDbInstance", cl.asset.webDBInstance);
2144
+ setIf$1(ax, "classification", cl.asset.classification);
1995
2145
  if (cl.asset.webOrDatabase) ax["webOrDatabase"] = true;
1996
2146
  if (Object.keys(ax).length > 0) ext["assetExtras"] = ax;
1997
2147
  return ext;
1998
2148
  }
1999
2149
  function baselineExtensions(s) {
2000
2150
  const ext = {};
2001
- setIf(ext, "stigid", s.stigID);
2002
- setIf(ext, "uuid", s.uuid);
2003
- setIf(ext, "releaseInfo", s.releaseInfo);
2004
- setIf(ext, "displayName", s.displayName);
2005
- setIf(ext, "referenceIdentifier", s.referenceIdentifier);
2006
- setIf(ext, "classification", s.classification);
2151
+ setIf$1(ext, "stigid", s.stigID);
2152
+ setIf$1(ext, "uuid", s.uuid);
2153
+ setIf$1(ext, "releaseInfo", s.releaseInfo);
2154
+ setIf$1(ext, "displayName", s.displayName);
2155
+ setIf$1(ext, "referenceIdentifier", s.referenceIdentifier);
2156
+ setIf$1(ext, "classification", s.classification);
2007
2157
  return ext;
2008
2158
  }
2009
- function setIf(m, key, val) {
2159
+ function setIf$1(m, key, val) {
2010
2160
  if (val) m[key] = val;
2011
2161
  }
2012
2162
  //#endregion
@@ -2018,7 +2168,7 @@ function setIf(m, key, val) {
2018
2168
  * fields are synthesized best-effort so any HDF yields a valid checklist.
2019
2169
  */
2020
2170
  function hdfToChecklist(input) {
2021
- const hdf = parseJSON(input);
2171
+ const hdf = parseHdf(input);
2022
2172
  if (!hdf || !Array.isArray(hdf.baselines) || hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
2023
2173
  const ext = hdf.extensions ?? {};
2024
2174
  const format = strVal(ext, "checklistFormat") || "ckl";
@@ -2036,7 +2186,8 @@ function buildAsset(hdf, ext) {
2036
2186
  const asset = {};
2037
2187
  const comp = hdf.components?.[0];
2038
2188
  if (comp) {
2039
- asset.hostName = comp.name;
2189
+ if (comp.hostname) asset.hostName = comp.hostname;
2190
+ else if (comp.name && comp.name !== comp.fqdn && comp.name !== comp.ipAddress) asset.hostName = comp.name;
2040
2191
  asset.hostIP = comp.ipAddress;
2041
2192
  asset.hostFQDN = comp.fqdn;
2042
2193
  asset.hostMAC = comp.macAddress;
@@ -2090,6 +2241,7 @@ function requirementToVuln(req) {
2090
2241
  legacyIDs: strSlice(tags, "legacy_ids"),
2091
2242
  status: statusFromHdf(req.results?.[0]?.status),
2092
2243
  findingDetails: req.results?.[0]?.message,
2244
+ comments: strVal(tags, "comments"),
2093
2245
  extra: extractCklMetadata(tags)
2094
2246
  };
2095
2247
  }
@@ -2251,11 +2403,15 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2251
2403
  label: "default",
2252
2404
  data: rep.description
2253
2405
  }];
2254
- const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, {
2255
- codeDesc: formatDependencyPath(vuln.from),
2256
- startTime: scanTime
2257
- }));
2258
- const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
2406
+ const results = vulns.map((vuln) => {
2407
+ const result = createResult(ResultStatus.Failed, void 0, {
2408
+ codeDesc: formatDependencyPath(vuln.from),
2409
+ startTime: scanTime
2410
+ });
2411
+ delete result.message;
2412
+ return result;
2413
+ });
2414
+ const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact$1(rep.severity), results, { tags });
2259
2415
  const controlType = deriveControlTypeFromTags(nist);
2260
2416
  if (controlType !== void 0) req.controlType = controlType;
2261
2417
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -2308,12 +2464,12 @@ function convertSingleProject(report, resultsChecksum, scanTime) {
2308
2464
  * @param input - Snyk JSON or SARIF string
2309
2465
  * @returns HDF JSON string
2310
2466
  */
2311
- async function convertSnykToHdf(input) {
2467
+ async function convertSnykToHdf(input, converterVersion = "1.0.0") {
2312
2468
  if (!input || input.trim().length === 0) throw new Error("snyk: empty input");
2313
2469
  validateInputSize(input, "snyk");
2314
2470
  registerAllFingerprints();
2315
2471
  const detected = detectConverter(input);
2316
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
2472
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
2317
2473
  const resultsChecksum = await inputChecksum(input);
2318
2474
  const scanTime = /* @__PURE__ */ new Date();
2319
2475
  const parsed = parseJSON(input);
@@ -2332,7 +2488,7 @@ async function convertSnykToHdf(input) {
2332
2488
  }
2333
2489
  return buildHdfResults({
2334
2490
  generatorName: "snyk-to-hdf",
2335
- converterVersion: "1.0.0",
2491
+ converterVersion,
2336
2492
  toolName: "Snyk",
2337
2493
  toolFormat: "JSON",
2338
2494
  baselines,
@@ -2377,11 +2533,12 @@ function getFixInfo(fix) {
2377
2533
  function getCVSSInfo(vuln, relatedVulns) {
2378
2534
  const cvssData = {};
2379
2535
  if (vuln.cvss && vuln.cvss.length > 0) cvssData.primary = vuln.cvss;
2380
- if (relatedVulns && relatedVulns.length > 0) cvssData.related = relatedVulns.filter((r) => r.cvss && r.cvss.length > 0).map((r) => ({
2536
+ const related = (relatedVulns ?? []).filter((r) => r.cvss && r.cvss.length > 0).map((r) => ({
2381
2537
  id: r.id,
2382
2538
  dataSource: r.dataSource,
2383
2539
  cvss: r.cvss
2384
2540
  }));
2541
+ if (related.length > 0) cvssData.related = related;
2385
2542
  return JSON.stringify(cvssData);
2386
2543
  }
2387
2544
  function getReferences(vuln, relatedVulns) {
@@ -2553,7 +2710,7 @@ function convertMatchToRequirement(match, isIgnored) {
2553
2710
  if (kev) requirement.kev = kev;
2554
2711
  return requirement;
2555
2712
  }
2556
- async function convertGrypeToHdf(input) {
2713
+ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
2557
2714
  validateInputSize(input, "grype");
2558
2715
  const resultsChecksum = await inputChecksum(input);
2559
2716
  const grypeData = parseJSON(input);
@@ -2574,8 +2731,8 @@ async function convertGrypeToHdf(input) {
2574
2731
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
2575
2732
  const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
2576
2733
  return buildHdfResults({
2577
- generatorName: grypeData.descriptor?.name || "grype",
2578
- converterVersion: grypeData.descriptor?.version || "unknown",
2734
+ generatorName: "grype-to-hdf",
2735
+ converterVersion,
2579
2736
  toolName: "Grype",
2580
2737
  toolVersion: grypeData.descriptor?.version,
2581
2738
  baselines: [baseline],
@@ -2590,7 +2747,6 @@ async function convertGrypeToHdf(input) {
2590
2747
  //#region converters/nessus-to-hdf/typescript/converter.ts
2591
2748
  const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
2592
2749
  const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
2593
- const converterVersion = "1.0.0";
2594
2750
  const IMPACT_MAPPING$7 = {
2595
2751
  "4": .9,
2596
2752
  "3": .7,
@@ -2607,20 +2763,47 @@ const IMPACT_MAPPING$7 = {
2607
2763
  function parseHtml(html) {
2608
2764
  return html.replace(/<[^>]*>/g, "").trim();
2609
2765
  }
2766
+ const NAMED_ENTITIES$3 = {
2767
+ lt: "<",
2768
+ gt: ">",
2769
+ quot: "\"",
2770
+ apos: "'",
2771
+ amp: "&"
2772
+ };
2773
+ /**
2774
+ * Resolve XML predefined and numeric character references. The shared parser
2775
+ * runs with `processEntities: false` (XXE defense), which also leaves `&apos;`
2776
+ * and friends unresolved — so scan text would otherwise carry raw entity
2777
+ * markup into every title, description and message.
2778
+ */
2779
+ function decodeXmlEntities$3(text) {
2780
+ return text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, ref) => {
2781
+ if (ref.startsWith("#x") || ref.startsWith("#X")) return String.fromCodePoint(Number.parseInt(ref.slice(2), 16));
2782
+ if (ref.startsWith("#")) return String.fromCodePoint(Number.parseInt(ref.slice(1), 10));
2783
+ return NAMED_ENTITIES$3[ref] ?? match;
2784
+ });
2785
+ }
2786
+ /** Apply decodeXmlEntities to every string in a parsed XML document. */
2787
+ function decodeEntitiesDeep(value) {
2788
+ if (typeof value === "string") return decodeXmlEntities$3(value);
2789
+ if (Array.isArray(value)) return value.map(decodeEntitiesDeep);
2790
+ if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) value[k] = decodeEntitiesDeep(v);
2791
+ return value;
2792
+ }
2610
2793
  /**
2611
2794
  * Convert Nessus XML scan results to HDF format
2612
2795
  */
2613
- async function convertNessusToHdf(nessusXml) {
2796
+ async function convertNessusToHdf(nessusXml, converterVersion = "1.0.0") {
2614
2797
  validateInputSize(nessusXml, "nessus");
2615
2798
  const resultsChecksum = await inputChecksum(nessusXml);
2616
- const parsed = parseXmlWithArrays(nessusXml, [
2799
+ const parsed = decodeEntitiesDeep(parseXmlWithArrays(nessusXml, [
2617
2800
  "preference",
2618
2801
  "tag",
2619
2802
  "ReportItem",
2620
2803
  "ReportHost",
2621
2804
  "cwe",
2622
2805
  "cve"
2623
- ]);
2806
+ ]));
2624
2807
  const policyName = parsed.NessusClientData_v2.Policy.policyName;
2625
2808
  const version = extractVersion(parsed);
2626
2809
  const reportHosts = parsed.NessusClientData_v2.Report.ReportHost;
@@ -2903,7 +3086,7 @@ function buildTags$2(item, isCompliance) {
2903
3086
  const cciTags = parseComplianceRef(item["compliance-reference"], "CCI");
2904
3087
  tags.cci = cciTags;
2905
3088
  const mappedControls = cciTags.flatMap((cci) => getCCINistMappings(cci) ?? []);
2906
- tags.nist = [...new Set(mappedControls)];
3089
+ tags.nist = [...new Set(mappedControls)].sort();
2907
3090
  } else {
2908
3091
  const nistControls = getNessusNistControl(item["pluginFamily"], item["pluginID"]);
2909
3092
  tags.nist = nistControls ? nistControls.split("|") : [];
@@ -2928,7 +3111,7 @@ function buildRefs(item) {
2928
3111
  function buildResult$1(item, host, isCompliance) {
2929
3112
  const status = getStatus$3(item, isCompliance);
2930
3113
  const codeDesc = getCodeDesc(item);
2931
- const message = item.plugin_output || item["compliance-actual-value"];
3114
+ const message = isCompliance && item["compliance-actual-value"] ? item["compliance-actual-value"] : item.plugin_output;
2932
3115
  const startTimeStr = getHostPropertyValue(host, "HOST_START");
2933
3116
  const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2934
3117
  return {
@@ -2966,6 +3149,7 @@ function convertReportHostToTarget(host) {
2966
3149
  name: hostName,
2967
3150
  type: TargetType.Host
2968
3151
  };
3152
+ if (hostProps["hostname"]) target.hostname = hostProps["hostname"];
2969
3153
  if (isFQDN(hostName)) target.fqdn = hostName;
2970
3154
  const hostIp = hostProps["host-ip"];
2971
3155
  if (hostIp) target.ipAddress = hostIp;
@@ -2974,7 +3158,6 @@ function convertReportHostToTarget(host) {
2974
3158
  if (hostProps["os"]) target.osVersion = hostProps["os"];
2975
3159
  if (hostProps["mac-address"]) target.macAddress = hostProps["mac-address"].split("\n")[0];
2976
3160
  if (hostProps["host-fqdn"]) target.fqdn = hostProps["host-fqdn"];
2977
- target.labels = {};
2978
3161
  return target;
2979
3162
  }
2980
3163
  function isFQDN(s) {
@@ -2986,7 +3169,7 @@ function isIPAddress(s) {
2986
3169
  //#endregion
2987
3170
  //#region converters/sonarqube-to-hdf/typescript/converter.ts
2988
3171
  /**
2989
- * Severity to impact mapping.
3172
+ * Deprecated legacy severity to impact mapping.
2990
3173
  * Canonical reference: heimdall2 sonarqube-mapper.ts IMPACT_MAPPING.
2991
3174
  */
2992
3175
  const SEVERITY_IMPACT_MAPPING = {
@@ -2996,6 +3179,58 @@ const SEVERITY_IMPACT_MAPPING = {
2996
3179
  MINOR: .3,
2997
3180
  INFO: 0
2998
3181
  };
3182
+ /** MQR software-quality severity to impact mapping. */
3183
+ const MQR_IMPACT_MAPPING = {
3184
+ BLOCKER: 1,
3185
+ HIGH: .7,
3186
+ MEDIUM: .5,
3187
+ LOW: .3,
3188
+ INFO: 0
3189
+ };
3190
+ /** MQR severities ordered weakest to strongest. */
3191
+ const MQR_SEVERITY_RANK = {
3192
+ INFO: 0,
3193
+ LOW: 1,
3194
+ MEDIUM: 2,
3195
+ HIGH: 3,
3196
+ BLOCKER: 4
3197
+ };
3198
+ /**
3199
+ * Which severity axis drove a requirement's severity/impact. Emitted as the
3200
+ * severitySource tag so downstream gates can pin the meaning of severity.
3201
+ */
3202
+ const SEVERITY_SOURCE_MQR = "mqr";
3203
+ const SEVERITY_SOURCE_LEGACY = "legacy";
3204
+ /**
3205
+ * Order by code unit, matching Go's sort.Strings (byte order). Not
3206
+ * localeCompare: locale-aware collation would order keys differently than the Go
3207
+ * converter and break output parity between the two implementations.
3208
+ */
3209
+ const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
3210
+ /**
3211
+ * Pick the authoritative severity axis for an issue. In MQR mode SonarQube
3212
+ * deprecates the top-level severity and the UI reports impacts[], so impacts[]
3213
+ * wins whenever present; pre-MQR servers fall back to the legacy field. An issue
3214
+ * is rated per software quality, so the worst rating governs — matching how the
3215
+ * SonarQube UI buckets it. The legacy→MQR relationship is per-rule, not a
3216
+ * constant offset, so the legacy value can never be relabelled after the fact.
3217
+ */
3218
+ function selectSeverity(issue) {
3219
+ const impacts = issue.impacts ?? [];
3220
+ if (impacts.length > 0) {
3221
+ const worst = impacts.reduce((a, b) => MQR_SEVERITY_RANK[b.severity] > MQR_SEVERITY_RANK[a.severity] ? b : a);
3222
+ return {
3223
+ severity: worst.severity,
3224
+ source: SEVERITY_SOURCE_MQR,
3225
+ impact: MQR_IMPACT_MAPPING[worst.severity] ?? .5
3226
+ };
3227
+ }
3228
+ return {
3229
+ severity: issue.severity,
3230
+ source: SEVERITY_SOURCE_LEGACY,
3231
+ impact: SEVERITY_IMPACT_MAPPING[issue.severity] ?? .5
3232
+ };
3233
+ }
2999
3234
  /**
3000
3235
  * Default NIST tag for SonarQube findings without CWE mappings.
3001
3236
  * SA-11 (Developer Security Testing and Evaluation) applies to all issue types —
@@ -3008,7 +3243,7 @@ const DEFAULT_NIST_TAGS = ["SA-11"];
3008
3243
  * @param input - JSON string from SonarQube /api/issues/search endpoint
3009
3244
  * @returns HDF JSON string
3010
3245
  */
3011
- async function convertSonarqubeToHdf(input) {
3246
+ async function convertSonarqubeToHdf(input, converterVersion = "1.0.0") {
3012
3247
  validateInputSize(input, "sonarqube");
3013
3248
  const resultsChecksum = await inputChecksum(input);
3014
3249
  const sonarData = parseJSON(input);
@@ -3027,12 +3262,13 @@ async function convertSonarqubeToHdf(input) {
3027
3262
  if (!issuesByProject.has(projectKey)) issuesByProject.set(projectKey, []);
3028
3263
  issuesByProject.get(projectKey).push(issue);
3029
3264
  }
3265
+ const projectKeys = Array.from(issuesByProject.keys()).sort(byCodeUnit);
3030
3266
  const baselines = [];
3031
- for (const [projectKey, issues] of issuesByProject) {
3032
- const baseline = convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum);
3267
+ for (const projectKey of projectKeys) {
3268
+ const baseline = convertProjectToBaseline(projectKey, issuesByProject.get(projectKey), componentMap, ruleMap, resultsChecksum);
3033
3269
  baselines.push(baseline);
3034
3270
  }
3035
- let components = Array.from(issuesByProject.keys()).map((projectKey) => ({
3271
+ let components = projectKeys.map((projectKey) => ({
3036
3272
  type: TargetType.Application,
3037
3273
  name: projectKey
3038
3274
  }));
@@ -3051,8 +3287,9 @@ async function convertSonarqubeToHdf(input) {
3051
3287
  }
3052
3288
  return buildHdfResults({
3053
3289
  generatorName: "sonarqube-to-hdf",
3054
- converterVersion: "1.0.0",
3290
+ converterVersion,
3055
3291
  toolName: "SonarQube",
3292
+ toolVersion: sonarData.serverVersion || void 0,
3056
3293
  baselines,
3057
3294
  components,
3058
3295
  timestamp: /* @__PURE__ */ new Date()
@@ -3074,8 +3311,8 @@ function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, res
3074
3311
  issuesByRule.get(ruleKey).push(issue);
3075
3312
  }
3076
3313
  const requirements = [];
3077
- for (const [ruleKey, ruleIssues] of issuesByRule) {
3078
- const requirement = convertRuleToRequirement(ruleKey, ruleIssues, componentMap, ruleMap);
3314
+ for (const ruleKey of Array.from(issuesByRule.keys()).sort(byCodeUnit)) {
3315
+ const requirement = convertRuleToRequirement(ruleKey, issuesByRule.get(ruleKey), componentMap, ruleMap);
3079
3316
  requirements.push(requirement);
3080
3317
  }
3081
3318
  return createMinimalBaseline(projectKey, requirements, {
@@ -3088,7 +3325,7 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
3088
3325
  const title = rule?.name || ruleKey;
3089
3326
  const description = extractDescription(rule);
3090
3327
  const firstIssue = issues[0];
3091
- const impact = SEVERITY_IMPACT_MAPPING[firstIssue.severity] || .5;
3328
+ const { severity, source: severitySource, impact } = selectSeverity(firstIssue);
3092
3329
  const { cweIds, owaspTags, allTags } = extractTags(rule, issues);
3093
3330
  const nistControls = mapCWEToNIST(cweIds, DEFAULT_NIST_TAGS);
3094
3331
  const cciControls = nistToCci(nistControls);
@@ -3096,12 +3333,18 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
3096
3333
  const issueWithLocation = issues.find((i) => i.line !== void 0);
3097
3334
  const sourceLocation = issueWithLocation ? extractSourceLocation(issueWithLocation, componentMap) : void 0;
3098
3335
  const options = { tags: {
3099
- severity: firstIssue.severity.toLowerCase(),
3336
+ severity: severity.toLowerCase(),
3337
+ severitySource,
3100
3338
  type: firstIssue.type.toLowerCase(),
3101
3339
  cwe: cweIds,
3102
3340
  owasp: owaspTags,
3103
3341
  nist: nistControls,
3104
3342
  cci: cciControls,
3343
+ ...severitySource === SEVERITY_SOURCE_MQR ? {
3344
+ legacySeverity: firstIssue.severity.toLowerCase(),
3345
+ impacts: firstIssue.impacts,
3346
+ ...firstIssue.cleanCodeAttribute ? { cleanCodeAttribute: firstIssue.cleanCodeAttribute } : {}
3347
+ } : {},
3105
3348
  ...allTags
3106
3349
  } };
3107
3350
  if (sourceLocation) options.sourceLocation = sourceLocation;
@@ -3195,11 +3438,15 @@ function mapComplianceStatus(complianceType) {
3195
3438
  default: return ResultStatus.NotReviewed;
3196
3439
  }
3197
3440
  }
3441
+ /** Mapping values pack multiple controls into one pipe-delimited string. */
3442
+ function splitControls(nistId) {
3443
+ return nistId.split("|").map((c) => c.trim()).filter(Boolean);
3444
+ }
3198
3445
  function buildNistTags$3(sourceIdentifier, ruleName) {
3199
- const byIdentifier = getAwsConfigNistControlByIdentifier(sourceIdentifier);
3200
- if (byIdentifier) return [byIdentifier];
3446
+ const byIdentifier = sourceIdentifier ? getAwsConfigNistControlByIdentifier(sourceIdentifier) : void 0;
3447
+ if (byIdentifier) return splitControls(byIdentifier);
3201
3448
  const byName = getAwsConfigNistControlByName(ruleName);
3202
- if (byName) return [byName];
3449
+ if (byName) return splitControls(byName);
3203
3450
  return [];
3204
3451
  }
3205
3452
  /**
@@ -3239,16 +3486,27 @@ function buildResultMessage(codeDesc, annotation, status) {
3239
3486
  if (status !== ResultStatus.Failed) return void 0;
3240
3487
  return `(${codeDesc}): ${annotation || "Rule does not pass rule compliance"}`;
3241
3488
  }
3489
+ /** Elapsed seconds between rule invocation and result recording; omitted if either time is unusable. */
3490
+ function computeRunTime(invoked, recorded) {
3491
+ const start = invoked ? parseTimestamp(invoked) : null;
3492
+ const end = recorded ? parseTimestamp(recorded) : null;
3493
+ if (!start || !end) return void 0;
3494
+ return (end.getTime() - start.getTime()) / 1e3;
3495
+ }
3242
3496
  function buildResult(r) {
3243
3497
  const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
3244
3498
  const status = mapComplianceStatus(r.ComplianceType);
3245
3499
  const codeDesc = buildCodeDesc$7(q);
3246
3500
  const message = buildResultMessage(codeDesc, r.Annotation, status);
3247
3501
  const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
3248
- return createResult(status, message ?? codeDesc, {
3502
+ const runTime = computeRunTime(r.ConfigRuleInvokedTime, r.ResultRecordedTime);
3503
+ return {
3504
+ status,
3249
3505
  codeDesc,
3250
- ...startTime ? { startTime } : {}
3251
- });
3506
+ startTime,
3507
+ ...runTime !== void 0 ? { runTime } : {},
3508
+ ...message !== void 0 ? { message } : {}
3509
+ };
3252
3510
  }
3253
3511
  /**
3254
3512
  * Synthesizes a single HDF result for a Config rule whose live evaluation
@@ -3259,10 +3517,11 @@ function buildResult(r) {
3259
3517
  */
3260
3518
  function buildNotApplicableResult(rule) {
3261
3519
  const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
3262
- return createResult(ResultStatus.NotApplicable, codeDesc, {
3520
+ return {
3521
+ status: ResultStatus.NotApplicable,
3263
3522
  codeDesc,
3264
3523
  startTime: /* @__PURE__ */ new Date()
3265
- });
3524
+ };
3266
3525
  }
3267
3526
  function buildRequirement$15(rule) {
3268
3527
  const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
@@ -3295,7 +3554,7 @@ function buildRequirement$15(rule) {
3295
3554
  * with get-compliance-details-by-config-rule
3296
3555
  * @returns HDF JSON string
3297
3556
  */
3298
- async function convertAwsConfigToHdf(input) {
3557
+ async function convertAwsConfigToHdf(input, converterVersion = "1.0.0") {
3299
3558
  validateInputSize(input, "aws-config");
3300
3559
  const resultsChecksum = await inputChecksum(input);
3301
3560
  const data = parseJSON(input);
@@ -3316,7 +3575,7 @@ async function convertAwsConfigToHdf(input) {
3316
3575
  const region = getRegion(firstArn);
3317
3576
  return buildHdfResults({
3318
3577
  generatorName: "aws-config-to-hdf",
3319
- converterVersion: "1.0.0",
3578
+ converterVersion,
3320
3579
  toolName: "AWS Config",
3321
3580
  baselines: [baseline],
3322
3581
  components: [{
@@ -3349,7 +3608,7 @@ function mapStatus$2(result) {
3349
3608
  */
3350
3609
  function getImpact$8(severity) {
3351
3610
  if (!severity) return .5;
3352
- return severityToImpact(severity);
3611
+ return severityToImpact$1(severity);
3353
3612
  }
3354
3613
  /**
3355
3614
  * Converts a single CheckovCheck to an HDF RequirementResult.
@@ -3359,10 +3618,12 @@ function checkToResult(check, scanTime) {
3359
3618
  const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
3360
3619
  let message;
3361
3620
  if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
3362
- return createResult(status, message ?? "", {
3621
+ return {
3622
+ status,
3363
3623
  codeDesc,
3364
- startTime: scanTime
3365
- });
3624
+ startTime: scanTime,
3625
+ ...message !== void 0 ? { message } : {}
3626
+ };
3366
3627
  }
3367
3628
  /**
3368
3629
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
@@ -3404,11 +3665,11 @@ function parseInput(input) {
3404
3665
  * @param input - checkov JSON or SARIF string
3405
3666
  * @returns HDF JSON string
3406
3667
  */
3407
- async function convertCheckovToHdf(input) {
3668
+ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
3408
3669
  validateInputSize(input, "checkov");
3409
3670
  registerAllFingerprints();
3410
3671
  const detected = detectConverter(input);
3411
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3672
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
3412
3673
  const resultsChecksum = await inputChecksum(input);
3413
3674
  const scanTime = /* @__PURE__ */ new Date();
3414
3675
  const reports = parseInput(input);
@@ -3441,7 +3702,7 @@ async function convertCheckovToHdf(input) {
3441
3702
  const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
3442
3703
  return buildHdfResults({
3443
3704
  generatorName: "checkov-to-hdf",
3444
- converterVersion: "1.0.0",
3705
+ converterVersion,
3445
3706
  toolName: "Checkov",
3446
3707
  toolVersion: version,
3447
3708
  toolFormat: format,
@@ -3525,11 +3786,11 @@ function buildRequirement$13(ruleId, issues, scanTime) {
3525
3786
  * @param input - gosec JSON or SARIF string
3526
3787
  * @returns HDF JSON string
3527
3788
  */
3528
- async function convertGosecToHdf(input) {
3789
+ async function convertGosecToHdf(input, converterVersion = "1.0.0") {
3529
3790
  validateInputSize(input, "gosec");
3530
3791
  registerAllFingerprints();
3531
3792
  const detected = detectConverter(input);
3532
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3793
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
3533
3794
  const resultsChecksum = await inputChecksum(input);
3534
3795
  const report = parseJSON(input);
3535
3796
  if (!report || typeof report !== "object") throw new Error("Invalid gosec structure: not a valid JSON object");
@@ -3550,7 +3811,7 @@ async function convertGosecToHdf(input) {
3550
3811
  const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
3551
3812
  return buildHdfResults({
3552
3813
  generatorName: "gosec-to-hdf",
3553
- converterVersion: "1.0.0",
3814
+ converterVersion,
3554
3815
  toolName: "gosec",
3555
3816
  toolVersion: report.GosecVersion || void 0,
3556
3817
  baselines: [baseline],
@@ -3597,7 +3858,7 @@ function convertVulnToRequirement(vuln) {
3597
3858
  req.verificationMethod = VerificationMethodEnum.Automated;
3598
3859
  return req;
3599
3860
  }
3600
- async function convertNiktoToHdf(input) {
3861
+ async function convertNiktoToHdf(input, converterVersion = "unknown") {
3601
3862
  validateInputSize(input, "nikto");
3602
3863
  const resultsChecksum = await inputChecksum(input);
3603
3864
  const niktoData = parseJSON(input);
@@ -3630,7 +3891,7 @@ async function convertNiktoToHdf(input) {
3630
3891
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
3631
3892
  return buildHdfResults({
3632
3893
  generatorName: "nikto-to-hdf",
3633
- converterVersion: "unknown",
3894
+ converterVersion,
3634
3895
  toolName: "Nikto",
3635
3896
  toolFormat: "JSON",
3636
3897
  baselines: [createMinimalBaseline(targetName, requirements, {
@@ -3703,11 +3964,11 @@ function parseZapTimestamp(s) {
3703
3964
  }
3704
3965
  return parseTimestamp(s) ?? void 0;
3705
3966
  }
3706
- async function convertZapToHdf(input) {
3967
+ async function convertZapToHdf(input, converterVersion = "1.0.0") {
3707
3968
  validateInputSize(input, "zap");
3708
3969
  registerAllFingerprints();
3709
3970
  const detected = detectConverter(input);
3710
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3971
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
3711
3972
  const resultsChecksum = await inputChecksum(input);
3712
3973
  const zapData = parseJSON(input);
3713
3974
  const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
@@ -3800,7 +4061,7 @@ async function convertZapToHdf(input) {
3800
4061
  components,
3801
4062
  generator: {
3802
4063
  name: "zap-to-hdf",
3803
- version: "unknown"
4064
+ version: converterVersion
3804
4065
  },
3805
4066
  tool
3806
4067
  };
@@ -3811,6 +4072,579 @@ async function convertZapToHdf(input) {
3811
4072
  return serializeHdf(hdf);
3812
4073
  }
3813
4074
  //#endregion
4075
+ //#region shared/typescript/bom/model.ts
4076
+ /**
4077
+ * BOM manifest-kind discriminator. The schema allows custom 'x-'-prefixed kinds
4078
+ * beyond the reserved set, so the generated `bomType` field is a plain string;
4079
+ * the reserved values below are the parser's normalized vocabulary. Mirrors the
4080
+ * Go bom package.
4081
+ */
4082
+ const BOMType = {
4083
+ Sbom: "sbom",
4084
+ AIModel: "ai-model",
4085
+ Dataset: "dataset"
4086
+ };
4087
+ //#endregion
4088
+ //#region shared/typescript/bom/fingerprints.ts
4089
+ function record(input) {
4090
+ return typeof input === "object" && input !== null && !Array.isArray(input) ? input : void 0;
4091
+ }
4092
+ /** CycloneDX: bomFormat === 'CycloneDX'. Returns 0..1 confidence. */
4093
+ function detectCycloneDX(input) {
4094
+ return record(input)?.bomFormat === "CycloneDX" ? 1 : 0;
4095
+ }
4096
+ /**
4097
+ * CycloneDX ML-BOM: a CycloneDX document with at least one
4098
+ * machine-learning-model component. Strictly more specific than plain
4099
+ * CycloneDX.
4100
+ */
4101
+ function detectCycloneDXML(input) {
4102
+ const obj = record(input);
4103
+ if (obj?.bomFormat !== "CycloneDX") return 0;
4104
+ const components = obj.components;
4105
+ if (!Array.isArray(components)) return 0;
4106
+ return components.some((c) => record(c)?.type === "machine-learning-model") ? 1 : 0;
4107
+ }
4108
+ /** SPDX: a non-empty spdxVersion string is present. Returns 0..1 confidence. */
4109
+ function detectSPDX(input) {
4110
+ const obj = record(input);
4111
+ return typeof obj?.spdxVersion === "string" && obj.spdxVersion.length > 0 ? 1 : 0;
4112
+ }
4113
+ /**
4114
+ * SPDX 3.0 AI/Dataset (JSON-LD): a document with an `@context` and an `@graph`
4115
+ * array carrying at least one ai_AIPackage or dataset_DatasetPackage element.
4116
+ * Structurally disjoint from the SPDX 2.3 detector (which keys on spdxVersion),
4117
+ * so the two never conflict.
4118
+ */
4119
+ function detectSPDX3(input) {
4120
+ const obj = record(input);
4121
+ if (!obj || obj["@context"] === void 0) return 0;
4122
+ const graph = obj["@graph"];
4123
+ if (!Array.isArray(graph)) return 0;
4124
+ return graph.some((el) => {
4125
+ const type = record(el)?.type;
4126
+ return type === "ai_AIPackage" || type === "dataset_DatasetPackage";
4127
+ }) ? 1 : 0;
4128
+ }
4129
+ /**
4130
+ * Detect the BOM format of a parsed JSON object. ML wins over plain CycloneDX
4131
+ * by precedence; returns undefined when no supported format matches.
4132
+ */
4133
+ function detectFormat(input) {
4134
+ const ml = detectCycloneDXML(input);
4135
+ if (ml > 0) return {
4136
+ format: "cyclonedx-ml",
4137
+ confidence: ml
4138
+ };
4139
+ const cdx = detectCycloneDX(input);
4140
+ if (cdx > 0) return {
4141
+ format: "cyclonedx",
4142
+ confidence: cdx
4143
+ };
4144
+ const spdx3 = detectSPDX3(input);
4145
+ if (spdx3 > 0) return {
4146
+ format: "spdx-3-ai",
4147
+ confidence: spdx3
4148
+ };
4149
+ const spdx = detectSPDX(input);
4150
+ if (spdx > 0) return {
4151
+ format: "spdx",
4152
+ confidence: spdx
4153
+ };
4154
+ }
4155
+ //#endregion
4156
+ //#region shared/typescript/bom/cyclonedx.ts
4157
+ /**
4158
+ * CycloneDX SBOM -> normalized HDF BillOfMaterials.
4159
+ *
4160
+ * Flattens the top-level components[] into SBOM packages. Nested subcomponents
4161
+ * (metadata.component.components[]) are the tool's own assembly tree and are
4162
+ * intentionally not treated as inventory packages here.
4163
+ */
4164
+ /** Extract license identifiers/expressions from a CycloneDX licenses[] array. */
4165
+ function extractLicenses$1(raw) {
4166
+ if (!Array.isArray(raw)) return [];
4167
+ const out = [];
4168
+ for (const entry of raw) {
4169
+ const e = asRecord(entry);
4170
+ if (!e) continue;
4171
+ const license = asRecord(e.license);
4172
+ const value = (license ? asString(license.id) ?? asString(license.name) : void 0) ?? asString(e.expression);
4173
+ if (value) out.push(value);
4174
+ }
4175
+ return out;
4176
+ }
4177
+ function componentToPackage(component) {
4178
+ const c = asRecord(component);
4179
+ const name = c ? asString(c.name) : void 0;
4180
+ if (!c || !name) return void 0;
4181
+ const pkg = { name };
4182
+ const version = asString(c.version);
4183
+ if (version) pkg.version = version;
4184
+ const purl = asString(c.purl);
4185
+ if (purl) pkg.purl = purl;
4186
+ const licenses = extractLicenses$1(c.licenses);
4187
+ if (licenses.length > 0) pkg.licenses = licenses;
4188
+ enrichFromPurl(pkg);
4189
+ return pkg;
4190
+ }
4191
+ function parseCycloneDX(obj) {
4192
+ const components = Array.isArray(obj.components) ? obj.components : [];
4193
+ const packages = [];
4194
+ for (const component of components) {
4195
+ const pkg = componentToPackage(component);
4196
+ if (pkg) packages.push(pkg);
4197
+ }
4198
+ const uniqueId = asString(obj.serialNumber);
4199
+ return buildBom({
4200
+ bomType: BOMType.Sbom,
4201
+ format: "cyclonedx",
4202
+ packages: limitArrayWithWarning(packages, "package"),
4203
+ uniqueId
4204
+ });
4205
+ }
4206
+ //#endregion
4207
+ //#region shared/typescript/bom/spdx.ts
4208
+ /**
4209
+ * SPDX SBOM -> normalized HDF BillOfMaterials.
4210
+ *
4211
+ * Maps packages[] to normalized SBOM packages. purl comes from an externalRefs
4212
+ * entry with referenceType 'purl'; licenses come from licenseConcluded /
4213
+ * licenseDeclared with NOASSERTION/NONE sentinels filtered out.
4214
+ */
4215
+ const SPDX_LICENSE_FIELDS = ["licenseConcluded", "licenseDeclared"];
4216
+ /** First externalRefs[].referenceLocator whose referenceType is 'purl'. */
4217
+ function purlFromExternalRefs(refs) {
4218
+ if (!Array.isArray(refs)) return void 0;
4219
+ for (const ref of refs) {
4220
+ const r = asRecord(ref);
4221
+ if (r?.referenceType === "purl") {
4222
+ const locator = asString(r.referenceLocator);
4223
+ if (locator) return locator;
4224
+ }
4225
+ }
4226
+ }
4227
+ function extractLicenses(pkg) {
4228
+ const out = [];
4229
+ for (const field of SPDX_LICENSE_FIELDS) {
4230
+ const license = cleanLicense(pkg[field]);
4231
+ if (license && !out.includes(license)) out.push(license);
4232
+ }
4233
+ return out;
4234
+ }
4235
+ function packageToPackage(source) {
4236
+ const p = asRecord(source);
4237
+ const name = p ? asString(p.name) : void 0;
4238
+ if (!p || !name) return void 0;
4239
+ const pkg = { name };
4240
+ const version = asString(p.versionInfo);
4241
+ if (version) pkg.version = version;
4242
+ const purl = purlFromExternalRefs(p.externalRefs);
4243
+ if (purl) pkg.purl = purl;
4244
+ const licenses = extractLicenses(p);
4245
+ if (licenses.length > 0) pkg.licenses = licenses;
4246
+ enrichFromPurl(pkg);
4247
+ return pkg;
4248
+ }
4249
+ function parseSPDX(obj) {
4250
+ const sourcePackages = Array.isArray(obj.packages) ? obj.packages : [];
4251
+ const packages = [];
4252
+ for (const source of sourcePackages) {
4253
+ const pkg = packageToPackage(source);
4254
+ if (pkg) packages.push(pkg);
4255
+ }
4256
+ const uniqueId = asString(obj.documentNamespace);
4257
+ return buildBom({
4258
+ bomType: BOMType.Sbom,
4259
+ format: "spdx",
4260
+ packages: limitArrayWithWarning(packages, "package"),
4261
+ uniqueId
4262
+ });
4263
+ }
4264
+ //#endregion
4265
+ //#region shared/typescript/bom/spdx3.ts
4266
+ /**
4267
+ * SPDX 3.0 AI/Dataset (JSON-LD) -> normalized HDF BillOfMaterials subjects.
4268
+ *
4269
+ * SPDX 3.0 is JSON-LD: a top-level { "@context", "@graph" } where "@graph" is an
4270
+ * array of typed elements. A single document is inherently MULTI-SUBJECT — it
4271
+ * can carry several ai_AIPackage and dataset_DatasetPackage elements — so this
4272
+ * parser returns one subject per AI/dataset element (unlike the single-BOM
4273
+ * parseSPDX / parseMLBOM paths). This is completely distinct from SPDX 2.3
4274
+ * (spdxVersion + packages[]), handled by parseSPDX.
4275
+ *
4276
+ * PARTIAL-FIDELITY: only fields that map cleanly onto the normalized extensions
4277
+ * are lifted; everything else (energy, autonomy, safety, bias, sensor, size, …)
4278
+ * is carried opaquely via the BOM `document` passthrough of the raw element.
4279
+ * Two conflation traps are deliberately avoided: ai_hyperparameter is training
4280
+ * knobs (-> hyperparameters, NEVER parameterCount) and dataset_datasetSize is
4281
+ * ambiguous/unlabeled (never -> recordCount).
4282
+ */
4283
+ /** SPDX-3 relationship types that link a model to a training/eval dataset. */
4284
+ const DATASET_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["trainedOn", "testedOn"]);
4285
+ function graphElements(obj) {
4286
+ const graph = obj["@graph"];
4287
+ if (!Array.isArray(graph)) return [];
4288
+ const out = [];
4289
+ for (const el of graph) {
4290
+ const r = asRecord(el);
4291
+ if (r) out.push(r);
4292
+ }
4293
+ return out;
4294
+ }
4295
+ /** Map SPDX DictionaryEntry[] ({key,value}) to normalized {name,value} pairs. */
4296
+ function dictionaryEntries(value) {
4297
+ if (!Array.isArray(value)) return [];
4298
+ const out = [];
4299
+ for (const entry of value) {
4300
+ const e = asRecord(entry);
4301
+ if (!e) continue;
4302
+ const name = asString(e.key);
4303
+ if (name === void 0) continue;
4304
+ const value = e.value !== void 0 && e.value !== null ? stringifyScalar(e.value) : "";
4305
+ out.push({
4306
+ name,
4307
+ value
4308
+ });
4309
+ }
4310
+ return out;
4311
+ }
4312
+ /** First element of a non-empty string array, else undefined. */
4313
+ function firstString(value) {
4314
+ if (!Array.isArray(value)) return void 0;
4315
+ for (const item of value) {
4316
+ const s = asString(item);
4317
+ if (s !== void 0) return s;
4318
+ }
4319
+ }
4320
+ /** Distinct non-empty strings of an array, joined with "; ". */
4321
+ function joinDistinct(value) {
4322
+ if (!Array.isArray(value)) return void 0;
4323
+ const seen = [];
4324
+ for (const item of value) {
4325
+ const s = asString(item);
4326
+ if (s !== void 0 && !seen.includes(s)) seen.push(s);
4327
+ }
4328
+ return seen.length > 0 ? seen.join("; ") : void 0;
4329
+ }
4330
+ /**
4331
+ * Dataset references for a model: targets of trainedOn/testedOn relationships
4332
+ * whose `from` is this model. Each target is resolved to the referenced
4333
+ * dataset element's name when present in the graph, else the raw id. Distinct,
4334
+ * first-seen order.
4335
+ */
4336
+ function datasetRefsFor(modelId, relationships, datasetNameById) {
4337
+ const out = [];
4338
+ for (const rel of relationships) {
4339
+ if (asString(rel.from) !== modelId) continue;
4340
+ if (!DATASET_RELATIONSHIP_TYPES.has(asString(rel.relationshipType) ?? "")) continue;
4341
+ const targets = Array.isArray(rel.to) ? rel.to : [rel.to];
4342
+ for (const target of targets) {
4343
+ const targetId = asString(target);
4344
+ if (targetId === void 0) continue;
4345
+ const ref = datasetNameById.get(targetId) ?? targetId;
4346
+ if (!out.includes(ref)) out.push(ref);
4347
+ }
4348
+ }
4349
+ return out;
4350
+ }
4351
+ function buildModelExtension$1(element, relationships, datasetNameById) {
4352
+ const model = {};
4353
+ const hyperparameters = dictionaryEntries(element.ai_hyperparameter);
4354
+ if (hyperparameters.length > 0) model.hyperparameters = hyperparameters;
4355
+ const performanceMetrics = dictionaryEntries(element.ai_metric);
4356
+ if (performanceMetrics.length > 0) model.performanceMetrics = performanceMetrics;
4357
+ const task = firstString(element.ai_domain);
4358
+ if (task !== void 0) model.task = task;
4359
+ const architecture = joinDistinct(element.ai_typeOfModel);
4360
+ if (architecture !== void 0) model.modelArchitecture = architecture;
4361
+ const intendedUse = asString(element.ai_informationAboutApplication);
4362
+ if (intendedUse !== void 0) model.intendedUse = intendedUse;
4363
+ const datasetRefs = datasetRefsFor(asString(element.spdxId) ?? "", relationships, datasetNameById);
4364
+ if (datasetRefs.length > 0) model.datasetRefs = datasetRefs;
4365
+ return model;
4366
+ }
4367
+ function buildDatasetExtension(element) {
4368
+ const dataset = {};
4369
+ const modality = element.dataset_datasetType;
4370
+ if (Array.isArray(modality)) {
4371
+ const values = modality.map(asString).filter((s) => s !== void 0);
4372
+ if (values.length > 0) dataset.modality = values;
4373
+ }
4374
+ const dataClassification = asString(element.dataset_confidentialityLevel);
4375
+ if (dataClassification !== void 0) dataset.dataClassification = dataClassification;
4376
+ const intendedUse = asString(element.dataset_intendedUse);
4377
+ if (intendedUse !== void 0) dataset.intendedUse = intendedUse;
4378
+ const provenance = asString(element.dataset_dataCollectionProcess);
4379
+ if (provenance !== void 0) dataset.provenance = provenance;
4380
+ return dataset;
4381
+ }
4382
+ /**
4383
+ * Parse an SPDX-3 JSON-LD document into its AI/dataset subjects. Emits one
4384
+ * aiModel subject per ai_AIPackage and one dataset subject per
4385
+ * dataset_DatasetPackage, in graph order.
4386
+ */
4387
+ function parseSPDX3(obj) {
4388
+ const elements = graphElements(obj);
4389
+ const relationships = elements.filter((el) => el.type === "Relationship");
4390
+ const datasetNameById = /* @__PURE__ */ new Map();
4391
+ for (const el of elements) {
4392
+ if (el.type !== "dataset_DatasetPackage") continue;
4393
+ const id = asString(el.spdxId);
4394
+ const name = asString(el.name);
4395
+ if (id !== void 0 && name !== void 0) datasetNameById.set(id, name);
4396
+ }
4397
+ const subjects = [];
4398
+ for (const element of elements) if (element.type === "ai_AIPackage") {
4399
+ const model = buildModelExtension$1(element, relationships, datasetNameById);
4400
+ subjects.push({
4401
+ kind: "aiModel",
4402
+ name: asString(element.name) ?? "",
4403
+ id: asString(element.spdxId) ?? "",
4404
+ bom: buildBom({
4405
+ bomType: BOMType.AIModel,
4406
+ format: "spdx-3-ai",
4407
+ model,
4408
+ document: element,
4409
+ uniqueId: asString(element.spdxId)
4410
+ })
4411
+ });
4412
+ } else if (element.type === "dataset_DatasetPackage") {
4413
+ const dataset = buildDatasetExtension(element);
4414
+ subjects.push({
4415
+ kind: "dataset",
4416
+ name: asString(element.name) ?? "",
4417
+ id: asString(element.spdxId) ?? "",
4418
+ bom: buildBom({
4419
+ bomType: BOMType.Dataset,
4420
+ format: "spdx-3-ai",
4421
+ dataset,
4422
+ document: element,
4423
+ uniqueId: asString(element.spdxId)
4424
+ })
4425
+ });
4426
+ }
4427
+ return { subjects: limitArrayWithWarning(subjects, "subject") };
4428
+ }
4429
+ //#endregion
4430
+ //#region shared/typescript/bom/ml-bom.ts
4431
+ /**
4432
+ * CycloneDX ML-BOM -> normalized HDF ai-model BillOfMaterials.
4433
+ *
4434
+ * PARTIAL-FIDELITY: only modelCard fields that map cleanly onto the normalized
4435
+ * AI_Model_Extension are lifted (modelArchitecture, datasetRefs, intendedUse,
4436
+ * learningApproach, task, performanceMetrics, inputOutput.dataTypes).
4437
+ * parameterCount, serializationFormat, hyperparameters, and the rest of
4438
+ * inputOutput have NO native CycloneDX ML source, so they are left undefined —
4439
+ * never fabricated. The raw machine-learning-model component is carried verbatim
4440
+ * in the BOM `document` passthrough so nothing is lost, satisfying the
4441
+ * "drop-or-passthrough, never invent" rule.
4442
+ */
4443
+ function findModelComponent(obj) {
4444
+ const components = Array.isArray(obj.components) ? obj.components : [];
4445
+ for (const component of components) {
4446
+ const c = asRecord(component);
4447
+ if (c?.type === "machine-learning-model") return c;
4448
+ }
4449
+ }
4450
+ /**
4451
+ * References to training/evaluation datasets. Only ref-shaped entries (a bare
4452
+ * ref string, or an object with a `ref`) are lifted; inline dataset descriptors
4453
+ * without a reference are left for a future dataset-normalization pass rather
4454
+ * than being synthesized into a ref.
4455
+ */
4456
+ function extractDatasetRefs(datasets) {
4457
+ if (!Array.isArray(datasets)) return [];
4458
+ const out = [];
4459
+ for (const dataset of datasets) {
4460
+ if (typeof dataset === "string") {
4461
+ if (dataset.length > 0) out.push(dataset);
4462
+ continue;
4463
+ }
4464
+ const ref = asString(asRecord(dataset)?.ref);
4465
+ if (ref) out.push(ref);
4466
+ }
4467
+ return out;
4468
+ }
4469
+ /** Intended-use statement from modelCard.considerations.useCases, if present. */
4470
+ function extractIntendedUse(considerations) {
4471
+ const c = asRecord(considerations);
4472
+ if (!c || !Array.isArray(c.useCases)) return void 0;
4473
+ const parts = c.useCases.map(asString).filter((s) => s !== void 0);
4474
+ return parts.length > 0 ? parts.join("; ") : void 0;
4475
+ }
4476
+ /**
4477
+ * Reported evaluation metrics from modelCard.quantitativeAnalysis. Each native
4478
+ * metric's `type` becomes the normalized `name` and its `value` is carried as a
4479
+ * string (metrics are heterogeneous). An entry contributes only when it has a
4480
+ * name or value; native slice/confidenceInterval are left in the `document`.
4481
+ */
4482
+ function extractPerformanceMetrics(quantitativeAnalysis) {
4483
+ const qa = asRecord(quantitativeAnalysis);
4484
+ if (!qa || !Array.isArray(qa.performanceMetrics)) return [];
4485
+ const out = [];
4486
+ for (const entry of qa.performanceMetrics) {
4487
+ const e = asRecord(entry);
4488
+ if (!e) continue;
4489
+ const name = asString(e.type);
4490
+ const value = e.value !== void 0 && e.value !== null ? stringifyScalar(e.value) : void 0;
4491
+ if (name === void 0 && value === void 0) continue;
4492
+ const metric = {};
4493
+ if (name !== void 0) metric.name = name;
4494
+ if (value !== void 0) metric.value = value;
4495
+ out.push(metric);
4496
+ }
4497
+ return out;
4498
+ }
4499
+ /** Distinct `format` strings across modelParameters.inputs[] and outputs[]. */
4500
+ function extractIODataTypes(parameters) {
4501
+ const out = [];
4502
+ for (const key of ["inputs", "outputs"]) {
4503
+ const entries = parameters[key];
4504
+ if (!Array.isArray(entries)) continue;
4505
+ for (const entry of entries) {
4506
+ const format = asString(asRecord(entry)?.format);
4507
+ if (format && !out.includes(format)) out.push(format);
4508
+ }
4509
+ }
4510
+ return out;
4511
+ }
4512
+ function buildModelExtension(modelComponent) {
4513
+ const model = {};
4514
+ const modelCard = asRecord(modelComponent.modelCard);
4515
+ if (!modelCard) return model;
4516
+ const parameters = asRecord(modelCard.modelParameters);
4517
+ if (parameters) {
4518
+ const architecture = asString(parameters.modelArchitecture) ?? asString(parameters.architectureFamily);
4519
+ if (architecture) model.modelArchitecture = architecture;
4520
+ const datasetRefs = extractDatasetRefs(parameters.datasets);
4521
+ if (datasetRefs.length > 0) model.datasetRefs = datasetRefs;
4522
+ const learningApproach = asString(asRecord(parameters.approach)?.type);
4523
+ if (learningApproach) model.learningApproach = learningApproach;
4524
+ const task = asString(parameters.task);
4525
+ if (task) model.task = task;
4526
+ const dataTypes = extractIODataTypes(parameters);
4527
+ if (dataTypes.length > 0) model.inputOutput = { dataTypes };
4528
+ }
4529
+ const performanceMetrics = extractPerformanceMetrics(modelCard.quantitativeAnalysis);
4530
+ if (performanceMetrics.length > 0) model.performanceMetrics = performanceMetrics;
4531
+ const intendedUse = extractIntendedUse(modelCard.considerations);
4532
+ if (intendedUse) model.intendedUse = intendedUse;
4533
+ return model;
4534
+ }
4535
+ function parseMLBOM(obj) {
4536
+ const modelComponent = findModelComponent(obj);
4537
+ const model = modelComponent ? buildModelExtension(modelComponent) : {};
4538
+ return buildBom({
4539
+ bomType: BOMType.AIModel,
4540
+ format: "cyclonedx-ml",
4541
+ model,
4542
+ document: modelComponent,
4543
+ uniqueId: asString(obj.serialNumber)
4544
+ });
4545
+ }
4546
+ //#endregion
4547
+ //#region shared/typescript/bom/normalize.ts
4548
+ /**
4549
+ * Shared BOM normalization: parseBom / buildBom / detectFormat re-dispatch and
4550
+ * the cross-format helpers (object guards, license cleaning, purl handling).
4551
+ * Format-specific extraction lives in cyclonedx.ts / spdx.ts / ml-bom.ts.
4552
+ */
4553
+ const CONVERTER = "bom-parser";
4554
+ /** SPDX sentinels that mean "no license", filtered out of normalized output. */
4555
+ const SPDX_NULL_LICENSES = /* @__PURE__ */ new Set(["noassertion", "none"]);
4556
+ /** Narrow an unknown value to a plain object (not array, not null). */
4557
+ function asRecord(value) {
4558
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4559
+ }
4560
+ /** Narrow an unknown value to a non-empty string. */
4561
+ function asString(value) {
4562
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4563
+ }
4564
+ /**
4565
+ * Render a heterogeneous scalar BOM value (metric value, SPDX DictionaryEntry
4566
+ * value) to a string. This is the REFERENCE the Go side matches byte-for-byte
4567
+ * (see stringifyScalar / jsNumberToString in normalize.go): JS String() gives
4568
+ * numbers their shortest round-tripping form, and Go reimplements that exact
4569
+ * formatting. Callers must guard null/undefined before calling.
4570
+ */
4571
+ function stringifyScalar(value) {
4572
+ return String(value);
4573
+ }
4574
+ /** Return an SPDX license string unless it is a NOASSERTION/NONE sentinel. */
4575
+ function cleanLicense(value) {
4576
+ const s = asString(value);
4577
+ if (!s) return void 0;
4578
+ return SPDX_NULL_LICENSES.has(s.trim().toLowerCase()) ? void 0 : s;
4579
+ }
4580
+ /**
4581
+ * Fill a package's missing name/version from its purl when parseable. Never
4582
+ * overwrites values the source BOM already provided (e.g. an SPDX versionInfo
4583
+ * is authoritative over a purl whose @segment is a git commit).
4584
+ */
4585
+ function enrichFromPurl(pkg) {
4586
+ if (!pkg.purl) return;
4587
+ const parsed = parsePurl(pkg.purl);
4588
+ if (!parsed) return;
4589
+ if (!pkg.version && parsed.version) pkg.version = parsed.version;
4590
+ if (!pkg.name && parsed.name) pkg.name = parsed.name;
4591
+ }
4592
+ /**
4593
+ * Assemble a schema-valid BillOfMaterials from normalized parts, enforcing the
4594
+ * three-tier discipline: the packages/model/dataset extension is kept only when
4595
+ * it matches bomType; a mismatched extension is silently dropped (the schema
4596
+ * forbids it, so carrying it would produce invalid output).
4597
+ */
4598
+ function buildBom(parts) {
4599
+ const bom = {
4600
+ bomType: parts.bomType,
4601
+ format: parts.format
4602
+ };
4603
+ if (parts.ref !== void 0) bom.ref = parts.ref;
4604
+ if (parts.document !== void 0) bom.document = parts.document;
4605
+ if (parts.uniqueId !== void 0) bom.uniqueId = parts.uniqueId;
4606
+ if (parts.hashes !== void 0 && parts.hashes.length > 0) bom.hashes = parts.hashes;
4607
+ if (parts.license !== void 0) bom.license = parts.license;
4608
+ if (parts.bomType === BOMType.Sbom && parts.packages !== void 0) bom.packages = parts.packages;
4609
+ if (parts.bomType === BOMType.AIModel && parts.model !== void 0) bom.model = parts.model;
4610
+ if (parts.bomType === BOMType.Dataset && parts.dataset !== void 0) bom.dataset = parts.dataset;
4611
+ return bom;
4612
+ }
4613
+ /**
4614
+ * Detect and parse a BOM document into normalized form. Validates input size
4615
+ * FIRST (security boundary), then dispatches on the detected format. Throws on
4616
+ * oversized or undetectable input.
4617
+ */
4618
+ function parseBom(input) {
4619
+ validateInputSize(input, CONVERTER);
4620
+ const obj = parseJSON(input);
4621
+ const detected = detectFormat(obj);
4622
+ if (!detected) throw new Error("bom-parser: could not detect a supported BOM format (expected CycloneDX or SPDX JSON)");
4623
+ const record = asRecord(obj) ?? {};
4624
+ switch (detected.format) {
4625
+ case "cyclonedx-ml": return {
4626
+ format: detected.format,
4627
+ normalized: parseMLBOM(record)
4628
+ };
4629
+ case "cyclonedx": return {
4630
+ format: detected.format,
4631
+ normalized: parseCycloneDX(record)
4632
+ };
4633
+ case "spdx": return {
4634
+ format: detected.format,
4635
+ normalized: parseSPDX(record)
4636
+ };
4637
+ case "spdx-3-ai": {
4638
+ const [first] = parseSPDX3(record).subjects;
4639
+ if (!first) throw new Error("bom-parser: SPDX-3 document carries no AI/dataset subjects");
4640
+ return {
4641
+ format: detected.format,
4642
+ normalized: first.bom
4643
+ };
4644
+ }
4645
+ }
4646
+ }
4647
+ //#endregion
3814
4648
  //#region converters/cyclonedx-to-hdf/typescript/converter.ts
3815
4649
  const CVSS_METHODS = /* @__PURE__ */ new Set([
3816
4650
  "CVSSv2",
@@ -3828,7 +4662,7 @@ function maxImpact(ratings) {
3828
4662
  for (const rating of ratings) {
3829
4663
  let impact;
3830
4664
  if (rating.method && CVSS_METHODS.has(rating.method) && rating.score !== void 0 && rating.score !== null) impact = rating.score / 10;
3831
- else impact = severityToImpact(rating.severity ?? "medium");
4665
+ else impact = severityToImpact$1(rating.severity ?? "medium");
3832
4666
  if (impact > max) max = impact;
3833
4667
  }
3834
4668
  return max;
@@ -3863,19 +4697,29 @@ function flattenComponents(components) {
3863
4697
  return result;
3864
4698
  }
3865
4699
  /**
4700
+ * Reports whether any (possibly nested) component is a machine-learning-model,
4701
+ * i.e. the CycloneDX document is an AI-BOM.
4702
+ */
4703
+ function hasMLModelComponent(components) {
4704
+ return flattenComponents(components).some((comp) => comp.type === "machine-learning-model");
4705
+ }
4706
+ /**
3866
4707
  * Converts CycloneDX SBOM/VEX JSON to HDF format.
3867
4708
  *
3868
4709
  * @param input - CycloneDX JSON string
3869
4710
  * @returns HDF JSON string
3870
4711
  */
3871
- async function convertCyclonedxToHdf(input) {
4712
+ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
3872
4713
  if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
3873
4714
  validateInputSize(input, "cyclonedx");
3874
4715
  const bom = parseJSON(input);
3875
4716
  if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
3876
4717
  if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
3877
4718
  if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
3878
- if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create --from <sbom-file> --component-name <name>");
4719
+ if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) {
4720
+ if (hasMLModelComponent(bom.components ?? [])) throw new Error("cyclonedx: this file is a CycloneDX AI-BOM (machine-learning-model inventory) with no vulnerabilities; to import it into a system document, use:\n hdf system create <file> --from cyclonedx-mlbom");
4721
+ throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create <sbom-file> --component-name <name>");
4722
+ }
3879
4723
  const resultsChecksum = await inputChecksum(input);
3880
4724
  const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
3881
4725
  const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
@@ -3917,13 +4761,12 @@ async function convertCyclonedxToHdf(input) {
3917
4761
  data: fixParts.join("\n\n")
3918
4762
  });
3919
4763
  const affects = vuln.affects ?? [];
3920
- const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, {
3921
- codeDesc: formatCodeDesc$4(componentLookup, affect.ref),
3922
- startTime: scanTime
3923
- })) : [createResult(ResultStatus.Failed, void 0, {
3924
- codeDesc: `Vulnerability ${vuln.id}`,
4764
+ const toResult = (codeDesc) => ({
4765
+ status: ResultStatus.Failed,
4766
+ codeDesc,
3925
4767
  startTime: scanTime
3926
- })];
4768
+ });
4769
+ const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
3927
4770
  const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
3928
4771
  const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
3929
4772
  const controlType = deriveControlTypeFromTags(nist);
@@ -3931,16 +4774,29 @@ async function convertCyclonedxToHdf(input) {
3931
4774
  requirements.push(req);
3932
4775
  }
3933
4776
  const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
3934
- const targetName = bom.metadata?.component?.name ?? "";
4777
+ const targetComponent = {
4778
+ name: bom.metadata?.component?.name ?? "",
4779
+ type: TargetType.Application
4780
+ };
4781
+ if (bom.metadata?.component?.version) targetComponent.version = bom.metadata.component.version;
4782
+ const parsedBom = parseBom(input);
4783
+ const bomParts = {
4784
+ bomType: BOMType.Sbom,
4785
+ format: "cyclonedx",
4786
+ uniqueId: parsedBom.normalized.uniqueId,
4787
+ document: JSON.parse(input)
4788
+ };
4789
+ if (parsedBom.normalized.packages && parsedBom.normalized.packages.length > 0) bomParts.packages = parsedBom.normalized.packages;
4790
+ const componentBom = buildBom(bomParts);
3935
4791
  return buildHdfResults({
3936
4792
  generatorName: "cyclonedx-to-hdf",
3937
- converterVersion: "1.0.0",
4793
+ converterVersion,
3938
4794
  toolName: "CycloneDX",
3939
4795
  toolFormat: "JSON",
3940
4796
  baselines: [baseline],
3941
4797
  components: [{
3942
- name: targetName,
3943
- type: TargetType.Application
4798
+ ...targetComponent,
4799
+ boms: [componentBom]
3944
4800
  }],
3945
4801
  timestamp: scanTime
3946
4802
  });
@@ -3954,7 +4810,7 @@ async function convertCyclonedxToHdf(input) {
3954
4810
  */
3955
4811
  function convertHdfToCsv(input) {
3956
4812
  validateInputSize(input, "hdf-to-csv");
3957
- const hdf = parseJSON(input);
4813
+ const hdf = parseHdf(input);
3958
4814
  if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
3959
4815
  if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
3960
4816
  const rows = [];
@@ -3974,6 +4830,11 @@ function convertHdfToCsv(input) {
3974
4830
  */
3975
4831
  function createRow(baseline, requirement, target) {
3976
4832
  const description = requirement.descriptions.find((d) => d.label === "default")?.data || "";
4833
+ const check = descriptionByLabel(requirement, "check");
4834
+ const fix = descriptionByLabel(requirement, "fix");
4835
+ const rationale = descriptionByLabel(requirement, "rationale");
4836
+ const code = requirement.code ?? "";
4837
+ const references = flattenRefs(requirement.refs);
3977
4838
  const severity = getSeverity(requirement);
3978
4839
  const firstResult = requirement.results[0];
3979
4840
  const status = firstResult?.status || "";
@@ -3989,6 +4850,11 @@ function createRow(baseline, requirement, target) {
3989
4850
  "Requirement ID": requirement.id,
3990
4851
  "Requirement Title": requirement.title || "",
3991
4852
  "Description": description,
4853
+ "Check": check,
4854
+ "Fix": fix,
4855
+ "Rationale": rationale,
4856
+ "Code": code,
4857
+ "References": references,
3992
4858
  "Severity": severity,
3993
4859
  "Impact": requirement.impact,
3994
4860
  "Status": String(status),
@@ -4011,21 +4877,826 @@ function getSeverity(requirement) {
4011
4877
  if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
4012
4878
  }
4013
4879
  }
4014
- const impact = requirement.impact;
4015
- if (impact >= .7) return "high";
4016
- if (impact >= .4) return "medium";
4017
- return "low";
4880
+ const impact = requirement.impact;
4881
+ if (impact >= .7) return "high";
4882
+ if (impact >= .4) return "medium";
4883
+ return "low";
4884
+ }
4885
+ /**
4886
+ * Extract array values from tags object and join with semicolons
4887
+ */
4888
+ function extractArrayFromTags(tags, key) {
4889
+ if (!tags || typeof tags !== "object") return "";
4890
+ const value = tags[key];
4891
+ if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
4892
+ return "";
4893
+ }
4894
+ /**
4895
+ * Get the data of the first description matching a label. Empty when absent.
4896
+ */
4897
+ function descriptionByLabel(requirement, label) {
4898
+ return requirement.descriptions.find((d) => d.label === label)?.data || "";
4899
+ }
4900
+ /**
4901
+ * Flatten a requirement's refs to one string: each Reference rendered as its
4902
+ * url/uri (or a string `ref`); array-form refs are skipped. Joined with '; ' to
4903
+ * match the NIST/CCI column convention.
4904
+ */
4905
+ function flattenRefs(refs) {
4906
+ if (!Array.isArray(refs)) return "";
4907
+ const out = [];
4908
+ for (const r of refs) {
4909
+ if (!r || typeof r !== "object") continue;
4910
+ const o = r;
4911
+ if (typeof o.url === "string") out.push(o.url);
4912
+ else if (typeof o.uri === "string") out.push(o.uri);
4913
+ else if (typeof o.ref === "string") out.push(o.ref);
4914
+ }
4915
+ return out.join("; ");
4916
+ }
4917
+ //#endregion
4918
+ //#region shared/typescript/exportmap.ts
4919
+ /**
4920
+ * Generic, source-tool-agnostic mapping helpers shared by the HDF export
4921
+ * converters (hdf-to-ecs, hdf-to-splunk, ...).
4922
+ *
4923
+ * The export converters deliberately operate on generically-parsed JSON rather
4924
+ * than typed HDF structs, so their output can be held byte-identical with the
4925
+ * Go implementations. This module centralizes the generic accessors, the status
4926
+ * roll-up, the requirement/document field extraction, and the canonical
4927
+ * (key-sorted, HTML-unescaped) line serialization. Target-specific event shaping
4928
+ * (ECS field names, CIM field names, envelopes) stays in each converter.
4929
+ */
4930
+ function asMap(v) {
4931
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
4932
+ }
4933
+ function asArr(v) {
4934
+ return Array.isArray(v) ? v : void 0;
4935
+ }
4936
+ function getStr(m, key) {
4937
+ const v = m?.[key];
4938
+ return typeof v === "string" ? v : "";
4939
+ }
4940
+ function setIf(m, key, val) {
4941
+ if (val !== "") m[key] = val;
4942
+ }
4943
+ function stringSlice(v) {
4944
+ if (typeof v === "string") return [v];
4945
+ if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
4946
+ return [];
4947
+ }
4948
+ /** Most-significant status across a requirement's results[] (lossless). */
4949
+ function worstOfResults(req) {
4950
+ const results = asArr(req.results) ?? [];
4951
+ const precedence = [
4952
+ "failed",
4953
+ "error",
4954
+ "passed",
4955
+ "notReviewed",
4956
+ "notApplicable"
4957
+ ];
4958
+ const present = /* @__PURE__ */ new Set();
4959
+ for (const rRaw of results) {
4960
+ const r = asMap(rRaw);
4961
+ if (r) present.add(getStr(r, "status"));
4962
+ }
4963
+ for (const s of precedence) if (present.has(s)) return s;
4964
+ return "notReviewed";
4965
+ }
4966
+ /**
4967
+ * Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
4968
+ * 'error' is indeterminate (not a compliance failure) and every other value is
4969
+ * non-failing. Single shared definition of "failing" for the suppression axis
4970
+ * and the per-exporter outcome maps.
4971
+ */
4972
+ function isFailing(status) {
4973
+ return status === "failed";
4974
+ }
4975
+ /** Resolve the status context for a requirement. */
4976
+ function statusOf(req) {
4977
+ const raw = worstOfResults(req);
4978
+ const effective = getStr(req, "effectiveStatus");
4979
+ const overrides = asArr(req.statusOverrides);
4980
+ const rollup = effective !== "" ? effective : raw;
4981
+ return {
4982
+ raw,
4983
+ effective,
4984
+ rollup,
4985
+ overridden: overrides !== void 0 && overrides.length > 0 || "effectiveStatus" in req,
4986
+ suppressed: isFailing(raw) && !isFailing(rollup)
4987
+ };
4988
+ }
4989
+ function firstComponent(doc) {
4990
+ const comps = asArr(doc.components);
4991
+ if (!comps || comps.length === 0) return void 0;
4992
+ return asMap(comps[0]);
4993
+ }
4994
+ function firstResultStartTime(req, fallback) {
4995
+ const results = asArr(req.results) ?? [];
4996
+ if (results.length > 0) {
4997
+ const st = getStr(asMap(results[0]), "startTime");
4998
+ if (st !== "") return st;
4999
+ }
5000
+ return fallback;
5001
+ }
5002
+ function defaultDescription(req) {
5003
+ const descs = asArr(req.descriptions) ?? [];
5004
+ for (const dRaw of descs) {
5005
+ const d = asMap(dRaw);
5006
+ if (d && getStr(d, "label") === "default") return getStr(d, "data");
5007
+ }
5008
+ return "";
5009
+ }
5010
+ function firstRefURL(req) {
5011
+ const refs = asArr(req.refs) ?? [];
5012
+ for (const rRaw of refs) {
5013
+ const url = getStr(asMap(rRaw), "url");
5014
+ if (url !== "") return url;
5015
+ }
5016
+ return "";
5017
+ }
5018
+ /** Deterministic event id: component | baseline | control. */
5019
+ function eventID(component, baselineName, controlID) {
5020
+ let comp = "";
5021
+ if (component) comp = getStr(component, "componentId") || getStr(component, "name");
5022
+ return [
5023
+ comp,
5024
+ baselineName,
5025
+ controlID
5026
+ ].join("|");
5027
+ }
5028
+ /**
5029
+ * Build the lossless hdf.* namespace shared by the export converters: promoted
5030
+ * snake_case scalars plus the full requirement sub-objects preserved verbatim.
5031
+ * `status` is the lossless results roll-up (statusOf().raw); `suppressed` is the
5032
+ * acceptance axis (statusOf().suppressed).
5033
+ */
5034
+ function buildHDFBlock(req, baseline, status, overridden, suppressed, generator, tool, converterVersion) {
5035
+ const hdf = {
5036
+ status,
5037
+ overridden,
5038
+ suppressed,
5039
+ exporter_version: converterVersion
5040
+ };
5041
+ setIf(hdf, "control_id", getStr(req, "id"));
5042
+ setIf(hdf, "baseline", getStr(baseline, "name"));
5043
+ if ("effectiveStatus" in req) hdf.effective_status = req.effectiveStatus;
5044
+ if ("effectiveImpact" in req) hdf.effective_impact = req.effectiveImpact;
5045
+ if ("impact" in req) hdf.impact = req.impact;
5046
+ if ("severity" in req) hdf.severity = req.severity;
5047
+ if ("disposition" in req) hdf.disposition = req.disposition;
5048
+ const tags = asMap(req.tags);
5049
+ if (tags?.nist !== void 0) hdf.nist = tags.nist;
5050
+ if (tags?.cci !== void 0) hdf.cci = tags.cci;
5051
+ for (const [src, dst] of Object.entries({
5052
+ tags: "tags",
5053
+ cvss: "cvss",
5054
+ cwe: "cwe",
5055
+ epss: "epss",
5056
+ kev: "kev",
5057
+ affectedPackages: "affected_packages",
5058
+ descriptions: "descriptions",
5059
+ results: "results",
5060
+ statusOverrides: "status_overrides",
5061
+ poams: "poams",
5062
+ code: "code",
5063
+ refs: "refs"
5064
+ })) if (src in req) hdf[dst] = req[src];
5065
+ if (generator) hdf.generator = generator;
5066
+ if (tool) hdf.tool = tool;
5067
+ return hdf;
5068
+ }
5069
+ /**
5070
+ * A numeric value that must serialize with an explicit decimal point, so a
5071
+ * whole-number renders as `10.0` rather than the integer `10`. Some consumers
5072
+ * type-check strictly (OCSF's `float_t` rejects an integer-shaped token).
5073
+ * JSON.stringify cannot emit `10.0` for a JS number, so the value is carried in
5074
+ * this wrapper (passed through canonicalize untouched) and rendered as a bare
5075
+ * numeric token by stringifyLine. Go's counterpart is encoding/json's json.Number.
5076
+ */
5077
+ var RawNumber = class {
5078
+ constructor(token) {
5079
+ this.token = token;
5080
+ }
5081
+ };
5082
+ /** Wrap a number as a RawNumber whose token always bears a decimal point. */
5083
+ function floatNumber(f) {
5084
+ let s = String(f);
5085
+ if (!/[.eE]/.test(s)) s += ".0";
5086
+ return new RawNumber(s);
5087
+ }
5088
+ const RAWNUM_MARK = String.fromCharCode(1);
5089
+ const RAWNUM_RE = /"\\u0001(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\\u0001"/g;
5090
+ /**
5091
+ * Compare two strings by Unicode code point. This matches Go's `encoding/json`
5092
+ * key ordering, which is a bytewise comparison of the UTF-8 encoding \u2014 and UTF-8
5093
+ * byte order is identical to code-point order. JS's default Array.sort() instead
5094
+ * compares UTF-16 code units, which diverges for supplementary-plane (non-BMP)
5095
+ * characters: a lead surrogate (0xD800\u20130xDBFF) sorts before a BMP char in
5096
+ * [0xE000,0xFFFF] under UTF-16, but a non-BMP code point (\u22650x10000) sorts after
5097
+ * it under UTF-8. Iterating the string yields whole code points (surrogate pairs
5098
+ * combined), so this comparator is correct for the full Unicode range.
5099
+ */
5100
+ function byCodePoint(a, b) {
5101
+ const ai = [...a];
5102
+ const bi = [...b];
5103
+ const n = Math.min(ai.length, bi.length);
5104
+ for (let i = 0; i < n; i++) {
5105
+ const ca = ai[i]?.codePointAt(0) ?? 0;
5106
+ const cb = bi[i]?.codePointAt(0) ?? 0;
5107
+ if (ca !== cb) return ca - cb;
5108
+ }
5109
+ return ai.length - bi.length;
5110
+ }
5111
+ /**
5112
+ * Recursively sort object keys in Go's map-key order (bytewise UTF-8 / code
5113
+ * point) so the emitted JSON is byte-identical to Go's encoder.
5114
+ */
5115
+ function canonicalize$1(v) {
5116
+ if (v instanceof RawNumber) return v;
5117
+ if (Array.isArray(v)) return v.map(canonicalize$1);
5118
+ const m = asMap(v);
5119
+ if (m) {
5120
+ const out = {};
5121
+ for (const k of Object.keys(m).sort(byCodePoint)) out[k] = canonicalize$1(m[k]);
5122
+ return out;
5123
+ }
5124
+ return v;
5125
+ }
5126
+ /**
5127
+ * Emit compact JSON matching Go's encoder byte-for-byte. A RawNumber is rendered
5128
+ * as a bare numeric token (Go's json.Number) via an SOH-delimited placeholder
5129
+ * that is stripped back to the token afterward. Go's encoding/json escapes
5130
+ * U+2028/U+2029 (JSONP safety) while JSON.stringify emits them raw, so we escape
5131
+ * them here. Key ordering is handled upstream by canonicalize (code point order).
5132
+ */
5133
+ function stringifyLine(v) {
5134
+ return JSON.stringify(v, (_key, val) => val instanceof RawNumber ? RAWNUM_MARK + val.token + RAWNUM_MARK : val).replace(RAWNUM_RE, "$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
5135
+ }
5136
+ /**
5137
+ * Shared entry-point driver for the HDF→SIEM exporters: runs the identical
5138
+ * prologue (validate, parse, baselines extraction, doc-level context), fans out
5139
+ * one output object per requirement via `build`, and joins the canonical NDJSON
5140
+ * lines (byte-identical with the Go `Export` driver). `converterName` prefixes
5141
+ * the missing-baselines error and drives validateInputSize.
5142
+ */
5143
+ function runExport(input, converterName, build) {
5144
+ validateInputSize(input, converterName);
5145
+ const doc = parseHdf(input);
5146
+ const baselines = asArr(doc.baselines);
5147
+ if (!baselines) throw new Error(`${converterName}: invalid HDF structure: missing baselines field`);
5148
+ const docTimestamp = getStr(doc, "timestamp");
5149
+ const tool = asMap(doc.tool);
5150
+ const generator = asMap(doc.generator);
5151
+ const component = firstComponent(doc);
5152
+ const lines = [];
5153
+ for (const bRaw of baselines) {
5154
+ const baseline = asMap(bRaw);
5155
+ if (!baseline) continue;
5156
+ const reqs = asArr(baseline.requirements) ?? [];
5157
+ for (const rRaw of reqs) {
5158
+ const req = asMap(rRaw);
5159
+ if (!req) continue;
5160
+ lines.push(stringifyLine(canonicalize$1(build(req, baseline, docTimestamp, tool, generator, component))));
5161
+ }
5162
+ }
5163
+ return lines.length === 0 ? "" : lines.join("\n") + "\n";
5164
+ }
5165
+ /**
5166
+ * The first cvss[].source that looks like a CVE id (case-insensitive "CVE-"
5167
+ * prefix), or ''. Shared by the exporters that key vulnerability identity off
5168
+ * the CVE (splunk, ocsf).
5169
+ */
5170
+ function firstCVE(cvssList) {
5171
+ for (const c of cvssList) {
5172
+ const src = getStr(asMap(c), "source");
5173
+ if (src.toUpperCase().startsWith("CVE-")) return src;
5174
+ }
5175
+ return "";
5176
+ }
5177
+ /**
5178
+ * Parse an HDF RFC3339 timestamp into integer epoch seconds (Splunk HEC `time`)
5179
+ * via the canonical parser, returning undefined when empty/unparseable. Integer
5180
+ * epoch keeps Go and TypeScript byte-identical.
5181
+ */
5182
+ function epochSeconds(s) {
5183
+ const d = parseTimestamp(s);
5184
+ if (d === null) return void 0;
5185
+ return Math.floor(d.getTime() / 1e3);
5186
+ }
5187
+ /**
5188
+ * Parse an HDF RFC3339 timestamp into integer epoch milliseconds (OCSF `time`)
5189
+ * via the canonical parser, returning undefined when empty/unparseable.
5190
+ */
5191
+ function epochMillis(s) {
5192
+ const d = parseTimestamp(s);
5193
+ if (d === null) return void 0;
5194
+ return d.getTime();
5195
+ }
5196
+ //#endregion
5197
+ //#region converters/hdf-to-ecs/typescript/converter.ts
5198
+ /**
5199
+ * HDF Results -> Elastic Common Schema (ECS) NDJSON exporter.
5200
+ *
5201
+ * One ECS event per Evaluated_Requirement, in the hybrid shape from ADR-0002:
5202
+ * a core-ECS-native projection plus a lossless hdf.* block, hot filter scalars
5203
+ * promoted flat. Output is plain NDJSON (LF-delimited, trailing newline), ECS
5204
+ * 9.4.0.
5205
+ *
5206
+ * Status is raw-primary: event.outcome carries the RAW verdict (a waived failure
5207
+ * is still failure), and hdf.suppressed is the separate acceptance axis. The
5208
+ * canonical "still actionable" query is
5209
+ * event.outcome:"failure" AND hdf.suppressed:false.
5210
+ *
5211
+ * Generic JSON access, the status roll-up, requirement/document field
5212
+ * extraction, and the canonical line serialization are shared with the other
5213
+ * export converters via ../../../shared/typescript/exportmap.js; only
5214
+ * ECS-specific event shaping lives here. Output is held byte-identical with the
5215
+ * Go implementation via key-sorted, HTML-unescaped compact JSON.
5216
+ */
5217
+ const ECS_VERSION = "9.4.0";
5218
+ function convertHdfToEcs(input, converterVersion = "0.1.0") {
5219
+ return runExport(input, "hdf-to-ecs", (req, baseline, docTimestamp, tool, generator, component) => buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5220
+ }
5221
+ function buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5222
+ const st = statusOf(req);
5223
+ const outcome = statusToOutcome(st.raw);
5224
+ const controlID = getStr(req, "id");
5225
+ const baselineName = getStr(baseline, "name");
5226
+ const title = getStr(req, "title");
5227
+ const cvssList = asArr(req.cvss);
5228
+ const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
5229
+ const categories = ["configuration"];
5230
+ if (hasCVSS) categories.push("vulnerability");
5231
+ const event = {
5232
+ kind: "state",
5233
+ category: categories,
5234
+ type: ["info"],
5235
+ outcome,
5236
+ id: eventID(component, baselineName, controlID),
5237
+ dataset: "hdf.findings",
5238
+ module: "hdf"
5239
+ };
5240
+ const obj = {
5241
+ "@timestamp": firstResultStartTime(req, docTimestamp),
5242
+ ecs: { version: ECS_VERSION },
5243
+ event,
5244
+ message: (title + " — " + st.raw).trim()
5245
+ };
5246
+ const observer = buildObserver(tool, generator);
5247
+ if (observer) obj.observer = observer;
5248
+ const host = buildHost(component);
5249
+ if (host) {
5250
+ obj.host = host;
5251
+ const related = buildRelated(component);
5252
+ if (related) obj.related = related;
5253
+ }
5254
+ obj.rule = buildRule(req, baseline, controlID, title);
5255
+ if (hasCVSS) obj.vulnerability = buildVulnerability$1(cvssList, req, tool);
5256
+ const threat = buildThreat(req);
5257
+ if (threat) obj.threat = threat;
5258
+ obj.hdf = buildHDFBlock(req, baseline, st.raw, st.overridden, st.suppressed, generator, tool, converterVersion);
5259
+ return obj;
5260
+ }
5261
+ function buildObserver(tool, generator) {
5262
+ let name = getStr(tool, "name");
5263
+ let version = getStr(tool, "version");
5264
+ if (name === "" && generator) {
5265
+ name = getStr(generator, "name");
5266
+ version = getStr(generator, "version");
5267
+ }
5268
+ if (name === "") return void 0;
5269
+ const observer = {
5270
+ name,
5271
+ type: "scanner"
5272
+ };
5273
+ if (version !== "") observer.version = version;
5274
+ const product = getStr(tool, "format");
5275
+ if (product !== "") observer.product = product;
5276
+ return observer;
5277
+ }
5278
+ function buildHost(component) {
5279
+ if (!component) return void 0;
5280
+ const host = {};
5281
+ const name = getStr(component, "fqdn") || getStr(component, "name");
5282
+ if (name !== "") host.name = name;
5283
+ setIf(host, "id", getStr(component, "componentId"));
5284
+ setIf(host, "ip", getStr(component, "ipAddress"));
5285
+ setIf(host, "mac", getStr(component, "macAddress"));
5286
+ const os = {};
5287
+ setIf(os, "name", getStr(component, "osName"));
5288
+ setIf(os, "version", getStr(component, "osVersion"));
5289
+ if (Object.keys(os).length > 0) host.os = os;
5290
+ return Object.keys(host).length === 0 ? void 0 : host;
5291
+ }
5292
+ function buildRelated(component) {
5293
+ const related = {};
5294
+ const name = getStr(component, "fqdn") || getStr(component, "name");
5295
+ if (name !== "") related.hosts = [name];
5296
+ const ip = getStr(component, "ipAddress");
5297
+ if (ip !== "") related.ip = [ip];
5298
+ return Object.keys(related).length === 0 ? void 0 : related;
5299
+ }
5300
+ function buildRule(req, baseline, controlID, title) {
5301
+ const rule = { id: controlID };
5302
+ if (title !== "") rule.name = title;
5303
+ setIf(rule, "description", defaultDescription(req));
5304
+ setIf(rule, "ruleset", getStr(baseline, "name"));
5305
+ setIf(rule, "version", getStr(baseline, "version"));
5306
+ setIf(rule, "reference", firstRefURL(req));
5307
+ return rule;
5308
+ }
5309
+ function buildVulnerability$1(cvssList, req, tool) {
5310
+ const first = asMap(cvssList[0]) ?? {};
5311
+ const vuln = {};
5312
+ const source = getStr(first, "source");
5313
+ if (source !== "") {
5314
+ vuln.id = source;
5315
+ if (source.toUpperCase().startsWith("CVE-")) vuln.enumeration = "CVE";
5316
+ } else setIf(vuln, "id", getStr(req, "id"));
5317
+ vuln.classification = "CVSS";
5318
+ const score = {};
5319
+ if ("baseScore" in first) score.base = first.baseScore;
5320
+ setIf(score, "version", getStr(first, "version"));
5321
+ if (Object.keys(score).length > 0) vuln.score = score;
5322
+ const severity = getStr(first, "baseSeverity") || getStr(req, "severity");
5323
+ if (severity !== "") vuln.severity = severity;
5324
+ const vendor = getStr(tool, "name");
5325
+ if (vendor !== "") vuln.scanner = { vendor };
5326
+ setIf(vuln, "description", defaultDescription(req));
5327
+ return vuln;
5328
+ }
5329
+ function buildThreat(req) {
5330
+ const tags = asMap(req.tags);
5331
+ if (!tags) return void 0;
5332
+ const techniques = [];
5333
+ for (const key of [
5334
+ "mitre_attack",
5335
+ "attack",
5336
+ "mitre_techniques"
5337
+ ]) for (const id of stringSlice(tags[key])) techniques.push({ id });
5338
+ if (techniques.length === 0) return void 0;
5339
+ return {
5340
+ framework: "MITRE ATT&CK",
5341
+ technique: techniques
5342
+ };
5343
+ }
5344
+ function statusToOutcome(status) {
5345
+ switch (status) {
5346
+ case "passed": return "success";
5347
+ case "failed": return "failure";
5348
+ default: return "unknown";
5349
+ }
5350
+ }
5351
+ //#endregion
5352
+ //#region converters/hdf-to-splunk/typescript/converter.ts
5353
+ /**
5354
+ * HDF Results -> Splunk HEC-envelope NDJSON, normalized to the Common
5355
+ * Information Model (CIM). See ADR-0004.
5356
+ *
5357
+ * One HEC event per Evaluated_Requirement, hybrid shape: flat CIM-named scalars
5358
+ * (signature/signature_id/cve/cvss/severity/dest/vendor_product/category) are
5359
+ * promoted to the top of the event payload; the hot query scalars among them
5360
+ * (signature/signature_id/dest/severity/cve/cvss + hdf_status + suppressed) are
5361
+ * additionally mirrored into the HEC indexed `fields` (surviving Splunk's
5362
+ * ~5000-char cutoff), plus a lossless hdf.* block.
5363
+ *
5364
+ * Status is raw-primary: hdf_status carries the RAW verdict (a waived failure is
5365
+ * still failed) and suppressed is the separate acceptance axis. The companion TA
5366
+ * (Splunk_TA_hdf) tags failed/error/CVE findings into the CIM Vulnerabilities
5367
+ * data model but excludes suppressed=true, so a waived control drops out while a
5368
+ * risk-adjusted still-failing control stays in. Canonical "still actionable"
5369
+ * query: hdf_status=failed suppressed=false.
5370
+ *
5371
+ * Generic access, the status roll-up, field extraction, the lossless hdf.*
5372
+ * block, and the canonical line serialization are shared with the other
5373
+ * exporters via exportmap; output is held byte-identical with the Go side.
5374
+ */
5375
+ const SOURCETYPE = "hdf:results";
5376
+ const SOURCE = "hdf-exporter";
5377
+ function convertHdfToSplunk(input, converterVersion = "0.1.0") {
5378
+ return runExport(input, "hdf-to-splunk", (req, baseline, docTimestamp, tool, generator, component) => buildHECEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5379
+ }
5380
+ function buildHECEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5381
+ const st = statusOf(req);
5382
+ const title = getStr(req, "title");
5383
+ const controlID = getStr(req, "id");
5384
+ const signature = title !== "" ? title : controlID;
5385
+ const dest = destHost(component);
5386
+ const sev = severity(req);
5387
+ const [cvss, hasCVSS] = maxCVSS(req);
5388
+ const cve = firstCVE(asArr(req.cvss) ?? []);
5389
+ const category = firstCWE(req);
5390
+ const vendorProduct = getStr(tool, "name");
5391
+ const event = {
5392
+ signature,
5393
+ hdf_status: st.raw,
5394
+ suppressed: st.suppressed,
5395
+ hdf: buildHDFBlock(req, baseline, st.raw, st.overridden, st.suppressed, generator, tool, converterVersion)
5396
+ };
5397
+ setIf(event, "signature_id", controlID);
5398
+ setIf(event, "dest", dest);
5399
+ setIf(event, "severity", sev);
5400
+ setIf(event, "cve", cve);
5401
+ setIf(event, "category", category);
5402
+ setIf(event, "vendor_product", vendorProduct);
5403
+ if (hasCVSS) event.cvss = cvss;
5404
+ const fields = {
5405
+ signature,
5406
+ hdf_status: st.raw,
5407
+ suppressed: st.suppressed
5408
+ };
5409
+ setIf(fields, "signature_id", controlID);
5410
+ setIf(fields, "dest", dest);
5411
+ setIf(fields, "severity", sev);
5412
+ setIf(fields, "cve", cve);
5413
+ if (hasCVSS) fields.cvss = cvss;
5414
+ const hec = {
5415
+ source: SOURCE,
5416
+ sourcetype: SOURCETYPE,
5417
+ event,
5418
+ fields
5419
+ };
5420
+ const t = epochSeconds(firstResultStartTime(req, docTimestamp));
5421
+ if (t !== void 0) hec.time = t;
5422
+ setIf(hec, "host", dest);
5423
+ return hec;
5424
+ }
5425
+ function destHost(component) {
5426
+ if (!component) return "";
5427
+ return getStr(component, "fqdn") || getStr(component, "name") || getStr(component, "ipAddress");
5428
+ }
5429
+ function severity(req) {
5430
+ if (typeof req.impact === "number") return impactToSeverity(req.impact);
5431
+ return normalizeSeverity(getStr(req, "severity"));
5432
+ }
5433
+ function normalizeSeverity(s) {
5434
+ switch (s) {
5435
+ case "critical":
5436
+ case "high":
5437
+ case "medium":
5438
+ case "low":
5439
+ case "informational": return s;
5440
+ default: return "informational";
5441
+ }
5442
+ }
5443
+ function maxCVSS(req) {
5444
+ const list = asArr(req.cvss) ?? [];
5445
+ let max = 0;
5446
+ let found = false;
5447
+ for (const c of list) {
5448
+ const m = asMap(c);
5449
+ if (m && typeof m.baseScore === "number") {
5450
+ if (!found || m.baseScore > max) max = m.baseScore;
5451
+ found = true;
5452
+ }
5453
+ }
5454
+ return [max, found];
4018
5455
  }
4019
- /**
4020
- * Extract array values from tags object and join with semicolons
4021
- */
4022
- function extractArrayFromTags(tags, key) {
4023
- if (!tags || typeof tags !== "object") return "";
4024
- const value = tags[key];
4025
- if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
5456
+ function firstCWE(req) {
5457
+ const list = asArr(req.cwe) ?? [];
5458
+ for (const c of list) if (typeof c === "string" && c !== "") return c;
4026
5459
  return "";
4027
5460
  }
4028
5461
  //#endregion
5462
+ //#region converters/hdf-to-ocsf/typescript/converter.ts
5463
+ /**
5464
+ * HDF Results -> OCSF (Open Cybersecurity Schema Framework) Finding NDJSON.
5465
+ * See the ADR-0002 addendum (HDF -> OCSF, wvc3.5). Pinned to OCSF v1.8.0.
5466
+ *
5467
+ * One Finding per requirement: a CVE finding -> Vulnerability Finding
5468
+ * (class_uid 2002), any other -> Compliance Finding (class_uid 2003), both in
5469
+ * the Findings category (2). Status model is raw-primary: compliance.status_id
5470
+ * carries the RAW verdict (a failed control stays Fail even when waived). The
5471
+ * acceptance axis rides the base finding status_id: a raw-failing finding that
5472
+ * an override drove non-failing (waiver/falsePositive/attestation) -> 3
5473
+ * Suppressed, everything else -> 1 New; a riskAdjustment/operationalRequirement/
5474
+ * poam that leaves the finding failing stays New (actionable, only re-scored).
5475
+ * The exact override chain is preserved in unmapped.hdf_requirement (+ comment).
5476
+ * Canonical "still actionable" query: compliance.status_id=3 AND status_id=1.
5477
+ *
5478
+ * Shares the generic access / status roll-up / field extraction / canonical
5479
+ * line serialization with the other exporters via exportmap; output is held
5480
+ * byte-identical with the Go implementation.
5481
+ */
5482
+ const OCSF_VERSION = "1.8.0";
5483
+ const CATEGORY_FINDINGS = 2;
5484
+ const CLASS_COMPLIANCE = 2003;
5485
+ const CLASS_VULNERABILITY = 2002;
5486
+ const ACTIVITY_CREATE = 1;
5487
+ const STATUS_NEW = 1;
5488
+ const STATUS_SUPPRESSED = 3;
5489
+ function convertHdfToOcsf(input, converterVersion = "0.1.0") {
5490
+ return runExport(input, "hdf-to-ocsf", (req, baseline, docTimestamp, tool, generator, component) => buildFinding(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5491
+ }
5492
+ function buildFinding(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5493
+ const st = statusOf(req);
5494
+ const cvssList = asArr(req.cvss);
5495
+ const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
5496
+ const title = getStr(req, "title");
5497
+ const controlID = getStr(req, "id");
5498
+ const cls = hasCVSS ? CLASS_VULNERABILITY : CLASS_COMPLIANCE;
5499
+ const findingInfo = { uid: controlID };
5500
+ setIf(findingInfo, "title", title);
5501
+ setIf(findingInfo, "desc", defaultDescription(req));
5502
+ if (hasCVSS) {
5503
+ const tags = frameworkTags(req);
5504
+ if (tags.length > 0) findingInfo.tags = tags;
5505
+ }
5506
+ const finding = {
5507
+ category_uid: CATEGORY_FINDINGS,
5508
+ class_uid: cls,
5509
+ type_uid: cls * 100 + ACTIVITY_CREATE,
5510
+ activity_id: ACTIVITY_CREATE,
5511
+ severity_id: severityID(req),
5512
+ status_id: st.suppressed ? STATUS_SUPPRESSED : STATUS_NEW,
5513
+ metadata: buildMetadata$1(tool, generator, converterVersion),
5514
+ finding_info: findingInfo,
5515
+ unmapped: { hdf_requirement: req }
5516
+ };
5517
+ finding.time = epochMillis(firstResultStartTime(req, docTimestamp)) ?? 0;
5518
+ setIf(finding, "comment", overrideComment(req));
5519
+ const device = buildDevice(component);
5520
+ if (device) finding.device = device;
5521
+ if (hasCVSS) finding.vulnerabilities = buildVulnerabilities(cvssList, req);
5522
+ else finding.compliance = buildCompliance(req, baseline, title, st.raw);
5523
+ return finding;
5524
+ }
5525
+ function severityID(req) {
5526
+ if (typeof req.impact === "number") return severityIDFromString(impactToSeverity(req.impact));
5527
+ return severityIDFromString(getStr(req, "severity"));
5528
+ }
5529
+ function severityIDFromString(s) {
5530
+ switch (s.toLowerCase()) {
5531
+ case "critical": return 5;
5532
+ case "high": return 4;
5533
+ case "medium": return 3;
5534
+ case "low": return 2;
5535
+ case "informational":
5536
+ case "info":
5537
+ case "none": return 1;
5538
+ default: return 0;
5539
+ }
5540
+ }
5541
+ function complianceStatusID(rawStatus) {
5542
+ switch (rawStatus) {
5543
+ case "passed": return 1;
5544
+ case "failed": return 3;
5545
+ default: return 2;
5546
+ }
5547
+ }
5548
+ function complianceStatusCaption(statusID) {
5549
+ switch (statusID) {
5550
+ case 1: return "Pass";
5551
+ case 3: return "Fail";
5552
+ default: return "Warning";
5553
+ }
5554
+ }
5555
+ function overrideComment(req) {
5556
+ const disposition = getStr(req, "disposition");
5557
+ const overrides = asArr(req.statusOverrides) ?? [];
5558
+ if (disposition === "" && overrides.length === 0) return "";
5559
+ let reason = "";
5560
+ if (overrides.length > 0) reason = getStr(asMap(overrides[0]), "reason");
5561
+ if (disposition !== "" && reason !== "") return `${disposition}: ${reason}`;
5562
+ if (disposition !== "") return disposition;
5563
+ return reason;
5564
+ }
5565
+ function buildMetadata$1(tool, generator, converterVersion) {
5566
+ const metadata = { version: OCSF_VERSION };
5567
+ let name = getStr(tool, "name");
5568
+ let version = getStr(tool, "version");
5569
+ let vendor = getStr(tool, "format");
5570
+ if (name === "" && generator) {
5571
+ name = getStr(generator, "name");
5572
+ version = getStr(generator, "version");
5573
+ }
5574
+ if (name === "") {
5575
+ name = "hdf-to-ocsf";
5576
+ version = converterVersion;
5577
+ vendor = "";
5578
+ }
5579
+ const product = { name };
5580
+ setIf(product, "version", version);
5581
+ setIf(product, "vendor_name", vendor);
5582
+ metadata.product = product;
5583
+ return metadata;
5584
+ }
5585
+ function buildDevice(component) {
5586
+ if (!component) return void 0;
5587
+ const device = { type_id: 0 };
5588
+ setIf(device, "name", getStr(component, "name"));
5589
+ setIf(device, "hostname", getStr(component, "fqdn"));
5590
+ setIf(device, "ip", getStr(component, "ipAddress"));
5591
+ setIf(device, "uid", getStr(component, "componentId"));
5592
+ const osName = getStr(component, "osName");
5593
+ if (osName !== "") {
5594
+ const os = {
5595
+ name: osName,
5596
+ type_id: osTypeID(osName)
5597
+ };
5598
+ setIf(os, "version", getStr(component, "osVersion"));
5599
+ device.os = os;
5600
+ }
5601
+ return Object.keys(device).length === 1 ? void 0 : device;
5602
+ }
5603
+ const OS_LINUX = [
5604
+ "linux",
5605
+ "rhel",
5606
+ "red hat",
5607
+ "ubuntu",
5608
+ "centos",
5609
+ "debian",
5610
+ "fedora",
5611
+ "suse"
5612
+ ];
5613
+ const OS_MAC = [
5614
+ "mac",
5615
+ "darwin",
5616
+ "os x"
5617
+ ];
5618
+ function osTypeID(osName) {
5619
+ const n = osName.toLowerCase();
5620
+ if (n.includes("windows")) return 100;
5621
+ if (OS_LINUX.some((k) => n.includes(k))) return 200;
5622
+ if (OS_MAC.some((k) => n.includes(k))) return 300;
5623
+ return 0;
5624
+ }
5625
+ function frameworkTags(req) {
5626
+ const tags = asMap(req.tags);
5627
+ const out = [];
5628
+ for (const key of ["nist", "cci"]) {
5629
+ const vals = stringSlice(tags?.[key]);
5630
+ if (vals.length > 0) out.push({
5631
+ name: key,
5632
+ values: vals
5633
+ });
5634
+ }
5635
+ return out;
5636
+ }
5637
+ function buildCompliance(req, baseline, title, rawStatus) {
5638
+ const statusID = complianceStatusID(rawStatus);
5639
+ const compliance = {
5640
+ status_id: statusID,
5641
+ status: complianceStatusCaption(statusID)
5642
+ };
5643
+ const controlID = getStr(req, "id");
5644
+ setIf(compliance, "control", controlID);
5645
+ const baselineName = getStr(baseline, "name");
5646
+ const tags = asMap(req.tags);
5647
+ const nist = stringSlice(tags?.nist);
5648
+ const cci = stringSlice(tags?.cci);
5649
+ const standards = [];
5650
+ if (baselineName !== "") standards.push(baselineName);
5651
+ if (nist.length > 0) standards.push("NIST SP 800-53");
5652
+ if (cci.length > 0) standards.push("CCI");
5653
+ if (standards.length > 0) compliance.standards = standards;
5654
+ const checks = [];
5655
+ if (controlID !== "") {
5656
+ const check = {
5657
+ uid: controlID,
5658
+ status_id: statusID
5659
+ };
5660
+ setIf(check, "name", title);
5661
+ if (baselineName !== "") check.standards = [baselineName];
5662
+ checks.push(check);
5663
+ }
5664
+ for (const id of nist) checks.push({
5665
+ uid: id,
5666
+ standards: ["NIST SP 800-53"]
5667
+ });
5668
+ for (const id of cci) checks.push({
5669
+ uid: id,
5670
+ standards: ["CCI"]
5671
+ });
5672
+ if (checks.length > 0) compliance.checks = checks;
5673
+ return compliance;
5674
+ }
5675
+ function buildVulnerabilities(cvssList, req) {
5676
+ const cve = {};
5677
+ cve.uid = firstCVE(cvssList) || getStr(req, "id");
5678
+ const cvssArr = [];
5679
+ for (const c of cvssList) {
5680
+ const m = asMap(c);
5681
+ if (!m) continue;
5682
+ const entry = {};
5683
+ if (typeof m.baseScore === "number") entry.base_score = floatNumber(m.baseScore);
5684
+ setIf(entry, "version", getStr(m, "version"));
5685
+ setIf(entry, "vector_string", getStr(m, "baseVector"));
5686
+ setIf(entry, "severity", getStr(m, "baseSeverity"));
5687
+ cvssArr.push(entry);
5688
+ }
5689
+ if (cvssArr.length > 0) cve.cvss = cvssArr;
5690
+ const cwes = stringSlice(req.cwe);
5691
+ if (cwes.length > 0) cve.related_cwes = cwes.map((id) => ({ uid: id }));
5692
+ const vuln = { cve };
5693
+ setIf(vuln, "title", getStr(req, "title"));
5694
+ setIf(vuln, "desc", defaultDescription(req));
5695
+ const ref = firstRefURL(req);
5696
+ if (ref !== "") vuln.references = [ref];
5697
+ return [vuln];
5698
+ }
5699
+ //#endregion
4029
5700
  //#region converters/splunk-to-hdf/typescript/converter.ts
4030
5701
  /**
4031
5702
  * Map a Splunk result status string to the HDF ResultStatus enum.
@@ -4041,11 +5712,12 @@ function mapStatus$1(status) {
4041
5712
  }
4042
5713
  /**
4043
5714
  * Convert a Splunk descriptions object (`{ key: value }`) to an array of
4044
- * HDF Description objects.
5715
+ * HDF Description objects. Labels are sorted: Go decodes the same object into
5716
+ * a map, so sorting is the only ordering both languages can agree on.
4045
5717
  */
4046
5718
  function convertDescriptions(descriptions) {
4047
5719
  if (!descriptions || typeof descriptions !== "object") return [];
4048
- return Object.entries(descriptions).map(([label, data]) => createDescription(label, data));
5720
+ return Object.entries(descriptions).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([label, data]) => createDescription(label, data));
4049
5721
  }
4050
5722
  /**
4051
5723
  * Convert Splunk profile groups (which use `controls`) to HDF
@@ -4069,7 +5741,7 @@ function convertGroups(groups) {
4069
5741
  * @param input - JSON string of a SplunkEvent array
4070
5742
  * @returns HDF Results JSON string
4071
5743
  */
4072
- async function convertSplunkToHdf(input) {
5744
+ async function convertSplunkToHdf(input, converterVersion = "1.0.0") {
4073
5745
  validateInputSize(input, "splunk");
4074
5746
  const resultsChecksum = await inputChecksum(input);
4075
5747
  const events = parseJSON(input);
@@ -4081,9 +5753,10 @@ async function convertSplunkToHdf(input) {
4081
5753
  eventsByGuid.get(guid).push(event);
4082
5754
  }
4083
5755
  const allBaselines = [];
4084
- let targetName = "unknown";
4085
- let targetRelease = "";
4086
- for (const [, guidEvents] of eventsByGuid) {
5756
+ const components = [];
5757
+ let statistics;
5758
+ for (const guid of [...eventsByGuid.keys()].sort()) {
5759
+ const guidEvents = eventsByGuid.get(guid);
4087
5760
  const headers = [];
4088
5761
  const profiles = [];
4089
5762
  const controls = [];
@@ -4100,8 +5773,13 @@ async function convertSplunkToHdf(input) {
4100
5773
  }
4101
5774
  if (headers.length !== 1) throw new Error(`Expected 1 header event, got ${headers.length}`);
4102
5775
  const header = headers[0];
4103
- targetName = header.platform.name;
4104
- targetRelease = header.platform.release;
5776
+ const component = {
5777
+ name: header.platform.name,
5778
+ type: TargetType.Host
5779
+ };
5780
+ if (header.platform.release) component.osVersion = header.platform.release;
5781
+ components.push(component);
5782
+ statistics = convertStatistics(header.statistics);
4105
5783
  const controlsByProfile = /* @__PURE__ */ new Map();
4106
5784
  for (const control of controls) {
4107
5785
  const sha = control.meta.profile_sha256 ?? "";
@@ -4111,16 +5789,24 @@ async function convertSplunkToHdf(input) {
4111
5789
  for (const profile of profiles) {
4112
5790
  const requirements = (controlsByProfile.get(profile.sha256) ?? []).map((control) => {
4113
5791
  const descriptions = convertDescriptions(control.descriptions);
4114
- const results = Array.isArray(control.results) ? control.results.map((result) => createResult(mapStatus$1(result.status), result.message, {
4115
- codeDesc: result.code_desc,
4116
- startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
4117
- runTime: result.run_time,
4118
- exception: result.exception,
4119
- backtrace: result.backtrace
4120
- })) : [];
5792
+ if (descriptions.length === 0) descriptions.push(createDescription("default", control.desc ?? ""));
5793
+ const results = Array.isArray(control.results) ? control.results.map((result) => {
5794
+ const res = createResult(mapStatus$1(result.status), result.skip_message || result.message, {
5795
+ codeDesc: result.code_desc,
5796
+ startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
5797
+ runTime: result.run_time,
5798
+ exception: result.exception,
5799
+ backtrace: result.backtrace
5800
+ });
5801
+ if (!res.message) delete res.message;
5802
+ if (result.resource) res.resource = result.resource;
5803
+ return res;
5804
+ }) : [];
4121
5805
  const options = { tags: control.tags ?? {} };
4122
5806
  if (control.source_location) options.sourceLocation = control.source_location;
4123
5807
  const req = createRequirement(control.id, control.title, descriptions, control.impact, results, options);
5808
+ if (!req.title) delete req.title;
5809
+ if (control.code) req.code = control.code;
4124
5810
  const nistTagsRaw = control.tags?.["nist"];
4125
5811
  const controlType = deriveControlTypeFromTags(Array.isArray(nistTagsRaw) ? nistTagsRaw.filter((t) => typeof t === "string") : []);
4126
5812
  if (controlType !== void 0) req.controlType = controlType;
@@ -4133,23 +5819,34 @@ async function convertSplunkToHdf(input) {
4133
5819
  if (profile.version) baselineOptions.version = profile.version;
4134
5820
  if (profile.summary) baselineOptions.summary = profile.summary;
4135
5821
  if (groups.length > 0) baselineOptions.groups = groups;
4136
- allBaselines.push(createMinimalBaseline(profile.name, requirements, baselineOptions));
5822
+ if (profile.sha256) baselineOptions.integrity = {
5823
+ algorithm: HashAlgorithm.Sha256,
5824
+ checksum: profile.sha256
5825
+ };
5826
+ const baseline = createMinimalBaseline(profile.name, requirements, baselineOptions);
5827
+ if (profile.copyright) baseline.copyright = profile.copyright;
5828
+ if (profile.maintainer) baseline.maintainer = profile.maintainer;
5829
+ if (profile.license) baseline.license = profile.license;
5830
+ allBaselines.push(baseline);
4137
5831
  }
4138
5832
  }
4139
- return serializeHdf({
5833
+ return buildHdfResults({
5834
+ generatorName: "splunk-to-hdf",
5835
+ converterVersion,
5836
+ toolName: "Splunk",
4140
5837
  baselines: allBaselines,
4141
- components: [{
4142
- name: targetName,
4143
- type: TargetType.Host,
4144
- osName: targetRelease || void 0,
4145
- labels: {}
4146
- }],
4147
- generator: {
4148
- name: "splunk-to-hdf",
4149
- version: "1.0.0"
4150
- }
5838
+ components,
5839
+ statistics,
5840
+ timestamp: /* @__PURE__ */ new Date()
4151
5841
  });
4152
5842
  }
5843
+ /** Extract the assessment duration from a Splunk header's statistics blob. */
5844
+ function convertStatistics(stats) {
5845
+ if (!stats || typeof stats !== "object") return void 0;
5846
+ const out = {};
5847
+ if (typeof stats["duration"] === "number") out.duration = stats["duration"];
5848
+ return out;
5849
+ }
4153
5850
  //#endregion
4154
5851
  //#region converters/hdf-to-xml/typescript/converter.ts
4155
5852
  /**
@@ -4159,7 +5856,7 @@ async function convertSplunkToHdf(input) {
4159
5856
  */
4160
5857
  function convertHdfToXml(input) {
4161
5858
  validateInputSize(input, "hdf-to-xml");
4162
- const hdf = parseJSON(input);
5859
+ const hdf = parseHdf(input);
4163
5860
  if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
4164
5861
  if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
4165
5862
  return buildXml({ HdfResults: transformHdfToXmlObject(hdf) });
@@ -4171,6 +5868,20 @@ function wrap$1(value) {
4171
5868
  return { "#text": value };
4172
5869
  }
4173
5870
  /**
5871
+ * Transform a polymorphic Reference ({url} | {uri} | {ref: string}) into an
5872
+ * XML-friendly object. Array-form `ref` values are skipped.
5873
+ */
5874
+ function transformReference(r) {
5875
+ const out = {};
5876
+ if (r && typeof r === "object") {
5877
+ const o = r;
5878
+ if (typeof o.url === "string") out.url = wrap$1(o.url);
5879
+ else if (typeof o.uri === "string") out.uri = wrap$1(o.uri);
5880
+ else if (typeof o.ref === "string") out.ref = wrap$1(o.ref);
5881
+ }
5882
+ return out;
5883
+ }
5884
+ /**
4174
5885
  * Transform HDF object to XML-compatible structure
4175
5886
  * Converts arrays to repeated singular elements
4176
5887
  */
@@ -4180,6 +5891,16 @@ function transformHdfToXmlObject(hdf) {
4180
5891
  name: wrap$1(baseline.name),
4181
5892
  ...baseline.version && { version: wrap$1(baseline.version) },
4182
5893
  ...baseline.title && { title: wrap$1(baseline.title) },
5894
+ ...baseline.summary && { summary: wrap$1(baseline.summary) },
5895
+ ...baseline.status && { status: wrap$1(baseline.status) },
5896
+ ...baseline.resultsChecksum && { resultsChecksum: {
5897
+ algorithm: wrap$1(baseline.resultsChecksum.algorithm),
5898
+ value: wrap$1(baseline.resultsChecksum.value)
5899
+ } },
5900
+ ...baseline.originalChecksum && { originalChecksum: {
5901
+ algorithm: wrap$1(baseline.originalChecksum.algorithm),
5902
+ value: wrap$1(baseline.originalChecksum.value)
5903
+ } },
4183
5904
  ...baseline.integrity && { integrity: {
4184
5905
  ...baseline.integrity.algorithm && { algorithm: wrap$1(baseline.integrity.algorithm) },
4185
5906
  ...baseline.integrity.checksum && { checksum: wrap$1(baseline.integrity.checksum) }
@@ -4189,11 +5910,13 @@ function transformHdfToXmlObject(hdf) {
4189
5910
  })) };
4190
5911
  else result.baselines = {};
4191
5912
  if (hdf.components && hdf.components.length > 0) result.components = { target: hdf.components.map((target) => ({
5913
+ ...target.componentId && { componentId: wrap$1(target.componentId) },
4192
5914
  name: wrap$1(target.name),
4193
5915
  type: wrap$1(target.type),
5916
+ ...target.hostname && { hostname: wrap$1(target.hostname) },
4194
5917
  ...target.fqdn && { fqdn: wrap$1(target.fqdn) },
4195
- ...target.ipAddress && { ipAddress: wrap$1(target.ipAddress) },
4196
- ...target.hostname && { hostname: wrap$1(target.hostname) }
5918
+ ...target.domain && { domain: wrap$1(target.domain) },
5919
+ ...target.ipAddress && { ipAddress: wrap$1(target.ipAddress) }
4197
5920
  })) };
4198
5921
  if (hdf.statistics) {
4199
5922
  result.statistics = {};
@@ -4201,7 +5924,10 @@ function transformHdfToXmlObject(hdf) {
4201
5924
  if (hdf.statistics.requirements) result.statistics.requirements = hdf.statistics.requirements;
4202
5925
  }
4203
5926
  if (hdf.timestamp) result.timestamp = wrap$1(typeof hdf.timestamp === "string" ? hdf.timestamp : hdf.timestamp.toISOString());
4204
- if (hdf.generator) result.generator = hdf.generator;
5927
+ if (hdf.generator) result.generator = {
5928
+ name: wrap$1(hdf.generator.name),
5929
+ ...hdf.generator.version && { version: wrap$1(hdf.generator.version) }
5930
+ };
4205
5931
  return result;
4206
5932
  }
4207
5933
  /**
@@ -4215,11 +5941,20 @@ function transformRequirement(req) {
4215
5941
  label: wrap$1(d.label),
4216
5942
  data: wrap$1(d.data)
4217
5943
  })) } },
5944
+ ...req.code && { code: wrap$1(req.code) },
5945
+ ...req.sourceLocation && { sourceLocation: {
5946
+ ...req.sourceLocation.ref !== void 0 && { ref: wrap$1(req.sourceLocation.ref) },
5947
+ ...req.sourceLocation.line !== void 0 && { line: wrap$1(req.sourceLocation.line) }
5948
+ } },
5949
+ ...req.controlType && { controlType: wrap$1(req.controlType) },
5950
+ ...req.verificationMethod && { verificationMethod: wrap$1(req.verificationMethod) },
5951
+ ...req.applicability && { applicability: wrap$1(req.applicability) },
5952
+ ...req.refs && req.refs.length > 0 && { refs: { ref: req.refs.map(transformReference) } },
4218
5953
  impact: wrap$1(req.impact)
4219
5954
  };
4220
5955
  if (req.tags && Object.keys(req.tags).length > 0) {
4221
5956
  const transformedTags = {};
4222
- for (const [key, value] of Object.entries(req.tags)) if (Array.isArray(value) && value.length > 0) transformedTags[key] = value.map((v) => wrap$1(v));
5957
+ for (const [key, value] of Object.entries(req.tags).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) if (Array.isArray(value) && value.length > 0) transformedTags[key] = value.map((v) => wrap$1(v));
4223
5958
  else if (!Array.isArray(value)) transformedTags[key] = wrap$1(value);
4224
5959
  if (Object.keys(transformedTags).length > 0) result.tags = transformedTags;
4225
5960
  }
@@ -4235,7 +5970,7 @@ function transformRequirement(req) {
4235
5970
  //#endregion
4236
5971
  //#region converters/gitlab-to-hdf/typescript/converter.ts
4237
5972
  function gitlabSeverityToImpact(severity) {
4238
- return severityToImpact(severity.toLowerCase());
5973
+ return severityToImpact$1(severity.toLowerCase());
4239
5974
  }
4240
5975
  function scanTypeToTargetType(scanType) {
4241
5976
  switch (scanType) {
@@ -4325,7 +6060,7 @@ function buildCodeDesc$4(scanType, location) {
4325
6060
  default: return `Location: ${JSON.stringify(location)}`;
4326
6061
  }
4327
6062
  }
4328
- async function convertGitlabToHdf(input) {
6063
+ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
4329
6064
  validateInputSize(input, "gitlab");
4330
6065
  const resultsChecksum = await inputChecksum(input);
4331
6066
  const report = parseJSON(input);
@@ -4400,7 +6135,7 @@ async function convertGitlabToHdf(input) {
4400
6135
  components,
4401
6136
  generator: {
4402
6137
  name: "gitlab-to-hdf",
4403
- version: "unknown"
6138
+ version: converterVersion
4404
6139
  },
4405
6140
  tool
4406
6141
  };
@@ -4453,6 +6188,18 @@ function groupFindings(findings) {
4453
6188
  return groups;
4454
6189
  }
4455
6190
  /**
6191
+ * Order ExtraData keys the way Go's map marshalling does (lexicographic), so the
6192
+ * embedded message string is byte-identical in both languages.
6193
+ */
6194
+ function canonicalize(value) {
6195
+ if (Array.isArray(value)) return value.map(canonicalize);
6196
+ if (value !== null && typeof value === "object") {
6197
+ const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
6198
+ return Object.fromEntries(entries.map(([k, v]) => [k, canonicalize(v)]));
6199
+ }
6200
+ return value;
6201
+ }
6202
+ /**
4456
6203
  * Build the Result.Message JSON from selected finding fields.
4457
6204
  */
4458
6205
  function buildMessage(f) {
@@ -4461,7 +6208,7 @@ function buildMessage(f) {
4461
6208
  Redacted: f.Redacted
4462
6209
  };
4463
6210
  if (f.VerificationError) msg.VerificationError = f.VerificationError;
4464
- if (f.ExtraData && Object.keys(f.ExtraData).length > 0) msg.ExtraData = f.ExtraData;
6211
+ if (f.ExtraData && Object.keys(f.ExtraData).length > 0) msg.ExtraData = canonicalize(f.ExtraData);
4465
6212
  return JSON.stringify(msg);
4466
6213
  }
4467
6214
  /**
@@ -4534,7 +6281,7 @@ function buildRequirement$12(reqID, findings) {
4534
6281
  * @param input - TruffleHog JSON/NDJSON string
4535
6282
  * @returns HDF JSON string
4536
6283
  */
4537
- async function convertTrufflehogToHdf(input) {
6284
+ async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
4538
6285
  if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
4539
6286
  validateInputSize(input, "trufflehog");
4540
6287
  const findings = parseFindings(input);
@@ -4554,7 +6301,7 @@ async function convertTrufflehogToHdf(input) {
4554
6301
  })],
4555
6302
  generator: {
4556
6303
  name: "trufflehog-to-hdf",
4557
- version: "1.0.0"
6304
+ version: converterVersion
4558
6305
  },
4559
6306
  tool: {
4560
6307
  name: "TruffleHog",
@@ -4605,7 +6352,7 @@ function formatCodeDesc$3(hostIP, hostURL, location, issueDetail, confidence) {
4605
6352
  * @param input - BurpSuite XML string
4606
6353
  * @returns HDF JSON string
4607
6354
  */
4608
- async function convertBurpsuiteToHdf(input) {
6355
+ async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
4609
6356
  if (!input || input.trim().length === 0) throw new Error("burpsuite: empty input");
4610
6357
  validateInputSize(input, "burpsuite");
4611
6358
  const resultsChecksum = await inputChecksum(input);
@@ -4645,7 +6392,7 @@ async function convertBurpsuiteToHdf(input) {
4645
6392
  }],
4646
6393
  generator: {
4647
6394
  name: "burpsuite-to-hdf",
4648
- version: "1.0.0"
6395
+ version: converterVersion
4649
6396
  },
4650
6397
  tool
4651
6398
  };
@@ -4706,6 +6453,26 @@ const IMPACT_MAPPING$4 = {
4706
6453
  low: .3,
4707
6454
  informational: 0
4708
6455
  };
6456
+ const NAMED_ENTITIES$2 = {
6457
+ amp: "&",
6458
+ lt: "<",
6459
+ gt: ">",
6460
+ quot: "\"",
6461
+ apos: "'"
6462
+ };
6463
+ /**
6464
+ * Decodes XML character references. The shared parser runs with
6465
+ * processEntities disabled to block entity-expansion attacks, which also leaves
6466
+ * the five predefined references undecoded — Go's encoding/xml decodes them, so
6467
+ * the raw text would otherwise differ between the two converters.
6468
+ */
6469
+ function decodeXmlEntities$2(s) {
6470
+ return s.replace(/&(#\d+|#x[0-9a-fA-F]+|amp|lt|gt|quot|apos);/g, (match, ref) => {
6471
+ if (ref.startsWith("#x") || ref.startsWith("#X")) return String.fromCodePoint(parseInt(ref.slice(2), 16));
6472
+ if (ref.startsWith("#")) return String.fromCodePoint(parseInt(ref.slice(1), 10));
6473
+ return NAMED_ENTITIES$2[ref] ?? match;
6474
+ });
6475
+ }
4709
6476
  /**
4710
6477
  * Maps metadata column names to row values by position index.
4711
6478
  * Mirrors the heimdall2 compileFindings function.
@@ -4720,7 +6487,7 @@ function compileFindings(parsed) {
4720
6487
  colNames.forEach((name, i) => {
4721
6488
  const val = values[i];
4722
6489
  if (val === null || val === void 0 || typeof val === "object") finding[name] = "";
4723
- else finding[name] = String(val);
6490
+ else finding[name] = decodeXmlEntities$2(String(val));
4724
6491
  });
4725
6492
  return finding;
4726
6493
  });
@@ -4795,10 +6562,11 @@ function buildRequirement$10(checkID, findings, hasStatus) {
4795
6562
  data: formatDesc$1(rep)
4796
6563
  }];
4797
6564
  const results = findings.map((f) => {
4798
- return createResult(hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed, void 0, {
6565
+ return {
6566
+ status: hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed,
4799
6567
  codeDesc: f["Details"] ?? "",
4800
6568
  startTime: parseDate(f["Date"] ?? "")
4801
- });
6569
+ };
4802
6570
  });
4803
6571
  const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
4804
6572
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -4814,7 +6582,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
4814
6582
  * @param input - DBProtect XML string
4815
6583
  * @returns HDF JSON string
4816
6584
  */
4817
- async function convertDbprotectToHdf(input) {
6585
+ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
4818
6586
  if (!input || input.trim().length === 0) throw new Error("dbprotect: empty input");
4819
6587
  validateInputSize(input, "dbprotect");
4820
6588
  const resultsChecksum = await inputChecksum(input);
@@ -4842,16 +6610,19 @@ async function convertDbprotectToHdf(input) {
4842
6610
  const title = firstFinding["Job Name"] ?? "";
4843
6611
  const summary = formatSummary(firstFinding);
4844
6612
  const targetName = firstFinding["Asset"] ?? "";
6613
+ const baseline = createMinimalBaseline("DBProtect Scan", requirements, {
6614
+ resultsChecksum,
6615
+ title,
6616
+ summary
6617
+ });
6618
+ const policy = firstFinding["Policy"] ?? "";
6619
+ if (policy) baseline.version = policy;
4845
6620
  return buildHdfResults({
4846
6621
  generatorName: "dbprotect-to-hdf",
4847
- converterVersion: "1.0.0",
6622
+ converterVersion,
4848
6623
  toolName: "DBProtect",
4849
6624
  toolFormat: "XML",
4850
- baselines: [createMinimalBaseline("DBProtect Scan", requirements, {
4851
- resultsChecksum,
4852
- title,
4853
- summary
4854
- })],
6625
+ baselines: [baseline],
4855
6626
  components: [{
4856
6627
  name: targetName,
4857
6628
  type: TargetType.Host
@@ -5040,7 +6811,7 @@ function buildPackageTypeIndex(pkgs) {
5040
6811
  */
5041
6812
  function formatCodeDesc$2(vuln) {
5042
6813
  const packageName = vuln.packageName ?? "N/A";
5043
- const impactedVersions = vuln.impactedVersions && vuln.impactedVersions.length > 0 ? JSON.stringify(vuln.impactedVersions) : "N/A";
6814
+ const impactedVersions = vuln.impactedVersions && vuln.impactedVersions.length > 0 ? `[${vuln.impactedVersions.join(" ")}]` : "N/A";
5044
6815
  return `Package ${JSON.stringify(packageName)} should be updated to latest version above impacted versions ${impactedVersions}`;
5045
6816
  }
5046
6817
  /**
@@ -5061,10 +6832,11 @@ function buildRequirement$9(vuln, packageTypes, distro) {
5061
6832
  data: vuln.description
5062
6833
  }];
5063
6834
  const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5064
- const results = [createResult(ResultStatus.Failed, void 0, {
6835
+ const results = [{
6836
+ status: ResultStatus.Failed,
5065
6837
  codeDesc: formatCodeDesc$2(vuln),
5066
6838
  startTime
5067
- })];
6839
+ }];
5068
6840
  const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
5069
6841
  const controlType = deriveControlTypeFromTags(nist);
5070
6842
  if (controlType !== void 0) req.controlType = controlType;
@@ -5106,7 +6878,7 @@ function convertSingleResult(result, resultsChecksum) {
5106
6878
  * @param input - Twistlock JSON string
5107
6879
  * @returns HDF JSON string
5108
6880
  */
5109
- async function convertTwistlockToHdf(input) {
6881
+ async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
5110
6882
  if (!input || input.trim().length === 0) throw new Error("twistlock: empty input");
5111
6883
  validateInputSize(input, "twistlock");
5112
6884
  const resultsChecksum = await inputChecksum(input);
@@ -5117,18 +6889,19 @@ async function convertTwistlockToHdf(input) {
5117
6889
  else results = [parsed];
5118
6890
  if (results.length === 0) throw new Error("twistlock: no scan results found");
5119
6891
  const baselines = results.map((result) => convertSingleResult(result, resultsChecksum));
5120
- const targetName = results[0].name ?? results[0].repository ?? "";
6892
+ const component = {
6893
+ name: results[0].name ?? results[0].repository ?? "",
6894
+ type: TargetType.ContainerImage
6895
+ };
6896
+ const imageID = results[0].id;
6897
+ if (imageID) component.labels = { image: imageID };
5121
6898
  return buildHdfResults({
5122
6899
  generatorName: "twistlock-to-hdf",
5123
- converterVersion: "1.0.0",
6900
+ converterVersion,
5124
6901
  toolName: "Twistlock",
5125
6902
  toolFormat: "JSON",
5126
6903
  baselines,
5127
- components: [{
5128
- name: targetName,
5129
- type: TargetType.ContainerImage,
5130
- labels: { image: results[0]?.id ?? targetName }
5131
- }],
6904
+ components: [component],
5132
6905
  timestamp: /* @__PURE__ */ new Date()
5133
6906
  });
5134
6907
  }
@@ -5190,10 +6963,11 @@ function buildRequirement$8(finding, timestamp) {
5190
6963
  data: finding.vulnerability.recommendation
5191
6964
  });
5192
6965
  const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
5193
- const results = [createResult(ResultStatus.Failed, void 0, {
6966
+ const results = [{
6967
+ status: ResultStatus.Failed,
5194
6968
  codeDesc,
5195
6969
  startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
5196
- })];
6970
+ }];
5197
6971
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
5198
6972
  req.verificationMethod = VerificationMethodEnum.Automated;
5199
6973
  const controlType = deriveControlTypeFromTags(nist);
@@ -5227,7 +7001,7 @@ function buildAffectedPackageFromComponent(c) {
5227
7001
  * @param input - Dependency-Track FPF JSON string
5228
7002
  * @returns HDF JSON string
5229
7003
  */
5230
- async function convertDeptrackToHdf(input) {
7004
+ async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
5231
7005
  if (!input || input.trim().length === 0) throw new Error("deptrack: empty input");
5232
7006
  validateInputSize(input, "deptrack");
5233
7007
  const resultsChecksum = await inputChecksum(input);
@@ -5247,7 +7021,7 @@ async function convertDeptrackToHdf(input) {
5247
7021
  const targetName = parsed.project?.name ?? parsed.project?.uuid ?? "";
5248
7022
  return buildHdfResults({
5249
7023
  generatorName: "deptrack-to-hdf",
5250
- converterVersion: "1.0.0",
7024
+ converterVersion,
5251
7025
  toolName: "Dependency-Track",
5252
7026
  toolFormat: "JSON",
5253
7027
  baselines: [baseline],
@@ -5326,11 +7100,12 @@ function buildRequirement$7(entryID, entries, scanTime) {
5326
7100
  label: "default",
5327
7101
  data: formatDescription(rep)
5328
7102
  }];
5329
- const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, {
7103
+ const results = entries.map((entry) => ({
7104
+ status: ResultStatus.Failed,
5330
7105
  codeDesc: formatCodeDesc$1(entry),
5331
7106
  startTime: scanTime
5332
7107
  }));
5333
- const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
7108
+ const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact$1(rep.severity), results, { tags });
5334
7109
  const controlType = deriveControlTypeFromTags(nist);
5335
7110
  if (controlType !== void 0) req.controlType = controlType;
5336
7111
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -5383,7 +7158,7 @@ function buildAffectedPackageFromEntry(entry) {
5383
7158
  * @param input - JFrog Xray JSON string
5384
7159
  * @returns HDF JSON string
5385
7160
  */
5386
- async function convertJfrogXrayToHdf(input) {
7161
+ async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
5387
7162
  if (!input || input.trim().length === 0) throw new Error("jfrog-xray: empty input");
5388
7163
  validateInputSize(input, "jfrog-xray");
5389
7164
  const resultsChecksum = await inputChecksum(input);
@@ -5410,7 +7185,7 @@ async function convertJfrogXrayToHdf(input) {
5410
7185
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
5411
7186
  return buildHdfResults({
5412
7187
  generatorName: "jfrog-xray-to-hdf",
5413
- converterVersion: "1.0.0",
7188
+ converterVersion,
5414
7189
  toolName: "JFrog Xray",
5415
7190
  toolFormat: "JSON",
5416
7191
  baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
@@ -5519,7 +7294,7 @@ function targetNameFromReport(report) {
5519
7294
  * @param input - NeuVector scan JSON string
5520
7295
  * @returns HDF JSON string
5521
7296
  */
5522
- async function convertNeuvectorToHdf(input) {
7297
+ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
5523
7298
  if (!input || input.trim().length === 0) throw new Error("neuvector: empty input");
5524
7299
  validateInputSize(input, "neuvector");
5525
7300
  const scan = parseJSON(input);
@@ -5541,7 +7316,7 @@ async function convertNeuvectorToHdf(input) {
5541
7316
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
5542
7317
  return buildHdfResults({
5543
7318
  generatorName: "neuvector-to-hdf",
5544
- converterVersion: "1.0.0",
7319
+ converterVersion,
5545
7320
  toolName: "NeuVector",
5546
7321
  toolFormat: "JSON",
5547
7322
  baselines: [createMinimalBaseline("NeuVector Scan", requirements, {
@@ -5563,6 +7338,23 @@ async function convertNeuvectorToHdf(input) {
5563
7338
  //#region converters/fortify-to-hdf/typescript/converter.ts
5564
7339
  const NIST_REFERENCE_NAME = "Standards Mapping - NIST Special Publication 800-53 Revision 4";
5565
7340
  const NIST_PATTERN = /[a-zA-Z]{2}-\d+/g;
7341
+ const NAMED_ENTITIES$1 = {
7342
+ lt: "<",
7343
+ gt: ">",
7344
+ quot: "\"",
7345
+ apos: "'",
7346
+ amp: "&"
7347
+ };
7348
+ function decodeXmlEntities$1(s) {
7349
+ return s.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|(lt|gt|quot|apos|amp));/g, (match, dec, hex, name) => {
7350
+ if (dec !== void 0) return String.fromCodePoint(Number.parseInt(dec, 10));
7351
+ if (hex !== void 0) return String.fromCodePoint(Number.parseInt(hex, 16));
7352
+ return name !== void 0 ? NAMED_ENTITIES$1[name] ?? match : match;
7353
+ });
7354
+ }
7355
+ function stripFvdlMarkup(s) {
7356
+ return stripHTML(decodeXmlEntities$1(s));
7357
+ }
5566
7358
  function buildSnippetMap(snippets) {
5567
7359
  const map = /* @__PURE__ */ new Map();
5568
7360
  for (const s of snippets) if (s.id) map.set(s.id, s);
@@ -5612,8 +7404,8 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5612
7404
  if (nistTags.length === 0) nistTags = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1];
5613
7405
  const cciTags = nistToCci(nistTags);
5614
7406
  const tags = buildNistCciTags(nistTags, cciTags);
5615
- const title = stripHTML(desc.Abstract ?? "");
5616
- let explanationText = stripHTML(desc.Explanation ?? "");
7407
+ const title = stripFvdlMarkup(desc.Abstract ?? "");
7408
+ let explanationText = stripFvdlMarkup(desc.Explanation ?? "");
5617
7409
  if (!explanationText) explanationText = title;
5618
7410
  const descriptions = [{
5619
7411
  label: "default",
@@ -5621,7 +7413,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5621
7413
  }];
5622
7414
  if (desc.Recommendations) descriptions.push({
5623
7415
  label: "fix",
5624
- data: stripHTML(desc.Recommendations)
7416
+ data: stripFvdlMarkup(desc.Recommendations)
5625
7417
  });
5626
7418
  let impact = 0;
5627
7419
  if (vulns.length > 0) impact = parseFloat(vulns[0].ClassInfo?.DefaultSeverity ?? "0") / 5;
@@ -5656,7 +7448,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5656
7448
  * @param input - Fortify FVDL XML string
5657
7449
  * @returns HDF JSON string
5658
7450
  */
5659
- async function convertFortifyToHdf(input) {
7451
+ async function convertFortifyToHdf(input, converterVersion = "1.0.0") {
5660
7452
  if (!input || input.trim().length === 0) throw new Error("fortify: empty input");
5661
7453
  validateInputSize(input, "fortify");
5662
7454
  const resultsChecksum = await inputChecksum(input);
@@ -5690,7 +7482,7 @@ async function convertFortifyToHdf(input) {
5690
7482
  }],
5691
7483
  generator: {
5692
7484
  name: "fortify-to-hdf",
5693
- version: "1.0.0"
7485
+ version: converterVersion
5694
7486
  },
5695
7487
  tool: {
5696
7488
  name: "Fortify",
@@ -5721,16 +7513,12 @@ const REQUIRED_COLUMNS = [
5721
7513
  * Prisma uses non-standard severity names: "important" and "moderate"
5722
7514
  * in addition to the standard critical/high/medium/low.
5723
7515
  */
7516
+ const PRISMA_SEVERITY_ALIASES = {
7517
+ important: .9,
7518
+ moderate: .5
7519
+ };
5724
7520
  function getImpact$3(severity) {
5725
- switch (severity.toLowerCase()) {
5726
- case "critical": return 1;
5727
- case "important": return .9;
5728
- case "high": return .7;
5729
- case "moderate":
5730
- case "medium": return .5;
5731
- case "low": return .3;
5732
- default: return .5;
5733
- }
7521
+ return PRISMA_SEVERITY_ALIASES[severity.toLowerCase()] ?? severityToImpact(severity);
5734
7522
  }
5735
7523
  /**
5736
7524
  * Returns NIST tags based on whether the finding has a CVE.
@@ -5875,7 +7663,7 @@ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
5875
7663
  * @param input - Prisma Cloud CSV string
5876
7664
  * @returns HDF JSON string
5877
7665
  */
5878
- async function convertPrismaToHdf(input) {
7666
+ async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
5879
7667
  if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
5880
7668
  validateInputSize(input, "prisma");
5881
7669
  const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
@@ -5901,7 +7689,7 @@ async function convertPrismaToHdf(input) {
5901
7689
  }
5902
7690
  return buildHdfResults({
5903
7691
  generatorName: "prisma-to-hdf",
5904
- converterVersion: "1.0.0",
7692
+ converterVersion,
5905
7693
  toolName: "Prisma Cloud",
5906
7694
  toolFormat: "CSV",
5907
7695
  baselines,
@@ -6040,7 +7828,7 @@ function buildRequirement$3(vuln, initiated) {
6040
7828
  * @param input - Netsparker/Invicti XML string
6041
7829
  * @returns HDF JSON string
6042
7830
  */
6043
- async function convertNetsparkerToHdf(input) {
7831
+ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
6044
7832
  if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
6045
7833
  validateInputSize(input, "netsparker");
6046
7834
  const resultsChecksum = await inputChecksum(input);
@@ -6063,7 +7851,7 @@ async function convertNetsparkerToHdf(input) {
6063
7851
  }
6064
7852
  return buildHdfResults({
6065
7853
  generatorName: "netsparker-to-hdf",
6066
- converterVersion: "1.0.0",
7854
+ converterVersion,
6067
7855
  toolName,
6068
7856
  toolFormat: "XML",
6069
7857
  baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
@@ -6180,7 +7968,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
6180
7968
  * @param input - ScoutSuite JS/JSON string
6181
7969
  * @returns HDF JSON string
6182
7970
  */
6183
- async function convertScoutsuiteToHdf(input) {
7971
+ async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
6184
7972
  if (!input || input.trim().length === 0) throw new Error("scoutsuite: empty input");
6185
7973
  validateInputSize(input, "scoutsuite");
6186
7974
  const jsonStr = stripJSPrefix(input);
@@ -6197,7 +7985,7 @@ async function convertScoutsuiteToHdf(input) {
6197
7985
  });
6198
7986
  return buildHdfResults({
6199
7987
  generatorName: "scoutsuite-to-hdf",
6200
- converterVersion: "1.0.0",
7988
+ converterVersion,
6201
7989
  toolName: "ScoutSuite",
6202
7990
  toolFormat: "JSON",
6203
7991
  toolVersion: report.last_run.version,
@@ -6310,17 +8098,12 @@ function buildRequirementFromResult(result, filename) {
6310
8098
  const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6311
8099
  const score = result.result.score;
6312
8100
  const status = determineStatus(score);
6313
- let results;
6314
- if (result.result.sections.length > 0) results = result.result.sections.map((section) => {
6315
- return createResult(status, void 0, {
6316
- codeDesc: buildCodeDesc$1(section, scannerName),
6317
- startTime
6318
- });
6319
- });
6320
- else results = [createResult(status, void 0, {
6321
- codeDesc: `No sections reported by ${scannerName}`,
8101
+ const toResult = (codeDesc) => ({
8102
+ status,
8103
+ codeDesc,
6322
8104
  startTime
6323
- })];
8105
+ });
8106
+ const results = result.result.sections.length > 0 ? result.result.sections.map((section) => toResult(buildCodeDesc$1(section, scannerName))) : [toResult(`No sections reported by ${scannerName}`)];
6324
8107
  const req = createRequirement(result.sha256, filename, descriptions, scoreToImpact(score), results, { tags });
6325
8108
  req.verificationMethod = VerificationMethodEnum.Automated;
6326
8109
  const controlType = deriveControlTypeFromTags([...nist]);
@@ -6345,7 +8128,7 @@ function buildScannerBaseline(scannerName, results, shaMap, resultsChecksum) {
6345
8128
  * @param input - Conveyor JSON string
6346
8129
  * @returns HDF JSON string
6347
8130
  */
6348
- async function convertConveyorToHdf(input) {
8131
+ async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
6349
8132
  if (!input || input.trim().length === 0) throw new Error("conveyor: empty input");
6350
8133
  validateInputSize(input, "conveyor");
6351
8134
  const data = parseJSON(input);
@@ -6367,7 +8150,7 @@ async function convertConveyorToHdf(input) {
6367
8150
  }));
6368
8151
  return buildHdfResults({
6369
8152
  generatorName: "conveyor-to-hdf",
6370
- converterVersion: "1.0.0",
8153
+ converterVersion,
6371
8154
  toolName: "Conveyor",
6372
8155
  toolFormat: "JSON",
6373
8156
  baselines,
@@ -6406,18 +8189,29 @@ const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
6406
8189
  function veracodeSeverityToImpact(severity) {
6407
8190
  return IMPACT_MAPPING$2.get(severity) ?? .1;
6408
8191
  }
6409
- /** Get an attribute from a parsed XML node. */
8192
+ /** Get an attribute from a parsed XML node, entity-decoded. */
6410
8193
  function attr(node, name) {
6411
- return node[`${A}${name}`] ?? "";
6412
- }
6413
- /** Decode common XML/HTML character references (&#xHH; and &#NNN;). */
8194
+ const raw = node[`${A}${name}`];
8195
+ return raw === void 0 ? "" : decodeXmlEntities(raw);
8196
+ }
8197
+ const NAMED_ENTITIES = {
8198
+ lt: "<",
8199
+ gt: ">",
8200
+ quot: "\"",
8201
+ apos: "'",
8202
+ amp: "&"
8203
+ };
6414
8204
  function decodeXmlEntities(s) {
6415
- return s.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
8205
+ return s.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|(lt|gt|quot|apos|amp));/g, (match, dec, hex, name) => {
8206
+ if (dec !== void 0) return String.fromCodePoint(Number.parseInt(dec, 10));
8207
+ if (hex !== void 0) return String.fromCodePoint(Number.parseInt(hex, 16));
8208
+ return name !== void 0 ? NAMED_ENTITIES[name] ?? match : match;
8209
+ });
6416
8210
  }
6417
8211
  /** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
6418
8212
  function parseVeracodeTimestamp(ts) {
6419
8213
  if (!ts) return void 0;
6420
- return parseTimestamp(decodeXmlEntities(ts).replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
8214
+ return parseTimestamp(ts.replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
6421
8215
  }
6422
8216
  /** Format description paragraphs into text. */
6423
8217
  function formatDesc(desc) {
@@ -6550,12 +8344,20 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
6550
8344
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
6551
8345
  const results = cwes.flatMap((c) => {
6552
8346
  const staticflaws = c.staticflaws;
6553
- return ensureArray(staticflaws?.flaw).map((flaw) => createResult(ResultStatus.Failed, void 0, {
8347
+ return ensureArray(staticflaws?.flaw).map((flaw) => ({
8348
+ status: ResultStatus.Failed,
6554
8349
  codeDesc: formatFlawCodeDesc(flaw),
6555
8350
  startTime
6556
8351
  }));
6557
8352
  });
6558
- const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, { tags });
8353
+ const sourceRef = cwes.flatMap((c) => {
8354
+ const staticflaws = c.staticflaws;
8355
+ return ensureArray(staticflaws?.flaw).map((flaw) => attr(flaw, "sourcefile")).filter(Boolean);
8356
+ }).join("\n");
8357
+ const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, {
8358
+ tags,
8359
+ sourceLocation: sourceRef ? { ref: sourceRef } : void 0
8360
+ });
6559
8361
  const controlType = deriveControlTypeFromTags(nist);
6560
8362
  if (controlType !== void 0) req.controlType = controlType;
6561
8363
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6605,16 +8407,26 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
6605
8407
  if (cweID) extras.cwe = cweID;
6606
8408
  const tags = buildNistCciTags(nist, cciTags, extras);
6607
8409
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
6608
- const results = components.map((comp) => createResult(ResultStatus.Failed, void 0, {
8410
+ const results = components.map((comp) => ({
8411
+ status: ResultStatus.Failed,
6609
8412
  codeDesc: formatSCACodeDesc(comp),
6610
8413
  startTime
6611
8414
  }));
6612
8415
  const cveSummary = attr(vuln, "cve_summary");
6613
8416
  const cveId = attr(vuln, "cve_id");
6614
- const req = createRequirement(cveId, cveId, [{
8417
+ const descriptions = [{
6615
8418
  label: "default",
6616
8419
  data: cveSummary || ""
6617
- }], impact, results, { tags });
8420
+ }];
8421
+ const sourceRef = components.flatMap((comp) => {
8422
+ const filePaths = comp.file_paths;
8423
+ if (!filePaths) return [];
8424
+ return ensureArray(filePaths.file_path).map((fp) => attr(fp, "value")).filter(Boolean);
8425
+ }).join("\n");
8426
+ const req = createRequirement(cveId, cveId, descriptions, impact, results, {
8427
+ tags,
8428
+ sourceLocation: sourceRef ? { ref: sourceRef } : void 0
8429
+ });
6618
8430
  const controlType = deriveControlTypeFromTags(nist);
6619
8431
  if (controlType !== void 0) req.controlType = controlType;
6620
8432
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6626,10 +8438,13 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
6626
8438
  * @param input - Veracode DetailedReport XML string
6627
8439
  * @returns HDF JSON string
6628
8440
  */
6629
- async function convertVeracodeToHdf(input) {
8441
+ async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
6630
8442
  if (!input || input.trim().length === 0) throw new Error("veracode: empty input");
6631
8443
  validateInputSize(input, "veracode");
6632
- const parsed = parseXml(input, { attributeNamePrefix: "@_" });
8444
+ const parsed = parseXml(input, {
8445
+ attributeNamePrefix: "@_",
8446
+ trimValues: false
8447
+ });
6633
8448
  if ("summaryreport" in parsed) throw new Error("veracode: summary reports are not supported; use a detailed report");
6634
8449
  const report = parsed.detailedreport;
6635
8450
  if (!report) throw new Error("veracode: invalid XML - no <detailedreport> root element");
@@ -6658,7 +8473,7 @@ async function convertVeracodeToHdf(input) {
6658
8473
  const timestamp = parseVeracodeTimestamp(firstBuildDate);
6659
8474
  return buildHdfResults({
6660
8475
  generatorName: "veracode-to-hdf",
6661
- converterVersion: "1.0.0",
8476
+ converterVersion,
6662
8477
  toolName: "Veracode",
6663
8478
  toolFormat: "XML",
6664
8479
  baselines: [baseline],
@@ -6734,12 +8549,11 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
6734
8549
  data: stripHTML(impacts[0])
6735
8550
  });
6736
8551
  }
6737
- const codeDesc = cs.implementationStatus || "No implementation status provided";
6738
- const startTime = (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6739
- const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
6740
- codeDesc,
6741
- ...startTime ? { startTime } : {}
6742
- })], { tags });
8552
+ const req = createRequirement(id, title, descriptions, impact, [{
8553
+ status,
8554
+ codeDesc: cs.implementationStatus || "No implementation status provided",
8555
+ startTime: (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
8556
+ }], { tags });
6743
8557
  const controlType = deriveControlTypeFromTags(nist);
6744
8558
  if (controlType !== void 0) req.controlType = controlType;
6745
8559
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6754,7 +8568,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
6754
8568
  * @param input - Combined JSON string with secureScore and profiles
6755
8569
  * @returns HDF JSON string
6756
8570
  */
6757
- async function convertMsftSecureScoreToHdf(input) {
8571
+ async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
6758
8572
  if (!input || input.trim().length === 0) throw new Error("msft-secure-score: empty input");
6759
8573
  validateInputSize(input, "msft-secure-score");
6760
8574
  const combined = parseJSON(input);
@@ -6767,7 +8581,7 @@ async function convertMsftSecureScoreToHdf(input) {
6767
8581
  let tenantId = "";
6768
8582
  return buildHdfResults({
6769
8583
  generatorName: "msft-secure-score-to-hdf",
6770
- converterVersion: "1.0.0",
8584
+ converterVersion,
6771
8585
  toolName: "Microsoft Secure Score",
6772
8586
  toolFormat: "JSON",
6773
8587
  baselines: combined.secureScore.value.map((ss) => {
@@ -6775,14 +8589,17 @@ async function convertMsftSecureScoreToHdf(input) {
6775
8589
  const { items: limitedControlScores, truncated } = limitArray(ss.controlScores);
6776
8590
  /* v8 ignore next -- truncation only triggers with >100K items */
6777
8591
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedControlScores.length} controlScore items (original: ${ss.controlScores.length})`);
6778
- return createMinimalBaseline("Microsoft Secure Score", limitedControlScores.map((cs) => buildRequirement$1(cs, profiles, ss.createdDateTime)), {
8592
+ const requirements = limitedControlScores.map((cs) => buildRequirement$1(cs, profiles, ss.createdDateTime));
8593
+ const title = `Azure Secure Score report - Tenant ID: ${ss.azureTenantId} - Run ID: ${ss.id}`;
8594
+ return createMinimalBaseline("Microsoft Secure Score", requirements, {
6779
8595
  resultsChecksum,
6780
- title: `Azure Secure Score report - Tenant ID: ${ss.azureTenantId} - Run ID: ${ss.id}`
8596
+ title
6781
8597
  });
6782
8598
  }),
6783
8599
  components: [{
6784
8600
  name: `Azure Tenant: ${tenantId}`,
6785
8601
  type: TargetType.CloudAccount,
8602
+ provider: "azure",
6786
8603
  labels: {
6787
8604
  account: tenantId,
6788
8605
  provider: "azure"
@@ -6798,12 +8615,12 @@ async function convertMsftSecureScoreToHdf(input) {
6798
8615
  * Delegates base conversion to the generic SARIF converter and enriches
6799
8616
  * the output with MSDO-specific metadata.
6800
8617
  */
6801
- async function convertMsftDefenderDevopsToHdf(input) {
8618
+ async function convertMsftDefenderDevopsToHdf(input, converterVersion = "1.0.0") {
6802
8619
  validateInputSize(input, "msft-defender-devops");
6803
8620
  const raw = parseJSON(input);
6804
8621
  if (!raw || !Array.isArray(raw.runs)) throw new Error("Invalid MSDO SARIF structure: missing or invalid runs field");
6805
8622
  const { components, runEnrichments } = extractEnrichments(raw);
6806
- const hdfJson = await convertSarifToHdf(input);
8623
+ const hdfJson = await convertSarifToHdf(input, converterVersion);
6807
8624
  const result = JSON.parse(hdfJson);
6808
8625
  synthesizeNoFindingsPlaceholders(result);
6809
8626
  applyEnrichments(result, components, runEnrichments);
@@ -6821,8 +8638,7 @@ function extractEnrichments(raw) {
6821
8638
  const target = {
6822
8639
  name: repoNameFromURI(vcp.repositoryUri),
6823
8640
  type: TargetType.Repository,
6824
- url: vcp.repositoryUri,
6825
- labels: {}
8641
+ url: vcp.repositoryUri
6826
8642
  };
6827
8643
  if (vcp.branch) target.branch = vcp.branch;
6828
8644
  if (vcp.revisionId) target.commit = vcp.revisionId;
@@ -6949,10 +8765,14 @@ function buildRequirement(assessmentID, assessments, scanTime) {
6949
8765
  function buildResultFromAssessment(a, scanTime) {
6950
8766
  const status = mapStatus(a.properties.status.code);
6951
8767
  const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
6952
- return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, {
8768
+ const message = a.properties.status.description || a.properties.status.cause || void 0;
8769
+ const result = {
8770
+ status,
6953
8771
  codeDesc,
6954
8772
  startTime: scanTime
6955
- });
8773
+ };
8774
+ if (message !== void 0) result.message = message;
8775
+ return result;
6956
8776
  }
6957
8777
  /**
6958
8778
  * Converts Microsoft Defender for Cloud assessment output to HDF format.
@@ -6960,7 +8780,7 @@ function buildResultFromAssessment(a, scanTime) {
6960
8780
  * @param input - JSON string containing Azure Security Assessments API response
6961
8781
  * @returns HDF JSON string
6962
8782
  */
6963
- async function convertMsftDefenderCloudToHdf(input) {
8783
+ async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0") {
6964
8784
  validateInputSize(input, "msft-defender-cloud");
6965
8785
  const scanTime = /* @__PURE__ */ new Date();
6966
8786
  const resultsChecksum = await inputChecksum(input);
@@ -6997,7 +8817,7 @@ async function convertMsftDefenderCloudToHdf(input) {
6997
8817
  });
6998
8818
  return buildHdfResults({
6999
8819
  generatorName: "msft-defender-cloud-to-hdf",
7000
- converterVersion: "1.0.0",
8820
+ converterVersion,
7001
8821
  toolName: "Microsoft Defender for Cloud",
7002
8822
  toolFormat: "JSON",
7003
8823
  baselines: [baseline],
@@ -7019,7 +8839,7 @@ const IMPACT_MAPPING = {
7019
8839
  /**
7020
8840
  * Maps MDE severity to HDF impact value.
7021
8841
  */
7022
- function severityToImpact$1(severity) {
8842
+ function severityToImpact$2(severity) {
7023
8843
  return IMPACT_MAPPING[severity.toLowerCase()] ?? .5;
7024
8844
  }
7025
8845
  /**
@@ -7112,7 +8932,7 @@ function resolveStartTime(alert, scanTime) {
7112
8932
  * Converts a single MDE alert to an HDF EvaluatedRequirement.
7113
8933
  */
7114
8934
  function alertToRequirement(alert, scanTime) {
7115
- const impact = severityToImpact$1(alert.severity);
8935
+ const impact = severityToImpact$2(alert.severity);
7116
8936
  const status = statusToResultStatus(alert.status, alert.classification);
7117
8937
  const codeDesc = formatEvidence(alert.evidence ?? []);
7118
8938
  const results = [createResult(status, formatMessage(alert), {
@@ -7142,7 +8962,7 @@ function alertToRequirement(alert, scanTime) {
7142
8962
  * @param input - JSON string of MDE alert response
7143
8963
  * @returns HDF JSON string
7144
8964
  */
7145
- async function convertMsftDefenderEndpointToHdf(input) {
8965
+ async function convertMsftDefenderEndpointToHdf(input, converterVersion = "1.0.0") {
7146
8966
  validateInputSize(input, "msft-defender-endpoint");
7147
8967
  const resultsChecksum = await inputChecksum(input);
7148
8968
  const response = parseJSON(input);
@@ -7165,7 +8985,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
7165
8985
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", scanTime));
7166
8986
  return buildHdfResults({
7167
8987
  generatorName: "msft-defender-endpoint-to-hdf",
7168
- converterVersion: "1.0.0",
8988
+ converterVersion,
7169
8989
  toolName: "Microsoft Defender for Endpoint",
7170
8990
  baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
7171
8991
  components,
@@ -7183,7 +9003,8 @@ const BUILD_OPTIONS = {
7183
9003
  ignoreAttributes: false,
7184
9004
  format: true,
7185
9005
  indentBy: " ",
7186
- suppressEmptyNode: false
9006
+ suppressEmptyNode: false,
9007
+ suppressBooleanAttributes: false
7187
9008
  };
7188
9009
  /**
7189
9010
  * Convert HDF Results JSON to XCCDF 1.2 Benchmark XML.
@@ -7193,7 +9014,7 @@ const BUILD_OPTIONS = {
7193
9014
  */
7194
9015
  function convertHdfToXccdf(input) {
7195
9016
  validateInputSize(input, "hdf-to-xccdf");
7196
- const hdfData = parseJSON(input);
9017
+ const hdfData = parseHdf(input);
7197
9018
  if (!hdfData || typeof hdfData !== "object" || !("baselines" in hdfData)) throw new Error("Invalid HDF structure: missing baselines field");
7198
9019
  if (!Array.isArray(hdfData.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
7199
9020
  return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ Benchmark: buildBenchmarkObj(hdfData) }, BUILD_OPTIONS)}`;
@@ -7229,19 +9050,19 @@ function sanitizeXccdfId(id) {
7229
9050
  }
7230
9051
  /** Build the Benchmark XML object from HDF data. */
7231
9052
  function buildBenchmarkObj(hdfData) {
9053
+ const empty = !hdfData.baselines || hdfData.baselines.length === 0;
7232
9054
  const benchmark = {
7233
9055
  [`${ATTR}xmlns`]: "http://checklists.nist.gov/xccdf/1.2",
9056
+ [`${ATTR}id`]: empty ? "xccdf_hdf_benchmark_exported" : sanitizeXccdfId("xccdf_hdf_benchmark_" + hdfData.baselines[0].name),
7234
9057
  [`${ATTR}resolved`]: "1",
7235
9058
  status: wrap("incomplete")
7236
9059
  };
7237
- if (!hdfData.baselines || hdfData.baselines.length === 0) {
7238
- benchmark[`${ATTR}id`] = "xccdf_hdf_benchmark_exported";
9060
+ if (empty) {
7239
9061
  benchmark.title = wrap("HDF Export");
7240
9062
  benchmark.version = wrap("1.0");
7241
9063
  return benchmark;
7242
9064
  }
7243
9065
  const baseline = hdfData.baselines[0];
7244
- benchmark[`${ATTR}id`] = sanitizeXccdfId("xccdf_hdf_benchmark_" + baseline.name);
7245
9066
  benchmark.title = wrap(baseline.title ?? baseline.name);
7246
9067
  benchmark.version = wrap(baseline.version ?? "1.0");
7247
9068
  benchmark.Profile = {
@@ -7263,20 +9084,40 @@ function buildRuleObj(req) {
7263
9084
  };
7264
9085
  const description = findDescription(req.descriptions, "default");
7265
9086
  if (description) rule.description = wrap(description);
9087
+ if (Array.isArray(req.refs) && req.refs.length > 0) {
9088
+ const refs = req.refs.map((r) => {
9089
+ const o = r;
9090
+ if (typeof o.url === "string") return { [`${ATTR}href`]: o.url };
9091
+ if (typeof o.uri === "string") return { [`${ATTR}href`]: o.uri };
9092
+ if (typeof o.ref === "string") return { "#text": o.ref };
9093
+ }).filter((x) => x !== void 0);
9094
+ if (refs.length > 0) rule.reference = refs;
9095
+ }
9096
+ const rationale = findDescription(req.descriptions, "rationale");
9097
+ if (rationale) rule.rationale = wrap(rationale);
7266
9098
  const fixtext = findDescription(req.descriptions, "fix");
7267
9099
  if (fixtext) rule.fixtext = wrap(fixtext);
7268
- if (req.tags && Array.isArray(req.tags["cci"])) {
7269
- const ccis = req.tags["cci"];
7270
- if (ccis.length > 0) rule.ident = ccis.map((cci) => ({
7271
- [`${ATTR}system`]: "http://cyber.mil/cci",
7272
- "#text": cci
7273
- }));
7274
- }
9100
+ const idents = [];
9101
+ if (req.tags && Array.isArray(req.tags["cci"])) for (const cci of req.tags["cci"]) idents.push({
9102
+ [`${ATTR}system`]: "http://cyber.mil/cci",
9103
+ "#text": cci
9104
+ });
9105
+ if (req.tags && Array.isArray(req.tags["nist"])) for (const n of req.tags["nist"]) idents.push({
9106
+ [`${ATTR}system`]: "https://csrc.nist.gov/projects/risk-management/sp800-53-controls",
9107
+ "#text": n
9108
+ });
9109
+ if (idents.length > 0) rule.ident = idents;
9110
+ const checks = [];
7275
9111
  const checkContent = findDescription(req.descriptions, "check");
7276
- if (checkContent) rule.check = {
9112
+ if (checkContent) checks.push({
7277
9113
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
7278
9114
  "check-content": wrap(checkContent)
7279
- };
9115
+ });
9116
+ if (req.code) checks.push({
9117
+ [`${ATTR}system`]: "http://inspec.io/",
9118
+ "check-content": wrap(req.code)
9119
+ });
9120
+ if (checks.length > 0) rule.check = checks;
7280
9121
  return rule;
7281
9122
  }
7282
9123
  /** Build the XCCDF TestResult object. */
@@ -7496,7 +9337,7 @@ async function convertHdfToOscalSar(input) {
7496
9337
  if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-sar: empty input");
7497
9338
  let hdfResults;
7498
9339
  try {
7499
- hdfResults = parseJSON(input);
9340
+ hdfResults = parseHdf(input);
7500
9341
  } catch {
7501
9342
  throw new Error("hdf-to-oscal-sar: failed to parse HDF JSON");
7502
9343
  }
@@ -7508,8 +9349,9 @@ async function convertHdfToOscalSar(input) {
7508
9349
  * Constructs the full OSCAL assessment-results document from HDF results.
7509
9350
  */
7510
9351
  function buildOSCALDocument(hdfResults) {
7511
- let timestamp = (/* @__PURE__ */ new Date()).toISOString();
7512
- if (hdfResults.timestamp) timestamp = typeof hdfResults.timestamp === "string" ? hdfResults.timestamp : hdfResults.timestamp.toISOString();
9352
+ let timestamp = formatTimestampSeconds(/* @__PURE__ */ new Date());
9353
+ const documentTime = hdfTime(hdfResults.timestamp);
9354
+ if (documentTime) timestamp = formatTimestampSeconds(documentTime);
7513
9355
  const metadata = {
7514
9356
  title: "HDF Assessment Results Export",
7515
9357
  "last-modified": timestamp,
@@ -7528,6 +9370,34 @@ function buildOSCALDocument(hdfResults) {
7528
9370
  results
7529
9371
  } };
7530
9372
  }
9373
+ /** Earliest startTime across the results, or undefined if none carry one. */
9374
+ function earliestResultTime(results) {
9375
+ let earliest;
9376
+ for (const result of results ?? []) {
9377
+ const parsed = hdfTime(result.startTime);
9378
+ if (!parsed) continue;
9379
+ if (earliest === void 0 || parsed < earliest) earliest = parsed;
9380
+ }
9381
+ return earliest;
9382
+ }
9383
+ /**
9384
+ * Render an assessment time. OSCAL requires result.start and
9385
+ * observation.collected, so a fallback is needed when HDF carries no result time
9386
+ * — but it must never win over a real one.
9387
+ */
9388
+ function formatAssessmentTime(time, fallback) {
9389
+ return time === void 0 ? fallback : formatTimestampSeconds(time);
9390
+ }
9391
+ /** When the assessment ran: the earliest result time anywhere in the baseline. */
9392
+ function assessmentStart(baseline, fallback) {
9393
+ let earliest;
9394
+ for (const req of baseline.requirements) {
9395
+ const reqEarliest = earliestResultTime(req.results);
9396
+ if (reqEarliest === void 0) continue;
9397
+ if (earliest === void 0 || reqEarliest < earliest) earliest = reqEarliest;
9398
+ }
9399
+ return formatAssessmentTime(earliest, fallback);
9400
+ }
7531
9401
  /**
7532
9402
  * Converts a single EvaluatedBaseline to an OSCAL Result.
7533
9403
  */
@@ -7549,7 +9419,7 @@ function baselineToResult(baseline, timestamp) {
7549
9419
  uuid: crypto.randomUUID(),
7550
9420
  title,
7551
9421
  description,
7552
- start: timestamp,
9422
+ start: assessmentStart(baseline, timestamp),
7553
9423
  findings,
7554
9424
  observations,
7555
9425
  risks
@@ -7563,14 +9433,59 @@ function requirementToFindingSet(req, timestamp) {
7563
9433
  const { state, reason } = aggregateStatus(req.results);
7564
9434
  const findingDesc = extractDefaultDescription(req.descriptions);
7565
9435
  const props = [];
7566
- if (req.tags) {
7567
- const nistRaw = req.tags["nist"];
7568
- if (Array.isArray(nistRaw)) {
7569
- for (const v of nistRaw) if (typeof v === "string") props.push({
7570
- name: "nist",
9436
+ const pushTagValues = (key) => {
9437
+ const raw = req.tags?.[key];
9438
+ if (Array.isArray(raw)) {
9439
+ for (const v of raw) if (typeof v === "string") props.push({
9440
+ name: key,
7571
9441
  value: v
7572
9442
  });
7573
9443
  }
9444
+ };
9445
+ pushTagValues("nist");
9446
+ pushTagValues("cci");
9447
+ if (req.code) props.push({
9448
+ name: "code",
9449
+ value: req.code
9450
+ });
9451
+ for (const label of [
9452
+ "check",
9453
+ "fix",
9454
+ "rationale"
9455
+ ]) {
9456
+ const d = req.descriptions.find((x) => x.label === label);
9457
+ if (d) props.push({
9458
+ name: label,
9459
+ value: d.data
9460
+ });
9461
+ }
9462
+ if (req.controlType) props.push({
9463
+ name: "control-type",
9464
+ value: req.controlType
9465
+ });
9466
+ if (req.verificationMethod) props.push({
9467
+ name: "verification-method",
9468
+ value: req.verificationMethod
9469
+ });
9470
+ if (req.applicability) props.push({
9471
+ name: "applicability",
9472
+ value: req.applicability
9473
+ });
9474
+ const links = [];
9475
+ if (Array.isArray(req.refs)) for (const r of req.refs) {
9476
+ const o = r;
9477
+ if (typeof o.url === "string") links.push({
9478
+ href: o.url,
9479
+ rel: "reference"
9480
+ });
9481
+ else if (typeof o.uri === "string") links.push({
9482
+ href: o.uri,
9483
+ rel: "reference"
9484
+ });
9485
+ else if (typeof o.ref === "string") props.push({
9486
+ name: "reference",
9487
+ value: o.ref
9488
+ });
7574
9489
  }
7575
9490
  let title = req.id;
7576
9491
  if (req.title && req.title !== "") title = req.title;
@@ -7586,6 +9501,7 @@ function requirementToFindingSet(req, timestamp) {
7586
9501
  title,
7587
9502
  description: findingDesc || "",
7588
9503
  props: props.length > 0 ? props : void 0,
9504
+ links: links.length > 0 ? links : void 0,
7589
9505
  target
7590
9506
  };
7591
9507
  let observation;
@@ -7595,7 +9511,7 @@ function requirementToFindingSet(req, timestamp) {
7595
9511
  uuid: obsUUID,
7596
9512
  description: buildObservationDescription(req.results),
7597
9513
  methods: ["TEST"],
7598
- collected: timestamp
9514
+ collected: formatAssessmentTime(earliestResultTime(req.results), timestamp)
7599
9515
  };
7600
9516
  finding["related-observations"] = [{ "observation-uuid": obsUUID }];
7601
9517
  }
@@ -7725,18 +9641,24 @@ async function convertHdfToOscalPoam(input) {
7725
9641
  if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-poam: empty input");
7726
9642
  let amendments;
7727
9643
  try {
7728
- amendments = parseJSON(input);
9644
+ amendments = parseHdf(input);
7729
9645
  } catch {
7730
9646
  throw new Error("hdf-to-oscal-poam: failed to parse JSON");
7731
9647
  }
7732
9648
  const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendments) };
7733
9649
  return JSON.stringify(doc, null, 2);
7734
9650
  }
9651
+ /** HDF dates arrive as strings from JSON.parse but are typed as Date. */
9652
+ function toDate(value) {
9653
+ const d = typeof value === "string" ? parseTimestamp(value) : value;
9654
+ if (!d || isNaN(d.getTime())) return void 0;
9655
+ return d;
9656
+ }
7735
9657
  /**
7736
9658
  * Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
7737
9659
  */
7738
9660
  function amendmentsToPOAM(amendments) {
7739
- const now = (/* @__PURE__ */ new Date()).toISOString();
9661
+ const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
7740
9662
  const metadata = {
7741
9663
  title: amendments.name,
7742
9664
  "last-modified": now,
@@ -7787,25 +9709,45 @@ function overrideToPOAMItem(override) {
7787
9709
  name: "impacted-control-id",
7788
9710
  value: nistTagToControlId(override.requirementId)
7789
9711
  }];
7790
- const remediations = [];
7791
- if (override.milestones) for (const ms of override.milestones) remediations.push({
7792
- uuid: crypto.randomUUID(),
7793
- lifecycle: "planned",
7794
- title: ms.description,
7795
- description: ms.description
9712
+ if (override.type) riskProps.push({
9713
+ name: "override-type",
9714
+ value: String(override.type)
7796
9715
  });
7797
- let riskLog;
7798
- const expiresAt = override.expiresAt;
7799
- if (expiresAt) {
7800
- const expiresDate = typeof expiresAt === "string" ? new Date(expiresAt) : expiresAt;
7801
- if (!isNaN(expiresDate.getTime()) && expiresDate.getTime() > 0) riskLog = { entries: [{
9716
+ if (override.impact && typeof override.impact.value === "number") riskProps.push({
9717
+ name: "impact-override",
9718
+ value: String(override.impact.value)
9719
+ });
9720
+ const remediations = [];
9721
+ if (override.milestones) for (const ms of override.milestones) {
9722
+ const msProps = [];
9723
+ if (ms.estimatedCompletion) {
9724
+ const d = toDate(ms.estimatedCompletion);
9725
+ if (d) msProps.push({
9726
+ name: "estimated-completion",
9727
+ value: formatTimestampSeconds(d)
9728
+ });
9729
+ }
9730
+ if (ms.status) msProps.push({
9731
+ name: "milestone-status",
9732
+ value: String(ms.status)
9733
+ });
9734
+ remediations.push({
7802
9735
  uuid: crypto.randomUUID(),
7803
- title: "Scheduled review",
7804
- description: "Amendment expiration date",
7805
- start: expiresDate.toISOString(),
7806
- "status-change": riskStatus
7807
- }] };
9736
+ lifecycle: "planned",
9737
+ title: ms.description,
9738
+ description: ms.description,
9739
+ props: msProps.length > 0 ? msProps : void 0
9740
+ });
7808
9741
  }
9742
+ let riskLog;
9743
+ const expiresDate = override.expiresAt ? toDate(override.expiresAt) : void 0;
9744
+ if (expiresDate && expiresDate.getTime() > 0) riskLog = { entries: [{
9745
+ uuid: crypto.randomUUID(),
9746
+ title: "Scheduled review",
9747
+ description: "Amendment expiration date",
9748
+ start: formatTimestampSeconds(expiresDate),
9749
+ "status-change": riskStatus
9750
+ }] };
7809
9751
  const risk = {
7810
9752
  uuid: riskUUID,
7811
9753
  title: override.requirementId,
@@ -7877,7 +9819,7 @@ function buildTags(dep) {
7877
9819
  if (dep.parentDependencies.length > 0) extras.parentDependencies = dep.parentDependencies;
7878
9820
  return buildNistCciTags(nist, cciTags, extras);
7879
9821
  }
7880
- async function convertIonchannelToHdf(input) {
9822
+ async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
7881
9823
  if (!input?.trim()) throw new Error("Empty input");
7882
9824
  validateInputSize(input, "ionchannel");
7883
9825
  const analysis = parseJSON(input);
@@ -7904,10 +9846,11 @@ async function convertIonchannelToHdf(input) {
7904
9846
  outdated_version: dep.outdated_version,
7905
9847
  dependencies: dep.dependencies
7906
9848
  }, null, 2);
7907
- const req = createRequirement(depId, title, [createDescription("default", `Dependency ${dep.org}/${dep.name}`)], 0, [createResult(ResultStatus.NotReviewed, "Dependency inventory item", {
9849
+ const req = createRequirement(depId, title, [createDescription("default", `Dependency ${dep.org}/${dep.name}`)], 0, [{
9850
+ status: ResultStatus.NotReviewed,
7908
9851
  codeDesc: "Dependency inventory item",
7909
9852
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
7910
- })], { tags });
9853
+ }], { tags });
7911
9854
  req.code = code;
7912
9855
  const controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
7913
9856
  if (controlType !== void 0) req.controlType = controlType;
@@ -7915,20 +9858,22 @@ async function convertIonchannelToHdf(input) {
7915
9858
  return req;
7916
9859
  });
7917
9860
  const resultsChecksum = await inputChecksum(input);
9861
+ const baseline = createMinimalBaseline("Ion Channel SBOM Analysis", requirements, {
9862
+ title: `Ion Channel Analysis of ${analysis.source}`,
9863
+ summary: analysis.summary,
9864
+ integrity: {
9865
+ algorithm: resultsChecksum.algorithm,
9866
+ checksum: resultsChecksum.value
9867
+ },
9868
+ status: "loaded"
9869
+ });
9870
+ baseline.maintainer = "saf@groups.mitre.org";
7918
9871
  return buildHdfResults({
7919
9872
  generatorName: "ionchannel-to-hdf",
7920
- converterVersion: "1.0.0",
9873
+ converterVersion,
7921
9874
  toolName: "Ion Channel",
7922
9875
  toolFormat: "JSON",
7923
- baselines: [createMinimalBaseline("Ion Channel SBOM Analysis", requirements, {
7924
- title: `Ion Channel Analysis of ${analysis.source}`,
7925
- summary: analysis.summary,
7926
- integrity: {
7927
- algorithm: resultsChecksum.algorithm,
7928
- checksum: resultsChecksum.value
7929
- },
7930
- status: "loaded"
7931
- })],
9876
+ baselines: [baseline],
7932
9877
  timestamp: /* @__PURE__ */ new Date()
7933
9878
  });
7934
9879
  }
@@ -8141,6 +10086,40 @@ function affectedPackageToIdentifier(pkg) {
8141
10086
  if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
8142
10087
  if (pkg.name) return pkg.name;
8143
10088
  }
10089
+ /** Replace the `@version` segment of a purl (between `@` and `?`/`#`), inserting one if absent. */
10090
+ function swapPurlVersion(purl, version) {
10091
+ const at = purl.indexOf("@");
10092
+ if (at >= 0) {
10093
+ const rest = purl.slice(at + 1);
10094
+ const end = rest.search(/[?#]/);
10095
+ const tail = end >= 0 ? rest.slice(end) : "";
10096
+ return `${purl.slice(0, at)}@${version}${tail}`;
10097
+ }
10098
+ const end = purl.search(/[?#]/);
10099
+ return end >= 0 ? `${purl.slice(0, end)}@${version}${purl.slice(end)}` : `${purl}@${version}`;
10100
+ }
10101
+ /**
10102
+ * Identifier for the FIXED version of a package (purl with the version swapped
10103
+ * to fixedInVersion, or name@fixedInVersion). Undefined when there is no
10104
+ * fixedInVersion or no purl/name to anchor it (a bare cpe cannot be swapped).
10105
+ */
10106
+ function fixedPackageIdentifier(pkg) {
10107
+ if (!pkg.fixedInVersion) return void 0;
10108
+ if (pkg.purl) return swapPurlVersion(pkg.purl, pkg.fixedInVersion);
10109
+ if (pkg.name) return `${pkg.name}@${pkg.fixedInVersion}`;
10110
+ }
10111
+ /**
10112
+ * `vers` (Package URL version-range) type for a package, from its ecosystem or
10113
+ * the purl type. Returns undefined when neither is known (so callers avoid
10114
+ * emitting an invalid range).
10115
+ */
10116
+ function versTypeFor(pkg) {
10117
+ if (pkg.ecosystem) return String(pkg.ecosystem).toLowerCase();
10118
+ if (pkg.purl) {
10119
+ const m = /^pkg:([^/]+)\//.exec(pkg.purl);
10120
+ if (m?.[1]) return m[1].toLowerCase();
10121
+ }
10122
+ }
8144
10123
  /**
8145
10124
  * Build an HDF Evidence entry pointing at the upstream VEX document. Used by
8146
10125
  * importers to preserve provenance even though we lose the structured
@@ -8635,12 +10614,27 @@ function publisherIdentityOrDefault(bom) {
8635
10614
  * fields the shared VEX mapping considers consumer-action-bearing survive
8636
10615
  * round-trip; the rest collapse into the available CSAF fields.
8637
10616
  */
10617
+ const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
8638
10618
  const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
8639
10619
  const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
8640
10620
  const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
10621
+ /** Map an HDF Cvss block to a CSAF score entry (cvss_v2/v3/v4 by version). */
10622
+ function buildCsafScore(cvss, products) {
10623
+ const inner = {
10624
+ version: cvss.version,
10625
+ ...cvss.baseVector && { vectorString: cvss.baseVector },
10626
+ ...typeof cvss.baseScore === "number" && { baseScore: cvss.baseScore },
10627
+ ...cvss.baseSeverity && { baseSeverity: cvss.baseSeverity }
10628
+ };
10629
+ if (inner.vectorString === void 0 && inner.baseScore === void 0) return;
10630
+ return {
10631
+ [cvss.version.startsWith("4") ? "cvss_v4" : cvss.version.startsWith("2") ? "cvss_v2" : "cvss_v3"]: inner,
10632
+ products
10633
+ };
10634
+ }
8641
10635
  function convertHdfToCsafVex(input, converterVersion) {
8642
10636
  validateInputSize(input, "hdf-to-csaf-vex");
8643
- const amendments = parseJSON(input);
10637
+ const amendments = parseHdf(input);
8644
10638
  const groups = groupByCVE(amendments.overrides ?? []);
8645
10639
  const productSet = /* @__PURE__ */ new Map();
8646
10640
  const vulnerabilities = [];
@@ -8649,6 +10643,7 @@ function convertHdfToCsafVex(input, converterVersion) {
8649
10643
  if (!v) continue;
8650
10644
  vulnerabilities.push(v);
8651
10645
  for (const p of productIDsForGroup(group)) productSet.set(p, true);
10646
+ for (const p of fixedProductIDsForGroup(group)) productSet.set(p, true);
8652
10647
  }
8653
10648
  if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
8654
10649
  const products = [...productSet.keys()].sort();
@@ -8684,6 +10679,15 @@ function productIDsForGroup(group) {
8684
10679
  for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
8685
10680
  return [...seen].sort();
8686
10681
  }
10682
+ /** Synthesized fixed-version product ids across a group's affectedPackages. */
10683
+ function fixedProductIDsForGroup(group) {
10684
+ const ids = [];
10685
+ for (const o of group.overrides) for (const p of o.affectedPackages ?? []) {
10686
+ const f = fixedPackageIdentifier(p);
10687
+ if (f) ids.push(f);
10688
+ }
10689
+ return ids;
10690
+ }
8687
10691
  function productIDsFor$1(o) {
8688
10692
  if (o.affectedPackages && o.affectedPackages.length > 0) {
8689
10693
  const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
@@ -8710,6 +10714,25 @@ function buildVulnerability(group) {
8710
10714
  let emitted = false;
8711
10715
  for (const o of group.overrides) {
8712
10716
  const pids = productIDsFor$1(o);
10717
+ if (o.cvss) {
10718
+ const score = buildCsafScore(o.cvss, pids);
10719
+ if (score) {
10720
+ v.scores = (v.scores ?? []).concat(score);
10721
+ emitted = true;
10722
+ }
10723
+ }
10724
+ for (const p of o.affectedPackages ?? []) {
10725
+ const fixedId = fixedPackageIdentifier(p);
10726
+ if (!fixedId) continue;
10727
+ status.first_fixed = (status.first_fixed ?? []).concat(fixedId);
10728
+ status.fixed = (status.fixed ?? []).concat(fixedId);
10729
+ v.remediations = (v.remediations ?? []).concat({
10730
+ category: "vendor_fix",
10731
+ details: `Fixed in ${p.fixedInVersion}`,
10732
+ product_ids: [fixedId]
10733
+ });
10734
+ emitted = true;
10735
+ }
8713
10736
  let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
8714
10737
  if (!canonical) continue;
8715
10738
  if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
@@ -8719,8 +10742,8 @@ function buildVulnerability(group) {
8719
10742
  v.flags = v.flags ?? [];
8720
10743
  v.flags.push({
8721
10744
  label: String(o.justification),
8722
- product_ids: pids,
8723
- date: formatTimestampSeconds(new Date(o.appliedAt))
10745
+ date: formatTimestampSeconds(hdfTime(o.appliedAt) ?? GO_ZERO_TIME$1),
10746
+ product_ids: pids
8724
10747
  });
8725
10748
  }
8726
10749
  emitted = true;
@@ -8770,9 +10793,9 @@ function buildVulnerability(group) {
8770
10793
  /* c8 ignore next */
8771
10794
  if (!emitted) return void 0;
8772
10795
  if (status.fixed) status.fixed = [...new Set(status.fixed)];
10796
+ if (status.first_fixed) status.first_fixed = [...new Set(status.first_fixed)];
8773
10797
  if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
8774
10798
  if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
8775
- v.product_status = status;
8776
10799
  if (v.references) {
8777
10800
  const seen = /* @__PURE__ */ new Set();
8778
10801
  v.references = v.references.filter((r) => {
@@ -8781,10 +10804,30 @@ function buildVulnerability(group) {
8781
10804
  return true;
8782
10805
  });
8783
10806
  }
8784
- return v;
10807
+ return {
10808
+ cve: v.cve,
10809
+ ...v.notes && { notes: v.notes },
10810
+ product_status: status,
10811
+ ...v.flags && { flags: v.flags },
10812
+ ...v.threats && { threats: v.threats },
10813
+ ...v.remediations && { remediations: v.remediations },
10814
+ ...v.references && { references: v.references },
10815
+ ...v.scores && { scores: v.scores }
10816
+ };
10817
+ }
10818
+ /** Stable document time: the earliest override appliedAt, falling back to now. */
10819
+ function earliestAppliedAt(amendments) {
10820
+ let earliest;
10821
+ for (const o of amendments.overrides ?? []) {
10822
+ if (!o.appliedAt) continue;
10823
+ const t = hdfTime(o.appliedAt);
10824
+ if (!t) continue;
10825
+ if (!earliest || t < earliest) earliest = t;
10826
+ }
10827
+ return earliest ?? /* @__PURE__ */ new Date();
8785
10828
  }
8786
10829
  function buildDocument(amendments, converterVersion) {
8787
- const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
10830
+ const now = formatTimestampSeconds(earliestAppliedAt(amendments));
8788
10831
  const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
8789
10832
  const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
8790
10833
  const docVersion = amendments.version || "1";
@@ -8800,7 +10843,7 @@ function buildDocument(amendments, converterVersion) {
8800
10843
  category: "csaf_vex",
8801
10844
  csaf_version: "2.0",
8802
10845
  title,
8803
- notes,
10846
+ ...notes.length > 0 && { notes },
8804
10847
  publisher: {
8805
10848
  category: "other",
8806
10849
  name: publisherName
@@ -8838,6 +10881,7 @@ function buildDocument(amendments, converterVersion) {
8838
10881
  * partial-fidelity by design — consumer-action-bearing fields (CVE id,
8839
10882
  * status, justification) survive round-trip; the rest collapse.
8840
10883
  */
10884
+ const GO_ZERO_TIME = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
8841
10885
  const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
8842
10886
  const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
8843
10887
  const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
@@ -8845,14 +10889,15 @@ const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
8845
10889
  const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
8846
10890
  async function convertHdfToOpenVex(input, converterVersion) {
8847
10891
  validateInputSize(input, "hdf-to-openvex");
8848
- const amendments = parseJSON(input);
10892
+ const amendments = parseHdf(input);
8849
10893
  const statements = [];
8850
10894
  let earliest;
8851
10895
  for (const o of amendments.overrides ?? []) {
8852
10896
  const s = overrideToStatement(o);
8853
10897
  if (!s) continue;
8854
10898
  statements.push(s);
8855
- const t = new Date(o.appliedAt);
10899
+ const t = hdfTime(o.appliedAt);
10900
+ if (!t) continue;
8856
10901
  if (!earliest || t < earliest) earliest = t;
8857
10902
  }
8858
10903
  if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
@@ -8875,22 +10920,24 @@ function overrideToStatement(o) {
8875
10920
  let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
8876
10921
  if (!canonical) return void 0;
8877
10922
  if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
8878
- const stmt = {
10923
+ const notAffected = canonical === VexStatus.NotAffected;
10924
+ const justification = notAffected && o.justification ? String(o.justification) : "";
10925
+ const impact = notAffected ? stripProductsLine(o.reason ?? "") : "";
10926
+ let action = "";
10927
+ if (canonical === VexStatus.Fixed) action = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
10928
+ else if (canonical === VexStatus.Affected) action = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
10929
+ return {
8879
10930
  vulnerability: {
8880
- name: o.requirementId,
8881
- "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
10931
+ "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`,
10932
+ name: o.requirementId
8882
10933
  },
10934
+ products: productsFor(o),
8883
10935
  status: String(canonical),
8884
- timestamp: formatTimestampSeconds(new Date(o.appliedAt)),
8885
- products: productsFor(o)
10936
+ ...justification && { justification },
10937
+ ...impact && { impact_statement: impact },
10938
+ ...action && { action_statement: action },
10939
+ timestamp: formatTimestampSeconds(hdfTime(o.appliedAt) ?? GO_ZERO_TIME)
8886
10940
  };
8887
- if (canonical === VexStatus.NotAffected) {
8888
- if (o.justification) stmt.justification = String(o.justification);
8889
- const impact = stripProductsLine(o.reason ?? "");
8890
- if (impact) stmt.impact_statement = impact;
8891
- } else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
8892
- else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
8893
- return stmt;
8894
10941
  }
8895
10942
  function productsFor(o) {
8896
10943
  if (o.affectedPackages && o.affectedPackages.length > 0) {
@@ -8919,7 +10966,8 @@ function stripProductsLine(reason) {
8919
10966
  }
8920
10967
  async function buildDocumentID(input, a) {
8921
10968
  if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
8922
- return `${OPENVEX_NAMESPACE}vex-${await sha256(input)}`;
10969
+ const digest = await sha256(input);
10970
+ return `${OPENVEX_NAMESPACE}vex-${digest}`;
8923
10971
  }
8924
10972
  //#endregion
8925
10973
  //#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
@@ -8936,9 +10984,41 @@ const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
8936
10984
  const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
8937
10985
  const RESPONSE_LINE = /^Response:.*$/gm;
8938
10986
  const DEFAULT_PRODUCT_ID = "HDFPID-0001";
8939
- function convertHdfToCyclonedxVex(input, converterVersion) {
10987
+ /**
10988
+ * Build CycloneDX affects[].versions[] for a package with a fixedInVersion:
10989
+ * the current version is `affected`, and `>= fixedInVersion` is `unaffected`,
10990
+ * expressed as a `vers` range. Undefined when there is no vers type to key the
10991
+ * range (caller falls back to a free-text recommendation).
10992
+ */
10993
+ function buildCdxAffectsVersions(pkg) {
10994
+ if (!pkg?.fixedInVersion) return void 0;
10995
+ const t = versTypeFor(pkg);
10996
+ if (!t) return void 0;
10997
+ const versions = [];
10998
+ if (pkg.version) versions.push({
10999
+ version: pkg.version,
11000
+ status: "affected"
11001
+ });
11002
+ versions.push({
11003
+ range: `vers:${t}/>=${pkg.fixedInVersion}`,
11004
+ status: "unaffected"
11005
+ });
11006
+ return versions;
11007
+ }
11008
+ /** Map an HDF Cvss block to a CycloneDX rating. */
11009
+ function buildCdxRating(cvss) {
11010
+ const rating = {};
11011
+ if (typeof cvss.baseScore === "number") rating.score = cvss.baseScore;
11012
+ if (cvss.baseSeverity) rating.severity = cvss.baseSeverity;
11013
+ if (cvss.baseVector) rating.vector = cvss.baseVector;
11014
+ const v = cvss.version;
11015
+ rating.method = v === "4.0" ? "CVSSv4" : v === "3.1" ? "CVSSv31" : v === "3.0" ? "CVSSv3" : v === "2.0" ? "CVSSv2" : "other";
11016
+ if (rating.score === void 0 && rating.vector === void 0) return void 0;
11017
+ return rating;
11018
+ }
11019
+ async function convertHdfToCyclonedxVex(input, converterVersion) {
8940
11020
  validateInputSize(input, "hdf-to-cyclonedx-vex");
8941
- const amendments = parseJSON(input);
11021
+ const amendments = parseHdf(input);
8942
11022
  const componentRegistry = /* @__PURE__ */ new Map();
8943
11023
  const vulnerabilities = [];
8944
11024
  let earliest;
@@ -8947,7 +11027,8 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
8947
11027
  const v = overrideToVulnerability(o, componentRegistry);
8948
11028
  if (!v) continue;
8949
11029
  vulnerabilities.push(v);
8950
- const t = new Date(o.appliedAt);
11030
+ const t = hdfTime(o.appliedAt);
11031
+ if (!t) continue;
8951
11032
  if (!earliest || t < earliest) earliest = t;
8952
11033
  }
8953
11034
  if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
@@ -8956,7 +11037,7 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
8956
11037
  const bom = {
8957
11038
  bomFormat: "CycloneDX",
8958
11039
  specVersion: "1.4",
8959
- serialNumber: buildSerialNumberSync(input, amendments),
11040
+ serialNumber: await buildSerialNumber(input, amendments),
8960
11041
  version: 1,
8961
11042
  metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
8962
11043
  components,
@@ -8980,23 +11061,14 @@ function overrideToVulnerability(o, componentRegistry) {
8980
11061
  const cdxJust = justificationForCycloneDX(o.justification);
8981
11062
  if (cdxJust) analysis.justification = cdxJust;
8982
11063
  }
8983
- const detail = stripReasonAnnotations(o.reason ?? "");
8984
- if (detail) analysis.detail = detail;
8985
11064
  if (canonical === VexStatus.Fixed) analysis.response = ["update"];
8986
11065
  else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
8987
- const v = {
8988
- id: o.requirementId,
8989
- source: {
8990
- name: "NVD",
8991
- url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
8992
- },
8993
- analysis,
8994
- affects: pids.map((p) => ({ ref: p }))
8995
- };
11066
+ const detail = stripReasonAnnotations(o.reason ?? "");
11067
+ if (detail) analysis.detail = detail;
11068
+ const references = [];
8996
11069
  for (const e of o.evidence ?? []) {
8997
11070
  if (e.type !== "url" || !e.data) continue;
8998
- v.references = v.references ?? [];
8999
- v.references.push({
11071
+ references.push({
9000
11072
  id: o.requirementId,
9001
11073
  source: {
9002
11074
  name: e.description ?? "",
@@ -9004,7 +11076,26 @@ function overrideToVulnerability(o, componentRegistry) {
9004
11076
  }
9005
11077
  });
9006
11078
  }
9007
- return v;
11079
+ const rating = o.cvss ? buildCdxRating(o.cvss) : void 0;
11080
+ const unranged = (o.affectedPackages ?? []).find((p) => p.fixedInVersion && !versTypeFor(p));
11081
+ return {
11082
+ id: o.requirementId,
11083
+ source: {
11084
+ name: "NVD",
11085
+ url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
11086
+ },
11087
+ ...references.length > 0 && { references },
11088
+ ...rating && { ratings: [rating] },
11089
+ ...unranged && { recommendation: `Upgrade to ${unranged.fixedInVersion}` },
11090
+ analysis,
11091
+ affects: pids.map((pid) => {
11092
+ const versions = buildCdxAffectsVersions(pkgById.get(pid));
11093
+ return versions ? {
11094
+ ref: pid,
11095
+ versions
11096
+ } : { ref: pid };
11097
+ })
11098
+ };
9008
11099
  }
9009
11100
  function canonicalToCycloneDXState(canonical) {
9010
11101
  switch (canonical) {
@@ -9063,18 +11154,9 @@ function buildMetadata(a, docTime, converterVersion) {
9063
11154
  }
9064
11155
  return metadata;
9065
11156
  }
9066
- function buildSerialNumberSync(input, a) {
11157
+ async function buildSerialNumber(input, a) {
9067
11158
  if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
9068
- return `urn:uuid:${fnv1a(input)}`;
9069
- }
9070
- function fnv1a(s) {
9071
- let h = 2166136261;
9072
- for (let i = 0; i < s.length; i++) {
9073
- h ^= s.charCodeAt(i);
9074
- h = Math.imul(h, 16777619) >>> 0;
9075
- }
9076
- const d = h.toString(16).padStart(8, "0");
9077
- return (d + d + d + d).slice(0, 32);
11159
+ return `urn:uuid:${(await sha256(input)).slice(0, 32)}`;
9078
11160
  }
9079
11161
  //#endregion
9080
11162
  //#region converters/oscal-to-hdf/typescript/detect.ts
@@ -10086,6 +12168,6 @@ function sarBaselineName(result, sar) {
10086
12168
  return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
10087
12169
  }
10088
12170
  //#endregion
10089
- export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
12171
+ export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToEcs, convertHdfToOcsf, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToSplunk, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
10090
12172
 
10091
12173
  //# sourceMappingURL=index.js.map