@mitre/hdf-converters 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { n as detectConverter, t as registerAllFingerprints } from "./register-all-DaYHszLd.js";
1
+ import { n as detectConverter, t as registerAllFingerprints } from "./register-all-C3lYICDC.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 { AuthorizationStatus, BoundaryDescription, CategorizationLevel, Copyright, HashAlgorithm, OverrideType, PlanType, ResultStatus, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact } from "@mitre/hdf-schema";
3
+ import { buildCsv, buildXml, cvssScoreToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML } 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
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";
6
6
  //#region shared/typescript/converterutil.ts
7
7
  /**
@@ -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)
@@ -202,6 +251,156 @@ function buildHdfResults(opts) {
202
251
  if (opts.statistics) hdf.statistics = opts.statistics;
203
252
  return JSON.stringify(hdf, null, 2);
204
253
  }
254
+ /**
255
+ * Captures a NIST 800-53 control identifier and its base sub-control number,
256
+ * ignoring enhancement suffixes like "(1)" or ".1". Mirrors the Go pattern
257
+ * in shared/go/converterutil.go.
258
+ */
259
+ const NIST_TAG_PATTERN = /^([A-Z]{2})-(\d+)/;
260
+ /**
261
+ * Maps each recognized NIST 800-53 Rev 5 family to its HDF controlType per
262
+ * SP 800-53 Appendix C / SP 800-53A classification. Unknown families resolve
263
+ * to undefined.
264
+ */
265
+ const NIST_FAMILY_CONTROL_TYPE = {
266
+ AC: ControlType.Technical,
267
+ AT: ControlType.Operational,
268
+ AU: ControlType.Operational,
269
+ CA: ControlType.Management,
270
+ CM: ControlType.Operational,
271
+ CP: ControlType.Operational,
272
+ IA: ControlType.Technical,
273
+ IR: ControlType.Operational,
274
+ MA: ControlType.Operational,
275
+ MP: ControlType.Operational,
276
+ PE: ControlType.Operational,
277
+ PL: ControlType.Management,
278
+ PM: ControlType.Management,
279
+ PS: ControlType.Operational,
280
+ PT: ControlType.Operational,
281
+ RA: ControlType.Management,
282
+ SA: ControlType.Management,
283
+ SC: ControlType.Technical,
284
+ SI: ControlType.Technical,
285
+ SR: ControlType.Management
286
+ };
287
+ /**
288
+ * Derive the HDF `controlType` for a single NIST 800-53 control identifier
289
+ * using a family-prefix heuristic. Returns undefined when the family is
290
+ * unrecognized or the tag is not a NIST control identifier.
291
+ *
292
+ * Heuristic, per NIST SP 800-53 Rev 5 Appendix C and SP 800-53A:
293
+ * - Management: PM, RA, CA, PL, SA, SR families
294
+ * - Operational: AT, AU, CM, CP, IR, MA, MP, PE, PS, PT families
295
+ * - Technical: AC, IA, SC, SI families
296
+ * - Policy: any "-1" sub-control (the per-family policy/procedure
297
+ * document, regardless of which family it belongs to). Enhancements of
298
+ * -1 controls (e.g., AC-1(1)) also resolve to policy.
299
+ *
300
+ * The "-1" rule takes precedence over the family rule because the per-family
301
+ * policy/procedure document is more usefully classified by its document
302
+ * nature than by the family it documents.
303
+ *
304
+ * @example
305
+ * deriveControlType("AC-3") // ControlType.Technical
306
+ * deriveControlType("AC-1") // ControlType.Policy (any *-1 is policy)
307
+ * deriveControlType("AC-3(1)") // ControlType.Technical (enhancement of AC-3)
308
+ * deriveControlType("PM-2") // ControlType.Management
309
+ * deriveControlType("SV-238196") // undefined (not a NIST tag)
310
+ */
311
+ function deriveControlType(nistTag) {
312
+ const match = NIST_TAG_PATTERN.exec(nistTag.trim().toUpperCase());
313
+ if (!match || match[1] === void 0 || match[2] === void 0) return void 0;
314
+ const family = match[1];
315
+ const subControl = match[2];
316
+ const familyClass = NIST_FAMILY_CONTROL_TYPE[family];
317
+ if (familyClass === void 0) return void 0;
318
+ if (subControl === "1") return ControlType.Policy;
319
+ return familyClass;
320
+ }
321
+ /**
322
+ * NIST tag sets this package uses as per-converter static fallbacks when no
323
+ * real per-finding mapping is available. When deriveControlTypeFromTags sees
324
+ * an input that exactly matches one of these bundles, it returns undefined —
325
+ * the input carries no real per-finding signal, and stamping every
326
+ * requirement with the same derived controlType is misleading.
327
+ *
328
+ * Keep in sync with DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS.
329
+ */
330
+ const NIST_FALLBACK_BUNDLES = [
331
+ ["RA-5", "SA-11"],
332
+ ["RA-5", "SI-2"],
333
+ ["CM-8"]
334
+ ];
335
+ function tagsMatchFallback(tags) {
336
+ if (tags.length === 0) return false;
337
+ const sorted = [...new Set(tags)].sort();
338
+ return NIST_FALLBACK_BUNDLES.some((bundle) => bundle.length === sorted.length && bundle.every((b, i) => b === sorted[i]));
339
+ }
340
+ /**
341
+ * Derive the HDF `controlType` for a slice of NIST tags. Resolves each tag
342
+ * via {@link deriveControlType}, then picks the most-specific class by
343
+ * precedence: technical > operational > management > policy > procedure.
344
+ * The rationale is that a control enforced via configuration (technical)
345
+ * is more actionable than the same control's management/policy framing.
346
+ *
347
+ * Returns undefined when no tag resolves to a known classification, OR when
348
+ * the input exactly matches a converter-level static-fallback bundle (e.g.
349
+ * the DEFAULT_STATIC_ANALYSIS_NIST_TAGS set). The fallback gate prevents
350
+ * converters that have no per-finding NIST signal from stamping every
351
+ * requirement with the same misleading controlType.
352
+ */
353
+ function deriveControlTypeFromTags(tags) {
354
+ if (tagsMatchFallback(tags)) return void 0;
355
+ const rank = {
356
+ [ControlType.Technical]: 0,
357
+ [ControlType.Operational]: 1,
358
+ [ControlType.Management]: 2,
359
+ [ControlType.Policy]: 3,
360
+ [ControlType.Procedure]: 4
361
+ };
362
+ let best;
363
+ let bestRank = Number.POSITIVE_INFINITY;
364
+ for (const tag of tags) {
365
+ const ct = deriveControlType(tag);
366
+ if (ct === void 0) continue;
367
+ const r = rank[ct];
368
+ if (r < bestRank) {
369
+ bestRank = r;
370
+ best = ct;
371
+ }
372
+ }
373
+ return best;
374
+ }
375
+ /**
376
+ * Derive the HDF `verificationMethod` for a requirement based on whether
377
+ * check code is present. Returns `automated` when code is non-empty (a check
378
+ * exists and runs without operator action), and undefined otherwise — the
379
+ * converter is responsible for distinguishing manual-by-design (statement-form,
380
+ * e.g. FedRAMP 20x KSI) from manual-pending-automation (a check that could
381
+ * be automated but isn't yet) when it has the source-format context to do so.
382
+ */
383
+ function deriveVerificationMethod(code) {
384
+ if (code === void 0 || code === null || code === "") return void 0;
385
+ return VerificationMethodEnum.Automated;
386
+ }
387
+ function buildNoFindingsRequirement(id, codeDesc, startTime) {
388
+ return {
389
+ id,
390
+ title: "No findings reported",
391
+ impact: 0,
392
+ descriptions: [{
393
+ label: "default",
394
+ data: codeDesc
395
+ }],
396
+ results: [{
397
+ status: ResultStatus.Passed,
398
+ codeDesc,
399
+ startTime
400
+ }],
401
+ tags: {}
402
+ };
403
+ }
205
404
  //#endregion
206
405
  //#region converters/legacyhdf-to-hdf/typescript/converter.ts
207
406
  /**
@@ -247,7 +446,7 @@ function impactToSeverity$2(impact) {
247
446
  * Normalize status values from v1.0 to v2.0 format.
248
447
  * Converts snake_case to camelCase.
249
448
  */
250
- function normalizeStatus(status) {
449
+ function normalizeStatus$1(status) {
251
450
  return {
252
451
  "passed": "passed",
253
452
  "failed": "failed",
@@ -293,7 +492,7 @@ function computeEffectiveStatus(impact, results) {
293
492
  * Transforms snake_case field names to camelCase.
294
493
  */
295
494
  function convertResult(v1Result) {
296
- const v2Result = { status: normalizeStatus(v1Result.status) };
495
+ const v2Result = { status: normalizeStatus$1(v1Result.status) };
297
496
  if (v1Result.code_desc !== void 0) v2Result.codeDesc = v1Result.code_desc;
298
497
  if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
299
498
  if (v1Result.start_time !== void 0) v2Result.startTime = v1Result.start_time;
@@ -337,10 +536,15 @@ function convertControl(v1Control) {
337
536
  if (v1Control.code !== void 0) v2Req.code = v1Control.code;
338
537
  if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
339
538
  if (v1Control.waiver_data !== void 0) v2Req.waiverData = v1Control.waiver_data;
340
- if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus(v1Control.status);
539
+ if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
341
540
  if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
342
541
  if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
343
542
  v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity$2(v1Control.impact);
543
+ const rawNist = v1Control.tags?.nist;
544
+ if (Array.isArray(rawNist)) {
545
+ const controlType = deriveControlTypeFromTags(rawNist.filter((v) => typeof v === "string"));
546
+ if (controlType !== void 0) v2Req.controlType = controlType;
547
+ }
344
548
  const knownFields = new Set([
345
549
  "id",
346
550
  "title",
@@ -544,18 +748,19 @@ async function convertSarifToHdf(input) {
544
748
  const { items: limitedRuns, truncated: truncatedRuns } = limitArray(sarif.runs);
545
749
  /* v8 ignore next -- truncation only triggers with >100K items */
546
750
  if (truncatedRuns) console.warn(`WARNING: Input truncated at ${limitedRuns.length} run items (original: ${sarif.runs.length})`);
751
+ const timestamp = /* @__PURE__ */ new Date();
547
752
  return buildHdfResults({
548
753
  generatorName: "sarif-to-hdf",
549
754
  converterVersion: "1.0.0",
550
755
  toolName: firstDriver?.name,
551
756
  toolVersion: firstDriver?.version,
552
757
  toolFormat: "SARIF",
553
- baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum)),
758
+ baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
554
759
  components: [],
555
- timestamp: /* @__PURE__ */ new Date()
760
+ timestamp
556
761
  });
557
762
  }
558
- function convertRun(run, version, resultsChecksum) {
763
+ function convertRun(run, version, resultsChecksum, timestamp) {
559
764
  const ruleMap = buildRuleMap(run);
560
765
  const { items: limitedResults, truncated: truncatedResults } = limitArray(run.results);
561
766
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -579,7 +784,14 @@ function convertRun(run, version, resultsChecksum) {
579
784
  const group = groupMap.get(ruleId);
580
785
  return convertResultGroup(ruleId, group.rule, group.results);
581
786
  });
582
- return createMinimalBaseline(run.tool?.driver?.name || "SARIF", requirements, {
787
+ const baselineName = run.tool?.driver?.name || "SARIF";
788
+ if (requirements.length === 0) {
789
+ const driverName = run.tool?.driver?.name?.trim() || "";
790
+ const target = driverName || "SARIF analyzer";
791
+ const idPrefix = driverName || "sarif";
792
+ requirements.push(buildNoFindingsRequirement(`${idPrefix}-no-findings`, `${target} ran and reported zero findings.`, timestamp));
793
+ }
794
+ return createMinimalBaseline(baselineName, requirements, {
583
795
  version,
584
796
  title: "Static Analysis Results Interchange Format",
585
797
  resultsChecksum
@@ -605,7 +817,45 @@ function convertResultGroup(ruleId, rule, sarifResults) {
605
817
  const descriptions = buildDescriptions$1(description, rule, firstResult);
606
818
  const options = { tags: buildTags$3(firstResult, rule, ruleLevel, cweIds, nistControls, cciControls) };
607
819
  if (sourceLocation) options.sourceLocation = sourceLocation;
608
- return createRequirement(ruleId, title, descriptions, impact, results, options);
820
+ const req = createRequirement(ruleId, title, descriptions, impact, results, options);
821
+ const controlType = deriveControlTypeFromTags(nistControls);
822
+ if (controlType !== void 0) req.controlType = controlType;
823
+ req.verificationMethod = VerificationMethodEnum.Automated;
824
+ const seenKeys = /* @__PURE__ */ new Set();
825
+ const packages = [];
826
+ for (const sr of sarifResults) {
827
+ const pkg = packageFromSarifProperties(sr.properties);
828
+ if (!pkg) continue;
829
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
830
+ if (seenKeys.has(key)) continue;
831
+ seenKeys.add(key);
832
+ packages.push(pkg);
833
+ }
834
+ if (packages.length > 0) req.affectedPackages = packages;
835
+ return req;
836
+ }
837
+ /**
838
+ * Extract an Affected_Package from a SARIF result.properties bag.
839
+ * Recognizes the SCA-tool convention of carrying purl / cpe /
840
+ * packageName+packageVersion / name+version. Returns undefined for
841
+ * SAST results that lack any package identity.
842
+ */
843
+ function packageFromSarifProperties(props) {
844
+ if (!props) return void 0;
845
+ const name = props.packageName ?? props.name;
846
+ const version = props.packageVersion ?? props.version;
847
+ let ecosystem;
848
+ if (props.purl) ecosystem = ecosystemFromPurlType(parsePurl(props.purl)?.type);
849
+ else if (props.ecosystem) ecosystem = ecosystemFromPurlType(props.ecosystem);
850
+ else if (name && version) ecosystem = Ecosystem.Generic;
851
+ return buildAffectedPackage$1({
852
+ name,
853
+ version,
854
+ ecosystem,
855
+ purl: props.purl,
856
+ cpe: props.cpe,
857
+ fixedInVersion: props.fixedInVersion
858
+ });
609
859
  }
610
860
  function resolveRuleLevel(rule, results) {
611
861
  if (rule?.defaultConfiguration?.level) return rule.defaultConfiguration.level;
@@ -810,8 +1060,8 @@ function createHDFResult(location, status, backtrace, suppressionMessage) {
810
1060
  //#endregion
811
1061
  //#region converters/junit-to-hdf/typescript/converter.ts
812
1062
  const DEFAULT_NIST = ["SA-11"];
813
- const CONVERTER_VERSION$1 = "1.0.0";
814
- const ARRAY_TAGS$1 = ["testsuite", "testcase"];
1063
+ const CONVERTER_VERSION$2 = "1.0.0";
1064
+ const ARRAY_TAGS$2 = ["testsuite", "testcase"];
815
1065
  /**
816
1066
  * Converts JUnit XML test results to HDF format.
817
1067
  */
@@ -819,21 +1069,23 @@ async function convertJunitToHdf(input) {
819
1069
  if (!input || !input.trim()) throw new Error("Empty input");
820
1070
  validateInputSize(input, "junit");
821
1071
  const { suites, name } = parseJUnitXML(input);
1072
+ const requirements = buildRequirements(suites);
1073
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, /* @__PURE__ */ new Date()));
822
1074
  return buildHdfResults({
823
- generatorName: "hdf-converters",
824
- converterVersion: CONVERTER_VERSION$1,
1075
+ generatorName: "junit-to-hdf",
1076
+ converterVersion: CONVERTER_VERSION$2,
825
1077
  toolName: "JUnit XML",
826
1078
  toolFormat: "XML",
827
- baselines: [createMinimalBaseline(name, buildRequirements(suites), { resultsChecksum: await inputChecksum(input) })],
1079
+ baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
828
1080
  components: [{
829
- type: Copyright.Application,
1081
+ type: TargetType.Application,
830
1082
  name
831
1083
  }],
832
1084
  timestamp: /* @__PURE__ */ new Date()
833
1085
  });
834
1086
  }
835
1087
  function parseJUnitXML(input) {
836
- const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1);
1088
+ const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
837
1089
  if (parsed.testsuites) return {
838
1090
  suites: parsed.testsuites.testsuite ?? [],
839
1091
  name: parsed.testsuites.name || "JUnit Test Results"
@@ -876,7 +1128,11 @@ function testCaseToRequirement(tc, suiteTimestamp) {
876
1128
  label: "default",
877
1129
  data: `JUnit test: ${tc.name} in ${tc.classname || "unknown"}`
878
1130
  }];
879
- return createRequirement(id, tc.name, descriptions, .5, [result], { tags: { nist: DEFAULT_NIST } });
1131
+ const req = createRequirement(id, tc.name, descriptions, .5, [result], { tags: { nist: DEFAULT_NIST } });
1132
+ const controlType = deriveControlTypeFromTags(DEFAULT_NIST);
1133
+ if (controlType !== void 0) req.controlType = controlType;
1134
+ req.verificationMethod = VerificationMethodEnum.Automated;
1135
+ return req;
880
1136
  }
881
1137
  function buildID(tc) {
882
1138
  if (tc.classname) return `${tc.classname}.${tc.name}`;
@@ -921,12 +1177,17 @@ function buildCodeDesc$9(tc) {
921
1177
  if (tc.classname) return `${tc.classname} :: ${tc.name}`;
922
1178
  return tc.name;
923
1179
  }
1180
+ function noFindingsTarget(baselineName, suites) {
1181
+ if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
1182
+ for (const s of suites) if (s.name) return s.name;
1183
+ return "JUnit test suite";
1184
+ }
924
1185
  //#endregion
925
1186
  //#region converters/xccdf-results-to-hdf/typescript/converter.ts
926
- const CONVERTER_VERSION = "1.0.0";
1187
+ const CONVERTER_VERSION$1 = "1.0.0";
927
1188
  const CCI_SYSTEM = "http://cyber.mil/cci";
928
1189
  /** Tags that must always be parsed as arrays even if only one element exists. */
929
- const ARRAY_TAGS = [
1190
+ const ARRAY_TAGS$1 = [
930
1191
  "Group",
931
1192
  "Rule",
932
1193
  "Profile",
@@ -969,7 +1230,7 @@ const STATUS_MAP = {
969
1230
  async function convertXccdfResultsToHdf(input) {
970
1231
  if (!input || !input.trim()) throw new Error("Empty input");
971
1232
  validateInputSize(input, "xccdf-results");
972
- const parsed = parseXmlWithArrays(input, ARRAY_TAGS);
1233
+ const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1);
973
1234
  const arfParsed = parsed;
974
1235
  if (arfParsed["asset-report-collection"]) return convertArfCollection(arfParsed["asset-report-collection"], input);
975
1236
  const benchmark = parsed.Benchmark;
@@ -985,6 +1246,10 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
985
1246
  /* v8 ignore next -- truncation only triggers with >100K items */
986
1247
  if (truncatedRR) console.warn(`WARNING: Input truncated at ${limitedRuleResults.length} rule-result items (original: ${ruleResults.length})`);
987
1248
  const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
1249
+ if (requirements.length === 0) {
1250
+ const target = xccdfTargetName(testResult, benchmark);
1251
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
1252
+ }
988
1253
  const resultsChecksum = await inputChecksum(rawInput);
989
1254
  const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
990
1255
  const components = buildTargets(testResult);
@@ -998,8 +1263,8 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
998
1263
  const hdf = {
999
1264
  baselines: [baseline],
1000
1265
  generator: {
1001
- name: "hdf-converters",
1002
- version: CONVERTER_VERSION
1266
+ name: "xccdf-results-to-hdf",
1267
+ version: CONVERTER_VERSION$1
1003
1268
  },
1004
1269
  tool: {
1005
1270
  name: "XCCDF Results",
@@ -1037,6 +1302,10 @@ async function convertArfCollection(arc, rawInput) {
1037
1302
  /* v8 ignore next -- truncation only triggers with >100K items */
1038
1303
  if (truncatedARFRR) console.warn(`WARNING: Input truncated at ${limitedARFRuleResults.length} rule-result items (original: ${ruleResults.length})`);
1039
1304
  const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
1305
+ if (requirements.length === 0) {
1306
+ const target = xccdfTargetName(testResult, benchmark);
1307
+ requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
1308
+ }
1040
1309
  let baselineName = "";
1041
1310
  if (benchmark) baselineName = extractText(benchmark.title) || "";
1042
1311
  if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "ARF Report";
@@ -1057,8 +1326,8 @@ async function convertArfCollection(arc, rawInput) {
1057
1326
  const hdf = {
1058
1327
  baselines,
1059
1328
  generator: {
1060
- name: "hdf-converters",
1061
- version: CONVERTER_VERSION
1329
+ name: "xccdf-results-to-hdf",
1330
+ version: CONVERTER_VERSION$1
1062
1331
  },
1063
1332
  tool: {
1064
1333
  name: "ARF",
@@ -1144,13 +1413,33 @@ function ruleResultToRequirement(rr, ruleIndex) {
1144
1413
  });
1145
1414
  const result = createResult(STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed, "", { codeDesc: `XCCDF rule ${id}` });
1146
1415
  const tags = {};
1416
+ let nistTags = [];
1147
1417
  const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
1148
1418
  if (cciIds.length > 0) {
1149
1419
  tags["cci"] = cciIds;
1150
- const nistTags = cciIds.flatMap((cci) => getCCINistMappings(cci) ?? []);
1151
- if (nistTags.length > 0) tags["nist"] = [...new Set(nistTags)];
1420
+ nistTags = [...new Set(cciIds.flatMap((cci) => getCCINistMappings(cci) ?? []))];
1421
+ if (nistTags.length > 0) tags["nist"] = nistTags;
1152
1422
  }
1153
- return createRequirement(id, title, descriptions, impact, [result], { tags });
1423
+ const req = createRequirement(id, title, descriptions, impact, [result], { tags });
1424
+ const controlType = deriveControlTypeFromTags(nistTags);
1425
+ if (controlType !== void 0) req.controlType = controlType;
1426
+ return req;
1427
+ }
1428
+ /**
1429
+ * Pick the most specific identifier available for a no-findings codeDesc.
1430
+ * Falls back through TestResult target/title, benchmark title/id, then a generic phrase.
1431
+ */
1432
+ function xccdfTargetName(testResult, benchmark) {
1433
+ const tr = testResult ?? {};
1434
+ const target = (tr.target ?? "").trim();
1435
+ if (target) return target;
1436
+ const trTitle = extractText(tr.title).trim();
1437
+ if (trTitle) return trTitle;
1438
+ const benchTitle = extractText(benchmark?.title).trim();
1439
+ if (benchTitle) return benchTitle;
1440
+ const benchId = (benchmark?.id ?? "").trim();
1441
+ if (benchId) return benchId;
1442
+ return "the target";
1154
1443
  }
1155
1444
  /**
1156
1445
  * Build Component array from TestResult metadata.
@@ -1161,7 +1450,7 @@ function buildTargets(testResult) {
1161
1450
  const addresses = testResult["target-address"] ?? [];
1162
1451
  const target = {
1163
1452
  name: targetName,
1164
- type: Copyright.Host,
1453
+ type: TargetType.Host,
1165
1454
  labels: {}
1166
1455
  };
1167
1456
  if (addresses.length > 0) target.ipAddress = addresses[0];
@@ -1210,6 +1499,713 @@ function extractCCIs(idents) {
1210
1499
  return idents.filter((i) => i.system === CCI_SYSTEM).map((i) => i["#text"] ?? "").filter((v) => v.length > 0);
1211
1500
  }
1212
1501
  //#endregion
1502
+ //#region shared/typescript/checklist/status.ts
1503
+ function normalizeKey(s) {
1504
+ return s.toLowerCase().trim().split("_").join("").split(" ").join("");
1505
+ }
1506
+ const STATUS_BY_KEY = {
1507
+ open: "Open",
1508
+ notafinding: "NotAFinding",
1509
+ notreviewed: "Not_Reviewed",
1510
+ notapplicable: "Not_Applicable"
1511
+ };
1512
+ /** Map any CKL/CKLB status spelling to the canonical CheckStatus. */
1513
+ function parseStatus(s) {
1514
+ return STATUS_BY_KEY[normalizeKey(s ?? "")] ?? "Not_Reviewed";
1515
+ }
1516
+ /** CKL (XML) spelling. */
1517
+ function statusToCkl(s) {
1518
+ return s || "Not_Reviewed";
1519
+ }
1520
+ /** CKLB (JSON) snake_case spelling. */
1521
+ function statusToCklb(s) {
1522
+ switch (s) {
1523
+ case "Open": return "open";
1524
+ case "NotAFinding": return "not_a_finding";
1525
+ case "Not_Applicable": return "not_applicable";
1526
+ default: return "not_reviewed";
1527
+ }
1528
+ }
1529
+ /** Canonical status -> HDF Result_Status. */
1530
+ function statusToHdf(s) {
1531
+ switch (s) {
1532
+ case "Open": return ResultStatus.Failed;
1533
+ case "NotAFinding": return ResultStatus.Passed;
1534
+ case "Not_Applicable": return ResultStatus.NotApplicable;
1535
+ default: return ResultStatus.NotReviewed;
1536
+ }
1537
+ }
1538
+ /** HDF Result_Status -> canonical status (error -> Open). */
1539
+ function statusFromHdf(s) {
1540
+ switch (s) {
1541
+ case ResultStatus.Passed: return "NotAFinding";
1542
+ case ResultStatus.Failed:
1543
+ case ResultStatus.Error: return "Open";
1544
+ case ResultStatus.NotApplicable: return "Not_Applicable";
1545
+ default: return "Not_Reviewed";
1546
+ }
1547
+ }
1548
+ //#endregion
1549
+ //#region shared/typescript/checklist/ckl.ts
1550
+ const ARRAY_TAGS = [
1551
+ "iSTIG",
1552
+ "VULN",
1553
+ "STIG_DATA",
1554
+ "SI_DATA"
1555
+ ];
1556
+ const BUILD_OPTIONS$1 = {
1557
+ attributeNamePrefix: "@_",
1558
+ textNodeName: "#text",
1559
+ ignoreAttributes: false,
1560
+ format: true,
1561
+ indentBy: " ",
1562
+ suppressEmptyNode: false
1563
+ };
1564
+ function str(v) {
1565
+ return v === void 0 || v === null ? "" : String(v);
1566
+ }
1567
+ const VULN_ATTR_ORDER = [
1568
+ "Vuln_Num",
1569
+ "Severity",
1570
+ "Group_Title",
1571
+ "Rule_ID",
1572
+ "Rule_Ver",
1573
+ "Rule_Title",
1574
+ "Vuln_Discuss",
1575
+ "IA_Controls",
1576
+ "Check_Content",
1577
+ "Fix_Text",
1578
+ "False_Positives",
1579
+ "False_Negatives",
1580
+ "Documentable",
1581
+ "Mitigations",
1582
+ "Potential_Impact",
1583
+ "Third_Party_Tools",
1584
+ "Mitigation_Control",
1585
+ "Responsibility",
1586
+ "Security_Override_Guidance",
1587
+ "Check_Content_Ref",
1588
+ "Weight",
1589
+ "Class",
1590
+ "STIG_UUID"
1591
+ ];
1592
+ const CORE_VULN_ATTR = new Set([
1593
+ "Vuln_Num",
1594
+ "Severity",
1595
+ "Group_Title",
1596
+ "Rule_ID",
1597
+ "Rule_Ver",
1598
+ "Rule_Title",
1599
+ "Vuln_Discuss",
1600
+ "Check_Content",
1601
+ "Fix_Text",
1602
+ "Weight",
1603
+ "Class"
1604
+ ]);
1605
+ /** Parse CKL XML into the format-neutral Checklist model. */
1606
+ function parseCkl(input) {
1607
+ const checklist = parseXmlWithArrays(input, ARRAY_TAGS, { processEntities: true }).CHECKLIST;
1608
+ const istigs = checklist?.STIGS?.iSTIG;
1609
+ if (!checklist || !istigs || istigs.length === 0) throw new Error("parse ckl: no <iSTIG> blocks found (not a CKL document?)");
1610
+ const a = checklist.ASSET ?? {};
1611
+ return {
1612
+ format: "ckl",
1613
+ asset: {
1614
+ role: str(a["ROLE"]),
1615
+ assetType: str(a["ASSET_TYPE"]),
1616
+ marking: str(a["MARKING"]),
1617
+ hostName: str(a["HOST_NAME"]),
1618
+ hostIP: str(a["HOST_IP"]),
1619
+ hostMAC: str(a["HOST_MAC"]),
1620
+ hostFQDN: str(a["HOST_FQDN"]),
1621
+ targetComment: str(a["TARGET_COMMENT"]),
1622
+ techArea: str(a["TECH_AREA"]),
1623
+ targetKey: str(a["TARGET_KEY"]),
1624
+ webOrDatabase: str(a["WEB_OR_DATABASE"]) === "true",
1625
+ webDBSite: str(a["WEB_DB_SITE"]),
1626
+ webDBInstance: str(a["WEB_DB_INSTANCE"])
1627
+ },
1628
+ stigs: istigs.map((is) => {
1629
+ const si = is.STIG_INFO?.SI_DATA ?? [];
1630
+ const siVal = (name) => str(si.find((d) => d.SID_NAME === name)?.SID_DATA);
1631
+ return {
1632
+ stigID: siVal("stigid"),
1633
+ title: siVal("title"),
1634
+ version: siVal("version"),
1635
+ releaseInfo: siVal("releaseinfo"),
1636
+ uuid: siVal("uuid"),
1637
+ classification: siVal("classification"),
1638
+ vulns: (is.VULN ?? []).map(cklVulnToModel)
1639
+ };
1640
+ })
1641
+ };
1642
+ }
1643
+ function cklVulnToModel(v) {
1644
+ const data = v.STIG_DATA ?? [];
1645
+ const attr = (name) => str(data.find((sd) => sd.VULN_ATTRIBUTE === name)?.ATTRIBUTE_DATA);
1646
+ const ccis = [];
1647
+ const extra = {};
1648
+ const promoted = new Set([
1649
+ "CCI_REF",
1650
+ "Vuln_Num",
1651
+ "Severity",
1652
+ "Group_Title",
1653
+ "Rule_ID",
1654
+ "Rule_Ver",
1655
+ "Rule_Title",
1656
+ "Vuln_Discuss",
1657
+ "Check_Content",
1658
+ "Fix_Text",
1659
+ "Weight",
1660
+ "Class"
1661
+ ]);
1662
+ for (const sd of data) {
1663
+ const name = sd.VULN_ATTRIBUTE ?? "";
1664
+ const val = str(sd.ATTRIBUTE_DATA);
1665
+ if (name === "CCI_REF") {
1666
+ if (val) ccis.push(val);
1667
+ } else if (!promoted.has(name) && val) extra[name] = val;
1668
+ }
1669
+ return {
1670
+ vulnNum: attr("Vuln_Num"),
1671
+ ruleID: attr("Rule_ID"),
1672
+ ruleVer: attr("Rule_Ver"),
1673
+ groupTitle: attr("Group_Title"),
1674
+ severity: attr("Severity"),
1675
+ ruleTitle: attr("Rule_Title"),
1676
+ vulnDiscuss: attr("Vuln_Discuss"),
1677
+ checkContent: attr("Check_Content"),
1678
+ fixText: attr("Fix_Text"),
1679
+ weight: attr("Weight"),
1680
+ classification: attr("Class"),
1681
+ ccis,
1682
+ status: parseStatus(v.STATUS),
1683
+ findingDetails: str(v.FINDING_DETAILS),
1684
+ comments: str(v.COMMENTS),
1685
+ extra
1686
+ };
1687
+ }
1688
+ /** Serialize the Checklist model to CKL XML (with declaration). */
1689
+ function serializeCkl(cl) {
1690
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ CHECKLIST: {
1691
+ ASSET: {
1692
+ ROLE: cl.asset.role || "None",
1693
+ ASSET_TYPE: cl.asset.assetType || "Computing",
1694
+ HOST_NAME: cl.asset.hostName ?? "",
1695
+ HOST_IP: cl.asset.hostIP ?? "",
1696
+ HOST_MAC: cl.asset.hostMAC ?? "",
1697
+ HOST_FQDN: cl.asset.hostFQDN ?? "",
1698
+ TARGET_COMMENT: cl.asset.targetComment ?? "",
1699
+ TECH_AREA: cl.asset.techArea ?? "",
1700
+ TARGET_KEY: cl.asset.targetKey ?? "",
1701
+ WEB_OR_DATABASE: cl.asset.webOrDatabase ? "true" : "false",
1702
+ WEB_DB_SITE: cl.asset.webDBSite ?? "",
1703
+ WEB_DB_INSTANCE: cl.asset.webDBInstance ?? ""
1704
+ },
1705
+ STIGS: { iSTIG: cl.stigs.map((s) => ({
1706
+ STIG_INFO: { SI_DATA: stigInfoSiData(s) },
1707
+ VULN: s.vulns.map(modelVulnToCkl)
1708
+ })) }
1709
+ } }, BUILD_OPTIONS$1)}`;
1710
+ }
1711
+ function stigInfoSiData(s) {
1712
+ return [
1713
+ {
1714
+ name: "version",
1715
+ data: s.version
1716
+ },
1717
+ {
1718
+ name: "classification",
1719
+ data: s.classification
1720
+ },
1721
+ {
1722
+ name: "stigid",
1723
+ data: s.stigID
1724
+ },
1725
+ {
1726
+ name: "releaseinfo",
1727
+ data: s.releaseInfo
1728
+ },
1729
+ {
1730
+ name: "title",
1731
+ data: s.title
1732
+ },
1733
+ {
1734
+ name: "uuid",
1735
+ data: s.uuid
1736
+ }
1737
+ ].filter((p) => p.data).map((p) => ({
1738
+ SID_NAME: p.name,
1739
+ SID_DATA: p.data
1740
+ }));
1741
+ }
1742
+ function modelVulnToCkl(v) {
1743
+ const typed = {
1744
+ Vuln_Num: v.vulnNum,
1745
+ Severity: v.severity ?? "",
1746
+ Group_Title: v.groupTitle ?? "",
1747
+ Rule_ID: v.ruleID ?? "",
1748
+ Rule_Ver: v.ruleVer ?? "",
1749
+ Rule_Title: v.ruleTitle ?? "",
1750
+ Vuln_Discuss: v.vulnDiscuss ?? "",
1751
+ Check_Content: v.checkContent ?? "",
1752
+ Fix_Text: v.fixText ?? "",
1753
+ Weight: v.weight || "10.0",
1754
+ Class: v.classification || "Unclass"
1755
+ };
1756
+ const stigData = [];
1757
+ for (const name of VULN_ATTR_ORDER) {
1758
+ const val = typed[name] ?? v.extra?.[name] ?? "";
1759
+ if (!val && !CORE_VULN_ATTR.has(name)) continue;
1760
+ stigData.push({
1761
+ VULN_ATTRIBUTE: name,
1762
+ ATTRIBUTE_DATA: val
1763
+ });
1764
+ }
1765
+ for (const cci of v.ccis) stigData.push({
1766
+ VULN_ATTRIBUTE: "CCI_REF",
1767
+ ATTRIBUTE_DATA: cci
1768
+ });
1769
+ return {
1770
+ STIG_DATA: stigData,
1771
+ STATUS: statusToCkl(v.status),
1772
+ FINDING_DETAILS: v.findingDetails ?? "",
1773
+ COMMENTS: v.comments ?? ""
1774
+ };
1775
+ }
1776
+ //#endregion
1777
+ //#region shared/typescript/checklist/cklb.ts
1778
+ /** Parse CKLB JSON into the format-neutral Checklist model. */
1779
+ function parseCklb(input) {
1780
+ const doc = parseJSON(input);
1781
+ if (!doc || !Array.isArray(doc.stigs) || doc.stigs.length === 0) throw new Error("parse cklb: no stigs[] found (not a CKLB document?)");
1782
+ const t = doc.target_data ?? {};
1783
+ const asset = {
1784
+ role: nz(t.role),
1785
+ assetType: nz(t.target_type),
1786
+ hostName: nz(t.host_name),
1787
+ hostIP: nz(t.ip_address),
1788
+ hostMAC: nz(t.mac_address),
1789
+ hostFQDN: nz(t.fqdn),
1790
+ targetComment: nz(t.comments),
1791
+ webOrDatabase: t.is_web_database === true,
1792
+ webDBSite: nz(t.web_db_site),
1793
+ webDBInstance: nz(t.web_db_instance),
1794
+ techArea: nz(t.technology_area),
1795
+ classification: nz(t.classification)
1796
+ };
1797
+ const stigs = doc.stigs.map((s) => ({
1798
+ stigID: nz(s.stig_id),
1799
+ title: nz(s.stig_name) || nz(s.display_name),
1800
+ displayName: nz(s.display_name),
1801
+ version: nz(s.version),
1802
+ releaseInfo: nz(s.release_info),
1803
+ uuid: nz(s.uuid),
1804
+ referenceIdentifier: nz(s.reference_identifier),
1805
+ vulns: (s.rules ?? []).map(cklbRuleToModel)
1806
+ }));
1807
+ return {
1808
+ format: "cklb",
1809
+ cklbVersion: nz(doc.cklb_version),
1810
+ asset,
1811
+ stigs
1812
+ };
1813
+ }
1814
+ /** Coerce a possibly-null/non-string JSON value to string | undefined.
1815
+ * Real STIG Viewer CKLB output uses null for unset fields (e.g.
1816
+ * target_data.classification: null); the model expects string | undefined. */
1817
+ function nz(v) {
1818
+ return typeof v === "string" ? v : void 0;
1819
+ }
1820
+ function cklbRuleToModel(r) {
1821
+ const extra = {};
1822
+ if (r.third_party_tools) extra["Third_Party_Tools"] = r.third_party_tools;
1823
+ if (r.srg_id) extra["SRG_ID"] = r.srg_id;
1824
+ return {
1825
+ vulnNum: nz(r.group_id) ?? "",
1826
+ ruleID: nz(r.rule_id),
1827
+ ruleVer: nz(r.rule_version),
1828
+ groupID: nz(r.group_id),
1829
+ groupTitle: nz(r.group_title),
1830
+ severity: nz(r.severity),
1831
+ ruleTitle: nz(r.rule_title),
1832
+ vulnDiscuss: nz(r.discussion),
1833
+ checkContent: nz(r.check_content),
1834
+ fixText: nz(r.fix_text),
1835
+ weight: nz(r.weight),
1836
+ classification: nz(r.classification),
1837
+ ccis: Array.isArray(r.ccis) ? r.ccis.filter((c) => typeof c === "string") : [],
1838
+ legacyIDs: r.legacy_ids,
1839
+ status: parseStatus(r.status),
1840
+ findingDetails: nz(r.finding_details),
1841
+ comments: nz(r.comments),
1842
+ extra
1843
+ };
1844
+ }
1845
+ /** Serialize the Checklist model to CKLB JSON. */
1846
+ function serializeCklb(cl) {
1847
+ const doc = {
1848
+ title: cklbTitle(cl),
1849
+ cklb_version: cl.cklbVersion || "1.0",
1850
+ active: false,
1851
+ has_path: false,
1852
+ target_data: {
1853
+ target_type: cl.asset.assetType || "Computing",
1854
+ host_name: cl.asset.hostName ?? "",
1855
+ ip_address: cl.asset.hostIP ?? "",
1856
+ mac_address: cl.asset.hostMAC ?? "",
1857
+ fqdn: cl.asset.hostFQDN ?? "",
1858
+ comments: cl.asset.targetComment ?? "",
1859
+ role: cl.asset.role || "None",
1860
+ is_web_database: cl.asset.webOrDatabase ?? false,
1861
+ technology_area: cl.asset.techArea ?? "",
1862
+ web_db_site: cl.asset.webDBSite ?? "",
1863
+ web_db_instance: cl.asset.webDBInstance ?? "",
1864
+ ...cl.asset.classification ? { classification: cl.asset.classification } : {}
1865
+ },
1866
+ stigs: cl.stigs.map((s) => ({
1867
+ stig_name: s.title ?? "",
1868
+ display_name: s.displayName || s.title || "",
1869
+ stig_id: s.stigID ?? "",
1870
+ release_info: s.releaseInfo ?? "",
1871
+ version: s.version ?? "",
1872
+ uuid: s.uuid ?? "",
1873
+ ...s.referenceIdentifier ? { reference_identifier: s.referenceIdentifier } : {},
1874
+ rules: s.vulns.map((v) => ({
1875
+ group_id: v.groupID || v.vulnNum,
1876
+ group_title: v.groupTitle ?? "",
1877
+ rule_id: v.ruleID ?? "",
1878
+ rule_version: v.ruleVer ?? "",
1879
+ rule_title: v.ruleTitle ?? "",
1880
+ severity: v.severity ?? "",
1881
+ weight: v.weight || "10.0",
1882
+ check_content: v.checkContent ?? "",
1883
+ fix_text: v.fixText ?? "",
1884
+ discussion: v.vulnDiscuss ?? "",
1885
+ ccis: v.ccis,
1886
+ ...v.legacyIDs && v.legacyIDs.length ? { legacy_ids: v.legacyIDs } : {},
1887
+ status: statusToCklb(v.status),
1888
+ comments: v.comments ?? "",
1889
+ finding_details: v.findingDetails ?? "",
1890
+ ...v.extra?.["Third_Party_Tools"] ? { third_party_tools: v.extra["Third_Party_Tools"] } : {}
1891
+ }))
1892
+ }))
1893
+ };
1894
+ return JSON.stringify(doc, null, 2);
1895
+ }
1896
+ function cklbTitle(cl) {
1897
+ return cl.stigs[0]?.title || "STIG Checklist";
1898
+ }
1899
+ //#endregion
1900
+ //#region shared/typescript/checklist/to-hdf.ts
1901
+ const CONVERTER_VERSION = "1.0.0";
1902
+ /**
1903
+ * Map the format-neutral Checklist model to an HDF Results object.
1904
+ * controlType is derived per-Vuln from CCI->NIST; verificationMethod and
1905
+ * applicability are omitted (the checklist format cannot substantiate them).
1906
+ * Original-format metadata is stashed in extensions/tags for round-trip.
1907
+ */
1908
+ function checklistToHdf(cl, resultsChecksum, generatorName) {
1909
+ const hdf = {
1910
+ baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
1911
+ generator: {
1912
+ name: generatorName,
1913
+ version: CONVERTER_VERSION
1914
+ },
1915
+ tool: {
1916
+ name: "DISA STIG Viewer",
1917
+ format: cl.format === "cklb" ? "CKLB" : "CKL"
1918
+ },
1919
+ timestamp: /* @__PURE__ */ new Date()
1920
+ };
1921
+ const component = assetToComponent(cl.asset);
1922
+ if (component) hdf.components = [component];
1923
+ const ext = rootExtensions(cl);
1924
+ if (Object.keys(ext).length > 0) hdf.extensions = ext;
1925
+ return hdf;
1926
+ }
1927
+ function stigToBaseline(s, resultsChecksum) {
1928
+ const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map(vulnToRequirement), { resultsChecksum });
1929
+ if (s.title) baseline.title = s.title;
1930
+ if (s.version) baseline.version = s.version;
1931
+ const ext = baselineExtensions(s);
1932
+ if (Object.keys(ext).length > 0) baseline.extensions = ext;
1933
+ return baseline;
1934
+ }
1935
+ function vulnToRequirement(v) {
1936
+ const severity = (v.severity ?? "").toLowerCase();
1937
+ const impact = severity ? severityToImpact(severity) : .5;
1938
+ const descriptions = [{
1939
+ label: "default",
1940
+ data: stripHTML(v.vulnDiscuss ?? "")
1941
+ }];
1942
+ if (v.checkContent) descriptions.push({
1943
+ label: "check",
1944
+ data: stripHTML(v.checkContent)
1945
+ });
1946
+ if (v.fixText) descriptions.push({
1947
+ label: "fix",
1948
+ data: stripHTML(v.fixText)
1949
+ });
1950
+ const message = [v.findingDetails, v.comments].map((s) => (s ?? "").trim()).filter(Boolean).join("\n\n");
1951
+ const result = createResult(statusToHdf(v.status), message, { codeDesc: `STIG rule ${v.ruleVer ?? ""}` });
1952
+ const tags = {};
1953
+ let nistTags = [];
1954
+ if (v.ccis.length > 0) {
1955
+ tags["cci"] = v.ccis;
1956
+ nistTags = [...new Set(v.ccis.flatMap((c) => getCCINistMappings(c) ?? []))].sort();
1957
+ tags["nist"] = nistTags;
1958
+ } else tags["nist"] = [];
1959
+ setIf(tags, "rid", v.ruleID);
1960
+ setIf(tags, "stig_id", v.ruleVer);
1961
+ setIf(tags, "gtitle", v.groupTitle);
1962
+ setIf(tags, "group_id", v.groupID);
1963
+ setIf(tags, "weight", v.weight);
1964
+ setIf(tags, "severity", severity);
1965
+ if (v.legacyIDs && v.legacyIDs.length) tags["legacy_ids"] = v.legacyIDs;
1966
+ if (v.extra && Object.keys(v.extra).length > 0) tags["cklMetadata"] = { ...v.extra };
1967
+ const req = createRequirement(v.vulnNum, v.ruleTitle ?? v.vulnNum, descriptions, impact, [result], { tags });
1968
+ if (severity) req.severity = severity;
1969
+ const controlType = deriveControlTypeFromTags(nistTags);
1970
+ if (controlType !== void 0) req.controlType = controlType;
1971
+ return req;
1972
+ }
1973
+ function assetToComponent(a) {
1974
+ if (!a.hostName && !a.hostIP && !a.hostFQDN) return void 0;
1975
+ const c = {
1976
+ name: a.hostName || a.hostFQDN || a.hostIP || "",
1977
+ type: TargetType.Host
1978
+ };
1979
+ if (a.hostIP) c.ipAddress = a.hostIP;
1980
+ if (a.hostFQDN) c.fqdn = a.hostFQDN;
1981
+ if (a.hostMAC) c.macAddress = a.hostMAC;
1982
+ return c;
1983
+ }
1984
+ function rootExtensions(cl) {
1985
+ const ext = { checklistFormat: cl.format || "ckl" };
1986
+ if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
1987
+ const ax = {};
1988
+ setIf(ax, "role", cl.asset.role);
1989
+ setIf(ax, "assetType", cl.asset.assetType);
1990
+ setIf(ax, "marking", cl.asset.marking);
1991
+ setIf(ax, "targetKey", cl.asset.targetKey);
1992
+ setIf(ax, "techArea", cl.asset.techArea);
1993
+ setIf(ax, "targetComment", cl.asset.targetComment);
1994
+ setIf(ax, "webDbSite", cl.asset.webDBSite);
1995
+ setIf(ax, "webDbInstance", cl.asset.webDBInstance);
1996
+ setIf(ax, "classification", cl.asset.classification);
1997
+ if (cl.asset.webOrDatabase) ax["webOrDatabase"] = true;
1998
+ if (Object.keys(ax).length > 0) ext["assetExtras"] = ax;
1999
+ return ext;
2000
+ }
2001
+ function baselineExtensions(s) {
2002
+ const ext = {};
2003
+ setIf(ext, "stigid", s.stigID);
2004
+ setIf(ext, "uuid", s.uuid);
2005
+ setIf(ext, "releaseInfo", s.releaseInfo);
2006
+ setIf(ext, "displayName", s.displayName);
2007
+ setIf(ext, "referenceIdentifier", s.referenceIdentifier);
2008
+ setIf(ext, "classification", s.classification);
2009
+ return ext;
2010
+ }
2011
+ function setIf(m, key, val) {
2012
+ if (val) m[key] = val;
2013
+ }
2014
+ //#endregion
2015
+ //#region shared/typescript/checklist/from-hdf.ts
2016
+ /**
2017
+ * Map HDF Results back to the format-neutral Checklist model. When the HDF
2018
+ * carries checklist passthrough (extensions/tags from checklistToHdf), the
2019
+ * original fields are reproduced losslessly; otherwise required checklist
2020
+ * fields are synthesized best-effort so any HDF yields a valid checklist.
2021
+ */
2022
+ function hdfToChecklist(input) {
2023
+ const hdf = parseJSON(input);
2024
+ if (!hdf || !Array.isArray(hdf.baselines) || hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
2025
+ const ext = hdf.extensions ?? {};
2026
+ const format = strVal(ext, "checklistFormat") || "ckl";
2027
+ const cklbVersion = strVal(ext, "cklbVersion");
2028
+ const asset = buildAsset(hdf, ext);
2029
+ const stigs = hdf.baselines.map(baselineToStig);
2030
+ return {
2031
+ format,
2032
+ cklbVersion: cklbVersion || void 0,
2033
+ asset,
2034
+ stigs
2035
+ };
2036
+ }
2037
+ function buildAsset(hdf, ext) {
2038
+ const asset = {};
2039
+ const comp = hdf.components?.[0];
2040
+ if (comp) {
2041
+ asset.hostName = comp.name;
2042
+ asset.hostIP = comp.ipAddress;
2043
+ asset.hostFQDN = comp.fqdn;
2044
+ asset.hostMAC = comp.macAddress;
2045
+ }
2046
+ const ax = ext["assetExtras"];
2047
+ if (ax && typeof ax === "object") {
2048
+ const a = ax;
2049
+ asset.role = strVal(a, "role");
2050
+ asset.assetType = strVal(a, "assetType");
2051
+ asset.marking = strVal(a, "marking");
2052
+ asset.targetKey = strVal(a, "targetKey");
2053
+ asset.techArea = strVal(a, "techArea");
2054
+ asset.targetComment = strVal(a, "targetComment");
2055
+ asset.webDBSite = strVal(a, "webDbSite");
2056
+ asset.webDBInstance = strVal(a, "webDbInstance");
2057
+ asset.classification = strVal(a, "classification");
2058
+ asset.webOrDatabase = a["webOrDatabase"] === true;
2059
+ }
2060
+ return asset;
2061
+ }
2062
+ function baselineToStig(bl) {
2063
+ const ext = bl.extensions ?? {};
2064
+ return {
2065
+ stigID: strVal(ext, "stigid") || bl.title || "",
2066
+ title: bl.title,
2067
+ version: bl.version,
2068
+ uuid: strVal(ext, "uuid"),
2069
+ releaseInfo: strVal(ext, "releaseInfo"),
2070
+ displayName: strVal(ext, "displayName"),
2071
+ referenceIdentifier: strVal(ext, "referenceIdentifier"),
2072
+ classification: strVal(ext, "classification"),
2073
+ vulns: bl.requirements.map(requirementToVuln)
2074
+ };
2075
+ }
2076
+ function requirementToVuln(req) {
2077
+ const tags = req.tags ?? {};
2078
+ const descs = req.descriptions ?? [];
2079
+ return {
2080
+ vulnNum: req.id,
2081
+ ruleID: strVal(tags, "rid"),
2082
+ ruleVer: strVal(tags, "stig_id"),
2083
+ groupID: strVal(tags, "group_id") || req.id,
2084
+ groupTitle: strVal(tags, "gtitle"),
2085
+ ruleTitle: req.title,
2086
+ weight: strVal(tags, "weight"),
2087
+ severity: resolveSeverity(req, tags),
2088
+ vulnDiscuss: descData(descs, "default"),
2089
+ checkContent: descData(descs, "check"),
2090
+ fixText: descData(descs, "fix"),
2091
+ ccis: resolveCcis(tags),
2092
+ legacyIDs: strSlice(tags, "legacy_ids"),
2093
+ status: statusFromHdf(req.results?.[0]?.status),
2094
+ findingDetails: req.results?.[0]?.message,
2095
+ extra: extractCklMetadata(tags)
2096
+ };
2097
+ }
2098
+ function resolveSeverity(req, tags) {
2099
+ const tagSev = strVal(tags, "severity");
2100
+ if (tagSev) return tagSev;
2101
+ if (req.severity) return String(req.severity).toLowerCase();
2102
+ const i = req.impact ?? 0;
2103
+ if (i >= .7) return "high";
2104
+ if (i >= .4) return "medium";
2105
+ if (i > 0) return "low";
2106
+ return "";
2107
+ }
2108
+ function resolveCcis(tags) {
2109
+ const direct = strSlice(tags, "cci");
2110
+ if (direct.length > 0) return direct;
2111
+ const nist = strSlice(tags, "nist");
2112
+ if (nist.length > 0) {
2113
+ const ccis = nistToCci(nist);
2114
+ if (ccis.length > 0) return ccis;
2115
+ }
2116
+ return [];
2117
+ }
2118
+ function extractCklMetadata(tags) {
2119
+ const meta = tags["cklMetadata"];
2120
+ if (!meta || typeof meta !== "object") return void 0;
2121
+ const out = {};
2122
+ for (const [k, v] of Object.entries(meta)) if (typeof v === "string") out[k] = v;
2123
+ return Object.keys(out).length > 0 ? out : void 0;
2124
+ }
2125
+ function descData(descs, label) {
2126
+ return descs.find((d) => d.label === label)?.data ?? "";
2127
+ }
2128
+ function strVal(m, key) {
2129
+ const v = m[key];
2130
+ return typeof v === "string" ? v : "";
2131
+ }
2132
+ function strSlice(m, key) {
2133
+ const v = m[key];
2134
+ if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
2135
+ return [];
2136
+ }
2137
+ //#endregion
2138
+ //#region converters/ckl-to-hdf/typescript/converter.ts
2139
+ /**
2140
+ * Convert a DISA STIG Viewer .ckl document to HDF Results JSON.
2141
+ *
2142
+ * Parsing and the HDF mapping live in the shared checklist module. v3.2
2143
+ * classification: controlType is derived per-VULN from CCI->NIST;
2144
+ * verificationMethod and applicability are omitted (the checklist format
2145
+ * cannot substantiate them — see the Go package doc and build-converter
2146
+ * skill Step 4d).
2147
+ */
2148
+ async function convertCklToHdf(input) {
2149
+ validateInputSize(input, "ckl-to-hdf");
2150
+ const resultsChecksum = await inputChecksum(input);
2151
+ const checklist = parseCkl(input);
2152
+ return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "ckl-to-hdf"), null, 2);
2153
+ }
2154
+ //#endregion
2155
+ //#region converters/cklb-to-hdf/typescript/converter.ts
2156
+ /**
2157
+ * Convert a DISA STIG Viewer 3.x .cklb document to HDF Results JSON.
2158
+ *
2159
+ * Parsing and the HDF mapping live in the shared checklist module. v3.2
2160
+ * classification: controlType is derived per-rule from CCI->NIST;
2161
+ * verificationMethod and applicability are omitted (the checklist format
2162
+ * cannot substantiate them — see the Go package doc and build-converter
2163
+ * skill Step 4d).
2164
+ */
2165
+ async function convertCklbToHdf(input) {
2166
+ validateInputSize(input, "cklb-to-hdf");
2167
+ const resultsChecksum = await inputChecksum(input);
2168
+ const checklist = parseCklb(input);
2169
+ return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "cklb-to-hdf"), null, 2);
2170
+ }
2171
+ //#endregion
2172
+ //#region converters/hdf-to-ckl/typescript/converter.ts
2173
+ /**
2174
+ * Convert HDF Results JSON into a DISA STIG Viewer checklist (.ckl XML).
2175
+ *
2176
+ * This is a thin wrapper over the shared checklist module, mirroring the
2177
+ * hdf-to-csv reverse converter. It produces a STIG Viewer 2.x .ckl from ANY
2178
+ * HDF: when the HDF carries checklist passthrough (extensions/tags written by
2179
+ * a prior ckl/cklb-to-hdf conversion) the original fields are reproduced
2180
+ * losslessly; otherwise the required checklist fields are synthesized
2181
+ * best-effort (id->Vuln_Num, nist->CCI reverse, status reverse) with safe
2182
+ * defaults. Mirrors heimdall2 PR #4841.
2183
+ *
2184
+ * @param input HDF Results JSON string
2185
+ * @returns CKL XML string (with the `<?xml ...?>` header)
2186
+ */
2187
+ function convertHdfToCkl(input) {
2188
+ return serializeCkl(hdfToChecklist(input));
2189
+ }
2190
+ //#endregion
2191
+ //#region converters/hdf-to-cklb/typescript/converter.ts
2192
+ /**
2193
+ * Convert HDF Results JSON into a DISA STIG Viewer 3.x checklist (.cklb, JSON).
2194
+ *
2195
+ * The HDF->checklist mapping and CKLB serialization live in the shared
2196
+ * checklist module; this converter is a thin wrapper. Any HDF input produces a
2197
+ * valid CKLB: when the HDF carries checklist passthrough (it originated from a
2198
+ * CKL/CKLB via the reverse converters), the original fields are reproduced
2199
+ * losslessly; otherwise the required checklist fields are synthesized
2200
+ * best-effort from the HDF requirements, tags, and results.
2201
+ *
2202
+ * @param input HDF Results JSON string
2203
+ * @returns CKLB JSON string
2204
+ */
2205
+ function convertHdfToCklb(input) {
2206
+ return serializeCklb(hdfToChecklist(input));
2207
+ }
2208
+ //#endregion
1213
2209
  //#region converters/snyk-to-hdf/typescript/converter.ts
1214
2210
  /**
1215
2211
  * Formats the "from" array as a human-readable dependency path.
@@ -1221,7 +2217,28 @@ function formatDependencyPath(from) {
1221
2217
  /**
1222
2218
  * Builds a single EvaluatedRequirement from a group of vulnerabilities sharing an ID.
1223
2219
  */
1224
- function buildRequirement$15(vulnID, vulns) {
2220
+ /**
2221
+ * Map Snyk's `packageManager` value to an Affected_Package ecosystem.
2222
+ * Snyk reports values that don't always match PURL types one-to-one
2223
+ * (pip → pypi, rubygems → gem, yarn → npm). Unknown managers fall back
2224
+ * to `generic`.
2225
+ */
2226
+ function ecosystemFromSnykPackageManager(pm) {
2227
+ if (!pm) return Ecosystem.Generic;
2228
+ const lower = pm.toLowerCase();
2229
+ if (lower === "pip" || lower === "pip3") return Ecosystem.Pypi;
2230
+ if (lower === "rubygems" || lower === "bundler") return Ecosystem.Gem;
2231
+ if (lower === "yarn" || lower === "npm") return Ecosystem.Npm;
2232
+ return ecosystemFromPurlType(lower);
2233
+ }
2234
+ /** Synthesize a `pkg:<type>/<name>@<version>` PURL when the ecosystem
2235
+ * maps cleanly. Returns undefined for `generic` so we don't emit a
2236
+ * fake `pkg:generic/...` PURL that downstream tools can't dereference. */
2237
+ function synthesizePurl(ecosystem, name, version) {
2238
+ if (ecosystem === Ecosystem.Generic) return void 0;
2239
+ return `pkg:${ecosystem}/${name}@${version}`;
2240
+ }
2241
+ function buildRequirement$16(vulnID, vulns, packageManager) {
1225
2242
  const rep = vulns[0];
1226
2243
  const cweIDs = rep.identifiers.CWE ?? [];
1227
2244
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -1237,7 +2254,24 @@ function buildRequirement$15(vulnID, vulns) {
1237
2254
  data: rep.description
1238
2255
  }];
1239
2256
  const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatDependencyPath(vuln.from) }));
1240
- return createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
2257
+ const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
2258
+ const controlType = deriveControlTypeFromTags(nist);
2259
+ if (controlType !== void 0) req.controlType = controlType;
2260
+ req.verificationMethod = VerificationMethodEnum.Automated;
2261
+ const name = rep.packageName ?? rep.moduleName;
2262
+ const version = rep.version;
2263
+ if (name && version) {
2264
+ const ecosystem = ecosystemFromSnykPackageManager(packageManager);
2265
+ const pkg = buildAffectedPackage$1({
2266
+ name,
2267
+ version,
2268
+ ecosystem,
2269
+ purl: synthesizePurl(ecosystem, name, version),
2270
+ fixedInVersion: rep.fixedIn?.[0]
2271
+ });
2272
+ if (pkg) req.affectedPackages = [pkg];
2273
+ }
2274
+ return req;
1241
2275
  }
1242
2276
  /**
1243
2277
  * Converts a single Snyk project report to an HDF baseline.
@@ -1253,7 +2287,11 @@ function convertSingleProject(report, resultsChecksum) {
1253
2287
  else groups.set(vuln.id, [vuln]);
1254
2288
  }
1255
2289
  const requirements = [];
1256
- for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$15(vulnID, vulns));
2290
+ for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, report.packageManager));
2291
+ if (requirements.length === 0) {
2292
+ const target = report.projectName ?? report.path ?? "project";
2293
+ requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
2294
+ }
1257
2295
  return createMinimalBaseline("Snyk Scan", requirements, {
1258
2296
  resultsChecksum,
1259
2297
  title: `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`,
@@ -1298,7 +2336,7 @@ async function convertSnykToHdf(input) {
1298
2336
  baselines,
1299
2337
  components: [{
1300
2338
  name: targetName,
1301
- type: Copyright.Application
2339
+ type: TargetType.Application
1302
2340
  }],
1303
2341
  timestamp: /* @__PURE__ */ new Date()
1304
2342
  });
@@ -1313,7 +2351,7 @@ const IMPACT_MAPPING$8 = new Map([
1313
2351
  ["negligible", 0],
1314
2352
  ["unknown", .5]
1315
2353
  ]);
1316
- function getImpact$8(severity) {
2354
+ function getImpact$9(severity) {
1317
2355
  if (!severity) return .5;
1318
2356
  return IMPACT_MAPPING$8.get(severity.toLowerCase()) ?? .5;
1319
2357
  }
@@ -1366,11 +2404,97 @@ function buildCodeDesc$8(match) {
1366
2404
  }
1367
2405
  return parts.join(" | ");
1368
2406
  }
2407
+ function cvssVersionToSchema(v) {
2408
+ switch (v) {
2409
+ case "2.0": return Version.The20;
2410
+ case "3.0": return Version.The30;
2411
+ case "4.0": return Version.The40;
2412
+ default: return Version.The31;
2413
+ }
2414
+ }
2415
+ function cvssBandSeverity(score) {
2416
+ switch (cvssScoreToSeverity(score)) {
2417
+ case "critical": return CVSSSeverity.Critical;
2418
+ case "high": return CVSSSeverity.High;
2419
+ case "medium": return CVSSSeverity.Medium;
2420
+ case "low": return CVSSSeverity.Low;
2421
+ default: return CVSSSeverity.None;
2422
+ }
2423
+ }
2424
+ function buildCvssEntries$1(vuln) {
2425
+ if (!vuln.cvss || vuln.cvss.length === 0) return;
2426
+ const entries = [];
2427
+ for (const c of vuln.cvss) {
2428
+ const entry = { version: cvssVersionToSchema(c.version) };
2429
+ if (c.vector) entry.baseVector = c.vector;
2430
+ const score = c.metrics?.baseScore;
2431
+ if (typeof score === "number" && Number.isFinite(score)) {
2432
+ entry.baseScore = score;
2433
+ entry.baseSeverity = cvssBandSeverity(score);
2434
+ }
2435
+ if (vuln.id) entry.source = vuln.id;
2436
+ if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
2437
+ entries.push(entry);
2438
+ }
2439
+ return entries.length > 0 ? entries : void 0;
2440
+ }
2441
+ function mapGrypeTypeToEcosystem(grypeType) {
2442
+ switch ((grypeType ?? "").toLowerCase()) {
2443
+ case "rpm": return Ecosystem.RPM;
2444
+ case "deb": return Ecosystem.Deb;
2445
+ case "npm": return Ecosystem.Npm;
2446
+ case "python": return Ecosystem.Pypi;
2447
+ case "gem": return Ecosystem.Gem;
2448
+ case "go-module": return Ecosystem.Go;
2449
+ case "java-archive":
2450
+ case "jenkins-plugin": return Ecosystem.Maven;
2451
+ case "dotnet": return Ecosystem.Nuget;
2452
+ case "rust-crate": return Ecosystem.Cargo;
2453
+ default: return Ecosystem.Generic;
2454
+ }
2455
+ }
2456
+ function buildAffectedPackages(match) {
2457
+ const artifact = match.artifact;
2458
+ const pkg = {
2459
+ name: artifact.name,
2460
+ version: artifact.version,
2461
+ ecosystem: mapGrypeTypeToEcosystem(artifact.type)
2462
+ };
2463
+ if (artifact.cpes && artifact.cpes.length > 0 && artifact.cpes[0]) pkg.cpe = artifact.cpes[0];
2464
+ if (artifact.purl) pkg.purl = artifact.purl;
2465
+ const fix = match.vulnerability.fix;
2466
+ if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
2467
+ return [pkg];
2468
+ }
2469
+ const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
2470
+ function extractCwe(raw) {
2471
+ if (!raw || raw.length === 0) return void 0;
2472
+ const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
2473
+ return out.length === 0 ? void 0 : out;
2474
+ }
2475
+ function buildEpss$1(entries) {
2476
+ if (!entries || entries.length === 0) return void 0;
2477
+ const e = entries[0];
2478
+ if (!e.date) return void 0;
2479
+ return {
2480
+ score: e.epss ?? 0,
2481
+ percentile: e.percentile ?? 0,
2482
+ date: e.date
2483
+ };
2484
+ }
2485
+ function buildKev(k) {
2486
+ if (!k) return void 0;
2487
+ const out = { inKev: Boolean(k.inKev) };
2488
+ if (k.dateAdded) out.dateAdded = k.dateAdded;
2489
+ if (k.dueDate) out.dueDate = k.dueDate;
2490
+ if (k.notes) out.notes = k.notes;
2491
+ return out;
2492
+ }
1369
2493
  function convertMatchToRequirement(match, isIgnored) {
1370
2494
  const vuln = match.vulnerability;
1371
2495
  const cveId = vuln.id;
1372
2496
  const severity = vuln.severity;
1373
- const impact = getImpact$8(severity);
2497
+ const impact = getImpact$9(severity);
1374
2498
  const description = getDescription(vuln, match.relatedVulnerabilities);
1375
2499
  const fixInfo = getFixInfo(vuln.fix);
1376
2500
  const cvssInfo = getCVSSInfo(vuln, match.relatedVulnerabilities);
@@ -1392,7 +2516,7 @@ function convertMatchToRequirement(match, isIgnored) {
1392
2516
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
1393
2517
  };
1394
2518
  const tags = buildNistCciTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS, nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS));
1395
- return {
2519
+ const requirement = {
1396
2520
  id: isIgnored ? `Grype-Ignored-Match/${cveId}` : `Grype/${cveId}`,
1397
2521
  impact,
1398
2522
  results: [result],
@@ -1411,8 +2535,21 @@ function convertMatchToRequirement(match, isIgnored) {
1411
2535
  data: cvssInfo
1412
2536
  }
1413
2537
  ],
1414
- refs: refs.length > 0 ? refs.map((url) => ({ url })) : void 0
2538
+ refs: refs.length > 0 ? refs.map((url) => ({ url })) : void 0,
2539
+ verificationMethod: VerificationMethodEnum.Automated
1415
2540
  };
2541
+ const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
2542
+ if (controlType !== void 0) requirement.controlType = controlType;
2543
+ const cvss = buildCvssEntries$1(vuln);
2544
+ if (cvss) requirement.cvss = cvss;
2545
+ requirement.affectedPackages = buildAffectedPackages(match);
2546
+ const cwe = extractCwe(vuln.cwe);
2547
+ if (cwe) requirement.cwe = cwe;
2548
+ const epss = buildEpss$1(vuln.epss);
2549
+ if (epss) requirement.epss = epss;
2550
+ const kev = buildKev(vuln.kev);
2551
+ if (kev) requirement.kev = kev;
2552
+ return requirement;
1416
2553
  }
1417
2554
  async function convertGrypeToHdf(input) {
1418
2555
  validateInputSize(input, "grype");
@@ -1432,6 +2569,7 @@ async function convertGrypeToHdf(input) {
1432
2569
  for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
1433
2570
  }
1434
2571
  const targetName = grypeData.source?.target?.userInput || "Grype Scan";
2572
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
1435
2573
  const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
1436
2574
  return buildHdfResults({
1437
2575
  generatorName: grypeData.descriptor?.name || "grype",
@@ -1440,7 +2578,7 @@ async function convertGrypeToHdf(input) {
1440
2578
  toolVersion: grypeData.descriptor?.version,
1441
2579
  baselines: [baseline],
1442
2580
  components: [{
1443
- type: Copyright.Artifact,
2581
+ type: TargetType.Artifact,
1444
2582
  name: targetName
1445
2583
  }],
1446
2584
  timestamp: grypeData.descriptor?.timestamp ? new Date(grypeData.descriptor.timestamp) : /* @__PURE__ */ new Date()
@@ -1448,6 +2586,8 @@ async function convertGrypeToHdf(input) {
1448
2586
  }
1449
2587
  //#endregion
1450
2588
  //#region converters/nessus-to-hdf/typescript/converter.ts
2589
+ const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
2590
+ const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
1451
2591
  const converterVersion = "1.0.0";
1452
2592
  const IMPACT_MAPPING$7 = {
1453
2593
  "4": .9,
@@ -1475,7 +2615,9 @@ async function convertNessusToHdf(nessusXml) {
1475
2615
  "preference",
1476
2616
  "tag",
1477
2617
  "ReportItem",
1478
- "ReportHost"
2618
+ "ReportHost",
2619
+ "cwe",
2620
+ "cve"
1479
2621
  ]);
1480
2622
  const policyName = parsed.NessusClientData_v2.Policy.policyName;
1481
2623
  const version = extractVersion(parsed);
@@ -1497,7 +2639,7 @@ async function convertNessusToHdf(nessusXml) {
1497
2639
  components,
1498
2640
  statistics: { duration },
1499
2641
  generator: {
1500
- name: "hdf-converters",
2642
+ name: "nessus-to-hdf",
1501
2643
  version: converterVersion
1502
2644
  },
1503
2645
  tool: { name: "Nessus" },
@@ -1544,6 +2686,12 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
1544
2686
  if (truncatedItems) console.warn(`WARNING: Input truncated at ${limitedItems.length} ReportItem items (original: ${items.length})`);
1545
2687
  requirements = limitedItems.map((item) => convertReportItemToRequirement(item, host));
1546
2688
  } else requirements = [];
2689
+ if (requirements.length === 0) {
2690
+ const target = host.name || getHostPropertyValue(host, "host-ip") || "host";
2691
+ const startTimeStr = getHostPropertyValue(host, "HOST_START");
2692
+ const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
2693
+ requirements = [buildNoFindingsRequirement("nessus-no-findings", `Nessus scanned ${target} and reported zero findings.`, startTime)];
2694
+ }
1547
2695
  return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
1548
2696
  title: `Nessus ${policyName}`,
1549
2697
  version,
@@ -1554,17 +2702,164 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
1554
2702
  }
1555
2703
  function convertReportItemToRequirement(item, host) {
1556
2704
  const isCompliance = !!item["compliance-reference"];
2705
+ const id = isCompliance ? parseComplianceRef(item["compliance-reference"], "Vuln-ID")[0] || item["pluginID"] : item["pluginID"];
2706
+ const title = isCompliance ? item["compliance-check-name"] || item["pluginName"] : item["pluginName"];
2707
+ const descriptions = buildDescriptions(item, isCompliance);
2708
+ const impact = calculateImpact(item, isCompliance);
2709
+ const tags = buildTags$2(item, isCompliance);
2710
+ const refs = buildRefs(item);
2711
+ const results = [buildResult$1(item, host, isCompliance)];
2712
+ const code = JSON.stringify(item, null, 2);
2713
+ const nistTags = tags["nist"];
2714
+ const controlType = deriveControlTypeFromTags(nistTags ?? []);
2715
+ const verificationMethod = deriveVerificationMethod(code);
2716
+ const req = {
2717
+ id,
2718
+ title,
2719
+ descriptions,
2720
+ impact,
2721
+ tags,
2722
+ refs,
2723
+ results,
2724
+ code
2725
+ };
2726
+ if (controlType !== void 0) req.controlType = controlType;
2727
+ if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
2728
+ if (!isCompliance) {
2729
+ const cvssEntries = buildCvssEntries(item);
2730
+ if (cvssEntries.length > 0) req.cvss = cvssEntries;
2731
+ const cweIDs = buildCweIDs(item);
2732
+ if (cweIDs.length > 0) req.cwe = cweIDs;
2733
+ const epss = buildEpss(item, host);
2734
+ if (epss !== void 0) req.epss = epss;
2735
+ }
2736
+ return req;
2737
+ }
2738
+ /**
2739
+ * Build a structured Cvss entry for a CVE finding. Returns an array because
2740
+ * the schema models cvss as a multi-entry array (Nessus emits one entry per
2741
+ * item; multi-vendor convergence may yield more).
2742
+ */
2743
+ function buildCvssEntries(item) {
2744
+ const source = (item.cvss_score_source || "").trim();
2745
+ if (!source || !CVE_SOURCE_RE.test(source)) return [];
2746
+ const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
2747
+ const hasV2 = !!(item.cvss_vector || item.cvss_base_score);
2748
+ if (!hasV3 && !hasV2) return [];
2749
+ let version;
2750
+ let baseVector;
2751
+ let baseScore;
2752
+ let threatVector;
2753
+ let threatScore;
2754
+ if (hasV3) {
2755
+ version = detectV3Version(item.cvss3_vector ?? "");
2756
+ baseVector = item.cvss3_vector || void 0;
2757
+ baseScore = parseFloatOrUndef(item.cvss3_base_score);
2758
+ threatVector = stripVersionPrefix(item.cvss3_temporal_vector);
2759
+ threatScore = item.cvss3_temporal_score ? parseFloatSafe(item.cvss3_temporal_score) : void 0;
2760
+ } else {
2761
+ version = Version.The20;
2762
+ baseVector = stripV2Prefix(item.cvss_vector ?? "") || void 0;
2763
+ baseScore = parseFloatOrUndef(item.cvss_base_score);
2764
+ threatVector = stripV2Prefix(item.cvss_temporal_vector ?? "") || void 0;
2765
+ threatScore = item.cvss_temporal_score ? parseFloatSafe(item.cvss_temporal_score) : void 0;
2766
+ }
2767
+ const entry = {
2768
+ version,
2769
+ source
2770
+ };
2771
+ if (baseVector) entry.baseVector = baseVector;
2772
+ if (baseScore !== void 0) {
2773
+ entry.baseScore = baseScore;
2774
+ const baseSeverity = mapCvssSeverity(baseScore);
2775
+ if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
2776
+ }
2777
+ if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
2778
+ if (threatScore !== void 0) {
2779
+ entry.threatScore = threatScore;
2780
+ entry.computedScore = threatScore;
2781
+ const computedSeverity = mapCvssSeverity(threatScore);
2782
+ if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
2783
+ }
2784
+ return [entry];
2785
+ }
2786
+ function detectV3Version(vector) {
2787
+ if (vector.startsWith("CVSS:3.1/")) return Version.The31;
2788
+ if (vector.startsWith("CVSS:3.0/")) return Version.The30;
2789
+ return Version.The30;
2790
+ }
2791
+ function stripVersionPrefix(vector) {
2792
+ if (!vector) return void 0;
2793
+ for (const prefix of [
2794
+ "CVSS:3.0/",
2795
+ "CVSS:3.1/",
2796
+ "CVSS:4.0/"
2797
+ ]) if (vector.startsWith(prefix)) return vector.slice(prefix.length);
2798
+ return vector;
2799
+ }
2800
+ function stripV2Prefix(vector) {
2801
+ return vector.startsWith("CVSS2#") ? vector.slice(6) : vector;
2802
+ }
2803
+ function parseFloatSafe(s) {
2804
+ if (!s) return 0;
2805
+ const f = Number.parseFloat(s);
2806
+ return Number.isFinite(f) ? f : 0;
2807
+ }
2808
+ function parseFloatOrUndef(s) {
2809
+ if (s === void 0 || s === "") return void 0;
2810
+ const f = Number.parseFloat(s);
2811
+ return Number.isFinite(f) ? f : void 0;
2812
+ }
2813
+ function mapCvssSeverity(score) {
2814
+ switch (cvssScoreToSeverity(score)) {
2815
+ case "critical": return CVSSSeverity.Critical;
2816
+ case "high": return CVSSSeverity.High;
2817
+ case "medium": return CVSSSeverity.Medium;
2818
+ case "low": return CVSSSeverity.Low;
2819
+ case "none": return CVSSSeverity.None;
2820
+ default: return;
2821
+ }
2822
+ }
2823
+ /**
2824
+ * Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
2825
+ * numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
2826
+ * forms appear. Output is "CWE-N" form per schema convention.
2827
+ */
2828
+ function buildCweIDs(item) {
2829
+ if (!item.cwe) return [];
2830
+ const raws = Array.isArray(item.cwe) ? item.cwe : [item.cwe];
2831
+ const seen = /* @__PURE__ */ new Set();
2832
+ for (const raw of raws) {
2833
+ const text = String(raw);
2834
+ for (const m of text.matchAll(CWE_PATTERN)) if (m[1]) seen.add(m[1]);
2835
+ for (const tok of text.split(/[^0-9]+/)) if (tok !== "") seen.add(tok);
2836
+ }
2837
+ if (seen.size === 0) return [];
2838
+ return [...seen].sort().map((id) => `CWE-${id}`);
2839
+ }
2840
+ /**
2841
+ * Build a structured Epss entry when the ReportItem includes EPSS data.
2842
+ * The date is derived from the host's HOST_START in YYYY-MM-DD form.
2843
+ */
2844
+ function buildEpss(item, host) {
2845
+ const hasScore = item.epss_score !== void 0 && item.epss_score !== "";
2846
+ const hasPct = item.epss_percentile !== void 0 && item.epss_percentile !== "";
2847
+ if (!hasScore && !hasPct) return void 0;
2848
+ const date = epssDate(host);
2849
+ if (date === void 0) return void 0;
1557
2850
  return {
1558
- id: isCompliance ? parseComplianceRef(item["compliance-reference"], "Vuln-ID")[0] || item["pluginID"] : item["pluginID"],
1559
- title: isCompliance ? item["compliance-check-name"] || item["pluginName"] : item["pluginName"],
1560
- descriptions: buildDescriptions(item, isCompliance),
1561
- impact: calculateImpact(item, isCompliance),
1562
- tags: buildTags$2(item, isCompliance),
1563
- refs: buildRefs(item),
1564
- results: [buildResult$1(item, host, isCompliance)],
1565
- code: JSON.stringify(item, null, 2)
2851
+ date,
2852
+ score: hasScore ? parseFloatSafe(item.epss_score) : 0,
2853
+ percentile: hasPct ? parseFloatSafe(item.epss_percentile) : 0
1566
2854
  };
1567
2855
  }
2856
+ function epssDate(host) {
2857
+ const hs = getHostPropertyValue(host, "HOST_START");
2858
+ if (hs) {
2859
+ const d = new Date(hs);
2860
+ if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
2861
+ }
2862
+ }
1568
2863
  function buildDescriptions(item, isCompliance) {
1569
2864
  const descriptions = [];
1570
2865
  if (isCompliance && item["compliance-info"]) descriptions.push({
@@ -1625,7 +2920,7 @@ function buildTags$2(item, isCompliance) {
1625
2920
  }
1626
2921
  function buildRefs(item) {
1627
2922
  const refs = [];
1628
- if (item.see_also) refs.push({ url: item.see_also });
2923
+ if (item.see_also) for (const url of item.see_also.split(/\s+/).filter(Boolean)) refs.push({ url });
1629
2924
  return refs.length > 0 ? refs : void 0;
1630
2925
  }
1631
2926
  function buildResult$1(item, host, isCompliance) {
@@ -1667,7 +2962,7 @@ function convertReportHostToTarget(host) {
1667
2962
  });
1668
2963
  const target = {
1669
2964
  name: hostName,
1670
- type: Copyright.Host
2965
+ type: TargetType.Host
1671
2966
  };
1672
2967
  if (isFQDN(hostName)) target.fqdn = hostName;
1673
2968
  const hostIp = hostProps["host-ip"];
@@ -1735,18 +3030,40 @@ async function convertSonarqubeToHdf(input) {
1735
3030
  const baseline = convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum);
1736
3031
  baselines.push(baseline);
1737
3032
  }
3033
+ let components = Array.from(issuesByProject.keys()).map((projectKey) => ({
3034
+ type: TargetType.Application,
3035
+ name: projectKey
3036
+ }));
3037
+ if (baselines.length === 0) {
3038
+ const targetName = deriveEmptyScanTarget(sonarData.components);
3039
+ baselines.push({
3040
+ name: targetName,
3041
+ title: `SonarQube Analysis for ${targetName}`,
3042
+ requirements: [buildNoFindingsRequirement("sonarqube-no-findings", `SonarQube scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())],
3043
+ resultsChecksum
3044
+ });
3045
+ components = [{
3046
+ type: TargetType.Application,
3047
+ name: targetName
3048
+ }];
3049
+ }
1738
3050
  return buildHdfResults({
1739
3051
  generatorName: "sonarqube-to-hdf",
1740
3052
  converterVersion: "1.0.0",
1741
3053
  toolName: "SonarQube",
1742
3054
  baselines,
1743
- components: Array.from(issuesByProject.keys()).map((projectKey) => ({
1744
- type: Copyright.Application,
1745
- name: projectKey
1746
- })),
3055
+ components,
1747
3056
  timestamp: /* @__PURE__ */ new Date()
1748
3057
  });
1749
3058
  }
3059
+ function deriveEmptyScanTarget(components) {
3060
+ if (components) {
3061
+ const project = components.find((c) => c.qualifier === "TRK");
3062
+ if (project) return project.key;
3063
+ if (components.length > 0) return components[0].key;
3064
+ }
3065
+ return "the SonarQube project";
3066
+ }
1750
3067
  function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum) {
1751
3068
  const issuesByRule = /* @__PURE__ */ new Map();
1752
3069
  for (const issue of issues) {
@@ -1786,7 +3103,11 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
1786
3103
  ...allTags
1787
3104
  } };
1788
3105
  if (sourceLocation) options.sourceLocation = sourceLocation;
1789
- return createRequirement(ruleKey, title, [createDescription("default", description)], impact, results, options);
3106
+ const req = createRequirement(ruleKey, title, [createDescription("default", description)], impact, results, options);
3107
+ const controlType = deriveControlTypeFromTags(nistControls);
3108
+ if (controlType !== void 0) req.controlType = controlType;
3109
+ req.verificationMethod = VerificationMethodEnum.Automated;
3110
+ return req;
1790
3111
  }
1791
3112
  function extractDescription(rule) {
1792
3113
  if (!rule) return "";
@@ -1905,7 +3226,21 @@ function buildResult(r) {
1905
3226
  ...startTime ? { startTime } : {}
1906
3227
  });
1907
3228
  }
1908
- function buildRequirement$14(rule) {
3229
+ /**
3230
+ * Synthesizes a single HDF result for a Config rule whose live evaluation
3231
+ * returned zero in-scope resources. The HDF schema requires `results` to have
3232
+ * minItems >= 1; this honestly signals to auditors that the rule's check ran
3233
+ * but had no scope in this account/region rather than vacuously claiming
3234
+ * "passed". See issue #80 bug 2.
3235
+ */
3236
+ function buildNotApplicableResult(rule) {
3237
+ const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
3238
+ return createResult(ResultStatus.NotApplicable, codeDesc, {
3239
+ codeDesc,
3240
+ startTime: /* @__PURE__ */ new Date()
3241
+ });
3242
+ }
3243
+ function buildRequirement$15(rule) {
1909
3244
  const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
1910
3245
  const tags = nist.length > 0 ? { nist } : {};
1911
3246
  const descriptions = [{
@@ -1916,14 +3251,18 @@ function buildRequirement$14(rule) {
1916
3251
  data: buildCheckText(rule)
1917
3252
  }];
1918
3253
  const title = `${getAccountId(rule.ConfigRuleArn)} - ${rule.ConfigRuleName}`;
1919
- const results = rule.EvaluationResults.map(buildResult);
1920
- return createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
3254
+ const results = rule.EvaluationResults.length > 0 ? rule.EvaluationResults.map(buildResult) : [buildNotApplicableResult(rule)];
3255
+ const req = createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
1921
3256
  tags,
1922
3257
  sourceLocation: {
1923
3258
  ref: rule.ConfigRuleArn,
1924
3259
  line: 1
1925
3260
  }
1926
3261
  });
3262
+ const controlType = deriveControlTypeFromTags(nist);
3263
+ if (controlType !== void 0) req.controlType = controlType;
3264
+ req.verificationMethod = VerificationMethodEnum.Automated;
3265
+ return req;
1927
3266
  }
1928
3267
  /**
1929
3268
  * Converts an AWS Config static export (ConfigRulesFile JSON) to HDF format.
@@ -1942,7 +3281,7 @@ async function convertAwsConfigToHdf(input) {
1942
3281
  /* v8 ignore next -- truncation only triggers with >100K items */
1943
3282
  if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
1944
3283
  const baseline = {
1945
- ...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$14), { resultsChecksum }),
3284
+ ...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$15), { resultsChecksum }),
1946
3285
  title: "AWS Config Compliance Results",
1947
3286
  version: "1.0.0",
1948
3287
  maintainer: "Amazon Web Services"
@@ -1956,7 +3295,7 @@ async function convertAwsConfigToHdf(input) {
1956
3295
  toolName: "AWS Config",
1957
3296
  baselines: [baseline],
1958
3297
  components: [{
1959
- type: Copyright.CloudAccount,
3298
+ type: TargetType.CloudAccount,
1960
3299
  name: `AWS Account ${accountId}`,
1961
3300
  labels: {
1962
3301
  account: accountId,
@@ -1968,6 +3307,120 @@ async function convertAwsConfigToHdf(input) {
1968
3307
  });
1969
3308
  }
1970
3309
  //#endregion
3310
+ //#region converters/checkov-to-hdf/typescript/converter.ts
3311
+ /**
3312
+ * Maps checkov result string to HDF ResultStatus.
3313
+ */
3314
+ function mapStatus$2(result) {
3315
+ switch (result.toUpperCase()) {
3316
+ case "PASSED": return ResultStatus.Passed;
3317
+ case "FAILED": return ResultStatus.Failed;
3318
+ case "SKIPPED": return ResultStatus.NotReviewed;
3319
+ default: return ResultStatus.NotReviewed;
3320
+ }
3321
+ }
3322
+ /**
3323
+ * Maps severity to impact, defaulting to 0.5 for null/unknown.
3324
+ */
3325
+ function getImpact$8(severity) {
3326
+ if (!severity) return .5;
3327
+ return severityToImpact(severity);
3328
+ }
3329
+ /**
3330
+ * Converts a single CheckovCheck to an HDF RequirementResult.
3331
+ */
3332
+ function checkToResult(check) {
3333
+ const status = mapStatus$2(check.check_result.result);
3334
+ const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
3335
+ let message;
3336
+ if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
3337
+ return createResult(status, message ?? "", { codeDesc });
3338
+ }
3339
+ /**
3340
+ * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
3341
+ */
3342
+ function buildRequirement$14(checkId, checks) {
3343
+ const rep = checks[0];
3344
+ const impact = getImpact$8(rep.severity);
3345
+ const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
3346
+ const descriptions = [{
3347
+ label: "default",
3348
+ data: rep.check_name
3349
+ }];
3350
+ if (rep.guideline) descriptions.push({
3351
+ label: "check",
3352
+ data: rep.guideline
3353
+ });
3354
+ const results = checks.map(checkToResult);
3355
+ const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
3356
+ req.verificationMethod = VerificationMethodEnum.Automated;
3357
+ const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
3358
+ if (controlType !== void 0) req.controlType = controlType;
3359
+ return req;
3360
+ }
3361
+ /**
3362
+ * Parses checkov input which can be a single object or array.
3363
+ */
3364
+ function parseInput(input) {
3365
+ const parsed = parseJSON(input);
3366
+ if (Array.isArray(parsed)) return parsed;
3367
+ if (!parsed || typeof parsed !== "object") throw new Error("Invalid checkov structure: not a valid JSON object");
3368
+ if (!parsed.results || typeof parsed.results !== "object") throw new Error("Invalid checkov structure: missing or invalid results field");
3369
+ return [parsed];
3370
+ }
3371
+ /**
3372
+ * Converts checkov output to HDF format.
3373
+ * Accepts native checkov JSON (single object or array) and SARIF format.
3374
+ * SARIF input is detected automatically and delegated to the shared SARIF converter.
3375
+ *
3376
+ * @param input - checkov JSON or SARIF string
3377
+ * @returns HDF JSON string
3378
+ */
3379
+ async function convertCheckovToHdf(input) {
3380
+ validateInputSize(input, "checkov");
3381
+ registerAllFingerprints();
3382
+ const detected = detectConverter(input);
3383
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3384
+ const resultsChecksum = await inputChecksum(input);
3385
+ const reports = parseInput(input);
3386
+ const allChecks = [];
3387
+ const checkTypes = [];
3388
+ let version;
3389
+ for (const report of reports) {
3390
+ checkTypes.push(report.check_type);
3391
+ if (!version && report.summary.checkov_version) version = report.summary.checkov_version;
3392
+ allChecks.push(...report.results.passed_checks);
3393
+ allChecks.push(...report.results.failed_checks);
3394
+ allChecks.push(...report.results.skipped_checks);
3395
+ }
3396
+ const { items: limitedChecks, truncated } = limitArray(allChecks);
3397
+ /* v8 ignore next -- truncation only triggers with >100K items */
3398
+ if (truncated) console.warn(`WARNING: Input truncated at ${limitedChecks.length} check items (original: ${allChecks.length})`);
3399
+ const groups = /* @__PURE__ */ new Map();
3400
+ for (const check of limitedChecks) {
3401
+ const existing = groups.get(check.check_id);
3402
+ if (existing) existing.push(check);
3403
+ else groups.set(check.check_id, [check]);
3404
+ }
3405
+ const requirements = [];
3406
+ for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
3407
+ const format = checkTypes.join(", ");
3408
+ if (requirements.length === 0) {
3409
+ const target = format || "input";
3410
+ requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
3411
+ }
3412
+ const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
3413
+ return buildHdfResults({
3414
+ generatorName: "checkov-to-hdf",
3415
+ converterVersion: "1.0.0",
3416
+ toolName: "Checkov",
3417
+ toolVersion: version,
3418
+ toolFormat: format,
3419
+ baselines: [baseline],
3420
+ timestamp: /* @__PURE__ */ new Date()
3421
+ });
3422
+ }
3423
+ //#endregion
1971
3424
  //#region converters/gosec-to-hdf/typescript/converter.ts
1972
3425
  /**
1973
3426
  * Severity to HDF impact mapping for gosec.
@@ -2010,8 +3463,9 @@ function issueToResult(issue) {
2010
3463
  function buildRequirement$13(ruleId, issues) {
2011
3464
  const rep = issues[0];
2012
3465
  const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
3466
+ const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
2013
3467
  const tags = {
2014
- nist: mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1),
3468
+ nist,
2015
3469
  cwe: {
2016
3470
  id: rep.cwe.id,
2017
3471
  url: rep.cwe.url
@@ -2025,7 +3479,11 @@ function buildRequirement$13(ruleId, issues) {
2025
3479
  label: "check",
2026
3480
  data: `CWE-${rep.cwe.id}: ${rep.cwe.url}`
2027
3481
  }];
2028
- return createRequirement(ruleId, rep.details, descriptions, impact, results, { tags });
3482
+ const req = createRequirement(ruleId, rep.details, descriptions, impact, results, { tags });
3483
+ req.verificationMethod = VerificationMethodEnum.Automated;
3484
+ const controlType = deriveControlTypeFromTags(nist);
3485
+ if (controlType !== void 0) req.controlType = controlType;
3486
+ return req;
2029
3487
  }
2030
3488
  /**
2031
3489
  * Converts gosec output to HDF format.
@@ -2055,6 +3513,7 @@ async function convertGosecToHdf(input) {
2055
3513
  }
2056
3514
  const requirements = [];
2057
3515
  for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues));
3516
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", /* @__PURE__ */ new Date()));
2058
3517
  const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
2059
3518
  return buildHdfResults({
2060
3519
  generatorName: "gosec-to-hdf",
@@ -2089,7 +3548,7 @@ function convertVulnToRequirement(vuln) {
2089
3548
  const extras = {};
2090
3549
  if (vuln.OSVDB && vuln.OSVDB !== "0") extras.osvdb = vuln.OSVDB;
2091
3550
  const tags = buildNistCciTags(nistTags, cciTags, Object.keys(extras).length > 0 ? extras : void 0);
2092
- return {
3551
+ const req = {
2093
3552
  id: vuln.id,
2094
3553
  title: vuln.msg,
2095
3554
  impact: .5,
@@ -2100,6 +3559,10 @@ function convertVulnToRequirement(vuln) {
2100
3559
  data: vuln.msg
2101
3560
  }]
2102
3561
  };
3562
+ const controlType = deriveControlTypeFromTags(nistTags);
3563
+ if (controlType !== void 0) req.controlType = controlType;
3564
+ req.verificationMethod = VerificationMethodEnum.Automated;
3565
+ return req;
2103
3566
  }
2104
3567
  async function convertNiktoToHdf(input) {
2105
3568
  validateInputSize(input, "nikto");
@@ -2131,6 +3594,7 @@ async function convertNiktoToHdf(input) {
2131
3594
  if (niktoData.host) targetParts.push(`Host: ${niktoData.host}`);
2132
3595
  if (niktoData.port) targetParts.push(`Port: ${niktoData.port}`);
2133
3596
  const targetName = targetParts.length > 0 ? targetParts.join(" ") : "Nikto Scan";
3597
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
2134
3598
  return buildHdfResults({
2135
3599
  generatorName: "nikto-to-hdf",
2136
3600
  converterVersion: "unknown",
@@ -2142,7 +3606,7 @@ async function convertNiktoToHdf(input) {
2142
3606
  summary: niktoData.banner || ""
2143
3607
  })],
2144
3608
  components: [{
2145
- type: Copyright.Application,
3609
+ type: TargetType.Application,
2146
3610
  name: targetName
2147
3611
  }]
2148
3612
  });
@@ -2204,45 +3668,8 @@ async function convertZapToHdf(input) {
2204
3668
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
2205
3669
  const resultsChecksum = await inputChecksum(input);
2206
3670
  const zapData = parseJSON(input);
2207
- if (!zapData.site || !Array.isArray(zapData.site)) {
2208
- const hdf = {
2209
- baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
2210
- resultsChecksum,
2211
- title: "OWASP ZAP Scan",
2212
- summary: ""
2213
- })],
2214
- generator: {
2215
- name: "zap-to-hdf",
2216
- version: "unknown"
2217
- },
2218
- tool: {
2219
- name: "OWASP ZAP",
2220
- format: "JSON"
2221
- }
2222
- };
2223
- return JSON.stringify(hdf, null, 2);
2224
- }
2225
- const site = selectSite(zapData.site);
2226
- if (!site) {
2227
- const hdf = {
2228
- baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
2229
- resultsChecksum,
2230
- title: "OWASP ZAP Scan",
2231
- summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
2232
- })],
2233
- generator: {
2234
- name: "zap-to-hdf",
2235
- version: "unknown"
2236
- },
2237
- tool: {
2238
- name: "OWASP ZAP",
2239
- format: "JSON"
2240
- }
2241
- };
2242
- if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
2243
- return JSON.stringify(hdf, null, 2);
2244
- }
2245
- const alerts = site.alerts ?? [];
3671
+ const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
3672
+ const alerts = site?.alerts ?? [];
2246
3673
  const pluginIdCount = /* @__PURE__ */ new Map();
2247
3674
  const { items: limitedAlerts, truncated } = limitArray(alerts);
2248
3675
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -2291,14 +3718,24 @@ async function convertZapToHdf(input) {
2291
3718
  impact,
2292
3719
  results,
2293
3720
  tags,
2294
- descriptions
3721
+ descriptions,
3722
+ verificationMethod: VerificationMethodEnum.Automated
2295
3723
  };
3724
+ const controlType = deriveControlTypeFromTags(nistTags);
3725
+ if (controlType !== void 0) req.controlType = controlType;
2296
3726
  requirements.push(req);
2297
3727
  }
2298
- const targetName = site["@host"] ?? "Unknown Host";
3728
+ const targetName = site?.["@host"] ?? "Unknown Host";
3729
+ const siteName = site?.["@name"] ?? "";
3730
+ const baselineName = site && (site["@name"] || site["@host"]) ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
3731
+ if (requirements.length === 0) {
3732
+ let target = siteName || targetName;
3733
+ if (!target || target === "Unknown Host") target = "the target site";
3734
+ requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
3735
+ }
2299
3736
  const baseline = createMinimalBaseline("OWASP ZAP Scan", requirements, {
2300
3737
  resultsChecksum,
2301
- title: `OWASP ZAP Scan of ${site["@name"] ?? targetName}`,
3738
+ title: baselineName,
2302
3739
  summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
2303
3740
  });
2304
3741
  const tool = {
@@ -2307,14 +3744,14 @@ async function convertZapToHdf(input) {
2307
3744
  };
2308
3745
  if (zapData["@version"]) tool.version = zapData["@version"];
2309
3746
  const components = [];
2310
- if (site["@name"]) components.push({
3747
+ if (site?.["@name"]) components.push({
2311
3748
  name: targetName,
2312
- type: Copyright.Application,
3749
+ type: TargetType.Application,
2313
3750
  url: site["@name"]
2314
3751
  });
2315
3752
  else if (targetName !== "Unknown Host") components.push({
2316
3753
  name: targetName,
2317
- type: Copyright.Application
3754
+ type: TargetType.Application
2318
3755
  });
2319
3756
  const hdf = {
2320
3757
  baselines: [baseline],
@@ -2435,7 +3872,10 @@ async function convertCyclonedxToHdf(input) {
2435
3872
  const affects = vuln.affects ?? [];
2436
3873
  const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$4(componentLookup, affect.ref) })) : [createResult(ResultStatus.Failed, void 0, { codeDesc: `Vulnerability ${vuln.id}` })];
2437
3874
  const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
2438
- requirements.push(createRequirement(vuln.id, title, descriptions, impact, results, { tags }));
3875
+ const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
3876
+ const controlType = deriveControlTypeFromTags(nist);
3877
+ if (controlType !== void 0) req.controlType = controlType;
3878
+ requirements.push(req);
2439
3879
  }
2440
3880
  const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
2441
3881
  const targetName = bom.metadata?.component?.name ?? "";
@@ -2447,7 +3887,7 @@ async function convertCyclonedxToHdf(input) {
2447
3887
  baselines: [baseline],
2448
3888
  components: [{
2449
3889
  name: targetName,
2450
- type: Copyright.Application
3890
+ type: TargetType.Application
2451
3891
  }],
2452
3892
  timestamp: /* @__PURE__ */ new Date()
2453
3893
  });
@@ -2501,6 +3941,9 @@ function createRow(baseline, requirement, target) {
2501
3941
  "Status": String(status),
2502
3942
  "NIST Controls": nistControls,
2503
3943
  "CCI Controls": cciControls,
3944
+ "Control Type": requirement.controlType ?? "",
3945
+ "Verification Method": requirement.verificationMethod ?? "",
3946
+ "Applicability": requirement.applicability ?? "",
2504
3947
  "Result Message": message
2505
3948
  };
2506
3949
  }
@@ -2624,7 +4067,12 @@ async function convertSplunkToHdf(input) {
2624
4067
  })) : [];
2625
4068
  const options = { tags: control.tags ?? {} };
2626
4069
  if (control.source_location) options.sourceLocation = control.source_location;
2627
- return createRequirement(control.id, control.title, descriptions, control.impact, results, options);
4070
+ const req = createRequirement(control.id, control.title, descriptions, control.impact, results, options);
4071
+ const nistTagsRaw = control.tags?.["nist"];
4072
+ const controlType = deriveControlTypeFromTags(Array.isArray(nistTagsRaw) ? nistTagsRaw.filter((t) => typeof t === "string") : []);
4073
+ if (controlType !== void 0) req.controlType = controlType;
4074
+ req.verificationMethod = VerificationMethodEnum.Automated;
4075
+ return req;
2628
4076
  });
2629
4077
  const groups = convertGroups(profile.groups);
2630
4078
  const baselineOptions = { resultsChecksum };
@@ -2639,7 +4087,7 @@ async function convertSplunkToHdf(input) {
2639
4087
  baselines: allBaselines,
2640
4088
  components: [{
2641
4089
  name: targetName,
2642
- type: Copyright.Host,
4090
+ type: TargetType.Host,
2643
4091
  osName: targetRelease || void 0,
2644
4092
  labels: {}
2645
4093
  }],
@@ -2739,9 +4187,9 @@ function gitlabSeverityToImpact(severity) {
2739
4187
  }
2740
4188
  function scanTypeToTargetType(scanType) {
2741
4189
  switch (scanType) {
2742
- case "dast": return Copyright.Application;
2743
- case "container_scanning": return Copyright.ContainerImage;
2744
- default: return Copyright.Repository;
4190
+ case "dast": return TargetType.Application;
4191
+ case "container_scanning": return TargetType.ContainerImage;
4192
+ default: return TargetType.Repository;
2745
4193
  }
2746
4194
  }
2747
4195
  function scanTypeLabel(scanType) {
@@ -2867,13 +4315,21 @@ async function convertGitlabToHdf(input) {
2867
4315
  impact,
2868
4316
  results: [result],
2869
4317
  tags,
2870
- descriptions
4318
+ descriptions,
4319
+ verificationMethod: VerificationMethodEnum.Automated
2871
4320
  };
4321
+ const controlType = deriveControlTypeFromTags(nistTags);
4322
+ if (controlType !== void 0) req.controlType = controlType;
2872
4323
  requirements.push(req);
2873
4324
  }
4325
+ const label = scanTypeLabel(scanType);
4326
+ if (requirements.length === 0) {
4327
+ const ts = startTime ? new Date(startTime) : /* @__PURE__ */ new Date();
4328
+ requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
4329
+ }
2874
4330
  const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
2875
4331
  resultsChecksum,
2876
- title: `GitLab ${scanTypeLabel(scanType)} Security Scan`,
4332
+ title: `GitLab ${label} Security Scan`,
2877
4333
  summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
2878
4334
  });
2879
4335
  const tool = {
@@ -3004,13 +4460,17 @@ function buildRequirement$12(reqID, findings) {
3004
4460
  }));
3005
4461
  const sourceFile = getSourceFile(rep);
3006
4462
  const sourceLine = getSourceLine(rep);
3007
- return createRequirement(reqID, title, descriptions, .5, results, {
4463
+ const req = createRequirement(reqID, title, descriptions, .5, results, {
3008
4464
  tags,
3009
4465
  sourceLocation: sourceFile ? {
3010
4466
  ref: sourceFile,
3011
4467
  line: sourceLine
3012
4468
  } : void 0
3013
4469
  });
4470
+ const controlType = deriveControlTypeFromTags(TRUFFLEHOG_NIST);
4471
+ if (controlType !== void 0) req.controlType = controlType;
4472
+ req.verificationMethod = VerificationMethodEnum.Automated;
4473
+ return req;
3014
4474
  }
3015
4475
  /**
3016
4476
  * Converts TruffleHog output to HDF format.
@@ -3023,19 +4483,22 @@ async function convertTrufflehogToHdf(input) {
3023
4483
  if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
3024
4484
  validateInputSize(input, "trufflehog");
3025
4485
  const findings = parseFindings(input);
3026
- if (findings.length === 0) throw new Error("trufflehog: no findings in input");
3027
4486
  const resultsChecksum = await inputChecksum(input);
3028
4487
  const limitedFindings = limitArrayWithWarning(findings, "finding");
3029
4488
  const groups = groupFindings(limitedFindings);
3030
4489
  const requirements = [];
3031
4490
  for (const [reqID, group] of groups) requirements.push(buildRequirement$12(reqID, group));
4491
+ if (requirements.length === 0) {
4492
+ const target = findGitRepoURL(limitedFindings) ?? limitedFindings[0]?.SourceName ?? "the target source";
4493
+ requirements.push(buildNoFindingsRequirement("trufflehog-no-findings", `TruffleHog scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
4494
+ }
3032
4495
  const hdf = {
3033
4496
  baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
3034
4497
  resultsChecksum,
3035
4498
  title: `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`
3036
4499
  })],
3037
4500
  generator: {
3038
- name: "hdf-converters",
4501
+ name: "trufflehog-to-hdf",
3039
4502
  version: "1.0.0"
3040
4503
  },
3041
4504
  tool: {
@@ -3047,7 +4510,7 @@ async function convertTrufflehogToHdf(input) {
3047
4510
  const repoURL = findGitRepoURL(limitedFindings);
3048
4511
  if (repoURL) hdf.components = [{
3049
4512
  name: repoURL,
3050
- type: Copyright.Repository
4513
+ type: TargetType.Repository
3051
4514
  }];
3052
4515
  return JSON.stringify(hdf, null, 2);
3053
4516
  }
@@ -3109,6 +4572,7 @@ async function convertBurpsuiteToHdf(input) {
3109
4572
  const requirements = [];
3110
4573
  for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues));
3111
4574
  const targetName = limitedIssues.length > 0 ? (limitedIssues[0].host?.["#text"] ?? "Unknown").trim() : "Unknown";
4575
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("burpsuite-no-findings", `Burp Suite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
3112
4576
  const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
3113
4577
  resultsChecksum,
3114
4578
  title: `BurpSuite Scan: ${targetName}`
@@ -3122,7 +4586,7 @@ async function convertBurpsuiteToHdf(input) {
3122
4586
  baselines: [baseline],
3123
4587
  components: [{
3124
4588
  name: targetName,
3125
- type: Copyright.Application
4589
+ type: TargetType.Application
3126
4590
  }],
3127
4591
  generator: {
3128
4592
  name: "burpsuite-to-hdf",
@@ -3165,14 +4629,18 @@ function buildRequirement$11(issueType, issues) {
3165
4629
  };
3166
4630
  });
3167
4631
  const impact = getImpact$7(rep.severity ?? "information");
3168
- return {
4632
+ const req = {
3169
4633
  id: issueType,
3170
4634
  title: rep.name ?? void 0,
3171
4635
  impact,
3172
4636
  tags,
3173
4637
  descriptions,
3174
- results
4638
+ results,
4639
+ verificationMethod: VerificationMethodEnum.Automated
3175
4640
  };
4641
+ const controlType = deriveControlTypeFromTags(nist);
4642
+ if (controlType !== void 0) req.controlType = controlType;
4643
+ return req;
3176
4644
  }
3177
4645
  //#endregion
3178
4646
  //#region converters/dbprotect-to-hdf/typescript/converter.ts
@@ -3261,7 +4729,11 @@ function buildRequirement$10(checkID, findings, hasStatus) {
3261
4729
  startTime: parseDate(f["Date"] ?? "")
3262
4730
  });
3263
4731
  });
3264
- return createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
4732
+ const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
4733
+ req.verificationMethod = VerificationMethodEnum.Automated;
4734
+ const controlType = deriveControlTypeFromTags([...nist]);
4735
+ if (controlType !== void 0) req.controlType = controlType;
4736
+ return req;
3265
4737
  }
3266
4738
  /**
3267
4739
  * Converts DBProtect Cognos XML output to HDF format.
@@ -3311,7 +4783,7 @@ async function convertDbprotectToHdf(input) {
3311
4783
  })],
3312
4784
  components: [{
3313
4785
  name: targetName,
3314
- type: Copyright.Host
4786
+ type: TargetType.Host
3315
4787
  }],
3316
4788
  timestamp: /* @__PURE__ */ new Date()
3317
4789
  });
@@ -3351,6 +4823,148 @@ function buildSummary(result) {
3351
4823
  return `Package Vulnerability Summary: ${result.vulnerabilityDistribution ? String(result.vulnerabilityDistribution.total) : "N/A"} Application Compliance Issue Total: ${result.complianceDistribution ? String(result.complianceDistribution.total) : "N/A"}`;
3352
4824
  }
3353
4825
  /**
4826
+ * Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
4827
+ * since modern Twistlock exclusively emits 3.x output.
4828
+ */
4829
+ function cvssVersionFromVector(vector) {
4830
+ if (!vector) return Version.The31;
4831
+ if (vector.startsWith("CVSS:2.0/")) return Version.The20;
4832
+ if (vector.startsWith("CVSS:3.0/")) return Version.The30;
4833
+ if (vector.startsWith("CVSS:4.0/")) return Version.The40;
4834
+ return Version.The31;
4835
+ }
4836
+ /**
4837
+ * Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
4838
+ */
4839
+ function cvssSeverityFromScore(score) {
4840
+ switch (cvssScoreToSeverity(score)) {
4841
+ case "none": return CVSSSeverity.None;
4842
+ case "low": return CVSSSeverity.Low;
4843
+ case "medium": return CVSSSeverity.Medium;
4844
+ case "high": return CVSSSeverity.High;
4845
+ case "critical": return CVSSSeverity.Critical;
4846
+ default: return;
4847
+ }
4848
+ }
4849
+ /**
4850
+ * Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
4851
+ * when neither a score nor a vector is available. When the vendor emits a
4852
+ * score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
4853
+ * entry is still emitted — the schema makes baseVector optional precisely
4854
+ * so vendor-final-score data isn't dropped.
4855
+ */
4856
+ function buildCvss(vuln) {
4857
+ const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
4858
+ const hasVector = !!vuln.vector;
4859
+ if (!hasScore && !hasVector) return void 0;
4860
+ const cv = { version: cvssVersionFromVector(vuln.vector) };
4861
+ if (hasScore) {
4862
+ cv.baseScore = vuln.cvss;
4863
+ const sev = cvssSeverityFromScore(vuln.cvss);
4864
+ if (sev !== void 0) cv.baseSeverity = sev;
4865
+ }
4866
+ if (hasVector) cv.baseVector = vuln.vector;
4867
+ const source = vuln.cve ?? vuln.id;
4868
+ if (source) cv.source = source;
4869
+ return cv;
4870
+ }
4871
+ const CWE_REGEX = /cwe[-_]?(\d+)/gi;
4872
+ /**
4873
+ * Extracts canonical CWE-N identifiers from a free-form string.
4874
+ */
4875
+ function parseCwes(raw) {
4876
+ if (!raw) return [];
4877
+ const out = [];
4878
+ const seen = /* @__PURE__ */ new Set();
4879
+ for (const m of raw.matchAll(CWE_REGEX)) {
4880
+ const id = `CWE-${m[1]}`;
4881
+ if (seen.has(id)) continue;
4882
+ seen.add(id);
4883
+ out.push(id);
4884
+ }
4885
+ return out;
4886
+ }
4887
+ function rhelDistro(distro) {
4888
+ const low = distro.toLowerCase();
4889
+ return [
4890
+ "red hat",
4891
+ "rhel",
4892
+ "centos",
4893
+ "fedora",
4894
+ "amazon linux",
4895
+ "oracle linux",
4896
+ "rocky",
4897
+ "alma"
4898
+ ].some((m) => low.includes(m));
4899
+ }
4900
+ function debDistro(distro) {
4901
+ const low = distro.toLowerCase();
4902
+ return ["debian", "ubuntu"].some((m) => low.includes(m));
4903
+ }
4904
+ /**
4905
+ * Maps a Twistlock package type plus the result's distro string to a schema
4906
+ * Ecosystem value. Defaults to 'generic' for unknown types.
4907
+ */
4908
+ function resolveEcosystem(packageType, distro) {
4909
+ const t = (packageType ?? "").toLowerCase();
4910
+ const d = distro ?? "";
4911
+ switch (t) {
4912
+ case "os":
4913
+ if (rhelDistro(d)) return Ecosystem.RPM;
4914
+ if (debDistro(d)) return Ecosystem.Deb;
4915
+ return Ecosystem.Generic;
4916
+ case "rpm": return Ecosystem.RPM;
4917
+ case "deb": return Ecosystem.Deb;
4918
+ case "jar":
4919
+ case "maven": return Ecosystem.Maven;
4920
+ case "python":
4921
+ case "pypi": return Ecosystem.Pypi;
4922
+ case "nodejs":
4923
+ case "npm": return Ecosystem.Npm;
4924
+ case "gem": return Ecosystem.Gem;
4925
+ case "nuget": return Ecosystem.Nuget;
4926
+ case "go": return Ecosystem.Go;
4927
+ case "cargo": return Ecosystem.Cargo;
4928
+ default: return Ecosystem.Generic;
4929
+ }
4930
+ }
4931
+ const FIX_VERSION_REGEX = /\d+(?:\.\d+)+[A-Za-z0-9._+\-]*/;
4932
+ /**
4933
+ * Extracts the first version-looking token from fixedBy or a "fixed in X"
4934
+ * status string. Returns empty string when no fix info is present.
4935
+ */
4936
+ function extractFixedInVersion(vuln) {
4937
+ if (vuln.fixedBy) return vuln.fixedBy;
4938
+ if (!(vuln.status ?? "").toLowerCase().includes("fixed")) return "";
4939
+ const m = (vuln.status ?? "").match(FIX_VERSION_REGEX);
4940
+ return m ? m[0] : "";
4941
+ }
4942
+ /**
4943
+ * Builds an AffectedPackage entry from per-vulnerability fields. Returns
4944
+ * undefined when packageName or packageVersion are missing (both required).
4945
+ */
4946
+ function buildAffectedPackage(vuln, packageTypes, distro) {
4947
+ if (!vuln.packageName || !vuln.packageVersion) return void 0;
4948
+ const pkgType = vuln.packageType ?? packageTypes.get(vuln.packageName);
4949
+ const pkg = {
4950
+ name: vuln.packageName,
4951
+ version: vuln.packageVersion,
4952
+ ecosystem: resolveEcosystem(pkgType, distro)
4953
+ };
4954
+ const fixed = extractFixedInVersion(vuln);
4955
+ if (fixed) pkg.fixedInVersion = fixed;
4956
+ return pkg;
4957
+ }
4958
+ /**
4959
+ * Indexes packageName → packageType from the result-level packages array.
4960
+ */
4961
+ function buildPackageTypeIndex(pkgs) {
4962
+ const idx = /* @__PURE__ */ new Map();
4963
+ if (!pkgs) return idx;
4964
+ for (const p of pkgs) if (p.name && p.type) idx.set(p.name, p.type);
4965
+ return idx;
4966
+ }
4967
+ /**
3354
4968
  * Builds the code_desc string for a vulnerability result.
3355
4969
  */
3356
4970
  function formatCodeDesc$2(vuln) {
@@ -3360,10 +4974,17 @@ function formatCodeDesc$2(vuln) {
3360
4974
  }
3361
4975
  /**
3362
4976
  * Converts a single vulnerability into an EvaluatedRequirement.
4977
+ *
4978
+ * @param vuln - The Twistlock vulnerability object
4979
+ * @param packageTypes - name→type lookup built from the enclosing result's packages array
4980
+ * @param distro - the enclosing result's distro string, used to disambiguate "os" packages
3363
4981
  */
3364
- function buildRequirement$9(vuln) {
4982
+ function buildRequirement$9(vuln, packageTypes, distro) {
3365
4983
  const nist = DEFAULT_REMEDIATION_NIST_TAGS;
3366
- const tags = buildNistCciTags(nist, nistToCci(nist), { cveid: [vuln.id] });
4984
+ const cciTags = nistToCci(nist);
4985
+ const extras = { cveid: [vuln.id] };
4986
+ if (typeof vuln.cvss === "number" && vuln.cvss > 0) extras["cvss_base_score"] = vuln.cvss;
4987
+ const tags = buildNistCciTags(nist, cciTags, extras);
3367
4988
  const descriptions = [{
3368
4989
  label: "default",
3369
4990
  data: vuln.description
@@ -3373,7 +4994,17 @@ function buildRequirement$9(vuln) {
3373
4994
  codeDesc: formatCodeDesc$2(vuln),
3374
4995
  startTime
3375
4996
  })];
3376
- return createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
4997
+ const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
4998
+ const controlType = deriveControlTypeFromTags(nist);
4999
+ if (controlType !== void 0) req.controlType = controlType;
5000
+ req.verificationMethod = VerificationMethodEnum.Automated;
5001
+ const cv = buildCvss(vuln);
5002
+ if (cv) req.cvss = [cv];
5003
+ const cwes = parseCwes(vuln.cwe);
5004
+ if (cwes.length > 0) req.cwe = cwes;
5005
+ const pkg = buildAffectedPackage(vuln, packageTypes, distro);
5006
+ if (pkg) req.affectedPackages = [pkg];
5007
+ return req;
3377
5008
  }
3378
5009
  /**
3379
5010
  * Converts a single TwistlockResult to an EvaluatedBaseline.
@@ -3383,7 +5014,13 @@ function convertSingleResult(result, resultsChecksum) {
3383
5014
  const { items: limitedVulns, truncated } = limitArray(vulns);
3384
5015
  /* v8 ignore next -- truncation only triggers with >100K items */
3385
5016
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
3386
- return createMinimalBaseline("Twistlock Scan", limitedVulns.map((vuln) => buildRequirement$9(vuln)), {
5017
+ const packageTypes = buildPackageTypeIndex(result.packages);
5018
+ const requirements = limitedVulns.map((vuln) => buildRequirement$9(vuln, packageTypes, result.distro));
5019
+ if (requirements.length === 0) {
5020
+ const target = result.name ?? result.repository ?? result.id ?? "scan target";
5021
+ requirements.push(buildNoFindingsRequirement("twistlock-no-findings", `Twistlock scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
5022
+ }
5023
+ return createMinimalBaseline("Twistlock Scan", requirements, {
3387
5024
  resultsChecksum,
3388
5025
  title: buildTitle$1(result),
3389
5026
  summary: buildSummary(result)
@@ -3418,7 +5055,7 @@ async function convertTwistlockToHdf(input) {
3418
5055
  baselines,
3419
5056
  components: [{
3420
5057
  name: targetName,
3421
- type: Copyright.ContainerImage,
5058
+ type: TargetType.ContainerImage,
3422
5059
  labels: { image: results[0]?.id ?? targetName }
3423
5060
  }],
3424
5061
  timestamp: /* @__PURE__ */ new Date()
@@ -3486,7 +5123,32 @@ function buildRequirement$8(finding, timestamp) {
3486
5123
  codeDesc,
3487
5124
  startTime: timestamp ? new Date(timestamp) : void 0
3488
5125
  })];
3489
- return createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
5126
+ const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
5127
+ req.verificationMethod = VerificationMethodEnum.Automated;
5128
+ const controlType = deriveControlTypeFromTags(nist);
5129
+ if (controlType !== void 0) req.controlType = controlType;
5130
+ const pkg = buildAffectedPackageFromComponent(finding.component);
5131
+ if (pkg) req.affectedPackages = [pkg];
5132
+ return req;
5133
+ }
5134
+ /**
5135
+ * Builds an Affected_Package from a Dependency-Track component. Prefers
5136
+ * the rich identifiers Dependency-Track already exposes (purl, cpe) and
5137
+ * augments with name/version/ecosystem when available. Returns undefined
5138
+ * when the component carries no schema-acceptable identifier.
5139
+ */
5140
+ function buildAffectedPackageFromComponent(c) {
5141
+ let ecosystem;
5142
+ if (c.purl) ecosystem = ecosystemFromPurlType(parsePurl(c.purl)?.type);
5143
+ else if (c.name && c.version) ecosystem = ecosystemFromPurlType(void 0);
5144
+ return buildAffectedPackage$1({
5145
+ name: c.name,
5146
+ version: c.version,
5147
+ ecosystem,
5148
+ purl: c.purl,
5149
+ cpe: c.cpe,
5150
+ fixedInVersion: c.latestVersion
5151
+ });
3490
5152
  }
3491
5153
  /**
3492
5154
  * Converts Dependency-Track FPF JSON output to HDF format.
@@ -3501,7 +5163,12 @@ async function convertDeptrackToHdf(input) {
3501
5163
  const parsed = parseJSON(input);
3502
5164
  if (!parsed || typeof parsed !== "object") throw new Error("deptrack: invalid JSON");
3503
5165
  if (!parsed.findings && !parsed.project && !parsed.meta) throw new Error("deptrack: input does not appear to be a Dependency-Track report");
3504
- const baseline = createMinimalBaseline("Dependency-Track Scan", (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp)), {
5166
+ const requirements = (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp));
5167
+ if (requirements.length === 0) {
5168
+ const projectName = parsed.project?.name ?? parsed.project?.uuid ?? "";
5169
+ requirements.push(buildNoFindingsRequirement("deptrack-no-findings", `Dependency-Track analyzed ${projectName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
5170
+ }
5171
+ const baseline = createMinimalBaseline("Dependency-Track Scan", requirements, {
3505
5172
  resultsChecksum,
3506
5173
  title: `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`,
3507
5174
  summary: parsed.project?.description
@@ -3515,7 +5182,7 @@ async function convertDeptrackToHdf(input) {
3515
5182
  baselines: [baseline],
3516
5183
  components: [{
3517
5184
  name: targetName,
3518
- type: Copyright.Application
5185
+ type: TargetType.Application
3519
5186
  }],
3520
5187
  timestamp: /* @__PURE__ */ new Date()
3521
5188
  });
@@ -3589,7 +5256,52 @@ function buildRequirement$7(entryID, entries) {
3589
5256
  data: formatDescription(rep)
3590
5257
  }];
3591
5258
  const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$1(entry) }));
3592
- return createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
5259
+ const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
5260
+ const controlType = deriveControlTypeFromTags(nist);
5261
+ if (controlType !== void 0) req.controlType = controlType;
5262
+ req.verificationMethod = VerificationMethodEnum.Automated;
5263
+ const pkg = buildAffectedPackageFromEntry(rep);
5264
+ if (pkg) req.affectedPackages = [pkg];
5265
+ return req;
5266
+ }
5267
+ function ecosystemFromXraySource(scheme) {
5268
+ if (scheme === "gav") return Ecosystem.Maven;
5269
+ return ecosystemFromPurlType(scheme);
5270
+ }
5271
+ function parseSourceCompID(s) {
5272
+ if (!s) return void 0;
5273
+ const m = /^([a-zA-Z0-9]+):\/\/(.+)$/.exec(s);
5274
+ if (!m) return void 0;
5275
+ const scheme = m[1].toLowerCase();
5276
+ const rest = m[2];
5277
+ const colonIdx = rest.lastIndexOf(":");
5278
+ if (colonIdx > 0) return {
5279
+ scheme,
5280
+ name: rest.slice(0, colonIdx),
5281
+ version: rest.slice(colonIdx + 1)
5282
+ };
5283
+ return {
5284
+ scheme,
5285
+ name: rest
5286
+ };
5287
+ }
5288
+ function buildAffectedPackageFromEntry(entry) {
5289
+ const parsed = parseSourceCompID(entry.source_comp_id ?? entry.source_id);
5290
+ let name = entry.component ?? "";
5291
+ let version;
5292
+ let ecosystem;
5293
+ if (parsed) {
5294
+ if (parsed.name) name = parsed.name;
5295
+ version = parsed.version;
5296
+ ecosystem = ecosystemFromXraySource(parsed.scheme);
5297
+ }
5298
+ const fixed = entry.component_versions?.fixed_versions?.[0];
5299
+ return buildAffectedPackage$1({
5300
+ name,
5301
+ version,
5302
+ ecosystem: ecosystem ?? (name ? Ecosystem.Generic : void 0),
5303
+ fixedInVersion: fixed
5304
+ });
3593
5305
  }
3594
5306
  /**
3595
5307
  * Converts JFrog Xray JSON output to HDF format.
@@ -3620,6 +5332,7 @@ async function convertJfrogXrayToHdf(input) {
3620
5332
  }
3621
5333
  const requirements = [];
3622
5334
  for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries));
5335
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", /* @__PURE__ */ new Date()));
3623
5336
  return buildHdfResults({
3624
5337
  generatorName: "jfrog-xray-to-hdf",
3625
5338
  converterVersion: "1.0.0",
@@ -3628,7 +5341,7 @@ async function convertJfrogXrayToHdf(input) {
3628
5341
  baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
3629
5342
  components: [{
3630
5343
  name: "JFrog Xray Scan",
3631
- type: Copyright.Application
5344
+ type: TargetType.Application
3632
5345
  }],
3633
5346
  timestamp: /* @__PURE__ */ new Date()
3634
5347
  });
@@ -3687,7 +5400,20 @@ function buildRequirement$6(vuln) {
3687
5400
  data: vuln.description
3688
5401
  }];
3689
5402
  const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), { codeDesc: "" })];
3690
- return createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
5403
+ const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
5404
+ const controlType = deriveControlTypeFromTags(nist);
5405
+ if (controlType !== void 0) req.controlType = controlType;
5406
+ req.verificationMethod = VerificationMethodEnum.Automated;
5407
+ const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
5408
+ const pkg = buildAffectedPackage$1({
5409
+ name: vuln.package_name,
5410
+ version: vuln.package_version,
5411
+ ecosystem: vuln.package_name && vuln.package_version ? Ecosystem.Generic : void 0,
5412
+ cpe: cpe23,
5413
+ fixedInVersion: vuln.fixed_version
5414
+ });
5415
+ if (pkg) req.affectedPackages = [pkg];
5416
+ return req;
3691
5417
  }
3692
5418
  /**
3693
5419
  * Constructs the baseline title from the image metadata.
@@ -3732,6 +5458,9 @@ async function convertNeuvectorToHdf(input) {
3732
5458
  seen.add(id);
3733
5459
  requirements.push(buildRequirement$6(vuln));
3734
5460
  }
5461
+ const now = /* @__PURE__ */ new Date();
5462
+ const target = targetNameFromReport(scan.report);
5463
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, now));
3735
5464
  return buildHdfResults({
3736
5465
  generatorName: "neuvector-to-hdf",
3737
5466
  converterVersion: "1.0.0",
@@ -3743,13 +5472,13 @@ async function convertNeuvectorToHdf(input) {
3743
5472
  })],
3744
5473
  components: [{
3745
5474
  name: targetNameFromReport(scan.report),
3746
- type: Copyright.ContainerImage,
5475
+ type: TargetType.ContainerImage,
3747
5476
  labels: {
3748
5477
  image: `${scan.report.registry}/${scan.report.repository}:${scan.report.tag}`,
3749
5478
  registry: scan.report.registry
3750
5479
  }
3751
5480
  }],
3752
- timestamp: /* @__PURE__ */ new Date()
5481
+ timestamp: now
3753
5482
  });
3754
5483
  }
3755
5484
  //#endregion
@@ -3826,14 +5555,18 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
3826
5555
  codeDesc: buildCodeDesc$2(vuln, snippetMap),
3827
5556
  startTime: new Date(startTimeStr)
3828
5557
  }));
3829
- return {
5558
+ const req = {
3830
5559
  id: desc.classID ?? "unknown",
3831
5560
  title,
3832
5561
  impact,
3833
5562
  tags,
3834
5563
  descriptions,
3835
- results
5564
+ results,
5565
+ verificationMethod: VerificationMethodEnum.Automated
3836
5566
  };
5567
+ const controlType = deriveControlTypeFromTags(nistTags);
5568
+ if (controlType !== void 0) req.controlType = controlType;
5569
+ return req;
3837
5570
  }
3838
5571
  /**
3839
5572
  * Converts Fortify FVDL XML to HDF format.
@@ -3860,21 +5593,22 @@ async function convertFortifyToHdf(input) {
3860
5593
  if (truncatedDescs) console.warn(`WARNING: Input truncated at ${limitedDescs.length} Description items (original: ${descriptions.length})`);
3861
5594
  const createdDate = fvdl.CreatedTS?.date ?? "";
3862
5595
  const startTimeStr = `${createdDate}T${fvdl.CreatedTS?.time ?? ""}`;
3863
- const baseline = createMinimalBaseline("Fortify Scan", limitedDescs.map((desc) => {
5596
+ const requirements = limitedDescs.map((desc) => {
3864
5597
  return buildRequirement$5(desc, vulnGroups.get(desc.classID ?? "") ?? [], snippetMap, startTimeStr);
3865
- }), {
3866
- resultsChecksum,
3867
- title: "Fortify Static Analyzer Scan",
3868
- summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
3869
- version: fvdl.EngineData?.EngineVersion ?? "",
3870
- status: "loaded"
3871
5598
  });
3872
5599
  const targetName = fvdl.Build?.SourceBasePath ?? fvdl.Build?.BuildID ?? "Unknown";
5600
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("fortify-no-findings", `Fortify scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
3873
5601
  const hdfResult = {
3874
- baselines: [baseline],
5602
+ baselines: [createMinimalBaseline("Fortify Scan", requirements, {
5603
+ resultsChecksum,
5604
+ title: "Fortify Static Analyzer Scan",
5605
+ summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
5606
+ version: fvdl.EngineData?.EngineVersion ?? "",
5607
+ status: "loaded"
5608
+ })],
3875
5609
  components: [{
3876
5610
  name: targetName,
3877
- type: Copyright.Repository
5611
+ type: TargetType.Repository
3878
5612
  }],
3879
5613
  generator: {
3880
5614
  name: "fortify-to-hdf",
@@ -3985,7 +5719,52 @@ function buildRequirement$4(rec) {
3985
5719
  data: rec.Description
3986
5720
  }];
3987
5721
  const results = [createResult(ResultStatus.Failed, message, { codeDesc })];
3988
- return createRequirement(id, title, descriptions, getImpact$3(rec.Severity), results, { tags });
5722
+ const req = createRequirement(id, title, descriptions, getImpact$3(rec.Severity), results, { tags });
5723
+ const controlType = deriveControlTypeFromTags(nist);
5724
+ if (controlType !== void 0) req.controlType = controlType;
5725
+ req.verificationMethod = VerificationMethodEnum.Automated;
5726
+ if (rec["CVE ID"]) {
5727
+ const pkg = buildAffectedPackageFromRecord(rec);
5728
+ if (pkg) req.affectedPackages = [pkg];
5729
+ }
5730
+ return req;
5731
+ }
5732
+ /**
5733
+ * Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
5734
+ * `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
5735
+ * mapped — unknown vendors fall back to `generic` rather than guessing.
5736
+ */
5737
+ function ecosystemFromDistro(distro) {
5738
+ if (!distro) return Ecosystem.Generic;
5739
+ switch (distro.split("-")[0]?.toLowerCase()) {
5740
+ case "redhat":
5741
+ case "rhel":
5742
+ case "centos":
5743
+ case "rocky":
5744
+ case "alma":
5745
+ case "fedora":
5746
+ case "amazon":
5747
+ case "amazonlinux":
5748
+ case "suse":
5749
+ case "sles":
5750
+ case "opensuse": return Ecosystem.RPM;
5751
+ case "debian":
5752
+ case "ubuntu": return Ecosystem.Deb;
5753
+ default: return Ecosystem.Generic;
5754
+ }
5755
+ }
5756
+ const FIX_VERSION_PATTERN = /fixed in\s+([^\s,;]+)/i;
5757
+ function buildAffectedPackageFromRecord(rec) {
5758
+ const name = rec["Source Package"] || rec.Packages;
5759
+ const version = rec["Package Version"];
5760
+ if (!name || !version) return void 0;
5761
+ const fixMatch = rec["Fix Status"] ? FIX_VERSION_PATTERN.exec(rec["Fix Status"]) : null;
5762
+ return buildAffectedPackage$1({
5763
+ name,
5764
+ version,
5765
+ ecosystem: ecosystemFromDistro(rec.Distro),
5766
+ fixedInVersion: fixMatch ? fixMatch[1] : void 0
5767
+ });
3989
5768
  }
3990
5769
  /**
3991
5770
  * Groups records by hostname, preserving insertion order.
@@ -4018,20 +5797,25 @@ function buildBaseline(hostname, records, resultsChecksum) {
4018
5797
  async function convertPrismaToHdf(input) {
4019
5798
  if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
4020
5799
  validateInputSize(input, "prisma");
5800
+ const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
5801
+ for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
4021
5802
  const records = parseCsv(input);
4022
- if (records.length === 0) throw new Error("prisma: no data rows in CSV");
4023
- const firstRecord = records[0];
4024
- for (const col of REQUIRED_COLUMNS) if (!(col in firstRecord)) throw new Error(`prisma: missing required CSV column "${col}"`);
4025
5803
  const resultsChecksum = await inputChecksum(input);
4026
- const hostGroups = groupByHostname(records);
4027
5804
  const baselines = [];
4028
5805
  const components = [];
4029
- for (const [hostname, hostRecords] of hostGroups) {
4030
- baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
4031
- components.push({
4032
- name: hostname,
4033
- type: Copyright.Host
4034
- });
5806
+ if (records.length === 0) baselines.push(createMinimalBaseline("Prisma Cloud Scan", [buildNoFindingsRequirement("prisma-no-findings", "Prisma Cloud scanned the workload and reported zero vulnerable components.", /* @__PURE__ */ new Date())], {
5807
+ resultsChecksum,
5808
+ title: "Prisma Cloud Scan"
5809
+ }));
5810
+ else {
5811
+ const hostGroups = groupByHostname(records);
5812
+ for (const [hostname, hostRecords] of hostGroups) {
5813
+ baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
5814
+ components.push({
5815
+ name: hostname,
5816
+ type: TargetType.Host
5817
+ });
5818
+ }
4035
5819
  }
4036
5820
  return buildHdfResults({
4037
5821
  generatorName: "prisma-to-hdf",
@@ -4145,7 +5929,7 @@ function buildRequirement$3(vuln, initiated) {
4145
5929
  startTime
4146
5930
  }];
4147
5931
  const impact = getImpact$2(vuln.severity ?? "");
4148
- return {
5932
+ const req = {
4149
5933
  id: vuln.LookupId ?? "",
4150
5934
  title: vuln.name ?? void 0,
4151
5935
  impact,
@@ -4153,6 +5937,10 @@ function buildRequirement$3(vuln, initiated) {
4153
5937
  descriptions,
4154
5938
  results
4155
5939
  };
5940
+ const controlType = deriveControlTypeFromTags(nist);
5941
+ if (controlType !== void 0) req.controlType = controlType;
5942
+ req.verificationMethod = VerificationMethodEnum.Automated;
5943
+ return req;
4156
5944
  }
4157
5945
  /**
4158
5946
  * Converts Netsparker/Invicti XML scan results to HDF format.
@@ -4176,20 +5964,25 @@ async function convertNetsparkerToHdf(input) {
4176
5964
  const { items: limitedVulns, truncated } = limitArray(vulns);
4177
5965
  /* v8 ignore next -- truncation only triggers with >100K items */
4178
5966
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
4179
- const baseline = createMinimalBaseline("Netsparker Scan", limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated)), {
4180
- resultsChecksum,
4181
- title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
4182
- });
4183
5967
  const targetName = target.url ?? "Unknown";
5968
+ const requirements = limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated));
5969
+ if (requirements.length === 0) {
5970
+ const initiatedDate = initiated ? new Date(initiated) : /* @__PURE__ */ new Date();
5971
+ const startTime = isNaN(initiatedDate.getTime()) ? /* @__PURE__ */ new Date() : initiatedDate;
5972
+ requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
5973
+ }
4184
5974
  return buildHdfResults({
4185
5975
  generatorName: "netsparker-to-hdf",
4186
5976
  converterVersion: "1.0.0",
4187
5977
  toolName,
4188
5978
  toolFormat: "XML",
4189
- baselines: [baseline],
5979
+ baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
5980
+ resultsChecksum,
5981
+ title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
5982
+ })],
4190
5983
  components: [{
4191
5984
  name: targetName,
4192
- type: Copyright.Application
5985
+ type: TargetType.Application
4193
5986
  }]
4194
5987
  });
4195
5988
  }
@@ -4284,7 +6077,11 @@ function buildRequirement$2(ruleID, finding, startTime) {
4284
6077
  codeDesc: finding.description,
4285
6078
  startTime: startTime ? new Date(startTime) : void 0
4286
6079
  });
4287
- return createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
6080
+ const req = createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
6081
+ const controlType = deriveControlTypeFromTags(nist);
6082
+ if (controlType !== void 0) req.controlType = controlType;
6083
+ req.verificationMethod = VerificationMethodEnum.Automated;
6084
+ return req;
4288
6085
  }
4289
6086
  /**
4290
6087
  * Converts ScoutSuite output to HDF format.
@@ -4300,12 +6097,14 @@ async function convertScoutsuiteToHdf(input) {
4300
6097
  const resultsChecksum = await inputChecksum(jsonStr);
4301
6098
  const report = parseJSON(jsonStr);
4302
6099
  if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
4303
- const baseline = createMinimalBaseline("ScoutSuite Scan", limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time)), {
6100
+ const requirements = limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time));
6101
+ const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
6102
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("scoutsuite-no-findings", `ScoutSuite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
6103
+ const baseline = createMinimalBaseline("ScoutSuite Scan", requirements, {
4304
6104
  resultsChecksum,
4305
6105
  title: `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`,
4306
6106
  summary: report.last_run.ruleset_about
4307
6107
  });
4308
- const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
4309
6108
  return buildHdfResults({
4310
6109
  generatorName: "scoutsuite-to-hdf",
4311
6110
  converterVersion: "1.0.0",
@@ -4315,7 +6114,7 @@ async function convertScoutsuiteToHdf(input) {
4315
6114
  baselines: [baseline],
4316
6115
  components: [{
4317
6116
  name: targetName,
4318
- type: Copyright.CloudAccount,
6117
+ type: TargetType.CloudAccount,
4319
6118
  labels: {
4320
6119
  account: report.account_id,
4321
6120
  provider: report.provider_code ?? report.provider_name
@@ -4432,7 +6231,11 @@ function buildRequirementFromResult(result, filename) {
4432
6231
  codeDesc: `No sections reported by ${scannerName}`,
4433
6232
  startTime
4434
6233
  })];
4435
- return createRequirement(result.sha256, filename, descriptions, scoreToImpact(score), results, { tags });
6234
+ const req = createRequirement(result.sha256, filename, descriptions, scoreToImpact(score), results, { tags });
6235
+ req.verificationMethod = VerificationMethodEnum.Automated;
6236
+ const controlType = deriveControlTypeFromTags([...nist]);
6237
+ if (controlType !== void 0) req.controlType = controlType;
6238
+ return req;
4436
6239
  }
4437
6240
  /**
4438
6241
  * Builds an HDF baseline for a single scanner's results.
@@ -4468,6 +6271,10 @@ async function convertConveyorToHdf(input) {
4468
6271
  const desc = data.api_response.params["description"];
4469
6272
  if (typeof desc === "string" && desc.length > 0) targetName = desc;
4470
6273
  }
6274
+ if (baselines.length === 0) baselines.push(createMinimalBaseline("Conveyor Scan", [buildNoFindingsRequirement("conveyor-no-findings", `Conveyor scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())], {
6275
+ resultsChecksum,
6276
+ title: "Conveyor Scan (no findings)"
6277
+ }));
4471
6278
  return buildHdfResults({
4472
6279
  generatorName: "conveyor-to-hdf",
4473
6280
  converterVersion: "1.0.0",
@@ -4476,7 +6283,7 @@ async function convertConveyorToHdf(input) {
4476
6283
  baselines,
4477
6284
  components: [{
4478
6285
  name: targetName,
4479
- type: Copyright.Application
6286
+ type: TargetType.Application
4480
6287
  }],
4481
6288
  timestamp: /* @__PURE__ */ new Date()
4482
6289
  });
@@ -4660,7 +6467,11 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
4660
6467
  startTime
4661
6468
  }));
4662
6469
  });
4663
- return createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, { tags });
6470
+ const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, { tags });
6471
+ const controlType = deriveControlTypeFromTags(nist);
6472
+ if (controlType !== void 0) req.controlType = controlType;
6473
+ req.verificationMethod = VerificationMethodEnum.Automated;
6474
+ return req;
4664
6475
  }
4665
6476
  /** Build CVE-based requirements from SCA components. */
4666
6477
  function buildCVERequirements(sca, firstBuildDate) {
@@ -4712,10 +6523,14 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
4712
6523
  }));
4713
6524
  const cveSummary = attr(vuln, "cve_summary");
4714
6525
  const cveId = attr(vuln, "cve_id");
4715
- return createRequirement(cveId, cveId, [{
6526
+ const req = createRequirement(cveId, cveId, [{
4716
6527
  label: "default",
4717
6528
  data: cveSummary || ""
4718
6529
  }], impact, results, { tags });
6530
+ const controlType = deriveControlTypeFromTags(nist);
6531
+ if (controlType !== void 0) req.controlType = controlType;
6532
+ req.verificationMethod = VerificationMethodEnum.Automated;
6533
+ return req;
4719
6534
  }
4720
6535
  /**
4721
6536
  * Convert Veracode DetailedReport XML to HDF JSON string.
@@ -4735,6 +6550,8 @@ async function convertVeracodeToHdf(input) {
4735
6550
  const cweRequirements = buildCWERequirements(ensureArray(report.severity), firstBuildDate);
4736
6551
  const cveRequirements = buildCVERequirements(report.software_composition_analysis, firstBuildDate);
4737
6552
  const allRequirements = [...cweRequirements, ...cveRequirements];
6553
+ const targetName = attr(report, "app_name") || "Veracode Application";
6554
+ if (allRequirements.length === 0) allRequirements.push(buildNoFindingsRequirement("veracode-no-findings", `Veracode scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
4738
6555
  let title;
4739
6556
  const staticAnalysis = report["static-analysis"];
4740
6557
  if (staticAnalysis) {
@@ -4750,7 +6567,6 @@ async function convertVeracodeToHdf(input) {
4750
6567
  version: attr(report, "policy_version"),
4751
6568
  summary: attr(report, "policy_name")
4752
6569
  });
4753
- const targetName = attr(report, "app_name") || "Veracode Application";
4754
6570
  const timestamp = parseVeracodeTimestamp(firstBuildDate);
4755
6571
  return buildHdfResults({
4756
6572
  generatorName: "veracode-to-hdf",
@@ -4760,7 +6576,7 @@ async function convertVeracodeToHdf(input) {
4760
6576
  baselines: [baseline],
4761
6577
  components: [{
4762
6578
  name: targetName,
4763
- type: Copyright.Application
6579
+ type: TargetType.Application
4764
6580
  }],
4765
6581
  timestamp
4766
6582
  });
@@ -4811,7 +6627,8 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
4811
6627
  const title = getTitle(profiles, cs);
4812
6628
  const impact = getImpact(profiles, cs);
4813
6629
  const status = getStatus(profiles, cs);
4814
- const tags = buildNistCciTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS], []);
6630
+ const nist = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
6631
+ const tags = buildNistCciTags(nist, []);
4815
6632
  const descriptions = [{
4816
6633
  label: "default",
4817
6634
  data: stripHTML(cs.description)
@@ -4831,10 +6648,14 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
4831
6648
  }
4832
6649
  const codeDesc = cs.implementationStatus || "No implementation status provided";
4833
6650
  const startTime = createdDateTime ? new Date(createdDateTime) : void 0;
4834
- return createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
6651
+ const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
4835
6652
  codeDesc,
4836
6653
  ...startTime ? { startTime } : {}
4837
6654
  })], { tags });
6655
+ const controlType = deriveControlTypeFromTags(nist);
6656
+ if (controlType !== void 0) req.controlType = controlType;
6657
+ req.verificationMethod = VerificationMethodEnum.Automated;
6658
+ return req;
4838
6659
  }
4839
6660
  /**
4840
6661
  * Converts Microsoft Secure Score combined JSON to HDF format.
@@ -4873,7 +6694,7 @@ async function convertMsftSecureScoreToHdf(input) {
4873
6694
  }),
4874
6695
  components: [{
4875
6696
  name: `Azure Tenant: ${tenantId}`,
4876
- type: Copyright.CloudAccount,
6697
+ type: TargetType.CloudAccount,
4877
6698
  labels: {
4878
6699
  account: tenantId,
4879
6700
  provider: "azure"
@@ -4896,6 +6717,7 @@ async function convertMsftDefenderDevopsToHdf(input) {
4896
6717
  const { components, runEnrichments } = extractEnrichments(raw);
4897
6718
  const hdfJson = await convertSarifToHdf(input);
4898
6719
  const result = JSON.parse(hdfJson);
6720
+ synthesizeNoFindingsPlaceholders(result);
4899
6721
  applyEnrichments(result, components, runEnrichments);
4900
6722
  if (result.generator) result.generator.name = "msft-defender-devops-to-hdf";
4901
6723
  if (result.tool) result.tool.name = "Microsoft Defender for DevOps";
@@ -4910,7 +6732,7 @@ function extractEnrichments(raw) {
4910
6732
  seenRepos.add(vcp.repositoryUri);
4911
6733
  const target = {
4912
6734
  name: repoNameFromURI(vcp.repositoryUri),
4913
- type: Copyright.Repository,
6735
+ type: TargetType.Repository,
4914
6736
  url: vcp.repositoryUri,
4915
6737
  labels: {}
4916
6738
  };
@@ -4960,6 +6782,14 @@ function applyEnrichments(result, components, runEnrichments) {
4960
6782
  }
4961
6783
  }
4962
6784
  }
6785
+ function synthesizeNoFindingsPlaceholders(result) {
6786
+ const startTime = result.timestamp ?? /* @__PURE__ */ new Date();
6787
+ for (const baseline of result.baselines ?? []) {
6788
+ if (baseline.requirements && baseline.requirements.length > 0) continue;
6789
+ const tool = baseline.name;
6790
+ baseline.requirements = [buildNoFindingsRequirement(`${tool}-no-findings`, `Microsoft Defender for DevOps scanner "${tool}" ran and reported zero findings.`, startTime)];
6791
+ }
6792
+ }
4963
6793
  function repoNameFromURI(uri) {
4964
6794
  const parts = uri.split("/");
4965
6795
  return parts[parts.length - 1] || uri;
@@ -5021,7 +6851,9 @@ function buildRequirement(assessmentID, assessments) {
5021
6851
  data: meta.remediationDescription
5022
6852
  });
5023
6853
  const results = assessments.map(buildResultFromAssessment);
5024
- return createRequirement(assessmentID, rep.properties.displayName, descriptions, impact, results, { tags });
6854
+ const req = createRequirement(assessmentID, rep.properties.displayName, descriptions, impact, results, { tags });
6855
+ req.verificationMethod = VerificationMethodEnum.Automated;
6856
+ return req;
5025
6857
  }
5026
6858
  /**
5027
6859
  * Converts a single assessment into an HDF RequirementResult.
@@ -5054,21 +6886,23 @@ async function convertMsftDefenderCloudToHdf(input) {
5054
6886
  }
5055
6887
  const requirements = [];
5056
6888
  for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments));
6889
+ const subscriptionID = limitedAssessments.length > 0 ? extractSubscriptionID(limitedAssessments[0].id) : "";
6890
+ if (requirements.length === 0) {
6891
+ const targetName = subscriptionID || "Unknown";
6892
+ requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
6893
+ }
5057
6894
  const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
5058
6895
  const components = [];
5059
- if (limitedAssessments.length > 0) {
5060
- const subscriptionID = extractSubscriptionID(limitedAssessments[0].id);
5061
- if (subscriptionID) components.push({
5062
- name: `Azure Subscription ${subscriptionID}`,
5063
- type: Copyright.CloudAccount,
5064
- accountId: subscriptionID,
5065
- provider: "azure",
5066
- labels: {
5067
- account: subscriptionID,
5068
- provider: "azure"
5069
- }
5070
- });
5071
- }
6896
+ if (subscriptionID) components.push({
6897
+ name: `Azure Subscription ${subscriptionID}`,
6898
+ type: TargetType.CloudAccount,
6899
+ accountId: subscriptionID,
6900
+ provider: "azure",
6901
+ labels: {
6902
+ account: subscriptionID,
6903
+ provider: "azure"
6904
+ }
6905
+ });
5072
6906
  return buildHdfResults({
5073
6907
  generatorName: "msft-defender-cloud-to-hdf",
5074
6908
  converterVersion: "1.0.0",
@@ -5143,7 +6977,7 @@ function extractDeviceTarget(alert) {
5143
6977
  for (const ev of alert.evidence) if ((ev["@odata.type"] ?? "").includes("deviceEvidence") && ev.deviceDnsName) {
5144
6978
  const target = {
5145
6979
  name: ev.deviceDnsName,
5146
- type: Copyright.Host,
6980
+ type: TargetType.Host,
5147
6981
  labels: { provider: "azure" }
5148
6982
  };
5149
6983
  if (ev.deviceDnsName) target.fqdn = ev.deviceDnsName;
@@ -5153,7 +6987,7 @@ function extractDeviceTarget(alert) {
5153
6987
  }
5154
6988
  return {
5155
6989
  name: alert.tenantId ?? "unknown",
5156
- type: Copyright.CloudAccount,
6990
+ type: TargetType.CloudAccount,
5157
6991
  accountId: alert.tenantId,
5158
6992
  labels: {
5159
6993
  account: alert.tenantId ?? "",
@@ -5189,7 +7023,12 @@ function alertToRequirement(alert) {
5189
7023
  data: alert.recommendedActions
5190
7024
  });
5191
7025
  const tags = buildTags$1(alert);
5192
- return createRequirement(alert.id, alert.title, descriptions, impact, results, { tags });
7026
+ const req = createRequirement(alert.id, alert.title, descriptions, impact, results, { tags });
7027
+ const nistTags = tags.nist;
7028
+ const controlType = deriveControlTypeFromTags(nistTags);
7029
+ if (controlType !== void 0) req.controlType = controlType;
7030
+ req.verificationMethod = VerificationMethodEnum.Automated;
7031
+ return req;
5193
7032
  }
5194
7033
  /**
5195
7034
  * Converts Microsoft Defender for Endpoint alerts (Microsoft Graph Security API v2 format) to HDF.
@@ -5217,6 +7056,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
5217
7056
  components.push(target);
5218
7057
  }
5219
7058
  }
7059
+ if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", /* @__PURE__ */ new Date()));
5220
7060
  return buildHdfResults({
5221
7061
  generatorName: "msft-defender-endpoint-to-hdf",
5222
7062
  converterVersion: "1.0.0",
@@ -5357,8 +7197,7 @@ function buildTestResultObj(hdfData, baseline) {
5357
7197
  [`${ATTR}idref`]: ruleIdRef,
5358
7198
  result: wrap(hdfStatusToXccdf(result.status))
5359
7199
  };
5360
- const startTime = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
5361
- rr[`${ATTR}time`] = startTime;
7200
+ rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
5362
7201
  if (result.message) rr.message = wrap(result.message);
5363
7202
  if (result.codeDesc) rr.check = {
5364
7203
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
@@ -5788,7 +7627,7 @@ async function convertHdfToOscalPoam(input) {
5788
7627
  return JSON.stringify(doc, null, 2);
5789
7628
  }
5790
7629
  /**
5791
- * Converts parsed HdfAmendments to an OSCAL PlanOfActionAndMilestones.
7630
+ * Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
5792
7631
  */
5793
7632
  function amendmentsToPOAM(amendments) {
5794
7633
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5964,6 +7803,9 @@ async function convertIonchannelToHdf(input) {
5964
7803
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
5965
7804
  })], { tags });
5966
7805
  req.code = code;
7806
+ const controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
7807
+ if (controlType !== void 0) req.controlType = controlType;
7808
+ req.verificationMethod = VerificationMethodEnum.Automated;
5967
7809
  return req;
5968
7810
  });
5969
7811
  const resultsChecksum = await inputChecksum(input);
@@ -5985,6 +7827,1150 @@ async function convertIonchannelToHdf(input) {
5985
7827
  });
5986
7828
  }
5987
7829
  //#endregion
7830
+ //#region shared/typescript/vex/mapping.ts
7831
+ /** Canonical VEX status across OpenVEX / CSAF / CycloneDX. */
7832
+ const VexStatus = {
7833
+ NotAffected: "not_affected",
7834
+ Affected: "affected",
7835
+ Fixed: "fixed",
7836
+ UnderInvestigation: "under_investigation"
7837
+ };
7838
+ /**
7839
+ * Map an ecosystem-specific status string to the canonical VexStatus.
7840
+ * Returns undefined for values without a clean mapping — caller should warn
7841
+ * and skip, not guess.
7842
+ */
7843
+ function normalizeStatus(raw) {
7844
+ switch (raw.trim().toLowerCase()) {
7845
+ case "not_affected":
7846
+ case "known_not_affected":
7847
+ case "false_positive": return VexStatus.NotAffected;
7848
+ case "affected":
7849
+ case "known_affected":
7850
+ case "exploitable": return VexStatus.Affected;
7851
+ case "fixed":
7852
+ case "first_fixed":
7853
+ case "resolved":
7854
+ case "resolved_with_pedigree": return VexStatus.Fixed;
7855
+ case "under_investigation":
7856
+ case "in_triage": return VexStatus.UnderInvestigation;
7857
+ default: return;
7858
+ }
7859
+ }
7860
+ /**
7861
+ * Map an ecosystem-specific justification string to the canonical HDF
7862
+ * Justification enum. Returns undefined for unknown values; callers SHOULD
7863
+ * log unknown values rather than silently dropping (the schema spec wants
7864
+ * pass-through, but practically we expect the enum to be extended when a
7865
+ * new vocabulary is integrated rather than carrying raw labels
7866
+ * indefinitely).
7867
+ *
7868
+ * The HDF Justification enum (v3.2.x) covers:
7869
+ * - the original OpenVEX / CSAF VEX five values
7870
+ * - CycloneDX-specific reachability values (requires_*, protected_*)
7871
+ * that describe why a vulnerable code path is unreachable in the
7872
+ * deployed configuration.
7873
+ */
7874
+ function normalizeJustification(raw) {
7875
+ switch (raw.trim().toLowerCase()) {
7876
+ case "component_not_present":
7877
+ case "code_not_present": return Justification.ComponentNotPresent;
7878
+ case "vulnerable_code_not_present": return Justification.VulnerableCodeNotPresent;
7879
+ case "vulnerable_code_not_in_execute_path":
7880
+ case "code_not_reachable": return Justification.VulnerableCodeNotInExecutePath;
7881
+ case "vulnerable_code_cannot_be_controlled_by_adversary": return Justification.VulnerableCodeCannotBeControlledByAdversary;
7882
+ case "inline_mitigations_already_exist":
7883
+ case "protected_by_mitigating_control": return Justification.InlineMitigationsAlreadyExist;
7884
+ case "requires_configuration": return Justification.RequiresConfiguration;
7885
+ case "requires_dependency": return Justification.RequiresDependency;
7886
+ case "requires_environment": return Justification.RequiresEnvironment;
7887
+ case "protected_by_compiler": return Justification.ProtectedByCompiler;
7888
+ case "protected_at_runtime": return Justification.ProtectedAtRuntime;
7889
+ case "protected_at_perimeter": return Justification.ProtectedAtPerimeter;
7890
+ default: return;
7891
+ }
7892
+ }
7893
+ /**
7894
+ * Render an HDF Justification value as the CycloneDX-native vocabulary.
7895
+ * CycloneDX uses short-form names (code_not_present, code_not_reachable,
7896
+ * protected_by_mitigating_control) for the three justifications shared
7897
+ * with OpenVEX/CSAF, and shares the six CycloneDX-specific reachability
7898
+ * values verbatim.
7899
+ *
7900
+ * Returns undefined when the HDF value has no equivalent in CycloneDX's
7901
+ * enum (vulnerable_code_not_present and
7902
+ * vulnerable_code_cannot_be_controlled_by_adversary). Callers should
7903
+ * omit the justification field in that case.
7904
+ */
7905
+ function justificationForCycloneDX(j) {
7906
+ switch (j) {
7907
+ case Justification.ComponentNotPresent: return "code_not_present";
7908
+ case Justification.VulnerableCodeNotInExecutePath: return "code_not_reachable";
7909
+ case Justification.InlineMitigationsAlreadyExist: return "protected_by_mitigating_control";
7910
+ case Justification.RequiresConfiguration:
7911
+ case Justification.RequiresDependency:
7912
+ case Justification.RequiresEnvironment:
7913
+ case Justification.ProtectedByCompiler:
7914
+ case Justification.ProtectedAtRuntime:
7915
+ case Justification.ProtectedAtPerimeter: return String(j);
7916
+ default: return;
7917
+ }
7918
+ }
7919
+ /**
7920
+ * Return the amendment shape an importer should produce for a canonical VEX
7921
+ * status. Returns undefined for "affected" and "under_investigation" — those
7922
+ * are informational; the consumer creates an amendment later if they act.
7923
+ */
7924
+ function importTargetFor(status) {
7925
+ switch (status) {
7926
+ case VexStatus.NotAffected: return {
7927
+ overrideType: OverrideType.FalsePositive,
7928
+ status: ResultStatus.Passed,
7929
+ setJustification: true,
7930
+ poamActionTemplate: ""
7931
+ };
7932
+ case VexStatus.Fixed: return {
7933
+ overrideType: OverrideType.Poam,
7934
+ status: ResultStatus.Failed,
7935
+ setJustification: false,
7936
+ poamActionTemplate: "vendor reports fix; apply and re-scan to verify"
7937
+ };
7938
+ case VexStatus.Affected:
7939
+ case VexStatus.UnderInvestigation: return;
7940
+ /* c8 ignore next 2 — every VexStatus has a case above */
7941
+ default: return;
7942
+ }
7943
+ }
7944
+ /**
7945
+ * Return the canonical VEX status an exporter should emit for an HDF
7946
+ * override. Returns undefined when no VEX statement should be emitted —
7947
+ * the consumer has not acted, and VEX requires a deliberate statement.
7948
+ *
7949
+ * `allMilestonesCompleted` and `closureChained` are consulted only for
7950
+ * POA&M overrides. Closure is signalled by BOTH flags being true; either
7951
+ * alone is insufficient.
7952
+ */
7953
+ function exportStatusFor(override, allMilestonesCompleted, closureChained) {
7954
+ if (!override) return;
7955
+ if (override.justification) return VexStatus.NotAffected;
7956
+ switch (override.type) {
7957
+ case OverrideType.FalsePositive:
7958
+ case OverrideType.Attestation:
7959
+ case OverrideType.Inherited: return VexStatus.NotAffected;
7960
+ case OverrideType.Waiver:
7961
+ case OverrideType.RiskAdjustment:
7962
+ case OverrideType.OperationalRequirement: return VexStatus.Affected;
7963
+ case OverrideType.Poam: return allMilestonesCompleted && closureChained ? VexStatus.Fixed : VexStatus.Affected;
7964
+ /* c8 ignore next 2 — every OverrideType has a case above */
7965
+ default: return;
7966
+ }
7967
+ }
7968
+ const ECOSYSTEM_FROM_PURL_TYPE = {
7969
+ npm: Ecosystem.Npm,
7970
+ pypi: Ecosystem.Pypi,
7971
+ rpm: Ecosystem.RPM,
7972
+ deb: Ecosystem.Deb,
7973
+ maven: Ecosystem.Maven,
7974
+ gem: Ecosystem.Gem,
7975
+ nuget: Ecosystem.Nuget,
7976
+ golang: Ecosystem.Go,
7977
+ go: Ecosystem.Go,
7978
+ cargo: Ecosystem.Cargo
7979
+ };
7980
+ /**
7981
+ * Build an AffectedPackage from a single product identifier string emitted
7982
+ * by a VEX format. Recognizes PURLs and CPE 2.3 strings; returns undefined
7983
+ * for opaque identifiers (importer should drop the entry — schema additions
7984
+ * forbid fabricating name+version).
7985
+ */
7986
+ function affectedPackageFromIdentifier(identifier) {
7987
+ if (!identifier) return void 0;
7988
+ if (identifier.startsWith("pkg:")) {
7989
+ const parsed = parsePurl(identifier);
7990
+ if (parsed) {
7991
+ const pkg = { purl: identifier };
7992
+ /* c8 ignore next 2 — parsePurl populates name/version when the
7993
+ identifier was a well-formed purl; the absent branches require a
7994
+ malformed-but-prefixed input that loses these fields without
7995
+ triggering the outer null return. Defensive. */
7996
+ if (parsed.name) pkg.name = parsed.name;
7997
+ if (parsed.version) pkg.version = parsed.version;
7998
+ pkg.ecosystem = ECOSYSTEM_FROM_PURL_TYPE[parsed.type] ?? Ecosystem.Generic;
7999
+ return pkg;
8000
+ }
8001
+ return { purl: identifier };
8002
+ }
8003
+ if (identifier.startsWith("cpe:2.3:")) return { cpe: identifier };
8004
+ }
8005
+ /**
8006
+ * Build a unique list of AffectedPackage entries from a sequence of product
8007
+ * identifier strings. Empty / unresolvable identifiers are dropped; duplicate
8008
+ * purls/cpes/names are collapsed.
8009
+ */
8010
+ function affectedPackagesFromIdentifiers(identifiers) {
8011
+ const out = [];
8012
+ const seen = /* @__PURE__ */ new Set();
8013
+ for (const id of identifiers) {
8014
+ const pkg = affectedPackageFromIdentifier(id);
8015
+ if (!pkg) continue;
8016
+ /* c8 ignore next — affectedPackageFromIdentifier always emits either
8017
+ purl or cpe, so the name fallback branch isn't reachable through
8018
+ this caller. Kept for safety if a hand-built AffectedPackage flows
8019
+ through a future caller. */
8020
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
8021
+ if (seen.has(key)) continue;
8022
+ seen.add(key);
8023
+ out.push(pkg);
8024
+ }
8025
+ return out;
8026
+ }
8027
+ /**
8028
+ * Render an AffectedPackage as a single identifier string suitable for
8029
+ * round-tripping into a VEX format. Prefers purl > cpe > name@version.
8030
+ * Returns undefined when nothing identifying is set.
8031
+ */
8032
+ function affectedPackageToIdentifier(pkg) {
8033
+ if (pkg.purl) return pkg.purl;
8034
+ if (pkg.cpe) return pkg.cpe;
8035
+ if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
8036
+ if (pkg.name) return pkg.name;
8037
+ }
8038
+ /**
8039
+ * Build an HDF Evidence entry pointing at the upstream VEX document. Used by
8040
+ * importers to preserve provenance even though we lose the structured
8041
+ * statement_id. Returns undefined when sourceURI is empty — don't fabricate.
8042
+ */
8043
+ function supplierEvidence(sourceURI, description) {
8044
+ if (!sourceURI.trim()) return;
8045
+ return {
8046
+ type: EvidenceType.URL,
8047
+ data: sourceURI,
8048
+ description: description?.trim() ? description : "Upstream VEX statement"
8049
+ };
8050
+ }
8051
+ //#endregion
8052
+ //#region converters/openvex-to-hdf/typescript/converter.ts
8053
+ /**
8054
+ * OpenVEX to HDF Amendments converter.
8055
+ *
8056
+ * VEX statements are consumer-attached context for CVE findings. The act of
8057
+ * attaching IS the amendment, so this converter emits HDF Amendments rather
8058
+ * than HDF Results.
8059
+ *
8060
+ * VEX 'fixed' is an abstract supplier claim; the assessed system has not
8061
+ * been verified to carry the fix. Imports therefore become open POA&M
8062
+ * overrides (status pinned to failed pending re-scan), not status flips.
8063
+ *
8064
+ * Spec: https://github.com/openvex/spec
8065
+ */
8066
+ /** One year in milliseconds. VEX statements are re-evaluated as new info
8067
+ * arrives; one year is a defensive default consistent with the
8068
+ * no-permanent-amendment rule on Standalone_Override. */
8069
+ const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
8070
+ async function convertOpenVexToHdf(input, converterVersion) {
8071
+ validateInputSize(input, "openvex-to-hdf");
8072
+ const doc = parseJSON(input);
8073
+ const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
8074
+ const overrides = [];
8075
+ for (const stmt of doc.statements ?? []) {
8076
+ const override = statementToOverride(stmt, doc, docTime);
8077
+ if (override) overrides.push(override);
8078
+ }
8079
+ 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");
8080
+ return {
8081
+ name: doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements",
8082
+ description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
8083
+ overrides,
8084
+ appliedBy: identityFor$1(doc.author, doc.role),
8085
+ generator: {
8086
+ name: "openvex-to-hdf",
8087
+ version: converterVersion
8088
+ },
8089
+ integrity: await inputIntegrity(input)
8090
+ };
8091
+ }
8092
+ function statementToOverride(stmt, doc, docTime) {
8093
+ const canonical = normalizeStatus(stmt.status ?? "");
8094
+ if (!canonical) return void 0;
8095
+ const target = importTargetFor(canonical);
8096
+ if (!target) return void 0;
8097
+ const requirementId = stmt.vulnerability?.name ?? stmt.vulnerability?.["@id"] ?? "";
8098
+ if (!requirementId) return void 0;
8099
+ const stmtTime = (stmt.timestamp ? parseTimestamp(stmt.timestamp) : null) ?? docTime;
8100
+ const author = stmt.author ?? doc.author ?? "";
8101
+ const expiresAt = new Date(stmtTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$2);
8102
+ const override = {
8103
+ type: target.overrideType,
8104
+ requirementId,
8105
+ appliedAt: stmtTime,
8106
+ expiresAt,
8107
+ appliedBy: identityFor$1(author, doc.role),
8108
+ reason: buildReason$2(stmt, target.poamActionTemplate)
8109
+ };
8110
+ const affectedPackages = affectedPackagesFromIdentifiers((stmt.products ?? []).map((p) => p["@id"]).filter((id) => Boolean(id)));
8111
+ if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
8112
+ if (target.status !== void 0) override.status = target.status;
8113
+ if (target.setJustification && stmt.justification) {
8114
+ const j = normalizeJustification(stmt.justification);
8115
+ if (j) override.justification = j;
8116
+ }
8117
+ const ev = supplierEvidence(doc["@id"] ?? "", "OpenVEX document");
8118
+ if (ev) override.evidence = [ev];
8119
+ if (target.overrideType === OverrideType.Poam) override.milestones = [{
8120
+ description: target.poamActionTemplate,
8121
+ status: MilestoneStatus.Pending,
8122
+ estimatedCompletion: expiresAt
8123
+ }];
8124
+ return override;
8125
+ }
8126
+ function buildReason$2(stmt, poamTemplate) {
8127
+ const parts = [];
8128
+ if (stmt.impact_statement) parts.push(stmt.impact_statement);
8129
+ if (stmt.action_statement) parts.push(stmt.action_statement);
8130
+ if (parts.length === 0) return poamTemplate || `Imported from OpenVEX status "${stmt.status ?? ""}"`;
8131
+ return parts.join("\n");
8132
+ }
8133
+ function identityFor$1(author, role) {
8134
+ if (!author) return {
8135
+ type: IdentityType.System,
8136
+ identifier: "openvex-import"
8137
+ };
8138
+ const id = {
8139
+ type: author.includes("@") ? IdentityType.Email : IdentityType.Simple,
8140
+ identifier: author
8141
+ };
8142
+ if (role) id.description = role;
8143
+ return id;
8144
+ }
8145
+ function truncateId(id) {
8146
+ const MAX = 80;
8147
+ return id.length <= MAX ? id : `${id.slice(0, MAX)}...`;
8148
+ }
8149
+ //#endregion
8150
+ //#region converters/csaf-vex-to-hdf/typescript/converter.ts
8151
+ /**
8152
+ * CSAF VEX to HDF Amendments converter.
8153
+ *
8154
+ * CSAF (OASIS Common Security Advisory Framework) VEX profile is the
8155
+ * vendor-advisory format. Per the amendment-output pattern (Step 4f),
8156
+ * 'fixed' becomes an open POA&M, not a status flip — supplier claim is
8157
+ * not assessed-system evidence.
8158
+ *
8159
+ * Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
8160
+ */
8161
+ const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
8162
+ async function convertCsafVexToHdf(input, converterVersion) {
8163
+ validateInputSize(input, "csaf-vex-to-hdf");
8164
+ const doc = parseJSON(input);
8165
+ 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`);
8166
+ const dm = doc.document;
8167
+ const tracking = dm.tracking ?? {};
8168
+ const publisher = dm.publisher ?? {};
8169
+ const docTime = (tracking.current_release_date ? parseTimestamp(tracking.current_release_date) : null) ?? /* @__PURE__ */ new Date();
8170
+ const productLookup = buildProductLookup$1(doc.product_tree);
8171
+ const overrides = [];
8172
+ for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
8173
+ 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");
8174
+ const publisherName = publisher.name ?? "";
8175
+ return {
8176
+ name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
8177
+ description: `Imported VEX advisory ${tracking.id ?? ""}`,
8178
+ overrides,
8179
+ appliedBy: identityFor(publisher),
8180
+ generator: {
8181
+ name: "csaf-vex-to-hdf",
8182
+ version: converterVersion
8183
+ },
8184
+ integrity: await inputIntegrity(input),
8185
+ version: tracking.version
8186
+ };
8187
+ }
8188
+ function vulnerabilityToOverrides(vuln, publisher, tracking, docTime, productLookup) {
8189
+ if (!vuln.cve) return [];
8190
+ const ps = vuln.product_status;
8191
+ if (!ps) return [];
8192
+ const out = [];
8193
+ if (ps.known_not_affected?.length) {
8194
+ const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.NotAffected, ps.known_not_affected, productLookup);
8195
+ if (o) out.push(o);
8196
+ }
8197
+ const fixedProducts = [...ps.fixed ?? [], ...ps.first_fixed ?? []];
8198
+ if (fixedProducts.length > 0) {
8199
+ const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.Fixed, fixedProducts, productLookup);
8200
+ if (o) out.push(o);
8201
+ }
8202
+ return out;
8203
+ }
8204
+ function buildOverride(vuln, publisher, tracking, docTime, canonical, products, productLookup) {
8205
+ const target = importTargetFor(canonical);
8206
+ if (!target) return void 0;
8207
+ const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$1);
8208
+ const override = {
8209
+ type: target.overrideType,
8210
+ requirementId: vuln.cve,
8211
+ appliedAt: docTime,
8212
+ expiresAt,
8213
+ appliedBy: identityFor(publisher),
8214
+ reason: buildReason$1(vuln, products)
8215
+ };
8216
+ const affectedPackages = resolveAffectedPackages(products, productLookup);
8217
+ if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
8218
+ if (target.status !== void 0) override.status = target.status;
8219
+ if (target.setJustification) {
8220
+ const j = pickJustification(vuln, products);
8221
+ if (j) override.justification = j;
8222
+ }
8223
+ const evidence = [];
8224
+ const advisoryEv = supplierEvidence(advisoryURI(publisher, tracking), "CSAF VEX advisory");
8225
+ if (advisoryEv) evidence.push(advisoryEv);
8226
+ for (const r of vuln.references ?? []) {
8227
+ if (!r.url) continue;
8228
+ const ev = supplierEvidence(r.url, r.summary ?? r.category ?? "");
8229
+ if (ev) evidence.push(ev);
8230
+ }
8231
+ if (evidence.length > 0) override.evidence = evidence;
8232
+ if (target.overrideType === OverrideType.Poam) override.milestones = [{
8233
+ description: firstActionRemediation(vuln, products) || target.poamActionTemplate,
8234
+ status: MilestoneStatus.Pending,
8235
+ estimatedCompletion: expiresAt
8236
+ }];
8237
+ return override;
8238
+ }
8239
+ function pickJustification(vuln, products) {
8240
+ const scope = new Set(products);
8241
+ for (const f of vuln.flags ?? []) {
8242
+ if (!overlap(f.product_ids ?? [], scope)) continue;
8243
+ if (!f.label) continue;
8244
+ const j = normalizeJustification(f.label);
8245
+ if (j) return j;
8246
+ }
8247
+ }
8248
+ function firstActionRemediation(vuln, products) {
8249
+ const scope = new Set(products);
8250
+ for (const r of vuln.remediations ?? []) {
8251
+ const ids = r.product_ids;
8252
+ if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
8253
+ if ((r.category === "vendor_fix" || r.category === "mitigation" || r.category === "workaround") && r.details) return r.details;
8254
+ }
8255
+ return "";
8256
+ }
8257
+ function buildReason$1(vuln, products) {
8258
+ const parts = [];
8259
+ const scope = new Set(products);
8260
+ for (const n of vuln.notes ?? []) if (n.category === "description" && n.text) parts.push(n.text);
8261
+ for (const t of vuln.threats ?? []) {
8262
+ if (!t.details) continue;
8263
+ const ids = t.product_ids;
8264
+ if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
8265
+ parts.push(t.details);
8266
+ }
8267
+ if (parts.length === 0) return `Imported from CSAF VEX (${vuln.cve ?? "unknown CVE"})`;
8268
+ return parts.join("\n");
8269
+ }
8270
+ /**
8271
+ * Walk a CSAF product_tree and build a lookup from product_id to a
8272
+ * structured AffectedPackage. Prefers product_identification_helper.purl,
8273
+ * then .cpe, then the full_product_name's `name` (used for matching only
8274
+ * — name+version is hard to derive from CSAF reliably, so we drop entries
8275
+ * with no purl and no cpe).
8276
+ */
8277
+ function buildProductLookup$1(tree) {
8278
+ const lookup = /* @__PURE__ */ new Map();
8279
+ if (!tree) return lookup;
8280
+ if (tree.full_product_names) for (const fp of tree.full_product_names) registerProduct(fp, lookup);
8281
+ if (tree.branches) walkBranches(tree.branches, lookup);
8282
+ return lookup;
8283
+ }
8284
+ function walkBranches(branches, lookup, ctx = {}) {
8285
+ for (const b of branches) {
8286
+ const next = { ...ctx };
8287
+ /* c8 ignore next 2 — falsy branches require a malformed CSAF branch
8288
+ node (category set but name empty) which we don't fixture. */
8289
+ if (b.category === "product_name" && b.name) next.productName = b.name;
8290
+ if (b.category === "product_version" && b.name) next.version = b.name;
8291
+ if (b.product) registerProduct(b.product, lookup, next);
8292
+ if (b.branches) walkBranches(b.branches, lookup, next);
8293
+ }
8294
+ }
8295
+ function registerProduct(fp, lookup, ctx = {}) {
8296
+ /* c8 ignore next — defensive against a malformed CSAF full_product_name
8297
+ missing its required product_id field; not fixtured. */
8298
+ if (!fp.product_id) return;
8299
+ const helper = fp.product_identification_helper;
8300
+ if (helper?.purl) {
8301
+ const pkg = affectedPackageFromIdentifier(helper.purl);
8302
+ /* c8 ignore next 4 — affectedPackageFromIdentifier always returns a
8303
+ non-null value for an input that starts with 'pkg:' (preserves
8304
+ malformed input as purl-only). The null branch is unreachable from
8305
+ this callsite. */
8306
+ if (pkg) {
8307
+ lookup.set(fp.product_id, pkg);
8308
+ return;
8309
+ }
8310
+ }
8311
+ if (helper?.cpe) {
8312
+ const pkg = affectedPackageFromIdentifier(helper.cpe);
8313
+ /* c8 ignore next 4 — same as above; identifiers starting with
8314
+ 'cpe:2.3:' always produce a cpe-only AffectedPackage. */
8315
+ if (pkg) {
8316
+ lookup.set(fp.product_id, pkg);
8317
+ return;
8318
+ }
8319
+ }
8320
+ if (ctx.productName && ctx.version) lookup.set(fp.product_id, {
8321
+ name: ctx.productName,
8322
+ version: ctx.version,
8323
+ ecosystem: Ecosystem.Generic
8324
+ });
8325
+ }
8326
+ function resolveAffectedPackages(productIds, lookup) {
8327
+ const out = [];
8328
+ const seenKeys = /* @__PURE__ */ new Set();
8329
+ for (const id of productIds) {
8330
+ const pkg = lookup.get(id);
8331
+ if (!pkg) continue;
8332
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
8333
+ if (seenKeys.has(key)) continue;
8334
+ seenKeys.add(key);
8335
+ out.push(pkg);
8336
+ }
8337
+ return out;
8338
+ }
8339
+ function identityFor(p) {
8340
+ if (!p.name) return {
8341
+ type: IdentityType.System,
8342
+ identifier: "csaf-vex-import"
8343
+ };
8344
+ const id = {
8345
+ type: IdentityType.Simple,
8346
+ identifier: p.name
8347
+ };
8348
+ if (p.category) id.description = p.category;
8349
+ return id;
8350
+ }
8351
+ function advisoryURI(publisher, tracking) {
8352
+ /* c8 ignore next */
8353
+ const id = tracking.id ?? "";
8354
+ const ns = publisher.namespace;
8355
+ if (ns && id) return `${ns.replace(/\/+$/, "")}/${id}`;
8356
+ return id;
8357
+ }
8358
+ function overlap(ids, scope) {
8359
+ return ids.some((id) => scope.has(id));
8360
+ }
8361
+ //#endregion
8362
+ //#region converters/cyclonedx-vex-to-hdf/typescript/converter.ts
8363
+ /**
8364
+ * CycloneDX VEX to HDF Amendments converter.
8365
+ *
8366
+ * CycloneDX VEX is not a separate format — it's a CycloneDX BOM whose
8367
+ * vulnerabilities[] carry an `analysis` object. CycloneDX-specific
8368
+ * justifications without an HDF equivalent (requires_configuration,
8369
+ * protected_by_compiler, etc.) are preserved verbatim in the reason
8370
+ * field via the shared helper's unknown-value passthrough.
8371
+ *
8372
+ * Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
8373
+ */
8374
+ const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
8375
+ async function convertCyclonedxVexToHdf(input, converterVersion) {
8376
+ validateInputSize(input, "cyclonedx-vex-to-hdf");
8377
+ const bom = parseJSON(input);
8378
+ if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
8379
+ const docTime = (bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) : null) ?? /* @__PURE__ */ new Date();
8380
+ const productLookup = buildProductLookup(bom);
8381
+ const publisher = publisherIdentityOrDefault(bom);
8382
+ const overrides = [];
8383
+ for (const v of bom.vulnerabilities ?? []) {
8384
+ const o = vulnerabilityToOverride(v, productLookup, docTime, publisher);
8385
+ if (o) overrides.push(o);
8386
+ }
8387
+ 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");
8388
+ const publisherName = publisher.identifier;
8389
+ return {
8390
+ name: publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements",
8391
+ description: bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX",
8392
+ overrides,
8393
+ appliedBy: publisher,
8394
+ generator: {
8395
+ name: "cyclonedx-vex-to-hdf",
8396
+ version: converterVersion
8397
+ },
8398
+ integrity: await inputIntegrity(input)
8399
+ };
8400
+ }
8401
+ function vulnerabilityToOverride(v, productLookup, docTime, publisher) {
8402
+ if (!v.id || !v.analysis?.state) return void 0;
8403
+ const canonical = normalizeStatus(v.analysis.state);
8404
+ if (!canonical) return void 0;
8405
+ const target = importTargetFor(canonical);
8406
+ if (!target) return void 0;
8407
+ const affectedPackages = affectedPackagesForVuln(v, productLookup);
8408
+ const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS);
8409
+ const override = {
8410
+ type: target.overrideType,
8411
+ requirementId: v.id,
8412
+ appliedAt: docTime,
8413
+ expiresAt,
8414
+ appliedBy: publisher,
8415
+ reason: buildReason(v)
8416
+ };
8417
+ if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
8418
+ if (target.status !== void 0) override.status = target.status;
8419
+ if (target.setJustification && v.analysis.justification) {
8420
+ const j = normalizeJustification(v.analysis.justification);
8421
+ if (j) override.justification = j;
8422
+ }
8423
+ const evidence = [];
8424
+ if (v.source?.url) {
8425
+ const ev = supplierEvidence(v.source.url, v.source.name ?? "");
8426
+ if (ev) evidence.push(ev);
8427
+ }
8428
+ for (const r of v.references ?? []) {
8429
+ if (!r.source?.url) continue;
8430
+ const ev = supplierEvidence(r.source.url, r.source.name ?? "");
8431
+ if (ev) evidence.push(ev);
8432
+ }
8433
+ if (evidence.length > 0) override.evidence = evidence;
8434
+ if (target.overrideType === OverrideType.Poam) override.milestones = [{
8435
+ description: firstActionFromResponse(v.analysis.response ?? []) || target.poamActionTemplate,
8436
+ status: MilestoneStatus.Pending,
8437
+ estimatedCompletion: expiresAt
8438
+ }];
8439
+ return override;
8440
+ }
8441
+ function buildReason(v) {
8442
+ const parts = [];
8443
+ if (v.description) parts.push(v.description);
8444
+ if (v.analysis?.detail) parts.push(v.analysis.detail);
8445
+ return parts.join("\n");
8446
+ }
8447
+ /**
8448
+ * Resolve CycloneDX affects[].ref entries to structured AffectedPackage
8449
+ * entries. Looks up the bom-ref in the component table to recover purl,
8450
+ * name/version. Opaque bom-refs with no component-table match are dropped
8451
+ * (the schema forbids fabricating name+version, and bom-refs aren't
8452
+ * portable identifiers outside their source BOM).
8453
+ */
8454
+ function affectedPackagesForVuln(v, lookup) {
8455
+ const out = [];
8456
+ const seenKeys = /* @__PURE__ */ new Set();
8457
+ for (const a of v.affects ?? []) {
8458
+ if (!a.ref) continue;
8459
+ const comp = lookup.get(a.ref);
8460
+ const pkg = comp ? affectedPackageFromComponent(comp) : affectedPackageFromIdentifier(a.ref);
8461
+ if (!pkg) continue;
8462
+ const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
8463
+ if (seenKeys.has(key)) continue;
8464
+ seenKeys.add(key);
8465
+ out.push(pkg);
8466
+ }
8467
+ return out;
8468
+ }
8469
+ /**
8470
+ * Build an AffectedPackage from a CycloneDX Component. Prefers structured
8471
+ * purl decomposition via the shared helper (so name/version/ecosystem are
8472
+ * also populated when the purl is parseable); falls back to the component's
8473
+ * name+version when no purl is set. Returns undefined when the component
8474
+ * carries neither a purl nor a name+version pair (schema requires at least
8475
+ * name+version+ecosystem, purl, or cpe).
8476
+ */
8477
+ function affectedPackageFromComponent(c) {
8478
+ if (c.purl) return affectedPackageFromIdentifier(c.purl);
8479
+ if (c.name && c.version) return {
8480
+ name: c.name,
8481
+ version: c.version,
8482
+ ecosystem: Ecosystem.Generic
8483
+ };
8484
+ }
8485
+ function buildProductLookup(bom) {
8486
+ const lookup = /* @__PURE__ */ new Map();
8487
+ const root = bom.metadata?.component;
8488
+ if (root?.["bom-ref"]) lookup.set(root["bom-ref"], root);
8489
+ for (const c of bom.components ?? []) if (c["bom-ref"]) lookup.set(c["bom-ref"], c);
8490
+ return lookup;
8491
+ }
8492
+ function firstActionFromResponse(resp) {
8493
+ for (const r of resp) switch (r.trim().toLowerCase()) {
8494
+ case "update": return "Apply vendor update and re-scan to verify.";
8495
+ case "rollback": return "Roll back to the unaffected version and re-scan to verify.";
8496
+ case "workaround_available": return "Apply the documented workaround.";
8497
+ }
8498
+ return "";
8499
+ }
8500
+ function publisherIdentityOrDefault(bom) {
8501
+ for (const a of bom.metadata?.authors ?? []) {
8502
+ if (a.email) return {
8503
+ type: IdentityType.Email,
8504
+ identifier: a.email
8505
+ };
8506
+ if (a.name) return {
8507
+ type: IdentityType.Simple,
8508
+ identifier: a.name
8509
+ };
8510
+ }
8511
+ for (const t of bom.metadata?.tools ?? []) {
8512
+ const ident = [t.vendor, t.name].filter(Boolean).join(" ").trim();
8513
+ if (ident) return {
8514
+ type: IdentityType.System,
8515
+ identifier: ident
8516
+ };
8517
+ }
8518
+ return {
8519
+ type: IdentityType.System,
8520
+ identifier: "cyclonedx-vex-import"
8521
+ };
8522
+ }
8523
+ //#endregion
8524
+ //#region converters/hdf-to-csaf-vex/typescript/converter.ts
8525
+ /**
8526
+ * HDF Amendments to CSAF VEX (csaf_vex profile) converter.
8527
+ *
8528
+ * Reverse direction of csaf-vex-to-hdf. Intentionally partial-fidelity:
8529
+ * fields the shared VEX mapping considers consumer-action-bearing survive
8530
+ * round-trip; the rest collapse into the available CSAF fields.
8531
+ */
8532
+ const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
8533
+ const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
8534
+ const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
8535
+ function convertHdfToCsafVex(input, converterVersion) {
8536
+ validateInputSize(input, "hdf-to-csaf-vex");
8537
+ const amendments = parseJSON(input);
8538
+ const groups = groupByCVE(amendments.overrides ?? []);
8539
+ const productSet = /* @__PURE__ */ new Map();
8540
+ const vulnerabilities = [];
8541
+ for (const group of groups) {
8542
+ const v = buildVulnerability(group);
8543
+ if (!v) continue;
8544
+ vulnerabilities.push(v);
8545
+ for (const p of productIDsForGroup(group)) productSet.set(p, true);
8546
+ }
8547
+ if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
8548
+ const products = [...productSet.keys()].sort();
8549
+ const doc = buildDocument(amendments, converterVersion);
8550
+ doc.vulnerabilities = vulnerabilities;
8551
+ doc.product_tree.full_product_names = products.length > 0 ? products.map((p) => ({
8552
+ name: p,
8553
+ product_id: p
8554
+ })) : [{
8555
+ name: DEFAULT_PRODUCT_ID$2,
8556
+ product_id: DEFAULT_PRODUCT_ID$2
8557
+ }];
8558
+ return JSON.stringify(doc, null, 2);
8559
+ }
8560
+ function groupByCVE(overrides) {
8561
+ const groups = /* @__PURE__ */ new Map();
8562
+ for (const o of overrides) {
8563
+ if (!CVE_ID_PATTERN$2.test(o.requirementId)) continue;
8564
+ let g = groups.get(o.requirementId);
8565
+ if (!g) {
8566
+ g = {
8567
+ cve: o.requirementId,
8568
+ overrides: []
8569
+ };
8570
+ groups.set(o.requirementId, g);
8571
+ }
8572
+ g.overrides.push(o);
8573
+ }
8574
+ return [...groups.values()].sort((a, b) => a.cve.localeCompare(b.cve));
8575
+ }
8576
+ function productIDsForGroup(group) {
8577
+ const seen = /* @__PURE__ */ new Set();
8578
+ for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
8579
+ return [...seen].sort();
8580
+ }
8581
+ function productIDsFor$1(o) {
8582
+ if (o.affectedPackages && o.affectedPackages.length > 0) {
8583
+ const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
8584
+ if (ids.length > 0) return ids;
8585
+ }
8586
+ if (o.componentRef) return [o.componentRef];
8587
+ const m = PRODUCTS_LINE$2.exec(o.reason ?? "");
8588
+ if (m && m[1]) {
8589
+ const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
8590
+ if (parts.length > 0) return parts;
8591
+ }
8592
+ return [DEFAULT_PRODUCT_ID$2];
8593
+ }
8594
+ function stripProductsLine$1(reason) {
8595
+ return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
8596
+ }
8597
+ function allMilestonesCompleted$2(o) {
8598
+ if (!o.milestones || o.milestones.length === 0) return false;
8599
+ return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
8600
+ }
8601
+ function buildVulnerability(group) {
8602
+ const v = { cve: group.cve };
8603
+ const status = {};
8604
+ let emitted = false;
8605
+ for (const o of group.overrides) {
8606
+ const pids = productIDsFor$1(o);
8607
+ let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
8608
+ if (!canonical) continue;
8609
+ if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
8610
+ if (canonical === VexStatus.NotAffected) {
8611
+ status.known_not_affected = (status.known_not_affected ?? []).concat(pids);
8612
+ if (o.justification) {
8613
+ v.flags = v.flags ?? [];
8614
+ v.flags.push({
8615
+ label: String(o.justification),
8616
+ product_ids: pids,
8617
+ date: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z")
8618
+ });
8619
+ }
8620
+ emitted = true;
8621
+ } else if (canonical === VexStatus.Fixed) {
8622
+ status.fixed = (status.fixed ?? []).concat(pids);
8623
+ for (const m of o.milestones ?? []) {
8624
+ if (!m.description) continue;
8625
+ v.remediations = v.remediations ?? [];
8626
+ v.remediations.push({
8627
+ category: "vendor_fix",
8628
+ details: m.description,
8629
+ product_ids: pids
8630
+ });
8631
+ }
8632
+ emitted = true;
8633
+ } else if (canonical === VexStatus.Affected) {
8634
+ status.known_affected = (status.known_affected ?? []).concat(pids);
8635
+ if (o.reason) {
8636
+ v.threats = v.threats ?? [];
8637
+ v.threats.push({
8638
+ category: "impact",
8639
+ details: stripProductsLine$1(o.reason),
8640
+ product_ids: pids
8641
+ });
8642
+ }
8643
+ if (o.type === OverrideType.Poam) for (const m of o.milestones ?? []) {
8644
+ if (!m.description) continue;
8645
+ v.remediations = v.remediations ?? [];
8646
+ v.remediations.push({
8647
+ category: "workaround",
8648
+ details: m.description,
8649
+ product_ids: pids
8650
+ });
8651
+ }
8652
+ emitted = true;
8653
+ }
8654
+ for (const e of o.evidence ?? []) {
8655
+ if (e.type !== "url" || !e.data) continue;
8656
+ v.references = v.references ?? [];
8657
+ v.references.push({
8658
+ category: "external",
8659
+ summary: e.description ?? "",
8660
+ url: e.data
8661
+ });
8662
+ }
8663
+ }
8664
+ /* c8 ignore next */
8665
+ if (!emitted) return void 0;
8666
+ if (status.fixed) status.fixed = [...new Set(status.fixed)];
8667
+ if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
8668
+ if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
8669
+ v.product_status = status;
8670
+ if (v.references) {
8671
+ const seen = /* @__PURE__ */ new Set();
8672
+ v.references = v.references.filter((r) => {
8673
+ if (seen.has(r.url)) return false;
8674
+ seen.add(r.url);
8675
+ return true;
8676
+ });
8677
+ }
8678
+ return v;
8679
+ }
8680
+ function buildDocument(amendments, converterVersion) {
8681
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
8682
+ const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
8683
+ const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
8684
+ const docVersion = amendments.version || "1";
8685
+ const title = amendments.name || "HDF Amendments exported as CSAF VEX";
8686
+ const notes = [];
8687
+ if (amendments.description) notes.push({
8688
+ category: "summary",
8689
+ text: amendments.description,
8690
+ title: "Description"
8691
+ });
8692
+ return {
8693
+ document: {
8694
+ category: "csaf_vex",
8695
+ csaf_version: "2.0",
8696
+ title,
8697
+ notes,
8698
+ publisher: {
8699
+ category: "other",
8700
+ name: publisherName
8701
+ },
8702
+ tracking: {
8703
+ id: trackingId,
8704
+ status: "final",
8705
+ version: docVersion,
8706
+ current_release_date: now,
8707
+ initial_release_date: now,
8708
+ revision_history: [{
8709
+ date: now,
8710
+ number: docVersion,
8711
+ summary: "Generated by hdf-to-csaf-vex from HDF Amendments."
8712
+ }],
8713
+ generator: {
8714
+ engine: {
8715
+ name: "hdf-to-csaf-vex",
8716
+ version: converterVersion
8717
+ },
8718
+ date: now
8719
+ }
8720
+ }
8721
+ },
8722
+ product_tree: { full_product_names: [] },
8723
+ vulnerabilities: []
8724
+ };
8725
+ }
8726
+ //#endregion
8727
+ //#region converters/hdf-to-openvex/typescript/converter.ts
8728
+ /**
8729
+ * HDF Amendments to OpenVEX converter.
8730
+ *
8731
+ * Reverse direction of openvex-to-hdf. Step 4f amendment-output pattern,
8732
+ * partial-fidelity by design — consumer-action-bearing fields (CVE id,
8733
+ * status, justification) survive round-trip; the rest collapse.
8734
+ */
8735
+ const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
8736
+ const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
8737
+ const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
8738
+ const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
8739
+ const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
8740
+ async function convertHdfToOpenVex(input, converterVersion) {
8741
+ validateInputSize(input, "hdf-to-openvex");
8742
+ const amendments = parseJSON(input);
8743
+ const statements = [];
8744
+ let earliest;
8745
+ for (const o of amendments.overrides ?? []) {
8746
+ const s = overrideToStatement(o);
8747
+ if (!s) continue;
8748
+ statements.push(s);
8749
+ const t = new Date(o.appliedAt);
8750
+ if (!earliest || t < earliest) earliest = t;
8751
+ }
8752
+ if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
8753
+ statements.sort((a, b) => a.vulnerability.name.localeCompare(b.vulnerability.name));
8754
+ const author = amendments.appliedBy?.identifier || "HDF Amendments Export";
8755
+ const role = amendments.appliedBy?.description;
8756
+ const doc = {
8757
+ "@context": OPENVEX_CONTEXT,
8758
+ "@id": await buildDocumentID(input, amendments),
8759
+ author,
8760
+ role,
8761
+ timestamp: (earliest ?? /* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z"),
8762
+ version: 1,
8763
+ statements
8764
+ };
8765
+ return JSON.stringify(doc, null, 2);
8766
+ }
8767
+ function overrideToStatement(o) {
8768
+ if (!CVE_ID_PATTERN$1.test(o.requirementId)) return void 0;
8769
+ let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
8770
+ if (!canonical) return void 0;
8771
+ if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
8772
+ const stmt = {
8773
+ vulnerability: {
8774
+ name: o.requirementId,
8775
+ "@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
8776
+ },
8777
+ status: String(canonical),
8778
+ timestamp: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z"),
8779
+ products: productsFor(o)
8780
+ };
8781
+ if (canonical === VexStatus.NotAffected) {
8782
+ if (o.justification) stmt.justification = String(o.justification);
8783
+ const impact = stripProductsLine(o.reason ?? "");
8784
+ if (impact) stmt.impact_statement = impact;
8785
+ } else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
8786
+ else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
8787
+ return stmt;
8788
+ }
8789
+ function productsFor(o) {
8790
+ if (o.affectedPackages && o.affectedPackages.length > 0) {
8791
+ const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
8792
+ if (ids.length > 0) return ids.map((id) => ({ "@id": id }));
8793
+ }
8794
+ let ids = [];
8795
+ if (o.componentRef) ids = [o.componentRef];
8796
+ else {
8797
+ const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
8798
+ if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
8799
+ }
8800
+ if (ids.length === 0) ids = [DEFAULT_PRODUCT_ID$1];
8801
+ return ids.map((id) => ({ "@id": id }));
8802
+ }
8803
+ function firstMilestoneAction(o) {
8804
+ for (const m of o.milestones ?? []) if (m.description) return m.description;
8805
+ return "";
8806
+ }
8807
+ function allMilestonesCompleted$1(o) {
8808
+ if (!o.milestones || o.milestones.length === 0) return false;
8809
+ return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
8810
+ }
8811
+ function stripProductsLine(reason) {
8812
+ return reason.replace(PRODUCTS_LINE$1, "").replace(/\n+$/, "");
8813
+ }
8814
+ async function buildDocumentID(input, a) {
8815
+ if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
8816
+ return `${OPENVEX_NAMESPACE}vex-${await sha256(input)}`;
8817
+ }
8818
+ //#endregion
8819
+ //#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
8820
+ /**
8821
+ * HDF Amendments to CycloneDX VEX converter (export side).
8822
+ *
8823
+ * Reverse direction of cyclonedx-vex-to-hdf. Step 4f amendment-output
8824
+ * pattern, partial-fidelity by design — consumer-action-bearing fields
8825
+ * (CVE id, status, justification) survive round-trip; the rest collapse
8826
+ * into the available CycloneDX VEX fields.
8827
+ */
8828
+ const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
8829
+ const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
8830
+ const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
8831
+ const RESPONSE_LINE = /^Response:.*$/gm;
8832
+ const DEFAULT_PRODUCT_ID = "HDFPID-0001";
8833
+ function convertHdfToCyclonedxVex(input, converterVersion) {
8834
+ validateInputSize(input, "hdf-to-cyclonedx-vex");
8835
+ const amendments = parseJSON(input);
8836
+ const componentRegistry = /* @__PURE__ */ new Map();
8837
+ const vulnerabilities = [];
8838
+ let earliest;
8839
+ for (const o of amendments.overrides ?? []) {
8840
+ if (!CVE_ID_PATTERN.test(o.requirementId)) continue;
8841
+ const v = overrideToVulnerability(o, componentRegistry);
8842
+ if (!v) continue;
8843
+ vulnerabilities.push(v);
8844
+ const t = new Date(o.appliedAt);
8845
+ if (!earliest || t < earliest) earliest = t;
8846
+ }
8847
+ if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
8848
+ vulnerabilities.sort((a, b) => a.id.localeCompare(b.id));
8849
+ const components = [...componentRegistry.values()].sort((a, b) => a["bom-ref"].localeCompare(b["bom-ref"]));
8850
+ const bom = {
8851
+ bomFormat: "CycloneDX",
8852
+ specVersion: "1.4",
8853
+ serialNumber: buildSerialNumberSync(input, amendments),
8854
+ version: 1,
8855
+ metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
8856
+ components,
8857
+ vulnerabilities
8858
+ };
8859
+ return JSON.stringify(bom, null, 2);
8860
+ }
8861
+ function overrideToVulnerability(o, componentRegistry) {
8862
+ let canonical = exportStatusFor(o, allMilestonesCompleted(o), false);
8863
+ if (!canonical) return void 0;
8864
+ if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted(o)) canonical = VexStatus.Fixed;
8865
+ const pids = productIDsFor(o);
8866
+ const pkgById = /* @__PURE__ */ new Map();
8867
+ for (const p of o.affectedPackages ?? []) {
8868
+ const id = affectedPackageToIdentifier(p);
8869
+ if (id) pkgById.set(id, p);
8870
+ }
8871
+ for (const pid of pids) componentRegistry.set(pid, componentFor(pid, pkgById.get(pid)));
8872
+ const analysis = { state: canonicalToCycloneDXState(canonical) };
8873
+ if (o.justification) {
8874
+ const cdxJust = justificationForCycloneDX(o.justification);
8875
+ if (cdxJust) analysis.justification = cdxJust;
8876
+ }
8877
+ const detail = stripReasonAnnotations(o.reason ?? "");
8878
+ if (detail) analysis.detail = detail;
8879
+ if (canonical === VexStatus.Fixed) analysis.response = ["update"];
8880
+ else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
8881
+ const v = {
8882
+ id: o.requirementId,
8883
+ source: {
8884
+ name: "NVD",
8885
+ url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
8886
+ },
8887
+ analysis,
8888
+ affects: pids.map((p) => ({ ref: p }))
8889
+ };
8890
+ for (const e of o.evidence ?? []) {
8891
+ if (e.type !== "url" || !e.data) continue;
8892
+ v.references = v.references ?? [];
8893
+ v.references.push({
8894
+ id: o.requirementId,
8895
+ source: {
8896
+ name: e.description ?? "",
8897
+ url: e.data
8898
+ }
8899
+ });
8900
+ }
8901
+ return v;
8902
+ }
8903
+ function canonicalToCycloneDXState(canonical) {
8904
+ switch (canonical) {
8905
+ case VexStatus.NotAffected: return "not_affected";
8906
+ case VexStatus.Fixed: return "resolved";
8907
+ case VexStatus.Affected: return "exploitable";
8908
+ /* c8 ignore next 2 — defensive: every VexStatus has a case above */
8909
+ default: return canonical;
8910
+ }
8911
+ }
8912
+ function componentFor(pid, pkg) {
8913
+ const c = {
8914
+ type: "application",
8915
+ name: pkg?.name ?? pid,
8916
+ "bom-ref": pid
8917
+ };
8918
+ if (pkg?.version) c.version = pkg.version;
8919
+ if (pkg?.purl ?? (pid.startsWith("pkg:") ? pid : void 0)) c.purl = pkg?.purl ?? pid;
8920
+ if (pkg?.cpe ?? (pid.startsWith("cpe:2.3:") ? pid : void 0)) c.cpe = pkg?.cpe ?? pid;
8921
+ return c;
8922
+ }
8923
+ function productIDsFor(o) {
8924
+ if (o.affectedPackages && o.affectedPackages.length > 0) {
8925
+ const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
8926
+ if (ids.length > 0) return ids;
8927
+ }
8928
+ if (o.componentRef) return [o.componentRef];
8929
+ const m = PRODUCTS_LINE.exec(o.reason ?? "");
8930
+ if (m && m[1]) {
8931
+ const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
8932
+ if (parts.length > 0) return parts;
8933
+ }
8934
+ return [DEFAULT_PRODUCT_ID];
8935
+ }
8936
+ function stripReasonAnnotations(reason) {
8937
+ return reason.replace(PRODUCTS_LINE, "").replace(RAW_JUST_LINE, "").replace(RESPONSE_LINE, "").trim();
8938
+ }
8939
+ function allMilestonesCompleted(o) {
8940
+ if (!o.milestones || o.milestones.length === 0) return false;
8941
+ return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
8942
+ }
8943
+ function buildMetadata(a, docTime, converterVersion) {
8944
+ const metadata = {
8945
+ timestamp: docTime.toISOString().replace(/\.\d+Z$/, "Z"),
8946
+ tools: [{
8947
+ vendor: "mitre",
8948
+ name: "hdf-to-cyclonedx-vex",
8949
+ version: converterVersion
8950
+ }]
8951
+ };
8952
+ if (a.appliedBy?.identifier) {
8953
+ const author = {};
8954
+ if (a.appliedBy.type === IdentityType.Email) author.email = a.appliedBy.identifier;
8955
+ else author.name = a.appliedBy.identifier;
8956
+ metadata.authors = [author];
8957
+ }
8958
+ return metadata;
8959
+ }
8960
+ function buildSerialNumberSync(input, a) {
8961
+ if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
8962
+ return `urn:uuid:${fnv1a(input)}`;
8963
+ }
8964
+ function fnv1a(s) {
8965
+ let h = 2166136261;
8966
+ for (let i = 0; i < s.length; i++) {
8967
+ h ^= s.charCodeAt(i);
8968
+ h = Math.imul(h, 16777619) >>> 0;
8969
+ }
8970
+ const d = h.toString(16).padStart(8, "0");
8971
+ return (d + d + d + d).slice(0, 32);
8972
+ }
8973
+ //#endregion
5988
8974
  //#region converters/oscal-to-hdf/typescript/detect.ts
5989
8975
  /**
5990
8976
  * OSCAL document type detection.
@@ -6073,7 +9059,7 @@ async function catalogToBaseline(catalog, rawInput) {
6073
9059
  requirements,
6074
9060
  groups,
6075
9061
  generator: {
6076
- name: "hdf-converters",
9062
+ name: "oscal-catalog-to-hdf",
6077
9063
  version: "1.0.0"
6078
9064
  }
6079
9065
  };
@@ -6083,13 +9069,17 @@ function controlToBaselineRequirement(ctrl) {
6083
9069
  const nistTag = controlIdToNistTag(ctrl.id);
6084
9070
  const descriptions = buildCatalogDescriptions(ctrl);
6085
9071
  const tags = buildCatalogTags(ctrl);
6086
- return {
9072
+ const req = {
6087
9073
  id: nistTag,
6088
9074
  title: ctrl.title,
6089
9075
  impact: .5,
6090
9076
  descriptions,
6091
9077
  tags
6092
9078
  };
9079
+ const controlType = deriveControlTypeFromTags([nistTag]);
9080
+ if (controlType !== void 0) req.controlType = controlType;
9081
+ if (extractPropValue(ctrl.props, "CORE") === "true") req.applicability = Applicability.Required;
9082
+ return req;
6093
9083
  }
6094
9084
  /** Creates HDF Description entries from control parts. */
6095
9085
  function buildCatalogDescriptions(ctrl) {
@@ -6312,7 +9302,7 @@ async function convertOscalComponentToHdf(input) {
6312
9302
  integrity,
6313
9303
  requirements,
6314
9304
  generator: {
6315
- name: "hdf-converters",
9305
+ name: "oscal-component-to-hdf",
6316
9306
  version: "1.0.0"
6317
9307
  }
6318
9308
  };
@@ -6373,7 +9363,7 @@ async function convertOscalSspToHdf(input) {
6373
9363
  integrity,
6374
9364
  components: [],
6375
9365
  generator: {
6376
- name: "hdf-converters",
9366
+ name: "oscal-ssp-to-hdf",
6377
9367
  version: "1.0.0"
6378
9368
  }
6379
9369
  };
@@ -6500,13 +9490,13 @@ function sspComponentToHDFComponent(sc, componentControls) {
6500
9490
  function mapOSCALComponentType(oscalType) {
6501
9491
  switch (oscalType.toLowerCase()) {
6502
9492
  case "software":
6503
- case "this-system": return BoundaryDescription.Application;
6504
- case "service": return BoundaryDescription.Application;
6505
- case "hardware": return BoundaryDescription.Host;
6506
- case "network": return BoundaryDescription.Network;
6507
- case "database": return BoundaryDescription.Database;
6508
- case "storage": return BoundaryDescription.Artifact;
6509
- default: return BoundaryDescription.Application;
9493
+ case "this-system": return TargetType.Application;
9494
+ case "service": return TargetType.Application;
9495
+ case "hardware": return TargetType.Host;
9496
+ case "network": return TargetType.Network;
9497
+ case "database": return TargetType.Database;
9498
+ case "storage": return TargetType.Artifact;
9499
+ default: return TargetType.Application;
6510
9500
  }
6511
9501
  }
6512
9502
  //#endregion
@@ -6543,7 +9533,7 @@ async function convertOscalSapToHdf(input) {
6543
9533
  type: planType,
6544
9534
  description,
6545
9535
  generator: {
6546
- name: "hdf-converters",
9536
+ name: "oscal-sap-to-hdf",
6547
9537
  version: "1.0.0"
6548
9538
  }
6549
9539
  };
@@ -6673,7 +9663,7 @@ async function convertOscalPoamToHdf(input) {
6673
9663
  version: meta.version,
6674
9664
  appliedBy,
6675
9665
  generator: {
6676
- name: "hdf-converters",
9666
+ name: "oscal-poam-to-hdf",
6677
9667
  version: "1.0.0"
6678
9668
  }
6679
9669
  };
@@ -6870,7 +9860,10 @@ function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, re
6870
9860
  const descriptions = sarBuildDescriptions(findings, obsMap);
6871
9861
  const results = [];
6872
9862
  for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result));
6873
- return createRequirement(nistTag, title, descriptions, impact, results, { tags: { nist: [nistTag] } });
9863
+ const req = createRequirement(nistTag, title, descriptions, impact, results, { tags: { nist: [nistTag] } });
9864
+ const controlType = deriveControlTypeFromTags([nistTag]);
9865
+ if (controlType !== void 0) req.controlType = controlType;
9866
+ return req;
6874
9867
  }
6875
9868
  function findingToRequirementResult(f, obsMap, riskMap, result) {
6876
9869
  const status = mapFindingStatus(f);
@@ -6983,6 +9976,6 @@ function sarBaselineName(result, sar) {
6983
9976
  return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
6984
9977
  }
6985
9978
  //#endregion
6986
- export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertConveyorToHdf, convertCyclonedxToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, 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 };
9979
+ 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 };
6987
9980
 
6988
9981
  //# sourceMappingURL=index.js.map