@mitre/hdf-converters 3.6.0 → 3.7.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,8 +1,8 @@
1
- import { n as detectFormat, r as detectConverter, t as registerAllFingerprints } from "./register-all-CDfYvwQF.js";
1
+ import { n as detectFormat, o as isCheckovTestCaseName, r as detectConverter, t as registerAllFingerprints } from "./register-all-BEuKQGEZ.js";
2
2
  import { flattenOverlays } from "@mitre/hdf-parsers";
3
- import { DEFAULT_MAX_INPUT_SIZE, buildCsv, buildXml, canonicalJson, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, encodeBase64Utf8, formatJsonNumber, formatTimestamp, formatTimestampSeconds, governingStatusOverride, impactToSeverity, isUnratedSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, severityToImpact, severityToImpactWithAliases, sha256, stripHtml, stripHtml as stripHTML, trimUtcFraction, validateInputSize, worstStatus, xmlSafeText } from "@mitre/hdf-utilities";
3
+ import { buildCsv, buildXml, canonicalJson, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, encodeBase64Utf8, formatJsonNumber, formatTimestamp, formatTimestampSeconds, governingStatusOverride, impactToSeverity, isUnratedSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, severityToImpact, severityToImpactWithAliases, sha256, stripHtml, stripHtml as stripHTML, trimUtcFraction, validateInputSize, worstStatus, xmlSafeText } from "@mitre/hdf-utilities";
4
4
  import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, affectedPackageSchema, amendmentsSchema, bomSchema, commonSchema, comparisonSchema, componentSchema, createDescription, createMinimalBaseline, createRequirement, createResult, cvssSchema, dataFlowSchema, epssSchema, eventsSchema, extensionsSchema, hdfAmendmentsSchema, hdfBaselineSchema, hdfComparisonSchema, hdfEvidencePackageSchema, hdfPlanSchema, hdfRequirementChangeEventSchema, hdfResultsSchema, hdfSystemSchema, kevSchema, parameterSchema, planSchema, platformSchema, resultSchema, runnerSchema, severityToImpact as severityToImpact$1, statisticsSchema, systemSchema, targetSchema } from "@mitre/hdf-schema";
5
- import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_CONFIG_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getHipcheckNistControls, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
5
+ import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_CONFIG_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, SUPPORTED_NIST_REVISIONS, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCheckovCciNistMapping, getCurrentNistRevision, getCweNistControl, getHipcheckNistControls, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistExists, nistToCci, normalizeNistId } from "@mitre/hdf-mappings";
6
6
  import Ajv from "ajv";
7
7
  import addFormats from "ajv-formats";
8
8
  //#region ../hdf-validators/dist/index.js
@@ -250,6 +250,13 @@ function limitArray(items, maxItems = DEFAULT_MAX_ITEMS) {
250
250
  };
251
251
  }
252
252
  /**
253
+ * Emit a non-fatal converter warning as "WARNING: <message>" on stderr, the
254
+ * TypeScript counterpart of Go's log.Printf("WARNING: ...").
255
+ */
256
+ function emitConverterWarning(message) {
257
+ console.warn(`WARNING: ${message}`);
258
+ }
259
+ /**
253
260
  * Limit an array and log a warning if truncated.
254
261
  *
255
262
  * Wraps {@link limitArray} with a console.warn call when items are truncated.
@@ -261,7 +268,7 @@ function limitArray(items, maxItems = DEFAULT_MAX_ITEMS) {
261
268
  */
262
269
  function limitArrayWithWarning(items, label, maxItems = DEFAULT_MAX_ITEMS) {
263
270
  const { items: limited, truncated } = limitArray(items, maxItems);
264
- if (truncated) console.warn(`WARNING: Input truncated at ${limited.length} ${label} items (original: ${items.length})`);
271
+ if (truncated) emitConverterWarning(`Input truncated at ${limited.length} ${label} items (original: ${items.length})`);
265
272
  return limited;
266
273
  }
267
274
  /**
@@ -323,10 +330,9 @@ function firstNonEmpty(...candidates) {
323
330
  * converter name prefixed onto the error. A non-positive maxSize means "use
324
331
  * the default" (an explicitly-passed 0 never rejects all non-empty input).
325
332
  */
326
- function validateInputSize$1(input, converterName, maxSize = DEFAULT_MAX_INPUT_SIZE) {
327
- const limit = maxSize > 0 ? maxSize : DEFAULT_MAX_INPUT_SIZE;
333
+ function validateInputSize$1(input, converterName, maxSize = 0) {
328
334
  try {
329
- validateInputSize(input, limit);
335
+ validateInputSize(input, maxSize);
330
336
  } catch (err) {
331
337
  throw new Error(`${converterName}: ${err instanceof Error ? err.message : String(err)}`);
332
338
  }
@@ -747,6 +753,24 @@ function typeViolations(result) {
747
753
  * - Results: snake_case → camelCase for all fields
748
754
  * - Overlay flattening: merge overlay/wrapper baselines so every requirement has results
749
755
  */
756
+ /** Reserved passthrough key for the components round-trip carrier. */
757
+ const RESERVED_COMPONENTS_KEY = "hdf_components";
758
+ /**
759
+ * Copy provenance into a null-prototype object, keeping only own enumerable keys
760
+ * and dropping the reserved carrier key. A null prototype makes `__proto__`/
761
+ * `constructor` entries plain data rather than a prototype-pollution vector, and
762
+ * only a plain (non-array) object is treated as provenance. Returns undefined
763
+ * when there is nothing to carry.
764
+ */
765
+ function sanitizeProvenance(raw) {
766
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return;
767
+ const out = Object.create(null);
768
+ for (const [key, value] of Object.entries(raw)) {
769
+ if (key === RESERVED_COMPONENTS_KEY) continue;
770
+ out[key] = value;
771
+ }
772
+ return Object.keys(out).length > 0 ? out : void 0;
773
+ }
750
774
  /**
751
775
  * Convert tags.severity string to a valid severity value.
752
776
  * Returns null if the value is not a recognized severity.
@@ -813,7 +837,7 @@ function convertResult(v1Result) {
813
837
  if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
814
838
  if (v1Result.backtrace !== void 0) v2Result.backtrace = v1Result.backtrace;
815
839
  if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
816
- if (v1Result.resource_id !== void 0) v2Result.resourceId = v1Result.resource_id;
840
+ if (v1Result.resource_id !== void 0 && v1Result.resource_id !== null) v2Result.resourceId = typeof v1Result.resource_id === "string" ? v1Result.resource_id : canonicalJson(v1Result.resource_id);
817
841
  return v2Result;
818
842
  }
819
843
  /**
@@ -879,7 +903,12 @@ function convertControl(v1Control) {
879
903
  impact: v1Control.impact
880
904
  };
881
905
  if (v1Control.title !== void 0) v2Req.title = v1Control.title;
882
- if (v1Control.descriptions !== void 0) v2Req.descriptions = v1Control.descriptions;
906
+ const descriptions = [...v1Control.descriptions ?? []];
907
+ if (!descriptions.some((d) => d.label === "default")) descriptions.unshift({
908
+ label: "default",
909
+ data: v1Control.desc ?? ""
910
+ });
911
+ v2Req.descriptions = descriptions;
883
912
  if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
884
913
  if (v1Control.code !== void 0) v2Req.code = v1Control.code;
885
914
  if (Array.isArray(v1Control.refs)) {
@@ -1087,6 +1116,7 @@ function convertLegacyHdf(v1Data, converterVersion = "1.0.0") {
1087
1116
  }
1088
1117
  v2.components = [component];
1089
1118
  }
1119
+ if (Array.isArray(v1Data.passthrough?.hdf_components) && v1Data.passthrough.hdf_components.length > 0) v2.components = v1Data.passthrough.hdf_components;
1090
1120
  v2.generator = v1Data.generator ?? {
1091
1121
  name: "legacyhdf-to-hdf",
1092
1122
  version: converterVersion
@@ -1094,13 +1124,15 @@ function convertLegacyHdf(v1Data, converterVersion = "1.0.0") {
1094
1124
  v2.tool = toolIdentity(v1Data.version);
1095
1125
  const timestamp = documentTimestamp(v1Data);
1096
1126
  if (timestamp) v2.timestamp = timestamp;
1127
+ const carriedProvenance = sanitizeProvenance(v1Data.passthrough);
1097
1128
  const knownV1Fields = /* @__PURE__ */ new Set([
1098
1129
  "version",
1099
1130
  "platform",
1100
1131
  "profiles",
1101
1132
  "statistics",
1102
1133
  "generator",
1103
- "timestamp"
1134
+ "timestamp",
1135
+ "passthrough"
1104
1136
  ]);
1105
1137
  const extensionFields = {};
1106
1138
  for (const [key, value] of Object.entries(v1Data)) if (!knownV1Fields.has(key)) extensionFields[key] = value;
@@ -1108,6 +1140,10 @@ function convertLegacyHdf(v1Data, converterVersion = "1.0.0") {
1108
1140
  ...extensionFields,
1109
1141
  v1_version: v1Data.version
1110
1142
  };
1143
+ if (carriedProvenance) v2.extensions = {
1144
+ ...v2.extensions ?? {},
1145
+ passthrough: carriedProvenance
1146
+ };
1111
1147
  return flattenOverlays(v2).results;
1112
1148
  }
1113
1149
  /**
@@ -1537,7 +1573,8 @@ function buildBaseline$1(name, findings, resultsChecksum) {
1537
1573
  groups.set(id, [f]);
1538
1574
  }
1539
1575
  }
1540
- return createMinimalBaseline(name, order.map((id) => buildRequirement$19(id, groups.get(id))), { resultsChecksum });
1576
+ const requirements = order.map((id) => buildRequirement$19(id, groups.get(id)));
1577
+ return createMinimalBaseline(name, requirements, { resultsChecksum });
1541
1578
  }
1542
1579
  /** Converts an ASFF document to HDF Results JSON. */
1543
1580
  async function convertAsffToHdf(input, converterVersion = "1.0.0") {
@@ -1983,14 +2020,16 @@ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
1983
2020
  if (!input || !input.trim()) throw new Error("Empty input");
1984
2021
  validateInputSize$1(input, "junit");
1985
2022
  const { suites, name } = parseJUnitXML(input);
2023
+ warnIfCheckov(suites);
1986
2024
  const scanTime = resolveScanTime(suites);
1987
2025
  const requirements = buildRequirements(suites, scanTime);
1988
2026
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
2027
+ const resultsChecksum = await inputChecksum(input);
1989
2028
  return buildHdfResults({
1990
2029
  generatorName: "junit-to-hdf",
1991
2030
  converterVersion,
1992
2031
  toolName: "JUnit XML",
1993
- baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
2032
+ baselines: [createMinimalBaseline(name, requirements, { resultsChecksum })],
1994
2033
  components: [{
1995
2034
  type: TargetType.Application,
1996
2035
  name
@@ -1998,6 +2037,10 @@ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
1998
2037
  timestamp: scanTime
1999
2038
  });
2000
2039
  }
2040
+ const CHECKOV_JUNIT_WARNING = "input looks like Checkov JUnit XML. junit-to-hdf keeps only Checkov's display strings and drops the check_id, severity and guideline that control mappings key on; re-run Checkov with -o json and convert that output with checkov-to-hdf instead.";
2041
+ function warnIfCheckov(suites) {
2042
+ if (suites.some((suite) => (suite.testcase ?? []).some((tc) => isCheckovTestCaseName(String(tc["@_name"] ?? ""))))) emitConverterWarning(CHECKOV_JUNIT_WARNING);
2043
+ }
2001
2044
  function hostComponents(suites) {
2002
2045
  const hosts = [];
2003
2046
  const seen = /* @__PURE__ */ new Set();
@@ -2020,10 +2063,19 @@ function resolveScanTime(suites) {
2020
2063
  }
2021
2064
  return /* @__PURE__ */ new Date();
2022
2065
  }
2066
+ function flattenSuites(suites) {
2067
+ const out = [];
2068
+ for (const suite of suites) {
2069
+ const { testsuite: nested, ...rest } = suite;
2070
+ out.push(rest);
2071
+ if (nested) out.push(...flattenSuites(nested));
2072
+ }
2073
+ return out;
2074
+ }
2023
2075
  function parseJUnitXML(input) {
2024
2076
  const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2, { attributeNamePrefix: "@_" });
2025
2077
  if (parsed.testsuites) {
2026
- const suites = [...parsed.testsuites.testsuite ?? []];
2078
+ const suites = flattenSuites(parsed.testsuites.testsuite ?? []);
2027
2079
  const name = parsed.testsuites["@_name"] ? decodeXmlEntities$6(parsed.testsuites["@_name"]) : "JUnit Test Results";
2028
2080
  const looseCases = parsed.testsuites.testcase ?? [];
2029
2081
  if (looseCases.length > 0) suites.push({
@@ -2037,11 +2089,12 @@ function parseJUnitXML(input) {
2037
2089
  }
2038
2090
  if (parsed.testsuite) {
2039
2091
  const suite = parsed.testsuite;
2040
- const suites = Array.isArray(suite) ? suite : [suite];
2041
- const suiteName = suites[0]?.["@_name"];
2092
+ const rootSuites = Array.isArray(suite) ? suite : [suite];
2093
+ const suiteName = rootSuites[0]?.["@_name"];
2094
+ const name = suiteName ? decodeXmlEntities$6(suiteName) : "JUnit Test Results";
2042
2095
  return {
2043
- suites,
2044
- name: suiteName ? decodeXmlEntities$6(suiteName) : "JUnit Test Results"
2096
+ suites: flattenSuites(rootSuites),
2097
+ name
2045
2098
  };
2046
2099
  }
2047
2100
  throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
@@ -2949,7 +3002,11 @@ function cklVulnToModel(v) {
2949
3002
  }
2950
3003
  /** Serialize the Checklist model to CKL XML (with declaration). */
2951
3004
  function serializeCkl(cl) {
2952
- return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ CHECKLIST: {
3005
+ if (cl.stigs.length === 0) throw new Error("serialize ckl: checklist has no stigs");
3006
+ cl.stigs.forEach((s, i) => {
3007
+ if (s.vulns.length === 0) throw new Error(`serialize ckl: stig ${i + 1} has no rules`);
3008
+ });
3009
+ const xmlObj = { CHECKLIST: {
2953
3010
  ASSET: {
2954
3011
  ROLE: cl.asset.role || "None",
2955
3012
  ASSET_TYPE: cl.asset.assetType || "Computing",
@@ -2968,7 +3025,8 @@ function serializeCkl(cl) {
2968
3025
  STIG_INFO: { SI_DATA: stigInfoSiData(s) },
2969
3026
  VULN: s.vulns.map(modelVulnToCkl)
2970
3027
  })) }
2971
- } }, BUILD_OPTIONS$1)}`;
3028
+ } };
3029
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml(xmlObj, BUILD_OPTIONS$1)}`;
2972
3030
  }
2973
3031
  function stigInfoSiData(s) {
2974
3032
  return [
@@ -3121,6 +3179,10 @@ function cklbRuleToModel(r) {
3121
3179
  }
3122
3180
  /** Serialize the Checklist model to CKLB JSON. */
3123
3181
  function serializeCklb(cl) {
3182
+ if (cl.stigs.length === 0) throw new Error("serialize cklb: checklist has no stigs");
3183
+ cl.stigs.forEach((s, i) => {
3184
+ if (s.vulns.length === 0) throw new Error(`serialize cklb: stig ${i + 1} has no rules`);
3185
+ });
3124
3186
  const doc = {
3125
3187
  title: cklbTitle(cl),
3126
3188
  cklb_version: cl.cklbVersion || "1.0",
@@ -3150,7 +3212,7 @@ function serializeCklb(cl) {
3150
3212
  uuid: s.uuid ?? "",
3151
3213
  ...s.referenceIdentifier ? { reference_identifier: s.referenceIdentifier } : {},
3152
3214
  rules: s.vulns.map((v) => ({
3153
- group_id: v.groupID || v.vulnNum,
3215
+ group_id: v.groupID || v.vulnNum || "",
3154
3216
  group_title: v.groupTitle ?? "",
3155
3217
  rule_id: v.ruleID ?? "",
3156
3218
  rule_version: v.ruleVer ?? "",
@@ -3208,7 +3270,8 @@ function checklistToHdf(cl, resultsChecksum, generatorName) {
3208
3270
  return hdf;
3209
3271
  }
3210
3272
  function stigToBaseline(s, resultsChecksum, scanTime) {
3211
- const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map((v) => vulnToRequirement(v, scanTime)), { resultsChecksum });
3273
+ const requirements = s.vulns.map((v) => vulnToRequirement(v, scanTime));
3274
+ const baseline = createMinimalBaseline("STIG Checklist Scan", requirements, { resultsChecksum });
3212
3275
  if (s.title) baseline.title = s.title;
3213
3276
  if (s.version) baseline.version = s.version;
3214
3277
  const ext = baselineExtensions(s);
@@ -3344,6 +3407,9 @@ function requirementEffectiveStatus(req) {
3344
3407
  function hdfToChecklist(input) {
3345
3408
  const hdf = requireHdfResults(input, "hdf to checklist").doc;
3346
3409
  if (hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
3410
+ hdf.baselines.forEach((bl, i) => {
3411
+ if (!bl || !Array.isArray(bl.requirements) || bl.requirements.length === 0) throw new Error(`hdf to checklist: baseline ${i + 1} has no requirements`);
3412
+ });
3347
3413
  const ext = hdf.extensions ?? {};
3348
3414
  const format = strVal(ext, "checklistFormat") || "ckl";
3349
3415
  const cklbVersion = strVal(ext, "cklbVersion");
@@ -3470,7 +3536,7 @@ function formatOverride(o) {
3470
3536
  }
3471
3537
  function overrideSeverity(req) {
3472
3538
  for (const o of req.statusOverrides ?? []) if (o.impact) return {
3473
- severity: cklSeverityFromImpact(o.impact.value) || "low",
3539
+ severity: cklSeverityOrFloor(o.impact.value),
3474
3540
  justification: o.reason ?? ""
3475
3541
  };
3476
3542
  return {
@@ -3487,11 +3553,14 @@ function cklSeverityFromImpact(impact) {
3487
3553
  default: return "";
3488
3554
  }
3489
3555
  }
3556
+ function cklSeverityOrFloor(impact) {
3557
+ return cklSeverityFromImpact(impact) || "low";
3558
+ }
3490
3559
  function resolveSeverity(req, tags) {
3491
3560
  const tagSev = strVal(tags, "severity");
3492
3561
  if (tagSev) return tagSev;
3493
3562
  if (req.severity) return String(req.severity).toLowerCase();
3494
- return cklSeverityFromImpact(req.impact ?? 0);
3563
+ return cklSeverityOrFloor(req.impact ?? 0);
3495
3564
  }
3496
3565
  function resolveCcis(tags) {
3497
3566
  const direct = strSlice(tags, "cci");
@@ -4120,9 +4189,10 @@ async function convertSemgrepToHdf(input, converterVersion = "1.0.0") {
4120
4189
  }
4121
4190
  if (report.errors.length > 0) requirements.push(buildErrorsRequirement(report.errors, startTime));
4122
4191
  requirements.push(buildCoverageRequirement(report, groups.size, startTime));
4192
+ const resultsChecksum = await inputChecksum(input);
4123
4193
  const baseline = createMinimalBaseline("Semgrep Scan", requirements, {
4124
4194
  title: "Semgrep static analysis scan",
4125
- resultsChecksum: await inputChecksum(input)
4195
+ resultsChecksum
4126
4196
  });
4127
4197
  return buildHdfResults({
4128
4198
  generatorName: "semgrep-to-hdf",
@@ -4363,9 +4433,10 @@ function convertSingleProject(report, resultsChecksum, scanTime) {
4363
4433
  const target = report.projectName ?? report.path ?? "project";
4364
4434
  requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, scanTime));
4365
4435
  }
4436
+ const title = `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`;
4366
4437
  return createMinimalBaseline("Snyk Scan", requirements, {
4367
4438
  resultsChecksum,
4368
- title: `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`,
4439
+ title,
4369
4440
  summary: report.summary
4370
4441
  });
4371
4442
  }
@@ -4558,7 +4629,8 @@ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
4558
4629
  message,
4559
4630
  startTime
4560
4631
  };
4561
- const tags = buildNistCciTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS, nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS));
4632
+ const cciTags = nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
4633
+ const tags = buildNistCciTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS, cciTags);
4562
4634
  markUnratedSeverity(tags, severity);
4563
4635
  const requirement = {
4564
4636
  id: isIgnored ? `Grype-Ignored-Match/${cveId}` : `Grype/${cveId}`,
@@ -4597,8 +4669,13 @@ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
4597
4669
  if (kev) requirement.kev = kev;
4598
4670
  return requirement;
4599
4671
  }
4672
+ function grypeTargetName(target) {
4673
+ if (typeof target === "string") return target || "Grype Scan";
4674
+ return target?.userInput || "Grype Scan";
4675
+ }
4600
4676
  function buildComponent$2(report, targetName) {
4601
- const t = report.source?.target;
4677
+ const rawTarget = report.source?.target;
4678
+ const t = typeof rawTarget === "object" ? rawTarget : void 0;
4602
4679
  const firstRepoDigest = t?.repoDigests?.find((d) => d);
4603
4680
  const firstTag = t?.tags?.find((tag) => tag);
4604
4681
  const isImage = Boolean(t && (t.imageID || t.manifestDigest || firstRepoDigest || firstTag));
@@ -4625,7 +4702,7 @@ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
4625
4702
  const resultsChecksum = await inputChecksum(input);
4626
4703
  const grypeData = parseJSON(input);
4627
4704
  const requirements = [];
4628
- const targetName = grypeData.source?.target?.userInput || "Grype Scan";
4705
+ const targetName = grypeTargetName(grypeData.source?.target);
4629
4706
  const scanTime = grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null;
4630
4707
  const resultStart = scanTime ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
4631
4708
  if (grypeData.matches && grypeData.matches.length > 0) {
@@ -5680,10 +5757,13 @@ function buildResult(r) {
5680
5757
  const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
5681
5758
  const status = mapComplianceStatus(r.ComplianceType);
5682
5759
  const codeDesc = buildCodeDesc$8(q);
5683
- return createResult(status, buildResultMessage(codeDesc, r.Annotation, status), {
5760
+ const message = buildResultMessage(codeDesc, r.Annotation, status);
5761
+ const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
5762
+ const runTime = computeRunTime$1(r.ConfigRuleInvokedTime, r.ResultRecordedTime);
5763
+ return createResult(status, message, {
5684
5764
  codeDesc,
5685
- startTime: (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
5686
- runTime: computeRunTime$1(r.ConfigRuleInvokedTime, r.ResultRecordedTime)
5765
+ startTime,
5766
+ runTime
5687
5767
  });
5688
5768
  }
5689
5769
  /**
@@ -5747,8 +5827,9 @@ async function convertAwsConfigToHdf(input, converterVersion = "1.0.0") {
5747
5827
  /* v8 ignore next -- truncation only triggers with >100K items */
5748
5828
  if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
5749
5829
  checkRevisionAlignment(limitedRules);
5830
+ const requirements = limitedRules.map(buildRequirement$15);
5750
5831
  const baseline = {
5751
- ...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$15), { resultsChecksum }),
5832
+ ...createMinimalBaseline("AWS Config", requirements, { resultsChecksum }),
5752
5833
  title: "AWS Config Compliance Results",
5753
5834
  version: "1.0.0",
5754
5835
  maintainer: "Amazon Web Services"
@@ -5826,11 +5907,25 @@ function renderCodeBlock(codeBlock) {
5826
5907
  /**
5827
5908
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
5828
5909
  */
5910
+ /**
5911
+ * Resolves a check's NIST controls and CCIs from the Checkov mapping dataset,
5912
+ * falling back to the static-analysis controls when unmapped. Mirrors Go's controlsFor.
5913
+ */
5914
+ function controlsFor(checkId) {
5915
+ const mapping = getCheckovCciNistMapping(checkId);
5916
+ if (mapping && mapping.nist.length > 0) return mapping;
5917
+ const fallback = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
5918
+ return {
5919
+ nist: fallback,
5920
+ cci: nistToCci(fallback)
5921
+ };
5922
+ }
5829
5923
  function buildRequirement$14(checkId, group, scanTime) {
5830
5924
  const checks = group.map((c) => c.check);
5831
5925
  const rep = checks[0];
5832
5926
  const impact = getImpact$8(rep.severity);
5833
- const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
5927
+ const { nist, cci } = controlsFor(checkId);
5928
+ const tags = buildNistCciTags(nist, cci);
5834
5929
  const checkTypes = [...new Set(group.map((c) => c.checkType).filter((t) => t !== ""))].sort();
5835
5930
  if (checkTypes.length > 0) tags["check_type"] = checkTypes;
5836
5931
  if (rep.bc_check_id) tags["bc_check_id"] = rep.bc_check_id;
@@ -5853,7 +5948,7 @@ function buildRequirement$14(checkId, group, scanTime) {
5853
5948
  if (Array.isArray(rep.file_line_range) && typeof rep.file_line_range[0] === "number") loc.line = rep.file_line_range[0];
5854
5949
  req.sourceLocation = loc;
5855
5950
  }
5856
- const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
5951
+ const controlType = deriveControlTypeFromTags(nist);
5857
5952
  if (controlType !== void 0) req.controlType = controlType;
5858
5953
  return req;
5859
5954
  }
@@ -6358,7 +6453,8 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
6358
6453
  requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
6359
6454
  }
6360
6455
  const baselineTitle = site["@name"] || site["@host"] ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
6361
- const baseline = createMinimalBaseline(multiSite ? `OWASP ZAP Scan: ${siteLabel(site, i)}` : "OWASP ZAP Scan", requirements, {
6456
+ const scanLabel = multiSite ? `OWASP ZAP Scan: ${siteLabel(site, i)}` : "OWASP ZAP Scan";
6457
+ const baseline = createMinimalBaseline(scanLabel, requirements, {
6362
6458
  resultsChecksum,
6363
6459
  title: baselineTitle,
6364
6460
  summary
@@ -6967,8 +7063,9 @@ function statusOf(req) {
6967
7063
  expiresAt: getStr(o, "expiresAt") || void 0
6968
7064
  }];
6969
7065
  });
7066
+ const impact = typeof req.impact === "number" ? req.impact : 1;
6970
7067
  const rollup = computeEffectiveStatus({
6971
- impact: typeof req.impact === "number" ? req.impact : 1,
7068
+ impact,
6972
7069
  resultStatuses: resultStatuses(req),
6973
7070
  overrides: overrideInputs
6974
7071
  });
@@ -7083,11 +7180,17 @@ var RawNumber = class {
7083
7180
  this.token = token;
7084
7181
  }
7085
7182
  };
7086
- /** Wrap a number as a RawNumber whose token always bears a decimal point. */
7183
+ /**
7184
+ * Wrap a number as a RawNumber whose token always bears a decimal point.
7185
+ *
7186
+ * formatJsonNumber renders positionally at every magnitude, matching Go's
7187
+ * strconv 'f'. String() would switch to exponent notation outside
7188
+ * [1e-6, 1e21) and emit a token with no decimal point at all — defeating the
7189
+ * one thing this function exists to guarantee, and diverging from the Go peer.
7190
+ */
7087
7191
  function floatNumber(f) {
7088
- let s = String(f);
7089
- if (!/[.eE]/.test(s)) s += ".0";
7090
- return new RawNumber(s);
7192
+ const s = Object.is(f, -0) ? "0" : formatJsonNumber(f);
7193
+ return new RawNumber(s.includes(".") ? s : s + ".0");
7091
7194
  }
7092
7195
  const RAWNUM_MARK = String.fromCharCode(1);
7093
7196
  const RAWNUM_RE = /"\\u0001(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\\u0001"/g;
@@ -8235,13 +8338,15 @@ function buildFinding$1(req, baseline, docTimestamp, tool, generator, component,
8235
8338
  if (device) finding.device = device;
8236
8339
  setIf(finding, "raw_data", getStr(req, "code"));
8237
8340
  setIf(finding, "message", firstResultMessage$1(req));
8238
- const evidences = buildEvidences(req);
8239
- if (evidences.length > 0) finding.evidences = evidences;
8240
8341
  setIf(finding, "status_detail", st.rollup);
8241
- const remediation = remediationText$1(req);
8242
- if (remediation !== "") finding.remediation = { desc: remediation };
8243
8342
  if (hasCVSS) finding.vulnerabilities = buildVulnerabilities(cvssList, req);
8244
- else finding.compliance = buildCompliance(req, baseline, title, st.raw);
8343
+ else {
8344
+ const evidences = buildEvidences(req);
8345
+ if (evidences.length > 0) finding.evidences = evidences;
8346
+ const remediation = remediationText$1(req);
8347
+ if (remediation !== "") finding.remediation = { desc: remediation };
8348
+ finding.compliance = buildCompliance(req, baseline, title, st.raw);
8349
+ }
8245
8350
  return finding;
8246
8351
  }
8247
8352
  function severityID(req) {
@@ -8585,8 +8690,10 @@ function buildFinding(req, ctx) {
8585
8690
  const rem = remediation(req);
8586
8691
  if (rem) finding.Remediation = rem;
8587
8692
  const refs = allRefURLs(req);
8588
- if (refs.length > 0) if (vulns.length > 0) vulns[0].ReferenceUrls = refs;
8589
- else finding.SourceUrl = refs[0];
8693
+ if (refs.length > 0) {
8694
+ if (vulns.length > 0) vulns[0].ReferenceUrls = refs;
8695
+ else finding.SourceUrl = refs[0];
8696
+ }
8590
8697
  if (vulns.length > 0) finding.Vulnerabilities = vulns;
8591
8698
  return finding;
8592
8699
  }
@@ -8814,9 +8921,7 @@ async function convertSplunkToHdf(input, converterVersion = "1.0.0") {
8814
8921
  case "profile":
8815
8922
  profiles.push(event);
8816
8923
  break;
8817
- case "control":
8818
- controls.push(event);
8819
- break;
8924
+ case "control": controls.push(event);
8820
8925
  }
8821
8926
  if (headers.length !== 1) throw new Error(`Expected 1 header event, got ${headers.length}`);
8822
8927
  const header = headers[0];
@@ -9235,9 +9340,10 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
9235
9340
  const ts = startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
9236
9341
  requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
9237
9342
  }
9343
+ const baselineTitle = `GitLab ${label} Security Scan`;
9238
9344
  const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
9239
9345
  resultsChecksum,
9240
- title: `GitLab ${label} Security Scan`,
9346
+ title: baselineTitle,
9241
9347
  summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
9242
9348
  });
9243
9349
  const tool = { name: scannerName };
@@ -9728,10 +9834,11 @@ async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
9728
9834
  const target = findGitRepoURL(limitedFindings) ?? limitedFindings[0]?.SourceName ?? "the target source";
9729
9835
  requirements.push(buildNoFindingsRequirement("trufflehog-no-findings", `TruffleHog scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
9730
9836
  }
9837
+ const baselineTitle = `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`;
9731
9838
  const hdf = {
9732
9839
  baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
9733
9840
  resultsChecksum,
9734
- title: `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`
9841
+ title: baselineTitle
9735
9842
  })],
9736
9843
  generator: {
9737
9844
  name: "trufflehog-to-hdf",
@@ -9861,10 +9968,12 @@ async function convertHipcheckToHdf(input, converterVersion = "1.0.0") {
9861
9968
  for (const e of report.errored ?? []) requirements.push(buildErroredRequirement(e, startTime));
9862
9969
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("hipcheck-no-findings", `Hipcheck analyzed ${repoIdent(report)} and reported zero analyses.`, startTime));
9863
9970
  const summary = buildSummary$1(report.recommendation ?? {});
9971
+ const title = `Hipcheck analysis of ${repoIdent(report)} @ ${report.repo_head ?? ""}`;
9972
+ const resultsChecksum = await inputChecksum(input);
9864
9973
  const baseline = createMinimalBaseline("Hipcheck Scan", requirements, {
9865
- title: `Hipcheck analysis of ${repoIdent(report)} @ ${report.repo_head ?? ""}`,
9974
+ title,
9866
9975
  summary,
9867
- resultsChecksum: await inputChecksum(input)
9976
+ resultsChecksum
9868
9977
  });
9869
9978
  const ident = repoIdent(report);
9870
9979
  return buildHdfResults({
@@ -9996,9 +10105,10 @@ async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
9996
10105
  for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues, resultStartTime));
9997
10106
  const targetName = limitedIssues.length > 0 ? (limitedIssues[0].host?.["#text"] ?? "Unknown").trim() : "Unknown";
9998
10107
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("burpsuite-no-findings", `Burp Suite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
10108
+ const title = `BurpSuite Scan: ${targetName}`;
9999
10109
  const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
10000
10110
  resultsChecksum,
10001
- title: `BurpSuite Scan: ${targetName}`
10111
+ title
10002
10112
  });
10003
10113
  const tool = { name: "BurpSuite" };
10004
10114
  if (burpVersion) tool.version = burpVersion;
@@ -10174,9 +10284,11 @@ function buildScanTarget(f) {
10174
10284
  const assetType = (f["Asset Type"] ?? "").trim();
10175
10285
  const asset = (f["Asset"] ?? "").trim();
10176
10286
  let name = instance;
10177
- if (!name) if (ip && port) name = `${ip}:${port}`;
10178
- else if (ip) name = ip;
10179
- else name = asset;
10287
+ if (!name) {
10288
+ if (ip && port) name = `${ip}:${port}`;
10289
+ else if (ip) name = ip;
10290
+ else name = asset;
10291
+ }
10180
10292
  if (!name) return;
10181
10293
  const comp = {
10182
10294
  name,
@@ -10215,7 +10327,8 @@ function parseDate(dateStr) {
10215
10327
  const trimmed = dateStr.trim();
10216
10328
  if (!trimmed) return ZERO_DATE;
10217
10329
  const month = /^([A-Z][a-z]{2}) (\d{1,2}) (\d{4}) (\d{2}:\d{2})$/.exec(trimmed);
10218
- return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO_DATE;
10330
+ const normalized = month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed;
10331
+ return parseTimestamp(normalized) ?? ZERO_DATE;
10219
10332
  }
10220
10333
  /**
10221
10334
  * Derives the top-level HDF timestamp from the source, preferring the "Start
@@ -10256,7 +10369,8 @@ function buildRequirement$10(checkID, findings, hasStatus) {
10256
10369
  data: formatDesc$1(rep)
10257
10370
  }];
10258
10371
  const results = findings.map((f) => {
10259
- return createResult(hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed, void 0, {
10372
+ const status = hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed;
10373
+ return createResult(status, void 0, {
10260
10374
  codeDesc: f["Details"] ?? "",
10261
10375
  startTime: parseDate(f["Date"] ?? ""),
10262
10376
  backtrace: getBacktrace(f["Result Status"] ?? "")
@@ -10302,10 +10416,12 @@ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
10302
10416
  const requirements = [];
10303
10417
  for (const [checkID, findingGroup] of groups) requirements.push(buildRequirement$10(checkID, findingGroup, hasStatus));
10304
10418
  const firstFinding = limitedFindings[0];
10419
+ const title = firstFinding["Job Name"] ?? "";
10420
+ const summary = formatSummary(firstFinding);
10305
10421
  const baseline = createMinimalBaseline("DBProtect Scan", requirements, {
10306
10422
  resultsChecksum,
10307
- title: firstFinding["Job Name"] ?? "",
10308
- summary: formatSummary(firstFinding)
10423
+ title,
10424
+ summary
10309
10425
  });
10310
10426
  const policy = firstFinding["Policy"] ?? "";
10311
10427
  if (policy) baseline.version = policy;
@@ -10323,7 +10439,9 @@ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
10323
10439
  //#region converters/twistlock-to-hdf/typescript/converter.ts
10324
10440
  const TWISTLOCK_ALIASES = {
10325
10441
  important: .9,
10326
- moderate: .5
10442
+ moderate: .5,
10443
+ unimportant: .3,
10444
+ negligible: 0
10327
10445
  };
10328
10446
  function twistlockSeverityToImpact(severity) {
10329
10447
  return severityToImpactWithAliases(severity, TWISTLOCK_ALIASES, .5);
@@ -10551,10 +10669,12 @@ function convertSingleResult(result, resultsChecksum) {
10551
10669
  const target = result.name ?? result.repository ?? result.id ?? "scan target";
10552
10670
  requirements.push(buildNoFindingsRequirement("twistlock-no-findings", `Twistlock scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
10553
10671
  }
10672
+ const title = buildTitle$1(result);
10673
+ const summary = buildSummary(result);
10554
10674
  return createMinimalBaseline("Twistlock Scan", requirements, {
10555
10675
  resultsChecksum,
10556
- title: buildTitle$1(result),
10557
- summary: buildSummary(result)
10676
+ title,
10677
+ summary
10558
10678
  });
10559
10679
  }
10560
10680
  /**
@@ -10802,9 +10922,10 @@ async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
10802
10922
  const projectName = parsed.project?.name ?? parsed.project?.uuid ?? "";
10803
10923
  requirements.push(buildNoFindingsRequirement("deptrack-no-findings", `Dependency-Track analyzed ${projectName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
10804
10924
  }
10925
+ const title = `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`;
10805
10926
  const baseline = createMinimalBaseline("Dependency-Track Scan", requirements, {
10806
10927
  resultsChecksum,
10807
- title: `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`,
10928
+ title,
10808
10929
  summary: parsed.project?.description
10809
10930
  });
10810
10931
  const targetName = parsed.project?.name ?? parsed.project?.uuid ?? "";
@@ -11383,9 +11504,10 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
11383
11504
  }
11384
11505
  const target = targetNameFromReport(scan.report);
11385
11506
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
11507
+ const title = imageTitle(scan.report);
11386
11508
  const baseline = createMinimalBaseline("NeuVector Scan", requirements, {
11387
11509
  resultsChecksum,
11388
- title: imageTitle(scan.report)
11510
+ title
11389
11511
  });
11390
11512
  const cmds = scan.report.cmds ?? [];
11391
11513
  if (cmds.length > 0) baseline.extensions = { neuvector: { cmds } };
@@ -11624,12 +11746,15 @@ async function convertFortifyToHdf(input, converterVersion = "1.0.0") {
11624
11746
  });
11625
11747
  const targetName = fvdl.Build?.SourceBasePath ?? fvdl.Build?.BuildID ?? "Unknown";
11626
11748
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("fortify-no-findings", `Fortify scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
11749
+ const title = "Fortify Static Analyzer Scan";
11750
+ const summary = `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`;
11751
+ const version = fvdl.EngineData?.EngineVersion ?? "";
11627
11752
  const hdfResult = {
11628
11753
  baselines: [createMinimalBaseline("Fortify Scan", requirements, {
11629
11754
  resultsChecksum,
11630
- title: "Fortify Static Analyzer Scan",
11631
- summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
11632
- version: fvdl.EngineData?.EngineVersion ?? "",
11755
+ title,
11756
+ summary,
11757
+ version,
11633
11758
  status: "loaded"
11634
11759
  })],
11635
11760
  components: [{
@@ -11875,9 +12000,11 @@ function groupByHostname(records) {
11875
12000
  * Builds an HDF baseline from all records for a single host.
11876
12001
  */
11877
12002
  function buildBaseline(hostname, records, resultsChecksum, scanTime) {
11878
- return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec, scanTime)), {
12003
+ const requirements = limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec, scanTime));
12004
+ const title = `Prisma Cloud Scan (${hostname})`;
12005
+ return createMinimalBaseline("Prisma Cloud Scan", requirements, {
11879
12006
  resultsChecksum,
11880
- title: `Prisma Cloud Scan (${hostname})`
12007
+ title
11881
12008
  });
11882
12009
  }
11883
12010
  /**
@@ -12143,6 +12270,7 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
12143
12270
  const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date();
12144
12271
  requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
12145
12272
  }
12273
+ const title = `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`;
12146
12274
  return buildHdfResults({
12147
12275
  generatorName: "netsparker-to-hdf",
12148
12276
  converterVersion,
@@ -12150,7 +12278,7 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
12150
12278
  timestamp,
12151
12279
  baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
12152
12280
  resultsChecksum,
12153
- title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
12281
+ title
12154
12282
  })],
12155
12283
  components: [{
12156
12284
  name: targetName,
@@ -12222,7 +12350,9 @@ function getNistControls(ruleID) {
12222
12350
  return ctrl.split("|");
12223
12351
  }
12224
12352
  /**
12225
- * Collapses all service findings into a flat list of (ruleID, finding) pairs.
12353
+ * Flattens every service's findings into one ordered list, services then rule
12354
+ * keys sorted for deterministic output. Each entry stands alone, so no finding
12355
+ * can be displaced by another service's.
12226
12356
  */
12227
12357
  function collapseFindings(report) {
12228
12358
  const result = [];
@@ -12235,15 +12365,33 @@ function collapseFindings(report) {
12235
12365
  const ruleNames = Object.keys(svc.findings).sort();
12236
12366
  for (const ruleName of ruleNames) {
12237
12367
  const finding = svc.findings[ruleName];
12238
- if (finding) result.push([ruleName, finding]);
12368
+ if (finding) result.push({
12369
+ service: serviceName,
12370
+ ruleID: ruleName,
12371
+ finding
12372
+ });
12239
12373
  }
12240
12374
  }
12241
12375
  return result;
12242
12376
  }
12243
12377
  /**
12378
+ * Returns the HDF requirement ID for each finding. A rule key is the ID on its
12379
+ * own, which is what every ScoutSuite report produces: a key names one rule file,
12380
+ * whose path binds it to a single service. Should a key arrive under two services
12381
+ * anyway, both are qualified with their service so the IDs stay unique —
12382
+ * qualifying only on collision keeps ordinary output unchanged rather than
12383
+ * renaming every requirement for a case that does not occur. Mirrors Go's
12384
+ * assignRequirementIDs.
12385
+ */
12386
+ function assignRequirementIDs(found) {
12387
+ const occurrences = /* @__PURE__ */ new Map();
12388
+ for (const sf of found) occurrences.set(sf.ruleID, (occurrences.get(sf.ruleID) ?? 0) + 1);
12389
+ return found.map((sf) => (occurrences.get(sf.ruleID) ?? 0) > 1 ? `${sf.service}:${sf.ruleID}` : sf.ruleID);
12390
+ }
12391
+ /**
12244
12392
  * Builds a single EvaluatedRequirement from a ScoutSuite finding.
12245
12393
  */
12246
- function buildRequirement$2(ruleID, finding, startTime) {
12394
+ function buildRequirement$2(id, ruleID, finding, startTime) {
12247
12395
  const nist = getNistControls(ruleID);
12248
12396
  const tags = buildNistCciTags(nist, nistToCci(nist));
12249
12397
  const compliance = complianceStrings(finding.compliance);
@@ -12258,11 +12406,13 @@ function buildRequirement$2(ruleID, finding, startTime) {
12258
12406
  });
12259
12407
  const refs = [];
12260
12408
  for (const url of finding.references ?? []) if (url) refs.push({ url });
12261
- const resultObj = createResult(getStatus$1(finding.checked_items, finding.flagged_items), getMessage(finding.checked_items, finding.flagged_items, finding.items), {
12409
+ const status = getStatus$1(finding.checked_items, finding.flagged_items);
12410
+ const message = getMessage(finding.checked_items, finding.flagged_items, finding.items);
12411
+ const resultObj = createResult(status, message, {
12262
12412
  codeDesc: finding.description,
12263
12413
  startTime: (startTime ? parseTimestamp(startTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
12264
12414
  });
12265
- const req = createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
12415
+ const req = createRequirement(id, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
12266
12416
  const controlType = deriveControlTypeFromTags(nist);
12267
12417
  if (controlType !== void 0) req.controlType = controlType;
12268
12418
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -12284,12 +12434,15 @@ async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
12284
12434
  const resultsChecksum = await inputChecksum(jsonStr);
12285
12435
  const report = parseJSON(jsonStr);
12286
12436
  if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
12287
- const requirements = limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time));
12437
+ const limitedPairs = limitArrayWithWarning(collapseFindings(report), "finding");
12438
+ const ids = assignRequirementIDs(limitedPairs);
12439
+ const requirements = limitedPairs.map((sf, i) => buildRequirement$2(ids[i], sf.ruleID, sf.finding, report.last_run.time));
12288
12440
  const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
12289
12441
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("scoutsuite-no-findings", `ScoutSuite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
12442
+ const title = `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`;
12290
12443
  const baseline = createMinimalBaseline("ScoutSuite Scan", requirements, {
12291
12444
  resultsChecksum,
12292
- title: `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`,
12445
+ title,
12293
12446
  summary: report.last_run.ruleset_about
12294
12447
  });
12295
12448
  return buildHdfResults({
@@ -12357,17 +12510,17 @@ function buildCodeDesc$1(section, scannerName) {
12357
12510
  parts.push(`body:${bodyToString(section.body)}`);
12358
12511
  parts.push(`body_format:${section.body_format ?? ""}`);
12359
12512
  parts.push(`classification:${section.classification ?? ""}`);
12360
- parts.push(`depth:${section.depth ?? 0}`);
12513
+ parts.push(`depth:${formatJsonNumber(section.depth ?? 0)}`);
12361
12514
  if (section.heuristic) {
12362
12515
  parts.push(`heuristic_heur_id:${section.heuristic.heur_id ?? ""}`);
12363
- parts.push(`heuristic_score:${section.heuristic.score ?? 0}`);
12516
+ parts.push(`heuristic_score:${formatJsonNumber(section.heuristic.score ?? 0)}`);
12364
12517
  parts.push(`heuristic_name:${section.heuristic.name ?? ""}`);
12365
12518
  }
12366
12519
  } else if (scannerName === "CodeQuality") {
12367
12520
  parts.push(`body:${bodyToString(section.body)}`);
12368
12521
  parts.push(`body_format:${section.body_format ?? ""}`);
12369
12522
  parts.push(`classification:${section.classification ?? ""}`);
12370
- parts.push(`depth:${section.depth ?? 0}`);
12523
+ parts.push(`depth:${formatJsonNumber(section.depth ?? 0)}`);
12371
12524
  parts.push(`title_text:${section.title_text ?? ""}`);
12372
12525
  } else return JSON.stringify(section);
12373
12526
  return parts.join("\n");
@@ -12475,11 +12628,13 @@ function buildRequirementFromResult(result, filename) {
12475
12628
  * Builds an HDF baseline for a single scanner's results.
12476
12629
  */
12477
12630
  function buildScannerBaseline(scannerName, results, shaMap, resultsChecksum) {
12478
- return createMinimalBaseline("Conveyor Scan", results.map((result) => {
12631
+ const requirements = results.map((result) => {
12479
12632
  return buildRequirementFromResult(result, shaMap.get(result.sha256) ?? "");
12480
- }), {
12633
+ });
12634
+ const title = `Conveyor Scan (${scannerName})`;
12635
+ return createMinimalBaseline("Conveyor Scan", requirements, {
12481
12636
  resultsChecksum,
12482
- title: `Conveyor Scan (${scannerName})`
12637
+ title
12483
12638
  });
12484
12639
  }
12485
12640
  /**
@@ -12574,7 +12729,8 @@ function decodeXmlEntities(s) {
12574
12729
  /** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
12575
12730
  function parseVeracodeTimestamp(ts) {
12576
12731
  if (!ts) return void 0;
12577
- return parseTimestamp(ts.replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
12732
+ const normalized = ts.replace(" UTC", "Z").replace(" ", "T");
12733
+ return parseTimestamp(normalized) ?? void 0;
12578
12734
  }
12579
12735
  /** Format description paragraphs into text. */
12580
12736
  function formatDesc(desc) {
@@ -13095,11 +13251,12 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
13095
13251
  }
13096
13252
  if (cs.on === "true") tags.on = true;
13097
13253
  else if (cs.on === "false") tags.on = false;
13098
- const req = createRequirement(id, title, descriptions, impact, [{
13254
+ const results = [{
13099
13255
  status,
13100
13256
  codeDesc: cs.implementationStatus || "No implementation status provided",
13101
13257
  startTime: parseTimestamp(cs.lastSynced ?? "") ?? parseTimestamp(createdDateTime ?? "") ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
13102
- }], {
13258
+ }];
13259
+ const req = createRequirement(id, title, descriptions, impact, results, {
13103
13260
  tags,
13104
13261
  ...refs.length > 0 ? { refs } : {}
13105
13262
  });
@@ -13464,9 +13621,10 @@ function formatEvidence$1(evidence) {
13464
13621
  for (const ev of evidence) {
13465
13622
  const odataType = ev["@odata.type"] ?? "";
13466
13623
  if (odataType.includes("deviceEvidence")) parts.push(`Device: ${ev.deviceDnsName ?? "unknown"} (OS: ${ev.osPlatform ?? "unknown"})`);
13467
- else if (odataType.includes("processEvidence")) if (ev.imageFile) parts.push(`Process: ${ev.imageFile.filePath ?? ""}\\${ev.imageFile.fileName ?? ""} (Command: ${ev.processCommandLine ?? ""})`);
13468
- else parts.push(`Process: (Command: ${ev.processCommandLine ?? ""})`);
13469
- else if (odataType.includes("fileEvidence")) parts.push(`File: ${ev.filePath ?? ""}\\${ev.fileName ?? ""}`);
13624
+ else if (odataType.includes("processEvidence")) {
13625
+ if (ev.imageFile) parts.push(`Process: ${ev.imageFile.filePath ?? ""}\\${ev.imageFile.fileName ?? ""} (Command: ${ev.processCommandLine ?? ""})`);
13626
+ else parts.push(`Process: (Command: ${ev.processCommandLine ?? ""})`);
13627
+ } else if (odataType.includes("fileEvidence")) parts.push(`File: ${ev.filePath ?? ""}\\${ev.fileName ?? ""}`);
13470
13628
  else parts.push(`Evidence type: ${odataType}`);
13471
13629
  }
13472
13630
  return parts.join("\n");
@@ -13671,12 +13829,18 @@ function convertHdfToXccdf(input) {
13671
13829
  const hdfData = parseHdf(input);
13672
13830
  if (!hdfData || typeof hdfData !== "object" || !("baselines" in hdfData)) throw new Error("Invalid HDF structure: missing baselines field");
13673
13831
  if (!Array.isArray(hdfData.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
13674
- return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ Benchmark: buildBenchmarkObj(hdfData) }, BUILD_OPTIONS)}`;
13832
+ const xmlObj = { Benchmark: buildBenchmarkObj(hdfData) };
13833
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml(xmlObj, BUILD_OPTIONS)}`;
13675
13834
  }
13676
13835
  /** Wrap a primitive value to force it as a text node in XML output. */
13677
13836
  function wrap(value) {
13678
13837
  return { "#text": value };
13679
13838
  }
13839
+ /** Map an XCCDF result to the severity of its message: an error result is the
13840
+ * checking engine reporting a fault, any other is check output. */
13841
+ function messageSeverity(status) {
13842
+ return status === "error" ? "error" : "info";
13843
+ }
13680
13844
  /** Map HDF impact (0.0-1.0) to XCCDF severity via the shared band mapper. The
13681
13845
  * XCCDF vocabulary has no critical band, so critical folds into high, and its
13682
13846
  * zero band is spelled 'info'. */
@@ -13904,7 +14068,8 @@ function resultTimeWindow(baseline) {
13904
14068
  let first;
13905
14069
  let last;
13906
14070
  for (const req of baseline.requirements) for (const res of req.results) {
13907
- const parsed = parseTimestamp(typeof res.startTime === "string" ? res.startTime : res.startTime.toISOString());
14071
+ const text = typeof res.startTime === "string" ? res.startTime : res.startTime.toISOString();
14072
+ const parsed = parseTimestamp(text);
13908
14073
  if (!parsed) continue;
13909
14074
  const at = parsed.getTime();
13910
14075
  const canonical = formatTimestamp(parsed);
@@ -13972,7 +14137,10 @@ function buildTestResultObj(hdfData, baseline) {
13972
14137
  rr[`${ATTR}time`] = parsedStart ? formatTimestamp(parsedStart) : rawStart;
13973
14138
  if (stigId) rr[`${ATTR}version`] = stigId;
13974
14139
  rr.result = wrap(status);
13975
- if (result.message) rr.message = wrap(result.message);
14140
+ if (result.message) rr.message = {
14141
+ [`${ATTR}severity`]: messageSeverity(status),
14142
+ "#text": result.message
14143
+ };
13976
14144
  if (result.codeDesc) rr.check = {
13977
14145
  [`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
13978
14146
  "check-content": wrap(result.codeDesc)
@@ -13990,6 +14158,752 @@ function buildTestResultObj(hdfData, baseline) {
13990
14158
  return testResult;
13991
14159
  }
13992
14160
  //#endregion
14161
+ //#region converters/oscal-to-hdf/go/oscal-vocabulary.json
14162
+ var oscal_vocabulary_default = {
14163
+ $comment: "The HDF OSCAL extension vocabulary (ADR-0014 §1.5): every prop an hdf-libs OSCAL exporter emits or importer reads by name, with the namespace it belongs to. The Go oscal package embeds this file and the TypeScript peer imports it; it is the only place a prop name or the HDF namespace URI is defined. 'objects' are OSCAL JSON paths; 'hdfField' is the HDF path the prop carries, or null when it carries none; 'legacy' is true exactly for the names exporters emitted without ns before ADR-0014, which the read helpers accept without ns for one minor release (§1.4).",
14164
+ namespace: "https://mitre.github.io/hdf-libs/ns/oscal",
14165
+ defaultNamespace: "http://csrc.nist.gov/ns/oscal",
14166
+ props: [
14167
+ {
14168
+ "name": "hdf-requirement-id",
14169
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14170
+ "objects": ["assessment-results.results[].findings[]", "plan-of-action-and-milestones.risks[]"],
14171
+ "meaning": "The HDF requirement id the finding assesses, or the requirement id of the override the POA&M risk represents, exactly as the source recorded it; omitted when the id is empty. The finding target-id carries an OSCAL token encoding of it, which is not injective, and the POA&M item and risk titles are display text.",
14172
+ "valueFormat": "String (§1.7)",
14173
+ "hdfField": "baselines[].requirements[].id, overrides[].requirementId",
14174
+ "legacy": true
14175
+ },
14176
+ {
14177
+ "name": "baseline-version",
14178
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14179
+ "objects": ["assessment-results.results[]"],
14180
+ "meaning": "The version of the HDF baseline the result reports.",
14181
+ "valueFormat": "String (§1.7)",
14182
+ "hdfField": "baselines[].version",
14183
+ "legacy": true
14184
+ },
14185
+ {
14186
+ "name": "nist",
14187
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14188
+ "objects": ["assessment-results.results[].findings[]"],
14189
+ "meaning": "A NIST SP 800-53 control the requirement maps to, one prop per mapping, in source order.",
14190
+ "valueFormat": "String (§1.7), NIST control notation as the source spelled it",
14191
+ "hdfField": "baselines[].requirements[].tags.nist[]",
14192
+ "legacy": true
14193
+ },
14194
+ {
14195
+ "name": "cci",
14196
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14197
+ "objects": ["assessment-results.results[].findings[]"],
14198
+ "meaning": "A DISA Control Correlation Identifier the requirement maps to, one prop per mapping, in source order.",
14199
+ "valueFormat": "String (§1.7)",
14200
+ "hdfField": "baselines[].requirements[].tags.cci[]",
14201
+ "legacy": true
14202
+ },
14203
+ {
14204
+ "name": "control-type",
14205
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14206
+ "objects": ["assessment-results.results[].findings[]"],
14207
+ "meaning": "The requirement's control type.",
14208
+ "valueFormat": "One of policy, procedure, technical, management, operational",
14209
+ "hdfField": "baselines[].requirements[].controlType",
14210
+ "legacy": true
14211
+ },
14212
+ {
14213
+ "name": "verification-method",
14214
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14215
+ "objects": ["assessment-results.results[].findings[]"],
14216
+ "meaning": "How the requirement is verified.",
14217
+ "valueFormat": "One of automated, manual-by-design, manual-pending-automation, hybrid",
14218
+ "hdfField": "baselines[].requirements[].verificationMethod",
14219
+ "legacy": true
14220
+ },
14221
+ {
14222
+ "name": "applicability",
14223
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14224
+ "objects": ["assessment-results.results[].findings[]"],
14225
+ "meaning": "Whether the requirement is required, optional or advisory.",
14226
+ "valueFormat": "One of required, optional, advisory",
14227
+ "hdfField": "baselines[].requirements[].applicability",
14228
+ "legacy": true
14229
+ },
14230
+ {
14231
+ "name": "cwe",
14232
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14233
+ "objects": ["assessment-results.results[].findings[]"],
14234
+ "meaning": "A CWE identifier associated with the requirement, one prop per identifier, in source order.",
14235
+ "valueFormat": "String (§1.7)",
14236
+ "hdfField": "baselines[].requirements[].cwe[]",
14237
+ "legacy": true
14238
+ },
14239
+ {
14240
+ "name": "epss-score",
14241
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14242
+ "objects": ["assessment-results.results[].findings[]"],
14243
+ "meaning": "The EPSS exploitation probability score.",
14244
+ "valueFormat": "Decimal number from 0 to 1",
14245
+ "hdfField": "baselines[].requirements[].epss.score",
14246
+ "legacy": true
14247
+ },
14248
+ {
14249
+ "name": "epss-percentile",
14250
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14251
+ "objects": ["assessment-results.results[].findings[]"],
14252
+ "meaning": "The EPSS percentile of the score.",
14253
+ "valueFormat": "Decimal number from 0 to 1",
14254
+ "hdfField": "baselines[].requirements[].epss.percentile",
14255
+ "legacy": true
14256
+ },
14257
+ {
14258
+ "name": "kev",
14259
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14260
+ "objects": ["assessment-results.results[].findings[]"],
14261
+ "meaning": "The vulnerability is in the CISA Known Exploited Vulnerabilities catalog; emitted only when it is.",
14262
+ "valueFormat": "The literal true",
14263
+ "hdfField": "baselines[].requirements[].kev.inKev",
14264
+ "legacy": true
14265
+ },
14266
+ {
14267
+ "name": "kev-due-date",
14268
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14269
+ "objects": ["assessment-results.results[].findings[]"],
14270
+ "meaning": "The remediation due date the KEV catalog sets for the vulnerability.",
14271
+ "valueFormat": "String (§1.7), a date as the source recorded it",
14272
+ "hdfField": "baselines[].requirements[].kev.dueDate",
14273
+ "legacy": true
14274
+ },
14275
+ {
14276
+ "name": "cvss-base-score",
14277
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14278
+ "objects": ["assessment-results.results[].findings[]"],
14279
+ "meaning": "A CVSS base score, one prop per CVSS record that carries one, in source order.",
14280
+ "valueFormat": "Decimal number from 0 to 10",
14281
+ "hdfField": "baselines[].requirements[].cvss[].baseScore",
14282
+ "legacy": true
14283
+ },
14284
+ {
14285
+ "name": "cvss-base-vector",
14286
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14287
+ "objects": ["assessment-results.results[].findings[]"],
14288
+ "meaning": "A CVSS base vector, one prop per CVSS record that carries one, in source order.",
14289
+ "valueFormat": "String (§1.7), a CVSS vector string",
14290
+ "hdfField": "baselines[].requirements[].cvss[].baseVector",
14291
+ "legacy": true
14292
+ },
14293
+ {
14294
+ "name": "reference",
14295
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14296
+ "objects": ["assessment-results.results[].findings[]"],
14297
+ "meaning": "A requirement reference that is plain text rather than a URL or URI (those become finding links).",
14298
+ "valueFormat": "String (§1.7)",
14299
+ "hdfField": "baselines[].requirements[].refs[].ref",
14300
+ "legacy": true
14301
+ },
14302
+ {
14303
+ "name": "check",
14304
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14305
+ "objects": ["assessment-results.results[].findings[]"],
14306
+ "meaning": "Pre-ADR only: a one-line preview of the requirement's check text, with the full text in remarks. No longer emitted; the check text is carried in observation relevant-evidence marked with description-label.",
14307
+ "valueFormat": "String (§1.7)",
14308
+ "hdfField": "baselines[].requirements[].descriptions[label=check].data",
14309
+ "legacy": true
14310
+ },
14311
+ {
14312
+ "name": "fix",
14313
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14314
+ "objects": ["assessment-results.results[].findings[]"],
14315
+ "meaning": "Pre-ADR only: a one-line preview of the requirement's fix text when no risk was emitted, with the full text in remarks. No longer emitted; the fix text is carried in a risk remediation or observation relevant-evidence marked with description-label.",
14316
+ "valueFormat": "String (§1.7)",
14317
+ "hdfField": "baselines[].requirements[].descriptions[label=fix].data",
14318
+ "legacy": true
14319
+ },
14320
+ {
14321
+ "name": "rationale",
14322
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14323
+ "objects": ["assessment-results.results[].findings[]"],
14324
+ "meaning": "Pre-ADR only: a one-line preview of the requirement's rationale text, with the full text in remarks. No longer emitted; the rationale is carried in the finding target description.",
14325
+ "valueFormat": "String (§1.7)",
14326
+ "hdfField": "baselines[].requirements[].descriptions[label=rationale].data",
14327
+ "legacy": true
14328
+ },
14329
+ {
14330
+ "name": "description-label",
14331
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14332
+ "objects": ["assessment-results.results[].observations[].relevant-evidence[]", "assessment-results.results[].risks[].remediations[]"],
14333
+ "meaning": "Marks the object as carrying the text of the HDF description with this label; evidence entries hold the full text in remarks, remediations in description.",
14334
+ "valueFormat": "One of check, fix",
14335
+ "hdfField": "baselines[].requirements[].descriptions[].label",
14336
+ "legacy": false
14337
+ },
14338
+ {
14339
+ "name": "empty-field",
14340
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14341
+ "objects": ["Any object that carries HDF props"],
14342
+ "meaning": "An optional HDF string field that is present but empty; the importer returns it present and empty (§1.7.3).",
14343
+ "valueFormat": "The HDF JSON property name, dot-separated relative to the OSCAL object that carries the marker (e.g. systemRef, checksum.value); for props that share a group, the field within that group (e.g. key, fixedInVersion) (§1.7.5)",
14344
+ "hdfField": null,
14345
+ "legacy": false
14346
+ },
14347
+ {
14348
+ "name": "absent-field",
14349
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14350
+ "objects": ["Any object that carries HDF props"],
14351
+ "meaning": "OSCAL requires a value the HDF field does not have, so the object holds display fallback text; the importer leaves the field absent and never reads the fallback as HDF data (§1.7.4).",
14352
+ "valueFormat": "The HDF JSON property name, dot-separated relative to the OSCAL object that carries the marker (e.g. systemRef, checksum.value); for props that share a group, the field within that group (e.g. key, fixedInVersion) (§1.7.5)",
14353
+ "hdfField": null,
14354
+ "legacy": false
14355
+ },
14356
+ {
14357
+ "name": "amendment-id",
14358
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14359
+ "objects": ["plan-of-action-and-milestones.metadata"],
14360
+ "meaning": "The identifier of the HDF amendments document.",
14361
+ "valueFormat": "String (§1.7), a UUID",
14362
+ "hdfField": "amendmentId",
14363
+ "legacy": true
14364
+ },
14365
+ {
14366
+ "name": "override-type",
14367
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14368
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14369
+ "meaning": "The type of the HDF override the risk represents. A risk carrying it in the HDF namespace is HDF-produced (ADR-0014 §4.3), so the importer reads every §4.6 field from its HDF props; a risk carrying it without ns is a pre-ADR export, which imports through the pre-ADR mapping.",
14370
+ "valueFormat": "An HDF override type, such as waiver, poam or falsePositive",
14371
+ "hdfField": "overrides[].type",
14372
+ "legacy": true
14373
+ },
14374
+ {
14375
+ "name": "impact-override",
14376
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14377
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14378
+ "meaning": "The impact the override assigns to the requirement.",
14379
+ "valueFormat": "Decimal number from 0 to 1",
14380
+ "hdfField": "overrides[].impact.value",
14381
+ "legacy": true
14382
+ },
14383
+ {
14384
+ "name": "justification",
14385
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14386
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14387
+ "meaning": "The controlled-vocabulary justification for the override.",
14388
+ "valueFormat": "An HDF override justification value",
14389
+ "hdfField": "overrides[].justification",
14390
+ "legacy": true
14391
+ },
14392
+ {
14393
+ "name": "baseline-ref",
14394
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14395
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14396
+ "meaning": "The baseline the override is scoped to.",
14397
+ "valueFormat": "String (§1.7)",
14398
+ "hdfField": "overrides[].baselineRef",
14399
+ "legacy": true
14400
+ },
14401
+ {
14402
+ "name": "component-ref",
14403
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14404
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14405
+ "meaning": "The component the override is scoped to.",
14406
+ "valueFormat": "String (§1.7), a UUID",
14407
+ "hdfField": "overrides[].componentRef",
14408
+ "legacy": true
14409
+ },
14410
+ {
14411
+ "name": "milestone-status",
14412
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14413
+ "objects": ["plan-of-action-and-milestones.risks[].remediations[].tasks[]"],
14414
+ "meaning": "The status of the milestone the remediation task represents. Documents exported before ADR-0014 carry it on the remediation, and import through the pre-ADR mapping (§4.3), which does not read it.",
14415
+ "valueFormat": "An HDF milestone status",
14416
+ "hdfField": "overrides[].milestones[].status",
14417
+ "legacy": true
14418
+ },
14419
+ {
14420
+ "name": "completed-at",
14421
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14422
+ "objects": ["plan-of-action-and-milestones.risks[].remediations[].tasks[]"],
14423
+ "meaning": "When the milestone was completed.",
14424
+ "valueFormat": "RFC 3339 date-time in UTC",
14425
+ "hdfField": "overrides[].milestones[].completedAt",
14426
+ "legacy": true
14427
+ },
14428
+ {
14429
+ "name": "completed-by",
14430
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14431
+ "objects": ["plan-of-action-and-milestones.risks[].remediations[].tasks[]"],
14432
+ "meaning": "Pre-ADR only: the identifier of whoever completed the milestone. No longer emitted; the task's responsible-roles entry with role-id completed-by names the party.",
14433
+ "valueFormat": "String (§1.7)",
14434
+ "hdfField": "overrides[].milestones[].completedBy.identifier",
14435
+ "legacy": true
14436
+ },
14437
+ {
14438
+ "name": "mime-type",
14439
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14440
+ "objects": ["plan-of-action-and-milestones.observations[]"],
14441
+ "meaning": "The media type of the evidence the observation carries.",
14442
+ "valueFormat": "String (§1.7), a media type",
14443
+ "hdfField": "overrides[].evidence[].mimeType",
14444
+ "legacy": true
14445
+ },
14446
+ {
14447
+ "name": "captured-by",
14448
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14449
+ "objects": ["plan-of-action-and-milestones.observations[]"],
14450
+ "meaning": "Pre-ADR only: the identifier of whoever captured the evidence. No longer emitted; the observation's origins actor names the party.",
14451
+ "valueFormat": "String (§1.7)",
14452
+ "hdfField": "overrides[].evidence[].capturedBy.identifier",
14453
+ "legacy": true
14454
+ },
14455
+ {
14456
+ "name": "source-name",
14457
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14458
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14459
+ "meaning": "The name of the source of the external reference the resource carries.",
14460
+ "valueFormat": "String (§1.7)",
14461
+ "hdfField": "overrides[].externalReferences[].sourceName",
14462
+ "legacy": true
14463
+ },
14464
+ {
14465
+ "name": "external-id",
14466
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14467
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14468
+ "meaning": "The identifier the source assigns to the external reference the resource carries.",
14469
+ "valueFormat": "String (§1.7)",
14470
+ "hdfField": "overrides[].externalReferences[].externalId",
14471
+ "legacy": true
14472
+ },
14473
+ {
14474
+ "name": "override-status",
14475
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14476
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14477
+ "meaning": "The status the override sets, emitted only when the override sets one; the risk status is closed for passed and notApplicable and open otherwise.",
14478
+ "valueFormat": "An HDF result status: passed, failed, notApplicable, notReviewed or error",
14479
+ "hdfField": "overrides[].status",
14480
+ "legacy": false
14481
+ },
14482
+ {
14483
+ "name": "inherited-from",
14484
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14485
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14486
+ "meaning": "The componentId of the local component that provides the control the override inherits.",
14487
+ "valueFormat": "String (§1.7), a UUID",
14488
+ "hdfField": "overrides[].inheritedFrom",
14489
+ "legacy": false
14490
+ },
14491
+ {
14492
+ "name": "affected-package-name",
14493
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14494
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14495
+ "meaning": "The name of a package the override is scoped to; every prop of one package shares the group package-<n>, n its 1-based position in source order.",
14496
+ "valueFormat": "String (§1.7)",
14497
+ "hdfField": "overrides[].affectedPackages[].name",
14498
+ "legacy": false
14499
+ },
14500
+ {
14501
+ "name": "affected-package-version",
14502
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14503
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14504
+ "meaning": "The version of a package the override is scoped to, grouped as affected-package-name.",
14505
+ "valueFormat": "String (§1.7)",
14506
+ "hdfField": "overrides[].affectedPackages[].version",
14507
+ "legacy": false
14508
+ },
14509
+ {
14510
+ "name": "affected-package-ecosystem",
14511
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14512
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14513
+ "meaning": "The ecosystem of a package the override is scoped to, grouped as affected-package-name.",
14514
+ "valueFormat": "An HDF package ecosystem, such as npm, rpm or generic",
14515
+ "hdfField": "overrides[].affectedPackages[].ecosystem",
14516
+ "legacy": false
14517
+ },
14518
+ {
14519
+ "name": "affected-package-cpe",
14520
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14521
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14522
+ "meaning": "The CPE 2.3 name of a package the override is scoped to, grouped as affected-package-name.",
14523
+ "valueFormat": "String (§1.7), a CPE 2.3 name",
14524
+ "hdfField": "overrides[].affectedPackages[].cpe",
14525
+ "legacy": false
14526
+ },
14527
+ {
14528
+ "name": "affected-package-purl",
14529
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14530
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14531
+ "meaning": "The package URL of a package the override is scoped to, grouped as affected-package-name.",
14532
+ "valueFormat": "String (§1.7), a package URL",
14533
+ "hdfField": "overrides[].affectedPackages[].purl",
14534
+ "legacy": false
14535
+ },
14536
+ {
14537
+ "name": "affected-package-fixed-in-version",
14538
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14539
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14540
+ "meaning": "The first version of a package the override is scoped to that contains the fix, grouped as affected-package-name.",
14541
+ "valueFormat": "String (§1.7)",
14542
+ "hdfField": "overrides[].affectedPackages[].fixedInVersion",
14543
+ "legacy": false
14544
+ },
14545
+ {
14546
+ "name": "evidence-encoding",
14547
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14548
+ "objects": ["plan-of-action-and-milestones.observations[]"],
14549
+ "meaning": "The encoding the evidence data declares; data whose encoding is exactly base64 is carried in the linked resource as-is.",
14550
+ "valueFormat": "String (§1.7)",
14551
+ "hdfField": "overrides[].evidence[].encoding",
14552
+ "legacy": false
14553
+ },
14554
+ {
14555
+ "name": "evidence-size",
14556
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14557
+ "objects": ["plan-of-action-and-milestones.observations[]"],
14558
+ "meaning": "The size of the evidence data in bytes.",
14559
+ "valueFormat": "Decimal number",
14560
+ "hdfField": "overrides[].evidence[].size",
14561
+ "legacy": false
14562
+ },
14563
+ {
14564
+ "name": "reference-rel",
14565
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14566
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14567
+ "meaning": "The relationship of the external reference the resource carries to the override.",
14568
+ "valueFormat": "String (§1.7)",
14569
+ "hdfField": "overrides[].externalReferences[].rel",
14570
+ "legacy": false
14571
+ },
14572
+ {
14573
+ "name": "reference-media-type",
14574
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14575
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14576
+ "meaning": "The media type of the artifact the external reference points at.",
14577
+ "valueFormat": "String (§1.7), a media type",
14578
+ "hdfField": "overrides[].externalReferences[].mediaType",
14579
+ "legacy": false
14580
+ },
14581
+ {
14582
+ "name": "checksum-algorithm",
14583
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14584
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14585
+ "meaning": "The hash algorithm of the checksum of the artifact the external reference points at.",
14586
+ "valueFormat": "One of sha256, sha384, sha512, blake3",
14587
+ "hdfField": "overrides[].externalReferences[].checksum.algorithm",
14588
+ "legacy": false
14589
+ },
14590
+ {
14591
+ "name": "checksum-value",
14592
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14593
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14594
+ "meaning": "The checksum of the artifact the external reference points at.",
14595
+ "valueFormat": "String (§1.7), a hex digest",
14596
+ "hdfField": "overrides[].externalReferences[].checksum.value",
14597
+ "legacy": false
14598
+ },
14599
+ {
14600
+ "name": "added-by",
14601
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14602
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14603
+ "meaning": "Who attached the external reference: the uuid of the metadata party carrying the identity.",
14604
+ "valueFormat": "A party UUID",
14605
+ "hdfField": "overrides[].externalReferences[].addedBy",
14606
+ "legacy": false
14607
+ },
14608
+ {
14609
+ "name": "added-at",
14610
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14611
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14612
+ "meaning": "When the external reference was attached.",
14613
+ "valueFormat": "RFC 3339 date-time in UTC",
14614
+ "hdfField": "overrides[].externalReferences[].addedAt",
14615
+ "legacy": false
14616
+ },
14617
+ {
14618
+ "name": "reference-kind",
14619
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14620
+ "objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
14621
+ "meaning": "The kind of payload the external reference classifies, such as threat-intel or advisory.",
14622
+ "valueFormat": "String (§1.7)",
14623
+ "hdfField": "overrides[].externalReferences[].kind",
14624
+ "legacy": false
14625
+ },
14626
+ {
14627
+ "name": "identity-identifier",
14628
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14629
+ "objects": ["plan-of-action-and-milestones.metadata.parties[]"],
14630
+ "meaning": "The identifier of the HDF identity the party carries; omitted when the identifier is empty. The party name is display text.",
14631
+ "valueFormat": "String (§1.7)",
14632
+ "hdfField": "Identity.identifier",
14633
+ "legacy": false
14634
+ },
14635
+ {
14636
+ "name": "identity-type",
14637
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14638
+ "objects": ["plan-of-action-and-milestones.metadata.parties[]"],
14639
+ "meaning": "The type of the HDF identity the party carries; the party type is display text. The party remarks hold the identity description.",
14640
+ "valueFormat": "One of email, username, system, agent, simple, other",
14641
+ "hdfField": "Identity.type",
14642
+ "legacy": false
14643
+ },
14644
+ {
14645
+ "name": "amendments-name",
14646
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14647
+ "objects": ["plan-of-action-and-milestones.metadata"],
14648
+ "meaning": "The name of the HDF amendments document; omitted when the name is empty. The metadata title is display text.",
14649
+ "valueFormat": "String (§1.7)",
14650
+ "hdfField": "name",
14651
+ "legacy": false
14652
+ },
14653
+ {
14654
+ "name": "label-key",
14655
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14656
+ "objects": ["plan-of-action-and-milestones.metadata"],
14657
+ "meaning": "The key of an amendments label, with class amendment-label and group label-<n>, n the label's 1-based position in sorted key order.",
14658
+ "valueFormat": "String (§1.7)",
14659
+ "hdfField": "labels (key)",
14660
+ "legacy": false
14661
+ },
14662
+ {
14663
+ "name": "label-value",
14664
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14665
+ "objects": ["plan-of-action-and-milestones.metadata"],
14666
+ "meaning": "The value of the amendments label its group names, with class amendment-label.",
14667
+ "valueFormat": "String (§1.7)",
14668
+ "hdfField": "labels (value)",
14669
+ "legacy": false
14670
+ },
14671
+ {
14672
+ "name": "impacted-control-id",
14673
+ "ns": "https://fedramp.gov/ns/oscal",
14674
+ "objects": ["plan-of-action-and-milestones.risks[]"],
14675
+ "meaning": "FedRAMP vocabulary: a control impacted by the POA&M item. The POA&M exporter emits it and the importer reads it as the override requirement id.",
14676
+ "valueFormat": "OSCAL control id token",
14677
+ "hdfField": "overrides[].requirementId",
14678
+ "legacy": true
14679
+ },
14680
+ {
14681
+ "name": "POAM-ID",
14682
+ "ns": "https://fedramp.gov/ns/oscal",
14683
+ "objects": ["plan-of-action-and-milestones.poam-items[]"],
14684
+ "meaning": "FedRAMP vocabulary: the CSP-assigned POA&M tracking identifier. The importer reads it as the override requirement id when no related risk carries impacted-control-id.",
14685
+ "valueFormat": "String, a CSP tracking number",
14686
+ "hdfField": "overrides[].requirementId",
14687
+ "legacy": false
14688
+ },
14689
+ {
14690
+ "name": "CORE",
14691
+ "ns": "https://fedramp.gov/ns/oscal",
14692
+ "objects": [
14693
+ "catalog.controls[]",
14694
+ "catalog.groups[].controls[]",
14695
+ "profile.modify.alters[].adds[]"
14696
+ ],
14697
+ "meaning": "FedRAMP vocabulary: the control must be included in every FedRAMP assessment. The importer maps the value true to applicability required.",
14698
+ "valueFormat": "The literal true",
14699
+ "hdfField": "requirements[].applicability",
14700
+ "legacy": false
14701
+ },
14702
+ {
14703
+ "name": "assessment-type",
14704
+ "ns": "https://fedramp.gov/ns/oscal",
14705
+ "objects": ["assessment-plan.metadata"],
14706
+ "meaning": "FedRAMP vocabulary: the type of assessment. The importer maps automated and manual to the plan type.",
14707
+ "valueFormat": "String",
14708
+ "hdfField": "type",
14709
+ "legacy": false
14710
+ },
14711
+ {
14712
+ "name": "label",
14713
+ "ns": "http://csrc.nist.gov/ns/oscal",
14714
+ "objects": ["catalog.controls[]", "catalog.groups[].controls[]"],
14715
+ "meaning": "NIST vocabulary: a human-readable label for the control, which may be rendered in place of its identifier.",
14716
+ "valueFormat": "String",
14717
+ "hdfField": "requirements[].tags.label",
14718
+ "legacy": false
14719
+ },
14720
+ {
14721
+ "name": "sort-id",
14722
+ "ns": "http://csrc.nist.gov/ns/oscal",
14723
+ "objects": ["catalog.controls[]", "catalog.groups[].controls[]"],
14724
+ "meaning": "NIST vocabulary: an alternative identifier that sorts easily among the other controls in the document.",
14725
+ "valueFormat": "String",
14726
+ "hdfField": "requirements[].tags.sort-id",
14727
+ "legacy": false
14728
+ },
14729
+ {
14730
+ "name": "version",
14731
+ "ns": "http://csrc.nist.gov/ns/oscal",
14732
+ "objects": ["assessment-plan.assessment-assets.components[]"],
14733
+ "meaning": "NIST vocabulary: the version of the component. The importer reads it from the first assessment-asset component as the runner version.",
14734
+ "valueFormat": "String",
14735
+ "hdfField": "assessments[].runner.version",
14736
+ "legacy": false
14737
+ },
14738
+ {
14739
+ "name": "type",
14740
+ "ns": "http://csrc.nist.gov/ns/oscal",
14741
+ "objects": ["assessment-results.back-matter.resources[]"],
14742
+ "meaning": "NIST vocabulary: the kind of resource. HDF emits the value evidence on the resource holding a requirement's source code.",
14743
+ "valueFormat": "A NIST resource type; HDF emits evidence",
14744
+ "hdfField": null,
14745
+ "legacy": false
14746
+ }
14747
+ ]
14748
+ };
14749
+ //#endregion
14750
+ //#region converters/oscal-to-hdf/typescript/vocabulary.ts
14751
+ /**
14752
+ * The HDF OSCAL extension vocabulary (ADR-0014 §1.5) and the helpers that emit
14753
+ * and read its props. The table is the JSON file the Go peer embeds, imported
14754
+ * directly so both languages read one file; the bundler inlines it into dist.
14755
+ */
14756
+ /**
14757
+ * Validates a decoded vocabulary table and indexes its rows by name, throwing on
14758
+ * a malformed table. Mirrors newVocabulary in Go.
14759
+ */
14760
+ function validateVocabularyTable(raw) {
14761
+ const table = raw ?? {};
14762
+ if (typeof table.namespace !== "string" || table.namespace === "") throw new Error("oscal: the OSCAL vocabulary table has no namespace");
14763
+ if (typeof table.defaultNamespace !== "string" || table.defaultNamespace === "") throw new Error("oscal: the OSCAL vocabulary table has no defaultNamespace");
14764
+ if (!Array.isArray(table.props) || table.props.length === 0) throw new Error("oscal: the OSCAL vocabulary table has no rows");
14765
+ const byName = /* @__PURE__ */ new Map();
14766
+ for (const row of table.props) {
14767
+ if (byName.has(row.name)) throw new Error(`oscal: the OSCAL vocabulary table defines "${row.name}" twice`);
14768
+ byName.set(row.name, row);
14769
+ }
14770
+ return {
14771
+ namespace: table.namespace,
14772
+ defaultNamespace: table.defaultNamespace,
14773
+ props: table.props,
14774
+ byName
14775
+ };
14776
+ }
14777
+ const table = validateVocabularyTable(oscal_vocabulary_default);
14778
+ const byName = table.byName;
14779
+ function mustVocabularyRow(name) {
14780
+ const row = byName.get(name);
14781
+ if (!row) throw new Error(`oscal: "${name}" is not a row of the OSCAL vocabulary`);
14782
+ return row;
14783
+ }
14784
+ /** Whether a UTF-16 code unit is ECMAScript \s whitespace, which is BMP-only. */
14785
+ function isWhitespace(unit) {
14786
+ return /\s/.test(unit);
14787
+ }
14788
+ /**
14789
+ * Renders an HDF value as an OSCAL StringDatatype (ADR-0014 §1.7.1): each run of
14790
+ * line terminators becomes one space, ECMAScript whitespace is trimmed from both
14791
+ * edges, and a value with nothing left becomes "_". An empty value stays empty;
14792
+ * carrying it is the caller's decision (§1.7.3). Mirrors NormalizePropValue in Go.
14793
+ */
14794
+ function normalizePropValue(value) {
14795
+ if (value === "") return "";
14796
+ const joined = value.replace(/[\r\n\u2028\u2029]+/g, " ");
14797
+ let start = 0;
14798
+ while (start < joined.length && isWhitespace(joined[start])) start++;
14799
+ let end = joined.length;
14800
+ while (end > start && isWhitespace(joined[end - 1])) end--;
14801
+ const normalized = joined.slice(start, end);
14802
+ return normalized === "" ? "_" : normalized;
14803
+ }
14804
+ /**
14805
+ * Builds the prop a vocabulary row names, in that row's namespace (omitted for
14806
+ * NIST's default namespace). The value is normalized (§1.7.1), and an HDF prop
14807
+ * whose value changed carries the exact value in remarks (§1.7.2). Returns
14808
+ * undefined for an empty value, which writes no prop. An unknown name throws:
14809
+ * every prop an exporter emits must be a row. Mirrors VocabularyProp in Go.
14810
+ */
14811
+ function vocabularyProp(name, value) {
14812
+ const row = mustVocabularyRow(name);
14813
+ if (value === "") return void 0;
14814
+ const prop = {
14815
+ name: row.name,
14816
+ value: normalizePropValue(value)
14817
+ };
14818
+ if (row.ns !== table.defaultNamespace) prop.ns = row.ns;
14819
+ if (row.ns === table.namespace && prop.value !== value) prop.remarks = value;
14820
+ return prop;
14821
+ }
14822
+ /** Pushes vocabularyProp(name, value) onto props when it writes a prop. */
14823
+ function pushVocabularyProp(props, name, value) {
14824
+ const prop = vocabularyProp(name, value);
14825
+ if (prop) props.push(prop);
14826
+ }
14827
+ /** Marks an optional HDF string field that is present but empty (§1.7.3). */
14828
+ function emptyFieldProp(field) {
14829
+ return mustFieldProp("empty-field", field);
14830
+ }
14831
+ /** Marks an HDF field OSCAL required a display fallback for (§1.7.4). */
14832
+ function absentFieldProp(field) {
14833
+ return mustFieldProp("absent-field", field);
14834
+ }
14835
+ function mustFieldProp(name, field) {
14836
+ const prop = vocabularyProp(name, field);
14837
+ if (!prop) throw new Error(`oscal: ${name} needs the name of an HDF field`);
14838
+ return prop;
14839
+ }
14840
+ /**
14841
+ * Matches a prop to a row by name and namespace. An absent ns is NIST's default
14842
+ * namespace, except that for a legacy row it is also accepted as the row's own prop.
14843
+ */
14844
+ function matchVocabularyProp(row, index, p) {
14845
+ if (p.name !== row.name) return void 0;
14846
+ const ns = p.ns ?? "";
14847
+ let legacy = false;
14848
+ if ((ns === "" ? table.defaultNamespace : ns) !== row.ns) {
14849
+ if (ns !== "" || !row.legacy) return void 0;
14850
+ legacy = true;
14851
+ }
14852
+ return {
14853
+ index,
14854
+ value: row.ns === table.namespace && p.remarks ? p.remarks : p.value,
14855
+ legacy
14856
+ };
14857
+ }
14858
+ /** The first prop in props that is the named row's prop. Mirrors FindVocabularyProp in Go. */
14859
+ function findVocabularyProp(props, name) {
14860
+ return findVocabularyProps(props, name)[0];
14861
+ }
14862
+ /** Every prop in props that is the named row's prop, in order. Mirrors FindVocabularyProps in Go. */
14863
+ function findVocabularyProps(props, name) {
14864
+ const row = byName.get(name);
14865
+ if (!row || !props) return [];
14866
+ const matches = [];
14867
+ props.forEach((p, i) => {
14868
+ const m = matchVocabularyProp(row, i, p);
14869
+ if (m) matches.push(m);
14870
+ });
14871
+ return matches;
14872
+ }
14873
+ /**
14874
+ * The first prop in props that is the named row's prop and carries group; an
14875
+ * empty group matches only ungrouped props. Mirrors FindGroupedVocabularyProp in Go.
14876
+ */
14877
+ function findGroupedVocabularyProp(props, name, group) {
14878
+ return findVocabularyProps(props, name).find((m) => (props[m.index].group ?? "") === group);
14879
+ }
14880
+ /**
14881
+ * Whether props carry the marker prop (empty-field or absent-field) naming field
14882
+ * within group (§1.7.3, §1.7.4). Mirrors HasFieldMarker in Go.
14883
+ */
14884
+ function hasFieldMarker(props, marker, field, group) {
14885
+ return findVocabularyProps(props, marker).some((m) => m.value === field && (props[m.index].group ?? "") === group);
14886
+ }
14887
+ /**
14888
+ * Reads an optional HDF string field: the named row's prop value when present, an
14889
+ * empty string when empty-field names field (§1.7.3), and undefined when the field
14890
+ * is absent. Mirrors VocabularyString in Go.
14891
+ */
14892
+ function vocabularyString(props, name, field, group) {
14893
+ const match = findGroupedVocabularyProp(props, name, group);
14894
+ if (match) return match.value;
14895
+ return hasFieldMarker(props, "empty-field", field, group) ? "" : void 0;
14896
+ }
14897
+ /**
14898
+ * Whether p is HDF's own and so must never be carried as a foreign prop: every
14899
+ * prop in the HDF namespace, and a prop with no ns whose name is a legacy row
14900
+ * (§1.4, §3.1). Mirrors ConsumedVocabularyProp in Go.
14901
+ */
14902
+ function consumedVocabularyProp(p) {
14903
+ if (p.ns === table.namespace) return true;
14904
+ return (p.ns ?? "") === "" && byName.get(p.name)?.legacy === true;
14905
+ }
14906
+ //#endregion
13993
14907
  //#region converters/oscal-to-hdf/typescript/shared.ts
13994
14908
  /**
13995
14909
  * Shared helper functions for OSCAL-to-HDF converters.
@@ -14023,7 +14937,7 @@ function controlIdsToNistTags(ids) {
14023
14937
  return tags;
14024
14938
  }
14025
14939
  /**
14026
- * Extracts the base control ID from a SAR objective ID.
14940
+ * Extracts the base control ID from an assessment-plan objective ID.
14027
14941
  * "ac-1.a.1_obj.1" -> "ac-1"
14028
14942
  */
14029
14943
  function extractControlIdFromObjectiveId(objectiveId) {
@@ -14111,28 +15025,96 @@ function extractMetadata(m) {
14111
15025
  lastModified: String(m["last-modified"])
14112
15026
  };
14113
15027
  }
14114
- /** Matches NIST 800-53 tags with enhancements like "AC-2 (3)". */
14115
- const nistEnhancementReverseRe = /^([A-Z]{2}-\d+)\s*\((\d+)\)$/;
14116
15028
  /**
14117
- * Converts NIST 800-53 notation back to OSCAL control ID.
14118
- * "AC-1" -> "ac-1", "AC-2 (3)" -> "ac-2.3", "SI-7 (1)" -> "si-7.1"
15029
+ * Returns the canonical OSCAL id of the NIST control a target-id names, when the
15030
+ * target, ignoring ASCII letter case, is a control ("ac-2", "ac-2.3"), optionally
15031
+ * followed by dot-separated parts and an objective or statement suffix
15032
+ * ("ac-2.3_obj.a", "au-1_smt.a", "ac-1.a.1_obj.1"), and NIST defines that control
15033
+ * at any supported revision; otherwise undefined, including for a target shaped
15034
+ * like a control NIST does not define. Mirrors Go's ConfirmedControlID.
14119
15035
  */
14120
- function nistTagToControlId(tag) {
14121
- tag = tag.trim();
14122
- const m = nistEnhancementReverseRe.exec(tag);
14123
- if (m) return `${m[1].toLowerCase()}.${m[2]}`;
14124
- return tag.toLowerCase();
15036
+ function confirmedControlId(targetId) {
15037
+ const target = asciiLower(targetId);
15038
+ const controlId = objectiveIDRe.exec(target)?.[0];
15039
+ if (controlId === void 0 || !isObjectiveOrStatementSuffix(target.slice(controlId.length))) return;
15040
+ const tag = controlIdToNistTag(controlId);
15041
+ return SUPPORTED_NIST_REVISIONS.some((rev) => nistExists(tag, rev)) ? nistTagToControlId(tag) : void 0;
15042
+ }
15043
+ /** Lowercases only ASCII letters, so Go and TypeScript fold identically. */
15044
+ function asciiLower(s) {
15045
+ return s.replace(/[A-Z]/g, (c) => c.toLowerCase());
14125
15046
  }
14126
15047
  /**
14127
- * Converts a 0.0-1.0 impact value to an OSCAL severity string.
14128
- * This is the reverse of extractRiskSeverity.
15048
+ * Whether rest, what follows a control id in a lowercased target-id, is empty or
15049
+ * names an objective or statement of that control: optional dot-separated
15050
+ * letter-and-digit parts, then "_obj" or "_smt", alone or followed by non-empty
15051
+ * dot-separated parts.
14129
15052
  */
14130
- function impactToSeverity$1(impact) {
14131
- return oscalSeverityFromHdf(impactToSeverity(impact));
15053
+ function isObjectiveOrStatementSuffix(rest) {
15054
+ if (rest === "") return true;
15055
+ const cut = rest.indexOf("_");
15056
+ if (cut < 0) return false;
15057
+ const parts = rest.slice(0, cut);
15058
+ if (parts !== "") {
15059
+ if (!parts.startsWith(".")) return false;
15060
+ if (parts.slice(1).split(".").some((part) => part === "" || !isLowerAlphanumeric(part))) return false;
15061
+ }
15062
+ const [kind, ...after] = rest.slice(cut + 1).split(".");
15063
+ return (kind === "obj" || kind === "smt") && !after.includes("");
15064
+ }
15065
+ function isLowerAlphanumeric(part) {
15066
+ return [...part].every((c) => c >= "a" && c <= "z" || c >= "0" && c <= "9");
14132
15067
  }
14133
15068
  /**
14134
- * Maps an HDF status string to an OSCAL risk status string.
14135
- * "passed"/"notApplicable" -> "closed", everything else -> "open".
15069
+ * Converts NIST 800-53 notation, in any spelling normalizeNistId accepts, to the
15070
+ * OSCAL id of the control it names; a statement part names its control. Anything
15071
+ * else is returned trimmed and lowercased.
15072
+ * "AC-1" -> "ac-1", "ac-2 (3)" -> "ac-2.3", "AC-8 c 1" -> "ac-8"
15073
+ */
15074
+ function nistTagToControlId(tag) {
15075
+ return nistTagToControlRef(tag).controlId;
15076
+ }
15077
+ /**
15078
+ * Converts NIST 800-53 notation to the OSCAL control id and, for a statement
15079
+ * part, the OSCAL statement id ("AC-8 c 1" -> "ac-8", "ac-8_smt.c.1";
15080
+ * "AC-2 (3) (a)" -> "ac-2.3", "ac-2.3_smt.a"). statementId is empty when the tag
15081
+ * names a whole control. A tag that is not a NIST spelling is returned trimmed and
15082
+ * lowercased as controlId. Mirrors Go's NistTagToControlRef.
15083
+ */
15084
+ function nistTagToControlRef(tag) {
15085
+ const trimmed = tag.trim();
15086
+ const normalized = normalizeNistId(trimmed.split(/\s+/).join(" "));
15087
+ if (normalized === void 0) return {
15088
+ controlId: trimmed.toLowerCase(),
15089
+ statementId: ""
15090
+ };
15091
+ const [head, ...parts] = normalized.split(" ");
15092
+ const [family, number] = head.split("-");
15093
+ let controlId = `${family.toLowerCase()}-${unpadNistNumber(number)}`;
15094
+ if (parts.length > 0 && parts[0][0] >= "0" && parts[0][0] <= "9") controlId += `.${unpadNistNumber(parts.shift())}`;
15095
+ if (parts.length === 0) return {
15096
+ controlId,
15097
+ statementId: ""
15098
+ };
15099
+ return {
15100
+ controlId,
15101
+ statementId: `${controlId}_smt.${parts.map(unpadNistNumber).join(".")}`
15102
+ };
15103
+ }
15104
+ /** Strips the zero normalizeNistId pads a one-digit number with. */
15105
+ function unpadNistNumber(part) {
15106
+ return part.length === 2 && part.startsWith("0") ? part.slice(1) : part;
15107
+ }
15108
+ /**
15109
+ * Converts a 0.0-1.0 impact value to an OSCAL severity string.
15110
+ * This is the reverse of extractRiskSeverity.
15111
+ */
15112
+ function impactToSeverity$1(impact) {
15113
+ return oscalSeverityFromHdf(impactToSeverity(impact));
15114
+ }
15115
+ /**
15116
+ * Maps an HDF status string to an OSCAL risk status string.
15117
+ * "passed"/"notApplicable" -> "closed", everything else -> "open".
14136
15118
  */
14137
15119
  function hdfStatusToOscalRiskStatus(status) {
14138
15120
  if (status === "passed" || status === "notApplicable") return "closed";
@@ -14141,6 +15123,22 @@ function hdfStatusToOscalRiskStatus(status) {
14141
15123
  /** OSCAL specification version used in reverse converter output documents. */
14142
15124
  const OSCAL_VERSION = "1.1.2";
14143
15125
  /**
15126
+ * Builds the description-label prop that marks an OSCAL prose home with the HDF
15127
+ * description label whose text it carries.
15128
+ */
15129
+ function descriptionLabelProp(label) {
15130
+ const prop = vocabularyProp("description-label", label);
15131
+ if (!prop) throw new Error(`oscal: description label ${JSON.stringify(label)} yields no description-label prop`);
15132
+ return prop;
15133
+ }
15134
+ /**
15135
+ * Returns props' description-label, or '' when there is none; a
15136
+ * description-label in any other namespace is foreign.
15137
+ */
15138
+ function descriptionLabel(props) {
15139
+ return findVocabularyProp(props, "description-label")?.value ?? "";
15140
+ }
15141
+ /**
14144
15142
  * Converts a title to kebab-case, truncated to 80 characters.
14145
15143
  * Shared utility used by multiple converters for deriving names.
14146
15144
  */
@@ -14185,8 +15183,7 @@ function toKebabCase(title, fallback) {
14185
15183
  *
14186
15184
  * Two different ids can encode to the same token ('a/b' and 'a:b' both yield
14187
15185
  * 'a_b'), which is why callers must also record the source id in the emitted
14188
- * document — for SAR that is a prop on the finding, trimmed because OSCAL's
14189
- * StringDatatype forbids a padded value.
15186
+ * document — for SAR that is the finding's hdf-requirement-id prop.
14190
15187
  */
14191
15188
  function oscalToken(s) {
14192
15189
  if (s === "") return "";
@@ -14220,6 +15217,33 @@ function oscalString(s) {
14220
15217
  * Results JSON and produces an OSCAL 1.1.2 assessment-results JSON document.
14221
15218
  */
14222
15219
  /**
15220
+ * Adds a control, or one statement of it, to the reviewed-controls selection, one
15221
+ * entry per control. A control selected whole carries no statement-ids, because
15222
+ * listing any would narrow the selection to them. Mirrors Go's selectControl.
15223
+ */
15224
+ function selectControl(controls, index, controlId, statementId) {
15225
+ const i = index.get(controlId);
15226
+ if (i === void 0) {
15227
+ index.set(controlId, controls.length);
15228
+ controls.push(statementId === "" ? { "control-id": controlId } : {
15229
+ "control-id": controlId,
15230
+ "statement-ids": [statementId]
15231
+ });
15232
+ return;
15233
+ }
15234
+ const selection = controls[i];
15235
+ const statementIds = selection["statement-ids"];
15236
+ if (statementIds === void 0) return;
15237
+ if (statementId === "") delete selection["statement-ids"];
15238
+ else if (!statementIds.includes(statementId)) statementIds.push(statementId);
15239
+ }
15240
+ /**
15241
+ * Accompanies the include-all selection of a result whose baseline names no
15242
+ * control, so the selection is not read as a claim. Mirrors Go's
15243
+ * noIdentifiableControlsRemark.
15244
+ */
15245
+ const NO_IDENTIFIABLE_CONTROLS_REMARK = "No controls were identifiable in the assessed input, so none is listed individually. OSCAL requires a control selection; include-all is emitted to satisfy it and does not assert that any control was assessed.";
15246
+ /**
14223
15247
  * Convert HDF Results JSON to OSCAL Assessment Results (SAR) JSON.
14224
15248
  *
14225
15249
  * @param input - HDF Results JSON string
@@ -14319,18 +15343,15 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
14319
15343
  let description = "Converted from HDF results";
14320
15344
  if (baseline.description && baseline.description !== "") description = baseline.description;
14321
15345
  const resultProps = [];
14322
- if (baseline.version && baseline.version !== "") resultProps.push({
14323
- name: "baseline-version",
14324
- value: baseline.version
14325
- });
15346
+ if (typeof baseline.version === "string") pushVocabularyProp(resultProps, "baseline-version", baseline.version);
14326
15347
  const findings = [];
14327
15348
  const observations = [];
14328
15349
  const risks = [];
14329
15350
  const resources = [];
14330
15351
  const includeControls = [];
14331
- const seenControl = /* @__PURE__ */ new Set();
15352
+ const controlIndex = /* @__PURE__ */ new Map();
14332
15353
  for (const req of baseline.requirements) {
14333
- const nistId = nistTagToControlId(req.id ?? "");
15354
+ const { controlId: nistId, statementId } = nistTagToControlRef(req.id ?? "");
14334
15355
  if (nistId === "") continue;
14335
15356
  const { finding, observation, risk, resource } = requirementToFindingSet(req, timestamp, toolActorUuid, subjects);
14336
15357
  findings.push(finding);
@@ -14338,12 +15359,12 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
14338
15359
  if (risk) risks.push(risk);
14339
15360
  if (resource) resources.push(resource);
14340
15361
  const cid = oscalToken(nistId);
14341
- if (cid !== "" && !seenControl.has(cid)) {
14342
- seenControl.add(cid);
14343
- includeControls.push({ "control-id": cid });
14344
- }
15362
+ if (cid !== "") selectControl(includeControls, controlIndex, cid, statementId);
14345
15363
  }
14346
- const controlSelection = includeControls.length > 0 ? { "include-controls": includeControls } : {};
15364
+ const controlSelection = includeControls.length > 0 ? { "include-controls": includeControls } : {
15365
+ "include-all": {},
15366
+ remarks: NO_IDENTIFIABLE_CONTROLS_REMARK
15367
+ };
14347
15368
  return {
14348
15369
  result: {
14349
15370
  uuid: crypto.randomUUID(),
@@ -14384,31 +15405,17 @@ function buildSubjects(components) {
14384
15405
  * Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
14385
15406
  */
14386
15407
  function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14387
- const controlID = oscalToken(nistTagToControlId(req.id ?? ""));
15408
+ const { controlId, statementId } = nistTagToControlRef(req.id ?? "");
15409
+ const [targetType, targetId] = statementId === "" ? ["objective-id", oscalToken(controlId)] : ["statement-id", statementId];
14388
15410
  const results = req.results ?? [];
14389
15411
  const descriptions = req.descriptions ?? [];
14390
15412
  const { state, reason } = effectiveState(req);
14391
15413
  const findingDesc = extractDefaultDescription(descriptions);
14392
- const props = [{
14393
- name: "hdf-requirement-id",
14394
- value: oscalString(req.id)
14395
- }];
15414
+ const props = [];
14396
15415
  const addProp = (name, value) => {
14397
- if (value !== "") props.push({
14398
- name,
14399
- value
14400
- });
14401
- };
14402
- const addProseProp = (name, text) => {
14403
- const preview = previewLine(text);
14404
- if (preview === "") return;
14405
- const p = {
14406
- name,
14407
- value: preview
14408
- };
14409
- if (preview !== text) p.remarks = text;
14410
- props.push(p);
15416
+ pushVocabularyProp(props, name, value);
14411
15417
  };
15418
+ addProp("hdf-requirement-id", req.id);
14412
15419
  const descriptionByLabel = (label) => {
14413
15420
  const d = descriptions.find((x) => x.label === label);
14414
15421
  return d ? d.data : "";
@@ -14421,9 +15428,6 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14421
15428
  };
14422
15429
  pushTagValues("nist");
14423
15430
  pushTagValues("cci");
14424
- addProseProp("check", descriptionByLabel("check"));
14425
- addProseProp("rationale", descriptionByLabel("rationale"));
14426
- if (req.impact <= 0) addProseProp("fix", descriptionByLabel("fix"));
14427
15431
  if (req.controlType) addProp("control-type", req.controlType);
14428
15432
  if (req.verificationMethod) addProp("verification-method", req.verificationMethod);
14429
15433
  if (req.applicability) addProp("applicability", req.applicability);
@@ -14451,7 +15455,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14451
15455
  href: o.uri,
14452
15456
  rel: "reference"
14453
15457
  });
14454
- else if (typeof o.ref === "string") addProseProp("reference", o.ref);
15458
+ else if (typeof o.ref === "string") addProp("reference", o.ref);
14455
15459
  }
14456
15460
  for (const er of req.externalReferences ?? []) if (typeof er.href === "string" && er.href !== "") links.push({
14457
15461
  href: er.href,
@@ -14465,6 +15469,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14465
15469
  resource = {
14466
15470
  uuid: resourceUuid,
14467
15471
  title: `Check source code for ${req.id}`,
15472
+ props: [vocabularyProp("type", "evidence")],
14468
15473
  base64: {
14469
15474
  value: encodeBase64Utf8(req.code),
14470
15475
  "media-type": "text/plain"
@@ -14479,9 +15484,11 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14479
15484
  if (reason) targetStatus.reason = reason;
14480
15485
  const remarks = overrideRemarks(req);
14481
15486
  if (remarks) targetStatus.remarks = remarks;
15487
+ const rationale = descriptionByLabel("rationale");
14482
15488
  const target = {
14483
- type: "objective-id",
14484
- "target-id": controlID,
15489
+ type: targetType,
15490
+ "target-id": targetId,
15491
+ ...rationale !== "" ? { description: rationale } : {},
14485
15492
  status: targetStatus
14486
15493
  };
14487
15494
  const finding = {
@@ -14506,7 +15513,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14506
15513
  ...relevantEvidence.length > 0 ? { "relevant-evidence": relevantEvidence } : {}
14507
15514
  };
14508
15515
  finding["related-observations"] = [{ "observation-uuid": obsUUID }];
14509
- }
15516
+ } else warnUncarriedProse(req);
14510
15517
  let risk;
14511
15518
  if (req.impact > 0) {
14512
15519
  const riskUUID = crypto.randomUUID();
@@ -14548,8 +15555,8 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
14548
15555
  };
14549
15556
  }
14550
15557
  /**
14551
- * Reduces prose to a single line legal as an OSCAL StringDatatype prop value:
14552
- * the first non-empty line, trimmed, truncated to 120 code points.
15558
+ * Reduces prose to a single line for an OSCAL single-line field: the first
15559
+ * non-empty line, trimmed, truncated to 120 code points.
14553
15560
  */
14554
15561
  function previewLine(text) {
14555
15562
  for (let line of text.split("\n")) {
@@ -14557,7 +15564,7 @@ function previewLine(text) {
14557
15564
  if (line === "") continue;
14558
15565
  const maxRunes = 120;
14559
15566
  const runes = Array.from(line);
14560
- if (runes.length > maxRunes) line = `${runes.slice(0, maxRunes - 3).join("").trim()}...`;
15567
+ if (runes.length > maxRunes) line = `${runes.slice(0, 117).join("").trim()}...`;
14561
15568
  return line;
14562
15569
  }
14563
15570
  return "";
@@ -14612,9 +15619,10 @@ function overrideRemarks(req) {
14612
15619
  return parts.join("; ");
14613
15620
  }
14614
15621
  /**
14615
- * Collects the requirement's refs, evidence, and source location into OSCAL
14616
- * observation relevant-evidence — the home the reverse SAR importer reads back
14617
- * into HDF refs (via href) and evidence (via description).
15622
+ * Collects the requirement's refs, evidence and source location, then its check
15623
+ * (and impact-0 fix) prose, into OSCAL observation relevant-evidence — the home
15624
+ * the reverse SAR importer reads back into HDF refs (via href), evidence (via
15625
+ * description) and check/fix (via description-label).
14618
15626
  */
14619
15627
  function buildRelevantEvidence(req) {
14620
15628
  const ev = [];
@@ -14638,8 +15646,37 @@ function buildRelevantEvidence(req) {
14638
15646
  const loc = sourceLocationText(req.sourceLocation);
14639
15647
  if (loc) ev.push({ description: "Source location: " + loc });
14640
15648
  }
15649
+ for (const label of observationProseLabels(req)) {
15650
+ const text = descriptionText(req, label);
15651
+ ev.push({
15652
+ description: previewLine(text),
15653
+ props: [descriptionLabelProp(label)],
15654
+ remarks: text
15655
+ });
15656
+ }
14641
15657
  return ev;
14642
15658
  }
15659
+ /** Data of the requirement's first description with the label, or ''. */
15660
+ function descriptionText(req, label) {
15661
+ return (req.descriptions ?? []).find((d) => d.label === label)?.data ?? "";
15662
+ }
15663
+ /**
15664
+ * Description labels whose text the requirement's observation carries as
15665
+ * labelled evidence: check, and fix when impact is 0 (otherwise fix's home is
15666
+ * the risk remediation). A description with no preview text carries nothing.
15667
+ */
15668
+ function observationProseLabels(req) {
15669
+ return (req.impact <= 0 ? ["check", "fix"] : ["check"]).filter((label) => previewLine(descriptionText(req, label)) !== "");
15670
+ }
15671
+ /**
15672
+ * Reports observation-scoped prose lost because a requirement with no results
15673
+ * emits no observation.
15674
+ */
15675
+ function warnUncarriedProse(req) {
15676
+ const labels = observationProseLabels(req);
15677
+ if (labels.length === 1) emitConverterWarning(`hdf-to-oscal-sar: requirement ${JSON.stringify(req.id)} has no results, so no observation holds its ${labels[0]} description; it was not carried`);
15678
+ else if (labels.length > 1) emitConverterWarning(`hdf-to-oscal-sar: requirement ${JSON.stringify(req.id)} has no results, so no observation holds its ${labels.join(" and ")} descriptions; they were not carried`);
15679
+ }
14643
15680
  /** Renders a source location as "ref:line", degrading to whichever is present. */
14644
15681
  function sourceLocationText(loc) {
14645
15682
  const ref = loc.ref ?? "";
@@ -14652,8 +15689,9 @@ function severityToFacetValue(s) {
14652
15689
  }
14653
15690
  /**
14654
15691
  * Turns the requirement's fix description and any governing risk-acceptance
14655
- * override into OSCAL risk remediations — the home the reverse importer reads
14656
- * back as the HDF remediation description.
15692
+ * override into OSCAL risk remediations. The reverse importer reads the
15693
+ * labelled fix back as the HDF fix description and the rest as the remediation
15694
+ * description.
14657
15695
  */
14658
15696
  function buildRemediations(req) {
14659
15697
  const rems = [];
@@ -14662,7 +15700,8 @@ function buildRemediations(req) {
14662
15700
  uuid: crypto.randomUUID(),
14663
15701
  lifecycle: "recommendation",
14664
15702
  title: "Recommended fix",
14665
- description: fix.data
15703
+ description: fix.data,
15704
+ props: [descriptionLabelProp("fix")]
14666
15705
  });
14667
15706
  const overrides = req.statusOverrides ?? [];
14668
15707
  if (req.disposition && overrides.length > 0) {
@@ -14726,6 +15765,13 @@ function riskStatusFromState(state) {
14726
15765
  * Converts HDF Amendments to OSCAL Plan of Action and Milestones (POA&M) format.
14727
15766
  *
14728
15767
  * This is the reverse direction of the oscal-poam to HDF converter.
15768
+ *
15769
+ * Every amendments field has an OSCAL home the importer reads back exactly
15770
+ * (ADR-0014 §4.6), with four exclusions: the document integrity and signature,
15771
+ * the generator (the importer stamps its own), and each override's signature and
15772
+ * previousChecksum. A signature covers the original HDF bytes and the checksum
15773
+ * chain covers document order; neither survives a format change, and the
15774
+ * importer re-chains the overrides it reads.
14729
15775
  */
14730
15776
  /**
14731
15777
  * Convert HDF Amendments JSON to OSCAL POA&M JSON.
@@ -14738,6 +15784,12 @@ async function convertHdfToOscalPoam(input) {
14738
15784
  const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendmentsDoc) };
14739
15785
  return JSON.stringify(doc, null, 2);
14740
15786
  }
15787
+ /** Role ids of the metadata roles the exporter defines. */
15788
+ const ROLE_PREPARED_BY = "prepared-by";
15789
+ const ROLE_APPROVED_BY = "approved-by";
15790
+ const ROLE_COMPLETED_BY = "completed-by";
15791
+ /** Marks the risk-log entry whose start is the override's appliedAt and whose logged-by names its appliedBy. */
15792
+ const OVERRIDE_APPLIED_TITLE = "Override applied";
14741
15793
  /**
14742
15794
  * Picks the document title, which OSCAL requires on metadata. The HDF name is
14743
15795
  * schema-required, so the fallbacks only matter for a document that slipped
@@ -14747,13 +15799,25 @@ function poamTitle(a) {
14747
15799
  return firstNonEmpty(a.name, a.amendmentId ?? "", "HDF Amendments");
14748
15800
  }
14749
15801
  /**
15802
+ * Stands in for an empty requirementId: HDF puts no minLength on it, and OSCAL
15803
+ * 1.2.x requires risk and POA&M item titles to be non-empty. It is display text
15804
+ * only; the requirement id rides in hdf-requirement-id. Mirrors Go's
15805
+ * unidentifiedRequirementTitle.
15806
+ */
15807
+ const UNIDENTIFIED_REQUIREMENT_TITLE = "Unidentified requirement";
15808
+ /** The title of the risk and POA&M item an override produces. */
15809
+ function requirementTitle(override) {
15810
+ return firstNonEmpty(override.requirementId, UNIDENTIFIED_REQUIREMENT_TITLE);
15811
+ }
15812
+ /**
14750
15813
  * Supplies the text OSCAL requires for a risk's description and statement. HDF
14751
15814
  * puts no minLength on reason, so an override can legitimately carry none; the
14752
15815
  * fallback states that absence rather than inventing an impact assessment the
14753
- * source never made.
15816
+ * source never made, and names the requirement only when there is one.
14754
15817
  */
14755
15818
  function riskRationale(override) {
14756
- return firstNonEmpty(override.reason, `No rationale was recorded for the ${String(override.type)} override applied to ${override.requirementId}.`);
15819
+ const absence = firstNonEmpty(override.requirementId) === "" ? `No rationale was recorded for the ${String(override.type)} override.` : `No rationale was recorded for the ${String(override.type)} override applied to ${override.requirementId}.`;
15820
+ return firstNonEmpty(override.reason, absence);
14757
15821
  }
14758
15822
  /** HDF dates arrive as strings from JSON.parse but are typed as Date. */
14759
15823
  function toDate(value) {
@@ -14763,72 +15827,87 @@ function toDate(value) {
14763
15827
  return d;
14764
15828
  }
14765
15829
  /**
14766
- * Deduplicates HDF identities into OSCAL metadata parties. A party keeps its
14767
- * UUID across reuse so origin actors and responsible parties reference the same
14768
- * entry. Insertion order is preserved for deterministic output.
15830
+ * Deduplicates HDF identities into OSCAL metadata parties, one per distinct
15831
+ * (identifier, type, description) triple; an absent and an empty description are
15832
+ * distinct. Insertion order is preserved for deterministic output.
14769
15833
  */
14770
15834
  var PartyRegistry = class {
14771
15835
  constructor() {
14772
- this.byId = /* @__PURE__ */ new Map();
15836
+ this.byKey = /* @__PURE__ */ new Map();
14773
15837
  }
14774
15838
  getOrAdd(id) {
14775
- const name = oscalString(id.identifier);
14776
- const existing = this.byId.get(name);
15839
+ const key = JSON.stringify([
15840
+ id.identifier,
15841
+ id.type,
15842
+ id.description ?? null
15843
+ ]);
15844
+ const existing = this.byKey.get(key);
14777
15845
  if (existing) return existing.uuid;
15846
+ const name = oscalString(id.identifier);
15847
+ const props = [];
15848
+ pushVocabularyProp(props, "identity-identifier", id.identifier ?? "");
15849
+ pushVocabularyProp(props, "identity-type", String(id.type));
15850
+ if (id.description === "") props.push(emptyFieldProp("description"));
14778
15851
  const party = {
14779
15852
  uuid: crypto.randomUUID(),
14780
15853
  type: "person",
14781
- ...name === "" ? {} : { name }
15854
+ ...name === "" ? {} : { name },
15855
+ ...props.length > 0 ? { props } : {},
15856
+ ...id.description ? { remarks: id.description } : {}
14782
15857
  };
14783
- this.byId.set(name, party);
15858
+ this.byKey.set(key, party);
14784
15859
  return party.uuid;
14785
15860
  }
14786
15861
  list() {
14787
- return [...this.byId.values()];
15862
+ return [...this.byKey.values()];
14788
15863
  }
14789
15864
  };
14790
15865
  /**
14791
15866
  * Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
14792
15867
  */
14793
15868
  function amendmentsToPOAM(amendments) {
14794
- const parties = new PartyRegistry();
15869
+ const b = {
15870
+ parties: new PartyRegistry(),
15871
+ observations: [],
15872
+ resources: [],
15873
+ completedBy: false
15874
+ };
14795
15875
  const roles = [];
14796
15876
  const responsibleParties = [];
14797
15877
  if (amendments.appliedBy) {
14798
- const uuid = parties.getOrAdd(amendments.appliedBy);
15878
+ const uuid = b.parties.getOrAdd(amendments.appliedBy);
14799
15879
  roles.push({
14800
- id: "prepared-by",
15880
+ id: ROLE_PREPARED_BY,
14801
15881
  title: "Prepared By"
14802
15882
  });
14803
15883
  responsibleParties.push({
14804
- "role-id": "prepared-by",
15884
+ "role-id": ROLE_PREPARED_BY,
14805
15885
  "party-uuids": [uuid]
14806
15886
  });
14807
15887
  }
14808
15888
  if (amendments.approvedBy) {
14809
- const uuid = parties.getOrAdd(amendments.approvedBy);
15889
+ const uuid = b.parties.getOrAdd(amendments.approvedBy);
14810
15890
  roles.push({
14811
- id: "approved-by",
15891
+ id: ROLE_APPROVED_BY,
14812
15892
  title: "Approved By"
14813
15893
  });
14814
15894
  responsibleParties.push({
14815
- "role-id": "approved-by",
15895
+ "role-id": ROLE_APPROVED_BY,
14816
15896
  "party-uuids": [uuid]
14817
15897
  });
14818
15898
  }
14819
- for (const override of amendments.overrides) parties.getOrAdd(override.appliedBy);
14820
- let importSSP;
14821
- if (amendments.systemRef && amendments.systemRef !== "") importSSP = { href: amendments.systemRef };
14822
- else importSSP = { href: "#" };
15899
+ for (const override of amendments.overrides) b.parties.getOrAdd(override.appliedBy);
14823
15900
  const poamItems = [];
14824
15901
  const risks = [];
14825
- const observations = [];
14826
15902
  for (const override of amendments.overrides) {
14827
- const { item, itemRisks, itemObs } = overrideToPOAMItem(override, parties);
15903
+ const { item, risk } = overrideToPOAMItem(override, b);
14828
15904
  poamItems.push(item);
14829
- risks.push(...itemRisks);
14830
- observations.push(...itemObs);
15905
+ risks.push(risk);
14831
15906
  }
15907
+ if (b.completedBy) roles.push({
15908
+ id: ROLE_COMPLETED_BY,
15909
+ title: "Completed By"
15910
+ });
14832
15911
  const metadata = {
14833
15912
  title: poamTitle(amendments),
14834
15913
  "last-modified": latestAppliedAt(amendments.overrides),
@@ -14836,139 +15915,201 @@ function amendmentsToPOAM(amendments) {
14836
15915
  "oscal-version": OSCAL_VERSION
14837
15916
  };
14838
15917
  if (amendments.description) metadata.remarks = amendments.description;
14839
- const partyList = parties.list();
15918
+ const partyList = b.parties.list();
14840
15919
  if (partyList.length > 0) metadata.parties = partyList;
14841
- if (roles.length > 0) {
14842
- metadata.roles = roles;
14843
- metadata["responsible-parties"] = responsibleParties;
14844
- }
15920
+ if (roles.length > 0) metadata.roles = roles;
15921
+ if (responsibleParties.length > 0) metadata["responsible-parties"] = responsibleParties;
14845
15922
  const metaProps = metadataProps(amendments);
14846
15923
  if (metaProps.length > 0) metadata.props = metaProps;
14847
15924
  const poam = {
14848
15925
  uuid: crypto.randomUUID(),
14849
15926
  metadata,
14850
- "import-ssp": importSSP,
15927
+ "import-ssp": { href: amendments.systemRef ? amendments.systemRef : "#" },
14851
15928
  "poam-items": poamItems
14852
15929
  };
14853
15930
  if (risks.length > 0) poam.risks = risks;
14854
- if (observations.length > 0) poam.observations = observations;
14855
- const resources = externalRefResources(amendments.overrides);
14856
- if (resources.length > 0) poam["back-matter"] = { resources };
15931
+ if (b.observations.length > 0) poam.observations = b.observations;
15932
+ if (b.resources.length > 0) poam["back-matter"] = { resources: b.resources };
14857
15933
  return poam;
14858
15934
  }
14859
15935
  /**
14860
- * Converts a single StandaloneOverride to a POAMItem, its associated Risk, and
14861
- * any evidence observations.
15936
+ * Carries an optional HDF string field: its prop when the value is non-empty,
15937
+ * empty-field when it is present and empty (§1.7.3), and nothing when it is
15938
+ * absent. Mirrors appendOptionalString in Go.
14862
15939
  */
14863
- function overrideToPOAMItem(override, parties) {
15940
+ function pushOptionalString(props, name, field, value) {
15941
+ if (value === void 0 || value === null) return;
15942
+ if (value === "") {
15943
+ props.push(emptyFieldProp(field));
15944
+ return;
15945
+ }
15946
+ pushVocabularyProp(props, name, value);
15947
+ }
15948
+ /** Renders HDF text as single-line OSCAL display text, falling back when there is none. */
15949
+ function displayLine(text, fallback) {
15950
+ return normalizePropValue(text) || fallback;
15951
+ }
15952
+ /** The HDF canonical trimmed-UTC form of a date, keeping its millisecond fraction. */
15953
+ function timestamp(value) {
15954
+ const d = toDate(value);
15955
+ return d ? formatTimestamp(d) : void 0;
15956
+ }
15957
+ /**
15958
+ * Converts a single StandaloneOverride to a POAMItem and its risk, adding its
15959
+ * evidence observations, back-matter resources and parties to the builder.
15960
+ */
15961
+ function overrideToPOAMItem(override, b) {
14864
15962
  const riskUUID = crypto.randomUUID();
14865
15963
  const riskStatus = override.status ? hdfStatusToOscalRiskStatus(String(override.status)) : "open";
14866
- const controlID = nistTagToControlId(override.requirementId);
14867
- const riskProps = [];
14868
- pushStringProp(riskProps, "impacted-control-id", controlID);
14869
- if (override.type) riskProps.push({
14870
- name: "override-type",
14871
- value: String(override.type)
14872
- });
14873
- if (override.impact && typeof override.impact.value === "number") riskProps.push({
14874
- name: "impact-override",
14875
- value: String(override.impact.value)
14876
- });
14877
- if (override.justification) pushStringProp(riskProps, "justification", String(override.justification));
14878
- if (override.baselineRef) pushStringProp(riskProps, "baseline-ref", override.baselineRef);
14879
- if (override.componentRef) pushStringProp(riskProps, "component-ref", override.componentRef);
14880
- const remediations = [];
14881
- if (override.milestones) for (const ms of override.milestones) {
14882
- const msProps = [];
14883
- if (ms.status) msProps.push({
14884
- name: "milestone-status",
14885
- value: String(ms.status)
14886
- });
14887
- let tasks;
14888
- const d = ms.estimatedCompletion ? toDate(ms.estimatedCompletion) : void 0;
14889
- if (d) {
14890
- const eta = formatTimestampSeconds(d);
14891
- const taskProps = milestoneCompletionProps(ms);
14892
- tasks = [{
14893
- uuid: crypto.randomUUID(),
14894
- type: "milestone",
14895
- title: ms.description,
14896
- timing: { "within-date-range": {
14897
- start: eta,
14898
- end: eta
14899
- } },
14900
- ...taskProps.length > 0 ? { props: taskProps } : {}
14901
- }];
14902
- }
14903
- remediations.push({
14904
- uuid: crypto.randomUUID(),
14905
- lifecycle: "planned",
14906
- title: ms.description,
14907
- description: ms.description,
14908
- props: msProps.length > 0 ? msProps : void 0,
14909
- tasks
14910
- });
15964
+ const applier = b.parties.getOrAdd(override.appliedBy);
15965
+ const remediations = milestoneRemediations(override.milestones ?? [], b);
15966
+ const characterizations = cvssCharacterizations(override.cvss, applier);
15967
+ const riskLog = riskLogFor(override, applier, riskStatus);
15968
+ const deadline = timestamp(override.expiresAt);
15969
+ const itemObs = [];
15970
+ for (const ev of override.evidence ?? []) {
15971
+ const obs = evidenceObservation(ev, override.appliedAt, b);
15972
+ b.observations.push(obs);
15973
+ itemObs.push(obs.uuid);
14911
15974
  }
14912
- const characterizations = [];
14913
- if (override.cvss) {
14914
- const actorUUID = parties.getOrAdd(override.appliedBy);
14915
- characterizations.push({
14916
- origin: { actors: [{
14917
- "actor-uuid": actorUUID,
14918
- type: "party"
14919
- }] },
14920
- facets: cvssFacets(override.cvss)
15975
+ const links = [];
15976
+ for (const ref of override.externalReferences ?? []) {
15977
+ const res = referenceResource(ref, b);
15978
+ b.resources.push(res);
15979
+ links.push({
15980
+ href: `#${res.uuid}`,
15981
+ rel: "reference"
14921
15982
  });
14922
15983
  }
14923
- let riskLog;
14924
- const expiresDate = override.expiresAt ? toDate(override.expiresAt) : void 0;
14925
- if (expiresDate && expiresDate.getTime() > 0) riskLog = { entries: [{
14926
- uuid: crypto.randomUUID(),
14927
- title: "Scheduled review",
14928
- description: "Amendment expiration date",
14929
- start: formatTimestampSeconds(expiresDate),
14930
- "status-change": riskStatus
14931
- }] };
14932
- const deadline = expiresDate && expiresDate.getTime() > 0 ? formatTimestampSeconds(expiresDate) : void 0;
14933
- const itemObs = [];
14934
- const relatedObs = [];
14935
- const collected = observationCollected(override);
14936
- if (override.evidence) for (const ev of override.evidence) {
14937
- const obsUUID = crypto.randomUUID();
14938
- itemObs.push(evidenceObservation(ev, obsUUID, collected));
14939
- relatedObs.push({ "observation-uuid": obsUUID });
14940
- }
14941
15984
  const rationale = riskRationale(override);
14942
15985
  const risk = {
14943
15986
  uuid: riskUUID,
14944
- title: override.requirementId,
15987
+ title: requirementTitle(override),
14945
15988
  description: rationale,
14946
15989
  statement: rationale,
15990
+ props: riskProps(override),
15991
+ ...links.length > 0 ? { links } : {},
14947
15992
  status: riskStatus,
14948
- deadline,
14949
- props: riskProps,
14950
- characterizations: characterizations.length > 0 ? characterizations : void 0,
14951
- remediations: remediations.length > 0 ? remediations : void 0,
14952
- "risk-log": riskLog
15993
+ ...deadline ? { deadline } : {},
15994
+ ...characterizations.length > 0 ? { characterizations } : {},
15995
+ ...remediations.length > 0 ? { remediations } : {},
15996
+ ...riskLog ? { "risk-log": riskLog } : {}
14953
15997
  };
14954
15998
  return {
14955
15999
  item: {
14956
16000
  uuid: crypto.randomUUID(),
14957
- title: override.requirementId,
16001
+ title: requirementTitle(override),
14958
16002
  description: override.reason,
14959
- "related-risks": [{ "risk-uuid": riskUUID }],
14960
- ...relatedObs.length > 0 ? { "related-observations": relatedObs } : {}
16003
+ ...itemObs.length > 0 ? { "related-observations": itemObs.map((uuid) => ({ "observation-uuid": uuid })) } : {},
16004
+ "related-risks": [{ "risk-uuid": riskUUID }]
14961
16005
  },
14962
- itemRisks: [risk],
14963
- itemObs
16006
+ risk
14964
16007
  };
14965
16008
  }
14966
16009
  /**
16010
+ * Carries the override's identity, disposition and scope. The FedRAMP
16011
+ * impacted-control-id is added only for a requirement id NIST defines.
16012
+ */
16013
+ function riskProps(override) {
16014
+ const props = [];
16015
+ pushVocabularyProp(props, "hdf-requirement-id", override.requirementId ?? "");
16016
+ if (nistExists(override.requirementId ?? "")) pushVocabularyProp(props, "impacted-control-id", nistTagToControlId(override.requirementId));
16017
+ pushVocabularyProp(props, "override-type", String(override.type));
16018
+ if (override.status !== void 0) pushVocabularyProp(props, "override-status", String(override.status));
16019
+ if (override.impact && typeof override.impact.value === "number") pushVocabularyProp(props, "impact-override", floatNumber(override.impact.value).token);
16020
+ if (override.justification !== void 0) pushVocabularyProp(props, "justification", String(override.justification));
16021
+ pushOptionalString(props, "baseline-ref", "baselineRef", override.baselineRef);
16022
+ pushOptionalString(props, "component-ref", "componentRef", override.componentRef);
16023
+ pushOptionalString(props, "inherited-from", "inheritedFrom", override.inheritedFrom);
16024
+ (override.affectedPackages ?? []).forEach((pkg, i) => {
16025
+ props.push(...affectedPackageProps(pkg, `package-${i + 1}`));
16026
+ });
16027
+ return props;
16028
+ }
16029
+ /** Carries one affected package, every prop in its group. */
16030
+ function affectedPackageProps(pkg, group) {
16031
+ const props = [];
16032
+ pushOptionalString(props, "affected-package-name", "name", pkg.name);
16033
+ pushOptionalString(props, "affected-package-version", "version", pkg.version);
16034
+ pushOptionalString(props, "affected-package-ecosystem", "ecosystem", pkg.ecosystem === void 0 ? void 0 : String(pkg.ecosystem));
16035
+ pushOptionalString(props, "affected-package-cpe", "cpe", pkg.cpe);
16036
+ pushOptionalString(props, "affected-package-purl", "purl", pkg.purl);
16037
+ pushOptionalString(props, "affected-package-fixed-in-version", "fixedInVersion", pkg.fixedInVersion);
16038
+ return props.map((p) => ({
16039
+ ...p,
16040
+ group
16041
+ }));
16042
+ }
16043
+ /** Records when and by whom the override was applied, and its scheduled review at expiry. */
16044
+ function riskLogFor(override, applier, riskStatus) {
16045
+ const entries = [];
16046
+ const appliedAt = timestamp(override.appliedAt);
16047
+ if (appliedAt) entries.push({
16048
+ uuid: crypto.randomUUID(),
16049
+ title: OVERRIDE_APPLIED_TITLE,
16050
+ start: appliedAt,
16051
+ "logged-by": [{ "party-uuid": applier }]
16052
+ });
16053
+ const expiresAt = timestamp(override.expiresAt);
16054
+ if (expiresAt) entries.push({
16055
+ uuid: crypto.randomUUID(),
16056
+ title: "Scheduled review",
16057
+ description: "Amendment expiration date",
16058
+ start: expiresAt,
16059
+ "status-change": riskStatus
16060
+ });
16061
+ return entries.length > 0 ? { entries } : void 0;
16062
+ }
16063
+ /**
16064
+ * Renders each milestone as a planned remediation whose title and description are
16065
+ * the milestone's and whose task carries its title, schedule, status and
16066
+ * completion. An untitled milestone is titled "Milestone <n>" and marked
16067
+ * absent-field on both objects (§1.7.4). Mirrors milestoneRemediations in Go.
16068
+ */
16069
+ function milestoneRemediations(milestones, b) {
16070
+ return milestones.map((ms, i) => {
16071
+ const marker = ms.title === void 0 ? [absentFieldProp("title")] : [];
16072
+ const title = ms.title ?? `Milestone ${i + 1}`;
16073
+ const rem = {
16074
+ uuid: crypto.randomUUID(),
16075
+ lifecycle: "planned",
16076
+ title,
16077
+ description: ms.description,
16078
+ ...marker.length > 0 ? { props: marker } : {}
16079
+ };
16080
+ const eta = timestamp(ms.estimatedCompletion);
16081
+ if (eta) {
16082
+ const props = [...marker];
16083
+ pushVocabularyProp(props, "milestone-status", String(ms.status));
16084
+ const completedAt = timestamp(ms.completedAt);
16085
+ if (completedAt) pushVocabularyProp(props, "completed-at", completedAt);
16086
+ const task = {
16087
+ uuid: crypto.randomUUID(),
16088
+ type: "milestone",
16089
+ title,
16090
+ ...props.length > 0 ? { props } : {},
16091
+ timing: { "within-date-range": {
16092
+ start: eta,
16093
+ end: eta
16094
+ } }
16095
+ };
16096
+ if (ms.completedBy) {
16097
+ b.completedBy = true;
16098
+ task["responsible-roles"] = [{
16099
+ "role-id": ROLE_COMPLETED_BY,
16100
+ "party-uuids": [b.parties.getOrAdd(ms.completedBy)]
16101
+ }];
16102
+ }
16103
+ rem.tasks = [task];
16104
+ }
16105
+ return rem;
16106
+ });
16107
+ }
16108
+ /**
14967
16109
  * Returns the most recent override appliedAt for metadata.last-modified.
14968
- * Sourcing it from the input keeps output deterministic and lets the reverse
14969
- * importer recover appliedAt. Falls back to the wall clock only when no override
14970
- * carries a date (appliedAt is schema-required, so real documents always supply
14971
- * one).
16110
+ * Sourcing it from the input keeps output deterministic. Falls back to the wall
16111
+ * clock only when no override carries a date (appliedAt is schema-required, so
16112
+ * real documents always supply one).
14972
16113
  */
14973
16114
  function latestAppliedAt(overrides) {
14974
16115
  let latest;
@@ -14979,160 +16120,182 @@ function latestAppliedAt(overrides) {
14979
16120
  return formatTimestampSeconds(latest ?? /* @__PURE__ */ new Date());
14980
16121
  }
14981
16122
  /**
14982
- * Add a property whose value OSCAL types as StringDatatype, trimming it and
14983
- * omitting the property entirely when nothing survives. A prop with an empty
14984
- * value carries no more than an absent one and is schema-invalid. Mirrors
14985
- * appendStringProp in the Go peer.
16123
+ * Sources metadata.version from the amendments document. OSCAL requires a
16124
+ * version, so an absent or empty one is written as 1.0.0 and marked by a metadata prop.
14986
16125
  */
14987
- function pushStringProp(props, name, value) {
14988
- const trimmed = oscalString(value ?? "");
14989
- if (trimmed !== "") props.push({
14990
- name,
14991
- value: trimmed
14992
- });
14993
- }
14994
- /** Sources metadata.version from the amendments document, defaulting when omitted. */
14995
16126
  function amendmentsVersion(a) {
14996
16127
  return oscalString(a.version ?? "") || "1.0.0";
14997
16128
  }
14998
16129
  /**
14999
- * Carries document identifiers and labels that have no first-class OSCAL home.
15000
- * Labels are emitted in sorted key order for deterministic output.
16130
+ * Carries the document fields that have no first-class OSCAL home and marks the
16131
+ * display fallbacks OSCAL forced. Labels are key/value prop pairs grouped in
16132
+ * sorted key order.
15001
16133
  */
15002
16134
  function metadataProps(a) {
15003
16135
  const props = [];
15004
- if (a.amendmentId) pushStringProp(props, "amendment-id", a.amendmentId);
15005
- if (a.labels) for (const [key, value] of Object.entries(a.labels).sort(([x], [y]) => x < y ? -1 : x > y ? 1 : 0)) {
15006
- const prop = labelProp(key, value);
15007
- if (prop.value !== "") props.push(prop);
15008
- }
16136
+ pushVocabularyProp(props, "amendments-name", a.name ?? "");
16137
+ pushOptionalString(props, "amendment-id", "amendmentId", a.amendmentId);
16138
+ if (a.description === "") props.push(emptyFieldProp("description"));
16139
+ pushFallbackMarker(props, "systemRef", a.systemRef);
16140
+ pushFallbackMarker(props, "version", a.version);
16141
+ Object.keys(a.labels ?? {}).sort(byCodePoint).forEach((key, i) => {
16142
+ const group = `label-${i + 1}`;
16143
+ props.push(labelProp("label-key", "key", key, group), labelProp("label-value", "value", a.labels[key], group));
16144
+ });
15009
16145
  return props;
15010
16146
  }
15011
16147
  /**
15012
- * Render one amendments label as a property. OSCAL types prop/@name as
15013
- * TokenDatatype while HDF puts no constraint on label keys, so the key is
15014
- * encoded and the source key kept in remarks when it had to change. Every label
15015
- * key in this package's converter fixtures is token-shaped today, so this guards a shape real
15016
- * data has not yet produced — but Kubernetes and OCI label keys are namespaced
15017
- * with '/', which HDF permits and OSCAL rejects.
15018
- *
15019
- * Mirrored by labelProp in the Go converter.
16148
+ * Marks an OSCAL-required field written as a display fallback: absent-field when
16149
+ * the HDF field is absent, empty-field when it is empty.
15020
16150
  */
15021
- function labelProp(key, value) {
15022
- const name = oscalToken(key) === "" ? "_" : oscalToken(key);
15023
- const prop = {
15024
- name,
15025
- value: oscalString(value),
15026
- class: "amendment-label"
16151
+ function pushFallbackMarker(props, field, value) {
16152
+ if (value === void 0) props.push(absentFieldProp(field));
16153
+ else if (value === "") props.push(emptyFieldProp(field));
16154
+ }
16155
+ /** Renders one half of an amendments label; an empty key or value is carried by empty-field (§1.7.3). */
16156
+ function labelProp(name, field, value, group) {
16157
+ return {
16158
+ ...vocabularyProp(name, value) ?? emptyFieldProp(field),
16159
+ class: "amendment-label",
16160
+ group
15027
16161
  };
15028
- if (key !== "" && name !== key) prop.remarks = key;
15029
- return prop;
15030
16162
  }
15031
- /** Picks the collection timestamp for evidence observations. */
15032
- function observationCollected(override) {
15033
- return formatTimestampSeconds(toDate(override.appliedAt) ?? /* @__PURE__ */ new Date());
16163
+ /** An RFC 3986 URI with a scheme, whose characters are all ones a URI may contain. */
16164
+ const ABSOLUTE_URI = /^[A-Za-z][A-Za-z0-9+.-]*:[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=%]*$/;
16165
+ /** Whether evidence data is carried as the observation's relevant-evidence href rather than in a back-matter resource. */
16166
+ function evidenceDataIsHref(ev) {
16167
+ switch (String(ev.type)) {
16168
+ case "url": return true;
16169
+ case "screenshot":
16170
+ case "file": return ABSOLUTE_URI.test(ev.data);
16171
+ default: return false;
16172
+ }
15034
16173
  }
15035
16174
  /** Renders a single HDF Evidence item as an OSCAL observation. */
15036
- function evidenceObservation(ev, uuid, defaultCollected) {
15037
- const captured = ev.capturedAt ? toDate(ev.capturedAt) : void 0;
15038
- const collected = captured ? formatTimestampSeconds(captured) : defaultCollected;
15039
- const desc = ev.description && ev.description !== "" ? ev.description : "supporting evidence";
15040
- const re = { description: desc };
15041
- if (ev.type === "url") re.href = ev.data;
15042
- else if (ev.data) re.remarks = ev.data;
16175
+ function evidenceObservation(ev, appliedAt, b) {
15043
16176
  const props = [];
15044
- if (ev.mimeType) pushStringProp(props, "mime-type", ev.mimeType);
15045
- if (ev.capturedBy && ev.capturedBy.identifier) pushStringProp(props, "captured-by", ev.capturedBy.identifier);
16177
+ let display = "Supporting evidence";
16178
+ let description = display;
16179
+ if (ev.description === void 0) props.push(absentFieldProp("description"));
16180
+ else {
16181
+ description = ev.description;
16182
+ display = displayLine(ev.description, display);
16183
+ }
16184
+ pushOptionalString(props, "mime-type", "mimeType", ev.mimeType);
16185
+ pushOptionalString(props, "evidence-encoding", "encoding", ev.encoding);
16186
+ if (typeof ev.size === "number") pushVocabularyProp(props, "evidence-size", floatNumber(ev.size).token);
16187
+ let collected = timestamp(ev.capturedAt);
16188
+ if (collected === void 0) {
16189
+ collected = timestamp(appliedAt) ?? formatTimestamp(/* @__PURE__ */ new Date());
16190
+ props.push(absentFieldProp("capturedAt"));
16191
+ }
16192
+ const re = { description: display };
16193
+ let links;
16194
+ if (evidenceDataIsHref(ev)) re.href = ev.data;
16195
+ else {
16196
+ const res = {
16197
+ uuid: crypto.randomUUID(),
16198
+ base64: {
16199
+ ...ev.mimeType ? { "media-type": normalizePropValue(ev.mimeType) } : {},
16200
+ value: ev.encoding === "base64" ? ev.data : encodeBase64Utf8(ev.data)
16201
+ }
16202
+ };
16203
+ b.resources.push(res);
16204
+ links = [{
16205
+ href: `#${res.uuid}`,
16206
+ rel: "evidence"
16207
+ }];
16208
+ }
15046
16209
  return {
15047
- uuid,
15048
- description: desc,
16210
+ uuid: crypto.randomUUID(),
16211
+ description,
16212
+ ...props.length > 0 ? { props } : {},
16213
+ ...links ? { links } : {},
15049
16214
  methods: ["EXAMINE"],
15050
16215
  types: [String(ev.type)],
16216
+ ...ev.capturedBy ? { origins: [{ actors: [{
16217
+ type: "party",
16218
+ "actor-uuid": b.parties.getOrAdd(ev.capturedBy)
16219
+ }] }] } : {},
15051
16220
  collected,
15052
- ...props.length > 0 ? { props } : {},
15053
16221
  "relevant-evidence": [re]
15054
16222
  };
15055
16223
  }
15056
16224
  /**
15057
- * Decomposes an HDF Cvss record into OSCAL risk facets. A version facet is
15058
- * always present so the characterization carries at least one facet.
16225
+ * Carries an HDF Cvss record as one risk characterization: a facet per present
16226
+ * field in the CVSS system for its version, attributed to the override's applier.
15059
16227
  */
15060
- function cvssFacets(c) {
16228
+ function cvssCharacterizations(c, applier) {
16229
+ if (!c) return [];
15061
16230
  const system = `http://www.first.org/cvss/v${String(c.version)}`;
15062
- const facets = [{
15063
- name: "cvss_version",
15064
- system,
15065
- value: String(c.version)
15066
- }];
15067
- const add = (name, value) => {
15068
- if (value !== void 0 && value !== null && value !== "") facets.push({
16231
+ const props = [];
16232
+ const facets = [];
16233
+ const add = (name, field, value) => {
16234
+ if (value === void 0 || value === null) return;
16235
+ if (value === "") {
16236
+ props.push(emptyFieldProp(field));
16237
+ return;
16238
+ }
16239
+ const facet = {
15069
16240
  name,
15070
16241
  system,
15071
- value
15072
- });
16242
+ value: normalizePropValue(value)
16243
+ };
16244
+ if (facet.value !== value) facet.remarks = value;
16245
+ facets.push(facet);
15073
16246
  };
15074
- add("base_score", num(c.baseScore));
15075
- add("base_severity", c.baseSeverity ? String(c.baseSeverity) : void 0);
15076
- add("base_vector", c.baseVector ?? void 0);
15077
- add("threat_score", num(c.threatScore));
15078
- add("threat_vector", c.threatVector ?? void 0);
15079
- add("environmental_score", num(c.environmentalScore));
15080
- add("environmental_vector", c.environmentalVector ?? void 0);
15081
- add("computed_score", num(c.computedScore));
15082
- add("computed_severity", c.computedSeverity ? String(c.computedSeverity) : void 0);
15083
- add("supplemental_vector", c.supplementalVector ?? void 0);
15084
- add("source", c.source ?? void 0);
15085
- return facets;
15086
- }
15087
- function num(n) {
15088
- return typeof n === "number" ? String(n) : void 0;
15089
- }
15090
- /**
15091
- * Collects every override's external references into back-matter resources —
15092
- * the OSCAL home for advisories, STIX, and CTI feeds.
15093
- */
15094
- function externalRefResources(overrides) {
15095
- const resources = [];
15096
- for (const o of overrides) {
15097
- if (!o.externalReferences) continue;
15098
- for (const ref of o.externalReferences) {
15099
- const res = {
15100
- uuid: crypto.randomUUID(),
15101
- title: ref.sourceName
15102
- };
15103
- if (ref.description && ref.description !== "") res.description = ref.description;
15104
- if (ref.href && ref.href !== "") res.rlinks = [{ href: ref.href }];
15105
- const props = [];
15106
- if (ref.sourceName) props.push({
15107
- name: "source-name",
15108
- value: ref.sourceName
15109
- });
15110
- if (ref.externalId) props.push({
15111
- name: "external-id",
15112
- value: ref.externalId
15113
- });
15114
- if (props.length > 0) res.props = props;
15115
- resources.push(res);
15116
- }
15117
- }
15118
- return resources;
16247
+ const score = (name, field, value) => {
16248
+ if (typeof value === "number") add(name, field, floatNumber(value).token);
16249
+ };
16250
+ add("cvss_version", "version", String(c.version));
16251
+ score("base_score", "baseScore", c.baseScore);
16252
+ add("base_severity", "baseSeverity", c.baseSeverity === void 0 ? void 0 : String(c.baseSeverity));
16253
+ add("base_vector", "baseVector", c.baseVector);
16254
+ score("threat_score", "threatScore", c.threatScore);
16255
+ add("threat_vector", "threatVector", c.threatVector);
16256
+ score("environmental_score", "environmentalScore", c.environmentalScore);
16257
+ add("environmental_vector", "environmentalVector", c.environmentalVector);
16258
+ score("computed_score", "computedScore", c.computedScore);
16259
+ add("computed_severity", "computedSeverity", c.computedSeverity === void 0 ? void 0 : String(c.computedSeverity));
16260
+ add("supplemental_vector", "supplementalVector", c.supplementalVector);
16261
+ add("source", "source", c.source);
16262
+ return [{
16263
+ ...props.length > 0 ? { props } : {},
16264
+ origin: { actors: [{
16265
+ type: "party",
16266
+ "actor-uuid": applier
16267
+ }] },
16268
+ facets
16269
+ }];
15119
16270
  }
15120
- /**
15121
- * Carries the actual completion attribution that the estimated-completion
15122
- * timing cannot express.
15123
- */
15124
- function milestoneCompletionProps(ms) {
16271
+ /** Carries one external reference as a back-matter resource. */
16272
+ function referenceResource(ref, b) {
15125
16273
  const props = [];
15126
- const completedAt = ms.completedAt ? toDate(ms.completedAt) : void 0;
15127
- if (completedAt) props.push({
15128
- name: "completed-at",
15129
- value: formatTimestampSeconds(completedAt)
15130
- });
15131
- if (ms.completedBy && ms.completedBy.identifier) props.push({
15132
- name: "completed-by",
15133
- value: ms.completedBy.identifier
15134
- });
15135
- return props;
16274
+ pushVocabularyProp(props, "source-name", ref.sourceName ?? "");
16275
+ pushOptionalString(props, "external-id", "externalId", ref.externalId);
16276
+ if (ref.href === "") props.push(emptyFieldProp("href"));
16277
+ if (ref.description === "") props.push(emptyFieldProp("description"));
16278
+ pushOptionalString(props, "reference-rel", "rel", ref.rel);
16279
+ pushOptionalString(props, "reference-media-type", "mediaType", ref.mediaType);
16280
+ if (ref.checksum) {
16281
+ pushOptionalString(props, "checksum-algorithm", "checksum.algorithm", String(ref.checksum.algorithm));
16282
+ pushOptionalString(props, "checksum-value", "checksum.value", ref.checksum.value);
16283
+ }
16284
+ if (ref.addedBy) pushVocabularyProp(props, "added-by", b.parties.getOrAdd(ref.addedBy));
16285
+ const addedAt = timestamp(ref.addedAt);
16286
+ if (addedAt) pushVocabularyProp(props, "added-at", addedAt);
16287
+ pushOptionalString(props, "reference-kind", "kind", ref.kind);
16288
+ return {
16289
+ uuid: crypto.randomUUID(),
16290
+ title: ref.sourceName,
16291
+ ...ref.description ? { description: ref.description } : {},
16292
+ ...props.length > 0 ? { props } : {},
16293
+ ...ref.href ? { rlinks: [{ href: ref.href }] } : {},
16294
+ ...ref.document ? { base64: {
16295
+ "media-type": "application/json",
16296
+ value: encodeBase64Utf8(stringifyLine(canonicalize$1(ref.document)))
16297
+ } } : {}
16298
+ };
15136
16299
  }
15137
16300
  //#endregion
15138
16301
  //#region converters/ionchannel-to-hdf/typescript/converter.ts
@@ -15231,7 +16394,8 @@ function analysisTimestamp(a) {
15231
16394
  }
15232
16395
  function buildScanRequirement(scan, analysis) {
15233
16396
  const title = scan.description || `${titleCaseFirst(scan.name)} scan`;
15234
- const descriptions = [createDescription("default", scan.summary || `${titleCaseFirst(scan.name)} scan summary`)];
16397
+ const desc = scan.summary || `${titleCaseFirst(scan.name)} scan summary`;
16398
+ const descriptions = [createDescription("default", desc)];
15235
16399
  const results = [{
15236
16400
  status: ResultStatus.NotReviewed,
15237
16401
  codeDesc: `${scan.name} scan summary`,
@@ -15296,11 +16460,13 @@ async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
15296
16460
  outdated_version: dep.outdated_version,
15297
16461
  dependencies: dep.dependencies
15298
16462
  }, null, 2);
15299
- const req = createRequirement(depId, title, [createDescription("default", `Dependency ${dep.org}/${dep.name}`)], 0, [{
16463
+ const descriptions = [createDescription("default", `Dependency ${dep.org}/${dep.name}`)];
16464
+ const results = [{
15300
16465
  status: ResultStatus.NotReviewed,
15301
16466
  codeDesc: "Dependency inventory item",
15302
16467
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
15303
- }], { tags });
16468
+ }];
16469
+ const req = createRequirement(depId, title, descriptions, 0, results, { tags });
15304
16470
  req.code = code;
15305
16471
  req.controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
15306
16472
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -16128,7 +17294,7 @@ async function convertSpdxVexToHdf(input, converterVersion = "1.0.0") {
16128
17294
  if (o) overrides.push(o);
16129
17295
  }
16130
17296
  if (overrides.length === 0) throw new Error("spdx-vex-to-hdf: SPDX document contains no actionable VEX statements (only affected/under_investigation); no amendment to write");
16131
- const appliedBy = documentIdentity(graph, idx);
17297
+ const appliedBy = documentIdentity$1(graph, idx);
16132
17298
  const name = appliedBy.identifier && appliedBy.identifier !== DEFAULT_APPLIED_BY ? `SPDX VEX statements from ${appliedBy.identifier}` : "SPDX VEX statements";
16133
17299
  await chainOverrides(overrides);
16134
17300
  return {
@@ -16238,7 +17404,7 @@ function identityFor(creationInfoRef, idx) {
16238
17404
  identifier: DEFAULT_APPLIED_BY
16239
17405
  };
16240
17406
  }
16241
- function documentIdentity(graph, idx) {
17407
+ function documentIdentity$1(graph, idx) {
16242
17408
  for (const el of graph) if (el.type === "SpdxDocument") {
16243
17409
  const id = identityFor(el.creationInfo, idx);
16244
17410
  if (id.identifier !== DEFAULT_APPLIED_BY) return id;
@@ -16339,19 +17505,58 @@ function isCvssType(type) {
16339
17505
  const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
16340
17506
  const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
16341
17507
  const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
16342
- const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
16343
- /** Map an HDF Cvss block to a CSAF score entry (cvss_v2/v3/v4 by version). */
16344
- function buildCsafScore(cvss, products) {
17508
+ const DEFAULT_PRODUCT_ID = "HDFPID-0001";
17509
+ const DEFAULT_PRODUCT_NAME = "No product identity was recorded in the source amendment";
17510
+ /**
17511
+ * Map an HDF Cvss_Severity band to the FIRST.org severityType enum: the same
17512
+ * five bands, which FIRST spells uppercase. Pinned in both languages to
17513
+ * fixtures/cvss-score-cases.json and to the vendored schema's enum.
17514
+ */
17515
+ function csafSeverity(s) {
17516
+ return s.toUpperCase();
17517
+ }
17518
+ /**
17519
+ * Map an HDF Cvss block to a CSAF score entry, or undefined when the block
17520
+ * cannot be exported.
17521
+ *
17522
+ * A score is emitted only when every field the FIRST.org schema for its
17523
+ * version requires is present: version, vectorString and baseScore, plus
17524
+ * baseSeverity for 3.x and 4.0 (2.0 defines no severity, so none is emitted
17525
+ * for it). HDF requires only version, so legal HDF can fall short, and the
17526
+ * choice is to drop such a block rather than emit it partially: a partial
17527
+ * score fails the CSAF schema, and conforming consumers reject a document that
17528
+ * fails it, so it would cost every statement in the export, not just itself.
17529
+ * Nothing is synthesized to close the gap — a vector cannot be recovered from
17530
+ * a score, and a band the source never stated would be asserted in its name —
17531
+ * and the drop is logged so the loss is visible. A block with no base metrics
17532
+ * at all is consumer enrichment by design (threat or environmental deltas on a
17533
+ * riskAdjustment); CSAF scores carry base metrics, so nothing exportable was
17534
+ * lost and it is skipped silently.
17535
+ */
17536
+ function buildCsafScore(cvss, cve, products) {
17537
+ const { version, baseVector, baseScore, baseSeverity } = cvss;
17538
+ const hasVector = Boolean(baseVector);
17539
+ const hasScore = typeof baseScore === "number";
17540
+ const hasSeverity = typeof baseSeverity === "string";
17541
+ if (!hasVector && !hasScore && !hasSeverity) return void 0;
17542
+ const v2 = version.startsWith("2");
17543
+ if (!baseVector || typeof baseScore !== "number" || !v2 && !hasSeverity) {
17544
+ emitConverterWarning(`hdf-to-csaf-vex: ${cve}: CVSS ${version} score not exported, CSAF requires ${[
17545
+ ...hasVector ? [] : ["vectorString"],
17546
+ ...hasScore ? [] : ["baseScore"],
17547
+ ...v2 || hasSeverity ? [] : ["baseSeverity"]
17548
+ ].join(", ")}`);
17549
+ return;
17550
+ }
16345
17551
  const inner = {
16346
- version: cvss.version,
16347
- ...cvss.baseVector && { vectorString: cvss.baseVector },
16348
- ...typeof cvss.baseScore === "number" && { baseScore: cvss.baseScore },
16349
- ...cvss.baseSeverity && { baseSeverity: cvss.baseSeverity }
17552
+ version,
17553
+ vectorString: baseVector,
17554
+ baseScore
16350
17555
  };
16351
- if (inner.vectorString === void 0 && inner.baseScore === void 0) return;
17556
+ if (!v2 && hasSeverity) inner.baseSeverity = csafSeverity(baseSeverity);
16352
17557
  return {
16353
- [cvss.version.startsWith("4") ? "cvss_v4" : cvss.version.startsWith("2") ? "cvss_v2" : "cvss_v3"]: inner,
16354
- products
17558
+ products,
17559
+ [version.startsWith("4") ? "cvss_v4" : v2 ? "cvss_v2" : "cvss_v3"]: inner
16355
17560
  };
16356
17561
  }
16357
17562
  function convertHdfToCsafVex(input, converterVersion) {
@@ -16374,8 +17579,8 @@ function convertHdfToCsafVex(input, converterVersion) {
16374
17579
  const doc = buildDocument(amendments, converterVersion);
16375
17580
  doc.vulnerabilities = vulnerabilities;
16376
17581
  doc.product_tree.full_product_names = names.length > 0 ? names : [{
16377
- name: DEFAULT_PRODUCT_ID$1,
16378
- product_id: DEFAULT_PRODUCT_ID$1
17582
+ name: DEFAULT_PRODUCT_NAME,
17583
+ product_id: DEFAULT_PRODUCT_ID
16379
17584
  }];
16380
17585
  return JSON.stringify(doc, null, 2);
16381
17586
  }
@@ -16428,8 +17633,8 @@ function productEntriesFor(o) {
16428
17633
  }));
16429
17634
  }
16430
17635
  return [{
16431
- name: DEFAULT_PRODUCT_ID$1,
16432
- product_id: DEFAULT_PRODUCT_ID$1
17636
+ name: DEFAULT_PRODUCT_NAME,
17637
+ product_id: DEFAULT_PRODUCT_ID
16433
17638
  }];
16434
17639
  }
16435
17640
  /** product_tree entries for the synthesized fixed-version products. */
@@ -16468,7 +17673,7 @@ function productIDsFor$1(o) {
16468
17673
  const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
16469
17674
  if (parts.length > 0) return parts;
16470
17675
  }
16471
- return [DEFAULT_PRODUCT_ID$1];
17676
+ return [DEFAULT_PRODUCT_ID];
16472
17677
  }
16473
17678
  function stripProductsLine$1(reason) {
16474
17679
  return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
@@ -16524,7 +17729,7 @@ function buildVulnerability(group) {
16524
17729
  for (const o of group.overrides) {
16525
17730
  const pids = productIDsFor$1(o);
16526
17731
  if (o.cvss) {
16527
- const score = buildCsafScore(o.cvss, pids);
17732
+ const score = buildCsafScore(o.cvss, group.cve, pids);
16528
17733
  if (score) {
16529
17734
  v.scores = (v.scores ?? []).concat(score);
16530
17735
  emitted = true;
@@ -16852,7 +18057,6 @@ const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
16852
18057
  const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
16853
18058
  const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
16854
18059
  const RESPONSE_LINE = /^Response:.*$/gm;
16855
- const DEFAULT_PRODUCT_ID = "HDFPID-0001";
16856
18060
  /**
16857
18061
  * Build CycloneDX affects[].versions[] for a package with a fixedInVersion:
16858
18062
  * the current version is `affected`, and `>= fixedInVersion` is `unaffected`,
@@ -16943,7 +18147,7 @@ async function convertHdfToCyclonedxVex(input, converterVersion) {
16943
18147
  serialNumber: await buildSerialNumber(input, amendments),
16944
18148
  version: 1,
16945
18149
  metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion, overrideIdentities),
16946
- components,
18150
+ ...components.length > 0 && { components },
16947
18151
  vulnerabilities
16948
18152
  };
16949
18153
  return JSON.stringify(bom, null, 2);
@@ -16974,7 +18178,7 @@ function overrideToVulnerability(o, componentRegistry) {
16974
18178
  references.push({
16975
18179
  id: o.requirementId,
16976
18180
  source: {
16977
- name: e.description ?? "",
18181
+ ...e.description && { name: e.description },
16978
18182
  url: e.data
16979
18183
  }
16980
18184
  });
@@ -17000,13 +18204,13 @@ function overrideToVulnerability(o, componentRegistry) {
17000
18204
  ...unranged && { recommendation: `Upgrade to ${unranged.fixedInVersion}` },
17001
18205
  ...advisories.length > 0 && { advisories },
17002
18206
  analysis,
17003
- affects: pids.map((pid) => {
18207
+ ...pids.length > 0 && { affects: pids.map((pid) => {
17004
18208
  const versions = buildCdxAffectsVersions(pkgById.get(pid));
17005
18209
  return versions ? {
17006
18210
  ref: pid,
17007
18211
  versions
17008
18212
  } : { ref: pid };
17009
- })
18213
+ }) }
17010
18214
  };
17011
18215
  }
17012
18216
  function canonicalToCycloneDXState(canonical) {
@@ -17040,7 +18244,7 @@ function productIDsFor(o) {
17040
18244
  const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
17041
18245
  if (parts.length > 0) return parts;
17042
18246
  }
17043
- return [DEFAULT_PRODUCT_ID];
18247
+ return [];
17044
18248
  }
17045
18249
  function stripReasonAnnotations(reason) {
17046
18250
  return reason.replace(PRODUCTS_LINE, "").replace(RAW_JUST_LINE, "").replace(RESPONSE_LINE, "").trim();
@@ -17240,11 +18444,13 @@ async function convertOscalComponentToHdf(input) {
17240
18444
  if (!compDef.components || compDef.components.length === 0) throw new Error("oscal-component-definition: document contains no components");
17241
18445
  const integrity = await inputIntegrity(input);
17242
18446
  const meta = extractMetadata(compDef.metadata);
17243
- const comp = compDef.components[0];
17244
18447
  const requirements = [];
17245
- for (const ci of comp["control-implementations"] ?? []) for (const ir of ci["implemented-requirements"] ?? []) requirements.push(implementedRequirementToBaselineRequirement(ir));
18448
+ for (const comp of compDef.components) for (const ci of comp["control-implementations"] ?? []) {
18449
+ const limited = limitArrayWithWarning(ci["implemented-requirements"] ?? [], "implemented requirement");
18450
+ for (const ir of limited) requirements.push(implementedRequirementToBaselineRequirement(ir, comp));
18451
+ }
17246
18452
  const baseline = {
17247
- name: componentBaselineName(comp, compDef),
18453
+ name: definitionBaselineName(compDef),
17248
18454
  title: meta.title,
17249
18455
  version: meta.version,
17250
18456
  status: "loaded",
@@ -17258,7 +18464,16 @@ async function convertOscalComponentToHdf(input) {
17258
18464
  return JSON.stringify(baseline, null, 2);
17259
18465
  }
17260
18466
  /** Converts a single ImplementedRequirement to a BaselineRequirement. */
17261
- function implementedRequirementToBaselineRequirement(ir) {
18467
+ /**
18468
+ * Qualifies the control with the component's uuid. OSCAL requires both a uuid
18469
+ * and a title on every component but only guarantees the uuid is unique, so a
18470
+ * title would still collide when one product appears twice — the duplicate-ID
18471
+ * defect this qualification prevents. The title rides in a tag instead.
18472
+ */
18473
+ function requirementId(comp, nistTag) {
18474
+ return comp?.uuid ? `${comp.uuid}/${nistTag}` : nistTag;
18475
+ }
18476
+ function implementedRequirementToBaselineRequirement(ir, comp) {
17262
18477
  const nistTag = controlIdToNistTag(ir["control-id"]);
17263
18478
  const descriptions = [];
17264
18479
  descriptions.push({
@@ -17275,23 +18490,37 @@ function implementedRequirementToBaselineRequirement(ir) {
17275
18490
  data: stmt.remarks
17276
18491
  });
17277
18492
  }
18493
+ const tags = { nist: [nistTag] };
18494
+ if (comp?.title) tags.component = comp.title;
18495
+ if (comp?.uuid) tags.componentUuid = comp.uuid;
17278
18496
  return {
17279
- id: nistTag,
18497
+ id: requirementId(comp, nistTag),
17280
18498
  title: nistTag,
17281
18499
  impact: .5,
17282
18500
  descriptions,
17283
- tags: { nist: [nistTag] }
18501
+ tags
17284
18502
  };
17285
18503
  }
17286
- /** Derives a baseline name from the component or component-definition metadata. */
17287
- function componentBaselineName(comp, compDef) {
17288
- return toKebabCase(comp.title || compDef.metadata.title, "oscal-component-definition");
18504
+ /**
18505
+ * Derives the baseline name: a single-component definition is named for its
18506
+ * component, a multi-component one for the definition itself.
18507
+ */
18508
+ function definitionBaselineName(compDef) {
18509
+ let name = compDef.metadata.title;
18510
+ if (compDef.components?.length === 1 && compDef.components[0].title) name = compDef.components[0].title;
18511
+ return toKebabCase(name, "oscal-component-definition");
17289
18512
  }
17290
18513
  //#endregion
17291
18514
  //#region converters/oscal-to-hdf/typescript/converter-poam.ts
17292
18515
  /**
17293
18516
  * OSCAL Plan of Action and Milestones (POA&M) to HDF Amendments converter.
17294
18517
  *
18518
+ * A POA&M hdf-to-oscal-poam produced returns every amendments field it carries
18519
+ * (ADR-0014 §4.6), apart from the integrity fields and generator that contract
18520
+ * excludes. Foreign POA&Ms, and HDF exports from before the ADR (§4.3), keep the
18521
+ * mapping they had before it: type "poam", with their own extension props not
18522
+ * carried (§3.6).
18523
+ *
17295
18524
  * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_poam.go.
17296
18525
  */
17297
18526
  /**
@@ -17310,30 +18539,113 @@ async function convertOscalPoamToHdf(input) {
17310
18539
  const meta = extractMetadata(poam.metadata);
17311
18540
  const riskMap = buildRiskMap$1(poam.risks ?? []);
17312
18541
  const overrides = [];
17313
- for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
17314
- const systemRef = poam["import-ssp"]?.href || void 0;
17315
- const appliedBy = extractAppliedBy(poam.metadata);
18542
+ let hdfProduced = false;
18543
+ for (const item of poam["poam-items"]) {
18544
+ const override = poamItemToOverride(item, riskMap, poam);
18545
+ if (!override) continue;
18546
+ if (hdfProducedRisk(item, riskMap)) hdfProduced = true;
18547
+ overrides.push(override);
18548
+ }
18549
+ if (overrides.length === 0) throw new Error(`oscal-poam-to-hdf: no poam-item names a requirement; every one of the ${poam["poam-items"].length} items was skipped`);
17316
18550
  await chainOverrides(overrides);
17317
- return serializeHdf({
18551
+ const preparedBy = firstResponsibleParty(poam.metadata, "prepared-by");
18552
+ const amendments = {
17318
18553
  name: toKebabCase(poam.metadata.title, "oscal-poam"),
17319
18554
  overrides,
17320
18555
  integrity,
17321
- systemRef,
18556
+ systemRef: poam["import-ssp"]?.href || void 0,
17322
18557
  version: meta.version,
17323
- appliedBy,
18558
+ appliedBy: preparedBy === "" ? void 0 : {
18559
+ type: IdentityType.Simple,
18560
+ identifier: preparedBy
18561
+ },
17324
18562
  generator: {
17325
18563
  name: "oscal-poam-to-hdf",
17326
18564
  version: "1.0.0"
17327
18565
  }
17328
- });
18566
+ };
18567
+ if (hdfProduced) readHdfDocumentFields(poam, amendments);
18568
+ return serializeHdf(amendments);
18569
+ }
18570
+ /**
18571
+ * Reads the amendments document fields from their ADR-0014 §4.6 homes, which a
18572
+ * POA&M carries only when one of its risks is HDF-produced (§4.3).
18573
+ */
18574
+ function readHdfDocumentFields(poam, amendments) {
18575
+ const props = poam.metadata.props;
18576
+ amendments.name = findGroupedVocabularyProp(props, "amendments-name", "")?.value ?? "";
18577
+ if (poam.metadata.remarks) amendments.description = poam.metadata.remarks;
18578
+ else if (hasFieldMarker(props, "empty-field", "description", "")) amendments.description = "";
18579
+ amendments.appliedBy = documentIdentity(poam, "prepared-by");
18580
+ const approvedBy = partyIdentity(poam, firstResponsibleParty(poam.metadata, "approved-by"));
18581
+ if (approvedBy) amendments.approvedBy = approvedBy;
18582
+ const amendmentId = vocabularyString(props, "amendment-id", "amendmentId", "");
18583
+ if (amendmentId !== void 0) amendments.amendmentId = amendmentId;
18584
+ const labels = amendmentLabels(props);
18585
+ if (labels) amendments.labels = labels;
18586
+ amendments.systemRef = markedField(props, "systemRef", amendments.systemRef);
18587
+ amendments.version = markedField(props, "version", amendments.version);
18588
+ }
18589
+ /**
18590
+ * Applies the metadata markers for an OSCAL-required field the exporter wrote a
18591
+ * display fallback for: absent-field leaves it absent and empty-field returns it empty.
18592
+ */
18593
+ function markedField(props, field, value) {
18594
+ if (hasFieldMarker(props, "absent-field", field, "")) return void 0;
18595
+ if (hasFieldMarker(props, "empty-field", field, "")) return "";
18596
+ return value;
18597
+ }
18598
+ /** Reads the label-key and label-value pairs, one per label-<n> group. */
18599
+ function amendmentLabels(props) {
18600
+ const groups = /* @__PURE__ */ new Set();
18601
+ for (const m of [...findVocabularyProps(props, "label-key"), ...findVocabularyProps(props, "empty-field")]) {
18602
+ const p = props[m.index];
18603
+ if (p.class === "amendment-label" && p.group) groups.add(p.group);
18604
+ }
18605
+ if (groups.size === 0) return void 0;
18606
+ const labels = {};
18607
+ for (const group of groups) {
18608
+ const key = vocabularyString(props, "label-key", "key", group);
18609
+ const value = vocabularyString(props, "label-value", "value", group);
18610
+ if (key !== void 0 && value !== void 0) labels[key] = value;
18611
+ }
18612
+ return labels;
17329
18613
  }
17330
18614
  function buildRiskMap$1(risks) {
17331
18615
  const m = /* @__PURE__ */ new Map();
17332
18616
  for (const risk of risks) m.set(risk.uuid, risk);
17333
18617
  return m;
17334
18618
  }
18619
+ /**
18620
+ * The item's first related risk that carries override-type in the HDF namespace,
18621
+ * which makes it HDF-produced (ADR-0014 §4.3), or undefined.
18622
+ */
18623
+ function hdfProducedRisk(item, riskMap) {
18624
+ return overrideTypeRisk(item, riskMap, false);
18625
+ }
18626
+ /** The item's first related risk whose override-type prop is (legacy) or is not (!legacy) matched through the pre-ADR fallback. */
18627
+ function overrideTypeRisk(item, riskMap, legacy) {
18628
+ for (const rr of item["related-risks"] ?? []) {
18629
+ const risk = rr["risk-uuid"] ? riskMap.get(rr["risk-uuid"]) : void 0;
18630
+ if (risk && findVocabularyProp(risk.props, "override-type")?.legacy === legacy) return risk;
18631
+ }
18632
+ }
18633
+ /** Converts a poam-item to an override, or undefined for a pre-ADR item with no requirement id, which is skipped. */
17335
18634
  function poamItemToOverride(item, riskMap, poam) {
17336
- const requirementId = extractRequirementIdFromPOAMItem(item, riskMap);
18635
+ const risk = hdfProducedRisk(item, riskMap);
18636
+ if (risk) {
18637
+ if (hdfRequirementId(risk) === void 0) {
18638
+ emitConverterWarning(`Skipping poam-item "${item.uuid}" titled "${item.title}": its HDF-produced risk has no hdf-requirement-id`);
18639
+ return;
18640
+ }
18641
+ return hdfOverride(item, risk, riskMap, poam);
18642
+ }
18643
+ const requirementId = foreignRequirementId(item, riskMap);
18644
+ if (requirementId === void 0) {
18645
+ emitConverterWarning(`Skipping poam-item "${item.uuid}" titled "${item.title}": its pre-ADR risk has no impacted-control-id`);
18646
+ return;
18647
+ }
18648
+ const milestones = extractMilestones(item, riskMap);
17337
18649
  return {
17338
18650
  type: OverrideType.Poam,
17339
18651
  requirementId,
@@ -17342,9 +18654,306 @@ function poamItemToOverride(item, riskMap, poam) {
17342
18654
  appliedBy: poamItemAppliedBy(poam),
17343
18655
  appliedAt: poamItemAppliedAt(poam, requirementId),
17344
18656
  expiresAt: poamItemExpiresAt(item, riskMap, requirementId),
17345
- milestones: extractMilestones(item, riskMap)
18657
+ ...milestones.length > 0 ? { milestones } : {}
18658
+ };
18659
+ }
18660
+ /**
18661
+ * The requirement id an HDF-produced risk carries. An absent or empty prop names
18662
+ * no requirement, and StandaloneOverride.requirementId is required and non-empty,
18663
+ * so the item is skipped rather than imported unusable.
18664
+ */
18665
+ function hdfRequirementId(risk) {
18666
+ return findVocabularyProp(risk.props, "hdf-requirement-id")?.value || void 0;
18667
+ }
18668
+ /**
18669
+ * Reads every override field from its ADR-0014 §4.6 home on an HDF-produced risk.
18670
+ * An absent prop is an absent field: nothing is derived from titles or uuids.
18671
+ */
18672
+ function hdfOverride(item, risk, riskMap, poam) {
18673
+ const props = risk.props;
18674
+ const requirementId = hdfRequirementId(risk) ?? "";
18675
+ const entry = overrideAppliedEntry(risk);
18676
+ const appliedAt = entry && parseTimestamp(String(entry.start)) || poamItemAppliedAt(poam, requirementId);
18677
+ const override = {
18678
+ type: findVocabularyProp(props, "override-type").value,
18679
+ requirementId,
18680
+ reason: item.description,
18681
+ appliedBy: poamItemAppliedBy(poam),
18682
+ appliedAt,
18683
+ expiresAt: poamItemExpiresAt(item, riskMap, requirementId)
18684
+ };
18685
+ const status = findVocabularyProp(props, "override-status");
18686
+ if (status) override.status = status.value;
18687
+ const impact = findVocabularyProp(props, "impact-override");
18688
+ if (impact && impact.value.trim() !== "" && !isNaN(Number(impact.value))) override.impact = { value: Number(impact.value) };
18689
+ const justification = findVocabularyProp(props, "justification");
18690
+ if (justification) override.justification = justification.value;
18691
+ const baselineRef = vocabularyString(props, "baseline-ref", "baselineRef", "");
18692
+ if (baselineRef !== void 0) override.baselineRef = baselineRef;
18693
+ const componentRef = vocabularyString(props, "component-ref", "componentRef", "");
18694
+ if (componentRef !== void 0) override.componentRef = componentRef;
18695
+ const inheritedFrom = vocabularyString(props, "inherited-from", "inheritedFrom", "");
18696
+ if (inheritedFrom !== void 0) override.inheritedFrom = inheritedFrom;
18697
+ const packages = affectedPackages(props);
18698
+ if (packages.length > 0) override.affectedPackages = packages;
18699
+ const loggedBy = entry?.["logged-by"]?.[0]?.["party-uuid"];
18700
+ const applier = loggedBy === void 0 ? void 0 : partyIdentity(poam, loggedBy);
18701
+ if (applier) override.appliedBy = applier;
18702
+ const milestones = hdfMilestones(risk, poam);
18703
+ if (milestones.length > 0) override.milestones = milestones;
18704
+ const cvss = riskCvss(risk);
18705
+ if (cvss) override.cvss = cvss;
18706
+ const evidence = itemEvidence(item, poam);
18707
+ if (evidence.length > 0) override.evidence = evidence;
18708
+ const refs = riskReferences(risk, poam);
18709
+ if (refs.length > 0) override.externalReferences = refs;
18710
+ return override;
18711
+ }
18712
+ /** The risk-log entry recording when the override was applied. */
18713
+ function overrideAppliedEntry(risk) {
18714
+ return risk["risk-log"]?.entries.find((e) => e.title === "Override applied");
18715
+ }
18716
+ /** Reads the package-<n> prop groups in position order. */
18717
+ function affectedPackages(props) {
18718
+ const positions = /* @__PURE__ */ new Set();
18719
+ for (const p of props ?? []) {
18720
+ const m = /^package-(\d+)$/.exec(p.group ?? "");
18721
+ if (m && consumedVocabularyProp(p) && Number(m[1]) > 0) positions.add(Number(m[1]));
18722
+ }
18723
+ return [...positions].sort((a, b) => a - b).map((position) => {
18724
+ const group = `package-${position}`;
18725
+ const pkg = {};
18726
+ const name = vocabularyString(props, "affected-package-name", "name", group);
18727
+ if (name !== void 0) pkg.name = name;
18728
+ const version = vocabularyString(props, "affected-package-version", "version", group);
18729
+ if (version !== void 0) pkg.version = version;
18730
+ const ecosystem = vocabularyString(props, "affected-package-ecosystem", "ecosystem", group);
18731
+ if (ecosystem !== void 0) pkg.ecosystem = ecosystem;
18732
+ const cpe = vocabularyString(props, "affected-package-cpe", "cpe", group);
18733
+ if (cpe !== void 0) pkg.cpe = cpe;
18734
+ const purl = vocabularyString(props, "affected-package-purl", "purl", group);
18735
+ if (purl !== void 0) pkg.purl = purl;
18736
+ const fixedInVersion = vocabularyString(props, "affected-package-fixed-in-version", "fixedInVersion", group);
18737
+ if (fixedInVersion !== void 0) pkg.fixedInVersion = fixedInVersion;
18738
+ return pkg;
18739
+ });
18740
+ }
18741
+ /** The first party uuid metadata assigns to roleId, or ''. */
18742
+ function firstResponsibleParty(meta, roleId) {
18743
+ for (const rp of meta["responsible-parties"] ?? []) if (rp["role-id"] === roleId && rp["party-uuids"].length > 0) return rp["party-uuids"][0];
18744
+ return "";
18745
+ }
18746
+ /** Reads the HDF identity a metadata party carries, or undefined for a party that carries none. */
18747
+ function partyIdentity(poam, partyUuid) {
18748
+ const party = (poam.metadata.parties ?? []).find((p) => p.uuid === partyUuid);
18749
+ if (!party) return void 0;
18750
+ const identityType = findVocabularyProp(party.props, "identity-type");
18751
+ if (!identityType) return void 0;
18752
+ const id = {
18753
+ identifier: findVocabularyProp(party.props, "identity-identifier")?.value ?? "",
18754
+ type: identityType.value
18755
+ };
18756
+ if (party.remarks) id.description = party.remarks;
18757
+ else if (hasFieldMarker(party.props, "empty-field", "description", "")) id.description = "";
18758
+ return id;
18759
+ }
18760
+ /** The identity metadata assigns to roleId: the HDF identity its party carries, or else the party uuid as a simple identifier. */
18761
+ function documentIdentity(poam, roleId) {
18762
+ const partyUuid = firstResponsibleParty(poam.metadata, roleId);
18763
+ if (partyUuid === "") return void 0;
18764
+ return partyIdentity(poam, partyUuid) ?? {
18765
+ type: IdentityType.Simple,
18766
+ identifier: partyUuid
17346
18767
  };
17347
18768
  }
18769
+ /**
18770
+ * Mirrors the schema's Milestone.title pattern: an OSCAL task title is
18771
+ * markup-line, which a foreign producer may wrap or pad.
18772
+ */
18773
+ const MILESTONE_TITLE_PATTERN = /^[^ \t\n\r]([^\n\r]*[^ \t\n\r])?$/;
18774
+ /** Reads each planned remediation task of an HDF-produced risk as a milestone whose description is the remediation description. */
18775
+ function hdfMilestones(risk, poam) {
18776
+ const milestones = [];
18777
+ for (const rem of risk.remediations ?? []) {
18778
+ if (rem.lifecycle !== "planned") continue;
18779
+ for (const task of rem.tasks ?? []) {
18780
+ const end = task.timing?.["within-date-range"]?.end;
18781
+ const estimatedCompletion = end && parseTimestamp(String(end));
18782
+ if (!estimatedCompletion) continue;
18783
+ const status = findVocabularyProp(task.props, "milestone-status");
18784
+ const ms = {
18785
+ description: rem.description,
18786
+ estimatedCompletion,
18787
+ status: status?.value ?? MilestoneStatus.Pending
18788
+ };
18789
+ if (!hasFieldMarker(task.props, "absent-field", "title", "")) {
18790
+ const title = task.title ?? "";
18791
+ if (MILESTONE_TITLE_PATTERN.test(title)) ms.title = title;
18792
+ else emitConverterWarning(`Dropping the title of task "${task.uuid}": ${JSON.stringify(title)} is not the single line Milestone.title is`);
18793
+ }
18794
+ const completedAt = findVocabularyProp(task.props, "completed-at");
18795
+ const completedAtDate = completedAt && parseTimestamp(completedAt.value);
18796
+ if (completedAtDate) ms.completedAt = completedAtDate;
18797
+ for (const rr of task["responsible-roles"] ?? []) {
18798
+ const partyUuid = rr["party-uuids"]?.[0];
18799
+ const completer = rr["role-id"] === "completed-by" && partyUuid !== void 0 ? partyIdentity(poam, partyUuid) : void 0;
18800
+ if (completer) ms.completedBy = completer;
18801
+ }
18802
+ milestones.push(ms);
18803
+ }
18804
+ }
18805
+ return milestones;
18806
+ }
18807
+ /** Begins the facet system of every CVSS version. */
18808
+ const CVSS_SYSTEM_PREFIX = "http://www.first.org/cvss/v";
18809
+ /** Reads the characterization whose facets all use a CVSS system. */
18810
+ function riskCvss(risk) {
18811
+ for (const ch of risk.characterizations ?? []) {
18812
+ if (ch.facets.length === 0 || !ch.facets.every((f) => f.system.startsWith(CVSS_SYSTEM_PREFIX))) continue;
18813
+ const values = new Map(ch.facets.map((f) => [f.name, f.remarks || f.value]));
18814
+ const str = (name, field) => values.get(name) ?? (hasFieldMarker(ch.props, "empty-field", field, "") ? "" : void 0);
18815
+ const score = (name) => {
18816
+ const value = values.get(name);
18817
+ return value !== void 0 && value.trim() !== "" && !isNaN(Number(value)) ? Number(value) : void 0;
18818
+ };
18819
+ const severity = (name) => values.get(name);
18820
+ return {
18821
+ version: values.get("cvss_version"),
18822
+ baseScore: score("base_score"),
18823
+ baseSeverity: severity("base_severity"),
18824
+ baseVector: str("base_vector", "baseVector"),
18825
+ threatScore: score("threat_score"),
18826
+ threatVector: str("threat_vector", "threatVector"),
18827
+ environmentalScore: score("environmental_score"),
18828
+ environmentalVector: str("environmental_vector", "environmentalVector"),
18829
+ computedScore: score("computed_score"),
18830
+ computedSeverity: severity("computed_severity"),
18831
+ supplementalVector: str("supplemental_vector", "supplementalVector"),
18832
+ source: str("source", "source")
18833
+ };
18834
+ }
18835
+ }
18836
+ /**
18837
+ * Reads the evidence observations an HDF-produced item lists. An observation with
18838
+ * no payload is not an HDF evidence item (ADR-0014 §4.6 gives Evidence.data no
18839
+ * other home) and Evidence.data is required and non-empty, so a foreign
18840
+ * observation listed alongside the exported ones is skipped.
18841
+ */
18842
+ function itemEvidence(item, poam) {
18843
+ const evidence = [];
18844
+ for (const ro of item["related-observations"] ?? []) {
18845
+ const obs = (poam.observations ?? []).find((o) => o.uuid === ro["observation-uuid"]);
18846
+ if (!obs) continue;
18847
+ const ev = observationEvidence(obs, poam);
18848
+ if (ev.data === "") {
18849
+ emitConverterWarning(`Skipping evidence observation "${obs.uuid}": no relevant-evidence href and no evidence resource`);
18850
+ continue;
18851
+ }
18852
+ evidence.push(ev);
18853
+ }
18854
+ return evidence;
18855
+ }
18856
+ /** Decodes base64 to the UTF-8 text it encodes, or undefined when it is not base64. */
18857
+ function decodeBase64Utf8(value) {
18858
+ try {
18859
+ const binary = atob(value);
18860
+ return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
18861
+ } catch {
18862
+ return;
18863
+ }
18864
+ }
18865
+ /** Reads one evidence item from its observation (ADR-0014 §4.6 Evidence table). */
18866
+ function observationEvidence(obs, poam) {
18867
+ const props = obs.props;
18868
+ const ev = {
18869
+ type: obs.types?.[0] ?? "",
18870
+ data: ""
18871
+ };
18872
+ if (!hasFieldMarker(props, "absent-field", "description", "")) ev.description = obs.description;
18873
+ const mimeType = vocabularyString(props, "mime-type", "mimeType", "");
18874
+ if (mimeType !== void 0) ev.mimeType = mimeType;
18875
+ const encoding = vocabularyString(props, "evidence-encoding", "encoding", "");
18876
+ if (encoding !== void 0) ev.encoding = encoding;
18877
+ const size = findVocabularyProp(props, "evidence-size");
18878
+ if (size && size.value.trim() !== "" && !isNaN(Number(size.value))) ev.size = Number(size.value);
18879
+ if (!hasFieldMarker(props, "absent-field", "capturedAt", "")) {
18880
+ const capturedAt = parseTimestamp(String(obs.collected));
18881
+ if (capturedAt) ev.capturedAt = capturedAt;
18882
+ }
18883
+ for (const origin of obs.origins ?? []) for (const actor of origin.actors) {
18884
+ const capturer = actor.type === "party" && ev.capturedBy === void 0 ? partyIdentity(poam, actor["actor-uuid"]) : void 0;
18885
+ if (capturer) ev.capturedBy = capturer;
18886
+ }
18887
+ const href = obs["relevant-evidence"]?.[0]?.href;
18888
+ if (href) {
18889
+ ev.data = href;
18890
+ return ev;
18891
+ }
18892
+ for (const link of obs.links ?? []) {
18893
+ const res = backMatterResource(poam, link);
18894
+ if (link.rel !== "evidence" || !res?.base64) continue;
18895
+ ev.data = encoding === "base64" ? res.base64.value : decodeBase64Utf8(res.base64.value) ?? res.base64.value;
18896
+ break;
18897
+ }
18898
+ return ev;
18899
+ }
18900
+ /** Resolves a link to the back-matter resource its "#<uuid>" href names. */
18901
+ function backMatterResource(poam, link) {
18902
+ if (!link.href.startsWith("#")) return void 0;
18903
+ return (poam["back-matter"]?.resources ?? []).find((r) => r.uuid === link.href.slice(1));
18904
+ }
18905
+ /** Reads the external references an HDF-produced risk links to (ADR-0014 §4.6 External reference table). */
18906
+ function riskReferences(risk, poam) {
18907
+ const refs = [];
18908
+ for (const link of risk.links ?? []) {
18909
+ const res = backMatterResource(poam, link);
18910
+ if (link.rel !== "reference" || !res) continue;
18911
+ const props = res.props;
18912
+ const ref = { sourceName: findVocabularyProp(props, "source-name")?.value ?? "" };
18913
+ const externalId = vocabularyString(props, "external-id", "externalId", "");
18914
+ if (externalId !== void 0) ref.externalId = externalId;
18915
+ if (res.rlinks && res.rlinks.length > 0) ref.href = res.rlinks[0].href;
18916
+ else if (hasFieldMarker(props, "empty-field", "href", "")) ref.href = "";
18917
+ if (res.description) ref.description = res.description;
18918
+ else if (hasFieldMarker(props, "empty-field", "description", "")) ref.description = "";
18919
+ const rel = vocabularyString(props, "reference-rel", "rel", "");
18920
+ if (rel !== void 0) ref.rel = rel;
18921
+ const mediaType = vocabularyString(props, "reference-media-type", "mediaType", "");
18922
+ if (mediaType !== void 0) ref.mediaType = mediaType;
18923
+ const algorithm = vocabularyString(props, "checksum-algorithm", "checksum.algorithm", "");
18924
+ if (algorithm !== void 0) ref.checksum = {
18925
+ algorithm,
18926
+ value: vocabularyString(props, "checksum-value", "checksum.value", "") ?? ""
18927
+ };
18928
+ const addedBy = findVocabularyProp(props, "added-by");
18929
+ const adder = addedBy ? partyIdentity(poam, addedBy.value) : void 0;
18930
+ if (adder) ref.addedBy = adder;
18931
+ const addedAt = findVocabularyProp(props, "added-at");
18932
+ const addedAtDate = addedAt && parseTimestamp(addedAt.value);
18933
+ if (addedAtDate) ref.addedAt = addedAtDate;
18934
+ const kind = vocabularyString(props, "reference-kind", "kind", "");
18935
+ if (kind !== void 0) ref.kind = kind;
18936
+ const documentJson = res.base64 ? decodeBase64Utf8(res.base64.value) : void 0;
18937
+ if (documentJson !== void 0) try {
18938
+ const document = JSON.parse(documentJson);
18939
+ if (document !== null && typeof document === "object" && !Array.isArray(document)) ref.document = document;
18940
+ } catch {}
18941
+ refs.push(ref);
18942
+ }
18943
+ return refs;
18944
+ }
18945
+ /**
18946
+ * The requirement id of an item that is not HDF-produced. A pre-ADR HDF export's id
18947
+ * comes only from its legacy impacted-control-id, never from a title (ADR-0014 §4.3),
18948
+ * so it is undefined for one that has none, which the conversion skips. Mirrors
18949
+ * foreignRequirementID in Go.
18950
+ */
18951
+ function foreignRequirementId(item, riskMap) {
18952
+ const preADR = overrideTypeRisk(item, riskMap, true);
18953
+ if (!preADR) return extractRequirementIdFromPOAMItem(item, riskMap);
18954
+ const control = findVocabularyProp(preADR.props, "impacted-control-id");
18955
+ return control ? controlIdToNistTag(control.value) : void 0;
18956
+ }
17348
18957
  function extractRequirementIdFromPOAMItem(item, riskMap) {
17349
18958
  for (const rr of item["related-risks"] ?? []) {
17350
18959
  const riskUuid = rr["risk-uuid"];
@@ -17381,17 +18990,16 @@ function poamItemStatus(item, riskMap) {
17381
18990
  return ResultStatus.Failed;
17382
18991
  }
17383
18992
  function poamItemAppliedBy(poam) {
18993
+ const preparedBy = firstResponsibleParty(poam.metadata, "prepared-by");
18994
+ if (preparedBy !== "") return {
18995
+ type: IdentityType.Simple,
18996
+ identifier: preparedBy
18997
+ };
17384
18998
  const rps = poam.metadata["responsible-parties"];
17385
- if (rps) {
17386
- for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
17387
- type: IdentityType.Simple,
17388
- identifier: rp["party-uuids"][0]
17389
- };
17390
- if (rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
17391
- type: IdentityType.Simple,
17392
- identifier: rps[0]["party-uuids"][0]
17393
- };
17394
- }
18999
+ if (rps && rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
19000
+ type: IdentityType.Simple,
19001
+ identifier: rps[0]["party-uuids"][0]
19002
+ };
17395
19003
  return {
17396
19004
  type: IdentityType.System,
17397
19005
  identifier: "oscal-poam-converter"
@@ -17436,15 +19044,6 @@ function extractMilestones(item, riskMap) {
17436
19044
  }
17437
19045
  return milestones;
17438
19046
  }
17439
- function extractAppliedBy(meta) {
17440
- const rps = meta["responsible-parties"];
17441
- if (rps) {
17442
- for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
17443
- type: IdentityType.Simple,
17444
- identifier: rp["party-uuids"][0]
17445
- };
17446
- }
17447
- }
17448
19047
  //#endregion
17449
19048
  //#region converters/oscal-to-hdf/typescript/converter-sap.ts
17450
19049
  /**
@@ -17598,11 +19197,18 @@ async function convertOscalSarToHdf(input) {
17598
19197
  const scanTime = /* @__PURE__ */ new Date();
17599
19198
  const baselines = [];
17600
19199
  for (const result of sar.results) {
19200
+ const title = result.title || result.uuid;
17601
19201
  if (!result.findings || result.findings.length === 0) {
17602
- console.warn(`WARNING: Skipping assessment result "${result.title || result.uuid}": no findings (empty result set)`);
19202
+ emitConverterWarning(`Skipping assessment result "${title}": no findings (empty result set)`);
19203
+ continue;
19204
+ }
19205
+ const { groups, skipped } = groupFindingsByRequirement(result.findings);
19206
+ for (const f of skipped) emitConverterWarning(`Skipping finding "${f.uuid}" titled "${f.title}": empty target-id`);
19207
+ if (groups.size === 0) {
19208
+ emitConverterWarning(`Skipping assessment result "${title}": no finding has a target-id`);
17603
19209
  continue;
17604
19210
  }
17605
- const baseline = await resultToEvaluatedBaseline(result, sar, input, scanTime);
19211
+ const baseline = await resultToEvaluatedBaseline(result, groups, sar, input, scanTime);
17606
19212
  baselines.push(baseline);
17607
19213
  }
17608
19214
  const planRef = sar["import-ap"]?.href || void 0;
@@ -17625,27 +19231,51 @@ async function convertOscalSarToHdf(input) {
17625
19231
  planRef
17626
19232
  });
17627
19233
  }
17628
- async function resultToEvaluatedBaseline(result, sar, rawInput, scanTime) {
19234
+ /**
19235
+ * Groups findings by requirement id in first-seen order (a Map keeps insertion
19236
+ * order), within the finding cap, and returns the findings skipped for an empty
19237
+ * target-id. Mirrors Go's groupFindingsByRequirement.
19238
+ */
19239
+ function groupFindingsByRequirement(findings) {
19240
+ const groups = /* @__PURE__ */ new Map();
19241
+ const skipped = [];
19242
+ for (const f of limitArrayWithWarning(findings, "finding")) {
19243
+ const id = sarRequirementId(f);
19244
+ if (id === void 0) {
19245
+ skipped.push(f);
19246
+ continue;
19247
+ }
19248
+ const existing = groups.get(id);
19249
+ if (existing) existing.push(f);
19250
+ else groups.set(id, [f]);
19251
+ }
19252
+ return {
19253
+ groups,
19254
+ skipped
19255
+ };
19256
+ }
19257
+ /**
19258
+ * The HDF requirement id a finding belongs to (ADR-0014 §4.5): the HDF-namespaced
19259
+ * hdf-requirement-id when present; else the NIST control a roster-confirmed
19260
+ * target names; else the target-id verbatim. Undefined for a finding whose
19261
+ * target-id is empty. Mirrors Go's sarRequirementID.
19262
+ */
19263
+ function sarRequirementId(f) {
19264
+ const targetId = f.target["target-id"];
19265
+ if (!targetId) return void 0;
19266
+ const prop = findVocabularyProp(f.props, "hdf-requirement-id");
19267
+ if (prop && prop.value !== "") return prop.value;
19268
+ const controlId = confirmedControlId(targetId);
19269
+ return controlId === void 0 ? targetId : controlIdToNistTag(controlId);
19270
+ }
19271
+ async function resultToEvaluatedBaseline(result, groups, sar, rawInput, scanTime) {
17629
19272
  const checksum = await inputChecksum(rawInput);
17630
19273
  const obsMap = buildObservationMap(result.observations ?? []);
17631
19274
  const riskMap = buildRiskMap(result.risks ?? []);
17632
- const controlOrder = [];
17633
- const controlMap = /* @__PURE__ */ new Map();
17634
- for (const f of result.findings ?? []) {
17635
- const controlId = extractControlIdFromFinding(f);
17636
- const existing = controlMap.get(controlId);
17637
- if (existing) existing.push(f);
17638
- else {
17639
- controlOrder.push(controlId);
17640
- controlMap.set(controlId, [f]);
17641
- }
17642
- }
17643
19275
  const requirements = [];
17644
- for (const controlId of controlOrder) {
17645
- const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result, scanTime);
17646
- requirements.push(req);
17647
- }
17648
- const baseline = createMinimalBaseline(sarBaselineName(result, sar), requirements, {
19276
+ for (const [id, findings] of groups) requirements.push(findingsToEvaluatedRequirement(id, findings, obsMap, riskMap, result, scanTime));
19277
+ const name = sarBaselineName(result, sar);
19278
+ const baseline = createMinimalBaseline(name, requirements, {
17649
19279
  resultsChecksum: checksum,
17650
19280
  integrity: await inputIntegrity(rawInput),
17651
19281
  status: "loaded",
@@ -17654,17 +19284,17 @@ async function resultToEvaluatedBaseline(result, sar, rawInput, scanTime) {
17654
19284
  if (result.description) baseline.description = result.description;
17655
19285
  return baseline;
17656
19286
  }
17657
- function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result, scanTime) {
17658
- const nistTag = controlIdToNistTag(controlId);
17659
- const title = findings[0].title || nistTag;
19287
+ function findingsToEvaluatedRequirement(id, findings, obsMap, riskMap, result, scanTime) {
19288
+ const title = findings[0].title || id;
17660
19289
  const impact = sarFindingsImpact(findings, riskMap);
17661
19290
  const descriptions = sarBuildDescriptions(findings, obsMap, riskMap);
17662
19291
  const refs = sarBuildRefs(findings, obsMap);
17663
19292
  const results = [];
17664
19293
  for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result, scanTime));
17665
- const nistTags = [nistTag];
17666
- const req = createRequirement(nistTag, title, descriptions, impact, results, {
17667
- tags: buildNistCciTags(nistTags, nistToCci(nistTags)),
19294
+ const nistTags = sarConfirmedNistTags(findings);
19295
+ const tags = buildNistCciTags(nistTags, nistToCci(nistTags));
19296
+ const req = createRequirement(id, title, descriptions, impact, results, {
19297
+ tags,
17668
19298
  ...refs ? { refs } : {}
17669
19299
  });
17670
19300
  const controlType = deriveControlTypeFromTags(nistTags);
@@ -17686,13 +19316,14 @@ function findingToRequirementResult(f, obsMap, riskMap, result, scanTime) {
17686
19316
  startTime
17687
19317
  });
17688
19318
  }
17689
- function extractControlIdFromFinding(f) {
17690
- const targetId = f.target["target-id"];
17691
- if (!targetId) return "unknown";
17692
- let controlId = extractControlIdFromObjectiveId(targetId);
17693
- const idx = controlId.indexOf("_");
17694
- if (idx > 0) controlId = controlId.slice(0, idx);
17695
- return controlId;
19319
+ /** The distinct NIST controls, in NIST notation, the findings' targets confirm, in finding order. */
19320
+ function sarConfirmedNistTags(findings) {
19321
+ const controlIds = [];
19322
+ for (const f of findings) {
19323
+ const controlId = confirmedControlId(f.target["target-id"]);
19324
+ if (controlId !== void 0) controlIds.push(controlId);
19325
+ }
19326
+ return controlIdsToNistTags(controlIds);
17696
19327
  }
17697
19328
  function mapFindingStatus(f) {
17698
19329
  const status = oscalStatusToHdf(f.target.status.state);
@@ -17751,19 +19382,19 @@ function sarBuildDescriptions(findings, obsMap, riskMap) {
17751
19382
  label: "default",
17752
19383
  data: findingDescs.join("\n") || ""
17753
19384
  });
17754
- const obsDescs = [];
17755
- const seen = /* @__PURE__ */ new Set();
17756
- for (const f of findings) for (const ref of f["related-observations"] ?? []) {
17757
- const obsUuid = ref["observation-uuid"];
17758
- if (!obsUuid || seen.has(obsUuid)) continue;
17759
- seen.add(obsUuid);
17760
- const obs = obsMap.get(obsUuid);
17761
- if (obs?.description) obsDescs.push(obs.description);
17762
- }
17763
- if (obsDescs.length > 0) descriptions.push({
19385
+ const rationales = findings.map((f) => f.target.description ?? "").filter((d) => d !== "");
19386
+ if (rationales.length > 0) descriptions.push({
17764
19387
  label: "rationale",
17765
- data: obsDescs.join("\n")
19388
+ data: rationales.join("\n")
17766
19389
  });
19390
+ const labelled = collectLabelledProse(findings, obsMap, riskMap);
19391
+ for (const label of ["check", "fix"]) {
19392
+ const texts = labelled.get(label);
19393
+ if (texts) descriptions.push({
19394
+ label,
19395
+ data: texts.join("\n")
19396
+ });
19397
+ }
17767
19398
  const statement = collectRiskStatements(findings, riskMap);
17768
19399
  if (statement) descriptions.push({
17769
19400
  label: "statement",
@@ -17781,6 +19412,46 @@ function sarBuildDescriptions(findings, obsMap, riskMap) {
17781
19412
  });
17782
19413
  return descriptions;
17783
19414
  }
19415
+ /** The HDF description label a relevant-evidence entry carries ("check" or "fix"), or '' for foreign evidence. */
19416
+ function evidenceDescriptionLabel(ev) {
19417
+ const label = descriptionLabel(ev.props);
19418
+ return label === "check" || label === "fix" ? label : "";
19419
+ }
19420
+ /** Whether a remediation is HDF's fix prose home. */
19421
+ function isLabelledFix(rem) {
19422
+ return descriptionLabel(rem.props) === "fix";
19423
+ }
19424
+ /**
19425
+ * Gathers the check and fix text HDF wrote into labelled relevant-evidence
19426
+ * entries (full text from remarks) and labelled remediations, in finding order,
19427
+ * reading each observation and risk once.
19428
+ */
19429
+ function collectLabelledProse(findings, obsMap, riskMap) {
19430
+ const texts = /* @__PURE__ */ new Map();
19431
+ const add = (label, text) => {
19432
+ texts.set(label, [...texts.get(label) ?? [], text]);
19433
+ };
19434
+ const seenObs = /* @__PURE__ */ new Set();
19435
+ const seenRisk = /* @__PURE__ */ new Set();
19436
+ for (const f of findings) {
19437
+ for (const ref of f["related-observations"] ?? []) {
19438
+ const obs = obsMap.get(ref["observation-uuid"]);
19439
+ if (!obs || seenObs.has(ref["observation-uuid"])) continue;
19440
+ seenObs.add(ref["observation-uuid"]);
19441
+ for (const ev of obs["relevant-evidence"] ?? []) {
19442
+ const label = evidenceDescriptionLabel(ev);
19443
+ if (label !== "") add(label, ev.remarks || ev.description);
19444
+ }
19445
+ }
19446
+ for (const ref of f["related-risks"] ?? []) {
19447
+ const risk = riskMap.get(ref["risk-uuid"]);
19448
+ if (!risk || seenRisk.has(ref["risk-uuid"])) continue;
19449
+ seenRisk.add(ref["risk-uuid"]);
19450
+ for (const rem of risk.remediations ?? []) if (isLabelledFix(rem)) add("fix", rem.description);
19451
+ }
19452
+ }
19453
+ return texts;
19454
+ }
17784
19455
  function collectRiskStatements(findings, riskMap) {
17785
19456
  const statements = [];
17786
19457
  const seen = /* @__PURE__ */ new Set();
@@ -17802,6 +19473,7 @@ function collectRemediations(findings, riskMap) {
17802
19473
  seen.add(riskUuid);
17803
19474
  const risk = riskMap.get(riskUuid);
17804
19475
  for (const rem of risk?.remediations ?? []) {
19476
+ if (isLabelledFix(rem)) continue;
17805
19477
  const text = remediationText(rem);
17806
19478
  if (text) remediations.push(text);
17807
19479
  }
@@ -17823,7 +19495,7 @@ function collectEvidenceDescriptions(findings, obsMap) {
17823
19495
  seenObs.add(obsUuid);
17824
19496
  const obs = obsMap.get(obsUuid);
17825
19497
  for (const ev of obs?.["relevant-evidence"] ?? []) {
17826
- if (!ev.description || seenText.has(ev.description)) continue;
19498
+ if (!ev.description || seenText.has(ev.description) || evidenceDescriptionLabel(ev) !== "") continue;
17827
19499
  seenText.add(ev.description);
17828
19500
  descs.push(ev.description);
17829
19501
  }
@@ -18024,6 +19696,7 @@ function sspComponentToHDFComponent(sc, componentControls) {
18024
19696
  name: sc.title,
18025
19697
  type: mapOSCALComponentType(sc.type)
18026
19698
  };
19699
+ if (sc.uuid) comp.componentId = sc.uuid;
18027
19700
  if (sc.description) comp.description = sc.description;
18028
19701
  const controls = componentControls.get(sc.uuid);
18029
19702
  if (controls && controls.size > 0) {
@@ -18252,7 +19925,7 @@ function stixObjectCVEs(obj) {
18252
19925
  }
18253
19926
  //#endregion
18254
19927
  //#region shared/typescript/enrichStix.ts
18255
- const DEFAULT_REVIEW_HORIZON_MS = 2160 * 60 * 60 * 1e3;
19928
+ const DEFAULT_REVIEW_HORIZON_MS = 7776e6;
18256
19929
  /** Format a Date as canonical trimmed-UTC RFC3339 (seconds precision, matching Go's time.RFC3339). */
18257
19930
  function toRfc3339(d) {
18258
19931
  return d.toISOString().replace(/\.\d{3}Z$/, "Z");