@fabasoad/sarif-to-slack 1.2.3 → 1.3.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.
@@ -0,0 +1,198 @@
1
+ import Finding from '../Finding'
2
+ import FindingArray from '../FindingArray'
3
+ import { SecurityLevel, SecuritySeverity } from '../../types'
4
+ import Logger from '../../Logger'
5
+ import {
6
+ ColorGroupByLevel,
7
+ ColorGroupBySeverity,
8
+ ColorGroupCommon,
9
+ ColorOptions
10
+ } from './ColorOptions'
11
+ import { Color } from './Color';
12
+
13
+ function logColorTaken(color: Color | undefined, prop: string): void {
14
+ Logger.debug(`Message has ${color?.color} color taken from '${prop}' property.`)
15
+ }
16
+
17
+ function logPropDefinedButNoFindings<K extends keyof Pick<Finding, 'level' | 'severity'>>(key: K, val: string): void {
18
+ const prop: string = key === 'level' ? 'byLevel' : 'bySeverity'
19
+ Logger.trace(`'${prop}.${val}' property is defined but no findings with "${val}" ${key} is found. Continue color identification...`)
20
+ }
21
+
22
+ function logPropIsNotDefined<K extends keyof Pick<Finding, 'level' | 'severity'>>(key: K, val: string): void {
23
+ const prop: string = key === 'level' ? 'byLevel' : 'bySeverity'
24
+ Logger.trace(`'${prop}.${val}' property is not defined. Continue color identification...`)
25
+ }
26
+
27
+ function identifyColorCommon<K extends keyof Pick<Finding, 'level' | 'severity'>>(
28
+ findings: FindingArray,
29
+ prop: K,
30
+ none: Finding[K],
31
+ unknown: Finding[K],
32
+ color: ColorGroupCommon
33
+ ): string | undefined {
34
+ if (color.none) {
35
+ if (findings.findByProperty(prop, none) != null) {
36
+ logColorTaken(color.none, `${prop === 'severity' ? 'bySeverity' : 'byLevel'}.none`)
37
+ return color.none.color
38
+ } else {
39
+ logPropDefinedButNoFindings(prop, 'none')
40
+ }
41
+ } else {
42
+ logPropIsNotDefined(prop, 'none')
43
+ }
44
+
45
+ if (color.unknown) {
46
+ if (findings.findByProperty(prop, unknown) != null) {
47
+ logColorTaken(color.unknown, `${prop === 'severity' ? 'bySeverity' : 'byLevel'}.unknown`)
48
+ return color.unknown.color
49
+ } else {
50
+ logPropDefinedButNoFindings(prop, 'unknown')
51
+ }
52
+ } else {
53
+ logPropIsNotDefined(prop, 'unknown')
54
+ }
55
+
56
+ return undefined
57
+ }
58
+
59
+ function identifyColorBySeverity(findings: FindingArray, color: ColorGroupBySeverity): string | undefined {
60
+ if (color.critical) {
61
+ if (findings.findByProperty('severity', SecuritySeverity.Critical) != null) {
62
+ logColorTaken(color.critical, 'bySeverity.critical')
63
+ return color.critical.color
64
+ } else {
65
+ logPropDefinedButNoFindings('severity', 'critical')
66
+ }
67
+ } else {
68
+ logPropIsNotDefined('severity', 'critical')
69
+ }
70
+
71
+ if (color.high) {
72
+ if (findings.findByProperty('severity', SecuritySeverity.High) != null) {
73
+ logColorTaken(color.high, 'bySeverity.high')
74
+ return color.high.color
75
+ } else {
76
+ logPropDefinedButNoFindings('severity', 'high')
77
+ }
78
+ } else {
79
+ logPropIsNotDefined('severity', 'high')
80
+ }
81
+
82
+ if (color.medium) {
83
+ if (findings.findByProperty('severity', SecuritySeverity.Medium) != null) {
84
+ logColorTaken(color.medium, 'bySeverity.medium')
85
+ return color.medium.color
86
+ } else {
87
+ logPropDefinedButNoFindings('severity', 'medium')
88
+ }
89
+ } else {
90
+ logPropIsNotDefined('severity', 'medium')
91
+ }
92
+
93
+ if (color.low) {
94
+ if (findings.findByProperty('severity', SecuritySeverity.Low) != null) {
95
+ logColorTaken(color.low, 'bySeverity.low')
96
+ return color.low.color
97
+ } else {
98
+ logPropDefinedButNoFindings('severity', 'low')
99
+ }
100
+ } else {
101
+ logPropIsNotDefined('severity', 'low')
102
+ }
103
+
104
+ return identifyColorCommon(findings, 'severity', SecuritySeverity.None, SecuritySeverity.Unknown, color)
105
+ }
106
+
107
+ function identifyColorByLevel(findings: FindingArray, color: ColorGroupByLevel): string | undefined {
108
+ if (color.error) {
109
+ if (findings.findByProperty('level', SecurityLevel.Error) != null) {
110
+ logColorTaken(color.error, 'byLevel.error')
111
+ return color.error.color
112
+ } else {
113
+ logPropDefinedButNoFindings('level', 'error')
114
+ }
115
+ } else {
116
+ logPropIsNotDefined('level', 'error')
117
+ }
118
+
119
+ if (color.warning) {
120
+ if (findings.findByProperty('level', SecurityLevel.Warning) != null) {
121
+ logColorTaken(color.warning, 'byLevel.warning')
122
+ return color.warning.color
123
+ } else {
124
+ logPropDefinedButNoFindings('level', 'warning')
125
+ }
126
+ } else {
127
+ logPropIsNotDefined('level', 'warning')
128
+ }
129
+
130
+ if (color.note != null) {
131
+ if (findings.findByProperty('level', SecurityLevel.Note) != null) {
132
+ logColorTaken(color.note, 'byLevel.note')
133
+ return color.note.color
134
+ } else {
135
+ logPropDefinedButNoFindings('level', 'note')
136
+ }
137
+ } else {
138
+ logPropIsNotDefined('level', 'note')
139
+ }
140
+
141
+ return identifyColorCommon(findings, 'level', SecurityLevel.None, SecurityLevel.Unknown, color)
142
+ }
143
+
144
+ /**
145
+ * Makes an ultimate decision on what color should be Slack message. The decision
146
+ * is based on the provided {@param colorOpts} parameter and {@param findings}
147
+ * list.
148
+ * @param findings An instance of {@link FindingArray} object.
149
+ * @param colorOpts An instance of {@link ColorOptions} type.
150
+ * @internal
151
+ */
152
+ export function identifyColor(findings: FindingArray, colorOpts?: ColorOptions): string | undefined {
153
+ if (!colorOpts) {
154
+ Logger.debug('Message has no color as color options are not defined.')
155
+ return undefined
156
+ }
157
+ Logger.trace(`Identifying color for ${findings.length} findings and the following color options:`, JSON.stringify(colorOpts, null, 2))
158
+
159
+ if (colorOpts.bySeverity) {
160
+ const color: string | undefined = identifyColorBySeverity(findings, colorOpts.bySeverity)
161
+ if (color) {
162
+ return color
163
+ }
164
+ Logger.trace('None of the properties in \'bySeverity\' group is applicable. Continue color identification...')
165
+ } else {
166
+ Logger.trace('\'bySeverity\' group is not defined. Continue color identification...')
167
+ }
168
+
169
+ if (colorOpts.byLevel) {
170
+ const color: string | undefined = identifyColorByLevel(findings, colorOpts.byLevel)
171
+ if (color) {
172
+ return color
173
+ }
174
+ Logger.trace('None of the properties in \'byLevel\' group is applicable. Continue color identification...')
175
+ } else {
176
+ Logger.trace('\'byLevel\' group is not defined. Continue color identification...')
177
+ }
178
+
179
+ if (findings.length === 0) {
180
+ Logger.trace('There are no findings in the provided SARIF file(s). Checking if color is defined in "empty" property...')
181
+ if (colorOpts.empty?.color) {
182
+ logColorTaken(colorOpts.empty, 'empty')
183
+ return colorOpts.empty.color
184
+ } else {
185
+ Logger.trace('"empty" color is not defined. Continue color identification...')
186
+ }
187
+ } else {
188
+ Logger.trace(`"empty" color is not taken into account because there are ${findings.length} findings in the provided SARIF file(s). Continue color identification...`)
189
+ }
190
+
191
+ if (colorOpts.default?.color) {
192
+ logColorTaken(colorOpts.default, 'default')
193
+ } else {
194
+ Logger.debug('Message has no color as none of the defined color options is applicable.')
195
+ }
196
+
197
+ return colorOpts?.default?.color
198
+ }
@@ -0,0 +1,63 @@
1
+ import { Color } from './Color'
2
+
3
+ /**
4
+ * Base type that has common fields for both {@link ColorGroupByLevel} and
5
+ * {@link ColorGroupBySeverity}.
6
+ * @public
7
+ */
8
+ export type ColorGroupCommon = {
9
+ none?: Color,
10
+ unknown?: Color,
11
+ }
12
+
13
+ /**
14
+ * Color schema for the findings with the certain level. Color is used by the
15
+ * level importance, i.e. if at least 1 error finding exists then
16
+ * {@link ColorGroupByLevel#error} color is used, then if at least 1 warning
17
+ * finding exists then {@link ColorGroupByLevel#warning} color is used, etc.
18
+ * @public
19
+ */
20
+ export type ColorGroupByLevel = ColorGroupCommon & {
21
+ error?: Color,
22
+ warning?: Color,
23
+ note?: Color,
24
+ }
25
+
26
+ /**
27
+ * Color schema for the findings with the certain severity. Color is used by the
28
+ * severity importance, i.e. if at least 1 critical finding exists then
29
+ * {@link ColorGroupBySeverity#critical} color is used, then if at least 1 high
30
+ * finding exists then {@link ColorGroupBySeverity#high} color is used, etc.
31
+ * @public
32
+ */
33
+ export type ColorGroupBySeverity = ColorGroupCommon & {
34
+ critical?: Color,
35
+ high?: Color,
36
+ medium?: Color,
37
+ low?: Color,
38
+ }
39
+
40
+ /**
41
+ * Represents configuration of the color scheme. If both {@link ColorOptions#byLevel}
42
+ * and {@link ColorOptions#bySeverity} are defined, then {@link ColorOptions#bySeverity}
43
+ * takes precedence.
44
+ * @public
45
+ */
46
+ export type ColorOptions = {
47
+ /**
48
+ * Default color if specific color was not found. It is a fallback option.
49
+ */
50
+ default?: Color,
51
+ /**
52
+ * Color scheme for the findings where certain level is presented.
53
+ */
54
+ byLevel?: ColorGroupByLevel,
55
+ /**
56
+ * Color scheme for the findings where certain severity is presented.
57
+ */
58
+ bySeverity?: ColorGroupBySeverity,
59
+ /**
60
+ * Color when no findings are found.
61
+ */
62
+ empty?: Color,
63
+ }
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Run } from 'sarif'
2
- import { ColorOptions } from './model/Color'
2
+ import { ColorOptions } from './model/color/ColorOptions'
3
3
  import FindingArray from './model/FindingArray'
4
4
  import { SendIf } from './model/SendIf'
5
5
 
@@ -7,24 +7,28 @@ import * as path from 'path'
7
7
  * Traverse directory recursively and returns list of files with the requested
8
8
  * extension.
9
9
  * @param dir A root directory. Starting point.
10
+ * @param recursive Whether to list files recursively or not.
10
11
  * @param extension An instance of {@link SarifFileExtension} type.
11
12
  * @param fileList Collected list of files.
12
13
  * @private
13
14
  */
14
- function listFilesRecursively(
15
+ function listFiles(
15
16
  dir: string,
17
+ recursive: boolean,
16
18
  extension: SarifFileExtension,
17
19
  fileList: string[] = []
18
20
  ): string[] {
19
- const entries: string[] = fs.readdirSync(dir)
20
- entries.forEach((entry: string): void => {
21
- const fullPath: string = path.join(dir, entry)
22
- if (fs.statSync(fullPath).isDirectory()) {
23
- listFilesRecursively(fullPath, extension, fileList)
24
- } else if (path.extname(fullPath).toLowerCase() === `.${extension}`) {
25
- fileList.push(fullPath)
26
- }
27
- })
21
+ if (fs.statSync(dir).isDirectory()) {
22
+ const entries: string[] = fs.readdirSync(dir)
23
+ entries.forEach((entry: string): void => {
24
+ const fullPath: string = path.join(dir, entry)
25
+ if (recursive && fs.statSync(fullPath).isDirectory()) {
26
+ listFiles(fullPath, recursive, extension, fileList)
27
+ } else if (path.extname(fullPath).toLowerCase() === `.${extension}`) {
28
+ fileList.push(fullPath)
29
+ }
30
+ })
31
+ }
28
32
  return fileList
29
33
  }
30
34
 
@@ -43,9 +47,7 @@ export function extractListOfFiles(opts: SarifOptions): string[] {
43
47
 
44
48
  if (stats.isDirectory()) {
45
49
  Logger.info(`Provided path is a directory: ${opts.path}`)
46
- const files: string[] = opts.recursive
47
- && listFilesRecursively(opts.path, opts.extension ?? 'sarif')
48
- || fs.readdirSync(opts.path)
50
+ const files: string[] = listFiles(opts.path, !!opts.recursive, opts.extension ?? 'sarif')
49
51
  Logger.info(`Found ${files.length} files in ${opts.path} directory with ${opts.extension} extension`)
50
52
  Logger.debug(`Found files: ${files.join(', ')}`)
51
53
  return files
@@ -107,28 +107,28 @@ describe('(integration): SendSarifToSlack', (): void => {
107
107
  username: process.env.SARIF_TO_SLACK_USERNAME,
108
108
  iconUrl: process.env.SARIF_TO_SLACK_ICON_URL,
109
109
  color: {
110
- default: new Color(process.env.SARIF_TO_SLACK_COLOR),
111
- empty: new Color(process.env.SARIF_TO_SLACK_COLOR_EMPTY),
110
+ default: Color.from(process.env.SARIF_TO_SLACK_COLOR),
111
+ empty: Color.from(process.env.SARIF_TO_SLACK_COLOR_EMPTY),
112
112
  byLevel: {
113
- error: new Color(process.env.SARIF_TO_SLACK_COLOR_ERROR),
114
- warning: new Color(process.env.SARIF_TO_SLACK_COLOR_WARNING),
115
- note: new Color(process.env.SARIF_TO_SLACK_COLOR_NOTE),
116
- none: new Color(process.env.SARIF_TO_SLACK_COLOR_NONE),
117
- unknown: new Color(process.env.SARIF_TO_SLACK_COLOR_UNKNOWN),
113
+ error: Color.from(process.env.SARIF_TO_SLACK_COLOR_ERROR),
114
+ warning: Color.from(process.env.SARIF_TO_SLACK_COLOR_WARNING),
115
+ note: Color.from(process.env.SARIF_TO_SLACK_COLOR_NOTE),
116
+ none: Color.from(process.env.SARIF_TO_SLACK_COLOR_NONE),
117
+ unknown: Color.from(process.env.SARIF_TO_SLACK_COLOR_UNKNOWN),
118
118
  },
119
119
  bySeverity: {
120
- critical: new Color(process.env.SARIF_TO_SLACK_COLOR_CRITICAL),
121
- high: new Color(process.env.SARIF_TO_SLACK_COLOR_HIGH),
122
- medium: new Color(process.env.SARIF_TO_SLACK_COLOR_MEDIUM),
123
- low: new Color(process.env.SARIF_TO_SLACK_COLOR_LOW),
124
- none: new Color(process.env.SARIF_TO_SLACK_COLOR_NONE),
125
- unknown: new Color(process.env.SARIF_TO_SLACK_COLOR_UNKNOWN),
120
+ critical: Color.from(process.env.SARIF_TO_SLACK_COLOR_CRITICAL),
121
+ high: Color.from(process.env.SARIF_TO_SLACK_COLOR_HIGH),
122
+ medium: Color.from(process.env.SARIF_TO_SLACK_COLOR_MEDIUM),
123
+ low: Color.from(process.env.SARIF_TO_SLACK_COLOR_LOW),
124
+ none: Color.from(process.env.SARIF_TO_SLACK_COLOR_NONE),
125
+ unknown: Color.from(process.env.SARIF_TO_SLACK_COLOR_UNKNOWN),
126
126
  },
127
127
  },
128
128
  sarif: {
129
129
  path: process.env.SARIF_TO_SLACK_SARIF_PATH as string,
130
130
  recursive: process.env.SARIF_TO_SLACK_SARIF_PATH_RECURSIVE
131
- ? Boolean(process.env.SARIF_TO_SLACK_SARIF_PATH_RECURSIVE)
131
+ ? process.env.SARIF_TO_SLACK_SARIF_PATH_RECURSIVE.toLowerCase() === 'true'
132
132
  : false,
133
133
  extension: process.env.SARIF_TO_SLACK_SARIF_FILE_EXTENSION
134
134
  ? processSarifExtension(process.env.SARIF_TO_SLACK_SARIF_FILE_EXTENSION)
@@ -1 +0,0 @@
1
- {"version":3,"file":"Color.d.ts","sourceRoot":"","sources":["../../src/model/Color.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAQ;IAEhC;;;;;;;;OAQG;gBACgB,KAAK,CAAC,EAAE,MAAM;IAKjC;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,GAAG,SAAS,CAErC;IAED,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,QAAQ;CAcjB;AAED;;;GAGG;AACH,KAAK,gBAAgB,GAAG;IACtB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB,IAAI,CAAC,EAAE,KAAK,CAAC;CACd,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG;IACpD,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,GAAG,CAAC,EAAE,KAAK,CAAC;CACb,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;OAEG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB;;OAEG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B;;OAEG;IACH,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,CAAA"}
@@ -1,115 +0,0 @@
1
- import { SecurityLevel, SecuritySeverity } from '../types';
2
- /**
3
- * This class represents a color in hex format.
4
- * @public
5
- */
6
- export class Color {
7
- _color;
8
- /**
9
- * Creates an instance of {@link Color} class. Before creating an instance of
10
- * {@link Color} class, it (if applicable) maps CI status into the hex color,
11
- * and also validates color parameter to be a valid string that represents a
12
- * color in hex format.
13
- * @param color - Can be either undefined, valid color in hex format or GitHub
14
- * CI status (one of: success, failure, cancelled, skipped)
15
- * @public
16
- */
17
- constructor(color) {
18
- this._color = this.mapColor(color);
19
- this.assertHexColor();
20
- }
21
- /**
22
- * Returns a valid string that represents a color in hex format, or undefined.
23
- */
24
- get value() {
25
- return this._color;
26
- }
27
- assertHexColor() {
28
- if (this._color) {
29
- const hexColorRegex = /^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
30
- if (!hexColorRegex.test(this._color)) {
31
- throw new Error(`Invalid hex color: "${this._color}"`);
32
- }
33
- }
34
- }
35
- mapColor(from) {
36
- switch (from) {
37
- case 'success':
38
- return '#008000';
39
- case 'failure':
40
- return '#ff0000';
41
- case 'cancelled':
42
- return '#0047ab';
43
- case 'skipped':
44
- return '#808080';
45
- default:
46
- return from;
47
- }
48
- }
49
- }
50
- function identifyColorCommon(findings, prop, none, unknown, color) {
51
- if (color.none != null && findings.findByProperty(prop, none) != null) {
52
- return color.none.value;
53
- }
54
- if (color.unknown != null && findings.findByProperty(prop, unknown) != null) {
55
- return color.unknown.value;
56
- }
57
- return undefined;
58
- }
59
- function identifyColorBySeverity(findings, color) {
60
- if (color.critical != null && findings.findByProperty('severity', SecuritySeverity.Critical) != null) {
61
- return color.critical.value;
62
- }
63
- if (color.high != null && findings.findByProperty('severity', SecuritySeverity.High) != null) {
64
- return color.high.value;
65
- }
66
- if (color.medium != null && findings.findByProperty('severity', SecuritySeverity.Medium) != null) {
67
- return color.medium.value;
68
- }
69
- if (color.low != null && findings.findByProperty('severity', SecuritySeverity.Low) != null) {
70
- return color.low.value;
71
- }
72
- return identifyColorCommon(findings, 'severity', SecuritySeverity.None, SecuritySeverity.Unknown, color);
73
- }
74
- function identifyColorByLevel(findings, color) {
75
- if (color.error != null && findings.findByProperty('level', SecurityLevel.Error) != null) {
76
- return color.error.value;
77
- }
78
- if (color.warning != null && findings.findByProperty('level', SecurityLevel.Warning) != null) {
79
- return color.warning.value;
80
- }
81
- if (color.note != null && findings.findByProperty('level', SecurityLevel.Note) != null) {
82
- return color.note.value;
83
- }
84
- return identifyColorCommon(findings, 'level', SecurityLevel.None, SecurityLevel.Unknown, color);
85
- }
86
- /**
87
- * Makes an ultimate decision on what color should be Slack message. The decision
88
- * is based on the provided {@param colorOpts} parameter and {@param findings}
89
- * list.
90
- * @param findings An instance of {@link FindingArray} object.
91
- * @param colorOpts An instance of {@link ColorOptions} type.
92
- * @internal
93
- */
94
- export function identifyColor(findings, colorOpts) {
95
- if (!colorOpts) {
96
- return undefined;
97
- }
98
- if (colorOpts.bySeverity) {
99
- const color = identifyColorBySeverity(findings, colorOpts.bySeverity);
100
- if (color !== undefined) {
101
- return color;
102
- }
103
- }
104
- if (colorOpts.byLevel) {
105
- const color = identifyColorByLevel(findings, colorOpts.byLevel);
106
- if (color !== undefined) {
107
- return color;
108
- }
109
- }
110
- if (findings.length === 0 && colorOpts.empty?.value !== undefined) {
111
- return colorOpts.empty.value;
112
- }
113
- return colorOpts?.default?.value;
114
- }
115
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQ29sb3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvbW9kZWwvQ29sb3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGFBQWEsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLFVBQVUsQ0FBQTtBQUkxRDs7O0dBR0c7QUFDSCxNQUFNLE9BQU8sS0FBSztJQUNDLE1BQU0sQ0FBUztJQUVoQzs7Ozs7Ozs7T0FRRztJQUNILFlBQW1CLEtBQWM7UUFDL0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFBO1FBQ2xDLElBQUksQ0FBQyxjQUFjLEVBQUUsQ0FBQTtJQUN2QixDQUFDO0lBRUQ7O09BRUc7SUFDSCxJQUFXLEtBQUs7UUFDZCxPQUFPLElBQUksQ0FBQyxNQUFNLENBQUE7SUFDcEIsQ0FBQztJQUVPLGNBQWM7UUFDcEIsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDaEIsTUFBTSxhQUFhLEdBQUcsb0VBQW9FLENBQUE7WUFFMUYsSUFBSSxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7Z0JBQ3JDLE1BQU0sSUFBSSxLQUFLLENBQUMsdUJBQXVCLElBQUksQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFBO1lBQ3hELENBQUM7UUFDSCxDQUFDO0lBQ0gsQ0FBQztJQUVPLFFBQVEsQ0FBQyxJQUFhO1FBQzVCLFFBQVEsSUFBSSxFQUFFLENBQUM7WUFDYixLQUFLLFNBQVM7Z0JBQ1osT0FBTyxTQUFTLENBQUE7WUFDbEIsS0FBSyxTQUFTO2dCQUNaLE9BQU8sU0FBUyxDQUFBO1lBQ2xCLEtBQUssV0FBVztnQkFDZCxPQUFPLFNBQVMsQ0FBQTtZQUNsQixLQUFLLFNBQVM7Z0JBQ1osT0FBTyxTQUFTLENBQUE7WUFDbEI7Z0JBQ0UsT0FBTyxJQUFJLENBQUE7UUFDZixDQUFDO0lBQ0gsQ0FBQztDQUNGO0FB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