@fabasoad/sarif-to-slack 0.2.5 → 1.1.0

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 (192) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/.github/workflows/security.yml +0 -1
  3. package/.github/workflows/send-sarif-to-slack.yml +145 -73
  4. package/.gitleaksignore +8 -0
  5. package/.pre-commit-config.yaml +3 -3
  6. package/.tool-versions +1 -1
  7. package/dist/Logger.js +4 -1
  8. package/dist/SarifToSlackClient.d.ts +33 -0
  9. package/dist/SarifToSlackClient.d.ts.map +1 -0
  10. package/dist/SarifToSlackClient.js +178 -0
  11. package/dist/SlackMessageBuilder.js +34 -82
  12. package/dist/System.d.ts +1 -3
  13. package/dist/System.d.ts.map +1 -1
  14. package/dist/System.js +10 -3
  15. package/dist/index.cjs +826 -472
  16. package/dist/index.d.ts +35 -12
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +36 -12
  19. package/dist/model/Color.d.ts +80 -0
  20. package/dist/model/Color.d.ts.map +1 -0
  21. package/dist/model/Color.js +106 -0
  22. package/dist/model/Finding.d.ts +2 -0
  23. package/dist/model/Finding.d.ts.map +1 -0
  24. package/dist/model/Finding.js +93 -0
  25. package/dist/model/FindingsArray.d.ts +2 -0
  26. package/dist/model/FindingsArray.d.ts.map +1 -0
  27. package/dist/model/FindingsArray.js +24 -0
  28. package/dist/processors/CodeQLProcessor.d.ts +2 -0
  29. package/dist/processors/CodeQLProcessor.d.ts.map +1 -0
  30. package/dist/processors/CodeQLProcessor.js +17 -0
  31. package/dist/processors/CommonProcessor.d.ts +2 -0
  32. package/dist/processors/CommonProcessor.d.ts.map +1 -0
  33. package/dist/processors/CommonProcessor.js +84 -0
  34. package/dist/processors/ProcessorFactory.d.ts +2 -0
  35. package/dist/processors/ProcessorFactory.d.ts.map +1 -0
  36. package/dist/processors/ProcessorFactory.js +22 -0
  37. package/dist/processors/SnykProcessor.d.ts +2 -0
  38. package/dist/processors/SnykProcessor.d.ts.map +1 -0
  39. package/dist/processors/SnykProcessor.js +18 -0
  40. package/dist/representations/CompactGroupByRepresentation.d.ts +2 -0
  41. package/dist/representations/CompactGroupByRepresentation.d.ts.map +1 -0
  42. package/dist/representations/CompactGroupByRepresentation.js +58 -0
  43. package/dist/representations/CompactGroupByRunPerLevelRepresentation.d.ts +2 -0
  44. package/dist/representations/CompactGroupByRunPerLevelRepresentation.d.ts.map +1 -0
  45. package/dist/representations/CompactGroupByRunPerLevelRepresentation.js +13 -0
  46. package/dist/representations/CompactGroupByRunPerSeverityRepresentation.d.ts +2 -0
  47. package/dist/representations/CompactGroupByRunPerSeverityRepresentation.d.ts.map +1 -0
  48. package/dist/representations/CompactGroupByRunPerSeverityRepresentation.js +13 -0
  49. package/dist/representations/CompactGroupByRunRepresentation.d.ts +2 -0
  50. package/dist/representations/CompactGroupByRunRepresentation.d.ts.map +1 -0
  51. package/dist/representations/CompactGroupByRunRepresentation.js +39 -0
  52. package/dist/representations/CompactGroupBySarifPerLevelRepresentation.d.ts +2 -0
  53. package/dist/representations/CompactGroupBySarifPerLevelRepresentation.d.ts.map +1 -0
  54. package/dist/representations/CompactGroupBySarifPerLevelRepresentation.js +13 -0
  55. package/dist/representations/CompactGroupBySarifPerSeverityRepresentation.d.ts +2 -0
  56. package/dist/representations/CompactGroupBySarifPerSeverityRepresentation.d.ts.map +1 -0
  57. package/dist/representations/CompactGroupBySarifPerSeverityRepresentation.js +13 -0
  58. package/dist/representations/CompactGroupBySarifRepresentation.d.ts +2 -0
  59. package/dist/representations/CompactGroupBySarifRepresentation.d.ts.map +1 -0
  60. package/dist/representations/CompactGroupBySarifRepresentation.js +40 -0
  61. package/dist/representations/CompactGroupByToolNamePerLevelRepresentation.d.ts +2 -0
  62. package/dist/representations/CompactGroupByToolNamePerLevelRepresentation.d.ts.map +1 -0
  63. package/dist/representations/CompactGroupByToolNamePerLevelRepresentation.js +13 -0
  64. package/dist/representations/CompactGroupByToolNamePerSeverityRepresentation.d.ts +2 -0
  65. package/dist/representations/CompactGroupByToolNamePerSeverityRepresentation.d.ts.map +1 -0
  66. package/dist/representations/CompactGroupByToolNamePerSeverityRepresentation.js +13 -0
  67. package/dist/representations/CompactGroupByToolNameRepresentation.d.ts +2 -0
  68. package/dist/representations/CompactGroupByToolNameRepresentation.d.ts.map +1 -0
  69. package/dist/representations/CompactGroupByToolNameRepresentation.js +39 -0
  70. package/dist/representations/CompactTotalPerLevelRepresentation.d.ts +2 -0
  71. package/dist/representations/CompactTotalPerLevelRepresentation.d.ts.map +1 -0
  72. package/dist/representations/CompactTotalPerLevelRepresentation.js +13 -0
  73. package/dist/representations/CompactTotalPerSeverityRepresentation.d.ts +2 -0
  74. package/dist/representations/CompactTotalPerSeverityRepresentation.d.ts.map +1 -0
  75. package/dist/representations/CompactTotalPerSeverityRepresentation.js +13 -0
  76. package/dist/representations/CompactTotalRepresentation.d.ts +2 -0
  77. package/dist/representations/CompactTotalRepresentation.d.ts.map +1 -0
  78. package/dist/representations/CompactTotalRepresentation.js +25 -0
  79. package/dist/representations/Representation.d.ts +2 -0
  80. package/dist/representations/Representation.d.ts.map +1 -0
  81. package/dist/representations/Representation.js +28 -0
  82. package/dist/representations/RepresentationFactory.d.ts +2 -0
  83. package/dist/representations/RepresentationFactory.d.ts.map +1 -0
  84. package/dist/representations/RepresentationFactory.js +37 -0
  85. package/dist/sarif-to-slack.d.ts +347 -85
  86. package/dist/tsdoc-metadata.json +1 -1
  87. package/dist/types.d.ts +215 -51
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/types.js +225 -33
  90. package/dist/utils/Comparators.d.ts +2 -0
  91. package/dist/utils/Comparators.d.ts.map +1 -0
  92. package/dist/utils/Comparators.js +18 -0
  93. package/dist/utils/ExtendedArray.d.ts +2 -0
  94. package/dist/utils/ExtendedArray.d.ts.map +1 -0
  95. package/dist/utils/ExtendedArray.js +11 -0
  96. package/dist/utils/FileUtils.d.ts +2 -0
  97. package/dist/utils/FileUtils.d.ts.map +1 -0
  98. package/dist/utils/FileUtils.js +51 -0
  99. package/dist/utils/SarifUtils.js +20 -54
  100. package/etc/sarif-to-slack.api.md +162 -99
  101. package/jest.config.json +2 -2
  102. package/package.json +7 -7
  103. package/scripts/save-metadata.sh +12 -10
  104. package/src/Logger.ts +4 -0
  105. package/src/SarifToSlackClient.ts +202 -0
  106. package/src/SlackMessageBuilder.ts +35 -115
  107. package/src/System.ts +9 -2
  108. package/src/index.ts +47 -20
  109. package/src/model/Color.ts +195 -0
  110. package/src/model/Finding.ts +137 -0
  111. package/src/model/FindingsArray.ts +27 -0
  112. package/src/processors/CodeQLProcessor.ts +19 -0
  113. package/src/processors/CommonProcessor.ts +103 -0
  114. package/src/processors/ProcessorFactory.ts +23 -0
  115. package/src/processors/SnykProcessor.ts +19 -0
  116. package/src/representations/CompactGroupByRepresentation.ts +67 -0
  117. package/src/representations/CompactGroupByRunPerLevelRepresentation.ts +14 -0
  118. package/src/representations/CompactGroupByRunPerSeverityRepresentation.ts +14 -0
  119. package/src/representations/CompactGroupByRunRepresentation.ts +44 -0
  120. package/src/representations/CompactGroupBySarifPerLevelRepresentation.ts +15 -0
  121. package/src/representations/CompactGroupBySarifPerSeverityRepresentation.ts +15 -0
  122. package/src/representations/CompactGroupBySarifRepresentation.ts +45 -0
  123. package/src/representations/CompactGroupByToolNamePerLevelRepresentation.ts +15 -0
  124. package/src/representations/CompactGroupByToolNamePerSeverityRepresentation.ts +15 -0
  125. package/src/representations/CompactGroupByToolNameRepresentation.ts +44 -0
  126. package/src/representations/CompactTotalPerLevelRepresentation.ts +14 -0
  127. package/src/representations/CompactTotalPerSeverityRepresentation.ts +14 -0
  128. package/src/representations/CompactTotalRepresentation.ts +27 -0
  129. package/src/representations/Representation.ts +35 -0
  130. package/src/representations/RepresentationFactory.ts +49 -0
  131. package/src/types.ts +270 -53
  132. package/src/utils/Comparators.ts +19 -0
  133. package/src/utils/ExtendedArray.ts +11 -0
  134. package/src/utils/FileUtils.ts +60 -0
  135. package/src/utils/SarifUtils.ts +20 -72
  136. package/test-data/sarif/codeql-python.sarif +1448 -1
  137. package/test-data/sarif/codeql-typescript.sarif +3474 -1
  138. package/test-data/sarif/grype-github-actions.sarif +65 -0
  139. package/test-data/sarif/osv-scanner-composer.sarif +972 -0
  140. package/test-data/sarif/osv-scanner-container.sarif +2278 -0
  141. package/test-data/sarif/osv-scanner-gomodules.sarif +813 -0
  142. package/test-data/sarif/osv-scanner-hex.sarif +147 -0
  143. package/test-data/sarif/osv-scanner-maven.sarif +171 -0
  144. package/test-data/sarif/osv-scanner-npm.sarif +627 -0
  145. package/test-data/sarif/osv-scanner-pip.sarif +206 -0
  146. package/test-data/sarif/osv-scanner-pipenv.sarif +243 -0
  147. package/test-data/sarif/osv-scanner-pnpm.sarif +174 -0
  148. package/test-data/sarif/osv-scanner-poetry.sarif +1893 -0
  149. package/test-data/sarif/osv-scanner-rubygems.sarif +402 -0
  150. package/test-data/sarif/osv-scanner-uv.sarif +206 -0
  151. package/test-data/sarif/osv-scanner-yarn.sarif +5207 -0
  152. package/test-data/sarif/runs-0.sarif +5 -0
  153. package/test-data/sarif/runs-2-tools-2-results-0.sarif +1 -1
  154. package/test-data/sarif/runs-2-tools-2.sarif +1 -1
  155. package/test-data/sarif/runs-3-tools-2-results-0.sarif +1 -1
  156. package/test-data/sarif/runs-3-tools-2.sarif +1 -1
  157. package/test-data/sarif/tmp/codeql-csharp.sarif +1 -0
  158. package/test-data/sarif/tmp/grype-container.sarif +1774 -0
  159. package/test-data/sarif/tmp/runs-1-tools-1-results-0.sarif +18 -0
  160. package/test-data/sarif/tmp/runs-2-tools-2.sarif +686 -0
  161. package/test-data/sarif/trivy-iac.sarif +1 -1
  162. package/tests/integration/SendSarifToSlack.spec.ts +95 -27
  163. package/tsconfig.json +2 -0
  164. package/dist/Processors.d.ts +0 -2
  165. package/dist/Processors.d.ts.map +0 -1
  166. package/dist/Processors.js +0 -61
  167. package/dist/SarifToSlackService.d.ts +0 -39
  168. package/dist/SarifToSlackService.d.ts.map +0 -1
  169. package/dist/SarifToSlackService.js +0 -104
  170. package/dist/metadata.d.ts +0 -2
  171. package/dist/metadata.d.ts.map +0 -1
  172. package/dist/metadata.js +0 -11
  173. package/dist/model/SarifModelPerRun.d.ts +0 -2
  174. package/dist/model/SarifModelPerRun.d.ts.map +0 -1
  175. package/dist/model/SarifModelPerRun.js +0 -90
  176. package/dist/model/SarifModelPerSarif.d.ts +0 -2
  177. package/dist/model/SarifModelPerSarif.d.ts.map +0 -1
  178. package/dist/model/SarifModelPerSarif.js +0 -102
  179. package/dist/model/types.d.ts +0 -2
  180. package/dist/model/types.d.ts.map +0 -1
  181. package/dist/model/types.js +0 -49
  182. package/dist/utils/SortUtils.d.ts +0 -2
  183. package/dist/utils/SortUtils.d.ts.map +0 -1
  184. package/dist/utils/SortUtils.js +0 -20
  185. package/src/Processors.ts +0 -68
  186. package/src/SarifToSlackService.ts +0 -117
  187. package/src/metadata.ts +0 -10
  188. package/src/model/SarifModelPerRun.ts +0 -120
  189. package/src/model/SarifModelPerSarif.ts +0 -126
  190. package/src/model/types.ts +0 -50
  191. package/src/utils/SortUtils.ts +0 -33
  192. package/tests/Processors.spec.ts +0 -76
@@ -0,0 +1,195 @@
1
+ import { SecurityLevel, SecuritySeverity } from '../types'
2
+ import { Finding } from './Finding'
3
+ import FindingsArray from './FindingsArray'
4
+
5
+ /**
6
+ * This class represents a color in hex format.
7
+ * @public
8
+ */
9
+ export class Color {
10
+ private readonly _color?: string
11
+
12
+ /**
13
+ * Creates an instance of {@link Color} class. Before creating an instance of
14
+ * {@link Color} class, it (if applicable) maps CI status into the hex color,
15
+ * and also validates {@param color} to be a valid string that represents a
16
+ * color in hex format.
17
+ * @param color Can be either undefined, valid color in hex format or GitHub
18
+ * CI status (one of: success, failure, cancelled, skipped)
19
+ * @public
20
+ */
21
+ public constructor(color?: string) {
22
+ this._color = this.mapColor(color)
23
+ this.validateHexColor()
24
+ }
25
+
26
+ /**
27
+ * Returns a valid string that represents a color in hex format, or undefined.
28
+ */
29
+ public get value(): string | undefined {
30
+ return this._color
31
+ }
32
+
33
+ private validateHexColor(): void {
34
+ if (this._color != null) {
35
+ const hexColorRegex = /^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
36
+
37
+ if (!hexColorRegex.test(this._color)) {
38
+ throw new Error(`Invalid hex color: "${this._color}"`);
39
+ }
40
+ }
41
+ }
42
+
43
+ private mapColor(from?: string): string | undefined {
44
+ switch (from) {
45
+ case 'success':
46
+ return '#008000'
47
+ case 'failure':
48
+ return '#ff0000'
49
+ case 'cancelled':
50
+ return '#0047ab'
51
+ case 'skipped':
52
+ return '#808080'
53
+ default:
54
+ return from
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Base type that has common fields for both {@link ColorGroupByLevel} and
61
+ * {@link ColorGroupBySeverity}.
62
+ * @private
63
+ */
64
+ type ColorGroupCommon = {
65
+ none?: Color,
66
+ unknown?: Color,
67
+ empty?: Color,
68
+ }
69
+
70
+ /**
71
+ * Color schema for the findings with the certain level. Color is used by the
72
+ * level importance, i.e. if at least 1 error finding exists then
73
+ * {@link ColorGroupByLevel.error} color is used, then if at least 1 warning
74
+ * finding exists then {@link ColorGroupByLevel.warning} color is used, etc.
75
+ * @public
76
+ */
77
+ export type ColorGroupByLevel = ColorGroupCommon & {
78
+ error?: Color,
79
+ warning?: Color,
80
+ note?: Color,
81
+ }
82
+
83
+ /**
84
+ * Color schema for the findings with the certain severity. Color is used by the
85
+ * severity importance, i.e. if at least 1 critical finding exists then
86
+ * {@link ColorGroupBySeverity.critical} color is used, then if at least 1 high
87
+ * finding exists then {@link ColorGroupBySeverity.high} color is used, etc.
88
+ * @public
89
+ */
90
+ export type ColorGroupBySeverity = ColorGroupCommon & {
91
+ critical?: Color,
92
+ high?: Color,
93
+ medium?: Color,
94
+ low?: Color,
95
+ }
96
+
97
+ /**
98
+ * Represents configuration of the color scheme. If both {@link ColorOptions.byLevel}
99
+ * and {@link ColorOptions.bySeverity} are defined, then {@link ColorOptions.bySeverity}
100
+ * takes precedence.
101
+ * @public
102
+ */
103
+ export type ColorOptions = {
104
+ /**
105
+ * Default color if specific color was not found. It is a fallback option.
106
+ */
107
+ default?: Color,
108
+ /**
109
+ * Color scheme for the findings where certain level is presented.
110
+ */
111
+ byLevel?: ColorGroupByLevel,
112
+ /**
113
+ * Color scheme for the findings where certain severity is presented.
114
+ */
115
+ bySeverity?: ColorGroupBySeverity,
116
+ }
117
+
118
+ function identifyColorCommon<K extends keyof Finding>(
119
+ findings: FindingsArray,
120
+ prop: K,
121
+ none: Finding[K],
122
+ unknown: Finding[K],
123
+ color: ColorGroupCommon,
124
+ defaultColor?: Color
125
+ ): string | undefined {
126
+ if (color.none != null && findings.findByProperty(prop, none) != null) {
127
+ return color.none.value
128
+ }
129
+
130
+ if (color.unknown != null && findings.findByProperty(prop, unknown) != null) {
131
+ return color.unknown.value
132
+ }
133
+
134
+ if (color.empty != null && findings.length === 0) {
135
+ return color.empty.value
136
+ }
137
+
138
+ return defaultColor?.value
139
+ }
140
+
141
+ function identifyColorBySeverity(findings: FindingsArray, color: ColorGroupBySeverity, defaultColor?: Color): string | undefined {
142
+ if (color.critical != null && findings.findByProperty('severity', SecuritySeverity.Critical) != null) {
143
+ return color.critical.value
144
+ }
145
+
146
+ if (color.high != null && findings.findByProperty('severity', SecuritySeverity.High) != null) {
147
+ return color.high.value
148
+ }
149
+
150
+ if (color.medium != null && findings.findByProperty('severity', SecuritySeverity.Medium) != null) {
151
+ return color.medium.value
152
+ }
153
+
154
+ if (color.low != null && findings.findByProperty('severity', SecuritySeverity.Low) != null) {
155
+ return color.low.value
156
+ }
157
+
158
+ return identifyColorCommon(findings, 'severity', SecuritySeverity.None, SecuritySeverity.Unknown, color, defaultColor)
159
+ }
160
+
161
+ function identifyColorByLevel(findings: FindingsArray, color: ColorGroupByLevel, defaultColor?: Color): string | undefined {
162
+ if (color.error != null && findings.findByProperty('level', SecurityLevel.Error) != null) {
163
+ return color.error.value
164
+ }
165
+
166
+ if (color.warning != null && findings.findByProperty('level', SecurityLevel.Warning) != null) {
167
+ return color.warning.value
168
+ }
169
+
170
+ if (color.note != null && findings.findByProperty('level', SecurityLevel.Note) != null) {
171
+ return color.note.value
172
+ }
173
+
174
+ return identifyColorCommon(findings, 'level', SecurityLevel.None, SecurityLevel.Unknown, color, defaultColor)
175
+ }
176
+
177
+ /**
178
+ * Makes an ultimate decision on what color should be Slack message. The decision
179
+ * is based on the provided {@param colorOpts} parameter and {@param findings}
180
+ * list.
181
+ * @param findings An instance of {@link FindingsArray} object.
182
+ * @param colorOpts An instance of {@link ColorOptions} type.
183
+ * @internal
184
+ */
185
+ export function identifyColor(findings: FindingsArray, colorOpts?: ColorOptions): string | undefined {
186
+ if (colorOpts?.bySeverity != null) {
187
+ return identifyColorBySeverity(findings, colorOpts.bySeverity, colorOpts.default)
188
+ }
189
+
190
+ if (colorOpts?.byLevel != null) {
191
+ return identifyColorByLevel(findings, colorOpts.byLevel, colorOpts.default)
192
+ }
193
+
194
+ return colorOpts?.default?.value
195
+ }
@@ -0,0 +1,137 @@
1
+ import type { ReportingDescriptor, Result } from 'sarif'
2
+ import { RunData, SecurityLevel, SecuritySeverity } from '../types'
3
+ import Logger from '../Logger'
4
+ import { CommonProcessor } from '../processors/CommonProcessor'
5
+ import { createProcessor } from '../processors/ProcessorFactory'
6
+
7
+ /**
8
+ * Parameters that are needed for the new {@link Finding} instance creation.
9
+ * @internal
10
+ */
11
+ export type FindingOptions = {
12
+ sarifPath: string,
13
+ runMetadata: RunData,
14
+ result: Result,
15
+ }
16
+
17
+ /**
18
+ * This interface represents a finding from SARIF file.
19
+ * @internal
20
+ */
21
+ export interface Finding {
22
+ get sarifPath(): string,
23
+ get runId(): number,
24
+ get toolName(): string,
25
+ get cvssScore(): number | undefined,
26
+ get level(): SecurityLevel,
27
+ get severity(): SecuritySeverity,
28
+ clone(): Finding,
29
+ }
30
+
31
+ /**
32
+ * Creates a new instance of {@link Finding} class.
33
+ * @internal
34
+ */
35
+ export function createFinding(opts: FindingOptions): Finding {
36
+ return new SarifFinding(opts)
37
+ }
38
+
39
+ /**
40
+ * The only implementation of {@link Finding} interface. This class is private
41
+ * and is not supposed to be exposed. {@link createFinding} should be used to
42
+ * create a new {@link Finding}.
43
+ * @private
44
+ */
45
+ class SarifFinding implements Finding {
46
+ private readonly _runMetadata: RunData
47
+ private readonly _result: Result
48
+ private readonly _sarifPath: string
49
+ private readonly _rule?: ReportingDescriptor
50
+ private readonly _processor: CommonProcessor
51
+
52
+ private _cvssScoreCacheProcessed: boolean = false
53
+ private _cvssScoreCache: number | undefined = undefined
54
+
55
+ private _levelCacheProcessed: boolean = false
56
+ private _levelCache: Result.level | undefined = undefined
57
+
58
+ constructor(opts: FindingOptions) {
59
+ this._processor = createProcessor(opts.runMetadata.run, opts.result)
60
+ this._sarifPath = opts.sarifPath
61
+ this._runMetadata = opts.runMetadata
62
+ this._result = opts.result
63
+ this._rule = this._processor.tryFindRule()
64
+ }
65
+
66
+ clone(): Finding {
67
+ return createFinding({
68
+ sarifPath: this._sarifPath,
69
+ runMetadata: this._runMetadata,
70
+ result: this._result
71
+ })
72
+ }
73
+
74
+ public get sarifPath(): string {
75
+ return this._sarifPath
76
+ }
77
+
78
+ public get runId(): number {
79
+ return this._runMetadata.id
80
+ }
81
+
82
+ public get toolName(): string {
83
+ return this._processor.findToolComponent().name
84
+ }
85
+
86
+ public get cvssScore(): number | undefined {
87
+ if (!this._cvssScoreCacheProcessed) {
88
+ this._cvssScoreCacheProcessed = true
89
+ this._cvssScoreCache = this._processor.tryFindCvssScore()
90
+ }
91
+ return this._cvssScoreCache
92
+ }
93
+
94
+ public get level(): SecurityLevel {
95
+ if (!this._levelCacheProcessed) {
96
+ this._levelCacheProcessed = true
97
+ this._levelCache = this._processor.tryFindLevel()
98
+ }
99
+
100
+ if (this._levelCache === undefined) {
101
+ Logger.debug(`Unknown level of ${this._rule?.id} rule`)
102
+ return SecurityLevel.Unknown
103
+ }
104
+
105
+ switch (this._levelCache) {
106
+ case 'error': return SecurityLevel.Error
107
+ case 'warning': return SecurityLevel.Warning
108
+ case 'note': return SecurityLevel.Note
109
+ default: return SecurityLevel.None
110
+ }
111
+ }
112
+
113
+ public get severity(): SecuritySeverity {
114
+ if (this.cvssScore == null || this.cvssScore < 0 || this.cvssScore > 10) {
115
+ Logger.debug(`Unsupported CVSS score ${this.cvssScore} in ${this._rule?.id} rule`)
116
+ return SecuritySeverity.Unknown
117
+ }
118
+
119
+ if (this.cvssScore >= 9) {
120
+ return SecuritySeverity.Critical
121
+ }
122
+
123
+ if (this.cvssScore >= 7) {
124
+ return SecuritySeverity.High
125
+ }
126
+
127
+ if (this.cvssScore >= 4) {
128
+ return SecuritySeverity.Medium
129
+ }
130
+
131
+ if (this.cvssScore >= 0.1) {
132
+ return SecuritySeverity.Low
133
+ }
134
+
135
+ return SecuritySeverity.None
136
+ }
137
+ }
@@ -0,0 +1,27 @@
1
+ import { Finding } from './Finding'
2
+ import ExtendedArray from '../utils/ExtendedArray'
3
+ import { SecurityLevel, SecuritySeverity } from '../types'
4
+
5
+ /**
6
+ * This class represents an array of {@link Finding} objects and adds additional
7
+ * useful methods to it.
8
+ * @internal
9
+ */
10
+ export default class FindingsArray extends ExtendedArray<Finding> {
11
+
12
+ public hasSeverityOrHigher(severity: SecuritySeverity): boolean {
13
+ return Object
14
+ .values(SecuritySeverity)
15
+ .filter((v: string | SecuritySeverity): v is SecuritySeverity => typeof v === 'number')
16
+ .filter((v: SecuritySeverity): boolean => v >= severity)
17
+ .some((v: SecuritySeverity): boolean => this.findByProperty('severity', v) != null)
18
+ }
19
+
20
+ public hasLevelOrHigher(level: SecurityLevel): boolean {
21
+ return Object
22
+ .values(SecurityLevel)
23
+ .filter((v: string | SecurityLevel): v is SecurityLevel => typeof v === 'number')
24
+ .filter((v: SecurityLevel): boolean => v >= level)
25
+ .some((v: SecurityLevel): boolean => this.findByProperty('level', v) != null)
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ import { CommonProcessor } from './CommonProcessor'
2
+ import { Result } from 'sarif';
3
+
4
+ /**
5
+ * This class has extra logic for processing SARIF files produced by CodeQL tool.
6
+ * @internal
7
+ */
8
+ export class CodeQLProcessor extends CommonProcessor {
9
+
10
+ /**
11
+ * Rules in SARIF files produced by CodeQL has additional "problem.severity"
12
+ * property where level is also defined. This method tries to get level in a
13
+ * common way but if it fails to do so, then it tries to get level from
14
+ * "problem.severity" property.
15
+ */
16
+ public override tryFindLevel(): Result.level | undefined {
17
+ return super.tryFindLevel() ?? this.tryFindRuleProperty('problem.severity')
18
+ }
19
+ }
@@ -0,0 +1,103 @@
1
+ import type { ReportingDescriptor, Result, Run, ToolComponent } from 'sarif'
2
+ import * as sarifUtils from '../utils/SarifUtils'
3
+
4
+ /**
5
+ * This class has logic of the SARIF file processing, such as finding rule,
6
+ * finding tool component, etc. It is used by default for all SARIF files.
7
+ * Derived classes from this class can implement extra logic for the specific
8
+ * use cases, such as SARIF files produced by specific tools. For example,
9
+ * {@link CodeQLProcessor} handles additional logic for processing SARIF files
10
+ * produced by CodeQL.
11
+ * @internal
12
+ */
13
+ export class CommonProcessor {
14
+ protected readonly _run: Run
15
+ protected readonly _result: Result
16
+
17
+ /**
18
+ * Creates an instance of {@link CommonProcessor} class.
19
+ * @param run An instance of {@link Run} object.
20
+ * @param result An instance of {@link Result} object.
21
+ */
22
+ public constructor(run: Run, result: Result) {
23
+ this._run = run
24
+ this._result = result
25
+ }
26
+
27
+ public tryFindCvssScore(): number | undefined {
28
+ return this.tryFindRuleProperty('security-severity')
29
+ }
30
+
31
+ public tryFindLevel(): Result.level | undefined {
32
+ return this._result.level ?? this.tryFindRule()?.defaultConfiguration?.level
33
+ }
34
+
35
+ public findToolComponentDriver(): ToolComponent {
36
+ return sarifUtils.findToolComponentDriver(this._run)
37
+ }
38
+
39
+ public tryFindToolComponentExtension(): ToolComponent | undefined {
40
+ return sarifUtils.tryFindToolComponentExtension(this._run, this._result)
41
+ }
42
+
43
+ public findToolComponent(): ToolComponent {
44
+ return sarifUtils.findToolComponent(this._run, this._result)
45
+ }
46
+
47
+ /**
48
+ * This function tries to find the respective rule for the given result.
49
+ * @internal
50
+ */
51
+ public tryFindRule(): ReportingDescriptor | undefined {
52
+ const ruleData: { id?: string, index?: number } = {}
53
+
54
+ if (this._result.rule) {
55
+ if (this._result.rule?.index != null) {
56
+ ruleData.index = this._result.rule.index
57
+ }
58
+ if (this._result.rule?.id) {
59
+ ruleData.id = this._result.rule.id
60
+ }
61
+ }
62
+
63
+ if (ruleData.index == null && this._result.ruleIndex != null) {
64
+ ruleData.index = this._result.ruleIndex
65
+ }
66
+
67
+ if (!ruleData.id && this._result.ruleId) {
68
+ ruleData.id = this._result.ruleId
69
+ }
70
+
71
+ const tool: ToolComponent = this.findToolComponent()
72
+
73
+ if (ruleData.index != null
74
+ && tool?.rules
75
+ && ruleData.index < tool.rules.length) {
76
+ return tool.rules[ruleData.index]
77
+ }
78
+
79
+ // If failed to find rule by index then try to find by ruleId
80
+ if (ruleData.id && tool?.rules) {
81
+ return tool.rules.find(
82
+ (r: ReportingDescriptor): boolean => r.id === ruleData.id
83
+ )
84
+ }
85
+
86
+ return undefined
87
+ }
88
+
89
+ /**
90
+ * This function searches respective rule for the given result, and then gets
91
+ * the property of interest from it.
92
+ * @param propertyName The property name that you want to get the value from.
93
+ * @protected
94
+ */
95
+ protected tryFindRuleProperty<T>(propertyName: string): T | undefined {
96
+ const rule: ReportingDescriptor | undefined = this.tryFindRule()
97
+ if (rule?.properties && propertyName in rule.properties) {
98
+ return rule.properties[propertyName] as T
99
+ }
100
+
101
+ return undefined
102
+ }
103
+ }
@@ -0,0 +1,23 @@
1
+ import { CommonProcessor } from './CommonProcessor'
2
+ import type { Result, Run, ToolComponent } from 'sarif'
3
+ import { findToolComponent } from '../utils/SarifUtils'
4
+ import { SnykProcessor } from './SnykProcessor'
5
+ import { CodeQLProcessor } from './CodeQLProcessor'
6
+
7
+ /**
8
+ * Creates a new instance of {@link CommonProcessor} class. It tries to find specific
9
+ * processor based on the tool component for the given {@param run} and
10
+ * {@param result} and if no specific processors exist, then it returns an
11
+ * instance of {@link CommonProcessor} class.
12
+ * @param run An instance of {@link Run} class.
13
+ * @param result An instance of {@link Result} class.
14
+ * @internal
15
+ */
16
+ export function createProcessor(run: Run, result: Result): CommonProcessor {
17
+ const toolComponent: ToolComponent = findToolComponent(run, result)
18
+ switch (toolComponent.name) {
19
+ case 'CodeQL': return new CodeQLProcessor(run, result)
20
+ case 'Snyk Open Source': return new SnykProcessor(run, result)
21
+ default: return new CommonProcessor(run, result)
22
+ }
23
+ }
@@ -0,0 +1,19 @@
1
+ import { CommonProcessor } from './CommonProcessor'
2
+
3
+ /**
4
+ * This class has extra logic for processing SARIF files produced by Snyk Open
5
+ * Source tool.
6
+ * @internal
7
+ */
8
+ export class SnykProcessor extends CommonProcessor {
9
+
10
+ /**
11
+ * Rules in SARIF files produced by Snyk Open Source has additional "cvssv3_baseScore"
12
+ * property where CVSS score is also defined. This method tries to get level
13
+ * from this "cvssv3_baseScore" property and if it fails to do so, then it tries
14
+ * to get CVSS score in a common way.
15
+ */
16
+ public override tryFindCvssScore(): number | undefined {
17
+ return this.tryFindRuleProperty<number>('cvssv3_baseScore') ?? super.tryFindCvssScore()
18
+ }
19
+ }
@@ -0,0 +1,67 @@
1
+ import Representation from './Representation'
2
+ import { Finding } from '../model/Finding'
3
+ import { findingsComparatorByKey } from '../utils/Comparators'
4
+ import { SecurityLevel, SecuritySeverity } from '../types';
5
+
6
+ const NO_VULNS_FOUND_TEXT = 'No vulnerabilities found'
7
+
8
+ /**
9
+ * Base class of all compact representation types. By "compact" means that it
10
+ * groups findings by the {@link Finding} property, such as "severity" or "level".
11
+ * So, in fact it already prepares this in case of "severity":
12
+ * @example
13
+ * ```text
14
+ * Critical: 1, High: 5, Medium: 2, Low: 20, None: 1, Unknown: 120
15
+ * ```
16
+ * or this in case of "level":
17
+ * @example
18
+ * ```text
19
+ * Error: 6, Warning: 2, Note: 20, None: 1, Unknown: 120
20
+ * ```
21
+ * It is an abstract class, so the only question that derived classes should
22
+ * "answer" is how to group finding to show the compact representation.
23
+ * @internal
24
+ */
25
+ export default abstract class CompactGroupByRepresentation extends Representation {
26
+
27
+ protected abstract groupFindings(): Map<string, Finding[]>
28
+
29
+ protected composeByProperty<K extends keyof Pick<Finding, 'level' | 'severity'>>(key: K): string {
30
+ const grouped: Map<string, Finding[]> = this.groupFindings()
31
+ if (grouped.size === 0) {
32
+ return NO_VULNS_FOUND_TEXT
33
+ }
34
+
35
+ return Array.from(grouped)
36
+ .map(([title, findings]: [string, Finding[]]): string => {
37
+ findings.sort(findingsComparatorByKey(key))
38
+ const summary: string =
39
+ findings.length === 0
40
+ ? NO_VULNS_FOUND_TEXT
41
+ : this.composeCompactReport(findings, key)
42
+ return `${title}\n${summary}`
43
+ })
44
+ .join('\n\n')
45
+ }
46
+
47
+ private composeCompactReport<K extends keyof Pick<Finding, 'level' | 'severity'>>(findings: Finding[], key: K): string {
48
+ return Object
49
+ .entries(Object.groupBy(findings, (f: Finding): PropertyKey => f[key] as PropertyKey))
50
+ .map(([prop, findings2]: [string, Finding[] | undefined]): string | undefined => {
51
+ if (findings2 == null) {
52
+ return undefined
53
+ }
54
+ return `${this.bold(this.extractEnumValue(key, prop))}: ${findings2.length}`
55
+ })
56
+ .filter((v: string | undefined): v is string => v != null)
57
+ .join(', ')
58
+ }
59
+
60
+ private extractEnumValue<K extends keyof Pick<Finding, 'level' | 'severity'>>(key: K, prop: string): string {
61
+ switch (key) {
62
+ case 'level': return SecurityLevel[Number(prop)]
63
+ case 'severity': return SecuritySeverity[Number(prop)]
64
+ default: throw new Error('Unknown property:', key)
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,14 @@
1
+ import CompactGroupByRunRepresentation from './CompactGroupByRunRepresentation';
2
+
3
+ /**
4
+ * Since {@link CompactGroupByRunRepresentation} is an abstract class, the only
5
+ * question that this class should "answer" is what property should be used in
6
+ * the compact representation. In this case it is "level".
7
+ * @internal
8
+ */
9
+ export default class CompactGroupByRunPerLevelRepresentation extends CompactGroupByRunRepresentation {
10
+
11
+ public override compose(): string {
12
+ return this.composeByProperty('level')
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ import CompactGroupByRunRepresentation from './CompactGroupByRunRepresentation';
2
+
3
+ /**
4
+ * Since {@link CompactGroupByRunRepresentation} is an abstract class, the only
5
+ * question that this class should "answer" is what property should be used in
6
+ * the compact representation. In this case it is "severity".
7
+ * @internal
8
+ */
9
+ export default class CompactGroupByRunPerSeverityRepresentation extends CompactGroupByRunRepresentation {
10
+
11
+ public override compose(): string {
12
+ return this.composeByProperty('severity')
13
+ }
14
+ }
@@ -0,0 +1,44 @@
1
+ import { Finding } from '../model/Finding'
2
+ import CompactGroupByRepresentation from './CompactGroupByRepresentation'
3
+ import { SarifModel } from '../types'
4
+
5
+ /**
6
+ * Since {@link CompactGroupByRepresentation} already prepares compact representation
7
+ * of findings, this class defines a grouping rule. In this case it groups
8
+ * findings by run. Every run will be grouped separately, such as:
9
+ * @example
10
+ * ```text
11
+ * [Run 1] Grype
12
+ * Error: 1, Warning: 4
13
+ * [Run 2] Grype
14
+ * Warning: 1, Note: 20
15
+ * ```
16
+ * @internal
17
+ * It is an abstract class, so the only question that derived classes should
18
+ * "answer" is what property should be used in the compact representation, such
19
+ * as "level" and "severity".
20
+ */
21
+ export default abstract class CompactGroupByRunRepresentation extends CompactGroupByRepresentation {
22
+
23
+ public constructor(model: SarifModel) {
24
+ super(model, 'runId')
25
+ }
26
+
27
+ protected override groupFindings(): Map<string, Finding[]> {
28
+ const result = new Map<string, Finding[]>()
29
+ for (const run of this._model.runs) {
30
+ const key: string = this.composeGroupTitle(run.id, run.toolName)
31
+ if (result.get(key) == null) {
32
+ result.set(key, [])
33
+ }
34
+ this._model.findings
35
+ .filter((f: Finding): boolean => f.runId === run.id)
36
+ .forEach((f: Finding): number | undefined => result.get(key)?.push(f))
37
+ }
38
+ return result
39
+ }
40
+
41
+ private composeGroupTitle(runId: number, toolName: string): string {
42
+ return `${this.italic(`[Run ${runId}]`)} ${this.bold(toolName)}`
43
+ }
44
+ }
@@ -0,0 +1,15 @@
1
+ import CompactGroupBySarifRepresentation
2
+ from './CompactGroupBySarifRepresentation'
3
+
4
+ /**
5
+ * Since {@link CompactGroupBySarifRepresentation} is an abstract class, the only
6
+ * question that this class should "answer" is what property should be used in
7
+ * the compact representation. In this case it is "level".
8
+ * @internal
9
+ */
10
+ export default class CompactGroupBySarifPerLevelRepresentation extends CompactGroupBySarifRepresentation {
11
+
12
+ public override compose(): string {
13
+ return this.composeByProperty('level')
14
+ }
15
+ }