@mitre/hdf-converters 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { n as detectConverter, t as registerAllFingerprints } from "./register-all-DxZN6IJ4.js";
1
+ import { n as detectConverter, t as registerAllFingerprints } from "./register-all-WRR-vAZf.js";
2
2
  import { flattenOverlays } from "@mitre/hdf-parsers";
3
3
  import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, severityToImpact, sha256, stripHtml as stripHTML, trimUtcFraction } from "@mitre/hdf-utilities";
4
4
  import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact as severityToImpact$1 } from "@mitre/hdf-schema";
5
- import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
5
+ import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
6
6
  //#region shared/typescript/converterutil.ts
7
7
  /**
8
8
  * Shared utilities for TypeScript HDF converters.
@@ -696,6 +696,33 @@ function convertSupports(supports) {
696
696
  return out.length ? out : void 0;
697
697
  }
698
698
  /**
699
+ * Convert v1.0 attributes to v2.0 Input objects. V1 attributes are
700
+ * {name, options: {...}}; InSpec nests value/type/required/sensitive/description
701
+ * under `options`. Mirrors convertAttributes in the Go converter (omit fields
702
+ * absent from options so both languages produce identical output).
703
+ */
704
+ function convertAttributes(attrs) {
705
+ const inputs = [];
706
+ for (const attr of attrs) {
707
+ if (attr === null || typeof attr !== "object") continue;
708
+ const a = attr;
709
+ const name = typeof a.name === "string" ? a.name : "";
710
+ if (!name) continue;
711
+ const input = { name };
712
+ const options = a.options;
713
+ if (options !== null && typeof options === "object") {
714
+ const o = options;
715
+ if ("value" in o) input.value = o.value;
716
+ if (typeof o.description === "string") input.description = o.description;
717
+ if (typeof o.sensitive === "boolean") input.sensitive = o.sensitive;
718
+ if (typeof o.required === "boolean") input.required = o.required;
719
+ if (typeof o.type === "string") input.type = o.type;
720
+ }
721
+ inputs.push(input);
722
+ }
723
+ return inputs;
724
+ }
725
+ /**
699
726
  * Convert v1.0 profile to v2.0 baseline.
700
727
  * Transforms field names and nested structures.
701
728
  */
@@ -712,7 +739,7 @@ function convertProfile(v1Profile) {
712
739
  const supports = convertSupports(v1Profile.supports);
713
740
  if (supports) v2Baseline.supports = supports;
714
741
  }
715
- if (v1Profile.attributes?.length) v2Baseline.inputs = v1Profile.attributes;
742
+ if (v1Profile.attributes?.length) v2Baseline.inputs = convertAttributes(v1Profile.attributes);
716
743
  if (v1Profile.status !== void 0) v2Baseline.status = v1Profile.status;
717
744
  if (v1Profile.sha256) v2Baseline.integrity = {
718
745
  algorithm: "sha256",
@@ -798,6 +825,335 @@ function isHDFV1(data) {
798
825
  return typeof obj.version === "string" && Array.isArray(obj.profiles) && typeof obj.platform === "object" && obj.platform !== null;
799
826
  }
800
827
  //#endregion
828
+ //#region converters/asff-to-hdf/typescript/converter.ts
829
+ /**
830
+ * AWS Security Finding Format (ASFF) to HDF converter.
831
+ *
832
+ * A single ASFF envelope can carry findings from many products, and Security Hub
833
+ * findings span multiple compliance standards (CIS, AFSBP, PCI, ...). Because
834
+ * hdf-results supports multiple baselines in one document, each product — and
835
+ * each Security Hub standard — becomes its own baseline entry, mirroring the
836
+ * per-file split the predecessor SAF CLI produced.
837
+ */
838
+ function productOf(f) {
839
+ const [company, prod] = productArnParts(f.ProductArn);
840
+ if (prod === "securityhub") return "securityHub";
841
+ if (company === "prowler" && prod === "prowler") return "prowler";
842
+ if (company === "aquasecurity" && prod === "aquasecurity") return "trivy";
843
+ return "default";
844
+ }
845
+ /** The CVE id Trivy stashes under Resources[0].Details.Other. */
846
+ function trivyCVE(f) {
847
+ return f.Resources?.[0]?.Details?.Other?.["CVE ID"] ?? "";
848
+ }
849
+ /** Accepts the `{ "Findings": [...] }` envelope, a bare array, a single finding, or NDJSON (Prowler). */
850
+ function parseFindings$1(input) {
851
+ try {
852
+ const parsed = parseJSON(input);
853
+ if (parsed !== null && typeof parsed === "object" && "Findings" in parsed) {
854
+ const findings = parsed.Findings;
855
+ return Array.isArray(findings) ? findings : [];
856
+ }
857
+ if (Array.isArray(parsed)) return parsed;
858
+ if (parsed !== null && typeof parsed === "object") return [parsed];
859
+ } catch {}
860
+ const out = input.trim().split("\n").filter((l) => l.trim().length > 0).map((l) => parseJSON(l.trim())).filter((x) => x !== null && typeof x === "object" && !Array.isArray(x));
861
+ if (out.length > 0) return out;
862
+ throw new Error("invalid ASFF JSON");
863
+ }
864
+ /**
865
+ * mapComplianceStatus maps ASFF Compliance.Status to an HDF result status.
866
+ * hdf-results has no "skipped": WARNING and NOT_AVAILABLE (no clean pass/fail
867
+ * verdict) map to notReviewed; an absent status defaults to failed.
868
+ */
869
+ function mapComplianceStatus$1(status) {
870
+ switch (status) {
871
+ case "PASSED": return ResultStatus.Passed;
872
+ case "FAILED": return ResultStatus.Failed;
873
+ case "WARNING":
874
+ case "NOT_AVAILABLE": return ResultStatus.NotReviewed;
875
+ case void 0:
876
+ case "": return ResultStatus.Failed;
877
+ default: return ResultStatus.Error;
878
+ }
879
+ }
880
+ function severityLabelToImpact(label) {
881
+ switch ((label ?? "").toUpperCase()) {
882
+ case "CRITICAL": return .9;
883
+ case "HIGH": return .7;
884
+ case "MEDIUM": return .5;
885
+ case "LOW": return .3;
886
+ default: return 0;
887
+ }
888
+ }
889
+ /**
890
+ * findingImpact derives a 0.0–1.0 impact. Suppressed findings are forced to 0.
891
+ * Security Hub's INFORMATIONAL is up-graded to MEDIUM (Security Hub over-marks
892
+ * findings INFORMATIONAL without context).
893
+ */
894
+ function findingImpact(f) {
895
+ if (f.Workflow?.Status === "SUPPRESSED") return 0;
896
+ let label = f.Severity?.Label;
897
+ if (isSecurityHub(f) && (label ?? "").toUpperCase() === "INFORMATIONAL") label = "MEDIUM";
898
+ if (label) return severityLabelToImpact(label);
899
+ if (typeof f.Severity?.Normalized === "number") return f.Severity.Normalized / 100;
900
+ return 0;
901
+ }
902
+ function productArnParts(arn) {
903
+ if (!arn) return ["", ""];
904
+ const segs = (arn.split(":").pop() ?? "").split("/");
905
+ if (segs.length >= 3) return [segs[1], segs[2]];
906
+ return ["", ""];
907
+ }
908
+ function isSecurityHub(f) {
909
+ return productOf(f) === "securityHub";
910
+ }
911
+ const normalizeStd = (s) => s.toLowerCase().replace(/-/g, " ");
912
+ function titleCaseWords(s) {
913
+ return s.split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
914
+ }
915
+ /**
916
+ * securityHubStandardName derives "CIS AWS Foundations Benchmark v1.2.0" from a
917
+ * finding's StandardsControlArn, preferring the nicer Types[0] casing when it
918
+ * matches the ARN's standard slug (mirrors the SAF CLI grouping key).
919
+ */
920
+ function securityHubStandardName(f) {
921
+ const arn = f.ProductFields?.StandardsControlArn ?? "";
922
+ if (!arn) return "";
923
+ const segs = arn.split("/");
924
+ if (segs.length < 4) return "";
925
+ const slug = segs[segs.length - 4];
926
+ const version = segs[segs.length - 2];
927
+ let typesLast = "";
928
+ if (f.Types && f.Types.length > 0) {
929
+ const ts = f.Types[0].split("/");
930
+ typesLast = ts[ts.length - 1];
931
+ }
932
+ return `${typesLast && normalizeStd(typesLast) === normalizeStd(slug) ? typesLast.replace(/-/g, " ") : titleCaseWords(slug.replace(/-/g, " "))} v${version}`;
933
+ }
934
+ /** Level-1 grouping key, per product. */
935
+ function baselineName(f) {
936
+ switch (productOf(f)) {
937
+ case "securityHub": {
938
+ const name = securityHubStandardName(f);
939
+ if (name) return name;
940
+ break;
941
+ }
942
+ case "prowler":
943
+ if (f.ProductFields?.ProviderName) return f.ProductFields.ProviderName;
944
+ break;
945
+ case "trivy": return "Aqua Security - Trivy";
946
+ }
947
+ const [company, prod] = productArnParts(f.ProductArn);
948
+ if (!company && !prod) return "AWS Security Finding Format";
949
+ return `${company} - ${prod}`;
950
+ }
951
+ /** Level-2 grouping key within a baseline, per product. */
952
+ function controlId(f) {
953
+ switch (productOf(f)) {
954
+ case "securityHub":
955
+ if (f.ProductFields?.ControlId) return f.ProductFields.ControlId;
956
+ if (f.ProductFields?.RuleId) return f.ProductFields.RuleId;
957
+ break;
958
+ case "prowler": {
959
+ const i = (f.GeneratorId ?? "").indexOf("-");
960
+ if (i >= 0) return (f.GeneratorId ?? "").slice(i + 1);
961
+ break;
962
+ }
963
+ case "trivy": return `${f.GeneratorId ?? ""}/${trivyCVE(f) || f.Id || ""}`;
964
+ }
965
+ if (f.Compliance?.Status && f.GeneratorId) {
966
+ const segs = f.GeneratorId.split("/");
967
+ if (segs[segs.length - 1]) return segs[segs.length - 1];
968
+ }
969
+ return f.Id ?? "";
970
+ }
971
+ /**
972
+ * nistTags derives NIST controls for a finding group: AWS Config rule → NIST via
973
+ * the shared awsconfig mapping, falling back to the static analysis default
974
+ * bundle (matching the SAF CLI) when no config rule applies.
975
+ */
976
+ function nistTags(group) {
977
+ if (group.length > 0 && productOf(group[0]) === "trivy" && trivyCVE(group[0])) return DEFAULT_REMEDIATION_NIST_TAGS$1;
978
+ const out = [];
979
+ const seen = /* @__PURE__ */ new Set();
980
+ for (const f of group) for (const tag of configRuleNist(f)) if (!seen.has(tag)) {
981
+ seen.add(tag);
982
+ out.push(tag);
983
+ }
984
+ return out.length > 0 ? out : DEFAULT_STATIC_ANALYSIS_NIST_TAGS;
985
+ }
986
+ function configRuleNist(f) {
987
+ if (f.ProductFields?.["RelatedAWSResources:0/type"] !== "AWS::Config::ConfigRule") return [];
988
+ const name = f.ProductFields?.["RelatedAWSResources:0/name"];
989
+ if (!name) return [];
990
+ return getAwsConfigNistControlsBySubstring(name);
991
+ }
992
+ function remediationText(f) {
993
+ const parts = [];
994
+ const rec = f.Remediation?.Recommendation;
995
+ if (rec?.Text) parts.push(rec.Text);
996
+ if (rec?.Url) parts.push(rec.Url);
997
+ return parts.join("\n");
998
+ }
999
+ function resourceCodeDesc(f) {
1000
+ return `Resources: [${(f.Resources ?? []).map((r) => {
1001
+ let seg = `Type: ${r.Type ?? ""}, Id: ${r.Id ?? ""}`;
1002
+ if (r.Partition) seg += `, Partition: ${r.Partition}`;
1003
+ if (r.Region) seg += `, Region: ${r.Region}`;
1004
+ return seg;
1005
+ }).join("; ")}]`;
1006
+ }
1007
+ function statusReason(f) {
1008
+ const lines = [];
1009
+ for (const r of f.Compliance?.StatusReasons ?? []) {
1010
+ if (r.ReasonCode) lines.push(`ReasonCode: ${r.ReasonCode}`);
1011
+ if (r.Description) lines.push(`Description: ${r.Description}`);
1012
+ }
1013
+ return lines.join("\n");
1014
+ }
1015
+ function buildResult$2(f) {
1016
+ const start = parseTimestamp(f.LastObservedAt ?? f.UpdatedAt ?? "") ?? /* @__PURE__ */ new Date();
1017
+ let status = mapComplianceStatus$1(f.Compliance?.Status);
1018
+ let codeDesc = resourceCodeDesc(f);
1019
+ let message = statusReason(f);
1020
+ switch (productOf(f)) {
1021
+ case "prowler":
1022
+ codeDesc = f.Description ?? "";
1023
+ break;
1024
+ case "trivy": {
1025
+ status = ResultStatus.Failed;
1026
+ const m = trivyMessage(f);
1027
+ if (m) message = m;
1028
+ break;
1029
+ }
1030
+ }
1031
+ const vuln = vulnerabilitySummary(f);
1032
+ if (vuln) message = message ? `${message}\n${vuln}` : vuln;
1033
+ return createResult(status, message || void 0, {
1034
+ codeDesc,
1035
+ startTime: start
1036
+ });
1037
+ }
1038
+ /**
1039
+ * Renders a finding's ASFF Vulnerabilities[] as a compact text block: CVE id,
1040
+ * CVSS base score, EPSS, exploit/fix availability, and affected packages.
1041
+ * Generic — applies to any producer that emits the field.
1042
+ */
1043
+ function vulnerabilitySummary(f) {
1044
+ return (f.Vulnerabilities ?? []).map((v) => {
1045
+ const parts = [v.Id ?? ""];
1046
+ const cvss = v.Cvss?.[0];
1047
+ if (cvss && typeof cvss.BaseScore === "number") parts.push(`CVSS ${cvss.Version ?? ""} ${cvss.BaseScore.toFixed(1)}`);
1048
+ if (typeof v.EpssScore === "number") parts.push(`EPSS ${v.EpssScore.toFixed(4)}`);
1049
+ if (v.ExploitAvailable) parts.push(`exploit ${v.ExploitAvailable.toLowerCase()}`);
1050
+ if (v.FixAvailable) parts.push(`fix ${v.FixAvailable.toLowerCase()}`);
1051
+ for (const p of v.VulnerablePackages ?? []) {
1052
+ let pkg = `${p.Name ?? ""}@${p.Version ?? ""}`;
1053
+ if (p.FixedInVersion) pkg += ` (fixed in ${p.FixedInVersion})`;
1054
+ parts.push(pkg);
1055
+ }
1056
+ return parts.join("; ");
1057
+ }).join("\n");
1058
+ }
1059
+ /** Summarizes the installed vs patched package for a Trivy CVE finding. */
1060
+ function trivyMessage(f) {
1061
+ const o = f.Resources?.[0]?.Details?.Other;
1062
+ if (!o || !o["CVE ID"]) return "";
1063
+ const patched = o["Patched Package"];
1064
+ const patchMsg = patched ? `The package has been patched since version(s): ${patched}.` : "There is no patched version of the package.";
1065
+ return `For package ${o["PkgName"]}, the current version that is installed is ${o["Installed Package"]}. ${patchMsg}`;
1066
+ }
1067
+ function buildRequirement$17(id, group) {
1068
+ const primary = group[0];
1069
+ const impact = Math.max(...group.map(findingImpact));
1070
+ const descriptions = [{
1071
+ label: "default",
1072
+ data: productOf(primary) === "prowler" ? " " : primary.Description ?? primary.Title ?? ""
1073
+ }];
1074
+ const fix = remediationText(primary);
1075
+ if (fix) descriptions.push({
1076
+ label: "fix",
1077
+ data: fix
1078
+ });
1079
+ const nist = nistTags(group);
1080
+ const tags = nist.length > 0 ? buildNistCciTags(nist, nistToCci(nist)) : {};
1081
+ const results = group.map(buildResult$2);
1082
+ const req = createRequirement(id, primary.Title ?? "", descriptions, impact, results, { tags });
1083
+ const controlType = deriveControlTypeFromTags(nist);
1084
+ if (controlType !== void 0) req.controlType = controlType;
1085
+ const refs = [];
1086
+ if (primary.SourceUrl) refs.push({ url: primary.SourceUrl });
1087
+ const seen = /* @__PURE__ */ new Set();
1088
+ for (const v of primary.Vulnerabilities ?? []) for (const u of v.ReferenceUrls ?? []) if (u && !seen.has(u)) {
1089
+ seen.add(u);
1090
+ refs.push({ url: u });
1091
+ }
1092
+ if (refs.length > 0) req.refs = refs;
1093
+ return req;
1094
+ }
1095
+ function buildBaseline$1(name, findings, resultsChecksum) {
1096
+ const order = [];
1097
+ const groups = /* @__PURE__ */ new Map();
1098
+ for (const f of findings) {
1099
+ const id = controlId(f);
1100
+ const existing = groups.get(id);
1101
+ if (existing) existing.push(f);
1102
+ else {
1103
+ order.push(id);
1104
+ groups.set(id, [f]);
1105
+ }
1106
+ }
1107
+ return createMinimalBaseline(name, order.map((id) => buildRequirement$17(id, groups.get(id))), { resultsChecksum });
1108
+ }
1109
+ /** Converts an ASFF document to HDF Results JSON. */
1110
+ async function convertAsffToHdf(input, converterVersion = "1.0.0") {
1111
+ if (!input || input.trim().length === 0) throw new Error("asff: empty input");
1112
+ validateInputSize(input, "asff");
1113
+ const findings = parseFindings$1(input);
1114
+ const resultsChecksum = await inputChecksum(input);
1115
+ const order = [];
1116
+ const byBaseline = /* @__PURE__ */ new Map();
1117
+ const accounts = [];
1118
+ const seenAccount = /* @__PURE__ */ new Set();
1119
+ for (const f of findings) {
1120
+ const name = baselineName(f);
1121
+ const existing = byBaseline.get(name);
1122
+ if (existing) existing.push(f);
1123
+ else {
1124
+ order.push(name);
1125
+ byBaseline.set(name, [f]);
1126
+ }
1127
+ if (f.AwsAccountId && !seenAccount.has(f.AwsAccountId)) {
1128
+ seenAccount.add(f.AwsAccountId);
1129
+ accounts.push(f.AwsAccountId);
1130
+ }
1131
+ }
1132
+ let baselines = order.map((name) => buildBaseline$1(name, byBaseline.get(name), resultsChecksum));
1133
+ if (baselines.length === 0) baselines = [createMinimalBaseline("AWS Security Finding Format", [buildNoFindingsRequirement("asff-no-findings", "AWS Security Finding Format input contained zero findings.", /* @__PURE__ */ new Date())], { resultsChecksum })];
1134
+ const hdf = {
1135
+ baselines,
1136
+ generator: {
1137
+ name: "asff-to-hdf",
1138
+ version: converterVersion
1139
+ },
1140
+ tool: {
1141
+ name: "AWS Security Finding Format",
1142
+ format: "JSON"
1143
+ },
1144
+ timestamp: /* @__PURE__ */ new Date()
1145
+ };
1146
+ if (accounts.length > 0) {
1147
+ const refs = baselines.map((b) => b.name);
1148
+ hdf.components = accounts.map((acct) => ({
1149
+ name: acct,
1150
+ type: TargetType.CloudAccount,
1151
+ baselineRefs: refs
1152
+ }));
1153
+ }
1154
+ return serializeHdf(hdf);
1155
+ }
1156
+ //#endregion
801
1157
  //#region converters/sarif-to-hdf/typescript/converter.ts
802
1158
  const IMPACT_MAPPING$9 = {
803
1159
  error: .7,
@@ -1949,6 +2305,9 @@ function parseCklb(input) {
1949
2305
  return {
1950
2306
  format: "cklb",
1951
2307
  cklbVersion: nz(doc.cklb_version),
2308
+ active: doc.active ?? false,
2309
+ hasPath: doc.has_path ?? false,
2310
+ mode: doc.mode ?? 0,
1952
2311
  asset,
1953
2312
  stigs
1954
2313
  };
@@ -1989,8 +2348,9 @@ function serializeCklb(cl) {
1989
2348
  const doc = {
1990
2349
  title: cklbTitle(cl),
1991
2350
  cklb_version: cl.cklbVersion || "1.0",
1992
- active: false,
1993
- has_path: false,
2351
+ active: cl.active ?? false,
2352
+ ...cl.mode ? { mode: cl.mode } : {},
2353
+ has_path: cl.hasPath ?? false,
1994
2354
  target_data: {
1995
2355
  target_type: cl.asset.assetType || "Computing",
1996
2356
  host_name: cl.asset.hostName ?? "",
@@ -2132,6 +2492,9 @@ function assetToComponent(a) {
2132
2492
  function rootExtensions(cl) {
2133
2493
  const ext = { checklistFormat: cl.format || "ckl" };
2134
2494
  if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
2495
+ if (cl.active) ext["cklbActive"] = true;
2496
+ if (cl.hasPath) ext["cklbHasPath"] = true;
2497
+ if (cl.mode) ext["cklbMode"] = cl.mode;
2135
2498
  const ax = {};
2136
2499
  setIf$1(ax, "role", cl.asset.role);
2137
2500
  setIf$1(ax, "assetType", cl.asset.assetType);
@@ -2178,6 +2541,9 @@ function hdfToChecklist(input) {
2178
2541
  return {
2179
2542
  format,
2180
2543
  cklbVersion: cklbVersion || void 0,
2544
+ active: boolVal(ext, "cklbActive"),
2545
+ hasPath: boolVal(ext, "cklbHasPath"),
2546
+ mode: numVal(ext, "cklbMode"),
2181
2547
  asset,
2182
2548
  stigs
2183
2549
  };
@@ -2279,6 +2645,13 @@ function strVal(m, key) {
2279
2645
  const v = m[key];
2280
2646
  return typeof v === "string" ? v : "";
2281
2647
  }
2648
+ function boolVal(m, key) {
2649
+ return m[key] === true;
2650
+ }
2651
+ function numVal(m, key) {
2652
+ const v = m[key];
2653
+ return typeof v === "number" ? v : 0;
2654
+ }
2282
2655
  function strSlice(m, key) {
2283
2656
  const v = m[key];
2284
2657
  if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
@@ -3497,16 +3870,11 @@ function buildResult(r) {
3497
3870
  const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
3498
3871
  const status = mapComplianceStatus(r.ComplianceType);
3499
3872
  const codeDesc = buildCodeDesc$7(q);
3500
- const message = buildResultMessage(codeDesc, r.Annotation, status);
3501
- const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
3502
- const runTime = computeRunTime(r.ConfigRuleInvokedTime, r.ResultRecordedTime);
3503
- return {
3504
- status,
3873
+ return createResult(status, buildResultMessage(codeDesc, r.Annotation, status), {
3505
3874
  codeDesc,
3506
- startTime,
3507
- ...runTime !== void 0 ? { runTime } : {},
3508
- ...message !== void 0 ? { message } : {}
3509
- };
3875
+ startTime: (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
3876
+ runTime: computeRunTime(r.ConfigRuleInvokedTime, r.ResultRecordedTime)
3877
+ });
3510
3878
  }
3511
3879
  /**
3512
3880
  * Synthesizes a single HDF result for a Config rule whose live evaluation
@@ -3517,11 +3885,10 @@ function buildResult(r) {
3517
3885
  */
3518
3886
  function buildNotApplicableResult(rule) {
3519
3887
  const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
3520
- return {
3521
- status: ResultStatus.NotApplicable,
3888
+ return createResult(ResultStatus.NotApplicable, void 0, {
3522
3889
  codeDesc,
3523
3890
  startTime: /* @__PURE__ */ new Date()
3524
- };
3891
+ });
3525
3892
  }
3526
3893
  function buildRequirement$15(rule) {
3527
3894
  const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
@@ -3618,12 +3985,10 @@ function checkToResult(check, scanTime) {
3618
3985
  const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
3619
3986
  let message;
3620
3987
  if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
3621
- return {
3622
- status,
3988
+ return createResult(status, message, {
3623
3989
  codeDesc,
3624
- startTime: scanTime,
3625
- ...message !== void 0 ? { message } : {}
3626
- };
3990
+ startTime: scanTime
3991
+ });
3627
3992
  }
3628
3993
  /**
3629
3994
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
@@ -4645,472 +5010,202 @@ function parseBom(input) {
4645
5010
  }
4646
5011
  }
4647
5012
  //#endregion
4648
- //#region converters/cyclonedx-to-hdf/typescript/converter.ts
4649
- const CVSS_METHODS = /* @__PURE__ */ new Set([
4650
- "CVSSv2",
4651
- "CVSSv3",
4652
- "CVSSv31",
4653
- "CVSSv4"
4654
- ]);
5013
+ //#region shared/typescript/exportmap.ts
4655
5014
  /**
4656
- * Computes the maximum impact across all ratings for a vulnerability.
4657
- * Prefers CVSS score/10 when available, falls back to severityToImpact().
5015
+ * Generic, source-tool-agnostic mapping helpers shared by the HDF export
5016
+ * converters (hdf-to-ecs, hdf-to-splunk, ...).
5017
+ *
5018
+ * The export converters deliberately operate on generically-parsed JSON rather
5019
+ * than typed HDF structs, so their output can be held byte-identical with the
5020
+ * Go implementations. This module centralizes the generic accessors, the status
5021
+ * roll-up, the requirement/document field extraction, and the canonical
5022
+ * (key-sorted, HTML-unescaped) line serialization. Target-specific event shaping
5023
+ * (ECS field names, CIM field names, envelopes) stays in each converter.
4658
5024
  */
4659
- function maxImpact(ratings) {
4660
- if (ratings.length === 0) return .5;
4661
- let max = 0;
4662
- for (const rating of ratings) {
4663
- let impact;
4664
- if (rating.method && CVSS_METHODS.has(rating.method) && rating.score !== void 0 && rating.score !== null) impact = rating.score / 10;
4665
- else impact = severityToImpact$1(rating.severity ?? "medium");
4666
- if (impact > max) max = impact;
4667
- }
4668
- return max;
5025
+ function asMap(v) {
5026
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
4669
5027
  }
4670
- /**
4671
- * Formats the ratings as a human-readable tag string.
4672
- */
4673
- function formatRatingsTag(ratings) {
4674
- return ratings.map((r) => `${r.source?.name ?? "Unknown"} - ${r.severity ?? "unrated"}`).join(", ");
5028
+ function asArr(v) {
5029
+ return Array.isArray(v) ? v : void 0;
4675
5030
  }
4676
- /**
4677
- * Formats a component reference as a code_desc string.
4678
- */
4679
- function formatCodeDesc$4(componentLookup, ref) {
4680
- const comp = componentLookup.get(ref);
4681
- if (!comp) return `Component ${ref} is vulnerable`;
4682
- let name = "";
4683
- if (comp.group) name += comp.group + "/";
4684
- name += comp.name;
4685
- if (comp.version) name += "@" + comp.version;
4686
- return `Component ${name} is vulnerable`;
5031
+ function getStr(m, key) {
5032
+ const v = m?.[key];
5033
+ return typeof v === "string" ? v : "";
4687
5034
  }
4688
- /**
4689
- * Flattens nested CycloneDX components into a single array.
4690
- */
4691
- function flattenComponents(components) {
4692
- const result = [];
4693
- for (const comp of components) {
4694
- result.push(comp);
4695
- if (comp.components) result.push(...flattenComponents(comp.components));
5035
+ function setIf(m, key, val) {
5036
+ if (val !== "") m[key] = val;
5037
+ }
5038
+ function stringSlice(v) {
5039
+ if (typeof v === "string") return [v];
5040
+ if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
5041
+ return [];
5042
+ }
5043
+ /** Most-significant status across a requirement's results[] (lossless). */
5044
+ function worstOfResults(req) {
5045
+ const results = asArr(req.results) ?? [];
5046
+ const precedence = [
5047
+ "failed",
5048
+ "error",
5049
+ "passed",
5050
+ "notReviewed",
5051
+ "notApplicable"
5052
+ ];
5053
+ const present = /* @__PURE__ */ new Set();
5054
+ for (const rRaw of results) {
5055
+ const r = asMap(rRaw);
5056
+ if (r) present.add(getStr(r, "status"));
4696
5057
  }
4697
- return result;
5058
+ for (const s of precedence) if (present.has(s)) return s;
5059
+ return "notReviewed";
4698
5060
  }
4699
5061
  /**
4700
- * Reports whether any (possibly nested) component is a machine-learning-model,
4701
- * i.e. the CycloneDX document is an AI-BOM.
5062
+ * Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
5063
+ * 'error' is indeterminate (not a compliance failure) and every other value is
5064
+ * non-failing. Single shared definition of "failing" for the suppression axis
5065
+ * and the per-exporter outcome maps.
4702
5066
  */
4703
- function hasMLModelComponent(components) {
4704
- return flattenComponents(components).some((comp) => comp.type === "machine-learning-model");
5067
+ function isFailing(status) {
5068
+ return status === "failed";
4705
5069
  }
4706
- /**
4707
- * Converts CycloneDX SBOM/VEX JSON to HDF format.
4708
- *
4709
- * @param input - CycloneDX JSON string
4710
- * @returns HDF JSON string
4711
- */
4712
- async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
4713
- if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
4714
- validateInputSize(input, "cyclonedx");
4715
- const bom = parseJSON(input);
4716
- if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
4717
- if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
4718
- if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
4719
- if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) {
4720
- if (hasMLModelComponent(bom.components ?? [])) throw new Error("cyclonedx: this file is a CycloneDX AI-BOM (machine-learning-model inventory) with no vulnerabilities; to import it into a system document, use:\n hdf system create <file> --from cyclonedx-mlbom");
4721
- throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create <sbom-file> --component-name <name>");
5070
+ /** Resolve the status context for a requirement. */
5071
+ function statusOf(req) {
5072
+ const raw = worstOfResults(req);
5073
+ const effective = getStr(req, "effectiveStatus");
5074
+ const overrides = asArr(req.statusOverrides);
5075
+ const rollup = effective !== "" ? effective : raw;
5076
+ return {
5077
+ raw,
5078
+ effective,
5079
+ rollup,
5080
+ overridden: overrides !== void 0 && overrides.length > 0 || "effectiveStatus" in req,
5081
+ suppressed: isFailing(raw) && !isFailing(rollup)
5082
+ };
5083
+ }
5084
+ function firstComponent(doc) {
5085
+ const comps = asArr(doc.components);
5086
+ if (!comps || comps.length === 0) return void 0;
5087
+ return asMap(comps[0]);
5088
+ }
5089
+ function firstResultStartTime(req, fallback) {
5090
+ const results = asArr(req.results) ?? [];
5091
+ if (results.length > 0) {
5092
+ const st = getStr(asMap(results[0]), "startTime");
5093
+ if (st !== "") return st;
4722
5094
  }
4723
- const resultsChecksum = await inputChecksum(input);
4724
- const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
4725
- const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
4726
- const allComponents = flattenComponents(bom.components ?? []);
4727
- const componentLookup = /* @__PURE__ */ new Map();
4728
- for (const comp of allComponents) if (comp["bom-ref"]) componentLookup.set(comp["bom-ref"], comp);
4729
- const requirements = [];
4730
- const { items: limitedVulns, truncated: truncatedVulns } = limitArray(bom.vulnerabilities ?? []);
4731
- /* v8 ignore next -- truncation only triggers with >100K items */
4732
- if (truncatedVulns) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${(bom.vulnerabilities ?? []).length})`);
4733
- for (const vuln of limitedVulns) {
4734
- const ratings = vuln.ratings ?? [];
4735
- const impact = maxImpact(ratings);
4736
- const cwes = vuln.cwes ?? [];
4737
- const nist = mapCWEToNIST(cwes.map((c) => `${c}`), DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
4738
- const tags = {
4739
- nist,
4740
- cci: nistToCci(nist)
4741
- };
4742
- if (cwes.length > 0) tags["cweid"] = cwes.map((c) => `CWE-${c}`);
4743
- if (ratings.length > 0) tags["ratings"] = formatRatingsTag(ratings);
4744
- const descriptions = [];
4745
- const defaultParts = [];
4746
- if (vuln.description) defaultParts.push(`Description: ${vuln.description}`);
4747
- if (vuln.detail) defaultParts.push(`Detail: ${vuln.detail}`);
4748
- if (defaultParts.length > 0) descriptions.push({
4749
- label: "default",
4750
- data: defaultParts.join("\n\n")
4751
- });
4752
- else descriptions.push({
4753
- label: "default",
4754
- data: vuln.id
4755
- });
4756
- const fixParts = [];
4757
- if (vuln.recommendation) fixParts.push(vuln.recommendation);
4758
- if (vuln.analysis?.detail) fixParts.push(`Workaround: ${vuln.analysis.detail}`);
4759
- if (fixParts.length > 0) descriptions.push({
4760
- label: "fix",
4761
- data: fixParts.join("\n\n")
4762
- });
4763
- const affects = vuln.affects ?? [];
4764
- const toResult = (codeDesc) => ({
4765
- status: ResultStatus.Failed,
4766
- codeDesc,
4767
- startTime: scanTime
4768
- });
4769
- const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
4770
- const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
4771
- const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
4772
- const controlType = deriveControlTypeFromTags(nist);
4773
- if (controlType !== void 0) req.controlType = controlType;
4774
- requirements.push(req);
5095
+ return fallback;
5096
+ }
5097
+ function defaultDescription(req) {
5098
+ const descs = asArr(req.descriptions) ?? [];
5099
+ for (const dRaw of descs) {
5100
+ const d = asMap(dRaw);
5101
+ if (d && getStr(d, "label") === "default") return getStr(d, "data");
4775
5102
  }
4776
- const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
4777
- const targetComponent = {
4778
- name: bom.metadata?.component?.name ?? "",
4779
- type: TargetType.Application
4780
- };
4781
- if (bom.metadata?.component?.version) targetComponent.version = bom.metadata.component.version;
4782
- const parsedBom = parseBom(input);
4783
- const bomParts = {
4784
- bomType: BOMType.Sbom,
4785
- format: "cyclonedx",
4786
- uniqueId: parsedBom.normalized.uniqueId,
4787
- document: JSON.parse(input)
4788
- };
4789
- if (parsedBom.normalized.packages && parsedBom.normalized.packages.length > 0) bomParts.packages = parsedBom.normalized.packages;
4790
- const componentBom = buildBom(bomParts);
4791
- return buildHdfResults({
4792
- generatorName: "cyclonedx-to-hdf",
4793
- converterVersion,
4794
- toolName: "CycloneDX",
4795
- toolFormat: "JSON",
4796
- baselines: [baseline],
4797
- components: [{
4798
- ...targetComponent,
4799
- boms: [componentBom]
4800
- }],
4801
- timestamp: scanTime
4802
- });
5103
+ return "";
4803
5104
  }
4804
- //#endregion
4805
- //#region converters/hdf-to-csv/typescript/converter.ts
4806
- /**
4807
- * Convert HDF JSON to CSV format
4808
- * @param input HDF JSON string
4809
- * @returns CSV string with sanitized output
4810
- */
4811
- function convertHdfToCsv(input) {
4812
- validateInputSize(input, "hdf-to-csv");
4813
- const hdf = parseHdf(input);
4814
- if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
4815
- if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
4816
- const rows = [];
4817
- const components = hdf.components || [];
4818
- const targetList = components.length > 0 ? components.map((t) => ({
4819
- name: t.name,
4820
- type: t.type
4821
- })) : [{
4822
- name: "",
4823
- type: ""
4824
- }];
4825
- for (const baseline of hdf.baselines) for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
4826
- return buildCsv(rows, { sanitize: true });
5105
+ function firstRefURL(req) {
5106
+ const refs = asArr(req.refs) ?? [];
5107
+ for (const rRaw of refs) {
5108
+ const url = getStr(asMap(rRaw), "url");
5109
+ if (url !== "") return url;
5110
+ }
5111
+ return "";
5112
+ }
5113
+ /** Deterministic event id: component | baseline | control. */
5114
+ function eventID(component, baselineName, controlID) {
5115
+ let comp = "";
5116
+ if (component) comp = getStr(component, "componentId") || getStr(component, "name");
5117
+ return [
5118
+ comp,
5119
+ baselineName,
5120
+ controlID
5121
+ ].join("|");
4827
5122
  }
4828
5123
  /**
4829
- * Create a CSV row from baseline, requirement, and target data
5124
+ * Build the lossless hdf.* namespace shared by the export converters: promoted
5125
+ * snake_case scalars plus the full requirement sub-objects preserved verbatim.
5126
+ * `status` is the lossless results roll-up (statusOf().raw); `suppressed` is the
5127
+ * acceptance axis (statusOf().suppressed).
4830
5128
  */
4831
- function createRow(baseline, requirement, target) {
4832
- const description = requirement.descriptions.find((d) => d.label === "default")?.data || "";
4833
- const check = descriptionByLabel(requirement, "check");
4834
- const fix = descriptionByLabel(requirement, "fix");
4835
- const rationale = descriptionByLabel(requirement, "rationale");
4836
- const code = requirement.code ?? "";
4837
- const references = flattenRefs(requirement.refs);
4838
- const severity = getSeverity(requirement);
4839
- const firstResult = requirement.results[0];
4840
- const status = firstResult?.status || "";
4841
- const message = firstResult?.message || "";
4842
- const nistControls = extractArrayFromTags(requirement.tags, "nist");
4843
- const cciControls = extractArrayFromTags(requirement.tags, "cci");
4844
- return {
4845
- "Baseline ID": baseline.name,
4846
- "Baseline Version": baseline.version || "",
4847
- "Baseline Title": baseline.title || "",
4848
- "Target ID": target.name,
4849
- "Target Type": target.type,
4850
- "Requirement ID": requirement.id,
4851
- "Requirement Title": requirement.title || "",
4852
- "Description": description,
4853
- "Check": check,
4854
- "Fix": fix,
4855
- "Rationale": rationale,
4856
- "Code": code,
4857
- "References": references,
4858
- "Severity": severity,
4859
- "Impact": requirement.impact,
4860
- "Status": String(status),
4861
- "NIST Controls": nistControls,
4862
- "CCI Controls": cciControls,
4863
- "Control Type": requirement.controlType ?? "",
4864
- "Verification Method": requirement.verificationMethod ?? "",
4865
- "Applicability": requirement.applicability ?? "",
4866
- "Result Message": message
5129
+ function buildHDFBlock(req, baseline, status, overridden, suppressed, generator, tool, converterVersion) {
5130
+ const hdf = {
5131
+ status,
5132
+ overridden,
5133
+ suppressed,
5134
+ exporter_version: converterVersion
4867
5135
  };
5136
+ setIf(hdf, "control_id", getStr(req, "id"));
5137
+ setIf(hdf, "baseline", getStr(baseline, "name"));
5138
+ if ("effectiveStatus" in req) hdf.effective_status = req.effectiveStatus;
5139
+ if ("effectiveImpact" in req) hdf.effective_impact = req.effectiveImpact;
5140
+ if ("impact" in req) hdf.impact = req.impact;
5141
+ if ("severity" in req) hdf.severity = req.severity;
5142
+ if ("disposition" in req) hdf.disposition = req.disposition;
5143
+ const tags = asMap(req.tags);
5144
+ if (tags?.nist !== void 0) hdf.nist = tags.nist;
5145
+ if (tags?.cci !== void 0) hdf.cci = tags.cci;
5146
+ for (const [src, dst] of Object.entries({
5147
+ tags: "tags",
5148
+ cvss: "cvss",
5149
+ cwe: "cwe",
5150
+ epss: "epss",
5151
+ kev: "kev",
5152
+ affectedPackages: "affected_packages",
5153
+ descriptions: "descriptions",
5154
+ results: "results",
5155
+ statusOverrides: "status_overrides",
5156
+ poams: "poams",
5157
+ code: "code",
5158
+ refs: "refs"
5159
+ })) if (src in req) hdf[dst] = req[src];
5160
+ if (generator) hdf.generator = generator;
5161
+ if (tool) hdf.tool = tool;
5162
+ return hdf;
4868
5163
  }
4869
5164
  /**
4870
- * Get severity from requirement tags or derive from impact
5165
+ * A numeric value that must serialize with an explicit decimal point, so a
5166
+ * whole-number renders as `10.0` rather than the integer `10`. Some consumers
5167
+ * type-check strictly (OCSF's `float_t` rejects an integer-shaped token).
5168
+ * JSON.stringify cannot emit `10.0` for a JS number, so the value is carried in
5169
+ * this wrapper (passed through canonicalize untouched) and rendered as a bare
5170
+ * numeric token by stringifyLine. Go's counterpart is encoding/json's json.Number.
4871
5171
  */
4872
- function getSeverity(requirement) {
4873
- if (requirement.tags && typeof requirement.tags === "object") {
4874
- if ("severity" in requirement.tags) {
4875
- const sev = requirement.tags.severity;
4876
- if (typeof sev === "string") return sev;
4877
- if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
4878
- }
5172
+ var RawNumber = class {
5173
+ constructor(token) {
5174
+ this.token = token;
4879
5175
  }
4880
- const impact = requirement.impact;
4881
- if (impact >= .7) return "high";
4882
- if (impact >= .4) return "medium";
4883
- return "low";
4884
- }
4885
- /**
4886
- * Extract array values from tags object and join with semicolons
4887
- */
4888
- function extractArrayFromTags(tags, key) {
4889
- if (!tags || typeof tags !== "object") return "";
4890
- const value = tags[key];
4891
- if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
4892
- return "";
5176
+ };
5177
+ /** Wrap a number as a RawNumber whose token always bears a decimal point. */
5178
+ function floatNumber(f) {
5179
+ let s = String(f);
5180
+ if (!/[.eE]/.test(s)) s += ".0";
5181
+ return new RawNumber(s);
4893
5182
  }
5183
+ const RAWNUM_MARK = String.fromCharCode(1);
5184
+ const RAWNUM_RE = /"\\u0001(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\\u0001"/g;
4894
5185
  /**
4895
- * Get the data of the first description matching a label. Empty when absent.
5186
+ * Compare two strings by Unicode code point. This matches Go's `encoding/json`
5187
+ * key ordering, which is a bytewise comparison of the UTF-8 encoding \u2014 and UTF-8
5188
+ * byte order is identical to code-point order. JS's default Array.sort() instead
5189
+ * compares UTF-16 code units, which diverges for supplementary-plane (non-BMP)
5190
+ * characters: a lead surrogate (0xD800\u20130xDBFF) sorts before a BMP char in
5191
+ * [0xE000,0xFFFF] under UTF-16, but a non-BMP code point (\u22650x10000) sorts after
5192
+ * it under UTF-8. Iterating the string yields whole code points (surrogate pairs
5193
+ * combined), so this comparator is correct for the full Unicode range.
4896
5194
  */
4897
- function descriptionByLabel(requirement, label) {
4898
- return requirement.descriptions.find((d) => d.label === label)?.data || "";
5195
+ function byCodePoint(a, b) {
5196
+ const ai = [...a];
5197
+ const bi = [...b];
5198
+ const n = Math.min(ai.length, bi.length);
5199
+ for (let i = 0; i < n; i++) {
5200
+ const ca = ai[i]?.codePointAt(0) ?? 0;
5201
+ const cb = bi[i]?.codePointAt(0) ?? 0;
5202
+ if (ca !== cb) return ca - cb;
5203
+ }
5204
+ return ai.length - bi.length;
4899
5205
  }
4900
5206
  /**
4901
- * Flatten a requirement's refs to one string: each Reference rendered as its
4902
- * url/uri (or a string `ref`); array-form refs are skipped. Joined with '; ' to
4903
- * match the NIST/CCI column convention.
4904
- */
4905
- function flattenRefs(refs) {
4906
- if (!Array.isArray(refs)) return "";
4907
- const out = [];
4908
- for (const r of refs) {
4909
- if (!r || typeof r !== "object") continue;
4910
- const o = r;
4911
- if (typeof o.url === "string") out.push(o.url);
4912
- else if (typeof o.uri === "string") out.push(o.uri);
4913
- else if (typeof o.ref === "string") out.push(o.ref);
4914
- }
4915
- return out.join("; ");
4916
- }
4917
- //#endregion
4918
- //#region shared/typescript/exportmap.ts
4919
- /**
4920
- * Generic, source-tool-agnostic mapping helpers shared by the HDF export
4921
- * converters (hdf-to-ecs, hdf-to-splunk, ...).
4922
- *
4923
- * The export converters deliberately operate on generically-parsed JSON rather
4924
- * than typed HDF structs, so their output can be held byte-identical with the
4925
- * Go implementations. This module centralizes the generic accessors, the status
4926
- * roll-up, the requirement/document field extraction, and the canonical
4927
- * (key-sorted, HTML-unescaped) line serialization. Target-specific event shaping
4928
- * (ECS field names, CIM field names, envelopes) stays in each converter.
4929
- */
4930
- function asMap(v) {
4931
- return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
4932
- }
4933
- function asArr(v) {
4934
- return Array.isArray(v) ? v : void 0;
4935
- }
4936
- function getStr(m, key) {
4937
- const v = m?.[key];
4938
- return typeof v === "string" ? v : "";
4939
- }
4940
- function setIf(m, key, val) {
4941
- if (val !== "") m[key] = val;
4942
- }
4943
- function stringSlice(v) {
4944
- if (typeof v === "string") return [v];
4945
- if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
4946
- return [];
4947
- }
4948
- /** Most-significant status across a requirement's results[] (lossless). */
4949
- function worstOfResults(req) {
4950
- const results = asArr(req.results) ?? [];
4951
- const precedence = [
4952
- "failed",
4953
- "error",
4954
- "passed",
4955
- "notReviewed",
4956
- "notApplicable"
4957
- ];
4958
- const present = /* @__PURE__ */ new Set();
4959
- for (const rRaw of results) {
4960
- const r = asMap(rRaw);
4961
- if (r) present.add(getStr(r, "status"));
4962
- }
4963
- for (const s of precedence) if (present.has(s)) return s;
4964
- return "notReviewed";
4965
- }
4966
- /**
4967
- * Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
4968
- * 'error' is indeterminate (not a compliance failure) and every other value is
4969
- * non-failing. Single shared definition of "failing" for the suppression axis
4970
- * and the per-exporter outcome maps.
4971
- */
4972
- function isFailing(status) {
4973
- return status === "failed";
4974
- }
4975
- /** Resolve the status context for a requirement. */
4976
- function statusOf(req) {
4977
- const raw = worstOfResults(req);
4978
- const effective = getStr(req, "effectiveStatus");
4979
- const overrides = asArr(req.statusOverrides);
4980
- const rollup = effective !== "" ? effective : raw;
4981
- return {
4982
- raw,
4983
- effective,
4984
- rollup,
4985
- overridden: overrides !== void 0 && overrides.length > 0 || "effectiveStatus" in req,
4986
- suppressed: isFailing(raw) && !isFailing(rollup)
4987
- };
4988
- }
4989
- function firstComponent(doc) {
4990
- const comps = asArr(doc.components);
4991
- if (!comps || comps.length === 0) return void 0;
4992
- return asMap(comps[0]);
4993
- }
4994
- function firstResultStartTime(req, fallback) {
4995
- const results = asArr(req.results) ?? [];
4996
- if (results.length > 0) {
4997
- const st = getStr(asMap(results[0]), "startTime");
4998
- if (st !== "") return st;
4999
- }
5000
- return fallback;
5001
- }
5002
- function defaultDescription(req) {
5003
- const descs = asArr(req.descriptions) ?? [];
5004
- for (const dRaw of descs) {
5005
- const d = asMap(dRaw);
5006
- if (d && getStr(d, "label") === "default") return getStr(d, "data");
5007
- }
5008
- return "";
5009
- }
5010
- function firstRefURL(req) {
5011
- const refs = asArr(req.refs) ?? [];
5012
- for (const rRaw of refs) {
5013
- const url = getStr(asMap(rRaw), "url");
5014
- if (url !== "") return url;
5015
- }
5016
- return "";
5017
- }
5018
- /** Deterministic event id: component | baseline | control. */
5019
- function eventID(component, baselineName, controlID) {
5020
- let comp = "";
5021
- if (component) comp = getStr(component, "componentId") || getStr(component, "name");
5022
- return [
5023
- comp,
5024
- baselineName,
5025
- controlID
5026
- ].join("|");
5027
- }
5028
- /**
5029
- * Build the lossless hdf.* namespace shared by the export converters: promoted
5030
- * snake_case scalars plus the full requirement sub-objects preserved verbatim.
5031
- * `status` is the lossless results roll-up (statusOf().raw); `suppressed` is the
5032
- * acceptance axis (statusOf().suppressed).
5033
- */
5034
- function buildHDFBlock(req, baseline, status, overridden, suppressed, generator, tool, converterVersion) {
5035
- const hdf = {
5036
- status,
5037
- overridden,
5038
- suppressed,
5039
- exporter_version: converterVersion
5040
- };
5041
- setIf(hdf, "control_id", getStr(req, "id"));
5042
- setIf(hdf, "baseline", getStr(baseline, "name"));
5043
- if ("effectiveStatus" in req) hdf.effective_status = req.effectiveStatus;
5044
- if ("effectiveImpact" in req) hdf.effective_impact = req.effectiveImpact;
5045
- if ("impact" in req) hdf.impact = req.impact;
5046
- if ("severity" in req) hdf.severity = req.severity;
5047
- if ("disposition" in req) hdf.disposition = req.disposition;
5048
- const tags = asMap(req.tags);
5049
- if (tags?.nist !== void 0) hdf.nist = tags.nist;
5050
- if (tags?.cci !== void 0) hdf.cci = tags.cci;
5051
- for (const [src, dst] of Object.entries({
5052
- tags: "tags",
5053
- cvss: "cvss",
5054
- cwe: "cwe",
5055
- epss: "epss",
5056
- kev: "kev",
5057
- affectedPackages: "affected_packages",
5058
- descriptions: "descriptions",
5059
- results: "results",
5060
- statusOverrides: "status_overrides",
5061
- poams: "poams",
5062
- code: "code",
5063
- refs: "refs"
5064
- })) if (src in req) hdf[dst] = req[src];
5065
- if (generator) hdf.generator = generator;
5066
- if (tool) hdf.tool = tool;
5067
- return hdf;
5068
- }
5069
- /**
5070
- * A numeric value that must serialize with an explicit decimal point, so a
5071
- * whole-number renders as `10.0` rather than the integer `10`. Some consumers
5072
- * type-check strictly (OCSF's `float_t` rejects an integer-shaped token).
5073
- * JSON.stringify cannot emit `10.0` for a JS number, so the value is carried in
5074
- * this wrapper (passed through canonicalize untouched) and rendered as a bare
5075
- * numeric token by stringifyLine. Go's counterpart is encoding/json's json.Number.
5076
- */
5077
- var RawNumber = class {
5078
- constructor(token) {
5079
- this.token = token;
5080
- }
5081
- };
5082
- /** Wrap a number as a RawNumber whose token always bears a decimal point. */
5083
- function floatNumber(f) {
5084
- let s = String(f);
5085
- if (!/[.eE]/.test(s)) s += ".0";
5086
- return new RawNumber(s);
5087
- }
5088
- const RAWNUM_MARK = String.fromCharCode(1);
5089
- const RAWNUM_RE = /"\\u0001(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\\u0001"/g;
5090
- /**
5091
- * Compare two strings by Unicode code point. This matches Go's `encoding/json`
5092
- * key ordering, which is a bytewise comparison of the UTF-8 encoding \u2014 and UTF-8
5093
- * byte order is identical to code-point order. JS's default Array.sort() instead
5094
- * compares UTF-16 code units, which diverges for supplementary-plane (non-BMP)
5095
- * characters: a lead surrogate (0xD800\u20130xDBFF) sorts before a BMP char in
5096
- * [0xE000,0xFFFF] under UTF-16, but a non-BMP code point (\u22650x10000) sorts after
5097
- * it under UTF-8. Iterating the string yields whole code points (surrogate pairs
5098
- * combined), so this comparator is correct for the full Unicode range.
5099
- */
5100
- function byCodePoint(a, b) {
5101
- const ai = [...a];
5102
- const bi = [...b];
5103
- const n = Math.min(ai.length, bi.length);
5104
- for (let i = 0; i < n; i++) {
5105
- const ca = ai[i]?.codePointAt(0) ?? 0;
5106
- const cb = bi[i]?.codePointAt(0) ?? 0;
5107
- if (ca !== cb) return ca - cb;
5108
- }
5109
- return ai.length - bi.length;
5110
- }
5111
- /**
5112
- * Recursively sort object keys in Go's map-key order (bytewise UTF-8 / code
5113
- * point) so the emitted JSON is byte-identical to Go's encoder.
5207
+ * Recursively sort object keys in Go's map-key order (bytewise UTF-8 / code
5208
+ * point) so the emitted JSON is byte-identical to Go's encoder.
5114
5209
  */
5115
5210
  function canonicalize$1(v) {
5116
5211
  if (v instanceof RawNumber) return v;
@@ -5194,37 +5289,306 @@ function epochMillis(s) {
5194
5289
  return d.getTime();
5195
5290
  }
5196
5291
  //#endregion
5197
- //#region converters/hdf-to-ecs/typescript/converter.ts
5292
+ //#region converters/cyclonedx-to-hdf/typescript/converter.ts
5293
+ const CVSS_METHODS = /* @__PURE__ */ new Set([
5294
+ "CVSSv2",
5295
+ "CVSSv3",
5296
+ "CVSSv31",
5297
+ "CVSSv4"
5298
+ ]);
5198
5299
  /**
5199
- * HDF Results -> Elastic Common Schema (ECS) NDJSON exporter.
5200
- *
5201
- * One ECS event per Evaluated_Requirement, in the hybrid shape from ADR-0002:
5202
- * a core-ECS-native projection plus a lossless hdf.* block, hot filter scalars
5203
- * promoted flat. Output is plain NDJSON (LF-delimited, trailing newline), ECS
5204
- * 9.4.0.
5205
- *
5206
- * Status is raw-primary: event.outcome carries the RAW verdict (a waived failure
5207
- * is still failure), and hdf.suppressed is the separate acceptance axis. The
5208
- * canonical "still actionable" query is
5209
- * event.outcome:"failure" AND hdf.suppressed:false.
5210
- *
5211
- * Generic JSON access, the status roll-up, requirement/document field
5212
- * extraction, and the canonical line serialization are shared with the other
5213
- * export converters via ../../../shared/typescript/exportmap.js; only
5214
- * ECS-specific event shaping lives here. Output is held byte-identical with the
5215
- * Go implementation via key-sorted, HTML-unescaped compact JSON.
5300
+ * Computes the maximum impact across all ratings for a vulnerability.
5301
+ * Prefers CVSS score/10 when available, falls back to severityToImpact().
5216
5302
  */
5217
- const ECS_VERSION = "9.4.0";
5218
- function convertHdfToEcs(input, converterVersion = "0.1.0") {
5219
- return runExport(input, "hdf-to-ecs", (req, baseline, docTimestamp, tool, generator, component) => buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5303
+ function maxImpact(ratings) {
5304
+ if (ratings.length === 0) return .5;
5305
+ let max = 0;
5306
+ for (const rating of ratings) {
5307
+ let impact;
5308
+ if (rating.method && CVSS_METHODS.has(rating.method) && rating.score !== void 0 && rating.score !== null) impact = rating.score / 10;
5309
+ else impact = severityToImpact$1(rating.severity ?? "medium");
5310
+ if (impact > max) max = impact;
5311
+ }
5312
+ return max;
5220
5313
  }
5221
- function buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5222
- const st = statusOf(req);
5223
- const outcome = statusToOutcome(st.raw);
5224
- const controlID = getStr(req, "id");
5225
- const baselineName = getStr(baseline, "name");
5226
- const title = getStr(req, "title");
5227
- const cvssList = asArr(req.cvss);
5314
+ /**
5315
+ * Formats the ratings as a human-readable tag string.
5316
+ */
5317
+ function formatRatingsTag(ratings) {
5318
+ return ratings.map((r) => `${r.source?.name ?? "Unknown"} - ${r.severity ?? "unrated"}`).join(", ");
5319
+ }
5320
+ /**
5321
+ * Formats a component reference as a code_desc string.
5322
+ */
5323
+ function formatCodeDesc$4(componentLookup, ref) {
5324
+ const comp = componentLookup.get(ref);
5325
+ if (!comp) return `Component ${ref} is vulnerable`;
5326
+ let name = "";
5327
+ if (comp.group) name += comp.group + "/";
5328
+ name += comp.name;
5329
+ if (comp.version) name += "@" + comp.version;
5330
+ return `Component ${name} is vulnerable`;
5331
+ }
5332
+ /**
5333
+ * Flattens nested CycloneDX components into a single array.
5334
+ */
5335
+ function flattenComponents(components) {
5336
+ const result = [];
5337
+ for (const comp of components) {
5338
+ result.push(comp);
5339
+ if (comp.components) result.push(...flattenComponents(comp.components));
5340
+ }
5341
+ return result;
5342
+ }
5343
+ /**
5344
+ * Reports whether any (possibly nested) component is a machine-learning-model,
5345
+ * i.e. the CycloneDX document is an AI-BOM.
5346
+ */
5347
+ function hasMLModelComponent(components) {
5348
+ return flattenComponents(components).some((comp) => comp.type === "machine-learning-model");
5349
+ }
5350
+ /**
5351
+ * Converts CycloneDX SBOM/VEX JSON to HDF format.
5352
+ *
5353
+ * @param input - CycloneDX JSON string
5354
+ * @returns HDF JSON string
5355
+ */
5356
+ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
5357
+ if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
5358
+ validateInputSize(input, "cyclonedx");
5359
+ const bom = parseJSON(input);
5360
+ if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
5361
+ if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
5362
+ if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
5363
+ if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) {
5364
+ if (hasMLModelComponent(bom.components ?? [])) throw new Error("cyclonedx: this file is a CycloneDX AI-BOM (machine-learning-model inventory) with no vulnerabilities; to import it into a system document, use:\n hdf system create <file> --from cyclonedx-mlbom");
5365
+ throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create <sbom-file> --component-name <name>");
5366
+ }
5367
+ const resultsChecksum = await inputChecksum(input);
5368
+ const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
5369
+ const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
5370
+ const allComponents = flattenComponents(bom.components ?? []);
5371
+ const componentLookup = /* @__PURE__ */ new Map();
5372
+ for (const comp of allComponents) if (comp["bom-ref"]) componentLookup.set(comp["bom-ref"], comp);
5373
+ const requirements = [];
5374
+ const { items: limitedVulns, truncated: truncatedVulns } = limitArray(bom.vulnerabilities ?? []);
5375
+ /* v8 ignore next -- truncation only triggers with >100K items */
5376
+ if (truncatedVulns) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${(bom.vulnerabilities ?? []).length})`);
5377
+ for (const vuln of limitedVulns) {
5378
+ const ratings = vuln.ratings ?? [];
5379
+ const impact = maxImpact(ratings);
5380
+ const cwes = vuln.cwes ?? [];
5381
+ const nist = mapCWEToNIST(cwes.map((c) => `${c}`), DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
5382
+ const tags = {
5383
+ nist,
5384
+ cci: nistToCci(nist)
5385
+ };
5386
+ if (cwes.length > 0) tags["cweid"] = cwes.map((c) => `CWE-${c}`);
5387
+ if (ratings.length > 0) tags["ratings"] = formatRatingsTag(ratings);
5388
+ const descriptions = [];
5389
+ const defaultParts = [];
5390
+ if (vuln.description) defaultParts.push(`Description: ${vuln.description}`);
5391
+ if (vuln.detail) defaultParts.push(`Detail: ${vuln.detail}`);
5392
+ if (defaultParts.length > 0) descriptions.push({
5393
+ label: "default",
5394
+ data: defaultParts.join("\n\n")
5395
+ });
5396
+ else descriptions.push({
5397
+ label: "default",
5398
+ data: vuln.id
5399
+ });
5400
+ const fixParts = [];
5401
+ if (vuln.recommendation) fixParts.push(vuln.recommendation);
5402
+ if (vuln.analysis?.detail) fixParts.push(`Workaround: ${vuln.analysis.detail}`);
5403
+ if (fixParts.length > 0) descriptions.push({
5404
+ label: "fix",
5405
+ data: fixParts.join("\n\n")
5406
+ });
5407
+ const affects = vuln.affects ?? [];
5408
+ const toResult = (codeDesc) => createResult(ResultStatus.Failed, void 0, {
5409
+ codeDesc,
5410
+ startTime: scanTime
5411
+ });
5412
+ const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
5413
+ const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
5414
+ const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
5415
+ const controlType = deriveControlTypeFromTags(nist);
5416
+ if (controlType !== void 0) req.controlType = controlType;
5417
+ requirements.push(req);
5418
+ }
5419
+ const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
5420
+ const targetComponent = {
5421
+ name: bom.metadata?.component?.name ?? "",
5422
+ type: TargetType.Application
5423
+ };
5424
+ if (bom.metadata?.component?.version) targetComponent.version = bom.metadata.component.version;
5425
+ const parsedBom = parseBom(input);
5426
+ const bomParts = {
5427
+ bomType: BOMType.Sbom,
5428
+ format: "cyclonedx",
5429
+ uniqueId: parsedBom.normalized.uniqueId,
5430
+ document: canonicalize$1(JSON.parse(input))
5431
+ };
5432
+ if (parsedBom.normalized.packages && parsedBom.normalized.packages.length > 0) bomParts.packages = parsedBom.normalized.packages;
5433
+ const componentBom = buildBom(bomParts);
5434
+ return buildHdfResults({
5435
+ generatorName: "cyclonedx-to-hdf",
5436
+ converterVersion,
5437
+ toolName: "CycloneDX",
5438
+ toolFormat: "JSON",
5439
+ baselines: [baseline],
5440
+ components: [{
5441
+ ...targetComponent,
5442
+ boms: [componentBom]
5443
+ }],
5444
+ timestamp: scanTime
5445
+ });
5446
+ }
5447
+ //#endregion
5448
+ //#region converters/hdf-to-csv/typescript/converter.ts
5449
+ /**
5450
+ * Convert HDF JSON to CSV format
5451
+ * @param input HDF JSON string
5452
+ * @returns CSV string with sanitized output
5453
+ */
5454
+ function convertHdfToCsv(input) {
5455
+ validateInputSize(input, "hdf-to-csv");
5456
+ const hdf = parseHdf(input);
5457
+ if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
5458
+ if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
5459
+ const rows = [];
5460
+ const components = hdf.components || [];
5461
+ const targetList = components.length > 0 ? components.map((t) => ({
5462
+ name: t.name,
5463
+ type: t.type
5464
+ })) : [{
5465
+ name: "",
5466
+ type: ""
5467
+ }];
5468
+ for (const baseline of hdf.baselines) for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
5469
+ return buildCsv(rows, { sanitize: true });
5470
+ }
5471
+ /**
5472
+ * Create a CSV row from baseline, requirement, and target data
5473
+ */
5474
+ function createRow(baseline, requirement, target) {
5475
+ const description = requirement.descriptions.find((d) => d.label === "default")?.data || "";
5476
+ const check = descriptionByLabel(requirement, "check");
5477
+ const fix = descriptionByLabel(requirement, "fix");
5478
+ const rationale = descriptionByLabel(requirement, "rationale");
5479
+ const code = requirement.code ?? "";
5480
+ const references = flattenRefs(requirement.refs);
5481
+ const severity = getSeverity(requirement);
5482
+ const firstResult = requirement.results[0];
5483
+ const status = firstResult?.status || "";
5484
+ const message = firstResult?.message || "";
5485
+ const nistControls = extractArrayFromTags(requirement.tags, "nist");
5486
+ const cciControls = extractArrayFromTags(requirement.tags, "cci");
5487
+ return {
5488
+ "Baseline ID": baseline.name,
5489
+ "Baseline Version": baseline.version || "",
5490
+ "Baseline Title": baseline.title || "",
5491
+ "Target ID": target.name,
5492
+ "Target Type": target.type,
5493
+ "Requirement ID": requirement.id,
5494
+ "Requirement Title": requirement.title || "",
5495
+ "Description": description,
5496
+ "Check": check,
5497
+ "Fix": fix,
5498
+ "Rationale": rationale,
5499
+ "Code": code,
5500
+ "References": references,
5501
+ "Severity": severity,
5502
+ "Impact": requirement.impact,
5503
+ "Status": String(status),
5504
+ "NIST Controls": nistControls,
5505
+ "CCI Controls": cciControls,
5506
+ "Control Type": requirement.controlType ?? "",
5507
+ "Verification Method": requirement.verificationMethod ?? "",
5508
+ "Applicability": requirement.applicability ?? "",
5509
+ "Result Message": message
5510
+ };
5511
+ }
5512
+ /**
5513
+ * Get severity from requirement tags or derive from impact
5514
+ */
5515
+ function getSeverity(requirement) {
5516
+ if (requirement.tags && typeof requirement.tags === "object") {
5517
+ if ("severity" in requirement.tags) {
5518
+ const sev = requirement.tags.severity;
5519
+ if (typeof sev === "string") return sev;
5520
+ if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
5521
+ }
5522
+ }
5523
+ const impact = requirement.impact;
5524
+ if (impact >= .7) return "high";
5525
+ if (impact >= .4) return "medium";
5526
+ return "low";
5527
+ }
5528
+ /**
5529
+ * Extract array values from tags object and join with semicolons
5530
+ */
5531
+ function extractArrayFromTags(tags, key) {
5532
+ if (!tags || typeof tags !== "object") return "";
5533
+ const value = tags[key];
5534
+ if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
5535
+ return "";
5536
+ }
5537
+ /**
5538
+ * Get the data of the first description matching a label. Empty when absent.
5539
+ */
5540
+ function descriptionByLabel(requirement, label) {
5541
+ return requirement.descriptions.find((d) => d.label === label)?.data || "";
5542
+ }
5543
+ /**
5544
+ * Flatten a requirement's refs to one string: each Reference rendered as its
5545
+ * url/uri (or a string `ref`); array-form refs are skipped. Joined with '; ' to
5546
+ * match the NIST/CCI column convention.
5547
+ */
5548
+ function flattenRefs(refs) {
5549
+ if (!Array.isArray(refs)) return "";
5550
+ const out = [];
5551
+ for (const r of refs) {
5552
+ if (!r || typeof r !== "object") continue;
5553
+ const o = r;
5554
+ if (typeof o.url === "string") out.push(o.url);
5555
+ else if (typeof o.uri === "string") out.push(o.uri);
5556
+ else if (typeof o.ref === "string") out.push(o.ref);
5557
+ }
5558
+ return out.join("; ");
5559
+ }
5560
+ //#endregion
5561
+ //#region converters/hdf-to-ecs/typescript/converter.ts
5562
+ /**
5563
+ * HDF Results -> Elastic Common Schema (ECS) NDJSON exporter.
5564
+ *
5565
+ * One ECS event per Evaluated_Requirement, in the hybrid shape from ADR-0002:
5566
+ * a core-ECS-native projection plus a lossless hdf.* block, hot filter scalars
5567
+ * promoted flat. Output is plain NDJSON (LF-delimited, trailing newline), ECS
5568
+ * 9.4.0.
5569
+ *
5570
+ * Status is raw-primary: event.outcome carries the RAW verdict (a waived failure
5571
+ * is still failure), and hdf.suppressed is the separate acceptance axis. The
5572
+ * canonical "still actionable" query is
5573
+ * event.outcome:"failure" AND hdf.suppressed:false.
5574
+ *
5575
+ * Generic JSON access, the status roll-up, requirement/document field
5576
+ * extraction, and the canonical line serialization are shared with the other
5577
+ * export converters via ../../../shared/typescript/exportmap.js; only
5578
+ * ECS-specific event shaping lives here. Output is held byte-identical with the
5579
+ * Go implementation via key-sorted, HTML-unescaped compact JSON.
5580
+ */
5581
+ const ECS_VERSION = "9.4.0";
5582
+ function convertHdfToEcs(input, converterVersion = "0.1.0") {
5583
+ return runExport(input, "hdf-to-ecs", (req, baseline, docTimestamp, tool, generator, component) => buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5584
+ }
5585
+ function buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5586
+ const st = statusOf(req);
5587
+ const outcome = statusToOutcome(st.raw);
5588
+ const controlID = getStr(req, "id");
5589
+ const baselineName = getStr(baseline, "name");
5590
+ const title = getStr(req, "title");
5591
+ const cvssList = asArr(req.cvss);
5228
5592
  const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
5229
5593
  const categories = ["configuration"];
5230
5594
  if (hasCVSS) categories.push("vulnerability");
@@ -5383,7 +5747,7 @@ function buildHECEvent(req, baseline, docTimestamp, tool, generator, component,
5383
5747
  const controlID = getStr(req, "id");
5384
5748
  const signature = title !== "" ? title : controlID;
5385
5749
  const dest = destHost(component);
5386
- const sev = severity(req);
5750
+ const sev = severity$1(req);
5387
5751
  const [cvss, hasCVSS] = maxCVSS(req);
5388
5752
  const cve = firstCVE(asArr(req.cvss) ?? []);
5389
5753
  const category = firstCWE(req);
@@ -5426,7 +5790,7 @@ function destHost(component) {
5426
5790
  if (!component) return "";
5427
5791
  return getStr(component, "fqdn") || getStr(component, "name") || getStr(component, "ipAddress");
5428
5792
  }
5429
- function severity(req) {
5793
+ function severity$1(req) {
5430
5794
  if (typeof req.impact === "number") return impactToSeverity(req.impact);
5431
5795
  return normalizeSeverity(getStr(req, "severity"));
5432
5796
  }
@@ -5487,9 +5851,9 @@ const ACTIVITY_CREATE = 1;
5487
5851
  const STATUS_NEW = 1;
5488
5852
  const STATUS_SUPPRESSED = 3;
5489
5853
  function convertHdfToOcsf(input, converterVersion = "0.1.0") {
5490
- return runExport(input, "hdf-to-ocsf", (req, baseline, docTimestamp, tool, generator, component) => buildFinding(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5854
+ return runExport(input, "hdf-to-ocsf", (req, baseline, docTimestamp, tool, generator, component) => buildFinding$1(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5491
5855
  }
5492
- function buildFinding(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5856
+ function buildFinding$1(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5493
5857
  const st = statusOf(req);
5494
5858
  const cvssList = asArr(req.cvss);
5495
5859
  const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
@@ -5697,6 +6061,174 @@ function buildVulnerabilities(cvssList, req) {
5697
6061
  return [vuln];
5698
6062
  }
5699
6063
  //#endregion
6064
+ //#region converters/hdf-to-asff/typescript/converter.ts
6065
+ /**
6066
+ * HDF Results -> AWS Security Finding Format (ASFF), the reverse of
6067
+ * asff-to-hdf. Output is the standard {"Findings": [...]} envelope that
6068
+ * BatchImportFindings accepts and asff-to-hdf reads back.
6069
+ *
6070
+ * One finding per requirement (matching hdf-to-ocsf): a requirement's results
6071
+ * roll up to one Compliance.Status via the shared statusOf. The mapping is
6072
+ * deliberately LOSSY and standard-compliant — HDF structure ASFF cannot hold is
6073
+ * dropped, NOT crammed into Types[] (heimdall2's rejected Types-string
6074
+ * encoding). Provenance rides ProductFields (ASFF's official string map).
6075
+ *
6076
+ * ProductArn / AwsAccountId / Region are ASFF-required but absent from HDF: the
6077
+ * account is recovered from a cloudAccount component when present, otherwise a
6078
+ * placeholder is emitted; the push path overrides all three. Output is held
6079
+ * byte-identical with the Go implementation via canonicalize + stringifyLine.
6080
+ */
6081
+ const ASFF_SCHEMA_VERSION = "2018-10-08";
6082
+ const PLACEHOLDER_REGION = "us-east-1";
6083
+ const PLACEHOLDER_ACCOUNT_ID = "000000000000";
6084
+ const DEFAULT_PARTITION = "aws";
6085
+ const MAX_TITLE = 256;
6086
+ const MAX_DESCRIPTION = 1024;
6087
+ const EPOCH_SENTINEL = "1970-01-01T00:00:00Z";
6088
+ function convertHdfToAsff(input, converterVersion = "0.1.0") {
6089
+ const name = "hdf-to-asff";
6090
+ validateInputSize(input, name);
6091
+ const doc = parseHdf(input);
6092
+ const baselines = asArr(doc.baselines);
6093
+ if (!baselines) throw new Error(`${name}: invalid HDF structure: missing baselines field`);
6094
+ const docTimestamp = getStr(doc, "timestamp");
6095
+ const component = firstComponent(doc);
6096
+ const accountID = recoverAccountID(doc);
6097
+ const productArn = `arn:aws:securityhub:${PLACEHOLDER_REGION}:${accountID}:product/${accountID}/default`;
6098
+ const findings = [];
6099
+ for (const bRaw of baselines) {
6100
+ const baseline = asMap(bRaw);
6101
+ if (!baseline) continue;
6102
+ const baselineName = getStr(baseline, "name");
6103
+ const reqs = asArr(baseline.requirements) ?? [];
6104
+ for (const rRaw of reqs) {
6105
+ const req = asMap(rRaw);
6106
+ if (!req) continue;
6107
+ findings.push(buildFinding(req, baselineName, docTimestamp, component, accountID, productArn, converterVersion));
6108
+ }
6109
+ }
6110
+ return stringifyLine(canonicalize$1({ Findings: findings })) + "\n";
6111
+ }
6112
+ function recoverAccountID(doc) {
6113
+ for (const cRaw of asArr(doc.components) ?? []) {
6114
+ const c = asMap(cRaw);
6115
+ if (!c || getStr(c, "type") !== "cloudAccount") continue;
6116
+ const id = getStr(c, "accountId") || getStr(c, "name");
6117
+ if (id) return id;
6118
+ }
6119
+ return PLACEHOLDER_ACCOUNT_ID;
6120
+ }
6121
+ function buildFinding(req, baselineName, docTimestamp, component, accountID, productArn, converterVersion) {
6122
+ const controlID = getStr(req, "id");
6123
+ const st = statusOf(req);
6124
+ const title = getStr(req, "title") || controlID;
6125
+ const desc = defaultDescription(req) || title;
6126
+ const cvssList = asArr(req.cvss);
6127
+ const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
6128
+ const ts = canonicalTime(firstResultStartTime(req, docTimestamp));
6129
+ const id = findingID(accountID, baselineName, controlID);
6130
+ const finding = {
6131
+ SchemaVersion: ASFF_SCHEMA_VERSION,
6132
+ Id: id,
6133
+ ProductArn: productArn,
6134
+ GeneratorId: controlID,
6135
+ AwsAccountId: accountID,
6136
+ CreatedAt: ts,
6137
+ UpdatedAt: ts,
6138
+ Title: truncate(title, MAX_TITLE),
6139
+ Description: truncate(desc, MAX_DESCRIPTION),
6140
+ Types: asffTypes(hasCVSS),
6141
+ Severity: severity(req),
6142
+ Resources: resources(component, id),
6143
+ RecordState: "ACTIVE",
6144
+ Compliance: { Status: complianceStatus(st.rollup) }
6145
+ };
6146
+ if (st.suppressed) finding.Workflow = { Status: "SUPPRESSED" };
6147
+ const pf = productFields(baselineName, controlID, converterVersion);
6148
+ if (Object.keys(pf).length > 0) finding.ProductFields = pf;
6149
+ const rem = remediation(req);
6150
+ if (rem) finding.Remediation = rem;
6151
+ return finding;
6152
+ }
6153
+ function findingID(accountID, baselineName, controlID) {
6154
+ return [
6155
+ accountID,
6156
+ PLACEHOLDER_REGION,
6157
+ baselineName,
6158
+ controlID
6159
+ ].join("/");
6160
+ }
6161
+ function asffTypes(hasCVSS) {
6162
+ return hasCVSS ? ["Software and Configuration Checks/Vulnerabilities/CVE"] : ["Software and Configuration Checks"];
6163
+ }
6164
+ function complianceStatus(status) {
6165
+ switch (status) {
6166
+ case "passed": return "PASSED";
6167
+ case "failed": return "FAILED";
6168
+ case "notApplicable": return "NOT_AVAILABLE";
6169
+ default: return "WARNING";
6170
+ }
6171
+ }
6172
+ function severity(req) {
6173
+ let label = "INFORMATIONAL";
6174
+ let normalized = 0;
6175
+ if (typeof req.impact === "number") {
6176
+ label = impactToSeverity(req.impact).toUpperCase();
6177
+ normalized = Math.trunc(req.impact * 100);
6178
+ }
6179
+ return {
6180
+ Label: label,
6181
+ Normalized: normalized
6182
+ };
6183
+ }
6184
+ function resources(component, id) {
6185
+ const res = {
6186
+ Type: "Other",
6187
+ Id: id,
6188
+ Partition: DEFAULT_PARTITION,
6189
+ Region: PLACEHOLDER_REGION
6190
+ };
6191
+ if (component) {
6192
+ const details = {};
6193
+ setIf(details, "Name", getStr(component, "name"));
6194
+ setIf(details, "Type", getStr(component, "type"));
6195
+ setIf(details, "IpAddress", getStr(component, "ipAddress"));
6196
+ setIf(details, "OsName", getStr(component, "osName"));
6197
+ if (Object.keys(details).length > 0) res.Details = { Other: details };
6198
+ }
6199
+ return [res];
6200
+ }
6201
+ function productFields(baselineName, controlID, converterVersion) {
6202
+ const pf = {};
6203
+ setIf(pf, "hdf/baseline", baselineName);
6204
+ setIf(pf, "hdf/control_id", controlID);
6205
+ setIf(pf, "hdf/exporter_version", converterVersion);
6206
+ return pf;
6207
+ }
6208
+ function remediation(req) {
6209
+ let fix = "";
6210
+ for (const dRaw of asArr(req.descriptions) ?? []) {
6211
+ const d = asMap(dRaw);
6212
+ if (d && getStr(d, "label") === "fix") {
6213
+ fix = getStr(d, "data");
6214
+ break;
6215
+ }
6216
+ }
6217
+ const url = firstRefURL(req);
6218
+ if (fix === "" && url === "") return void 0;
6219
+ const rec = {};
6220
+ setIf(rec, "Text", truncate(fix, MAX_DESCRIPTION));
6221
+ setIf(rec, "Url", url);
6222
+ return { Recommendation: rec };
6223
+ }
6224
+ function canonicalTime(s) {
6225
+ return parseTimestamp(s) ? s : EPOCH_SENTINEL;
6226
+ }
6227
+ function truncate(s, max) {
6228
+ const r = [...s];
6229
+ return r.length <= max ? s : r.slice(0, max).join("");
6230
+ }
6231
+ //#endregion
5700
6232
  //#region converters/splunk-to-hdf/typescript/converter.ts
5701
6233
  /**
5702
6234
  * Map a Splunk result status string to the HDF ResultStatus enum.
@@ -6562,11 +7094,10 @@ function buildRequirement$10(checkID, findings, hasStatus) {
6562
7094
  data: formatDesc$1(rep)
6563
7095
  }];
6564
7096
  const results = findings.map((f) => {
6565
- return {
6566
- status: hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed,
7097
+ return createResult(hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed, void 0, {
6567
7098
  codeDesc: f["Details"] ?? "",
6568
7099
  startTime: parseDate(f["Date"] ?? "")
6569
- };
7100
+ });
6570
7101
  });
6571
7102
  const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
6572
7103
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6832,11 +7363,10 @@ function buildRequirement$9(vuln, packageTypes, distro) {
6832
7363
  data: vuln.description
6833
7364
  }];
6834
7365
  const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6835
- const results = [{
6836
- status: ResultStatus.Failed,
7366
+ const results = [createResult(ResultStatus.Failed, void 0, {
6837
7367
  codeDesc: formatCodeDesc$2(vuln),
6838
7368
  startTime
6839
- }];
7369
+ })];
6840
7370
  const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
6841
7371
  const controlType = deriveControlTypeFromTags(nist);
6842
7372
  if (controlType !== void 0) req.controlType = controlType;
@@ -6963,11 +7493,10 @@ function buildRequirement$8(finding, timestamp) {
6963
7493
  data: finding.vulnerability.recommendation
6964
7494
  });
6965
7495
  const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
6966
- const results = [{
6967
- status: ResultStatus.Failed,
7496
+ const results = [createResult(ResultStatus.Failed, void 0, {
6968
7497
  codeDesc,
6969
7498
  startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
6970
- }];
7499
+ })];
6971
7500
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
6972
7501
  req.verificationMethod = VerificationMethodEnum.Automated;
6973
7502
  const controlType = deriveControlTypeFromTags(nist);
@@ -8098,8 +8627,7 @@ function buildRequirementFromResult(result, filename) {
8098
8627
  const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
8099
8628
  const score = result.result.score;
8100
8629
  const status = determineStatus(score);
8101
- const toResult = (codeDesc) => ({
8102
- status,
8630
+ const toResult = (codeDesc) => createResult(status, void 0, {
8103
8631
  codeDesc,
8104
8632
  startTime
8105
8633
  });
@@ -8344,8 +8872,7 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
8344
8872
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
8345
8873
  const results = cwes.flatMap((c) => {
8346
8874
  const staticflaws = c.staticflaws;
8347
- return ensureArray(staticflaws?.flaw).map((flaw) => ({
8348
- status: ResultStatus.Failed,
8875
+ return ensureArray(staticflaws?.flaw).map((flaw) => createResult(ResultStatus.Failed, void 0, {
8349
8876
  codeDesc: formatFlawCodeDesc(flaw),
8350
8877
  startTime
8351
8878
  }));
@@ -8407,8 +8934,7 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
8407
8934
  if (cweID) extras.cwe = cweID;
8408
8935
  const tags = buildNistCciTags(nist, cciTags, extras);
8409
8936
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
8410
- const results = components.map((comp) => ({
8411
- status: ResultStatus.Failed,
8937
+ const results = components.map((comp) => createResult(ResultStatus.Failed, void 0, {
8412
8938
  codeDesc: formatSCACodeDesc(comp),
8413
8939
  startTime
8414
8940
  }));
@@ -9720,23 +10246,31 @@ function overrideToPOAMItem(override) {
9720
10246
  const remediations = [];
9721
10247
  if (override.milestones) for (const ms of override.milestones) {
9722
10248
  const msProps = [];
9723
- if (ms.estimatedCompletion) {
9724
- const d = toDate(ms.estimatedCompletion);
9725
- if (d) msProps.push({
9726
- name: "estimated-completion",
9727
- value: formatTimestampSeconds(d)
9728
- });
9729
- }
9730
10249
  if (ms.status) msProps.push({
9731
10250
  name: "milestone-status",
9732
10251
  value: String(ms.status)
9733
10252
  });
10253
+ let tasks;
10254
+ const d = ms.estimatedCompletion ? toDate(ms.estimatedCompletion) : void 0;
10255
+ if (d) {
10256
+ const eta = formatTimestampSeconds(d);
10257
+ tasks = [{
10258
+ uuid: crypto.randomUUID(),
10259
+ type: "milestone",
10260
+ title: ms.description,
10261
+ timing: { "within-date-range": {
10262
+ start: eta,
10263
+ end: eta
10264
+ } }
10265
+ }];
10266
+ }
9734
10267
  remediations.push({
9735
10268
  uuid: crypto.randomUUID(),
9736
10269
  lifecycle: "planned",
9737
10270
  title: ms.description,
9738
10271
  description: ms.description,
9739
- props: msProps.length > 0 ? msProps : void 0
10272
+ props: msProps.length > 0 ? msProps : void 0,
10273
+ tasks
9740
10274
  });
9741
10275
  }
9742
10276
  let riskLog;
@@ -9748,12 +10282,14 @@ function overrideToPOAMItem(override) {
9748
10282
  start: formatTimestampSeconds(expiresDate),
9749
10283
  "status-change": riskStatus
9750
10284
  }] };
10285
+ const deadline = expiresDate && expiresDate.getTime() > 0 ? formatTimestampSeconds(expiresDate) : void 0;
9751
10286
  const risk = {
9752
10287
  uuid: riskUUID,
9753
10288
  title: override.requirementId,
9754
10289
  description: override.reason,
9755
10290
  statement: override.reason,
9756
10291
  status: riskStatus,
10292
+ deadline,
9757
10293
  props: riskProps,
9758
10294
  remediations: remediations.length > 0 ? remediations : void 0,
9759
10295
  "risk-log": riskLog
@@ -10152,7 +10688,7 @@ function supplierEvidence(sourceURI, description) {
10152
10688
  * arrives; one year is a defensive default consistent with the
10153
10689
  * no-permanent-amendment rule on Standalone_Override. */
10154
10690
  const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
10155
- async function convertOpenVexToHdf(input, converterVersion) {
10691
+ async function convertOpenVexToHdf(input, converterVersion = "1.0.0") {
10156
10692
  validateInputSize(input, "openvex-to-hdf");
10157
10693
  const doc = parseJSON(input);
10158
10694
  const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
@@ -10244,7 +10780,7 @@ function truncateId(id) {
10244
10780
  * Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
10245
10781
  */
10246
10782
  const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
10247
- async function convertCsafVexToHdf(input, converterVersion) {
10783
+ async function convertCsafVexToHdf(input, converterVersion = "1.0.0") {
10248
10784
  validateInputSize(input, "csaf-vex-to-hdf");
10249
10785
  const doc = parseJSON(input);
10250
10786
  if (doc.document?.category !== "csaf_vex") throw new Error(`csaf-vex-to-hdf: document.category is ${JSON.stringify(doc.document?.category)}; only 'csaf_vex' is supported`);
@@ -10457,7 +10993,7 @@ function overlap(ids, scope) {
10457
10993
  * Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
10458
10994
  */
10459
10995
  const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
10460
- async function convertCyclonedxVexToHdf(input, converterVersion) {
10996
+ async function convertCyclonedxVexToHdf(input, converterVersion = "1.0.0") {
10461
10997
  validateInputSize(input, "cyclonedx-vex-to-hdf");
10462
10998
  const bom = parseJSON(input);
10463
10999
  if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
@@ -11217,245 +11753,91 @@ async function catalogToBaseline(catalog, rawInput) {
11217
11753
  for (const ctrl of group.controls ?? []) {
11218
11754
  const req = controlToBaselineRequirement(ctrl);
11219
11755
  requirements.push(req);
11220
- reqIDs.push(req.id);
11221
- for (const enh of ctrl.controls ?? []) {
11222
- const enhReq = controlToBaselineRequirement(enh);
11223
- requirements.push(enhReq);
11224
- reqIDs.push(enhReq.id);
11225
- }
11226
- }
11227
- if (reqIDs.length > 0) groups.push({
11228
- id: group.id ?? "",
11229
- title: group.title,
11230
- requirements: reqIDs
11231
- });
11232
- }
11233
- for (const ctrl of catalog.controls ?? []) {
11234
- const req = controlToBaselineRequirement(ctrl);
11235
- requirements.push(req);
11236
- for (const enh of ctrl.controls ?? []) {
11237
- const enhReq = controlToBaselineRequirement(enh);
11238
- requirements.push(enhReq);
11239
- }
11240
- }
11241
- return {
11242
- name: catalogBaselineName(catalog),
11243
- title: meta.title,
11244
- version: meta.version,
11245
- status: "loaded",
11246
- integrity,
11247
- requirements,
11248
- groups,
11249
- generator: {
11250
- name: "oscal-catalog-to-hdf",
11251
- version: "1.0.0"
11252
- }
11253
- };
11254
- }
11255
- /** Converts a single OSCAL Control to an HDF BaselineRequirement. */
11256
- function controlToBaselineRequirement(ctrl) {
11257
- const nistTag = controlIdToNistTag(ctrl.id);
11258
- const descriptions = buildCatalogDescriptions(ctrl);
11259
- const tags = buildCatalogTags(ctrl);
11260
- const req = {
11261
- id: nistTag,
11262
- title: ctrl.title,
11263
- impact: .5,
11264
- descriptions,
11265
- tags
11266
- };
11267
- const controlType = deriveControlTypeFromTags([nistTag]);
11268
- if (controlType !== void 0) req.controlType = controlType;
11269
- if (extractPropValue(ctrl.props, "CORE") === "true") req.applicability = Applicability.Required;
11270
- return req;
11271
- }
11272
- /** Creates HDF Description entries from control parts. */
11273
- function buildCatalogDescriptions(ctrl) {
11274
- const descriptions = [];
11275
- const statement = flattenPartsByName(ctrl.parts, "statement");
11276
- descriptions.push({
11277
- label: "default",
11278
- data: statement || ""
11279
- });
11280
- const guidance = flattenPartsByName(ctrl.parts, "guidance");
11281
- if (guidance) descriptions.push({
11282
- label: "rationale",
11283
- data: guidance
11284
- });
11285
- const check = flattenPartsByName(ctrl.parts, "assessment-objective");
11286
- if (check) descriptions.push({
11287
- label: "check",
11288
- data: check
11289
- });
11290
- return descriptions;
11291
- }
11292
- /** Builds the tags map for an OSCAL catalog control. */
11293
- function buildCatalogTags(ctrl) {
11294
- const tags = {};
11295
- tags["nist"] = [controlIdToNistTag(ctrl.id)];
11296
- const label = extractPropValue(ctrl.props, "label");
11297
- if (label !== void 0) tags["label"] = label;
11298
- const sortId = extractPropValue(ctrl.props, "sort-id");
11299
- if (sortId !== void 0) tags["sort-id"] = sortId;
11300
- return tags;
11301
- }
11302
- /** Derives a baseline name from catalog metadata. */
11303
- function catalogBaselineName(catalog) {
11304
- return toKebabCase(catalog.metadata.title, "oscal-catalog");
11305
- }
11306
- //#endregion
11307
- //#region converters/oscal-to-hdf/typescript/converter-profile.ts
11308
- /**
11309
- * OSCAL Profile to HDF Baseline converter.
11310
- *
11311
- * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_profile.go.
11312
- *
11313
- * This implements the "simple resolver" -- it handles:
11314
- * - A single catalog import
11315
- * - include-controls with with-ids filtering
11316
- * - set-parameters from modify section
11317
- * - merge as-is (preserve catalog structure)
11318
- *
11319
- * It does NOT handle:
11320
- * - Multiple imports
11321
- * - Nested profile imports
11322
- * - alter directives
11323
- * - Complex merge strategies
11324
- */
11325
- /**
11326
- * Converts an OSCAL Profile against a provided catalog to HDF Baseline JSON.
11327
- *
11328
- * @param profileInput - Raw JSON string containing an OSCAL profile
11329
- * @param catalogInput - Raw JSON string containing an OSCAL catalog
11330
- * @returns HDF Baseline JSON string
11331
- */
11332
- async function convertOscalProfileToHdf(profileInput, catalogInput) {
11333
- validateInputSize(profileInput, "oscal-profile");
11334
- if (!profileInput || profileInput.trim().length === 0) throw new Error("empty profile input");
11335
- if (!catalogInput || catalogInput.trim().length === 0) throw new Error("empty catalog input");
11336
- const profileDoc = parseJSON(profileInput);
11337
- if (!profileDoc.profile) throw new Error("oscal-profile: input is not a profile document (root key is not 'profile')");
11338
- const profile = profileDoc.profile;
11339
- const catalogDoc = parseJSON(catalogInput);
11340
- if (!catalogDoc.catalog) throw new Error("oscal-profile: catalog input is not a catalog document (root key is not 'catalog')");
11341
- const catalog = catalogDoc.catalog;
11342
- if (!profile.imports || profile.imports.length === 0) throw new Error("oscal-profile: profile has no imports");
11343
- if (profile.imports.length > 1) throw new Error(`oscal-profile: profile has ${profile.imports.length} imports -- this converter only supports single-catalog imports. Use NIST's oscal-cli to resolve complex profiles, or use a pre-resolved catalog`);
11344
- if (profile.modify?.alters && profile.modify.alters.length > 0) throw new Error(`oscal-profile: profile contains ${profile.modify.alters.length} alter directives -- this converter only supports parameter overrides. Use NIST's oscal-cli to resolve profiles with alter directives, or use a pre-resolved catalog`);
11345
- const imp = profile.imports[0];
11346
- const resolvedCatalog = filterCatalog(catalog, collectIncludedIDs(imp), collectExcludedIDs(imp));
11347
- if (profile.modify?.["set-parameters"]) applyParameterOverrides(resolvedCatalog, profile.modify["set-parameters"]);
11348
- resolvedCatalog.metadata = profile.metadata;
11349
- resolvedCatalog.uuid = profile.uuid;
11350
- const baseline = await catalogToBaseline(resolvedCatalog, profileInput);
11351
- baseline.integrity = await inputIntegrity(profileInput);
11352
- return JSON.stringify(baseline, null, 2);
11353
- }
11354
- /** Extracts all control IDs from an import's include-controls. Returns null if include all. */
11355
- function collectIncludedIDs(imp) {
11356
- const includeControls = imp["include-controls"];
11357
- if (!includeControls || includeControls.length === 0) return null;
11358
- const ids = /* @__PURE__ */ new Set();
11359
- for (const ic of includeControls) for (const id of ic["with-ids"] ?? []) ids.add(id);
11360
- return ids;
11361
- }
11362
- /** Extracts all control IDs from an import's exclude-controls. */
11363
- function collectExcludedIDs(imp) {
11364
- const excludeControls = imp["exclude-controls"];
11365
- if (!excludeControls || excludeControls.length === 0) return null;
11366
- const ids = /* @__PURE__ */ new Set();
11367
- for (const ec of excludeControls) for (const id of ec["with-ids"] ?? []) ids.add(id);
11368
- return ids;
11369
- }
11370
- /** Creates a new catalog containing only the controls matching filters. */
11371
- function filterCatalog(catalog, includedIDs, excludedIDs) {
11372
- const includeAll = includedIDs === null;
11373
- const result = {
11374
- uuid: catalog.uuid,
11375
- metadata: catalog.metadata,
11376
- "back-matter": catalog["back-matter"],
11377
- groups: [],
11378
- controls: []
11379
- };
11380
- for (const group of catalog.groups ?? []) {
11381
- const filtered = filterGroupControls(group, includedIDs, excludedIDs, includeAll);
11382
- if (filtered.controls && filtered.controls.length > 0) result.groups.push(filtered);
11756
+ reqIDs.push(req.id);
11757
+ for (const enh of ctrl.controls ?? []) {
11758
+ const enhReq = controlToBaselineRequirement(enh);
11759
+ requirements.push(enhReq);
11760
+ reqIDs.push(enhReq.id);
11761
+ }
11762
+ }
11763
+ if (reqIDs.length > 0) groups.push({
11764
+ id: group.id ?? "",
11765
+ title: group.title,
11766
+ requirements: reqIDs
11767
+ });
11383
11768
  }
11384
- for (const ctrl of catalog.controls ?? []) if (shouldIncludeControl(ctrl.id, includedIDs, excludedIDs, includeAll)) {
11385
- const filteredCtrl = filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll);
11386
- result.controls.push(filteredCtrl);
11769
+ for (const ctrl of catalog.controls ?? []) {
11770
+ const req = controlToBaselineRequirement(ctrl);
11771
+ requirements.push(req);
11772
+ for (const enh of ctrl.controls ?? []) {
11773
+ const enhReq = controlToBaselineRequirement(enh);
11774
+ requirements.push(enhReq);
11775
+ }
11387
11776
  }
11388
- return result;
11389
- }
11390
- function filterGroupControls(group, includedIDs, excludedIDs, includeAll) {
11391
- const filtered = {
11392
- id: group.id,
11393
- class: group.class,
11394
- title: group.title,
11395
- props: group.props,
11396
- parts: group.parts,
11397
- controls: []
11777
+ return {
11778
+ name: catalogBaselineName(catalog),
11779
+ title: meta.title,
11780
+ version: meta.version,
11781
+ status: "loaded",
11782
+ integrity,
11783
+ requirements,
11784
+ groups,
11785
+ generator: {
11786
+ name: "oscal-catalog-to-hdf",
11787
+ version: "1.0.0"
11788
+ }
11398
11789
  };
11399
- for (const ctrl of group.controls ?? []) if (shouldIncludeControl(ctrl.id, includedIDs, excludedIDs, includeAll)) {
11400
- const filteredCtrl = filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll);
11401
- filtered.controls.push(filteredCtrl);
11402
- }
11403
- return filtered;
11404
11790
  }
11405
- function filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll) {
11406
- const result = {
11407
- id: ctrl.id,
11408
- class: ctrl.class,
11791
+ /** Converts a single OSCAL Control to an HDF BaselineRequirement. */
11792
+ function controlToBaselineRequirement(ctrl) {
11793
+ const nistTag = controlIdToNistTag(ctrl.id);
11794
+ const descriptions = buildCatalogDescriptions(ctrl);
11795
+ const tags = buildCatalogTags(ctrl);
11796
+ const req = {
11797
+ id: nistTag,
11409
11798
  title: ctrl.title,
11410
- params: ctrl.params,
11411
- props: ctrl.props,
11412
- links: ctrl.links,
11413
- parts: ctrl.parts,
11414
- controls: []
11799
+ impact: .5,
11800
+ descriptions,
11801
+ tags
11415
11802
  };
11416
- for (const enh of ctrl.controls ?? []) if (shouldIncludeControl(enh.id, includedIDs, excludedIDs, includeAll)) result.controls.push(enh);
11417
- return result;
11418
- }
11419
- function shouldIncludeControl(id, includedIDs, excludedIDs, includeAll) {
11420
- if (excludedIDs !== null && excludedIDs.has(id)) return false;
11421
- if (includeAll) return true;
11422
- return includedIDs !== null && includedIDs.has(id);
11423
- }
11424
- /** Applies set-parameters from the profile's modify section to the resolved catalog. */
11425
- function applyParameterOverrides(catalog, setParams) {
11426
- if (setParams.length === 0) return;
11427
- const overrides = /* @__PURE__ */ new Map();
11428
- for (const sp of setParams) if (sp["param-id"]) overrides.set(sp["param-id"], sp.values ?? []);
11429
- for (const group of catalog.groups ?? []) for (const ctrl of group.controls ?? []) {
11430
- applyParamOverridesToControl(ctrl, overrides);
11431
- for (const enh of ctrl.controls ?? []) applyParamOverridesToControl(enh, overrides);
11432
- }
11433
- for (const ctrl of catalog.controls ?? []) {
11434
- applyParamOverridesToControl(ctrl, overrides);
11435
- for (const enh of ctrl.controls ?? []) applyParamOverridesToControl(enh, overrides);
11436
- }
11803
+ const controlType = deriveControlTypeFromTags([nistTag]);
11804
+ if (controlType !== void 0) req.controlType = controlType;
11805
+ if (extractPropValue(ctrl.props, "CORE") === "true") req.applicability = Applicability.Required;
11806
+ return req;
11437
11807
  }
11438
- function applyParamOverridesToControl(ctrl, overrides) {
11439
- if (ctrl.params) for (const param of ctrl.params) {
11440
- if (!param.id) continue;
11441
- const values = overrides.get(param.id);
11442
- if (values) param.label = values.join(", ");
11443
- }
11444
- if (ctrl.parts) for (const part of ctrl.parts) replaceParamInsertions(part, overrides);
11808
+ /** Creates HDF Description entries from control parts. */
11809
+ function buildCatalogDescriptions(ctrl) {
11810
+ const descriptions = [];
11811
+ const statement = flattenPartsByName(ctrl.parts, "statement");
11812
+ descriptions.push({
11813
+ label: "default",
11814
+ data: statement || ""
11815
+ });
11816
+ const guidance = flattenPartsByName(ctrl.parts, "guidance");
11817
+ if (guidance) descriptions.push({
11818
+ label: "rationale",
11819
+ data: guidance
11820
+ });
11821
+ const check = flattenPartsByName(ctrl.parts, "assessment-objective");
11822
+ if (check) descriptions.push({
11823
+ label: "check",
11824
+ data: check
11825
+ });
11826
+ return descriptions;
11445
11827
  }
11446
- function replaceParamInsertions(part, overrides) {
11447
- if (part.prose) part.prose = substituteParams(part.prose, overrides);
11448
- if (part.parts) for (const child of part.parts) replaceParamInsertions(child, overrides);
11828
+ /** Builds the tags map for an OSCAL catalog control. */
11829
+ function buildCatalogTags(ctrl) {
11830
+ const tags = {};
11831
+ tags["nist"] = [controlIdToNistTag(ctrl.id)];
11832
+ const label = extractPropValue(ctrl.props, "label");
11833
+ if (label !== void 0) tags["label"] = label;
11834
+ const sortId = extractPropValue(ctrl.props, "sort-id");
11835
+ if (sortId !== void 0) tags["sort-id"] = sortId;
11836
+ return tags;
11449
11837
  }
11450
- /** Replaces OSCAL parameter insertion patterns: {{ insert: param, <param-id> }} */
11451
- function substituteParams(text, overrides) {
11452
- let result = text;
11453
- for (const [paramId, values] of overrides) {
11454
- const placeholder = `{{ insert: param, ${paramId} }}`;
11455
- const replacement = values.join(", ");
11456
- result = result.split(placeholder).join(replacement);
11457
- }
11458
- return result;
11838
+ /** Derives a baseline name from catalog metadata. */
11839
+ function catalogBaselineName(catalog) {
11840
+ return toKebabCase(catalog.metadata.title, "oscal-catalog");
11459
11841
  }
11460
11842
  //#endregion
11461
11843
  //#region converters/oscal-to-hdf/typescript/converter-component.ts
@@ -11527,164 +11909,160 @@ function componentBaselineName(comp, compDef) {
11527
11909
  return toKebabCase(comp.title || compDef.metadata.title, "oscal-component-definition");
11528
11910
  }
11529
11911
  //#endregion
11530
- //#region converters/oscal-to-hdf/typescript/converter-ssp.ts
11912
+ //#region converters/oscal-to-hdf/typescript/converter-poam.ts
11531
11913
  /**
11532
- * OSCAL System Security Plan (SSP) to HDF System converter.
11914
+ * OSCAL Plan of Action and Milestones (POA&M) to HDF Amendments converter.
11533
11915
  *
11534
- * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_ssp.go.
11916
+ * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_poam.go.
11535
11917
  */
11536
11918
  /**
11537
- * Converts an OSCAL System Security Plan document to HDF System JSON.
11919
+ * Converts an OSCAL POA&M document to HDF Amendments JSON.
11538
11920
  *
11539
- * @param input - Raw JSON string containing an OSCAL SSP
11540
- * @returns HDF System JSON string
11921
+ * @param input - Raw JSON string containing an OSCAL POA&M
11922
+ * @returns HDF Amendments JSON string
11541
11923
  */
11542
- async function convertOscalSspToHdf(input) {
11543
- validateInputSize(input, "oscal-ssp");
11924
+ async function convertOscalPoamToHdf(input) {
11925
+ validateInputSize(input, "oscal-poam");
11544
11926
  if (!input || input.trim().length === 0) throw new Error("empty input");
11545
11927
  const doc = parseJSON(input);
11546
- if (!doc["system-security-plan"]) throw new Error("oscal-ssp: input is not a system-security-plan document (root key is not 'system-security-plan')");
11547
- const ssp = doc["system-security-plan"];
11928
+ if (!doc["plan-of-action-and-milestones"]) throw new Error("oscal-poam: input is not a plan-of-action-and-milestones document (root key is not 'plan-of-action-and-milestones')");
11929
+ const poam = doc["plan-of-action-and-milestones"];
11548
11930
  const integrity = await inputIntegrity(input);
11549
- const system = {
11550
- name: sspSystemName(ssp),
11931
+ const meta = extractMetadata(poam.metadata);
11932
+ const riskMap = buildRiskMap$1(poam.risks ?? []);
11933
+ const overrides = [];
11934
+ for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
11935
+ const systemRef = poam["import-ssp"]?.href || void 0;
11936
+ const appliedBy = extractAppliedBy(poam.metadata);
11937
+ return serializeHdf({
11938
+ name: toKebabCase(poam.metadata.title, "oscal-poam"),
11939
+ overrides,
11551
11940
  integrity,
11552
- components: [],
11941
+ systemRef,
11942
+ version: meta.version,
11943
+ appliedBy,
11553
11944
  generator: {
11554
- name: "oscal-ssp-to-hdf",
11945
+ name: "oscal-poam-to-hdf",
11555
11946
  version: "1.0.0"
11556
11947
  }
11557
- };
11558
- const sc = ssp["system-characteristics"];
11559
- if (sc) {
11560
- if (sc.description) {
11561
- let desc = sc.description;
11562
- if (sc["authorization-boundary"]?.description) desc += "\n\nAuthorization Boundary: " + sc["authorization-boundary"].description;
11563
- system.description = desc;
11564
- }
11565
- const sil = sc["security-impact-level"];
11566
- if (sil) system.categorizationLevel = sspCategorizationLevel(sil) ?? void 0;
11567
- else if (sc["security-sensitivity-level"]) system.categorizationLevel = mapSensitivityToCategorizationLevel(sc["security-sensitivity-level"]) ?? void 0;
11568
- if (sc.status) system.authorizationStatus = sspAuthorizationStatus(sc.status.state) ?? void 0;
11569
- if (sc["authorization-boundary"]?.description) system.boundaryDescription = sc["authorization-boundary"].description;
11570
- const systemIds = sc["system-ids"];
11571
- if (systemIds && systemIds.length > 0) {
11572
- system.identifier = systemIds[0].id;
11573
- if (systemIds[0]["identifier-type"]) system.identifierScheme = systemIds[0]["identifier-type"];
11574
- }
11575
- }
11576
- const meta = extractMetadata(ssp.metadata);
11577
- if (meta.version) system.version = meta.version;
11578
- const componentControls = buildComponentControlMap(ssp["control-implementation"]);
11579
- const si = ssp["system-implementation"];
11580
- if (si?.components) for (const comp of si.components) system.components.push(sspComponentToHDFComponent(comp, componentControls));
11581
- return JSON.stringify(system, null, 2);
11948
+ });
11582
11949
  }
11583
- function sspSystemName(ssp) {
11584
- const sc = ssp["system-characteristics"];
11585
- if (sc?.["system-name"]) return sc["system-name"];
11586
- if (ssp.metadata.title) return ssp.metadata.title;
11587
- return "oscal-ssp";
11950
+ function buildRiskMap$1(risks) {
11951
+ const m = /* @__PURE__ */ new Map();
11952
+ for (const risk of risks) m.set(risk.uuid, risk);
11953
+ return m;
11588
11954
  }
11589
- function sspCategorizationLevel(sil) {
11590
- const levels = [
11591
- sil["security-objective-confidentiality"] ?? "",
11592
- sil["security-objective-integrity"] ?? "",
11593
- sil["security-objective-availability"] ?? ""
11594
- ];
11595
- let highest = "";
11596
- for (const l of levels) {
11597
- const normalized = normalizeFIPSLevel(l);
11598
- if (fipsLevelRank(normalized) > fipsLevelRank(highest)) highest = normalized;
11599
- }
11600
- if (!highest) return null;
11601
- switch (highest) {
11602
- case "high": return CategorizationLevel.High;
11603
- case "moderate": return CategorizationLevel.Moderate;
11604
- case "low": return CategorizationLevel.Low;
11605
- default: return null;
11606
- }
11955
+ function poamItemToOverride(item, riskMap, poam) {
11956
+ const requirementId = extractRequirementIdFromPOAMItem(item, riskMap);
11957
+ return {
11958
+ type: OverrideType.Poam,
11959
+ requirementId,
11960
+ reason: poamItemReason(item),
11961
+ status: poamItemStatus(item, riskMap),
11962
+ appliedBy: poamItemAppliedBy(poam),
11963
+ appliedAt: poamItemAppliedAt(poam, requirementId),
11964
+ expiresAt: poamItemExpiresAt(item, riskMap, requirementId),
11965
+ milestones: extractMilestones(item, riskMap)
11966
+ };
11607
11967
  }
11608
- function normalizeFIPSLevel(level) {
11609
- let lower = level.toLowerCase();
11610
- lower = lower.replace(/^fips-199-/, "");
11611
- switch (lower) {
11612
- case "high": return "high";
11613
- case "moderate":
11614
- case "medium": return "moderate";
11615
- case "low": return "low";
11616
- default: return "";
11968
+ function extractRequirementIdFromPOAMItem(item, riskMap) {
11969
+ for (const rr of item["related-risks"] ?? []) {
11970
+ const riskUuid = rr["risk-uuid"];
11971
+ if (riskUuid) {
11972
+ const risk = riskMap.get(riskUuid);
11973
+ if (risk) {
11974
+ const controlId = extractPropValue(risk.props, "impacted-control-id");
11975
+ if (controlId) return controlIdToNistTag(controlId);
11976
+ }
11977
+ }
11617
11978
  }
11979
+ const poamId = extractPropValue(item.props, "POAM-ID");
11980
+ if (poamId) return poamId;
11981
+ if (item.title) return item.title;
11982
+ return "unknown";
11618
11983
  }
11619
- function fipsLevelRank(level) {
11620
- switch (level) {
11621
- case "high": return 3;
11622
- case "moderate": return 2;
11623
- case "low": return 1;
11624
- default: return 0;
11625
- }
11984
+ function poamItemReason(item) {
11985
+ if (item.description) return item.description;
11986
+ if (item.title) return item.title;
11987
+ return "POA&M item";
11626
11988
  }
11627
- function mapSensitivityToCategorizationLevel(level) {
11628
- const normalized = normalizeFIPSLevel(level);
11629
- if (!normalized) return null;
11630
- switch (normalized) {
11631
- case "high": return CategorizationLevel.High;
11632
- case "moderate": return CategorizationLevel.Moderate;
11633
- case "low": return CategorizationLevel.Low;
11634
- default: return null;
11989
+ function poamItemStatus(item, riskMap) {
11990
+ for (const rr of item["related-risks"] ?? []) {
11991
+ const riskUuid = rr["risk-uuid"];
11992
+ if (riskUuid) {
11993
+ const risk = riskMap.get(riskUuid);
11994
+ if (risk) {
11995
+ const status = oscalStatusToHdf(risk.status);
11996
+ if (status === "passed") return "passed";
11997
+ if (status === "failed") return "failed";
11998
+ }
11999
+ }
11635
12000
  }
12001
+ return "failed";
11636
12002
  }
11637
- function sspAuthorizationStatus(state) {
11638
- switch (state.toLowerCase()) {
11639
- case "operational": return AuthorizationStatus.Authorized;
11640
- case "under-development": return AuthorizationStatus.PendingAuthorization;
11641
- case "disposition": return AuthorizationStatus.Revoked;
11642
- case "other": return AuthorizationStatus.NotYetRequested;
11643
- default: return null;
12003
+ function poamItemAppliedBy(poam) {
12004
+ const rps = poam.metadata["responsible-parties"];
12005
+ if (rps) {
12006
+ for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
12007
+ type: "simple",
12008
+ identifier: rp["party-uuids"][0]
12009
+ };
12010
+ if (rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
12011
+ type: "simple",
12012
+ identifier: rps[0]["party-uuids"][0]
12013
+ };
11644
12014
  }
12015
+ return {
12016
+ type: "system",
12017
+ identifier: "oscal-poam-converter"
12018
+ };
11645
12019
  }
11646
- function buildComponentControlMap(ci) {
11647
- const result = /* @__PURE__ */ new Map();
11648
- if (!ci) return result;
11649
- for (const ir of ci["implemented-requirements"] ?? []) {
11650
- const controlId = ir["control-id"];
11651
- for (const bc of ir["by-components"] ?? []) addComponentControl(result, bc["component-uuid"], controlId);
11652
- for (const stmt of ir.statements ?? []) for (const bc of stmt["by-components"] ?? []) addComponentControl(result, bc["component-uuid"], controlId);
11653
- }
11654
- return result;
12020
+ function poamItemAppliedAt(poam, requirementId) {
12021
+ const t = poam.metadata["last-modified"] && parseTimestamp(String(poam.metadata["last-modified"]));
12022
+ if (t) return t;
12023
+ throw new Error(`poam-item "${requirementId}": no usable metadata.last-modified for appliedAt`);
11655
12024
  }
11656
- function addComponentControl(m, compUUID, controlId) {
11657
- let controls = m.get(compUUID);
11658
- if (!controls) {
11659
- controls = /* @__PURE__ */ new Set();
11660
- m.set(compUUID, controls);
12025
+ function poamItemExpiresAt(item, riskMap, requirementId) {
12026
+ for (const rr of item["related-risks"] ?? []) {
12027
+ const riskUuid = rr["risk-uuid"];
12028
+ if (!riskUuid) continue;
12029
+ const risk = riskMap.get(riskUuid);
12030
+ if (!risk?.deadline) continue;
12031
+ const t = parseTimestamp(String(risk.deadline));
12032
+ if (t) return t;
11661
12033
  }
11662
- controls.add(controlId);
12034
+ throw new Error(`poam-item "${requirementId}": no related risk carries a usable deadline; a POA&M requires a time commitment`);
11663
12035
  }
11664
- function sspComponentToHDFComponent(sc, componentControls) {
11665
- const comp = {
11666
- name: sc.title,
11667
- type: mapOSCALComponentType(sc.type)
11668
- };
11669
- if (sc.description) comp.description = sc.description;
11670
- const controls = componentControls.get(sc.uuid);
11671
- if (controls && controls.size > 0) {
11672
- const refs = [];
11673
- for (const controlId of controls) refs.push(controlIdToNistTag(controlId));
11674
- if (refs.length > 0) comp.baselineRefs = refs;
12036
+ function extractMilestones(item, riskMap) {
12037
+ const milestones = [];
12038
+ for (const rr of item["related-risks"] ?? []) {
12039
+ const riskUuid = rr["risk-uuid"];
12040
+ if (!riskUuid) continue;
12041
+ const risk = riskMap.get(riskUuid);
12042
+ if (!risk) continue;
12043
+ for (const rem of risk.remediations ?? []) {
12044
+ if (rem.lifecycle !== "planned") continue;
12045
+ for (const task of rem.tasks ?? []) {
12046
+ const end = task.timing?.["within-date-range"]?.end;
12047
+ const estimatedCompletion = end && parseTimestamp(String(end));
12048
+ if (!estimatedCompletion) continue;
12049
+ milestones.push({
12050
+ description: task.description ? `${task.title}: ${task.description}` : task.title,
12051
+ estimatedCompletion,
12052
+ status: "pending"
12053
+ });
12054
+ }
12055
+ }
11675
12056
  }
11676
- return comp;
12057
+ return milestones;
11677
12058
  }
11678
- function mapOSCALComponentType(oscalType) {
11679
- switch (oscalType.toLowerCase()) {
11680
- case "software":
11681
- case "this-system": return TargetType.Application;
11682
- case "service": return TargetType.Application;
11683
- case "hardware": return TargetType.Host;
11684
- case "network": return TargetType.Network;
11685
- case "database": return TargetType.Database;
11686
- case "storage": return TargetType.Artifact;
11687
- default: return TargetType.Application;
12059
+ function extractAppliedBy(meta) {
12060
+ const rps = meta["responsible-parties"];
12061
+ if (rps) {
12062
+ for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
12063
+ type: "simple",
12064
+ identifier: rp["party-uuids"][0]
12065
+ };
11688
12066
  }
11689
12067
  }
11690
12068
  //#endregion
@@ -11790,181 +12168,32 @@ function buildTargetSelector(ap) {
11790
12168
  const selector = {};
11791
12169
  for (const subject of subjects) {
11792
12170
  if (subject.type) {
11793
- const key = "subject-type";
11794
- if (selector[key] !== void 0) selector[key] += "," + subject.type;
11795
- else selector[key] = subject.type;
11796
- }
11797
- if (subject["include-all"] !== void 0) selector["include-" + subject.type] = "all";
11798
- }
11799
- if (Object.keys(selector).length === 0) return null;
11800
- return selector;
11801
- }
11802
- function determinePlanType(ap) {
11803
- const aType = extractPropValue(ap.metadata.props, "assessment-type");
11804
- if (aType) switch (aType.toLowerCase()) {
11805
- case "automated": return PlanType.Automated;
11806
- case "manual": return PlanType.Manual;
11807
- }
11808
- if (ap.tasks && ap.tasks.length > 0) return PlanType.Hybrid;
11809
- }
11810
- function buildPlanDescription(ap) {
11811
- const parts = [];
11812
- if (ap.metadata.remarks) parts.push(ap.metadata.remarks);
11813
- if (ap["terms-and-conditions"]?.parts && ap["terms-and-conditions"].parts.length > 0) {
11814
- const terms = flattenParts(ap["terms-and-conditions"].parts);
11815
- if (terms) parts.push("Terms and Conditions: " + terms);
11816
- }
11817
- if (parts.length === 0) return void 0;
11818
- return parts.join("\n\n");
11819
- }
11820
- //#endregion
11821
- //#region converters/oscal-to-hdf/typescript/converter-poam.ts
11822
- /**
11823
- * OSCAL Plan of Action and Milestones (POA&M) to HDF Amendments converter.
11824
- *
11825
- * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_poam.go.
11826
- */
11827
- /**
11828
- * Converts an OSCAL POA&M document to HDF Amendments JSON.
11829
- *
11830
- * @param input - Raw JSON string containing an OSCAL POA&M
11831
- * @returns HDF Amendments JSON string
11832
- */
11833
- async function convertOscalPoamToHdf(input) {
11834
- validateInputSize(input, "oscal-poam");
11835
- if (!input || input.trim().length === 0) throw new Error("empty input");
11836
- const doc = parseJSON(input);
11837
- if (!doc["plan-of-action-and-milestones"]) throw new Error("oscal-poam: input is not a plan-of-action-and-milestones document (root key is not 'plan-of-action-and-milestones')");
11838
- const poam = doc["plan-of-action-and-milestones"];
11839
- const integrity = await inputIntegrity(input);
11840
- const meta = extractMetadata(poam.metadata);
11841
- const riskMap = buildRiskMap$1(poam.risks ?? []);
11842
- const overrides = [];
11843
- for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
11844
- const systemRef = poam["import-ssp"]?.href || void 0;
11845
- const appliedBy = extractAppliedBy(poam.metadata);
11846
- return serializeHdf({
11847
- name: toKebabCase(poam.metadata.title, "oscal-poam"),
11848
- overrides,
11849
- integrity,
11850
- systemRef,
11851
- version: meta.version,
11852
- appliedBy,
11853
- generator: {
11854
- name: "oscal-poam-to-hdf",
11855
- version: "1.0.0"
11856
- }
11857
- });
11858
- }
11859
- function buildRiskMap$1(risks) {
11860
- const m = /* @__PURE__ */ new Map();
11861
- for (const risk of risks) m.set(risk.uuid, risk);
11862
- return m;
11863
- }
11864
- function poamItemToOverride(item, riskMap, poam) {
11865
- return {
11866
- type: OverrideType.Poam,
11867
- requirementId: extractRequirementIdFromPOAMItem(item, riskMap),
11868
- reason: poamItemReason(item),
11869
- status: poamItemStatus(item, riskMap),
11870
- appliedBy: poamItemAppliedBy(poam),
11871
- appliedAt: poamItemAppliedAt(poam),
11872
- expiresAt: poamItemExpiresAt(),
11873
- milestones: extractMilestones(item, riskMap)
11874
- };
11875
- }
11876
- function extractRequirementIdFromPOAMItem(item, riskMap) {
11877
- for (const rr of item["related-risks"] ?? []) {
11878
- const riskUuid = rr["risk-uuid"];
11879
- if (riskUuid) {
11880
- const risk = riskMap.get(riskUuid);
11881
- if (risk) {
11882
- const controlId = extractPropValue(risk.props, "impacted-control-id");
11883
- if (controlId) return controlIdToNistTag(controlId);
11884
- }
11885
- }
11886
- }
11887
- const poamId = extractPropValue(item.props, "POAM-ID");
11888
- if (poamId) return poamId;
11889
- if (item.title) return item.title;
11890
- return "unknown";
11891
- }
11892
- function poamItemReason(item) {
11893
- if (item.description) return item.description;
11894
- if (item.title) return item.title;
11895
- return "POA&M item";
11896
- }
11897
- function poamItemStatus(item, riskMap) {
11898
- for (const rr of item["related-risks"] ?? []) {
11899
- const riskUuid = rr["risk-uuid"];
11900
- if (riskUuid) {
11901
- const risk = riskMap.get(riskUuid);
11902
- if (risk) {
11903
- const status = oscalStatusToHdf(risk.status);
11904
- if (status === "passed") return "passed";
11905
- if (status === "failed") return "failed";
11906
- }
11907
- }
11908
- }
11909
- return "failed";
11910
- }
11911
- function poamItemAppliedBy(poam) {
11912
- const rps = poam.metadata["responsible-parties"];
11913
- if (rps) {
11914
- for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
11915
- type: "simple",
11916
- identifier: rp["party-uuids"][0]
11917
- };
11918
- if (rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
11919
- type: "simple",
11920
- identifier: rps[0]["party-uuids"][0]
11921
- };
11922
- }
11923
- return {
11924
- type: "system",
11925
- identifier: "oscal-poam-converter"
11926
- };
11927
- }
11928
- function poamItemAppliedAt(poam) {
11929
- if (poam.metadata["last-modified"]) {
11930
- const t = parseTimestamp(String(poam.metadata["last-modified"]));
11931
- if (t) return t;
12171
+ const key = "subject-type";
12172
+ if (selector[key] !== void 0) selector[key] += "," + subject.type;
12173
+ else selector[key] = subject.type;
12174
+ }
12175
+ if (subject["include-all"] !== void 0) selector["include-" + subject.type] = "all";
11932
12176
  }
11933
- return /* @__PURE__ */ new Date();
11934
- }
11935
- function poamItemExpiresAt() {
11936
- const d = /* @__PURE__ */ new Date();
11937
- d.setFullYear(d.getFullYear() + 1);
11938
- return d;
12177
+ if (Object.keys(selector).length === 0) return null;
12178
+ return selector;
11939
12179
  }
11940
- function extractMilestones(item, riskMap) {
11941
- const milestones = [];
11942
- for (const rr of item["related-risks"] ?? []) {
11943
- const riskUuid = rr["risk-uuid"];
11944
- if (!riskUuid) continue;
11945
- const risk = riskMap.get(riskUuid);
11946
- if (!risk) continue;
11947
- for (const rem of risk.remediations ?? []) {
11948
- if (rem.lifecycle !== "planned") continue;
11949
- const estimatedCompletion = /* @__PURE__ */ new Date();
11950
- estimatedCompletion.setMonth(estimatedCompletion.getMonth() + 3);
11951
- milestones.push({
11952
- description: rem.title + ": " + rem.description,
11953
- estimatedCompletion,
11954
- status: "pending"
11955
- });
11956
- }
12180
+ function determinePlanType(ap) {
12181
+ const aType = extractPropValue(ap.metadata.props, "assessment-type");
12182
+ if (aType) switch (aType.toLowerCase()) {
12183
+ case "automated": return PlanType.Automated;
12184
+ case "manual": return PlanType.Manual;
11957
12185
  }
11958
- return milestones;
12186
+ if (ap.tasks && ap.tasks.length > 0) return PlanType.Hybrid;
11959
12187
  }
11960
- function extractAppliedBy(meta) {
11961
- const rps = meta["responsible-parties"];
11962
- if (rps) {
11963
- for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
11964
- type: "simple",
11965
- identifier: rp["party-uuids"][0]
11966
- };
12188
+ function buildPlanDescription(ap) {
12189
+ const parts = [];
12190
+ if (ap.metadata.remarks) parts.push(ap.metadata.remarks);
12191
+ if (ap["terms-and-conditions"]?.parts && ap["terms-and-conditions"].parts.length > 0) {
12192
+ const terms = flattenParts(ap["terms-and-conditions"].parts);
12193
+ if (terms) parts.push("Terms and Conditions: " + terms);
11967
12194
  }
12195
+ if (parts.length === 0) return void 0;
12196
+ return parts.join("\n\n");
11968
12197
  }
11969
12198
  //#endregion
11970
12199
  //#region converters/oscal-to-hdf/typescript/converter-sar.ts
@@ -12075,99 +12304,417 @@ function extractControlIdFromFinding(f) {
12075
12304
  if (idx > 0) controlId = controlId.slice(0, idx);
12076
12305
  return controlId;
12077
12306
  }
12078
- function mapFindingStatus(f) {
12079
- const status = oscalStatusToHdf(f.target.status.state);
12080
- if (status === "passed") return ResultStatus.Passed;
12081
- if (status === "failed") return ResultStatus.Failed;
12082
- return ResultStatus.NotReviewed;
12307
+ function mapFindingStatus(f) {
12308
+ const status = oscalStatusToHdf(f.target.status.state);
12309
+ if (status === "passed") return ResultStatus.Passed;
12310
+ if (status === "failed") return ResultStatus.Failed;
12311
+ return ResultStatus.NotReviewed;
12312
+ }
12313
+ function buildCodeDesc(f, obsMap) {
12314
+ const parts = [];
12315
+ for (const ref of f["related-observations"] ?? []) {
12316
+ const obsUuid = ref["observation-uuid"];
12317
+ if (!obsUuid) continue;
12318
+ const obs = obsMap.get(obsUuid);
12319
+ if (!obs) continue;
12320
+ if (obs.methods && obs.methods.length > 0) parts.push("Methods: " + obs.methods.join(", "));
12321
+ for (const subj of obs.subjects ?? []) {
12322
+ let subjDesc = subj.type;
12323
+ if (subj.title) subjDesc = subj.title + " (" + subj.type + ")";
12324
+ parts.push("Subject: " + subjDesc);
12325
+ }
12326
+ }
12327
+ if (parts.length === 0) return f.title;
12328
+ return parts.join("; ");
12329
+ }
12330
+ function buildRiskMessage(f, riskMap) {
12331
+ const messages = [];
12332
+ for (const ref of f["related-risks"] ?? []) {
12333
+ const riskUuid = ref["risk-uuid"];
12334
+ if (!riskUuid) continue;
12335
+ const risk = riskMap.get(riskUuid);
12336
+ if (!risk) continue;
12337
+ let msg = risk.title;
12338
+ if (risk.description) msg += ": " + risk.description;
12339
+ messages.push(msg);
12340
+ }
12341
+ return messages.join("\n");
12342
+ }
12343
+ function sarFindingsImpact(findings, riskMap) {
12344
+ let highestImpact = -1;
12345
+ for (const f of findings) for (const ref of f["related-risks"] ?? []) {
12346
+ const riskUuid = ref["risk-uuid"];
12347
+ if (!riskUuid) continue;
12348
+ const risk = riskMap.get(riskUuid);
12349
+ if (!risk) continue;
12350
+ const impact = extractRiskSeverity(risk.characterizations, -1);
12351
+ if (impact > highestImpact) highestImpact = impact;
12352
+ }
12353
+ if (highestImpact < 0) return .5;
12354
+ return highestImpact;
12355
+ }
12356
+ function sarBuildDescriptions(findings, obsMap) {
12357
+ const descriptions = [];
12358
+ const findingDescs = [];
12359
+ for (const f of findings) if (f.description) findingDescs.push(f.description);
12360
+ descriptions.push({
12361
+ label: "default",
12362
+ data: findingDescs.join("\n") || ""
12363
+ });
12364
+ const obsDescs = [];
12365
+ const seen = /* @__PURE__ */ new Set();
12366
+ for (const f of findings) for (const ref of f["related-observations"] ?? []) {
12367
+ const obsUuid = ref["observation-uuid"];
12368
+ if (!obsUuid || seen.has(obsUuid)) continue;
12369
+ seen.add(obsUuid);
12370
+ const obs = obsMap.get(obsUuid);
12371
+ if (obs?.description) obsDescs.push(obs.description);
12372
+ }
12373
+ if (obsDescs.length > 0) descriptions.push({
12374
+ label: "rationale",
12375
+ data: obsDescs.join("\n")
12376
+ });
12377
+ return descriptions;
12378
+ }
12379
+ function buildObservationMap(observations) {
12380
+ const m = /* @__PURE__ */ new Map();
12381
+ for (const obs of observations) m.set(obs.uuid, obs);
12382
+ return m;
12383
+ }
12384
+ function buildRiskMap(risks) {
12385
+ const m = /* @__PURE__ */ new Map();
12386
+ for (const risk of risks) m.set(risk.uuid, risk);
12387
+ return m;
12388
+ }
12389
+ function parseResultStartTime(result) {
12390
+ if (result.start) {
12391
+ const t = parseTimestamp(String(result.start));
12392
+ if (t) return t;
12393
+ }
12394
+ return /* @__PURE__ */ new Date(0);
12395
+ }
12396
+ function sarBaselineName(result, sar) {
12397
+ return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
12398
+ }
12399
+ //#endregion
12400
+ //#region converters/oscal-to-hdf/typescript/converter-ssp.ts
12401
+ /**
12402
+ * OSCAL System Security Plan (SSP) to HDF System converter.
12403
+ *
12404
+ * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_ssp.go.
12405
+ */
12406
+ /**
12407
+ * Converts an OSCAL System Security Plan document to HDF System JSON.
12408
+ *
12409
+ * @param input - Raw JSON string containing an OSCAL SSP
12410
+ * @returns HDF System JSON string
12411
+ */
12412
+ async function convertOscalSspToHdf(input) {
12413
+ validateInputSize(input, "oscal-ssp");
12414
+ if (!input || input.trim().length === 0) throw new Error("empty input");
12415
+ const doc = parseJSON(input);
12416
+ if (!doc["system-security-plan"]) throw new Error("oscal-ssp: input is not a system-security-plan document (root key is not 'system-security-plan')");
12417
+ const ssp = doc["system-security-plan"];
12418
+ const integrity = await inputIntegrity(input);
12419
+ const system = {
12420
+ name: sspSystemName(ssp),
12421
+ integrity,
12422
+ components: [],
12423
+ generator: {
12424
+ name: "oscal-ssp-to-hdf",
12425
+ version: "1.0.0"
12426
+ }
12427
+ };
12428
+ const sc = ssp["system-characteristics"];
12429
+ if (sc) {
12430
+ if (sc.description) {
12431
+ let desc = sc.description;
12432
+ if (sc["authorization-boundary"]?.description) desc += "\n\nAuthorization Boundary: " + sc["authorization-boundary"].description;
12433
+ system.description = desc;
12434
+ }
12435
+ const sil = sc["security-impact-level"];
12436
+ if (sil) system.categorizationLevel = sspCategorizationLevel(sil) ?? void 0;
12437
+ else if (sc["security-sensitivity-level"]) system.categorizationLevel = mapSensitivityToCategorizationLevel(sc["security-sensitivity-level"]) ?? void 0;
12438
+ if (sc.status) system.authorizationStatus = sspAuthorizationStatus(sc.status.state) ?? void 0;
12439
+ if (sc["authorization-boundary"]?.description) system.boundaryDescription = sc["authorization-boundary"].description;
12440
+ const systemIds = sc["system-ids"];
12441
+ if (systemIds && systemIds.length > 0) {
12442
+ system.identifier = systemIds[0].id;
12443
+ if (systemIds[0]["identifier-type"]) system.identifierScheme = systemIds[0]["identifier-type"];
12444
+ }
12445
+ }
12446
+ const meta = extractMetadata(ssp.metadata);
12447
+ if (meta.version) system.version = meta.version;
12448
+ const componentControls = buildComponentControlMap(ssp["control-implementation"]);
12449
+ const si = ssp["system-implementation"];
12450
+ if (si?.components) for (const comp of si.components) system.components.push(sspComponentToHDFComponent(comp, componentControls));
12451
+ return JSON.stringify(system, null, 2);
12452
+ }
12453
+ function sspSystemName(ssp) {
12454
+ const sc = ssp["system-characteristics"];
12455
+ if (sc?.["system-name"]) return sc["system-name"];
12456
+ if (ssp.metadata.title) return ssp.metadata.title;
12457
+ return "oscal-ssp";
12458
+ }
12459
+ function sspCategorizationLevel(sil) {
12460
+ const levels = [
12461
+ sil["security-objective-confidentiality"] ?? "",
12462
+ sil["security-objective-integrity"] ?? "",
12463
+ sil["security-objective-availability"] ?? ""
12464
+ ];
12465
+ let highest = "";
12466
+ for (const l of levels) {
12467
+ const normalized = normalizeFIPSLevel(l);
12468
+ if (fipsLevelRank(normalized) > fipsLevelRank(highest)) highest = normalized;
12469
+ }
12470
+ if (!highest) return null;
12471
+ switch (highest) {
12472
+ case "high": return CategorizationLevel.High;
12473
+ case "moderate": return CategorizationLevel.Moderate;
12474
+ case "low": return CategorizationLevel.Low;
12475
+ default: return null;
12476
+ }
12477
+ }
12478
+ function normalizeFIPSLevel(level) {
12479
+ let lower = level.toLowerCase();
12480
+ lower = lower.replace(/^fips-199-/, "");
12481
+ switch (lower) {
12482
+ case "high": return "high";
12483
+ case "moderate":
12484
+ case "medium": return "moderate";
12485
+ case "low": return "low";
12486
+ default: return "";
12487
+ }
12488
+ }
12489
+ function fipsLevelRank(level) {
12490
+ switch (level) {
12491
+ case "high": return 3;
12492
+ case "moderate": return 2;
12493
+ case "low": return 1;
12494
+ default: return 0;
12495
+ }
12496
+ }
12497
+ function mapSensitivityToCategorizationLevel(level) {
12498
+ const normalized = normalizeFIPSLevel(level);
12499
+ if (!normalized) return null;
12500
+ switch (normalized) {
12501
+ case "high": return CategorizationLevel.High;
12502
+ case "moderate": return CategorizationLevel.Moderate;
12503
+ case "low": return CategorizationLevel.Low;
12504
+ default: return null;
12505
+ }
12506
+ }
12507
+ function sspAuthorizationStatus(state) {
12508
+ switch (state.toLowerCase()) {
12509
+ case "operational": return AuthorizationStatus.Authorized;
12510
+ case "under-development": return AuthorizationStatus.PendingAuthorization;
12511
+ case "disposition": return AuthorizationStatus.Revoked;
12512
+ case "other": return AuthorizationStatus.NotYetRequested;
12513
+ default: return null;
12514
+ }
12515
+ }
12516
+ function buildComponentControlMap(ci) {
12517
+ const result = /* @__PURE__ */ new Map();
12518
+ if (!ci) return result;
12519
+ for (const ir of ci["implemented-requirements"] ?? []) {
12520
+ const controlId = ir["control-id"];
12521
+ for (const bc of ir["by-components"] ?? []) addComponentControl(result, bc["component-uuid"], controlId);
12522
+ for (const stmt of ir.statements ?? []) for (const bc of stmt["by-components"] ?? []) addComponentControl(result, bc["component-uuid"], controlId);
12523
+ }
12524
+ return result;
12525
+ }
12526
+ function addComponentControl(m, compUUID, controlId) {
12527
+ let controls = m.get(compUUID);
12528
+ if (!controls) {
12529
+ controls = /* @__PURE__ */ new Set();
12530
+ m.set(compUUID, controls);
12531
+ }
12532
+ controls.add(controlId);
12083
12533
  }
12084
- function buildCodeDesc(f, obsMap) {
12085
- const parts = [];
12086
- for (const ref of f["related-observations"] ?? []) {
12087
- const obsUuid = ref["observation-uuid"];
12088
- if (!obsUuid) continue;
12089
- const obs = obsMap.get(obsUuid);
12090
- if (!obs) continue;
12091
- if (obs.methods && obs.methods.length > 0) parts.push("Methods: " + obs.methods.join(", "));
12092
- for (const subj of obs.subjects ?? []) {
12093
- let subjDesc = subj.type;
12094
- if (subj.title) subjDesc = subj.title + " (" + subj.type + ")";
12095
- parts.push("Subject: " + subjDesc);
12534
+ function sspComponentToHDFComponent(sc, componentControls) {
12535
+ const comp = {
12536
+ name: sc.title,
12537
+ type: mapOSCALComponentType(sc.type)
12538
+ };
12539
+ if (sc.description) comp.description = sc.description;
12540
+ const controls = componentControls.get(sc.uuid);
12541
+ if (controls && controls.size > 0) {
12542
+ const refs = [];
12543
+ for (const controlId of controls) refs.push(controlIdToNistTag(controlId));
12544
+ if (refs.length > 0) {
12545
+ refs.sort();
12546
+ comp.baselineRefs = refs;
12096
12547
  }
12097
12548
  }
12098
- if (parts.length === 0) return f.title;
12099
- return parts.join("; ");
12549
+ return comp;
12100
12550
  }
12101
- function buildRiskMessage(f, riskMap) {
12102
- const messages = [];
12103
- for (const ref of f["related-risks"] ?? []) {
12104
- const riskUuid = ref["risk-uuid"];
12105
- if (!riskUuid) continue;
12106
- const risk = riskMap.get(riskUuid);
12107
- if (!risk) continue;
12108
- let msg = risk.title;
12109
- if (risk.description) msg += ": " + risk.description;
12110
- messages.push(msg);
12551
+ function mapOSCALComponentType(oscalType) {
12552
+ switch (oscalType.toLowerCase()) {
12553
+ case "software":
12554
+ case "this-system": return TargetType.Application;
12555
+ case "service": return TargetType.Application;
12556
+ case "hardware": return TargetType.Host;
12557
+ case "network": return TargetType.Network;
12558
+ case "database": return TargetType.Database;
12559
+ case "storage": return TargetType.Artifact;
12560
+ default: return TargetType.Application;
12111
12561
  }
12112
- return messages.join("\n");
12113
12562
  }
12114
- function sarFindingsImpact(findings, riskMap) {
12115
- let highestImpact = -1;
12116
- for (const f of findings) for (const ref of f["related-risks"] ?? []) {
12117
- const riskUuid = ref["risk-uuid"];
12118
- if (!riskUuid) continue;
12119
- const risk = riskMap.get(riskUuid);
12120
- if (!risk) continue;
12121
- const impact = extractRiskSeverity(risk.characterizations, -1);
12122
- if (impact > highestImpact) highestImpact = impact;
12563
+ //#endregion
12564
+ //#region converters/oscal-to-hdf/typescript/converter-profile.ts
12565
+ /**
12566
+ * OSCAL Profile to HDF Baseline converter.
12567
+ *
12568
+ * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_profile.go.
12569
+ *
12570
+ * This implements the "simple resolver" -- it handles:
12571
+ * - A single catalog import
12572
+ * - include-controls with with-ids filtering
12573
+ * - set-parameters from modify section
12574
+ * - merge as-is (preserve catalog structure)
12575
+ *
12576
+ * It does NOT handle:
12577
+ * - Multiple imports
12578
+ * - Nested profile imports
12579
+ * - alter directives
12580
+ * - Complex merge strategies
12581
+ */
12582
+ /**
12583
+ * Converts an OSCAL Profile against a provided catalog to HDF Baseline JSON.
12584
+ *
12585
+ * @param profileInput - Raw JSON string containing an OSCAL profile
12586
+ * @param catalogInput - Raw JSON string containing an OSCAL catalog
12587
+ * @returns HDF Baseline JSON string
12588
+ */
12589
+ async function convertOscalProfileToHdf(profileInput, catalogInput) {
12590
+ validateInputSize(profileInput, "oscal-profile");
12591
+ if (!profileInput || profileInput.trim().length === 0) throw new Error("empty profile input");
12592
+ if (!catalogInput || catalogInput.trim().length === 0) throw new Error("empty catalog input");
12593
+ const profileDoc = parseJSON(profileInput);
12594
+ if (!profileDoc.profile) throw new Error("oscal-profile: input is not a profile document (root key is not 'profile')");
12595
+ const profile = profileDoc.profile;
12596
+ const catalogDoc = parseJSON(catalogInput);
12597
+ if (!catalogDoc.catalog) throw new Error("oscal-profile: catalog input is not a catalog document (root key is not 'catalog')");
12598
+ const catalog = catalogDoc.catalog;
12599
+ if (!profile.imports || profile.imports.length === 0) throw new Error("oscal-profile: profile has no imports");
12600
+ if (profile.imports.length > 1) throw new Error(`oscal-profile: profile has ${profile.imports.length} imports -- this converter only supports single-catalog imports. Use NIST's oscal-cli to resolve complex profiles, or use a pre-resolved catalog`);
12601
+ if (profile.modify?.alters && profile.modify.alters.length > 0) throw new Error(`oscal-profile: profile contains ${profile.modify.alters.length} alter directives -- this converter only supports parameter overrides. Use NIST's oscal-cli to resolve profiles with alter directives, or use a pre-resolved catalog`);
12602
+ const imp = profile.imports[0];
12603
+ const resolvedCatalog = filterCatalog(catalog, collectIncludedIDs(imp), collectExcludedIDs(imp));
12604
+ if (profile.modify?.["set-parameters"]) applyParameterOverrides(resolvedCatalog, profile.modify["set-parameters"]);
12605
+ resolvedCatalog.metadata = profile.metadata;
12606
+ resolvedCatalog.uuid = profile.uuid;
12607
+ const baseline = await catalogToBaseline(resolvedCatalog, profileInput);
12608
+ baseline.integrity = await inputIntegrity(profileInput);
12609
+ return JSON.stringify(baseline, null, 2);
12610
+ }
12611
+ /** Extracts all control IDs from an import's include-controls. Returns null if include all. */
12612
+ function collectIncludedIDs(imp) {
12613
+ const includeControls = imp["include-controls"];
12614
+ if (!includeControls || includeControls.length === 0) return null;
12615
+ const ids = /* @__PURE__ */ new Set();
12616
+ for (const ic of includeControls) for (const id of ic["with-ids"] ?? []) ids.add(id);
12617
+ return ids;
12618
+ }
12619
+ /** Extracts all control IDs from an import's exclude-controls. */
12620
+ function collectExcludedIDs(imp) {
12621
+ const excludeControls = imp["exclude-controls"];
12622
+ if (!excludeControls || excludeControls.length === 0) return null;
12623
+ const ids = /* @__PURE__ */ new Set();
12624
+ for (const ec of excludeControls) for (const id of ec["with-ids"] ?? []) ids.add(id);
12625
+ return ids;
12626
+ }
12627
+ /** Creates a new catalog containing only the controls matching filters. */
12628
+ function filterCatalog(catalog, includedIDs, excludedIDs) {
12629
+ const includeAll = includedIDs === null;
12630
+ const result = {
12631
+ uuid: catalog.uuid,
12632
+ metadata: catalog.metadata,
12633
+ "back-matter": catalog["back-matter"],
12634
+ groups: [],
12635
+ controls: []
12636
+ };
12637
+ for (const group of catalog.groups ?? []) {
12638
+ const filtered = filterGroupControls(group, includedIDs, excludedIDs, includeAll);
12639
+ if (filtered.controls && filtered.controls.length > 0) result.groups.push(filtered);
12123
12640
  }
12124
- if (highestImpact < 0) return .5;
12125
- return highestImpact;
12641
+ for (const ctrl of catalog.controls ?? []) if (shouldIncludeControl(ctrl.id, includedIDs, excludedIDs, includeAll)) {
12642
+ const filteredCtrl = filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll);
12643
+ result.controls.push(filteredCtrl);
12644
+ }
12645
+ return result;
12126
12646
  }
12127
- function sarBuildDescriptions(findings, obsMap) {
12128
- const descriptions = [];
12129
- const findingDescs = [];
12130
- for (const f of findings) if (f.description) findingDescs.push(f.description);
12131
- descriptions.push({
12132
- label: "default",
12133
- data: findingDescs.join("\n") || ""
12134
- });
12135
- const obsDescs = [];
12136
- const seen = /* @__PURE__ */ new Set();
12137
- for (const f of findings) for (const ref of f["related-observations"] ?? []) {
12138
- const obsUuid = ref["observation-uuid"];
12139
- if (!obsUuid || seen.has(obsUuid)) continue;
12140
- seen.add(obsUuid);
12141
- const obs = obsMap.get(obsUuid);
12142
- if (obs?.description) obsDescs.push(obs.description);
12647
+ function filterGroupControls(group, includedIDs, excludedIDs, includeAll) {
12648
+ const filtered = {
12649
+ id: group.id,
12650
+ class: group.class,
12651
+ title: group.title,
12652
+ props: group.props,
12653
+ parts: group.parts,
12654
+ controls: []
12655
+ };
12656
+ for (const ctrl of group.controls ?? []) if (shouldIncludeControl(ctrl.id, includedIDs, excludedIDs, includeAll)) {
12657
+ const filteredCtrl = filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll);
12658
+ filtered.controls.push(filteredCtrl);
12143
12659
  }
12144
- if (obsDescs.length > 0) descriptions.push({
12145
- label: "rationale",
12146
- data: obsDescs.join("\n")
12147
- });
12148
- return descriptions;
12660
+ return filtered;
12149
12661
  }
12150
- function buildObservationMap(observations) {
12151
- const m = /* @__PURE__ */ new Map();
12152
- for (const obs of observations) m.set(obs.uuid, obs);
12153
- return m;
12662
+ function filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll) {
12663
+ const result = {
12664
+ id: ctrl.id,
12665
+ class: ctrl.class,
12666
+ title: ctrl.title,
12667
+ params: ctrl.params,
12668
+ props: ctrl.props,
12669
+ links: ctrl.links,
12670
+ parts: ctrl.parts,
12671
+ controls: []
12672
+ };
12673
+ for (const enh of ctrl.controls ?? []) if (shouldIncludeControl(enh.id, includedIDs, excludedIDs, includeAll)) result.controls.push(enh);
12674
+ return result;
12154
12675
  }
12155
- function buildRiskMap(risks) {
12156
- const m = /* @__PURE__ */ new Map();
12157
- for (const risk of risks) m.set(risk.uuid, risk);
12158
- return m;
12676
+ function shouldIncludeControl(id, includedIDs, excludedIDs, includeAll) {
12677
+ if (excludedIDs !== null && excludedIDs.has(id)) return false;
12678
+ if (includeAll) return true;
12679
+ return includedIDs !== null && includedIDs.has(id);
12159
12680
  }
12160
- function parseResultStartTime(result) {
12161
- if (result.start) {
12162
- const t = parseTimestamp(String(result.start));
12163
- if (t) return t;
12681
+ /** Applies set-parameters from the profile's modify section to the resolved catalog. */
12682
+ function applyParameterOverrides(catalog, setParams) {
12683
+ if (setParams.length === 0) return;
12684
+ const overrides = /* @__PURE__ */ new Map();
12685
+ for (const sp of setParams) if (sp["param-id"]) overrides.set(sp["param-id"], sp.values ?? []);
12686
+ for (const group of catalog.groups ?? []) for (const ctrl of group.controls ?? []) {
12687
+ applyParamOverridesToControl(ctrl, overrides);
12688
+ for (const enh of ctrl.controls ?? []) applyParamOverridesToControl(enh, overrides);
12689
+ }
12690
+ for (const ctrl of catalog.controls ?? []) {
12691
+ applyParamOverridesToControl(ctrl, overrides);
12692
+ for (const enh of ctrl.controls ?? []) applyParamOverridesToControl(enh, overrides);
12164
12693
  }
12165
- return /* @__PURE__ */ new Date(0);
12166
12694
  }
12167
- function sarBaselineName(result, sar) {
12168
- return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
12695
+ function applyParamOverridesToControl(ctrl, overrides) {
12696
+ if (ctrl.params) for (const param of ctrl.params) {
12697
+ if (!param.id) continue;
12698
+ const values = overrides.get(param.id);
12699
+ if (values) param.label = values.join(", ");
12700
+ }
12701
+ if (ctrl.parts) for (const part of ctrl.parts) replaceParamInsertions(part, overrides);
12702
+ }
12703
+ function replaceParamInsertions(part, overrides) {
12704
+ if (part.prose) part.prose = substituteParams(part.prose, overrides);
12705
+ if (part.parts) for (const child of part.parts) replaceParamInsertions(child, overrides);
12706
+ }
12707
+ /** Replaces OSCAL parameter insertion patterns: {{ insert: param, <param-id> }} */
12708
+ function substituteParams(text, overrides) {
12709
+ let result = text;
12710
+ for (const [paramId, values] of overrides) {
12711
+ const placeholder = `{{ insert: param, ${paramId} }}`;
12712
+ const replacement = values.join(", ");
12713
+ result = result.split(placeholder).join(replacement);
12714
+ }
12715
+ return result;
12169
12716
  }
12170
12717
  //#endregion
12171
- export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToEcs, convertHdfToOcsf, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToSplunk, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
12718
+ export { convertAsffToHdf, convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToAsff, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToEcs, convertHdfToOcsf, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToSplunk, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
12172
12719
 
12173
12720
  //# sourceMappingURL=index.js.map