@mitre/hdf-converters 3.2.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-ik8sNfNf.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 { Applicability, AuthorizationStatus, BoundaryDescription, CategorizationLevel, ControlType, Copyright, HashAlgorithm, OverrideType, PlanType, ResultStatus, VerificationMethodEnum, VerificationMethodEnum as VerificationMethodEnum$1, 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)
@@ -333,7 +382,24 @@ function deriveControlTypeFromTags(tags) {
333
382
  */
334
383
  function deriveVerificationMethod(code) {
335
384
  if (code === void 0 || code === null || code === "") return void 0;
336
- return VerificationMethodEnum$1.Automated;
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
+ };
337
403
  }
338
404
  //#endregion
339
405
  //#region converters/legacyhdf-to-hdf/typescript/converter.ts
@@ -380,7 +446,7 @@ function impactToSeverity$2(impact) {
380
446
  * Normalize status values from v1.0 to v2.0 format.
381
447
  * Converts snake_case to camelCase.
382
448
  */
383
- function normalizeStatus(status) {
449
+ function normalizeStatus$1(status) {
384
450
  return {
385
451
  "passed": "passed",
386
452
  "failed": "failed",
@@ -426,7 +492,7 @@ function computeEffectiveStatus(impact, results) {
426
492
  * Transforms snake_case field names to camelCase.
427
493
  */
428
494
  function convertResult(v1Result) {
429
- const v2Result = { status: normalizeStatus(v1Result.status) };
495
+ const v2Result = { status: normalizeStatus$1(v1Result.status) };
430
496
  if (v1Result.code_desc !== void 0) v2Result.codeDesc = v1Result.code_desc;
431
497
  if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
432
498
  if (v1Result.start_time !== void 0) v2Result.startTime = v1Result.start_time;
@@ -470,7 +536,7 @@ function convertControl(v1Control) {
470
536
  if (v1Control.code !== void 0) v2Req.code = v1Control.code;
471
537
  if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
472
538
  if (v1Control.waiver_data !== void 0) v2Req.waiverData = v1Control.waiver_data;
473
- if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus(v1Control.status);
539
+ if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
474
540
  if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
475
541
  if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
476
542
  v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity$2(v1Control.impact);
@@ -682,18 +748,19 @@ async function convertSarifToHdf(input) {
682
748
  const { items: limitedRuns, truncated: truncatedRuns } = limitArray(sarif.runs);
683
749
  /* v8 ignore next -- truncation only triggers with >100K items */
684
750
  if (truncatedRuns) console.warn(`WARNING: Input truncated at ${limitedRuns.length} run items (original: ${sarif.runs.length})`);
751
+ const timestamp = /* @__PURE__ */ new Date();
685
752
  return buildHdfResults({
686
753
  generatorName: "sarif-to-hdf",
687
754
  converterVersion: "1.0.0",
688
755
  toolName: firstDriver?.name,
689
756
  toolVersion: firstDriver?.version,
690
757
  toolFormat: "SARIF",
691
- baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum)),
758
+ baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
692
759
  components: [],
693
- timestamp: /* @__PURE__ */ new Date()
760
+ timestamp
694
761
  });
695
762
  }
696
- function convertRun(run, version, resultsChecksum) {
763
+ function convertRun(run, version, resultsChecksum, timestamp) {
697
764
  const ruleMap = buildRuleMap(run);
698
765
  const { items: limitedResults, truncated: truncatedResults } = limitArray(run.results);
699
766
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -717,7 +784,14 @@ function convertRun(run, version, resultsChecksum) {
717
784
  const group = groupMap.get(ruleId);
718
785
  return convertResultGroup(ruleId, group.rule, group.results);
719
786
  });
720
- 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, {
721
795
  version,
722
796
  title: "Static Analysis Results Interchange Format",
723
797
  resultsChecksum
@@ -747,8 +821,42 @@ function convertResultGroup(ruleId, rule, sarifResults) {
747
821
  const controlType = deriveControlTypeFromTags(nistControls);
748
822
  if (controlType !== void 0) req.controlType = controlType;
749
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;
750
835
  return req;
751
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
+ });
859
+ }
752
860
  function resolveRuleLevel(rule, results) {
753
861
  if (rule?.defaultConfiguration?.level) return rule.defaultConfiguration.level;
754
862
  for (const r of results) if (!r.kind || r.kind === "fail") {
@@ -961,14 +1069,16 @@ async function convertJunitToHdf(input) {
961
1069
  if (!input || !input.trim()) throw new Error("Empty input");
962
1070
  validateInputSize(input, "junit");
963
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()));
964
1074
  return buildHdfResults({
965
- generatorName: "hdf-converters",
1075
+ generatorName: "junit-to-hdf",
966
1076
  converterVersion: CONVERTER_VERSION$2,
967
1077
  toolName: "JUnit XML",
968
1078
  toolFormat: "XML",
969
- baselines: [createMinimalBaseline(name, buildRequirements(suites), { resultsChecksum: await inputChecksum(input) })],
1079
+ baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
970
1080
  components: [{
971
- type: Copyright.Application,
1081
+ type: TargetType.Application,
972
1082
  name
973
1083
  }],
974
1084
  timestamp: /* @__PURE__ */ new Date()
@@ -1067,6 +1177,11 @@ function buildCodeDesc$9(tc) {
1067
1177
  if (tc.classname) return `${tc.classname} :: ${tc.name}`;
1068
1178
  return tc.name;
1069
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
+ }
1070
1185
  //#endregion
1071
1186
  //#region converters/xccdf-results-to-hdf/typescript/converter.ts
1072
1187
  const CONVERTER_VERSION$1 = "1.0.0";
@@ -1131,6 +1246,10 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1131
1246
  /* v8 ignore next -- truncation only triggers with >100K items */
1132
1247
  if (truncatedRR) console.warn(`WARNING: Input truncated at ${limitedRuleResults.length} rule-result items (original: ${ruleResults.length})`);
1133
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
+ }
1134
1253
  const resultsChecksum = await inputChecksum(rawInput);
1135
1254
  const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
1136
1255
  const components = buildTargets(testResult);
@@ -1144,7 +1263,7 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
1144
1263
  const hdf = {
1145
1264
  baselines: [baseline],
1146
1265
  generator: {
1147
- name: "hdf-converters",
1266
+ name: "xccdf-results-to-hdf",
1148
1267
  version: CONVERTER_VERSION$1
1149
1268
  },
1150
1269
  tool: {
@@ -1183,6 +1302,10 @@ async function convertArfCollection(arc, rawInput) {
1183
1302
  /* v8 ignore next -- truncation only triggers with >100K items */
1184
1303
  if (truncatedARFRR) console.warn(`WARNING: Input truncated at ${limitedARFRuleResults.length} rule-result items (original: ${ruleResults.length})`);
1185
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
+ }
1186
1309
  let baselineName = "";
1187
1310
  if (benchmark) baselineName = extractText(benchmark.title) || "";
1188
1311
  if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "ARF Report";
@@ -1203,7 +1326,7 @@ async function convertArfCollection(arc, rawInput) {
1203
1326
  const hdf = {
1204
1327
  baselines,
1205
1328
  generator: {
1206
- name: "hdf-converters",
1329
+ name: "xccdf-results-to-hdf",
1207
1330
  version: CONVERTER_VERSION$1
1208
1331
  },
1209
1332
  tool: {
@@ -1303,6 +1426,22 @@ function ruleResultToRequirement(rr, ruleIndex) {
1303
1426
  return req;
1304
1427
  }
1305
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";
1443
+ }
1444
+ /**
1306
1445
  * Build Component array from TestResult metadata.
1307
1446
  */
1308
1447
  function buildTargets(testResult) {
@@ -1311,7 +1450,7 @@ function buildTargets(testResult) {
1311
1450
  const addresses = testResult["target-address"] ?? [];
1312
1451
  const target = {
1313
1452
  name: targetName,
1314
- type: Copyright.Host,
1453
+ type: TargetType.Host,
1315
1454
  labels: {}
1316
1455
  };
1317
1456
  if (addresses.length > 0) target.ipAddress = addresses[0];
@@ -1766,11 +1905,11 @@ const CONVERTER_VERSION = "1.0.0";
1766
1905
  * applicability are omitted (the checklist format cannot substantiate them).
1767
1906
  * Original-format metadata is stashed in extensions/tags for round-trip.
1768
1907
  */
1769
- function checklistToHdf(cl, resultsChecksum) {
1908
+ function checklistToHdf(cl, resultsChecksum, generatorName) {
1770
1909
  const hdf = {
1771
1910
  baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
1772
1911
  generator: {
1773
- name: "hdf-converters",
1912
+ name: generatorName,
1774
1913
  version: CONVERTER_VERSION
1775
1914
  },
1776
1915
  tool: {
@@ -1835,7 +1974,7 @@ function assetToComponent(a) {
1835
1974
  if (!a.hostName && !a.hostIP && !a.hostFQDN) return void 0;
1836
1975
  const c = {
1837
1976
  name: a.hostName || a.hostFQDN || a.hostIP || "",
1838
- type: Copyright.Host
1977
+ type: TargetType.Host
1839
1978
  };
1840
1979
  if (a.hostIP) c.ipAddress = a.hostIP;
1841
1980
  if (a.hostFQDN) c.fqdn = a.hostFQDN;
@@ -2010,7 +2149,7 @@ async function convertCklToHdf(input) {
2010
2149
  validateInputSize(input, "ckl-to-hdf");
2011
2150
  const resultsChecksum = await inputChecksum(input);
2012
2151
  const checklist = parseCkl(input);
2013
- return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
2152
+ return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "ckl-to-hdf"), null, 2);
2014
2153
  }
2015
2154
  //#endregion
2016
2155
  //#region converters/cklb-to-hdf/typescript/converter.ts
@@ -2027,7 +2166,7 @@ async function convertCklbToHdf(input) {
2027
2166
  validateInputSize(input, "cklb-to-hdf");
2028
2167
  const resultsChecksum = await inputChecksum(input);
2029
2168
  const checklist = parseCklb(input);
2030
- return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
2169
+ return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "cklb-to-hdf"), null, 2);
2031
2170
  }
2032
2171
  //#endregion
2033
2172
  //#region converters/hdf-to-ckl/typescript/converter.ts
@@ -2078,7 +2217,28 @@ function formatDependencyPath(from) {
2078
2217
  /**
2079
2218
  * Builds a single EvaluatedRequirement from a group of vulnerabilities sharing an ID.
2080
2219
  */
2081
- function buildRequirement$16(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) {
2082
2242
  const rep = vulns[0];
2083
2243
  const cweIDs = rep.identifiers.CWE ?? [];
2084
2244
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
@@ -2098,6 +2258,19 @@ function buildRequirement$16(vulnID, vulns) {
2098
2258
  const controlType = deriveControlTypeFromTags(nist);
2099
2259
  if (controlType !== void 0) req.controlType = controlType;
2100
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
+ }
2101
2274
  return req;
2102
2275
  }
2103
2276
  /**
@@ -2114,7 +2287,11 @@ function convertSingleProject(report, resultsChecksum) {
2114
2287
  else groups.set(vuln.id, [vuln]);
2115
2288
  }
2116
2289
  const requirements = [];
2117
- for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(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
+ }
2118
2295
  return createMinimalBaseline("Snyk Scan", requirements, {
2119
2296
  resultsChecksum,
2120
2297
  title: `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`,
@@ -2159,7 +2336,7 @@ async function convertSnykToHdf(input) {
2159
2336
  baselines,
2160
2337
  components: [{
2161
2338
  name: targetName,
2162
- type: Copyright.Application
2339
+ type: TargetType.Application
2163
2340
  }],
2164
2341
  timestamp: /* @__PURE__ */ new Date()
2165
2342
  });
@@ -2227,6 +2404,92 @@ function buildCodeDesc$8(match) {
2227
2404
  }
2228
2405
  return parts.join(" | ");
2229
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
+ }
2230
2493
  function convertMatchToRequirement(match, isIgnored) {
2231
2494
  const vuln = match.vulnerability;
2232
2495
  const cveId = vuln.id;
@@ -2277,6 +2540,15 @@ function convertMatchToRequirement(match, isIgnored) {
2277
2540
  };
2278
2541
  const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
2279
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;
2280
2552
  return requirement;
2281
2553
  }
2282
2554
  async function convertGrypeToHdf(input) {
@@ -2297,6 +2569,7 @@ async function convertGrypeToHdf(input) {
2297
2569
  for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
2298
2570
  }
2299
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()));
2300
2573
  const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
2301
2574
  return buildHdfResults({
2302
2575
  generatorName: grypeData.descriptor?.name || "grype",
@@ -2305,7 +2578,7 @@ async function convertGrypeToHdf(input) {
2305
2578
  toolVersion: grypeData.descriptor?.version,
2306
2579
  baselines: [baseline],
2307
2580
  components: [{
2308
- type: Copyright.Artifact,
2581
+ type: TargetType.Artifact,
2309
2582
  name: targetName
2310
2583
  }],
2311
2584
  timestamp: grypeData.descriptor?.timestamp ? new Date(grypeData.descriptor.timestamp) : /* @__PURE__ */ new Date()
@@ -2313,6 +2586,8 @@ async function convertGrypeToHdf(input) {
2313
2586
  }
2314
2587
  //#endregion
2315
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;
2316
2591
  const converterVersion = "1.0.0";
2317
2592
  const IMPACT_MAPPING$7 = {
2318
2593
  "4": .9,
@@ -2340,7 +2615,9 @@ async function convertNessusToHdf(nessusXml) {
2340
2615
  "preference",
2341
2616
  "tag",
2342
2617
  "ReportItem",
2343
- "ReportHost"
2618
+ "ReportHost",
2619
+ "cwe",
2620
+ "cve"
2344
2621
  ]);
2345
2622
  const policyName = parsed.NessusClientData_v2.Policy.policyName;
2346
2623
  const version = extractVersion(parsed);
@@ -2362,7 +2639,7 @@ async function convertNessusToHdf(nessusXml) {
2362
2639
  components,
2363
2640
  statistics: { duration },
2364
2641
  generator: {
2365
- name: "hdf-converters",
2642
+ name: "nessus-to-hdf",
2366
2643
  version: converterVersion
2367
2644
  },
2368
2645
  tool: { name: "Nessus" },
@@ -2409,6 +2686,12 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
2409
2686
  if (truncatedItems) console.warn(`WARNING: Input truncated at ${limitedItems.length} ReportItem items (original: ${items.length})`);
2410
2687
  requirements = limitedItems.map((item) => convertReportItemToRequirement(item, host));
2411
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
+ }
2412
2695
  return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
2413
2696
  title: `Nessus ${policyName}`,
2414
2697
  version,
@@ -2442,8 +2725,141 @@ function convertReportItemToRequirement(item, host) {
2442
2725
  };
2443
2726
  if (controlType !== void 0) req.controlType = controlType;
2444
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
+ }
2445
2736
  return req;
2446
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;
2850
+ return {
2851
+ date,
2852
+ score: hasScore ? parseFloatSafe(item.epss_score) : 0,
2853
+ percentile: hasPct ? parseFloatSafe(item.epss_percentile) : 0
2854
+ };
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
+ }
2447
2863
  function buildDescriptions(item, isCompliance) {
2448
2864
  const descriptions = [];
2449
2865
  if (isCompliance && item["compliance-info"]) descriptions.push({
@@ -2504,7 +2920,7 @@ function buildTags$2(item, isCompliance) {
2504
2920
  }
2505
2921
  function buildRefs(item) {
2506
2922
  const refs = [];
2507
- 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 });
2508
2924
  return refs.length > 0 ? refs : void 0;
2509
2925
  }
2510
2926
  function buildResult$1(item, host, isCompliance) {
@@ -2546,7 +2962,7 @@ function convertReportHostToTarget(host) {
2546
2962
  });
2547
2963
  const target = {
2548
2964
  name: hostName,
2549
- type: Copyright.Host
2965
+ type: TargetType.Host
2550
2966
  };
2551
2967
  if (isFQDN(hostName)) target.fqdn = hostName;
2552
2968
  const hostIp = hostProps["host-ip"];
@@ -2614,18 +3030,40 @@ async function convertSonarqubeToHdf(input) {
2614
3030
  const baseline = convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum);
2615
3031
  baselines.push(baseline);
2616
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
+ }
2617
3050
  return buildHdfResults({
2618
3051
  generatorName: "sonarqube-to-hdf",
2619
3052
  converterVersion: "1.0.0",
2620
3053
  toolName: "SonarQube",
2621
3054
  baselines,
2622
- components: Array.from(issuesByProject.keys()).map((projectKey) => ({
2623
- type: Copyright.Application,
2624
- name: projectKey
2625
- })),
3055
+ components,
2626
3056
  timestamp: /* @__PURE__ */ new Date()
2627
3057
  });
2628
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
+ }
2629
3067
  function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum) {
2630
3068
  const issuesByRule = /* @__PURE__ */ new Map();
2631
3069
  for (const issue of issues) {
@@ -2788,6 +3226,20 @@ function buildResult(r) {
2788
3226
  ...startTime ? { startTime } : {}
2789
3227
  });
2790
3228
  }
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
+ }
2791
3243
  function buildRequirement$15(rule) {
2792
3244
  const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
2793
3245
  const tags = nist.length > 0 ? { nist } : {};
@@ -2799,7 +3251,7 @@ function buildRequirement$15(rule) {
2799
3251
  data: buildCheckText(rule)
2800
3252
  }];
2801
3253
  const title = `${getAccountId(rule.ConfigRuleArn)} - ${rule.ConfigRuleName}`;
2802
- const results = rule.EvaluationResults.map(buildResult);
3254
+ const results = rule.EvaluationResults.length > 0 ? rule.EvaluationResults.map(buildResult) : [buildNotApplicableResult(rule)];
2803
3255
  const req = createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
2804
3256
  tags,
2805
3257
  sourceLocation: {
@@ -2843,7 +3295,7 @@ async function convertAwsConfigToHdf(input) {
2843
3295
  toolName: "AWS Config",
2844
3296
  baselines: [baseline],
2845
3297
  components: [{
2846
- type: Copyright.CloudAccount,
3298
+ type: TargetType.CloudAccount,
2847
3299
  name: `AWS Account ${accountId}`,
2848
3300
  labels: {
2849
3301
  account: accountId,
@@ -2952,8 +3404,12 @@ async function convertCheckovToHdf(input) {
2952
3404
  }
2953
3405
  const requirements = [];
2954
3406
  for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
2955
- const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
2956
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 });
2957
3413
  return buildHdfResults({
2958
3414
  generatorName: "checkov-to-hdf",
2959
3415
  converterVersion: "1.0.0",
@@ -3057,6 +3513,7 @@ async function convertGosecToHdf(input) {
3057
3513
  }
3058
3514
  const requirements = [];
3059
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()));
3060
3517
  const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
3061
3518
  return buildHdfResults({
3062
3519
  generatorName: "gosec-to-hdf",
@@ -3137,6 +3594,7 @@ async function convertNiktoToHdf(input) {
3137
3594
  if (niktoData.host) targetParts.push(`Host: ${niktoData.host}`);
3138
3595
  if (niktoData.port) targetParts.push(`Port: ${niktoData.port}`);
3139
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()));
3140
3598
  return buildHdfResults({
3141
3599
  generatorName: "nikto-to-hdf",
3142
3600
  converterVersion: "unknown",
@@ -3148,7 +3606,7 @@ async function convertNiktoToHdf(input) {
3148
3606
  summary: niktoData.banner || ""
3149
3607
  })],
3150
3608
  components: [{
3151
- type: Copyright.Application,
3609
+ type: TargetType.Application,
3152
3610
  name: targetName
3153
3611
  }]
3154
3612
  });
@@ -3210,45 +3668,8 @@ async function convertZapToHdf(input) {
3210
3668
  if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
3211
3669
  const resultsChecksum = await inputChecksum(input);
3212
3670
  const zapData = parseJSON(input);
3213
- if (!zapData.site || !Array.isArray(zapData.site)) {
3214
- const hdf = {
3215
- baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
3216
- resultsChecksum,
3217
- title: "OWASP ZAP Scan",
3218
- summary: ""
3219
- })],
3220
- generator: {
3221
- name: "zap-to-hdf",
3222
- version: "unknown"
3223
- },
3224
- tool: {
3225
- name: "OWASP ZAP",
3226
- format: "JSON"
3227
- }
3228
- };
3229
- return JSON.stringify(hdf, null, 2);
3230
- }
3231
- const site = selectSite(zapData.site);
3232
- if (!site) {
3233
- const hdf = {
3234
- baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
3235
- resultsChecksum,
3236
- title: "OWASP ZAP Scan",
3237
- summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
3238
- })],
3239
- generator: {
3240
- name: "zap-to-hdf",
3241
- version: "unknown"
3242
- },
3243
- tool: {
3244
- name: "OWASP ZAP",
3245
- format: "JSON"
3246
- }
3247
- };
3248
- if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
3249
- return JSON.stringify(hdf, null, 2);
3250
- }
3251
- const alerts = site.alerts ?? [];
3671
+ const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
3672
+ const alerts = site?.alerts ?? [];
3252
3673
  const pluginIdCount = /* @__PURE__ */ new Map();
3253
3674
  const { items: limitedAlerts, truncated } = limitArray(alerts);
3254
3675
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -3304,10 +3725,17 @@ async function convertZapToHdf(input) {
3304
3725
  if (controlType !== void 0) req.controlType = controlType;
3305
3726
  requirements.push(req);
3306
3727
  }
3307
- 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
+ }
3308
3736
  const baseline = createMinimalBaseline("OWASP ZAP Scan", requirements, {
3309
3737
  resultsChecksum,
3310
- title: `OWASP ZAP Scan of ${site["@name"] ?? targetName}`,
3738
+ title: baselineName,
3311
3739
  summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
3312
3740
  });
3313
3741
  const tool = {
@@ -3316,14 +3744,14 @@ async function convertZapToHdf(input) {
3316
3744
  };
3317
3745
  if (zapData["@version"]) tool.version = zapData["@version"];
3318
3746
  const components = [];
3319
- if (site["@name"]) components.push({
3747
+ if (site?.["@name"]) components.push({
3320
3748
  name: targetName,
3321
- type: Copyright.Application,
3749
+ type: TargetType.Application,
3322
3750
  url: site["@name"]
3323
3751
  });
3324
3752
  else if (targetName !== "Unknown Host") components.push({
3325
3753
  name: targetName,
3326
- type: Copyright.Application
3754
+ type: TargetType.Application
3327
3755
  });
3328
3756
  const hdf = {
3329
3757
  baselines: [baseline],
@@ -3459,7 +3887,7 @@ async function convertCyclonedxToHdf(input) {
3459
3887
  baselines: [baseline],
3460
3888
  components: [{
3461
3889
  name: targetName,
3462
- type: Copyright.Application
3890
+ type: TargetType.Application
3463
3891
  }],
3464
3892
  timestamp: /* @__PURE__ */ new Date()
3465
3893
  });
@@ -3659,7 +4087,7 @@ async function convertSplunkToHdf(input) {
3659
4087
  baselines: allBaselines,
3660
4088
  components: [{
3661
4089
  name: targetName,
3662
- type: Copyright.Host,
4090
+ type: TargetType.Host,
3663
4091
  osName: targetRelease || void 0,
3664
4092
  labels: {}
3665
4093
  }],
@@ -3759,9 +4187,9 @@ function gitlabSeverityToImpact(severity) {
3759
4187
  }
3760
4188
  function scanTypeToTargetType(scanType) {
3761
4189
  switch (scanType) {
3762
- case "dast": return Copyright.Application;
3763
- case "container_scanning": return Copyright.ContainerImage;
3764
- default: return Copyright.Repository;
4190
+ case "dast": return TargetType.Application;
4191
+ case "container_scanning": return TargetType.ContainerImage;
4192
+ default: return TargetType.Repository;
3765
4193
  }
3766
4194
  }
3767
4195
  function scanTypeLabel(scanType) {
@@ -3894,9 +4322,14 @@ async function convertGitlabToHdf(input) {
3894
4322
  if (controlType !== void 0) req.controlType = controlType;
3895
4323
  requirements.push(req);
3896
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
+ }
3897
4330
  const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
3898
4331
  resultsChecksum,
3899
- title: `GitLab ${scanTypeLabel(scanType)} Security Scan`,
4332
+ title: `GitLab ${label} Security Scan`,
3900
4333
  summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
3901
4334
  });
3902
4335
  const tool = {
@@ -4050,19 +4483,22 @@ async function convertTrufflehogToHdf(input) {
4050
4483
  if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
4051
4484
  validateInputSize(input, "trufflehog");
4052
4485
  const findings = parseFindings(input);
4053
- if (findings.length === 0) throw new Error("trufflehog: no findings in input");
4054
4486
  const resultsChecksum = await inputChecksum(input);
4055
4487
  const limitedFindings = limitArrayWithWarning(findings, "finding");
4056
4488
  const groups = groupFindings(limitedFindings);
4057
4489
  const requirements = [];
4058
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
+ }
4059
4495
  const hdf = {
4060
4496
  baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
4061
4497
  resultsChecksum,
4062
4498
  title: `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`
4063
4499
  })],
4064
4500
  generator: {
4065
- name: "hdf-converters",
4501
+ name: "trufflehog-to-hdf",
4066
4502
  version: "1.0.0"
4067
4503
  },
4068
4504
  tool: {
@@ -4074,7 +4510,7 @@ async function convertTrufflehogToHdf(input) {
4074
4510
  const repoURL = findGitRepoURL(limitedFindings);
4075
4511
  if (repoURL) hdf.components = [{
4076
4512
  name: repoURL,
4077
- type: Copyright.Repository
4513
+ type: TargetType.Repository
4078
4514
  }];
4079
4515
  return JSON.stringify(hdf, null, 2);
4080
4516
  }
@@ -4136,6 +4572,7 @@ async function convertBurpsuiteToHdf(input) {
4136
4572
  const requirements = [];
4137
4573
  for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues));
4138
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()));
4139
4576
  const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
4140
4577
  resultsChecksum,
4141
4578
  title: `BurpSuite Scan: ${targetName}`
@@ -4149,7 +4586,7 @@ async function convertBurpsuiteToHdf(input) {
4149
4586
  baselines: [baseline],
4150
4587
  components: [{
4151
4588
  name: targetName,
4152
- type: Copyright.Application
4589
+ type: TargetType.Application
4153
4590
  }],
4154
4591
  generator: {
4155
4592
  name: "burpsuite-to-hdf",
@@ -4346,7 +4783,7 @@ async function convertDbprotectToHdf(input) {
4346
4783
  })],
4347
4784
  components: [{
4348
4785
  name: targetName,
4349
- type: Copyright.Host
4786
+ type: TargetType.Host
4350
4787
  }],
4351
4788
  timestamp: /* @__PURE__ */ new Date()
4352
4789
  });
@@ -4386,6 +4823,148 @@ function buildSummary(result) {
4386
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"}`;
4387
4824
  }
4388
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
+ /**
4389
4968
  * Builds the code_desc string for a vulnerability result.
4390
4969
  */
4391
4970
  function formatCodeDesc$2(vuln) {
@@ -4395,10 +4974,17 @@ function formatCodeDesc$2(vuln) {
4395
4974
  }
4396
4975
  /**
4397
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
4398
4981
  */
4399
- function buildRequirement$9(vuln) {
4982
+ function buildRequirement$9(vuln, packageTypes, distro) {
4400
4983
  const nist = DEFAULT_REMEDIATION_NIST_TAGS;
4401
- 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);
4402
4988
  const descriptions = [{
4403
4989
  label: "default",
4404
4990
  data: vuln.description
@@ -4412,6 +4998,12 @@ function buildRequirement$9(vuln) {
4412
4998
  const controlType = deriveControlTypeFromTags(nist);
4413
4999
  if (controlType !== void 0) req.controlType = controlType;
4414
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];
4415
5007
  return req;
4416
5008
  }
4417
5009
  /**
@@ -4422,7 +5014,13 @@ function convertSingleResult(result, resultsChecksum) {
4422
5014
  const { items: limitedVulns, truncated } = limitArray(vulns);
4423
5015
  /* v8 ignore next -- truncation only triggers with >100K items */
4424
5016
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
4425
- return createMinimalBaseline("Twistlock Scan", limitedVulns.map((vuln) => buildRequirement$9(vuln)), {
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, {
4426
5024
  resultsChecksum,
4427
5025
  title: buildTitle$1(result),
4428
5026
  summary: buildSummary(result)
@@ -4457,7 +5055,7 @@ async function convertTwistlockToHdf(input) {
4457
5055
  baselines,
4458
5056
  components: [{
4459
5057
  name: targetName,
4460
- type: Copyright.ContainerImage,
5058
+ type: TargetType.ContainerImage,
4461
5059
  labels: { image: results[0]?.id ?? targetName }
4462
5060
  }],
4463
5061
  timestamp: /* @__PURE__ */ new Date()
@@ -4529,9 +5127,30 @@ function buildRequirement$8(finding, timestamp) {
4529
5127
  req.verificationMethod = VerificationMethodEnum.Automated;
4530
5128
  const controlType = deriveControlTypeFromTags(nist);
4531
5129
  if (controlType !== void 0) req.controlType = controlType;
5130
+ const pkg = buildAffectedPackageFromComponent(finding.component);
5131
+ if (pkg) req.affectedPackages = [pkg];
4532
5132
  return req;
4533
5133
  }
4534
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
+ });
5152
+ }
5153
+ /**
4535
5154
  * Converts Dependency-Track FPF JSON output to HDF format.
4536
5155
  *
4537
5156
  * @param input - Dependency-Track FPF JSON string
@@ -4544,7 +5163,12 @@ async function convertDeptrackToHdf(input) {
4544
5163
  const parsed = parseJSON(input);
4545
5164
  if (!parsed || typeof parsed !== "object") throw new Error("deptrack: invalid JSON");
4546
5165
  if (!parsed.findings && !parsed.project && !parsed.meta) throw new Error("deptrack: input does not appear to be a Dependency-Track report");
4547
- const baseline = createMinimalBaseline("Dependency-Track Scan", (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp)), {
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, {
4548
5172
  resultsChecksum,
4549
5173
  title: `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`,
4550
5174
  summary: parsed.project?.description
@@ -4558,7 +5182,7 @@ async function convertDeptrackToHdf(input) {
4558
5182
  baselines: [baseline],
4559
5183
  components: [{
4560
5184
  name: targetName,
4561
- type: Copyright.Application
5185
+ type: TargetType.Application
4562
5186
  }],
4563
5187
  timestamp: /* @__PURE__ */ new Date()
4564
5188
  });
@@ -4636,8 +5260,49 @@ function buildRequirement$7(entryID, entries) {
4636
5260
  const controlType = deriveControlTypeFromTags(nist);
4637
5261
  if (controlType !== void 0) req.controlType = controlType;
4638
5262
  req.verificationMethod = VerificationMethodEnum.Automated;
5263
+ const pkg = buildAffectedPackageFromEntry(rep);
5264
+ if (pkg) req.affectedPackages = [pkg];
4639
5265
  return req;
4640
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
+ });
5305
+ }
4641
5306
  /**
4642
5307
  * Converts JFrog Xray JSON output to HDF format.
4643
5308
  *
@@ -4667,6 +5332,7 @@ async function convertJfrogXrayToHdf(input) {
4667
5332
  }
4668
5333
  const requirements = [];
4669
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()));
4670
5336
  return buildHdfResults({
4671
5337
  generatorName: "jfrog-xray-to-hdf",
4672
5338
  converterVersion: "1.0.0",
@@ -4675,7 +5341,7 @@ async function convertJfrogXrayToHdf(input) {
4675
5341
  baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
4676
5342
  components: [{
4677
5343
  name: "JFrog Xray Scan",
4678
- type: Copyright.Application
5344
+ type: TargetType.Application
4679
5345
  }],
4680
5346
  timestamp: /* @__PURE__ */ new Date()
4681
5347
  });
@@ -4738,6 +5404,15 @@ function buildRequirement$6(vuln) {
4738
5404
  const controlType = deriveControlTypeFromTags(nist);
4739
5405
  if (controlType !== void 0) req.controlType = controlType;
4740
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];
4741
5416
  return req;
4742
5417
  }
4743
5418
  /**
@@ -4783,6 +5458,9 @@ async function convertNeuvectorToHdf(input) {
4783
5458
  seen.add(id);
4784
5459
  requirements.push(buildRequirement$6(vuln));
4785
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));
4786
5464
  return buildHdfResults({
4787
5465
  generatorName: "neuvector-to-hdf",
4788
5466
  converterVersion: "1.0.0",
@@ -4794,13 +5472,13 @@ async function convertNeuvectorToHdf(input) {
4794
5472
  })],
4795
5473
  components: [{
4796
5474
  name: targetNameFromReport(scan.report),
4797
- type: Copyright.ContainerImage,
5475
+ type: TargetType.ContainerImage,
4798
5476
  labels: {
4799
5477
  image: `${scan.report.registry}/${scan.report.repository}:${scan.report.tag}`,
4800
5478
  registry: scan.report.registry
4801
5479
  }
4802
5480
  }],
4803
- timestamp: /* @__PURE__ */ new Date()
5481
+ timestamp: now
4804
5482
  });
4805
5483
  }
4806
5484
  //#endregion
@@ -4915,21 +5593,22 @@ async function convertFortifyToHdf(input) {
4915
5593
  if (truncatedDescs) console.warn(`WARNING: Input truncated at ${limitedDescs.length} Description items (original: ${descriptions.length})`);
4916
5594
  const createdDate = fvdl.CreatedTS?.date ?? "";
4917
5595
  const startTimeStr = `${createdDate}T${fvdl.CreatedTS?.time ?? ""}`;
4918
- const baseline = createMinimalBaseline("Fortify Scan", limitedDescs.map((desc) => {
5596
+ const requirements = limitedDescs.map((desc) => {
4919
5597
  return buildRequirement$5(desc, vulnGroups.get(desc.classID ?? "") ?? [], snippetMap, startTimeStr);
4920
- }), {
4921
- resultsChecksum,
4922
- title: "Fortify Static Analyzer Scan",
4923
- summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
4924
- version: fvdl.EngineData?.EngineVersion ?? "",
4925
- status: "loaded"
4926
5598
  });
4927
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()));
4928
5601
  const hdfResult = {
4929
- 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
+ })],
4930
5609
  components: [{
4931
5610
  name: targetName,
4932
- type: Copyright.Repository
5611
+ type: TargetType.Repository
4933
5612
  }],
4934
5613
  generator: {
4935
5614
  name: "fortify-to-hdf",
@@ -5044,9 +5723,50 @@ function buildRequirement$4(rec) {
5044
5723
  const controlType = deriveControlTypeFromTags(nist);
5045
5724
  if (controlType !== void 0) req.controlType = controlType;
5046
5725
  req.verificationMethod = VerificationMethodEnum.Automated;
5726
+ if (rec["CVE ID"]) {
5727
+ const pkg = buildAffectedPackageFromRecord(rec);
5728
+ if (pkg) req.affectedPackages = [pkg];
5729
+ }
5047
5730
  return req;
5048
5731
  }
5049
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
+ });
5768
+ }
5769
+ /**
5050
5770
  * Groups records by hostname, preserving insertion order.
5051
5771
  */
5052
5772
  function groupByHostname(records) {
@@ -5077,20 +5797,25 @@ function buildBaseline(hostname, records, resultsChecksum) {
5077
5797
  async function convertPrismaToHdf(input) {
5078
5798
  if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
5079
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}"`);
5080
5802
  const records = parseCsv(input);
5081
- if (records.length === 0) throw new Error("prisma: no data rows in CSV");
5082
- const firstRecord = records[0];
5083
- for (const col of REQUIRED_COLUMNS) if (!(col in firstRecord)) throw new Error(`prisma: missing required CSV column "${col}"`);
5084
5803
  const resultsChecksum = await inputChecksum(input);
5085
- const hostGroups = groupByHostname(records);
5086
5804
  const baselines = [];
5087
5805
  const components = [];
5088
- for (const [hostname, hostRecords] of hostGroups) {
5089
- baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
5090
- components.push({
5091
- name: hostname,
5092
- type: Copyright.Host
5093
- });
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
+ }
5094
5819
  }
5095
5820
  return buildHdfResults({
5096
5821
  generatorName: "prisma-to-hdf",
@@ -5239,20 +5964,25 @@ async function convertNetsparkerToHdf(input) {
5239
5964
  const { items: limitedVulns, truncated } = limitArray(vulns);
5240
5965
  /* v8 ignore next -- truncation only triggers with >100K items */
5241
5966
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
5242
- const baseline = createMinimalBaseline("Netsparker Scan", limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated)), {
5243
- resultsChecksum,
5244
- title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
5245
- });
5246
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
+ }
5247
5974
  return buildHdfResults({
5248
5975
  generatorName: "netsparker-to-hdf",
5249
5976
  converterVersion: "1.0.0",
5250
5977
  toolName,
5251
5978
  toolFormat: "XML",
5252
- baselines: [baseline],
5979
+ baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
5980
+ resultsChecksum,
5981
+ title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
5982
+ })],
5253
5983
  components: [{
5254
5984
  name: targetName,
5255
- type: Copyright.Application
5985
+ type: TargetType.Application
5256
5986
  }]
5257
5987
  });
5258
5988
  }
@@ -5367,12 +6097,14 @@ async function convertScoutsuiteToHdf(input) {
5367
6097
  const resultsChecksum = await inputChecksum(jsonStr);
5368
6098
  const report = parseJSON(jsonStr);
5369
6099
  if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
5370
- const baseline = createMinimalBaseline("ScoutSuite Scan", limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time)), {
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, {
5371
6104
  resultsChecksum,
5372
6105
  title: `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`,
5373
6106
  summary: report.last_run.ruleset_about
5374
6107
  });
5375
- const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
5376
6108
  return buildHdfResults({
5377
6109
  generatorName: "scoutsuite-to-hdf",
5378
6110
  converterVersion: "1.0.0",
@@ -5382,7 +6114,7 @@ async function convertScoutsuiteToHdf(input) {
5382
6114
  baselines: [baseline],
5383
6115
  components: [{
5384
6116
  name: targetName,
5385
- type: Copyright.CloudAccount,
6117
+ type: TargetType.CloudAccount,
5386
6118
  labels: {
5387
6119
  account: report.account_id,
5388
6120
  provider: report.provider_code ?? report.provider_name
@@ -5539,6 +6271,10 @@ async function convertConveyorToHdf(input) {
5539
6271
  const desc = data.api_response.params["description"];
5540
6272
  if (typeof desc === "string" && desc.length > 0) targetName = desc;
5541
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
+ }));
5542
6278
  return buildHdfResults({
5543
6279
  generatorName: "conveyor-to-hdf",
5544
6280
  converterVersion: "1.0.0",
@@ -5547,7 +6283,7 @@ async function convertConveyorToHdf(input) {
5547
6283
  baselines,
5548
6284
  components: [{
5549
6285
  name: targetName,
5550
- type: Copyright.Application
6286
+ type: TargetType.Application
5551
6287
  }],
5552
6288
  timestamp: /* @__PURE__ */ new Date()
5553
6289
  });
@@ -5814,6 +6550,8 @@ async function convertVeracodeToHdf(input) {
5814
6550
  const cweRequirements = buildCWERequirements(ensureArray(report.severity), firstBuildDate);
5815
6551
  const cveRequirements = buildCVERequirements(report.software_composition_analysis, firstBuildDate);
5816
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()));
5817
6555
  let title;
5818
6556
  const staticAnalysis = report["static-analysis"];
5819
6557
  if (staticAnalysis) {
@@ -5829,7 +6567,6 @@ async function convertVeracodeToHdf(input) {
5829
6567
  version: attr(report, "policy_version"),
5830
6568
  summary: attr(report, "policy_name")
5831
6569
  });
5832
- const targetName = attr(report, "app_name") || "Veracode Application";
5833
6570
  const timestamp = parseVeracodeTimestamp(firstBuildDate);
5834
6571
  return buildHdfResults({
5835
6572
  generatorName: "veracode-to-hdf",
@@ -5839,7 +6576,7 @@ async function convertVeracodeToHdf(input) {
5839
6576
  baselines: [baseline],
5840
6577
  components: [{
5841
6578
  name: targetName,
5842
- type: Copyright.Application
6579
+ type: TargetType.Application
5843
6580
  }],
5844
6581
  timestamp
5845
6582
  });
@@ -5957,7 +6694,7 @@ async function convertMsftSecureScoreToHdf(input) {
5957
6694
  }),
5958
6695
  components: [{
5959
6696
  name: `Azure Tenant: ${tenantId}`,
5960
- type: Copyright.CloudAccount,
6697
+ type: TargetType.CloudAccount,
5961
6698
  labels: {
5962
6699
  account: tenantId,
5963
6700
  provider: "azure"
@@ -5980,6 +6717,7 @@ async function convertMsftDefenderDevopsToHdf(input) {
5980
6717
  const { components, runEnrichments } = extractEnrichments(raw);
5981
6718
  const hdfJson = await convertSarifToHdf(input);
5982
6719
  const result = JSON.parse(hdfJson);
6720
+ synthesizeNoFindingsPlaceholders(result);
5983
6721
  applyEnrichments(result, components, runEnrichments);
5984
6722
  if (result.generator) result.generator.name = "msft-defender-devops-to-hdf";
5985
6723
  if (result.tool) result.tool.name = "Microsoft Defender for DevOps";
@@ -5994,7 +6732,7 @@ function extractEnrichments(raw) {
5994
6732
  seenRepos.add(vcp.repositoryUri);
5995
6733
  const target = {
5996
6734
  name: repoNameFromURI(vcp.repositoryUri),
5997
- type: Copyright.Repository,
6735
+ type: TargetType.Repository,
5998
6736
  url: vcp.repositoryUri,
5999
6737
  labels: {}
6000
6738
  };
@@ -6044,6 +6782,14 @@ function applyEnrichments(result, components, runEnrichments) {
6044
6782
  }
6045
6783
  }
6046
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
+ }
6047
6793
  function repoNameFromURI(uri) {
6048
6794
  const parts = uri.split("/");
6049
6795
  return parts[parts.length - 1] || uri;
@@ -6140,21 +6886,23 @@ async function convertMsftDefenderCloudToHdf(input) {
6140
6886
  }
6141
6887
  const requirements = [];
6142
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
+ }
6143
6894
  const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
6144
6895
  const components = [];
6145
- if (limitedAssessments.length > 0) {
6146
- const subscriptionID = extractSubscriptionID(limitedAssessments[0].id);
6147
- if (subscriptionID) components.push({
6148
- name: `Azure Subscription ${subscriptionID}`,
6149
- type: Copyright.CloudAccount,
6150
- accountId: subscriptionID,
6151
- provider: "azure",
6152
- labels: {
6153
- account: subscriptionID,
6154
- provider: "azure"
6155
- }
6156
- });
6157
- }
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
+ });
6158
6906
  return buildHdfResults({
6159
6907
  generatorName: "msft-defender-cloud-to-hdf",
6160
6908
  converterVersion: "1.0.0",
@@ -6229,7 +6977,7 @@ function extractDeviceTarget(alert) {
6229
6977
  for (const ev of alert.evidence) if ((ev["@odata.type"] ?? "").includes("deviceEvidence") && ev.deviceDnsName) {
6230
6978
  const target = {
6231
6979
  name: ev.deviceDnsName,
6232
- type: Copyright.Host,
6980
+ type: TargetType.Host,
6233
6981
  labels: { provider: "azure" }
6234
6982
  };
6235
6983
  if (ev.deviceDnsName) target.fqdn = ev.deviceDnsName;
@@ -6239,7 +6987,7 @@ function extractDeviceTarget(alert) {
6239
6987
  }
6240
6988
  return {
6241
6989
  name: alert.tenantId ?? "unknown",
6242
- type: Copyright.CloudAccount,
6990
+ type: TargetType.CloudAccount,
6243
6991
  accountId: alert.tenantId,
6244
6992
  labels: {
6245
6993
  account: alert.tenantId ?? "",
@@ -6308,6 +7056,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
6308
7056
  components.push(target);
6309
7057
  }
6310
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()));
6311
7060
  return buildHdfResults({
6312
7061
  generatorName: "msft-defender-endpoint-to-hdf",
6313
7062
  converterVersion: "1.0.0",
@@ -6448,8 +7197,7 @@ function buildTestResultObj(hdfData, baseline) {
6448
7197
  [`${ATTR}idref`]: ruleIdRef,
6449
7198
  result: wrap(hdfStatusToXccdf(result.status))
6450
7199
  };
6451
- const startTime = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
6452
- rr[`${ATTR}time`] = startTime;
7200
+ rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
6453
7201
  if (result.message) rr.message = wrap(result.message);
6454
7202
  if (result.codeDesc) rr.check = {
6455
7203
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
@@ -6879,7 +7627,7 @@ async function convertHdfToOscalPoam(input) {
6879
7627
  return JSON.stringify(doc, null, 2);
6880
7628
  }
6881
7629
  /**
6882
- * Converts parsed HdfAmendments to an OSCAL PlanOfActionAndMilestones.
7630
+ * Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
6883
7631
  */
6884
7632
  function amendmentsToPOAM(amendments) {
6885
7633
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -7079,6 +7827,1150 @@ async function convertIonchannelToHdf(input) {
7079
7827
  });
7080
7828
  }
7081
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
7082
8974
  //#region converters/oscal-to-hdf/typescript/detect.ts
7083
8975
  /**
7084
8976
  * OSCAL document type detection.
@@ -7167,7 +9059,7 @@ async function catalogToBaseline(catalog, rawInput) {
7167
9059
  requirements,
7168
9060
  groups,
7169
9061
  generator: {
7170
- name: "hdf-converters",
9062
+ name: "oscal-catalog-to-hdf",
7171
9063
  version: "1.0.0"
7172
9064
  }
7173
9065
  };
@@ -7410,7 +9302,7 @@ async function convertOscalComponentToHdf(input) {
7410
9302
  integrity,
7411
9303
  requirements,
7412
9304
  generator: {
7413
- name: "hdf-converters",
9305
+ name: "oscal-component-to-hdf",
7414
9306
  version: "1.0.0"
7415
9307
  }
7416
9308
  };
@@ -7471,7 +9363,7 @@ async function convertOscalSspToHdf(input) {
7471
9363
  integrity,
7472
9364
  components: [],
7473
9365
  generator: {
7474
- name: "hdf-converters",
9366
+ name: "oscal-ssp-to-hdf",
7475
9367
  version: "1.0.0"
7476
9368
  }
7477
9369
  };
@@ -7598,13 +9490,13 @@ function sspComponentToHDFComponent(sc, componentControls) {
7598
9490
  function mapOSCALComponentType(oscalType) {
7599
9491
  switch (oscalType.toLowerCase()) {
7600
9492
  case "software":
7601
- case "this-system": return BoundaryDescription.Application;
7602
- case "service": return BoundaryDescription.Application;
7603
- case "hardware": return BoundaryDescription.Host;
7604
- case "network": return BoundaryDescription.Network;
7605
- case "database": return BoundaryDescription.Database;
7606
- case "storage": return BoundaryDescription.Artifact;
7607
- default: return BoundaryDescription.Application;
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;
7608
9500
  }
7609
9501
  }
7610
9502
  //#endregion
@@ -7641,7 +9533,7 @@ async function convertOscalSapToHdf(input) {
7641
9533
  type: planType,
7642
9534
  description,
7643
9535
  generator: {
7644
- name: "hdf-converters",
9536
+ name: "oscal-sap-to-hdf",
7645
9537
  version: "1.0.0"
7646
9538
  }
7647
9539
  };
@@ -7771,7 +9663,7 @@ async function convertOscalPoamToHdf(input) {
7771
9663
  version: meta.version,
7772
9664
  appliedBy,
7773
9665
  generator: {
7774
- name: "hdf-converters",
9666
+ name: "oscal-poam-to-hdf",
7775
9667
  version: "1.0.0"
7776
9668
  }
7777
9669
  };
@@ -8084,6 +9976,6 @@ function sarBaselineName(result, sar) {
8084
9976
  return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
8085
9977
  }
8086
9978
  //#endregion
8087
- export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCyclonedxToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsv, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
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 };
8088
9980
 
8089
9981
  //# sourceMappingURL=index.js.map