@mitre/hdf-converters 3.2.0 → 3.3.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 +7 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2062 -170
- package/dist/index.js.map +1 -1
- package/dist/{register-all-ik8sNfNf.js → register-all-C3lYICDC.js} +63 -3
- package/dist/register-all-C3lYICDC.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/package.json +9 -6
- package/dist/register-all-ik8sNfNf.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-
|
|
1
|
+
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-C3lYICDC.js";
|
|
2
2
|
import { flattenOverlays } from "@mitre/hdf-parsers";
|
|
3
|
-
import { buildCsv, buildXml, parseCsv, parseJSON, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML } from "@mitre/hdf-utilities";
|
|
4
|
-
import { Applicability, AuthorizationStatus,
|
|
3
|
+
import { buildCsv, buildXml, cvssScoreToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML } 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";
|
|
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, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getCCINistMappings, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, nistToCci } from "@mitre/hdf-mappings";
|
|
6
6
|
//#region shared/typescript/converterutil.ts
|
|
7
7
|
/**
|
|
@@ -30,7 +30,7 @@ async function inputChecksum(input) {
|
|
|
30
30
|
* Compute an Integrity object (for root-level document integrity) from raw input.
|
|
31
31
|
*
|
|
32
32
|
* Returns an Integrity with algorithm and checksum fields, suitable for
|
|
33
|
-
*
|
|
33
|
+
* HDFBaseline.integrity, HDFSystem.integrity, HDFPlan.integrity, etc.
|
|
34
34
|
*
|
|
35
35
|
* @param input - Raw input string (JSON, XML, etc.)
|
|
36
36
|
* @returns Integrity object with SHA-256 algorithm and checksum
|
|
@@ -120,7 +120,7 @@ function mapCWEToNIST(cweIDs, fallback) {
|
|
|
120
120
|
return controls.size > 0 ? [...controls].sort() : fallback;
|
|
121
121
|
}
|
|
122
122
|
/** Matches CWE identifiers like "CWE-79", "CWE 89", "cwe22". */
|
|
123
|
-
const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
123
|
+
const CWE_PATTERN$1 = /CWE[- ]?(\d+)/gi;
|
|
124
124
|
/**
|
|
125
125
|
* Extract all numeric CWE IDs from text.
|
|
126
126
|
* Returns deduplicated sorted array of numeric ID strings (e.g., ["79", "89"]).
|
|
@@ -129,7 +129,7 @@ const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
|
129
129
|
* @returns Sorted, deduplicated numeric CWE ID strings
|
|
130
130
|
*/
|
|
131
131
|
function extractCWEIDs(text) {
|
|
132
|
-
const matches = [...text.matchAll(CWE_PATTERN)];
|
|
132
|
+
const matches = [...text.matchAll(CWE_PATTERN$1)];
|
|
133
133
|
if (matches.length === 0) return [];
|
|
134
134
|
const ids = [...new Set(matches.map((m) => m[1]))];
|
|
135
135
|
ids.sort();
|
|
@@ -175,10 +175,59 @@ function ensureArray(value) {
|
|
|
175
175
|
*/
|
|
176
176
|
const DEFAULT_REMEDIATION_NIST_TAGS$1 = ["SI-2", "RA-5"];
|
|
177
177
|
/**
|
|
178
|
+
* Map a PURL `type` segment to the AffectedPackage `ecosystem` enum.
|
|
179
|
+
* Unknown types fall back to `generic`, which the schema enum permits
|
|
180
|
+
* as a catch-all.
|
|
181
|
+
*/
|
|
182
|
+
const PURL_TYPE_TO_ECOSYSTEM = {
|
|
183
|
+
npm: Ecosystem.Npm,
|
|
184
|
+
pypi: Ecosystem.Pypi,
|
|
185
|
+
rpm: Ecosystem.RPM,
|
|
186
|
+
deb: Ecosystem.Deb,
|
|
187
|
+
maven: Ecosystem.Maven,
|
|
188
|
+
gem: Ecosystem.Gem,
|
|
189
|
+
nuget: Ecosystem.Nuget,
|
|
190
|
+
golang: Ecosystem.Go,
|
|
191
|
+
go: Ecosystem.Go,
|
|
192
|
+
cargo: Ecosystem.Cargo
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Resolve an Ecosystem from a PURL type string. Returns `generic` for
|
|
196
|
+
* unknown types so callers can keep the schema's name+version+ecosystem
|
|
197
|
+
* triple valid without inventing a synthetic ecosystem.
|
|
198
|
+
*/
|
|
199
|
+
function ecosystemFromPurlType(type) {
|
|
200
|
+
if (!type) return Ecosystem.Generic;
|
|
201
|
+
return PURL_TYPE_TO_ECOSYSTEM[type.toLowerCase()] ?? Ecosystem.Generic;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build an Affected_Package primitive from any combination of the
|
|
205
|
+
* vocabulary the schema accepts (purl / cpe / name+version+ecosystem).
|
|
206
|
+
* Returns undefined when no identifier or full triple is present —
|
|
207
|
+
* callers should skip the entry rather than emit a schema-invalid
|
|
208
|
+
* AffectedPackage. Empty strings are treated as missing.
|
|
209
|
+
*
|
|
210
|
+
* The schema's anyOf requires at least one of:
|
|
211
|
+
* - name + version + ecosystem
|
|
212
|
+
* - purl alone
|
|
213
|
+
* - cpe alone
|
|
214
|
+
*/
|
|
215
|
+
function buildAffectedPackage$1(opts) {
|
|
216
|
+
const pkg = {};
|
|
217
|
+
if (opts.purl) pkg.purl = opts.purl;
|
|
218
|
+
if (opts.cpe) pkg.cpe = opts.cpe;
|
|
219
|
+
if (opts.name) pkg.name = opts.name;
|
|
220
|
+
if (opts.version) pkg.version = opts.version;
|
|
221
|
+
if (opts.ecosystem) pkg.ecosystem = opts.ecosystem;
|
|
222
|
+
if (opts.fixedInVersion) pkg.fixedInVersion = opts.fixedInVersion;
|
|
223
|
+
if (!Boolean(pkg.name && pkg.version && pkg.ecosystem) && !pkg.purl && !pkg.cpe) return void 0;
|
|
224
|
+
return pkg;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
178
227
|
* Build an HDF Results document from options.
|
|
179
228
|
*
|
|
180
229
|
* Eliminates the repeated boilerplate of constructing generator, tool,
|
|
181
|
-
* and assembling the top-level
|
|
230
|
+
* and assembling the top-level HDFResults in every converter. Mirrors
|
|
182
231
|
* the Go shared.BuildHDFResults() function.
|
|
183
232
|
*
|
|
184
233
|
* @returns JSON string of the HDF Results document (pretty-printed)
|
|
@@ -333,7 +382,24 @@ function deriveControlTypeFromTags(tags) {
|
|
|
333
382
|
*/
|
|
334
383
|
function deriveVerificationMethod(code) {
|
|
335
384
|
if (code === void 0 || code === null || code === "") return void 0;
|
|
336
|
-
return VerificationMethodEnum
|
|
385
|
+
return VerificationMethodEnum.Automated;
|
|
386
|
+
}
|
|
387
|
+
function buildNoFindingsRequirement(id, codeDesc, startTime) {
|
|
388
|
+
return {
|
|
389
|
+
id,
|
|
390
|
+
title: "No findings reported",
|
|
391
|
+
impact: 0,
|
|
392
|
+
descriptions: [{
|
|
393
|
+
label: "default",
|
|
394
|
+
data: codeDesc
|
|
395
|
+
}],
|
|
396
|
+
results: [{
|
|
397
|
+
status: ResultStatus.Passed,
|
|
398
|
+
codeDesc,
|
|
399
|
+
startTime
|
|
400
|
+
}],
|
|
401
|
+
tags: {}
|
|
402
|
+
};
|
|
337
403
|
}
|
|
338
404
|
//#endregion
|
|
339
405
|
//#region converters/legacyhdf-to-hdf/typescript/converter.ts
|
|
@@ -380,7 +446,7 @@ function impactToSeverity$2(impact) {
|
|
|
380
446
|
* Normalize status values from v1.0 to v2.0 format.
|
|
381
447
|
* Converts snake_case to camelCase.
|
|
382
448
|
*/
|
|
383
|
-
function normalizeStatus(status) {
|
|
449
|
+
function normalizeStatus$1(status) {
|
|
384
450
|
return {
|
|
385
451
|
"passed": "passed",
|
|
386
452
|
"failed": "failed",
|
|
@@ -426,7 +492,7 @@ function computeEffectiveStatus(impact, results) {
|
|
|
426
492
|
* Transforms snake_case field names to camelCase.
|
|
427
493
|
*/
|
|
428
494
|
function convertResult(v1Result) {
|
|
429
|
-
const v2Result = { status: normalizeStatus(v1Result.status) };
|
|
495
|
+
const v2Result = { status: normalizeStatus$1(v1Result.status) };
|
|
430
496
|
if (v1Result.code_desc !== void 0) v2Result.codeDesc = v1Result.code_desc;
|
|
431
497
|
if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
|
|
432
498
|
if (v1Result.start_time !== void 0) v2Result.startTime = v1Result.start_time;
|
|
@@ -470,7 +536,7 @@ function convertControl(v1Control) {
|
|
|
470
536
|
if (v1Control.code !== void 0) v2Req.code = v1Control.code;
|
|
471
537
|
if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
|
|
472
538
|
if (v1Control.waiver_data !== void 0) v2Req.waiverData = v1Control.waiver_data;
|
|
473
|
-
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus(v1Control.status);
|
|
539
|
+
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
|
|
474
540
|
if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
|
|
475
541
|
if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
|
|
476
542
|
v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity$2(v1Control.impact);
|
|
@@ -682,18 +748,19 @@ async function convertSarifToHdf(input) {
|
|
|
682
748
|
const { items: limitedRuns, truncated: truncatedRuns } = limitArray(sarif.runs);
|
|
683
749
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
684
750
|
if (truncatedRuns) console.warn(`WARNING: Input truncated at ${limitedRuns.length} run items (original: ${sarif.runs.length})`);
|
|
751
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
685
752
|
return buildHdfResults({
|
|
686
753
|
generatorName: "sarif-to-hdf",
|
|
687
754
|
converterVersion: "1.0.0",
|
|
688
755
|
toolName: firstDriver?.name,
|
|
689
756
|
toolVersion: firstDriver?.version,
|
|
690
757
|
toolFormat: "SARIF",
|
|
691
|
-
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum)),
|
|
758
|
+
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
|
|
692
759
|
components: [],
|
|
693
|
-
timestamp
|
|
760
|
+
timestamp
|
|
694
761
|
});
|
|
695
762
|
}
|
|
696
|
-
function convertRun(run, version, resultsChecksum) {
|
|
763
|
+
function convertRun(run, version, resultsChecksum, timestamp) {
|
|
697
764
|
const ruleMap = buildRuleMap(run);
|
|
698
765
|
const { items: limitedResults, truncated: truncatedResults } = limitArray(run.results);
|
|
699
766
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -717,7 +784,14 @@ function convertRun(run, version, resultsChecksum) {
|
|
|
717
784
|
const group = groupMap.get(ruleId);
|
|
718
785
|
return convertResultGroup(ruleId, group.rule, group.results);
|
|
719
786
|
});
|
|
720
|
-
|
|
787
|
+
const baselineName = run.tool?.driver?.name || "SARIF";
|
|
788
|
+
if (requirements.length === 0) {
|
|
789
|
+
const driverName = run.tool?.driver?.name?.trim() || "";
|
|
790
|
+
const target = driverName || "SARIF analyzer";
|
|
791
|
+
const idPrefix = driverName || "sarif";
|
|
792
|
+
requirements.push(buildNoFindingsRequirement(`${idPrefix}-no-findings`, `${target} ran and reported zero findings.`, timestamp));
|
|
793
|
+
}
|
|
794
|
+
return createMinimalBaseline(baselineName, requirements, {
|
|
721
795
|
version,
|
|
722
796
|
title: "Static Analysis Results Interchange Format",
|
|
723
797
|
resultsChecksum
|
|
@@ -747,8 +821,42 @@ function convertResultGroup(ruleId, rule, sarifResults) {
|
|
|
747
821
|
const controlType = deriveControlTypeFromTags(nistControls);
|
|
748
822
|
if (controlType !== void 0) req.controlType = controlType;
|
|
749
823
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
824
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
825
|
+
const packages = [];
|
|
826
|
+
for (const sr of sarifResults) {
|
|
827
|
+
const pkg = packageFromSarifProperties(sr.properties);
|
|
828
|
+
if (!pkg) continue;
|
|
829
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
830
|
+
if (seenKeys.has(key)) continue;
|
|
831
|
+
seenKeys.add(key);
|
|
832
|
+
packages.push(pkg);
|
|
833
|
+
}
|
|
834
|
+
if (packages.length > 0) req.affectedPackages = packages;
|
|
750
835
|
return req;
|
|
751
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Extract an Affected_Package from a SARIF result.properties bag.
|
|
839
|
+
* Recognizes the SCA-tool convention of carrying purl / cpe /
|
|
840
|
+
* packageName+packageVersion / name+version. Returns undefined for
|
|
841
|
+
* SAST results that lack any package identity.
|
|
842
|
+
*/
|
|
843
|
+
function packageFromSarifProperties(props) {
|
|
844
|
+
if (!props) return void 0;
|
|
845
|
+
const name = props.packageName ?? props.name;
|
|
846
|
+
const version = props.packageVersion ?? props.version;
|
|
847
|
+
let ecosystem;
|
|
848
|
+
if (props.purl) ecosystem = ecosystemFromPurlType(parsePurl(props.purl)?.type);
|
|
849
|
+
else if (props.ecosystem) ecosystem = ecosystemFromPurlType(props.ecosystem);
|
|
850
|
+
else if (name && version) ecosystem = Ecosystem.Generic;
|
|
851
|
+
return buildAffectedPackage$1({
|
|
852
|
+
name,
|
|
853
|
+
version,
|
|
854
|
+
ecosystem,
|
|
855
|
+
purl: props.purl,
|
|
856
|
+
cpe: props.cpe,
|
|
857
|
+
fixedInVersion: props.fixedInVersion
|
|
858
|
+
});
|
|
859
|
+
}
|
|
752
860
|
function resolveRuleLevel(rule, results) {
|
|
753
861
|
if (rule?.defaultConfiguration?.level) return rule.defaultConfiguration.level;
|
|
754
862
|
for (const r of results) if (!r.kind || r.kind === "fail") {
|
|
@@ -961,14 +1069,16 @@ async function convertJunitToHdf(input) {
|
|
|
961
1069
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
962
1070
|
validateInputSize(input, "junit");
|
|
963
1071
|
const { suites, name } = parseJUnitXML(input);
|
|
1072
|
+
const requirements = buildRequirements(suites);
|
|
1073
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
964
1074
|
return buildHdfResults({
|
|
965
|
-
generatorName: "hdf
|
|
1075
|
+
generatorName: "junit-to-hdf",
|
|
966
1076
|
converterVersion: CONVERTER_VERSION$2,
|
|
967
1077
|
toolName: "JUnit XML",
|
|
968
1078
|
toolFormat: "XML",
|
|
969
|
-
baselines: [createMinimalBaseline(name,
|
|
1079
|
+
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
|
|
970
1080
|
components: [{
|
|
971
|
-
type:
|
|
1081
|
+
type: TargetType.Application,
|
|
972
1082
|
name
|
|
973
1083
|
}],
|
|
974
1084
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -1067,6 +1177,11 @@ function buildCodeDesc$9(tc) {
|
|
|
1067
1177
|
if (tc.classname) return `${tc.classname} :: ${tc.name}`;
|
|
1068
1178
|
return tc.name;
|
|
1069
1179
|
}
|
|
1180
|
+
function noFindingsTarget(baselineName, suites) {
|
|
1181
|
+
if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
|
|
1182
|
+
for (const s of suites) if (s.name) return s.name;
|
|
1183
|
+
return "JUnit test suite";
|
|
1184
|
+
}
|
|
1070
1185
|
//#endregion
|
|
1071
1186
|
//#region converters/xccdf-results-to-hdf/typescript/converter.ts
|
|
1072
1187
|
const CONVERTER_VERSION$1 = "1.0.0";
|
|
@@ -1131,6 +1246,10 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
1131
1246
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
1132
1247
|
if (truncatedRR) console.warn(`WARNING: Input truncated at ${limitedRuleResults.length} rule-result items (original: ${ruleResults.length})`);
|
|
1133
1248
|
const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
|
|
1249
|
+
if (requirements.length === 0) {
|
|
1250
|
+
const target = xccdfTargetName(testResult, benchmark);
|
|
1251
|
+
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
1252
|
+
}
|
|
1134
1253
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
1135
1254
|
const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
|
|
1136
1255
|
const components = buildTargets(testResult);
|
|
@@ -1144,7 +1263,7 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
1144
1263
|
const hdf = {
|
|
1145
1264
|
baselines: [baseline],
|
|
1146
1265
|
generator: {
|
|
1147
|
-
name: "hdf
|
|
1266
|
+
name: "xccdf-results-to-hdf",
|
|
1148
1267
|
version: CONVERTER_VERSION$1
|
|
1149
1268
|
},
|
|
1150
1269
|
tool: {
|
|
@@ -1183,6 +1302,10 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1183
1302
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
1184
1303
|
if (truncatedARFRR) console.warn(`WARNING: Input truncated at ${limitedARFRuleResults.length} rule-result items (original: ${ruleResults.length})`);
|
|
1185
1304
|
const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
|
|
1305
|
+
if (requirements.length === 0) {
|
|
1306
|
+
const target = xccdfTargetName(testResult, benchmark);
|
|
1307
|
+
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
1308
|
+
}
|
|
1186
1309
|
let baselineName = "";
|
|
1187
1310
|
if (benchmark) baselineName = extractText(benchmark.title) || "";
|
|
1188
1311
|
if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "ARF Report";
|
|
@@ -1203,7 +1326,7 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1203
1326
|
const hdf = {
|
|
1204
1327
|
baselines,
|
|
1205
1328
|
generator: {
|
|
1206
|
-
name: "hdf
|
|
1329
|
+
name: "xccdf-results-to-hdf",
|
|
1207
1330
|
version: CONVERTER_VERSION$1
|
|
1208
1331
|
},
|
|
1209
1332
|
tool: {
|
|
@@ -1303,6 +1426,22 @@ function ruleResultToRequirement(rr, ruleIndex) {
|
|
|
1303
1426
|
return req;
|
|
1304
1427
|
}
|
|
1305
1428
|
/**
|
|
1429
|
+
* Pick the most specific identifier available for a no-findings codeDesc.
|
|
1430
|
+
* Falls back through TestResult target/title, benchmark title/id, then a generic phrase.
|
|
1431
|
+
*/
|
|
1432
|
+
function xccdfTargetName(testResult, benchmark) {
|
|
1433
|
+
const tr = testResult ?? {};
|
|
1434
|
+
const target = (tr.target ?? "").trim();
|
|
1435
|
+
if (target) return target;
|
|
1436
|
+
const trTitle = extractText(tr.title).trim();
|
|
1437
|
+
if (trTitle) return trTitle;
|
|
1438
|
+
const benchTitle = extractText(benchmark?.title).trim();
|
|
1439
|
+
if (benchTitle) return benchTitle;
|
|
1440
|
+
const benchId = (benchmark?.id ?? "").trim();
|
|
1441
|
+
if (benchId) return benchId;
|
|
1442
|
+
return "the target";
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1306
1445
|
* Build Component array from TestResult metadata.
|
|
1307
1446
|
*/
|
|
1308
1447
|
function buildTargets(testResult) {
|
|
@@ -1311,7 +1450,7 @@ function buildTargets(testResult) {
|
|
|
1311
1450
|
const addresses = testResult["target-address"] ?? [];
|
|
1312
1451
|
const target = {
|
|
1313
1452
|
name: targetName,
|
|
1314
|
-
type:
|
|
1453
|
+
type: TargetType.Host,
|
|
1315
1454
|
labels: {}
|
|
1316
1455
|
};
|
|
1317
1456
|
if (addresses.length > 0) target.ipAddress = addresses[0];
|
|
@@ -1766,11 +1905,11 @@ const CONVERTER_VERSION = "1.0.0";
|
|
|
1766
1905
|
* applicability are omitted (the checklist format cannot substantiate them).
|
|
1767
1906
|
* Original-format metadata is stashed in extensions/tags for round-trip.
|
|
1768
1907
|
*/
|
|
1769
|
-
function checklistToHdf(cl, resultsChecksum) {
|
|
1908
|
+
function checklistToHdf(cl, resultsChecksum, generatorName) {
|
|
1770
1909
|
const hdf = {
|
|
1771
1910
|
baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
|
|
1772
1911
|
generator: {
|
|
1773
|
-
name:
|
|
1912
|
+
name: generatorName,
|
|
1774
1913
|
version: CONVERTER_VERSION
|
|
1775
1914
|
},
|
|
1776
1915
|
tool: {
|
|
@@ -1835,7 +1974,7 @@ function assetToComponent(a) {
|
|
|
1835
1974
|
if (!a.hostName && !a.hostIP && !a.hostFQDN) return void 0;
|
|
1836
1975
|
const c = {
|
|
1837
1976
|
name: a.hostName || a.hostFQDN || a.hostIP || "",
|
|
1838
|
-
type:
|
|
1977
|
+
type: TargetType.Host
|
|
1839
1978
|
};
|
|
1840
1979
|
if (a.hostIP) c.ipAddress = a.hostIP;
|
|
1841
1980
|
if (a.hostFQDN) c.fqdn = a.hostFQDN;
|
|
@@ -2010,7 +2149,7 @@ async function convertCklToHdf(input) {
|
|
|
2010
2149
|
validateInputSize(input, "ckl-to-hdf");
|
|
2011
2150
|
const resultsChecksum = await inputChecksum(input);
|
|
2012
2151
|
const checklist = parseCkl(input);
|
|
2013
|
-
return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
|
|
2152
|
+
return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "ckl-to-hdf"), null, 2);
|
|
2014
2153
|
}
|
|
2015
2154
|
//#endregion
|
|
2016
2155
|
//#region converters/cklb-to-hdf/typescript/converter.ts
|
|
@@ -2027,7 +2166,7 @@ async function convertCklbToHdf(input) {
|
|
|
2027
2166
|
validateInputSize(input, "cklb-to-hdf");
|
|
2028
2167
|
const resultsChecksum = await inputChecksum(input);
|
|
2029
2168
|
const checklist = parseCklb(input);
|
|
2030
|
-
return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
|
|
2169
|
+
return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "cklb-to-hdf"), null, 2);
|
|
2031
2170
|
}
|
|
2032
2171
|
//#endregion
|
|
2033
2172
|
//#region converters/hdf-to-ckl/typescript/converter.ts
|
|
@@ -2078,7 +2217,28 @@ function formatDependencyPath(from) {
|
|
|
2078
2217
|
/**
|
|
2079
2218
|
* Builds a single EvaluatedRequirement from a group of vulnerabilities sharing an ID.
|
|
2080
2219
|
*/
|
|
2081
|
-
|
|
2220
|
+
/**
|
|
2221
|
+
* Map Snyk's `packageManager` value to an Affected_Package ecosystem.
|
|
2222
|
+
* Snyk reports values that don't always match PURL types one-to-one
|
|
2223
|
+
* (pip → pypi, rubygems → gem, yarn → npm). Unknown managers fall back
|
|
2224
|
+
* to `generic`.
|
|
2225
|
+
*/
|
|
2226
|
+
function ecosystemFromSnykPackageManager(pm) {
|
|
2227
|
+
if (!pm) return Ecosystem.Generic;
|
|
2228
|
+
const lower = pm.toLowerCase();
|
|
2229
|
+
if (lower === "pip" || lower === "pip3") return Ecosystem.Pypi;
|
|
2230
|
+
if (lower === "rubygems" || lower === "bundler") return Ecosystem.Gem;
|
|
2231
|
+
if (lower === "yarn" || lower === "npm") return Ecosystem.Npm;
|
|
2232
|
+
return ecosystemFromPurlType(lower);
|
|
2233
|
+
}
|
|
2234
|
+
/** Synthesize a `pkg:<type>/<name>@<version>` PURL when the ecosystem
|
|
2235
|
+
* maps cleanly. Returns undefined for `generic` so we don't emit a
|
|
2236
|
+
* fake `pkg:generic/...` PURL that downstream tools can't dereference. */
|
|
2237
|
+
function synthesizePurl(ecosystem, name, version) {
|
|
2238
|
+
if (ecosystem === Ecosystem.Generic) return void 0;
|
|
2239
|
+
return `pkg:${ecosystem}/${name}@${version}`;
|
|
2240
|
+
}
|
|
2241
|
+
function buildRequirement$16(vulnID, vulns, packageManager) {
|
|
2082
2242
|
const rep = vulns[0];
|
|
2083
2243
|
const cweIDs = rep.identifiers.CWE ?? [];
|
|
2084
2244
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
@@ -2098,6 +2258,19 @@ function buildRequirement$16(vulnID, vulns) {
|
|
|
2098
2258
|
const controlType = deriveControlTypeFromTags(nist);
|
|
2099
2259
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2100
2260
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
2261
|
+
const name = rep.packageName ?? rep.moduleName;
|
|
2262
|
+
const version = rep.version;
|
|
2263
|
+
if (name && version) {
|
|
2264
|
+
const ecosystem = ecosystemFromSnykPackageManager(packageManager);
|
|
2265
|
+
const pkg = buildAffectedPackage$1({
|
|
2266
|
+
name,
|
|
2267
|
+
version,
|
|
2268
|
+
ecosystem,
|
|
2269
|
+
purl: synthesizePurl(ecosystem, name, version),
|
|
2270
|
+
fixedInVersion: rep.fixedIn?.[0]
|
|
2271
|
+
});
|
|
2272
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
2273
|
+
}
|
|
2101
2274
|
return req;
|
|
2102
2275
|
}
|
|
2103
2276
|
/**
|
|
@@ -2114,7 +2287,11 @@ function convertSingleProject(report, resultsChecksum) {
|
|
|
2114
2287
|
else groups.set(vuln.id, [vuln]);
|
|
2115
2288
|
}
|
|
2116
2289
|
const requirements = [];
|
|
2117
|
-
for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns));
|
|
2290
|
+
for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, report.packageManager));
|
|
2291
|
+
if (requirements.length === 0) {
|
|
2292
|
+
const target = report.projectName ?? report.path ?? "project";
|
|
2293
|
+
requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
2294
|
+
}
|
|
2118
2295
|
return createMinimalBaseline("Snyk Scan", requirements, {
|
|
2119
2296
|
resultsChecksum,
|
|
2120
2297
|
title: `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`,
|
|
@@ -2159,7 +2336,7 @@ async function convertSnykToHdf(input) {
|
|
|
2159
2336
|
baselines,
|
|
2160
2337
|
components: [{
|
|
2161
2338
|
name: targetName,
|
|
2162
|
-
type:
|
|
2339
|
+
type: TargetType.Application
|
|
2163
2340
|
}],
|
|
2164
2341
|
timestamp: /* @__PURE__ */ new Date()
|
|
2165
2342
|
});
|
|
@@ -2227,6 +2404,92 @@ function buildCodeDesc$8(match) {
|
|
|
2227
2404
|
}
|
|
2228
2405
|
return parts.join(" | ");
|
|
2229
2406
|
}
|
|
2407
|
+
function cvssVersionToSchema(v) {
|
|
2408
|
+
switch (v) {
|
|
2409
|
+
case "2.0": return Version.The20;
|
|
2410
|
+
case "3.0": return Version.The30;
|
|
2411
|
+
case "4.0": return Version.The40;
|
|
2412
|
+
default: return Version.The31;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function cvssBandSeverity(score) {
|
|
2416
|
+
switch (cvssScoreToSeverity(score)) {
|
|
2417
|
+
case "critical": return CVSSSeverity.Critical;
|
|
2418
|
+
case "high": return CVSSSeverity.High;
|
|
2419
|
+
case "medium": return CVSSSeverity.Medium;
|
|
2420
|
+
case "low": return CVSSSeverity.Low;
|
|
2421
|
+
default: return CVSSSeverity.None;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
function buildCvssEntries$1(vuln) {
|
|
2425
|
+
if (!vuln.cvss || vuln.cvss.length === 0) return;
|
|
2426
|
+
const entries = [];
|
|
2427
|
+
for (const c of vuln.cvss) {
|
|
2428
|
+
const entry = { version: cvssVersionToSchema(c.version) };
|
|
2429
|
+
if (c.vector) entry.baseVector = c.vector;
|
|
2430
|
+
const score = c.metrics?.baseScore;
|
|
2431
|
+
if (typeof score === "number" && Number.isFinite(score)) {
|
|
2432
|
+
entry.baseScore = score;
|
|
2433
|
+
entry.baseSeverity = cvssBandSeverity(score);
|
|
2434
|
+
}
|
|
2435
|
+
if (vuln.id) entry.source = vuln.id;
|
|
2436
|
+
if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
|
|
2437
|
+
entries.push(entry);
|
|
2438
|
+
}
|
|
2439
|
+
return entries.length > 0 ? entries : void 0;
|
|
2440
|
+
}
|
|
2441
|
+
function mapGrypeTypeToEcosystem(grypeType) {
|
|
2442
|
+
switch ((grypeType ?? "").toLowerCase()) {
|
|
2443
|
+
case "rpm": return Ecosystem.RPM;
|
|
2444
|
+
case "deb": return Ecosystem.Deb;
|
|
2445
|
+
case "npm": return Ecosystem.Npm;
|
|
2446
|
+
case "python": return Ecosystem.Pypi;
|
|
2447
|
+
case "gem": return Ecosystem.Gem;
|
|
2448
|
+
case "go-module": return Ecosystem.Go;
|
|
2449
|
+
case "java-archive":
|
|
2450
|
+
case "jenkins-plugin": return Ecosystem.Maven;
|
|
2451
|
+
case "dotnet": return Ecosystem.Nuget;
|
|
2452
|
+
case "rust-crate": return Ecosystem.Cargo;
|
|
2453
|
+
default: return Ecosystem.Generic;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
function buildAffectedPackages(match) {
|
|
2457
|
+
const artifact = match.artifact;
|
|
2458
|
+
const pkg = {
|
|
2459
|
+
name: artifact.name,
|
|
2460
|
+
version: artifact.version,
|
|
2461
|
+
ecosystem: mapGrypeTypeToEcosystem(artifact.type)
|
|
2462
|
+
};
|
|
2463
|
+
if (artifact.cpes && artifact.cpes.length > 0 && artifact.cpes[0]) pkg.cpe = artifact.cpes[0];
|
|
2464
|
+
if (artifact.purl) pkg.purl = artifact.purl;
|
|
2465
|
+
const fix = match.vulnerability.fix;
|
|
2466
|
+
if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
|
|
2467
|
+
return [pkg];
|
|
2468
|
+
}
|
|
2469
|
+
const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
|
|
2470
|
+
function extractCwe(raw) {
|
|
2471
|
+
if (!raw || raw.length === 0) return void 0;
|
|
2472
|
+
const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
|
|
2473
|
+
return out.length === 0 ? void 0 : out;
|
|
2474
|
+
}
|
|
2475
|
+
function buildEpss$1(entries) {
|
|
2476
|
+
if (!entries || entries.length === 0) return void 0;
|
|
2477
|
+
const e = entries[0];
|
|
2478
|
+
if (!e.date) return void 0;
|
|
2479
|
+
return {
|
|
2480
|
+
score: e.epss ?? 0,
|
|
2481
|
+
percentile: e.percentile ?? 0,
|
|
2482
|
+
date: e.date
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
function buildKev(k) {
|
|
2486
|
+
if (!k) return void 0;
|
|
2487
|
+
const out = { inKev: Boolean(k.inKev) };
|
|
2488
|
+
if (k.dateAdded) out.dateAdded = k.dateAdded;
|
|
2489
|
+
if (k.dueDate) out.dueDate = k.dueDate;
|
|
2490
|
+
if (k.notes) out.notes = k.notes;
|
|
2491
|
+
return out;
|
|
2492
|
+
}
|
|
2230
2493
|
function convertMatchToRequirement(match, isIgnored) {
|
|
2231
2494
|
const vuln = match.vulnerability;
|
|
2232
2495
|
const cveId = vuln.id;
|
|
@@ -2277,6 +2540,15 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
2277
2540
|
};
|
|
2278
2541
|
const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
2279
2542
|
if (controlType !== void 0) requirement.controlType = controlType;
|
|
2543
|
+
const cvss = buildCvssEntries$1(vuln);
|
|
2544
|
+
if (cvss) requirement.cvss = cvss;
|
|
2545
|
+
requirement.affectedPackages = buildAffectedPackages(match);
|
|
2546
|
+
const cwe = extractCwe(vuln.cwe);
|
|
2547
|
+
if (cwe) requirement.cwe = cwe;
|
|
2548
|
+
const epss = buildEpss$1(vuln.epss);
|
|
2549
|
+
if (epss) requirement.epss = epss;
|
|
2550
|
+
const kev = buildKev(vuln.kev);
|
|
2551
|
+
if (kev) requirement.kev = kev;
|
|
2280
2552
|
return requirement;
|
|
2281
2553
|
}
|
|
2282
2554
|
async function convertGrypeToHdf(input) {
|
|
@@ -2297,6 +2569,7 @@ async function convertGrypeToHdf(input) {
|
|
|
2297
2569
|
for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
|
|
2298
2570
|
}
|
|
2299
2571
|
const targetName = grypeData.source?.target?.userInput || "Grype Scan";
|
|
2572
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
2300
2573
|
const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
|
|
2301
2574
|
return buildHdfResults({
|
|
2302
2575
|
generatorName: grypeData.descriptor?.name || "grype",
|
|
@@ -2305,7 +2578,7 @@ async function convertGrypeToHdf(input) {
|
|
|
2305
2578
|
toolVersion: grypeData.descriptor?.version,
|
|
2306
2579
|
baselines: [baseline],
|
|
2307
2580
|
components: [{
|
|
2308
|
-
type:
|
|
2581
|
+
type: TargetType.Artifact,
|
|
2309
2582
|
name: targetName
|
|
2310
2583
|
}],
|
|
2311
2584
|
timestamp: grypeData.descriptor?.timestamp ? new Date(grypeData.descriptor.timestamp) : /* @__PURE__ */ new Date()
|
|
@@ -2313,6 +2586,8 @@ async function convertGrypeToHdf(input) {
|
|
|
2313
2586
|
}
|
|
2314
2587
|
//#endregion
|
|
2315
2588
|
//#region converters/nessus-to-hdf/typescript/converter.ts
|
|
2589
|
+
const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
|
|
2590
|
+
const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
2316
2591
|
const converterVersion = "1.0.0";
|
|
2317
2592
|
const IMPACT_MAPPING$7 = {
|
|
2318
2593
|
"4": .9,
|
|
@@ -2340,7 +2615,9 @@ async function convertNessusToHdf(nessusXml) {
|
|
|
2340
2615
|
"preference",
|
|
2341
2616
|
"tag",
|
|
2342
2617
|
"ReportItem",
|
|
2343
|
-
"ReportHost"
|
|
2618
|
+
"ReportHost",
|
|
2619
|
+
"cwe",
|
|
2620
|
+
"cve"
|
|
2344
2621
|
]);
|
|
2345
2622
|
const policyName = parsed.NessusClientData_v2.Policy.policyName;
|
|
2346
2623
|
const version = extractVersion(parsed);
|
|
@@ -2362,7 +2639,7 @@ async function convertNessusToHdf(nessusXml) {
|
|
|
2362
2639
|
components,
|
|
2363
2640
|
statistics: { duration },
|
|
2364
2641
|
generator: {
|
|
2365
|
-
name: "hdf
|
|
2642
|
+
name: "nessus-to-hdf",
|
|
2366
2643
|
version: converterVersion
|
|
2367
2644
|
},
|
|
2368
2645
|
tool: { name: "Nessus" },
|
|
@@ -2409,6 +2686,12 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
|
|
|
2409
2686
|
if (truncatedItems) console.warn(`WARNING: Input truncated at ${limitedItems.length} ReportItem items (original: ${items.length})`);
|
|
2410
2687
|
requirements = limitedItems.map((item) => convertReportItemToRequirement(item, host));
|
|
2411
2688
|
} else requirements = [];
|
|
2689
|
+
if (requirements.length === 0) {
|
|
2690
|
+
const target = host.name || getHostPropertyValue(host, "host-ip") || "host";
|
|
2691
|
+
const startTimeStr = getHostPropertyValue(host, "HOST_START");
|
|
2692
|
+
const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
|
|
2693
|
+
requirements = [buildNoFindingsRequirement("nessus-no-findings", `Nessus scanned ${target} and reported zero findings.`, startTime)];
|
|
2694
|
+
}
|
|
2412
2695
|
return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
|
|
2413
2696
|
title: `Nessus ${policyName}`,
|
|
2414
2697
|
version,
|
|
@@ -2442,8 +2725,141 @@ function convertReportItemToRequirement(item, host) {
|
|
|
2442
2725
|
};
|
|
2443
2726
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2444
2727
|
if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
|
|
2728
|
+
if (!isCompliance) {
|
|
2729
|
+
const cvssEntries = buildCvssEntries(item);
|
|
2730
|
+
if (cvssEntries.length > 0) req.cvss = cvssEntries;
|
|
2731
|
+
const cweIDs = buildCweIDs(item);
|
|
2732
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
2733
|
+
const epss = buildEpss(item, host);
|
|
2734
|
+
if (epss !== void 0) req.epss = epss;
|
|
2735
|
+
}
|
|
2445
2736
|
return req;
|
|
2446
2737
|
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Build a structured Cvss entry for a CVE finding. Returns an array because
|
|
2740
|
+
* the schema models cvss as a multi-entry array (Nessus emits one entry per
|
|
2741
|
+
* item; multi-vendor convergence may yield more).
|
|
2742
|
+
*/
|
|
2743
|
+
function buildCvssEntries(item) {
|
|
2744
|
+
const source = (item.cvss_score_source || "").trim();
|
|
2745
|
+
if (!source || !CVE_SOURCE_RE.test(source)) return [];
|
|
2746
|
+
const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
|
|
2747
|
+
const hasV2 = !!(item.cvss_vector || item.cvss_base_score);
|
|
2748
|
+
if (!hasV3 && !hasV2) return [];
|
|
2749
|
+
let version;
|
|
2750
|
+
let baseVector;
|
|
2751
|
+
let baseScore;
|
|
2752
|
+
let threatVector;
|
|
2753
|
+
let threatScore;
|
|
2754
|
+
if (hasV3) {
|
|
2755
|
+
version = detectV3Version(item.cvss3_vector ?? "");
|
|
2756
|
+
baseVector = item.cvss3_vector || void 0;
|
|
2757
|
+
baseScore = parseFloatOrUndef(item.cvss3_base_score);
|
|
2758
|
+
threatVector = stripVersionPrefix(item.cvss3_temporal_vector);
|
|
2759
|
+
threatScore = item.cvss3_temporal_score ? parseFloatSafe(item.cvss3_temporal_score) : void 0;
|
|
2760
|
+
} else {
|
|
2761
|
+
version = Version.The20;
|
|
2762
|
+
baseVector = stripV2Prefix(item.cvss_vector ?? "") || void 0;
|
|
2763
|
+
baseScore = parseFloatOrUndef(item.cvss_base_score);
|
|
2764
|
+
threatVector = stripV2Prefix(item.cvss_temporal_vector ?? "") || void 0;
|
|
2765
|
+
threatScore = item.cvss_temporal_score ? parseFloatSafe(item.cvss_temporal_score) : void 0;
|
|
2766
|
+
}
|
|
2767
|
+
const entry = {
|
|
2768
|
+
version,
|
|
2769
|
+
source
|
|
2770
|
+
};
|
|
2771
|
+
if (baseVector) entry.baseVector = baseVector;
|
|
2772
|
+
if (baseScore !== void 0) {
|
|
2773
|
+
entry.baseScore = baseScore;
|
|
2774
|
+
const baseSeverity = mapCvssSeverity(baseScore);
|
|
2775
|
+
if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
|
|
2776
|
+
}
|
|
2777
|
+
if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
|
|
2778
|
+
if (threatScore !== void 0) {
|
|
2779
|
+
entry.threatScore = threatScore;
|
|
2780
|
+
entry.computedScore = threatScore;
|
|
2781
|
+
const computedSeverity = mapCvssSeverity(threatScore);
|
|
2782
|
+
if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
|
|
2783
|
+
}
|
|
2784
|
+
return [entry];
|
|
2785
|
+
}
|
|
2786
|
+
function detectV3Version(vector) {
|
|
2787
|
+
if (vector.startsWith("CVSS:3.1/")) return Version.The31;
|
|
2788
|
+
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
2789
|
+
return Version.The30;
|
|
2790
|
+
}
|
|
2791
|
+
function stripVersionPrefix(vector) {
|
|
2792
|
+
if (!vector) return void 0;
|
|
2793
|
+
for (const prefix of [
|
|
2794
|
+
"CVSS:3.0/",
|
|
2795
|
+
"CVSS:3.1/",
|
|
2796
|
+
"CVSS:4.0/"
|
|
2797
|
+
]) if (vector.startsWith(prefix)) return vector.slice(prefix.length);
|
|
2798
|
+
return vector;
|
|
2799
|
+
}
|
|
2800
|
+
function stripV2Prefix(vector) {
|
|
2801
|
+
return vector.startsWith("CVSS2#") ? vector.slice(6) : vector;
|
|
2802
|
+
}
|
|
2803
|
+
function parseFloatSafe(s) {
|
|
2804
|
+
if (!s) return 0;
|
|
2805
|
+
const f = Number.parseFloat(s);
|
|
2806
|
+
return Number.isFinite(f) ? f : 0;
|
|
2807
|
+
}
|
|
2808
|
+
function parseFloatOrUndef(s) {
|
|
2809
|
+
if (s === void 0 || s === "") return void 0;
|
|
2810
|
+
const f = Number.parseFloat(s);
|
|
2811
|
+
return Number.isFinite(f) ? f : void 0;
|
|
2812
|
+
}
|
|
2813
|
+
function mapCvssSeverity(score) {
|
|
2814
|
+
switch (cvssScoreToSeverity(score)) {
|
|
2815
|
+
case "critical": return CVSSSeverity.Critical;
|
|
2816
|
+
case "high": return CVSSSeverity.High;
|
|
2817
|
+
case "medium": return CVSSSeverity.Medium;
|
|
2818
|
+
case "low": return CVSSSeverity.Low;
|
|
2819
|
+
case "none": return CVSSSeverity.None;
|
|
2820
|
+
default: return;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
|
|
2825
|
+
* numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
|
|
2826
|
+
* forms appear. Output is "CWE-N" form per schema convention.
|
|
2827
|
+
*/
|
|
2828
|
+
function buildCweIDs(item) {
|
|
2829
|
+
if (!item.cwe) return [];
|
|
2830
|
+
const raws = Array.isArray(item.cwe) ? item.cwe : [item.cwe];
|
|
2831
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2832
|
+
for (const raw of raws) {
|
|
2833
|
+
const text = String(raw);
|
|
2834
|
+
for (const m of text.matchAll(CWE_PATTERN)) if (m[1]) seen.add(m[1]);
|
|
2835
|
+
for (const tok of text.split(/[^0-9]+/)) if (tok !== "") seen.add(tok);
|
|
2836
|
+
}
|
|
2837
|
+
if (seen.size === 0) return [];
|
|
2838
|
+
return [...seen].sort().map((id) => `CWE-${id}`);
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Build a structured Epss entry when the ReportItem includes EPSS data.
|
|
2842
|
+
* The date is derived from the host's HOST_START in YYYY-MM-DD form.
|
|
2843
|
+
*/
|
|
2844
|
+
function buildEpss(item, host) {
|
|
2845
|
+
const hasScore = item.epss_score !== void 0 && item.epss_score !== "";
|
|
2846
|
+
const hasPct = item.epss_percentile !== void 0 && item.epss_percentile !== "";
|
|
2847
|
+
if (!hasScore && !hasPct) return void 0;
|
|
2848
|
+
const date = epssDate(host);
|
|
2849
|
+
if (date === void 0) return void 0;
|
|
2850
|
+
return {
|
|
2851
|
+
date,
|
|
2852
|
+
score: hasScore ? parseFloatSafe(item.epss_score) : 0,
|
|
2853
|
+
percentile: hasPct ? parseFloatSafe(item.epss_percentile) : 0
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
function epssDate(host) {
|
|
2857
|
+
const hs = getHostPropertyValue(host, "HOST_START");
|
|
2858
|
+
if (hs) {
|
|
2859
|
+
const d = new Date(hs);
|
|
2860
|
+
if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2447
2863
|
function buildDescriptions(item, isCompliance) {
|
|
2448
2864
|
const descriptions = [];
|
|
2449
2865
|
if (isCompliance && item["compliance-info"]) descriptions.push({
|
|
@@ -2504,7 +2920,7 @@ function buildTags$2(item, isCompliance) {
|
|
|
2504
2920
|
}
|
|
2505
2921
|
function buildRefs(item) {
|
|
2506
2922
|
const refs = [];
|
|
2507
|
-
if (item.see_also) refs.push({ url
|
|
2923
|
+
if (item.see_also) for (const url of item.see_also.split(/\s+/).filter(Boolean)) refs.push({ url });
|
|
2508
2924
|
return refs.length > 0 ? refs : void 0;
|
|
2509
2925
|
}
|
|
2510
2926
|
function buildResult$1(item, host, isCompliance) {
|
|
@@ -2546,7 +2962,7 @@ function convertReportHostToTarget(host) {
|
|
|
2546
2962
|
});
|
|
2547
2963
|
const target = {
|
|
2548
2964
|
name: hostName,
|
|
2549
|
-
type:
|
|
2965
|
+
type: TargetType.Host
|
|
2550
2966
|
};
|
|
2551
2967
|
if (isFQDN(hostName)) target.fqdn = hostName;
|
|
2552
2968
|
const hostIp = hostProps["host-ip"];
|
|
@@ -2614,18 +3030,40 @@ async function convertSonarqubeToHdf(input) {
|
|
|
2614
3030
|
const baseline = convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum);
|
|
2615
3031
|
baselines.push(baseline);
|
|
2616
3032
|
}
|
|
3033
|
+
let components = Array.from(issuesByProject.keys()).map((projectKey) => ({
|
|
3034
|
+
type: TargetType.Application,
|
|
3035
|
+
name: projectKey
|
|
3036
|
+
}));
|
|
3037
|
+
if (baselines.length === 0) {
|
|
3038
|
+
const targetName = deriveEmptyScanTarget(sonarData.components);
|
|
3039
|
+
baselines.push({
|
|
3040
|
+
name: targetName,
|
|
3041
|
+
title: `SonarQube Analysis for ${targetName}`,
|
|
3042
|
+
requirements: [buildNoFindingsRequirement("sonarqube-no-findings", `SonarQube scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())],
|
|
3043
|
+
resultsChecksum
|
|
3044
|
+
});
|
|
3045
|
+
components = [{
|
|
3046
|
+
type: TargetType.Application,
|
|
3047
|
+
name: targetName
|
|
3048
|
+
}];
|
|
3049
|
+
}
|
|
2617
3050
|
return buildHdfResults({
|
|
2618
3051
|
generatorName: "sonarqube-to-hdf",
|
|
2619
3052
|
converterVersion: "1.0.0",
|
|
2620
3053
|
toolName: "SonarQube",
|
|
2621
3054
|
baselines,
|
|
2622
|
-
components
|
|
2623
|
-
type: Copyright.Application,
|
|
2624
|
-
name: projectKey
|
|
2625
|
-
})),
|
|
3055
|
+
components,
|
|
2626
3056
|
timestamp: /* @__PURE__ */ new Date()
|
|
2627
3057
|
});
|
|
2628
3058
|
}
|
|
3059
|
+
function deriveEmptyScanTarget(components) {
|
|
3060
|
+
if (components) {
|
|
3061
|
+
const project = components.find((c) => c.qualifier === "TRK");
|
|
3062
|
+
if (project) return project.key;
|
|
3063
|
+
if (components.length > 0) return components[0].key;
|
|
3064
|
+
}
|
|
3065
|
+
return "the SonarQube project";
|
|
3066
|
+
}
|
|
2629
3067
|
function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum) {
|
|
2630
3068
|
const issuesByRule = /* @__PURE__ */ new Map();
|
|
2631
3069
|
for (const issue of issues) {
|
|
@@ -2788,6 +3226,20 @@ function buildResult(r) {
|
|
|
2788
3226
|
...startTime ? { startTime } : {}
|
|
2789
3227
|
});
|
|
2790
3228
|
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Synthesizes a single HDF result for a Config rule whose live evaluation
|
|
3231
|
+
* returned zero in-scope resources. The HDF schema requires `results` to have
|
|
3232
|
+
* minItems >= 1; this honestly signals to auditors that the rule's check ran
|
|
3233
|
+
* but had no scope in this account/region rather than vacuously claiming
|
|
3234
|
+
* "passed". See issue #80 bug 2.
|
|
3235
|
+
*/
|
|
3236
|
+
function buildNotApplicableResult(rule) {
|
|
3237
|
+
const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
|
|
3238
|
+
return createResult(ResultStatus.NotApplicable, codeDesc, {
|
|
3239
|
+
codeDesc,
|
|
3240
|
+
startTime: /* @__PURE__ */ new Date()
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
2791
3243
|
function buildRequirement$15(rule) {
|
|
2792
3244
|
const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
|
|
2793
3245
|
const tags = nist.length > 0 ? { nist } : {};
|
|
@@ -2799,7 +3251,7 @@ function buildRequirement$15(rule) {
|
|
|
2799
3251
|
data: buildCheckText(rule)
|
|
2800
3252
|
}];
|
|
2801
3253
|
const title = `${getAccountId(rule.ConfigRuleArn)} - ${rule.ConfigRuleName}`;
|
|
2802
|
-
const results = rule.EvaluationResults.map(buildResult);
|
|
3254
|
+
const results = rule.EvaluationResults.length > 0 ? rule.EvaluationResults.map(buildResult) : [buildNotApplicableResult(rule)];
|
|
2803
3255
|
const req = createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
|
|
2804
3256
|
tags,
|
|
2805
3257
|
sourceLocation: {
|
|
@@ -2843,7 +3295,7 @@ async function convertAwsConfigToHdf(input) {
|
|
|
2843
3295
|
toolName: "AWS Config",
|
|
2844
3296
|
baselines: [baseline],
|
|
2845
3297
|
components: [{
|
|
2846
|
-
type:
|
|
3298
|
+
type: TargetType.CloudAccount,
|
|
2847
3299
|
name: `AWS Account ${accountId}`,
|
|
2848
3300
|
labels: {
|
|
2849
3301
|
account: accountId,
|
|
@@ -2952,8 +3404,12 @@ async function convertCheckovToHdf(input) {
|
|
|
2952
3404
|
}
|
|
2953
3405
|
const requirements = [];
|
|
2954
3406
|
for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
|
|
2955
|
-
const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
|
|
2956
3407
|
const format = checkTypes.join(", ");
|
|
3408
|
+
if (requirements.length === 0) {
|
|
3409
|
+
const target = format || "input";
|
|
3410
|
+
requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3411
|
+
}
|
|
3412
|
+
const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
|
|
2957
3413
|
return buildHdfResults({
|
|
2958
3414
|
generatorName: "checkov-to-hdf",
|
|
2959
3415
|
converterVersion: "1.0.0",
|
|
@@ -3057,6 +3513,7 @@ async function convertGosecToHdf(input) {
|
|
|
3057
3513
|
}
|
|
3058
3514
|
const requirements = [];
|
|
3059
3515
|
for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues));
|
|
3516
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", /* @__PURE__ */ new Date()));
|
|
3060
3517
|
const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
|
|
3061
3518
|
return buildHdfResults({
|
|
3062
3519
|
generatorName: "gosec-to-hdf",
|
|
@@ -3137,6 +3594,7 @@ async function convertNiktoToHdf(input) {
|
|
|
3137
3594
|
if (niktoData.host) targetParts.push(`Host: ${niktoData.host}`);
|
|
3138
3595
|
if (niktoData.port) targetParts.push(`Port: ${niktoData.port}`);
|
|
3139
3596
|
const targetName = targetParts.length > 0 ? targetParts.join(" ") : "Nikto Scan";
|
|
3597
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3140
3598
|
return buildHdfResults({
|
|
3141
3599
|
generatorName: "nikto-to-hdf",
|
|
3142
3600
|
converterVersion: "unknown",
|
|
@@ -3148,7 +3606,7 @@ async function convertNiktoToHdf(input) {
|
|
|
3148
3606
|
summary: niktoData.banner || ""
|
|
3149
3607
|
})],
|
|
3150
3608
|
components: [{
|
|
3151
|
-
type:
|
|
3609
|
+
type: TargetType.Application,
|
|
3152
3610
|
name: targetName
|
|
3153
3611
|
}]
|
|
3154
3612
|
});
|
|
@@ -3210,45 +3668,8 @@ async function convertZapToHdf(input) {
|
|
|
3210
3668
|
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3211
3669
|
const resultsChecksum = await inputChecksum(input);
|
|
3212
3670
|
const zapData = parseJSON(input);
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
|
|
3216
|
-
resultsChecksum,
|
|
3217
|
-
title: "OWASP ZAP Scan",
|
|
3218
|
-
summary: ""
|
|
3219
|
-
})],
|
|
3220
|
-
generator: {
|
|
3221
|
-
name: "zap-to-hdf",
|
|
3222
|
-
version: "unknown"
|
|
3223
|
-
},
|
|
3224
|
-
tool: {
|
|
3225
|
-
name: "OWASP ZAP",
|
|
3226
|
-
format: "JSON"
|
|
3227
|
-
}
|
|
3228
|
-
};
|
|
3229
|
-
return JSON.stringify(hdf, null, 2);
|
|
3230
|
-
}
|
|
3231
|
-
const site = selectSite(zapData.site);
|
|
3232
|
-
if (!site) {
|
|
3233
|
-
const hdf = {
|
|
3234
|
-
baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
|
|
3235
|
-
resultsChecksum,
|
|
3236
|
-
title: "OWASP ZAP Scan",
|
|
3237
|
-
summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
|
|
3238
|
-
})],
|
|
3239
|
-
generator: {
|
|
3240
|
-
name: "zap-to-hdf",
|
|
3241
|
-
version: "unknown"
|
|
3242
|
-
},
|
|
3243
|
-
tool: {
|
|
3244
|
-
name: "OWASP ZAP",
|
|
3245
|
-
format: "JSON"
|
|
3246
|
-
}
|
|
3247
|
-
};
|
|
3248
|
-
if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
|
|
3249
|
-
return JSON.stringify(hdf, null, 2);
|
|
3250
|
-
}
|
|
3251
|
-
const alerts = site.alerts ?? [];
|
|
3671
|
+
const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
|
|
3672
|
+
const alerts = site?.alerts ?? [];
|
|
3252
3673
|
const pluginIdCount = /* @__PURE__ */ new Map();
|
|
3253
3674
|
const { items: limitedAlerts, truncated } = limitArray(alerts);
|
|
3254
3675
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -3304,10 +3725,17 @@ async function convertZapToHdf(input) {
|
|
|
3304
3725
|
if (controlType !== void 0) req.controlType = controlType;
|
|
3305
3726
|
requirements.push(req);
|
|
3306
3727
|
}
|
|
3307
|
-
const targetName = site["@host"] ?? "Unknown Host";
|
|
3728
|
+
const targetName = site?.["@host"] ?? "Unknown Host";
|
|
3729
|
+
const siteName = site?.["@name"] ?? "";
|
|
3730
|
+
const baselineName = site && (site["@name"] || site["@host"]) ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
|
|
3731
|
+
if (requirements.length === 0) {
|
|
3732
|
+
let target = siteName || targetName;
|
|
3733
|
+
if (!target || target === "Unknown Host") target = "the target site";
|
|
3734
|
+
requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3735
|
+
}
|
|
3308
3736
|
const baseline = createMinimalBaseline("OWASP ZAP Scan", requirements, {
|
|
3309
3737
|
resultsChecksum,
|
|
3310
|
-
title:
|
|
3738
|
+
title: baselineName,
|
|
3311
3739
|
summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
|
|
3312
3740
|
});
|
|
3313
3741
|
const tool = {
|
|
@@ -3316,14 +3744,14 @@ async function convertZapToHdf(input) {
|
|
|
3316
3744
|
};
|
|
3317
3745
|
if (zapData["@version"]) tool.version = zapData["@version"];
|
|
3318
3746
|
const components = [];
|
|
3319
|
-
if (site["@name"]) components.push({
|
|
3747
|
+
if (site?.["@name"]) components.push({
|
|
3320
3748
|
name: targetName,
|
|
3321
|
-
type:
|
|
3749
|
+
type: TargetType.Application,
|
|
3322
3750
|
url: site["@name"]
|
|
3323
3751
|
});
|
|
3324
3752
|
else if (targetName !== "Unknown Host") components.push({
|
|
3325
3753
|
name: targetName,
|
|
3326
|
-
type:
|
|
3754
|
+
type: TargetType.Application
|
|
3327
3755
|
});
|
|
3328
3756
|
const hdf = {
|
|
3329
3757
|
baselines: [baseline],
|
|
@@ -3459,7 +3887,7 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3459
3887
|
baselines: [baseline],
|
|
3460
3888
|
components: [{
|
|
3461
3889
|
name: targetName,
|
|
3462
|
-
type:
|
|
3890
|
+
type: TargetType.Application
|
|
3463
3891
|
}],
|
|
3464
3892
|
timestamp: /* @__PURE__ */ new Date()
|
|
3465
3893
|
});
|
|
@@ -3659,7 +4087,7 @@ async function convertSplunkToHdf(input) {
|
|
|
3659
4087
|
baselines: allBaselines,
|
|
3660
4088
|
components: [{
|
|
3661
4089
|
name: targetName,
|
|
3662
|
-
type:
|
|
4090
|
+
type: TargetType.Host,
|
|
3663
4091
|
osName: targetRelease || void 0,
|
|
3664
4092
|
labels: {}
|
|
3665
4093
|
}],
|
|
@@ -3759,9 +4187,9 @@ function gitlabSeverityToImpact(severity) {
|
|
|
3759
4187
|
}
|
|
3760
4188
|
function scanTypeToTargetType(scanType) {
|
|
3761
4189
|
switch (scanType) {
|
|
3762
|
-
case "dast": return
|
|
3763
|
-
case "container_scanning": return
|
|
3764
|
-
default: return
|
|
4190
|
+
case "dast": return TargetType.Application;
|
|
4191
|
+
case "container_scanning": return TargetType.ContainerImage;
|
|
4192
|
+
default: return TargetType.Repository;
|
|
3765
4193
|
}
|
|
3766
4194
|
}
|
|
3767
4195
|
function scanTypeLabel(scanType) {
|
|
@@ -3894,9 +4322,14 @@ async function convertGitlabToHdf(input) {
|
|
|
3894
4322
|
if (controlType !== void 0) req.controlType = controlType;
|
|
3895
4323
|
requirements.push(req);
|
|
3896
4324
|
}
|
|
4325
|
+
const label = scanTypeLabel(scanType);
|
|
4326
|
+
if (requirements.length === 0) {
|
|
4327
|
+
const ts = startTime ? new Date(startTime) : /* @__PURE__ */ new Date();
|
|
4328
|
+
requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
|
|
4329
|
+
}
|
|
3897
4330
|
const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
|
|
3898
4331
|
resultsChecksum,
|
|
3899
|
-
title: `GitLab ${
|
|
4332
|
+
title: `GitLab ${label} Security Scan`,
|
|
3900
4333
|
summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
|
|
3901
4334
|
});
|
|
3902
4335
|
const tool = {
|
|
@@ -4050,19 +4483,22 @@ async function convertTrufflehogToHdf(input) {
|
|
|
4050
4483
|
if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
|
|
4051
4484
|
validateInputSize(input, "trufflehog");
|
|
4052
4485
|
const findings = parseFindings(input);
|
|
4053
|
-
if (findings.length === 0) throw new Error("trufflehog: no findings in input");
|
|
4054
4486
|
const resultsChecksum = await inputChecksum(input);
|
|
4055
4487
|
const limitedFindings = limitArrayWithWarning(findings, "finding");
|
|
4056
4488
|
const groups = groupFindings(limitedFindings);
|
|
4057
4489
|
const requirements = [];
|
|
4058
4490
|
for (const [reqID, group] of groups) requirements.push(buildRequirement$12(reqID, group));
|
|
4491
|
+
if (requirements.length === 0) {
|
|
4492
|
+
const target = findGitRepoURL(limitedFindings) ?? limitedFindings[0]?.SourceName ?? "the target source";
|
|
4493
|
+
requirements.push(buildNoFindingsRequirement("trufflehog-no-findings", `TruffleHog scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4494
|
+
}
|
|
4059
4495
|
const hdf = {
|
|
4060
4496
|
baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
|
|
4061
4497
|
resultsChecksum,
|
|
4062
4498
|
title: `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`
|
|
4063
4499
|
})],
|
|
4064
4500
|
generator: {
|
|
4065
|
-
name: "hdf
|
|
4501
|
+
name: "trufflehog-to-hdf",
|
|
4066
4502
|
version: "1.0.0"
|
|
4067
4503
|
},
|
|
4068
4504
|
tool: {
|
|
@@ -4074,7 +4510,7 @@ async function convertTrufflehogToHdf(input) {
|
|
|
4074
4510
|
const repoURL = findGitRepoURL(limitedFindings);
|
|
4075
4511
|
if (repoURL) hdf.components = [{
|
|
4076
4512
|
name: repoURL,
|
|
4077
|
-
type:
|
|
4513
|
+
type: TargetType.Repository
|
|
4078
4514
|
}];
|
|
4079
4515
|
return JSON.stringify(hdf, null, 2);
|
|
4080
4516
|
}
|
|
@@ -4136,6 +4572,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
4136
4572
|
const requirements = [];
|
|
4137
4573
|
for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues));
|
|
4138
4574
|
const targetName = limitedIssues.length > 0 ? (limitedIssues[0].host?.["#text"] ?? "Unknown").trim() : "Unknown";
|
|
4575
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("burpsuite-no-findings", `Burp Suite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4139
4576
|
const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
|
|
4140
4577
|
resultsChecksum,
|
|
4141
4578
|
title: `BurpSuite Scan: ${targetName}`
|
|
@@ -4149,7 +4586,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
4149
4586
|
baselines: [baseline],
|
|
4150
4587
|
components: [{
|
|
4151
4588
|
name: targetName,
|
|
4152
|
-
type:
|
|
4589
|
+
type: TargetType.Application
|
|
4153
4590
|
}],
|
|
4154
4591
|
generator: {
|
|
4155
4592
|
name: "burpsuite-to-hdf",
|
|
@@ -4346,7 +4783,7 @@ async function convertDbprotectToHdf(input) {
|
|
|
4346
4783
|
})],
|
|
4347
4784
|
components: [{
|
|
4348
4785
|
name: targetName,
|
|
4349
|
-
type:
|
|
4786
|
+
type: TargetType.Host
|
|
4350
4787
|
}],
|
|
4351
4788
|
timestamp: /* @__PURE__ */ new Date()
|
|
4352
4789
|
});
|
|
@@ -4386,6 +4823,148 @@ function buildSummary(result) {
|
|
|
4386
4823
|
return `Package Vulnerability Summary: ${result.vulnerabilityDistribution ? String(result.vulnerabilityDistribution.total) : "N/A"} Application Compliance Issue Total: ${result.complianceDistribution ? String(result.complianceDistribution.total) : "N/A"}`;
|
|
4387
4824
|
}
|
|
4388
4825
|
/**
|
|
4826
|
+
* Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
|
|
4827
|
+
* since modern Twistlock exclusively emits 3.x output.
|
|
4828
|
+
*/
|
|
4829
|
+
function cvssVersionFromVector(vector) {
|
|
4830
|
+
if (!vector) return Version.The31;
|
|
4831
|
+
if (vector.startsWith("CVSS:2.0/")) return Version.The20;
|
|
4832
|
+
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
4833
|
+
if (vector.startsWith("CVSS:4.0/")) return Version.The40;
|
|
4834
|
+
return Version.The31;
|
|
4835
|
+
}
|
|
4836
|
+
/**
|
|
4837
|
+
* Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
|
|
4838
|
+
*/
|
|
4839
|
+
function cvssSeverityFromScore(score) {
|
|
4840
|
+
switch (cvssScoreToSeverity(score)) {
|
|
4841
|
+
case "none": return CVSSSeverity.None;
|
|
4842
|
+
case "low": return CVSSSeverity.Low;
|
|
4843
|
+
case "medium": return CVSSSeverity.Medium;
|
|
4844
|
+
case "high": return CVSSSeverity.High;
|
|
4845
|
+
case "critical": return CVSSSeverity.Critical;
|
|
4846
|
+
default: return;
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
/**
|
|
4850
|
+
* Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
|
|
4851
|
+
* when neither a score nor a vector is available. When the vendor emits a
|
|
4852
|
+
* score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
|
|
4853
|
+
* entry is still emitted — the schema makes baseVector optional precisely
|
|
4854
|
+
* so vendor-final-score data isn't dropped.
|
|
4855
|
+
*/
|
|
4856
|
+
function buildCvss(vuln) {
|
|
4857
|
+
const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
|
|
4858
|
+
const hasVector = !!vuln.vector;
|
|
4859
|
+
if (!hasScore && !hasVector) return void 0;
|
|
4860
|
+
const cv = { version: cvssVersionFromVector(vuln.vector) };
|
|
4861
|
+
if (hasScore) {
|
|
4862
|
+
cv.baseScore = vuln.cvss;
|
|
4863
|
+
const sev = cvssSeverityFromScore(vuln.cvss);
|
|
4864
|
+
if (sev !== void 0) cv.baseSeverity = sev;
|
|
4865
|
+
}
|
|
4866
|
+
if (hasVector) cv.baseVector = vuln.vector;
|
|
4867
|
+
const source = vuln.cve ?? vuln.id;
|
|
4868
|
+
if (source) cv.source = source;
|
|
4869
|
+
return cv;
|
|
4870
|
+
}
|
|
4871
|
+
const CWE_REGEX = /cwe[-_]?(\d+)/gi;
|
|
4872
|
+
/**
|
|
4873
|
+
* Extracts canonical CWE-N identifiers from a free-form string.
|
|
4874
|
+
*/
|
|
4875
|
+
function parseCwes(raw) {
|
|
4876
|
+
if (!raw) return [];
|
|
4877
|
+
const out = [];
|
|
4878
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4879
|
+
for (const m of raw.matchAll(CWE_REGEX)) {
|
|
4880
|
+
const id = `CWE-${m[1]}`;
|
|
4881
|
+
if (seen.has(id)) continue;
|
|
4882
|
+
seen.add(id);
|
|
4883
|
+
out.push(id);
|
|
4884
|
+
}
|
|
4885
|
+
return out;
|
|
4886
|
+
}
|
|
4887
|
+
function rhelDistro(distro) {
|
|
4888
|
+
const low = distro.toLowerCase();
|
|
4889
|
+
return [
|
|
4890
|
+
"red hat",
|
|
4891
|
+
"rhel",
|
|
4892
|
+
"centos",
|
|
4893
|
+
"fedora",
|
|
4894
|
+
"amazon linux",
|
|
4895
|
+
"oracle linux",
|
|
4896
|
+
"rocky",
|
|
4897
|
+
"alma"
|
|
4898
|
+
].some((m) => low.includes(m));
|
|
4899
|
+
}
|
|
4900
|
+
function debDistro(distro) {
|
|
4901
|
+
const low = distro.toLowerCase();
|
|
4902
|
+
return ["debian", "ubuntu"].some((m) => low.includes(m));
|
|
4903
|
+
}
|
|
4904
|
+
/**
|
|
4905
|
+
* Maps a Twistlock package type plus the result's distro string to a schema
|
|
4906
|
+
* Ecosystem value. Defaults to 'generic' for unknown types.
|
|
4907
|
+
*/
|
|
4908
|
+
function resolveEcosystem(packageType, distro) {
|
|
4909
|
+
const t = (packageType ?? "").toLowerCase();
|
|
4910
|
+
const d = distro ?? "";
|
|
4911
|
+
switch (t) {
|
|
4912
|
+
case "os":
|
|
4913
|
+
if (rhelDistro(d)) return Ecosystem.RPM;
|
|
4914
|
+
if (debDistro(d)) return Ecosystem.Deb;
|
|
4915
|
+
return Ecosystem.Generic;
|
|
4916
|
+
case "rpm": return Ecosystem.RPM;
|
|
4917
|
+
case "deb": return Ecosystem.Deb;
|
|
4918
|
+
case "jar":
|
|
4919
|
+
case "maven": return Ecosystem.Maven;
|
|
4920
|
+
case "python":
|
|
4921
|
+
case "pypi": return Ecosystem.Pypi;
|
|
4922
|
+
case "nodejs":
|
|
4923
|
+
case "npm": return Ecosystem.Npm;
|
|
4924
|
+
case "gem": return Ecosystem.Gem;
|
|
4925
|
+
case "nuget": return Ecosystem.Nuget;
|
|
4926
|
+
case "go": return Ecosystem.Go;
|
|
4927
|
+
case "cargo": return Ecosystem.Cargo;
|
|
4928
|
+
default: return Ecosystem.Generic;
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
const FIX_VERSION_REGEX = /\d+(?:\.\d+)+[A-Za-z0-9._+\-]*/;
|
|
4932
|
+
/**
|
|
4933
|
+
* Extracts the first version-looking token from fixedBy or a "fixed in X"
|
|
4934
|
+
* status string. Returns empty string when no fix info is present.
|
|
4935
|
+
*/
|
|
4936
|
+
function extractFixedInVersion(vuln) {
|
|
4937
|
+
if (vuln.fixedBy) return vuln.fixedBy;
|
|
4938
|
+
if (!(vuln.status ?? "").toLowerCase().includes("fixed")) return "";
|
|
4939
|
+
const m = (vuln.status ?? "").match(FIX_VERSION_REGEX);
|
|
4940
|
+
return m ? m[0] : "";
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Builds an AffectedPackage entry from per-vulnerability fields. Returns
|
|
4944
|
+
* undefined when packageName or packageVersion are missing (both required).
|
|
4945
|
+
*/
|
|
4946
|
+
function buildAffectedPackage(vuln, packageTypes, distro) {
|
|
4947
|
+
if (!vuln.packageName || !vuln.packageVersion) return void 0;
|
|
4948
|
+
const pkgType = vuln.packageType ?? packageTypes.get(vuln.packageName);
|
|
4949
|
+
const pkg = {
|
|
4950
|
+
name: vuln.packageName,
|
|
4951
|
+
version: vuln.packageVersion,
|
|
4952
|
+
ecosystem: resolveEcosystem(pkgType, distro)
|
|
4953
|
+
};
|
|
4954
|
+
const fixed = extractFixedInVersion(vuln);
|
|
4955
|
+
if (fixed) pkg.fixedInVersion = fixed;
|
|
4956
|
+
return pkg;
|
|
4957
|
+
}
|
|
4958
|
+
/**
|
|
4959
|
+
* Indexes packageName → packageType from the result-level packages array.
|
|
4960
|
+
*/
|
|
4961
|
+
function buildPackageTypeIndex(pkgs) {
|
|
4962
|
+
const idx = /* @__PURE__ */ new Map();
|
|
4963
|
+
if (!pkgs) return idx;
|
|
4964
|
+
for (const p of pkgs) if (p.name && p.type) idx.set(p.name, p.type);
|
|
4965
|
+
return idx;
|
|
4966
|
+
}
|
|
4967
|
+
/**
|
|
4389
4968
|
* Builds the code_desc string for a vulnerability result.
|
|
4390
4969
|
*/
|
|
4391
4970
|
function formatCodeDesc$2(vuln) {
|
|
@@ -4395,10 +4974,17 @@ function formatCodeDesc$2(vuln) {
|
|
|
4395
4974
|
}
|
|
4396
4975
|
/**
|
|
4397
4976
|
* Converts a single vulnerability into an EvaluatedRequirement.
|
|
4977
|
+
*
|
|
4978
|
+
* @param vuln - The Twistlock vulnerability object
|
|
4979
|
+
* @param packageTypes - name→type lookup built from the enclosing result's packages array
|
|
4980
|
+
* @param distro - the enclosing result's distro string, used to disambiguate "os" packages
|
|
4398
4981
|
*/
|
|
4399
|
-
function buildRequirement$9(vuln) {
|
|
4982
|
+
function buildRequirement$9(vuln, packageTypes, distro) {
|
|
4400
4983
|
const nist = DEFAULT_REMEDIATION_NIST_TAGS;
|
|
4401
|
-
const
|
|
4984
|
+
const cciTags = nistToCci(nist);
|
|
4985
|
+
const extras = { cveid: [vuln.id] };
|
|
4986
|
+
if (typeof vuln.cvss === "number" && vuln.cvss > 0) extras["cvss_base_score"] = vuln.cvss;
|
|
4987
|
+
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
4402
4988
|
const descriptions = [{
|
|
4403
4989
|
label: "default",
|
|
4404
4990
|
data: vuln.description
|
|
@@ -4412,6 +4998,12 @@ function buildRequirement$9(vuln) {
|
|
|
4412
4998
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4413
4999
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4414
5000
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5001
|
+
const cv = buildCvss(vuln);
|
|
5002
|
+
if (cv) req.cvss = [cv];
|
|
5003
|
+
const cwes = parseCwes(vuln.cwe);
|
|
5004
|
+
if (cwes.length > 0) req.cwe = cwes;
|
|
5005
|
+
const pkg = buildAffectedPackage(vuln, packageTypes, distro);
|
|
5006
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4415
5007
|
return req;
|
|
4416
5008
|
}
|
|
4417
5009
|
/**
|
|
@@ -4422,7 +5014,13 @@ function convertSingleResult(result, resultsChecksum) {
|
|
|
4422
5014
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
4423
5015
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
4424
5016
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
4425
|
-
|
|
5017
|
+
const packageTypes = buildPackageTypeIndex(result.packages);
|
|
5018
|
+
const requirements = limitedVulns.map((vuln) => buildRequirement$9(vuln, packageTypes, result.distro));
|
|
5019
|
+
if (requirements.length === 0) {
|
|
5020
|
+
const target = result.name ?? result.repository ?? result.id ?? "scan target";
|
|
5021
|
+
requirements.push(buildNoFindingsRequirement("twistlock-no-findings", `Twistlock scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
5022
|
+
}
|
|
5023
|
+
return createMinimalBaseline("Twistlock Scan", requirements, {
|
|
4426
5024
|
resultsChecksum,
|
|
4427
5025
|
title: buildTitle$1(result),
|
|
4428
5026
|
summary: buildSummary(result)
|
|
@@ -4457,7 +5055,7 @@ async function convertTwistlockToHdf(input) {
|
|
|
4457
5055
|
baselines,
|
|
4458
5056
|
components: [{
|
|
4459
5057
|
name: targetName,
|
|
4460
|
-
type:
|
|
5058
|
+
type: TargetType.ContainerImage,
|
|
4461
5059
|
labels: { image: results[0]?.id ?? targetName }
|
|
4462
5060
|
}],
|
|
4463
5061
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -4529,9 +5127,30 @@ function buildRequirement$8(finding, timestamp) {
|
|
|
4529
5127
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4530
5128
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4531
5129
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5130
|
+
const pkg = buildAffectedPackageFromComponent(finding.component);
|
|
5131
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4532
5132
|
return req;
|
|
4533
5133
|
}
|
|
4534
5134
|
/**
|
|
5135
|
+
* Builds an Affected_Package from a Dependency-Track component. Prefers
|
|
5136
|
+
* the rich identifiers Dependency-Track already exposes (purl, cpe) and
|
|
5137
|
+
* augments with name/version/ecosystem when available. Returns undefined
|
|
5138
|
+
* when the component carries no schema-acceptable identifier.
|
|
5139
|
+
*/
|
|
5140
|
+
function buildAffectedPackageFromComponent(c) {
|
|
5141
|
+
let ecosystem;
|
|
5142
|
+
if (c.purl) ecosystem = ecosystemFromPurlType(parsePurl(c.purl)?.type);
|
|
5143
|
+
else if (c.name && c.version) ecosystem = ecosystemFromPurlType(void 0);
|
|
5144
|
+
return buildAffectedPackage$1({
|
|
5145
|
+
name: c.name,
|
|
5146
|
+
version: c.version,
|
|
5147
|
+
ecosystem,
|
|
5148
|
+
purl: c.purl,
|
|
5149
|
+
cpe: c.cpe,
|
|
5150
|
+
fixedInVersion: c.latestVersion
|
|
5151
|
+
});
|
|
5152
|
+
}
|
|
5153
|
+
/**
|
|
4535
5154
|
* Converts Dependency-Track FPF JSON output to HDF format.
|
|
4536
5155
|
*
|
|
4537
5156
|
* @param input - Dependency-Track FPF JSON string
|
|
@@ -4544,7 +5163,12 @@ async function convertDeptrackToHdf(input) {
|
|
|
4544
5163
|
const parsed = parseJSON(input);
|
|
4545
5164
|
if (!parsed || typeof parsed !== "object") throw new Error("deptrack: invalid JSON");
|
|
4546
5165
|
if (!parsed.findings && !parsed.project && !parsed.meta) throw new Error("deptrack: input does not appear to be a Dependency-Track report");
|
|
4547
|
-
const
|
|
5166
|
+
const requirements = (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp));
|
|
5167
|
+
if (requirements.length === 0) {
|
|
5168
|
+
const projectName = parsed.project?.name ?? parsed.project?.uuid ?? "";
|
|
5169
|
+
requirements.push(buildNoFindingsRequirement("deptrack-no-findings", `Dependency-Track analyzed ${projectName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
5170
|
+
}
|
|
5171
|
+
const baseline = createMinimalBaseline("Dependency-Track Scan", requirements, {
|
|
4548
5172
|
resultsChecksum,
|
|
4549
5173
|
title: `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`,
|
|
4550
5174
|
summary: parsed.project?.description
|
|
@@ -4558,7 +5182,7 @@ async function convertDeptrackToHdf(input) {
|
|
|
4558
5182
|
baselines: [baseline],
|
|
4559
5183
|
components: [{
|
|
4560
5184
|
name: targetName,
|
|
4561
|
-
type:
|
|
5185
|
+
type: TargetType.Application
|
|
4562
5186
|
}],
|
|
4563
5187
|
timestamp: /* @__PURE__ */ new Date()
|
|
4564
5188
|
});
|
|
@@ -4636,8 +5260,49 @@ function buildRequirement$7(entryID, entries) {
|
|
|
4636
5260
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4637
5261
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4638
5262
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5263
|
+
const pkg = buildAffectedPackageFromEntry(rep);
|
|
5264
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4639
5265
|
return req;
|
|
4640
5266
|
}
|
|
5267
|
+
function ecosystemFromXraySource(scheme) {
|
|
5268
|
+
if (scheme === "gav") return Ecosystem.Maven;
|
|
5269
|
+
return ecosystemFromPurlType(scheme);
|
|
5270
|
+
}
|
|
5271
|
+
function parseSourceCompID(s) {
|
|
5272
|
+
if (!s) return void 0;
|
|
5273
|
+
const m = /^([a-zA-Z0-9]+):\/\/(.+)$/.exec(s);
|
|
5274
|
+
if (!m) return void 0;
|
|
5275
|
+
const scheme = m[1].toLowerCase();
|
|
5276
|
+
const rest = m[2];
|
|
5277
|
+
const colonIdx = rest.lastIndexOf(":");
|
|
5278
|
+
if (colonIdx > 0) return {
|
|
5279
|
+
scheme,
|
|
5280
|
+
name: rest.slice(0, colonIdx),
|
|
5281
|
+
version: rest.slice(colonIdx + 1)
|
|
5282
|
+
};
|
|
5283
|
+
return {
|
|
5284
|
+
scheme,
|
|
5285
|
+
name: rest
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
function buildAffectedPackageFromEntry(entry) {
|
|
5289
|
+
const parsed = parseSourceCompID(entry.source_comp_id ?? entry.source_id);
|
|
5290
|
+
let name = entry.component ?? "";
|
|
5291
|
+
let version;
|
|
5292
|
+
let ecosystem;
|
|
5293
|
+
if (parsed) {
|
|
5294
|
+
if (parsed.name) name = parsed.name;
|
|
5295
|
+
version = parsed.version;
|
|
5296
|
+
ecosystem = ecosystemFromXraySource(parsed.scheme);
|
|
5297
|
+
}
|
|
5298
|
+
const fixed = entry.component_versions?.fixed_versions?.[0];
|
|
5299
|
+
return buildAffectedPackage$1({
|
|
5300
|
+
name,
|
|
5301
|
+
version,
|
|
5302
|
+
ecosystem: ecosystem ?? (name ? Ecosystem.Generic : void 0),
|
|
5303
|
+
fixedInVersion: fixed
|
|
5304
|
+
});
|
|
5305
|
+
}
|
|
4641
5306
|
/**
|
|
4642
5307
|
* Converts JFrog Xray JSON output to HDF format.
|
|
4643
5308
|
*
|
|
@@ -4667,6 +5332,7 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
4667
5332
|
}
|
|
4668
5333
|
const requirements = [];
|
|
4669
5334
|
for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries));
|
|
5335
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", /* @__PURE__ */ new Date()));
|
|
4670
5336
|
return buildHdfResults({
|
|
4671
5337
|
generatorName: "jfrog-xray-to-hdf",
|
|
4672
5338
|
converterVersion: "1.0.0",
|
|
@@ -4675,7 +5341,7 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
4675
5341
|
baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
|
|
4676
5342
|
components: [{
|
|
4677
5343
|
name: "JFrog Xray Scan",
|
|
4678
|
-
type:
|
|
5344
|
+
type: TargetType.Application
|
|
4679
5345
|
}],
|
|
4680
5346
|
timestamp: /* @__PURE__ */ new Date()
|
|
4681
5347
|
});
|
|
@@ -4738,6 +5404,15 @@ function buildRequirement$6(vuln) {
|
|
|
4738
5404
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4739
5405
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4740
5406
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5407
|
+
const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
|
|
5408
|
+
const pkg = buildAffectedPackage$1({
|
|
5409
|
+
name: vuln.package_name,
|
|
5410
|
+
version: vuln.package_version,
|
|
5411
|
+
ecosystem: vuln.package_name && vuln.package_version ? Ecosystem.Generic : void 0,
|
|
5412
|
+
cpe: cpe23,
|
|
5413
|
+
fixedInVersion: vuln.fixed_version
|
|
5414
|
+
});
|
|
5415
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4741
5416
|
return req;
|
|
4742
5417
|
}
|
|
4743
5418
|
/**
|
|
@@ -4783,6 +5458,9 @@ async function convertNeuvectorToHdf(input) {
|
|
|
4783
5458
|
seen.add(id);
|
|
4784
5459
|
requirements.push(buildRequirement$6(vuln));
|
|
4785
5460
|
}
|
|
5461
|
+
const now = /* @__PURE__ */ new Date();
|
|
5462
|
+
const target = targetNameFromReport(scan.report);
|
|
5463
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, now));
|
|
4786
5464
|
return buildHdfResults({
|
|
4787
5465
|
generatorName: "neuvector-to-hdf",
|
|
4788
5466
|
converterVersion: "1.0.0",
|
|
@@ -4794,13 +5472,13 @@ async function convertNeuvectorToHdf(input) {
|
|
|
4794
5472
|
})],
|
|
4795
5473
|
components: [{
|
|
4796
5474
|
name: targetNameFromReport(scan.report),
|
|
4797
|
-
type:
|
|
5475
|
+
type: TargetType.ContainerImage,
|
|
4798
5476
|
labels: {
|
|
4799
5477
|
image: `${scan.report.registry}/${scan.report.repository}:${scan.report.tag}`,
|
|
4800
5478
|
registry: scan.report.registry
|
|
4801
5479
|
}
|
|
4802
5480
|
}],
|
|
4803
|
-
timestamp:
|
|
5481
|
+
timestamp: now
|
|
4804
5482
|
});
|
|
4805
5483
|
}
|
|
4806
5484
|
//#endregion
|
|
@@ -4915,21 +5593,22 @@ async function convertFortifyToHdf(input) {
|
|
|
4915
5593
|
if (truncatedDescs) console.warn(`WARNING: Input truncated at ${limitedDescs.length} Description items (original: ${descriptions.length})`);
|
|
4916
5594
|
const createdDate = fvdl.CreatedTS?.date ?? "";
|
|
4917
5595
|
const startTimeStr = `${createdDate}T${fvdl.CreatedTS?.time ?? ""}`;
|
|
4918
|
-
const
|
|
5596
|
+
const requirements = limitedDescs.map((desc) => {
|
|
4919
5597
|
return buildRequirement$5(desc, vulnGroups.get(desc.classID ?? "") ?? [], snippetMap, startTimeStr);
|
|
4920
|
-
}), {
|
|
4921
|
-
resultsChecksum,
|
|
4922
|
-
title: "Fortify Static Analyzer Scan",
|
|
4923
|
-
summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
|
|
4924
|
-
version: fvdl.EngineData?.EngineVersion ?? "",
|
|
4925
|
-
status: "loaded"
|
|
4926
5598
|
});
|
|
4927
5599
|
const targetName = fvdl.Build?.SourceBasePath ?? fvdl.Build?.BuildID ?? "Unknown";
|
|
5600
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("fortify-no-findings", `Fortify scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4928
5601
|
const hdfResult = {
|
|
4929
|
-
baselines: [
|
|
5602
|
+
baselines: [createMinimalBaseline("Fortify Scan", requirements, {
|
|
5603
|
+
resultsChecksum,
|
|
5604
|
+
title: "Fortify Static Analyzer Scan",
|
|
5605
|
+
summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
|
|
5606
|
+
version: fvdl.EngineData?.EngineVersion ?? "",
|
|
5607
|
+
status: "loaded"
|
|
5608
|
+
})],
|
|
4930
5609
|
components: [{
|
|
4931
5610
|
name: targetName,
|
|
4932
|
-
type:
|
|
5611
|
+
type: TargetType.Repository
|
|
4933
5612
|
}],
|
|
4934
5613
|
generator: {
|
|
4935
5614
|
name: "fortify-to-hdf",
|
|
@@ -5044,9 +5723,50 @@ function buildRequirement$4(rec) {
|
|
|
5044
5723
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5045
5724
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5046
5725
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5726
|
+
if (rec["CVE ID"]) {
|
|
5727
|
+
const pkg = buildAffectedPackageFromRecord(rec);
|
|
5728
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
5729
|
+
}
|
|
5047
5730
|
return req;
|
|
5048
5731
|
}
|
|
5049
5732
|
/**
|
|
5733
|
+
* Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
|
|
5734
|
+
* `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
|
|
5735
|
+
* mapped — unknown vendors fall back to `generic` rather than guessing.
|
|
5736
|
+
*/
|
|
5737
|
+
function ecosystemFromDistro(distro) {
|
|
5738
|
+
if (!distro) return Ecosystem.Generic;
|
|
5739
|
+
switch (distro.split("-")[0]?.toLowerCase()) {
|
|
5740
|
+
case "redhat":
|
|
5741
|
+
case "rhel":
|
|
5742
|
+
case "centos":
|
|
5743
|
+
case "rocky":
|
|
5744
|
+
case "alma":
|
|
5745
|
+
case "fedora":
|
|
5746
|
+
case "amazon":
|
|
5747
|
+
case "amazonlinux":
|
|
5748
|
+
case "suse":
|
|
5749
|
+
case "sles":
|
|
5750
|
+
case "opensuse": return Ecosystem.RPM;
|
|
5751
|
+
case "debian":
|
|
5752
|
+
case "ubuntu": return Ecosystem.Deb;
|
|
5753
|
+
default: return Ecosystem.Generic;
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
const FIX_VERSION_PATTERN = /fixed in\s+([^\s,;]+)/i;
|
|
5757
|
+
function buildAffectedPackageFromRecord(rec) {
|
|
5758
|
+
const name = rec["Source Package"] || rec.Packages;
|
|
5759
|
+
const version = rec["Package Version"];
|
|
5760
|
+
if (!name || !version) return void 0;
|
|
5761
|
+
const fixMatch = rec["Fix Status"] ? FIX_VERSION_PATTERN.exec(rec["Fix Status"]) : null;
|
|
5762
|
+
return buildAffectedPackage$1({
|
|
5763
|
+
name,
|
|
5764
|
+
version,
|
|
5765
|
+
ecosystem: ecosystemFromDistro(rec.Distro),
|
|
5766
|
+
fixedInVersion: fixMatch ? fixMatch[1] : void 0
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5769
|
+
/**
|
|
5050
5770
|
* Groups records by hostname, preserving insertion order.
|
|
5051
5771
|
*/
|
|
5052
5772
|
function groupByHostname(records) {
|
|
@@ -5077,20 +5797,25 @@ function buildBaseline(hostname, records, resultsChecksum) {
|
|
|
5077
5797
|
async function convertPrismaToHdf(input) {
|
|
5078
5798
|
if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
|
|
5079
5799
|
validateInputSize(input, "prisma");
|
|
5800
|
+
const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
|
|
5801
|
+
for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
|
|
5080
5802
|
const records = parseCsv(input);
|
|
5081
|
-
if (records.length === 0) throw new Error("prisma: no data rows in CSV");
|
|
5082
|
-
const firstRecord = records[0];
|
|
5083
|
-
for (const col of REQUIRED_COLUMNS) if (!(col in firstRecord)) throw new Error(`prisma: missing required CSV column "${col}"`);
|
|
5084
5803
|
const resultsChecksum = await inputChecksum(input);
|
|
5085
|
-
const hostGroups = groupByHostname(records);
|
|
5086
5804
|
const baselines = [];
|
|
5087
5805
|
const components = [];
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5806
|
+
if (records.length === 0) baselines.push(createMinimalBaseline("Prisma Cloud Scan", [buildNoFindingsRequirement("prisma-no-findings", "Prisma Cloud scanned the workload and reported zero vulnerable components.", /* @__PURE__ */ new Date())], {
|
|
5807
|
+
resultsChecksum,
|
|
5808
|
+
title: "Prisma Cloud Scan"
|
|
5809
|
+
}));
|
|
5810
|
+
else {
|
|
5811
|
+
const hostGroups = groupByHostname(records);
|
|
5812
|
+
for (const [hostname, hostRecords] of hostGroups) {
|
|
5813
|
+
baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
|
|
5814
|
+
components.push({
|
|
5815
|
+
name: hostname,
|
|
5816
|
+
type: TargetType.Host
|
|
5817
|
+
});
|
|
5818
|
+
}
|
|
5094
5819
|
}
|
|
5095
5820
|
return buildHdfResults({
|
|
5096
5821
|
generatorName: "prisma-to-hdf",
|
|
@@ -5239,20 +5964,25 @@ async function convertNetsparkerToHdf(input) {
|
|
|
5239
5964
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
5240
5965
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
5241
5966
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
5242
|
-
const baseline = createMinimalBaseline("Netsparker Scan", limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated)), {
|
|
5243
|
-
resultsChecksum,
|
|
5244
|
-
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
5245
|
-
});
|
|
5246
5967
|
const targetName = target.url ?? "Unknown";
|
|
5968
|
+
const requirements = limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated));
|
|
5969
|
+
if (requirements.length === 0) {
|
|
5970
|
+
const initiatedDate = initiated ? new Date(initiated) : /* @__PURE__ */ new Date();
|
|
5971
|
+
const startTime = isNaN(initiatedDate.getTime()) ? /* @__PURE__ */ new Date() : initiatedDate;
|
|
5972
|
+
requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
|
|
5973
|
+
}
|
|
5247
5974
|
return buildHdfResults({
|
|
5248
5975
|
generatorName: "netsparker-to-hdf",
|
|
5249
5976
|
converterVersion: "1.0.0",
|
|
5250
5977
|
toolName,
|
|
5251
5978
|
toolFormat: "XML",
|
|
5252
|
-
baselines: [
|
|
5979
|
+
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
5980
|
+
resultsChecksum,
|
|
5981
|
+
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
5982
|
+
})],
|
|
5253
5983
|
components: [{
|
|
5254
5984
|
name: targetName,
|
|
5255
|
-
type:
|
|
5985
|
+
type: TargetType.Application
|
|
5256
5986
|
}]
|
|
5257
5987
|
});
|
|
5258
5988
|
}
|
|
@@ -5367,12 +6097,14 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
5367
6097
|
const resultsChecksum = await inputChecksum(jsonStr);
|
|
5368
6098
|
const report = parseJSON(jsonStr);
|
|
5369
6099
|
if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
|
|
5370
|
-
const
|
|
6100
|
+
const requirements = limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time));
|
|
6101
|
+
const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
|
|
6102
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("scoutsuite-no-findings", `ScoutSuite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
6103
|
+
const baseline = createMinimalBaseline("ScoutSuite Scan", requirements, {
|
|
5371
6104
|
resultsChecksum,
|
|
5372
6105
|
title: `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`,
|
|
5373
6106
|
summary: report.last_run.ruleset_about
|
|
5374
6107
|
});
|
|
5375
|
-
const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
|
|
5376
6108
|
return buildHdfResults({
|
|
5377
6109
|
generatorName: "scoutsuite-to-hdf",
|
|
5378
6110
|
converterVersion: "1.0.0",
|
|
@@ -5382,7 +6114,7 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
5382
6114
|
baselines: [baseline],
|
|
5383
6115
|
components: [{
|
|
5384
6116
|
name: targetName,
|
|
5385
|
-
type:
|
|
6117
|
+
type: TargetType.CloudAccount,
|
|
5386
6118
|
labels: {
|
|
5387
6119
|
account: report.account_id,
|
|
5388
6120
|
provider: report.provider_code ?? report.provider_name
|
|
@@ -5539,6 +6271,10 @@ async function convertConveyorToHdf(input) {
|
|
|
5539
6271
|
const desc = data.api_response.params["description"];
|
|
5540
6272
|
if (typeof desc === "string" && desc.length > 0) targetName = desc;
|
|
5541
6273
|
}
|
|
6274
|
+
if (baselines.length === 0) baselines.push(createMinimalBaseline("Conveyor Scan", [buildNoFindingsRequirement("conveyor-no-findings", `Conveyor scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())], {
|
|
6275
|
+
resultsChecksum,
|
|
6276
|
+
title: "Conveyor Scan (no findings)"
|
|
6277
|
+
}));
|
|
5542
6278
|
return buildHdfResults({
|
|
5543
6279
|
generatorName: "conveyor-to-hdf",
|
|
5544
6280
|
converterVersion: "1.0.0",
|
|
@@ -5547,7 +6283,7 @@ async function convertConveyorToHdf(input) {
|
|
|
5547
6283
|
baselines,
|
|
5548
6284
|
components: [{
|
|
5549
6285
|
name: targetName,
|
|
5550
|
-
type:
|
|
6286
|
+
type: TargetType.Application
|
|
5551
6287
|
}],
|
|
5552
6288
|
timestamp: /* @__PURE__ */ new Date()
|
|
5553
6289
|
});
|
|
@@ -5814,6 +6550,8 @@ async function convertVeracodeToHdf(input) {
|
|
|
5814
6550
|
const cweRequirements = buildCWERequirements(ensureArray(report.severity), firstBuildDate);
|
|
5815
6551
|
const cveRequirements = buildCVERequirements(report.software_composition_analysis, firstBuildDate);
|
|
5816
6552
|
const allRequirements = [...cweRequirements, ...cveRequirements];
|
|
6553
|
+
const targetName = attr(report, "app_name") || "Veracode Application";
|
|
6554
|
+
if (allRequirements.length === 0) allRequirements.push(buildNoFindingsRequirement("veracode-no-findings", `Veracode scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
5817
6555
|
let title;
|
|
5818
6556
|
const staticAnalysis = report["static-analysis"];
|
|
5819
6557
|
if (staticAnalysis) {
|
|
@@ -5829,7 +6567,6 @@ async function convertVeracodeToHdf(input) {
|
|
|
5829
6567
|
version: attr(report, "policy_version"),
|
|
5830
6568
|
summary: attr(report, "policy_name")
|
|
5831
6569
|
});
|
|
5832
|
-
const targetName = attr(report, "app_name") || "Veracode Application";
|
|
5833
6570
|
const timestamp = parseVeracodeTimestamp(firstBuildDate);
|
|
5834
6571
|
return buildHdfResults({
|
|
5835
6572
|
generatorName: "veracode-to-hdf",
|
|
@@ -5839,7 +6576,7 @@ async function convertVeracodeToHdf(input) {
|
|
|
5839
6576
|
baselines: [baseline],
|
|
5840
6577
|
components: [{
|
|
5841
6578
|
name: targetName,
|
|
5842
|
-
type:
|
|
6579
|
+
type: TargetType.Application
|
|
5843
6580
|
}],
|
|
5844
6581
|
timestamp
|
|
5845
6582
|
});
|
|
@@ -5957,7 +6694,7 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
5957
6694
|
}),
|
|
5958
6695
|
components: [{
|
|
5959
6696
|
name: `Azure Tenant: ${tenantId}`,
|
|
5960
|
-
type:
|
|
6697
|
+
type: TargetType.CloudAccount,
|
|
5961
6698
|
labels: {
|
|
5962
6699
|
account: tenantId,
|
|
5963
6700
|
provider: "azure"
|
|
@@ -5980,6 +6717,7 @@ async function convertMsftDefenderDevopsToHdf(input) {
|
|
|
5980
6717
|
const { components, runEnrichments } = extractEnrichments(raw);
|
|
5981
6718
|
const hdfJson = await convertSarifToHdf(input);
|
|
5982
6719
|
const result = JSON.parse(hdfJson);
|
|
6720
|
+
synthesizeNoFindingsPlaceholders(result);
|
|
5983
6721
|
applyEnrichments(result, components, runEnrichments);
|
|
5984
6722
|
if (result.generator) result.generator.name = "msft-defender-devops-to-hdf";
|
|
5985
6723
|
if (result.tool) result.tool.name = "Microsoft Defender for DevOps";
|
|
@@ -5994,7 +6732,7 @@ function extractEnrichments(raw) {
|
|
|
5994
6732
|
seenRepos.add(vcp.repositoryUri);
|
|
5995
6733
|
const target = {
|
|
5996
6734
|
name: repoNameFromURI(vcp.repositoryUri),
|
|
5997
|
-
type:
|
|
6735
|
+
type: TargetType.Repository,
|
|
5998
6736
|
url: vcp.repositoryUri,
|
|
5999
6737
|
labels: {}
|
|
6000
6738
|
};
|
|
@@ -6044,6 +6782,14 @@ function applyEnrichments(result, components, runEnrichments) {
|
|
|
6044
6782
|
}
|
|
6045
6783
|
}
|
|
6046
6784
|
}
|
|
6785
|
+
function synthesizeNoFindingsPlaceholders(result) {
|
|
6786
|
+
const startTime = result.timestamp ?? /* @__PURE__ */ new Date();
|
|
6787
|
+
for (const baseline of result.baselines ?? []) {
|
|
6788
|
+
if (baseline.requirements && baseline.requirements.length > 0) continue;
|
|
6789
|
+
const tool = baseline.name;
|
|
6790
|
+
baseline.requirements = [buildNoFindingsRequirement(`${tool}-no-findings`, `Microsoft Defender for DevOps scanner "${tool}" ran and reported zero findings.`, startTime)];
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6047
6793
|
function repoNameFromURI(uri) {
|
|
6048
6794
|
const parts = uri.split("/");
|
|
6049
6795
|
return parts[parts.length - 1] || uri;
|
|
@@ -6140,21 +6886,23 @@ async function convertMsftDefenderCloudToHdf(input) {
|
|
|
6140
6886
|
}
|
|
6141
6887
|
const requirements = [];
|
|
6142
6888
|
for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments));
|
|
6889
|
+
const subscriptionID = limitedAssessments.length > 0 ? extractSubscriptionID(limitedAssessments[0].id) : "";
|
|
6890
|
+
if (requirements.length === 0) {
|
|
6891
|
+
const targetName = subscriptionID || "Unknown";
|
|
6892
|
+
requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
6893
|
+
}
|
|
6143
6894
|
const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
|
|
6144
6895
|
const components = [];
|
|
6145
|
-
if (
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
}
|
|
6156
|
-
});
|
|
6157
|
-
}
|
|
6896
|
+
if (subscriptionID) components.push({
|
|
6897
|
+
name: `Azure Subscription ${subscriptionID}`,
|
|
6898
|
+
type: TargetType.CloudAccount,
|
|
6899
|
+
accountId: subscriptionID,
|
|
6900
|
+
provider: "azure",
|
|
6901
|
+
labels: {
|
|
6902
|
+
account: subscriptionID,
|
|
6903
|
+
provider: "azure"
|
|
6904
|
+
}
|
|
6905
|
+
});
|
|
6158
6906
|
return buildHdfResults({
|
|
6159
6907
|
generatorName: "msft-defender-cloud-to-hdf",
|
|
6160
6908
|
converterVersion: "1.0.0",
|
|
@@ -6229,7 +6977,7 @@ function extractDeviceTarget(alert) {
|
|
|
6229
6977
|
for (const ev of alert.evidence) if ((ev["@odata.type"] ?? "").includes("deviceEvidence") && ev.deviceDnsName) {
|
|
6230
6978
|
const target = {
|
|
6231
6979
|
name: ev.deviceDnsName,
|
|
6232
|
-
type:
|
|
6980
|
+
type: TargetType.Host,
|
|
6233
6981
|
labels: { provider: "azure" }
|
|
6234
6982
|
};
|
|
6235
6983
|
if (ev.deviceDnsName) target.fqdn = ev.deviceDnsName;
|
|
@@ -6239,7 +6987,7 @@ function extractDeviceTarget(alert) {
|
|
|
6239
6987
|
}
|
|
6240
6988
|
return {
|
|
6241
6989
|
name: alert.tenantId ?? "unknown",
|
|
6242
|
-
type:
|
|
6990
|
+
type: TargetType.CloudAccount,
|
|
6243
6991
|
accountId: alert.tenantId,
|
|
6244
6992
|
labels: {
|
|
6245
6993
|
account: alert.tenantId ?? "",
|
|
@@ -6308,6 +7056,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
|
|
|
6308
7056
|
components.push(target);
|
|
6309
7057
|
}
|
|
6310
7058
|
}
|
|
7059
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", /* @__PURE__ */ new Date()));
|
|
6311
7060
|
return buildHdfResults({
|
|
6312
7061
|
generatorName: "msft-defender-endpoint-to-hdf",
|
|
6313
7062
|
converterVersion: "1.0.0",
|
|
@@ -6448,8 +7197,7 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
6448
7197
|
[`${ATTR}idref`]: ruleIdRef,
|
|
6449
7198
|
result: wrap(hdfStatusToXccdf(result.status))
|
|
6450
7199
|
};
|
|
6451
|
-
|
|
6452
|
-
rr[`${ATTR}time`] = startTime;
|
|
7200
|
+
rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
|
|
6453
7201
|
if (result.message) rr.message = wrap(result.message);
|
|
6454
7202
|
if (result.codeDesc) rr.check = {
|
|
6455
7203
|
[`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
|
|
@@ -6879,7 +7627,7 @@ async function convertHdfToOscalPoam(input) {
|
|
|
6879
7627
|
return JSON.stringify(doc, null, 2);
|
|
6880
7628
|
}
|
|
6881
7629
|
/**
|
|
6882
|
-
* Converts parsed
|
|
7630
|
+
* Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
|
|
6883
7631
|
*/
|
|
6884
7632
|
function amendmentsToPOAM(amendments) {
|
|
6885
7633
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -7079,6 +7827,1150 @@ async function convertIonchannelToHdf(input) {
|
|
|
7079
7827
|
});
|
|
7080
7828
|
}
|
|
7081
7829
|
//#endregion
|
|
7830
|
+
//#region shared/typescript/vex/mapping.ts
|
|
7831
|
+
/** Canonical VEX status across OpenVEX / CSAF / CycloneDX. */
|
|
7832
|
+
const VexStatus = {
|
|
7833
|
+
NotAffected: "not_affected",
|
|
7834
|
+
Affected: "affected",
|
|
7835
|
+
Fixed: "fixed",
|
|
7836
|
+
UnderInvestigation: "under_investigation"
|
|
7837
|
+
};
|
|
7838
|
+
/**
|
|
7839
|
+
* Map an ecosystem-specific status string to the canonical VexStatus.
|
|
7840
|
+
* Returns undefined for values without a clean mapping — caller should warn
|
|
7841
|
+
* and skip, not guess.
|
|
7842
|
+
*/
|
|
7843
|
+
function normalizeStatus(raw) {
|
|
7844
|
+
switch (raw.trim().toLowerCase()) {
|
|
7845
|
+
case "not_affected":
|
|
7846
|
+
case "known_not_affected":
|
|
7847
|
+
case "false_positive": return VexStatus.NotAffected;
|
|
7848
|
+
case "affected":
|
|
7849
|
+
case "known_affected":
|
|
7850
|
+
case "exploitable": return VexStatus.Affected;
|
|
7851
|
+
case "fixed":
|
|
7852
|
+
case "first_fixed":
|
|
7853
|
+
case "resolved":
|
|
7854
|
+
case "resolved_with_pedigree": return VexStatus.Fixed;
|
|
7855
|
+
case "under_investigation":
|
|
7856
|
+
case "in_triage": return VexStatus.UnderInvestigation;
|
|
7857
|
+
default: return;
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
/**
|
|
7861
|
+
* Map an ecosystem-specific justification string to the canonical HDF
|
|
7862
|
+
* Justification enum. Returns undefined for unknown values; callers SHOULD
|
|
7863
|
+
* log unknown values rather than silently dropping (the schema spec wants
|
|
7864
|
+
* pass-through, but practically we expect the enum to be extended when a
|
|
7865
|
+
* new vocabulary is integrated rather than carrying raw labels
|
|
7866
|
+
* indefinitely).
|
|
7867
|
+
*
|
|
7868
|
+
* The HDF Justification enum (v3.2.x) covers:
|
|
7869
|
+
* - the original OpenVEX / CSAF VEX five values
|
|
7870
|
+
* - CycloneDX-specific reachability values (requires_*, protected_*)
|
|
7871
|
+
* that describe why a vulnerable code path is unreachable in the
|
|
7872
|
+
* deployed configuration.
|
|
7873
|
+
*/
|
|
7874
|
+
function normalizeJustification(raw) {
|
|
7875
|
+
switch (raw.trim().toLowerCase()) {
|
|
7876
|
+
case "component_not_present":
|
|
7877
|
+
case "code_not_present": return Justification.ComponentNotPresent;
|
|
7878
|
+
case "vulnerable_code_not_present": return Justification.VulnerableCodeNotPresent;
|
|
7879
|
+
case "vulnerable_code_not_in_execute_path":
|
|
7880
|
+
case "code_not_reachable": return Justification.VulnerableCodeNotInExecutePath;
|
|
7881
|
+
case "vulnerable_code_cannot_be_controlled_by_adversary": return Justification.VulnerableCodeCannotBeControlledByAdversary;
|
|
7882
|
+
case "inline_mitigations_already_exist":
|
|
7883
|
+
case "protected_by_mitigating_control": return Justification.InlineMitigationsAlreadyExist;
|
|
7884
|
+
case "requires_configuration": return Justification.RequiresConfiguration;
|
|
7885
|
+
case "requires_dependency": return Justification.RequiresDependency;
|
|
7886
|
+
case "requires_environment": return Justification.RequiresEnvironment;
|
|
7887
|
+
case "protected_by_compiler": return Justification.ProtectedByCompiler;
|
|
7888
|
+
case "protected_at_runtime": return Justification.ProtectedAtRuntime;
|
|
7889
|
+
case "protected_at_perimeter": return Justification.ProtectedAtPerimeter;
|
|
7890
|
+
default: return;
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
/**
|
|
7894
|
+
* Render an HDF Justification value as the CycloneDX-native vocabulary.
|
|
7895
|
+
* CycloneDX uses short-form names (code_not_present, code_not_reachable,
|
|
7896
|
+
* protected_by_mitigating_control) for the three justifications shared
|
|
7897
|
+
* with OpenVEX/CSAF, and shares the six CycloneDX-specific reachability
|
|
7898
|
+
* values verbatim.
|
|
7899
|
+
*
|
|
7900
|
+
* Returns undefined when the HDF value has no equivalent in CycloneDX's
|
|
7901
|
+
* enum (vulnerable_code_not_present and
|
|
7902
|
+
* vulnerable_code_cannot_be_controlled_by_adversary). Callers should
|
|
7903
|
+
* omit the justification field in that case.
|
|
7904
|
+
*/
|
|
7905
|
+
function justificationForCycloneDX(j) {
|
|
7906
|
+
switch (j) {
|
|
7907
|
+
case Justification.ComponentNotPresent: return "code_not_present";
|
|
7908
|
+
case Justification.VulnerableCodeNotInExecutePath: return "code_not_reachable";
|
|
7909
|
+
case Justification.InlineMitigationsAlreadyExist: return "protected_by_mitigating_control";
|
|
7910
|
+
case Justification.RequiresConfiguration:
|
|
7911
|
+
case Justification.RequiresDependency:
|
|
7912
|
+
case Justification.RequiresEnvironment:
|
|
7913
|
+
case Justification.ProtectedByCompiler:
|
|
7914
|
+
case Justification.ProtectedAtRuntime:
|
|
7915
|
+
case Justification.ProtectedAtPerimeter: return String(j);
|
|
7916
|
+
default: return;
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
/**
|
|
7920
|
+
* Return the amendment shape an importer should produce for a canonical VEX
|
|
7921
|
+
* status. Returns undefined for "affected" and "under_investigation" — those
|
|
7922
|
+
* are informational; the consumer creates an amendment later if they act.
|
|
7923
|
+
*/
|
|
7924
|
+
function importTargetFor(status) {
|
|
7925
|
+
switch (status) {
|
|
7926
|
+
case VexStatus.NotAffected: return {
|
|
7927
|
+
overrideType: OverrideType.FalsePositive,
|
|
7928
|
+
status: ResultStatus.Passed,
|
|
7929
|
+
setJustification: true,
|
|
7930
|
+
poamActionTemplate: ""
|
|
7931
|
+
};
|
|
7932
|
+
case VexStatus.Fixed: return {
|
|
7933
|
+
overrideType: OverrideType.Poam,
|
|
7934
|
+
status: ResultStatus.Failed,
|
|
7935
|
+
setJustification: false,
|
|
7936
|
+
poamActionTemplate: "vendor reports fix; apply and re-scan to verify"
|
|
7937
|
+
};
|
|
7938
|
+
case VexStatus.Affected:
|
|
7939
|
+
case VexStatus.UnderInvestigation: return;
|
|
7940
|
+
/* c8 ignore next 2 — every VexStatus has a case above */
|
|
7941
|
+
default: return;
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
/**
|
|
7945
|
+
* Return the canonical VEX status an exporter should emit for an HDF
|
|
7946
|
+
* override. Returns undefined when no VEX statement should be emitted —
|
|
7947
|
+
* the consumer has not acted, and VEX requires a deliberate statement.
|
|
7948
|
+
*
|
|
7949
|
+
* `allMilestonesCompleted` and `closureChained` are consulted only for
|
|
7950
|
+
* POA&M overrides. Closure is signalled by BOTH flags being true; either
|
|
7951
|
+
* alone is insufficient.
|
|
7952
|
+
*/
|
|
7953
|
+
function exportStatusFor(override, allMilestonesCompleted, closureChained) {
|
|
7954
|
+
if (!override) return;
|
|
7955
|
+
if (override.justification) return VexStatus.NotAffected;
|
|
7956
|
+
switch (override.type) {
|
|
7957
|
+
case OverrideType.FalsePositive:
|
|
7958
|
+
case OverrideType.Attestation:
|
|
7959
|
+
case OverrideType.Inherited: return VexStatus.NotAffected;
|
|
7960
|
+
case OverrideType.Waiver:
|
|
7961
|
+
case OverrideType.RiskAdjustment:
|
|
7962
|
+
case OverrideType.OperationalRequirement: return VexStatus.Affected;
|
|
7963
|
+
case OverrideType.Poam: return allMilestonesCompleted && closureChained ? VexStatus.Fixed : VexStatus.Affected;
|
|
7964
|
+
/* c8 ignore next 2 — every OverrideType has a case above */
|
|
7965
|
+
default: return;
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
const ECOSYSTEM_FROM_PURL_TYPE = {
|
|
7969
|
+
npm: Ecosystem.Npm,
|
|
7970
|
+
pypi: Ecosystem.Pypi,
|
|
7971
|
+
rpm: Ecosystem.RPM,
|
|
7972
|
+
deb: Ecosystem.Deb,
|
|
7973
|
+
maven: Ecosystem.Maven,
|
|
7974
|
+
gem: Ecosystem.Gem,
|
|
7975
|
+
nuget: Ecosystem.Nuget,
|
|
7976
|
+
golang: Ecosystem.Go,
|
|
7977
|
+
go: Ecosystem.Go,
|
|
7978
|
+
cargo: Ecosystem.Cargo
|
|
7979
|
+
};
|
|
7980
|
+
/**
|
|
7981
|
+
* Build an AffectedPackage from a single product identifier string emitted
|
|
7982
|
+
* by a VEX format. Recognizes PURLs and CPE 2.3 strings; returns undefined
|
|
7983
|
+
* for opaque identifiers (importer should drop the entry — schema additions
|
|
7984
|
+
* forbid fabricating name+version).
|
|
7985
|
+
*/
|
|
7986
|
+
function affectedPackageFromIdentifier(identifier) {
|
|
7987
|
+
if (!identifier) return void 0;
|
|
7988
|
+
if (identifier.startsWith("pkg:")) {
|
|
7989
|
+
const parsed = parsePurl(identifier);
|
|
7990
|
+
if (parsed) {
|
|
7991
|
+
const pkg = { purl: identifier };
|
|
7992
|
+
/* c8 ignore next 2 — parsePurl populates name/version when the
|
|
7993
|
+
identifier was a well-formed purl; the absent branches require a
|
|
7994
|
+
malformed-but-prefixed input that loses these fields without
|
|
7995
|
+
triggering the outer null return. Defensive. */
|
|
7996
|
+
if (parsed.name) pkg.name = parsed.name;
|
|
7997
|
+
if (parsed.version) pkg.version = parsed.version;
|
|
7998
|
+
pkg.ecosystem = ECOSYSTEM_FROM_PURL_TYPE[parsed.type] ?? Ecosystem.Generic;
|
|
7999
|
+
return pkg;
|
|
8000
|
+
}
|
|
8001
|
+
return { purl: identifier };
|
|
8002
|
+
}
|
|
8003
|
+
if (identifier.startsWith("cpe:2.3:")) return { cpe: identifier };
|
|
8004
|
+
}
|
|
8005
|
+
/**
|
|
8006
|
+
* Build a unique list of AffectedPackage entries from a sequence of product
|
|
8007
|
+
* identifier strings. Empty / unresolvable identifiers are dropped; duplicate
|
|
8008
|
+
* purls/cpes/names are collapsed.
|
|
8009
|
+
*/
|
|
8010
|
+
function affectedPackagesFromIdentifiers(identifiers) {
|
|
8011
|
+
const out = [];
|
|
8012
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8013
|
+
for (const id of identifiers) {
|
|
8014
|
+
const pkg = affectedPackageFromIdentifier(id);
|
|
8015
|
+
if (!pkg) continue;
|
|
8016
|
+
/* c8 ignore next — affectedPackageFromIdentifier always emits either
|
|
8017
|
+
purl or cpe, so the name fallback branch isn't reachable through
|
|
8018
|
+
this caller. Kept for safety if a hand-built AffectedPackage flows
|
|
8019
|
+
through a future caller. */
|
|
8020
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8021
|
+
if (seen.has(key)) continue;
|
|
8022
|
+
seen.add(key);
|
|
8023
|
+
out.push(pkg);
|
|
8024
|
+
}
|
|
8025
|
+
return out;
|
|
8026
|
+
}
|
|
8027
|
+
/**
|
|
8028
|
+
* Render an AffectedPackage as a single identifier string suitable for
|
|
8029
|
+
* round-tripping into a VEX format. Prefers purl > cpe > name@version.
|
|
8030
|
+
* Returns undefined when nothing identifying is set.
|
|
8031
|
+
*/
|
|
8032
|
+
function affectedPackageToIdentifier(pkg) {
|
|
8033
|
+
if (pkg.purl) return pkg.purl;
|
|
8034
|
+
if (pkg.cpe) return pkg.cpe;
|
|
8035
|
+
if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
|
|
8036
|
+
if (pkg.name) return pkg.name;
|
|
8037
|
+
}
|
|
8038
|
+
/**
|
|
8039
|
+
* Build an HDF Evidence entry pointing at the upstream VEX document. Used by
|
|
8040
|
+
* importers to preserve provenance even though we lose the structured
|
|
8041
|
+
* statement_id. Returns undefined when sourceURI is empty — don't fabricate.
|
|
8042
|
+
*/
|
|
8043
|
+
function supplierEvidence(sourceURI, description) {
|
|
8044
|
+
if (!sourceURI.trim()) return;
|
|
8045
|
+
return {
|
|
8046
|
+
type: EvidenceType.URL,
|
|
8047
|
+
data: sourceURI,
|
|
8048
|
+
description: description?.trim() ? description : "Upstream VEX statement"
|
|
8049
|
+
};
|
|
8050
|
+
}
|
|
8051
|
+
//#endregion
|
|
8052
|
+
//#region converters/openvex-to-hdf/typescript/converter.ts
|
|
8053
|
+
/**
|
|
8054
|
+
* OpenVEX to HDF Amendments converter.
|
|
8055
|
+
*
|
|
8056
|
+
* VEX statements are consumer-attached context for CVE findings. The act of
|
|
8057
|
+
* attaching IS the amendment, so this converter emits HDF Amendments rather
|
|
8058
|
+
* than HDF Results.
|
|
8059
|
+
*
|
|
8060
|
+
* VEX 'fixed' is an abstract supplier claim; the assessed system has not
|
|
8061
|
+
* been verified to carry the fix. Imports therefore become open POA&M
|
|
8062
|
+
* overrides (status pinned to failed pending re-scan), not status flips.
|
|
8063
|
+
*
|
|
8064
|
+
* Spec: https://github.com/openvex/spec
|
|
8065
|
+
*/
|
|
8066
|
+
/** One year in milliseconds. VEX statements are re-evaluated as new info
|
|
8067
|
+
* arrives; one year is a defensive default consistent with the
|
|
8068
|
+
* no-permanent-amendment rule on Standalone_Override. */
|
|
8069
|
+
const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
|
|
8070
|
+
async function convertOpenVexToHdf(input, converterVersion) {
|
|
8071
|
+
validateInputSize(input, "openvex-to-hdf");
|
|
8072
|
+
const doc = parseJSON(input);
|
|
8073
|
+
const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
|
|
8074
|
+
const overrides = [];
|
|
8075
|
+
for (const stmt of doc.statements ?? []) {
|
|
8076
|
+
const override = statementToOverride(stmt, doc, docTime);
|
|
8077
|
+
if (override) overrides.push(override);
|
|
8078
|
+
}
|
|
8079
|
+
if (overrides.length === 0) throw new Error("openvex-to-hdf: VEX document contains no actionable statements (all 'affected' or 'under_investigation'); no amendment to write");
|
|
8080
|
+
return {
|
|
8081
|
+
name: doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements",
|
|
8082
|
+
description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
|
|
8083
|
+
overrides,
|
|
8084
|
+
appliedBy: identityFor$1(doc.author, doc.role),
|
|
8085
|
+
generator: {
|
|
8086
|
+
name: "openvex-to-hdf",
|
|
8087
|
+
version: converterVersion
|
|
8088
|
+
},
|
|
8089
|
+
integrity: await inputIntegrity(input)
|
|
8090
|
+
};
|
|
8091
|
+
}
|
|
8092
|
+
function statementToOverride(stmt, doc, docTime) {
|
|
8093
|
+
const canonical = normalizeStatus(stmt.status ?? "");
|
|
8094
|
+
if (!canonical) return void 0;
|
|
8095
|
+
const target = importTargetFor(canonical);
|
|
8096
|
+
if (!target) return void 0;
|
|
8097
|
+
const requirementId = stmt.vulnerability?.name ?? stmt.vulnerability?.["@id"] ?? "";
|
|
8098
|
+
if (!requirementId) return void 0;
|
|
8099
|
+
const stmtTime = (stmt.timestamp ? parseTimestamp(stmt.timestamp) : null) ?? docTime;
|
|
8100
|
+
const author = stmt.author ?? doc.author ?? "";
|
|
8101
|
+
const expiresAt = new Date(stmtTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$2);
|
|
8102
|
+
const override = {
|
|
8103
|
+
type: target.overrideType,
|
|
8104
|
+
requirementId,
|
|
8105
|
+
appliedAt: stmtTime,
|
|
8106
|
+
expiresAt,
|
|
8107
|
+
appliedBy: identityFor$1(author, doc.role),
|
|
8108
|
+
reason: buildReason$2(stmt, target.poamActionTemplate)
|
|
8109
|
+
};
|
|
8110
|
+
const affectedPackages = affectedPackagesFromIdentifiers((stmt.products ?? []).map((p) => p["@id"]).filter((id) => Boolean(id)));
|
|
8111
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8112
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8113
|
+
if (target.setJustification && stmt.justification) {
|
|
8114
|
+
const j = normalizeJustification(stmt.justification);
|
|
8115
|
+
if (j) override.justification = j;
|
|
8116
|
+
}
|
|
8117
|
+
const ev = supplierEvidence(doc["@id"] ?? "", "OpenVEX document");
|
|
8118
|
+
if (ev) override.evidence = [ev];
|
|
8119
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8120
|
+
description: target.poamActionTemplate,
|
|
8121
|
+
status: MilestoneStatus.Pending,
|
|
8122
|
+
estimatedCompletion: expiresAt
|
|
8123
|
+
}];
|
|
8124
|
+
return override;
|
|
8125
|
+
}
|
|
8126
|
+
function buildReason$2(stmt, poamTemplate) {
|
|
8127
|
+
const parts = [];
|
|
8128
|
+
if (stmt.impact_statement) parts.push(stmt.impact_statement);
|
|
8129
|
+
if (stmt.action_statement) parts.push(stmt.action_statement);
|
|
8130
|
+
if (parts.length === 0) return poamTemplate || `Imported from OpenVEX status "${stmt.status ?? ""}"`;
|
|
8131
|
+
return parts.join("\n");
|
|
8132
|
+
}
|
|
8133
|
+
function identityFor$1(author, role) {
|
|
8134
|
+
if (!author) return {
|
|
8135
|
+
type: IdentityType.System,
|
|
8136
|
+
identifier: "openvex-import"
|
|
8137
|
+
};
|
|
8138
|
+
const id = {
|
|
8139
|
+
type: author.includes("@") ? IdentityType.Email : IdentityType.Simple,
|
|
8140
|
+
identifier: author
|
|
8141
|
+
};
|
|
8142
|
+
if (role) id.description = role;
|
|
8143
|
+
return id;
|
|
8144
|
+
}
|
|
8145
|
+
function truncateId(id) {
|
|
8146
|
+
const MAX = 80;
|
|
8147
|
+
return id.length <= MAX ? id : `${id.slice(0, MAX)}...`;
|
|
8148
|
+
}
|
|
8149
|
+
//#endregion
|
|
8150
|
+
//#region converters/csaf-vex-to-hdf/typescript/converter.ts
|
|
8151
|
+
/**
|
|
8152
|
+
* CSAF VEX to HDF Amendments converter.
|
|
8153
|
+
*
|
|
8154
|
+
* CSAF (OASIS Common Security Advisory Framework) VEX profile is the
|
|
8155
|
+
* vendor-advisory format. Per the amendment-output pattern (Step 4f),
|
|
8156
|
+
* 'fixed' becomes an open POA&M, not a status flip — supplier claim is
|
|
8157
|
+
* not assessed-system evidence.
|
|
8158
|
+
*
|
|
8159
|
+
* Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
|
|
8160
|
+
*/
|
|
8161
|
+
const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
|
|
8162
|
+
async function convertCsafVexToHdf(input, converterVersion) {
|
|
8163
|
+
validateInputSize(input, "csaf-vex-to-hdf");
|
|
8164
|
+
const doc = parseJSON(input);
|
|
8165
|
+
if (doc.document?.category !== "csaf_vex") throw new Error(`csaf-vex-to-hdf: document.category is ${JSON.stringify(doc.document?.category)}; only 'csaf_vex' is supported`);
|
|
8166
|
+
const dm = doc.document;
|
|
8167
|
+
const tracking = dm.tracking ?? {};
|
|
8168
|
+
const publisher = dm.publisher ?? {};
|
|
8169
|
+
const docTime = (tracking.current_release_date ? parseTimestamp(tracking.current_release_date) : null) ?? /* @__PURE__ */ new Date();
|
|
8170
|
+
const productLookup = buildProductLookup$1(doc.product_tree);
|
|
8171
|
+
const overrides = [];
|
|
8172
|
+
for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
|
|
8173
|
+
if (overrides.length === 0) throw new Error("csaf-vex-to-hdf: CSAF VEX document contains no actionable statements (only affected/under_investigation/recommended); no amendment to write");
|
|
8174
|
+
const publisherName = publisher.name ?? "";
|
|
8175
|
+
return {
|
|
8176
|
+
name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
|
|
8177
|
+
description: `Imported VEX advisory ${tracking.id ?? ""}`,
|
|
8178
|
+
overrides,
|
|
8179
|
+
appliedBy: identityFor(publisher),
|
|
8180
|
+
generator: {
|
|
8181
|
+
name: "csaf-vex-to-hdf",
|
|
8182
|
+
version: converterVersion
|
|
8183
|
+
},
|
|
8184
|
+
integrity: await inputIntegrity(input),
|
|
8185
|
+
version: tracking.version
|
|
8186
|
+
};
|
|
8187
|
+
}
|
|
8188
|
+
function vulnerabilityToOverrides(vuln, publisher, tracking, docTime, productLookup) {
|
|
8189
|
+
if (!vuln.cve) return [];
|
|
8190
|
+
const ps = vuln.product_status;
|
|
8191
|
+
if (!ps) return [];
|
|
8192
|
+
const out = [];
|
|
8193
|
+
if (ps.known_not_affected?.length) {
|
|
8194
|
+
const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.NotAffected, ps.known_not_affected, productLookup);
|
|
8195
|
+
if (o) out.push(o);
|
|
8196
|
+
}
|
|
8197
|
+
const fixedProducts = [...ps.fixed ?? [], ...ps.first_fixed ?? []];
|
|
8198
|
+
if (fixedProducts.length > 0) {
|
|
8199
|
+
const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.Fixed, fixedProducts, productLookup);
|
|
8200
|
+
if (o) out.push(o);
|
|
8201
|
+
}
|
|
8202
|
+
return out;
|
|
8203
|
+
}
|
|
8204
|
+
function buildOverride(vuln, publisher, tracking, docTime, canonical, products, productLookup) {
|
|
8205
|
+
const target = importTargetFor(canonical);
|
|
8206
|
+
if (!target) return void 0;
|
|
8207
|
+
const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$1);
|
|
8208
|
+
const override = {
|
|
8209
|
+
type: target.overrideType,
|
|
8210
|
+
requirementId: vuln.cve,
|
|
8211
|
+
appliedAt: docTime,
|
|
8212
|
+
expiresAt,
|
|
8213
|
+
appliedBy: identityFor(publisher),
|
|
8214
|
+
reason: buildReason$1(vuln, products)
|
|
8215
|
+
};
|
|
8216
|
+
const affectedPackages = resolveAffectedPackages(products, productLookup);
|
|
8217
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8218
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8219
|
+
if (target.setJustification) {
|
|
8220
|
+
const j = pickJustification(vuln, products);
|
|
8221
|
+
if (j) override.justification = j;
|
|
8222
|
+
}
|
|
8223
|
+
const evidence = [];
|
|
8224
|
+
const advisoryEv = supplierEvidence(advisoryURI(publisher, tracking), "CSAF VEX advisory");
|
|
8225
|
+
if (advisoryEv) evidence.push(advisoryEv);
|
|
8226
|
+
for (const r of vuln.references ?? []) {
|
|
8227
|
+
if (!r.url) continue;
|
|
8228
|
+
const ev = supplierEvidence(r.url, r.summary ?? r.category ?? "");
|
|
8229
|
+
if (ev) evidence.push(ev);
|
|
8230
|
+
}
|
|
8231
|
+
if (evidence.length > 0) override.evidence = evidence;
|
|
8232
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8233
|
+
description: firstActionRemediation(vuln, products) || target.poamActionTemplate,
|
|
8234
|
+
status: MilestoneStatus.Pending,
|
|
8235
|
+
estimatedCompletion: expiresAt
|
|
8236
|
+
}];
|
|
8237
|
+
return override;
|
|
8238
|
+
}
|
|
8239
|
+
function pickJustification(vuln, products) {
|
|
8240
|
+
const scope = new Set(products);
|
|
8241
|
+
for (const f of vuln.flags ?? []) {
|
|
8242
|
+
if (!overlap(f.product_ids ?? [], scope)) continue;
|
|
8243
|
+
if (!f.label) continue;
|
|
8244
|
+
const j = normalizeJustification(f.label);
|
|
8245
|
+
if (j) return j;
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
function firstActionRemediation(vuln, products) {
|
|
8249
|
+
const scope = new Set(products);
|
|
8250
|
+
for (const r of vuln.remediations ?? []) {
|
|
8251
|
+
const ids = r.product_ids;
|
|
8252
|
+
if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
|
|
8253
|
+
if ((r.category === "vendor_fix" || r.category === "mitigation" || r.category === "workaround") && r.details) return r.details;
|
|
8254
|
+
}
|
|
8255
|
+
return "";
|
|
8256
|
+
}
|
|
8257
|
+
function buildReason$1(vuln, products) {
|
|
8258
|
+
const parts = [];
|
|
8259
|
+
const scope = new Set(products);
|
|
8260
|
+
for (const n of vuln.notes ?? []) if (n.category === "description" && n.text) parts.push(n.text);
|
|
8261
|
+
for (const t of vuln.threats ?? []) {
|
|
8262
|
+
if (!t.details) continue;
|
|
8263
|
+
const ids = t.product_ids;
|
|
8264
|
+
if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
|
|
8265
|
+
parts.push(t.details);
|
|
8266
|
+
}
|
|
8267
|
+
if (parts.length === 0) return `Imported from CSAF VEX (${vuln.cve ?? "unknown CVE"})`;
|
|
8268
|
+
return parts.join("\n");
|
|
8269
|
+
}
|
|
8270
|
+
/**
|
|
8271
|
+
* Walk a CSAF product_tree and build a lookup from product_id to a
|
|
8272
|
+
* structured AffectedPackage. Prefers product_identification_helper.purl,
|
|
8273
|
+
* then .cpe, then the full_product_name's `name` (used for matching only
|
|
8274
|
+
* — name+version is hard to derive from CSAF reliably, so we drop entries
|
|
8275
|
+
* with no purl and no cpe).
|
|
8276
|
+
*/
|
|
8277
|
+
function buildProductLookup$1(tree) {
|
|
8278
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
8279
|
+
if (!tree) return lookup;
|
|
8280
|
+
if (tree.full_product_names) for (const fp of tree.full_product_names) registerProduct(fp, lookup);
|
|
8281
|
+
if (tree.branches) walkBranches(tree.branches, lookup);
|
|
8282
|
+
return lookup;
|
|
8283
|
+
}
|
|
8284
|
+
function walkBranches(branches, lookup, ctx = {}) {
|
|
8285
|
+
for (const b of branches) {
|
|
8286
|
+
const next = { ...ctx };
|
|
8287
|
+
/* c8 ignore next 2 — falsy branches require a malformed CSAF branch
|
|
8288
|
+
node (category set but name empty) which we don't fixture. */
|
|
8289
|
+
if (b.category === "product_name" && b.name) next.productName = b.name;
|
|
8290
|
+
if (b.category === "product_version" && b.name) next.version = b.name;
|
|
8291
|
+
if (b.product) registerProduct(b.product, lookup, next);
|
|
8292
|
+
if (b.branches) walkBranches(b.branches, lookup, next);
|
|
8293
|
+
}
|
|
8294
|
+
}
|
|
8295
|
+
function registerProduct(fp, lookup, ctx = {}) {
|
|
8296
|
+
/* c8 ignore next — defensive against a malformed CSAF full_product_name
|
|
8297
|
+
missing its required product_id field; not fixtured. */
|
|
8298
|
+
if (!fp.product_id) return;
|
|
8299
|
+
const helper = fp.product_identification_helper;
|
|
8300
|
+
if (helper?.purl) {
|
|
8301
|
+
const pkg = affectedPackageFromIdentifier(helper.purl);
|
|
8302
|
+
/* c8 ignore next 4 — affectedPackageFromIdentifier always returns a
|
|
8303
|
+
non-null value for an input that starts with 'pkg:' (preserves
|
|
8304
|
+
malformed input as purl-only). The null branch is unreachable from
|
|
8305
|
+
this callsite. */
|
|
8306
|
+
if (pkg) {
|
|
8307
|
+
lookup.set(fp.product_id, pkg);
|
|
8308
|
+
return;
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
if (helper?.cpe) {
|
|
8312
|
+
const pkg = affectedPackageFromIdentifier(helper.cpe);
|
|
8313
|
+
/* c8 ignore next 4 — same as above; identifiers starting with
|
|
8314
|
+
'cpe:2.3:' always produce a cpe-only AffectedPackage. */
|
|
8315
|
+
if (pkg) {
|
|
8316
|
+
lookup.set(fp.product_id, pkg);
|
|
8317
|
+
return;
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
if (ctx.productName && ctx.version) lookup.set(fp.product_id, {
|
|
8321
|
+
name: ctx.productName,
|
|
8322
|
+
version: ctx.version,
|
|
8323
|
+
ecosystem: Ecosystem.Generic
|
|
8324
|
+
});
|
|
8325
|
+
}
|
|
8326
|
+
function resolveAffectedPackages(productIds, lookup) {
|
|
8327
|
+
const out = [];
|
|
8328
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
8329
|
+
for (const id of productIds) {
|
|
8330
|
+
const pkg = lookup.get(id);
|
|
8331
|
+
if (!pkg) continue;
|
|
8332
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8333
|
+
if (seenKeys.has(key)) continue;
|
|
8334
|
+
seenKeys.add(key);
|
|
8335
|
+
out.push(pkg);
|
|
8336
|
+
}
|
|
8337
|
+
return out;
|
|
8338
|
+
}
|
|
8339
|
+
function identityFor(p) {
|
|
8340
|
+
if (!p.name) return {
|
|
8341
|
+
type: IdentityType.System,
|
|
8342
|
+
identifier: "csaf-vex-import"
|
|
8343
|
+
};
|
|
8344
|
+
const id = {
|
|
8345
|
+
type: IdentityType.Simple,
|
|
8346
|
+
identifier: p.name
|
|
8347
|
+
};
|
|
8348
|
+
if (p.category) id.description = p.category;
|
|
8349
|
+
return id;
|
|
8350
|
+
}
|
|
8351
|
+
function advisoryURI(publisher, tracking) {
|
|
8352
|
+
/* c8 ignore next */
|
|
8353
|
+
const id = tracking.id ?? "";
|
|
8354
|
+
const ns = publisher.namespace;
|
|
8355
|
+
if (ns && id) return `${ns.replace(/\/+$/, "")}/${id}`;
|
|
8356
|
+
return id;
|
|
8357
|
+
}
|
|
8358
|
+
function overlap(ids, scope) {
|
|
8359
|
+
return ids.some((id) => scope.has(id));
|
|
8360
|
+
}
|
|
8361
|
+
//#endregion
|
|
8362
|
+
//#region converters/cyclonedx-vex-to-hdf/typescript/converter.ts
|
|
8363
|
+
/**
|
|
8364
|
+
* CycloneDX VEX to HDF Amendments converter.
|
|
8365
|
+
*
|
|
8366
|
+
* CycloneDX VEX is not a separate format — it's a CycloneDX BOM whose
|
|
8367
|
+
* vulnerabilities[] carry an `analysis` object. CycloneDX-specific
|
|
8368
|
+
* justifications without an HDF equivalent (requires_configuration,
|
|
8369
|
+
* protected_by_compiler, etc.) are preserved verbatim in the reason
|
|
8370
|
+
* field via the shared helper's unknown-value passthrough.
|
|
8371
|
+
*
|
|
8372
|
+
* Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
|
|
8373
|
+
*/
|
|
8374
|
+
const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
8375
|
+
async function convertCyclonedxVexToHdf(input, converterVersion) {
|
|
8376
|
+
validateInputSize(input, "cyclonedx-vex-to-hdf");
|
|
8377
|
+
const bom = parseJSON(input);
|
|
8378
|
+
if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
|
|
8379
|
+
const docTime = (bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) : null) ?? /* @__PURE__ */ new Date();
|
|
8380
|
+
const productLookup = buildProductLookup(bom);
|
|
8381
|
+
const publisher = publisherIdentityOrDefault(bom);
|
|
8382
|
+
const overrides = [];
|
|
8383
|
+
for (const v of bom.vulnerabilities ?? []) {
|
|
8384
|
+
const o = vulnerabilityToOverride(v, productLookup, docTime, publisher);
|
|
8385
|
+
if (o) overrides.push(o);
|
|
8386
|
+
}
|
|
8387
|
+
if (overrides.length === 0) throw new Error("cyclonedx-vex-to-hdf: BOM contains no actionable VEX statements (only exploitable/in_triage or no analysis); no amendment to write");
|
|
8388
|
+
const publisherName = publisher.identifier;
|
|
8389
|
+
return {
|
|
8390
|
+
name: publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements",
|
|
8391
|
+
description: bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX",
|
|
8392
|
+
overrides,
|
|
8393
|
+
appliedBy: publisher,
|
|
8394
|
+
generator: {
|
|
8395
|
+
name: "cyclonedx-vex-to-hdf",
|
|
8396
|
+
version: converterVersion
|
|
8397
|
+
},
|
|
8398
|
+
integrity: await inputIntegrity(input)
|
|
8399
|
+
};
|
|
8400
|
+
}
|
|
8401
|
+
function vulnerabilityToOverride(v, productLookup, docTime, publisher) {
|
|
8402
|
+
if (!v.id || !v.analysis?.state) return void 0;
|
|
8403
|
+
const canonical = normalizeStatus(v.analysis.state);
|
|
8404
|
+
if (!canonical) return void 0;
|
|
8405
|
+
const target = importTargetFor(canonical);
|
|
8406
|
+
if (!target) return void 0;
|
|
8407
|
+
const affectedPackages = affectedPackagesForVuln(v, productLookup);
|
|
8408
|
+
const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS);
|
|
8409
|
+
const override = {
|
|
8410
|
+
type: target.overrideType,
|
|
8411
|
+
requirementId: v.id,
|
|
8412
|
+
appliedAt: docTime,
|
|
8413
|
+
expiresAt,
|
|
8414
|
+
appliedBy: publisher,
|
|
8415
|
+
reason: buildReason(v)
|
|
8416
|
+
};
|
|
8417
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8418
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8419
|
+
if (target.setJustification && v.analysis.justification) {
|
|
8420
|
+
const j = normalizeJustification(v.analysis.justification);
|
|
8421
|
+
if (j) override.justification = j;
|
|
8422
|
+
}
|
|
8423
|
+
const evidence = [];
|
|
8424
|
+
if (v.source?.url) {
|
|
8425
|
+
const ev = supplierEvidence(v.source.url, v.source.name ?? "");
|
|
8426
|
+
if (ev) evidence.push(ev);
|
|
8427
|
+
}
|
|
8428
|
+
for (const r of v.references ?? []) {
|
|
8429
|
+
if (!r.source?.url) continue;
|
|
8430
|
+
const ev = supplierEvidence(r.source.url, r.source.name ?? "");
|
|
8431
|
+
if (ev) evidence.push(ev);
|
|
8432
|
+
}
|
|
8433
|
+
if (evidence.length > 0) override.evidence = evidence;
|
|
8434
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8435
|
+
description: firstActionFromResponse(v.analysis.response ?? []) || target.poamActionTemplate,
|
|
8436
|
+
status: MilestoneStatus.Pending,
|
|
8437
|
+
estimatedCompletion: expiresAt
|
|
8438
|
+
}];
|
|
8439
|
+
return override;
|
|
8440
|
+
}
|
|
8441
|
+
function buildReason(v) {
|
|
8442
|
+
const parts = [];
|
|
8443
|
+
if (v.description) parts.push(v.description);
|
|
8444
|
+
if (v.analysis?.detail) parts.push(v.analysis.detail);
|
|
8445
|
+
return parts.join("\n");
|
|
8446
|
+
}
|
|
8447
|
+
/**
|
|
8448
|
+
* Resolve CycloneDX affects[].ref entries to structured AffectedPackage
|
|
8449
|
+
* entries. Looks up the bom-ref in the component table to recover purl,
|
|
8450
|
+
* name/version. Opaque bom-refs with no component-table match are dropped
|
|
8451
|
+
* (the schema forbids fabricating name+version, and bom-refs aren't
|
|
8452
|
+
* portable identifiers outside their source BOM).
|
|
8453
|
+
*/
|
|
8454
|
+
function affectedPackagesForVuln(v, lookup) {
|
|
8455
|
+
const out = [];
|
|
8456
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
8457
|
+
for (const a of v.affects ?? []) {
|
|
8458
|
+
if (!a.ref) continue;
|
|
8459
|
+
const comp = lookup.get(a.ref);
|
|
8460
|
+
const pkg = comp ? affectedPackageFromComponent(comp) : affectedPackageFromIdentifier(a.ref);
|
|
8461
|
+
if (!pkg) continue;
|
|
8462
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8463
|
+
if (seenKeys.has(key)) continue;
|
|
8464
|
+
seenKeys.add(key);
|
|
8465
|
+
out.push(pkg);
|
|
8466
|
+
}
|
|
8467
|
+
return out;
|
|
8468
|
+
}
|
|
8469
|
+
/**
|
|
8470
|
+
* Build an AffectedPackage from a CycloneDX Component. Prefers structured
|
|
8471
|
+
* purl decomposition via the shared helper (so name/version/ecosystem are
|
|
8472
|
+
* also populated when the purl is parseable); falls back to the component's
|
|
8473
|
+
* name+version when no purl is set. Returns undefined when the component
|
|
8474
|
+
* carries neither a purl nor a name+version pair (schema requires at least
|
|
8475
|
+
* name+version+ecosystem, purl, or cpe).
|
|
8476
|
+
*/
|
|
8477
|
+
function affectedPackageFromComponent(c) {
|
|
8478
|
+
if (c.purl) return affectedPackageFromIdentifier(c.purl);
|
|
8479
|
+
if (c.name && c.version) return {
|
|
8480
|
+
name: c.name,
|
|
8481
|
+
version: c.version,
|
|
8482
|
+
ecosystem: Ecosystem.Generic
|
|
8483
|
+
};
|
|
8484
|
+
}
|
|
8485
|
+
function buildProductLookup(bom) {
|
|
8486
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
8487
|
+
const root = bom.metadata?.component;
|
|
8488
|
+
if (root?.["bom-ref"]) lookup.set(root["bom-ref"], root);
|
|
8489
|
+
for (const c of bom.components ?? []) if (c["bom-ref"]) lookup.set(c["bom-ref"], c);
|
|
8490
|
+
return lookup;
|
|
8491
|
+
}
|
|
8492
|
+
function firstActionFromResponse(resp) {
|
|
8493
|
+
for (const r of resp) switch (r.trim().toLowerCase()) {
|
|
8494
|
+
case "update": return "Apply vendor update and re-scan to verify.";
|
|
8495
|
+
case "rollback": return "Roll back to the unaffected version and re-scan to verify.";
|
|
8496
|
+
case "workaround_available": return "Apply the documented workaround.";
|
|
8497
|
+
}
|
|
8498
|
+
return "";
|
|
8499
|
+
}
|
|
8500
|
+
function publisherIdentityOrDefault(bom) {
|
|
8501
|
+
for (const a of bom.metadata?.authors ?? []) {
|
|
8502
|
+
if (a.email) return {
|
|
8503
|
+
type: IdentityType.Email,
|
|
8504
|
+
identifier: a.email
|
|
8505
|
+
};
|
|
8506
|
+
if (a.name) return {
|
|
8507
|
+
type: IdentityType.Simple,
|
|
8508
|
+
identifier: a.name
|
|
8509
|
+
};
|
|
8510
|
+
}
|
|
8511
|
+
for (const t of bom.metadata?.tools ?? []) {
|
|
8512
|
+
const ident = [t.vendor, t.name].filter(Boolean).join(" ").trim();
|
|
8513
|
+
if (ident) return {
|
|
8514
|
+
type: IdentityType.System,
|
|
8515
|
+
identifier: ident
|
|
8516
|
+
};
|
|
8517
|
+
}
|
|
8518
|
+
return {
|
|
8519
|
+
type: IdentityType.System,
|
|
8520
|
+
identifier: "cyclonedx-vex-import"
|
|
8521
|
+
};
|
|
8522
|
+
}
|
|
8523
|
+
//#endregion
|
|
8524
|
+
//#region converters/hdf-to-csaf-vex/typescript/converter.ts
|
|
8525
|
+
/**
|
|
8526
|
+
* HDF Amendments to CSAF VEX (csaf_vex profile) converter.
|
|
8527
|
+
*
|
|
8528
|
+
* Reverse direction of csaf-vex-to-hdf. Intentionally partial-fidelity:
|
|
8529
|
+
* fields the shared VEX mapping considers consumer-action-bearing survive
|
|
8530
|
+
* round-trip; the rest collapse into the available CSAF fields.
|
|
8531
|
+
*/
|
|
8532
|
+
const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
|
|
8533
|
+
const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
|
|
8534
|
+
const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
|
|
8535
|
+
function convertHdfToCsafVex(input, converterVersion) {
|
|
8536
|
+
validateInputSize(input, "hdf-to-csaf-vex");
|
|
8537
|
+
const amendments = parseJSON(input);
|
|
8538
|
+
const groups = groupByCVE(amendments.overrides ?? []);
|
|
8539
|
+
const productSet = /* @__PURE__ */ new Map();
|
|
8540
|
+
const vulnerabilities = [];
|
|
8541
|
+
for (const group of groups) {
|
|
8542
|
+
const v = buildVulnerability(group);
|
|
8543
|
+
if (!v) continue;
|
|
8544
|
+
vulnerabilities.push(v);
|
|
8545
|
+
for (const p of productIDsForGroup(group)) productSet.set(p, true);
|
|
8546
|
+
}
|
|
8547
|
+
if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8548
|
+
const products = [...productSet.keys()].sort();
|
|
8549
|
+
const doc = buildDocument(amendments, converterVersion);
|
|
8550
|
+
doc.vulnerabilities = vulnerabilities;
|
|
8551
|
+
doc.product_tree.full_product_names = products.length > 0 ? products.map((p) => ({
|
|
8552
|
+
name: p,
|
|
8553
|
+
product_id: p
|
|
8554
|
+
})) : [{
|
|
8555
|
+
name: DEFAULT_PRODUCT_ID$2,
|
|
8556
|
+
product_id: DEFAULT_PRODUCT_ID$2
|
|
8557
|
+
}];
|
|
8558
|
+
return JSON.stringify(doc, null, 2);
|
|
8559
|
+
}
|
|
8560
|
+
function groupByCVE(overrides) {
|
|
8561
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8562
|
+
for (const o of overrides) {
|
|
8563
|
+
if (!CVE_ID_PATTERN$2.test(o.requirementId)) continue;
|
|
8564
|
+
let g = groups.get(o.requirementId);
|
|
8565
|
+
if (!g) {
|
|
8566
|
+
g = {
|
|
8567
|
+
cve: o.requirementId,
|
|
8568
|
+
overrides: []
|
|
8569
|
+
};
|
|
8570
|
+
groups.set(o.requirementId, g);
|
|
8571
|
+
}
|
|
8572
|
+
g.overrides.push(o);
|
|
8573
|
+
}
|
|
8574
|
+
return [...groups.values()].sort((a, b) => a.cve.localeCompare(b.cve));
|
|
8575
|
+
}
|
|
8576
|
+
function productIDsForGroup(group) {
|
|
8577
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8578
|
+
for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
|
|
8579
|
+
return [...seen].sort();
|
|
8580
|
+
}
|
|
8581
|
+
function productIDsFor$1(o) {
|
|
8582
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8583
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8584
|
+
if (ids.length > 0) return ids;
|
|
8585
|
+
}
|
|
8586
|
+
if (o.componentRef) return [o.componentRef];
|
|
8587
|
+
const m = PRODUCTS_LINE$2.exec(o.reason ?? "");
|
|
8588
|
+
if (m && m[1]) {
|
|
8589
|
+
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8590
|
+
if (parts.length > 0) return parts;
|
|
8591
|
+
}
|
|
8592
|
+
return [DEFAULT_PRODUCT_ID$2];
|
|
8593
|
+
}
|
|
8594
|
+
function stripProductsLine$1(reason) {
|
|
8595
|
+
return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
|
|
8596
|
+
}
|
|
8597
|
+
function allMilestonesCompleted$2(o) {
|
|
8598
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8599
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8600
|
+
}
|
|
8601
|
+
function buildVulnerability(group) {
|
|
8602
|
+
const v = { cve: group.cve };
|
|
8603
|
+
const status = {};
|
|
8604
|
+
let emitted = false;
|
|
8605
|
+
for (const o of group.overrides) {
|
|
8606
|
+
const pids = productIDsFor$1(o);
|
|
8607
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
|
|
8608
|
+
if (!canonical) continue;
|
|
8609
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
|
|
8610
|
+
if (canonical === VexStatus.NotAffected) {
|
|
8611
|
+
status.known_not_affected = (status.known_not_affected ?? []).concat(pids);
|
|
8612
|
+
if (o.justification) {
|
|
8613
|
+
v.flags = v.flags ?? [];
|
|
8614
|
+
v.flags.push({
|
|
8615
|
+
label: String(o.justification),
|
|
8616
|
+
product_ids: pids,
|
|
8617
|
+
date: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z")
|
|
8618
|
+
});
|
|
8619
|
+
}
|
|
8620
|
+
emitted = true;
|
|
8621
|
+
} else if (canonical === VexStatus.Fixed) {
|
|
8622
|
+
status.fixed = (status.fixed ?? []).concat(pids);
|
|
8623
|
+
for (const m of o.milestones ?? []) {
|
|
8624
|
+
if (!m.description) continue;
|
|
8625
|
+
v.remediations = v.remediations ?? [];
|
|
8626
|
+
v.remediations.push({
|
|
8627
|
+
category: "vendor_fix",
|
|
8628
|
+
details: m.description,
|
|
8629
|
+
product_ids: pids
|
|
8630
|
+
});
|
|
8631
|
+
}
|
|
8632
|
+
emitted = true;
|
|
8633
|
+
} else if (canonical === VexStatus.Affected) {
|
|
8634
|
+
status.known_affected = (status.known_affected ?? []).concat(pids);
|
|
8635
|
+
if (o.reason) {
|
|
8636
|
+
v.threats = v.threats ?? [];
|
|
8637
|
+
v.threats.push({
|
|
8638
|
+
category: "impact",
|
|
8639
|
+
details: stripProductsLine$1(o.reason),
|
|
8640
|
+
product_ids: pids
|
|
8641
|
+
});
|
|
8642
|
+
}
|
|
8643
|
+
if (o.type === OverrideType.Poam) for (const m of o.milestones ?? []) {
|
|
8644
|
+
if (!m.description) continue;
|
|
8645
|
+
v.remediations = v.remediations ?? [];
|
|
8646
|
+
v.remediations.push({
|
|
8647
|
+
category: "workaround",
|
|
8648
|
+
details: m.description,
|
|
8649
|
+
product_ids: pids
|
|
8650
|
+
});
|
|
8651
|
+
}
|
|
8652
|
+
emitted = true;
|
|
8653
|
+
}
|
|
8654
|
+
for (const e of o.evidence ?? []) {
|
|
8655
|
+
if (e.type !== "url" || !e.data) continue;
|
|
8656
|
+
v.references = v.references ?? [];
|
|
8657
|
+
v.references.push({
|
|
8658
|
+
category: "external",
|
|
8659
|
+
summary: e.description ?? "",
|
|
8660
|
+
url: e.data
|
|
8661
|
+
});
|
|
8662
|
+
}
|
|
8663
|
+
}
|
|
8664
|
+
/* c8 ignore next */
|
|
8665
|
+
if (!emitted) return void 0;
|
|
8666
|
+
if (status.fixed) status.fixed = [...new Set(status.fixed)];
|
|
8667
|
+
if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
|
|
8668
|
+
if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
|
|
8669
|
+
v.product_status = status;
|
|
8670
|
+
if (v.references) {
|
|
8671
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8672
|
+
v.references = v.references.filter((r) => {
|
|
8673
|
+
if (seen.has(r.url)) return false;
|
|
8674
|
+
seen.add(r.url);
|
|
8675
|
+
return true;
|
|
8676
|
+
});
|
|
8677
|
+
}
|
|
8678
|
+
return v;
|
|
8679
|
+
}
|
|
8680
|
+
function buildDocument(amendments, converterVersion) {
|
|
8681
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
8682
|
+
const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8683
|
+
const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
|
|
8684
|
+
const docVersion = amendments.version || "1";
|
|
8685
|
+
const title = amendments.name || "HDF Amendments exported as CSAF VEX";
|
|
8686
|
+
const notes = [];
|
|
8687
|
+
if (amendments.description) notes.push({
|
|
8688
|
+
category: "summary",
|
|
8689
|
+
text: amendments.description,
|
|
8690
|
+
title: "Description"
|
|
8691
|
+
});
|
|
8692
|
+
return {
|
|
8693
|
+
document: {
|
|
8694
|
+
category: "csaf_vex",
|
|
8695
|
+
csaf_version: "2.0",
|
|
8696
|
+
title,
|
|
8697
|
+
notes,
|
|
8698
|
+
publisher: {
|
|
8699
|
+
category: "other",
|
|
8700
|
+
name: publisherName
|
|
8701
|
+
},
|
|
8702
|
+
tracking: {
|
|
8703
|
+
id: trackingId,
|
|
8704
|
+
status: "final",
|
|
8705
|
+
version: docVersion,
|
|
8706
|
+
current_release_date: now,
|
|
8707
|
+
initial_release_date: now,
|
|
8708
|
+
revision_history: [{
|
|
8709
|
+
date: now,
|
|
8710
|
+
number: docVersion,
|
|
8711
|
+
summary: "Generated by hdf-to-csaf-vex from HDF Amendments."
|
|
8712
|
+
}],
|
|
8713
|
+
generator: {
|
|
8714
|
+
engine: {
|
|
8715
|
+
name: "hdf-to-csaf-vex",
|
|
8716
|
+
version: converterVersion
|
|
8717
|
+
},
|
|
8718
|
+
date: now
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
},
|
|
8722
|
+
product_tree: { full_product_names: [] },
|
|
8723
|
+
vulnerabilities: []
|
|
8724
|
+
};
|
|
8725
|
+
}
|
|
8726
|
+
//#endregion
|
|
8727
|
+
//#region converters/hdf-to-openvex/typescript/converter.ts
|
|
8728
|
+
/**
|
|
8729
|
+
* HDF Amendments to OpenVEX converter.
|
|
8730
|
+
*
|
|
8731
|
+
* Reverse direction of openvex-to-hdf. Step 4f amendment-output pattern,
|
|
8732
|
+
* partial-fidelity by design — consumer-action-bearing fields (CVE id,
|
|
8733
|
+
* status, justification) survive round-trip; the rest collapse.
|
|
8734
|
+
*/
|
|
8735
|
+
const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
|
|
8736
|
+
const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
|
|
8737
|
+
const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
8738
|
+
const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
|
|
8739
|
+
const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
|
|
8740
|
+
async function convertHdfToOpenVex(input, converterVersion) {
|
|
8741
|
+
validateInputSize(input, "hdf-to-openvex");
|
|
8742
|
+
const amendments = parseJSON(input);
|
|
8743
|
+
const statements = [];
|
|
8744
|
+
let earliest;
|
|
8745
|
+
for (const o of amendments.overrides ?? []) {
|
|
8746
|
+
const s = overrideToStatement(o);
|
|
8747
|
+
if (!s) continue;
|
|
8748
|
+
statements.push(s);
|
|
8749
|
+
const t = new Date(o.appliedAt);
|
|
8750
|
+
if (!earliest || t < earliest) earliest = t;
|
|
8751
|
+
}
|
|
8752
|
+
if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8753
|
+
statements.sort((a, b) => a.vulnerability.name.localeCompare(b.vulnerability.name));
|
|
8754
|
+
const author = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8755
|
+
const role = amendments.appliedBy?.description;
|
|
8756
|
+
const doc = {
|
|
8757
|
+
"@context": OPENVEX_CONTEXT,
|
|
8758
|
+
"@id": await buildDocumentID(input, amendments),
|
|
8759
|
+
author,
|
|
8760
|
+
role,
|
|
8761
|
+
timestamp: (earliest ?? /* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z"),
|
|
8762
|
+
version: 1,
|
|
8763
|
+
statements
|
|
8764
|
+
};
|
|
8765
|
+
return JSON.stringify(doc, null, 2);
|
|
8766
|
+
}
|
|
8767
|
+
function overrideToStatement(o) {
|
|
8768
|
+
if (!CVE_ID_PATTERN$1.test(o.requirementId)) return void 0;
|
|
8769
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
|
|
8770
|
+
if (!canonical) return void 0;
|
|
8771
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
|
|
8772
|
+
const stmt = {
|
|
8773
|
+
vulnerability: {
|
|
8774
|
+
name: o.requirementId,
|
|
8775
|
+
"@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
8776
|
+
},
|
|
8777
|
+
status: String(canonical),
|
|
8778
|
+
timestamp: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z"),
|
|
8779
|
+
products: productsFor(o)
|
|
8780
|
+
};
|
|
8781
|
+
if (canonical === VexStatus.NotAffected) {
|
|
8782
|
+
if (o.justification) stmt.justification = String(o.justification);
|
|
8783
|
+
const impact = stripProductsLine(o.reason ?? "");
|
|
8784
|
+
if (impact) stmt.impact_statement = impact;
|
|
8785
|
+
} else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
|
|
8786
|
+
else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
|
|
8787
|
+
return stmt;
|
|
8788
|
+
}
|
|
8789
|
+
function productsFor(o) {
|
|
8790
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8791
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8792
|
+
if (ids.length > 0) return ids.map((id) => ({ "@id": id }));
|
|
8793
|
+
}
|
|
8794
|
+
let ids = [];
|
|
8795
|
+
if (o.componentRef) ids = [o.componentRef];
|
|
8796
|
+
else {
|
|
8797
|
+
const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
|
|
8798
|
+
if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8799
|
+
}
|
|
8800
|
+
if (ids.length === 0) ids = [DEFAULT_PRODUCT_ID$1];
|
|
8801
|
+
return ids.map((id) => ({ "@id": id }));
|
|
8802
|
+
}
|
|
8803
|
+
function firstMilestoneAction(o) {
|
|
8804
|
+
for (const m of o.milestones ?? []) if (m.description) return m.description;
|
|
8805
|
+
return "";
|
|
8806
|
+
}
|
|
8807
|
+
function allMilestonesCompleted$1(o) {
|
|
8808
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8809
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8810
|
+
}
|
|
8811
|
+
function stripProductsLine(reason) {
|
|
8812
|
+
return reason.replace(PRODUCTS_LINE$1, "").replace(/\n+$/, "");
|
|
8813
|
+
}
|
|
8814
|
+
async function buildDocumentID(input, a) {
|
|
8815
|
+
if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
|
|
8816
|
+
return `${OPENVEX_NAMESPACE}vex-${await sha256(input)}`;
|
|
8817
|
+
}
|
|
8818
|
+
//#endregion
|
|
8819
|
+
//#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
|
|
8820
|
+
/**
|
|
8821
|
+
* HDF Amendments to CycloneDX VEX converter (export side).
|
|
8822
|
+
*
|
|
8823
|
+
* Reverse direction of cyclonedx-vex-to-hdf. Step 4f amendment-output
|
|
8824
|
+
* pattern, partial-fidelity by design — consumer-action-bearing fields
|
|
8825
|
+
* (CVE id, status, justification) survive round-trip; the rest collapse
|
|
8826
|
+
* into the available CycloneDX VEX fields.
|
|
8827
|
+
*/
|
|
8828
|
+
const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
|
8829
|
+
const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
|
|
8830
|
+
const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
|
|
8831
|
+
const RESPONSE_LINE = /^Response:.*$/gm;
|
|
8832
|
+
const DEFAULT_PRODUCT_ID = "HDFPID-0001";
|
|
8833
|
+
function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
8834
|
+
validateInputSize(input, "hdf-to-cyclonedx-vex");
|
|
8835
|
+
const amendments = parseJSON(input);
|
|
8836
|
+
const componentRegistry = /* @__PURE__ */ new Map();
|
|
8837
|
+
const vulnerabilities = [];
|
|
8838
|
+
let earliest;
|
|
8839
|
+
for (const o of amendments.overrides ?? []) {
|
|
8840
|
+
if (!CVE_ID_PATTERN.test(o.requirementId)) continue;
|
|
8841
|
+
const v = overrideToVulnerability(o, componentRegistry);
|
|
8842
|
+
if (!v) continue;
|
|
8843
|
+
vulnerabilities.push(v);
|
|
8844
|
+
const t = new Date(o.appliedAt);
|
|
8845
|
+
if (!earliest || t < earliest) earliest = t;
|
|
8846
|
+
}
|
|
8847
|
+
if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8848
|
+
vulnerabilities.sort((a, b) => a.id.localeCompare(b.id));
|
|
8849
|
+
const components = [...componentRegistry.values()].sort((a, b) => a["bom-ref"].localeCompare(b["bom-ref"]));
|
|
8850
|
+
const bom = {
|
|
8851
|
+
bomFormat: "CycloneDX",
|
|
8852
|
+
specVersion: "1.4",
|
|
8853
|
+
serialNumber: buildSerialNumberSync(input, amendments),
|
|
8854
|
+
version: 1,
|
|
8855
|
+
metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
|
|
8856
|
+
components,
|
|
8857
|
+
vulnerabilities
|
|
8858
|
+
};
|
|
8859
|
+
return JSON.stringify(bom, null, 2);
|
|
8860
|
+
}
|
|
8861
|
+
function overrideToVulnerability(o, componentRegistry) {
|
|
8862
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted(o), false);
|
|
8863
|
+
if (!canonical) return void 0;
|
|
8864
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted(o)) canonical = VexStatus.Fixed;
|
|
8865
|
+
const pids = productIDsFor(o);
|
|
8866
|
+
const pkgById = /* @__PURE__ */ new Map();
|
|
8867
|
+
for (const p of o.affectedPackages ?? []) {
|
|
8868
|
+
const id = affectedPackageToIdentifier(p);
|
|
8869
|
+
if (id) pkgById.set(id, p);
|
|
8870
|
+
}
|
|
8871
|
+
for (const pid of pids) componentRegistry.set(pid, componentFor(pid, pkgById.get(pid)));
|
|
8872
|
+
const analysis = { state: canonicalToCycloneDXState(canonical) };
|
|
8873
|
+
if (o.justification) {
|
|
8874
|
+
const cdxJust = justificationForCycloneDX(o.justification);
|
|
8875
|
+
if (cdxJust) analysis.justification = cdxJust;
|
|
8876
|
+
}
|
|
8877
|
+
const detail = stripReasonAnnotations(o.reason ?? "");
|
|
8878
|
+
if (detail) analysis.detail = detail;
|
|
8879
|
+
if (canonical === VexStatus.Fixed) analysis.response = ["update"];
|
|
8880
|
+
else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
|
|
8881
|
+
const v = {
|
|
8882
|
+
id: o.requirementId,
|
|
8883
|
+
source: {
|
|
8884
|
+
name: "NVD",
|
|
8885
|
+
url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
8886
|
+
},
|
|
8887
|
+
analysis,
|
|
8888
|
+
affects: pids.map((p) => ({ ref: p }))
|
|
8889
|
+
};
|
|
8890
|
+
for (const e of o.evidence ?? []) {
|
|
8891
|
+
if (e.type !== "url" || !e.data) continue;
|
|
8892
|
+
v.references = v.references ?? [];
|
|
8893
|
+
v.references.push({
|
|
8894
|
+
id: o.requirementId,
|
|
8895
|
+
source: {
|
|
8896
|
+
name: e.description ?? "",
|
|
8897
|
+
url: e.data
|
|
8898
|
+
}
|
|
8899
|
+
});
|
|
8900
|
+
}
|
|
8901
|
+
return v;
|
|
8902
|
+
}
|
|
8903
|
+
function canonicalToCycloneDXState(canonical) {
|
|
8904
|
+
switch (canonical) {
|
|
8905
|
+
case VexStatus.NotAffected: return "not_affected";
|
|
8906
|
+
case VexStatus.Fixed: return "resolved";
|
|
8907
|
+
case VexStatus.Affected: return "exploitable";
|
|
8908
|
+
/* c8 ignore next 2 — defensive: every VexStatus has a case above */
|
|
8909
|
+
default: return canonical;
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
function componentFor(pid, pkg) {
|
|
8913
|
+
const c = {
|
|
8914
|
+
type: "application",
|
|
8915
|
+
name: pkg?.name ?? pid,
|
|
8916
|
+
"bom-ref": pid
|
|
8917
|
+
};
|
|
8918
|
+
if (pkg?.version) c.version = pkg.version;
|
|
8919
|
+
if (pkg?.purl ?? (pid.startsWith("pkg:") ? pid : void 0)) c.purl = pkg?.purl ?? pid;
|
|
8920
|
+
if (pkg?.cpe ?? (pid.startsWith("cpe:2.3:") ? pid : void 0)) c.cpe = pkg?.cpe ?? pid;
|
|
8921
|
+
return c;
|
|
8922
|
+
}
|
|
8923
|
+
function productIDsFor(o) {
|
|
8924
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8925
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8926
|
+
if (ids.length > 0) return ids;
|
|
8927
|
+
}
|
|
8928
|
+
if (o.componentRef) return [o.componentRef];
|
|
8929
|
+
const m = PRODUCTS_LINE.exec(o.reason ?? "");
|
|
8930
|
+
if (m && m[1]) {
|
|
8931
|
+
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8932
|
+
if (parts.length > 0) return parts;
|
|
8933
|
+
}
|
|
8934
|
+
return [DEFAULT_PRODUCT_ID];
|
|
8935
|
+
}
|
|
8936
|
+
function stripReasonAnnotations(reason) {
|
|
8937
|
+
return reason.replace(PRODUCTS_LINE, "").replace(RAW_JUST_LINE, "").replace(RESPONSE_LINE, "").trim();
|
|
8938
|
+
}
|
|
8939
|
+
function allMilestonesCompleted(o) {
|
|
8940
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8941
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8942
|
+
}
|
|
8943
|
+
function buildMetadata(a, docTime, converterVersion) {
|
|
8944
|
+
const metadata = {
|
|
8945
|
+
timestamp: docTime.toISOString().replace(/\.\d+Z$/, "Z"),
|
|
8946
|
+
tools: [{
|
|
8947
|
+
vendor: "mitre",
|
|
8948
|
+
name: "hdf-to-cyclonedx-vex",
|
|
8949
|
+
version: converterVersion
|
|
8950
|
+
}]
|
|
8951
|
+
};
|
|
8952
|
+
if (a.appliedBy?.identifier) {
|
|
8953
|
+
const author = {};
|
|
8954
|
+
if (a.appliedBy.type === IdentityType.Email) author.email = a.appliedBy.identifier;
|
|
8955
|
+
else author.name = a.appliedBy.identifier;
|
|
8956
|
+
metadata.authors = [author];
|
|
8957
|
+
}
|
|
8958
|
+
return metadata;
|
|
8959
|
+
}
|
|
8960
|
+
function buildSerialNumberSync(input, a) {
|
|
8961
|
+
if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
|
|
8962
|
+
return `urn:uuid:${fnv1a(input)}`;
|
|
8963
|
+
}
|
|
8964
|
+
function fnv1a(s) {
|
|
8965
|
+
let h = 2166136261;
|
|
8966
|
+
for (let i = 0; i < s.length; i++) {
|
|
8967
|
+
h ^= s.charCodeAt(i);
|
|
8968
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
8969
|
+
}
|
|
8970
|
+
const d = h.toString(16).padStart(8, "0");
|
|
8971
|
+
return (d + d + d + d).slice(0, 32);
|
|
8972
|
+
}
|
|
8973
|
+
//#endregion
|
|
7082
8974
|
//#region converters/oscal-to-hdf/typescript/detect.ts
|
|
7083
8975
|
/**
|
|
7084
8976
|
* OSCAL document type detection.
|
|
@@ -7167,7 +9059,7 @@ async function catalogToBaseline(catalog, rawInput) {
|
|
|
7167
9059
|
requirements,
|
|
7168
9060
|
groups,
|
|
7169
9061
|
generator: {
|
|
7170
|
-
name: "hdf
|
|
9062
|
+
name: "oscal-catalog-to-hdf",
|
|
7171
9063
|
version: "1.0.0"
|
|
7172
9064
|
}
|
|
7173
9065
|
};
|
|
@@ -7410,7 +9302,7 @@ async function convertOscalComponentToHdf(input) {
|
|
|
7410
9302
|
integrity,
|
|
7411
9303
|
requirements,
|
|
7412
9304
|
generator: {
|
|
7413
|
-
name: "hdf
|
|
9305
|
+
name: "oscal-component-to-hdf",
|
|
7414
9306
|
version: "1.0.0"
|
|
7415
9307
|
}
|
|
7416
9308
|
};
|
|
@@ -7471,7 +9363,7 @@ async function convertOscalSspToHdf(input) {
|
|
|
7471
9363
|
integrity,
|
|
7472
9364
|
components: [],
|
|
7473
9365
|
generator: {
|
|
7474
|
-
name: "hdf
|
|
9366
|
+
name: "oscal-ssp-to-hdf",
|
|
7475
9367
|
version: "1.0.0"
|
|
7476
9368
|
}
|
|
7477
9369
|
};
|
|
@@ -7598,13 +9490,13 @@ function sspComponentToHDFComponent(sc, componentControls) {
|
|
|
7598
9490
|
function mapOSCALComponentType(oscalType) {
|
|
7599
9491
|
switch (oscalType.toLowerCase()) {
|
|
7600
9492
|
case "software":
|
|
7601
|
-
case "this-system": return
|
|
7602
|
-
case "service": return
|
|
7603
|
-
case "hardware": return
|
|
7604
|
-
case "network": return
|
|
7605
|
-
case "database": return
|
|
7606
|
-
case "storage": return
|
|
7607
|
-
default: return
|
|
9493
|
+
case "this-system": return TargetType.Application;
|
|
9494
|
+
case "service": return TargetType.Application;
|
|
9495
|
+
case "hardware": return TargetType.Host;
|
|
9496
|
+
case "network": return TargetType.Network;
|
|
9497
|
+
case "database": return TargetType.Database;
|
|
9498
|
+
case "storage": return TargetType.Artifact;
|
|
9499
|
+
default: return TargetType.Application;
|
|
7608
9500
|
}
|
|
7609
9501
|
}
|
|
7610
9502
|
//#endregion
|
|
@@ -7641,7 +9533,7 @@ async function convertOscalSapToHdf(input) {
|
|
|
7641
9533
|
type: planType,
|
|
7642
9534
|
description,
|
|
7643
9535
|
generator: {
|
|
7644
|
-
name: "hdf
|
|
9536
|
+
name: "oscal-sap-to-hdf",
|
|
7645
9537
|
version: "1.0.0"
|
|
7646
9538
|
}
|
|
7647
9539
|
};
|
|
@@ -7771,7 +9663,7 @@ async function convertOscalPoamToHdf(input) {
|
|
|
7771
9663
|
version: meta.version,
|
|
7772
9664
|
appliedBy,
|
|
7773
9665
|
generator: {
|
|
7774
|
-
name: "hdf
|
|
9666
|
+
name: "oscal-poam-to-hdf",
|
|
7775
9667
|
version: "1.0.0"
|
|
7776
9668
|
}
|
|
7777
9669
|
};
|
|
@@ -8084,6 +9976,6 @@ function sarBaselineName(result, sar) {
|
|
|
8084
9976
|
return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
|
|
8085
9977
|
}
|
|
8086
9978
|
//#endregion
|
|
8087
|
-
export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCyclonedxToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsv, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
|
|
9979
|
+
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 };
|
|
8088
9980
|
|
|
8089
9981
|
//# sourceMappingURL=index.js.map
|