[7.6] [SIEM] Detections container/rules unit tests (#58055) (#58138)

* [SIEM] Detections container/rules unit tests (#58055)

* add unit test for rules api

* add unit test for useFetchIndexPatterns

* fix useFetchIndexPatterns and add unit test for usePersistRule

* add more unit test for container/rules

* review

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

* fix types + adapt test to the old fetch way

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Xavier Mouligneau 2020-02-24 19:53:17 -05:00 committed by GitHub
parent b55d8f35ed
commit 5d333d5de0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 2292 additions and 18 deletions

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
AddRulesProps,
NewRule,
PrePackagedRulesStatusResponse,
BasicFetchProps,
RuleStatusResponse,
Rule,
FetchRuleProps,
FetchRulesResponse,
FetchRulesProps,
} from '../types';
import { ruleMock, savedRuleMock, rulesMock } from '../mock';
export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> =>
Promise.resolve(ruleMock);
export const getPrePackagedRulesStatus = async ({
signal,
}: {
signal: AbortSignal;
}): Promise<PrePackagedRulesStatusResponse> =>
Promise.resolve({
rules_custom_installed: 33,
rules_installed: 12,
rules_not_installed: 0,
rules_not_updated: 0,
});
export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> =>
Promise.resolve(true);
export const getRuleStatusById = async ({
id,
signal,
}: {
id: string;
signal: AbortSignal;
}): Promise<RuleStatusResponse> =>
Promise.resolve({
myOwnRuleID: {
current_status: {
alert_id: 'alertId',
status_date: 'mm/dd/yyyyTHH:MM:sssz',
status: 'succeeded',
last_failure_at: null,
last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
last_failure_message: null,
last_success_message: 'it is a success',
},
failures: [],
},
});
export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> =>
Promise.resolve(savedRuleMock);
export const fetchRules = async ({
filterOptions = {
filter: '',
sortField: 'enabled',
sortOrder: 'desc',
showCustomRules: false,
showElasticRules: false,
tags: [],
},
pagination = {
page: 1,
perPage: 20,
total: 0,
},
signal,
}: FetchRulesProps): Promise<FetchRulesResponse> => Promise.resolve(rulesMock);
export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> =>
Promise.resolve(['elastic', 'love', 'quality', 'code']);

View file

@ -0,0 +1,876 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
addRule,
fetchRules,
fetchRuleById,
enableRules,
deleteRules,
duplicateRules,
createPrepackagedRules,
importRules,
exportRules,
getRuleStatusById,
fetchTags,
getPrePackagedRulesStatus,
} from './api';
import { ruleMock, rulesMock } from './mock';
import { ToasterErrors } from '../../../components/ml/api/throw_if_not_ok';
import { globalNode } from '../../../mock';
const abortCtrl = new AbortController();
jest.mock('ui/chrome', () => ({
getBasePath: () => {
return '';
},
getUiSettingsClient: () => ({
get: jest.fn(),
}),
}));
const mockfetchSuccess = (body: unknown, fetchMock?: jest.Mock) => {
if (fetchMock) {
globalNode.window.fetch = fetchMock;
} else {
globalNode.window.fetch = () => ({
ok: true,
message: 'success',
text: 'success',
body,
json: () => body,
blob: () => body,
});
}
};
const mockfetchError = () => {
globalNode.window.fetch = () => ({
ok: false,
text: () =>
JSON.stringify({
message: 'super mega error, it is not that bad',
}),
body: null,
});
};
describe('Detections Rules API', () => {
const fetchMock = jest.fn();
describe('addRule', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url, body', async () => {
mockfetchSuccess(null, fetchMock);
await addRule({ rule: ruleMock, signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
body:
'{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[]}',
method: 'POST',
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
mockfetchSuccess(ruleMock);
const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal });
expect(ruleResp).toEqual(ruleMock);
});
test('unhappy path', async () => {
mockfetchError();
try {
await addRule({ rule: ruleMock, signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('fetchRules', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: rulesMock,
json: () => rulesMock,
}));
});
test('check parameter url, query without any options', async () => {
mockfetchSuccess(null, fetchMock);
await fetchRules({ signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_find?page=1&per_page=20&sort_field=enabled&sort_order=desc',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
}
);
});
test('check parameter url, query with a filter', async () => {
mockfetchSuccess(null, fetchMock);
await fetchRules({
filterOptions: {
filter: 'hello world',
sortField: 'enabled',
sortOrder: 'desc',
showCustomRules: false,
showElasticRules: false,
tags: [],
},
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_find?page=1&per_page=20&sort_field=enabled&sort_order=desc&filter=alert.attributes.name:%20hello%20world',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
}
);
});
test('check parameter url, query with showCustomRules', async () => {
mockfetchSuccess(null, fetchMock);
await fetchRules({
filterOptions: {
filter: '',
sortField: 'enabled',
sortOrder: 'desc',
showCustomRules: true,
showElasticRules: false,
tags: [],
},
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_find?page=1&per_page=20&sort_field=enabled&sort_order=desc&filter=alert.attributes.tags:%20%22__internal_immutable:false%22',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
}
);
});
test('check parameter url, query with showElasticRules', async () => {
mockfetchSuccess(null, fetchMock);
await fetchRules({
filterOptions: {
filter: '',
sortField: 'enabled',
sortOrder: 'desc',
showCustomRules: false,
showElasticRules: true,
tags: [],
},
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_find?page=1&per_page=20&sort_field=enabled&sort_order=desc&filter=alert.attributes.tags:%20%22__internal_immutable:true%22',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
}
);
});
test('check parameter url, query with tags', async () => {
mockfetchSuccess(null, fetchMock);
await fetchRules({
filterOptions: {
filter: '',
sortField: 'enabled',
sortOrder: 'desc',
showCustomRules: false,
showElasticRules: false,
tags: ['hello', 'world'],
},
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_find?page=1&per_page=20&sort_field=enabled&sort_order=desc&filter=alert.attributes.tags:hello%20AND%20alert.attributes.tags:world',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
}
);
});
test('check parameter url, query with all options', async () => {
mockfetchSuccess(null, fetchMock);
await fetchRules({
filterOptions: {
filter: 'ruleName',
sortField: 'enabled',
sortOrder: 'desc',
showCustomRules: true,
showElasticRules: true,
tags: ['hello', 'world'],
},
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_find?page=1&per_page=20&sort_field=enabled&sort_order=desc&filter=alert.attributes.name:%20ruleName%20AND%20alert.attributes.tags:%20%22__internal_immutable:false%22%20AND%20alert.attributes.tags:%20%22__internal_immutable:true%22%20AND%20alert.attributes.tags:hello%20AND%20alert.attributes.tags:world',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
}
);
});
test('happy path', async () => {
mockfetchSuccess(rulesMock);
const rulesResp = await fetchRules({ signal: abortCtrl.signal });
expect(rulesResp).toEqual(rulesMock);
});
test('unhappy path', async () => {
mockfetchError();
try {
await fetchRules({ signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('fetchRuleById', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url, query', async () => {
mockfetchSuccess(null, fetchMock);
await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules?id=mySuperRuleId', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
mockfetchSuccess(ruleMock);
const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal });
expect(ruleResp).toEqual(ruleMock);
});
test('unhappy path', async () => {
mockfetchError();
try {
await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('enableRules', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url, body when enabling rules', async () => {
mockfetchSuccess(null, fetchMock);
await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
body: '[{"id":"mySuperRuleId","enabled":true},{"id":"mySuperRuleId_II","enabled":true}]',
method: 'PATCH',
});
});
test('check parameter url, body when disabling rules', async () => {
mockfetchSuccess(null, fetchMock);
await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: false });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
body: '[{"id":"mySuperRuleId","enabled":false},{"id":"mySuperRuleId_II","enabled":false}]',
method: 'PATCH',
});
});
test('happy path', async () => {
mockfetchSuccess(rulesMock.data);
const ruleResp = await enableRules({
ids: ['mySuperRuleId', 'mySuperRuleId_II'],
enabled: true,
});
expect(ruleResp).toEqual(rulesMock.data);
});
test('unhappy path', async () => {
mockfetchError();
try {
await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('deleteRules', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url, body when deleting rules', async () => {
mockfetchSuccess(null, fetchMock);
await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_delete', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
body: '[{"id":"mySuperRuleId"},{"id":"mySuperRuleId_II"}]',
method: 'DELETE',
});
});
test('happy path', async () => {
mockfetchSuccess(ruleMock);
const ruleResp = await deleteRules({
ids: ['mySuperRuleId', 'mySuperRuleId_II'],
});
expect(ruleResp).toEqual(ruleMock);
});
test('unhappy path', async () => {
mockfetchError();
try {
await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('duplicateRules', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url, body when duplicating rules', async () => {
mockfetchSuccess(null, fetchMock);
await duplicateRules({ rules: rulesMock.data });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
body:
'[{"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1},{"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1}]',
method: 'POST',
});
});
test('happy path', async () => {
mockfetchSuccess(rulesMock.data);
const ruleResp = await duplicateRules({ rules: rulesMock.data });
expect(ruleResp).toEqual(rulesMock.data);
});
test('unhappy path', async () => {
mockfetchError();
try {
await duplicateRules({ rules: rulesMock.data });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('createPrepackagedRules', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url when creating pre-packaged rules', async () => {
mockfetchSuccess(null, fetchMock);
await createPrepackagedRules({ signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'PUT',
});
});
test('happy path', async () => {
mockfetchSuccess(true);
const resp = await createPrepackagedRules({ signal: abortCtrl.signal });
expect(resp).toEqual(true);
});
test('unhappy path', async () => {
mockfetchError();
try {
await createPrepackagedRules({ signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('importRules', () => {
const fileToImport: File = {
lastModified: 33,
name: 'fileToImport',
size: 89,
type: 'json',
arrayBuffer: jest.fn(),
slice: jest.fn(),
stream: jest.fn(),
text: jest.fn(),
} as File;
const formData = new FormData();
formData.append('file', fileToImport);
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url, body and query when importing rules', async () => {
mockfetchSuccess(null, fetchMock);
await importRules({ fileToImport, signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_import?overwrite=false',
{
credentials: 'same-origin',
headers: {
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'POST',
body: formData,
}
);
});
test('check parameter url, body and query when importing rules with overwrite', async () => {
mockfetchSuccess(null, fetchMock);
await importRules({ fileToImport, overwrite: true, signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import?overwrite=true', {
credentials: 'same-origin',
headers: {
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'POST',
body: formData,
});
});
test('happy path', async () => {
mockfetchSuccess({
success: true,
success_count: 33,
errors: [],
});
const resp = await importRules({ fileToImport, signal: abortCtrl.signal });
expect(resp).toEqual({
success: true,
success_count: 33,
errors: [],
});
});
test('unhappy path', async () => {
mockfetchError();
try {
await importRules({ fileToImport, signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('exportRules', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
blob: () => ruleMock,
}));
});
test('check parameter url, body and query when exporting rules', async () => {
mockfetchSuccess(null, fetchMock);
await exportRules({
ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'],
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_export?exclude_export_details=false&file_name=rules_export.ndjson',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'POST',
body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}',
}
);
});
test('check parameter url, body and query when exporting rules with excludeExportDetails', async () => {
mockfetchSuccess(null, fetchMock);
await exportRules({
excludeExportDetails: true,
ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'],
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_export?exclude_export_details=true&file_name=rules_export.ndjson',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'POST',
body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}',
}
);
});
test('check parameter url, body and query when exporting rules with fileName', async () => {
mockfetchSuccess(null, fetchMock);
await exportRules({
filename: 'myFileName.ndjson',
ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'],
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_export?exclude_export_details=false&file_name=myFileName.ndjson',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'POST',
body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}',
}
);
});
test('check parameter url, body and query when exporting rules with all options', async () => {
mockfetchSuccess(null, fetchMock);
await exportRules({
excludeExportDetails: true,
filename: 'myFileName.ndjson',
ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'],
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_export?exclude_export_details=true&file_name=myFileName.ndjson',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'POST',
body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}',
}
);
});
test('happy path', async () => {
const blob: Blob = {
size: 89,
type: 'json',
arrayBuffer: jest.fn(),
slice: jest.fn(),
stream: jest.fn(),
text: jest.fn(),
} as Blob;
mockfetchSuccess(blob);
const resp = await exportRules({
ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'],
signal: abortCtrl.signal,
});
expect(resp).toEqual(blob);
});
test('unhappy path', async () => {
mockfetchError();
try {
await exportRules({
ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'],
signal: abortCtrl.signal,
});
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('getRuleStatusById', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url, query', async () => {
mockfetchSuccess(null, fetchMock);
await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_find_statuses?ids=%5B%22mySuperRuleId%22%5D',
{
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
method: 'GET',
signal: abortCtrl.signal,
}
);
});
test('happy path', async () => {
const statusMock = {
myRule: {
current_status: {
alert_id: 'alertId',
status_date: 'mm/dd/yyyyTHH:MM:sssz',
status: 'succeeded',
last_failure_at: null,
last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
last_failure_message: null,
last_success_message: 'it is a success',
},
failures: [],
},
};
mockfetchSuccess(statusMock);
const ruleResp = await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal });
expect(ruleResp).toEqual(statusMock);
});
test('unhappy path', async () => {
mockfetchError();
try {
await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('fetchTags', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url when fetching tags', async () => {
mockfetchSuccess(null, fetchMock);
await fetchTags({ signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'GET',
});
});
test('happy path', async () => {
mockfetchSuccess(['hello', 'tags']);
const resp = await fetchTags({ signal: abortCtrl.signal });
expect(resp).toEqual(['hello', 'tags']);
});
test('unhappy path', async () => {
mockfetchError();
try {
await fetchTags({ signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
describe('getPrePackagedRulesStatus', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockImplementation(() => ({
ok: true,
message: 'success',
text: 'success',
body: ruleMock,
json: () => ruleMock,
}));
});
test('check parameter url when fetching tags', async () => {
mockfetchSuccess(null, fetchMock);
await getPrePackagedRulesStatus({ signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged/_status', {
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal: abortCtrl.signal,
method: 'GET',
});
});
test('happy path', async () => {
const prePackagesRulesStatus = {
rules_custom_installed: 33,
rules_installed: 12,
rules_not_installed: 0,
rules_not_updated: 2,
};
mockfetchSuccess(prePackagesRulesStatus);
const resp = await getPrePackagedRulesStatus({ signal: abortCtrl.signal });
expect(resp).toEqual(prePackagesRulesStatus);
});
test('unhappy path', async () => {
mockfetchError();
try {
await getPrePackagedRulesStatus({ signal: abortCtrl.signal });
} catch (exp) {
expect(exp).toBeInstanceOf(ToasterErrors);
expect(exp.message).toEqual('super mega error, it is not that bad');
}
});
});
});

View file

@ -49,7 +49,6 @@ export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule>
body: JSON.stringify(rule),
signal,
});
await throwIfNotOk(response);
return response.json();
};

View file

@ -0,0 +1,460 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { defaultIndexPattern } from '../../../../default_index_pattern';
import { useApolloClient } from '../../../utils/apollo_context';
import { mocksSource } from '../../source/mock';
import { useFetchIndexPatterns, Return } from './fetch_index_patterns';
const mockUseApolloClient = useApolloClient as jest.Mock;
jest.mock('../../../utils/apollo_context');
describe('useFetchIndexPatterns', () => {
beforeEach(() => {
mockUseApolloClient.mockClear();
});
test('happy path', async () => {
await act(async () => {
mockUseApolloClient.mockImplementation(() => ({
query: () => Promise.resolve(mocksSource[0].result),
}));
const { result, waitForNextUpdate } = renderHook<unknown, Return>(() =>
useFetchIndexPatterns(defaultIndexPattern)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual([
{
browserFields: {
base: {
fields: {
'@timestamp': {
category: 'base',
description:
'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.',
example: '2016-05-23T08:05:34.853Z',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: '@timestamp',
searchable: true,
type: 'date',
aggregatable: true,
},
},
},
agent: {
fields: {
'agent.ephemeral_id': {
category: 'agent',
description:
'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.',
example: '8a4f500f',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.ephemeral_id',
searchable: true,
type: 'string',
aggregatable: true,
},
'agent.hostname': {
category: 'agent',
description: null,
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.hostname',
searchable: true,
type: 'string',
aggregatable: true,
},
'agent.id': {
category: 'agent',
description:
'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.',
example: '8a4f500d',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.id',
searchable: true,
type: 'string',
aggregatable: true,
},
'agent.name': {
category: 'agent',
description:
'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.',
example: 'foo',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.name',
searchable: true,
type: 'string',
aggregatable: true,
},
},
},
auditd: {
fields: {
'auditd.data.a0': {
category: 'auditd',
description: null,
example: null,
format: '',
indexes: ['auditbeat'],
name: 'auditd.data.a0',
searchable: true,
type: 'string',
aggregatable: true,
},
'auditd.data.a1': {
category: 'auditd',
description: null,
example: null,
format: '',
indexes: ['auditbeat'],
name: 'auditd.data.a1',
searchable: true,
type: 'string',
aggregatable: true,
},
'auditd.data.a2': {
category: 'auditd',
description: null,
example: null,
format: '',
indexes: ['auditbeat'],
name: 'auditd.data.a2',
searchable: true,
type: 'string',
aggregatable: true,
},
},
},
client: {
fields: {
'client.address': {
category: 'client',
description:
'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.address',
searchable: true,
type: 'string',
aggregatable: true,
},
'client.bytes': {
category: 'client',
description: 'Bytes sent from the client to the server.',
example: '184',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.bytes',
searchable: true,
type: 'number',
aggregatable: true,
},
'client.domain': {
category: 'client',
description: 'Client domain.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.domain',
searchable: true,
type: 'string',
aggregatable: true,
},
'client.geo.country_iso_code': {
category: 'client',
description: 'Country ISO code.',
example: 'CA',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.geo.country_iso_code',
searchable: true,
type: 'string',
aggregatable: true,
},
},
},
cloud: {
fields: {
'cloud.account.id': {
category: 'cloud',
description:
'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.',
example: '666777888999',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'cloud.account.id',
searchable: true,
type: 'string',
aggregatable: true,
},
'cloud.availability_zone': {
category: 'cloud',
description: 'Availability zone in which this host is running.',
example: 'us-east-1c',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'cloud.availability_zone',
searchable: true,
type: 'string',
aggregatable: true,
},
},
},
container: {
fields: {
'container.id': {
category: 'container',
description: 'Unique container id.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'container.id',
searchable: true,
type: 'string',
aggregatable: true,
},
'container.image.name': {
category: 'container',
description: 'Name of the image the container was built on.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'container.image.name',
searchable: true,
type: 'string',
aggregatable: true,
},
'container.image.tag': {
category: 'container',
description: 'Container image tag.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'container.image.tag',
searchable: true,
type: 'string',
aggregatable: true,
},
},
},
destination: {
fields: {
'destination.address': {
category: 'destination',
description:
'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.address',
searchable: true,
type: 'string',
aggregatable: true,
},
'destination.bytes': {
category: 'destination',
description: 'Bytes sent from the destination to the source.',
example: '184',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.bytes',
searchable: true,
type: 'number',
aggregatable: true,
},
'destination.domain': {
category: 'destination',
description: 'Destination domain.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.domain',
searchable: true,
type: 'string',
aggregatable: true,
},
'destination.ip': {
aggregatable: true,
category: 'destination',
description:
'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.ip',
searchable: true,
type: 'ip',
},
'destination.port': {
aggregatable: true,
category: 'destination',
description: 'Port of the destination.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.port',
searchable: true,
type: 'long',
},
},
},
source: {
fields: {
'source.ip': {
aggregatable: true,
category: 'source',
description:
'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'source.ip',
searchable: true,
type: 'ip',
},
'source.port': {
aggregatable: true,
category: 'source',
description: 'Port of the source.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'source.port',
searchable: true,
type: 'long',
},
},
},
event: {
fields: {
'event.end': {
aggregatable: true,
category: 'event',
description:
'event.end contains the date when the event ended or when the activity was last observed.',
example: null,
format: '',
indexes: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
name: 'event.end',
searchable: true,
type: 'date',
},
},
},
},
isLoading: false,
indices: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
indicesExists: true,
indexPatterns: {
fields: [
{ name: '@timestamp', searchable: true, type: 'date', aggregatable: true },
{ name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true },
{ name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true },
{ name: 'agent.id', searchable: true, type: 'string', aggregatable: true },
{ name: 'agent.name', searchable: true, type: 'string', aggregatable: true },
{ name: 'auditd.data.a0', searchable: true, type: 'string', aggregatable: true },
{ name: 'auditd.data.a1', searchable: true, type: 'string', aggregatable: true },
{ name: 'auditd.data.a2', searchable: true, type: 'string', aggregatable: true },
{ name: 'client.address', searchable: true, type: 'string', aggregatable: true },
{ name: 'client.bytes', searchable: true, type: 'number', aggregatable: true },
{ name: 'client.domain', searchable: true, type: 'string', aggregatable: true },
{
name: 'client.geo.country_iso_code',
searchable: true,
type: 'string',
aggregatable: true,
},
{ name: 'cloud.account.id', searchable: true, type: 'string', aggregatable: true },
{
name: 'cloud.availability_zone',
searchable: true,
type: 'string',
aggregatable: true,
},
{ name: 'container.id', searchable: true, type: 'string', aggregatable: true },
{
name: 'container.image.name',
searchable: true,
type: 'string',
aggregatable: true,
},
{ name: 'container.image.tag', searchable: true, type: 'string', aggregatable: true },
{ name: 'destination.address', searchable: true, type: 'string', aggregatable: true },
{ name: 'destination.bytes', searchable: true, type: 'number', aggregatable: true },
{ name: 'destination.domain', searchable: true, type: 'string', aggregatable: true },
{ name: 'destination.ip', searchable: true, type: 'ip', aggregatable: true },
{ name: 'destination.port', searchable: true, type: 'long', aggregatable: true },
{ name: 'source.ip', searchable: true, type: 'ip', aggregatable: true },
{ name: 'source.port', searchable: true, type: 'long', aggregatable: true },
{ name: 'event.end', searchable: true, type: 'date', aggregatable: true },
],
title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*',
},
},
result.current[1],
]);
});
});
test('unhappy path', async () => {
await act(async () => {
mockUseApolloClient.mockImplementation(() => ({
query: () => Promise.reject(new Error('Something went wrong')),
}));
const { result, waitForNextUpdate } = renderHook<unknown, Return>(() =>
useFetchIndexPatterns(defaultIndexPattern)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual([
{
browserFields: {},
indexPatterns: {
fields: [],
title: '',
},
indices: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
indicesExists: false,
isLoading: false,
},
result.current[1],
]);
});
});
});

View file

@ -29,7 +29,7 @@ interface FetchIndexPatternReturn {
indexPatterns: IIndexPattern;
}
type Return = [FetchIndexPatternReturn, Dispatch<SetStateAction<string[]>>];
export type Return = [FetchIndexPatternReturn, Dispatch<SetStateAction<string[]>>];
export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => {
const apolloClient = useApolloClient();

View file

@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { NewRule, FetchRulesResponse, Rule } from './types';
export const ruleMock: NewRule = {
description: 'some desc',
enabled: true,
false_positives: [],
filters: [],
from: 'now-360s',
index: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
interval: '5m',
rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf',
language: 'kuery',
risk_score: 75,
name: 'Test rule',
query: "user.email: 'root@elastic.co'",
references: [],
severity: 'high',
tags: ['APM'],
to: 'now',
type: 'query',
threat: [],
};
export const savedRuleMock: Rule = {
created_at: 'mm/dd/yyyyTHH:MM:sssz',
created_by: 'mockUser',
description: 'some desc',
enabled: true,
false_positives: [],
filters: [],
from: 'now-360s',
id: '12345678987654321',
index: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
interval: '5m',
immutable: false,
rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf',
language: 'kuery',
risk_score: 75,
name: 'Test rule',
max_signals: 100,
query: "user.email: 'root@elastic.co'",
references: [],
severity: 'high',
tags: ['APM'],
to: 'now',
type: 'query',
threat: [],
updated_at: 'mm/dd/yyyyTHH:MM:sssz',
updated_by: 'mockUser',
};
export const rulesMock: FetchRulesResponse = {
page: 1,
perPage: 2,
total: 2,
data: [
{
created_at: '2020-02-14T19:49:28.178Z',
updated_at: '2020-02-14T19:49:28.320Z',
created_by: 'elastic',
description:
'Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.',
enabled: false,
false_positives: [],
from: 'now-660s',
id: '80c59768-8e1f-400e-908e-7b25c4ce29c3',
immutable: true,
index: ['endgame-*'],
interval: '10m',
rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e',
language: 'kuery',
output_index: '.siem-signals-default',
max_signals: 100,
risk_score: 73,
name: 'Credential Dumping - Detected - Elastic Endpoint',
query:
'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection',
filters: [],
references: [],
severity: 'high',
updated_by: 'elastic',
tags: ['Elastic', 'Endpoint'],
to: 'now',
type: 'query',
threat: [],
version: 1,
},
{
created_at: '2020-02-14T19:49:28.189Z',
updated_at: '2020-02-14T19:49:28.326Z',
created_by: 'elastic',
description:
'Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.',
enabled: false,
false_positives: [],
from: 'now-660s',
id: '2e846086-bd64-4dbc-9c56-42b46b5b2c8c',
immutable: true,
index: ['endgame-*'],
interval: '10m',
rule_id: '77a3c3df-8ec4-4da4-b758-878f551dee69',
language: 'kuery',
output_index: '.siem-signals-default',
max_signals: 100,
risk_score: 47,
name: 'Adversary Behavior - Detected - Elastic Endpoint',
query: 'event.kind:alert and event.module:endgame and event.action:rules_engine_event',
filters: [],
references: [],
severity: 'medium',
updated_by: 'elastic',
tags: ['Elastic', 'Endpoint'],
to: 'now',
type: 'query',
threat: [],
version: 1,
},
],
};

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { usePersistRule, ReturnPersistRule } from './persist_rule';
import { ruleMock } from './mock';
jest.mock('./api');
describe('usePersistRule', () => {
test('init', async () => {
const { result } = renderHook<unknown, ReturnPersistRule>(() => usePersistRule());
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
});
test('saving rule with isLoading === true', async () => {
await act(async () => {
const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnPersistRule>(() =>
usePersistRule()
);
await waitForNextUpdate();
result.current[1](ruleMock);
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
});
});
test('saved rule with isSaved === true', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<void, ReturnPersistRule>(() =>
usePersistRule()
);
await waitForNextUpdate();
result.current[1](ruleMock);
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});
});
});

View file

@ -18,9 +18,9 @@ interface PersistRuleReturn {
isSaved: boolean;
}
type Return = [PersistRuleReturn, Dispatch<NewRule | null>];
export type ReturnPersistRule = [PersistRuleReturn, Dispatch<NewRule | null>];
export const usePersistRule = (): Return => {
export const usePersistRule = (): ReturnPersistRule => {
const [rule, setRule] = useState<NewRule | null>(null);
const [isSaved, setIsSaved] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@ -35,7 +35,6 @@ export const usePersistRule = (): Return => {
try {
setIsLoading(true);
await persistRule({ rule, signal: abortCtrl.signal });
if (isSubscribed) {
setIsSaved(true);
}

View file

@ -64,7 +64,6 @@ export const RuleSchema = t.intersection([
language: t.string,
name: t.string,
max_signals: t.number,
meta: MetaRule,
query: t.string,
references: t.array(t.string),
risk_score: t.number,
@ -80,6 +79,7 @@ export const RuleSchema = t.intersection([
t.partial({
last_failure_at: t.string,
last_failure_message: t.string,
meta: MetaRule,
output_index: t.string,
saved_id: t.string,
status: t.string,
@ -197,3 +197,12 @@ export interface RuleInfoStatus {
last_failure_message: string | null;
last_success_message: string | null;
}
export type RuleStatusResponse = Record<string, RuleStatus>;
export interface PrePackagedRulesStatusResponse {
rules_custom_installed: number;
rules_installed: number;
rules_not_installed: number;
rules_not_updated: number;
}

View file

@ -0,0 +1,267 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { ReturnPrePackagedRules, usePrePackagedRules } from './use_pre_packaged_rules';
import * as api from './api';
jest.mock('./api');
describe('usePersistRule', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: null,
hasIndexWrite: null,
hasManageApiKey: null,
isAuthenticated: null,
hasEncryptionKey: null,
isSignalIndexExists: null,
})
);
await waitForNextUpdate();
expect(result.current).toEqual({
createPrePackagedRules: null,
loading: true,
loadingCreatePrePackagedRules: false,
refetchPrePackagedRulesStatus: null,
rulesCustomInstalled: null,
rulesInstalled: null,
rulesNotInstalled: null,
rulesNotUpdated: null,
});
});
});
test('fetch getPrePackagedRulesStatus', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: null,
hasIndexWrite: null,
hasManageApiKey: null,
isAuthenticated: null,
hasEncryptionKey: null,
isSignalIndexExists: null,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
createPrePackagedRules: result.current.createPrePackagedRules,
loading: false,
loadingCreatePrePackagedRules: false,
refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus,
rulesCustomInstalled: 33,
rulesInstalled: 12,
rulesNotInstalled: 0,
rulesNotUpdated: 0,
});
});
});
test('happy path to createPrePackagedRules', async () => {
const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: true,
hasIndexWrite: true,
hasManageApiKey: true,
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(true);
expect(spyOnCreatePrepackagedRules).toHaveBeenCalled();
expect(result.current).toEqual({
createPrePackagedRules: result.current.createPrePackagedRules,
loading: false,
loadingCreatePrePackagedRules: false,
refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus,
rulesCustomInstalled: 33,
rulesInstalled: 12,
rulesNotInstalled: 0,
rulesNotUpdated: 0,
});
});
});
test('unhappy path to createPrePackagedRules', async () => {
const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules');
spyOnCreatePrepackagedRules.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: true,
hasIndexWrite: true,
hasManageApiKey: true,
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
expect(spyOnCreatePrepackagedRules).toHaveBeenCalled();
});
});
test('can NOT createPrePackagedRules because canUserCrud === false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: false,
hasIndexWrite: true,
hasManageApiKey: true,
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
});
});
test('can NOT createPrePackagedRules because hasIndexWrite === false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: true,
hasIndexWrite: false,
hasManageApiKey: true,
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
});
});
test('can NOT createPrePackagedRules because hasManageApiKey === false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: true,
hasIndexWrite: true,
hasManageApiKey: false,
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
});
});
test('can NOT createPrePackagedRules because isAuthenticated === false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: true,
hasIndexWrite: true,
hasManageApiKey: true,
isAuthenticated: false,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
});
});
test('can NOT createPrePackagedRules because hasEncryptionKey === false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: true,
hasIndexWrite: true,
hasManageApiKey: true,
isAuthenticated: true,
hasEncryptionKey: false,
isSignalIndexExists: true,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
});
});
test('can NOT createPrePackagedRules because isSignalIndexExists === false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRules>(() =>
usePrePackagedRules({
canUserCRUD: true,
hasIndexWrite: true,
hasManageApiKey: true,
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: false,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
});
});
});

View file

@ -13,7 +13,7 @@ import * as i18n from './translations';
type Func = () => void;
export type CreatePreBuiltRules = () => Promise<boolean>;
interface Return {
export interface ReturnPrePackagedRules {
createPrePackagedRules: null | CreatePreBuiltRules;
loading: boolean;
loadingCreatePrePackagedRules: boolean;
@ -50,10 +50,10 @@ export const usePrePackagedRules = ({
isAuthenticated,
hasEncryptionKey,
isSignalIndexExists,
}: UsePrePackagedRuleProps): Return => {
}: UsePrePackagedRuleProps): ReturnPrePackagedRules => {
const [rulesStatus, setRuleStatus] = useState<
Pick<
Return,
ReturnPrePackagedRules,
| 'createPrePackagedRules'
| 'refetchPrePackagedRulesStatus'
| 'rulesCustomInstalled'
@ -167,6 +167,8 @@ export const usePrePackagedRules = ({
}, 300);
timeoutId = reFetch();
}
} else {
resolve(false);
}
} catch (error) {
if (isSubscribed) {

View file

@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useRule, ReturnRule } from './use_rule';
import * as api from './api';
jest.mock('./api');
describe('useRule', () => {
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, ReturnRule>(() =>
useRule('myOwnRuleID')
);
await waitForNextUpdate();
expect(result.current).toEqual([true, null]);
});
});
test('fetch rule', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, ReturnRule>(() =>
useRule('myOwnRuleID')
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual([
false,
{
created_at: 'mm/dd/yyyyTHH:MM:sssz',
created_by: 'mockUser',
description: 'some desc',
enabled: true,
false_positives: [],
filters: [],
from: 'now-360s',
id: '12345678987654321',
immutable: false,
index: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
interval: '5m',
language: 'kuery',
name: 'Test rule',
max_signals: 100,
query: "user.email: 'root@elastic.co'",
references: [],
risk_score: 75,
rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf',
severity: 'high',
tags: ['APM'],
threat: [],
to: 'now',
type: 'query',
updated_at: 'mm/dd/yyyyTHH:MM:sssz',
updated_by: 'mockUser',
},
]);
});
});
test('fetch a new rule', async () => {
const spyOnfetchRuleById = jest.spyOn(api, 'fetchRuleById');
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, ReturnRule>(id => useRule(id), {
initialProps: 'myOwnRuleID',
});
await waitForNextUpdate();
await waitForNextUpdate();
rerender('newRuleId');
await waitForNextUpdate();
expect(spyOnfetchRuleById).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -12,7 +12,7 @@ import { fetchRuleById } from './api';
import * as i18n from './translations';
import { Rule } from './types';
type Return = [boolean, Rule | null];
export type ReturnRule = [boolean, Rule | null];
/**
* Hook for using to get a Rule from the Detection Engine API
@ -20,7 +20,7 @@ type Return = [boolean, Rule | null];
* @param id desired Rule ID's (not rule_id)
*
*/
export const useRule = (id: string | undefined): Return => {
export const useRule = (id: string | undefined): ReturnRule => {
const [rule, setRule] = useState<Rule | null>(null);
const [loading, setLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();
@ -36,7 +36,6 @@ export const useRule = (id: string | undefined): Return => {
id: idToFetch,
signal: abortCtrl.signal,
});
if (isSubscribed) {
setRule(ruleResponse);
}

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useRuleStatus, ReturnRuleStatus } from './use_rule_status';
import * as api from './api';
jest.mock('./api');
describe('useRuleStatus', () => {
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() =>
useRuleStatus('myOwnRuleID')
);
await waitForNextUpdate();
expect(result.current).toEqual([true, null, null]);
});
});
test('fetch rule status', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() =>
useRuleStatus('myOwnRuleID')
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual([
false,
{
current_status: {
alert_id: 'alertId',
last_failure_at: null,
last_failure_message: null,
last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
last_success_message: 'it is a success',
status: 'succeeded',
status_date: 'mm/dd/yyyyTHH:MM:sssz',
},
failures: [],
},
result.current[2],
]);
});
});
test('re-fetch rule status', async () => {
const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() =>
useRuleStatus('myOwnRuleID')
);
await waitForNextUpdate();
await waitForNextUpdate();
if (result.current[2]) {
result.current[2]('myOwnRuleID');
}
await waitForNextUpdate();
expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -13,7 +13,7 @@ import * as i18n from './translations';
import { RuleStatus } from './types';
type Func = (ruleId: string) => void;
type Return = [boolean, RuleStatus | null, Func | null];
export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null];
/**
* Hook for using to get a Rule from the Detection Engine API
@ -21,7 +21,7 @@ type Return = [boolean, RuleStatus | null, Func | null];
* @param id desired Rule ID's (not rule_id)
*
*/
export const useRuleStatus = (id: string | undefined | null): Return => {
export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus => {
const [ruleStatus, setRuleStatus] = useState<RuleStatus | null>(null);
const fetchRuleStatus = useRef<Func | null>(null);
const [loading, setLoading] = useState(true);
@ -34,6 +34,7 @@ export const useRuleStatus = (id: string | undefined | null): Return => {
const fetchData = async (idToFetch: string) => {
try {
setLoading(true);
const ruleStatusResponse = await getRuleStatusById({
id: idToFetch,
signal: abortCtrl.signal,

View file

@ -0,0 +1,216 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useRules, ReturnRules } from './use_rules';
import * as api from './api';
import { PaginationOptions, FilterOptions } from '.';
jest.mock('./api');
describe('useRules', () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
[PaginationOptions, FilterOptions],
ReturnRules
>(props =>
useRules(
{
page: 1,
perPage: 10,
total: 100,
},
{
filter: '',
sortField: 'created_at',
sortOrder: 'desc',
}
)
);
await waitForNextUpdate();
expect(result.current).toEqual([
true,
{
data: [],
page: 1,
perPage: 20,
total: 0,
},
null,
]);
});
});
test('fetch rules', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
[PaginationOptions, FilterOptions],
ReturnRules
>(() =>
useRules(
{
page: 1,
perPage: 10,
total: 100,
},
{
filter: '',
sortField: 'created_at',
sortOrder: 'desc',
}
)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual([
false,
{
data: [
{
created_at: '2020-02-14T19:49:28.178Z',
created_by: 'elastic',
description:
'Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.',
enabled: false,
false_positives: [],
filters: [],
from: 'now-660s',
id: '80c59768-8e1f-400e-908e-7b25c4ce29c3',
immutable: true,
index: ['endgame-*'],
interval: '10m',
language: 'kuery',
max_signals: 100,
name: 'Credential Dumping - Detected - Elastic Endpoint',
output_index: '.siem-signals-default',
query:
'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection',
references: [],
risk_score: 73,
rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e',
severity: 'high',
tags: ['Elastic', 'Endpoint'],
threat: [],
to: 'now',
type: 'query',
updated_at: '2020-02-14T19:49:28.320Z',
updated_by: 'elastic',
version: 1,
},
{
created_at: '2020-02-14T19:49:28.189Z',
created_by: 'elastic',
description:
'Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.',
enabled: false,
false_positives: [],
filters: [],
from: 'now-660s',
id: '2e846086-bd64-4dbc-9c56-42b46b5b2c8c',
immutable: true,
index: ['endgame-*'],
interval: '10m',
language: 'kuery',
max_signals: 100,
name: 'Adversary Behavior - Detected - Elastic Endpoint',
output_index: '.siem-signals-default',
query:
'event.kind:alert and event.module:endgame and event.action:rules_engine_event',
references: [],
risk_score: 47,
rule_id: '77a3c3df-8ec4-4da4-b758-878f551dee69',
severity: 'medium',
tags: ['Elastic', 'Endpoint'],
threat: [],
to: 'now',
type: 'query',
updated_at: '2020-02-14T19:49:28.326Z',
updated_by: 'elastic',
version: 1,
},
],
page: 1,
perPage: 2,
total: 2,
},
result.current[2],
]);
});
});
test('re-fetch rules', async () => {
const spyOnfetchRules = jest.spyOn(api, 'fetchRules');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
[PaginationOptions, FilterOptions],
ReturnRules
>(id =>
useRules(
{
page: 1,
perPage: 10,
total: 100,
},
{
filter: '',
sortField: 'created_at',
sortOrder: 'desc',
}
)
);
await waitForNextUpdate();
await waitForNextUpdate();
if (result.current[2]) {
result.current[2]();
}
await waitForNextUpdate();
expect(spyOnfetchRules).toHaveBeenCalledTimes(2);
});
});
test('fetch rules if props changes', async () => {
const spyOnfetchRules = jest.spyOn(api, 'fetchRules');
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<
[PaginationOptions, FilterOptions],
ReturnRules
>(args => useRules(args[0], args[1]), {
initialProps: [
{
page: 1,
perPage: 10,
total: 100,
},
{
filter: '',
sortField: 'created_at',
sortOrder: 'desc',
},
],
});
await waitForNextUpdate();
await waitForNextUpdate();
rerender([
{
page: 1,
perPage: 10,
total: 100,
},
{
filter: 'hello world',
sortField: 'created_at',
sortOrder: 'desc',
},
]);
await waitForNextUpdate();
expect(spyOnfetchRules).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -13,7 +13,7 @@ import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import * as i18n from './translations';
type Func = () => void;
type Return = [boolean, FetchRulesResponse, Func | null];
export type ReturnRules = [boolean, FetchRulesResponse, Func | null];
/**
* Hook for using the list of Rules from the Detection Engine API
@ -21,7 +21,10 @@ type Return = [boolean, FetchRulesResponse, Func | null];
* @param pagination desired pagination options (e.g. page/perPage)
* @param filterOptions desired filters (e.g. filter/sortField/sortOrder)
*/
export const useRules = (pagination: PaginationOptions, filterOptions: FilterOptions): Return => {
export const useRules = (
pagination: PaginationOptions,
filterOptions: FilterOptions
): ReturnRules => {
const [rules, setRules] = useState<FetchRulesResponse>({
page: 1,
perPage: 20,

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useTags, ReturnTags } from './use_tags';
jest.mock('./api');
describe('useTags', () => {
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnTags>(() => useTags());
await waitForNextUpdate();
expect(result.current).toEqual([true, []]);
});
});
test('fetch tags', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnTags>(() => useTags());
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual([false, ['elastic', 'love', 'quality', 'code']]);
});
});
});

View file

@ -10,13 +10,13 @@ import { fetchTags } from './api';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import * as i18n from './translations';
type Return = [boolean, string[]];
export type ReturnTags = [boolean, string[]];
/**
* Hook for using the list of Tags from the Detection Engine API
*
*/
export const useTags = (): Return => {
export const useTags = (): ReturnTags => {
const [tags, setTags] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();

View file

@ -12,6 +12,7 @@ interface HookWrapperProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hookProps?: any;
}
export const HookWrapper = ({ hook, hookProps }: HookWrapperProps) => {
const myHook = hook ? (hookProps ? hook(hookProps) : hook()) : null;
return <div>{JSON.stringify(myHook)}</div>;