Skip to content

Commit

Permalink
[Case] Detection rules for case (#88726)
Browse files Browse the repository at this point in the history
* Adding type field to client

* Removing context and adding association type

* Handle alerts from multiple indices

* Adding flow for adding a sub case

* Making progress on creating alerts from rules

* Refactored add comment to handle case and sub case

* Starting sub case API and refactoring of case client

* Fleshing out find cases

* Finished the find cases api

* Filtering comments by association type

* Fixing tests and types

* Updating snapshots

* Cleaning up comment references

* Working unit tests

* Fixing integration tests and got ES to work

* Unit tests and api integration test working

* Refactoring find and get_status

* Starting patch, and update

* script for sub cases

* Removing converted_by and fixing type errors

* Adding docs for script

* Removing converted_by and fixing integration test

* Adding sub case id to comment routes

* Removing stringify comparison

* Adding delete api and tests

* Updating license

* missed license files

* Integration tests passing

* Adding more tests for sub cases

* Find int tests, scoped client, patch sub user actions

* fixing types and call cluster

* fixing get sub case param issue

* Adding user actions for sub cases

* Preventing alerts on collections and refactoring user

* Allowing type to be updated for ind cases

* Refactoring and writing tests

* Fixing sub case status filtering

* Adding more tests not allowing gen alerts patch

* Working unit tests

* Push to connector gets all sub case comments

* Writing more tests and cleaning up

* Updating push functionality for generated alerts and sub cases

* Adding comment about updating collection sync

* Refactoring update alert status for sub cases and removing request and cleaning up

* Addressing alert service feedback

* Fixing sub case sync bug and cleaning up comment types

* Addressing more feedback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
jonathan-buttner and kibanamachine authored Feb 12, 2021
1 parent 2fcf2a9 commit f8b8d5b
Show file tree
Hide file tree
Showing 123 changed files with 8,319 additions and 2,181 deletions.
54 changes: 42 additions & 12 deletions x-pack/plugins/case/common/api/cases/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,21 @@ import * as rt from 'io-ts';
import { NumberFromString } from '../saved_object';
import { UserRT } from '../user';
import { CommentResponseRt } from './comment';
import { CasesStatusResponseRt } from './status';
import { CasesStatusResponseRt, CaseStatusRt } from './status';
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
import { SubCaseResponseRt } from './sub_case';

export enum CaseStatuses {
open = 'open',
'in-progress' = 'in-progress',
closed = 'closed',
export enum CaseType {
collection = 'collection',
individual = 'individual',
}

const CaseStatusRt = rt.union([
rt.literal(CaseStatuses.open),
rt.literal(CaseStatuses['in-progress']),
rt.literal(CaseStatuses.closed),
]);
/**
* Exposing the field used to define the case type so that it can be used for filtering in saved object find queries.
*/
export const caseTypeField = 'type';

export const caseStatuses = Object.values(CaseStatuses);
const CaseTypeRt = rt.union([rt.literal(CaseType.collection), rt.literal(CaseType.individual)]);

const SettingsRt = rt.type({
syncAlerts: rt.boolean,
Expand All @@ -36,6 +35,7 @@ const CaseBasicRt = rt.type({
status: CaseStatusRt,
tags: rt.array(rt.string),
title: rt.string,
[caseTypeField]: CaseTypeRt,
connector: CaseConnectorRt,
settings: SettingsRt,
});
Expand Down Expand Up @@ -72,15 +72,35 @@ export const CaseAttributesRt = rt.intersection([
}),
]);

export const CasePostRequestRt = rt.type({
const CasePostRequestNoTypeRt = rt.type({
description: rt.string,
tags: rt.array(rt.string),
title: rt.string,
connector: CaseConnectorRt,
settings: SettingsRt,
});

/**
* This type is used for validating a create case request. It requires that the type field be defined.
*/
export const CaseClientPostRequestRt = rt.type({
...CasePostRequestNoTypeRt.props,
[caseTypeField]: CaseTypeRt,
});

/**
* This type is not used for validation when decoding a request because intersection does not have props defined which
* required for the excess function. Instead we use this as the type used by the UI. This allows the type field to be
* optional and the server will handle setting it to a default value before validating that the request
* has all the necessary fields. CaseClientPostRequestRt is used for validation.
*/
export const CasePostRequestRt = rt.intersection([
rt.partial({ type: CaseTypeRt }),
CasePostRequestNoTypeRt,
]);

export const CasesFindRequestRt = rt.partial({
type: CaseTypeRt,
tags: rt.union([rt.array(rt.string), rt.string]),
status: CaseStatusRt,
reporters: rt.union([rt.array(rt.string), rt.string]),
Expand All @@ -99,9 +119,11 @@ export const CaseResponseRt = rt.intersection([
rt.type({
id: rt.string,
totalComment: rt.number,
totalAlerts: rt.number,
version: rt.string,
}),
rt.partial({
subCases: rt.array(SubCaseResponseRt),
comments: rt.array(CommentResponseRt),
}),
]);
Expand Down Expand Up @@ -150,13 +172,21 @@ export const ExternalServiceResponseRt = rt.intersection([
]);

export type CaseAttributes = rt.TypeOf<typeof CaseAttributesRt>;
/**
* This field differs from the CasePostRequest in that the post request's type field can be optional. This type requires
* that the type field be defined. The CasePostRequest should be used in most places (the UI etc). This type is really
* only necessary for validation.
*/
export type CaseClientPostRequest = rt.TypeOf<typeof CaseClientPostRequestRt>;
export type CasePostRequest = rt.TypeOf<typeof CasePostRequestRt>;
export type CaseResponse = rt.TypeOf<typeof CaseResponseRt>;
export type CasesResponse = rt.TypeOf<typeof CasesResponseRt>;
export type CasesFindRequest = rt.TypeOf<typeof CasesFindRequestRt>;
export type CasesFindResponse = rt.TypeOf<typeof CasesFindResponseRt>;
export type CasePatchRequest = rt.TypeOf<typeof CasePatchRequestRt>;
export type CasesPatchRequest = rt.TypeOf<typeof CasesPatchRequestRt>;
export type CaseFullExternalService = rt.TypeOf<typeof CaseFullExternalServiceRt>;
export type CaseSettings = rt.TypeOf<typeof SettingsRt>;
export type ExternalServiceResponse = rt.TypeOf<typeof ExternalServiceResponseRt>;

export type ESCaseAttributes = Omit<CaseAttributes, 'connector'> & { connector: ESCaseConnector };
Expand Down
51 changes: 35 additions & 16 deletions x-pack/plugins/case/common/api/cases/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,22 @@ import * as rt from 'io-ts';

import { UserRT } from '../user';

/**
* this is used to differentiate between an alert attached to a top-level case and a group of alerts that should only
* be attached to a sub case. The reason we need this is because an alert group comment will have references to both a case and
* sub case when it is created. For us to be able to filter out alert groups in a top-level case we need a field to
* use as a filter.
*/
export enum AssociationType {
case = 'case',
subCase = 'sub_case',
}

export const CommentAttributesBasicRt = rt.type({
associationType: rt.union([
rt.literal(AssociationType.case),
rt.literal(AssociationType.subCase),
]),
created_at: rt.string,
created_by: UserRT,
pushed_at: rt.union([rt.string, rt.null]),
Expand All @@ -18,24 +33,33 @@ export const CommentAttributesBasicRt = rt.type({
updated_by: rt.union([UserRT, rt.null]),
});

export enum CommentType {
user = 'user',
alert = 'alert',
generatedAlert = 'generated_alert',
}

export const ContextTypeUserRt = rt.type({
comment: rt.string,
type: rt.literal('user'),
type: rt.literal(CommentType.user),
});

export const ContextTypeAlertRt = rt.type({
type: rt.literal('alert'),
alertId: rt.string,
/**
* This defines the structure of how alerts (generated or user attached) are stored in saved objects documents. It also
* represents of an alert after it has been transformed. A generated alert will be transformed by the connector so that
* it matches this structure. User attached alerts do not need to be transformed.
*/
export const AlertCommentRequestRt = rt.type({
type: rt.union([rt.literal(CommentType.generatedAlert), rt.literal(CommentType.alert)]),
alertId: rt.union([rt.array(rt.string), rt.string]),
index: rt.string,
});

const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]);
const AttributesTypeAlertsRt = rt.intersection([ContextTypeAlertRt, CommentAttributesBasicRt]);
const AttributesTypeAlertsRt = rt.intersection([AlertCommentRequestRt, CommentAttributesBasicRt]);
const CommentAttributesRt = rt.union([AttributesTypeUserRt, AttributesTypeAlertsRt]);

const ContextBasicRt = rt.union([ContextTypeUserRt, ContextTypeAlertRt]);

export const CommentRequestRt = ContextBasicRt;
export const CommentRequestRt = rt.union([ContextTypeUserRt, AlertCommentRequestRt]);

export const CommentResponseRt = rt.intersection([
CommentAttributesRt,
Expand All @@ -60,7 +84,7 @@ export const CommentPatchRequestRt = rt.intersection([
* Partial updates are not allowed.
* We want to prevent the user for changing the type without removing invalid fields.
*/
ContextBasicRt,
CommentRequestRt,
rt.type({ id: rt.string, version: rt.string }),
]);

Expand All @@ -71,7 +95,7 @@ export const CommentPatchRequestRt = rt.intersection([
* We ensure that partial updates of CommentContext is not going to happen inside the patch comment route.
*/
export const CommentPatchAttributesRt = rt.intersection([
rt.union([rt.partial(CommentAttributesBasicRt.props), rt.partial(ContextTypeAlertRt.props)]),
rt.union([rt.partial(CommentAttributesBasicRt.props), rt.partial(AlertCommentRequestRt.props)]),
rt.partial(CommentAttributesBasicRt.props),
]);

Expand All @@ -82,11 +106,6 @@ export const CommentsResponseRt = rt.type({
total: rt.number,
});

export enum CommentType {
user = 'user',
alert = 'alert',
}

export const AllCommentsResponseRt = rt.array(CommentResponseRt);

export type CommentAttributes = rt.TypeOf<typeof CommentAttributesRt>;
Expand All @@ -98,4 +117,4 @@ export type CommentsResponse = rt.TypeOf<typeof CommentsResponseRt>;
export type CommentPatchRequest = rt.TypeOf<typeof CommentPatchRequestRt>;
export type CommentPatchAttributes = rt.TypeOf<typeof CommentPatchAttributesRt>;
export type CommentRequestUserType = rt.TypeOf<typeof ContextTypeUserRt>;
export type CommentRequestAlertType = rt.TypeOf<typeof ContextTypeAlertRt>;
export type CommentRequestAlertType = rt.TypeOf<typeof AlertCommentRequestRt>;
35 changes: 35 additions & 0 deletions x-pack/plugins/case/common/api/cases/commentable_case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 rt from 'io-ts';
import { CaseAttributesRt } from './case';
import { CommentResponseRt } from './comment';
import { SubCaseAttributesRt, SubCaseResponseRt } from './sub_case';

export const CollectionSubCaseAttributesRt = rt.intersection([
rt.partial({ subCase: SubCaseAttributesRt }),
rt.type({
case: CaseAttributesRt,
}),
]);

export const CollectWithSubCaseResponseRt = rt.intersection([
CaseAttributesRt,
rt.type({
id: rt.string,
totalComment: rt.number,
version: rt.string,
}),
rt.partial({
subCase: SubCaseResponseRt,
totalAlerts: rt.number,
comments: rt.array(CommentResponseRt),
}),
]);

export type CollectionWithSubCaseResponse = rt.TypeOf<typeof CollectWithSubCaseResponseRt>;
export type CollectionWithSubCaseAttributes = rt.TypeOf<typeof CollectionSubCaseAttributesRt>;
2 changes: 2 additions & 0 deletions x-pack/plugins/case/common/api/cases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export * from './configure';
export * from './comment';
export * from './status';
export * from './user_actions';
export * from './sub_case';
export * from './commentable_case';
14 changes: 14 additions & 0 deletions x-pack/plugins/case/common/api/cases/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@

import * as rt from 'io-ts';

export enum CaseStatuses {
open = 'open',
'in-progress' = 'in-progress',
closed = 'closed',
}

export const CaseStatusRt = rt.union([
rt.literal(CaseStatuses.open),
rt.literal(CaseStatuses['in-progress']),
rt.literal(CaseStatuses.closed),
]);

export const caseStatuses = Object.values(CaseStatuses);

export const CasesStatusResponseRt = rt.type({
count_open_cases: rt.number,
count_in_progress_cases: rt.number,
Expand Down
80 changes: 80 additions & 0 deletions x-pack/plugins/case/common/api/cases/sub_case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 rt from 'io-ts';

import { NumberFromString } from '../saved_object';
import { UserRT } from '../user';
import { CommentResponseRt } from './comment';
import { CasesStatusResponseRt } from './status';
import { CaseStatusRt } from './status';

const SubCaseBasicRt = rt.type({
status: CaseStatusRt,
});

export const SubCaseAttributesRt = rt.intersection([
SubCaseBasicRt,
rt.type({
closed_at: rt.union([rt.string, rt.null]),
closed_by: rt.union([UserRT, rt.null]),
created_at: rt.string,
created_by: rt.union([UserRT, rt.null]),
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRT, rt.null]),
}),
]);

export const SubCasesFindRequestRt = rt.partial({
status: CaseStatusRt,
defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]),
fields: rt.array(rt.string),
page: NumberFromString,
perPage: NumberFromString,
search: rt.string,
searchFields: rt.array(rt.string),
sortField: rt.string,
sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]),
});

export const SubCaseResponseRt = rt.intersection([
SubCaseAttributesRt,
rt.type({
id: rt.string,
totalComment: rt.number,
totalAlerts: rt.number,
version: rt.string,
}),
rt.partial({
comments: rt.array(CommentResponseRt),
}),
]);

export const SubCasesFindResponseRt = rt.intersection([
rt.type({
subCases: rt.array(SubCaseResponseRt),
page: rt.number,
per_page: rt.number,
total: rt.number,
}),
CasesStatusResponseRt,
]);

export const SubCasePatchRequestRt = rt.intersection([
rt.partial(SubCaseBasicRt.props),
rt.type({ id: rt.string, version: rt.string }),
]);

export const SubCasesPatchRequestRt = rt.type({ subCases: rt.array(SubCasePatchRequestRt) });
export const SubCasesResponseRt = rt.array(SubCaseResponseRt);

export type SubCaseAttributes = rt.TypeOf<typeof SubCaseAttributesRt>;
export type SubCaseResponse = rt.TypeOf<typeof SubCaseResponseRt>;
export type SubCasesResponse = rt.TypeOf<typeof SubCasesResponseRt>;
export type SubCasesFindResponse = rt.TypeOf<typeof SubCasesFindResponseRt>;
export type SubCasePatchRequest = rt.TypeOf<typeof SubCasePatchRequestRt>;
export type SubCasesPatchRequest = rt.TypeOf<typeof SubCasesPatchRequestRt>;
25 changes: 13 additions & 12 deletions x-pack/plugins/case/common/api/cases/user_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ import { UserRT } from '../user';
/* To the next developer, if you add/removed fields here
* make sure to check this file (x-pack/plugins/case/server/services/user_actions/helpers.ts) too
*/
const UserActionFieldRt = rt.array(
rt.union([
rt.literal('comment'),
rt.literal('connector'),
rt.literal('description'),
rt.literal('pushed'),
rt.literal('tags'),
rt.literal('title'),
rt.literal('status'),
rt.literal('settings'),
])
);
const UserActionFieldTypeRt = rt.union([
rt.literal('comment'),
rt.literal('connector'),
rt.literal('description'),
rt.literal('pushed'),
rt.literal('tags'),
rt.literal('title'),
rt.literal('status'),
rt.literal('settings'),
rt.literal('sub_case'),
]);
const UserActionFieldRt = rt.array(UserActionFieldTypeRt);
const UserActionRt = rt.union([
rt.literal('add'),
rt.literal('create'),
Expand Down Expand Up @@ -60,3 +60,4 @@ export type CaseUserActionsResponse = rt.TypeOf<typeof CaseUserActionsResponseRt

export type UserAction = rt.TypeOf<typeof UserActionRt>;
export type UserActionField = rt.TypeOf<typeof UserActionFieldRt>;
export type UserActionFieldType = rt.TypeOf<typeof UserActionFieldTypeRt>;
Loading

0 comments on commit f8b8d5b

Please sign in to comment.