@mitre/hdf-converters 3.4.3 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as detectConverter, t as registerAllFingerprints } from "./register-all-WRR-vAZf.js";
2
2
  import { flattenOverlays } from "@mitre/hdf-parsers";
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
- 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";
3
+ import { buildCsv, buildXml, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, roundImpact, severityToImpact, sha256, stripHtml as stripHTML, trimUtcFraction, worstStatus } from "@mitre/hdf-utilities";
4
+ import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact as severityToImpact$1 } from "@mitre/hdf-schema";
5
5
  import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_CONFIG_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getHipcheckNistControls, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
6
6
  //#region shared/typescript/converterutil.ts
7
7
  /**
@@ -150,6 +150,7 @@ const DEFAULT_MAX_INPUT_SIZE = 50 * 1024 * 1024;
150
150
  * @throws Error if input exceeds maxSize
151
151
  */
152
152
  function validateInputSize(input, converterName, maxSize = DEFAULT_MAX_INPUT_SIZE) {
153
+ if (maxSize <= 0) maxSize = DEFAULT_MAX_INPUT_SIZE;
153
154
  if (input.length > maxSize) throw new Error(`${converterName}: input exceeds maximum allowed size of ${maxSize} characters`);
154
155
  }
155
156
  /**
@@ -487,35 +488,15 @@ function normalizeStatus$1(status) {
487
488
  }[status] || "notReviewed";
488
489
  }
489
490
  /**
490
- * Compute effectiveStatus from impact and v2 results.
491
- * Implements InSpec enhanced outcomes precedence:
492
- * impact=0 → notApplicable
493
- * error > failed > passed > notApplicable > notReviewed
494
- *
495
- * See docs/design/status-determination.md for full specification.
491
+ * Compute effectiveStatus from impact and v2 results via the canonical
492
+ * worst-wins ordering in @mitre/hdf-utilities (impact=0 → notApplicable;
493
+ * error > failed > passed > notApplicable > notReviewed).
496
494
  */
497
- function computeEffectiveStatus(impact, results) {
498
- if (impact === 0) return "notApplicable";
499
- if (results.length === 0) return "notReviewed";
500
- let hasFailed = false;
501
- let hasPassed = false;
502
- let hasNotApplicable = false;
503
- for (const r of results) switch (r.status) {
504
- case "error": return "error";
505
- case "failed":
506
- hasFailed = true;
507
- break;
508
- case "passed":
509
- hasPassed = true;
510
- break;
511
- case "notApplicable":
512
- hasNotApplicable = true;
513
- break;
514
- }
515
- if (hasFailed) return "failed";
516
- if (hasPassed) return "passed";
517
- if (hasNotApplicable) return "notApplicable";
518
- return "notReviewed";
495
+ function computeEffectiveStatus$1(impact, results) {
496
+ return computeEffectiveStatus({
497
+ impact,
498
+ resultStatuses: results.map((r) => r.status)
499
+ });
519
500
  }
520
501
  /**
521
502
  * Convert v1.0 result to v2.0 result.
@@ -613,7 +594,7 @@ function convertControl(v1Control) {
613
594
  if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
614
595
  if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
615
596
  if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
616
- if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
597
+ if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus$1(v1Control.impact, v2Req.results ?? []);
617
598
  v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity(v1Control.impact);
618
599
  const rawNist = v1Control.tags?.nist;
619
600
  if (Array.isArray(rawNist)) {
@@ -753,31 +734,47 @@ function convertProfile(v1Profile) {
753
734
  if (v1Profile.depends?.length) v2Baseline.depends = v1Profile.depends.map(convertDependency);
754
735
  return v2Baseline;
755
736
  }
737
+ /** Parse the leading major-version integer; -1 when the string has none. */
738
+ function inspecMajor(version) {
739
+ const major = /^(\d+)/.exec(version ?? "")?.[1];
740
+ return major !== void 0 ? parseInt(major, 10) : -1;
741
+ }
756
742
  /**
757
- * Convert HDF v1.0 results to v2.0 format.
758
- *
759
- * Performs comprehensive transformation at all levels:
760
- * - Top-level: version removed, profiles → baselines, platform → components
761
- * - Baselines: sha256 → checksum, controls → requirements, field renaming
762
- * - Requirements: snake_case → camelCase, status → effectiveStatus
763
- * - Results: snake_case → camelCase for all fields
764
- *
765
- * @param v1Data - HDF v1.0 results object
766
- * @returns HDF v2.0 results object
767
- *
768
- * @example
769
- * ```typescript
770
- * const v1 = {
771
- * version: "1.0.0",
772
- * platform: { name: "ubuntu", release: "20.04" },
773
- * profiles: [...],
774
- * statistics: {...}
775
- * };
776
- * const v2 = convertV1ToV2(v1);
777
- * // v2 = { baselines: [...], components: [{...}], statistics: {...} }
778
- * ```
779
- */
780
- function convertV1ToV2(v1Data) {
743
+ * Map the source's top-level version to tool metadata. A genuine InSpec
744
+ * exec-json run carries the InSpec CLI version (major >= 2 for every modern
745
+ * release), so those flip the tool to InSpec/exec-json. A legacy HDF v1
746
+ * document with no InSpec provenance (e.g. a bare "1.0.0" format marker) keeps
747
+ * the historical label.
748
+ */
749
+ function toolIdentity(version) {
750
+ if (inspecMajor(version) >= 2) return {
751
+ name: "InSpec",
752
+ version,
753
+ format: "exec-json"
754
+ };
755
+ return { name: "Heimdall Data Format v1" };
756
+ }
757
+ /**
758
+ * The assessment's execution time for the top-level timestamp. An explicit
759
+ * source timestamp wins; otherwise it is the latest (last-observed) result
760
+ * start_time — the assessment's effective as-of instant, the value
761
+ * `hdf events derive` consumes. Returns undefined only when the source carries
762
+ * no usable time; never the wall clock. Canonical trimmed-UTC form.
763
+ */
764
+ function documentTimestamp(v1Data) {
765
+ if (v1Data.timestamp) {
766
+ const t = parseTimestamp(v1Data.timestamp);
767
+ if (t) return formatTimestamp(t);
768
+ }
769
+ let latest;
770
+ for (const profile of v1Data.profiles || []) for (const control of profile.controls || []) for (const result of control.results || []) {
771
+ if (!result.start_time) continue;
772
+ const t = parseTimestamp(result.start_time);
773
+ if (t && (!latest || t > latest)) latest = t;
774
+ }
775
+ return latest ? formatTimestamp(latest) : void 0;
776
+ }
777
+ function convertV1ToV2(v1Data, converterVersion = "1.0.0") {
781
778
  validateInputSize(JSON.stringify(v1Data), "legacyhdf-to-hdf");
782
779
  const v2 = {
783
780
  baselines: (v1Data.profiles || []).map(convertProfile),
@@ -794,9 +791,13 @@ function convertV1ToV2(v1Data) {
794
791
  }
795
792
  v2.components = [component];
796
793
  }
797
- if (v1Data.generator) v2.generator = v1Data.generator;
798
- v2.tool = { name: "Heimdall Data Format v1" };
799
- if (v1Data.timestamp) v2.timestamp = v1Data.timestamp;
794
+ v2.generator = v1Data.generator ?? {
795
+ name: "legacyhdf-to-hdf",
796
+ version: converterVersion
797
+ };
798
+ v2.tool = toolIdentity(v1Data.version);
799
+ const timestamp = documentTimestamp(v1Data);
800
+ if (timestamp) v2.timestamp = timestamp;
800
801
  const knownV1Fields = /* @__PURE__ */ new Set([
801
802
  "version",
802
803
  "platform",
@@ -825,6 +826,63 @@ function isHDFV1(data) {
825
826
  return typeof obj.version === "string" && Array.isArray(obj.profiles) && typeof obj.platform === "object" && obj.platform !== null;
826
827
  }
827
828
  //#endregion
829
+ //#region shared/typescript/cvss.ts
830
+ /**
831
+ * Returns the schema CVSS Version enum for a vector-string prefix (CVSS:2.0/,
832
+ * CVSS:3.0/, CVSS:4.0/). An absent or unrecognized prefix (including CVSS:3.1/)
833
+ * defaults to 3.1 — the version modern scanners emit most often.
834
+ */
835
+ function cvssVersionFromVector(vector) {
836
+ if (!vector) return Version.The31;
837
+ if (vector.startsWith("CVSS:2.0/")) return Version.The20;
838
+ if (vector.startsWith("CVSS:3.0/")) return Version.The30;
839
+ if (vector.startsWith("CVSS:4.0/")) return Version.The40;
840
+ return Version.The31;
841
+ }
842
+ /**
843
+ * Maps a bare CVSS version number ("2.0"/"3.0"/"3.1"/"4.0") — as emitted in a
844
+ * structured version field rather than a vector prefix — to the schema Version
845
+ * enum. Unrecognized values default to 3.1.
846
+ */
847
+ function cvssVersionFromString(v) {
848
+ switch (v) {
849
+ case "2.0": return Version.The20;
850
+ case "3.0": return Version.The30;
851
+ case "4.0": return Version.The40;
852
+ default: return Version.The31;
853
+ }
854
+ }
855
+ /**
856
+ * Maps a CVSS base/computed score to the schema CVSSSeverity enum via the shared
857
+ * band thresholds in cvssScoreToSeverity. Scores below the low threshold (and
858
+ * non-finite inputs, per cvssScoreToSeverity) map to the "none" band.
859
+ */
860
+ function cvssSeverityFromScore(score) {
861
+ switch (cvssScoreToSeverity(score)) {
862
+ case "critical": return CVSSSeverity.Critical;
863
+ case "high": return CVSSSeverity.High;
864
+ case "medium": return CVSSSeverity.Medium;
865
+ case "low": return CVSSSeverity.Low;
866
+ default: return CVSSSeverity.None;
867
+ }
868
+ }
869
+ /**
870
+ * Assembles a Cvss from the base-metric fields a converter provides. version is
871
+ * always set. baseScore (and its derived baseSeverity) is set only when a finite
872
+ * number is supplied; baseVector and source are set only when non-empty. Consumers
873
+ * needing threat/environmental/computed enrichment set those fields on the result.
874
+ */
875
+ function buildCvss$2(input) {
876
+ const cv = { version: input.version };
877
+ if (typeof input.baseScore === "number" && Number.isFinite(input.baseScore)) {
878
+ cv.baseScore = input.baseScore;
879
+ cv.baseSeverity = cvssSeverityFromScore(input.baseScore);
880
+ }
881
+ if (input.baseVector) cv.baseVector = input.baseVector;
882
+ if (input.source) cv.source = input.source;
883
+ return cv;
884
+ }
885
+ //#endregion
828
886
  //#region converters/asff-to-hdf/typescript/converter.ts
829
887
  /**
830
888
  * AWS Security Finding Format (ASFF) to HDF converter.
@@ -889,14 +947,17 @@ function severityLabelToImpact(label) {
889
947
  /**
890
948
  * findingImpact derives a 0.0–1.0 impact. Suppressed findings are forced to 0.
891
949
  * Security Hub's INFORMATIONAL is up-graded to MEDIUM (Security Hub over-marks
892
- * findings INFORMATIONAL without context).
950
+ * findings INFORMATIONAL without context). FindingProviderFields.Severity — the
951
+ * provider's own rating — is preferred over the top-level Severity, which
952
+ * Security Hub may overwrite on ingest.
893
953
  */
894
954
  function findingImpact(f) {
895
955
  if (f.Workflow?.Status === "SUPPRESSED") return 0;
896
- let label = f.Severity?.Label;
956
+ const sev = f.FindingProviderFields?.Severity?.Label ? f.FindingProviderFields.Severity : f.Severity;
957
+ let label = sev?.Label;
897
958
  if (isSecurityHub(f) && (label ?? "").toUpperCase() === "INFORMATIONAL") label = "MEDIUM";
898
959
  if (label) return severityLabelToImpact(label);
899
- if (typeof f.Severity?.Normalized === "number") return f.Severity.Normalized / 100;
960
+ if (typeof sev?.Normalized === "number") return sev.Normalized / 100;
900
961
  return 0;
901
962
  }
902
963
  function productArnParts(arn) {
@@ -1061,6 +1122,37 @@ function vulnerabilitySummary(f) {
1061
1122
  return parts.join("; ");
1062
1123
  }).join("\n");
1063
1124
  }
1125
+ /**
1126
+ * Assembles requirement.cvss[] from a finding's ASFF Vulnerabilities[]. Each Cvss
1127
+ * entry carrying a BaseScore becomes one structured Cvss via the shared builder.
1128
+ * ASFF's Cvss shape has no vector string, so only version, score, and source map;
1129
+ * an entry without a BaseScore contributes nothing.
1130
+ */
1131
+ function vulnCvss(f) {
1132
+ const out = [];
1133
+ for (const v of f.Vulnerabilities ?? []) for (const c of v.Cvss ?? []) {
1134
+ if (typeof c.BaseScore !== "number") continue;
1135
+ out.push(buildCvss$2({
1136
+ version: cvssVersionFromString(c.Version),
1137
+ baseScore: c.BaseScore,
1138
+ source: c.Source
1139
+ }));
1140
+ }
1141
+ return out;
1142
+ }
1143
+ /**
1144
+ * Collects the CVE ids a finding's Vulnerabilities[] carry (dedup, first-seen
1145
+ * order) for tags.cve. ASFF stores the CVE in Vulnerabilities[].Id.
1146
+ */
1147
+ function vulnCves(f) {
1148
+ const out = [];
1149
+ const seen = /* @__PURE__ */ new Set();
1150
+ for (const v of f.Vulnerabilities ?? []) if (v.Id && !seen.has(v.Id)) {
1151
+ seen.add(v.Id);
1152
+ out.push(v.Id);
1153
+ }
1154
+ return out;
1155
+ }
1064
1156
  function trivyMessage(f) {
1065
1157
  const o = f.Resources?.[0]?.Details?.Other;
1066
1158
  if (!o) return "";
@@ -1100,10 +1192,15 @@ function buildRequirement$17(id, group) {
1100
1192
  });
1101
1193
  const nist = nistTags$1(group);
1102
1194
  const tags = nist.length > 0 ? buildNistCciTags(nist, nistToCci(nist)) : {};
1195
+ const cves = vulnCves(primary);
1196
+ if (cves.length > 0) tags.cve = cves;
1103
1197
  const results = group.map(buildResult$2);
1104
1198
  const req = createRequirement(id, primary.Title ?? "", descriptions, impact, results, { tags });
1105
1199
  const controlType = deriveControlTypeFromTags(nist);
1106
1200
  if (controlType !== void 0) req.controlType = controlType;
1201
+ const cvss = vulnCvss(primary);
1202
+ if (cvss.length > 0) req.cvss = cvss;
1203
+ req.code = JSON.stringify(primary, null, 2);
1107
1204
  const refs = [];
1108
1205
  if (primary.SourceUrl) refs.push({ url: primary.SourceUrl });
1109
1206
  const seen = /* @__PURE__ */ new Set();
@@ -1159,10 +1256,7 @@ async function convertAsffToHdf(input, converterVersion = "1.0.0") {
1159
1256
  name: "asff-to-hdf",
1160
1257
  version: converterVersion
1161
1258
  },
1162
- tool: {
1163
- name: "AWS Security Finding Format",
1164
- format: "JSON"
1165
- },
1259
+ tool: { name: "AWS Security Finding Format" },
1166
1260
  timestamp: /* @__PURE__ */ new Date()
1167
1261
  };
1168
1262
  if (accounts.length > 0) {
@@ -1264,6 +1358,8 @@ function convertResultGroup(ruleId, rule, sarifResults) {
1264
1358
  const controlType = deriveControlTypeFromTags(nistControls);
1265
1359
  if (controlType !== void 0) req.controlType = controlType;
1266
1360
  req.verificationMethod = VerificationMethodEnum.Automated;
1361
+ const snippet = (firstResult.locations ?? []).map((loc) => loc.physicalLocation?.region?.snippet?.text).find((text) => Boolean(text));
1362
+ if (snippet) req.code = snippet;
1267
1363
  const seenKeys = /* @__PURE__ */ new Set();
1268
1364
  const packages = [];
1269
1365
  for (const sr of sarifResults) {
@@ -1518,7 +1614,6 @@ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
1518
1614
  generatorName: "junit-to-hdf",
1519
1615
  converterVersion,
1520
1616
  toolName: "JUnit XML",
1521
- toolFormat: "XML",
1522
1617
  baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
1523
1618
  components: [{
1524
1619
  type: TargetType.Application,
@@ -1570,7 +1665,7 @@ function testCaseToRequirement(tc, scanTime) {
1570
1665
  const { status, message } = resolveStatus(tc);
1571
1666
  const result = {
1572
1667
  status,
1573
- codeDesc: buildCodeDesc$9(tc),
1668
+ codeDesc: buildCodeDesc$10(tc),
1574
1669
  startTime: scanTime
1575
1670
  };
1576
1671
  if (message !== void 0) result.message = message;
@@ -1627,7 +1722,7 @@ function buildFailureMessage(message, typeName, body) {
1627
1722
  if (body) result += "\n" + decodeXmlEntities$5(body);
1628
1723
  return result;
1629
1724
  }
1630
- function buildCodeDesc$9(tc) {
1725
+ function buildCodeDesc$10(tc) {
1631
1726
  if (tc.classname) return `${decodeXmlEntities$5(tc.classname)} :: ${decodeXmlEntities$5(tc.name)}`;
1632
1727
  return decodeXmlEntities$5(tc.name);
1633
1728
  }
@@ -1639,6 +1734,25 @@ function noFindingsTarget(baselineName, suites) {
1639
1734
  //#endregion
1640
1735
  //#region converters/xccdf-results-to-hdf/typescript/converter.ts
1641
1736
  /**
1737
+ * Map XCCDF's severity vocabulary (unknown|info|low|medium|high) onto HDF's
1738
+ * (critical|high|medium|low|informational). Casting the raw attribute straight to
1739
+ * Severity emitted schema-invalid documents, since XCCDF "unknown" and "info" are
1740
+ * not HDF severities.
1741
+ *
1742
+ * "unknown" states that severity was not determined. HDF cannot express that, so
1743
+ * emit no severity at all rather than assert "informational", which would silently
1744
+ * downgrade a rule whose severity is merely unstated.
1745
+ */
1746
+ function xccdfSeverityToHdf(severity) {
1747
+ switch (severity.toLowerCase()) {
1748
+ case "high": return Severity.High;
1749
+ case "medium": return Severity.Medium;
1750
+ case "low": return Severity.Low;
1751
+ case "info": return Severity.Informational;
1752
+ default: return;
1753
+ }
1754
+ }
1755
+ /**
1642
1756
  * Walk the Group tree depth-first, collecting every rule at any depth. Rules may
1643
1757
  * sit directly on a Group that also has nested Groups.
1644
1758
  */
@@ -1883,6 +1997,12 @@ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
1883
1997
  label: "default",
1884
1998
  data: stripHTML(extractVulnDiscussion(extractText(ruleDef?.description)))
1885
1999
  }];
2000
+ const check = selectCheck(ruleDef?.check);
2001
+ const checkContent = extractCheckContent(check);
2002
+ if (checkContent) descriptions.push({
2003
+ label: "check",
2004
+ data: stripHTML(checkContent)
2005
+ });
1886
2006
  const fixtext = extractFixtext(ruleDef?.fixtext);
1887
2007
  if (fixtext) descriptions.push({
1888
2008
  label: "fix",
@@ -1899,6 +2019,10 @@ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
1899
2019
  const tags = buildCciNistTags(extractCCIs([...rr.ident ?? [], ...ruleDef?.ident ?? []]));
1900
2020
  const nistTags = tags["nist"];
1901
2021
  const req = createRequirement(id, title, descriptions, impact, [result], { tags });
2022
+ const code = buildCheckCode(check);
2023
+ if (code) req.code = code;
2024
+ const hdfSeverity = xccdfSeverityToHdf(severity);
2025
+ if (hdfSeverity) req.severity = hdfSeverity;
1902
2026
  const controlType = deriveControlTypeFromTags(nistTags);
1903
2027
  if (controlType !== void 0) req.controlType = controlType;
1904
2028
  return req;
@@ -1947,6 +2071,35 @@ function decodeXmlEntities$4(s) {
1947
2071
  return name !== void 0 ? NAMED_ENTITIES$4[name] ?? match : match;
1948
2072
  });
1949
2073
  }
2074
+ /**
2075
+ * Extract plain text from a field that may be a string or {#text: string}.
2076
+ */
2077
+ /**
2078
+ * A rule can carry several <check> elements (SSG emits SCE, OVAL and OCIL for the
2079
+ * same rule). Go decodes <check> into one struct field, so encoding/xml overwrites
2080
+ * it and the last element wins; match that or the two languages disagree.
2081
+ */
2082
+ function checkPriority(system) {
2083
+ const s = system ?? "";
2084
+ if (s.includes("oval.mitre.org")) return 0;
2085
+ if (s.includes("open-scap.org") || s.includes("/SCE")) return 1;
2086
+ if (s.includes("ocil")) return 2;
2087
+ return 3;
2088
+ }
2089
+ function selectCheck(check) {
2090
+ if (check === void 0) return void 0;
2091
+ const checks = Array.isArray(check) ? check : [check];
2092
+ let best;
2093
+ let bestPri = Infinity;
2094
+ for (const c of checks) {
2095
+ const p = checkPriority(c.system);
2096
+ if (p < bestPri) {
2097
+ best = c;
2098
+ bestPri = p;
2099
+ }
2100
+ }
2101
+ return best;
2102
+ }
1950
2103
  function extractText(field) {
1951
2104
  if (field === void 0 || field === null) return "";
1952
2105
  if (typeof field === "string") return decodeXmlEntities$4(field);
@@ -1969,6 +2122,67 @@ function extractFixtext(field) {
1969
2122
  return decodeXmlEntities$4(field["#text"] ?? "");
1970
2123
  }
1971
2124
  /**
2125
+ * Extract check-content text from a check element.
2126
+ */
2127
+ function extractCheckContent(check) {
2128
+ if (!check) return "";
2129
+ const cc = check["check-content"];
2130
+ if (!cc) return "";
2131
+ if (typeof cc === "string") return decodeXmlEntities$4(cc);
2132
+ if (Array.isArray(cc)) {
2133
+ const first = cc[0];
2134
+ if (typeof first === "string") return decodeXmlEntities$4(first);
2135
+ if (first && typeof first === "object" && "#text" in first) return decodeXmlEntities$4(first["#text"] ?? "");
2136
+ return "";
2137
+ }
2138
+ return decodeXmlEntities$4(cc["#text"] ?? "");
2139
+ }
2140
+ /**
2141
+ * Extract the OVAL/SCE check-content-ref (name + href) from a check element.
2142
+ * check-content-ref is not forced into an array, so handle both single and
2143
+ * (rare) multi cases, taking the first.
2144
+ */
2145
+ function extractCheckContentRef(check) {
2146
+ if (!check) return {
2147
+ name: "",
2148
+ href: ""
2149
+ };
2150
+ let ref = check["check-content-ref"];
2151
+ if (Array.isArray(ref)) ref = ref[0];
2152
+ if (!ref || typeof ref !== "object") return {
2153
+ name: "",
2154
+ href: ""
2155
+ };
2156
+ return {
2157
+ name: ref.name ?? "",
2158
+ href: ref.href ?? ""
2159
+ };
2160
+ }
2161
+ /**
2162
+ * Render a rule's selected <check> as the indented-JSON blob carried in
2163
+ * requirement.code — the automated-check logic (system + OVAL/SCE definition
2164
+ * reference + any inline content). Returns '' when the check is empty so the
2165
+ * caller leaves code unset rather than fabricating one. Key order and the
2166
+ * escape-free JSON.stringify mirror the Go buildCheckCode for byte parity.
2167
+ */
2168
+ function buildCheckCode(check) {
2169
+ if (!check) return "";
2170
+ const system = check.system ?? "";
2171
+ const ref = extractCheckContentRef(check);
2172
+ const content = extractCheckContent(check).trim();
2173
+ if (!system && !ref.name && !ref.href && !content) return "";
2174
+ const code = {};
2175
+ if (system) code.system = system;
2176
+ if (ref.name || ref.href) {
2177
+ const refObj = {};
2178
+ if (ref.name) refObj.name = ref.name;
2179
+ if (ref.href) refObj.href = ref.href;
2180
+ code.checkContentRef = refObj;
2181
+ }
2182
+ if (content) code.checkContent = content;
2183
+ return JSON.stringify(code, null, 2);
2184
+ }
2185
+ /**
1972
2186
  * Extract the VulnDiscussion text from an XCCDF description that contains
1973
2187
  * embedded XML like `<VulnDiscussion>...</VulnDiscussion>`. If no
1974
2188
  * VulnDiscussion tag is found, returns the original text.
@@ -2783,6 +2997,19 @@ function synthesizePurl(ecosystem, name, version) {
2783
2997
  if (ecosystem === Ecosystem.Generic) return void 0;
2784
2998
  return `pkg:${ecosystem}/${name}@${version}`;
2785
2999
  }
3000
+ /**
3001
+ * Assembles the structured cvss[] entry for a Snyk vulnerability from its
3002
+ * cvssScore (base score) and CVSSv3 (base vector, carrying a CVSS:3.1/ prefix).
3003
+ * Returns [] when the source carries neither so the field is omitted.
3004
+ */
3005
+ function buildSnykCvss(vuln) {
3006
+ if (!vuln.cvssScore && !vuln.CVSSv3) return [];
3007
+ return [buildCvss$2({
3008
+ version: cvssVersionFromVector(vuln.CVSSv3),
3009
+ baseScore: vuln.cvssScore,
3010
+ baseVector: vuln.CVSSv3
3011
+ })];
3012
+ }
2786
3013
  function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2787
3014
  const rep = vulns[0];
2788
3015
  const cweIDs = rep.identifiers.CWE ?? [];
@@ -2791,8 +3018,7 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2791
3018
  nist,
2792
3019
  cci: nistToCci(nist)
2793
3020
  };
2794
- if (cweIDs.length > 0) tags["cweid"] = cweIDs;
2795
- if (rep.identifiers.CVE && rep.identifiers.CVE.length > 0) tags["cveid"] = rep.identifiers.CVE;
3021
+ if (rep.identifiers.CVE && rep.identifiers.CVE.length > 0) tags["cve"] = rep.identifiers.CVE;
2796
3022
  if (rep.identifiers.GHSA && rep.identifiers.GHSA.length > 0) tags["ghsaid"] = rep.identifiers.GHSA;
2797
3023
  const descriptions = [{
2798
3024
  label: "default",
@@ -2810,6 +3036,9 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
2810
3036
  const controlType = deriveControlTypeFromTags(nist);
2811
3037
  if (controlType !== void 0) req.controlType = controlType;
2812
3038
  req.verificationMethod = VerificationMethodEnum.Automated;
3039
+ const cvss = buildSnykCvss(rep);
3040
+ if (cvss.length > 0) req.cvss = cvss;
3041
+ if (cweIDs.length > 0) req.cwe = cweIDs;
2813
3042
  const name = rep.packageName ?? rep.moduleName;
2814
3043
  const version = rep.version;
2815
3044
  if (name && version) {
@@ -2885,7 +3114,6 @@ async function convertSnykToHdf(input, converterVersion = "1.0.0") {
2885
3114
  generatorName: "snyk-to-hdf",
2886
3115
  converterVersion,
2887
3116
  toolName: "Snyk",
2888
- toolFormat: "JSON",
2889
3117
  baselines,
2890
3118
  components: [{
2891
3119
  name: targetName,
@@ -2944,7 +3172,7 @@ function getReferences(vuln, relatedVulns) {
2944
3172
  }
2945
3173
  return Array.from(refs);
2946
3174
  }
2947
- function buildCodeDesc$8(match) {
3175
+ function buildCodeDesc$9(match) {
2948
3176
  const parts = [];
2949
3177
  parts.push(`Package: ${match.artifact.name}@${match.artifact.version}`);
2950
3178
  if (match.artifact.type) parts.push(`Type: ${match.artifact.type}`);
@@ -2958,35 +3186,16 @@ function buildCodeDesc$8(match) {
2958
3186
  }
2959
3187
  return parts.join(" | ");
2960
3188
  }
2961
- function cvssVersionToSchema(v) {
2962
- switch (v) {
2963
- case "2.0": return Version.The20;
2964
- case "3.0": return Version.The30;
2965
- case "4.0": return Version.The40;
2966
- default: return Version.The31;
2967
- }
2968
- }
2969
- function cvssBandSeverity(score) {
2970
- switch (cvssScoreToSeverity(score)) {
2971
- case "critical": return CVSSSeverity.Critical;
2972
- case "high": return CVSSSeverity.High;
2973
- case "medium": return CVSSSeverity.Medium;
2974
- case "low": return CVSSSeverity.Low;
2975
- default: return CVSSSeverity.None;
2976
- }
2977
- }
2978
- function buildCvssEntries$1(vuln) {
3189
+ function buildCvssEntries$5(vuln) {
2979
3190
  if (!vuln.cvss || vuln.cvss.length === 0) return;
2980
3191
  const entries = [];
2981
3192
  for (const c of vuln.cvss) {
2982
- const entry = { version: cvssVersionToSchema(c.version) };
2983
- if (c.vector) entry.baseVector = c.vector;
2984
- const score = c.metrics?.baseScore;
2985
- if (typeof score === "number" && Number.isFinite(score)) {
2986
- entry.baseScore = score;
2987
- entry.baseSeverity = cvssBandSeverity(score);
2988
- }
2989
- if (vuln.id) entry.source = vuln.id;
3193
+ const entry = buildCvss$2({
3194
+ version: cvssVersionFromString(c.version),
3195
+ baseScore: c.metrics?.baseScore,
3196
+ baseVector: c.vector,
3197
+ source: vuln.id
3198
+ });
2990
3199
  if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
2991
3200
  entries.push(entry);
2992
3201
  }
@@ -3020,10 +3229,10 @@ function buildAffectedPackages(match) {
3020
3229
  if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
3021
3230
  return [pkg];
3022
3231
  }
3023
- const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
3232
+ const CWE_ID_PATTERN$1 = /^CWE-[1-9]\d*$/;
3024
3233
  function extractCwe(raw) {
3025
3234
  if (!raw || raw.length === 0) return void 0;
3026
- const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
3235
+ const out = raw.filter((c) => CWE_ID_PATTERN$1.test(c));
3027
3236
  return out.length === 0 ? void 0 : out;
3028
3237
  }
3029
3238
  function buildEpss$2(entries) {
@@ -3044,7 +3253,7 @@ function buildKev(k) {
3044
3253
  if (k.notes) out.notes = k.notes;
3045
3254
  return out;
3046
3255
  }
3047
- function convertMatchToRequirement(match, isIgnored) {
3256
+ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
3048
3257
  const vuln = match.vulnerability;
3049
3258
  const cveId = vuln.id;
3050
3259
  const severity = vuln.severity;
@@ -3065,14 +3274,16 @@ function convertMatchToRequirement(match, isIgnored) {
3065
3274
  const message = messageParts.join(" ");
3066
3275
  const result = {
3067
3276
  status,
3068
- codeDesc: buildCodeDesc$8(match),
3277
+ codeDesc: buildCodeDesc$9(match),
3069
3278
  message,
3070
- startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
3279
+ startTime
3071
3280
  };
3072
3281
  const tags = buildNistCciTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS, nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS));
3073
3282
  const requirement = {
3074
3283
  id: isIgnored ? `Grype-Ignored-Match/${cveId}` : `Grype/${cveId}`,
3284
+ title: `Grype found a vulnerability to ${cveId} in ${targetName}`,
3075
3285
  impact,
3286
+ code: JSON.stringify(match, null, 2),
3076
3287
  results: [result],
3077
3288
  tags,
3078
3289
  descriptions: [
@@ -3094,7 +3305,7 @@ function convertMatchToRequirement(match, isIgnored) {
3094
3305
  };
3095
3306
  const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
3096
3307
  if (controlType !== void 0) requirement.controlType = controlType;
3097
- const cvss = buildCvssEntries$1(vuln);
3308
+ const cvss = buildCvssEntries$5(vuln);
3098
3309
  if (cvss) requirement.cvss = cvss;
3099
3310
  requirement.affectedPackages = buildAffectedPackages(match);
3100
3311
  const cwe = extractCwe(vuln.cwe);
@@ -3110,19 +3321,21 @@ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
3110
3321
  const resultsChecksum = await inputChecksum(input);
3111
3322
  const grypeData = parseJSON(input);
3112
3323
  const requirements = [];
3324
+ const targetName = grypeData.source?.target?.userInput || "Grype Scan";
3325
+ const scanTime = grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null;
3326
+ const resultStart = scanTime ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
3113
3327
  if (grypeData.matches && grypeData.matches.length > 0) {
3114
3328
  const { items: limitedMatches, truncated: truncatedMatches } = limitArray(grypeData.matches);
3115
3329
  /* v8 ignore next -- truncation only triggers with >100K items */
3116
3330
  if (truncatedMatches) console.warn(`WARNING: Input truncated at ${limitedMatches.length} match items (original: ${grypeData.matches.length})`);
3117
- for (const match of limitedMatches) requirements.push(convertMatchToRequirement(match, false));
3331
+ for (const match of limitedMatches) requirements.push(convertMatchToRequirement(match, false, targetName, resultStart));
3118
3332
  }
3119
3333
  if (grypeData.ignoredMatches && grypeData.ignoredMatches.length > 0) {
3120
3334
  const { items: limitedIgnored, truncated: truncatedIgnored } = limitArray(grypeData.ignoredMatches);
3121
3335
  /* v8 ignore next -- truncation only triggers with >100K items */
3122
3336
  if (truncatedIgnored) console.warn(`WARNING: Input truncated at ${limitedIgnored.length} ignoredMatch items (original: ${grypeData.ignoredMatches.length})`);
3123
- for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
3337
+ for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true, targetName, resultStart));
3124
3338
  }
3125
- const targetName = grypeData.source?.target?.userInput || "Grype Scan";
3126
3339
  if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
3127
3340
  const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
3128
3341
  return buildHdfResults({
@@ -3135,7 +3348,7 @@ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
3135
3348
  type: TargetType.Artifact,
3136
3349
  name: targetName
3137
3350
  }],
3138
- timestamp: (grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null) ?? /* @__PURE__ */ new Date()
3351
+ timestamp: scanTime ?? void 0
3139
3352
  });
3140
3353
  }
3141
3354
  //#endregion
@@ -3200,26 +3413,15 @@ function findingId(f) {
3200
3413
  function cveList(f) {
3201
3414
  return (f.vulnerability_ids ?? []).map((v) => v.vulnerability_id).filter(Boolean);
3202
3415
  }
3203
- function cvssBand(score) {
3204
- switch (cvssScoreToSeverity(score)) {
3205
- case "critical": return CVSSSeverity.Critical;
3206
- case "high": return CVSSSeverity.High;
3207
- case "medium": return CVSSSeverity.Medium;
3208
- case "low": return CVSSSeverity.Low;
3209
- default: return CVSSSeverity.None;
3210
- }
3211
- }
3212
3416
  function buildCvss$1(f) {
3213
3417
  const out = [];
3214
3418
  const add = (version, vector, score) => {
3215
3419
  if ((score === void 0 || score === null) && !vector) return;
3216
- const entry = { version };
3217
- if (score !== void 0 && score !== null) {
3218
- entry.baseScore = score;
3219
- entry.baseSeverity = cvssBand(score);
3220
- }
3221
- if (vector) entry.baseVector = vector;
3222
- out.push(entry);
3420
+ out.push(buildCvss$2({
3421
+ version,
3422
+ baseScore: score,
3423
+ baseVector: vector
3424
+ }));
3223
3425
  };
3224
3426
  add(Version.The31, f.cvssv3, f.cvssv3_score);
3225
3427
  add(Version.The40, f.cvssv4, f.cvssv4_score);
@@ -3262,6 +3464,8 @@ function codeDesc(f) {
3262
3464
  function convertFinding(f) {
3263
3465
  const nist = nistTags(f);
3264
3466
  const tags = buildNistCciTags(nist, nistToCci(nist), triageTags(f));
3467
+ const cves = cveList(f);
3468
+ if (cves.length > 0) tags.cve = cves;
3265
3469
  const result = {
3266
3470
  status: deriveStatus(f),
3267
3471
  codeDesc: codeDesc(f),
@@ -3281,6 +3485,7 @@ function convertFinding(f) {
3281
3485
  const epss = buildEpss$1(f);
3282
3486
  if (epss) req.epss = epss;
3283
3487
  if (f.cwe && f.cwe > 0) req.cwe = [`CWE-${f.cwe}`];
3488
+ req.code = JSON.stringify(f, null, 2);
3284
3489
  const firstRisk = f.accepted_risks?.[0];
3285
3490
  if (f.risk_accepted && firstRisk) {
3286
3491
  req.statusOverrides = [buildWaiverOverride(firstRisk)];
@@ -3489,7 +3694,7 @@ function convertReportItemToRequirement(item, host) {
3489
3694
  if (controlType !== void 0) req.controlType = controlType;
3490
3695
  if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
3491
3696
  if (!isCompliance) {
3492
- const cvssEntries = buildCvssEntries(item);
3697
+ const cvssEntries = buildCvssEntries$4(item);
3493
3698
  if (cvssEntries.length > 0) req.cvss = cvssEntries;
3494
3699
  const cweIDs = buildCweIDs(item);
3495
3700
  if (cweIDs.length > 0) req.cwe = cweIDs;
@@ -3503,7 +3708,7 @@ function convertReportItemToRequirement(item, host) {
3503
3708
  * the schema models cvss as a multi-entry array (Nessus emits one entry per
3504
3709
  * item; multi-vendor convergence may yield more).
3505
3710
  */
3506
- function buildCvssEntries(item) {
3711
+ function buildCvssEntries$4(item) {
3507
3712
  const source = (item.cvss_score_source || "").trim();
3508
3713
  if (!source || !CVE_SOURCE_RE.test(source)) return [];
3509
3714
  const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
@@ -3534,15 +3739,13 @@ function buildCvssEntries(item) {
3534
3739
  if (baseVector) entry.baseVector = baseVector;
3535
3740
  if (baseScore !== void 0) {
3536
3741
  entry.baseScore = baseScore;
3537
- const baseSeverity = mapCvssSeverity(baseScore);
3538
- if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
3742
+ entry.baseSeverity = cvssSeverityFromScore(baseScore);
3539
3743
  }
3540
3744
  if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
3541
3745
  if (threatScore !== void 0) {
3542
3746
  entry.threatScore = threatScore;
3543
3747
  entry.computedScore = threatScore;
3544
- const computedSeverity = mapCvssSeverity(threatScore);
3545
- if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
3748
+ entry.computedSeverity = cvssSeverityFromScore(threatScore);
3546
3749
  }
3547
3750
  return [entry];
3548
3751
  }
@@ -3573,16 +3776,6 @@ function parseFloatOrUndef(s) {
3573
3776
  const f = Number.parseFloat(s);
3574
3777
  return Number.isFinite(f) ? f : void 0;
3575
3778
  }
3576
- function mapCvssSeverity(score) {
3577
- switch (cvssScoreToSeverity(score)) {
3578
- case "critical": return CVSSSeverity.Critical;
3579
- case "high": return CVSSSeverity.High;
3580
- case "medium": return CVSSSeverity.Medium;
3581
- case "low": return CVSSSeverity.Low;
3582
- case "none": return CVSSSeverity.None;
3583
- default: return;
3584
- }
3585
- }
3586
3779
  /**
3587
3780
  * Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
3588
3781
  * numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
@@ -4069,7 +4262,7 @@ function buildCheckText(rule) {
4069
4262
  }
4070
4263
  return parts.join("<br/>");
4071
4264
  }
4072
- function buildCodeDesc$7(q) {
4265
+ function buildCodeDesc$8(q) {
4073
4266
  return `config_rule_name: ${q.ConfigRuleName}, resource_type: ${q.ResourceType}, resource_id: ${q.ResourceId}`;
4074
4267
  }
4075
4268
  function buildResultMessage(codeDesc, annotation, status) {
@@ -4086,7 +4279,7 @@ function computeRunTime(invoked, recorded) {
4086
4279
  function buildResult(r) {
4087
4280
  const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
4088
4281
  const status = mapComplianceStatus(r.ComplianceType);
4089
- const codeDesc = buildCodeDesc$7(q);
4282
+ const codeDesc = buildCodeDesc$8(q);
4090
4283
  return createResult(status, buildResultMessage(codeDesc, r.Annotation, status), {
4091
4284
  codeDesc,
4092
4285
  startTime: (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
@@ -4214,12 +4407,32 @@ function checkToResult(check, scanTime) {
4214
4407
  });
4215
4408
  }
4216
4409
  /**
4410
+ * Renders checkov's code_block ([[lineno, "source"], ...]) into a readable,
4411
+ * line-numbered source snippet for the Heimdall CODE tab. Returns undefined when
4412
+ * the code_block is absent or not an array so requirement.code is omitted.
4413
+ */
4414
+ function renderCodeBlock(codeBlock) {
4415
+ if (!Array.isArray(codeBlock)) return void 0;
4416
+ const lines = [];
4417
+ for (const entry of codeBlock) {
4418
+ if (!Array.isArray(entry) || entry.length < 2) continue;
4419
+ const lineno = entry[0];
4420
+ const src = typeof entry[1] === "string" ? entry[1] : "";
4421
+ const trimmed = src.endsWith("\n") ? src.slice(0, -1) : src;
4422
+ lines.push(`${lineno} ${trimmed}`);
4423
+ }
4424
+ return lines.length ? lines.join("\n") : void 0;
4425
+ }
4426
+ /**
4217
4427
  * Converts a group of checks sharing a check_id into one EvaluatedRequirement.
4218
4428
  */
4219
- function buildRequirement$14(checkId, checks, scanTime) {
4429
+ function buildRequirement$14(checkId, group, scanTime) {
4430
+ const checks = group.map((c) => c.check);
4220
4431
  const rep = checks[0];
4221
4432
  const impact = getImpact$8(rep.severity);
4222
4433
  const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
4434
+ const checkTypes = [...new Set(group.map((c) => c.checkType).filter((t) => t !== ""))].sort();
4435
+ if (checkTypes.length > 0) tags["check_type"] = checkTypes;
4223
4436
  const descriptions = [{
4224
4437
  label: "default",
4225
4438
  data: rep.check_name
@@ -4231,6 +4444,8 @@ function buildRequirement$14(checkId, checks, scanTime) {
4231
4444
  const results = checks.map((check) => checkToResult(check, scanTime));
4232
4445
  const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
4233
4446
  req.verificationMethod = VerificationMethodEnum.Automated;
4447
+ const code = renderCodeBlock(rep.code_block);
4448
+ if (code !== void 0) req.code = code;
4234
4449
  const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
4235
4450
  if (controlType !== void 0) req.controlType = controlType;
4236
4451
  return req;
@@ -4267,18 +4482,23 @@ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
4267
4482
  for (const report of reports) {
4268
4483
  checkTypes.push(report.check_type);
4269
4484
  if (!version && report.summary.checkov_version) version = report.summary.checkov_version;
4270
- allChecks.push(...report.results.passed_checks);
4271
- allChecks.push(...report.results.failed_checks);
4272
- allChecks.push(...report.results.skipped_checks);
4485
+ for (const check of [
4486
+ ...report.results.passed_checks,
4487
+ ...report.results.failed_checks,
4488
+ ...report.results.skipped_checks
4489
+ ]) allChecks.push({
4490
+ check,
4491
+ checkType: report.check_type
4492
+ });
4273
4493
  }
4274
4494
  const { items: limitedChecks, truncated } = limitArray(allChecks);
4275
4495
  /* v8 ignore next -- truncation only triggers with >100K items */
4276
4496
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedChecks.length} check items (original: ${allChecks.length})`);
4277
4497
  const groups = /* @__PURE__ */ new Map();
4278
- for (const check of limitedChecks) {
4279
- const existing = groups.get(check.check_id);
4280
- if (existing) existing.push(check);
4281
- else groups.set(check.check_id, [check]);
4498
+ for (const entry of limitedChecks) {
4499
+ const existing = groups.get(entry.check.check_id);
4500
+ if (existing) existing.push(entry);
4501
+ else groups.set(entry.check.check_id, [entry]);
4282
4502
  }
4283
4503
  const requirements = [];
4284
4504
  for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks, scanTime));
@@ -4293,7 +4513,6 @@ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
4293
4513
  converterVersion,
4294
4514
  toolName: "Checkov",
4295
4515
  toolVersion: version,
4296
- toolFormat: format,
4297
4516
  baselines: [baseline],
4298
4517
  timestamp: scanTime
4299
4518
  });
@@ -4309,6 +4528,13 @@ const IMPACT_MAPPING$6 = {
4309
4528
  LOW: .3
4310
4529
  };
4311
4530
  /**
4531
+ * Returns the first-class CWE identifiers for an issue in "CWE-N" form,
4532
+ * or an empty array when the issue carries no CWE.
4533
+ */
4534
+ function cweIds(issue) {
4535
+ return issue.cwe.id ? [`CWE-${issue.cwe.id}`] : [];
4536
+ }
4537
+ /**
4312
4538
  * Returns true if the issue is suppressed (via nosec flag or suppressions list).
4313
4539
  */
4314
4540
  function isSuppressed(issue) {
@@ -4345,13 +4571,7 @@ function buildRequirement$13(ruleId, issues, scanTime) {
4345
4571
  const rep = issues[0];
4346
4572
  const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
4347
4573
  const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
4348
- const tags = {
4349
- nist,
4350
- cwe: {
4351
- id: rep.cwe.id,
4352
- url: rep.cwe.url
4353
- }
4354
- };
4574
+ const tags = { nist };
4355
4575
  const results = issues.map((issue) => issueToResult(issue, scanTime));
4356
4576
  const descriptions = [{
4357
4577
  label: "default",
@@ -4361,6 +4581,9 @@ function buildRequirement$13(ruleId, issues, scanTime) {
4361
4581
  data: `CWE-${rep.cwe.id}: ${rep.cwe.url}`
4362
4582
  }];
4363
4583
  const req = createRequirement(ruleId, rep.details, descriptions, impact, results, { tags });
4584
+ const cwe = cweIds(rep);
4585
+ if (cwe.length > 0) req.cwe = cwe;
4586
+ req.code = rep.code;
4364
4587
  req.verificationMethod = VerificationMethodEnum.Automated;
4365
4588
  const controlType = deriveControlTypeFromTags(nist);
4366
4589
  if (controlType !== void 0) req.controlType = controlType;
@@ -4413,7 +4636,7 @@ function buildNistTags$2(niktoId) {
4413
4636
  if (control) return [control];
4414
4637
  return [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
4415
4638
  }
4416
- function buildCodeDesc$6(vuln) {
4639
+ function buildCodeDesc$7(vuln) {
4417
4640
  const parts = [];
4418
4641
  if (vuln.url) parts.push(`URL: ${vuln.url}`);
4419
4642
  if (vuln.method) parts.push(`Method: ${vuln.method}`);
@@ -4424,7 +4647,7 @@ function convertVulnToRequirement(vuln) {
4424
4647
  const cciTags = nistToCci(nistTags);
4425
4648
  const result = {
4426
4649
  status: ResultStatus.Failed,
4427
- codeDesc: buildCodeDesc$6(vuln),
4650
+ codeDesc: buildCodeDesc$7(vuln),
4428
4651
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4429
4652
  };
4430
4653
  const extras = {};
@@ -4467,7 +4690,7 @@ async function convertNiktoToHdf(input, converterVersion = "unknown") {
4467
4690
  const req = convertVulnToRequirement(primary);
4468
4691
  if (vulns.length > 1) for (let i = 1; i < vulns.length; i++) req.results.push({
4469
4692
  status: ResultStatus.Failed,
4470
- codeDesc: buildCodeDesc$6(vulns[i]),
4693
+ codeDesc: buildCodeDesc$7(vulns[i]),
4471
4694
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4472
4695
  });
4473
4696
  requirements.push(req);
@@ -4481,7 +4704,6 @@ async function convertNiktoToHdf(input, converterVersion = "unknown") {
4481
4704
  generatorName: "nikto-to-hdf",
4482
4705
  converterVersion,
4483
4706
  toolName: "Nikto",
4484
- toolFormat: "JSON",
4485
4707
  baselines: [createMinimalBaseline(targetName, requirements, {
4486
4708
  resultsChecksum,
4487
4709
  title: `Nikto Target: ${targetName}`,
@@ -4514,7 +4736,13 @@ function buildNistTags$1(cweid) {
4514
4736
  }
4515
4737
  return [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
4516
4738
  }
4517
- function buildCodeDesc$5(instance) {
4739
+ function buildCwe(cweid) {
4740
+ if (!cweid || cweid === "0") return;
4741
+ const cweNum = parseInt(cweid, 10);
4742
+ if (isNaN(cweNum)) return;
4743
+ return [`CWE-${cweNum}`];
4744
+ }
4745
+ function buildCodeDesc$6(instance) {
4518
4746
  const parts = [];
4519
4747
  if (instance.uri) parts.push(`URI: ${instance.uri}`);
4520
4748
  if (instance.method) parts.push(`Method: ${instance.method}`);
@@ -4522,6 +4750,21 @@ function buildCodeDesc$5(instance) {
4522
4750
  if (instance.evidence) parts.push(`Evidence: ${instance.evidence}`);
4523
4751
  return parts.join(" | ");
4524
4752
  }
4753
+ function representativeInstance(instances) {
4754
+ if (!instances || instances.length === 0) return;
4755
+ return instances.find((inst) => inst.attack) ?? instances[0];
4756
+ }
4757
+ function buildRequirementCode$1(alert) {
4758
+ const inst = representativeInstance(alert.instances);
4759
+ if (!inst) return;
4760
+ const parts = [];
4761
+ const requestLine = `${inst.method ?? ""} ${inst.uri ?? ""}`.trim();
4762
+ if (requestLine) parts.push(requestLine);
4763
+ if (inst.param) parts.push(`Param: ${inst.param}`);
4764
+ if (inst.attack) parts.push(`Attack: ${inst.attack}`);
4765
+ if (parts.length === 0) return;
4766
+ return parts.join("\n");
4767
+ }
4525
4768
  function buildCheckDescription(alert) {
4526
4769
  const parts = [];
4527
4770
  if (alert.solution) {
@@ -4534,33 +4777,11 @@ function buildCheckDescription(alert) {
4534
4777
  }
4535
4778
  return parts.join("\n");
4536
4779
  }
4537
- function selectSite(sites) {
4538
- if (sites.length === 0) return void 0;
4539
- let best = sites[0];
4540
- for (let i = 1; i < sites.length; i++) {
4541
- const site = sites[i];
4542
- if ((site.alerts?.length ?? 0) > (best.alerts?.length ?? 0)) best = site;
4543
- }
4544
- return best;
4545
- }
4546
- const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
4547
- function parseZapTimestamp(s) {
4548
- const trimmed = s.trim();
4549
- if (ZAP_RFC1123_LIKE.test(trimmed)) {
4550
- const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
4551
- if (!isNaN(d.getTime())) return d;
4552
- }
4553
- return parseTimestamp(s) ?? void 0;
4780
+ function siteLabel(site, index) {
4781
+ return site["@host"] || site["@name"] || `site ${index + 1}`;
4554
4782
  }
4555
- async function convertZapToHdf(input, converterVersion = "1.0.0") {
4556
- validateInputSize(input, "zap");
4557
- registerAllFingerprints();
4558
- const detected = detectConverter(input);
4559
- if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
4560
- const resultsChecksum = await inputChecksum(input);
4561
- const zapData = parseJSON(input);
4562
- const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
4563
- const alerts = site?.alerts ?? [];
4783
+ function buildSiteRequirements(site) {
4784
+ const alerts = site.alerts ?? [];
4564
4785
  const pluginIdCount = /* @__PURE__ */ new Map();
4565
4786
  const { items: limitedAlerts, truncated } = limitArray(alerts);
4566
4787
  /* v8 ignore next -- truncation only triggers with >100K items */
@@ -4573,7 +4794,6 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
4573
4794
  const nistTags = buildNistTags$1(alert.cweid ?? "");
4574
4795
  const cciTags = nistToCci(nistTags);
4575
4796
  const extras = {};
4576
- if (alert.cweid) extras.cweid = alert.cweid;
4577
4797
  if (alert.wascid) extras.wascid = alert.wascid;
4578
4798
  if (alert.riskdesc) extras.riskdesc = alert.riskdesc;
4579
4799
  if (alert.confidence) extras.confidence = alert.confidence;
@@ -4585,7 +4805,7 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
4585
4805
  for (const instance of limitedInstances) {
4586
4806
  const result = {
4587
4807
  status: ResultStatus.Failed,
4588
- codeDesc: buildCodeDesc$5(instance),
4808
+ codeDesc: buildCodeDesc$6(instance),
4589
4809
  startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
4590
4810
  };
4591
4811
  if (instance.attack) result.message = instance.attack;
@@ -4612,40 +4832,76 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
4612
4832
  descriptions,
4613
4833
  verificationMethod: VerificationMethodEnum.Automated
4614
4834
  };
4835
+ const cwe = buildCwe(alert.cweid);
4836
+ if (cwe !== void 0) req.cwe = cwe;
4615
4837
  const controlType = deriveControlTypeFromTags(nistTags);
4616
4838
  if (controlType !== void 0) req.controlType = controlType;
4839
+ const code = buildRequirementCode$1(alert);
4840
+ if (code !== void 0) req.code = code;
4617
4841
  requirements.push(req);
4618
4842
  }
4619
- const targetName = site?.["@host"] ?? "Unknown Host";
4620
- const siteName = site?.["@name"] ?? "";
4621
- const baselineName = site && (site["@name"] || site["@host"]) ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
4622
- if (requirements.length === 0) {
4623
- let target = siteName || targetName;
4624
- if (!target || target === "Unknown Host") target = "the target site";
4625
- requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
4843
+ return requirements;
4844
+ }
4845
+ const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
4846
+ function parseZapTimestamp(s) {
4847
+ const trimmed = s.trim();
4848
+ if (ZAP_RFC1123_LIKE.test(trimmed)) {
4849
+ const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
4850
+ if (!isNaN(d.getTime())) return d;
4626
4851
  }
4627
- const baseline = createMinimalBaseline("OWASP ZAP Scan", requirements, {
4628
- resultsChecksum,
4629
- title: baselineName,
4630
- summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
4631
- });
4632
- const tool = {
4633
- name: "OWASP ZAP",
4634
- format: "JSON"
4635
- };
4636
- if (zapData["@version"]) tool.version = zapData["@version"];
4852
+ return parseTimestamp(s) ?? void 0;
4853
+ }
4854
+ async function convertZapToHdf(input, converterVersion = "1.0.0") {
4855
+ validateInputSize(input, "zap");
4856
+ registerAllFingerprints();
4857
+ const detected = detectConverter(input);
4858
+ if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
4859
+ const resultsChecksum = await inputChecksum(input);
4860
+ const zapData = parseJSON(input);
4861
+ const sites = Array.isArray(zapData.site) ? zapData.site : [];
4862
+ const multiSite = sites.length > 1;
4863
+ const summary = `ZAP Version ${zapData["@version"] ?? "unknown"}`;
4864
+ const baselines = [];
4637
4865
  const components = [];
4638
- if (site?.["@name"]) components.push({
4639
- name: targetName,
4640
- type: TargetType.Application,
4641
- url: site["@name"]
4642
- });
4643
- else if (targetName !== "Unknown Host") components.push({
4644
- name: targetName,
4645
- type: TargetType.Application
4866
+ sites.forEach((site, i) => {
4867
+ const targetName = site["@host"] ?? "Unknown Host";
4868
+ const siteName = site["@name"] ?? "";
4869
+ const requirements = buildSiteRequirements(site);
4870
+ if (requirements.length === 0) {
4871
+ let target = siteName || targetName;
4872
+ if (!target || target === "Unknown Host") target = "the target site";
4873
+ requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
4874
+ }
4875
+ const baselineTitle = site["@name"] || site["@host"] ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
4876
+ const baseline = createMinimalBaseline(multiSite ? `OWASP ZAP Scan: ${siteLabel(site, i)}` : "OWASP ZAP Scan", requirements, {
4877
+ resultsChecksum,
4878
+ title: baselineTitle,
4879
+ summary
4880
+ });
4881
+ if (site["@host"]) baseline.labels = { component: site["@host"] };
4882
+ baselines.push(baseline);
4883
+ if (site["@name"]) components.push({
4884
+ name: targetName,
4885
+ type: TargetType.Application,
4886
+ url: site["@name"]
4887
+ });
4888
+ else if (targetName !== "Unknown Host") components.push({
4889
+ name: targetName,
4890
+ type: TargetType.Application
4891
+ });
4646
4892
  });
4893
+ if (baselines.length === 0) {
4894
+ const requirements = [buildNoFindingsRequirement("zap-no-findings", "OWASP ZAP scanned the target site and reported zero findings.", /* @__PURE__ */ new Date())];
4895
+ baselines.push(createMinimalBaseline("OWASP ZAP Scan", requirements, {
4896
+ resultsChecksum,
4897
+ title: "OWASP ZAP Scan",
4898
+ summary
4899
+ }));
4900
+ }
4901
+ const tool = { name: "OWASP ZAP" };
4902
+ if (zapData["@version"]) tool.version = zapData["@version"];
4647
4903
  const hdf = {
4648
- baselines: [baseline],
4904
+ baselines,
4649
4905
  components,
4650
4906
  generator: {
4651
4907
  name: "zap-to-hdf",
@@ -5263,23 +5519,18 @@ function stringSlice(v) {
5263
5519
  if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
5264
5520
  return [];
5265
5521
  }
5266
- /** Most-significant status across a requirement's results[] (lossless). */
5522
+ /**
5523
+ * Most-significant status across a requirement's results[] (lossless), using
5524
+ * the canonical worst-wins ordering from @mitre/hdf-utilities.
5525
+ */
5267
5526
  function worstOfResults(req) {
5268
5527
  const results = asArr(req.results) ?? [];
5269
- const precedence = [
5270
- "failed",
5271
- "error",
5272
- "passed",
5273
- "notReviewed",
5274
- "notApplicable"
5275
- ];
5276
- const present = /* @__PURE__ */ new Set();
5528
+ const statuses = [];
5277
5529
  for (const rRaw of results) {
5278
5530
  const r = asMap(rRaw);
5279
- if (r) present.add(getStr(r, "status"));
5531
+ if (r) statuses.push(getStr(r, "status"));
5280
5532
  }
5281
- for (const s of precedence) if (present.has(s)) return s;
5282
- return "notReviewed";
5533
+ return worstStatus(statuses);
5283
5534
  }
5284
5535
  /**
5285
5536
  * Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
@@ -5535,10 +5786,34 @@ function maxImpact(ratings) {
5535
5786
  return max;
5536
5787
  }
5537
5788
  /**
5538
- * Formats the ratings as a human-readable tag string.
5789
+ * Assembles structured requirement.cvss[] entries from the CycloneDX ratings. A
5790
+ * rating contributes an entry only when it carries a CVSS method
5791
+ * (CVSSv2/v3/v31/v4) and at least a score or a vector — ratings that only state a
5792
+ * qualitative severity (method "other") carry no CVSS metrics and are left out,
5793
+ * their severity already reflected in the requirement impact.
5539
5794
  */
5540
- function formatRatingsTag(ratings) {
5541
- return ratings.map((r) => `${r.source?.name ?? "Unknown"} - ${r.severity ?? "unrated"}`).join(", ");
5795
+ function cvssVersionFromMethod(method, vector) {
5796
+ if (vector !== void 0 && vector.startsWith("CVSS:")) return cvssVersionFromVector(vector);
5797
+ switch (method) {
5798
+ case "CVSSv2": return cvssVersionFromString("2.0");
5799
+ case "CVSSv4": return cvssVersionFromString("4.0");
5800
+ default: return cvssVersionFromVector(vector);
5801
+ }
5802
+ }
5803
+ function buildCvssEntries$3(ratings) {
5804
+ const entries = [];
5805
+ for (const r of ratings) {
5806
+ const hasCvssMethod = r.method !== void 0 && CVSS_METHODS.has(r.method);
5807
+ const hasMetric = r.score !== void 0 && r.score !== null || r.vector !== void 0 && r.vector !== "";
5808
+ if (!hasCvssMethod || !hasMetric) continue;
5809
+ entries.push(buildCvss$2({
5810
+ version: cvssVersionFromMethod(r.method, r.vector),
5811
+ baseScore: r.score,
5812
+ baseVector: r.vector,
5813
+ source: r.source?.name
5814
+ }));
5815
+ }
5816
+ return entries;
5542
5817
  }
5543
5818
  /**
5544
5819
  * Formats a component reference as a code_desc string.
@@ -5606,8 +5881,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
5606
5881
  nist,
5607
5882
  cci: nistToCci(nist)
5608
5883
  };
5609
- if (cwes.length > 0) tags["cweid"] = cwes.map((c) => `CWE-${c}`);
5610
- if (ratings.length > 0) tags["ratings"] = formatRatingsTag(ratings);
5884
+ const cweIds = cwes.map((c) => `CWE-${c}`);
5885
+ const cvssEntries = buildCvssEntries$3(ratings);
5611
5886
  const descriptions = [];
5612
5887
  const defaultParts = [];
5613
5888
  if (vuln.description) defaultParts.push(`Description: ${vuln.description}`);
@@ -5635,6 +5910,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
5635
5910
  const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
5636
5911
  const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
5637
5912
  const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
5913
+ if (cvssEntries.length > 0) req.cvss = cvssEntries;
5914
+ if (cweIds.length > 0) req.cwe = cweIds;
5638
5915
  const controlType = deriveControlTypeFromTags(nist);
5639
5916
  if (controlType !== void 0) req.controlType = controlType;
5640
5917
  requirements.push(req);
@@ -5658,7 +5935,6 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
5658
5935
  generatorName: "cyclonedx-to-hdf",
5659
5936
  converterVersion,
5660
5937
  toolName: "CycloneDX",
5661
- toolFormat: "JSON",
5662
5938
  baselines: [baseline],
5663
5939
  components: [{
5664
5940
  ...targetComponent,
@@ -6769,7 +7045,7 @@ function collectIdentifierTags(identifiers) {
6769
7045
  }
6770
7046
  return result;
6771
7047
  }
6772
- function buildCodeDesc$4(scanType, location) {
7048
+ function buildCodeDesc$5(scanType, location) {
6773
7049
  if (!location) return "";
6774
7050
  switch (scanType) {
6775
7051
  case "sast":
@@ -6847,7 +7123,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
6847
7123
  });
6848
7124
  const result = {
6849
7125
  status: ResultStatus.Failed,
6850
- codeDesc: buildCodeDesc$4(scanType, vuln.location),
7126
+ codeDesc: buildCodeDesc$5(scanType, vuln.location),
6851
7127
  startTime: startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z") : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
6852
7128
  };
6853
7129
  const impact = gitlabSeverityToImpact(vuln.severity ?? "Unknown");
@@ -6855,6 +7131,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
6855
7131
  id: vuln.id,
6856
7132
  title: vuln.name ?? vuln.id,
6857
7133
  impact,
7134
+ code: JSON.stringify(vuln, null, 2),
6858
7135
  results: [result],
6859
7136
  tags,
6860
7137
  descriptions,
@@ -6874,10 +7151,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
6874
7151
  title: `GitLab ${label} Security Scan`,
6875
7152
  summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
6876
7153
  });
6877
- const tool = {
6878
- name: scannerName,
6879
- format: "JSON"
6880
- };
7154
+ const tool = { name: scannerName };
6881
7155
  if (scannerVersion) tool.version = scannerVersion;
6882
7156
  const components = [];
6883
7157
  const targetType = scanTypeToTargetType(scanType);
@@ -6910,6 +7184,20 @@ const TRUFFLEHOG_CCI = [
6910
7184
  "CCI-002367"
6911
7185
  ];
6912
7186
  /**
7187
+ * A verified secret is a confirmed-live credential (TruffleHog reached the
7188
+ * provider and the credential authenticated) and rates high; an unverified
7189
+ * candidate rates medium.
7190
+ */
7191
+ const IMPACT_VERIFIED = .7;
7192
+ const IMPACT_UNVERIFIED = .5;
7193
+ /**
7194
+ * Rate a requirement by its strongest signal: any verified finding in the group
7195
+ * elevates the whole requirement to the verified impact.
7196
+ */
7197
+ function groupImpact(findings) {
7198
+ return findings.some((f) => f.Verified) ? IMPACT_VERIFIED : IMPACT_UNVERIFIED;
7199
+ }
7200
+ /**
6913
7201
  * Parse TruffleHog input as JSON array, single object, or NDJSON.
6914
7202
  */
6915
7203
  function parseFindings(input) {
@@ -6969,7 +7257,7 @@ function buildMessage(f) {
6969
7257
  /**
6970
7258
  * Build the Result.CodeDesc as JSON of SourceMetadata.
6971
7259
  */
6972
- function buildCodeDesc$3(f) {
7260
+ function buildCodeDesc$4(f) {
6973
7261
  return JSON.stringify(f.SourceMetadata ?? {});
6974
7262
  }
6975
7263
  /**
@@ -7012,12 +7300,12 @@ function buildRequirement$12(reqID, findings) {
7012
7300
  data: rep.DetectorDescription ?? `${rep.DetectorName} secret detected by ${rep.DecoderName} decoder`
7013
7301
  }];
7014
7302
  const results = findings.map((f) => createResult(ResultStatus.Failed, buildMessage(f), {
7015
- codeDesc: buildCodeDesc$3(f),
7303
+ codeDesc: buildCodeDesc$4(f),
7016
7304
  startTime: getTimestamp(f)
7017
7305
  }));
7018
7306
  const sourceFile = getSourceFile(rep);
7019
7307
  const sourceLine = getSourceLine(rep);
7020
- const req = createRequirement(reqID, title, descriptions, .5, results, {
7308
+ const req = createRequirement(reqID, title, descriptions, groupImpact(findings), results, {
7021
7309
  tags,
7022
7310
  sourceLocation: sourceFile ? {
7023
7311
  ref: sourceFile,
@@ -7057,10 +7345,7 @@ async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
7057
7345
  name: "trufflehog-to-hdf",
7058
7346
  version: converterVersion
7059
7347
  },
7060
- tool: {
7061
- name: "TruffleHog",
7062
- format: "JSON"
7063
- },
7348
+ tool: { name: "TruffleHog" },
7064
7349
  timestamp: /* @__PURE__ */ new Date()
7065
7350
  };
7066
7351
  const repoURL = findGitRepoURL(limitedFindings);
@@ -7194,7 +7479,6 @@ async function convertHipcheckToHdf(input, converterVersion = "1.0.0") {
7194
7479
  generatorName: "hipcheck-to-hdf",
7195
7480
  converterVersion,
7196
7481
  toolName: "Hipcheck",
7197
- toolFormat: "JSON",
7198
7482
  toolVersion: report.hipcheck_version,
7199
7483
  baselines: [baseline],
7200
7484
  components: ident ? [{
@@ -7267,10 +7551,7 @@ async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
7267
7551
  resultsChecksum,
7268
7552
  title: `BurpSuite Scan: ${targetName}`
7269
7553
  });
7270
- const tool = {
7271
- name: "BurpSuite",
7272
- format: "XML"
7273
- };
7554
+ const tool = { name: "BurpSuite" };
7274
7555
  if (burpVersion) tool.version = burpVersion;
7275
7556
  const hdf = {
7276
7557
  baselines: [baseline],
@@ -7439,6 +7720,17 @@ function parseDate(dateStr) {
7439
7720
  return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO;
7440
7721
  }
7441
7722
  /**
7723
+ * Renders a finding's parsed row (column→value map) as the indented JSON blob
7724
+ * carried in requirement.code. DBProtect ships no literal check source, so the
7725
+ * row itself is the richest available representation. Keys are sorted so the
7726
+ * bytes match the Go twin, whose encoding/json emits map keys in sorted order.
7727
+ */
7728
+ function marshalFindingCode(f) {
7729
+ const sorted = {};
7730
+ for (const key of Object.keys(f).sort()) sorted[key] = f[key];
7731
+ return JSON.stringify(sorted, null, 2);
7732
+ }
7733
+ /**
7442
7734
  * Builds a single EvaluatedRequirement from a group of findings sharing a Check ID.
7443
7735
  */
7444
7736
  function buildRequirement$10(checkID, findings, hasStatus) {
@@ -7457,6 +7749,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
7457
7749
  });
7458
7750
  const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
7459
7751
  req.verificationMethod = VerificationMethodEnum.Automated;
7752
+ req.code = marshalFindingCode(rep);
7460
7753
  const controlType = deriveControlTypeFromTags([...nist]);
7461
7754
  if (controlType !== void 0) req.controlType = controlType;
7462
7755
  return req;
@@ -7508,7 +7801,6 @@ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
7508
7801
  generatorName: "dbprotect-to-hdf",
7509
7802
  converterVersion,
7510
7803
  toolName: "DBProtect",
7511
- toolFormat: "XML",
7512
7804
  baselines: [baseline],
7513
7805
  components: [{
7514
7806
  name: targetName,
@@ -7552,30 +7844,6 @@ function buildSummary(result) {
7552
7844
  return `Package Vulnerability Summary: ${result.vulnerabilityDistribution ? String(result.vulnerabilityDistribution.total) : "N/A"} Application Compliance Issue Total: ${result.complianceDistribution ? String(result.complianceDistribution.total) : "N/A"}`;
7553
7845
  }
7554
7846
  /**
7555
- * Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
7556
- * since modern Twistlock exclusively emits 3.x output.
7557
- */
7558
- function cvssVersionFromVector(vector) {
7559
- if (!vector) return Version.The31;
7560
- if (vector.startsWith("CVSS:2.0/")) return Version.The20;
7561
- if (vector.startsWith("CVSS:3.0/")) return Version.The30;
7562
- if (vector.startsWith("CVSS:4.0/")) return Version.The40;
7563
- return Version.The31;
7564
- }
7565
- /**
7566
- * Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
7567
- */
7568
- function cvssSeverityFromScore(score) {
7569
- switch (cvssScoreToSeverity(score)) {
7570
- case "none": return CVSSSeverity.None;
7571
- case "low": return CVSSSeverity.Low;
7572
- case "medium": return CVSSSeverity.Medium;
7573
- case "high": return CVSSSeverity.High;
7574
- case "critical": return CVSSSeverity.Critical;
7575
- default: return;
7576
- }
7577
- }
7578
- /**
7579
7847
  * Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
7580
7848
  * when neither a score nor a vector is available. When the vendor emits a
7581
7849
  * score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
@@ -7586,16 +7854,12 @@ function buildCvss(vuln) {
7586
7854
  const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
7587
7855
  const hasVector = !!vuln.vector;
7588
7856
  if (!hasScore && !hasVector) return void 0;
7589
- const cv = { version: cvssVersionFromVector(vuln.vector) };
7590
- if (hasScore) {
7591
- cv.baseScore = vuln.cvss;
7592
- const sev = cvssSeverityFromScore(vuln.cvss);
7593
- if (sev !== void 0) cv.baseSeverity = sev;
7594
- }
7595
- if (hasVector) cv.baseVector = vuln.vector;
7596
- const source = vuln.cve ?? vuln.id;
7597
- if (source) cv.source = source;
7598
- return cv;
7857
+ return buildCvss$2({
7858
+ version: cvssVersionFromVector(vuln.vector),
7859
+ baseScore: hasScore ? vuln.cvss : void 0,
7860
+ baseVector: vuln.vector,
7861
+ source: vuln.cve ?? vuln.id
7862
+ });
7599
7863
  }
7600
7864
  const CWE_REGEX = /cwe[-_]?(\d+)/gi;
7601
7865
  /**
@@ -7785,7 +8049,6 @@ async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
7785
8049
  generatorName: "twistlock-to-hdf",
7786
8050
  converterVersion,
7787
8051
  toolName: "Twistlock",
7788
- toolFormat: "JSON",
7789
8052
  baselines,
7790
8053
  components: [component],
7791
8054
  timestamp: /* @__PURE__ */ new Date()
@@ -7826,16 +8089,32 @@ function getCweIDs(cwes) {
7826
8089
  return cwes.map((cwe) => `CWE-${cwe.cweId}`);
7827
8090
  }
7828
8091
  /**
8092
+ * Collects CVE identifiers from vulnerability.aliases[].cveId, deduped in
8093
+ * first-seen order. The finding.matrix (requirement id) is a UUID composite, so
8094
+ * the CVE has no other home; it goes to tags.cve (interim, pending an
8095
+ * identifiers[] schema field).
8096
+ */
8097
+ function getCVEs(vuln) {
8098
+ const seen = /* @__PURE__ */ new Set();
8099
+ const out = [];
8100
+ for (const alias of vuln.aliases ?? []) if (alias.cveId && !seen.has(alias.cveId)) {
8101
+ seen.add(alias.cveId);
8102
+ out.push(alias.cveId);
8103
+ }
8104
+ return out;
8105
+ }
8106
+ /**
7829
8107
  * Builds a single EvaluatedRequirement from a Dependency-Track finding.
7830
8108
  */
7831
8109
  function buildRequirement$8(finding, timestamp) {
7832
8110
  const cweIDs = getCweIDs(finding.vulnerability.cwes);
8111
+ const cveIDs = getCVEs(finding.vulnerability);
7833
8112
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
7834
8113
  const tags = {
7835
8114
  nist,
7836
8115
  cci: nistToCci(nist)
7837
8116
  };
7838
- if (cweIDs.length > 0) tags["cweIds"] = cweIDs;
8117
+ if (cveIDs.length > 0) tags["cve"] = cveIDs;
7839
8118
  const descriptions = [{
7840
8119
  label: "default",
7841
8120
  data: finding.vulnerability.description ?? ""
@@ -7855,8 +8134,10 @@ function buildRequirement$8(finding, timestamp) {
7855
8134
  })];
7856
8135
  const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
7857
8136
  req.verificationMethod = VerificationMethodEnum.Automated;
8137
+ req.code = JSON.stringify(finding, null, 2);
7858
8138
  const controlType = deriveControlTypeFromTags(nist);
7859
8139
  if (controlType !== void 0) req.controlType = controlType;
8140
+ if (cweIDs.length > 0) req.cwe = cweIDs;
7860
8141
  const pkg = buildAffectedPackageFromComponent(finding.component);
7861
8142
  if (pkg) req.affectedPackages = [pkg];
7862
8143
  return req;
@@ -7908,7 +8189,7 @@ async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
7908
8189
  generatorName: "deptrack-to-hdf",
7909
8190
  converterVersion,
7910
8191
  toolName: "Dependency-Track",
7911
- toolFormat: "JSON",
8192
+ toolFormat: "FPF",
7912
8193
  baselines: [baseline],
7913
8194
  components: [{
7914
8195
  name: targetName,
@@ -7927,12 +8208,79 @@ async function hashID(summary) {
7927
8208
  return (await sha256(summary)).substring(0, 32);
7928
8209
  }
7929
8210
  /**
7930
- * Extract CWE identifiers from the first CVE entry.
8211
+ * Collect the distinct CWE identifiers across every CVE entry, preserving
8212
+ * first-seen order. These feed both requirement.cwe[] and the CWE→NIST mapping.
7931
8213
  */
7932
8214
  function extractCWEs$1(entry) {
7933
- const cves = entry.component_versions?.more_details?.cves;
7934
- if (!cves || cves.length === 0) return [];
7935
- return cves[0]?.cwe ?? [];
8215
+ const out = [];
8216
+ const seen = /* @__PURE__ */ new Set();
8217
+ for (const cve of entry.component_versions?.more_details?.cves ?? []) for (const cwe of cve.cwe ?? []) if (cwe && !seen.has(cwe)) {
8218
+ seen.add(cwe);
8219
+ out.push(cwe);
8220
+ }
8221
+ return out;
8222
+ }
8223
+ /**
8224
+ * Collect the distinct CVE identifiers across every CVE entry, preserving
8225
+ * first-seen order. The requirement id is a summary hash (never the CVE), so
8226
+ * tags.cve is where the CVE list lives.
8227
+ */
8228
+ function extractCVEs$1(entry) {
8229
+ const out = [];
8230
+ const seen = /* @__PURE__ */ new Set();
8231
+ for (const cve of entry.component_versions?.more_details?.cves ?? []) if (cve.cve && !seen.has(cve.cve)) {
8232
+ seen.add(cve.cve);
8233
+ out.push(cve.cve);
8234
+ }
8235
+ return out;
8236
+ }
8237
+ /**
8238
+ * Split a JFrog cvss_v2/cvss_v3 field ("<score>/<vector>", e.g.
8239
+ * "9.8/CVSS:3.1/AV:N/...") on the FIRST "/" into the numeric base score and the
8240
+ * remaining vector. A field with no "/" is a bare score with no vector; an
8241
+ * unparseable score yields undefined (the vector may still stand alone).
8242
+ */
8243
+ function parseCvssField(field) {
8244
+ if (!field) return { vector: "" };
8245
+ const idx = field.indexOf("/");
8246
+ let scorePart = field;
8247
+ let vector = "";
8248
+ if (idx >= 0) {
8249
+ scorePart = field.slice(0, idx);
8250
+ vector = field.slice(idx + 1);
8251
+ }
8252
+ const parsed = Number.parseFloat(scorePart.trim());
8253
+ return {
8254
+ score: Number.isFinite(parsed) ? parsed : void 0,
8255
+ vector
8256
+ };
8257
+ }
8258
+ /**
8259
+ * Assemble the structured requirement.cvss[] for a group's representative
8260
+ * entry. Each CVE contributes its v3 metric (version from the vector prefix)
8261
+ * then its v2 metric (always CVSS 2.0 — the cvss_v2 vector prefix is
8262
+ * inconsistent, so the field name is the authority). A field with neither a
8263
+ * score nor a vector is skipped.
8264
+ */
8265
+ function buildCvssEntries$2(cves) {
8266
+ const out = [];
8267
+ for (const cve of cves ?? []) {
8268
+ const v3 = parseCvssField(cve.cvss_v3);
8269
+ if (v3.score !== void 0 || v3.vector !== "") out.push(buildCvss$2({
8270
+ version: cvssVersionFromVector(v3.vector),
8271
+ baseScore: v3.score,
8272
+ baseVector: v3.vector,
8273
+ source: cve.cve
8274
+ }));
8275
+ const v2 = parseCvssField(cve.cvss_v2);
8276
+ if (v2.score !== void 0 || v2.vector !== "") out.push(buildCvss$2({
8277
+ version: cvssVersionFromString("2.0"),
8278
+ baseScore: v2.score,
8279
+ baseVector: v2.vector,
8280
+ source: cve.cve
8281
+ }));
8282
+ }
8283
+ return out;
7936
8284
  }
7937
8285
  /**
7938
8286
  * Build description from more_details, including CVE data.
@@ -7970,17 +8318,57 @@ function formatCodeDesc$1(entry) {
7970
8318
  return parts.join("\n").replace(/,/g, ", ");
7971
8319
  }
7972
8320
  /**
8321
+ * Render an Xray entry as the indented JSON blob carried in the requirement's
8322
+ * code field (the ionchannel pattern), so Heimdall's CODE tab shows the raw
8323
+ * finding. The object is reconstructed with a fixed key order and full field set
8324
+ * matching the Go twin's struct marshal, keeping Go and TS byte-identical
8325
+ * (verified by the shared snapshot test).
8326
+ */
8327
+ function marshalEntryCode(entry) {
8328
+ const cv = entry.component_versions;
8329
+ const md = cv?.more_details;
8330
+ const cves = md?.cves;
8331
+ const codeObject = {
8332
+ id: entry.id ?? "",
8333
+ severity: entry.severity ?? "",
8334
+ summary: entry.summary ?? "",
8335
+ issue_type: entry.issue_type ?? "",
8336
+ provider: entry.provider ?? "",
8337
+ component: entry.component ?? "",
8338
+ source_id: entry.source_id ?? "",
8339
+ source_comp_id: entry.source_comp_id ?? "",
8340
+ component_versions: {
8341
+ id: cv?.id ?? "",
8342
+ vulnerable_versions: cv?.vulnerable_versions ?? null,
8343
+ fixed_versions: cv?.fixed_versions ?? null,
8344
+ more_details: {
8345
+ cves: cves ? cves.map((c) => ({
8346
+ cve: c.cve ?? "",
8347
+ cwe: c.cwe ?? null,
8348
+ cvss_v2: c.cvss_v2 ?? "",
8349
+ cvss_v3: c.cvss_v3 ?? ""
8350
+ })) : null,
8351
+ description: md?.description ?? "",
8352
+ provider: md?.provider ?? ""
8353
+ }
8354
+ },
8355
+ edited: entry.edited ?? ""
8356
+ };
8357
+ return JSON.stringify(codeObject, null, 2);
8358
+ }
8359
+ /**
7973
8360
  * Builds a single EvaluatedRequirement from a group of entries sharing an ID.
7974
8361
  */
7975
8362
  function buildRequirement$7(entryID, entries, scanTime) {
7976
8363
  const rep = entries[0];
7977
8364
  const cweIDs = extractCWEs$1(rep);
8365
+ const cveIDs = extractCVEs$1(rep);
7978
8366
  const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
7979
8367
  const tags = {
7980
8368
  nist,
7981
8369
  cci: nistToCci(nist)
7982
8370
  };
7983
- if (cweIDs.length > 0) tags["cweid"] = cweIDs;
8371
+ if (cveIDs.length > 0) tags["cve"] = cveIDs;
7984
8372
  const descriptions = [{
7985
8373
  label: "default",
7986
8374
  data: formatDescription(rep)
@@ -7991,9 +8379,13 @@ function buildRequirement$7(entryID, entries, scanTime) {
7991
8379
  startTime: scanTime
7992
8380
  }));
7993
8381
  const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact$1(rep.severity), results, { tags });
8382
+ req.code = marshalEntryCode(rep);
7994
8383
  const controlType = deriveControlTypeFromTags(nist);
7995
8384
  if (controlType !== void 0) req.controlType = controlType;
7996
8385
  req.verificationMethod = VerificationMethodEnum.Automated;
8386
+ if (cweIDs.length > 0) req.cwe = cweIDs;
8387
+ const cvss = buildCvssEntries$2(rep.component_versions?.more_details?.cves);
8388
+ if (cvss.length > 0) req.cvss = cvss;
7997
8389
  const pkg = buildAffectedPackageFromEntry(rep);
7998
8390
  if (pkg) req.affectedPackages = [pkg];
7999
8391
  return req;
@@ -8072,7 +8464,6 @@ async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
8072
8464
  generatorName: "jfrog-xray-to-hdf",
8073
8465
  converterVersion,
8074
8466
  toolName: "JFrog Xray",
8075
- toolFormat: "JSON",
8076
8467
  baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
8077
8468
  components: [{
8078
8469
  name: "JFrog Xray Scan",
@@ -8091,6 +8482,43 @@ function extractCWEs(description) {
8091
8482
  return extractCWEIDs(description).map((id) => `CWE-${id}`);
8092
8483
  }
8093
8484
  /**
8485
+ * Collects the distinct CVE identifiers a vulnerability carries in its cves[]
8486
+ * field, preserving first-seen order. The requirement ID is a
8487
+ * name/package/version composite (not the CVE), so tags.cve is where the CVE
8488
+ * list lives.
8489
+ */
8490
+ function extractCVEs(vuln) {
8491
+ const out = [];
8492
+ const seen = /* @__PURE__ */ new Set();
8493
+ for (const cve of vuln.cves ?? []) if (cve && !seen.has(cve)) {
8494
+ seen.add(cve);
8495
+ out.push(cve);
8496
+ }
8497
+ return out;
8498
+ }
8499
+ /**
8500
+ * Assembles the structured requirement.cvss[] from the scoring a vulnerability
8501
+ * carries. NeuVector emits a CVSS v3 vector (version-prefixed) and, separately,
8502
+ * a legacy v2 vector (prefix-less). The v3 metric is preferred; the v2 metric
8503
+ * is the fallback. A vulnerability with neither vector contributes no entry —
8504
+ * its score still drives impact.
8505
+ */
8506
+ function buildCvssEntries$1(vuln) {
8507
+ if (vuln.vectors_v3) return [buildCvss$2({
8508
+ version: cvssVersionFromVector(vuln.vectors_v3),
8509
+ baseScore: vuln.score_v3 > 0 ? vuln.score_v3 : void 0,
8510
+ baseVector: vuln.vectors_v3,
8511
+ source: "NeuVector"
8512
+ })];
8513
+ if (vuln.vectors) return [buildCvss$2({
8514
+ version: cvssVersionFromString("2.0"),
8515
+ baseScore: vuln.score > 0 ? vuln.score : void 0,
8516
+ baseVector: vuln.vectors,
8517
+ source: "NeuVector"
8518
+ })];
8519
+ return [];
8520
+ }
8521
+ /**
8094
8522
  * Computes the HDF impact from NeuVector CVSS scores.
8095
8523
  * Prefers CVSS v3 score; falls back to CVSS v2 if v3 is 0.
8096
8524
  * Impact is normalized to 0.0-1.0 by dividing by 10.
@@ -8119,29 +8547,66 @@ function vulnMessage(vuln) {
8119
8547
  if (!vuln.fixed_version) return `Vulnerable package ${vuln.package_name} is at version ${vuln.package_version}. No fixed version available.`;
8120
8548
  return `Vulnerable package ${vuln.package_name} is at version ${vuln.package_version}. Update to fixed version ${vuln.fixed_version}.`;
8121
8549
  }
8550
+ const WHITESPACE_RUN = /[ \t\n\r]+/g;
8551
+ const CODE_DESC_SNIPPET_RUNES = 100;
8552
+ /**
8553
+ * Returns the CVSS score used in code_desc: CVSS v3 preferred, else v2.
8554
+ */
8555
+ function cvssScore(vuln) {
8556
+ return vuln.score_v3 > 0 ? vuln.score_v3 : vuln.score;
8557
+ }
8558
+ /**
8559
+ * Collapses the description to a single line and truncates it to
8560
+ * CODE_DESC_SNIPPET_RUNES code points (Array.from matches Go's []rune).
8561
+ */
8562
+ function descSnippet(description) {
8563
+ const collapsed = description.replace(WHITESPACE_RUN, " ").trim();
8564
+ const runes = Array.from(collapsed);
8565
+ return runes.length > CODE_DESC_SNIPPET_RUNES ? runes.slice(0, CODE_DESC_SNIPPET_RUNES).join("") + "…" : collapsed;
8566
+ }
8567
+ /**
8568
+ * Builds the pipe-joined result code_desc from the fields the vuln carries:
8569
+ * package@version | name | CVSS score | description snippet. Only parts the
8570
+ * source actually provides are included.
8571
+ */
8572
+ function buildCodeDesc$3(vuln) {
8573
+ const parts = [];
8574
+ if (vuln.package_name) parts.push(vuln.package_version ? `${vuln.package_name}@${vuln.package_version}` : vuln.package_name);
8575
+ if (vuln.name) parts.push(vuln.name);
8576
+ const score = cvssScore(vuln);
8577
+ if (score > 0) parts.push(`CVSS ${score}`);
8578
+ const snippet = descSnippet(vuln.description);
8579
+ if (snippet) parts.push(snippet);
8580
+ return parts.join(" | ");
8581
+ }
8122
8582
  /**
8123
8583
  * Builds a single EvaluatedRequirement from a NeuVector vulnerability.
8124
8584
  */
8125
8585
  function buildRequirement$6(vuln, scanTime) {
8126
8586
  const cweIDs = extractCWEs(vuln.description);
8587
+ const cveIDs = extractCVEs(vuln);
8127
8588
  const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS);
8128
8589
  const tags = {
8129
8590
  nist,
8130
8591
  cci: nistToCci(nist)
8131
8592
  };
8132
- if (cweIDs.length > 0) tags["cwe"] = cweIDs;
8593
+ if (cveIDs.length > 0) tags["cve"] = cveIDs;
8133
8594
  const descriptions = [{
8134
8595
  label: "default",
8135
8596
  data: vuln.description
8136
8597
  }];
8137
8598
  const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
8138
- codeDesc: "",
8599
+ codeDesc: buildCodeDesc$3(vuln),
8139
8600
  startTime: scanTime
8140
8601
  })];
8141
8602
  const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
8603
+ req.code = JSON.stringify(vuln, null, 2);
8142
8604
  const controlType = deriveControlTypeFromTags(nist);
8143
8605
  if (controlType !== void 0) req.controlType = controlType;
8144
8606
  req.verificationMethod = VerificationMethodEnum.Automated;
8607
+ if (cweIDs.length > 0) req.cwe = cweIDs;
8608
+ const cvss = buildCvssEntries$1(vuln);
8609
+ if (cvss.length > 0) req.cvss = cvss;
8145
8610
  const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
8146
8611
  const pkg = buildAffectedPackage$1({
8147
8612
  name: vuln.package_name,
@@ -8203,7 +8668,6 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
8203
8668
  generatorName: "neuvector-to-hdf",
8204
8669
  converterVersion,
8205
8670
  toolName: "NeuVector",
8206
- toolFormat: "JSON",
8207
8671
  baselines: [createMinimalBaseline("NeuVector Scan", requirements, {
8208
8672
  resultsChecksum,
8209
8673
  title: imageTitle(scan.report)
@@ -8222,7 +8686,9 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
8222
8686
  //#endregion
8223
8687
  //#region converters/fortify-to-hdf/typescript/converter.ts
8224
8688
  const NIST_REFERENCE_NAME = "Standards Mapping - NIST Special Publication 800-53 Revision 4";
8689
+ const CWE_REFERENCE_NAME = "Standards Mapping - Common Weakness Enumeration";
8225
8690
  const NIST_PATTERN = /[a-zA-Z]{2}-\d+/g;
8691
+ const CWE_ID_PATTERN = /\d+/g;
8226
8692
  const NAMED_ENTITIES$1 = {
8227
8693
  lt: "<",
8228
8694
  gt: ">",
@@ -8262,6 +8728,18 @@ function extractNISTFromReferences(refs) {
8262
8728
  }
8263
8729
  return [];
8264
8730
  }
8731
+ function extractCWEFromReferences(refs) {
8732
+ for (const ref of refs) if (ref.Author === CWE_REFERENCE_NAME) {
8733
+ const matches = ref.Title?.match(CWE_ID_PATTERN);
8734
+ if (matches && matches.length > 0) return matches.map((m) => `CWE-${m}`);
8735
+ }
8736
+ return [];
8737
+ }
8738
+ function mergeCweNist(nist, cweIDs) {
8739
+ const merged = [...nist];
8740
+ for (const ctrl of mapCWEToNIST(cweIDs, [])) if (!merged.includes(ctrl)) merged.push(ctrl);
8741
+ return merged;
8742
+ }
8265
8743
  function formatSnippet(snippet) {
8266
8744
  const text = (snippet.Text ?? "").trim();
8267
8745
  return `Path: ${snippet.File ?? ""}\nStartLine: ${snippet.StartLine ?? ""}, EndLine: ${snippet.EndLine ?? ""}\nCode:\n${text}`;
@@ -8284,8 +8762,26 @@ function buildCodeDesc$2(vuln, snippetMap) {
8284
8762
  if (parts.length === 0) return `ClassID: ${vuln.ClassInfo?.ClassID ?? ""}, InstanceID: ${vuln.InstanceInfo?.InstanceID ?? ""}`;
8285
8763
  return parts.join("\n");
8286
8764
  }
8765
+ function buildRequirementCode(vulns, snippetMap) {
8766
+ if (vulns.length === 0) return void 0;
8767
+ const parts = [];
8768
+ const entries = ensureArray(vulns[0].AnalysisInfo?.Unified?.Trace?.Primary?.Entry);
8769
+ for (const entry of entries) {
8770
+ if (!entry.Node) continue;
8771
+ const snippetID = entry.Node.SourceLocation?.snippet;
8772
+ if (!snippetID) continue;
8773
+ const snippet = snippetMap.get(snippetID);
8774
+ if (!snippet) continue;
8775
+ const text = (snippet.Text ?? "").trim();
8776
+ if (text) parts.push(text);
8777
+ }
8778
+ if (parts.length === 0) return void 0;
8779
+ return parts.join("\n");
8780
+ }
8287
8781
  function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
8288
- let nistTags = extractNISTFromReferences(ensureArray(desc.References?.Reference));
8782
+ const refs = ensureArray(desc.References?.Reference);
8783
+ const cweIDs = extractCWEFromReferences(refs);
8784
+ let nistTags = mergeCweNist(extractNISTFromReferences(refs), cweIDs);
8289
8785
  if (nistTags.length === 0) nistTags = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1];
8290
8786
  const cciTags = nistToCci(nistTags);
8291
8787
  const tags = buildNistCciTags(nistTags, cciTags);
@@ -8301,7 +8797,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
8301
8797
  data: stripFvdlMarkup(desc.Recommendations)
8302
8798
  });
8303
8799
  let impact = 0;
8304
- if (vulns.length > 0) impact = parseFloat(vulns[0].ClassInfo?.DefaultSeverity ?? "0") / 5;
8800
+ if (vulns.length > 0) impact = parseFloat(vulns[0].InstanceInfo?.InstanceSeverity ?? "0") / 5;
8305
8801
  const { items: limitedVulns, truncated } = limitArray(vulns);
8306
8802
  /* v8 ignore next -- truncation only triggers with >100K items */
8307
8803
  if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
@@ -8319,8 +8815,11 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
8319
8815
  results,
8320
8816
  verificationMethod: VerificationMethodEnum.Automated
8321
8817
  };
8818
+ if (cweIDs.length > 0) req.cwe = cweIDs;
8322
8819
  const controlType = deriveControlTypeFromTags(nistTags);
8323
8820
  if (controlType !== void 0) req.controlType = controlType;
8821
+ const code = buildRequirementCode(vulns, snippetMap);
8822
+ if (code !== void 0) req.code = code;
8324
8823
  return req;
8325
8824
  }
8326
8825
  /**
@@ -8477,6 +8976,8 @@ function buildRequirement$4(rec, scanTime) {
8477
8976
  const controlType = deriveControlTypeFromTags(nist);
8478
8977
  if (controlType !== void 0) req.controlType = controlType;
8479
8978
  req.verificationMethod = VerificationMethodEnum.Automated;
8979
+ const cvss = buildCvssEntries(rec);
8980
+ if (cvss) req.cvss = cvss;
8480
8981
  if (rec["CVE ID"]) {
8481
8982
  const pkg = buildAffectedPackageFromRecord(rec);
8482
8983
  if (pkg) req.affectedPackages = [pkg];
@@ -8484,6 +8985,32 @@ function buildRequirement$4(rec, scanTime) {
8484
8985
  return req;
8485
8986
  }
8486
8987
  /**
8988
+ * Maps the CSV CVSS column to a structured cvss[] entry. Prisma emits a bare
8989
+ * base score with no vector, so version defaults to 3.1 via the shared helper.
8990
+ * Config rows carry "0.00" (or blank); only a positive score yields an entry,
8991
+ * and the source is the associated CVE when present.
8992
+ */
8993
+ function buildCvssEntries(rec) {
8994
+ const score = parseCvssScore(rec.CVSS);
8995
+ if (score === void 0) return void 0;
8996
+ return [buildCvss$2({
8997
+ version: cvssVersionFromString(void 0),
8998
+ baseScore: score,
8999
+ source: rec["CVE ID"] || void 0
9000
+ })];
9001
+ }
9002
+ /**
9003
+ * Parses the CVSS column, returning undefined for blank, non-numeric, or
9004
+ * non-positive scores (the placeholder value on config rows).
9005
+ */
9006
+ function parseCvssScore(field) {
9007
+ const s = field?.trim();
9008
+ if (!s) return void 0;
9009
+ const v = Number.parseFloat(s);
9010
+ if (!Number.isFinite(v) || v <= 0) return void 0;
9011
+ return v;
9012
+ }
9013
+ /**
8487
9014
  * Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
8488
9015
  * `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
8489
9016
  * mapped — unknown vendors fall back to `generic` rather than guessing.
@@ -8576,7 +9103,6 @@ async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
8576
9103
  generatorName: "prisma-to-hdf",
8577
9104
  converterVersion,
8578
9105
  toolName: "Prisma Cloud",
8579
- toolFormat: "CSV",
8580
9106
  baselines,
8581
9107
  components,
8582
9108
  timestamp: scanTime
@@ -8633,6 +9159,36 @@ function formatControlDesc(vuln) {
8633
9159
  return parts.join("\n");
8634
9160
  }
8635
9161
  /**
9162
+ * Returns the parsed Base-metric score from a CVSS block, or undefined when
9163
+ * there is no Base score or its value does not parse as a finite number.
9164
+ */
9165
+ function baseScoreFromBlock(block) {
9166
+ const scores = block?.score ?? [];
9167
+ for (const s of scores) if ((s.type ?? "").toLowerCase() === "base") {
9168
+ const v = parseFloat((s.value ?? "").trim());
9169
+ return Number.isFinite(v) ? v : void 0;
9170
+ }
9171
+ }
9172
+ /**
9173
+ * Assembles the structured cvss[] from the <cvss> (3.0) and <cvss31> (3.1)
9174
+ * classification blocks. Each block carrying a vector or a Base score becomes
9175
+ * one entry; the schema version derives from the vector prefix.
9176
+ */
9177
+ function buildNetsparkerCvss(classification) {
9178
+ const out = [];
9179
+ for (const block of [classification?.cvss, classification?.cvss31]) {
9180
+ const baseScore = baseScoreFromBlock(block);
9181
+ const vector = block?.vector ?? "";
9182
+ if (!vector && baseScore === void 0) continue;
9183
+ out.push(buildCvss$2({
9184
+ version: cvssVersionFromVector(vector),
9185
+ baseScore,
9186
+ baseVector: vector
9187
+ }));
9188
+ }
9189
+ return out;
9190
+ }
9191
+ /**
8636
9192
  * Performs dual NIST mapping from both CWE and OWASP IDs.
8637
9193
  * Returns a deduplicated sorted list of NIST controls, falling back to
8638
9194
  * DEFAULT_STATIC_ANALYSIS_NIST_TAGS if no mappings are found.
@@ -8704,6 +9260,10 @@ function buildRequirement$3(vuln, initiated) {
8704
9260
  const controlType = deriveControlTypeFromTags(nist);
8705
9261
  if (controlType !== void 0) req.controlType = controlType;
8706
9262
  req.verificationMethod = VerificationMethodEnum.Automated;
9263
+ const rawRequest = vuln["http-request"]?.content;
9264
+ if (rawRequest) req.code = rawRequest;
9265
+ const cvss = buildNetsparkerCvss(vuln.classification);
9266
+ if (cvss.length > 0) req.cvss = cvss;
8707
9267
  return req;
8708
9268
  }
8709
9269
  /**
@@ -8717,7 +9277,7 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
8717
9277
  if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
8718
9278
  validateInputSize(input, "netsparker");
8719
9279
  const resultsChecksum = await inputChecksum(input);
8720
- const parsed = parseXmlWithArrays(input, ["vulnerability"]);
9280
+ const parsed = parseXmlWithArrays(input, ["vulnerability", "score"]);
8721
9281
  const isInvicti = !!parsed["invicti-enterprise"];
8722
9282
  const data = parsed["invicti-enterprise"] ?? parsed["netsparker-enterprise"] ?? {};
8723
9283
  if (!data.vulnerabilities && !data.target) throw new Error("netsparker: invalid XML — missing expected root element");
@@ -8738,7 +9298,6 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
8738
9298
  generatorName: "netsparker-to-hdf",
8739
9299
  converterVersion,
8740
9300
  toolName,
8741
- toolFormat: "XML",
8742
9301
  baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
8743
9302
  resultsChecksum,
8744
9303
  title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
@@ -8872,7 +9431,6 @@ async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
8872
9431
  generatorName: "scoutsuite-to-hdf",
8873
9432
  converterVersion,
8874
9433
  toolName: "ScoutSuite",
8875
- toolFormat: "JSON",
8876
9434
  toolVersion: report.last_run.version,
8877
9435
  baselines: [baseline],
8878
9436
  components: [{
@@ -9036,7 +9594,6 @@ async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
9036
9594
  generatorName: "conveyor-to-hdf",
9037
9595
  converterVersion,
9038
9596
  toolName: "Conveyor",
9039
- toolFormat: "JSON",
9040
9597
  baselines,
9041
9598
  components: [{
9042
9599
  name: targetName,
@@ -9118,23 +9675,6 @@ function formatRecommendations(rec) {
9118
9675
  }
9119
9676
  return parts.join("\n");
9120
9677
  }
9121
- /** Format CWE data for the cweid tag. */
9122
- function formatCWEData(cwes) {
9123
- return cwes.map((c) => {
9124
- let entry = `CWE-${attr(c, "cweid")}: ${attr(c, "cwename")}`;
9125
- const categories = [
9126
- ["pcrirelated", attr(c, "pcirelated")],
9127
- ["owasp", attr(c, "owasp")],
9128
- ["sans", attr(c, "sans")],
9129
- ["certc", attr(c, "certc")],
9130
- ["certcpp", attr(c, "certcpp")],
9131
- ["certjava", attr(c, "certjava")],
9132
- ["owaspmobile", attr(c, "owaspmobile")]
9133
- ];
9134
- for (const [name, val] of categories) if (val) entry += `${name}: ${val}\n`;
9135
- return entry;
9136
- }).join("\n");
9137
- }
9138
9678
  /** Format CWE descriptions for the cweDescription tag. */
9139
9679
  function formatCWEDesc(cwes) {
9140
9680
  return cwes.map((c) => {
@@ -9195,6 +9735,54 @@ function formatSCACodeDesc(comp) {
9195
9735
  }
9196
9736
  return parts.join("\n");
9197
9737
  }
9738
+ /**
9739
+ * Synthesize a static flaw's source-context locus from its function prototype
9740
+ * and source-file position. Returns '' when the flaw carries neither a prototype
9741
+ * nor a source location (the NOT-IN-SOURCE case).
9742
+ */
9743
+ function synthesizeFlawCode(flaw) {
9744
+ let locus = attr(flaw, "sourcefilepath") + attr(flaw, "sourcefile");
9745
+ const line = attr(flaw, "line");
9746
+ if (locus && line) locus += `:${line}`;
9747
+ const proto = attr(flaw, "functionprototype");
9748
+ if (proto && locus) return `${proto} at ${locus}`;
9749
+ if (proto) return proto;
9750
+ return locus;
9751
+ }
9752
+ /**
9753
+ * Serialize a CVE and its affected components as indented JSON. Field order is
9754
+ * load-bearing: it must match the Go twin's struct declaration order so the two
9755
+ * `code` strings are byte-identical.
9756
+ */
9757
+ function buildSCACode(vuln, components) {
9758
+ const entry = {
9759
+ cve_id: attr(vuln, "cve_id"),
9760
+ cvss_score: attr(vuln, "cvss_score"),
9761
+ severity: attr(vuln, "severity"),
9762
+ cwe_id: attr(vuln, "cwe_id"),
9763
+ first_found_date: attr(vuln, "first_found_date"),
9764
+ cve_summary: attr(vuln, "cve_summary"),
9765
+ severity_desc: attr(vuln, "severity_desc"),
9766
+ components: components.map((comp) => {
9767
+ const filePathsElem = comp.file_paths;
9768
+ const fps = filePathsElem ? ensureArray(filePathsElem.file_path) : [];
9769
+ return {
9770
+ component_id: attr(comp, "component_id"),
9771
+ file_name: attr(comp, "file_name"),
9772
+ sha1: attr(comp, "sha1"),
9773
+ version: attr(comp, "version"),
9774
+ library: attr(comp, "library"),
9775
+ library_id: attr(comp, "library_id"),
9776
+ vendor: attr(comp, "vendor"),
9777
+ description: attr(comp, "description"),
9778
+ max_cvss_score: attr(comp, "max_cvss_score"),
9779
+ added_date: attr(comp, "added_date"),
9780
+ file_paths: fps.map((fp) => attr(fp, "value"))
9781
+ };
9782
+ })
9783
+ };
9784
+ return JSON.stringify(entry, null, 2);
9785
+ }
9198
9786
  /** Build CWE-based requirements from severity categories. */
9199
9787
  function buildCWERequirements(severities, firstBuildDate) {
9200
9788
  const requirements = [];
@@ -9208,14 +9796,14 @@ function buildCWERequirements(severities, firstBuildDate) {
9208
9796
  /** Build a single CWE-based requirement from a category. */
9209
9797
  function buildCWERequirement(cat, impact, firstBuildDate) {
9210
9798
  const cwes = ensureArray(cat.cwe);
9211
- const nist = mapCWEToNIST(cwes.map((c) => attr(c, "cweid")).filter(Boolean), DEFAULT_REMEDIATION_NIST_TAGS$1);
9799
+ const cweIDs = cwes.map((c) => attr(c, "cweid")).filter(Boolean);
9800
+ const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS$1);
9212
9801
  const cciTags = nistToCci(nist);
9213
9802
  const extras = {};
9214
- const cweData = formatCWEData(cwes);
9215
- if (cweData) extras.cweid = cweData;
9216
9803
  const cweDescStr = formatCWEDesc(cwes);
9217
9804
  if (cweDescStr) extras.cweDescription = cweDescStr;
9218
9805
  const tags = buildNistCciTags(nist, cciTags, extras);
9806
+ const cweList = cweIDs.map((id) => `CWE-${id}`);
9219
9807
  const descriptions = [{
9220
9808
  label: "default",
9221
9809
  data: formatDesc(cat.desc)
@@ -9237,6 +9825,10 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
9237
9825
  const staticflaws = c.staticflaws;
9238
9826
  return ensureArray(staticflaws?.flaw).map((flaw) => attr(flaw, "sourcefile")).filter(Boolean);
9239
9827
  }).join("\n");
9828
+ const codeLines = cwes.flatMap((c) => {
9829
+ const staticflaws = c.staticflaws;
9830
+ return ensureArray(staticflaws?.flaw).map((flaw) => synthesizeFlawCode(flaw)).filter(Boolean);
9831
+ });
9240
9832
  const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, {
9241
9833
  tags,
9242
9834
  sourceLocation: sourceRef ? { ref: sourceRef } : void 0
@@ -9244,6 +9836,8 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
9244
9836
  const controlType = deriveControlTypeFromTags(nist);
9245
9837
  if (controlType !== void 0) req.controlType = controlType;
9246
9838
  req.verificationMethod = VerificationMethodEnum.Automated;
9839
+ if (cweList.length > 0) req.cwe = cweList;
9840
+ if (codeLines.length > 0) req.code = codeLines.join("\n");
9247
9841
  return req;
9248
9842
  }
9249
9843
  /** Build CVE-based requirements from SCA components. */
@@ -9278,6 +9872,30 @@ function buildCVERequirements(sca, firstBuildDate) {
9278
9872
  return buildCVERequirement(entry.vuln, entry.components, firstBuildDate);
9279
9873
  });
9280
9874
  }
9875
+ /**
9876
+ * Assemble the structured CVSS entry for an SCA CVE. Veracode reports a bare
9877
+ * numeric base score (no vector, no version), so the version defaults to 3.1 via
9878
+ * the shared helper. When the vulnerability itself carries no cvss_score, the
9879
+ * first affected component's max_cvss_score is used as a fallback. A missing or
9880
+ * non-numeric score yields no entry.
9881
+ */
9882
+ function buildVeracodeCvss(vuln, components) {
9883
+ let scoreStr = attr(vuln, "cvss_score");
9884
+ if (!scoreStr) for (const comp of components) {
9885
+ const max = attr(comp, "max_cvss_score");
9886
+ if (max) {
9887
+ scoreStr = max;
9888
+ break;
9889
+ }
9890
+ }
9891
+ if (!scoreStr) return [];
9892
+ const score = Number(scoreStr);
9893
+ if (!Number.isFinite(score)) return [];
9894
+ return [buildCvss$2({
9895
+ version: cvssVersionFromString(void 0),
9896
+ baseScore: score
9897
+ })];
9898
+ }
9281
9899
  /** Build a single CVE-based requirement. */
9282
9900
  function buildCVERequirement(vuln, components, firstBuildDate) {
9283
9901
  const impact = veracodeSeverityToImpact(attr(vuln, "severity"));
@@ -9286,9 +9904,7 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
9286
9904
  if (cweID) nist = mapCWEToNIST([cweID.replace(/^CWE-/, "")], DEFAULT_REMEDIATION_NIST_TAGS$1);
9287
9905
  else nist = DEFAULT_REMEDIATION_NIST_TAGS$1;
9288
9906
  const cciTags = nistToCci(nist);
9289
- const extras = {};
9290
- if (cweID) extras.cwe = cweID;
9291
- const tags = buildNistCciTags(nist, cciTags, extras);
9907
+ const tags = buildNistCciTags(nist, cciTags, {});
9292
9908
  const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
9293
9909
  const results = components.map((comp) => createResult(ResultStatus.Failed, void 0, {
9294
9910
  codeDesc: formatSCACodeDesc(comp),
@@ -9312,6 +9928,10 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
9312
9928
  const controlType = deriveControlTypeFromTags(nist);
9313
9929
  if (controlType !== void 0) req.controlType = controlType;
9314
9930
  req.verificationMethod = VerificationMethodEnum.Automated;
9931
+ const cvss = buildVeracodeCvss(vuln, components);
9932
+ if (cvss.length > 0) req.cvss = cvss;
9933
+ if (cweID) req.cwe = [cweID];
9934
+ req.code = buildSCACode(vuln, components);
9315
9935
  return req;
9316
9936
  }
9317
9937
  /**
@@ -9357,7 +9977,6 @@ async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
9357
9977
  generatorName: "veracode-to-hdf",
9358
9978
  converterVersion,
9359
9979
  toolName: "Veracode",
9360
- toolFormat: "XML",
9361
9980
  baselines: [baseline],
9362
9981
  components: [{
9363
9982
  name: targetName,
@@ -9465,7 +10084,6 @@ async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
9465
10084
  generatorName: "msft-secure-score-to-hdf",
9466
10085
  converterVersion,
9467
10086
  toolName: "Microsoft Secure Score",
9468
- toolFormat: "JSON",
9469
10087
  baselines: combined.secureScore.value.map((ss) => {
9470
10088
  if (!tenantId) tenantId = ss.azureTenantId;
9471
10089
  const { items: limitedControlScores, truncated } = limitArray(ss.controlScores);
@@ -9701,7 +10319,6 @@ async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0")
9701
10319
  generatorName: "msft-defender-cloud-to-hdf",
9702
10320
  converterVersion,
9703
10321
  toolName: "Microsoft Defender for Cloud",
9704
- toolFormat: "JSON",
9705
10322
  baselines: [baseline],
9706
10323
  components,
9707
10324
  timestamp: scanTime
@@ -9977,8 +10594,6 @@ function buildRuleObj(req) {
9977
10594
  }
9978
10595
  const rationale = findDescription(req.descriptions, "rationale");
9979
10596
  if (rationale) rule.rationale = wrap(rationale);
9980
- const fixtext = findDescription(req.descriptions, "fix");
9981
- if (fixtext) rule.fixtext = wrap(fixtext);
9982
10597
  const idents = [];
9983
10598
  if (req.tags && Array.isArray(req.tags["cci"])) for (const cci of req.tags["cci"]) idents.push({
9984
10599
  [`${ATTR}system`]: "http://cyber.mil/cci",
@@ -9989,6 +10604,8 @@ function buildRuleObj(req) {
9989
10604
  "#text": n
9990
10605
  });
9991
10606
  if (idents.length > 0) rule.ident = idents;
10607
+ const fixtext = findDescription(req.descriptions, "fix");
10608
+ if (fixtext) rule.fixtext = wrap(fixtext);
9992
10609
  const checks = [];
9993
10610
  const checkContent = findDescription(req.descriptions, "check");
9994
10611
  if (checkContent) checks.push({
@@ -10019,12 +10636,19 @@ function buildTestResultObj(hdfData, baseline) {
10019
10636
  if (target.ipAddress) testResult["target-address"] = wrap(target.ipAddress);
10020
10637
  } else testResult.target = wrap("unknown");
10021
10638
  const ruleResults = [];
10639
+ let passed = 0;
10640
+ let scorable = 0;
10022
10641
  for (const req of baseline.requirements) {
10023
10642
  const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
10024
10643
  for (const result of req.results) {
10644
+ const status = hdfStatusToXccdf(result.status);
10645
+ if (status === "pass") {
10646
+ passed++;
10647
+ scorable++;
10648
+ } else if (status === "fail") scorable++;
10025
10649
  const rr = {
10026
10650
  [`${ATTR}idref`]: ruleIdRef,
10027
- result: wrap(hdfStatusToXccdf(result.status))
10651
+ result: wrap(status)
10028
10652
  };
10029
10653
  rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
10030
10654
  if (result.message) rr.message = wrap(result.message);
@@ -10036,6 +10660,12 @@ function buildTestResultObj(hdfData, baseline) {
10036
10660
  }
10037
10661
  }
10038
10662
  testResult["rule-result"] = ruleResults;
10663
+ const score = scorable > 0 ? passed / scorable * 100 : 0;
10664
+ testResult.score = {
10665
+ [`${ATTR}system`]: "urn:xccdf:scoring:default",
10666
+ [`${ATTR}maximum`]: "100.000000",
10667
+ "#text": score.toFixed(6)
10668
+ };
10039
10669
  return testResult;
10040
10670
  }
10041
10671
  //#endregion
@@ -10228,23 +10858,40 @@ async function convertHdfToOscalSar(input) {
10228
10858
  return JSON.stringify(doc, null, 2);
10229
10859
  }
10230
10860
  /**
10861
+ * Human-readable assessment-tool label from the HDF tool/generator identity,
10862
+ * falling back when neither is present.
10863
+ */
10864
+ function toolPartyName(hdfResults) {
10865
+ const tool = hdfResults.tool;
10866
+ if (tool?.name) return tool.version ? `${tool.name} ${tool.version}` : tool.name;
10867
+ const gen = hdfResults.generator;
10868
+ if (gen?.name) return gen.version ? `${gen.name} ${gen.version}` : gen.name;
10869
+ return "HDF Assessment Tool";
10870
+ }
10871
+ /**
10231
10872
  * Constructs the full OSCAL assessment-results document from HDF results.
10232
10873
  */
10233
10874
  function buildOSCALDocument(hdfResults) {
10234
10875
  let timestamp = formatTimestampSeconds(/* @__PURE__ */ new Date());
10235
10876
  const documentTime = hdfTime(hdfResults.timestamp);
10236
10877
  if (documentTime) timestamp = formatTimestampSeconds(documentTime);
10878
+ const toolActorUuid = crypto.randomUUID();
10237
10879
  const metadata = {
10238
10880
  title: "HDF Assessment Results Export",
10239
10881
  "last-modified": timestamp,
10240
10882
  version: "1.0.0",
10241
- "oscal-version": OSCAL_VERSION
10883
+ "oscal-version": OSCAL_VERSION,
10884
+ parties: [{
10885
+ uuid: toolActorUuid,
10886
+ type: "organization",
10887
+ name: toolPartyName(hdfResults)
10888
+ }]
10242
10889
  };
10243
10890
  let importAP;
10244
10891
  if (hdfResults.planRef && hdfResults.planRef !== "") importAP = { href: hdfResults.planRef };
10245
10892
  else importAP = { href: "#" };
10246
10893
  const results = [];
10247
- for (const baseline of hdfResults.baselines) results.push(baselineToResult(baseline, timestamp));
10894
+ for (const baseline of hdfResults.baselines) results.push(baselineToResult(baseline, timestamp, toolActorUuid));
10248
10895
  return { "assessment-results": {
10249
10896
  uuid: crypto.randomUUID(),
10250
10897
  metadata,
@@ -10283,7 +10930,7 @@ function assessmentStart(baseline, fallback) {
10283
10930
  /**
10284
10931
  * Converts a single EvaluatedBaseline to an OSCAL Result.
10285
10932
  */
10286
- function baselineToResult(baseline, timestamp) {
10933
+ function baselineToResult(baseline, timestamp, toolActorUuid) {
10287
10934
  let title = baseline.name;
10288
10935
  if (baseline.title && baseline.title !== "") title = baseline.title;
10289
10936
  let description = "Converted from HDF results";
@@ -10291,17 +10938,26 @@ function baselineToResult(baseline, timestamp) {
10291
10938
  const findings = [];
10292
10939
  const observations = [];
10293
10940
  const risks = [];
10941
+ const includeControls = [];
10942
+ const seenControl = /* @__PURE__ */ new Set();
10294
10943
  for (const req of baseline.requirements) {
10295
- const { finding, observation, risk } = requirementToFindingSet(req, timestamp);
10944
+ const { finding, observation, risk } = requirementToFindingSet(req, timestamp, toolActorUuid);
10296
10945
  findings.push(finding);
10297
10946
  if (observation) observations.push(observation);
10298
10947
  if (risk) risks.push(risk);
10948
+ const cid = nistTagToControlId(req.id);
10949
+ if (cid !== "" && !seenControl.has(cid)) {
10950
+ seenControl.add(cid);
10951
+ includeControls.push({ "control-id": cid });
10952
+ }
10299
10953
  }
10954
+ const controlSelection = includeControls.length > 0 ? { "include-controls": includeControls } : {};
10300
10955
  return {
10301
10956
  uuid: crypto.randomUUID(),
10302
10957
  title,
10303
10958
  description,
10304
10959
  start: assessmentStart(baseline, timestamp),
10960
+ "reviewed-controls": { "control-selections": [controlSelection] },
10305
10961
  findings,
10306
10962
  observations,
10307
10963
  risks
@@ -10310,49 +10966,39 @@ function baselineToResult(baseline, timestamp) {
10310
10966
  /**
10311
10967
  * Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
10312
10968
  */
10313
- function requirementToFindingSet(req, timestamp) {
10969
+ function requirementToFindingSet(req, timestamp, toolActorUuid) {
10314
10970
  const controlID = nistTagToControlId(req.id);
10315
- const { state, reason } = aggregateStatus(req.results);
10316
- const findingDesc = extractDefaultDescription(req.descriptions);
10971
+ const results = req.results ?? [];
10972
+ const descriptions = req.descriptions ?? [];
10973
+ const { state, reason } = aggregateStatus(results);
10974
+ const findingDesc = extractDefaultDescription(descriptions);
10317
10975
  const props = [];
10976
+ const addProp = (name, value) => {
10977
+ if (value !== "") props.push({
10978
+ name,
10979
+ value
10980
+ });
10981
+ };
10318
10982
  const pushTagValues = (key) => {
10319
10983
  const raw = req.tags?.[key];
10320
10984
  if (Array.isArray(raw)) {
10321
- for (const v of raw) if (typeof v === "string") props.push({
10322
- name: key,
10323
- value: v
10324
- });
10985
+ for (const v of raw) if (typeof v === "string") addProp(key, v);
10325
10986
  }
10326
10987
  };
10327
10988
  pushTagValues("nist");
10328
10989
  pushTagValues("cci");
10329
- if (req.code) props.push({
10330
- name: "code",
10331
- value: req.code
10332
- });
10990
+ if (req.code != null) addProp("code", req.code);
10333
10991
  for (const label of [
10334
10992
  "check",
10335
10993
  "fix",
10336
10994
  "rationale"
10337
10995
  ]) {
10338
- const d = req.descriptions.find((x) => x.label === label);
10339
- if (d) props.push({
10340
- name: label,
10341
- value: d.data
10342
- });
10996
+ const d = descriptions.find((x) => x.label === label);
10997
+ addProp(label, d ? d.data : "");
10343
10998
  }
10344
- if (req.controlType) props.push({
10345
- name: "control-type",
10346
- value: req.controlType
10347
- });
10348
- if (req.verificationMethod) props.push({
10349
- name: "verification-method",
10350
- value: req.verificationMethod
10351
- });
10352
- if (req.applicability) props.push({
10353
- name: "applicability",
10354
- value: req.applicability
10355
- });
10999
+ if (req.controlType) addProp("control-type", req.controlType);
11000
+ if (req.verificationMethod) addProp("verification-method", req.verificationMethod);
11001
+ if (req.applicability) addProp("applicability", req.applicability);
10356
11002
  const links = [];
10357
11003
  if (Array.isArray(req.refs)) for (const r of req.refs) {
10358
11004
  const o = r;
@@ -10381,19 +11027,19 @@ function requirementToFindingSet(req, timestamp) {
10381
11027
  const finding = {
10382
11028
  uuid: crypto.randomUUID(),
10383
11029
  title,
10384
- description: findingDesc || "",
11030
+ description: findingDesc || title,
10385
11031
  props: props.length > 0 ? props : void 0,
10386
11032
  links: links.length > 0 ? links : void 0,
10387
11033
  target
10388
11034
  };
10389
11035
  let observation;
10390
- if (req.results.length > 0) {
11036
+ if (results.length > 0) {
10391
11037
  const obsUUID = crypto.randomUUID();
10392
11038
  observation = {
10393
11039
  uuid: obsUUID,
10394
- description: buildObservationDescription(req.results),
11040
+ description: buildObservationDescription(results),
10395
11041
  methods: ["TEST"],
10396
- collected: formatAssessmentTime(earliestResultTime(req.results), timestamp)
11042
+ collected: formatAssessmentTime(earliestResultTime(results), timestamp)
10397
11043
  };
10398
11044
  finding["related-observations"] = [{ "observation-uuid": obsUUID }];
10399
11045
  }
@@ -10407,11 +11053,17 @@ function requirementToFindingSet(req, timestamp) {
10407
11053
  description: `Impact: ${req.impact.toFixed(1)} (${severity})`,
10408
11054
  statement: `Impact: ${req.impact.toFixed(1)} (${severity})`,
10409
11055
  status: riskStatusFromState(state),
10410
- characterizations: [{ facets: [{
10411
- name: "impact",
10412
- system: "https://fedramp.gov",
10413
- value: severity
10414
- }] }]
11056
+ characterizations: [{
11057
+ origin: { actors: [{
11058
+ type: "party",
11059
+ "actor-uuid": toolActorUuid
11060
+ }] },
11061
+ facets: [{
11062
+ name: "impact",
11063
+ system: "https://fedramp.gov",
11064
+ value: severity
11065
+ }]
11066
+ }]
10415
11067
  };
10416
11068
  finding["related-risks"] = [{ "risk-uuid": riskUUID }];
10417
11069
  }
@@ -10711,15 +11363,56 @@ function buildTags(dep) {
10711
11363
  if (dep.parentDependencies.length > 0) extras.parentDependencies = dep.parentDependencies;
10712
11364
  return buildNistCciTags(nist, cciTags, extras);
10713
11365
  }
11366
+ function titleCaseFirst(s) {
11367
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
11368
+ }
11369
+ function buildScanRequirement(scan) {
11370
+ const title = scan.description || `${titleCaseFirst(scan.name)} scan`;
11371
+ const descriptions = [createDescription("default", scan.summary || `${titleCaseFirst(scan.name)} scan summary`)];
11372
+ const results = [{
11373
+ status: ResultStatus.NotReviewed,
11374
+ codeDesc: `${scan.name} scan summary`,
11375
+ startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
11376
+ }];
11377
+ const req = createRequirement(`scan-${scan.name}`, title, descriptions, 0, results, { tags: {
11378
+ name: scan.name,
11379
+ type: scan.results?.type ?? ""
11380
+ } });
11381
+ req.code = JSON.stringify(scan.results?.data ?? {}, null, 2);
11382
+ req.verificationMethod = VerificationMethodEnum.Automated;
11383
+ return req;
11384
+ }
11385
+ function verdictDescription(a) {
11386
+ let desc = `Ion Channel analysis verdict: ${a.passed ? "PASSED" : "FAILED"}`;
11387
+ if (a.risk) desc += ` (risk: ${a.risk})`;
11388
+ if (a.ruleset_name) {
11389
+ desc += `. Ruleset: ${a.ruleset_name}`;
11390
+ if (a.ruleset_id) desc += ` (${a.ruleset_id})`;
11391
+ }
11392
+ return `${desc}.`;
11393
+ }
11394
+ function verdictLabels(a) {
11395
+ const labels = { passed: String(a.passed) };
11396
+ if (a.risk) labels.risk = a.risk;
11397
+ if (a.ruleset_name) labels.ruleset_name = a.ruleset_name;
11398
+ if (a.ruleset_id) labels.ruleset_id = a.ruleset_id;
11399
+ return labels;
11400
+ }
10714
11401
  async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
10715
11402
  if (!input?.trim()) throw new Error("Empty input");
10716
11403
  validateInputSize(input, "ionchannel");
10717
11404
  const analysis = parseJSON(input);
10718
11405
  if (!Array.isArray(analysis.scan_summaries)) throw new Error(`Ion Channel scan_summaries invalid summary data (expecting array, got ${typeof analysis.scan_summaries})`);
10719
11406
  let allDeps = [];
10720
- for (const scan of analysis.scan_summaries) if (scan.name === "dependency") {
10721
- allDeps = scan.results?.data?.dependencies ?? [];
10722
- break;
11407
+ const nonDepScans = [];
11408
+ let foundDep = false;
11409
+ for (const scan of analysis.scan_summaries) {
11410
+ if (scan.name === "dependency" && !foundDep) {
11411
+ allDeps = scan.results?.data?.dependencies ?? [];
11412
+ foundDep = true;
11413
+ continue;
11414
+ }
11415
+ nonDepScans.push(scan);
10723
11416
  }
10724
11417
  const requirements = buildDependencyGraph(allDeps).map((dep) => {
10725
11418
  const depId = `dependency-${dep.org}/${dep.name}`;
@@ -10760,12 +11453,27 @@ async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
10760
11453
  status: "loaded"
10761
11454
  });
10762
11455
  baseline.maintainer = "saf@groups.mitre.org";
11456
+ baseline.description = verdictDescription(analysis);
11457
+ baseline.labels = verdictLabels(analysis);
11458
+ const baselines = [baseline];
11459
+ for (const scan of nonDepScans) {
11460
+ const scanBaseline = createMinimalBaseline(`Ion Channel ${scan.name} Scan`, [buildScanRequirement(scan)], {
11461
+ title: `Ion Channel Analysis of ${analysis.source}`,
11462
+ summary: scan.summary,
11463
+ integrity: {
11464
+ algorithm: resultsChecksum.algorithm,
11465
+ checksum: resultsChecksum.value
11466
+ },
11467
+ status: "loaded"
11468
+ });
11469
+ scanBaseline.maintainer = "saf@groups.mitre.org";
11470
+ baselines.push(scanBaseline);
11471
+ }
10763
11472
  return buildHdfResults({
10764
11473
  generatorName: "ionchannel-to-hdf",
10765
11474
  converterVersion,
10766
11475
  toolName: "Ion Channel",
10767
- toolFormat: "JSON",
10768
- baselines: [baseline],
11476
+ baselines,
10769
11477
  timestamp: /* @__PURE__ */ new Date()
10770
11478
  });
10771
11479
  }
@@ -11738,7 +12446,8 @@ function buildDocument(amendments, converterVersion) {
11738
12446
  ...notes.length > 0 && { notes },
11739
12447
  publisher: {
11740
12448
  category: "other",
11741
- name: publisherName
12449
+ name: publisherName,
12450
+ namespace: "https://saf.mitre.org"
11742
12451
  },
11743
12452
  tracking: {
11744
12453
  id: trackingId,
@@ -12048,7 +12757,11 @@ function buildMetadata(a, docTime, converterVersion) {
12048
12757
  }
12049
12758
  async function buildSerialNumber(input, a) {
12050
12759
  if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
12051
- return `urn:uuid:${(await sha256(input)).slice(0, 32)}`;
12760
+ const c = (await sha256(input)).slice(0, 32).split("");
12761
+ c[12] = "5";
12762
+ c[16] = (parseInt(c[16], 16) & 3 | 8).toString(16);
12763
+ const h = c.join("");
12764
+ return `urn:uuid:${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
12052
12765
  }
12053
12766
  //#endregion
12054
12767
  //#region converters/oscal-to-hdf/typescript/detect.ts
@@ -13071,6 +13784,225 @@ function substituteParams(text, overrides) {
13071
13784
  return result;
13072
13785
  }
13073
13786
  //#endregion
13074
- export { convertAsffToHdf, convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDefectDojoToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToAsff, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToEcs, convertHdfToOcsf, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToSplunk, convertHdfToXccdf, convertHdfToXml, convertHipcheckToHdf, 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 };
13787
+ //#region shared/typescript/stix.ts
13788
+ /** Parse and validate a STIX 2.1 bundle. Objects are kept raw for lossless carriage. */
13789
+ function parseStixBundle(input) {
13790
+ if (!input) throw new Error("stix: empty input");
13791
+ let raw;
13792
+ try {
13793
+ raw = JSON.parse(input);
13794
+ } catch (e) {
13795
+ throw new Error(`stix: parsing bundle: ${e.message}`);
13796
+ }
13797
+ const obj = raw ?? {};
13798
+ if (obj.type !== "bundle") throw new Error(`stix: not a STIX bundle (type=${JSON.stringify(obj.type)})`);
13799
+ if (!Array.isArray(obj.objects)) throw new Error("stix: bundle has no objects[]");
13800
+ return {
13801
+ type: "bundle",
13802
+ id: typeof obj.id === "string" ? obj.id : void 0,
13803
+ objects: obj.objects.filter((o) => typeof o === "object" && o !== null && !Array.isArray(o))
13804
+ };
13805
+ }
13806
+ /** Report whether the input looks like a STIX 2.1 bundle, without throwing. */
13807
+ function detectStixBundle(input) {
13808
+ try {
13809
+ const o = JSON.parse(input);
13810
+ return o?.type === "bundle" && Array.isArray(o.objects);
13811
+ } catch {
13812
+ return false;
13813
+ }
13814
+ }
13815
+ /** A STIX object's id ('' if absent). */
13816
+ function stixObjectId(obj) {
13817
+ return typeof obj.id === "string" ? obj.id : "";
13818
+ }
13819
+ /**
13820
+ * The CVE ids a STIX object cites via external_references (source_name "cve").
13821
+ * A STIX vulnerability records its CVE this way, not in a native field
13822
+ * (STIX 2.1 §4.19).
13823
+ */
13824
+ function stixObjectCVEs(obj) {
13825
+ const refs = obj.external_references;
13826
+ if (!Array.isArray(refs)) return [];
13827
+ const cves = [];
13828
+ for (const r of refs) {
13829
+ const ref = r;
13830
+ if (typeof ref.source_name === "string" && ref.source_name.toLowerCase() === "cve" && typeof ref.external_id === "string" && ref.external_id) cves.push(ref.external_id);
13831
+ }
13832
+ return cves;
13833
+ }
13834
+ //#endregion
13835
+ //#region shared/typescript/enrichStix.ts
13836
+ const DEFAULT_REVIEW_HORIZON_MS = 2160 * 60 * 60 * 1e3;
13837
+ /** Format a Date as canonical trimmed-UTC RFC3339 (seconds precision, matching Go's time.RFC3339). */
13838
+ function toRfc3339(d) {
13839
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
13840
+ }
13841
+ /**
13842
+ * Overlay a STIX 2.1 bundle onto an existing HDF results document, attaching
13843
+ * each STIX object as an inert externalReferences[] entry: a CVE-bearing object
13844
+ * attaches to the finding whose requirementId is that CVE (fanning out to every
13845
+ * match), and everything else — non-CVE objects and CVEs with no matching
13846
+ * finding — attaches to the results root. Each entry carries the raw STIX object
13847
+ * losslessly in `document`.
13848
+ *
13849
+ * Informational only: authors no overrides and fabricates no status/impact (the
13850
+ * E:A recompute is a separate, opt-in step). The results document is preserved
13851
+ * verbatim except for the appended references — manipulated structurally so
13852
+ * every pre-existing field, including timestamp strings, round-trips unchanged.
13853
+ * TypeScript peer of shared/go/enrich_stix.go (kept at parity).
13854
+ */
13855
+ function enrichStix(resultsInput, bundleInput, opts) {
13856
+ validateInputSize(resultsInput, "enrich-stix", opts?.maxSize);
13857
+ validateInputSize(bundleInput, "enrich-stix", opts?.maxSize);
13858
+ if (!resultsInput) throw new Error("enrich-stix: empty results input");
13859
+ const bundle = parseStixBundle(bundleInput);
13860
+ let parsed;
13861
+ try {
13862
+ parsed = JSON.parse(resultsInput);
13863
+ } catch (e) {
13864
+ throw new Error(`enrich-stix: parsing results: ${e.message}`);
13865
+ }
13866
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("enrich-stix: results input is not a JSON object");
13867
+ const doc = parsed;
13868
+ const reqById = indexRequirementsById(doc);
13869
+ for (const obj of bundle.objects) {
13870
+ let matched = false;
13871
+ for (const cve of stixObjectCVEs(obj)) for (const req of reqById.get(cve) ?? []) {
13872
+ appendExternalReference(req, buildStixRef(obj, "investigate"));
13873
+ matched = true;
13874
+ }
13875
+ if (!matched) appendExternalReference(doc, buildStixRef(obj, "reference"));
13876
+ }
13877
+ if (opts?.recomputeCvss) recomputeExploitation(bundle, reqById, opts);
13878
+ return JSON.stringify(doc, null, 2);
13879
+ }
13880
+ /**
13881
+ * Author an inline riskAdjustment on each CVE-matched finding that has a 3.1 base
13882
+ * vector and whose CVE carries a structural exploitation signal in the bundle.
13883
+ * Applies CVSS Exploit Maturity E:H (the 3.1 analog of 4.0 E:A) and recomputes
13884
+ * the Threat score. Skips (no fabrication) when a base vector is absent or non-3.1.
13885
+ * TypeScript peer of recomputeExploitation in enrich_stix.go.
13886
+ */
13887
+ function recomputeExploitation(bundle, reqById, opts) {
13888
+ const appliedAt = opts.asOf ?? /* @__PURE__ */ new Date();
13889
+ const expiresAt = new Date(appliedAt.getTime() + (opts.reviewHorizonMs ?? DEFAULT_REVIEW_HORIZON_MS));
13890
+ for (const [cve, src] of exploitedCves(bundle)) for (const req of reqById.get(cve) ?? []) {
13891
+ const entry = findBaseVectorEntry(req, cve);
13892
+ if (!entry) continue;
13893
+ const baseVector = entry.baseVector;
13894
+ let score;
13895
+ try {
13896
+ score = computeCvssScore(`${baseVector}/E:H`);
13897
+ } catch {
13898
+ continue;
13899
+ }
13900
+ const ov = buildRiskAdjustment(cve, baseVector, score, src, appliedAt, expiresAt);
13901
+ req.statusOverrides = [...Array.isArray(req.statusOverrides) ? req.statusOverrides : [], ov];
13902
+ }
13903
+ }
13904
+ /**
13905
+ * Map each CVE with a structural exploitation signal in the bundle to the STIX
13906
+ * object providing it: a sighting (sighting_of_ref), a targets/exploits
13907
+ * relationship (target_ref), or an indicator/report (object_refs).
13908
+ */
13909
+ function exploitedCves(bundle) {
13910
+ const vulnCves = /* @__PURE__ */ new Map();
13911
+ for (const o of bundle.objects) if (o.type === "vulnerability") {
13912
+ const id = stixObjectId(o);
13913
+ vulnCves.set(id, [...vulnCves.get(id) ?? [], ...stixObjectCVEs(o)]);
13914
+ }
13915
+ const exploited = /* @__PURE__ */ new Map();
13916
+ const mark = (vulnId, src) => {
13917
+ for (const cve of vulnCves.get(vulnId) ?? []) if (!exploited.has(cve)) exploited.set(cve, src);
13918
+ };
13919
+ for (const o of bundle.objects) if (o.type === "sighting" && typeof o.sighting_of_ref === "string") mark(o.sighting_of_ref, o);
13920
+ else if (o.type === "relationship" && (o.relationship_type === "targets" || o.relationship_type === "exploits") && typeof o.target_ref === "string") mark(o.target_ref, o);
13921
+ else if ((o.type === "indicator" || o.type === "report") && Array.isArray(o.object_refs)) {
13922
+ for (const r of o.object_refs) if (typeof r === "string") mark(r, o);
13923
+ }
13924
+ return exploited;
13925
+ }
13926
+ /** The finding's cvss[] entry carrying a base vector for the CVE (or a version-agnostic entry). */
13927
+ function findBaseVectorEntry(req, cve) {
13928
+ const arr = req.cvss;
13929
+ if (!Array.isArray(arr)) return void 0;
13930
+ for (const e of arr) {
13931
+ const m = e;
13932
+ const bv = m.baseVector;
13933
+ if (typeof bv === "string" && bv) {
13934
+ const id = m.id;
13935
+ if (id === void 0 || id === cve) return m;
13936
+ }
13937
+ }
13938
+ }
13939
+ /** Build the inline riskAdjustment recording the E:H-recomputed Threat score + STIX source. */
13940
+ function buildRiskAdjustment(cve, baseVector, score, src, appliedAt, expiresAt) {
13941
+ return {
13942
+ type: "riskAdjustment",
13943
+ reason: `${cve} actively exploited per STIX threat intelligence (${stixObjectId(src)}); CVSS Threat recomputed with Exploit Maturity E:H.`,
13944
+ impact: { value: roundImpact(score.temporalScore / 10) },
13945
+ appliedBy: {
13946
+ type: "other",
13947
+ identifier: "hdf-enrich"
13948
+ },
13949
+ appliedAt: toRfc3339(appliedAt),
13950
+ expiresAt: toRfc3339(expiresAt),
13951
+ cvss: {
13952
+ version: "3.1",
13953
+ id: cve,
13954
+ baseVector,
13955
+ baseScore: score.baseScore,
13956
+ threatVector: "E:H",
13957
+ threatScore: score.temporalScore,
13958
+ computedScore: score.temporalScore
13959
+ },
13960
+ externalReferences: [buildStixRef(src, "evidence")]
13961
+ };
13962
+ }
13963
+ /** Map each finding id to the requirement objects carrying it (fan-out across baselines). */
13964
+ function indexRequirementsById(doc) {
13965
+ const index = /* @__PURE__ */ new Map();
13966
+ const baselines = Array.isArray(doc.baselines) ? doc.baselines : [];
13967
+ for (const b of baselines) {
13968
+ const reqs = b?.requirements;
13969
+ if (!Array.isArray(reqs)) continue;
13970
+ for (const r of reqs) {
13971
+ const req = r;
13972
+ if (typeof req.id === "string" && req.id) {
13973
+ const list = index.get(req.id) ?? [];
13974
+ list.push(req);
13975
+ index.set(req.id, list);
13976
+ }
13977
+ }
13978
+ }
13979
+ return index;
13980
+ }
13981
+ /** Build an External_Reference envelope carrying the raw STIX object in `document`. `rel` is an open token. */
13982
+ function buildStixRef(obj, rel) {
13983
+ const ref = {
13984
+ sourceName: "stix",
13985
+ kind: "threat-intel",
13986
+ rel,
13987
+ document: obj
13988
+ };
13989
+ const id = stixObjectId(obj);
13990
+ if (id) ref.externalId = id;
13991
+ else ref.description = stixFallbackDescription(obj);
13992
+ return ref;
13993
+ }
13994
+ /** Human-readable description for an id-less STIX object: its name, else a
13995
+ * type-derived label, else a generic one — so its reference satisfies anyOf. */
13996
+ function stixFallbackDescription(obj) {
13997
+ if (typeof obj.name === "string" && obj.name) return obj.name;
13998
+ if (typeof obj.type === "string" && obj.type) return `STIX ${obj.type} object`;
13999
+ return "STIX object";
14000
+ }
14001
+ /** Append a reference to a container's externalReferences[], creating it if absent. */
14002
+ function appendExternalReference(container, ref) {
14003
+ container.externalReferences = [...Array.isArray(container.externalReferences) ? container.externalReferences : [], ref];
14004
+ }
14005
+ //#endregion
14006
+ export { convertAsffToHdf, convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDefectDojoToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToAsff, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToEcs, convertHdfToOcsf, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToSplunk, convertHdfToXccdf, convertHdfToXml, convertHipcheckToHdf, 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, detectStixBundle, enrichStix, isHDFV1, parseStixBundle };
13075
14007
 
13076
14008
  //# sourceMappingURL=index.js.map