@mitre/hdf-converters 3.3.2 → 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 +2489 -488
- 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
|
*
|
|
@@ -776,7 +804,7 @@ const IMPACT_MAPPING$9 = {
|
|
|
776
804
|
warning: .5,
|
|
777
805
|
note: .3
|
|
778
806
|
};
|
|
779
|
-
async function convertSarifToHdf(input) {
|
|
807
|
+
async function convertSarifToHdf(input, converterVersion = "1.0.0") {
|
|
780
808
|
validateInputSize(input, "sarif");
|
|
781
809
|
const resultsChecksum = await inputChecksum(input);
|
|
782
810
|
const sarif = parseJSON(input);
|
|
@@ -789,12 +817,11 @@ async function convertSarifToHdf(input) {
|
|
|
789
817
|
const timestamp = /* @__PURE__ */ new Date();
|
|
790
818
|
return buildHdfResults({
|
|
791
819
|
generatorName: "sarif-to-hdf",
|
|
792
|
-
converterVersion
|
|
820
|
+
converterVersion,
|
|
793
821
|
toolName: firstDriver?.name,
|
|
794
822
|
toolVersion: firstDriver?.version,
|
|
795
823
|
toolFormat: "SARIF",
|
|
796
824
|
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
|
|
797
|
-
components: [],
|
|
798
825
|
timestamp
|
|
799
826
|
});
|
|
800
827
|
}
|
|
@@ -918,9 +945,9 @@ function convertSarifResultToHDFResults(result) {
|
|
|
918
945
|
const locationlessResult = {
|
|
919
946
|
status,
|
|
920
947
|
codeDesc: "No source location",
|
|
921
|
-
startTime: /* @__PURE__ */ new Date()
|
|
922
|
-
backtrace
|
|
948
|
+
startTime: /* @__PURE__ */ new Date()
|
|
923
949
|
};
|
|
950
|
+
if (backtrace.length > 0) locationlessResult.backtrace = backtrace;
|
|
924
951
|
if (suppressionJustification) locationlessResult.message = `Suppressed: ${suppressionJustification}`;
|
|
925
952
|
results.push(locationlessResult);
|
|
926
953
|
}
|
|
@@ -972,28 +999,17 @@ function extractCweFromRule(rule) {
|
|
|
972
999
|
if (cweIds.length > 0) return cweIds;
|
|
973
1000
|
}
|
|
974
1001
|
if (rule.properties?.tags && Array.isArray(rule.properties.tags)) {
|
|
975
|
-
const cweTagPattern = /^CWE-\d+$/;
|
|
976
1002
|
const cweIds = [];
|
|
977
|
-
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
|
+
}
|
|
978
1007
|
if (cweIds.length > 0) return cweIds;
|
|
979
1008
|
}
|
|
980
1009
|
return [];
|
|
981
1010
|
}
|
|
982
1011
|
function extractCweIds(text) {
|
|
983
|
-
|
|
984
|
-
if (!matches) return [];
|
|
985
|
-
const cweIds = [];
|
|
986
|
-
for (const match of matches) {
|
|
987
|
-
const content = match.slice(1, -1);
|
|
988
|
-
if (content.includes("CWE-")) {
|
|
989
|
-
const parts = content.split(/,\s*|!\//);
|
|
990
|
-
for (const part of parts) {
|
|
991
|
-
const trimmed = part.trim();
|
|
992
|
-
if (trimmed.startsWith("CWE-")) cweIds.push(trimmed);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return cweIds;
|
|
1012
|
+
return extractCWEIDs(text).map((id) => `CWE-${id}`);
|
|
997
1013
|
}
|
|
998
1014
|
function mapKindToStatus(kind) {
|
|
999
1015
|
switch (kind) {
|
|
@@ -1089,21 +1105,31 @@ function createHDFResult(location, status, backtrace, suppressionMessage) {
|
|
|
1089
1105
|
const snippet = location.physicalLocation?.region?.snippet?.text;
|
|
1090
1106
|
let codeDesc = `URL : ${uri} LINE : ${line} COLUMN : ${column}`;
|
|
1091
1107
|
if (snippet) codeDesc = `${codeDesc}\n${snippet}`;
|
|
1092
|
-
|
|
1108
|
+
const result = createResult(status, void 0, {
|
|
1093
1109
|
codeDesc,
|
|
1094
|
-
startTime: /* @__PURE__ */ new Date()
|
|
1095
|
-
backtrace
|
|
1110
|
+
startTime: /* @__PURE__ */ new Date()
|
|
1096
1111
|
});
|
|
1112
|
+
delete result.message;
|
|
1113
|
+
if (suppressionMessage) result.message = `Suppressed: ${suppressionMessage}`;
|
|
1114
|
+
if (backtrace.length > 0) result.backtrace = backtrace;
|
|
1115
|
+
return result;
|
|
1097
1116
|
}
|
|
1098
1117
|
//#endregion
|
|
1099
1118
|
//#region converters/junit-to-hdf/typescript/converter.ts
|
|
1100
1119
|
const DEFAULT_NIST = ["SA-11"];
|
|
1101
|
-
|
|
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
|
+
}
|
|
1102
1128
|
const ARRAY_TAGS$2 = ["testsuite", "testcase"];
|
|
1103
1129
|
/**
|
|
1104
1130
|
* Converts JUnit XML test results to HDF format.
|
|
1105
1131
|
*/
|
|
1106
|
-
async function convertJunitToHdf(input) {
|
|
1132
|
+
async function convertJunitToHdf(input, converterVersion = "1.0.0") {
|
|
1107
1133
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
1108
1134
|
validateInputSize(input, "junit");
|
|
1109
1135
|
const { suites, name } = parseJUnitXML(input);
|
|
@@ -1112,7 +1138,7 @@ async function convertJunitToHdf(input) {
|
|
|
1112
1138
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
|
|
1113
1139
|
return buildHdfResults({
|
|
1114
1140
|
generatorName: "junit-to-hdf",
|
|
1115
|
-
converterVersion
|
|
1141
|
+
converterVersion,
|
|
1116
1142
|
toolName: "JUnit XML",
|
|
1117
1143
|
toolFormat: "XML",
|
|
1118
1144
|
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
|
|
@@ -1134,14 +1160,15 @@ function parseJUnitXML(input) {
|
|
|
1134
1160
|
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
|
|
1135
1161
|
if (parsed.testsuites) return {
|
|
1136
1162
|
suites: parsed.testsuites.testsuite ?? [],
|
|
1137
|
-
name: parsed.testsuites.name
|
|
1163
|
+
name: parsed.testsuites.name ? decodeXmlEntities$5(parsed.testsuites.name) : "JUnit Test Results"
|
|
1138
1164
|
};
|
|
1139
1165
|
if (parsed.testsuite) {
|
|
1140
1166
|
const suite = parsed.testsuite;
|
|
1141
1167
|
const suites = Array.isArray(suite) ? suite : [suite];
|
|
1168
|
+
const suiteName = suites[0]?.name;
|
|
1142
1169
|
return {
|
|
1143
1170
|
suites,
|
|
1144
|
-
name:
|
|
1171
|
+
name: suiteName ? decodeXmlEntities$5(suiteName) : "JUnit Test Results"
|
|
1145
1172
|
};
|
|
1146
1173
|
}
|
|
1147
1174
|
throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
|
|
@@ -1163,28 +1190,29 @@ function buildRequirements(suites, scanTime) {
|
|
|
1163
1190
|
function testCaseToRequirement(tc, scanTime) {
|
|
1164
1191
|
const id = buildID(tc);
|
|
1165
1192
|
const { status, message } = resolveStatus(tc);
|
|
1166
|
-
const
|
|
1193
|
+
const result = {
|
|
1194
|
+
status,
|
|
1167
1195
|
codeDesc: buildCodeDesc$9(tc),
|
|
1168
1196
|
startTime: scanTime
|
|
1169
1197
|
};
|
|
1198
|
+
if (message !== void 0) result.message = message;
|
|
1170
1199
|
if (tc.time) {
|
|
1171
1200
|
const parsed = parseFloat(tc.time);
|
|
1172
|
-
if (!isNaN(parsed))
|
|
1201
|
+
if (!isNaN(parsed)) result.runTime = parsed;
|
|
1173
1202
|
}
|
|
1174
|
-
const
|
|
1175
|
-
const
|
|
1203
|
+
const name = decodeXmlEntities$5(tc.name);
|
|
1204
|
+
const req = createRequirement(id, name, [{
|
|
1176
1205
|
label: "default",
|
|
1177
|
-
data: `JUnit test: ${
|
|
1178
|
-
}];
|
|
1179
|
-
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 } });
|
|
1180
1208
|
const controlType = deriveControlTypeFromTags(DEFAULT_NIST);
|
|
1181
1209
|
if (controlType !== void 0) req.controlType = controlType;
|
|
1182
1210
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
1183
1211
|
return req;
|
|
1184
1212
|
}
|
|
1185
1213
|
function buildID(tc) {
|
|
1186
|
-
if (tc.classname) return `${tc.classname}.${tc.name}`;
|
|
1187
|
-
return tc.name;
|
|
1214
|
+
if (tc.classname) return `${decodeXmlEntities$5(tc.classname)}.${decodeXmlEntities$5(tc.name)}`;
|
|
1215
|
+
return decodeXmlEntities$5(tc.name);
|
|
1188
1216
|
}
|
|
1189
1217
|
function resolveStatus(tc) {
|
|
1190
1218
|
if (tc.failure) {
|
|
@@ -1205,7 +1233,7 @@ function resolveStatus(tc) {
|
|
|
1205
1233
|
const skipped = typeof tc.skipped === "object" ? tc.skipped : null;
|
|
1206
1234
|
if (skipped?.message) return {
|
|
1207
1235
|
status: ResultStatus.NotReviewed,
|
|
1208
|
-
message: `Skipped: ${skipped.message}`
|
|
1236
|
+
message: `Skipped: ${decodeXmlEntities$5(skipped.message)}`
|
|
1209
1237
|
};
|
|
1210
1238
|
return {
|
|
1211
1239
|
status: ResultStatus.NotReviewed,
|
|
@@ -1216,24 +1244,40 @@ function resolveStatus(tc) {
|
|
|
1216
1244
|
}
|
|
1217
1245
|
function buildFailureMessage(message, typeName, body) {
|
|
1218
1246
|
let result = "";
|
|
1219
|
-
if (typeName) result = `${typeName}: `;
|
|
1220
|
-
result += message;
|
|
1221
|
-
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);
|
|
1222
1250
|
return result;
|
|
1223
1251
|
}
|
|
1224
1252
|
function buildCodeDesc$9(tc) {
|
|
1225
|
-
if (tc.classname) return `${tc.classname} :: ${tc.name}`;
|
|
1226
|
-
return tc.name;
|
|
1253
|
+
if (tc.classname) return `${decodeXmlEntities$5(tc.classname)} :: ${decodeXmlEntities$5(tc.name)}`;
|
|
1254
|
+
return decodeXmlEntities$5(tc.name);
|
|
1227
1255
|
}
|
|
1228
1256
|
function noFindingsTarget(baselineName, suites) {
|
|
1229
1257
|
if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
|
|
1230
|
-
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);
|
|
1231
1259
|
return "JUnit test suite";
|
|
1232
1260
|
}
|
|
1233
1261
|
//#endregion
|
|
1234
1262
|
//#region converters/xccdf-results-to-hdf/typescript/converter.ts
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
+
}
|
|
1237
1281
|
/** Tags that must always be parsed as arrays even if only one element exists. */
|
|
1238
1282
|
const ARRAY_TAGS$1 = [
|
|
1239
1283
|
"Group",
|
|
@@ -1282,18 +1326,18 @@ function parseStartTime(raw) {
|
|
|
1282
1326
|
}
|
|
1283
1327
|
return /* @__PURE__ */ new Date();
|
|
1284
1328
|
}
|
|
1285
|
-
async function convertXccdfResultsToHdf(input) {
|
|
1329
|
+
async function convertXccdfResultsToHdf(input, converterVersion = "1.0.0") {
|
|
1286
1330
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
1287
1331
|
validateInputSize(input, "xccdf-results");
|
|
1288
|
-
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1);
|
|
1332
|
+
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1, { trimValues: false });
|
|
1289
1333
|
const arfParsed = parsed;
|
|
1290
|
-
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);
|
|
1291
1335
|
const benchmark = parsed.Benchmark;
|
|
1292
1336
|
if (!benchmark) throw new Error("Input is not an XCCDF document: expected <Benchmark> root element");
|
|
1293
1337
|
if (!benchmark.TestResult) throw new Error("Input has no TestResult elements — this is a benchmark. Use 'xccdf-benchmark' or 'xccdf' instead");
|
|
1294
|
-
return convertBenchmarkResultsToHdf(benchmark, input);
|
|
1338
|
+
return convertBenchmarkResultsToHdf(benchmark, input, converterVersion);
|
|
1295
1339
|
}
|
|
1296
|
-
async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
1340
|
+
async function convertBenchmarkResultsToHdf(benchmark, rawInput, converterVersion) {
|
|
1297
1341
|
const testResult = benchmark.TestResult;
|
|
1298
1342
|
const ruleIndex = buildRuleIndex(benchmark);
|
|
1299
1343
|
const ruleResults = testResult["rule-result"] ?? [];
|
|
@@ -1307,32 +1351,35 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
1307
1351
|
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
|
|
1308
1352
|
}
|
|
1309
1353
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
1310
|
-
const
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
if (start !== void 0 && end !== void 0 && end >= start) durationSeconds = (end - start) / 1e3;
|
|
1318
|
-
}
|
|
1319
|
-
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({
|
|
1320
1361
|
baselines: [baseline],
|
|
1321
1362
|
generator: {
|
|
1322
1363
|
name: "xccdf-results-to-hdf",
|
|
1323
|
-
version:
|
|
1364
|
+
version: converterVersion
|
|
1324
1365
|
},
|
|
1325
1366
|
tool: {
|
|
1326
|
-
name: "XCCDF
|
|
1327
|
-
format: "
|
|
1367
|
+
name: "XCCDF",
|
|
1368
|
+
format: "XCCDF"
|
|
1328
1369
|
},
|
|
1329
|
-
components,
|
|
1330
|
-
timestamp
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
return serializeHdf(hdf);
|
|
1370
|
+
components: buildTargets(testResult),
|
|
1371
|
+
timestamp: scanTime,
|
|
1372
|
+
statistics: { duration: calculateDuration(testResult) }
|
|
1373
|
+
});
|
|
1334
1374
|
}
|
|
1335
|
-
|
|
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) {
|
|
1336
1383
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
1337
1384
|
const benchmark = findBenchmarkInArf(arc);
|
|
1338
1385
|
const ruleIndex = benchmark ? buildRuleIndex(benchmark) : /* @__PURE__ */ new Map();
|
|
@@ -1347,16 +1394,9 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1347
1394
|
for (const report of arc.reports?.report ?? []) {
|
|
1348
1395
|
const testResult = report.content?.TestResult;
|
|
1349
1396
|
if (!testResult?.id) continue;
|
|
1350
|
-
if (testResult["start-time"] && !firstTimestamp) {
|
|
1351
|
-
const t = parseTimestamp(testResult["start-time"]);
|
|
1352
|
-
if (t) firstTimestamp = t;
|
|
1353
|
-
}
|
|
1354
|
-
if (testResult["start-time"] && testResult["end-time"]) {
|
|
1355
|
-
const start = parseTimestamp(testResult["start-time"])?.getTime();
|
|
1356
|
-
const end = parseTimestamp(testResult["end-time"])?.getTime();
|
|
1357
|
-
if (start !== void 0 && end !== void 0 && end >= start) totalDuration += (end - start) / 1e3;
|
|
1358
|
-
}
|
|
1359
1397
|
const scanTime = parseStartTime(testResult["start-time"]);
|
|
1398
|
+
if (!firstTimestamp) firstTimestamp = scanTime;
|
|
1399
|
+
totalDuration += calculateDuration(testResult);
|
|
1360
1400
|
const ruleResults = testResult["rule-result"] ?? [];
|
|
1361
1401
|
const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
|
|
1362
1402
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -1367,9 +1407,15 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1367
1407
|
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
|
|
1368
1408
|
}
|
|
1369
1409
|
let baselineName = "";
|
|
1370
|
-
if (benchmark) baselineName = extractText(benchmark.title) || "";
|
|
1371
|
-
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 || "";
|
|
1372
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
|
+
}
|
|
1373
1419
|
baselines.push(baseline);
|
|
1374
1420
|
const reportTargets = buildTargets(testResult);
|
|
1375
1421
|
const target = reportTargets[0];
|
|
@@ -1383,21 +1429,20 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1383
1429
|
components.push(...reportTargets);
|
|
1384
1430
|
}
|
|
1385
1431
|
if (baselines.length === 0) throw new Error("ARF document contains no XCCDF TestResult reports");
|
|
1386
|
-
|
|
1432
|
+
return serializeHdf({
|
|
1387
1433
|
baselines,
|
|
1388
1434
|
generator: {
|
|
1389
1435
|
name: "xccdf-results-to-hdf",
|
|
1390
|
-
version:
|
|
1436
|
+
version: converterVersion
|
|
1391
1437
|
},
|
|
1392
1438
|
tool: {
|
|
1393
1439
|
name: "ARF",
|
|
1394
1440
|
format: "ARF"
|
|
1395
1441
|
},
|
|
1396
1442
|
components,
|
|
1397
|
-
timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
return serializeHdf(hdf);
|
|
1443
|
+
timestamp: firstTimestamp ?? /* @__PURE__ */ new Date(),
|
|
1444
|
+
statistics: { duration: totalDuration }
|
|
1445
|
+
});
|
|
1401
1446
|
}
|
|
1402
1447
|
/**
|
|
1403
1448
|
* Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
|
|
@@ -1443,11 +1488,7 @@ function buildRuleIndex(benchmark) {
|
|
|
1443
1488
|
const index = /* @__PURE__ */ new Map();
|
|
1444
1489
|
const topRules = benchmark.Rule ?? [];
|
|
1445
1490
|
for (const rule of topRules) if (rule.id) index.set(rule.id, rule);
|
|
1446
|
-
const
|
|
1447
|
-
for (const group of groups) {
|
|
1448
|
-
const rules = group.Rule ?? [];
|
|
1449
|
-
for (const rule of rules) if (rule.id) index.set(rule.id, rule);
|
|
1450
|
-
}
|
|
1491
|
+
for (const { rule } of flattenGroups(benchmark.Group ?? [])) index.set(rule.id, rule);
|
|
1451
1492
|
return index;
|
|
1452
1493
|
}
|
|
1453
1494
|
/**
|
|
@@ -1456,39 +1497,29 @@ function buildRuleIndex(benchmark) {
|
|
|
1456
1497
|
function ruleResultToRequirement(rr, ruleIndex, scanTime) {
|
|
1457
1498
|
const ruleId = rr.idref ?? "";
|
|
1458
1499
|
const ruleDef = ruleIndex.get(ruleId);
|
|
1459
|
-
const id =
|
|
1460
|
-
const title = extractText(ruleDef?.title) ||
|
|
1461
|
-
const severity = rr.severity
|
|
1462
|
-
const impact = severity ? severityToImpact(severity) : .5;
|
|
1463
|
-
const descriptions = [
|
|
1464
|
-
const rawDesc = extractText(ruleDef?.description);
|
|
1465
|
-
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 = [{
|
|
1466
1505
|
label: "default",
|
|
1467
|
-
data: extractVulnDiscussion(
|
|
1468
|
-
}
|
|
1506
|
+
data: stripHTML(extractVulnDiscussion(extractText(ruleDef?.description)))
|
|
1507
|
+
}];
|
|
1469
1508
|
const fixtext = extractFixtext(ruleDef?.fixtext);
|
|
1470
1509
|
if (fixtext) descriptions.push({
|
|
1471
1510
|
label: "fix",
|
|
1472
|
-
data: fixtext
|
|
1473
|
-
});
|
|
1474
|
-
if (descriptions.length === 0) descriptions.push({
|
|
1475
|
-
label: "default",
|
|
1476
|
-
data: ""
|
|
1511
|
+
data: stripHTML(fixtext)
|
|
1477
1512
|
});
|
|
1478
|
-
const
|
|
1513
|
+
const xccdfResult = (rr.result ?? "").trim().toLowerCase();
|
|
1514
|
+
const status = STATUS_MAP[xccdfResult] ?? ResultStatus.Error;
|
|
1479
1515
|
const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
|
|
1480
|
-
const result =
|
|
1481
|
-
|
|
1516
|
+
const result = {
|
|
1517
|
+
status,
|
|
1518
|
+
codeDesc: `XCCDF rule ${ruleId}`,
|
|
1482
1519
|
startTime: perRuleTime ?? scanTime
|
|
1483
|
-
}
|
|
1484
|
-
const tags =
|
|
1485
|
-
|
|
1486
|
-
const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
|
|
1487
|
-
if (cciIds.length > 0) {
|
|
1488
|
-
tags["cci"] = cciIds;
|
|
1489
|
-
nistTags = [...new Set(cciIds.flatMap((cci) => getCCINistMappings(cci) ?? []))];
|
|
1490
|
-
if (nistTags.length > 0) tags["nist"] = nistTags;
|
|
1491
|
-
}
|
|
1520
|
+
};
|
|
1521
|
+
const tags = buildCciNistTags(extractCCIs([...rr.ident ?? [], ...ruleDef?.ident ?? []]));
|
|
1522
|
+
const nistTags = tags["nist"];
|
|
1492
1523
|
const req = createRequirement(id, title, descriptions, impact, [result], { tags });
|
|
1493
1524
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
1494
1525
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -1519,19 +1550,29 @@ function buildTargets(testResult) {
|
|
|
1519
1550
|
const addresses = testResult["target-address"] ?? [];
|
|
1520
1551
|
const target = {
|
|
1521
1552
|
name: targetName,
|
|
1522
|
-
type: TargetType.Host
|
|
1523
|
-
labels: {}
|
|
1553
|
+
type: TargetType.Host
|
|
1524
1554
|
};
|
|
1525
1555
|
if (addresses.length > 0) target.ipAddress = addresses[0];
|
|
1526
1556
|
return [target];
|
|
1527
1557
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
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
|
+
}
|
|
1531
1572
|
function extractText(field) {
|
|
1532
1573
|
if (field === void 0 || field === null) return "";
|
|
1533
|
-
if (typeof field === "string") return field;
|
|
1534
|
-
return field["#text"] ?? "";
|
|
1574
|
+
if (typeof field === "string") return decodeXmlEntities$4(field);
|
|
1575
|
+
return decodeXmlEntities$4(field["#text"] ?? "");
|
|
1535
1576
|
}
|
|
1536
1577
|
/**
|
|
1537
1578
|
* Extract version string from a version field.
|
|
@@ -1546,8 +1587,8 @@ function extractVersion$1(field) {
|
|
|
1546
1587
|
*/
|
|
1547
1588
|
function extractFixtext(field) {
|
|
1548
1589
|
if (field === void 0 || field === null) return "";
|
|
1549
|
-
if (typeof field === "string") return field;
|
|
1550
|
-
return field["#text"] ?? "";
|
|
1590
|
+
if (typeof field === "string") return decodeXmlEntities$4(field);
|
|
1591
|
+
return decodeXmlEntities$4(field["#text"] ?? "");
|
|
1551
1592
|
}
|
|
1552
1593
|
/**
|
|
1553
1594
|
* Extract the VulnDiscussion text from an XCCDF description that contains
|
|
@@ -1556,16 +1597,38 @@ function extractFixtext(field) {
|
|
|
1556
1597
|
*/
|
|
1557
1598
|
function extractVulnDiscussion(description) {
|
|
1558
1599
|
const match = description.match(/<VulnDiscussion>([\s\S]*?)<\/VulnDiscussion>/);
|
|
1559
|
-
|
|
1560
|
-
const entityMatch = description.match(/<VulnDiscussion>([\s\S]*?)<\/VulnDiscussion>/);
|
|
1561
|
-
if (entityMatch) return entityMatch[1].trim();
|
|
1562
|
-
return description;
|
|
1600
|
+
return match ? match[1] : description;
|
|
1563
1601
|
}
|
|
1564
1602
|
/**
|
|
1565
|
-
* Extract CCI identifiers from ident elements
|
|
1603
|
+
* Extract CCI identifiers from ident elements, deduplicated in first-seen order.
|
|
1566
1604
|
*/
|
|
1567
1605
|
function extractCCIs(idents) {
|
|
1568
|
-
|
|
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}`;
|
|
1569
1632
|
}
|
|
1570
1633
|
//#endregion
|
|
1571
1634
|
//#region shared/typescript/checklist/status.ts
|
|
@@ -1753,6 +1816,8 @@ function cklVulnToModel(v) {
|
|
|
1753
1816
|
status: parseStatus(v.STATUS),
|
|
1754
1817
|
findingDetails: str(v.FINDING_DETAILS),
|
|
1755
1818
|
comments: str(v.COMMENTS),
|
|
1819
|
+
severityOverride: str(v.SEVERITY_OVERRIDE),
|
|
1820
|
+
severityJustification: str(v.SEVERITY_JUSTIFICATION),
|
|
1756
1821
|
extra
|
|
1757
1822
|
};
|
|
1758
1823
|
}
|
|
@@ -1841,7 +1906,9 @@ function modelVulnToCkl(v) {
|
|
|
1841
1906
|
STIG_DATA: stigData,
|
|
1842
1907
|
STATUS: statusToCkl(v.status),
|
|
1843
1908
|
FINDING_DETAILS: v.findingDetails ?? "",
|
|
1844
|
-
COMMENTS: v.comments ?? ""
|
|
1909
|
+
COMMENTS: v.comments ?? "",
|
|
1910
|
+
SEVERITY_OVERRIDE: v.severityOverride ?? "",
|
|
1911
|
+
SEVERITY_JUSTIFICATION: v.severityJustification ?? ""
|
|
1845
1912
|
};
|
|
1846
1913
|
}
|
|
1847
1914
|
//#endregion
|
|
@@ -2010,7 +2077,7 @@ function stigToBaseline(s, resultsChecksum, scanTime) {
|
|
|
2010
2077
|
}
|
|
2011
2078
|
function vulnToRequirement(v, scanTime) {
|
|
2012
2079
|
const severity = (v.severity ?? "").toLowerCase();
|
|
2013
|
-
const impact = severity ? severityToImpact(severity) : .5;
|
|
2080
|
+
const impact = severity ? severityToImpact$1(severity) : .5;
|
|
2014
2081
|
const descriptions = [{
|
|
2015
2082
|
label: "default",
|
|
2016
2083
|
data: stripHTML(v.vulnDiscuss ?? "")
|
|
@@ -2023,7 +2090,7 @@ function vulnToRequirement(v, scanTime) {
|
|
|
2023
2090
|
label: "fix",
|
|
2024
2091
|
data: stripHTML(v.fixText)
|
|
2025
2092
|
});
|
|
2026
|
-
const message =
|
|
2093
|
+
const message = (v.findingDetails ?? "").trim();
|
|
2027
2094
|
const result = createResult(statusToHdf(v.status), message, {
|
|
2028
2095
|
codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
|
|
2029
2096
|
startTime: scanTime
|
|
@@ -2035,12 +2102,13 @@ function vulnToRequirement(v, scanTime) {
|
|
|
2035
2102
|
nistTags = [...new Set(v.ccis.flatMap((c) => getCCINistMappings(c) ?? []))].sort();
|
|
2036
2103
|
tags["nist"] = nistTags;
|
|
2037
2104
|
} else tags["nist"] = [];
|
|
2038
|
-
setIf(tags, "rid", v.ruleID);
|
|
2039
|
-
setIf(tags, "stig_id", v.ruleVer);
|
|
2040
|
-
setIf(tags, "gtitle", v.groupTitle);
|
|
2041
|
-
setIf(tags, "group_id", v.groupID);
|
|
2042
|
-
setIf(tags, "weight", v.weight);
|
|
2043
|
-
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);
|
|
2044
2112
|
if (v.legacyIDs && v.legacyIDs.length) tags["legacy_ids"] = v.legacyIDs;
|
|
2045
2113
|
if (v.extra && Object.keys(v.extra).length > 0) tags["cklMetadata"] = { ...v.extra };
|
|
2046
2114
|
const req = createRequirement(v.vulnNum, v.ruleTitle ?? v.vulnNum, descriptions, impact, [result], { tags });
|
|
@@ -2055,6 +2123,7 @@ function assetToComponent(a) {
|
|
|
2055
2123
|
name: a.hostName || a.hostFQDN || a.hostIP || "",
|
|
2056
2124
|
type: TargetType.Host
|
|
2057
2125
|
};
|
|
2126
|
+
if (a.hostName) c.hostname = a.hostName;
|
|
2058
2127
|
if (a.hostIP) c.ipAddress = a.hostIP;
|
|
2059
2128
|
if (a.hostFQDN) c.fqdn = a.hostFQDN;
|
|
2060
2129
|
if (a.hostMAC) c.macAddress = a.hostMAC;
|
|
@@ -2064,30 +2133,30 @@ function rootExtensions(cl) {
|
|
|
2064
2133
|
const ext = { checklistFormat: cl.format || "ckl" };
|
|
2065
2134
|
if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
|
|
2066
2135
|
const ax = {};
|
|
2067
|
-
setIf(ax, "role", cl.asset.role);
|
|
2068
|
-
setIf(ax, "assetType", cl.asset.assetType);
|
|
2069
|
-
setIf(ax, "marking", cl.asset.marking);
|
|
2070
|
-
setIf(ax, "targetKey", cl.asset.targetKey);
|
|
2071
|
-
setIf(ax, "techArea", cl.asset.techArea);
|
|
2072
|
-
setIf(ax, "targetComment", cl.asset.targetComment);
|
|
2073
|
-
setIf(ax, "webDbSite", cl.asset.webDBSite);
|
|
2074
|
-
setIf(ax, "webDbInstance", cl.asset.webDBInstance);
|
|
2075
|
-
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);
|
|
2076
2145
|
if (cl.asset.webOrDatabase) ax["webOrDatabase"] = true;
|
|
2077
2146
|
if (Object.keys(ax).length > 0) ext["assetExtras"] = ax;
|
|
2078
2147
|
return ext;
|
|
2079
2148
|
}
|
|
2080
2149
|
function baselineExtensions(s) {
|
|
2081
2150
|
const ext = {};
|
|
2082
|
-
setIf(ext, "stigid", s.stigID);
|
|
2083
|
-
setIf(ext, "uuid", s.uuid);
|
|
2084
|
-
setIf(ext, "releaseInfo", s.releaseInfo);
|
|
2085
|
-
setIf(ext, "displayName", s.displayName);
|
|
2086
|
-
setIf(ext, "referenceIdentifier", s.referenceIdentifier);
|
|
2087
|
-
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);
|
|
2088
2157
|
return ext;
|
|
2089
2158
|
}
|
|
2090
|
-
function setIf(m, key, val) {
|
|
2159
|
+
function setIf$1(m, key, val) {
|
|
2091
2160
|
if (val) m[key] = val;
|
|
2092
2161
|
}
|
|
2093
2162
|
//#endregion
|
|
@@ -2099,7 +2168,7 @@ function setIf(m, key, val) {
|
|
|
2099
2168
|
* fields are synthesized best-effort so any HDF yields a valid checklist.
|
|
2100
2169
|
*/
|
|
2101
2170
|
function hdfToChecklist(input) {
|
|
2102
|
-
const hdf =
|
|
2171
|
+
const hdf = parseHdf(input);
|
|
2103
2172
|
if (!hdf || !Array.isArray(hdf.baselines) || hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
|
|
2104
2173
|
const ext = hdf.extensions ?? {};
|
|
2105
2174
|
const format = strVal(ext, "checklistFormat") || "ckl";
|
|
@@ -2117,7 +2186,8 @@ function buildAsset(hdf, ext) {
|
|
|
2117
2186
|
const asset = {};
|
|
2118
2187
|
const comp = hdf.components?.[0];
|
|
2119
2188
|
if (comp) {
|
|
2120
|
-
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;
|
|
2121
2191
|
asset.hostIP = comp.ipAddress;
|
|
2122
2192
|
asset.hostFQDN = comp.fqdn;
|
|
2123
2193
|
asset.hostMAC = comp.macAddress;
|
|
@@ -2171,6 +2241,7 @@ function requirementToVuln(req) {
|
|
|
2171
2241
|
legacyIDs: strSlice(tags, "legacy_ids"),
|
|
2172
2242
|
status: statusFromHdf(req.results?.[0]?.status),
|
|
2173
2243
|
findingDetails: req.results?.[0]?.message,
|
|
2244
|
+
comments: strVal(tags, "comments"),
|
|
2174
2245
|
extra: extractCklMetadata(tags)
|
|
2175
2246
|
};
|
|
2176
2247
|
}
|
|
@@ -2332,11 +2403,15 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
|
2332
2403
|
label: "default",
|
|
2333
2404
|
data: rep.description
|
|
2334
2405
|
}];
|
|
2335
|
-
const results = vulns.map((vuln) =>
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
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 });
|
|
2340
2415
|
const controlType = deriveControlTypeFromTags(nist);
|
|
2341
2416
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2342
2417
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -2389,12 +2464,12 @@ function convertSingleProject(report, resultsChecksum, scanTime) {
|
|
|
2389
2464
|
* @param input - Snyk JSON or SARIF string
|
|
2390
2465
|
* @returns HDF JSON string
|
|
2391
2466
|
*/
|
|
2392
|
-
async function convertSnykToHdf(input) {
|
|
2467
|
+
async function convertSnykToHdf(input, converterVersion = "1.0.0") {
|
|
2393
2468
|
if (!input || input.trim().length === 0) throw new Error("snyk: empty input");
|
|
2394
2469
|
validateInputSize(input, "snyk");
|
|
2395
2470
|
registerAllFingerprints();
|
|
2396
2471
|
const detected = detectConverter(input);
|
|
2397
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
2472
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
2398
2473
|
const resultsChecksum = await inputChecksum(input);
|
|
2399
2474
|
const scanTime = /* @__PURE__ */ new Date();
|
|
2400
2475
|
const parsed = parseJSON(input);
|
|
@@ -2413,7 +2488,7 @@ async function convertSnykToHdf(input) {
|
|
|
2413
2488
|
}
|
|
2414
2489
|
return buildHdfResults({
|
|
2415
2490
|
generatorName: "snyk-to-hdf",
|
|
2416
|
-
converterVersion
|
|
2491
|
+
converterVersion,
|
|
2417
2492
|
toolName: "Snyk",
|
|
2418
2493
|
toolFormat: "JSON",
|
|
2419
2494
|
baselines,
|
|
@@ -2458,11 +2533,12 @@ function getFixInfo(fix) {
|
|
|
2458
2533
|
function getCVSSInfo(vuln, relatedVulns) {
|
|
2459
2534
|
const cvssData = {};
|
|
2460
2535
|
if (vuln.cvss && vuln.cvss.length > 0) cvssData.primary = vuln.cvss;
|
|
2461
|
-
|
|
2536
|
+
const related = (relatedVulns ?? []).filter((r) => r.cvss && r.cvss.length > 0).map((r) => ({
|
|
2462
2537
|
id: r.id,
|
|
2463
2538
|
dataSource: r.dataSource,
|
|
2464
2539
|
cvss: r.cvss
|
|
2465
2540
|
}));
|
|
2541
|
+
if (related.length > 0) cvssData.related = related;
|
|
2466
2542
|
return JSON.stringify(cvssData);
|
|
2467
2543
|
}
|
|
2468
2544
|
function getReferences(vuln, relatedVulns) {
|
|
@@ -2634,7 +2710,7 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
2634
2710
|
if (kev) requirement.kev = kev;
|
|
2635
2711
|
return requirement;
|
|
2636
2712
|
}
|
|
2637
|
-
async function convertGrypeToHdf(input) {
|
|
2713
|
+
async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
|
|
2638
2714
|
validateInputSize(input, "grype");
|
|
2639
2715
|
const resultsChecksum = await inputChecksum(input);
|
|
2640
2716
|
const grypeData = parseJSON(input);
|
|
@@ -2655,8 +2731,8 @@ async function convertGrypeToHdf(input) {
|
|
|
2655
2731
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
2656
2732
|
const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
|
|
2657
2733
|
return buildHdfResults({
|
|
2658
|
-
generatorName:
|
|
2659
|
-
converterVersion
|
|
2734
|
+
generatorName: "grype-to-hdf",
|
|
2735
|
+
converterVersion,
|
|
2660
2736
|
toolName: "Grype",
|
|
2661
2737
|
toolVersion: grypeData.descriptor?.version,
|
|
2662
2738
|
baselines: [baseline],
|
|
@@ -2671,7 +2747,6 @@ async function convertGrypeToHdf(input) {
|
|
|
2671
2747
|
//#region converters/nessus-to-hdf/typescript/converter.ts
|
|
2672
2748
|
const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
|
|
2673
2749
|
const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
2674
|
-
const converterVersion = "1.0.0";
|
|
2675
2750
|
const IMPACT_MAPPING$7 = {
|
|
2676
2751
|
"4": .9,
|
|
2677
2752
|
"3": .7,
|
|
@@ -2688,20 +2763,47 @@ const IMPACT_MAPPING$7 = {
|
|
|
2688
2763
|
function parseHtml(html) {
|
|
2689
2764
|
return html.replace(/<[^>]*>/g, "").trim();
|
|
2690
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
|
+
}
|
|
2691
2793
|
/**
|
|
2692
2794
|
* Convert Nessus XML scan results to HDF format
|
|
2693
2795
|
*/
|
|
2694
|
-
async function convertNessusToHdf(nessusXml) {
|
|
2796
|
+
async function convertNessusToHdf(nessusXml, converterVersion = "1.0.0") {
|
|
2695
2797
|
validateInputSize(nessusXml, "nessus");
|
|
2696
2798
|
const resultsChecksum = await inputChecksum(nessusXml);
|
|
2697
|
-
const parsed = parseXmlWithArrays(nessusXml, [
|
|
2799
|
+
const parsed = decodeEntitiesDeep(parseXmlWithArrays(nessusXml, [
|
|
2698
2800
|
"preference",
|
|
2699
2801
|
"tag",
|
|
2700
2802
|
"ReportItem",
|
|
2701
2803
|
"ReportHost",
|
|
2702
2804
|
"cwe",
|
|
2703
2805
|
"cve"
|
|
2704
|
-
]);
|
|
2806
|
+
]));
|
|
2705
2807
|
const policyName = parsed.NessusClientData_v2.Policy.policyName;
|
|
2706
2808
|
const version = extractVersion(parsed);
|
|
2707
2809
|
const reportHosts = parsed.NessusClientData_v2.Report.ReportHost;
|
|
@@ -2984,7 +3086,7 @@ function buildTags$2(item, isCompliance) {
|
|
|
2984
3086
|
const cciTags = parseComplianceRef(item["compliance-reference"], "CCI");
|
|
2985
3087
|
tags.cci = cciTags;
|
|
2986
3088
|
const mappedControls = cciTags.flatMap((cci) => getCCINistMappings(cci) ?? []);
|
|
2987
|
-
tags.nist = [...new Set(mappedControls)];
|
|
3089
|
+
tags.nist = [...new Set(mappedControls)].sort();
|
|
2988
3090
|
} else {
|
|
2989
3091
|
const nistControls = getNessusNistControl(item["pluginFamily"], item["pluginID"]);
|
|
2990
3092
|
tags.nist = nistControls ? nistControls.split("|") : [];
|
|
@@ -3009,7 +3111,7 @@ function buildRefs(item) {
|
|
|
3009
3111
|
function buildResult$1(item, host, isCompliance) {
|
|
3010
3112
|
const status = getStatus$3(item, isCompliance);
|
|
3011
3113
|
const codeDesc = getCodeDesc(item);
|
|
3012
|
-
const message = item
|
|
3114
|
+
const message = isCompliance && item["compliance-actual-value"] ? item["compliance-actual-value"] : item.plugin_output;
|
|
3013
3115
|
const startTimeStr = getHostPropertyValue(host, "HOST_START");
|
|
3014
3116
|
const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
|
|
3015
3117
|
return {
|
|
@@ -3047,6 +3149,7 @@ function convertReportHostToTarget(host) {
|
|
|
3047
3149
|
name: hostName,
|
|
3048
3150
|
type: TargetType.Host
|
|
3049
3151
|
};
|
|
3152
|
+
if (hostProps["hostname"]) target.hostname = hostProps["hostname"];
|
|
3050
3153
|
if (isFQDN(hostName)) target.fqdn = hostName;
|
|
3051
3154
|
const hostIp = hostProps["host-ip"];
|
|
3052
3155
|
if (hostIp) target.ipAddress = hostIp;
|
|
@@ -3055,7 +3158,6 @@ function convertReportHostToTarget(host) {
|
|
|
3055
3158
|
if (hostProps["os"]) target.osVersion = hostProps["os"];
|
|
3056
3159
|
if (hostProps["mac-address"]) target.macAddress = hostProps["mac-address"].split("\n")[0];
|
|
3057
3160
|
if (hostProps["host-fqdn"]) target.fqdn = hostProps["host-fqdn"];
|
|
3058
|
-
target.labels = {};
|
|
3059
3161
|
return target;
|
|
3060
3162
|
}
|
|
3061
3163
|
function isFQDN(s) {
|
|
@@ -3067,7 +3169,7 @@ function isIPAddress(s) {
|
|
|
3067
3169
|
//#endregion
|
|
3068
3170
|
//#region converters/sonarqube-to-hdf/typescript/converter.ts
|
|
3069
3171
|
/**
|
|
3070
|
-
*
|
|
3172
|
+
* Deprecated legacy severity to impact mapping.
|
|
3071
3173
|
* Canonical reference: heimdall2 sonarqube-mapper.ts IMPACT_MAPPING.
|
|
3072
3174
|
*/
|
|
3073
3175
|
const SEVERITY_IMPACT_MAPPING = {
|
|
@@ -3077,6 +3179,58 @@ const SEVERITY_IMPACT_MAPPING = {
|
|
|
3077
3179
|
MINOR: .3,
|
|
3078
3180
|
INFO: 0
|
|
3079
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
|
+
}
|
|
3080
3234
|
/**
|
|
3081
3235
|
* Default NIST tag for SonarQube findings without CWE mappings.
|
|
3082
3236
|
* SA-11 (Developer Security Testing and Evaluation) applies to all issue types —
|
|
@@ -3089,7 +3243,7 @@ const DEFAULT_NIST_TAGS = ["SA-11"];
|
|
|
3089
3243
|
* @param input - JSON string from SonarQube /api/issues/search endpoint
|
|
3090
3244
|
* @returns HDF JSON string
|
|
3091
3245
|
*/
|
|
3092
|
-
async function convertSonarqubeToHdf(input) {
|
|
3246
|
+
async function convertSonarqubeToHdf(input, converterVersion = "1.0.0") {
|
|
3093
3247
|
validateInputSize(input, "sonarqube");
|
|
3094
3248
|
const resultsChecksum = await inputChecksum(input);
|
|
3095
3249
|
const sonarData = parseJSON(input);
|
|
@@ -3108,12 +3262,13 @@ async function convertSonarqubeToHdf(input) {
|
|
|
3108
3262
|
if (!issuesByProject.has(projectKey)) issuesByProject.set(projectKey, []);
|
|
3109
3263
|
issuesByProject.get(projectKey).push(issue);
|
|
3110
3264
|
}
|
|
3265
|
+
const projectKeys = Array.from(issuesByProject.keys()).sort(byCodeUnit);
|
|
3111
3266
|
const baselines = [];
|
|
3112
|
-
for (const
|
|
3113
|
-
const baseline = convertProjectToBaseline(projectKey,
|
|
3267
|
+
for (const projectKey of projectKeys) {
|
|
3268
|
+
const baseline = convertProjectToBaseline(projectKey, issuesByProject.get(projectKey), componentMap, ruleMap, resultsChecksum);
|
|
3114
3269
|
baselines.push(baseline);
|
|
3115
3270
|
}
|
|
3116
|
-
let components =
|
|
3271
|
+
let components = projectKeys.map((projectKey) => ({
|
|
3117
3272
|
type: TargetType.Application,
|
|
3118
3273
|
name: projectKey
|
|
3119
3274
|
}));
|
|
@@ -3132,8 +3287,9 @@ async function convertSonarqubeToHdf(input) {
|
|
|
3132
3287
|
}
|
|
3133
3288
|
return buildHdfResults({
|
|
3134
3289
|
generatorName: "sonarqube-to-hdf",
|
|
3135
|
-
converterVersion
|
|
3290
|
+
converterVersion,
|
|
3136
3291
|
toolName: "SonarQube",
|
|
3292
|
+
toolVersion: sonarData.serverVersion || void 0,
|
|
3137
3293
|
baselines,
|
|
3138
3294
|
components,
|
|
3139
3295
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -3155,8 +3311,8 @@ function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, res
|
|
|
3155
3311
|
issuesByRule.get(ruleKey).push(issue);
|
|
3156
3312
|
}
|
|
3157
3313
|
const requirements = [];
|
|
3158
|
-
for (const
|
|
3159
|
-
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);
|
|
3160
3316
|
requirements.push(requirement);
|
|
3161
3317
|
}
|
|
3162
3318
|
return createMinimalBaseline(projectKey, requirements, {
|
|
@@ -3169,7 +3325,7 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
|
|
|
3169
3325
|
const title = rule?.name || ruleKey;
|
|
3170
3326
|
const description = extractDescription(rule);
|
|
3171
3327
|
const firstIssue = issues[0];
|
|
3172
|
-
const impact =
|
|
3328
|
+
const { severity, source: severitySource, impact } = selectSeverity(firstIssue);
|
|
3173
3329
|
const { cweIds, owaspTags, allTags } = extractTags(rule, issues);
|
|
3174
3330
|
const nistControls = mapCWEToNIST(cweIds, DEFAULT_NIST_TAGS);
|
|
3175
3331
|
const cciControls = nistToCci(nistControls);
|
|
@@ -3177,12 +3333,18 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
|
|
|
3177
3333
|
const issueWithLocation = issues.find((i) => i.line !== void 0);
|
|
3178
3334
|
const sourceLocation = issueWithLocation ? extractSourceLocation(issueWithLocation, componentMap) : void 0;
|
|
3179
3335
|
const options = { tags: {
|
|
3180
|
-
severity:
|
|
3336
|
+
severity: severity.toLowerCase(),
|
|
3337
|
+
severitySource,
|
|
3181
3338
|
type: firstIssue.type.toLowerCase(),
|
|
3182
3339
|
cwe: cweIds,
|
|
3183
3340
|
owasp: owaspTags,
|
|
3184
3341
|
nist: nistControls,
|
|
3185
3342
|
cci: cciControls,
|
|
3343
|
+
...severitySource === SEVERITY_SOURCE_MQR ? {
|
|
3344
|
+
legacySeverity: firstIssue.severity.toLowerCase(),
|
|
3345
|
+
impacts: firstIssue.impacts,
|
|
3346
|
+
...firstIssue.cleanCodeAttribute ? { cleanCodeAttribute: firstIssue.cleanCodeAttribute } : {}
|
|
3347
|
+
} : {},
|
|
3186
3348
|
...allTags
|
|
3187
3349
|
} };
|
|
3188
3350
|
if (sourceLocation) options.sourceLocation = sourceLocation;
|
|
@@ -3276,11 +3438,15 @@ function mapComplianceStatus(complianceType) {
|
|
|
3276
3438
|
default: return ResultStatus.NotReviewed;
|
|
3277
3439
|
}
|
|
3278
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
|
+
}
|
|
3279
3445
|
function buildNistTags$3(sourceIdentifier, ruleName) {
|
|
3280
|
-
const byIdentifier = getAwsConfigNistControlByIdentifier(sourceIdentifier);
|
|
3281
|
-
if (byIdentifier) return
|
|
3446
|
+
const byIdentifier = sourceIdentifier ? getAwsConfigNistControlByIdentifier(sourceIdentifier) : void 0;
|
|
3447
|
+
if (byIdentifier) return splitControls(byIdentifier);
|
|
3282
3448
|
const byName = getAwsConfigNistControlByName(ruleName);
|
|
3283
|
-
if (byName) return
|
|
3449
|
+
if (byName) return splitControls(byName);
|
|
3284
3450
|
return [];
|
|
3285
3451
|
}
|
|
3286
3452
|
/**
|
|
@@ -3320,16 +3486,27 @@ function buildResultMessage(codeDesc, annotation, status) {
|
|
|
3320
3486
|
if (status !== ResultStatus.Failed) return void 0;
|
|
3321
3487
|
return `(${codeDesc}): ${annotation || "Rule does not pass rule compliance"}`;
|
|
3322
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
|
+
}
|
|
3323
3496
|
function buildResult(r) {
|
|
3324
3497
|
const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
|
|
3325
3498
|
const status = mapComplianceStatus(r.ComplianceType);
|
|
3326
3499
|
const codeDesc = buildCodeDesc$7(q);
|
|
3327
3500
|
const message = buildResultMessage(codeDesc, r.Annotation, status);
|
|
3328
3501
|
const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
3329
|
-
|
|
3502
|
+
const runTime = computeRunTime(r.ConfigRuleInvokedTime, r.ResultRecordedTime);
|
|
3503
|
+
return {
|
|
3504
|
+
status,
|
|
3330
3505
|
codeDesc,
|
|
3331
|
-
|
|
3332
|
-
|
|
3506
|
+
startTime,
|
|
3507
|
+
...runTime !== void 0 ? { runTime } : {},
|
|
3508
|
+
...message !== void 0 ? { message } : {}
|
|
3509
|
+
};
|
|
3333
3510
|
}
|
|
3334
3511
|
/**
|
|
3335
3512
|
* Synthesizes a single HDF result for a Config rule whose live evaluation
|
|
@@ -3340,10 +3517,11 @@ function buildResult(r) {
|
|
|
3340
3517
|
*/
|
|
3341
3518
|
function buildNotApplicableResult(rule) {
|
|
3342
3519
|
const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
|
|
3343
|
-
return
|
|
3520
|
+
return {
|
|
3521
|
+
status: ResultStatus.NotApplicable,
|
|
3344
3522
|
codeDesc,
|
|
3345
3523
|
startTime: /* @__PURE__ */ new Date()
|
|
3346
|
-
}
|
|
3524
|
+
};
|
|
3347
3525
|
}
|
|
3348
3526
|
function buildRequirement$15(rule) {
|
|
3349
3527
|
const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
|
|
@@ -3376,7 +3554,7 @@ function buildRequirement$15(rule) {
|
|
|
3376
3554
|
* with get-compliance-details-by-config-rule
|
|
3377
3555
|
* @returns HDF JSON string
|
|
3378
3556
|
*/
|
|
3379
|
-
async function convertAwsConfigToHdf(input) {
|
|
3557
|
+
async function convertAwsConfigToHdf(input, converterVersion = "1.0.0") {
|
|
3380
3558
|
validateInputSize(input, "aws-config");
|
|
3381
3559
|
const resultsChecksum = await inputChecksum(input);
|
|
3382
3560
|
const data = parseJSON(input);
|
|
@@ -3397,7 +3575,7 @@ async function convertAwsConfigToHdf(input) {
|
|
|
3397
3575
|
const region = getRegion(firstArn);
|
|
3398
3576
|
return buildHdfResults({
|
|
3399
3577
|
generatorName: "aws-config-to-hdf",
|
|
3400
|
-
converterVersion
|
|
3578
|
+
converterVersion,
|
|
3401
3579
|
toolName: "AWS Config",
|
|
3402
3580
|
baselines: [baseline],
|
|
3403
3581
|
components: [{
|
|
@@ -3430,7 +3608,7 @@ function mapStatus$2(result) {
|
|
|
3430
3608
|
*/
|
|
3431
3609
|
function getImpact$8(severity) {
|
|
3432
3610
|
if (!severity) return .5;
|
|
3433
|
-
return severityToImpact(severity);
|
|
3611
|
+
return severityToImpact$1(severity);
|
|
3434
3612
|
}
|
|
3435
3613
|
/**
|
|
3436
3614
|
* Converts a single CheckovCheck to an HDF RequirementResult.
|
|
@@ -3440,10 +3618,12 @@ function checkToResult(check, scanTime) {
|
|
|
3440
3618
|
const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
|
|
3441
3619
|
let message;
|
|
3442
3620
|
if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
|
|
3443
|
-
return
|
|
3621
|
+
return {
|
|
3622
|
+
status,
|
|
3444
3623
|
codeDesc,
|
|
3445
|
-
startTime: scanTime
|
|
3446
|
-
|
|
3624
|
+
startTime: scanTime,
|
|
3625
|
+
...message !== void 0 ? { message } : {}
|
|
3626
|
+
};
|
|
3447
3627
|
}
|
|
3448
3628
|
/**
|
|
3449
3629
|
* Converts a group of checks sharing a check_id into one EvaluatedRequirement.
|
|
@@ -3485,11 +3665,11 @@ function parseInput(input) {
|
|
|
3485
3665
|
* @param input - checkov JSON or SARIF string
|
|
3486
3666
|
* @returns HDF JSON string
|
|
3487
3667
|
*/
|
|
3488
|
-
async function convertCheckovToHdf(input) {
|
|
3668
|
+
async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
|
|
3489
3669
|
validateInputSize(input, "checkov");
|
|
3490
3670
|
registerAllFingerprints();
|
|
3491
3671
|
const detected = detectConverter(input);
|
|
3492
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3672
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
3493
3673
|
const resultsChecksum = await inputChecksum(input);
|
|
3494
3674
|
const scanTime = /* @__PURE__ */ new Date();
|
|
3495
3675
|
const reports = parseInput(input);
|
|
@@ -3522,7 +3702,7 @@ async function convertCheckovToHdf(input) {
|
|
|
3522
3702
|
const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
|
|
3523
3703
|
return buildHdfResults({
|
|
3524
3704
|
generatorName: "checkov-to-hdf",
|
|
3525
|
-
converterVersion
|
|
3705
|
+
converterVersion,
|
|
3526
3706
|
toolName: "Checkov",
|
|
3527
3707
|
toolVersion: version,
|
|
3528
3708
|
toolFormat: format,
|
|
@@ -3606,11 +3786,11 @@ function buildRequirement$13(ruleId, issues, scanTime) {
|
|
|
3606
3786
|
* @param input - gosec JSON or SARIF string
|
|
3607
3787
|
* @returns HDF JSON string
|
|
3608
3788
|
*/
|
|
3609
|
-
async function convertGosecToHdf(input) {
|
|
3789
|
+
async function convertGosecToHdf(input, converterVersion = "1.0.0") {
|
|
3610
3790
|
validateInputSize(input, "gosec");
|
|
3611
3791
|
registerAllFingerprints();
|
|
3612
3792
|
const detected = detectConverter(input);
|
|
3613
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3793
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
3614
3794
|
const resultsChecksum = await inputChecksum(input);
|
|
3615
3795
|
const report = parseJSON(input);
|
|
3616
3796
|
if (!report || typeof report !== "object") throw new Error("Invalid gosec structure: not a valid JSON object");
|
|
@@ -3631,7 +3811,7 @@ async function convertGosecToHdf(input) {
|
|
|
3631
3811
|
const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
|
|
3632
3812
|
return buildHdfResults({
|
|
3633
3813
|
generatorName: "gosec-to-hdf",
|
|
3634
|
-
converterVersion
|
|
3814
|
+
converterVersion,
|
|
3635
3815
|
toolName: "gosec",
|
|
3636
3816
|
toolVersion: report.GosecVersion || void 0,
|
|
3637
3817
|
baselines: [baseline],
|
|
@@ -3678,7 +3858,7 @@ function convertVulnToRequirement(vuln) {
|
|
|
3678
3858
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3679
3859
|
return req;
|
|
3680
3860
|
}
|
|
3681
|
-
async function convertNiktoToHdf(input) {
|
|
3861
|
+
async function convertNiktoToHdf(input, converterVersion = "unknown") {
|
|
3682
3862
|
validateInputSize(input, "nikto");
|
|
3683
3863
|
const resultsChecksum = await inputChecksum(input);
|
|
3684
3864
|
const niktoData = parseJSON(input);
|
|
@@ -3711,7 +3891,7 @@ async function convertNiktoToHdf(input) {
|
|
|
3711
3891
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3712
3892
|
return buildHdfResults({
|
|
3713
3893
|
generatorName: "nikto-to-hdf",
|
|
3714
|
-
converterVersion
|
|
3894
|
+
converterVersion,
|
|
3715
3895
|
toolName: "Nikto",
|
|
3716
3896
|
toolFormat: "JSON",
|
|
3717
3897
|
baselines: [createMinimalBaseline(targetName, requirements, {
|
|
@@ -3784,11 +3964,11 @@ function parseZapTimestamp(s) {
|
|
|
3784
3964
|
}
|
|
3785
3965
|
return parseTimestamp(s) ?? void 0;
|
|
3786
3966
|
}
|
|
3787
|
-
async function convertZapToHdf(input) {
|
|
3967
|
+
async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
3788
3968
|
validateInputSize(input, "zap");
|
|
3789
3969
|
registerAllFingerprints();
|
|
3790
3970
|
const detected = detectConverter(input);
|
|
3791
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3971
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
3792
3972
|
const resultsChecksum = await inputChecksum(input);
|
|
3793
3973
|
const zapData = parseJSON(input);
|
|
3794
3974
|
const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
|
|
@@ -3881,7 +4061,7 @@ async function convertZapToHdf(input) {
|
|
|
3881
4061
|
components,
|
|
3882
4062
|
generator: {
|
|
3883
4063
|
name: "zap-to-hdf",
|
|
3884
|
-
version:
|
|
4064
|
+
version: converterVersion
|
|
3885
4065
|
},
|
|
3886
4066
|
tool
|
|
3887
4067
|
};
|
|
@@ -3892,6 +4072,579 @@ async function convertZapToHdf(input) {
|
|
|
3892
4072
|
return serializeHdf(hdf);
|
|
3893
4073
|
}
|
|
3894
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
|
|
3895
4648
|
//#region converters/cyclonedx-to-hdf/typescript/converter.ts
|
|
3896
4649
|
const CVSS_METHODS = /* @__PURE__ */ new Set([
|
|
3897
4650
|
"CVSSv2",
|
|
@@ -3909,7 +4662,7 @@ function maxImpact(ratings) {
|
|
|
3909
4662
|
for (const rating of ratings) {
|
|
3910
4663
|
let impact;
|
|
3911
4664
|
if (rating.method && CVSS_METHODS.has(rating.method) && rating.score !== void 0 && rating.score !== null) impact = rating.score / 10;
|
|
3912
|
-
else impact = severityToImpact(rating.severity ?? "medium");
|
|
4665
|
+
else impact = severityToImpact$1(rating.severity ?? "medium");
|
|
3913
4666
|
if (impact > max) max = impact;
|
|
3914
4667
|
}
|
|
3915
4668
|
return max;
|
|
@@ -3944,19 +4697,29 @@ function flattenComponents(components) {
|
|
|
3944
4697
|
return result;
|
|
3945
4698
|
}
|
|
3946
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
|
+
/**
|
|
3947
4707
|
* Converts CycloneDX SBOM/VEX JSON to HDF format.
|
|
3948
4708
|
*
|
|
3949
4709
|
* @param input - CycloneDX JSON string
|
|
3950
4710
|
* @returns HDF JSON string
|
|
3951
4711
|
*/
|
|
3952
|
-
async function convertCyclonedxToHdf(input) {
|
|
4712
|
+
async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
3953
4713
|
if (!input || input.trim().length === 0) throw new Error("cyclonedx: empty input");
|
|
3954
4714
|
validateInputSize(input, "cyclonedx");
|
|
3955
4715
|
const bom = parseJSON(input);
|
|
3956
4716
|
if (!bom || typeof bom !== "object") throw new Error("cyclonedx: invalid JSON");
|
|
3957
4717
|
if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx: missing or invalid bomFormat (expected "CycloneDX", got "${bom.bomFormat ?? "undefined"}")`);
|
|
3958
4718
|
if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
|
|
3959
|
-
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
|
+
}
|
|
3960
4723
|
const resultsChecksum = await inputChecksum(input);
|
|
3961
4724
|
const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
|
|
3962
4725
|
const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
|
|
@@ -3998,13 +4761,12 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3998
4761
|
data: fixParts.join("\n\n")
|
|
3999
4762
|
});
|
|
4000
4763
|
const affects = vuln.affects ?? [];
|
|
4001
|
-
const
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
})) : [createResult(ResultStatus.Failed, void 0, {
|
|
4005
|
-
codeDesc: `Vulnerability ${vuln.id}`,
|
|
4764
|
+
const toResult = (codeDesc) => ({
|
|
4765
|
+
status: ResultStatus.Failed,
|
|
4766
|
+
codeDesc,
|
|
4006
4767
|
startTime: scanTime
|
|
4007
|
-
})
|
|
4768
|
+
});
|
|
4769
|
+
const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
|
|
4008
4770
|
const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
|
|
4009
4771
|
const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
|
|
4010
4772
|
const controlType = deriveControlTypeFromTags(nist);
|
|
@@ -4012,16 +4774,29 @@ async function convertCyclonedxToHdf(input) {
|
|
|
4012
4774
|
requirements.push(req);
|
|
4013
4775
|
}
|
|
4014
4776
|
const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
|
|
4015
|
-
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);
|
|
4016
4791
|
return buildHdfResults({
|
|
4017
4792
|
generatorName: "cyclonedx-to-hdf",
|
|
4018
|
-
converterVersion
|
|
4793
|
+
converterVersion,
|
|
4019
4794
|
toolName: "CycloneDX",
|
|
4020
4795
|
toolFormat: "JSON",
|
|
4021
4796
|
baselines: [baseline],
|
|
4022
4797
|
components: [{
|
|
4023
|
-
|
|
4024
|
-
|
|
4798
|
+
...targetComponent,
|
|
4799
|
+
boms: [componentBom]
|
|
4025
4800
|
}],
|
|
4026
4801
|
timestamp: scanTime
|
|
4027
4802
|
});
|
|
@@ -4035,7 +4810,7 @@ async function convertCyclonedxToHdf(input) {
|
|
|
4035
4810
|
*/
|
|
4036
4811
|
function convertHdfToCsv(input) {
|
|
4037
4812
|
validateInputSize(input, "hdf-to-csv");
|
|
4038
|
-
const hdf =
|
|
4813
|
+
const hdf = parseHdf(input);
|
|
4039
4814
|
if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
4040
4815
|
if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
4041
4816
|
const rows = [];
|
|
@@ -4055,6 +4830,11 @@ function convertHdfToCsv(input) {
|
|
|
4055
4830
|
*/
|
|
4056
4831
|
function createRow(baseline, requirement, target) {
|
|
4057
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);
|
|
4058
4838
|
const severity = getSeverity(requirement);
|
|
4059
4839
|
const firstResult = requirement.results[0];
|
|
4060
4840
|
const status = firstResult?.status || "";
|
|
@@ -4070,6 +4850,11 @@ function createRow(baseline, requirement, target) {
|
|
|
4070
4850
|
"Requirement ID": requirement.id,
|
|
4071
4851
|
"Requirement Title": requirement.title || "",
|
|
4072
4852
|
"Description": description,
|
|
4853
|
+
"Check": check,
|
|
4854
|
+
"Fix": fix,
|
|
4855
|
+
"Rationale": rationale,
|
|
4856
|
+
"Code": code,
|
|
4857
|
+
"References": references,
|
|
4073
4858
|
"Severity": severity,
|
|
4074
4859
|
"Impact": requirement.impact,
|
|
4075
4860
|
"Status": String(status),
|
|
@@ -4092,21 +4877,826 @@ function getSeverity(requirement) {
|
|
|
4092
4877
|
if (Array.isArray(sev) && sev.length > 0) return String(sev[0]);
|
|
4093
4878
|
}
|
|
4094
4879
|
}
|
|
4095
|
-
const impact = requirement.impact;
|
|
4096
|
-
if (impact >= .7) return "high";
|
|
4097
|
-
if (impact >= .4) return "medium";
|
|
4098
|
-
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];
|
|
4099
5455
|
}
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
function extractArrayFromTags(tags, key) {
|
|
4104
|
-
if (!tags || typeof tags !== "object") return "";
|
|
4105
|
-
const value = tags[key];
|
|
4106
|
-
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;
|
|
4107
5459
|
return "";
|
|
4108
5460
|
}
|
|
4109
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
|
|
4110
5700
|
//#region converters/splunk-to-hdf/typescript/converter.ts
|
|
4111
5701
|
/**
|
|
4112
5702
|
* Map a Splunk result status string to the HDF ResultStatus enum.
|
|
@@ -4122,11 +5712,12 @@ function mapStatus$1(status) {
|
|
|
4122
5712
|
}
|
|
4123
5713
|
/**
|
|
4124
5714
|
* Convert a Splunk descriptions object (`{ key: value }`) to an array of
|
|
4125
|
-
* 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.
|
|
4126
5717
|
*/
|
|
4127
5718
|
function convertDescriptions(descriptions) {
|
|
4128
5719
|
if (!descriptions || typeof descriptions !== "object") return [];
|
|
4129
|
-
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));
|
|
4130
5721
|
}
|
|
4131
5722
|
/**
|
|
4132
5723
|
* Convert Splunk profile groups (which use `controls`) to HDF
|
|
@@ -4150,7 +5741,7 @@ function convertGroups(groups) {
|
|
|
4150
5741
|
* @param input - JSON string of a SplunkEvent array
|
|
4151
5742
|
* @returns HDF Results JSON string
|
|
4152
5743
|
*/
|
|
4153
|
-
async function convertSplunkToHdf(input) {
|
|
5744
|
+
async function convertSplunkToHdf(input, converterVersion = "1.0.0") {
|
|
4154
5745
|
validateInputSize(input, "splunk");
|
|
4155
5746
|
const resultsChecksum = await inputChecksum(input);
|
|
4156
5747
|
const events = parseJSON(input);
|
|
@@ -4162,9 +5753,10 @@ async function convertSplunkToHdf(input) {
|
|
|
4162
5753
|
eventsByGuid.get(guid).push(event);
|
|
4163
5754
|
}
|
|
4164
5755
|
const allBaselines = [];
|
|
4165
|
-
|
|
4166
|
-
let
|
|
4167
|
-
for (const
|
|
5756
|
+
const components = [];
|
|
5757
|
+
let statistics;
|
|
5758
|
+
for (const guid of [...eventsByGuid.keys()].sort()) {
|
|
5759
|
+
const guidEvents = eventsByGuid.get(guid);
|
|
4168
5760
|
const headers = [];
|
|
4169
5761
|
const profiles = [];
|
|
4170
5762
|
const controls = [];
|
|
@@ -4181,8 +5773,13 @@ async function convertSplunkToHdf(input) {
|
|
|
4181
5773
|
}
|
|
4182
5774
|
if (headers.length !== 1) throw new Error(`Expected 1 header event, got ${headers.length}`);
|
|
4183
5775
|
const header = headers[0];
|
|
4184
|
-
|
|
4185
|
-
|
|
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);
|
|
4186
5783
|
const controlsByProfile = /* @__PURE__ */ new Map();
|
|
4187
5784
|
for (const control of controls) {
|
|
4188
5785
|
const sha = control.meta.profile_sha256 ?? "";
|
|
@@ -4192,16 +5789,24 @@ async function convertSplunkToHdf(input) {
|
|
|
4192
5789
|
for (const profile of profiles) {
|
|
4193
5790
|
const requirements = (controlsByProfile.get(profile.sha256) ?? []).map((control) => {
|
|
4194
5791
|
const descriptions = convertDescriptions(control.descriptions);
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
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
|
+
}) : [];
|
|
4202
5805
|
const options = { tags: control.tags ?? {} };
|
|
4203
5806
|
if (control.source_location) options.sourceLocation = control.source_location;
|
|
4204
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;
|
|
4205
5810
|
const nistTagsRaw = control.tags?.["nist"];
|
|
4206
5811
|
const controlType = deriveControlTypeFromTags(Array.isArray(nistTagsRaw) ? nistTagsRaw.filter((t) => typeof t === "string") : []);
|
|
4207
5812
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -4214,23 +5819,34 @@ async function convertSplunkToHdf(input) {
|
|
|
4214
5819
|
if (profile.version) baselineOptions.version = profile.version;
|
|
4215
5820
|
if (profile.summary) baselineOptions.summary = profile.summary;
|
|
4216
5821
|
if (groups.length > 0) baselineOptions.groups = groups;
|
|
4217
|
-
|
|
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);
|
|
4218
5831
|
}
|
|
4219
5832
|
}
|
|
4220
|
-
return
|
|
5833
|
+
return buildHdfResults({
|
|
5834
|
+
generatorName: "splunk-to-hdf",
|
|
5835
|
+
converterVersion,
|
|
5836
|
+
toolName: "Splunk",
|
|
4221
5837
|
baselines: allBaselines,
|
|
4222
|
-
components
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
osName: targetRelease || void 0,
|
|
4226
|
-
labels: {}
|
|
4227
|
-
}],
|
|
4228
|
-
generator: {
|
|
4229
|
-
name: "splunk-to-hdf",
|
|
4230
|
-
version: "1.0.0"
|
|
4231
|
-
}
|
|
5838
|
+
components,
|
|
5839
|
+
statistics,
|
|
5840
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
4232
5841
|
});
|
|
4233
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
|
+
}
|
|
4234
5850
|
//#endregion
|
|
4235
5851
|
//#region converters/hdf-to-xml/typescript/converter.ts
|
|
4236
5852
|
/**
|
|
@@ -4240,7 +5856,7 @@ async function convertSplunkToHdf(input) {
|
|
|
4240
5856
|
*/
|
|
4241
5857
|
function convertHdfToXml(input) {
|
|
4242
5858
|
validateInputSize(input, "hdf-to-xml");
|
|
4243
|
-
const hdf =
|
|
5859
|
+
const hdf = parseHdf(input);
|
|
4244
5860
|
if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
4245
5861
|
if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
4246
5862
|
return buildXml({ HdfResults: transformHdfToXmlObject(hdf) });
|
|
@@ -4252,6 +5868,20 @@ function wrap$1(value) {
|
|
|
4252
5868
|
return { "#text": value };
|
|
4253
5869
|
}
|
|
4254
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
|
+
/**
|
|
4255
5885
|
* Transform HDF object to XML-compatible structure
|
|
4256
5886
|
* Converts arrays to repeated singular elements
|
|
4257
5887
|
*/
|
|
@@ -4261,6 +5891,16 @@ function transformHdfToXmlObject(hdf) {
|
|
|
4261
5891
|
name: wrap$1(baseline.name),
|
|
4262
5892
|
...baseline.version && { version: wrap$1(baseline.version) },
|
|
4263
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
|
+
} },
|
|
4264
5904
|
...baseline.integrity && { integrity: {
|
|
4265
5905
|
...baseline.integrity.algorithm && { algorithm: wrap$1(baseline.integrity.algorithm) },
|
|
4266
5906
|
...baseline.integrity.checksum && { checksum: wrap$1(baseline.integrity.checksum) }
|
|
@@ -4270,11 +5910,13 @@ function transformHdfToXmlObject(hdf) {
|
|
|
4270
5910
|
})) };
|
|
4271
5911
|
else result.baselines = {};
|
|
4272
5912
|
if (hdf.components && hdf.components.length > 0) result.components = { target: hdf.components.map((target) => ({
|
|
5913
|
+
...target.componentId && { componentId: wrap$1(target.componentId) },
|
|
4273
5914
|
name: wrap$1(target.name),
|
|
4274
5915
|
type: wrap$1(target.type),
|
|
5916
|
+
...target.hostname && { hostname: wrap$1(target.hostname) },
|
|
4275
5917
|
...target.fqdn && { fqdn: wrap$1(target.fqdn) },
|
|
4276
|
-
...target.
|
|
4277
|
-
...target.
|
|
5918
|
+
...target.domain && { domain: wrap$1(target.domain) },
|
|
5919
|
+
...target.ipAddress && { ipAddress: wrap$1(target.ipAddress) }
|
|
4278
5920
|
})) };
|
|
4279
5921
|
if (hdf.statistics) {
|
|
4280
5922
|
result.statistics = {};
|
|
@@ -4282,7 +5924,10 @@ function transformHdfToXmlObject(hdf) {
|
|
|
4282
5924
|
if (hdf.statistics.requirements) result.statistics.requirements = hdf.statistics.requirements;
|
|
4283
5925
|
}
|
|
4284
5926
|
if (hdf.timestamp) result.timestamp = wrap$1(typeof hdf.timestamp === "string" ? hdf.timestamp : hdf.timestamp.toISOString());
|
|
4285
|
-
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
|
+
};
|
|
4286
5931
|
return result;
|
|
4287
5932
|
}
|
|
4288
5933
|
/**
|
|
@@ -4296,11 +5941,20 @@ function transformRequirement(req) {
|
|
|
4296
5941
|
label: wrap$1(d.label),
|
|
4297
5942
|
data: wrap$1(d.data)
|
|
4298
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) } },
|
|
4299
5953
|
impact: wrap$1(req.impact)
|
|
4300
5954
|
};
|
|
4301
5955
|
if (req.tags && Object.keys(req.tags).length > 0) {
|
|
4302
5956
|
const transformedTags = {};
|
|
4303
|
-
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));
|
|
4304
5958
|
else if (!Array.isArray(value)) transformedTags[key] = wrap$1(value);
|
|
4305
5959
|
if (Object.keys(transformedTags).length > 0) result.tags = transformedTags;
|
|
4306
5960
|
}
|
|
@@ -4316,7 +5970,7 @@ function transformRequirement(req) {
|
|
|
4316
5970
|
//#endregion
|
|
4317
5971
|
//#region converters/gitlab-to-hdf/typescript/converter.ts
|
|
4318
5972
|
function gitlabSeverityToImpact(severity) {
|
|
4319
|
-
return severityToImpact(severity.toLowerCase());
|
|
5973
|
+
return severityToImpact$1(severity.toLowerCase());
|
|
4320
5974
|
}
|
|
4321
5975
|
function scanTypeToTargetType(scanType) {
|
|
4322
5976
|
switch (scanType) {
|
|
@@ -4406,7 +6060,7 @@ function buildCodeDesc$4(scanType, location) {
|
|
|
4406
6060
|
default: return `Location: ${JSON.stringify(location)}`;
|
|
4407
6061
|
}
|
|
4408
6062
|
}
|
|
4409
|
-
async function convertGitlabToHdf(input) {
|
|
6063
|
+
async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
4410
6064
|
validateInputSize(input, "gitlab");
|
|
4411
6065
|
const resultsChecksum = await inputChecksum(input);
|
|
4412
6066
|
const report = parseJSON(input);
|
|
@@ -4481,7 +6135,7 @@ async function convertGitlabToHdf(input) {
|
|
|
4481
6135
|
components,
|
|
4482
6136
|
generator: {
|
|
4483
6137
|
name: "gitlab-to-hdf",
|
|
4484
|
-
version:
|
|
6138
|
+
version: converterVersion
|
|
4485
6139
|
},
|
|
4486
6140
|
tool
|
|
4487
6141
|
};
|
|
@@ -4534,6 +6188,18 @@ function groupFindings(findings) {
|
|
|
4534
6188
|
return groups;
|
|
4535
6189
|
}
|
|
4536
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
|
+
/**
|
|
4537
6203
|
* Build the Result.Message JSON from selected finding fields.
|
|
4538
6204
|
*/
|
|
4539
6205
|
function buildMessage(f) {
|
|
@@ -4542,7 +6208,7 @@ function buildMessage(f) {
|
|
|
4542
6208
|
Redacted: f.Redacted
|
|
4543
6209
|
};
|
|
4544
6210
|
if (f.VerificationError) msg.VerificationError = f.VerificationError;
|
|
4545
|
-
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);
|
|
4546
6212
|
return JSON.stringify(msg);
|
|
4547
6213
|
}
|
|
4548
6214
|
/**
|
|
@@ -4615,7 +6281,7 @@ function buildRequirement$12(reqID, findings) {
|
|
|
4615
6281
|
* @param input - TruffleHog JSON/NDJSON string
|
|
4616
6282
|
* @returns HDF JSON string
|
|
4617
6283
|
*/
|
|
4618
|
-
async function convertTrufflehogToHdf(input) {
|
|
6284
|
+
async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
|
|
4619
6285
|
if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
|
|
4620
6286
|
validateInputSize(input, "trufflehog");
|
|
4621
6287
|
const findings = parseFindings(input);
|
|
@@ -4635,7 +6301,7 @@ async function convertTrufflehogToHdf(input) {
|
|
|
4635
6301
|
})],
|
|
4636
6302
|
generator: {
|
|
4637
6303
|
name: "trufflehog-to-hdf",
|
|
4638
|
-
version:
|
|
6304
|
+
version: converterVersion
|
|
4639
6305
|
},
|
|
4640
6306
|
tool: {
|
|
4641
6307
|
name: "TruffleHog",
|
|
@@ -4686,7 +6352,7 @@ function formatCodeDesc$3(hostIP, hostURL, location, issueDetail, confidence) {
|
|
|
4686
6352
|
* @param input - BurpSuite XML string
|
|
4687
6353
|
* @returns HDF JSON string
|
|
4688
6354
|
*/
|
|
4689
|
-
async function convertBurpsuiteToHdf(input) {
|
|
6355
|
+
async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
4690
6356
|
if (!input || input.trim().length === 0) throw new Error("burpsuite: empty input");
|
|
4691
6357
|
validateInputSize(input, "burpsuite");
|
|
4692
6358
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -4726,7 +6392,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
4726
6392
|
}],
|
|
4727
6393
|
generator: {
|
|
4728
6394
|
name: "burpsuite-to-hdf",
|
|
4729
|
-
version:
|
|
6395
|
+
version: converterVersion
|
|
4730
6396
|
},
|
|
4731
6397
|
tool
|
|
4732
6398
|
};
|
|
@@ -4787,6 +6453,26 @@ const IMPACT_MAPPING$4 = {
|
|
|
4787
6453
|
low: .3,
|
|
4788
6454
|
informational: 0
|
|
4789
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
|
+
}
|
|
4790
6476
|
/**
|
|
4791
6477
|
* Maps metadata column names to row values by position index.
|
|
4792
6478
|
* Mirrors the heimdall2 compileFindings function.
|
|
@@ -4801,7 +6487,7 @@ function compileFindings(parsed) {
|
|
|
4801
6487
|
colNames.forEach((name, i) => {
|
|
4802
6488
|
const val = values[i];
|
|
4803
6489
|
if (val === null || val === void 0 || typeof val === "object") finding[name] = "";
|
|
4804
|
-
else finding[name] = String(val);
|
|
6490
|
+
else finding[name] = decodeXmlEntities$2(String(val));
|
|
4805
6491
|
});
|
|
4806
6492
|
return finding;
|
|
4807
6493
|
});
|
|
@@ -4876,10 +6562,11 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
4876
6562
|
data: formatDesc$1(rep)
|
|
4877
6563
|
}];
|
|
4878
6564
|
const results = findings.map((f) => {
|
|
4879
|
-
return
|
|
6565
|
+
return {
|
|
6566
|
+
status: hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed,
|
|
4880
6567
|
codeDesc: f["Details"] ?? "",
|
|
4881
6568
|
startTime: parseDate(f["Date"] ?? "")
|
|
4882
|
-
}
|
|
6569
|
+
};
|
|
4883
6570
|
});
|
|
4884
6571
|
const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
|
|
4885
6572
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -4895,7 +6582,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
4895
6582
|
* @param input - DBProtect XML string
|
|
4896
6583
|
* @returns HDF JSON string
|
|
4897
6584
|
*/
|
|
4898
|
-
async function convertDbprotectToHdf(input) {
|
|
6585
|
+
async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
|
|
4899
6586
|
if (!input || input.trim().length === 0) throw new Error("dbprotect: empty input");
|
|
4900
6587
|
validateInputSize(input, "dbprotect");
|
|
4901
6588
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -4923,16 +6610,19 @@ async function convertDbprotectToHdf(input) {
|
|
|
4923
6610
|
const title = firstFinding["Job Name"] ?? "";
|
|
4924
6611
|
const summary = formatSummary(firstFinding);
|
|
4925
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;
|
|
4926
6620
|
return buildHdfResults({
|
|
4927
6621
|
generatorName: "dbprotect-to-hdf",
|
|
4928
|
-
converterVersion
|
|
6622
|
+
converterVersion,
|
|
4929
6623
|
toolName: "DBProtect",
|
|
4930
6624
|
toolFormat: "XML",
|
|
4931
|
-
baselines: [
|
|
4932
|
-
resultsChecksum,
|
|
4933
|
-
title,
|
|
4934
|
-
summary
|
|
4935
|
-
})],
|
|
6625
|
+
baselines: [baseline],
|
|
4936
6626
|
components: [{
|
|
4937
6627
|
name: targetName,
|
|
4938
6628
|
type: TargetType.Host
|
|
@@ -5121,7 +6811,7 @@ function buildPackageTypeIndex(pkgs) {
|
|
|
5121
6811
|
*/
|
|
5122
6812
|
function formatCodeDesc$2(vuln) {
|
|
5123
6813
|
const packageName = vuln.packageName ?? "N/A";
|
|
5124
|
-
const impactedVersions = vuln.impactedVersions && vuln.impactedVersions.length > 0 ?
|
|
6814
|
+
const impactedVersions = vuln.impactedVersions && vuln.impactedVersions.length > 0 ? `[${vuln.impactedVersions.join(" ")}]` : "N/A";
|
|
5125
6815
|
return `Package ${JSON.stringify(packageName)} should be updated to latest version above impacted versions ${impactedVersions}`;
|
|
5126
6816
|
}
|
|
5127
6817
|
/**
|
|
@@ -5142,10 +6832,11 @@ function buildRequirement$9(vuln, packageTypes, distro) {
|
|
|
5142
6832
|
data: vuln.description
|
|
5143
6833
|
}];
|
|
5144
6834
|
const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
5145
|
-
const results = [
|
|
6835
|
+
const results = [{
|
|
6836
|
+
status: ResultStatus.Failed,
|
|
5146
6837
|
codeDesc: formatCodeDesc$2(vuln),
|
|
5147
6838
|
startTime
|
|
5148
|
-
}
|
|
6839
|
+
}];
|
|
5149
6840
|
const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
|
|
5150
6841
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5151
6842
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -5187,7 +6878,7 @@ function convertSingleResult(result, resultsChecksum) {
|
|
|
5187
6878
|
* @param input - Twistlock JSON string
|
|
5188
6879
|
* @returns HDF JSON string
|
|
5189
6880
|
*/
|
|
5190
|
-
async function convertTwistlockToHdf(input) {
|
|
6881
|
+
async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
|
|
5191
6882
|
if (!input || input.trim().length === 0) throw new Error("twistlock: empty input");
|
|
5192
6883
|
validateInputSize(input, "twistlock");
|
|
5193
6884
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5198,18 +6889,19 @@ async function convertTwistlockToHdf(input) {
|
|
|
5198
6889
|
else results = [parsed];
|
|
5199
6890
|
if (results.length === 0) throw new Error("twistlock: no scan results found");
|
|
5200
6891
|
const baselines = results.map((result) => convertSingleResult(result, resultsChecksum));
|
|
5201
|
-
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 };
|
|
5202
6898
|
return buildHdfResults({
|
|
5203
6899
|
generatorName: "twistlock-to-hdf",
|
|
5204
|
-
converterVersion
|
|
6900
|
+
converterVersion,
|
|
5205
6901
|
toolName: "Twistlock",
|
|
5206
6902
|
toolFormat: "JSON",
|
|
5207
6903
|
baselines,
|
|
5208
|
-
components: [
|
|
5209
|
-
name: targetName,
|
|
5210
|
-
type: TargetType.ContainerImage,
|
|
5211
|
-
labels: { image: results[0]?.id ?? targetName }
|
|
5212
|
-
}],
|
|
6904
|
+
components: [component],
|
|
5213
6905
|
timestamp: /* @__PURE__ */ new Date()
|
|
5214
6906
|
});
|
|
5215
6907
|
}
|
|
@@ -5271,10 +6963,11 @@ function buildRequirement$8(finding, timestamp) {
|
|
|
5271
6963
|
data: finding.vulnerability.recommendation
|
|
5272
6964
|
});
|
|
5273
6965
|
const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
|
|
5274
|
-
const results = [
|
|
6966
|
+
const results = [{
|
|
6967
|
+
status: ResultStatus.Failed,
|
|
5275
6968
|
codeDesc,
|
|
5276
6969
|
startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
5277
|
-
}
|
|
6970
|
+
}];
|
|
5278
6971
|
const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
|
|
5279
6972
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5280
6973
|
const controlType = deriveControlTypeFromTags(nist);
|
|
@@ -5308,7 +7001,7 @@ function buildAffectedPackageFromComponent(c) {
|
|
|
5308
7001
|
* @param input - Dependency-Track FPF JSON string
|
|
5309
7002
|
* @returns HDF JSON string
|
|
5310
7003
|
*/
|
|
5311
|
-
async function convertDeptrackToHdf(input) {
|
|
7004
|
+
async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
|
|
5312
7005
|
if (!input || input.trim().length === 0) throw new Error("deptrack: empty input");
|
|
5313
7006
|
validateInputSize(input, "deptrack");
|
|
5314
7007
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5328,7 +7021,7 @@ async function convertDeptrackToHdf(input) {
|
|
|
5328
7021
|
const targetName = parsed.project?.name ?? parsed.project?.uuid ?? "";
|
|
5329
7022
|
return buildHdfResults({
|
|
5330
7023
|
generatorName: "deptrack-to-hdf",
|
|
5331
|
-
converterVersion
|
|
7024
|
+
converterVersion,
|
|
5332
7025
|
toolName: "Dependency-Track",
|
|
5333
7026
|
toolFormat: "JSON",
|
|
5334
7027
|
baselines: [baseline],
|
|
@@ -5407,11 +7100,12 @@ function buildRequirement$7(entryID, entries, scanTime) {
|
|
|
5407
7100
|
label: "default",
|
|
5408
7101
|
data: formatDescription(rep)
|
|
5409
7102
|
}];
|
|
5410
|
-
const results = entries.map((entry) =>
|
|
7103
|
+
const results = entries.map((entry) => ({
|
|
7104
|
+
status: ResultStatus.Failed,
|
|
5411
7105
|
codeDesc: formatCodeDesc$1(entry),
|
|
5412
7106
|
startTime: scanTime
|
|
5413
7107
|
}));
|
|
5414
|
-
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 });
|
|
5415
7109
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5416
7110
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5417
7111
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -5464,7 +7158,7 @@ function buildAffectedPackageFromEntry(entry) {
|
|
|
5464
7158
|
* @param input - JFrog Xray JSON string
|
|
5465
7159
|
* @returns HDF JSON string
|
|
5466
7160
|
*/
|
|
5467
|
-
async function convertJfrogXrayToHdf(input) {
|
|
7161
|
+
async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
|
|
5468
7162
|
if (!input || input.trim().length === 0) throw new Error("jfrog-xray: empty input");
|
|
5469
7163
|
validateInputSize(input, "jfrog-xray");
|
|
5470
7164
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5491,7 +7185,7 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
5491
7185
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
|
|
5492
7186
|
return buildHdfResults({
|
|
5493
7187
|
generatorName: "jfrog-xray-to-hdf",
|
|
5494
|
-
converterVersion
|
|
7188
|
+
converterVersion,
|
|
5495
7189
|
toolName: "JFrog Xray",
|
|
5496
7190
|
toolFormat: "JSON",
|
|
5497
7191
|
baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
|
|
@@ -5600,7 +7294,7 @@ function targetNameFromReport(report) {
|
|
|
5600
7294
|
* @param input - NeuVector scan JSON string
|
|
5601
7295
|
* @returns HDF JSON string
|
|
5602
7296
|
*/
|
|
5603
|
-
async function convertNeuvectorToHdf(input) {
|
|
7297
|
+
async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
|
|
5604
7298
|
if (!input || input.trim().length === 0) throw new Error("neuvector: empty input");
|
|
5605
7299
|
validateInputSize(input, "neuvector");
|
|
5606
7300
|
const scan = parseJSON(input);
|
|
@@ -5622,7 +7316,7 @@ async function convertNeuvectorToHdf(input) {
|
|
|
5622
7316
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
|
|
5623
7317
|
return buildHdfResults({
|
|
5624
7318
|
generatorName: "neuvector-to-hdf",
|
|
5625
|
-
converterVersion
|
|
7319
|
+
converterVersion,
|
|
5626
7320
|
toolName: "NeuVector",
|
|
5627
7321
|
toolFormat: "JSON",
|
|
5628
7322
|
baselines: [createMinimalBaseline("NeuVector Scan", requirements, {
|
|
@@ -5644,6 +7338,23 @@ async function convertNeuvectorToHdf(input) {
|
|
|
5644
7338
|
//#region converters/fortify-to-hdf/typescript/converter.ts
|
|
5645
7339
|
const NIST_REFERENCE_NAME = "Standards Mapping - NIST Special Publication 800-53 Revision 4";
|
|
5646
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
|
+
}
|
|
5647
7358
|
function buildSnippetMap(snippets) {
|
|
5648
7359
|
const map = /* @__PURE__ */ new Map();
|
|
5649
7360
|
for (const s of snippets) if (s.id) map.set(s.id, s);
|
|
@@ -5693,8 +7404,8 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
5693
7404
|
if (nistTags.length === 0) nistTags = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1];
|
|
5694
7405
|
const cciTags = nistToCci(nistTags);
|
|
5695
7406
|
const tags = buildNistCciTags(nistTags, cciTags);
|
|
5696
|
-
const title =
|
|
5697
|
-
let explanationText =
|
|
7407
|
+
const title = stripFvdlMarkup(desc.Abstract ?? "");
|
|
7408
|
+
let explanationText = stripFvdlMarkup(desc.Explanation ?? "");
|
|
5698
7409
|
if (!explanationText) explanationText = title;
|
|
5699
7410
|
const descriptions = [{
|
|
5700
7411
|
label: "default",
|
|
@@ -5702,7 +7413,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
5702
7413
|
}];
|
|
5703
7414
|
if (desc.Recommendations) descriptions.push({
|
|
5704
7415
|
label: "fix",
|
|
5705
|
-
data:
|
|
7416
|
+
data: stripFvdlMarkup(desc.Recommendations)
|
|
5706
7417
|
});
|
|
5707
7418
|
let impact = 0;
|
|
5708
7419
|
if (vulns.length > 0) impact = parseFloat(vulns[0].ClassInfo?.DefaultSeverity ?? "0") / 5;
|
|
@@ -5737,7 +7448,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
5737
7448
|
* @param input - Fortify FVDL XML string
|
|
5738
7449
|
* @returns HDF JSON string
|
|
5739
7450
|
*/
|
|
5740
|
-
async function convertFortifyToHdf(input) {
|
|
7451
|
+
async function convertFortifyToHdf(input, converterVersion = "1.0.0") {
|
|
5741
7452
|
if (!input || input.trim().length === 0) throw new Error("fortify: empty input");
|
|
5742
7453
|
validateInputSize(input, "fortify");
|
|
5743
7454
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5771,7 +7482,7 @@ async function convertFortifyToHdf(input) {
|
|
|
5771
7482
|
}],
|
|
5772
7483
|
generator: {
|
|
5773
7484
|
name: "fortify-to-hdf",
|
|
5774
|
-
version:
|
|
7485
|
+
version: converterVersion
|
|
5775
7486
|
},
|
|
5776
7487
|
tool: {
|
|
5777
7488
|
name: "Fortify",
|
|
@@ -5802,16 +7513,12 @@ const REQUIRED_COLUMNS = [
|
|
|
5802
7513
|
* Prisma uses non-standard severity names: "important" and "moderate"
|
|
5803
7514
|
* in addition to the standard critical/high/medium/low.
|
|
5804
7515
|
*/
|
|
7516
|
+
const PRISMA_SEVERITY_ALIASES = {
|
|
7517
|
+
important: .9,
|
|
7518
|
+
moderate: .5
|
|
7519
|
+
};
|
|
5805
7520
|
function getImpact$3(severity) {
|
|
5806
|
-
|
|
5807
|
-
case "critical": return 1;
|
|
5808
|
-
case "important": return .9;
|
|
5809
|
-
case "high": return .7;
|
|
5810
|
-
case "moderate":
|
|
5811
|
-
case "medium": return .5;
|
|
5812
|
-
case "low": return .3;
|
|
5813
|
-
default: return .5;
|
|
5814
|
-
}
|
|
7521
|
+
return PRISMA_SEVERITY_ALIASES[severity.toLowerCase()] ?? severityToImpact(severity);
|
|
5815
7522
|
}
|
|
5816
7523
|
/**
|
|
5817
7524
|
* Returns NIST tags based on whether the finding has a CVE.
|
|
@@ -5956,7 +7663,7 @@ function buildBaseline(hostname, records, resultsChecksum, scanTime) {
|
|
|
5956
7663
|
* @param input - Prisma Cloud CSV string
|
|
5957
7664
|
* @returns HDF JSON string
|
|
5958
7665
|
*/
|
|
5959
|
-
async function convertPrismaToHdf(input) {
|
|
7666
|
+
async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
|
|
5960
7667
|
if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
|
|
5961
7668
|
validateInputSize(input, "prisma");
|
|
5962
7669
|
const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
|
|
@@ -5982,7 +7689,7 @@ async function convertPrismaToHdf(input) {
|
|
|
5982
7689
|
}
|
|
5983
7690
|
return buildHdfResults({
|
|
5984
7691
|
generatorName: "prisma-to-hdf",
|
|
5985
|
-
converterVersion
|
|
7692
|
+
converterVersion,
|
|
5986
7693
|
toolName: "Prisma Cloud",
|
|
5987
7694
|
toolFormat: "CSV",
|
|
5988
7695
|
baselines,
|
|
@@ -6121,7 +7828,7 @@ function buildRequirement$3(vuln, initiated) {
|
|
|
6121
7828
|
* @param input - Netsparker/Invicti XML string
|
|
6122
7829
|
* @returns HDF JSON string
|
|
6123
7830
|
*/
|
|
6124
|
-
async function convertNetsparkerToHdf(input) {
|
|
7831
|
+
async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
6125
7832
|
if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
|
|
6126
7833
|
validateInputSize(input, "netsparker");
|
|
6127
7834
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -6144,7 +7851,7 @@ async function convertNetsparkerToHdf(input) {
|
|
|
6144
7851
|
}
|
|
6145
7852
|
return buildHdfResults({
|
|
6146
7853
|
generatorName: "netsparker-to-hdf",
|
|
6147
|
-
converterVersion
|
|
7854
|
+
converterVersion,
|
|
6148
7855
|
toolName,
|
|
6149
7856
|
toolFormat: "XML",
|
|
6150
7857
|
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
@@ -6261,7 +7968,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
|
|
|
6261
7968
|
* @param input - ScoutSuite JS/JSON string
|
|
6262
7969
|
* @returns HDF JSON string
|
|
6263
7970
|
*/
|
|
6264
|
-
async function convertScoutsuiteToHdf(input) {
|
|
7971
|
+
async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
6265
7972
|
if (!input || input.trim().length === 0) throw new Error("scoutsuite: empty input");
|
|
6266
7973
|
validateInputSize(input, "scoutsuite");
|
|
6267
7974
|
const jsonStr = stripJSPrefix(input);
|
|
@@ -6278,7 +7985,7 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
6278
7985
|
});
|
|
6279
7986
|
return buildHdfResults({
|
|
6280
7987
|
generatorName: "scoutsuite-to-hdf",
|
|
6281
|
-
converterVersion
|
|
7988
|
+
converterVersion,
|
|
6282
7989
|
toolName: "ScoutSuite",
|
|
6283
7990
|
toolFormat: "JSON",
|
|
6284
7991
|
toolVersion: report.last_run.version,
|
|
@@ -6391,17 +8098,12 @@ function buildRequirementFromResult(result, filename) {
|
|
|
6391
8098
|
const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
6392
8099
|
const score = result.result.score;
|
|
6393
8100
|
const status = determineStatus(score);
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
codeDesc: buildCodeDesc$1(section, scannerName),
|
|
6398
|
-
startTime
|
|
6399
|
-
});
|
|
6400
|
-
});
|
|
6401
|
-
else results = [createResult(status, void 0, {
|
|
6402
|
-
codeDesc: `No sections reported by ${scannerName}`,
|
|
8101
|
+
const toResult = (codeDesc) => ({
|
|
8102
|
+
status,
|
|
8103
|
+
codeDesc,
|
|
6403
8104
|
startTime
|
|
6404
|
-
})
|
|
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}`)];
|
|
6405
8107
|
const req = createRequirement(result.sha256, filename, descriptions, scoreToImpact(score), results, { tags });
|
|
6406
8108
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6407
8109
|
const controlType = deriveControlTypeFromTags([...nist]);
|
|
@@ -6426,7 +8128,7 @@ function buildScannerBaseline(scannerName, results, shaMap, resultsChecksum) {
|
|
|
6426
8128
|
* @param input - Conveyor JSON string
|
|
6427
8129
|
* @returns HDF JSON string
|
|
6428
8130
|
*/
|
|
6429
|
-
async function convertConveyorToHdf(input) {
|
|
8131
|
+
async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
|
|
6430
8132
|
if (!input || input.trim().length === 0) throw new Error("conveyor: empty input");
|
|
6431
8133
|
validateInputSize(input, "conveyor");
|
|
6432
8134
|
const data = parseJSON(input);
|
|
@@ -6448,7 +8150,7 @@ async function convertConveyorToHdf(input) {
|
|
|
6448
8150
|
}));
|
|
6449
8151
|
return buildHdfResults({
|
|
6450
8152
|
generatorName: "conveyor-to-hdf",
|
|
6451
|
-
converterVersion
|
|
8153
|
+
converterVersion,
|
|
6452
8154
|
toolName: "Conveyor",
|
|
6453
8155
|
toolFormat: "JSON",
|
|
6454
8156
|
baselines,
|
|
@@ -6487,18 +8189,29 @@ const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
|
|
|
6487
8189
|
function veracodeSeverityToImpact(severity) {
|
|
6488
8190
|
return IMPACT_MAPPING$2.get(severity) ?? .1;
|
|
6489
8191
|
}
|
|
6490
|
-
/** Get an attribute from a parsed XML node. */
|
|
8192
|
+
/** Get an attribute from a parsed XML node, entity-decoded. */
|
|
6491
8193
|
function attr(node, name) {
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
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
|
+
};
|
|
6495
8204
|
function decodeXmlEntities(s) {
|
|
6496
|
-
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
|
+
});
|
|
6497
8210
|
}
|
|
6498
8211
|
/** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
|
|
6499
8212
|
function parseVeracodeTimestamp(ts) {
|
|
6500
8213
|
if (!ts) return void 0;
|
|
6501
|
-
return parseTimestamp(
|
|
8214
|
+
return parseTimestamp(ts.replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
|
|
6502
8215
|
}
|
|
6503
8216
|
/** Format description paragraphs into text. */
|
|
6504
8217
|
function formatDesc(desc) {
|
|
@@ -6631,12 +8344,20 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
|
6631
8344
|
const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
|
|
6632
8345
|
const results = cwes.flatMap((c) => {
|
|
6633
8346
|
const staticflaws = c.staticflaws;
|
|
6634
|
-
return ensureArray(staticflaws?.flaw).map((flaw) =>
|
|
8347
|
+
return ensureArray(staticflaws?.flaw).map((flaw) => ({
|
|
8348
|
+
status: ResultStatus.Failed,
|
|
6635
8349
|
codeDesc: formatFlawCodeDesc(flaw),
|
|
6636
8350
|
startTime
|
|
6637
8351
|
}));
|
|
6638
8352
|
});
|
|
6639
|
-
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
|
+
});
|
|
6640
8361
|
const controlType = deriveControlTypeFromTags(nist);
|
|
6641
8362
|
if (controlType !== void 0) req.controlType = controlType;
|
|
6642
8363
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -6686,16 +8407,26 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
6686
8407
|
if (cweID) extras.cwe = cweID;
|
|
6687
8408
|
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
6688
8409
|
const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
|
|
6689
|
-
const results = components.map((comp) =>
|
|
8410
|
+
const results = components.map((comp) => ({
|
|
8411
|
+
status: ResultStatus.Failed,
|
|
6690
8412
|
codeDesc: formatSCACodeDesc(comp),
|
|
6691
8413
|
startTime
|
|
6692
8414
|
}));
|
|
6693
8415
|
const cveSummary = attr(vuln, "cve_summary");
|
|
6694
8416
|
const cveId = attr(vuln, "cve_id");
|
|
6695
|
-
const
|
|
8417
|
+
const descriptions = [{
|
|
6696
8418
|
label: "default",
|
|
6697
8419
|
data: cveSummary || ""
|
|
6698
|
-
}]
|
|
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
|
+
});
|
|
6699
8430
|
const controlType = deriveControlTypeFromTags(nist);
|
|
6700
8431
|
if (controlType !== void 0) req.controlType = controlType;
|
|
6701
8432
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -6707,10 +8438,13 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
6707
8438
|
* @param input - Veracode DetailedReport XML string
|
|
6708
8439
|
* @returns HDF JSON string
|
|
6709
8440
|
*/
|
|
6710
|
-
async function convertVeracodeToHdf(input) {
|
|
8441
|
+
async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
|
|
6711
8442
|
if (!input || input.trim().length === 0) throw new Error("veracode: empty input");
|
|
6712
8443
|
validateInputSize(input, "veracode");
|
|
6713
|
-
const parsed = parseXml(input, {
|
|
8444
|
+
const parsed = parseXml(input, {
|
|
8445
|
+
attributeNamePrefix: "@_",
|
|
8446
|
+
trimValues: false
|
|
8447
|
+
});
|
|
6714
8448
|
if ("summaryreport" in parsed) throw new Error("veracode: summary reports are not supported; use a detailed report");
|
|
6715
8449
|
const report = parsed.detailedreport;
|
|
6716
8450
|
if (!report) throw new Error("veracode: invalid XML - no <detailedreport> root element");
|
|
@@ -6739,7 +8473,7 @@ async function convertVeracodeToHdf(input) {
|
|
|
6739
8473
|
const timestamp = parseVeracodeTimestamp(firstBuildDate);
|
|
6740
8474
|
return buildHdfResults({
|
|
6741
8475
|
generatorName: "veracode-to-hdf",
|
|
6742
|
-
converterVersion
|
|
8476
|
+
converterVersion,
|
|
6743
8477
|
toolName: "Veracode",
|
|
6744
8478
|
toolFormat: "XML",
|
|
6745
8479
|
baselines: [baseline],
|
|
@@ -6815,12 +8549,11 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
6815
8549
|
data: stripHTML(impacts[0])
|
|
6816
8550
|
});
|
|
6817
8551
|
}
|
|
6818
|
-
const
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
})], { 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 });
|
|
6824
8557
|
const controlType = deriveControlTypeFromTags(nist);
|
|
6825
8558
|
if (controlType !== void 0) req.controlType = controlType;
|
|
6826
8559
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -6835,7 +8568,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
6835
8568
|
* @param input - Combined JSON string with secureScore and profiles
|
|
6836
8569
|
* @returns HDF JSON string
|
|
6837
8570
|
*/
|
|
6838
|
-
async function convertMsftSecureScoreToHdf(input) {
|
|
8571
|
+
async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
|
|
6839
8572
|
if (!input || input.trim().length === 0) throw new Error("msft-secure-score: empty input");
|
|
6840
8573
|
validateInputSize(input, "msft-secure-score");
|
|
6841
8574
|
const combined = parseJSON(input);
|
|
@@ -6848,7 +8581,7 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
6848
8581
|
let tenantId = "";
|
|
6849
8582
|
return buildHdfResults({
|
|
6850
8583
|
generatorName: "msft-secure-score-to-hdf",
|
|
6851
|
-
converterVersion
|
|
8584
|
+
converterVersion,
|
|
6852
8585
|
toolName: "Microsoft Secure Score",
|
|
6853
8586
|
toolFormat: "JSON",
|
|
6854
8587
|
baselines: combined.secureScore.value.map((ss) => {
|
|
@@ -6856,14 +8589,17 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
6856
8589
|
const { items: limitedControlScores, truncated } = limitArray(ss.controlScores);
|
|
6857
8590
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
6858
8591
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedControlScores.length} controlScore items (original: ${ss.controlScores.length})`);
|
|
6859
|
-
|
|
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, {
|
|
6860
8595
|
resultsChecksum,
|
|
6861
|
-
title
|
|
8596
|
+
title
|
|
6862
8597
|
});
|
|
6863
8598
|
}),
|
|
6864
8599
|
components: [{
|
|
6865
8600
|
name: `Azure Tenant: ${tenantId}`,
|
|
6866
8601
|
type: TargetType.CloudAccount,
|
|
8602
|
+
provider: "azure",
|
|
6867
8603
|
labels: {
|
|
6868
8604
|
account: tenantId,
|
|
6869
8605
|
provider: "azure"
|
|
@@ -6879,12 +8615,12 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
6879
8615
|
* Delegates base conversion to the generic SARIF converter and enriches
|
|
6880
8616
|
* the output with MSDO-specific metadata.
|
|
6881
8617
|
*/
|
|
6882
|
-
async function convertMsftDefenderDevopsToHdf(input) {
|
|
8618
|
+
async function convertMsftDefenderDevopsToHdf(input, converterVersion = "1.0.0") {
|
|
6883
8619
|
validateInputSize(input, "msft-defender-devops");
|
|
6884
8620
|
const raw = parseJSON(input);
|
|
6885
8621
|
if (!raw || !Array.isArray(raw.runs)) throw new Error("Invalid MSDO SARIF structure: missing or invalid runs field");
|
|
6886
8622
|
const { components, runEnrichments } = extractEnrichments(raw);
|
|
6887
|
-
const hdfJson = await convertSarifToHdf(input);
|
|
8623
|
+
const hdfJson = await convertSarifToHdf(input, converterVersion);
|
|
6888
8624
|
const result = JSON.parse(hdfJson);
|
|
6889
8625
|
synthesizeNoFindingsPlaceholders(result);
|
|
6890
8626
|
applyEnrichments(result, components, runEnrichments);
|
|
@@ -6902,8 +8638,7 @@ function extractEnrichments(raw) {
|
|
|
6902
8638
|
const target = {
|
|
6903
8639
|
name: repoNameFromURI(vcp.repositoryUri),
|
|
6904
8640
|
type: TargetType.Repository,
|
|
6905
|
-
url: vcp.repositoryUri
|
|
6906
|
-
labels: {}
|
|
8641
|
+
url: vcp.repositoryUri
|
|
6907
8642
|
};
|
|
6908
8643
|
if (vcp.branch) target.branch = vcp.branch;
|
|
6909
8644
|
if (vcp.revisionId) target.commit = vcp.revisionId;
|
|
@@ -7030,10 +8765,14 @@ function buildRequirement(assessmentID, assessments, scanTime) {
|
|
|
7030
8765
|
function buildResultFromAssessment(a, scanTime) {
|
|
7031
8766
|
const status = mapStatus(a.properties.status.code);
|
|
7032
8767
|
const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
|
|
7033
|
-
|
|
8768
|
+
const message = a.properties.status.description || a.properties.status.cause || void 0;
|
|
8769
|
+
const result = {
|
|
8770
|
+
status,
|
|
7034
8771
|
codeDesc,
|
|
7035
8772
|
startTime: scanTime
|
|
7036
|
-
}
|
|
8773
|
+
};
|
|
8774
|
+
if (message !== void 0) result.message = message;
|
|
8775
|
+
return result;
|
|
7037
8776
|
}
|
|
7038
8777
|
/**
|
|
7039
8778
|
* Converts Microsoft Defender for Cloud assessment output to HDF format.
|
|
@@ -7041,7 +8780,7 @@ function buildResultFromAssessment(a, scanTime) {
|
|
|
7041
8780
|
* @param input - JSON string containing Azure Security Assessments API response
|
|
7042
8781
|
* @returns HDF JSON string
|
|
7043
8782
|
*/
|
|
7044
|
-
async function convertMsftDefenderCloudToHdf(input) {
|
|
8783
|
+
async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0") {
|
|
7045
8784
|
validateInputSize(input, "msft-defender-cloud");
|
|
7046
8785
|
const scanTime = /* @__PURE__ */ new Date();
|
|
7047
8786
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -7078,7 +8817,7 @@ async function convertMsftDefenderCloudToHdf(input) {
|
|
|
7078
8817
|
});
|
|
7079
8818
|
return buildHdfResults({
|
|
7080
8819
|
generatorName: "msft-defender-cloud-to-hdf",
|
|
7081
|
-
converterVersion
|
|
8820
|
+
converterVersion,
|
|
7082
8821
|
toolName: "Microsoft Defender for Cloud",
|
|
7083
8822
|
toolFormat: "JSON",
|
|
7084
8823
|
baselines: [baseline],
|
|
@@ -7100,7 +8839,7 @@ const IMPACT_MAPPING = {
|
|
|
7100
8839
|
/**
|
|
7101
8840
|
* Maps MDE severity to HDF impact value.
|
|
7102
8841
|
*/
|
|
7103
|
-
function severityToImpact$
|
|
8842
|
+
function severityToImpact$2(severity) {
|
|
7104
8843
|
return IMPACT_MAPPING[severity.toLowerCase()] ?? .5;
|
|
7105
8844
|
}
|
|
7106
8845
|
/**
|
|
@@ -7193,7 +8932,7 @@ function resolveStartTime(alert, scanTime) {
|
|
|
7193
8932
|
* Converts a single MDE alert to an HDF EvaluatedRequirement.
|
|
7194
8933
|
*/
|
|
7195
8934
|
function alertToRequirement(alert, scanTime) {
|
|
7196
|
-
const impact = severityToImpact$
|
|
8935
|
+
const impact = severityToImpact$2(alert.severity);
|
|
7197
8936
|
const status = statusToResultStatus(alert.status, alert.classification);
|
|
7198
8937
|
const codeDesc = formatEvidence(alert.evidence ?? []);
|
|
7199
8938
|
const results = [createResult(status, formatMessage(alert), {
|
|
@@ -7223,7 +8962,7 @@ function alertToRequirement(alert, scanTime) {
|
|
|
7223
8962
|
* @param input - JSON string of MDE alert response
|
|
7224
8963
|
* @returns HDF JSON string
|
|
7225
8964
|
*/
|
|
7226
|
-
async function convertMsftDefenderEndpointToHdf(input) {
|
|
8965
|
+
async function convertMsftDefenderEndpointToHdf(input, converterVersion = "1.0.0") {
|
|
7227
8966
|
validateInputSize(input, "msft-defender-endpoint");
|
|
7228
8967
|
const resultsChecksum = await inputChecksum(input);
|
|
7229
8968
|
const response = parseJSON(input);
|
|
@@ -7246,7 +8985,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
|
|
|
7246
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));
|
|
7247
8986
|
return buildHdfResults({
|
|
7248
8987
|
generatorName: "msft-defender-endpoint-to-hdf",
|
|
7249
|
-
converterVersion
|
|
8988
|
+
converterVersion,
|
|
7250
8989
|
toolName: "Microsoft Defender for Endpoint",
|
|
7251
8990
|
baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
|
|
7252
8991
|
components,
|
|
@@ -7264,7 +9003,8 @@ const BUILD_OPTIONS = {
|
|
|
7264
9003
|
ignoreAttributes: false,
|
|
7265
9004
|
format: true,
|
|
7266
9005
|
indentBy: " ",
|
|
7267
|
-
suppressEmptyNode: false
|
|
9006
|
+
suppressEmptyNode: false,
|
|
9007
|
+
suppressBooleanAttributes: false
|
|
7268
9008
|
};
|
|
7269
9009
|
/**
|
|
7270
9010
|
* Convert HDF Results JSON to XCCDF 1.2 Benchmark XML.
|
|
@@ -7274,7 +9014,7 @@ const BUILD_OPTIONS = {
|
|
|
7274
9014
|
*/
|
|
7275
9015
|
function convertHdfToXccdf(input) {
|
|
7276
9016
|
validateInputSize(input, "hdf-to-xccdf");
|
|
7277
|
-
const hdfData =
|
|
9017
|
+
const hdfData = parseHdf(input);
|
|
7278
9018
|
if (!hdfData || typeof hdfData !== "object" || !("baselines" in hdfData)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
7279
9019
|
if (!Array.isArray(hdfData.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
7280
9020
|
return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ Benchmark: buildBenchmarkObj(hdfData) }, BUILD_OPTIONS)}`;
|
|
@@ -7310,19 +9050,19 @@ function sanitizeXccdfId(id) {
|
|
|
7310
9050
|
}
|
|
7311
9051
|
/** Build the Benchmark XML object from HDF data. */
|
|
7312
9052
|
function buildBenchmarkObj(hdfData) {
|
|
9053
|
+
const empty = !hdfData.baselines || hdfData.baselines.length === 0;
|
|
7313
9054
|
const benchmark = {
|
|
7314
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),
|
|
7315
9057
|
[`${ATTR}resolved`]: "1",
|
|
7316
9058
|
status: wrap("incomplete")
|
|
7317
9059
|
};
|
|
7318
|
-
if (
|
|
7319
|
-
benchmark[`${ATTR}id`] = "xccdf_hdf_benchmark_exported";
|
|
9060
|
+
if (empty) {
|
|
7320
9061
|
benchmark.title = wrap("HDF Export");
|
|
7321
9062
|
benchmark.version = wrap("1.0");
|
|
7322
9063
|
return benchmark;
|
|
7323
9064
|
}
|
|
7324
9065
|
const baseline = hdfData.baselines[0];
|
|
7325
|
-
benchmark[`${ATTR}id`] = sanitizeXccdfId("xccdf_hdf_benchmark_" + baseline.name);
|
|
7326
9066
|
benchmark.title = wrap(baseline.title ?? baseline.name);
|
|
7327
9067
|
benchmark.version = wrap(baseline.version ?? "1.0");
|
|
7328
9068
|
benchmark.Profile = {
|
|
@@ -7344,20 +9084,40 @@ function buildRuleObj(req) {
|
|
|
7344
9084
|
};
|
|
7345
9085
|
const description = findDescription(req.descriptions, "default");
|
|
7346
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);
|
|
7347
9098
|
const fixtext = findDescription(req.descriptions, "fix");
|
|
7348
9099
|
if (fixtext) rule.fixtext = wrap(fixtext);
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
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 = [];
|
|
7356
9111
|
const checkContent = findDescription(req.descriptions, "check");
|
|
7357
|
-
if (checkContent)
|
|
9112
|
+
if (checkContent) checks.push({
|
|
7358
9113
|
[`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
|
|
7359
9114
|
"check-content": wrap(checkContent)
|
|
7360
|
-
};
|
|
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;
|
|
7361
9121
|
return rule;
|
|
7362
9122
|
}
|
|
7363
9123
|
/** Build the XCCDF TestResult object. */
|
|
@@ -7577,7 +9337,7 @@ async function convertHdfToOscalSar(input) {
|
|
|
7577
9337
|
if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-sar: empty input");
|
|
7578
9338
|
let hdfResults;
|
|
7579
9339
|
try {
|
|
7580
|
-
hdfResults =
|
|
9340
|
+
hdfResults = parseHdf(input);
|
|
7581
9341
|
} catch {
|
|
7582
9342
|
throw new Error("hdf-to-oscal-sar: failed to parse HDF JSON");
|
|
7583
9343
|
}
|
|
@@ -7589,8 +9349,9 @@ async function convertHdfToOscalSar(input) {
|
|
|
7589
9349
|
* Constructs the full OSCAL assessment-results document from HDF results.
|
|
7590
9350
|
*/
|
|
7591
9351
|
function buildOSCALDocument(hdfResults) {
|
|
7592
|
-
let timestamp = (/* @__PURE__ */ new Date())
|
|
7593
|
-
|
|
9352
|
+
let timestamp = formatTimestampSeconds(/* @__PURE__ */ new Date());
|
|
9353
|
+
const documentTime = hdfTime(hdfResults.timestamp);
|
|
9354
|
+
if (documentTime) timestamp = formatTimestampSeconds(documentTime);
|
|
7594
9355
|
const metadata = {
|
|
7595
9356
|
title: "HDF Assessment Results Export",
|
|
7596
9357
|
"last-modified": timestamp,
|
|
@@ -7609,6 +9370,34 @@ function buildOSCALDocument(hdfResults) {
|
|
|
7609
9370
|
results
|
|
7610
9371
|
} };
|
|
7611
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
|
+
}
|
|
7612
9401
|
/**
|
|
7613
9402
|
* Converts a single EvaluatedBaseline to an OSCAL Result.
|
|
7614
9403
|
*/
|
|
@@ -7630,7 +9419,7 @@ function baselineToResult(baseline, timestamp) {
|
|
|
7630
9419
|
uuid: crypto.randomUUID(),
|
|
7631
9420
|
title,
|
|
7632
9421
|
description,
|
|
7633
|
-
start: timestamp,
|
|
9422
|
+
start: assessmentStart(baseline, timestamp),
|
|
7634
9423
|
findings,
|
|
7635
9424
|
observations,
|
|
7636
9425
|
risks
|
|
@@ -7644,14 +9433,59 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
7644
9433
|
const { state, reason } = aggregateStatus(req.results);
|
|
7645
9434
|
const findingDesc = extractDefaultDescription(req.descriptions);
|
|
7646
9435
|
const props = [];
|
|
7647
|
-
|
|
7648
|
-
const
|
|
7649
|
-
if (Array.isArray(
|
|
7650
|
-
for (const v of
|
|
7651
|
-
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,
|
|
7652
9441
|
value: v
|
|
7653
9442
|
});
|
|
7654
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
|
+
});
|
|
7655
9489
|
}
|
|
7656
9490
|
let title = req.id;
|
|
7657
9491
|
if (req.title && req.title !== "") title = req.title;
|
|
@@ -7667,6 +9501,7 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
7667
9501
|
title,
|
|
7668
9502
|
description: findingDesc || "",
|
|
7669
9503
|
props: props.length > 0 ? props : void 0,
|
|
9504
|
+
links: links.length > 0 ? links : void 0,
|
|
7670
9505
|
target
|
|
7671
9506
|
};
|
|
7672
9507
|
let observation;
|
|
@@ -7676,7 +9511,7 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
7676
9511
|
uuid: obsUUID,
|
|
7677
9512
|
description: buildObservationDescription(req.results),
|
|
7678
9513
|
methods: ["TEST"],
|
|
7679
|
-
collected: timestamp
|
|
9514
|
+
collected: formatAssessmentTime(earliestResultTime(req.results), timestamp)
|
|
7680
9515
|
};
|
|
7681
9516
|
finding["related-observations"] = [{ "observation-uuid": obsUUID }];
|
|
7682
9517
|
}
|
|
@@ -7806,18 +9641,24 @@ async function convertHdfToOscalPoam(input) {
|
|
|
7806
9641
|
if (!input || input.trim().length === 0) throw new Error("hdf-to-oscal-poam: empty input");
|
|
7807
9642
|
let amendments;
|
|
7808
9643
|
try {
|
|
7809
|
-
amendments =
|
|
9644
|
+
amendments = parseHdf(input);
|
|
7810
9645
|
} catch {
|
|
7811
9646
|
throw new Error("hdf-to-oscal-poam: failed to parse JSON");
|
|
7812
9647
|
}
|
|
7813
9648
|
const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendments) };
|
|
7814
9649
|
return JSON.stringify(doc, null, 2);
|
|
7815
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
|
+
}
|
|
7816
9657
|
/**
|
|
7817
9658
|
* Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
|
|
7818
9659
|
*/
|
|
7819
9660
|
function amendmentsToPOAM(amendments) {
|
|
7820
|
-
const now = (/* @__PURE__ */ new Date())
|
|
9661
|
+
const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
|
|
7821
9662
|
const metadata = {
|
|
7822
9663
|
title: amendments.name,
|
|
7823
9664
|
"last-modified": now,
|
|
@@ -7868,25 +9709,45 @@ function overrideToPOAMItem(override) {
|
|
|
7868
9709
|
name: "impacted-control-id",
|
|
7869
9710
|
value: nistTagToControlId(override.requirementId)
|
|
7870
9711
|
}];
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
lifecycle: "planned",
|
|
7875
|
-
title: ms.description,
|
|
7876
|
-
description: ms.description
|
|
9712
|
+
if (override.type) riskProps.push({
|
|
9713
|
+
name: "override-type",
|
|
9714
|
+
value: String(override.type)
|
|
7877
9715
|
});
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
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({
|
|
7883
9735
|
uuid: crypto.randomUUID(),
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
}
|
|
9736
|
+
lifecycle: "planned",
|
|
9737
|
+
title: ms.description,
|
|
9738
|
+
description: ms.description,
|
|
9739
|
+
props: msProps.length > 0 ? msProps : void 0
|
|
9740
|
+
});
|
|
7889
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
|
+
}] };
|
|
7890
9751
|
const risk = {
|
|
7891
9752
|
uuid: riskUUID,
|
|
7892
9753
|
title: override.requirementId,
|
|
@@ -7958,7 +9819,7 @@ function buildTags(dep) {
|
|
|
7958
9819
|
if (dep.parentDependencies.length > 0) extras.parentDependencies = dep.parentDependencies;
|
|
7959
9820
|
return buildNistCciTags(nist, cciTags, extras);
|
|
7960
9821
|
}
|
|
7961
|
-
async function convertIonchannelToHdf(input) {
|
|
9822
|
+
async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
|
|
7962
9823
|
if (!input?.trim()) throw new Error("Empty input");
|
|
7963
9824
|
validateInputSize(input, "ionchannel");
|
|
7964
9825
|
const analysis = parseJSON(input);
|
|
@@ -7985,10 +9846,11 @@ async function convertIonchannelToHdf(input) {
|
|
|
7985
9846
|
outdated_version: dep.outdated_version,
|
|
7986
9847
|
dependencies: dep.dependencies
|
|
7987
9848
|
}, null, 2);
|
|
7988
|
-
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,
|
|
7989
9851
|
codeDesc: "Dependency inventory item",
|
|
7990
9852
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
7991
|
-
}
|
|
9853
|
+
}], { tags });
|
|
7992
9854
|
req.code = code;
|
|
7993
9855
|
const controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
|
|
7994
9856
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -7996,20 +9858,22 @@ async function convertIonchannelToHdf(input) {
|
|
|
7996
9858
|
return req;
|
|
7997
9859
|
});
|
|
7998
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";
|
|
7999
9871
|
return buildHdfResults({
|
|
8000
9872
|
generatorName: "ionchannel-to-hdf",
|
|
8001
|
-
converterVersion
|
|
9873
|
+
converterVersion,
|
|
8002
9874
|
toolName: "Ion Channel",
|
|
8003
9875
|
toolFormat: "JSON",
|
|
8004
|
-
baselines: [
|
|
8005
|
-
title: `Ion Channel Analysis of ${analysis.source}`,
|
|
8006
|
-
summary: analysis.summary,
|
|
8007
|
-
integrity: {
|
|
8008
|
-
algorithm: resultsChecksum.algorithm,
|
|
8009
|
-
checksum: resultsChecksum.value
|
|
8010
|
-
},
|
|
8011
|
-
status: "loaded"
|
|
8012
|
-
})],
|
|
9876
|
+
baselines: [baseline],
|
|
8013
9877
|
timestamp: /* @__PURE__ */ new Date()
|
|
8014
9878
|
});
|
|
8015
9879
|
}
|
|
@@ -8222,6 +10086,40 @@ function affectedPackageToIdentifier(pkg) {
|
|
|
8222
10086
|
if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
|
|
8223
10087
|
if (pkg.name) return pkg.name;
|
|
8224
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
|
+
}
|
|
8225
10123
|
/**
|
|
8226
10124
|
* Build an HDF Evidence entry pointing at the upstream VEX document. Used by
|
|
8227
10125
|
* importers to preserve provenance even though we lose the structured
|
|
@@ -8716,12 +10614,27 @@ function publisherIdentityOrDefault(bom) {
|
|
|
8716
10614
|
* fields the shared VEX mapping considers consumer-action-bearing survive
|
|
8717
10615
|
* round-trip; the rest collapse into the available CSAF fields.
|
|
8718
10616
|
*/
|
|
10617
|
+
const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
8719
10618
|
const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
|
|
8720
10619
|
const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
|
|
8721
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
|
+
}
|
|
8722
10635
|
function convertHdfToCsafVex(input, converterVersion) {
|
|
8723
10636
|
validateInputSize(input, "hdf-to-csaf-vex");
|
|
8724
|
-
const amendments =
|
|
10637
|
+
const amendments = parseHdf(input);
|
|
8725
10638
|
const groups = groupByCVE(amendments.overrides ?? []);
|
|
8726
10639
|
const productSet = /* @__PURE__ */ new Map();
|
|
8727
10640
|
const vulnerabilities = [];
|
|
@@ -8730,6 +10643,7 @@ function convertHdfToCsafVex(input, converterVersion) {
|
|
|
8730
10643
|
if (!v) continue;
|
|
8731
10644
|
vulnerabilities.push(v);
|
|
8732
10645
|
for (const p of productIDsForGroup(group)) productSet.set(p, true);
|
|
10646
|
+
for (const p of fixedProductIDsForGroup(group)) productSet.set(p, true);
|
|
8733
10647
|
}
|
|
8734
10648
|
if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8735
10649
|
const products = [...productSet.keys()].sort();
|
|
@@ -8765,6 +10679,15 @@ function productIDsForGroup(group) {
|
|
|
8765
10679
|
for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
|
|
8766
10680
|
return [...seen].sort();
|
|
8767
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
|
+
}
|
|
8768
10691
|
function productIDsFor$1(o) {
|
|
8769
10692
|
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8770
10693
|
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
@@ -8791,6 +10714,25 @@ function buildVulnerability(group) {
|
|
|
8791
10714
|
let emitted = false;
|
|
8792
10715
|
for (const o of group.overrides) {
|
|
8793
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
|
+
}
|
|
8794
10736
|
let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
|
|
8795
10737
|
if (!canonical) continue;
|
|
8796
10738
|
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
|
|
@@ -8800,8 +10742,8 @@ function buildVulnerability(group) {
|
|
|
8800
10742
|
v.flags = v.flags ?? [];
|
|
8801
10743
|
v.flags.push({
|
|
8802
10744
|
label: String(o.justification),
|
|
8803
|
-
|
|
8804
|
-
|
|
10745
|
+
date: formatTimestampSeconds(hdfTime(o.appliedAt) ?? GO_ZERO_TIME$1),
|
|
10746
|
+
product_ids: pids
|
|
8805
10747
|
});
|
|
8806
10748
|
}
|
|
8807
10749
|
emitted = true;
|
|
@@ -8851,9 +10793,9 @@ function buildVulnerability(group) {
|
|
|
8851
10793
|
/* c8 ignore next */
|
|
8852
10794
|
if (!emitted) return void 0;
|
|
8853
10795
|
if (status.fixed) status.fixed = [...new Set(status.fixed)];
|
|
10796
|
+
if (status.first_fixed) status.first_fixed = [...new Set(status.first_fixed)];
|
|
8854
10797
|
if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
|
|
8855
10798
|
if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
|
|
8856
|
-
v.product_status = status;
|
|
8857
10799
|
if (v.references) {
|
|
8858
10800
|
const seen = /* @__PURE__ */ new Set();
|
|
8859
10801
|
v.references = v.references.filter((r) => {
|
|
@@ -8862,10 +10804,30 @@ function buildVulnerability(group) {
|
|
|
8862
10804
|
return true;
|
|
8863
10805
|
});
|
|
8864
10806
|
}
|
|
8865
|
-
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();
|
|
8866
10828
|
}
|
|
8867
10829
|
function buildDocument(amendments, converterVersion) {
|
|
8868
|
-
const now = formatTimestampSeconds(
|
|
10830
|
+
const now = formatTimestampSeconds(earliestAppliedAt(amendments));
|
|
8869
10831
|
const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8870
10832
|
const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
|
|
8871
10833
|
const docVersion = amendments.version || "1";
|
|
@@ -8881,7 +10843,7 @@ function buildDocument(amendments, converterVersion) {
|
|
|
8881
10843
|
category: "csaf_vex",
|
|
8882
10844
|
csaf_version: "2.0",
|
|
8883
10845
|
title,
|
|
8884
|
-
notes,
|
|
10846
|
+
...notes.length > 0 && { notes },
|
|
8885
10847
|
publisher: {
|
|
8886
10848
|
category: "other",
|
|
8887
10849
|
name: publisherName
|
|
@@ -8919,6 +10881,7 @@ function buildDocument(amendments, converterVersion) {
|
|
|
8919
10881
|
* partial-fidelity by design — consumer-action-bearing fields (CVE id,
|
|
8920
10882
|
* status, justification) survive round-trip; the rest collapse.
|
|
8921
10883
|
*/
|
|
10884
|
+
const GO_ZERO_TIME = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
8922
10885
|
const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
|
|
8923
10886
|
const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
|
|
8924
10887
|
const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
@@ -8926,14 +10889,15 @@ const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
|
|
|
8926
10889
|
const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
|
|
8927
10890
|
async function convertHdfToOpenVex(input, converterVersion) {
|
|
8928
10891
|
validateInputSize(input, "hdf-to-openvex");
|
|
8929
|
-
const amendments =
|
|
10892
|
+
const amendments = parseHdf(input);
|
|
8930
10893
|
const statements = [];
|
|
8931
10894
|
let earliest;
|
|
8932
10895
|
for (const o of amendments.overrides ?? []) {
|
|
8933
10896
|
const s = overrideToStatement(o);
|
|
8934
10897
|
if (!s) continue;
|
|
8935
10898
|
statements.push(s);
|
|
8936
|
-
const t =
|
|
10899
|
+
const t = hdfTime(o.appliedAt);
|
|
10900
|
+
if (!t) continue;
|
|
8937
10901
|
if (!earliest || t < earliest) earliest = t;
|
|
8938
10902
|
}
|
|
8939
10903
|
if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
@@ -8956,22 +10920,24 @@ function overrideToStatement(o) {
|
|
|
8956
10920
|
let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
|
|
8957
10921
|
if (!canonical) return void 0;
|
|
8958
10922
|
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
|
|
8959
|
-
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 {
|
|
8960
10930
|
vulnerability: {
|
|
8961
|
-
|
|
8962
|
-
|
|
10931
|
+
"@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`,
|
|
10932
|
+
name: o.requirementId
|
|
8963
10933
|
},
|
|
10934
|
+
products: productsFor(o),
|
|
8964
10935
|
status: String(canonical),
|
|
8965
|
-
|
|
8966
|
-
|
|
10936
|
+
...justification && { justification },
|
|
10937
|
+
...impact && { impact_statement: impact },
|
|
10938
|
+
...action && { action_statement: action },
|
|
10939
|
+
timestamp: formatTimestampSeconds(hdfTime(o.appliedAt) ?? GO_ZERO_TIME)
|
|
8967
10940
|
};
|
|
8968
|
-
if (canonical === VexStatus.NotAffected) {
|
|
8969
|
-
if (o.justification) stmt.justification = String(o.justification);
|
|
8970
|
-
const impact = stripProductsLine(o.reason ?? "");
|
|
8971
|
-
if (impact) stmt.impact_statement = impact;
|
|
8972
|
-
} else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
|
|
8973
|
-
else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
|
|
8974
|
-
return stmt;
|
|
8975
10941
|
}
|
|
8976
10942
|
function productsFor(o) {
|
|
8977
10943
|
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
@@ -9000,7 +10966,8 @@ function stripProductsLine(reason) {
|
|
|
9000
10966
|
}
|
|
9001
10967
|
async function buildDocumentID(input, a) {
|
|
9002
10968
|
if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
|
|
9003
|
-
|
|
10969
|
+
const digest = await sha256(input);
|
|
10970
|
+
return `${OPENVEX_NAMESPACE}vex-${digest}`;
|
|
9004
10971
|
}
|
|
9005
10972
|
//#endregion
|
|
9006
10973
|
//#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
|
|
@@ -9017,9 +10984,41 @@ const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
|
|
|
9017
10984
|
const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
|
|
9018
10985
|
const RESPONSE_LINE = /^Response:.*$/gm;
|
|
9019
10986
|
const DEFAULT_PRODUCT_ID = "HDFPID-0001";
|
|
9020
|
-
|
|
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) {
|
|
9021
11020
|
validateInputSize(input, "hdf-to-cyclonedx-vex");
|
|
9022
|
-
const amendments =
|
|
11021
|
+
const amendments = parseHdf(input);
|
|
9023
11022
|
const componentRegistry = /* @__PURE__ */ new Map();
|
|
9024
11023
|
const vulnerabilities = [];
|
|
9025
11024
|
let earliest;
|
|
@@ -9028,7 +11027,8 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
|
9028
11027
|
const v = overrideToVulnerability(o, componentRegistry);
|
|
9029
11028
|
if (!v) continue;
|
|
9030
11029
|
vulnerabilities.push(v);
|
|
9031
|
-
const t =
|
|
11030
|
+
const t = hdfTime(o.appliedAt);
|
|
11031
|
+
if (!t) continue;
|
|
9032
11032
|
if (!earliest || t < earliest) earliest = t;
|
|
9033
11033
|
}
|
|
9034
11034
|
if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
@@ -9037,7 +11037,7 @@ function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
|
9037
11037
|
const bom = {
|
|
9038
11038
|
bomFormat: "CycloneDX",
|
|
9039
11039
|
specVersion: "1.4",
|
|
9040
|
-
serialNumber:
|
|
11040
|
+
serialNumber: await buildSerialNumber(input, amendments),
|
|
9041
11041
|
version: 1,
|
|
9042
11042
|
metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
|
|
9043
11043
|
components,
|
|
@@ -9061,23 +11061,14 @@ function overrideToVulnerability(o, componentRegistry) {
|
|
|
9061
11061
|
const cdxJust = justificationForCycloneDX(o.justification);
|
|
9062
11062
|
if (cdxJust) analysis.justification = cdxJust;
|
|
9063
11063
|
}
|
|
9064
|
-
const detail = stripReasonAnnotations(o.reason ?? "");
|
|
9065
|
-
if (detail) analysis.detail = detail;
|
|
9066
11064
|
if (canonical === VexStatus.Fixed) analysis.response = ["update"];
|
|
9067
11065
|
else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
|
|
9068
|
-
const
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
name: "NVD",
|
|
9072
|
-
url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
9073
|
-
},
|
|
9074
|
-
analysis,
|
|
9075
|
-
affects: pids.map((p) => ({ ref: p }))
|
|
9076
|
-
};
|
|
11066
|
+
const detail = stripReasonAnnotations(o.reason ?? "");
|
|
11067
|
+
if (detail) analysis.detail = detail;
|
|
11068
|
+
const references = [];
|
|
9077
11069
|
for (const e of o.evidence ?? []) {
|
|
9078
11070
|
if (e.type !== "url" || !e.data) continue;
|
|
9079
|
-
|
|
9080
|
-
v.references.push({
|
|
11071
|
+
references.push({
|
|
9081
11072
|
id: o.requirementId,
|
|
9082
11073
|
source: {
|
|
9083
11074
|
name: e.description ?? "",
|
|
@@ -9085,7 +11076,26 @@ function overrideToVulnerability(o, componentRegistry) {
|
|
|
9085
11076
|
}
|
|
9086
11077
|
});
|
|
9087
11078
|
}
|
|
9088
|
-
|
|
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
|
+
};
|
|
9089
11099
|
}
|
|
9090
11100
|
function canonicalToCycloneDXState(canonical) {
|
|
9091
11101
|
switch (canonical) {
|
|
@@ -9144,18 +11154,9 @@ function buildMetadata(a, docTime, converterVersion) {
|
|
|
9144
11154
|
}
|
|
9145
11155
|
return metadata;
|
|
9146
11156
|
}
|
|
9147
|
-
function
|
|
11157
|
+
async function buildSerialNumber(input, a) {
|
|
9148
11158
|
if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
|
|
9149
|
-
return `urn:uuid:${
|
|
9150
|
-
}
|
|
9151
|
-
function fnv1a(s) {
|
|
9152
|
-
let h = 2166136261;
|
|
9153
|
-
for (let i = 0; i < s.length; i++) {
|
|
9154
|
-
h ^= s.charCodeAt(i);
|
|
9155
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
9156
|
-
}
|
|
9157
|
-
const d = h.toString(16).padStart(8, "0");
|
|
9158
|
-
return (d + d + d + d).slice(0, 32);
|
|
11159
|
+
return `urn:uuid:${(await sha256(input)).slice(0, 32)}`;
|
|
9159
11160
|
}
|
|
9160
11161
|
//#endregion
|
|
9161
11162
|
//#region converters/oscal-to-hdf/typescript/detect.ts
|
|
@@ -10167,6 +12168,6 @@ function sarBaselineName(result, sar) {
|
|
|
10167
12168
|
return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
|
|
10168
12169
|
}
|
|
10169
12170
|
//#endregion
|
|
10170
|
-
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 };
|
|
10171
12172
|
|
|
10172
12173
|
//# sourceMappingURL=index.js.map
|