Commit b2c4afb9 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '210327-show-vulnerability-report-type' into 'master'

Add scanner column to the project-level security dashboard and pipeline security tab

See merge request gitlab-org/gitlab!35150
parents 3dbea317 3fe2890d
......@@ -80,6 +80,9 @@ export default {
<div class="table-section section-15" role="rowheader">
{{ s__('Reports|Identifier') }}
</div>
<div class="table-section section-15" role="rowheader">
{{ s__('Reports|Scanner') }}
</div>
<div class="table-section section-20" role="rowheader"></div>
</div>
......
......@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import VulnerabilityActionButtons from './vulnerability_action_buttons.vue';
import VulnerabilityIssueLink from './vulnerability_issue_link.vue';
import { DASHBOARD_TYPES } from '../store/constants';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
export default {
......@@ -66,6 +67,9 @@ export default {
isSelected() {
return Boolean(this.selectedVulnerabilities[this.vulnerability.id]);
},
useConvertReportType() {
return convertReportType(this.vulnerability.report_type);
},
},
methods: {
...mapActions('vulnerabilities', ['openModal', 'selectVulnerability', 'deselectVulnerability']),
......@@ -146,6 +150,13 @@ export default {
</div>
</div>
<div class="table-section section-15">
<div class="table-mobile-header" role="rowheader">{{ s__('Reports|Scanner') }}</div>
<div class="table-mobile-content text-capitalize">
{{ useConvertReportType }}
</div>
</div>
<div class="table-section section-20">
<div class="table-mobile-header" role="rowheader">{{ s__('Reports|Actions') }}</div>
<div class="table-mobile-content action-buttons d-flex justify-content-end">
......
......@@ -29,7 +29,7 @@ export const initFirstClassVulnerabilityFilters = projects => {
selection: new Set([ALL]),
},
{
name: s__('SecurityReports|Report type'),
name: s__('Reports|Scanner'),
id: 'reportType',
options: [BASE_FILTERS.report_type, ...parseOptions(REPORT_TYPES)],
selection: new Set([ALL]),
......
......@@ -8,7 +8,7 @@ export const BASE_FILTERS = {
id: ALL,
},
report_type: {
name: s__('ciReport|All report types'),
name: s__('ciReport|All scanner types'),
id: ALL,
},
project_id: {
......
......@@ -14,7 +14,7 @@ export default () => ({
selection: new Set([BASE_FILTERS.severity.id]),
},
{
name: s__('SecurityReports|Report type'),
name: s__('SecurityReports|Scanner type'),
id: 'report_type',
options: [BASE_FILTERS.report_type, ...optionsObjectToArray(REPORT_TYPES)],
hidden: false,
......
import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
import { humanize } from '~/lib/utils/text_utility';
/**
* Takes the report type, that is not human-readable and converts it to be human-readable
* @param {string} reportType that is not human-readable
* @returns {string} a human-readable version of the report type
*/
const convertReportType = reportType => {
if (!reportType) return '';
const lowerCaseType = reportType.toLowerCase();
return REPORT_TYPES[lowerCaseType] || humanize(lowerCaseType);
};
export default convertReportType;
......@@ -109,6 +109,7 @@ export default {
:empty-state-svg-path="emptyStateSvgPath"
:filters="filters"
:vulnerabilities="vulnerabilities"
should-show-report-type
@refetch-vulnerabilities="refetchVulnerabilities"
>
<template #emptyState>
......
......@@ -6,6 +6,7 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import IssueLink from './issue_link.vue';
import { VULNERABILITIES_PER_PAGE } from '../constants';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
export default {
name: 'VulnerabilityList',
......@@ -34,6 +35,11 @@ export default {
required: false,
default: null,
},
shouldShowReportType: {
type: Boolean,
required: false,
default: false,
},
shouldShowSelection: {
type: Boolean,
required: false,
......@@ -69,20 +75,12 @@ export default {
shouldShowSelectionSummary() {
return this.shouldShowSelection && Boolean(this.numOfSelectedVulnerabilities);
},
checkboxClass() {
return this.shouldShowSelection ? '' : 'gl-display-none';
},
theadClass() {
return this.shouldShowSelectionSummary ? 'below-selection-summary' : '';
},
fields() {
const commonThClass = ['table-th-transparent', 'original-gl-th', 'gl-bg-white!'].join(' ');
return [
{
key: 'checkbox',
class: this.checkboxClass,
thClass: `gl-w-9 ${commonThClass}`,
},
const baseFields = [
{
key: 'state',
label: s__('Vulnerability|Status'),
......@@ -100,6 +98,22 @@ export default {
tdClass: 'gl-word-break-all',
},
];
if (this.shouldShowSelection) {
baseFields.unshift({
key: 'checkbox',
thClass: `gl-w-9 ${commonThClass}`,
});
}
if (this.shouldShowReportType) {
baseFields.push({
key: 'reportType',
label: s__('Reports|Scanner'),
thClass: commonThClass,
});
}
return baseFields;
},
},
watch: {
......@@ -151,6 +165,9 @@ export default {
issue(item) {
return item.issueLinks?.nodes[0]?.issue;
},
useConvertReportType(reportType) {
return convertReportType(reportType);
},
},
VULNERABILITIES_PER_PAGE,
};
......@@ -176,6 +193,7 @@ export default {
<template #head(checkbox)>
<gl-form-checkbox
class="mr-0 mb-0"
data-testid="vulnerability-checkbox-all"
:checked="hasSelectedAllVulnerabilities"
@change="toggleAllVulnerabilities"
/>
......@@ -184,6 +202,7 @@ export default {
<template #cell(checkbox)="{ item }">
<gl-form-checkbox
class="d-inline-block mr-0 mb-0"
data-testid="vulnerability-checkbox"
:checked="isSelected(item)"
@change="toggleVulnerability(item)"
/>
......@@ -224,6 +243,12 @@ export default {
<remediated-badge v-if="item.resolved_on_default_branch" class="ml-2" />
</template>
<template #cell(reportType)="{ item }">
<span data-testid="vulnerability-report-type" class="text-capitalize">{{
useConvertReportType(item.reportType)
}}</span>
</template>
<template #table-busy>
<gl-skeleton-loading
v-for="n in $options.VULNERABILITIES_PER_PAGE"
......
......@@ -31,4 +31,5 @@ fragment Vulnerability on Vulnerability {
project {
nameWithNamespace
}
reportType
}
---
title: Add scanner type to vulnerability row
merge_request: 35150
author:
type: changed
......@@ -52,6 +52,7 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath,
filters: null,
isLoading: true,
shouldShowReportType: false,
shouldShowSelection: true,
shouldShowProjectNamespace: true,
vulnerabilities: [],
......@@ -143,6 +144,7 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath,
filters: null,
isLoading: false,
shouldShowReportType: false,
shouldShowSelection: true,
shouldShowProjectNamespace: true,
vulnerabilities,
......
......@@ -77,6 +77,7 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath,
filters: null,
isLoading: true,
shouldShowReportType: false,
shouldShowSelection: true,
shouldShowProjectNamespace: true,
vulnerabilities: [],
......@@ -159,6 +160,7 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath,
filters: null,
isLoading: false,
shouldShowReportType: false,
shouldShowSelection: true,
shouldShowProjectNamespace: true,
vulnerabilities,
......
......@@ -81,6 +81,14 @@ describe('Security Dashboard Table Row', () => {
expect(findContent(2).text()).toContain(vulnerability.identifiers[0].name);
});
it('should render the report type', () => {
expect(
findContent(3)
.text()
.toLowerCase(),
).toContain(vulnerability.report_type.toLowerCase());
});
describe('the project name', () => {
it('should render the name', () => {
expect(findContent(1).text()).toContain(vulnerability.name);
......
......@@ -5,6 +5,7 @@ import {
countVulnerabilities,
groupedReportText,
} from 'ee/vue_shared/security_reports/store/utils';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import getFileLocation from 'ee/vue_shared/security_reports/store/utils/get_file_location';
import {
......@@ -44,6 +45,24 @@ describe('security reports utils', () => {
});
});
describe('convertReportType', () => {
it.each`
reportType | output
${'sast'} | ${'SAST'}
${'dependency_scanning'} | ${'Dependency Scanning'}
${'CONTAINER_SCANNING'} | ${'Container Scanning'}
${'CUSTOM_SCANNER'} | ${'Custom scanner'}
${'mast'} | ${'Mast'}
${'TAST'} | ${'Tast'}
${undefined} | ${''}
`(
'converts the report type "$reportType" to the human-readable string "$output"',
({ reportType, output }) => {
expect(convertReportType(reportType)).toEqual(output);
},
);
});
describe('filterByKey', () => {
it('filters the array with the provided key', () => {
const array1 = [{ id: '1234' }, { id: 'abg543' }, { id: '214swfA' }];
......
export const generateVulnerabilities = () => [
{
id: 'id_0',
title: 'Vulnerability 1',
title: 'Vulnerability 0',
severity: 'critical',
state: 'dismissed',
reportType: 'SAST',
location: {
image:
'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff',
......@@ -14,9 +15,10 @@ export const generateVulnerabilities = () => [
},
{
id: 'id_1',
title: 'Vulnerability 2',
title: 'Vulnerability 1',
severity: 'high',
state: 'opened',
reportType: 'DEPENDENCY_SCANNING',
location: {
file: 'src/main/java/com/gitlab/security_products/tests/App.java',
},
......@@ -24,6 +26,31 @@ export const generateVulnerabilities = () => [
nameWithNamespace: 'Administrator / Vulnerability reports',
},
},
{
id: 'id_2',
title: 'Vulnerability 2',
severity: 'high',
state: 'opened',
reportType: 'CUSTOM_SCANNER_WITHOUT_TRANSLATION',
location: {
file: 'yarn.lock',
},
project: {
nameWithNamespace: 'Mixed Vulnerabilities / Dependency List Test 01',
},
},
{
id: 'id_3',
title: 'Vulnerability 3',
severity: 'high',
state: 'opened',
location: {
file: 'yarn.lock',
},
project: {
nameWithNamespace: 'Mixed Vulnerabilities / Dependency List Test 01',
},
},
];
export const vulnerabilities = generateVulnerabilities();
......@@ -31,9 +31,8 @@ describe('Vulnerability list component', () => {
const findCell = label => wrapper.find(`.js-${label}`);
const findRow = (index = 0) => wrapper.findAll('tbody tr').at(index);
const findSelectionSummary = () => wrapper.find(SelectionSummary);
const findCheckAllCheckboxCell = () => wrapper.find('thead tr th');
const findFirstCheckboxCell = () => wrapper.find('tbody tr td');
const findLocation = id => wrapper.find(`[data-testid="location-${id}"]`);
const findDataCell = label => wrapper.find(`[data-testid="${label}"]`);
const findDataCells = label => wrapper.findAll(`[data-testid="${label}"]`);
afterEach(() => {
wrapper.destroy();
......@@ -74,9 +73,7 @@ describe('Vulnerability list component', () => {
});
it('should show the selection summary when a checkbox is selected', () => {
findFirstCheckboxCell()
.find('input')
.setChecked(true);
findDataCell('vulnerability-checkbox').setChecked(true);
return wrapper.vm.$nextTick().then(() => {
expect(findSelectionSummary().exists()).toBe(true);
......@@ -84,9 +81,7 @@ describe('Vulnerability list component', () => {
});
it('should sync selected vulnerabilities when the vulnerability list is updated', () => {
findFirstCheckboxCell()
.find('input')
.setChecked(true);
findDataCell('vulnerability-checkbox').setChecked(true);
expect(findSelectionSummary().props('selectedVulnerabilities')).toHaveLength(1);
wrapper.setProps({ vulnerabilities: [] });
......@@ -101,38 +96,44 @@ describe('Vulnerability list component', () => {
wrapper = createWrapper({
props: { vulnerabilities, shouldShowSelection: false },
});
findFirstCheckboxCell()
.find('input')
.setChecked(true);
});
it('should not show the checkboxes if shouldShowSelection is passed in', () => {
expect(findCheckAllCheckboxCell().classes()).toContain('gl-display-none');
expect(findFirstCheckboxCell().classes()).toContain('gl-display-none');
expect(findDataCell('vulnerability-checkbox-all').exists()).toBe(false);
expect(findDataCell('vulnerability-checkbox').exists()).toBe(false);
});
});
describe('when displayed on instance or group level dashboard', () => {
it('should display the vulnerability locations', () => {
const newVulnerabilities = generateVulnerabilities();
let newVulnerabilities;
beforeEach(() => {
newVulnerabilities = generateVulnerabilities();
wrapper = createWrapper({
props: { vulnerabilities: newVulnerabilities, shouldShowProjectNamespace: true },
});
expect(findLocation(newVulnerabilities[0].id).text()).toContain(
});
it('should display the vulnerability locations', () => {
expect(findDataCell(`location-${newVulnerabilities[0].id}`).text()).toContain(
'Administrator / Security reports',
);
expect(findLocation(newVulnerabilities[0].id).text()).toContain(
expect(findDataCell(`location-${newVulnerabilities[0].id}`).text()).toContain(
'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff',
);
expect(findLocation(newVulnerabilities[1].id).text()).toContain(
expect(findDataCell(`location-${newVulnerabilities[1].id}`).text()).toContain(
'Administrator / Vulnerability reports',
);
expect(findLocation(newVulnerabilities[1].id).text()).toContain(
expect(findDataCell(`location-${newVulnerabilities[1].id}`).text()).toContain(
'src/main/java/com/gitlab/security_products/tests/App.java',
);
});
it('should not display the vulnerability report type', () => {
const scannerCell = findRow().find('[data-testid="vulnerability-report-type"');
expect(scannerCell.exists()).toBe(false);
});
it('should not display the vulnerability locations', () => {
const vulnerabilityWithoutLocation = [
{
......@@ -149,10 +150,12 @@ describe('Vulnerability list component', () => {
wrapper = createWrapper({
props: { vulnerabilities: vulnerabilityWithoutLocation, shouldShowProjectNamespace: true },
});
expect(findLocation(vulnerabilityWithoutLocation[0].id).text()).toContain(
expect(findDataCell(`location-${vulnerabilityWithoutLocation[0].id}`).text()).toContain(
'Administrator / Security reports',
);
expect(findLocation(vulnerabilityWithoutLocation[0].id).findAll('div').length).toBe(2);
expect(
findDataCell(`location-${vulnerabilityWithoutLocation[0].id}`).findAll('div').length,
).toBe(2);
});
});
......@@ -161,24 +164,32 @@ describe('Vulnerability list component', () => {
beforeEach(() => {
newVulnerabilities = generateVulnerabilities();
wrapper = createWrapper({
props: { vulnerabilities: newVulnerabilities },
props: { vulnerabilities: newVulnerabilities, shouldShowReportType: true },
});
});
it('should not display the vulnerability locations', () => {
expect(findLocation(newVulnerabilities[0].id).text()).not.toContain(
expect(findDataCell(`location-${newVulnerabilities[0].id}`).text()).not.toContain(
'Administrator / Security reports',
);
expect(findLocation(newVulnerabilities[0].id).text()).toContain(
expect(findDataCell(`location-${newVulnerabilities[0].id}`).text()).toContain(
'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff',
);
expect(findLocation(newVulnerabilities[1].id).text()).not.toContain(
expect(findDataCell(`location-${newVulnerabilities[1].id}`).text()).not.toContain(
'Administrator / Vulnerability reports',
);
expect(findLocation(newVulnerabilities[1].id).text()).toContain(
expect(findDataCell(`location-${newVulnerabilities[1].id}`).text()).toContain(
'src/main/java/com/gitlab/security_products/tests/App.java',
);
});
it('should display the vulnerability report type', () => {
const cells = findDataCells('vulnerability-report-type');
expect(cells.at(0).text()).toBe('SAST');
expect(cells.at(1).text()).toBe('Dependency Scanning');
expect(cells.at(2).text()).toBe('Custom scanner without translation');
expect(cells.at(3).text()).toBe('');
});
});
describe('when has an issue associated', () => {
......
......@@ -19150,6 +19150,9 @@ msgstr ""
msgid "Reports|Metrics reports failed loading results"
msgstr ""
msgid "Reports|Scanner"
msgstr ""
msgid "Reports|Severity"
msgstr ""
......@@ -20115,15 +20118,15 @@ msgstr ""
msgid "SecurityReports|Remove project from dashboard"
msgstr ""
msgid "SecurityReports|Report type"
msgstr ""
msgid "SecurityReports|Return to dashboard"
msgstr ""
msgid "SecurityReports|Scan details"
msgstr ""
msgid "SecurityReports|Scanner type"
msgstr ""
msgid "SecurityReports|Security Dashboard"
msgstr ""
......@@ -26702,7 +26705,7 @@ msgstr ""
msgid "ciReport|All projects"
msgstr ""
msgid "ciReport|All report types"
msgid "ciReport|All scanner types"
msgstr ""
msgid "ciReport|All severities"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment