Skip to content

Commit

Permalink
[8.x] [EDR Workflows][Serverless] Gate custom note (#193171) (#194576)
Browse files Browse the repository at this point in the history
# Backport

This will backport the following commits from `main` to `8.x`:
- [[EDR Workflows][Serverless] Gate custom note
(#193171)](#193171)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Konrad
Szwarc","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-10-01T13:20:27Z","message":"[EDR
Workflows][Serverless] Gate custom note (#193171)\n\nThis PR implements
tier-based gating for custom notification messages in\r\nProtections.
Only users on the Endpoint Complete tier will have the\r\nability to
modify these messages, while users on the Endpoint Essentials\r\ntier
will no longer have this capability. If a user on the Essentials\r\ntier
had made any changes to custom notifications before this
update,\r\nthose messages will be reset to the default ones.\r\n\r\nThe
changes are applied in three areas:\r\n1. UI - An upsell banner is
displayed for Essentials users.\r\n2. API - We now prevent API calls
that attempt to set or modify custom\r\nnotification messages for
Essentials users.\r\n3. Policy Watcher - Upon Kibana startup (e.g.,
after a downgrade), we\r\nvalidate all policies for tier compliance. If
a policy contains a custom\r\nnotification message and the user is on
the Essentials tier, the message\r\nwill be reset to the
default.\r\n\r\n![Screenshot 2024-09-20 at 14
32\r\n52](https://github.com/user-attachments/assets/75739338-e32b-47da-934e-9948f44099ae)\r\n![Screenshot
2024-09-20 at 14
33\r\n21](https://github.com/user-attachments/assets/1af081eb-f75f-4c9d-8f01-df9a01f8f2b2)\r\n![Screenshot
2024-09-20 at 14
33\r\n40](https://github.com/user-attachments/assets/4c0014f5-89f0-48b6-88dc-cc4c2dba666a)\r\n![Screenshot
2024-09-20 at 14
52\r\n25](https://github.com/user-attachments/assets/202e5e1a-7c58-4af1-a85a-399c94313f0b)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"af1dc871eb020a885278df280a6f830bc1179d56","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Defend
Workflows","ci:project-deploy-security","v8.16.0","backport:version"],"title":"[EDR
Workflows][Serverless] Gate custom
note","number":193171,"url":"https://github.com/elastic/kibana/pull/193171","mergeCommit":{"message":"[EDR
Workflows][Serverless] Gate custom note (#193171)\n\nThis PR implements
tier-based gating for custom notification messages in\r\nProtections.
Only users on the Endpoint Complete tier will have the\r\nability to
modify these messages, while users on the Endpoint Essentials\r\ntier
will no longer have this capability. If a user on the Essentials\r\ntier
had made any changes to custom notifications before this
update,\r\nthose messages will be reset to the default ones.\r\n\r\nThe
changes are applied in three areas:\r\n1. UI - An upsell banner is
displayed for Essentials users.\r\n2. API - We now prevent API calls
that attempt to set or modify custom\r\nnotification messages for
Essentials users.\r\n3. Policy Watcher - Upon Kibana startup (e.g.,
after a downgrade), we\r\nvalidate all policies for tier compliance. If
a policy contains a custom\r\nnotification message and the user is on
the Essentials tier, the message\r\nwill be reset to the
default.\r\n\r\n![Screenshot 2024-09-20 at 14
32\r\n52](https://github.com/user-attachments/assets/75739338-e32b-47da-934e-9948f44099ae)\r\n![Screenshot
2024-09-20 at 14
33\r\n21](https://github.com/user-attachments/assets/1af081eb-f75f-4c9d-8f01-df9a01f8f2b2)\r\n![Screenshot
2024-09-20 at 14
33\r\n40](https://github.com/user-attachments/assets/4c0014f5-89f0-48b6-88dc-cc4c2dba666a)\r\n![Screenshot
2024-09-20 at 14
52\r\n25](https://github.com/user-attachments/assets/202e5e1a-7c58-4af1-a85a-399c94313f0b)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"af1dc871eb020a885278df280a6f830bc1179d56"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/193171","number":193171,"mergeCommit":{"message":"[EDR
Workflows][Serverless] Gate custom note (#193171)\n\nThis PR implements
tier-based gating for custom notification messages in\r\nProtections.
Only users on the Endpoint Complete tier will have the\r\nability to
modify these messages, while users on the Endpoint Essentials\r\ntier
will no longer have this capability. If a user on the Essentials\r\ntier
had made any changes to custom notifications before this
update,\r\nthose messages will be reset to the default ones.\r\n\r\nThe
changes are applied in three areas:\r\n1. UI - An upsell banner is
displayed for Essentials users.\r\n2. API - We now prevent API calls
that attempt to set or modify custom\r\nnotification messages for
Essentials users.\r\n3. Policy Watcher - Upon Kibana startup (e.g.,
after a downgrade), we\r\nvalidate all policies for tier compliance. If
a policy contains a custom\r\nnotification message and the user is on
the Essentials tier, the message\r\nwill be reset to the
default.\r\n\r\n![Screenshot 2024-09-20 at 14
32\r\n52](https://github.com/user-attachments/assets/75739338-e32b-47da-934e-9948f44099ae)\r\n![Screenshot
2024-09-20 at 14
33\r\n21](https://github.com/user-attachments/assets/1af081eb-f75f-4c9d-8f01-df9a01f8f2b2)\r\n![Screenshot
2024-09-20 at 14
33\r\n40](https://github.com/user-attachments/assets/4c0014f5-89f0-48b6-88dc-cc4c2dba666a)\r\n![Screenshot
2024-09-20 at 14
52\r\n25](https://github.com/user-attachments/assets/202e5e1a-7c58-4af1-a85a-399c94313f0b)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"af1dc871eb020a885278df280a6f830bc1179d56"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Konrad Szwarc <[email protected]>
  • Loading branch information
kibanamachine and szwarckonrad authored Oct 1, 2024
1 parent a471bec commit 4a1a04b
Show file tree
Hide file tree
Showing 17 changed files with 876 additions and 393 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,16 @@ export enum ProductFeatureSecurityKey {
osqueryAutomatedResponseActions = 'osquery_automated_response_actions',

/**
* Enables Agent Tamper Protection
* Enables Protection Updates
*/
endpointProtectionUpdates = 'endpoint_protection_updates',

/**
* Enables Endpoint Custom Notification
*/

endpointCustomNotification = 'endpoint_custom_notification',

/**
* Enables Agent Tamper Protection
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
[ProductFeatureSecurityKey.osqueryAutomatedResponseActions]: {},
[ProductFeatureSecurityKey.endpointProtectionUpdates]: {},
[ProductFeatureSecurityKey.endpointAgentTamperProtection]: {},
[ProductFeatureSecurityKey.endpointCustomNotification]: {},
[ProductFeatureSecurityKey.externalRuleActions]: {},
[ProductFeatureSecurityKey.cloudSecurityPosture]: {},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type UpsellingSectionId =
| 'osquery_automated_response_actions'
| 'endpoint_protection_updates'
| 'endpoint_agent_tamper_protection'
| 'endpoint_custom_notification'
| 'cloud_security_posture_integration_installation'
| 'ruleDetailsEndpointExceptions'
| 'integration_assistant';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@

import type { PolicyConfig } from '../types';
import { PolicyOperatingSystem, ProtectionModes, AntivirusRegistrationModes } from '../types';
import { policyFactory } from './policy_config';
import { DefaultPolicyNotificationMessage, policyFactory } from './policy_config';
import {
disableProtections,
isPolicySetToEventCollectionOnly,
ensureOnlyEventCollectionIsAllowed,
isBillablePolicy,
getPolicyProtectionsReference,
checkIfPopupMessagesContainCustomNotifications,
resetCustomNotifications,
} from './policy_config_helpers';
import { set } from 'lodash';
import { get, merge, set } from 'lodash';

describe('Policy Config helpers', () => {
describe('disableProtections', () => {
Expand Down Expand Up @@ -223,6 +225,107 @@ describe('Policy Config helpers', () => {
}
);
});

describe('checkIfPopupMessagesContainCustomNotifications', () => {
let policy: PolicyConfig;

beforeEach(() => {
policy = policyFactory();
});

it('returns false when all popup messages are default', () => {
expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(false);
});

it('returns true when any popup message is custom', () => {
set(policy, 'windows.popup.malware.message', 'Custom message');
expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(true);
});

it('returns false when all popup messages are empty', () => {
set(policy, 'windows.popup.malware.message', '');
set(policy, 'mac.popup.memory_protection.message', '');
expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(false);
});

it('returns true when any popup message is not empty or default', () => {
set(policy, 'linux.popup.behavior_protection.message', 'Another custom message');
expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(true);
});

it('returns false when all popup messages are default across all OS', () => {
set(policy, 'windows.popup.malware.message', DefaultPolicyNotificationMessage);
set(policy, 'mac.popup.memory_protection.message', DefaultPolicyNotificationMessage);
set(policy, 'linux.popup.behavior_protection.message', DefaultPolicyNotificationMessage);
set(policy, 'windows.popup.ransomware.message', '');
expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(false);
});
});

describe('resetCustomNotifications', () => {
let policy: PolicyConfig;

beforeEach(() => {
policy = policyFactory();
});

it.each([
'windows.popup.malware.message',
'windows.popup.behavior_protection.message',
'windows.popup.memory_protection.message',
'windows.popup.ransomware.message',
'linux.popup.malware.message',
'linux.popup.behavior_protection.message',
'linux.popup.memory_protection.message',
'mac.popup.malware.message',
'mac.popup.behavior_protection.message',
'mac.popup.memory_protection.message',
])('resets %s to default message', (keyPath) => {
set(policy, keyPath, `Custom message`);
const defaultNotifications = resetCustomNotifications();

const updatedPolicy = merge({}, policy, defaultNotifications);
expect(get(updatedPolicy, keyPath)).toBe(DefaultPolicyNotificationMessage);
});

it('does not change default messages', () => {
set(policy, 'windows.popup.malware.message', DefaultPolicyNotificationMessage);
const defaultNotifications = resetCustomNotifications();

const updatedPolicy = merge({}, policy, defaultNotifications);
expect(get(updatedPolicy, 'windows.popup.malware.message')).toBe(
DefaultPolicyNotificationMessage
);
});

it('resets empty messages to default messages', () => {
set(policy, 'windows.popup.malware.message', '');
const defaultNotifications = resetCustomNotifications();

const updatedPolicy = merge({}, policy, defaultNotifications);
expect(get(updatedPolicy, 'windows.popup.malware.message')).toBe(
DefaultPolicyNotificationMessage
);
});

it('resets messages for all operating systems', () => {
set(policy, 'windows.popup.malware.message', 'Custom message');
set(policy, 'mac.popup.memory_protection.message', 'Another custom message');
set(policy, 'linux.popup.behavior_protection.message', 'Yet another custom message');
const defaultNotifications = resetCustomNotifications();

const updatedPolicy = merge({}, policy, defaultNotifications);
expect(get(updatedPolicy, 'windows.popup.malware.message')).toBe(
DefaultPolicyNotificationMessage
);
expect(get(updatedPolicy, 'mac.popup.memory_protection.message')).toBe(
DefaultPolicyNotificationMessage
);
expect(get(updatedPolicy, 'linux.popup.behavior_protection.message')).toBe(
DefaultPolicyNotificationMessage
);
});
});
});

// This constant makes sure that if the type `PolicyConfig` is ever modified,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { get, set } from 'lodash';
import { DefaultPolicyNotificationMessage } from './policy_config';
import type { PolicyConfig } from '../types';
import { PolicyOperatingSystem, ProtectionModes, AntivirusRegistrationModes } from '../types';

Expand All @@ -22,6 +23,28 @@ const allOsValues = [
PolicyOperatingSystem.windows,
];

const getPolicyPopupReference = (): Array<{
keyPath: string;
osList: PolicyOperatingSystem[];
}> => [
{
keyPath: 'popup.malware.message',
osList: [...allOsValues],
},
{
keyPath: 'popup.memory_protection.message',
osList: [...allOsValues],
},
{
keyPath: 'popup.behavior_protection.message',
osList: [...allOsValues],
},
{
keyPath: 'popup.ransomware.message',
osList: [PolicyOperatingSystem.windows],
},
];

export const getPolicyProtectionsReference = (): PolicyProtectionReference[] => [
{
keyPath: 'malware.mode',
Expand Down Expand Up @@ -210,3 +233,28 @@ export function isBillablePolicy(policy: PolicyConfig) {

return !isPolicySetToEventCollectionOnly(policy).isOnlyCollectingEvents;
}

export const checkIfPopupMessagesContainCustomNotifications = (policy: PolicyConfig): boolean => {
const popupRefs = getPolicyPopupReference();

return popupRefs.some(({ keyPath, osList }) => {
return osList.some((osValue) => {
const fullKeyPathForOs = `${osValue}.${keyPath}`;
const currentValue = get(policy, fullKeyPathForOs);
return currentValue !== '' && currentValue !== DefaultPolicyNotificationMessage;
});
});
};

export const resetCustomNotifications = (
customNotification = DefaultPolicyNotificationMessage
): Partial<PolicyConfig> => {
const popupRefs = getPolicyPopupReference();

return popupRefs.reduce((acc, { keyPath, osList }) => {
osList.forEach((osValue) => {
set(acc, `${osValue}.${keyPath}`, customNotification);
});
return acc;
}, {});
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe(
login();
});

it('should display upselling section for protection updates', () => {
it('should not display upselling section for protection updates', () => {
loadPage(`${APP_POLICIES_PATH}/${policyId}/protectionUpdates`);
[
'endpointPolicy-protectionUpdatesLockedCard-title',
Expand All @@ -67,5 +67,29 @@ describe(
cy.getByTestSubj(testSubj).should('exist').and('be.visible');
});
});

it(`should not display upselling section for custom notification`, () => {
const testData = ['malware', 'ransomware', 'memory', 'behaviour'];

loadPage(`${APP_POLICIES_PATH}/${policyId}/settings`);

testData.forEach((protection) => {
cy.getByTestSubj(`endpointPolicyForm-${protection}`).within(() => {
cy.getByTestSubj(`endpointPolicyForm-${protection}-enableDisableSwitch`).click();
// User should not see the locked card since the feature is available under Endpoint Complete tier
[
'endpointPolicy-customNotificationLockedCard-title',
'endpointPolicy-customNotificationLockedCard',
'endpointPolicy-customNotificationLockedCard-badge',
].forEach((testSubj) => {
cy.getByTestSubj(testSubj).should('not.exist');
});
// User should see the custom notification section
cy.getByTestSubj(`endpointPolicyForm-${protection}-notifyUser-customMessage`)
.should('exist')
.and('be.visible');
});
});
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,29 @@ describe(
});
cy.getByTestSubj('protection-updates-layout').should('not.exist');
});

it(`should display upselling section for custom notification`, () => {
const testData = ['malware', 'ransomware', 'memory', 'behaviour'];

loadPage(`${APP_POLICIES_PATH}/${policyId}/settings`);

testData.forEach((protection) => {
cy.getByTestSubj(`endpointPolicyForm-${protection}`).within(() => {
cy.getByTestSubj(`endpointPolicyForm-${protection}-enableDisableSwitch`).click();
// User should see the custom notification locked card since it is not enabled under Endpoint Essentials
[
'endpointPolicy-customNotificationLockedCard-title',
'endpointPolicy-customNotificationLockedCard',
'endpointPolicy-customNotificationLockedCard-badge',
].forEach((testSubj) => {
cy.getByTestSubj(testSubj, { timeout: 60000 }).should('exist').and('be.visible');
});
// User should not see the custom message input since it is not enabled under Endpoint Essentials
cy.getByTestSubj(`endpointPolicyForm-${protection}-notifyUser-customMessage`).should(
'not.exist'
);
});
});
});
}
);
Loading

0 comments on commit 4a1a04b

Please sign in to comment.