@mitre/hdf-converters 3.7.0 → 3.7.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
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
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
- 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";
4
+ import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, CloudProvider, 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
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";
@@ -5292,16 +5292,25 @@ function calculateImpact(item, isCompliance) {
5292
5292
  }
5293
5293
  return severityToImpactWithAliases(item["severity"], NESSUS_ALIASES, 0);
5294
5294
  }
5295
+ /**
5296
+ * Substitutes the shared remediation fallback when a lookup resolved nothing, so
5297
+ * the CCI path and the plugin-family path behave alike. A finding the mapping
5298
+ * tables do not cover is still a flaw to remediate, and an empty nist tag would
5299
+ * drop it from every NIST-based view.
5300
+ */
5301
+ function orDefaultNist(controls) {
5302
+ return controls.length > 0 ? controls : [...DEFAULT_REMEDIATION_NIST_TAGS$1];
5303
+ }
5295
5304
  function buildTags$3(item, isCompliance) {
5296
5305
  const tags = { rid: isCompliance ? parseComplianceRef(item["compliance-reference"], "Rule-ID").join(",") : item["pluginID"] };
5297
5306
  if (isCompliance && item["compliance-reference"]) {
5298
5307
  const cciTags = parseComplianceRef(item["compliance-reference"], "CCI");
5299
5308
  tags.cci = cciTags;
5300
5309
  const mappedControls = cciTags.flatMap((cci) => getCCINistMappings(cci) ?? []);
5301
- tags.nist = [...new Set(mappedControls)].sort();
5310
+ tags.nist = orDefaultNist([...new Set(mappedControls)].sort());
5302
5311
  } else {
5303
5312
  const nistControls = getNessusNistControl(item["pluginFamily"], item["pluginID"]);
5304
- tags.nist = nistControls ? nistControls.split("|") : [];
5313
+ tags.nist = orDefaultNist(nistControls ? nistControls.split("|") : []);
5305
5314
  }
5306
5315
  if (isCompliance && item["compliance-reference"]) {
5307
5316
  const stigId = parseComplianceRef(item["compliance-reference"], "STIG-ID").join(",");
@@ -14173,6 +14182,15 @@ var oscal_vocabulary_default = {
14173
14182
  "hdfField": "baselines[].requirements[].id, overrides[].requirementId",
14174
14183
  "legacy": true
14175
14184
  },
14185
+ {
14186
+ "name": "requirement-title",
14187
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14188
+ "objects": ["assessment-results.results[].findings[]"],
14189
+ "meaning": "The exact HDF requirement title the finding reports. Requirement_Core.title is prose that may carry line breaks (§2); the finding title is display text OSCAL types single-line, so the exact title is preserved here while the display title is normalized (§4.3).",
14190
+ "valueFormat": "String (§1.7)",
14191
+ "hdfField": "baselines[].requirements[].title",
14192
+ "legacy": false
14193
+ },
14176
14194
  {
14177
14195
  "name": "baseline-version",
14178
14196
  "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
@@ -14182,6 +14200,24 @@ var oscal_vocabulary_default = {
14182
14200
  "hdfField": "baselines[].version",
14183
14201
  "legacy": true
14184
14202
  },
14203
+ {
14204
+ "name": "baseline-name",
14205
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14206
+ "objects": ["assessment-results.results[]"],
14207
+ "meaning": "The exact HDF baseline name the result reports, so an HDF-produced SAR round-trips the name that hdf-diff and baseline references key on. The result title is display text and is not injective, so two baselines sharing a title would otherwise collide (§4.3, §4.5).",
14208
+ "valueFormat": "String (§1.7)",
14209
+ "hdfField": "baselines[].name",
14210
+ "legacy": false
14211
+ },
14212
+ {
14213
+ "name": "baseline-title",
14214
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14215
+ "objects": ["assessment-results.results[]"],
14216
+ "meaning": "The exact HDF baseline title the result reports, distinct from the baseline name (baseline-name). The result title is display text OSCAL types single-line, so a title carrying a line terminator is preserved here while the display title is normalized (§4.3).",
14217
+ "valueFormat": "String (§1.7)",
14218
+ "hdfField": "baselines[].title",
14219
+ "legacy": false
14220
+ },
14185
14221
  {
14186
14222
  "name": "nist",
14187
14223
  "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
@@ -14353,6 +14389,393 @@ var oscal_vocabulary_default = {
14353
14389
  "hdfField": null,
14354
14390
  "legacy": false
14355
14391
  },
14392
+ {
14393
+ "name": "component-name",
14394
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14395
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14396
+ "meaning": "The exact HDF component name the assessment subject reconstitutes. The subject title is display text OSCAL types single-line, so a name carrying a line terminator is preserved here while the display title is normalized (§4.3).",
14397
+ "valueFormat": "String (§1.7)",
14398
+ "hdfField": "components[].name",
14399
+ "legacy": false
14400
+ },
14401
+ {
14402
+ "name": "component-description",
14403
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14404
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14405
+ "meaning": "The description of the component the assessment subject reconstitutes.",
14406
+ "valueFormat": "String (§1.7)",
14407
+ "hdfField": "components[].description",
14408
+ "legacy": false
14409
+ },
14410
+ {
14411
+ "name": "component-hostname",
14412
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14413
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14414
+ "meaning": "A host component's short OS-reported hostname.",
14415
+ "valueFormat": "String (§1.7)",
14416
+ "hdfField": "components[].hostname",
14417
+ "legacy": false
14418
+ },
14419
+ {
14420
+ "name": "component-fqdn",
14421
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14422
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14423
+ "meaning": "A host component's fully qualified domain name.",
14424
+ "valueFormat": "String (§1.7)",
14425
+ "hdfField": "components[].fqdn",
14426
+ "legacy": false
14427
+ },
14428
+ {
14429
+ "name": "component-domain",
14430
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14431
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14432
+ "meaning": "A host component's directory domain.",
14433
+ "valueFormat": "String (§1.7)",
14434
+ "hdfField": "components[].domain",
14435
+ "legacy": false
14436
+ },
14437
+ {
14438
+ "name": "component-ip-address",
14439
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14440
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14441
+ "meaning": "A host component's IP address.",
14442
+ "valueFormat": "String (§1.7)",
14443
+ "hdfField": "components[].ipAddress",
14444
+ "legacy": false
14445
+ },
14446
+ {
14447
+ "name": "component-mac-address",
14448
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14449
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14450
+ "meaning": "A host component's MAC address.",
14451
+ "valueFormat": "String (§1.7)",
14452
+ "hdfField": "components[].macAddress",
14453
+ "legacy": false
14454
+ },
14455
+ {
14456
+ "name": "component-os-name",
14457
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14458
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14459
+ "meaning": "A host component's operating system name.",
14460
+ "valueFormat": "String (§1.7)",
14461
+ "hdfField": "components[].osName",
14462
+ "legacy": false
14463
+ },
14464
+ {
14465
+ "name": "component-os-version",
14466
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14467
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14468
+ "meaning": "A host component's operating system version.",
14469
+ "valueFormat": "String (§1.7)",
14470
+ "hdfField": "components[].osVersion",
14471
+ "legacy": false
14472
+ },
14473
+ {
14474
+ "name": "component-image-id",
14475
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14476
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14477
+ "meaning": "A container image component's image id.",
14478
+ "valueFormat": "String (§1.7)",
14479
+ "hdfField": "components[].imageId",
14480
+ "legacy": false
14481
+ },
14482
+ {
14483
+ "name": "component-registry",
14484
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14485
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14486
+ "meaning": "A container image component's registry.",
14487
+ "valueFormat": "String (§1.7)",
14488
+ "hdfField": "components[].registry",
14489
+ "legacy": false
14490
+ },
14491
+ {
14492
+ "name": "component-repository",
14493
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14494
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14495
+ "meaning": "A container image component's repository.",
14496
+ "valueFormat": "String (§1.7)",
14497
+ "hdfField": "components[].repository",
14498
+ "legacy": false
14499
+ },
14500
+ {
14501
+ "name": "component-tag",
14502
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14503
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14504
+ "meaning": "A container image component's tag.",
14505
+ "valueFormat": "String (§1.7)",
14506
+ "hdfField": "components[].tag",
14507
+ "legacy": false
14508
+ },
14509
+ {
14510
+ "name": "component-container-id",
14511
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14512
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14513
+ "meaning": "A container instance component's running container id.",
14514
+ "valueFormat": "String (§1.7)",
14515
+ "hdfField": "components[].containerId",
14516
+ "legacy": false
14517
+ },
14518
+ {
14519
+ "name": "component-image",
14520
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14521
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14522
+ "meaning": "A container instance component's source image.",
14523
+ "valueFormat": "String (§1.7)",
14524
+ "hdfField": "components[].image",
14525
+ "legacy": false
14526
+ },
14527
+ {
14528
+ "name": "component-runtime",
14529
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14530
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14531
+ "meaning": "A container instance component's runtime.",
14532
+ "valueFormat": "String (§1.7)",
14533
+ "hdfField": "components[].runtime",
14534
+ "legacy": false
14535
+ },
14536
+ {
14537
+ "name": "component-platform-type",
14538
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14539
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14540
+ "meaning": "A container platform component's platform type.",
14541
+ "valueFormat": "String (§1.7)",
14542
+ "hdfField": "components[].platformType",
14543
+ "legacy": false
14544
+ },
14545
+ {
14546
+ "name": "component-cluster-name",
14547
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14548
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14549
+ "meaning": "A container platform component's cluster name.",
14550
+ "valueFormat": "String (§1.7)",
14551
+ "hdfField": "components[].clusterName",
14552
+ "legacy": false
14553
+ },
14554
+ {
14555
+ "name": "component-namespace",
14556
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14557
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14558
+ "meaning": "A container platform component's namespace.",
14559
+ "valueFormat": "String (§1.7)",
14560
+ "hdfField": "components[].namespace",
14561
+ "legacy": false
14562
+ },
14563
+ {
14564
+ "name": "component-version",
14565
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14566
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14567
+ "meaning": "A component's version (container platform, application, artifact, database, aiModel or dataset).",
14568
+ "valueFormat": "String (§1.7)",
14569
+ "hdfField": "components[].version",
14570
+ "legacy": false
14571
+ },
14572
+ {
14573
+ "name": "component-provider",
14574
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14575
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14576
+ "meaning": "A cloud account or cloud resource component's cloud provider.",
14577
+ "valueFormat": "An HDF cloud provider value (aws, azure, gcp, oci, other)",
14578
+ "hdfField": "components[].provider",
14579
+ "legacy": false
14580
+ },
14581
+ {
14582
+ "name": "component-account-id",
14583
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14584
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14585
+ "meaning": "A cloud account component's account identifier.",
14586
+ "valueFormat": "String (§1.7)",
14587
+ "hdfField": "components[].accountId",
14588
+ "legacy": false
14589
+ },
14590
+ {
14591
+ "name": "component-region",
14592
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14593
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14594
+ "meaning": "A cloud account or cloud resource component's region.",
14595
+ "valueFormat": "String (§1.7)",
14596
+ "hdfField": "components[].region",
14597
+ "legacy": false
14598
+ },
14599
+ {
14600
+ "name": "component-resource-type",
14601
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14602
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14603
+ "meaning": "A cloud resource component's resource type.",
14604
+ "valueFormat": "String (§1.7)",
14605
+ "hdfField": "components[].resourceType",
14606
+ "legacy": false
14607
+ },
14608
+ {
14609
+ "name": "component-resource-id",
14610
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14611
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14612
+ "meaning": "A cloud resource component's provider-specific resource id.",
14613
+ "valueFormat": "String (§1.7)",
14614
+ "hdfField": "components[].resourceId",
14615
+ "legacy": false
14616
+ },
14617
+ {
14618
+ "name": "component-arn",
14619
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14620
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14621
+ "meaning": "A cloud resource component's Amazon Resource Name.",
14622
+ "valueFormat": "String (§1.7)",
14623
+ "hdfField": "components[].arn",
14624
+ "legacy": false
14625
+ },
14626
+ {
14627
+ "name": "component-url",
14628
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14629
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14630
+ "meaning": "A repository or application component's URL.",
14631
+ "valueFormat": "String (§1.7), a URL",
14632
+ "hdfField": "components[].url",
14633
+ "legacy": false
14634
+ },
14635
+ {
14636
+ "name": "component-branch",
14637
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14638
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14639
+ "meaning": "A repository component's scanned branch.",
14640
+ "valueFormat": "String (§1.7)",
14641
+ "hdfField": "components[].branch",
14642
+ "legacy": false
14643
+ },
14644
+ {
14645
+ "name": "component-commit",
14646
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14647
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14648
+ "meaning": "A repository component's scanned commit SHA.",
14649
+ "valueFormat": "String (§1.7)",
14650
+ "hdfField": "components[].commit",
14651
+ "legacy": false
14652
+ },
14653
+ {
14654
+ "name": "component-environment",
14655
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14656
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14657
+ "meaning": "An application component's environment.",
14658
+ "valueFormat": "String (§1.7)",
14659
+ "hdfField": "components[].environment",
14660
+ "legacy": false
14661
+ },
14662
+ {
14663
+ "name": "component-package-manager",
14664
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14665
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14666
+ "meaning": "An artifact component's package manager.",
14667
+ "valueFormat": "String (§1.7)",
14668
+ "hdfField": "components[].packageManager",
14669
+ "legacy": false
14670
+ },
14671
+ {
14672
+ "name": "component-package-name",
14673
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14674
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14675
+ "meaning": "An artifact component's package name.",
14676
+ "valueFormat": "String (§1.7)",
14677
+ "hdfField": "components[].packageName",
14678
+ "legacy": false
14679
+ },
14680
+ {
14681
+ "name": "component-cidr",
14682
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14683
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14684
+ "meaning": "A network component's CIDR block.",
14685
+ "valueFormat": "String (§1.7)",
14686
+ "hdfField": "components[].cidr",
14687
+ "legacy": false
14688
+ },
14689
+ {
14690
+ "name": "component-gateway",
14691
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14692
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14693
+ "meaning": "A network component's gateway address.",
14694
+ "valueFormat": "String (§1.7)",
14695
+ "hdfField": "components[].gateway",
14696
+ "legacy": false
14697
+ },
14698
+ {
14699
+ "name": "component-engine",
14700
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14701
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14702
+ "meaning": "A database component's engine.",
14703
+ "valueFormat": "String (§1.7)",
14704
+ "hdfField": "components[].engine",
14705
+ "legacy": false
14706
+ },
14707
+ {
14708
+ "name": "component-host",
14709
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14710
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14711
+ "meaning": "A database component's host.",
14712
+ "valueFormat": "String (§1.7)",
14713
+ "hdfField": "components[].host",
14714
+ "legacy": false
14715
+ },
14716
+ {
14717
+ "name": "component-port",
14718
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14719
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14720
+ "meaning": "A database component's port.",
14721
+ "valueFormat": "Decimal number, a TCP port",
14722
+ "hdfField": "components[].port",
14723
+ "legacy": false
14724
+ },
14725
+ {
14726
+ "name": "component-model-id",
14727
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14728
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14729
+ "meaning": "An aiModel component's provider/registry model id.",
14730
+ "valueFormat": "String (§1.7)",
14731
+ "hdfField": "components[].modelId",
14732
+ "legacy": false
14733
+ },
14734
+ {
14735
+ "name": "component-dataset-id",
14736
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14737
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14738
+ "meaning": "A dataset component's provider/registry dataset id.",
14739
+ "valueFormat": "String (§1.7)",
14740
+ "hdfField": "components[].datasetId",
14741
+ "legacy": false
14742
+ },
14743
+ {
14744
+ "name": "component-label-key",
14745
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14746
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14747
+ "meaning": "The key of a component label, grouped component-label-<n>, n the label's 1-based position in sorted key order.",
14748
+ "valueFormat": "String (§1.7)",
14749
+ "hdfField": "components[].labels (key)",
14750
+ "legacy": false
14751
+ },
14752
+ {
14753
+ "name": "component-label-value",
14754
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14755
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14756
+ "meaning": "The value of the component label its group names.",
14757
+ "valueFormat": "String (§1.7)",
14758
+ "hdfField": "components[].labels (value)",
14759
+ "legacy": false
14760
+ },
14761
+ {
14762
+ "name": "component-external-id-key",
14763
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14764
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14765
+ "meaning": "The scheme of a component external id, grouped component-external-id-<n>, n its 1-based position in sorted key order.",
14766
+ "valueFormat": "String (§1.7)",
14767
+ "hdfField": "components[].externalIds (key)",
14768
+ "legacy": false
14769
+ },
14770
+ {
14771
+ "name": "component-external-id-value",
14772
+ "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
14773
+ "objects": ["assessment-results.results[].observations[].subjects[]", "assessment-results.results[].local-definitions.components[]"],
14774
+ "meaning": "The value of the component external id its group names.",
14775
+ "valueFormat": "String (§1.7)",
14776
+ "hdfField": "components[].externalIds (value)",
14777
+ "legacy": false
14778
+ },
14356
14779
  {
14357
14780
  "name": "amendment-id",
14358
14781
  "ns": "https://mitre.github.io/hdf-libs/ns/oscal",
@@ -14776,6 +15199,10 @@ function validateVocabularyTable(raw) {
14776
15199
  }
14777
15200
  const table = validateVocabularyTable(oscal_vocabulary_default);
14778
15201
  const byName = table.byName;
15202
+ /** NIST's default namespace, which a prop with no ns belongs to. */
15203
+ function vocabularyDefaultNamespace() {
15204
+ return table.defaultNamespace;
15205
+ }
14779
15206
  function mustVocabularyRow(name) {
14780
15207
  const row = byName.get(name);
14781
15208
  if (!row) throw new Error(`oscal: "${name}" is not a row of the OSCAL vocabulary`);
@@ -15209,6 +15636,346 @@ function oscalString(s) {
15209
15636
  return s.trim();
15210
15637
  }
15211
15638
  //#endregion
15639
+ //#region converters/oscal-to-hdf/typescript/component-props.ts
15640
+ /**
15641
+ * The one mapping from HDF component identity fields to the HDF-namespaced props
15642
+ * that carry them on an OSCAL assessment subject (ADR-0014 §4.5), driven by both
15643
+ * the SAR exporter and the SAR importer. Mirrors the Go peer component_props.go.
15644
+ */
15645
+ /** The hdf-results bounds on Component.port. */
15646
+ const MIN_PORT = 1;
15647
+ const MAX_PORT = 65535;
15648
+ /**
15649
+ * The whole grammar a port prop may use. Go and JavaScript disagree over what a
15650
+ * lenient numeric parse accepts ("80abc", "1e3"), so the grammar is spelled out
15651
+ * rather than left to either language's parser.
15652
+ */
15653
+ const DECIMAL_DIGITS = /^[0-9]+$/;
15654
+ function componentPort(v) {
15655
+ if (!DECIMAL_DIGITS.test(v)) return void 0;
15656
+ const n = Number(v);
15657
+ if (!Number.isSafeInteger(n) || n < MIN_PORT || n > MAX_PORT) return void 0;
15658
+ return n;
15659
+ }
15660
+ /**
15661
+ * The Cloud_Provider enum in sorted order, read from the generated schema types'
15662
+ * runtime enum rather than a hand-kept copy; the Go peer reports the same order so
15663
+ * the two warnings read alike.
15664
+ */
15665
+ function cloudProviderValues() {
15666
+ return Object.values(CloudProvider).map(String).sort();
15667
+ }
15668
+ function validCloudProvider(v) {
15669
+ return Object.values(CloudProvider).some((p) => String(p) === v);
15670
+ }
15671
+ function stringField(prop, field) {
15672
+ return {
15673
+ prop,
15674
+ field,
15675
+ get: (c) => c[field],
15676
+ set: (c, v) => {
15677
+ c[field] = v;
15678
+ return true;
15679
+ }
15680
+ };
15681
+ }
15682
+ function stringMapField(field, prefix) {
15683
+ return {
15684
+ field,
15685
+ prefix,
15686
+ keyProp: `${prefix}-key`,
15687
+ valueProp: `${prefix}-value`,
15688
+ get: (c) => c[field],
15689
+ set: (c, m) => {
15690
+ c[field] = m;
15691
+ }
15692
+ };
15693
+ }
15694
+ /**
15695
+ * The table order is the order the exporter emits props in, and the test pins it to
15696
+ * the components[] rows of the vocabulary table.
15697
+ */
15698
+ const COMPONENT_PROPS = [
15699
+ {
15700
+ prop: "component-name",
15701
+ field: "name",
15702
+ required: true,
15703
+ get: (c) => c.name,
15704
+ set: (c, v) => {
15705
+ c.name = v;
15706
+ return true;
15707
+ }
15708
+ },
15709
+ stringField("component-description", "description"),
15710
+ stringField("component-hostname", "hostname"),
15711
+ stringField("component-fqdn", "fqdn"),
15712
+ stringField("component-domain", "domain"),
15713
+ stringField("component-ip-address", "ipAddress"),
15714
+ stringField("component-mac-address", "macAddress"),
15715
+ stringField("component-os-name", "osName"),
15716
+ stringField("component-os-version", "osVersion"),
15717
+ stringField("component-image-id", "imageId"),
15718
+ stringField("component-registry", "registry"),
15719
+ stringField("component-repository", "repository"),
15720
+ stringField("component-tag", "tag"),
15721
+ stringField("component-container-id", "containerId"),
15722
+ stringField("component-image", "image"),
15723
+ stringField("component-runtime", "runtime"),
15724
+ stringField("component-platform-type", "platformType"),
15725
+ stringField("component-cluster-name", "clusterName"),
15726
+ stringField("component-namespace", "namespace"),
15727
+ stringField("component-version", "version"),
15728
+ {
15729
+ prop: "component-provider",
15730
+ field: "provider",
15731
+ accepts: `one of ${cloudProviderValues().join(", ")}`,
15732
+ get: (c) => c.provider ?? void 0,
15733
+ set: (c, v) => {
15734
+ if (!validCloudProvider(v)) return false;
15735
+ c.provider = v;
15736
+ return true;
15737
+ }
15738
+ },
15739
+ stringField("component-account-id", "accountId"),
15740
+ stringField("component-region", "region"),
15741
+ stringField("component-resource-type", "resourceType"),
15742
+ stringField("component-resource-id", "resourceId"),
15743
+ stringField("component-arn", "arn"),
15744
+ stringField("component-url", "url"),
15745
+ stringField("component-branch", "branch"),
15746
+ stringField("component-commit", "commit"),
15747
+ stringField("component-environment", "environment"),
15748
+ stringField("component-package-manager", "packageManager"),
15749
+ stringField("component-package-name", "packageName"),
15750
+ stringField("component-cidr", "cidr"),
15751
+ stringField("component-gateway", "gateway"),
15752
+ stringField("component-engine", "engine"),
15753
+ stringField("component-host", "host"),
15754
+ {
15755
+ prop: "component-port",
15756
+ field: "port",
15757
+ accepts: `a decimal integer in ${MIN_PORT}..${MAX_PORT}`,
15758
+ get: (c) => c.port === void 0 ? void 0 : String(c.port),
15759
+ set: (c, v) => {
15760
+ const n = componentPort(v);
15761
+ if (n === void 0) return false;
15762
+ c.port = n;
15763
+ return true;
15764
+ }
15765
+ },
15766
+ stringField("component-model-id", "modelId"),
15767
+ stringField("component-dataset-id", "datasetId")
15768
+ ];
15769
+ const COMPONENT_GROUP_PROPS = [stringMapField("labels", "component-label"), stringMapField("externalIds", "component-external-id")];
15770
+ /**
15771
+ * Carries a component's identity fields as HDF-namespaced props, in table order. A
15772
+ * present-but-empty optional string is carried by an empty-field marker (§1.7.3);
15773
+ * an absent field emits nothing. Mirrors the Go peer.
15774
+ */
15775
+ function componentSubjectProps(c) {
15776
+ const props = [];
15777
+ for (const e of COMPONENT_PROPS) {
15778
+ const v = e.get(c);
15779
+ if (v === void 0) continue;
15780
+ if (v === "") {
15781
+ if (!e.required) props.push(emptyFieldProp(e.field));
15782
+ continue;
15783
+ }
15784
+ pushVocabularyProp(props, e.prop, v);
15785
+ }
15786
+ for (const g of COMPONENT_GROUP_PROPS) appendComponentMap(props, g, g.get(c));
15787
+ return props;
15788
+ }
15789
+ /**
15790
+ * Fills a component's identity fields from the HDF-namespaced props on its
15791
+ * subject, leaving a field the props do not carry as the caller set it. A value
15792
+ * outside its HDF field's domain — which a foreign or hand-edited SAR can carry —
15793
+ * is dropped with a warning rather than written into a document the HDF validator
15794
+ * would reject. Mirrors the Go peer.
15795
+ */
15796
+ function readComponentSubjectProps(c, props) {
15797
+ for (const e of COMPONENT_PROPS) {
15798
+ const v = readComponentProp(props, e);
15799
+ if (v === void 0) continue;
15800
+ if (!e.set(c, v)) emitConverterWarning(`Dropping ${e.prop} ${JSON.stringify(v)} on component ${JSON.stringify(c.name)}: not ${e.accepts}`);
15801
+ }
15802
+ for (const g of COMPONENT_GROUP_PROPS) {
15803
+ const m = readComponentMap(props, g);
15804
+ if (m) g.set(c, m);
15805
+ }
15806
+ }
15807
+ /**
15808
+ * Reads the HDF value the props carry for one entry. A required field has no
15809
+ * empty-field marker to consult, so its prop is read wherever it sits rather than
15810
+ * through the optional-string helper.
15811
+ */
15812
+ function readComponentProp(props, e) {
15813
+ if (e.required) return findVocabularyProp(props, e.prop)?.value;
15814
+ return vocabularyString(props, e.prop, e.field, "");
15815
+ }
15816
+ /**
15817
+ * Carries a component string map as grouped key/value props in sorted key order,
15818
+ * one group per entry, so the importer can rebuild the map. An empty key or value
15819
+ * is carried by an empty-field marker in the same group (§1.7.3). Keys sort by
15820
+ * Unicode code point, which is what Go's map-key sort produces; the default sort
15821
+ * compares UTF-16 code units and would number the groups differently.
15822
+ */
15823
+ function appendComponentMap(props, g, m) {
15824
+ if (!m) return;
15825
+ Object.keys(m).sort(byCodePoint).forEach((k, i) => {
15826
+ const group = `${g.prefix}-${i + 1}`;
15827
+ props.push(groupedComponentProp(g.keyProp, "key", group, k));
15828
+ props.push(groupedComponentProp(g.valueProp, "value", group, m[k]));
15829
+ });
15830
+ }
15831
+ /** Builds one grouped map prop, degrading to an empty-field marker when empty. */
15832
+ function groupedComponentProp(name, field, group, value) {
15833
+ if (value === "") return {
15834
+ ...emptyFieldProp(field),
15835
+ group
15836
+ };
15837
+ return {
15838
+ ...vocabularyProp(name, value),
15839
+ group
15840
+ };
15841
+ }
15842
+ /**
15843
+ * Rebuilds a component string map from grouped key/value props. Groups are
15844
+ * numbered from 1 in the order the exporter emitted them (sorted key order), so
15845
+ * reading stops at the first group with neither a key nor a value.
15846
+ */
15847
+ function readComponentMap(props, g) {
15848
+ const m = {};
15849
+ for (let n = 1;; n++) {
15850
+ const group = `${g.prefix}-${n}`;
15851
+ const key = vocabularyString(props, g.keyProp, "key", group);
15852
+ const value = vocabularyString(props, g.valueProp, "value", group);
15853
+ if (key === void 0 && value === void 0) break;
15854
+ m[key ?? ""] = value ?? "";
15855
+ }
15856
+ return Object.keys(m).length > 0 ? m : void 0;
15857
+ }
15858
+ //#endregion
15859
+ //#region converters/oscal-to-hdf/typescript/carriage.ts
15860
+ /**
15861
+ * Foreign-prop carriage through HDF for OSCAL SAR (ADR-0014 §3). The importer
15862
+ * stores every OSCAL prop it does not consume in the reserved requirement tag
15863
+ * `oscal-props`, and the exporter re-emits them. Mirrors the Go peer in
15864
+ * converters/oscal-to-hdf/go/carriage.go.
15865
+ */
15866
+ /** The reserved HDF requirement tag that carries unconsumed OSCAL SAR props. */
15867
+ const OSCAL_PROPS_TAG = "oscal-props";
15868
+ /**
15869
+ * Appends a carriage entry for every prop that HDF does not consume (ADR-0014
15870
+ * §3.1). `on` is the OSCAL object the props hang on. Source order is preserved.
15871
+ */
15872
+ function carryForeignProps(entries, on, props) {
15873
+ for (const p of props ?? []) {
15874
+ if (consumedVocabularyProp(p)) continue;
15875
+ const entry = {
15876
+ on,
15877
+ name: p.name,
15878
+ value: p.value
15879
+ };
15880
+ if (p.ns) entry.ns = p.ns;
15881
+ if (p.class) entry.class = p.class;
15882
+ if (p.group) entry.group = p.group;
15883
+ if (p.uuid) entry.uuid = p.uuid;
15884
+ if (p.remarks) entry.remarks = p.remarks;
15885
+ entries.push(entry);
15886
+ }
15887
+ }
15888
+ const isStr = (v) => typeof v === "string";
15889
+ const CARRIAGE_ON = [
15890
+ "finding",
15891
+ "observation",
15892
+ "risk"
15893
+ ];
15894
+ const CARRIAGE_OPTIONAL_MEMBERS = [
15895
+ "ns",
15896
+ "class",
15897
+ "group",
15898
+ "uuid",
15899
+ "remarks"
15900
+ ];
15901
+ const CARRIAGE_MEMBERS = /* @__PURE__ */ new Set([
15902
+ "on",
15903
+ "name",
15904
+ "value",
15905
+ ...CARRIAGE_OPTIONAL_MEMBERS
15906
+ ]);
15907
+ /**
15908
+ * Whether one decoded oscal-props entry conforms to
15909
+ * shared/oscal-props.schema.json. A foreign document may put anything in the tag,
15910
+ * and an entry the emitter would turn into a non-string OSCAL prop member, or
15911
+ * place on no object at all, is not carriage but corruption. Mirrors the Go peer.
15912
+ */
15913
+ function validCarriedProp(entry) {
15914
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return false;
15915
+ const o = entry;
15916
+ for (const k of Object.keys(o)) if (!CARRIAGE_MEMBERS.has(k)) return false;
15917
+ if (!isStr(o.on) || !CARRIAGE_ON.includes(o.on)) return false;
15918
+ if (!isStr(o.name) || o.name.length === 0) return false;
15919
+ if (!isStr(o.value)) return false;
15920
+ for (const k of CARRIAGE_OPTIONAL_MEMBERS) if (k in o && !isStr(o[k])) return false;
15921
+ return true;
15922
+ }
15923
+ /**
15924
+ * Reads the oscal-props entries stored on requirement `requirementId`'s tags.
15925
+ * Carriage is best-effort preservation, so reading is per-entry tolerant: a
15926
+ * conforming entry is kept, a malformed one is dropped, and the requirement's
15927
+ * drops are reported once. Mirrors the Go peer.
15928
+ */
15929
+ function readCarriedProps(tags, requirementId) {
15930
+ const raw = tags?.[OSCAL_PROPS_TAG];
15931
+ if (raw === void 0) return [];
15932
+ if (!Array.isArray(raw)) {
15933
+ emitConverterWarning(`Dropping the oscal-props tag on requirement ${JSON.stringify(requirementId)}: it is not an array`);
15934
+ return [];
15935
+ }
15936
+ const entries = raw.filter(validCarriedProp);
15937
+ const dropped = raw.length - entries.length;
15938
+ if (dropped > 0) emitConverterWarning(`Dropping ${dropped} malformed oscal-props ${dropped === 1 ? "entry" : "entries"} on requirement ${JSON.stringify(requirementId)}`);
15939
+ return entries;
15940
+ }
15941
+ /** The entries whose `on` matches, in carried order. */
15942
+ function carriedFor(entries, on) {
15943
+ return entries.filter((e) => e.on === on);
15944
+ }
15945
+ /**
15946
+ * Re-emits carried entries onto an object's props after its own props (ADR-0014
15947
+ * §3.4), skipping any entry whose (ns, name, value) is already present — an
15948
+ * absent ns compares equal to NIST's default namespace.
15949
+ */
15950
+ function appendCarriedProps(props, entries) {
15951
+ const seen = /* @__PURE__ */ new Set();
15952
+ for (const p of props) seen.add(carriageKey(p.ns, p.name, p.value));
15953
+ for (const e of entries) {
15954
+ const key = carriageKey(e.ns, e.name, e.value);
15955
+ if (seen.has(key)) continue;
15956
+ seen.add(key);
15957
+ const prop = {
15958
+ name: e.name,
15959
+ value: e.value
15960
+ };
15961
+ if (e.ns) prop.ns = e.ns;
15962
+ if (e.class) prop.class = e.class;
15963
+ if (e.group) prop.group = e.group;
15964
+ if (e.uuid) prop.uuid = e.uuid;
15965
+ if (e.remarks) prop.remarks = e.remarks;
15966
+ props.push(prop);
15967
+ }
15968
+ return props;
15969
+ }
15970
+ function carriageKey(ns, name, value) {
15971
+ const namespace = ns === void 0 || ns === "" ? vocabularyDefaultNamespace() : ns;
15972
+ return JSON.stringify([
15973
+ namespace,
15974
+ name,
15975
+ value
15976
+ ]);
15977
+ }
15978
+ //#endregion
15212
15979
  //#region converters/hdf-to-oscal-sar/typescript/converter.ts
15213
15980
  /**
15214
15981
  * Converts HDF Results to OSCAL Assessment Results (SAR) format.
@@ -15283,7 +16050,7 @@ function buildOSCALDocument(hdfResults) {
15283
16050
  parties: [{
15284
16051
  uuid: toolActorUuid,
15285
16052
  type: "organization",
15286
- name: toolPartyName(hdfResults)
16053
+ name: normalizePropValue(toolPartyName(hdfResults))
15287
16054
  }]
15288
16055
  };
15289
16056
  let importAP;
@@ -15343,6 +16110,8 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
15343
16110
  let description = "Converted from HDF results";
15344
16111
  if (baseline.description && baseline.description !== "") description = baseline.description;
15345
16112
  const resultProps = [];
16113
+ pushVocabularyProp(resultProps, "baseline-name", baseline.name);
16114
+ if (baseline.title && baseline.title !== "") pushVocabularyProp(resultProps, "baseline-title", baseline.title);
15346
16115
  if (typeof baseline.version === "string") pushVocabularyProp(resultProps, "baseline-version", baseline.version);
15347
16116
  const findings = [];
15348
16117
  const observations = [];
@@ -15368,10 +16137,11 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
15368
16137
  return {
15369
16138
  result: {
15370
16139
  uuid: crypto.randomUUID(),
15371
- title,
16140
+ title: normalizePropValue(title),
15372
16141
  description,
15373
16142
  start: assessmentStart(baseline, timestamp),
15374
16143
  ...resultProps.length > 0 ? { props: resultProps } : {},
16144
+ ...subjects.length > 0 ? { "local-definitions": { components: componentDefsFromSubjects(subjects) } } : {},
15375
16145
  "reviewed-controls": { "control-selections": [controlSelection] },
15376
16146
  ...findings.length > 0 ? { findings } : {},
15377
16147
  ...observations.length > 0 ? { observations } : {},
@@ -15384,7 +16154,9 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
15384
16154
  * Turns the top-level HDF components[] into OSCAL assessment subjects. Each
15385
16155
  * component's UUID (componentId when present, otherwise a fresh one) identifies
15386
16156
  * the subject; the HDF component type is a valid OSCAL subject type token and
15387
- * its name becomes the subject title.
16157
+ * its name becomes the subject title. A subject holds only uuid/type/title, so
16158
+ * each component's type-specific identity fields ride as HDF-namespaced props on
16159
+ * the subject (ADR-0014 §1.5) and reconstitute on read-back.
15388
16160
  *
15389
16161
  * A component with no type is skipped rather than given one. OSCAL requires both
15390
16162
  * subject-uuid and type on a subject-reference, so the type cannot simply be
@@ -15395,11 +16167,60 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
15395
16167
  */
15396
16168
  function buildSubjects(components) {
15397
16169
  if (!Array.isArray(components) || components.length === 0) return [];
15398
- return components.filter((c) => oscalString(c.type ?? "") !== "").map((c) => ({
15399
- "subject-uuid": c.componentId && c.componentId !== "" ? c.componentId : crypto.randomUUID(),
15400
- type: String(c.type),
15401
- title: c.name
15402
- }));
16170
+ const subjects = [];
16171
+ for (const c of components) {
16172
+ if (oscalString(c.type ?? "") === "") {
16173
+ emitConverterWarning(`hdf-to-oscal-sar: component "${c.name}" has no type, which an OSCAL assessment subject requires; not carried`);
16174
+ continue;
16175
+ }
16176
+ const subject = {
16177
+ "subject-uuid": c.componentId && c.componentId !== "" ? c.componentId : crypto.randomUUID(),
16178
+ type: String(c.type),
16179
+ title: normalizePropValue(c.name)
16180
+ };
16181
+ const props = componentSubjectProps(c);
16182
+ if (props.length > 0) subject.props = props;
16183
+ subjects.push(subject);
16184
+ warnUncarriedComponentFields(c);
16185
+ }
16186
+ return subjects;
16187
+ }
16188
+ /**
16189
+ * Turns the assessment subjects into result-level OSCAL system-components so the
16190
+ * assessed components survive a round trip even when no requirement produces an
16191
+ * observation (ADR-0014 §4.5). Each component keeps the subject's identity
16192
+ * (uuid/type/title and the HDF-namespaced props); the OSCAL-required description
16193
+ * and status have no HDF counterpart, so they are display fallbacks the importer
16194
+ * never reads back — the exact identity lives entirely in the props. Mirrors Go.
16195
+ */
16196
+ function componentDefsFromSubjects(subjects) {
16197
+ return subjects.map((s) => {
16198
+ const display = s.title !== "" ? s.title : "Assessed component";
16199
+ return {
16200
+ uuid: s["subject-uuid"],
16201
+ type: s.type,
16202
+ title: display,
16203
+ description: display,
16204
+ ...s.props ? { props: s.props } : {},
16205
+ status: { state: "other" }
16206
+ };
16207
+ });
16208
+ }
16209
+ /**
16210
+ * Reports component identity beyond the type-specific fields a SAR subject can
16211
+ * hold — owner, BOMs, artifact integrity, and the migration-only fields — so it
16212
+ * is not silently dropped (matching the lossy-conversion warning UX). Mirrors Go.
16213
+ */
16214
+ function warnUncarriedComponentFields(c) {
16215
+ const dropped = [];
16216
+ if (c.owner) dropped.push("owner");
16217
+ if (Array.isArray(c.boms) && c.boms.length > 0) dropped.push("boms");
16218
+ if (Array.isArray(c.integrity) && c.integrity.length > 0) dropped.push("integrity");
16219
+ if (Array.isArray(c.baselineRefs) && c.baselineRefs.length > 0) dropped.push("baselineRefs");
16220
+ if (Array.isArray(c.inputOverrides) && c.inputOverrides.length > 0) dropped.push("inputOverrides");
16221
+ if (c.targetSelector && Object.keys(c.targetSelector).length > 0) dropped.push("targetSelector");
16222
+ if (dropped.length === 0) return;
16223
+ emitConverterWarning(`hdf-to-oscal-sar: component "${c.name}" carries ${dropped.join(", ")}, which an OSCAL SAR assessment subject cannot represent; not carried`);
15403
16224
  }
15404
16225
  /**
15405
16226
  * Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
@@ -15462,13 +16283,18 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
15462
16283
  rel: "reference"
15463
16284
  });
15464
16285
  let title = req.id;
15465
- if (req.title && req.title !== "") title = req.title;
16286
+ if (req.title && req.title !== "") {
16287
+ title = req.title;
16288
+ pushVocabularyProp(props, "requirement-title", req.title);
16289
+ }
16290
+ const carried = readCarriedProps(req.tags, req.id);
16291
+ appendCarriedProps(props, carriedFor(carried, "finding"));
15466
16292
  let resource;
15467
16293
  if (req.code != null && req.code.trim() !== "") {
15468
16294
  const resourceUuid = crypto.randomUUID();
15469
16295
  resource = {
15470
16296
  uuid: resourceUuid,
15471
- title: `Check source code for ${req.id}`,
16297
+ title: normalizePropValue(`Check source code for ${req.id}`),
15472
16298
  props: [vocabularyProp("type", "evidence")],
15473
16299
  base64: {
15474
16300
  value: encodeBase64Utf8(req.code),
@@ -15493,7 +16319,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
15493
16319
  };
15494
16320
  const finding = {
15495
16321
  uuid: crypto.randomUUID(),
15496
- title,
16322
+ title: normalizePropValue(title),
15497
16323
  description: findingDesc || title,
15498
16324
  props: props.length > 0 ? props : void 0,
15499
16325
  links: links.length > 0 ? links : void 0,
@@ -15502,15 +16328,16 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
15502
16328
  let observation;
15503
16329
  if (results.length > 0) {
15504
16330
  const obsUUID = crypto.randomUUID();
15505
- const obsDesc = buildObservationDescription(results);
15506
16331
  const relevantEvidence = buildRelevantEvidence(req);
16332
+ const obsProps = appendCarriedProps([], carriedFor(carried, "observation"));
15507
16333
  observation = {
15508
16334
  uuid: obsUUID,
15509
- description: obsDesc,
16335
+ description: "",
15510
16336
  methods: ["TEST"],
15511
16337
  collected: formatAssessmentTime(earliestResultTime(results), timestamp),
15512
16338
  ...subjects.length > 0 ? { subjects } : {},
15513
- ...relevantEvidence.length > 0 ? { "relevant-evidence": relevantEvidence } : {}
16339
+ ...relevantEvidence.length > 0 ? { "relevant-evidence": relevantEvidence } : {},
16340
+ ...obsProps.length > 0 ? { props: obsProps } : {}
15514
16341
  };
15515
16342
  finding["related-observations"] = [{ "observation-uuid": obsUUID }];
15516
16343
  } else warnUncarriedProse(req);
@@ -15525,9 +16352,10 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
15525
16352
  }
15526
16353
  const remediations = buildRemediations(req);
15527
16354
  const deadline = riskDeadline(req);
16355
+ const riskProps = appendCarriedProps([], carriedFor(carried, "risk"));
15528
16356
  risk = {
15529
16357
  uuid: riskUUID,
15530
- title: `Risk for ${req.id}`,
16358
+ title: normalizePropValue(`Risk for ${req.id}`),
15531
16359
  description: `Impact: ${req.impact.toFixed(1)} (${severity})`,
15532
16360
  statement: `Impact: ${req.impact.toFixed(1)} (${severity})`,
15533
16361
  status: riskStatusFromState(state),
@@ -15543,10 +16371,12 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
15543
16371
  }]
15544
16372
  }],
15545
16373
  ...remediations.length > 0 ? { remediations } : {},
15546
- ...deadline ? { deadline } : {}
16374
+ ...deadline ? { deadline } : {},
16375
+ ...riskProps.length > 0 ? { props: riskProps } : {}
15547
16376
  };
15548
16377
  finding["related-risks"] = [{ "risk-uuid": riskUUID }];
15549
16378
  }
16379
+ if (observation) observation.description = observationDisplayDescription(roundTripStatus(state), observation, risk, title);
15550
16380
  return {
15551
16381
  finding,
15552
16382
  observation,
@@ -15740,17 +16570,50 @@ function extractDefaultDescription(descriptions) {
15740
16570
  return "";
15741
16571
  }
15742
16572
  /**
15743
- * Concatenates result code descriptions and messages.
16573
+ * Synthesizes an observation's display description from the OSCAL objects the
16574
+ * exporter emits, mirroring how the reverse SAR importer reconstructs a result's
16575
+ * status, codeDesc and message. Deriving it from the emitted observation and risk
16576
+ * — not from the HDF result's stored codeDesc/message — makes the SAR round trip
16577
+ * idempotent on observation.description (ADR-0014 §3.5): the string already equals
16578
+ * what the importer reads back and the next export re-synthesizes, so the first
16579
+ * HDF-produced export equals the second. observation.description is display text,
16580
+ * not a prose home (ADR-0014 §2); the requirement's real prose rides its own homes
16581
+ * (finding.target.description, relevant-evidence, risk.remediations).
15744
16582
  */
15745
- function buildObservationDescription(results) {
16583
+ function observationDisplayDescription(status, obs, risk, fallbackTitle) {
16584
+ let desc = `[${status}] ${reconstructedCodeDesc(obs, fallbackTitle)}`;
16585
+ const msg = reconstructedRiskMessage(risk);
16586
+ if (msg !== "") desc += ": " + msg;
16587
+ return desc;
16588
+ }
16589
+ /**
16590
+ * The HDF status the emitted finding's target state maps back to on import
16591
+ * (oscal-to-hdf mapFindingStatus): an unrecognized state is notReviewed, as there.
16592
+ */
16593
+ function roundTripStatus(state) {
16594
+ return oscalStatusToHdf(state) ?? "notReviewed";
16595
+ }
16596
+ /** Mirrors oscal-to-hdf buildCodeDesc for the single observation the exporter emits. */
16597
+ function reconstructedCodeDesc(obs, fallbackTitle) {
15746
16598
  const parts = [];
15747
- for (const r of results) {
15748
- let desc = `[${r.status}] ${r.codeDesc}`;
15749
- if (r.message && r.message !== "") desc += ": " + r.message;
15750
- parts.push(desc);
16599
+ if (obs.methods && obs.methods.length > 0) parts.push("Methods: " + obs.methods.join(", "));
16600
+ for (const subj of obs.subjects ?? []) {
16601
+ let subjDesc = subj.type;
16602
+ if (subj.title) subjDesc = subj.title + " (" + subj.type + ")";
16603
+ parts.push("Subject: " + subjDesc);
15751
16604
  }
15752
- if (parts.length === 0) return "No observations recorded";
15753
- return parts.join("\n");
16605
+ if (parts.length === 0) return fallbackTitle;
16606
+ return parts.join("; ");
16607
+ }
16608
+ /**
16609
+ * Mirrors oscal-to-hdf buildRiskMessage for the single risk the exporter emits
16610
+ * (undefined when the requirement's impact is 0).
16611
+ */
16612
+ function reconstructedRiskMessage(risk) {
16613
+ if (!risk) return "";
16614
+ let msg = risk.title;
16615
+ if (risk.description) msg += ": " + risk.description;
16616
+ return msg;
15754
16617
  }
15755
16618
  /**
15756
16619
  * Maps OSCAL finding state to risk status.
@@ -15796,7 +16659,7 @@ const OVERRIDE_APPLIED_TITLE = "Override applied";
15796
16659
  * through some other producer's validation.
15797
16660
  */
15798
16661
  function poamTitle(a) {
15799
- return firstNonEmpty(a.name, a.amendmentId ?? "", "HDF Amendments");
16662
+ return normalizePropValue(firstNonEmpty(a.name, a.amendmentId ?? "", "HDF Amendments"));
15800
16663
  }
15801
16664
  /**
15802
16665
  * Stands in for an empty requirementId: HDF puts no minLength on it, and OSCAL
@@ -15807,7 +16670,7 @@ function poamTitle(a) {
15807
16670
  const UNIDENTIFIED_REQUIREMENT_TITLE = "Unidentified requirement";
15808
16671
  /** The title of the risk and POA&M item an override produces. */
15809
16672
  function requirementTitle(override) {
15810
- return firstNonEmpty(override.requirementId, UNIDENTIFIED_REQUIREMENT_TITLE);
16673
+ return normalizePropValue(firstNonEmpty(override.requirementId, UNIDENTIFIED_REQUIREMENT_TITLE));
15811
16674
  }
15812
16675
  /**
15813
16676
  * Supplies the text OSCAL requires for a risk's description and statement. HDF
@@ -15843,7 +16706,7 @@ var PartyRegistry = class {
15843
16706
  ]);
15844
16707
  const existing = this.byKey.get(key);
15845
16708
  if (existing) return existing.uuid;
15846
- const name = oscalString(id.identifier);
16709
+ const name = normalizePropValue(id.identifier ?? "");
15847
16710
  const props = [];
15848
16711
  pushVocabularyProp(props, "identity-identifier", id.identifier ?? "");
15849
16712
  pushVocabularyProp(props, "identity-type", String(id.type));
@@ -16287,7 +17150,7 @@ function referenceResource(ref, b) {
16287
17150
  pushOptionalString(props, "reference-kind", "kind", ref.kind);
16288
17151
  return {
16289
17152
  uuid: crypto.randomUUID(),
16290
- title: ref.sourceName,
17153
+ title: normalizePropValue(ref.sourceName ?? ""),
16291
17154
  ...ref.description ? { description: ref.description } : {},
16292
17155
  ...props.length > 0 ? { props } : {},
16293
17156
  ...ref.href ? { rlinks: [{ href: ref.href }] } : {},
@@ -18521,6 +19384,12 @@ function definitionBaselineName(compDef) {
18521
19384
  * mapping they had before it: type "poam", with their own extension props not
18522
19385
  * carried (§3.6).
18523
19386
  *
19387
+ * Foreign-prop carriage is SAR-only (ADR-0014 §3.6): unlike a SAR requirement's
19388
+ * tags, Standalone_Override has no extension slot (unevaluatedProperties: false),
19389
+ * so a foreign POA&M's own extension props are a stated loss on import — there is
19390
+ * no oscal-props tag here and none is invented. HDF-produced POA&Ms are
19391
+ * unaffected; every HDF field round-trips through §4.6.
19392
+ *
18524
19393
  * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_poam.go.
18525
19394
  */
18526
19395
  /**
@@ -19208,7 +20077,7 @@ async function convertOscalSarToHdf(input) {
19208
20077
  emitConverterWarning(`Skipping assessment result "${title}": no finding has a target-id`);
19209
20078
  continue;
19210
20079
  }
19211
- const baseline = await resultToEvaluatedBaseline(result, groups, sar, input, scanTime);
20080
+ const baseline = await resultToEvaluatedBaseline(result, groups, input, scanTime);
19212
20081
  baselines.push(baseline);
19213
20082
  }
19214
20083
  const planRef = sar["import-ap"]?.href || void 0;
@@ -19217,6 +20086,7 @@ async function convertOscalSarToHdf(input) {
19217
20086
  const t = parseTimestamp(meta.lastModified);
19218
20087
  if (t) timestamp = t;
19219
20088
  }
20089
+ const components = sarComponents(sar);
19220
20090
  return serializeHdf({
19221
20091
  baselines,
19222
20092
  generator: {
@@ -19228,10 +20098,76 @@ async function convertOscalSarToHdf(input) {
19228
20098
  format: "OSCAL"
19229
20099
  },
19230
20100
  timestamp: timestamp ?? /* @__PURE__ */ new Date(),
19231
- planRef
20101
+ planRef,
20102
+ ...components.length > 0 ? { components } : {}
19232
20103
  });
19233
20104
  }
19234
20105
  /**
20106
+ * The set of OSCAL subject types that are HDF component types. A foreign SAR's
20107
+ * subject types (component, inventory-item, party, …) are not among them, so
20108
+ * those subjects do not reconstitute as HDF components. Mirrors the Go peer.
20109
+ */
20110
+ const HDF_COMPONENT_TYPES = /* @__PURE__ */ new Set([
20111
+ "host",
20112
+ "containerImage",
20113
+ "containerInstance",
20114
+ "containerPlatform",
20115
+ "cloudAccount",
20116
+ "cloudResource",
20117
+ "repository",
20118
+ "application",
20119
+ "artifact",
20120
+ "network",
20121
+ "database",
20122
+ "aiModel",
20123
+ "dataset"
20124
+ ]);
20125
+ /**
20126
+ * Reconstitutes the top-level HDF components from a SAR. The authoritative home is
20127
+ * each result's local-definitions.components (ADR-0014 §4.5), which survives even
20128
+ * when no requirement produced an observation; the per-observation subjects are
20129
+ * read too for documents exported before the result-level home existed. Components are deduplicated by uuid in first-seen document order — a
20130
+ * component appears identically at the result level and on every observation — and
20131
+ * one whose type is not an HDF component type is left alone so foreign SARs gain no
20132
+ * invalid components. Mirrors Go.
20133
+ */
20134
+ function sarComponents(sar) {
20135
+ const components = [];
20136
+ const seen = /* @__PURE__ */ new Set();
20137
+ const add = (subj) => {
20138
+ const uuid = subj["subject-uuid"];
20139
+ if (!uuid || seen.has(uuid) || !HDF_COMPONENT_TYPES.has(subj.type)) return;
20140
+ seen.add(uuid);
20141
+ components.push(subjectToComponent(subj));
20142
+ };
20143
+ for (const result of sar.results) {
20144
+ for (const sc of result["local-definitions"]?.components ?? []) add({
20145
+ "subject-uuid": sc.uuid,
20146
+ type: sc.type,
20147
+ title: sc.title,
20148
+ props: sc.props
20149
+ });
20150
+ for (const obs of result.observations ?? []) for (const subj of obs.subjects ?? []) add(subj);
20151
+ }
20152
+ return components;
20153
+ }
20154
+ /**
20155
+ * Rebuilds one HDF component from an assessment subject: the subject uuid is the
20156
+ * componentId (ADR-0014 §4.5 SSP analog), and every type-specific identity field
20157
+ * comes from the HDF-namespaced props the exporter stamped on the subject. The
20158
+ * subject title is the name a foreign subject carries no component-name prop for
20159
+ * (§4.3, §4.5). Mirrors the Go peer.
20160
+ */
20161
+ function subjectToComponent(subj) {
20162
+ const c = {
20163
+ type: subj.type,
20164
+ name: subj.title ?? "",
20165
+ componentId: subj["subject-uuid"]
20166
+ };
20167
+ readComponentSubjectProps(c, subj.props);
20168
+ return c;
20169
+ }
20170
+ /**
19235
20171
  * Groups findings by requirement id in first-seen order (a Map keeps insertion
19236
20172
  * order), within the finding cap, and returns the findings skipped for an empty
19237
20173
  * target-id. Mirrors Go's groupFindingsByRequirement.
@@ -19268,24 +20204,25 @@ function sarRequirementId(f) {
19268
20204
  const controlId = confirmedControlId(targetId);
19269
20205
  return controlId === void 0 ? targetId : controlIdToNistTag(controlId);
19270
20206
  }
19271
- async function resultToEvaluatedBaseline(result, groups, sar, rawInput, scanTime) {
20207
+ async function resultToEvaluatedBaseline(result, groups, rawInput, scanTime) {
19272
20208
  const checksum = await inputChecksum(rawInput);
19273
20209
  const obsMap = buildObservationMap(result.observations ?? []);
19274
20210
  const riskMap = buildRiskMap(result.risks ?? []);
19275
20211
  const requirements = [];
19276
20212
  for (const [id, findings] of groups) requirements.push(findingsToEvaluatedRequirement(id, findings, obsMap, riskMap, result, scanTime));
19277
- const name = sarBaselineName(result, sar);
20213
+ const name = sarBaselineName(result);
19278
20214
  const baseline = createMinimalBaseline(name, requirements, {
19279
20215
  resultsChecksum: checksum,
19280
20216
  integrity: await inputIntegrity(rawInput),
19281
20217
  status: "loaded",
19282
- title: result.title
20218
+ title: sarBaselineTitle(result)
19283
20219
  });
19284
20220
  if (result.description) baseline.description = result.description;
19285
20221
  return baseline;
19286
20222
  }
19287
20223
  function findingsToEvaluatedRequirement(id, findings, obsMap, riskMap, result, scanTime) {
19288
- const title = findings[0].title || id;
20224
+ const firstFinding = findings[0];
20225
+ const title = sarRequirementTitle(firstFinding) || id;
19289
20226
  const impact = sarFindingsImpact(findings, riskMap);
19290
20227
  const descriptions = sarBuildDescriptions(findings, obsMap, riskMap);
19291
20228
  const refs = sarBuildRefs(findings, obsMap);
@@ -19293,6 +20230,8 @@ function findingsToEvaluatedRequirement(id, findings, obsMap, riskMap, result, s
19293
20230
  for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result, scanTime));
19294
20231
  const nistTags = sarConfirmedNistTags(findings);
19295
20232
  const tags = buildNistCciTags(nistTags, nistToCci(nistTags));
20233
+ const carried = sarCarriedProps(findings, obsMap, riskMap);
20234
+ if (carried.length > 0) tags[OSCAL_PROPS_TAG] = carried;
19296
20235
  const req = createRequirement(id, title, descriptions, impact, results, {
19297
20236
  tags,
19298
20237
  ...refs ? { refs } : {}
@@ -19325,6 +20264,33 @@ function sarConfirmedNistTags(findings) {
19325
20264
  }
19326
20265
  return controlIdsToNistTags(controlIds);
19327
20266
  }
20267
+ /**
20268
+ * Collects the carriage entries for a requirement (ADR-0014 §3.1): every
20269
+ * unconsumed prop on its finding(s), then on their related observations, then on
20270
+ * their related risks. Observations and risks are read once each (deduplicated
20271
+ * by UUID). Mirrors Go's sarCarriedProps.
20272
+ */
20273
+ function sarCarriedProps(findings, obsMap, riskMap) {
20274
+ const entries = [];
20275
+ for (const f of findings) carryForeignProps(entries, "finding", f.props);
20276
+ const seenObs = /* @__PURE__ */ new Set();
20277
+ for (const f of findings) for (const ref of f["related-observations"] ?? []) {
20278
+ const uuid = ref["observation-uuid"];
20279
+ if (!uuid || seenObs.has(uuid)) continue;
20280
+ seenObs.add(uuid);
20281
+ const obs = obsMap.get(uuid);
20282
+ if (obs) carryForeignProps(entries, "observation", obs.props);
20283
+ }
20284
+ const seenRisk = /* @__PURE__ */ new Set();
20285
+ for (const f of findings) for (const ref of f["related-risks"] ?? []) {
20286
+ const uuid = ref["risk-uuid"];
20287
+ if (!uuid || seenRisk.has(uuid)) continue;
20288
+ seenRisk.add(uuid);
20289
+ const risk = riskMap.get(uuid);
20290
+ if (risk) carryForeignProps(entries, "risk", risk.props);
20291
+ }
20292
+ return entries;
20293
+ }
19328
20294
  function mapFindingStatus(f) {
19329
20295
  const status = oscalStatusToHdf(f.target.status.state);
19330
20296
  if (status === "passed") return ResultStatus.Passed;
@@ -19553,8 +20519,19 @@ function parseResultStartTime(result) {
19553
20519
  }
19554
20520
  return /* @__PURE__ */ new Date(0);
19555
20521
  }
19556
- function sarBaselineName(result, sar) {
19557
- return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
20522
+ function sarBaselineName(result) {
20523
+ const match = findVocabularyProp(result.props, "baseline-name");
20524
+ if (match) return match.value;
20525
+ const kebab = toKebabCase(result.title ?? "", "");
20526
+ return kebab === "" ? result.uuid : `${kebab}--${result.uuid}`;
20527
+ }
20528
+ function sarBaselineTitle(result) {
20529
+ const match = findVocabularyProp(result.props, "baseline-title");
20530
+ return match ? match.value : result.title ?? "";
20531
+ }
20532
+ function sarRequirementTitle(f) {
20533
+ const match = findVocabularyProp(f.props, "requirement-title");
20534
+ return match ? match.value : f.title ?? "";
19558
20535
  }
19559
20536
  //#endregion
19560
20537
  //#region converters/oscal-to-hdf/typescript/converter-ssp.ts