Commit 8e4fed9e authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '301003-add-details-section' into 'master'

Add details section to new vulnerability page

See merge request gitlab-org/gitlab!76124
parents f28991ef 8bf097cc
......@@ -2,6 +2,14 @@ import { s__ } from '~/locale';
export const VULNERABILITIES_PER_PAGE = 20;
export const DETECTION_METHODS = [
s__('Vulnerability|GitLab Security Report'),
s__('Vulnerability|External Security Report'),
s__('Vulnerability|Bug Bounty'),
s__('Vulnerability|Code Review'),
s__('Vulnerability|Security Audit'),
];
export const SEVERITY_LEVELS = {
critical: s__('severity|Critical'),
high: s__('severity|High'),
......
......@@ -2,14 +2,17 @@
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { s__, __ } from '~/locale';
import SectionDetails from './section_details.vue';
export default {
name: 'NewVulnerabilityForm',
components: {
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
MarkdownField,
SectionDetails,
},
props: {
markdownDocsPath: {
......@@ -27,9 +30,17 @@ export default {
form: {
vulnerabilityName: '',
vulnerabilityDesc: '',
severity: '',
status: '',
detectionMethod: '',
},
};
},
methods: {
updateFormValues(values) {
this.form = { ...this.form, ...values };
},
},
i18n: {
title: s__('VulnerabilityManagement|Add vulnerability finding'),
description: s__(
......@@ -106,6 +117,7 @@ export default {
</markdown-field>
</div>
</gl-form-group>
<section-details @change="updateFormValues" />
</gl-form>
</div>
</template>
<script>
import {
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlFormRadio,
GlFormRadioGroup,
} from '@gitlab/ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import { SEVERITY_LEVELS, DETECTION_METHODS } from 'ee/security_dashboard/store/constants';
import { s__, __ } from '~/locale';
export default {
components: {
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlFormRadio,
GlFormRadioGroup,
SeverityBadge,
},
data() {
return {
// Note: The cvss field is disabled during the MVC because the backend implementation
// is still missing. Since I learned this after implementing the design, I decided to
// hide it from the UI until the backend is ready so that we don't lose the work
// already completed.
showCvss: false,
cvss: '',
statusId: '',
severity: '',
detectionMethod: -1,
};
},
computed: {
selectedSeverity() {
return this.severityOptions.find(({ id }) => id === this.severity);
},
severityPlaceholder() {
return this.selectedSeverity?.name || this.$options.i18n.severity.placeholder;
},
cvssDescription() {
const scores = this.$options.cvss[this.selectedSeverity?.id];
if (!scores) {
return '';
}
return `${this.selectedSeverity.name}: ${scores[0].toFixed(1)} - ${scores[1].toFixed(1)}`;
},
detectionMethodPlaceholder() {
return (
this.detectionMethodOptions.find(({ id }) => id === this.detectionMethod)?.name ||
this.$options.i18n.detectionMethod.placeholder
);
},
detectionMethodOptions() {
return Object.entries(DETECTION_METHODS).map(([id, name]) => ({ id, name }));
},
severityOptions() {
return Object.entries(SEVERITY_LEVELS).map(([id, name]) => ({ id, name }));
},
statusOptions() {
return [
{ ...VULNERABILITY_STATE_OBJECTS.detected },
{ ...VULNERABILITY_STATE_OBJECTS.confirmed },
{ ...VULNERABILITY_STATE_OBJECTS.resolved },
];
},
},
methods: {
selectSeverity(value) {
this.severity = value;
this.emitChanges();
},
selectDetectionMethod(value) {
this.detectionMethod = value;
this.emitChanges();
},
emitChanges() {
this.$emit('change', {
status: this.statusId,
severity: this.severity,
detectionMethod: this.detectionMethod,
});
},
},
cvss: {
none: [0, 0],
low: [0.1, 3.9],
medium: [4.0, 6.9],
high: [7.0, 8.9],
critical: [9.0, 10.0],
},
i18n: {
title: s__('Vulnerability|Details'),
description: s__(
'Vulnerability|Information related how the vulnerability was discovered and its impact to the system.',
),
detectionMethod: {
label: s__('Vulnerability|Detection method'),
placeholder: s__('VulnerabilityManagement|Select a method'),
},
severity: {
label: s__('Vulnerability|Severity'),
placeholder: s__('Vulnerability|Select a severity'),
},
cvssLabel: s__('Vulnerability|CVSS v3'),
optional: __('Optional'),
status: {
label: __('Status'),
description: s__(
'Vulnerability|Set the status of the vulnerability finding based on the information available to you.',
),
},
},
};
</script>
<template>
<div>
<header class="gl-pt-4 gl-my-6 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1">
<h3 class="gl-mt-0 gl-mb-3">
{{ $options.i18n.title }}
</h3>
<p data-testid="section-description">
{{ $options.i18n.description }}
</p>
</header>
<gl-form-group
:label="$options.i18n.detectionMethod.label"
label-for="form-detection-method"
class="gl-mb-6"
>
<gl-dropdown id="form-detection-method" :text="detectionMethodPlaceholder">
<gl-dropdown-item
v-for="scanner in detectionMethodOptions"
:key="scanner.id"
is-check-item
:is-checked="detectionMethod === scanner.id"
@click="selectDetectionMethod(scanner.id)"
>
{{ scanner.name }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<div class="gl-display-flex gl-mb-6">
<gl-form-group
:label="$options.i18n.severity.label"
label-for="form-severity"
class="gl-mr-6 gl-mb-0"
>
<gl-dropdown id="form-severity" :text="severityPlaceholder">
<gl-dropdown-item
v-for="{ id } in severityOptions"
:key="id"
:is-checked="severity === id"
is-check-item
@click="selectSeverity(id)"
>
<severity-badge :severity="id" />
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group v-if="showCvss" :description="cvssDescription" class="gl-mb-0">
<label for="form-cvss" class="gl-display-block gl-mb-2"
>{{ $options.i18n.cvssLabel }}
<span class="gl-font-weight-300">({{ $options.i18n.optional }})</span></label
>
<gl-form-input id="form-cvss" v-model="cvss" class="gl-mb-2" type="text" />
</gl-form-group>
</div>
<gl-form-group :label="$options.i18n.status.label">
<p>{{ $options.i18n.status.description }}</p>
<gl-form-radio-group :checked="statusId" @change="emitChanges">
<label
v-for="status in statusOptions"
:key="status.state"
class="gl-display-flex gl-mb-5 gl-font-weight-normal gl-overflow-break-word"
>
<gl-form-radio v-model="statusId" :value="status.state">
{{ status.buttonText }}
<template #help>
<span class="gl-text-gray-500">{{ status.description }}</span>
</template>
</gl-form-radio>
</label>
</gl-form-radio-group>
</gl-form-group>
</div>
</template>
......@@ -20,6 +20,7 @@ export const VULNERABILITY_STATE_OBJECTS = {
buttonText: VULNERABILITY_STATES.detected,
dropdownText: s__('VulnerabilityManagement|Needs triage'),
dropdownDescription: s__('VulnerabilityManagement|Requires assessment'),
description: s__('VulnerabilityManagement|An unverified non-confirmed finding'),
},
dismissed: {
action: 'dismiss',
......@@ -37,6 +38,7 @@ export const VULNERABILITY_STATE_OBJECTS = {
buttonText: VULNERABILITY_STATES.confirmed,
dropdownText: __('Confirm'),
dropdownDescription: s__('VulnerabilityManagement|A true-positive and will fix'),
description: s__('VulnerabilityManagement|A verified true-positive vulnerability'),
},
resolved: {
action: 'resolve',
......@@ -44,6 +46,7 @@ export const VULNERABILITY_STATE_OBJECTS = {
buttonText: VULNERABILITY_STATES.resolved,
dropdownText: __('Resolve'),
dropdownDescription: s__('VulnerabilityManagement|Verified as fixed or mitigated'),
description: s__('VulnerabilityManagement|A removed or remediated vulnerability'),
},
};
......
......@@ -2,12 +2,15 @@ import { GlForm } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import NewVulnerability from 'ee/vulnerabilities/components/new_vulnerability/new_vulnerability.vue';
import SectionDetails from 'ee/vulnerabilities/components/new_vulnerability/section_details.vue';
describe('New vulnerability component', () => {
let wrapper;
const markdownDocsPath = '/path/to/markdown/docs';
const markdownPreviewPath = '/path/to/markdown/preview';
const findSectionDetails = () => wrapper.findComponent(SectionDetails);
const createWrapper = () => {
return mountExtended(NewVulnerability, {
propsData: {
......@@ -39,16 +42,14 @@ describe('New vulnerability component', () => {
});
it('creates markdown editor with correct props', () => {
expect(wrapper.findComponent(MarkdownField).props()).toEqual(
expect.objectContaining({
markdownDocsPath,
markdownPreviewPath,
textareaValue: '',
canAttachFile: false,
addSpacingClasses: false,
isSubmitting: false,
}),
);
expect(wrapper.findComponent(MarkdownField).props()).toMatchObject({
markdownDocsPath,
markdownPreviewPath,
textareaValue: '',
canAttachFile: false,
addSpacingClasses: false,
isSubmitting: false,
});
});
it.each`
......@@ -62,4 +63,14 @@ describe('New vulnerability component', () => {
expect(wrapper.findByText(description).exists()).toBe(true);
}
});
it.each`
section | selector | fields
${'Details'} | ${findSectionDetails} | ${{ severity: 'low', detectionMethod: 2, status: 'confirmed' }}
`('mounts the section $section and reacts on the change event', ({ selector, fields }) => {
const section = selector();
expect(section.exists()).toBe(true);
section.vm.$emit('change', fields);
expect(wrapper.vm.form).toMatchObject(fields);
});
});
import { nextTick } from 'vue';
import { GlDropdown, GlDropdownItem, GlFormRadio } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SectionDetails from 'ee/vulnerabilities/components/new_vulnerability/section_details.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
describe('New vulnerability - Section Details', () => {
let wrapper;
const findDetectionMethodItem = (at) =>
wrapper.findAllComponents(GlDropdown).at(0).findAllComponents(GlDropdownItem).at(at);
const findSeverityMethodItem = (at) =>
wrapper.findAllComponents(GlDropdown).at(1).findAllComponents(GlDropdownItem).at(at);
const findStatusItem = (at) => wrapper.findAllComponents(GlFormRadio).at(at);
const createWrapper = () => {
return mountExtended(SectionDetails, {});
};
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it.each`
value | text
${0} | ${'GitLab Security Report'}
${1} | ${'External Security Report'}
${2} | ${'Bug Bounty'}
${3} | ${'Code Review'}
${4} | ${'Security Audit'}
`('displays and handles detection method field: $text', ({ value, text }) => {
const dropdownItem = findDetectionMethodItem(value);
expect(dropdownItem.text()).toBe(text);
dropdownItem.vm.$emit('click');
expect(wrapper.emitted('change')[0][0]).toEqual({
detectionMethod: `${value}`,
severity: '',
status: '',
});
});
it.each`
index | value
${0} | ${'critical'}
${1} | ${'high'}
${2} | ${'medium'}
${3} | ${'low'}
${4} | ${'unknown'}
${5} | ${'info'}
`('displays and handles severity field: $value', ({ index, value }) => {
const dropdownItem = findSeverityMethodItem(index);
expect(dropdownItem.findComponent(SeverityBadge).props('severity')).toBe(value);
dropdownItem.vm.$emit('click');
expect(wrapper.emitted('change')[0][0]).toEqual({
detectionMethod: -1,
severity: value,
status: '',
});
});
it.each`
index | value
${0} | ${'detected'}
${1} | ${'confirmed'}
${2} | ${'resolved'}
`('displays and handles status field', async ({ index, value }) => {
findStatusItem(index).trigger('click');
await nextTick();
expect(wrapper.emitted('change')[0][0]).toEqual({
detectionMethod: -1,
severity: '',
status: value,
});
});
});
......@@ -38792,12 +38792,21 @@ msgstr ""
msgid "VulnerabilityManagement|%{statusStart}Resolved%{statusEnd} %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|A removed or remediated vulnerability"
msgstr ""
msgid "VulnerabilityManagement|A true-positive and will fix"
msgstr ""
msgid "VulnerabilityManagement|A verified true-positive vulnerability"
msgstr ""
msgid "VulnerabilityManagement|Add vulnerability finding"
msgstr ""
msgid "VulnerabilityManagement|An unverified non-confirmed finding"
msgstr ""
msgid "VulnerabilityManagement|Change status"
msgstr ""
......@@ -38825,6 +38834,9 @@ msgstr ""
msgid "VulnerabilityManagement|Requires assessment"
msgstr ""
msgid "VulnerabilityManagement|Select a method"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
msgstr ""
......@@ -38894,9 +38906,18 @@ msgstr ""
msgid "Vulnerability|Additional Info"
msgstr ""
msgid "Vulnerability|Bug Bounty"
msgstr ""
msgid "Vulnerability|CVSS v3"
msgstr ""
msgid "Vulnerability|Class"
msgstr ""
msgid "Vulnerability|Code Review"
msgstr ""
msgid "Vulnerability|Comments"
msgstr ""
......@@ -38912,21 +38933,33 @@ msgstr ""
msgid "Vulnerability|Description"
msgstr ""
msgid "Vulnerability|Details"
msgstr ""
msgid "Vulnerability|Detected"
msgstr ""
msgid "Vulnerability|Detection method"
msgstr ""
msgid "Vulnerability|Download"
msgstr ""
msgid "Vulnerability|Evidence"
msgstr ""
msgid "Vulnerability|External Security Report"
msgstr ""
msgid "Vulnerability|False positive detected"
msgstr ""
msgid "Vulnerability|File"
msgstr ""
msgid "Vulnerability|GitLab Security Report"
msgstr ""
msgid "Vulnerability|Identifier"
msgstr ""
......@@ -38936,6 +38969,9 @@ msgstr ""
msgid "Vulnerability|Image"
msgstr ""
msgid "Vulnerability|Information related how the vulnerability was discovered and its impact to the system."
msgstr ""
msgid "Vulnerability|Links"
msgstr ""
......@@ -38960,6 +38996,15 @@ msgstr ""
msgid "Vulnerability|Scanner Provider"
msgstr ""
msgid "Vulnerability|Security Audit"
msgstr ""
msgid "Vulnerability|Select a severity"
msgstr ""
msgid "Vulnerability|Set the status of the vulnerability finding based on the information available to you."
msgstr ""
msgid "Vulnerability|Severity"
msgstr ""
......
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