Commit 58415b0c authored by David Pisek's avatar David Pisek Committed by Paul Slaughter

GraphQL Vuln Modal: Add solution and report-type

This commit adds the solutions-card and report-type to the GrahQL version
of the vulnerability detail modal.

It also adds some minor styling improvements and hides unused
fields within the vulnerability details section.
parent e524d530
<script> <script>
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card_vuex.vue';
import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue'; import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue';
export default { export default {
components: { components: {
GlModal, GlModal,
SolutionCard,
VulnerabilityDetails, VulnerabilityDetails,
}, },
props: { props: {
...@@ -13,6 +15,14 @@ export default { ...@@ -13,6 +15,14 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
remediation() {
return this.finding.remediations?.[0];
},
canDownloadPatchForThisVulnerability() {
return !this.finding.hasMergeRequest && this.remediation?.diff?.length > 0;
},
},
}; };
</script> </script>
...@@ -23,7 +33,16 @@ export default { ...@@ -23,7 +33,16 @@ export default {
:title="finding.name" :title="finding.name"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<!-- NOTE: adding the rest of the modal's functionality is captured in https://gitlab.com/gitlab-org/gitlab/-/issues/300755 -->
<vulnerability-details :vulnerability="finding" /> <vulnerability-details :vulnerability="finding" />
<solution-card
:solution="finding.solution"
:remediation="remediation"
:has-mr="finding.hasMergeRequest"
:has-download="canDownloadPatchForThisVulnerability"
/>
<!-- adding the comment-history is captured in: https://gitlab.com/gitlab-org/gitlab/-/issues/337488 -->
<!-- implementing and adding the footer is captured in: https://gitlab.com/gitlab-org/gitlab/-/issues/337487 -->
</gl-modal> </gl-modal>
</template> </template>
...@@ -30,11 +30,13 @@ query pipelineFindings( ...@@ -30,11 +30,13 @@ query pipelineFindings(
externalType externalType
name name
} }
reportType
scanner { scanner {
vendor vendor
} }
state state
severity severity
solution
location { location {
...VulnerabilityLocation ...VulnerabilityLocation
} }
......
...@@ -43,6 +43,7 @@ export default { ...@@ -43,6 +43,7 @@ export default {
</script> </script>
<template> <template>
<gl-card <gl-card
v-if="solutionText || showCreateMergeRequestMsg"
class="gl-my-6" class="gl-my-6"
:body-class="{ 'gl-p-0': !solutionText }" :body-class="{ 'gl-p-0': !solutionText }"
:footer-class="{ 'gl-border-0': !solutionText }" :footer-class="{ 'gl-border-0': !solutionText }"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import { bodyWithFallBack } from 'ee/vue_shared/security_reports/components/helpers'; import { bodyWithFallBack } from 'ee/vue_shared/security_reports/components/helpers';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
...@@ -25,6 +26,9 @@ export default { ...@@ -25,6 +26,9 @@ export default {
}, },
}, },
computed: { computed: {
humanReadableReportType() {
return convertReportType(this.vulnerability.reportType);
},
location() { location() {
return this.vulnerability.location || {}; return this.vulnerability.location || {};
}, },
...@@ -180,6 +184,7 @@ export default { ...@@ -180,6 +184,7 @@ export default {
<template> <template>
<div class="md" data-qa-selector="vulnerability_details"> <div class="md" data-qa-selector="vulnerability_details">
<h1 <h1
v-if="vulnerability.title"
class="mt-3 mb-2 border-bottom-0" class="mt-3 mb-2 border-bottom-0"
data-testid="title" data-testid="title"
data-qa-selector="vulnerability_title" data-qa-selector="vulnerability_title"
...@@ -195,9 +200,9 @@ export default { ...@@ -195,9 +200,9 @@ export default {
<detail-item :sprintf-message="__('%{labelStart}Severity:%{labelEnd} %{severity}')"> <detail-item :sprintf-message="__('%{labelStart}Severity:%{labelEnd} %{severity}')">
<severity-badge :severity="vulnerability.severity" class="gl-display-inline ml-1" /> <severity-badge :severity="vulnerability.severity" class="gl-display-inline ml-1" />
</detail-item> </detail-item>
<detail-item :sprintf-message="__('%{labelStart}Scan Type:%{labelEnd} %{reportType}')" <detail-item :sprintf-message="__('%{labelStart}Scan Type:%{labelEnd} %{reportType}')">{{
>{{ vulnerability.reportType }} humanReadableReportType
</detail-item> }}</detail-item>
<detail-item <detail-item
v-if="scanner.name" v-if="scanner.name"
:sprintf-message="__('%{labelStart}Scanner:%{labelEnd} %{scanner}')" :sprintf-message="__('%{labelStart}Scanner:%{labelEnd} %{scanner}')"
......
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import VulnerabilityFindingModal from 'ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue'; import VulnerabilityFindingModal from 'ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card_vuex.vue';
import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue'; import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue';
const TEST_VULNERABILITY = { const TEST_VULNERABILITY = {
name: 'foo', name: 'foo',
solution: 'foo',
hasMergeRequest: false,
remediations: [{}],
}; };
describe('Vulnerability finding modal', () => { describe('Vulnerability finding modal', () => {
let wrapper; let wrapper;
const createWrapper = () => const createWrapper = (options = {}) =>
shallowMount(VulnerabilityFindingModal, { shallowMount(VulnerabilityFindingModal, {
propsData: { propsData: {
finding: TEST_VULNERABILITY, finding: TEST_VULNERABILITY,
}, },
...options,
}); });
const findModal = () => wrapper.findComponent(GlModal); const findModal = () => wrapper.findComponent(GlModal);
const findVulnerabilityDetails = () => wrapper.findComponent(VulnerabilityDetails); const findVulnerabilityDetails = () => wrapper.findComponent(VulnerabilityDetails);
const findSolutionCard = () => wrapper.findComponent(SolutionCard);
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper(); wrapper = createWrapper();
...@@ -50,4 +56,33 @@ describe('Vulnerability finding modal', () => { ...@@ -50,4 +56,33 @@ describe('Vulnerability finding modal', () => {
expect(findVulnerabilityDetails().props('vulnerability')).toBe(TEST_VULNERABILITY); expect(findVulnerabilityDetails().props('vulnerability')).toBe(TEST_VULNERABILITY);
}); });
}); });
describe('solution card', () => {
it('gets passed the correct props', () => {
expect(findSolutionCard().props()).toMatchObject({
solution: TEST_VULNERABILITY.solution,
hasMr: TEST_VULNERABILITY.hasMergeRequest,
remediation: TEST_VULNERABILITY.remediations[0],
});
});
it.each`
condition | expectedHasDownloadValue | vulnerabilityData
${'merge request and remediations diff'} | ${true} | ${{ hasMergeRequest: false, remediations: [{ diff: ['foo'] }] }}
${'merge request'} | ${false} | ${{ hasMergeRequest: true }}
${'no remediation'} | ${false} | ${{ remediations: [] }}
${'no remediation diff'} | ${false} | ${{ remediations: [{ diff: [] }] }}
`(
'gets passed the "hasDownload" prop as "$expectedHasDownloadValue" when the vulnerability has $condition',
({ vulnerabilityData, expectedHasDownloadValue }) => {
wrapper = createWrapper({
propsData: {
finding: { ...TEST_VULNERABILITY, ...vulnerabilityData },
},
});
expect(findSolutionCard().props('hasDownload')).toBe(expectedHasDownloadValue);
},
);
});
}); });
...@@ -265,6 +265,8 @@ export const mockPipelineFindingsResponse = ({ hasNextPage } = {}) => ({ ...@@ -265,6 +265,8 @@ export const mockPipelineFindingsResponse = ({ hasNextPage } = {}) => ({
scanner: null, scanner: null,
severity: 'HIGH', severity: 'HIGH',
state: 'DETECTED', state: 'DETECTED',
solution: 'Upgrade to versions 5.2.2.1, 6.0.0 or above.',
reportType: 'DEPENDENCY_SCANNING',
location: { location: {
__typename: 'VulnerabilityLocationDependencyScanning', __typename: 'VulnerabilityLocationDependencyScanning',
blobPath: null, blobPath: null,
...@@ -291,6 +293,8 @@ export const mockPipelineFindingsResponse = ({ hasNextPage } = {}) => ({ ...@@ -291,6 +293,8 @@ export const mockPipelineFindingsResponse = ({ hasNextPage } = {}) => ({
], ],
scanner: null, scanner: null,
severity: 'HIGH', severity: 'HIGH',
reportType: 'DEPENDENCY_SCANNING',
solution: 'Upgrade to versions 5.2.2.1, 6.0.0 or above.',
location: { location: {
__typename: 'VulnerabilityLocationDependencyScanning', __typename: 'VulnerabilityLocationDependencyScanning',
blobPath: null, blobPath: null,
......
...@@ -105,5 +105,16 @@ describe('Solution Card', () => { ...@@ -105,5 +105,16 @@ describe('Solution Card', () => {
}); });
}); });
}); });
describe('without solution and remediation', () => {
beforeEach(() => {
const propsData = { remediation: {}, solution: '' };
wrapper = shallowMount(Component, { propsData });
});
it('does not render the card', () => {
expect(wrapper.findComponent(GlCard).exists()).toBe(false);
});
});
}); });
}); });
...@@ -9,10 +9,9 @@ describe('Vulnerability Details', () => { ...@@ -9,10 +9,9 @@ describe('Vulnerability Details', () => {
let wrapper; let wrapper;
const vulnerability = { const vulnerability = {
title: 'some title',
severity: 'bad severity', severity: 'bad severity',
confidence: 'high confidence', confidence: 'high confidence',
reportType: 'nice report_type', reportType: 'Some report type',
description: 'vulnerability description', description: 'vulnerability description',
}; };
...@@ -34,11 +33,11 @@ describe('Vulnerability Details', () => { ...@@ -34,11 +33,11 @@ describe('Vulnerability Details', () => {
it('shows the properties that should always be shown', () => { it('shows the properties that should always be shown', () => {
createWrapper(); createWrapper();
expect(getText('title')).toBe(vulnerability.title);
expect(getText('description')).toBe(vulnerability.description); expect(getText('description')).toBe(vulnerability.description);
expect(wrapper.find(SeverityBadge).props('severity')).toBe(vulnerability.severity); expect(wrapper.find(SeverityBadge).props('severity')).toBe(vulnerability.severity);
expect(getText('reportType')).toBe(`Scan Type: ${vulnerability.reportType}`); expect(getText('reportType')).toBe(`Scan Type: ${vulnerability.reportType}`);
expect(getById('title').exists()).toBe(false);
expect(getById('image').exists()).toBe(false); expect(getById('image').exists()).toBe(false);
expect(getById('os').exists()).toBe(false); expect(getById('os').exists()).toBe(false);
expect(getById('file').exists()).toBe(false); expect(getById('file').exists()).toBe(false);
...@@ -50,6 +49,29 @@ describe('Vulnerability Details', () => { ...@@ -50,6 +49,29 @@ describe('Vulnerability Details', () => {
expect(getAllById('identifier')).toHaveLength(0); expect(getAllById('identifier')).toHaveLength(0);
}); });
it.each`
reportType | expectedOutput
${'SAST'} | ${'SAST'}
${'DAST'} | ${'DAST'}
${'DEPENDENCY_SCANNING'} | ${'Dependency Scanning'}
${'CONTAINER_SCANNING'} | ${'Container Scanning'}
${'SECRET_DETECTION'} | ${'Secret Detection'}
${'COVERAGE_FUZZING'} | ${'Coverage Fuzzing'}
${'API_FUZZING'} | ${'API Fuzzing'}
${'CLUSTER_IMAGE_SCANNING'} | ${'Cluster Image Scanning'}
`(
'displays "$expectedOutput" when report type is "$reportType"',
({ reportType, expectedOutput }) => {
createWrapper({ reportType });
expect(getText('reportType')).toBe(`Scan Type: ${expectedOutput}`);
},
);
it('shows the title if it exists', () => {
createWrapper({ title: 'some title' });
expect(getText('title')).toBe('some title');
});
it('shows the location image if it exists', () => { it('shows the location image if it exists', () => {
createWrapper({ location: { image: 'some image' } }); createWrapper({ location: { image: 'some image' } });
expect(getText('image')).toBe(`Image: some image`); expect(getText('image')).toBe(`Image: some image`);
......
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