@mitre/hdf-converters 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { n as detectConverter, t as registerAllFingerprints } from "./register-all-ik8sNfNf.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, parseCsv, parseJSON, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML } from "@mitre/hdf-utilities";
4
- import { Applicability, AuthorizationStatus, BoundaryDescription, CategorizationLevel, ControlType, Copyright, HashAlgorithm, OverrideType, PlanType, ResultStatus, VerificationMethodEnum, VerificationMethodEnum as VerificationMethodEnum$1, 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";
3
+ import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML, trimUtcFraction } from "@mitre/hdf-utilities";
4
+ import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact } from "@mitre/hdf-schema";
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.
@@ -30,7 +30,7 @@ async function inputChecksum(input) {
30
30
  * Compute an Integrity object (for root-level document integrity) from raw input.
31
31
  *
32
32
  * Returns an Integrity with algorithm and checksum fields, suitable for
33
- * HdfBaseline.integrity, HdfSystem.integrity, HdfPlan.integrity, etc.
33
+ * HDFBaseline.integrity, HDFSystem.integrity, HDFPlan.integrity, etc.
34
34
  *
35
35
  * @param input - Raw input string (JSON, XML, etc.)
36
36
  * @returns Integrity object with SHA-256 algorithm and checksum
@@ -120,7 +120,7 @@ function mapCWEToNIST(cweIDs, fallback) {
120
120
  return controls.size > 0 ? [...controls].sort() : fallback;
121
121
  }
122
122
  /** Matches CWE identifiers like "CWE-79", "CWE 89", "cwe22". */
123
- const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
123
+ const CWE_PATTERN$1 = /CWE[- ]?(\d+)/gi;
124
124
  /**
125
125
  * Extract all numeric CWE IDs from text.
126
126
  * Returns deduplicated sorted array of numeric ID strings (e.g., ["79", "89"]).
@@ -129,7 +129,7 @@ const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
129
129
  * @returns Sorted, deduplicated numeric CWE ID strings
130
130
  */
131
131
  function extractCWEIDs(text) {
132
- const matches = [...text.matchAll(CWE_PATTERN)];
132
+ const matches = [...text.matchAll(CWE_PATTERN$1)];
133
133
  if (matches.length === 0) return [];
134
134
  const ids = [...new Set(matches.map((m) => m[1]))];
135
135
  ids.sort();
@@ -175,10 +175,59 @@ function ensureArray(value) {
175
175
  */
176
176
  const DEFAULT_REMEDIATION_NIST_TAGS$1 = ["SI-2", "RA-5"];
177
177
  /**
178
+ * Map a PURL `type` segment to the AffectedPackage `ecosystem` enum.
179
+ * Unknown types fall back to `generic`, which the schema enum permits
180
+ * as a catch-all.
181
+ */
182
+ const PURL_TYPE_TO_ECOSYSTEM = {
183
+ npm: Ecosystem.Npm,
184
+ pypi: Ecosystem.Pypi,
185
+ rpm: Ecosystem.RPM,
186
+ deb: Ecosystem.Deb,
187
+ maven: Ecosystem.Maven,
188
+ gem: Ecosystem.Gem,
189
+ nuget: Ecosystem.Nuget,
190
+ golang: Ecosystem.Go,
191
+ go: Ecosystem.Go,
192
+ cargo: Ecosystem.Cargo
193
+ };
194
+ /**
195
+ * Resolve an Ecosystem from a PURL type string. Returns `generic` for
196
+ * unknown types so callers can keep the schema's name+version+ecosystem
197
+ * triple valid without inventing a synthetic ecosystem.
198
+ */
199
+ function ecosystemFromPurlType(type) {
200
+ if (!type) return Ecosystem.Generic;
201
+ return PURL_TYPE_TO_ECOSYSTEM[type.toLowerCase()] ?? Ecosystem.Generic;
202
+ }
203
+ /**
204
+ * Build an Affected_Package primitive from any combination of the
205
+ * vocabulary the schema accepts (purl / cpe / name+version+ecosystem).
206
+ * Returns undefined when no identifier or full triple is present —
207
+ * callers should skip the entry rather than emit a schema-invalid
208
+ * AffectedPackage. Empty strings are treated as missing.
209
+ *
210
+ * The schema's anyOf requires at least one of:
211
+ * - name + version + ecosystem
212
+ * - purl alone
213
+ * - cpe alone
214
+ */
215
+ function buildAffectedPackage$1(opts) {
216
+ const pkg = {};
217
+ if (opts.purl) pkg.purl = opts.purl;
218
+ if (opts.cpe) pkg.cpe = opts.cpe;
219
+ if (opts.name) pkg.name = opts.name;
220
+ if (opts.version) pkg.version = opts.version;
221
+ if (opts.ecosystem) pkg.ecosystem = opts.ecosystem;
222
+ if (opts.fixedInVersion) pkg.fixedInVersion = opts.fixedInVersion;
223
+ if (!Boolean(pkg.name && pkg.version && pkg.ecosystem) && !pkg.purl && !pkg.cpe) return void 0;
224
+ return pkg;
225
+ }
226
+ /**
178
227
  * Build an HDF Results document from options.
179
228
  *
180
229
  * Eliminates the repeated boilerplate of constructing generator, tool,
181
- * and assembling the top-level HdfResults in every converter. Mirrors
230
+ * and assembling the top-level HDFResults in every converter. Mirrors
182
231
  * the Go shared.BuildHDFResults() function.
183
232
  *
184
233
  * @returns JSON string of the HDF Results document (pretty-printed)
@@ -200,7 +249,21 @@ function buildHdfResults(opts) {
200
249
  if (opts.components) hdf.components = opts.components;
201
250
  if (opts.timestamp) hdf.timestamp = opts.timestamp;
202
251
  if (opts.statistics) hdf.statistics = opts.statistics;
203
- 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);
204
267
  }
205
268
  /**
206
269
  * Captures a NIST 800-53 control identifier and its base sub-control number,
@@ -333,7 +396,24 @@ function deriveControlTypeFromTags(tags) {
333
396
  */
334
397
  function deriveVerificationMethod(code) {
335
398
  if (code === void 0 || code === null || code === "") return void 0;
336
- return VerificationMethodEnum$1.Automated;
399
+ return VerificationMethodEnum.Automated;
400
+ }
401
+ function buildNoFindingsRequirement(id, codeDesc, startTime) {
402
+ return {
403
+ id,
404
+ title: "No findings reported",
405
+ impact: 0,
406
+ descriptions: [{
407
+ label: "default",
408
+ data: codeDesc
409
+ }],
410
+ results: [{
411
+ status: ResultStatus.Passed,
412
+ codeDesc,
413
+ startTime
414
+ }],
415
+ tags: {}
416
+ };
337
417
  }
338
418
  //#endregion
339
419
  //#region converters/legacyhdf-to-hdf/typescript/converter.ts
@@ -347,14 +427,13 @@ function deriveVerificationMethod(code) {
347
427
  * - Results: snake_case → camelCase for all fields
348
428
  * - Overlay flattening: merge overlay/wrapper baselines so every requirement has results
349
429
  */
350
- /** Valid severity values per HDF v2.0 schema. */
351
- 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([
352
432
  "critical",
353
433
  "high",
354
434
  "medium",
355
435
  "low",
356
- "informational",
357
- "none"
436
+ "informational"
358
437
  ]);
359
438
  /**
360
439
  * Convert tags.severity string to a valid severity value.
@@ -366,21 +445,10 @@ function tagSeverityToSeverity(tagSeverity) {
366
445
  return VALID_SEVERITIES.has(normalized) ? normalized : null;
367
446
  }
368
447
  /**
369
- * Derive severity from numeric impact score.
370
- * Follows InSpec conventions: 0.9+ critical, 0.7+ high, 0.5+ medium, >0 low, 0 none.
371
- */
372
- function impactToSeverity$2(impact) {
373
- if (impact >= .9) return "critical";
374
- if (impact >= .7) return "high";
375
- if (impact >= .5) return "medium";
376
- if (impact > 0) return "low";
377
- return "none";
378
- }
379
- /**
380
448
  * Normalize status values from v1.0 to v2.0 format.
381
449
  * Converts snake_case to camelCase.
382
450
  */
383
- function normalizeStatus(status) {
451
+ function normalizeStatus$1(status) {
384
452
  return {
385
453
  "passed": "passed",
386
454
  "failed": "failed",
@@ -425,32 +493,22 @@ function computeEffectiveStatus(impact, results) {
425
493
  * Convert v1.0 result to v2.0 result.
426
494
  * Transforms snake_case field names to camelCase.
427
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
+ }
428
502
  function convertResult(v1Result) {
429
- const v2Result = { status: normalizeStatus(v1Result.status) };
430
- if (v1Result.code_desc !== void 0) v2Result.codeDesc = v1Result.code_desc;
503
+ const v2Result = { status: normalizeStatus$1(v1Result.status) };
504
+ v2Result.codeDesc = v1Result.code_desc ?? "";
431
505
  if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
432
- if (v1Result.start_time !== void 0) v2Result.startTime = v1Result.start_time;
506
+ v2Result.startTime = legacyStartTime(v1Result.start_time);
433
507
  if (v1Result.message !== void 0) v2Result.message = v1Result.message;
434
508
  if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
435
509
  if (v1Result.backtrace !== void 0) v2Result.backtrace = v1Result.backtrace;
436
- if (v1Result.resource_class !== void 0) v2Result.resourceClass = v1Result.resource_class;
437
- if (v1Result.resource_params !== void 0) v2Result.resourceParams = v1Result.resource_params;
510
+ if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
438
511
  if (v1Result.resource_id !== void 0) v2Result.resourceId = v1Result.resource_id;
439
- if (v1Result.skip_message !== void 0) v2Result.skipMessage = v1Result.skip_message;
440
- const knownFields = new Set([
441
- "status",
442
- "code_desc",
443
- "run_time",
444
- "start_time",
445
- "message",
446
- "exception",
447
- "backtrace",
448
- "resource_class",
449
- "resource_params",
450
- "resource_id",
451
- "skip_message"
452
- ]);
453
- for (const [key, value] of Object.entries(v1Result)) if (!knownFields.has(key) && !(key in v2Result)) v2Result[key] = value;
454
512
  return v2Result;
455
513
  }
456
514
  /**
@@ -463,37 +521,19 @@ function convertControl(v1Control) {
463
521
  impact: v1Control.impact
464
522
  };
465
523
  if (v1Control.title !== void 0) v2Req.title = v1Control.title;
466
- if (v1Control.desc !== void 0) v2Req.desc = v1Control.desc;
467
524
  if (v1Control.descriptions !== void 0) v2Req.descriptions = v1Control.descriptions;
468
- if (v1Control.refs !== void 0) v2Req.refs = v1Control.refs;
469
525
  if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
470
526
  if (v1Control.code !== void 0) v2Req.code = v1Control.code;
471
527
  if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
472
- if (v1Control.waiver_data !== void 0) v2Req.waiverData = v1Control.waiver_data;
473
- if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus(v1Control.status);
528
+ if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
474
529
  if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
475
530
  if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
476
- v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity$2(v1Control.impact);
531
+ v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity(v1Control.impact);
477
532
  const rawNist = v1Control.tags?.nist;
478
533
  if (Array.isArray(rawNist)) {
479
534
  const controlType = deriveControlTypeFromTags(rawNist.filter((v) => typeof v === "string"));
480
535
  if (controlType !== void 0) v2Req.controlType = controlType;
481
536
  }
482
- const knownFields = new Set([
483
- "id",
484
- "title",
485
- "desc",
486
- "descriptions",
487
- "impact",
488
- "refs",
489
- "tags",
490
- "code",
491
- "source_location",
492
- "waiver_data",
493
- "results",
494
- "status"
495
- ]);
496
- for (const [key, value] of Object.entries(v1Control)) if (!knownFields.has(key) && !(key in v2Req)) v2Req[key] = value;
497
537
  return v2Req;
498
538
  }
499
539
  /**
@@ -506,7 +546,7 @@ function convertGroup(v1Group) {
506
546
  requirements: v1Group.controls
507
547
  };
508
548
  if (v1Group.title !== void 0) v2Group.title = v1Group.title;
509
- const knownFields = new Set([
549
+ const knownFields = /* @__PURE__ */ new Set([
510
550
  "id",
511
551
  "title",
512
552
  "controls"
@@ -532,7 +572,7 @@ function convertDependency(v1Dep) {
532
572
  if (v1Dep.compliance !== void 0) v2Dep.compliance = v1Dep.compliance;
533
573
  if (v1Dep.status !== void 0) v2Dep.status = v1Dep.status;
534
574
  if (v1Dep.skip_message !== void 0) v2Dep.skipMessage = v1Dep.skip_message;
535
- const knownFields = new Set([
575
+ const knownFields = /* @__PURE__ */ new Set([
536
576
  "name",
537
577
  "url",
538
578
  "path",
@@ -561,9 +601,9 @@ function convertProfile(v1Profile) {
561
601
  if (v1Profile.summary !== void 0) v2Baseline.summary = v1Profile.summary;
562
602
  if (v1Profile.license !== void 0) v2Baseline.license = v1Profile.license;
563
603
  if (v1Profile.copyright !== void 0) v2Baseline.copyright = v1Profile.copyright;
564
- if (v1Profile.copyright_email !== void 0) v2Baseline.copyright_email = v1Profile.copyright_email;
565
- if (v1Profile.supports !== void 0) v2Baseline.supports = v1Profile.supports;
566
- if (v1Profile.attributes !== void 0) v2Baseline.inputs = v1Profile.attributes;
604
+ if (v1Profile.copyright_email !== void 0) v2Baseline.copyrightEmail = v1Profile.copyright_email;
605
+ if (v1Profile.supports?.length) v2Baseline.supports = v1Profile.supports;
606
+ if (v1Profile.attributes?.length) v2Baseline.inputs = v1Profile.attributes;
567
607
  if (v1Profile.status !== void 0) v2Baseline.status = v1Profile.status;
568
608
  if (v1Profile.sha256) v2Baseline.integrity = {
569
609
  algorithm: "sha256",
@@ -572,30 +612,9 @@ function convertProfile(v1Profile) {
572
612
  if (v1Profile.parent_profile !== void 0) v2Baseline.parentBaseline = v1Profile.parent_profile;
573
613
  if (v1Profile.status_message !== void 0) v2Baseline.statusMessage = v1Profile.status_message;
574
614
  if (v1Profile.skip_message !== void 0) v2Baseline.skipMessage = v1Profile.skip_message;
575
- if (v1Profile.groups && Array.isArray(v1Profile.groups)) v2Baseline.groups = v1Profile.groups.map(convertGroup);
615
+ if (v1Profile.groups?.length) v2Baseline.groups = v1Profile.groups.map(convertGroup);
576
616
  if (v1Profile.controls && Array.isArray(v1Profile.controls)) v2Baseline.requirements = v1Profile.controls.map(convertControl);
577
- if (v1Profile.depends && Array.isArray(v1Profile.depends)) v2Baseline.depends = v1Profile.depends.map(convertDependency);
578
- const knownFields = new Set([
579
- "name",
580
- "version",
581
- "title",
582
- "maintainer",
583
- "summary",
584
- "license",
585
- "copyright",
586
- "copyright_email",
587
- "supports",
588
- "attributes",
589
- "groups",
590
- "controls",
591
- "sha256",
592
- "depends",
593
- "parent_profile",
594
- "status",
595
- "status_message",
596
- "skip_message"
597
- ]);
598
- for (const [key, value] of Object.entries(v1Profile)) if (!knownFields.has(key) && !(key in v2Baseline)) v2Baseline[key] = value;
617
+ if (v1Profile.depends?.length) v2Baseline.depends = v1Profile.depends.map(convertDependency);
599
618
  return v2Baseline;
600
619
  }
601
620
  /**
@@ -628,17 +647,21 @@ function convertV1ToV2(v1Data) {
628
647
  baselines: (v1Data.profiles || []).map(convertProfile),
629
648
  statistics: v1Data.statistics || {}
630
649
  };
631
- if (v1Data.platform) v2.components = [{
632
- type: "host",
633
- id: v1Data.platform.target_id || v1Data.platform.name,
634
- name: v1Data.platform.name,
635
- ...v1Data.platform.release && { release: v1Data.platform.release },
636
- labels: {}
637
- }];
650
+ if (v1Data.platform) {
651
+ const component = {
652
+ type: "host",
653
+ name: v1Data.platform.name
654
+ };
655
+ if (v1Data.platform.target_id) {
656
+ component.osName = v1Data.platform.name;
657
+ if (v1Data.platform.release !== void 0) component.osVersion = v1Data.platform.release;
658
+ }
659
+ v2.components = [component];
660
+ }
638
661
  if (v1Data.generator) v2.generator = v1Data.generator;
639
662
  v2.tool = { name: "Heimdall Data Format v1" };
640
663
  if (v1Data.timestamp) v2.timestamp = v1Data.timestamp;
641
- const knownV1Fields = new Set([
664
+ const knownV1Fields = /* @__PURE__ */ new Set([
642
665
  "version",
643
666
  "platform",
644
667
  "profiles",
@@ -682,18 +705,19 @@ async function convertSarifToHdf(input) {
682
705
  const { items: limitedRuns, truncated: truncatedRuns } = limitArray(sarif.runs);
683
706
  /* v8 ignore next -- truncation only triggers with >100K items */
684
707
  if (truncatedRuns) console.warn(`WARNING: Input truncated at ${limitedRuns.length} run items (original: ${sarif.runs.length})`);
708
+ const timestamp = /* @__PURE__ */ new Date();
685
709
  return buildHdfResults({
686
710
  generatorName: "sarif-to-hdf",
687
711
  converterVersion: "1.0.0",
688
712
  toolName: firstDriver?.name,
689
713
  toolVersion: firstDriver?.version,
690
714
  toolFormat: "SARIF",
691
- baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum)),
715
+ baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
692
716
  components: [],
693
- timestamp: /* @__PURE__ */ new Date()
717
+ timestamp
694
718
  });
695
719
  }
696
- function convertRun(run, version, resultsChecksum) {
720
+ function convertRun(run, version, resultsChecksum, timestamp) {
697
721
  const ruleMap = buildRuleMap(run);
698
722
  const { items: limitedResults, truncated: truncatedResults } = limitArray(run.results);
699
723
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -717,7 +741,14 @@ function convertRun(run, version, resultsChecksum) {
717
741
  const group = groupMap.get(ruleId);
718
742
  return convertResultGroup(ruleId, group.rule, group.results);
719
743
  });
720
- return createMinimalBaseline(run.tool?.driver?.name || "SARIF", requirements, {
744
+ const baselineName = run.tool?.driver?.name || "SARIF";
745
+ if (requirements.length === 0) {
746
+ const driverName = run.tool?.driver?.name?.trim() || "";
747
+ const target = driverName || "SARIF analyzer";
748
+ const idPrefix = driverName || "sarif";
749
+ requirements.push(buildNoFindingsRequirement(`${idPrefix}-no-findings`, `${target} ran and reported zero findings.`, timestamp));
750
+ }
751
+ return createMinimalBaseline(baselineName, requirements, {
721
752
  version,
722
753
  title: "Static Analysis Results Interchange Format",
723
754
  resultsChecksum
@@ -747,8 +778,42 @@ function convertResultGroup(ruleId, rule, sarifResults) {
747
778
  const controlType = deriveControlTypeFromTags(nistControls);
748
779
  if (controlType !== void 0) req.controlType = controlType;
749
780
  req.verificationMethod = VerificationMethodEnum.Automated;
781
+ const seenKeys = /* @__PURE__ */ new Set();
782
+ const packages = [];
783
+ for (const sr of sarifResults) {
784
+ const pkg = packageFromSarifProperties(sr.properties);
785
+ if (!pkg) continue;
786
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
787
+ if (seenKeys.has(key)) continue;
788
+ seenKeys.add(key);
789
+ packages.push(pkg);
790
+ }
791
+ if (packages.length > 0) req.affectedPackages = packages;
750
792
  return req;
751
793
  }
794
+ /**
795
+ * Extract an Affected_Package from a SARIF result.properties bag.
796
+ * Recognizes the SCA-tool convention of carrying purl / cpe /
797
+ * packageName+packageVersion / name+version. Returns undefined for
798
+ * SAST results that lack any package identity.
799
+ */
800
+ function packageFromSarifProperties(props) {
801
+ if (!props) return void 0;
802
+ const name = props.packageName ?? props.name;
803
+ const version = props.packageVersion ?? props.version;
804
+ let ecosystem;
805
+ if (props.purl) ecosystem = ecosystemFromPurlType(parsePurl(props.purl)?.type);
806
+ else if (props.ecosystem) ecosystem = ecosystemFromPurlType(props.ecosystem);
807
+ else if (name && version) ecosystem = Ecosystem.Generic;
808
+ return buildAffectedPackage$1({
809
+ name,
810
+ version,
811
+ ecosystem,
812
+ purl: props.purl,
813
+ cpe: props.cpe,
814
+ fixedInVersion: props.fixedInVersion
815
+ });
816
+ }
752
817
  function resolveRuleLevel(rule, results) {
753
818
  if (rule?.defaultConfiguration?.level) return rule.defaultConfiguration.level;
754
819
  for (const r of results) if (!r.kind || r.kind === "fail") {
@@ -961,19 +1026,29 @@ async function convertJunitToHdf(input) {
961
1026
  if (!input || !input.trim()) throw new Error("Empty input");
962
1027
  validateInputSize(input, "junit");
963
1028
  const { suites, name } = parseJUnitXML(input);
1029
+ const scanTime = resolveScanTime(suites);
1030
+ const requirements = buildRequirements(suites, scanTime);
1031
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
964
1032
  return buildHdfResults({
965
- generatorName: "hdf-converters",
1033
+ generatorName: "junit-to-hdf",
966
1034
  converterVersion: CONVERTER_VERSION$2,
967
1035
  toolName: "JUnit XML",
968
1036
  toolFormat: "XML",
969
- baselines: [createMinimalBaseline(name, buildRequirements(suites), { resultsChecksum: await inputChecksum(input) })],
1037
+ baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
970
1038
  components: [{
971
- type: Copyright.Application,
1039
+ type: TargetType.Application,
972
1040
  name
973
1041
  }],
974
- timestamp: /* @__PURE__ */ new Date()
1042
+ timestamp: scanTime
975
1043
  });
976
1044
  }
1045
+ function resolveScanTime(suites) {
1046
+ for (const suite of suites) if (suite.timestamp) {
1047
+ const parsed = parseTimestamp(suite.timestamp);
1048
+ if (parsed) return parsed;
1049
+ }
1050
+ return /* @__PURE__ */ new Date();
1051
+ }
977
1052
  function parseJUnitXML(input) {
978
1053
  const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
979
1054
  if (parsed.testsuites) return {
@@ -990,7 +1065,7 @@ function parseJUnitXML(input) {
990
1065
  }
991
1066
  throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
992
1067
  }
993
- function buildRequirements(suites) {
1068
+ function buildRequirements(suites, scanTime) {
994
1069
  const { items: limitedSuites, truncated: truncatedSuites } = limitArray(suites);
995
1070
  /* v8 ignore next -- truncation only triggers with >100K items */
996
1071
  if (truncatedSuites) console.warn(`WARNING: Input truncated at ${limitedSuites.length} test suite items (original: ${suites.length})`);
@@ -1000,19 +1075,21 @@ function buildRequirements(suites) {
1000
1075
  const { items: limitedTestCases, truncated: truncatedTC } = limitArray(testcases);
1001
1076
  /* v8 ignore next -- truncation only triggers with >100K items */
1002
1077
  if (truncatedTC) console.warn(`WARNING: Input truncated at ${limitedTestCases.length} test case items (original: ${testcases.length})`);
1003
- for (const tc of limitedTestCases) requirements.push(testCaseToRequirement(tc, suite.timestamp));
1078
+ for (const tc of limitedTestCases) requirements.push(testCaseToRequirement(tc, scanTime));
1004
1079
  }
1005
1080
  return requirements;
1006
1081
  }
1007
- function testCaseToRequirement(tc, suiteTimestamp) {
1082
+ function testCaseToRequirement(tc, scanTime) {
1008
1083
  const id = buildID(tc);
1009
1084
  const { status, message } = resolveStatus(tc);
1010
- const resultOptions = { codeDesc: buildCodeDesc$9(tc) };
1085
+ const resultOptions = {
1086
+ codeDesc: buildCodeDesc$9(tc),
1087
+ startTime: scanTime
1088
+ };
1011
1089
  if (tc.time) {
1012
1090
  const parsed = parseFloat(tc.time);
1013
1091
  if (!isNaN(parsed)) resultOptions.runTime = parsed;
1014
1092
  }
1015
- if (suiteTimestamp) resultOptions.startTime = suiteTimestamp;
1016
1093
  const result = createResult(status, message ?? "", resultOptions);
1017
1094
  const descriptions = [{
1018
1095
  label: "default",
@@ -1067,6 +1144,11 @@ function buildCodeDesc$9(tc) {
1067
1144
  if (tc.classname) return `${tc.classname} :: ${tc.name}`;
1068
1145
  return tc.name;
1069
1146
  }
1147
+ function noFindingsTarget(baselineName, suites) {
1148
+ if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
1149
+ for (const s of suites) if (s.name) return s.name;
1150
+ return "JUnit test suite";
1151
+ }
1070
1152
  //#endregion
1071
1153
  //#region converters/xccdf-results-to-hdf/typescript/converter.ts
1072
1154
  const CONVERTER_VERSION$1 = "1.0.0";
@@ -1112,6 +1194,13 @@ const STATUS_MAP = {
1112
1194
  * @param input - Raw XML string (XCCDF Benchmark with TestResult, or ARF asset-report-collection)
1113
1195
  * @returns Stringified HDF Results JSON
1114
1196
  */
1197
+ function parseStartTime(raw) {
1198
+ if (raw) {
1199
+ const t = parseTimestamp(raw);
1200
+ if (t) return t;
1201
+ }
1202
+ return /* @__PURE__ */ new Date();
1203
+ }
1115
1204
  async function convertXccdfResultsToHdf(input) {
1116
1205
  if (!input || !input.trim()) throw new Error("Empty input");
1117
1206
  validateInputSize(input, "xccdf-results");
@@ -1130,21 +1219,26 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1130
1219
  const { items: limitedRuleResults, truncated: truncatedRR } = limitArray(ruleResults);
1131
1220
  /* v8 ignore next -- truncation only triggers with >100K items */
1132
1221
  if (truncatedRR) console.warn(`WARNING: Input truncated at ${limitedRuleResults.length} rule-result items (original: ${ruleResults.length})`);
1133
- const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
1222
+ const scanTime = parseStartTime(testResult["start-time"]);
1223
+ const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
1224
+ if (requirements.length === 0) {
1225
+ const target = xccdfTargetName(testResult, benchmark);
1226
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1227
+ }
1134
1228
  const resultsChecksum = await inputChecksum(rawInput);
1135
1229
  const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
1136
1230
  const components = buildTargets(testResult);
1137
- const timestamp = testResult["start-time"] ? new Date(testResult["start-time"]) : /* @__PURE__ */ new Date();
1231
+ const timestamp = scanTime;
1138
1232
  let durationSeconds;
1139
1233
  if (testResult["start-time"] && testResult["end-time"]) {
1140
- const start = new Date(testResult["start-time"]).getTime();
1141
- const end = new Date(testResult["end-time"]).getTime();
1142
- if (!isNaN(start) && !isNaN(end) && end >= start) durationSeconds = (end - start) / 1e3;
1234
+ const start = parseTimestamp(testResult["start-time"])?.getTime();
1235
+ const end = parseTimestamp(testResult["end-time"])?.getTime();
1236
+ if (start !== void 0 && end !== void 0 && end >= start) durationSeconds = (end - start) / 1e3;
1143
1237
  }
1144
1238
  const hdf = {
1145
1239
  baselines: [baseline],
1146
1240
  generator: {
1147
- name: "hdf-converters",
1241
+ name: "xccdf-results-to-hdf",
1148
1242
  version: CONVERTER_VERSION$1
1149
1243
  },
1150
1244
  tool: {
@@ -1155,7 +1249,7 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1155
1249
  timestamp
1156
1250
  };
1157
1251
  if (durationSeconds !== void 0) hdf.statistics = { duration: durationSeconds };
1158
- return JSON.stringify(hdf, null, 2);
1252
+ return serializeHdf(hdf);
1159
1253
  }
1160
1254
  async function convertArfCollection(arc, rawInput) {
1161
1255
  const resultsChecksum = await inputChecksum(rawInput);
@@ -1172,17 +1266,25 @@ async function convertArfCollection(arc, rawInput) {
1172
1266
  for (const report of arc.reports?.report ?? []) {
1173
1267
  const testResult = report.content?.TestResult;
1174
1268
  if (!testResult?.id) continue;
1175
- if (testResult["start-time"] && !firstTimestamp) firstTimestamp = new Date(testResult["start-time"]);
1269
+ if (testResult["start-time"] && !firstTimestamp) {
1270
+ const t = parseTimestamp(testResult["start-time"]);
1271
+ if (t) firstTimestamp = t;
1272
+ }
1176
1273
  if (testResult["start-time"] && testResult["end-time"]) {
1177
- const start = new Date(testResult["start-time"]).getTime();
1178
- const end = new Date(testResult["end-time"]).getTime();
1179
- if (!isNaN(start) && !isNaN(end) && end >= start) totalDuration += (end - start) / 1e3;
1274
+ const start = parseTimestamp(testResult["start-time"])?.getTime();
1275
+ const end = parseTimestamp(testResult["end-time"])?.getTime();
1276
+ if (start !== void 0 && end !== void 0 && end >= start) totalDuration += (end - start) / 1e3;
1180
1277
  }
1278
+ const scanTime = parseStartTime(testResult["start-time"]);
1181
1279
  const ruleResults = testResult["rule-result"] ?? [];
1182
1280
  const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
1183
1281
  /* v8 ignore next -- truncation only triggers with >100K items */
1184
1282
  if (truncatedARFRR) console.warn(`WARNING: Input truncated at ${limitedARFRuleResults.length} rule-result items (original: ${ruleResults.length})`);
1185
- const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
1283
+ const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
1284
+ if (requirements.length === 0) {
1285
+ const target = xccdfTargetName(testResult, benchmark);
1286
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
1287
+ }
1186
1288
  let baselineName = "";
1187
1289
  if (benchmark) baselineName = extractText(benchmark.title) || "";
1188
1290
  if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "ARF Report";
@@ -1203,7 +1305,7 @@ async function convertArfCollection(arc, rawInput) {
1203
1305
  const hdf = {
1204
1306
  baselines,
1205
1307
  generator: {
1206
- name: "hdf-converters",
1308
+ name: "xccdf-results-to-hdf",
1207
1309
  version: CONVERTER_VERSION$1
1208
1310
  },
1209
1311
  tool: {
@@ -1214,7 +1316,7 @@ async function convertArfCollection(arc, rawInput) {
1214
1316
  timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
1215
1317
  };
1216
1318
  if (totalDuration > 0) hdf.statistics = { duration: totalDuration };
1217
- return JSON.stringify(hdf, null, 2);
1319
+ return serializeHdf(hdf);
1218
1320
  }
1219
1321
  /**
1220
1322
  * Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
@@ -1270,7 +1372,7 @@ function buildRuleIndex(benchmark) {
1270
1372
  /**
1271
1373
  * Convert a single <rule-result> into an EvaluatedRequirement.
1272
1374
  */
1273
- function ruleResultToRequirement(rr, ruleIndex) {
1375
+ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
1274
1376
  const ruleId = rr.idref ?? "";
1275
1377
  const ruleDef = ruleIndex.get(ruleId);
1276
1378
  const id = extractVersion$1(ruleDef?.version) || ruleId;
@@ -1288,7 +1390,16 @@ function ruleResultToRequirement(rr, ruleIndex) {
1288
1390
  label: "fix",
1289
1391
  data: fixtext
1290
1392
  });
1291
- const result = createResult(STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed, "", { codeDesc: `XCCDF rule ${id}` });
1393
+ if (descriptions.length === 0) descriptions.push({
1394
+ label: "default",
1395
+ data: ""
1396
+ });
1397
+ const status = STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed;
1398
+ const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
1399
+ const result = createResult(status, "", {
1400
+ codeDesc: `XCCDF rule ${id}`,
1401
+ startTime: perRuleTime ?? scanTime
1402
+ });
1292
1403
  const tags = {};
1293
1404
  let nistTags = [];
1294
1405
  const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
@@ -1303,6 +1414,22 @@ function ruleResultToRequirement(rr, ruleIndex) {
1303
1414
  return req;
1304
1415
  }
1305
1416
  /**
1417
+ * Pick the most specific identifier available for a no-findings codeDesc.
1418
+ * Falls back through TestResult target/title, benchmark title/id, then a generic phrase.
1419
+ */
1420
+ function xccdfTargetName(testResult, benchmark) {
1421
+ const tr = testResult ?? {};
1422
+ const target = (tr.target ?? "").trim();
1423
+ if (target) return target;
1424
+ const trTitle = extractText(tr.title).trim();
1425
+ if (trTitle) return trTitle;
1426
+ const benchTitle = extractText(benchmark?.title).trim();
1427
+ if (benchTitle) return benchTitle;
1428
+ const benchId = (benchmark?.id ?? "").trim();
1429
+ if (benchId) return benchId;
1430
+ return "the target";
1431
+ }
1432
+ /**
1306
1433
  * Build Component array from TestResult metadata.
1307
1434
  */
1308
1435
  function buildTargets(testResult) {
@@ -1311,7 +1438,7 @@ function buildTargets(testResult) {
1311
1438
  const addresses = testResult["target-address"] ?? [];
1312
1439
  const target = {
1313
1440
  name: targetName,
1314
- type: Copyright.Host,
1441
+ type: TargetType.Host,
1315
1442
  labels: {}
1316
1443
  };
1317
1444
  if (addresses.length > 0) target.ipAddress = addresses[0];
@@ -1450,7 +1577,7 @@ const VULN_ATTR_ORDER = [
1450
1577
  "Class",
1451
1578
  "STIG_UUID"
1452
1579
  ];
1453
- const CORE_VULN_ATTR = new Set([
1580
+ const CORE_VULN_ATTR = /* @__PURE__ */ new Set([
1454
1581
  "Vuln_Num",
1455
1582
  "Severity",
1456
1583
  "Group_Title",
@@ -1486,7 +1613,9 @@ function parseCkl(input) {
1486
1613
  webDBSite: str(a["WEB_DB_SITE"]),
1487
1614
  webDBInstance: str(a["WEB_DB_INSTANCE"])
1488
1615
  },
1489
- stigs: istigs.map((is) => {
1616
+ stigs: istigs.map((is, i) => {
1617
+ const vulns = is.VULN ?? [];
1618
+ if (vulns.length === 0) throw new Error(`parse ckl: <iSTIG> block ${i + 1} contains no <VULN> rules`);
1490
1619
  const si = is.STIG_INFO?.SI_DATA ?? [];
1491
1620
  const siVal = (name) => str(si.find((d) => d.SID_NAME === name)?.SID_DATA);
1492
1621
  return {
@@ -1496,7 +1625,7 @@ function parseCkl(input) {
1496
1625
  releaseInfo: siVal("releaseinfo"),
1497
1626
  uuid: siVal("uuid"),
1498
1627
  classification: siVal("classification"),
1499
- vulns: (is.VULN ?? []).map(cklVulnToModel)
1628
+ vulns: vulns.map(cklVulnToModel)
1500
1629
  };
1501
1630
  })
1502
1631
  };
@@ -1506,7 +1635,7 @@ function cklVulnToModel(v) {
1506
1635
  const attr = (name) => str(data.find((sd) => sd.VULN_ATTRIBUTE === name)?.ATTRIBUTE_DATA);
1507
1636
  const ccis = [];
1508
1637
  const extra = {};
1509
- const promoted = new Set([
1638
+ const promoted = /* @__PURE__ */ new Set([
1510
1639
  "CCI_REF",
1511
1640
  "Vuln_Num",
1512
1641
  "Severity",
@@ -1655,16 +1784,20 @@ function parseCklb(input) {
1655
1784
  techArea: nz(t.technology_area),
1656
1785
  classification: nz(t.classification)
1657
1786
  };
1658
- const stigs = doc.stigs.map((s) => ({
1659
- stigID: nz(s.stig_id),
1660
- title: nz(s.stig_name) || nz(s.display_name),
1661
- displayName: nz(s.display_name),
1662
- version: nz(s.version),
1663
- releaseInfo: nz(s.release_info),
1664
- uuid: nz(s.uuid),
1665
- referenceIdentifier: nz(s.reference_identifier),
1666
- vulns: (s.rules ?? []).map(cklbRuleToModel)
1667
- }));
1787
+ const stigs = doc.stigs.map((s, i) => {
1788
+ const rules = s.rules ?? [];
1789
+ if (rules.length === 0) throw new Error(`parse cklb: stigs[${i}] contains no rules[]`);
1790
+ return {
1791
+ stigID: nz(s.stig_id),
1792
+ title: nz(s.stig_name) || nz(s.display_name),
1793
+ displayName: nz(s.display_name),
1794
+ version: nz(s.version),
1795
+ releaseInfo: nz(s.release_info),
1796
+ uuid: nz(s.uuid),
1797
+ referenceIdentifier: nz(s.reference_identifier),
1798
+ vulns: rules.map(cklbRuleToModel)
1799
+ };
1800
+ });
1668
1801
  return {
1669
1802
  format: "cklb",
1670
1803
  cklbVersion: nz(doc.cklb_version),
@@ -1766,18 +1899,19 @@ const CONVERTER_VERSION = "1.0.0";
1766
1899
  * applicability are omitted (the checklist format cannot substantiate them).
1767
1900
  * Original-format metadata is stashed in extensions/tags for round-trip.
1768
1901
  */
1769
- function checklistToHdf(cl, resultsChecksum) {
1902
+ function checklistToHdf(cl, resultsChecksum, generatorName) {
1903
+ const scanTime = /* @__PURE__ */ new Date();
1770
1904
  const hdf = {
1771
- baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
1905
+ baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum, scanTime)),
1772
1906
  generator: {
1773
- name: "hdf-converters",
1907
+ name: generatorName,
1774
1908
  version: CONVERTER_VERSION
1775
1909
  },
1776
1910
  tool: {
1777
1911
  name: "DISA STIG Viewer",
1778
1912
  format: cl.format === "cklb" ? "CKLB" : "CKL"
1779
1913
  },
1780
- timestamp: /* @__PURE__ */ new Date()
1914
+ timestamp: scanTime
1781
1915
  };
1782
1916
  const component = assetToComponent(cl.asset);
1783
1917
  if (component) hdf.components = [component];
@@ -1785,15 +1919,15 @@ function checklistToHdf(cl, resultsChecksum) {
1785
1919
  if (Object.keys(ext).length > 0) hdf.extensions = ext;
1786
1920
  return hdf;
1787
1921
  }
1788
- function stigToBaseline(s, resultsChecksum) {
1789
- const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map(vulnToRequirement), { resultsChecksum });
1922
+ function stigToBaseline(s, resultsChecksum, scanTime) {
1923
+ const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map((v) => vulnToRequirement(v, scanTime)), { resultsChecksum });
1790
1924
  if (s.title) baseline.title = s.title;
1791
1925
  if (s.version) baseline.version = s.version;
1792
1926
  const ext = baselineExtensions(s);
1793
1927
  if (Object.keys(ext).length > 0) baseline.extensions = ext;
1794
1928
  return baseline;
1795
1929
  }
1796
- function vulnToRequirement(v) {
1930
+ function vulnToRequirement(v, scanTime) {
1797
1931
  const severity = (v.severity ?? "").toLowerCase();
1798
1932
  const impact = severity ? severityToImpact(severity) : .5;
1799
1933
  const descriptions = [{
@@ -1809,7 +1943,10 @@ function vulnToRequirement(v) {
1809
1943
  data: stripHTML(v.fixText)
1810
1944
  });
1811
1945
  const message = [v.findingDetails, v.comments].map((s) => (s ?? "").trim()).filter(Boolean).join("\n\n");
1812
- const result = createResult(statusToHdf(v.status), message, { codeDesc: `STIG rule ${v.ruleVer ?? ""}` });
1946
+ const result = createResult(statusToHdf(v.status), message, {
1947
+ codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
1948
+ startTime: scanTime
1949
+ });
1813
1950
  const tags = {};
1814
1951
  let nistTags = [];
1815
1952
  if (v.ccis.length > 0) {
@@ -1835,7 +1972,7 @@ function assetToComponent(a) {
1835
1972
  if (!a.hostName && !a.hostIP && !a.hostFQDN) return void 0;
1836
1973
  const c = {
1837
1974
  name: a.hostName || a.hostFQDN || a.hostIP || "",
1838
- type: Copyright.Host
1975
+ type: TargetType.Host
1839
1976
  };
1840
1977
  if (a.hostIP) c.ipAddress = a.hostIP;
1841
1978
  if (a.hostFQDN) c.fqdn = a.hostFQDN;
@@ -2010,7 +2147,7 @@ async function convertCklToHdf(input) {
2010
2147
  validateInputSize(input, "ckl-to-hdf");
2011
2148
  const resultsChecksum = await inputChecksum(input);
2012
2149
  const checklist = parseCkl(input);
2013
- return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
2150
+ return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "ckl-to-hdf"), null, 2);
2014
2151
  }
2015
2152
  //#endregion
2016
2153
  //#region converters/cklb-to-hdf/typescript/converter.ts
@@ -2027,7 +2164,7 @@ async function convertCklbToHdf(input) {
2027
2164
  validateInputSize(input, "cklb-to-hdf");
2028
2165
  const resultsChecksum = await inputChecksum(input);
2029
2166
  const checklist = parseCklb(input);
2030
- return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
2167
+ return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "cklb-to-hdf"), null, 2);
2031
2168
  }
2032
2169
  //#endregion
2033
2170
  //#region converters/hdf-to-ckl/typescript/converter.ts
@@ -2078,7 +2215,28 @@ function formatDependencyPath(from) {
2078
2215
  /**
2079
2216
  * Builds a single EvaluatedRequirement from a group of vulnerabilities sharing an ID.
2080
2217
  */
2081
- function buildRequirement$16(vulnID, vulns) {
2218
+ /**
2219
+ * Map Snyk's `packageManager` value to an Affected_Package ecosystem.
2220
+ * Snyk reports values that don't always match PURL types one-to-one
2221
+ * (pip → pypi, rubygems → gem, yarn → npm). Unknown managers fall back
2222
+ * to `generic`.
2223
+ */
2224
+ function ecosystemFromSnykPackageManager(pm) {
2225
+ if (!pm) return Ecosystem.Generic;
2226
+ const lower = pm.toLowerCase();
2227
+ if (lower === "pip" || lower === "pip3") return Ecosystem.Pypi;
2228
+ if (lower === "rubygems" || lower === "bundler") return Ecosystem.Gem;
2229
+ if (lower === "yarn" || lower === "npm") return Ecosystem.Npm;
2230
+ return ecosystemFromPurlType(lower);
2231
+ }
2232
+ /** Synthesize a `pkg:<type>/<name>@<version>` PURL when the ecosystem
2233
+ * maps cleanly. Returns undefined for `generic` so we don't emit a
2234
+ * fake `pkg:generic/...` PURL that downstream tools can't dereference. */
2235
+ function synthesizePurl(ecosystem, name, version) {
2236
+ if (ecosystem === Ecosystem.Generic) return void 0;
2237
+ return `pkg:${ecosystem}/${name}@${version}`;
2238
+ }
2239
+ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2082
2240
  const rep = vulns[0];
2083
2241
  const cweIDs = rep.identifiers.CWE ?? [];
2084
2242
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -2093,17 +2251,33 @@ function buildRequirement$16(vulnID, vulns) {
2093
2251
  label: "default",
2094
2252
  data: rep.description
2095
2253
  }];
2096
- const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatDependencyPath(vuln.from) }));
2254
+ const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, {
2255
+ codeDesc: formatDependencyPath(vuln.from),
2256
+ startTime: scanTime
2257
+ }));
2097
2258
  const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
2098
2259
  const controlType = deriveControlTypeFromTags(nist);
2099
2260
  if (controlType !== void 0) req.controlType = controlType;
2100
2261
  req.verificationMethod = VerificationMethodEnum.Automated;
2262
+ const name = rep.packageName ?? rep.moduleName;
2263
+ const version = rep.version;
2264
+ if (name && version) {
2265
+ const ecosystem = ecosystemFromSnykPackageManager(packageManager);
2266
+ const pkg = buildAffectedPackage$1({
2267
+ name,
2268
+ version,
2269
+ ecosystem,
2270
+ purl: synthesizePurl(ecosystem, name, version),
2271
+ fixedInVersion: rep.fixedIn?.[0]
2272
+ });
2273
+ if (pkg) req.affectedPackages = [pkg];
2274
+ }
2101
2275
  return req;
2102
2276
  }
2103
2277
  /**
2104
2278
  * Converts a single Snyk project report to an HDF baseline.
2105
2279
  */
2106
- function convertSingleProject(report, resultsChecksum) {
2280
+ function convertSingleProject(report, resultsChecksum, scanTime) {
2107
2281
  const { items: limitedVulns, truncated: truncatedVulns } = limitArray(report.vulnerabilities);
2108
2282
  /* v8 ignore next -- truncation only triggers with >100K items */
2109
2283
  if (truncatedVulns) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${report.vulnerabilities.length})`);
@@ -2114,7 +2288,11 @@ function convertSingleProject(report, resultsChecksum) {
2114
2288
  else groups.set(vuln.id, [vuln]);
2115
2289
  }
2116
2290
  const requirements = [];
2117
- for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns));
2291
+ for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, scanTime, report.packageManager));
2292
+ if (requirements.length === 0) {
2293
+ const target = report.projectName ?? report.path ?? "project";
2294
+ requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, scanTime));
2295
+ }
2118
2296
  return createMinimalBaseline("Snyk Scan", requirements, {
2119
2297
  resultsChecksum,
2120
2298
  title: `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`,
@@ -2137,6 +2315,7 @@ async function convertSnykToHdf(input) {
2137
2315
  const detected = detectConverter(input);
2138
2316
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
2139
2317
  const resultsChecksum = await inputChecksum(input);
2318
+ const scanTime = /* @__PURE__ */ new Date();
2140
2319
  const parsed = parseJSON(input);
2141
2320
  if (!parsed || typeof parsed !== "object") throw new Error("snyk: invalid JSON");
2142
2321
  let baselines;
@@ -2145,10 +2324,10 @@ async function convertSnykToHdf(input) {
2145
2324
  const { items: limitedProjects, truncated: truncatedProjects } = limitArray(parsed);
2146
2325
  /* v8 ignore next -- truncation only triggers with >100K items */
2147
2326
  if (truncatedProjects) console.warn(`WARNING: Input truncated at ${limitedProjects.length} project items (original: ${parsed.length})`);
2148
- baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum));
2327
+ baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum, scanTime));
2149
2328
  targetName = limitedProjects[0]?.projectName ?? limitedProjects[0]?.path ?? "";
2150
2329
  } else {
2151
- baselines = [convertSingleProject(parsed, resultsChecksum)];
2330
+ baselines = [convertSingleProject(parsed, resultsChecksum, scanTime)];
2152
2331
  targetName = parsed.projectName ?? parsed.path ?? "";
2153
2332
  }
2154
2333
  return buildHdfResults({
@@ -2159,14 +2338,14 @@ async function convertSnykToHdf(input) {
2159
2338
  baselines,
2160
2339
  components: [{
2161
2340
  name: targetName,
2162
- type: Copyright.Application
2341
+ type: TargetType.Application
2163
2342
  }],
2164
- timestamp: /* @__PURE__ */ new Date()
2343
+ timestamp: scanTime
2165
2344
  });
2166
2345
  }
2167
2346
  //#endregion
2168
2347
  //#region converters/grype-to-hdf/typescript/converter.ts
2169
- const IMPACT_MAPPING$8 = new Map([
2348
+ const IMPACT_MAPPING$8 = /* @__PURE__ */ new Map([
2170
2349
  ["critical", .9],
2171
2350
  ["high", .7],
2172
2351
  ["medium", .5],
@@ -2227,6 +2406,92 @@ function buildCodeDesc$8(match) {
2227
2406
  }
2228
2407
  return parts.join(" | ");
2229
2408
  }
2409
+ function cvssVersionToSchema(v) {
2410
+ switch (v) {
2411
+ case "2.0": return Version.The20;
2412
+ case "3.0": return Version.The30;
2413
+ case "4.0": return Version.The40;
2414
+ default: return Version.The31;
2415
+ }
2416
+ }
2417
+ function cvssBandSeverity(score) {
2418
+ switch (cvssScoreToSeverity(score)) {
2419
+ case "critical": return CVSSSeverity.Critical;
2420
+ case "high": return CVSSSeverity.High;
2421
+ case "medium": return CVSSSeverity.Medium;
2422
+ case "low": return CVSSSeverity.Low;
2423
+ default: return CVSSSeverity.None;
2424
+ }
2425
+ }
2426
+ function buildCvssEntries$1(vuln) {
2427
+ if (!vuln.cvss || vuln.cvss.length === 0) return;
2428
+ const entries = [];
2429
+ for (const c of vuln.cvss) {
2430
+ const entry = { version: cvssVersionToSchema(c.version) };
2431
+ if (c.vector) entry.baseVector = c.vector;
2432
+ const score = c.metrics?.baseScore;
2433
+ if (typeof score === "number" && Number.isFinite(score)) {
2434
+ entry.baseScore = score;
2435
+ entry.baseSeverity = cvssBandSeverity(score);
2436
+ }
2437
+ if (vuln.id) entry.source = vuln.id;
2438
+ if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
2439
+ entries.push(entry);
2440
+ }
2441
+ return entries.length > 0 ? entries : void 0;
2442
+ }
2443
+ function mapGrypeTypeToEcosystem(grypeType) {
2444
+ switch ((grypeType ?? "").toLowerCase()) {
2445
+ case "rpm": return Ecosystem.RPM;
2446
+ case "deb": return Ecosystem.Deb;
2447
+ case "npm": return Ecosystem.Npm;
2448
+ case "python": return Ecosystem.Pypi;
2449
+ case "gem": return Ecosystem.Gem;
2450
+ case "go-module": return Ecosystem.Go;
2451
+ case "java-archive":
2452
+ case "jenkins-plugin": return Ecosystem.Maven;
2453
+ case "dotnet": return Ecosystem.Nuget;
2454
+ case "rust-crate": return Ecosystem.Cargo;
2455
+ default: return Ecosystem.Generic;
2456
+ }
2457
+ }
2458
+ function buildAffectedPackages(match) {
2459
+ const artifact = match.artifact;
2460
+ const pkg = {
2461
+ name: artifact.name,
2462
+ version: artifact.version,
2463
+ ecosystem: mapGrypeTypeToEcosystem(artifact.type)
2464
+ };
2465
+ if (artifact.cpes && artifact.cpes.length > 0 && artifact.cpes[0]) pkg.cpe = artifact.cpes[0];
2466
+ if (artifact.purl) pkg.purl = artifact.purl;
2467
+ const fix = match.vulnerability.fix;
2468
+ if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
2469
+ return [pkg];
2470
+ }
2471
+ const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
2472
+ function extractCwe(raw) {
2473
+ if (!raw || raw.length === 0) return void 0;
2474
+ const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
2475
+ return out.length === 0 ? void 0 : out;
2476
+ }
2477
+ function buildEpss$1(entries) {
2478
+ if (!entries || entries.length === 0) return void 0;
2479
+ const e = entries[0];
2480
+ if (!e.date) return void 0;
2481
+ return {
2482
+ score: e.epss ?? 0,
2483
+ percentile: e.percentile ?? 0,
2484
+ date: e.date
2485
+ };
2486
+ }
2487
+ function buildKev(k) {
2488
+ if (!k) return void 0;
2489
+ const out = { inKev: Boolean(k.inKev) };
2490
+ if (k.dateAdded) out.dateAdded = k.dateAdded;
2491
+ if (k.dueDate) out.dueDate = k.dueDate;
2492
+ if (k.notes) out.notes = k.notes;
2493
+ return out;
2494
+ }
2230
2495
  function convertMatchToRequirement(match, isIgnored) {
2231
2496
  const vuln = match.vulnerability;
2232
2497
  const cveId = vuln.id;
@@ -2277,6 +2542,15 @@ function convertMatchToRequirement(match, isIgnored) {
2277
2542
  };
2278
2543
  const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
2279
2544
  if (controlType !== void 0) requirement.controlType = controlType;
2545
+ const cvss = buildCvssEntries$1(vuln);
2546
+ if (cvss) requirement.cvss = cvss;
2547
+ requirement.affectedPackages = buildAffectedPackages(match);
2548
+ const cwe = extractCwe(vuln.cwe);
2549
+ if (cwe) requirement.cwe = cwe;
2550
+ const epss = buildEpss$1(vuln.epss);
2551
+ if (epss) requirement.epss = epss;
2552
+ const kev = buildKev(vuln.kev);
2553
+ if (kev) requirement.kev = kev;
2280
2554
  return requirement;
2281
2555
  }
2282
2556
  async function convertGrypeToHdf(input) {
@@ -2297,6 +2571,7 @@ async function convertGrypeToHdf(input) {
2297
2571
  for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
2298
2572
  }
2299
2573
  const targetName = grypeData.source?.target?.userInput || "Grype Scan";
2574
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
2300
2575
  const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
2301
2576
  return buildHdfResults({
2302
2577
  generatorName: grypeData.descriptor?.name || "grype",
@@ -2305,14 +2580,16 @@ async function convertGrypeToHdf(input) {
2305
2580
  toolVersion: grypeData.descriptor?.version,
2306
2581
  baselines: [baseline],
2307
2582
  components: [{
2308
- type: Copyright.Artifact,
2583
+ type: TargetType.Artifact,
2309
2584
  name: targetName
2310
2585
  }],
2311
- timestamp: grypeData.descriptor?.timestamp ? new Date(grypeData.descriptor.timestamp) : /* @__PURE__ */ new Date()
2586
+ timestamp: (grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null) ?? /* @__PURE__ */ new Date()
2312
2587
  });
2313
2588
  }
2314
2589
  //#endregion
2315
2590
  //#region converters/nessus-to-hdf/typescript/converter.ts
2591
+ const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
2592
+ const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
2316
2593
  const converterVersion = "1.0.0";
2317
2594
  const IMPACT_MAPPING$7 = {
2318
2595
  "4": .9,
@@ -2340,7 +2617,9 @@ async function convertNessusToHdf(nessusXml) {
2340
2617
  "preference",
2341
2618
  "tag",
2342
2619
  "ReportItem",
2343
- "ReportHost"
2620
+ "ReportHost",
2621
+ "cwe",
2622
+ "cve"
2344
2623
  ]);
2345
2624
  const policyName = parsed.NessusClientData_v2.Policy.policyName;
2346
2625
  const version = extractVersion(parsed);
@@ -2362,7 +2641,7 @@ async function convertNessusToHdf(nessusXml) {
2362
2641
  components,
2363
2642
  statistics: { duration },
2364
2643
  generator: {
2365
- name: "hdf-converters",
2644
+ name: "nessus-to-hdf",
2366
2645
  version: converterVersion
2367
2646
  },
2368
2647
  tool: { name: "Nessus" },
@@ -2387,8 +2666,8 @@ function calculateTiming(hosts) {
2387
2666
  const lastHost = hosts[hosts.length - 1];
2388
2667
  const startTimeStr = getHostPropertyValue(firstHost, "HOST_START");
2389
2668
  const endTimeStr = getHostPropertyValue(lastHost, "HOST_END") || getHostPropertyValue(lastHost, "HOST_START");
2390
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
2391
- const endTime = endTimeStr ? new Date(endTimeStr) : startTime;
2669
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2670
+ const endTime = endTimeStr ? parseTimestamp(endTimeStr) ?? startTime : startTime;
2392
2671
  return {
2393
2672
  startTime,
2394
2673
  endTime,
@@ -2409,6 +2688,12 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
2409
2688
  if (truncatedItems) console.warn(`WARNING: Input truncated at ${limitedItems.length} ReportItem items (original: ${items.length})`);
2410
2689
  requirements = limitedItems.map((item) => convertReportItemToRequirement(item, host));
2411
2690
  } else requirements = [];
2691
+ if (requirements.length === 0) {
2692
+ const target = host.name || getHostPropertyValue(host, "host-ip") || "host";
2693
+ const startTimeStr = getHostPropertyValue(host, "HOST_START");
2694
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2695
+ requirements = [buildNoFindingsRequirement("nessus-no-findings", `Nessus scanned ${target} and reported zero findings.`, startTime)];
2696
+ }
2412
2697
  return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
2413
2698
  title: `Nessus ${policyName}`,
2414
2699
  version,
@@ -2442,8 +2727,141 @@ function convertReportItemToRequirement(item, host) {
2442
2727
  };
2443
2728
  if (controlType !== void 0) req.controlType = controlType;
2444
2729
  if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
2730
+ if (!isCompliance) {
2731
+ const cvssEntries = buildCvssEntries(item);
2732
+ if (cvssEntries.length > 0) req.cvss = cvssEntries;
2733
+ const cweIDs = buildCweIDs(item);
2734
+ if (cweIDs.length > 0) req.cwe = cweIDs;
2735
+ const epss = buildEpss(item, host);
2736
+ if (epss !== void 0) req.epss = epss;
2737
+ }
2445
2738
  return req;
2446
2739
  }
2740
+ /**
2741
+ * Build a structured Cvss entry for a CVE finding. Returns an array because
2742
+ * the schema models cvss as a multi-entry array (Nessus emits one entry per
2743
+ * item; multi-vendor convergence may yield more).
2744
+ */
2745
+ function buildCvssEntries(item) {
2746
+ const source = (item.cvss_score_source || "").trim();
2747
+ if (!source || !CVE_SOURCE_RE.test(source)) return [];
2748
+ const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
2749
+ const hasV2 = !!(item.cvss_vector || item.cvss_base_score);
2750
+ if (!hasV3 && !hasV2) return [];
2751
+ let version;
2752
+ let baseVector;
2753
+ let baseScore;
2754
+ let threatVector;
2755
+ let threatScore;
2756
+ if (hasV3) {
2757
+ version = detectV3Version(item.cvss3_vector ?? "");
2758
+ baseVector = item.cvss3_vector || void 0;
2759
+ baseScore = parseFloatOrUndef(item.cvss3_base_score);
2760
+ threatVector = stripVersionPrefix(item.cvss3_temporal_vector);
2761
+ threatScore = item.cvss3_temporal_score ? parseFloatSafe(item.cvss3_temporal_score) : void 0;
2762
+ } else {
2763
+ version = Version.The20;
2764
+ baseVector = stripV2Prefix(item.cvss_vector ?? "") || void 0;
2765
+ baseScore = parseFloatOrUndef(item.cvss_base_score);
2766
+ threatVector = stripV2Prefix(item.cvss_temporal_vector ?? "") || void 0;
2767
+ threatScore = item.cvss_temporal_score ? parseFloatSafe(item.cvss_temporal_score) : void 0;
2768
+ }
2769
+ const entry = {
2770
+ version,
2771
+ source
2772
+ };
2773
+ if (baseVector) entry.baseVector = baseVector;
2774
+ if (baseScore !== void 0) {
2775
+ entry.baseScore = baseScore;
2776
+ const baseSeverity = mapCvssSeverity(baseScore);
2777
+ if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
2778
+ }
2779
+ if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
2780
+ if (threatScore !== void 0) {
2781
+ entry.threatScore = threatScore;
2782
+ entry.computedScore = threatScore;
2783
+ const computedSeverity = mapCvssSeverity(threatScore);
2784
+ if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
2785
+ }
2786
+ return [entry];
2787
+ }
2788
+ function detectV3Version(vector) {
2789
+ if (vector.startsWith("CVSS:3.1/")) return Version.The31;
2790
+ if (vector.startsWith("CVSS:3.0/")) return Version.The30;
2791
+ return Version.The30;
2792
+ }
2793
+ function stripVersionPrefix(vector) {
2794
+ if (!vector) return void 0;
2795
+ for (const prefix of [
2796
+ "CVSS:3.0/",
2797
+ "CVSS:3.1/",
2798
+ "CVSS:4.0/"
2799
+ ]) if (vector.startsWith(prefix)) return vector.slice(prefix.length);
2800
+ return vector;
2801
+ }
2802
+ function stripV2Prefix(vector) {
2803
+ return vector.startsWith("CVSS2#") ? vector.slice(6) : vector;
2804
+ }
2805
+ function parseFloatSafe(s) {
2806
+ if (!s) return 0;
2807
+ const f = Number.parseFloat(s);
2808
+ return Number.isFinite(f) ? f : 0;
2809
+ }
2810
+ function parseFloatOrUndef(s) {
2811
+ if (s === void 0 || s === "") return void 0;
2812
+ const f = Number.parseFloat(s);
2813
+ return Number.isFinite(f) ? f : void 0;
2814
+ }
2815
+ function mapCvssSeverity(score) {
2816
+ switch (cvssScoreToSeverity(score)) {
2817
+ case "critical": return CVSSSeverity.Critical;
2818
+ case "high": return CVSSSeverity.High;
2819
+ case "medium": return CVSSSeverity.Medium;
2820
+ case "low": return CVSSSeverity.Low;
2821
+ case "none": return CVSSSeverity.None;
2822
+ default: return;
2823
+ }
2824
+ }
2825
+ /**
2826
+ * Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
2827
+ * numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
2828
+ * forms appear. Output is "CWE-N" form per schema convention.
2829
+ */
2830
+ function buildCweIDs(item) {
2831
+ if (!item.cwe) return [];
2832
+ const raws = Array.isArray(item.cwe) ? item.cwe : [item.cwe];
2833
+ const seen = /* @__PURE__ */ new Set();
2834
+ for (const raw of raws) {
2835
+ const text = String(raw);
2836
+ for (const m of text.matchAll(CWE_PATTERN)) if (m[1]) seen.add(m[1]);
2837
+ for (const tok of text.split(/[^0-9]+/)) if (tok !== "") seen.add(tok);
2838
+ }
2839
+ if (seen.size === 0) return [];
2840
+ return [...seen].sort().map((id) => `CWE-${id}`);
2841
+ }
2842
+ /**
2843
+ * Build a structured Epss entry when the ReportItem includes EPSS data.
2844
+ * The date is derived from the host's HOST_START in YYYY-MM-DD form.
2845
+ */
2846
+ function buildEpss(item, host) {
2847
+ const hasScore = item.epss_score !== void 0 && item.epss_score !== "";
2848
+ const hasPct = item.epss_percentile !== void 0 && item.epss_percentile !== "";
2849
+ if (!hasScore && !hasPct) return void 0;
2850
+ const date = epssDate(host);
2851
+ if (date === void 0) return void 0;
2852
+ return {
2853
+ date,
2854
+ score: hasScore ? parseFloatSafe(item.epss_score) : 0,
2855
+ percentile: hasPct ? parseFloatSafe(item.epss_percentile) : 0
2856
+ };
2857
+ }
2858
+ function epssDate(host) {
2859
+ const hs = getHostPropertyValue(host, "HOST_START");
2860
+ if (hs) {
2861
+ const d = parseTimestamp(hs);
2862
+ if (d) return d.toISOString().slice(0, 10);
2863
+ }
2864
+ }
2447
2865
  function buildDescriptions(item, isCompliance) {
2448
2866
  const descriptions = [];
2449
2867
  if (isCompliance && item["compliance-info"]) descriptions.push({
@@ -2504,7 +2922,7 @@ function buildTags$2(item, isCompliance) {
2504
2922
  }
2505
2923
  function buildRefs(item) {
2506
2924
  const refs = [];
2507
- if (item.see_also) refs.push({ url: item.see_also });
2925
+ if (item.see_also) for (const url of item.see_also.split(/\s+/).filter(Boolean)) refs.push({ url });
2508
2926
  return refs.length > 0 ? refs : void 0;
2509
2927
  }
2510
2928
  function buildResult$1(item, host, isCompliance) {
@@ -2512,7 +2930,7 @@ function buildResult$1(item, host, isCompliance) {
2512
2930
  const codeDesc = getCodeDesc(item);
2513
2931
  const message = item.plugin_output || item["compliance-actual-value"];
2514
2932
  const startTimeStr = getHostPropertyValue(host, "HOST_START");
2515
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
2933
+ const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
2516
2934
  return {
2517
2935
  status,
2518
2936
  codeDesc,
@@ -2546,7 +2964,7 @@ function convertReportHostToTarget(host) {
2546
2964
  });
2547
2965
  const target = {
2548
2966
  name: hostName,
2549
- type: Copyright.Host
2967
+ type: TargetType.Host
2550
2968
  };
2551
2969
  if (isFQDN(hostName)) target.fqdn = hostName;
2552
2970
  const hostIp = hostProps["host-ip"];
@@ -2614,18 +3032,40 @@ async function convertSonarqubeToHdf(input) {
2614
3032
  const baseline = convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum);
2615
3033
  baselines.push(baseline);
2616
3034
  }
3035
+ let components = Array.from(issuesByProject.keys()).map((projectKey) => ({
3036
+ type: TargetType.Application,
3037
+ name: projectKey
3038
+ }));
3039
+ if (baselines.length === 0) {
3040
+ const targetName = deriveEmptyScanTarget(sonarData.components);
3041
+ baselines.push({
3042
+ name: targetName,
3043
+ title: `SonarQube Analysis for ${targetName}`,
3044
+ requirements: [buildNoFindingsRequirement("sonarqube-no-findings", `SonarQube scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())],
3045
+ resultsChecksum
3046
+ });
3047
+ components = [{
3048
+ type: TargetType.Application,
3049
+ name: targetName
3050
+ }];
3051
+ }
2617
3052
  return buildHdfResults({
2618
3053
  generatorName: "sonarqube-to-hdf",
2619
3054
  converterVersion: "1.0.0",
2620
3055
  toolName: "SonarQube",
2621
3056
  baselines,
2622
- components: Array.from(issuesByProject.keys()).map((projectKey) => ({
2623
- type: Copyright.Application,
2624
- name: projectKey
2625
- })),
3057
+ components,
2626
3058
  timestamp: /* @__PURE__ */ new Date()
2627
3059
  });
2628
3060
  }
3061
+ function deriveEmptyScanTarget(components) {
3062
+ if (components) {
3063
+ const project = components.find((c) => c.qualifier === "TRK");
3064
+ if (project) return project.key;
3065
+ if (components.length > 0) return components[0].key;
3066
+ }
3067
+ return "the SonarQube project";
3068
+ }
2629
3069
  function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum) {
2630
3070
  const issuesByRule = /* @__PURE__ */ new Map();
2631
3071
  for (const issue of issues) {
@@ -2725,7 +3165,7 @@ function createResultFromIssue(issue, componentMap) {
2725
3165
  const codeDesc = `${component?.path || component?.longName || issue.component}${issue.line ? ` LINE : ${issue.line}` : ""}`;
2726
3166
  return createResult(status, issue.message, {
2727
3167
  codeDesc,
2728
- startTime: new Date(issue.creationDate)
3168
+ startTime: parseTimestamp(issue.creationDate) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
2729
3169
  });
2730
3170
  }
2731
3171
  function extractSourceLocation(issue, componentMap) {
@@ -2762,6 +3202,28 @@ function buildNistTags$3(sourceIdentifier, ruleName) {
2762
3202
  if (byName) return [byName];
2763
3203
  return [];
2764
3204
  }
3205
+ /**
3206
+ * Flag rules whose NIST mappings exist at a revision other than the one
3207
+ * currently selected — they emit no NIST tags here even though a mapping
3208
+ * exists elsewhere, a likely sign of a wrong revision selection. Rules
3209
+ * unmapped at every revision are not flagged. Throws in strict mode; otherwise
3210
+ * logs a single aggregated warning.
3211
+ */
3212
+ function checkRevisionAlignment(rules) {
3213
+ const rev = getCurrentNistRevision();
3214
+ const seen = /* @__PURE__ */ new Set();
3215
+ const lines = [];
3216
+ for (const rule of rules) {
3217
+ const covered = awsConfigMappedRevisions(rule.Source.SourceIdentifier, rule.ConfigRuleName);
3218
+ if (covered.length === 0 || covered.includes(rev) || seen.has(rule.ConfigRuleName)) continue;
3219
+ seen.add(rule.ConfigRuleName);
3220
+ lines.push(` - ${rule.ConfigRuleName} (mapped at Rev ${covered.join(", ")})`);
3221
+ }
3222
+ if (lines.length === 0) return;
3223
+ const detail = `${lines.length} AWS Config rule(s) have NIST 800-53 mappings at a different revision than the requested Rev ${rev}; their NIST tags were omitted:\n${lines.join("\n")}`;
3224
+ if (isNistStrict()) throw new Error(`aws-config: ${detail}\nre-run with a matching revision, or disable strict mode to convert with the gaps`);
3225
+ console.warn(`WARNING: ${detail}`);
3226
+ }
2765
3227
  function buildCheckText(rule) {
2766
3228
  const parts = [`ARN: ${rule.ConfigRuleArn || "N/A"}`, `Source Identifier: ${rule.Source.SourceIdentifier || "N/A"}`];
2767
3229
  if (rule.InputParameters && rule.InputParameters !== "{}") {
@@ -2782,12 +3244,26 @@ function buildResult(r) {
2782
3244
  const status = mapComplianceStatus(r.ComplianceType);
2783
3245
  const codeDesc = buildCodeDesc$7(q);
2784
3246
  const message = buildResultMessage(codeDesc, r.Annotation, status);
2785
- const startTime = r.ConfigRuleInvokedTime ? new Date(r.ConfigRuleInvokedTime) : void 0;
3247
+ const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
2786
3248
  return createResult(status, message ?? codeDesc, {
2787
3249
  codeDesc,
2788
3250
  ...startTime ? { startTime } : {}
2789
3251
  });
2790
3252
  }
3253
+ /**
3254
+ * Synthesizes a single HDF result for a Config rule whose live evaluation
3255
+ * returned zero in-scope resources. The HDF schema requires `results` to have
3256
+ * minItems >= 1; this honestly signals to auditors that the rule's check ran
3257
+ * but had no scope in this account/region rather than vacuously claiming
3258
+ * "passed". See issue #80 bug 2.
3259
+ */
3260
+ function buildNotApplicableResult(rule) {
3261
+ const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
3262
+ return createResult(ResultStatus.NotApplicable, codeDesc, {
3263
+ codeDesc,
3264
+ startTime: /* @__PURE__ */ new Date()
3265
+ });
3266
+ }
2791
3267
  function buildRequirement$15(rule) {
2792
3268
  const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
2793
3269
  const tags = nist.length > 0 ? { nist } : {};
@@ -2799,7 +3275,7 @@ function buildRequirement$15(rule) {
2799
3275
  data: buildCheckText(rule)
2800
3276
  }];
2801
3277
  const title = `${getAccountId(rule.ConfigRuleArn)} - ${rule.ConfigRuleName}`;
2802
- const results = rule.EvaluationResults.map(buildResult);
3278
+ const results = rule.EvaluationResults.length > 0 ? rule.EvaluationResults.map(buildResult) : [buildNotApplicableResult(rule)];
2803
3279
  const req = createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
2804
3280
  tags,
2805
3281
  sourceLocation: {
@@ -2828,6 +3304,7 @@ async function convertAwsConfigToHdf(input) {
2828
3304
  const { items: limitedRules, truncated: truncatedRules } = limitArray(data.ConfigRules);
2829
3305
  /* v8 ignore next -- truncation only triggers with >100K items */
2830
3306
  if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
3307
+ checkRevisionAlignment(limitedRules);
2831
3308
  const baseline = {
2832
3309
  ...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$15), { resultsChecksum }),
2833
3310
  title: "AWS Config Compliance Results",
@@ -2843,7 +3320,7 @@ async function convertAwsConfigToHdf(input) {
2843
3320
  toolName: "AWS Config",
2844
3321
  baselines: [baseline],
2845
3322
  components: [{
2846
- type: Copyright.CloudAccount,
3323
+ type: TargetType.CloudAccount,
2847
3324
  name: `AWS Account ${accountId}`,
2848
3325
  labels: {
2849
3326
  account: accountId,
@@ -2877,17 +3354,20 @@ function getImpact$8(severity) {
2877
3354
  /**
2878
3355
  * Converts a single CheckovCheck to an HDF RequirementResult.
2879
3356
  */
2880
- function checkToResult(check) {
3357
+ function checkToResult(check, scanTime) {
2881
3358
  const status = mapStatus$2(check.check_result.result);
2882
3359
  const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
2883
3360
  let message;
2884
3361
  if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
2885
- return createResult(status, message ?? "", { codeDesc });
3362
+ return createResult(status, message ?? "", {
3363
+ codeDesc,
3364
+ startTime: scanTime
3365
+ });
2886
3366
  }
2887
3367
  /**
2888
3368
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
2889
3369
  */
2890
- function buildRequirement$14(checkId, checks) {
3370
+ function buildRequirement$14(checkId, checks, scanTime) {
2891
3371
  const rep = checks[0];
2892
3372
  const impact = getImpact$8(rep.severity);
2893
3373
  const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
@@ -2899,7 +3379,7 @@ function buildRequirement$14(checkId, checks) {
2899
3379
  label: "check",
2900
3380
  data: rep.guideline
2901
3381
  });
2902
- const results = checks.map(checkToResult);
3382
+ const results = checks.map((check) => checkToResult(check, scanTime));
2903
3383
  const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
2904
3384
  req.verificationMethod = VerificationMethodEnum.Automated;
2905
3385
  const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
@@ -2930,6 +3410,7 @@ async function convertCheckovToHdf(input) {
2930
3410
  const detected = detectConverter(input);
2931
3411
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
2932
3412
  const resultsChecksum = await inputChecksum(input);
3413
+ const scanTime = /* @__PURE__ */ new Date();
2933
3414
  const reports = parseInput(input);
2934
3415
  const allChecks = [];
2935
3416
  const checkTypes = [];
@@ -2951,9 +3432,13 @@ async function convertCheckovToHdf(input) {
2951
3432
  else groups.set(check.check_id, [check]);
2952
3433
  }
2953
3434
  const requirements = [];
2954
- for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
2955
- const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
3435
+ for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks, scanTime));
2956
3436
  const format = checkTypes.join(", ");
3437
+ if (requirements.length === 0) {
3438
+ const target = format || "input";
3439
+ requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, scanTime));
3440
+ }
3441
+ const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
2957
3442
  return buildHdfResults({
2958
3443
  generatorName: "checkov-to-hdf",
2959
3444
  converterVersion: "1.0.0",
@@ -2961,7 +3446,7 @@ async function convertCheckovToHdf(input) {
2961
3446
  toolVersion: version,
2962
3447
  toolFormat: format,
2963
3448
  baselines: [baseline],
2964
- timestamp: /* @__PURE__ */ new Date()
3449
+ timestamp: scanTime
2965
3450
  });
2966
3451
  }
2967
3452
  //#endregion
@@ -2992,19 +3477,22 @@ function formatSkipMessage(suppressions) {
2992
3477
  /**
2993
3478
  * Converts a single GosecIssue to an HDF RequirementResult.
2994
3479
  */
2995
- function issueToResult(issue) {
3480
+ function issueToResult(issue, scanTime) {
2996
3481
  const suppressed = isSuppressed(issue);
2997
3482
  const status = suppressed ? ResultStatus.NotReviewed : ResultStatus.Failed;
2998
3483
  let message;
2999
3484
  if (suppressed) message = formatSkipMessage(issue.suppressions) ?? "No justification provided";
3000
3485
  else message = `${issue.confidence} confidence of rule violation at:\n${issue.code}`;
3001
3486
  const codeDesc = `Rule ${issue.rule_id} violation detected at:\nFile: ${issue.file}\nLine: ${issue.line}\nColumn: ${issue.column}`;
3002
- return createResult(status, message, { codeDesc });
3487
+ return createResult(status, message, {
3488
+ codeDesc,
3489
+ startTime: scanTime
3490
+ });
3003
3491
  }
3004
3492
  /**
3005
3493
  * Converts a group of issues sharing a rule_id into one EvaluatedRequirement.
3006
3494
  */
3007
- function buildRequirement$13(ruleId, issues) {
3495
+ function buildRequirement$13(ruleId, issues, scanTime) {
3008
3496
  const rep = issues[0];
3009
3497
  const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
3010
3498
  const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
@@ -3015,7 +3503,7 @@ function buildRequirement$13(ruleId, issues) {
3015
3503
  url: rep.cwe.url
3016
3504
  }
3017
3505
  };
3018
- const results = issues.map(issueToResult);
3506
+ const results = issues.map((issue) => issueToResult(issue, scanTime));
3019
3507
  const descriptions = [{
3020
3508
  label: "default",
3021
3509
  data: rep.details
@@ -3055,8 +3543,10 @@ async function convertGosecToHdf(input) {
3055
3543
  if (existing) existing.push(issue);
3056
3544
  else groups.set(issue.rule_id, [issue]);
3057
3545
  }
3546
+ const scanTime = /* @__PURE__ */ new Date();
3058
3547
  const requirements = [];
3059
- for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues));
3548
+ for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues, scanTime));
3549
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", scanTime));
3060
3550
  const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
3061
3551
  return buildHdfResults({
3062
3552
  generatorName: "gosec-to-hdf",
@@ -3064,7 +3554,7 @@ async function convertGosecToHdf(input) {
3064
3554
  toolName: "gosec",
3065
3555
  toolVersion: report.GosecVersion || void 0,
3066
3556
  baselines: [baseline],
3067
- timestamp: /* @__PURE__ */ new Date()
3557
+ timestamp: scanTime
3068
3558
  });
3069
3559
  }
3070
3560
  //#endregion
@@ -3137,6 +3627,7 @@ async function convertNiktoToHdf(input) {
3137
3627
  if (niktoData.host) targetParts.push(`Host: ${niktoData.host}`);
3138
3628
  if (niktoData.port) targetParts.push(`Port: ${niktoData.port}`);
3139
3629
  const targetName = targetParts.length > 0 ? targetParts.join(" ") : "Nikto Scan";
3630
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
3140
3631
  return buildHdfResults({
3141
3632
  generatorName: "nikto-to-hdf",
3142
3633
  converterVersion: "unknown",
@@ -3148,7 +3639,7 @@ async function convertNiktoToHdf(input) {
3148
3639
  summary: niktoData.banner || ""
3149
3640
  })],
3150
3641
  components: [{
3151
- type: Copyright.Application,
3642
+ type: TargetType.Application,
3152
3643
  name: targetName
3153
3644
  }]
3154
3645
  });
@@ -3203,6 +3694,15 @@ function selectSite(sites) {
3203
3694
  }
3204
3695
  return best;
3205
3696
  }
3697
+ const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
3698
+ function parseZapTimestamp(s) {
3699
+ const trimmed = s.trim();
3700
+ if (ZAP_RFC1123_LIKE.test(trimmed)) {
3701
+ const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
3702
+ if (!isNaN(d.getTime())) return d;
3703
+ }
3704
+ return parseTimestamp(s) ?? void 0;
3705
+ }
3206
3706
  async function convertZapToHdf(input) {
3207
3707
  validateInputSize(input, "zap");
3208
3708
  registerAllFingerprints();
@@ -3210,45 +3710,8 @@ async function convertZapToHdf(input) {
3210
3710
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3211
3711
  const resultsChecksum = await inputChecksum(input);
3212
3712
  const zapData = parseJSON(input);
3213
- if (!zapData.site || !Array.isArray(zapData.site)) {
3214
- const hdf = {
3215
- baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
3216
- resultsChecksum,
3217
- title: "OWASP ZAP Scan",
3218
- summary: ""
3219
- })],
3220
- generator: {
3221
- name: "zap-to-hdf",
3222
- version: "unknown"
3223
- },
3224
- tool: {
3225
- name: "OWASP ZAP",
3226
- format: "JSON"
3227
- }
3228
- };
3229
- return JSON.stringify(hdf, null, 2);
3230
- }
3231
- const site = selectSite(zapData.site);
3232
- if (!site) {
3233
- const hdf = {
3234
- baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
3235
- resultsChecksum,
3236
- title: "OWASP ZAP Scan",
3237
- summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
3238
- })],
3239
- generator: {
3240
- name: "zap-to-hdf",
3241
- version: "unknown"
3242
- },
3243
- tool: {
3244
- name: "OWASP ZAP",
3245
- format: "JSON"
3246
- }
3247
- };
3248
- if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
3249
- return JSON.stringify(hdf, null, 2);
3250
- }
3251
- const alerts = site.alerts ?? [];
3713
+ const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
3714
+ const alerts = site?.alerts ?? [];
3252
3715
  const pluginIdCount = /* @__PURE__ */ new Map();
3253
3716
  const { items: limitedAlerts, truncated } = limitArray(alerts);
3254
3717
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -3304,10 +3767,17 @@ async function convertZapToHdf(input) {
3304
3767
  if (controlType !== void 0) req.controlType = controlType;
3305
3768
  requirements.push(req);
3306
3769
  }
3307
- const targetName = site["@host"] ?? "Unknown Host";
3770
+ const targetName = site?.["@host"] ?? "Unknown Host";
3771
+ const siteName = site?.["@name"] ?? "";
3772
+ const baselineName = site && (site["@name"] || site["@host"]) ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
3773
+ if (requirements.length === 0) {
3774
+ let target = siteName || targetName;
3775
+ if (!target || target === "Unknown Host") target = "the target site";
3776
+ requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
3777
+ }
3308
3778
  const baseline = createMinimalBaseline("OWASP ZAP Scan", requirements, {
3309
3779
  resultsChecksum,
3310
- title: `OWASP ZAP Scan of ${site["@name"] ?? targetName}`,
3780
+ title: baselineName,
3311
3781
  summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
3312
3782
  });
3313
3783
  const tool = {
@@ -3316,14 +3786,14 @@ async function convertZapToHdf(input) {
3316
3786
  };
3317
3787
  if (zapData["@version"]) tool.version = zapData["@version"];
3318
3788
  const components = [];
3319
- if (site["@name"]) components.push({
3789
+ if (site?.["@name"]) components.push({
3320
3790
  name: targetName,
3321
- type: Copyright.Application,
3791
+ type: TargetType.Application,
3322
3792
  url: site["@name"]
3323
3793
  });
3324
3794
  else if (targetName !== "Unknown Host") components.push({
3325
3795
  name: targetName,
3326
- type: Copyright.Application
3796
+ type: TargetType.Application
3327
3797
  });
3328
3798
  const hdf = {
3329
3799
  baselines: [baseline],
@@ -3334,12 +3804,15 @@ async function convertZapToHdf(input) {
3334
3804
  },
3335
3805
  tool
3336
3806
  };
3337
- if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
3338
- return JSON.stringify(hdf, null, 2);
3807
+ if (zapData["@generated"]) {
3808
+ const ts = parseZapTimestamp(zapData["@generated"]);
3809
+ if (ts) hdf.timestamp = ts;
3810
+ }
3811
+ return serializeHdf(hdf);
3339
3812
  }
3340
3813
  //#endregion
3341
3814
  //#region converters/cyclonedx-to-hdf/typescript/converter.ts
3342
- const CVSS_METHODS = new Set([
3815
+ const CVSS_METHODS = /* @__PURE__ */ new Set([
3343
3816
  "CVSSv2",
3344
3817
  "CVSSv3",
3345
3818
  "CVSSv31",
@@ -3404,6 +3877,8 @@ async function convertCyclonedxToHdf(input) {
3404
3877
  if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
3405
3878
  if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create --from <sbom-file> --component-name <name>");
3406
3879
  const resultsChecksum = await inputChecksum(input);
3880
+ const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
3881
+ const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
3407
3882
  const allComponents = flattenComponents(bom.components ?? []);
3408
3883
  const componentLookup = /* @__PURE__ */ new Map();
3409
3884
  for (const comp of allComponents) if (comp["bom-ref"]) componentLookup.set(comp["bom-ref"], comp);
@@ -3442,7 +3917,13 @@ async function convertCyclonedxToHdf(input) {
3442
3917
  data: fixParts.join("\n\n")
3443
3918
  });
3444
3919
  const affects = vuln.affects ?? [];
3445
- const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$4(componentLookup, affect.ref) })) : [createResult(ResultStatus.Failed, void 0, { codeDesc: `Vulnerability ${vuln.id}` })];
3920
+ const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, {
3921
+ codeDesc: formatCodeDesc$4(componentLookup, affect.ref),
3922
+ startTime: scanTime
3923
+ })) : [createResult(ResultStatus.Failed, void 0, {
3924
+ codeDesc: `Vulnerability ${vuln.id}`,
3925
+ startTime: scanTime
3926
+ })];
3446
3927
  const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
3447
3928
  const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
3448
3929
  const controlType = deriveControlTypeFromTags(nist);
@@ -3459,9 +3940,9 @@ async function convertCyclonedxToHdf(input) {
3459
3940
  baselines: [baseline],
3460
3941
  components: [{
3461
3942
  name: targetName,
3462
- type: Copyright.Application
3943
+ type: TargetType.Application
3463
3944
  }],
3464
- timestamp: /* @__PURE__ */ new Date()
3945
+ timestamp: scanTime
3465
3946
  });
3466
3947
  }
3467
3948
  //#endregion
@@ -3632,7 +4113,7 @@ async function convertSplunkToHdf(input) {
3632
4113
  const descriptions = convertDescriptions(control.descriptions);
3633
4114
  const results = Array.isArray(control.results) ? control.results.map((result) => createResult(mapStatus$1(result.status), result.message, {
3634
4115
  codeDesc: result.code_desc,
3635
- startTime: new Date(result.start_time),
4116
+ startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
3636
4117
  runTime: result.run_time,
3637
4118
  exception: result.exception,
3638
4119
  backtrace: result.backtrace
@@ -3655,11 +4136,11 @@ async function convertSplunkToHdf(input) {
3655
4136
  allBaselines.push(createMinimalBaseline(profile.name, requirements, baselineOptions));
3656
4137
  }
3657
4138
  }
3658
- const hdf = {
4139
+ return serializeHdf({
3659
4140
  baselines: allBaselines,
3660
4141
  components: [{
3661
4142
  name: targetName,
3662
- type: Copyright.Host,
4143
+ type: TargetType.Host,
3663
4144
  osName: targetRelease || void 0,
3664
4145
  labels: {}
3665
4146
  }],
@@ -3667,8 +4148,7 @@ async function convertSplunkToHdf(input) {
3667
4148
  name: "splunk-to-hdf",
3668
4149
  version: "1.0.0"
3669
4150
  }
3670
- };
3671
- return JSON.stringify(hdf, null, 2);
4151
+ });
3672
4152
  }
3673
4153
  //#endregion
3674
4154
  //#region converters/hdf-to-xml/typescript/converter.ts
@@ -3759,9 +4239,9 @@ function gitlabSeverityToImpact(severity) {
3759
4239
  }
3760
4240
  function scanTypeToTargetType(scanType) {
3761
4241
  switch (scanType) {
3762
- case "dast": return Copyright.Application;
3763
- case "container_scanning": return Copyright.ContainerImage;
3764
- default: return Copyright.Repository;
4242
+ case "dast": return TargetType.Application;
4243
+ case "container_scanning": return TargetType.ContainerImage;
4244
+ default: return TargetType.Repository;
3765
4245
  }
3766
4246
  }
3767
4247
  function scanTypeLabel(scanType) {
@@ -3878,7 +4358,7 @@ async function convertGitlabToHdf(input) {
3878
4358
  const result = {
3879
4359
  status: ResultStatus.Failed,
3880
4360
  codeDesc: buildCodeDesc$4(scanType, vuln.location),
3881
- startTime: startTime ? new Date(startTime) : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4361
+ startTime: startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z") : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
3882
4362
  };
3883
4363
  const impact = gitlabSeverityToImpact(vuln.severity ?? "Unknown");
3884
4364
  const req = {
@@ -3894,9 +4374,14 @@ async function convertGitlabToHdf(input) {
3894
4374
  if (controlType !== void 0) req.controlType = controlType;
3895
4375
  requirements.push(req);
3896
4376
  }
4377
+ const label = scanTypeLabel(scanType);
4378
+ if (requirements.length === 0) {
4379
+ const ts = startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
4380
+ requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
4381
+ }
3897
4382
  const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
3898
4383
  resultsChecksum,
3899
- title: `GitLab ${scanTypeLabel(scanType)} Security Scan`,
4384
+ title: `GitLab ${label} Security Scan`,
3900
4385
  summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
3901
4386
  });
3902
4387
  const tool = {
@@ -3919,8 +4404,11 @@ async function convertGitlabToHdf(input) {
3919
4404
  },
3920
4405
  tool
3921
4406
  };
3922
- if (report.scan?.end_time) hdf.timestamp = new Date(report.scan.end_time);
3923
- return JSON.stringify(hdf, null, 2);
4407
+ if (report.scan?.end_time) {
4408
+ const endTime = parseTimestamp(report.scan.end_time);
4409
+ if (endTime) hdf.timestamp = endTime;
4410
+ }
4411
+ return serializeHdf(hdf);
3924
4412
  }
3925
4413
  //#endregion
3926
4414
  //#region converters/trufflehog-to-hdf/typescript/converter.ts
@@ -3987,8 +4475,8 @@ function buildCodeDesc$3(f) {
3987
4475
  */
3988
4476
  function getTimestamp(f) {
3989
4477
  if (f.SourceMetadata?.Data?.Git?.timestamp) {
3990
- const ts = new Date(f.SourceMetadata.Data.Git.timestamp);
3991
- if (!isNaN(ts.getTime())) return ts;
4478
+ const ts = parseTimestamp(f.SourceMetadata.Data.Git.timestamp);
4479
+ if (ts) return ts;
3992
4480
  }
3993
4481
  return /* @__PURE__ */ new Date();
3994
4482
  }
@@ -4050,19 +4538,22 @@ async function convertTrufflehogToHdf(input) {
4050
4538
  if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
4051
4539
  validateInputSize(input, "trufflehog");
4052
4540
  const findings = parseFindings(input);
4053
- if (findings.length === 0) throw new Error("trufflehog: no findings in input");
4054
4541
  const resultsChecksum = await inputChecksum(input);
4055
4542
  const limitedFindings = limitArrayWithWarning(findings, "finding");
4056
4543
  const groups = groupFindings(limitedFindings);
4057
4544
  const requirements = [];
4058
4545
  for (const [reqID, group] of groups) requirements.push(buildRequirement$12(reqID, group));
4546
+ if (requirements.length === 0) {
4547
+ const target = findGitRepoURL(limitedFindings) ?? limitedFindings[0]?.SourceName ?? "the target source";
4548
+ requirements.push(buildNoFindingsRequirement("trufflehog-no-findings", `TruffleHog scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
4549
+ }
4059
4550
  const hdf = {
4060
4551
  baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
4061
4552
  resultsChecksum,
4062
4553
  title: `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`
4063
4554
  })],
4064
4555
  generator: {
4065
- name: "hdf-converters",
4556
+ name: "trufflehog-to-hdf",
4066
4557
  version: "1.0.0"
4067
4558
  },
4068
4559
  tool: {
@@ -4074,9 +4565,9 @@ async function convertTrufflehogToHdf(input) {
4074
4565
  const repoURL = findGitRepoURL(limitedFindings);
4075
4566
  if (repoURL) hdf.components = [{
4076
4567
  name: repoURL,
4077
- type: Copyright.Repository
4568
+ type: TargetType.Repository
4078
4569
  }];
4079
- return JSON.stringify(hdf, null, 2);
4570
+ return serializeHdf(hdf);
4080
4571
  }
4081
4572
  //#endregion
4082
4573
  //#region converters/burpsuite-to-hdf/typescript/converter.ts
@@ -4136,6 +4627,7 @@ async function convertBurpsuiteToHdf(input) {
4136
4627
  const requirements = [];
4137
4628
  for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues));
4138
4629
  const targetName = limitedIssues.length > 0 ? (limitedIssues[0].host?.["#text"] ?? "Unknown").trim() : "Unknown";
4630
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("burpsuite-no-findings", `Burp Suite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
4139
4631
  const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
4140
4632
  resultsChecksum,
4141
4633
  title: `BurpSuite Scan: ${targetName}`
@@ -4149,7 +4641,7 @@ async function convertBurpsuiteToHdf(input) {
4149
4641
  baselines: [baseline],
4150
4642
  components: [{
4151
4643
  name: targetName,
4152
- type: Copyright.Application
4644
+ type: TargetType.Application
4153
4645
  }],
4154
4646
  generator: {
4155
4647
  name: "burpsuite-to-hdf",
@@ -4157,8 +4649,8 @@ async function convertBurpsuiteToHdf(input) {
4157
4649
  },
4158
4650
  tool
4159
4651
  };
4160
- if (exportTime) hdf.timestamp = new Date(exportTime);
4161
- return JSON.stringify(hdf, null, 2);
4652
+ if (exportTime) hdf.timestamp = parseTimestamp(exportTime) ?? void 0;
4653
+ return serializeHdf(hdf);
4162
4654
  }
4163
4655
  function buildRequirement$11(issueType, issues) {
4164
4656
  const rep = issues[0];
@@ -4265,15 +4757,31 @@ function formatSummary(f) {
4265
4757
  `IP Address, Port, Instance : ${f["IP Address, Port, Instance"] ?? ""}`
4266
4758
  ].join("\n");
4267
4759
  }
4760
+ const MONTH_ABBR = {
4761
+ Jan: "01",
4762
+ Feb: "02",
4763
+ Mar: "03",
4764
+ Apr: "04",
4765
+ May: "05",
4766
+ Jun: "06",
4767
+ Jul: "07",
4768
+ Aug: "08",
4769
+ Sep: "09",
4770
+ Oct: "10",
4771
+ Nov: "11",
4772
+ Dec: "12"
4773
+ };
4268
4774
  /**
4269
- * Parses DBProtect date format "Feb 18 2021 15:57"
4775
+ * Parses DBProtect date formats ("Feb 18 2021 15:57" or "2021-02-18 15:55").
4776
+ * Month-name values are normalized to ISO so parseTimestamp interprets them as
4777
+ * UTC, matching the Go peer and keeping output host-timezone-independent.
4270
4778
  */
4271
4779
  function parseDate(dateStr) {
4272
4780
  const trimmed = dateStr.trim();
4273
- if (!trimmed) return /* @__PURE__ */ new Date();
4274
- const parsed = new Date(trimmed);
4275
- if (!isNaN(parsed.getTime())) return parsed;
4276
- return /* @__PURE__ */ new Date();
4781
+ const ZERO = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
4782
+ if (!trimmed) return ZERO;
4783
+ const month = /^([A-Z][a-z]{2}) (\d{1,2}) (\d{4}) (\d{2}:\d{2})$/.exec(trimmed);
4784
+ return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO;
4277
4785
  }
4278
4786
  /**
4279
4787
  * Builds a single EvaluatedRequirement from a group of findings sharing a Check ID.
@@ -4346,7 +4854,7 @@ async function convertDbprotectToHdf(input) {
4346
4854
  })],
4347
4855
  components: [{
4348
4856
  name: targetName,
4349
- type: Copyright.Host
4857
+ type: TargetType.Host
4350
4858
  }],
4351
4859
  timestamp: /* @__PURE__ */ new Date()
4352
4860
  });
@@ -4386,6 +4894,148 @@ function buildSummary(result) {
4386
4894
  return `Package Vulnerability Summary: ${result.vulnerabilityDistribution ? String(result.vulnerabilityDistribution.total) : "N/A"} Application Compliance Issue Total: ${result.complianceDistribution ? String(result.complianceDistribution.total) : "N/A"}`;
4387
4895
  }
4388
4896
  /**
4897
+ * Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
4898
+ * since modern Twistlock exclusively emits 3.x output.
4899
+ */
4900
+ function cvssVersionFromVector(vector) {
4901
+ if (!vector) return Version.The31;
4902
+ if (vector.startsWith("CVSS:2.0/")) return Version.The20;
4903
+ if (vector.startsWith("CVSS:3.0/")) return Version.The30;
4904
+ if (vector.startsWith("CVSS:4.0/")) return Version.The40;
4905
+ return Version.The31;
4906
+ }
4907
+ /**
4908
+ * Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
4909
+ */
4910
+ function cvssSeverityFromScore(score) {
4911
+ switch (cvssScoreToSeverity(score)) {
4912
+ case "none": return CVSSSeverity.None;
4913
+ case "low": return CVSSSeverity.Low;
4914
+ case "medium": return CVSSSeverity.Medium;
4915
+ case "high": return CVSSSeverity.High;
4916
+ case "critical": return CVSSSeverity.Critical;
4917
+ default: return;
4918
+ }
4919
+ }
4920
+ /**
4921
+ * Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
4922
+ * when neither a score nor a vector is available. When the vendor emits a
4923
+ * score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
4924
+ * entry is still emitted — the schema makes baseVector optional precisely
4925
+ * so vendor-final-score data isn't dropped.
4926
+ */
4927
+ function buildCvss(vuln) {
4928
+ const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
4929
+ const hasVector = !!vuln.vector;
4930
+ if (!hasScore && !hasVector) return void 0;
4931
+ const cv = { version: cvssVersionFromVector(vuln.vector) };
4932
+ if (hasScore) {
4933
+ cv.baseScore = vuln.cvss;
4934
+ const sev = cvssSeverityFromScore(vuln.cvss);
4935
+ if (sev !== void 0) cv.baseSeverity = sev;
4936
+ }
4937
+ if (hasVector) cv.baseVector = vuln.vector;
4938
+ const source = vuln.cve ?? vuln.id;
4939
+ if (source) cv.source = source;
4940
+ return cv;
4941
+ }
4942
+ const CWE_REGEX = /cwe[-_]?(\d+)/gi;
4943
+ /**
4944
+ * Extracts canonical CWE-N identifiers from a free-form string.
4945
+ */
4946
+ function parseCwes(raw) {
4947
+ if (!raw) return [];
4948
+ const out = [];
4949
+ const seen = /* @__PURE__ */ new Set();
4950
+ for (const m of raw.matchAll(CWE_REGEX)) {
4951
+ const id = `CWE-${m[1]}`;
4952
+ if (seen.has(id)) continue;
4953
+ seen.add(id);
4954
+ out.push(id);
4955
+ }
4956
+ return out;
4957
+ }
4958
+ function rhelDistro(distro) {
4959
+ const low = distro.toLowerCase();
4960
+ return [
4961
+ "red hat",
4962
+ "rhel",
4963
+ "centos",
4964
+ "fedora",
4965
+ "amazon linux",
4966
+ "oracle linux",
4967
+ "rocky",
4968
+ "alma"
4969
+ ].some((m) => low.includes(m));
4970
+ }
4971
+ function debDistro(distro) {
4972
+ const low = distro.toLowerCase();
4973
+ return ["debian", "ubuntu"].some((m) => low.includes(m));
4974
+ }
4975
+ /**
4976
+ * Maps a Twistlock package type plus the result's distro string to a schema
4977
+ * Ecosystem value. Defaults to 'generic' for unknown types.
4978
+ */
4979
+ function resolveEcosystem(packageType, distro) {
4980
+ const t = (packageType ?? "").toLowerCase();
4981
+ const d = distro ?? "";
4982
+ switch (t) {
4983
+ case "os":
4984
+ if (rhelDistro(d)) return Ecosystem.RPM;
4985
+ if (debDistro(d)) return Ecosystem.Deb;
4986
+ return Ecosystem.Generic;
4987
+ case "rpm": return Ecosystem.RPM;
4988
+ case "deb": return Ecosystem.Deb;
4989
+ case "jar":
4990
+ case "maven": return Ecosystem.Maven;
4991
+ case "python":
4992
+ case "pypi": return Ecosystem.Pypi;
4993
+ case "nodejs":
4994
+ case "npm": return Ecosystem.Npm;
4995
+ case "gem": return Ecosystem.Gem;
4996
+ case "nuget": return Ecosystem.Nuget;
4997
+ case "go": return Ecosystem.Go;
4998
+ case "cargo": return Ecosystem.Cargo;
4999
+ default: return Ecosystem.Generic;
5000
+ }
5001
+ }
5002
+ const FIX_VERSION_REGEX = /\d+(?:\.\d+)+[A-Za-z0-9._+\-]*/;
5003
+ /**
5004
+ * Extracts the first version-looking token from fixedBy or a "fixed in X"
5005
+ * status string. Returns empty string when no fix info is present.
5006
+ */
5007
+ function extractFixedInVersion(vuln) {
5008
+ if (vuln.fixedBy) return vuln.fixedBy;
5009
+ if (!(vuln.status ?? "").toLowerCase().includes("fixed")) return "";
5010
+ const m = (vuln.status ?? "").match(FIX_VERSION_REGEX);
5011
+ return m ? m[0] : "";
5012
+ }
5013
+ /**
5014
+ * Builds an AffectedPackage entry from per-vulnerability fields. Returns
5015
+ * undefined when packageName or packageVersion are missing (both required).
5016
+ */
5017
+ function buildAffectedPackage(vuln, packageTypes, distro) {
5018
+ if (!vuln.packageName || !vuln.packageVersion) return void 0;
5019
+ const pkgType = vuln.packageType ?? packageTypes.get(vuln.packageName);
5020
+ const pkg = {
5021
+ name: vuln.packageName,
5022
+ version: vuln.packageVersion,
5023
+ ecosystem: resolveEcosystem(pkgType, distro)
5024
+ };
5025
+ const fixed = extractFixedInVersion(vuln);
5026
+ if (fixed) pkg.fixedInVersion = fixed;
5027
+ return pkg;
5028
+ }
5029
+ /**
5030
+ * Indexes packageName → packageType from the result-level packages array.
5031
+ */
5032
+ function buildPackageTypeIndex(pkgs) {
5033
+ const idx = /* @__PURE__ */ new Map();
5034
+ if (!pkgs) return idx;
5035
+ for (const p of pkgs) if (p.name && p.type) idx.set(p.name, p.type);
5036
+ return idx;
5037
+ }
5038
+ /**
4389
5039
  * Builds the code_desc string for a vulnerability result.
4390
5040
  */
4391
5041
  function formatCodeDesc$2(vuln) {
@@ -4395,15 +5045,22 @@ function formatCodeDesc$2(vuln) {
4395
5045
  }
4396
5046
  /**
4397
5047
  * Converts a single vulnerability into an EvaluatedRequirement.
5048
+ *
5049
+ * @param vuln - The Twistlock vulnerability object
5050
+ * @param packageTypes - name→type lookup built from the enclosing result's packages array
5051
+ * @param distro - the enclosing result's distro string, used to disambiguate "os" packages
4398
5052
  */
4399
- function buildRequirement$9(vuln) {
5053
+ function buildRequirement$9(vuln, packageTypes, distro) {
4400
5054
  const nist = DEFAULT_REMEDIATION_NIST_TAGS;
4401
- const tags = buildNistCciTags(nist, nistToCci(nist), { cveid: [vuln.id] });
5055
+ const cciTags = nistToCci(nist);
5056
+ const extras = { cveid: [vuln.id] };
5057
+ if (typeof vuln.cvss === "number" && vuln.cvss > 0) extras["cvss_base_score"] = vuln.cvss;
5058
+ const tags = buildNistCciTags(nist, cciTags, extras);
4402
5059
  const descriptions = [{
4403
5060
  label: "default",
4404
5061
  data: vuln.description
4405
5062
  }];
4406
- const startTime = vuln.discoveredDate ? new Date(vuln.discoveredDate) : void 0;
5063
+ const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
4407
5064
  const results = [createResult(ResultStatus.Failed, void 0, {
4408
5065
  codeDesc: formatCodeDesc$2(vuln),
4409
5066
  startTime
@@ -4412,6 +5069,12 @@ function buildRequirement$9(vuln) {
4412
5069
  const controlType = deriveControlTypeFromTags(nist);
4413
5070
  if (controlType !== void 0) req.controlType = controlType;
4414
5071
  req.verificationMethod = VerificationMethodEnum.Automated;
5072
+ const cv = buildCvss(vuln);
5073
+ if (cv) req.cvss = [cv];
5074
+ const cwes = parseCwes(vuln.cwe);
5075
+ if (cwes.length > 0) req.cwe = cwes;
5076
+ const pkg = buildAffectedPackage(vuln, packageTypes, distro);
5077
+ if (pkg) req.affectedPackages = [pkg];
4415
5078
  return req;
4416
5079
  }
4417
5080
  /**
@@ -4422,7 +5085,13 @@ function convertSingleResult(result, resultsChecksum) {
4422
5085
  const { items: limitedVulns, truncated } = limitArray(vulns);
4423
5086
  /* v8 ignore next -- truncation only triggers with >100K items */
4424
5087
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
4425
- return createMinimalBaseline("Twistlock Scan", limitedVulns.map((vuln) => buildRequirement$9(vuln)), {
5088
+ const packageTypes = buildPackageTypeIndex(result.packages);
5089
+ const requirements = limitedVulns.map((vuln) => buildRequirement$9(vuln, packageTypes, result.distro));
5090
+ if (requirements.length === 0) {
5091
+ const target = result.name ?? result.repository ?? result.id ?? "scan target";
5092
+ requirements.push(buildNoFindingsRequirement("twistlock-no-findings", `Twistlock scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
5093
+ }
5094
+ return createMinimalBaseline("Twistlock Scan", requirements, {
4426
5095
  resultsChecksum,
4427
5096
  title: buildTitle$1(result),
4428
5097
  summary: buildSummary(result)
@@ -4457,7 +5126,7 @@ async function convertTwistlockToHdf(input) {
4457
5126
  baselines,
4458
5127
  components: [{
4459
5128
  name: targetName,
4460
- type: Copyright.ContainerImage,
5129
+ type: TargetType.ContainerImage,
4461
5130
  labels: { image: results[0]?.id ?? targetName }
4462
5131
  }],
4463
5132
  timestamp: /* @__PURE__ */ new Date()
@@ -4523,15 +5192,36 @@ function buildRequirement$8(finding, timestamp) {
4523
5192
  const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
4524
5193
  const results = [createResult(ResultStatus.Failed, void 0, {
4525
5194
  codeDesc,
4526
- startTime: timestamp ? new Date(timestamp) : void 0
5195
+ startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4527
5196
  })];
4528
5197
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
4529
5198
  req.verificationMethod = VerificationMethodEnum.Automated;
4530
5199
  const controlType = deriveControlTypeFromTags(nist);
4531
5200
  if (controlType !== void 0) req.controlType = controlType;
5201
+ const pkg = buildAffectedPackageFromComponent(finding.component);
5202
+ if (pkg) req.affectedPackages = [pkg];
4532
5203
  return req;
4533
5204
  }
4534
5205
  /**
5206
+ * Builds an Affected_Package from a Dependency-Track component. Prefers
5207
+ * the rich identifiers Dependency-Track already exposes (purl, cpe) and
5208
+ * augments with name/version/ecosystem when available. Returns undefined
5209
+ * when the component carries no schema-acceptable identifier.
5210
+ */
5211
+ function buildAffectedPackageFromComponent(c) {
5212
+ let ecosystem;
5213
+ if (c.purl) ecosystem = ecosystemFromPurlType(parsePurl(c.purl)?.type);
5214
+ else if (c.name && c.version) ecosystem = ecosystemFromPurlType(void 0);
5215
+ return buildAffectedPackage$1({
5216
+ name: c.name,
5217
+ version: c.version,
5218
+ ecosystem,
5219
+ purl: c.purl,
5220
+ cpe: c.cpe,
5221
+ fixedInVersion: c.latestVersion
5222
+ });
5223
+ }
5224
+ /**
4535
5225
  * Converts Dependency-Track FPF JSON output to HDF format.
4536
5226
  *
4537
5227
  * @param input - Dependency-Track FPF JSON string
@@ -4544,7 +5234,12 @@ async function convertDeptrackToHdf(input) {
4544
5234
  const parsed = parseJSON(input);
4545
5235
  if (!parsed || typeof parsed !== "object") throw new Error("deptrack: invalid JSON");
4546
5236
  if (!parsed.findings && !parsed.project && !parsed.meta) throw new Error("deptrack: input does not appear to be a Dependency-Track report");
4547
- const baseline = createMinimalBaseline("Dependency-Track Scan", (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp)), {
5237
+ const requirements = (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp));
5238
+ if (requirements.length === 0) {
5239
+ const projectName = parsed.project?.name ?? parsed.project?.uuid ?? "";
5240
+ requirements.push(buildNoFindingsRequirement("deptrack-no-findings", `Dependency-Track analyzed ${projectName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
5241
+ }
5242
+ const baseline = createMinimalBaseline("Dependency-Track Scan", requirements, {
4548
5243
  resultsChecksum,
4549
5244
  title: `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`,
4550
5245
  summary: parsed.project?.description
@@ -4558,7 +5253,7 @@ async function convertDeptrackToHdf(input) {
4558
5253
  baselines: [baseline],
4559
5254
  components: [{
4560
5255
  name: targetName,
4561
- type: Copyright.Application
5256
+ type: TargetType.Application
4562
5257
  }],
4563
5258
  timestamp: /* @__PURE__ */ new Date()
4564
5259
  });
@@ -4618,7 +5313,7 @@ function formatCodeDesc$1(entry) {
4618
5313
  /**
4619
5314
  * Builds a single EvaluatedRequirement from a group of entries sharing an ID.
4620
5315
  */
4621
- function buildRequirement$7(entryID, entries) {
5316
+ function buildRequirement$7(entryID, entries, scanTime) {
4622
5317
  const rep = entries[0];
4623
5318
  const cweIDs = extractCWEs$1(rep);
4624
5319
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -4631,13 +5326,57 @@ function buildRequirement$7(entryID, entries) {
4631
5326
  label: "default",
4632
5327
  data: formatDescription(rep)
4633
5328
  }];
4634
- const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$1(entry) }));
5329
+ const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, {
5330
+ codeDesc: formatCodeDesc$1(entry),
5331
+ startTime: scanTime
5332
+ }));
4635
5333
  const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
4636
5334
  const controlType = deriveControlTypeFromTags(nist);
4637
5335
  if (controlType !== void 0) req.controlType = controlType;
4638
5336
  req.verificationMethod = VerificationMethodEnum.Automated;
5337
+ const pkg = buildAffectedPackageFromEntry(rep);
5338
+ if (pkg) req.affectedPackages = [pkg];
4639
5339
  return req;
4640
5340
  }
5341
+ function ecosystemFromXraySource(scheme) {
5342
+ if (scheme === "gav") return Ecosystem.Maven;
5343
+ return ecosystemFromPurlType(scheme);
5344
+ }
5345
+ function parseSourceCompID(s) {
5346
+ if (!s) return void 0;
5347
+ const m = /^([a-zA-Z0-9]+):\/\/(.+)$/.exec(s);
5348
+ if (!m) return void 0;
5349
+ const scheme = m[1].toLowerCase();
5350
+ const rest = m[2];
5351
+ const colonIdx = rest.lastIndexOf(":");
5352
+ if (colonIdx > 0) return {
5353
+ scheme,
5354
+ name: rest.slice(0, colonIdx),
5355
+ version: rest.slice(colonIdx + 1)
5356
+ };
5357
+ return {
5358
+ scheme,
5359
+ name: rest
5360
+ };
5361
+ }
5362
+ function buildAffectedPackageFromEntry(entry) {
5363
+ const parsed = parseSourceCompID(entry.source_comp_id ?? entry.source_id);
5364
+ let name = entry.component ?? "";
5365
+ let version;
5366
+ let ecosystem;
5367
+ if (parsed) {
5368
+ if (parsed.name) name = parsed.name;
5369
+ version = parsed.version;
5370
+ ecosystem = ecosystemFromXraySource(parsed.scheme);
5371
+ }
5372
+ const fixed = entry.component_versions?.fixed_versions?.[0];
5373
+ return buildAffectedPackage$1({
5374
+ name,
5375
+ version,
5376
+ ecosystem: ecosystem ?? (name ? Ecosystem.Generic : void 0),
5377
+ fixedInVersion: fixed
5378
+ });
5379
+ }
4641
5380
  /**
4642
5381
  * Converts JFrog Xray JSON output to HDF format.
4643
5382
  *
@@ -4650,6 +5389,7 @@ async function convertJfrogXrayToHdf(input) {
4650
5389
  const resultsChecksum = await inputChecksum(input);
4651
5390
  const parsed = parseJSON(input);
4652
5391
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.data)) throw new Error("jfrog-xray: invalid JSON structure");
5392
+ const scanTime = /* @__PURE__ */ new Date();
4653
5393
  const { items: limitedEntries, truncated } = limitArray(parsed.data);
4654
5394
  /* v8 ignore next -- truncation only triggers with >100K items */
4655
5395
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedEntries.length} entries (original: ${parsed.data.length})`);
@@ -4666,7 +5406,8 @@ async function convertJfrogXrayToHdf(input) {
4666
5406
  else groups.set(id, [entry]);
4667
5407
  }
4668
5408
  const requirements = [];
4669
- for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries));
5409
+ for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries, scanTime));
5410
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
4670
5411
  return buildHdfResults({
4671
5412
  generatorName: "jfrog-xray-to-hdf",
4672
5413
  converterVersion: "1.0.0",
@@ -4675,9 +5416,9 @@ async function convertJfrogXrayToHdf(input) {
4675
5416
  baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
4676
5417
  components: [{
4677
5418
  name: "JFrog Xray Scan",
4678
- type: Copyright.Application
5419
+ type: TargetType.Application
4679
5420
  }],
4680
- timestamp: /* @__PURE__ */ new Date()
5421
+ timestamp: scanTime
4681
5422
  });
4682
5423
  }
4683
5424
  //#endregion
@@ -4721,7 +5462,7 @@ function vulnMessage(vuln) {
4721
5462
  /**
4722
5463
  * Builds a single EvaluatedRequirement from a NeuVector vulnerability.
4723
5464
  */
4724
- function buildRequirement$6(vuln) {
5465
+ function buildRequirement$6(vuln, scanTime) {
4725
5466
  const cweIDs = extractCWEs(vuln.description);
4726
5467
  const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS);
4727
5468
  const tags = {
@@ -4733,11 +5474,23 @@ function buildRequirement$6(vuln) {
4733
5474
  label: "default",
4734
5475
  data: vuln.description
4735
5476
  }];
4736
- const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), { codeDesc: "" })];
5477
+ const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
5478
+ codeDesc: "",
5479
+ startTime: scanTime
5480
+ })];
4737
5481
  const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
4738
5482
  const controlType = deriveControlTypeFromTags(nist);
4739
5483
  if (controlType !== void 0) req.controlType = controlType;
4740
5484
  req.verificationMethod = VerificationMethodEnum.Automated;
5485
+ const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
5486
+ const pkg = buildAffectedPackage$1({
5487
+ name: vuln.package_name,
5488
+ version: vuln.package_version,
5489
+ ecosystem: vuln.package_name && vuln.package_version ? Ecosystem.Generic : void 0,
5490
+ cpe: cpe23,
5491
+ fixedInVersion: vuln.fixed_version
5492
+ });
5493
+ if (pkg) req.affectedPackages = [pkg];
4741
5494
  return req;
4742
5495
  }
4743
5496
  /**
@@ -4775,14 +5528,17 @@ async function convertNeuvectorToHdf(input) {
4775
5528
  const { items: limitedVulns, truncated } = limitArray(scan.report.vulnerabilities ?? []);
4776
5529
  /* v8 ignore next -- truncation only triggers with >100K items */
4777
5530
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${scan.report.vulnerabilities.length})`);
5531
+ const scanTime = /* @__PURE__ */ new Date();
4778
5532
  const seen = /* @__PURE__ */ new Set();
4779
5533
  const requirements = [];
4780
5534
  for (const vuln of limitedVulns) {
4781
5535
  const id = vulnID(vuln);
4782
5536
  if (seen.has(id)) continue;
4783
5537
  seen.add(id);
4784
- requirements.push(buildRequirement$6(vuln));
5538
+ requirements.push(buildRequirement$6(vuln, scanTime));
4785
5539
  }
5540
+ const target = targetNameFromReport(scan.report);
5541
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
4786
5542
  return buildHdfResults({
4787
5543
  generatorName: "neuvector-to-hdf",
4788
5544
  converterVersion: "1.0.0",
@@ -4794,13 +5550,13 @@ async function convertNeuvectorToHdf(input) {
4794
5550
  })],
4795
5551
  components: [{
4796
5552
  name: targetNameFromReport(scan.report),
4797
- type: Copyright.ContainerImage,
5553
+ type: TargetType.ContainerImage,
4798
5554
  labels: {
4799
5555
  image: `${scan.report.registry}/${scan.report.repository}:${scan.report.tag}`,
4800
5556
  registry: scan.report.registry
4801
5557
  }
4802
5558
  }],
4803
- timestamp: /* @__PURE__ */ new Date()
5559
+ timestamp: scanTime
4804
5560
  });
4805
5561
  }
4806
5562
  //#endregion
@@ -4875,7 +5631,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
4875
5631
  const results = limitedVulns.map((vuln) => ({
4876
5632
  status: ResultStatus.Failed,
4877
5633
  codeDesc: buildCodeDesc$2(vuln, snippetMap),
4878
- startTime: new Date(startTimeStr)
5634
+ startTime: parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date()
4879
5635
  }));
4880
5636
  const req = {
4881
5637
  id: desc.classID ?? "unknown",
@@ -4915,21 +5671,22 @@ async function convertFortifyToHdf(input) {
4915
5671
  if (truncatedDescs) console.warn(`WARNING: Input truncated at ${limitedDescs.length} Description items (original: ${descriptions.length})`);
4916
5672
  const createdDate = fvdl.CreatedTS?.date ?? "";
4917
5673
  const startTimeStr = `${createdDate}T${fvdl.CreatedTS?.time ?? ""}`;
4918
- const baseline = createMinimalBaseline("Fortify Scan", limitedDescs.map((desc) => {
5674
+ const requirements = limitedDescs.map((desc) => {
4919
5675
  return buildRequirement$5(desc, vulnGroups.get(desc.classID ?? "") ?? [], snippetMap, startTimeStr);
4920
- }), {
4921
- resultsChecksum,
4922
- title: "Fortify Static Analyzer Scan",
4923
- summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
4924
- version: fvdl.EngineData?.EngineVersion ?? "",
4925
- status: "loaded"
4926
5676
  });
4927
5677
  const targetName = fvdl.Build?.SourceBasePath ?? fvdl.Build?.BuildID ?? "Unknown";
5678
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("fortify-no-findings", `Fortify scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
4928
5679
  const hdfResult = {
4929
- baselines: [baseline],
5680
+ baselines: [createMinimalBaseline("Fortify Scan", requirements, {
5681
+ resultsChecksum,
5682
+ title: "Fortify Static Analyzer Scan",
5683
+ summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
5684
+ version: fvdl.EngineData?.EngineVersion ?? "",
5685
+ status: "loaded"
5686
+ })],
4930
5687
  components: [{
4931
5688
  name: targetName,
4932
- type: Copyright.Repository
5689
+ type: TargetType.Repository
4933
5690
  }],
4934
5691
  generator: {
4935
5692
  name: "fortify-to-hdf",
@@ -4940,8 +5697,8 @@ async function convertFortifyToHdf(input) {
4940
5697
  format: "FVDL"
4941
5698
  }
4942
5699
  };
4943
- if (createdDate) hdfResult.timestamp = new Date(startTimeStr);
4944
- return JSON.stringify(hdfResult, null, 2);
5700
+ if (createdDate) hdfResult.timestamp = parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date();
5701
+ return serializeHdf(hdfResult);
4945
5702
  }
4946
5703
  //#endregion
4947
5704
  //#region converters/prisma-to-hdf/typescript/converter.ts
@@ -5025,7 +5782,7 @@ function makeTitle(rec) {
5025
5782
  /**
5026
5783
  * Builds a single EvaluatedRequirement from a Prisma record.
5027
5784
  */
5028
- function buildRequirement$4(rec) {
5785
+ function buildRequirement$4(rec, scanTime) {
5029
5786
  const id = makeRequirementID(rec);
5030
5787
  const title = makeTitle(rec);
5031
5788
  const codeDesc = makeCodeDesc(rec);
@@ -5039,14 +5796,58 @@ function buildRequirement$4(rec) {
5039
5796
  label: "default",
5040
5797
  data: rec.Description
5041
5798
  }];
5042
- const results = [createResult(ResultStatus.Failed, message, { codeDesc })];
5799
+ const results = [createResult(ResultStatus.Failed, message, {
5800
+ codeDesc,
5801
+ startTime: scanTime
5802
+ })];
5043
5803
  const req = createRequirement(id, title, descriptions, getImpact$3(rec.Severity), results, { tags });
5044
5804
  const controlType = deriveControlTypeFromTags(nist);
5045
5805
  if (controlType !== void 0) req.controlType = controlType;
5046
5806
  req.verificationMethod = VerificationMethodEnum.Automated;
5807
+ if (rec["CVE ID"]) {
5808
+ const pkg = buildAffectedPackageFromRecord(rec);
5809
+ if (pkg) req.affectedPackages = [pkg];
5810
+ }
5047
5811
  return req;
5048
5812
  }
5049
5813
  /**
5814
+ * Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
5815
+ * `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
5816
+ * mapped — unknown vendors fall back to `generic` rather than guessing.
5817
+ */
5818
+ function ecosystemFromDistro(distro) {
5819
+ if (!distro) return Ecosystem.Generic;
5820
+ switch (distro.split("-")[0]?.toLowerCase()) {
5821
+ case "redhat":
5822
+ case "rhel":
5823
+ case "centos":
5824
+ case "rocky":
5825
+ case "alma":
5826
+ case "fedora":
5827
+ case "amazon":
5828
+ case "amazonlinux":
5829
+ case "suse":
5830
+ case "sles":
5831
+ case "opensuse": return Ecosystem.RPM;
5832
+ case "debian":
5833
+ case "ubuntu": return Ecosystem.Deb;
5834
+ default: return Ecosystem.Generic;
5835
+ }
5836
+ }
5837
+ const FIX_VERSION_PATTERN = /fixed in\s+([^\s,;]+)/i;
5838
+ function buildAffectedPackageFromRecord(rec) {
5839
+ const name = rec["Source Package"] || rec.Packages;
5840
+ const version = rec["Package Version"];
5841
+ if (!name || !version) return void 0;
5842
+ const fixMatch = rec["Fix Status"] ? FIX_VERSION_PATTERN.exec(rec["Fix Status"]) : null;
5843
+ return buildAffectedPackage$1({
5844
+ name,
5845
+ version,
5846
+ ecosystem: ecosystemFromDistro(rec.Distro),
5847
+ fixedInVersion: fixMatch ? fixMatch[1] : void 0
5848
+ });
5849
+ }
5850
+ /**
5050
5851
  * Groups records by hostname, preserving insertion order.
5051
5852
  */
5052
5853
  function groupByHostname(records) {
@@ -5061,8 +5862,8 @@ function groupByHostname(records) {
5061
5862
  /**
5062
5863
  * Builds an HDF baseline from all records for a single host.
5063
5864
  */
5064
- function buildBaseline(hostname, records, resultsChecksum) {
5065
- return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec)), {
5865
+ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
5866
+ return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec, scanTime)), {
5066
5867
  resultsChecksum,
5067
5868
  title: `Prisma Cloud Scan (${hostname})`
5068
5869
  });
@@ -5077,20 +5878,26 @@ function buildBaseline(hostname, records, resultsChecksum) {
5077
5878
  async function convertPrismaToHdf(input) {
5078
5879
  if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
5079
5880
  validateInputSize(input, "prisma");
5881
+ const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
5882
+ for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
5080
5883
  const records = parseCsv(input);
5081
- if (records.length === 0) throw new Error("prisma: no data rows in CSV");
5082
- const firstRecord = records[0];
5083
- for (const col of REQUIRED_COLUMNS) if (!(col in firstRecord)) throw new Error(`prisma: missing required CSV column "${col}"`);
5084
5884
  const resultsChecksum = await inputChecksum(input);
5085
- const hostGroups = groupByHostname(records);
5885
+ const scanTime = /* @__PURE__ */ new Date();
5086
5886
  const baselines = [];
5087
5887
  const components = [];
5088
- for (const [hostname, hostRecords] of hostGroups) {
5089
- baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
5090
- components.push({
5091
- name: hostname,
5092
- type: Copyright.Host
5093
- });
5888
+ if (records.length === 0) baselines.push(createMinimalBaseline("Prisma Cloud Scan", [buildNoFindingsRequirement("prisma-no-findings", "Prisma Cloud scanned the workload and reported zero vulnerable components.", scanTime)], {
5889
+ resultsChecksum,
5890
+ title: "Prisma Cloud Scan"
5891
+ }));
5892
+ else {
5893
+ const hostGroups = groupByHostname(records);
5894
+ for (const [hostname, hostRecords] of hostGroups) {
5895
+ baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum, scanTime));
5896
+ components.push({
5897
+ name: hostname,
5898
+ type: TargetType.Host
5899
+ });
5900
+ }
5094
5901
  }
5095
5902
  return buildHdfResults({
5096
5903
  generatorName: "prisma-to-hdf",
@@ -5099,7 +5906,7 @@ async function convertPrismaToHdf(input) {
5099
5906
  toolFormat: "CSV",
5100
5907
  baselines,
5101
5908
  components,
5102
- timestamp: /* @__PURE__ */ new Date()
5909
+ timestamp: scanTime
5103
5910
  });
5104
5911
  }
5105
5912
  //#endregion
@@ -5115,6 +5922,15 @@ const IMPACT_MAPPING$3 = {
5115
5922
  function getImpact$2(severity) {
5116
5923
  return IMPACT_MAPPING$3[severity.toLowerCase()] ?? .5;
5117
5924
  }
5925
+ const NETSPARKER_US_DATETIME = /^\d{2}\/\d{2}\/\d{4} \d{1,2}:\d{2} (AM|PM)$/;
5926
+ function parseNetsparkerTimestamp(s) {
5927
+ const trimmed = s.trim();
5928
+ if (NETSPARKER_US_DATETIME.test(trimmed)) {
5929
+ const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
5930
+ if (!isNaN(d.getTime())) return d;
5931
+ }
5932
+ return parseTimestamp(s);
5933
+ }
5118
5934
  function formatCodeDesc(request) {
5119
5935
  const parts = [];
5120
5936
  parts.push(`http-request : ${request?.content ?? ""}`);
@@ -5196,7 +6012,7 @@ function buildRequirement$3(vuln, initiated) {
5196
6012
  });
5197
6013
  const codeDesc = formatCodeDesc(vuln["http-request"]);
5198
6014
  const message = formatMessage$1(vuln["http-response"]);
5199
- const startTime = initiated ? new Date(initiated) : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6015
+ const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5200
6016
  const results = [{
5201
6017
  status: ResultStatus.Failed,
5202
6018
  codeDesc,
@@ -5239,20 +6055,24 @@ async function convertNetsparkerToHdf(input) {
5239
6055
  const { items: limitedVulns, truncated } = limitArray(vulns);
5240
6056
  /* v8 ignore next -- truncation only triggers with >100K items */
5241
6057
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
5242
- const baseline = createMinimalBaseline("Netsparker Scan", limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated)), {
5243
- resultsChecksum,
5244
- title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
5245
- });
5246
6058
  const targetName = target.url ?? "Unknown";
6059
+ const requirements = limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated));
6060
+ if (requirements.length === 0) {
6061
+ const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date();
6062
+ requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
6063
+ }
5247
6064
  return buildHdfResults({
5248
6065
  generatorName: "netsparker-to-hdf",
5249
6066
  converterVersion: "1.0.0",
5250
6067
  toolName,
5251
6068
  toolFormat: "XML",
5252
- baselines: [baseline],
6069
+ baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
6070
+ resultsChecksum,
6071
+ title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
6072
+ })],
5253
6073
  components: [{
5254
6074
  name: targetName,
5255
- type: Copyright.Application
6075
+ type: TargetType.Application
5256
6076
  }]
5257
6077
  });
5258
6078
  }
@@ -5345,7 +6165,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
5345
6165
  });
5346
6166
  const resultObj = createResult(getStatus$1(finding.checked_items, finding.flagged_items), getMessage(finding.checked_items, finding.flagged_items, finding.items), {
5347
6167
  codeDesc: finding.description,
5348
- startTime: startTime ? new Date(startTime) : void 0
6168
+ startTime: (startTime ? parseTimestamp(startTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
5349
6169
  });
5350
6170
  const req = createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
5351
6171
  const controlType = deriveControlTypeFromTags(nist);
@@ -5367,12 +6187,14 @@ async function convertScoutsuiteToHdf(input) {
5367
6187
  const resultsChecksum = await inputChecksum(jsonStr);
5368
6188
  const report = parseJSON(jsonStr);
5369
6189
  if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
5370
- const baseline = createMinimalBaseline("ScoutSuite Scan", limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time)), {
6190
+ const requirements = limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time));
6191
+ const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
6192
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("scoutsuite-no-findings", `ScoutSuite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
6193
+ const baseline = createMinimalBaseline("ScoutSuite Scan", requirements, {
5371
6194
  resultsChecksum,
5372
6195
  title: `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`,
5373
6196
  summary: report.last_run.ruleset_about
5374
6197
  });
5375
- const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
5376
6198
  return buildHdfResults({
5377
6199
  generatorName: "scoutsuite-to-hdf",
5378
6200
  converterVersion: "1.0.0",
@@ -5382,13 +6204,13 @@ async function convertScoutsuiteToHdf(input) {
5382
6204
  baselines: [baseline],
5383
6205
  components: [{
5384
6206
  name: targetName,
5385
- type: Copyright.CloudAccount,
6207
+ type: TargetType.CloudAccount,
5386
6208
  labels: {
5387
6209
  account: report.account_id,
5388
6210
  provider: report.provider_code ?? report.provider_name
5389
6211
  }
5390
6212
  }],
5391
- timestamp: report.last_run.time ? new Date(report.last_run.time) : /* @__PURE__ */ new Date()
6213
+ timestamp: report.last_run.time ? parseTimestamp(report.last_run.time) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date()
5392
6214
  });
5393
6215
  }
5394
6216
  //#endregion
@@ -5485,7 +6307,7 @@ function buildRequirementFromResult(result, filename) {
5485
6307
  }];
5486
6308
  const scannerName = result.response.service_name;
5487
6309
  const startTimeStr = result.response.milestones?.service_started ?? "";
5488
- const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
6310
+ const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5489
6311
  const score = result.result.score;
5490
6312
  const status = determineStatus(score);
5491
6313
  let results;
@@ -5539,6 +6361,10 @@ async function convertConveyorToHdf(input) {
5539
6361
  const desc = data.api_response.params["description"];
5540
6362
  if (typeof desc === "string" && desc.length > 0) targetName = desc;
5541
6363
  }
6364
+ if (baselines.length === 0) baselines.push(createMinimalBaseline("Conveyor Scan", [buildNoFindingsRequirement("conveyor-no-findings", `Conveyor scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())], {
6365
+ resultsChecksum,
6366
+ title: "Conveyor Scan (no findings)"
6367
+ }));
5542
6368
  return buildHdfResults({
5543
6369
  generatorName: "conveyor-to-hdf",
5544
6370
  converterVersion: "1.0.0",
@@ -5547,7 +6373,7 @@ async function convertConveyorToHdf(input) {
5547
6373
  baselines,
5548
6374
  components: [{
5549
6375
  name: targetName,
5550
- type: Copyright.Application
6376
+ type: TargetType.Application
5551
6377
  }],
5552
6378
  timestamp: /* @__PURE__ */ new Date()
5553
6379
  });
@@ -5569,7 +6395,7 @@ async function convertConveyorToHdf(input) {
5569
6395
  /** Attribute prefix used by fast-xml-parser — all XML attributes are accessed via `@_name`. */
5570
6396
  const A = "@_";
5571
6397
  /** Veracode severity level (0-5) to HDF impact mapping. */
5572
- const IMPACT_MAPPING$2 = new Map([
6398
+ const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
5573
6399
  ["5", .9],
5574
6400
  ["4", .7],
5575
6401
  ["3", .5],
@@ -5591,9 +6417,7 @@ function decodeXmlEntities(s) {
5591
6417
  /** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
5592
6418
  function parseVeracodeTimestamp(ts) {
5593
6419
  if (!ts) return void 0;
5594
- const decoded = decodeXmlEntities(ts);
5595
- const d = new Date(decoded.replace(" UTC", "Z").replace(" ", "T"));
5596
- return isNaN(d.getTime()) ? void 0 : d;
6420
+ return parseTimestamp(decodeXmlEntities(ts).replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
5597
6421
  }
5598
6422
  /** Format description paragraphs into text. */
5599
6423
  function formatDesc(desc) {
@@ -5814,6 +6638,8 @@ async function convertVeracodeToHdf(input) {
5814
6638
  const cweRequirements = buildCWERequirements(ensureArray(report.severity), firstBuildDate);
5815
6639
  const cveRequirements = buildCVERequirements(report.software_composition_analysis, firstBuildDate);
5816
6640
  const allRequirements = [...cweRequirements, ...cveRequirements];
6641
+ const targetName = attr(report, "app_name") || "Veracode Application";
6642
+ if (allRequirements.length === 0) allRequirements.push(buildNoFindingsRequirement("veracode-no-findings", `Veracode scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
5817
6643
  let title;
5818
6644
  const staticAnalysis = report["static-analysis"];
5819
6645
  if (staticAnalysis) {
@@ -5829,7 +6655,6 @@ async function convertVeracodeToHdf(input) {
5829
6655
  version: attr(report, "policy_version"),
5830
6656
  summary: attr(report, "policy_name")
5831
6657
  });
5832
- const targetName = attr(report, "app_name") || "Veracode Application";
5833
6658
  const timestamp = parseVeracodeTimestamp(firstBuildDate);
5834
6659
  return buildHdfResults({
5835
6660
  generatorName: "veracode-to-hdf",
@@ -5839,7 +6664,7 @@ async function convertVeracodeToHdf(input) {
5839
6664
  baselines: [baseline],
5840
6665
  components: [{
5841
6666
  name: targetName,
5842
- type: Copyright.Application
6667
+ type: TargetType.Application
5843
6668
  }],
5844
6669
  timestamp
5845
6670
  });
@@ -5910,7 +6735,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
5910
6735
  });
5911
6736
  }
5912
6737
  const codeDesc = cs.implementationStatus || "No implementation status provided";
5913
- const startTime = createdDateTime ? new Date(createdDateTime) : void 0;
6738
+ const startTime = (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5914
6739
  const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
5915
6740
  codeDesc,
5916
6741
  ...startTime ? { startTime } : {}
@@ -5957,7 +6782,7 @@ async function convertMsftSecureScoreToHdf(input) {
5957
6782
  }),
5958
6783
  components: [{
5959
6784
  name: `Azure Tenant: ${tenantId}`,
5960
- type: Copyright.CloudAccount,
6785
+ type: TargetType.CloudAccount,
5961
6786
  labels: {
5962
6787
  account: tenantId,
5963
6788
  provider: "azure"
@@ -5980,6 +6805,7 @@ async function convertMsftDefenderDevopsToHdf(input) {
5980
6805
  const { components, runEnrichments } = extractEnrichments(raw);
5981
6806
  const hdfJson = await convertSarifToHdf(input);
5982
6807
  const result = JSON.parse(hdfJson);
6808
+ synthesizeNoFindingsPlaceholders(result);
5983
6809
  applyEnrichments(result, components, runEnrichments);
5984
6810
  if (result.generator) result.generator.name = "msft-defender-devops-to-hdf";
5985
6811
  if (result.tool) result.tool.name = "Microsoft Defender for DevOps";
@@ -5994,7 +6820,7 @@ function extractEnrichments(raw) {
5994
6820
  seenRepos.add(vcp.repositoryUri);
5995
6821
  const target = {
5996
6822
  name: repoNameFromURI(vcp.repositoryUri),
5997
- type: Copyright.Repository,
6823
+ type: TargetType.Repository,
5998
6824
  url: vcp.repositoryUri,
5999
6825
  labels: {}
6000
6826
  };
@@ -6044,6 +6870,14 @@ function applyEnrichments(result, components, runEnrichments) {
6044
6870
  }
6045
6871
  }
6046
6872
  }
6873
+ function synthesizeNoFindingsPlaceholders(result) {
6874
+ const startTime = result.timestamp ?? /* @__PURE__ */ new Date();
6875
+ for (const baseline of result.baselines ?? []) {
6876
+ if (baseline.requirements && baseline.requirements.length > 0) continue;
6877
+ const tool = baseline.name;
6878
+ baseline.requirements = [buildNoFindingsRequirement(`${tool}-no-findings`, `Microsoft Defender for DevOps scanner "${tool}" ran and reported zero findings.`, startTime)];
6879
+ }
6880
+ }
6047
6881
  function repoNameFromURI(uri) {
6048
6882
  const parts = uri.split("/");
6049
6883
  return parts[parts.length - 1] || uri;
@@ -6083,7 +6917,7 @@ function extractSubscriptionID(resourcePath) {
6083
6917
  /**
6084
6918
  * Converts a group of assessments sharing an assessment ID into one EvaluatedRequirement.
6085
6919
  */
6086
- function buildRequirement(assessmentID, assessments) {
6920
+ function buildRequirement(assessmentID, assessments, scanTime) {
6087
6921
  const rep = assessments[0];
6088
6922
  const meta = rep.properties.metadata;
6089
6923
  const impact = IMPACT_MAPPING$1[meta.severity.toLowerCase()] ?? .5;
@@ -6104,7 +6938,7 @@ function buildRequirement(assessmentID, assessments) {
6104
6938
  label: "fix",
6105
6939
  data: meta.remediationDescription
6106
6940
  });
6107
- const results = assessments.map(buildResultFromAssessment);
6941
+ const results = assessments.map((a) => buildResultFromAssessment(a, scanTime));
6108
6942
  const req = createRequirement(assessmentID, rep.properties.displayName, descriptions, impact, results, { tags });
6109
6943
  req.verificationMethod = VerificationMethodEnum.Automated;
6110
6944
  return req;
@@ -6112,10 +6946,13 @@ function buildRequirement(assessmentID, assessments) {
6112
6946
  /**
6113
6947
  * Converts a single assessment into an HDF RequirementResult.
6114
6948
  */
6115
- function buildResultFromAssessment(a) {
6949
+ function buildResultFromAssessment(a, scanTime) {
6116
6950
  const status = mapStatus(a.properties.status.code);
6117
6951
  const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
6118
- return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, { codeDesc });
6952
+ return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, {
6953
+ codeDesc,
6954
+ startTime: scanTime
6955
+ });
6119
6956
  }
6120
6957
  /**
6121
6958
  * Converts Microsoft Defender for Cloud assessment output to HDF format.
@@ -6125,6 +6962,7 @@ function buildResultFromAssessment(a) {
6125
6962
  */
6126
6963
  async function convertMsftDefenderCloudToHdf(input) {
6127
6964
  validateInputSize(input, "msft-defender-cloud");
6965
+ const scanTime = /* @__PURE__ */ new Date();
6128
6966
  const resultsChecksum = await inputChecksum(input);
6129
6967
  const raw = parseJSON(input);
6130
6968
  if (!raw || typeof raw !== "object") throw new Error("Invalid Defender for Cloud structure: not a valid JSON object");
@@ -6139,22 +6977,24 @@ async function convertMsftDefenderCloudToHdf(input) {
6139
6977
  else groups.set(a.name, [a]);
6140
6978
  }
6141
6979
  const requirements = [];
6142
- for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments));
6980
+ for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments, scanTime));
6981
+ const subscriptionID = limitedAssessments.length > 0 ? extractSubscriptionID(limitedAssessments[0].id) : "";
6982
+ if (requirements.length === 0) {
6983
+ const targetName = subscriptionID || "Unknown";
6984
+ requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, scanTime));
6985
+ }
6143
6986
  const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
6144
6987
  const components = [];
6145
- if (limitedAssessments.length > 0) {
6146
- const subscriptionID = extractSubscriptionID(limitedAssessments[0].id);
6147
- if (subscriptionID) components.push({
6148
- name: `Azure Subscription ${subscriptionID}`,
6149
- type: Copyright.CloudAccount,
6150
- accountId: subscriptionID,
6151
- provider: "azure",
6152
- labels: {
6153
- account: subscriptionID,
6154
- provider: "azure"
6155
- }
6156
- });
6157
- }
6988
+ if (subscriptionID) components.push({
6989
+ name: `Azure Subscription ${subscriptionID}`,
6990
+ type: TargetType.CloudAccount,
6991
+ accountId: subscriptionID,
6992
+ provider: "azure",
6993
+ labels: {
6994
+ account: subscriptionID,
6995
+ provider: "azure"
6996
+ }
6997
+ });
6158
6998
  return buildHdfResults({
6159
6999
  generatorName: "msft-defender-cloud-to-hdf",
6160
7000
  converterVersion: "1.0.0",
@@ -6162,7 +7002,7 @@ async function convertMsftDefenderCloudToHdf(input) {
6162
7002
  toolFormat: "JSON",
6163
7003
  baselines: [baseline],
6164
7004
  components,
6165
- timestamp: /* @__PURE__ */ new Date()
7005
+ timestamp: scanTime
6166
7006
  });
6167
7007
  }
6168
7008
  //#endregion
@@ -6229,7 +7069,7 @@ function extractDeviceTarget(alert) {
6229
7069
  for (const ev of alert.evidence) if ((ev["@odata.type"] ?? "").includes("deviceEvidence") && ev.deviceDnsName) {
6230
7070
  const target = {
6231
7071
  name: ev.deviceDnsName,
6232
- type: Copyright.Host,
7072
+ type: TargetType.Host,
6233
7073
  labels: { provider: "azure" }
6234
7074
  };
6235
7075
  if (ev.deviceDnsName) target.fqdn = ev.deviceDnsName;
@@ -6239,7 +7079,7 @@ function extractDeviceTarget(alert) {
6239
7079
  }
6240
7080
  return {
6241
7081
  name: alert.tenantId ?? "unknown",
6242
- type: Copyright.CloudAccount,
7082
+ type: TargetType.CloudAccount,
6243
7083
  accountId: alert.tenantId,
6244
7084
  labels: {
6245
7085
  account: alert.tenantId ?? "",
@@ -6259,13 +7099,26 @@ function buildTags$1(alert) {
6259
7099
  return tags;
6260
7100
  }
6261
7101
  /**
7102
+ * Resolves a clean per-finding source timestamp, falling back to the conversion time.
7103
+ */
7104
+ function resolveStartTime(alert, scanTime) {
7105
+ const first = alert.firstActivityDateTime ? parseTimestamp(alert.firstActivityDateTime) : null;
7106
+ if (first) return first;
7107
+ const created = alert.createdDateTime ? parseTimestamp(alert.createdDateTime) : null;
7108
+ if (created) return created;
7109
+ return scanTime;
7110
+ }
7111
+ /**
6262
7112
  * Converts a single MDE alert to an HDF EvaluatedRequirement.
6263
7113
  */
6264
- function alertToRequirement(alert) {
7114
+ function alertToRequirement(alert, scanTime) {
6265
7115
  const impact = severityToImpact$1(alert.severity);
6266
7116
  const status = statusToResultStatus(alert.status, alert.classification);
6267
7117
  const codeDesc = formatEvidence(alert.evidence ?? []);
6268
- const results = [createResult(status, formatMessage(alert), { codeDesc })];
7118
+ const results = [createResult(status, formatMessage(alert), {
7119
+ codeDesc,
7120
+ startTime: resolveStartTime(alert, scanTime)
7121
+ })];
6269
7122
  const descriptions = [{
6270
7123
  label: "default",
6271
7124
  data: alert.description
@@ -6295,10 +7148,11 @@ async function convertMsftDefenderEndpointToHdf(input) {
6295
7148
  const response = parseJSON(input);
6296
7149
  if (!response || typeof response !== "object") throw new Error("Invalid Microsoft Defender for Endpoint structure: not a valid JSON object");
6297
7150
  if (!Array.isArray(response.value)) throw new Error("Invalid Microsoft Defender for Endpoint structure: missing or invalid value array");
7151
+ const scanTime = /* @__PURE__ */ new Date();
6298
7152
  const { items: limitedAlerts, truncated } = limitArray(response.value);
6299
7153
  /* v8 ignore next -- truncation only triggers with >100K items */
6300
7154
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedAlerts.length} alert items (original: ${response.value.length})`);
6301
- const requirements = limitedAlerts.map(alertToRequirement);
7155
+ const requirements = limitedAlerts.map((alert) => alertToRequirement(alert, scanTime));
6302
7156
  const seenTargets = /* @__PURE__ */ new Set();
6303
7157
  const components = [];
6304
7158
  for (const alert of limitedAlerts) {
@@ -6308,13 +7162,14 @@ async function convertMsftDefenderEndpointToHdf(input) {
6308
7162
  components.push(target);
6309
7163
  }
6310
7164
  }
7165
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", scanTime));
6311
7166
  return buildHdfResults({
6312
7167
  generatorName: "msft-defender-endpoint-to-hdf",
6313
7168
  converterVersion: "1.0.0",
6314
7169
  toolName: "Microsoft Defender for Endpoint",
6315
7170
  baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
6316
7171
  components,
6317
- timestamp: /* @__PURE__ */ new Date()
7172
+ timestamp: scanTime
6318
7173
  });
6319
7174
  }
6320
7175
  //#endregion
@@ -6348,7 +7203,7 @@ function wrap(value) {
6348
7203
  return { "#text": value };
6349
7204
  }
6350
7205
  /** Map HDF impact (0.0-1.0) to XCCDF severity string. */
6351
- function impactToSeverity$1(impact) {
7206
+ function impactToSeverity$2(impact) {
6352
7207
  if (impact >= .7) return "high";
6353
7208
  if (impact >= .4) return "medium";
6354
7209
  if (impact >= .1) return "low";
@@ -6402,7 +7257,7 @@ function buildRuleObj(req) {
6402
7257
  const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
6403
7258
  const rule = {
6404
7259
  [`${ATTR}id`]: ruleId,
6405
- [`${ATTR}severity`]: impactToSeverity$1(req.impact),
7260
+ [`${ATTR}severity`]: impactToSeverity$2(req.impact),
6406
7261
  [`${ATTR}selected`]: "true",
6407
7262
  title: wrap(req.title ?? req.id)
6408
7263
  };
@@ -6448,8 +7303,7 @@ function buildTestResultObj(hdfData, baseline) {
6448
7303
  [`${ATTR}idref`]: ruleIdRef,
6449
7304
  result: wrap(hdfStatusToXccdf(result.status))
6450
7305
  };
6451
- const startTime = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
6452
- rr[`${ATTR}time`] = startTime;
7306
+ rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
6453
7307
  if (result.message) rr.message = wrap(result.message);
6454
7308
  if (result.codeDesc) rr.check = {
6455
7309
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
@@ -6593,7 +7447,7 @@ function nistTagToControlId(tag) {
6593
7447
  * Converts a 0.0-1.0 impact value to an OSCAL severity string.
6594
7448
  * This is the reverse of extractRiskSeverity.
6595
7449
  */
6596
- function impactToSeverity(impact) {
7450
+ function impactToSeverity$1(impact) {
6597
7451
  if (impact >= .9) return "critical";
6598
7452
  if (impact >= .7) return "high";
6599
7453
  if (impact >= .4) return "moderate";
@@ -6748,7 +7602,7 @@ function requirementToFindingSet(req, timestamp) {
6748
7602
  let risk;
6749
7603
  if (req.impact > 0) {
6750
7604
  const riskUUID = crypto.randomUUID();
6751
- const severity = impactToSeverity(req.impact);
7605
+ const severity = impactToSeverity$1(req.impact);
6752
7606
  risk = {
6753
7607
  uuid: riskUUID,
6754
7608
  title: `Risk for ${req.id}`,
@@ -6879,7 +7733,7 @@ async function convertHdfToOscalPoam(input) {
6879
7733
  return JSON.stringify(doc, null, 2);
6880
7734
  }
6881
7735
  /**
6882
- * Converts parsed HdfAmendments to an OSCAL PlanOfActionAndMilestones.
7736
+ * Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
6883
7737
  */
6884
7738
  function amendmentsToPOAM(amendments) {
6885
7739
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -7079,6 +7933,1150 @@ async function convertIonchannelToHdf(input) {
7079
7933
  });
7080
7934
  }
7081
7935
  //#endregion
7936
+ //#region shared/typescript/vex/mapping.ts
7937
+ /** Canonical VEX status across OpenVEX / CSAF / CycloneDX. */
7938
+ const VexStatus = {
7939
+ NotAffected: "not_affected",
7940
+ Affected: "affected",
7941
+ Fixed: "fixed",
7942
+ UnderInvestigation: "under_investigation"
7943
+ };
7944
+ /**
7945
+ * Map an ecosystem-specific status string to the canonical VexStatus.
7946
+ * Returns undefined for values without a clean mapping — caller should warn
7947
+ * and skip, not guess.
7948
+ */
7949
+ function normalizeStatus(raw) {
7950
+ switch (raw.trim().toLowerCase()) {
7951
+ case "not_affected":
7952
+ case "known_not_affected":
7953
+ case "false_positive": return VexStatus.NotAffected;
7954
+ case "affected":
7955
+ case "known_affected":
7956
+ case "exploitable": return VexStatus.Affected;
7957
+ case "fixed":
7958
+ case "first_fixed":
7959
+ case "resolved":
7960
+ case "resolved_with_pedigree": return VexStatus.Fixed;
7961
+ case "under_investigation":
7962
+ case "in_triage": return VexStatus.UnderInvestigation;
7963
+ default: return;
7964
+ }
7965
+ }
7966
+ /**
7967
+ * Map an ecosystem-specific justification string to the canonical HDF
7968
+ * Justification enum. Returns undefined for unknown values; callers SHOULD
7969
+ * log unknown values rather than silently dropping (the schema spec wants
7970
+ * pass-through, but practically we expect the enum to be extended when a
7971
+ * new vocabulary is integrated rather than carrying raw labels
7972
+ * indefinitely).
7973
+ *
7974
+ * The HDF Justification enum (v3.2.x) covers:
7975
+ * - the original OpenVEX / CSAF VEX five values
7976
+ * - CycloneDX-specific reachability values (requires_*, protected_*)
7977
+ * that describe why a vulnerable code path is unreachable in the
7978
+ * deployed configuration.
7979
+ */
7980
+ function normalizeJustification(raw) {
7981
+ switch (raw.trim().toLowerCase()) {
7982
+ case "component_not_present":
7983
+ case "code_not_present": return Justification.ComponentNotPresent;
7984
+ case "vulnerable_code_not_present": return Justification.VulnerableCodeNotPresent;
7985
+ case "vulnerable_code_not_in_execute_path":
7986
+ case "code_not_reachable": return Justification.VulnerableCodeNotInExecutePath;
7987
+ case "vulnerable_code_cannot_be_controlled_by_adversary": return Justification.VulnerableCodeCannotBeControlledByAdversary;
7988
+ case "inline_mitigations_already_exist":
7989
+ case "protected_by_mitigating_control": return Justification.InlineMitigationsAlreadyExist;
7990
+ case "requires_configuration": return Justification.RequiresConfiguration;
7991
+ case "requires_dependency": return Justification.RequiresDependency;
7992
+ case "requires_environment": return Justification.RequiresEnvironment;
7993
+ case "protected_by_compiler": return Justification.ProtectedByCompiler;
7994
+ case "protected_at_runtime": return Justification.ProtectedAtRuntime;
7995
+ case "protected_at_perimeter": return Justification.ProtectedAtPerimeter;
7996
+ default: return;
7997
+ }
7998
+ }
7999
+ /**
8000
+ * Render an HDF Justification value as the CycloneDX-native vocabulary.
8001
+ * CycloneDX uses short-form names (code_not_present, code_not_reachable,
8002
+ * protected_by_mitigating_control) for the three justifications shared
8003
+ * with OpenVEX/CSAF, and shares the six CycloneDX-specific reachability
8004
+ * values verbatim.
8005
+ *
8006
+ * Returns undefined when the HDF value has no equivalent in CycloneDX's
8007
+ * enum (vulnerable_code_not_present and
8008
+ * vulnerable_code_cannot_be_controlled_by_adversary). Callers should
8009
+ * omit the justification field in that case.
8010
+ */
8011
+ function justificationForCycloneDX(j) {
8012
+ switch (j) {
8013
+ case Justification.ComponentNotPresent: return "code_not_present";
8014
+ case Justification.VulnerableCodeNotInExecutePath: return "code_not_reachable";
8015
+ case Justification.InlineMitigationsAlreadyExist: return "protected_by_mitigating_control";
8016
+ case Justification.RequiresConfiguration:
8017
+ case Justification.RequiresDependency:
8018
+ case Justification.RequiresEnvironment:
8019
+ case Justification.ProtectedByCompiler:
8020
+ case Justification.ProtectedAtRuntime:
8021
+ case Justification.ProtectedAtPerimeter: return String(j);
8022
+ default: return;
8023
+ }
8024
+ }
8025
+ /**
8026
+ * Return the amendment shape an importer should produce for a canonical VEX
8027
+ * status. Returns undefined for "affected" and "under_investigation" — those
8028
+ * are informational; the consumer creates an amendment later if they act.
8029
+ */
8030
+ function importTargetFor(status) {
8031
+ switch (status) {
8032
+ case VexStatus.NotAffected: return {
8033
+ overrideType: OverrideType.FalsePositive,
8034
+ status: ResultStatus.Passed,
8035
+ setJustification: true,
8036
+ poamActionTemplate: ""
8037
+ };
8038
+ case VexStatus.Fixed: return {
8039
+ overrideType: OverrideType.Poam,
8040
+ status: ResultStatus.Failed,
8041
+ setJustification: false,
8042
+ poamActionTemplate: "vendor reports fix; apply and re-scan to verify"
8043
+ };
8044
+ case VexStatus.Affected:
8045
+ case VexStatus.UnderInvestigation: return;
8046
+ /* c8 ignore next 2 — every VexStatus has a case above */
8047
+ default: return;
8048
+ }
8049
+ }
8050
+ /**
8051
+ * Return the canonical VEX status an exporter should emit for an HDF
8052
+ * override. Returns undefined when no VEX statement should be emitted —
8053
+ * the consumer has not acted, and VEX requires a deliberate statement.
8054
+ *
8055
+ * `allMilestonesCompleted` and `closureChained` are consulted only for
8056
+ * POA&M overrides. Closure is signalled by BOTH flags being true; either
8057
+ * alone is insufficient.
8058
+ */
8059
+ function exportStatusFor(override, allMilestonesCompleted, closureChained) {
8060
+ if (!override) return;
8061
+ if (override.justification) return VexStatus.NotAffected;
8062
+ switch (override.type) {
8063
+ case OverrideType.FalsePositive:
8064
+ case OverrideType.Attestation:
8065
+ case OverrideType.Inherited: return VexStatus.NotAffected;
8066
+ case OverrideType.Waiver:
8067
+ case OverrideType.RiskAdjustment:
8068
+ case OverrideType.OperationalRequirement: return VexStatus.Affected;
8069
+ case OverrideType.Poam: return allMilestonesCompleted && closureChained ? VexStatus.Fixed : VexStatus.Affected;
8070
+ /* c8 ignore next 2 — every OverrideType has a case above */
8071
+ default: return;
8072
+ }
8073
+ }
8074
+ const ECOSYSTEM_FROM_PURL_TYPE = {
8075
+ npm: Ecosystem.Npm,
8076
+ pypi: Ecosystem.Pypi,
8077
+ rpm: Ecosystem.RPM,
8078
+ deb: Ecosystem.Deb,
8079
+ maven: Ecosystem.Maven,
8080
+ gem: Ecosystem.Gem,
8081
+ nuget: Ecosystem.Nuget,
8082
+ golang: Ecosystem.Go,
8083
+ go: Ecosystem.Go,
8084
+ cargo: Ecosystem.Cargo
8085
+ };
8086
+ /**
8087
+ * Build an AffectedPackage from a single product identifier string emitted
8088
+ * by a VEX format. Recognizes PURLs and CPE 2.3 strings; returns undefined
8089
+ * for opaque identifiers (importer should drop the entry — schema additions
8090
+ * forbid fabricating name+version).
8091
+ */
8092
+ function affectedPackageFromIdentifier(identifier) {
8093
+ if (!identifier) return void 0;
8094
+ if (identifier.startsWith("pkg:")) {
8095
+ const parsed = parsePurl(identifier);
8096
+ if (parsed) {
8097
+ const pkg = { purl: identifier };
8098
+ /* c8 ignore next 2 — parsePurl populates name/version when the
8099
+ identifier was a well-formed purl; the absent branches require a
8100
+ malformed-but-prefixed input that loses these fields without
8101
+ triggering the outer null return. Defensive. */
8102
+ if (parsed.name) pkg.name = parsed.name;
8103
+ if (parsed.version) pkg.version = parsed.version;
8104
+ pkg.ecosystem = ECOSYSTEM_FROM_PURL_TYPE[parsed.type] ?? Ecosystem.Generic;
8105
+ return pkg;
8106
+ }
8107
+ return { purl: identifier };
8108
+ }
8109
+ if (identifier.startsWith("cpe:2.3:")) return { cpe: identifier };
8110
+ }
8111
+ /**
8112
+ * Build a unique list of AffectedPackage entries from a sequence of product
8113
+ * identifier strings. Empty / unresolvable identifiers are dropped; duplicate
8114
+ * purls/cpes/names are collapsed.
8115
+ */
8116
+ function affectedPackagesFromIdentifiers(identifiers) {
8117
+ const out = [];
8118
+ const seen = /* @__PURE__ */ new Set();
8119
+ for (const id of identifiers) {
8120
+ const pkg = affectedPackageFromIdentifier(id);
8121
+ if (!pkg) continue;
8122
+ /* c8 ignore next — affectedPackageFromIdentifier always emits either
8123
+ purl or cpe, so the name fallback branch isn't reachable through
8124
+ this caller. Kept for safety if a hand-built AffectedPackage flows
8125
+ through a future caller. */
8126
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
8127
+ if (seen.has(key)) continue;
8128
+ seen.add(key);
8129
+ out.push(pkg);
8130
+ }
8131
+ return out;
8132
+ }
8133
+ /**
8134
+ * Render an AffectedPackage as a single identifier string suitable for
8135
+ * round-tripping into a VEX format. Prefers purl > cpe > name@version.
8136
+ * Returns undefined when nothing identifying is set.
8137
+ */
8138
+ function affectedPackageToIdentifier(pkg) {
8139
+ if (pkg.purl) return pkg.purl;
8140
+ if (pkg.cpe) return pkg.cpe;
8141
+ if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
8142
+ if (pkg.name) return pkg.name;
8143
+ }
8144
+ /**
8145
+ * Build an HDF Evidence entry pointing at the upstream VEX document. Used by
8146
+ * importers to preserve provenance even though we lose the structured
8147
+ * statement_id. Returns undefined when sourceURI is empty — don't fabricate.
8148
+ */
8149
+ function supplierEvidence(sourceURI, description) {
8150
+ if (!sourceURI.trim()) return;
8151
+ return {
8152
+ type: EvidenceType.URL,
8153
+ data: sourceURI,
8154
+ description: description?.trim() ? description : "Upstream VEX statement"
8155
+ };
8156
+ }
8157
+ //#endregion
8158
+ //#region converters/openvex-to-hdf/typescript/converter.ts
8159
+ /**
8160
+ * OpenVEX to HDF Amendments converter.
8161
+ *
8162
+ * VEX statements are consumer-attached context for CVE findings. The act of
8163
+ * attaching IS the amendment, so this converter emits HDF Amendments rather
8164
+ * than HDF Results.
8165
+ *
8166
+ * VEX 'fixed' is an abstract supplier claim; the assessed system has not
8167
+ * been verified to carry the fix. Imports therefore become open POA&M
8168
+ * overrides (status pinned to failed pending re-scan), not status flips.
8169
+ *
8170
+ * Spec: https://github.com/openvex/spec
8171
+ */
8172
+ /** One year in milliseconds. VEX statements are re-evaluated as new info
8173
+ * arrives; one year is a defensive default consistent with the
8174
+ * no-permanent-amendment rule on Standalone_Override. */
8175
+ const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
8176
+ async function convertOpenVexToHdf(input, converterVersion) {
8177
+ validateInputSize(input, "openvex-to-hdf");
8178
+ const doc = parseJSON(input);
8179
+ const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
8180
+ const overrides = [];
8181
+ for (const stmt of doc.statements ?? []) {
8182
+ const override = statementToOverride(stmt, doc, docTime);
8183
+ if (override) overrides.push(override);
8184
+ }
8185
+ if (overrides.length === 0) throw new Error("openvex-to-hdf: VEX document contains no actionable statements (all 'affected' or 'under_investigation'); no amendment to write");
8186
+ return {
8187
+ name: doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements",
8188
+ description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
8189
+ overrides,
8190
+ appliedBy: identityFor$1(doc.author, doc.role),
8191
+ generator: {
8192
+ name: "openvex-to-hdf",
8193
+ version: converterVersion
8194
+ },
8195
+ integrity: await inputIntegrity(input)
8196
+ };
8197
+ }
8198
+ function statementToOverride(stmt, doc, docTime) {
8199
+ const canonical = normalizeStatus(stmt.status ?? "");
8200
+ if (!canonical) return void 0;
8201
+ const target = importTargetFor(canonical);
8202
+ if (!target) return void 0;
8203
+ const requirementId = stmt.vulnerability?.name ?? stmt.vulnerability?.["@id"] ?? "";
8204
+ if (!requirementId) return void 0;
8205
+ const stmtTime = (stmt.timestamp ? parseTimestamp(stmt.timestamp) : null) ?? docTime;
8206
+ const author = stmt.author ?? doc.author ?? "";
8207
+ const expiresAt = new Date(stmtTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$2);
8208
+ const override = {
8209
+ type: target.overrideType,
8210
+ requirementId,
8211
+ appliedAt: stmtTime,
8212
+ expiresAt,
8213
+ appliedBy: identityFor$1(author, doc.role),
8214
+ reason: buildReason$2(stmt, target.poamActionTemplate)
8215
+ };
8216
+ const affectedPackages = affectedPackagesFromIdentifiers((stmt.products ?? []).map((p) => p["@id"]).filter((id) => Boolean(id)));
8217
+ if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
8218
+ if (target.status !== void 0) override.status = target.status;
8219
+ if (target.setJustification && stmt.justification) {
8220
+ const j = normalizeJustification(stmt.justification);
8221
+ if (j) override.justification = j;
8222
+ }
8223
+ const ev = supplierEvidence(doc["@id"] ?? "", "OpenVEX document");
8224
+ if (ev) override.evidence = [ev];
8225
+ if (target.overrideType === OverrideType.Poam) override.milestones = [{
8226
+ description: target.poamActionTemplate,
8227
+ status: MilestoneStatus.Pending,
8228
+ estimatedCompletion: expiresAt
8229
+ }];
8230
+ return override;
8231
+ }
8232
+ function buildReason$2(stmt, poamTemplate) {
8233
+ const parts = [];
8234
+ if (stmt.impact_statement) parts.push(stmt.impact_statement);
8235
+ if (stmt.action_statement) parts.push(stmt.action_statement);
8236
+ if (parts.length === 0) return poamTemplate || `Imported from OpenVEX status "${stmt.status ?? ""}"`;
8237
+ return parts.join("\n");
8238
+ }
8239
+ function identityFor$1(author, role) {
8240
+ if (!author) return {
8241
+ type: IdentityType.System,
8242
+ identifier: "openvex-import"
8243
+ };
8244
+ const id = {
8245
+ type: author.includes("@") ? IdentityType.Email : IdentityType.Simple,
8246
+ identifier: author
8247
+ };
8248
+ if (role) id.description = role;
8249
+ return id;
8250
+ }
8251
+ function truncateId(id) {
8252
+ const MAX = 80;
8253
+ return id.length <= MAX ? id : `${id.slice(0, MAX)}...`;
8254
+ }
8255
+ //#endregion
8256
+ //#region converters/csaf-vex-to-hdf/typescript/converter.ts
8257
+ /**
8258
+ * CSAF VEX to HDF Amendments converter.
8259
+ *
8260
+ * CSAF (OASIS Common Security Advisory Framework) VEX profile is the
8261
+ * vendor-advisory format. Per the amendment-output pattern (Step 4f),
8262
+ * 'fixed' becomes an open POA&M, not a status flip — supplier claim is
8263
+ * not assessed-system evidence.
8264
+ *
8265
+ * Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
8266
+ */
8267
+ const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
8268
+ async function convertCsafVexToHdf(input, converterVersion) {
8269
+ validateInputSize(input, "csaf-vex-to-hdf");
8270
+ const doc = parseJSON(input);
8271
+ if (doc.document?.category !== "csaf_vex") throw new Error(`csaf-vex-to-hdf: document.category is ${JSON.stringify(doc.document?.category)}; only 'csaf_vex' is supported`);
8272
+ const dm = doc.document;
8273
+ const tracking = dm.tracking ?? {};
8274
+ const publisher = dm.publisher ?? {};
8275
+ const docTime = (tracking.current_release_date ? parseTimestamp(tracking.current_release_date) : null) ?? /* @__PURE__ */ new Date();
8276
+ const productLookup = buildProductLookup$1(doc.product_tree);
8277
+ const overrides = [];
8278
+ for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
8279
+ if (overrides.length === 0) throw new Error("csaf-vex-to-hdf: CSAF VEX document contains no actionable statements (only affected/under_investigation/recommended); no amendment to write");
8280
+ const publisherName = publisher.name ?? "";
8281
+ return {
8282
+ name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
8283
+ description: `Imported VEX advisory ${tracking.id ?? ""}`,
8284
+ overrides,
8285
+ appliedBy: identityFor(publisher),
8286
+ generator: {
8287
+ name: "csaf-vex-to-hdf",
8288
+ version: converterVersion
8289
+ },
8290
+ integrity: await inputIntegrity(input),
8291
+ version: tracking.version
8292
+ };
8293
+ }
8294
+ function vulnerabilityToOverrides(vuln, publisher, tracking, docTime, productLookup) {
8295
+ if (!vuln.cve) return [];
8296
+ const ps = vuln.product_status;
8297
+ if (!ps) return [];
8298
+ const out = [];
8299
+ if (ps.known_not_affected?.length) {
8300
+ const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.NotAffected, ps.known_not_affected, productLookup);
8301
+ if (o) out.push(o);
8302
+ }
8303
+ const fixedProducts = [...ps.fixed ?? [], ...ps.first_fixed ?? []];
8304
+ if (fixedProducts.length > 0) {
8305
+ const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.Fixed, fixedProducts, productLookup);
8306
+ if (o) out.push(o);
8307
+ }
8308
+ return out;
8309
+ }
8310
+ function buildOverride(vuln, publisher, tracking, docTime, canonical, products, productLookup) {
8311
+ const target = importTargetFor(canonical);
8312
+ if (!target) return void 0;
8313
+ const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$1);
8314
+ const override = {
8315
+ type: target.overrideType,
8316
+ requirementId: vuln.cve,
8317
+ appliedAt: docTime,
8318
+ expiresAt,
8319
+ appliedBy: identityFor(publisher),
8320
+ reason: buildReason$1(vuln, products)
8321
+ };
8322
+ const affectedPackages = resolveAffectedPackages(products, productLookup);
8323
+ if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
8324
+ if (target.status !== void 0) override.status = target.status;
8325
+ if (target.setJustification) {
8326
+ const j = pickJustification(vuln, products);
8327
+ if (j) override.justification = j;
8328
+ }
8329
+ const evidence = [];
8330
+ const advisoryEv = supplierEvidence(advisoryURI(publisher, tracking), "CSAF VEX advisory");
8331
+ if (advisoryEv) evidence.push(advisoryEv);
8332
+ for (const r of vuln.references ?? []) {
8333
+ if (!r.url) continue;
8334
+ const ev = supplierEvidence(r.url, r.summary ?? r.category ?? "");
8335
+ if (ev) evidence.push(ev);
8336
+ }
8337
+ if (evidence.length > 0) override.evidence = evidence;
8338
+ if (target.overrideType === OverrideType.Poam) override.milestones = [{
8339
+ description: firstActionRemediation(vuln, products) || target.poamActionTemplate,
8340
+ status: MilestoneStatus.Pending,
8341
+ estimatedCompletion: expiresAt
8342
+ }];
8343
+ return override;
8344
+ }
8345
+ function pickJustification(vuln, products) {
8346
+ const scope = new Set(products);
8347
+ for (const f of vuln.flags ?? []) {
8348
+ if (!overlap(f.product_ids ?? [], scope)) continue;
8349
+ if (!f.label) continue;
8350
+ const j = normalizeJustification(f.label);
8351
+ if (j) return j;
8352
+ }
8353
+ }
8354
+ function firstActionRemediation(vuln, products) {
8355
+ const scope = new Set(products);
8356
+ for (const r of vuln.remediations ?? []) {
8357
+ const ids = r.product_ids;
8358
+ if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
8359
+ if ((r.category === "vendor_fix" || r.category === "mitigation" || r.category === "workaround") && r.details) return r.details;
8360
+ }
8361
+ return "";
8362
+ }
8363
+ function buildReason$1(vuln, products) {
8364
+ const parts = [];
8365
+ const scope = new Set(products);
8366
+ for (const n of vuln.notes ?? []) if (n.category === "description" && n.text) parts.push(n.text);
8367
+ for (const t of vuln.threats ?? []) {
8368
+ if (!t.details) continue;
8369
+ const ids = t.product_ids;
8370
+ if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
8371
+ parts.push(t.details);
8372
+ }
8373
+ if (parts.length === 0) return `Imported from CSAF VEX (${vuln.cve ?? "unknown CVE"})`;
8374
+ return parts.join("\n");
8375
+ }
8376
+ /**
8377
+ * Walk a CSAF product_tree and build a lookup from product_id to a
8378
+ * structured AffectedPackage. Prefers product_identification_helper.purl,
8379
+ * then .cpe, then the full_product_name's `name` (used for matching only
8380
+ * — name+version is hard to derive from CSAF reliably, so we drop entries
8381
+ * with no purl and no cpe).
8382
+ */
8383
+ function buildProductLookup$1(tree) {
8384
+ const lookup = /* @__PURE__ */ new Map();
8385
+ if (!tree) return lookup;
8386
+ if (tree.full_product_names) for (const fp of tree.full_product_names) registerProduct(fp, lookup);
8387
+ if (tree.branches) walkBranches(tree.branches, lookup);
8388
+ return lookup;
8389
+ }
8390
+ function walkBranches(branches, lookup, ctx = {}) {
8391
+ for (const b of branches) {
8392
+ const next = { ...ctx };
8393
+ /* c8 ignore next 2 — falsy branches require a malformed CSAF branch
8394
+ node (category set but name empty) which we don't fixture. */
8395
+ if (b.category === "product_name" && b.name) next.productName = b.name;
8396
+ if (b.category === "product_version" && b.name) next.version = b.name;
8397
+ if (b.product) registerProduct(b.product, lookup, next);
8398
+ if (b.branches) walkBranches(b.branches, lookup, next);
8399
+ }
8400
+ }
8401
+ function registerProduct(fp, lookup, ctx = {}) {
8402
+ /* c8 ignore next — defensive against a malformed CSAF full_product_name
8403
+ missing its required product_id field; not fixtured. */
8404
+ if (!fp.product_id) return;
8405
+ const helper = fp.product_identification_helper;
8406
+ if (helper?.purl) {
8407
+ const pkg = affectedPackageFromIdentifier(helper.purl);
8408
+ /* c8 ignore next 4 — affectedPackageFromIdentifier always returns a
8409
+ non-null value for an input that starts with 'pkg:' (preserves
8410
+ malformed input as purl-only). The null branch is unreachable from
8411
+ this callsite. */
8412
+ if (pkg) {
8413
+ lookup.set(fp.product_id, pkg);
8414
+ return;
8415
+ }
8416
+ }
8417
+ if (helper?.cpe) {
8418
+ const pkg = affectedPackageFromIdentifier(helper.cpe);
8419
+ /* c8 ignore next 4 — same as above; identifiers starting with
8420
+ 'cpe:2.3:' always produce a cpe-only AffectedPackage. */
8421
+ if (pkg) {
8422
+ lookup.set(fp.product_id, pkg);
8423
+ return;
8424
+ }
8425
+ }
8426
+ if (ctx.productName && ctx.version) lookup.set(fp.product_id, {
8427
+ name: ctx.productName,
8428
+ version: ctx.version,
8429
+ ecosystem: Ecosystem.Generic
8430
+ });
8431
+ }
8432
+ function resolveAffectedPackages(productIds, lookup) {
8433
+ const out = [];
8434
+ const seenKeys = /* @__PURE__ */ new Set();
8435
+ for (const id of productIds) {
8436
+ const pkg = lookup.get(id);
8437
+ if (!pkg) continue;
8438
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
8439
+ if (seenKeys.has(key)) continue;
8440
+ seenKeys.add(key);
8441
+ out.push(pkg);
8442
+ }
8443
+ return out;
8444
+ }
8445
+ function identityFor(p) {
8446
+ if (!p.name) return {
8447
+ type: IdentityType.System,
8448
+ identifier: "csaf-vex-import"
8449
+ };
8450
+ const id = {
8451
+ type: IdentityType.Simple,
8452
+ identifier: p.name
8453
+ };
8454
+ if (p.category) id.description = p.category;
8455
+ return id;
8456
+ }
8457
+ function advisoryURI(publisher, tracking) {
8458
+ /* c8 ignore next */
8459
+ const id = tracking.id ?? "";
8460
+ const ns = publisher.namespace;
8461
+ if (ns && id) return `${ns.replace(/\/+$/, "")}/${id}`;
8462
+ return id;
8463
+ }
8464
+ function overlap(ids, scope) {
8465
+ return ids.some((id) => scope.has(id));
8466
+ }
8467
+ //#endregion
8468
+ //#region converters/cyclonedx-vex-to-hdf/typescript/converter.ts
8469
+ /**
8470
+ * CycloneDX VEX to HDF Amendments converter.
8471
+ *
8472
+ * CycloneDX VEX is not a separate format — it's a CycloneDX BOM whose
8473
+ * vulnerabilities[] carry an `analysis` object. CycloneDX-specific
8474
+ * justifications without an HDF equivalent (requires_configuration,
8475
+ * protected_by_compiler, etc.) are preserved verbatim in the reason
8476
+ * field via the shared helper's unknown-value passthrough.
8477
+ *
8478
+ * Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
8479
+ */
8480
+ const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
8481
+ async function convertCyclonedxVexToHdf(input, converterVersion) {
8482
+ validateInputSize(input, "cyclonedx-vex-to-hdf");
8483
+ const bom = parseJSON(input);
8484
+ if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
8485
+ const docTime = (bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) : null) ?? /* @__PURE__ */ new Date();
8486
+ const productLookup = buildProductLookup(bom);
8487
+ const publisher = publisherIdentityOrDefault(bom);
8488
+ const overrides = [];
8489
+ for (const v of bom.vulnerabilities ?? []) {
8490
+ const o = vulnerabilityToOverride(v, productLookup, docTime, publisher);
8491
+ if (o) overrides.push(o);
8492
+ }
8493
+ if (overrides.length === 0) throw new Error("cyclonedx-vex-to-hdf: BOM contains no actionable VEX statements (only exploitable/in_triage or no analysis); no amendment to write");
8494
+ const publisherName = publisher.identifier;
8495
+ return {
8496
+ name: publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements",
8497
+ description: bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX",
8498
+ overrides,
8499
+ appliedBy: publisher,
8500
+ generator: {
8501
+ name: "cyclonedx-vex-to-hdf",
8502
+ version: converterVersion
8503
+ },
8504
+ integrity: await inputIntegrity(input)
8505
+ };
8506
+ }
8507
+ function vulnerabilityToOverride(v, productLookup, docTime, publisher) {
8508
+ if (!v.id || !v.analysis?.state) return void 0;
8509
+ const canonical = normalizeStatus(v.analysis.state);
8510
+ if (!canonical) return void 0;
8511
+ const target = importTargetFor(canonical);
8512
+ if (!target) return void 0;
8513
+ const affectedPackages = affectedPackagesForVuln(v, productLookup);
8514
+ const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS);
8515
+ const override = {
8516
+ type: target.overrideType,
8517
+ requirementId: v.id,
8518
+ appliedAt: docTime,
8519
+ expiresAt,
8520
+ appliedBy: publisher,
8521
+ reason: buildReason(v)
8522
+ };
8523
+ if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
8524
+ if (target.status !== void 0) override.status = target.status;
8525
+ if (target.setJustification && v.analysis.justification) {
8526
+ const j = normalizeJustification(v.analysis.justification);
8527
+ if (j) override.justification = j;
8528
+ }
8529
+ const evidence = [];
8530
+ if (v.source?.url) {
8531
+ const ev = supplierEvidence(v.source.url, v.source.name ?? "");
8532
+ if (ev) evidence.push(ev);
8533
+ }
8534
+ for (const r of v.references ?? []) {
8535
+ if (!r.source?.url) continue;
8536
+ const ev = supplierEvidence(r.source.url, r.source.name ?? "");
8537
+ if (ev) evidence.push(ev);
8538
+ }
8539
+ if (evidence.length > 0) override.evidence = evidence;
8540
+ if (target.overrideType === OverrideType.Poam) override.milestones = [{
8541
+ description: firstActionFromResponse(v.analysis.response ?? []) || target.poamActionTemplate,
8542
+ status: MilestoneStatus.Pending,
8543
+ estimatedCompletion: expiresAt
8544
+ }];
8545
+ return override;
8546
+ }
8547
+ function buildReason(v) {
8548
+ const parts = [];
8549
+ if (v.description) parts.push(v.description);
8550
+ if (v.analysis?.detail) parts.push(v.analysis.detail);
8551
+ return parts.join("\n");
8552
+ }
8553
+ /**
8554
+ * Resolve CycloneDX affects[].ref entries to structured AffectedPackage
8555
+ * entries. Looks up the bom-ref in the component table to recover purl,
8556
+ * name/version. Opaque bom-refs with no component-table match are dropped
8557
+ * (the schema forbids fabricating name+version, and bom-refs aren't
8558
+ * portable identifiers outside their source BOM).
8559
+ */
8560
+ function affectedPackagesForVuln(v, lookup) {
8561
+ const out = [];
8562
+ const seenKeys = /* @__PURE__ */ new Set();
8563
+ for (const a of v.affects ?? []) {
8564
+ if (!a.ref) continue;
8565
+ const comp = lookup.get(a.ref);
8566
+ const pkg = comp ? affectedPackageFromComponent(comp) : affectedPackageFromIdentifier(a.ref);
8567
+ if (!pkg) continue;
8568
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
8569
+ if (seenKeys.has(key)) continue;
8570
+ seenKeys.add(key);
8571
+ out.push(pkg);
8572
+ }
8573
+ return out;
8574
+ }
8575
+ /**
8576
+ * Build an AffectedPackage from a CycloneDX Component. Prefers structured
8577
+ * purl decomposition via the shared helper (so name/version/ecosystem are
8578
+ * also populated when the purl is parseable); falls back to the component's
8579
+ * name+version when no purl is set. Returns undefined when the component
8580
+ * carries neither a purl nor a name+version pair (schema requires at least
8581
+ * name+version+ecosystem, purl, or cpe).
8582
+ */
8583
+ function affectedPackageFromComponent(c) {
8584
+ if (c.purl) return affectedPackageFromIdentifier(c.purl);
8585
+ if (c.name && c.version) return {
8586
+ name: c.name,
8587
+ version: c.version,
8588
+ ecosystem: Ecosystem.Generic
8589
+ };
8590
+ }
8591
+ function buildProductLookup(bom) {
8592
+ const lookup = /* @__PURE__ */ new Map();
8593
+ const root = bom.metadata?.component;
8594
+ if (root?.["bom-ref"]) lookup.set(root["bom-ref"], root);
8595
+ for (const c of bom.components ?? []) if (c["bom-ref"]) lookup.set(c["bom-ref"], c);
8596
+ return lookup;
8597
+ }
8598
+ function firstActionFromResponse(resp) {
8599
+ for (const r of resp) switch (r.trim().toLowerCase()) {
8600
+ case "update": return "Apply vendor update and re-scan to verify.";
8601
+ case "rollback": return "Roll back to the unaffected version and re-scan to verify.";
8602
+ case "workaround_available": return "Apply the documented workaround.";
8603
+ }
8604
+ return "";
8605
+ }
8606
+ function publisherIdentityOrDefault(bom) {
8607
+ for (const a of bom.metadata?.authors ?? []) {
8608
+ if (a.email) return {
8609
+ type: IdentityType.Email,
8610
+ identifier: a.email
8611
+ };
8612
+ if (a.name) return {
8613
+ type: IdentityType.Simple,
8614
+ identifier: a.name
8615
+ };
8616
+ }
8617
+ for (const t of bom.metadata?.tools ?? []) {
8618
+ const ident = [t.vendor, t.name].filter(Boolean).join(" ").trim();
8619
+ if (ident) return {
8620
+ type: IdentityType.System,
8621
+ identifier: ident
8622
+ };
8623
+ }
8624
+ return {
8625
+ type: IdentityType.System,
8626
+ identifier: "cyclonedx-vex-import"
8627
+ };
8628
+ }
8629
+ //#endregion
8630
+ //#region converters/hdf-to-csaf-vex/typescript/converter.ts
8631
+ /**
8632
+ * HDF Amendments to CSAF VEX (csaf_vex profile) converter.
8633
+ *
8634
+ * Reverse direction of csaf-vex-to-hdf. Intentionally partial-fidelity:
8635
+ * fields the shared VEX mapping considers consumer-action-bearing survive
8636
+ * round-trip; the rest collapse into the available CSAF fields.
8637
+ */
8638
+ const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
8639
+ const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
8640
+ const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
8641
+ function convertHdfToCsafVex(input, converterVersion) {
8642
+ validateInputSize(input, "hdf-to-csaf-vex");
8643
+ const amendments = parseJSON(input);
8644
+ const groups = groupByCVE(amendments.overrides ?? []);
8645
+ const productSet = /* @__PURE__ */ new Map();
8646
+ const vulnerabilities = [];
8647
+ for (const group of groups) {
8648
+ const v = buildVulnerability(group);
8649
+ if (!v) continue;
8650
+ vulnerabilities.push(v);
8651
+ for (const p of productIDsForGroup(group)) productSet.set(p, true);
8652
+ }
8653
+ if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
8654
+ const products = [...productSet.keys()].sort();
8655
+ const doc = buildDocument(amendments, converterVersion);
8656
+ doc.vulnerabilities = vulnerabilities;
8657
+ doc.product_tree.full_product_names = products.length > 0 ? products.map((p) => ({
8658
+ name: p,
8659
+ product_id: p
8660
+ })) : [{
8661
+ name: DEFAULT_PRODUCT_ID$2,
8662
+ product_id: DEFAULT_PRODUCT_ID$2
8663
+ }];
8664
+ return JSON.stringify(doc, null, 2);
8665
+ }
8666
+ function groupByCVE(overrides) {
8667
+ const groups = /* @__PURE__ */ new Map();
8668
+ for (const o of overrides) {
8669
+ if (!CVE_ID_PATTERN$2.test(o.requirementId)) continue;
8670
+ let g = groups.get(o.requirementId);
8671
+ if (!g) {
8672
+ g = {
8673
+ cve: o.requirementId,
8674
+ overrides: []
8675
+ };
8676
+ groups.set(o.requirementId, g);
8677
+ }
8678
+ g.overrides.push(o);
8679
+ }
8680
+ return [...groups.values()].sort((a, b) => a.cve.localeCompare(b.cve));
8681
+ }
8682
+ function productIDsForGroup(group) {
8683
+ const seen = /* @__PURE__ */ new Set();
8684
+ for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
8685
+ return [...seen].sort();
8686
+ }
8687
+ function productIDsFor$1(o) {
8688
+ if (o.affectedPackages && o.affectedPackages.length > 0) {
8689
+ const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
8690
+ if (ids.length > 0) return ids;
8691
+ }
8692
+ if (o.componentRef) return [o.componentRef];
8693
+ const m = PRODUCTS_LINE$2.exec(o.reason ?? "");
8694
+ if (m && m[1]) {
8695
+ const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
8696
+ if (parts.length > 0) return parts;
8697
+ }
8698
+ return [DEFAULT_PRODUCT_ID$2];
8699
+ }
8700
+ function stripProductsLine$1(reason) {
8701
+ return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
8702
+ }
8703
+ function allMilestonesCompleted$2(o) {
8704
+ if (!o.milestones || o.milestones.length === 0) return false;
8705
+ return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
8706
+ }
8707
+ function buildVulnerability(group) {
8708
+ const v = { cve: group.cve };
8709
+ const status = {};
8710
+ let emitted = false;
8711
+ for (const o of group.overrides) {
8712
+ const pids = productIDsFor$1(o);
8713
+ let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
8714
+ if (!canonical) continue;
8715
+ if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
8716
+ if (canonical === VexStatus.NotAffected) {
8717
+ status.known_not_affected = (status.known_not_affected ?? []).concat(pids);
8718
+ if (o.justification) {
8719
+ v.flags = v.flags ?? [];
8720
+ v.flags.push({
8721
+ label: String(o.justification),
8722
+ product_ids: pids,
8723
+ date: formatTimestampSeconds(new Date(o.appliedAt))
8724
+ });
8725
+ }
8726
+ emitted = true;
8727
+ } else if (canonical === VexStatus.Fixed) {
8728
+ status.fixed = (status.fixed ?? []).concat(pids);
8729
+ for (const m of o.milestones ?? []) {
8730
+ if (!m.description) continue;
8731
+ v.remediations = v.remediations ?? [];
8732
+ v.remediations.push({
8733
+ category: "vendor_fix",
8734
+ details: m.description,
8735
+ product_ids: pids
8736
+ });
8737
+ }
8738
+ emitted = true;
8739
+ } else if (canonical === VexStatus.Affected) {
8740
+ status.known_affected = (status.known_affected ?? []).concat(pids);
8741
+ if (o.reason) {
8742
+ v.threats = v.threats ?? [];
8743
+ v.threats.push({
8744
+ category: "impact",
8745
+ details: stripProductsLine$1(o.reason),
8746
+ product_ids: pids
8747
+ });
8748
+ }
8749
+ if (o.type === OverrideType.Poam) for (const m of o.milestones ?? []) {
8750
+ if (!m.description) continue;
8751
+ v.remediations = v.remediations ?? [];
8752
+ v.remediations.push({
8753
+ category: "workaround",
8754
+ details: m.description,
8755
+ product_ids: pids
8756
+ });
8757
+ }
8758
+ emitted = true;
8759
+ }
8760
+ for (const e of o.evidence ?? []) {
8761
+ if (e.type !== "url" || !e.data) continue;
8762
+ v.references = v.references ?? [];
8763
+ v.references.push({
8764
+ category: "external",
8765
+ summary: e.description ?? "",
8766
+ url: e.data
8767
+ });
8768
+ }
8769
+ }
8770
+ /* c8 ignore next */
8771
+ if (!emitted) return void 0;
8772
+ if (status.fixed) status.fixed = [...new Set(status.fixed)];
8773
+ if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
8774
+ if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
8775
+ v.product_status = status;
8776
+ if (v.references) {
8777
+ const seen = /* @__PURE__ */ new Set();
8778
+ v.references = v.references.filter((r) => {
8779
+ if (seen.has(r.url)) return false;
8780
+ seen.add(r.url);
8781
+ return true;
8782
+ });
8783
+ }
8784
+ return v;
8785
+ }
8786
+ function buildDocument(amendments, converterVersion) {
8787
+ const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
8788
+ const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
8789
+ const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
8790
+ const docVersion = amendments.version || "1";
8791
+ const title = amendments.name || "HDF Amendments exported as CSAF VEX";
8792
+ const notes = [];
8793
+ if (amendments.description) notes.push({
8794
+ category: "summary",
8795
+ text: amendments.description,
8796
+ title: "Description"
8797
+ });
8798
+ return {
8799
+ document: {
8800
+ category: "csaf_vex",
8801
+ csaf_version: "2.0",
8802
+ title,
8803
+ notes,
8804
+ publisher: {
8805
+ category: "other",
8806
+ name: publisherName
8807
+ },
8808
+ tracking: {
8809
+ id: trackingId,
8810
+ status: "final",
8811
+ version: docVersion,
8812
+ current_release_date: now,
8813
+ initial_release_date: now,
8814
+ revision_history: [{
8815
+ date: now,
8816
+ number: docVersion,
8817
+ summary: "Generated by hdf-to-csaf-vex from HDF Amendments."
8818
+ }],
8819
+ generator: {
8820
+ engine: {
8821
+ name: "hdf-to-csaf-vex",
8822
+ version: converterVersion
8823
+ },
8824
+ date: now
8825
+ }
8826
+ }
8827
+ },
8828
+ product_tree: { full_product_names: [] },
8829
+ vulnerabilities: []
8830
+ };
8831
+ }
8832
+ //#endregion
8833
+ //#region converters/hdf-to-openvex/typescript/converter.ts
8834
+ /**
8835
+ * HDF Amendments to OpenVEX converter.
8836
+ *
8837
+ * Reverse direction of openvex-to-hdf. Step 4f amendment-output pattern,
8838
+ * partial-fidelity by design — consumer-action-bearing fields (CVE id,
8839
+ * status, justification) survive round-trip; the rest collapse.
8840
+ */
8841
+ const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
8842
+ const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
8843
+ const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
8844
+ const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
8845
+ const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
8846
+ async function convertHdfToOpenVex(input, converterVersion) {
8847
+ validateInputSize(input, "hdf-to-openvex");
8848
+ const amendments = parseJSON(input);
8849
+ const statements = [];
8850
+ let earliest;
8851
+ for (const o of amendments.overrides ?? []) {
8852
+ const s = overrideToStatement(o);
8853
+ if (!s) continue;
8854
+ statements.push(s);
8855
+ const t = new Date(o.appliedAt);
8856
+ if (!earliest || t < earliest) earliest = t;
8857
+ }
8858
+ if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
8859
+ statements.sort((a, b) => a.vulnerability.name.localeCompare(b.vulnerability.name));
8860
+ const author = amendments.appliedBy?.identifier || "HDF Amendments Export";
8861
+ const role = amendments.appliedBy?.description;
8862
+ const doc = {
8863
+ "@context": OPENVEX_CONTEXT,
8864
+ "@id": await buildDocumentID(input, amendments),
8865
+ author,
8866
+ role,
8867
+ timestamp: formatTimestampSeconds(earliest ?? /* @__PURE__ */ new Date()),
8868
+ version: 1,
8869
+ statements
8870
+ };
8871
+ return JSON.stringify(doc, null, 2);
8872
+ }
8873
+ function overrideToStatement(o) {
8874
+ if (!CVE_ID_PATTERN$1.test(o.requirementId)) return void 0;
8875
+ let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
8876
+ if (!canonical) return void 0;
8877
+ if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
8878
+ const stmt = {
8879
+ vulnerability: {
8880
+ name: o.requirementId,
8881
+ "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
8882
+ },
8883
+ status: String(canonical),
8884
+ timestamp: formatTimestampSeconds(new Date(o.appliedAt)),
8885
+ products: productsFor(o)
8886
+ };
8887
+ if (canonical === VexStatus.NotAffected) {
8888
+ if (o.justification) stmt.justification = String(o.justification);
8889
+ const impact = stripProductsLine(o.reason ?? "");
8890
+ if (impact) stmt.impact_statement = impact;
8891
+ } else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
8892
+ else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
8893
+ return stmt;
8894
+ }
8895
+ function productsFor(o) {
8896
+ if (o.affectedPackages && o.affectedPackages.length > 0) {
8897
+ const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
8898
+ if (ids.length > 0) return ids.map((id) => ({ "@id": id }));
8899
+ }
8900
+ let ids = [];
8901
+ if (o.componentRef) ids = [o.componentRef];
8902
+ else {
8903
+ const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
8904
+ if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
8905
+ }
8906
+ if (ids.length === 0) ids = [DEFAULT_PRODUCT_ID$1];
8907
+ return ids.map((id) => ({ "@id": id }));
8908
+ }
8909
+ function firstMilestoneAction(o) {
8910
+ for (const m of o.milestones ?? []) if (m.description) return m.description;
8911
+ return "";
8912
+ }
8913
+ function allMilestonesCompleted$1(o) {
8914
+ if (!o.milestones || o.milestones.length === 0) return false;
8915
+ return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
8916
+ }
8917
+ function stripProductsLine(reason) {
8918
+ return reason.replace(PRODUCTS_LINE$1, "").replace(/\n+$/, "");
8919
+ }
8920
+ async function buildDocumentID(input, a) {
8921
+ if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
8922
+ return `${OPENVEX_NAMESPACE}vex-${await sha256(input)}`;
8923
+ }
8924
+ //#endregion
8925
+ //#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
8926
+ /**
8927
+ * HDF Amendments to CycloneDX VEX converter (export side).
8928
+ *
8929
+ * Reverse direction of cyclonedx-vex-to-hdf. Step 4f amendment-output
8930
+ * pattern, partial-fidelity by design — consumer-action-bearing fields
8931
+ * (CVE id, status, justification) survive round-trip; the rest collapse
8932
+ * into the available CycloneDX VEX fields.
8933
+ */
8934
+ const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
8935
+ const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
8936
+ const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
8937
+ const RESPONSE_LINE = /^Response:.*$/gm;
8938
+ const DEFAULT_PRODUCT_ID = "HDFPID-0001";
8939
+ function convertHdfToCyclonedxVex(input, converterVersion) {
8940
+ validateInputSize(input, "hdf-to-cyclonedx-vex");
8941
+ const amendments = parseJSON(input);
8942
+ const componentRegistry = /* @__PURE__ */ new Map();
8943
+ const vulnerabilities = [];
8944
+ let earliest;
8945
+ for (const o of amendments.overrides ?? []) {
8946
+ if (!CVE_ID_PATTERN.test(o.requirementId)) continue;
8947
+ const v = overrideToVulnerability(o, componentRegistry);
8948
+ if (!v) continue;
8949
+ vulnerabilities.push(v);
8950
+ const t = new Date(o.appliedAt);
8951
+ if (!earliest || t < earliest) earliest = t;
8952
+ }
8953
+ if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
8954
+ vulnerabilities.sort((a, b) => a.id.localeCompare(b.id));
8955
+ const components = [...componentRegistry.values()].sort((a, b) => a["bom-ref"].localeCompare(b["bom-ref"]));
8956
+ const bom = {
8957
+ bomFormat: "CycloneDX",
8958
+ specVersion: "1.4",
8959
+ serialNumber: buildSerialNumberSync(input, amendments),
8960
+ version: 1,
8961
+ metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
8962
+ components,
8963
+ vulnerabilities
8964
+ };
8965
+ return JSON.stringify(bom, null, 2);
8966
+ }
8967
+ function overrideToVulnerability(o, componentRegistry) {
8968
+ let canonical = exportStatusFor(o, allMilestonesCompleted(o), false);
8969
+ if (!canonical) return void 0;
8970
+ if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted(o)) canonical = VexStatus.Fixed;
8971
+ const pids = productIDsFor(o);
8972
+ const pkgById = /* @__PURE__ */ new Map();
8973
+ for (const p of o.affectedPackages ?? []) {
8974
+ const id = affectedPackageToIdentifier(p);
8975
+ if (id) pkgById.set(id, p);
8976
+ }
8977
+ for (const pid of pids) componentRegistry.set(pid, componentFor(pid, pkgById.get(pid)));
8978
+ const analysis = { state: canonicalToCycloneDXState(canonical) };
8979
+ if (o.justification) {
8980
+ const cdxJust = justificationForCycloneDX(o.justification);
8981
+ if (cdxJust) analysis.justification = cdxJust;
8982
+ }
8983
+ const detail = stripReasonAnnotations(o.reason ?? "");
8984
+ if (detail) analysis.detail = detail;
8985
+ if (canonical === VexStatus.Fixed) analysis.response = ["update"];
8986
+ else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
8987
+ const v = {
8988
+ id: o.requirementId,
8989
+ source: {
8990
+ name: "NVD",
8991
+ url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
8992
+ },
8993
+ analysis,
8994
+ affects: pids.map((p) => ({ ref: p }))
8995
+ };
8996
+ for (const e of o.evidence ?? []) {
8997
+ if (e.type !== "url" || !e.data) continue;
8998
+ v.references = v.references ?? [];
8999
+ v.references.push({
9000
+ id: o.requirementId,
9001
+ source: {
9002
+ name: e.description ?? "",
9003
+ url: e.data
9004
+ }
9005
+ });
9006
+ }
9007
+ return v;
9008
+ }
9009
+ function canonicalToCycloneDXState(canonical) {
9010
+ switch (canonical) {
9011
+ case VexStatus.NotAffected: return "not_affected";
9012
+ case VexStatus.Fixed: return "resolved";
9013
+ case VexStatus.Affected: return "exploitable";
9014
+ /* c8 ignore next 2 — defensive: every VexStatus has a case above */
9015
+ default: return canonical;
9016
+ }
9017
+ }
9018
+ function componentFor(pid, pkg) {
9019
+ const c = {
9020
+ type: "application",
9021
+ name: pkg?.name ?? pid,
9022
+ "bom-ref": pid
9023
+ };
9024
+ if (pkg?.version) c.version = pkg.version;
9025
+ if (pkg?.purl ?? (pid.startsWith("pkg:") ? pid : void 0)) c.purl = pkg?.purl ?? pid;
9026
+ if (pkg?.cpe ?? (pid.startsWith("cpe:2.3:") ? pid : void 0)) c.cpe = pkg?.cpe ?? pid;
9027
+ return c;
9028
+ }
9029
+ function productIDsFor(o) {
9030
+ if (o.affectedPackages && o.affectedPackages.length > 0) {
9031
+ const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
9032
+ if (ids.length > 0) return ids;
9033
+ }
9034
+ if (o.componentRef) return [o.componentRef];
9035
+ const m = PRODUCTS_LINE.exec(o.reason ?? "");
9036
+ if (m && m[1]) {
9037
+ const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
9038
+ if (parts.length > 0) return parts;
9039
+ }
9040
+ return [DEFAULT_PRODUCT_ID];
9041
+ }
9042
+ function stripReasonAnnotations(reason) {
9043
+ return reason.replace(PRODUCTS_LINE, "").replace(RAW_JUST_LINE, "").replace(RESPONSE_LINE, "").trim();
9044
+ }
9045
+ function allMilestonesCompleted(o) {
9046
+ if (!o.milestones || o.milestones.length === 0) return false;
9047
+ return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
9048
+ }
9049
+ function buildMetadata(a, docTime, converterVersion) {
9050
+ const metadata = {
9051
+ timestamp: formatTimestampSeconds(docTime),
9052
+ tools: [{
9053
+ vendor: "mitre",
9054
+ name: "hdf-to-cyclonedx-vex",
9055
+ version: converterVersion
9056
+ }]
9057
+ };
9058
+ if (a.appliedBy?.identifier) {
9059
+ const author = {};
9060
+ if (a.appliedBy.type === IdentityType.Email) author.email = a.appliedBy.identifier;
9061
+ else author.name = a.appliedBy.identifier;
9062
+ metadata.authors = [author];
9063
+ }
9064
+ return metadata;
9065
+ }
9066
+ function buildSerialNumberSync(input, a) {
9067
+ if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
9068
+ return `urn:uuid:${fnv1a(input)}`;
9069
+ }
9070
+ function fnv1a(s) {
9071
+ let h = 2166136261;
9072
+ for (let i = 0; i < s.length; i++) {
9073
+ h ^= s.charCodeAt(i);
9074
+ h = Math.imul(h, 16777619) >>> 0;
9075
+ }
9076
+ const d = h.toString(16).padStart(8, "0");
9077
+ return (d + d + d + d).slice(0, 32);
9078
+ }
9079
+ //#endregion
7082
9080
  //#region converters/oscal-to-hdf/typescript/detect.ts
7083
9081
  /**
7084
9082
  * OSCAL document type detection.
@@ -7167,7 +9165,7 @@ async function catalogToBaseline(catalog, rawInput) {
7167
9165
  requirements,
7168
9166
  groups,
7169
9167
  generator: {
7170
- name: "hdf-converters",
9168
+ name: "oscal-catalog-to-hdf",
7171
9169
  version: "1.0.0"
7172
9170
  }
7173
9171
  };
@@ -7410,7 +9408,7 @@ async function convertOscalComponentToHdf(input) {
7410
9408
  integrity,
7411
9409
  requirements,
7412
9410
  generator: {
7413
- name: "hdf-converters",
9411
+ name: "oscal-component-to-hdf",
7414
9412
  version: "1.0.0"
7415
9413
  }
7416
9414
  };
@@ -7471,7 +9469,7 @@ async function convertOscalSspToHdf(input) {
7471
9469
  integrity,
7472
9470
  components: [],
7473
9471
  generator: {
7474
- name: "hdf-converters",
9472
+ name: "oscal-ssp-to-hdf",
7475
9473
  version: "1.0.0"
7476
9474
  }
7477
9475
  };
@@ -7598,13 +9596,13 @@ function sspComponentToHDFComponent(sc, componentControls) {
7598
9596
  function mapOSCALComponentType(oscalType) {
7599
9597
  switch (oscalType.toLowerCase()) {
7600
9598
  case "software":
7601
- case "this-system": return BoundaryDescription.Application;
7602
- case "service": return BoundaryDescription.Application;
7603
- case "hardware": return BoundaryDescription.Host;
7604
- case "network": return BoundaryDescription.Network;
7605
- case "database": return BoundaryDescription.Database;
7606
- case "storage": return BoundaryDescription.Artifact;
7607
- default: return BoundaryDescription.Application;
9599
+ case "this-system": return TargetType.Application;
9600
+ case "service": return TargetType.Application;
9601
+ case "hardware": return TargetType.Host;
9602
+ case "network": return TargetType.Network;
9603
+ case "database": return TargetType.Database;
9604
+ case "storage": return TargetType.Artifact;
9605
+ default: return TargetType.Application;
7608
9606
  }
7609
9607
  }
7610
9608
  //#endregion
@@ -7641,7 +9639,7 @@ async function convertOscalSapToHdf(input) {
7641
9639
  type: planType,
7642
9640
  description,
7643
9641
  generator: {
7644
- name: "hdf-converters",
9642
+ name: "oscal-sap-to-hdf",
7645
9643
  version: "1.0.0"
7646
9644
  }
7647
9645
  };
@@ -7763,7 +9761,7 @@ async function convertOscalPoamToHdf(input) {
7763
9761
  for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
7764
9762
  const systemRef = poam["import-ssp"]?.href || void 0;
7765
9763
  const appliedBy = extractAppliedBy(poam.metadata);
7766
- const amendments = {
9764
+ return serializeHdf({
7767
9765
  name: toKebabCase(poam.metadata.title, "oscal-poam"),
7768
9766
  overrides,
7769
9767
  integrity,
@@ -7771,11 +9769,10 @@ async function convertOscalPoamToHdf(input) {
7771
9769
  version: meta.version,
7772
9770
  appliedBy,
7773
9771
  generator: {
7774
- name: "hdf-converters",
9772
+ name: "oscal-poam-to-hdf",
7775
9773
  version: "1.0.0"
7776
9774
  }
7777
- };
7778
- return JSON.stringify(amendments, null, 2);
9775
+ });
7779
9776
  }
7780
9777
  function buildRiskMap$1(risks) {
7781
9778
  const m = /* @__PURE__ */ new Map();
@@ -7848,8 +9845,8 @@ function poamItemAppliedBy(poam) {
7848
9845
  }
7849
9846
  function poamItemAppliedAt(poam) {
7850
9847
  if (poam.metadata["last-modified"]) {
7851
- const t = new Date(poam.metadata["last-modified"]);
7852
- if (!isNaN(t.getTime())) return t;
9848
+ const t = parseTimestamp(String(poam.metadata["last-modified"]));
9849
+ if (t) return t;
7853
9850
  }
7854
9851
  return /* @__PURE__ */ new Date();
7855
9852
  }
@@ -7907,18 +9904,23 @@ async function convertOscalSarToHdf(input) {
7907
9904
  if (!doc["assessment-results"]) throw new Error("oscal-assessment-results: input is not an assessment-results document (root key is not 'assessment-results')");
7908
9905
  const sar = doc["assessment-results"];
7909
9906
  const meta = extractMetadata(sar.metadata);
9907
+ const scanTime = /* @__PURE__ */ new Date();
7910
9908
  const baselines = [];
7911
9909
  for (const result of sar.results) {
7912
- const baseline = await resultToEvaluatedBaseline(result, sar, input);
9910
+ if (!result.findings || result.findings.length === 0) {
9911
+ console.warn(`WARNING: Skipping assessment result "${result.title || result.uuid}": no findings (empty result set)`);
9912
+ continue;
9913
+ }
9914
+ const baseline = await resultToEvaluatedBaseline(result, sar, input, scanTime);
7913
9915
  baselines.push(baseline);
7914
9916
  }
7915
9917
  const planRef = sar["import-ap"]?.href || void 0;
7916
9918
  let timestamp;
7917
9919
  if (meta.lastModified) {
7918
- const t = new Date(meta.lastModified);
7919
- if (!isNaN(t.getTime())) timestamp = t;
9920
+ const t = parseTimestamp(meta.lastModified);
9921
+ if (t) timestamp = t;
7920
9922
  }
7921
- return JSON.stringify({
9923
+ return serializeHdf({
7922
9924
  baselines,
7923
9925
  generator: {
7924
9926
  name: "oscal-assessment-results-to-hdf",
@@ -7930,9 +9932,9 @@ async function convertOscalSarToHdf(input) {
7930
9932
  },
7931
9933
  timestamp: timestamp ?? /* @__PURE__ */ new Date(),
7932
9934
  planRef
7933
- }, null, 2);
9935
+ });
7934
9936
  }
7935
- async function resultToEvaluatedBaseline(result, sar, rawInput) {
9937
+ async function resultToEvaluatedBaseline(result, sar, rawInput, scanTime) {
7936
9938
  const checksum = await inputChecksum(rawInput);
7937
9939
  const obsMap = buildObservationMap(result.observations ?? []);
7938
9940
  const riskMap = buildRiskMap(result.risks ?? []);
@@ -7949,7 +9951,7 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
7949
9951
  }
7950
9952
  const requirements = [];
7951
9953
  for (const controlId of controlOrder) {
7952
- const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result);
9954
+ const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result, scanTime);
7953
9955
  requirements.push(req);
7954
9956
  }
7955
9957
  const baseline = createMinimalBaseline(sarBaselineName(result, sar), requirements, {
@@ -7961,26 +9963,26 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
7961
9963
  if (result.description) baseline.description = result.description;
7962
9964
  return baseline;
7963
9965
  }
7964
- function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result) {
9966
+ function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result, scanTime) {
7965
9967
  const nistTag = controlIdToNistTag(controlId);
7966
9968
  const title = findings[0].title || nistTag;
7967
9969
  const impact = sarFindingsImpact(findings, riskMap);
7968
9970
  const descriptions = sarBuildDescriptions(findings, obsMap);
7969
9971
  const results = [];
7970
- for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result));
9972
+ for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result, scanTime));
7971
9973
  const req = createRequirement(nistTag, title, descriptions, impact, results, { tags: { nist: [nistTag] } });
7972
9974
  const controlType = deriveControlTypeFromTags([nistTag]);
7973
9975
  if (controlType !== void 0) req.controlType = controlType;
7974
9976
  return req;
7975
9977
  }
7976
- function findingToRequirementResult(f, obsMap, riskMap, result) {
9978
+ function findingToRequirementResult(f, obsMap, riskMap, result, scanTime) {
7977
9979
  const status = mapFindingStatus(f);
7978
9980
  const codeDesc = buildCodeDesc(f, obsMap);
7979
9981
  const message = buildRiskMessage(f, riskMap);
7980
9982
  const startTime = parseResultStartTime(result);
7981
9983
  return createResult(status, message || void 0, {
7982
9984
  codeDesc,
7983
- startTime: startTime.getTime() > 0 ? startTime : void 0
9985
+ startTime: startTime.getTime() > 0 ? startTime : scanTime
7984
9986
  });
7985
9987
  }
7986
9988
  function extractControlIdFromFinding(f) {
@@ -8075,8 +10077,8 @@ function buildRiskMap(risks) {
8075
10077
  }
8076
10078
  function parseResultStartTime(result) {
8077
10079
  if (result.start) {
8078
- const t = new Date(result.start);
8079
- if (!isNaN(t.getTime())) return t;
10080
+ const t = parseTimestamp(String(result.start));
10081
+ if (t) return t;
8080
10082
  }
8081
10083
  return /* @__PURE__ */ new Date(0);
8082
10084
  }
@@ -8084,6 +10086,6 @@ function sarBaselineName(result, sar) {
8084
10086
  return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
8085
10087
  }
8086
10088
  //#endregion
8087
- export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCyclonedxToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsv, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
10089
+ export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
8088
10090
 
8089
10091
  //# sourceMappingURL=index.js.map