@mitre/inspec-objects 0.0.2 → 0.0.5

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@mitre/inspec-objects",
3
- "version": "0.0.2",
3
+ "version": "0.0.5",
4
4
  "description": "Typescript objects for normalizing between InSpec profiles and XCCDF benchmarks",
5
- "main": "index.ts",
5
+ "main": "lib/index.js",
6
6
  "publishConfig": {
7
7
  "main": "lib/index.js"
8
8
  },
@@ -22,7 +22,6 @@
22
22
  },
23
23
  "homepage": "https://github.com/mitre/ts-inspec-objects#readme",
24
24
  "dependencies": {
25
- "@mitre/hdf-converters": "file:../heimdall2/libs/hdf-converters/mitre-hdf-converters-v2.6.27.tgz",
26
25
  "@types/flat": "^5.0.2",
27
26
  "@types/json-diff": "^0.7.0",
28
27
  "@types/lodash": "^4.14.178",
@@ -41,7 +40,8 @@
41
40
  "@types/jest": "^28.1.1",
42
41
  "@types/node": "^17.0.18",
43
42
  "@typescript-eslint/eslint-plugin": "^5.12.0",
44
- "eslint": "^8.9.0"
43
+ "eslint": "^8.9.0",
44
+ "tslib": "^2.4.0"
45
45
  },
46
46
  "jest": {
47
47
  "rootDir": ".",
package/tsconfig.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "include": [
17
17
  "index.ts",
18
18
  "src/**/*",
19
- "types/**/*.d.ts"
19
+ "types/*"
20
20
  ],
21
21
  }
22
22
 
package/error.log DELETED
@@ -1,12 +0,0 @@
1
- yarn run v1.22.19
2
- $ jest 2
3
- No tests found, exiting with code 1
4
- Run with `--passWithNoTests` to exit with code 0
5
- In /home/c/Documents/SAF/ts-inspec-objects
6
- 219 files checked.
7
- testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x) - 4 matches
8
- testPathIgnorePatterns: /node_modules/ - 219 matches
9
- testRegex: - 0 matches
10
- Pattern: 2 - 0 matches
11
- error Command failed with exit code 1.
12
- info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Binary file
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
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'
@@ -1,148 +0,0 @@
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 | undefined } | null;
12
- impact?: number;
13
- ref?: string;
14
- refs?: (string | {
15
- ref?: string;
16
- url?: string;
17
- uri?: string;
18
- })[];
19
- tags: {
20
- check?: string;
21
- fix?: string;
22
- severity?: string;
23
- gtitle?: string;
24
- gid?: string;
25
- satisfies?: string[];
26
- rid?: string;
27
- stig_id?: string;
28
- fix_id?: string | null;
29
- cci?: string[];
30
- cis_controls?: Record<string, string[]>[];
31
- nist?: string[];
32
- legacy?: string[];
33
- false_negatives?: string;
34
- false_positives?: string;
35
- documentable?: boolean;
36
- mitigations?: string;
37
- severity_override_guidance?: string;
38
- potential_impacts?: string;
39
- third_party_tools?: string;
40
- mitigation_controls?: string;
41
- responsibility?: string;
42
- ia_controls?: string;
43
- [key: string]:
44
- | string
45
- | string[]
46
- | Record<string, string[]>[]
47
- | boolean
48
- | undefined
49
- | null;
50
- } = {};
51
-
52
- constructor(data?: Partial<Control>) {
53
- this.refs = []
54
- this.tags = {}
55
- if (data) {
56
- Object.entries(data).forEach(([key, value]) => {
57
- _.set(this, key, value);
58
- });
59
- }
60
- }
61
-
62
- toUnformattedObject(): Control {
63
- const flattened: Record<string, string | number> = flatten(this)
64
-
65
- Object.entries(flattened).forEach(([key, value]) => {
66
- if(typeof value === 'string') {
67
- _.set(flattened, key, unformatText(value));
68
- }
69
- });
70
-
71
- return new Control(unflatten(flattened));
72
- }
73
-
74
- toRuby(lineLength: number = 80) {
75
- let result = "# encoding: UTF-8\n\n";
76
-
77
- result += `control "${this.id}" do\n`;
78
- if (this.title) {
79
- result += ` title "${wrapAndEscapeQuotes(this.title, lineLength)}"\n`;
80
- } else {
81
- console.error(`${this.id} does not have a title`);
82
- }
83
-
84
- if (this.desc) {
85
- result += ` desc "${wrapAndEscapeQuotes(this.desc, lineLength)}"\n`;
86
- } else {
87
- console.error(`${this.id} does not have a desc`);
88
- }
89
-
90
- if (this.descs) {
91
- Object.entries(this.descs).forEach(([key, desc]) => {
92
- if (desc) {
93
- result += ` desc "${key}", "${wrapAndEscapeQuotes(
94
- desc,
95
- lineLength
96
- )}"\n`;
97
- } else {
98
- console.error(`${this.id} does not have a desc for the value ${key}`);
99
- }
100
- });
101
- }
102
-
103
- if (this.impact) {
104
- result += ` impact ${this.impact}\n`;
105
- } else {
106
- console.error(`${this.id} does not have an impact`);
107
- }
108
-
109
- if (this.refs) {
110
- this.refs.forEach((ref) => {
111
- if (typeof ref === 'string') {
112
- result += ` ref '${escapeQuotes(ref)}'\n`;
113
- } else {
114
- result += ` ref '${escapeQuotes(ref.ref || '')}', url: '${escapeQuotes(ref.url || '')}'`
115
- }
116
-
117
- });
118
- }
119
-
120
- Object.entries(this.tags).forEach(([tag, value]) => {
121
- if (value) {
122
- if (typeof value === "object") {
123
- if (Array.isArray(value) && typeof value[0] === "string") {
124
- result += ` tag ${tag}: ${JSON.stringify(value)}\n`;
125
- } else {
126
- // Convert JSON Object to Ruby Hash
127
- const stringifiedObject = JSON.stringify(value, null, 2)
128
- .replace(/\n/g, "\n ")
129
- .replace(/\{\n {6}/g, "{")
130
- .replace(/\[\n {8}/g, "[")
131
- .replace(/\n {6}\]/g, "]")
132
- .replace(/\n {4}\}/g, "}")
133
- .replace(/": \[/g, '" => [');
134
- result += ` tag ${tag}: ${stringifiedObject}\n`;
135
- }
136
- } else if (typeof value === "string") {
137
- result += ` tag ${tag}: "${wrapAndEscapeQuotes(
138
- value,
139
- lineLength
140
- )}"\n`;
141
- }
142
- }
143
- });
144
- result += "end";
145
-
146
- return result;
147
- }
148
- }
@@ -1,93 +0,0 @@
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
- }
@@ -1,92 +0,0 @@
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
- }
@@ -1,18 +0,0 @@
1
- import { convertEncodedXmlIntoJson } from "../utilities/xccdf"
2
- import {OvalDefinitionValue, Oval} from '../types/oval'
3
-
4
- export function processOVAL(oval: string): Record<string, OvalDefinitionValue> {
5
- const parsed: Oval = convertEncodedXmlIntoJson(oval)
6
-
7
- const extractedDefinitions: Record<string, OvalDefinitionValue> = {}
8
-
9
- for (const ovalDefinitions of parsed.oval_definitions) {
10
- for (const definitionList of ovalDefinitions.definitions) {
11
- for (const definition of definitionList.definition) {
12
- extractedDefinitions[definition["@_id"]] = definition
13
- }
14
- }
15
- }
16
-
17
- return extractedDefinitions
18
- }
@@ -1,222 +0,0 @@
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 { BenchmarkGroup, BenchmarkRule, DecodedDescription, FrontMatter, Notice, ParsedXCCDF, RationaleElement } from '../types/xccdf';
5
- import Control from '../objects/control';
6
- import _ from 'lodash';
7
- import { OvalDefinitionValue } from '../types/oval';
8
- import { CciNistMappingData } from '@mitre/hdf-converters';
9
-
10
- export type GroupContextualizedRule = BenchmarkRule & {group: Omit<BenchmarkGroup, 'Rule' | 'Group'>}
11
-
12
- export function extractAllRules(groups: BenchmarkGroup[]): GroupContextualizedRule[] {
13
- const rules: GroupContextualizedRule[] = [];
14
- groups.forEach((group) => {
15
- if (group.Rule) {
16
- rules.push(...(group.Rule.map((rule) => {
17
- return {
18
- ...rule,
19
- group: _.omit(group, ['Rule', 'Group'])
20
- }
21
- })))
22
- }
23
- if (group.Group) {
24
- rules.push(...extractAllRules(group.Group))
25
- }
26
- })
27
- return rules
28
- }
29
-
30
- export function processXCCDF(xml: string, ovalDefinitions?: Record<string, OvalDefinitionValue>): Profile {
31
- const parsedXML: ParsedXCCDF = convertEncodedXmlIntoJson(xml)
32
- const rules = extractAllRules(parsedXML.Benchmark[0].Group)
33
-
34
- const profile = new Profile({
35
- name: parsedXML.Benchmark[0]['@_id'],
36
- title: (parsedXML.Benchmark[0].title[0] as FrontMatter)['#text'],
37
- summary: (parsedXML.Benchmark[0].description[0] as RationaleElement)['#text']
38
- });
39
-
40
- rules.forEach(rule => {
41
- let extractedDescription: string | DecodedDescription;
42
- if (Array.isArray(rule.description)) {
43
- extractedDescription = rule.description[0]['#text']
44
- } else {
45
- extractedDescription = convertEncodedHTMLIntoJson(rule.description)
46
- }
47
- const control = new Control();
48
-
49
- control.id = rule.group['@_id']
50
- control.title = rule['@_severity'] ? rule.title : `[[[MISSING SEVERITY FROM STIG]]] ${rule.title}`
51
- control.desc = typeof extractedDescription === 'string' ? extractedDescription : extractedDescription.VulnDiscussion?.split('Satisfies: ')[0]
52
- control.impact = severityStringToImpact(rule['@_severity'] || 'critical', rule.group['@_id'])
53
-
54
- if (!control.descs || Array.isArray(control.descs)) {
55
- control.descs = {}
56
- }
57
-
58
- if (rule.check) {
59
- if (rule.check.some((ruleValue) => 'check-content' in ruleValue)) {
60
- control.descs.check = rule.check ? rule.check[0]['check-content'] : 'Missing description'
61
- } else if (rule.check.some((ruleValue) => 'check-content-ref' in ruleValue) && ovalDefinitions) {
62
- let referenceID: string | null = null;
63
- for (const checkContent of rule.check) {
64
- if ('check-content-ref' in checkContent && checkContent['@_system'].includes('oval')) {
65
- for (const checkContentRef of checkContent['check-content-ref']) {
66
- if (checkContentRef['@_name']) {
67
- referenceID = checkContentRef['@_name']
68
- }
69
- }
70
- }
71
- }
72
- if (referenceID && referenceID in ovalDefinitions) {
73
- control.descs.check = ovalDefinitions[referenceID].metadata[0].title
74
- } else if (referenceID ) {
75
- console.warn(`Could not find OVAL definition for ${referenceID}`)
76
- }
77
- }
78
- }
79
-
80
- control.descs.fix = rule.fixtext ? rule.fixtext[0]['#text'] : (rule.fix ? (rule.fix[0] as Notice)['#text'] || 'Missing fix text' : 'Missing fix text')
81
- control.tags.severity = impactNumberToSeverityString(severityStringToImpact(rule['@_severity'] || 'critical', control.id))
82
- control.tags.gid = rule.group['@_id'],
83
- control.tags.rid = rule['@_id']
84
- control.tags.stig_id = rule['version']
85
-
86
- if (typeof rule.group.title === "string") {
87
- control.tags.gtitle = rule.group.title
88
- } else {
89
- control.tags.gtitle = _.get(rule.group, 'title[0].#text')
90
- }
91
-
92
- if (rule['fix'] && rule['fix'].length > 0) {
93
- control.tags.fix_id = rule['fix'][0]['@_id']
94
- } else {
95
- control.tags.fix_id = null
96
- }
97
-
98
- if (rule['rationale']) {
99
- control.tags.rationale = rule['rationale'][0]['#text']
100
- } else {
101
- control.tags.rationale = null
102
- }
103
-
104
- if (typeof extractedDescription === 'object') {
105
- control.tags.satisfies = extractedDescription.VulnDiscussion?.includes('Satisfies: ') && extractedDescription.VulnDiscussion.split('Satisfies: ').length >= 1 ? extractedDescription.VulnDiscussion.split('Satisfies: ')[1].split(',').map(satisfaction => satisfaction.trim()) : undefined
106
- control.tags.false_negatives = extractedDescription.FalseNegatives || undefined
107
- control.tags.false_positives = extractedDescription.FalsePositives || undefined
108
- control.tags.documentable = typeof extractedDescription.Documentable === 'boolean' ? extractedDescription.Documentable : undefined
109
- control.tags.mitigations = extractedDescription.Mitigations || undefined
110
- control.tags.severity_override_guidance = extractedDescription.SeverityOverrideGuidance || undefined
111
- control.tags.potential_impacts = extractedDescription.PotentialImpacts || undefined
112
- control.tags.third_party_tools = extractedDescription.ThirdPartyTools || undefined
113
- control.tags.mitigation_control = extractedDescription.MitigationControl || undefined
114
- control.tags.mitigation_controls = extractedDescription.MitigationControls || undefined
115
- control.tags.responsibility = extractedDescription.Responsibility || undefined
116
- control.tags.ia_controls = extractedDescription.IAControls || undefined
117
- }
118
-
119
- control.tags = _.omitBy(control.tags, (value) => value === undefined)
120
-
121
- // Get all identifiers from the rule
122
- if (rule.ident) {
123
- rule.ident.forEach((identifier) => {
124
- // Get CCIs
125
- if (identifier['@_system'].toLowerCase().includes('cci')) {
126
- if (!('cci' in control.tags)) {
127
- control.tags.cci = []
128
- }
129
- control.tags.cci?.push(identifier['#text'])
130
- }
131
- // Get legacy identifiers
132
- else if (identifier['@_system'].toLowerCase().includes('legacy')) {
133
- if (!('legacy' in control.tags)) {
134
- control.tags.legacy = []
135
- }
136
- control.tags.legacy?.push(identifier['#text'])
137
- }
138
- // Get NIST identifiers
139
- else if (identifier['@_system'].toLowerCase().includes('nist')) {
140
- if (!('nist' in control.tags)) {
141
- control.tags.nist = []
142
- }
143
- control.tags.nist?.push(identifier['#text'])
144
- } else {
145
- // console.log('Alert')
146
- // console.log(identifier['@_system'])
147
- // console.log(identifier['#text'])
148
- }
149
- })
150
- }
151
-
152
- rule.reference?.forEach((reference) => {
153
- if (_.get(reference, '@_href') === '') {
154
- control.refs?.push(_.get(reference, '#text'))
155
- } else {
156
- try {
157
- const referenceText = _.get(reference, '#text') || ''
158
- const referenceURL = _.get(reference, '@_href') || ''
159
- if (referenceURL) {
160
- const parsedURL = new URL(_.get(reference, '@_href'))
161
- if (parsedURL.protocol.toLowerCase().includes('http') || parsedURL.protocol.toLowerCase().includes('https')) {
162
- control.refs?.push({
163
- ref: referenceText,
164
- url: referenceURL
165
- })
166
- } else {
167
- control.refs?.push({
168
- ref: referenceText,
169
- uri: referenceURL
170
- })
171
- }
172
- } else {
173
- if ('title' in reference) {
174
- control.refs?.push(_.get(reference, 'title') as string)
175
- }
176
- }
177
-
178
- // Add the reference to the control tags when seperated by §
179
- if (typeof referenceText === 'string' && referenceText.indexOf('§') !== -1) {
180
- const referenceParts = referenceText.split('§')
181
- if (referenceParts.length == 2) {
182
- let [identifierType, identifier] = referenceText.split('§')
183
- identifierType = identifierType.toLowerCase();
184
- if (!(identifierType in control.tags)) {
185
- control.tags[identifierType] = [identifier]
186
- } else if (Array.isArray(control.tags[identifierType])) {
187
- control.tags[identifierType] = _.union(control.tags[identifierType] as ArrayLike<string>, [identifier])
188
- } else {
189
- console.warn(`Attempted to push identifier to control tags when identifier already exists: ${identifierType}: ${identifier}`)
190
- }
191
- } else {
192
- console.warn("Reference parts of invalid length:")
193
- console.log(referenceParts)
194
- }
195
- }
196
- } catch (e){
197
- console.warn(`Error parsing ref for control ${control.id}: `)
198
- console.warn(JSON.stringify(reference, null, 2))
199
- console.warn(e);
200
- }
201
- }
202
- })
203
-
204
- // Associate any CCIs with NIST tags
205
- if (control.tags.cci) {
206
- control.tags.cci.forEach((cci: string) => {
207
- if (!('nist' in control.tags)) {
208
- control.tags.nist = []
209
- }
210
- if (cci in CciNistMappingData.data) {
211
- control.tags.nist?.push(_.get(CciNistMappingData.data, cci))
212
- }
213
- })
214
- }
215
-
216
- profile.controls.push(control)
217
- })
218
-
219
- profile.controls = _.sortBy(profile.controls, 'id')
220
-
221
- return profile.toUnformattedObject()
222
- }
@@ -1,9 +0,0 @@
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
- }