Skip to content

Commit 2f5e91b

Browse files
authored
feat: add support for showing suppressed issues (#175)
1 parent 01eba7d commit 2f5e91b

File tree

8 files changed

+334
-2
lines changed

8 files changed

+334
-2
lines changed

trivy-task/trivyV1/task.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"author": "Aqua Security",
1111
"version": {
1212
"Major": 1,
13-
"Minor": 15,
13+
"Minor": 16,
1414
"Patch": 0
1515
},
1616
"instanceNameFormat": "Echo trivy $(version)",

trivy-task/trivyV2/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function configureScan(runner: ToolRunner, inputs: TaskInputs) {
8181
runner.arg('--list-all-pkgs');
8282
runner.argIf(inputs.severities, ['--severity', inputs.severities]);
8383
runner.argIf(inputs.ignoreUnfixed, ['--ignore-unfixed']);
84+
runner.argIf(inputs.showSuppressed, ['--show-suppressed']);
8485
runner.arg(['--output', resultsFilePath]);
8586
runner.arg(inputs.options);
8687
runner.arg(inputs.target);

trivy-task/trivyV2/inputs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type TaskInputs = {
1313
templates?: string;
1414
publish: boolean;
1515
ignoreUnfixed: boolean;
16+
showSuppressed: boolean;
1617
ignoreScanErrors: boolean;
1718
hasAquaAccount: boolean;
1819
aquaKey?: string;
@@ -42,6 +43,7 @@ export function getTaskInputs(): TaskInputs {
4243
.map((s) => s.trim())
4344
.join(','),
4445
ignoreUnfixed: task.getBoolInput('ignoreUnfixed', false),
46+
showSuppressed: task.getBoolInput('showSuppressed', false),
4547
ignoreScanErrors: task.getBoolInput('ignoreScanErrors', false),
4648
reports: task.getDelimitedInput('reports', ',').map((s) => s.trim()),
4749
publish: task.getBoolInput('publish', false),

trivy-task/trivyV2/task.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"author": "Aqua Security",
1111
"version": {
1212
"Major": 2,
13-
"Minor": 3,
13+
"Minor": 4,
1414
"Patch": 0
1515
},
1616
"instanceNameFormat": "Echo trivy $(version)",
@@ -153,6 +153,15 @@
153153
"required": false,
154154
"helpMarkDown": "Include only fixed vulnerabilities."
155155
},
156+
{
157+
"groupName": "scanInput",
158+
"name": "showSuppressed",
159+
"type": "boolean",
160+
"label": "Show Suppressed",
161+
"defaultValue": false,
162+
"required": false,
163+
"helpMarkDown": "Include any issues that have been ignored through config."
164+
},
156165
{
157166
"groupName": "scanInput",
158167
"name": "ignoreScanErrors",

ui/src/BaseReport.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {
55
countReportLicenses,
66
countReportMisconfigurations,
77
countReportSecrets,
8+
countReportSuppressed,
89
countReportVulnerabilities,
910
Report,
1011
} from './trivy';
1112
import { SecretsTable } from './SecretsTable';
1213
import { VulnerabilitiesTable } from './VulnerabilitiesTable';
1314
import { MisconfigurationsTable } from './MisconfigurationsTable';
1415
import { LicensesTable } from './LicenseTable';
16+
import { SuppressedTable } from './SuppressedTable';
1517
import { Tab, TabBar, TabSize } from 'azure-devops-ui/Tabs';
1618
import { AssuranceTable } from './AssuranceTable';
1719

@@ -47,6 +49,7 @@ export class BaseReport extends React.Component<
4749
const misconfigCount = countReportMisconfigurations(this.props.report);
4850
const secretsCount = countReportSecrets(this.props.report);
4951
const licensesCount = countReportLicenses(this.props.report);
52+
const suppressedCount = countReportSuppressed(this.props.report);
5053
const assuranceCount = countAssuranceIssues(this.props.assurance);
5154

5255
return (
@@ -89,6 +92,14 @@ export class BaseReport extends React.Component<
8992
badgeCount={licensesCount}
9093
/>
9194
)}
95+
{suppressedCount > 0 && (
96+
<Tab
97+
id="suppressed"
98+
name="Suppressed"
99+
key="Suppressed"
100+
badgeCount={suppressedCount}
101+
/>
102+
)}
92103
{assuranceCount > 0 && (
93104
<Tab
94105
id="assurance"
@@ -132,6 +143,14 @@ export class BaseReport extends React.Component<
132143
/>
133144
</div>
134145
)}
146+
{this.state.selectedTabId === 'suppressed' && (
147+
<div className="flex-grow">
148+
<SuppressedTable
149+
key={this.props.report.DisplayName}
150+
report={this.props.report}
151+
/>
152+
</div>
153+
)}
135154
{this.state.selectedTabId === 'assurance' && (
136155
<div className="flex-grow">
137156
<AssuranceTable

ui/src/ReportStats.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
countReportLicenses,
55
countReportMisconfigurations,
66
countReportSecrets,
7+
countReportSuppressed,
78
countReportVulnerabilities,
89
Report,
910
} from './trivy';
@@ -52,6 +53,10 @@ export class ReportStats extends React.Component<ReportStatsProps> {
5253
name: 'Licenses',
5354
value: countReportLicenses(this.props.report),
5455
},
56+
{
57+
name: 'Suppressed',
58+
value: countReportSuppressed(this.props.report),
59+
},
5560
];
5661
return (
5762
<Card className="flex-grow">

ui/src/SuppressedTable.tsx

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import * as React from 'react';
2+
import {
3+
ObservableArray,
4+
ObservableValue,
5+
} from 'azure-devops-ui/Core/Observable';
6+
import {
7+
ColumnSorting,
8+
ISimpleTableCell,
9+
renderSimpleCell,
10+
sortItems,
11+
SortOrder,
12+
Table,
13+
TableColumnLayout,
14+
} from 'azure-devops-ui/Table';
15+
import {
16+
Report,
17+
Result,
18+
Severity,
19+
ExperimentalModifiedFindings,
20+
} from './trivy';
21+
import { ISimpleListCell } from 'azure-devops-ui/List';
22+
import { ZeroData } from 'azure-devops-ui/ZeroData';
23+
import { compareSeverity, renderSeverity } from './severity';
24+
import { ITableColumn } from 'azure-devops-ui/Components/Table/Table.Props';
25+
import { ArrayItemProvider } from 'azure-devops-ui/Utilities/Provider';
26+
27+
interface SuppressedTableProps {
28+
report: Report;
29+
}
30+
31+
interface ListSuppressed extends ISimpleTableCell {
32+
Type: ISimpleTableCell;
33+
ID: ISimpleTableCell;
34+
Status: ISimpleTableCell;
35+
Statement: ISimpleTableCell;
36+
Source: ISimpleTableCell;
37+
Title: ISimpleTableCell;
38+
Severity: ISimpleListCell;
39+
FilePath: ISimpleListCell;
40+
}
41+
42+
function renderSuppressedSeverity(
43+
rowIndex: number,
44+
columnIndex: number,
45+
tableColumn: ITableColumn<ListSuppressed>,
46+
tableItem: ListSuppressed
47+
): JSX.Element {
48+
return renderSeverity(
49+
rowIndex,
50+
columnIndex,
51+
tableColumn,
52+
tableItem.Severity.text as Severity
53+
);
54+
}
55+
56+
const fixedColumns = [
57+
{
58+
columnLayout: TableColumnLayout.singleLine,
59+
id: 'ID',
60+
name: 'ID',
61+
readonly: true,
62+
renderCell: renderSimpleCell,
63+
width: 150,
64+
},
65+
{
66+
columnLayout: TableColumnLayout.singleLine,
67+
id: 'Type',
68+
name: 'Type',
69+
readonly: true,
70+
renderCell: renderSimpleCell,
71+
width: 150,
72+
sortProps: {
73+
ariaLabelAscending: 'Sorted A to Z',
74+
ariaLabelDescending: 'Sorted Z to A',
75+
},
76+
},
77+
{
78+
columnLayout: TableColumnLayout.singleLine,
79+
id: 'Status',
80+
name: 'Status',
81+
readonly: true,
82+
renderCell: renderSimpleCell,
83+
width: 100,
84+
},
85+
{
86+
columnLayout: TableColumnLayout.singleLine,
87+
id: 'Statement',
88+
name: 'Statement',
89+
readonly: true,
90+
renderCell: renderSimpleCell,
91+
width: new ObservableValue(-20),
92+
},
93+
{
94+
columnLayout: TableColumnLayout.singleLine,
95+
id: 'Source',
96+
name: 'Source',
97+
readonly: true,
98+
renderCell: renderSimpleCell,
99+
width: new ObservableValue(-8),
100+
},
101+
{
102+
columnLayout: TableColumnLayout.singleLine,
103+
id: 'Title',
104+
name: 'Title',
105+
readonly: true,
106+
renderCell: renderSimpleCell,
107+
width: new ObservableValue(-20),
108+
},
109+
{
110+
columnLayout: TableColumnLayout.singleLine,
111+
id: 'Severity',
112+
name: 'Severity',
113+
readonly: true,
114+
renderCell: renderSuppressedSeverity,
115+
width: 120,
116+
sortProps: {
117+
ariaLabelAscending: 'Sorted by severity ascending',
118+
ariaLabelDescending: 'Sorted by severity descending',
119+
},
120+
},
121+
{
122+
columnLayout: TableColumnLayout.singleLine,
123+
id: 'FilePath',
124+
name: 'Location',
125+
readonly: true,
126+
renderCell: renderSimpleCell,
127+
width: new ObservableValue(-35),
128+
sortProps: {
129+
ariaLabelAscending: 'Sorted A to Z',
130+
ariaLabelDescending: 'Sorted Z to A',
131+
},
132+
},
133+
];
134+
135+
const sortFunctions = [
136+
(item1: ListSuppressed, item2: ListSuppressed): number => {
137+
const severity1: ISimpleListCell = item1.Severity;
138+
const severity2: ISimpleListCell = item2.Severity;
139+
return compareSeverity(severity1.text, severity2.text);
140+
},
141+
(item1: ListSuppressed, item2: ListSuppressed): number => {
142+
const value1: ISimpleListCell = item1.Type;
143+
const value2: ISimpleListCell = item2.Type;
144+
return value1.text?.localeCompare(value2.text ?? '') || 0;
145+
},
146+
null,
147+
(item1: ListSuppressed, item2: ListSuppressed): number => {
148+
const value1: ISimpleListCell = item1.FilePath;
149+
const value2: ISimpleListCell = item2.FilePath;
150+
return value1.text?.localeCompare(value2.text ?? '') || 0;
151+
},
152+
null,
153+
];
154+
155+
export class SuppressedTable extends React.Component<SuppressedTableProps> {
156+
private readonly results: ObservableArray<ListSuppressed> =
157+
new ObservableArray<ListSuppressed>([]);
158+
159+
constructor(props: SuppressedTableProps) {
160+
super(props);
161+
this.results = new ObservableArray<ListSuppressed>(
162+
convertSuppressed(props.report.Results || [])
163+
);
164+
// sort by severity desc by default
165+
this.results.splice(
166+
0,
167+
this.results.length,
168+
...sortItems<ListSuppressed>(
169+
0,
170+
SortOrder.descending,
171+
sortFunctions,
172+
fixedColumns,
173+
this.results.value
174+
)
175+
);
176+
}
177+
178+
render() {
179+
const sortingBehavior = new ColumnSorting<ListSuppressed>(
180+
(columnIndex: number, proposedSortOrder: SortOrder) => {
181+
this.results.splice(
182+
0,
183+
this.results.length,
184+
...sortItems<ListSuppressed>(
185+
columnIndex,
186+
proposedSortOrder,
187+
sortFunctions,
188+
fixedColumns,
189+
this.results.value
190+
)
191+
);
192+
}
193+
);
194+
195+
return this.results.length == 0 ? (
196+
<ZeroData
197+
primaryText="No problems found."
198+
secondaryText={
199+
<span>No suppressions were found for this scan target.</span>
200+
}
201+
imageAltText="trivy"
202+
imagePath={'images/trivy.png'}
203+
/>
204+
) : (
205+
<Table
206+
pageSize={this.results.length}
207+
selectableText={true}
208+
ariaLabel="Suppressed Table"
209+
role="table"
210+
behaviors={[sortingBehavior]}
211+
columns={fixedColumns}
212+
itemProvider={new ArrayItemProvider(this.results.value)}
213+
containerClassName="h-scroll-auto"
214+
/>
215+
);
216+
}
217+
}
218+
219+
function convertSuppressed(results: Result[]): ListSuppressed[] {
220+
const output: ListSuppressed[] = [];
221+
results.forEach((result) => {
222+
if (
223+
Object.prototype.hasOwnProperty.call(
224+
result,
225+
'ExperimentalModifiedFindings'
226+
) &&
227+
result.ExperimentalModifiedFindings !== null
228+
) {
229+
const target = result.Target;
230+
result.ExperimentalModifiedFindings.forEach(function (
231+
suppressed: ExperimentalModifiedFindings
232+
) {
233+
let id = '';
234+
if (suppressed.Finding.ID) {
235+
id = suppressed.Finding.ID;
236+
} else if (suppressed.Finding.VulnerabilityID) {
237+
id = suppressed.Finding.VulnerabilityID;
238+
}
239+
240+
output.push({
241+
ID: { text: id },
242+
Type: {
243+
text: suppressed.Type.split(' ')
244+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
245+
.join(' '),
246+
},
247+
Severity: { text: suppressed.Finding.Severity },
248+
Status: {
249+
text: suppressed.Status.split(' ')
250+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
251+
.join(' '),
252+
},
253+
Statement: { text: suppressed.Statement },
254+
Source: { text: suppressed.Source },
255+
Title: { text: suppressed.Finding.Title },
256+
FilePath: { text: target },
257+
});
258+
});
259+
}
260+
});
261+
return output;
262+
}

0 commit comments

Comments
 (0)