@mitre/hdf-converters 3.3.1 → 3.4.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/README.md +4 -0
- package/dist/detect.d.ts +0 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/index.d.ts +44 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2572 -490
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/package.json +9 -7
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-DxZN6IJ4.js";
|
|
2
2
|
import { flattenOverlays } from "@mitre/hdf-parsers";
|
|
3
|
-
import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, 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 } from "@mitre/hdf-schema";
|
|
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";
|
|
5
5
|
import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
|
|
6
6
|
//#region shared/typescript/converterutil.ts
|
|
7
7
|
/**
|
|
@@ -153,6 +153,34 @@ function validateInputSize(input, converterName, maxSize = DEFAULT_MAX_INPUT_SIZ
|
|
|
153
153
|
if (input.length > maxSize) throw new Error(`${converterName}: input exceeds maximum allowed size of ${maxSize} characters`);
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
+
* Parse an HDF document. The single ingest point for every HDF-consuming
|
|
157
|
+
* converter: real HDF carries zone-less timestamps (InSpec emits startTime with
|
|
158
|
+
* no offset), so the raw JSON is normalized to canonical trimmed-UTC RFC3339
|
|
159
|
+
* before parsing — otherwise the non-canonical value is laundered into the
|
|
160
|
+
* converter's output (and Go, whose schema types decode date-time into
|
|
161
|
+
* time.Time, rejects the document outright).
|
|
162
|
+
*/
|
|
163
|
+
function parseHdf(input) {
|
|
164
|
+
return parseJSON(normalizeHdfTimestamps(input));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Read a timestamp field off a parsed HDF document.
|
|
168
|
+
*
|
|
169
|
+
* The generated schema types these as `Date` (startTime, appliedAt, ...), but
|
|
170
|
+
* JSON.parse does not revive Dates, so at runtime they are strings. Reaching for
|
|
171
|
+
* `new Date(value)` to bridge that gap reads a zone-less timestamp as host-local,
|
|
172
|
+
* where Go reads it as UTC — so the same document converts differently depending
|
|
173
|
+
* on the machine. parseTimestamp reads it as UTC, matching Go.
|
|
174
|
+
*
|
|
175
|
+
* Returns null when the field is absent or unparseable; callers decide the
|
|
176
|
+
* fallback, since a missing time is not the same as the epoch.
|
|
177
|
+
*/
|
|
178
|
+
function hdfTime(value) {
|
|
179
|
+
if (value instanceof Date) return value;
|
|
180
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
181
|
+
return parseTimestamp(String(value));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
156
184
|
* Normalise a value that may be a single item, an array, undefined, or null
|
|
157
185
|
* into a guaranteed array.
|
|
158
186
|
*
|
|
@@ -505,6 +533,7 @@ function convertResult(v1Result) {
|
|
|
505
533
|
if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
|
|
506
534
|
v2Result.startTime = legacyStartTime(v1Result.start_time);
|
|
507
535
|
if (v1Result.message !== void 0) v2Result.message = v1Result.message;
|
|
536
|
+
else if (v1Result.skip_message !== void 0) v2Result.message = v1Result.skip_message;
|
|
508
537
|
if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
|
|
509
538
|
if (v1Result.backtrace !== void 0) v2Result.backtrace = v1Result.backtrace;
|
|
510
539
|
if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
|
|
@@ -512,6 +541,59 @@ function convertResult(v1Result) {
|
|
|
512
541
|
return v2Result;
|
|
513
542
|
}
|
|
514
543
|
/**
|
|
544
|
+
* Convert a v1 ref value (a string or an array of objects) to the v2
|
|
545
|
+
* Reference.ref union value. Returns undefined when there is no content (e.g.
|
|
546
|
+
* an empty array), so callers can drop empty references — matching the Go
|
|
547
|
+
* converter's toRef.
|
|
548
|
+
*/
|
|
549
|
+
function toRefValue(raw) {
|
|
550
|
+
if (typeof raw === "string") return raw === "" ? void 0 : raw;
|
|
551
|
+
if (Array.isArray(raw)) {
|
|
552
|
+
const maps = raw.filter((e) => !!e && typeof e === "object" && !Array.isArray(e));
|
|
553
|
+
return maps.length === 0 ? void 0 : maps;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Convert a single v1 refs[] element (a bare string or an object with
|
|
558
|
+
* ref/url/uri) to a v2 Reference object. Returns null when the element carries
|
|
559
|
+
* no usable content. Key order (ref, url, uri) matches the Go Reference struct.
|
|
560
|
+
*/
|
|
561
|
+
function convertRef(raw) {
|
|
562
|
+
if (typeof raw === "string") return raw === "" ? null : { ref: raw };
|
|
563
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
564
|
+
const m = raw;
|
|
565
|
+
const out = {};
|
|
566
|
+
let has = false;
|
|
567
|
+
const refVal = toRefValue(m.ref);
|
|
568
|
+
if (refVal !== void 0) {
|
|
569
|
+
out.ref = refVal;
|
|
570
|
+
has = true;
|
|
571
|
+
}
|
|
572
|
+
if (typeof m.url === "string" && m.url !== "") {
|
|
573
|
+
out.url = m.url;
|
|
574
|
+
has = true;
|
|
575
|
+
}
|
|
576
|
+
if (typeof m.uri === "string" && m.uri !== "") {
|
|
577
|
+
out.uri = m.uri;
|
|
578
|
+
has = true;
|
|
579
|
+
}
|
|
580
|
+
return has ? out : null;
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Map v1 control-level refs to v2 requirement refs, dropping empty/contentless
|
|
586
|
+
* entries. Returns undefined when nothing maps.
|
|
587
|
+
*/
|
|
588
|
+
function convertRefs(refs) {
|
|
589
|
+
const out = [];
|
|
590
|
+
for (const raw of refs) {
|
|
591
|
+
const ref = convertRef(raw);
|
|
592
|
+
if (ref) out.push(ref);
|
|
593
|
+
}
|
|
594
|
+
return out.length ? out : void 0;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
515
597
|
* Convert v1.0 control to v2.0 requirement.
|
|
516
598
|
* Transforms field names and structure.
|
|
517
599
|
*/
|
|
@@ -524,6 +606,10 @@ function convertControl(v1Control) {
|
|
|
524
606
|
if (v1Control.descriptions !== void 0) v2Req.descriptions = v1Control.descriptions;
|
|
525
607
|
if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
|
|
526
608
|
if (v1Control.code !== void 0) v2Req.code = v1Control.code;
|
|
609
|
+
if (Array.isArray(v1Control.refs)) {
|
|
610
|
+
const refs = convertRefs(v1Control.refs);
|
|
611
|
+
if (refs) v2Req.refs = refs;
|
|
612
|
+
}
|
|
527
613
|
if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
|
|
528
614
|
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
|
|
529
615
|
if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
|
|
@@ -590,6 +676,26 @@ function convertDependency(v1Dep) {
|
|
|
590
676
|
return v2Dep;
|
|
591
677
|
}
|
|
592
678
|
/**
|
|
679
|
+
* Map v1 profile `supports` entries (InSpec hyphenated keys) to v2
|
|
680
|
+
* SupportedPlatform objects. Entries that map no recognized key are dropped.
|
|
681
|
+
* Key order (platform, platformFamily, platformName, release) matches the Go
|
|
682
|
+
* SupportedPlatform struct. Returns undefined when nothing maps.
|
|
683
|
+
*/
|
|
684
|
+
function convertSupports(supports) {
|
|
685
|
+
const out = [];
|
|
686
|
+
for (const raw of supports) {
|
|
687
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
688
|
+
const s = raw;
|
|
689
|
+
const sp = {};
|
|
690
|
+
if (typeof s.platform === "string" && s.platform !== "") sp.platform = s.platform;
|
|
691
|
+
if (typeof s["platform-family"] === "string" && s["platform-family"] !== "") sp.platformFamily = s["platform-family"];
|
|
692
|
+
if (typeof s["platform-name"] === "string" && s["platform-name"] !== "") sp.platformName = s["platform-name"];
|
|
693
|
+
if (typeof s.release === "string" && s.release !== "") sp.release = s.release;
|
|
694
|
+
if (Object.keys(sp).length > 0) out.push(sp);
|
|
695
|
+
}
|
|
696
|
+
return out.length ? out : void 0;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
593
699
|
* Convert v1.0 profile to v2.0 baseline.
|
|
594
700
|
* Transforms field names and nested structures.
|
|
595
701
|
*/
|
|
@@ -602,7 +708,10 @@ function convertProfile(v1Profile) {
|
|
|
602
708
|
if (v1Profile.license !== void 0) v2Baseline.license = v1Profile.license;
|
|
603
709
|
if (v1Profile.copyright !== void 0) v2Baseline.copyright = v1Profile.copyright;
|
|
604
710
|
if (v1Profile.copyright_email !== void 0) v2Baseline.copyrightEmail = v1Profile.copyright_email;
|
|
605
|
-
if (v1Profile.supports?.length)
|
|
711
|
+
if (v1Profile.supports?.length) {
|
|
712
|
+
const supports = convertSupports(v1Profile.supports);
|
|
713
|
+
if (supports) v2Baseline.supports = supports;
|
|
714
|
+
}
|
|
606
715
|
if (v1Profile.attributes?.length) v2Baseline.inputs = v1Profile.attributes;
|
|
607
716
|
if (v1Profile.status !== void 0) v2Baseline.status = v1Profile.status;
|
|
608
717
|
if (v1Profile.sha256) v2Baseline.integrity = {
|
|
@@ -652,7 +761,7 @@ function convertV1ToV2(v1Data) {
|
|
|
652
761
|
type: "host",
|
|
653
762
|
name: v1Data.platform.name
|
|
654
763
|
};
|
|
655
|
-
if (v1Data.platform.target_id) {
|
|
764
|
+
if (v1Data.platform.target_id || v1Data.platform.release !== void 0) {
|
|
656
765
|
component.osName = v1Data.platform.name;
|
|
657
766
|
if (v1Data.platform.release !== void 0) component.osVersion = v1Data.platform.release;
|
|
658
767
|
}
|
|
@@ -695,7 +804,7 @@ const IMPACT_MAPPING$9 = {
|
|
|
695
804
|
warning: .5,
|
|
696
805
|
note: .3
|
|
697
806
|
};
|
|
698
|
-
async function convertSarifToHdf(input) {
|
|
807
|
+
async function convertSarifToHdf(input, converterVersion = "1.0.0") {
|
|
699
808
|
validateInputSize(input, "sarif");
|
|
700
809
|
const resultsChecksum = await inputChecksum(input);
|
|
701
810
|
const sarif = parseJSON(input);
|
|
@@ -708,12 +817,11 @@ async function convertSarifToHdf(input) {
|
|
|
708
817
|
const timestamp = /* @__PURE__ */ new Date();
|
|
709
818
|
return buildHdfResults({
|
|
710
819
|
generatorName: "sarif-to-hdf",
|
|
711
|
-
converterVersion
|
|
820
|
+
converterVersion,
|
|
712
821
|
toolName: firstDriver?.name,
|
|
713
822
|
toolVersion: firstDriver?.version,
|
|
714
823
|
toolFormat: "SARIF",
|
|
715
824
|
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
|
|
716
|
-
components: [],
|
|
717
825
|
timestamp
|
|
718
826
|
});
|
|
719
827
|
}
|
|
@@ -837,9 +945,9 @@ function convertSarifResultToHDFResults(result) {
|
|
|
837
945
|
const locationlessResult = {
|
|
838
946
|
status,
|
|
839
947
|
codeDesc: "No source location",
|
|
840
|
-
startTime: /* @__PURE__ */ new Date()
|
|
841
|
-
backtrace
|
|
948
|
+
startTime: /* @__PURE__ */ new Date()
|
|
842
949
|
};
|
|
950
|
+
if (backtrace.length > 0) locationlessResult.backtrace = backtrace;
|
|
843
951
|
if (suppressionJustification) locationlessResult.message = `Suppressed: ${suppressionJustification}`;
|
|
844
952
|
results.push(locationlessResult);
|
|
845
953
|
}
|
|
@@ -891,28 +999,17 @@ function extractCweFromRule(rule) {
|
|
|
891
999
|
if (cweIds.length > 0) return cweIds;
|
|
892
1000
|
}
|
|
893
1001
|
if (rule.properties?.tags && Array.isArray(rule.properties.tags)) {
|
|
894
|
-
const cweTagPattern = /^CWE-\d+$/;
|
|
895
1002
|
const cweIds = [];
|
|
896
|
-
for (const tag of rule.properties.tags)
|
|
1003
|
+
for (const tag of rule.properties.tags) {
|
|
1004
|
+
if (typeof tag !== "string") continue;
|
|
1005
|
+
cweIds.push(...extractCWEIDs(tag).map((id) => `CWE-${id}`));
|
|
1006
|
+
}
|
|
897
1007
|
if (cweIds.length > 0) return cweIds;
|
|
898
1008
|
}
|
|
899
1009
|
return [];
|
|
900
1010
|
}
|
|
901
1011
|
function extractCweIds(text) {
|
|
902
|
-
|
|
903
|
-
if (!matches) return [];
|
|
904
|
-
const cweIds = [];
|
|
905
|
-
for (const match of matches) {
|
|
906
|
-
const content = match.slice(1, -1);
|
|
907
|
-
if (content.includes("CWE-")) {
|
|
908
|
-
const parts = content.split(/,\s*|!\//);
|
|
909
|
-
for (const part of parts) {
|
|
910
|
-
const trimmed = part.trim();
|
|
911
|
-
if (trimmed.startsWith("CWE-")) cweIds.push(trimmed);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return cweIds;
|
|
1012
|
+
return extractCWEIDs(text).map((id) => `CWE-${id}`);
|
|
916
1013
|
}
|
|
917
1014
|
function mapKindToStatus(kind) {
|
|
918
1015
|
switch (kind) {
|
|
@@ -1008,21 +1105,31 @@ function createHDFResult(location, status, backtrace, suppressionMessage) {
|
|
|
1008
1105
|
const snippet = location.physicalLocation?.region?.snippet?.text;
|
|
1009
1106
|
let codeDesc = `URL : ${uri} LINE : ${line} COLUMN : ${column}`;
|
|
1010
1107
|
if (snippet) codeDesc = `${codeDesc}\n${snippet}`;
|
|
1011
|
-
|
|
1108
|
+
const result = createResult(status, void 0, {
|
|
1012
1109
|
codeDesc,
|
|
1013
|
-
startTime: /* @__PURE__ */ new Date()
|
|
1014
|
-
backtrace
|
|
1110
|
+
startTime: /* @__PURE__ */ new Date()
|
|
1015
1111
|
});
|
|
1112
|
+
delete result.message;
|
|
1113
|
+
if (suppressionMessage) result.message = `Suppressed: ${suppressionMessage}`;
|
|
1114
|
+
if (backtrace.length > 0) result.backtrace = backtrace;
|
|
1115
|
+
return result;
|
|
1016
1116
|
}
|
|
1017
1117
|
//#endregion
|
|
1018
1118
|
//#region converters/junit-to-hdf/typescript/converter.ts
|
|
1019
1119
|
const DEFAULT_NIST = ["SA-11"];
|
|
1020
|
-
|
|
1120
|
+
/**
|
|
1121
|
+
* The shared XML parser runs with `processEntities: false` (XXE defense), so
|
|
1122
|
+
* character references reach us undecoded. Surefire escapes quotes and angle
|
|
1123
|
+
* brackets in failure messages, so decode them here.
|
|
1124
|
+
*/
|
|
1125
|
+
function decodeXmlEntities$5(s) {
|
|
1126
|
+
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
1127
|
+
}
|
|
1021
1128
|
const ARRAY_TAGS$2 = ["testsuite", "testcase"];
|
|
1022
1129
|
/**
|
|
1023
1130
|
* Converts JUnit XML test results to HDF format.
|
|
1024
1131
|
*/
|
|
1025
|
-
async function convertJunitToHdf(input) {
|
|
1132
|
+
async function convertJunitToHdf(input, converterVersion = "1.0.0") {
|
|
1026
1133
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
1027
1134
|
validateInputSize(input, "junit");
|
|
1028
1135
|
const { suites, name } = parseJUnitXML(input);
|
|
@@ -1031,7 +1138,7 @@ async function convertJunitToHdf(input) {
|
|
|
1031
1138
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
|
|
1032
1139
|
return buildHdfResults({
|
|
1033
1140
|
generatorName: "junit-to-hdf",
|
|
1034
|
-
converterVersion
|
|
1141
|
+
converterVersion,
|
|
1035
1142
|
toolName: "JUnit XML",
|
|
1036
1143
|
toolFormat: "XML",
|
|
1037
1144
|
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
|
|
@@ -1053,14 +1160,15 @@ function parseJUnitXML(input) {
|
|
|
1053
1160
|
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
|
|
1054
1161
|
if (parsed.testsuites) return {
|
|
1055
1162
|
suites: parsed.testsuites.testsuite ?? [],
|
|
1056
|
-
name: parsed.testsuites.name
|
|
1163
|
+
name: parsed.testsuites.name ? decodeXmlEntities$5(parsed.testsuites.name) : "JUnit Test Results"
|
|
1057
1164
|
};
|
|
1058
1165
|
if (parsed.testsuite) {
|
|
1059
1166
|
const suite = parsed.testsuite;
|
|
1060
1167
|
const suites = Array.isArray(suite) ? suite : [suite];
|
|
1168
|
+
const suiteName = suites[0]?.name;
|
|
1061
1169
|
return {
|
|
1062
1170
|
suites,
|
|
1063
|
-
name:
|
|
1171
|
+
name: suiteName ? decodeXmlEntities$5(suiteName) : "JUnit Test Results"
|
|
1064
1172
|
};
|
|
1065
1173
|
}
|
|
1066
1174
|
throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
|
|
@@ -1082,28 +1190,29 @@ function buildRequirements(suites, scanTime) {
|
|
|
1082
1190
|
function testCaseToRequirement(tc, scanTime) {
|
|
1083
1191
|
const id = buildID(tc);
|
|
1084
1192
|
const { status, message } = resolveStatus(tc);
|
|
1085
|
-
const
|
|
1193
|
+
const result = {
|
|
1194
|
+
status,
|
|
1086
1195
|
codeDesc: buildCodeDesc$9(tc),
|
|
1087
1196
|
startTime: scanTime
|
|
1088
1197
|
};
|
|
1198
|
+
if (message !== void 0) result.message = message;
|
|
1089
1199
|
if (tc.time) {
|
|
1090
1200
|
const parsed = parseFloat(tc.time);
|
|
1091
|
-
if (!isNaN(parsed))
|
|
1201
|
+
if (!isNaN(parsed)) result.runTime = parsed;
|
|
1092
1202
|
}
|
|
1093
|
-
const
|
|
1094
|
-
const
|
|
1203
|
+
const name = decodeXmlEntities$5(tc.name);
|
|
1204
|
+
const req = createRequirement(id, name, [{
|
|
1095
1205
|
label: "default",
|
|
1096
|
-
data: `JUnit test: ${
|
|
1097
|
-
}];
|
|
1098
|
-
const req = createRequirement(id, tc.name, descriptions, .5, [result], { tags: { nist: DEFAULT_NIST } });
|
|
1206
|
+
data: `JUnit test: ${name} in ${tc.classname ? decodeXmlEntities$5(tc.classname) : "unknown"}`
|
|
1207
|
+
}], .5, [result], { tags: { nist: DEFAULT_NIST } });
|
|
1099
1208
|
const controlType = deriveControlTypeFromTags(DEFAULT_NIST);
|
|
1100
1209
|
if (controlType !== void 0) req.controlType = controlType;
|
|
1101
1210
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
1102
1211
|
return req;
|
|
1103
1212
|
}
|
|
1104
1213
|
function buildID(tc) {
|
|
1105
|
-
if (tc.classname) return `${tc.classname}.${tc.name}`;
|
|
1106
|
-
return tc.name;
|
|
1214
|
+
if (tc.classname) return `${decodeXmlEntities$5(tc.classname)}.${decodeXmlEntities$5(tc.name)}`;
|
|
1215
|
+
return decodeXmlEntities$5(tc.name);
|
|
1107
1216
|
}
|
|
1108
1217
|
function resolveStatus(tc) {
|
|
1109
1218
|
if (tc.failure) {
|
|
@@ -1124,7 +1233,7 @@ function resolveStatus(tc) {
|
|
|
1124
1233
|
const skipped = typeof tc.skipped === "object" ? tc.skipped : null;
|
|
1125
1234
|
if (skipped?.message) return {
|
|
1126
1235
|
status: ResultStatus.NotReviewed,
|
|
1127
|
-
message: `Skipped: ${skipped.message}`
|
|
1236
|
+
message: `Skipped: ${decodeXmlEntities$5(skipped.message)}`
|
|
1128
1237
|
};
|
|
1129
1238
|
return {
|
|
1130
1239
|
status: ResultStatus.NotReviewed,
|
|
@@ -1135,24 +1244,40 @@ function resolveStatus(tc) {
|
|
|
1135
1244
|
}
|
|
1136
1245
|
function buildFailureMessage(message, typeName, body) {
|
|
1137
1246
|
let result = "";
|
|
1138
|
-
if (typeName) result = `${typeName}: `;
|
|
1139
|
-
result += message;
|
|
1140
|
-
if (body) result += "\n" + body;
|
|
1247
|
+
if (typeName) result = `${decodeXmlEntities$5(typeName)}: `;
|
|
1248
|
+
result += decodeXmlEntities$5(message);
|
|
1249
|
+
if (body) result += "\n" + decodeXmlEntities$5(body);
|
|
1141
1250
|
return result;
|
|
1142
1251
|
}
|
|
1143
1252
|
function buildCodeDesc$9(tc) {
|
|
1144
|
-
if (tc.classname) return `${tc.classname} :: ${tc.name}`;
|
|
1145
|
-
return tc.name;
|
|
1253
|
+
if (tc.classname) return `${decodeXmlEntities$5(tc.classname)} :: ${decodeXmlEntities$5(tc.name)}`;
|
|
1254
|
+
return decodeXmlEntities$5(tc.name);
|
|
1146
1255
|
}
|
|
1147
1256
|
function noFindingsTarget(baselineName, suites) {
|
|
1148
1257
|
if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
|
|
1149
|
-
for (const s of suites) if (s.name) return s.name;
|
|
1258
|
+
for (const s of suites) if (s.name) return decodeXmlEntities$5(s.name);
|
|
1150
1259
|
return "JUnit test suite";
|
|
1151
1260
|
}
|
|
1152
1261
|
//#endregion
|
|
1153
1262
|
//#region converters/xccdf-results-to-hdf/typescript/converter.ts
|
|
1154
|
-
|
|
1155
|
-
|
|
1263
|
+
/**
|
|
1264
|
+
* Walk the Group tree depth-first, collecting every rule at any depth. Rules may
|
|
1265
|
+
* sit directly on a Group that also has nested Groups.
|
|
1266
|
+
*/
|
|
1267
|
+
function flattenGroups(groups) {
|
|
1268
|
+
const out = [];
|
|
1269
|
+
for (const group of groups) {
|
|
1270
|
+
for (const rule of group.Rule ?? []) {
|
|
1271
|
+
if (!rule.id) continue;
|
|
1272
|
+
out.push({
|
|
1273
|
+
rule,
|
|
1274
|
+
group
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
out.push(...flattenGroups(group.Group ?? []));
|
|
1278
|
+
}
|
|
1279
|
+
return out;
|
|
1280
|
+
}
|
|
1156
1281
|
/** Tags that must always be parsed as arrays even if only one element exists. */
|
|
1157
1282
|
const ARRAY_TAGS$1 = [
|
|
1158
1283
|
"Group",
|
|
@@ -1201,18 +1326,18 @@ function parseStartTime(raw) {
|
|
|
1201
1326
|
}
|
|
1202
1327
|
return /* @__PURE__ */ new Date();
|
|
1203
1328
|
}
|
|
1204
|
-
async function convertXccdfResultsToHdf(input) {
|
|
1329
|
+
async function convertXccdfResultsToHdf(input, converterVersion = "1.0.0") {
|
|
1205
1330
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
1206
1331
|
validateInputSize(input, "xccdf-results");
|
|
1207
|
-
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1);
|
|
1332
|
+
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1, { trimValues: false });
|
|
1208
1333
|
const arfParsed = parsed;
|
|
1209
|
-
if (arfParsed["asset-report-collection"]) return convertArfCollection(arfParsed["asset-report-collection"], input);
|
|
1334
|
+
if (arfParsed["asset-report-collection"]) return convertArfCollection(arfParsed["asset-report-collection"], input, converterVersion);
|
|
1210
1335
|
const benchmark = parsed.Benchmark;
|
|
1211
1336
|
if (!benchmark) throw new Error("Input is not an XCCDF document: expected <Benchmark> root element");
|
|
1212
1337
|
if (!benchmark.TestResult) throw new Error("Input has no TestResult elements — this is a benchmark. Use 'xccdf-benchmark' or 'xccdf' instead");
|
|
1213
|
-
return convertBenchmarkResultsToHdf(benchmark, input);
|
|
1338
|
+
return convertBenchmarkResultsToHdf(benchmark, input, converterVersion);
|
|
1214
1339
|
}
|
|
1215
|
-
async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
1340
|
+
async function convertBenchmarkResultsToHdf(benchmark, rawInput, converterVersion) {
|
|
1216
1341
|
const testResult = benchmark.TestResult;
|
|
1217
1342
|
const ruleIndex = buildRuleIndex(benchmark);
|
|
1218
1343
|
const ruleResults = testResult["rule-result"] ?? [];
|
|
@@ -1226,32 +1351,35 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
1226
1351
|
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
|
|
1227
1352
|
}
|
|
1228
1353
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
1229
|
-
const
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
if (start !== void 0 && end !== void 0 && end >= start) durationSeconds = (end - start) / 1e3;
|
|
1237
|
-
}
|
|
1238
|
-
const hdf = {
|
|
1354
|
+
const baselineName = extractText(benchmark.title) || benchmark.id || "";
|
|
1355
|
+
const baseline = createMinimalBaseline(baselineName, requirements, { resultsChecksum });
|
|
1356
|
+
baseline.title = baselineName;
|
|
1357
|
+
baseline.version = extractVersion$1(benchmark.version);
|
|
1358
|
+
baseline.status = "loaded";
|
|
1359
|
+
baseline.summary = stripHTML(extractText(benchmark.description));
|
|
1360
|
+
return serializeHdf({
|
|
1239
1361
|
baselines: [baseline],
|
|
1240
1362
|
generator: {
|
|
1241
1363
|
name: "xccdf-results-to-hdf",
|
|
1242
|
-
version:
|
|
1364
|
+
version: converterVersion
|
|
1243
1365
|
},
|
|
1244
1366
|
tool: {
|
|
1245
|
-
name: "XCCDF
|
|
1246
|
-
format: "
|
|
1367
|
+
name: "XCCDF",
|
|
1368
|
+
format: "XCCDF"
|
|
1247
1369
|
},
|
|
1248
|
-
components,
|
|
1249
|
-
timestamp
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
return serializeHdf(hdf);
|
|
1370
|
+
components: buildTargets(testResult),
|
|
1371
|
+
timestamp: scanTime,
|
|
1372
|
+
statistics: { duration: calculateDuration(testResult) }
|
|
1373
|
+
});
|
|
1253
1374
|
}
|
|
1254
|
-
|
|
1375
|
+
/** Seconds between the TestResult start-time and end-time; 0 when unavailable. */
|
|
1376
|
+
function calculateDuration(testResult) {
|
|
1377
|
+
const start = testResult["start-time"] ? parseTimestamp(testResult["start-time"])?.getTime() : void 0;
|
|
1378
|
+
const end = testResult["end-time"] ? parseTimestamp(testResult["end-time"])?.getTime() : void 0;
|
|
1379
|
+
if (start === void 0 || end === void 0 || end < start) return 0;
|
|
1380
|
+
return (end - start) / 1e3;
|
|
1381
|
+
}
|
|
1382
|
+
async function convertArfCollection(arc, rawInput, converterVersion) {
|
|
1255
1383
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
1256
1384
|
const benchmark = findBenchmarkInArf(arc);
|
|
1257
1385
|
const ruleIndex = benchmark ? buildRuleIndex(benchmark) : /* @__PURE__ */ new Map();
|
|
@@ -1266,16 +1394,9 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1266
1394
|
for (const report of arc.reports?.report ?? []) {
|
|
1267
1395
|
const testResult = report.content?.TestResult;
|
|
1268
1396
|
if (!testResult?.id) continue;
|
|
1269
|
-
if (testResult["start-time"] && !firstTimestamp) {
|
|
1270
|
-
const t = parseTimestamp(testResult["start-time"]);
|
|
1271
|
-
if (t) firstTimestamp = t;
|
|
1272
|
-
}
|
|
1273
|
-
if (testResult["start-time"] && testResult["end-time"]) {
|
|
1274
|
-
const start = parseTimestamp(testResult["start-time"])?.getTime();
|
|
1275
|
-
const end = parseTimestamp(testResult["end-time"])?.getTime();
|
|
1276
|
-
if (start !== void 0 && end !== void 0 && end >= start) totalDuration += (end - start) / 1e3;
|
|
1277
|
-
}
|
|
1278
1397
|
const scanTime = parseStartTime(testResult["start-time"]);
|
|
1398
|
+
if (!firstTimestamp) firstTimestamp = scanTime;
|
|
1399
|
+
totalDuration += calculateDuration(testResult);
|
|
1279
1400
|
const ruleResults = testResult["rule-result"] ?? [];
|
|
1280
1401
|
const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
|
|
1281
1402
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -1286,9 +1407,15 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1286
1407
|
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
|
|
1287
1408
|
}
|
|
1288
1409
|
let baselineName = "";
|
|
1289
|
-
if (benchmark) baselineName = extractText(benchmark.title) || "";
|
|
1290
|
-
if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "
|
|
1410
|
+
if (benchmark) baselineName = extractText(benchmark.title) || benchmark.id || "";
|
|
1411
|
+
if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "";
|
|
1291
1412
|
const baseline = createMinimalBaseline(baselineName, requirements, { resultsChecksum });
|
|
1413
|
+
baseline.title = baselineName;
|
|
1414
|
+
baseline.status = "loaded";
|
|
1415
|
+
if (benchmark) {
|
|
1416
|
+
baseline.version = extractVersion$1(benchmark.version);
|
|
1417
|
+
baseline.summary = stripHTML(extractText(benchmark.description));
|
|
1418
|
+
}
|
|
1292
1419
|
baselines.push(baseline);
|
|
1293
1420
|
const reportTargets = buildTargets(testResult);
|
|
1294
1421
|
const target = reportTargets[0];
|
|
@@ -1302,21 +1429,20 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1302
1429
|
components.push(...reportTargets);
|
|
1303
1430
|
}
|
|
1304
1431
|
if (baselines.length === 0) throw new Error("ARF document contains no XCCDF TestResult reports");
|
|
1305
|
-
|
|
1432
|
+
return serializeHdf({
|
|
1306
1433
|
baselines,
|
|
1307
1434
|
generator: {
|
|
1308
1435
|
name: "xccdf-results-to-hdf",
|
|
1309
|
-
version:
|
|
1436
|
+
version: converterVersion
|
|
1310
1437
|
},
|
|
1311
1438
|
tool: {
|
|
1312
1439
|
name: "ARF",
|
|
1313
1440
|
format: "ARF"
|
|
1314
1441
|
},
|
|
1315
1442
|
components,
|
|
1316
|
-
timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
return serializeHdf(hdf);
|
|
1443
|
+
timestamp: firstTimestamp ?? /* @__PURE__ */ new Date(),
|
|
1444
|
+
statistics: { duration: totalDuration }
|
|
1445
|
+
});
|
|
1320
1446
|
}
|
|
1321
1447
|
/**
|
|
1322
1448
|
* Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
|
|
@@ -1362,11 +1488,7 @@ function buildRuleIndex(benchmark) {
|
|
|
1362
1488
|
const index = /* @__PURE__ */ new Map();
|
|
1363
1489
|
const topRules = benchmark.Rule ?? [];
|
|
1364
1490
|
for (const rule of topRules) if (rule.id) index.set(rule.id, rule);
|
|
1365
|
-
const
|
|
1366
|
-
for (const group of groups) {
|
|
1367
|
-
const rules = group.Rule ?? [];
|
|
1368
|
-
for (const rule of rules) if (rule.id) index.set(rule.id, rule);
|
|
1369
|
-
}
|
|
1491
|
+
for (const { rule } of flattenGroups(benchmark.Group ?? [])) index.set(rule.id, rule);
|
|
1370
1492
|
return index;
|
|
1371
1493
|
}
|
|
1372
1494
|
/**
|
|
@@ -1375,39 +1497,29 @@ function buildRuleIndex(benchmark) {
|
|
|
1375
1497
|
function ruleResultToRequirement(rr, ruleIndex, scanTime) {
|
|
1376
1498
|
const ruleId = rr.idref ?? "";
|
|
1377
1499
|
const ruleDef = ruleIndex.get(ruleId);
|
|
1378
|
-
const id =
|
|
1379
|
-
const title = extractText(ruleDef?.title) ||
|
|
1380
|
-
const severity = rr.severity
|
|
1381
|
-
const impact = severity ? severityToImpact(severity) : .5;
|
|
1382
|
-
const descriptions = [
|
|
1383
|
-
const rawDesc = extractText(ruleDef?.description);
|
|
1384
|
-
if (rawDesc) descriptions.push({
|
|
1500
|
+
const id = ruleDef?.id ? extractRuleID(ruleDef.id) : ruleId;
|
|
1501
|
+
const title = extractText(ruleDef?.title) || ruleId;
|
|
1502
|
+
const severity = (rr.severity || ruleDef?.severity || "").toLowerCase();
|
|
1503
|
+
const impact = severity ? severityToImpact$1(severity) : .5;
|
|
1504
|
+
const descriptions = [{
|
|
1385
1505
|
label: "default",
|
|
1386
|
-
data: extractVulnDiscussion(
|
|
1387
|
-
}
|
|
1506
|
+
data: stripHTML(extractVulnDiscussion(extractText(ruleDef?.description)))
|
|
1507
|
+
}];
|
|
1388
1508
|
const fixtext = extractFixtext(ruleDef?.fixtext);
|
|
1389
1509
|
if (fixtext) descriptions.push({
|
|
1390
1510
|
label: "fix",
|
|
1391
|
-
data: fixtext
|
|
1511
|
+
data: stripHTML(fixtext)
|
|
1392
1512
|
});
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
data: ""
|
|
1396
|
-
});
|
|
1397
|
-
const status = STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed;
|
|
1513
|
+
const xccdfResult = (rr.result ?? "").trim().toLowerCase();
|
|
1514
|
+
const status = STATUS_MAP[xccdfResult] ?? ResultStatus.Error;
|
|
1398
1515
|
const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
|
|
1399
|
-
const result =
|
|
1400
|
-
|
|
1516
|
+
const result = {
|
|
1517
|
+
status,
|
|
1518
|
+
codeDesc: `XCCDF rule ${ruleId}`,
|
|
1401
1519
|
startTime: perRuleTime ?? scanTime
|
|
1402
|
-
}
|
|
1403
|
-
const tags =
|
|
1404
|
-
|
|
1405
|
-
const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
|
|
1406
|
-
if (cciIds.length > 0) {
|
|
1407
|
-
tags["cci"] = cciIds;
|
|
1408
|
-
nistTags = [...new Set(cciIds.flatMap((cci) => getCCINistMappings(cci) ?? []))];
|
|
1409
|
-
if (nistTags.length > 0) tags["nist"] = nistTags;
|
|
1410
|
-
}
|
|
1520
|
+
};
|
|
1521
|
+
const tags = buildCciNistTags(extractCCIs([...rr.ident ?? [], ...ruleDef?.ident ?? []]));
|
|
1522
|
+
const nistTags = tags["nist"];
|
|
1411
1523
|
const req = createRequirement(id, title, descriptions, impact, [result], { tags });
|
|
1412
1524
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
1413
1525
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -1438,19 +1550,29 @@ function buildTargets(testResult) {
|
|
|
1438
1550
|
const addresses = testResult["target-address"] ?? [];
|
|
1439
1551
|
const target = {
|
|
1440
1552
|
name: targetName,
|
|
1441
|
-
type: TargetType.Host
|
|
1442
|
-
labels: {}
|
|
1553
|
+
type: TargetType.Host
|
|
1443
1554
|
};
|
|
1444
1555
|
if (addresses.length > 0) target.ipAddress = addresses[0];
|
|
1445
1556
|
return [target];
|
|
1446
1557
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1558
|
+
const NAMED_ENTITIES$4 = {
|
|
1559
|
+
lt: "<",
|
|
1560
|
+
gt: ">",
|
|
1561
|
+
quot: "\"",
|
|
1562
|
+
apos: "'",
|
|
1563
|
+
amp: "&"
|
|
1564
|
+
};
|
|
1565
|
+
function decodeXmlEntities$4(s) {
|
|
1566
|
+
return s.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|(lt|gt|quot|apos|amp));/g, (match, dec, hex, name) => {
|
|
1567
|
+
if (dec !== void 0) return String.fromCodePoint(Number.parseInt(dec, 10));
|
|
1568
|
+
if (hex !== void 0) return String.fromCodePoint(Number.parseInt(hex, 16));
|
|
1569
|
+
return name !== void 0 ? NAMED_ENTITIES$4[name] ?? match : match;
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1450
1572
|
function extractText(field) {
|
|
1451
1573
|
if (field === void 0 || field === null) return "";
|
|
1452
|
-
if (typeof field === "string") return field;
|
|
1453
|
-
return field["#text"] ?? "";
|
|
1574
|
+
if (typeof field === "string") return decodeXmlEntities$4(field);
|
|
1575
|
+
return decodeXmlEntities$4(field["#text"] ?? "");
|
|
1454
1576
|
}
|
|
1455
1577
|
/**
|
|
1456
1578
|
* Extract version string from a version field.
|
|
@@ -1465,8 +1587,8 @@ function extractVersion$1(field) {
|
|
|
1465
1587
|
*/
|
|
1466
1588
|
function extractFixtext(field) {
|
|
1467
1589
|
if (field === void 0 || field === null) return "";
|
|
1468
|
-
if (typeof field === "string") return field;
|
|
1469
|
-
return field["#text"] ?? "";
|
|
1590
|
+
if (typeof field === "string") return decodeXmlEntities$4(field);
|
|
1591
|
+
return decodeXmlEntities$4(field["#text"] ?? "");
|
|
1470
1592
|
}
|
|
1471
1593
|
/**
|
|
1472
1594
|
* Extract the VulnDiscussion text from an XCCDF description that contains
|
|
@@ -1475,16 +1597,38 @@ function extractFixtext(field) {
|
|
|
1475
1597
|
*/
|
|
1476
1598
|
function extractVulnDiscussion(description) {
|
|
1477
1599
|
const match = description.match(/<VulnDiscussion>([\s\S]*?)<\/VulnDiscussion>/);
|
|
1478
|
-
|
|
1479
|
-
const entityMatch = description.match(/<VulnDiscussion>([\s\S]*?)<\/VulnDiscussion>/);
|
|
1480
|
-
if (entityMatch) return entityMatch[1].trim();
|
|
1481
|
-
return description;
|
|
1600
|
+
return match ? match[1] : description;
|
|
1482
1601
|
}
|
|
1483
1602
|
/**
|
|
1484
|
-
* Extract CCI identifiers from ident elements
|
|
1603
|
+
* Extract CCI identifiers from ident elements, deduplicated in first-seen order.
|
|
1485
1604
|
*/
|
|
1486
1605
|
function extractCCIs(idents) {
|
|
1487
|
-
|
|
1606
|
+
const ccis = idents.filter((i) => (i.system ?? "").toLowerCase().includes("cci")).map((i) => i["#text"] ?? "").filter((v) => v.length > 0);
|
|
1607
|
+
return [...new Set(ccis)];
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Build the cci/nist tag pair. `nist` is always present (empty when there are no
|
|
1611
|
+
* CCIs) and sorted, matching Go's cci.CCIToNIST.
|
|
1612
|
+
*/
|
|
1613
|
+
function buildCciNistTags(cciIds) {
|
|
1614
|
+
if (cciIds.length === 0) return { nist: [] };
|
|
1615
|
+
return {
|
|
1616
|
+
cci: cciIds,
|
|
1617
|
+
nist: [...new Set(cciIds.flatMap((c) => getCCINistMappings(c) ?? []))].sort()
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Extract the vulnerability ID from an XCCDF Rule ID:
|
|
1622
|
+
* "SV-254238r991589_rule" and "xccdf_mil.disa.stig_rule_SV-204393r603261_rule"
|
|
1623
|
+
* both yield "SV-254238"/"SV-204393". Non-SV IDs pass through unchanged.
|
|
1624
|
+
*/
|
|
1625
|
+
function extractRuleID(ruleID) {
|
|
1626
|
+
const svIdx = ruleID.toUpperCase().indexOf("SV-");
|
|
1627
|
+
if (svIdx < 0) return ruleID;
|
|
1628
|
+
const digits = ruleID.slice(svIdx + 3);
|
|
1629
|
+
const revIdx = digits.indexOf("r");
|
|
1630
|
+
if (revIdx > 0) return `SV-${digits.slice(0, revIdx)}`;
|
|
1631
|
+
return `SV-${digits}`;
|
|
1488
1632
|
}
|
|
1489
1633
|
//#endregion
|
|
1490
1634
|
//#region shared/typescript/checklist/status.ts
|
|
@@ -1672,6 +1816,8 @@ function cklVulnToModel(v) {
|
|
|
1672
1816
|
status: parseStatus(v.STATUS),
|
|
1673
1817
|
findingDetails: str(v.FINDING_DETAILS),
|
|
1674
1818
|
comments: str(v.COMMENTS),
|
|
1819
|
+
severityOverride: str(v.SEVERITY_OVERRIDE),
|
|
1820
|
+
severityJustification: str(v.SEVERITY_JUSTIFICATION),
|
|
1675
1821
|
extra
|
|
1676
1822
|
};
|
|
1677
1823
|
}
|
|
@@ -1760,7 +1906,9 @@ function modelVulnToCkl(v) {
|
|
|
1760
1906
|
STIG_DATA: stigData,
|
|
1761
1907
|
STATUS: statusToCkl(v.status),
|
|
1762
1908
|
FINDING_DETAILS: v.findingDetails ?? "",
|
|
1763
|
-
COMMENTS: v.comments ?? ""
|
|
1909
|
+
COMMENTS: v.comments ?? "",
|
|
1910
|
+
SEVERITY_OVERRIDE: v.severityOverride ?? "",
|
|
1911
|
+
SEVERITY_JUSTIFICATION: v.severityJustification ?? ""
|
|
1764
1912
|
};
|
|
1765
1913
|
}
|
|
1766
1914
|
//#endregion
|
|
@@ -1929,7 +2077,7 @@ function stigToBaseline(s, resultsChecksum, scanTime) {
|
|
|
1929
2077
|
}
|
|
1930
2078
|
function vulnToRequirement(v, scanTime) {
|
|
1931
2079
|
const severity = (v.severity ?? "").toLowerCase();
|
|
1932
|
-
const impact = severity ? severityToImpact(severity) : .5;
|
|
2080
|
+
const impact = severity ? severityToImpact$1(severity) : .5;
|
|
1933
2081
|
const descriptions = [{
|
|
1934
2082
|
label: "default",
|
|
1935
2083
|
data: stripHTML(v.vulnDiscuss ?? "")
|
|
@@ -1942,7 +2090,7 @@ function vulnToRequirement(v, scanTime) {
|
|
|
1942
2090
|
label: "fix",
|
|
1943
2091
|
data: stripHTML(v.fixText)
|
|
1944
2092
|
});
|
|
1945
|
-
const message =
|
|
2093
|
+
const message = (v.findingDetails ?? "").trim();
|
|
1946
2094
|
const result = createResult(statusToHdf(v.status), message, {
|
|
1947
2095
|
codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
|
|
1948
2096
|
startTime: scanTime
|
|
@@ -1954,12 +2102,13 @@ function vulnToRequirement(v, scanTime) {
|
|
|
1954
2102
|
nistTags = [...new Set(v.ccis.flatMap((c) => getCCINistMappings(c) ?? []))].sort();
|
|
1955
2103
|
tags["nist"] = nistTags;
|
|
1956
2104
|
} else tags["nist"] = [];
|
|
1957
|
-
setIf(tags, "rid", v.ruleID);
|
|
1958
|
-
setIf(tags, "stig_id", v.ruleVer);
|
|
1959
|
-
setIf(tags, "gtitle", v.groupTitle);
|
|
1960
|
-
setIf(tags, "group_id", v.groupID);
|
|
1961
|
-
setIf(tags, "weight", v.weight);
|
|
1962
|
-
setIf(tags, "
|
|
2105
|
+
setIf$1(tags, "rid", v.ruleID);
|
|
2106
|
+
setIf$1(tags, "stig_id", v.ruleVer);
|
|
2107
|
+
setIf$1(tags, "gtitle", v.groupTitle);
|
|
2108
|
+
setIf$1(tags, "group_id", v.groupID);
|
|
2109
|
+
setIf$1(tags, "weight", v.weight);
|
|
2110
|
+
setIf$1(tags, "comments", v.comments);
|
|
2111
|
+
setIf$1(tags, "severity", severity);
|
|
1963
2112
|
if (v.legacyIDs && v.legacyIDs.length) tags["legacy_ids"] = v.legacyIDs;
|
|
1964
2113
|
if (v.extra && Object.keys(v.extra).length > 0) tags["cklMetadata"] = { ...v.extra };
|
|
1965
2114
|
const req = createRequirement(v.vulnNum, v.ruleTitle ?? v.vulnNum, descriptions, impact, [result], { tags });
|
|
@@ -1974,6 +2123,7 @@ function assetToComponent(a) {
|
|
|
1974
2123
|
name: a.hostName || a.hostFQDN || a.hostIP || "",
|
|
1975
2124
|
type: TargetType.Host
|
|
1976
2125
|
};
|
|
2126
|
+
if (a.hostName) c.hostname = a.hostName;
|
|
1977
2127
|
if (a.hostIP) c.ipAddress = a.hostIP;
|
|
1978
2128
|
if (a.hostFQDN) c.fqdn = a.hostFQDN;
|
|
1979
2129
|
if (a.hostMAC) c.macAddress = a.hostMAC;
|
|
@@ -1983,30 +2133,30 @@ function rootExtensions(cl) {
|
|
|
1983
2133
|
const ext = { checklistFormat: cl.format || "ckl" };
|
|
1984
2134
|
if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
|
|
1985
2135
|
const ax = {};
|
|
1986
|
-
setIf(ax, "role", cl.asset.role);
|
|
1987
|
-
setIf(ax, "assetType", cl.asset.assetType);
|
|
1988
|
-
setIf(ax, "marking", cl.asset.marking);
|
|
1989
|
-
setIf(ax, "targetKey", cl.asset.targetKey);
|
|
1990
|
-
setIf(ax, "techArea", cl.asset.techArea);
|
|
1991
|
-
setIf(ax, "targetComment", cl.asset.targetComment);
|
|
1992
|
-
setIf(ax, "webDbSite", cl.asset.webDBSite);
|
|
1993
|
-
setIf(ax, "webDbInstance", cl.asset.webDBInstance);
|
|
1994
|
-
setIf(ax, "classification", cl.asset.classification);
|
|
2136
|
+
setIf$1(ax, "role", cl.asset.role);
|
|
2137
|
+
setIf$1(ax, "assetType", cl.asset.assetType);
|
|
2138
|
+
setIf$1(ax, "marking", cl.asset.marking);
|
|
2139
|
+
setIf$1(ax, "targetKey", cl.asset.targetKey);
|
|
2140
|
+
setIf$1(ax, "techArea", cl.asset.techArea);
|
|
2141
|
+
setIf$1(ax, "targetComment", cl.asset.targetComment);
|
|
2142
|
+
setIf$1(ax, "webDbSite", cl.asset.webDBSite);
|
|
2143
|
+
setIf$1(ax, "webDbInstance", cl.asset.webDBInstance);
|
|
2144
|
+
setIf$1(ax, "classification", cl.asset.classification);
|
|
1995
2145
|
if (cl.asset.webOrDatabase) ax["webOrDatabase"] = true;
|
|
1996
2146
|
if (Object.keys(ax).length > 0) ext["assetExtras"] = ax;
|
|
1997
2147
|
return ext;
|
|
1998
2148
|
}
|
|
1999
2149
|
function baselineExtensions(s) {
|
|
2000
2150
|
const ext = {};
|
|
2001
|
-
setIf(ext, "stigid", s.stigID);
|
|
2002
|
-
setIf(ext, "uuid", s.uuid);
|
|
2003
|
-
setIf(ext, "releaseInfo", s.releaseInfo);
|
|
2004
|
-
setIf(ext, "displayName", s.displayName);
|
|
2005
|
-
setIf(ext, "referenceIdentifier", s.referenceIdentifier);
|
|
2006
|
-
setIf(ext, "classification", s.classification);
|
|
2151
|
+
setIf$1(ext, "stigid", s.stigID);
|
|
2152
|
+
setIf$1(ext, "uuid", s.uuid);
|
|
2153
|
+
setIf$1(ext, "releaseInfo", s.releaseInfo);
|
|
2154
|
+
setIf$1(ext, "displayName", s.displayName);
|
|
2155
|
+
setIf$1(ext, "referenceIdentifier", s.referenceIdentifier);
|
|
2156
|
+
setIf$1(ext, "classification", s.classification);
|
|
2007
2157
|
return ext;
|
|
2008
2158
|
}
|
|
2009
|
-
function setIf(m, key, val) {
|
|
2159
|
+
function setIf$1(m, key, val) {
|
|
2010
2160
|
if (val) m[key] = val;
|
|
2011
2161
|
}
|
|
2012
2162
|
//#endregion
|
|
@@ -2018,7 +2168,7 @@ function setIf(m, key, val) {
|
|
|
2018
2168
|
* fields are synthesized best-effort so any HDF yields a valid checklist.
|
|
2019
2169
|
*/
|
|
2020
2170
|
function hdfToChecklist(input) {
|
|
2021
|
-
const hdf =
|
|
2171
|
+
const hdf = parseHdf(input);
|
|
2022
2172
|
if (!hdf || !Array.isArray(hdf.baselines) || hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
|
|
2023
2173
|
const ext = hdf.extensions ?? {};
|
|
2024
2174
|
const format = strVal(ext, "checklistFormat") || "ckl";
|
|
@@ -2036,7 +2186,8 @@ function buildAsset(hdf, ext) {
|
|
|
2036
2186
|
const asset = {};
|
|
2037
2187
|
const comp = hdf.components?.[0];
|
|
2038
2188
|
if (comp) {
|
|
2039
|
-
asset.hostName = comp.
|
|
2189
|
+
if (comp.hostname) asset.hostName = comp.hostname;
|
|
2190
|
+
else if (comp.name && comp.name !== comp.fqdn && comp.name !== comp.ipAddress) asset.hostName = comp.name;
|
|
2040
2191
|
asset.hostIP = comp.ipAddress;
|
|
2041
2192
|
asset.hostFQDN = comp.fqdn;
|
|
2042
2193
|
asset.hostMAC = comp.macAddress;
|
|
@@ -2090,6 +2241,7 @@ function requirementToVuln(req) {
|
|
|
2090
2241
|
legacyIDs: strSlice(tags, "legacy_ids"),
|
|
2091
2242
|
status: statusFromHdf(req.results?.[0]?.status),
|
|
2092
2243
|
findingDetails: req.results?.[0]?.message,
|
|
2244
|
+
comments: strVal(tags, "comments"),
|
|
2093
2245
|
extra: extractCklMetadata(tags)
|
|
2094
2246
|
};
|
|
2095
2247
|
}
|
|
@@ -2251,11 +2403,15 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
|
2251
2403
|
label: "default",
|
|
2252
2404
|
data: rep.description
|
|
2253
2405
|
}];
|
|
2254
|
-
const results = vulns.map((vuln) =>
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2406
|
+
const results = vulns.map((vuln) => {
|
|
2407
|
+
const result = createResult(ResultStatus.Failed, void 0, {
|
|
2408
|
+
codeDesc: formatDependencyPath(vuln.from),
|
|
2409
|
+
startTime: scanTime
|
|
2410
|
+
});
|
|
2411
|
+
delete result.message;
|
|
2412
|
+
return result;
|
|
2413
|
+
});
|
|
2414
|
+
const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact$1(rep.severity), results, { tags });
|
|
2259
2415
|
const controlType = deriveControlTypeFromTags(nist);
|
|
2260
2416
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2261
2417
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -2308,12 +2464,12 @@ function convertSingleProject(report, resultsChecksum, scanTime) {
|
|
|
2308
2464
|
* @param input - Snyk JSON or SARIF string
|
|
2309
2465
|
* @returns HDF JSON string
|
|
2310
2466
|
*/
|
|
2311
|
-
async function convertSnykToHdf(input) {
|
|
2467
|
+
async function convertSnykToHdf(input, converterVersion = "1.0.0") {
|
|
2312
2468
|
if (!input || input.trim().length === 0) throw new Error("snyk: empty input");
|
|
2313
2469
|
validateInputSize(input, "snyk");
|
|
2314
2470
|
registerAllFingerprints();
|
|
2315
2471
|
const detected = detectConverter(input);
|
|
2316
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
2472
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
2317
2473
|
const resultsChecksum = await inputChecksum(input);
|
|
2318
2474
|
const scanTime = /* @__PURE__ */ new Date();
|
|
2319
2475
|
const parsed = parseJSON(input);
|
|
@@ -2332,7 +2488,7 @@ async function convertSnykToHdf(input) {
|
|
|
2332
2488
|
}
|
|
2333
2489
|
return buildHdfResults({
|
|
2334
2490
|
generatorName: "snyk-to-hdf",
|
|
2335
|
-
converterVersion
|
|
2491
|
+
converterVersion,
|
|
2336
2492
|
toolName: "Snyk",
|
|
2337
2493
|
toolFormat: "JSON",
|
|
2338
2494
|
baselines,
|
|
@@ -2377,11 +2533,12 @@ function getFixInfo(fix) {
|
|
|
2377
2533
|
function getCVSSInfo(vuln, relatedVulns) {
|
|
2378
2534
|
const cvssData = {};
|
|
2379
2535
|
if (vuln.cvss && vuln.cvss.length > 0) cvssData.primary = vuln.cvss;
|
|
2380
|
-
|
|
2536
|
+
const related = (relatedVulns ?? []).filter((r) => r.cvss && r.cvss.length > 0).map((r) => ({
|
|
2381
2537
|
id: r.id,
|
|
2382
2538
|
dataSource: r.dataSource,
|
|
2383
2539
|
cvss: r.cvss
|
|
2384
2540
|
}));
|
|
2541
|
+
if (related.length > 0) cvssData.related = related;
|
|
2385
2542
|
return JSON.stringify(cvssData);
|
|
2386
2543
|
}
|
|
2387
2544
|
function getReferences(vuln, relatedVulns) {
|
|
@@ -2553,7 +2710,7 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
2553
2710
|
if (kev) requirement.kev = kev;
|
|
2554
2711
|
return requirement;
|
|
2555
2712
|
}
|
|
2556
|
-
async function convertGrypeToHdf(input) {
|
|
2713
|
+
async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
|
|
2557
2714
|
validateInputSize(input, "grype");
|
|
2558
2715
|
const resultsChecksum = await inputChecksum(input);
|
|
2559
2716
|
const grypeData = parseJSON(input);
|
|
@@ -2574,8 +2731,8 @@ async function convertGrypeToHdf(input) {
|
|
|
2574
2731
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
2575
2732
|
const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
|
|
2576
2733
|
return buildHdfResults({
|
|
2577
|
-
generatorName:
|
|
2578
|
-
converterVersion
|
|
2734
|
+
generatorName: "grype-to-hdf",
|
|
2735
|
+
converterVersion,
|
|
2579
2736
|
toolName: "Grype",
|
|
2580
2737
|
toolVersion: grypeData.descriptor?.version,
|
|
2581
2738
|
baselines: [baseline],
|
|
@@ -2590,7 +2747,6 @@ async function convertGrypeToHdf(input) {
|
|
|
2590
2747
|
//#region converters/nessus-to-hdf/typescript/converter.ts
|
|
2591
2748
|
const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
|
|
2592
2749
|
const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
2593
|
-
const converterVersion = "1.0.0";
|
|
2594
2750
|
const IMPACT_MAPPING$7 = {
|
|
2595
2751
|
"4": .9,
|
|
2596
2752
|
"3": .7,
|
|
@@ -2607,20 +2763,47 @@ const IMPACT_MAPPING$7 = {
|
|
|
2607
2763
|
function parseHtml(html) {
|
|
2608
2764
|
return html.replace(/<[^>]*>/g, "").trim();
|
|
2609
2765
|
}
|
|
2766
|
+
const NAMED_ENTITIES$3 = {
|
|
2767
|
+
lt: "<",
|
|
2768
|
+
gt: ">",
|
|
2769
|
+
quot: "\"",
|
|
2770
|
+
apos: "'",
|
|
2771
|
+
amp: "&"
|
|
2772
|
+
};
|
|
2773
|
+
/**
|
|
2774
|
+
* Resolve XML predefined and numeric character references. The shared parser
|
|
2775
|
+
* runs with `processEntities: false` (XXE defense), which also leaves `'`
|
|
2776
|
+
* and friends unresolved — so scan text would otherwise carry raw entity
|
|
2777
|
+
* markup into every title, description and message.
|
|
2778
|
+
*/
|
|
2779
|
+
function decodeXmlEntities$3(text) {
|
|
2780
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, ref) => {
|
|
2781
|
+
if (ref.startsWith("#x") || ref.startsWith("#X")) return String.fromCodePoint(Number.parseInt(ref.slice(2), 16));
|
|
2782
|
+
if (ref.startsWith("#")) return String.fromCodePoint(Number.parseInt(ref.slice(1), 10));
|
|
2783
|
+
return NAMED_ENTITIES$3[ref] ?? match;
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
/** Apply decodeXmlEntities to every string in a parsed XML document. */
|
|
2787
|
+
function decodeEntitiesDeep(value) {
|
|
2788
|
+
if (typeof value === "string") return decodeXmlEntities$3(value);
|
|
2789
|
+
if (Array.isArray(value)) return value.map(decodeEntitiesDeep);
|
|
2790
|
+
if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) value[k] = decodeEntitiesDeep(v);
|
|
2791
|
+
return value;
|
|
2792
|
+
}
|
|
2610
2793
|
/**
|
|
2611
2794
|
* Convert Nessus XML scan results to HDF format
|
|
2612
2795
|
*/
|
|
2613
|
-
async function convertNessusToHdf(nessusXml) {
|
|
2796
|
+
async function convertNessusToHdf(nessusXml, converterVersion = "1.0.0") {
|
|
2614
2797
|
validateInputSize(nessusXml, "nessus");
|
|
2615
2798
|
const resultsChecksum = await inputChecksum(nessusXml);
|
|
2616
|
-
const parsed = parseXmlWithArrays(nessusXml, [
|
|
2799
|
+
const parsed = decodeEntitiesDeep(parseXmlWithArrays(nessusXml, [
|
|
2617
2800
|
"preference",
|
|
2618
2801
|
"tag",
|
|
2619
2802
|
"ReportItem",
|
|
2620
2803
|
"ReportHost",
|
|
2621
2804
|
"cwe",
|
|
2622
2805
|
"cve"
|
|
2623
|
-
]);
|
|
2806
|
+
]));
|
|
2624
2807
|
const policyName = parsed.NessusClientData_v2.Policy.policyName;
|
|
2625
2808
|
const version = extractVersion(parsed);
|
|
2626
2809
|
const reportHosts = parsed.NessusClientData_v2.Report.ReportHost;
|
|
@@ -2903,7 +3086,7 @@ function buildTags$2(item, isCompliance) {
|
|
|
2903
3086
|
const cciTags = parseComplianceRef(item["compliance-reference"], "CCI");
|
|
2904
3087
|
tags.cci = cciTags;
|
|
2905
3088
|
const mappedControls = cciTags.flatMap((cci) => getCCINistMappings(cci) ?? []);
|
|
2906
|
-
tags.nist = [...new Set(mappedControls)];
|
|
3089
|
+
tags.nist = [...new Set(mappedControls)].sort();
|
|
2907
3090
|
} else {
|
|
2908
3091
|
const nistControls = getNessusNistControl(item["pluginFamily"], item["pluginID"]);
|
|
2909
3092
|
tags.nist = nistControls ? nistControls.split("|") : [];
|
|
@@ -2928,7 +3111,7 @@ function buildRefs(item) {
|
|
|
2928
3111
|
function buildResult$1(item, host, isCompliance) {
|
|
2929
3112
|
const status = getStatus$3(item, isCompliance);
|
|
2930
3113
|
const codeDesc = getCodeDesc(item);
|
|
2931
|
-
const message = item
|
|
3114
|
+
const message = isCompliance && item["compliance-actual-value"] ? item["compliance-actual-value"] : item.plugin_output;
|
|
2932
3115
|
const startTimeStr = getHostPropertyValue(host, "HOST_START");
|
|
2933
3116
|
const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
|
|
2934
3117
|
return {
|
|
@@ -2966,6 +3149,7 @@ function convertReportHostToTarget(host) {
|
|
|
2966
3149
|
name: hostName,
|
|
2967
3150
|
type: TargetType.Host
|
|
2968
3151
|
};
|
|
3152
|
+
if (hostProps["hostname"]) target.hostname = hostProps["hostname"];
|
|
2969
3153
|
if (isFQDN(hostName)) target.fqdn = hostName;
|
|
2970
3154
|
const hostIp = hostProps["host-ip"];
|
|
2971
3155
|
if (hostIp) target.ipAddress = hostIp;
|
|
@@ -2974,7 +3158,6 @@ function convertReportHostToTarget(host) {
|
|
|
2974
3158
|
if (hostProps["os"]) target.osVersion = hostProps["os"];
|
|
2975
3159
|
if (hostProps["mac-address"]) target.macAddress = hostProps["mac-address"].split("\n")[0];
|
|
2976
3160
|
if (hostProps["host-fqdn"]) target.fqdn = hostProps["host-fqdn"];
|
|
2977
|
-
target.labels = {};
|
|
2978
3161
|
return target;
|
|
2979
3162
|
}
|
|
2980
3163
|
function isFQDN(s) {
|
|
@@ -2986,7 +3169,7 @@ function isIPAddress(s) {
|
|
|
2986
3169
|
//#endregion
|
|
2987
3170
|
//#region converters/sonarqube-to-hdf/typescript/converter.ts
|
|
2988
3171
|
/**
|
|
2989
|
-
*
|
|
3172
|
+
* Deprecated legacy severity to impact mapping.
|
|
2990
3173
|
* Canonical reference: heimdall2 sonarqube-mapper.ts IMPACT_MAPPING.
|
|
2991
3174
|
*/
|
|
2992
3175
|
const SEVERITY_IMPACT_MAPPING = {
|
|
@@ -2996,6 +3179,58 @@ const SEVERITY_IMPACT_MAPPING = {
|
|
|
2996
3179
|
MINOR: .3,
|
|
2997
3180
|
INFO: 0
|
|
2998
3181
|
};
|
|
3182
|
+
/** MQR software-quality severity to impact mapping. */
|
|
3183
|
+
const MQR_IMPACT_MAPPING = {
|
|
3184
|
+
BLOCKER: 1,
|
|
3185
|
+
HIGH: .7,
|
|
3186
|
+
MEDIUM: .5,
|
|
3187
|
+
LOW: .3,
|
|
3188
|
+
INFO: 0
|
|
3189
|
+
};
|
|
3190
|
+
/** MQR severities ordered weakest to strongest. */
|
|
3191
|
+
const MQR_SEVERITY_RANK = {
|
|
3192
|
+
INFO: 0,
|
|
3193
|
+
LOW: 1,
|
|
3194
|
+
MEDIUM: 2,
|
|
3195
|
+
HIGH: 3,
|
|
3196
|
+
BLOCKER: 4
|
|
3197
|
+
};
|
|
3198
|
+
/**
|
|
3199
|
+
* Which severity axis drove a requirement's severity/impact. Emitted as the
|
|
3200
|
+
* severitySource tag so downstream gates can pin the meaning of severity.
|
|
3201
|
+
*/
|
|
3202
|
+
const SEVERITY_SOURCE_MQR = "mqr";
|
|
3203
|
+
const SEVERITY_SOURCE_LEGACY = "legacy";
|
|
3204
|
+
/**
|
|
3205
|
+
* Order by code unit, matching Go's sort.Strings (byte order). Not
|
|
3206
|
+
* localeCompare: locale-aware collation would order keys differently than the Go
|
|
3207
|
+
* converter and break output parity between the two implementations.
|
|
3208
|
+
*/
|
|
3209
|
+
const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
3210
|
+
/**
|
|
3211
|
+
* Pick the authoritative severity axis for an issue. In MQR mode SonarQube
|
|
3212
|
+
* deprecates the top-level severity and the UI reports impacts[], so impacts[]
|
|
3213
|
+
* wins whenever present; pre-MQR servers fall back to the legacy field. An issue
|
|
3214
|
+
* is rated per software quality, so the worst rating governs — matching how the
|
|
3215
|
+
* SonarQube UI buckets it. The legacy→MQR relationship is per-rule, not a
|
|
3216
|
+
* constant offset, so the legacy value can never be relabelled after the fact.
|
|
3217
|
+
*/
|
|
3218
|
+
function selectSeverity(issue) {
|
|
3219
|
+
const impacts = issue.impacts ?? [];
|
|
3220
|
+
if (impacts.length > 0) {
|
|
3221
|
+
const worst = impacts.reduce((a, b) => MQR_SEVERITY_RANK[b.severity] > MQR_SEVERITY_RANK[a.severity] ? b : a);
|
|
3222
|
+
return {
|
|
3223
|
+
severity: worst.severity,
|
|
3224
|
+
source: SEVERITY_SOURCE_MQR,
|
|
3225
|
+
impact: MQR_IMPACT_MAPPING[worst.severity] ?? .5
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
return {
|
|
3229
|
+
severity: issue.severity,
|
|
3230
|
+
source: SEVERITY_SOURCE_LEGACY,
|
|
3231
|
+
impact: SEVERITY_IMPACT_MAPPING[issue.severity] ?? .5
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
2999
3234
|
/**
|
|
3000
3235
|
* Default NIST tag for SonarQube findings without CWE mappings.
|
|
3001
3236
|
* SA-11 (Developer Security Testing and Evaluation) applies to all issue types —
|
|
@@ -3008,7 +3243,7 @@ const DEFAULT_NIST_TAGS = ["SA-11"];
|
|
|
3008
3243
|
* @param input - JSON string from SonarQube /api/issues/search endpoint
|
|
3009
3244
|
* @returns HDF JSON string
|
|
3010
3245
|
*/
|
|
3011
|
-
async function convertSonarqubeToHdf(input) {
|
|
3246
|
+
async function convertSonarqubeToHdf(input, converterVersion = "1.0.0") {
|
|
3012
3247
|
validateInputSize(input, "sonarqube");
|
|
3013
3248
|
const resultsChecksum = await inputChecksum(input);
|
|
3014
3249
|
const sonarData = parseJSON(input);
|
|
@@ -3027,12 +3262,13 @@ async function convertSonarqubeToHdf(input) {
|
|
|
3027
3262
|
if (!issuesByProject.has(projectKey)) issuesByProject.set(projectKey, []);
|
|
3028
3263
|
issuesByProject.get(projectKey).push(issue);
|
|
3029
3264
|
}
|
|
3265
|
+
const projectKeys = Array.from(issuesByProject.keys()).sort(byCodeUnit);
|
|
3030
3266
|
const baselines = [];
|
|
3031
|
-
for (const
|
|
3032
|
-
const baseline = convertProjectToBaseline(projectKey,
|
|
3267
|
+
for (const projectKey of projectKeys) {
|
|
3268
|
+
const baseline = convertProjectToBaseline(projectKey, issuesByProject.get(projectKey), componentMap, ruleMap, resultsChecksum);
|
|
3033
3269
|
baselines.push(baseline);
|
|
3034
3270
|
}
|
|
3035
|
-
let components =
|
|
3271
|
+
let components = projectKeys.map((projectKey) => ({
|
|
3036
3272
|
type: TargetType.Application,
|
|
3037
3273
|
name: projectKey
|
|
3038
3274
|
}));
|
|
@@ -3051,8 +3287,9 @@ async function convertSonarqubeToHdf(input) {
|
|
|
3051
3287
|
}
|
|
3052
3288
|
return buildHdfResults({
|
|
3053
3289
|
generatorName: "sonarqube-to-hdf",
|
|
3054
|
-
converterVersion
|
|
3290
|
+
converterVersion,
|
|
3055
3291
|
toolName: "SonarQube",
|
|
3292
|
+
toolVersion: sonarData.serverVersion || void 0,
|
|
3056
3293
|
baselines,
|
|
3057
3294
|
components,
|
|
3058
3295
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -3074,8 +3311,8 @@ function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, res
|
|
|
3074
3311
|
issuesByRule.get(ruleKey).push(issue);
|
|
3075
3312
|
}
|
|
3076
3313
|
const requirements = [];
|
|
3077
|
-
for (const
|
|
3078
|
-
const requirement = convertRuleToRequirement(ruleKey,
|
|
3314
|
+
for (const ruleKey of Array.from(issuesByRule.keys()).sort(byCodeUnit)) {
|
|
3315
|
+
const requirement = convertRuleToRequirement(ruleKey, issuesByRule.get(ruleKey), componentMap, ruleMap);
|
|
3079
3316
|
requirements.push(requirement);
|
|
3080
3317
|
}
|
|
3081
3318
|
return createMinimalBaseline(projectKey, requirements, {
|
|
@@ -3088,7 +3325,7 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
|
|
|
3088
3325
|
const title = rule?.name || ruleKey;
|
|
3089
3326
|
const description = extractDescription(rule);
|
|
3090
3327
|
const firstIssue = issues[0];
|
|
3091
|
-
const impact =
|
|
3328
|
+
const { severity, source: severitySource, impact } = selectSeverity(firstIssue);
|
|
3092
3329
|
const { cweIds, owaspTags, allTags } = extractTags(rule, issues);
|
|
3093
3330
|
const nistControls = mapCWEToNIST(cweIds, DEFAULT_NIST_TAGS);
|
|
3094
3331
|
const cciControls = nistToCci(nistControls);
|
|
@@ -3096,12 +3333,18 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
|
|
|
3096
3333
|
const issueWithLocation = issues.find((i) => i.line !== void 0);
|
|
3097
3334
|
const sourceLocation = issueWithLocation ? extractSourceLocation(issueWithLocation, componentMap) : void 0;
|
|
3098
3335
|
const options = { tags: {
|
|
3099
|
-
severity:
|
|
3336
|
+
severity: severity.toLowerCase(),
|
|
3337
|
+
severitySource,
|
|
3100
3338
|
type: firstIssue.type.toLowerCase(),
|
|
3101
3339
|
cwe: cweIds,
|
|
3102
3340
|
owasp: owaspTags,
|
|
3103
3341
|
nist: nistControls,
|
|
3104
3342
|
cci: cciControls,
|
|
3343
|
+
...severitySource === SEVERITY_SOURCE_MQR ? {
|
|
3344
|
+
legacySeverity: firstIssue.severity.toLowerCase(),
|
|
3345
|
+
impacts: firstIssue.impacts,
|
|
3346
|
+
...firstIssue.cleanCodeAttribute ? { cleanCodeAttribute: firstIssue.cleanCodeAttribute } : {}
|
|
3347
|
+
} : {},
|
|
3105
3348
|
...allTags
|
|
3106
3349
|
} };
|
|
3107
3350
|
if (sourceLocation) options.sourceLocation = sourceLocation;
|
|
@@ -3195,11 +3438,15 @@ function mapComplianceStatus(complianceType) {
|
|
|
3195
3438
|
default: return ResultStatus.NotReviewed;
|
|
3196
3439
|
}
|
|
3197
3440
|
}
|
|
3441
|
+
/** Mapping values pack multiple controls into one pipe-delimited string. */
|
|
3442
|
+
function splitControls(nistId) {
|
|
3443
|
+
return nistId.split("|").map((c) => c.trim()).filter(Boolean);
|
|
3444
|
+
}
|
|
3198
3445
|
function buildNistTags$3(sourceIdentifier, ruleName) {
|
|
3199
|
-
const byIdentifier = getAwsConfigNistControlByIdentifier(sourceIdentifier);
|
|
3200
|
-
if (byIdentifier) return
|
|
3446
|
+
const byIdentifier = sourceIdentifier ? getAwsConfigNistControlByIdentifier(sourceIdentifier) : void 0;
|
|
3447
|
+
if (byIdentifier) return splitControls(byIdentifier);
|
|
3201
3448
|
const byName = getAwsConfigNistControlByName(ruleName);
|
|
3202
|
-
if (byName) return
|
|
3449
|
+
if (byName) return splitControls(byName);
|
|
3203
3450
|
return [];
|
|
3204
3451
|
}
|
|
3205
3452
|
/**
|
|
@@ -3239,16 +3486,27 @@ function buildResultMessage(codeDesc, annotation, status) {
|
|
|
3239
3486
|
if (status !== ResultStatus.Failed) return void 0;
|
|
3240
3487
|
return `(${codeDesc}): ${annotation || "Rule does not pass rule compliance"}`;
|
|
3241
3488
|
}
|
|
3489
|
+
/** Elapsed seconds between rule invocation and result recording; omitted if either time is unusable. */
|
|
3490
|
+
function computeRunTime(invoked, recorded) {
|
|
3491
|
+
const start = invoked ? parseTimestamp(invoked) : null;
|
|
3492
|
+
const end = recorded ? parseTimestamp(recorded) : null;
|
|
3493
|
+
if (!start || !end) return void 0;
|
|
3494
|
+
return (end.getTime() - start.getTime()) / 1e3;
|
|
3495
|
+
}
|
|
3242
3496
|
function buildResult(r) {
|
|
3243
3497
|
const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
|
|
3244
3498
|
const status = mapComplianceStatus(r.ComplianceType);
|
|
3245
3499
|
const codeDesc = buildCodeDesc$7(q);
|
|
3246
3500
|
const message = buildResultMessage(codeDesc, r.Annotation, status);
|
|
3247
3501
|
const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
3248
|
-
|
|
3502
|
+
const runTime = computeRunTime(r.ConfigRuleInvokedTime, r.ResultRecordedTime);
|
|
3503
|
+
return {
|
|
3504
|
+
status,
|
|
3249
3505
|
codeDesc,
|
|
3250
|
-
|
|
3251
|
-
|
|
3506
|
+
startTime,
|
|
3507
|
+
...runTime !== void 0 ? { runTime } : {},
|
|
3508
|
+
...message !== void 0 ? { message } : {}
|
|
3509
|
+
};
|
|
3252
3510
|
}
|
|
3253
3511
|
/**
|
|
3254
3512
|
* Synthesizes a single HDF result for a Config rule whose live evaluation
|
|
@@ -3259,10 +3517,11 @@ function buildResult(r) {
|
|
|
3259
3517
|
*/
|
|
3260
3518
|
function buildNotApplicableResult(rule) {
|
|
3261
3519
|
const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
|
|
3262
|
-
return
|
|
3520
|
+
return {
|
|
3521
|
+
status: ResultStatus.NotApplicable,
|
|
3263
3522
|
codeDesc,
|
|
3264
3523
|
startTime: /* @__PURE__ */ new Date()
|
|
3265
|
-
}
|
|
3524
|
+
};
|
|
3266
3525
|
}
|
|
3267
3526
|
function buildRequirement$15(rule) {
|
|
3268
3527
|
const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
|
|
@@ -3295,7 +3554,7 @@ function buildRequirement$15(rule) {
|
|
|
3295
3554
|
* with get-compliance-details-by-config-rule
|
|
3296
3555
|
* @returns HDF JSON string
|
|
3297
3556
|
*/
|
|
3298
|
-
async function convertAwsConfigToHdf(input) {
|
|
3557
|
+
async function convertAwsConfigToHdf(input, converterVersion = "1.0.0") {
|
|
3299
3558
|
validateInputSize(input, "aws-config");
|
|
3300
3559
|
const resultsChecksum = await inputChecksum(input);
|
|
3301
3560
|
const data = parseJSON(input);
|
|
@@ -3316,7 +3575,7 @@ async function convertAwsConfigToHdf(input) {
|
|
|
3316
3575
|
const region = getRegion(firstArn);
|
|
3317
3576
|
return buildHdfResults({
|
|
3318
3577
|
generatorName: "aws-config-to-hdf",
|
|
3319
|
-
converterVersion
|
|
3578
|
+
converterVersion,
|
|
3320
3579
|
toolName: "AWS Config",
|
|
3321
3580
|
baselines: [baseline],
|
|
3322
3581
|
components: [{
|
|
@@ -3349,7 +3608,7 @@ function mapStatus$2(result) {
|
|
|
3349
3608
|
*/
|
|
3350
3609
|
function getImpact$8(severity) {
|
|
3351
3610
|
if (!severity) return .5;
|
|
3352
|
-
return severityToImpact(severity);
|
|
3611
|
+
return severityToImpact$1(severity);
|
|
3353
3612
|
}
|
|
3354
3613
|
/**
|
|
3355
3614
|
* Converts a single CheckovCheck to an HDF RequirementResult.
|
|
@@ -3359,10 +3618,12 @@ function checkToResult(check, scanTime) {
|
|
|
3359
3618
|
const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
|
|
3360
3619
|
let message;
|
|
3361
3620
|
if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
|
|
3362
|
-
return
|
|
3621
|
+
return {
|
|
3622
|
+
status,
|
|
3363
3623
|
codeDesc,
|
|
3364
|
-
startTime: scanTime
|
|
3365
|
-
|
|
3624
|
+
startTime: scanTime,
|
|
3625
|
+
...message !== void 0 ? { message } : {}
|
|
3626
|
+
};
|
|
3366
3627
|
}
|
|
3367
3628
|
/**
|
|
3368
3629
|
* Converts a group of checks sharing a check_id into one EvaluatedRequirement.
|
|
@@ -3404,11 +3665,11 @@ function parseInput(input) {
|
|
|
3404
3665
|
* @param input - checkov JSON or SARIF string
|
|
3405
3666
|
* @returns HDF JSON string
|
|
3406
3667
|
*/
|
|
3407
|
-
async function convertCheckovToHdf(input) {
|
|
3668
|
+
async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
|
|
3408
3669
|
validateInputSize(input, "checkov");
|
|
3409
3670
|
registerAllFingerprints();
|
|
3410
3671
|
const detected = detectConverter(input);
|
|
3411
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3672
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
3412
3673
|
const resultsChecksum = await inputChecksum(input);
|
|
3413
3674
|
const scanTime = /* @__PURE__ */ new Date();
|
|
3414
3675
|
const reports = parseInput(input);
|
|
@@ -3441,7 +3702,7 @@ async function convertCheckovToHdf(input) {
|
|
|
3441
3702
|
const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
|
|
3442
3703
|
return buildHdfResults({
|
|
3443
3704
|
generatorName: "checkov-to-hdf",
|
|
3444
|
-
converterVersion
|
|
3705
|
+
converterVersion,
|
|
3445
3706
|
toolName: "Checkov",
|
|
3446
3707
|
toolVersion: version,
|
|
3447
3708
|
toolFormat: format,
|
|
@@ -3525,11 +3786,11 @@ function buildRequirement$13(ruleId, issues, scanTime) {
|
|
|
3525
3786
|
* @param input - gosec JSON or SARIF string
|
|
3526
3787
|
* @returns HDF JSON string
|
|
3527
3788
|
*/
|
|
3528
|
-
async function convertGosecToHdf(input) {
|
|
3789
|
+
async function convertGosecToHdf(input, converterVersion = "1.0.0") {
|
|
3529
3790
|
validateInputSize(input, "gosec");
|
|
3530
3791
|
registerAllFingerprints();
|
|
3531
3792
|
const detected = detectConverter(input);
|
|
3532
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3793
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
3533
3794
|
const resultsChecksum = await inputChecksum(input);
|
|
3534
3795
|
const report = parseJSON(input);
|
|
3535
3796
|
if (!report || typeof report !== "object") throw new Error("Invalid gosec structure: not a valid JSON object");
|
|
@@ -3550,7 +3811,7 @@ async function convertGosecToHdf(input) {
|
|
|
3550
3811
|
const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
|
|
3551
3812
|
return buildHdfResults({
|
|
3552
3813
|
generatorName: "gosec-to-hdf",
|
|
3553
|
-
converterVersion
|
|
3814
|
+
converterVersion,
|
|
3554
3815
|
toolName: "gosec",
|
|
3555
3816
|
toolVersion: report.GosecVersion || void 0,
|
|
3556
3817
|
baselines: [baseline],
|
|
@@ -3597,7 +3858,7 @@ function convertVulnToRequirement(vuln) {
|
|
|
3597
3858
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3598
3859
|
return req;
|
|
3599
3860
|
}
|
|
3600
|
-
async function convertNiktoToHdf(input) {
|
|
3861
|
+
async function convertNiktoToHdf(input, converterVersion = "unknown") {
|
|
3601
3862
|
validateInputSize(input, "nikto");
|
|
3602
3863
|
const resultsChecksum = await inputChecksum(input);
|
|
3603
3864
|
const niktoData = parseJSON(input);
|
|
@@ -3630,7 +3891,7 @@ async function convertNiktoToHdf(input) {
|
|
|
3630
3891
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3631
3892
|
return buildHdfResults({
|
|
3632
3893
|
generatorName: "nikto-to-hdf",
|
|
3633
|
-
converterVersion
|
|
3894
|
+
converterVersion,
|
|
3634
3895
|
toolName: "Nikto",
|
|
3635
3896
|
toolFormat: "JSON",
|
|
3636
3897
|
baselines: [createMinimalBaseline(targetName, requirements, {
|
|
@@ -3703,11 +3964,11 @@ function parseZapTimestamp(s) {
|
|
|
3703
3964
|
}
|
|
3704
3965
|
return parseTimestamp(s) ?? void 0;
|
|
3705
3966
|
}
|
|
3706
|
-
async function convertZapToHdf(input) {
|
|
3967
|
+
async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
3707
3968
|
validateInputSize(input, "zap");
|
|
3708
3969
|
registerAllFingerprints();
|
|
3709
3970
|
const detected = detectConverter(input);
|
|
3710
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3971
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
3711
3972
|
const resultsChecksum = await inputChecksum(input);
|
|
3712
3973
|
const zapData = parseJSON(input);
|
|
3713
3974
|
const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
|
|
@@ -3800,7 +4061,7 @@ async function convertZapToHdf(input) {
|
|
|
3800
4061
|
components,
|
|
3801
4062
|
generator: {
|
|
3802
4063
|
name: "zap-to-hdf",
|
|
3803
|
-
version:
|
|
4064
|
+
version: converterVersion
|
|
3804
4065
|
},
|
|
3805
4066
|
tool
|
|
3806
4067
|
};
|
|
@@ -3811,6 +4072,579 @@ async function convertZapToHdf(input) {
|
|
|
3811
4072
|
return serializeHdf(hdf);
|
|
3812
4073
|
}
|
|
3813
4074
|
//#endregion
|
|
4075
|
+
//#region shared/typescript/bom/model.ts
|
|
4076
|
+
/**
|
|
4077
|
+
* BOM manifest-kind discriminator. The schema allows custom 'x-'-prefixed kinds
|
|
4078
|
+
* beyond the reserved set, so the generated `bomType` field is a plain string;
|
|
4079
|
+
* the reserved values below are the parser's normalized vocabulary. Mirrors the
|
|
4080
|
+
* Go bom package.
|
|
4081
|
+
*/
|
|
4082
|
+
const BOMType = {
|
|
4083
|
+
Sbom: "sbom",
|
|
4084
|
+
AIModel: "ai-model",
|
|
4085
|
+
Dataset: "dataset"
|
|
4086
|
+
};
|
|
4087
|
+
//#endregion
|
|
4088
|
+
//#region shared/typescript/bom/fingerprints.ts
|
|
4089
|
+
function record(input) {
|
|
4090
|
+
return typeof input === "object" && input !== null && !Array.isArray(input) ? input : void 0;
|
|
4091
|
+
}
|
|
4092
|
+
/** CycloneDX: bomFormat === 'CycloneDX'. Returns 0..1 confidence. */
|
|
4093
|
+
function detectCycloneDX(input) {
|
|
4094
|
+
return record(input)?.bomFormat === "CycloneDX" ? 1 : 0;
|
|
4095
|
+
}
|
|
4096
|
+
/**
|
|
4097
|
+
* CycloneDX ML-BOM: a CycloneDX document with at least one
|
|
4098
|
+
* machine-learning-model component. Strictly more specific than plain
|
|
4099
|
+
* CycloneDX.
|
|
4100
|
+
*/
|
|
4101
|
+
function detectCycloneDXML(input) {
|
|
4102
|
+
const obj = record(input);
|
|
4103
|
+
if (obj?.bomFormat !== "CycloneDX") return 0;
|
|
4104
|
+
const components = obj.components;
|
|
4105
|
+
if (!Array.isArray(components)) return 0;
|
|
4106
|
+
return components.some((c) => record(c)?.type === "machine-learning-model") ? 1 : 0;
|
|
4107
|
+
}
|
|
4108
|
+
/** SPDX: a non-empty spdxVersion string is present. Returns 0..1 confidence. */
|
|
4109
|
+
function detectSPDX(input) {
|
|
4110
|
+
const obj = record(input);
|
|
4111
|
+
return typeof obj?.spdxVersion === "string" && obj.spdxVersion.length > 0 ? 1 : 0;
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* SPDX 3.0 AI/Dataset (JSON-LD): a document with an `@context` and an `@graph`
|
|
4115
|
+
* array carrying at least one ai_AIPackage or dataset_DatasetPackage element.
|
|
4116
|
+
* Structurally disjoint from the SPDX 2.3 detector (which keys on spdxVersion),
|
|
4117
|
+
* so the two never conflict.
|
|
4118
|
+
*/
|
|
4119
|
+
function detectSPDX3(input) {
|
|
4120
|
+
const obj = record(input);
|
|
4121
|
+
if (!obj || obj["@context"] === void 0) return 0;
|
|
4122
|
+
const graph = obj["@graph"];
|
|
4123
|
+
if (!Array.isArray(graph)) return 0;
|
|
4124
|
+
return graph.some((el) => {
|
|
4125
|
+
const type = record(el)?.type;
|
|
4126
|
+
return type === "ai_AIPackage" || type === "dataset_DatasetPackage";
|
|
4127
|
+
}) ? 1 : 0;
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Detect the BOM format of a parsed JSON object. ML wins over plain CycloneDX
|
|
4131
|
+
* by precedence; returns undefined when no supported format matches.
|
|
4132
|
+
*/
|
|
4133
|
+
function detectFormat(input) {
|
|
4134
|
+
const ml = detectCycloneDXML(input);
|
|
4135
|
+
if (ml > 0) return {
|
|
4136
|
+
format: "cyclonedx-ml",
|
|
4137
|
+
confidence: ml
|
|
4138
|
+
};
|
|
4139
|
+
const cdx = detectCycloneDX(input);
|
|
4140
|
+
if (cdx > 0) return {
|
|
4141
|
+
format: "cyclonedx",
|
|
4142
|
+
confidence: cdx
|
|
4143
|
+
};
|
|
4144
|
+
const spdx3 = detectSPDX3(input);
|
|
4145
|
+
if (spdx3 > 0) return {
|
|
4146
|
+
format: "spdx-3-ai",
|
|
4147
|
+
confidence: spdx3
|
|
4148
|
+
};
|
|
4149
|
+
const spdx = detectSPDX(input);
|
|
4150
|
+
if (spdx > 0) return {
|
|
4151
|
+
format: "spdx",
|
|
4152
|
+
confidence: spdx
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
//#endregion
|
|
4156
|
+
//#region shared/typescript/bom/cyclonedx.ts
|
|
4157
|
+
/**
|
|
4158
|
+
* CycloneDX SBOM -> normalized HDF BillOfMaterials.
|
|
4159
|
+
*
|
|
4160
|
+
* Flattens the top-level components[] into SBOM packages. Nested subcomponents
|
|
4161
|
+
* (metadata.component.components[]) are the tool's own assembly tree and are
|
|
4162
|
+
* intentionally not treated as inventory packages here.
|
|
4163
|
+
*/
|
|
4164
|
+
/** Extract license identifiers/expressions from a CycloneDX licenses[] array. */
|
|
4165
|
+
function extractLicenses$1(raw) {
|
|
4166
|
+
if (!Array.isArray(raw)) return [];
|
|
4167
|
+
const out = [];
|
|
4168
|
+
for (const entry of raw) {
|
|
4169
|
+
const e = asRecord(entry);
|
|
4170
|
+
if (!e) continue;
|
|
4171
|
+
const license = asRecord(e.license);
|
|
4172
|
+
const value = (license ? asString(license.id) ?? asString(license.name) : void 0) ?? asString(e.expression);
|
|
4173
|
+
if (value) out.push(value);
|
|
4174
|
+
}
|
|
4175
|
+
return out;
|
|
4176
|
+
}
|
|
4177
|
+
function componentToPackage(component) {
|
|
4178
|
+
const c = asRecord(component);
|
|
4179
|
+
const name = c ? asString(c.name) : void 0;
|
|
4180
|
+
if (!c || !name) return void 0;
|
|
4181
|
+
const pkg = { name };
|
|
4182
|
+
const version = asString(c.version);
|
|
4183
|
+
if (version) pkg.version = version;
|
|
4184
|
+
const purl = asString(c.purl);
|
|
4185
|
+
if (purl) pkg.purl = purl;
|
|
4186
|
+
const licenses = extractLicenses$1(c.licenses);
|
|
4187
|
+
if (licenses.length > 0) pkg.licenses = licenses;
|
|
4188
|
+
enrichFromPurl(pkg);
|
|
4189
|
+
return pkg;
|
|
4190
|
+
}
|
|
4191
|
+
function parseCycloneDX(obj) {
|
|
4192
|
+
const components = Array.isArray(obj.components) ? obj.components : [];
|
|
4193
|
+
const packages = [];
|
|
4194
|
+
for (const component of components) {
|
|
4195
|
+
const pkg = componentToPackage(component);
|
|
4196
|
+
if (pkg) packages.push(pkg);
|
|
4197
|
+
}
|
|
4198
|
+
const uniqueId = asString(obj.serialNumber);
|
|
4199
|
+
return buildBom({
|
|
4200
|
+
bomType: BOMType.Sbom,
|
|
4201
|
+
format: "cyclonedx",
|
|
4202
|
+
packages: limitArrayWithWarning(packages, "package"),
|
|
4203
|
+
uniqueId
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
//#endregion
|
|
4207
|
+
//#region shared/typescript/bom/spdx.ts
|
|
4208
|
+
/**
|
|
4209
|
+
* SPDX SBOM -> normalized HDF BillOfMaterials.
|
|
4210
|
+
*
|
|
4211
|
+
* Maps packages[] to normalized SBOM packages. purl comes from an externalRefs
|
|
4212
|
+
* entry with referenceType 'purl'; licenses come from licenseConcluded /
|
|
4213
|
+
* licenseDeclared with NOASSERTION/NONE sentinels filtered out.
|
|
4214
|
+
*/
|
|
4215
|
+
const SPDX_LICENSE_FIELDS = ["licenseConcluded", "licenseDeclared"];
|
|
4216
|
+
/** First externalRefs[].referenceLocator whose referenceType is 'purl'. */
|
|
4217
|
+
function purlFromExternalRefs(refs) {
|
|
4218
|
+
if (!Array.isArray(refs)) return void 0;
|
|
4219
|
+
for (const ref of refs) {
|
|
4220
|
+
const r = asRecord(ref);
|
|
4221
|
+
if (r?.referenceType === "purl") {
|
|
4222
|
+
const locator = asString(r.referenceLocator);
|
|
4223
|
+
if (locator) return locator;
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
function extractLicenses(pkg) {
|
|
4228
|
+
const out = [];
|
|
4229
|
+
for (const field of SPDX_LICENSE_FIELDS) {
|
|
4230
|
+
const license = cleanLicense(pkg[field]);
|
|
4231
|
+
if (license && !out.includes(license)) out.push(license);
|
|
4232
|
+
}
|
|
4233
|
+
return out;
|
|
4234
|
+
}
|
|
4235
|
+
function packageToPackage(source) {
|
|
4236
|
+
const p = asRecord(source);
|
|
4237
|
+
const name = p ? asString(p.name) : void 0;
|
|
4238
|
+
if (!p || !name) return void 0;
|
|
4239
|
+
const pkg = { name };
|
|
4240
|
+
const version = asString(p.versionInfo);
|
|
4241
|
+
if (version) pkg.version = version;
|
|
4242
|
+
const purl = purlFromExternalRefs(p.externalRefs);
|
|
4243
|
+
if (purl) pkg.purl = purl;
|
|
4244
|
+
const licenses = extractLicenses(p);
|
|
4245
|
+
if (licenses.length > 0) pkg.licenses = licenses;
|
|
4246
|
+
enrichFromPurl(pkg);
|
|
4247
|
+
return pkg;
|
|
4248
|
+
}
|
|
4249
|
+
function parseSPDX(obj) {
|
|
4250
|
+
const sourcePackages = Array.isArray(obj.packages) ? obj.packages : [];
|
|
4251
|
+
const packages = [];
|
|
4252
|
+
for (const source of sourcePackages) {
|
|
4253
|
+
const pkg = packageToPackage(source);
|
|
4254
|
+
if (pkg) packages.push(pkg);
|
|
4255
|
+
}
|
|
4256
|
+
const uniqueId = asString(obj.documentNamespace);
|
|
4257
|
+
return buildBom({
|
|
4258
|
+
bomType: BOMType.Sbom,
|
|
4259
|
+
format: "spdx",
|
|
4260
|
+
packages: limitArrayWithWarning(packages, "package"),
|
|
4261
|
+
uniqueId
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
//#endregion
|
|
4265
|
+
//#region shared/typescript/bom/spdx3.ts
|
|
4266
|
+
/**
|
|
4267
|
+
* SPDX 3.0 AI/Dataset (JSON-LD) -> normalized HDF BillOfMaterials subjects.
|
|
4268
|
+
*
|
|
4269
|
+
* SPDX 3.0 is JSON-LD: a top-level { "@context", "@graph" } where "@graph" is an
|
|
4270
|
+
* array of typed elements. A single document is inherently MULTI-SUBJECT — it
|
|
4271
|
+
* can carry several ai_AIPackage and dataset_DatasetPackage elements — so this
|
|
4272
|
+
* parser returns one subject per AI/dataset element (unlike the single-BOM
|
|
4273
|
+
* parseSPDX / parseMLBOM paths). This is completely distinct from SPDX 2.3
|
|
4274
|
+
* (spdxVersion + packages[]), handled by parseSPDX.
|
|
4275
|
+
*
|
|
4276
|
+
* PARTIAL-FIDELITY: only fields that map cleanly onto the normalized extensions
|
|
4277
|
+
* are lifted; everything else (energy, autonomy, safety, bias, sensor, size, …)
|
|
4278
|
+
* is carried opaquely via the BOM `document` passthrough of the raw element.
|
|
4279
|
+
* Two conflation traps are deliberately avoided: ai_hyperparameter is training
|
|
4280
|
+
* knobs (-> hyperparameters, NEVER parameterCount) and dataset_datasetSize is
|
|
4281
|
+
* ambiguous/unlabeled (never -> recordCount).
|
|
4282
|
+
*/
|
|
4283
|
+
/** SPDX-3 relationship types that link a model to a training/eval dataset. */
|
|
4284
|
+
const DATASET_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["trainedOn", "testedOn"]);
|
|
4285
|
+
function graphElements(obj) {
|
|
4286
|
+
const graph = obj["@graph"];
|
|
4287
|
+
if (!Array.isArray(graph)) return [];
|
|
4288
|
+
const out = [];
|
|
4289
|
+
for (const el of graph) {
|
|
4290
|
+
const r = asRecord(el);
|
|
4291
|
+
if (r) out.push(r);
|
|
4292
|
+
}
|
|
4293
|
+
return out;
|
|
4294
|
+
}
|
|
4295
|
+
/** Map SPDX DictionaryEntry[] ({key,value}) to normalized {name,value} pairs. */
|
|
4296
|
+
function dictionaryEntries(value) {
|
|
4297
|
+
if (!Array.isArray(value)) return [];
|
|
4298
|
+
const out = [];
|
|
4299
|
+
for (const entry of value) {
|
|
4300
|
+
const e = asRecord(entry);
|
|
4301
|
+
if (!e) continue;
|
|
4302
|
+
const name = asString(e.key);
|
|
4303
|
+
if (name === void 0) continue;
|
|
4304
|
+
const value = e.value !== void 0 && e.value !== null ? stringifyScalar(e.value) : "";
|
|
4305
|
+
out.push({
|
|
4306
|
+
name,
|
|
4307
|
+
value
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
return out;
|
|
4311
|
+
}
|
|
4312
|
+
/** First element of a non-empty string array, else undefined. */
|
|
4313
|
+
function firstString(value) {
|
|
4314
|
+
if (!Array.isArray(value)) return void 0;
|
|
4315
|
+
for (const item of value) {
|
|
4316
|
+
const s = asString(item);
|
|
4317
|
+
if (s !== void 0) return s;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
/** Distinct non-empty strings of an array, joined with "; ". */
|
|
4321
|
+
function joinDistinct(value) {
|
|
4322
|
+
if (!Array.isArray(value)) return void 0;
|
|
4323
|
+
const seen = [];
|
|
4324
|
+
for (const item of value) {
|
|
4325
|
+
const s = asString(item);
|
|
4326
|
+
if (s !== void 0 && !seen.includes(s)) seen.push(s);
|
|
4327
|
+
}
|
|
4328
|
+
return seen.length > 0 ? seen.join("; ") : void 0;
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* Dataset references for a model: targets of trainedOn/testedOn relationships
|
|
4332
|
+
* whose `from` is this model. Each target is resolved to the referenced
|
|
4333
|
+
* dataset element's name when present in the graph, else the raw id. Distinct,
|
|
4334
|
+
* first-seen order.
|
|
4335
|
+
*/
|
|
4336
|
+
function datasetRefsFor(modelId, relationships, datasetNameById) {
|
|
4337
|
+
const out = [];
|
|
4338
|
+
for (const rel of relationships) {
|
|
4339
|
+
if (asString(rel.from) !== modelId) continue;
|
|
4340
|
+
if (!DATASET_RELATIONSHIP_TYPES.has(asString(rel.relationshipType) ?? "")) continue;
|
|
4341
|
+
const targets = Array.isArray(rel.to) ? rel.to : [rel.to];
|
|
4342
|
+
for (const target of targets) {
|
|
4343
|
+
const targetId = asString(target);
|
|
4344
|
+
if (targetId === void 0) continue;
|
|
4345
|
+
const ref = datasetNameById.get(targetId) ?? targetId;
|
|
4346
|
+
if (!out.includes(ref)) out.push(ref);
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
return out;
|
|
4350
|
+
}
|
|
4351
|
+
function buildModelExtension$1(element, relationships, datasetNameById) {
|
|
4352
|
+
const model = {};
|
|
4353
|
+
const hyperparameters = dictionaryEntries(element.ai_hyperparameter);
|
|
4354
|
+
if (hyperparameters.length > 0) model.hyperparameters = hyperparameters;
|
|
4355
|
+
const performanceMetrics = dictionaryEntries(element.ai_metric);
|
|
4356
|
+
if (performanceMetrics.length > 0) model.performanceMetrics = performanceMetrics;
|
|
4357
|
+
const task = firstString(element.ai_domain);
|
|
4358
|
+
if (task !== void 0) model.task = task;
|
|
4359
|
+
const architecture = joinDistinct(element.ai_typeOfModel);
|
|
4360
|
+
if (architecture !== void 0) model.modelArchitecture = architecture;
|
|
4361
|
+
const intendedUse = asString(element.ai_informationAboutApplication);
|
|
4362
|
+
if (intendedUse !== void 0) model.intendedUse = intendedUse;
|
|
4363
|
+
const datasetRefs = datasetRefsFor(asString(element.spdxId) ?? "", relationships, datasetNameById);
|
|
4364
|
+
if (datasetRefs.length > 0) model.datasetRefs = datasetRefs;
|
|
4365
|
+
return model;
|
|
4366
|
+
}
|
|
4367
|
+
function buildDatasetExtension(element) {
|
|
4368
|
+
const dataset = {};
|
|
4369
|
+
const modality = element.dataset_datasetType;
|
|
4370
|
+
if (Array.isArray(modality)) {
|
|
4371
|
+
const values = modality.map(asString).filter((s) => s !== void 0);
|
|
4372
|
+
if (values.length > 0) dataset.modality = values;
|
|
4373
|
+
}
|
|
4374
|
+
const dataClassification = asString(element.dataset_confidentialityLevel);
|
|
4375
|
+
if (dataClassification !== void 0) dataset.dataClassification = dataClassification;
|
|
4376
|
+
const intendedUse = asString(element.dataset_intendedUse);
|
|
4377
|
+
if (intendedUse !== void 0) dataset.intendedUse = intendedUse;
|
|
4378
|
+
const provenance = asString(element.dataset_dataCollectionProcess);
|
|
4379
|
+
if (provenance !== void 0) dataset.provenance = provenance;
|
|
4380
|
+
return dataset;
|
|
4381
|
+
}
|
|
4382
|
+
/**
|
|
4383
|
+
* Parse an SPDX-3 JSON-LD document into its AI/dataset subjects. Emits one
|
|
4384
|
+
* aiModel subject per ai_AIPackage and one dataset subject per
|
|
4385
|
+
* dataset_DatasetPackage, in graph order.
|
|
4386
|
+
*/
|
|
4387
|
+
function parseSPDX3(obj) {
|
|
4388
|
+
const elements = graphElements(obj);
|
|
4389
|
+
const relationships = elements.filter((el) => el.type === "Relationship");
|
|
4390
|
+
const datasetNameById = /* @__PURE__ */ new Map();
|
|
4391
|
+
for (const el of elements) {
|
|
4392
|
+
if (el.type !== "dataset_DatasetPackage") continue;
|
|
4393
|
+
const id = asString(el.spdxId);
|
|
4394
|
+
const name = asString(el.name);
|
|
4395
|
+
if (id !== void 0 && name !== void 0) datasetNameById.set(id, name);
|
|
4396
|
+
}
|
|
4397
|
+
const subjects = [];
|
|
4398
|
+
for (const element of elements) if (element.type === "ai_AIPackage") {
|
|
4399
|
+
const model = buildModelExtension$1(element, relationships, datasetNameById);
|
|
4400
|
+
subjects.push({
|
|
4401
|
+
kind: "aiModel",
|
|
4402
|
+
name: asString(element.name) ?? "",
|
|
4403
|
+
id: asString(element.spdxId) ?? "",
|
|
4404
|
+
bom: buildBom({
|
|
4405
|
+
bomType: BOMType.AIModel,
|
|
4406
|
+
format: "spdx-3-ai",
|
|
4407
|
+
model,
|
|
4408
|
+
document: element,
|
|
4409
|
+
uniqueId: asString(element.spdxId)
|
|
4410
|
+
})
|
|
4411
|
+
});
|
|
4412
|
+
} else if (element.type === "dataset_DatasetPackage") {
|
|
4413
|
+
const dataset = buildDatasetExtension(element);
|
|
4414
|
+
subjects.push({
|
|
4415
|
+
kind: "dataset",
|
|
4416
|
+
name: asString(element.name) ?? "",
|
|
4417
|
+
id: asString(element.spdxId) ?? "",
|
|
4418
|
+
bom: buildBom({
|
|
4419
|
+
bomType: BOMType.Dataset,
|
|
4420
|
+
format: "spdx-3-ai",
|
|
4421
|
+
dataset,
|
|
4422
|
+
document: element,
|
|
4423
|
+
uniqueId: asString(element.spdxId)
|
|
4424
|
+
})
|
|
4425
|
+
});
|
|
4426
|
+
}
|
|
4427
|
+
return { subjects: limitArrayWithWarning(subjects, "subject") };
|
|
4428
|
+
}
|
|
4429
|
+
//#endregion
|
|
4430
|
+
//#region shared/typescript/bom/ml-bom.ts
|
|
4431
|
+
/**
|
|
4432
|
+
* CycloneDX ML-BOM -> normalized HDF ai-model BillOfMaterials.
|
|
4433
|
+
*
|
|
4434
|
+
* PARTIAL-FIDELITY: only modelCard fields that map cleanly onto the normalized
|
|
4435
|
+
* AI_Model_Extension are lifted (modelArchitecture, datasetRefs, intendedUse,
|
|
4436
|
+
* learningApproach, task, performanceMetrics, inputOutput.dataTypes).
|
|
4437
|
+
* parameterCount, serializationFormat, hyperparameters, and the rest of
|
|
4438
|
+
* inputOutput have NO native CycloneDX ML source, so they are left undefined —
|
|
4439
|
+
* never fabricated. The raw machine-learning-model component is carried verbatim
|
|
4440
|
+
* in the BOM `document` passthrough so nothing is lost, satisfying the
|
|
4441
|
+
* "drop-or-passthrough, never invent" rule.
|
|
4442
|
+
*/
|
|
4443
|
+
function findModelComponent(obj) {
|
|
4444
|
+
const components = Array.isArray(obj.components) ? obj.components : [];
|
|
4445
|
+
for (const component of components) {
|
|
4446
|
+
const c = asRecord(component);
|
|
4447
|
+
if (c?.type === "machine-learning-model") return c;
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
/**
|
|
4451
|
+
* References to training/evaluation datasets. Only ref-shaped entries (a bare
|
|
4452
|
+
* ref string, or an object with a `ref`) are lifted; inline dataset descriptors
|
|
4453
|
+
* without a reference are left for a future dataset-normalization pass rather
|
|
4454
|
+
* than being synthesized into a ref.
|
|
4455
|
+
*/
|
|
4456
|
+
function extractDatasetRefs(datasets) {
|
|
4457
|
+
if (!Array.isArray(datasets)) return [];
|
|
4458
|
+
const out = [];
|
|
4459
|
+
for (const dataset of datasets) {
|
|
4460
|
+
if (typeof dataset === "string") {
|
|
4461
|
+
if (dataset.length > 0) out.push(dataset);
|
|
4462
|
+
continue;
|
|
4463
|
+
}
|
|
4464
|
+
const ref = asString(asRecord(dataset)?.ref);
|
|
4465
|
+
if (ref) out.push(ref);
|
|
4466
|
+
}
|
|
4467
|
+
return out;
|
|
4468
|
+
}
|
|
4469
|
+
/** Intended-use statement from modelCard.considerations.useCases, if present. */
|
|
4470
|
+
function extractIntendedUse(considerations) {
|
|
4471
|
+
const c = asRecord(considerations);
|
|
4472
|
+
if (!c || !Array.isArray(c.useCases)) return void 0;
|
|
4473
|
+
const parts = c.useCases.map(asString).filter((s) => s !== void 0);
|
|
4474
|
+
return parts.length > 0 ? parts.join("; ") : void 0;
|
|
4475
|
+
}
|
|
4476
|
+
/**
|
|
4477
|
+
* Reported evaluation metrics from modelCard.quantitativeAnalysis. Each native
|
|
4478
|
+
* metric's `type` becomes the normalized `name` and its `value` is carried as a
|
|
4479
|
+
* string (metrics are heterogeneous). An entry contributes only when it has a
|
|
4480
|
+
* name or value; native slice/confidenceInterval are left in the `document`.
|
|
4481
|
+
*/
|
|
4482
|
+
function extractPerformanceMetrics(quantitativeAnalysis) {
|
|
4483
|
+
const qa = asRecord(quantitativeAnalysis);
|
|
4484
|
+
if (!qa || !Array.isArray(qa.performanceMetrics)) return [];
|
|
4485
|
+
const out = [];
|
|
4486
|
+
for (const entry of qa.performanceMetrics) {
|
|
4487
|
+
const e = asRecord(entry);
|
|
4488
|
+
if (!e) continue;
|
|
4489
|
+
const name = asString(e.type);
|
|
4490
|
+
const value = e.value !== void 0 && e.value !== null ? stringifyScalar(e.value) : void 0;
|
|
4491
|
+
if (name === void 0 && value === void 0) continue;
|
|
4492
|
+
const metric = {};
|
|
4493
|
+
if (name !== void 0) metric.name = name;
|
|
4494
|
+
if (value !== void 0) metric.value = value;
|
|
4495
|
+
out.push(metric);
|
|
4496
|
+
}
|
|
4497
|
+
return out;
|
|
4498
|
+
}
|
|
4499
|
+
/** Distinct `format` strings across modelParameters.inputs[] and outputs[]. */
|
|
4500
|
+
function extractIODataTypes(parameters) {
|
|
4501
|
+
const out = [];
|
|
4502
|
+
for (const key of ["inputs", "outputs"]) {
|
|
4503
|
+
const entries = parameters[key];
|
|
4504
|
+
if (!Array.isArray(entries)) continue;
|
|
4505
|
+
for (const entry of entries) {
|
|
4506
|
+
const format = asString(asRecord(entry)?.format);
|
|
4507
|
+
if (format && !out.includes(format)) out.push(format);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
return out;
|
|
4511
|
+
}
|
|
4512
|
+
function buildModelExtension(modelComponent) {
|
|
4513
|
+
const model = {};
|
|
4514
|
+
const modelCard = asRecord(modelComponent.modelCard);
|
|
4515
|
+
if (!modelCard) return model;
|
|
4516
|
+
const parameters = asRecord(modelCard.modelParameters);
|
|
4517
|
+
if (parameters) {
|
|
4518
|
+
const architecture = asString(parameters.modelArchitecture) ?? asString(parameters.architectureFamily);
|
|
4519
|
+
if (architecture) model.modelArchitecture = architecture;
|
|
4520
|
+
const datasetRefs = extractDatasetRefs(parameters.datasets);
|
|
4521
|
+
if (datasetRefs.length > 0) model.datasetRefs = datasetRefs;
|
|
4522
|
+
const learningApproach = asString(asRecord(parameters.approach)?.type);
|
|
4523
|
+
if (learningApproach) model.learningApproach = learningApproach;
|
|
4524
|
+
const task = asString(parameters.task);
|
|
4525
|
+
if (task) model.task = task;
|
|
4526
|
+
const dataTypes = extractIODataTypes(parameters);
|
|
4527
|
+
if (dataTypes.length > 0) model.inputOutput = { dataTypes };
|
|
4528
|
+
}
|
|
4529
|
+
const performanceMetrics = extractPerformanceMetrics(modelCard.quantitativeAnalysis);
|
|
4530
|
+
if (performanceMetrics.length > 0) model.performanceMetrics = performanceMetrics;
|
|
4531
|
+
const intendedUse = extractIntendedUse(modelCard.considerations);
|
|
4532
|
+
if (intendedUse) model.intendedUse = intendedUse;
|
|
4533
|
+
return model;
|
|
4534
|
+
}
|
|
4535
|
+
function parseMLBOM(obj) {
|
|
4536
|
+
const modelComponent = findModelComponent(obj);
|
|
4537
|
+
const model = modelComponent ? buildModelExtension(modelComponent) : {};
|
|
4538
|
+
return buildBom({
|
|
4539
|
+
bomType: BOMType.AIModel,
|
|
4540
|
+
format: "cyclonedx-ml",
|
|
4541
|
+
model,
|
|
4542
|
+
document: modelComponent,
|
|
4543
|
+
uniqueId: asString(obj.serialNumber)
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
//#endregion
|
|
4547
|
+
//#region shared/typescript/bom/normalize.ts
|
|
4548
|
+
/**
|
|
4549
|
+
* Shared BOM normalization: parseBom / buildBom / detectFormat re-dispatch and
|
|
4550
|
+
* the cross-format helpers (object guards, license cleaning, purl handling).
|
|
4551
|
+
* Format-specific extraction lives in cyclonedx.ts / spdx.ts / ml-bom.ts.
|
|
4552
|
+
*/
|
|
4553
|
+
const CONVERTER = "bom-parser";
|
|
4554
|
+
/** SPDX sentinels that mean "no license", filtered out of normalized output. */
|
|
4555
|
+
const SPDX_NULL_LICENSES = /* @__PURE__ */ new Set(["noassertion", "none"]);
|
|
4556
|
+
/** Narrow an unknown value to a plain object (not array, not null). */
|
|
4557
|
+
function asRecord(value) {
|
|
4558
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
4559
|
+
}
|
|
4560
|
+
/** Narrow an unknown value to a non-empty string. */
|
|
4561
|
+
function asString(value) {
|
|
4562
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4563
|
+
}
|
|
4564
|
+
/**
|
|
4565
|
+
* Render a heterogeneous scalar BOM value (metric value, SPDX DictionaryEntry
|
|
4566
|
+
* value) to a string. This is the REFERENCE the Go side matches byte-for-byte
|
|
4567
|
+
* (see stringifyScalar / jsNumberToString in normalize.go): JS String() gives
|
|
4568
|
+
* numbers their shortest round-tripping form, and Go reimplements that exact
|
|
4569
|
+
* formatting. Callers must guard null/undefined before calling.
|
|
4570
|
+
*/
|
|
4571
|
+
function stringifyScalar(value) {
|
|
4572
|
+
return String(value);
|
|
4573
|
+
}
|
|
4574
|
+
/** Return an SPDX license string unless it is a NOASSERTION/NONE sentinel. */
|
|
4575
|
+
function cleanLicense(value) {
|
|
4576
|
+
const s = asString(value);
|
|
4577
|
+
if (!s) return void 0;
|
|
4578
|
+
return SPDX_NULL_LICENSES.has(s.trim().toLowerCase()) ? void 0 : s;
|
|
4579
|
+
}
|
|
4580
|
+
/**
|
|
4581
|
+
* Fill a package's missing name/version from its purl when parseable. Never
|
|
4582
|
+
* overwrites values the source BOM already provided (e.g. an SPDX versionInfo
|
|
4583
|
+
* is authoritative over a purl whose @segment is a git commit).
|
|
4584
|
+
*/
|
|
4585
|
+
function enrichFromPurl(pkg) {
|
|
4586
|
+
if (!pkg.purl) return;
|
|
4587
|
+
const parsed = parsePurl(pkg.purl);
|
|
4588
|
+
if (!parsed) return;
|
|
4589
|
+
if (!pkg.version && parsed.version) pkg.version = parsed.version;
|
|
4590
|
+
if (!pkg.name && parsed.name) pkg.name = parsed.name;
|
|
4591
|
+
}
|
|
4592
|
+
/**
|
|
4593
|
+
* Assemble a schema-valid BillOfMaterials from normalized parts, enforcing the
|
|
4594
|
+
* three-tier discipline: the packages/model/dataset extension is kept only when
|
|
4595
|
+
* it matches bomType; a mismatched extension is silently dropped (the schema
|
|
4596
|
+
* forbids it, so carrying it would produce invalid output).
|
|
4597
|
+
*/
|
|
4598
|
+
function buildBom(parts) {
|
|
4599
|
+
const bom = {
|
|
4600
|
+
bomType: parts.bomType,
|
|
4601
|
+
format: parts.format
|
|
4602
|
+
};
|
|
4603
|
+
if (parts.ref !== void 0) bom.ref = parts.ref;
|
|
4604
|
+
if (parts.document !== void 0) bom.document = parts.document;
|
|
4605
|
+
if (parts.uniqueId !== void 0) bom.uniqueId = parts.uniqueId;
|
|
4606
|
+
if (parts.hashes !== void 0 && parts.hashes.length > 0) bom.hashes = parts.hashes;
|
|
4607
|
+
if (parts.license !== void 0) bom.license = parts.license;
|
|
4608
|
+
if (parts.bomType === BOMType.Sbom && parts.packages !== void 0) bom.packages = parts.packages;
|
|
4609
|
+
if (parts.bomType === BOMType.AIModel && parts.model !== void 0) bom.model = parts.model;
|
|
4610
|
+
if (parts.bomType === BOMType.Dataset && parts.dataset !== void 0) bom.dataset = parts.dataset;
|
|
4611
|
+
return bom;
|
|
4612
|
+
}
|
|
4613
|
+
/**
|
|
4614
|
+
* Detect and parse a BOM document into normalized form. Validates input size
|
|
4615
|
+
* FIRST (security boundary), then dispatches on the detected format. Throws on
|
|
4616
|
+
* oversized or undetectable input.
|
|
4617
|
+
*/
|
|
4618
|
+
function parseBom(input) {
|
|
4619
|
+
validateInputSize(input, CONVERTER);
|
|
4620
|
+
const obj = parseJSON(input);
|
|
4621
|
+
const detected = detectFormat(obj);
|
|
4622
|
+
if (!detected) throw new Error("bom-parser: could not detect a supported BOM format (expected CycloneDX or SPDX JSON)");
|
|
4623
|
+
const record = asRecord(obj) ?? {};
|
|
4624
|
+
switch (detected.format) {
|
|
4625
|
+
case "cyclonedx-ml": return {
|
|
4626
|
+
format: detected.format,
|
|
4627
|
+
normalized: parseMLBOM(record)
|
|
4628
|
+
};
|
|
4629
|
+
case "cyclonedx": return {
|
|
4630
|
+
format: detected.format,
|
|
4631
|
+
normalized: parseCycloneDX(record)
|
|
4632
|
+
};
|
|
4633
|
+
case "spdx": return {
|
|
4634
|
+
format: detected.format,
|
|
4635
|
+
normalized: parseSPDX(record)
|
|
4636
|
+
};
|
|
4637
|
+
case "spdx-3-ai": {
|
|
4638
|
+
const [first] = parseSPDX3(record).subjects;
|
|
4639
|
+
if (!first) throw new Error("bom-parser: SPDX-3 document carries no AI/dataset subjects");
|
|
4640
|
+
return {
|
|
4641
|
+
format: detected.format,
|
|
4642
|
+
normalized: first.bom
|
|
4643
|
+
};
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
//#endregion
|
|
3814
4648
|
//#region converters/cyclonedx-to-hdf/typescript/converter.ts
|
|
3815
4649
|
const CVSS_METHODS = /* @__PURE__ */ new Set([
|
|
3816
4650
|
"CVSSv2",
|
|
@@ -3828,7 +4662,7 @@ function maxImpact(ratings) {
|
|
|
3828
4662
|
for (const rating of ratings) {
|
|
3829
4663
|
let impact;
|
|
3830
4664
|
if (rating.method && CVSS_METHODS.has(rating.method) && rating.score !== void 0 && rating.score !== null) impact = rating.score / 10;
|
|
3831
|
-
else impact = severityToImpact(rating.severity ?? "medium");
|
|
4665
|
+
else impact = severityToImpact$1(rating.severity ?? "medium");
|
|
3832
4666
|
if (impact > max) max = impact;
|
|
3833
4667
|
}
|
|
3834
4668
|
return max;
|
|
@@ -3863,19 +4697,29 @@ function flattenComponents(components) {
|
|
|
3863
4697
|
return result;
|
|
3864
4698
|
}
|
|
3865
4699
|
/**
|
|
4700
|
+
* Reports whether any (possibly nested) component is a machine-learning-model,
|
|
4701
|
+
* i.e. the CycloneDX document is an AI-BOM.
|
|
4702
|
+
*/
|
|
4703
|
+
function hasMLModelComponent(components) {
|
|
4704
|
+
return flattenComponents(components).some((comp) => comp.type === "machine-learning-model");
|
|
4705
|
+
}
|
|
4706
|
+
/**
|
|
3866
4707
|
* Converts CycloneDX SBOM/VEX JSON to HDF format.
|
|
3867
4708
|
*
|
|
3868
4709
|
* @param input - CycloneDX JSON string
|
|
3869
4710
|
* @returns HDF JSON string
|
|
3870
4711
|
*/
|
|
3871
|
-
async function convertCyclonedxToHdf(input) {
|
|
4712
|
+
async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
3872
4713
|
if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
|
|
3873
4714
|
validateInputSize(input, "cyclonedx");
|
|
3874
4715
|
const bom = parseJSON(input);
|
|
3875
4716
|
if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
|
|
3876
4717
|
if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
|
|
3877
4718
|
if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
|
|
3878
|
-
if (!bom.vulnerabilities || bom.vulnerabilities.length === 0)
|
|
4719
|
+
if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) {
|
|
4720
|
+
if (hasMLModelComponent(bom.components ?? [])) throw new Error("cyclonedx: this file is a CycloneDX AI-BOM (machine-learning-model inventory) with no vulnerabilities; to import it into a system document, use:\n hdf system create <file> --from cyclonedx-mlbom");
|
|
4721
|
+
throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create <sbom-file> --component-name <name>");
|
|
4722
|
+
}
|
|
3879
4723
|
const resultsChecksum = await inputChecksum(input);
|
|
3880
4724
|
const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
|
|
3881
4725
|
const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
|
|
@@ -3917,13 +4761,12 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3917
4761
|
data: fixParts.join("\n\n")
|
|
3918
4762
|
});
|
|
3919
4763
|
const affects = vuln.affects ?? [];
|
|
3920
|
-
const
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
})) : [createResult(ResultStatus.Failed, void 0, {
|
|
3924
|
-
codeDesc: `Vulnerability ${vuln.id}`,
|
|
4764
|
+
const toResult = (codeDesc) => ({
|
|
4765
|
+
status: ResultStatus.Failed,
|
|
4766
|
+
codeDesc,
|
|
3925
4767
|
startTime: scanTime
|
|
3926
|
-
})
|
|
4768
|
+
});
|
|
4769
|
+
const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
|
|
3927
4770
|
const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
|
|
3928
4771
|
const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
|
|
3929
4772
|
const controlType = deriveControlTypeFromTags(nist);
|
|
@@ -3931,16 +4774,29 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3931
4774
|
requirements.push(req);
|
|
3932
4775
|
}
|
|
3933
4776
|
const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
|
|
3934
|
-
const
|
|
4777
|
+
const targetComponent = {
|
|
4778
|
+
name: bom.metadata?.component?.name ?? "",
|
|
4779
|
+
type: TargetType.Application
|
|
4780
|
+
};
|
|
4781
|
+
if (bom.metadata?.component?.version) targetComponent.version = bom.metadata.component.version;
|
|
4782
|
+
const parsedBom = parseBom(input);
|
|
4783
|
+
const bomParts = {
|
|
4784
|
+
bomType: BOMType.Sbom,
|
|
4785
|
+
format: "cyclonedx",
|
|
4786
|
+
uniqueId: parsedBom.normalized.uniqueId,
|
|
4787
|
+
document: JSON.parse(input)
|
|
4788
|
+
};
|
|
4789
|
+
if (parsedBom.normalized.packages && parsedBom.normalized.packages.length > 0) bomParts.packages = parsedBom.normalized.packages;
|
|
4790
|
+
const componentBom = buildBom(bomParts);
|
|
3935
4791
|
return buildHdfResults({
|
|
3936
4792
|
generatorName: "cyclonedx-to-hdf",
|
|
3937
|
-
converterVersion
|
|
4793
|
+
converterVersion,
|
|
3938
4794
|
toolName: "CycloneDX",
|
|
3939
4795
|
toolFormat: "JSON",
|
|
3940
4796
|
baselines: [baseline],
|
|
3941
4797
|
components: [{
|
|
3942
|
-
|
|
3943
|
-
|
|
4798
|
+
...targetComponent,
|
|
4799
|
+
boms: [componentBom]
|
|
3944
4800
|
}],
|
|
3945
4801
|
timestamp: scanTime
|
|
3946
4802
|
});
|
|
@@ -3954,7 +4810,7 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3954
4810
|
*/
|
|
3955
4811
|
function convertHdfToCsv(input) {
|
|
3956
4812
|
validateInputSize(input, "hdf-to-csv");
|
|
3957
|
-
const hdf =
|
|
4813
|
+
const hdf = parseHdf(input);
|
|
3958
4814
|
if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
3959
4815
|
if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
3960
4816
|
const rows = [];
|
|
@@ -3974,6 +4830,11 @@ function convertHdfToCsv(input) {
|
|
|
3974
4830
|
*/
|
|
3975
4831
|
function createRow(baseline, requirement, target) {
|
|
3976
4832
|
const description = requirement.descriptions.find((d) => d.label === "default")?.data || "";
|
|
4833
|
+
const check = descriptionByLabel(requirement, "check");
|
|
4834
|
+
const fix = descriptionByLabel(requirement, "fix");
|
|
4835
|
+
const rationale = descriptionByLabel(requirement, "rationale");
|
|
4836
|
+
const code = requirement.code ?? "";
|
|
4837
|
+
const references = flattenRefs(requirement.refs);
|
|
3977
4838
|
const severity = getSeverity(requirement);
|
|
3978
4839
|
const firstResult = requirement.results[0];
|
|
3979
4840
|
const status = firstResult?.status || "";
|
|
@@ -3989,6 +4850,11 @@ function createRow(baseline, requirement, target) {
|
|
|
3989
4850
|
"Requirement ID": requirement.id,
|
|
3990
4851
|
"Requirement Title": requirement.title || "",
|
|
3991
4852
|
"Description": description,
|
|
4853
|
+
"Check": check,
|
|
4854
|
+
"Fix": fix,
|
|
4855
|
+
"Rationale": rationale,
|
|
4856
|
+
"Code": code,
|
|
4857
|
+
"References": references,
|
|
3992
4858
|
"Severity": severity,
|
|
3993
4859
|
"Impact": requirement.impact,
|
|
3994
4860
|
"Status": String(status),
|
|
@@ -4011,21 +4877,826 @@ function getSeverity(requirement) {
|
|
|
4011
4877
|
if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
|
|
4012
4878
|
}
|
|
4013
4879
|
}
|
|
4014
|
-
const impact = requirement.impact;
|
|
4015
|
-
if (impact >= .7) return "high";
|
|
4016
|
-
if (impact >= .4) return "medium";
|
|
4017
|
-
return "low";
|
|
4880
|
+
const impact = requirement.impact;
|
|
4881
|
+
if (impact >= .7) return "high";
|
|
4882
|
+
if (impact >= .4) return "medium";
|
|
4883
|
+
return "low";
|
|
4884
|
+
}
|
|
4885
|
+
/**
|
|
4886
|
+
* Extract array values from tags object and join with semicolons
|
|
4887
|
+
*/
|
|
4888
|
+
function extractArrayFromTags(tags, key) {
|
|
4889
|
+
if (!tags || typeof tags !== "object") return "";
|
|
4890
|
+
const value = tags[key];
|
|
4891
|
+
if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
|
|
4892
|
+
return "";
|
|
4893
|
+
}
|
|
4894
|
+
/**
|
|
4895
|
+
* Get the data of the first description matching a label. Empty when absent.
|
|
4896
|
+
*/
|
|
4897
|
+
function descriptionByLabel(requirement, label) {
|
|
4898
|
+
return requirement.descriptions.find((d) => d.label === label)?.data || "";
|
|
4899
|
+
}
|
|
4900
|
+
/**
|
|
4901
|
+
* Flatten a requirement's refs to one string: each Reference rendered as its
|
|
4902
|
+
* url/uri (or a string `ref`); array-form refs are skipped. Joined with '; ' to
|
|
4903
|
+
* match the NIST/CCI column convention.
|
|
4904
|
+
*/
|
|
4905
|
+
function flattenRefs(refs) {
|
|
4906
|
+
if (!Array.isArray(refs)) return "";
|
|
4907
|
+
const out = [];
|
|
4908
|
+
for (const r of refs) {
|
|
4909
|
+
if (!r || typeof r !== "object") continue;
|
|
4910
|
+
const o = r;
|
|
4911
|
+
if (typeof o.url === "string") out.push(o.url);
|
|
4912
|
+
else if (typeof o.uri === "string") out.push(o.uri);
|
|
4913
|
+
else if (typeof o.ref === "string") out.push(o.ref);
|
|
4914
|
+
}
|
|
4915
|
+
return out.join("; ");
|
|
4916
|
+
}
|
|
4917
|
+
//#endregion
|
|
4918
|
+
//#region shared/typescript/exportmap.ts
|
|
4919
|
+
/**
|
|
4920
|
+
* Generic, source-tool-agnostic mapping helpers shared by the HDF export
|
|
4921
|
+
* converters (hdf-to-ecs, hdf-to-splunk, ...).
|
|
4922
|
+
*
|
|
4923
|
+
* The export converters deliberately operate on generically-parsed JSON rather
|
|
4924
|
+
* than typed HDF structs, so their output can be held byte-identical with the
|
|
4925
|
+
* Go implementations. This module centralizes the generic accessors, the status
|
|
4926
|
+
* roll-up, the requirement/document field extraction, and the canonical
|
|
4927
|
+
* (key-sorted, HTML-unescaped) line serialization. Target-specific event shaping
|
|
4928
|
+
* (ECS field names, CIM field names, envelopes) stays in each converter.
|
|
4929
|
+
*/
|
|
4930
|
+
function asMap(v) {
|
|
4931
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
4932
|
+
}
|
|
4933
|
+
function asArr(v) {
|
|
4934
|
+
return Array.isArray(v) ? v : void 0;
|
|
4935
|
+
}
|
|
4936
|
+
function getStr(m, key) {
|
|
4937
|
+
const v = m?.[key];
|
|
4938
|
+
return typeof v === "string" ? v : "";
|
|
4939
|
+
}
|
|
4940
|
+
function setIf(m, key, val) {
|
|
4941
|
+
if (val !== "") m[key] = val;
|
|
4942
|
+
}
|
|
4943
|
+
function stringSlice(v) {
|
|
4944
|
+
if (typeof v === "string") return [v];
|
|
4945
|
+
if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
|
|
4946
|
+
return [];
|
|
4947
|
+
}
|
|
4948
|
+
/** Most-significant status across a requirement's results[] (lossless). */
|
|
4949
|
+
function worstOfResults(req) {
|
|
4950
|
+
const results = asArr(req.results) ?? [];
|
|
4951
|
+
const precedence = [
|
|
4952
|
+
"failed",
|
|
4953
|
+
"error",
|
|
4954
|
+
"passed",
|
|
4955
|
+
"notReviewed",
|
|
4956
|
+
"notApplicable"
|
|
4957
|
+
];
|
|
4958
|
+
const present = /* @__PURE__ */ new Set();
|
|
4959
|
+
for (const rRaw of results) {
|
|
4960
|
+
const r = asMap(rRaw);
|
|
4961
|
+
if (r) present.add(getStr(r, "status"));
|
|
4962
|
+
}
|
|
4963
|
+
for (const s of precedence) if (present.has(s)) return s;
|
|
4964
|
+
return "notReviewed";
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
|
|
4968
|
+
* 'error' is indeterminate (not a compliance failure) and every other value is
|
|
4969
|
+
* non-failing. Single shared definition of "failing" for the suppression axis
|
|
4970
|
+
* and the per-exporter outcome maps.
|
|
4971
|
+
*/
|
|
4972
|
+
function isFailing(status) {
|
|
4973
|
+
return status === "failed";
|
|
4974
|
+
}
|
|
4975
|
+
/** Resolve the status context for a requirement. */
|
|
4976
|
+
function statusOf(req) {
|
|
4977
|
+
const raw = worstOfResults(req);
|
|
4978
|
+
const effective = getStr(req, "effectiveStatus");
|
|
4979
|
+
const overrides = asArr(req.statusOverrides);
|
|
4980
|
+
const rollup = effective !== "" ? effective : raw;
|
|
4981
|
+
return {
|
|
4982
|
+
raw,
|
|
4983
|
+
effective,
|
|
4984
|
+
rollup,
|
|
4985
|
+
overridden: overrides !== void 0 && overrides.length > 0 || "effectiveStatus" in req,
|
|
4986
|
+
suppressed: isFailing(raw) && !isFailing(rollup)
|
|
4987
|
+
};
|
|
4988
|
+
}
|
|
4989
|
+
function firstComponent(doc) {
|
|
4990
|
+
const comps = asArr(doc.components);
|
|
4991
|
+
if (!comps || comps.length === 0) return void 0;
|
|
4992
|
+
return asMap(comps[0]);
|
|
4993
|
+
}
|
|
4994
|
+
function firstResultStartTime(req, fallback) {
|
|
4995
|
+
const results = asArr(req.results) ?? [];
|
|
4996
|
+
if (results.length > 0) {
|
|
4997
|
+
const st = getStr(asMap(results[0]), "startTime");
|
|
4998
|
+
if (st !== "") return st;
|
|
4999
|
+
}
|
|
5000
|
+
return fallback;
|
|
5001
|
+
}
|
|
5002
|
+
function defaultDescription(req) {
|
|
5003
|
+
const descs = asArr(req.descriptions) ?? [];
|
|
5004
|
+
for (const dRaw of descs) {
|
|
5005
|
+
const d = asMap(dRaw);
|
|
5006
|
+
if (d && getStr(d, "label") === "default") return getStr(d, "data");
|
|
5007
|
+
}
|
|
5008
|
+
return "";
|
|
5009
|
+
}
|
|
5010
|
+
function firstRefURL(req) {
|
|
5011
|
+
const refs = asArr(req.refs) ?? [];
|
|
5012
|
+
for (const rRaw of refs) {
|
|
5013
|
+
const url = getStr(asMap(rRaw), "url");
|
|
5014
|
+
if (url !== "") return url;
|
|
5015
|
+
}
|
|
5016
|
+
return "";
|
|
5017
|
+
}
|
|
5018
|
+
/** Deterministic event id: component | baseline | control. */
|
|
5019
|
+
function eventID(component, baselineName, controlID) {
|
|
5020
|
+
let comp = "";
|
|
5021
|
+
if (component) comp = getStr(component, "componentId") || getStr(component, "name");
|
|
5022
|
+
return [
|
|
5023
|
+
comp,
|
|
5024
|
+
baselineName,
|
|
5025
|
+
controlID
|
|
5026
|
+
].join("|");
|
|
5027
|
+
}
|
|
5028
|
+
/**
|
|
5029
|
+
* Build the lossless hdf.* namespace shared by the export converters: promoted
|
|
5030
|
+
* snake_case scalars plus the full requirement sub-objects preserved verbatim.
|
|
5031
|
+
* `status` is the lossless results roll-up (statusOf().raw); `suppressed` is the
|
|
5032
|
+
* acceptance axis (statusOf().suppressed).
|
|
5033
|
+
*/
|
|
5034
|
+
function buildHDFBlock(req, baseline, status, overridden, suppressed, generator, tool, converterVersion) {
|
|
5035
|
+
const hdf = {
|
|
5036
|
+
status,
|
|
5037
|
+
overridden,
|
|
5038
|
+
suppressed,
|
|
5039
|
+
exporter_version: converterVersion
|
|
5040
|
+
};
|
|
5041
|
+
setIf(hdf, "control_id", getStr(req, "id"));
|
|
5042
|
+
setIf(hdf, "baseline", getStr(baseline, "name"));
|
|
5043
|
+
if ("effectiveStatus" in req) hdf.effective_status = req.effectiveStatus;
|
|
5044
|
+
if ("effectiveImpact" in req) hdf.effective_impact = req.effectiveImpact;
|
|
5045
|
+
if ("impact" in req) hdf.impact = req.impact;
|
|
5046
|
+
if ("severity" in req) hdf.severity = req.severity;
|
|
5047
|
+
if ("disposition" in req) hdf.disposition = req.disposition;
|
|
5048
|
+
const tags = asMap(req.tags);
|
|
5049
|
+
if (tags?.nist !== void 0) hdf.nist = tags.nist;
|
|
5050
|
+
if (tags?.cci !== void 0) hdf.cci = tags.cci;
|
|
5051
|
+
for (const [src, dst] of Object.entries({
|
|
5052
|
+
tags: "tags",
|
|
5053
|
+
cvss: "cvss",
|
|
5054
|
+
cwe: "cwe",
|
|
5055
|
+
epss: "epss",
|
|
5056
|
+
kev: "kev",
|
|
5057
|
+
affectedPackages: "affected_packages",
|
|
5058
|
+
descriptions: "descriptions",
|
|
5059
|
+
results: "results",
|
|
5060
|
+
statusOverrides: "status_overrides",
|
|
5061
|
+
poams: "poams",
|
|
5062
|
+
code: "code",
|
|
5063
|
+
refs: "refs"
|
|
5064
|
+
})) if (src in req) hdf[dst] = req[src];
|
|
5065
|
+
if (generator) hdf.generator = generator;
|
|
5066
|
+
if (tool) hdf.tool = tool;
|
|
5067
|
+
return hdf;
|
|
5068
|
+
}
|
|
5069
|
+
/**
|
|
5070
|
+
* A numeric value that must serialize with an explicit decimal point, so a
|
|
5071
|
+
* whole-number renders as `10.0` rather than the integer `10`. Some consumers
|
|
5072
|
+
* type-check strictly (OCSF's `float_t` rejects an integer-shaped token).
|
|
5073
|
+
* JSON.stringify cannot emit `10.0` for a JS number, so the value is carried in
|
|
5074
|
+
* this wrapper (passed through canonicalize untouched) and rendered as a bare
|
|
5075
|
+
* numeric token by stringifyLine. Go's counterpart is encoding/json's json.Number.
|
|
5076
|
+
*/
|
|
5077
|
+
var RawNumber = class {
|
|
5078
|
+
constructor(token) {
|
|
5079
|
+
this.token = token;
|
|
5080
|
+
}
|
|
5081
|
+
};
|
|
5082
|
+
/** Wrap a number as a RawNumber whose token always bears a decimal point. */
|
|
5083
|
+
function floatNumber(f) {
|
|
5084
|
+
let s = String(f);
|
|
5085
|
+
if (!/[.eE]/.test(s)) s += ".0";
|
|
5086
|
+
return new RawNumber(s);
|
|
5087
|
+
}
|
|
5088
|
+
const RAWNUM_MARK = String.fromCharCode(1);
|
|
5089
|
+
const RAWNUM_RE = /"\\u0001(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\\u0001"/g;
|
|
5090
|
+
/**
|
|
5091
|
+
* Compare two strings by Unicode code point. This matches Go's `encoding/json`
|
|
5092
|
+
* key ordering, which is a bytewise comparison of the UTF-8 encoding \u2014 and UTF-8
|
|
5093
|
+
* byte order is identical to code-point order. JS's default Array.sort() instead
|
|
5094
|
+
* compares UTF-16 code units, which diverges for supplementary-plane (non-BMP)
|
|
5095
|
+
* characters: a lead surrogate (0xD800\u20130xDBFF) sorts before a BMP char in
|
|
5096
|
+
* [0xE000,0xFFFF] under UTF-16, but a non-BMP code point (\u22650x10000) sorts after
|
|
5097
|
+
* it under UTF-8. Iterating the string yields whole code points (surrogate pairs
|
|
5098
|
+
* combined), so this comparator is correct for the full Unicode range.
|
|
5099
|
+
*/
|
|
5100
|
+
function byCodePoint(a, b) {
|
|
5101
|
+
const ai = [...a];
|
|
5102
|
+
const bi = [...b];
|
|
5103
|
+
const n = Math.min(ai.length, bi.length);
|
|
5104
|
+
for (let i = 0; i < n; i++) {
|
|
5105
|
+
const ca = ai[i]?.codePointAt(0) ?? 0;
|
|
5106
|
+
const cb = bi[i]?.codePointAt(0) ?? 0;
|
|
5107
|
+
if (ca !== cb) return ca - cb;
|
|
5108
|
+
}
|
|
5109
|
+
return ai.length - bi.length;
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* Recursively sort object keys in Go's map-key order (bytewise UTF-8 / code
|
|
5113
|
+
* point) so the emitted JSON is byte-identical to Go's encoder.
|
|
5114
|
+
*/
|
|
5115
|
+
function canonicalize$1(v) {
|
|
5116
|
+
if (v instanceof RawNumber) return v;
|
|
5117
|
+
if (Array.isArray(v)) return v.map(canonicalize$1);
|
|
5118
|
+
const m = asMap(v);
|
|
5119
|
+
if (m) {
|
|
5120
|
+
const out = {};
|
|
5121
|
+
for (const k of Object.keys(m).sort(byCodePoint)) out[k] = canonicalize$1(m[k]);
|
|
5122
|
+
return out;
|
|
5123
|
+
}
|
|
5124
|
+
return v;
|
|
5125
|
+
}
|
|
5126
|
+
/**
|
|
5127
|
+
* Emit compact JSON matching Go's encoder byte-for-byte. A RawNumber is rendered
|
|
5128
|
+
* as a bare numeric token (Go's json.Number) via an SOH-delimited placeholder
|
|
5129
|
+
* that is stripped back to the token afterward. Go's encoding/json escapes
|
|
5130
|
+
* U+2028/U+2029 (JSONP safety) while JSON.stringify emits them raw, so we escape
|
|
5131
|
+
* them here. Key ordering is handled upstream by canonicalize (code point order).
|
|
5132
|
+
*/
|
|
5133
|
+
function stringifyLine(v) {
|
|
5134
|
+
return JSON.stringify(v, (_key, val) => val instanceof RawNumber ? RAWNUM_MARK + val.token + RAWNUM_MARK : val).replace(RAWNUM_RE, "$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
5135
|
+
}
|
|
5136
|
+
/**
|
|
5137
|
+
* Shared entry-point driver for the HDF→SIEM exporters: runs the identical
|
|
5138
|
+
* prologue (validate, parse, baselines extraction, doc-level context), fans out
|
|
5139
|
+
* one output object per requirement via `build`, and joins the canonical NDJSON
|
|
5140
|
+
* lines (byte-identical with the Go `Export` driver). `converterName` prefixes
|
|
5141
|
+
* the missing-baselines error and drives validateInputSize.
|
|
5142
|
+
*/
|
|
5143
|
+
function runExport(input, converterName, build) {
|
|
5144
|
+
validateInputSize(input, converterName);
|
|
5145
|
+
const doc = parseHdf(input);
|
|
5146
|
+
const baselines = asArr(doc.baselines);
|
|
5147
|
+
if (!baselines) throw new Error(`${converterName}: invalid HDF structure: missing baselines field`);
|
|
5148
|
+
const docTimestamp = getStr(doc, "timestamp");
|
|
5149
|
+
const tool = asMap(doc.tool);
|
|
5150
|
+
const generator = asMap(doc.generator);
|
|
5151
|
+
const component = firstComponent(doc);
|
|
5152
|
+
const lines = [];
|
|
5153
|
+
for (const bRaw of baselines) {
|
|
5154
|
+
const baseline = asMap(bRaw);
|
|
5155
|
+
if (!baseline) continue;
|
|
5156
|
+
const reqs = asArr(baseline.requirements) ?? [];
|
|
5157
|
+
for (const rRaw of reqs) {
|
|
5158
|
+
const req = asMap(rRaw);
|
|
5159
|
+
if (!req) continue;
|
|
5160
|
+
lines.push(stringifyLine(canonicalize$1(build(req, baseline, docTimestamp, tool, generator, component))));
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
return lines.length === 0 ? "" : lines.join("\n") + "\n";
|
|
5164
|
+
}
|
|
5165
|
+
/**
|
|
5166
|
+
* The first cvss[].source that looks like a CVE id (case-insensitive "CVE-"
|
|
5167
|
+
* prefix), or ''. Shared by the exporters that key vulnerability identity off
|
|
5168
|
+
* the CVE (splunk, ocsf).
|
|
5169
|
+
*/
|
|
5170
|
+
function firstCVE(cvssList) {
|
|
5171
|
+
for (const c of cvssList) {
|
|
5172
|
+
const src = getStr(asMap(c), "source");
|
|
5173
|
+
if (src.toUpperCase().startsWith("CVE-")) return src;
|
|
5174
|
+
}
|
|
5175
|
+
return "";
|
|
5176
|
+
}
|
|
5177
|
+
/**
|
|
5178
|
+
* Parse an HDF RFC3339 timestamp into integer epoch seconds (Splunk HEC `time`)
|
|
5179
|
+
* via the canonical parser, returning undefined when empty/unparseable. Integer
|
|
5180
|
+
* epoch keeps Go and TypeScript byte-identical.
|
|
5181
|
+
*/
|
|
5182
|
+
function epochSeconds(s) {
|
|
5183
|
+
const d = parseTimestamp(s);
|
|
5184
|
+
if (d === null) return void 0;
|
|
5185
|
+
return Math.floor(d.getTime() / 1e3);
|
|
5186
|
+
}
|
|
5187
|
+
/**
|
|
5188
|
+
* Parse an HDF RFC3339 timestamp into integer epoch milliseconds (OCSF `time`)
|
|
5189
|
+
* via the canonical parser, returning undefined when empty/unparseable.
|
|
5190
|
+
*/
|
|
5191
|
+
function epochMillis(s) {
|
|
5192
|
+
const d = parseTimestamp(s);
|
|
5193
|
+
if (d === null) return void 0;
|
|
5194
|
+
return d.getTime();
|
|
5195
|
+
}
|
|
5196
|
+
//#endregion
|
|
5197
|
+
//#region converters/hdf-to-ecs/typescript/converter.ts
|
|
5198
|
+
/**
|
|
5199
|
+
* HDF Results -> Elastic Common Schema (ECS) NDJSON exporter.
|
|
5200
|
+
*
|
|
5201
|
+
* One ECS event per Evaluated_Requirement, in the hybrid shape from ADR-0002:
|
|
5202
|
+
* a core-ECS-native projection plus a lossless hdf.* block, hot filter scalars
|
|
5203
|
+
* promoted flat. Output is plain NDJSON (LF-delimited, trailing newline), ECS
|
|
5204
|
+
* 9.4.0.
|
|
5205
|
+
*
|
|
5206
|
+
* Status is raw-primary: event.outcome carries the RAW verdict (a waived failure
|
|
5207
|
+
* is still failure), and hdf.suppressed is the separate acceptance axis. The
|
|
5208
|
+
* canonical "still actionable" query is
|
|
5209
|
+
* event.outcome:"failure" AND hdf.suppressed:false.
|
|
5210
|
+
*
|
|
5211
|
+
* Generic JSON access, the status roll-up, requirement/document field
|
|
5212
|
+
* extraction, and the canonical line serialization are shared with the other
|
|
5213
|
+
* export converters via ../../../shared/typescript/exportmap.js; only
|
|
5214
|
+
* ECS-specific event shaping lives here. Output is held byte-identical with the
|
|
5215
|
+
* Go implementation via key-sorted, HTML-unescaped compact JSON.
|
|
5216
|
+
*/
|
|
5217
|
+
const ECS_VERSION = "9.4.0";
|
|
5218
|
+
function convertHdfToEcs(input, converterVersion = "0.1.0") {
|
|
5219
|
+
return runExport(input, "hdf-to-ecs", (req, baseline, docTimestamp, tool, generator, component) => buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion));
|
|
5220
|
+
}
|
|
5221
|
+
function buildEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
|
|
5222
|
+
const st = statusOf(req);
|
|
5223
|
+
const outcome = statusToOutcome(st.raw);
|
|
5224
|
+
const controlID = getStr(req, "id");
|
|
5225
|
+
const baselineName = getStr(baseline, "name");
|
|
5226
|
+
const title = getStr(req, "title");
|
|
5227
|
+
const cvssList = asArr(req.cvss);
|
|
5228
|
+
const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
|
|
5229
|
+
const categories = ["configuration"];
|
|
5230
|
+
if (hasCVSS) categories.push("vulnerability");
|
|
5231
|
+
const event = {
|
|
5232
|
+
kind: "state",
|
|
5233
|
+
category: categories,
|
|
5234
|
+
type: ["info"],
|
|
5235
|
+
outcome,
|
|
5236
|
+
id: eventID(component, baselineName, controlID),
|
|
5237
|
+
dataset: "hdf.findings",
|
|
5238
|
+
module: "hdf"
|
|
5239
|
+
};
|
|
5240
|
+
const obj = {
|
|
5241
|
+
"@timestamp": firstResultStartTime(req, docTimestamp),
|
|
5242
|
+
ecs: { version: ECS_VERSION },
|
|
5243
|
+
event,
|
|
5244
|
+
message: (title + " — " + st.raw).trim()
|
|
5245
|
+
};
|
|
5246
|
+
const observer = buildObserver(tool, generator);
|
|
5247
|
+
if (observer) obj.observer = observer;
|
|
5248
|
+
const host = buildHost(component);
|
|
5249
|
+
if (host) {
|
|
5250
|
+
obj.host = host;
|
|
5251
|
+
const related = buildRelated(component);
|
|
5252
|
+
if (related) obj.related = related;
|
|
5253
|
+
}
|
|
5254
|
+
obj.rule = buildRule(req, baseline, controlID, title);
|
|
5255
|
+
if (hasCVSS) obj.vulnerability = buildVulnerability$1(cvssList, req, tool);
|
|
5256
|
+
const threat = buildThreat(req);
|
|
5257
|
+
if (threat) obj.threat = threat;
|
|
5258
|
+
obj.hdf = buildHDFBlock(req, baseline, st.raw, st.overridden, st.suppressed, generator, tool, converterVersion);
|
|
5259
|
+
return obj;
|
|
5260
|
+
}
|
|
5261
|
+
function buildObserver(tool, generator) {
|
|
5262
|
+
let name = getStr(tool, "name");
|
|
5263
|
+
let version = getStr(tool, "version");
|
|
5264
|
+
if (name === "" && generator) {
|
|
5265
|
+
name = getStr(generator, "name");
|
|
5266
|
+
version = getStr(generator, "version");
|
|
5267
|
+
}
|
|
5268
|
+
if (name === "") return void 0;
|
|
5269
|
+
const observer = {
|
|
5270
|
+
name,
|
|
5271
|
+
type: "scanner"
|
|
5272
|
+
};
|
|
5273
|
+
if (version !== "") observer.version = version;
|
|
5274
|
+
const product = getStr(tool, "format");
|
|
5275
|
+
if (product !== "") observer.product = product;
|
|
5276
|
+
return observer;
|
|
5277
|
+
}
|
|
5278
|
+
function buildHost(component) {
|
|
5279
|
+
if (!component) return void 0;
|
|
5280
|
+
const host = {};
|
|
5281
|
+
const name = getStr(component, "fqdn") || getStr(component, "name");
|
|
5282
|
+
if (name !== "") host.name = name;
|
|
5283
|
+
setIf(host, "id", getStr(component, "componentId"));
|
|
5284
|
+
setIf(host, "ip", getStr(component, "ipAddress"));
|
|
5285
|
+
setIf(host, "mac", getStr(component, "macAddress"));
|
|
5286
|
+
const os = {};
|
|
5287
|
+
setIf(os, "name", getStr(component, "osName"));
|
|
5288
|
+
setIf(os, "version", getStr(component, "osVersion"));
|
|
5289
|
+
if (Object.keys(os).length > 0) host.os = os;
|
|
5290
|
+
return Object.keys(host).length === 0 ? void 0 : host;
|
|
5291
|
+
}
|
|
5292
|
+
function buildRelated(component) {
|
|
5293
|
+
const related = {};
|
|
5294
|
+
const name = getStr(component, "fqdn") || getStr(component, "name");
|
|
5295
|
+
if (name !== "") related.hosts = [name];
|
|
5296
|
+
const ip = getStr(component, "ipAddress");
|
|
5297
|
+
if (ip !== "") related.ip = [ip];
|
|
5298
|
+
return Object.keys(related).length === 0 ? void 0 : related;
|
|
5299
|
+
}
|
|
5300
|
+
function buildRule(req, baseline, controlID, title) {
|
|
5301
|
+
const rule = { id: controlID };
|
|
5302
|
+
if (title !== "") rule.name = title;
|
|
5303
|
+
setIf(rule, "description", defaultDescription(req));
|
|
5304
|
+
setIf(rule, "ruleset", getStr(baseline, "name"));
|
|
5305
|
+
setIf(rule, "version", getStr(baseline, "version"));
|
|
5306
|
+
setIf(rule, "reference", firstRefURL(req));
|
|
5307
|
+
return rule;
|
|
5308
|
+
}
|
|
5309
|
+
function buildVulnerability$1(cvssList, req, tool) {
|
|
5310
|
+
const first = asMap(cvssList[0]) ?? {};
|
|
5311
|
+
const vuln = {};
|
|
5312
|
+
const source = getStr(first, "source");
|
|
5313
|
+
if (source !== "") {
|
|
5314
|
+
vuln.id = source;
|
|
5315
|
+
if (source.toUpperCase().startsWith("CVE-")) vuln.enumeration = "CVE";
|
|
5316
|
+
} else setIf(vuln, "id", getStr(req, "id"));
|
|
5317
|
+
vuln.classification = "CVSS";
|
|
5318
|
+
const score = {};
|
|
5319
|
+
if ("baseScore" in first) score.base = first.baseScore;
|
|
5320
|
+
setIf(score, "version", getStr(first, "version"));
|
|
5321
|
+
if (Object.keys(score).length > 0) vuln.score = score;
|
|
5322
|
+
const severity = getStr(first, "baseSeverity") || getStr(req, "severity");
|
|
5323
|
+
if (severity !== "") vuln.severity = severity;
|
|
5324
|
+
const vendor = getStr(tool, "name");
|
|
5325
|
+
if (vendor !== "") vuln.scanner = { vendor };
|
|
5326
|
+
setIf(vuln, "description", defaultDescription(req));
|
|
5327
|
+
return vuln;
|
|
5328
|
+
}
|
|
5329
|
+
function buildThreat(req) {
|
|
5330
|
+
const tags = asMap(req.tags);
|
|
5331
|
+
if (!tags) return void 0;
|
|
5332
|
+
const techniques = [];
|
|
5333
|
+
for (const key of [
|
|
5334
|
+
"mitre_attack",
|
|
5335
|
+
"attack",
|
|
5336
|
+
"mitre_techniques"
|
|
5337
|
+
]) for (const id of stringSlice(tags[key])) techniques.push({ id });
|
|
5338
|
+
if (techniques.length === 0) return void 0;
|
|
5339
|
+
return {
|
|
5340
|
+
framework: "MITRE ATT&CK",
|
|
5341
|
+
technique: techniques
|
|
5342
|
+
};
|
|
5343
|
+
}
|
|
5344
|
+
function statusToOutcome(status) {
|
|
5345
|
+
switch (status) {
|
|
5346
|
+
case "passed": return "success";
|
|
5347
|
+
case "failed": return "failure";
|
|
5348
|
+
default: return "unknown";
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
//#endregion
|
|
5352
|
+
//#region converters/hdf-to-splunk/typescript/converter.ts
|
|
5353
|
+
/**
|
|
5354
|
+
* HDF Results -> Splunk HEC-envelope NDJSON, normalized to the Common
|
|
5355
|
+
* Information Model (CIM). See ADR-0004.
|
|
5356
|
+
*
|
|
5357
|
+
* One HEC event per Evaluated_Requirement, hybrid shape: flat CIM-named scalars
|
|
5358
|
+
* (signature/signature_id/cve/cvss/severity/dest/vendor_product/category) are
|
|
5359
|
+
* promoted to the top of the event payload; the hot query scalars among them
|
|
5360
|
+
* (signature/signature_id/dest/severity/cve/cvss + hdf_status + suppressed) are
|
|
5361
|
+
* additionally mirrored into the HEC indexed `fields` (surviving Splunk's
|
|
5362
|
+
* ~5000-char cutoff), plus a lossless hdf.* block.
|
|
5363
|
+
*
|
|
5364
|
+
* Status is raw-primary: hdf_status carries the RAW verdict (a waived failure is
|
|
5365
|
+
* still failed) and suppressed is the separate acceptance axis. The companion TA
|
|
5366
|
+
* (Splunk_TA_hdf) tags failed/error/CVE findings into the CIM Vulnerabilities
|
|
5367
|
+
* data model but excludes suppressed=true, so a waived control drops out while a
|
|
5368
|
+
* risk-adjusted still-failing control stays in. Canonical "still actionable"
|
|
5369
|
+
* query: hdf_status=failed suppressed=false.
|
|
5370
|
+
*
|
|
5371
|
+
* Generic access, the status roll-up, field extraction, the lossless hdf.*
|
|
5372
|
+
* block, and the canonical line serialization are shared with the other
|
|
5373
|
+
* exporters via exportmap; output is held byte-identical with the Go side.
|
|
5374
|
+
*/
|
|
5375
|
+
const SOURCETYPE = "hdf:results";
|
|
5376
|
+
const SOURCE = "hdf-exporter";
|
|
5377
|
+
function convertHdfToSplunk(input, converterVersion = "0.1.0") {
|
|
5378
|
+
return runExport(input, "hdf-to-splunk", (req, baseline, docTimestamp, tool, generator, component) => buildHECEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion));
|
|
5379
|
+
}
|
|
5380
|
+
function buildHECEvent(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
|
|
5381
|
+
const st = statusOf(req);
|
|
5382
|
+
const title = getStr(req, "title");
|
|
5383
|
+
const controlID = getStr(req, "id");
|
|
5384
|
+
const signature = title !== "" ? title : controlID;
|
|
5385
|
+
const dest = destHost(component);
|
|
5386
|
+
const sev = severity(req);
|
|
5387
|
+
const [cvss, hasCVSS] = maxCVSS(req);
|
|
5388
|
+
const cve = firstCVE(asArr(req.cvss) ?? []);
|
|
5389
|
+
const category = firstCWE(req);
|
|
5390
|
+
const vendorProduct = getStr(tool, "name");
|
|
5391
|
+
const event = {
|
|
5392
|
+
signature,
|
|
5393
|
+
hdf_status: st.raw,
|
|
5394
|
+
suppressed: st.suppressed,
|
|
5395
|
+
hdf: buildHDFBlock(req, baseline, st.raw, st.overridden, st.suppressed, generator, tool, converterVersion)
|
|
5396
|
+
};
|
|
5397
|
+
setIf(event, "signature_id", controlID);
|
|
5398
|
+
setIf(event, "dest", dest);
|
|
5399
|
+
setIf(event, "severity", sev);
|
|
5400
|
+
setIf(event, "cve", cve);
|
|
5401
|
+
setIf(event, "category", category);
|
|
5402
|
+
setIf(event, "vendor_product", vendorProduct);
|
|
5403
|
+
if (hasCVSS) event.cvss = cvss;
|
|
5404
|
+
const fields = {
|
|
5405
|
+
signature,
|
|
5406
|
+
hdf_status: st.raw,
|
|
5407
|
+
suppressed: st.suppressed
|
|
5408
|
+
};
|
|
5409
|
+
setIf(fields, "signature_id", controlID);
|
|
5410
|
+
setIf(fields, "dest", dest);
|
|
5411
|
+
setIf(fields, "severity", sev);
|
|
5412
|
+
setIf(fields, "cve", cve);
|
|
5413
|
+
if (hasCVSS) fields.cvss = cvss;
|
|
5414
|
+
const hec = {
|
|
5415
|
+
source: SOURCE,
|
|
5416
|
+
sourcetype: SOURCETYPE,
|
|
5417
|
+
event,
|
|
5418
|
+
fields
|
|
5419
|
+
};
|
|
5420
|
+
const t = epochSeconds(firstResultStartTime(req, docTimestamp));
|
|
5421
|
+
if (t !== void 0) hec.time = t;
|
|
5422
|
+
setIf(hec, "host", dest);
|
|
5423
|
+
return hec;
|
|
5424
|
+
}
|
|
5425
|
+
function destHost(component) {
|
|
5426
|
+
if (!component) return "";
|
|
5427
|
+
return getStr(component, "fqdn") || getStr(component, "name") || getStr(component, "ipAddress");
|
|
5428
|
+
}
|
|
5429
|
+
function severity(req) {
|
|
5430
|
+
if (typeof req.impact === "number") return impactToSeverity(req.impact);
|
|
5431
|
+
return normalizeSeverity(getStr(req, "severity"));
|
|
5432
|
+
}
|
|
5433
|
+
function normalizeSeverity(s) {
|
|
5434
|
+
switch (s) {
|
|
5435
|
+
case "critical":
|
|
5436
|
+
case "high":
|
|
5437
|
+
case "medium":
|
|
5438
|
+
case "low":
|
|
5439
|
+
case "informational": return s;
|
|
5440
|
+
default: return "informational";
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
function maxCVSS(req) {
|
|
5444
|
+
const list = asArr(req.cvss) ?? [];
|
|
5445
|
+
let max = 0;
|
|
5446
|
+
let found = false;
|
|
5447
|
+
for (const c of list) {
|
|
5448
|
+
const m = asMap(c);
|
|
5449
|
+
if (m && typeof m.baseScore === "number") {
|
|
5450
|
+
if (!found || m.baseScore > max) max = m.baseScore;
|
|
5451
|
+
found = true;
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
return [max, found];
|
|
4018
5455
|
}
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
function extractArrayFromTags(tags, key) {
|
|
4023
|
-
if (!tags || typeof tags !== "object") return "";
|
|
4024
|
-
const value = tags[key];
|
|
4025
|
-
if (Array.isArray(value)) return value.map((v) => String(v)).join("; ");
|
|
5456
|
+
function firstCWE(req) {
|
|
5457
|
+
const list = asArr(req.cwe) ?? [];
|
|
5458
|
+
for (const c of list) if (typeof c === "string" && c !== "") return c;
|
|
4026
5459
|
return "";
|
|
4027
5460
|
}
|
|
4028
5461
|
//#endregion
|
|
5462
|
+
//#region converters/hdf-to-ocsf/typescript/converter.ts
|
|
5463
|
+
/**
|
|
5464
|
+
* HDF Results -> OCSF (Open Cybersecurity Schema Framework) Finding NDJSON.
|
|
5465
|
+
* See the ADR-0002 addendum (HDF -> OCSF, wvc3.5). Pinned to OCSF v1.8.0.
|
|
5466
|
+
*
|
|
5467
|
+
* One Finding per requirement: a CVE finding -> Vulnerability Finding
|
|
5468
|
+
* (class_uid 2002), any other -> Compliance Finding (class_uid 2003), both in
|
|
5469
|
+
* the Findings category (2). Status model is raw-primary: compliance.status_id
|
|
5470
|
+
* carries the RAW verdict (a failed control stays Fail even when waived). The
|
|
5471
|
+
* acceptance axis rides the base finding status_id: a raw-failing finding that
|
|
5472
|
+
* an override drove non-failing (waiver/falsePositive/attestation) -> 3
|
|
5473
|
+
* Suppressed, everything else -> 1 New; a riskAdjustment/operationalRequirement/
|
|
5474
|
+
* poam that leaves the finding failing stays New (actionable, only re-scored).
|
|
5475
|
+
* The exact override chain is preserved in unmapped.hdf_requirement (+ comment).
|
|
5476
|
+
* Canonical "still actionable" query: compliance.status_id=3 AND status_id=1.
|
|
5477
|
+
*
|
|
5478
|
+
* Shares the generic access / status roll-up / field extraction / canonical
|
|
5479
|
+
* line serialization with the other exporters via exportmap; output is held
|
|
5480
|
+
* byte-identical with the Go implementation.
|
|
5481
|
+
*/
|
|
5482
|
+
const OCSF_VERSION = "1.8.0";
|
|
5483
|
+
const CATEGORY_FINDINGS = 2;
|
|
5484
|
+
const CLASS_COMPLIANCE = 2003;
|
|
5485
|
+
const CLASS_VULNERABILITY = 2002;
|
|
5486
|
+
const ACTIVITY_CREATE = 1;
|
|
5487
|
+
const STATUS_NEW = 1;
|
|
5488
|
+
const STATUS_SUPPRESSED = 3;
|
|
5489
|
+
function convertHdfToOcsf(input, converterVersion = "0.1.0") {
|
|
5490
|
+
return runExport(input, "hdf-to-ocsf", (req, baseline, docTimestamp, tool, generator, component) => buildFinding(req, baseline, docTimestamp, tool, generator, component, converterVersion));
|
|
5491
|
+
}
|
|
5492
|
+
function buildFinding(req, baseline, docTimestamp, tool, generator, component, converterVersion) {
|
|
5493
|
+
const st = statusOf(req);
|
|
5494
|
+
const cvssList = asArr(req.cvss);
|
|
5495
|
+
const hasCVSS = cvssList !== void 0 && cvssList.length > 0;
|
|
5496
|
+
const title = getStr(req, "title");
|
|
5497
|
+
const controlID = getStr(req, "id");
|
|
5498
|
+
const cls = hasCVSS ? CLASS_VULNERABILITY : CLASS_COMPLIANCE;
|
|
5499
|
+
const findingInfo = { uid: controlID };
|
|
5500
|
+
setIf(findingInfo, "title", title);
|
|
5501
|
+
setIf(findingInfo, "desc", defaultDescription(req));
|
|
5502
|
+
if (hasCVSS) {
|
|
5503
|
+
const tags = frameworkTags(req);
|
|
5504
|
+
if (tags.length > 0) findingInfo.tags = tags;
|
|
5505
|
+
}
|
|
5506
|
+
const finding = {
|
|
5507
|
+
category_uid: CATEGORY_FINDINGS,
|
|
5508
|
+
class_uid: cls,
|
|
5509
|
+
type_uid: cls * 100 + ACTIVITY_CREATE,
|
|
5510
|
+
activity_id: ACTIVITY_CREATE,
|
|
5511
|
+
severity_id: severityID(req),
|
|
5512
|
+
status_id: st.suppressed ? STATUS_SUPPRESSED : STATUS_NEW,
|
|
5513
|
+
metadata: buildMetadata$1(tool, generator, converterVersion),
|
|
5514
|
+
finding_info: findingInfo,
|
|
5515
|
+
unmapped: { hdf_requirement: req }
|
|
5516
|
+
};
|
|
5517
|
+
finding.time = epochMillis(firstResultStartTime(req, docTimestamp)) ?? 0;
|
|
5518
|
+
setIf(finding, "comment", overrideComment(req));
|
|
5519
|
+
const device = buildDevice(component);
|
|
5520
|
+
if (device) finding.device = device;
|
|
5521
|
+
if (hasCVSS) finding.vulnerabilities = buildVulnerabilities(cvssList, req);
|
|
5522
|
+
else finding.compliance = buildCompliance(req, baseline, title, st.raw);
|
|
5523
|
+
return finding;
|
|
5524
|
+
}
|
|
5525
|
+
function severityID(req) {
|
|
5526
|
+
if (typeof req.impact === "number") return severityIDFromString(impactToSeverity(req.impact));
|
|
5527
|
+
return severityIDFromString(getStr(req, "severity"));
|
|
5528
|
+
}
|
|
5529
|
+
function severityIDFromString(s) {
|
|
5530
|
+
switch (s.toLowerCase()) {
|
|
5531
|
+
case "critical": return 5;
|
|
5532
|
+
case "high": return 4;
|
|
5533
|
+
case "medium": return 3;
|
|
5534
|
+
case "low": return 2;
|
|
5535
|
+
case "informational":
|
|
5536
|
+
case "info":
|
|
5537
|
+
case "none": return 1;
|
|
5538
|
+
default: return 0;
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
function complianceStatusID(rawStatus) {
|
|
5542
|
+
switch (rawStatus) {
|
|
5543
|
+
case "passed": return 1;
|
|
5544
|
+
case "failed": return 3;
|
|
5545
|
+
default: return 2;
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
function complianceStatusCaption(statusID) {
|
|
5549
|
+
switch (statusID) {
|
|
5550
|
+
case 1: return "Pass";
|
|
5551
|
+
case 3: return "Fail";
|
|
5552
|
+
default: return "Warning";
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
function overrideComment(req) {
|
|
5556
|
+
const disposition = getStr(req, "disposition");
|
|
5557
|
+
const overrides = asArr(req.statusOverrides) ?? [];
|
|
5558
|
+
if (disposition === "" && overrides.length === 0) return "";
|
|
5559
|
+
let reason = "";
|
|
5560
|
+
if (overrides.length > 0) reason = getStr(asMap(overrides[0]), "reason");
|
|
5561
|
+
if (disposition !== "" && reason !== "") return `${disposition}: ${reason}`;
|
|
5562
|
+
if (disposition !== "") return disposition;
|
|
5563
|
+
return reason;
|
|
5564
|
+
}
|
|
5565
|
+
function buildMetadata$1(tool, generator, converterVersion) {
|
|
5566
|
+
const metadata = { version: OCSF_VERSION };
|
|
5567
|
+
let name = getStr(tool, "name");
|
|
5568
|
+
let version = getStr(tool, "version");
|
|
5569
|
+
let vendor = getStr(tool, "format");
|
|
5570
|
+
if (name === "" && generator) {
|
|
5571
|
+
name = getStr(generator, "name");
|
|
5572
|
+
version = getStr(generator, "version");
|
|
5573
|
+
}
|
|
5574
|
+
if (name === "") {
|
|
5575
|
+
name = "hdf-to-ocsf";
|
|
5576
|
+
version = converterVersion;
|
|
5577
|
+
vendor = "";
|
|
5578
|
+
}
|
|
5579
|
+
const product = { name };
|
|
5580
|
+
setIf(product, "version", version);
|
|
5581
|
+
setIf(product, "vendor_name", vendor);
|
|
5582
|
+
metadata.product = product;
|
|
5583
|
+
return metadata;
|
|
5584
|
+
}
|
|
5585
|
+
function buildDevice(component) {
|
|
5586
|
+
if (!component) return void 0;
|
|
5587
|
+
const device = { type_id: 0 };
|
|
5588
|
+
setIf(device, "name", getStr(component, "name"));
|
|
5589
|
+
setIf(device, "hostname", getStr(component, "fqdn"));
|
|
5590
|
+
setIf(device, "ip", getStr(component, "ipAddress"));
|
|
5591
|
+
setIf(device, "uid", getStr(component, "componentId"));
|
|
5592
|
+
const osName = getStr(component, "osName");
|
|
5593
|
+
if (osName !== "") {
|
|
5594
|
+
const os = {
|
|
5595
|
+
name: osName,
|
|
5596
|
+
type_id: osTypeID(osName)
|
|
5597
|
+
};
|
|
5598
|
+
setIf(os, "version", getStr(component, "osVersion"));
|
|
5599
|
+
device.os = os;
|
|
5600
|
+
}
|
|
5601
|
+
return Object.keys(device).length === 1 ? void 0 : device;
|
|
5602
|
+
}
|
|
5603
|
+
const OS_LINUX = [
|
|
5604
|
+
"linux",
|
|
5605
|
+
"rhel",
|
|
5606
|
+
"red hat",
|
|
5607
|
+
"ubuntu",
|
|
5608
|
+
"centos",
|
|
5609
|
+
"debian",
|
|
5610
|
+
"fedora",
|
|
5611
|
+
"suse"
|
|
5612
|
+
];
|
|
5613
|
+
const OS_MAC = [
|
|
5614
|
+
"mac",
|
|
5615
|
+
"darwin",
|
|
5616
|
+
"os x"
|
|
5617
|
+
];
|
|
5618
|
+
function osTypeID(osName) {
|
|
5619
|
+
const n = osName.toLowerCase();
|
|
5620
|
+
if (n.includes("windows")) return 100;
|
|
5621
|
+
if (OS_LINUX.some((k) => n.includes(k))) return 200;
|
|
5622
|
+
if (OS_MAC.some((k) => n.includes(k))) return 300;
|
|
5623
|
+
return 0;
|
|
5624
|
+
}
|
|
5625
|
+
function frameworkTags(req) {
|
|
5626
|
+
const tags = asMap(req.tags);
|
|
5627
|
+
const out = [];
|
|
5628
|
+
for (const key of ["nist", "cci"]) {
|
|
5629
|
+
const vals = stringSlice(tags?.[key]);
|
|
5630
|
+
if (vals.length > 0) out.push({
|
|
5631
|
+
name: key,
|
|
5632
|
+
values: vals
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
return out;
|
|
5636
|
+
}
|
|
5637
|
+
function buildCompliance(req, baseline, title, rawStatus) {
|
|
5638
|
+
const statusID = complianceStatusID(rawStatus);
|
|
5639
|
+
const compliance = {
|
|
5640
|
+
status_id: statusID,
|
|
5641
|
+
status: complianceStatusCaption(statusID)
|
|
5642
|
+
};
|
|
5643
|
+
const controlID = getStr(req, "id");
|
|
5644
|
+
setIf(compliance, "control", controlID);
|
|
5645
|
+
const baselineName = getStr(baseline, "name");
|
|
5646
|
+
const tags = asMap(req.tags);
|
|
5647
|
+
const nist = stringSlice(tags?.nist);
|
|
5648
|
+
const cci = stringSlice(tags?.cci);
|
|
5649
|
+
const standards = [];
|
|
5650
|
+
if (baselineName !== "") standards.push(baselineName);
|
|
5651
|
+
if (nist.length > 0) standards.push("NIST SP 800-53");
|
|
5652
|
+
if (cci.length > 0) standards.push("CCI");
|
|
5653
|
+
if (standards.length > 0) compliance.standards = standards;
|
|
5654
|
+
const checks = [];
|
|
5655
|
+
if (controlID !== "") {
|
|
5656
|
+
const check = {
|
|
5657
|
+
uid: controlID,
|
|
5658
|
+
status_id: statusID
|
|
5659
|
+
};
|
|
5660
|
+
setIf(check, "name", title);
|
|
5661
|
+
if (baselineName !== "") check.standards = [baselineName];
|
|
5662
|
+
checks.push(check);
|
|
5663
|
+
}
|
|
5664
|
+
for (const id of nist) checks.push({
|
|
5665
|
+
uid: id,
|
|
5666
|
+
standards: ["NIST SP 800-53"]
|
|
5667
|
+
});
|
|
5668
|
+
for (const id of cci) checks.push({
|
|
5669
|
+
uid: id,
|
|
5670
|
+
standards: ["CCI"]
|
|
5671
|
+
});
|
|
5672
|
+
if (checks.length > 0) compliance.checks = checks;
|
|
5673
|
+
return compliance;
|
|
5674
|
+
}
|
|
5675
|
+
function buildVulnerabilities(cvssList, req) {
|
|
5676
|
+
const cve = {};
|
|
5677
|
+
cve.uid = firstCVE(cvssList) || getStr(req, "id");
|
|
5678
|
+
const cvssArr = [];
|
|
5679
|
+
for (const c of cvssList) {
|
|
5680
|
+
const m = asMap(c);
|
|
5681
|
+
if (!m) continue;
|
|
5682
|
+
const entry = {};
|
|
5683
|
+
if (typeof m.baseScore === "number") entry.base_score = floatNumber(m.baseScore);
|
|
5684
|
+
setIf(entry, "version", getStr(m, "version"));
|
|
5685
|
+
setIf(entry, "vector_string", getStr(m, "baseVector"));
|
|
5686
|
+
setIf(entry, "severity", getStr(m, "baseSeverity"));
|
|
5687
|
+
cvssArr.push(entry);
|
|
5688
|
+
}
|
|
5689
|
+
if (cvssArr.length > 0) cve.cvss = cvssArr;
|
|
5690
|
+
const cwes = stringSlice(req.cwe);
|
|
5691
|
+
if (cwes.length > 0) cve.related_cwes = cwes.map((id) => ({ uid: id }));
|
|
5692
|
+
const vuln = { cve };
|
|
5693
|
+
setIf(vuln, "title", getStr(req, "title"));
|
|
5694
|
+
setIf(vuln, "desc", defaultDescription(req));
|
|
5695
|
+
const ref = firstRefURL(req);
|
|
5696
|
+
if (ref !== "") vuln.references = [ref];
|
|
5697
|
+
return [vuln];
|
|
5698
|
+
}
|
|
5699
|
+
//#endregion
|
|
4029
5700
|
//#region converters/splunk-to-hdf/typescript/converter.ts
|
|
4030
5701
|
/**
|
|
4031
5702
|
* Map a Splunk result status string to the HDF ResultStatus enum.
|
|
@@ -4041,11 +5712,12 @@ function mapStatus$1(status) {
|
|
|
4041
5712
|
}
|
|
4042
5713
|
/**
|
|
4043
5714
|
* Convert a Splunk descriptions object (`{ key: value }`) to an array of
|
|
4044
|
-
* HDF Description objects.
|
|
5715
|
+
* HDF Description objects. Labels are sorted: Go decodes the same object into
|
|
5716
|
+
* a map, so sorting is the only ordering both languages can agree on.
|
|
4045
5717
|
*/
|
|
4046
5718
|
function convertDescriptions(descriptions) {
|
|
4047
5719
|
if (!descriptions || typeof descriptions !== "object") return [];
|
|
4048
|
-
return Object.entries(descriptions).map(([label, data]) => createDescription(label, data));
|
|
5720
|
+
return Object.entries(descriptions).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([label, data]) => createDescription(label, data));
|
|
4049
5721
|
}
|
|
4050
5722
|
/**
|
|
4051
5723
|
* Convert Splunk profile groups (which use `controls`) to HDF
|
|
@@ -4069,7 +5741,7 @@ function convertGroups(groups) {
|
|
|
4069
5741
|
* @param input - JSON string of a SplunkEvent array
|
|
4070
5742
|
* @returns HDF Results JSON string
|
|
4071
5743
|
*/
|
|
4072
|
-
async function convertSplunkToHdf(input) {
|
|
5744
|
+
async function convertSplunkToHdf(input, converterVersion = "1.0.0") {
|
|
4073
5745
|
validateInputSize(input, "splunk");
|
|
4074
5746
|
const resultsChecksum = await inputChecksum(input);
|
|
4075
5747
|
const events = parseJSON(input);
|
|
@@ -4081,9 +5753,10 @@ async function convertSplunkToHdf(input) {
|
|
|
4081
5753
|
eventsByGuid.get(guid).push(event);
|
|
4082
5754
|
}
|
|
4083
5755
|
const allBaselines = [];
|
|
4084
|
-
|
|
4085
|
-
let
|
|
4086
|
-
for (const
|
|
5756
|
+
const components = [];
|
|
5757
|
+
let statistics;
|
|
5758
|
+
for (const guid of [...eventsByGuid.keys()].sort()) {
|
|
5759
|
+
const guidEvents = eventsByGuid.get(guid);
|
|
4087
5760
|
const headers = [];
|
|
4088
5761
|
const profiles = [];
|
|
4089
5762
|
const controls = [];
|
|
@@ -4100,8 +5773,13 @@ async function convertSplunkToHdf(input) {
|
|
|
4100
5773
|
}
|
|
4101
5774
|
if (headers.length !== 1) throw new Error(`Expected 1 header event, got ${headers.length}`);
|
|
4102
5775
|
const header = headers[0];
|
|
4103
|
-
|
|
4104
|
-
|
|
5776
|
+
const component = {
|
|
5777
|
+
name: header.platform.name,
|
|
5778
|
+
type: TargetType.Host
|
|
5779
|
+
};
|
|
5780
|
+
if (header.platform.release) component.osVersion = header.platform.release;
|
|
5781
|
+
components.push(component);
|
|
5782
|
+
statistics = convertStatistics(header.statistics);
|
|
4105
5783
|
const controlsByProfile = /* @__PURE__ */ new Map();
|
|
4106
5784
|
for (const control of controls) {
|
|
4107
5785
|
const sha = control.meta.profile_sha256 ?? "";
|
|
@@ -4111,16 +5789,24 @@ async function convertSplunkToHdf(input) {
|
|
|
4111
5789
|
for (const profile of profiles) {
|
|
4112
5790
|
const requirements = (controlsByProfile.get(profile.sha256) ?? []).map((control) => {
|
|
4113
5791
|
const descriptions = convertDescriptions(control.descriptions);
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
5792
|
+
if (descriptions.length === 0) descriptions.push(createDescription("default", control.desc ?? ""));
|
|
5793
|
+
const results = Array.isArray(control.results) ? control.results.map((result) => {
|
|
5794
|
+
const res = createResult(mapStatus$1(result.status), result.skip_message || result.message, {
|
|
5795
|
+
codeDesc: result.code_desc,
|
|
5796
|
+
startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
|
|
5797
|
+
runTime: result.run_time,
|
|
5798
|
+
exception: result.exception,
|
|
5799
|
+
backtrace: result.backtrace
|
|
5800
|
+
});
|
|
5801
|
+
if (!res.message) delete res.message;
|
|
5802
|
+
if (result.resource) res.resource = result.resource;
|
|
5803
|
+
return res;
|
|
5804
|
+
}) : [];
|
|
4121
5805
|
const options = { tags: control.tags ?? {} };
|
|
4122
5806
|
if (control.source_location) options.sourceLocation = control.source_location;
|
|
4123
5807
|
const req = createRequirement(control.id, control.title, descriptions, control.impact, results, options);
|
|
5808
|
+
if (!req.title) delete req.title;
|
|
5809
|
+
if (control.code) req.code = control.code;
|
|
4124
5810
|
const nistTagsRaw = control.tags?.["nist"];
|
|
4125
5811
|
const controlType = deriveControlTypeFromTags(Array.isArray(nistTagsRaw) ? nistTagsRaw.filter((t) => typeof t === "string") : []);
|
|
4126
5812
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -4133,23 +5819,34 @@ async function convertSplunkToHdf(input) {
|
|
|
4133
5819
|
if (profile.version) baselineOptions.version = profile.version;
|
|
4134
5820
|
if (profile.summary) baselineOptions.summary = profile.summary;
|
|
4135
5821
|
if (groups.length > 0) baselineOptions.groups = groups;
|
|
4136
|
-
|
|
5822
|
+
if (profile.sha256) baselineOptions.integrity = {
|
|
5823
|
+
algorithm: HashAlgorithm.Sha256,
|
|
5824
|
+
checksum: profile.sha256
|
|
5825
|
+
};
|
|
5826
|
+
const baseline = createMinimalBaseline(profile.name, requirements, baselineOptions);
|
|
5827
|
+
if (profile.copyright) baseline.copyright = profile.copyright;
|
|
5828
|
+
if (profile.maintainer) baseline.maintainer = profile.maintainer;
|
|
5829
|
+
if (profile.license) baseline.license = profile.license;
|
|
5830
|
+
allBaselines.push(baseline);
|
|
4137
5831
|
}
|
|
4138
5832
|
}
|
|
4139
|
-
return
|
|
5833
|
+
return buildHdfResults({
|
|
5834
|
+
generatorName: "splunk-to-hdf",
|
|
5835
|
+
converterVersion,
|
|
5836
|
+
toolName: "Splunk",
|
|
4140
5837
|
baselines: allBaselines,
|
|
4141
|
-
components
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
osName: targetRelease || void 0,
|
|
4145
|
-
labels: {}
|
|
4146
|
-
}],
|
|
4147
|
-
generator: {
|
|
4148
|
-
name: "splunk-to-hdf",
|
|
4149
|
-
version: "1.0.0"
|
|
4150
|
-
}
|
|
5838
|
+
components,
|
|
5839
|
+
statistics,
|
|
5840
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
4151
5841
|
});
|
|
4152
5842
|
}
|
|
5843
|
+
/** Extract the assessment duration from a Splunk header's statistics blob. */
|
|
5844
|
+
function convertStatistics(stats) {
|
|
5845
|
+
if (!stats || typeof stats !== "object") return void 0;
|
|
5846
|
+
const out = {};
|
|
5847
|
+
if (typeof stats["duration"] === "number") out.duration = stats["duration"];
|
|
5848
|
+
return out;
|
|
5849
|
+
}
|
|
4153
5850
|
//#endregion
|
|
4154
5851
|
//#region converters/hdf-to-xml/typescript/converter.ts
|
|
4155
5852
|
/**
|
|
@@ -4159,7 +5856,7 @@ async function convertSplunkToHdf(input) {
|
|
|
4159
5856
|
*/
|
|
4160
5857
|
function convertHdfToXml(input) {
|
|
4161
5858
|
validateInputSize(input, "hdf-to-xml");
|
|
4162
|
-
const hdf =
|
|
5859
|
+
const hdf = parseHdf(input);
|
|
4163
5860
|
if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
4164
5861
|
if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
4165
5862
|
return buildXml({ HdfResults: transformHdfToXmlObject(hdf) });
|
|
@@ -4171,6 +5868,20 @@ function wrap$1(value) {
|
|
|
4171
5868
|
return { "#text": value };
|
|
4172
5869
|
}
|
|
4173
5870
|
/**
|
|
5871
|
+
* Transform a polymorphic Reference ({url} | {uri} | {ref: string}) into an
|
|
5872
|
+
* XML-friendly object. Array-form `ref` values are skipped.
|
|
5873
|
+
*/
|
|
5874
|
+
function transformReference(r) {
|
|
5875
|
+
const out = {};
|
|
5876
|
+
if (r && typeof r === "object") {
|
|
5877
|
+
const o = r;
|
|
5878
|
+
if (typeof o.url === "string") out.url = wrap$1(o.url);
|
|
5879
|
+
else if (typeof o.uri === "string") out.uri = wrap$1(o.uri);
|
|
5880
|
+
else if (typeof o.ref === "string") out.ref = wrap$1(o.ref);
|
|
5881
|
+
}
|
|
5882
|
+
return out;
|
|
5883
|
+
}
|
|
5884
|
+
/**
|
|
4174
5885
|
* Transform HDF object to XML-compatible structure
|
|
4175
5886
|
* Converts arrays to repeated singular elements
|
|
4176
5887
|
*/
|
|
@@ -4180,6 +5891,16 @@ function transformHdfToXmlObject(hdf) {
|
|
|
4180
5891
|
name: wrap$1(baseline.name),
|
|
4181
5892
|
...baseline.version && { version: wrap$1(baseline.version) },
|
|
4182
5893
|
...baseline.title && { title: wrap$1(baseline.title) },
|
|
5894
|
+
...baseline.summary && { summary: wrap$1(baseline.summary) },
|
|
5895
|
+
...baseline.status && { status: wrap$1(baseline.status) },
|
|
5896
|
+
...baseline.resultsChecksum && { resultsChecksum: {
|
|
5897
|
+
algorithm: wrap$1(baseline.resultsChecksum.algorithm),
|
|
5898
|
+
value: wrap$1(baseline.resultsChecksum.value)
|
|
5899
|
+
} },
|
|
5900
|
+
...baseline.originalChecksum && { originalChecksum: {
|
|
5901
|
+
algorithm: wrap$1(baseline.originalChecksum.algorithm),
|
|
5902
|
+
value: wrap$1(baseline.originalChecksum.value)
|
|
5903
|
+
} },
|
|
4183
5904
|
...baseline.integrity && { integrity: {
|
|
4184
5905
|
...baseline.integrity.algorithm && { algorithm: wrap$1(baseline.integrity.algorithm) },
|
|
4185
5906
|
...baseline.integrity.checksum && { checksum: wrap$1(baseline.integrity.checksum) }
|
|
@@ -4189,11 +5910,13 @@ function transformHdfToXmlObject(hdf) {
|
|
|
4189
5910
|
})) };
|
|
4190
5911
|
else result.baselines = {};
|
|
4191
5912
|
if (hdf.components && hdf.components.length > 0) result.components = { target: hdf.components.map((target) => ({
|
|
5913
|
+
...target.componentId && { componentId: wrap$1(target.componentId) },
|
|
4192
5914
|
name: wrap$1(target.name),
|
|
4193
5915
|
type: wrap$1(target.type),
|
|
5916
|
+
...target.hostname && { hostname: wrap$1(target.hostname) },
|
|
4194
5917
|
...target.fqdn && { fqdn: wrap$1(target.fqdn) },
|
|
4195
|
-
...target.
|
|
4196
|
-
...target.
|
|
5918
|
+
...target.domain && { domain: wrap$1(target.domain) },
|
|
5919
|
+
...target.ipAddress && { ipAddress: wrap$1(target.ipAddress) }
|
|
4197
5920
|
})) };
|
|
4198
5921
|
if (hdf.statistics) {
|
|
4199
5922
|
result.statistics = {};
|
|
@@ -4201,7 +5924,10 @@ function transformHdfToXmlObject(hdf) {
|
|
|
4201
5924
|
if (hdf.statistics.requirements) result.statistics.requirements = hdf.statistics.requirements;
|
|
4202
5925
|
}
|
|
4203
5926
|
if (hdf.timestamp) result.timestamp = wrap$1(typeof hdf.timestamp === "string" ? hdf.timestamp : hdf.timestamp.toISOString());
|
|
4204
|
-
if (hdf.generator) result.generator =
|
|
5927
|
+
if (hdf.generator) result.generator = {
|
|
5928
|
+
name: wrap$1(hdf.generator.name),
|
|
5929
|
+
...hdf.generator.version && { version: wrap$1(hdf.generator.version) }
|
|
5930
|
+
};
|
|
4205
5931
|
return result;
|
|
4206
5932
|
}
|
|
4207
5933
|
/**
|
|
@@ -4215,11 +5941,20 @@ function transformRequirement(req) {
|
|
|
4215
5941
|
label: wrap$1(d.label),
|
|
4216
5942
|
data: wrap$1(d.data)
|
|
4217
5943
|
})) } },
|
|
5944
|
+
...req.code && { code: wrap$1(req.code) },
|
|
5945
|
+
...req.sourceLocation && { sourceLocation: {
|
|
5946
|
+
...req.sourceLocation.ref !== void 0 && { ref: wrap$1(req.sourceLocation.ref) },
|
|
5947
|
+
...req.sourceLocation.line !== void 0 && { line: wrap$1(req.sourceLocation.line) }
|
|
5948
|
+
} },
|
|
5949
|
+
...req.controlType && { controlType: wrap$1(req.controlType) },
|
|
5950
|
+
...req.verificationMethod && { verificationMethod: wrap$1(req.verificationMethod) },
|
|
5951
|
+
...req.applicability && { applicability: wrap$1(req.applicability) },
|
|
5952
|
+
...req.refs && req.refs.length > 0 && { refs: { ref: req.refs.map(transformReference) } },
|
|
4218
5953
|
impact: wrap$1(req.impact)
|
|
4219
5954
|
};
|
|
4220
5955
|
if (req.tags && Object.keys(req.tags).length > 0) {
|
|
4221
5956
|
const transformedTags = {};
|
|
4222
|
-
for (const [key, value] of Object.entries(req.tags)) if (Array.isArray(value) && value.length > 0) transformedTags[key] = value.map((v) => wrap$1(v));
|
|
5957
|
+
for (const [key, value] of Object.entries(req.tags).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) if (Array.isArray(value) && value.length > 0) transformedTags[key] = value.map((v) => wrap$1(v));
|
|
4223
5958
|
else if (!Array.isArray(value)) transformedTags[key] = wrap$1(value);
|
|
4224
5959
|
if (Object.keys(transformedTags).length > 0) result.tags = transformedTags;
|
|
4225
5960
|
}
|
|
@@ -4235,7 +5970,7 @@ function transformRequirement(req) {
|
|
|
4235
5970
|
//#endregion
|
|
4236
5971
|
//#region converters/gitlab-to-hdf/typescript/converter.ts
|
|
4237
5972
|
function gitlabSeverityToImpact(severity) {
|
|
4238
|
-
return severityToImpact(severity.toLowerCase());
|
|
5973
|
+
return severityToImpact$1(severity.toLowerCase());
|
|
4239
5974
|
}
|
|
4240
5975
|
function scanTypeToTargetType(scanType) {
|
|
4241
5976
|
switch (scanType) {
|
|
@@ -4325,7 +6060,7 @@ function buildCodeDesc$4(scanType, location) {
|
|
|
4325
6060
|
default: return `Location: ${JSON.stringify(location)}`;
|
|
4326
6061
|
}
|
|
4327
6062
|
}
|
|
4328
|
-
async function convertGitlabToHdf(input) {
|
|
6063
|
+
async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
4329
6064
|
validateInputSize(input, "gitlab");
|
|
4330
6065
|
const resultsChecksum = await inputChecksum(input);
|
|
4331
6066
|
const report = parseJSON(input);
|
|
@@ -4400,7 +6135,7 @@ async function convertGitlabToHdf(input) {
|
|
|
4400
6135
|
components,
|
|
4401
6136
|
generator: {
|
|
4402
6137
|
name: "gitlab-to-hdf",
|
|
4403
|
-
version:
|
|
6138
|
+
version: converterVersion
|
|
4404
6139
|
},
|
|
4405
6140
|
tool
|
|
4406
6141
|
};
|
|
@@ -4453,6 +6188,18 @@ function groupFindings(findings) {
|
|
|
4453
6188
|
return groups;
|
|
4454
6189
|
}
|
|
4455
6190
|
/**
|
|
6191
|
+
* Order ExtraData keys the way Go's map marshalling does (lexicographic), so the
|
|
6192
|
+
* embedded message string is byte-identical in both languages.
|
|
6193
|
+
*/
|
|
6194
|
+
function canonicalize(value) {
|
|
6195
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
6196
|
+
if (value !== null && typeof value === "object") {
|
|
6197
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
6198
|
+
return Object.fromEntries(entries.map(([k, v]) => [k, canonicalize(v)]));
|
|
6199
|
+
}
|
|
6200
|
+
return value;
|
|
6201
|
+
}
|
|
6202
|
+
/**
|
|
4456
6203
|
* Build the Result.Message JSON from selected finding fields.
|
|
4457
6204
|
*/
|
|
4458
6205
|
function buildMessage(f) {
|
|
@@ -4461,7 +6208,7 @@ function buildMessage(f) {
|
|
|
4461
6208
|
Redacted: f.Redacted
|
|
4462
6209
|
};
|
|
4463
6210
|
if (f.VerificationError) msg.VerificationError = f.VerificationError;
|
|
4464
|
-
if (f.ExtraData && Object.keys(f.ExtraData).length > 0) msg.ExtraData = f.ExtraData;
|
|
6211
|
+
if (f.ExtraData && Object.keys(f.ExtraData).length > 0) msg.ExtraData = canonicalize(f.ExtraData);
|
|
4465
6212
|
return JSON.stringify(msg);
|
|
4466
6213
|
}
|
|
4467
6214
|
/**
|
|
@@ -4534,7 +6281,7 @@ function buildRequirement$12(reqID, findings) {
|
|
|
4534
6281
|
* @param input - TruffleHog JSON/NDJSON string
|
|
4535
6282
|
* @returns HDF JSON string
|
|
4536
6283
|
*/
|
|
4537
|
-
async function convertTrufflehogToHdf(input) {
|
|
6284
|
+
async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
|
|
4538
6285
|
if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
|
|
4539
6286
|
validateInputSize(input, "trufflehog");
|
|
4540
6287
|
const findings = parseFindings(input);
|
|
@@ -4554,7 +6301,7 @@ async function convertTrufflehogToHdf(input) {
|
|
|
4554
6301
|
})],
|
|
4555
6302
|
generator: {
|
|
4556
6303
|
name: "trufflehog-to-hdf",
|
|
4557
|
-
version:
|
|
6304
|
+
version: converterVersion
|
|
4558
6305
|
},
|
|
4559
6306
|
tool: {
|
|
4560
6307
|
name: "TruffleHog",
|
|
@@ -4605,7 +6352,7 @@ function formatCodeDesc$3(hostIP, hostURL, location, issueDetail, confidence) {
|
|
|
4605
6352
|
* @param input - BurpSuite XML string
|
|
4606
6353
|
* @returns HDF JSON string
|
|
4607
6354
|
*/
|
|
4608
|
-
async function convertBurpsuiteToHdf(input) {
|
|
6355
|
+
async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
4609
6356
|
if (!input || input.trim().length === 0) throw new Error("burpsuite: empty input");
|
|
4610
6357
|
validateInputSize(input, "burpsuite");
|
|
4611
6358
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -4645,7 +6392,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
4645
6392
|
}],
|
|
4646
6393
|
generator: {
|
|
4647
6394
|
name: "burpsuite-to-hdf",
|
|
4648
|
-
version:
|
|
6395
|
+
version: converterVersion
|
|
4649
6396
|
},
|
|
4650
6397
|
tool
|
|
4651
6398
|
};
|
|
@@ -4706,6 +6453,26 @@ const IMPACT_MAPPING$4 = {
|
|
|
4706
6453
|
low: .3,
|
|
4707
6454
|
informational: 0
|
|
4708
6455
|
};
|
|
6456
|
+
const NAMED_ENTITIES$2 = {
|
|
6457
|
+
amp: "&",
|
|
6458
|
+
lt: "<",
|
|
6459
|
+
gt: ">",
|
|
6460
|
+
quot: "\"",
|
|
6461
|
+
apos: "'"
|
|
6462
|
+
};
|
|
6463
|
+
/**
|
|
6464
|
+
* Decodes XML character references. The shared parser runs with
|
|
6465
|
+
* processEntities disabled to block entity-expansion attacks, which also leaves
|
|
6466
|
+
* the five predefined references undecoded — Go's encoding/xml decodes them, so
|
|
6467
|
+
* the raw text would otherwise differ between the two converters.
|
|
6468
|
+
*/
|
|
6469
|
+
function decodeXmlEntities$2(s) {
|
|
6470
|
+
return s.replace(/&(#\d+|#x[0-9a-fA-F]+|amp|lt|gt|quot|apos);/g, (match, ref) => {
|
|
6471
|
+
if (ref.startsWith("#x") || ref.startsWith("#X")) return String.fromCodePoint(parseInt(ref.slice(2), 16));
|
|
6472
|
+
if (ref.startsWith("#")) return String.fromCodePoint(parseInt(ref.slice(1), 10));
|
|
6473
|
+
return NAMED_ENTITIES$2[ref] ?? match;
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
4709
6476
|
/**
|
|
4710
6477
|
* Maps metadata column names to row values by position index.
|
|
4711
6478
|
* Mirrors the heimdall2 compileFindings function.
|
|
@@ -4720,7 +6487,7 @@ function compileFindings(parsed) {
|
|
|
4720
6487
|
colNames.forEach((name, i) => {
|
|
4721
6488
|
const val = values[i];
|
|
4722
6489
|
if (val === null || val === void 0 || typeof val === "object") finding[name] = "";
|
|
4723
|
-
else finding[name] = String(val);
|
|
6490
|
+
else finding[name] = decodeXmlEntities$2(String(val));
|
|
4724
6491
|
});
|
|
4725
6492
|
return finding;
|
|
4726
6493
|
});
|
|
@@ -4795,10 +6562,11 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
4795
6562
|
data: formatDesc$1(rep)
|
|
4796
6563
|
}];
|
|
4797
6564
|
const results = findings.map((f) => {
|
|
4798
|
-
return
|
|
6565
|
+
return {
|
|
6566
|
+
status: hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed,
|
|
4799
6567
|
codeDesc: f["Details"] ?? "",
|
|
4800
6568
|
startTime: parseDate(f["Date"] ?? "")
|
|
4801
|
-
}
|
|
6569
|
+
};
|
|
4802
6570
|
});
|
|
4803
6571
|
const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
|
|
4804
6572
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -4814,7 +6582,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
4814
6582
|
* @param input - DBProtect XML string
|
|
4815
6583
|
* @returns HDF JSON string
|
|
4816
6584
|
*/
|
|
4817
|
-
async function convertDbprotectToHdf(input) {
|
|
6585
|
+
async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
|
|
4818
6586
|
if (!input || input.trim().length === 0) throw new Error("dbprotect: empty input");
|
|
4819
6587
|
validateInputSize(input, "dbprotect");
|
|
4820
6588
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -4842,16 +6610,19 @@ async function convertDbprotectToHdf(input) {
|
|
|
4842
6610
|
const title = firstFinding["Job Name"] ?? "";
|
|
4843
6611
|
const summary = formatSummary(firstFinding);
|
|
4844
6612
|
const targetName = firstFinding["Asset"] ?? "";
|
|
6613
|
+
const baseline = createMinimalBaseline("DBProtect Scan", requirements, {
|
|
6614
|
+
resultsChecksum,
|
|
6615
|
+
title,
|
|
6616
|
+
summary
|
|
6617
|
+
});
|
|
6618
|
+
const policy = firstFinding["Policy"] ?? "";
|
|
6619
|
+
if (policy) baseline.version = policy;
|
|
4845
6620
|
return buildHdfResults({
|
|
4846
6621
|
generatorName: "dbprotect-to-hdf",
|
|
4847
|
-
converterVersion
|
|
6622
|
+
converterVersion,
|
|
4848
6623
|
toolName: "DBProtect",
|
|
4849
6624
|
toolFormat: "XML",
|
|
4850
|
-
baselines: [
|
|
4851
|
-
resultsChecksum,
|
|
4852
|
-
title,
|
|
4853
|
-
summary
|
|
4854
|
-
})],
|
|
6625
|
+
baselines: [baseline],
|
|
4855
6626
|
components: [{
|
|
4856
6627
|
name: targetName,
|
|
4857
6628
|
type: TargetType.Host
|
|
@@ -5040,7 +6811,7 @@ function buildPackageTypeIndex(pkgs) {
|
|
|
5040
6811
|
*/
|
|
5041
6812
|
function formatCodeDesc$2(vuln) {
|
|
5042
6813
|
const packageName = vuln.packageName ?? "N/A";
|
|
5043
|
-
const impactedVersions = vuln.impactedVersions && vuln.impactedVersions.length > 0 ?
|
|
6814
|
+
const impactedVersions = vuln.impactedVersions && vuln.impactedVersions.length > 0 ? `[${vuln.impactedVersions.join(" ")}]` : "N/A";
|
|
5044
6815
|
return `Package ${JSON.stringify(packageName)} should be updated to latest version above impacted versions ${impactedVersions}`;
|
|
5045
6816
|
}
|
|
5046
6817
|
/**
|
|
@@ -5061,10 +6832,11 @@ function buildRequirement$9(vuln, packageTypes, distro) {
|
|
|
5061
6832
|
data: vuln.description
|
|
5062
6833
|
}];
|
|
5063
6834
|
const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
5064
|
-
const results = [
|
|
6835
|
+
const results = [{
|
|
6836
|
+
status: ResultStatus.Failed,
|
|
5065
6837
|
codeDesc: formatCodeDesc$2(vuln),
|
|
5066
6838
|
startTime
|
|
5067
|
-
}
|
|
6839
|
+
}];
|
|
5068
6840
|
const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
|
|
5069
6841
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5070
6842
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -5106,7 +6878,7 @@ function convertSingleResult(result, resultsChecksum) {
|
|
|
5106
6878
|
* @param input - Twistlock JSON string
|
|
5107
6879
|
* @returns HDF JSON string
|
|
5108
6880
|
*/
|
|
5109
|
-
async function convertTwistlockToHdf(input) {
|
|
6881
|
+
async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
|
|
5110
6882
|
if (!input || input.trim().length === 0) throw new Error("twistlock: empty input");
|
|
5111
6883
|
validateInputSize(input, "twistlock");
|
|
5112
6884
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5117,18 +6889,19 @@ async function convertTwistlockToHdf(input) {
|
|
|
5117
6889
|
else results = [parsed];
|
|
5118
6890
|
if (results.length === 0) throw new Error("twistlock: no scan results found");
|
|
5119
6891
|
const baselines = results.map((result) => convertSingleResult(result, resultsChecksum));
|
|
5120
|
-
const
|
|
6892
|
+
const component = {
|
|
6893
|
+
name: results[0].name ?? results[0].repository ?? "",
|
|
6894
|
+
type: TargetType.ContainerImage
|
|
6895
|
+
};
|
|
6896
|
+
const imageID = results[0].id;
|
|
6897
|
+
if (imageID) component.labels = { image: imageID };
|
|
5121
6898
|
return buildHdfResults({
|
|
5122
6899
|
generatorName: "twistlock-to-hdf",
|
|
5123
|
-
converterVersion
|
|
6900
|
+
converterVersion,
|
|
5124
6901
|
toolName: "Twistlock",
|
|
5125
6902
|
toolFormat: "JSON",
|
|
5126
6903
|
baselines,
|
|
5127
|
-
components: [
|
|
5128
|
-
name: targetName,
|
|
5129
|
-
type: TargetType.ContainerImage,
|
|
5130
|
-
labels: { image: results[0]?.id ?? targetName }
|
|
5131
|
-
}],
|
|
6904
|
+
components: [component],
|
|
5132
6905
|
timestamp: /* @__PURE__ */ new Date()
|
|
5133
6906
|
});
|
|
5134
6907
|
}
|
|
@@ -5190,10 +6963,11 @@ function buildRequirement$8(finding, timestamp) {
|
|
|
5190
6963
|
data: finding.vulnerability.recommendation
|
|
5191
6964
|
});
|
|
5192
6965
|
const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
|
|
5193
|
-
const results = [
|
|
6966
|
+
const results = [{
|
|
6967
|
+
status: ResultStatus.Failed,
|
|
5194
6968
|
codeDesc,
|
|
5195
6969
|
startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
5196
|
-
}
|
|
6970
|
+
}];
|
|
5197
6971
|
const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
|
|
5198
6972
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5199
6973
|
const controlType = deriveControlTypeFromTags(nist);
|
|
@@ -5227,7 +7001,7 @@ function buildAffectedPackageFromComponent(c) {
|
|
|
5227
7001
|
* @param input - Dependency-Track FPF JSON string
|
|
5228
7002
|
* @returns HDF JSON string
|
|
5229
7003
|
*/
|
|
5230
|
-
async function convertDeptrackToHdf(input) {
|
|
7004
|
+
async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
|
|
5231
7005
|
if (!input || input.trim().length === 0) throw new Error("deptrack: empty input");
|
|
5232
7006
|
validateInputSize(input, "deptrack");
|
|
5233
7007
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5247,7 +7021,7 @@ async function convertDeptrackToHdf(input) {
|
|
|
5247
7021
|
const targetName = parsed.project?.name ?? parsed.project?.uuid ?? "";
|
|
5248
7022
|
return buildHdfResults({
|
|
5249
7023
|
generatorName: "deptrack-to-hdf",
|
|
5250
|
-
converterVersion
|
|
7024
|
+
converterVersion,
|
|
5251
7025
|
toolName: "Dependency-Track",
|
|
5252
7026
|
toolFormat: "JSON",
|
|
5253
7027
|
baselines: [baseline],
|
|
@@ -5326,11 +7100,12 @@ function buildRequirement$7(entryID, entries, scanTime) {
|
|
|
5326
7100
|
label: "default",
|
|
5327
7101
|
data: formatDescription(rep)
|
|
5328
7102
|
}];
|
|
5329
|
-
const results = entries.map((entry) =>
|
|
7103
|
+
const results = entries.map((entry) => ({
|
|
7104
|
+
status: ResultStatus.Failed,
|
|
5330
7105
|
codeDesc: formatCodeDesc$1(entry),
|
|
5331
7106
|
startTime: scanTime
|
|
5332
7107
|
}));
|
|
5333
|
-
const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
|
|
7108
|
+
const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact$1(rep.severity), results, { tags });
|
|
5334
7109
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5335
7110
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5336
7111
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -5383,7 +7158,7 @@ function buildAffectedPackageFromEntry(entry) {
|
|
|
5383
7158
|
* @param input - JFrog Xray JSON string
|
|
5384
7159
|
* @returns HDF JSON string
|
|
5385
7160
|
*/
|
|
5386
|
-
async function convertJfrogXrayToHdf(input) {
|
|
7161
|
+
async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
|
|
5387
7162
|
if (!input || input.trim().length === 0) throw new Error("jfrog-xray: empty input");
|
|
5388
7163
|
validateInputSize(input, "jfrog-xray");
|
|
5389
7164
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5410,7 +7185,7 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
5410
7185
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
|
|
5411
7186
|
return buildHdfResults({
|
|
5412
7187
|
generatorName: "jfrog-xray-to-hdf",
|
|
5413
|
-
converterVersion
|
|
7188
|
+
converterVersion,
|
|
5414
7189
|
toolName: "JFrog Xray",
|
|
5415
7190
|
toolFormat: "JSON",
|
|
5416
7191
|
baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
|
|
@@ -5519,7 +7294,7 @@ function targetNameFromReport(report) {
|
|
|
5519
7294
|
* @param input - NeuVector scan JSON string
|
|
5520
7295
|
* @returns HDF JSON string
|
|
5521
7296
|
*/
|
|
5522
|
-
async function convertNeuvectorToHdf(input) {
|
|
7297
|
+
async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
|
|
5523
7298
|
if (!input || input.trim().length === 0) throw new Error("neuvector: empty input");
|
|
5524
7299
|
validateInputSize(input, "neuvector");
|
|
5525
7300
|
const scan = parseJSON(input);
|
|
@@ -5541,7 +7316,7 @@ async function convertNeuvectorToHdf(input) {
|
|
|
5541
7316
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
|
|
5542
7317
|
return buildHdfResults({
|
|
5543
7318
|
generatorName: "neuvector-to-hdf",
|
|
5544
|
-
converterVersion
|
|
7319
|
+
converterVersion,
|
|
5545
7320
|
toolName: "NeuVector",
|
|
5546
7321
|
toolFormat: "JSON",
|
|
5547
7322
|
baselines: [createMinimalBaseline("NeuVector Scan", requirements, {
|
|
@@ -5563,6 +7338,23 @@ async function convertNeuvectorToHdf(input) {
|
|
|
5563
7338
|
//#region converters/fortify-to-hdf/typescript/converter.ts
|
|
5564
7339
|
const NIST_REFERENCE_NAME = "Standards Mapping - NIST Special Publication 800-53 Revision 4";
|
|
5565
7340
|
const NIST_PATTERN = /[a-zA-Z]{2}-\d+/g;
|
|
7341
|
+
const NAMED_ENTITIES$1 = {
|
|
7342
|
+
lt: "<",
|
|
7343
|
+
gt: ">",
|
|
7344
|
+
quot: "\"",
|
|
7345
|
+
apos: "'",
|
|
7346
|
+
amp: "&"
|
|
7347
|
+
};
|
|
7348
|
+
function decodeXmlEntities$1(s) {
|
|
7349
|
+
return s.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|(lt|gt|quot|apos|amp));/g, (match, dec, hex, name) => {
|
|
7350
|
+
if (dec !== void 0) return String.fromCodePoint(Number.parseInt(dec, 10));
|
|
7351
|
+
if (hex !== void 0) return String.fromCodePoint(Number.parseInt(hex, 16));
|
|
7352
|
+
return name !== void 0 ? NAMED_ENTITIES$1[name] ?? match : match;
|
|
7353
|
+
});
|
|
7354
|
+
}
|
|
7355
|
+
function stripFvdlMarkup(s) {
|
|
7356
|
+
return stripHTML(decodeXmlEntities$1(s));
|
|
7357
|
+
}
|
|
5566
7358
|
function buildSnippetMap(snippets) {
|
|
5567
7359
|
const map = /* @__PURE__ */ new Map();
|
|
5568
7360
|
for (const s of snippets) if (s.id) map.set(s.id, s);
|
|
@@ -5612,8 +7404,8 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
5612
7404
|
if (nistTags.length === 0) nistTags = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1];
|
|
5613
7405
|
const cciTags = nistToCci(nistTags);
|
|
5614
7406
|
const tags = buildNistCciTags(nistTags, cciTags);
|
|
5615
|
-
const title =
|
|
5616
|
-
let explanationText =
|
|
7407
|
+
const title = stripFvdlMarkup(desc.Abstract ?? "");
|
|
7408
|
+
let explanationText = stripFvdlMarkup(desc.Explanation ?? "");
|
|
5617
7409
|
if (!explanationText) explanationText = title;
|
|
5618
7410
|
const descriptions = [{
|
|
5619
7411
|
label: "default",
|
|
@@ -5621,7 +7413,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
5621
7413
|
}];
|
|
5622
7414
|
if (desc.Recommendations) descriptions.push({
|
|
5623
7415
|
label: "fix",
|
|
5624
|
-
data:
|
|
7416
|
+
data: stripFvdlMarkup(desc.Recommendations)
|
|
5625
7417
|
});
|
|
5626
7418
|
let impact = 0;
|
|
5627
7419
|
if (vulns.length > 0) impact = parseFloat(vulns[0].ClassInfo?.DefaultSeverity ?? "0") / 5;
|
|
@@ -5656,7 +7448,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
5656
7448
|
* @param input - Fortify FVDL XML string
|
|
5657
7449
|
* @returns HDF JSON string
|
|
5658
7450
|
*/
|
|
5659
|
-
async function convertFortifyToHdf(input) {
|
|
7451
|
+
async function convertFortifyToHdf(input, converterVersion = "1.0.0") {
|
|
5660
7452
|
if (!input || input.trim().length === 0) throw new Error("fortify: empty input");
|
|
5661
7453
|
validateInputSize(input, "fortify");
|
|
5662
7454
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5690,7 +7482,7 @@ async function convertFortifyToHdf(input) {
|
|
|
5690
7482
|
}],
|
|
5691
7483
|
generator: {
|
|
5692
7484
|
name: "fortify-to-hdf",
|
|
5693
|
-
version:
|
|
7485
|
+
version: converterVersion
|
|
5694
7486
|
},
|
|
5695
7487
|
tool: {
|
|
5696
7488
|
name: "Fortify",
|
|
@@ -5721,16 +7513,12 @@ const REQUIRED_COLUMNS = [
|
|
|
5721
7513
|
* Prisma uses non-standard severity names: "important" and "moderate"
|
|
5722
7514
|
* in addition to the standard critical/high/medium/low.
|
|
5723
7515
|
*/
|
|
7516
|
+
const PRISMA_SEVERITY_ALIASES = {
|
|
7517
|
+
important: .9,
|
|
7518
|
+
moderate: .5
|
|
7519
|
+
};
|
|
5724
7520
|
function getImpact$3(severity) {
|
|
5725
|
-
|
|
5726
|
-
case "critical": return 1;
|
|
5727
|
-
case "important": return .9;
|
|
5728
|
-
case "high": return .7;
|
|
5729
|
-
case "moderate":
|
|
5730
|
-
case "medium": return .5;
|
|
5731
|
-
case "low": return .3;
|
|
5732
|
-
default: return .5;
|
|
5733
|
-
}
|
|
7521
|
+
return PRISMA_SEVERITY_ALIASES[severity.toLowerCase()] ?? severityToImpact(severity);
|
|
5734
7522
|
}
|
|
5735
7523
|
/**
|
|
5736
7524
|
* Returns NIST tags based on whether the finding has a CVE.
|
|
@@ -5875,7 +7663,7 @@ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
|
|
|
5875
7663
|
* @param input - Prisma Cloud CSV string
|
|
5876
7664
|
* @returns HDF JSON string
|
|
5877
7665
|
*/
|
|
5878
|
-
async function convertPrismaToHdf(input) {
|
|
7666
|
+
async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
|
|
5879
7667
|
if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
|
|
5880
7668
|
validateInputSize(input, "prisma");
|
|
5881
7669
|
const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
|
|
@@ -5901,7 +7689,7 @@ async function convertPrismaToHdf(input) {
|
|
|
5901
7689
|
}
|
|
5902
7690
|
return buildHdfResults({
|
|
5903
7691
|
generatorName: "prisma-to-hdf",
|
|
5904
|
-
converterVersion
|
|
7692
|
+
converterVersion,
|
|
5905
7693
|
toolName: "Prisma Cloud",
|
|
5906
7694
|
toolFormat: "CSV",
|
|
5907
7695
|
baselines,
|
|
@@ -6040,7 +7828,7 @@ function buildRequirement$3(vuln, initiated) {
|
|
|
6040
7828
|
* @param input - Netsparker/Invicti XML string
|
|
6041
7829
|
* @returns HDF JSON string
|
|
6042
7830
|
*/
|
|
6043
|
-
async function convertNetsparkerToHdf(input) {
|
|
7831
|
+
async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
6044
7832
|
if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
|
|
6045
7833
|
validateInputSize(input, "netsparker");
|
|
6046
7834
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -6063,7 +7851,7 @@ async function convertNetsparkerToHdf(input) {
|
|
|
6063
7851
|
}
|
|
6064
7852
|
return buildHdfResults({
|
|
6065
7853
|
generatorName: "netsparker-to-hdf",
|
|
6066
|
-
converterVersion
|
|
7854
|
+
converterVersion,
|
|
6067
7855
|
toolName,
|
|
6068
7856
|
toolFormat: "XML",
|
|
6069
7857
|
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
@@ -6180,7 +7968,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
|
|
|
6180
7968
|
* @param input - ScoutSuite JS/JSON string
|
|
6181
7969
|
* @returns HDF JSON string
|
|
6182
7970
|
*/
|
|
6183
|
-
async function convertScoutsuiteToHdf(input) {
|
|
7971
|
+
async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
6184
7972
|
if (!input || input.trim().length === 0) throw new Error("scoutsuite: empty input");
|
|
6185
7973
|
validateInputSize(input, "scoutsuite");
|
|
6186
7974
|
const jsonStr = stripJSPrefix(input);
|
|
@@ -6197,7 +7985,7 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
6197
7985
|
});
|
|
6198
7986
|
return buildHdfResults({
|
|
6199
7987
|
generatorName: "scoutsuite-to-hdf",
|
|
6200
|
-
converterVersion
|
|
7988
|
+
converterVersion,
|
|
6201
7989
|
toolName: "ScoutSuite",
|
|
6202
7990
|
toolFormat: "JSON",
|
|
6203
7991
|
toolVersion: report.last_run.version,
|
|
@@ -6310,17 +8098,12 @@ function buildRequirementFromResult(result, filename) {
|
|
|
6310
8098
|
const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
6311
8099
|
const score = result.result.score;
|
|
6312
8100
|
const status = determineStatus(score);
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
codeDesc: buildCodeDesc$1(section, scannerName),
|
|
6317
|
-
startTime
|
|
6318
|
-
});
|
|
6319
|
-
});
|
|
6320
|
-
else results = [createResult(status, void 0, {
|
|
6321
|
-
codeDesc: `No sections reported by ${scannerName}`,
|
|
8101
|
+
const toResult = (codeDesc) => ({
|
|
8102
|
+
status,
|
|
8103
|
+
codeDesc,
|
|
6322
8104
|
startTime
|
|
6323
|
-
})
|
|
8105
|
+
});
|
|
8106
|
+
const results = result.result.sections.length > 0 ? result.result.sections.map((section) => toResult(buildCodeDesc$1(section, scannerName))) : [toResult(`No sections reported by ${scannerName}`)];
|
|
6324
8107
|
const req = createRequirement(result.sha256, filename, descriptions, scoreToImpact(score), results, { tags });
|
|
6325
8108
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6326
8109
|
const controlType = deriveControlTypeFromTags([...nist]);
|
|
@@ -6345,7 +8128,7 @@ function buildScannerBaseline(scannerName, results, shaMap, resultsChecksum) {
|
|
|
6345
8128
|
* @param input - Conveyor JSON string
|
|
6346
8129
|
* @returns HDF JSON string
|
|
6347
8130
|
*/
|
|
6348
|
-
async function convertConveyorToHdf(input) {
|
|
8131
|
+
async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
|
|
6349
8132
|
if (!input || input.trim().length === 0) throw new Error("conveyor: empty input");
|
|
6350
8133
|
validateInputSize(input, "conveyor");
|
|
6351
8134
|
const data = parseJSON(input);
|
|
@@ -6367,7 +8150,7 @@ async function convertConveyorToHdf(input) {
|
|
|
6367
8150
|
}));
|
|
6368
8151
|
return buildHdfResults({
|
|
6369
8152
|
generatorName: "conveyor-to-hdf",
|
|
6370
|
-
converterVersion
|
|
8153
|
+
converterVersion,
|
|
6371
8154
|
toolName: "Conveyor",
|
|
6372
8155
|
toolFormat: "JSON",
|
|
6373
8156
|
baselines,
|
|
@@ -6406,18 +8189,29 @@ const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
|
|
|
6406
8189
|
function veracodeSeverityToImpact(severity) {
|
|
6407
8190
|
return IMPACT_MAPPING$2.get(severity) ?? .1;
|
|
6408
8191
|
}
|
|
6409
|
-
/** Get an attribute from a parsed XML node. */
|
|
8192
|
+
/** Get an attribute from a parsed XML node, entity-decoded. */
|
|
6410
8193
|
function attr(node, name) {
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
8194
|
+
const raw = node[`${A}${name}`];
|
|
8195
|
+
return raw === void 0 ? "" : decodeXmlEntities(raw);
|
|
8196
|
+
}
|
|
8197
|
+
const NAMED_ENTITIES = {
|
|
8198
|
+
lt: "<",
|
|
8199
|
+
gt: ">",
|
|
8200
|
+
quot: "\"",
|
|
8201
|
+
apos: "'",
|
|
8202
|
+
amp: "&"
|
|
8203
|
+
};
|
|
6414
8204
|
function decodeXmlEntities(s) {
|
|
6415
|
-
return s.replace(
|
|
8205
|
+
return s.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|(lt|gt|quot|apos|amp));/g, (match, dec, hex, name) => {
|
|
8206
|
+
if (dec !== void 0) return String.fromCodePoint(Number.parseInt(dec, 10));
|
|
8207
|
+
if (hex !== void 0) return String.fromCodePoint(Number.parseInt(hex, 16));
|
|
8208
|
+
return name !== void 0 ? NAMED_ENTITIES[name] ?? match : match;
|
|
8209
|
+
});
|
|
6416
8210
|
}
|
|
6417
8211
|
/** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
|
|
6418
8212
|
function parseVeracodeTimestamp(ts) {
|
|
6419
8213
|
if (!ts) return void 0;
|
|
6420
|
-
return parseTimestamp(
|
|
8214
|
+
return parseTimestamp(ts.replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
|
|
6421
8215
|
}
|
|
6422
8216
|
/** Format description paragraphs into text. */
|
|
6423
8217
|
function formatDesc(desc) {
|
|
@@ -6550,12 +8344,20 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
|
6550
8344
|
const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
|
|
6551
8345
|
const results = cwes.flatMap((c) => {
|
|
6552
8346
|
const staticflaws = c.staticflaws;
|
|
6553
|
-
return ensureArray(staticflaws?.flaw).map((flaw) =>
|
|
8347
|
+
return ensureArray(staticflaws?.flaw).map((flaw) => ({
|
|
8348
|
+
status: ResultStatus.Failed,
|
|
6554
8349
|
codeDesc: formatFlawCodeDesc(flaw),
|
|
6555
8350
|
startTime
|
|
6556
8351
|
}));
|
|
6557
8352
|
});
|
|
6558
|
-
const
|
|
8353
|
+
const sourceRef = cwes.flatMap((c) => {
|
|
8354
|
+
const staticflaws = c.staticflaws;
|
|
8355
|
+
return ensureArray(staticflaws?.flaw).map((flaw) => attr(flaw, "sourcefile")).filter(Boolean);
|
|
8356
|
+
}).join("\n");
|
|
8357
|
+
const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, {
|
|
8358
|
+
tags,
|
|
8359
|
+
sourceLocation: sourceRef ? { ref: sourceRef } : void 0
|
|
8360
|
+
});
|
|
6559
8361
|
const controlType = deriveControlTypeFromTags(nist);
|
|
6560
8362
|
if (controlType !== void 0) req.controlType = controlType;
|
|
6561
8363
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -6605,16 +8407,26 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
6605
8407
|
if (cweID) extras.cwe = cweID;
|
|
6606
8408
|
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
6607
8409
|
const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
|
|
6608
|
-
const results = components.map((comp) =>
|
|
8410
|
+
const results = components.map((comp) => ({
|
|
8411
|
+
status: ResultStatus.Failed,
|
|
6609
8412
|
codeDesc: formatSCACodeDesc(comp),
|
|
6610
8413
|
startTime
|
|
6611
8414
|
}));
|
|
6612
8415
|
const cveSummary = attr(vuln, "cve_summary");
|
|
6613
8416
|
const cveId = attr(vuln, "cve_id");
|
|
6614
|
-
const
|
|
8417
|
+
const descriptions = [{
|
|
6615
8418
|
label: "default",
|
|
6616
8419
|
data: cveSummary || ""
|
|
6617
|
-
}]
|
|
8420
|
+
}];
|
|
8421
|
+
const sourceRef = components.flatMap((comp) => {
|
|
8422
|
+
const filePaths = comp.file_paths;
|
|
8423
|
+
if (!filePaths) return [];
|
|
8424
|
+
return ensureArray(filePaths.file_path).map((fp) => attr(fp, "value")).filter(Boolean);
|
|
8425
|
+
}).join("\n");
|
|
8426
|
+
const req = createRequirement(cveId, cveId, descriptions, impact, results, {
|
|
8427
|
+
tags,
|
|
8428
|
+
sourceLocation: sourceRef ? { ref: sourceRef } : void 0
|
|
8429
|
+
});
|
|
6618
8430
|
const controlType = deriveControlTypeFromTags(nist);
|
|
6619
8431
|
if (controlType !== void 0) req.controlType = controlType;
|
|
6620
8432
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -6626,10 +8438,13 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
6626
8438
|
* @param input - Veracode DetailedReport XML string
|
|
6627
8439
|
* @returns HDF JSON string
|
|
6628
8440
|
*/
|
|
6629
|
-
async function convertVeracodeToHdf(input) {
|
|
8441
|
+
async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
|
|
6630
8442
|
if (!input || input.trim().length === 0) throw new Error("veracode: empty input");
|
|
6631
8443
|
validateInputSize(input, "veracode");
|
|
6632
|
-
const parsed = parseXml(input, {
|
|
8444
|
+
const parsed = parseXml(input, {
|
|
8445
|
+
attributeNamePrefix: "@_",
|
|
8446
|
+
trimValues: false
|
|
8447
|
+
});
|
|
6633
8448
|
if ("summaryreport" in parsed) throw new Error("veracode: summary reports are not supported; use a detailed report");
|
|
6634
8449
|
const report = parsed.detailedreport;
|
|
6635
8450
|
if (!report) throw new Error("veracode: invalid XML - no <detailedreport> root element");
|
|
@@ -6658,7 +8473,7 @@ async function convertVeracodeToHdf(input) {
|
|
|
6658
8473
|
const timestamp = parseVeracodeTimestamp(firstBuildDate);
|
|
6659
8474
|
return buildHdfResults({
|
|
6660
8475
|
generatorName: "veracode-to-hdf",
|
|
6661
|
-
converterVersion
|
|
8476
|
+
converterVersion,
|
|
6662
8477
|
toolName: "Veracode",
|
|
6663
8478
|
toolFormat: "XML",
|
|
6664
8479
|
baselines: [baseline],
|
|
@@ -6734,12 +8549,11 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
6734
8549
|
data: stripHTML(impacts[0])
|
|
6735
8550
|
});
|
|
6736
8551
|
}
|
|
6737
|
-
const
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
})], { tags });
|
|
8552
|
+
const req = createRequirement(id, title, descriptions, impact, [{
|
|
8553
|
+
status,
|
|
8554
|
+
codeDesc: cs.implementationStatus || "No implementation status provided",
|
|
8555
|
+
startTime: (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
8556
|
+
}], { tags });
|
|
6743
8557
|
const controlType = deriveControlTypeFromTags(nist);
|
|
6744
8558
|
if (controlType !== void 0) req.controlType = controlType;
|
|
6745
8559
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -6754,7 +8568,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
6754
8568
|
* @param input - Combined JSON string with secureScore and profiles
|
|
6755
8569
|
* @returns HDF JSON string
|
|
6756
8570
|
*/
|
|
6757
|
-
async function convertMsftSecureScoreToHdf(input) {
|
|
8571
|
+
async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
|
|
6758
8572
|
if (!input || input.trim().length === 0) throw new Error("msft-secure-score: empty input");
|
|
6759
8573
|
validateInputSize(input, "msft-secure-score");
|
|
6760
8574
|
const combined = parseJSON(input);
|
|
@@ -6767,7 +8581,7 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
6767
8581
|
let tenantId = "";
|
|
6768
8582
|
return buildHdfResults({
|
|
6769
8583
|
generatorName: "msft-secure-score-to-hdf",
|
|
6770
|
-
converterVersion
|
|
8584
|
+
converterVersion,
|
|
6771
8585
|
toolName: "Microsoft Secure Score",
|
|
6772
8586
|
toolFormat: "JSON",
|
|
6773
8587
|
baselines: combined.secureScore.value.map((ss) => {
|
|
@@ -6775,14 +8589,17 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
6775
8589
|
const { items: limitedControlScores, truncated } = limitArray(ss.controlScores);
|
|
6776
8590
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
6777
8591
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedControlScores.length} controlScore items (original: ${ss.controlScores.length})`);
|
|
6778
|
-
|
|
8592
|
+
const requirements = limitedControlScores.map((cs) => buildRequirement$1(cs, profiles, ss.createdDateTime));
|
|
8593
|
+
const title = `Azure Secure Score report - Tenant ID: ${ss.azureTenantId} - Run ID: ${ss.id}`;
|
|
8594
|
+
return createMinimalBaseline("Microsoft Secure Score", requirements, {
|
|
6779
8595
|
resultsChecksum,
|
|
6780
|
-
title
|
|
8596
|
+
title
|
|
6781
8597
|
});
|
|
6782
8598
|
}),
|
|
6783
8599
|
components: [{
|
|
6784
8600
|
name: `Azure Tenant: ${tenantId}`,
|
|
6785
8601
|
type: TargetType.CloudAccount,
|
|
8602
|
+
provider: "azure",
|
|
6786
8603
|
labels: {
|
|
6787
8604
|
account: tenantId,
|
|
6788
8605
|
provider: "azure"
|
|
@@ -6798,12 +8615,12 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
6798
8615
|
* Delegates base conversion to the generic SARIF converter and enriches
|
|
6799
8616
|
* the output with MSDO-specific metadata.
|
|
6800
8617
|
*/
|
|
6801
|
-
async function convertMsftDefenderDevopsToHdf(input) {
|
|
8618
|
+
async function convertMsftDefenderDevopsToHdf(input, converterVersion = "1.0.0") {
|
|
6802
8619
|
validateInputSize(input, "msft-defender-devops");
|
|
6803
8620
|
const raw = parseJSON(input);
|
|
6804
8621
|
if (!raw || !Array.isArray(raw.runs)) throw new Error("Invalid MSDO SARIF structure: missing or invalid runs field");
|
|
6805
8622
|
const { components, runEnrichments } = extractEnrichments(raw);
|
|
6806
|
-
const hdfJson = await convertSarifToHdf(input);
|
|
8623
|
+
const hdfJson = await convertSarifToHdf(input, converterVersion);
|
|
6807
8624
|
const result = JSON.parse(hdfJson);
|
|
6808
8625
|
synthesizeNoFindingsPlaceholders(result);
|
|
6809
8626
|
applyEnrichments(result, components, runEnrichments);
|
|
@@ -6821,8 +8638,7 @@ function extractEnrichments(raw) {
|
|
|
6821
8638
|
const target = {
|
|
6822
8639
|
name: repoNameFromURI(vcp.repositoryUri),
|
|
6823
8640
|
type: TargetType.Repository,
|
|
6824
|
-
url: vcp.repositoryUri
|
|
6825
|
-
labels: {}
|
|
8641
|
+
url: vcp.repositoryUri
|
|
6826
8642
|
};
|
|
6827
8643
|
if (vcp.branch) target.branch = vcp.branch;
|
|
6828
8644
|
if (vcp.revisionId) target.commit = vcp.revisionId;
|
|
@@ -6949,10 +8765,14 @@ function buildRequirement(assessmentID, assessments, scanTime) {
|
|
|
6949
8765
|
function buildResultFromAssessment(a, scanTime) {
|
|
6950
8766
|
const status = mapStatus(a.properties.status.code);
|
|
6951
8767
|
const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
|
|
6952
|
-
|
|
8768
|
+
const message = a.properties.status.description || a.properties.status.cause || void 0;
|
|
8769
|
+
const result = {
|
|
8770
|
+
status,
|
|
6953
8771
|
codeDesc,
|
|
6954
8772
|
startTime: scanTime
|
|
6955
|
-
}
|
|
8773
|
+
};
|
|
8774
|
+
if (message !== void 0) result.message = message;
|
|
8775
|
+
return result;
|
|
6956
8776
|
}
|
|
6957
8777
|
/**
|
|
6958
8778
|
* Converts Microsoft Defender for Cloud assessment output to HDF format.
|
|
@@ -6960,7 +8780,7 @@ function buildResultFromAssessment(a, scanTime) {
|
|
|
6960
8780
|
* @param input - JSON string containing Azure Security Assessments API response
|
|
6961
8781
|
* @returns HDF JSON string
|
|
6962
8782
|
*/
|
|
6963
|
-
async function convertMsftDefenderCloudToHdf(input) {
|
|
8783
|
+
async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0") {
|
|
6964
8784
|
validateInputSize(input, "msft-defender-cloud");
|
|
6965
8785
|
const scanTime = /* @__PURE__ */ new Date();
|
|
6966
8786
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -6997,7 +8817,7 @@ async function convertMsftDefenderCloudToHdf(input) {
|
|
|
6997
8817
|
});
|
|
6998
8818
|
return buildHdfResults({
|
|
6999
8819
|
generatorName: "msft-defender-cloud-to-hdf",
|
|
7000
|
-
converterVersion
|
|
8820
|
+
converterVersion,
|
|
7001
8821
|
toolName: "Microsoft Defender for Cloud",
|
|
7002
8822
|
toolFormat: "JSON",
|
|
7003
8823
|
baselines: [baseline],
|
|
@@ -7019,7 +8839,7 @@ const IMPACT_MAPPING = {
|
|
|
7019
8839
|
/**
|
|
7020
8840
|
* Maps MDE severity to HDF impact value.
|
|
7021
8841
|
*/
|
|
7022
|
-
function severityToImpact$
|
|
8842
|
+
function severityToImpact$2(severity) {
|
|
7023
8843
|
return IMPACT_MAPPING[severity.toLowerCase()] ?? .5;
|
|
7024
8844
|
}
|
|
7025
8845
|
/**
|
|
@@ -7112,7 +8932,7 @@ function resolveStartTime(alert, scanTime) {
|
|
|
7112
8932
|
* Converts a single MDE alert to an HDF EvaluatedRequirement.
|
|
7113
8933
|
*/
|
|
7114
8934
|
function alertToRequirement(alert, scanTime) {
|
|
7115
|
-
const impact = severityToImpact$
|
|
8935
|
+
const impact = severityToImpact$2(alert.severity);
|
|
7116
8936
|
const status = statusToResultStatus(alert.status, alert.classification);
|
|
7117
8937
|
const codeDesc = formatEvidence(alert.evidence ?? []);
|
|
7118
8938
|
const results = [createResult(status, formatMessage(alert), {
|
|
@@ -7142,7 +8962,7 @@ function alertToRequirement(alert, scanTime) {
|
|
|
7142
8962
|
* @param input - JSON string of MDE alert response
|
|
7143
8963
|
* @returns HDF JSON string
|
|
7144
8964
|
*/
|
|
7145
|
-
async function convertMsftDefenderEndpointToHdf(input) {
|
|
8965
|
+
async function convertMsftDefenderEndpointToHdf(input, converterVersion = "1.0.0") {
|
|
7146
8966
|
validateInputSize(input, "msft-defender-endpoint");
|
|
7147
8967
|
const resultsChecksum = await inputChecksum(input);
|
|
7148
8968
|
const response = parseJSON(input);
|
|
@@ -7165,7 +8985,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
|
|
|
7165
8985
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", scanTime));
|
|
7166
8986
|
return buildHdfResults({
|
|
7167
8987
|
generatorName: "msft-defender-endpoint-to-hdf",
|
|
7168
|
-
converterVersion
|
|
8988
|
+
converterVersion,
|
|
7169
8989
|
toolName: "Microsoft Defender for Endpoint",
|
|
7170
8990
|
baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
|
|
7171
8991
|
components,
|
|
@@ -7183,7 +9003,8 @@ const BUILD_OPTIONS = {
|
|
|
7183
9003
|
ignoreAttributes: false,
|
|
7184
9004
|
format: true,
|
|
7185
9005
|
indentBy: " ",
|
|
7186
|
-
suppressEmptyNode: false
|
|
9006
|
+
suppressEmptyNode: false,
|
|
9007
|
+
suppressBooleanAttributes: false
|
|
7187
9008
|
};
|
|
7188
9009
|
/**
|
|
7189
9010
|
* Convert HDF Results JSON to XCCDF 1.2 Benchmark XML.
|
|
@@ -7193,7 +9014,7 @@ const BUILD_OPTIONS = {
|
|
|
7193
9014
|
*/
|
|
7194
9015
|
function convertHdfToXccdf(input) {
|
|
7195
9016
|
validateInputSize(input, "hdf-to-xccdf");
|
|
7196
|
-
const hdfData =
|
|
9017
|
+
const hdfData = parseHdf(input);
|
|
7197
9018
|
if (!hdfData || typeof hdfData !== "object" || !("baselines" in hdfData)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
7198
9019
|
if (!Array.isArray(hdfData.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
7199
9020
|
return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ Benchmark: buildBenchmarkObj(hdfData) }, BUILD_OPTIONS)}`;
|
|
@@ -7229,19 +9050,19 @@ function sanitizeXccdfId(id) {
|
|
|
7229
9050
|
}
|
|
7230
9051
|
/** Build the Benchmark XML object from HDF data. */
|
|
7231
9052
|
function buildBenchmarkObj(hdfData) {
|
|
9053
|
+
const empty = !hdfData.baselines || hdfData.baselines.length === 0;
|
|
7232
9054
|
const benchmark = {
|
|
7233
9055
|
[`${ATTR}xmlns`]: "http://checklists.nist.gov/xccdf/1.2",
|
|
9056
|
+
[`${ATTR}id`]: empty ? "xccdf_hdf_benchmark_exported" : sanitizeXccdfId("xccdf_hdf_benchmark_" + hdfData.baselines[0].name),
|
|
7234
9057
|
[`${ATTR}resolved`]: "1",
|
|
7235
9058
|
status: wrap("incomplete")
|
|
7236
9059
|
};
|
|
7237
|
-
if (
|
|
7238
|
-
benchmark[`${ATTR}id`] = "xccdf_hdf_benchmark_exported";
|
|
9060
|
+
if (empty) {
|
|
7239
9061
|
benchmark.title = wrap("HDF Export");
|
|
7240
9062
|
benchmark.version = wrap("1.0");
|
|
7241
9063
|
return benchmark;
|
|
7242
9064
|
}
|
|
7243
9065
|
const baseline = hdfData.baselines[0];
|
|
7244
|
-
benchmark[`${ATTR}id`] = sanitizeXccdfId("xccdf_hdf_benchmark_" + baseline.name);
|
|
7245
9066
|
benchmark.title = wrap(baseline.title ?? baseline.name);
|
|
7246
9067
|
benchmark.version = wrap(baseline.version ?? "1.0");
|
|
7247
9068
|
benchmark.Profile = {
|
|
@@ -7263,20 +9084,40 @@ function buildRuleObj(req) {
|
|
|
7263
9084
|
};
|
|
7264
9085
|
const description = findDescription(req.descriptions, "default");
|
|
7265
9086
|
if (description) rule.description = wrap(description);
|
|
9087
|
+
if (Array.isArray(req.refs) && req.refs.length > 0) {
|
|
9088
|
+
const refs = req.refs.map((r) => {
|
|
9089
|
+
const o = r;
|
|
9090
|
+
if (typeof o.url === "string") return { [`${ATTR}href`]: o.url };
|
|
9091
|
+
if (typeof o.uri === "string") return { [`${ATTR}href`]: o.uri };
|
|
9092
|
+
if (typeof o.ref === "string") return { "#text": o.ref };
|
|
9093
|
+
}).filter((x) => x !== void 0);
|
|
9094
|
+
if (refs.length > 0) rule.reference = refs;
|
|
9095
|
+
}
|
|
9096
|
+
const rationale = findDescription(req.descriptions, "rationale");
|
|
9097
|
+
if (rationale) rule.rationale = wrap(rationale);
|
|
7266
9098
|
const fixtext = findDescription(req.descriptions, "fix");
|
|
7267
9099
|
if (fixtext) rule.fixtext = wrap(fixtext);
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
9100
|
+
const idents = [];
|
|
9101
|
+
if (req.tags && Array.isArray(req.tags["cci"])) for (const cci of req.tags["cci"]) idents.push({
|
|
9102
|
+
[`${ATTR}system`]: "http://cyber.mil/cci",
|
|
9103
|
+
"#text": cci
|
|
9104
|
+
});
|
|
9105
|
+
if (req.tags && Array.isArray(req.tags["nist"])) for (const n of req.tags["nist"]) idents.push({
|
|
9106
|
+
[`${ATTR}system`]: "https://csrc.nist.gov/projects/risk-management/sp800-53-controls",
|
|
9107
|
+
"#text": n
|
|
9108
|
+
});
|
|
9109
|
+
if (idents.length > 0) rule.ident = idents;
|
|
9110
|
+
const checks = [];
|
|
7275
9111
|
const checkContent = findDescription(req.descriptions, "check");
|
|
7276
|
-
if (checkContent)
|
|
9112
|
+
if (checkContent) checks.push({
|
|
7277
9113
|
[`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
|
|
7278
9114
|
"check-content": wrap(checkContent)
|
|
7279
|
-
};
|
|
9115
|
+
});
|
|
9116
|
+
if (req.code) checks.push({
|
|
9117
|
+
[`${ATTR}system`]: "http://inspec.io/",
|
|
9118
|
+
"check-content": wrap(req.code)
|
|
9119
|
+
});
|
|
9120
|
+
if (checks.length > 0) rule.check = checks;
|
|
7280
9121
|
return rule;
|
|
7281
9122
|
}
|
|
7282
9123
|
/** Build the XCCDF TestResult object. */
|
|
@@ -7496,7 +9337,7 @@ async function convertHdfToOscalSar(input) {
|
|
|
7496
9337
|
if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-sar: empty input");
|
|
7497
9338
|
let hdfResults;
|
|
7498
9339
|
try {
|
|
7499
|
-
hdfResults =
|
|
9340
|
+
hdfResults = parseHdf(input);
|
|
7500
9341
|
} catch {
|
|
7501
9342
|
throw new Error("hdf-to-oscal-sar: failed to parse HDF JSON");
|
|
7502
9343
|
}
|
|
@@ -7508,8 +9349,9 @@ async function convertHdfToOscalSar(input) {
|
|
|
7508
9349
|
* Constructs the full OSCAL assessment-results document from HDF results.
|
|
7509
9350
|
*/
|
|
7510
9351
|
function buildOSCALDocument(hdfResults) {
|
|
7511
|
-
let timestamp = (/* @__PURE__ */ new Date())
|
|
7512
|
-
|
|
9352
|
+
let timestamp = formatTimestampSeconds(/* @__PURE__ */ new Date());
|
|
9353
|
+
const documentTime = hdfTime(hdfResults.timestamp);
|
|
9354
|
+
if (documentTime) timestamp = formatTimestampSeconds(documentTime);
|
|
7513
9355
|
const metadata = {
|
|
7514
9356
|
title: "HDF Assessment Results Export",
|
|
7515
9357
|
"last-modified": timestamp,
|
|
@@ -7528,6 +9370,34 @@ function buildOSCALDocument(hdfResults) {
|
|
|
7528
9370
|
results
|
|
7529
9371
|
} };
|
|
7530
9372
|
}
|
|
9373
|
+
/** Earliest startTime across the results, or undefined if none carry one. */
|
|
9374
|
+
function earliestResultTime(results) {
|
|
9375
|
+
let earliest;
|
|
9376
|
+
for (const result of results ?? []) {
|
|
9377
|
+
const parsed = hdfTime(result.startTime);
|
|
9378
|
+
if (!parsed) continue;
|
|
9379
|
+
if (earliest === void 0 || parsed < earliest) earliest = parsed;
|
|
9380
|
+
}
|
|
9381
|
+
return earliest;
|
|
9382
|
+
}
|
|
9383
|
+
/**
|
|
9384
|
+
* Render an assessment time. OSCAL requires result.start and
|
|
9385
|
+
* observation.collected, so a fallback is needed when HDF carries no result time
|
|
9386
|
+
* — but it must never win over a real one.
|
|
9387
|
+
*/
|
|
9388
|
+
function formatAssessmentTime(time, fallback) {
|
|
9389
|
+
return time === void 0 ? fallback : formatTimestampSeconds(time);
|
|
9390
|
+
}
|
|
9391
|
+
/** When the assessment ran: the earliest result time anywhere in the baseline. */
|
|
9392
|
+
function assessmentStart(baseline, fallback) {
|
|
9393
|
+
let earliest;
|
|
9394
|
+
for (const req of baseline.requirements) {
|
|
9395
|
+
const reqEarliest = earliestResultTime(req.results);
|
|
9396
|
+
if (reqEarliest === void 0) continue;
|
|
9397
|
+
if (earliest === void 0 || reqEarliest < earliest) earliest = reqEarliest;
|
|
9398
|
+
}
|
|
9399
|
+
return formatAssessmentTime(earliest, fallback);
|
|
9400
|
+
}
|
|
7531
9401
|
/**
|
|
7532
9402
|
* Converts a single EvaluatedBaseline to an OSCAL Result.
|
|
7533
9403
|
*/
|
|
@@ -7549,7 +9419,7 @@ function baselineToResult(baseline, timestamp) {
|
|
|
7549
9419
|
uuid: crypto.randomUUID(),
|
|
7550
9420
|
title,
|
|
7551
9421
|
description,
|
|
7552
|
-
start: timestamp,
|
|
9422
|
+
start: assessmentStart(baseline, timestamp),
|
|
7553
9423
|
findings,
|
|
7554
9424
|
observations,
|
|
7555
9425
|
risks
|
|
@@ -7563,14 +9433,59 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
7563
9433
|
const { state, reason } = aggregateStatus(req.results);
|
|
7564
9434
|
const findingDesc = extractDefaultDescription(req.descriptions);
|
|
7565
9435
|
const props = [];
|
|
7566
|
-
|
|
7567
|
-
const
|
|
7568
|
-
if (Array.isArray(
|
|
7569
|
-
for (const v of
|
|
7570
|
-
name:
|
|
9436
|
+
const pushTagValues = (key) => {
|
|
9437
|
+
const raw = req.tags?.[key];
|
|
9438
|
+
if (Array.isArray(raw)) {
|
|
9439
|
+
for (const v of raw) if (typeof v === "string") props.push({
|
|
9440
|
+
name: key,
|
|
7571
9441
|
value: v
|
|
7572
9442
|
});
|
|
7573
9443
|
}
|
|
9444
|
+
};
|
|
9445
|
+
pushTagValues("nist");
|
|
9446
|
+
pushTagValues("cci");
|
|
9447
|
+
if (req.code) props.push({
|
|
9448
|
+
name: "code",
|
|
9449
|
+
value: req.code
|
|
9450
|
+
});
|
|
9451
|
+
for (const label of [
|
|
9452
|
+
"check",
|
|
9453
|
+
"fix",
|
|
9454
|
+
"rationale"
|
|
9455
|
+
]) {
|
|
9456
|
+
const d = req.descriptions.find((x) => x.label === label);
|
|
9457
|
+
if (d) props.push({
|
|
9458
|
+
name: label,
|
|
9459
|
+
value: d.data
|
|
9460
|
+
});
|
|
9461
|
+
}
|
|
9462
|
+
if (req.controlType) props.push({
|
|
9463
|
+
name: "control-type",
|
|
9464
|
+
value: req.controlType
|
|
9465
|
+
});
|
|
9466
|
+
if (req.verificationMethod) props.push({
|
|
9467
|
+
name: "verification-method",
|
|
9468
|
+
value: req.verificationMethod
|
|
9469
|
+
});
|
|
9470
|
+
if (req.applicability) props.push({
|
|
9471
|
+
name: "applicability",
|
|
9472
|
+
value: req.applicability
|
|
9473
|
+
});
|
|
9474
|
+
const links = [];
|
|
9475
|
+
if (Array.isArray(req.refs)) for (const r of req.refs) {
|
|
9476
|
+
const o = r;
|
|
9477
|
+
if (typeof o.url === "string") links.push({
|
|
9478
|
+
href: o.url,
|
|
9479
|
+
rel: "reference"
|
|
9480
|
+
});
|
|
9481
|
+
else if (typeof o.uri === "string") links.push({
|
|
9482
|
+
href: o.uri,
|
|
9483
|
+
rel: "reference"
|
|
9484
|
+
});
|
|
9485
|
+
else if (typeof o.ref === "string") props.push({
|
|
9486
|
+
name: "reference",
|
|
9487
|
+
value: o.ref
|
|
9488
|
+
});
|
|
7574
9489
|
}
|
|
7575
9490
|
let title = req.id;
|
|
7576
9491
|
if (req.title && req.title !== "") title = req.title;
|
|
@@ -7586,6 +9501,7 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
7586
9501
|
title,
|
|
7587
9502
|
description: findingDesc || "",
|
|
7588
9503
|
props: props.length > 0 ? props : void 0,
|
|
9504
|
+
links: links.length > 0 ? links : void 0,
|
|
7589
9505
|
target
|
|
7590
9506
|
};
|
|
7591
9507
|
let observation;
|
|
@@ -7595,7 +9511,7 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
7595
9511
|
uuid: obsUUID,
|
|
7596
9512
|
description: buildObservationDescription(req.results),
|
|
7597
9513
|
methods: ["TEST"],
|
|
7598
|
-
collected: timestamp
|
|
9514
|
+
collected: formatAssessmentTime(earliestResultTime(req.results), timestamp)
|
|
7599
9515
|
};
|
|
7600
9516
|
finding["related-observations"] = [{ "observation-uuid": obsUUID }];
|
|
7601
9517
|
}
|
|
@@ -7725,18 +9641,24 @@ async function convertHdfToOscalPoam(input) {
|
|
|
7725
9641
|
if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-poam: empty input");
|
|
7726
9642
|
let amendments;
|
|
7727
9643
|
try {
|
|
7728
|
-
amendments =
|
|
9644
|
+
amendments = parseHdf(input);
|
|
7729
9645
|
} catch {
|
|
7730
9646
|
throw new Error("hdf-to-oscal-poam: failed to parse JSON");
|
|
7731
9647
|
}
|
|
7732
9648
|
const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendments) };
|
|
7733
9649
|
return JSON.stringify(doc, null, 2);
|
|
7734
9650
|
}
|
|
9651
|
+
/** HDF dates arrive as strings from JSON.parse but are typed as Date. */
|
|
9652
|
+
function toDate(value) {
|
|
9653
|
+
const d = typeof value === "string" ? parseTimestamp(value) : value;
|
|
9654
|
+
if (!d || isNaN(d.getTime())) return void 0;
|
|
9655
|
+
return d;
|
|
9656
|
+
}
|
|
7735
9657
|
/**
|
|
7736
9658
|
* Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
|
|
7737
9659
|
*/
|
|
7738
9660
|
function amendmentsToPOAM(amendments) {
|
|
7739
|
-
const now = (/* @__PURE__ */ new Date())
|
|
9661
|
+
const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
|
|
7740
9662
|
const metadata = {
|
|
7741
9663
|
title: amendments.name,
|
|
7742
9664
|
"last-modified": now,
|
|
@@ -7787,25 +9709,45 @@ function overrideToPOAMItem(override) {
|
|
|
7787
9709
|
name: "impacted-control-id",
|
|
7788
9710
|
value: nistTagToControlId(override.requirementId)
|
|
7789
9711
|
}];
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
lifecycle: "planned",
|
|
7794
|
-
title: ms.description,
|
|
7795
|
-
description: ms.description
|
|
9712
|
+
if (override.type) riskProps.push({
|
|
9713
|
+
name: "override-type",
|
|
9714
|
+
value: String(override.type)
|
|
7796
9715
|
});
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
9716
|
+
if (override.impact && typeof override.impact.value === "number") riskProps.push({
|
|
9717
|
+
name: "impact-override",
|
|
9718
|
+
value: String(override.impact.value)
|
|
9719
|
+
});
|
|
9720
|
+
const remediations = [];
|
|
9721
|
+
if (override.milestones) for (const ms of override.milestones) {
|
|
9722
|
+
const msProps = [];
|
|
9723
|
+
if (ms.estimatedCompletion) {
|
|
9724
|
+
const d = toDate(ms.estimatedCompletion);
|
|
9725
|
+
if (d) msProps.push({
|
|
9726
|
+
name: "estimated-completion",
|
|
9727
|
+
value: formatTimestampSeconds(d)
|
|
9728
|
+
});
|
|
9729
|
+
}
|
|
9730
|
+
if (ms.status) msProps.push({
|
|
9731
|
+
name: "milestone-status",
|
|
9732
|
+
value: String(ms.status)
|
|
9733
|
+
});
|
|
9734
|
+
remediations.push({
|
|
7802
9735
|
uuid: crypto.randomUUID(),
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
}
|
|
9736
|
+
lifecycle: "planned",
|
|
9737
|
+
title: ms.description,
|
|
9738
|
+
description: ms.description,
|
|
9739
|
+
props: msProps.length > 0 ? msProps : void 0
|
|
9740
|
+
});
|
|
7808
9741
|
}
|
|
9742
|
+
let riskLog;
|
|
9743
|
+
const expiresDate = override.expiresAt ? toDate(override.expiresAt) : void 0;
|
|
9744
|
+
if (expiresDate && expiresDate.getTime() > 0) riskLog = { entries: [{
|
|
9745
|
+
uuid: crypto.randomUUID(),
|
|
9746
|
+
title: "Scheduled review",
|
|
9747
|
+
description: "Amendment expiration date",
|
|
9748
|
+
start: formatTimestampSeconds(expiresDate),
|
|
9749
|
+
"status-change": riskStatus
|
|
9750
|
+
}] };
|
|
7809
9751
|
const risk = {
|
|
7810
9752
|
uuid: riskUUID,
|
|
7811
9753
|
title: override.requirementId,
|
|
@@ -7877,7 +9819,7 @@ function buildTags(dep) {
|
|
|
7877
9819
|
if (dep.parentDependencies.length > 0) extras.parentDependencies = dep.parentDependencies;
|
|
7878
9820
|
return buildNistCciTags(nist, cciTags, extras);
|
|
7879
9821
|
}
|
|
7880
|
-
async function convertIonchannelToHdf(input) {
|
|
9822
|
+
async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
|
|
7881
9823
|
if (!input?.trim()) throw new Error("Empty input");
|
|
7882
9824
|
validateInputSize(input, "ionchannel");
|
|
7883
9825
|
const analysis = parseJSON(input);
|
|
@@ -7904,10 +9846,11 @@ async function convertIonchannelToHdf(input) {
|
|
|
7904
9846
|
outdated_version: dep.outdated_version,
|
|
7905
9847
|
dependencies: dep.dependencies
|
|
7906
9848
|
}, null, 2);
|
|
7907
|
-
const req = createRequirement(depId, title, [createDescription("default", `Dependency ${dep.org}/${dep.name}`)], 0, [
|
|
9849
|
+
const req = createRequirement(depId, title, [createDescription("default", `Dependency ${dep.org}/${dep.name}`)], 0, [{
|
|
9850
|
+
status: ResultStatus.NotReviewed,
|
|
7908
9851
|
codeDesc: "Dependency inventory item",
|
|
7909
9852
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
7910
|
-
}
|
|
9853
|
+
}], { tags });
|
|
7911
9854
|
req.code = code;
|
|
7912
9855
|
const controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
|
|
7913
9856
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -7915,20 +9858,22 @@ async function convertIonchannelToHdf(input) {
|
|
|
7915
9858
|
return req;
|
|
7916
9859
|
});
|
|
7917
9860
|
const resultsChecksum = await inputChecksum(input);
|
|
9861
|
+
const baseline = createMinimalBaseline("Ion Channel SBOM Analysis", requirements, {
|
|
9862
|
+
title: `Ion Channel Analysis of ${analysis.source}`,
|
|
9863
|
+
summary: analysis.summary,
|
|
9864
|
+
integrity: {
|
|
9865
|
+
algorithm: resultsChecksum.algorithm,
|
|
9866
|
+
checksum: resultsChecksum.value
|
|
9867
|
+
},
|
|
9868
|
+
status: "loaded"
|
|
9869
|
+
});
|
|
9870
|
+
baseline.maintainer = "saf@groups.mitre.org";
|
|
7918
9871
|
return buildHdfResults({
|
|
7919
9872
|
generatorName: "ionchannel-to-hdf",
|
|
7920
|
-
converterVersion
|
|
9873
|
+
converterVersion,
|
|
7921
9874
|
toolName: "Ion Channel",
|
|
7922
9875
|
toolFormat: "JSON",
|
|
7923
|
-
baselines: [
|
|
7924
|
-
title: `Ion Channel Analysis of ${analysis.source}`,
|
|
7925
|
-
summary: analysis.summary,
|
|
7926
|
-
integrity: {
|
|
7927
|
-
algorithm: resultsChecksum.algorithm,
|
|
7928
|
-
checksum: resultsChecksum.value
|
|
7929
|
-
},
|
|
7930
|
-
status: "loaded"
|
|
7931
|
-
})],
|
|
9876
|
+
baselines: [baseline],
|
|
7932
9877
|
timestamp: /* @__PURE__ */ new Date()
|
|
7933
9878
|
});
|
|
7934
9879
|
}
|
|
@@ -8141,6 +10086,40 @@ function affectedPackageToIdentifier(pkg) {
|
|
|
8141
10086
|
if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
|
|
8142
10087
|
if (pkg.name) return pkg.name;
|
|
8143
10088
|
}
|
|
10089
|
+
/** Replace the `@version` segment of a purl (between `@` and `?`/`#`), inserting one if absent. */
|
|
10090
|
+
function swapPurlVersion(purl, version) {
|
|
10091
|
+
const at = purl.indexOf("@");
|
|
10092
|
+
if (at >= 0) {
|
|
10093
|
+
const rest = purl.slice(at + 1);
|
|
10094
|
+
const end = rest.search(/[?#]/);
|
|
10095
|
+
const tail = end >= 0 ? rest.slice(end) : "";
|
|
10096
|
+
return `${purl.slice(0, at)}@${version}${tail}`;
|
|
10097
|
+
}
|
|
10098
|
+
const end = purl.search(/[?#]/);
|
|
10099
|
+
return end >= 0 ? `${purl.slice(0, end)}@${version}${purl.slice(end)}` : `${purl}@${version}`;
|
|
10100
|
+
}
|
|
10101
|
+
/**
|
|
10102
|
+
* Identifier for the FIXED version of a package (purl with the version swapped
|
|
10103
|
+
* to fixedInVersion, or name@fixedInVersion). Undefined when there is no
|
|
10104
|
+
* fixedInVersion or no purl/name to anchor it (a bare cpe cannot be swapped).
|
|
10105
|
+
*/
|
|
10106
|
+
function fixedPackageIdentifier(pkg) {
|
|
10107
|
+
if (!pkg.fixedInVersion) return void 0;
|
|
10108
|
+
if (pkg.purl) return swapPurlVersion(pkg.purl, pkg.fixedInVersion);
|
|
10109
|
+
if (pkg.name) return `${pkg.name}@${pkg.fixedInVersion}`;
|
|
10110
|
+
}
|
|
10111
|
+
/**
|
|
10112
|
+
* `vers` (Package URL version-range) type for a package, from its ecosystem or
|
|
10113
|
+
* the purl type. Returns undefined when neither is known (so callers avoid
|
|
10114
|
+
* emitting an invalid range).
|
|
10115
|
+
*/
|
|
10116
|
+
function versTypeFor(pkg) {
|
|
10117
|
+
if (pkg.ecosystem) return String(pkg.ecosystem).toLowerCase();
|
|
10118
|
+
if (pkg.purl) {
|
|
10119
|
+
const m = /^pkg:([^/]+)\//.exec(pkg.purl);
|
|
10120
|
+
if (m?.[1]) return m[1].toLowerCase();
|
|
10121
|
+
}
|
|
10122
|
+
}
|
|
8144
10123
|
/**
|
|
8145
10124
|
* Build an HDF Evidence entry pointing at the upstream VEX document. Used by
|
|
8146
10125
|
* importers to preserve provenance even though we lose the structured
|
|
@@ -8635,12 +10614,27 @@ function publisherIdentityOrDefault(bom) {
|
|
|
8635
10614
|
* fields the shared VEX mapping considers consumer-action-bearing survive
|
|
8636
10615
|
* round-trip; the rest collapse into the available CSAF fields.
|
|
8637
10616
|
*/
|
|
10617
|
+
const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
8638
10618
|
const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
|
|
8639
10619
|
const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
|
|
8640
10620
|
const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
|
|
10621
|
+
/** Map an HDF Cvss block to a CSAF score entry (cvss_v2/v3/v4 by version). */
|
|
10622
|
+
function buildCsafScore(cvss, products) {
|
|
10623
|
+
const inner = {
|
|
10624
|
+
version: cvss.version,
|
|
10625
|
+
...cvss.baseVector && { vectorString: cvss.baseVector },
|
|
10626
|
+
...typeof cvss.baseScore === "number" && { baseScore: cvss.baseScore },
|
|
10627
|
+
...cvss.baseSeverity && { baseSeverity: cvss.baseSeverity }
|
|
10628
|
+
};
|
|
10629
|
+
if (inner.vectorString === void 0 && inner.baseScore === void 0) return;
|
|
10630
|
+
return {
|
|
10631
|
+
[cvss.version.startsWith("4") ? "cvss_v4" : cvss.version.startsWith("2") ? "cvss_v2" : "cvss_v3"]: inner,
|
|
10632
|
+
products
|
|
10633
|
+
};
|
|
10634
|
+
}
|
|
8641
10635
|
function convertHdfToCsafVex(input, converterVersion) {
|
|
8642
10636
|
validateInputSize(input, "hdf-to-csaf-vex");
|
|
8643
|
-
const amendments =
|
|
10637
|
+
const amendments = parseHdf(input);
|
|
8644
10638
|
const groups = groupByCVE(amendments.overrides ?? []);
|
|
8645
10639
|
const productSet = /* @__PURE__ */ new Map();
|
|
8646
10640
|
const vulnerabilities = [];
|
|
@@ -8649,6 +10643,7 @@ function convertHdfToCsafVex(input, converterVersion) {
|
|
|
8649
10643
|
if (!v) continue;
|
|
8650
10644
|
vulnerabilities.push(v);
|
|
8651
10645
|
for (const p of productIDsForGroup(group)) productSet.set(p, true);
|
|
10646
|
+
for (const p of fixedProductIDsForGroup(group)) productSet.set(p, true);
|
|
8652
10647
|
}
|
|
8653
10648
|
if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8654
10649
|
const products = [...productSet.keys()].sort();
|
|
@@ -8684,6 +10679,15 @@ function productIDsForGroup(group) {
|
|
|
8684
10679
|
for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
|
|
8685
10680
|
return [...seen].sort();
|
|
8686
10681
|
}
|
|
10682
|
+
/** Synthesized fixed-version product ids across a group's affectedPackages. */
|
|
10683
|
+
function fixedProductIDsForGroup(group) {
|
|
10684
|
+
const ids = [];
|
|
10685
|
+
for (const o of group.overrides) for (const p of o.affectedPackages ?? []) {
|
|
10686
|
+
const f = fixedPackageIdentifier(p);
|
|
10687
|
+
if (f) ids.push(f);
|
|
10688
|
+
}
|
|
10689
|
+
return ids;
|
|
10690
|
+
}
|
|
8687
10691
|
function productIDsFor$1(o) {
|
|
8688
10692
|
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8689
10693
|
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
@@ -8710,6 +10714,25 @@ function buildVulnerability(group) {
|
|
|
8710
10714
|
let emitted = false;
|
|
8711
10715
|
for (const o of group.overrides) {
|
|
8712
10716
|
const pids = productIDsFor$1(o);
|
|
10717
|
+
if (o.cvss) {
|
|
10718
|
+
const score = buildCsafScore(o.cvss, pids);
|
|
10719
|
+
if (score) {
|
|
10720
|
+
v.scores = (v.scores ?? []).concat(score);
|
|
10721
|
+
emitted = true;
|
|
10722
|
+
}
|
|
10723
|
+
}
|
|
10724
|
+
for (const p of o.affectedPackages ?? []) {
|
|
10725
|
+
const fixedId = fixedPackageIdentifier(p);
|
|
10726
|
+
if (!fixedId) continue;
|
|
10727
|
+
status.first_fixed = (status.first_fixed ?? []).concat(fixedId);
|
|
10728
|
+
status.fixed = (status.fixed ?? []).concat(fixedId);
|
|
10729
|
+
v.remediations = (v.remediations ?? []).concat({
|
|
10730
|
+
category: "vendor_fix",
|
|
10731
|
+
details: `Fixed in ${p.fixedInVersion}`,
|
|
10732
|
+
product_ids: [fixedId]
|
|
10733
|
+
});
|
|
10734
|
+
emitted = true;
|
|
10735
|
+
}
|
|
8713
10736
|
let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
|
|
8714
10737
|
if (!canonical) continue;
|
|
8715
10738
|
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
|
|
@@ -8719,8 +10742,8 @@ function buildVulnerability(group) {
|
|
|
8719
10742
|
v.flags = v.flags ?? [];
|
|
8720
10743
|
v.flags.push({
|
|
8721
10744
|
label: String(o.justification),
|
|
8722
|
-
|
|
8723
|
-
|
|
10745
|
+
date: formatTimestampSeconds(hdfTime(o.appliedAt) ?? GO_ZERO_TIME$1),
|
|
10746
|
+
product_ids: pids
|
|
8724
10747
|
});
|
|
8725
10748
|
}
|
|
8726
10749
|
emitted = true;
|
|
@@ -8770,9 +10793,9 @@ function buildVulnerability(group) {
|
|
|
8770
10793
|
/* c8 ignore next */
|
|
8771
10794
|
if (!emitted) return void 0;
|
|
8772
10795
|
if (status.fixed) status.fixed = [...new Set(status.fixed)];
|
|
10796
|
+
if (status.first_fixed) status.first_fixed = [...new Set(status.first_fixed)];
|
|
8773
10797
|
if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
|
|
8774
10798
|
if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
|
|
8775
|
-
v.product_status = status;
|
|
8776
10799
|
if (v.references) {
|
|
8777
10800
|
const seen = /* @__PURE__ */ new Set();
|
|
8778
10801
|
v.references = v.references.filter((r) => {
|
|
@@ -8781,10 +10804,30 @@ function buildVulnerability(group) {
|
|
|
8781
10804
|
return true;
|
|
8782
10805
|
});
|
|
8783
10806
|
}
|
|
8784
|
-
return
|
|
10807
|
+
return {
|
|
10808
|
+
cve: v.cve,
|
|
10809
|
+
...v.notes && { notes: v.notes },
|
|
10810
|
+
product_status: status,
|
|
10811
|
+
...v.flags && { flags: v.flags },
|
|
10812
|
+
...v.threats && { threats: v.threats },
|
|
10813
|
+
...v.remediations && { remediations: v.remediations },
|
|
10814
|
+
...v.references && { references: v.references },
|
|
10815
|
+
...v.scores && { scores: v.scores }
|
|
10816
|
+
};
|
|
10817
|
+
}
|
|
10818
|
+
/** Stable document time: the earliest override appliedAt, falling back to now. */
|
|
10819
|
+
function earliestAppliedAt(amendments) {
|
|
10820
|
+
let earliest;
|
|
10821
|
+
for (const o of amendments.overrides ?? []) {
|
|
10822
|
+
if (!o.appliedAt) continue;
|
|
10823
|
+
const t = hdfTime(o.appliedAt);
|
|
10824
|
+
if (!t) continue;
|
|
10825
|
+
if (!earliest || t < earliest) earliest = t;
|
|
10826
|
+
}
|
|
10827
|
+
return earliest ?? /* @__PURE__ */ new Date();
|
|
8785
10828
|
}
|
|
8786
10829
|
function buildDocument(amendments, converterVersion) {
|
|
8787
|
-
const now = formatTimestampSeconds(
|
|
10830
|
+
const now = formatTimestampSeconds(earliestAppliedAt(amendments));
|
|
8788
10831
|
const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8789
10832
|
const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
|
|
8790
10833
|
const docVersion = amendments.version || "1";
|
|
@@ -8800,7 +10843,7 @@ function buildDocument(amendments, converterVersion) {
|
|
|
8800
10843
|
category: "csaf_vex",
|
|
8801
10844
|
csaf_version: "2.0",
|
|
8802
10845
|
title,
|
|
8803
|
-
notes,
|
|
10846
|
+
...notes.length > 0 && { notes },
|
|
8804
10847
|
publisher: {
|
|
8805
10848
|
category: "other",
|
|
8806
10849
|
name: publisherName
|
|
@@ -8838,6 +10881,7 @@ function buildDocument(amendments, converterVersion) {
|
|
|
8838
10881
|
* partial-fidelity by design — consumer-action-bearing fields (CVE id,
|
|
8839
10882
|
* status, justification) survive round-trip; the rest collapse.
|
|
8840
10883
|
*/
|
|
10884
|
+
const GO_ZERO_TIME = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
8841
10885
|
const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
|
|
8842
10886
|
const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
|
|
8843
10887
|
const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
@@ -8845,14 +10889,15 @@ const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
|
|
|
8845
10889
|
const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
|
|
8846
10890
|
async function convertHdfToOpenVex(input, converterVersion) {
|
|
8847
10891
|
validateInputSize(input, "hdf-to-openvex");
|
|
8848
|
-
const amendments =
|
|
10892
|
+
const amendments = parseHdf(input);
|
|
8849
10893
|
const statements = [];
|
|
8850
10894
|
let earliest;
|
|
8851
10895
|
for (const o of amendments.overrides ?? []) {
|
|
8852
10896
|
const s = overrideToStatement(o);
|
|
8853
10897
|
if (!s) continue;
|
|
8854
10898
|
statements.push(s);
|
|
8855
|
-
const t =
|
|
10899
|
+
const t = hdfTime(o.appliedAt);
|
|
10900
|
+
if (!t) continue;
|
|
8856
10901
|
if (!earliest || t < earliest) earliest = t;
|
|
8857
10902
|
}
|
|
8858
10903
|
if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
@@ -8875,22 +10920,24 @@ function overrideToStatement(o) {
|
|
|
8875
10920
|
let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
|
|
8876
10921
|
if (!canonical) return void 0;
|
|
8877
10922
|
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
|
|
8878
|
-
const
|
|
10923
|
+
const notAffected = canonical === VexStatus.NotAffected;
|
|
10924
|
+
const justification = notAffected && o.justification ? String(o.justification) : "";
|
|
10925
|
+
const impact = notAffected ? stripProductsLine(o.reason ?? "") : "";
|
|
10926
|
+
let action = "";
|
|
10927
|
+
if (canonical === VexStatus.Fixed) action = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
|
|
10928
|
+
else if (canonical === VexStatus.Affected) action = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
|
|
10929
|
+
return {
|
|
8879
10930
|
vulnerability: {
|
|
8880
|
-
|
|
8881
|
-
|
|
10931
|
+
"@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`,
|
|
10932
|
+
name: o.requirementId
|
|
8882
10933
|
},
|
|
10934
|
+
products: productsFor(o),
|
|
8883
10935
|
status: String(canonical),
|
|
8884
|
-
|
|
8885
|
-
|
|
10936
|
+
...justification && { justification },
|
|
10937
|
+
...impact && { impact_statement: impact },
|
|
10938
|
+
...action && { action_statement: action },
|
|
10939
|
+
timestamp: formatTimestampSeconds(hdfTime(o.appliedAt) ?? GO_ZERO_TIME)
|
|
8886
10940
|
};
|
|
8887
|
-
if (canonical === VexStatus.NotAffected) {
|
|
8888
|
-
if (o.justification) stmt.justification = String(o.justification);
|
|
8889
|
-
const impact = stripProductsLine(o.reason ?? "");
|
|
8890
|
-
if (impact) stmt.impact_statement = impact;
|
|
8891
|
-
} else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
|
|
8892
|
-
else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
|
|
8893
|
-
return stmt;
|
|
8894
10941
|
}
|
|
8895
10942
|
function productsFor(o) {
|
|
8896
10943
|
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
@@ -8919,7 +10966,8 @@ function stripProductsLine(reason) {
|
|
|
8919
10966
|
}
|
|
8920
10967
|
async function buildDocumentID(input, a) {
|
|
8921
10968
|
if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
|
|
8922
|
-
|
|
10969
|
+
const digest = await sha256(input);
|
|
10970
|
+
return `${OPENVEX_NAMESPACE}vex-${digest}`;
|
|
8923
10971
|
}
|
|
8924
10972
|
//#endregion
|
|
8925
10973
|
//#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
|
|
@@ -8936,9 +10984,41 @@ const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
|
|
|
8936
10984
|
const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
|
|
8937
10985
|
const RESPONSE_LINE = /^Response:.*$/gm;
|
|
8938
10986
|
const DEFAULT_PRODUCT_ID = "HDFPID-0001";
|
|
8939
|
-
|
|
10987
|
+
/**
|
|
10988
|
+
* Build CycloneDX affects[].versions[] for a package with a fixedInVersion:
|
|
10989
|
+
* the current version is `affected`, and `>= fixedInVersion` is `unaffected`,
|
|
10990
|
+
* expressed as a `vers` range. Undefined when there is no vers type to key the
|
|
10991
|
+
* range (caller falls back to a free-text recommendation).
|
|
10992
|
+
*/
|
|
10993
|
+
function buildCdxAffectsVersions(pkg) {
|
|
10994
|
+
if (!pkg?.fixedInVersion) return void 0;
|
|
10995
|
+
const t = versTypeFor(pkg);
|
|
10996
|
+
if (!t) return void 0;
|
|
10997
|
+
const versions = [];
|
|
10998
|
+
if (pkg.version) versions.push({
|
|
10999
|
+
version: pkg.version,
|
|
11000
|
+
status: "affected"
|
|
11001
|
+
});
|
|
11002
|
+
versions.push({
|
|
11003
|
+
range: `vers:${t}/>=${pkg.fixedInVersion}`,
|
|
11004
|
+
status: "unaffected"
|
|
11005
|
+
});
|
|
11006
|
+
return versions;
|
|
11007
|
+
}
|
|
11008
|
+
/** Map an HDF Cvss block to a CycloneDX rating. */
|
|
11009
|
+
function buildCdxRating(cvss) {
|
|
11010
|
+
const rating = {};
|
|
11011
|
+
if (typeof cvss.baseScore === "number") rating.score = cvss.baseScore;
|
|
11012
|
+
if (cvss.baseSeverity) rating.severity = cvss.baseSeverity;
|
|
11013
|
+
if (cvss.baseVector) rating.vector = cvss.baseVector;
|
|
11014
|
+
const v = cvss.version;
|
|
11015
|
+
rating.method = v === "4.0" ? "CVSSv4" : v === "3.1" ? "CVSSv31" : v === "3.0" ? "CVSSv3" : v === "2.0" ? "CVSSv2" : "other";
|
|
11016
|
+
if (rating.score === void 0 && rating.vector === void 0) return void 0;
|
|
11017
|
+
return rating;
|
|
11018
|
+
}
|
|
11019
|
+
async function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
8940
11020
|
validateInputSize(input, "hdf-to-cyclonedx-vex");
|
|
8941
|
-
const amendments =
|
|
11021
|
+
const amendments = parseHdf(input);
|
|
8942
11022
|
const componentRegistry = /* @__PURE__ */ new Map();
|
|
8943
11023
|
const vulnerabilities = [];
|
|
8944
11024
|
let earliest;
|
|
@@ -8947,7 +11027,8 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
|
8947
11027
|
const v = overrideToVulnerability(o, componentRegistry);
|
|
8948
11028
|
if (!v) continue;
|
|
8949
11029
|
vulnerabilities.push(v);
|
|
8950
|
-
const t =
|
|
11030
|
+
const t = hdfTime(o.appliedAt);
|
|
11031
|
+
if (!t) continue;
|
|
8951
11032
|
if (!earliest || t < earliest) earliest = t;
|
|
8952
11033
|
}
|
|
8953
11034
|
if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
@@ -8956,7 +11037,7 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
|
8956
11037
|
const bom = {
|
|
8957
11038
|
bomFormat: "CycloneDX",
|
|
8958
11039
|
specVersion: "1.4",
|
|
8959
|
-
serialNumber:
|
|
11040
|
+
serialNumber: await buildSerialNumber(input, amendments),
|
|
8960
11041
|
version: 1,
|
|
8961
11042
|
metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
|
|
8962
11043
|
components,
|
|
@@ -8980,23 +11061,14 @@ function overrideToVulnerability(o, componentRegistry) {
|
|
|
8980
11061
|
const cdxJust = justificationForCycloneDX(o.justification);
|
|
8981
11062
|
if (cdxJust) analysis.justification = cdxJust;
|
|
8982
11063
|
}
|
|
8983
|
-
const detail = stripReasonAnnotations(o.reason ?? "");
|
|
8984
|
-
if (detail) analysis.detail = detail;
|
|
8985
11064
|
if (canonical === VexStatus.Fixed) analysis.response = ["update"];
|
|
8986
11065
|
else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
|
|
8987
|
-
const
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
name: "NVD",
|
|
8991
|
-
url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
8992
|
-
},
|
|
8993
|
-
analysis,
|
|
8994
|
-
affects: pids.map((p) => ({ ref: p }))
|
|
8995
|
-
};
|
|
11066
|
+
const detail = stripReasonAnnotations(o.reason ?? "");
|
|
11067
|
+
if (detail) analysis.detail = detail;
|
|
11068
|
+
const references = [];
|
|
8996
11069
|
for (const e of o.evidence ?? []) {
|
|
8997
11070
|
if (e.type !== "url" || !e.data) continue;
|
|
8998
|
-
|
|
8999
|
-
v.references.push({
|
|
11071
|
+
references.push({
|
|
9000
11072
|
id: o.requirementId,
|
|
9001
11073
|
source: {
|
|
9002
11074
|
name: e.description ?? "",
|
|
@@ -9004,7 +11076,26 @@ function overrideToVulnerability(o, componentRegistry) {
|
|
|
9004
11076
|
}
|
|
9005
11077
|
});
|
|
9006
11078
|
}
|
|
9007
|
-
|
|
11079
|
+
const rating = o.cvss ? buildCdxRating(o.cvss) : void 0;
|
|
11080
|
+
const unranged = (o.affectedPackages ?? []).find((p) => p.fixedInVersion && !versTypeFor(p));
|
|
11081
|
+
return {
|
|
11082
|
+
id: o.requirementId,
|
|
11083
|
+
source: {
|
|
11084
|
+
name: "NVD",
|
|
11085
|
+
url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
11086
|
+
},
|
|
11087
|
+
...references.length > 0 && { references },
|
|
11088
|
+
...rating && { ratings: [rating] },
|
|
11089
|
+
...unranged && { recommendation: `Upgrade to ${unranged.fixedInVersion}` },
|
|
11090
|
+
analysis,
|
|
11091
|
+
affects: pids.map((pid) => {
|
|
11092
|
+
const versions = buildCdxAffectsVersions(pkgById.get(pid));
|
|
11093
|
+
return versions ? {
|
|
11094
|
+
ref: pid,
|
|
11095
|
+
versions
|
|
11096
|
+
} : { ref: pid };
|
|
11097
|
+
})
|
|
11098
|
+
};
|
|
9008
11099
|
}
|
|
9009
11100
|
function canonicalToCycloneDXState(canonical) {
|
|
9010
11101
|
switch (canonical) {
|
|
@@ -9063,18 +11154,9 @@ function buildMetadata(a, docTime, converterVersion) {
|
|
|
9063
11154
|
}
|
|
9064
11155
|
return metadata;
|
|
9065
11156
|
}
|
|
9066
|
-
function
|
|
11157
|
+
async function buildSerialNumber(input, a) {
|
|
9067
11158
|
if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
|
|
9068
|
-
return `urn:uuid:${
|
|
9069
|
-
}
|
|
9070
|
-
function fnv1a(s) {
|
|
9071
|
-
let h = 2166136261;
|
|
9072
|
-
for (let i = 0; i < s.length; i++) {
|
|
9073
|
-
h ^= s.charCodeAt(i);
|
|
9074
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
9075
|
-
}
|
|
9076
|
-
const d = h.toString(16).padStart(8, "0");
|
|
9077
|
-
return (d + d + d + d).slice(0, 32);
|
|
11159
|
+
return `urn:uuid:${(await sha256(input)).slice(0, 32)}`;
|
|
9078
11160
|
}
|
|
9079
11161
|
//#endregion
|
|
9080
11162
|
//#region converters/oscal-to-hdf/typescript/detect.ts
|
|
@@ -10086,6 +12168,6 @@ function sarBaselineName(result, sar) {
|
|
|
10086
12168
|
return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
|
|
10087
12169
|
}
|
|
10088
12170
|
//#endregion
|
|
10089
|
-
export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
|
|
12171
|
+
export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToEcs, convertHdfToOcsf, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToSplunk, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
|
|
10090
12172
|
|
|
10091
12173
|
//# sourceMappingURL=index.js.map
|