@fabasoad/sarif-to-slack 1.3.5 → 1.4.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 (142) hide show
  1. package/.github/workflows/security.yml +1 -0
  2. package/.github/workflows/send-sarif-to-slack.yml +39 -15
  3. package/README.md +8 -7
  4. package/dist/Logger.js +40 -30
  5. package/dist/SarifToSlackClient.d.ts +0 -1
  6. package/dist/SarifToSlackClient.d.ts.map +1 -1
  7. package/dist/SarifToSlackClient.js +11 -8
  8. package/dist/globalState.d.ts +2 -0
  9. package/dist/globalState.d.ts.map +1 -0
  10. package/dist/globalState.js +2 -0
  11. package/dist/index.cjs +111 -81
  12. package/dist/index.d.ts +40 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +9 -2
  15. package/dist/model/Finding.js +7 -5
  16. package/dist/model/FindingArray.js +1 -1
  17. package/dist/model/SendIf.js +1 -1
  18. package/dist/model/SlackMessage.js +6 -6
  19. package/dist/model/color/Color.d.ts.map +1 -1
  20. package/dist/model/color/Color.js +1 -1
  21. package/dist/model/color/ColorIdentification.js +5 -5
  22. package/dist/model/color/ColorOptions.d.ts.map +1 -1
  23. package/dist/processors/CodeQLProcessor.js +1 -1
  24. package/dist/processors/CommonProcessor.js +1 -1
  25. package/dist/processors/ProcessorFactory.js +1 -1
  26. package/dist/processors/SnykProcessor.js +2 -1
  27. package/dist/representations/CompactGroupByRepresentation.js +1 -1
  28. package/dist/representations/CompactGroupByRunPerLevelRepresentation.js +1 -1
  29. package/dist/representations/CompactGroupByRunPerSeverityRepresentation.js +1 -1
  30. package/dist/representations/CompactGroupByRunRepresentation.js +1 -1
  31. package/dist/representations/CompactGroupBySarifPerLevelRepresentation.js +1 -1
  32. package/dist/representations/CompactGroupBySarifPerSeverityRepresentation.js +1 -1
  33. package/dist/representations/CompactGroupBySarifRepresentation.js +1 -1
  34. package/dist/representations/CompactGroupByToolNamePerLevelRepresentation.js +1 -1
  35. package/dist/representations/CompactGroupByToolNamePerSeverityRepresentation.js +1 -1
  36. package/dist/representations/CompactGroupByToolNameRepresentation.js +1 -1
  37. package/dist/representations/CompactTotalPerLevelRepresentation.js +1 -1
  38. package/dist/representations/CompactTotalPerSeverityRepresentation.js +1 -1
  39. package/dist/representations/CompactTotalRepresentation.js +1 -1
  40. package/dist/representations/Representation.js +1 -1
  41. package/dist/representations/RepresentationFactory.js +1 -1
  42. package/dist/representations/TableGroupByRunPerLevelRepresentation.d.ts.map +1 -1
  43. package/dist/representations/TableGroupByRunPerLevelRepresentation.js +1 -1
  44. package/dist/representations/TableGroupByRunPerSeverityRepresentation.d.ts.map +1 -1
  45. package/dist/representations/TableGroupByRunPerSeverityRepresentation.js +1 -1
  46. package/dist/representations/TableGroupByRunRepresentation.d.ts.map +1 -1
  47. package/dist/representations/TableGroupByRunRepresentation.js +1 -1
  48. package/dist/representations/TableGroupBySarifPerLevelRepresentation.d.ts.map +1 -1
  49. package/dist/representations/TableGroupBySarifPerLevelRepresentation.js +1 -1
  50. package/dist/representations/TableGroupBySarifPerSeverityRepresentation.d.ts.map +1 -1
  51. package/dist/representations/TableGroupBySarifPerSeverityRepresentation.js +1 -1
  52. package/dist/representations/TableGroupBySarifRepresentation.d.ts.map +1 -1
  53. package/dist/representations/TableGroupBySarifRepresentation.js +1 -1
  54. package/dist/representations/TableGroupByToolNamePerLevelRepresentation.d.ts.map +1 -1
  55. package/dist/representations/TableGroupByToolNamePerLevelRepresentation.js +1 -1
  56. package/dist/representations/TableGroupByToolNamePerSeverityRepresentation.d.ts.map +1 -1
  57. package/dist/representations/TableGroupByToolNamePerSeverityRepresentation.js +1 -1
  58. package/dist/representations/TableGroupByToolNameRepresentation.d.ts.map +1 -1
  59. package/dist/representations/TableGroupByToolNameRepresentation.js +1 -1
  60. package/dist/representations/TableGroupRepresentation.d.ts +0 -1
  61. package/dist/representations/TableGroupRepresentation.d.ts.map +1 -1
  62. package/dist/representations/TableGroupRepresentation.js +3 -3
  63. package/dist/representations/table/Cell.d.ts.map +1 -1
  64. package/dist/representations/table/Cell.js +1 -1
  65. package/dist/representations/table/Column.d.ts +0 -1
  66. package/dist/representations/table/Column.d.ts.map +1 -1
  67. package/dist/representations/table/Column.js +4 -3
  68. package/dist/representations/table/Row.d.ts +0 -1
  69. package/dist/representations/table/Row.d.ts.map +1 -1
  70. package/dist/representations/table/Row.js +3 -3
  71. package/dist/representations/table/Table.d.ts.map +1 -1
  72. package/dist/representations/table/Table.js +1 -1
  73. package/dist/system.js +5 -5
  74. package/dist/tsdoc-metadata.json +1 -1
  75. package/dist/types.d.ts +29 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/types.js +11 -1
  78. package/dist/utils/Comparators.js +1 -1
  79. package/dist/utils/ExtendedArray.js +1 -1
  80. package/dist/utils/FileUtils.js +2 -2
  81. package/dist/utils/SarifUtils.js +1 -1
  82. package/dist/utils/StringUtils.js +1 -1
  83. package/etc/sarif-to-slack.api.md +21 -1
  84. package/jest.config.json +4 -4
  85. package/package.json +5 -4
  86. package/src/Logger.ts +50 -34
  87. package/src/SarifToSlackClient.ts +73 -68
  88. package/src/globalState.ts +11 -0
  89. package/src/index.ts +23 -12
  90. package/src/model/Finding.ts +36 -35
  91. package/src/model/FindingArray.ts +5 -5
  92. package/src/model/SendIf.ts +25 -25
  93. package/src/model/SlackMessage.ts +49 -49
  94. package/src/model/color/Color.ts +7 -7
  95. package/src/model/color/ColorIdentification.ts +77 -77
  96. package/src/model/color/ColorOptions.ts +1 -1
  97. package/src/processors/CodeQLProcessor.ts +3 -3
  98. package/src/processors/CommonProcessor.ts +24 -24
  99. package/src/processors/ProcessorFactory.ts +9 -9
  100. package/src/processors/SnykProcessor.ts +3 -2
  101. package/src/representations/CompactGroupByRepresentation.ts +20 -20
  102. package/src/representations/CompactGroupByRunPerLevelRepresentation.ts +2 -2
  103. package/src/representations/CompactGroupByRunPerSeverityRepresentation.ts +2 -2
  104. package/src/representations/CompactGroupByRunRepresentation.ts +10 -10
  105. package/src/representations/CompactGroupBySarifPerLevelRepresentation.ts +2 -2
  106. package/src/representations/CompactGroupBySarifPerSeverityRepresentation.ts +2 -2
  107. package/src/representations/CompactGroupBySarifRepresentation.ts +11 -11
  108. package/src/representations/CompactGroupByToolNamePerLevelRepresentation.ts +2 -2
  109. package/src/representations/CompactGroupByToolNamePerSeverityRepresentation.ts +2 -2
  110. package/src/representations/CompactGroupByToolNameRepresentation.ts +10 -10
  111. package/src/representations/CompactTotalPerLevelRepresentation.ts +2 -2
  112. package/src/representations/CompactTotalPerSeverityRepresentation.ts +2 -2
  113. package/src/representations/CompactTotalRepresentation.ts +5 -5
  114. package/src/representations/Representation.ts +8 -8
  115. package/src/representations/RepresentationFactory.ts +32 -32
  116. package/src/representations/TableGroupByRunPerLevelRepresentation.ts +3 -3
  117. package/src/representations/TableGroupByRunPerSeverityRepresentation.ts +3 -3
  118. package/src/representations/TableGroupByRunRepresentation.ts +5 -5
  119. package/src/representations/TableGroupBySarifPerLevelRepresentation.ts +3 -3
  120. package/src/representations/TableGroupBySarifPerSeverityRepresentation.ts +3 -3
  121. package/src/representations/TableGroupBySarifRepresentation.ts +9 -9
  122. package/src/representations/TableGroupByToolNamePerLevelRepresentation.ts +3 -3
  123. package/src/representations/TableGroupByToolNamePerSeverityRepresentation.ts +3 -3
  124. package/src/representations/TableGroupByToolNameRepresentation.ts +4 -4
  125. package/src/representations/TableGroupRepresentation.ts +32 -32
  126. package/src/representations/table/Cell.ts +8 -8
  127. package/src/representations/table/Column.ts +13 -13
  128. package/src/representations/table/Row.ts +17 -17
  129. package/src/representations/table/Table.ts +21 -21
  130. package/src/system.ts +5 -5
  131. package/src/types.ts +43 -13
  132. package/src/utils/Comparators.ts +6 -6
  133. package/src/utils/ExtendedArray.ts +1 -1
  134. package/src/utils/FileUtils.ts +3 -3
  135. package/src/utils/SarifUtils.ts +6 -6
  136. package/src/utils/StringUtils.ts +3 -3
  137. package/tests/integration/SendSarifToSlack.spec.ts +73 -67
  138. package/tests/representations/CompactGroupByRunPerLevelRepresentation.spec.ts +121 -0
  139. package/tests/representations/CompactGroupByRunPerSeverityRepresentation.spec.ts +122 -0
  140. package/tests/representations/CompactGroupBySarifPerLevelRepresentation.spec.ts +132 -0
  141. package/tests/representations/CompactGroupBySarifPerSeverityRepresentation.spec.ts +133 -0
  142. package/tsconfig.json +1 -1
@@ -1,4 +1,4 @@
1
- import type { Result, Run, ToolComponent } from 'sarif'
1
+ import type { Result, Run, ToolComponent } from 'sarif';
2
2
 
3
3
  /**
4
4
  * Returns {@link ToolComponent} instance for the given {@link Run}. It does not
@@ -7,7 +7,7 @@ import type { Result, Run, ToolComponent } from 'sarif'
7
7
  * @internal
8
8
  */
9
9
  export function findToolComponentDriver(run: Run): ToolComponent {
10
- return run.tool.driver
10
+ return run.tool.driver;
11
11
  }
12
12
 
13
13
  /**
@@ -17,11 +17,11 @@ export function findToolComponentDriver(run: Run): ToolComponent {
17
17
  * @internal
18
18
  */
19
19
  export function tryFindToolComponentExtension(run: Run, result: Result): ToolComponent | undefined {
20
- let tool: ToolComponent | undefined
20
+ let tool: ToolComponent | undefined;
21
21
  if (result.rule?.toolComponent?.index != null) {
22
- tool = run.tool.extensions?.[result.rule.toolComponent.index]
22
+ tool = run.tool.extensions?.[result.rule.toolComponent.index];
23
23
  }
24
- return tool
24
+ return tool;
25
25
  }
26
26
 
27
27
  /**
@@ -31,5 +31,5 @@ export function tryFindToolComponentExtension(run: Run, result: Result): ToolCom
31
31
  * @internal
32
32
  */
33
33
  export function findToolComponent(run: Run, result: Result): ToolComponent {
34
- return tryFindToolComponentExtension(run, result) ?? findToolComponentDriver(run)
34
+ return tryFindToolComponentExtension(run, result) ?? findToolComponentDriver(run);
35
35
  }
@@ -1,7 +1,7 @@
1
1
  export function randomAlphabetic(length: number): string {
2
- const alphabet = 'abcdefghijklmnopqrstuvwxyz'
2
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz';
3
3
  return Array.from(
4
4
  { length },
5
- (): string => alphabet[Math.floor(Math.random() * alphabet.length)]
6
- ).join('')
5
+ (): string => alphabet[Math.floor(Math.random() * alphabet.length)],
6
+ ).join('');
7
7
  }
@@ -1,105 +1,103 @@
1
1
  import { z, ZodSafeParseResult } from 'zod';
2
2
  import {
3
3
  Color,
4
- RepresentationType, SarifFileExtension,
4
+ LogLevel,
5
+ LogLevelItems,
6
+ RepresentationType,
7
+ SarifFileExtensionItems,
5
8
  SarifToSlackClient,
6
- SendIf
9
+ SendIf,
7
10
  } from '../../src';
8
11
 
9
12
  describe('(integration): SendSarifToSlack', (): void => {
10
- function processSarifExtension(extension: string): SarifFileExtension {
11
- const allowed: string[] = ['sarif', 'json']
12
- if (allowed.includes(extension)) {
13
- return extension as SarifFileExtension
14
- }
15
-
16
- throw new Error(`Unknown extension: ${extension}`)
17
- }
18
-
19
13
  function processRepresentationType(representation?: string): RepresentationType | undefined {
20
14
  if (representation == null) {
21
- return undefined
15
+ return undefined;
22
16
  }
23
17
 
24
18
  switch (representation.toLowerCase()) {
25
19
  case 'compact-group-by-run-per-level':
26
- return RepresentationType.CompactGroupByRunPerLevel
20
+ return RepresentationType.CompactGroupByRunPerLevel;
27
21
  case 'compact-group-by-run-per-severity':
28
- return RepresentationType.CompactGroupByRunPerSeverity
22
+ return RepresentationType.CompactGroupByRunPerSeverity;
29
23
  case 'compact-group-by-tool-name-per-level':
30
- return RepresentationType.CompactGroupByToolNamePerLevel
24
+ return RepresentationType.CompactGroupByToolNamePerLevel;
31
25
  case 'compact-group-by-tool-name-per-severity':
32
- return RepresentationType.CompactGroupByToolNamePerSeverity
26
+ return RepresentationType.CompactGroupByToolNamePerSeverity;
33
27
  case 'compact-group-by-sarif-per-level':
34
- return RepresentationType.CompactGroupBySarifPerLevel
28
+ return RepresentationType.CompactGroupBySarifPerLevel;
35
29
  case 'compact-group-by-sarif-per-severity':
36
- return RepresentationType.CompactGroupBySarifPerSeverity
30
+ return RepresentationType.CompactGroupBySarifPerSeverity;
37
31
  case 'compact-total-per-level':
38
- return RepresentationType.CompactTotalPerLevel
32
+ return RepresentationType.CompactTotalPerLevel;
39
33
  case 'compact-total-per-severity':
40
- return RepresentationType.CompactTotalPerSeverity
34
+ return RepresentationType.CompactTotalPerSeverity;
41
35
  case 'table-group-by-run-per-level':
42
- return RepresentationType.TableGroupByRunPerLevel
36
+ return RepresentationType.TableGroupByRunPerLevel;
43
37
  case 'table-group-by-run-per-severity':
44
- return RepresentationType.TableGroupByRunPerSeverity
38
+ return RepresentationType.TableGroupByRunPerSeverity;
45
39
  case 'table-group-by-tool-name-per-level':
46
- return RepresentationType.TableGroupByToolNamePerLevel
40
+ return RepresentationType.TableGroupByToolNamePerLevel;
47
41
  case 'table-group-by-tool-name-per-severity':
48
- return RepresentationType.TableGroupByToolNamePerSeverity
42
+ return RepresentationType.TableGroupByToolNamePerSeverity;
49
43
  case 'table-group-by-sarif-per-level':
50
- return RepresentationType.TableGroupBySarifPerLevel
44
+ return RepresentationType.TableGroupBySarifPerLevel;
51
45
  case 'table-group-by-sarif-per-severity':
52
- return RepresentationType.TableGroupBySarifPerSeverity
46
+ return RepresentationType.TableGroupBySarifPerSeverity;
53
47
  default:
54
- return undefined
48
+ return undefined;
55
49
  }
56
50
  }
57
51
 
58
52
  function processSendIf(sendIf?: string): SendIf | undefined {
59
53
  if (sendIf == null) {
60
- return undefined
54
+ return undefined;
61
55
  }
62
56
 
63
57
  switch (sendIf.toLowerCase()) {
64
- case 'severity-critical': return SendIf.SeverityCritical
65
- case 'severity-high': return SendIf.SeverityHigh
66
- case 'severity-high-or-higher': return SendIf.SeverityHighOrHigher
67
- case 'severity-medium': return SendIf.SeverityMedium
68
- case 'severity-medium-or-higher': return SendIf.SeverityMediumOrHigher
69
- case 'severity-low': return SendIf.SeverityLow
70
- case 'severity-low-or-higher': return SendIf.SeverityLowOrHigher
71
- case 'severity-none': return SendIf.SeverityNone
72
- case 'severity-none-or-higher': return SendIf.SeverityNoneOrHigher
73
- case 'severity-unknown': return SendIf.SeverityUnknown
74
- case 'severity-unknown-or-higher': return SendIf.SeverityUnknownOrHigher
75
- case 'level-error': return SendIf.LevelError
76
- case 'level-warning': return SendIf.LevelWarning
77
- case 'level-warning-or-higher': return SendIf.LevelWarningOrHigher
78
- case 'level-note': return SendIf.LevelNote
79
- case 'level-note-or-higher': return SendIf.LevelNoteOrHigher
80
- case 'level-none': return SendIf.LevelNone
81
- case 'level-none-or-higher': return SendIf.LevelNoneOrHigher
82
- case 'level-unknown': return SendIf.LevelUnknown
83
- case 'level-unknown-or-higher': return SendIf.LevelUnknownOrHigher
84
- case 'always': return SendIf.Always
85
- case 'some': return SendIf.Some
86
- case 'empty': return SendIf.Empty
87
- case 'never': return SendIf.Never
88
- default: return undefined
58
+ case 'severity-critical': return SendIf.SeverityCritical;
59
+ case 'severity-high': return SendIf.SeverityHigh;
60
+ case 'severity-high-or-higher': return SendIf.SeverityHighOrHigher;
61
+ case 'severity-medium': return SendIf.SeverityMedium;
62
+ case 'severity-medium-or-higher': return SendIf.SeverityMediumOrHigher;
63
+ case 'severity-low': return SendIf.SeverityLow;
64
+ case 'severity-low-or-higher': return SendIf.SeverityLowOrHigher;
65
+ case 'severity-none': return SendIf.SeverityNone;
66
+ case 'severity-none-or-higher': return SendIf.SeverityNoneOrHigher;
67
+ case 'severity-unknown': return SendIf.SeverityUnknown;
68
+ case 'severity-unknown-or-higher': return SendIf.SeverityUnknownOrHigher;
69
+ case 'level-error': return SendIf.LevelError;
70
+ case 'level-warning': return SendIf.LevelWarning;
71
+ case 'level-warning-or-higher': return SendIf.LevelWarningOrHigher;
72
+ case 'level-note': return SendIf.LevelNote;
73
+ case 'level-note-or-higher': return SendIf.LevelNoteOrHigher;
74
+ case 'level-none': return SendIf.LevelNone;
75
+ case 'level-none-or-higher': return SendIf.LevelNoneOrHigher;
76
+ case 'level-unknown': return SendIf.LevelUnknown;
77
+ case 'level-unknown-or-higher': return SendIf.LevelUnknownOrHigher;
78
+ case 'always': return SendIf.Always;
79
+ case 'some': return SendIf.Some;
80
+ case 'empty': return SendIf.Empty;
81
+ case 'never': return SendIf.Never;
82
+ default: return undefined;
89
83
  }
90
84
  }
91
85
 
92
86
  test('should send Sarif to Slack', async () => {
93
- const recursiveParseResult: ZodSafeParseResult<boolean> = z
94
- .stringbool()
95
- .safeParse(process.env.SARIF_TO_SLACK_SARIF_PATH_RECURSIVE);
96
- const sarifExtensionParseResult: ZodSafeParseResult<SarifFileExtension> = z
97
- .string()
98
- .transform(processSarifExtension)
99
- .safeParse(process.env.SARIF_TO_SLACK_SARIF_FILE_EXTENSION);
100
- const includeRunParseResult: ZodSafeParseResult<boolean> = z
101
- .stringbool()
102
- .safeParse(process.env.SARIF_TO_SLACK_INCLUDE_RUN);
87
+ const parseBoolean = <T>(envVar: string | undefined, defaultVal: T): boolean | T => {
88
+ const parseResult: ZodSafeParseResult<boolean> = z
89
+ .string()
90
+ .transform((val: string): string => val.toLowerCase())
91
+ .refine((val: string): val is "true" | "false" => val === "true" || val === "false")
92
+ .transform((val: "true" | "false"): val is "true" => val === "true")
93
+ .safeParse(envVar);
94
+ return parseResult.success ? parseResult.data : defaultVal;
95
+ }
96
+
97
+ const logLevelParseResult: ZodSafeParseResult<LogLevel> =
98
+ z.enum(LogLevelItems).safeParse(process.env.SARIF_TO_SLACK_LOG_LEVEL);
99
+ const logFunctionNameOnPositionParseResult: ZodSafeParseResult<number> =
100
+ z.coerce.number().safeParse(process.env.SARIF_TO_SLACK_LOG_FUNCTION_NAME_ON_POSITION);
103
101
 
104
102
  const client: SarifToSlackClient = await SarifToSlackClient.create(
105
103
  process.env.SARIF_TO_SLACK_WEBHOOK_URL as string,
@@ -127,8 +125,8 @@ describe('(integration): SendSarifToSlack', (): void => {
127
125
  },
128
126
  sarif: {
129
127
  path: process.env.SARIF_TO_SLACK_SARIF_PATH as string,
130
- recursive: recursiveParseResult.success ? recursiveParseResult.data : false,
131
- extension: sarifExtensionParseResult.success ? sarifExtensionParseResult.data : 'sarif',
128
+ recursive: parseBoolean(process.env.SARIF_TO_SLACK_SARIF_PATH_RECURSIVE, false),
129
+ extension: z.enum(SarifFileExtensionItems).parse(process.env.SARIF_TO_SLACK_SARIF_FILE_EXTENSION),
132
130
  },
133
131
  header: {
134
132
  include: process.env.SARIF_TO_SLACK_HEADER !== 'skip',
@@ -143,11 +141,19 @@ describe('(integration): SendSarifToSlack', (): void => {
143
141
  value: process.env.SARIF_TO_SLACK_ACTOR,
144
142
  },
145
143
  run: {
146
- include: includeRunParseResult.success ? includeRunParseResult.data : false,
144
+ include: parseBoolean(process.env.SARIF_TO_SLACK_INCLUDE_RUN, false),
147
145
  },
148
146
  representation: processRepresentationType(process.env.SARIF_TO_SLACK_REPRESENTATION),
149
147
  sendIf: processSendIf(process.env.SARIF_TO_SLACK_SEND_IF),
150
- }
148
+ loggerOptions: {
149
+ logFunctionName: parseBoolean(process.env.SARIF_TO_SLACK_LOG_FUNCTION_NAME, undefined),
150
+ logFunctionNameOnPosition: logFunctionNameOnPositionParseResult.success ? logFunctionNameOnPositionParseResult.data : undefined,
151
+ minLevel: logLevelParseResult.success ? logLevelParseResult.data : undefined,
152
+ name: 'integration-test',
153
+ stylePrettyLogs: parseBoolean(process.env.SARIF_TO_SLACK_STYLE_PRETTY_LOGS, undefined),
154
+ prettyLogTemplate: process.env.SARIF_TO_SLACK_LOG_TEMPLATE,
155
+ },
156
+ },
151
157
  );
152
158
  await client.send();
153
159
  })
@@ -0,0 +1,121 @@
1
+ import CompactGroupByRunPerLevelRepresentation from '../../src/representations/CompactGroupByRunPerLevelRepresentation';
2
+ import FindingArray from '../../src/model/FindingArray';
3
+ import type Finding from '../../src/model/Finding';
4
+ import { SecurityLevel, SecuritySeverity } from '../../src/types';
5
+ import type { RunData, SarifModel } from '../../src/types';
6
+
7
+ function mockFinding(opts: {
8
+ sarifPath?: string,
9
+ runId?: number,
10
+ toolName?: string,
11
+ level?: SecurityLevel,
12
+ severity?: SecuritySeverity,
13
+ }): Finding {
14
+ const finding: Finding = {
15
+ get sarifPath() { return opts.sarifPath ?? '/default.sarif' },
16
+ get runId() { return opts.runId ?? 1 },
17
+ get toolName() { return opts.toolName ?? 'Tool' },
18
+ get cvssScore() { return undefined },
19
+ get level() { return opts.level ?? SecurityLevel.Unknown },
20
+ get severity() { return opts.severity ?? SecuritySeverity.Unknown },
21
+ clone() { return mockFinding(opts) },
22
+ };
23
+ return finding;
24
+ }
25
+
26
+ function buildModel(
27
+ runs: Array<{ id: number; toolName: string }>,
28
+ findings: Finding[],
29
+ ): SarifModel {
30
+ const arr = new FindingArray();
31
+ findings.forEach(f => arr.push(f));
32
+ return {
33
+ sarifFiles: [],
34
+ runs: runs.map(r => ({ id: r.id, toolName: r.toolName, run: {} as RunData['run'] })),
35
+ findings: arr,
36
+ };
37
+ }
38
+
39
+ describe('(unit): CompactGroupByRunPerLevelRepresentation', (): void => {
40
+ describe('compose()', (): void => {
41
+ test('should return "No vulnerabilities found" when there are no runs', (): void => {
42
+ const repr = new CompactGroupByRunPerLevelRepresentation(buildModel([], []));
43
+ expect(repr.compose()).toBe('No vulnerabilities found');
44
+ })
45
+
46
+ test('should return group header with "No vulnerabilities found" when run has no findings', (): void => {
47
+ const repr = new CompactGroupByRunPerLevelRepresentation(
48
+ buildModel([{ id: 1, toolName: 'Grype' }], []),
49
+ );
50
+ expect(repr.compose()).toBe('_[Run 1]_ *Grype*\nNo vulnerabilities found');
51
+ })
52
+
53
+ test('should compose single finding with correct level label', (): void => {
54
+ const repr = new CompactGroupByRunPerLevelRepresentation(
55
+ buildModel(
56
+ [{ id: 1, toolName: 'Grype' }],
57
+ [mockFinding({ runId: 1, level: SecurityLevel.Error })],
58
+ ),
59
+ );
60
+ expect(repr.compose()).toBe('_[Run 1]_ *Grype*\n*Error*: 1');
61
+ })
62
+
63
+ test('should group and sort findings by level descending', (): void => {
64
+ const findings = [
65
+ mockFinding({ runId: 1, level: SecurityLevel.Note }),
66
+ mockFinding({ runId: 1, level: SecurityLevel.Error }),
67
+ mockFinding({ runId: 1, level: SecurityLevel.Warning }),
68
+ mockFinding({ runId: 1, level: SecurityLevel.Warning }),
69
+ ];
70
+ const repr = new CompactGroupByRunPerLevelRepresentation(
71
+ buildModel([{ id: 1, toolName: 'Trivy' }], findings),
72
+ );
73
+ expect(repr.compose()).toBe('_[Run 1]_ *Trivy*\n*Error*: 1, *Warning*: 2, *Note*: 1');
74
+ })
75
+
76
+ test('should compose multiple runs each with their own findings', (): void => {
77
+ const findings = [
78
+ mockFinding({ runId: 1, level: SecurityLevel.Error }),
79
+ mockFinding({ runId: 2, level: SecurityLevel.Warning }),
80
+ ];
81
+ const repr = new CompactGroupByRunPerLevelRepresentation(
82
+ buildModel(
83
+ [{ id: 1, toolName: 'Grype' }, { id: 2, toolName: 'Trivy' }],
84
+ findings,
85
+ ),
86
+ );
87
+ expect(repr.compose()).toBe(
88
+ '_[Run 1]_ *Grype*\n*Error*: 1\n\n_[Run 2]_ *Trivy*\n*Warning*: 1',
89
+ )
90
+ })
91
+
92
+ test('should show "No vulnerabilities found" for a run that has no matching findings', (): void => {
93
+ const findings = [mockFinding({ runId: 1, level: SecurityLevel.Error })];
94
+ const repr = new CompactGroupByRunPerLevelRepresentation(
95
+ buildModel(
96
+ [{ id: 1, toolName: 'Grype' }, { id: 2, toolName: 'Trivy' }],
97
+ findings,
98
+ ),
99
+ );
100
+ expect(repr.compose()).toBe(
101
+ '_[Run 1]_ *Grype*\n*Error*: 1\n\n_[Run 2]_ *Trivy*\nNo vulnerabilities found',
102
+ )
103
+ })
104
+
105
+ test('should handle all level variants correctly', (): void => {
106
+ const findings = [
107
+ mockFinding({ runId: 1, level: SecurityLevel.Error }),
108
+ mockFinding({ runId: 1, level: SecurityLevel.Warning }),
109
+ mockFinding({ runId: 1, level: SecurityLevel.Note }),
110
+ mockFinding({ runId: 1, level: SecurityLevel.None }),
111
+ mockFinding({ runId: 1, level: SecurityLevel.Unknown }),
112
+ ];
113
+ const repr = new CompactGroupByRunPerLevelRepresentation(
114
+ buildModel([{ id: 1, toolName: 'Scanner' }], findings),
115
+ );
116
+ expect(repr.compose()).toBe(
117
+ '_[Run 1]_ *Scanner*\n*Error*: 1, *Warning*: 1, *Note*: 1, *None*: 1, *Unknown*: 1',
118
+ );
119
+ })
120
+ })
121
+ })
@@ -0,0 +1,122 @@
1
+ import CompactGroupByRunPerSeverityRepresentation from '../../src/representations/CompactGroupByRunPerSeverityRepresentation';
2
+ import FindingArray from '../../src/model/FindingArray';
3
+ import type Finding from '../../src/model/Finding';
4
+ import { SecurityLevel, SecuritySeverity } from '../../src/types';
5
+ import type { RunData, SarifModel } from '../../src/types';
6
+
7
+ function mockFinding(opts: {
8
+ sarifPath?: string,
9
+ runId?: number,
10
+ toolName?: string,
11
+ level?: SecurityLevel,
12
+ severity?: SecuritySeverity,
13
+ }): Finding {
14
+ const finding: Finding = {
15
+ get sarifPath() { return opts.sarifPath ?? '/default.sarif' },
16
+ get runId() { return opts.runId ?? 1 },
17
+ get toolName() { return opts.toolName ?? 'Tool' },
18
+ get cvssScore() { return undefined },
19
+ get level() { return opts.level ?? SecurityLevel.Unknown },
20
+ get severity() { return opts.severity ?? SecuritySeverity.Unknown },
21
+ clone() { return mockFinding(opts) },
22
+ };
23
+ return finding;
24
+ }
25
+
26
+ function buildModel(
27
+ runs: Array<{ id: number; toolName: string }>,
28
+ findings: Finding[],
29
+ ): SarifModel {
30
+ const arr = new FindingArray();
31
+ findings.forEach(f => arr.push(f));
32
+ return {
33
+ sarifFiles: [],
34
+ runs: runs.map(r => ({ id: r.id, toolName: r.toolName, run: {} as RunData['run'] })),
35
+ findings: arr,
36
+ };
37
+ }
38
+
39
+ describe('(unit): CompactGroupByRunPerSeverityRepresentation', (): void => {
40
+ describe('compose()', (): void => {
41
+ test('should return "No vulnerabilities found" when there are no runs', (): void => {
42
+ const repr = new CompactGroupByRunPerSeverityRepresentation(buildModel([], []));
43
+ expect(repr.compose()).toBe('No vulnerabilities found');
44
+ })
45
+
46
+ test('should return group header with "No vulnerabilities found" when run has no findings', (): void => {
47
+ const repr = new CompactGroupByRunPerSeverityRepresentation(
48
+ buildModel([{ id: 1, toolName: 'Grype' }], []),
49
+ );
50
+ expect(repr.compose()).toBe('_[Run 1]_ *Grype*\nNo vulnerabilities found');
51
+ })
52
+
53
+ test('should compose single finding with correct severity label', (): void => {
54
+ const repr = new CompactGroupByRunPerSeverityRepresentation(
55
+ buildModel(
56
+ [{ id: 1, toolName: 'Grype' }],
57
+ [mockFinding({ runId: 1, severity: SecuritySeverity.Critical })],
58
+ ),
59
+ );
60
+ expect(repr.compose()).toBe('_[Run 1]_ *Grype*\n*Critical*: 1');
61
+ })
62
+
63
+ test('should group and sort findings by severity descending', (): void => {
64
+ const findings = [
65
+ mockFinding({ runId: 1, severity: SecuritySeverity.Low }),
66
+ mockFinding({ runId: 1, severity: SecuritySeverity.Critical }),
67
+ mockFinding({ runId: 1, severity: SecuritySeverity.High }),
68
+ mockFinding({ runId: 1, severity: SecuritySeverity.High }),
69
+ ];
70
+ const repr = new CompactGroupByRunPerSeverityRepresentation(
71
+ buildModel([{ id: 1, toolName: 'Trivy' }], findings),
72
+ );
73
+ expect(repr.compose()).toBe('_[Run 1]_ *Trivy*\n*Critical*: 1, *High*: 2, *Low*: 1');
74
+ })
75
+
76
+ test('should compose multiple runs each with their own findings', (): void => {
77
+ const findings = [
78
+ mockFinding({ runId: 1, severity: SecuritySeverity.High }),
79
+ mockFinding({ runId: 2, severity: SecuritySeverity.Medium }),
80
+ ];
81
+ const repr = new CompactGroupByRunPerSeverityRepresentation(
82
+ buildModel(
83
+ [{ id: 1, toolName: 'Grype' }, { id: 2, toolName: 'Trivy' }],
84
+ findings,
85
+ ),
86
+ );
87
+ expect(repr.compose()).toBe(
88
+ '_[Run 1]_ *Grype*\n*High*: 1\n\n_[Run 2]_ *Trivy*\n*Medium*: 1',
89
+ );
90
+ })
91
+
92
+ test('should show "No vulnerabilities found" for a run that has no matching findings', (): void => {
93
+ const findings = [mockFinding({ runId: 1, severity: SecuritySeverity.Critical })];
94
+ const repr = new CompactGroupByRunPerSeverityRepresentation(
95
+ buildModel(
96
+ [{ id: 1, toolName: 'Grype' }, { id: 2, toolName: 'Trivy' }],
97
+ findings,
98
+ ),
99
+ );
100
+ expect(repr.compose()).toBe(
101
+ '_[Run 1]_ *Grype*\n*Critical*: 1\n\n_[Run 2]_ *Trivy*\nNo vulnerabilities found',
102
+ );
103
+ })
104
+
105
+ test('should handle all severity variants correctly', (): void => {
106
+ const findings = [
107
+ mockFinding({ runId: 1, severity: SecuritySeverity.Critical }),
108
+ mockFinding({ runId: 1, severity: SecuritySeverity.High }),
109
+ mockFinding({ runId: 1, severity: SecuritySeverity.Medium }),
110
+ mockFinding({ runId: 1, severity: SecuritySeverity.Low }),
111
+ mockFinding({ runId: 1, severity: SecuritySeverity.None }),
112
+ mockFinding({ runId: 1, severity: SecuritySeverity.Unknown }),
113
+ ];
114
+ const repr = new CompactGroupByRunPerSeverityRepresentation(
115
+ buildModel([{ id: 1, toolName: 'Scanner' }], findings),
116
+ );
117
+ expect(repr.compose()).toBe(
118
+ '_[Run 1]_ *Scanner*\n*Critical*: 1, *High*: 1, *Medium*: 1, *Low*: 1, *None*: 1, *Unknown*: 1',
119
+ );
120
+ })
121
+ })
122
+ })
@@ -0,0 +1,132 @@
1
+ import CompactGroupBySarifPerLevelRepresentation from '../../src/representations/CompactGroupBySarifPerLevelRepresentation';
2
+ import FindingArray from '../../src/model/FindingArray';
3
+ import type Finding from '../../src/model/Finding';
4
+ import { SecurityLevel, SecuritySeverity } from '../../src/types';
5
+ import type { RunData, SarifModel } from '../../src/types';
6
+
7
+ function mockFinding(opts: {
8
+ sarifPath?: string,
9
+ runId?: number,
10
+ toolName?: string,
11
+ level?: SecurityLevel,
12
+ severity?: SecuritySeverity,
13
+ }): Finding {
14
+ const finding: Finding = {
15
+ get sarifPath() { return opts.sarifPath ?? '/default.sarif' },
16
+ get runId() { return opts.runId ?? 1 },
17
+ get toolName() { return opts.toolName ?? 'Tool' },
18
+ get cvssScore() { return undefined },
19
+ get level() { return opts.level ?? SecurityLevel.Unknown },
20
+ get severity() { return opts.severity ?? SecuritySeverity.Unknown },
21
+ clone() { return mockFinding(opts) },
22
+ };
23
+ return finding;
24
+ }
25
+
26
+ function buildModel(
27
+ sarifFiles: string[],
28
+ findings: Finding[],
29
+ ): SarifModel {
30
+ const arr = new FindingArray();
31
+ findings.forEach(f => arr.push(f));
32
+ return {
33
+ sarifFiles,
34
+ runs: [{ id: 1, toolName: 'Tool', run: {} as RunData['run'] }],
35
+ findings: arr,
36
+ };
37
+ }
38
+
39
+ describe('(unit): CompactGroupBySarifPerLevelRepresentation', (): void => {
40
+ describe('compose()', (): void => {
41
+ test('should return "No vulnerabilities found" when there are no sarif files', (): void => {
42
+ const repr = new CompactGroupBySarifPerLevelRepresentation(buildModel([], []));
43
+ expect(repr.compose()).toBe('No vulnerabilities found');
44
+ })
45
+
46
+ test('should return group header with "No vulnerabilities found" when file has no findings', (): void => {
47
+ const repr = new CompactGroupBySarifPerLevelRepresentation(
48
+ buildModel(['/path/to/results.sarif'], []),
49
+ );
50
+ expect(repr.compose()).toBe('_[File 1]_ *results.sarif*\nNo vulnerabilities found');
51
+ })
52
+
53
+ test('should compose single finding with correct level label', (): void => {
54
+ const repr = new CompactGroupBySarifPerLevelRepresentation(
55
+ buildModel(
56
+ ['/path/to/grype.sarif'],
57
+ [mockFinding({ sarifPath: '/path/to/grype.sarif', level: SecurityLevel.Error })],
58
+ ),
59
+ );
60
+ expect(repr.compose()).toBe('_[File 1]_ *grype.sarif*\n*Error*: 1');
61
+ })
62
+
63
+ test('should group and sort findings by level descending', (): void => {
64
+ const sarifPath = '/scans/results.sarif';
65
+ const findings = [
66
+ mockFinding({ sarifPath, level: SecurityLevel.Note }),
67
+ mockFinding({ sarifPath, level: SecurityLevel.Error }),
68
+ mockFinding({ sarifPath, level: SecurityLevel.Warning }),
69
+ mockFinding({ sarifPath, level: SecurityLevel.Warning }),
70
+ ];
71
+ const repr = new CompactGroupBySarifPerLevelRepresentation(
72
+ buildModel([sarifPath], findings),
73
+ );
74
+ expect(repr.compose()).toBe('_[File 1]_ *results.sarif*\n*Error*: 1, *Warning*: 2, *Note*: 1');
75
+ })
76
+
77
+ test('should compose multiple sarif files with incrementing indices', (): void => {
78
+ const file1 = '/scans/grype-01.sarif';
79
+ const file2 = '/scans/grype-02.sarif';
80
+ const findings = [
81
+ mockFinding({ sarifPath: file1, level: SecurityLevel.Error }),
82
+ mockFinding({ sarifPath: file2, level: SecurityLevel.Warning }),
83
+ ];
84
+ const repr = new CompactGroupBySarifPerLevelRepresentation(
85
+ buildModel([file1, file2], findings),
86
+ );
87
+ expect(repr.compose()).toBe(
88
+ '_[File 1]_ *grype-01.sarif*\n*Error*: 1\n\n_[File 2]_ *grype-02.sarif*\n*Warning*: 1',
89
+ );
90
+ })
91
+
92
+ test('should show "No vulnerabilities found" for a file that has no matching findings', (): void => {
93
+ const file1 = '/scans/grype-01.sarif';
94
+ const file2 = '/scans/grype-02.sarif';
95
+ const findings = [mockFinding({ sarifPath: file1, level: SecurityLevel.Error })];
96
+ const repr = new CompactGroupBySarifPerLevelRepresentation(
97
+ buildModel([file1, file2], findings),
98
+ );
99
+ expect(repr.compose()).toBe(
100
+ '_[File 1]_ *grype-01.sarif*\n*Error*: 1\n\n_[File 2]_ *grype-02.sarif*\nNo vulnerabilities found',
101
+ );
102
+ })
103
+
104
+ test('should use only basename for the group title', (): void => {
105
+ const sarifPath = '/very/long/path/to/nested/scan-results.sarif';
106
+ const repr = new CompactGroupBySarifPerLevelRepresentation(
107
+ buildModel(
108
+ [sarifPath],
109
+ [mockFinding({ sarifPath, level: SecurityLevel.Note })],
110
+ ),
111
+ );
112
+ expect(repr.compose()).toBe('_[File 1]_ *scan-results.sarif*\n*Note*: 1');
113
+ })
114
+
115
+ test('should handle all level variants correctly', (): void => {
116
+ const sarifPath = '/scans/all-levels.sarif';
117
+ const findings = [
118
+ mockFinding({ sarifPath, level: SecurityLevel.Error }),
119
+ mockFinding({ sarifPath, level: SecurityLevel.Warning }),
120
+ mockFinding({ sarifPath, level: SecurityLevel.Note }),
121
+ mockFinding({ sarifPath, level: SecurityLevel.None }),
122
+ mockFinding({ sarifPath, level: SecurityLevel.Unknown }),
123
+ ];
124
+ const repr = new CompactGroupBySarifPerLevelRepresentation(
125
+ buildModel([sarifPath], findings),
126
+ );
127
+ expect(repr.compose()).toBe(
128
+ '_[File 1]_ *all-levels.sarif*\n*Error*: 1, *Warning*: 1, *Note*: 1, *None*: 1, *Unknown*: 1',
129
+ );
130
+ })
131
+ })
132
+ })