@mitre/hdf-converters 3.3.0 → 3.3.2

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,35 +493,79 @@ 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;
508
+ else if (v1Result.skip_message !== void 0) v2Result.message = v1Result.skip_message;
500
509
  if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
501
510
  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;
511
+ if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
504
512
  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
513
  return v2Result;
521
514
  }
522
515
  /**
516
+ * Convert a v1 ref value (a string or an array of objects) to the v2
517
+ * Reference.ref union value. Returns undefined when there is no content (e.g.
518
+ * an empty array), so callers can drop empty references — matching the Go
519
+ * converter's toRef.
520
+ */
521
+ function toRefValue(raw) {
522
+ if (typeof raw === "string") return raw === "" ? void 0 : raw;
523
+ if (Array.isArray(raw)) {
524
+ const maps = raw.filter((e) => !!e && typeof e === "object" && !Array.isArray(e));
525
+ return maps.length === 0 ? void 0 : maps;
526
+ }
527
+ }
528
+ /**
529
+ * Convert a single v1 refs[] element (a bare string or an object with
530
+ * ref/url/uri) to a v2 Reference object. Returns null when the element carries
531
+ * no usable content. Key order (ref, url, uri) matches the Go Reference struct.
532
+ */
533
+ function convertRef(raw) {
534
+ if (typeof raw === "string") return raw === "" ? null : { ref: raw };
535
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
536
+ const m = raw;
537
+ const out = {};
538
+ let has = false;
539
+ const refVal = toRefValue(m.ref);
540
+ if (refVal !== void 0) {
541
+ out.ref = refVal;
542
+ has = true;
543
+ }
544
+ if (typeof m.url === "string" && m.url !== "") {
545
+ out.url = m.url;
546
+ has = true;
547
+ }
548
+ if (typeof m.uri === "string" && m.uri !== "") {
549
+ out.uri = m.uri;
550
+ has = true;
551
+ }
552
+ return has ? out : null;
553
+ }
554
+ return null;
555
+ }
556
+ /**
557
+ * Map v1 control-level refs to v2 requirement refs, dropping empty/contentless
558
+ * entries. Returns undefined when nothing maps.
559
+ */
560
+ function convertRefs(refs) {
561
+ const out = [];
562
+ for (const raw of refs) {
563
+ const ref = convertRef(raw);
564
+ if (ref) out.push(ref);
565
+ }
566
+ return out.length ? out : void 0;
567
+ }
568
+ /**
523
569
  * Convert v1.0 control to v2.0 requirement.
524
570
  * Transforms field names and structure.
525
571
  */
@@ -529,37 +575,23 @@ function convertControl(v1Control) {
529
575
  impact: v1Control.impact
530
576
  };
531
577
  if (v1Control.title !== void 0) v2Req.title = v1Control.title;
532
- if (v1Control.desc !== void 0) v2Req.desc = v1Control.desc;
533
578
  if (v1Control.descriptions !== void 0) v2Req.descriptions = v1Control.descriptions;
534
- if (v1Control.refs !== void 0) v2Req.refs = v1Control.refs;
535
579
  if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
536
580
  if (v1Control.code !== void 0) v2Req.code = v1Control.code;
581
+ if (Array.isArray(v1Control.refs)) {
582
+ const refs = convertRefs(v1Control.refs);
583
+ if (refs) v2Req.refs = refs;
584
+ }
537
585
  if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
538
- if (v1Control.waiver_data !== void 0) v2Req.waiverData = v1Control.waiver_data;
539
586
  if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
540
587
  if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
541
588
  if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
542
- v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity$2(v1Control.impact);
589
+ v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity(v1Control.impact);
543
590
  const rawNist = v1Control.tags?.nist;
544
591
  if (Array.isArray(rawNist)) {
545
592
  const controlType = deriveControlTypeFromTags(rawNist.filter((v) => typeof v === "string"));
546
593
  if (controlType !== void 0) v2Req.controlType = controlType;
547
594
  }
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
595
  return v2Req;
564
596
  }
565
597
  /**
@@ -572,7 +604,7 @@ function convertGroup(v1Group) {
572
604
  requirements: v1Group.controls
573
605
  };
574
606
  if (v1Group.title !== void 0) v2Group.title = v1Group.title;
575
- const knownFields = new Set([
607
+ const knownFields = /* @__PURE__ */ new Set([
576
608
  "id",
577
609
  "title",
578
610
  "controls"
@@ -598,7 +630,7 @@ function convertDependency(v1Dep) {
598
630
  if (v1Dep.compliance !== void 0) v2Dep.compliance = v1Dep.compliance;
599
631
  if (v1Dep.status !== void 0) v2Dep.status = v1Dep.status;
600
632
  if (v1Dep.skip_message !== void 0) v2Dep.skipMessage = v1Dep.skip_message;
601
- const knownFields = new Set([
633
+ const knownFields = /* @__PURE__ */ new Set([
602
634
  "name",
603
635
  "url",
604
636
  "path",
@@ -616,6 +648,26 @@ function convertDependency(v1Dep) {
616
648
  return v2Dep;
617
649
  }
618
650
  /**
651
+ * Map v1 profile `supports` entries (InSpec hyphenated keys) to v2
652
+ * SupportedPlatform objects. Entries that map no recognized key are dropped.
653
+ * Key order (platform, platformFamily, platformName, release) matches the Go
654
+ * SupportedPlatform struct. Returns undefined when nothing maps.
655
+ */
656
+ function convertSupports(supports) {
657
+ const out = [];
658
+ for (const raw of supports) {
659
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
660
+ const s = raw;
661
+ const sp = {};
662
+ if (typeof s.platform === "string" && s.platform !== "") sp.platform = s.platform;
663
+ if (typeof s["platform-family"] === "string" && s["platform-family"] !== "") sp.platformFamily = s["platform-family"];
664
+ if (typeof s["platform-name"] === "string" && s["platform-name"] !== "") sp.platformName = s["platform-name"];
665
+ if (typeof s.release === "string" && s.release !== "") sp.release = s.release;
666
+ if (Object.keys(sp).length > 0) out.push(sp);
667
+ }
668
+ return out.length ? out : void 0;
669
+ }
670
+ /**
619
671
  * Convert v1.0 profile to v2.0 baseline.
620
672
  * Transforms field names and nested structures.
621
673
  */
@@ -627,9 +679,12 @@ function convertProfile(v1Profile) {
627
679
  if (v1Profile.summary !== void 0) v2Baseline.summary = v1Profile.summary;
628
680
  if (v1Profile.license !== void 0) v2Baseline.license = v1Profile.license;
629
681
  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;
682
+ if (v1Profile.copyright_email !== void 0) v2Baseline.copyrightEmail = v1Profile.copyright_email;
683
+ if (v1Profile.supports?.length) {
684
+ const supports = convertSupports(v1Profile.supports);
685
+ if (supports) v2Baseline.supports = supports;
686
+ }
687
+ if (v1Profile.attributes?.length) v2Baseline.inputs = v1Profile.attributes;
633
688
  if (v1Profile.status !== void 0) v2Baseline.status = v1Profile.status;
634
689
  if (v1Profile.sha256) v2Baseline.integrity = {
635
690
  algorithm: "sha256",
@@ -638,30 +693,9 @@ function convertProfile(v1Profile) {
638
693
  if (v1Profile.parent_profile !== void 0) v2Baseline.parentBaseline = v1Profile.parent_profile;
639
694
  if (v1Profile.status_message !== void 0) v2Baseline.statusMessage = v1Profile.status_message;
640
695
  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);
696
+ if (v1Profile.groups?.length) v2Baseline.groups = v1Profile.groups.map(convertGroup);
642
697
  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;
698
+ if (v1Profile.depends?.length) v2Baseline.depends = v1Profile.depends.map(convertDependency);
665
699
  return v2Baseline;
666
700
  }
667
701
  /**
@@ -694,17 +728,21 @@ function convertV1ToV2(v1Data) {
694
728
  baselines: (v1Data.profiles || []).map(convertProfile),
695
729
  statistics: v1Data.statistics || {}
696
730
  };
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
- }];
731
+ if (v1Data.platform) {
732
+ const component = {
733
+ type: "host",
734
+ name: v1Data.platform.name
735
+ };
736
+ if (v1Data.platform.target_id || v1Data.platform.release !== void 0) {
737
+ component.osName = v1Data.platform.name;
738
+ if (v1Data.platform.release !== void 0) component.osVersion = v1Data.platform.release;
739
+ }
740
+ v2.components = [component];
741
+ }
704
742
  if (v1Data.generator) v2.generator = v1Data.generator;
705
743
  v2.tool = { name: "Heimdall Data Format v1" };
706
744
  if (v1Data.timestamp) v2.timestamp = v1Data.timestamp;
707
- const knownV1Fields = new Set([
745
+ const knownV1Fields = /* @__PURE__ */ new Set([
708
746
  "version",
709
747
  "platform",
710
748
  "profiles",
@@ -1069,8 +1107,9 @@ async function convertJunitToHdf(input) {
1069
1107
  if (!input || !input.trim()) throw new Error("Empty input");
1070
1108
  validateInputSize(input, "junit");
1071
1109
  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()));
1110
+ const scanTime = resolveScanTime(suites);
1111
+ const requirements = buildRequirements(suites, scanTime);
1112
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
1074
1113
  return buildHdfResults({
1075
1114
  generatorName: "junit-to-hdf",
1076
1115
  converterVersion: CONVERTER_VERSION$2,
@@ -1081,9 +1120,16 @@ async function convertJunitToHdf(input) {
1081
1120
  type: TargetType.Application,
1082
1121
  name
1083
1122
  }],
1084
- timestamp: /* @__PURE__ */ new Date()
1123
+ timestamp: scanTime
1085
1124
  });
1086
1125
  }
1126
+ function resolveScanTime(suites) {
1127
+ for (const suite of suites) if (suite.timestamp) {
1128
+ const parsed = parseTimestamp(suite.timestamp);
1129
+ if (parsed) return parsed;
1130
+ }
1131
+ return /* @__PURE__ */ new Date();
1132
+ }
1087
1133
  function parseJUnitXML(input) {
1088
1134
  const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
1089
1135
  if (parsed.testsuites) return {
@@ -1100,7 +1146,7 @@ function parseJUnitXML(input) {
1100
1146
  }
1101
1147
  throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
1102
1148
  }
1103
- function buildRequirements(suites) {
1149
+ function buildRequirements(suites, scanTime) {
1104
1150
  const { items: limitedSuites, truncated: truncatedSuites } = limitArray(suites);
1105
1151
  /* v8 ignore next -- truncation only triggers with >100K items */
1106
1152
  if (truncatedSuites) console.warn(`WARNING: Input truncated at ${limitedSuites.length} test suite items (original: ${suites.length})`);
@@ -1110,19 +1156,21 @@ function buildRequirements(suites) {
1110
1156
  const { items: limitedTestCases, truncated: truncatedTC } = limitArray(testcases);
1111
1157
  /* v8 ignore next -- truncation only triggers with >100K items */
1112
1158
  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));
1159
+ for (const tc of limitedTestCases) requirements.push(testCaseToRequirement(tc, scanTime));
1114
1160
  }
1115
1161
  return requirements;
1116
1162
  }
1117
- function testCaseToRequirement(tc, suiteTimestamp) {
1163
+ function testCaseToRequirement(tc, scanTime) {
1118
1164
  const id = buildID(tc);
1119
1165
  const { status, message } = resolveStatus(tc);
1120
- const resultOptions = { codeDesc: buildCodeDesc$9(tc) };
1166
+ const resultOptions = {
1167
+ codeDesc: buildCodeDesc$9(tc),
1168
+ startTime: scanTime
1169
+ };
1121
1170
  if (tc.time) {
1122
1171
  const parsed = parseFloat(tc.time);
1123
1172
  if (!isNaN(parsed)) resultOptions.runTime = parsed;
1124
1173
  }
1125
- if (suiteTimestamp) resultOptions.startTime = suiteTimestamp;
1126
1174
  const result = createResult(status, message ?? "", resultOptions);
1127
1175
  const descriptions = [{
1128
1176
  label: "default",
@@ -1227,6 +1275,13 @@ const STATUS_MAP = {
1227
1275
  * @param input - Raw XML string (XCCDF Benchmark with TestResult, or ARF asset-report-collection)
1228
1276
  * @returns Stringified HDF Results JSON
1229
1277
  */
1278
+ function parseStartTime(raw) {
1279
+ if (raw) {
1280
+ const t = parseTimestamp(raw);
1281
+ if (t) return t;
1282
+ }
1283
+ return /* @__PURE__ */ new Date();
1284
+ }
1230
1285
  async function convertXccdfResultsToHdf(input) {
1231
1286
  if (!input || !input.trim()) throw new Error("Empty input");
1232
1287
  validateInputSize(input, "xccdf-results");
@@ -1245,20 +1300,21 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1245
1300
  const { items: limitedRuleResults, truncated: truncatedRR } = limitArray(ruleResults);
1246
1301
  /* v8 ignore next -- truncation only triggers with >100K items */
1247
1302
  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));
1303
+ const scanTime = parseStartTime(testResult["start-time"]);
1304
+ const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
1249
1305
  if (requirements.length === 0) {
1250
1306
  const target = xccdfTargetName(testResult, benchmark);
1251
- requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
1307
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1252
1308
  }
1253
1309
  const resultsChecksum = await inputChecksum(rawInput);
1254
1310
  const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
1255
1311
  const components = buildTargets(testResult);
1256
- const timestamp = testResult["start-time"] ? new Date(testResult["start-time"]) : /* @__PURE__ */ new Date();
1312
+ const timestamp = scanTime;
1257
1313
  let durationSeconds;
1258
1314
  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;
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;
1262
1318
  }
1263
1319
  const hdf = {
1264
1320
  baselines: [baseline],
@@ -1274,7 +1330,7 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1274
1330
  timestamp
1275
1331
  };
1276
1332
  if (durationSeconds !== void 0) hdf.statistics = { duration: durationSeconds };
1277
- return JSON.stringify(hdf, null, 2);
1333
+ return serializeHdf(hdf);
1278
1334
  }
1279
1335
  async function convertArfCollection(arc, rawInput) {
1280
1336
  const resultsChecksum = await inputChecksum(rawInput);
@@ -1291,20 +1347,24 @@ async function convertArfCollection(arc, rawInput) {
1291
1347
  for (const report of arc.reports?.report ?? []) {
1292
1348
  const testResult = report.content?.TestResult;
1293
1349
  if (!testResult?.id) continue;
1294
- if (testResult["start-time"] && !firstTimestamp) firstTimestamp = new Date(testResult["start-time"]);
1350
+ if (testResult["start-time"] && !firstTimestamp) {
1351
+ const t = parseTimestamp(testResult["start-time"]);
1352
+ if (t) firstTimestamp = t;
1353
+ }
1295
1354
  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;
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;
1299
1358
  }
1359
+ const scanTime = parseStartTime(testResult["start-time"]);
1300
1360
  const ruleResults = testResult["rule-result"] ?? [];
1301
1361
  const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
1302
1362
  /* v8 ignore next -- truncation only triggers with >100K items */
1303
1363
  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));
1364
+ const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
1305
1365
  if (requirements.length === 0) {
1306
1366
  const target = xccdfTargetName(testResult, benchmark);
1307
- requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
1367
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1308
1368
  }
1309
1369
  let baselineName = "";
1310
1370
  if (benchmark) baselineName = extractText(benchmark.title) || "";
@@ -1337,7 +1397,7 @@ async function convertArfCollection(arc, rawInput) {
1337
1397
  timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
1338
1398
  };
1339
1399
  if (totalDuration > 0) hdf.statistics = { duration: totalDuration };
1340
- return JSON.stringify(hdf, null, 2);
1400
+ return serializeHdf(hdf);
1341
1401
  }
1342
1402
  /**
1343
1403
  * Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
@@ -1393,7 +1453,7 @@ function buildRuleIndex(benchmark) {
1393
1453
  /**
1394
1454
  * Convert a single <rule-result> into an EvaluatedRequirement.
1395
1455
  */
1396
- function ruleResultToRequirement(rr, ruleIndex) {
1456
+ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
1397
1457
  const ruleId = rr.idref ?? "";
1398
1458
  const ruleDef = ruleIndex.get(ruleId);
1399
1459
  const id = extractVersion$1(ruleDef?.version) || ruleId;
@@ -1411,7 +1471,16 @@ function ruleResultToRequirement(rr, ruleIndex) {
1411
1471
  label: "fix",
1412
1472
  data: fixtext
1413
1473
  });
1414
- const result = createResult(STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed, "", { codeDesc: `XCCDF rule ${id}` });
1474
+ if (descriptions.length === 0) descriptions.push({
1475
+ label: "default",
1476
+ data: ""
1477
+ });
1478
+ const status = STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed;
1479
+ const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
1480
+ const result = createResult(status, "", {
1481
+ codeDesc: `XCCDF rule ${id}`,
1482
+ startTime: perRuleTime ?? scanTime
1483
+ });
1415
1484
  const tags = {};
1416
1485
  let nistTags = [];
1417
1486
  const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
@@ -1589,7 +1658,7 @@ const VULN_ATTR_ORDER = [
1589
1658
  "Class",
1590
1659
  "STIG_UUID"
1591
1660
  ];
1592
- const CORE_VULN_ATTR = new Set([
1661
+ const CORE_VULN_ATTR = /* @__PURE__ */ new Set([
1593
1662
  "Vuln_Num",
1594
1663
  "Severity",
1595
1664
  "Group_Title",
@@ -1625,7 +1694,9 @@ function parseCkl(input) {
1625
1694
  webDBSite: str(a["WEB_DB_SITE"]),
1626
1695
  webDBInstance: str(a["WEB_DB_INSTANCE"])
1627
1696
  },
1628
- stigs: istigs.map((is) => {
1697
+ stigs: istigs.map((is, i) => {
1698
+ const vulns = is.VULN ?? [];
1699
+ if (vulns.length === 0) throw new Error(`parse ckl: <iSTIG> block ${i + 1} contains no <VULN> rules`);
1629
1700
  const si = is.STIG_INFO?.SI_DATA ?? [];
1630
1701
  const siVal = (name) => str(si.find((d) => d.SID_NAME === name)?.SID_DATA);
1631
1702
  return {
@@ -1635,7 +1706,7 @@ function parseCkl(input) {
1635
1706
  releaseInfo: siVal("releaseinfo"),
1636
1707
  uuid: siVal("uuid"),
1637
1708
  classification: siVal("classification"),
1638
- vulns: (is.VULN ?? []).map(cklVulnToModel)
1709
+ vulns: vulns.map(cklVulnToModel)
1639
1710
  };
1640
1711
  })
1641
1712
  };
@@ -1645,7 +1716,7 @@ function cklVulnToModel(v) {
1645
1716
  const attr = (name) => str(data.find((sd) => sd.VULN_ATTRIBUTE === name)?.ATTRIBUTE_DATA);
1646
1717
  const ccis = [];
1647
1718
  const extra = {};
1648
- const promoted = new Set([
1719
+ const promoted = /* @__PURE__ */ new Set([
1649
1720
  "CCI_REF",
1650
1721
  "Vuln_Num",
1651
1722
  "Severity",
@@ -1794,16 +1865,20 @@ function parseCklb(input) {
1794
1865
  techArea: nz(t.technology_area),
1795
1866
  classification: nz(t.classification)
1796
1867
  };
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
- }));
1868
+ const stigs = doc.stigs.map((s, i) => {
1869
+ const rules = s.rules ?? [];
1870
+ if (rules.length === 0) throw new Error(`parse cklb: stigs[${i}] contains no rules[]`);
1871
+ return {
1872
+ stigID: nz(s.stig_id),
1873
+ title: nz(s.stig_name) || nz(s.display_name),
1874
+ displayName: nz(s.display_name),
1875
+ version: nz(s.version),
1876
+ releaseInfo: nz(s.release_info),
1877
+ uuid: nz(s.uuid),
1878
+ referenceIdentifier: nz(s.reference_identifier),
1879
+ vulns: rules.map(cklbRuleToModel)
1880
+ };
1881
+ });
1807
1882
  return {
1808
1883
  format: "cklb",
1809
1884
  cklbVersion: nz(doc.cklb_version),
@@ -1906,8 +1981,9 @@ const CONVERTER_VERSION = "1.0.0";
1906
1981
  * Original-format metadata is stashed in extensions/tags for round-trip.
1907
1982
  */
1908
1983
  function checklistToHdf(cl, resultsChecksum, generatorName) {
1984
+ const scanTime = /* @__PURE__ */ new Date();
1909
1985
  const hdf = {
1910
- baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
1986
+ baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum, scanTime)),
1911
1987
  generator: {
1912
1988
  name: generatorName,
1913
1989
  version: CONVERTER_VERSION
@@ -1916,7 +1992,7 @@ function checklistToHdf(cl, resultsChecksum, generatorName) {
1916
1992
  name: "DISA STIG Viewer",
1917
1993
  format: cl.format === "cklb" ? "CKLB" : "CKL"
1918
1994
  },
1919
- timestamp: /* @__PURE__ */ new Date()
1995
+ timestamp: scanTime
1920
1996
  };
1921
1997
  const component = assetToComponent(cl.asset);
1922
1998
  if (component) hdf.components = [component];
@@ -1924,15 +2000,15 @@ function checklistToHdf(cl, resultsChecksum, generatorName) {
1924
2000
  if (Object.keys(ext).length > 0) hdf.extensions = ext;
1925
2001
  return hdf;
1926
2002
  }
1927
- function stigToBaseline(s, resultsChecksum) {
1928
- const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map(vulnToRequirement), { resultsChecksum });
2003
+ function stigToBaseline(s, resultsChecksum, scanTime) {
2004
+ const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map((v) => vulnToRequirement(v, scanTime)), { resultsChecksum });
1929
2005
  if (s.title) baseline.title = s.title;
1930
2006
  if (s.version) baseline.version = s.version;
1931
2007
  const ext = baselineExtensions(s);
1932
2008
  if (Object.keys(ext).length > 0) baseline.extensions = ext;
1933
2009
  return baseline;
1934
2010
  }
1935
- function vulnToRequirement(v) {
2011
+ function vulnToRequirement(v, scanTime) {
1936
2012
  const severity = (v.severity ?? "").toLowerCase();
1937
2013
  const impact = severity ? severityToImpact(severity) : .5;
1938
2014
  const descriptions = [{
@@ -1948,7 +2024,10 @@ function vulnToRequirement(v) {
1948
2024
  data: stripHTML(v.fixText)
1949
2025
  });
1950
2026
  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 ?? ""}` });
2027
+ const result = createResult(statusToHdf(v.status), message, {
2028
+ codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
2029
+ startTime: scanTime
2030
+ });
1952
2031
  const tags = {};
1953
2032
  let nistTags = [];
1954
2033
  if (v.ccis.length > 0) {
@@ -2238,7 +2317,7 @@ function synthesizePurl(ecosystem, name, version) {
2238
2317
  if (ecosystem === Ecosystem.Generic) return void 0;
2239
2318
  return `pkg:${ecosystem}/${name}@${version}`;
2240
2319
  }
2241
- function buildRequirement$16(vulnID, vulns, packageManager) {
2320
+ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2242
2321
  const rep = vulns[0];
2243
2322
  const cweIDs = rep.identifiers.CWE ?? [];
2244
2323
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -2253,7 +2332,10 @@ function buildRequirement$16(vulnID, vulns, packageManager) {
2253
2332
  label: "default",
2254
2333
  data: rep.description
2255
2334
  }];
2256
- const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatDependencyPath(vuln.from) }));
2335
+ const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, {
2336
+ codeDesc: formatDependencyPath(vuln.from),
2337
+ startTime: scanTime
2338
+ }));
2257
2339
  const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
2258
2340
  const controlType = deriveControlTypeFromTags(nist);
2259
2341
  if (controlType !== void 0) req.controlType = controlType;
@@ -2276,7 +2358,7 @@ function buildRequirement$16(vulnID, vulns, packageManager) {
2276
2358
  /**
2277
2359
  * Converts a single Snyk project report to an HDF baseline.
2278
2360
  */
2279
- function convertSingleProject(report, resultsChecksum) {
2361
+ function convertSingleProject(report, resultsChecksum, scanTime) {
2280
2362
  const { items: limitedVulns, truncated: truncatedVulns } = limitArray(report.vulnerabilities);
2281
2363
  /* v8 ignore next -- truncation only triggers with >100K items */
2282
2364
  if (truncatedVulns) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${report.vulnerabilities.length})`);
@@ -2287,10 +2369,10 @@ function convertSingleProject(report, resultsChecksum) {
2287
2369
  else groups.set(vuln.id, [vuln]);
2288
2370
  }
2289
2371
  const requirements = [];
2290
- for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, report.packageManager));
2372
+ for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, scanTime, report.packageManager));
2291
2373
  if (requirements.length === 0) {
2292
2374
  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()));
2375
+ requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, scanTime));
2294
2376
  }
2295
2377
  return createMinimalBaseline("Snyk Scan", requirements, {
2296
2378
  resultsChecksum,
@@ -2314,6 +2396,7 @@ async function convertSnykToHdf(input) {
2314
2396
  const detected = detectConverter(input);
2315
2397
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
2316
2398
  const resultsChecksum = await inputChecksum(input);
2399
+ const scanTime = /* @__PURE__ */ new Date();
2317
2400
  const parsed = parseJSON(input);
2318
2401
  if (!parsed || typeof parsed !== "object") throw new Error("snyk: invalid JSON");
2319
2402
  let baselines;
@@ -2322,10 +2405,10 @@ async function convertSnykToHdf(input) {
2322
2405
  const { items: limitedProjects, truncated: truncatedProjects } = limitArray(parsed);
2323
2406
  /* v8 ignore next -- truncation only triggers with >100K items */
2324
2407
  if (truncatedProjects) console.warn(`WARNING: Input truncated at ${limitedProjects.length} project items (original: ${parsed.length})`);
2325
- baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum));
2408
+ baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum, scanTime));
2326
2409
  targetName = limitedProjects[0]?.projectName ?? limitedProjects[0]?.path ?? "";
2327
2410
  } else {
2328
- baselines = [convertSingleProject(parsed, resultsChecksum)];
2411
+ baselines = [convertSingleProject(parsed, resultsChecksum, scanTime)];
2329
2412
  targetName = parsed.projectName ?? parsed.path ?? "";
2330
2413
  }
2331
2414
  return buildHdfResults({
@@ -2338,12 +2421,12 @@ async function convertSnykToHdf(input) {
2338
2421
  name: targetName,
2339
2422
  type: TargetType.Application
2340
2423
  }],
2341
- timestamp: /* @__PURE__ */ new Date()
2424
+ timestamp: scanTime
2342
2425
  });
2343
2426
  }
2344
2427
  //#endregion
2345
2428
  //#region converters/grype-to-hdf/typescript/converter.ts
2346
- const IMPACT_MAPPING$8 = new Map([
2429
+ const IMPACT_MAPPING$8 = /* @__PURE__ */ new Map([
2347
2430
  ["critical", .9],
2348
2431
  ["high", .7],
2349
2432
  ["medium", .5],
@@ -2581,7 +2664,7 @@ async function convertGrypeToHdf(input) {
2581
2664
  type: TargetType.Artifact,
2582
2665
  name: targetName
2583
2666
  }],
2584
- timestamp: grypeData.descriptor?.timestamp ? new Date(grypeData.descriptor.timestamp) : /* @__PURE__ */ new Date()
2667
+ timestamp: (grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null) ?? /* @__PURE__ */ new Date()
2585
2668
  });
2586
2669
  }
2587
2670
  //#endregion
@@ -2664,8 +2747,8 @@ function calculateTiming(hosts) {
2664
2747
  const lastHost = hosts[hosts.length - 1];
2665
2748
  const startTimeStr = getHostPropertyValue(firstHost, "HOST_START");
2666
2749
  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;
2750
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2751
+ const endTime = endTimeStr ? parseTimestamp(endTimeStr) ?? startTime : startTime;
2669
2752
  return {
2670
2753
  startTime,
2671
2754
  endTime,
@@ -2689,7 +2772,7 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
2689
2772
  if (requirements.length === 0) {
2690
2773
  const target = host.name || getHostPropertyValue(host, "host-ip") || "host";
2691
2774
  const startTimeStr = getHostPropertyValue(host, "HOST_START");
2692
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
2775
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2693
2776
  requirements = [buildNoFindingsRequirement("nessus-no-findings", `Nessus scanned ${target} and reported zero findings.`, startTime)];
2694
2777
  }
2695
2778
  return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
@@ -2856,8 +2939,8 @@ function buildEpss(item, host) {
2856
2939
  function epssDate(host) {
2857
2940
  const hs = getHostPropertyValue(host, "HOST_START");
2858
2941
  if (hs) {
2859
- const d = new Date(hs);
2860
- if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
2942
+ const d = parseTimestamp(hs);
2943
+ if (d) return d.toISOString().slice(0, 10);
2861
2944
  }
2862
2945
  }
2863
2946
  function buildDescriptions(item, isCompliance) {
@@ -2928,7 +3011,7 @@ function buildResult$1(item, host, isCompliance) {
2928
3011
  const codeDesc = getCodeDesc(item);
2929
3012
  const message = item.plugin_output || item["compliance-actual-value"];
2930
3013
  const startTimeStr = getHostPropertyValue(host, "HOST_START");
2931
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
3014
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2932
3015
  return {
2933
3016
  status,
2934
3017
  codeDesc,
@@ -3163,7 +3246,7 @@ function createResultFromIssue(issue, componentMap) {
3163
3246
  const codeDesc = `${component?.path || component?.longName || issue.component}${issue.line ? ` LINE : ${issue.line}` : ""}`;
3164
3247
  return createResult(status, issue.message, {
3165
3248
  codeDesc,
3166
- startTime: new Date(issue.creationDate)
3249
+ startTime: parseTimestamp(issue.creationDate) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
3167
3250
  });
3168
3251
  }
3169
3252
  function extractSourceLocation(issue, componentMap) {
@@ -3200,6 +3283,28 @@ function buildNistTags$3(sourceIdentifier, ruleName) {
3200
3283
  if (byName) return [byName];
3201
3284
  return [];
3202
3285
  }
3286
+ /**
3287
+ * Flag rules whose NIST mappings exist at a revision other than the one
3288
+ * currently selected — they emit no NIST tags here even though a mapping
3289
+ * exists elsewhere, a likely sign of a wrong revision selection. Rules
3290
+ * unmapped at every revision are not flagged. Throws in strict mode; otherwise
3291
+ * logs a single aggregated warning.
3292
+ */
3293
+ function checkRevisionAlignment(rules) {
3294
+ const rev = getCurrentNistRevision();
3295
+ const seen = /* @__PURE__ */ new Set();
3296
+ const lines = [];
3297
+ for (const rule of rules) {
3298
+ const covered = awsConfigMappedRevisions(rule.Source.SourceIdentifier, rule.ConfigRuleName);
3299
+ if (covered.length === 0 || covered.includes(rev) || seen.has(rule.ConfigRuleName)) continue;
3300
+ seen.add(rule.ConfigRuleName);
3301
+ lines.push(` - ${rule.ConfigRuleName} (mapped at Rev ${covered.join(", ")})`);
3302
+ }
3303
+ if (lines.length === 0) return;
3304
+ 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")}`;
3305
+ if (isNistStrict()) throw new Error(`aws-config: ${detail}\nre-run with a matching revision, or disable strict mode to convert with the gaps`);
3306
+ console.warn(`WARNING: ${detail}`);
3307
+ }
3203
3308
  function buildCheckText(rule) {
3204
3309
  const parts = [`ARN: ${rule.ConfigRuleArn || "N/A"}`, `Source Identifier: ${rule.Source.SourceIdentifier || "N/A"}`];
3205
3310
  if (rule.InputParameters && rule.InputParameters !== "{}") {
@@ -3220,7 +3325,7 @@ function buildResult(r) {
3220
3325
  const status = mapComplianceStatus(r.ComplianceType);
3221
3326
  const codeDesc = buildCodeDesc$7(q);
3222
3327
  const message = buildResultMessage(codeDesc, r.Annotation, status);
3223
- const startTime = r.ConfigRuleInvokedTime ? new Date(r.ConfigRuleInvokedTime) : void 0;
3328
+ const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
3224
3329
  return createResult(status, message ?? codeDesc, {
3225
3330
  codeDesc,
3226
3331
  ...startTime ? { startTime } : {}
@@ -3280,6 +3385,7 @@ async function convertAwsConfigToHdf(input) {
3280
3385
  const { items: limitedRules, truncated: truncatedRules } = limitArray(data.ConfigRules);
3281
3386
  /* v8 ignore next -- truncation only triggers with >100K items */
3282
3387
  if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
3388
+ checkRevisionAlignment(limitedRules);
3283
3389
  const baseline = {
3284
3390
  ...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$15), { resultsChecksum }),
3285
3391
  title: "AWS Config Compliance Results",
@@ -3329,17 +3435,20 @@ function getImpact$8(severity) {
3329
3435
  /**
3330
3436
  * Converts a single CheckovCheck to an HDF RequirementResult.
3331
3437
  */
3332
- function checkToResult(check) {
3438
+ function checkToResult(check, scanTime) {
3333
3439
  const status = mapStatus$2(check.check_result.result);
3334
3440
  const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
3335
3441
  let message;
3336
3442
  if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
3337
- return createResult(status, message ?? "", { codeDesc });
3443
+ return createResult(status, message ?? "", {
3444
+ codeDesc,
3445
+ startTime: scanTime
3446
+ });
3338
3447
  }
3339
3448
  /**
3340
3449
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
3341
3450
  */
3342
- function buildRequirement$14(checkId, checks) {
3451
+ function buildRequirement$14(checkId, checks, scanTime) {
3343
3452
  const rep = checks[0];
3344
3453
  const impact = getImpact$8(rep.severity);
3345
3454
  const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
@@ -3351,7 +3460,7 @@ function buildRequirement$14(checkId, checks) {
3351
3460
  label: "check",
3352
3461
  data: rep.guideline
3353
3462
  });
3354
- const results = checks.map(checkToResult);
3463
+ const results = checks.map((check) => checkToResult(check, scanTime));
3355
3464
  const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
3356
3465
  req.verificationMethod = VerificationMethodEnum.Automated;
3357
3466
  const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
@@ -3382,6 +3491,7 @@ async function convertCheckovToHdf(input) {
3382
3491
  const detected = detectConverter(input);
3383
3492
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3384
3493
  const resultsChecksum = await inputChecksum(input);
3494
+ const scanTime = /* @__PURE__ */ new Date();
3385
3495
  const reports = parseInput(input);
3386
3496
  const allChecks = [];
3387
3497
  const checkTypes = [];
@@ -3403,11 +3513,11 @@ async function convertCheckovToHdf(input) {
3403
3513
  else groups.set(check.check_id, [check]);
3404
3514
  }
3405
3515
  const requirements = [];
3406
- for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
3516
+ for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks, scanTime));
3407
3517
  const format = checkTypes.join(", ");
3408
3518
  if (requirements.length === 0) {
3409
3519
  const target = format || "input";
3410
- requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
3520
+ requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, scanTime));
3411
3521
  }
3412
3522
  const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
3413
3523
  return buildHdfResults({
@@ -3417,7 +3527,7 @@ async function convertCheckovToHdf(input) {
3417
3527
  toolVersion: version,
3418
3528
  toolFormat: format,
3419
3529
  baselines: [baseline],
3420
- timestamp: /* @__PURE__ */ new Date()
3530
+ timestamp: scanTime
3421
3531
  });
3422
3532
  }
3423
3533
  //#endregion
@@ -3448,19 +3558,22 @@ function formatSkipMessage(suppressions) {
3448
3558
  /**
3449
3559
  * Converts a single GosecIssue to an HDF RequirementResult.
3450
3560
  */
3451
- function issueToResult(issue) {
3561
+ function issueToResult(issue, scanTime) {
3452
3562
  const suppressed = isSuppressed(issue);
3453
3563
  const status = suppressed ? ResultStatus.NotReviewed : ResultStatus.Failed;
3454
3564
  let message;
3455
3565
  if (suppressed) message = formatSkipMessage(issue.suppressions) ?? "No justification provided";
3456
3566
  else message = `${issue.confidence} confidence of rule violation at:\n${issue.code}`;
3457
3567
  const codeDesc = `Rule ${issue.rule_id} violation detected at:\nFile: ${issue.file}\nLine: ${issue.line}\nColumn: ${issue.column}`;
3458
- return createResult(status, message, { codeDesc });
3568
+ return createResult(status, message, {
3569
+ codeDesc,
3570
+ startTime: scanTime
3571
+ });
3459
3572
  }
3460
3573
  /**
3461
3574
  * Converts a group of issues sharing a rule_id into one EvaluatedRequirement.
3462
3575
  */
3463
- function buildRequirement$13(ruleId, issues) {
3576
+ function buildRequirement$13(ruleId, issues, scanTime) {
3464
3577
  const rep = issues[0];
3465
3578
  const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
3466
3579
  const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
@@ -3471,7 +3584,7 @@ function buildRequirement$13(ruleId, issues) {
3471
3584
  url: rep.cwe.url
3472
3585
  }
3473
3586
  };
3474
- const results = issues.map(issueToResult);
3587
+ const results = issues.map((issue) => issueToResult(issue, scanTime));
3475
3588
  const descriptions = [{
3476
3589
  label: "default",
3477
3590
  data: rep.details
@@ -3511,9 +3624,10 @@ async function convertGosecToHdf(input) {
3511
3624
  if (existing) existing.push(issue);
3512
3625
  else groups.set(issue.rule_id, [issue]);
3513
3626
  }
3627
+ const scanTime = /* @__PURE__ */ new Date();
3514
3628
  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()));
3629
+ for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues, scanTime));
3630
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", scanTime));
3517
3631
  const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
3518
3632
  return buildHdfResults({
3519
3633
  generatorName: "gosec-to-hdf",
@@ -3521,7 +3635,7 @@ async function convertGosecToHdf(input) {
3521
3635
  toolName: "gosec",
3522
3636
  toolVersion: report.GosecVersion || void 0,
3523
3637
  baselines: [baseline],
3524
- timestamp: /* @__PURE__ */ new Date()
3638
+ timestamp: scanTime
3525
3639
  });
3526
3640
  }
3527
3641
  //#endregion
@@ -3661,6 +3775,15 @@ function selectSite(sites) {
3661
3775
  }
3662
3776
  return best;
3663
3777
  }
3778
+ const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
3779
+ function parseZapTimestamp(s) {
3780
+ const trimmed = s.trim();
3781
+ if (ZAP_RFC1123_LIKE.test(trimmed)) {
3782
+ const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
3783
+ if (!isNaN(d.getTime())) return d;
3784
+ }
3785
+ return parseTimestamp(s) ?? void 0;
3786
+ }
3664
3787
  async function convertZapToHdf(input) {
3665
3788
  validateInputSize(input, "zap");
3666
3789
  registerAllFingerprints();
@@ -3762,12 +3885,15 @@ async function convertZapToHdf(input) {
3762
3885
  },
3763
3886
  tool
3764
3887
  };
3765
- if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
3766
- return JSON.stringify(hdf, null, 2);
3888
+ if (zapData["@generated"]) {
3889
+ const ts = parseZapTimestamp(zapData["@generated"]);
3890
+ if (ts) hdf.timestamp = ts;
3891
+ }
3892
+ return serializeHdf(hdf);
3767
3893
  }
3768
3894
  //#endregion
3769
3895
  //#region converters/cyclonedx-to-hdf/typescript/converter.ts
3770
- const CVSS_METHODS = new Set([
3896
+ const CVSS_METHODS = /* @__PURE__ */ new Set([
3771
3897
  "CVSSv2",
3772
3898
  "CVSSv3",
3773
3899
  "CVSSv31",
@@ -3832,6 +3958,8 @@ async function convertCyclonedxToHdf(input) {
3832
3958
  if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
3833
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>");
3834
3960
  const resultsChecksum = await inputChecksum(input);
3961
+ const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
3962
+ const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
3835
3963
  const allComponents = flattenComponents(bom.components ?? []);
3836
3964
  const componentLookup = /* @__PURE__ */ new Map();
3837
3965
  for (const comp of allComponents) if (comp["bom-ref"]) componentLookup.set(comp["bom-ref"], comp);
@@ -3870,7 +3998,13 @@ async function convertCyclonedxToHdf(input) {
3870
3998
  data: fixParts.join("\n\n")
3871
3999
  });
3872
4000
  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}` })];
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}`,
4006
+ startTime: scanTime
4007
+ })];
3874
4008
  const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
3875
4009
  const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
3876
4010
  const controlType = deriveControlTypeFromTags(nist);
@@ -3889,7 +4023,7 @@ async function convertCyclonedxToHdf(input) {
3889
4023
  name: targetName,
3890
4024
  type: TargetType.Application
3891
4025
  }],
3892
- timestamp: /* @__PURE__ */ new Date()
4026
+ timestamp: scanTime
3893
4027
  });
3894
4028
  }
3895
4029
  //#endregion
@@ -4060,7 +4194,7 @@ async function convertSplunkToHdf(input) {
4060
4194
  const descriptions = convertDescriptions(control.descriptions);
4061
4195
  const results = Array.isArray(control.results) ? control.results.map((result) => createResult(mapStatus$1(result.status), result.message, {
4062
4196
  codeDesc: result.code_desc,
4063
- startTime: new Date(result.start_time),
4197
+ startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
4064
4198
  runTime: result.run_time,
4065
4199
  exception: result.exception,
4066
4200
  backtrace: result.backtrace
@@ -4083,7 +4217,7 @@ async function convertSplunkToHdf(input) {
4083
4217
  allBaselines.push(createMinimalBaseline(profile.name, requirements, baselineOptions));
4084
4218
  }
4085
4219
  }
4086
- const hdf = {
4220
+ return serializeHdf({
4087
4221
  baselines: allBaselines,
4088
4222
  components: [{
4089
4223
  name: targetName,
@@ -4095,8 +4229,7 @@ async function convertSplunkToHdf(input) {
4095
4229
  name: "splunk-to-hdf",
4096
4230
  version: "1.0.0"
4097
4231
  }
4098
- };
4099
- return JSON.stringify(hdf, null, 2);
4232
+ });
4100
4233
  }
4101
4234
  //#endregion
4102
4235
  //#region converters/hdf-to-xml/typescript/converter.ts
@@ -4306,7 +4439,7 @@ async function convertGitlabToHdf(input) {
4306
4439
  const result = {
4307
4440
  status: ResultStatus.Failed,
4308
4441
  codeDesc: buildCodeDesc$4(scanType, vuln.location),
4309
- startTime: startTime ? new Date(startTime) : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4442
+ startTime: startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z") : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4310
4443
  };
4311
4444
  const impact = gitlabSeverityToImpact(vuln.severity ?? "Unknown");
4312
4445
  const req = {
@@ -4324,7 +4457,7 @@ async function convertGitlabToHdf(input) {
4324
4457
  }
4325
4458
  const label = scanTypeLabel(scanType);
4326
4459
  if (requirements.length === 0) {
4327
- const ts = startTime ? new Date(startTime) : /* @__PURE__ */ new Date();
4460
+ const ts = startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
4328
4461
  requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
4329
4462
  }
4330
4463
  const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
@@ -4352,8 +4485,11 @@ async function convertGitlabToHdf(input) {
4352
4485
  },
4353
4486
  tool
4354
4487
  };
4355
- if (report.scan?.end_time) hdf.timestamp = new Date(report.scan.end_time);
4356
- return JSON.stringify(hdf, null, 2);
4488
+ if (report.scan?.end_time) {
4489
+ const endTime = parseTimestamp(report.scan.end_time);
4490
+ if (endTime) hdf.timestamp = endTime;
4491
+ }
4492
+ return serializeHdf(hdf);
4357
4493
  }
4358
4494
  //#endregion
4359
4495
  //#region converters/trufflehog-to-hdf/typescript/converter.ts
@@ -4420,8 +4556,8 @@ function buildCodeDesc$3(f) {
4420
4556
  */
4421
4557
  function getTimestamp(f) {
4422
4558
  if (f.SourceMetadata?.Data?.Git?.timestamp) {
4423
- const ts = new Date(f.SourceMetadata.Data.Git.timestamp);
4424
- if (!isNaN(ts.getTime())) return ts;
4559
+ const ts = parseTimestamp(f.SourceMetadata.Data.Git.timestamp);
4560
+ if (ts) return ts;
4425
4561
  }
4426
4562
  return /* @__PURE__ */ new Date();
4427
4563
  }
@@ -4512,7 +4648,7 @@ async function convertTrufflehogToHdf(input) {
4512
4648
  name: repoURL,
4513
4649
  type: TargetType.Repository
4514
4650
  }];
4515
- return JSON.stringify(hdf, null, 2);
4651
+ return serializeHdf(hdf);
4516
4652
  }
4517
4653
  //#endregion
4518
4654
  //#region converters/burpsuite-to-hdf/typescript/converter.ts
@@ -4594,8 +4730,8 @@ async function convertBurpsuiteToHdf(input) {
4594
4730
  },
4595
4731
  tool
4596
4732
  };
4597
- if (exportTime) hdf.timestamp = new Date(exportTime);
4598
- return JSON.stringify(hdf, null, 2);
4733
+ if (exportTime) hdf.timestamp = parseTimestamp(exportTime) ?? void 0;
4734
+ return serializeHdf(hdf);
4599
4735
  }
4600
4736
  function buildRequirement$11(issueType, issues) {
4601
4737
  const rep = issues[0];
@@ -4702,15 +4838,31 @@ function formatSummary(f) {
4702
4838
  `IP Address, Port, Instance : ${f["IP Address, Port, Instance"] ?? ""}`
4703
4839
  ].join("\n");
4704
4840
  }
4841
+ const MONTH_ABBR = {
4842
+ Jan: "01",
4843
+ Feb: "02",
4844
+ Mar: "03",
4845
+ Apr: "04",
4846
+ May: "05",
4847
+ Jun: "06",
4848
+ Jul: "07",
4849
+ Aug: "08",
4850
+ Sep: "09",
4851
+ Oct: "10",
4852
+ Nov: "11",
4853
+ Dec: "12"
4854
+ };
4705
4855
  /**
4706
- * Parses DBProtect date format "Feb 18 2021 15:57"
4856
+ * Parses DBProtect date formats ("Feb 18 2021 15:57" or "2021-02-18 15:55").
4857
+ * Month-name values are normalized to ISO so parseTimestamp interprets them as
4858
+ * UTC, matching the Go peer and keeping output host-timezone-independent.
4707
4859
  */
4708
4860
  function parseDate(dateStr) {
4709
4861
  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();
4862
+ const ZERO = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
4863
+ if (!trimmed) return ZERO;
4864
+ const month = /^([A-Z][a-z]{2}) (\d{1,2}) (\d{4}) (\d{2}:\d{2})$/.exec(trimmed);
4865
+ return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO;
4714
4866
  }
4715
4867
  /**
4716
4868
  * Builds a single EvaluatedRequirement from a group of findings sharing a Check ID.
@@ -4989,7 +5141,7 @@ function buildRequirement$9(vuln, packageTypes, distro) {
4989
5141
  label: "default",
4990
5142
  data: vuln.description
4991
5143
  }];
4992
- const startTime = vuln.discoveredDate ? new Date(vuln.discoveredDate) : void 0;
5144
+ const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
4993
5145
  const results = [createResult(ResultStatus.Failed, void 0, {
4994
5146
  codeDesc: formatCodeDesc$2(vuln),
4995
5147
  startTime
@@ -5121,7 +5273,7 @@ function buildRequirement$8(finding, timestamp) {
5121
5273
  const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
5122
5274
  const results = [createResult(ResultStatus.Failed, void 0, {
5123
5275
  codeDesc,
5124
- startTime: timestamp ? new Date(timestamp) : void 0
5276
+ startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
5125
5277
  })];
5126
5278
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
5127
5279
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -5242,7 +5394,7 @@ function formatCodeDesc$1(entry) {
5242
5394
  /**
5243
5395
  * Builds a single EvaluatedRequirement from a group of entries sharing an ID.
5244
5396
  */
5245
- function buildRequirement$7(entryID, entries) {
5397
+ function buildRequirement$7(entryID, entries, scanTime) {
5246
5398
  const rep = entries[0];
5247
5399
  const cweIDs = extractCWEs$1(rep);
5248
5400
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -5255,7 +5407,10 @@ function buildRequirement$7(entryID, entries) {
5255
5407
  label: "default",
5256
5408
  data: formatDescription(rep)
5257
5409
  }];
5258
- const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$1(entry) }));
5410
+ const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, {
5411
+ codeDesc: formatCodeDesc$1(entry),
5412
+ startTime: scanTime
5413
+ }));
5259
5414
  const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
5260
5415
  const controlType = deriveControlTypeFromTags(nist);
5261
5416
  if (controlType !== void 0) req.controlType = controlType;
@@ -5315,6 +5470,7 @@ async function convertJfrogXrayToHdf(input) {
5315
5470
  const resultsChecksum = await inputChecksum(input);
5316
5471
  const parsed = parseJSON(input);
5317
5472
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.data)) throw new Error("jfrog-xray: invalid JSON structure");
5473
+ const scanTime = /* @__PURE__ */ new Date();
5318
5474
  const { items: limitedEntries, truncated } = limitArray(parsed.data);
5319
5475
  /* v8 ignore next -- truncation only triggers with >100K items */
5320
5476
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedEntries.length} entries (original: ${parsed.data.length})`);
@@ -5331,8 +5487,8 @@ async function convertJfrogXrayToHdf(input) {
5331
5487
  else groups.set(id, [entry]);
5332
5488
  }
5333
5489
  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()));
5490
+ for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries, scanTime));
5491
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
5336
5492
  return buildHdfResults({
5337
5493
  generatorName: "jfrog-xray-to-hdf",
5338
5494
  converterVersion: "1.0.0",
@@ -5343,7 +5499,7 @@ async function convertJfrogXrayToHdf(input) {
5343
5499
  name: "JFrog Xray Scan",
5344
5500
  type: TargetType.Application
5345
5501
  }],
5346
- timestamp: /* @__PURE__ */ new Date()
5502
+ timestamp: scanTime
5347
5503
  });
5348
5504
  }
5349
5505
  //#endregion
@@ -5387,7 +5543,7 @@ function vulnMessage(vuln) {
5387
5543
  /**
5388
5544
  * Builds a single EvaluatedRequirement from a NeuVector vulnerability.
5389
5545
  */
5390
- function buildRequirement$6(vuln) {
5546
+ function buildRequirement$6(vuln, scanTime) {
5391
5547
  const cweIDs = extractCWEs(vuln.description);
5392
5548
  const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS);
5393
5549
  const tags = {
@@ -5399,7 +5555,10 @@ function buildRequirement$6(vuln) {
5399
5555
  label: "default",
5400
5556
  data: vuln.description
5401
5557
  }];
5402
- const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), { codeDesc: "" })];
5558
+ const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
5559
+ codeDesc: "",
5560
+ startTime: scanTime
5561
+ })];
5403
5562
  const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
5404
5563
  const controlType = deriveControlTypeFromTags(nist);
5405
5564
  if (controlType !== void 0) req.controlType = controlType;
@@ -5450,17 +5609,17 @@ async function convertNeuvectorToHdf(input) {
5450
5609
  const { items: limitedVulns, truncated } = limitArray(scan.report.vulnerabilities ?? []);
5451
5610
  /* v8 ignore next -- truncation only triggers with >100K items */
5452
5611
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${scan.report.vulnerabilities.length})`);
5612
+ const scanTime = /* @__PURE__ */ new Date();
5453
5613
  const seen = /* @__PURE__ */ new Set();
5454
5614
  const requirements = [];
5455
5615
  for (const vuln of limitedVulns) {
5456
5616
  const id = vulnID(vuln);
5457
5617
  if (seen.has(id)) continue;
5458
5618
  seen.add(id);
5459
- requirements.push(buildRequirement$6(vuln));
5619
+ requirements.push(buildRequirement$6(vuln, scanTime));
5460
5620
  }
5461
- const now = /* @__PURE__ */ new Date();
5462
5621
  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));
5622
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
5464
5623
  return buildHdfResults({
5465
5624
  generatorName: "neuvector-to-hdf",
5466
5625
  converterVersion: "1.0.0",
@@ -5478,7 +5637,7 @@ async function convertNeuvectorToHdf(input) {
5478
5637
  registry: scan.report.registry
5479
5638
  }
5480
5639
  }],
5481
- timestamp: now
5640
+ timestamp: scanTime
5482
5641
  });
5483
5642
  }
5484
5643
  //#endregion
@@ -5553,7 +5712,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
5553
5712
  const results = limitedVulns.map((vuln) => ({
5554
5713
  status: ResultStatus.Failed,
5555
5714
  codeDesc: buildCodeDesc$2(vuln, snippetMap),
5556
- startTime: new Date(startTimeStr)
5715
+ startTime: parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date()
5557
5716
  }));
5558
5717
  const req = {
5559
5718
  id: desc.classID ?? "unknown",
@@ -5619,8 +5778,8 @@ async function convertFortifyToHdf(input) {
5619
5778
  format: "FVDL"
5620
5779
  }
5621
5780
  };
5622
- if (createdDate) hdfResult.timestamp = new Date(startTimeStr);
5623
- return JSON.stringify(hdfResult, null, 2);
5781
+ if (createdDate) hdfResult.timestamp = parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date();
5782
+ return serializeHdf(hdfResult);
5624
5783
  }
5625
5784
  //#endregion
5626
5785
  //#region converters/prisma-to-hdf/typescript/converter.ts
@@ -5704,7 +5863,7 @@ function makeTitle(rec) {
5704
5863
  /**
5705
5864
  * Builds a single EvaluatedRequirement from a Prisma record.
5706
5865
  */
5707
- function buildRequirement$4(rec) {
5866
+ function buildRequirement$4(rec, scanTime) {
5708
5867
  const id = makeRequirementID(rec);
5709
5868
  const title = makeTitle(rec);
5710
5869
  const codeDesc = makeCodeDesc(rec);
@@ -5718,7 +5877,10 @@ function buildRequirement$4(rec) {
5718
5877
  label: "default",
5719
5878
  data: rec.Description
5720
5879
  }];
5721
- const results = [createResult(ResultStatus.Failed, message, { codeDesc })];
5880
+ const results = [createResult(ResultStatus.Failed, message, {
5881
+ codeDesc,
5882
+ startTime: scanTime
5883
+ })];
5722
5884
  const req = createRequirement(id, title, descriptions, getImpact$3(rec.Severity), results, { tags });
5723
5885
  const controlType = deriveControlTypeFromTags(nist);
5724
5886
  if (controlType !== void 0) req.controlType = controlType;
@@ -5781,8 +5943,8 @@ function groupByHostname(records) {
5781
5943
  /**
5782
5944
  * Builds an HDF baseline from all records for a single host.
5783
5945
  */
5784
- function buildBaseline(hostname, records, resultsChecksum) {
5785
- return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec)), {
5946
+ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
5947
+ return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec, scanTime)), {
5786
5948
  resultsChecksum,
5787
5949
  title: `Prisma Cloud Scan (${hostname})`
5788
5950
  });
@@ -5801,16 +5963,17 @@ async function convertPrismaToHdf(input) {
5801
5963
  for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
5802
5964
  const records = parseCsv(input);
5803
5965
  const resultsChecksum = await inputChecksum(input);
5966
+ const scanTime = /* @__PURE__ */ new Date();
5804
5967
  const baselines = [];
5805
5968
  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())], {
5969
+ 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
5970
  resultsChecksum,
5808
5971
  title: "Prisma Cloud Scan"
5809
5972
  }));
5810
5973
  else {
5811
5974
  const hostGroups = groupByHostname(records);
5812
5975
  for (const [hostname, hostRecords] of hostGroups) {
5813
- baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
5976
+ baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum, scanTime));
5814
5977
  components.push({
5815
5978
  name: hostname,
5816
5979
  type: TargetType.Host
@@ -5824,7 +5987,7 @@ async function convertPrismaToHdf(input) {
5824
5987
  toolFormat: "CSV",
5825
5988
  baselines,
5826
5989
  components,
5827
- timestamp: /* @__PURE__ */ new Date()
5990
+ timestamp: scanTime
5828
5991
  });
5829
5992
  }
5830
5993
  //#endregion
@@ -5840,6 +6003,15 @@ const IMPACT_MAPPING$3 = {
5840
6003
  function getImpact$2(severity) {
5841
6004
  return IMPACT_MAPPING$3[severity.toLowerCase()] ?? .5;
5842
6005
  }
6006
+ const NETSPARKER_US_DATETIME = /^\d{2}\/\d{2}\/\d{4} \d{1,2}:\d{2} (AM|PM)$/;
6007
+ function parseNetsparkerTimestamp(s) {
6008
+ const trimmed = s.trim();
6009
+ if (NETSPARKER_US_DATETIME.test(trimmed)) {
6010
+ const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
6011
+ if (!isNaN(d.getTime())) return d;
6012
+ }
6013
+ return parseTimestamp(s);
6014
+ }
5843
6015
  function formatCodeDesc(request) {
5844
6016
  const parts = [];
5845
6017
  parts.push(`http-request : ${request?.content ?? ""}`);
@@ -5921,7 +6093,7 @@ function buildRequirement$3(vuln, initiated) {
5921
6093
  });
5922
6094
  const codeDesc = formatCodeDesc(vuln["http-request"]);
5923
6095
  const message = formatMessage$1(vuln["http-response"]);
5924
- const startTime = initiated ? new Date(initiated) : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6096
+ const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5925
6097
  const results = [{
5926
6098
  status: ResultStatus.Failed,
5927
6099
  codeDesc,
@@ -5967,8 +6139,7 @@ async function convertNetsparkerToHdf(input) {
5967
6139
  const targetName = target.url ?? "Unknown";
5968
6140
  const requirements = limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated));
5969
6141
  if (requirements.length === 0) {
5970
- const initiatedDate = initiated ? new Date(initiated) : /* @__PURE__ */ new Date();
5971
- const startTime = isNaN(initiatedDate.getTime()) ? /* @__PURE__ */ new Date() : initiatedDate;
6142
+ const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date();
5972
6143
  requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
5973
6144
  }
5974
6145
  return buildHdfResults({
@@ -6075,7 +6246,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
6075
6246
  });
6076
6247
  const resultObj = createResult(getStatus$1(finding.checked_items, finding.flagged_items), getMessage(finding.checked_items, finding.flagged_items, finding.items), {
6077
6248
  codeDesc: finding.description,
6078
- startTime: startTime ? new Date(startTime) : void 0
6249
+ startTime: (startTime ? parseTimestamp(startTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
6079
6250
  });
6080
6251
  const req = createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
6081
6252
  const controlType = deriveControlTypeFromTags(nist);
@@ -6120,7 +6291,7 @@ async function convertScoutsuiteToHdf(input) {
6120
6291
  provider: report.provider_code ?? report.provider_name
6121
6292
  }
6122
6293
  }],
6123
- timestamp: report.last_run.time ? new Date(report.last_run.time) : /* @__PURE__ */ new Date()
6294
+ timestamp: report.last_run.time ? parseTimestamp(report.last_run.time) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date()
6124
6295
  });
6125
6296
  }
6126
6297
  //#endregion
@@ -6217,7 +6388,7 @@ function buildRequirementFromResult(result, filename) {
6217
6388
  }];
6218
6389
  const scannerName = result.response.service_name;
6219
6390
  const startTimeStr = result.response.milestones?.service_started ?? "";
6220
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
6391
+ const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6221
6392
  const score = result.result.score;
6222
6393
  const status = determineStatus(score);
6223
6394
  let results;
@@ -6305,7 +6476,7 @@ async function convertConveyorToHdf(input) {
6305
6476
  /** Attribute prefix used by fast-xml-parser — all XML attributes are accessed via `@_name`. */
6306
6477
  const A = "@_";
6307
6478
  /** Veracode severity level (0-5) to HDF impact mapping. */
6308
- const IMPACT_MAPPING$2 = new Map([
6479
+ const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
6309
6480
  ["5", .9],
6310
6481
  ["4", .7],
6311
6482
  ["3", .5],
@@ -6327,9 +6498,7 @@ function decodeXmlEntities(s) {
6327
6498
  /** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
6328
6499
  function parseVeracodeTimestamp(ts) {
6329
6500
  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;
6501
+ return parseTimestamp(decodeXmlEntities(ts).replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
6333
6502
  }
6334
6503
  /** Format description paragraphs into text. */
6335
6504
  function formatDesc(desc) {
@@ -6647,7 +6816,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
6647
6816
  });
6648
6817
  }
6649
6818
  const codeDesc = cs.implementationStatus || "No implementation status provided";
6650
- const startTime = createdDateTime ? new Date(createdDateTime) : void 0;
6819
+ const startTime = (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6651
6820
  const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
6652
6821
  codeDesc,
6653
6822
  ...startTime ? { startTime } : {}
@@ -6829,7 +6998,7 @@ function extractSubscriptionID(resourcePath) {
6829
6998
  /**
6830
6999
  * Converts a group of assessments sharing an assessment ID into one EvaluatedRequirement.
6831
7000
  */
6832
- function buildRequirement(assessmentID, assessments) {
7001
+ function buildRequirement(assessmentID, assessments, scanTime) {
6833
7002
  const rep = assessments[0];
6834
7003
  const meta = rep.properties.metadata;
6835
7004
  const impact = IMPACT_MAPPING$1[meta.severity.toLowerCase()] ?? .5;
@@ -6850,7 +7019,7 @@ function buildRequirement(assessmentID, assessments) {
6850
7019
  label: "fix",
6851
7020
  data: meta.remediationDescription
6852
7021
  });
6853
- const results = assessments.map(buildResultFromAssessment);
7022
+ const results = assessments.map((a) => buildResultFromAssessment(a, scanTime));
6854
7023
  const req = createRequirement(assessmentID, rep.properties.displayName, descriptions, impact, results, { tags });
6855
7024
  req.verificationMethod = VerificationMethodEnum.Automated;
6856
7025
  return req;
@@ -6858,10 +7027,13 @@ function buildRequirement(assessmentID, assessments) {
6858
7027
  /**
6859
7028
  * Converts a single assessment into an HDF RequirementResult.
6860
7029
  */
6861
- function buildResultFromAssessment(a) {
7030
+ function buildResultFromAssessment(a, scanTime) {
6862
7031
  const status = mapStatus(a.properties.status.code);
6863
7032
  const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
6864
- return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, { codeDesc });
7033
+ return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, {
7034
+ codeDesc,
7035
+ startTime: scanTime
7036
+ });
6865
7037
  }
6866
7038
  /**
6867
7039
  * Converts Microsoft Defender for Cloud assessment output to HDF format.
@@ -6871,6 +7043,7 @@ function buildResultFromAssessment(a) {
6871
7043
  */
6872
7044
  async function convertMsftDefenderCloudToHdf(input) {
6873
7045
  validateInputSize(input, "msft-defender-cloud");
7046
+ const scanTime = /* @__PURE__ */ new Date();
6874
7047
  const resultsChecksum = await inputChecksum(input);
6875
7048
  const raw = parseJSON(input);
6876
7049
  if (!raw || typeof raw !== "object") throw new Error("Invalid Defender for Cloud structure: not a valid JSON object");
@@ -6885,11 +7058,11 @@ async function convertMsftDefenderCloudToHdf(input) {
6885
7058
  else groups.set(a.name, [a]);
6886
7059
  }
6887
7060
  const requirements = [];
6888
- for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments));
7061
+ for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments, scanTime));
6889
7062
  const subscriptionID = limitedAssessments.length > 0 ? extractSubscriptionID(limitedAssessments[0].id) : "";
6890
7063
  if (requirements.length === 0) {
6891
7064
  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()));
7065
+ requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, scanTime));
6893
7066
  }
6894
7067
  const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
6895
7068
  const components = [];
@@ -6910,7 +7083,7 @@ async function convertMsftDefenderCloudToHdf(input) {
6910
7083
  toolFormat: "JSON",
6911
7084
  baselines: [baseline],
6912
7085
  components,
6913
- timestamp: /* @__PURE__ */ new Date()
7086
+ timestamp: scanTime
6914
7087
  });
6915
7088
  }
6916
7089
  //#endregion
@@ -7007,13 +7180,26 @@ function buildTags$1(alert) {
7007
7180
  return tags;
7008
7181
  }
7009
7182
  /**
7183
+ * Resolves a clean per-finding source timestamp, falling back to the conversion time.
7184
+ */
7185
+ function resolveStartTime(alert, scanTime) {
7186
+ const first = alert.firstActivityDateTime ? parseTimestamp(alert.firstActivityDateTime) : null;
7187
+ if (first) return first;
7188
+ const created = alert.createdDateTime ? parseTimestamp(alert.createdDateTime) : null;
7189
+ if (created) return created;
7190
+ return scanTime;
7191
+ }
7192
+ /**
7010
7193
  * Converts a single MDE alert to an HDF EvaluatedRequirement.
7011
7194
  */
7012
- function alertToRequirement(alert) {
7195
+ function alertToRequirement(alert, scanTime) {
7013
7196
  const impact = severityToImpact$1(alert.severity);
7014
7197
  const status = statusToResultStatus(alert.status, alert.classification);
7015
7198
  const codeDesc = formatEvidence(alert.evidence ?? []);
7016
- const results = [createResult(status, formatMessage(alert), { codeDesc })];
7199
+ const results = [createResult(status, formatMessage(alert), {
7200
+ codeDesc,
7201
+ startTime: resolveStartTime(alert, scanTime)
7202
+ })];
7017
7203
  const descriptions = [{
7018
7204
  label: "default",
7019
7205
  data: alert.description
@@ -7043,10 +7229,11 @@ async function convertMsftDefenderEndpointToHdf(input) {
7043
7229
  const response = parseJSON(input);
7044
7230
  if (!response || typeof response !== "object") throw new Error("Invalid Microsoft Defender for Endpoint structure: not a valid JSON object");
7045
7231
  if (!Array.isArray(response.value)) throw new Error("Invalid Microsoft Defender for Endpoint structure: missing or invalid value array");
7232
+ const scanTime = /* @__PURE__ */ new Date();
7046
7233
  const { items: limitedAlerts, truncated } = limitArray(response.value);
7047
7234
  /* v8 ignore next -- truncation only triggers with >100K items */
7048
7235
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedAlerts.length} alert items (original: ${response.value.length})`);
7049
- const requirements = limitedAlerts.map(alertToRequirement);
7236
+ const requirements = limitedAlerts.map((alert) => alertToRequirement(alert, scanTime));
7050
7237
  const seenTargets = /* @__PURE__ */ new Set();
7051
7238
  const components = [];
7052
7239
  for (const alert of limitedAlerts) {
@@ -7056,14 +7243,14 @@ async function convertMsftDefenderEndpointToHdf(input) {
7056
7243
  components.push(target);
7057
7244
  }
7058
7245
  }
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()));
7246
+ 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
7247
  return buildHdfResults({
7061
7248
  generatorName: "msft-defender-endpoint-to-hdf",
7062
7249
  converterVersion: "1.0.0",
7063
7250
  toolName: "Microsoft Defender for Endpoint",
7064
7251
  baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
7065
7252
  components,
7066
- timestamp: /* @__PURE__ */ new Date()
7253
+ timestamp: scanTime
7067
7254
  });
7068
7255
  }
7069
7256
  //#endregion
@@ -7097,7 +7284,7 @@ function wrap(value) {
7097
7284
  return { "#text": value };
7098
7285
  }
7099
7286
  /** Map HDF impact (0.0-1.0) to XCCDF severity string. */
7100
- function impactToSeverity$1(impact) {
7287
+ function impactToSeverity$2(impact) {
7101
7288
  if (impact >= .7) return "high";
7102
7289
  if (impact >= .4) return "medium";
7103
7290
  if (impact >= .1) return "low";
@@ -7151,7 +7338,7 @@ function buildRuleObj(req) {
7151
7338
  const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
7152
7339
  const rule = {
7153
7340
  [`${ATTR}id`]: ruleId,
7154
- [`${ATTR}severity`]: impactToSeverity$1(req.impact),
7341
+ [`${ATTR}severity`]: impactToSeverity$2(req.impact),
7155
7342
  [`${ATTR}selected`]: "true",
7156
7343
  title: wrap(req.title ?? req.id)
7157
7344
  };
@@ -7341,7 +7528,7 @@ function nistTagToControlId(tag) {
7341
7528
  * Converts a 0.0-1.0 impact value to an OSCAL severity string.
7342
7529
  * This is the reverse of extractRiskSeverity.
7343
7530
  */
7344
- function impactToSeverity(impact) {
7531
+ function impactToSeverity$1(impact) {
7345
7532
  if (impact >= .9) return "critical";
7346
7533
  if (impact >= .7) return "high";
7347
7534
  if (impact >= .4) return "moderate";
@@ -7496,7 +7683,7 @@ function requirementToFindingSet(req, timestamp) {
7496
7683
  let risk;
7497
7684
  if (req.impact > 0) {
7498
7685
  const riskUUID = crypto.randomUUID();
7499
- const severity = impactToSeverity(req.impact);
7686
+ const severity = impactToSeverity$1(req.impact);
7500
7687
  risk = {
7501
7688
  uuid: riskUUID,
7502
7689
  title: `Risk for ${req.id}`,
@@ -8614,7 +8801,7 @@ function buildVulnerability(group) {
8614
8801
  v.flags.push({
8615
8802
  label: String(o.justification),
8616
8803
  product_ids: pids,
8617
- date: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z")
8804
+ date: formatTimestampSeconds(new Date(o.appliedAt))
8618
8805
  });
8619
8806
  }
8620
8807
  emitted = true;
@@ -8678,7 +8865,7 @@ function buildVulnerability(group) {
8678
8865
  return v;
8679
8866
  }
8680
8867
  function buildDocument(amendments, converterVersion) {
8681
- const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
8868
+ const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
8682
8869
  const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
8683
8870
  const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
8684
8871
  const docVersion = amendments.version || "1";
@@ -8758,7 +8945,7 @@ async function convertHdfToOpenVex(input, converterVersion) {
8758
8945
  "@id": await buildDocumentID(input, amendments),
8759
8946
  author,
8760
8947
  role,
8761
- timestamp: (earliest ?? /* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z"),
8948
+ timestamp: formatTimestampSeconds(earliest ?? /* @__PURE__ */ new Date()),
8762
8949
  version: 1,
8763
8950
  statements
8764
8951
  };
@@ -8775,7 +8962,7 @@ function overrideToStatement(o) {
8775
8962
  "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
8776
8963
  },
8777
8964
  status: String(canonical),
8778
- timestamp: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z"),
8965
+ timestamp: formatTimestampSeconds(new Date(o.appliedAt)),
8779
8966
  products: productsFor(o)
8780
8967
  };
8781
8968
  if (canonical === VexStatus.NotAffected) {
@@ -8942,7 +9129,7 @@ function allMilestonesCompleted(o) {
8942
9129
  }
8943
9130
  function buildMetadata(a, docTime, converterVersion) {
8944
9131
  const metadata = {
8945
- timestamp: docTime.toISOString().replace(/\.\d+Z$/, "Z"),
9132
+ timestamp: formatTimestampSeconds(docTime),
8946
9133
  tools: [{
8947
9134
  vendor: "mitre",
8948
9135
  name: "hdf-to-cyclonedx-vex",
@@ -9655,7 +9842,7 @@ async function convertOscalPoamToHdf(input) {
9655
9842
  for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
9656
9843
  const systemRef = poam["import-ssp"]?.href || void 0;
9657
9844
  const appliedBy = extractAppliedBy(poam.metadata);
9658
- const amendments = {
9845
+ return serializeHdf({
9659
9846
  name: toKebabCase(poam.metadata.title, "oscal-poam"),
9660
9847
  overrides,
9661
9848
  integrity,
@@ -9666,8 +9853,7 @@ async function convertOscalPoamToHdf(input) {
9666
9853
  name: "oscal-poam-to-hdf",
9667
9854
  version: "1.0.0"
9668
9855
  }
9669
- };
9670
- return JSON.stringify(amendments, null, 2);
9856
+ });
9671
9857
  }
9672
9858
  function buildRiskMap$1(risks) {
9673
9859
  const m = /* @__PURE__ */ new Map();
@@ -9740,8 +9926,8 @@ function poamItemAppliedBy(poam) {
9740
9926
  }
9741
9927
  function poamItemAppliedAt(poam) {
9742
9928
  if (poam.metadata["last-modified"]) {
9743
- const t = new Date(poam.metadata["last-modified"]);
9744
- if (!isNaN(t.getTime())) return t;
9929
+ const t = parseTimestamp(String(poam.metadata["last-modified"]));
9930
+ if (t) return t;
9745
9931
  }
9746
9932
  return /* @__PURE__ */ new Date();
9747
9933
  }
@@ -9799,18 +9985,23 @@ async function convertOscalSarToHdf(input) {
9799
9985
  if (!doc["assessment-results"]) throw new Error("oscal-assessment-results: input is not an assessment-results document (root key is not 'assessment-results')");
9800
9986
  const sar = doc["assessment-results"];
9801
9987
  const meta = extractMetadata(sar.metadata);
9988
+ const scanTime = /* @__PURE__ */ new Date();
9802
9989
  const baselines = [];
9803
9990
  for (const result of sar.results) {
9804
- const baseline = await resultToEvaluatedBaseline(result, sar, input);
9991
+ if (!result.findings || result.findings.length === 0) {
9992
+ console.warn(`WARNING: Skipping assessment result "${result.title || result.uuid}": no findings (empty result set)`);
9993
+ continue;
9994
+ }
9995
+ const baseline = await resultToEvaluatedBaseline(result, sar, input, scanTime);
9805
9996
  baselines.push(baseline);
9806
9997
  }
9807
9998
  const planRef = sar["import-ap"]?.href || void 0;
9808
9999
  let timestamp;
9809
10000
  if (meta.lastModified) {
9810
- const t = new Date(meta.lastModified);
9811
- if (!isNaN(t.getTime())) timestamp = t;
10001
+ const t = parseTimestamp(meta.lastModified);
10002
+ if (t) timestamp = t;
9812
10003
  }
9813
- return JSON.stringify({
10004
+ return serializeHdf({
9814
10005
  baselines,
9815
10006
  generator: {
9816
10007
  name: "oscal-assessment-results-to-hdf",
@@ -9822,9 +10013,9 @@ async function convertOscalSarToHdf(input) {
9822
10013
  },
9823
10014
  timestamp: timestamp ?? /* @__PURE__ */ new Date(),
9824
10015
  planRef
9825
- }, null, 2);
10016
+ });
9826
10017
  }
9827
- async function resultToEvaluatedBaseline(result, sar, rawInput) {
10018
+ async function resultToEvaluatedBaseline(result, sar, rawInput, scanTime) {
9828
10019
  const checksum = await inputChecksum(rawInput);
9829
10020
  const obsMap = buildObservationMap(result.observations ?? []);
9830
10021
  const riskMap = buildRiskMap(result.risks ?? []);
@@ -9841,7 +10032,7 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
9841
10032
  }
9842
10033
  const requirements = [];
9843
10034
  for (const controlId of controlOrder) {
9844
- const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result);
10035
+ const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result, scanTime);
9845
10036
  requirements.push(req);
9846
10037
  }
9847
10038
  const baseline = createMinimalBaseline(sarBaselineName(result, sar), requirements, {
@@ -9853,26 +10044,26 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
9853
10044
  if (result.description) baseline.description = result.description;
9854
10045
  return baseline;
9855
10046
  }
9856
- function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result) {
10047
+ function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result, scanTime) {
9857
10048
  const nistTag = controlIdToNistTag(controlId);
9858
10049
  const title = findings[0].title || nistTag;
9859
10050
  const impact = sarFindingsImpact(findings, riskMap);
9860
10051
  const descriptions = sarBuildDescriptions(findings, obsMap);
9861
10052
  const results = [];
9862
- for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result));
10053
+ for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result, scanTime));
9863
10054
  const req = createRequirement(nistTag, title, descriptions, impact, results, { tags: { nist: [nistTag] } });
9864
10055
  const controlType = deriveControlTypeFromTags([nistTag]);
9865
10056
  if (controlType !== void 0) req.controlType = controlType;
9866
10057
  return req;
9867
10058
  }
9868
- function findingToRequirementResult(f, obsMap, riskMap, result) {
10059
+ function findingToRequirementResult(f, obsMap, riskMap, result, scanTime) {
9869
10060
  const status = mapFindingStatus(f);
9870
10061
  const codeDesc = buildCodeDesc(f, obsMap);
9871
10062
  const message = buildRiskMessage(f, riskMap);
9872
10063
  const startTime = parseResultStartTime(result);
9873
10064
  return createResult(status, message || void 0, {
9874
10065
  codeDesc,
9875
- startTime: startTime.getTime() > 0 ? startTime : void 0
10066
+ startTime: startTime.getTime() > 0 ? startTime : scanTime
9876
10067
  });
9877
10068
  }
9878
10069
  function extractControlIdFromFinding(f) {
@@ -9967,8 +10158,8 @@ function buildRiskMap(risks) {
9967
10158
  }
9968
10159
  function parseResultStartTime(result) {
9969
10160
  if (result.start) {
9970
- const t = new Date(result.start);
9971
- if (!isNaN(t.getTime())) return t;
10161
+ const t = parseTimestamp(String(result.start));
10162
+ if (t) return t;
9972
10163
  }
9973
10164
  return /* @__PURE__ */ new Date(0);
9974
10165
  }