@mitre/inspec-objects 0.0.34 → 1.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/release-drafter.yml +21 -0
- package/.github/workflows/draft-release.yml +16 -0
- package/lib/objects/control.d.ts +2 -1
- package/lib/objects/control.js +92 -8
- package/lib/parsers/oval.js +6 -4
- package/lib/parsers/xccdf.js +17 -12
- package/lib/utilities/global.js +4 -1
- package/lib/utilities/update.js +23 -6
- package/package.json +1 -1
|
@@ -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 }}
|
package/lib/objects/control.d.ts
CHANGED
package/lib/objects/control.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,18 @@ class Control {
|
|
|
115
187
|
result += ` tag ${tag}: ${(0, global_1.escapeQuotes)(value)}\n`;
|
|
116
188
|
}
|
|
117
189
|
}
|
|
190
|
+
else {
|
|
191
|
+
const nilTagList = ['severity', 'satisfies'];
|
|
192
|
+
if (nilTagList.includes(tag)) {
|
|
193
|
+
result += ` tag ${tag}: nil\n`;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
result += ` tag '${tag}'\n`;
|
|
197
|
+
}
|
|
198
|
+
if (verbose) {
|
|
199
|
+
logger.info(`${this.id} does not have a value for tag: ${tag}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
118
202
|
});
|
|
119
203
|
if (this.describe) {
|
|
120
204
|
result += '\n';
|
package/lib/parsers/oval.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
logger.warn(`Could not find state ${stateRef} for test ${criteriaRef}`);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
});
|
package/lib/parsers/xccdf.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
+
logger.warn(`Attempted to push identifier to control tags when identifier already exists: ${identifierType}: ${identifier}`);
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
343
|
else {
|
|
339
|
-
|
|
340
|
-
|
|
344
|
+
logger.warn('Reference parts of invalid length:');
|
|
345
|
+
logger.info(referenceParts);
|
|
341
346
|
}
|
|
342
347
|
}
|
|
343
348
|
}
|
|
344
349
|
catch (e) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
});
|
package/lib/utilities/global.js
CHANGED
|
@@ -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))}"`;
|
package/lib/utilities/update.js
CHANGED
|
@@ -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) {
|
|
@@ -51,7 +51,7 @@ function getRangesForLines(text) {
|
|
|
51
51
|
- Percent literals (%; delimiters: (), {}, [], <>, most non-
|
|
52
52
|
alphanumeric characters); (e.g., "%()")
|
|
53
53
|
- Multi-line comments (e.g., =begin\nSome comment\n=end)
|
|
54
|
-
- Variable delimiters (i.e.,
|
|
54
|
+
- Variable delimiters (i.e., parenthesis: (); array: []; hash: {})
|
|
55
55
|
*/
|
|
56
56
|
const stringDelimiters = { '(': ')', '{': '}', '[': ']', '<': '>' };
|
|
57
57
|
const variableDelimiters = { '(': ')', '{': '}', '[': ']' };
|
|
@@ -62,6 +62,7 @@ function getRangesForLines(text) {
|
|
|
62
62
|
skipCharLength[skipCharLength["string"] = '('.length] = "string";
|
|
63
63
|
skipCharLength[skipCharLength["percentString"] = 'q('.length] = "percentString";
|
|
64
64
|
skipCharLength[skipCharLength["commentBegin"] = '=begin'.length] = "commentBegin";
|
|
65
|
+
skipCharLength[skipCharLength["inlineInterpolationBegin"] = '{'.length] = "inlineInterpolationBegin";
|
|
65
66
|
})(skipCharLength || (skipCharLength = {}));
|
|
66
67
|
const stack = [];
|
|
67
68
|
const rangeStack = [];
|
|
@@ -80,6 +81,8 @@ function getRangesForLines(text) {
|
|
|
80
81
|
const isVariableDelimiterChar = Object.keys(variableDelimiters).includes(char);
|
|
81
82
|
const isStringDelimiterChar = ((j < line.length - 1) && (/^[^A-Za-z0-9]$/.test(line[j + 1])));
|
|
82
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] == '{'));
|
|
83
86
|
const isPercentStringKeyChar = ((j < line.length - 1) && (strings.includes(line[j + 1])));
|
|
84
87
|
const isPercentStringDelimiterChar = ((j < line.length - 2) && (/^[^A-Za-z0-9]$/.test(line[j + 2])));
|
|
85
88
|
const isPercentString = (isPercentStringKeyChar && isPercentStringDelimiterChar);
|
|
@@ -89,6 +92,11 @@ function getRangesForLines(text) {
|
|
|
89
92
|
const stringPushCondition = (baseCondition && isPercentChar && isStringDelimiterChar);
|
|
90
93
|
const percentStringPushCondition = (baseCondition && isPercentChar && isPercentString);
|
|
91
94
|
const commentBeginCondition = (baseCondition && isCommentBeginChar);
|
|
95
|
+
const commentCondition = (baseCondition && isCommentChar);
|
|
96
|
+
const inlineInterpolationCondition = (isNotEmptyStack && isInlineInterpolation);
|
|
97
|
+
if (commentCondition) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
92
100
|
if (stringPushCondition) {
|
|
93
101
|
j += skipCharLength.string; // j += 1
|
|
94
102
|
}
|
|
@@ -98,6 +106,9 @@ function getRangesForLines(text) {
|
|
|
98
106
|
else if (commentBeginCondition) {
|
|
99
107
|
j += skipCharLength.commentBegin; // j += 6
|
|
100
108
|
}
|
|
109
|
+
else if (inlineInterpolationCondition) {
|
|
110
|
+
j += skipCharLength.inlineInterpolationBegin; // j += 1
|
|
111
|
+
}
|
|
101
112
|
char = line[j];
|
|
102
113
|
baseCondition = (isNotEmptyStack && isNotEscapeChar);
|
|
103
114
|
const delimiterCondition = (baseCondition && Object.keys(stringDelimiters).includes(stack[stack.length - 1]));
|
|
@@ -108,7 +119,7 @@ function getRangesForLines(text) {
|
|
|
108
119
|
const commentEndCondition = (baseCondition && isCommentEndChar && (stack[stack.length - 1] == '=begin'));
|
|
109
120
|
const popCondition = (basePopCondition || delimiterPopCondition || commentEndCondition);
|
|
110
121
|
const pushCondition = (quotePushCondition || variablePushCondition || stringPushCondition ||
|
|
111
|
-
percentStringPushCondition || delimiterPushCondition || commentBeginCondition);
|
|
122
|
+
percentStringPushCondition || delimiterPushCondition || commentBeginCondition || inlineInterpolationCondition);
|
|
112
123
|
if (popCondition) {
|
|
113
124
|
stack.pop();
|
|
114
125
|
rangeStack[rangeStack.length - 1].push(i);
|
|
@@ -121,6 +132,9 @@ function getRangesForLines(text) {
|
|
|
121
132
|
if (commentBeginCondition) {
|
|
122
133
|
stack.push('=begin');
|
|
123
134
|
}
|
|
135
|
+
else if (inlineInterpolationCondition) {
|
|
136
|
+
stack.push('{');
|
|
137
|
+
}
|
|
124
138
|
else {
|
|
125
139
|
stack.push(char);
|
|
126
140
|
}
|
|
@@ -177,9 +191,11 @@ function getExistingDescribeFromControl(control) {
|
|
|
177
191
|
if (control.code) {
|
|
178
192
|
// Join multi-line strings in InSpec control.
|
|
179
193
|
const ranges = getRangesForLines(control.code);
|
|
194
|
+
// Get the entries that have delimiters that span multi-lines
|
|
180
195
|
const multiLineRanges = getMultiLineRanges(ranges);
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
183
199
|
const skip = ['control\\W', ' title\\W', ' desc\\W', ' impact\\W', ' tag\\W', ' ref\\W'];
|
|
184
200
|
const skipRegExp = RegExp(skip.map(x => `(^${x})`).join('|'));
|
|
185
201
|
// Extract describe block from InSpec control with collapsed multiline strings.
|
|
@@ -197,7 +213,8 @@ function getExistingDescribeFromControl(control) {
|
|
|
197
213
|
ignoreNewLine = true;
|
|
198
214
|
}
|
|
199
215
|
}
|
|
200
|
-
|
|
216
|
+
// Return synthesized logic as describe block
|
|
217
|
+
return describeBlock.slice(0, describeBlock.lastIndexOf('end')).join('\n'); // Drop trailing ['end', '\n'] from Control block.
|
|
201
218
|
}
|
|
202
219
|
else {
|
|
203
220
|
return '';
|