Commit fbc099e2 authored by Mark Florian's avatar Mark Florian

Merge branch '300753-mount-graphql-vulnerabilities-for-pipeline' into 'master'

Mount vulnerability list on pipeline dashboard

See merge request gitlab-org/gitlab!61536
parents 5b3df8b5 bbd0ef54
<script>
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { produce } from 'immer';
import findingsQuery from '../graphql/queries/pipeline_findings.query.graphql';
import { preparePageInfo } from '../helpers';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';
import VulnerabilityList from './vulnerability_list.vue';
export default {
name: 'PipelineFindings',
components: {
GlAlert,
GlIntersectionObserver,
GlLoadingIcon,
VulnerabilityList,
},
inject: ['pipeline', 'projectFullPath'],
props: {
filters: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
pageInfo: {},
findings: [],
errorLoadingFindings: false,
sortBy: 'severity',
sortDirection: 'desc',
};
},
computed: {
isLoadingQuery() {
return this.$apollo.queries.findings.loading;
},
isLoadingFirstResult() {
return this.isLoadingQuery && this.findings.length === 0;
},
sort() {
return `${this.sortBy}_${this.sortDirection}`;
},
},
apollo: {
findings: {
query: findingsQuery,
variables() {
return {
pipelineId: this.pipeline.iid,
fullPath: this.projectFullPath,
first: VULNERABILITIES_PER_PAGE,
};
},
update: ({ project }) =>
project?.pipeline?.securityReportFindings?.nodes?.map((finding) => ({
...finding,
// vulnerabilties and findings are different but similar entities. Vulnerabilities have
// ids, findings have uuid. To make the selection work with the vulnerability list, we're
// going to massage the data and add an `id` field to the finding.
id: finding.uuid,
})),
result({ data }) {
this.pageInfo = preparePageInfo(data.project?.pipeline?.securityReportFindings?.pageInfo);
},
error() {
this.errorLoadingFindings = true;
},
skip() {
return !this.filters;
},
},
},
watch: {
filters() {
// Clear out the existing vulnerabilities so that the skeleton loader is shown.
this.findings = [];
this.pageInfo = {};
},
sort() {
// Clear out the existing vulnerabilities so that the skeleton loader is shown.
this.findings = [];
},
},
methods: {
onErrorDismiss() {
this.errorLoadingFindings = false;
},
fetchNextPage() {
if (this.pageInfo.hasNextPage) {
this.$apollo.queries.findings.fetchMore({
variables: { after: this.pageInfo.endCursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
return produce(fetchMoreResult, (draftData) => {
draftData.project.pipeline.securityReportFindings.nodes = [
...previousResult.project.pipeline.securityReportFindings.nodes,
...draftData.project.pipeline.securityReportFindings.nodes,
];
});
},
});
}
},
handleSortChange({ sortBy, sortDesc }) {
this.sortDirection = sortDesc ? 'desc' : 'asc';
this.sortBy = sortBy;
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="errorLoadingFindings"
class="gl-mb-6"
variant="danger"
@dismiss="onErrorDismiss"
>
{{
s__(
'SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again.',
)
}}
</gl-alert>
<vulnerability-list
v-else
:filters="filters"
:is-loading="isLoadingFirstResult"
:vulnerabilities="findings"
@sort-changed="handleSortChange"
/>
<gl-intersection-observer
v-if="pageInfo.hasNextPage"
class="gl-text-center"
@appear="fetchNextPage"
>
<gl-loading-icon v-if="isLoadingQuery" size="md" />
</gl-intersection-observer>
</div>
</template>
...@@ -20,7 +20,7 @@ import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants'; ...@@ -20,7 +20,7 @@ import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { VULNERABILITIES_PER_PAGE } from '../store/constants'; import { VULNERABILITIES_PER_PAGE, DASHBOARD_TYPES } from '../store/constants';
import IssuesBadge from './issues_badge.vue'; import IssuesBadge from './issues_badge.vue';
import SelectionSummary from './selection_summary.vue'; import SelectionSummary from './selection_summary.vue';
...@@ -51,6 +51,7 @@ export default { ...@@ -51,6 +51,7 @@ export default {
hasJiraVulnerabilitiesIntegrationEnabled: { hasJiraVulnerabilitiesIntegrationEnabled: {
default: false, default: false,
}, },
dashboardType: {},
}, },
props: { props: {
...@@ -87,27 +88,22 @@ export default { ...@@ -87,27 +88,22 @@ export default {
}; };
}, },
computed: { computed: {
// This is a workaround to remove vulnerabilities from the list when their state has changed
// through the bulk update feature, but no longer matches the filters. For more details:
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43468#note_420050017
filteredVulnerabilities() {
return this.vulnerabilities.filter((x) =>
this.filters.state?.length ? this.filters.state.includes(x.state) : true,
);
},
isSortable() { isSortable() {
return Boolean(this.$listeners['sort-changed']); return Boolean(this.$listeners['sort-changed']);
}, },
isPipelineDashboard() {
return this.dashboardType === DASHBOARD_TYPES.PIPELINE;
},
hasAnyScannersOtherThanGitLab() { hasAnyScannersOtherThanGitLab() {
return this.filteredVulnerabilities.some( return this.vulnerabilities.some(
(v) => v.scanner?.vendor !== 'GitLab' && v.scanner?.vendor !== '', (v) => v.scanner?.vendor !== 'GitLab' && v.scanner?.vendor !== '',
); );
}, },
hasSelectedAllVulnerabilities() { hasSelectedAllVulnerabilities() {
if (!this.filteredVulnerabilities.length) { if (!this.vulnerabilities.length) {
return false; return false;
} }
return this.numOfSelectedVulnerabilities === this.filteredVulnerabilities.length; return this.numOfSelectedVulnerabilities === this.vulnerabilities.length;
}, },
numOfSelectedVulnerabilities() { numOfSelectedVulnerabilities() {
return Object.keys(this.selectedVulnerabilities).length; return Object.keys(this.selectedVulnerabilities).length;
...@@ -120,11 +116,17 @@ export default { ...@@ -120,11 +116,17 @@ export default {
}, },
fields() { fields() {
const baseFields = [ const baseFields = [
{
key: 'checkbox',
class: 'checkbox',
skip: !this.shouldShowSelection,
},
{ {
key: 'detected', key: 'detected',
label: s__('Vulnerability|Detected'), label: s__('Vulnerability|Detected'),
class: 'detected', class: 'detected',
sortable: this.isSortable, sortable: this.isSortable,
skip: this.isPipelineDashboard,
}, },
{ {
key: 'state', key: 'state',
...@@ -160,15 +162,9 @@ export default { ...@@ -160,15 +162,9 @@ export default {
label: s__('Vulnerability|Activity'), label: s__('Vulnerability|Activity'),
thClass: 'gl-text-right', thClass: 'gl-text-right',
class: 'activity', class: 'activity',
skip: this.isPipelineDashboard,
}, },
]; ].filter((f) => !f.skip);
if (this.shouldShowSelection) {
baseFields.unshift({
key: 'checkbox',
class: 'checkbox',
});
}
// Apply gl-bg-white! to every header. // Apply gl-bg-white! to every header.
baseFields.forEach((field) => { baseFields.forEach((field) => {
...@@ -182,8 +178,8 @@ export default { ...@@ -182,8 +178,8 @@ export default {
filters() { filters() {
this.selectedVulnerabilities = {}; this.selectedVulnerabilities = {};
}, },
filteredVulnerabilities() { vulnerabilities() {
const ids = new Set(this.filteredVulnerabilities.map((v) => v.id)); const ids = new Set(this.vulnerabilities.map((v) => v.id));
Object.keys(this.selectedVulnerabilities).forEach((vulnerabilityId) => { Object.keys(this.selectedVulnerabilities).forEach((vulnerabilityId) => {
if (!ids.has(vulnerabilityId)) { if (!ids.has(vulnerabilityId)) {
...@@ -314,7 +310,7 @@ export default { ...@@ -314,7 +310,7 @@ export default {
v-if="filters" v-if="filters"
:busy="isLoading" :busy="isLoading"
:fields="fields" :fields="fields"
:items="filteredVulnerabilities" :items="vulnerabilities"
:thead-class="theadClass" :thead-class="theadClass"
:sort-desc="sortDesc" :sort-desc="sortDesc"
:sort-by="sortBy" :sort-by="sortBy"
...@@ -326,7 +322,7 @@ export default { ...@@ -326,7 +322,7 @@ export default {
responsive responsive
hover hover
primary-key="id" primary-key="id"
:tbody-tr-class="{ 'gl-cursor-pointer': filteredVulnerabilities.length }" :tbody-tr-class="{ 'gl-cursor-pointer': vulnerabilities.length }"
@sort-changed="handleSortChange" @sort-changed="handleSortChange"
@row-clicked="toggleVulnerability" @row-clicked="toggleVulnerability"
> >
...@@ -369,10 +365,10 @@ export default { ...@@ -369,10 +365,10 @@ export default {
<gl-link <gl-link
class="gl-text-body vulnerability-title js-description" class="gl-text-body vulnerability-title js-description"
:href="item.vulnerabilityPath" :href="item.vulnerabilityPath"
:data-qa-vulnerability-description="item.title" :data-qa-vulnerability-description="item.title || item.name"
data-qa-selector="vulnerability" data-qa-selector="vulnerability"
> >
{{ item.title }} {{ item.title || item.name }}
</gl-link> </gl-link>
<vulnerability-comment-icon v-if="hasComments(item)" :vulnerability="item" /> <vulnerability-comment-icon v-if="hasComments(item)" :vulnerability="item" />
</div> </div>
...@@ -415,7 +411,7 @@ export default { ...@@ -415,7 +411,7 @@ export default {
{{ useConvertReportType(item.reportType) }} {{ useConvertReportType(item.reportType) }}
</div> </div>
<div <div
v-if="hasAnyScannersOtherThanGitLab" v-if="hasAnyScannersOtherThanGitLab && item.scanner"
data-testid="vulnerability-vendor" data-testid="vulnerability-vendor"
class="gl-text-gray-300" class="gl-text-gray-300"
> >
......
...@@ -14,10 +14,11 @@ import CsvExportButton from './csv_export_button.vue'; ...@@ -14,10 +14,11 @@ import CsvExportButton from './csv_export_button.vue';
import DashboardNotConfiguredGroup from './empty_states/group_dashboard_not_configured.vue'; import DashboardNotConfiguredGroup from './empty_states/group_dashboard_not_configured.vue';
import DashboardNotConfiguredInstance from './empty_states/instance_dashboard_not_configured.vue'; import DashboardNotConfiguredInstance from './empty_states/instance_dashboard_not_configured.vue';
import DashboardNotConfiguredProject from './empty_states/reports_not_configured.vue'; import DashboardNotConfiguredProject from './empty_states/reports_not_configured.vue';
import GroupSecurityVulnerabilities from './first_class_group_security_dashboard_vulnerabilities.vue'; import GroupVulnerabilities from './first_class_group_security_dashboard_vulnerabilities.vue';
import InstanceSecurityVulnerabilities from './first_class_instance_security_dashboard_vulnerabilities.vue'; import InstanceVulnerabilities from './first_class_instance_security_dashboard_vulnerabilities.vue';
import PipelineFindings from './pipeline_findings.vue';
import ProjectPipelineStatus from './project_pipeline_status.vue'; import ProjectPipelineStatus from './project_pipeline_status.vue';
import ProjectSecurityVulnerabilities from './project_vulnerabilities.vue'; import ProjectVulnerabilities from './project_vulnerabilities.vue';
import SurveyRequestBanner from './survey_request_banner.vue'; import SurveyRequestBanner from './survey_request_banner.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue'; import VulnerabilitiesCountList from './vulnerability_count_list.vue';
...@@ -25,9 +26,10 @@ export default { ...@@ -25,9 +26,10 @@ export default {
components: { components: {
AutoFixUserCallout, AutoFixUserCallout,
SecurityDashboardLayout, SecurityDashboardLayout,
GroupSecurityVulnerabilities, GroupVulnerabilities,
InstanceSecurityVulnerabilities, InstanceVulnerabilities,
ProjectSecurityVulnerabilities, ProjectVulnerabilities,
PipelineFindings,
Filters, Filters,
CsvExportButton, CsvExportButton,
SurveyRequestBanner, SurveyRequestBanner,
...@@ -105,7 +107,7 @@ export default { ...@@ -105,7 +107,7 @@ export default {
return !this.isPipeline; return !this.isPipeline;
}, },
isDashboardConfigured() { isDashboardConfigured() {
return this.isProject return this.isProject || this.isPipeline
? Boolean(this.pipeline?.id) ? Boolean(this.pipeline?.id)
: this.projects.length > 0 && this.projectsWereFetched; : this.projects.length > 0 && this.projectsWereFetched;
}, },
...@@ -144,7 +146,7 @@ export default { ...@@ -144,7 +146,7 @@ export default {
@close="handleAutoFixUserCalloutClose" @close="handleAutoFixUserCalloutClose"
/> />
<security-dashboard-layout> <security-dashboard-layout>
<template #header> <template v-if="!isPipeline" #header>
<survey-request-banner class="gl-mt-5" /> <survey-request-banner class="gl-mt-5" />
<header class="gl-my-6 gl-display-flex gl-align-items-center"> <header class="gl-my-6 gl-display-flex gl-align-items-center">
<h2 class="gl-flex-grow-1 gl-my-0"> <h2 class="gl-flex-grow-1 gl-my-0">
...@@ -158,9 +160,10 @@ export default { ...@@ -158,9 +160,10 @@ export default {
<template #sticky> <template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" /> <filters :projects="projects" @filterChange="handleFilterChange" />
</template> </template>
<group-security-vulnerabilities v-if="isGroup" :filters="filters" /> <group-vulnerabilities v-if="isGroup" :filters="filters" />
<instance-security-vulnerabilities v-else-if="isInstance" :filters="filters" /> <instance-vulnerabilities v-else-if="isInstance" :filters="filters" />
<project-security-vulnerabilities v-else-if="isProject" :filters="filters" /> <project-vulnerabilities v-else-if="isProject" :filters="filters" />
<pipeline-findings v-else-if="isPipeline" :filters="filters" />
</security-dashboard-layout> </security-dashboard-layout>
</template> </template>
</div> </div>
......
#import "./vulnerability_location.fragment.graphql"
fragment Vulnerability on Vulnerability { fragment Vulnerability on Vulnerability {
id id
title title
...@@ -23,26 +25,7 @@ fragment Vulnerability on Vulnerability { ...@@ -23,26 +25,7 @@ fragment Vulnerability on Vulnerability {
name name
} }
location { location {
... on VulnerabilityLocationContainerScanning { ...VulnerabilityLocation
image
}
... on VulnerabilityLocationDependencyScanning {
blobPath
file
}
... on VulnerabilityLocationSast {
blobPath
file
startLine
}
... on VulnerabilityLocationSecretDetection {
blobPath
file
startLine
}
... on VulnerabilityLocationDast {
path
}
} }
project { project {
nameWithNamespace nameWithNamespace
......
fragment VulnerabilityLocation on VulnerabilityLocation {
... on VulnerabilityLocationContainerScanning {
image
}
... on VulnerabilityLocationDependencyScanning {
blobPath
file
}
... on VulnerabilityLocationSast {
blobPath
file
startLine
}
... on VulnerabilityLocationSecretDetection {
blobPath
file
startLine
}
... on VulnerabilityLocationDast {
path
}
}
#import "~/graphql_shared/fragments/pageInfoCursorsOnly.fragment.graphql"
#import "../fragments/vulnerability_location.fragment.graphql"
query pipelineFindings(
$fullPath: ID!
$pipelineId: ID!
$first: Int
$after: String
$severity: [String!]
$reportType: [String!]
$scanner: [String!]
) {
project(fullPath: $fullPath) {
pipeline(iid: $pipelineId) {
securityReportFindings(
after: $after
first: $first
severity: $severity
reportType: $reportType
scanner: $scanner
) {
nodes {
uuid
name
description
confidence
identifiers {
externalType
name
}
scanner {
vendor
}
severity
location {
...VulnerabilityLocation
}
}
pageInfo {
...PageInfo
}
}
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import PipelineSecurityDashboard from './components/pipeline_security_dashboard.vue'; import PipelineSecurityDashboard from './components/pipeline_security_dashboard.vue';
import apolloProvider from './graphql/provider'; import apolloProvider from './graphql/provider';
import createRouter from './router';
import createDashboardStore from './store'; import createDashboardStore from './store';
import { DASHBOARD_TYPES } from './store/constants'; import { DASHBOARD_TYPES } from './store/constants';
import { LOADING_VULNERABILITIES_ERROR_CODES } from './store/modules/vulnerabilities/constants'; import { LOADING_VULNERABILITIES_ERROR_CODES } from './store/modules/vulnerabilities/constants';
...@@ -31,8 +32,11 @@ export default () => { ...@@ -31,8 +32,11 @@ export default () => {
[LOADING_VULNERABILITIES_ERROR_CODES.FORBIDDEN]: emptyStateForbiddenSvgPath, [LOADING_VULNERABILITIES_ERROR_CODES.FORBIDDEN]: emptyStateForbiddenSvgPath,
}; };
const router = createRouter();
return new Vue({ return new Vue({
el, el,
router,
apolloProvider, apolloProvider,
store: createDashboardStore({ store: createDashboardStore({
dashboardType: DASHBOARD_TYPES.PIPELINE, dashboardType: DASHBOARD_TYPES.PIPELINE,
......
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import PipelineFindings from 'ee/security_dashboard/components/pipeline_findings.vue';
import VulnerabilityList from 'ee/security_dashboard/components/vulnerability_list.vue';
import pipelineFindingsQuery from 'ee/security_dashboard/graphql/queries/pipeline_findings.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockPipelineFindingsResponse } from '../mock_data';
describe('Pipeline findings', () => {
let wrapper;
const apolloMock = {
queries: { findings: { loading: true } },
};
const createWrapper = ({ props = {}, mocks, apolloProvider } = {}) => {
const localVue = createLocalVue();
if (apolloProvider) {
localVue.use(VueApollo);
}
wrapper = shallowMount(PipelineFindings, {
localVue,
apolloProvider,
provide: {
projectFullPath: 'gitlab/security-reports',
pipeline: {
id: 77,
iid: 8,
},
},
propsData: {
filters: {},
...props,
},
mocks,
});
};
const createWrapperWithApollo = (resolver) => {
return createWrapper({
apolloProvider: createMockApollo([[pipelineFindingsQuery, resolver]]),
});
};
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findAlert = () => wrapper.find(GlAlert);
const findVulnerabilityList = () => wrapper.find(VulnerabilityList);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
afterEach(() => {
wrapper.destroy();
});
describe('when the findings are loading', () => {
beforeEach(() => {
createWrapper({ mocks: { $apollo: apolloMock } });
});
it('should show the initial loading state', () => {
expect(findVulnerabilityList().props('isLoading')).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('with findings', () => {
beforeEach(() => {
createWrapperWithApollo(jest.fn().mockResolvedValue(mockPipelineFindingsResponse()));
});
it('passes false as the loading state prop', () => {
expect(findVulnerabilityList().props('isLoading')).toBe(false);
});
it('passes down findings', () => {
expect(findVulnerabilityList().props('vulnerabilities')).toMatchObject([
{ confidence: 'unknown', id: '322ace94-2d2a-5efa-bd62-a04c927a4b9a', severity: 'HIGH' },
{ location: { file: 'package.json' }, id: '31ad79c6-b545-5408-89af-c4e90fc21eb4' },
]);
});
it('does not show the insersection loader when there is no next page', () => {
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('with multiple page findings', () => {
beforeEach(() => {
createWrapperWithApollo(
jest.fn().mockResolvedValue(mockPipelineFindingsResponse({ hasNextPage: true })),
);
});
it('shows the insersection loader', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
});
describe('with failed query', () => {
beforeEach(() => {
createWrapperWithApollo(jest.fn().mockRejectedValue(new Error('GrahpQL error')));
});
it('does not show the vulnerability list', () => {
expect(findVulnerabilityList().exists()).toBe(false);
});
it('shows the error', () => {
expect(findAlert().exists()).toBe(true);
});
});
});
...@@ -6,6 +6,7 @@ import IssuesBadge from 'ee/security_dashboard/components/issues_badge.vue'; ...@@ -6,6 +6,7 @@ import IssuesBadge from 'ee/security_dashboard/components/issues_badge.vue';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue'; import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue'; import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import VulnerabilityList from 'ee/security_dashboard/components/vulnerability_list.vue'; import VulnerabilityList from 'ee/security_dashboard/components/vulnerability_list.vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue'; import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
...@@ -14,7 +15,7 @@ import { generateVulnerabilities, vulnerabilities } from './mock_data'; ...@@ -14,7 +15,7 @@ import { generateVulnerabilities, vulnerabilities } from './mock_data';
describe('Vulnerability list component', () => { describe('Vulnerability list component', () => {
let wrapper; let wrapper;
const createWrapper = ({ props = {}, listeners, provide = {} } = {}) => { const createWrapper = ({ props = {}, listeners, provide = {}, stubs } = {}) => {
return mountExtended(VulnerabilityList, { return mountExtended(VulnerabilityList, {
propsData: { propsData: {
vulnerabilities: [], vulnerabilities: [],
...@@ -22,9 +23,11 @@ describe('Vulnerability list component', () => { ...@@ -22,9 +23,11 @@ describe('Vulnerability list component', () => {
}, },
stubs: { stubs: {
GlPopover: true, GlPopover: true,
...stubs,
}, },
listeners, listeners,
provide: () => ({ provide: () => ({
dashboardType: DASHBOARD_TYPES.PROJECT,
noVulnerabilitiesSvgPath: '#', noVulnerabilitiesSvgPath: '#',
dashboardDocumentation: '#', dashboardDocumentation: '#',
emptyStateSvgPath: '#', emptyStateSvgPath: '#',
...@@ -43,6 +46,7 @@ describe('Vulnerability list component', () => { ...@@ -43,6 +46,7 @@ describe('Vulnerability list component', () => {
const findCell = (label) => wrapper.find(`.js-${label}`); const findCell = (label) => wrapper.find(`.js-${label}`);
const findRows = () => wrapper.findAll('tbody tr'); const findRows = () => wrapper.findAll('tbody tr');
const findRow = (index = 0) => findRows().at(index); const findRow = (index = 0) => findRows().at(index);
const findColumn = (className) => wrapper.find(`[role="columnheader"].${className}`);
const findRowById = (id) => wrapper.find(`tbody tr[data-pk="${id}"`); const findRowById = (id) => wrapper.find(`tbody tr[data-pk="${id}"`);
const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]'); const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]');
const findIssuesBadge = (index = 0) => wrapper.findAllComponents(IssuesBadge).at(index); const findIssuesBadge = (index = 0) => wrapper.findAllComponents(IssuesBadge).at(index);
...@@ -61,7 +65,6 @@ describe('Vulnerability list component', () => { ...@@ -61,7 +65,6 @@ describe('Vulnerability list component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('with vulnerabilities', () => { describe('with vulnerabilities', () => {
...@@ -567,4 +570,27 @@ describe('Vulnerability list component', () => { ...@@ -567,4 +570,27 @@ describe('Vulnerability list component', () => {
expectRowCheckboxesToBe(() => false); expectRowCheckboxesToBe(() => false);
}); });
}); });
describe('when it is the pipeline dashboard', () => {
beforeEach(() => {
wrapper = createWrapper({
props: { vulnerabilities },
provide: { dashboardType: DASHBOARD_TYPES.PIPELINE },
stubs: {
GlTable,
},
});
});
it.each([['detected'], ['activity']])('does not render %s column', (className) => {
expect(findColumn(className).exists()).toBe(false);
});
it.each([['status'], ['severity'], ['description'], ['identifier'], ['scanner']])(
'renders %s column',
(className) => {
expect(findColumn(className).exists()).toBe(true);
},
);
});
}); });
...@@ -243,3 +243,73 @@ export const mockVulnerabilitySeveritiesGraphQLResponse = ({ dashboardType }) => ...@@ -243,3 +243,73 @@ export const mockVulnerabilitySeveritiesGraphQLResponse = ({ dashboardType }) =>
], ],
}, },
}); });
export const mockPipelineFindingsResponse = ({ hasNextPage } = {}) => ({
data: {
project: {
pipeline: {
securityReportFindings: {
nodes: [
{
uuid: '322ace94-2d2a-5efa-bd62-a04c927a4b9a',
name: 'growl_command-injection in growl',
description: null,
confidence: 'unknown',
identifiers: [
{
externalType: 'npm',
name: 'NPM-146',
__typename: 'VulnerabilityIdentifier',
},
],
scanner: null,
severity: 'HIGH',
location: {
__typename: 'VulnerabilityLocationDependencyScanning',
blobPath: null,
file: 'package.json',
image: null,
startLine: null,
path: null,
},
__typename: 'PipelineSecurityReportFinding',
},
{
uuid: '31ad79c6-b545-5408-89af-c4e90fc21eb4',
name:
'A prototype pollution vulnerability in handlebars may lead to remote code execution if an attacker can control the template in handlebars',
description: null,
confidence: 'unknown',
identifiers: [
{
externalType: 'retire.js',
name: 'RETIRE-JS-baf1b2b5f9a7c1dc0fb152365126e6c3',
__typename: 'VulnerabilityIdentifier',
},
],
scanner: null,
severity: 'HIGH',
location: {
__typename: 'VulnerabilityLocationDependencyScanning',
blobPath: null,
file: 'package.json',
image: null,
startLine: null,
path: null,
},
__typename: 'PipelineSecurityReportFinding',
},
],
pageInfo: {
__typename: 'PageInfo',
startCursor: 'MQ',
endCursor: hasNextPage ? 'MjA' : false,
},
__typename: 'PipelineSecurityReportFindingConnection',
},
__typename: 'Pipeline',
},
__typename: 'Project',
},
},
});
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