Skip to content

Commit

Permalink
[Security Solution][Endpoint] Remove use of Redux from the Endpoint P…
Browse files Browse the repository at this point in the history
…olicy Settings form (#161511)

## Summary

- Re-creates all policy settings form components so that the Policy
settings are provided as a prop
- Adds `mode = view` to the policy form. When in this mode (user has no
authz to edit), form will be displayed in view only mode (no more
`disabled` form elements)
  • Loading branch information
paul-tavares authored Jul 12, 2023
1 parent f022456 commit 3ba51e4
Show file tree
Hide file tree
Showing 65 changed files with 2,795 additions and 2,836 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import type { MaybeImmutable, NewPolicyData, PolicyData } from '../../types';
*/
export const getPolicyDataForUpdate = (policy: MaybeImmutable<PolicyData>): NewPolicyData => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { id, revision, created_by, created_at, updated_by, updated_at, ...newPolicy } = policy;
// cast to `NewPolicyData` (mutable) since we cloned the entire object
const policyDataForUpdate = cloneDeep(newPolicy) as NewPolicyData;
const { id, revision, created_by, created_at, updated_by, updated_at, ...rest } =
policy as PolicyData;

const policyDataForUpdate: NewPolicyData = cloneDeep(rest);
const endpointPolicy = policyDataForUpdate.inputs[0].config.policy.value;

// trim custom malware notification string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { getPolicySettingsFormTestSubjects } from '../../../pages/policy/view/policy_settings_form/mocks';
import { ProtectionModes } from '../../../../../common/endpoint/types';
import {
PackagePolicyBackupHelper,
Expand All @@ -30,7 +31,7 @@ describe('Policy Details', () => {

beforeEach(() => {
login();
visitPolicyDetailsPage();
visitPolicyDetailsPage(indexedHostsData.data.integrationPolicies[0].id);
});

afterEach(() => {
Expand All @@ -42,33 +43,43 @@ describe('Policy Details', () => {
});

describe('Malware Protection card', () => {
const malwareTestSubj = getPolicySettingsFormTestSubjects().malware;

it('user should be able to see related rules', () => {
cy.getByTestSubj('malwareProtectionsForm').contains('related detection rules').click();
cy.getByTestSubj(malwareTestSubj.card).contains('related detection rules').click();

cy.url().should('contain', 'app/security/rules/management');
});

it('changing protection level should enable or disable user notification', () => {
cy.getByTestSubj('malwareProtectionSwitch').click();
cy.getByTestSubj('malwareProtectionSwitch').should('have.attr', 'aria-checked', 'true');
cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click();
cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).should(
'have.attr',
'aria-checked',
'true'
);

// Default: Prevent + Notify user enabled
cy.getByTestSubj('malwareProtectionMode_prevent').find('input').should('be.checked');
cy.getByTestSubj('malwareUserNotificationCheckbox').should('be.checked');
cy.getByTestSubj(malwareTestSubj.protectionPreventRadio).find('input').should('be.checked');
cy.getByTestSubj(malwareTestSubj.notifyUserCheckbox).should('be.checked');

// Changing to Detect -> Notify user disabled
cy.getByTestSubj('malwareProtectionMode_detect').find('label').click();
cy.getByTestSubj('malwareUserNotificationCheckbox').should('not.be.checked');
cy.getByTestSubj(malwareTestSubj.protectionDetectRadio).find('label').click();
cy.getByTestSubj(malwareTestSubj.notifyUserCheckbox).should('not.be.checked');

// Changing back to Prevent -> Notify user enabled
cy.getByTestSubj('malwareProtectionMode_prevent').find('label').click();
cy.getByTestSubj('malwareUserNotificationCheckbox').should('be.checked');
cy.getByTestSubj(malwareTestSubj.protectionPreventRadio).find('label').click();
cy.getByTestSubj(malwareTestSubj.notifyUserCheckbox).should('be.checked');
});

it('disabling protection should disable notification in yaml for every OS', () => {
// Enable malware protection and user notification
cy.getByTestSubj('malwareProtectionSwitch').click();
cy.getByTestSubj('malwareProtectionSwitch').should('have.attr', 'aria-checked', 'true');
cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click();
cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).should(
'have.attr',
'aria-checked',
'true'
);
savePolicyForm();

yieldPolicyConfig().then((policyConfig) => {
Expand All @@ -78,8 +89,12 @@ describe('Policy Details', () => {
});

// disable malware protection
cy.getByTestSubj('malwareProtectionSwitch').click();
cy.getByTestSubj('malwareProtectionSwitch').should('have.attr', 'aria-checked', 'false');
cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click();
cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).should(
'have.attr',
'aria-checked',
'false'
);
savePolicyForm();

yieldPolicyConfig().then((policyConfig) => {
Expand All @@ -96,11 +111,12 @@ describe('Policy Details', () => {
expect(policyConfig.windows.malware.mode).to.equal(ProtectionModes.off);
});

cy.getByTestSubj('malwareProtectionsForm').should('contain.text', 'Linux');
cy.getByTestSubj('malwareProtectionsForm').should('contain.text', 'Windows');
cy.getByTestSubj('malwareProtectionsForm').should('contain.text', 'Mac');
cy.getByTestSubj(malwareTestSubj.osValuesContainer).should(
'contain.text',
'Windows, Mac, Linux'
);

cy.getByTestSubj('malwareProtectionSwitch').click();
cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click();
savePolicyForm();

yieldPolicyConfig().then((policyConfig) => {
Expand All @@ -112,40 +128,50 @@ describe('Policy Details', () => {
});

describe('Ransomware Protection card', () => {
const ransomwareTestSubj = getPolicySettingsFormTestSubjects().ransomware;

it('user should be able to see related rules', () => {
cy.getByTestSubj('ransomwareProtectionsForm').contains('related detection rules').click();
cy.getByTestSubj(ransomwareTestSubj.card).contains('related detection rules').click();

cy.url().should('contain', 'app/security/rules/management');
});

it('changing protection level should enable or disable user notification', () => {
cy.getByTestSubj('ransomwareProtectionSwitch').click();
cy.getByTestSubj('ransomwareProtectionSwitch').should('have.attr', 'aria-checked', 'true');
cy.getByTestSubj(ransomwareTestSubj.enableDisableSwitch).click();
cy.getByTestSubj(ransomwareTestSubj.enableDisableSwitch).should(
'have.attr',
'aria-checked',
'true'
);

// Default: Prevent + Notify user enabled
cy.getByTestSubj('ransomwareProtectionMode_prevent').find('input').should('be.checked');
cy.getByTestSubj('ransomwareUserNotificationCheckbox').should('be.checked');
cy.getByTestSubj(ransomwareTestSubj.protectionPreventRadio)
.find('input')
.should('be.checked');
cy.getByTestSubj(ransomwareTestSubj.notifyUserCheckbox).should('be.checked');

// Changing to Detect -> Notify user disabled
cy.getByTestSubj('ransomwareProtectionMode_detect').find('label').click();
cy.getByTestSubj('ransomwareUserNotificationCheckbox').should('not.be.checked');
cy.getByTestSubj(ransomwareTestSubj.protectionDetectRadio).find('label').click();
cy.getByTestSubj(ransomwareTestSubj.notifyUserCheckbox).should('not.be.checked');

// Changing back to Prevent -> Notify user enabled
cy.getByTestSubj('ransomwareProtectionMode_prevent').find('label').click();
cy.getByTestSubj('ransomwareUserNotificationCheckbox').should('be.checked');
cy.getByTestSubj(ransomwareTestSubj.protectionPreventRadio).find('label').click();
cy.getByTestSubj(ransomwareTestSubj.notifyUserCheckbox).should('be.checked');
});
});

describe('Advanced settings', () => {
const testSubjects = getPolicySettingsFormTestSubjects().advancedSection;

it('should show empty text inputs except for some settings', () => {
const settingsWithDefaultValues = [
'mac.advanced.capture_env_vars',
'linux.advanced.capture_env_vars',
];

cy.getByTestSubj('advancedPolicyButton').click();
cy.getByTestSubj(testSubjects.showHideButton).click();

cy.getByTestSubj('advancedPolicyPanel')
cy.getByTestSubj(testSubjects.settingsContainer)
.children()
.each(($child) => {
const settingName = $child.find('label').text();
Expand All @@ -167,8 +193,8 @@ describe('Policy Details', () => {
});

// Set agent.connection_delay entry for every OS
cy.getByTestSubj('advancedPolicyButton').click();
cy.getByTestSubj('advancedPolicyPanel')
cy.getByTestSubj(testSubjects.showHideButton).click();
cy.getByTestSubj(testSubjects.settingsContainer)
.children()
.each(($child) => {
const settingName = $child.find('label').text();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import type { PolicyConfig } from '../../../../common/endpoint/types';
import { request, loadPage } from '../tasks/common';
import { expectAndCloseSuccessToast } from '../tasks/toasts';

export const visitPolicyDetailsPage = () => {
loadPage(APP_POLICIES_PATH);

cy.getByTestSubj('policyNameCellLink').eq(0).click({ force: true });
export const visitPolicyDetailsPage = (policyId?: string) => {
if (policyId) {
loadPage(`${APP_POLICIES_PATH}/${policyId}`);
} else {
cy.visit(APP_POLICIES_PATH);
cy.getByTestSubj('policyNameCellLink').eq(0).click({ force: true });
}
cy.getByTestSubj('policyDetailsPage').should('exist');
cy.get('#settings').should('exist'); // waiting for Policy Settings tab
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import { useQuery } from '@tanstack/react-query';
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
import {
DefaultPolicyNotificationMessage,
DefaultPolicyRuleNotificationMessage,
} from '../../../../common/endpoint/models/policy_config';
import type { GetPolicyResponse } from '../../pages/policy/types';
import { useHttp } from '../../../common/lib/kibana';
import type { PolicyData, PolicyConfig } from '../../../../common/endpoint/types';
import type { ManifestSchema } from '../../../../common/endpoint/schema/manifest';

interface ApiDataResponse {
/** Data return from the Fleet API. Its the full integration policy (package policy) */
item: PolicyData;
/** Endpoint policy settings from the data retrieved from fleet */
settings: PolicyConfig;
/** Endpoint policy manifest info from the data retrieved from fleet */
artifactManifest: ManifestSchema;
}

type UseFetchEndpointPolicyResponse = UseQueryResult<ApiDataResponse, IHttpFetchError>;

/**
* Retrieve a single endpoint integration policy (details)
* @param policyId
* @param options
*/
export const useFetchEndpointPolicy = (
policyId: string,
options: UseQueryOptions<ApiDataResponse, IHttpFetchError> = {}
): UseFetchEndpointPolicyResponse => {
const http = useHttp();

return useQuery<ApiDataResponse, IHttpFetchError>({
queryKey: ['get-policy-details', policyId],
...options,
queryFn: async () => {
const apiResponse = await http.get<GetPolicyResponse>(
packagePolicyRouteService.getInfoPath(policyId)
);

applyDefaultsToPolicyIfNeeded(apiResponse.item);

return {
item: apiResponse.item,
settings: apiResponse.item.inputs[0].config.policy.value,
artifactManifest: apiResponse.item.inputs[0].config.artifact_manifest.value,
};
},
});
};

const applyDefaultsToPolicyIfNeeded = (policyItem: PolicyData): void => {
const settings = policyItem.inputs[0].config.policy.value;

// sets default user notification message if policy config message is empty
if (settings.windows.popup.malware.message === '') {
settings.windows.popup.malware.message = DefaultPolicyNotificationMessage;
settings.mac.popup.malware.message = DefaultPolicyNotificationMessage;
settings.linux.popup.malware.message = DefaultPolicyNotificationMessage;
}
if (settings.windows.popup.ransomware.message === '') {
settings.windows.popup.ransomware.message = DefaultPolicyNotificationMessage;
}
if (settings.windows.popup.memory_protection.message === '') {
settings.windows.popup.memory_protection.message = DefaultPolicyRuleNotificationMessage;
}
if (settings.mac.popup.memory_protection.message === '') {
settings.mac.popup.memory_protection.message = DefaultPolicyRuleNotificationMessage;
}
if (settings.linux.popup.memory_protection.message === '') {
settings.linux.popup.memory_protection.message = DefaultPolicyRuleNotificationMessage;
}
if (settings.windows.popup.behavior_protection.message === '') {
settings.windows.popup.behavior_protection.message = DefaultPolicyRuleNotificationMessage;
}
if (settings.mac.popup.behavior_protection.message === '') {
settings.mac.popup.behavior_protection.message = DefaultPolicyRuleNotificationMessage;
}
if (settings.linux.popup.behavior_protection.message === '') {
settings.linux.popup.behavior_protection.message = DefaultPolicyRuleNotificationMessage;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import type { GetAgentStatusResponse } from '@kbn/fleet-plugin/common';
import { useQuery } from '@tanstack/react-query';
import { agentRouteService } from '@kbn/fleet-plugin/common';
import { useHttp } from '../../../common/lib/kibana';

type EndpointPolicyAgentSummary = GetAgentStatusResponse['results'];

export const useFetchAgentByAgentPolicySummary = (
/**
* The Fleet Agent Policy ID (NOT the endpoint policy id)
*/
agentPolicyId: string,
options: UseQueryOptions<EndpointPolicyAgentSummary, IHttpFetchError> = {}
): UseQueryResult<EndpointPolicyAgentSummary, IHttpFetchError> => {
const http = useHttp();

return useQuery<EndpointPolicyAgentSummary, IHttpFetchError>({
queryKey: ['get-policy-agent-summary', agentPolicyId],
...options,
queryFn: async () => {
return (
await http.get<GetAgentStatusResponse>(agentRouteService.getStatusPath(), {
query: { policyId: agentPolicyId },
})
).results;
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import { useMutation } from '@tanstack/react-query';
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
import { getPolicyDataForUpdate } from '../../../../common/endpoint/service/policy';
import { useHttp } from '../../../common/lib/kibana';
import type { PolicyData } from '../../../../common/endpoint/types';
import type { UpdatePolicyResponse } from '../../pages/policy/types';

interface UpdateParams {
policy: PolicyData;
}

type UseUpdateEndpointPolicyOptions = UseMutationOptions<
UpdatePolicyResponse,
IHttpFetchError,
UpdateParams
>;

type UseUpdateEndpointPolicyResult = UseMutationResult<
UpdatePolicyResponse,
IHttpFetchError,
UpdateParams
>;

export const useUpdateEndpointPolicy = (
options?: UseUpdateEndpointPolicyOptions
): UseUpdateEndpointPolicyResult => {
const http = useHttp();

return useMutation<UpdatePolicyResponse, IHttpFetchError, UpdateParams>(({ policy }) => {
const update = getPolicyDataForUpdate(policy);

return http.put(packagePolicyRouteService.getUpdatePath(policy.id), {
body: JSON.stringify(update),
});
}, options);
};
Loading

0 comments on commit 3ba51e4

Please sign in to comment.