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>
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';
export default {
components: {
GlModal,
SolutionCard,
VulnerabilityDetails,
},
props: {
......@@ -13,6 +15,14 @@ export default {
required: true,
},
},
computed: {
remediation() {
return this.finding.remediations?.[0];
},
canDownloadPatchForThisVulnerability() {
return !this.finding.hasMergeRequest && this.remediation?.diff?.length > 0;
},
},
};
</script>
......@@ -23,7 +33,16 @@ export default {
:title="finding.name"
@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" />
<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>
</template>
......@@ -30,11 +30,13 @@ query pipelineFindings(
externalType
name
}
reportType
scanner {
vendor
}
state
severity
solution
location {
...VulnerabilityLocation
}
......
......@@ -43,6 +43,7 @@ export default {
</script>
<template>
<gl-card
v-if="solutionText || showCreateMergeRequestMsg"
class="gl-my-6"
:body-class="{ 'gl-p-0': !solutionText }"
:footer-class="{ 'gl-border-0': !solutionText }"
......
......@@ -2,6 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { bodyWithFallBack } from 'ee/vue_shared/security_reports/components/helpers';
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 { s__, __ } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
......@@ -25,6 +26,9 @@ export default {
},
},
computed: {
humanReadableReportType() {
return convertReportType(this.vulnerability.reportType);
},
location() {
return this.vulnerability.location || {};
},
......@@ -180,6 +184,7 @@ export default {
<template>
<div class="md" data-qa-selector="vulnerability_details">
<h1
v-if="vulnerability.title"
class="mt-3 mb-2 border-bottom-0"
data-testid="title"
data-qa-selector="vulnerability_title"
......@@ -195,9 +200,9 @@ export default {
<detail-item :sprintf-message="__('%{labelStart}Severity:%{labelEnd} %{severity}')">
<severity-badge :severity="vulnerability.severity" class="gl-display-inline ml-1" />
</detail-item>
<detail-item :sprintf-message="__('%{labelStart}Scan Type:%{labelEnd} %{reportType}')"
>{{ vulnerability.reportType }}
</detail-item>
<detail-item :sprintf-message="__('%{labelStart}Scan Type:%{labelEnd} %{reportType}')">{{
humanReadableReportType
}}</detail-item>
<detail-item
v-if="scanner.name"
:sprintf-message="__('%{labelStart}Scanner:%{labelEnd} %{scanner}')"
......
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
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';
const TEST_VULNERABILITY = {
name: 'foo',
solution: 'foo',
hasMergeRequest: false,
remediations: [{}],
};
describe('Vulnerability finding modal', () => {
let wrapper;
const createWrapper = () =>
const createWrapper = (options = {}) =>
shallowMount(VulnerabilityFindingModal, {
propsData: {
finding: TEST_VULNERABILITY,
},
...options,
});
const findModal = () => wrapper.findComponent(GlModal);
const findVulnerabilityDetails = () => wrapper.findComponent(VulnerabilityDetails);
const findSolutionCard = () => wrapper.findComponent(SolutionCard);
beforeEach(() => {
wrapper = createWrapper();
......@@ -50,4 +56,33 @@ describe('Vulnerability finding modal', () => {
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 } = {}) => ({
scanner: null,
severity: 'HIGH',
state: 'DETECTED',
solution: 'Upgrade to versions 5.2.2.1, 6.0.0 or above.',
reportType: 'DEPENDENCY_SCANNING',
location: {
__typename: 'VulnerabilityLocationDependencyScanning',
blobPath: null,
......@@ -291,6 +293,8 @@ export const mockPipelineFindingsResponse = ({ hasNextPage } = {}) => ({
],
scanner: null,
severity: 'HIGH',
reportType: 'DEPENDENCY_SCANNING',
solution: 'Upgrade to versions 5.2.2.1, 6.0.0 or above.',
location: {
__typename: 'VulnerabilityLocationDependencyScanning',
blobPath: null,
......
......@@ -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', () => {
let wrapper;
const vulnerability = {
title: 'some title',
severity: 'bad severity',
confidence: 'high confidence',
reportType: 'nice report_type',
reportType: 'Some report type',
description: 'vulnerability description',
};
......@@ -34,11 +33,11 @@ describe('Vulnerability Details', () => {
it('shows the properties that should always be shown', () => {
createWrapper();
expect(getText('title')).toBe(vulnerability.title);
expect(getText('description')).toBe(vulnerability.description);
expect(wrapper.find(SeverityBadge).props('severity')).toBe(vulnerability.severity);
expect(getText('reportType')).toBe(`Scan Type: ${vulnerability.reportType}`);
expect(getById('title').exists()).toBe(false);
expect(getById('image').exists()).toBe(false);
expect(getById('os').exists()).toBe(false);
expect(getById('file').exists()).toBe(false);
......@@ -50,6 +49,29 @@ describe('Vulnerability Details', () => {
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', () => {
createWrapper({ location: { 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