@mitre/inspec-objects 2.0.0 → 2.0.2
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/workflows/auto-approve-and-merge.yml +2 -5
- package/.github/workflows/draft-release.yml +3 -3
- package/.github/workflows/e2e-test.yml +4 -4
- package/.github/workflows/linter.yml +4 -4
- package/.github/workflows/push-to-gpr.yml +8 -11
- package/.github/workflows/push-to-npm.yml +4 -4
- package/README.md +39 -7
- package/images/ts-inspec-objects-inspec-profile-workflow-process.png +0 -0
- package/images/ts-inspec-objects-oval-workflow-process.png +0 -0
- package/images/ts-inspec-objects-process-updateControl-workflow.png +0 -0
- package/images/ts-inspec-objects-updateProfileUsingXccdf-workflow.png +0 -0
- package/images/ts-inspec-objects-xccdf-workflow-process.png +0 -0
- package/lib/parsers/oval.js +1 -1
- package/lib/parsers/xccdf.d.ts +7 -2
- package/lib/parsers/xccdf.js +130 -33
- package/lib/utilities/diff.js +1 -1
- package/lib/utilities/update.d.ts +10 -0
- package/lib/utilities/update.js +12 -2
- package/lib/utilities/xccdf.d.ts +52 -6
- package/lib/utilities/xccdf.js +91 -14
- package/package.json +6 -6
|
@@ -4,17 +4,14 @@ on:
|
|
|
4
4
|
types: [labeled]
|
|
5
5
|
permissions:
|
|
6
6
|
pull-requests: write
|
|
7
|
-
contents: write
|
|
8
7
|
|
|
9
8
|
jobs:
|
|
10
9
|
approve:
|
|
11
10
|
name: Auto-approve dependabot PRs
|
|
12
11
|
if: github.event.pull_request.user.login == 'dependabot[bot]' && contains(github.event.pull_request.labels.*.name, 'dependencies')
|
|
13
|
-
runs-on: ubuntu-
|
|
12
|
+
runs-on: ubuntu-24.04
|
|
14
13
|
steps:
|
|
15
|
-
- uses: hmarr/auto-approve-action@
|
|
16
|
-
with:
|
|
17
|
-
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
14
|
+
- uses: hmarr/auto-approve-action@v4
|
|
18
15
|
- name: Enable auto-merge for Dependabot PRs
|
|
19
16
|
run: gh pr merge --auto --merge "$PR_URL"
|
|
20
17
|
env:
|
|
@@ -8,9 +8,9 @@ on:
|
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
update_draft_release:
|
|
11
|
-
runs-on: ubuntu-
|
|
11
|
+
runs-on: ubuntu-24.04
|
|
12
12
|
steps:
|
|
13
13
|
# Drafts your next Release notes as Pull Requests are merged into "master"
|
|
14
|
-
- uses:
|
|
14
|
+
- uses: release-drafter/release-drafter@v6
|
|
15
15
|
env:
|
|
16
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
16
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -7,15 +7,15 @@ on:
|
|
|
7
7
|
jobs:
|
|
8
8
|
build:
|
|
9
9
|
name: Run TS-InSpec-Objects E2E Tests
|
|
10
|
-
runs-on: ubuntu-
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
11
|
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
14
|
|
|
15
15
|
- name: Setup Node.js
|
|
16
|
-
uses: actions/setup-node@
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
17
|
with:
|
|
18
|
-
node-version:
|
|
18
|
+
node-version: 22
|
|
19
19
|
cache: 'npm'
|
|
20
20
|
|
|
21
21
|
- name: Install dependencies
|
|
@@ -7,16 +7,16 @@ on:
|
|
|
7
7
|
jobs:
|
|
8
8
|
build:
|
|
9
9
|
name: Lint TS-InSpec-Objects
|
|
10
|
-
runs-on: ubuntu-
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
11
|
|
|
12
12
|
steps:
|
|
13
13
|
- name: Checkout code
|
|
14
|
-
uses: actions/checkout@
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
15
|
|
|
16
16
|
- name: Setup Node.js
|
|
17
|
-
uses: actions/setup-node@
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
18
|
with:
|
|
19
|
-
node-version:
|
|
19
|
+
node-version: 22
|
|
20
20
|
cache: 'npm'
|
|
21
21
|
|
|
22
22
|
- name: Install project dependencies
|
|
@@ -6,31 +6,28 @@ on:
|
|
|
6
6
|
|
|
7
7
|
jobs:
|
|
8
8
|
build-deploy:
|
|
9
|
-
runs-on: ubuntu-
|
|
9
|
+
runs-on: ubuntu-24.04
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
12
|
|
|
13
13
|
- name: Setup node
|
|
14
|
-
uses: actions/setup-node@
|
|
14
|
+
uses: actions/setup-node@v4
|
|
15
15
|
with:
|
|
16
|
-
node-version:
|
|
17
|
-
registry-url: https://npm.pkg.github.com
|
|
16
|
+
node-version: 22
|
|
17
|
+
registry-url: 'https://npm.pkg.github.com'
|
|
18
18
|
scope: '@mitre'
|
|
19
19
|
|
|
20
20
|
- name: Build the NPM Package
|
|
21
21
|
run: |
|
|
22
22
|
npm install
|
|
23
|
+
rm -rf test
|
|
23
24
|
npm run build
|
|
25
|
+
|
|
24
26
|
- name: Pack all items that are published
|
|
25
27
|
run: npm pack
|
|
26
|
-
# Setup .npmrc file to publish to GitHub Package Registry
|
|
27
|
-
- uses: actions/setup-node@v1
|
|
28
|
-
with:
|
|
29
|
-
registry-url: 'https://npm.pkg.github.com'
|
|
30
|
-
scope: '@mitre'
|
|
31
28
|
|
|
32
29
|
# Publish inspec-objects to GitHub Package Registry
|
|
33
30
|
- name: Publish inspec-objects to GPR
|
|
34
31
|
run: npm publish mitre-inspec-objects-*.tgz
|
|
35
32
|
env:
|
|
36
|
-
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -6,14 +6,14 @@ on:
|
|
|
6
6
|
|
|
7
7
|
jobs:
|
|
8
8
|
build-deploy:
|
|
9
|
-
runs-on: ubuntu-
|
|
9
|
+
runs-on: ubuntu-24.04
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
12
|
|
|
13
13
|
- name: setup node
|
|
14
|
-
uses: actions/setup-node@
|
|
14
|
+
uses: actions/setup-node@v4
|
|
15
15
|
with:
|
|
16
|
-
node-version:
|
|
16
|
+
node-version: 22
|
|
17
17
|
registry-url: 'https://registry.npmjs.org'
|
|
18
18
|
|
|
19
19
|
- name: Install project dependencies
|
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# ts-inspec-objects
|
|
2
|
-
Typescript objects for InSpec
|
|
2
|
+
Typescript objects for InSpec Profiles
|
|
3
3
|
|
|
4
|
-
This repository contains the source code that facilitates the writing of InSpec
|
|
4
|
+
This repository contains the source code that facilitates the writing of InSpec Profiles (for use in things like stub generation and delta comparisons) more consistent with `Chef Cookstyle` formatting for ease of use when comparing with new changes from delta and when generating InSpec stubs that match a standard format.
|
|
5
5
|
|
|
6
6
|
For more information about Chef Cookstyle see:
|
|
7
7
|
- [chef/cookstyle on GitHub](https://github.com/chef/cookstyle)
|
|
8
8
|
- [Chef Cookstyle on Chef documents page](https://docs.chef.io/workstation/cookstyle/)
|
|
9
9
|
|
|
10
|
+
The `ts-inspec-objects` provides the capability of updating InSpect Profiles and creating stub Profiles based on XCCDF Benchmarks. This is accomplished by providing `Profile` and `Control` classes and supporting methods (functions).
|
|
11
|
+
|
|
10
12
|
## How to Use
|
|
11
13
|
The process code maintained in this repository generates a `npm` executable that is published to the `npm registry` as [mitre-inspec-objects](https://www.npmjs.com/package/@mitre/inspec-objects).
|
|
12
14
|
|
|
@@ -18,7 +20,7 @@ The package is a CommonJS-based npm written in TypeScript
|
|
|
18
20
|
|
|
19
21
|
## Parsing Process
|
|
20
22
|
|
|
21
|
-
When using this library to parse `InSpec
|
|
23
|
+
When using this library to parse `InSpec Profiles` or `xccdf files` for the purposes of generating InSpec profiles, the general workflow is as follows:
|
|
22
24
|
```
|
|
23
25
|
- The input is processed, read into a typescript object
|
|
24
26
|
- Operated on with any required action / logic
|
|
@@ -39,16 +41,46 @@ Here are some formatting choices that are being made.
|
|
|
39
41
|
2. Tag keywords are not quoted (ex: tag severity: 'medium')
|
|
40
42
|
3. Each control file ends with a newline
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
---
|
|
45
|
+
### XCCDF Workflow Process
|
|
46
|
+
Processes an XCCDF (Extensible Configuration Checklist Description Format) XML string based on the `Rule Identifier` provided and converts it into a Profile object.
|
|
47
|
+
If and OVAL definition is provided it retrives the oval objects and associated states and the Controls description check text is update with the content.
|
|
48
|
+
<div align="center">
|
|
49
|
+
<img src="images/ts-inspec-objects-xccdf-workflow-process.png" alt="Typescript Objects XCCDF Conversion Workflow Process" title="Typescript Objects XCCDF Conversion Workflow Process">
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
### OVAL Workflow Process
|
|
53
|
+
Processes an OVAL (Open Vulnerability and Assessment Language) XML string and converts it into a JSON object.
|
|
54
|
+
The process extracts definitions and their associated criteria references and resolved values.
|
|
55
|
+
The process executes the following steps:
|
|
56
|
+
1. Converts the OVAL XML string into a JSON object.
|
|
57
|
+
2. Iterates through the OVAL definitions and extracts each definition.
|
|
58
|
+
3. For each definition, extracts criteria references and resolves the associated objects and states.
|
|
59
|
+
4. Logs warnings if any objects or states cannot be found.
|
|
60
|
+
<div align="center">
|
|
61
|
+
<img src="images/ts-inspec-objects-oval-workflow-process.png" alt="Typescript Objects Oval Conversion Workflow Process" title="Typescript Objects Oval Conversion Workflow Process">
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
### InSpec Profile Workflow Process
|
|
65
|
+
Process a JSON string representing an InSpec profile, converts it, and processes it to return a `Profile` object.
|
|
66
|
+
It handles different versions of the InSpec JSON format and sorts the controls by their ID.
|
|
67
|
+
<div align="center">
|
|
68
|
+
<img src="images/ts-inspec-objects-inspec-profile-workflow-process.png" alt="Typescript Objects InSpec Profile Workflow Process" title="Typescript Objects InSpec Profile Workflow Process">
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
### Update Profile Using XCCDF Workflow Process
|
|
72
|
+
Updates a Profile with new metadata from and XCCDF, based on the `Rule Indetefier` and logs the process.
|
|
43
73
|
<div align="center">
|
|
44
|
-
<img src="images/ts-inspec-objects.
|
|
74
|
+
<img src="images/ts-inspec-objects-updateProfileUsingXccdf-workflow.png" alt="Typescript Objects Update Profile Using XCCDF Workflow Process" title="Typescript Objects Update Profile Using XCCDF Workflow Process">
|
|
45
75
|
</div>
|
|
46
76
|
|
|
47
|
-
###
|
|
77
|
+
### Update Control Workflow Process
|
|
78
|
+
Updates a given control object with the provided partial control and logs the process.
|
|
48
79
|
<div align="center">
|
|
49
|
-
<img src="images/
|
|
80
|
+
<img src="images/ts-inspec-objects-process-updateControl-workflow.png" alt="Typescript Objects Update Control Workflow Process" title="Typescript Objects Update Control Workflow Process">
|
|
50
81
|
</div>
|
|
51
82
|
|
|
83
|
+
---
|
|
52
84
|
## Development Environment Configuration
|
|
53
85
|
### Installation
|
|
54
86
|
To install the project, clone the repository and install the dependencies:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/parsers/oval.js
CHANGED
|
@@ -99,7 +99,7 @@ function processOVAL(oval) {
|
|
|
99
99
|
if (!oval) {
|
|
100
100
|
return undefined;
|
|
101
101
|
}
|
|
102
|
-
const parsed = (0, xccdf_1.convertEncodedXmlIntoJson)(oval);
|
|
102
|
+
const parsed = (0, xccdf_1.convertEncodedXmlIntoJson)(oval, 'withArrayNoEntitiesOption');
|
|
103
103
|
const extractedDefinitions = {};
|
|
104
104
|
for (const ovalDefinitions of parsed.oval_definitions) {
|
|
105
105
|
for (const definitionList of ovalDefinitions.definitions) {
|
package/lib/parsers/xccdf.d.ts
CHANGED
|
@@ -27,8 +27,13 @@ export type InputTextLang = {
|
|
|
27
27
|
'@_lang': string;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
* Processes an XCCDF
|
|
31
|
-
*
|
|
30
|
+
* Processes an XCCDF (Extensible Configuration Checklist Description Format) XML
|
|
31
|
+
* string and converts it into a Profile object.
|
|
32
|
+
* NOTE: We are using the fast xml parser (FXP) V4 which requires to specify
|
|
33
|
+
* which Whether a single tag should be parsed as an array or an object,
|
|
34
|
+
* it can't be decided by FXP. We process every tag as an array, this is
|
|
35
|
+
* the reason there are numerous tag test, were array index zero [0] is
|
|
36
|
+
* tested.
|
|
32
37
|
*
|
|
33
38
|
* @param xml - The XCCDF XML string to process.
|
|
34
39
|
* @param removeNewlines - A flag indicating whether to remove newlines from the processed data.
|
package/lib/parsers/xccdf.js
CHANGED
|
@@ -55,21 +55,33 @@ function extractAllComplexChecks(complexCheck) {
|
|
|
55
55
|
return complexChecks;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Ensures that the input is decoded
|
|
58
|
+
* Ensures that the input is decoded to a string value.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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.
|
|
66
69
|
*/
|
|
67
70
|
function ensureDecodedXMLStringValue(input, defaultValue) {
|
|
68
|
-
return lodash_1.default.isString(input)
|
|
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);
|
|
69
76
|
}
|
|
70
77
|
/**
|
|
71
|
-
* Processes an XCCDF
|
|
72
|
-
*
|
|
78
|
+
* Processes an XCCDF (Extensible Configuration Checklist Description Format) XML
|
|
79
|
+
* string and converts it into a Profile object.
|
|
80
|
+
* NOTE: We are using the fast xml parser (FXP) V4 which requires to specify
|
|
81
|
+
* which Whether a single tag should be parsed as an array or an object,
|
|
82
|
+
* it can't be decided by FXP. We process every tag as an array, this is
|
|
83
|
+
* the reason there are numerous tag test, were array index zero [0] is
|
|
84
|
+
* tested.
|
|
73
85
|
*
|
|
74
86
|
* @param xml - The XCCDF XML string to process.
|
|
75
87
|
* @param removeNewlines - A flag indicating whether to remove newlines from the processed data.
|
|
@@ -84,14 +96,40 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
84
96
|
if (parsedXML.Benchmark === undefined) {
|
|
85
97
|
throw new Error('Could not process the XCCDF file, check the input to make sure this is a properly formatted XCCDF file.');
|
|
86
98
|
}
|
|
99
|
+
// Extracts all rules from the given benchmark groups.
|
|
87
100
|
const rules = extractAllRules(parsedXML.Benchmark[0].Group);
|
|
101
|
+
// Variable used to store the profile data.
|
|
102
|
+
// The name is the benchmark Id, title and summary are from benchmark.
|
|
88
103
|
const profile = new profile_1.default({
|
|
89
|
-
name: parsedXML.Benchmark[0]['@_id'],
|
|
90
|
-
title: parsedXML.Benchmark[0].title[0]['#text'],
|
|
91
|
-
summary: parsedXML.Benchmark[0].description[0]['#text']
|
|
104
|
+
//name: parsedXML.Benchmark[0]['@_id'],
|
|
105
|
+
// title: (parsedXML.Benchmark[0].title[0] as FrontMatter)['#text'],
|
|
106
|
+
// summary: (parsedXML.Benchmark[0].description[0] as RationaleElement)['#text']
|
|
107
|
+
name: Array.isArray(parsedXML.Benchmark[0]['@_id'])
|
|
108
|
+
? parsedXML.Benchmark[0]['@_id'].map(n => n['@_id']).join(' ') === ''
|
|
109
|
+
? parsedXML.Benchmark[0]['@_id'].map(n => n).join(' ')
|
|
110
|
+
: parsedXML.Benchmark[0]['@_id'].join(' ')
|
|
111
|
+
: parsedXML.Benchmark[0]['@_id'],
|
|
112
|
+
title: Array.isArray(parsedXML.Benchmark[0].title)
|
|
113
|
+
? parsedXML.Benchmark[0].title.map(t => t['#text']).join(' ') === ''
|
|
114
|
+
? parsedXML.Benchmark[0].title.map(t => t).join(' ')
|
|
115
|
+
: parsedXML.Benchmark[0].title.map(t => t['#text']).join(' ')
|
|
116
|
+
: parsedXML.Benchmark[0].title,
|
|
117
|
+
summary: Array.isArray(parsedXML.Benchmark[0].description)
|
|
118
|
+
? parsedXML.Benchmark[0].description.map(d => d['#text']).join(' ') === ''
|
|
119
|
+
? parsedXML.Benchmark[0].description.map(d => d['p'] || '').join(' ') === ''
|
|
120
|
+
? parsedXML.Benchmark[0].description.map(d => d).join(' ')
|
|
121
|
+
: parsedXML.Benchmark[0].description.map(d => d['p'] || '').join(' ')
|
|
122
|
+
: parsedXML.Benchmark[0].description.map(d => d['#text']).join(' ')
|
|
123
|
+
: parsedXML.Benchmark[0].description
|
|
92
124
|
});
|
|
125
|
+
// Process each rule, extracting the necessary
|
|
126
|
+
// data and save it to the profile variable.
|
|
93
127
|
rules.forEach(rule => {
|
|
94
128
|
var _a, _b, _c;
|
|
129
|
+
// The description tag contains the following tags:
|
|
130
|
+
// "FalsePositives", "FalseNegatives", "Documentable", "Mitigations",
|
|
131
|
+
// "SeverityOverrideGuidance", "PotentialImpacts", "ThirdPartyTools",
|
|
132
|
+
// "MitigationControl", "Responsibility", "IAControls"
|
|
95
133
|
let extractedDescription;
|
|
96
134
|
if (typeof rule.description === 'object') {
|
|
97
135
|
if (Array.isArray(rule.description) && lodash_1.default.get(rule, "description[0]['#text']")) {
|
|
@@ -105,6 +143,10 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
105
143
|
if (Array.isArray(lodash_1.default.get(rule.description, '[0].p'))) {
|
|
106
144
|
const joinedDescriptions = lodash_1.default.get(rule.description, '[0].p');
|
|
107
145
|
extractedDescription = (0, pretty_1.default)(joinedDescriptions.join('\n\n'));
|
|
146
|
+
extractedDescription = (0, xccdf_1.removeHtmlTags)(extractedDescription).replace('\n', ' ');
|
|
147
|
+
}
|
|
148
|
+
else if (Array.isArray(rule.description)) {
|
|
149
|
+
extractedDescription = (0, xccdf_1.convertEncodedHTMLIntoJson)(rule.description[0]);
|
|
108
150
|
}
|
|
109
151
|
else {
|
|
110
152
|
extractedDescription = JSON.stringify(rule.description);
|
|
@@ -115,10 +157,12 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
115
157
|
else {
|
|
116
158
|
extractedDescription = (0, xccdf_1.convertEncodedHTMLIntoJson)(rule.description);
|
|
117
159
|
}
|
|
160
|
+
// Create a new control object and populate it with the necessary data.
|
|
118
161
|
const control = new control_1.default();
|
|
162
|
+
// Update the control Id with the appropriate value based on the rule id.
|
|
119
163
|
switch (useRuleId) {
|
|
120
164
|
case 'group':
|
|
121
|
-
control.id = rule.group['@_id'];
|
|
165
|
+
control.id = rule.group['@_id'].toString();
|
|
122
166
|
break;
|
|
123
167
|
case 'rule':
|
|
124
168
|
if (rule['@_id'][0].toLowerCase().startsWith('sv')) {
|
|
@@ -129,27 +173,51 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
129
173
|
}
|
|
130
174
|
break;
|
|
131
175
|
case 'version':
|
|
132
|
-
|
|
176
|
+
if (rule.version !== undefined) {
|
|
177
|
+
(lodash_1.default.isArray(rule.version))
|
|
178
|
+
? control.id = rule.version[0]
|
|
179
|
+
: control.id = rule.version;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw new Error('The rule type "version" did not provide an identification (Id) value');
|
|
183
|
+
}
|
|
133
184
|
break;
|
|
134
|
-
case 'cis':
|
|
135
|
-
//
|
|
185
|
+
case 'cis': {
|
|
186
|
+
// Regex explained
|
|
187
|
+
// \d:
|
|
188
|
+
// matches a single digit (0-9), the required starting point of the match.
|
|
189
|
+
// (\d?):
|
|
190
|
+
// matches an optional digit, there are three of these in sequence
|
|
191
|
+
// (.\d(\d?)(\d?)(\d?))?:
|
|
192
|
+
// matches an optional group that starts with a period (.) followed
|
|
193
|
+
// by one digit and up to three additional optional digits
|
|
194
|
+
// The pattern is repeated four times to match between zero and four
|
|
195
|
+
// groups of a period followed by one required digit and up to three
|
|
196
|
+
// additional optional digits. The pattern matches:
|
|
197
|
+
// 1, 123, 1.2, 1.234, 1.2.3.4.5, or 1.23.456.7.89
|
|
136
198
|
const controlIdRegex = /\d(\d?)(\d?)(\d?)(.\d(\d?)(\d?)(\d?))?(.\d(\d?)(\d?)(\d?))?(.\d(\d?)(\d?)(\d?))?(.\d(\d?)(\d?)(\d?))?/g;
|
|
137
|
-
// eslint-disable-next-line no-case-declarations
|
|
138
199
|
const controlIdMatch = controlIdRegex.exec(rule['@_id']);
|
|
139
200
|
if (controlIdMatch) {
|
|
140
201
|
control.id = controlIdMatch[0];
|
|
141
202
|
}
|
|
142
203
|
else {
|
|
143
|
-
throw new Error(`Could not parse control ID from rule ID: ${rule['@_id']}. Expecting something in this example format:
|
|
204
|
+
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`);
|
|
144
205
|
}
|
|
145
206
|
break;
|
|
207
|
+
}
|
|
146
208
|
default:
|
|
147
|
-
throw new Error('useRuleId must be one of "group", "rule", or "
|
|
209
|
+
throw new Error('useRuleId must be one of "group", "rule", "version" for STIG benchmarks, or "cis" for CIS benchmarks');
|
|
148
210
|
}
|
|
149
211
|
if (!(lodash_1.default.isArray(rule.title) && rule.title.length === 1)) {
|
|
150
212
|
throw new Error('Rule title is not an array of length 1. Investigate if the file is in the proper format.');
|
|
151
213
|
}
|
|
152
|
-
|
|
214
|
+
// Update the control title with the rule.tile content if a rule severity
|
|
215
|
+
// exists after removing any special characters, otherwise set the control
|
|
216
|
+
// title to [[[MISSING SEVERITY FROM BENCHMARK]]], undefined title.
|
|
217
|
+
control.title = (0, xccdf_1.removeXMLSpecialCharacters)(rule['@_severity'] || rule['@_weight']
|
|
218
|
+
? ensureDecodedXMLStringValue(rule.title[0], 'undefined title')
|
|
219
|
+
: `[[[MISSING SEVERITY or WEIGHT FROM BENCHMARK]]] ${ensureDecodedXMLStringValue(rule.title[0], 'undefined title')}`);
|
|
220
|
+
// Update the control description (desc) with the extracted description content
|
|
153
221
|
if (typeof extractedDescription === 'object' && !Array.isArray(extractedDescription)) {
|
|
154
222
|
control.desc = ((_a = extractedDescription.VulnDiscussion) === null || _a === void 0 ? void 0 : _a.split('Satisfies: ')[0]) || '';
|
|
155
223
|
}
|
|
@@ -162,10 +230,13 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
162
230
|
else {
|
|
163
231
|
logger.warn(`Invalid value for extracted description: ${extractedDescription}`);
|
|
164
232
|
}
|
|
233
|
+
// Update the control impact with the severity value from the rule,
|
|
234
|
+
// default to medium (0.5) if not found.
|
|
165
235
|
control.impact = (0, xccdf_1.severityStringToImpact)(rule['@_severity'] || 'medium');
|
|
166
236
|
if (!control.descs || Array.isArray(control.descs)) {
|
|
167
237
|
control.descs = {};
|
|
168
238
|
}
|
|
239
|
+
// Update the control descriptions (descs) check with the check text from the rule,
|
|
169
240
|
if (rule.check) {
|
|
170
241
|
if (rule.check.some((ruleValue) => 'check-content' in ruleValue)) {
|
|
171
242
|
control.descs.check = (0, xccdf_1.removeXMLSpecialCharacters)(rule.check ? rule.check[0]['check-content'][0] : 'Missing description');
|
|
@@ -240,20 +311,22 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
240
311
|
control.descs.check = checkTexts.join('\n');
|
|
241
312
|
}
|
|
242
313
|
}
|
|
314
|
+
// Update the control descriptions (descs) fix with content from the rule
|
|
315
|
+
// fixtest, if not found, defaults to "Missing fix text"
|
|
243
316
|
if (lodash_1.default.get(rule.fixtext, '[0]["#text"]')) {
|
|
244
317
|
control.descs.fix = (0, xccdf_1.removeXMLSpecialCharacters)(rule.fixtext[0]['#text']);
|
|
245
318
|
}
|
|
246
319
|
else if (typeof rule.fixtext === 'undefined') {
|
|
247
320
|
if (rule.fix && rule.fix[0]) {
|
|
248
|
-
control.descs.fix = (0, xccdf_1.
|
|
321
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)(rule.fix[0]['#text'] || 'Missing fix text');
|
|
249
322
|
}
|
|
250
323
|
}
|
|
251
324
|
else if (typeof rule.fixtext[0] === 'string') {
|
|
252
|
-
control.descs.fix = (0, xccdf_1.
|
|
325
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)(rule.fixtext[0]);
|
|
253
326
|
}
|
|
254
327
|
else if (typeof rule.fixtext[0] === 'object') {
|
|
255
328
|
if (Array.isArray(rule.fixtext[0])) {
|
|
256
|
-
control.descs.fix = (0, xccdf_1.
|
|
329
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)((0, pretty_1.default)((0, xccdf_1.convertJsonIntoXML)(rule.fixtext[0].map((fixtext) => {
|
|
257
330
|
if (fixtext.div) {
|
|
258
331
|
return fixtext.div;
|
|
259
332
|
}
|
|
@@ -263,21 +336,25 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
263
336
|
}))));
|
|
264
337
|
}
|
|
265
338
|
else {
|
|
266
|
-
control.descs.fix = (0, xccdf_1.removeXMLSpecialCharacters)((0, pretty_1.default)((0, xccdf_1.convertJsonIntoXML)(rule.fixtext)));
|
|
339
|
+
control.descs.fix = (0, xccdf_1.removeHtmlTags)((0, xccdf_1.removeXMLSpecialCharacters)((0, pretty_1.default)((0, xccdf_1.convertJsonIntoXML)(rule.fixtext)))).replace('\n', ' ').trim();
|
|
267
340
|
}
|
|
268
341
|
}
|
|
269
342
|
else {
|
|
270
343
|
control.descs.fix = 'Missing fix text';
|
|
271
344
|
}
|
|
345
|
+
// Update the control tags base on corresponding rule tags.
|
|
272
346
|
control.tags.severity = (0, xccdf_1.impactNumberToSeverityString)((0, xccdf_1.severityStringToImpact)(rule['@_severity'] || 'medium'));
|
|
273
347
|
control.tags.gid = rule.group['@_id'],
|
|
274
348
|
control.tags.rid = rule['@_id'];
|
|
275
349
|
control.tags.stig_id = rule['version'];
|
|
276
|
-
if (typeof rule.group.title
|
|
277
|
-
control.tags.gtitle = (0, xccdf_1.removeXMLSpecialCharacters)(rule.group.title
|
|
350
|
+
if (typeof rule.group.title === 'string') {
|
|
351
|
+
control.tags.gtitle = (0, xccdf_1.removeXMLSpecialCharacters)(rule.group.title);
|
|
278
352
|
}
|
|
279
353
|
else {
|
|
280
|
-
|
|
354
|
+
const gtitle = lodash_1.default.get(rule.group, 'title[0].#text', 'undefined title') === 'undefined title'
|
|
355
|
+
? lodash_1.default.get(rule.group, 'title[0]', 'undefined title')
|
|
356
|
+
: lodash_1.default.get(rule.group, 'title[0].#text', 'undefined title');
|
|
357
|
+
control.tags.gtitle = typeof gtitle === 'string' ? gtitle : gtitle['#text'] || 'undefined title';
|
|
281
358
|
}
|
|
282
359
|
if (rule['fix'] && rule['fix'].length > 0) {
|
|
283
360
|
control.tags.fix_id = rule['fix'][0]['@_id'];
|
|
@@ -285,11 +362,20 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
285
362
|
if (rule['rationale']) {
|
|
286
363
|
control.tags.rationale = rule['rationale'][0]['#text'];
|
|
287
364
|
}
|
|
365
|
+
// The description tag contains the following tags as well:
|
|
366
|
+
// "FalsePositives", "FalseNegatives", "Documentable", "Mitigations",
|
|
367
|
+
// "SeverityOverrideGuidance", "PotentialImpacts", "ThirdPartyTools",
|
|
368
|
+
// "MitigationControl", "Responsibility", "IAControls"
|
|
288
369
|
if (typeof extractedDescription === 'object') {
|
|
289
|
-
control.tags.satisfies =
|
|
370
|
+
control.tags.satisfies =
|
|
371
|
+
((_b = extractedDescription.VulnDiscussion) === null || _b === void 0 ? void 0 : _b.includes('Satisfies: ')) && extractedDescription.VulnDiscussion.split('Satisfies: ').length >= 1
|
|
372
|
+
? extractedDescription.VulnDiscussion.split('Satisfies: ')[1].split(',').map(satisfaction => satisfaction.trim())
|
|
373
|
+
: undefined;
|
|
290
374
|
control.tags.false_negatives = extractedDescription.FalseNegatives || undefined;
|
|
291
375
|
control.tags.false_positives = extractedDescription.FalsePositives || undefined;
|
|
292
|
-
control.tags.documentable = typeof extractedDescription.Documentable === 'boolean'
|
|
376
|
+
control.tags.documentable = typeof extractedDescription.Documentable === 'boolean'
|
|
377
|
+
? extractedDescription.Documentable
|
|
378
|
+
: undefined;
|
|
293
379
|
control.tags.mitigations = extractedDescription.Mitigations || undefined;
|
|
294
380
|
control.tags.severity_override_guidance = extractedDescription.SeverityOverrideGuidance || undefined;
|
|
295
381
|
control.tags.potential_impacts = extractedDescription.PotentialImpacts || undefined;
|
|
@@ -299,11 +385,15 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
299
385
|
control.tags.responsibility = extractedDescription.Responsibility || undefined;
|
|
300
386
|
control.tags.ia_controls = extractedDescription.IAControls || undefined;
|
|
301
387
|
}
|
|
388
|
+
// Ensure that tags inside the tags array are not an array
|
|
302
389
|
control.tags = lodash_1.default.mapValues(lodash_1.default.omitBy(control.tags, (value) => value === undefined), (value) => {
|
|
303
390
|
if (value && Array.isArray(value)) {
|
|
304
391
|
if (Array.isArray(value[0])) {
|
|
305
392
|
return (0, xccdf_1.removeXMLSpecialCharacters)(value[0][0]);
|
|
306
393
|
}
|
|
394
|
+
else if (value.length > 1) {
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
307
397
|
else {
|
|
308
398
|
return (0, xccdf_1.removeXMLSpecialCharacters)(value[0]);
|
|
309
399
|
}
|
|
@@ -315,7 +405,7 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
315
405
|
return value;
|
|
316
406
|
}
|
|
317
407
|
});
|
|
318
|
-
// Get all identifiers from the rule
|
|
408
|
+
// Get all identifiers from the rule; cci, nist, and legacy
|
|
319
409
|
if (rule.ident) {
|
|
320
410
|
rule.ident.forEach((identifier) => {
|
|
321
411
|
var _a, _b, _c;
|
|
@@ -342,8 +432,9 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
342
432
|
}
|
|
343
433
|
});
|
|
344
434
|
}
|
|
435
|
+
// Update control references with content from the benchmark rule object
|
|
345
436
|
(_c = rule.reference) === null || _c === void 0 ? void 0 : _c.forEach((reference) => {
|
|
346
|
-
var _a, _b, _c, _d;
|
|
437
|
+
var _a, _b, _c, _d, _e;
|
|
347
438
|
if (lodash_1.default.get(reference, '@_href') === '') {
|
|
348
439
|
(_a = control.refs) === null || _a === void 0 ? void 0 : _a.push(lodash_1.default.get(reference, '#text', 'undefined href'));
|
|
349
440
|
}
|
|
@@ -368,7 +459,13 @@ function processXCCDF(xml, removeNewlines, useRuleId, ovalDefinitions) {
|
|
|
368
459
|
}
|
|
369
460
|
else {
|
|
370
461
|
if ('title' in reference) {
|
|
371
|
-
|
|
462
|
+
const title = lodash_1.default.get(reference, 'title');
|
|
463
|
+
if (Array.isArray(title)) {
|
|
464
|
+
(_d = control.refs) === null || _d === void 0 ? void 0 : _d.push(title[0]);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
(_e = control.refs) === null || _e === void 0 ? void 0 : _e.push(lodash_1.default.get(reference, 'title'));
|
|
468
|
+
}
|
|
372
469
|
}
|
|
373
470
|
}
|
|
374
471
|
// Add the reference to the control tags when separated by §
|
package/lib/utilities/diff.js
CHANGED
|
@@ -151,7 +151,7 @@ function diffProfile(fromProfile, toProfile, logger) {
|
|
|
151
151
|
logger.error(`Unable to find existing control ${diffValue[1]}`);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
else if (diffValue[0] === '+' && !changedControlIds.includes(diffValue[1].
|
|
154
|
+
else if (diffValue[0] === '+' && !changedControlIds.includes(diffValue[1].toLowerCase()) && diffValue[1]) {
|
|
155
155
|
logger.info(JSON.stringify(diffValue));
|
|
156
156
|
logger.info(JSON.stringify(changedControlIds));
|
|
157
157
|
profileDiff.addedControlIDs.push(diffValue[1]);
|
|
@@ -74,4 +74,14 @@ export declare function updateControlDescribeBlock(from: Control, update: Partia
|
|
|
74
74
|
* @throws Will throw an error if a new control is added but the control data is not available.
|
|
75
75
|
*/
|
|
76
76
|
export declare function updateProfile(from: Profile, using: Profile, logger: winston.Logger): Omit<UpdatedProfileReturn, 'markdown'>;
|
|
77
|
+
/**
|
|
78
|
+
* Update a Profile with with new metadata from a XCCDF benchmark
|
|
79
|
+
*
|
|
80
|
+
* @param from - A Profile object
|
|
81
|
+
* @param using - An XCCDF in string format (XML)
|
|
82
|
+
* @param id - Specifies the rule ID format to use ('group', 'rule', 'version', or 'cis').
|
|
83
|
+
* @param logger - A winston logger instance for logging debug information.
|
|
84
|
+
* @param ovalDefinitions - Optional OVAL definitions to use for resolving values.
|
|
85
|
+
* @returns The Updated Profile (profile, the diff between from and using, and the markdown)
|
|
86
|
+
*/
|
|
77
87
|
export declare function updateProfileUsingXCCDF(from: Profile, using: string, id: 'group' | 'rule' | 'version' | 'cis', logger: winston.Logger, ovalDefinitions?: Record<string, OvalDefinitionValue>): UpdatedProfileReturn;
|
package/lib/utilities/update.js
CHANGED
|
@@ -285,7 +285,7 @@ function getExistingDescribeFromControl(control) {
|
|
|
285
285
|
function findUpdatedControlByAllIdentifiers(existingControl, updatedControls) {
|
|
286
286
|
// Try to match based on IDs
|
|
287
287
|
let updatedControl = updatedControls.find((updatedControl) => {
|
|
288
|
-
return updatedControl.id
|
|
288
|
+
return updatedControl.id.toLowerCase() === existingControl.id.toLowerCase();
|
|
289
289
|
});
|
|
290
290
|
if (updatedControl) {
|
|
291
291
|
return updatedControl;
|
|
@@ -377,9 +377,19 @@ function updateProfile(from, using, logger) {
|
|
|
377
377
|
diff,
|
|
378
378
|
};
|
|
379
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Update a Profile with with new metadata from a XCCDF benchmark
|
|
382
|
+
*
|
|
383
|
+
* @param from - A Profile object
|
|
384
|
+
* @param using - An XCCDF in string format (XML)
|
|
385
|
+
* @param id - Specifies the rule ID format to use ('group', 'rule', 'version', or 'cis').
|
|
386
|
+
* @param logger - A winston logger instance for logging debug information.
|
|
387
|
+
* @param ovalDefinitions - Optional OVAL definitions to use for resolving values.
|
|
388
|
+
* @returns The Updated Profile (profile, the diff between from and using, and the markdown)
|
|
389
|
+
*/
|
|
380
390
|
function updateProfileUsingXCCDF(from, using, id, logger, ovalDefinitions) {
|
|
381
391
|
logger.info(`Updating profile ${from.name} with control IDs type: ${id}`);
|
|
382
|
-
// Parse the XCCDF benchmark and convert it into a Profile
|
|
392
|
+
// Parse the XCCDF benchmark and convert it into a Profile object
|
|
383
393
|
logger.debug('Loading XCCDF File');
|
|
384
394
|
const xccdfProfile = (0, xccdf_1.processXCCDF)(using, false, id, ovalDefinitions);
|
|
385
395
|
logger.debug('Loaded XCCDF File');
|
package/lib/utilities/xccdf.d.ts
CHANGED
|
@@ -1,23 +1,55 @@
|
|
|
1
1
|
import { DecodedDescription } from '../types/xccdf';
|
|
2
2
|
/**
|
|
3
|
-
* Converts an encoded XML string into a JSON object
|
|
3
|
+
* Converts an encoded XML string into a JSON object using specified
|
|
4
|
+
* parsing options.
|
|
4
5
|
*
|
|
5
|
-
* @param encodedXml
|
|
6
|
+
* @param encodedXml - The encoded XML string to be converted.
|
|
7
|
+
* @param xmlParserOption - The parsing option to be used. Defaults to
|
|
8
|
+
* 'withArrayOption'.
|
|
9
|
+
* Possible values are:
|
|
10
|
+
* - 'withArrayOption': Parses XML with array option enabled.
|
|
11
|
+
* - 'withArrayNoEntitiesOption': Parses XML with array option
|
|
12
|
+
* enabled and processes entities.
|
|
13
|
+
* - Any other value: Parses XML without array option.
|
|
6
14
|
* @returns The JSON representation of the XML string.
|
|
7
15
|
*
|
|
8
16
|
* @remarks
|
|
9
17
|
* This function uses the `fast-xml-parser` library to parse the XML string.
|
|
10
18
|
* The parser options are configured to:
|
|
11
|
-
* -
|
|
19
|
+
* - Prevent the parser from converting XML entities (converting < into <)
|
|
20
|
+
* - Ignore attributes, allow or disallows attributes to be parsed
|
|
12
21
|
* - Remove namespace prefixes.
|
|
13
22
|
* - Prefix attribute names with '@_'.
|
|
14
|
-
* - Stop parsing
|
|
15
|
-
* - Treat all nodes as arrays
|
|
23
|
+
* - Stop parsing 'div' and 'p' tags.
|
|
24
|
+
* - Treat all nodes as arrays or not
|
|
25
|
+
*
|
|
26
|
+
* Options being used for the XML parser (V4) are:
|
|
27
|
+
* - processEntities: true or false (based on xmlParserOption)
|
|
28
|
+
* - ignoreAttributes: false (allow attributes to be parsed)
|
|
29
|
+
* - removeNSPrefix: true (remove namespace prefixes)
|
|
30
|
+
* - attributeNamePrefix: '@_' (prefix all attribute names with @_)
|
|
31
|
+
* - stopNodes: ["*.pre", "*.p"]
|
|
32
|
+
* - isArray(): true or false (based on xmlParserOption)
|
|
33
|
+
*
|
|
34
|
+
* NOTE: The isArray can specify what tags to always convert into an array, we
|
|
35
|
+
* do not specify specific fields as it could break parsing if future
|
|
36
|
+
* fields are added, we parse all fields as an array.
|
|
16
37
|
*
|
|
17
38
|
* For more details on the parser options, see the documentation for the v4 or v5 version of the library:
|
|
18
39
|
* {@link https://github.com/NaturalIntelligence/fast-xml-parser/tree/master/docs/v4}
|
|
19
40
|
*/
|
|
20
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Converts an encoded XML string into a JSON object using specified parsing options.
|
|
43
|
+
*
|
|
44
|
+
* @param encodedXml - The encoded XML string to be converted.
|
|
45
|
+
* @param xmlParserOption - The parsing option to be used. Defaults to 'withArrayOption'.
|
|
46
|
+
* Possible values are:
|
|
47
|
+
* - 'withArrayOption': Parses XML with array option enabled.
|
|
48
|
+
* - 'withArrayNoEntitiesOption': Parses XML with array option enabled and processes entities.
|
|
49
|
+
* - Any other value: Parses XML without array option.
|
|
50
|
+
* @returns The JSON object resulting from the XML parsing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function convertEncodedXmlIntoJson(encodedXml: string, xmlParserOption?: string): any;
|
|
21
53
|
/**
|
|
22
54
|
* Converts a JSON object into an XML string.
|
|
23
55
|
*
|
|
@@ -35,6 +67,20 @@ export declare function convertJsonIntoXML(data: any): string;
|
|
|
35
67
|
* @returns The decoded string with XML special characters removed.
|
|
36
68
|
*/
|
|
37
69
|
export declare function removeXMLSpecialCharacters(str: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Removes all of the HTML tags and leaves only the text content.
|
|
72
|
+
*
|
|
73
|
+
* @param input - The string from which HTML tags should be removed.
|
|
74
|
+
* @returns A new string with all HTML tags removed.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const str = '<div>Hello <b>World</b>!</div>';
|
|
79
|
+
* const stripped = removeHtmlTags(str);
|
|
80
|
+
* console.log(stripped); // Output: "Hello World!"
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function removeHtmlTags(input: string): string;
|
|
38
84
|
/**
|
|
39
85
|
* Converts a severity string to a numerical impact value.
|
|
40
86
|
*
|
package/lib/utilities/xccdf.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.convertEncodedXmlIntoJson = convertEncodedXmlIntoJson;
|
|
4
4
|
exports.convertJsonIntoXML = convertJsonIntoXML;
|
|
5
5
|
exports.removeXMLSpecialCharacters = removeXMLSpecialCharacters;
|
|
6
|
+
exports.removeHtmlTags = removeHtmlTags;
|
|
6
7
|
exports.severityStringToImpact = severityStringToImpact;
|
|
7
8
|
exports.impactNumberToSeverityString = impactNumberToSeverityString;
|
|
8
9
|
exports.convertEncodedHTMLIntoJson = convertEncodedHTMLIntoJson;
|
|
@@ -13,33 +14,85 @@ const htmlparser = tslib_1.__importStar(require("htmlparser2"));
|
|
|
13
14
|
const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
|
14
15
|
const he_1 = tslib_1.__importDefault(require("he"));
|
|
15
16
|
/**
|
|
16
|
-
* Converts an encoded XML string into a JSON object
|
|
17
|
+
* Converts an encoded XML string into a JSON object using specified
|
|
18
|
+
* parsing options.
|
|
17
19
|
*
|
|
18
|
-
* @param encodedXml
|
|
20
|
+
* @param encodedXml - The encoded XML string to be converted.
|
|
21
|
+
* @param xmlParserOption - The parsing option to be used. Defaults to
|
|
22
|
+
* 'withArrayOption'.
|
|
23
|
+
* Possible values are:
|
|
24
|
+
* - 'withArrayOption': Parses XML with array option enabled.
|
|
25
|
+
* - 'withArrayNoEntitiesOption': Parses XML with array option
|
|
26
|
+
* enabled and processes entities.
|
|
27
|
+
* - Any other value: Parses XML without array option.
|
|
19
28
|
* @returns The JSON representation of the XML string.
|
|
20
29
|
*
|
|
21
30
|
* @remarks
|
|
22
31
|
* This function uses the `fast-xml-parser` library to parse the XML string.
|
|
23
32
|
* The parser options are configured to:
|
|
24
|
-
* -
|
|
33
|
+
* - Prevent the parser from converting XML entities (converting < into <)
|
|
34
|
+
* - Ignore attributes, allow or disallows attributes to be parsed
|
|
25
35
|
* - Remove namespace prefixes.
|
|
26
36
|
* - Prefix attribute names with '@_'.
|
|
27
|
-
* - Stop parsing
|
|
28
|
-
* - Treat all nodes as arrays
|
|
37
|
+
* - Stop parsing 'div' and 'p' tags.
|
|
38
|
+
* - Treat all nodes as arrays or not
|
|
39
|
+
*
|
|
40
|
+
* Options being used for the XML parser (V4) are:
|
|
41
|
+
* - processEntities: true or false (based on xmlParserOption)
|
|
42
|
+
* - ignoreAttributes: false (allow attributes to be parsed)
|
|
43
|
+
* - removeNSPrefix: true (remove namespace prefixes)
|
|
44
|
+
* - attributeNamePrefix: '@_' (prefix all attribute names with @_)
|
|
45
|
+
* - stopNodes: ["*.pre", "*.p"]
|
|
46
|
+
* - isArray(): true or false (based on xmlParserOption)
|
|
47
|
+
*
|
|
48
|
+
* NOTE: The isArray can specify what tags to always convert into an array, we
|
|
49
|
+
* do not specify specific fields as it could break parsing if future
|
|
50
|
+
* fields are added, we parse all fields as an array.
|
|
29
51
|
*
|
|
30
52
|
* For more details on the parser options, see the documentation for the v4 or v5 version of the library:
|
|
31
53
|
* {@link https://github.com/NaturalIntelligence/fast-xml-parser/tree/master/docs/v4}
|
|
32
54
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Converts an encoded XML string into a JSON object using specified parsing options.
|
|
57
|
+
*
|
|
58
|
+
* @param encodedXml - The encoded XML string to be converted.
|
|
59
|
+
* @param xmlParserOption - The parsing option to be used. Defaults to 'withArrayOption'.
|
|
60
|
+
* Possible values are:
|
|
61
|
+
* - 'withArrayOption': Parses XML with array option enabled.
|
|
62
|
+
* - 'withArrayNoEntitiesOption': Parses XML with array option enabled and processes entities.
|
|
63
|
+
* - Any other value: Parses XML without array option.
|
|
64
|
+
* @returns The JSON object resulting from the XML parsing.
|
|
65
|
+
*/
|
|
66
|
+
function convertEncodedXmlIntoJson(encodedXml, xmlParserOption = 'withArrayOption') {
|
|
67
|
+
const withArrayOption = {
|
|
68
|
+
processEntities: false,
|
|
35
69
|
ignoreAttributes: false,
|
|
36
70
|
removeNSPrefix: true,
|
|
37
71
|
attributeNamePrefix: '@_',
|
|
38
|
-
stopNodes: ['div', 'p'],
|
|
39
|
-
|
|
40
|
-
isArray: (_name, _jpath, _isLeafNode, _isAttribute) => true,
|
|
72
|
+
stopNodes: ['*.div', '*.p'],
|
|
73
|
+
isArray: () => true,
|
|
41
74
|
};
|
|
42
|
-
const
|
|
75
|
+
const withArrayNoEntitiesOption = {
|
|
76
|
+
processEntities: true,
|
|
77
|
+
ignoreAttributes: false,
|
|
78
|
+
removeNSPrefix: true,
|
|
79
|
+
attributeNamePrefix: '@_',
|
|
80
|
+
stopNodes: ['*.div', '*.p'],
|
|
81
|
+
isArray: () => true,
|
|
82
|
+
};
|
|
83
|
+
const noArrayOption = {
|
|
84
|
+
processEntities: false,
|
|
85
|
+
ignoreAttributes: false,
|
|
86
|
+
removeNSPrefix: true,
|
|
87
|
+
attributeNamePrefix: '@_',
|
|
88
|
+
stopNodes: ['*.div', '*.p'],
|
|
89
|
+
isArray: () => false,
|
|
90
|
+
};
|
|
91
|
+
const parser = new fast_xml_parser_1.XMLParser(xmlParserOption === 'withArrayOption'
|
|
92
|
+
? withArrayOption
|
|
93
|
+
: xmlParserOption === 'withArrayNoEntitiesOption'
|
|
94
|
+
? withArrayNoEntitiesOption
|
|
95
|
+
: noArrayOption);
|
|
43
96
|
return parser.parse(encodedXml);
|
|
44
97
|
}
|
|
45
98
|
/**
|
|
@@ -61,11 +114,35 @@ function convertJsonIntoXML(data) {
|
|
|
61
114
|
* @returns The decoded string with XML special characters removed.
|
|
62
115
|
*/
|
|
63
116
|
function removeXMLSpecialCharacters(str) {
|
|
64
|
-
//console.log('Remove special characters: ', JSON.stringify(str, null, 2));
|
|
65
117
|
const result = he_1.default.decode(str);
|
|
66
|
-
//console.log('Result of he.decode: ', JSON.stringify(result));
|
|
67
118
|
return result;
|
|
68
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Removes all of the HTML tags and leaves only the text content.
|
|
122
|
+
*
|
|
123
|
+
* @param input - The string from which HTML tags should be removed.
|
|
124
|
+
* @returns A new string with all HTML tags removed.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const str = '<div>Hello <b>World</b>!</div>';
|
|
129
|
+
* const stripped = removeHtmlTags(str);
|
|
130
|
+
* console.log(stripped); // Output: "Hello World!"
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
function removeHtmlTags(input) {
|
|
134
|
+
// Regex explained
|
|
135
|
+
// <: Matches the opening angle bracket of an HTML tag
|
|
136
|
+
// /?: Matches zero or one forward slash /, to include closing tags
|
|
137
|
+
// [^>]: Matches any character except the > symbol
|
|
138
|
+
// +: Ensures preceding pattern ([^>]) matches one or more characters
|
|
139
|
+
// (>|$):
|
|
140
|
+
// > matches the closing angle bracket of an HTML tag.
|
|
141
|
+
// $ matches the end of the string. This ensures the regex can handle
|
|
142
|
+
// cases where the tag is incomplete or unclosed (e.g., <div)
|
|
143
|
+
// g: Global flag to find all matches in the input string
|
|
144
|
+
return input.replace(/<\/?[^>]+(>|$)/g, '');
|
|
145
|
+
}
|
|
69
146
|
/**
|
|
70
147
|
* Converts a severity string to a numerical impact value.
|
|
71
148
|
*
|
|
@@ -166,7 +243,7 @@ function convertEncodedHTMLIntoJson(encodedHTML) {
|
|
|
166
243
|
});
|
|
167
244
|
htmlParser.write(patchedHTML);
|
|
168
245
|
htmlParser.end();
|
|
169
|
-
const converted = convertEncodedXmlIntoJson(xmlChunks.join(''));
|
|
246
|
+
const converted = convertEncodedXmlIntoJson(xmlChunks.join(''), 'noArrayOption');
|
|
170
247
|
let cleaned = {};
|
|
171
248
|
// Some STIGs have xml tags inside of the actual text which breaks processing,
|
|
172
249
|
// e.g U_ASD_STIG_V5R1_Manual-xccdf.xml and all Oracle Database STIGs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mitre/inspec-objects",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Typescript objects for normalizing between InSpec profiles and XCCDF benchmarks",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"@types/lodash": "^4.14.178",
|
|
32
32
|
"@types/mustache": "^4.2.0",
|
|
33
33
|
"@types/pretty": "^2.0.1",
|
|
34
|
-
"fast-xml-parser": "^
|
|
34
|
+
"fast-xml-parser": "^5.0.7",
|
|
35
35
|
"flat": "5.0.2",
|
|
36
36
|
"he": "^1.2.0",
|
|
37
37
|
"htmlparser2": "^10.0.0",
|
|
38
38
|
"inspecjs": "^2.6.6",
|
|
39
39
|
"json-diff": "^1.0.6",
|
|
40
|
-
"jstoxml": "^
|
|
40
|
+
"jstoxml": "^6.0.1",
|
|
41
41
|
"lodash": "^4.17.21",
|
|
42
42
|
"mustache": "^4.2.0",
|
|
43
43
|
"pretty": "^2.0.0",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"yaml": "^2.3.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/jest": "^
|
|
49
|
-
"@types/node": "^
|
|
48
|
+
"@types/jest": "^30.0.0",
|
|
49
|
+
"@types/node": "^24.0.0",
|
|
50
50
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
|
51
51
|
"@typescript-eslint/parser": "^6.0.0",
|
|
52
52
|
"eslint": "^8.30.0",
|
|
53
|
-
"jest": "^
|
|
53
|
+
"jest": "^30.0.0",
|
|
54
54
|
"ts-jest": "^29.1.1",
|
|
55
55
|
"tslib": "^2.4.0",
|
|
56
56
|
"typescript": "^5.2.2"
|