Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Add a rule management filters internal endpoint #146826

Merged
merged 37 commits into from
Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f5bfe34
add a rule management filters internal endpoint
maximpn Dec 1, 2022
8b794e1
rename the custom rules count field
maximpn Dec 2, 2022
a7142fd
add functional tests
maximpn Dec 2, 2022
91f3854
use the new added endpoint at the UI
maximpn Dec 2, 2022
f9606fa
add tags field for the rule management filters endpoint
maximpn Dec 2, 2022
ed7eda6
fetch tags from the new endpoint
maximpn Dec 3, 2022
6aca19b
rename the new endpoint
maximpn Dec 3, 2022
2c64e98
remove unused translation
maximpn Dec 3, 2022
d8d3b09
avoid extending default rule testing data
maximpn Dec 4, 2022
7c991dc
use the new endpoint in the rules table
maximpn Dec 4, 2022
d384fa0
rename i18n constant
maximpn Dec 4, 2022
27498d5
fix tags data invalidation after bulk tags update
maximpn Dec 4, 2022
4d58880
get rid of outdated naming
maximpn Dec 4, 2022
7bccfa8
fix an error message
maximpn Dec 5, 2022
a6491df
simplify unit response schema unit tests
maximpn Dec 5, 2022
d348da7
add explicit @ts-expect-error for testing invalid values
maximpn Dec 5, 2022
b5545e9
get rid of unnecessary prebuilt rules status invalidation
maximpn Dec 5, 2022
26345c2
replace readTags with the new implementation
maximpn Dec 5, 2022
8872d6d
update the comment
maximpn Dec 5, 2022
f37d1ce
split rule management filters response into categories
maximpn Dec 5, 2022
7a8ec6e
update rules filters fetch failure message
maximpn Dec 5, 2022
52bd762
rename the new endpoint
maximpn Dec 5, 2022
51bc5fd
display rules table without waiting for filters to load
maximpn Dec 5, 2022
30496a6
restructure integration tests
maximpn Dec 5, 2022
44942f1
revert prebuilt rules invalidation upon rules deletion
maximpn Dec 6, 2022
aa374e9
fix integration tests
maximpn Dec 6, 2022
113a356
remove unused local functions
maximpn Dec 8, 2022
8012ce5
increase the max number of tags
maximpn Dec 8, 2022
6eadf46
rename unrenamed leftovers
maximpn Dec 8, 2022
4f8d0ef
removed unused core context
maximpn Dec 8, 2022
6022760
add max tags limit
maximpn Dec 8, 2022
665ee5e
fetch only security rule tags
maximpn Dec 17, 2022
8f6f904
add tags number limit unit tests
maximpn Dec 17, 2022
9755803
add a functional test on the tags limit
maximpn Dec 17, 2022
f153369
fix linting errors
maximpn Dec 17, 2022
4317558
Merge branch 'main' into add-rule-filters-endpoint
kibanamachine Dec 17, 2022
f6af276
Merge branch 'main' into add-rule-filters-endpoint
kibanamachine Dec 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/server/rules_client.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type RulesClientMock = jest.Mocked<Schema>;

const createRulesClientMock = () => {
const mocked: RulesClientMock = {
aggregate: jest.fn(),
aggregate: jest.fn().mockReturnValue({ alertExecutionStatus: {}, ruleLastRunOutcome: {} }),
create: jest.fn(),
get: jest.fn(),
resolve: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface AggregateOptions extends IndexType {
id: string;
};
filter?: string | KueryNode;
maxTags?: number;
}

interface IndexType {
Expand Down Expand Up @@ -79,7 +80,9 @@ export interface RuleAggregation {

export async function aggregate(
context: RulesClientContext,
{ options: { fields, filter, ...options } = {} }: { options?: AggregateOptions } = {}
{
options: { fields, filter, maxTags = 50, ...options } = {},
}: { options?: AggregateOptions } = {}
): Promise<AggregateResult> {
let authorizationTuple;
try {
Expand Down Expand Up @@ -123,7 +126,7 @@ export async function aggregate(
terms: { field: 'alert.attributes.muteAll' },
},
tags: {
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 },
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: maxTags },
},
snoozed: {
nested: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,38 @@ describe('aggregate()', () => {
})
);
});

describe('tags number limit', () => {
test('sets to default (50) if it is not provided', async () => {
const rulesClient = new RulesClient(rulesClientParams);

await rulesClient.aggregate();

expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([
{
aggs: {
tags: {
terms: { size: 50 },
},
},
},
]);
});

test('sets to the provided value', async () => {
const rulesClient = new RulesClient(rulesClientParams);

await rulesClient.aggregate({ options: { maxTags: 1000 } });

expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([
{
aggs: {
tags: {
terms: { size: 1000 },
},
},
},
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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 { left } from 'fp-ts/lib/Either';
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';

import { RuleManagementFiltersResponse } from './response_schema';

describe('Rule management filters response schema', () => {
test('it should validate an empty response with defaults', () => {
const payload: RuleManagementFiltersResponse = {
rules_summary: {
custom_count: 0,
prebuilt_installed_count: 0,
},
aggregated_fields: {
tags: [],
},
};
const decoded = RuleManagementFiltersResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should validate an non empty response with defaults', () => {
const payload: RuleManagementFiltersResponse = {
rules_summary: {
custom_count: 10,
prebuilt_installed_count: 20,
},
aggregated_fields: {
tags: ['a', 'b', 'c'],
},
};
const decoded = RuleManagementFiltersResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should not validate an extra invalid field added', () => {
const payload: RuleManagementFiltersResponse & { invalid_field: string } = {
rules_summary: {
custom_count: 0,
prebuilt_installed_count: 0,
},
aggregated_fields: {
tags: [],
},
invalid_field: 'invalid',
};
const decoded = RuleManagementFiltersResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']);
expect(message.schema).toEqual({});
});

test('it should NOT validate an empty response with a negative "summary.prebuilt_installed_count" number', () => {
const payload: RuleManagementFiltersResponse = {
rules_summary: {
custom_count: 0,
prebuilt_installed_count: -1,
},
aggregated_fields: {
tags: [],
},
};
const decoded = RuleManagementFiltersResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "rules_summary,prebuilt_installed_count"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate an empty response with a negative "summary.custom_count"', () => {
const payload: RuleManagementFiltersResponse = {
rules_summary: {
custom_count: -1,
prebuilt_installed_count: 0,
},
aggregated_fields: {
tags: [],
},
};
const decoded = RuleManagementFiltersResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "rules_summary,custom_count"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate an empty prepackaged response if "summary.prebuilt_installed_count" is not there', () => {
const payload: RuleManagementFiltersResponse = {
rules_summary: {
custom_count: 0,
prebuilt_installed_count: 0,
},
aggregated_fields: {
tags: [],
},
};
// @ts-expect-error
delete payload.rules_summary.prebuilt_installed_count;
const decoded = RuleManagementFiltersResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "rules_summary,prebuilt_installed_count"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate an empty response with wrong "aggregated_fields.tags"', () => {
const payload: RuleManagementFiltersResponse = {
rules_summary: {
custom_count: 0,
prebuilt_installed_count: 0,
},
aggregated_fields: {
// @ts-expect-error Passing an invalid value for the test
tags: [1],
},
};
const decoded = RuleManagementFiltersResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "aggregated_fields,tags"',
]);
expect(message.schema).toEqual({});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 * as t from 'io-ts';
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';

export type RuleManagementFiltersResponse = t.TypeOf<typeof RuleManagementFiltersResponse>;
export const RuleManagementFiltersResponse = t.exact(
t.type({
rules_summary: t.type({
custom_count: PositiveInteger,
prebuilt_installed_count: PositiveInteger,
}),
aggregated_fields: t.type({
tags: t.array(t.string),
}),
})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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 { INTERNAL_DETECTION_ENGINE_URL } from '../../../constants';

export const RULE_MANAGEMENT_FILTERS_URL = `${INTERNAL_DETECTION_ENGINE_URL}/rules/_rule_management_filters`;
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
createPrepackagedRules,
importRules,
exportRules,
fetchTags,
getPrePackagedRulesStatus,
previewRule,
findRuleExceptionReferences,
Expand Down Expand Up @@ -630,26 +629,6 @@ describe('Detections Rules API', () => {
});
});

describe('fetchTags', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(['some', 'tags']);
});

test('check parameter url when fetching tags', async () => {
await fetchTags({ signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', {
signal: abortCtrl.signal,
method: 'GET',
});
});

test('happy path', async () => {
const resp = await fetchTags({ signal: abortCtrl.signal });
expect(resp).toEqual(['some', 'tags']);
});
});

describe('getPrePackagedRulesStatus', () => {
const prePackagedRulesStatus = {
rules_custom_installed: 33,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import type {
ExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';

import type { RuleManagementFiltersResponse } from '../../../../common/detection_engine/rule_management/api/rules/filters/response_schema';
import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../common/detection_engine/rule_management/api/urls';
import type { BulkActionsDryRunErrCode } from '../../../../common/constants';
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_PREVIEW,
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_RULES_URL_FIND,
DETECTION_ENGINE_TAGS_URL,
} from '../../../../common/constants';

import {
Expand All @@ -33,7 +34,6 @@ import type {
BulkActionDuplicatePayload,
} from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
import { BulkActionType } from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';

import type {
RuleResponse,
PreviewResponse,
Expand Down Expand Up @@ -388,17 +388,19 @@ export const exportRules = async ({
});
};

export type FetchTagsResponse = string[];

/**
* Fetch all unique Tags used by Rules
* Fetch rule filters related information like installed rules count, tags and etc
*
* @param signal to cancel request
*
* @throws An error if response is not OK
*/
export const fetchTags = async ({ signal }: { signal?: AbortSignal }): Promise<FetchTagsResponse> =>
KibanaServices.get().http.fetch<FetchTagsResponse>(DETECTION_ENGINE_TAGS_URL, {
export const fetchRuleManagementFilters = async ({
signal,
}: {
signal?: AbortSignal;
}): Promise<RuleManagementFiltersResponse> =>
KibanaServices.get().http.fetch<RuleManagementFiltersResponse>(RULE_MANAGEMENT_FILTERS_URL, {
method: 'GET',
signal,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import type { IHttpFetchError } from '@kbn/core/public';
import { BulkActionType } from '../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
import type { BulkActionErrorResponse, BulkActionResponse, PerformBulkActionProps } from '../api';
import { performBulkAction } from '../api';
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants';
import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query';
import { useInvalidateFindRulesQuery, useUpdateRulesCache } from './use_find_rules_query';
import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query';
import { useInvalidateFetchRuleByIdQuery } from './use_fetch_rule_by_id_query';
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants';
import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query';

export const BULK_ACTION_MUTATION_KEY = ['POST', DETECTION_ENGINE_RULES_BULK_ACTION];

Expand All @@ -27,7 +27,7 @@ export const useBulkActionMutation = (
) => {
const invalidateFindRulesQuery = useInvalidateFindRulesQuery();
const invalidateFetchRuleByIdQuery = useInvalidateFetchRuleByIdQuery();
const invalidateFetchTagsQuery = useInvalidateFetchTagsQuery();
const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery();
const invalidateFetchPrebuiltRulesStatusQuery = useInvalidateFetchPrebuiltRulesStatusQuery();
const updateRulesCache = useUpdateRulesCache();

Expand Down Expand Up @@ -66,12 +66,12 @@ export const useBulkActionMutation = (
case BulkActionType.delete:
invalidateFindRulesQuery();
invalidateFetchRuleByIdQuery();
invalidateFetchTagsQuery();
invalidateFetchRuleManagementFilters();
invalidateFetchPrebuiltRulesStatusQuery();
break;
case BulkActionType.duplicate:
invalidateFindRulesQuery();
invalidateFetchPrebuiltRulesStatusQuery();
invalidateFetchRuleManagementFilters();
break;
case BulkActionType.edit:
if (updatedRules) {
Expand All @@ -82,7 +82,7 @@ export const useBulkActionMutation = (
invalidateFindRulesQuery();
}
invalidateFetchRuleByIdQuery();
invalidateFetchTagsQuery();
invalidateFetchRuleManagementFilters();
break;
}

Expand Down
Loading