Commit 246e8194 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '273789-scan-execution-policy-page' into 'master'

Create scan execution policy page [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!63585
parents 23008948 b481209b
<script>
import {
fromYaml,
humanizeNetworkPolicy,
removeUnnecessaryDashes,
} from '../policy_editor/network_policy/lib';
import { removeUnnecessaryDashes } from '../../utils';
import { fromYaml, humanizeNetworkPolicy } from '../policy_editor/network_policy/lib';
import PolicyPreview from '../policy_editor/policy_preview.vue';
import BasePolicy from './base_policy.vue';
import PolicyInfoRow from './policy_info_row.vue';
......
<script>
import { GlLink } from '@gitlab/ui';
import { safeLoad } from 'js-yaml';
import { fromYaml } from '../policy_editor/scan_execution_policy/lib';
import BasePolicy from './base_policy.vue';
import PolicyInfoRow from './policy_info_row.vue';
......@@ -18,7 +18,7 @@ export default {
},
computed: {
policy() {
return safeLoad(this.value, { json: true });
return fromYaml(this.value);
},
},
};
......
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
export const EditorModeRule = 'rule';
export const EditorModeYAML = 'yaml';
export const EDITOR_MODE_RULE = 'rule';
export const EDITOR_MODE_YAML = 'yaml';
export const PARSING_ERROR_MESSAGE = s__(
'NetworkPolicies|Rule mode is unavailable for this policy. In some cases, we cannot parse the YAML file back into the rules editor.',
);
export const EDITOR_MODES = [
{ value: EDITOR_MODE_RULE, text: s__('NetworkPolicies|Rule mode') },
{ value: EDITOR_MODE_YAML, text: s__('NetworkPolicies|.yaml mode') },
];
export const POLICY_TYPES = {
networkPolicy: {
value: 'networkPolicy',
text: s__('NetworkPolicies|Network Policy'),
component: 'network-policy-editor',
shouldShowEnvironmentPicker: true,
},
scanExecution: {
value: 'scanExecution',
text: s__('NetworkPolicies|Scan Execution'),
component: 'scan-execution-policy-editor',
shouldShowMergeRequestButton: true,
},
};
export const DELETE_MODAL_CONFIG = {
id: 'delete-modal',
secondary: {
text: s__('NetworkPolicies|Delete policy'),
attributes: { variant: 'danger' },
},
cancel: {
text: __('Cancel'),
},
};
......@@ -165,16 +165,6 @@ const hasUnsupportedAttribute = (manifest) => {
return isUnsupported;
};
/**
* Removes inital line dashes from a policy YAML that is received from the API, which
* is not required for the user.
* @param {String} manifest the policy from the API request
* @returns {String} the policy without the initial dashes or the initial string
*/
export const removeUnnecessaryDashes = (manifest) => {
return manifest.replace('---\n', '');
};
/*
Construct a policy object expected by the policy editor from a yaml manifest.
Expected yaml structure is defined in the official documentation:
......
import { EndpointMatchModeAny } from './constants';
export * from './constants';
export { default as fromYaml, removeUnnecessaryDashes } from './from_yaml';
export { default as fromYaml } from './from_yaml';
export { default as humanizeNetworkPolicy } from './humanize';
export { buildRule } from './rules';
export { default as toYaml } from './to_yaml';
......
<script>
import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
import { mapActions } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentPicker from '../environment_picker.vue';
import { POLICY_TYPES } from './constants';
import NetworkPolicyEditor from './network_policy/network_policy_editor.vue';
import ScanExecutionPolicyEditor from './scan_execution_policy/scan_execution_policy_editor.vue';
export default {
components: {
......@@ -11,21 +13,15 @@ export default {
GlFormSelect,
EnvironmentPicker,
NetworkPolicyEditor,
ScanExecutionPolicyEditor,
},
mixins: [glFeatureFlagMixin()],
props: {
threatMonitoringPath: {
type: String,
required: true,
},
existingPolicy: {
type: Object,
required: false,
default: null,
},
projectId: {
type: String,
required: true,
},
},
data() {
return {
......@@ -36,6 +32,12 @@ export default {
policyComponent() {
return POLICY_TYPES[this.policyType].component;
},
shouldAllowPolicyTypeSelection() {
return this.glFeatures.scanExecutionPolicyUi;
},
shouldShowEnvironmentPicker() {
return POLICY_TYPES[this.policyType].shouldShowEnvironmentPicker;
},
},
created() {
this.fetchEnvironments();
......@@ -61,17 +63,12 @@ export default {
id="policyType"
:value="policyType"
:options="$options.policyTypes"
disabled
:disabled="!shouldAllowPolicyTypeSelection"
@change="updatePolicyType"
/>
</gl-form-group>
<environment-picker />
<environment-picker v-if="shouldShowEnvironmentPicker" />
</div>
<component
:is="policyComponent"
:threat-monitoring-path="threatMonitoringPath"
:existing-policy="existingPolicy"
:project-id="projectId"
/>
<component :is="policyComponent" :existing-policy="existingPolicy" />
</section>
</template>
<script>
import { GlButton, GlFormGroup, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { DELETE_MODAL_CONFIG, EDITOR_MODES, EDITOR_MODE_RULE, EDITOR_MODE_YAML } from './constants';
export default {
i18n: {
DELETE_MODAL_CONFIG,
},
components: {
GlButton,
GlFormGroup,
GlModal,
GlSegmentedControl,
PolicyYamlEditor: () =>
import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'),
},
directives: { GlModal: GlModalDirective },
inject: ['threatMonitoringPath'],
props: {
defaultEditorMode: {
type: String,
required: false,
default: EDITOR_MODE_RULE,
},
editorModes: {
type: Array,
required: false,
default: () => EDITOR_MODES,
},
isEditing: {
type: Boolean,
required: false,
default: false,
},
isRemovingPolicy: {
type: Boolean,
required: false,
default: false,
},
isUpdatingPolicy: {
type: Boolean,
required: false,
default: false,
},
policyName: {
type: String,
required: false,
default: '',
},
yamlEditorValue: {
type: String,
required: false,
default: '',
},
},
data() {
return {
selectedEditorMode: this.defaultEditorMode,
};
},
computed: {
deleteModalTitle() {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policyName });
},
saveButtonText() {
return this.isEditing
? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy');
},
shouldShowRuleEditor() {
return this.selectedEditorMode === EDITOR_MODE_RULE;
},
shouldShowYamlEditor() {
return this.selectedEditorMode === EDITOR_MODE_YAML;
},
},
methods: {
removePolicy() {
this.$emit('remove-policy');
},
savePolicy() {
this.$emit('save-policy', this.selectedEditorMode);
},
updateEditorMode(mode) {
this.selectedEditorMode = mode;
this.$emit('update-editor-mode', mode);
},
updateYaml(manifest) {
this.$emit('update-yaml', manifest);
},
},
};
</script>
<template>
<section>
<div class="gl-mb-5 gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base">
<gl-form-group
class="gl-px-5 gl-py-3 gl-mb-0 gl-bg-gray-10 gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
>
<gl-segmented-control
:options="editorModes"
:checked="selectedEditorMode"
@input="updateEditorMode"
/>
</gl-form-group>
<div class="gl-display-flex gl-sm-flex-direction-column">
<section class="gl-w-full gl-p-5 gl-flex-fill-4 policy-table-left">
<div v-if="shouldShowRuleEditor" data-testid="rule-editor">
<slot name="rule-editor"></slot>
</div>
<policy-yaml-editor
v-if="shouldShowYamlEditor"
data-testid="policy-yaml-editor"
:value="yamlEditorValue"
:read-only="false"
@input="updateYaml"
/>
</section>
<section
v-if="shouldShowRuleEditor"
class="gl-w-30p gl-p-5 gl-border-l-gray-100 gl-border-l-1 gl-border-l-solid gl-flex-fill-2"
data-testid="rule-editor-preview"
>
<slot name="rule-editor-preview"></slot>
</section>
</div>
</div>
<gl-button
type="submit"
variant="success"
data-testid="save-policy"
:loading="isUpdatingPolicy"
@click="savePolicy"
>
<slot name="save-button-text">
{{ saveButtonText }}
</slot>
</gl-button>
<gl-button
v-if="isEditing"
v-gl-modal="'delete-modal'"
category="secondary"
variant="danger"
data-testid="delete-policy"
:loading="isRemovingPolicy"
>{{ s__('NetworkPolicies|Delete policy') }}</gl-button
>
<gl-button category="secondary" :href="threatMonitoringPath">{{ __('Cancel') }}</gl-button>
<gl-modal
modal-id="delete-modal"
:title="deleteModalTitle"
:action-secondary="$options.i18n.DELETE_MODAL_CONFIG.secondary"
:action-cancel="$options.i18n.DELETE_MODAL_CONFIG.cancel"
@secondary="removePolicy"
>
{{
s__(
'NetworkPolicies|Are you sure you want to delete this policy? This action cannot be undone.',
)
}}
</gl-modal>
</section>
</template>
import { safeLoad } from 'js-yaml';
/*
Construct a policy object expected by the policy editor from a yaml manifest.
*/
export const fromYaml = (manifest) => {
return safeLoad(manifest, { json: true });
};
export { fromYaml } from './from_yaml';
export const DEFAULT_SCAN_EXECUTION_POLICY = `type: scan_execution_policy
name: ''
description: ''
enabled: false
rules:
- type: pipeline
branches:
- main
actions:
- scan: dast
site_profile: ''
scanner_profile: ''
`;
<script>
import { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
import { __ } from '~/locale';
import { EDITOR_MODES, EDITOR_MODE_YAML } from '../constants';
import PolicyEditorLayout from '../policy_editor_layout.vue';
import { DEFAULT_SCAN_EXECUTION_POLICY, fromYaml } from './lib';
export default {
DEFAULT_EDITOR_MODE: EDITOR_MODE_YAML,
EDITOR_MODES: [EDITOR_MODES[1]],
i18n: {
createMergeRequest: __('Create merge request'),
},
components: {
PolicyEditorLayout,
},
inject: ['threatMonitoringPath', 'projectId'],
props: {
existingPolicy: {
type: Object,
required: false,
default: null,
},
},
data() {
const policy = this.existingPolicy
? fromYaml(this.existingPolicy.manifest)
: fromYaml(DEFAULT_SCAN_EXECUTION_POLICY);
const yamlEditorValue = this.existingPolicy
? removeUnnecessaryDashes(this.existingPolicy.manifest)
: DEFAULT_SCAN_EXECUTION_POLICY;
return {
policy,
yamlEditorValue,
};
},
computed: {
isEditing() {
return Boolean(this.existingPolicy);
},
},
methods: {
updateYaml(manifest) {
this.yamlEditorValue = manifest;
},
},
};
</script>
<template>
<policy-editor-layout
:default-editor-mode="$options.DEFAULT_EDITOR_MODE"
:editor-modes="$options.EDITOR_MODES"
:is-editing="isEditing"
:policy-name="policy.name"
:yaml-editor-value="yamlEditorValue"
@update-yaml="updateYaml"
>
<template #save-button-text>
{{ $options.i18n.createMergeRequest }}
</template>
</policy-editor-layout>
</template>
......@@ -36,10 +36,7 @@ export default () => {
store.dispatch('threatMonitoring/setCurrentEnvironmentId', parseInt(environmentId, 10));
}
const props = { threatMonitoringPath, projectId };
if (policy) {
props.existingPolicy = JSON.parse(policy);
}
const props = policy ? { existingPolicy: JSON.parse(policy) } : {};
return new Vue({
el,
......@@ -47,7 +44,9 @@ export default () => {
provide: {
configureAgentHelpPath,
createAgentHelpPath,
projectId,
projectPath,
threatMonitoringPath,
},
store,
render(createElement) {
......
......@@ -9,3 +9,13 @@ export const getContentWrapperHeight = (contentWrapperClass) => {
const wrapperEl = document.querySelector(contentWrapperClass);
return wrapperEl ? `${wrapperEl.offsetTop}px` : '';
};
/**
* Removes inital line dashes from a policy YAML that is received from the API, which
* is not required for the user.
* @param {String} manifest the policy from the API request
* @returns {String} the policy without the initial dashes or the initial string
*/
export const removeUnnecessaryDashes = (manifest) => {
return manifest.replace('---\n', '');
};
......@@ -6,6 +6,10 @@ module Projects
before_action :authorize_read_threat_monitoring!
before_action do
push_frontend_feature_flag(:scan_execution_policy_ui, @project)
end
feature_category :web_firewall
# rubocop: disable CodeReuse/ActiveRecord
......
---
name: scan_execution_policy_ui
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63585
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273791
milestone: '14.0'
type: development
group: group::container security
default_enabled: false
......@@ -11,7 +11,6 @@ import {
RuleTypeFQDN,
EntityTypes,
fromYaml,
removeUnnecessaryDashes,
buildRule,
toYaml,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
......@@ -332,15 +331,3 @@ spec:
);
});
});
describe('removeUnnecessaryDashes', () => {
it.each`
input | output
${'---\none'} | ${'one'}
${'two'} | ${'two'}
${'--\nthree'} | ${'--\nthree'}
${'four---\n'} | ${'four'}
`('returns $output when used on $input', ({ input, output }) => {
expect(removeUnnecessaryDashes(input)).toBe(output);
});
});
import { GlModal, GlSegmentedControl } from '@gitlab/ui';
import { EDITOR_MODE_YAML } from 'ee/threat_monitoring/components/policy_editor/constants';
import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('PolicyEditorLayout component', () => {
let wrapper;
const threatMonitoringPath = '/threat-monitoring';
const factory = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(PolicyEditorLayout, {
propsData: {
...propsData,
},
provide: {
threatMonitoringPath,
},
stubs: { PolicyYamlEditor: true },
});
};
const findDeletePolicyButton = () => wrapper.findByTestId('delete-policy');
const findDeletePolicyModal = () => wrapper.findComponent(GlModal);
const findEditorModeToggle = () => wrapper.findComponent(GlSegmentedControl);
const findYamlModeSection = () => wrapper.findByTestId('policy-yaml-editor');
const findRuleModeSection = () => wrapper.findByTestId('rule-editor');
const findRuleModePreviewSection = () => wrapper.findByTestId('rule-editor-preview');
const findSavePolicyButton = () => wrapper.findByTestId('save-policy');
afterEach(() => {
wrapper.destroy();
});
describe('default behavior', () => {
beforeEach(() => {
factory();
});
it.each`
component | status | findComponent | state
${'editor mode toggle'} | ${'does display'} | ${findEditorModeToggle} | ${true}
${'delete button'} | ${'does not display'} | ${findDeletePolicyButton} | ${false}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('does display the correct save button text when creating a new policy', () => {
const saveButton = findSavePolicyButton();
expect(saveButton.exists()).toBe(true);
expect(saveButton.text()).toBe('Create policy');
});
it('emits properly with the current mode when the save button is clicked', () => {
findSavePolicyButton().vm.$emit('click');
expect(wrapper.emitted('save-policy')).toStrictEqual([['rule']]);
});
it('mode changes appropriately when new mode is selected', async () => {
expect(findRuleModeSection().exists()).toBe(true);
expect(findYamlModeSection().exists()).toBe(false);
await findEditorModeToggle().vm.$emit('input', EDITOR_MODE_YAML);
expect(findRuleModeSection().exists()).toBe(false);
expect(findYamlModeSection().exists()).toBe(true);
expect(wrapper.emitted('update-editor-mode')).toStrictEqual([[EDITOR_MODE_YAML]]);
});
it('does display custom save button text', () => {
const saveButton = findSavePolicyButton();
expect(saveButton.exists()).toBe(true);
expect(saveButton.text()).toBe('Create policy');
});
});
describe('editing a policy', () => {
beforeEach(() => {
factory({ propsData: { isEditing: true } });
});
it('does not emit when the delete button is clicked', () => {
findDeletePolicyButton().vm.$emit('click');
expect(wrapper.emitted('remove-policy')).toStrictEqual(undefined);
});
it('emits properly when the delete modal is closed', () => {
findDeletePolicyModal().vm.$emit('secondary');
expect(wrapper.emitted('remove-policy')).toStrictEqual([[]]);
});
});
describe('rule mode', () => {
beforeEach(() => {
factory();
});
it.each`
component | status | findComponent | state
${'rule mode section'} | ${'does display'} | ${findRuleModeSection} | ${true}
${'rule mode preview section'} | ${'does display'} | ${findRuleModePreviewSection} | ${true}
${'yaml mode section'} | ${'does not display'} | ${findYamlModeSection} | ${false}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
});
describe('yaml mode', () => {
beforeEach(() => {
factory({ propsData: { defaultEditorMode: EDITOR_MODE_YAML } });
});
it.each`
component | status | findComponent | state
${'rule mode section'} | ${'does not display'} | ${findRuleModeSection} | ${false}
${'rule mode preview section'} | ${'does not display'} | ${findRuleModePreviewSection} | ${false}
${'yaml mode section'} | ${'does display'} | ${findYamlModeSection} | ${true}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('emits properly when yaml is updated', () => {
const newManifest = 'new yaml!';
findYamlModeSection().vm.$emit('input', newManifest);
expect(wrapper.emitted('update-yaml')).toStrictEqual([[newManifest]]);
});
});
});
......@@ -14,7 +14,7 @@ describe('PolicyEditor component', () => {
const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const findNeworkPolicyEditor = () => wrapper.findComponent(NetworkPolicyEditor);
const factory = ({ propsData } = {}) => {
const factory = ({ propsData = {}, provide = {} } = {}) => {
store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
......@@ -25,18 +25,19 @@ describe('PolicyEditor component', () => {
projectId: '21',
...propsData,
},
provide,
store,
stubs: { GlFormSelect },
});
};
beforeEach(factory);
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(factory);
it('renders the environment picker', () => {
expect(findEnvironmentPicker().exists()).toBe(true);
});
......@@ -45,10 +46,23 @@ describe('PolicyEditor component', () => {
const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true);
expect(formSelect.attributes('value')).toBe(POLICY_TYPES.networkPolicy.value);
expect(formSelect.attributes('disabled')).toBe('true');
});
it('renders the "NetworkPolicyEditor" component', () => {
expect(findNeworkPolicyEditor().exists()).toBe(true);
});
});
describe('with "scanExecutionPolicyUi" feature flag enabled', () => {
beforeEach(() => {
factory({ provide: { glFeatures: { scanExecutionPolicyUi: true } } });
});
it('renders the form select', () => {
const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true);
expect(formSelect.attributes('disabled')).toBe(undefined);
});
});
});
import {
DEFAULT_SCAN_EXECUTION_POLICY,
fromYaml,
} from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib';
describe('fromYaml', () => {
it('returns policy object', () => {
expect(fromYaml(DEFAULT_SCAN_EXECUTION_POLICY)).toMatchObject({
name: '',
description: '',
enabled: false,
actions: [{ scan: 'dast', site_profile: '', scanner_profile: '' }],
rules: [{ branches: ['main'], type: 'pipeline' }],
});
});
});
import { shallowMount } from '@vue/test-utils';
import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue';
import { DEFAULT_SCAN_EXECUTION_POLICY } from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib';
import ScanExecutionPolicyEditor from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/scan_execution_policy_editor.vue';
describe('ScanExecutionPolicyEditor', () => {
let wrapper;
const factory = ({ propsData = {} } = {}) => {
wrapper = shallowMount(ScanExecutionPolicyEditor, {
propsData,
provide: {
threatMonitoringPath: '',
projectId: 1,
},
});
};
const findPolicyEditorLayout = () => wrapper.findComponent(PolicyEditorLayout);
beforeEach(() => {
factory();
});
afterEach(() => {
wrapper.destroy();
});
it('updates the policy yaml when "update-yaml" is emitted', async () => {
const newManifest = 'new yaml!';
expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(
DEFAULT_SCAN_EXECUTION_POLICY,
);
await findPolicyEditorLayout().vm.$emit('update-yaml', newManifest);
expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(newManifest);
});
});
......@@ -181,3 +181,40 @@ export const mockAlertDetails = {
title: 'dropingress',
monitorTool: 'Cilium',
};
export const mockL7Manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: limit-inbound-ip
spec:
endpointSelector: {}
ingress:
- toPorts:
- ports:
- port: '80'
protocol: TCP
- port: '443'
protocol: TCP
rules:
http:
- headers:
- 'X-Forwarded-For: 192.168.1.1'
fromEntities:
- cluster`;
export const mockL3Manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
description: test description
metadata:
name: test-policy
labels:
app.gitlab.com/proj: '21'
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
foo: bar
ingress:
- fromEndpoints:
- matchLabels:
foo: bar`;
import { getContentWrapperHeight } from 'ee/threat_monitoring/utils';
import { getContentWrapperHeight, removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
import { setHTMLFixture } from 'helpers/fixtures';
describe('Threat Monitoring Utils', () => {
......@@ -23,4 +23,16 @@ describe('Threat Monitoring Utils', () => {
expect(getContentWrapperHeight('.does-not-exist')).toBe('');
});
});
describe('removeUnnecessaryDashes', () => {
it.each`
input | output
${'---\none'} | ${'one'}
${'two'} | ${'two'}
${'--\nthree'} | ${'--\nthree'}
${'four---\n'} | ${'four'}
`('returns $output when used on $input', ({ input, output }) => {
expect(removeUnnecessaryDashes(input)).toBe(output);
});
});
});
......@@ -21791,6 +21791,9 @@ msgstr ""
msgid "NetworkPolicies|Save changes"
msgstr ""
msgid "NetworkPolicies|Scan Execution"
msgstr ""
msgid "NetworkPolicies|Something went wrong, failed to update policy"
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