@mitre/hdf-converters 3.4.0 → 3.4.2

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,352 @@ 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
+ function trivyMessage(f) {
1060
+ const o = f.Resources?.[0]?.Details?.Other;
1061
+ if (!o) return "";
1062
+ if (o["CVE ID"]) {
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
+ if (o["Message"]) {
1068
+ const loc = trivyLocation(o);
1069
+ return loc ? `${o["Message"]} (${loc})` : o["Message"];
1070
+ }
1071
+ if (o["Filename"]) return `Secret detected in ${o["Filename"]}.`;
1072
+ return "";
1073
+ }
1074
+ function trivyLocation(o) {
1075
+ const file = o["Filename"] ?? "";
1076
+ if (!file) return "";
1077
+ const sl = o["StartLine"];
1078
+ if (!sl || sl === "0") return file;
1079
+ let loc = `${file}:${sl}`;
1080
+ const el = o["EndLine"];
1081
+ if (el && el !== "0" && el !== sl) loc += `-${el}`;
1082
+ return loc;
1083
+ }
1084
+ function buildRequirement$17(id, group) {
1085
+ const primary = group[0];
1086
+ const impact = Math.max(...group.map(findingImpact));
1087
+ const descriptions = [{
1088
+ label: "default",
1089
+ data: productOf(primary) === "prowler" ? " " : primary.Description ?? primary.Title ?? ""
1090
+ }];
1091
+ const fix = remediationText(primary);
1092
+ if (fix) descriptions.push({
1093
+ label: "fix",
1094
+ data: fix
1095
+ });
1096
+ const nist = nistTags(group);
1097
+ const tags = nist.length > 0 ? buildNistCciTags(nist, nistToCci(nist)) : {};
1098
+ const results = group.map(buildResult$2);
1099
+ const req = createRequirement(id, primary.Title ?? "", descriptions, impact, results, { tags });
1100
+ const controlType = deriveControlTypeFromTags(nist);
1101
+ if (controlType !== void 0) req.controlType = controlType;
1102
+ const refs = [];
1103
+ if (primary.SourceUrl) refs.push({ url: primary.SourceUrl });
1104
+ const seen = /* @__PURE__ */ new Set();
1105
+ for (const v of primary.Vulnerabilities ?? []) for (const u of v.ReferenceUrls ?? []) if (u && !seen.has(u)) {
1106
+ seen.add(u);
1107
+ refs.push({ url: u });
1108
+ }
1109
+ if (refs.length > 0) req.refs = refs;
1110
+ return req;
1111
+ }
1112
+ function buildBaseline$1(name, findings, resultsChecksum) {
1113
+ const order = [];
1114
+ const groups = /* @__PURE__ */ new Map();
1115
+ for (const f of findings) {
1116
+ const id = controlId(f);
1117
+ const existing = groups.get(id);
1118
+ if (existing) existing.push(f);
1119
+ else {
1120
+ order.push(id);
1121
+ groups.set(id, [f]);
1122
+ }
1123
+ }
1124
+ return createMinimalBaseline(name, order.map((id) => buildRequirement$17(id, groups.get(id))), { resultsChecksum });
1125
+ }
1126
+ /** Converts an ASFF document to HDF Results JSON. */
1127
+ async function convertAsffToHdf(input, converterVersion = "1.0.0") {
1128
+ if (!input || input.trim().length === 0) throw new Error("asff: empty input");
1129
+ validateInputSize(input, "asff");
1130
+ const findings = parseFindings$1(input);
1131
+ const resultsChecksum = await inputChecksum(input);
1132
+ const order = [];
1133
+ const byBaseline = /* @__PURE__ */ new Map();
1134
+ const accounts = [];
1135
+ const seenAccount = /* @__PURE__ */ new Set();
1136
+ for (const f of findings) {
1137
+ const name = baselineName(f);
1138
+ const existing = byBaseline.get(name);
1139
+ if (existing) existing.push(f);
1140
+ else {
1141
+ order.push(name);
1142
+ byBaseline.set(name, [f]);
1143
+ }
1144
+ if (f.AwsAccountId && !seenAccount.has(f.AwsAccountId)) {
1145
+ seenAccount.add(f.AwsAccountId);
1146
+ accounts.push(f.AwsAccountId);
1147
+ }
1148
+ }
1149
+ let baselines = order.map((name) => buildBaseline$1(name, byBaseline.get(name), resultsChecksum));
1150
+ 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 })];
1151
+ const hdf = {
1152
+ baselines,
1153
+ generator: {
1154
+ name: "asff-to-hdf",
1155
+ version: converterVersion
1156
+ },
1157
+ tool: {
1158
+ name: "AWS Security Finding Format",
1159
+ format: "JSON"
1160
+ },
1161
+ timestamp: /* @__PURE__ */ new Date()
1162
+ };
1163
+ if (accounts.length > 0) {
1164
+ const refs = baselines.map((b) => b.name);
1165
+ hdf.components = accounts.map((acct) => ({
1166
+ name: acct,
1167
+ type: TargetType.CloudAccount,
1168
+ baselineRefs: refs
1169
+ }));
1170
+ }
1171
+ return serializeHdf(hdf);
1172
+ }
1173
+ //#endregion
801
1174
  //#region converters/sarif-to-hdf/typescript/converter.ts
802
1175
  const IMPACT_MAPPING$9 = {
803
1176
  error: .7,
@@ -1949,6 +2322,9 @@ function parseCklb(input) {
1949
2322
  return {
1950
2323
  format: "cklb",
1951
2324
  cklbVersion: nz(doc.cklb_version),
2325
+ active: doc.active ?? false,
2326
+ hasPath: doc.has_path ?? false,
2327
+ mode: doc.mode ?? 0,
1952
2328
  asset,
1953
2329
  stigs
1954
2330
  };
@@ -1989,8 +2365,9 @@ function serializeCklb(cl) {
1989
2365
  const doc = {
1990
2366
  title: cklbTitle(cl),
1991
2367
  cklb_version: cl.cklbVersion || "1.0",
1992
- active: false,
1993
- has_path: false,
2368
+ active: cl.active ?? false,
2369
+ ...cl.mode ? { mode: cl.mode } : {},
2370
+ has_path: cl.hasPath ?? false,
1994
2371
  target_data: {
1995
2372
  target_type: cl.asset.assetType || "Computing",
1996
2373
  host_name: cl.asset.hostName ?? "",
@@ -2132,6 +2509,9 @@ function assetToComponent(a) {
2132
2509
  function rootExtensions(cl) {
2133
2510
  const ext = { checklistFormat: cl.format || "ckl" };
2134
2511
  if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
2512
+ if (cl.active) ext["cklbActive"] = true;
2513
+ if (cl.hasPath) ext["cklbHasPath"] = true;
2514
+ if (cl.mode) ext["cklbMode"] = cl.mode;
2135
2515
  const ax = {};
2136
2516
  setIf$1(ax, "role", cl.asset.role);
2137
2517
  setIf$1(ax, "assetType", cl.asset.assetType);
@@ -2178,6 +2558,9 @@ function hdfToChecklist(input) {
2178
2558
  return {
2179
2559
  format,
2180
2560
  cklbVersion: cklbVersion || void 0,
2561
+ active: boolVal(ext, "cklbActive"),
2562
+ hasPath: boolVal(ext, "cklbHasPath"),
2563
+ mode: numVal(ext, "cklbMode"),
2181
2564
  asset,
2182
2565
  stigs
2183
2566
  };
@@ -2279,6 +2662,13 @@ function strVal(m, key) {
2279
2662
  const v = m[key];
2280
2663
  return typeof v === "string" ? v : "";
2281
2664
  }
2665
+ function boolVal(m, key) {
2666
+ return m[key] === true;
2667
+ }
2668
+ function numVal(m, key) {
2669
+ const v = m[key];
2670
+ return typeof v === "number" ? v : 0;
2671
+ }
2282
2672
  function strSlice(m, key) {
2283
2673
  const v = m[key];
2284
2674
  if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
@@ -3471,6 +3861,18 @@ function checkRevisionAlignment(rules) {
3471
3861
  if (isNistStrict()) throw new Error(`aws-config: ${detail}\nre-run with a matching revision, or disable strict mode to convert with the gaps`);
3472
3862
  console.warn(`WARNING: ${detail}`);
3473
3863
  }
3864
+ function buildFixText(rule) {
3865
+ const r = rule.Remediation;
3866
+ if (!r || !r.TargetId) return "";
3867
+ const trigger = r.Automatic ? "automatically on non-compliance" : "on demand";
3868
+ const doc = r.TargetVersion ? `${r.TargetId} (version ${r.TargetVersion})` : r.TargetId;
3869
+ return `Remediate via ${remediationTargetLabel(r.TargetType)} ${doc}, applied ${trigger}.`;
3870
+ }
3871
+ function remediationTargetLabel(targetType) {
3872
+ if (targetType === "SSM_DOCUMENT") return "SSM Automation document";
3873
+ if (!targetType) return "automation document";
3874
+ return `${targetType} document`;
3875
+ }
3474
3876
  function buildCheckText(rule) {
3475
3877
  const parts = [`ARN: ${rule.ConfigRuleArn || "N/A"}`, `Source Identifier: ${rule.Source.SourceIdentifier || "N/A"}`];
3476
3878
  if (rule.InputParameters && rule.InputParameters !== "{}") {
@@ -3497,31 +3899,25 @@ function buildResult(r) {
3497
3899
  const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
3498
3900
  const status = mapComplianceStatus(r.ComplianceType);
3499
3901
  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,
3902
+ return createResult(status, buildResultMessage(codeDesc, r.Annotation, status), {
3505
3903
  codeDesc,
3506
- startTime,
3507
- ...runTime !== void 0 ? { runTime } : {},
3508
- ...message !== void 0 ? { message } : {}
3509
- };
3904
+ startTime: (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
3905
+ runTime: computeRunTime(r.ConfigRuleInvokedTime, r.ResultRecordedTime)
3906
+ });
3510
3907
  }
3511
3908
  /**
3512
3909
  * Synthesizes a single HDF result for a Config rule whose live evaluation
3513
3910
  * returned zero in-scope resources. The HDF schema requires `results` to have
3514
3911
  * minItems >= 1; this honestly signals to auditors that the rule's check ran
3515
3912
  * but had no scope in this account/region rather than vacuously claiming
3516
- * "passed". See issue #80 bug 2.
3913
+ * "passed".
3517
3914
  */
3518
3915
  function buildNotApplicableResult(rule) {
3519
3916
  const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
3520
- return {
3521
- status: ResultStatus.NotApplicable,
3917
+ return createResult(ResultStatus.NotApplicable, void 0, {
3522
3918
  codeDesc,
3523
3919
  startTime: /* @__PURE__ */ new Date()
3524
- };
3920
+ });
3525
3921
  }
3526
3922
  function buildRequirement$15(rule) {
3527
3923
  const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
@@ -3533,6 +3929,11 @@ function buildRequirement$15(rule) {
3533
3929
  label: "check",
3534
3930
  data: buildCheckText(rule)
3535
3931
  }];
3932
+ const fix = buildFixText(rule);
3933
+ if (fix) descriptions.push({
3934
+ label: "fix",
3935
+ data: fix
3936
+ });
3536
3937
  const title = `${getAccountId(rule.ConfigRuleArn)} - ${rule.ConfigRuleName}`;
3537
3938
  const results = rule.EvaluationResults.length > 0 ? rule.EvaluationResults.map(buildResult) : [buildNotApplicableResult(rule)];
3538
3939
  const req = createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
@@ -3618,12 +4019,10 @@ function checkToResult(check, scanTime) {
3618
4019
  const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
3619
4020
  let message;
3620
4021
  if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
3621
- return {
3622
- status,
4022
+ return createResult(status, message, {
3623
4023
  codeDesc,
3624
- startTime: scanTime,
3625
- ...message !== void 0 ? { message } : {}
3626
- };
4024
+ startTime: scanTime
4025
+ });
3627
4026
  }
3628
4027
  /**
3629
4028
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
@@ -4645,472 +5044,202 @@ function parseBom(input) {
4645
5044
  }
4646
5045
  }
4647
5046
  //#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
- ]);
5047
+ //#region shared/typescript/exportmap.ts
4655
5048
  /**
4656
- * Computes the maximum impact across all ratings for a vulnerability.
4657
- * Prefers CVSS score/10 when available, falls back to severityToImpact().
5049
+ * Generic, source-tool-agnostic mapping helpers shared by the HDF export
5050
+ * converters (hdf-to-ecs, hdf-to-splunk, ...).
5051
+ *
5052
+ * The export converters deliberately operate on generically-parsed JSON rather
5053
+ * than typed HDF structs, so their output can be held byte-identical with the
5054
+ * Go implementations. This module centralizes the generic accessors, the status
5055
+ * roll-up, the requirement/document field extraction, and the canonical
5056
+ * (key-sorted, HTML-unescaped) line serialization. Target-specific event shaping
5057
+ * (ECS field names, CIM field names, envelopes) stays in each converter.
4658
5058
  */
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;
5059
+ function asMap(v) {
5060
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
4669
5061
  }
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(", ");
5062
+ function asArr(v) {
5063
+ return Array.isArray(v) ? v : void 0;
4675
5064
  }
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`;
5065
+ function getStr(m, key) {
5066
+ const v = m?.[key];
5067
+ return typeof v === "string" ? v : "";
4687
5068
  }
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));
5069
+ function setIf(m, key, val) {
5070
+ if (val !== "") m[key] = val;
5071
+ }
5072
+ function stringSlice(v) {
5073
+ if (typeof v === "string") return [v];
5074
+ if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
5075
+ return [];
5076
+ }
5077
+ /** Most-significant status across a requirement's results[] (lossless). */
5078
+ function worstOfResults(req) {
5079
+ const results = asArr(req.results) ?? [];
5080
+ const precedence = [
5081
+ "failed",
5082
+ "error",
5083
+ "passed",
5084
+ "notReviewed",
5085
+ "notApplicable"
5086
+ ];
5087
+ const present = /* @__PURE__ */ new Set();
5088
+ for (const rRaw of results) {
5089
+ const r = asMap(rRaw);
5090
+ if (r) present.add(getStr(r, "status"));
4696
5091
  }
4697
- return result;
5092
+ for (const s of precedence) if (present.has(s)) return s;
5093
+ return "notReviewed";
4698
5094
  }
4699
5095
  /**
4700
- * Reports whether any (possibly nested) component is a machine-learning-model,
4701
- * i.e. the CycloneDX document is an AI-BOM.
5096
+ * Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
5097
+ * 'error' is indeterminate (not a compliance failure) and every other value is
5098
+ * non-failing. Single shared definition of "failing" for the suppression axis
5099
+ * and the per-exporter outcome maps.
4702
5100
  */
4703
- function hasMLModelComponent(components) {
4704
- return flattenComponents(components).some((comp) => comp.type === "machine-learning-model");
5101
+ function isFailing(status) {
5102
+ return status === "failed";
4705
5103
  }
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>");
5104
+ /** Resolve the status context for a requirement. */
5105
+ function statusOf(req) {
5106
+ const raw = worstOfResults(req);
5107
+ const effective = getStr(req, "effectiveStatus");
5108
+ const overrides = asArr(req.statusOverrides);
5109
+ const rollup = effective !== "" ? effective : raw;
5110
+ return {
5111
+ raw,
5112
+ effective,
5113
+ rollup,
5114
+ overridden: overrides !== void 0 && overrides.length > 0 || "effectiveStatus" in req,
5115
+ suppressed: isFailing(raw) && !isFailing(rollup)
5116
+ };
5117
+ }
5118
+ function firstComponent(doc) {
5119
+ const comps = asArr(doc.components);
5120
+ if (!comps || comps.length === 0) return void 0;
5121
+ return asMap(comps[0]);
5122
+ }
5123
+ function firstResultStartTime(req, fallback) {
5124
+ const results = asArr(req.results) ?? [];
5125
+ if (results.length > 0) {
5126
+ const st = getStr(asMap(results[0]), "startTime");
5127
+ if (st !== "") return st;
4722
5128
  }
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);
5129
+ return fallback;
5130
+ }
5131
+ function defaultDescription(req) {
5132
+ const descs = asArr(req.descriptions) ?? [];
5133
+ for (const dRaw of descs) {
5134
+ const d = asMap(dRaw);
5135
+ if (d && getStr(d, "label") === "default") return getStr(d, "data");
4775
5136
  }
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
- });
5137
+ return "";
4803
5138
  }
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 });
5139
+ function firstRefURL(req) {
5140
+ const refs = asArr(req.refs) ?? [];
5141
+ for (const rRaw of refs) {
5142
+ const url = getStr(asMap(rRaw), "url");
5143
+ if (url !== "") return url;
5144
+ }
5145
+ return "";
5146
+ }
5147
+ /** Deterministic event id: component | baseline | control. */
5148
+ function eventID(component, baselineName, controlID) {
5149
+ let comp = "";
5150
+ if (component) comp = getStr(component, "componentId") || getStr(component, "name");
5151
+ return [
5152
+ comp,
5153
+ baselineName,
5154
+ controlID
5155
+ ].join("|");
4827
5156
  }
4828
5157
  /**
4829
- * Create a CSV row from baseline, requirement, and target data
5158
+ * Build the lossless hdf.* namespace shared by the export converters: promoted
5159
+ * snake_case scalars plus the full requirement sub-objects preserved verbatim.
5160
+ * `status` is the lossless results roll-up (statusOf().raw); `suppressed` is the
5161
+ * acceptance axis (statusOf().suppressed).
4830
5162
  */
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
5163
+ function buildHDFBlock(req, baseline, status, overridden, suppressed, generator, tool, converterVersion) {
5164
+ const hdf = {
5165
+ status,
5166
+ overridden,
5167
+ suppressed,
5168
+ exporter_version: converterVersion
4867
5169
  };
5170
+ setIf(hdf, "control_id", getStr(req, "id"));
5171
+ setIf(hdf, "baseline", getStr(baseline, "name"));
5172
+ if ("effectiveStatus" in req) hdf.effective_status = req.effectiveStatus;
5173
+ if ("effectiveImpact" in req) hdf.effective_impact = req.effectiveImpact;
5174
+ if ("impact" in req) hdf.impact = req.impact;
5175
+ if ("severity" in req) hdf.severity = req.severity;
5176
+ if ("disposition" in req) hdf.disposition = req.disposition;
5177
+ const tags = asMap(req.tags);
5178
+ if (tags?.nist !== void 0) hdf.nist = tags.nist;
5179
+ if (tags?.cci !== void 0) hdf.cci = tags.cci;
5180
+ for (const [src, dst] of Object.entries({
5181
+ tags: "tags",
5182
+ cvss: "cvss",
5183
+ cwe: "cwe",
5184
+ epss: "epss",
5185
+ kev: "kev",
5186
+ affectedPackages: "affected_packages",
5187
+ descriptions: "descriptions",
5188
+ results: "results",
5189
+ statusOverrides: "status_overrides",
5190
+ poams: "poams",
5191
+ code: "code",
5192
+ refs: "refs"
5193
+ })) if (src in req) hdf[dst] = req[src];
5194
+ if (generator) hdf.generator = generator;
5195
+ if (tool) hdf.tool = tool;
5196
+ return hdf;
4868
5197
  }
4869
5198
  /**
4870
- * Get severity from requirement tags or derive from impact
5199
+ * A numeric value that must serialize with an explicit decimal point, so a
5200
+ * whole-number renders as `10.0` rather than the integer `10`. Some consumers
5201
+ * type-check strictly (OCSF's `float_t` rejects an integer-shaped token).
5202
+ * JSON.stringify cannot emit `10.0` for a JS number, so the value is carried in
5203
+ * this wrapper (passed through canonicalize untouched) and rendered as a bare
5204
+ * numeric token by stringifyLine. Go's counterpart is encoding/json's json.Number.
4871
5205
  */
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
- }
5206
+ var RawNumber = class {
5207
+ constructor(token) {
5208
+ this.token = token;
4879
5209
  }
4880
- const impact = requirement.impact;
4881
- if (impact >= .7) return "high";
4882
- if (impact >= .4) return "medium";
4883
- return "low";
5210
+ };
5211
+ /** Wrap a number as a RawNumber whose token always bears a decimal point. */
5212
+ function floatNumber(f) {
5213
+ let s = String(f);
5214
+ if (!/[.eE]/.test(s)) s += ".0";
5215
+ return new RawNumber(s);
4884
5216
  }
5217
+ const RAWNUM_MARK = String.fromCharCode(1);
5218
+ const RAWNUM_RE = /"\\u0001(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\\u0001"/g;
4885
5219
  /**
4886
- * Extract array values from tags object and join with semicolons
5220
+ * Compare two strings by Unicode code point. This matches Go's `encoding/json`
5221
+ * key ordering, which is a bytewise comparison of the UTF-8 encoding \u2014 and UTF-8
5222
+ * byte order is identical to code-point order. JS's default Array.sort() instead
5223
+ * compares UTF-16 code units, which diverges for supplementary-plane (non-BMP)
5224
+ * characters: a lead surrogate (0xD800\u20130xDBFF) sorts before a BMP char in
5225
+ * [0xE000,0xFFFF] under UTF-16, but a non-BMP code point (\u22650x10000) sorts after
5226
+ * it under UTF-8. Iterating the string yields whole code points (surrogate pairs
5227
+ * combined), so this comparator is correct for the full Unicode range.
4887
5228
  */
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 "";
5229
+ function byCodePoint(a, b) {
5230
+ const ai = [...a];
5231
+ const bi = [...b];
5232
+ const n = Math.min(ai.length, bi.length);
5233
+ for (let i = 0; i < n; i++) {
5234
+ const ca = ai[i]?.codePointAt(0) ?? 0;
5235
+ const cb = bi[i]?.codePointAt(0) ?? 0;
5236
+ if (ca !== cb) return ca - cb;
5237
+ }
5238
+ return ai.length - bi.length;
4893
5239
  }
4894
5240
  /**
4895
- * Get the data of the first description matching a label. Empty when absent.
4896
- */
4897
- function descriptionByLabel(requirement, label) {
4898
- return requirement.descriptions.find((d) => d.label === label)?.data || "";
4899
- }
4900
- /**
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.
5241
+ * Recursively sort object keys in Go's map-key order (bytewise UTF-8 / code
5242
+ * point) so the emitted JSON is byte-identical to Go's encoder.
5114
5243
  */
5115
5244
  function canonicalize$1(v) {
5116
5245
  if (v instanceof RawNumber) return v;
@@ -5194,38 +5323,307 @@ function epochMillis(s) {
5194
5323
  return d.getTime();
5195
5324
  }
5196
5325
  //#endregion
5197
- //#region converters/hdf-to-ecs/typescript/converter.ts
5326
+ //#region converters/cyclonedx-to-hdf/typescript/converter.ts
5327
+ const CVSS_METHODS = /* @__PURE__ */ new Set([
5328
+ "CVSSv2",
5329
+ "CVSSv3",
5330
+ "CVSSv31",
5331
+ "CVSSv4"
5332
+ ]);
5198
5333
  /**
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.
5334
+ * Computes the maximum impact across all ratings for a vulnerability.
5335
+ * Prefers CVSS score/10 when available, falls back to severityToImpact().
5216
5336
  */
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));
5220
- }
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);
5228
- const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
5337
+ function maxImpact(ratings) {
5338
+ if (ratings.length === 0) return .5;
5339
+ let max = 0;
5340
+ for (const rating of ratings) {
5341
+ let impact;
5342
+ if (rating.method && CVSS_METHODS.has(rating.method) && rating.score !== void 0 && rating.score !== null) impact = rating.score / 10;
5343
+ else impact = severityToImpact$1(rating.severity ?? "medium");
5344
+ if (impact > max) max = impact;
5345
+ }
5346
+ return max;
5347
+ }
5348
+ /**
5349
+ * Formats the ratings as a human-readable tag string.
5350
+ */
5351
+ function formatRatingsTag(ratings) {
5352
+ return ratings.map((r) => `${r.source?.name ?? "Unknown"} - ${r.severity ?? "unrated"}`).join(", ");
5353
+ }
5354
+ /**
5355
+ * Formats a component reference as a code_desc string.
5356
+ */
5357
+ function formatCodeDesc$4(componentLookup, ref) {
5358
+ const comp = componentLookup.get(ref);
5359
+ if (!comp) return `Component ${ref} is vulnerable`;
5360
+ let name = "";
5361
+ if (comp.group) name += comp.group + "/";
5362
+ name += comp.name;
5363
+ if (comp.version) name += "@" + comp.version;
5364
+ return `Component ${name} is vulnerable`;
5365
+ }
5366
+ /**
5367
+ * Flattens nested CycloneDX components into a single array.
5368
+ */
5369
+ function flattenComponents(components) {
5370
+ const result = [];
5371
+ for (const comp of components) {
5372
+ result.push(comp);
5373
+ if (comp.components) result.push(...flattenComponents(comp.components));
5374
+ }
5375
+ return result;
5376
+ }
5377
+ /**
5378
+ * Reports whether any (possibly nested) component is a machine-learning-model,
5379
+ * i.e. the CycloneDX document is an AI-BOM.
5380
+ */
5381
+ function hasMLModelComponent(components) {
5382
+ return flattenComponents(components).some((comp) => comp.type === "machine-learning-model");
5383
+ }
5384
+ /**
5385
+ * Converts CycloneDX SBOM/VEX JSON to HDF format.
5386
+ *
5387
+ * @param input - CycloneDX JSON string
5388
+ * @returns HDF JSON string
5389
+ */
5390
+ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
5391
+ if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
5392
+ validateInputSize(input, "cyclonedx");
5393
+ const bom = parseJSON(input);
5394
+ if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
5395
+ if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
5396
+ if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
5397
+ if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) {
5398
+ 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");
5399
+ 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>");
5400
+ }
5401
+ const resultsChecksum = await inputChecksum(input);
5402
+ const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
5403
+ const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
5404
+ const allComponents = flattenComponents(bom.components ?? []);
5405
+ const componentLookup = /* @__PURE__ */ new Map();
5406
+ for (const comp of allComponents) if (comp["bom-ref"]) componentLookup.set(comp["bom-ref"], comp);
5407
+ const requirements = [];
5408
+ const { items: limitedVulns, truncated: truncatedVulns } = limitArray(bom.vulnerabilities ?? []);
5409
+ /* v8 ignore next -- truncation only triggers with >100K items */
5410
+ if (truncatedVulns) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${(bom.vulnerabilities ?? []).length})`);
5411
+ for (const vuln of limitedVulns) {
5412
+ const ratings = vuln.ratings ?? [];
5413
+ const impact = maxImpact(ratings);
5414
+ const cwes = vuln.cwes ?? [];
5415
+ const nist = mapCWEToNIST(cwes.map((c) => `${c}`), DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
5416
+ const tags = {
5417
+ nist,
5418
+ cci: nistToCci(nist)
5419
+ };
5420
+ if (cwes.length > 0) tags["cweid"] = cwes.map((c) => `CWE-${c}`);
5421
+ if (ratings.length > 0) tags["ratings"] = formatRatingsTag(ratings);
5422
+ const descriptions = [];
5423
+ const defaultParts = [];
5424
+ if (vuln.description) defaultParts.push(`Description: ${vuln.description}`);
5425
+ if (vuln.detail) defaultParts.push(`Detail: ${vuln.detail}`);
5426
+ if (defaultParts.length > 0) descriptions.push({
5427
+ label: "default",
5428
+ data: defaultParts.join("\n\n")
5429
+ });
5430
+ else descriptions.push({
5431
+ label: "default",
5432
+ data: vuln.id
5433
+ });
5434
+ const fixParts = [];
5435
+ if (vuln.recommendation) fixParts.push(vuln.recommendation);
5436
+ if (vuln.analysis?.detail) fixParts.push(`Workaround: ${vuln.analysis.detail}`);
5437
+ if (fixParts.length > 0) descriptions.push({
5438
+ label: "fix",
5439
+ data: fixParts.join("\n\n")
5440
+ });
5441
+ const affects = vuln.affects ?? [];
5442
+ const toResult = (codeDesc) => createResult(ResultStatus.Failed, void 0, {
5443
+ codeDesc,
5444
+ startTime: scanTime
5445
+ });
5446
+ const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
5447
+ const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
5448
+ const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
5449
+ const controlType = deriveControlTypeFromTags(nist);
5450
+ if (controlType !== void 0) req.controlType = controlType;
5451
+ requirements.push(req);
5452
+ }
5453
+ const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
5454
+ const targetComponent = {
5455
+ name: bom.metadata?.component?.name ?? "",
5456
+ type: TargetType.Application
5457
+ };
5458
+ if (bom.metadata?.component?.version) targetComponent.version = bom.metadata.component.version;
5459
+ const parsedBom = parseBom(input);
5460
+ const bomParts = {
5461
+ bomType: BOMType.Sbom,
5462
+ format: "cyclonedx",
5463
+ uniqueId: parsedBom.normalized.uniqueId,
5464
+ document: canonicalize$1(JSON.parse(input))
5465
+ };
5466
+ if (parsedBom.normalized.packages && parsedBom.normalized.packages.length > 0) bomParts.packages = parsedBom.normalized.packages;
5467
+ const componentBom = buildBom(bomParts);
5468
+ return buildHdfResults({
5469
+ generatorName: "cyclonedx-to-hdf",
5470
+ converterVersion,
5471
+ toolName: "CycloneDX",
5472
+ toolFormat: "JSON",
5473
+ baselines: [baseline],
5474
+ components: [{
5475
+ ...targetComponent,
5476
+ boms: [componentBom]
5477
+ }],
5478
+ timestamp: scanTime
5479
+ });
5480
+ }
5481
+ //#endregion
5482
+ //#region converters/hdf-to-csv/typescript/converter.ts
5483
+ /**
5484
+ * Convert HDF JSON to CSV format
5485
+ * @param input HDF JSON string
5486
+ * @returns CSV string with sanitized output
5487
+ */
5488
+ function convertHdfToCsv(input) {
5489
+ validateInputSize(input, "hdf-to-csv");
5490
+ const hdf = parseHdf(input);
5491
+ if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
5492
+ if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
5493
+ const rows = [];
5494
+ const components = hdf.components || [];
5495
+ const targetList = components.length > 0 ? components.map((t) => ({
5496
+ name: t.name,
5497
+ type: t.type
5498
+ })) : [{
5499
+ name: "",
5500
+ type: ""
5501
+ }];
5502
+ for (const baseline of hdf.baselines) for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
5503
+ return buildCsv(rows, { sanitize: true });
5504
+ }
5505
+ /**
5506
+ * Create a CSV row from baseline, requirement, and target data
5507
+ */
5508
+ function createRow(baseline, requirement, target) {
5509
+ const description = requirement.descriptions.find((d) => d.label === "default")?.data || "";
5510
+ const check = descriptionByLabel(requirement, "check");
5511
+ const fix = descriptionByLabel(requirement, "fix");
5512
+ const rationale = descriptionByLabel(requirement, "rationale");
5513
+ const code = requirement.code ?? "";
5514
+ const references = flattenRefs(requirement.refs);
5515
+ const severity = getSeverity(requirement);
5516
+ const firstResult = requirement.results[0];
5517
+ const status = firstResult?.status || "";
5518
+ const message = firstResult?.message || "";
5519
+ const nistControls = extractArrayFromTags(requirement.tags, "nist");
5520
+ const cciControls = extractArrayFromTags(requirement.tags, "cci");
5521
+ return {
5522
+ "Baseline ID": baseline.name,
5523
+ "Baseline Version": baseline.version || "",
5524
+ "Baseline Title": baseline.title || "",
5525
+ "Target ID": target.name,
5526
+ "Target Type": target.type,
5527
+ "Requirement ID": requirement.id,
5528
+ "Requirement Title": requirement.title || "",
5529
+ "Description": description,
5530
+ "Check": check,
5531
+ "Fix": fix,
5532
+ "Rationale": rationale,
5533
+ "Code": code,
5534
+ "References": references,
5535
+ "Severity": severity,
5536
+ "Impact": requirement.impact,
5537
+ "Status": String(status),
5538
+ "NIST Controls": nistControls,
5539
+ "CCI Controls": cciControls,
5540
+ "Control Type": requirement.controlType ?? "",
5541
+ "Verification Method": requirement.verificationMethod ?? "",
5542
+ "Applicability": requirement.applicability ?? "",
5543
+ "Result Message": message
5544
+ };
5545
+ }
5546
+ /**
5547
+ * Get severity from requirement tags or derive from impact
5548
+ */
5549
+ function getSeverity(requirement) {
5550
+ if (requirement.tags && typeof requirement.tags === "object") {
5551
+ if ("severity" in requirement.tags) {
5552
+ const sev = requirement.tags.severity;
5553
+ if (typeof sev === "string") return sev;
5554
+ if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
5555
+ }
5556
+ }
5557
+ const impact = requirement.impact;
5558
+ if (impact >= .7) return "high";
5559
+ if (impact >= .4) return "medium";
5560
+ return "low";
5561
+ }
5562
+ /**
5563
+ * Extract array values from tags object and join with semicolons
5564
+ */
5565
+ function extractArrayFromTags(tags, key) {
5566
+ if (!tags || typeof tags !== "object") return "";
5567
+ const value = tags[key];
5568
+ if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
5569
+ return "";
5570
+ }
5571
+ /**
5572
+ * Get the data of the first description matching a label. Empty when absent.
5573
+ */
5574
+ function descriptionByLabel(requirement, label) {
5575
+ return requirement.descriptions.find((d) => d.label === label)?.data || "";
5576
+ }
5577
+ /**
5578
+ * Flatten a requirement's refs to one string: each Reference rendered as its
5579
+ * url/uri (or a string `ref`); array-form refs are skipped. Joined with '; ' to
5580
+ * match the NIST/CCI column convention.
5581
+ */
5582
+ function flattenRefs(refs) {
5583
+ if (!Array.isArray(refs)) return "";
5584
+ const out = [];
5585
+ for (const r of refs) {
5586
+ if (!r || typeof r !== "object") continue;
5587
+ const o = r;
5588
+ if (typeof o.url === "string") out.push(o.url);
5589
+ else if (typeof o.uri === "string") out.push(o.uri);
5590
+ else if (typeof o.ref === "string") out.push(o.ref);
5591
+ }
5592
+ return out.join("; ");
5593
+ }
5594
+ //#endregion
5595
+ //#region converters/hdf-to-ecs/typescript/converter.ts
5596
+ /**
5597
+ * HDF Results -> Elastic Common Schema (ECS) NDJSON exporter.
5598
+ *
5599
+ * One ECS event per Evaluated_Requirement, in the hybrid shape from ADR-0002:
5600
+ * a core-ECS-native projection plus a lossless hdf.* block, hot filter scalars
5601
+ * promoted flat. Output is plain NDJSON (LF-delimited, trailing newline), ECS
5602
+ * 9.4.0.
5603
+ *
5604
+ * Status is raw-primary: event.outcome carries the RAW verdict (a waived failure
5605
+ * is still failure), and hdf.suppressed is the separate acceptance axis. The
5606
+ * canonical "still actionable" query is
5607
+ * event.outcome:"failure" AND hdf.suppressed:false.
5608
+ *
5609
+ * Generic JSON access, the status roll-up, requirement/document field
5610
+ * extraction, and the canonical line serialization are shared with the other
5611
+ * export converters via ../../../shared/typescript/exportmap.js; only
5612
+ * ECS-specific event shaping lives here. Output is held byte-identical with the
5613
+ * Go implementation via key-sorted, HTML-unescaped compact JSON.
5614
+ */
5615
+ const ECS_VERSION = "9.4.0";
5616
+ function convertHdfToEcs(input, converterVersion = "0.1.0") {
5617
+ return runExport(input, "hdf-to-ecs", (req, baseline, docTimestamp, tool, generator, component) => buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5618
+ }
5619
+ function buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5620
+ const st = statusOf(req);
5621
+ const outcome = statusToOutcome(st.raw);
5622
+ const controlID = getStr(req, "id");
5623
+ const baselineName = getStr(baseline, "name");
5624
+ const title = getStr(req, "title");
5625
+ const cvssList = asArr(req.cvss);
5626
+ const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
5229
5627
  const categories = ["configuration"];
5230
5628
  if (hasCVSS) categories.push("vulnerability");
5231
5629
  const event = {
@@ -5383,7 +5781,7 @@ function buildHECEvent(req, baseline, docTimestamp, tool, generator, component,
5383
5781
  const controlID = getStr(req, "id");
5384
5782
  const signature = title !== "" ? title : controlID;
5385
5783
  const dest = destHost(component);
5386
- const sev = severity(req);
5784
+ const sev = severity$1(req);
5387
5785
  const [cvss, hasCVSS] = maxCVSS(req);
5388
5786
  const cve = firstCVE(asArr(req.cvss) ?? []);
5389
5787
  const category = firstCWE(req);
@@ -5426,7 +5824,7 @@ function destHost(component) {
5426
5824
  if (!component) return "";
5427
5825
  return getStr(component, "fqdn") || getStr(component, "name") || getStr(component, "ipAddress");
5428
5826
  }
5429
- function severity(req) {
5827
+ function severity$1(req) {
5430
5828
  if (typeof req.impact === "number") return impactToSeverity(req.impact);
5431
5829
  return normalizeSeverity(getStr(req, "severity"));
5432
5830
  }
@@ -5487,9 +5885,9 @@ const ACTIVITY_CREATE = 1;
5487
5885
  const STATUS_NEW = 1;
5488
5886
  const STATUS_SUPPRESSED = 3;
5489
5887
  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));
5888
+ return runExport(input, "hdf-to-ocsf", (req, baseline, docTimestamp, tool, generator, component) => buildFinding$1(req, baseline, docTimestamp, tool, generator, component, converterVersion));
5491
5889
  }
5492
- function buildFinding(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5890
+ function buildFinding$1(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
5493
5891
  const st = statusOf(req);
5494
5892
  const cvssList = asArr(req.cvss);
5495
5893
  const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
@@ -5697,6 +6095,174 @@ function buildVulnerabilities(cvssList, req) {
5697
6095
  return [vuln];
5698
6096
  }
5699
6097
  //#endregion
6098
+ //#region converters/hdf-to-asff/typescript/converter.ts
6099
+ /**
6100
+ * HDF Results -> AWS Security Finding Format (ASFF), the reverse of
6101
+ * asff-to-hdf. Output is the standard {"Findings": [...]} envelope that
6102
+ * BatchImportFindings accepts and asff-to-hdf reads back.
6103
+ *
6104
+ * One finding per requirement (matching hdf-to-ocsf): a requirement's results
6105
+ * roll up to one Compliance.Status via the shared statusOf. The mapping is
6106
+ * deliberately LOSSY and standard-compliant — HDF structure ASFF cannot hold is
6107
+ * dropped, NOT crammed into Types[] (heimdall2's rejected Types-string
6108
+ * encoding). Provenance rides ProductFields (ASFF's official string map).
6109
+ *
6110
+ * ProductArn / AwsAccountId / Region are ASFF-required but absent from HDF: the
6111
+ * account is recovered from a cloudAccount component when present, otherwise a
6112
+ * placeholder is emitted; the push path overrides all three. Output is held
6113
+ * byte-identical with the Go implementation via canonicalize + stringifyLine.
6114
+ */
6115
+ const ASFF_SCHEMA_VERSION = "2018-10-08";
6116
+ const PLACEHOLDER_REGION = "us-east-1";
6117
+ const PLACEHOLDER_ACCOUNT_ID = "000000000000";
6118
+ const DEFAULT_PARTITION = "aws";
6119
+ const MAX_TITLE = 256;
6120
+ const MAX_DESCRIPTION = 1024;
6121
+ const EPOCH_SENTINEL = "1970-01-01T00:00:00Z";
6122
+ function convertHdfToAsff(input, converterVersion = "0.1.0") {
6123
+ const name = "hdf-to-asff";
6124
+ validateInputSize(input, name);
6125
+ const doc = parseHdf(input);
6126
+ const baselines = asArr(doc.baselines);
6127
+ if (!baselines) throw new Error(`${name}: invalid HDF structure: missing baselines field`);
6128
+ const docTimestamp = getStr(doc, "timestamp");
6129
+ const component = firstComponent(doc);
6130
+ const accountID = recoverAccountID(doc);
6131
+ const productArn = `arn:aws:securityhub:${PLACEHOLDER_REGION}:${accountID}:product/${accountID}/default`;
6132
+ const findings = [];
6133
+ for (const bRaw of baselines) {
6134
+ const baseline = asMap(bRaw);
6135
+ if (!baseline) continue;
6136
+ const baselineName = getStr(baseline, "name");
6137
+ const reqs = asArr(baseline.requirements) ?? [];
6138
+ for (const rRaw of reqs) {
6139
+ const req = asMap(rRaw);
6140
+ if (!req) continue;
6141
+ findings.push(buildFinding(req, baselineName, docTimestamp, component, accountID, productArn, converterVersion));
6142
+ }
6143
+ }
6144
+ return stringifyLine(canonicalize$1({ Findings: findings })) + "\n";
6145
+ }
6146
+ function recoverAccountID(doc) {
6147
+ for (const cRaw of asArr(doc.components) ?? []) {
6148
+ const c = asMap(cRaw);
6149
+ if (!c || getStr(c, "type") !== "cloudAccount") continue;
6150
+ const id = getStr(c, "accountId") || getStr(c, "name");
6151
+ if (id) return id;
6152
+ }
6153
+ return PLACEHOLDER_ACCOUNT_ID;
6154
+ }
6155
+ function buildFinding(req, baselineName, docTimestamp, component, accountID, productArn, converterVersion) {
6156
+ const controlID = getStr(req, "id");
6157
+ const st = statusOf(req);
6158
+ const title = getStr(req, "title") || controlID;
6159
+ const desc = defaultDescription(req) || title;
6160
+ const cvssList = asArr(req.cvss);
6161
+ const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
6162
+ const ts = canonicalTime(firstResultStartTime(req, docTimestamp));
6163
+ const id = findingID(accountID, baselineName, controlID);
6164
+ const finding = {
6165
+ SchemaVersion: ASFF_SCHEMA_VERSION,
6166
+ Id: id,
6167
+ ProductArn: productArn,
6168
+ GeneratorId: controlID,
6169
+ AwsAccountId: accountID,
6170
+ CreatedAt: ts,
6171
+ UpdatedAt: ts,
6172
+ Title: truncate(title, MAX_TITLE),
6173
+ Description: truncate(desc, MAX_DESCRIPTION),
6174
+ Types: asffTypes(hasCVSS),
6175
+ Severity: severity(req),
6176
+ Resources: resources(component, id),
6177
+ RecordState: "ACTIVE",
6178
+ Compliance: { Status: complianceStatus(st.rollup) }
6179
+ };
6180
+ if (st.suppressed) finding.Workflow = { Status: "SUPPRESSED" };
6181
+ const pf = productFields(baselineName, controlID, converterVersion);
6182
+ if (Object.keys(pf).length > 0) finding.ProductFields = pf;
6183
+ const rem = remediation(req);
6184
+ if (rem) finding.Remediation = rem;
6185
+ return finding;
6186
+ }
6187
+ function findingID(accountID, baselineName, controlID) {
6188
+ return [
6189
+ accountID,
6190
+ PLACEHOLDER_REGION,
6191
+ baselineName,
6192
+ controlID
6193
+ ].join("/");
6194
+ }
6195
+ function asffTypes(hasCVSS) {
6196
+ return hasCVSS ? ["Software and Configuration Checks/Vulnerabilities/CVE"] : ["Software and Configuration Checks"];
6197
+ }
6198
+ function complianceStatus(status) {
6199
+ switch (status) {
6200
+ case "passed": return "PASSED";
6201
+ case "failed": return "FAILED";
6202
+ case "notApplicable": return "NOT_AVAILABLE";
6203
+ default: return "WARNING";
6204
+ }
6205
+ }
6206
+ function severity(req) {
6207
+ let label = "INFORMATIONAL";
6208
+ let normalized = 0;
6209
+ if (typeof req.impact === "number") {
6210
+ label = impactToSeverity(req.impact).toUpperCase();
6211
+ normalized = Math.trunc(req.impact * 100);
6212
+ }
6213
+ return {
6214
+ Label: label,
6215
+ Normalized: normalized
6216
+ };
6217
+ }
6218
+ function resources(component, id) {
6219
+ const res = {
6220
+ Type: "Other",
6221
+ Id: id,
6222
+ Partition: DEFAULT_PARTITION,
6223
+ Region: PLACEHOLDER_REGION
6224
+ };
6225
+ if (component) {
6226
+ const details = {};
6227
+ setIf(details, "Name", getStr(component, "name"));
6228
+ setIf(details, "Type", getStr(component, "type"));
6229
+ setIf(details, "IpAddress", getStr(component, "ipAddress"));
6230
+ setIf(details, "OsName", getStr(component, "osName"));
6231
+ if (Object.keys(details).length > 0) res.Details = { Other: details };
6232
+ }
6233
+ return [res];
6234
+ }
6235
+ function productFields(baselineName, controlID, converterVersion) {
6236
+ const pf = {};
6237
+ setIf(pf, "hdf/baseline", baselineName);
6238
+ setIf(pf, "hdf/control_id", controlID);
6239
+ setIf(pf, "hdf/exporter_version", converterVersion);
6240
+ return pf;
6241
+ }
6242
+ function remediation(req) {
6243
+ let fix = "";
6244
+ for (const dRaw of asArr(req.descriptions) ?? []) {
6245
+ const d = asMap(dRaw);
6246
+ if (d && getStr(d, "label") === "fix") {
6247
+ fix = getStr(d, "data");
6248
+ break;
6249
+ }
6250
+ }
6251
+ const url = firstRefURL(req);
6252
+ if (fix === "" && url === "") return void 0;
6253
+ const rec = {};
6254
+ setIf(rec, "Text", truncate(fix, MAX_DESCRIPTION));
6255
+ setIf(rec, "Url", url);
6256
+ return { Recommendation: rec };
6257
+ }
6258
+ function canonicalTime(s) {
6259
+ return parseTimestamp(s) ? s : EPOCH_SENTINEL;
6260
+ }
6261
+ function truncate(s, max) {
6262
+ const r = [...s];
6263
+ return r.length <= max ? s : r.slice(0, max).join("");
6264
+ }
6265
+ //#endregion
5700
6266
  //#region converters/splunk-to-hdf/typescript/converter.ts
5701
6267
  /**
5702
6268
  * Map a Splunk result status string to the HDF ResultStatus enum.
@@ -6282,9 +6848,8 @@ function buildRequirement$12(reqID, findings) {
6282
6848
  * @returns HDF JSON string
6283
6849
  */
6284
6850
  async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
6285
- if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
6286
6851
  validateInputSize(input, "trufflehog");
6287
- const findings = parseFindings(input);
6852
+ const findings = !input || input.trim().length === 0 ? [] : parseFindings(input);
6288
6853
  const resultsChecksum = await inputChecksum(input);
6289
6854
  const limitedFindings = limitArrayWithWarning(findings, "finding");
6290
6855
  const groups = groupFindings(limitedFindings);
@@ -6562,11 +7127,10 @@ function buildRequirement$10(checkID, findings, hasStatus) {
6562
7127
  data: formatDesc$1(rep)
6563
7128
  }];
6564
7129
  const results = findings.map((f) => {
6565
- return {
6566
- status: hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed,
7130
+ return createResult(hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed, void 0, {
6567
7131
  codeDesc: f["Details"] ?? "",
6568
7132
  startTime: parseDate(f["Date"] ?? "")
6569
- };
7133
+ });
6570
7134
  });
6571
7135
  const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
6572
7136
  req.verificationMethod = VerificationMethodEnum.Automated;
@@ -6832,11 +7396,10 @@ function buildRequirement$9(vuln, packageTypes, distro) {
6832
7396
  data: vuln.description
6833
7397
  }];
6834
7398
  const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
6835
- const results = [{
6836
- status: ResultStatus.Failed,
7399
+ const results = [createResult(ResultStatus.Failed, void 0, {
6837
7400
  codeDesc: formatCodeDesc$2(vuln),
6838
7401
  startTime
6839
- }];
7402
+ })];
6840
7403
  const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
6841
7404
  const controlType = deriveControlTypeFromTags(nist);
6842
7405
  if (controlType !== void 0) req.controlType = controlType;
@@ -6963,11 +7526,10 @@ function buildRequirement$8(finding, timestamp) {
6963
7526
  data: finding.vulnerability.recommendation
6964
7527
  });
6965
7528
  const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
6966
- const results = [{
6967
- status: ResultStatus.Failed,
7529
+ const results = [createResult(ResultStatus.Failed, void 0, {
6968
7530
  codeDesc,
6969
7531
  startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
6970
- }];
7532
+ })];
6971
7533
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
6972
7534
  req.verificationMethod = VerificationMethodEnum.Automated;
6973
7535
  const controlType = deriveControlTypeFromTags(nist);
@@ -8098,8 +8660,7 @@ function buildRequirementFromResult(result, filename) {
8098
8660
  const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
8099
8661
  const score = result.result.score;
8100
8662
  const status = determineStatus(score);
8101
- const toResult = (codeDesc) => ({
8102
- status,
8663
+ const toResult = (codeDesc) => createResult(status, void 0, {
8103
8664
  codeDesc,
8104
8665
  startTime
8105
8666
  });
@@ -8344,8 +8905,7 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
8344
8905
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
8345
8906
  const results = cwes.flatMap((c) => {
8346
8907
  const staticflaws = c.staticflaws;
8347
- return ensureArray(staticflaws?.flaw).map((flaw) => ({
8348
- status: ResultStatus.Failed,
8908
+ return ensureArray(staticflaws?.flaw).map((flaw) => createResult(ResultStatus.Failed, void 0, {
8349
8909
  codeDesc: formatFlawCodeDesc(flaw),
8350
8910
  startTime
8351
8911
  }));
@@ -8407,8 +8967,7 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
8407
8967
  if (cweID) extras.cwe = cweID;
8408
8968
  const tags = buildNistCciTags(nist, cciTags, extras);
8409
8969
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
8410
- const results = components.map((comp) => ({
8411
- status: ResultStatus.Failed,
8970
+ const results = components.map((comp) => createResult(ResultStatus.Failed, void 0, {
8412
8971
  codeDesc: formatSCACodeDesc(comp),
8413
8972
  startTime
8414
8973
  }));
@@ -9720,23 +10279,31 @@ function overrideToPOAMItem(override) {
9720
10279
  const remediations = [];
9721
10280
  if (override.milestones) for (const ms of override.milestones) {
9722
10281
  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
10282
  if (ms.status) msProps.push({
9731
10283
  name: "milestone-status",
9732
10284
  value: String(ms.status)
9733
10285
  });
10286
+ let tasks;
10287
+ const d = ms.estimatedCompletion ? toDate(ms.estimatedCompletion) : void 0;
10288
+ if (d) {
10289
+ const eta = formatTimestampSeconds(d);
10290
+ tasks = [{
10291
+ uuid: crypto.randomUUID(),
10292
+ type: "milestone",
10293
+ title: ms.description,
10294
+ timing: { "within-date-range": {
10295
+ start: eta,
10296
+ end: eta
10297
+ } }
10298
+ }];
10299
+ }
9734
10300
  remediations.push({
9735
10301
  uuid: crypto.randomUUID(),
9736
10302
  lifecycle: "planned",
9737
10303
  title: ms.description,
9738
10304
  description: ms.description,
9739
- props: msProps.length > 0 ? msProps : void 0
10305
+ props: msProps.length > 0 ? msProps : void 0,
10306
+ tasks
9740
10307
  });
9741
10308
  }
9742
10309
  let riskLog;
@@ -9748,12 +10315,14 @@ function overrideToPOAMItem(override) {
9748
10315
  start: formatTimestampSeconds(expiresDate),
9749
10316
  "status-change": riskStatus
9750
10317
  }] };
10318
+ const deadline = expiresDate && expiresDate.getTime() > 0 ? formatTimestampSeconds(expiresDate) : void 0;
9751
10319
  const risk = {
9752
10320
  uuid: riskUUID,
9753
10321
  title: override.requirementId,
9754
10322
  description: override.reason,
9755
10323
  statement: override.reason,
9756
10324
  status: riskStatus,
10325
+ deadline,
9757
10326
  props: riskProps,
9758
10327
  remediations: remediations.length > 0 ? remediations : void 0,
9759
10328
  "risk-log": riskLog
@@ -10152,7 +10721,7 @@ function supplierEvidence(sourceURI, description) {
10152
10721
  * arrives; one year is a defensive default consistent with the
10153
10722
  * no-permanent-amendment rule on Standalone_Override. */
10154
10723
  const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
10155
- async function convertOpenVexToHdf(input, converterVersion) {
10724
+ async function convertOpenVexToHdf(input, converterVersion = "1.0.0") {
10156
10725
  validateInputSize(input, "openvex-to-hdf");
10157
10726
  const doc = parseJSON(input);
10158
10727
  const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
@@ -10244,7 +10813,7 @@ function truncateId(id) {
10244
10813
  * Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
10245
10814
  */
10246
10815
  const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
10247
- async function convertCsafVexToHdf(input, converterVersion) {
10816
+ async function convertCsafVexToHdf(input, converterVersion = "1.0.0") {
10248
10817
  validateInputSize(input, "csaf-vex-to-hdf");
10249
10818
  const doc = parseJSON(input);
10250
10819
  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 +11026,7 @@ function overlap(ids, scope) {
10457
11026
  * Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
10458
11027
  */
10459
11028
  const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
10460
- async function convertCyclonedxVexToHdf(input, converterVersion) {
11029
+ async function convertCyclonedxVexToHdf(input, converterVersion = "1.0.0") {
10461
11030
  validateInputSize(input, "cyclonedx-vex-to-hdf");
10462
11031
  const bom = parseJSON(input);
10463
11032
  if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
@@ -11217,245 +11786,91 @@ async function catalogToBaseline(catalog, rawInput) {
11217
11786
  for (const ctrl of group.controls ?? []) {
11218
11787
  const req = controlToBaselineRequirement(ctrl);
11219
11788
  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);
11789
+ reqIDs.push(req.id);
11790
+ for (const enh of ctrl.controls ?? []) {
11791
+ const enhReq = controlToBaselineRequirement(enh);
11792
+ requirements.push(enhReq);
11793
+ reqIDs.push(enhReq.id);
11794
+ }
11795
+ }
11796
+ if (reqIDs.length > 0) groups.push({
11797
+ id: group.id ?? "",
11798
+ title: group.title,
11799
+ requirements: reqIDs
11800
+ });
11383
11801
  }
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);
11802
+ for (const ctrl of catalog.controls ?? []) {
11803
+ const req = controlToBaselineRequirement(ctrl);
11804
+ requirements.push(req);
11805
+ for (const enh of ctrl.controls ?? []) {
11806
+ const enhReq = controlToBaselineRequirement(enh);
11807
+ requirements.push(enhReq);
11808
+ }
11387
11809
  }
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: []
11810
+ return {
11811
+ name: catalogBaselineName(catalog),
11812
+ title: meta.title,
11813
+ version: meta.version,
11814
+ status: "loaded",
11815
+ integrity,
11816
+ requirements,
11817
+ groups,
11818
+ generator: {
11819
+ name: "oscal-catalog-to-hdf",
11820
+ version: "1.0.0"
11821
+ }
11398
11822
  };
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
11823
  }
11405
- function filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll) {
11406
- const result = {
11407
- id: ctrl.id,
11408
- class: ctrl.class,
11824
+ /** Converts a single OSCAL Control to an HDF BaselineRequirement. */
11825
+ function controlToBaselineRequirement(ctrl) {
11826
+ const nistTag = controlIdToNistTag(ctrl.id);
11827
+ const descriptions = buildCatalogDescriptions(ctrl);
11828
+ const tags = buildCatalogTags(ctrl);
11829
+ const req = {
11830
+ id: nistTag,
11409
11831
  title: ctrl.title,
11410
- params: ctrl.params,
11411
- props: ctrl.props,
11412
- links: ctrl.links,
11413
- parts: ctrl.parts,
11414
- controls: []
11832
+ impact: .5,
11833
+ descriptions,
11834
+ tags
11415
11835
  };
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
- }
11836
+ const controlType = deriveControlTypeFromTags([nistTag]);
11837
+ if (controlType !== void 0) req.controlType = controlType;
11838
+ if (extractPropValue(ctrl.props, "CORE") === "true") req.applicability = Applicability.Required;
11839
+ return req;
11437
11840
  }
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);
11841
+ /** Creates HDF Description entries from control parts. */
11842
+ function buildCatalogDescriptions(ctrl) {
11843
+ const descriptions = [];
11844
+ const statement = flattenPartsByName(ctrl.parts, "statement");
11845
+ descriptions.push({
11846
+ label: "default",
11847
+ data: statement || ""
11848
+ });
11849
+ const guidance = flattenPartsByName(ctrl.parts, "guidance");
11850
+ if (guidance) descriptions.push({
11851
+ label: "rationale",
11852
+ data: guidance
11853
+ });
11854
+ const check = flattenPartsByName(ctrl.parts, "assessment-objective");
11855
+ if (check) descriptions.push({
11856
+ label: "check",
11857
+ data: check
11858
+ });
11859
+ return descriptions;
11445
11860
  }
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);
11861
+ /** Builds the tags map for an OSCAL catalog control. */
11862
+ function buildCatalogTags(ctrl) {
11863
+ const tags = {};
11864
+ tags["nist"] = [controlIdToNistTag(ctrl.id)];
11865
+ const label = extractPropValue(ctrl.props, "label");
11866
+ if (label !== void 0) tags["label"] = label;
11867
+ const sortId = extractPropValue(ctrl.props, "sort-id");
11868
+ if (sortId !== void 0) tags["sort-id"] = sortId;
11869
+ return tags;
11449
11870
  }
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;
11871
+ /** Derives a baseline name from catalog metadata. */
11872
+ function catalogBaselineName(catalog) {
11873
+ return toKebabCase(catalog.metadata.title, "oscal-catalog");
11459
11874
  }
11460
11875
  //#endregion
11461
11876
  //#region converters/oscal-to-hdf/typescript/converter-component.ts
@@ -11527,164 +11942,160 @@ function componentBaselineName(comp, compDef) {
11527
11942
  return toKebabCase(comp.title || compDef.metadata.title, "oscal-component-definition");
11528
11943
  }
11529
11944
  //#endregion
11530
- //#region converters/oscal-to-hdf/typescript/converter-ssp.ts
11945
+ //#region converters/oscal-to-hdf/typescript/converter-poam.ts
11531
11946
  /**
11532
- * OSCAL System Security Plan (SSP) to HDF System converter.
11947
+ * OSCAL Plan of Action and Milestones (POA&M) to HDF Amendments converter.
11533
11948
  *
11534
- * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_ssp.go.
11949
+ * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_poam.go.
11535
11950
  */
11536
11951
  /**
11537
- * Converts an OSCAL System Security Plan document to HDF System JSON.
11952
+ * Converts an OSCAL POA&M document to HDF Amendments JSON.
11538
11953
  *
11539
- * @param input - Raw JSON string containing an OSCAL SSP
11540
- * @returns HDF System JSON string
11954
+ * @param input - Raw JSON string containing an OSCAL POA&M
11955
+ * @returns HDF Amendments JSON string
11541
11956
  */
11542
- async function convertOscalSspToHdf(input) {
11543
- validateInputSize(input, "oscal-ssp");
11957
+ async function convertOscalPoamToHdf(input) {
11958
+ validateInputSize(input, "oscal-poam");
11544
11959
  if (!input || input.trim().length === 0) throw new Error("empty input");
11545
11960
  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"];
11961
+ 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')");
11962
+ const poam = doc["plan-of-action-and-milestones"];
11548
11963
  const integrity = await inputIntegrity(input);
11549
- const system = {
11550
- name: sspSystemName(ssp),
11964
+ const meta = extractMetadata(poam.metadata);
11965
+ const riskMap = buildRiskMap$1(poam.risks ?? []);
11966
+ const overrides = [];
11967
+ for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
11968
+ const systemRef = poam["import-ssp"]?.href || void 0;
11969
+ const appliedBy = extractAppliedBy(poam.metadata);
11970
+ return serializeHdf({
11971
+ name: toKebabCase(poam.metadata.title, "oscal-poam"),
11972
+ overrides,
11551
11973
  integrity,
11552
- components: [],
11974
+ systemRef,
11975
+ version: meta.version,
11976
+ appliedBy,
11553
11977
  generator: {
11554
- name: "oscal-ssp-to-hdf",
11978
+ name: "oscal-poam-to-hdf",
11555
11979
  version: "1.0.0"
11556
11980
  }
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);
11981
+ });
11582
11982
  }
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";
11983
+ function buildRiskMap$1(risks) {
11984
+ const m = /* @__PURE__ */ new Map();
11985
+ for (const risk of risks) m.set(risk.uuid, risk);
11986
+ return m;
11588
11987
  }
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
- }
11988
+ function poamItemToOverride(item, riskMap, poam) {
11989
+ const requirementId = extractRequirementIdFromPOAMItem(item, riskMap);
11990
+ return {
11991
+ type: OverrideType.Poam,
11992
+ requirementId,
11993
+ reason: poamItemReason(item),
11994
+ status: poamItemStatus(item, riskMap),
11995
+ appliedBy: poamItemAppliedBy(poam),
11996
+ appliedAt: poamItemAppliedAt(poam, requirementId),
11997
+ expiresAt: poamItemExpiresAt(item, riskMap, requirementId),
11998
+ milestones: extractMilestones(item, riskMap)
11999
+ };
11607
12000
  }
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 "";
12001
+ function extractRequirementIdFromPOAMItem(item, riskMap) {
12002
+ for (const rr of item["related-risks"] ?? []) {
12003
+ const riskUuid = rr["risk-uuid"];
12004
+ if (riskUuid) {
12005
+ const risk = riskMap.get(riskUuid);
12006
+ if (risk) {
12007
+ const controlId = extractPropValue(risk.props, "impacted-control-id");
12008
+ if (controlId) return controlIdToNistTag(controlId);
12009
+ }
12010
+ }
11617
12011
  }
12012
+ const poamId = extractPropValue(item.props, "POAM-ID");
12013
+ if (poamId) return poamId;
12014
+ if (item.title) return item.title;
12015
+ return "unknown";
11618
12016
  }
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
- }
12017
+ function poamItemReason(item) {
12018
+ if (item.description) return item.description;
12019
+ if (item.title) return item.title;
12020
+ return "POA&M item";
11626
12021
  }
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;
12022
+ function poamItemStatus(item, riskMap) {
12023
+ for (const rr of item["related-risks"] ?? []) {
12024
+ const riskUuid = rr["risk-uuid"];
12025
+ if (riskUuid) {
12026
+ const risk = riskMap.get(riskUuid);
12027
+ if (risk) {
12028
+ const status = oscalStatusToHdf(risk.status);
12029
+ if (status === "passed") return "passed";
12030
+ if (status === "failed") return "failed";
12031
+ }
12032
+ }
11635
12033
  }
12034
+ return "failed";
11636
12035
  }
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;
12036
+ function poamItemAppliedBy(poam) {
12037
+ const rps = poam.metadata["responsible-parties"];
12038
+ if (rps) {
12039
+ for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
12040
+ type: "simple",
12041
+ identifier: rp["party-uuids"][0]
12042
+ };
12043
+ if (rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
12044
+ type: "simple",
12045
+ identifier: rps[0]["party-uuids"][0]
12046
+ };
11644
12047
  }
12048
+ return {
12049
+ type: "system",
12050
+ identifier: "oscal-poam-converter"
12051
+ };
11645
12052
  }
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;
12053
+ function poamItemAppliedAt(poam, requirementId) {
12054
+ const t = poam.metadata["last-modified"] && parseTimestamp(String(poam.metadata["last-modified"]));
12055
+ if (t) return t;
12056
+ throw new Error(`poam-item "${requirementId}": no usable metadata.last-modified for appliedAt`);
11655
12057
  }
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);
12058
+ function poamItemExpiresAt(item, riskMap, requirementId) {
12059
+ for (const rr of item["related-risks"] ?? []) {
12060
+ const riskUuid = rr["risk-uuid"];
12061
+ if (!riskUuid) continue;
12062
+ const risk = riskMap.get(riskUuid);
12063
+ if (!risk?.deadline) continue;
12064
+ const t = parseTimestamp(String(risk.deadline));
12065
+ if (t) return t;
11661
12066
  }
11662
- controls.add(controlId);
12067
+ throw new Error(`poam-item "${requirementId}": no related risk carries a usable deadline; a POA&M requires a time commitment`);
11663
12068
  }
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;
12069
+ function extractMilestones(item, riskMap) {
12070
+ const milestones = [];
12071
+ for (const rr of item["related-risks"] ?? []) {
12072
+ const riskUuid = rr["risk-uuid"];
12073
+ if (!riskUuid) continue;
12074
+ const risk = riskMap.get(riskUuid);
12075
+ if (!risk) continue;
12076
+ for (const rem of risk.remediations ?? []) {
12077
+ if (rem.lifecycle !== "planned") continue;
12078
+ for (const task of rem.tasks ?? []) {
12079
+ const end = task.timing?.["within-date-range"]?.end;
12080
+ const estimatedCompletion = end && parseTimestamp(String(end));
12081
+ if (!estimatedCompletion) continue;
12082
+ milestones.push({
12083
+ description: task.description ? `${task.title}: ${task.description}` : task.title,
12084
+ estimatedCompletion,
12085
+ status: "pending"
12086
+ });
12087
+ }
12088
+ }
11675
12089
  }
11676
- return comp;
12090
+ return milestones;
11677
12091
  }
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;
12092
+ function extractAppliedBy(meta) {
12093
+ const rps = meta["responsible-parties"];
12094
+ if (rps) {
12095
+ for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
12096
+ type: "simple",
12097
+ identifier: rp["party-uuids"][0]
12098
+ };
11688
12099
  }
11689
12100
  }
11690
12101
  //#endregion
@@ -11790,181 +12201,32 @@ function buildTargetSelector(ap) {
11790
12201
  const selector = {};
11791
12202
  for (const subject of subjects) {
11792
12203
  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;
12204
+ const key = "subject-type";
12205
+ if (selector[key] !== void 0) selector[key] += "," + subject.type;
12206
+ else selector[key] = subject.type;
12207
+ }
12208
+ if (subject["include-all"] !== void 0) selector["include-" + subject.type] = "all";
11932
12209
  }
11933
- return /* @__PURE__ */ new Date();
11934
- }
11935
- function poamItemExpiresAt() {
11936
- const d = /* @__PURE__ */ new Date();
11937
- d.setFullYear(d.getFullYear() + 1);
11938
- return d;
12210
+ if (Object.keys(selector).length === 0) return null;
12211
+ return selector;
11939
12212
  }
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
- }
12213
+ function determinePlanType(ap) {
12214
+ const aType = extractPropValue(ap.metadata.props, "assessment-type");
12215
+ if (aType) switch (aType.toLowerCase()) {
12216
+ case "automated": return PlanType.Automated;
12217
+ case "manual": return PlanType.Manual;
11957
12218
  }
11958
- return milestones;
12219
+ if (ap.tasks && ap.tasks.length > 0) return PlanType.Hybrid;
11959
12220
  }
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
- };
12221
+ function buildPlanDescription(ap) {
12222
+ const parts = [];
12223
+ if (ap.metadata.remarks) parts.push(ap.metadata.remarks);
12224
+ if (ap["terms-and-conditions"]?.parts && ap["terms-and-conditions"].parts.length > 0) {
12225
+ const terms = flattenParts(ap["terms-and-conditions"].parts);
12226
+ if (terms) parts.push("Terms and Conditions: " + terms);
11967
12227
  }
12228
+ if (parts.length === 0) return void 0;
12229
+ return parts.join("\n\n");
11968
12230
  }
11969
12231
  //#endregion
11970
12232
  //#region converters/oscal-to-hdf/typescript/converter-sar.ts
@@ -12075,99 +12337,417 @@ function extractControlIdFromFinding(f) {
12075
12337
  if (idx > 0) controlId = controlId.slice(0, idx);
12076
12338
  return controlId;
12077
12339
  }
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;
12340
+ function mapFindingStatus(f) {
12341
+ const status = oscalStatusToHdf(f.target.status.state);
12342
+ if (status === "passed") return ResultStatus.Passed;
12343
+ if (status === "failed") return ResultStatus.Failed;
12344
+ return ResultStatus.NotReviewed;
12345
+ }
12346
+ function buildCodeDesc(f, obsMap) {
12347
+ const parts = [];
12348
+ for (const ref of f["related-observations"] ?? []) {
12349
+ const obsUuid = ref["observation-uuid"];
12350
+ if (!obsUuid) continue;
12351
+ const obs = obsMap.get(obsUuid);
12352
+ if (!obs) continue;
12353
+ if (obs.methods && obs.methods.length > 0) parts.push("Methods: " + obs.methods.join(", "));
12354
+ for (const subj of obs.subjects ?? []) {
12355
+ let subjDesc = subj.type;
12356
+ if (subj.title) subjDesc = subj.title + " (" + subj.type + ")";
12357
+ parts.push("Subject: " + subjDesc);
12358
+ }
12359
+ }
12360
+ if (parts.length === 0) return f.title;
12361
+ return parts.join("; ");
12362
+ }
12363
+ function buildRiskMessage(f, riskMap) {
12364
+ const messages = [];
12365
+ for (const ref of f["related-risks"] ?? []) {
12366
+ const riskUuid = ref["risk-uuid"];
12367
+ if (!riskUuid) continue;
12368
+ const risk = riskMap.get(riskUuid);
12369
+ if (!risk) continue;
12370
+ let msg = risk.title;
12371
+ if (risk.description) msg += ": " + risk.description;
12372
+ messages.push(msg);
12373
+ }
12374
+ return messages.join("\n");
12375
+ }
12376
+ function sarFindingsImpact(findings, riskMap) {
12377
+ let highestImpact = -1;
12378
+ for (const f of findings) for (const ref of f["related-risks"] ?? []) {
12379
+ const riskUuid = ref["risk-uuid"];
12380
+ if (!riskUuid) continue;
12381
+ const risk = riskMap.get(riskUuid);
12382
+ if (!risk) continue;
12383
+ const impact = extractRiskSeverity(risk.characterizations, -1);
12384
+ if (impact > highestImpact) highestImpact = impact;
12385
+ }
12386
+ if (highestImpact < 0) return .5;
12387
+ return highestImpact;
12388
+ }
12389
+ function sarBuildDescriptions(findings, obsMap) {
12390
+ const descriptions = [];
12391
+ const findingDescs = [];
12392
+ for (const f of findings) if (f.description) findingDescs.push(f.description);
12393
+ descriptions.push({
12394
+ label: "default",
12395
+ data: findingDescs.join("\n") || ""
12396
+ });
12397
+ const obsDescs = [];
12398
+ const seen = /* @__PURE__ */ new Set();
12399
+ for (const f of findings) for (const ref of f["related-observations"] ?? []) {
12400
+ const obsUuid = ref["observation-uuid"];
12401
+ if (!obsUuid || seen.has(obsUuid)) continue;
12402
+ seen.add(obsUuid);
12403
+ const obs = obsMap.get(obsUuid);
12404
+ if (obs?.description) obsDescs.push(obs.description);
12405
+ }
12406
+ if (obsDescs.length > 0) descriptions.push({
12407
+ label: "rationale",
12408
+ data: obsDescs.join("\n")
12409
+ });
12410
+ return descriptions;
12411
+ }
12412
+ function buildObservationMap(observations) {
12413
+ const m = /* @__PURE__ */ new Map();
12414
+ for (const obs of observations) m.set(obs.uuid, obs);
12415
+ return m;
12416
+ }
12417
+ function buildRiskMap(risks) {
12418
+ const m = /* @__PURE__ */ new Map();
12419
+ for (const risk of risks) m.set(risk.uuid, risk);
12420
+ return m;
12421
+ }
12422
+ function parseResultStartTime(result) {
12423
+ if (result.start) {
12424
+ const t = parseTimestamp(String(result.start));
12425
+ if (t) return t;
12426
+ }
12427
+ return /* @__PURE__ */ new Date(0);
12428
+ }
12429
+ function sarBaselineName(result, sar) {
12430
+ return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
12431
+ }
12432
+ //#endregion
12433
+ //#region converters/oscal-to-hdf/typescript/converter-ssp.ts
12434
+ /**
12435
+ * OSCAL System Security Plan (SSP) to HDF System converter.
12436
+ *
12437
+ * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_ssp.go.
12438
+ */
12439
+ /**
12440
+ * Converts an OSCAL System Security Plan document to HDF System JSON.
12441
+ *
12442
+ * @param input - Raw JSON string containing an OSCAL SSP
12443
+ * @returns HDF System JSON string
12444
+ */
12445
+ async function convertOscalSspToHdf(input) {
12446
+ validateInputSize(input, "oscal-ssp");
12447
+ if (!input || input.trim().length === 0) throw new Error("empty input");
12448
+ const doc = parseJSON(input);
12449
+ 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')");
12450
+ const ssp = doc["system-security-plan"];
12451
+ const integrity = await inputIntegrity(input);
12452
+ const system = {
12453
+ name: sspSystemName(ssp),
12454
+ integrity,
12455
+ components: [],
12456
+ generator: {
12457
+ name: "oscal-ssp-to-hdf",
12458
+ version: "1.0.0"
12459
+ }
12460
+ };
12461
+ const sc = ssp["system-characteristics"];
12462
+ if (sc) {
12463
+ if (sc.description) {
12464
+ let desc = sc.description;
12465
+ if (sc["authorization-boundary"]?.description) desc += "\n\nAuthorization Boundary: " + sc["authorization-boundary"].description;
12466
+ system.description = desc;
12467
+ }
12468
+ const sil = sc["security-impact-level"];
12469
+ if (sil) system.categorizationLevel = sspCategorizationLevel(sil) ?? void 0;
12470
+ else if (sc["security-sensitivity-level"]) system.categorizationLevel = mapSensitivityToCategorizationLevel(sc["security-sensitivity-level"]) ?? void 0;
12471
+ if (sc.status) system.authorizationStatus = sspAuthorizationStatus(sc.status.state) ?? void 0;
12472
+ if (sc["authorization-boundary"]?.description) system.boundaryDescription = sc["authorization-boundary"].description;
12473
+ const systemIds = sc["system-ids"];
12474
+ if (systemIds && systemIds.length > 0) {
12475
+ system.identifier = systemIds[0].id;
12476
+ if (systemIds[0]["identifier-type"]) system.identifierScheme = systemIds[0]["identifier-type"];
12477
+ }
12478
+ }
12479
+ const meta = extractMetadata(ssp.metadata);
12480
+ if (meta.version) system.version = meta.version;
12481
+ const componentControls = buildComponentControlMap(ssp["control-implementation"]);
12482
+ const si = ssp["system-implementation"];
12483
+ if (si?.components) for (const comp of si.components) system.components.push(sspComponentToHDFComponent(comp, componentControls));
12484
+ return JSON.stringify(system, null, 2);
12485
+ }
12486
+ function sspSystemName(ssp) {
12487
+ const sc = ssp["system-characteristics"];
12488
+ if (sc?.["system-name"]) return sc["system-name"];
12489
+ if (ssp.metadata.title) return ssp.metadata.title;
12490
+ return "oscal-ssp";
12491
+ }
12492
+ function sspCategorizationLevel(sil) {
12493
+ const levels = [
12494
+ sil["security-objective-confidentiality"] ?? "",
12495
+ sil["security-objective-integrity"] ?? "",
12496
+ sil["security-objective-availability"] ?? ""
12497
+ ];
12498
+ let highest = "";
12499
+ for (const l of levels) {
12500
+ const normalized = normalizeFIPSLevel(l);
12501
+ if (fipsLevelRank(normalized) > fipsLevelRank(highest)) highest = normalized;
12502
+ }
12503
+ if (!highest) return null;
12504
+ switch (highest) {
12505
+ case "high": return CategorizationLevel.High;
12506
+ case "moderate": return CategorizationLevel.Moderate;
12507
+ case "low": return CategorizationLevel.Low;
12508
+ default: return null;
12509
+ }
12510
+ }
12511
+ function normalizeFIPSLevel(level) {
12512
+ let lower = level.toLowerCase();
12513
+ lower = lower.replace(/^fips-199-/, "");
12514
+ switch (lower) {
12515
+ case "high": return "high";
12516
+ case "moderate":
12517
+ case "medium": return "moderate";
12518
+ case "low": return "low";
12519
+ default: return "";
12520
+ }
12521
+ }
12522
+ function fipsLevelRank(level) {
12523
+ switch (level) {
12524
+ case "high": return 3;
12525
+ case "moderate": return 2;
12526
+ case "low": return 1;
12527
+ default: return 0;
12528
+ }
12529
+ }
12530
+ function mapSensitivityToCategorizationLevel(level) {
12531
+ const normalized = normalizeFIPSLevel(level);
12532
+ if (!normalized) return null;
12533
+ switch (normalized) {
12534
+ case "high": return CategorizationLevel.High;
12535
+ case "moderate": return CategorizationLevel.Moderate;
12536
+ case "low": return CategorizationLevel.Low;
12537
+ default: return null;
12538
+ }
12539
+ }
12540
+ function sspAuthorizationStatus(state) {
12541
+ switch (state.toLowerCase()) {
12542
+ case "operational": return AuthorizationStatus.Authorized;
12543
+ case "under-development": return AuthorizationStatus.PendingAuthorization;
12544
+ case "disposition": return AuthorizationStatus.Revoked;
12545
+ case "other": return AuthorizationStatus.NotYetRequested;
12546
+ default: return null;
12547
+ }
12548
+ }
12549
+ function buildComponentControlMap(ci) {
12550
+ const result = /* @__PURE__ */ new Map();
12551
+ if (!ci) return result;
12552
+ for (const ir of ci["implemented-requirements"] ?? []) {
12553
+ const controlId = ir["control-id"];
12554
+ for (const bc of ir["by-components"] ?? []) addComponentControl(result, bc["component-uuid"], controlId);
12555
+ for (const stmt of ir.statements ?? []) for (const bc of stmt["by-components"] ?? []) addComponentControl(result, bc["component-uuid"], controlId);
12556
+ }
12557
+ return result;
12558
+ }
12559
+ function addComponentControl(m, compUUID, controlId) {
12560
+ let controls = m.get(compUUID);
12561
+ if (!controls) {
12562
+ controls = /* @__PURE__ */ new Set();
12563
+ m.set(compUUID, controls);
12564
+ }
12565
+ controls.add(controlId);
12083
12566
  }
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);
12567
+ function sspComponentToHDFComponent(sc, componentControls) {
12568
+ const comp = {
12569
+ name: sc.title,
12570
+ type: mapOSCALComponentType(sc.type)
12571
+ };
12572
+ if (sc.description) comp.description = sc.description;
12573
+ const controls = componentControls.get(sc.uuid);
12574
+ if (controls && controls.size > 0) {
12575
+ const refs = [];
12576
+ for (const controlId of controls) refs.push(controlIdToNistTag(controlId));
12577
+ if (refs.length > 0) {
12578
+ refs.sort();
12579
+ comp.baselineRefs = refs;
12096
12580
  }
12097
12581
  }
12098
- if (parts.length === 0) return f.title;
12099
- return parts.join("; ");
12582
+ return comp;
12100
12583
  }
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);
12584
+ function mapOSCALComponentType(oscalType) {
12585
+ switch (oscalType.toLowerCase()) {
12586
+ case "software":
12587
+ case "this-system": return TargetType.Application;
12588
+ case "service": return TargetType.Application;
12589
+ case "hardware": return TargetType.Host;
12590
+ case "network": return TargetType.Network;
12591
+ case "database": return TargetType.Database;
12592
+ case "storage": return TargetType.Artifact;
12593
+ default: return TargetType.Application;
12111
12594
  }
12112
- return messages.join("\n");
12113
12595
  }
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;
12596
+ //#endregion
12597
+ //#region converters/oscal-to-hdf/typescript/converter-profile.ts
12598
+ /**
12599
+ * OSCAL Profile to HDF Baseline converter.
12600
+ *
12601
+ * Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_profile.go.
12602
+ *
12603
+ * This implements the "simple resolver" -- it handles:
12604
+ * - A single catalog import
12605
+ * - include-controls with with-ids filtering
12606
+ * - set-parameters from modify section
12607
+ * - merge as-is (preserve catalog structure)
12608
+ *
12609
+ * It does NOT handle:
12610
+ * - Multiple imports
12611
+ * - Nested profile imports
12612
+ * - alter directives
12613
+ * - Complex merge strategies
12614
+ */
12615
+ /**
12616
+ * Converts an OSCAL Profile against a provided catalog to HDF Baseline JSON.
12617
+ *
12618
+ * @param profileInput - Raw JSON string containing an OSCAL profile
12619
+ * @param catalogInput - Raw JSON string containing an OSCAL catalog
12620
+ * @returns HDF Baseline JSON string
12621
+ */
12622
+ async function convertOscalProfileToHdf(profileInput, catalogInput) {
12623
+ validateInputSize(profileInput, "oscal-profile");
12624
+ if (!profileInput || profileInput.trim().length === 0) throw new Error("empty profile input");
12625
+ if (!catalogInput || catalogInput.trim().length === 0) throw new Error("empty catalog input");
12626
+ const profileDoc = parseJSON(profileInput);
12627
+ if (!profileDoc.profile) throw new Error("oscal-profile: input is not a profile document (root key is not 'profile')");
12628
+ const profile = profileDoc.profile;
12629
+ const catalogDoc = parseJSON(catalogInput);
12630
+ if (!catalogDoc.catalog) throw new Error("oscal-profile: catalog input is not a catalog document (root key is not 'catalog')");
12631
+ const catalog = catalogDoc.catalog;
12632
+ if (!profile.imports || profile.imports.length === 0) throw new Error("oscal-profile: profile has no imports");
12633
+ 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`);
12634
+ 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`);
12635
+ const imp = profile.imports[0];
12636
+ const resolvedCatalog = filterCatalog(catalog, collectIncludedIDs(imp), collectExcludedIDs(imp));
12637
+ if (profile.modify?.["set-parameters"]) applyParameterOverrides(resolvedCatalog, profile.modify["set-parameters"]);
12638
+ resolvedCatalog.metadata = profile.metadata;
12639
+ resolvedCatalog.uuid = profile.uuid;
12640
+ const baseline = await catalogToBaseline(resolvedCatalog, profileInput);
12641
+ baseline.integrity = await inputIntegrity(profileInput);
12642
+ return JSON.stringify(baseline, null, 2);
12643
+ }
12644
+ /** Extracts all control IDs from an import's include-controls. Returns null if include all. */
12645
+ function collectIncludedIDs(imp) {
12646
+ const includeControls = imp["include-controls"];
12647
+ if (!includeControls || includeControls.length === 0) return null;
12648
+ const ids = /* @__PURE__ */ new Set();
12649
+ for (const ic of includeControls) for (const id of ic["with-ids"] ?? []) ids.add(id);
12650
+ return ids;
12651
+ }
12652
+ /** Extracts all control IDs from an import's exclude-controls. */
12653
+ function collectExcludedIDs(imp) {
12654
+ const excludeControls = imp["exclude-controls"];
12655
+ if (!excludeControls || excludeControls.length === 0) return null;
12656
+ const ids = /* @__PURE__ */ new Set();
12657
+ for (const ec of excludeControls) for (const id of ec["with-ids"] ?? []) ids.add(id);
12658
+ return ids;
12659
+ }
12660
+ /** Creates a new catalog containing only the controls matching filters. */
12661
+ function filterCatalog(catalog, includedIDs, excludedIDs) {
12662
+ const includeAll = includedIDs === null;
12663
+ const result = {
12664
+ uuid: catalog.uuid,
12665
+ metadata: catalog.metadata,
12666
+ "back-matter": catalog["back-matter"],
12667
+ groups: [],
12668
+ controls: []
12669
+ };
12670
+ for (const group of catalog.groups ?? []) {
12671
+ const filtered = filterGroupControls(group, includedIDs, excludedIDs, includeAll);
12672
+ if (filtered.controls && filtered.controls.length > 0) result.groups.push(filtered);
12123
12673
  }
12124
- if (highestImpact < 0) return .5;
12125
- return highestImpact;
12674
+ for (const ctrl of catalog.controls ?? []) if (shouldIncludeControl(ctrl.id, includedIDs, excludedIDs, includeAll)) {
12675
+ const filteredCtrl = filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll);
12676
+ result.controls.push(filteredCtrl);
12677
+ }
12678
+ return result;
12126
12679
  }
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);
12680
+ function filterGroupControls(group, includedIDs, excludedIDs, includeAll) {
12681
+ const filtered = {
12682
+ id: group.id,
12683
+ class: group.class,
12684
+ title: group.title,
12685
+ props: group.props,
12686
+ parts: group.parts,
12687
+ controls: []
12688
+ };
12689
+ for (const ctrl of group.controls ?? []) if (shouldIncludeControl(ctrl.id, includedIDs, excludedIDs, includeAll)) {
12690
+ const filteredCtrl = filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll);
12691
+ filtered.controls.push(filteredCtrl);
12143
12692
  }
12144
- if (obsDescs.length > 0) descriptions.push({
12145
- label: "rationale",
12146
- data: obsDescs.join("\n")
12147
- });
12148
- return descriptions;
12693
+ return filtered;
12149
12694
  }
12150
- function buildObservationMap(observations) {
12151
- const m = /* @__PURE__ */ new Map();
12152
- for (const obs of observations) m.set(obs.uuid, obs);
12153
- return m;
12695
+ function filterControlEnhancements(ctrl, includedIDs, excludedIDs, includeAll) {
12696
+ const result = {
12697
+ id: ctrl.id,
12698
+ class: ctrl.class,
12699
+ title: ctrl.title,
12700
+ params: ctrl.params,
12701
+ props: ctrl.props,
12702
+ links: ctrl.links,
12703
+ parts: ctrl.parts,
12704
+ controls: []
12705
+ };
12706
+ for (const enh of ctrl.controls ?? []) if (shouldIncludeControl(enh.id, includedIDs, excludedIDs, includeAll)) result.controls.push(enh);
12707
+ return result;
12154
12708
  }
12155
- function buildRiskMap(risks) {
12156
- const m = /* @__PURE__ */ new Map();
12157
- for (const risk of risks) m.set(risk.uuid, risk);
12158
- return m;
12709
+ function shouldIncludeControl(id, includedIDs, excludedIDs, includeAll) {
12710
+ if (excludedIDs !== null && excludedIDs.has(id)) return false;
12711
+ if (includeAll) return true;
12712
+ return includedIDs !== null && includedIDs.has(id);
12159
12713
  }
12160
- function parseResultStartTime(result) {
12161
- if (result.start) {
12162
- const t = parseTimestamp(String(result.start));
12163
- if (t) return t;
12714
+ /** Applies set-parameters from the profile's modify section to the resolved catalog. */
12715
+ function applyParameterOverrides(catalog, setParams) {
12716
+ if (setParams.length === 0) return;
12717
+ const overrides = /* @__PURE__ */ new Map();
12718
+ for (const sp of setParams) if (sp["param-id"]) overrides.set(sp["param-id"], sp.values ?? []);
12719
+ for (const group of catalog.groups ?? []) for (const ctrl of group.controls ?? []) {
12720
+ applyParamOverridesToControl(ctrl, overrides);
12721
+ for (const enh of ctrl.controls ?? []) applyParamOverridesToControl(enh, overrides);
12722
+ }
12723
+ for (const ctrl of catalog.controls ?? []) {
12724
+ applyParamOverridesToControl(ctrl, overrides);
12725
+ for (const enh of ctrl.controls ?? []) applyParamOverridesToControl(enh, overrides);
12164
12726
  }
12165
- return /* @__PURE__ */ new Date(0);
12166
12727
  }
12167
- function sarBaselineName(result, sar) {
12168
- return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
12728
+ function applyParamOverridesToControl(ctrl, overrides) {
12729
+ if (ctrl.params) for (const param of ctrl.params) {
12730
+ if (!param.id) continue;
12731
+ const values = overrides.get(param.id);
12732
+ if (values) param.label = values.join(", ");
12733
+ }
12734
+ if (ctrl.parts) for (const part of ctrl.parts) replaceParamInsertions(part, overrides);
12735
+ }
12736
+ function replaceParamInsertions(part, overrides) {
12737
+ if (part.prose) part.prose = substituteParams(part.prose, overrides);
12738
+ if (part.parts) for (const child of part.parts) replaceParamInsertions(child, overrides);
12739
+ }
12740
+ /** Replaces OSCAL parameter insertion patterns: {{ insert: param, <param-id> }} */
12741
+ function substituteParams(text, overrides) {
12742
+ let result = text;
12743
+ for (const [paramId, values] of overrides) {
12744
+ const placeholder = `{{ insert: param, ${paramId} }}`;
12745
+ const replacement = values.join(", ");
12746
+ result = result.split(placeholder).join(replacement);
12747
+ }
12748
+ return result;
12169
12749
  }
12170
12750
  //#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 };
12751
+ 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
12752
 
12173
12753
  //# sourceMappingURL=index.js.map