@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.
Files changed (36) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +20 -0
  3. package/lib/index.d.ts +5 -0
  4. package/lib/index.js +8 -0
  5. package/lib/objects/control.d.ts +42 -0
  6. package/lib/objects/control.js +88 -0
  7. package/lib/objects/profile.d.ts +50 -0
  8. package/lib/objects/profile.js +48 -0
  9. package/lib/parsers/json.d.ts +6 -0
  10. package/lib/parsers/json.js +80 -0
  11. package/lib/parsers/xccdf.d.ts +2 -0
  12. package/lib/parsers/xccdf.js +73 -0
  13. package/lib/utilities/diff.d.ts +3 -0
  14. package/lib/utilities/diff.js +53 -0
  15. package/lib/utilities/global.d.ts +6 -0
  16. package/lib/utilities/global.js +18 -0
  17. package/lib/utilities/xccdf.d.ts +5 -0
  18. package/lib/utilities/xccdf.js +103 -0
  19. package/mitre-inspec-objects-v0.0.1.tgz +0 -0
  20. package/package-lock.json +11247 -0
  21. package/package.json +53 -0
  22. package/src/index.ts +5 -0
  23. package/src/objects/control.ts +137 -0
  24. package/src/objects/profile.ts +93 -0
  25. package/src/parsers/json.ts +92 -0
  26. package/src/parsers/xccdf.ts +74 -0
  27. package/src/types/diff.d.ts +9 -0
  28. package/src/types/xccdf.d.ts +126 -0
  29. package/src/utilities/diff.ts +54 -0
  30. package/src/utilities/global.ts +23 -0
  31. package/src/utilities/xccdf.ts +110 -0
  32. package/test/sample_data/inspec/profiles/redhat-enterprise-linux-7-stig-baseline/spec/fixtures/kitchen/manifests/site.pp +29 -0
  33. package/test/sample_data/inspec/profiles/redhat-enterprise-linux-7-stig-baseline/spec/fixtures/kitchen/modules/garbage/.gitignore +0 -0
  34. package/test/sample_data/inspec/profiles/redhat-enterprise-linux-7-stig-baseline/spec/results/.gitkeep +0 -0
  35. package/tsconfig.build.json +5 -0
  36. 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,5 @@
1
+ export * from './objects/control'
2
+ export * from './objects/profile'
3
+ export * from './parsers/json'
4
+ export * from './parsers/xccdf'
5
+ export * from './utilities/diff'
@@ -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,9 @@
1
+ import Control from "../objects/control"
2
+
3
+ export type ProfileDiff = {
4
+ removedControlIDs: string[];
5
+ addedControlIDs: string[];
6
+ changedControls: {
7
+ [key: string]: Partial<Control>;
8
+ }
9
+ }
@@ -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 };