diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/lib/index.js b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/lib/index.js index df2c462e6aa1e8fab1e44836b908a24b180cf846..a5a148d7b8749cb2d1d4b4e1c7fd33f5f2488e21 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/lib/index.js +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/lib/index.js @@ -1,5 +1,6 @@ export { fromYaml } from './from_yaml'; export { toYaml } from './to_yaml'; +export { buildRule } from './rules'; export * from './humanize'; export const DEFAULT_SCAN_RESULT_POLICY = `type: scan_result_policy diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/lib/rules.js b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/lib/rules.js new file mode 100644 index 0000000000000000000000000000000000000000..74452c3e1e7f293c46206db622add0b8e3267def --- /dev/null +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/lib/rules.js @@ -0,0 +1,13 @@ +/* + Construct a new rule object. +*/ +export function buildRule() { + return { + type: 'scan_finding', + branches: [], + scanners: [], + vulnerabilities_allowed: 0, + severity_levels: [], + vulnerability_states: [], + }; +} diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/policy_rule_builder.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/policy_rule_builder.vue new file mode 100644 index 0000000000000000000000000000000000000000..5f488745cb15f83d1509aa0484c7df29366e6673 --- /dev/null +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/policy_rule_builder.vue @@ -0,0 +1,166 @@ + + + diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue index f11806712d019bb26cd4c3a04463687d22f4d3c4..74f1cd9686477d9e558a675973f5734333f419b4 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue @@ -20,7 +20,8 @@ import PolicyEditorLayout from '../policy_editor_layout.vue'; import { assignSecurityPolicyProject, modifyPolicy } from '../utils'; import DimDisableContainer from '../dim_disable_container.vue'; import PolicyActionBuilder from './policy_action_builder.vue'; -import { DEFAULT_SCAN_RESULT_POLICY, fromYaml, toYaml } from './lib'; +import PolicyRuleBuilder from './policy_rule_builder.vue'; +import { DEFAULT_SCAN_RESULT_POLICY, fromYaml, toYaml, buildRule } from './lib'; export default { SECURITY_POLICY_ACTIONS, @@ -51,6 +52,7 @@ export default { GlFormTextarea, GlAlert, PolicyActionBuilder, + PolicyRuleBuilder, PolicyEditorLayout, DimDisableContainer, }, @@ -121,6 +123,15 @@ export default { updateAction(actionIndex, values) { this.policy.actions.splice(actionIndex, 1, values); }, + addRule() { + this.policy.rules.push(buildRule()); + }, + removeRule(ruleIndex) { + this.policy.rules.splice(ruleIndex, 1); + }, + updateRule(ruleIndex, values) { + this.policy.rules.splice(ruleIndex, 1, values); + }, handleError(error) { if (error.message.toLowerCase().includes('graphql')) { this.$emit('error', GRAPHQL_ERROR_MESSAGE); @@ -254,8 +265,17 @@ export default {
+ +
- + {{ $options.i18n.addRule }}
diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/lib/policy_rule_builder_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/lib/policy_rule_builder_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..d2dbe771eec25fba0cc8998b692a1f324a16ff6e --- /dev/null +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/lib/policy_rule_builder_spec.js @@ -0,0 +1,109 @@ +import { mount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import Api from 'ee/api'; +import PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/scan_result_policy/policy_rule_builder.vue'; +import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue'; + +describe('PolicyRuleBuilder', () => { + let wrapper; + + const PROTECTED_BRANCHES_MOCK = [{ id: 1, name: 'main' }]; + + const DEFAULT_RULE = { + type: 'scan_finding', + branches: [PROTECTED_BRANCHES_MOCK[0].name], + scanners: [], + vulnerabilities_allowed: 0, + severity_levels: [], + vulnerability_states: [], + }; + + const UPDATED_RULE = { + type: 'scan_finding', + branches: [PROTECTED_BRANCHES_MOCK[0].name], + scanners: ['dast'], + vulnerabilities_allowed: 1, + severity_levels: ['high'], + vulnerability_states: ['newly_detected'], + }; + + const factory = (propsData = {}) => { + wrapper = mount(PolicyRuleBuilder, { + propsData: { + initRule: DEFAULT_RULE, + ...propsData, + }, + provide: { + projectId: '1', + }, + }); + }; + + const findBranches = () => wrapper.findComponent(ProtectedBranchesSelector); + const findScanners = () => wrapper.find('[data-testid="scanners-select"]'); + const findSeverities = () => wrapper.find('[data-testid="severities-select"]'); + const findVulnStates = () => wrapper.find('[data-testid="vulnerability-states-select"]'); + const findVulnAllowed = () => wrapper.find('[data-testid="vulnerabilities-allowed-input"]'); + const findDeleteBtn = () => wrapper.findComponent(GlButton); + + beforeEach(() => { + jest + .spyOn(Api, 'projectProtectedBranches') + .mockReturnValue(Promise.resolve(PROTECTED_BRANCHES_MOCK)); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('initial rendering', () => { + it('renders one field for each attribute of the rule', async () => { + factory(); + await nextTick(); + + expect(findBranches().exists()).toBe(true); + expect(findScanners().exists()).toBe(true); + expect(findSeverities().exists()).toBe(true); + expect(findVulnStates().exists()).toBe(true); + expect(findVulnAllowed().exists()).toBe(true); + }); + + it('renders the delete buttom', async () => { + factory(); + await nextTick(); + + expect(findDeleteBtn().exists()).toBe(true); + }); + }); + + describe('when removing the rule', () => { + it('emits remove event', async () => { + factory(); + await nextTick(); + await findDeleteBtn().vm.$emit('click'); + + expect(wrapper.emitted().remove).toHaveLength(1); + }); + }); + + describe('when editing any attribute of the rule', () => { + it.each` + currentComponent | newValue | expected + ${findBranches} | ${PROTECTED_BRANCHES_MOCK[0]} | ${{ branches: UPDATED_RULE.branches }} + ${findScanners} | ${UPDATED_RULE.scanners} | ${{ scanners: UPDATED_RULE.scanners }} + ${findSeverities} | ${UPDATED_RULE.severity_levels} | ${{ severity_levels: UPDATED_RULE.severity_levels }} + ${findVulnStates} | ${UPDATED_RULE.vulnerability_states} | ${{ vulnerability_states: UPDATED_RULE.vulnerability_states }} + ${findVulnAllowed} | ${UPDATED_RULE.vulnerabilities_allowed} | ${{ vulnerabilities_allowed: UPDATED_RULE.vulnerabilities_allowed }} + `( + 'triggers a changed event (by $currentComponent) with the updated rule', + async ({ currentComponent, newValue, expected }) => { + factory(); + await nextTick(); + await currentComponent().vm.$emit('input', newValue); + + expect(wrapper.emitted().changed).toEqual([[expect.objectContaining(expected)]]); + }, + ); + }); +}); diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js index 610f99bfc81f49bfe85bfed25e4b6fdaa1a46fb4..3fc086ab042c2c5b1beea85e5e3c50970b3e4ec2 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js @@ -22,6 +22,7 @@ import { } from 'ee/threat_monitoring/components/policy_editor/constants'; import DimDisableContainer from 'ee/threat_monitoring/components/policy_editor/dim_disable_container.vue'; import PolicyActionBuilder from 'ee/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder.vue'; +import PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/scan_result_policy/policy_rule_builder.vue'; jest.mock('~/lib/utils/url_utility', () => ({ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, @@ -92,6 +93,7 @@ describe('ScanResultPolicyEditor', () => { const findEnableToggle = () => wrapper.findComponent(GlToggle); const findAllDisabledComponents = () => wrapper.findAllComponents(DimDisableContainer); const findYamlPreview = () => wrapper.find('[data-testid="yaml-preview"]'); + const findAllRuleBuilders = () => wrapper.findAllComponents(PolicyRuleBuilder); afterEach(() => { wrapper.destroy(); @@ -111,10 +113,11 @@ describe('ScanResultPolicyEditor', () => { expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(newManifest); }); - it('disables add rule button until feature is merged', async () => { + it('displays the inital rule and add rule button', async () => { await factory(); - expect(findAddRuleButton().props('disabled')).toBe(true); + expect(findAllRuleBuilders().length).toBe(1); + expect(findAddRuleButton().exists()).toBe(true); }); it('displays alert for invalid yaml', async () => { @@ -198,6 +201,60 @@ describe('ScanResultPolicyEditor', () => { ); }, ); + + it('adds a new rule', async () => { + const rulesCount = 1; + factory(); + await nextTick(); + + expect(findAllRuleBuilders().length).toBe(rulesCount); + + await findAddRuleButton().vm.$emit('click'); + + expect(findAllRuleBuilders()).toHaveLength(rulesCount + 1); + }); + + it('hides add button when the limit of five rules has been reached', async () => { + const limit = 5; + factory(); + await nextTick(); + await findAddRuleButton().vm.$emit('click'); + await findAddRuleButton().vm.$emit('click'); + await findAddRuleButton().vm.$emit('click'); + await findAddRuleButton().vm.$emit('click'); + + expect(findAllRuleBuilders()).toHaveLength(limit); + expect(findAddRuleButton().exists()).toBe(false); + }); + + it('updates an existing rule', async () => { + const newValue = { + type: 'scan_finding', + branches: [], + scanners: [], + vulnerabilities_allowed: 1, + severity_levels: [], + vulnerability_states: [], + }; + factory(); + await nextTick(); + await findAllRuleBuilders().at(0).vm.$emit('changed', newValue); + + expect(wrapper.vm.policy.rules[0]).toEqual(newValue); + expect(findYamlPreview().html()).toMatch('vulnerabilities_allowed: 1'); + }); + + it('deletes the initial rule', async () => { + const initialRuleCount = 1; + factory(); + await nextTick(); + + expect(findAllRuleBuilders()).toHaveLength(initialRuleCount); + + await findAllRuleBuilders().at(0).vm.$emit('remove', 0); + + expect(findAllRuleBuilders()).toHaveLength(initialRuleCount - 1); + }); }); describe('when a user is not an owner of the project', () => { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 37f8a3fd7b453994e303bb03a505f47ac5b7a7a9..8ee83196f8f8e4d73603d39e75b7a03c6c923eb8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31841,12 +31841,24 @@ msgstr "" msgid "Saving project." msgstr "" +msgid "ScanResultPolicy|%{ifLabelStart}if%{ifLabelEnd} %{scanners} scan in an open merge request targeting the %{branches} branch(es) finds %{vulnerabilitiesAllowed} or more %{severities} vulnerabilities that are %{vulnerabilityStates}" +msgstr "" + msgid "ScanResultPolicy|%{thenLabelStart}Then%{thenLabelEnd} Require approval from %{approvalsRequired} of the following approvers: %{approvers}" msgstr "" msgid "ScanResultPolicy|add an approver" msgstr "" +msgid "ScanResultPolicy|scanners" +msgstr "" + +msgid "ScanResultPolicy|severity levels" +msgstr "" + +msgid "ScanResultPolicy|vulnerability states" +msgstr "" + msgid "Scanner" msgstr ""