@mitre/inspec-objects 0.0.32 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ name-template: "$NEXT_PATCH_VERSION"
2
+ tag-template: "$NEXT_PATCH_VERSION"
3
+ categories:
4
+ - title: "New Features"
5
+ labels:
6
+ - "feature"
7
+ - "enhancement"
8
+ - title: "Bug Fixes"
9
+ labels:
10
+ - "fix"
11
+ - "bugfix"
12
+ - "bug"
13
+ - title: "Security Enhancements"
14
+ labels:
15
+ - "security"
16
+ - title: "Dependency Updates"
17
+ labels:
18
+ - "dependencies"
19
+ change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
20
+ template: |
21
+ $CHANGES
@@ -0,0 +1,16 @@
1
+ name: Draft Release
2
+
3
+ on:
4
+ push:
5
+ # branches to consider in the event; optional, defaults to all
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ update_draft_release:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ # Drafts your next Release notes as Pull Requests are merged into "master"
14
+ - uses: toolmantim/release-drafter@v5.2.0
15
+ env:
16
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -49,5 +49,6 @@ export default class Control {
49
49
  };
50
50
  constructor(data?: Partial<Control>);
51
51
  toUnformattedObject(): Control;
52
- toRuby(): string;
52
+ toString(): string;
53
+ toRuby(verbose?: boolean): string;
53
54
  }
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
5
5
  const lodash_1 = tslib_1.__importDefault(require("lodash"));
6
6
  const flat_1 = require("flat");
7
7
  const global_1 = require("../utilities/global");
8
+ const logging_1 = require("../utilities/logging");
8
9
  function objectifyDescriptions(descs) {
9
10
  if (Array.isArray(descs)) {
10
11
  const descriptions = {};
@@ -36,21 +37,86 @@ class Control {
36
37
  });
37
38
  return new Control((0, flat_1.unflatten)(flattened));
38
39
  }
39
- toRuby() {
40
+ // WIP - provides the ability to get the control in its raw form
41
+ toString() {
42
+ let result = '';
43
+ result += `control '${this.id}' do\n`;
44
+ if (this.title) {
45
+ result += ` title "${this.title}"\n`;
46
+ }
47
+ // This is the known 'default' description - on previous version this content was repeated on descriptions processed by "descs"
48
+ if (this.desc) {
49
+ result += ` desc "${this.desc}"\n`;
50
+ }
51
+ if (this.descs) {
52
+ Object.entries(this.descs).forEach(([key, subDesc]) => {
53
+ if (subDesc) {
54
+ result += ` desc '${key}', "${subDesc}"\n`;
55
+ }
56
+ });
57
+ }
58
+ if (this.impact) {
59
+ result += ` impact ${this.impact}\n`;
60
+ }
61
+ if (this.refs) {
62
+ this.refs.forEach((ref) => {
63
+ var _a;
64
+ if (typeof ref === 'string') {
65
+ result += ` ref "${ref}"\n`;
66
+ }
67
+ else {
68
+ result += ` ref ${((_a = ref.ref) === null || _a === void 0 ? void 0 : _a.toString()) || ''}, url: ${ref.url || ''}`;
69
+ }
70
+ });
71
+ }
72
+ Object.entries(this.tags).forEach(([tag, value]) => {
73
+ if (typeof value === 'object') {
74
+ if (Array.isArray(value) && typeof value[0] === 'string') {
75
+ result += ` tag ${tag}: ${JSON.stringify(value)}\n`;
76
+ }
77
+ else {
78
+ result += ` tag '${tag}': ${(value == null ? 'nil' : value)}\n`;
79
+ }
80
+ }
81
+ else if (typeof value === 'string') {
82
+ if (value.includes('"')) {
83
+ result += ` tag "${tag}": "${value}"\n`;
84
+ }
85
+ else {
86
+ result += ` tag '${tag}': '${value}'\n`;
87
+ }
88
+ }
89
+ });
90
+ if (this.describe) {
91
+ result += '\n';
92
+ result += this.describe;
93
+ }
94
+ if (!result.slice(-1).match('\n')) {
95
+ result += '\n';
96
+ }
97
+ result += 'end\n';
98
+ return result;
99
+ }
100
+ toRuby(verbose = true) {
101
+ const logger = (0, logging_1.createWinstonLogger)();
40
102
  let result = '';
41
103
  result += `control '${this.id}' do\n`;
42
104
  if (this.title) {
43
105
  result += ` title ${(0, global_1.escapeQuotes)(this.title)}\n`;
44
106
  }
45
107
  else {
46
- console.error(`${this.id} does not have a title`);
108
+ if (verbose) {
109
+ logger.error(`${this.id} does not have a title`);
110
+ }
47
111
  }
48
112
  // This is the known 'default' description - on previous version this content was repeated on descriptions processed by "descs"
49
113
  if (this.desc) {
50
114
  result += ` desc ${(0, global_1.escapeQuotes)(this.desc)}\n`;
51
115
  }
52
116
  else {
53
- console.error(`${this.id} does not have a desc`);
117
+ if (verbose) {
118
+ logger.error(`${this.id} does not have a desc`);
119
+ }
54
120
  }
55
121
  if (this.descs) {
56
122
  Object.entries(this.descs).forEach(([key, subDesc]) => {
@@ -60,7 +126,9 @@ class Control {
60
126
  // The "default" keyword may have the same content as the desc content for backward compatibility with different historical InSpec versions.
61
127
  // In that case, we can ignore writing the "default" subdescription field.
62
128
  // If they are different, however, someone may be trying to use the keyword "default" for a unique subdescription, which should not be done.
63
- console.error(`${this.id} has a subdescription called "default" with contents that do not match the main description. "Default" should not be used as a keyword for unique sub-descriptions.`);
129
+ if (verbose) {
130
+ logger.error(`${this.id} has a subdescription called "default" with contents that do not match the main description. "Default" should not be used as a keyword for unique sub-descriptions.`);
131
+ }
64
132
  }
65
133
  }
66
134
  else {
@@ -68,15 +136,19 @@ class Control {
68
136
  }
69
137
  }
70
138
  else {
71
- console.error(`${this.id} does not have a desc for the value ${key}`);
139
+ if (verbose) {
140
+ logger.error(`${this.id} does not have a desc for the value ${key}`);
141
+ }
72
142
  }
73
143
  });
74
144
  }
75
- if (this.impact) {
76
- result += ` impact ${this.impact}\n`;
145
+ if (this.impact !== undefined) {
146
+ result += ` impact ${(this.impact <= 0 ? this.impact.toFixed(1) : this.impact)}\n`;
77
147
  }
78
148
  else {
79
- console.error(`${this.id} does not have an impact`);
149
+ if (verbose) {
150
+ logger.error(`${this.id} does not have an impact`);
151
+ }
80
152
  }
81
153
  if (this.refs) {
82
154
  this.refs.forEach((ref) => {
@@ -115,6 +187,9 @@ class Control {
115
187
  result += ` tag ${tag}: ${(0, global_1.escapeQuotes)(value)}\n`;
116
188
  }
117
189
  }
190
+ else {
191
+ result += ` tag ${tag}: nil\n`;
192
+ }
118
193
  });
119
194
  if (this.describe) {
120
195
  result += '\n';
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.processOVAL = exports.extractAllCriteriaRefs = void 0;
4
4
  const xccdf_1 = require("../utilities/xccdf");
5
+ const logging_1 = require("../utilities/logging");
5
6
  // https://stackoverflow.com/questions/9133500/how-to-find-a-node-in-a-tree-with-javascript
6
7
  function searchTree(aTree, fCompair, bGreedy) {
7
8
  let oNode; // always the current node
@@ -53,6 +54,7 @@ function extractAllCriteriaRefs(initialCriteria) {
53
54
  exports.extractAllCriteriaRefs = extractAllCriteriaRefs;
54
55
  function processOVAL(oval) {
55
56
  var _a;
57
+ const logger = (0, logging_1.createWinstonLogger)();
56
58
  if (!oval) {
57
59
  return undefined;
58
60
  }
@@ -73,7 +75,7 @@ function processOVAL(oval) {
73
75
  if (foundCriteriaRefererence.object) {
74
76
  foundCriteriaRefererence.object.forEach((object) => {
75
77
  if (!object['@_object_ref']) {
76
- console.warn(`Found object without object_ref in test ${criteriaRef}`);
78
+ logger.warn(`Found object without object_ref in test ${criteriaRef}`);
77
79
  }
78
80
  else {
79
81
  const objectRef = object['@_object_ref'];
@@ -82,7 +84,7 @@ function processOVAL(oval) {
82
84
  foundObjects.push(foundObjectReference);
83
85
  }
84
86
  else {
85
- console.warn(`Could not find object ${objectRef} for test ${criteriaRef}`);
87
+ logger.warn(`Could not find object ${objectRef} for test ${criteriaRef}`);
86
88
  }
87
89
  }
88
90
  });
@@ -90,7 +92,7 @@ function processOVAL(oval) {
90
92
  if (foundCriteriaRefererence.state) {
91
93
  foundCriteriaRefererence.state.forEach((state) => {
92
94
  if (!state['@_state_ref']) {
93
- console.warn(`Found state without state_ref in test ${criteriaRef}`);
95
+ logger.warn(`Found state without state_ref in test ${criteriaRef}`);
94
96
  }
95
97
  else {
96
98
  const stateRef = state['@_state_ref'];
@@ -99,7 +101,7 @@ function processOVAL(oval) {
99
101
  foundStates.push(foundStateReference);
100
102
  }
101
103
  else {
102
- console.warn(`Could not find state ${stateRef} for test ${criteriaRef}`);
104
+ logger.warn(`Could not find state ${stateRef} for test ${criteriaRef}`);
103
105
  }
104
106
  }
105
107
  });
@@ -8,6 +8,7 @@ const control_1 = tslib_1.__importDefault(require("../objects/control"));
8
8
  const lodash_1 = tslib_1.__importDefault(require("lodash"));
9
9
  const CciNistMappingData_1 = require("../mappings/CciNistMappingData");
10
10
  const pretty_1 = tslib_1.__importDefault(require("pretty"));
11
+ const logging_1 = require("../utilities/logging");
11
12
  function extractAllRules(groups) {
12
13
  const rules = [];
13
14
  groups.forEach((group) => {
@@ -42,7 +43,11 @@ function ensureDecodedXMLStringValue(input) {
42
43
  }
43
44
  // Moving the newline removal to diff library rather than processXCCDF level
44
45
  function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
46
+ const logger = (0, logging_1.createWinstonLogger)();
45
47
  const parsedXML = (0, xccdf_1.convertEncodedXmlIntoJson)(xml);
48
+ if (parsedXML.Benchmark === undefined) {
49
+ throw new Error('Could not process the XCCDF file, check the input to make sure this is a properly formatted XCCDF file.');
50
+ }
46
51
  const rules = extractAllRules(parsedXML.Benchmark[0].Group);
47
52
  const profile = new profile_1.default({
48
53
  name: parsedXML.Benchmark[0]['@_id'],
@@ -116,7 +121,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
116
121
  control.desc = extractedDescription || '';
117
122
  }
118
123
  else {
119
- console.warn(`Invalid value for extracted description: ${extractedDescription}`);
124
+ logger.warn(`Invalid value for extracted description: ${extractedDescription}`);
120
125
  }
121
126
  control.impact = (0, xccdf_1.severityStringToImpact)(rule['@_severity'] || 'medium', rule.group['@_id']);
122
127
  if (!control.descs || Array.isArray(control.descs)) {
@@ -131,7 +136,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
131
136
  let referenceID = null;
132
137
  for (const checkContent of rule.check) {
133
138
  if ('check-content-ref' in checkContent && checkContent['@_system'].includes('oval')) {
134
- console.log(`Found OVAL reference: ${checkContent['@_system']}`);
139
+ logger.info(`Found OVAL reference: ${checkContent['@_system']}`);
135
140
  for (const checkContentRef of checkContent['check-content-ref']) {
136
141
  if (checkContentRef['@_name']) {
137
142
  referenceID = checkContentRef['@_name'];
@@ -143,7 +148,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
143
148
  control.descs.check = (0, xccdf_1.removeXMLSpecialCharacters)(ovalDefinitions[referenceID].metadata[0].title);
144
149
  }
145
150
  else if (referenceID) {
146
- console.warn(`Could not find OVAL definition for ${referenceID}`);
151
+ logger.warn(`Could not find OVAL definition for ${referenceID}`);
147
152
  }
148
153
  }
149
154
  }
@@ -153,7 +158,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
153
158
  for (const complexChecks of rule['complex-check']) {
154
159
  const allComplexChecks = extractAllComplexChecks(complexChecks);
155
160
  if (control.id === '1.1.1.5') {
156
- console.log(allComplexChecks);
161
+ logger.info(allComplexChecks);
157
162
  }
158
163
  allComplexChecks.forEach((complexCheck) => {
159
164
  if (complexCheck.check) {
@@ -162,7 +167,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
162
167
  if ((_a = check['@_system']) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('oval')) {
163
168
  const ovalReference = check['check-content-ref'][0]['@_name'];
164
169
  if (!ovalDefinitions) {
165
- console.warn(`Missing OVAL definitions! Unable to process OVAL reference: ${ovalReference}`);
170
+ logger.warn(`Missing OVAL definitions! Unable to process OVAL reference: ${ovalReference}`);
166
171
  }
167
172
  else if (ovalReference && ovalReference in ovalDefinitions) {
168
173
  ovalDefinitions[ovalReference].resolvedValues.forEach((resolvedValue) => {
@@ -185,7 +190,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
185
190
  }
186
191
  }
187
192
  else {
188
- console.warn(`Found external reference to unknown system: ${check['@_system']}, only OVAL is supported`);
193
+ logger.warn(`Found external reference to unknown system: ${check['@_system']}, only OVAL is supported`);
189
194
  }
190
195
  });
191
196
  }
@@ -332,19 +337,19 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
332
337
  control.tags[identifierType] = lodash_1.default.union(control.tags[identifierType], [identifier]);
333
338
  }
334
339
  else {
335
- console.warn(`Attempted to push identifier to control tags when identifier already exists: ${identifierType}: ${identifier}`);
340
+ logger.warn(`Attempted to push identifier to control tags when identifier already exists: ${identifierType}: ${identifier}`);
336
341
  }
337
342
  }
338
343
  else {
339
- console.warn('Reference parts of invalid length:');
340
- console.log(referenceParts);
344
+ logger.warn('Reference parts of invalid length:');
345
+ logger.info(referenceParts);
341
346
  }
342
347
  }
343
348
  }
344
349
  catch (e) {
345
- console.warn(`Error parsing ref for control ${control.id}: `);
346
- console.warn(JSON.stringify(reference, null, 2));
347
- console.warn(e);
350
+ logger.warn(`Error parsing ref for control ${control.id}: `);
351
+ logger.warn(JSON.stringify(reference, null, 2));
352
+ logger.warn(e);
348
353
  }
349
354
  }
350
355
  });
@@ -44,6 +44,9 @@ function removeWhitespace(input) {
44
44
  return input.replace(/\s/gi, '');
45
45
  }
46
46
  exports.removeWhitespace = removeWhitespace;
47
+ const escapeSpecialCaseBackslashes = (s) => {
48
+ return s.replace(/\\\)/g, '\\\\)'); // Escape backslashes if preceding close parentheses
49
+ };
47
50
  const escapeSingleQuotes = (s) => {
48
51
  return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); // Escape backslashes and quotes
49
52
  };
@@ -52,7 +55,7 @@ const escapeDoubleQuotes = (s) => {
52
55
  };
53
56
  function escapeQuotes(s) {
54
57
  if (s.includes("'") && s.includes('"')) {
55
- return `%q(${removeNewlinePlaceholders(s)})`;
58
+ return `%q(${escapeSpecialCaseBackslashes(removeNewlinePlaceholders(s))})`;
56
59
  }
57
60
  else if (s.includes("'")) {
58
61
  return `"${escapeDoubleQuotes(removeNewlinePlaceholders(s))}"`;
@@ -5,9 +5,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.updateProfileUsingXCCDF = exports.updateProfile = exports.updateControl = exports.findUpdatedControlByAllIdentifiers = exports.getExistingDescribeFromControl = void 0;
6
6
  const tslib_1 = require("tslib");
7
7
  const lodash_1 = tslib_1.__importDefault(require("lodash"));
8
+ const diff_1 = require("./diff");
8
9
  const profile_1 = tslib_1.__importDefault(require("../objects/profile"));
9
10
  const xccdf_1 = require("../parsers/xccdf");
10
- const diff_1 = require("./diff");
11
11
  const diffMarkdown_1 = require("./diffMarkdown");
12
12
  function projectValuesOntoExistingObj(dst, src, currentPath = '') {
13
13
  for (const updatedValue in src) {
@@ -31,63 +31,190 @@ function projectValuesOntoExistingObj(dst, src, currentPath = '') {
31
31
  }
32
32
  return dst;
33
33
  }
34
- /*
35
- Return first index found from given array that is not an empty entry (cell)
36
- */
37
- function getIndexOfFirstLine(auditArray, index, action) {
38
- let indexVal = index;
39
- while (auditArray[indexVal] === '') {
40
- switch (action) {
41
- case '-':
42
- indexVal--;
34
+ function getRangesForLines(text) {
35
+ /*
36
+ Returns an array containing two numerical indices (i.e., start and stop
37
+ line numbers) for each string or multi-line comment, given raw text as
38
+ an input parameter. The raw text is a string containing the entirety of an
39
+ InSpec control.
40
+
41
+ Algorithm utilizes a pair of stacks (i.e., `stack`, `rangeStack`) to keep
42
+ track of string delimiters and their associated line numbers, respectively.
43
+
44
+ Combinations Handled:
45
+ - Single quotes (')
46
+ - Double quotes (")
47
+ - Back ticks (`)
48
+ - Mixed quotes ("`'")
49
+ - Percent strings (%; keys: q, Q, r, i, I, w, W, x; delimiters: (), {},
50
+ [], <>, most non-alphanumeric characters); (e.g., "%q()")
51
+ - Percent literals (%; delimiters: (), {}, [], <>, most non-
52
+ alphanumeric characters); (e.g., "%()")
53
+ - Multi-line comments (e.g., =begin\nSome comment\n=end)
54
+ - Variable delimiters (i.e., parenthesis: (); array: []; hash: {})
55
+ */
56
+ const stringDelimiters = { '(': ')', '{': '}', '[': ']', '<': '>' };
57
+ const variableDelimiters = { '(': ')', '{': '}', '[': ']' };
58
+ const quotes = '\'"`';
59
+ const strings = 'qQriIwWxs';
60
+ let skipCharLength;
61
+ (function (skipCharLength) {
62
+ skipCharLength[skipCharLength["string"] = '('.length] = "string";
63
+ skipCharLength[skipCharLength["percentString"] = 'q('.length] = "percentString";
64
+ skipCharLength[skipCharLength["commentBegin"] = '=begin'.length] = "commentBegin";
65
+ skipCharLength[skipCharLength["inlineInterpolationBegin"] = '{'.length] = "inlineInterpolationBegin";
66
+ })(skipCharLength || (skipCharLength = {}));
67
+ const stack = [];
68
+ const rangeStack = [];
69
+ const ranges = [];
70
+ const lines = text.split('\n');
71
+ for (let i = 0; i < lines.length; i++) {
72
+ let j = 0;
73
+ while (j < lines[i].length) {
74
+ const line = lines[i];
75
+ let char = line[j];
76
+ const isEmptyStack = (stack.length == 0);
77
+ const isNotEmptyStack = (stack.length > 0);
78
+ const isQuoteChar = quotes.includes(char);
79
+ const isNotEscapeChar = ((j == 0) || (j > 0 && line[j - 1] != '\\'));
80
+ const isPercentChar = (char == '%');
81
+ const isVariableDelimiterChar = Object.keys(variableDelimiters).includes(char);
82
+ const isStringDelimiterChar = ((j < line.length - 1) && (/^[^A-Za-z0-9]$/.test(line[j + 1])));
83
+ const isCommentBeginChar = ((j == 0) && (line.length >= 6) && (line.slice(0, 6) == '=begin'));
84
+ const isCommentChar = /^\s*#/.test(line);
85
+ const isInlineInterpolation = (char == '#' && ((j < line.length - 1) && line[j + 1] == '{'));
86
+ const isPercentStringKeyChar = ((j < line.length - 1) && (strings.includes(line[j + 1])));
87
+ const isPercentStringDelimiterChar = ((j < line.length - 2) && (/^[^A-Za-z0-9]$/.test(line[j + 2])));
88
+ const isPercentString = (isPercentStringKeyChar && isPercentStringDelimiterChar);
89
+ let baseCondition = (isEmptyStack && isNotEscapeChar);
90
+ const quotePushCondition = (baseCondition && isQuoteChar);
91
+ const variablePushCondition = (baseCondition && isVariableDelimiterChar);
92
+ const stringPushCondition = (baseCondition && isPercentChar && isStringDelimiterChar);
93
+ const percentStringPushCondition = (baseCondition && isPercentChar && isPercentString);
94
+ const commentBeginCondition = (baseCondition && isCommentBeginChar);
95
+ const commentCondition = (baseCondition && isCommentChar);
96
+ const inlineInterpolationCondition = (isNotEmptyStack && isInlineInterpolation);
97
+ if (commentCondition) {
43
98
  break;
44
- case '+':
45
- indexVal++;
99
+ }
100
+ if (stringPushCondition) {
101
+ j += skipCharLength.string; // j += 1
102
+ }
103
+ else if (percentStringPushCondition) {
104
+ j += skipCharLength.percentString; // j += 2
105
+ }
106
+ else if (commentBeginCondition) {
107
+ j += skipCharLength.commentBegin; // j += 6
108
+ }
109
+ else if (inlineInterpolationCondition) {
110
+ j += skipCharLength.inlineInterpolationBegin; // j += 1
111
+ }
112
+ char = line[j];
113
+ baseCondition = (isNotEmptyStack && isNotEscapeChar);
114
+ const delimiterCondition = (baseCondition && Object.keys(stringDelimiters).includes(stack[stack.length - 1]));
115
+ const delimiterPushCondition = (delimiterCondition && (stack[stack.length - 1] == char));
116
+ const delimiterPopCondition = (delimiterCondition && (stringDelimiters[stack[stack.length - 1]] == char));
117
+ const basePopCondition = (baseCondition && (stack[stack.length - 1] == char) && !Object.keys(stringDelimiters).includes(char));
118
+ const isCommentEndChar = ((j == 0) && (line.length >= 4) && (line.slice(0, 4) == '=end'));
119
+ const commentEndCondition = (baseCondition && isCommentEndChar && (stack[stack.length - 1] == '=begin'));
120
+ const popCondition = (basePopCondition || delimiterPopCondition || commentEndCondition);
121
+ const pushCondition = (quotePushCondition || variablePushCondition || stringPushCondition ||
122
+ percentStringPushCondition || delimiterPushCondition || commentBeginCondition || inlineInterpolationCondition);
123
+ if (popCondition) {
124
+ stack.pop();
125
+ rangeStack[rangeStack.length - 1].push(i);
126
+ const range_ = rangeStack.pop();
127
+ if (rangeStack.length == 0) {
128
+ ranges.push(range_);
129
+ }
130
+ }
131
+ else if (pushCondition) {
132
+ if (commentBeginCondition) {
133
+ stack.push('=begin');
134
+ }
135
+ else if (inlineInterpolationCondition) {
136
+ stack.push('{');
137
+ }
138
+ else {
139
+ stack.push(char);
140
+ }
141
+ rangeStack.push([i]);
142
+ }
143
+ j++;
144
+ }
145
+ }
146
+ return ranges;
147
+ }
148
+ function joinMultiLineStringsFromRanges(text, ranges) {
149
+ /*
150
+ Returns an array of strings and joined strings at specified ranges, given
151
+ raw text as an input parameter.
152
+ */
153
+ const originalLines = text.split('\n');
154
+ const joinedLines = [];
155
+ let i = 0;
156
+ while (i < originalLines.length) {
157
+ let foundInRanges = false;
158
+ for (const [startIndex, stopIndex] of ranges) {
159
+ if (i >= startIndex && i <= stopIndex) {
160
+ joinedLines.push(originalLines.slice(startIndex, stopIndex + 1).join('\n'));
161
+ foundInRanges = true;
162
+ i = stopIndex;
46
163
  break;
164
+ }
165
+ }
166
+ if (!foundInRanges) {
167
+ joinedLines.push(originalLines[i]);
47
168
  }
169
+ i++;
48
170
  }
49
- return indexVal;
171
+ return joinedLines;
172
+ }
173
+ function getMultiLineRanges(ranges) {
174
+ /*
175
+ Drops ranges with the same start and stop line numbers (i.e., strings
176
+ that populate a single line)
177
+ */
178
+ const multiLineRanges = [];
179
+ for (const [start, stop] of ranges) {
180
+ if (start !== stop) {
181
+ multiLineRanges.push([start, stop]);
182
+ }
183
+ }
184
+ return multiLineRanges;
50
185
  }
51
186
  /*
52
187
  This is the most likely thing to break if you are getting code formatting issues.
53
188
  Extract the existing describe blocks (what is actually run by inspec for validation)
54
189
  */
55
190
  function getExistingDescribeFromControl(control) {
56
- // Algorithm:
57
- // Locate the start and end of the control string
58
- // Update the end of the control that contains information (if empty lines are at the end of the control)
59
- // loop: until the start index is changed (loop is done from the bottom up)
60
- // Clean testing array entry line (removes any non-print characters)
61
- // if: line starts with meta-information 'tag' or 'ref'
62
- // set start index to found location
63
- // break out of the loop
64
- // end
65
- // end
66
- // Remove any empty lines after the start index (in any)
67
- // Extract the describe block from the audit control given the start and end indices
68
- // Assumptions:
69
- // 1 - The meta-information 'tag' or 'ref' precedes the describe block
70
- // Pros: Solves the potential issue with option 1, as the lookup for the meta-information
71
- // 'tag' or 'ref' is expected to the at the beginning of the line.
72
191
  if (control.code) {
73
- let existingDescribeBlock = '';
74
- let indexStart = control.code.toLowerCase().indexOf('control');
75
- let indexEnd = control.code.toLowerCase().trimEnd().lastIndexOf('end');
76
- const auditControl = control.code.substring(indexStart, indexEnd).split('\n');
77
- indexStart = 0;
78
- indexEnd = auditControl.length - 1;
79
- indexEnd = getIndexOfFirstLine(auditControl, indexEnd, '-');
80
- let index = indexEnd;
81
- while (indexStart === 0) {
82
- const line = auditControl[index].toLowerCase().trim();
83
- if (line.indexOf('ref') === 0 || line.indexOf('tag') === 0) {
84
- indexStart = index + 1;
85
- }
86
- index--;
192
+ // Join multi-line strings in InSpec control.
193
+ const ranges = getRangesForLines(control.code);
194
+ // Get the entries that have delimiters that span multi-lines
195
+ const multiLineRanges = getMultiLineRanges(ranges);
196
+ // Array of lines representing the full InSpec control, with multi-line strings collapsed
197
+ const lines = joinMultiLineStringsFromRanges(control.code, multiLineRanges);
198
+ // Define RegExp for lines to skip.
199
+ const skip = ['control\\W', ' title\\W', ' desc\\W', ' impact\\W', ' tag\\W', ' ref\\W'];
200
+ const skipRegExp = RegExp(skip.map(x => `(^${x})`).join('|'));
201
+ // Extract describe block from InSpec control with collapsed multiline strings.
202
+ const describeBlock = [];
203
+ let ignoreNewLine = true;
204
+ for (const line of lines) {
205
+ const checkRegExp = ((line.trim() !== '') && !skipRegExp.test(line));
206
+ const checkNewLine = ((line.trim() === '') && !ignoreNewLine);
207
+ // Include '\n' if it is part of describe block, otherwise skip line.
208
+ if (checkRegExp || checkNewLine) {
209
+ describeBlock.push(line);
210
+ ignoreNewLine = false;
211
+ }
212
+ else {
213
+ ignoreNewLine = true;
214
+ }
87
215
  }
88
- indexStart = getIndexOfFirstLine(auditControl, indexStart, '+');
89
- existingDescribeBlock = auditControl.slice(indexStart, indexEnd + 1).join('\n').toString();
90
- return existingDescribeBlock;
216
+ // Return synthesized logic as describe block
217
+ return describeBlock.slice(0, describeBlock.lastIndexOf('end')).join('\n'); // Drop trailing ['end', '\n'] from Control block.
91
218
  }
92
219
  else {
93
220
  return '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mitre/inspec-objects",
3
- "version": "0.0.32",
3
+ "version": "1.0.0",
4
4
  "description": "Typescript objects for normalizing between InSpec profiles and XCCDF benchmarks",
5
5
  "main": "lib/index.js",
6
6
  "publishConfig": {