diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details.vue b/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details.vue index a10fa3c42498c1276859a60121d82e7949d74f0b..d6d82d049e0fc4cee9ba8ba54198725ac375829e 100644 --- a/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details.vue +++ b/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details.vue @@ -183,9 +183,15 @@ export default { isAiResolvable(vuln) { return vuln.ai_resolution_enabled && this.glAbilities.resolveVulnerabilityWithAi; }, + matchesAutoDismissPolicy(vuln) { + return vuln.matches_auto_dismiss_policy; + }, getAiResolvableBadgeId(uuid) { return `ai-resolvable-badge-${uuid}`; }, + getAutoDismissPolicyBadgeId(uuid) { + return `auto-dismiss-policy-badge-${uuid}`; + }, tabTitle(scanType) { return this.report[scanType]?.numberOfNewFindings ?? '-'; }, @@ -204,6 +210,12 @@ export default { anchor: 'vulnerability-resolution-in-a-merge-request', }), }, + policyAutoDismissPopover: { + text: s__('ciReport|Vulnerability was matched by a policy and will be auto-dismissed.'), + learnMorePath: helpPagePath( + 'user/application_security/policies/vulnerability_management_policy', + ), + }, diffBasedScansLearnMorePath: helpPagePath('user/application_security/sast/gitlab_advanced_sast', { anchor: 'use-diff-based-scanning-to-improve-performance', }), @@ -344,6 +356,28 @@ export default { + diff --git a/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details_spec.js b/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details_spec.js index 9b04cb4768c19511e289a984efe67fac48f0fed8..92263e02e359fe41d9bb74da1e3f961896fe384a 100644 --- a/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details_spec.js +++ b/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_report_details_spec.js @@ -110,6 +110,9 @@ describe('MR Widget Security Reports - Finding', () => { const findSummaryText = () => wrapper.findComponent(SummaryText); const findSummaryHighlights = () => wrapper.findComponent(SummaryHighlights); const findDismissedBadge = () => wrapper.findByTestId('dismissed-badge'); + const findAutoDismissPolicyBadge = () => wrapper.findByTestId('auto-dismiss-policy-badge'); + const findAutoDismissPolicyBadgePopover = (uuid) => + wrapper.findByTestId(`auto-dismiss-policy-badge-popover-${uuid}`); const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller'); beforeEach(() => { @@ -359,6 +362,265 @@ describe('MR Widget Security Reports - Finding', () => { }); }); + describe('auto dismiss policy badge', () => { + const findingUuid = '1'; + + describe.each` + matchesAutoDismissPolicy + ${false} + ${undefined} + ${null} + `( + 'when "matches_auto_dismiss_policy" is set to "$matchesAutoDismissPolicy"', + ({ matchesAutoDismissPolicy }) => { + beforeEach(async () => { + createComponent({ + propsData: { + report: mockReport({ + full: mockReportData({ + numberOfNewFindings: 1, + findings: [ + { + uuid: findingUuid, + severity: 'critical', + name: 'Password leak', + state: 'new', + matches_auto_dismiss_policy: matchesAutoDismissPolicy, + }, + ], + }), + }), + }, + }); + + await nextTick(); + }); + + it('should not show the auto-dismiss-policy badge', () => { + expect(findAutoDismissPolicyBadge().exists()).toBe(false); + }); + + it('should not show the auto-dismiss-policy badge popover', () => { + expect(findAutoDismissPolicyBadgePopover(findingUuid).exists()).toBe(false); + }); + }, + ); + + describe('when "matches_auto_dismiss_policy" is set to "true"', () => { + beforeEach(async () => { + createComponent({ + propsData: { + report: mockReport({ + full: { + numberOfNewFindings: 1, + findings: [ + { + uuid: findingUuid, + severity: 'critical', + name: 'Password leak', + state: 'new', + matches_auto_dismiss_policy: true, + }, + ], + }, + }), + }, + }); + + await nextTick(); + }); + + it('should show the auto-dismiss-policy badge', () => { + expect(findAutoDismissPolicyBadge().exists()).toBe(true); + }); + + it('should add the correct id-attribute to the auto-dismiss-policy badge', () => { + expect(findAutoDismissPolicyBadge().attributes('id')).toBe( + `auto-dismiss-policy-badge-${findingUuid}`, + ); + }); + + it('should have the correct data-testid attribute', () => { + expect(findAutoDismissPolicyBadge().attributes('data-testid')).toBe( + 'auto-dismiss-policy-badge', + ); + }); + + it('should have the correct variant', () => { + expect(findAutoDismissPolicyBadge().props('variant')).toBe('info'); + }); + + it('should show a popover for the auto-dismiss-policy badge', () => { + expect(findAutoDismissPolicyBadgePopover(findingUuid).exists()).toBe(true); + }); + + it('should pass the correct props to the auto-dismiss-policy badge popover', () => { + expect(findAutoDismissPolicyBadgePopover(findingUuid).props()).toMatchObject({ + target: `auto-dismiss-policy-badge-${findingUuid}`, + boundary: 'viewport', + placement: 'top', + }); + }); + + it('should display the correct popover text', () => { + expect(findAutoDismissPolicyBadgePopover(findingUuid).text()).toContain( + 'Vulnerability was matched by a policy and will be auto-dismissed.', + ); + }); + + it('should have a learn more link in the popover', () => { + const learnMoreLink = findAutoDismissPolicyBadgePopover(findingUuid).findComponent({ + name: 'GlLink', + }); + + expect(learnMoreLink.exists()).toBe(true); + expect(learnMoreLink.text()).toBe('Learn more'); + expect(learnMoreLink.attributes('href')).toContain( + 'user/application_security/policies/vulnerability_management_policy', + ); + }); + }); + + describe('when multiple vulnerabilities have different auto-dismiss-policy values', () => { + beforeEach(async () => { + createComponent({ + propsData: { + report: mockReport({ + full: { + numberOfNewFindings: 3, + findings: [ + { + uuid: '1', + severity: 'critical', + name: 'Password leak', + state: 'new', + matches_auto_dismiss_policy: true, + }, + { + uuid: '2', + severity: 'high', + name: 'XSS vulnerability', + state: 'new', + matches_auto_dismiss_policy: false, + }, + { + uuid: '3', + severity: 'medium', + name: 'SQL vulnerability', + state: 'new', + matches_auto_dismiss_policy: true, + }, + ], + }, + }), + }, + }); + + await nextTick(); + }); + + it('should show the badge only for vulnerabilities with matches_auto_dismiss_policy set to true', () => { + const badges = wrapper.findAllByTestId('auto-dismiss-policy-badge'); + + expect(badges).toHaveLength(2); + expect(badges.at(0).attributes('id')).toBe('auto-dismiss-policy-badge-1'); + expect(badges.at(1).attributes('id')).toBe('auto-dismiss-policy-badge-3'); + }); + + it('should show popovers only for vulnerabilities with matches_auto_dismiss_policy set to true', () => { + expect(findAutoDismissPolicyBadgePopover('1').exists()).toBe(true); + expect(findAutoDismissPolicyBadgePopover('2').exists()).toBe(false); + expect(findAutoDismissPolicyBadgePopover('3').exists()).toBe(true); + }); + }); + + describe('when auto-dismiss-policy badge is shown alongside other badges', () => { + beforeEach(async () => { + createComponent({ + provide: { + glAbilities: { + resolveVulnerabilityWithAi: true, + }, + }, + propsData: { + report: mockReport({ + full: { + numberOfNewFindings: 1, + findings: [ + { + uuid: findingUuid, + severity: 'critical', + name: 'Password leak', + state: 'dismissed', + matches_auto_dismiss_policy: true, + ai_resolution_enabled: true, + }, + ], + }, + }), + }, + }); + + await nextTick(); + }); + + it('should show both dismissed and auto-dismiss-policy badges', () => { + expect(findDismissedBadge().exists()).toBe(true); + expect(findAutoDismissPolicyBadge().exists()).toBe(true); + }); + + it('should show both AI-resolvable and auto-dismiss-policy badges', () => { + const aiResolvableBadge = wrapper.findByTestId('ai-resolvable-badge'); + + expect(aiResolvableBadge.exists()).toBe(true); + expect(findAutoDismissPolicyBadge().exists()).toBe(true); + }); + + it('should display all three badges', () => { + const dismissedBadge = findDismissedBadge(); + const aiResolvableBadge = wrapper.findByTestId('ai-resolvable-badge'); + const autoDismissBadge = findAutoDismissPolicyBadge(); + + expect(dismissedBadge.exists()).toBe(true); + expect(aiResolvableBadge.exists()).toBe(true); + expect(autoDismissBadge.exists()).toBe(true); + }); + }); + + describe('when auto-dismiss-policy badge is shown in partial scan', () => { + beforeEach(async () => { + createComponent({ + propsData: { + report: mockReport({ + partial: { + numberOfNewFindings: 1, + findings: [ + { + uuid: findingUuid, + severity: 'high', + name: 'SQL Injection', + state: 'new', + matches_auto_dismiss_policy: true, + }, + ], + }, + }), + }, + }); + + await nextTick(); + }); + + it('should show the auto-dismiss-policy badge in partial scan', () => { + expect(findAutoDismissPolicyBadge().exists()).toBe(true); + }); + + it('should show the popover for the auto-dismiss-policy badge in partial scan', () => { + expect(findAutoDismissPolicyBadgePopover(findingUuid).exists()).toBe(true); + }); + }); + }); + describe('tab view', () => { it.each` report | diffBasedScan | fullScan | testCase diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bb90f781b70423f2ae34317074dccd70f2d545fe..3bef5e0dc2357997132664232ce153f8a032c00d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -80399,6 +80399,9 @@ msgstr "" msgid "ciReport|View vulnerabilities" msgstr "" +msgid "ciReport|Vulnerability was matched by a policy and will be auto-dismissed." +msgstr "" + msgid "ciReport|in" msgstr ""