@mitre/hdf-converters 3.3.2 → 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
  *
@@ -776,7 +804,7 @@ const IMPACT_MAPPING$9 = {
776
804
  warning: .5,
777
805
  note: .3
778
806
  };
779
- async function convertSarifToHdf(input) {
807
+ async function convertSarifToHdf(input, converterVersion = "1.0.0") {
780
808
  validateInputSize(input, "sarif");
781
809
  const resultsChecksum = await inputChecksum(input);
782
810
  const sarif = parseJSON(input);
@@ -789,12 +817,11 @@ async function convertSarifToHdf(input) {
789
817
  const timestamp = /* @__PURE__ */ new Date();
790
818
  return buildHdfResults({
791
819
  generatorName: "sarif-to-hdf",
792
- converterVersion: "1.0.0",
820
+ converterVersion,
793
821
  toolName: firstDriver?.name,
794
822
  toolVersion: firstDriver?.version,
795
823
  toolFormat: "SARIF",
796
824
  baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
797
- components: [],
798
825
  timestamp
799
826
  });
800
827
  }
@@ -918,9 +945,9 @@ function convertSarifResultToHDFResults(result) {
918
945
  const locationlessResult = {
919
946
  status,
920
947
  codeDesc: "No source location",
921
- startTime: /* @__PURE__ */ new Date(),
922
- backtrace
948
+ startTime: /* @__PURE__ */ new Date()
923
949
  };
950
+ if (backtrace.length > 0) locationlessResult.backtrace = backtrace;
924
951
  if (suppressionJustification) locationlessResult.message = `Suppressed: ${suppressionJustification}`;
925
952
  results.push(locationlessResult);
926
953
  }
@@ -972,28 +999,17 @@ function extractCweFromRule(rule) {
972
999
  if (cweIds.length > 0) return cweIds;
973
1000
  }
974
1001
  if (rule.properties?.tags && Array.isArray(rule.properties.tags)) {
975
- const cweTagPattern = /^CWE-\d+$/;
976
1002
  const cweIds = [];
977
- 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
+ }
978
1007
  if (cweIds.length > 0) return cweIds;
979
1008
  }
980
1009
  return [];
981
1010
  }
982
1011
  function extractCweIds(text) {
983
- const matches = text.match(/\(([^)]+)\)/g);
984
- if (!matches) return [];
985
- const cweIds = [];
986
- for (const match of matches) {
987
- const content = match.slice(1, -1);
988
- if (content.includes("CWE-")) {
989
- const parts = content.split(/,\s*|!\//);
990
- for (const part of parts) {
991
- const trimmed = part.trim();
992
- if (trimmed.startsWith("CWE-")) cweIds.push(trimmed);
993
- }
994
- }
995
- }
996
- return cweIds;
1012
+ return extractCWEIDs(text).map((id) => `CWE-${id}`);
997
1013
  }
998
1014
  function mapKindToStatus(kind) {
999
1015
  switch (kind) {
@@ -1089,21 +1105,31 @@ function createHDFResult(location, status, backtrace, suppressionMessage) {
1089
1105
  const snippet = location.physicalLocation?.region?.snippet?.text;
1090
1106
  let codeDesc = `URL : ${uri} LINE : ${line} COLUMN : ${column}`;
1091
1107
  if (snippet) codeDesc = `${codeDesc}\n${snippet}`;
1092
- return createResult(status, suppressionMessage ? `Suppressed: ${suppressionMessage}` : "", {
1108
+ const result = createResult(status, void 0, {
1093
1109
  codeDesc,
1094
- startTime: /* @__PURE__ */ new Date(),
1095
- backtrace
1110
+ startTime: /* @__PURE__ */ new Date()
1096
1111
  });
1112
+ delete result.message;
1113
+ if (suppressionMessage) result.message = `Suppressed: ${suppressionMessage}`;
1114
+ if (backtrace.length > 0) result.backtrace = backtrace;
1115
+ return result;
1097
1116
  }
1098
1117
  //#endregion
1099
1118
  //#region converters/junit-to-hdf/typescript/converter.ts
1100
1119
  const DEFAULT_NIST = ["SA-11"];
1101
- 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
+ }
1102
1128
  const ARRAY_TAGS$2 = ["testsuite", "testcase"];
1103
1129
  /**
1104
1130
  * Converts JUnit XML test results to HDF format.
1105
1131
  */
1106
- async function convertJunitToHdf(input) {
1132
+ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
1107
1133
  if (!input || !input.trim()) throw new Error("Empty input");
1108
1134
  validateInputSize(input, "junit");
1109
1135
  const { suites, name } = parseJUnitXML(input);
@@ -1112,7 +1138,7 @@ async function convertJunitToHdf(input) {
1112
1138
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
1113
1139
  return buildHdfResults({
1114
1140
  generatorName: "junit-to-hdf",
1115
- converterVersion: CONVERTER_VERSION$2,
1141
+ converterVersion,
1116
1142
  toolName: "JUnit XML",
1117
1143
  toolFormat: "XML",
1118
1144
  baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
@@ -1134,14 +1160,15 @@ function parseJUnitXML(input) {
1134
1160
  const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
1135
1161
  if (parsed.testsuites) return {
1136
1162
  suites: parsed.testsuites.testsuite ?? [],
1137
- name: parsed.testsuites.name || "JUnit Test Results"
1163
+ name: parsed.testsuites.name ? decodeXmlEntities$5(parsed.testsuites.name) : "JUnit Test Results"
1138
1164
  };
1139
1165
  if (parsed.testsuite) {
1140
1166
  const suite = parsed.testsuite;
1141
1167
  const suites = Array.isArray(suite) ? suite : [suite];
1168
+ const suiteName = suites[0]?.name;
1142
1169
  return {
1143
1170
  suites,
1144
- name: suites[0]?.name || "JUnit Test Results"
1171
+ name: suiteName ? decodeXmlEntities$5(suiteName) : "JUnit Test Results"
1145
1172
  };
1146
1173
  }
1147
1174
  throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
@@ -1163,28 +1190,29 @@ function buildRequirements(suites, scanTime) {
1163
1190
  function testCaseToRequirement(tc, scanTime) {
1164
1191
  const id = buildID(tc);
1165
1192
  const { status, message } = resolveStatus(tc);
1166
- const resultOptions = {
1193
+ const result = {
1194
+ status,
1167
1195
  codeDesc: buildCodeDesc$9(tc),
1168
1196
  startTime: scanTime
1169
1197
  };
1198
+ if (message !== void 0) result.message = message;
1170
1199
  if (tc.time) {
1171
1200
  const parsed = parseFloat(tc.time);
1172
- if (!isNaN(parsed)) resultOptions.runTime = parsed;
1201
+ if (!isNaN(parsed)) result.runTime = parsed;
1173
1202
  }
1174
- const result = createResult(status, message ?? "", resultOptions);
1175
- const descriptions = [{
1203
+ const name = decodeXmlEntities$5(tc.name);
1204
+ const req = createRequirement(id, name, [{
1176
1205
  label: "default",
1177
- data: `JUnit test: ${tc.name} in ${tc.classname || "unknown"}`
1178
- }];
1179
- 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 } });
1180
1208
  const controlType = deriveControlTypeFromTags(DEFAULT_NIST);
1181
1209
  if (controlType !== void 0) req.controlType = controlType;
1182
1210
  req.verificationMethod = VerificationMethodEnum.Automated;
1183
1211
  return req;
1184
1212
  }
1185
1213
  function buildID(tc) {
1186
- if (tc.classname) return `${tc.classname}.${tc.name}`;
1187
- return tc.name;
1214
+ if (tc.classname) return `${decodeXmlEntities$5(tc.classname)}.${decodeXmlEntities$5(tc.name)}`;
1215
+ return decodeXmlEntities$5(tc.name);
1188
1216
  }
1189
1217
  function resolveStatus(tc) {
1190
1218
  if (tc.failure) {
@@ -1205,7 +1233,7 @@ function resolveStatus(tc) {
1205
1233
  const skipped = typeof tc.skipped === "object" ? tc.skipped : null;
1206
1234
  if (skipped?.message) return {
1207
1235
  status: ResultStatus.NotReviewed,
1208
- message: `Skipped: ${skipped.message}`
1236
+ message: `Skipped: ${decodeXmlEntities$5(skipped.message)}`
1209
1237
  };
1210
1238
  return {
1211
1239
  status: ResultStatus.NotReviewed,
@@ -1216,24 +1244,40 @@ function resolveStatus(tc) {
1216
1244
  }
1217
1245
  function buildFailureMessage(message, typeName, body) {
1218
1246
  let result = "";
1219
- if (typeName) result = `${typeName}: `;
1220
- result += message;
1221
- 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);
1222
1250
  return result;
1223
1251
  }
1224
1252
  function buildCodeDesc$9(tc) {
1225
- if (tc.classname) return `${tc.classname} :: ${tc.name}`;
1226
- return tc.name;
1253
+ if (tc.classname) return `${decodeXmlEntities$5(tc.classname)} :: ${decodeXmlEntities$5(tc.name)}`;
1254
+ return decodeXmlEntities$5(tc.name);
1227
1255
  }
1228
1256
  function noFindingsTarget(baselineName, suites) {
1229
1257
  if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
1230
- 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);
1231
1259
  return "JUnit test suite";
1232
1260
  }
1233
1261
  //#endregion
1234
1262
  //#region converters/xccdf-results-to-hdf/typescript/converter.ts
1235
- const CONVERTER_VERSION$1 = "1.0.0";
1236
- 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
+ }
1237
1281
  /** Tags that must always be parsed as arrays even if only one element exists. */
1238
1282
  const ARRAY_TAGS$1 = [
1239
1283
  "Group",
@@ -1282,18 +1326,18 @@ function parseStartTime(raw) {
1282
1326
  }
1283
1327
  return /* @__PURE__ */ new Date();
1284
1328
  }
1285
- async function convertXccdfResultsToHdf(input) {
1329
+ async function convertXccdfResultsToHdf(input, converterVersion = "1.0.0") {
1286
1330
  if (!input || !input.trim()) throw new Error("Empty input");
1287
1331
  validateInputSize(input, "xccdf-results");
1288
- const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1);
1332
+ const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1, { trimValues: false });
1289
1333
  const arfParsed = parsed;
1290
- 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);
1291
1335
  const benchmark = parsed.Benchmark;
1292
1336
  if (!benchmark) throw new Error("Input is not an XCCDF document: expected <Benchmark> root element");
1293
1337
  if (!benchmark.TestResult) throw new Error("Input has no TestResult elements — this is a benchmark. Use 'xccdf-benchmark' or 'xccdf' instead");
1294
- return convertBenchmarkResultsToHdf(benchmark, input);
1338
+ return convertBenchmarkResultsToHdf(benchmark, input, converterVersion);
1295
1339
  }
1296
- async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1340
+ async function convertBenchmarkResultsToHdf(benchmark, rawInput, converterVersion) {
1297
1341
  const testResult = benchmark.TestResult;
1298
1342
  const ruleIndex = buildRuleIndex(benchmark);
1299
1343
  const ruleResults = testResult["rule-result"] ?? [];
@@ -1307,32 +1351,35 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1307
1351
  requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1308
1352
  }
1309
1353
  const resultsChecksum = await inputChecksum(rawInput);
1310
- const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
1311
- const components = buildTargets(testResult);
1312
- const timestamp = scanTime;
1313
- let durationSeconds;
1314
- if (testResult["start-time"] && testResult["end-time"]) {
1315
- const start = parseTimestamp(testResult["start-time"])?.getTime();
1316
- const end = parseTimestamp(testResult["end-time"])?.getTime();
1317
- if (start !== void 0 && end !== void 0 && end >= start) durationSeconds = (end - start) / 1e3;
1318
- }
1319
- 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({
1320
1361
  baselines: [baseline],
1321
1362
  generator: {
1322
1363
  name: "xccdf-results-to-hdf",
1323
- version: CONVERTER_VERSION$1
1364
+ version: converterVersion
1324
1365
  },
1325
1366
  tool: {
1326
- name: "XCCDF Results",
1327
- format: "XML"
1367
+ name: "XCCDF",
1368
+ format: "XCCDF"
1328
1369
  },
1329
- components,
1330
- timestamp
1331
- };
1332
- if (durationSeconds !== void 0) hdf.statistics = { duration: durationSeconds };
1333
- return serializeHdf(hdf);
1370
+ components: buildTargets(testResult),
1371
+ timestamp: scanTime,
1372
+ statistics: { duration: calculateDuration(testResult) }
1373
+ });
1334
1374
  }
1335
- 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) {
1336
1383
  const resultsChecksum = await inputChecksum(rawInput);
1337
1384
  const benchmark = findBenchmarkInArf(arc);
1338
1385
  const ruleIndex = benchmark ? buildRuleIndex(benchmark) : /* @__PURE__ */ new Map();
@@ -1347,16 +1394,9 @@ async function convertArfCollection(arc, rawInput) {
1347
1394
  for (const report of arc.reports?.report ?? []) {
1348
1395
  const testResult = report.content?.TestResult;
1349
1396
  if (!testResult?.id) continue;
1350
- if (testResult["start-time"] && !firstTimestamp) {
1351
- const t = parseTimestamp(testResult["start-time"]);
1352
- if (t) firstTimestamp = t;
1353
- }
1354
- if (testResult["start-time"] && testResult["end-time"]) {
1355
- const start = parseTimestamp(testResult["start-time"])?.getTime();
1356
- const end = parseTimestamp(testResult["end-time"])?.getTime();
1357
- if (start !== void 0 && end !== void 0 && end >= start) totalDuration += (end - start) / 1e3;
1358
- }
1359
1397
  const scanTime = parseStartTime(testResult["start-time"]);
1398
+ if (!firstTimestamp) firstTimestamp = scanTime;
1399
+ totalDuration += calculateDuration(testResult);
1360
1400
  const ruleResults = testResult["rule-result"] ?? [];
1361
1401
  const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
1362
1402
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -1367,9 +1407,15 @@ async function convertArfCollection(arc, rawInput) {
1367
1407
  requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1368
1408
  }
1369
1409
  let baselineName = "";
1370
- if (benchmark) baselineName = extractText(benchmark.title) || "";
1371
- 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 || "";
1372
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
+ }
1373
1419
  baselines.push(baseline);
1374
1420
  const reportTargets = buildTargets(testResult);
1375
1421
  const target = reportTargets[0];
@@ -1383,21 +1429,20 @@ async function convertArfCollection(arc, rawInput) {
1383
1429
  components.push(...reportTargets);
1384
1430
  }
1385
1431
  if (baselines.length === 0) throw new Error("ARF document contains no XCCDF TestResult reports");
1386
- const hdf = {
1432
+ return serializeHdf({
1387
1433
  baselines,
1388
1434
  generator: {
1389
1435
  name: "xccdf-results-to-hdf",
1390
- version: CONVERTER_VERSION$1
1436
+ version: converterVersion
1391
1437
  },
1392
1438
  tool: {
1393
1439
  name: "ARF",
1394
1440
  format: "ARF"
1395
1441
  },
1396
1442
  components,
1397
- timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
1398
- };
1399
- if (totalDuration > 0) hdf.statistics = { duration: totalDuration };
1400
- return serializeHdf(hdf);
1443
+ timestamp: firstTimestamp ?? /* @__PURE__ */ new Date(),
1444
+ statistics: { duration: totalDuration }
1445
+ });
1401
1446
  }
1402
1447
  /**
1403
1448
  * Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
@@ -1443,11 +1488,7 @@ function buildRuleIndex(benchmark) {
1443
1488
  const index = /* @__PURE__ */ new Map();
1444
1489
  const topRules = benchmark.Rule ?? [];
1445
1490
  for (const rule of topRules) if (rule.id) index.set(rule.id, rule);
1446
- const groups = benchmark.Group ?? [];
1447
- for (const group of groups) {
1448
- const rules = group.Rule ?? [];
1449
- for (const rule of rules) if (rule.id) index.set(rule.id, rule);
1450
- }
1491
+ for (const { rule } of flattenGroups(benchmark.Group ?? [])) index.set(rule.id, rule);
1451
1492
  return index;
1452
1493
  }
1453
1494
  /**
@@ -1456,39 +1497,29 @@ function buildRuleIndex(benchmark) {
1456
1497
  function ruleResultToRequirement(rr, ruleIndex, scanTime) {
1457
1498
  const ruleId = rr.idref ?? "";
1458
1499
  const ruleDef = ruleIndex.get(ruleId);
1459
- const id = extractVersion$1(ruleDef?.version) || ruleId;
1460
- const title = extractText(ruleDef?.title) || id;
1461
- const severity = rr.severity ?? ruleDef?.severity ?? "";
1462
- const impact = severity ? severityToImpact(severity) : .5;
1463
- const descriptions = [];
1464
- const rawDesc = extractText(ruleDef?.description);
1465
- 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 = [{
1466
1505
  label: "default",
1467
- data: extractVulnDiscussion(rawDesc)
1468
- });
1506
+ data: stripHTML(extractVulnDiscussion(extractText(ruleDef?.description)))
1507
+ }];
1469
1508
  const fixtext = extractFixtext(ruleDef?.fixtext);
1470
1509
  if (fixtext) descriptions.push({
1471
1510
  label: "fix",
1472
- data: fixtext
1473
- });
1474
- if (descriptions.length === 0) descriptions.push({
1475
- label: "default",
1476
- data: ""
1511
+ data: stripHTML(fixtext)
1477
1512
  });
1478
- const status = STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed;
1513
+ const xccdfResult = (rr.result ?? "").trim().toLowerCase();
1514
+ const status = STATUS_MAP[xccdfResult] ?? ResultStatus.Error;
1479
1515
  const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
1480
- const result = createResult(status, "", {
1481
- codeDesc: `XCCDF rule ${id}`,
1516
+ const result = {
1517
+ status,
1518
+ codeDesc: `XCCDF rule ${ruleId}`,
1482
1519
  startTime: perRuleTime ?? scanTime
1483
- });
1484
- const tags = {};
1485
- let nistTags = [];
1486
- const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
1487
- if (cciIds.length > 0) {
1488
- tags["cci"] = cciIds;
1489
- nistTags = [...new Set(cciIds.flatMap((cci) => getCCINistMappings(cci) ?? []))];
1490
- if (nistTags.length > 0) tags["nist"] = nistTags;
1491
- }
1520
+ };
1521
+ const tags = buildCciNistTags(extractCCIs([...rr.ident ?? [], ...ruleDef?.ident ?? []]));
1522
+ const nistTags = tags["nist"];
1492
1523
  const req = createRequirement(id, title, descriptions, impact, [result], { tags });
1493
1524
  const controlType = deriveControlTypeFromTags(nistTags);
1494
1525
  if (controlType !== void 0) req.controlType = controlType;
@@ -1519,19 +1550,29 @@ function buildTargets(testResult) {
1519
1550
  const addresses = testResult["target-address"] ?? [];
1520
1551
  const target = {
1521
1552
  name: targetName,
1522
- type: TargetType.Host,
1523
- labels: {}
1553
+ type: TargetType.Host
1524
1554
  };
1525
1555
  if (addresses.length > 0) target.ipAddress = addresses[0];
1526
1556
  return [target];
1527
1557
  }
1528
- /**
1529
- * Extract plain text from a field that may be a string or {#text: string}.
1530
- */
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
+ }
1531
1572
  function extractText(field) {
1532
1573
  if (field === void 0 || field === null) return "";
1533
- if (typeof field === "string") return field;
1534
- return field["#text"] ?? "";
1574
+ if (typeof field === "string") return decodeXmlEntities$4(field);
1575
+ return decodeXmlEntities$4(field["#text"] ?? "");
1535
1576
  }
1536
1577
  /**
1537
1578
  * Extract version string from a version field.
@@ -1546,8 +1587,8 @@ function extractVersion$1(field) {
1546
1587
  */
1547
1588
  function extractFixtext(field) {
1548
1589
  if (field === void 0 || field === null) return "";
1549
- if (typeof field === "string") return field;
1550
- return field["#text"] ?? "";
1590
+ if (typeof field === "string") return decodeXmlEntities$4(field);
1591
+ return decodeXmlEntities$4(field["#text"] ?? "");
1551
1592
  }
1552
1593
  /**
1553
1594
  * Extract the VulnDiscussion text from an XCCDF description that contains
@@ -1556,16 +1597,38 @@ function extractFixtext(field) {
1556
1597
  */
1557
1598
  function extractVulnDiscussion(description) {
1558
1599
  const match = description.match(/<VulnDiscussion>([\s\S]*?)<\/VulnDiscussion>/);
1559
- if (match) return match[1].trim();
1560
- const entityMatch = description.match(/&lt;VulnDiscussion&gt;([\s\S]*?)&lt;\/VulnDiscussion&gt;/);
1561
- if (entityMatch) return entityMatch[1].trim();
1562
- return description;
1600
+ return match ? match[1] : description;
1563
1601
  }
1564
1602
  /**
1565
- * Extract CCI identifiers from ident elements (system="http://cyber.mil/cci").
1603
+ * Extract CCI identifiers from ident elements, deduplicated in first-seen order.
1566
1604
  */
1567
1605
  function extractCCIs(idents) {
1568
- 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}`;
1569
1632
  }
1570
1633
  //#endregion
1571
1634
  //#region shared/typescript/checklist/status.ts
@@ -1753,6 +1816,8 @@ function cklVulnToModel(v) {
1753
1816
  status: parseStatus(v.STATUS),
1754
1817
  findingDetails: str(v.FINDING_DETAILS),
1755
1818
  comments: str(v.COMMENTS),
1819
+ severityOverride: str(v.SEVERITY_OVERRIDE),
1820
+ severityJustification: str(v.SEVERITY_JUSTIFICATION),
1756
1821
  extra
1757
1822
  };
1758
1823
  }
@@ -1841,7 +1906,9 @@ function modelVulnToCkl(v) {
1841
1906
  STIG_DATA: stigData,
1842
1907
  STATUS: statusToCkl(v.status),
1843
1908
  FINDING_DETAILS: v.findingDetails ?? "",
1844
- COMMENTS: v.comments ?? ""
1909
+ COMMENTS: v.comments ?? "",
1910
+ SEVERITY_OVERRIDE: v.severityOverride ?? "",
1911
+ SEVERITY_JUSTIFICATION: v.severityJustification ?? ""
1845
1912
  };
1846
1913
  }
1847
1914
  //#endregion
@@ -2010,7 +2077,7 @@ function stigToBaseline(s, resultsChecksum, scanTime) {
2010
2077
  }
2011
2078
  function vulnToRequirement(v, scanTime) {
2012
2079
  const severity = (v.severity ?? "").toLowerCase();
2013
- const impact = severity ? severityToImpact(severity) : .5;
2080
+ const impact = severity ? severityToImpact$1(severity) : .5;
2014
2081
  const descriptions = [{
2015
2082
  label: "default",
2016
2083
  data: stripHTML(v.vulnDiscuss ?? "")
@@ -2023,7 +2090,7 @@ function vulnToRequirement(v, scanTime) {
2023
2090
  label: "fix",
2024
2091
  data: stripHTML(v.fixText)
2025
2092
  });
2026
- const message = [v.findingDetails, v.comments].map((s) => (s ?? "").trim()).filter(Boolean).join("\n\n");
2093
+ const message = (v.findingDetails ?? "").trim();
2027
2094
  const result = createResult(statusToHdf(v.status), message, {
2028
2095
  codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
2029
2096
  startTime: scanTime
@@ -2035,12 +2102,13 @@ function vulnToRequirement(v, scanTime) {
2035
2102
  nistTags = [...new Set(v.ccis.flatMap((c) => getCCINistMappings(c) ?? []))].sort();
2036
2103
  tags["nist"] = nistTags;
2037
2104
  } else tags["nist"] = [];
2038
- setIf(tags, "rid", v.ruleID);
2039
- setIf(tags, "stig_id", v.ruleVer);
2040
- setIf(tags, "gtitle", v.groupTitle);
2041
- setIf(tags, "group_id", v.groupID);
2042
- setIf(tags, "weight", v.weight);
2043
- 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);
2044
2112
  if (v.legacyIDs && v.legacyIDs.length) tags["legacy_ids"] = v.legacyIDs;
2045
2113
  if (v.extra && Object.keys(v.extra).length > 0) tags["cklMetadata"] = { ...v.extra };
2046
2114
  const req = createRequirement(v.vulnNum, v.ruleTitle ?? v.vulnNum, descriptions, impact, [result], { tags });
@@ -2055,6 +2123,7 @@ function assetToComponent(a) {
2055
2123
  name: a.hostName || a.hostFQDN || a.hostIP || "",
2056
2124
  type: TargetType.Host
2057
2125
  };
2126
+ if (a.hostName) c.hostname = a.hostName;
2058
2127
  if (a.hostIP) c.ipAddress = a.hostIP;
2059
2128
  if (a.hostFQDN) c.fqdn = a.hostFQDN;
2060
2129
  if (a.hostMAC) c.macAddress = a.hostMAC;
@@ -2064,30 +2133,30 @@ function rootExtensions(cl) {
2064
2133
  const ext = { checklistFormat: cl.format || "ckl" };
2065
2134
  if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
2066
2135
  const ax = {};
2067
- setIf(ax, "role", cl.asset.role);
2068
- setIf(ax, "assetType", cl.asset.assetType);
2069
- setIf(ax, "marking", cl.asset.marking);
2070
- setIf(ax, "targetKey", cl.asset.targetKey);
2071
- setIf(ax, "techArea", cl.asset.techArea);
2072
- setIf(ax, "targetComment", cl.asset.targetComment);
2073
- setIf(ax, "webDbSite", cl.asset.webDBSite);
2074
- setIf(ax, "webDbInstance", cl.asset.webDBInstance);
2075
- 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);
2076
2145
  if (cl.asset.webOrDatabase) ax["webOrDatabase"] = true;
2077
2146
  if (Object.keys(ax).length > 0) ext["assetExtras"] = ax;
2078
2147
  return ext;
2079
2148
  }
2080
2149
  function baselineExtensions(s) {
2081
2150
  const ext = {};
2082
- setIf(ext, "stigid", s.stigID);
2083
- setIf(ext, "uuid", s.uuid);
2084
- setIf(ext, "releaseInfo", s.releaseInfo);
2085
- setIf(ext, "displayName", s.displayName);
2086
- setIf(ext, "referenceIdentifier", s.referenceIdentifier);
2087
- 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);
2088
2157
  return ext;
2089
2158
  }
2090
- function setIf(m, key, val) {
2159
+ function setIf$1(m, key, val) {
2091
2160
  if (val) m[key] = val;
2092
2161
  }
2093
2162
  //#endregion
@@ -2099,7 +2168,7 @@ function setIf(m, key, val) {
2099
2168
  * fields are synthesized best-effort so any HDF yields a valid checklist.
2100
2169
  */
2101
2170
  function hdfToChecklist(input) {
2102
- const hdf = parseJSON(input);
2171
+ const hdf = parseHdf(input);
2103
2172
  if (!hdf || !Array.isArray(hdf.baselines) || hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
2104
2173
  const ext = hdf.extensions ?? {};
2105
2174
  const format = strVal(ext, "checklistFormat") || "ckl";
@@ -2117,7 +2186,8 @@ function buildAsset(hdf, ext) {
2117
2186
  const asset = {};
2118
2187
  const comp = hdf.components?.[0];
2119
2188
  if (comp) {
2120
- 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;
2121
2191
  asset.hostIP = comp.ipAddress;
2122
2192
  asset.hostFQDN = comp.fqdn;
2123
2193
  asset.hostMAC = comp.macAddress;
@@ -2171,6 +2241,7 @@ function requirementToVuln(req) {
2171
2241
  legacyIDs: strSlice(tags, "legacy_ids"),
2172
2242
  status: statusFromHdf(req.results?.[0]?.status),
2173
2243
  findingDetails: req.results?.[0]?.message,
2244
+ comments: strVal(tags, "comments"),
2174
2245
  extra: extractCklMetadata(tags)
2175
2246
  };
2176
2247
  }
@@ -2332,11 +2403,15 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2332
2403
  label: "default",
2333
2404
  data: rep.description
2334
2405
  }];
2335
- const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, {
2336
- codeDesc: formatDependencyPath(vuln.from),
2337
- startTime: scanTime
2338
- }));
2339
- 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 });
2340
2415
  const controlType = deriveControlTypeFromTags(nist);
2341
2416
  if (controlType !== void 0) req.controlType = controlType;
2342
2417
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -2389,12 +2464,12 @@ function convertSingleProject(report, resultsChecksum, scanTime) {
2389
2464
  * @param input - Snyk JSON or SARIF string
2390
2465
  * @returns HDF JSON string
2391
2466
  */
2392
- async function convertSnykToHdf(input) {
2467
+ async function convertSnykToHdf(input, converterVersion = "1.0.0") {
2393
2468
  if (!input || input.trim().length === 0) throw new Error("snyk: empty input");
2394
2469
  validateInputSize(input, "snyk");
2395
2470
  registerAllFingerprints();
2396
2471
  const detected = detectConverter(input);
2397
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
2472
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
2398
2473
  const resultsChecksum = await inputChecksum(input);
2399
2474
  const scanTime = /* @__PURE__ */ new Date();
2400
2475
  const parsed = parseJSON(input);
@@ -2413,7 +2488,7 @@ async function convertSnykToHdf(input) {
2413
2488
  }
2414
2489
  return buildHdfResults({
2415
2490
  generatorName: "snyk-to-hdf",
2416
- converterVersion: "1.0.0",
2491
+ converterVersion,
2417
2492
  toolName: "Snyk",
2418
2493
  toolFormat: "JSON",
2419
2494
  baselines,
@@ -2458,11 +2533,12 @@ function getFixInfo(fix) {
2458
2533
  function getCVSSInfo(vuln, relatedVulns) {
2459
2534
  const cvssData = {};
2460
2535
  if (vuln.cvss && vuln.cvss.length > 0) cvssData.primary = vuln.cvss;
2461
- 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) => ({
2462
2537
  id: r.id,
2463
2538
  dataSource: r.dataSource,
2464
2539
  cvss: r.cvss
2465
2540
  }));
2541
+ if (related.length > 0) cvssData.related = related;
2466
2542
  return JSON.stringify(cvssData);
2467
2543
  }
2468
2544
  function getReferences(vuln, relatedVulns) {
@@ -2634,7 +2710,7 @@ function convertMatchToRequirement(match, isIgnored) {
2634
2710
  if (kev) requirement.kev = kev;
2635
2711
  return requirement;
2636
2712
  }
2637
- async function convertGrypeToHdf(input) {
2713
+ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
2638
2714
  validateInputSize(input, "grype");
2639
2715
  const resultsChecksum = await inputChecksum(input);
2640
2716
  const grypeData = parseJSON(input);
@@ -2655,8 +2731,8 @@ async function convertGrypeToHdf(input) {
2655
2731
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
2656
2732
  const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
2657
2733
  return buildHdfResults({
2658
- generatorName: grypeData.descriptor?.name || "grype",
2659
- converterVersion: grypeData.descriptor?.version || "unknown",
2734
+ generatorName: "grype-to-hdf",
2735
+ converterVersion,
2660
2736
  toolName: "Grype",
2661
2737
  toolVersion: grypeData.descriptor?.version,
2662
2738
  baselines: [baseline],
@@ -2671,7 +2747,6 @@ async function convertGrypeToHdf(input) {
2671
2747
  //#region converters/nessus-to-hdf/typescript/converter.ts
2672
2748
  const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
2673
2749
  const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
2674
- const converterVersion = "1.0.0";
2675
2750
  const IMPACT_MAPPING$7 = {
2676
2751
  "4": .9,
2677
2752
  "3": .7,
@@ -2688,20 +2763,47 @@ const IMPACT_MAPPING$7 = {
2688
2763
  function parseHtml(html) {
2689
2764
  return html.replace(/<[^>]*>/g, "").trim();
2690
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
+ }
2691
2793
  /**
2692
2794
  * Convert Nessus XML scan results to HDF format
2693
2795
  */
2694
- async function convertNessusToHdf(nessusXml) {
2796
+ async function convertNessusToHdf(nessusXml, converterVersion = "1.0.0") {
2695
2797
  validateInputSize(nessusXml, "nessus");
2696
2798
  const resultsChecksum = await inputChecksum(nessusXml);
2697
- const parsed = parseXmlWithArrays(nessusXml, [
2799
+ const parsed = decodeEntitiesDeep(parseXmlWithArrays(nessusXml, [
2698
2800
  "preference",
2699
2801
  "tag",
2700
2802
  "ReportItem",
2701
2803
  "ReportHost",
2702
2804
  "cwe",
2703
2805
  "cve"
2704
- ]);
2806
+ ]));
2705
2807
  const policyName = parsed.NessusClientData_v2.Policy.policyName;
2706
2808
  const version = extractVersion(parsed);
2707
2809
  const reportHosts = parsed.NessusClientData_v2.Report.ReportHost;
@@ -2984,7 +3086,7 @@ function buildTags$2(item, isCompliance) {
2984
3086
  const cciTags = parseComplianceRef(item["compliance-reference"], "CCI");
2985
3087
  tags.cci = cciTags;
2986
3088
  const mappedControls = cciTags.flatMap((cci) => getCCINistMappings(cci) ?? []);
2987
- tags.nist = [...new Set(mappedControls)];
3089
+ tags.nist = [...new Set(mappedControls)].sort();
2988
3090
  } else {
2989
3091
  const nistControls = getNessusNistControl(item["pluginFamily"], item["pluginID"]);
2990
3092
  tags.nist = nistControls ? nistControls.split("|") : [];
@@ -3009,7 +3111,7 @@ function buildRefs(item) {
3009
3111
  function buildResult$1(item, host, isCompliance) {
3010
3112
  const status = getStatus$3(item, isCompliance);
3011
3113
  const codeDesc = getCodeDesc(item);
3012
- const message = item.plugin_output || item["compliance-actual-value"];
3114
+ const message = isCompliance && item["compliance-actual-value"] ? item["compliance-actual-value"] : item.plugin_output;
3013
3115
  const startTimeStr = getHostPropertyValue(host, "HOST_START");
3014
3116
  const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
3015
3117
  return {
@@ -3047,6 +3149,7 @@ function convertReportHostToTarget(host) {
3047
3149
  name: hostName,
3048
3150
  type: TargetType.Host
3049
3151
  };
3152
+ if (hostProps["hostname"]) target.hostname = hostProps["hostname"];
3050
3153
  if (isFQDN(hostName)) target.fqdn = hostName;
3051
3154
  const hostIp = hostProps["host-ip"];
3052
3155
  if (hostIp) target.ipAddress = hostIp;
@@ -3055,7 +3158,6 @@ function convertReportHostToTarget(host) {
3055
3158
  if (hostProps["os"]) target.osVersion = hostProps["os"];
3056
3159
  if (hostProps["mac-address"]) target.macAddress = hostProps["mac-address"].split("\n")[0];
3057
3160
  if (hostProps["host-fqdn"]) target.fqdn = hostProps["host-fqdn"];
3058
- target.labels = {};
3059
3161
  return target;
3060
3162
  }
3061
3163
  function isFQDN(s) {
@@ -3067,7 +3169,7 @@ function isIPAddress(s) {
3067
3169
  //#endregion
3068
3170
  //#region converters/sonarqube-to-hdf/typescript/converter.ts
3069
3171
  /**
3070
- * Severity to impact mapping.
3172
+ * Deprecated legacy severity to impact mapping.
3071
3173
  * Canonical reference: heimdall2 sonarqube-mapper.ts IMPACT_MAPPING.
3072
3174
  */
3073
3175
  const SEVERITY_IMPACT_MAPPING = {
@@ -3077,6 +3179,58 @@ const SEVERITY_IMPACT_MAPPING = {
3077
3179
  MINOR: .3,
3078
3180
  INFO: 0
3079
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
+ }
3080
3234
  /**
3081
3235
  * Default NIST tag for SonarQube findings without CWE mappings.
3082
3236
  * SA-11 (Developer Security Testing and Evaluation) applies to all issue types —
@@ -3089,7 +3243,7 @@ const DEFAULT_NIST_TAGS = ["SA-11"];
3089
3243
  * @param input - JSON string from SonarQube /api/issues/search endpoint
3090
3244
  * @returns HDF JSON string
3091
3245
  */
3092
- async function convertSonarqubeToHdf(input) {
3246
+ async function convertSonarqubeToHdf(input, converterVersion = "1.0.0") {
3093
3247
  validateInputSize(input, "sonarqube");
3094
3248
  const resultsChecksum = await inputChecksum(input);
3095
3249
  const sonarData = parseJSON(input);
@@ -3108,12 +3262,13 @@ async function convertSonarqubeToHdf(input) {
3108
3262
  if (!issuesByProject.has(projectKey)) issuesByProject.set(projectKey, []);
3109
3263
  issuesByProject.get(projectKey).push(issue);
3110
3264
  }
3265
+ const projectKeys = Array.from(issuesByProject.keys()).sort(byCodeUnit);
3111
3266
  const baselines = [];
3112
- for (const [projectKey, issues] of issuesByProject) {
3113
- 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);
3114
3269
  baselines.push(baseline);
3115
3270
  }
3116
- let components = Array.from(issuesByProject.keys()).map((projectKey) => ({
3271
+ let components = projectKeys.map((projectKey) => ({
3117
3272
  type: TargetType.Application,
3118
3273
  name: projectKey
3119
3274
  }));
@@ -3132,8 +3287,9 @@ async function convertSonarqubeToHdf(input) {
3132
3287
  }
3133
3288
  return buildHdfResults({
3134
3289
  generatorName: "sonarqube-to-hdf",
3135
- converterVersion: "1.0.0",
3290
+ converterVersion,
3136
3291
  toolName: "SonarQube",
3292
+ toolVersion: sonarData.serverVersion || void 0,
3137
3293
  baselines,
3138
3294
  components,
3139
3295
  timestamp: /* @__PURE__ */ new Date()
@@ -3155,8 +3311,8 @@ function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, res
3155
3311
  issuesByRule.get(ruleKey).push(issue);
3156
3312
  }
3157
3313
  const requirements = [];
3158
- for (const [ruleKey, ruleIssues] of issuesByRule) {
3159
- 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);
3160
3316
  requirements.push(requirement);
3161
3317
  }
3162
3318
  return createMinimalBaseline(projectKey, requirements, {
@@ -3169,7 +3325,7 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
3169
3325
  const title = rule?.name || ruleKey;
3170
3326
  const description = extractDescription(rule);
3171
3327
  const firstIssue = issues[0];
3172
- const impact = SEVERITY_IMPACT_MAPPING[firstIssue.severity] || .5;
3328
+ const { severity, source: severitySource, impact } = selectSeverity(firstIssue);
3173
3329
  const { cweIds, owaspTags, allTags } = extractTags(rule, issues);
3174
3330
  const nistControls = mapCWEToNIST(cweIds, DEFAULT_NIST_TAGS);
3175
3331
  const cciControls = nistToCci(nistControls);
@@ -3177,12 +3333,18 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
3177
3333
  const issueWithLocation = issues.find((i) => i.line !== void 0);
3178
3334
  const sourceLocation = issueWithLocation ? extractSourceLocation(issueWithLocation, componentMap) : void 0;
3179
3335
  const options = { tags: {
3180
- severity: firstIssue.severity.toLowerCase(),
3336
+ severity: severity.toLowerCase(),
3337
+ severitySource,
3181
3338
  type: firstIssue.type.toLowerCase(),
3182
3339
  cwe: cweIds,
3183
3340
  owasp: owaspTags,
3184
3341
  nist: nistControls,
3185
3342
  cci: cciControls,
3343
+ ...severitySource === SEVERITY_SOURCE_MQR ? {
3344
+ legacySeverity: firstIssue.severity.toLowerCase(),
3345
+ impacts: firstIssue.impacts,
3346
+ ...firstIssue.cleanCodeAttribute ? { cleanCodeAttribute: firstIssue.cleanCodeAttribute } : {}
3347
+ } : {},
3186
3348
  ...allTags
3187
3349
  } };
3188
3350
  if (sourceLocation) options.sourceLocation = sourceLocation;
@@ -3276,11 +3438,15 @@ function mapComplianceStatus(complianceType) {
3276
3438
  default: return ResultStatus.NotReviewed;
3277
3439
  }
3278
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
+ }
3279
3445
  function buildNistTags$3(sourceIdentifier, ruleName) {
3280
- const byIdentifier = getAwsConfigNistControlByIdentifier(sourceIdentifier);
3281
- if (byIdentifier) return [byIdentifier];
3446
+ const byIdentifier = sourceIdentifier ? getAwsConfigNistControlByIdentifier(sourceIdentifier) : void 0;
3447
+ if (byIdentifier) return splitControls(byIdentifier);
3282
3448
  const byName = getAwsConfigNistControlByName(ruleName);
3283
- if (byName) return [byName];
3449
+ if (byName) return splitControls(byName);
3284
3450
  return [];
3285
3451
  }
3286
3452
  /**
@@ -3320,16 +3486,27 @@ function buildResultMessage(codeDesc, annotation, status) {
3320
3486
  if (status !== ResultStatus.Failed) return void 0;
3321
3487
  return `(${codeDesc}): ${annotation || "Rule does not pass rule compliance"}`;
3322
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
+ }
3323
3496
  function buildResult(r) {
3324
3497
  const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
3325
3498
  const status = mapComplianceStatus(r.ComplianceType);
3326
3499
  const codeDesc = buildCodeDesc$7(q);
3327
3500
  const message = buildResultMessage(codeDesc, r.Annotation, status);
3328
3501
  const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
3329
- return createResult(status, message ?? codeDesc, {
3502
+ const runTime = computeRunTime(r.ConfigRuleInvokedTime, r.ResultRecordedTime);
3503
+ return {
3504
+ status,
3330
3505
  codeDesc,
3331
- ...startTime ? { startTime } : {}
3332
- });
3506
+ startTime,
3507
+ ...runTime !== void 0 ? { runTime } : {},
3508
+ ...message !== void 0 ? { message } : {}
3509
+ };
3333
3510
  }
3334
3511
  /**
3335
3512
  * Synthesizes a single HDF result for a Config rule whose live evaluation
@@ -3340,10 +3517,11 @@ function buildResult(r) {
3340
3517
  */
3341
3518
  function buildNotApplicableResult(rule) {
3342
3519
  const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
3343
- return createResult(ResultStatus.NotApplicable, codeDesc, {
3520
+ return {
3521
+ status: ResultStatus.NotApplicable,
3344
3522
  codeDesc,
3345
3523
  startTime: /* @__PURE__ */ new Date()
3346
- });
3524
+ };
3347
3525
  }
3348
3526
  function buildRequirement$15(rule) {
3349
3527
  const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
@@ -3376,7 +3554,7 @@ function buildRequirement$15(rule) {
3376
3554
  * with get-compliance-details-by-config-rule
3377
3555
  * @returns HDF JSON string
3378
3556
  */
3379
- async function convertAwsConfigToHdf(input) {
3557
+ async function convertAwsConfigToHdf(input, converterVersion = "1.0.0") {
3380
3558
  validateInputSize(input, "aws-config");
3381
3559
  const resultsChecksum = await inputChecksum(input);
3382
3560
  const data = parseJSON(input);
@@ -3397,7 +3575,7 @@ async function convertAwsConfigToHdf(input) {
3397
3575
  const region = getRegion(firstArn);
3398
3576
  return buildHdfResults({
3399
3577
  generatorName: "aws-config-to-hdf",
3400
- converterVersion: "1.0.0",
3578
+ converterVersion,
3401
3579
  toolName: "AWS Config",
3402
3580
  baselines: [baseline],
3403
3581
  components: [{
@@ -3430,7 +3608,7 @@ function mapStatus$2(result) {
3430
3608
  */
3431
3609
  function getImpact$8(severity) {
3432
3610
  if (!severity) return .5;
3433
- return severityToImpact(severity);
3611
+ return severityToImpact$1(severity);
3434
3612
  }
3435
3613
  /**
3436
3614
  * Converts a single CheckovCheck to an HDF RequirementResult.
@@ -3440,10 +3618,12 @@ function checkToResult(check, scanTime) {
3440
3618
  const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
3441
3619
  let message;
3442
3620
  if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
3443
- return createResult(status, message ?? "", {
3621
+ return {
3622
+ status,
3444
3623
  codeDesc,
3445
- startTime: scanTime
3446
- });
3624
+ startTime: scanTime,
3625
+ ...message !== void 0 ? { message } : {}
3626
+ };
3447
3627
  }
3448
3628
  /**
3449
3629
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
@@ -3485,11 +3665,11 @@ function parseInput(input) {
3485
3665
  * @param input - checkov JSON or SARIF string
3486
3666
  * @returns HDF JSON string
3487
3667
  */
3488
- async function convertCheckovToHdf(input) {
3668
+ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
3489
3669
  validateInputSize(input, "checkov");
3490
3670
  registerAllFingerprints();
3491
3671
  const detected = detectConverter(input);
3492
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3672
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
3493
3673
  const resultsChecksum = await inputChecksum(input);
3494
3674
  const scanTime = /* @__PURE__ */ new Date();
3495
3675
  const reports = parseInput(input);
@@ -3522,7 +3702,7 @@ async function convertCheckovToHdf(input) {
3522
3702
  const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
3523
3703
  return buildHdfResults({
3524
3704
  generatorName: "checkov-to-hdf",
3525
- converterVersion: "1.0.0",
3705
+ converterVersion,
3526
3706
  toolName: "Checkov",
3527
3707
  toolVersion: version,
3528
3708
  toolFormat: format,
@@ -3606,11 +3786,11 @@ function buildRequirement$13(ruleId, issues, scanTime) {
3606
3786
  * @param input - gosec JSON or SARIF string
3607
3787
  * @returns HDF JSON string
3608
3788
  */
3609
- async function convertGosecToHdf(input) {
3789
+ async function convertGosecToHdf(input, converterVersion = "1.0.0") {
3610
3790
  validateInputSize(input, "gosec");
3611
3791
  registerAllFingerprints();
3612
3792
  const detected = detectConverter(input);
3613
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3793
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
3614
3794
  const resultsChecksum = await inputChecksum(input);
3615
3795
  const report = parseJSON(input);
3616
3796
  if (!report || typeof report !== "object") throw new Error("Invalid gosec structure: not a valid JSON object");
@@ -3631,7 +3811,7 @@ async function convertGosecToHdf(input) {
3631
3811
  const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
3632
3812
  return buildHdfResults({
3633
3813
  generatorName: "gosec-to-hdf",
3634
- converterVersion: "1.0.0",
3814
+ converterVersion,
3635
3815
  toolName: "gosec",
3636
3816
  toolVersion: report.GosecVersion || void 0,
3637
3817
  baselines: [baseline],
@@ -3678,7 +3858,7 @@ function convertVulnToRequirement(vuln) {
3678
3858
  req.verificationMethod = VerificationMethodEnum.Automated;
3679
3859
  return req;
3680
3860
  }
3681
- async function convertNiktoToHdf(input) {
3861
+ async function convertNiktoToHdf(input, converterVersion = "unknown") {
3682
3862
  validateInputSize(input, "nikto");
3683
3863
  const resultsChecksum = await inputChecksum(input);
3684
3864
  const niktoData = parseJSON(input);
@@ -3711,7 +3891,7 @@ async function convertNiktoToHdf(input) {
3711
3891
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
3712
3892
  return buildHdfResults({
3713
3893
  generatorName: "nikto-to-hdf",
3714
- converterVersion: "unknown",
3894
+ converterVersion,
3715
3895
  toolName: "Nikto",
3716
3896
  toolFormat: "JSON",
3717
3897
  baselines: [createMinimalBaseline(targetName, requirements, {
@@ -3784,11 +3964,11 @@ function parseZapTimestamp(s) {
3784
3964
  }
3785
3965
  return parseTimestamp(s) ?? void 0;
3786
3966
  }
3787
- async function convertZapToHdf(input) {
3967
+ async function convertZapToHdf(input, converterVersion = "1.0.0") {
3788
3968
  validateInputSize(input, "zap");
3789
3969
  registerAllFingerprints();
3790
3970
  const detected = detectConverter(input);
3791
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3971
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
3792
3972
  const resultsChecksum = await inputChecksum(input);
3793
3973
  const zapData = parseJSON(input);
3794
3974
  const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
@@ -3881,7 +4061,7 @@ async function convertZapToHdf(input) {
3881
4061
  components,
3882
4062
  generator: {
3883
4063
  name: "zap-to-hdf",
3884
- version: "unknown"
4064
+ version: converterVersion
3885
4065
  },
3886
4066
  tool
3887
4067
  };
@@ -3892,6 +4072,579 @@ async function convertZapToHdf(input) {
3892
4072
  return serializeHdf(hdf);
3893
4073
  }
3894
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
3895
4648
  //#region converters/cyclonedx-to-hdf/typescript/converter.ts
3896
4649
  const CVSS_METHODS = /* @__PURE__ */ new Set([
3897
4650
  "CVSSv2",
@@ -3909,7 +4662,7 @@ function maxImpact(ratings) {
3909
4662
  for (const rating of ratings) {
3910
4663
  let impact;
3911
4664
  if (rating.method && CVSS_METHODS.has(rating.method) && rating.score !== void 0 && rating.score !== null) impact = rating.score / 10;
3912
- else impact = severityToImpact(rating.severity ?? "medium");
4665
+ else impact = severityToImpact$1(rating.severity ?? "medium");
3913
4666
  if (impact > max) max = impact;
3914
4667
  }
3915
4668
  return max;
@@ -3944,19 +4697,29 @@ function flattenComponents(components) {
3944
4697
  return result;
3945
4698
  }
3946
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
+ /**
3947
4707
  * Converts CycloneDX SBOM/VEX JSON to HDF format.
3948
4708
  *
3949
4709
  * @param input - CycloneDX JSON string
3950
4710
  * @returns HDF JSON string
3951
4711
  */
3952
- async function convertCyclonedxToHdf(input) {
4712
+ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
3953
4713
  if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
3954
4714
  validateInputSize(input, "cyclonedx");
3955
4715
  const bom = parseJSON(input);
3956
4716
  if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
3957
4717
  if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
3958
4718
  if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
3959
- 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
+ }
3960
4723
  const resultsChecksum = await inputChecksum(input);
3961
4724
  const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
3962
4725
  const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
@@ -3998,13 +4761,12 @@ async function convertCyclonedxToHdf(input) {
3998
4761
  data: fixParts.join("\n\n")
3999
4762
  });
4000
4763
  const affects = vuln.affects ?? [];
4001
- const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, {
4002
- codeDesc: formatCodeDesc$4(componentLookup, affect.ref),
4003
- startTime: scanTime
4004
- })) : [createResult(ResultStatus.Failed, void 0, {
4005
- codeDesc: `Vulnerability ${vuln.id}`,
4764
+ const toResult = (codeDesc) => ({
4765
+ status: ResultStatus.Failed,
4766
+ codeDesc,
4006
4767
  startTime: scanTime
4007
- })];
4768
+ });
4769
+ const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
4008
4770
  const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
4009
4771
  const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
4010
4772
  const controlType = deriveControlTypeFromTags(nist);
@@ -4012,16 +4774,29 @@ async function convertCyclonedxToHdf(input) {
4012
4774
  requirements.push(req);
4013
4775
  }
4014
4776
  const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
4015
- 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);
4016
4791
  return buildHdfResults({
4017
4792
  generatorName: "cyclonedx-to-hdf",
4018
- converterVersion: "1.0.0",
4793
+ converterVersion,
4019
4794
  toolName: "CycloneDX",
4020
4795
  toolFormat: "JSON",
4021
4796
  baselines: [baseline],
4022
4797
  components: [{
4023
- name: targetName,
4024
- type: TargetType.Application
4798
+ ...targetComponent,
4799
+ boms: [componentBom]
4025
4800
  }],
4026
4801
  timestamp: scanTime
4027
4802
  });
@@ -4035,7 +4810,7 @@ async function convertCyclonedxToHdf(input) {
4035
4810
  */
4036
4811
  function convertHdfToCsv(input) {
4037
4812
  validateInputSize(input, "hdf-to-csv");
4038
- const hdf = parseJSON(input);
4813
+ const hdf = parseHdf(input);
4039
4814
  if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
4040
4815
  if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
4041
4816
  const rows = [];
@@ -4055,6 +4830,11 @@ function convertHdfToCsv(input) {
4055
4830
  */
4056
4831
  function createRow(baseline, requirement, target) {
4057
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);
4058
4838
  const severity = getSeverity(requirement);
4059
4839
  const firstResult = requirement.results[0];
4060
4840
  const status = firstResult?.status || "";
@@ -4070,6 +4850,11 @@ function createRow(baseline, requirement, target) {
4070
4850
  "Requirement ID": requirement.id,
4071
4851
  "Requirement Title": requirement.title || "",
4072
4852
  "Description": description,
4853
+ "Check": check,
4854
+ "Fix": fix,
4855
+ "Rationale": rationale,
4856
+ "Code": code,
4857
+ "References": references,
4073
4858
  "Severity": severity,
4074
4859
  "Impact": requirement.impact,
4075
4860
  "Status": String(status),
@@ -4092,21 +4877,826 @@ function getSeverity(requirement) {
4092
4877
  if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
4093
4878
  }
4094
4879
  }
4095
- const impact = requirement.impact;
4096
- if (impact >= .7) return "high";
4097
- if (impact >= .4) return "medium";
4098
- 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];
4099
5455
  }
4100
- /**
4101
- * Extract array values from tags object and join with semicolons
4102
- */
4103
- function extractArrayFromTags(tags, key) {
4104
- if (!tags || typeof tags !== "object") return "";
4105
- const value = tags[key];
4106
- 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;
4107
5459
  return "";
4108
5460
  }
4109
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
4110
5700
  //#region converters/splunk-to-hdf/typescript/converter.ts
4111
5701
  /**
4112
5702
  * Map a Splunk result status string to the HDF ResultStatus enum.
@@ -4122,11 +5712,12 @@ function mapStatus$1(status) {
4122
5712
  }
4123
5713
  /**
4124
5714
  * Convert a Splunk descriptions object (`{ key: value }`) to an array of
4125
- * 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.
4126
5717
  */
4127
5718
  function convertDescriptions(descriptions) {
4128
5719
  if (!descriptions || typeof descriptions !== "object") return [];
4129
- 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));
4130
5721
  }
4131
5722
  /**
4132
5723
  * Convert Splunk profile groups (which use `controls`) to HDF
@@ -4150,7 +5741,7 @@ function convertGroups(groups) {
4150
5741
  * @param input - JSON string of a SplunkEvent array
4151
5742
  * @returns HDF Results JSON string
4152
5743
  */
4153
- async function convertSplunkToHdf(input) {
5744
+ async function convertSplunkToHdf(input, converterVersion = "1.0.0") {
4154
5745
  validateInputSize(input, "splunk");
4155
5746
  const resultsChecksum = await inputChecksum(input);
4156
5747
  const events = parseJSON(input);
@@ -4162,9 +5753,10 @@ async function convertSplunkToHdf(input) {
4162
5753
  eventsByGuid.get(guid).push(event);
4163
5754
  }
4164
5755
  const allBaselines = [];
4165
- let targetName = "unknown";
4166
- let targetRelease = "";
4167
- 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);
4168
5760
  const headers = [];
4169
5761
  const profiles = [];
4170
5762
  const controls = [];
@@ -4181,8 +5773,13 @@ async function convertSplunkToHdf(input) {
4181
5773
  }
4182
5774
  if (headers.length !== 1) throw new Error(`Expected 1 header event, got ${headers.length}`);
4183
5775
  const header = headers[0];
4184
- targetName = header.platform.name;
4185
- 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);
4186
5783
  const controlsByProfile = /* @__PURE__ */ new Map();
4187
5784
  for (const control of controls) {
4188
5785
  const sha = control.meta.profile_sha256 ?? "";
@@ -4192,16 +5789,24 @@ async function convertSplunkToHdf(input) {
4192
5789
  for (const profile of profiles) {
4193
5790
  const requirements = (controlsByProfile.get(profile.sha256) ?? []).map((control) => {
4194
5791
  const descriptions = convertDescriptions(control.descriptions);
4195
- const results = Array.isArray(control.results) ? control.results.map((result) => createResult(mapStatus$1(result.status), result.message, {
4196
- codeDesc: result.code_desc,
4197
- startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
4198
- runTime: result.run_time,
4199
- exception: result.exception,
4200
- backtrace: result.backtrace
4201
- })) : [];
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
+ }) : [];
4202
5805
  const options = { tags: control.tags ?? {} };
4203
5806
  if (control.source_location) options.sourceLocation = control.source_location;
4204
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;
4205
5810
  const nistTagsRaw = control.tags?.["nist"];
4206
5811
  const controlType = deriveControlTypeFromTags(Array.isArray(nistTagsRaw) ? nistTagsRaw.filter((t) => typeof t === "string") : []);
4207
5812
  if (controlType !== void 0) req.controlType = controlType;
@@ -4214,23 +5819,34 @@ async function convertSplunkToHdf(input) {
4214
5819
  if (profile.version) baselineOptions.version = profile.version;
4215
5820
  if (profile.summary) baselineOptions.summary = profile.summary;
4216
5821
  if (groups.length > 0) baselineOptions.groups = groups;
4217
- 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);
4218
5831
  }
4219
5832
  }
4220
- return serializeHdf({
5833
+ return buildHdfResults({
5834
+ generatorName: "splunk-to-hdf",
5835
+ converterVersion,
5836
+ toolName: "Splunk",
4221
5837
  baselines: allBaselines,
4222
- components: [{
4223
- name: targetName,
4224
- type: TargetType.Host,
4225
- osName: targetRelease || void 0,
4226
- labels: {}
4227
- }],
4228
- generator: {
4229
- name: "splunk-to-hdf",
4230
- version: "1.0.0"
4231
- }
5838
+ components,
5839
+ statistics,
5840
+ timestamp: /* @__PURE__ */ new Date()
4232
5841
  });
4233
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
+ }
4234
5850
  //#endregion
4235
5851
  //#region converters/hdf-to-xml/typescript/converter.ts
4236
5852
  /**
@@ -4240,7 +5856,7 @@ async function convertSplunkToHdf(input) {
4240
5856
  */
4241
5857
  function convertHdfToXml(input) {
4242
5858
  validateInputSize(input, "hdf-to-xml");
4243
- const hdf = parseJSON(input);
5859
+ const hdf = parseHdf(input);
4244
5860
  if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
4245
5861
  if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
4246
5862
  return buildXml({ HdfResults: transformHdfToXmlObject(hdf) });
@@ -4252,6 +5868,20 @@ function wrap$1(value) {
4252
5868
  return { "#text": value };
4253
5869
  }
4254
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
+ /**
4255
5885
  * Transform HDF object to XML-compatible structure
4256
5886
  * Converts arrays to repeated singular elements
4257
5887
  */
@@ -4261,6 +5891,16 @@ function transformHdfToXmlObject(hdf) {
4261
5891
  name: wrap$1(baseline.name),
4262
5892
  ...baseline.version && { version: wrap$1(baseline.version) },
4263
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
+ } },
4264
5904
  ...baseline.integrity && { integrity: {
4265
5905
  ...baseline.integrity.algorithm && { algorithm: wrap$1(baseline.integrity.algorithm) },
4266
5906
  ...baseline.integrity.checksum && { checksum: wrap$1(baseline.integrity.checksum) }
@@ -4270,11 +5910,13 @@ function transformHdfToXmlObject(hdf) {
4270
5910
  })) };
4271
5911
  else result.baselines = {};
4272
5912
  if (hdf.components && hdf.components.length > 0) result.components = { target: hdf.components.map((target) => ({
5913
+ ...target.componentId && { componentId: wrap$1(target.componentId) },
4273
5914
  name: wrap$1(target.name),
4274
5915
  type: wrap$1(target.type),
5916
+ ...target.hostname && { hostname: wrap$1(target.hostname) },
4275
5917
  ...target.fqdn && { fqdn: wrap$1(target.fqdn) },
4276
- ...target.ipAddress && { ipAddress: wrap$1(target.ipAddress) },
4277
- ...target.hostname && { hostname: wrap$1(target.hostname) }
5918
+ ...target.domain && { domain: wrap$1(target.domain) },
5919
+ ...target.ipAddress && { ipAddress: wrap$1(target.ipAddress) }
4278
5920
  })) };
4279
5921
  if (hdf.statistics) {
4280
5922
  result.statistics = {};
@@ -4282,7 +5924,10 @@ function transformHdfToXmlObject(hdf) {
4282
5924
  if (hdf.statistics.requirements) result.statistics.requirements = hdf.statistics.requirements;
4283
5925
  }
4284
5926
  if (hdf.timestamp) result.timestamp = wrap$1(typeof hdf.timestamp === "string" ? hdf.timestamp : hdf.timestamp.toISOString());
4285
- 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
+ };
4286
5931
  return result;
4287
5932
  }
4288
5933
  /**
@@ -4296,11 +5941,20 @@ function transformRequirement(req) {
4296
5941
  label: wrap$1(d.label),
4297
5942
  data: wrap$1(d.data)
4298
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) } },
4299
5953
  impact: wrap$1(req.impact)
4300
5954
  };
4301
5955
  if (req.tags && Object.keys(req.tags).length > 0) {
4302
5956
  const transformedTags = {};
4303
- 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));
4304
5958
  else if (!Array.isArray(value)) transformedTags[key] = wrap$1(value);
4305
5959
  if (Object.keys(transformedTags).length > 0) result.tags = transformedTags;
4306
5960
  }
@@ -4316,7 +5970,7 @@ function transformRequirement(req) {
4316
5970
  //#endregion
4317
5971
  //#region converters/gitlab-to-hdf/typescript/converter.ts
4318
5972
  function gitlabSeverityToImpact(severity) {
4319
- return severityToImpact(severity.toLowerCase());
5973
+ return severityToImpact$1(severity.toLowerCase());
4320
5974
  }
4321
5975
  function scanTypeToTargetType(scanType) {
4322
5976
  switch (scanType) {
@@ -4406,7 +6060,7 @@ function buildCodeDesc$4(scanType, location) {
4406
6060
  default: return `Location: ${JSON.stringify(location)}`;
4407
6061
  }
4408
6062
  }
4409
- async function convertGitlabToHdf(input) {
6063
+ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
4410
6064
  validateInputSize(input, "gitlab");
4411
6065
  const resultsChecksum = await inputChecksum(input);
4412
6066
  const report = parseJSON(input);
@@ -4481,7 +6135,7 @@ async function convertGitlabToHdf(input) {
4481
6135
  components,
4482
6136
  generator: {
4483
6137
  name: "gitlab-to-hdf",
4484
- version: "unknown"
6138
+ version: converterVersion
4485
6139
  },
4486
6140
  tool
4487
6141
  };
@@ -4534,6 +6188,18 @@ function groupFindings(findings) {
4534
6188
  return groups;
4535
6189
  }
4536
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
+ /**
4537
6203
  * Build the Result.Message JSON from selected finding fields.
4538
6204
  */
4539
6205
  function buildMessage(f) {
@@ -4542,7 +6208,7 @@ function buildMessage(f) {
4542
6208
  Redacted: f.Redacted
4543
6209
  };
4544
6210
  if (f.VerificationError) msg.VerificationError = f.VerificationError;
4545
- 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);
4546
6212
  return JSON.stringify(msg);
4547
6213
  }
4548
6214
  /**
@@ -4615,7 +6281,7 @@ function buildRequirement$12(reqID, findings) {
4615
6281
  * @param input - TruffleHog JSON/NDJSON string
4616
6282
  * @returns HDF JSON string
4617
6283
  */
4618
- async function convertTrufflehogToHdf(input) {
6284
+ async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
4619
6285
  if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
4620
6286
  validateInputSize(input, "trufflehog");
4621
6287
  const findings = parseFindings(input);
@@ -4635,7 +6301,7 @@ async function convertTrufflehogToHdf(input) {
4635
6301
  })],
4636
6302
  generator: {
4637
6303
  name: "trufflehog-to-hdf",
4638
- version: "1.0.0"
6304
+ version: converterVersion
4639
6305
  },
4640
6306
  tool: {
4641
6307
  name: "TruffleHog",
@@ -4686,7 +6352,7 @@ function formatCodeDesc$3(hostIP, hostURL, location, issueDetail, confidence) {
4686
6352
  * @param input - BurpSuite XML string
4687
6353
  * @returns HDF JSON string
4688
6354
  */
4689
- async function convertBurpsuiteToHdf(input) {
6355
+ async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
4690
6356
  if (!input || input.trim().length === 0) throw new Error("burpsuite: empty input");
4691
6357
  validateInputSize(input, "burpsuite");
4692
6358
  const resultsChecksum = await inputChecksum(input);
@@ -4726,7 +6392,7 @@ async function convertBurpsuiteToHdf(input) {
4726
6392
  }],
4727
6393
  generator: {
4728
6394
  name: "burpsuite-to-hdf",
4729
- version: "1.0.0"
6395
+ version: converterVersion
4730
6396
  },
4731
6397
  tool
4732
6398
  };
@@ -4787,6 +6453,26 @@ const IMPACT_MAPPING$4 = {
4787
6453
  low: .3,
4788
6454
  informational: 0
4789
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
+ }
4790
6476
  /**
4791
6477
  * Maps metadata column names to row values by position index.
4792
6478
  * Mirrors the heimdall2 compileFindings function.
@@ -4801,7 +6487,7 @@ function compileFindings(parsed) {
4801
6487
  colNames.forEach((name, i) => {
4802
6488
  const val = values[i];
4803
6489
  if (val === null || val === void 0 || typeof val === "object") finding[name] = "";
4804
- else finding[name] = String(val);
6490
+ else finding[name] = decodeXmlEntities$2(String(val));
4805
6491
  });
4806
6492
  return finding;
4807
6493
  });
@@ -4876,10 +6562,11 @@ function buildRequirement$10(checkID, findings, hasStatus) {
4876
6562
  data: formatDesc$1(rep)
4877
6563
  }];
4878
6564
  const results = findings.map((f) => {
4879
- return createResult(hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed, void 0, {
6565
+ return {
6566
+ status: hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed,
4880
6567
  codeDesc: f["Details"] ?? "",
4881
6568
  startTime: parseDate(f["Date"] ?? "")
4882
- });
6569
+ };
4883
6570
  });
4884
6571
  const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
4885
6572
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -4895,7 +6582,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
4895
6582
  * @param input - DBProtect XML string
4896
6583
  * @returns HDF JSON string
4897
6584
  */
4898
- async function convertDbprotectToHdf(input) {
6585
+ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
4899
6586
  if (!input || input.trim().length === 0) throw new Error("dbprotect: empty input");
4900
6587
  validateInputSize(input, "dbprotect");
4901
6588
  const resultsChecksum = await inputChecksum(input);
@@ -4923,16 +6610,19 @@ async function convertDbprotectToHdf(input) {
4923
6610
  const title = firstFinding["Job Name"] ?? "";
4924
6611
  const summary = formatSummary(firstFinding);
4925
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;
4926
6620
  return buildHdfResults({
4927
6621
  generatorName: "dbprotect-to-hdf",
4928
- converterVersion: "1.0.0",
6622
+ converterVersion,
4929
6623
  toolName: "DBProtect",
4930
6624
  toolFormat: "XML",
4931
- baselines: [createMinimalBaseline("DBProtect Scan", requirements, {
4932
- resultsChecksum,
4933
- title,
4934
- summary
4935
- })],
6625
+ baselines: [baseline],
4936
6626
  components: [{
4937
6627
  name: targetName,
4938
6628
  type: TargetType.Host
@@ -5121,7 +6811,7 @@ function buildPackageTypeIndex(pkgs) {
5121
6811
  */
5122
6812
  function formatCodeDesc$2(vuln) {
5123
6813
  const packageName = vuln.packageName ?? "N/A";
5124
- 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";
5125
6815
  return `Package ${JSON.stringify(packageName)} should be updated to latest version above impacted versions ${impactedVersions}`;
5126
6816
  }
5127
6817
  /**
@@ -5142,10 +6832,11 @@ function buildRequirement$9(vuln, packageTypes, distro) {
5142
6832
  data: vuln.description
5143
6833
  }];
5144
6834
  const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5145
- const results = [createResult(ResultStatus.Failed, void 0, {
6835
+ const results = [{
6836
+ status: ResultStatus.Failed,
5146
6837
  codeDesc: formatCodeDesc$2(vuln),
5147
6838
  startTime
5148
- })];
6839
+ }];
5149
6840
  const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
5150
6841
  const controlType = deriveControlTypeFromTags(nist);
5151
6842
  if (controlType !== void 0) req.controlType = controlType;
@@ -5187,7 +6878,7 @@ function convertSingleResult(result, resultsChecksum) {
5187
6878
  * @param input - Twistlock JSON string
5188
6879
  * @returns HDF JSON string
5189
6880
  */
5190
- async function convertTwistlockToHdf(input) {
6881
+ async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
5191
6882
  if (!input || input.trim().length === 0) throw new Error("twistlock: empty input");
5192
6883
  validateInputSize(input, "twistlock");
5193
6884
  const resultsChecksum = await inputChecksum(input);
@@ -5198,18 +6889,19 @@ async function convertTwistlockToHdf(input) {
5198
6889
  else results = [parsed];
5199
6890
  if (results.length === 0) throw new Error("twistlock: no scan results found");
5200
6891
  const baselines = results.map((result) => convertSingleResult(result, resultsChecksum));
5201
- 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 };
5202
6898
  return buildHdfResults({
5203
6899
  generatorName: "twistlock-to-hdf",
5204
- converterVersion: "1.0.0",
6900
+ converterVersion,
5205
6901
  toolName: "Twistlock",
5206
6902
  toolFormat: "JSON",
5207
6903
  baselines,
5208
- components: [{
5209
- name: targetName,
5210
- type: TargetType.ContainerImage,
5211
- labels: { image: results[0]?.id ?? targetName }
5212
- }],
6904
+ components: [component],
5213
6905
  timestamp: /* @__PURE__ */ new Date()
5214
6906
  });
5215
6907
  }
@@ -5271,10 +6963,11 @@ function buildRequirement$8(finding, timestamp) {
5271
6963
  data: finding.vulnerability.recommendation
5272
6964
  });
5273
6965
  const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
5274
- const results = [createResult(ResultStatus.Failed, void 0, {
6966
+ const results = [{
6967
+ status: ResultStatus.Failed,
5275
6968
  codeDesc,
5276
6969
  startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
5277
- })];
6970
+ }];
5278
6971
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
5279
6972
  req.verificationMethod = VerificationMethodEnum.Automated;
5280
6973
  const controlType = deriveControlTypeFromTags(nist);
@@ -5308,7 +7001,7 @@ function buildAffectedPackageFromComponent(c) {
5308
7001
  * @param input - Dependency-Track FPF JSON string
5309
7002
  * @returns HDF JSON string
5310
7003
  */
5311
- async function convertDeptrackToHdf(input) {
7004
+ async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
5312
7005
  if (!input || input.trim().length === 0) throw new Error("deptrack: empty input");
5313
7006
  validateInputSize(input, "deptrack");
5314
7007
  const resultsChecksum = await inputChecksum(input);
@@ -5328,7 +7021,7 @@ async function convertDeptrackToHdf(input) {
5328
7021
  const targetName = parsed.project?.name ?? parsed.project?.uuid ?? "";
5329
7022
  return buildHdfResults({
5330
7023
  generatorName: "deptrack-to-hdf",
5331
- converterVersion: "1.0.0",
7024
+ converterVersion,
5332
7025
  toolName: "Dependency-Track",
5333
7026
  toolFormat: "JSON",
5334
7027
  baselines: [baseline],
@@ -5407,11 +7100,12 @@ function buildRequirement$7(entryID, entries, scanTime) {
5407
7100
  label: "default",
5408
7101
  data: formatDescription(rep)
5409
7102
  }];
5410
- const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, {
7103
+ const results = entries.map((entry) => ({
7104
+ status: ResultStatus.Failed,
5411
7105
  codeDesc: formatCodeDesc$1(entry),
5412
7106
  startTime: scanTime
5413
7107
  }));
5414
- 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 });
5415
7109
  const controlType = deriveControlTypeFromTags(nist);
5416
7110
  if (controlType !== void 0) req.controlType = controlType;
5417
7111
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -5464,7 +7158,7 @@ function buildAffectedPackageFromEntry(entry) {
5464
7158
  * @param input - JFrog Xray JSON string
5465
7159
  * @returns HDF JSON string
5466
7160
  */
5467
- async function convertJfrogXrayToHdf(input) {
7161
+ async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
5468
7162
  if (!input || input.trim().length === 0) throw new Error("jfrog-xray: empty input");
5469
7163
  validateInputSize(input, "jfrog-xray");
5470
7164
  const resultsChecksum = await inputChecksum(input);
@@ -5491,7 +7185,7 @@ async function convertJfrogXrayToHdf(input) {
5491
7185
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
5492
7186
  return buildHdfResults({
5493
7187
  generatorName: "jfrog-xray-to-hdf",
5494
- converterVersion: "1.0.0",
7188
+ converterVersion,
5495
7189
  toolName: "JFrog Xray",
5496
7190
  toolFormat: "JSON",
5497
7191
  baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
@@ -5600,7 +7294,7 @@ function targetNameFromReport(report) {
5600
7294
  * @param input - NeuVector scan JSON string
5601
7295
  * @returns HDF JSON string
5602
7296
  */
5603
- async function convertNeuvectorToHdf(input) {
7297
+ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
5604
7298
  if (!input || input.trim().length === 0) throw new Error("neuvector: empty input");
5605
7299
  validateInputSize(input, "neuvector");
5606
7300
  const scan = parseJSON(input);
@@ -5622,7 +7316,7 @@ async function convertNeuvectorToHdf(input) {
5622
7316
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
5623
7317
  return buildHdfResults({
5624
7318
  generatorName: "neuvector-to-hdf",
5625
- converterVersion: "1.0.0",
7319
+ converterVersion,
5626
7320
  toolName: "NeuVector",
5627
7321
  toolFormat: "JSON",
5628
7322
  baselines: [createMinimalBaseline("NeuVector Scan", requirements, {
@@ -5644,6 +7338,23 @@ async function convertNeuvectorToHdf(input) {
5644
7338
  //#region converters/fortify-to-hdf/typescript/converter.ts
5645
7339
  const NIST_REFERENCE_NAME = "Standards Mapping - NIST Special Publication 800-53 Revision 4";
5646
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
+ }
5647
7358
  function buildSnippetMap(snippets) {
5648
7359
  const map = /* @__PURE__ */ new Map();
5649
7360
  for (const s of snippets) if (s.id) map.set(s.id, s);
@@ -5693,8 +7404,8 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5693
7404
  if (nistTags.length === 0) nistTags = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1];
5694
7405
  const cciTags = nistToCci(nistTags);
5695
7406
  const tags = buildNistCciTags(nistTags, cciTags);
5696
- const title = stripHTML(desc.Abstract ?? "");
5697
- let explanationText = stripHTML(desc.Explanation ?? "");
7407
+ const title = stripFvdlMarkup(desc.Abstract ?? "");
7408
+ let explanationText = stripFvdlMarkup(desc.Explanation ?? "");
5698
7409
  if (!explanationText) explanationText = title;
5699
7410
  const descriptions = [{
5700
7411
  label: "default",
@@ -5702,7 +7413,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5702
7413
  }];
5703
7414
  if (desc.Recommendations) descriptions.push({
5704
7415
  label: "fix",
5705
- data: stripHTML(desc.Recommendations)
7416
+ data: stripFvdlMarkup(desc.Recommendations)
5706
7417
  });
5707
7418
  let impact = 0;
5708
7419
  if (vulns.length > 0) impact = parseFloat(vulns[0].ClassInfo?.DefaultSeverity ?? "0") / 5;
@@ -5737,7 +7448,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5737
7448
  * @param input - Fortify FVDL XML string
5738
7449
  * @returns HDF JSON string
5739
7450
  */
5740
- async function convertFortifyToHdf(input) {
7451
+ async function convertFortifyToHdf(input, converterVersion = "1.0.0") {
5741
7452
  if (!input || input.trim().length === 0) throw new Error("fortify: empty input");
5742
7453
  validateInputSize(input, "fortify");
5743
7454
  const resultsChecksum = await inputChecksum(input);
@@ -5771,7 +7482,7 @@ async function convertFortifyToHdf(input) {
5771
7482
  }],
5772
7483
  generator: {
5773
7484
  name: "fortify-to-hdf",
5774
- version: "1.0.0"
7485
+ version: converterVersion
5775
7486
  },
5776
7487
  tool: {
5777
7488
  name: "Fortify",
@@ -5802,16 +7513,12 @@ const REQUIRED_COLUMNS = [
5802
7513
  * Prisma uses non-standard severity names: "important" and "moderate"
5803
7514
  * in addition to the standard critical/high/medium/low.
5804
7515
  */
7516
+ const PRISMA_SEVERITY_ALIASES = {
7517
+ important: .9,
7518
+ moderate: .5
7519
+ };
5805
7520
  function getImpact$3(severity) {
5806
- switch (severity.toLowerCase()) {
5807
- case "critical": return 1;
5808
- case "important": return .9;
5809
- case "high": return .7;
5810
- case "moderate":
5811
- case "medium": return .5;
5812
- case "low": return .3;
5813
- default: return .5;
5814
- }
7521
+ return PRISMA_SEVERITY_ALIASES[severity.toLowerCase()] ?? severityToImpact(severity);
5815
7522
  }
5816
7523
  /**
5817
7524
  * Returns NIST tags based on whether the finding has a CVE.
@@ -5956,7 +7663,7 @@ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
5956
7663
  * @param input - Prisma Cloud CSV string
5957
7664
  * @returns HDF JSON string
5958
7665
  */
5959
- async function convertPrismaToHdf(input) {
7666
+ async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
5960
7667
  if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
5961
7668
  validateInputSize(input, "prisma");
5962
7669
  const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
@@ -5982,7 +7689,7 @@ async function convertPrismaToHdf(input) {
5982
7689
  }
5983
7690
  return buildHdfResults({
5984
7691
  generatorName: "prisma-to-hdf",
5985
- converterVersion: "1.0.0",
7692
+ converterVersion,
5986
7693
  toolName: "Prisma Cloud",
5987
7694
  toolFormat: "CSV",
5988
7695
  baselines,
@@ -6121,7 +7828,7 @@ function buildRequirement$3(vuln, initiated) {
6121
7828
  * @param input - Netsparker/Invicti XML string
6122
7829
  * @returns HDF JSON string
6123
7830
  */
6124
- async function convertNetsparkerToHdf(input) {
7831
+ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
6125
7832
  if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
6126
7833
  validateInputSize(input, "netsparker");
6127
7834
  const resultsChecksum = await inputChecksum(input);
@@ -6144,7 +7851,7 @@ async function convertNetsparkerToHdf(input) {
6144
7851
  }
6145
7852
  return buildHdfResults({
6146
7853
  generatorName: "netsparker-to-hdf",
6147
- converterVersion: "1.0.0",
7854
+ converterVersion,
6148
7855
  toolName,
6149
7856
  toolFormat: "XML",
6150
7857
  baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
@@ -6261,7 +7968,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
6261
7968
  * @param input - ScoutSuite JS/JSON string
6262
7969
  * @returns HDF JSON string
6263
7970
  */
6264
- async function convertScoutsuiteToHdf(input) {
7971
+ async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
6265
7972
  if (!input || input.trim().length === 0) throw new Error("scoutsuite: empty input");
6266
7973
  validateInputSize(input, "scoutsuite");
6267
7974
  const jsonStr = stripJSPrefix(input);
@@ -6278,7 +7985,7 @@ async function convertScoutsuiteToHdf(input) {
6278
7985
  });
6279
7986
  return buildHdfResults({
6280
7987
  generatorName: "scoutsuite-to-hdf",
6281
- converterVersion: "1.0.0",
7988
+ converterVersion,
6282
7989
  toolName: "ScoutSuite",
6283
7990
  toolFormat: "JSON",
6284
7991
  toolVersion: report.last_run.version,
@@ -6391,17 +8098,12 @@ function buildRequirementFromResult(result, filename) {
6391
8098
  const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6392
8099
  const score = result.result.score;
6393
8100
  const status = determineStatus(score);
6394
- let results;
6395
- if (result.result.sections.length > 0) results = result.result.sections.map((section) => {
6396
- return createResult(status, void 0, {
6397
- codeDesc: buildCodeDesc$1(section, scannerName),
6398
- startTime
6399
- });
6400
- });
6401
- else results = [createResult(status, void 0, {
6402
- codeDesc: `No sections reported by ${scannerName}`,
8101
+ const toResult = (codeDesc) => ({
8102
+ status,
8103
+ codeDesc,
6403
8104
  startTime
6404
- })];
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}`)];
6405
8107
  const req = createRequirement(result.sha256, filename, descriptions, scoreToImpact(score), results, { tags });
6406
8108
  req.verificationMethod = VerificationMethodEnum.Automated;
6407
8109
  const controlType = deriveControlTypeFromTags([...nist]);
@@ -6426,7 +8128,7 @@ function buildScannerBaseline(scannerName, results, shaMap, resultsChecksum) {
6426
8128
  * @param input - Conveyor JSON string
6427
8129
  * @returns HDF JSON string
6428
8130
  */
6429
- async function convertConveyorToHdf(input) {
8131
+ async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
6430
8132
  if (!input || input.trim().length === 0) throw new Error("conveyor: empty input");
6431
8133
  validateInputSize(input, "conveyor");
6432
8134
  const data = parseJSON(input);
@@ -6448,7 +8150,7 @@ async function convertConveyorToHdf(input) {
6448
8150
  }));
6449
8151
  return buildHdfResults({
6450
8152
  generatorName: "conveyor-to-hdf",
6451
- converterVersion: "1.0.0",
8153
+ converterVersion,
6452
8154
  toolName: "Conveyor",
6453
8155
  toolFormat: "JSON",
6454
8156
  baselines,
@@ -6487,18 +8189,29 @@ const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
6487
8189
  function veracodeSeverityToImpact(severity) {
6488
8190
  return IMPACT_MAPPING$2.get(severity) ?? .1;
6489
8191
  }
6490
- /** Get an attribute from a parsed XML node. */
8192
+ /** Get an attribute from a parsed XML node, entity-decoded. */
6491
8193
  function attr(node, name) {
6492
- return node[`${A}${name}`] ?? "";
6493
- }
6494
- /** 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
+ };
6495
8204
  function decodeXmlEntities(s) {
6496
- 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
+ });
6497
8210
  }
6498
8211
  /** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
6499
8212
  function parseVeracodeTimestamp(ts) {
6500
8213
  if (!ts) return void 0;
6501
- return parseTimestamp(decodeXmlEntities(ts).replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
8214
+ return parseTimestamp(ts.replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
6502
8215
  }
6503
8216
  /** Format description paragraphs into text. */
6504
8217
  function formatDesc(desc) {
@@ -6631,12 +8344,20 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
6631
8344
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
6632
8345
  const results = cwes.flatMap((c) => {
6633
8346
  const staticflaws = c.staticflaws;
6634
- return ensureArray(staticflaws?.flaw).map((flaw) => createResult(ResultStatus.Failed, void 0, {
8347
+ return ensureArray(staticflaws?.flaw).map((flaw) => ({
8348
+ status: ResultStatus.Failed,
6635
8349
  codeDesc: formatFlawCodeDesc(flaw),
6636
8350
  startTime
6637
8351
  }));
6638
8352
  });
6639
- 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
+ });
6640
8361
  const controlType = deriveControlTypeFromTags(nist);
6641
8362
  if (controlType !== void 0) req.controlType = controlType;
6642
8363
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6686,16 +8407,26 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
6686
8407
  if (cweID) extras.cwe = cweID;
6687
8408
  const tags = buildNistCciTags(nist, cciTags, extras);
6688
8409
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
6689
- const results = components.map((comp) => createResult(ResultStatus.Failed, void 0, {
8410
+ const results = components.map((comp) => ({
8411
+ status: ResultStatus.Failed,
6690
8412
  codeDesc: formatSCACodeDesc(comp),
6691
8413
  startTime
6692
8414
  }));
6693
8415
  const cveSummary = attr(vuln, "cve_summary");
6694
8416
  const cveId = attr(vuln, "cve_id");
6695
- const req = createRequirement(cveId, cveId, [{
8417
+ const descriptions = [{
6696
8418
  label: "default",
6697
8419
  data: cveSummary || ""
6698
- }], 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
+ });
6699
8430
  const controlType = deriveControlTypeFromTags(nist);
6700
8431
  if (controlType !== void 0) req.controlType = controlType;
6701
8432
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6707,10 +8438,13 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
6707
8438
  * @param input - Veracode DetailedReport XML string
6708
8439
  * @returns HDF JSON string
6709
8440
  */
6710
- async function convertVeracodeToHdf(input) {
8441
+ async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
6711
8442
  if (!input || input.trim().length === 0) throw new Error("veracode: empty input");
6712
8443
  validateInputSize(input, "veracode");
6713
- const parsed = parseXml(input, { attributeNamePrefix: "@_" });
8444
+ const parsed = parseXml(input, {
8445
+ attributeNamePrefix: "@_",
8446
+ trimValues: false
8447
+ });
6714
8448
  if ("summaryreport" in parsed) throw new Error("veracode: summary reports are not supported; use a detailed report");
6715
8449
  const report = parsed.detailedreport;
6716
8450
  if (!report) throw new Error("veracode: invalid XML - no <detailedreport> root element");
@@ -6739,7 +8473,7 @@ async function convertVeracodeToHdf(input) {
6739
8473
  const timestamp = parseVeracodeTimestamp(firstBuildDate);
6740
8474
  return buildHdfResults({
6741
8475
  generatorName: "veracode-to-hdf",
6742
- converterVersion: "1.0.0",
8476
+ converterVersion,
6743
8477
  toolName: "Veracode",
6744
8478
  toolFormat: "XML",
6745
8479
  baselines: [baseline],
@@ -6815,12 +8549,11 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
6815
8549
  data: stripHTML(impacts[0])
6816
8550
  });
6817
8551
  }
6818
- const codeDesc = cs.implementationStatus || "No implementation status provided";
6819
- const startTime = (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6820
- const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
6821
- codeDesc,
6822
- ...startTime ? { startTime } : {}
6823
- })], { 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 });
6824
8557
  const controlType = deriveControlTypeFromTags(nist);
6825
8558
  if (controlType !== void 0) req.controlType = controlType;
6826
8559
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6835,7 +8568,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
6835
8568
  * @param input - Combined JSON string with secureScore and profiles
6836
8569
  * @returns HDF JSON string
6837
8570
  */
6838
- async function convertMsftSecureScoreToHdf(input) {
8571
+ async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
6839
8572
  if (!input || input.trim().length === 0) throw new Error("msft-secure-score: empty input");
6840
8573
  validateInputSize(input, "msft-secure-score");
6841
8574
  const combined = parseJSON(input);
@@ -6848,7 +8581,7 @@ async function convertMsftSecureScoreToHdf(input) {
6848
8581
  let tenantId = "";
6849
8582
  return buildHdfResults({
6850
8583
  generatorName: "msft-secure-score-to-hdf",
6851
- converterVersion: "1.0.0",
8584
+ converterVersion,
6852
8585
  toolName: "Microsoft Secure Score",
6853
8586
  toolFormat: "JSON",
6854
8587
  baselines: combined.secureScore.value.map((ss) => {
@@ -6856,14 +8589,17 @@ async function convertMsftSecureScoreToHdf(input) {
6856
8589
  const { items: limitedControlScores, truncated } = limitArray(ss.controlScores);
6857
8590
  /* v8 ignore next -- truncation only triggers with >100K items */
6858
8591
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedControlScores.length} controlScore items (original: ${ss.controlScores.length})`);
6859
- 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, {
6860
8595
  resultsChecksum,
6861
- title: `Azure Secure Score report - Tenant ID: ${ss.azureTenantId} - Run ID: ${ss.id}`
8596
+ title
6862
8597
  });
6863
8598
  }),
6864
8599
  components: [{
6865
8600
  name: `Azure Tenant: ${tenantId}`,
6866
8601
  type: TargetType.CloudAccount,
8602
+ provider: "azure",
6867
8603
  labels: {
6868
8604
  account: tenantId,
6869
8605
  provider: "azure"
@@ -6879,12 +8615,12 @@ async function convertMsftSecureScoreToHdf(input) {
6879
8615
  * Delegates base conversion to the generic SARIF converter and enriches
6880
8616
  * the output with MSDO-specific metadata.
6881
8617
  */
6882
- async function convertMsftDefenderDevopsToHdf(input) {
8618
+ async function convertMsftDefenderDevopsToHdf(input, converterVersion = "1.0.0") {
6883
8619
  validateInputSize(input, "msft-defender-devops");
6884
8620
  const raw = parseJSON(input);
6885
8621
  if (!raw || !Array.isArray(raw.runs)) throw new Error("Invalid MSDO SARIF structure: missing or invalid runs field");
6886
8622
  const { components, runEnrichments } = extractEnrichments(raw);
6887
- const hdfJson = await convertSarifToHdf(input);
8623
+ const hdfJson = await convertSarifToHdf(input, converterVersion);
6888
8624
  const result = JSON.parse(hdfJson);
6889
8625
  synthesizeNoFindingsPlaceholders(result);
6890
8626
  applyEnrichments(result, components, runEnrichments);
@@ -6902,8 +8638,7 @@ function extractEnrichments(raw) {
6902
8638
  const target = {
6903
8639
  name: repoNameFromURI(vcp.repositoryUri),
6904
8640
  type: TargetType.Repository,
6905
- url: vcp.repositoryUri,
6906
- labels: {}
8641
+ url: vcp.repositoryUri
6907
8642
  };
6908
8643
  if (vcp.branch) target.branch = vcp.branch;
6909
8644
  if (vcp.revisionId) target.commit = vcp.revisionId;
@@ -7030,10 +8765,14 @@ function buildRequirement(assessmentID, assessments, scanTime) {
7030
8765
  function buildResultFromAssessment(a, scanTime) {
7031
8766
  const status = mapStatus(a.properties.status.code);
7032
8767
  const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
7033
- 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,
7034
8771
  codeDesc,
7035
8772
  startTime: scanTime
7036
- });
8773
+ };
8774
+ if (message !== void 0) result.message = message;
8775
+ return result;
7037
8776
  }
7038
8777
  /**
7039
8778
  * Converts Microsoft Defender for Cloud assessment output to HDF format.
@@ -7041,7 +8780,7 @@ function buildResultFromAssessment(a, scanTime) {
7041
8780
  * @param input - JSON string containing Azure Security Assessments API response
7042
8781
  * @returns HDF JSON string
7043
8782
  */
7044
- async function convertMsftDefenderCloudToHdf(input) {
8783
+ async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0") {
7045
8784
  validateInputSize(input, "msft-defender-cloud");
7046
8785
  const scanTime = /* @__PURE__ */ new Date();
7047
8786
  const resultsChecksum = await inputChecksum(input);
@@ -7078,7 +8817,7 @@ async function convertMsftDefenderCloudToHdf(input) {
7078
8817
  });
7079
8818
  return buildHdfResults({
7080
8819
  generatorName: "msft-defender-cloud-to-hdf",
7081
- converterVersion: "1.0.0",
8820
+ converterVersion,
7082
8821
  toolName: "Microsoft Defender for Cloud",
7083
8822
  toolFormat: "JSON",
7084
8823
  baselines: [baseline],
@@ -7100,7 +8839,7 @@ const IMPACT_MAPPING = {
7100
8839
  /**
7101
8840
  * Maps MDE severity to HDF impact value.
7102
8841
  */
7103
- function severityToImpact$1(severity) {
8842
+ function severityToImpact$2(severity) {
7104
8843
  return IMPACT_MAPPING[severity.toLowerCase()] ?? .5;
7105
8844
  }
7106
8845
  /**
@@ -7193,7 +8932,7 @@ function resolveStartTime(alert, scanTime) {
7193
8932
  * Converts a single MDE alert to an HDF EvaluatedRequirement.
7194
8933
  */
7195
8934
  function alertToRequirement(alert, scanTime) {
7196
- const impact = severityToImpact$1(alert.severity);
8935
+ const impact = severityToImpact$2(alert.severity);
7197
8936
  const status = statusToResultStatus(alert.status, alert.classification);
7198
8937
  const codeDesc = formatEvidence(alert.evidence ?? []);
7199
8938
  const results = [createResult(status, formatMessage(alert), {
@@ -7223,7 +8962,7 @@ function alertToRequirement(alert, scanTime) {
7223
8962
  * @param input - JSON string of MDE alert response
7224
8963
  * @returns HDF JSON string
7225
8964
  */
7226
- async function convertMsftDefenderEndpointToHdf(input) {
8965
+ async function convertMsftDefenderEndpointToHdf(input, converterVersion = "1.0.0") {
7227
8966
  validateInputSize(input, "msft-defender-endpoint");
7228
8967
  const resultsChecksum = await inputChecksum(input);
7229
8968
  const response = parseJSON(input);
@@ -7246,7 +8985,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
7246
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));
7247
8986
  return buildHdfResults({
7248
8987
  generatorName: "msft-defender-endpoint-to-hdf",
7249
- converterVersion: "1.0.0",
8988
+ converterVersion,
7250
8989
  toolName: "Microsoft Defender for Endpoint",
7251
8990
  baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
7252
8991
  components,
@@ -7264,7 +9003,8 @@ const BUILD_OPTIONS = {
7264
9003
  ignoreAttributes: false,
7265
9004
  format: true,
7266
9005
  indentBy: " ",
7267
- suppressEmptyNode: false
9006
+ suppressEmptyNode: false,
9007
+ suppressBooleanAttributes: false
7268
9008
  };
7269
9009
  /**
7270
9010
  * Convert HDF Results JSON to XCCDF 1.2 Benchmark XML.
@@ -7274,7 +9014,7 @@ const BUILD_OPTIONS = {
7274
9014
  */
7275
9015
  function convertHdfToXccdf(input) {
7276
9016
  validateInputSize(input, "hdf-to-xccdf");
7277
- const hdfData = parseJSON(input);
9017
+ const hdfData = parseHdf(input);
7278
9018
  if (!hdfData || typeof hdfData !== "object" || !("baselines" in hdfData)) throw new Error("Invalid HDF structure: missing baselines field");
7279
9019
  if (!Array.isArray(hdfData.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
7280
9020
  return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ Benchmark: buildBenchmarkObj(hdfData) }, BUILD_OPTIONS)}`;
@@ -7310,19 +9050,19 @@ function sanitizeXccdfId(id) {
7310
9050
  }
7311
9051
  /** Build the Benchmark XML object from HDF data. */
7312
9052
  function buildBenchmarkObj(hdfData) {
9053
+ const empty = !hdfData.baselines || hdfData.baselines.length === 0;
7313
9054
  const benchmark = {
7314
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),
7315
9057
  [`${ATTR}resolved`]: "1",
7316
9058
  status: wrap("incomplete")
7317
9059
  };
7318
- if (!hdfData.baselines || hdfData.baselines.length === 0) {
7319
- benchmark[`${ATTR}id`] = "xccdf_hdf_benchmark_exported";
9060
+ if (empty) {
7320
9061
  benchmark.title = wrap("HDF Export");
7321
9062
  benchmark.version = wrap("1.0");
7322
9063
  return benchmark;
7323
9064
  }
7324
9065
  const baseline = hdfData.baselines[0];
7325
- benchmark[`${ATTR}id`] = sanitizeXccdfId("xccdf_hdf_benchmark_" + baseline.name);
7326
9066
  benchmark.title = wrap(baseline.title ?? baseline.name);
7327
9067
  benchmark.version = wrap(baseline.version ?? "1.0");
7328
9068
  benchmark.Profile = {
@@ -7344,20 +9084,40 @@ function buildRuleObj(req) {
7344
9084
  };
7345
9085
  const description = findDescription(req.descriptions, "default");
7346
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);
7347
9098
  const fixtext = findDescription(req.descriptions, "fix");
7348
9099
  if (fixtext) rule.fixtext = wrap(fixtext);
7349
- if (req.tags && Array.isArray(req.tags["cci"])) {
7350
- const ccis = req.tags["cci"];
7351
- if (ccis.length > 0) rule.ident = ccis.map((cci) => ({
7352
- [`${ATTR}system`]: "http://cyber.mil/cci",
7353
- "#text": cci
7354
- }));
7355
- }
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 = [];
7356
9111
  const checkContent = findDescription(req.descriptions, "check");
7357
- if (checkContent) rule.check = {
9112
+ if (checkContent) checks.push({
7358
9113
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
7359
9114
  "check-content": wrap(checkContent)
7360
- };
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;
7361
9121
  return rule;
7362
9122
  }
7363
9123
  /** Build the XCCDF TestResult object. */
@@ -7577,7 +9337,7 @@ async function convertHdfToOscalSar(input) {
7577
9337
  if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-sar: empty input");
7578
9338
  let hdfResults;
7579
9339
  try {
7580
- hdfResults = parseJSON(input);
9340
+ hdfResults = parseHdf(input);
7581
9341
  } catch {
7582
9342
  throw new Error("hdf-to-oscal-sar: failed to parse HDF JSON");
7583
9343
  }
@@ -7589,8 +9349,9 @@ async function convertHdfToOscalSar(input) {
7589
9349
  * Constructs the full OSCAL assessment-results document from HDF results.
7590
9350
  */
7591
9351
  function buildOSCALDocument(hdfResults) {
7592
- let timestamp = (/* @__PURE__ */ new Date()).toISOString();
7593
- 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);
7594
9355
  const metadata = {
7595
9356
  title: "HDF Assessment Results Export",
7596
9357
  "last-modified": timestamp,
@@ -7609,6 +9370,34 @@ function buildOSCALDocument(hdfResults) {
7609
9370
  results
7610
9371
  } };
7611
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
+ }
7612
9401
  /**
7613
9402
  * Converts a single EvaluatedBaseline to an OSCAL Result.
7614
9403
  */
@@ -7630,7 +9419,7 @@ function baselineToResult(baseline, timestamp) {
7630
9419
  uuid: crypto.randomUUID(),
7631
9420
  title,
7632
9421
  description,
7633
- start: timestamp,
9422
+ start: assessmentStart(baseline, timestamp),
7634
9423
  findings,
7635
9424
  observations,
7636
9425
  risks
@@ -7644,14 +9433,59 @@ function requirementToFindingSet(req, timestamp) {
7644
9433
  const { state, reason } = aggregateStatus(req.results);
7645
9434
  const findingDesc = extractDefaultDescription(req.descriptions);
7646
9435
  const props = [];
7647
- if (req.tags) {
7648
- const nistRaw = req.tags["nist"];
7649
- if (Array.isArray(nistRaw)) {
7650
- for (const v of nistRaw) if (typeof v === "string") props.push({
7651
- 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,
7652
9441
  value: v
7653
9442
  });
7654
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
+ });
7655
9489
  }
7656
9490
  let title = req.id;
7657
9491
  if (req.title && req.title !== "") title = req.title;
@@ -7667,6 +9501,7 @@ function requirementToFindingSet(req, timestamp) {
7667
9501
  title,
7668
9502
  description: findingDesc || "",
7669
9503
  props: props.length > 0 ? props : void 0,
9504
+ links: links.length > 0 ? links : void 0,
7670
9505
  target
7671
9506
  };
7672
9507
  let observation;
@@ -7676,7 +9511,7 @@ function requirementToFindingSet(req, timestamp) {
7676
9511
  uuid: obsUUID,
7677
9512
  description: buildObservationDescription(req.results),
7678
9513
  methods: ["TEST"],
7679
- collected: timestamp
9514
+ collected: formatAssessmentTime(earliestResultTime(req.results), timestamp)
7680
9515
  };
7681
9516
  finding["related-observations"] = [{ "observation-uuid": obsUUID }];
7682
9517
  }
@@ -7806,18 +9641,24 @@ async function convertHdfToOscalPoam(input) {
7806
9641
  if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-poam: empty input");
7807
9642
  let amendments;
7808
9643
  try {
7809
- amendments = parseJSON(input);
9644
+ amendments = parseHdf(input);
7810
9645
  } catch {
7811
9646
  throw new Error("hdf-to-oscal-poam: failed to parse JSON");
7812
9647
  }
7813
9648
  const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendments) };
7814
9649
  return JSON.stringify(doc, null, 2);
7815
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
+ }
7816
9657
  /**
7817
9658
  * Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
7818
9659
  */
7819
9660
  function amendmentsToPOAM(amendments) {
7820
- const now = (/* @__PURE__ */ new Date()).toISOString();
9661
+ const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
7821
9662
  const metadata = {
7822
9663
  title: amendments.name,
7823
9664
  "last-modified": now,
@@ -7868,25 +9709,45 @@ function overrideToPOAMItem(override) {
7868
9709
  name: "impacted-control-id",
7869
9710
  value: nistTagToControlId(override.requirementId)
7870
9711
  }];
7871
- const remediations = [];
7872
- if (override.milestones) for (const ms of override.milestones) remediations.push({
7873
- uuid: crypto.randomUUID(),
7874
- lifecycle: "planned",
7875
- title: ms.description,
7876
- description: ms.description
9712
+ if (override.type) riskProps.push({
9713
+ name: "override-type",
9714
+ value: String(override.type)
7877
9715
  });
7878
- let riskLog;
7879
- const expiresAt = override.expiresAt;
7880
- if (expiresAt) {
7881
- const expiresDate = typeof expiresAt === "string" ? new Date(expiresAt) : expiresAt;
7882
- 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({
7883
9735
  uuid: crypto.randomUUID(),
7884
- title: "Scheduled review",
7885
- description: "Amendment expiration date",
7886
- start: expiresDate.toISOString(),
7887
- "status-change": riskStatus
7888
- }] };
9736
+ lifecycle: "planned",
9737
+ title: ms.description,
9738
+ description: ms.description,
9739
+ props: msProps.length > 0 ? msProps : void 0
9740
+ });
7889
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
+ }] };
7890
9751
  const risk = {
7891
9752
  uuid: riskUUID,
7892
9753
  title: override.requirementId,
@@ -7958,7 +9819,7 @@ function buildTags(dep) {
7958
9819
  if (dep.parentDependencies.length > 0) extras.parentDependencies = dep.parentDependencies;
7959
9820
  return buildNistCciTags(nist, cciTags, extras);
7960
9821
  }
7961
- async function convertIonchannelToHdf(input) {
9822
+ async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
7962
9823
  if (!input?.trim()) throw new Error("Empty input");
7963
9824
  validateInputSize(input, "ionchannel");
7964
9825
  const analysis = parseJSON(input);
@@ -7985,10 +9846,11 @@ async function convertIonchannelToHdf(input) {
7985
9846
  outdated_version: dep.outdated_version,
7986
9847
  dependencies: dep.dependencies
7987
9848
  }, null, 2);
7988
- 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,
7989
9851
  codeDesc: "Dependency inventory item",
7990
9852
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
7991
- })], { tags });
9853
+ }], { tags });
7992
9854
  req.code = code;
7993
9855
  const controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
7994
9856
  if (controlType !== void 0) req.controlType = controlType;
@@ -7996,20 +9858,22 @@ async function convertIonchannelToHdf(input) {
7996
9858
  return req;
7997
9859
  });
7998
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";
7999
9871
  return buildHdfResults({
8000
9872
  generatorName: "ionchannel-to-hdf",
8001
- converterVersion: "1.0.0",
9873
+ converterVersion,
8002
9874
  toolName: "Ion Channel",
8003
9875
  toolFormat: "JSON",
8004
- baselines: [createMinimalBaseline("Ion Channel SBOM Analysis", requirements, {
8005
- title: `Ion Channel Analysis of ${analysis.source}`,
8006
- summary: analysis.summary,
8007
- integrity: {
8008
- algorithm: resultsChecksum.algorithm,
8009
- checksum: resultsChecksum.value
8010
- },
8011
- status: "loaded"
8012
- })],
9876
+ baselines: [baseline],
8013
9877
  timestamp: /* @__PURE__ */ new Date()
8014
9878
  });
8015
9879
  }
@@ -8222,6 +10086,40 @@ function affectedPackageToIdentifier(pkg) {
8222
10086
  if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
8223
10087
  if (pkg.name) return pkg.name;
8224
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
+ }
8225
10123
  /**
8226
10124
  * Build an HDF Evidence entry pointing at the upstream VEX document. Used by
8227
10125
  * importers to preserve provenance even though we lose the structured
@@ -8716,12 +10614,27 @@ function publisherIdentityOrDefault(bom) {
8716
10614
  * fields the shared VEX mapping considers consumer-action-bearing survive
8717
10615
  * round-trip; the rest collapse into the available CSAF fields.
8718
10616
  */
10617
+ const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
8719
10618
  const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
8720
10619
  const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
8721
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
+ }
8722
10635
  function convertHdfToCsafVex(input, converterVersion) {
8723
10636
  validateInputSize(input, "hdf-to-csaf-vex");
8724
- const amendments = parseJSON(input);
10637
+ const amendments = parseHdf(input);
8725
10638
  const groups = groupByCVE(amendments.overrides ?? []);
8726
10639
  const productSet = /* @__PURE__ */ new Map();
8727
10640
  const vulnerabilities = [];
@@ -8730,6 +10643,7 @@ function convertHdfToCsafVex(input, converterVersion) {
8730
10643
  if (!v) continue;
8731
10644
  vulnerabilities.push(v);
8732
10645
  for (const p of productIDsForGroup(group)) productSet.set(p, true);
10646
+ for (const p of fixedProductIDsForGroup(group)) productSet.set(p, true);
8733
10647
  }
8734
10648
  if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
8735
10649
  const products = [...productSet.keys()].sort();
@@ -8765,6 +10679,15 @@ function productIDsForGroup(group) {
8765
10679
  for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
8766
10680
  return [...seen].sort();
8767
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
+ }
8768
10691
  function productIDsFor$1(o) {
8769
10692
  if (o.affectedPackages && o.affectedPackages.length > 0) {
8770
10693
  const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
@@ -8791,6 +10714,25 @@ function buildVulnerability(group) {
8791
10714
  let emitted = false;
8792
10715
  for (const o of group.overrides) {
8793
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
+ }
8794
10736
  let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
8795
10737
  if (!canonical) continue;
8796
10738
  if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
@@ -8800,8 +10742,8 @@ function buildVulnerability(group) {
8800
10742
  v.flags = v.flags ?? [];
8801
10743
  v.flags.push({
8802
10744
  label: String(o.justification),
8803
- product_ids: pids,
8804
- date: formatTimestampSeconds(new Date(o.appliedAt))
10745
+ date: formatTimestampSeconds(hdfTime(o.appliedAt) ?? GO_ZERO_TIME$1),
10746
+ product_ids: pids
8805
10747
  });
8806
10748
  }
8807
10749
  emitted = true;
@@ -8851,9 +10793,9 @@ function buildVulnerability(group) {
8851
10793
  /* c8 ignore next */
8852
10794
  if (!emitted) return void 0;
8853
10795
  if (status.fixed) status.fixed = [...new Set(status.fixed)];
10796
+ if (status.first_fixed) status.first_fixed = [...new Set(status.first_fixed)];
8854
10797
  if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
8855
10798
  if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
8856
- v.product_status = status;
8857
10799
  if (v.references) {
8858
10800
  const seen = /* @__PURE__ */ new Set();
8859
10801
  v.references = v.references.filter((r) => {
@@ -8862,10 +10804,30 @@ function buildVulnerability(group) {
8862
10804
  return true;
8863
10805
  });
8864
10806
  }
8865
- 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();
8866
10828
  }
8867
10829
  function buildDocument(amendments, converterVersion) {
8868
- const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
10830
+ const now = formatTimestampSeconds(earliestAppliedAt(amendments));
8869
10831
  const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
8870
10832
  const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
8871
10833
  const docVersion = amendments.version || "1";
@@ -8881,7 +10843,7 @@ function buildDocument(amendments, converterVersion) {
8881
10843
  category: "csaf_vex",
8882
10844
  csaf_version: "2.0",
8883
10845
  title,
8884
- notes,
10846
+ ...notes.length > 0 && { notes },
8885
10847
  publisher: {
8886
10848
  category: "other",
8887
10849
  name: publisherName
@@ -8919,6 +10881,7 @@ function buildDocument(amendments, converterVersion) {
8919
10881
  * partial-fidelity by design — consumer-action-bearing fields (CVE id,
8920
10882
  * status, justification) survive round-trip; the rest collapse.
8921
10883
  */
10884
+ const GO_ZERO_TIME = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
8922
10885
  const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
8923
10886
  const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
8924
10887
  const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
@@ -8926,14 +10889,15 @@ const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
8926
10889
  const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
8927
10890
  async function convertHdfToOpenVex(input, converterVersion) {
8928
10891
  validateInputSize(input, "hdf-to-openvex");
8929
- const amendments = parseJSON(input);
10892
+ const amendments = parseHdf(input);
8930
10893
  const statements = [];
8931
10894
  let earliest;
8932
10895
  for (const o of amendments.overrides ?? []) {
8933
10896
  const s = overrideToStatement(o);
8934
10897
  if (!s) continue;
8935
10898
  statements.push(s);
8936
- const t = new Date(o.appliedAt);
10899
+ const t = hdfTime(o.appliedAt);
10900
+ if (!t) continue;
8937
10901
  if (!earliest || t < earliest) earliest = t;
8938
10902
  }
8939
10903
  if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
@@ -8956,22 +10920,24 @@ function overrideToStatement(o) {
8956
10920
  let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
8957
10921
  if (!canonical) return void 0;
8958
10922
  if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
8959
- 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 {
8960
10930
  vulnerability: {
8961
- name: o.requirementId,
8962
- "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
10931
+ "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`,
10932
+ name: o.requirementId
8963
10933
  },
10934
+ products: productsFor(o),
8964
10935
  status: String(canonical),
8965
- timestamp: formatTimestampSeconds(new Date(o.appliedAt)),
8966
- 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)
8967
10940
  };
8968
- if (canonical === VexStatus.NotAffected) {
8969
- if (o.justification) stmt.justification = String(o.justification);
8970
- const impact = stripProductsLine(o.reason ?? "");
8971
- if (impact) stmt.impact_statement = impact;
8972
- } else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
8973
- else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
8974
- return stmt;
8975
10941
  }
8976
10942
  function productsFor(o) {
8977
10943
  if (o.affectedPackages && o.affectedPackages.length > 0) {
@@ -9000,7 +10966,8 @@ function stripProductsLine(reason) {
9000
10966
  }
9001
10967
  async function buildDocumentID(input, a) {
9002
10968
  if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
9003
- return `${OPENVEX_NAMESPACE}vex-${await sha256(input)}`;
10969
+ const digest = await sha256(input);
10970
+ return `${OPENVEX_NAMESPACE}vex-${digest}`;
9004
10971
  }
9005
10972
  //#endregion
9006
10973
  //#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
@@ -9017,9 +10984,41 @@ const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
9017
10984
  const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
9018
10985
  const RESPONSE_LINE = /^Response:.*$/gm;
9019
10986
  const DEFAULT_PRODUCT_ID = "HDFPID-0001";
9020
- 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) {
9021
11020
  validateInputSize(input, "hdf-to-cyclonedx-vex");
9022
- const amendments = parseJSON(input);
11021
+ const amendments = parseHdf(input);
9023
11022
  const componentRegistry = /* @__PURE__ */ new Map();
9024
11023
  const vulnerabilities = [];
9025
11024
  let earliest;
@@ -9028,7 +11027,8 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
9028
11027
  const v = overrideToVulnerability(o, componentRegistry);
9029
11028
  if (!v) continue;
9030
11029
  vulnerabilities.push(v);
9031
- const t = new Date(o.appliedAt);
11030
+ const t = hdfTime(o.appliedAt);
11031
+ if (!t) continue;
9032
11032
  if (!earliest || t < earliest) earliest = t;
9033
11033
  }
9034
11034
  if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
@@ -9037,7 +11037,7 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
9037
11037
  const bom = {
9038
11038
  bomFormat: "CycloneDX",
9039
11039
  specVersion: "1.4",
9040
- serialNumber: buildSerialNumberSync(input, amendments),
11040
+ serialNumber: await buildSerialNumber(input, amendments),
9041
11041
  version: 1,
9042
11042
  metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
9043
11043
  components,
@@ -9061,23 +11061,14 @@ function overrideToVulnerability(o, componentRegistry) {
9061
11061
  const cdxJust = justificationForCycloneDX(o.justification);
9062
11062
  if (cdxJust) analysis.justification = cdxJust;
9063
11063
  }
9064
- const detail = stripReasonAnnotations(o.reason ?? "");
9065
- if (detail) analysis.detail = detail;
9066
11064
  if (canonical === VexStatus.Fixed) analysis.response = ["update"];
9067
11065
  else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
9068
- const v = {
9069
- id: o.requirementId,
9070
- source: {
9071
- name: "NVD",
9072
- url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
9073
- },
9074
- analysis,
9075
- affects: pids.map((p) => ({ ref: p }))
9076
- };
11066
+ const detail = stripReasonAnnotations(o.reason ?? "");
11067
+ if (detail) analysis.detail = detail;
11068
+ const references = [];
9077
11069
  for (const e of o.evidence ?? []) {
9078
11070
  if (e.type !== "url" || !e.data) continue;
9079
- v.references = v.references ?? [];
9080
- v.references.push({
11071
+ references.push({
9081
11072
  id: o.requirementId,
9082
11073
  source: {
9083
11074
  name: e.description ?? "",
@@ -9085,7 +11076,26 @@ function overrideToVulnerability(o, componentRegistry) {
9085
11076
  }
9086
11077
  });
9087
11078
  }
9088
- 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
+ };
9089
11099
  }
9090
11100
  function canonicalToCycloneDXState(canonical) {
9091
11101
  switch (canonical) {
@@ -9144,18 +11154,9 @@ function buildMetadata(a, docTime, converterVersion) {
9144
11154
  }
9145
11155
  return metadata;
9146
11156
  }
9147
- function buildSerialNumberSync(input, a) {
11157
+ async function buildSerialNumber(input, a) {
9148
11158
  if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
9149
- return `urn:uuid:${fnv1a(input)}`;
9150
- }
9151
- function fnv1a(s) {
9152
- let h = 2166136261;
9153
- for (let i = 0; i < s.length; i++) {
9154
- h ^= s.charCodeAt(i);
9155
- h = Math.imul(h, 16777619) >>> 0;
9156
- }
9157
- const d = h.toString(16).padStart(8, "0");
9158
- return (d + d + d + d).slice(0, 32);
11159
+ return `urn:uuid:${(await sha256(input)).slice(0, 32)}`;
9159
11160
  }
9160
11161
  //#endregion
9161
11162
  //#region converters/oscal-to-hdf/typescript/detect.ts
@@ -10167,6 +12168,6 @@ function sarBaselineName(result, sar) {
10167
12168
  return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
10168
12169
  }
10169
12170
  //#endregion
10170
- 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 };
10171
12172
 
10172
12173
  //# sourceMappingURL=index.js.map