@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.
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processXCCDF = exports.extractAllComplexChecks = exports.extractAllRules = void 0;
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
- exports.extractAllRules = extractAllRules;
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
- exports.extractAllComplexChecks = extractAllComplexChecks;
41
- function ensureDecodedXMLStringValue(input) {
42
- return lodash_1.default.get(input, '[0].#text') ? lodash_1.default.get(input, '[0].#text') : input;
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
- // Moving the newline removal to diff library rather than processXCCDF level
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
- control.id = rule.version;
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
- // eslint-disable-next-line no-case-declarations
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: 'xccdf_org.cisecurity.benchmarks_rule_1.1.11_Rule_title_summary`);
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 "version"');
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.title = (0, xccdf_1.removeXMLSpecialCharacters)(rule['@_severity'] ? ensureDecodedXMLStringValue(rule.title) : `[[[MISSING SEVERITY FROM BENCHMARK]]] ${ensureDecodedXMLStringValue(rule.title)}`);
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.impact = (0, xccdf_1.severityStringToImpact)(rule['@_severity'] || 'medium', rule.group['@_id']);
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
- control.descs.check = (0, xccdf_1.removeXMLSpecialCharacters)(ovalDefinitions[referenceID].metadata[0].title);
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 === 'string') {
207
- control.descs.fix = (0, xccdf_1.removeXMLSpecialCharacters)(rule.fixtext);
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.removeXMLSpecialCharacters)((0, pretty_1.default)((0, xccdf_1.convertJsonIntoXML)(rule.fixtext.map((fixtext) => {
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.tags.severity = (0, xccdf_1.impactNumberToSeverityString)((0, xccdf_1.severityStringToImpact)(rule['@_severity'] || 'medium', control.id || 'Unknown'));
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
- control.tags.gtitle = (0, xccdf_1.removeXMLSpecialCharacters)(lodash_1.default.get(rule.group, 'title[0].#text'));
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 = ((_b = extractedDescription.VulnDiscussion) === null || _b === void 0 ? void 0 : _b.includes('Satisfies: ')) && extractedDescription.VulnDiscussion.split('Satisfies: ').length >= 1 ? extractedDescription.VulnDiscussion.split('Satisfies: ')[1].split(',').map(satisfaction => satisfaction.trim()) : undefined;
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' ? extractedDescription.Documentable : undefined;
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 (typeof value === 'string') {
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
- (_d = control.refs) === null || _d === void 0 ? void 0 : _d.push(lodash_1.default.get(reference, 'title'));
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;
@@ -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>;
@@ -1,11 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.diffProfile = exports.ignoreFormattingDiff = exports.removeNewlines = void 0;
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
- exports.removeNewlines = removeNewlines;
24
- // Goal is to use a linter for the formatting and compare characters without whitespaces here
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
- exports.ignoreFormattingDiff = ignoreFormattingDiff;
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;