Commit 7a415b19 authored by Alexander Turinske's avatar Alexander Turinske Committed by Natalia Tepluhina

Add policy alert picker

- hide it behind threatMonitoringAlerts feature flag
- update to_yaml file to handle annotations
- create alert-picker component to allow for picker creation
- on alert addition, update yaml file to include correct
  url and status
- show warning for alert capacity
- add tests
- update tests that improperly use gon
parent 3b140ad4
......@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import { GlAlert, GlEmptyState, GlIcon, GlLink, GlPopover, GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Alerts from './alerts/alerts.vue';
import ThreatMonitoringFilters from './threat_monitoring_filters.vue';
import ThreatMonitoringSection from './threat_monitoring_section.vue';
......@@ -24,6 +25,7 @@ export default {
NetworkPolicyList,
},
inject: ['documentationPath'],
mixins: [glFeatureFlagsMixin()],
props: {
defaultEnvironmentId: {
type: Number,
......@@ -75,7 +77,7 @@ export default {
},
computed: {
showAlertsTab() {
return gon.features.threatMonitoringAlerts;
return this.glFeatures.threatMonitoringAlerts;
},
},
created() {
......
......@@ -113,7 +113,7 @@ function parseRule(item, direction) {
*/
export default function fromYaml(manifest) {
const { description, metadata, spec } = safeLoad(manifest, { json: true });
const { name, resourceVersion } = metadata;
const { name, resourceVersion, annotations } = metadata;
const { endpointSelector = {}, ingress = [], egress = [] } = spec;
const matchLabels = endpointSelector.matchLabels || {};
......@@ -134,6 +134,7 @@ export default function fromYaml(manifest) {
name,
resourceVersion,
description,
annotations,
isEnabled: !Object.keys(matchLabels).includes(DisabledByLabel),
endpointMatchMode: endpointLabels.length > 0 ? EndpointMatchModeLabel : EndpointMatchModeAny,
endpointLabels: endpointLabels.join(' '),
......
......@@ -33,8 +33,11 @@ function spec({ rules, isEnabled, endpointMatchMode, endpointLabels }) {
Return yaml representation of a policy.
*/
export default function toYaml(policy) {
const { name, resourceVersion, description } = policy;
const { annotations, name, resourceVersion, description } = policy;
const metadata = { name };
if (annotations) {
metadata.annotations = annotations;
}
if (resourceVersion) {
metadata.resourceVersion = resourceVersion;
}
......
<script>
import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
ACTION: s__(
'NetworkPolicies|%{labelStart}And%{labelEnd} %{spanStart}send an Alert to GitLab.%{spanEnd}',
),
BUTTON_LABEL: s__('NetworkPolicies|+ Add alert'),
HIGH_VOLUME_WARNING: s__(
`NetworkPolicies|Alerts are intended to be selectively used for a limited number of events that are potentially concerning and warrant a manual review. Alerts should not be used as a substitute for a SIEM or a logging tool. High volume alerts are likely to be dropped so as to preserve the stability of GitLab's integration with Kubernetes.`,
),
},
components: {
GlAlert,
GlButton,
GlSprintf,
},
props: {
policyAlert: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div>
<gl-alert v-if="policyAlert" variant="warning" :dismissible="false" class="gl-mt-5">
{{ $options.i18n.HIGH_VOLUME_WARNING }}
</gl-alert>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-5"
:class="{ 'gl-mt-5': !policyAlert }"
>
<gl-button
v-if="!policyAlert"
variant="link"
category="primary"
data-testid="add-alert"
@click="$emit('update-alert', !policyAlert)"
>
{{ $options.i18n.BUTTON_LABEL }}
</gl-button>
<div
v-else
class="gl-w-full gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<span>
<gl-sprintf :message="$options.i18n.ACTION">
<template #label="{ content }">
<label for="actionType" class="text-uppercase gl-font-lg gl-mr-4 gl-mb-0">{{
content
}}</label>
</template>
<template #span="{ content }">
<span>{{ content }}</span>
</template>
</gl-sprintf>
</span>
<gl-button
data-testid="remove-alert"
icon="remove"
category="tertiary"
@click="$emit('update-alert', !policyAlert)"
/>
</div>
</div>
</div>
</template>
......@@ -14,11 +14,13 @@ import {
} from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentPicker from '../environment_picker.vue';
import NetworkPolicyEditor from '../network_policy_editor.vue';
import PolicyRuleBuilder from './policy_rule_builder.vue';
import PolicyPreview from './policy_preview.vue';
import PolicyActionPicker from './policy_action_picker.vue';
import PolicyAlertPicker from './policy_alert_picker.vue';
import DimDisableContainer from './dim_disable_container.vue';
import {
EditorModeRule,
......@@ -47,9 +49,11 @@ export default {
PolicyRuleBuilder,
PolicyPreview,
PolicyActionPicker,
PolicyAlertPicker,
DimDisableContainer,
},
directives: { GlModal: GlModalDirective },
mixins: [glFeatureFlagsMixin()],
props: {
threatMonitoringPath: {
type: String,
......@@ -71,6 +75,7 @@ export default {
endpointMatchMode: EndpointMatchModeAny,
endpointLabels: '',
rules: [],
annotations: '',
};
return {
......@@ -84,6 +89,9 @@ export default {
humanizedPolicy() {
return humanizeNetworkPolicy(this.policy);
},
policyAlert() {
return Boolean(this.policy.annotations);
},
policyYaml() {
return toYaml(this.policy);
},
......@@ -111,6 +119,9 @@ export default {
? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy');
},
showAlertsPicker() {
return this.glFeatures.threatMonitoringAlerts;
},
deleteModalTitle() {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policy.name });
},
......@@ -124,6 +135,9 @@ export default {
addRule() {
this.policy.rules.push(buildRule(RuleTypeEndpoint));
},
handleAlertUpdate(includeAlert) {
this.policy.annotations = includeAlert ? { 'app.gitlab.com/alert': 'true' } : '';
},
updateEndpointMatchMode(mode) {
this.policy.endpointMatchMode = mode;
},
......@@ -308,6 +322,11 @@ export default {
</template>
<policy-action-picker />
<policy-alert-picker
v-if="showAlertsPicker"
:policy-alert="policyAlert"
@update-alert="handleAlertUpdate"
/>
</dim-disable-container>
</div>
<div class="col-sm-12 col-md-6 col-lg-5 col-xl-4">
......
......@@ -234,7 +234,7 @@ export default {
<gl-button
icon="remove"
size="small"
category="tertiary"
class="gl-absolute gl-top-3 gl-right-3"
data-testid="remove-rule"
@click="$emit('remove')"
......
......@@ -24,9 +24,8 @@ const userCalloutsPath = `${TEST_HOST}/user_callouts`;
describe('ThreatMonitoringApp component', () => {
let store;
let wrapper;
window.gon = { features: {} };
const factory = ({ propsData, state, options } = {}) => {
const factory = ({ propsData, provide = {}, state, options } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
environmentsEndpoint,
......@@ -52,6 +51,8 @@ describe('ThreatMonitoringApp component', () => {
},
provide: {
documentationPath,
glFeatures: { threatMonitoringAlerts: false },
...provide,
},
store,
...options,
......@@ -68,7 +69,6 @@ describe('ThreatMonitoringApp component', () => {
const findAlertTab = () => wrapper.find('[data-testid="threat-monitoring-alerts-tab"]');
afterEach(() => {
window.gon.features = {};
wrapper.destroy();
wrapper = null;
});
......@@ -176,8 +176,7 @@ describe('ThreatMonitoringApp component', () => {
describe('alerts tab', () => {
beforeEach(() => {
window.gon.features.threatMonitoringAlerts = true;
factory({});
factory({ provide: { glFeatures: { threatMonitoringAlerts: true } } });
});
it('shows the alerts tab', () => {
expect(findAlertTab().exists()).toBe(true);
......
......@@ -178,6 +178,8 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = `
>
<policy-action-picker-stub />
<!---->
</dim-disable-container-stub>
</div>
......
......@@ -288,4 +288,24 @@ spec:
});
});
});
describe('when annotations is not empty', () => {
it('returns policy object', () => {
const manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
annotations:
app.gitlab.com/alert: 'true'
spec:
endpointSelector:
matchLabels:
{}
`;
expect(fromYaml(manifest)).toMatchObject({
annotations: { 'app.gitlab.com/alert': 'true' },
});
});
});
});
......@@ -21,6 +21,26 @@ spec:
`);
});
describe('when annotations is not empty', () => {
beforeEach(() => {
policy.annotations = { 'test annotation': true };
});
it('returns yaml representation', () => {
expect(toYaml(policy)).toEqual(`apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
annotations:
test annotation: true
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
`);
});
});
describe('when description is not empty', () => {
beforeEach(() => {
policy.description = 'test description';
......
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf } from '@gitlab/ui';
import PolicyAlertPicker from 'ee/threat_monitoring/components/policy_editor/policy_alert_picker.vue';
describe('PolicyAlertPicker component', () => {
let wrapper;
const defaultProps = { policyAlert: false };
const findAddAlertButton = () => wrapper.find("[data-testid='add-alert']");
const findGlAlert = () => wrapper.find(GlAlert);
const findGlSprintf = () => wrapper.find(GlSprintf);
const findRemoveAlertButton = () => wrapper.find("[data-testid='remove-alert']");
const createWrapper = ({ propsData = defaultProps } = {}) => {
wrapper = shallowMount(PolicyAlertPicker, {
propsData: {
...propsData,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('default state', () => {
beforeEach(() => {
createWrapper();
});
it('does render the add alert button', () => {
expect(findAddAlertButton().exists()).toBe(true);
});
it('does not render the high volume warning', () => {
expect(findGlAlert().exists()).toBe(false);
});
it('does not render the alert message', () => {
expect(findGlSprintf().exists()).toBe(false);
});
it('does not render the remove alert button', () => {
expect(findRemoveAlertButton().exists()).toBe(false);
});
it('does emit an event to add the alert', () => {
findAddAlertButton().vm.$emit('click');
expect(wrapper.emitted('update-alert')).toEqual([[true]]);
});
});
describe('alert enabled', () => {
beforeEach(() => {
createWrapper({ propsData: { policyAlert: true } });
});
it('does not render the add alert button', () => {
expect(findAddAlertButton().exists()).toBe(false);
});
it('does render the high volume warning', () => {
expect(findGlAlert().exists()).toBe(true);
});
it('does render the alert message', () => {
expect(findGlSprintf().exists()).toBe(true);
});
it('does render the remove alert button', () => {
expect(findRemoveAlertButton().exists()).toBe(true);
});
it('does emit an event to remove the alert', () => {
findRemoveAlertButton().vm.$emit('click');
expect(wrapper.emitted('update-alert')).toEqual([[false]]);
});
});
});
......@@ -14,6 +14,7 @@ import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml';
import PolicyEditorApp from 'ee/threat_monitoring/components/policy_editor/policy_editor.vue';
import PolicyPreview from 'ee/threat_monitoring/components/policy_editor/policy_preview.vue';
import PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/policy_rule_builder.vue';
import PolicyAlertPicker from 'ee/threat_monitoring/components/policy_editor/policy_alert_picker.vue';
import createStore from 'ee/threat_monitoring/store';
import { redirectTo } from '~/lib/utils/url_utility';
......@@ -23,7 +24,7 @@ describe('PolicyEditorApp component', () => {
let store;
let wrapper;
const factory = ({ propsData, state, data } = {}) => {
const factory = ({ propsData, provide = {}, state, data } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
...state,
......@@ -39,6 +40,10 @@ describe('PolicyEditorApp component', () => {
threatMonitoringPath: '/threat-monitoring',
...propsData,
},
provide: {
glFeatures: { threatMonitoringAlerts: false },
...provide,
},
store,
data,
});
......@@ -50,17 +55,28 @@ describe('PolicyEditorApp component', () => {
const findAddRuleButton = () => wrapper.find('[data-testid="add-rule"]');
const findYAMLParsingAlert = () => wrapper.find('[data-testid="parsing-alert"]');
const findNetworkPolicyEditor = () => wrapper.find(NetworkPolicyEditor);
const findPolicyAlertPicker = () => wrapper.find(PolicyAlertPicker);
const findPolicyName = () => wrapper.find("[id='policyName']");
const findSavePolicy = () => wrapper.find("[data-testid='save-policy']");
const findDeletePolicy = () => wrapper.find("[data-testid='delete-policy']");
const findEditorModeToggle = () => wrapper.find("[data-testid='editor-mode']");
const modifyPolicyAlert = async ({ isAlertEnabled }) => {
const policyAlertPicker = findPolicyAlertPicker();
policyAlertPicker.vm.$emit('update-alert', isAlertEnabled);
await wrapper.vm.$nextTick();
expect(policyAlertPicker.props('policyAlert')).toBe(isAlertEnabled);
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
};
beforeEach(() => {
factory();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the policy editor layout', () => {
......@@ -79,6 +95,10 @@ describe('PolicyEditorApp component', () => {
expect(findDeletePolicy().exists()).toBe(false);
});
it('does not render the policy alert picker', () => {
expect(findPolicyAlertPicker().exists()).toBe(false);
});
describe('given .yaml editor mode is enabled', () => {
beforeEach(() => {
factory({
......@@ -339,4 +359,34 @@ spec:
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
});
describe('add alert picker', () => {
beforeEach(() => {
factory({ provide: { glFeatures: { threatMonitoringAlerts: true } } });
});
it('does render the policy alert picker', () => {
expect(findPolicyAlertPicker().exists()).toBe(true);
});
it('adds a policy annotation on alert addition', async () => {
await modifyPolicyAlert({ isAlertEnabled: true });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: {
manifest: expect.stringContaining("app.gitlab.com/alert: 'true'"),
},
});
});
it('removes a policy annotation on alert removal', async () => {
await modifyPolicyAlert({ isAlertEnabled: false });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: {
manifest: expect.not.stringContaining("app.gitlab.com/alert: 'true'"),
},
});
});
});
});
......@@ -18480,6 +18480,9 @@ msgstr ""
msgid "NetworkPolicies|%{ifLabelStart}if%{ifLabelEnd} %{ruleType} %{isLabelStart}is%{isLabelEnd} %{ruleDirection} %{ruleSelector} %{directionLabelStart}and is outbound to a%{directionLabelEnd} %{rule} %{portsLabelStart}on%{portsLabelEnd} %{ports}"
msgstr ""
msgid "NetworkPolicies|%{labelStart}And%{labelEnd} %{spanStart}send an Alert to GitLab.%{spanEnd}"
msgstr ""
msgid "NetworkPolicies|%{labelStart}Then%{labelEnd} %{action} %{spanStart}the network traffic.%{spanEnd}"
msgstr ""
......@@ -18492,6 +18495,9 @@ msgstr ""
msgid "NetworkPolicies|%{strongOpen}any%{strongClose} port"
msgstr ""
msgid "NetworkPolicies|+ Add alert"
msgstr ""
msgid "NetworkPolicies|.yaml"
msgstr ""
......@@ -18501,6 +18507,9 @@ msgstr ""
msgid "NetworkPolicies|Actions"
msgstr ""
msgid "NetworkPolicies|Alerts are intended to be selectively used for a limited number of events that are potentially concerning and warrant a manual review. Alerts should not be used as a substitute for a SIEM or a logging tool. High volume alerts are likely to be dropped so as to preserve the stability of GitLab's integration with Kubernetes."
msgstr ""
msgid "NetworkPolicies|All selected"
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