@mitre/inspec-objects 0.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/LICENSE.md +9 -0
- package/README.md +20 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +8 -0
- package/lib/objects/control.d.ts +42 -0
- package/lib/objects/control.js +88 -0
- package/lib/objects/profile.d.ts +50 -0
- package/lib/objects/profile.js +48 -0
- package/lib/parsers/json.d.ts +6 -0
- package/lib/parsers/json.js +80 -0
- package/lib/parsers/xccdf.d.ts +2 -0
- package/lib/parsers/xccdf.js +73 -0
- package/lib/utilities/diff.d.ts +3 -0
- package/lib/utilities/diff.js +53 -0
- package/lib/utilities/global.d.ts +6 -0
- package/lib/utilities/global.js +18 -0
- package/lib/utilities/xccdf.d.ts +5 -0
- package/lib/utilities/xccdf.js +103 -0
- package/mitre-inspec-objects-v0.0.1.tgz +0 -0
- package/package-lock.json +11247 -0
- package/package.json +53 -0
- package/src/index.ts +5 -0
- package/src/objects/control.ts +137 -0
- package/src/objects/profile.ts +93 -0
- package/src/parsers/json.ts +92 -0
- package/src/parsers/xccdf.ts +74 -0
- package/src/types/diff.d.ts +9 -0
- package/src/types/xccdf.d.ts +126 -0
- package/src/utilities/diff.ts +54 -0
- package/src/utilities/global.ts +23 -0
- package/src/utilities/xccdf.ts +110 -0
- package/test/sample_data/inspec/profiles/redhat-enterprise-linux-7-stig-baseline/spec/fixtures/kitchen/manifests/site.pp +29 -0
- package/test/sample_data/inspec/profiles/redhat-enterprise-linux-7-stig-baseline/spec/fixtures/kitchen/modules/garbage/.gitignore +0 -0
- package/test/sample_data/inspec/profiles/redhat-enterprise-linux-7-stig-baseline/spec/results/.gitkeep +0 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +21 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mitre/inspec-objects",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Typescript objects for normalizing between InSpec profiles and XCCDF benchmarks",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"main": "lib/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p ./tsconfig.build.json",
|
|
11
|
+
"dev": "npx -y ts-node test.ts",
|
|
12
|
+
"test": "jest"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/mitre/ts-inspec-objects.git"
|
|
17
|
+
},
|
|
18
|
+
"author": "The MITRE Security Automation Framework",
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/mitre/ts-inspec-objects/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/mitre/ts-inspec-objects#readme",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@mitre/hdf-converters": "^2.6.9",
|
|
26
|
+
"@types/flat": "^5.0.2",
|
|
27
|
+
"@types/json-diff": "^0.7.0",
|
|
28
|
+
"@types/lodash": "^4.14.178",
|
|
29
|
+
"fast-xml-parser": "^3.1.19",
|
|
30
|
+
"flat": "^5.0.2",
|
|
31
|
+
"htmlparser2": "^7.2.0",
|
|
32
|
+
"inspecjs": "^2.6.6",
|
|
33
|
+
"jest": "^28.1.1",
|
|
34
|
+
"json-diff": "^0.9.0",
|
|
35
|
+
"lodash": "^4.17.21",
|
|
36
|
+
"ts-jest": "^28.0.4",
|
|
37
|
+
"typescript": "^4.5.5",
|
|
38
|
+
"yaml": "^1.10.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/jest": "^28.1.1",
|
|
42
|
+
"@types/node": "^17.0.18",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
|
44
|
+
"eslint": "^8.9.0"
|
|
45
|
+
},
|
|
46
|
+
"jest": {
|
|
47
|
+
"rootDir": ".",
|
|
48
|
+
"testTimeout": 10000000,
|
|
49
|
+
"transform": {
|
|
50
|
+
"^.+\\.ts$": "ts-jest"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ExecJSON } from "inspecjs";
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
import {flatten, unflatten} from "flat"
|
|
4
|
+
import { escapeQuotes, unformatText, wrapAndEscapeQuotes } from "../utilities/global";
|
|
5
|
+
|
|
6
|
+
export default class Control {
|
|
7
|
+
id?: string | null;
|
|
8
|
+
title?: string | null;
|
|
9
|
+
code?: string | null;
|
|
10
|
+
desc?: string | null;
|
|
11
|
+
descs?: ExecJSON.ControlDescription[] | { [key: string]: string } | null;
|
|
12
|
+
impact?: number;
|
|
13
|
+
ref?: string;
|
|
14
|
+
refs?: string[];
|
|
15
|
+
tags: {
|
|
16
|
+
check?: string;
|
|
17
|
+
fix?: string;
|
|
18
|
+
severity?: string;
|
|
19
|
+
gtitle?: string;
|
|
20
|
+
gid?: string;
|
|
21
|
+
satisfies?: string[];
|
|
22
|
+
rid?: string;
|
|
23
|
+
stig_id?: string;
|
|
24
|
+
fix_id?: string;
|
|
25
|
+
cci?: string[];
|
|
26
|
+
cis_controls?: Record<string, string[]>[];
|
|
27
|
+
nist?: string[];
|
|
28
|
+
legacy?: string[];
|
|
29
|
+
false_negatives?: string;
|
|
30
|
+
false_positives?: string;
|
|
31
|
+
documentable?: boolean;
|
|
32
|
+
mitigations?: string;
|
|
33
|
+
severity_override_guidance?: string;
|
|
34
|
+
potential_impacts?: string;
|
|
35
|
+
third_party_tools?: string;
|
|
36
|
+
mitigation_controls?: string;
|
|
37
|
+
responsibility?: string;
|
|
38
|
+
ia_controls?: string;
|
|
39
|
+
[key: string]:
|
|
40
|
+
| string
|
|
41
|
+
| string[]
|
|
42
|
+
| Record<string, string[]>[]
|
|
43
|
+
| boolean
|
|
44
|
+
| undefined
|
|
45
|
+
| null;
|
|
46
|
+
} = {};
|
|
47
|
+
|
|
48
|
+
constructor(data?: Partial<Control>) {
|
|
49
|
+
if (data) {
|
|
50
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
51
|
+
_.set(this, key, value);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
toUnformattedObject(): Control {
|
|
57
|
+
const flattened: Record<string, string | number> = flatten(this)
|
|
58
|
+
|
|
59
|
+
Object.entries(flattened).forEach(([key, value]) => {
|
|
60
|
+
if(typeof value === 'string') {
|
|
61
|
+
_.set(flattened, key, unformatText(value));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return new Control(unflatten(flattened));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
toRuby(lineLength: number = 80) {
|
|
69
|
+
let result = "# encoding: UTF-8\n\n";
|
|
70
|
+
|
|
71
|
+
result += `control "${this.id}" do\n`;
|
|
72
|
+
if (this.title) {
|
|
73
|
+
result += ` title "${wrapAndEscapeQuotes(this.title, lineLength)}"\n`;
|
|
74
|
+
} else {
|
|
75
|
+
console.error(`${this.id} does not have a title`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (this.desc) {
|
|
79
|
+
result += ` desc "${wrapAndEscapeQuotes(this.desc, lineLength)}"\n`;
|
|
80
|
+
} else {
|
|
81
|
+
console.error(`${this.id} does not have a desc`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this.descs) {
|
|
85
|
+
Object.entries(this.descs).forEach(([key, desc]) => {
|
|
86
|
+
if (desc) {
|
|
87
|
+
result += ` desc "${key}", "${wrapAndEscapeQuotes(
|
|
88
|
+
desc,
|
|
89
|
+
lineLength
|
|
90
|
+
)}"\n`;
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`${this.id} does not have a desc for the value ${key}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.impact) {
|
|
98
|
+
result += ` impact ${this.impact}\n`;
|
|
99
|
+
} else {
|
|
100
|
+
console.error(`${this.id} does not have an impact`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (this.refs) {
|
|
104
|
+
this.refs.forEach((ref) => {
|
|
105
|
+
result += ` ref '${escapeQuotes(ref)}'\n`;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Object.entries(this.tags).forEach(([tag, value]) => {
|
|
110
|
+
if (value) {
|
|
111
|
+
if (typeof value === "object") {
|
|
112
|
+
if (Array.isArray(value) && typeof value[0] === "string") {
|
|
113
|
+
result += ` tag ${tag}: ${JSON.stringify(value)}\n`;
|
|
114
|
+
} else {
|
|
115
|
+
// Convert JSON Object to Ruby Hash
|
|
116
|
+
const stringifiedObject = JSON.stringify(value, null, 2)
|
|
117
|
+
.replace(/\n/g, "\n ")
|
|
118
|
+
.replace(/\{\n {6}/g, "{")
|
|
119
|
+
.replace(/\[\n {8}/g, "[")
|
|
120
|
+
.replace(/\n {6}\]/g, "]")
|
|
121
|
+
.replace(/\n {4}\}/g, "}")
|
|
122
|
+
.replace(/": \[/g, '" => [');
|
|
123
|
+
result += ` tag ${tag}: ${stringifiedObject}\n`;
|
|
124
|
+
}
|
|
125
|
+
} else if (typeof value === "string") {
|
|
126
|
+
result += ` tag ${tag}: "${wrapAndEscapeQuotes(
|
|
127
|
+
value,
|
|
128
|
+
lineLength
|
|
129
|
+
)}"\n`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
result += "end";
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Control from "./control";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import _ from "lodash";
|
|
4
|
+
import { unformatText } from "../utilities/global";
|
|
5
|
+
|
|
6
|
+
export default class Profile {
|
|
7
|
+
name?: string | null;
|
|
8
|
+
title?: string | null;
|
|
9
|
+
maintainer?: string | null;
|
|
10
|
+
copyright?: string | null;
|
|
11
|
+
copyright_email?: string | null;
|
|
12
|
+
license?: string | null;
|
|
13
|
+
summary?: string | null;
|
|
14
|
+
description?: string | null;
|
|
15
|
+
version?: string | null;
|
|
16
|
+
inspec_version?: string | null;
|
|
17
|
+
supports: {
|
|
18
|
+
"platform-family"?: string;
|
|
19
|
+
"platform-name"?: string;
|
|
20
|
+
"os-name"?: string;
|
|
21
|
+
"os-family"?: string;
|
|
22
|
+
release?: string;
|
|
23
|
+
platform?: string;
|
|
24
|
+
}[] = [];
|
|
25
|
+
depends: {
|
|
26
|
+
// Required for all
|
|
27
|
+
name: string; // Required for all
|
|
28
|
+
|
|
29
|
+
// Local file
|
|
30
|
+
path?: string; // Local path on disk
|
|
31
|
+
|
|
32
|
+
// Remote HTTP(s)
|
|
33
|
+
url?: string; // Remote URL tarball
|
|
34
|
+
username?: string; // HTTP Basic Authentication Username
|
|
35
|
+
password?: string; // HTTP Basic Authentication Password
|
|
36
|
+
|
|
37
|
+
// Git Repository
|
|
38
|
+
git?: string;
|
|
39
|
+
branch?: string;
|
|
40
|
+
tag?: string;
|
|
41
|
+
commit?: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
relative_path?: string;
|
|
44
|
+
|
|
45
|
+
// Chef Supermarket
|
|
46
|
+
supermarket?: string;
|
|
47
|
+
|
|
48
|
+
// Base Compliance
|
|
49
|
+
compliance?: string;
|
|
50
|
+
}[] = [];
|
|
51
|
+
inputs: { [key: string]: string }[] = [];
|
|
52
|
+
gem_dependencies?: {name: string, version: string}[];
|
|
53
|
+
libraries: string[] = [];
|
|
54
|
+
readme?: string | null;
|
|
55
|
+
files: string[] = [];
|
|
56
|
+
controls: Control[] = [];
|
|
57
|
+
|
|
58
|
+
constructor(data?: Omit<Partial<Profile>, "controls">) {
|
|
59
|
+
if (data) {
|
|
60
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
61
|
+
_.set(this, key, value);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
createInspecYaml(): string {
|
|
67
|
+
return YAML.stringify({
|
|
68
|
+
name: this.name,
|
|
69
|
+
title: this.title,
|
|
70
|
+
maintainer: this.maintainer,
|
|
71
|
+
copyright: this.copyright,
|
|
72
|
+
copyright_email: this.copyright_email,
|
|
73
|
+
license: this.license,
|
|
74
|
+
summary: this.summary,
|
|
75
|
+
description: this.description,
|
|
76
|
+
version: this.version,
|
|
77
|
+
supports: this.supports,
|
|
78
|
+
depends: this.depends,
|
|
79
|
+
inspec_version: this.inspec_version,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toUnformattedObject(): Profile {
|
|
84
|
+
const unformattedProfile: Profile = new Profile(this);
|
|
85
|
+
Object.entries(this).forEach(([key, value]) => {
|
|
86
|
+
if (typeof value === "string") {
|
|
87
|
+
_.set(unformattedProfile, key, unformatText(value));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
unformattedProfile.controls = this.controls.map((control) => control.toUnformattedObject())
|
|
91
|
+
return unformattedProfile;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContextualizedEvaluation,
|
|
3
|
+
ContextualizedProfile,
|
|
4
|
+
contextualizeEvaluation,
|
|
5
|
+
contextualizeProfile,
|
|
6
|
+
ConversionResult,
|
|
7
|
+
convertFile,
|
|
8
|
+
ExecJSON
|
|
9
|
+
} from "inspecjs";
|
|
10
|
+
import _ from "lodash";
|
|
11
|
+
import Control from "../objects/control";
|
|
12
|
+
import Profile from "../objects/profile";
|
|
13
|
+
|
|
14
|
+
export function processEvaluation(evaluationInput: ContextualizedEvaluation) {
|
|
15
|
+
const topLevelProfile = evaluationInput.contains[0];
|
|
16
|
+
const profile = new Profile({
|
|
17
|
+
name: topLevelProfile.data.name,
|
|
18
|
+
title: topLevelProfile.data.title,
|
|
19
|
+
maintainer: topLevelProfile.data.maintainer,
|
|
20
|
+
copyright: topLevelProfile.data.copyright,
|
|
21
|
+
copyright_email: topLevelProfile.data.copyright_email,
|
|
22
|
+
license: _.get(topLevelProfile.data, "license"),
|
|
23
|
+
summary: _.get(topLevelProfile.data, "summary"),
|
|
24
|
+
description: _.get(topLevelProfile.data, "description"),
|
|
25
|
+
version: topLevelProfile.data.version,
|
|
26
|
+
});
|
|
27
|
+
topLevelProfile.contains.forEach((control) => {
|
|
28
|
+
profile.controls.push(
|
|
29
|
+
new Control({
|
|
30
|
+
id: control.data.id,
|
|
31
|
+
title: control.data.title,
|
|
32
|
+
impact: control.data.impact,
|
|
33
|
+
desc: control.data.desc,
|
|
34
|
+
descs: control.hdf.wraps.descriptions,
|
|
35
|
+
tags: control.hdf.wraps.tags,
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
return profile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function processProfileJSON(
|
|
43
|
+
profileInput: ContextualizedProfile
|
|
44
|
+
): Profile {
|
|
45
|
+
const profile = new Profile({
|
|
46
|
+
name: profileInput.data.name,
|
|
47
|
+
title: profileInput.data.title,
|
|
48
|
+
maintainer: profileInput.data.maintainer,
|
|
49
|
+
copyright: profileInput.data.copyright,
|
|
50
|
+
copyright_email: profileInput.data.copyright_email,
|
|
51
|
+
license: _.get(profileInput.data, "license"),
|
|
52
|
+
summary: _.get(profileInput.data, "summary"),
|
|
53
|
+
description: _.get(profileInput.data, "description"),
|
|
54
|
+
version: profileInput.data.version,
|
|
55
|
+
});
|
|
56
|
+
profileInput.data.controls.forEach((control) => {
|
|
57
|
+
profile.controls.push(
|
|
58
|
+
new Control({
|
|
59
|
+
id: control.id,
|
|
60
|
+
title: control.title,
|
|
61
|
+
desc: control.desc,
|
|
62
|
+
impact: control.impact,
|
|
63
|
+
code: control.code,
|
|
64
|
+
tags: control.tags,
|
|
65
|
+
descs: control.descriptions,
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
return profile;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function processExecJSON(execJSON: ExecJSON.Execution) {
|
|
73
|
+
return processEvaluation(contextualizeEvaluation(execJSON));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function processJSON(json: string): Profile {
|
|
77
|
+
const convertedFile: ConversionResult = convertFile(json, true);
|
|
78
|
+
let profile = new Profile();
|
|
79
|
+
if (convertedFile["1_0_ExecJson"]) {
|
|
80
|
+
profile = processEvaluation(
|
|
81
|
+
contextualizeEvaluation(convertedFile["1_0_ExecJson"])
|
|
82
|
+
).toUnformattedObject();
|
|
83
|
+
} else if (convertedFile["1_0_ProfileJson"]) {
|
|
84
|
+
profile = processProfileJSON(contextualizeProfile(JSON.parse(json))).toUnformattedObject();
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error("Unknown file type passed");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
profile.controls = _.sortBy(profile.controls, "id");
|
|
90
|
+
|
|
91
|
+
return profile;
|
|
92
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {data as CCINistMappings} from '@mitre/hdf-converters/lib/src/mappings/CciNistMappingData'
|
|
2
|
+
import Profile from '../objects/profile';
|
|
3
|
+
import { convertEncodedHTMLIntoJson, convertEncodedXmlIntoJson, impactNumberToSeverityString, severityStringToImpact } from '../utilities/xccdf';
|
|
4
|
+
import { DecodedDescription, DisaStig } from '../types/xccdf';
|
|
5
|
+
import Control from '../objects/control';
|
|
6
|
+
import _ from 'lodash';
|
|
7
|
+
|
|
8
|
+
export function processXCCDF(xml: string): Profile {
|
|
9
|
+
const parsedXML: DisaStig = convertEncodedXmlIntoJson(xml)
|
|
10
|
+
const groups = parsedXML.Benchmark.Group;
|
|
11
|
+
|
|
12
|
+
const profile = new Profile({
|
|
13
|
+
name: parsedXML.Benchmark['@_id'],
|
|
14
|
+
title: parsedXML.Benchmark.title,
|
|
15
|
+
summary: parsedXML.Benchmark.description
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
groups.forEach(group => {
|
|
19
|
+
const extractedDescription: DecodedDescription = convertEncodedHTMLIntoJson(group.Rule?.description)
|
|
20
|
+
const control = new Control({
|
|
21
|
+
id: group['@_id'],
|
|
22
|
+
title: group.Rule['@_severity'] ? group.Rule.title : `[[[MISSING SEVERITY FROM STIG]]] ${group.Rule.title}`,
|
|
23
|
+
desc: extractedDescription.VulnDiscussion?.split('Satisfies: ')[0],
|
|
24
|
+
impact: severityStringToImpact(group.Rule['@_severity'] || 'critical'),
|
|
25
|
+
descs: {
|
|
26
|
+
check: group.Rule.check['check-content'],
|
|
27
|
+
fix: group.Rule.fixtext['#text']
|
|
28
|
+
},
|
|
29
|
+
tags: _.omitBy({
|
|
30
|
+
severity: impactNumberToSeverityString(severityStringToImpact(group.Rule['@_severity'] || 'critical')),
|
|
31
|
+
gtitle: group.title,
|
|
32
|
+
satisfies: extractedDescription.VulnDiscussion?.includes('Satisfies: ') && extractedDescription.VulnDiscussion.split('Satisfies: ').length >= 1 ? extractedDescription.VulnDiscussion.split('Satisfies: ')[1].split(',').map(satisfaction => satisfaction.trim()) : undefined,
|
|
33
|
+
gid: group['@_id'],
|
|
34
|
+
rid: group.Rule['@_id'],
|
|
35
|
+
stig_id: group.Rule.version,
|
|
36
|
+
fix_id: group.Rule.fix['@_id'],
|
|
37
|
+
false_negatives: extractedDescription.FalseNegatives,
|
|
38
|
+
false_positives: extractedDescription.FalsePositives,
|
|
39
|
+
documentable: extractedDescription.Documentable,
|
|
40
|
+
mitigations: extractedDescription.Mitigations,
|
|
41
|
+
severity_override_guidance: extractedDescription.SeverityOverrideGuidance,
|
|
42
|
+
potential_impacts: extractedDescription.PotentialImpacts,
|
|
43
|
+
third_party_tools: extractedDescription.ThirdPartyTools,
|
|
44
|
+
mitigation_control: extractedDescription.MitigationControl, // This exists as mitigation_controls in inspec_tools, but is called mitigation_control in the xccdf, this shouldn't ever be defined but is still here for backwards compatibility
|
|
45
|
+
mitigation_controls: extractedDescription.MitigationControls,
|
|
46
|
+
responsibility: extractedDescription.Responsibility,
|
|
47
|
+
ia_controls: extractedDescription.IAControls
|
|
48
|
+
}, i => !Boolean(i))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if ('ident' in group.Rule) {
|
|
52
|
+
const identifiers = Array.isArray(group.Rule.ident) ? group.Rule.ident : [group.Rule.ident]
|
|
53
|
+
// Grab CCI/NIST/Legacy identifiers
|
|
54
|
+
identifiers.forEach(identifier => {
|
|
55
|
+
const identifierText = identifier['#text']
|
|
56
|
+
if (identifier['@_system'].toLowerCase().endsWith('cci')) {
|
|
57
|
+
control.tags.cci?.push(identifierText)
|
|
58
|
+
if (identifierText in CCINistMappings) {
|
|
59
|
+
control.tags.nist?.push(_.get(CCINistMappings, identifierText))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (identifier['@_system'].toLowerCase().endsWith('legacy')) {
|
|
63
|
+
control.tags.legacy?.push(identifierText)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
profile.controls.push(control)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
profile.controls = _.sortBy(profile.controls, 'id')
|
|
72
|
+
|
|
73
|
+
return profile.toUnformattedObject()
|
|
74
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export interface DisaStig {
|
|
2
|
+
Benchmark: Benchmark;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface Benchmark {
|
|
6
|
+
'@_xmlns:dc': string;
|
|
7
|
+
'@_xmlns:xsi': string;
|
|
8
|
+
'@_xmlns:cpe': string;
|
|
9
|
+
'@_xmlns:xhtml': string;
|
|
10
|
+
'@_xmlns:dsig': string;
|
|
11
|
+
'@_xsi:schemaLocation': string;
|
|
12
|
+
'@_id': string;
|
|
13
|
+
'@_xml:lang': string;
|
|
14
|
+
'@_xmlns': string;
|
|
15
|
+
status: Status;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
notice: Notice;
|
|
19
|
+
'front-matter': Matter;
|
|
20
|
+
'rear-matter': Matter;
|
|
21
|
+
reference: BenchmarkReference;
|
|
22
|
+
'plain-text': PlainText[];
|
|
23
|
+
version: number;
|
|
24
|
+
Profile: Group[];
|
|
25
|
+
Group: Group[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Group {
|
|
29
|
+
'@_id': string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
Rule: Rule;
|
|
33
|
+
select?: Select[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Rule {
|
|
37
|
+
'@_id': string;
|
|
38
|
+
'@_weight': string;
|
|
39
|
+
'@_severity': string;
|
|
40
|
+
version: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description: string;
|
|
43
|
+
reference: RuleReference;
|
|
44
|
+
ident: Ident[];
|
|
45
|
+
fixtext: Fixtext;
|
|
46
|
+
fix: Fix;
|
|
47
|
+
check: Check;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Check {
|
|
51
|
+
'@_system': string;
|
|
52
|
+
'check-content-ref': CheckContentRef;
|
|
53
|
+
'check-content': string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CheckContentRef {
|
|
57
|
+
'@_href': string;
|
|
58
|
+
'@_name': string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Fix {
|
|
62
|
+
'@_id': string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Fixtext {
|
|
66
|
+
'#text': string;
|
|
67
|
+
'@_fixref': string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface Ident {
|
|
71
|
+
'#text': string;
|
|
72
|
+
'@_system': string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RuleReference {
|
|
76
|
+
'dc:title': string;
|
|
77
|
+
'dc:publisher': string;
|
|
78
|
+
'dc:type': string;
|
|
79
|
+
'dc:subject': string;
|
|
80
|
+
'dc:identifier': number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface Select {
|
|
84
|
+
'@_idref': string;
|
|
85
|
+
'@_selected': string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface Matter {
|
|
89
|
+
'@_xml:lang': string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface Notice {
|
|
93
|
+
'@_id': string;
|
|
94
|
+
'@_xml:lang': string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PlainText {
|
|
98
|
+
'#text': string;
|
|
99
|
+
'@_id': string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface BenchmarkReference {
|
|
103
|
+
'@_href': string;
|
|
104
|
+
'dc:publisher': string;
|
|
105
|
+
'dc:source': string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface Status {
|
|
109
|
+
'#text': string;
|
|
110
|
+
'@_date': Date;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface DecodedDescription {
|
|
114
|
+
VulnDiscussion?: string;
|
|
115
|
+
FalsePositives?: string;
|
|
116
|
+
FalseNegatives?: string;
|
|
117
|
+
Documentable?: boolean;
|
|
118
|
+
Mitigations?: string;
|
|
119
|
+
SeverityOverrideGuidance?: string;
|
|
120
|
+
PotentialImpacts?: string;
|
|
121
|
+
ThirdPartyTools?: string;
|
|
122
|
+
MitigationControl?: string;
|
|
123
|
+
MitigationControls?: string;
|
|
124
|
+
Responsibility?: string;
|
|
125
|
+
IAControls?: string;
|
|
126
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { diff } from 'json-diff';
|
|
2
|
+
import Profile from '../objects/profile';
|
|
3
|
+
import { ProfileDiff } from '../types/diff';
|
|
4
|
+
import _ from 'lodash'
|
|
5
|
+
|
|
6
|
+
export function diffProfile(fromProfile: Profile, toProfile: Profile): ProfileDiff {
|
|
7
|
+
const profileDiff: ProfileDiff = {
|
|
8
|
+
addedControlIDs: [],
|
|
9
|
+
removedControlIDs: [],
|
|
10
|
+
changedControls: {}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const fromControlIDs = fromProfile.controls.map((control) => control.id).sort();
|
|
14
|
+
const toControlIDs = toProfile.controls.map((control) => control.id).sort();
|
|
15
|
+
|
|
16
|
+
// Find new controls
|
|
17
|
+
const controlIDDiff: string[][] = diff(fromControlIDs, toControlIDs)
|
|
18
|
+
controlIDDiff.forEach((diffValue) => {
|
|
19
|
+
if (diffValue[0] === '-') {
|
|
20
|
+
profileDiff.removedControlIDs.push(diffValue[1])
|
|
21
|
+
} else if (diffValue[0] === '+') {
|
|
22
|
+
profileDiff.addedControlIDs.push(diffValue[1])
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Add new controls to changedControls
|
|
27
|
+
profileDiff.addedControlIDs.forEach((addedControl) => {
|
|
28
|
+
const newControl = toProfile.controls.find((control) => addedControl === control.id)
|
|
29
|
+
if (newControl) {
|
|
30
|
+
profileDiff.changedControls[addedControl] = newControl
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Find changed controls
|
|
35
|
+
for (const fromControl of fromProfile.controls) {
|
|
36
|
+
const toControl = toProfile.controls.find((control) => control.id === fromControl.id)
|
|
37
|
+
if (toControl) {
|
|
38
|
+
const controlDiff: Record<string, any> | undefined = diff(fromControl, toControl);
|
|
39
|
+
if (controlDiff) {
|
|
40
|
+
Object.entries(controlDiff).forEach(([key, value]) => {
|
|
41
|
+
if (_.has(value, '__new')) {
|
|
42
|
+
_.set(profileDiff, 'changedControls.'+fromControl.id +'.'+key.replace('.', '\\.'), _.get(controlDiff, key+'.__new'))
|
|
43
|
+
} else if (typeof value === 'object') {
|
|
44
|
+
Object.entries(value).forEach(([subKey, subValue]) => {
|
|
45
|
+
_.set(profileDiff, 'changedControls.'+fromControl.id +'.'+key.replace('.', '\\.')+'.'+subKey.replace('.', '\\.'), _.get(controlDiff, key+'.'+subKey+'.__new'))
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return profileDiff
|
|
54
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
|
|
3
|
+
// Breaks lines down to lineLength number of characters
|
|
4
|
+
export function wrap(s: string, lineLength = 80): string {
|
|
5
|
+
return s.replace(
|
|
6
|
+
new RegExp(`(?![^\n]{1,${lineLength}}$)([^\n]{1,${lineLength}})`, "g"),
|
|
7
|
+
"$1\n"
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function unformatText(s: string): string {
|
|
12
|
+
return s.replace(/\n/g, ' ').replace(/\\n/g, ' ').replace(/( +|\t)/g, ' ')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const escapeQuotes = (s: string) =>
|
|
16
|
+
s.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); // Escape backslashes and quotes
|
|
17
|
+
const escapeDoubleQuotes = (s: string) =>
|
|
18
|
+
s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); // Escape backslashes and double quotes
|
|
19
|
+
|
|
20
|
+
const wrapAndEscapeQuotes = (s: string, lineLength?: number) =>
|
|
21
|
+
escapeDoubleQuotes(wrap(s, lineLength)); // Escape backslashes and quotes, and wrap long lines
|
|
22
|
+
|
|
23
|
+
export { escapeQuotes, escapeDoubleQuotes, wrapAndEscapeQuotes };
|