@mitre/inspec-objects 1.0.1 → 2.0.1
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/.github/dependabot.yml +11 -0
- package/.github/workflows/auto-approve-and-merge.yml +22 -0
- package/.github/workflows/e2e-test.yml +1 -1
- package/.github/workflows/linter.yml +1 -2
- package/README.md +45 -4
- package/lib/index.d.ts +4 -2
- package/lib/index.js +5 -2
- package/lib/objects/control.d.ts +78 -0
- package/lib/objects/control.js +101 -18
- package/lib/objects/profile.d.ts +58 -0
- package/lib/objects/profile.js +60 -1
- package/lib/parsers/json.d.ts +32 -0
- package/lib/parsers/json.js +36 -5
- package/lib/parsers/oval.d.ts +28 -0
- package/lib/parsers/oval.js +46 -6
- package/lib/parsers/xccdf.d.ts +36 -1
- package/lib/parsers/xccdf.js +194 -52
- package/lib/utilities/diff.d.ts +42 -0
- package/lib/utilities/diff.js +50 -11
- package/lib/utilities/diffMarkdown.d.ts +13 -0
- package/lib/utilities/diffMarkdown.js +24 -12
- package/lib/utilities/global.d.ts +53 -0
- package/lib/utilities/global.js +92 -13
- package/lib/utilities/logging.d.ts +1 -1
- package/lib/utilities/logging.js +8 -5
- package/lib/utilities/update.d.ts +60 -1
- package/lib/utilities/update.js +131 -41
- package/lib/utilities/xccdf.d.ts +122 -2
- package/lib/utilities/xccdf.js +176 -25
- package/package.json +17 -17
package/lib/parsers/xccdf.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.extractAllRules = extractAllRules;
|
|
4
|
+
exports.extractAllComplexChecks = extractAllComplexChecks;
|
|
5
|
+
exports.processXCCDF = processXCCDF;
|
|
4
6
|
const tslib_1 = require("tslib");
|
|
5
7
|
const profile_1 = tslib_1.__importDefault(require("../objects/profile"));
|
|
6
8
|
const xccdf_1 = require("../utilities/xccdf");
|
|
@@ -9,6 +11,12 @@ const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
|
|
9
11
|
const CciNistMappingData_1 = require("../mappings/CciNistMappingData");
|
|
10
12
|
const pretty_1 = tslib_1.__importDefault(require("pretty"));
|
|
11
13
|
const logging_1 = require("../utilities/logging");
|
|
14
|
+
/**
|
|
15
|
+
* Extracts all rules from the given benchmark groups, including nested groups.
|
|
16
|
+
*
|
|
17
|
+
* @param groups - An array of benchmark groups to extract rules from.
|
|
18
|
+
* @returns An array of contextualized rules, each rule includes its parent group context.
|
|
19
|
+
*/
|
|
12
20
|
function extractAllRules(groups) {
|
|
13
21
|
const rules = [];
|
|
14
22
|
groups.forEach((group) => {
|
|
@@ -26,7 +34,16 @@ function extractAllRules(groups) {
|
|
|
26
34
|
});
|
|
27
35
|
return rules;
|
|
28
36
|
}
|
|
29
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Extracts all nested complex checks from a given `RuleComplexCheck` object.
|
|
39
|
+
*
|
|
40
|
+
* This function recursively traverses the `complex-check` property of the input
|
|
41
|
+
* `RuleComplexCheck` object and collects all nested complex checks into a flat array.
|
|
42
|
+
* Each complex check in the resulting array will have its own `complex-check` property omitted.
|
|
43
|
+
*
|
|
44
|
+
* @param complexCheck - The `RuleComplexCheck` object to extract complex checks from.
|
|
45
|
+
* @returns An array of `RuleComplexCheck` objects with the `complex-check` property omitted.
|
|
46
|
+
*/
|
|
30
47
|
function extractAllComplexChecks(complexCheck) {
|
|
31
48
|
const complexChecks = [lodash_1.default.omit(complexCheck, 'complex-check')];
|
|
32
49
|
if (complexCheck['complex-check']) {
|
|
@@ -37,25 +54,81 @@ function extractAllComplexChecks(complexCheck) {
|
|
|
37
54
|
}
|
|
38
55
|
return complexChecks;
|
|
39
56
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Ensures that the input is decoded to a string value.
|
|
59
|
+
*
|
|
60
|
+
* This function takes an input which can be either a string or an array of `InputTextLang` objects.
|
|
61
|
+
* If the input is a string, it returns the input as is.
|
|
62
|
+
* If the input is an array, it attempts to retrieve the `#text` property from the first element of the array.
|
|
63
|
+
* If the input is neither a string nor an array, it attempts to retrieve the `#text` property from the input.
|
|
64
|
+
* If the `#text` property is not found, it returns the provided default value.
|
|
65
|
+
*
|
|
66
|
+
* @param input - The input value which can be a string or an array of `InputTextLang` objects.
|
|
67
|
+
* @param defaultValue - The default value to return if the `#text` property is not found.
|
|
68
|
+
* @returns The decoded string value or the default value.
|
|
69
|
+
*/
|
|
70
|
+
function ensureDecodedXMLStringValue(input, defaultValue) {
|
|
71
|
+
return lodash_1.default.isString(input)
|
|
72
|
+
? input
|
|
73
|
+
: lodash_1.default.isArray(input)
|
|
74
|
+
? lodash_1.default.get(input, '[0].#text', defaultValue)
|
|
75
|
+
: lodash_1.default.get(input, '#text', defaultValue);
|
|
43
76
|
}
|
|
44
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Processes an XCCDF XML string and converts it into a Profile object.
|
|
79
|
+
* NOTE: We are using the fast xml parser (FXP) V4 which requires to specify
|
|
80
|
+
* which Whether a single tag should be parsed as an array or an object,
|
|
81
|
+
* it can't be decided by FXP. We process every tag as an array, this is
|
|
82
|
+
* the reason there are numerous tag test, were array index zero [0] is
|
|
83
|
+
* tested.
|
|
84
|
+
*
|
|
85
|
+
* @param xml - The XCCDF XML string to process.
|
|
86
|
+
* @param removeNewlines - A flag indicating whether to remove newlines from the processed data.
|
|
87
|
+
* @param useRuleId - Specifies the rule ID format to use. Can be 'group', 'rule', 'version', or 'cis'.
|
|
88
|
+
* @param ovalDefinitions - Optional OVAL definitions to use for resolving values.
|
|
89
|
+
* @returns A Profile object representing the processed XCCDF data.
|
|
90
|
+
* @throws Will throw an error if the XCCDF file is not properly formatted or if required data is missing.
|
|
91
|
+
*/
|
|
45
92
|
function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
46
|
-
const logger = (0, logging_1.createWinstonLogger)();
|
|
93
|
+
const logger = (0, logging_1.createWinstonLogger)('ts-inspec-objects');
|
|
47
94
|
const parsedXML = (0, xccdf_1.convertEncodedXmlIntoJson)(xml);
|
|
48
95
|
if (parsedXML.Benchmark === undefined) {
|
|
49
96
|
throw new Error('Could not process the XCCDF file, check the input to make sure this is a properly formatted XCCDF file.');
|
|
50
97
|
}
|
|
98
|
+
// Extracts all rules from the given benchmark groups.
|
|
51
99
|
const rules = extractAllRules(parsedXML.Benchmark[0].Group);
|
|
100
|
+
// Variable used to store the profile data.
|
|
101
|
+
// The name is the benchmark Id, title and summary are from benchmark.
|
|
52
102
|
const profile = new profile_1.default({
|
|
53
|
-
name: parsedXML.Benchmark[0]['@_id'],
|
|
54
|
-
title: parsedXML.Benchmark[0].title[0]['#text'],
|
|
55
|
-
summary: parsedXML.Benchmark[0].description[0]['#text']
|
|
103
|
+
//name: parsedXML.Benchmark[0]['@_id'],
|
|
104
|
+
// title: (parsedXML.Benchmark[0].title[0] as FrontMatter)['#text'],
|
|
105
|
+
// summary: (parsedXML.Benchmark[0].description[0] as RationaleElement)['#text']
|
|
106
|
+
name: Array.isArray(parsedXML.Benchmark[0]['@_id'])
|
|
107
|
+
? parsedXML.Benchmark[0]['@_id'].map(n => n['@_id']).join(' ') === ''
|
|
108
|
+
? parsedXML.Benchmark[0]['@_id'].map(n => n).join(' ')
|
|
109
|
+
: parsedXML.Benchmark[0]['@_id'].join(' ')
|
|
110
|
+
: parsedXML.Benchmark[0]['@_id'],
|
|
111
|
+
title: Array.isArray(parsedXML.Benchmark[0].title)
|
|
112
|
+
? parsedXML.Benchmark[0].title.map(t => t['#text']).join(' ') === ''
|
|
113
|
+
? parsedXML.Benchmark[0].title.map(t => t).join(' ')
|
|
114
|
+
: parsedXML.Benchmark[0].title.map(t => t['#text']).join(' ')
|
|
115
|
+
: parsedXML.Benchmark[0].title,
|
|
116
|
+
summary: Array.isArray(parsedXML.Benchmark[0].description)
|
|
117
|
+
? parsedXML.Benchmark[0].description.map(d => d['#text']).join(' ') === ''
|
|
118
|
+
? parsedXML.Benchmark[0].description.map(d => d['p'] || '').join(' ') === ''
|
|
119
|
+
? parsedXML.Benchmark[0].description.map(d => d).join(' ')
|
|
120
|
+
: parsedXML.Benchmark[0].description.map(d => d['p'] || '').join(' ')
|
|
121
|
+
: parsedXML.Benchmark[0].description.map(d => d['#text']).join(' ')
|
|
122
|
+
: parsedXML.Benchmark[0].description
|
|
56
123
|
});
|
|
124
|
+
// Process each rule, extracting the necessary
|
|
125
|
+
// data and save it to the profile variable.
|
|
57
126
|
rules.forEach(rule => {
|
|
58
127
|
var _a, _b, _c;
|
|
128
|
+
// The description tag contains the following tags:
|
|
129
|
+
// "FalsePositives", "FalseNegatives", "Documentable", "Mitigations",
|
|
130
|
+
// "SeverityOverrideGuidance", "PotentialImpacts", "ThirdPartyTools",
|
|
131
|
+
// "MitigationControl", "Responsibility", "IAControls"
|
|
59
132
|
let extractedDescription;
|
|
60
133
|
if (typeof rule.description === 'object') {
|
|
61
134
|
if (Array.isArray(rule.description) && lodash_1.default.get(rule, "description[0]['#text']")) {
|
|
@@ -69,6 +142,10 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
69
142
|
if (Array.isArray(lodash_1.default.get(rule.description, '[0].p'))) {
|
|
70
143
|
const joinedDescriptions = lodash_1.default.get(rule.description, '[0].p');
|
|
71
144
|
extractedDescription = (0, pretty_1.default)(joinedDescriptions.join('\n\n'));
|
|
145
|
+
extractedDescription = (0, xccdf_1.removeHtmlTags)(extractedDescription).replace('\n', ' ');
|
|
146
|
+
}
|
|
147
|
+
else if (Array.isArray(rule.description)) {
|
|
148
|
+
extractedDescription = (0, xccdf_1.convertEncodedHTMLIntoJson)(rule.description[0]);
|
|
72
149
|
}
|
|
73
150
|
else {
|
|
74
151
|
extractedDescription = JSON.stringify(rule.description);
|
|
@@ -79,38 +156,67 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
79
156
|
else {
|
|
80
157
|
extractedDescription = (0, xccdf_1.convertEncodedHTMLIntoJson)(rule.description);
|
|
81
158
|
}
|
|
159
|
+
// Create a new control object and populate it with the necessary data.
|
|
82
160
|
const control = new control_1.default();
|
|
161
|
+
// Update the control Id with the appropriate value based on the rule id.
|
|
83
162
|
switch (useRuleId) {
|
|
84
163
|
case 'group':
|
|
85
|
-
control.id = rule.group['@_id'];
|
|
164
|
+
control.id = rule.group['@_id'].toString();
|
|
86
165
|
break;
|
|
87
166
|
case 'rule':
|
|
88
|
-
if (rule['@_id'].toLowerCase().startsWith('sv')) {
|
|
89
|
-
control.id = rule['@_id'].split('r')[0];
|
|
167
|
+
if (rule['@_id'][0].toLowerCase().startsWith('sv')) {
|
|
168
|
+
control.id = rule['@_id'][0].split('r')[0];
|
|
90
169
|
}
|
|
91
170
|
else {
|
|
92
|
-
control.id = rule['@_id'];
|
|
171
|
+
control.id = rule['@_id'][0];
|
|
93
172
|
}
|
|
94
173
|
break;
|
|
95
174
|
case 'version':
|
|
96
|
-
|
|
175
|
+
if (rule.version !== undefined) {
|
|
176
|
+
(lodash_1.default.isArray(rule.version))
|
|
177
|
+
? control.id = rule.version[0]
|
|
178
|
+
: control.id = rule.version;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
throw new Error('The rule type "version" did not provide an identification (Id) value');
|
|
182
|
+
}
|
|
97
183
|
break;
|
|
98
|
-
case 'cis':
|
|
99
|
-
//
|
|
184
|
+
case 'cis': {
|
|
185
|
+
// Regex explained
|
|
186
|
+
// \d:
|
|
187
|
+
// matches a single digit (0-9), the required starting point of the match.
|
|
188
|
+
// (\d?):
|
|
189
|
+
// matches an optional digit, there are three of these in sequence
|
|
190
|
+
// (.\d(\d?)(\d?)(\d?))?:
|
|
191
|
+
// matches an optional group that starts with a period (.) followed
|
|
192
|
+
// by one digit and up to three additional optional digits
|
|
193
|
+
// The pattern is repeated four times to match between zero and four
|
|
194
|
+
// groups of a period followed by one required digit and up to three
|
|
195
|
+
// additional optional digits. The pattern matches:
|
|
196
|
+
// 1, 123, 1.2, 1.234, 1.2.3.4.5, or 1.23.456.7.89
|
|
100
197
|
const controlIdRegex = /\d(\d?)(\d?)(\d?)(.\d(\d?)(\d?)(\d?))?(.\d(\d?)(\d?)(\d?))?(.\d(\d?)(\d?)(\d?))?(.\d(\d?)(\d?)(\d?))?/g;
|
|
101
|
-
// eslint-disable-next-line no-case-declarations
|
|
102
198
|
const controlIdMatch = controlIdRegex.exec(rule['@_id']);
|
|
103
199
|
if (controlIdMatch) {
|
|
104
200
|
control.id = controlIdMatch[0];
|
|
105
201
|
}
|
|
106
202
|
else {
|
|
107
|
-
throw new Error(`Could not parse control ID from rule ID: ${rule['@_id']}. Expecting something in this example format:
|
|
203
|
+
throw new Error(`Could not parse control ID from rule ID: ${rule['@_id']}. Expecting something in this example format: xccdf_org.cisecurity.benchmarks_rule_1.1.11_Rule_title_summary`);
|
|
108
204
|
}
|
|
109
205
|
break;
|
|
206
|
+
}
|
|
110
207
|
default:
|
|
111
|
-
throw new Error('useRuleId must be one of "group", "rule", or "
|
|
208
|
+
throw new Error('useRuleId must be one of "group", "rule", "version" for STIG benchmarks, or "cis" for CIS benchmarks');
|
|
209
|
+
}
|
|
210
|
+
if (!(lodash_1.default.isArray(rule.title) && rule.title.length === 1)) {
|
|
211
|
+
throw new Error('Rule title is not an array of length 1. Investigate if the file is in the proper format.');
|
|
112
212
|
}
|
|
113
|
-
control
|
|
213
|
+
// Update the control title with the rule.tile content if a rule severity
|
|
214
|
+
// exists after removing any special characters, otherwise set the control
|
|
215
|
+
// title to [[[MISSING SEVERITY FROM BENCHMARK]]], undefined title.
|
|
216
|
+
control.title = (0, xccdf_1.removeXMLSpecialCharacters)(rule['@_severity'] || rule['@_weight']
|
|
217
|
+
? ensureDecodedXMLStringValue(rule.title[0], 'undefined title')
|
|
218
|
+
: `[[[MISSING SEVERITY or WEIGHT FROM BENCHMARK]]] ${ensureDecodedXMLStringValue(rule.title[0], 'undefined title')}`);
|
|
219
|
+
// Update the control description (desc) with the extracted description content
|
|
114
220
|
if (typeof extractedDescription === 'object' && !Array.isArray(extractedDescription)) {
|
|
115
221
|
control.desc = ((_a = extractedDescription.VulnDiscussion) === null || _a === void 0 ? void 0 : _a.split('Satisfies: ')[0]) || '';
|
|
116
222
|
}
|
|
@@ -123,13 +229,16 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
123
229
|
else {
|
|
124
230
|
logger.warn(`Invalid value for extracted description: ${extractedDescription}`);
|
|
125
231
|
}
|
|
126
|
-
control
|
|
232
|
+
// Update the control impact with the severity value from the rule,
|
|
233
|
+
// default to medium (0.5) if not found.
|
|
234
|
+
control.impact = (0, xccdf_1.severityStringToImpact)(rule['@_severity'] || 'medium');
|
|
127
235
|
if (!control.descs || Array.isArray(control.descs)) {
|
|
128
236
|
control.descs = {};
|
|
129
237
|
}
|
|
238
|
+
// Update the control descriptions (descs) check with the check text from the rule,
|
|
130
239
|
if (rule.check) {
|
|
131
240
|
if (rule.check.some((ruleValue) => 'check-content' in ruleValue)) {
|
|
132
|
-
control.descs.check = (0, xccdf_1.removeXMLSpecialCharacters)(rule.check ? rule.check[0]['check-content'] : 'Missing description');
|
|
241
|
+
control.descs.check = (0, xccdf_1.removeXMLSpecialCharacters)(rule.check ? rule.check[0]['check-content'][0] : 'Missing description');
|
|
133
242
|
control.tags.check_id = rule.check[0]['@_system'];
|
|
134
243
|
}
|
|
135
244
|
else if (rule.check.some((ruleValue) => 'check-content-ref' in ruleValue) && ovalDefinitions) {
|
|
@@ -145,7 +254,8 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
145
254
|
}
|
|
146
255
|
}
|
|
147
256
|
if (referenceID && referenceID in ovalDefinitions) {
|
|
148
|
-
|
|
257
|
+
// May need to further check if ovalDefinitions[referenceID].metadata[0].title[0] are not populated?
|
|
258
|
+
control.descs.check = (0, xccdf_1.removeXMLSpecialCharacters)(ovalDefinitions[referenceID].metadata[0].title[0]);
|
|
149
259
|
}
|
|
150
260
|
else if (referenceID) {
|
|
151
261
|
logger.warn(`Could not find OVAL definition for ${referenceID}`);
|
|
@@ -164,7 +274,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
164
274
|
if (complexCheck.check) {
|
|
165
275
|
complexCheck.check.forEach((check) => {
|
|
166
276
|
var _a;
|
|
167
|
-
if ((_a = check['@_system']) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('oval')) {
|
|
277
|
+
if ((_a = check['@_system']) === null || _a === void 0 ? void 0 : _a.toString().toLowerCase().includes('oval')) {
|
|
168
278
|
const ovalReference = check['check-content-ref'][0]['@_name'];
|
|
169
279
|
if (!ovalDefinitions) {
|
|
170
280
|
logger.warn(`Missing OVAL definitions! Unable to process OVAL reference: ${ovalReference}`);
|
|
@@ -200,15 +310,22 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
200
310
|
control.descs.check = checkTexts.join('\n');
|
|
201
311
|
}
|
|
202
312
|
}
|
|
313
|
+
// Update the control descriptions (descs) fix with content from the rule
|
|
314
|
+
// fixtest, if not found, defaults to "Missing fix text"
|
|
203
315
|
if (lodash_1.default.get(rule.fixtext, '[0]["#text"]')) {
|
|
204
316
|
control.descs.fix = (0, xccdf_1.removeXMLSpecialCharacters)(rule.fixtext[0]['#text']);
|
|
205
317
|
}
|
|
206
|
-
else if (typeof rule.fixtext === '
|
|
207
|
-
|
|
318
|
+
else if (typeof rule.fixtext === 'undefined') {
|
|
319
|
+
if (rule.fix && rule.fix[0]) {
|
|
320
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)(rule.fix[0]['#text'] || 'Missing fix text');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (typeof rule.fixtext[0] === 'string') {
|
|
324
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)(rule.fixtext[0]);
|
|
208
325
|
}
|
|
209
|
-
else if (typeof rule.fixtext === 'object') {
|
|
210
|
-
if (Array.isArray(rule.fixtext)) {
|
|
211
|
-
control.descs.fix = (0, xccdf_1.
|
|
326
|
+
else if (typeof rule.fixtext[0] === 'object') {
|
|
327
|
+
if (Array.isArray(rule.fixtext[0])) {
|
|
328
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)((0, pretty_1.default)((0, xccdf_1.convertJsonIntoXML)(rule.fixtext[0].map((fixtext) => {
|
|
212
329
|
if (fixtext.div) {
|
|
213
330
|
return fixtext.div;
|
|
214
331
|
}
|
|
@@ -218,18 +335,14 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
218
335
|
}))));
|
|
219
336
|
}
|
|
220
337
|
else {
|
|
221
|
-
control.descs.fix = (0, xccdf_1.removeXMLSpecialCharacters)((0, pretty_1.default)((0, xccdf_1.convertJsonIntoXML)(rule.fixtext)));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else if (typeof rule.fixtext === 'undefined') {
|
|
225
|
-
if (rule.fix && rule.fix[0]) {
|
|
226
|
-
control.descs.fix = (0, xccdf_1.removeXMLSpecialCharacters)(rule.fix[0]['#text'] || 'Missing fix text');
|
|
338
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)((0, xccdf_1.removeXMLSpecialCharacters)((0, pretty_1.default)((0, xccdf_1.convertJsonIntoXML)(rule.fixtext)))).replace('\n', ' ').trim();
|
|
227
339
|
}
|
|
228
340
|
}
|
|
229
341
|
else {
|
|
230
342
|
control.descs.fix = 'Missing fix text';
|
|
231
343
|
}
|
|
232
|
-
control
|
|
344
|
+
// Update the control tags base on corresponding rule tags.
|
|
345
|
+
control.tags.severity = (0, xccdf_1.impactNumberToSeverityString)((0, xccdf_1.severityStringToImpact)(rule['@_severity'] || 'medium'));
|
|
233
346
|
control.tags.gid = rule.group['@_id'],
|
|
234
347
|
control.tags.rid = rule['@_id'];
|
|
235
348
|
control.tags.stig_id = rule['version'];
|
|
@@ -237,7 +350,10 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
237
350
|
control.tags.gtitle = (0, xccdf_1.removeXMLSpecialCharacters)(rule.group.title);
|
|
238
351
|
}
|
|
239
352
|
else {
|
|
240
|
-
|
|
353
|
+
const gtitle = lodash_1.default.get(rule.group, 'title[0].#text', 'undefined title') === 'undefined title'
|
|
354
|
+
? lodash_1.default.get(rule.group, 'title[0]', 'undefined title')
|
|
355
|
+
: lodash_1.default.get(rule.group, 'title[0].#text', 'undefined title');
|
|
356
|
+
control.tags.gtitle = typeof gtitle === 'string' ? gtitle : gtitle['#text'] || 'undefined title';
|
|
241
357
|
}
|
|
242
358
|
if (rule['fix'] && rule['fix'].length > 0) {
|
|
243
359
|
control.tags.fix_id = rule['fix'][0]['@_id'];
|
|
@@ -245,11 +361,20 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
245
361
|
if (rule['rationale']) {
|
|
246
362
|
control.tags.rationale = rule['rationale'][0]['#text'];
|
|
247
363
|
}
|
|
364
|
+
// The description tag contains the following tags as well:
|
|
365
|
+
// "FalsePositives", "FalseNegatives", "Documentable", "Mitigations",
|
|
366
|
+
// "SeverityOverrideGuidance", "PotentialImpacts", "ThirdPartyTools",
|
|
367
|
+
// "MitigationControl", "Responsibility", "IAControls"
|
|
248
368
|
if (typeof extractedDescription === 'object') {
|
|
249
|
-
control.tags.satisfies =
|
|
369
|
+
control.tags.satisfies =
|
|
370
|
+
((_b = extractedDescription.VulnDiscussion) === null || _b === void 0 ? void 0 : _b.includes('Satisfies: ')) && extractedDescription.VulnDiscussion.split('Satisfies: ').length >= 1
|
|
371
|
+
? extractedDescription.VulnDiscussion.split('Satisfies: ')[1].split(',').map(satisfaction => satisfaction.trim())
|
|
372
|
+
: undefined;
|
|
250
373
|
control.tags.false_negatives = extractedDescription.FalseNegatives || undefined;
|
|
251
374
|
control.tags.false_positives = extractedDescription.FalsePositives || undefined;
|
|
252
|
-
control.tags.documentable = typeof extractedDescription.Documentable === 'boolean'
|
|
375
|
+
control.tags.documentable = typeof extractedDescription.Documentable === 'boolean'
|
|
376
|
+
? extractedDescription.Documentable
|
|
377
|
+
: undefined;
|
|
253
378
|
control.tags.mitigations = extractedDescription.Mitigations || undefined;
|
|
254
379
|
control.tags.severity_override_guidance = extractedDescription.SeverityOverrideGuidance || undefined;
|
|
255
380
|
control.tags.potential_impacts = extractedDescription.PotentialImpacts || undefined;
|
|
@@ -259,34 +384,46 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
259
384
|
control.tags.responsibility = extractedDescription.Responsibility || undefined;
|
|
260
385
|
control.tags.ia_controls = extractedDescription.IAControls || undefined;
|
|
261
386
|
}
|
|
387
|
+
// Ensure that tags inside the tags array are not an array
|
|
262
388
|
control.tags = lodash_1.default.mapValues(lodash_1.default.omitBy(control.tags, (value) => value === undefined), (value) => {
|
|
263
|
-
if (
|
|
389
|
+
if (value && Array.isArray(value)) {
|
|
390
|
+
if (Array.isArray(value[0])) {
|
|
391
|
+
return (0, xccdf_1.removeXMLSpecialCharacters)(value[0][0]);
|
|
392
|
+
}
|
|
393
|
+
else if (value.length > 1) {
|
|
394
|
+
return value;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
return (0, xccdf_1.removeXMLSpecialCharacters)(value[0]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (typeof value === 'string') {
|
|
264
401
|
return (0, xccdf_1.removeXMLSpecialCharacters)(value);
|
|
265
402
|
}
|
|
266
403
|
else {
|
|
267
404
|
return value;
|
|
268
405
|
}
|
|
269
406
|
});
|
|
270
|
-
// Get all identifiers from the rule
|
|
407
|
+
// Get all identifiers from the rule; cci, nist, and legacy
|
|
271
408
|
if (rule.ident) {
|
|
272
409
|
rule.ident.forEach((identifier) => {
|
|
273
410
|
var _a, _b, _c;
|
|
274
411
|
// Get CCIs
|
|
275
|
-
if (identifier['@_system'].toLowerCase().includes('cci')) {
|
|
412
|
+
if (identifier['@_system'][0].toLowerCase().includes('cci')) {
|
|
276
413
|
if (!('cci' in control.tags)) {
|
|
277
414
|
control.tags.cci = [];
|
|
278
415
|
}
|
|
279
416
|
(_a = control.tags.cci) === null || _a === void 0 ? void 0 : _a.push(identifier['#text']);
|
|
280
417
|
}
|
|
281
418
|
// Get legacy identifiers
|
|
282
|
-
else if (identifier['@_system'].toLowerCase().includes('legacy')) {
|
|
419
|
+
else if (identifier['@_system'][0].toLowerCase().includes('legacy')) {
|
|
283
420
|
if (!('legacy' in control.tags)) {
|
|
284
421
|
control.tags.legacy = [];
|
|
285
422
|
}
|
|
286
423
|
(_b = control.tags.legacy) === null || _b === void 0 ? void 0 : _b.push(identifier['#text']);
|
|
287
424
|
}
|
|
288
425
|
// Get NIST identifiers
|
|
289
|
-
else if (identifier['@_system'].toLowerCase().includes('nist')) {
|
|
426
|
+
else if (identifier['@_system'].toString().toLowerCase().includes('nist')) {
|
|
290
427
|
if (!('nist' in control.tags)) {
|
|
291
428
|
control.tags.nist = [];
|
|
292
429
|
}
|
|
@@ -294,17 +431,18 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
294
431
|
}
|
|
295
432
|
});
|
|
296
433
|
}
|
|
434
|
+
// Update control references with content from the benchmark rule object
|
|
297
435
|
(_c = rule.reference) === null || _c === void 0 ? void 0 : _c.forEach((reference) => {
|
|
298
|
-
var _a, _b, _c, _d;
|
|
436
|
+
var _a, _b, _c, _d, _e;
|
|
299
437
|
if (lodash_1.default.get(reference, '@_href') === '') {
|
|
300
|
-
(_a = control.refs) === null || _a === void 0 ? void 0 : _a.push(lodash_1.default.get(reference, '#text'));
|
|
438
|
+
(_a = control.refs) === null || _a === void 0 ? void 0 : _a.push(lodash_1.default.get(reference, '#text', 'undefined href'));
|
|
301
439
|
}
|
|
302
440
|
else {
|
|
303
441
|
try {
|
|
304
442
|
const referenceText = lodash_1.default.get(reference, '#text') || '';
|
|
305
443
|
const referenceURL = lodash_1.default.get(reference, '@_href') || '';
|
|
306
444
|
if (referenceURL) {
|
|
307
|
-
const parsedURL = new URL(lodash_1.default.get(reference, '@_href'));
|
|
445
|
+
const parsedURL = new URL(lodash_1.default.get(reference, '@_href', 'undefined href'));
|
|
308
446
|
if (parsedURL.protocol.toLowerCase().includes('http') || parsedURL.protocol.toLowerCase().includes('https')) {
|
|
309
447
|
(_b = control.refs) === null || _b === void 0 ? void 0 : _b.push({
|
|
310
448
|
ref: referenceText,
|
|
@@ -320,7 +458,13 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
320
458
|
}
|
|
321
459
|
else {
|
|
322
460
|
if ('title' in reference) {
|
|
323
|
-
|
|
461
|
+
const title = lodash_1.default.get(reference, 'title');
|
|
462
|
+
if (Array.isArray(title)) {
|
|
463
|
+
(_d = control.refs) === null || _d === void 0 ? void 0 : _d.push(title[0]);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
(_e = control.refs) === null || _e === void 0 ? void 0 : _e.push(lodash_1.default.get(reference, 'title'));
|
|
467
|
+
}
|
|
324
468
|
}
|
|
325
469
|
}
|
|
326
470
|
// Add the reference to the control tags when separated by §
|
|
@@ -341,8 +485,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
341
485
|
}
|
|
342
486
|
}
|
|
343
487
|
else {
|
|
344
|
-
logger.warn('Reference parts of invalid length:');
|
|
345
|
-
logger.info(referenceParts);
|
|
488
|
+
logger.warn('Reference parts of invalid length: ', referenceParts);
|
|
346
489
|
}
|
|
347
490
|
}
|
|
348
491
|
}
|
|
@@ -370,4 +513,3 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
370
513
|
profile.controls = lodash_1.default.sortBy(profile.controls, 'id');
|
|
371
514
|
return profile.toUnformattedObject();
|
|
372
515
|
}
|
|
373
|
-
exports.processXCCDF = processXCCDF;
|
package/lib/utilities/diff.d.ts
CHANGED
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
import Profile from '../objects/profile';
|
|
2
2
|
import { ProfileDiff } from '../types/diff';
|
|
3
3
|
import winston from 'winston';
|
|
4
|
+
/**
|
|
5
|
+
* Removes newlines from all string values within a nested object.
|
|
6
|
+
*
|
|
7
|
+
* This function recursively traverses the provided object and replaces
|
|
8
|
+
* newline characters (`\n`) in string values with the placeholder `{{{{newlineHERE}}}}`.
|
|
9
|
+
* It also trims any leading or trailing whitespace from the string values.
|
|
10
|
+
*
|
|
11
|
+
* @param control - The object from which to remove newlines. If not provided,
|
|
12
|
+
* an empty object is returned.
|
|
13
|
+
* @returns A new object with newlines removed from all string values.
|
|
14
|
+
*/
|
|
4
15
|
export declare function removeNewlines(control?: Record<string, unknown>): Record<string, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Processes a diff object to ignore formatting differences such as whitespace.
|
|
18
|
+
* Goal is to use a linter for the formatting and compare characters without
|
|
19
|
+
* whitespaces here.
|
|
20
|
+
*
|
|
21
|
+
* The function performs the following:
|
|
22
|
+
* - If the diff value has a `__new` property, it compares the `__new` and
|
|
23
|
+
* `__old` values after removing whitespace. If they are different, it sets
|
|
24
|
+
* the result to the `__new` value.
|
|
25
|
+
* - If the diff value is an array, it maps and filters the array to include
|
|
26
|
+
* only the new values.
|
|
27
|
+
* - If the diff value is an object, it recursively processes the object.
|
|
28
|
+
* - If the key ends with `__deleted`, it ignores the value.
|
|
29
|
+
* - Otherwise, it sets the result to the diff value.
|
|
30
|
+
*
|
|
31
|
+
* @param diffData - The diff object containing differences to process.
|
|
32
|
+
* @returns A new object with formatting differences ignored.
|
|
33
|
+
*
|
|
34
|
+
|
|
35
|
+
*/
|
|
5
36
|
export declare function ignoreFormattingDiff(diffData: Record<string, unknown>): Record<string, unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* Computes the differences between two profiles and logs the process.
|
|
39
|
+
*
|
|
40
|
+
* @param fromProfile - The original profile to compare from.
|
|
41
|
+
* @param toProfile - The target profile to compare to.
|
|
42
|
+
* @param logger - The logger instance to use for logging information.
|
|
43
|
+
*
|
|
44
|
+
* @returns An object containing two properties:
|
|
45
|
+
* - `ignoreFormattingDiff`: The profile differences ignoring formatting changes.
|
|
46
|
+
* - `rawDiff`: The raw profile differences.
|
|
47
|
+
*/
|
|
6
48
|
export declare function diffProfile(fromProfile: Profile, toProfile: Profile, logger: winston.Logger): {
|
|
7
49
|
ignoreFormattingDiff: ProfileDiff;
|
|
8
50
|
rawDiff: Record<string, unknown>;
|
package/lib/utilities/diff.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.removeNewlines = removeNewlines;
|
|
4
|
+
exports.ignoreFormattingDiff = ignoreFormattingDiff;
|
|
5
|
+
exports.diffProfile = diffProfile;
|
|
4
6
|
const tslib_1 = require("tslib");
|
|
5
7
|
const json_diff_1 = require("json-diff");
|
|
6
8
|
const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
|
7
9
|
const update_1 = require("./update");
|
|
8
10
|
const global_1 = require("./global");
|
|
11
|
+
/**
|
|
12
|
+
* Removes newlines from all string values within a nested object.
|
|
13
|
+
*
|
|
14
|
+
* This function recursively traverses the provided object and replaces
|
|
15
|
+
* newline characters (`\n`) in string values with the placeholder `{{{{newlineHERE}}}}`.
|
|
16
|
+
* It also trims any leading or trailing whitespace from the string values.
|
|
17
|
+
*
|
|
18
|
+
* @param control - The object from which to remove newlines. If not provided,
|
|
19
|
+
* an empty object is returned.
|
|
20
|
+
* @returns A new object with newlines removed from all string values.
|
|
21
|
+
*/
|
|
9
22
|
function removeNewlines(control) {
|
|
10
23
|
if (!control) {
|
|
11
24
|
return {};
|
|
@@ -20,16 +33,34 @@ function removeNewlines(control) {
|
|
|
20
33
|
return value;
|
|
21
34
|
});
|
|
22
35
|
}
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Processes a diff object to ignore formatting differences such as whitespace.
|
|
38
|
+
* Goal is to use a linter for the formatting and compare characters without
|
|
39
|
+
* whitespaces here.
|
|
40
|
+
*
|
|
41
|
+
* The function performs the following:
|
|
42
|
+
* - If the diff value has a `__new` property, it compares the `__new` and
|
|
43
|
+
* `__old` values after removing whitespace. If they are different, it sets
|
|
44
|
+
* the result to the `__new` value.
|
|
45
|
+
* - If the diff value is an array, it maps and filters the array to include
|
|
46
|
+
* only the new values.
|
|
47
|
+
* - If the diff value is an object, it recursively processes the object.
|
|
48
|
+
* - If the key ends with `__deleted`, it ignores the value.
|
|
49
|
+
* - Otherwise, it sets the result to the diff value.
|
|
50
|
+
*
|
|
51
|
+
* @param diffData - The diff object containing differences to process.
|
|
52
|
+
* @returns A new object with formatting differences ignored.
|
|
53
|
+
*
|
|
54
|
+
|
|
55
|
+
*/
|
|
25
56
|
function ignoreFormattingDiff(diffData) {
|
|
26
57
|
return lodash_1.default.transform(diffData, (result, diffValue, key) => {
|
|
27
58
|
if (lodash_1.default.has(diffValue, '__new')) {
|
|
28
59
|
// Remove any trailing space
|
|
29
60
|
if (typeof lodash_1.default.get(diffValue, '__new') === 'string' &&
|
|
30
61
|
typeof lodash_1.default.get(diffValue, '__old') === 'string') {
|
|
31
|
-
if ((0, global_1.removeWhitespace)(lodash_1.default.get(diffValue, '__new')) !==
|
|
32
|
-
(0, global_1.removeWhitespace)(lodash_1.default.get(diffValue, '__old'))) {
|
|
62
|
+
if ((0, global_1.removeWhitespace)(lodash_1.default.get(diffValue, '__new', 'undefined')) !==
|
|
63
|
+
(0, global_1.removeWhitespace)(lodash_1.default.get(diffValue, '__old', 'undefined'))) {
|
|
33
64
|
lodash_1.default.set(result, key, lodash_1.default.get(diffValue, '__new'));
|
|
34
65
|
}
|
|
35
66
|
}
|
|
@@ -53,9 +84,20 @@ function ignoreFormattingDiff(diffData) {
|
|
|
53
84
|
}
|
|
54
85
|
});
|
|
55
86
|
}
|
|
56
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Computes the differences between two profiles and logs the process.
|
|
89
|
+
*
|
|
90
|
+
* @param fromProfile - The original profile to compare from.
|
|
91
|
+
* @param toProfile - The target profile to compare to.
|
|
92
|
+
* @param logger - The logger instance to use for logging information.
|
|
93
|
+
*
|
|
94
|
+
* @returns An object containing two properties:
|
|
95
|
+
* - `ignoreFormattingDiff`: The profile differences ignoring formatting changes.
|
|
96
|
+
* - `rawDiff`: The raw profile differences.
|
|
97
|
+
*/
|
|
57
98
|
function diffProfile(fromProfile, toProfile, logger) {
|
|
58
99
|
var _a;
|
|
100
|
+
logger.info(`Processing diff between: ${fromProfile.name}(v:${fromProfile.version}) and: ${toProfile.name}(v:${toProfile.version})`);
|
|
59
101
|
const profileDiff = {
|
|
60
102
|
addedControlIDs: [],
|
|
61
103
|
removedControlIDs: [],
|
|
@@ -72,9 +114,7 @@ function diffProfile(fromProfile, toProfile, logger) {
|
|
|
72
114
|
addedControls: {},
|
|
73
115
|
changedControls: {},
|
|
74
116
|
};
|
|
75
|
-
const fromControlIDs = fromProfile.controls
|
|
76
|
-
.map((control) => control.id)
|
|
77
|
-
.sort();
|
|
117
|
+
const fromControlIDs = fromProfile.controls.map((control) => control.id).sort();
|
|
78
118
|
const toControlIDs = toProfile.controls.map((control) => control.id).sort();
|
|
79
119
|
// Find new controls
|
|
80
120
|
const controlIDDiff = (_a = (0, json_diff_1.diff)(fromControlIDs, toControlIDs)) === null || _a === void 0 ? void 0 : _a.filter((item) => !(item.length === 1 && item[0] === ' '));
|
|
@@ -95,12 +135,12 @@ function diffProfile(fromProfile, toProfile, logger) {
|
|
|
95
135
|
const controlDiff = lodash_1.default.omit((0, json_diff_1.diff)(existingControl, newControl), 'code__deleted');
|
|
96
136
|
// logger.info("CONTROL DIFF:" + JSON.stringify(controlDiff, null, 2))
|
|
97
137
|
const renamedControlIgnoredFormatting = ignoreFormattingDiff(controlDiff);
|
|
98
|
-
logger.info(JSON.stringify(renamedControlIgnoredFormatting));
|
|
99
138
|
profileDiff.changedControls[newControl.id] = renamedControlIgnoredFormatting;
|
|
100
139
|
profileDiff.changedControlIDs.push(newControl.id);
|
|
101
140
|
originalDiff.changedControls[newControl.id] = controlDiff;
|
|
102
141
|
originalDiff.changedControlIDs.push(newControl.id);
|
|
103
142
|
logger.verbose(`Control ${existingControl.id} has been updated to ${newControl.id}`);
|
|
143
|
+
logger.debug(`Updated control content: ${JSON.stringify(renamedControlIgnoredFormatting)}`);
|
|
104
144
|
}
|
|
105
145
|
else {
|
|
106
146
|
profileDiff.removedControlIDs.push(diffValue[1]);
|
|
@@ -147,4 +187,3 @@ function diffProfile(fromProfile, toProfile, logger) {
|
|
|
147
187
|
}
|
|
148
188
|
return { ignoreFormattingDiff: profileDiff, rawDiff: originalDiff };
|
|
149
189
|
}
|
|
150
|
-
exports.diffProfile = diffProfile;
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { ProfileDiff } from '../types/diff';
|
|
2
|
+
/**
|
|
3
|
+
* Generates a markdown representation of the differences between two profiles.
|
|
4
|
+
*
|
|
5
|
+
* The function processes the differences to create a renderable data structure
|
|
6
|
+
* that includes added controls, renamed controls, and updated properties such as
|
|
7
|
+
* checks, fixes, impacts, titles, and descriptions. It then uses a mustache template
|
|
8
|
+
* to render the markdown output.
|
|
9
|
+
*
|
|
10
|
+
* @param diff - An object containing the differences between two profiles.
|
|
11
|
+
* @param diff.ignoreFormattingDiff - The profile differences ignoring formatting changes.
|
|
12
|
+
* @param diff.rawDiff - The raw differences between the profiles.
|
|
13
|
+
* @returns A string containing the markdown representation of the differences.
|
|
14
|
+
*/
|
|
2
15
|
export declare function createDiffMarkdown(diff: {
|
|
3
16
|
ignoreFormattingDiff: ProfileDiff;
|
|
4
17
|
rawDiff: any;
|