diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.stories.js b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.stories.js index c3b64fd09aca5ce521e8bdeae6f0614999ec4938..33c53a9fad7886f366b71c6ebe5d472e2b03753e 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.stories.js +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.stories.js @@ -27,4 +27,10 @@ Default.args = { name: 'GitLab.com', webUrl: 'http://gitlab.com', }, + location: { + file: '/src/js/main.js', + blobPath: '/project/namespace/-/blob/e3343434/src/js/main.js', + startLine: '10', + endLine: '20', + }, }; diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.vue index ee64b594dada61bc6ae395b126df586e1b83aa0b..d71bd981cc45c9e703dc46598ef29872506a6531 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_details_graphql/index.vue @@ -11,6 +11,8 @@ export default { descriptionSectionHeading: s__('Vulnerability|Description'), severityLabel: s__('Vulnerability|Severity:'), projectLabel: s__('Vulnerability|Project:'), + locationHeading: s__('Vulnerability|Location'), + locationFileLabel: s__('Vulnerability|File:'), }, components: { GlLink, @@ -51,6 +53,32 @@ export default { type: Object, required: true, }, + /** + * Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. + * + * @typedef {{ startLine?: string, endLine?: string, blobPath?: string, file?: string }} VulnerabilityLocationSast + * @typedef {( VulnerabilityLocationSast | null )} VulnerabilityLocation + * @type VulnerabilityLocation + */ + location: { + type: Object, + required: false, + default: null, + }, + }, + computed: { + file() { + const { startLine, endLine, blobPath, file } = this.location; + + const lineNumber = endLine > startLine ? `${startLine}-${endLine}` : startLine; + const url = `${blobPath}#L${lineNumber}`; + const name = `${file}:${lineNumber}`; + + return { + url, + name, + }; + }, }, }; </script> @@ -64,7 +92,7 @@ export default { <p>{{ description }}</p> </details-section> - <details-section> + <details-section data-testid="main-section"> <template #list> <details-section-list-item :label="$options.i18n.severityLabel" @@ -80,5 +108,21 @@ export default { </details-section-list-item> </template> </details-section> + + <details-section + v-if="location" + :heading="$options.i18n.locationHeading" + data-testid="location-section" + > + <template #list> + <details-section-list-item + v-if="location.file" + :label="$options.i18n.locationFileLabel" + data-testid="location-file-list-item" + > + <gl-link :href="file.url">{{ file.name }}</gl-link> + </details-section-list-item> + </template> + </details-section> </article> </template> diff --git a/ee/spec/frontend/security_dashboard/components/shared/vulnerability_details_graphql/index_spec.js b/ee/spec/frontend/security_dashboard/components/shared/vulnerability_details_graphql/index_spec.js index 5b6923d158dda295292ff0bcf692f4a53c3f6b23..5e078c6a0d757e84ca27547f0092c83089c3072b 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/vulnerability_details_graphql/index_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/vulnerability_details_graphql/index_spec.js @@ -17,7 +17,7 @@ const TEST_VULNERABILITY = { describe('ee/security_dashboard/components/shared/vulnerability_details_graphql/index.vue', () => { let wrapper; - const createComponent = () => { + const createComponent = (options = {}) => { wrapper = shallowMountExtended(Details, { propsData: { ...TEST_VULNERABILITY, @@ -27,25 +27,26 @@ describe('ee/security_dashboard/components/shared/vulnerability_details_graphql/ DetailsSection, DetailsSectionListItem, }, + ...options, }); }; + const expectToBeDetailsSection = (sectionWrapper, { heading = '' } = {}) => { + expect(sectionWrapper.is(DetailsSection)).toBe(true); + expect(sectionWrapper.props('heading')).toBe(heading); + }; + afterEach(() => { wrapper.destroy(); }); - beforeEach(createComponent); - describe('description section', () => { - const findDescriptionSection = () => wrapper.findByTestId('description-section'); + beforeEach(createComponent); - it('is a details-section with the correct heading', () => { - const descriptionSection = findDescriptionSection(); + const findDescriptionSection = () => wrapper.findByTestId('description-section'); - expect(descriptionSection.is(DetailsSection)).toBe(true); - expect(descriptionSection.props()).toMatchObject({ - heading: 'Description', - }); + it('is a details section with the correct heading', () => { + expectToBeDetailsSection(findDescriptionSection(), { heading: 'Description' }); }); it(`contains the vulnerability's description`, () => { @@ -53,19 +54,83 @@ describe('ee/security_dashboard/components/shared/vulnerability_details_graphql/ }); }); - it('renders the severity with a badge', () => { - const severity = wrapper.findByTestId('severity-list-item'); + describe('main section', () => { + beforeEach(createComponent); + + it('is a details section', () => { + expectToBeDetailsSection(wrapper.findByTestId('main-section')); + }); + + it('renders the severity with a badge', () => { + const severity = wrapper.findByTestId('severity-list-item'); + + expect(severity.text()).toContain('Severity:'); + expect(severity.findComponent(SeverityBadge).exists()).toBe(true); + }); + + it('renders the project with a link to it', () => { + const project = wrapper.findByTestId('project-list-item'); - expect(severity.text()).toContain('Severity:'); - expect(severity.findComponent(SeverityBadge).exists()).toBe(true); + expect(project.text()).toContain('Project:'); + expect(project.findComponent(GlLink).attributes('href')).toBe( + TEST_VULNERABILITY.project.webUrl, + ); + }); }); - it('renders the project with a link to it', () => { - const project = wrapper.findByTestId('project-list-item'); + describe('location section', () => { + const findLocationSection = () => wrapper.findByTestId('location-section'); + + describe('with no location data', () => { + beforeEach(createComponent); + + it('does not get rendered', () => { + expect(findLocationSection().exists()).toBe(false); + }); + }); + + describe('with location data', () => { + beforeEach(() => + createComponent({ + propsData: { + ...TEST_VULNERABILITY, + location: {}, + }, + }), + ); - expect(project.text()).toContain('Project:'); - expect(project.findComponent(GlLink).attributes('href')).toBe( - TEST_VULNERABILITY.project.webUrl, - ); + it('is a details section with the correct heading', () => { + expectToBeDetailsSection(findLocationSection(), { heading: 'Location' }); + }); + + describe('with file information', () => { + it.each` + description | lineData | expectedLineRange + ${'end line is after start line'} | ${{ startLine: 0, endLine: 1 }} | ${'0-1'} + ${'end line is equal to start line'} | ${{ startLine: 1, endLine: 1 }} | ${'1'} + `( + `links to the vulnerable file's line range "$expectedLineRange" when $description`, + ({ lineData, expectedLineRange }) => { + const location = { + blobPath: '/project/namespace/-/blob/e3343434/src/js/main.js', + file: '/src/js/main.js', + ...lineData, + }; + createComponent({ + propsData: { + ...TEST_VULNERABILITY, + location, + }, + }); + + const { blobPath, file } = location; + const fileLink = wrapper.findByTestId('location-file-list-item').find(GlLink); + + expect(fileLink.attributes('href')).toBe(`${blobPath}#L${expectedLineRange}`); + expect(fileLink.text()).toBe(`${file}:${expectedLineRange}`); + }, + ); + }); + }); }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8118c4b705daa67f7f29aa5aa7a00a255e723f46..3f8811989e899be9eec6fb65256b37a70972e4fd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -44263,6 +44263,9 @@ msgstr "" msgid "Vulnerability|File" msgstr "" +msgid "Vulnerability|File:" +msgstr "" + msgid "Vulnerability|GitLab Security Report" msgstr "" @@ -44287,6 +44290,9 @@ msgstr "" msgid "Vulnerability|Links" msgstr "" +msgid "Vulnerability|Location" +msgstr "" + msgid "Vulnerability|Method" msgstr ""