@fabasoad/sarif-to-slack 0.1.1 → 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 (99) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/.github/pull_request_template.md +3 -3
  3. package/.github/workflows/linting.yml +14 -0
  4. package/.github/workflows/release.yml +5 -1
  5. package/.github/workflows/send-sarif-to-slack.yml +214 -0
  6. package/.github/workflows/unit-tests.yml +1 -0
  7. package/.pre-commit-config.yaml +3 -3
  8. package/.tool-versions +1 -1
  9. package/CONTRIBUTING.md +1 -1
  10. package/Makefile +10 -3
  11. package/README.md +36 -5
  12. package/biome.json +15 -12
  13. package/dist/Logger.js +17 -6
  14. package/dist/Processors.js +23 -22
  15. package/dist/SarifToSlackService.d.ts.map +1 -1
  16. package/dist/SarifToSlackService.js +6 -7
  17. package/dist/SlackMessageBuilder.js +51 -55
  18. package/dist/index.d.ts +9 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +10 -5
  21. package/dist/model/SarifModelPerRun.d.ts +17 -0
  22. package/dist/model/SarifModelPerRun.d.ts.map +1 -0
  23. package/dist/model/SarifModelPerRun.js +84 -0
  24. package/dist/model/SarifModelPerSarif.d.ts +20 -0
  25. package/dist/model/SarifModelPerSarif.d.ts.map +1 -0
  26. package/dist/model/SarifModelPerSarif.js +97 -0
  27. package/dist/model/types.d.ts +17 -0
  28. package/dist/model/types.d.ts.map +1 -0
  29. package/dist/model/types.js +31 -0
  30. package/dist/sarif-to-slack.d.ts +121 -18
  31. package/dist/tsdoc-metadata.json +1 -1
  32. package/dist/types.d.ts +107 -15
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/types.js +73 -7
  35. package/dist/utils/SarifUtils.d.ts +5 -0
  36. package/dist/utils/SarifUtils.d.ts.map +1 -0
  37. package/dist/utils/SarifUtils.js +32 -0
  38. package/dist/utils/SortUtils.d.ts +5 -0
  39. package/dist/utils/SortUtils.d.ts.map +1 -0
  40. package/dist/utils/SortUtils.js +8 -0
  41. package/dist/version.d.ts +2 -0
  42. package/dist/version.d.ts.map +1 -0
  43. package/dist/version.js +4 -0
  44. package/etc/sarif-to-slack.api.md +47 -9
  45. package/jest.config.json +4 -4
  46. package/package.json +14 -10
  47. package/scripts/save-version.sh +6 -0
  48. package/src/Logger.ts +22 -17
  49. package/src/Processors.ts +22 -22
  50. package/src/SarifToSlackService.ts +6 -7
  51. package/src/SlackMessageBuilder.ts +85 -68
  52. package/src/index.ts +17 -6
  53. package/src/model/SarifModelPerRun.ts +114 -0
  54. package/src/model/SarifModelPerSarif.ts +116 -0
  55. package/src/model/types.ts +31 -0
  56. package/src/types.ts +113 -15
  57. package/src/utils/SarifUtils.ts +44 -0
  58. package/src/utils/SortUtils.ts +21 -0
  59. package/src/version.ts +3 -0
  60. package/test-data/sarif/codeql-csharp.sarif +1 -0
  61. package/test-data/sarif/codeql-go.sarif +1 -0
  62. package/test-data/sarif/codeql-python.sarif +1 -0
  63. package/test-data/sarif/codeql-ruby.sarif +1 -0
  64. package/test-data/sarif/codeql-typescript.sarif +1 -0
  65. package/test-data/sarif/grype-container.sarif +1774 -0
  66. package/test-data/sarif/runs-1-tools-1-results-0.sarif +18 -0
  67. package/test-data/sarif/runs-2-tools-1-results-0.sarif +30 -0
  68. package/test-data/sarif/runs-2-tools-1.sarif +656 -0
  69. package/test-data/sarif/runs-2-tools-2-results-0.sarif +44 -0
  70. package/test-data/sarif/runs-2-tools-2.sarif +686 -0
  71. package/test-data/sarif/runs-3-tools-2-results-0.sarif +48 -0
  72. package/test-data/sarif/runs-3-tools-2.sarif +278 -0
  73. package/test-data/sarif/snyk-composer.sarif +934 -0
  74. package/test-data/sarif/snyk-container.sarif +313 -0
  75. package/test-data/sarif/snyk-gomodules.sarif +388 -0
  76. package/test-data/sarif/snyk-gradle.sarif +274 -0
  77. package/test-data/sarif/snyk-hex.sarif +66 -0
  78. package/test-data/sarif/snyk-maven.sarif +274 -0
  79. package/test-data/sarif/snyk-npm.sarif +896 -0
  80. package/test-data/sarif/snyk-nuget.sarif +90 -0
  81. package/test-data/sarif/snyk-pip.sarif +66 -0
  82. package/test-data/sarif/snyk-pnpm.sarif +90 -0
  83. package/test-data/sarif/snyk-poetry.sarif +1952 -0
  84. package/test-data/sarif/snyk-rubygems.sarif +440 -0
  85. package/test-data/sarif/snyk-sbt.sarif +178 -0
  86. package/test-data/sarif/snyk-swift.sarif +112 -0
  87. package/test-data/sarif/snyk-yarn.sarif +2900 -0
  88. package/test-data/sarif/trivy-iac.sarif +134 -0
  89. package/test-data/sarif/wiz-container.sarif +30916 -0
  90. package/test-data/sarif/wiz-iac.sarif +558 -0
  91. package/tests/Processors.spec.ts +3 -3
  92. package/tests/integration/SendSarifToSlack.spec.ts +56 -0
  93. package/tsconfig.json +14 -14
  94. package/dist/Logger.js.map +0 -1
  95. package/dist/Processors.js.map +0 -1
  96. package/dist/SarifToSlackService.js.map +0 -1
  97. package/dist/SlackMessageBuilder.js.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/types.js.map +0 -1
@@ -7,12 +7,36 @@
7
7
  import type { Log } from 'sarif';
8
8
 
9
9
  // @public
10
- export type IncludeAwareProps = {
10
+ export enum CalculateResultsBy {
11
+ Level = 0,
12
+ Severity = 1
13
+ }
14
+
15
+ // @public
16
+ export type FooterOptions = IncludeAwareWithValueOptions & {
17
+ type?: FooterType;
18
+ };
19
+
20
+ // @public
21
+ export enum FooterType {
22
+ Markdown = "mrkdwn",
23
+ PlainText = "plain_text"
24
+ }
25
+
26
+ // @public
27
+ export enum GroupResultsBy {
28
+ Run = 1,
29
+ ToolName = 0,
30
+ Total = 2
31
+ }
32
+
33
+ // @public
34
+ export type IncludeAwareOptions = {
11
35
  include: boolean;
12
36
  };
13
37
 
14
38
  // @public
15
- export type IncludeAwareWithValueProps = IncludeAwareProps & {
39
+ export type IncludeAwareWithValueOptions = IncludeAwareOptions & {
16
40
  value?: string;
17
41
  };
18
42
 
@@ -28,7 +52,20 @@ export enum LogLevel {
28
52
  }
29
53
 
30
54
  // @public
31
- export type Sarif = Log;
55
+ export type LogOptions = {
56
+ level?: LogLevel;
57
+ template?: string;
58
+ colored?: boolean;
59
+ };
60
+
61
+ // @public
62
+ export type SarifLog = Log;
63
+
64
+ // @public
65
+ export type SarifToSlackOutput = {
66
+ groupBy: GroupResultsBy;
67
+ calculateBy: CalculateResultsBy;
68
+ };
32
69
 
33
70
  // @public
34
71
  export class SarifToSlackService {
@@ -45,16 +82,17 @@ export type SarifToSlackServiceOptions = {
45
82
  username?: string;
46
83
  iconUrl?: string;
47
84
  color?: string;
48
- logLevel?: LogLevel | string;
49
- header?: IncludeAwareWithValueProps;
50
- footer?: IncludeAwareWithValueProps;
51
- actor?: IncludeAwareWithValueProps;
52
- run?: IncludeAwareProps;
85
+ log?: LogOptions;
86
+ header?: IncludeAwareWithValueOptions;
87
+ footer?: FooterOptions;
88
+ actor?: IncludeAwareWithValueOptions;
89
+ run?: IncludeAwareOptions;
90
+ output?: SarifToSlackOutput;
53
91
  };
54
92
 
55
93
  // @public
56
94
  export interface SlackMessage {
57
- sarif: Sarif;
95
+ sarif: SarifLog;
58
96
  send: () => Promise<string>;
59
97
  }
60
98
 
package/jest.config.json CHANGED
@@ -10,10 +10,10 @@
10
10
  ],
11
11
  "coverageThreshold": {
12
12
  "global": {
13
- "branches": 20,
14
- "functions": 15,
15
- "lines": 25,
16
- "statements": 25
13
+ "branches": 15,
14
+ "functions": 10,
15
+ "lines": 15,
16
+ "statements": 15
17
17
  }
18
18
  },
19
19
  "moduleFileExtensions": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabasoad/sarif-to-slack",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "TypeScript library to send results of SARIF file to Slack webhook URL.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -8,12 +8,15 @@
8
8
  "private": false,
9
9
  "scripts": {
10
10
  "lint": "biome lint --write src",
11
- "test": "jest --config=jest.config.json --json --outputFile=jest-report.json --coverage",
11
+ "test": "jest --config=jest.config.json --json --outputFile=jest-report.json --coverage --testNamePattern=unit",
12
+ "test:integration": "jest --config=jest.config.json --testNamePattern=integration",
12
13
  "clean": "rm -rf coverage && rm -rf temp",
13
14
  "clean:unsafe": "rm -f package-lock.json && rm -rf node_modules && rm -rf dist && rm -rf lib",
14
15
  "tsc": "tsc",
15
- "build": "tsc && api-extractor run --local --verbose",
16
+ "prebuild": "./scripts/save-version.sh",
17
+ "build": "npm run tsc && api-extractor run --local --verbose",
16
18
  "prepublishOnly": "npm run build",
19
+ "preinstall": "./scripts/save-version.sh",
17
20
  "version:patch": "npm version patch --commit-hooks --git-tag-version --message 'chore: bump to version %s'",
18
21
  "version:minor": "npm version minor --commit-hooks --git-tag-version --message 'chore: bump to version %s'",
19
22
  "version:major": "npm version major --commit-hooks --git-tag-version --message 'chore: bump to version %s'",
@@ -33,24 +36,25 @@
33
36
  "author": "Yevhen Fabizhevskyi",
34
37
  "license": "MIT",
35
38
  "bugs": {
36
- "url": "https://github.com/fabasoad/sarif-to-slack-action/issues"
39
+ "url": "https://github.com/fabasoad/sarif-to-slack/issues"
37
40
  },
38
41
  "publishConfig": {
39
42
  "access": "public"
40
43
  },
41
- "homepage": "https://github.com/fabasoad/sarif-to-slack-action#readme",
44
+ "homepage": "https://github.com/fabasoad/sarif-to-slack#readme",
42
45
  "dependencies": {
43
46
  "@slack/webhook": "7.0.5",
44
47
  "@types/sarif": "2.1.7",
48
+ "immutable": "5.1.3",
45
49
  "tslog": "4.9.3"
46
50
  },
47
51
  "devDependencies": {
48
- "@biomejs/biome": "1.9.4",
49
- "@microsoft/api-documenter": "7.26.29",
50
- "@microsoft/api-extractor": "7.52.8",
52
+ "@biomejs/biome": "2.1.2",
53
+ "@microsoft/api-documenter": "7.26.30",
54
+ "@microsoft/api-extractor": "7.52.9",
51
55
  "@types/jest": "30.0.0",
52
- "jest": "30.0.3",
53
- "jest-circus": "30.0.3",
56
+ "jest": "30.0.5",
57
+ "jest-circus": "30.0.5",
54
58
  "ts-jest": "29.4.0",
55
59
  "typescript": "5.8.3"
56
60
  }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env sh
2
+
3
+ version=$(jq -r '.version' package.json)
4
+ echo "// This file is autogenerated by scripts/save-version.sh" > src/version.ts
5
+ echo "// Do not edit it manually!" >> src/version.ts
6
+ echo "export const LIB_VERSION = '${version}'" >> src/version.ts
package/src/Logger.ts CHANGED
@@ -1,34 +1,39 @@
1
- import { Logger as TSLogger, ILogObj } from 'tslog'
2
- import { LogLevel } from './types'
3
-
4
- /**
5
- * Logger options for configuring the logging behavior.
6
- * @internal
7
- */
8
- export type LoggerOptions = {
9
- logLevel?: LogLevel
10
- }
1
+ import { ILogObj, Logger as TSLogger } from 'tslog'
2
+ import { LogLevel, LogOptions } from './types'
11
3
 
12
4
  /**
13
5
  * Logger class for managing logging operations.
14
6
  * @internal
15
7
  */
16
8
  export default class Logger {
17
- private static instance: TSLogger<ILogObj> = new TSLogger();
9
+ private static DEFAULT_LOG_LEVEL: LogLevel = LogLevel.Info
10
+ private static DEFAULT_LOG_TEMPLATE: string = '[{{logLevelName}}] [{{name}}] {{dateIsoStr}} '
11
+ private static DEFAULT_LOG_COLORED: boolean = true
12
+
13
+ private static instance: TSLogger<ILogObj>
18
14
 
19
- public static initialize({ logLevel = LogLevel.Info }: LoggerOptions): void {
15
+ public static initialize(opts?: LogOptions): void {
20
16
  if (!Logger.instance) {
21
17
  Logger.instance = new TSLogger({
22
- minLevel: process.env.ACTIONS_STEP_DEBUG === 'true' ? 0 : logLevel,
18
+ name: '@fabasoad/sarif-to-slack',
19
+ minLevel: process.env.ACTIONS_STEP_DEBUG === 'true' ? LogLevel.Silly : (opts?.level ?? Logger.DEFAULT_LOG_LEVEL),
20
+ type: 'pretty',
21
+ prettyLogTimeZone: 'UTC',
22
+ prettyLogTemplate: opts?.template ?? Logger.DEFAULT_LOG_TEMPLATE,
23
+ stylePrettyLogs: opts?.colored ?? Logger.DEFAULT_LOG_COLORED,
23
24
  })
24
25
  }
25
26
  }
26
27
 
27
- public static info(...args: string[]): void {
28
- Logger.instance.info(args)
28
+ public static warn(...args: unknown[]): void {
29
+ Logger.instance.warn(...args)
30
+ }
31
+
32
+ public static info(...args: unknown[]): void {
33
+ Logger.instance.info(...args)
29
34
  }
30
35
 
31
- public static debug(...args: string[]): void {
32
- Logger.instance.debug(args)
36
+ public static debug(...args: unknown[]): void {
37
+ Logger.instance.debug(...args)
33
38
  }
34
39
  }
package/src/Processors.ts CHANGED
@@ -27,7 +27,7 @@ export function processColor(color?: string): string | undefined {
27
27
  Logger.info(`Converting "${color}" to #808080`)
28
28
  return '#808080'
29
29
  default:
30
- Logger.debug(`"${color}" color is not a CI status identifier. Returning as is...`)
30
+ Logger.debug(`"${color}" color is not a CI status identifier. Returning as is.`)
31
31
  return color
32
32
  }
33
33
  }
@@ -39,28 +39,28 @@ export function processColor(color?: string): string | undefined {
39
39
  * @throws Error If the input string does not match any known log level.
40
40
  * @internal
41
41
  */
42
- export function processLogLevel(logLevel?: LogLevel | string): LogLevel | undefined {
43
- if (typeof logLevel === 'string') {
44
- switch (logLevel.toLowerCase()) {
45
- case 'silly':
46
- return 0
47
- case 'trace':
48
- return 1
49
- case 'debug':
50
- return 2
51
- case 'info':
52
- return 3
53
- case 'warning':
54
- return 4
55
- case 'error':
56
- return 5
57
- case 'fatal':
58
- return 6
59
- default:
60
- throw new Error(`Unknown log level: ${logLevel}`)
61
- }
42
+ export function processLogLevel(logLevel?: string): LogLevel | undefined {
43
+ if (!logLevel) {
44
+ return undefined
45
+ }
46
+ switch (logLevel.toLowerCase()) {
47
+ case 'silly':
48
+ return LogLevel.Silly
49
+ case 'trace':
50
+ return LogLevel.Trace
51
+ case 'debug':
52
+ return LogLevel.Debug
53
+ case 'info':
54
+ return LogLevel.Info
55
+ case 'warning':
56
+ return LogLevel.Warning
57
+ case 'error':
58
+ return LogLevel.Error
59
+ case 'fatal':
60
+ return LogLevel.Fatal
61
+ default:
62
+ throw new Error(`Unknown log level: ${logLevel}`)
62
63
  }
63
- return logLevel
64
64
  }
65
65
 
66
66
  /**
@@ -1,9 +1,9 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import Logger from './Logger'
3
- import { processColor, processLogLevel, processSarifPath } from './Processors'
3
+ import { processColor, processSarifPath } from './Processors'
4
4
  import { SlackMessageBuilder } from './SlackMessageBuilder'
5
5
  import {
6
- Sarif,
6
+ SarifLog,
7
7
  SarifToSlackServiceOptions,
8
8
  SlackMessage
9
9
  } from './types'
@@ -22,13 +22,14 @@ async function initialize(opts: SarifToSlackServiceOptions): Promise<Map<string,
22
22
  username: opts.username,
23
23
  iconUrl: opts.iconUrl,
24
24
  color: processColor(opts.color),
25
- sarif: JSON.parse(jsonString) as Sarif
25
+ sarif: JSON.parse(jsonString) as SarifLog,
26
+ output: opts.output,
26
27
  })
27
28
  if (opts.header?.include) {
28
29
  messageBuilder.withHeader(opts.header?.value)
29
30
  }
30
31
  if (opts.footer?.include) {
31
- messageBuilder.withFooter(opts.footer?.value)
32
+ messageBuilder.withFooter(opts.footer?.value, opts.footer?.type)
32
33
  }
33
34
  if (opts.actor?.include) {
34
35
  messageBuilder.withActor(opts.actor?.value)
@@ -69,9 +70,7 @@ export class SarifToSlackService {
69
70
  * @public
70
71
  */
71
72
  public static async create(opts: SarifToSlackServiceOptions): Promise<SarifToSlackService> {
72
- Logger.initialize({
73
- logLevel: processLogLevel(opts.logLevel)
74
- })
73
+ Logger.initialize(opts.log)
75
74
  const instance: SarifToSlackService = new SarifToSlackService()
76
75
  const map: Map<string, SlackMessage> = await initialize(opts)
77
76
  map.forEach((val: SlackMessage, key: string) => instance._slackMessages.set(key, val))
@@ -1,8 +1,22 @@
1
1
  import { AnyBlock } from '@slack/types'
2
- import { HeaderBlock, ContextBlock } from '@slack/types/dist/block-kit/blocks'
2
+ import { ContextBlock, HeaderBlock } from '@slack/types/dist/block-kit/blocks'
3
+ import { TextObject } from '@slack/types/dist/block-kit/composition-objects'
3
4
  import { IncomingWebhook } from '@slack/webhook'
4
- import type { Run, Result, ReportingDescriptor } from 'sarif'
5
- import { Sarif, SlackMessage } from './types'
5
+ import { Map as ImmutableMap } from 'immutable'
6
+ import {
7
+ CalculateResultsBy,
8
+ FooterType,
9
+ GroupResultsBy,
10
+ SarifLog,
11
+ SarifToSlackOutput,
12
+ SlackMessage
13
+ } from './types'
14
+ import { LIB_VERSION } from './version'
15
+ import {
16
+ DataGroupedByRun,
17
+ SarifModelPerSarif
18
+ } from './model/SarifModelPerSarif';
19
+ import { SecurityLevel, SecuritySeverity } from './model/types';
6
20
 
7
21
  /**
8
22
  * Options for the SlackMessageBuilder.
@@ -12,11 +26,10 @@ export type SlackMessageBuilderOptions = {
12
26
  username?: string
13
27
  iconUrl?: string
14
28
  color?: string
15
- sarif: Sarif
29
+ sarif: SarifLog,
30
+ output?: SarifToSlackOutput,
16
31
  }
17
32
 
18
- type RuleData = { id?: string, index?: number }
19
-
20
33
  /**
21
34
  * Class for building and sending Slack messages based on SARIF logs.
22
35
  * @internal
@@ -25,22 +38,29 @@ export class SlackMessageBuilder implements SlackMessage {
25
38
  private readonly webhook: IncomingWebhook
26
39
  private readonly gitHubServerUrl: string
27
40
  private readonly color?: string
28
-
41
+ private readonly sarifModelPerSarif: SarifModelPerSarif
42
+ private readonly output: SarifToSlackOutput
29
43
  private header?: HeaderBlock
44
+
30
45
  private footer?: ContextBlock
31
46
  private actor?: string
32
47
  private runId?: string
33
48
 
34
- public readonly sarif: Sarif
49
+ public readonly sarif: SarifLog
35
50
 
36
51
  constructor(url: string, opts: SlackMessageBuilderOptions) {
37
52
  this.webhook = new IncomingWebhook(url, {
38
53
  username: opts.username || 'SARIF results',
39
54
  icon_url: opts.iconUrl
40
55
  })
56
+ this.gitHubServerUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'
41
57
  this.color = opts.color
42
58
  this.sarif = opts.sarif
43
- 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
+ }
44
64
  }
45
65
 
46
66
  withHeader(header?: string): void {
@@ -61,14 +81,14 @@ export class SlackMessageBuilder implements SlackMessage {
61
81
  this.runId = process.env.GITHUB_RUN_ID
62
82
  }
63
83
 
64
- withFooter(footer?: string): void {
65
- const repoName = 'fabasoad/sarif-to-slack-action'
84
+ withFooter(text?: string, type?: FooterType): void {
85
+ const repoName = 'fabasoad/sarif-to-slack'
86
+ const element: TextObject = text
87
+ ? { type: type || FooterType.PlainText, text }
88
+ : { type: FooterType.Markdown, text: `Generated by <${this.gitHubServerUrl}/${repoName}|@${repoName}@${LIB_VERSION}>` }
66
89
  this.footer = {
67
90
  type: 'context',
68
- elements: [{
69
- type: footer ? 'plain_text' : 'mrkdwn',
70
- text: footer || `Generated by <${this.gitHubServerUrl}/${repoName}|${repoName}>`
71
- }],
91
+ elements: [element],
72
92
  }
73
93
  }
74
94
 
@@ -109,68 +129,65 @@ export class SlackMessageBuilder implements SlackMessage {
109
129
  }
110
130
  text.push(runText)
111
131
  }
112
- return text.join('\n')
132
+ return text.join('\n\n')
113
133
  }
114
134
 
115
- private composeRunSummary(toolName: string, map: Map<string, number>): string {
116
- const levelsText: string[] = []
117
- for (const [level, count] of map.entries()) {
118
- const levelCapitalized = level.charAt(0).toUpperCase() + level.slice(1)
119
- 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}`)
120
142
  }
121
- return `*${toolName}*\n${levelsText.join(', ')}`
143
+ return resultProcessor(
144
+ stats.length == 0 ? 'No issues found' : stats.join(', ')
145
+ )
122
146
  }
123
147
 
124
148
  private composeSummary(): string {
125
- const data = new Map<string, Map<string, number>>()
126
- for (const run of this.sarif.runs) {
127
- const toolName = run.tool.driver.name
128
- if (!data.has(toolName)) {
129
- data.set(toolName, new Map<string, number>())
130
- }
131
- const results: Result[] = run.results ?? []
132
- for (const result of results) {
133
- const level: string = this.tryGetLevel(run, result)
134
- const count: number = data.get(toolName)?.get(level) || 0
135
- data.get(toolName)?.set(level, count + 1)
136
- }
137
- }
138
- const summaries: string[] = []
139
- for (const [toolName, map] of data.entries()) {
140
- summaries.push(this.composeRunSummary(toolName, map))
141
- }
142
- return summaries.join('\n')
143
- }
144
-
145
- private tryGetLevel(run: Run, result: Result): string {
146
- if (result.level) {
147
- return result.level
148
- }
149
-
150
- const ruleData: RuleData = {}
151
-
152
- if (result.rule) {
153
- if (result.rule?.index) {
154
- 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
155
163
  }
156
- if (result.rule?.id) {
157
- 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
158
177
  }
159
- }
160
-
161
- if (!ruleData.index && result.ruleIndex) {
162
- ruleData.index = result.ruleIndex
163
- }
164
-
165
- if (ruleData.index
166
- && run.tool.driver?.rules
167
- && ruleData.index < run.tool.driver.rules.length) {
168
- const rule: ReportingDescriptor = run.tool.driver.rules[ruleData.index]
169
- if (rule.properties && 'problem.severity' in rule.properties) {
170
- 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
171
189
  }
172
190
  }
173
-
174
- return 'unknown'
191
+ return summaries.join('\n\n')
175
192
  }
176
193
  }
package/src/index.ts CHANGED
@@ -9,12 +9,16 @@
9
9
  *
10
10
  * @example
11
11
  * ```typescript
12
- * import { SarifToSlackService } from 'sarif-to-slack';
12
+ * import { SarifToSlackService, FooterType } from '@fabasoad/sarif-to-slack';
13
13
  *
14
- * const service = new SarifToSlackService({
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',
@@ -24,6 +28,7 @@
24
28
  * },
25
29
  * footer: {
26
30
  * include: true,
31
+ * type: FooterType.PLAIN_TEXT,
27
32
  * value: 'Generated by @fabasoad/sarif-to-slack'
28
33
  * },
29
34
  * actor: {
@@ -43,10 +48,16 @@
43
48
  */
44
49
  export { SarifToSlackService } from './SarifToSlackService'
45
50
  export {
46
- IncludeAwareProps,
47
- IncludeAwareWithValueProps,
51
+ CalculateResultsBy,
52
+ FooterOptions,
53
+ FooterType,
54
+ GroupResultsBy,
55
+ IncludeAwareOptions,
56
+ IncludeAwareWithValueOptions,
48
57
  LogLevel,
49
- Sarif,
58
+ LogOptions,
59
+ SarifLog,
60
+ SarifToSlackOutput,
50
61
  SarifToSlackServiceOptions,
51
62
  SlackMessage,
52
63
  } from './types'