@mitre/hdf-converters 3.6.0-rc.2 → 3.6.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +642 -238
- package/dist/index.js.map +1 -1
- package/package.json +8 -9
package/dist/index.js
CHANGED
|
@@ -1,8 +1,150 @@
|
|
|
1
1
|
import { n as detectFormat, r as detectConverter, t as registerAllFingerprints } from "./register-all-CDfYvwQF.js";
|
|
2
2
|
import { flattenOverlays } from "@mitre/hdf-parsers";
|
|
3
|
-
import { buildCsv, buildXml, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, governingStatusOverride, impactToSeverity, isUnratedSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, severityToImpact, severityToImpactWithAliases, sha256, stripHtml, stripHtml as stripHTML, trimUtcFraction, worstStatus } from "@mitre/hdf-utilities";
|
|
4
|
-
import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact as severityToImpact$1 } from "@mitre/hdf-schema";
|
|
3
|
+
import { buildCsv, buildXml, canonicalJson, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, governingStatusOverride, impactToSeverity, isUnratedSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, severityToImpact, severityToImpactWithAliases, sha256, stripHtml, stripHtml as stripHTML, trimUtcFraction, worstStatus, xmlSafeText } from "@mitre/hdf-utilities";
|
|
4
|
+
import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, affectedPackageSchema, amendmentsSchema, bomSchema, commonSchema, comparisonSchema, componentSchema, createDescription, createMinimalBaseline, createRequirement, createResult, cvssSchema, dataFlowSchema, epssSchema, eventsSchema, extensionsSchema, hdfAmendmentsSchema, hdfBaselineSchema, hdfComparisonSchema, hdfEvidencePackageSchema, hdfPlanSchema, hdfRequirementChangeEventSchema, hdfResultsSchema, hdfSystemSchema, kevSchema, parameterSchema, planSchema, platformSchema, resultSchema, runnerSchema, severityToImpact as severityToImpact$1, statisticsSchema, systemSchema, targetSchema } from "@mitre/hdf-schema";
|
|
5
5
|
import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_CONFIG_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getHipcheckNistControls, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
|
|
6
|
+
import Ajv from "ajv";
|
|
7
|
+
import addFormats from "ajv-formats";
|
|
8
|
+
//#region ../hdf-validators/dist/index.js
|
|
9
|
+
/**
|
|
10
|
+
* Create and configure Ajv instance with all HDF schemas
|
|
11
|
+
*/
|
|
12
|
+
function createValidator() {
|
|
13
|
+
const ajv = new Ajv({
|
|
14
|
+
allErrors: true,
|
|
15
|
+
verbose: true,
|
|
16
|
+
strict: false,
|
|
17
|
+
validateFormats: true,
|
|
18
|
+
validateSchema: false
|
|
19
|
+
});
|
|
20
|
+
addFormats(ajv);
|
|
21
|
+
const registeredDateTime = ajv.formats["date-time"];
|
|
22
|
+
ajv.addFormat("date-time", {
|
|
23
|
+
type: "string",
|
|
24
|
+
validate: (v) => /^\d{4}-\d{2}-\d{2}[Tt]/.test(v) && registeredDateTime.validate(v)
|
|
25
|
+
});
|
|
26
|
+
ajv.addSchema(commonSchema);
|
|
27
|
+
ajv.addSchema(platformSchema);
|
|
28
|
+
ajv.addSchema(resultSchema);
|
|
29
|
+
ajv.addSchema(runnerSchema);
|
|
30
|
+
ajv.addSchema(statisticsSchema);
|
|
31
|
+
ajv.addSchema(targetSchema);
|
|
32
|
+
ajv.addSchema(parameterSchema);
|
|
33
|
+
ajv.addSchema(cvssSchema);
|
|
34
|
+
ajv.addSchema(epssSchema);
|
|
35
|
+
ajv.addSchema(kevSchema);
|
|
36
|
+
ajv.addSchema(affectedPackageSchema);
|
|
37
|
+
ajv.addSchema(bomSchema);
|
|
38
|
+
ajv.addSchema(amendmentsSchema);
|
|
39
|
+
ajv.addSchema(extensionsSchema);
|
|
40
|
+
ajv.addSchema(systemSchema);
|
|
41
|
+
ajv.addSchema(planSchema);
|
|
42
|
+
ajv.addSchema(comparisonSchema);
|
|
43
|
+
ajv.addSchema(eventsSchema);
|
|
44
|
+
ajv.addSchema(componentSchema);
|
|
45
|
+
ajv.addSchema(dataFlowSchema);
|
|
46
|
+
ajv.addSchema(hdfResultsSchema);
|
|
47
|
+
ajv.addSchema(hdfBaselineSchema);
|
|
48
|
+
ajv.addSchema(hdfComparisonSchema);
|
|
49
|
+
ajv.addSchema(hdfSystemSchema);
|
|
50
|
+
ajv.addSchema(hdfPlanSchema);
|
|
51
|
+
ajv.addSchema(hdfAmendmentsSchema);
|
|
52
|
+
ajv.addSchema(hdfRequirementChangeEventSchema);
|
|
53
|
+
ajv.addSchema(hdfEvidencePackageSchema);
|
|
54
|
+
return ajv;
|
|
55
|
+
}
|
|
56
|
+
const ajv = createValidator();
|
|
57
|
+
let resultsValidator = null;
|
|
58
|
+
function getResultsValidator() {
|
|
59
|
+
if (!resultsValidator) resultsValidator = ajv.compile(hdfResultsSchema);
|
|
60
|
+
return resultsValidator;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert Ajv errors to ValidationError format
|
|
64
|
+
*/
|
|
65
|
+
function formatErrors(errors) {
|
|
66
|
+
if (!errors || errors.length === 0) return [];
|
|
67
|
+
return errors.map((err) => {
|
|
68
|
+
let field = err.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
|
69
|
+
if (!field && err.schemaPath) {
|
|
70
|
+
const pathParts = err.schemaPath.split("/");
|
|
71
|
+
field = pathParts[pathParts.length - 1] || "(root)";
|
|
72
|
+
}
|
|
73
|
+
let message = err.message || "validation failed";
|
|
74
|
+
if (err.params) {
|
|
75
|
+
if ("missingProperty" in err.params) {
|
|
76
|
+
field = field ? `${field}.${err.params.missingProperty}` : err.params.missingProperty;
|
|
77
|
+
message = "is required";
|
|
78
|
+
} else if ("additionalProperty" in err.params) {
|
|
79
|
+
field = field ? `${field}.${err.params.additionalProperty}` : err.params.additionalProperty;
|
|
80
|
+
message = "is not allowed";
|
|
81
|
+
} else if ("limit" in err.params) message = `${message} (limit: ${err.params.limit})`;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
field: field || "(root)",
|
|
85
|
+
keyword: err.keyword,
|
|
86
|
+
...err.keyword === "format" && err.params && "format" in err.params ? { format: String(err.params.format) } : {},
|
|
87
|
+
message,
|
|
88
|
+
value: err.data
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create ValidationResult from validator output
|
|
94
|
+
*/
|
|
95
|
+
function createResult$1(validator, data) {
|
|
96
|
+
const valid = validator(data);
|
|
97
|
+
const errors = formatErrors(validator.errors);
|
|
98
|
+
return {
|
|
99
|
+
valid: valid === true,
|
|
100
|
+
errors,
|
|
101
|
+
getErrorMessage() {
|
|
102
|
+
if (this.valid) return "";
|
|
103
|
+
return this.errors.map((e) => {
|
|
104
|
+
if (e.field === "(root)") return e.message;
|
|
105
|
+
return `${e.field}: ${e.message}`;
|
|
106
|
+
}).join("; ");
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validate HDF Results document against schema
|
|
112
|
+
*/
|
|
113
|
+
function validateResults(data) {
|
|
114
|
+
return createResult$1(getResultsValidator(), data);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region shared/typescript/amendmentchain.ts
|
|
118
|
+
/**
|
|
119
|
+
* Stamps `previousChecksum` across amendment overrides in document order,
|
|
120
|
+
* linking each to the one before it and leaving the first unlinked.
|
|
121
|
+
*
|
|
122
|
+
* Every route that authors amendments calls this, so whether a document carries
|
|
123
|
+
* tamper-evidence does not depend on which one wrote it. The checksum is the
|
|
124
|
+
* canonical JSON form shared with the Go converters and with `hdf amend verify`,
|
|
125
|
+
* so a chain written here verifies everywhere.
|
|
126
|
+
*
|
|
127
|
+
* The override is put through serializeHdf first, so what gets hashed is the
|
|
128
|
+
* shape the document will actually carry. The TS converters hold
|
|
129
|
+
* `appliedAt`/`expiresAt` as `Date` objects, which stringify with a `.000`
|
|
130
|
+
* fraction that trimmed-UTC HDF output never carries; hashing them raw would
|
|
131
|
+
* make every TS-written chain disagree with its own serialized document, and
|
|
132
|
+
* with Go.
|
|
133
|
+
*
|
|
134
|
+
* Mutates the array in place, mirroring the Go helper.
|
|
135
|
+
*/
|
|
136
|
+
async function chainOverrides(overrides) {
|
|
137
|
+
let previous;
|
|
138
|
+
for (const override of overrides) {
|
|
139
|
+
if (previous === void 0) delete override.previousChecksum;
|
|
140
|
+
else override.previousChecksum = previous;
|
|
141
|
+
previous = {
|
|
142
|
+
algorithm: HashAlgorithm.Sha256,
|
|
143
|
+
value: await sha256(canonicalJson(JSON.parse(serializeHdf(override, 0))))
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
6
148
|
//#region shared/typescript/converterutil.ts
|
|
7
149
|
/**
|
|
8
150
|
* Shared utilities for TypeScript HDF converters.
|
|
@@ -161,6 +303,20 @@ function extractCWEIDs(text) {
|
|
|
161
303
|
ids.sort();
|
|
162
304
|
return ids;
|
|
163
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Return the first candidate whose content is not empty or whitespace-only,
|
|
308
|
+
* as-is (the emptiness test trims, the returned value does not). Returns '' when
|
|
309
|
+
* every candidate is empty or whitespace. The caller supplies its own documented
|
|
310
|
+
* fallback as the last candidate so the substituted value is meaningful per
|
|
311
|
+
* field — this helper never invents a placeholder like "N/A". Use it where a
|
|
312
|
+
* target schema requires a non-empty (often minLength:1) string and a source
|
|
313
|
+
* value may be absent; it is a selection helper, not a normalizer, and never
|
|
314
|
+
* decides to OMIT a field (that stays the caller's call). Go parity: FirstNonEmpty.
|
|
315
|
+
*/
|
|
316
|
+
function firstNonEmpty(...candidates) {
|
|
317
|
+
for (const c of candidates) if (c != null && c.trim() !== "") return c;
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
164
320
|
/** Default maximum input size for converters (50MB) */
|
|
165
321
|
const DEFAULT_MAX_INPUT_SIZE = 50 * 1024 * 1024;
|
|
166
322
|
/**
|
|
@@ -202,6 +358,15 @@ function requireHdfResults(input, converterName) {
|
|
|
202
358
|
return requireHdfStructure(input, converterName, "baselines");
|
|
203
359
|
}
|
|
204
360
|
/**
|
|
361
|
+
* The HDF Amendments counterpart of requireHdfResults, keyed on the top-level
|
|
362
|
+
* `overrides` array instead of `baselines`.
|
|
363
|
+
*/
|
|
364
|
+
function requireHdfAmendments(input, converterName) {
|
|
365
|
+
const r = requireHdfStructure(input, converterName, "overrides");
|
|
366
|
+
if (r.items.length === 0) throw new Error(`${converterName}: invalid HDF structure: missing overrides field`);
|
|
367
|
+
return r;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
205
370
|
* Reject malformed HDF input (empty, oversized, non-JSON, top-level array/null,
|
|
206
371
|
* or a missing/wrong-typed structural key) before a converter zero-fills it into
|
|
207
372
|
* a typed document. A parsed JSON `null` (or array, or primitive) is NOT an HDF
|
|
@@ -216,6 +381,10 @@ function requireHdfStructure(input, converterName, field) {
|
|
|
216
381
|
const doc = parsed;
|
|
217
382
|
const items = doc[field];
|
|
218
383
|
if (!Array.isArray(items)) throw new Error(`${converterName}: invalid HDF structure: missing ${field} field`);
|
|
384
|
+
if (field === "baselines") {
|
|
385
|
+
const violations = typeViolations(validateResults(doc));
|
|
386
|
+
if (violations.length > 0) throw new Error(`${converterName}: input is not valid HDF: ${violations.slice(0, 5).join("; ")}`);
|
|
387
|
+
}
|
|
219
388
|
return {
|
|
220
389
|
doc,
|
|
221
390
|
items
|
|
@@ -511,6 +680,24 @@ const UNRATED_SEVERITY_VALUE = "unrated";
|
|
|
511
680
|
function markUnratedSeverity(tags, severity) {
|
|
512
681
|
if (isUnratedSeverity(severity)) tags[UNRATED_SEVERITY_TAG] = UNRATED_SEVERITY_VALUE;
|
|
513
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* The schema violations Go's typed decode would also reject.
|
|
685
|
+
*
|
|
686
|
+
* encoding/json refuses exactly two things: a JSON value whose type does not fit
|
|
687
|
+
* the target field, and a string that will not parse into a time.Time. Every
|
|
688
|
+
* other schema rule — required, enum, minItems, uuid and uri formats — decodes
|
|
689
|
+
* cleanly into the generated structs, so rejecting it here would put the two
|
|
690
|
+
* languages back out of step in the opposite direction.
|
|
691
|
+
*
|
|
692
|
+
* null is exempt for the same reason. Go unmarshals a JSON null into a slice,
|
|
693
|
+
* map or pointer as nil and into anything else as a no-op, never as an error, so
|
|
694
|
+
* a null cvss entry or a null results array reaches the converter there and must
|
|
695
|
+
* reach it here too — several parity goldens pin exactly that.
|
|
696
|
+
*/
|
|
697
|
+
function typeViolations(result) {
|
|
698
|
+
if (result.valid) return [];
|
|
699
|
+
return (result.errors ?? []).filter((e) => e.value !== null).filter((e) => e.keyword === "type" || e.keyword === "format" && e.format === "date-time").map((e) => `${e.field ?? ""} ${e.message ?? ""}`.trim());
|
|
700
|
+
}
|
|
514
701
|
//#endregion
|
|
515
702
|
//#region converters/legacyhdf-to-hdf/typescript/converter.ts
|
|
516
703
|
/**
|
|
@@ -1725,11 +1912,10 @@ function buildTags$4(result, rule, resolvedLevel, cweIds, nistControls, cciContr
|
|
|
1725
1912
|
function extractSourceLocation$1(location) {
|
|
1726
1913
|
const uri = location.physicalLocation?.artifactLocation?.uri;
|
|
1727
1914
|
const line = location.physicalLocation?.region?.startLine;
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
};
|
|
1915
|
+
const sourceLocation = {};
|
|
1916
|
+
if (uri) sourceLocation.ref = uri;
|
|
1917
|
+
if (line) sourceLocation.line = line;
|
|
1918
|
+
return sourceLocation.ref !== void 0 || sourceLocation.line !== void 0 ? sourceLocation : void 0;
|
|
1733
1919
|
}
|
|
1734
1920
|
function createHDFResult(location, status, backtrace, suppressionMessage) {
|
|
1735
1921
|
const uri = location.physicalLocation?.artifactLocation?.uri || "";
|
|
@@ -1790,7 +1976,7 @@ function hostComponents(suites) {
|
|
|
1790
1976
|
const hosts = [];
|
|
1791
1977
|
const seen = /* @__PURE__ */ new Set();
|
|
1792
1978
|
for (const suite of suites) {
|
|
1793
|
-
const hostname = suite
|
|
1979
|
+
const hostname = suite["@_hostname"]?.trim();
|
|
1794
1980
|
if (!hostname || seen.has(hostname)) continue;
|
|
1795
1981
|
seen.add(hostname);
|
|
1796
1982
|
hosts.push({
|
|
@@ -1802,22 +1988,31 @@ function hostComponents(suites) {
|
|
|
1802
1988
|
return hosts;
|
|
1803
1989
|
}
|
|
1804
1990
|
function resolveScanTime(suites) {
|
|
1805
|
-
for (const suite of suites) if (suite
|
|
1806
|
-
const parsed = parseTimestamp(suite
|
|
1991
|
+
for (const suite of suites) if (suite["@_timestamp"]) {
|
|
1992
|
+
const parsed = parseTimestamp(suite["@_timestamp"]);
|
|
1807
1993
|
if (parsed) return parsed;
|
|
1808
1994
|
}
|
|
1809
1995
|
return /* @__PURE__ */ new Date();
|
|
1810
1996
|
}
|
|
1811
1997
|
function parseJUnitXML(input) {
|
|
1812
|
-
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
|
|
1813
|
-
if (parsed.testsuites)
|
|
1814
|
-
suites
|
|
1815
|
-
name
|
|
1816
|
-
|
|
1998
|
+
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2, { attributeNamePrefix: "@_" });
|
|
1999
|
+
if (parsed.testsuites) {
|
|
2000
|
+
const suites = [...parsed.testsuites.testsuite ?? []];
|
|
2001
|
+
const name = parsed.testsuites["@_name"] ? decodeXmlEntities$6(parsed.testsuites["@_name"]) : "JUnit Test Results";
|
|
2002
|
+
const looseCases = parsed.testsuites.testcase ?? [];
|
|
2003
|
+
if (looseCases.length > 0) suites.push({
|
|
2004
|
+
"@_name": name,
|
|
2005
|
+
testcase: looseCases
|
|
2006
|
+
});
|
|
2007
|
+
return {
|
|
2008
|
+
suites,
|
|
2009
|
+
name
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
1817
2012
|
if (parsed.testsuite) {
|
|
1818
2013
|
const suite = parsed.testsuite;
|
|
1819
2014
|
const suites = Array.isArray(suite) ? suite : [suite];
|
|
1820
|
-
const suiteName = suites[0]?.
|
|
2015
|
+
const suiteName = suites[0]?.["@_name"];
|
|
1821
2016
|
return {
|
|
1822
2017
|
suites,
|
|
1823
2018
|
name: suiteName ? decodeXmlEntities$6(suiteName) : "JUnit Test Results"
|
|
@@ -1848,14 +2043,14 @@ function testCaseToRequirement(tc, scanTime) {
|
|
|
1848
2043
|
startTime: scanTime
|
|
1849
2044
|
};
|
|
1850
2045
|
if (message !== void 0) result.message = message;
|
|
1851
|
-
if (tc
|
|
1852
|
-
const parsed = parseFloat(tc
|
|
2046
|
+
if (tc["@_time"]) {
|
|
2047
|
+
const parsed = parseFloat(tc["@_time"]);
|
|
1853
2048
|
if (!isNaN(parsed)) result.runTime = parsed;
|
|
1854
2049
|
}
|
|
1855
|
-
const name = decodeXmlEntities$6(tc
|
|
2050
|
+
const name = decodeXmlEntities$6(tc["@_name"]);
|
|
1856
2051
|
const descriptions = [{
|
|
1857
2052
|
label: "default",
|
|
1858
|
-
data: `JUnit test: ${name} in ${tc
|
|
2053
|
+
data: `JUnit test: ${name} in ${tc["@_classname"] ? decodeXmlEntities$6(tc["@_classname"]) : "unknown"}`
|
|
1859
2054
|
}];
|
|
1860
2055
|
const { systemOut, systemErr } = collectSystemStreams(tc);
|
|
1861
2056
|
if (systemOut) descriptions.push({
|
|
@@ -1894,19 +2089,19 @@ function collectSystemStreams(tc) {
|
|
|
1894
2089
|
};
|
|
1895
2090
|
}
|
|
1896
2091
|
function buildID(tc) {
|
|
1897
|
-
if (tc
|
|
1898
|
-
return decodeXmlEntities$6(tc
|
|
2092
|
+
if (tc["@_classname"]) return `${decodeXmlEntities$6(tc["@_classname"])}.${decodeXmlEntities$6(tc["@_name"])}`;
|
|
2093
|
+
return decodeXmlEntities$6(tc["@_name"]);
|
|
1899
2094
|
}
|
|
1900
2095
|
function resolveStatus(tc) {
|
|
1901
2096
|
if (tc.failure) {
|
|
1902
|
-
const msg = buildFailureMessage(tc.failure
|
|
2097
|
+
const msg = buildFailureMessage(tc.failure["@_message"] ?? "", tc.failure["@_type"] ?? "", tc.failure["#text"] ?? "");
|
|
1903
2098
|
return {
|
|
1904
2099
|
status: ResultStatus.Failed,
|
|
1905
2100
|
message: msg
|
|
1906
2101
|
};
|
|
1907
2102
|
}
|
|
1908
2103
|
if (tc.error) {
|
|
1909
|
-
const msg = buildFailureMessage(tc.error
|
|
2104
|
+
const msg = buildFailureMessage(tc.error["@_message"] ?? "", tc.error["@_type"] ?? "", tc.error["#text"] ?? "");
|
|
1910
2105
|
return {
|
|
1911
2106
|
status: ResultStatus.Error,
|
|
1912
2107
|
message: msg
|
|
@@ -1914,9 +2109,9 @@ function resolveStatus(tc) {
|
|
|
1914
2109
|
}
|
|
1915
2110
|
if (tc.skipped !== void 0) {
|
|
1916
2111
|
const skipped = typeof tc.skipped === "object" ? tc.skipped : null;
|
|
1917
|
-
if (skipped?.
|
|
2112
|
+
if (skipped?.["@_message"]) return {
|
|
1918
2113
|
status: ResultStatus.NotReviewed,
|
|
1919
|
-
message: `Skipped: ${decodeXmlEntities$6(skipped
|
|
2114
|
+
message: `Skipped: ${decodeXmlEntities$6(skipped["@_message"])}`
|
|
1920
2115
|
};
|
|
1921
2116
|
return {
|
|
1922
2117
|
status: ResultStatus.NotReviewed,
|
|
@@ -1933,12 +2128,12 @@ function buildFailureMessage(message, typeName, body) {
|
|
|
1933
2128
|
return result;
|
|
1934
2129
|
}
|
|
1935
2130
|
function buildCodeDesc$10(tc) {
|
|
1936
|
-
if (tc
|
|
1937
|
-
return decodeXmlEntities$6(tc
|
|
2131
|
+
if (tc["@_classname"]) return `${decodeXmlEntities$6(tc["@_classname"])} :: ${decodeXmlEntities$6(tc["@_name"])}`;
|
|
2132
|
+
return decodeXmlEntities$6(tc["@_name"]);
|
|
1938
2133
|
}
|
|
1939
2134
|
function noFindingsTarget(baselineName, suites) {
|
|
1940
2135
|
if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
|
|
1941
|
-
for (const s of suites) if (s
|
|
2136
|
+
for (const s of suites) if (s["@_name"]) return decodeXmlEntities$6(s["@_name"]);
|
|
1942
2137
|
return "JUnit test suite";
|
|
1943
2138
|
}
|
|
1944
2139
|
//#endregion
|
|
@@ -3105,9 +3300,9 @@ function stamp(v) {
|
|
|
3105
3300
|
/** Maps a requirement onto the canonical effective-status input shape. */
|
|
3106
3301
|
function requirementStatusInput(req) {
|
|
3107
3302
|
return {
|
|
3108
|
-
impact: req.impact,
|
|
3109
|
-
resultStatuses: (req.results ?? []).map((r) => String(r.status)),
|
|
3110
|
-
overrides: (req.statusOverrides ?? []).map((o) => ({
|
|
3303
|
+
impact: req.impact ?? 0,
|
|
3304
|
+
resultStatuses: (req.results ?? []).filter((r) => r != null).map((r) => String(r.status)),
|
|
3305
|
+
overrides: (req.statusOverrides ?? []).filter((o) => o != null).map((o) => ({
|
|
3111
3306
|
status: o.status ? String(o.status) : void 0,
|
|
3112
3307
|
appliedAt: stamp(o.appliedAt),
|
|
3113
3308
|
expiresAt: stamp(o.expiresAt)
|
|
@@ -7413,10 +7608,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
|
7413
7608
|
* @returns CSV string with sanitized output
|
|
7414
7609
|
*/
|
|
7415
7610
|
function convertHdfToCsv(input) {
|
|
7416
|
-
|
|
7417
|
-
const hdf =
|
|
7418
|
-
if (!hdf || typeof hdf !== "object" || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
7419
|
-
if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
7611
|
+
const { doc: hdfDoc } = requireHdfResults(input, "hdf-to-csv");
|
|
7612
|
+
const hdf = hdfDoc;
|
|
7420
7613
|
const rows = [];
|
|
7421
7614
|
const components = hdf.components || [];
|
|
7422
7615
|
const targetList = components.length > 0 ? components.map((t) => ({
|
|
@@ -7430,35 +7623,64 @@ function convertHdfToCsv(input) {
|
|
|
7430
7623
|
fqdn: "",
|
|
7431
7624
|
ipAddress: ""
|
|
7432
7625
|
}];
|
|
7433
|
-
for (const baseline of hdf.baselines)
|
|
7626
|
+
for (const baseline of hdf.baselines) {
|
|
7627
|
+
if (!Array.isArray(baseline.requirements) || baseline.requirements.length === 0) throw new Error(`hdf-to-csv: baseline "${text(baseline.name)}" has no requirements`);
|
|
7628
|
+
for (const requirement of baseline.requirements) for (const target of targetList) rows.push(createRow(baseline, requirement, target));
|
|
7629
|
+
}
|
|
7434
7630
|
return buildCsv(rows, { sanitize: true });
|
|
7435
7631
|
}
|
|
7436
7632
|
/**
|
|
7633
|
+
* A schema-required number as a number. Go decodes an absent one to the zero
|
|
7634
|
+
* value and cannot tell it apart from a real zero — impact 0 legitimately means
|
|
7635
|
+
* Not Applicable — so distinguishing them would need a schema validation pass
|
|
7636
|
+
* the converters deliberately do not do. Rendering 0 here matches what Go emits,
|
|
7637
|
+
* where reading .toFixed off an absent value threw a raw TypeError instead.
|
|
7638
|
+
*/
|
|
7639
|
+
function numeric(value) {
|
|
7640
|
+
return typeof value === "number" ? value : 0;
|
|
7641
|
+
}
|
|
7642
|
+
/**
|
|
7643
|
+
* A schema-required string as text. Nested-invalid input reaches the converter
|
|
7644
|
+
* with these absent, and an absent value reached sanitizeCsvValue's String() as
|
|
7645
|
+
* the literal "undefined" — where Go, formatting a zero-value string, wrote
|
|
7646
|
+
* nothing. Every schema-required STRING cell goes through here; the numeric
|
|
7647
|
+
* ones go through numeric(), which serves the same purpose for a different type.
|
|
7648
|
+
*/
|
|
7649
|
+
function text(value) {
|
|
7650
|
+
return typeof value === "string" ? value : "";
|
|
7651
|
+
}
|
|
7652
|
+
/** The requirement id as text; also names the requirement in the no-results error. */
|
|
7653
|
+
function requirementIdText$1(id) {
|
|
7654
|
+
return text(id);
|
|
7655
|
+
}
|
|
7656
|
+
/**
|
|
7437
7657
|
* Create a CSV row from baseline, requirement, and target data
|
|
7438
7658
|
*/
|
|
7439
7659
|
function createRow(baseline, requirement, target) {
|
|
7440
|
-
const
|
|
7660
|
+
const [firstResult] = Array.isArray(requirement.results) ? requirement.results : [];
|
|
7661
|
+
if (!firstResult) throw new Error(`hdf-to-csv: requirement "${requirementIdText$1(requirement.id)}" has no results`);
|
|
7662
|
+
if (!Array.isArray(requirement.descriptions) || requirement.descriptions.length === 0) throw new Error(`hdf-to-csv: requirement "${requirementIdText$1(requirement.id)}" has no descriptions`);
|
|
7663
|
+
const description = descriptionByLabel(requirement, "default");
|
|
7441
7664
|
const check = descriptionByLabel(requirement, "check");
|
|
7442
7665
|
const fix = descriptionByLabel(requirement, "fix");
|
|
7443
7666
|
const rationale = descriptionByLabel(requirement, "rationale");
|
|
7444
7667
|
const code = requirement.code ?? "";
|
|
7445
7668
|
const references = flattenRefs(requirement.refs);
|
|
7446
7669
|
const severity = getSeverity(requirement);
|
|
7447
|
-
const
|
|
7448
|
-
const
|
|
7449
|
-
const message = firstResult?.message || "";
|
|
7670
|
+
const status = firstResult.status;
|
|
7671
|
+
const message = firstResult.message ?? "";
|
|
7450
7672
|
const nistControls = extractArrayFromTags(requirement.tags, "nist");
|
|
7451
7673
|
const cciControls = extractArrayFromTags(requirement.tags, "cci");
|
|
7452
7674
|
const effectiveStatus = requirementEffectiveStatus(requirement);
|
|
7453
|
-
const effectiveImpact = (requirement.effectiveImpact ?? requirement.impact).toFixed(
|
|
7675
|
+
const effectiveImpact = numeric(requirement.effectiveImpact ?? requirement.impact).toFixed(2);
|
|
7454
7676
|
const disposition = requirement.disposition ? String(requirement.disposition) : "";
|
|
7455
7677
|
return {
|
|
7456
|
-
"Baseline ID": baseline.name,
|
|
7678
|
+
"Baseline ID": text(baseline.name),
|
|
7457
7679
|
"Baseline Version": baseline.version || "",
|
|
7458
7680
|
"Baseline Title": baseline.title || "",
|
|
7459
|
-
"Target ID": target.name,
|
|
7460
|
-
"Target Type": target.type,
|
|
7461
|
-
"Requirement ID": requirement.id,
|
|
7681
|
+
"Target ID": text(target.name),
|
|
7682
|
+
"Target Type": text(target.type),
|
|
7683
|
+
"Requirement ID": requirementIdText$1(requirement.id),
|
|
7462
7684
|
"Requirement Title": requirement.title || "",
|
|
7463
7685
|
"Description": description,
|
|
7464
7686
|
"Check": check,
|
|
@@ -7467,8 +7689,8 @@ function createRow(baseline, requirement, target) {
|
|
|
7467
7689
|
"Code": code,
|
|
7468
7690
|
"References": references,
|
|
7469
7691
|
"Severity": severity,
|
|
7470
|
-
"Impact": requirement.impact.toFixed(
|
|
7471
|
-
"Status":
|
|
7692
|
+
"Impact": numeric(requirement.impact).toFixed(2),
|
|
7693
|
+
"Status": text(status),
|
|
7472
7694
|
"NIST Controls": nistControls,
|
|
7473
7695
|
"CCI Controls": cciControls,
|
|
7474
7696
|
"Control Type": requirement.controlType ?? "",
|
|
@@ -7482,8 +7704,8 @@ function createRow(baseline, requirement, target) {
|
|
|
7482
7704
|
"Applied By": joinOverrides(requirement.statusOverrides, (o) => o.appliedBy?.identifier ?? ""),
|
|
7483
7705
|
"Expires At": joinOverrides(requirement.statusOverrides, (o) => formatExpires(o.expiresAt)),
|
|
7484
7706
|
"CVSS": cvssScores(requirement.cvss),
|
|
7485
|
-
"CWE":
|
|
7486
|
-
"EPSS": requirement.epss ? requirement.epss.score.toFixed(5) : "",
|
|
7707
|
+
"CWE": (requirement.cwe ?? []).join("; "),
|
|
7708
|
+
"EPSS": requirement.epss ? numeric(requirement.epss.score).toFixed(5) : "",
|
|
7487
7709
|
"KEV": requirement.kev ? requirement.kev.inKev ? "true" : "false" : "",
|
|
7488
7710
|
"Target FQDN": target.fqdn,
|
|
7489
7711
|
"Target IP": target.ipAddress
|
|
@@ -7497,6 +7719,7 @@ function joinOverrides(overrides, pick) {
|
|
|
7497
7719
|
if (!Array.isArray(overrides)) return "";
|
|
7498
7720
|
const out = [];
|
|
7499
7721
|
for (const o of overrides) {
|
|
7722
|
+
if (o === null || o === void 0) continue;
|
|
7500
7723
|
const v = pick(o);
|
|
7501
7724
|
if (v) out.push(v);
|
|
7502
7725
|
}
|
|
@@ -7512,14 +7735,17 @@ function formatExpires(value) {
|
|
|
7512
7735
|
}
|
|
7513
7736
|
/**
|
|
7514
7737
|
* Render each CVSS entry's score (computed when present, else base) to one
|
|
7515
|
-
* decimal
|
|
7738
|
+
* decimal — the precision the CVSS spec defines and the source carries — joined
|
|
7739
|
+
* with '; ' to preserve multi-CVE findings. The Go peer rounds through a shared
|
|
7740
|
+
* helper matching toFixed, so a .x5 score renders the same digits in both.
|
|
7516
7741
|
*/
|
|
7517
7742
|
function cvssScores(entries) {
|
|
7518
7743
|
if (!Array.isArray(entries)) return "";
|
|
7519
7744
|
const out = [];
|
|
7520
7745
|
for (const c of entries) {
|
|
7746
|
+
if (c === null || c === void 0) continue;
|
|
7521
7747
|
const score = c.computedScore ?? c.baseScore;
|
|
7522
|
-
if (score
|
|
7748
|
+
if (typeof score === "number") out.push(score.toFixed(1));
|
|
7523
7749
|
}
|
|
7524
7750
|
return out.join("; ");
|
|
7525
7751
|
}
|
|
@@ -7531,7 +7757,7 @@ function getSeverity(requirement) {
|
|
|
7531
7757
|
if ("severity" in requirement.tags) {
|
|
7532
7758
|
const sev = requirement.tags.severity;
|
|
7533
7759
|
if (typeof sev === "string") return sev;
|
|
7534
|
-
if (Array.isArray(sev) && sev
|
|
7760
|
+
if (Array.isArray(sev) && typeof sev[0] === "string") return sev[0];
|
|
7535
7761
|
}
|
|
7536
7762
|
}
|
|
7537
7763
|
const impact = requirement.impact;
|
|
@@ -7545,18 +7771,20 @@ function getSeverity(requirement) {
|
|
|
7545
7771
|
function extractArrayFromTags(tags, key) {
|
|
7546
7772
|
if (!tags || typeof tags !== "object") return "";
|
|
7547
7773
|
const value = tags[key];
|
|
7548
|
-
if (Array.isArray(value)) return value.
|
|
7774
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string").join("; ");
|
|
7549
7775
|
return "";
|
|
7550
7776
|
}
|
|
7551
7777
|
/**
|
|
7552
7778
|
* Get the data of the first description matching a label. Empty when absent.
|
|
7553
7779
|
*/
|
|
7554
7780
|
function descriptionByLabel(requirement, label) {
|
|
7555
|
-
return requirement.descriptions.find((d) => d.label === label)?.data || "";
|
|
7781
|
+
return requirement.descriptions.find((d) => d !== null && d !== void 0 && d.label === label)?.data || "";
|
|
7556
7782
|
}
|
|
7557
7783
|
/**
|
|
7558
7784
|
* Flatten a requirement's refs to one string: each Reference rendered as its
|
|
7559
|
-
* url/uri (or a string `ref`); array-form refs are skipped
|
|
7785
|
+
* url/uri (or a string `ref`); array-form refs are skipped here, though a document
|
|
7786
|
+
* containing one never reaches this function in Go, whose typed decode rejects
|
|
7787
|
+
* it outright — a reject-versus-degrade split tracked on its own card. Joined with '; ' to
|
|
7560
7788
|
* match the NIST/CCI column convention.
|
|
7561
7789
|
*/
|
|
7562
7790
|
function flattenRefs(refs) {
|
|
@@ -8655,9 +8883,20 @@ function convertStatistics(stats) {
|
|
|
8655
8883
|
* The document is walked as a plain JSON tree and every key is emitted in
|
|
8656
8884
|
* source-JSON order, so the output can never silently lag a schema addition the
|
|
8657
8885
|
* way the previous hand-maintained struct mirror did (it dropped ~30 post-v3.2
|
|
8658
|
-
* fields).
|
|
8659
|
-
*
|
|
8660
|
-
*
|
|
8886
|
+
* fields). The Go converter walks the same normalized JSON in the same order, so
|
|
8887
|
+
* the two languages emit output that is identical after the shared XML golden
|
|
8888
|
+
* normalization for every shape this repo's fixtures and parity tests cover.
|
|
8889
|
+
*
|
|
8890
|
+
* Equality after that normalization is NOT guaranteed in general, and the reason
|
|
8891
|
+
* is structural rather than a fixed list of cases: JSON.parse loses duplicate
|
|
8892
|
+
* keys and hoists
|
|
8893
|
+
* array-index keys before this builder ever runs, V8's number-to-string forms
|
|
8894
|
+
* differ from Go's strconv at the extremes, and Go's encoding/xml sanitizes every
|
|
8895
|
+
* XML-illegal rune to U+FFFD where this builder emits it verbatim. This builder
|
|
8896
|
+
* also caps nesting depth, which the Go peer does not. Anything landing in those
|
|
8897
|
+
* seams can differ; known instances are tracked as cards under the
|
|
8898
|
+
* exporter-conformance epic. Assume a shape outside the fixture corpus needs
|
|
8899
|
+
* checking against the peer rather than that it is covered.
|
|
8661
8900
|
*/
|
|
8662
8901
|
/**
|
|
8663
8902
|
* Maps a container (plural) key to the element name emitted for each of its
|
|
@@ -8688,13 +8927,33 @@ function singularFor(key) {
|
|
|
8688
8927
|
return PLURAL_TO_SINGULAR[key] ?? "item";
|
|
8689
8928
|
}
|
|
8690
8929
|
/**
|
|
8691
|
-
*
|
|
8692
|
-
*
|
|
8930
|
+
* Encode a JSON key to a legal XML element name, reporting whether it had to
|
|
8931
|
+
* change. HDF leaves object keys unconstrained — tags carry vendor-namespaced
|
|
8932
|
+
* keys like "sonarqube/hash", and additionalProperties lets a converter park
|
|
8933
|
+
* native fields anywhere — while XML constrains element names to Name, so an
|
|
8934
|
+
* unencoded key produced a document no parser could read.
|
|
8935
|
+
*
|
|
8936
|
+
* The kept set is ASCII — [A-Za-z0-9._-] — not the far wider set XML Name
|
|
8937
|
+
* allows, so the two languages agree by construction rather than by way of two
|
|
8938
|
+
* Unicode tables that can drift apart. It costs nothing here: no tag key in this
|
|
8939
|
+
* package's converter fixtures is non-ASCII. ':' is deliberately not kept —
|
|
8940
|
+
* it is a legal Name character but would invent an undeclared namespace prefix.
|
|
8941
|
+
*
|
|
8942
|
+
* Mirrored by xmlElementName in the Go converter and pinned to a shared table.
|
|
8693
8943
|
*/
|
|
8694
|
-
function
|
|
8695
|
-
|
|
8944
|
+
function xmlElementName(key) {
|
|
8945
|
+
const out = [...key].map((ch) => /[A-Za-z0-9.\-_]/.test(ch) ? ch : "_").join("");
|
|
8946
|
+
const [first = ""] = [...out];
|
|
8947
|
+
const name = /[A-Za-z_]/.test(first) ? out : `_${out}`;
|
|
8948
|
+
return [name, name !== key];
|
|
8696
8949
|
}
|
|
8697
8950
|
/**
|
|
8951
|
+
* The attribute holding the original key of a rewritten element. The prefix is
|
|
8952
|
+
* what separates an attribute from a child element in the builder; it cannot
|
|
8953
|
+
* collide with a child, because any key spelled this way is itself rewritten.
|
|
8954
|
+
*/
|
|
8955
|
+
const NAME_ATTR = "@_name";
|
|
8956
|
+
/**
|
|
8698
8957
|
* True for a JSON scalar (string, number, bool) or a null/undefined placeholder.
|
|
8699
8958
|
* Nested objects and arrays are not scalar, which selects the wrapped
|
|
8700
8959
|
* object-array rendering.
|
|
@@ -8702,37 +8961,48 @@ function wrapScalar(value) {
|
|
|
8702
8961
|
function isScalar(value) {
|
|
8703
8962
|
return value === null || value === void 0 || typeof value !== "object";
|
|
8704
8963
|
}
|
|
8964
|
+
function present(value) {
|
|
8965
|
+
return value !== null && value !== void 0;
|
|
8966
|
+
}
|
|
8705
8967
|
/**
|
|
8706
|
-
*
|
|
8707
|
-
*
|
|
8708
|
-
*
|
|
8968
|
+
* Render one JSON key as the sibling nodes it becomes, mirroring the Go
|
|
8969
|
+
* writeValue case for case. A list rather than a single node because a scalar
|
|
8970
|
+
* array repeats its key unwrapped, and because two keys can encode to one
|
|
8971
|
+
* element name — preserveOrder keeps each at its own position, which a plain
|
|
8972
|
+
* object keyed by element name could not.
|
|
8709
8973
|
*/
|
|
8710
|
-
function
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
if (
|
|
8714
|
-
|
|
8974
|
+
function nodesFor(key, value) {
|
|
8975
|
+
const [name, rewritten] = xmlElementName(key);
|
|
8976
|
+
const attrs = rewritten ? { ":@": { [NAME_ATTR]: xmlSafeText(key) } } : {};
|
|
8977
|
+
if (Array.isArray(value)) {
|
|
8978
|
+
if (value.length === 0) return [{
|
|
8979
|
+
[name]: [],
|
|
8980
|
+
...attrs
|
|
8981
|
+
}];
|
|
8982
|
+
if (value.every(isScalar)) return value.filter(present).flatMap((item) => nodesFor(key, item));
|
|
8983
|
+
const child = singularFor(key);
|
|
8984
|
+
return [{
|
|
8985
|
+
[name]: value.filter(present).flatMap((item) => nodesFor(child, item)),
|
|
8986
|
+
...attrs
|
|
8987
|
+
}];
|
|
8988
|
+
}
|
|
8989
|
+
if (typeof value === "object") return [{
|
|
8990
|
+
[name]: buildNodes(value),
|
|
8991
|
+
...attrs
|
|
8992
|
+
}];
|
|
8993
|
+
const text = typeof value === "string" ? xmlSafeText(value) : value;
|
|
8994
|
+
return [{
|
|
8995
|
+
[name]: [{ "#text": text }],
|
|
8996
|
+
...attrs
|
|
8997
|
+
}];
|
|
8715
8998
|
}
|
|
8716
8999
|
/**
|
|
8717
|
-
* Walk a JSON object into
|
|
8718
|
-
* source order. Null-valued keys are omitted
|
|
8719
|
-
* semantics
|
|
8720
|
-
* object array becomes a wrapper whose per-item child name comes from the
|
|
8721
|
-
* plural->singular map; an empty array becomes an empty wrapper element.
|
|
9000
|
+
* Walk a JSON object into fast-xml-parser preserveOrder nodes, emitting keys in
|
|
9001
|
+
* source order. Null-valued keys are omitted, matching the old omitempty
|
|
9002
|
+
* semantics and the Go peer.
|
|
8722
9003
|
*/
|
|
8723
|
-
function
|
|
8724
|
-
|
|
8725
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
8726
|
-
if (value === null || value === void 0) continue;
|
|
8727
|
-
if (Array.isArray(value)) {
|
|
8728
|
-
const items = value.filter((v) => v !== null && v !== void 0);
|
|
8729
|
-
if (value.length === 0) out[key] = {};
|
|
8730
|
-
else if (value.every(isScalar)) out[key] = items.map((v) => wrapScalar(v));
|
|
8731
|
-
else out[key] = { [singularFor(key)]: items.map(toXmlValue) };
|
|
8732
|
-
} else if (typeof value === "object") out[key] = buildObject(value);
|
|
8733
|
-
else out[key] = wrapScalar(value);
|
|
8734
|
-
}
|
|
8735
|
-
return out;
|
|
9004
|
+
function buildNodes(obj) {
|
|
9005
|
+
return Object.entries(obj).flatMap(([key, value]) => present(value) ? nodesFor(key, value) : []);
|
|
8736
9006
|
}
|
|
8737
9007
|
/**
|
|
8738
9008
|
* Convert HDF Results JSON to XML.
|
|
@@ -8745,7 +9015,10 @@ function convertHdfToXml(input) {
|
|
|
8745
9015
|
const hdf = parseHdf(input);
|
|
8746
9016
|
if (!hdf || typeof hdf !== "object" || Array.isArray(hdf) || !("baselines" in hdf)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
8747
9017
|
if (!Array.isArray(hdf.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
8748
|
-
return buildXml({ HdfResults:
|
|
9018
|
+
return `${buildXml([{ HdfResults: buildNodes(hdf) }], {
|
|
9019
|
+
preserveOrder: true,
|
|
9020
|
+
attributeNamePrefix: "@_"
|
|
9021
|
+
}).trim()}\n`;
|
|
8749
9022
|
}
|
|
8750
9023
|
//#endregion
|
|
8751
9024
|
//#region converters/gitlab-to-hdf/typescript/converter.ts
|
|
@@ -8970,18 +9243,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
8970
9243
|
const BASELINE_NAME = "Trivy Scan";
|
|
8971
9244
|
const STATIC_CCI = nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
8972
9245
|
const CWE_ID_PATTERN$1 = /^CWE-[1-9]\d*$/;
|
|
8973
|
-
const
|
|
8974
|
-
critical: .9,
|
|
8975
|
-
high: .7,
|
|
8976
|
-
medium: .5,
|
|
8977
|
-
low: .3,
|
|
8978
|
-
unknown: .5,
|
|
8979
|
-
none: 0,
|
|
8980
|
-
info: 0
|
|
8981
|
-
};
|
|
8982
|
-
function severityToImpact$2(severity) {
|
|
8983
|
-
return IMPACT_MAP[(severity || "").toLowerCase()] ?? .5;
|
|
8984
|
-
}
|
|
9246
|
+
const SEVERITY_ALIASES = { critical: .9 };
|
|
8985
9247
|
/**
|
|
8986
9248
|
* Route Trivy output to HDF Results: native Trivy JSON (SchemaVersion 2) is
|
|
8987
9249
|
* parsed here; SARIF / CycloneDX / ASFF / GitLab are delegated to their
|
|
@@ -9031,7 +9293,7 @@ async function convertNative(input, converterVersion) {
|
|
|
9031
9293
|
function convertVuln(v, res, startTime) {
|
|
9032
9294
|
const descriptions = [{
|
|
9033
9295
|
label: "default",
|
|
9034
|
-
data: firstNonEmpty(v.Description, v.Title, v.VulnerabilityID)
|
|
9296
|
+
data: firstNonEmpty(v.Description ?? "", v.Title ?? "", v.VulnerabilityID ?? "")
|
|
9035
9297
|
}];
|
|
9036
9298
|
if (v.FixedVersion) descriptions.push({
|
|
9037
9299
|
label: "fix",
|
|
@@ -9049,7 +9311,7 @@ function convertVuln(v, res, startTime) {
|
|
|
9049
9311
|
id: `Trivy/${v.VulnerabilityID ?? ""}`,
|
|
9050
9312
|
title: `Trivy found ${v.VulnerabilityID ?? ""} in ${pkgLabel(v.PkgName, v.InstalledVersion)}`,
|
|
9051
9313
|
descriptions,
|
|
9052
|
-
impact:
|
|
9314
|
+
impact: severityToImpactWithAliases(v.Severity, SEVERITY_ALIASES, .5),
|
|
9053
9315
|
tags,
|
|
9054
9316
|
cwe: filterCwes(v.CweIDs),
|
|
9055
9317
|
cvss: buildCvssEntries$4(v.CVSS),
|
|
@@ -9061,7 +9323,7 @@ function convertVuln(v, res, startTime) {
|
|
|
9061
9323
|
status: ResultStatus.Failed,
|
|
9062
9324
|
codeDesc: buildVulnCodeDesc(v, res),
|
|
9063
9325
|
startTime,
|
|
9064
|
-
message: `Severity: ${firstNonEmpty(v.Severity, "UNKNOWN")}`
|
|
9326
|
+
message: `Severity: ${firstNonEmpty(v.Severity ?? "", "UNKNOWN")}`
|
|
9065
9327
|
}]
|
|
9066
9328
|
};
|
|
9067
9329
|
const ap = buildAffectedPackage$1(v);
|
|
@@ -9072,7 +9334,7 @@ function convertVuln(v, res, startTime) {
|
|
|
9072
9334
|
function convertMisconf(m, res, startTime) {
|
|
9073
9335
|
const descriptions = [{
|
|
9074
9336
|
label: "default",
|
|
9075
|
-
data: firstNonEmpty(m.Description, m.Message, m.Title)
|
|
9337
|
+
data: firstNonEmpty(m.Description ?? "", m.Message ?? "", m.Title ?? "")
|
|
9076
9338
|
}];
|
|
9077
9339
|
if (m.Resolution) descriptions.push({
|
|
9078
9340
|
label: "fix",
|
|
@@ -9084,10 +9346,10 @@ function convertMisconf(m, res, startTime) {
|
|
|
9084
9346
|
});
|
|
9085
9347
|
markUnratedSeverity(tags, m.Severity);
|
|
9086
9348
|
const req = {
|
|
9087
|
-
id: `Trivy/${firstNonEmpty(m.ID, m.AVDID)}`,
|
|
9349
|
+
id: `Trivy/${firstNonEmpty(m.ID ?? "", m.AVDID ?? "")}`,
|
|
9088
9350
|
title: m.Title,
|
|
9089
9351
|
descriptions,
|
|
9090
|
-
impact:
|
|
9352
|
+
impact: severityToImpactWithAliases(m.Severity, SEVERITY_ALIASES, .5),
|
|
9091
9353
|
tags,
|
|
9092
9354
|
refs: buildRefs$5([m.PrimaryURL, ...m.References ?? []]),
|
|
9093
9355
|
code: JSON.stringify(m, null, 2),
|
|
@@ -9095,7 +9357,7 @@ function convertMisconf(m, res, startTime) {
|
|
|
9095
9357
|
verificationMethod: VerificationMethodEnum.Automated,
|
|
9096
9358
|
results: [{
|
|
9097
9359
|
status: misconfStatus(m.Status),
|
|
9098
|
-
codeDesc: firstNonEmpty(m.Message, m.Title),
|
|
9360
|
+
codeDesc: firstNonEmpty(m.Message ?? "", m.Title ?? ""),
|
|
9099
9361
|
startTime
|
|
9100
9362
|
}]
|
|
9101
9363
|
};
|
|
@@ -9117,9 +9379,9 @@ function convertSecret(s, res, startTime) {
|
|
|
9117
9379
|
title: s.Title,
|
|
9118
9380
|
descriptions: [{
|
|
9119
9381
|
label: "default",
|
|
9120
|
-
data: `${firstNonEmpty(s.Title, s.RuleID)} detected in ${res.Target ?? ""} (value redacted by Trivy).`
|
|
9382
|
+
data: `${firstNonEmpty(s.Title ?? "", s.RuleID ?? "")} detected in ${res.Target ?? ""} (value redacted by Trivy).`
|
|
9121
9383
|
}],
|
|
9122
|
-
impact:
|
|
9384
|
+
impact: severityToImpactWithAliases(s.Severity, SEVERITY_ALIASES, .5),
|
|
9123
9385
|
tags,
|
|
9124
9386
|
code: JSON.stringify(s, null, 2),
|
|
9125
9387
|
controlType: deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS),
|
|
@@ -9151,7 +9413,7 @@ function convertLicense(l, res, startTime) {
|
|
|
9151
9413
|
label: "default",
|
|
9152
9414
|
data: `Package ${l.PkgName ?? ""} uses the ${l.Name ?? ""} license (category: ${l.Category ?? ""}).`
|
|
9153
9415
|
}],
|
|
9154
|
-
impact:
|
|
9416
|
+
impact: severityToImpactWithAliases(l.Severity, SEVERITY_ALIASES, .5),
|
|
9155
9417
|
tags,
|
|
9156
9418
|
refs: buildRefs$5([l.Link]),
|
|
9157
9419
|
code: JSON.stringify(l, null, 2),
|
|
@@ -9232,17 +9494,6 @@ function buildCvssEntries$4(m) {
|
|
|
9232
9494
|
}
|
|
9233
9495
|
return entries.length > 0 ? entries : void 0;
|
|
9234
9496
|
}
|
|
9235
|
-
const PURL_ECOSYSTEMS = {
|
|
9236
|
-
deb: Ecosystem.Deb,
|
|
9237
|
-
rpm: Ecosystem.RPM,
|
|
9238
|
-
maven: Ecosystem.Maven,
|
|
9239
|
-
npm: Ecosystem.Npm,
|
|
9240
|
-
pypi: Ecosystem.Pypi,
|
|
9241
|
-
gem: Ecosystem.Gem,
|
|
9242
|
-
cargo: Ecosystem.Cargo,
|
|
9243
|
-
golang: Ecosystem.Go,
|
|
9244
|
-
nuget: Ecosystem.Nuget
|
|
9245
|
-
};
|
|
9246
9497
|
function buildAffectedPackage$1(v) {
|
|
9247
9498
|
const ap = {};
|
|
9248
9499
|
if (v.PkgName) ap.name = v.PkgName;
|
|
@@ -9257,8 +9508,8 @@ function buildAffectedPackage$1(v) {
|
|
|
9257
9508
|
return ap;
|
|
9258
9509
|
}
|
|
9259
9510
|
function ecosystemFromPurl(purl) {
|
|
9260
|
-
const
|
|
9261
|
-
return
|
|
9511
|
+
const eco = ecosystemFromPurlType(purl.replace(/^pkg:/, "").split(/[/@?]/)[0] ?? "");
|
|
9512
|
+
return eco === Ecosystem.Generic ? void 0 : eco;
|
|
9262
9513
|
}
|
|
9263
9514
|
function buildRefs$5(urls) {
|
|
9264
9515
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -9285,10 +9536,6 @@ function digestPart(ref) {
|
|
|
9285
9536
|
const at = ref.lastIndexOf("@");
|
|
9286
9537
|
return at >= 0 ? ref.slice(at + 1) : ref;
|
|
9287
9538
|
}
|
|
9288
|
-
function firstNonEmpty(...vals) {
|
|
9289
|
-
for (const v of vals) if (v) return v;
|
|
9290
|
-
return "";
|
|
9291
|
-
}
|
|
9292
9539
|
function putIf(m, key, val) {
|
|
9293
9540
|
if (val) m[key] = val;
|
|
9294
9541
|
}
|
|
@@ -13441,7 +13688,44 @@ function findDescription(descriptions, label) {
|
|
|
13441
13688
|
}
|
|
13442
13689
|
/** Replace characters not valid in XCCDF IDs with underscores. */
|
|
13443
13690
|
function sanitizeXccdfId(id) {
|
|
13444
|
-
return id.replace(/[^a-zA-Z0-9_.\-]/
|
|
13691
|
+
return id.replace(/[^a-zA-Z0-9_.\-]/gu, "_");
|
|
13692
|
+
}
|
|
13693
|
+
/**
|
|
13694
|
+
* Supply the trailing name segment that benchmarkIdType and profileIdType
|
|
13695
|
+
* require via their .+ — a baseline with an empty name is valid HDF but
|
|
13696
|
+
* produced "xccdf_hdf_benchmark_", which the XSD rejects.
|
|
13697
|
+
*/
|
|
13698
|
+
function xccdfNamePart(name) {
|
|
13699
|
+
return name === "" ? "unnamed" : name;
|
|
13700
|
+
}
|
|
13701
|
+
/**
|
|
13702
|
+
* Render an HDF tags.gid as a groupIdType: an NCName that must also match
|
|
13703
|
+
* xccdf_[^_]+_group_.+ (xccdf_1.2.xsd:821). HDF constrains gid not at all, and
|
|
13704
|
+
* most real data does not satisfy it — 783 of the 1216 distinct gid values in
|
|
13705
|
+
* this package's converter fixtures are bare STIG ids like "V-257777" — so copying one
|
|
13706
|
+
* through produced XSD-invalid output from valid HDF.
|
|
13707
|
+
*
|
|
13708
|
+
* A gid that already conforms is a genuine XCCDF group id (STIG content carries
|
|
13709
|
+
* "xccdf_mil.disa.stig_group_V-204393") and is passed through; anything else is
|
|
13710
|
+
* sanitized and namespaced under this converter, matching what the benchmark,
|
|
13711
|
+
* profile, and rule ids already do.
|
|
13712
|
+
*
|
|
13713
|
+
* Mirrored by xccdfGroupID in the Go converter. Exported so the shared table
|
|
13714
|
+
* can be asserted against the encoder directly, including the empty gid the
|
|
13715
|
+
* converter itself skips.
|
|
13716
|
+
*/
|
|
13717
|
+
function xccdfGroupId(gid) {
|
|
13718
|
+
if (isXccdfGroupId(gid)) return gid;
|
|
13719
|
+
return sanitizeXccdfId("xccdf_hdf_group_" + (gid === "" ? "unnamed" : gid));
|
|
13720
|
+
}
|
|
13721
|
+
/**
|
|
13722
|
+
* Whether a value already satisfies groupIdType. The character test is
|
|
13723
|
+
* ASCII-only, deliberately narrower than NCName: a conforming non-ASCII id is
|
|
13724
|
+
* re-encoded rather than trusted, which errs toward valid output and keeps the
|
|
13725
|
+
* check identical to the Go peer's.
|
|
13726
|
+
*/
|
|
13727
|
+
function isXccdfGroupId(id) {
|
|
13728
|
+
return /^xccdf_[^_]+_group_.+$/.test(id) && /^[A-Za-z0-9._-]+$/.test(id);
|
|
13445
13729
|
}
|
|
13446
13730
|
/** Read a string-valued tag, or undefined if absent/non-string. */
|
|
13447
13731
|
function tagString(tags, key) {
|
|
@@ -13466,12 +13750,22 @@ function toolTestSystem(tool) {
|
|
|
13466
13750
|
const name = tool.name ? cpeField(tool.name) : "tool";
|
|
13467
13751
|
return `cpe:/a:${name}:${name}:${cpeField(tool.version)}`;
|
|
13468
13752
|
}
|
|
13753
|
+
/**
|
|
13754
|
+
* The requirement id as text for id construction. HDF makes id required, but
|
|
13755
|
+
* nested-invalid input reaches the converter and concatenating an absent id
|
|
13756
|
+
* wrote the literal string "undefined" into the emitted identifier — where Go,
|
|
13757
|
+
* concatenating a zero-value string, wrote nothing. Both satisfied the XCCDF
|
|
13758
|
+
* pattern, so no schema gate caught the divergence.
|
|
13759
|
+
*/
|
|
13760
|
+
function requirementIdText(id) {
|
|
13761
|
+
return typeof id === "string" ? id : "";
|
|
13762
|
+
}
|
|
13469
13763
|
/** Build the Benchmark XML object from HDF data. */
|
|
13470
13764
|
function buildBenchmarkObj(hdfData) {
|
|
13471
13765
|
const empty = !hdfData.baselines || hdfData.baselines.length === 0;
|
|
13472
13766
|
const benchmark = {
|
|
13473
13767
|
[`${ATTR}xmlns`]: "http://checklists.nist.gov/xccdf/1.2",
|
|
13474
|
-
[`${ATTR}id`]: empty ? "xccdf_hdf_benchmark_exported" : sanitizeXccdfId("xccdf_hdf_benchmark_" + hdfData.baselines[0].name),
|
|
13768
|
+
[`${ATTR}id`]: empty ? "xccdf_hdf_benchmark_exported" : sanitizeXccdfId("xccdf_hdf_benchmark_" + xccdfNamePart(hdfData.baselines[0].name)),
|
|
13475
13769
|
[`${ATTR}resolved`]: "1",
|
|
13476
13770
|
status: wrap("incomplete")
|
|
13477
13771
|
};
|
|
@@ -13485,7 +13779,7 @@ function buildBenchmarkObj(hdfData) {
|
|
|
13485
13779
|
if (baseline.summary) benchmark.description = wrap(baseline.summary);
|
|
13486
13780
|
benchmark.version = wrap(baseline.version ?? "1.0");
|
|
13487
13781
|
benchmark.Profile = {
|
|
13488
|
-
[`${ATTR}id`]: sanitizeXccdfId("xccdf_hdf_profile_" + baseline.name),
|
|
13782
|
+
[`${ATTR}id`]: sanitizeXccdfId("xccdf_hdf_profile_" + xccdfNamePart(baseline.name)),
|
|
13489
13783
|
title: wrap(baseline.title ?? baseline.name)
|
|
13490
13784
|
};
|
|
13491
13785
|
const groups = [];
|
|
@@ -13502,7 +13796,7 @@ function buildBenchmarkObj(hdfData) {
|
|
|
13502
13796
|
if (idx === void 0) {
|
|
13503
13797
|
idx = groups.length;
|
|
13504
13798
|
groupIndex.set(gid, idx);
|
|
13505
|
-
const group = { [`${ATTR}id`]: gid };
|
|
13799
|
+
const group = { [`${ATTR}id`]: xccdfGroupId(gid) };
|
|
13506
13800
|
const gtitle = tagString(req.tags, "gtitle");
|
|
13507
13801
|
if (gtitle) group.title = wrap(gtitle);
|
|
13508
13802
|
group.Rule = [];
|
|
@@ -13512,12 +13806,13 @@ function buildBenchmarkObj(hdfData) {
|
|
|
13512
13806
|
}
|
|
13513
13807
|
if (groups.length > 0) benchmark.Group = groups;
|
|
13514
13808
|
if (flatRules.length > 0) benchmark.Rule = flatRules;
|
|
13515
|
-
|
|
13809
|
+
const testResult = buildTestResultObj(hdfData, baseline);
|
|
13810
|
+
if (testResult) benchmark.TestResult = testResult;
|
|
13516
13811
|
return benchmark;
|
|
13517
13812
|
}
|
|
13518
13813
|
/** Build an XCCDF Rule object from an HDF EvaluatedRequirement. */
|
|
13519
13814
|
function buildRuleObj(req) {
|
|
13520
|
-
const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
|
|
13815
|
+
const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + requirementIdText(req.id) + "_rule");
|
|
13521
13816
|
const rule = {
|
|
13522
13817
|
[`${ATTR}id`]: ruleId,
|
|
13523
13818
|
[`${ATTR}severity`]: impactToSeverity$2(req.impact),
|
|
@@ -13566,27 +13861,74 @@ function buildRuleObj(req) {
|
|
|
13566
13861
|
[`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
|
|
13567
13862
|
"check-content": wrap(checkContent)
|
|
13568
13863
|
});
|
|
13569
|
-
|
|
13864
|
+
const code = firstNonEmpty(req.code ?? "");
|
|
13865
|
+
if (code !== "") checks.push({
|
|
13570
13866
|
[`${ATTR}system`]: "http://inspec.io/",
|
|
13571
|
-
"check-content": wrap(
|
|
13867
|
+
"check-content": wrap(code)
|
|
13572
13868
|
});
|
|
13573
13869
|
if (checks.length > 0) rule.check = checks;
|
|
13574
13870
|
return rule;
|
|
13575
13871
|
}
|
|
13576
|
-
/**
|
|
13872
|
+
/**
|
|
13873
|
+
* The earliest and latest result time in a baseline.
|
|
13874
|
+
*
|
|
13875
|
+
* XCCDF makes TestResult/@end-time required while HDF's top-level timestamp is
|
|
13876
|
+
* optional, so a document without one still has to carry a scan window. It is
|
|
13877
|
+
* derived from the results rather than the wall clock, which would break both
|
|
13878
|
+
* determinism and the golden comparison. A TestResult is only built when a
|
|
13879
|
+
* baseline exists, and the HDF schema puts minItems 1 on both requirements and
|
|
13880
|
+
* results with startTime required on each, so a window is always available
|
|
13881
|
+
* here; undefined is returned anyway rather than assuming it.
|
|
13882
|
+
*
|
|
13883
|
+
* Times are emitted through formatTimestamp, HDF's canonical form, which is
|
|
13884
|
+
* documented as byte-identical to the RFC3339Nano string the Go converter
|
|
13885
|
+
* emits for the same instant.
|
|
13886
|
+
*
|
|
13887
|
+
* Mirrored by resultTimeWindow in the Go converter.
|
|
13888
|
+
*/
|
|
13889
|
+
function resultTimeWindow(baseline) {
|
|
13890
|
+
let first;
|
|
13891
|
+
let last;
|
|
13892
|
+
for (const req of baseline.requirements) for (const res of req.results) {
|
|
13893
|
+
const parsed = parseTimestamp(typeof res.startTime === "string" ? res.startTime : res.startTime.toISOString());
|
|
13894
|
+
if (!parsed) continue;
|
|
13895
|
+
const at = parsed.getTime();
|
|
13896
|
+
const canonical = formatTimestamp(parsed);
|
|
13897
|
+
if (!first || at < first.at) first = {
|
|
13898
|
+
at,
|
|
13899
|
+
text: canonical
|
|
13900
|
+
};
|
|
13901
|
+
if (!last || at > last.at) last = {
|
|
13902
|
+
at,
|
|
13903
|
+
text: canonical
|
|
13904
|
+
};
|
|
13905
|
+
}
|
|
13906
|
+
return first && last ? {
|
|
13907
|
+
first: first.text,
|
|
13908
|
+
last: last.text
|
|
13909
|
+
} : void 0;
|
|
13910
|
+
}
|
|
13911
|
+
/** Build the XCCDF TestResult object, or undefined when it cannot carry a time. */
|
|
13577
13912
|
function buildTestResultObj(hdfData, baseline) {
|
|
13578
13913
|
const testResult = {
|
|
13579
13914
|
[`${ATTR}id`]: "xccdf_hdf_testresult_1",
|
|
13580
13915
|
title: wrap("HDF Assessment Results")
|
|
13581
13916
|
};
|
|
13582
13917
|
if (hdfData.timestamp) {
|
|
13583
|
-
const
|
|
13918
|
+
const raw = typeof hdfData.timestamp === "string" ? hdfData.timestamp : hdfData.timestamp.toISOString();
|
|
13919
|
+
const parsedTs = parseTimestamp(raw);
|
|
13920
|
+
const ts = parsedTs ? formatTimestamp(parsedTs) : raw;
|
|
13584
13921
|
testResult[`${ATTR}start-time`] = ts;
|
|
13585
13922
|
let endTime = ts;
|
|
13586
13923
|
const duration = hdfData.statistics?.duration;
|
|
13587
13924
|
const start = parseTimestamp(ts);
|
|
13588
13925
|
if (start && typeof duration === "number" && duration > 0) endTime = formatTimestamp(new Date(start.getTime() + duration * 1e3));
|
|
13589
13926
|
testResult[`${ATTR}end-time`] = endTime;
|
|
13927
|
+
} else {
|
|
13928
|
+
const window = resultTimeWindow(baseline);
|
|
13929
|
+
if (!window) return;
|
|
13930
|
+
testResult[`${ATTR}start-time`] = window.first;
|
|
13931
|
+
testResult[`${ATTR}end-time`] = window.last;
|
|
13590
13932
|
}
|
|
13591
13933
|
const testSystem = toolTestSystem(hdfData.tool);
|
|
13592
13934
|
if (testSystem) testResult[`${ATTR}test-system`] = testSystem;
|
|
@@ -13599,7 +13941,7 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
13599
13941
|
let passed = 0;
|
|
13600
13942
|
let scorable = 0;
|
|
13601
13943
|
for (const req of baseline.requirements) {
|
|
13602
|
-
const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
|
|
13944
|
+
const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + requirementIdText(req.id) + "_rule");
|
|
13603
13945
|
const stigId = tagString(req.tags, "stig_id");
|
|
13604
13946
|
const statusInput = requirementStatusInput(req);
|
|
13605
13947
|
const resolved = computeEffectiveStatus(statusInput);
|
|
@@ -13611,7 +13953,9 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
13611
13953
|
scorable++;
|
|
13612
13954
|
} else if (status === "fail") scorable++;
|
|
13613
13955
|
const rr = { [`${ATTR}idref`]: ruleIdRef };
|
|
13614
|
-
|
|
13956
|
+
const rawStart = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
|
|
13957
|
+
const parsedStart = parseTimestamp(rawStart);
|
|
13958
|
+
rr[`${ATTR}time`] = parsedStart ? formatTimestamp(parsedStart) : rawStart;
|
|
13615
13959
|
if (stigId) rr[`${ATTR}version`] = stigId;
|
|
13616
13960
|
rr.result = wrap(status);
|
|
13617
13961
|
if (result.message) rr.message = wrap(result.message);
|
|
@@ -13866,15 +14210,9 @@ function oscalString(s) {
|
|
|
13866
14210
|
* @returns OSCAL SAR JSON string
|
|
13867
14211
|
*/
|
|
13868
14212
|
async function convertHdfToOscalSar(input) {
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
try {
|
|
13873
|
-
hdfResults = parseHdf(input);
|
|
13874
|
-
} catch {
|
|
13875
|
-
throw new Error("hdf-to-oscal-sar: failed to parse HDF JSON");
|
|
13876
|
-
}
|
|
13877
|
-
if (!hdfResults || typeof hdfResults !== "object" || !("baselines" in hdfResults)) throw new Error("hdf-to-oscal-sar: invalid HDF structure: missing baselines field");
|
|
14213
|
+
const { doc: hdfResultsDoc } = requireHdfResults(input, "hdf-to-oscal-sar");
|
|
14214
|
+
const hdfResults = hdfResultsDoc;
|
|
14215
|
+
if (hdfResults.baselines.length === 0) throw new Error("hdf-to-oscal-sar: cannot represent an assessment with no evaluated baselines as OSCAL Assessment Results, which requires at least one result");
|
|
13878
14216
|
const doc = buildOSCALDocument(hdfResults);
|
|
13879
14217
|
return JSON.stringify(doc, null, 2);
|
|
13880
14218
|
}
|
|
@@ -13976,7 +14314,7 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
|
|
|
13976
14314
|
const includeControls = [];
|
|
13977
14315
|
const seenControl = /* @__PURE__ */ new Set();
|
|
13978
14316
|
for (const req of baseline.requirements) {
|
|
13979
|
-
const nistId = nistTagToControlId(req.id);
|
|
14317
|
+
const nistId = nistTagToControlId(req.id ?? "");
|
|
13980
14318
|
if (nistId === "") continue;
|
|
13981
14319
|
const { finding, observation, risk, resource } = requirementToFindingSet(req, timestamp, toolActorUuid, subjects);
|
|
13982
14320
|
findings.push(finding);
|
|
@@ -13998,9 +14336,9 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
|
|
|
13998
14336
|
start: assessmentStart(baseline, timestamp),
|
|
13999
14337
|
...resultProps.length > 0 ? { props: resultProps } : {},
|
|
14000
14338
|
"reviewed-controls": { "control-selections": [controlSelection] },
|
|
14001
|
-
findings,
|
|
14002
|
-
observations,
|
|
14003
|
-
risks
|
|
14339
|
+
...findings.length > 0 ? { findings } : {},
|
|
14340
|
+
...observations.length > 0 ? { observations } : {},
|
|
14341
|
+
...risks.length > 0 ? { risks } : {}
|
|
14004
14342
|
},
|
|
14005
14343
|
resources
|
|
14006
14344
|
};
|
|
@@ -14010,10 +14348,17 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
|
|
|
14010
14348
|
* component's UUID (componentId when present, otherwise a fresh one) identifies
|
|
14011
14349
|
* the subject; the HDF component type is a valid OSCAL subject type token and
|
|
14012
14350
|
* its name becomes the subject title.
|
|
14351
|
+
*
|
|
14352
|
+
* A component with no type is skipped rather than given one. OSCAL requires both
|
|
14353
|
+
* subject-uuid and type on a subject-reference, so the type cannot simply be
|
|
14354
|
+
* omitted, and hdf-results defines no default component type to fall back on.
|
|
14355
|
+
* String(c.type) turned an absent type into the literal "undefined" — a valid
|
|
14356
|
+
* OSCAL token, so a schema check could not see it, asserting a component type
|
|
14357
|
+
* the source never stated. Mirrors the Go peer.
|
|
14013
14358
|
*/
|
|
14014
14359
|
function buildSubjects(components) {
|
|
14015
14360
|
if (!Array.isArray(components) || components.length === 0) return [];
|
|
14016
|
-
return components.map((c) => ({
|
|
14361
|
+
return components.filter((c) => oscalString(c.type ?? "") !== "").map((c) => ({
|
|
14017
14362
|
"subject-uuid": c.componentId && c.componentId !== "" ? c.componentId : crypto.randomUUID(),
|
|
14018
14363
|
type: String(c.type),
|
|
14019
14364
|
title: c.name
|
|
@@ -14023,7 +14368,7 @@ function buildSubjects(components) {
|
|
|
14023
14368
|
* Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
|
|
14024
14369
|
*/
|
|
14025
14370
|
function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
14026
|
-
const controlID = oscalToken(nistTagToControlId(req.id));
|
|
14371
|
+
const controlID = oscalToken(nistTagToControlId(req.id ?? ""));
|
|
14027
14372
|
const results = req.results ?? [];
|
|
14028
14373
|
const descriptions = req.descriptions ?? [];
|
|
14029
14374
|
const { state, reason } = effectiveState(req);
|
|
@@ -14380,17 +14725,27 @@ function riskStatusFromState(state) {
|
|
|
14380
14725
|
* @returns OSCAL POA&M JSON string
|
|
14381
14726
|
*/
|
|
14382
14727
|
async function convertHdfToOscalPoam(input) {
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
let amendments;
|
|
14386
|
-
try {
|
|
14387
|
-
amendments = parseHdf(input);
|
|
14388
|
-
} catch {
|
|
14389
|
-
throw new Error("hdf-to-oscal-poam: failed to parse JSON");
|
|
14390
|
-
}
|
|
14391
|
-
const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendments) };
|
|
14728
|
+
const { doc: amendmentsDoc } = requireHdfAmendments(input, "hdf-to-oscal-poam");
|
|
14729
|
+
const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendmentsDoc) };
|
|
14392
14730
|
return JSON.stringify(doc, null, 2);
|
|
14393
14731
|
}
|
|
14732
|
+
/**
|
|
14733
|
+
* Picks the document title, which OSCAL requires on metadata. The HDF name is
|
|
14734
|
+
* schema-required, so the fallbacks only matter for a document that slipped
|
|
14735
|
+
* through some other producer's validation.
|
|
14736
|
+
*/
|
|
14737
|
+
function poamTitle(a) {
|
|
14738
|
+
return firstNonEmpty(a.name, a.amendmentId ?? "", "HDF Amendments");
|
|
14739
|
+
}
|
|
14740
|
+
/**
|
|
14741
|
+
* Supplies the text OSCAL requires for a risk's description and statement. HDF
|
|
14742
|
+
* puts no minLength on reason, so an override can legitimately carry none; the
|
|
14743
|
+
* fallback states that absence rather than inventing an impact assessment the
|
|
14744
|
+
* source never made.
|
|
14745
|
+
*/
|
|
14746
|
+
function riskRationale(override) {
|
|
14747
|
+
return firstNonEmpty(override.reason, `No rationale was recorded for the ${String(override.type)} override applied to ${override.requirementId}.`);
|
|
14748
|
+
}
|
|
14394
14749
|
/** HDF dates arrive as strings from JSON.parse but are typed as Date. */
|
|
14395
14750
|
function toDate(value) {
|
|
14396
14751
|
if (value === void 0) return void 0;
|
|
@@ -14408,14 +14763,15 @@ var PartyRegistry = class {
|
|
|
14408
14763
|
this.byId = /* @__PURE__ */ new Map();
|
|
14409
14764
|
}
|
|
14410
14765
|
getOrAdd(id) {
|
|
14411
|
-
const
|
|
14766
|
+
const name = oscalString(id.identifier);
|
|
14767
|
+
const existing = this.byId.get(name);
|
|
14412
14768
|
if (existing) return existing.uuid;
|
|
14413
14769
|
const party = {
|
|
14414
14770
|
uuid: crypto.randomUUID(),
|
|
14415
14771
|
type: "person",
|
|
14416
|
-
name:
|
|
14772
|
+
...name === "" ? {} : { name }
|
|
14417
14773
|
};
|
|
14418
|
-
this.byId.set(
|
|
14774
|
+
this.byId.set(name, party);
|
|
14419
14775
|
return party.uuid;
|
|
14420
14776
|
}
|
|
14421
14777
|
list() {
|
|
@@ -14465,7 +14821,7 @@ function amendmentsToPOAM(amendments) {
|
|
|
14465
14821
|
observations.push(...itemObs);
|
|
14466
14822
|
}
|
|
14467
14823
|
const metadata = {
|
|
14468
|
-
title: amendments
|
|
14824
|
+
title: poamTitle(amendments),
|
|
14469
14825
|
"last-modified": latestAppliedAt(amendments.overrides),
|
|
14470
14826
|
version: amendmentsVersion(amendments),
|
|
14471
14827
|
"oscal-version": OSCAL_VERSION
|
|
@@ -14483,9 +14839,9 @@ function amendmentsToPOAM(amendments) {
|
|
|
14483
14839
|
uuid: crypto.randomUUID(),
|
|
14484
14840
|
metadata,
|
|
14485
14841
|
"import-ssp": importSSP,
|
|
14486
|
-
risks,
|
|
14487
14842
|
"poam-items": poamItems
|
|
14488
14843
|
};
|
|
14844
|
+
if (risks.length > 0) poam.risks = risks;
|
|
14489
14845
|
if (observations.length > 0) poam.observations = observations;
|
|
14490
14846
|
const resources = externalRefResources(amendments.overrides);
|
|
14491
14847
|
if (resources.length > 0) poam["back-matter"] = { resources };
|
|
@@ -14498,10 +14854,9 @@ function amendmentsToPOAM(amendments) {
|
|
|
14498
14854
|
function overrideToPOAMItem(override, parties) {
|
|
14499
14855
|
const riskUUID = crypto.randomUUID();
|
|
14500
14856
|
const riskStatus = override.status ? hdfStatusToOscalRiskStatus(String(override.status)) : "open";
|
|
14501
|
-
const
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
}];
|
|
14857
|
+
const controlID = nistTagToControlId(override.requirementId);
|
|
14858
|
+
const riskProps = [];
|
|
14859
|
+
pushStringProp(riskProps, "impacted-control-id", controlID);
|
|
14505
14860
|
if (override.type) riskProps.push({
|
|
14506
14861
|
name: "override-type",
|
|
14507
14862
|
value: String(override.type)
|
|
@@ -14510,18 +14865,9 @@ function overrideToPOAMItem(override, parties) {
|
|
|
14510
14865
|
name: "impact-override",
|
|
14511
14866
|
value: String(override.impact.value)
|
|
14512
14867
|
});
|
|
14513
|
-
if (override.justification) riskProps.
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
});
|
|
14517
|
-
if (override.baselineRef) riskProps.push({
|
|
14518
|
-
name: "baseline-ref",
|
|
14519
|
-
value: override.baselineRef
|
|
14520
|
-
});
|
|
14521
|
-
if (override.componentRef) riskProps.push({
|
|
14522
|
-
name: "component-ref",
|
|
14523
|
-
value: override.componentRef
|
|
14524
|
-
});
|
|
14868
|
+
if (override.justification) pushStringProp(riskProps, "justification", String(override.justification));
|
|
14869
|
+
if (override.baselineRef) pushStringProp(riskProps, "baseline-ref", override.baselineRef);
|
|
14870
|
+
if (override.componentRef) pushStringProp(riskProps, "component-ref", override.componentRef);
|
|
14525
14871
|
const remediations = [];
|
|
14526
14872
|
if (override.milestones) for (const ms of override.milestones) {
|
|
14527
14873
|
const msProps = [];
|
|
@@ -14583,11 +14929,12 @@ function overrideToPOAMItem(override, parties) {
|
|
|
14583
14929
|
itemObs.push(evidenceObservation(ev, obsUUID, collected));
|
|
14584
14930
|
relatedObs.push({ "observation-uuid": obsUUID });
|
|
14585
14931
|
}
|
|
14932
|
+
const rationale = riskRationale(override);
|
|
14586
14933
|
const risk = {
|
|
14587
14934
|
uuid: riskUUID,
|
|
14588
14935
|
title: override.requirementId,
|
|
14589
|
-
description:
|
|
14590
|
-
statement:
|
|
14936
|
+
description: rationale,
|
|
14937
|
+
statement: rationale,
|
|
14591
14938
|
status: riskStatus,
|
|
14592
14939
|
deadline,
|
|
14593
14940
|
props: riskProps,
|
|
@@ -14622,9 +14969,22 @@ function latestAppliedAt(overrides) {
|
|
|
14622
14969
|
}
|
|
14623
14970
|
return formatTimestampSeconds(latest ?? /* @__PURE__ */ new Date());
|
|
14624
14971
|
}
|
|
14972
|
+
/**
|
|
14973
|
+
* Add a property whose value OSCAL types as StringDatatype, trimming it and
|
|
14974
|
+
* omitting the property entirely when nothing survives. A prop with an empty
|
|
14975
|
+
* value carries no more than an absent one and is schema-invalid. Mirrors
|
|
14976
|
+
* appendStringProp in the Go peer.
|
|
14977
|
+
*/
|
|
14978
|
+
function pushStringProp(props, name, value) {
|
|
14979
|
+
const trimmed = oscalString(value ?? "");
|
|
14980
|
+
if (trimmed !== "") props.push({
|
|
14981
|
+
name,
|
|
14982
|
+
value: trimmed
|
|
14983
|
+
});
|
|
14984
|
+
}
|
|
14625
14985
|
/** Sources metadata.version from the amendments document, defaulting when omitted. */
|
|
14626
14986
|
function amendmentsVersion(a) {
|
|
14627
|
-
return a.version
|
|
14987
|
+
return oscalString(a.version ?? "") || "1.0.0";
|
|
14628
14988
|
}
|
|
14629
14989
|
/**
|
|
14630
14990
|
* Carries document identifiers and labels that have no first-class OSCAL home.
|
|
@@ -14632,17 +14992,33 @@ function amendmentsVersion(a) {
|
|
|
14632
14992
|
*/
|
|
14633
14993
|
function metadataProps(a) {
|
|
14634
14994
|
const props = [];
|
|
14635
|
-
if (a.amendmentId) props.
|
|
14636
|
-
|
|
14637
|
-
value
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
name: k,
|
|
14641
|
-
value: a.labels[k],
|
|
14642
|
-
class: "amendment-label"
|
|
14643
|
-
});
|
|
14995
|
+
if (a.amendmentId) pushStringProp(props, "amendment-id", a.amendmentId);
|
|
14996
|
+
if (a.labels) for (const [key, value] of Object.entries(a.labels).sort(([x], [y]) => x < y ? -1 : x > y ? 1 : 0)) {
|
|
14997
|
+
const prop = labelProp(key, value);
|
|
14998
|
+
if (prop.value !== "") props.push(prop);
|
|
14999
|
+
}
|
|
14644
15000
|
return props;
|
|
14645
15001
|
}
|
|
15002
|
+
/**
|
|
15003
|
+
* Render one amendments label as a property. OSCAL types prop/@name as
|
|
15004
|
+
* TokenDatatype while HDF puts no constraint on label keys, so the key is
|
|
15005
|
+
* encoded and the source key kept in remarks when it had to change. Every label
|
|
15006
|
+
* key in this package's converter fixtures is token-shaped today, so this guards a shape real
|
|
15007
|
+
* data has not yet produced — but Kubernetes and OCI label keys are namespaced
|
|
15008
|
+
* with '/', which HDF permits and OSCAL rejects.
|
|
15009
|
+
*
|
|
15010
|
+
* Mirrored by labelProp in the Go converter.
|
|
15011
|
+
*/
|
|
15012
|
+
function labelProp(key, value) {
|
|
15013
|
+
const name = oscalToken(key) === "" ? "_" : oscalToken(key);
|
|
15014
|
+
const prop = {
|
|
15015
|
+
name,
|
|
15016
|
+
value: oscalString(value),
|
|
15017
|
+
class: "amendment-label"
|
|
15018
|
+
};
|
|
15019
|
+
if (key !== "" && name !== key) prop.remarks = key;
|
|
15020
|
+
return prop;
|
|
15021
|
+
}
|
|
14646
15022
|
/** Picks the collection timestamp for evidence observations. */
|
|
14647
15023
|
function observationCollected(override) {
|
|
14648
15024
|
return formatTimestampSeconds(toDate(override.appliedAt) ?? /* @__PURE__ */ new Date());
|
|
@@ -14656,14 +15032,8 @@ function evidenceObservation(ev, uuid, defaultCollected) {
|
|
|
14656
15032
|
if (ev.type === "url") re.href = ev.data;
|
|
14657
15033
|
else if (ev.data) re.remarks = ev.data;
|
|
14658
15034
|
const props = [];
|
|
14659
|
-
if (ev.mimeType) props.
|
|
14660
|
-
|
|
14661
|
-
value: ev.mimeType
|
|
14662
|
-
});
|
|
14663
|
-
if (ev.capturedBy && ev.capturedBy.identifier) props.push({
|
|
14664
|
-
name: "captured-by",
|
|
14665
|
-
value: ev.capturedBy.identifier
|
|
14666
|
-
});
|
|
15035
|
+
if (ev.mimeType) pushStringProp(props, "mime-type", ev.mimeType);
|
|
15036
|
+
if (ev.capturedBy && ev.capturedBy.identifier) pushStringProp(props, "captured-by", ev.capturedBy.identifier);
|
|
14667
15037
|
return {
|
|
14668
15038
|
uuid,
|
|
14669
15039
|
description: desc,
|
|
@@ -15262,8 +15632,10 @@ async function convertOpenVexToHdf(input, converterVersion = "1.0.0") {
|
|
|
15262
15632
|
if (override) overrides.push(override);
|
|
15263
15633
|
}
|
|
15264
15634
|
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");
|
|
15635
|
+
const name = doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements";
|
|
15636
|
+
await chainOverrides(overrides);
|
|
15265
15637
|
return {
|
|
15266
|
-
name
|
|
15638
|
+
name,
|
|
15267
15639
|
description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
|
|
15268
15640
|
overrides,
|
|
15269
15641
|
appliedBy: identityFor$2(doc.author, doc.role),
|
|
@@ -15357,6 +15729,7 @@ async function convertCsafVexToHdf(input, converterVersion = "1.0.0") {
|
|
|
15357
15729
|
for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
|
|
15358
15730
|
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");
|
|
15359
15731
|
const publisherName = publisher.name ?? "";
|
|
15732
|
+
await chainOverrides(overrides);
|
|
15360
15733
|
return {
|
|
15361
15734
|
name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
|
|
15362
15735
|
description: `Imported VEX advisory ${tracking.id ?? ""}`,
|
|
@@ -15575,9 +15948,12 @@ async function convertCyclonedxVexToHdf(input, converterVersion = "1.0.0") {
|
|
|
15575
15948
|
}
|
|
15576
15949
|
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");
|
|
15577
15950
|
const publisherName = publisher.identifier;
|
|
15951
|
+
const name = publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements";
|
|
15952
|
+
const description = bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX";
|
|
15953
|
+
await chainOverrides(overrides);
|
|
15578
15954
|
return {
|
|
15579
|
-
name
|
|
15580
|
-
description
|
|
15955
|
+
name,
|
|
15956
|
+
description,
|
|
15581
15957
|
overrides,
|
|
15582
15958
|
appliedBy: publisher,
|
|
15583
15959
|
generator: {
|
|
@@ -15752,8 +16128,10 @@ async function convertSpdxVexToHdf(input, converterVersion = "1.0.0") {
|
|
|
15752
16128
|
}
|
|
15753
16129
|
if (overrides.length === 0) throw new Error("spdx-vex-to-hdf: SPDX document contains no actionable VEX statements (only affected/under_investigation); no amendment to write");
|
|
15754
16130
|
const appliedBy = documentIdentity(graph, idx);
|
|
16131
|
+
const name = appliedBy.identifier && appliedBy.identifier !== DEFAULT_APPLIED_BY ? `SPDX VEX statements from ${appliedBy.identifier}` : "SPDX VEX statements";
|
|
16132
|
+
await chainOverrides(overrides);
|
|
15755
16133
|
return {
|
|
15756
|
-
name
|
|
16134
|
+
name,
|
|
15757
16135
|
description: "Imported SPDX 3.0.1 security-profile VEX statements",
|
|
15758
16136
|
overrides,
|
|
15759
16137
|
appliedBy,
|
|
@@ -15965,7 +16343,7 @@ function isCvssType(type) {
|
|
|
15965
16343
|
const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
15966
16344
|
const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
|
|
15967
16345
|
const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
|
|
15968
|
-
const DEFAULT_PRODUCT_ID$
|
|
16346
|
+
const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
|
|
15969
16347
|
/** Map an HDF Cvss block to a CSAF score entry (cvss_v2/v3/v4 by version). */
|
|
15970
16348
|
function buildCsafScore(cvss, products) {
|
|
15971
16349
|
const inner = {
|
|
@@ -16000,8 +16378,8 @@ function convertHdfToCsafVex(input, converterVersion) {
|
|
|
16000
16378
|
const doc = buildDocument(amendments, converterVersion);
|
|
16001
16379
|
doc.vulnerabilities = vulnerabilities;
|
|
16002
16380
|
doc.product_tree.full_product_names = names.length > 0 ? names : [{
|
|
16003
|
-
name: DEFAULT_PRODUCT_ID$
|
|
16004
|
-
product_id: DEFAULT_PRODUCT_ID$
|
|
16381
|
+
name: DEFAULT_PRODUCT_ID$1,
|
|
16382
|
+
product_id: DEFAULT_PRODUCT_ID$1
|
|
16005
16383
|
}];
|
|
16006
16384
|
return JSON.stringify(doc, null, 2);
|
|
16007
16385
|
}
|
|
@@ -16054,8 +16432,8 @@ function productEntriesFor(o) {
|
|
|
16054
16432
|
}));
|
|
16055
16433
|
}
|
|
16056
16434
|
return [{
|
|
16057
|
-
name: DEFAULT_PRODUCT_ID$
|
|
16058
|
-
product_id: DEFAULT_PRODUCT_ID$
|
|
16435
|
+
name: DEFAULT_PRODUCT_ID$1,
|
|
16436
|
+
product_id: DEFAULT_PRODUCT_ID$1
|
|
16059
16437
|
}];
|
|
16060
16438
|
}
|
|
16061
16439
|
/** product_tree entries for the synthesized fixed-version products. */
|
|
@@ -16094,18 +16472,36 @@ function productIDsFor$1(o) {
|
|
|
16094
16472
|
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
16095
16473
|
if (parts.length > 0) return parts;
|
|
16096
16474
|
}
|
|
16097
|
-
return [DEFAULT_PRODUCT_ID$
|
|
16475
|
+
return [DEFAULT_PRODUCT_ID$1];
|
|
16098
16476
|
}
|
|
16099
16477
|
function stripProductsLine$1(reason) {
|
|
16100
16478
|
return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
|
|
16101
16479
|
}
|
|
16102
16480
|
/**
|
|
16481
|
+
* Fill CSAF references[].summary, which is required with minLength 1 while the
|
|
16482
|
+
* HDF description backing it is optional. The URL is the documented fallback:
|
|
16483
|
+
* the only text a reference always carries, where a placeholder would assert a
|
|
16484
|
+
* description the source never gave.
|
|
16485
|
+
*/
|
|
16486
|
+
function referenceSummary(description, url) {
|
|
16487
|
+
return firstNonEmpty(description, url);
|
|
16488
|
+
}
|
|
16489
|
+
/**
|
|
16490
|
+
* The override reason with the machine-written 'Products:' line removed, or ''
|
|
16491
|
+
* when no prose remains. Both sinks it feeds — threats[].details and
|
|
16492
|
+
* notes[].text — are minLength 1 in CSAF, so an empty result means the element
|
|
16493
|
+
* is omitted, never emitted blank.
|
|
16494
|
+
*/
|
|
16495
|
+
function reasonProse(reason) {
|
|
16496
|
+
return firstNonEmpty(stripProductsLine$1(reason ?? ""));
|
|
16497
|
+
}
|
|
16498
|
+
/**
|
|
16103
16499
|
* Surface the override reason prose as a CSAF description note. Used on the
|
|
16104
16500
|
* not_affected/fixed paths, which otherwise drop reason (the affected path
|
|
16105
16501
|
* keeps it as threats[impact]).
|
|
16106
16502
|
*/
|
|
16107
16503
|
function appendReasonNote(v, reason) {
|
|
16108
|
-
const text =
|
|
16504
|
+
const text = reasonProse(reason);
|
|
16109
16505
|
if (!text) return;
|
|
16110
16506
|
v.notes = v.notes ?? [];
|
|
16111
16507
|
v.notes.push({
|
|
@@ -16182,11 +16578,12 @@ function buildVulnerability(group) {
|
|
|
16182
16578
|
emitted = true;
|
|
16183
16579
|
} else if (canonical === VexStatus.Affected) {
|
|
16184
16580
|
status.known_affected = (status.known_affected ?? []).concat(pids);
|
|
16185
|
-
|
|
16581
|
+
const details = reasonProse(o.reason);
|
|
16582
|
+
if (details) {
|
|
16186
16583
|
v.threats = v.threats ?? [];
|
|
16187
16584
|
v.threats.push({
|
|
16188
16585
|
category: "impact",
|
|
16189
|
-
details
|
|
16586
|
+
details,
|
|
16190
16587
|
product_ids: pids
|
|
16191
16588
|
});
|
|
16192
16589
|
}
|
|
@@ -16208,7 +16605,7 @@ function buildVulnerability(group) {
|
|
|
16208
16605
|
v.references = v.references ?? [];
|
|
16209
16606
|
v.references.push({
|
|
16210
16607
|
category: "external",
|
|
16211
|
-
summary: e.description
|
|
16608
|
+
summary: referenceSummary(e.description, e.data),
|
|
16212
16609
|
url: e.data
|
|
16213
16610
|
});
|
|
16214
16611
|
}
|
|
@@ -16217,7 +16614,7 @@ function buildVulnerability(group) {
|
|
|
16217
16614
|
v.references = v.references ?? [];
|
|
16218
16615
|
v.references.push({
|
|
16219
16616
|
category: "external",
|
|
16220
|
-
summary: r.description
|
|
16617
|
+
summary: referenceSummary(r.description, r.href),
|
|
16221
16618
|
url: r.href
|
|
16222
16619
|
});
|
|
16223
16620
|
}
|
|
@@ -16239,7 +16636,7 @@ function buildVulnerability(group) {
|
|
|
16239
16636
|
return {
|
|
16240
16637
|
cve: v.cve,
|
|
16241
16638
|
...v.notes && { notes: v.notes },
|
|
16242
|
-
product_status: status,
|
|
16639
|
+
...Object.keys(status).length > 0 && { product_status: status },
|
|
16243
16640
|
...v.flags && { flags: v.flags },
|
|
16244
16641
|
...v.threats && { threats: v.threats },
|
|
16245
16642
|
...v.remediations && { remediations: v.remediations },
|
|
@@ -16319,7 +16716,6 @@ const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
|
|
|
16319
16716
|
const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
|
|
16320
16717
|
const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
16321
16718
|
const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
|
|
16322
|
-
const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
|
|
16323
16719
|
async function convertHdfToOpenVex(input, converterVersion) {
|
|
16324
16720
|
validateInputSize(input, "hdf-to-openvex");
|
|
16325
16721
|
const amendments = parseHdf(input);
|
|
@@ -16361,19 +16757,12 @@ function overrideToStatement(o, docAuthor) {
|
|
|
16361
16757
|
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
|
|
16362
16758
|
const notAffected = canonical === VexStatus.NotAffected;
|
|
16363
16759
|
const justification = notAffected && o.justification ? String(o.justification) : "";
|
|
16364
|
-
const reason = stripProductsLine(o.reason ?? "");
|
|
16365
|
-
const impact = notAffected ? reason : "";
|
|
16760
|
+
const reason = firstNonEmpty(stripProductsLine(o.reason ?? ""));
|
|
16761
|
+
const impact = notAffected ? firstNonEmpty(reason, justification ? "" : noImpactRationaleRecorded(o)) : "";
|
|
16366
16762
|
let action = "";
|
|
16367
|
-
|
|
16368
|
-
if (canonical === VexStatus.
|
|
16369
|
-
|
|
16370
|
-
action = firstMilestoneAction(o);
|
|
16371
|
-
if (!action) {
|
|
16372
|
-
action = reason;
|
|
16373
|
-
reasonEmitted = reason !== "";
|
|
16374
|
-
}
|
|
16375
|
-
}
|
|
16376
|
-
const statusNotes = buildStatusNotes(o, reason, reasonEmitted, docAuthor);
|
|
16763
|
+
if (canonical === VexStatus.Fixed) action = firstNonEmpty(firstMilestoneAction(o), "Fix applied; consumer re-scan confirmed clean.");
|
|
16764
|
+
else if (canonical === VexStatus.Affected) action = firstNonEmpty(firstMilestoneAction(o), reason, noRemediationRecorded(o));
|
|
16765
|
+
const statusNotes = buildStatusNotes(o, reason, reason !== "" && (impact === reason || action === reason), docAuthor);
|
|
16377
16766
|
return {
|
|
16378
16767
|
vulnerability: {
|
|
16379
16768
|
"@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`,
|
|
@@ -16420,12 +16809,26 @@ function productsFor(o) {
|
|
|
16420
16809
|
const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
|
|
16421
16810
|
if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
16422
16811
|
}
|
|
16423
|
-
if (ids.length === 0)
|
|
16812
|
+
if (ids.length === 0) return void 0;
|
|
16424
16813
|
return ids.map((id) => ({ "@id": id }));
|
|
16425
16814
|
}
|
|
16815
|
+
/**
|
|
16816
|
+
* States the absence OpenVEX has no other way to express: action_statement is
|
|
16817
|
+
* required on an affected statement, and a source override may carry neither a
|
|
16818
|
+
* milestone nor a reason to fill it.
|
|
16819
|
+
*/
|
|
16820
|
+
function noRemediationRecorded(o) {
|
|
16821
|
+
return `No remediation action was recorded for the ${String(o.type)} override applied to ${o.requirementId}.`;
|
|
16822
|
+
}
|
|
16823
|
+
/**
|
|
16824
|
+
* The not_affected peer of noRemediationRecorded, for an override carrying
|
|
16825
|
+
* neither a justification enum nor a reason.
|
|
16826
|
+
*/
|
|
16827
|
+
function noImpactRationaleRecorded(o) {
|
|
16828
|
+
return `No impact rationale was recorded for the ${String(o.type)} override applied to ${o.requirementId}.`;
|
|
16829
|
+
}
|
|
16426
16830
|
function firstMilestoneAction(o) {
|
|
16427
|
-
|
|
16428
|
-
return "";
|
|
16831
|
+
return firstNonEmpty(...(o.milestones ?? []).map((m) => m.description));
|
|
16429
16832
|
}
|
|
16430
16833
|
function allMilestonesCompleted$1(o) {
|
|
16431
16834
|
if (!o.milestones || o.milestones.length === 0) return false;
|
|
@@ -16914,6 +17317,7 @@ async function convertOscalPoamToHdf(input) {
|
|
|
16914
17317
|
for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
|
|
16915
17318
|
const systemRef = poam["import-ssp"]?.href || void 0;
|
|
16916
17319
|
const appliedBy = extractAppliedBy(poam.metadata);
|
|
17320
|
+
await chainOverrides(overrides);
|
|
16917
17321
|
return serializeHdf({
|
|
16918
17322
|
name: toKebabCase(poam.metadata.title, "oscal-poam"),
|
|
16919
17323
|
overrides,
|