@mitre/hdf-converters 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { n as detectConverter, t as registerAllFingerprints } from "./register-all-C3lYICDC.js";
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, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML } from "@mitre/hdf-utilities";
3
+ import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML, trimUtcFraction } from "@mitre/hdf-utilities";
4
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";
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, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getCCINistMappings, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, nistToCci } from "@mitre/hdf-mappings";
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
  /**
8
8
  * Shared utilities for TypeScript HDF converters.
@@ -249,7 +249,21 @@ function buildHdfResults(opts) {
249
249
  if (opts.components) hdf.components = opts.components;
250
250
  if (opts.timestamp) hdf.timestamp = opts.timestamp;
251
251
  if (opts.statistics) hdf.statistics = opts.statistics;
252
- return JSON.stringify(hdf, null, 2);
252
+ return serializeHdf(hdf);
253
+ }
254
+ const ISO_UTC_WITH_FRACTION = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$/;
255
+ function hdfTimestampReplacer(_key, value) {
256
+ if (typeof value === "string" && ISO_UTC_WITH_FRACTION.test(value)) return trimUtcFraction(value);
257
+ return value;
258
+ }
259
+ /**
260
+ * Serialize an HDF document to pretty-printed JSON, normalizing Date-derived
261
+ * timestamps to canonical trimmed-UTC RFC3339 (byte-identical to the Go
262
+ * converters). Use this instead of a bare `JSON.stringify` for any HDF output
263
+ * so the fractional-second trim is applied consistently.
264
+ */
265
+ function serializeHdf(doc, space = 2) {
266
+ return JSON.stringify(doc, hdfTimestampReplacer, space);
253
267
  }
254
268
  /**
255
269
  * Captures a NIST 800-53 control identifier and its base sub-control number,
@@ -413,14 +427,13 @@ function buildNoFindingsRequirement(id, codeDesc, startTime) {
413
427
  * - Results: snake_case → camelCase for all fields
414
428
  * - Overlay flattening: merge overlay/wrapper baselines so every requirement has results
415
429
  */
416
- /** Valid severity values per HDF v2.0 schema. */
417
- const VALID_SEVERITIES = new Set([
430
+ /** Valid severity values per HDF v2.0 schema (matches the Go converter). */
431
+ const VALID_SEVERITIES = /* @__PURE__ */ new Set([
418
432
  "critical",
419
433
  "high",
420
434
  "medium",
421
435
  "low",
422
- "informational",
423
- "none"
436
+ "informational"
424
437
  ]);
425
438
  /**
426
439
  * Convert tags.severity string to a valid severity value.
@@ -432,17 +445,6 @@ function tagSeverityToSeverity(tagSeverity) {
432
445
  return VALID_SEVERITIES.has(normalized) ? normalized : null;
433
446
  }
434
447
  /**
435
- * Derive severity from numeric impact score.
436
- * Follows InSpec conventions: 0.9+ critical, 0.7+ high, 0.5+ medium, >0 low, 0 none.
437
- */
438
- function impactToSeverity$2(impact) {
439
- if (impact >= .9) return "critical";
440
- if (impact >= .7) return "high";
441
- if (impact >= .5) return "medium";
442
- if (impact > 0) return "low";
443
- return "none";
444
- }
445
- /**
446
448
  * Normalize status values from v1.0 to v2.0 format.
447
449
  * Converts snake_case to camelCase.
448
450
  */
@@ -491,32 +493,22 @@ function computeEffectiveStatus(impact, results) {
491
493
  * Convert v1.0 result to v2.0 result.
492
494
  * Transforms snake_case field names to camelCase.
493
495
  */
496
+ const LEGACY_ZERO_TIME = "0001-01-01T00:00:00Z";
497
+ function legacyStartTime(s) {
498
+ if (s === void 0) return LEGACY_ZERO_TIME;
499
+ const d = parseTimestamp(s);
500
+ return d ? formatTimestamp(d) : LEGACY_ZERO_TIME;
501
+ }
494
502
  function convertResult(v1Result) {
495
503
  const v2Result = { status: normalizeStatus$1(v1Result.status) };
496
- if (v1Result.code_desc !== void 0) v2Result.codeDesc = v1Result.code_desc;
504
+ v2Result.codeDesc = v1Result.code_desc ?? "";
497
505
  if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
498
- if (v1Result.start_time !== void 0) v2Result.startTime = v1Result.start_time;
506
+ v2Result.startTime = legacyStartTime(v1Result.start_time);
499
507
  if (v1Result.message !== void 0) v2Result.message = v1Result.message;
500
508
  if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
501
509
  if (v1Result.backtrace !== void 0) v2Result.backtrace = v1Result.backtrace;
502
- if (v1Result.resource_class !== void 0) v2Result.resourceClass = v1Result.resource_class;
503
- if (v1Result.resource_params !== void 0) v2Result.resourceParams = v1Result.resource_params;
510
+ if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
504
511
  if (v1Result.resource_id !== void 0) v2Result.resourceId = v1Result.resource_id;
505
- if (v1Result.skip_message !== void 0) v2Result.skipMessage = v1Result.skip_message;
506
- const knownFields = new Set([
507
- "status",
508
- "code_desc",
509
- "run_time",
510
- "start_time",
511
- "message",
512
- "exception",
513
- "backtrace",
514
- "resource_class",
515
- "resource_params",
516
- "resource_id",
517
- "skip_message"
518
- ]);
519
- for (const [key, value] of Object.entries(v1Result)) if (!knownFields.has(key) && !(key in v2Result)) v2Result[key] = value;
520
512
  return v2Result;
521
513
  }
522
514
  /**
@@ -529,37 +521,19 @@ function convertControl(v1Control) {
529
521
  impact: v1Control.impact
530
522
  };
531
523
  if (v1Control.title !== void 0) v2Req.title = v1Control.title;
532
- if (v1Control.desc !== void 0) v2Req.desc = v1Control.desc;
533
524
  if (v1Control.descriptions !== void 0) v2Req.descriptions = v1Control.descriptions;
534
- if (v1Control.refs !== void 0) v2Req.refs = v1Control.refs;
535
525
  if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
536
526
  if (v1Control.code !== void 0) v2Req.code = v1Control.code;
537
527
  if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
538
- if (v1Control.waiver_data !== void 0) v2Req.waiverData = v1Control.waiver_data;
539
528
  if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
540
529
  if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
541
530
  if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
542
- v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity$2(v1Control.impact);
531
+ v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity(v1Control.impact);
543
532
  const rawNist = v1Control.tags?.nist;
544
533
  if (Array.isArray(rawNist)) {
545
534
  const controlType = deriveControlTypeFromTags(rawNist.filter((v) => typeof v === "string"));
546
535
  if (controlType !== void 0) v2Req.controlType = controlType;
547
536
  }
548
- const knownFields = new Set([
549
- "id",
550
- "title",
551
- "desc",
552
- "descriptions",
553
- "impact",
554
- "refs",
555
- "tags",
556
- "code",
557
- "source_location",
558
- "waiver_data",
559
- "results",
560
- "status"
561
- ]);
562
- for (const [key, value] of Object.entries(v1Control)) if (!knownFields.has(key) && !(key in v2Req)) v2Req[key] = value;
563
537
  return v2Req;
564
538
  }
565
539
  /**
@@ -572,7 +546,7 @@ function convertGroup(v1Group) {
572
546
  requirements: v1Group.controls
573
547
  };
574
548
  if (v1Group.title !== void 0) v2Group.title = v1Group.title;
575
- const knownFields = new Set([
549
+ const knownFields = /* @__PURE__ */ new Set([
576
550
  "id",
577
551
  "title",
578
552
  "controls"
@@ -598,7 +572,7 @@ function convertDependency(v1Dep) {
598
572
  if (v1Dep.compliance !== void 0) v2Dep.compliance = v1Dep.compliance;
599
573
  if (v1Dep.status !== void 0) v2Dep.status = v1Dep.status;
600
574
  if (v1Dep.skip_message !== void 0) v2Dep.skipMessage = v1Dep.skip_message;
601
- const knownFields = new Set([
575
+ const knownFields = /* @__PURE__ */ new Set([
602
576
  "name",
603
577
  "url",
604
578
  "path",
@@ -627,9 +601,9 @@ function convertProfile(v1Profile) {
627
601
  if (v1Profile.summary !== void 0) v2Baseline.summary = v1Profile.summary;
628
602
  if (v1Profile.license !== void 0) v2Baseline.license = v1Profile.license;
629
603
  if (v1Profile.copyright !== void 0) v2Baseline.copyright = v1Profile.copyright;
630
- if (v1Profile.copyright_email !== void 0) v2Baseline.copyright_email = v1Profile.copyright_email;
631
- if (v1Profile.supports !== void 0) v2Baseline.supports = v1Profile.supports;
632
- if (v1Profile.attributes !== void 0) v2Baseline.inputs = v1Profile.attributes;
604
+ if (v1Profile.copyright_email !== void 0) v2Baseline.copyrightEmail = v1Profile.copyright_email;
605
+ if (v1Profile.supports?.length) v2Baseline.supports = v1Profile.supports;
606
+ if (v1Profile.attributes?.length) v2Baseline.inputs = v1Profile.attributes;
633
607
  if (v1Profile.status !== void 0) v2Baseline.status = v1Profile.status;
634
608
  if (v1Profile.sha256) v2Baseline.integrity = {
635
609
  algorithm: "sha256",
@@ -638,30 +612,9 @@ function convertProfile(v1Profile) {
638
612
  if (v1Profile.parent_profile !== void 0) v2Baseline.parentBaseline = v1Profile.parent_profile;
639
613
  if (v1Profile.status_message !== void 0) v2Baseline.statusMessage = v1Profile.status_message;
640
614
  if (v1Profile.skip_message !== void 0) v2Baseline.skipMessage = v1Profile.skip_message;
641
- if (v1Profile.groups && Array.isArray(v1Profile.groups)) v2Baseline.groups = v1Profile.groups.map(convertGroup);
615
+ if (v1Profile.groups?.length) v2Baseline.groups = v1Profile.groups.map(convertGroup);
642
616
  if (v1Profile.controls && Array.isArray(v1Profile.controls)) v2Baseline.requirements = v1Profile.controls.map(convertControl);
643
- if (v1Profile.depends && Array.isArray(v1Profile.depends)) v2Baseline.depends = v1Profile.depends.map(convertDependency);
644
- const knownFields = new Set([
645
- "name",
646
- "version",
647
- "title",
648
- "maintainer",
649
- "summary",
650
- "license",
651
- "copyright",
652
- "copyright_email",
653
- "supports",
654
- "attributes",
655
- "groups",
656
- "controls",
657
- "sha256",
658
- "depends",
659
- "parent_profile",
660
- "status",
661
- "status_message",
662
- "skip_message"
663
- ]);
664
- for (const [key, value] of Object.entries(v1Profile)) if (!knownFields.has(key) && !(key in v2Baseline)) v2Baseline[key] = value;
617
+ if (v1Profile.depends?.length) v2Baseline.depends = v1Profile.depends.map(convertDependency);
665
618
  return v2Baseline;
666
619
  }
667
620
  /**
@@ -694,17 +647,21 @@ function convertV1ToV2(v1Data) {
694
647
  baselines: (v1Data.profiles || []).map(convertProfile),
695
648
  statistics: v1Data.statistics || {}
696
649
  };
697
- if (v1Data.platform) v2.components = [{
698
- type: "host",
699
- id: v1Data.platform.target_id || v1Data.platform.name,
700
- name: v1Data.platform.name,
701
- ...v1Data.platform.release && { release: v1Data.platform.release },
702
- labels: {}
703
- }];
650
+ if (v1Data.platform) {
651
+ const component = {
652
+ type: "host",
653
+ name: v1Data.platform.name
654
+ };
655
+ if (v1Data.platform.target_id) {
656
+ component.osName = v1Data.platform.name;
657
+ if (v1Data.platform.release !== void 0) component.osVersion = v1Data.platform.release;
658
+ }
659
+ v2.components = [component];
660
+ }
704
661
  if (v1Data.generator) v2.generator = v1Data.generator;
705
662
  v2.tool = { name: "Heimdall Data Format v1" };
706
663
  if (v1Data.timestamp) v2.timestamp = v1Data.timestamp;
707
- const knownV1Fields = new Set([
664
+ const knownV1Fields = /* @__PURE__ */ new Set([
708
665
  "version",
709
666
  "platform",
710
667
  "profiles",
@@ -1069,8 +1026,9 @@ async function convertJunitToHdf(input) {
1069
1026
  if (!input || !input.trim()) throw new Error("Empty input");
1070
1027
  validateInputSize(input, "junit");
1071
1028
  const { suites, name } = parseJUnitXML(input);
1072
- const requirements = buildRequirements(suites);
1073
- if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, /* @__PURE__ */ new Date()));
1029
+ const scanTime = resolveScanTime(suites);
1030
+ const requirements = buildRequirements(suites, scanTime);
1031
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
1074
1032
  return buildHdfResults({
1075
1033
  generatorName: "junit-to-hdf",
1076
1034
  converterVersion: CONVERTER_VERSION$2,
@@ -1081,9 +1039,16 @@ async function convertJunitToHdf(input) {
1081
1039
  type: TargetType.Application,
1082
1040
  name
1083
1041
  }],
1084
- timestamp: /* @__PURE__ */ new Date()
1042
+ timestamp: scanTime
1085
1043
  });
1086
1044
  }
1045
+ function resolveScanTime(suites) {
1046
+ for (const suite of suites) if (suite.timestamp) {
1047
+ const parsed = parseTimestamp(suite.timestamp);
1048
+ if (parsed) return parsed;
1049
+ }
1050
+ return /* @__PURE__ */ new Date();
1051
+ }
1087
1052
  function parseJUnitXML(input) {
1088
1053
  const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
1089
1054
  if (parsed.testsuites) return {
@@ -1100,7 +1065,7 @@ function parseJUnitXML(input) {
1100
1065
  }
1101
1066
  throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
1102
1067
  }
1103
- function buildRequirements(suites) {
1068
+ function buildRequirements(suites, scanTime) {
1104
1069
  const { items: limitedSuites, truncated: truncatedSuites } = limitArray(suites);
1105
1070
  /* v8 ignore next -- truncation only triggers with >100K items */
1106
1071
  if (truncatedSuites) console.warn(`WARNING: Input truncated at ${limitedSuites.length} test suite items (original: ${suites.length})`);
@@ -1110,19 +1075,21 @@ function buildRequirements(suites) {
1110
1075
  const { items: limitedTestCases, truncated: truncatedTC } = limitArray(testcases);
1111
1076
  /* v8 ignore next -- truncation only triggers with >100K items */
1112
1077
  if (truncatedTC) console.warn(`WARNING: Input truncated at ${limitedTestCases.length} test case items (original: ${testcases.length})`);
1113
- for (const tc of limitedTestCases) requirements.push(testCaseToRequirement(tc, suite.timestamp));
1078
+ for (const tc of limitedTestCases) requirements.push(testCaseToRequirement(tc, scanTime));
1114
1079
  }
1115
1080
  return requirements;
1116
1081
  }
1117
- function testCaseToRequirement(tc, suiteTimestamp) {
1082
+ function testCaseToRequirement(tc, scanTime) {
1118
1083
  const id = buildID(tc);
1119
1084
  const { status, message } = resolveStatus(tc);
1120
- const resultOptions = { codeDesc: buildCodeDesc$9(tc) };
1085
+ const resultOptions = {
1086
+ codeDesc: buildCodeDesc$9(tc),
1087
+ startTime: scanTime
1088
+ };
1121
1089
  if (tc.time) {
1122
1090
  const parsed = parseFloat(tc.time);
1123
1091
  if (!isNaN(parsed)) resultOptions.runTime = parsed;
1124
1092
  }
1125
- if (suiteTimestamp) resultOptions.startTime = suiteTimestamp;
1126
1093
  const result = createResult(status, message ?? "", resultOptions);
1127
1094
  const descriptions = [{
1128
1095
  label: "default",
@@ -1227,6 +1194,13 @@ const STATUS_MAP = {
1227
1194
  * @param input - Raw XML string (XCCDF Benchmark with TestResult, or ARF asset-report-collection)
1228
1195
  * @returns Stringified HDF Results JSON
1229
1196
  */
1197
+ function parseStartTime(raw) {
1198
+ if (raw) {
1199
+ const t = parseTimestamp(raw);
1200
+ if (t) return t;
1201
+ }
1202
+ return /* @__PURE__ */ new Date();
1203
+ }
1230
1204
  async function convertXccdfResultsToHdf(input) {
1231
1205
  if (!input || !input.trim()) throw new Error("Empty input");
1232
1206
  validateInputSize(input, "xccdf-results");
@@ -1245,20 +1219,21 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1245
1219
  const { items: limitedRuleResults, truncated: truncatedRR } = limitArray(ruleResults);
1246
1220
  /* v8 ignore next -- truncation only triggers with >100K items */
1247
1221
  if (truncatedRR) console.warn(`WARNING: Input truncated at ${limitedRuleResults.length} rule-result items (original: ${ruleResults.length})`);
1248
- const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
1222
+ const scanTime = parseStartTime(testResult["start-time"]);
1223
+ const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
1249
1224
  if (requirements.length === 0) {
1250
1225
  const target = xccdfTargetName(testResult, benchmark);
1251
- requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
1226
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1252
1227
  }
1253
1228
  const resultsChecksum = await inputChecksum(rawInput);
1254
1229
  const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
1255
1230
  const components = buildTargets(testResult);
1256
- const timestamp = testResult["start-time"] ? new Date(testResult["start-time"]) : /* @__PURE__ */ new Date();
1231
+ const timestamp = scanTime;
1257
1232
  let durationSeconds;
1258
1233
  if (testResult["start-time"] && testResult["end-time"]) {
1259
- const start = new Date(testResult["start-time"]).getTime();
1260
- const end = new Date(testResult["end-time"]).getTime();
1261
- if (!isNaN(start) && !isNaN(end) && end >= start) durationSeconds = (end - start) / 1e3;
1234
+ const start = parseTimestamp(testResult["start-time"])?.getTime();
1235
+ const end = parseTimestamp(testResult["end-time"])?.getTime();
1236
+ if (start !== void 0 && end !== void 0 && end >= start) durationSeconds = (end - start) / 1e3;
1262
1237
  }
1263
1238
  const hdf = {
1264
1239
  baselines: [baseline],
@@ -1274,7 +1249,7 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1274
1249
  timestamp
1275
1250
  };
1276
1251
  if (durationSeconds !== void 0) hdf.statistics = { duration: durationSeconds };
1277
- return JSON.stringify(hdf, null, 2);
1252
+ return serializeHdf(hdf);
1278
1253
  }
1279
1254
  async function convertArfCollection(arc, rawInput) {
1280
1255
  const resultsChecksum = await inputChecksum(rawInput);
@@ -1291,20 +1266,24 @@ async function convertArfCollection(arc, rawInput) {
1291
1266
  for (const report of arc.reports?.report ?? []) {
1292
1267
  const testResult = report.content?.TestResult;
1293
1268
  if (!testResult?.id) continue;
1294
- if (testResult["start-time"] && !firstTimestamp) firstTimestamp = new Date(testResult["start-time"]);
1269
+ if (testResult["start-time"] && !firstTimestamp) {
1270
+ const t = parseTimestamp(testResult["start-time"]);
1271
+ if (t) firstTimestamp = t;
1272
+ }
1295
1273
  if (testResult["start-time"] && testResult["end-time"]) {
1296
- const start = new Date(testResult["start-time"]).getTime();
1297
- const end = new Date(testResult["end-time"]).getTime();
1298
- if (!isNaN(start) && !isNaN(end) && end >= start) totalDuration += (end - start) / 1e3;
1274
+ const start = parseTimestamp(testResult["start-time"])?.getTime();
1275
+ const end = parseTimestamp(testResult["end-time"])?.getTime();
1276
+ if (start !== void 0 && end !== void 0 && end >= start) totalDuration += (end - start) / 1e3;
1299
1277
  }
1278
+ const scanTime = parseStartTime(testResult["start-time"]);
1300
1279
  const ruleResults = testResult["rule-result"] ?? [];
1301
1280
  const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
1302
1281
  /* v8 ignore next -- truncation only triggers with >100K items */
1303
1282
  if (truncatedARFRR) console.warn(`WARNING: Input truncated at ${limitedARFRuleResults.length} rule-result items (original: ${ruleResults.length})`);
1304
- const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
1283
+ const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
1305
1284
  if (requirements.length === 0) {
1306
1285
  const target = xccdfTargetName(testResult, benchmark);
1307
- requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
1286
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1308
1287
  }
1309
1288
  let baselineName = "";
1310
1289
  if (benchmark) baselineName = extractText(benchmark.title) || "";
@@ -1337,7 +1316,7 @@ async function convertArfCollection(arc, rawInput) {
1337
1316
  timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
1338
1317
  };
1339
1318
  if (totalDuration > 0) hdf.statistics = { duration: totalDuration };
1340
- return JSON.stringify(hdf, null, 2);
1319
+ return serializeHdf(hdf);
1341
1320
  }
1342
1321
  /**
1343
1322
  * Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
@@ -1393,7 +1372,7 @@ function buildRuleIndex(benchmark) {
1393
1372
  /**
1394
1373
  * Convert a single <rule-result> into an EvaluatedRequirement.
1395
1374
  */
1396
- function ruleResultToRequirement(rr, ruleIndex) {
1375
+ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
1397
1376
  const ruleId = rr.idref ?? "";
1398
1377
  const ruleDef = ruleIndex.get(ruleId);
1399
1378
  const id = extractVersion$1(ruleDef?.version) || ruleId;
@@ -1411,7 +1390,16 @@ function ruleResultToRequirement(rr, ruleIndex) {
1411
1390
  label: "fix",
1412
1391
  data: fixtext
1413
1392
  });
1414
- const result = createResult(STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed, "", { codeDesc: `XCCDF rule ${id}` });
1393
+ if (descriptions.length === 0) descriptions.push({
1394
+ label: "default",
1395
+ data: ""
1396
+ });
1397
+ const status = STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed;
1398
+ const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
1399
+ const result = createResult(status, "", {
1400
+ codeDesc: `XCCDF rule ${id}`,
1401
+ startTime: perRuleTime ?? scanTime
1402
+ });
1415
1403
  const tags = {};
1416
1404
  let nistTags = [];
1417
1405
  const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
@@ -1589,7 +1577,7 @@ const VULN_ATTR_ORDER = [
1589
1577
  "Class",
1590
1578
  "STIG_UUID"
1591
1579
  ];
1592
- const CORE_VULN_ATTR = new Set([
1580
+ const CORE_VULN_ATTR = /* @__PURE__ */ new Set([
1593
1581
  "Vuln_Num",
1594
1582
  "Severity",
1595
1583
  "Group_Title",
@@ -1625,7 +1613,9 @@ function parseCkl(input) {
1625
1613
  webDBSite: str(a["WEB_DB_SITE"]),
1626
1614
  webDBInstance: str(a["WEB_DB_INSTANCE"])
1627
1615
  },
1628
- stigs: istigs.map((is) => {
1616
+ stigs: istigs.map((is, i) => {
1617
+ const vulns = is.VULN ?? [];
1618
+ if (vulns.length === 0) throw new Error(`parse ckl: <iSTIG> block ${i + 1} contains no <VULN> rules`);
1629
1619
  const si = is.STIG_INFO?.SI_DATA ?? [];
1630
1620
  const siVal = (name) => str(si.find((d) => d.SID_NAME === name)?.SID_DATA);
1631
1621
  return {
@@ -1635,7 +1625,7 @@ function parseCkl(input) {
1635
1625
  releaseInfo: siVal("releaseinfo"),
1636
1626
  uuid: siVal("uuid"),
1637
1627
  classification: siVal("classification"),
1638
- vulns: (is.VULN ?? []).map(cklVulnToModel)
1628
+ vulns: vulns.map(cklVulnToModel)
1639
1629
  };
1640
1630
  })
1641
1631
  };
@@ -1645,7 +1635,7 @@ function cklVulnToModel(v) {
1645
1635
  const attr = (name) => str(data.find((sd) => sd.VULN_ATTRIBUTE === name)?.ATTRIBUTE_DATA);
1646
1636
  const ccis = [];
1647
1637
  const extra = {};
1648
- const promoted = new Set([
1638
+ const promoted = /* @__PURE__ */ new Set([
1649
1639
  "CCI_REF",
1650
1640
  "Vuln_Num",
1651
1641
  "Severity",
@@ -1794,16 +1784,20 @@ function parseCklb(input) {
1794
1784
  techArea: nz(t.technology_area),
1795
1785
  classification: nz(t.classification)
1796
1786
  };
1797
- const stigs = doc.stigs.map((s) => ({
1798
- stigID: nz(s.stig_id),
1799
- title: nz(s.stig_name) || nz(s.display_name),
1800
- displayName: nz(s.display_name),
1801
- version: nz(s.version),
1802
- releaseInfo: nz(s.release_info),
1803
- uuid: nz(s.uuid),
1804
- referenceIdentifier: nz(s.reference_identifier),
1805
- vulns: (s.rules ?? []).map(cklbRuleToModel)
1806
- }));
1787
+ const stigs = doc.stigs.map((s, i) => {
1788
+ const rules = s.rules ?? [];
1789
+ if (rules.length === 0) throw new Error(`parse cklb: stigs[${i}] contains no rules[]`);
1790
+ return {
1791
+ stigID: nz(s.stig_id),
1792
+ title: nz(s.stig_name) || nz(s.display_name),
1793
+ displayName: nz(s.display_name),
1794
+ version: nz(s.version),
1795
+ releaseInfo: nz(s.release_info),
1796
+ uuid: nz(s.uuid),
1797
+ referenceIdentifier: nz(s.reference_identifier),
1798
+ vulns: rules.map(cklbRuleToModel)
1799
+ };
1800
+ });
1807
1801
  return {
1808
1802
  format: "cklb",
1809
1803
  cklbVersion: nz(doc.cklb_version),
@@ -1906,8 +1900,9 @@ const CONVERTER_VERSION = "1.0.0";
1906
1900
  * Original-format metadata is stashed in extensions/tags for round-trip.
1907
1901
  */
1908
1902
  function checklistToHdf(cl, resultsChecksum, generatorName) {
1903
+ const scanTime = /* @__PURE__ */ new Date();
1909
1904
  const hdf = {
1910
- baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
1905
+ baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum, scanTime)),
1911
1906
  generator: {
1912
1907
  name: generatorName,
1913
1908
  version: CONVERTER_VERSION
@@ -1916,7 +1911,7 @@ function checklistToHdf(cl, resultsChecksum, generatorName) {
1916
1911
  name: "DISA STIG Viewer",
1917
1912
  format: cl.format === "cklb" ? "CKLB" : "CKL"
1918
1913
  },
1919
- timestamp: /* @__PURE__ */ new Date()
1914
+ timestamp: scanTime
1920
1915
  };
1921
1916
  const component = assetToComponent(cl.asset);
1922
1917
  if (component) hdf.components = [component];
@@ -1924,15 +1919,15 @@ function checklistToHdf(cl, resultsChecksum, generatorName) {
1924
1919
  if (Object.keys(ext).length > 0) hdf.extensions = ext;
1925
1920
  return hdf;
1926
1921
  }
1927
- function stigToBaseline(s, resultsChecksum) {
1928
- const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map(vulnToRequirement), { resultsChecksum });
1922
+ function stigToBaseline(s, resultsChecksum, scanTime) {
1923
+ const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map((v) => vulnToRequirement(v, scanTime)), { resultsChecksum });
1929
1924
  if (s.title) baseline.title = s.title;
1930
1925
  if (s.version) baseline.version = s.version;
1931
1926
  const ext = baselineExtensions(s);
1932
1927
  if (Object.keys(ext).length > 0) baseline.extensions = ext;
1933
1928
  return baseline;
1934
1929
  }
1935
- function vulnToRequirement(v) {
1930
+ function vulnToRequirement(v, scanTime) {
1936
1931
  const severity = (v.severity ?? "").toLowerCase();
1937
1932
  const impact = severity ? severityToImpact(severity) : .5;
1938
1933
  const descriptions = [{
@@ -1948,7 +1943,10 @@ function vulnToRequirement(v) {
1948
1943
  data: stripHTML(v.fixText)
1949
1944
  });
1950
1945
  const message = [v.findingDetails, v.comments].map((s) => (s ?? "").trim()).filter(Boolean).join("\n\n");
1951
- const result = createResult(statusToHdf(v.status), message, { codeDesc: `STIG rule ${v.ruleVer ?? ""}` });
1946
+ const result = createResult(statusToHdf(v.status), message, {
1947
+ codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
1948
+ startTime: scanTime
1949
+ });
1952
1950
  const tags = {};
1953
1951
  let nistTags = [];
1954
1952
  if (v.ccis.length > 0) {
@@ -2238,7 +2236,7 @@ function synthesizePurl(ecosystem, name, version) {
2238
2236
  if (ecosystem === Ecosystem.Generic) return void 0;
2239
2237
  return `pkg:${ecosystem}/${name}@${version}`;
2240
2238
  }
2241
- function buildRequirement$16(vulnID, vulns, packageManager) {
2239
+ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2242
2240
  const rep = vulns[0];
2243
2241
  const cweIDs = rep.identifiers.CWE ?? [];
2244
2242
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -2253,7 +2251,10 @@ function buildRequirement$16(vulnID, vulns, packageManager) {
2253
2251
  label: "default",
2254
2252
  data: rep.description
2255
2253
  }];
2256
- const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatDependencyPath(vuln.from) }));
2254
+ const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, {
2255
+ codeDesc: formatDependencyPath(vuln.from),
2256
+ startTime: scanTime
2257
+ }));
2257
2258
  const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
2258
2259
  const controlType = deriveControlTypeFromTags(nist);
2259
2260
  if (controlType !== void 0) req.controlType = controlType;
@@ -2276,7 +2277,7 @@ function buildRequirement$16(vulnID, vulns, packageManager) {
2276
2277
  /**
2277
2278
  * Converts a single Snyk project report to an HDF baseline.
2278
2279
  */
2279
- function convertSingleProject(report, resultsChecksum) {
2280
+ function convertSingleProject(report, resultsChecksum, scanTime) {
2280
2281
  const { items: limitedVulns, truncated: truncatedVulns } = limitArray(report.vulnerabilities);
2281
2282
  /* v8 ignore next -- truncation only triggers with >100K items */
2282
2283
  if (truncatedVulns) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${report.vulnerabilities.length})`);
@@ -2287,10 +2288,10 @@ function convertSingleProject(report, resultsChecksum) {
2287
2288
  else groups.set(vuln.id, [vuln]);
2288
2289
  }
2289
2290
  const requirements = [];
2290
- for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, report.packageManager));
2291
+ for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, scanTime, report.packageManager));
2291
2292
  if (requirements.length === 0) {
2292
2293
  const target = report.projectName ?? report.path ?? "project";
2293
- requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
2294
+ requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, scanTime));
2294
2295
  }
2295
2296
  return createMinimalBaseline("Snyk Scan", requirements, {
2296
2297
  resultsChecksum,
@@ -2314,6 +2315,7 @@ async function convertSnykToHdf(input) {
2314
2315
  const detected = detectConverter(input);
2315
2316
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
2316
2317
  const resultsChecksum = await inputChecksum(input);
2318
+ const scanTime = /* @__PURE__ */ new Date();
2317
2319
  const parsed = parseJSON(input);
2318
2320
  if (!parsed || typeof parsed !== "object") throw new Error("snyk: invalid JSON");
2319
2321
  let baselines;
@@ -2322,10 +2324,10 @@ async function convertSnykToHdf(input) {
2322
2324
  const { items: limitedProjects, truncated: truncatedProjects } = limitArray(parsed);
2323
2325
  /* v8 ignore next -- truncation only triggers with >100K items */
2324
2326
  if (truncatedProjects) console.warn(`WARNING: Input truncated at ${limitedProjects.length} project items (original: ${parsed.length})`);
2325
- baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum));
2327
+ baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum, scanTime));
2326
2328
  targetName = limitedProjects[0]?.projectName ?? limitedProjects[0]?.path ?? "";
2327
2329
  } else {
2328
- baselines = [convertSingleProject(parsed, resultsChecksum)];
2330
+ baselines = [convertSingleProject(parsed, resultsChecksum, scanTime)];
2329
2331
  targetName = parsed.projectName ?? parsed.path ?? "";
2330
2332
  }
2331
2333
  return buildHdfResults({
@@ -2338,12 +2340,12 @@ async function convertSnykToHdf(input) {
2338
2340
  name: targetName,
2339
2341
  type: TargetType.Application
2340
2342
  }],
2341
- timestamp: /* @__PURE__ */ new Date()
2343
+ timestamp: scanTime
2342
2344
  });
2343
2345
  }
2344
2346
  //#endregion
2345
2347
  //#region converters/grype-to-hdf/typescript/converter.ts
2346
- const IMPACT_MAPPING$8 = new Map([
2348
+ const IMPACT_MAPPING$8 = /* @__PURE__ */ new Map([
2347
2349
  ["critical", .9],
2348
2350
  ["high", .7],
2349
2351
  ["medium", .5],
@@ -2581,7 +2583,7 @@ async function convertGrypeToHdf(input) {
2581
2583
  type: TargetType.Artifact,
2582
2584
  name: targetName
2583
2585
  }],
2584
- timestamp: grypeData.descriptor?.timestamp ? new Date(grypeData.descriptor.timestamp) : /* @__PURE__ */ new Date()
2586
+ timestamp: (grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null) ?? /* @__PURE__ */ new Date()
2585
2587
  });
2586
2588
  }
2587
2589
  //#endregion
@@ -2664,8 +2666,8 @@ function calculateTiming(hosts) {
2664
2666
  const lastHost = hosts[hosts.length - 1];
2665
2667
  const startTimeStr = getHostPropertyValue(firstHost, "HOST_START");
2666
2668
  const endTimeStr = getHostPropertyValue(lastHost, "HOST_END") || getHostPropertyValue(lastHost, "HOST_START");
2667
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
2668
- const endTime = endTimeStr ? new Date(endTimeStr) : startTime;
2669
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2670
+ const endTime = endTimeStr ? parseTimestamp(endTimeStr) ?? startTime : startTime;
2669
2671
  return {
2670
2672
  startTime,
2671
2673
  endTime,
@@ -2689,7 +2691,7 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
2689
2691
  if (requirements.length === 0) {
2690
2692
  const target = host.name || getHostPropertyValue(host, "host-ip") || "host";
2691
2693
  const startTimeStr = getHostPropertyValue(host, "HOST_START");
2692
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
2694
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2693
2695
  requirements = [buildNoFindingsRequirement("nessus-no-findings", `Nessus scanned ${target} and reported zero findings.`, startTime)];
2694
2696
  }
2695
2697
  return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
@@ -2856,8 +2858,8 @@ function buildEpss(item, host) {
2856
2858
  function epssDate(host) {
2857
2859
  const hs = getHostPropertyValue(host, "HOST_START");
2858
2860
  if (hs) {
2859
- const d = new Date(hs);
2860
- if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
2861
+ const d = parseTimestamp(hs);
2862
+ if (d) return d.toISOString().slice(0, 10);
2861
2863
  }
2862
2864
  }
2863
2865
  function buildDescriptions(item, isCompliance) {
@@ -2928,7 +2930,7 @@ function buildResult$1(item, host, isCompliance) {
2928
2930
  const codeDesc = getCodeDesc(item);
2929
2931
  const message = item.plugin_output || item["compliance-actual-value"];
2930
2932
  const startTimeStr = getHostPropertyValue(host, "HOST_START");
2931
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
2933
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2932
2934
  return {
2933
2935
  status,
2934
2936
  codeDesc,
@@ -3163,7 +3165,7 @@ function createResultFromIssue(issue, componentMap) {
3163
3165
  const codeDesc = `${component?.path || component?.longName || issue.component}${issue.line ? ` LINE : ${issue.line}` : ""}`;
3164
3166
  return createResult(status, issue.message, {
3165
3167
  codeDesc,
3166
- startTime: new Date(issue.creationDate)
3168
+ startTime: parseTimestamp(issue.creationDate) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
3167
3169
  });
3168
3170
  }
3169
3171
  function extractSourceLocation(issue, componentMap) {
@@ -3200,6 +3202,28 @@ function buildNistTags$3(sourceIdentifier, ruleName) {
3200
3202
  if (byName) return [byName];
3201
3203
  return [];
3202
3204
  }
3205
+ /**
3206
+ * Flag rules whose NIST mappings exist at a revision other than the one
3207
+ * currently selected — they emit no NIST tags here even though a mapping
3208
+ * exists elsewhere, a likely sign of a wrong revision selection. Rules
3209
+ * unmapped at every revision are not flagged. Throws in strict mode; otherwise
3210
+ * logs a single aggregated warning.
3211
+ */
3212
+ function checkRevisionAlignment(rules) {
3213
+ const rev = getCurrentNistRevision();
3214
+ const seen = /* @__PURE__ */ new Set();
3215
+ const lines = [];
3216
+ for (const rule of rules) {
3217
+ const covered = awsConfigMappedRevisions(rule.Source.SourceIdentifier, rule.ConfigRuleName);
3218
+ if (covered.length === 0 || covered.includes(rev) || seen.has(rule.ConfigRuleName)) continue;
3219
+ seen.add(rule.ConfigRuleName);
3220
+ lines.push(` - ${rule.ConfigRuleName} (mapped at Rev ${covered.join(", ")})`);
3221
+ }
3222
+ if (lines.length === 0) return;
3223
+ const detail = `${lines.length} AWS Config rule(s) have NIST 800-53 mappings at a different revision than the requested Rev ${rev}; their NIST tags were omitted:\n${lines.join("\n")}`;
3224
+ if (isNistStrict()) throw new Error(`aws-config: ${detail}\nre-run with a matching revision, or disable strict mode to convert with the gaps`);
3225
+ console.warn(`WARNING: ${detail}`);
3226
+ }
3203
3227
  function buildCheckText(rule) {
3204
3228
  const parts = [`ARN: ${rule.ConfigRuleArn || "N/A"}`, `Source Identifier: ${rule.Source.SourceIdentifier || "N/A"}`];
3205
3229
  if (rule.InputParameters && rule.InputParameters !== "{}") {
@@ -3220,7 +3244,7 @@ function buildResult(r) {
3220
3244
  const status = mapComplianceStatus(r.ComplianceType);
3221
3245
  const codeDesc = buildCodeDesc$7(q);
3222
3246
  const message = buildResultMessage(codeDesc, r.Annotation, status);
3223
- const startTime = r.ConfigRuleInvokedTime ? new Date(r.ConfigRuleInvokedTime) : void 0;
3247
+ const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
3224
3248
  return createResult(status, message ?? codeDesc, {
3225
3249
  codeDesc,
3226
3250
  ...startTime ? { startTime } : {}
@@ -3280,6 +3304,7 @@ async function convertAwsConfigToHdf(input) {
3280
3304
  const { items: limitedRules, truncated: truncatedRules } = limitArray(data.ConfigRules);
3281
3305
  /* v8 ignore next -- truncation only triggers with >100K items */
3282
3306
  if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
3307
+ checkRevisionAlignment(limitedRules);
3283
3308
  const baseline = {
3284
3309
  ...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$15), { resultsChecksum }),
3285
3310
  title: "AWS Config Compliance Results",
@@ -3329,17 +3354,20 @@ function getImpact$8(severity) {
3329
3354
  /**
3330
3355
  * Converts a single CheckovCheck to an HDF RequirementResult.
3331
3356
  */
3332
- function checkToResult(check) {
3357
+ function checkToResult(check, scanTime) {
3333
3358
  const status = mapStatus$2(check.check_result.result);
3334
3359
  const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
3335
3360
  let message;
3336
3361
  if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
3337
- return createResult(status, message ?? "", { codeDesc });
3362
+ return createResult(status, message ?? "", {
3363
+ codeDesc,
3364
+ startTime: scanTime
3365
+ });
3338
3366
  }
3339
3367
  /**
3340
3368
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
3341
3369
  */
3342
- function buildRequirement$14(checkId, checks) {
3370
+ function buildRequirement$14(checkId, checks, scanTime) {
3343
3371
  const rep = checks[0];
3344
3372
  const impact = getImpact$8(rep.severity);
3345
3373
  const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
@@ -3351,7 +3379,7 @@ function buildRequirement$14(checkId, checks) {
3351
3379
  label: "check",
3352
3380
  data: rep.guideline
3353
3381
  });
3354
- const results = checks.map(checkToResult);
3382
+ const results = checks.map((check) => checkToResult(check, scanTime));
3355
3383
  const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
3356
3384
  req.verificationMethod = VerificationMethodEnum.Automated;
3357
3385
  const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
@@ -3382,6 +3410,7 @@ async function convertCheckovToHdf(input) {
3382
3410
  const detected = detectConverter(input);
3383
3411
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3384
3412
  const resultsChecksum = await inputChecksum(input);
3413
+ const scanTime = /* @__PURE__ */ new Date();
3385
3414
  const reports = parseInput(input);
3386
3415
  const allChecks = [];
3387
3416
  const checkTypes = [];
@@ -3403,11 +3432,11 @@ async function convertCheckovToHdf(input) {
3403
3432
  else groups.set(check.check_id, [check]);
3404
3433
  }
3405
3434
  const requirements = [];
3406
- for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
3435
+ for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks, scanTime));
3407
3436
  const format = checkTypes.join(", ");
3408
3437
  if (requirements.length === 0) {
3409
3438
  const target = format || "input";
3410
- requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
3439
+ requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, scanTime));
3411
3440
  }
3412
3441
  const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
3413
3442
  return buildHdfResults({
@@ -3417,7 +3446,7 @@ async function convertCheckovToHdf(input) {
3417
3446
  toolVersion: version,
3418
3447
  toolFormat: format,
3419
3448
  baselines: [baseline],
3420
- timestamp: /* @__PURE__ */ new Date()
3449
+ timestamp: scanTime
3421
3450
  });
3422
3451
  }
3423
3452
  //#endregion
@@ -3448,19 +3477,22 @@ function formatSkipMessage(suppressions) {
3448
3477
  /**
3449
3478
  * Converts a single GosecIssue to an HDF RequirementResult.
3450
3479
  */
3451
- function issueToResult(issue) {
3480
+ function issueToResult(issue, scanTime) {
3452
3481
  const suppressed = isSuppressed(issue);
3453
3482
  const status = suppressed ? ResultStatus.NotReviewed : ResultStatus.Failed;
3454
3483
  let message;
3455
3484
  if (suppressed) message = formatSkipMessage(issue.suppressions) ?? "No justification provided";
3456
3485
  else message = `${issue.confidence} confidence of rule violation at:\n${issue.code}`;
3457
3486
  const codeDesc = `Rule ${issue.rule_id} violation detected at:\nFile: ${issue.file}\nLine: ${issue.line}\nColumn: ${issue.column}`;
3458
- return createResult(status, message, { codeDesc });
3487
+ return createResult(status, message, {
3488
+ codeDesc,
3489
+ startTime: scanTime
3490
+ });
3459
3491
  }
3460
3492
  /**
3461
3493
  * Converts a group of issues sharing a rule_id into one EvaluatedRequirement.
3462
3494
  */
3463
- function buildRequirement$13(ruleId, issues) {
3495
+ function buildRequirement$13(ruleId, issues, scanTime) {
3464
3496
  const rep = issues[0];
3465
3497
  const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
3466
3498
  const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
@@ -3471,7 +3503,7 @@ function buildRequirement$13(ruleId, issues) {
3471
3503
  url: rep.cwe.url
3472
3504
  }
3473
3505
  };
3474
- const results = issues.map(issueToResult);
3506
+ const results = issues.map((issue) => issueToResult(issue, scanTime));
3475
3507
  const descriptions = [{
3476
3508
  label: "default",
3477
3509
  data: rep.details
@@ -3511,9 +3543,10 @@ async function convertGosecToHdf(input) {
3511
3543
  if (existing) existing.push(issue);
3512
3544
  else groups.set(issue.rule_id, [issue]);
3513
3545
  }
3546
+ const scanTime = /* @__PURE__ */ new Date();
3514
3547
  const requirements = [];
3515
- for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues));
3516
- if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", /* @__PURE__ */ new Date()));
3548
+ for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues, scanTime));
3549
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", scanTime));
3517
3550
  const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
3518
3551
  return buildHdfResults({
3519
3552
  generatorName: "gosec-to-hdf",
@@ -3521,7 +3554,7 @@ async function convertGosecToHdf(input) {
3521
3554
  toolName: "gosec",
3522
3555
  toolVersion: report.GosecVersion || void 0,
3523
3556
  baselines: [baseline],
3524
- timestamp: /* @__PURE__ */ new Date()
3557
+ timestamp: scanTime
3525
3558
  });
3526
3559
  }
3527
3560
  //#endregion
@@ -3661,6 +3694,15 @@ function selectSite(sites) {
3661
3694
  }
3662
3695
  return best;
3663
3696
  }
3697
+ const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
3698
+ function parseZapTimestamp(s) {
3699
+ const trimmed = s.trim();
3700
+ if (ZAP_RFC1123_LIKE.test(trimmed)) {
3701
+ const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
3702
+ if (!isNaN(d.getTime())) return d;
3703
+ }
3704
+ return parseTimestamp(s) ?? void 0;
3705
+ }
3664
3706
  async function convertZapToHdf(input) {
3665
3707
  validateInputSize(input, "zap");
3666
3708
  registerAllFingerprints();
@@ -3762,12 +3804,15 @@ async function convertZapToHdf(input) {
3762
3804
  },
3763
3805
  tool
3764
3806
  };
3765
- if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
3766
- return JSON.stringify(hdf, null, 2);
3807
+ if (zapData["@generated"]) {
3808
+ const ts = parseZapTimestamp(zapData["@generated"]);
3809
+ if (ts) hdf.timestamp = ts;
3810
+ }
3811
+ return serializeHdf(hdf);
3767
3812
  }
3768
3813
  //#endregion
3769
3814
  //#region converters/cyclonedx-to-hdf/typescript/converter.ts
3770
- const CVSS_METHODS = new Set([
3815
+ const CVSS_METHODS = /* @__PURE__ */ new Set([
3771
3816
  "CVSSv2",
3772
3817
  "CVSSv3",
3773
3818
  "CVSSv31",
@@ -3832,6 +3877,8 @@ async function convertCyclonedxToHdf(input) {
3832
3877
  if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
3833
3878
  if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create --from <sbom-file> --component-name <name>");
3834
3879
  const resultsChecksum = await inputChecksum(input);
3880
+ const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
3881
+ const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
3835
3882
  const allComponents = flattenComponents(bom.components ?? []);
3836
3883
  const componentLookup = /* @__PURE__ */ new Map();
3837
3884
  for (const comp of allComponents) if (comp["bom-ref"]) componentLookup.set(comp["bom-ref"], comp);
@@ -3870,7 +3917,13 @@ async function convertCyclonedxToHdf(input) {
3870
3917
  data: fixParts.join("\n\n")
3871
3918
  });
3872
3919
  const affects = vuln.affects ?? [];
3873
- const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$4(componentLookup, affect.ref) })) : [createResult(ResultStatus.Failed, void 0, { codeDesc: `Vulnerability ${vuln.id}` })];
3920
+ const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, {
3921
+ codeDesc: formatCodeDesc$4(componentLookup, affect.ref),
3922
+ startTime: scanTime
3923
+ })) : [createResult(ResultStatus.Failed, void 0, {
3924
+ codeDesc: `Vulnerability ${vuln.id}`,
3925
+ startTime: scanTime
3926
+ })];
3874
3927
  const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
3875
3928
  const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
3876
3929
  const controlType = deriveControlTypeFromTags(nist);
@@ -3889,7 +3942,7 @@ async function convertCyclonedxToHdf(input) {
3889
3942
  name: targetName,
3890
3943
  type: TargetType.Application
3891
3944
  }],
3892
- timestamp: /* @__PURE__ */ new Date()
3945
+ timestamp: scanTime
3893
3946
  });
3894
3947
  }
3895
3948
  //#endregion
@@ -4060,7 +4113,7 @@ async function convertSplunkToHdf(input) {
4060
4113
  const descriptions = convertDescriptions(control.descriptions);
4061
4114
  const results = Array.isArray(control.results) ? control.results.map((result) => createResult(mapStatus$1(result.status), result.message, {
4062
4115
  codeDesc: result.code_desc,
4063
- startTime: new Date(result.start_time),
4116
+ startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
4064
4117
  runTime: result.run_time,
4065
4118
  exception: result.exception,
4066
4119
  backtrace: result.backtrace
@@ -4083,7 +4136,7 @@ async function convertSplunkToHdf(input) {
4083
4136
  allBaselines.push(createMinimalBaseline(profile.name, requirements, baselineOptions));
4084
4137
  }
4085
4138
  }
4086
- const hdf = {
4139
+ return serializeHdf({
4087
4140
  baselines: allBaselines,
4088
4141
  components: [{
4089
4142
  name: targetName,
@@ -4095,8 +4148,7 @@ async function convertSplunkToHdf(input) {
4095
4148
  name: "splunk-to-hdf",
4096
4149
  version: "1.0.0"
4097
4150
  }
4098
- };
4099
- return JSON.stringify(hdf, null, 2);
4151
+ });
4100
4152
  }
4101
4153
  //#endregion
4102
4154
  //#region converters/hdf-to-xml/typescript/converter.ts
@@ -4306,7 +4358,7 @@ async function convertGitlabToHdf(input) {
4306
4358
  const result = {
4307
4359
  status: ResultStatus.Failed,
4308
4360
  codeDesc: buildCodeDesc$4(scanType, vuln.location),
4309
- startTime: startTime ? new Date(startTime) : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4361
+ startTime: startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z") : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4310
4362
  };
4311
4363
  const impact = gitlabSeverityToImpact(vuln.severity ?? "Unknown");
4312
4364
  const req = {
@@ -4324,7 +4376,7 @@ async function convertGitlabToHdf(input) {
4324
4376
  }
4325
4377
  const label = scanTypeLabel(scanType);
4326
4378
  if (requirements.length === 0) {
4327
- const ts = startTime ? new Date(startTime) : /* @__PURE__ */ new Date();
4379
+ const ts = startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
4328
4380
  requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
4329
4381
  }
4330
4382
  const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
@@ -4352,8 +4404,11 @@ async function convertGitlabToHdf(input) {
4352
4404
  },
4353
4405
  tool
4354
4406
  };
4355
- if (report.scan?.end_time) hdf.timestamp = new Date(report.scan.end_time);
4356
- return JSON.stringify(hdf, null, 2);
4407
+ if (report.scan?.end_time) {
4408
+ const endTime = parseTimestamp(report.scan.end_time);
4409
+ if (endTime) hdf.timestamp = endTime;
4410
+ }
4411
+ return serializeHdf(hdf);
4357
4412
  }
4358
4413
  //#endregion
4359
4414
  //#region converters/trufflehog-to-hdf/typescript/converter.ts
@@ -4420,8 +4475,8 @@ function buildCodeDesc$3(f) {
4420
4475
  */
4421
4476
  function getTimestamp(f) {
4422
4477
  if (f.SourceMetadata?.Data?.Git?.timestamp) {
4423
- const ts = new Date(f.SourceMetadata.Data.Git.timestamp);
4424
- if (!isNaN(ts.getTime())) return ts;
4478
+ const ts = parseTimestamp(f.SourceMetadata.Data.Git.timestamp);
4479
+ if (ts) return ts;
4425
4480
  }
4426
4481
  return /* @__PURE__ */ new Date();
4427
4482
  }
@@ -4512,7 +4567,7 @@ async function convertTrufflehogToHdf(input) {
4512
4567
  name: repoURL,
4513
4568
  type: TargetType.Repository
4514
4569
  }];
4515
- return JSON.stringify(hdf, null, 2);
4570
+ return serializeHdf(hdf);
4516
4571
  }
4517
4572
  //#endregion
4518
4573
  //#region converters/burpsuite-to-hdf/typescript/converter.ts
@@ -4594,8 +4649,8 @@ async function convertBurpsuiteToHdf(input) {
4594
4649
  },
4595
4650
  tool
4596
4651
  };
4597
- if (exportTime) hdf.timestamp = new Date(exportTime);
4598
- return JSON.stringify(hdf, null, 2);
4652
+ if (exportTime) hdf.timestamp = parseTimestamp(exportTime) ?? void 0;
4653
+ return serializeHdf(hdf);
4599
4654
  }
4600
4655
  function buildRequirement$11(issueType, issues) {
4601
4656
  const rep = issues[0];
@@ -4702,15 +4757,31 @@ function formatSummary(f) {
4702
4757
  `IP Address, Port, Instance : ${f["IP Address, Port, Instance"] ?? ""}`
4703
4758
  ].join("\n");
4704
4759
  }
4760
+ const MONTH_ABBR = {
4761
+ Jan: "01",
4762
+ Feb: "02",
4763
+ Mar: "03",
4764
+ Apr: "04",
4765
+ May: "05",
4766
+ Jun: "06",
4767
+ Jul: "07",
4768
+ Aug: "08",
4769
+ Sep: "09",
4770
+ Oct: "10",
4771
+ Nov: "11",
4772
+ Dec: "12"
4773
+ };
4705
4774
  /**
4706
- * Parses DBProtect date format "Feb 18 2021 15:57"
4775
+ * Parses DBProtect date formats ("Feb 18 2021 15:57" or "2021-02-18 15:55").
4776
+ * Month-name values are normalized to ISO so parseTimestamp interprets them as
4777
+ * UTC, matching the Go peer and keeping output host-timezone-independent.
4707
4778
  */
4708
4779
  function parseDate(dateStr) {
4709
4780
  const trimmed = dateStr.trim();
4710
- if (!trimmed) return /* @__PURE__ */ new Date();
4711
- const parsed = new Date(trimmed);
4712
- if (!isNaN(parsed.getTime())) return parsed;
4713
- return /* @__PURE__ */ new Date();
4781
+ const ZERO = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
4782
+ if (!trimmed) return ZERO;
4783
+ const month = /^([A-Z][a-z]{2}) (\d{1,2}) (\d{4}) (\d{2}:\d{2})$/.exec(trimmed);
4784
+ return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO;
4714
4785
  }
4715
4786
  /**
4716
4787
  * Builds a single EvaluatedRequirement from a group of findings sharing a Check ID.
@@ -4989,7 +5060,7 @@ function buildRequirement$9(vuln, packageTypes, distro) {
4989
5060
  label: "default",
4990
5061
  data: vuln.description
4991
5062
  }];
4992
- const startTime = vuln.discoveredDate ? new Date(vuln.discoveredDate) : void 0;
5063
+ const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
4993
5064
  const results = [createResult(ResultStatus.Failed, void 0, {
4994
5065
  codeDesc: formatCodeDesc$2(vuln),
4995
5066
  startTime
@@ -5121,7 +5192,7 @@ function buildRequirement$8(finding, timestamp) {
5121
5192
  const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
5122
5193
  const results = [createResult(ResultStatus.Failed, void 0, {
5123
5194
  codeDesc,
5124
- startTime: timestamp ? new Date(timestamp) : void 0
5195
+ startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
5125
5196
  })];
5126
5197
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
5127
5198
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -5242,7 +5313,7 @@ function formatCodeDesc$1(entry) {
5242
5313
  /**
5243
5314
  * Builds a single EvaluatedRequirement from a group of entries sharing an ID.
5244
5315
  */
5245
- function buildRequirement$7(entryID, entries) {
5316
+ function buildRequirement$7(entryID, entries, scanTime) {
5246
5317
  const rep = entries[0];
5247
5318
  const cweIDs = extractCWEs$1(rep);
5248
5319
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -5255,7 +5326,10 @@ function buildRequirement$7(entryID, entries) {
5255
5326
  label: "default",
5256
5327
  data: formatDescription(rep)
5257
5328
  }];
5258
- const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$1(entry) }));
5329
+ const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, {
5330
+ codeDesc: formatCodeDesc$1(entry),
5331
+ startTime: scanTime
5332
+ }));
5259
5333
  const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
5260
5334
  const controlType = deriveControlTypeFromTags(nist);
5261
5335
  if (controlType !== void 0) req.controlType = controlType;
@@ -5315,6 +5389,7 @@ async function convertJfrogXrayToHdf(input) {
5315
5389
  const resultsChecksum = await inputChecksum(input);
5316
5390
  const parsed = parseJSON(input);
5317
5391
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.data)) throw new Error("jfrog-xray: invalid JSON structure");
5392
+ const scanTime = /* @__PURE__ */ new Date();
5318
5393
  const { items: limitedEntries, truncated } = limitArray(parsed.data);
5319
5394
  /* v8 ignore next -- truncation only triggers with >100K items */
5320
5395
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedEntries.length} entries (original: ${parsed.data.length})`);
@@ -5331,8 +5406,8 @@ async function convertJfrogXrayToHdf(input) {
5331
5406
  else groups.set(id, [entry]);
5332
5407
  }
5333
5408
  const requirements = [];
5334
- for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries));
5335
- if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", /* @__PURE__ */ new Date()));
5409
+ for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries, scanTime));
5410
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
5336
5411
  return buildHdfResults({
5337
5412
  generatorName: "jfrog-xray-to-hdf",
5338
5413
  converterVersion: "1.0.0",
@@ -5343,7 +5418,7 @@ async function convertJfrogXrayToHdf(input) {
5343
5418
  name: "JFrog Xray Scan",
5344
5419
  type: TargetType.Application
5345
5420
  }],
5346
- timestamp: /* @__PURE__ */ new Date()
5421
+ timestamp: scanTime
5347
5422
  });
5348
5423
  }
5349
5424
  //#endregion
@@ -5387,7 +5462,7 @@ function vulnMessage(vuln) {
5387
5462
  /**
5388
5463
  * Builds a single EvaluatedRequirement from a NeuVector vulnerability.
5389
5464
  */
5390
- function buildRequirement$6(vuln) {
5465
+ function buildRequirement$6(vuln, scanTime) {
5391
5466
  const cweIDs = extractCWEs(vuln.description);
5392
5467
  const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS);
5393
5468
  const tags = {
@@ -5399,7 +5474,10 @@ function buildRequirement$6(vuln) {
5399
5474
  label: "default",
5400
5475
  data: vuln.description
5401
5476
  }];
5402
- const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), { codeDesc: "" })];
5477
+ const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
5478
+ codeDesc: "",
5479
+ startTime: scanTime
5480
+ })];
5403
5481
  const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
5404
5482
  const controlType = deriveControlTypeFromTags(nist);
5405
5483
  if (controlType !== void 0) req.controlType = controlType;
@@ -5450,17 +5528,17 @@ async function convertNeuvectorToHdf(input) {
5450
5528
  const { items: limitedVulns, truncated } = limitArray(scan.report.vulnerabilities ?? []);
5451
5529
  /* v8 ignore next -- truncation only triggers with >100K items */
5452
5530
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${scan.report.vulnerabilities.length})`);
5531
+ const scanTime = /* @__PURE__ */ new Date();
5453
5532
  const seen = /* @__PURE__ */ new Set();
5454
5533
  const requirements = [];
5455
5534
  for (const vuln of limitedVulns) {
5456
5535
  const id = vulnID(vuln);
5457
5536
  if (seen.has(id)) continue;
5458
5537
  seen.add(id);
5459
- requirements.push(buildRequirement$6(vuln));
5538
+ requirements.push(buildRequirement$6(vuln, scanTime));
5460
5539
  }
5461
- const now = /* @__PURE__ */ new Date();
5462
5540
  const target = targetNameFromReport(scan.report);
5463
- if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, now));
5541
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
5464
5542
  return buildHdfResults({
5465
5543
  generatorName: "neuvector-to-hdf",
5466
5544
  converterVersion: "1.0.0",
@@ -5478,7 +5556,7 @@ async function convertNeuvectorToHdf(input) {
5478
5556
  registry: scan.report.registry
5479
5557
  }
5480
5558
  }],
5481
- timestamp: now
5559
+ timestamp: scanTime
5482
5560
  });
5483
5561
  }
5484
5562
  //#endregion
@@ -5553,7 +5631,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5553
5631
  const results = limitedVulns.map((vuln) => ({
5554
5632
  status: ResultStatus.Failed,
5555
5633
  codeDesc: buildCodeDesc$2(vuln, snippetMap),
5556
- startTime: new Date(startTimeStr)
5634
+ startTime: parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date()
5557
5635
  }));
5558
5636
  const req = {
5559
5637
  id: desc.classID ?? "unknown",
@@ -5619,8 +5697,8 @@ async function convertFortifyToHdf(input) {
5619
5697
  format: "FVDL"
5620
5698
  }
5621
5699
  };
5622
- if (createdDate) hdfResult.timestamp = new Date(startTimeStr);
5623
- return JSON.stringify(hdfResult, null, 2);
5700
+ if (createdDate) hdfResult.timestamp = parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date();
5701
+ return serializeHdf(hdfResult);
5624
5702
  }
5625
5703
  //#endregion
5626
5704
  //#region converters/prisma-to-hdf/typescript/converter.ts
@@ -5704,7 +5782,7 @@ function makeTitle(rec) {
5704
5782
  /**
5705
5783
  * Builds a single EvaluatedRequirement from a Prisma record.
5706
5784
  */
5707
- function buildRequirement$4(rec) {
5785
+ function buildRequirement$4(rec, scanTime) {
5708
5786
  const id = makeRequirementID(rec);
5709
5787
  const title = makeTitle(rec);
5710
5788
  const codeDesc = makeCodeDesc(rec);
@@ -5718,7 +5796,10 @@ function buildRequirement$4(rec) {
5718
5796
  label: "default",
5719
5797
  data: rec.Description
5720
5798
  }];
5721
- const results = [createResult(ResultStatus.Failed, message, { codeDesc })];
5799
+ const results = [createResult(ResultStatus.Failed, message, {
5800
+ codeDesc,
5801
+ startTime: scanTime
5802
+ })];
5722
5803
  const req = createRequirement(id, title, descriptions, getImpact$3(rec.Severity), results, { tags });
5723
5804
  const controlType = deriveControlTypeFromTags(nist);
5724
5805
  if (controlType !== void 0) req.controlType = controlType;
@@ -5781,8 +5862,8 @@ function groupByHostname(records) {
5781
5862
  /**
5782
5863
  * Builds an HDF baseline from all records for a single host.
5783
5864
  */
5784
- function buildBaseline(hostname, records, resultsChecksum) {
5785
- return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec)), {
5865
+ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
5866
+ return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec, scanTime)), {
5786
5867
  resultsChecksum,
5787
5868
  title: `Prisma Cloud Scan (${hostname})`
5788
5869
  });
@@ -5801,16 +5882,17 @@ async function convertPrismaToHdf(input) {
5801
5882
  for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
5802
5883
  const records = parseCsv(input);
5803
5884
  const resultsChecksum = await inputChecksum(input);
5885
+ const scanTime = /* @__PURE__ */ new Date();
5804
5886
  const baselines = [];
5805
5887
  const components = [];
5806
- if (records.length === 0) baselines.push(createMinimalBaseline("Prisma Cloud Scan", [buildNoFindingsRequirement("prisma-no-findings", "Prisma Cloud scanned the workload and reported zero vulnerable components.", /* @__PURE__ */ new Date())], {
5888
+ if (records.length === 0) baselines.push(createMinimalBaseline("Prisma Cloud Scan", [buildNoFindingsRequirement("prisma-no-findings", "Prisma Cloud scanned the workload and reported zero vulnerable components.", scanTime)], {
5807
5889
  resultsChecksum,
5808
5890
  title: "Prisma Cloud Scan"
5809
5891
  }));
5810
5892
  else {
5811
5893
  const hostGroups = groupByHostname(records);
5812
5894
  for (const [hostname, hostRecords] of hostGroups) {
5813
- baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
5895
+ baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum, scanTime));
5814
5896
  components.push({
5815
5897
  name: hostname,
5816
5898
  type: TargetType.Host
@@ -5824,7 +5906,7 @@ async function convertPrismaToHdf(input) {
5824
5906
  toolFormat: "CSV",
5825
5907
  baselines,
5826
5908
  components,
5827
- timestamp: /* @__PURE__ */ new Date()
5909
+ timestamp: scanTime
5828
5910
  });
5829
5911
  }
5830
5912
  //#endregion
@@ -5840,6 +5922,15 @@ const IMPACT_MAPPING$3 = {
5840
5922
  function getImpact$2(severity) {
5841
5923
  return IMPACT_MAPPING$3[severity.toLowerCase()] ?? .5;
5842
5924
  }
5925
+ const NETSPARKER_US_DATETIME = /^\d{2}\/\d{2}\/\d{4} \d{1,2}:\d{2} (AM|PM)$/;
5926
+ function parseNetsparkerTimestamp(s) {
5927
+ const trimmed = s.trim();
5928
+ if (NETSPARKER_US_DATETIME.test(trimmed)) {
5929
+ const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
5930
+ if (!isNaN(d.getTime())) return d;
5931
+ }
5932
+ return parseTimestamp(s);
5933
+ }
5843
5934
  function formatCodeDesc(request) {
5844
5935
  const parts = [];
5845
5936
  parts.push(`http-request : ${request?.content ?? ""}`);
@@ -5921,7 +6012,7 @@ function buildRequirement$3(vuln, initiated) {
5921
6012
  });
5922
6013
  const codeDesc = formatCodeDesc(vuln["http-request"]);
5923
6014
  const message = formatMessage$1(vuln["http-response"]);
5924
- const startTime = initiated ? new Date(initiated) : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6015
+ const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5925
6016
  const results = [{
5926
6017
  status: ResultStatus.Failed,
5927
6018
  codeDesc,
@@ -5967,8 +6058,7 @@ async function convertNetsparkerToHdf(input) {
5967
6058
  const targetName = target.url ?? "Unknown";
5968
6059
  const requirements = limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated));
5969
6060
  if (requirements.length === 0) {
5970
- const initiatedDate = initiated ? new Date(initiated) : /* @__PURE__ */ new Date();
5971
- const startTime = isNaN(initiatedDate.getTime()) ? /* @__PURE__ */ new Date() : initiatedDate;
6061
+ const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date();
5972
6062
  requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
5973
6063
  }
5974
6064
  return buildHdfResults({
@@ -6075,7 +6165,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
6075
6165
  });
6076
6166
  const resultObj = createResult(getStatus$1(finding.checked_items, finding.flagged_items), getMessage(finding.checked_items, finding.flagged_items, finding.items), {
6077
6167
  codeDesc: finding.description,
6078
- startTime: startTime ? new Date(startTime) : void 0
6168
+ startTime: (startTime ? parseTimestamp(startTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
6079
6169
  });
6080
6170
  const req = createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
6081
6171
  const controlType = deriveControlTypeFromTags(nist);
@@ -6120,7 +6210,7 @@ async function convertScoutsuiteToHdf(input) {
6120
6210
  provider: report.provider_code ?? report.provider_name
6121
6211
  }
6122
6212
  }],
6123
- timestamp: report.last_run.time ? new Date(report.last_run.time) : /* @__PURE__ */ new Date()
6213
+ timestamp: report.last_run.time ? parseTimestamp(report.last_run.time) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date()
6124
6214
  });
6125
6215
  }
6126
6216
  //#endregion
@@ -6217,7 +6307,7 @@ function buildRequirementFromResult(result, filename) {
6217
6307
  }];
6218
6308
  const scannerName = result.response.service_name;
6219
6309
  const startTimeStr = result.response.milestones?.service_started ?? "";
6220
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
6310
+ const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6221
6311
  const score = result.result.score;
6222
6312
  const status = determineStatus(score);
6223
6313
  let results;
@@ -6305,7 +6395,7 @@ async function convertConveyorToHdf(input) {
6305
6395
  /** Attribute prefix used by fast-xml-parser — all XML attributes are accessed via `@_name`. */
6306
6396
  const A = "@_";
6307
6397
  /** Veracode severity level (0-5) to HDF impact mapping. */
6308
- const IMPACT_MAPPING$2 = new Map([
6398
+ const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
6309
6399
  ["5", .9],
6310
6400
  ["4", .7],
6311
6401
  ["3", .5],
@@ -6327,9 +6417,7 @@ function decodeXmlEntities(s) {
6327
6417
  /** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
6328
6418
  function parseVeracodeTimestamp(ts) {
6329
6419
  if (!ts) return void 0;
6330
- const decoded = decodeXmlEntities(ts);
6331
- const d = new Date(decoded.replace(" UTC", "Z").replace(" ", "T"));
6332
- return isNaN(d.getTime()) ? void 0 : d;
6420
+ return parseTimestamp(decodeXmlEntities(ts).replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
6333
6421
  }
6334
6422
  /** Format description paragraphs into text. */
6335
6423
  function formatDesc(desc) {
@@ -6647,7 +6735,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
6647
6735
  });
6648
6736
  }
6649
6737
  const codeDesc = cs.implementationStatus || "No implementation status provided";
6650
- const startTime = createdDateTime ? new Date(createdDateTime) : void 0;
6738
+ const startTime = (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6651
6739
  const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
6652
6740
  codeDesc,
6653
6741
  ...startTime ? { startTime } : {}
@@ -6829,7 +6917,7 @@ function extractSubscriptionID(resourcePath) {
6829
6917
  /**
6830
6918
  * Converts a group of assessments sharing an assessment ID into one EvaluatedRequirement.
6831
6919
  */
6832
- function buildRequirement(assessmentID, assessments) {
6920
+ function buildRequirement(assessmentID, assessments, scanTime) {
6833
6921
  const rep = assessments[0];
6834
6922
  const meta = rep.properties.metadata;
6835
6923
  const impact = IMPACT_MAPPING$1[meta.severity.toLowerCase()] ?? .5;
@@ -6850,7 +6938,7 @@ function buildRequirement(assessmentID, assessments) {
6850
6938
  label: "fix",
6851
6939
  data: meta.remediationDescription
6852
6940
  });
6853
- const results = assessments.map(buildResultFromAssessment);
6941
+ const results = assessments.map((a) => buildResultFromAssessment(a, scanTime));
6854
6942
  const req = createRequirement(assessmentID, rep.properties.displayName, descriptions, impact, results, { tags });
6855
6943
  req.verificationMethod = VerificationMethodEnum.Automated;
6856
6944
  return req;
@@ -6858,10 +6946,13 @@ function buildRequirement(assessmentID, assessments) {
6858
6946
  /**
6859
6947
  * Converts a single assessment into an HDF RequirementResult.
6860
6948
  */
6861
- function buildResultFromAssessment(a) {
6949
+ function buildResultFromAssessment(a, scanTime) {
6862
6950
  const status = mapStatus(a.properties.status.code);
6863
6951
  const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
6864
- return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, { codeDesc });
6952
+ return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, {
6953
+ codeDesc,
6954
+ startTime: scanTime
6955
+ });
6865
6956
  }
6866
6957
  /**
6867
6958
  * Converts Microsoft Defender for Cloud assessment output to HDF format.
@@ -6871,6 +6962,7 @@ function buildResultFromAssessment(a) {
6871
6962
  */
6872
6963
  async function convertMsftDefenderCloudToHdf(input) {
6873
6964
  validateInputSize(input, "msft-defender-cloud");
6965
+ const scanTime = /* @__PURE__ */ new Date();
6874
6966
  const resultsChecksum = await inputChecksum(input);
6875
6967
  const raw = parseJSON(input);
6876
6968
  if (!raw || typeof raw !== "object") throw new Error("Invalid Defender for Cloud structure: not a valid JSON object");
@@ -6885,11 +6977,11 @@ async function convertMsftDefenderCloudToHdf(input) {
6885
6977
  else groups.set(a.name, [a]);
6886
6978
  }
6887
6979
  const requirements = [];
6888
- for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments));
6980
+ for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments, scanTime));
6889
6981
  const subscriptionID = limitedAssessments.length > 0 ? extractSubscriptionID(limitedAssessments[0].id) : "";
6890
6982
  if (requirements.length === 0) {
6891
6983
  const targetName = subscriptionID || "Unknown";
6892
- requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
6984
+ requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, scanTime));
6893
6985
  }
6894
6986
  const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
6895
6987
  const components = [];
@@ -6910,7 +7002,7 @@ async function convertMsftDefenderCloudToHdf(input) {
6910
7002
  toolFormat: "JSON",
6911
7003
  baselines: [baseline],
6912
7004
  components,
6913
- timestamp: /* @__PURE__ */ new Date()
7005
+ timestamp: scanTime
6914
7006
  });
6915
7007
  }
6916
7008
  //#endregion
@@ -7007,13 +7099,26 @@ function buildTags$1(alert) {
7007
7099
  return tags;
7008
7100
  }
7009
7101
  /**
7102
+ * Resolves a clean per-finding source timestamp, falling back to the conversion time.
7103
+ */
7104
+ function resolveStartTime(alert, scanTime) {
7105
+ const first = alert.firstActivityDateTime ? parseTimestamp(alert.firstActivityDateTime) : null;
7106
+ if (first) return first;
7107
+ const created = alert.createdDateTime ? parseTimestamp(alert.createdDateTime) : null;
7108
+ if (created) return created;
7109
+ return scanTime;
7110
+ }
7111
+ /**
7010
7112
  * Converts a single MDE alert to an HDF EvaluatedRequirement.
7011
7113
  */
7012
- function alertToRequirement(alert) {
7114
+ function alertToRequirement(alert, scanTime) {
7013
7115
  const impact = severityToImpact$1(alert.severity);
7014
7116
  const status = statusToResultStatus(alert.status, alert.classification);
7015
7117
  const codeDesc = formatEvidence(alert.evidence ?? []);
7016
- const results = [createResult(status, formatMessage(alert), { codeDesc })];
7118
+ const results = [createResult(status, formatMessage(alert), {
7119
+ codeDesc,
7120
+ startTime: resolveStartTime(alert, scanTime)
7121
+ })];
7017
7122
  const descriptions = [{
7018
7123
  label: "default",
7019
7124
  data: alert.description
@@ -7043,10 +7148,11 @@ async function convertMsftDefenderEndpointToHdf(input) {
7043
7148
  const response = parseJSON(input);
7044
7149
  if (!response || typeof response !== "object") throw new Error("Invalid Microsoft Defender for Endpoint structure: not a valid JSON object");
7045
7150
  if (!Array.isArray(response.value)) throw new Error("Invalid Microsoft Defender for Endpoint structure: missing or invalid value array");
7151
+ const scanTime = /* @__PURE__ */ new Date();
7046
7152
  const { items: limitedAlerts, truncated } = limitArray(response.value);
7047
7153
  /* v8 ignore next -- truncation only triggers with >100K items */
7048
7154
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedAlerts.length} alert items (original: ${response.value.length})`);
7049
- const requirements = limitedAlerts.map(alertToRequirement);
7155
+ const requirements = limitedAlerts.map((alert) => alertToRequirement(alert, scanTime));
7050
7156
  const seenTargets = /* @__PURE__ */ new Set();
7051
7157
  const components = [];
7052
7158
  for (const alert of limitedAlerts) {
@@ -7056,14 +7162,14 @@ async function convertMsftDefenderEndpointToHdf(input) {
7056
7162
  components.push(target);
7057
7163
  }
7058
7164
  }
7059
- if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", /* @__PURE__ */ new Date()));
7165
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", scanTime));
7060
7166
  return buildHdfResults({
7061
7167
  generatorName: "msft-defender-endpoint-to-hdf",
7062
7168
  converterVersion: "1.0.0",
7063
7169
  toolName: "Microsoft Defender for Endpoint",
7064
7170
  baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
7065
7171
  components,
7066
- timestamp: /* @__PURE__ */ new Date()
7172
+ timestamp: scanTime
7067
7173
  });
7068
7174
  }
7069
7175
  //#endregion
@@ -7097,7 +7203,7 @@ function wrap(value) {
7097
7203
  return { "#text": value };
7098
7204
  }
7099
7205
  /** Map HDF impact (0.0-1.0) to XCCDF severity string. */
7100
- function impactToSeverity$1(impact) {
7206
+ function impactToSeverity$2(impact) {
7101
7207
  if (impact >= .7) return "high";
7102
7208
  if (impact >= .4) return "medium";
7103
7209
  if (impact >= .1) return "low";
@@ -7151,7 +7257,7 @@ function buildRuleObj(req) {
7151
7257
  const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
7152
7258
  const rule = {
7153
7259
  [`${ATTR}id`]: ruleId,
7154
- [`${ATTR}severity`]: impactToSeverity$1(req.impact),
7260
+ [`${ATTR}severity`]: impactToSeverity$2(req.impact),
7155
7261
  [`${ATTR}selected`]: "true",
7156
7262
  title: wrap(req.title ?? req.id)
7157
7263
  };
@@ -7341,7 +7447,7 @@ function nistTagToControlId(tag) {
7341
7447
  * Converts a 0.0-1.0 impact value to an OSCAL severity string.
7342
7448
  * This is the reverse of extractRiskSeverity.
7343
7449
  */
7344
- function impactToSeverity(impact) {
7450
+ function impactToSeverity$1(impact) {
7345
7451
  if (impact >= .9) return "critical";
7346
7452
  if (impact >= .7) return "high";
7347
7453
  if (impact >= .4) return "moderate";
@@ -7496,7 +7602,7 @@ function requirementToFindingSet(req, timestamp) {
7496
7602
  let risk;
7497
7603
  if (req.impact > 0) {
7498
7604
  const riskUUID = crypto.randomUUID();
7499
- const severity = impactToSeverity(req.impact);
7605
+ const severity = impactToSeverity$1(req.impact);
7500
7606
  risk = {
7501
7607
  uuid: riskUUID,
7502
7608
  title: `Risk for ${req.id}`,
@@ -8614,7 +8720,7 @@ function buildVulnerability(group) {
8614
8720
  v.flags.push({
8615
8721
  label: String(o.justification),
8616
8722
  product_ids: pids,
8617
- date: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z")
8723
+ date: formatTimestampSeconds(new Date(o.appliedAt))
8618
8724
  });
8619
8725
  }
8620
8726
  emitted = true;
@@ -8678,7 +8784,7 @@ function buildVulnerability(group) {
8678
8784
  return v;
8679
8785
  }
8680
8786
  function buildDocument(amendments, converterVersion) {
8681
- const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
8787
+ const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
8682
8788
  const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
8683
8789
  const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
8684
8790
  const docVersion = amendments.version || "1";
@@ -8758,7 +8864,7 @@ async function convertHdfToOpenVex(input, converterVersion) {
8758
8864
  "@id": await buildDocumentID(input, amendments),
8759
8865
  author,
8760
8866
  role,
8761
- timestamp: (earliest ?? /* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z"),
8867
+ timestamp: formatTimestampSeconds(earliest ?? /* @__PURE__ */ new Date()),
8762
8868
  version: 1,
8763
8869
  statements
8764
8870
  };
@@ -8775,7 +8881,7 @@ function overrideToStatement(o) {
8775
8881
  "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
8776
8882
  },
8777
8883
  status: String(canonical),
8778
- timestamp: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z"),
8884
+ timestamp: formatTimestampSeconds(new Date(o.appliedAt)),
8779
8885
  products: productsFor(o)
8780
8886
  };
8781
8887
  if (canonical === VexStatus.NotAffected) {
@@ -8942,7 +9048,7 @@ function allMilestonesCompleted(o) {
8942
9048
  }
8943
9049
  function buildMetadata(a, docTime, converterVersion) {
8944
9050
  const metadata = {
8945
- timestamp: docTime.toISOString().replace(/\.\d+Z$/, "Z"),
9051
+ timestamp: formatTimestampSeconds(docTime),
8946
9052
  tools: [{
8947
9053
  vendor: "mitre",
8948
9054
  name: "hdf-to-cyclonedx-vex",
@@ -9655,7 +9761,7 @@ async function convertOscalPoamToHdf(input) {
9655
9761
  for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
9656
9762
  const systemRef = poam["import-ssp"]?.href || void 0;
9657
9763
  const appliedBy = extractAppliedBy(poam.metadata);
9658
- const amendments = {
9764
+ return serializeHdf({
9659
9765
  name: toKebabCase(poam.metadata.title, "oscal-poam"),
9660
9766
  overrides,
9661
9767
  integrity,
@@ -9666,8 +9772,7 @@ async function convertOscalPoamToHdf(input) {
9666
9772
  name: "oscal-poam-to-hdf",
9667
9773
  version: "1.0.0"
9668
9774
  }
9669
- };
9670
- return JSON.stringify(amendments, null, 2);
9775
+ });
9671
9776
  }
9672
9777
  function buildRiskMap$1(risks) {
9673
9778
  const m = /* @__PURE__ */ new Map();
@@ -9740,8 +9845,8 @@ function poamItemAppliedBy(poam) {
9740
9845
  }
9741
9846
  function poamItemAppliedAt(poam) {
9742
9847
  if (poam.metadata["last-modified"]) {
9743
- const t = new Date(poam.metadata["last-modified"]);
9744
- if (!isNaN(t.getTime())) return t;
9848
+ const t = parseTimestamp(String(poam.metadata["last-modified"]));
9849
+ if (t) return t;
9745
9850
  }
9746
9851
  return /* @__PURE__ */ new Date();
9747
9852
  }
@@ -9799,18 +9904,23 @@ async function convertOscalSarToHdf(input) {
9799
9904
  if (!doc["assessment-results"]) throw new Error("oscal-assessment-results: input is not an assessment-results document (root key is not 'assessment-results')");
9800
9905
  const sar = doc["assessment-results"];
9801
9906
  const meta = extractMetadata(sar.metadata);
9907
+ const scanTime = /* @__PURE__ */ new Date();
9802
9908
  const baselines = [];
9803
9909
  for (const result of sar.results) {
9804
- const baseline = await resultToEvaluatedBaseline(result, sar, input);
9910
+ if (!result.findings || result.findings.length === 0) {
9911
+ console.warn(`WARNING: Skipping assessment result "${result.title || result.uuid}": no findings (empty result set)`);
9912
+ continue;
9913
+ }
9914
+ const baseline = await resultToEvaluatedBaseline(result, sar, input, scanTime);
9805
9915
  baselines.push(baseline);
9806
9916
  }
9807
9917
  const planRef = sar["import-ap"]?.href || void 0;
9808
9918
  let timestamp;
9809
9919
  if (meta.lastModified) {
9810
- const t = new Date(meta.lastModified);
9811
- if (!isNaN(t.getTime())) timestamp = t;
9920
+ const t = parseTimestamp(meta.lastModified);
9921
+ if (t) timestamp = t;
9812
9922
  }
9813
- return JSON.stringify({
9923
+ return serializeHdf({
9814
9924
  baselines,
9815
9925
  generator: {
9816
9926
  name: "oscal-assessment-results-to-hdf",
@@ -9822,9 +9932,9 @@ async function convertOscalSarToHdf(input) {
9822
9932
  },
9823
9933
  timestamp: timestamp ?? /* @__PURE__ */ new Date(),
9824
9934
  planRef
9825
- }, null, 2);
9935
+ });
9826
9936
  }
9827
- async function resultToEvaluatedBaseline(result, sar, rawInput) {
9937
+ async function resultToEvaluatedBaseline(result, sar, rawInput, scanTime) {
9828
9938
  const checksum = await inputChecksum(rawInput);
9829
9939
  const obsMap = buildObservationMap(result.observations ?? []);
9830
9940
  const riskMap = buildRiskMap(result.risks ?? []);
@@ -9841,7 +9951,7 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
9841
9951
  }
9842
9952
  const requirements = [];
9843
9953
  for (const controlId of controlOrder) {
9844
- const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result);
9954
+ const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result, scanTime);
9845
9955
  requirements.push(req);
9846
9956
  }
9847
9957
  const baseline = createMinimalBaseline(sarBaselineName(result, sar), requirements, {
@@ -9853,26 +9963,26 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
9853
9963
  if (result.description) baseline.description = result.description;
9854
9964
  return baseline;
9855
9965
  }
9856
- function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result) {
9966
+ function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result, scanTime) {
9857
9967
  const nistTag = controlIdToNistTag(controlId);
9858
9968
  const title = findings[0].title || nistTag;
9859
9969
  const impact = sarFindingsImpact(findings, riskMap);
9860
9970
  const descriptions = sarBuildDescriptions(findings, obsMap);
9861
9971
  const results = [];
9862
- for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result));
9972
+ for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result, scanTime));
9863
9973
  const req = createRequirement(nistTag, title, descriptions, impact, results, { tags: { nist: [nistTag] } });
9864
9974
  const controlType = deriveControlTypeFromTags([nistTag]);
9865
9975
  if (controlType !== void 0) req.controlType = controlType;
9866
9976
  return req;
9867
9977
  }
9868
- function findingToRequirementResult(f, obsMap, riskMap, result) {
9978
+ function findingToRequirementResult(f, obsMap, riskMap, result, scanTime) {
9869
9979
  const status = mapFindingStatus(f);
9870
9980
  const codeDesc = buildCodeDesc(f, obsMap);
9871
9981
  const message = buildRiskMessage(f, riskMap);
9872
9982
  const startTime = parseResultStartTime(result);
9873
9983
  return createResult(status, message || void 0, {
9874
9984
  codeDesc,
9875
- startTime: startTime.getTime() > 0 ? startTime : void 0
9985
+ startTime: startTime.getTime() > 0 ? startTime : scanTime
9876
9986
  });
9877
9987
  }
9878
9988
  function extractControlIdFromFinding(f) {
@@ -9967,8 +10077,8 @@ function buildRiskMap(risks) {
9967
10077
  }
9968
10078
  function parseResultStartTime(result) {
9969
10079
  if (result.start) {
9970
- const t = new Date(result.start);
9971
- if (!isNaN(t.getTime())) return t;
10080
+ const t = parseTimestamp(String(result.start));
10081
+ if (t) return t;
9972
10082
  }
9973
10083
  return /* @__PURE__ */ new Date(0);
9974
10084
  }