@mitre/inspec-objects 0.0.32 → 0.0.34

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.
@@ -31,63 +31,173 @@ 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--;
43
- break;
44
- case '+':
45
- 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., paranthesis: (); 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 = {}));
66
+ const stack = [];
67
+ const rangeStack = [];
68
+ const ranges = [];
69
+ const lines = text.split('\n');
70
+ for (let i = 0; i < lines.length; i++) {
71
+ let j = 0;
72
+ while (j < lines[i].length) {
73
+ const line = lines[i];
74
+ let char = line[j];
75
+ const isEmptyStack = (stack.length == 0);
76
+ const isNotEmptyStack = (stack.length > 0);
77
+ const isQuoteChar = quotes.includes(char);
78
+ const isNotEscapeChar = ((j == 0) || (j > 0 && line[j - 1] != '\\'));
79
+ const isPercentChar = (char == '%');
80
+ const isVariableDelimiterChar = Object.keys(variableDelimiters).includes(char);
81
+ const isStringDelimiterChar = ((j < line.length - 1) && (/^[^A-Za-z0-9]$/.test(line[j + 1])));
82
+ const isCommentBeginChar = ((j == 0) && (line.length >= 6) && (line.slice(0, 6) == '=begin'));
83
+ const isPercentStringKeyChar = ((j < line.length - 1) && (strings.includes(line[j + 1])));
84
+ const isPercentStringDelimiterChar = ((j < line.length - 2) && (/^[^A-Za-z0-9]$/.test(line[j + 2])));
85
+ const isPercentString = (isPercentStringKeyChar && isPercentStringDelimiterChar);
86
+ let baseCondition = (isEmptyStack && isNotEscapeChar);
87
+ const quotePushCondition = (baseCondition && isQuoteChar);
88
+ const variablePushCondition = (baseCondition && isVariableDelimiterChar);
89
+ const stringPushCondition = (baseCondition && isPercentChar && isStringDelimiterChar);
90
+ const percentStringPushCondition = (baseCondition && isPercentChar && isPercentString);
91
+ const commentBeginCondition = (baseCondition && isCommentBeginChar);
92
+ if (stringPushCondition) {
93
+ j += skipCharLength.string; // j += 1
94
+ }
95
+ else if (percentStringPushCondition) {
96
+ j += skipCharLength.percentString; // j += 2
97
+ }
98
+ else if (commentBeginCondition) {
99
+ j += skipCharLength.commentBegin; // j += 6
100
+ }
101
+ char = line[j];
102
+ baseCondition = (isNotEmptyStack && isNotEscapeChar);
103
+ const delimiterCondition = (baseCondition && Object.keys(stringDelimiters).includes(stack[stack.length - 1]));
104
+ const delimiterPushCondition = (delimiterCondition && (stack[stack.length - 1] == char));
105
+ const delimiterPopCondition = (delimiterCondition && (stringDelimiters[stack[stack.length - 1]] == char));
106
+ const basePopCondition = (baseCondition && (stack[stack.length - 1] == char) && !Object.keys(stringDelimiters).includes(char));
107
+ const isCommentEndChar = ((j == 0) && (line.length >= 4) && (line.slice(0, 4) == '=end'));
108
+ const commentEndCondition = (baseCondition && isCommentEndChar && (stack[stack.length - 1] == '=begin'));
109
+ const popCondition = (basePopCondition || delimiterPopCondition || commentEndCondition);
110
+ const pushCondition = (quotePushCondition || variablePushCondition || stringPushCondition ||
111
+ percentStringPushCondition || delimiterPushCondition || commentBeginCondition);
112
+ if (popCondition) {
113
+ stack.pop();
114
+ rangeStack[rangeStack.length - 1].push(i);
115
+ const range_ = rangeStack.pop();
116
+ if (rangeStack.length == 0) {
117
+ ranges.push(range_);
118
+ }
119
+ }
120
+ else if (pushCondition) {
121
+ if (commentBeginCondition) {
122
+ stack.push('=begin');
123
+ }
124
+ else {
125
+ stack.push(char);
126
+ }
127
+ rangeStack.push([i]);
128
+ }
129
+ j++;
130
+ }
131
+ }
132
+ return ranges;
133
+ }
134
+ function joinMultiLineStringsFromRanges(text, ranges) {
135
+ /*
136
+ Returns an array of strings and joined strings at specified ranges, given
137
+ raw text as an input parameter.
138
+ */
139
+ const originalLines = text.split('\n');
140
+ const joinedLines = [];
141
+ let i = 0;
142
+ while (i < originalLines.length) {
143
+ let foundInRanges = false;
144
+ for (const [startIndex, stopIndex] of ranges) {
145
+ if (i >= startIndex && i <= stopIndex) {
146
+ joinedLines.push(originalLines.slice(startIndex, stopIndex + 1).join('\n'));
147
+ foundInRanges = true;
148
+ i = stopIndex;
46
149
  break;
150
+ }
47
151
  }
152
+ if (!foundInRanges) {
153
+ joinedLines.push(originalLines[i]);
154
+ }
155
+ i++;
48
156
  }
49
- return indexVal;
157
+ return joinedLines;
158
+ }
159
+ function getMultiLineRanges(ranges) {
160
+ /*
161
+ Drops ranges with the same start and stop line numbers (i.e., strings
162
+ that populate a single line)
163
+ */
164
+ const multiLineRanges = [];
165
+ for (const [start, stop] of ranges) {
166
+ if (start !== stop) {
167
+ multiLineRanges.push([start, stop]);
168
+ }
169
+ }
170
+ return multiLineRanges;
50
171
  }
51
172
  /*
52
173
  This is the most likely thing to break if you are getting code formatting issues.
53
174
  Extract the existing describe blocks (what is actually run by inspec for validation)
54
175
  */
55
176
  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
177
  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;
178
+ // Join multi-line strings in InSpec control.
179
+ const ranges = getRangesForLines(control.code);
180
+ const multiLineRanges = getMultiLineRanges(ranges);
181
+ const lines = joinMultiLineStringsFromRanges(control.code, multiLineRanges); // Array of lines representing the full InSpec control, with multi-line strings collapsed
182
+ // Define RegExp for lines to skip when searching for describe block.
183
+ const skip = ['control\\W', ' title\\W', ' desc\\W', ' impact\\W', ' tag\\W', ' ref\\W'];
184
+ const skipRegExp = RegExp(skip.map(x => `(^${x})`).join('|'));
185
+ // Extract describe block from InSpec control with collapsed multiline strings.
186
+ const describeBlock = [];
187
+ let ignoreNewLine = true;
188
+ for (const line of lines) {
189
+ const checkRegExp = ((line.trim() !== '') && !skipRegExp.test(line));
190
+ const checkNewLine = ((line.trim() === '') && !ignoreNewLine);
191
+ // Include '\n' if it is part of describe block, otherwise skip line.
192
+ if (checkRegExp || checkNewLine) {
193
+ describeBlock.push(line);
194
+ ignoreNewLine = false;
195
+ }
196
+ else {
197
+ ignoreNewLine = true;
85
198
  }
86
- index--;
87
199
  }
88
- indexStart = getIndexOfFirstLine(auditControl, indexStart, '+');
89
- existingDescribeBlock = auditControl.slice(indexStart, indexEnd + 1).join('\n').toString();
90
- return existingDescribeBlock;
200
+ return describeBlock.slice(0, describeBlock.length - 2).join('\n'); // Drop trailing ['end', '\n'] from Control block.
91
201
  }
92
202
  else {
93
203
  return '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mitre/inspec-objects",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "Typescript objects for normalizing between InSpec profiles and XCCDF benchmarks",
5
5
  "main": "lib/index.js",
6
6
  "publishConfig": {