Skip to content

Commit

Permalink
[Cloud Posture] Display and save rules per benchmark (#131412)
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanSh authored May 3, 2022
1 parent 6a6d202 commit 9d7b459
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useFindCspRules, useBulkUpdateCspRules, type RuleSavedObject } from './
import * as TEST_SUBJECTS from './test_subjects';
import { Chance } from 'chance';
import { TestProvider } from '../../test/test_provider';
import { useParams } from 'react-router-dom';

const chance = new Chance();

Expand All @@ -21,6 +22,11 @@ jest.mock('./use_csp_rules', () => ({
useBulkUpdateCspRules: jest.fn(),
}));

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn(),
}));

const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
Expand All @@ -32,21 +38,54 @@ const getWrapper =
({ children }) =>
<TestProvider>{children}</TestProvider>;

const getRuleMock = ({ id = chance.guid(), enabled }: { id?: string; enabled: boolean }) =>
const getRuleMock = ({
packagePolicyId = chance.guid(),
policyId = chance.guid(),
savedObjectId = chance.guid(),
id = chance.guid(),
enabled,
}: {
packagePolicyId?: string;
policyId?: string;
savedObjectId?: string;
id?: string;
enabled: boolean;
}) =>
({
id,
id: savedObjectId,
updatedAt: chance.date().toISOString(),
attributes: {
id,
name: chance.sentence(),
package_policy_id: packagePolicyId,
policy_id: policyId,
enabled,
},
} as RuleSavedObject);

const getSavedObjectRuleEnable = (
rule: RuleSavedObject,
enabled: RuleSavedObject['attributes']['enabled']
) => ({
...rule,
attributes: {
...rule.attributes,
enabled,
},
});

const params = {
policyId: chance.guid(),
packagePolicyId: chance.guid(),
};

describe('<RulesContainer />', () => {
beforeEach(() => {
queryClient.clear();
jest.clearAllMocks();

(useParams as jest.Mock).mockReturnValue(params);

(useBulkUpdateCspRules as jest.Mock).mockReturnValue({
status: 'idle',
mutate: jest.fn(),
Expand Down Expand Up @@ -102,6 +141,13 @@ describe('<RulesContainer />', () => {
const switchId1 = TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule1.id);
const switchId2 = TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule2.id);

expect(screen.getByTestId(switchId1).getAttribute('aria-checked')).toEqual(
rule1.attributes.enabled.toString()
);
expect(screen.getByTestId(switchId2).getAttribute('aria-checked')).toEqual(
rule2.attributes.enabled.toString()
);

fireEvent.click(screen.getByTestId(switchId1));
fireEvent.click(screen.getByTestId(switchId2));

Expand Down Expand Up @@ -154,6 +200,7 @@ describe('<RulesContainer />', () => {

it('updates rules with local changes done by non-bulk toggles', () => {
const Wrapper = getWrapper();

const rule1 = getRuleMock({ enabled: false });
const rule2 = getRuleMock({ enabled: true });
const rule3 = getRuleMock({ enabled: true });
Expand Down Expand Up @@ -184,10 +231,13 @@ describe('<RulesContainer />', () => {
fireEvent.click(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_SAVE_BUTTON));

expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith([
{ ...rule1.attributes, enabled: !rule1.attributes.enabled },
{ ...rule2.attributes, enabled: !rule2.attributes.enabled },
]);
expect(mutate).toHaveBeenCalledWith({
packagePolicyId: params.packagePolicyId,
savedObjectRules: [
getSavedObjectRuleEnable(rule1, !rule1.attributes.enabled),
getSavedObjectRuleEnable(rule2, !rule2.attributes.enabled),
],
});
});

it('updates rules with local changes done by bulk toggles', () => {
Expand Down Expand Up @@ -217,9 +267,10 @@ describe('<RulesContainer />', () => {
fireEvent.click(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_SAVE_BUTTON));

expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith([
{ ...rule1.attributes, enabled: !rule1.attributes.enabled },
]);
expect(mutate).toHaveBeenCalledWith({
packagePolicyId: params.packagePolicyId,
savedObjectRules: [getSavedObjectRuleEnable(rule1, !rule1.attributes.enabled)],
});
});

it('only changes selected rules in bulk operations', () => {
Expand Down Expand Up @@ -254,11 +305,14 @@ describe('<RulesContainer />', () => {
fireEvent.click(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_SAVE_BUTTON));

expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith([
{ ...rule1.attributes, enabled: !rule1.attributes.enabled },
{ ...rule4.attributes, enabled: !rule4.attributes.enabled },
{ ...rule5.attributes, enabled: !rule5.attributes.enabled },
]);
expect(mutate).toHaveBeenCalledWith({
packagePolicyId: params.packagePolicyId,
savedObjectRules: [
getSavedObjectRuleEnable(rule1, !rule1.attributes.enabled),
getSavedObjectRuleEnable(rule4, !rule4.attributes.enabled),
getSavedObjectRuleEnable(rule5, !rule5.attributes.enabled),
],
});
});

it('updates rules with changes of both bulk/non-bulk toggles', () => {
Expand Down Expand Up @@ -295,11 +349,14 @@ describe('<RulesContainer />', () => {
fireEvent.click(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_SAVE_BUTTON));

expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith([
{ ...rule1.attributes, enabled: !rule1.attributes.enabled },
{ ...rule4.attributes, enabled: !rule4.attributes.enabled },
{ ...rule5.attributes, enabled: !rule5.attributes.enabled },
]);
expect(mutate).toHaveBeenCalledWith({
packagePolicyId: params.packagePolicyId,
savedObjectRules: [
getSavedObjectRuleEnable(rule1, !rule1.attributes.enabled),
getSavedObjectRuleEnable(rule4, !rule4.attributes.enabled),
getSavedObjectRuleEnable(rule5, !rule5.attributes.enabled),
],
});
});

it('selects and updates all rules', async () => {
Expand Down Expand Up @@ -329,12 +386,12 @@ describe('<RulesContainer />', () => {
fireEvent.click(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_SAVE_BUTTON));

expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith(
rules.map((rule) => ({
...rule.attributes,
enabled: !enabled,
}))
);
expect(mutate).toHaveBeenCalledWith({
packagePolicyId: params.packagePolicyId,
savedObjectRules: rules.map((rule) =>
getSavedObjectRuleEnable(rule, !rule.attributes.enabled)
),
});
});

it('updates the rules from within the flyout', () => {
Expand Down Expand Up @@ -370,6 +427,9 @@ describe('<RulesContainer />', () => {
const { mutate } = useBulkUpdateCspRules();

expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith([{ ...rule.attributes, enabled: !enabled }]);
expect(mutate).toHaveBeenCalledWith({
packagePolicyId: params.packagePolicyId,
savedObjectRules: [getSavedObjectRuleEnable(rule, !rule.attributes.enabled)],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { useParams } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import { cspRuleAssetSavedObjectType } from '../../../common/schemas/csp_rule';
import { extractErrorMessage, isNonNullable } from '../../../common/utils/helpers';
import { RulesTable } from './rules_table';
import { RulesBottomBar } from './rules_bottom_bar';
Expand Down Expand Up @@ -81,7 +82,7 @@ const getRulesPageData = (
error: error ? extractErrorMessage(error) : undefined,
all_rules: rules,
rules_map: new Map(rules.map((rule) => [rule.id, rule])),
rules_page: page.map((rule) => changedRules.get(rule.attributes.id) || rule),
rules_page: page.map((rule) => changedRules.get(rule.id) || rule),
total: data?.total || 0,
lastModified: getLastModified(rules) || null,
};
Expand All @@ -107,9 +108,15 @@ export const RulesContainer = () => {
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
const [isAllSelected, setIsAllSelected] = useState<boolean>(false);
const [visibleSelectedRulesIds, setVisibleSelectedRulesIds] = useState<string[]>([]);
const [rulesQuery, setRulesQuery] = useState<RulesQuery>({ page: 0, perPage: 5, search: '' });
const [rulesQuery, setRulesQuery] = useState<RulesQuery>({
filter: `${cspRuleAssetSavedObjectType}.attributes.policy_id: "${params.policyId}" and ${cspRuleAssetSavedObjectType}.attributes.package_policy_id: "${params.packagePolicyId}"`,
search: '',
page: 0,
perPage: 5,
});

const { data, status, error, refetch } = useFindCspRules({
filter: rulesQuery.filter,
search: getSimpleQueryString(rulesQuery.search),
page: 1,
perPage: MAX_ITEMS_PER_PAGE,
Expand Down Expand Up @@ -152,7 +159,11 @@ export const RulesContainer = () => {

const toggleRule = (rule: RuleSavedObject) => toggleRules([rule], !rule.attributes.enabled);

const bulkUpdateRules = () => bulkUpdate([...changedRules].map(([, rule]) => rule.attributes));
const bulkUpdateRules = () =>
bulkUpdate({
savedObjectRules: [...changedRules].map(([, savedObjectRule]) => savedObjectRule),
packagePolicyId: params.packagePolicyId,
});

const discardChanges = useCallback(() => setChangedRules(new Map()), []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const RuleOverviewTab = ({ rule, toggleRule }: { rule: RuleSavedObject; toggleRu
label={TEXT.ACTIVATED}
checked={rule.attributes.enabled}
onChange={toggleRule}
data-test-subj={TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule.attributes.id)}
data-test-subj={TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule.id)}
/>
</EuiToolTip>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const getColumns = ({
label={enabled ? TEXT.DISABLE : TEXT.ENABLE}
checked={enabled}
onChange={() => toggleRule(rule)}
data-test-subj={TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule.attributes.id)}
data-test-subj={TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule.id)}
/>
</EuiToolTip>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { useQuery, useMutation, useQueryClient } from 'react-query';
import { FunctionKeys } from 'utility-types';
import type { SavedObjectsFindOptions, SimpleSavedObject } from '@kbn/core/public';
import { UPDATE_RULES_CONFIG_ROUTE_PATH } from '../../../common/constants';
import { cspRuleAssetSavedObjectType, type CspRuleSchema } from '../../../common/schemas/csp_rule';
import { useKibana } from '../../common/hooks/use_kibana';
import { UPDATE_FAILED } from './translations';
Expand All @@ -16,11 +17,14 @@ export type RuleSavedObject = Omit<
FunctionKeys<SimpleSavedObject>
>;

export type RulesQuery = Required<Pick<SavedObjectsFindOptions, 'search' | 'page' | 'perPage'>>;
export type RulesQuery = Required<
Pick<SavedObjectsFindOptions, 'search' | 'page' | 'perPage' | 'filter'>
>;
export type RulesQueryResult = ReturnType<typeof useFindCspRules>;

export const useFindCspRules = ({ search, page, perPage }: RulesQuery) => {
export const useFindCspRules = ({ search, page, perPage, filter }: RulesQuery) => {
const { savedObjects } = useKibana().services;

return useQuery(
[cspRuleAssetSavedObjectType, { search, page, perPage }],
() =>
Expand All @@ -32,24 +36,37 @@ export const useFindCspRules = ({ search, page, perPage }: RulesQuery) => {
// NOTE: 'name.raw' is a field mapping we defined on 'name'
sortField: 'name.raw',
perPage,
filter,
}),
{ refetchOnWindowFocus: false }
);
};

export const useBulkUpdateCspRules = () => {
const { savedObjects, notifications } = useKibana().services;
const { savedObjects, notifications, http } = useKibana().services;
const queryClient = useQueryClient();

return useMutation(
(rules: CspRuleSchema[]) =>
savedObjects.client.bulkUpdate<CspRuleSchema>(
rules.map((rule) => ({
async ({
savedObjectRules,
packagePolicyId,
}: {
savedObjectRules: RuleSavedObject[];
packagePolicyId: CspRuleSchema['package_policy_id'];
}) => {
await savedObjects.client.bulkUpdate<RuleSavedObject>(
savedObjectRules.map((savedObjectRule) => ({
type: cspRuleAssetSavedObjectType,
id: rule.id,
attributes: rule,
id: savedObjectRule.id,
attributes: savedObjectRule.attributes,
}))
),
);
await http.post(UPDATE_RULES_CONFIG_ROUTE_PATH, {
body: JSON.stringify({
package_policy_id: packagePolicyId,
}),
});
},
{
onError: (err) => {
if (err instanceof Error) notifications.toasts.addError(err, { title: UPDATE_FAILED });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const defineUpdateRulesConfigRoute = (router: CspRouter, cspContext: CspA
router.post(
{
path: UPDATE_RULES_CONFIG_ROUTE_PATH,
validate: { query: configurationUpdateInputSchema },
validate: { body: configurationUpdateInputSchema },
},
async (context, request, response) => {
if (!(await context.fleet).authz.fleet.all) {
Expand All @@ -118,7 +118,7 @@ export const defineUpdateRulesConfigRoute = (router: CspRouter, cspContext: CspA
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const soClient = coreContext.savedObjects.client;
const packagePolicyService = cspContext.service.packagePolicyService;
const packagePolicyId = request.query.package_policy_id;
const packagePolicyId = request.body.package_policy_id;

if (!packagePolicyService) {
throw new Error(`Failed to get Fleet services`);
Expand Down

0 comments on commit 9d7b459

Please sign in to comment.