@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.
- 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 +83 -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 +173 -46
- 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,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';
|
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) {
|
|
@@ -31,63 +31,190 @@ function projectValuesOntoExistingObj(dst, src, currentPath = '') {
|
|
|
31
31
|
}
|
|
32
32
|
return dst;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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 '';
|