@fabasoad/sarif-to-slack 0.2.0 → 0.2.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 (84) hide show
  1. package/.github/workflows/release.yml +3 -1
  2. package/.github/workflows/send-sarif-to-slack.yml +214 -0
  3. package/.pre-commit-config.yaml +3 -3
  4. package/.tool-versions +1 -1
  5. package/Makefile +9 -2
  6. package/README.md +1 -1
  7. package/dist/Logger.js +15 -6
  8. package/dist/Processors.js +23 -22
  9. package/dist/SarifToSlackService.d.ts.map +1 -1
  10. package/dist/SarifToSlackService.js +5 -6
  11. package/dist/SlackMessageBuilder.js +46 -52
  12. package/dist/index.d.ts +6 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +7 -3
  15. package/dist/model/SarifModelPerRun.d.ts +17 -0
  16. package/dist/model/SarifModelPerRun.d.ts.map +1 -0
  17. package/dist/model/SarifModelPerRun.js +84 -0
  18. package/dist/model/SarifModelPerSarif.d.ts +20 -0
  19. package/dist/model/SarifModelPerSarif.d.ts.map +1 -0
  20. package/dist/model/SarifModelPerSarif.js +97 -0
  21. package/dist/model/types.d.ts +17 -0
  22. package/dist/model/types.d.ts.map +1 -0
  23. package/dist/model/types.js +31 -0
  24. package/dist/sarif-to-slack.d.ts +96 -12
  25. package/dist/tsdoc-metadata.json +1 -1
  26. package/dist/types.d.ts +87 -11
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/types.js +66 -9
  29. package/dist/utils/SarifUtils.d.ts +5 -0
  30. package/dist/utils/SarifUtils.d.ts.map +1 -0
  31. package/dist/utils/SarifUtils.js +32 -0
  32. package/dist/utils/SortUtils.d.ts +5 -0
  33. package/dist/utils/SortUtils.d.ts.map +1 -0
  34. package/dist/utils/SortUtils.js +8 -0
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/etc/sarif-to-slack.api.md +32 -7
  38. package/jest.config.json +4 -4
  39. package/package.json +9 -7
  40. package/src/Logger.ts +20 -17
  41. package/src/Processors.ts +22 -22
  42. package/src/SarifToSlackService.ts +5 -6
  43. package/src/SlackMessageBuilder.ts +78 -63
  44. package/src/index.ts +10 -2
  45. package/src/model/SarifModelPerRun.ts +114 -0
  46. package/src/model/SarifModelPerSarif.ts +116 -0
  47. package/src/model/types.ts +31 -0
  48. package/src/types.ts +91 -11
  49. package/src/utils/SarifUtils.ts +44 -0
  50. package/src/utils/SortUtils.ts +21 -0
  51. package/src/version.ts +1 -1
  52. package/test-data/sarif/codeql-csharp.sarif +1 -0
  53. package/test-data/sarif/codeql-go.sarif +1 -0
  54. package/test-data/sarif/codeql-python.sarif +1 -0
  55. package/test-data/sarif/codeql-ruby.sarif +1 -0
  56. package/test-data/sarif/codeql-typescript.sarif +1 -0
  57. package/test-data/sarif/grype-container.sarif +1774 -0
  58. package/test-data/sarif/runs-1-tools-1-results-0.sarif +18 -0
  59. package/test-data/sarif/runs-2-tools-1-results-0.sarif +30 -0
  60. package/test-data/sarif/runs-2-tools-1.sarif +656 -0
  61. package/test-data/sarif/runs-2-tools-2-results-0.sarif +44 -0
  62. package/test-data/sarif/runs-2-tools-2.sarif +686 -0
  63. package/test-data/sarif/runs-3-tools-2-results-0.sarif +48 -0
  64. package/test-data/sarif/runs-3-tools-2.sarif +278 -0
  65. package/test-data/sarif/snyk-composer.sarif +934 -0
  66. package/test-data/sarif/snyk-container.sarif +313 -0
  67. package/test-data/sarif/snyk-gomodules.sarif +388 -0
  68. package/test-data/sarif/snyk-gradle.sarif +274 -0
  69. package/test-data/sarif/snyk-hex.sarif +66 -0
  70. package/test-data/sarif/snyk-maven.sarif +274 -0
  71. package/test-data/sarif/snyk-npm.sarif +896 -0
  72. package/test-data/sarif/snyk-nuget.sarif +90 -0
  73. package/test-data/sarif/snyk-pip.sarif +66 -0
  74. package/test-data/sarif/snyk-pnpm.sarif +90 -0
  75. package/test-data/sarif/snyk-poetry.sarif +1952 -0
  76. package/test-data/sarif/snyk-rubygems.sarif +440 -0
  77. package/test-data/sarif/snyk-sbt.sarif +178 -0
  78. package/test-data/sarif/snyk-swift.sarif +112 -0
  79. package/test-data/sarif/snyk-yarn.sarif +2900 -0
  80. package/test-data/sarif/trivy-iac.sarif +134 -0
  81. package/test-data/sarif/wiz-container.sarif +30916 -0
  82. package/test-data/sarif/wiz-iac.sarif +558 -0
  83. package/tests/Processors.spec.ts +3 -3
  84. package/tests/integration/SendSarifToSlack.spec.ts +56 -0
@@ -2,9 +2,21 @@ import { AnyBlock } from '@slack/types'
2
2
  import { ContextBlock, HeaderBlock } from '@slack/types/dist/block-kit/blocks'
3
3
  import { TextObject } from '@slack/types/dist/block-kit/composition-objects'
4
4
  import { IncomingWebhook } from '@slack/webhook'
5
- import { FooterType, Sarif, SlackMessage } from './types'
6
- import type { ReportingDescriptor, Result, Run } from 'sarif'
5
+ import { Map as ImmutableMap } from 'immutable'
6
+ import {
7
+ CalculateResultsBy,
8
+ FooterType,
9
+ GroupResultsBy,
10
+ SarifLog,
11
+ SarifToSlackOutput,
12
+ SlackMessage
13
+ } from './types'
7
14
  import { LIB_VERSION } from './version'
15
+ import {
16
+ DataGroupedByRun,
17
+ SarifModelPerSarif
18
+ } from './model/SarifModelPerSarif';
19
+ import { SecurityLevel, SecuritySeverity } from './model/types';
8
20
 
9
21
  /**
10
22
  * Options for the SlackMessageBuilder.
@@ -14,11 +26,10 @@ export type SlackMessageBuilderOptions = {
14
26
  username?: string
15
27
  iconUrl?: string
16
28
  color?: string
17
- sarif: Sarif
29
+ sarif: SarifLog,
30
+ output?: SarifToSlackOutput,
18
31
  }
19
32
 
20
- type RuleData = { id?: string, index?: number }
21
-
22
33
  /**
23
34
  * Class for building and sending Slack messages based on SARIF logs.
24
35
  * @internal
@@ -27,22 +38,29 @@ export class SlackMessageBuilder implements SlackMessage {
27
38
  private readonly webhook: IncomingWebhook
28
39
  private readonly gitHubServerUrl: string
29
40
  private readonly color?: string
30
-
41
+ private readonly sarifModelPerSarif: SarifModelPerSarif
42
+ private readonly output: SarifToSlackOutput
31
43
  private header?: HeaderBlock
44
+
32
45
  private footer?: ContextBlock
33
46
  private actor?: string
34
47
  private runId?: string
35
48
 
36
- public readonly sarif: Sarif
49
+ public readonly sarif: SarifLog
37
50
 
38
51
  constructor(url: string, opts: SlackMessageBuilderOptions) {
39
52
  this.webhook = new IncomingWebhook(url, {
40
53
  username: opts.username || 'SARIF results',
41
54
  icon_url: opts.iconUrl
42
55
  })
56
+ this.gitHubServerUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'
43
57
  this.color = opts.color
44
58
  this.sarif = opts.sarif
45
- this.gitHubServerUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'
59
+ this.sarifModelPerSarif = new SarifModelPerSarif(opts.sarif)
60
+ this.output = opts.output || {
61
+ groupBy: GroupResultsBy.ToolName,
62
+ calculateBy: CalculateResultsBy.Level
63
+ }
46
64
  }
47
65
 
48
66
  withHeader(header?: string): void {
@@ -66,8 +84,8 @@ export class SlackMessageBuilder implements SlackMessage {
66
84
  withFooter(text?: string, type?: FooterType): void {
67
85
  const repoName = 'fabasoad/sarif-to-slack'
68
86
  const element: TextObject = text
69
- ? { type: type || FooterType.PLAIN_TEXT, text }
70
- : { type: FooterType.MARKDOWN, text: `Generated by <${this.gitHubServerUrl}/${repoName}|@${repoName}@${LIB_VERSION}>` }
87
+ ? { type: type || FooterType.PlainText, text }
88
+ : { type: FooterType.Markdown, text: `Generated by <${this.gitHubServerUrl}/${repoName}|@${repoName}@${LIB_VERSION}>` }
71
89
  this.footer = {
72
90
  type: 'context',
73
91
  elements: [element],
@@ -111,68 +129,65 @@ export class SlackMessageBuilder implements SlackMessage {
111
129
  }
112
130
  text.push(runText)
113
131
  }
114
- return text.join('\n')
132
+ return text.join('\n\n')
115
133
  }
116
134
 
117
- private composeRunSummary(toolName: string, map: Map<string, number>): string {
118
- const levelsText: string[] = []
119
- for (const [level, count] of map.entries()) {
120
- const levelCapitalized = level.charAt(0).toUpperCase() + level.slice(1)
121
- levelsText.push(`*${levelCapitalized}*: ${count}`)
135
+ private composeSummaryWith(
136
+ map: ImmutableMap<SecurityLevel | SecuritySeverity, number>,
137
+ resultProcessor: (result: string) => string = (result: string): string => result,
138
+ ): string {
139
+ const stats = new Array<string>()
140
+ for (const [key, count] of map.entries()) {
141
+ stats.push(`*${key}*: ${count}`)
122
142
  }
123
- return `*${toolName}*\n${levelsText.join(', ')}`
143
+ return resultProcessor(
144
+ stats.length == 0 ? 'No issues found' : stats.join(', ')
145
+ )
124
146
  }
125
147
 
126
148
  private composeSummary(): string {
127
- const data = new Map<string, Map<string, number>>()
128
- for (const run of this.sarif.runs) {
129
- const toolName = run.tool.driver.name
130
- if (!data.has(toolName)) {
131
- data.set(toolName, new Map<string, number>())
132
- }
133
- const results: Result[] = run.results ?? []
134
- for (const result of results) {
135
- const level: string = this.tryGetLevel(run, result)
136
- const count: number = data.get(toolName)?.get(level) || 0
137
- data.get(toolName)?.set(level, count + 1)
138
- }
139
- }
140
- const summaries: string[] = []
141
- for (const [toolName, map] of data.entries()) {
142
- summaries.push(this.composeRunSummary(toolName, map))
143
- }
144
- return summaries.join('\n')
145
- }
146
-
147
- private tryGetLevel(run: Run, result: Result): string {
148
- if (result.level) {
149
- return result.level
150
- }
151
-
152
- const ruleData: RuleData = {}
153
-
154
- if (result.rule) {
155
- if (result.rule?.index) {
156
- ruleData.index = result.rule.index
149
+ const summaries = new Array<string>()
150
+ switch (this.output.groupBy) {
151
+ case GroupResultsBy.ToolName: {
152
+ const dataGroupedByToolName: Map<string, ImmutableMap<SecurityLevel | SecuritySeverity, number>> =
153
+ this.output.calculateBy === CalculateResultsBy.Level
154
+ ? this.sarifModelPerSarif.groupByToolNameWithSecurityLevel()
155
+ : this.sarifModelPerSarif.groupByToolNameWithSecuritySeverity()
156
+ for (const [toolName, map] of dataGroupedByToolName.entries()) {
157
+ summaries.push(this.composeSummaryWith(
158
+ map,
159
+ (result: string): string => `*${toolName}*\n${result}`
160
+ ))
161
+ }
162
+ break
157
163
  }
158
- if (result.rule?.id) {
159
- ruleData.id = result.rule.id
164
+ case GroupResultsBy.Run: {
165
+ const dataGroupedByRun: Array<DataGroupedByRun<SecurityLevel | SecuritySeverity>> =
166
+ this.output.calculateBy === CalculateResultsBy.Level
167
+ ? this.sarifModelPerSarif.groupByRunWithSecurityLevel()
168
+ : this.sarifModelPerSarif.groupByRunWithSecuritySeverity()
169
+ for (let i = 0; i < dataGroupedByRun.length; i++) {
170
+ const { data, toolName } = dataGroupedByRun[i]
171
+ summaries.push(this.composeSummaryWith(
172
+ data,
173
+ (result: string): string => `_[Run ${i + 1}]_: *${toolName}*\n${result}`
174
+ ))
175
+ }
176
+ break
160
177
  }
161
- }
162
-
163
- if (!ruleData.index && result.ruleIndex) {
164
- ruleData.index = result.ruleIndex
165
- }
166
-
167
- if (ruleData.index
168
- && run.tool.driver?.rules
169
- && ruleData.index < run.tool.driver.rules.length) {
170
- const rule: ReportingDescriptor = run.tool.driver.rules[ruleData.index]
171
- if (rule.properties && 'problem.severity' in rule.properties) {
172
- return rule.properties['problem.severity'] as string
178
+ default: {
179
+ const dataTotal: ImmutableMap<SecurityLevel | SecuritySeverity, number> =
180
+ this.output.calculateBy === CalculateResultsBy.Level
181
+ ? this.sarifModelPerSarif.groupByTotalWithSecurityLevel()
182
+ : this.sarifModelPerSarif.groupByTotalWithSecuritySeverity()
183
+ const toolNames: Set<string> = this.sarifModelPerSarif.listToolNames()
184
+ summaries.push(this.composeSummaryWith(
185
+ dataTotal,
186
+ (result: string): string => `*${Array.from(toolNames).join('*, *')}*\n${result}`
187
+ ))
188
+ break
173
189
  }
174
190
  }
175
-
176
- return 'unknown'
191
+ return summaries.join('\n\n')
177
192
  }
178
193
  }
package/src/index.ts CHANGED
@@ -14,7 +14,11 @@
14
14
  * const service = await SarifToSlackService.create({
15
15
  * webhookUrl: 'https://hooks.slack.com/services/your/webhook/url',
16
16
  * sarifPath: 'path/to/your/sarif/file.sarif',
17
- * logLevel: 'info',
17
+ * log: {
18
+ * level: 'info',
19
+ * template: '[{{logLevelName}}] [{{name}}] {{dateIsoStr}} ',
20
+ * colored: false,
21
+ * },
18
22
  * username: 'SARIF Bot',
19
23
  * iconUrl: 'https://example.com/icon.png',
20
24
  * color: '#36a64f',
@@ -44,12 +48,16 @@
44
48
  */
45
49
  export { SarifToSlackService } from './SarifToSlackService'
46
50
  export {
51
+ CalculateResultsBy,
47
52
  FooterOptions,
48
53
  FooterType,
54
+ GroupResultsBy,
49
55
  IncludeAwareOptions,
50
56
  IncludeAwareWithValueOptions,
51
57
  LogLevel,
52
- Sarif,
58
+ LogOptions,
59
+ SarifLog,
60
+ SarifToSlackOutput,
53
61
  SarifToSlackServiceOptions,
54
62
  SlackMessage,
55
63
  } from './types'
@@ -0,0 +1,114 @@
1
+ import type { Result, Run } from 'sarif';
2
+ import { tryGetRulePropertyByResult } from '../utils/SarifUtils'
3
+ import { SecurityLevel, SecuritySeverity } from './types'
4
+ import Logger from '../Logger'
5
+ import { Map as ImmutableMap } from 'immutable'
6
+ import {
7
+ sortSecurityLevelMap,
8
+ sortSecuritySeverityMap
9
+ } from '../utils/SortUtils';
10
+
11
+ export class SarifModelPerRun {
12
+ public readonly toolName: string
13
+
14
+ private readonly _securitySeverityMap: ImmutableMap<SecuritySeverity, number>
15
+ private readonly _securityLevelMap: ImmutableMap<SecurityLevel, number>
16
+
17
+ constructor(run: Run) {
18
+ this.toolName = run.tool.driver.name
19
+
20
+ this._securitySeverityMap = ImmutableMap<SecuritySeverity, number>().asMutable()
21
+ this._securityLevelMap = ImmutableMap<SecurityLevel, number>().asMutable()
22
+
23
+ this.buildSecuritySeverityMap(run)
24
+ this.buildSecurityLevelMap(run)
25
+ }
26
+
27
+ private identifySecuritySeverity(score?: number): SecuritySeverity {
28
+ if (score === undefined) {
29
+ return SecuritySeverity.Unknown
30
+ }
31
+
32
+ if (score >= 9 && score <= 10) {
33
+ return SecuritySeverity.Critical
34
+ }
35
+
36
+ if (score >= 7) {
37
+ return SecuritySeverity.High
38
+ }
39
+
40
+ if (score >= 4) {
41
+ return SecuritySeverity.Medium
42
+ }
43
+
44
+ if (score >= 0.1) {
45
+ return SecuritySeverity.Low
46
+ }
47
+
48
+ if (score == 0) {
49
+ return SecuritySeverity.None
50
+ }
51
+
52
+ Logger.warn(`Unsupported "${score}" security severity. Saving as "Unknown".`)
53
+ return SecuritySeverity.Unknown
54
+ }
55
+
56
+ private identifySecurityLevel(level?: string): SecurityLevel {
57
+ if (level === undefined) {
58
+ return SecurityLevel.Unknown
59
+ }
60
+
61
+ if (level.toLowerCase() === 'error') {
62
+ return SecurityLevel.Error
63
+ }
64
+
65
+ if (level.toLowerCase() === 'warning') {
66
+ return SecurityLevel.Warning
67
+ }
68
+
69
+ if (level.toLowerCase() === 'note') {
70
+ return SecurityLevel.Note
71
+ }
72
+
73
+ Logger.warn(`Unsupported ${level} security level. Saving as "Unknown".`)
74
+ return SecurityLevel.Unknown
75
+ }
76
+
77
+ private buildSecuritySeverityMap(run: Run): void {
78
+ const results: Result[] = run.results ?? []
79
+ for (const result of results) {
80
+ const severity: SecuritySeverity = this.identifySecuritySeverity(
81
+ tryGetRulePropertyByResult(run, result, 'security-severity')
82
+ )
83
+ const count: number = this._securitySeverityMap.get(severity) || 0
84
+ this._securitySeverityMap.set(severity, count + 1)
85
+ }
86
+ }
87
+
88
+ private tryGetSecurityLevel(run: Run, result: Result): string | undefined {
89
+ if (result.level) {
90
+ return result.level
91
+ }
92
+
93
+ return tryGetRulePropertyByResult(run, result, 'problem.severity')
94
+ }
95
+
96
+ private buildSecurityLevelMap(run: Run): void {
97
+ const results: Result[] = run.results ?? []
98
+ for (const result of results) {
99
+ const level: SecurityLevel = this.identifySecurityLevel(
100
+ this.tryGetSecurityLevel(run, result)
101
+ )
102
+ const count: number = this._securityLevelMap.get(level) || 0
103
+ this._securityLevelMap.set(level, count + 1)
104
+ }
105
+ }
106
+
107
+ public get securitySeverityMap(): ImmutableMap<SecuritySeverity, number> {
108
+ return sortSecuritySeverityMap(this._securitySeverityMap)
109
+ }
110
+
111
+ public get securityLevelMap(): ImmutableMap<SecurityLevel, number> {
112
+ return sortSecurityLevelMap(this._securityLevelMap)
113
+ }
114
+ }
@@ -0,0 +1,116 @@
1
+ import type { SarifLog } from '../types'
2
+ import { Map as ImmutableMap } from 'immutable'
3
+ import { SarifModelPerRun } from './SarifModelPerRun'
4
+ import { SecurityLevel, SecuritySeverity } from './types'
5
+ import {
6
+ sortSecurityLevelMap,
7
+ sortSecuritySeverityMap
8
+ } from '../utils/SortUtils';
9
+
10
+ export type DataGroupedByRun<T> = {
11
+ toolName: string,
12
+ data: ImmutableMap<T, number>
13
+ }
14
+
15
+ export class SarifModelPerSarif {
16
+ private readonly sarifModelPerRunList: Array<SarifModelPerRun>;
17
+
18
+ constructor(sarif: SarifLog) {
19
+ this.sarifModelPerRunList = new Array<SarifModelPerRun>()
20
+ this.buildRunsList(sarif)
21
+ }
22
+
23
+ private buildRunsList(sarif: SarifLog): void {
24
+ for (const run of sarif.runs) {
25
+ this.sarifModelPerRunList.push(new SarifModelPerRun(run))
26
+ }
27
+ }
28
+
29
+ public groupByToolNameWithSecurityLevel(): Map<string, ImmutableMap<SecurityLevel, number>> {
30
+ const result = new Map<string, ImmutableMap<SecurityLevel, number>>()
31
+ for (const sarifModelPerRun of this.sarifModelPerRunList) {
32
+ if (!result.has(sarifModelPerRun.toolName)) {
33
+ result.set(sarifModelPerRun.toolName, ImmutableMap<SecurityLevel, number>().asMutable())
34
+ }
35
+ for (const [k, v] of sarifModelPerRun.securityLevelMap.entries()) {
36
+ const count: number = result.get(sarifModelPerRun.toolName)?.get(k) || 0
37
+ result.get(sarifModelPerRun.toolName)?.set(k, count + v)
38
+ }
39
+ }
40
+ // Sort
41
+ for (const [k, v] of result) {
42
+ result.set(k, sortSecurityLevelMap(v))
43
+ }
44
+ return result
45
+ }
46
+
47
+ public groupByRunWithSecurityLevel(): DataGroupedByRun<SecurityLevel>[] {
48
+ const result = new Array<DataGroupedByRun<SecurityLevel>>()
49
+ for (const sarifModelPerRun of this.sarifModelPerRunList) {
50
+ result.push({
51
+ toolName: sarifModelPerRun.toolName,
52
+ data: sarifModelPerRun.securityLevelMap,
53
+ })
54
+ }
55
+ return result
56
+ }
57
+
58
+ public groupByTotalWithSecurityLevel(): ImmutableMap<SecurityLevel, number> {
59
+ const result = ImmutableMap<SecurityLevel, number>().asMutable()
60
+ for (const sarifModelPerRun of this.sarifModelPerRunList) {
61
+ for (const [k, v] of sarifModelPerRun.securityLevelMap.entries()) {
62
+ const count: number = result.get(k) || 0
63
+ result.set(k, count + v)
64
+ }
65
+ }
66
+ return sortSecurityLevelMap(result)
67
+ }
68
+
69
+ public groupByToolNameWithSecuritySeverity(): Map<string, ImmutableMap<SecuritySeverity, number>> {
70
+ const result = new Map<string, ImmutableMap<SecuritySeverity, number>>()
71
+ for (const sarifModelPerRun of this.sarifModelPerRunList) {
72
+ if (!result.has(sarifModelPerRun.toolName)) {
73
+ result.set(sarifModelPerRun.toolName, ImmutableMap<SecuritySeverity, number>().asMutable())
74
+ }
75
+ for (const [k, v] of sarifModelPerRun.securitySeverityMap.entries()) {
76
+ const count: number = result.get(sarifModelPerRun.toolName)?.get(k) || 0
77
+ result.get(sarifModelPerRun.toolName)?.set(k, count + v)
78
+ }
79
+ }
80
+ // Sort
81
+ for (const [k, v] of result.entries()) {
82
+ result.set(k, sortSecuritySeverityMap(v))
83
+ }
84
+ return result
85
+ }
86
+
87
+ public groupByRunWithSecuritySeverity(): DataGroupedByRun<SecuritySeverity>[] {
88
+ const result = new Array<DataGroupedByRun<SecuritySeverity>>()
89
+ for (const sarifModelPerRun of this.sarifModelPerRunList) {
90
+ result.push({
91
+ toolName: sarifModelPerRun.toolName,
92
+ data: sarifModelPerRun.securitySeverityMap,
93
+ })
94
+ }
95
+ return result
96
+ }
97
+
98
+ public groupByTotalWithSecuritySeverity(): ImmutableMap<SecuritySeverity, number> {
99
+ const result = ImmutableMap<SecuritySeverity, number>().asMutable()
100
+ for (const sarifModelPerRun of this.sarifModelPerRunList) {
101
+ for (const [k, v] of sarifModelPerRun.securitySeverityMap.entries()) {
102
+ const count: number = result.get(k) || 0
103
+ result.set(k, count + v)
104
+ }
105
+ }
106
+ return sortSecuritySeverityMap(result)
107
+ }
108
+
109
+ public listToolNames(): Set<string> {
110
+ const toolNames = new Set<string>()
111
+ for (const sarifModelPerRun of this.sarifModelPerRunList) {
112
+ toolNames.add(sarifModelPerRun.toolName)
113
+ }
114
+ return toolNames
115
+ }
116
+ }
@@ -0,0 +1,31 @@
1
+ export enum SecuritySeverity {
2
+ Unknown = 'Unknown',
3
+ None = 'None',
4
+ Low = 'Low',
5
+ Medium = 'Medium',
6
+ High = 'High',
7
+ Critical = 'Critical'
8
+ }
9
+
10
+ export const SecuritySeverityOrder: SecuritySeverity[] = [
11
+ SecuritySeverity.Critical,
12
+ SecuritySeverity.High,
13
+ SecuritySeverity.Medium,
14
+ SecuritySeverity.Low,
15
+ SecuritySeverity.None,
16
+ SecuritySeverity.Unknown
17
+ ]
18
+
19
+ export enum SecurityLevel {
20
+ Unknown = 'Unknown',
21
+ Note = 'Note',
22
+ Warning = 'Warning',
23
+ Error = 'Error'
24
+ }
25
+
26
+ export const SecurityLevelOrder: SecurityLevel[] = [
27
+ SecurityLevel.Error,
28
+ SecurityLevel.Warning,
29
+ SecurityLevel.Note,
30
+ SecurityLevel.Unknown
31
+ ]
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@ import type { Log } from 'sarif'
4
4
  * Type representing a SARIF log.
5
5
  * @public
6
6
  */
7
- export type Sarif = Log
7
+ export type SarifLog = Log
8
8
 
9
9
  /**
10
10
  * Interface for a Slack message that can be sent.
@@ -19,7 +19,7 @@ export interface SlackMessage {
19
19
  /**
20
20
  * The SARIF log associated with this Slack message.
21
21
  */
22
- sarif: Sarif
22
+ sarif: SarifLog
23
23
  }
24
24
 
25
25
  /**
@@ -28,7 +28,8 @@ export interface SlackMessage {
28
28
  */
29
29
  export enum LogLevel {
30
30
  /**
31
- * Represents the most verbose logging level, typically used for detailed debugging information.
31
+ * Represents the most verbose logging level, typically used for detailed
32
+ * debugging information.
32
33
  */
33
34
  Silly = 0,
34
35
  /**
@@ -36,23 +37,28 @@ export enum LogLevel {
36
37
  */
37
38
  Trace = 1,
38
39
  /**
39
- * Represents a logging level for debugging information that is less verbose than silly.
40
+ * Represents a logging level for debugging information that is less verbose
41
+ * than silly.
40
42
  */
41
43
  Debug = 2,
42
44
  /**
43
- * Represents a logging level for general informational messages that highlight the progress of the application.
45
+ * Represents a logging level for general informational messages that highlight
46
+ * the progress of the application.
44
47
  */
45
48
  Info = 3,
46
49
  /**
47
- * Represents a logging level for potentially harmful situations that require attention.
50
+ * Represents a logging level for potentially harmful situations that require
51
+ * attention.
48
52
  */
49
53
  Warning = 4,
50
54
  /**
51
- * Represents a logging level for error conditions that do not require immediate action but should be noted.
55
+ * Represents a logging level for error conditions that do not require immediate
56
+ * action but should be noted.
52
57
  */
53
58
  Error = 5,
54
59
  /**
55
- * Represents a logging level for critical errors that require immediate attention and may cause the application to terminate.
60
+ * Represents a logging level for critical errors that require immediate attention
61
+ * and may cause the application to terminate.
56
62
  */
57
63
  Fatal = 6
58
64
  }
@@ -80,8 +86,15 @@ export type IncludeAwareWithValueOptions = IncludeAwareOptions & {
80
86
  * @public
81
87
  */
82
88
  export enum FooterType {
83
- PLAIN_TEXT = 'plain_text',
84
- MARKDOWN = 'mrkdwn'
89
+ /**
90
+ * Represents a plain text footer. Text is not formatted and appears as-is.
91
+ */
92
+ PlainText = 'plain_text',
93
+ /**
94
+ * Represents a footer with Markdown formatting. Text can include formatting
95
+ * such as bold, italics, and links.
96
+ */
97
+ Markdown = 'mrkdwn'
85
98
  }
86
99
 
87
100
  /**
@@ -93,6 +106,72 @@ export type FooterOptions = IncludeAwareWithValueOptions & {
93
106
  type?: FooterType
94
107
  }
95
108
 
109
+ /**
110
+ * Enum representing how to group results.
111
+ * @public
112
+ */
113
+ export enum GroupResultsBy {
114
+ /**
115
+ * Groups results by the tool name. Particularly, groups by the runs[].tool.driver.name
116
+ * property from the SARIF file(s).
117
+ */
118
+ ToolName = 0,
119
+ /**
120
+ * Groups results by the run. It provides the result from each run individually.
121
+ */
122
+ Run = 1,
123
+ /**
124
+ * Does not group results. It provides the result from all the runs from all
125
+ * the provided SARIF files.
126
+ */
127
+ Total = 2,
128
+ }
129
+
130
+ /**
131
+ * Enum representing how to calculate results.
132
+ * @public
133
+ */
134
+ export enum CalculateResultsBy {
135
+ /**
136
+ * Calculates results by the security level of the findings: Error, Warning,
137
+ * Note and Unknown. At first, it tries to get the security level from runs[].results[].level
138
+ * property. If it is not defined, it tries to get the security level from the
139
+ * respective rule of each result, using the rules[].properties['problem.severity']
140
+ * property.
141
+ */
142
+ Level = 0,
143
+ /**
144
+ * Calculates results by the security severity of the findings: Critical, High,
145
+ * Medium, Low, None and Unknown. it tries to get the security severity from the
146
+ * respective rule of each result, using the rules[].properties['security-severity']
147
+ * property. This property contains CVSS score, which is then mapped to the
148
+ * security severity value.
149
+ */
150
+ Severity = 1,
151
+ }
152
+
153
+ /**
154
+ * Options for how to output the results in the Slack message.
155
+ * @public
156
+ */
157
+ export type SarifToSlackOutput = {
158
+ groupBy: GroupResultsBy,
159
+ calculateBy: CalculateResultsBy,
160
+ }
161
+
162
+ /**
163
+ * Options for logging.
164
+ * @public
165
+ */
166
+ export type LogOptions = {
167
+ level?: LogLevel,
168
+ /**
169
+ * More details here: https://github.com/fullstack-build/tslog?tab=readme-ov-file#pretty-templates-and-styles-color-settings
170
+ */
171
+ template?: string,
172
+ colored?: boolean,
173
+ }
174
+
96
175
  /**
97
176
  * Options for the SarifToSlackService.
98
177
  * @public
@@ -104,9 +183,10 @@ export type SarifToSlackServiceOptions = {
104
183
  username?: string,
105
184
  iconUrl?: string,
106
185
  color?: string,
107
- logLevel?: LogLevel | string,
186
+ log?: LogOptions,
108
187
  header?: IncludeAwareWithValueOptions,
109
188
  footer?: FooterOptions,
110
189
  actor?: IncludeAwareWithValueOptions,
111
190
  run?: IncludeAwareOptions,
191
+ output?: SarifToSlackOutput,
112
192
  }