diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/utils.js b/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/utils.js index 57ba57d758c69d857c7f204ac8aba9219e41ef7d..c69655c20da1abcdc5248ae88f8095092c8522e9 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/utils.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/utils.js @@ -9,6 +9,8 @@ import { SCAN_RESULT_BRANCH_TYPE_OPTIONS, GREATER_THAN_OPERATOR, LESS_THAN_OPERATOR, + MATCH_ON_INCLUSION_LICENSE, + MATCH_ON_INCLUSION, } from '../../policy_editor/constants'; import { createHumanizedScanners } from '../../policy_editor/utils'; import { @@ -292,9 +294,12 @@ const humanizeRule = (rule) => { const branchExceptions = humanizedBranchExceptions(rule.branch_exceptions); const branchExceptionsString = buildBranchExceptionsString(rule.branch_exceptions); + const MATCH_LICENSE_KEY = gon?.features?.securityPoliciesBreakingChanges + ? MATCH_ON_INCLUSION_LICENSE + : MATCH_ON_INCLUSION; if (rule.type === LICENSE_FINDING) { - const summaryText = rule.match_on_inclusion + const summaryText = rule[MATCH_LICENSE_KEY] ? s__( 'SecurityOrchestration|When license scanner finds any license matching %{licenses}%{detection} in an open merge request %{targeting}%{branches}%{branchExceptionsString}', ) diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js index 51e45316899797bf8603d1f80453bcd2c55075f9..5a42a4a444b7bafada8ddb6dc8386a59e5e3bb3d 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js @@ -40,6 +40,9 @@ export const DELETE_MODAL_CONFIG = { }, }; +export const MATCH_ON_INCLUSION = 'match_on_inclusion'; +export const MATCH_ON_INCLUSION_LICENSE = 'match_on_inclusion_license'; + export const DEFAULT_MR_TITLE = s__('SecurityOrchestration|Update scan policies'); export const POLICY_RUN_TIME_MESSAGE = s__( diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js index 1060408a81eba8b314dd54e3783713764645d03c..5421acf6dbc1259d4277ac5d61241f5c9c9f5dc7 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js @@ -1,7 +1,11 @@ import { safeLoad } from 'js-yaml'; import { isBoolean, isEqual } from 'lodash'; import { addIdsToPolicy, hasInvalidKey, isValidPolicy } from '../../utils'; -import { PRIMARY_POLICY_KEYS } from '../../constants'; +import { + MATCH_ON_INCLUSION, + MATCH_ON_INCLUSION_LICENSE, + PRIMARY_POLICY_KEYS, +} from '../../constants'; import { VALID_APPROVAL_SETTINGS, PERMITTED_INVALID_SETTINGS, @@ -27,6 +31,9 @@ export const fromYaml = ({ manifest, validateRuleMode = false }) => { const hasPolicyScope = gon?.features?.securityPoliciesPolicyScope || gon?.features?.securityPoliciesPolicyScopeProject; + const MATCH_LICENSE_KEY = gon?.features?.securityPoliciesBreakingChanges + ? MATCH_ON_INCLUSION_LICENSE + : MATCH_ON_INCLUSION; const primaryKeys = [...PRIMARY_POLICY_KEYS, ...(hasPolicyScope ? ['policy_scope'] : [])]; const rulesKeys = [ @@ -37,7 +44,6 @@ export const fromYaml = ({ manifest, validateRuleMode = false }) => { 'commits', 'license_states', 'license_types', - 'match_on_inclusion', 'scanners', 'severity_levels', 'vulnerabilities_allowed', @@ -45,6 +51,7 @@ export const fromYaml = ({ manifest, validateRuleMode = false }) => { 'vulnerability_age', 'vulnerability_attributes', 'id', + MATCH_LICENSE_KEY, ]; const actionsKeys = [ 'type', diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/rules.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/rules.js index 8762987bb97039c477f27946196a4e58271039ca..5db2c1d7429d05917bd37f4824efbc40abea005b 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/rules.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/rules.js @@ -8,6 +8,8 @@ import { ANY_COMMIT, BRANCH_TYPE_KEY, INVALID_PROTECTED_BRANCHES, + MATCH_ON_INCLUSION, + MATCH_ON_INCLUSION_LICENSE, VALID_SCAN_RESULT_BRANCH_TYPE_OPTIONS, VULNERABILITY_AGE_OPERATORS, } from 'ee/security_orchestration/components/policy_editor/constants'; @@ -51,14 +53,20 @@ export const securityScanBuildRule = () => ({ branch_type: ALL_PROTECTED_BRANCHES.value, }); -export const licenseScanBuildRule = () => ({ - id: uniqueId('rule_'), - type: LICENSE_FINDING, - match_on_inclusion: true, - license_types: [], - license_states: [], - branch_type: ALL_PROTECTED_BRANCHES.value, -}); +export const licenseScanBuildRule = () => { + const MATCH_KEY = gon?.features?.securityPoliciesBreakingChanges + ? MATCH_ON_INCLUSION_LICENSE + : MATCH_ON_INCLUSION; + + return { + id: uniqueId('rule_'), + type: LICENSE_FINDING, + [MATCH_KEY]: true, + license_types: [], + license_states: [], + branch_type: ALL_PROTECTED_BRANCHES.value, + }; +}; export const anyMergeRequestBuildRule = () => ({ id: uniqueId('rule_'), diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter.vue index 5d17216d0f7218155f2aaf4aefae80581b64716a..8b941b7d6c538beecf57048980979efb64e1fdc1 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter.vue @@ -3,6 +3,11 @@ import { GlCollapsibleListbox } from '@gitlab/ui'; import { sprintf, __, s__ } from '~/locale'; import { parseBoolean } from '~/lib/utils/common_utils'; import SectionLayout from 'ee/security_orchestration/components/policy_editor/section_layout.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { + MATCH_ON_INCLUSION, + MATCH_ON_INCLUSION_LICENSE, +} from 'ee/security_orchestration/components/policy_editor/constants'; import { EXCEPT, MATCHING } from '../../lib/rules'; import { UNKNOWN_LICENSE } from './constants'; @@ -30,6 +35,7 @@ export default { SectionLayout, GlCollapsibleListbox, }, + mixins: [glFeatureFlagsMixin()], inject: ['parsedSoftwareLicenses'], props: { initRule: { @@ -55,6 +61,11 @@ export default { return this.allLicenses; }, + matchTypeKey() { + return this.glFeatures.securityPoliciesBreakingChanges + ? MATCH_ON_INCLUSION_LICENSE + : MATCH_ON_INCLUSION; + }, licenseTypes: { get() { return this.initRule.license_types; @@ -65,10 +76,10 @@ export default { }, matchType: { get() { - return this.initRule.match_on_inclusion?.toString(); + return this.initRule?.[this.matchTypeKey]?.toString(); }, set(value) { - this.triggerChanged({ match_on_inclusion: parseBoolean(value) }); + this.triggerChanged({ [this.matchTypeKey]: parseBoolean(value) }); }, }, matchTypeToggleText() { diff --git a/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/utils_spec.js b/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/utils_spec.js index a57fe204336687c87ad19e831c067730d65b2f53..66e7f2e45aec74aa50a2e6246700c817976b5e95 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/utils_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/utils_spec.js @@ -272,6 +272,12 @@ const branchExceptionLicenseScanRule = (branchExceptions = []) => ({ } except branches:`, branchExceptions: ['test', 'test1'], }, + humanized_match_on_inclusion_license: { + summary: `When license scanner finds any license matching CMU License, CNRI Jython License and CNRI Python License in an open merge request targeting ${ + HUMANIZED_BRANCH_TYPE_TEXT_DICT[PROJECT_DEFAULT_BRANCH.value] + } except branches:`, + branchExceptions: ['test', 'test1'], + }, }); describe('humanizeRules', () => { @@ -366,6 +372,21 @@ describe('humanizeRules', () => { humanizeRules([branchExceptionLicenseScanRule(['test', 'test1']).rule]), ).toStrictEqual([branchExceptionLicenseScanRule(['test', 'test1']).humanized]); }); + + it('returns a single rule as a human-readable string when breaking changes flag is enabled', () => { + window.gon = { features: { securityPoliciesBreakingChanges: true } }; + + expect( + humanizeRules([ + { + ...branchExceptionLicenseScanRule(['test', 'test1']).rule, + match_on_inclusion_license: true, + }, + ]), + ).toStrictEqual([ + branchExceptionLicenseScanRule(['test', 'test1']).humanized_match_on_inclusion_license, + ]); + }); }); describe('any merge request rule', () => { diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/rules_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/rules_spec.js index ad538b55caaef0b5cd8dd6474fc7709cfc527914..3e129eafa92719926cef0530a31b60c7742a017b 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/rules_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/rules_spec.js @@ -12,6 +12,7 @@ import { invalidVulnerabilityAttributes, VULNERABILITY_STATE_KEYS, humanizeInvalidBranchesError, + licenseScanBuildRule, } from 'ee/security_orchestration/components/policy_editor/scan_result/lib/rules'; import { APPROVAL_VULNERABILITY_STATES, @@ -22,6 +23,8 @@ import { import { ANY_OPERATOR, GREATER_THAN_OPERATOR, + MATCH_ON_INCLUSION, + MATCH_ON_INCLUSION_LICENSE, } from 'ee/security_orchestration/components/policy_editor/constants'; describe('invalidScanners', () => { @@ -268,4 +271,18 @@ describe('invalidVulnerabilityAttributes', () => { `('returns $expectedResult', ({ rules, expectedResult }) => { expect(invalidVulnerabilityAttributes(rules)).toStrictEqual(expectedResult); }); + + describe('licenseScanBuildRule', () => { + it.each([false, true])( + 'creates license rule with different match option based on flag', + (securityPoliciesBreakingChanges) => { + window.gon = { features: { securityPoliciesBreakingChanges } }; + const MATCH_KEY = securityPoliciesBreakingChanges + ? MATCH_ON_INCLUSION_LICENSE + : MATCH_ON_INCLUSION; + + expect(licenseScanBuildRule()).toEqual(expect.objectContaining({ [MATCH_KEY]: true })); + }, + ); + }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter_spec.js index b596067ccf5df8351be9a2384a9166338ac5a812..9ef7a186127118f05779342f99b2de200e64e60a 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/rule/scan_filters/license_filter_spec.js @@ -20,13 +20,14 @@ describe('LicenseFilter', () => { const parsedSoftwareLicenses = [APACHE_LICENSE, MIT_LICENSE].map((l) => ({ text: l, value: l })); const allLicenses = [...parsedSoftwareLicenses, UNKNOWN_LICENSE]; - const createComponent = (props = DEFAULT_PROPS) => { + const createComponent = ({ props = DEFAULT_PROPS, provide = {} } = {}) => { wrapper = shallowMountExtended(LicenseFilter, { propsData: { ...props, }, provide: { parsedSoftwareLicenses, + ...provide, }, stubs: { SectionLayout, @@ -74,13 +75,13 @@ describe('LicenseFilter', () => { describe('updated rule', () => { it('displays the toggle text properly with a single license selected', () => { - createComponent({ initRule: UPDATED_RULE([MIT_LICENSE]) }); + createComponent({ props: { initRule: UPDATED_RULE([MIT_LICENSE]) } }); const listBox = findLicenseTypeListBox(); expect(listBox.props('toggleText')).toBe(MIT_LICENSE); }); it('displays the toggle text properly with multiple licenses selected', () => { - createComponent({ initRule: UPDATED_RULE([MIT_LICENSE, APACHE_LICENSE]) }); + createComponent({ props: { initRule: UPDATED_RULE([MIT_LICENSE, APACHE_LICENSE]) } }); const listBox = findLicenseTypeListBox(); expect(listBox.props('toggleText')).toBe('2 licenses'); }); @@ -117,4 +118,22 @@ describe('LicenseFilter', () => { }); }); }); + + describe('new filter value match_on_inclusion_license', () => { + it('selects updated status value when feature flag is enabled', () => { + createComponent({ + provide: { + glFeatures: { + securityPoliciesBreakingChanges: true, + }, + }, + }); + + const matchType = true; + findMatchTypeListBox().vm.$emit('select', matchType); + expect(wrapper.emitted('changed')).toStrictEqual([ + [{ match_on_inclusion_license: matchType }], + ]); + }); + }); }); diff --git a/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/rules_spec.js b/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/rules_spec.js index b85d21ff1ed6630544123a6da6fa1ec5b73f1521..aba0cea3138478efd362fdc83e6dbd06bc21cb83 100644 --- a/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/rules_spec.js +++ b/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/rules_spec.js @@ -80,19 +80,34 @@ describe('Scan result policy rules', () => { }); describe('license rule', () => { + const verifyRuleMode = () => { + expect(findDefaultRuleBuilder().exists()).toBe(false); + expect(findLicenseScanRuleBuilder().exists()).toBe(true); + expect(findSettingsSection().exists()).toBe(true); + }; + beforeEach(() => { createWrapper(); }); it('should select license rule', async () => { - const verifyRuleMode = () => { - expect(findDefaultRuleBuilder().exists()).toBe(false); - expect(findLicenseScanRuleBuilder().exists()).toBe(true); - expect(findSettingsSection().exists()).toBe(true); - }; await findScanTypeSelect().vm.$emit('select', LICENSE_FINDING); await verify({ manifest: mockLicenseApprovalManifest, verifyRuleMode, wrapper }); }); + + it('should select license rule after breaking changes for match on inclusion license', async () => { + window.gon = { features: { securityPoliciesBreakingChanges: true } }; + + await findScanTypeSelect().vm.$emit('select', LICENSE_FINDING); + await verify({ + manifest: mockLicenseApprovalManifest.replace( + 'match_on_inclusion', + 'match_on_inclusion_license', + ), + verifyRuleMode, + wrapper, + }); + }); }); describe('any merge request rule', () => {