Skip to content

Commit

Permalink
[SIEM] [CASES] API with io-ts validation (#59265)
Browse files Browse the repository at this point in the history
* refactor to use io-ts, to be able to have ressource with sub, add total comments via comment_ids, be able to delete multiple cases/comments

* fix test

* adapt UI to refactor of the API

* put it back the way it was

* clean up to get cases

* review I

* review II - bring back url  parameter

* fix merge

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
XavierM and elasticmachine authored Mar 6, 2020
1 parent f511afa commit c29ef14
Show file tree
Hide file tree
Showing 86 changed files with 1,889 additions and 1,248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { isAnError, isToasterError, errorToToaster } from './error_to_toaster';
import { ToasterErrors } from './throw_if_not_ok';
import { ToasterErrors } from '../../../hooks/api/throw_if_not_ok';

describe('error_to_toaster', () => {
let dispatchToaster = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { isError } from 'lodash/fp';
import uuid from 'uuid';
import { ActionToaster, AppToast } from '../../toasters';
import { ToasterErrorsType, ToasterErrors } from './throw_if_not_ok';
import { ToasterErrorsType, ToasterErrors } from '../../../hooks/api/throw_if_not_ok';

export type ErrorToToasterArgs = Partial<AppToast> & {
error: unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
StartDatafeedResponse,
StopDatafeedResponse,
} from './types';
import { throwIfErrorAttached, throwIfErrorAttachedToSetup } from '../ml/api/throw_if_not_ok';
import { throwIfErrorAttached, throwIfErrorAttachedToSetup } from '../../hooks/api/throw_if_not_ok';
import { throwIfNotOk } from '../../hooks/api/api';
import { KibanaServices } from '../../lib/kibana';

Expand Down
102 changes: 60 additions & 42 deletions x-pack/legacy/plugins/siem/public/containers/case/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,46 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { KibanaServices } from '../../lib/kibana';
import {
AllCases,
Case,
CaseSnake,
Comment,
CommentSnake,
FetchCasesProps,
NewCase,
NewComment,
SortFieldCase,
} from './types';
CaseResponse,
CasesResponse,
CaseRequest,
CommentRequest,
CommentResponse,
} from '../../../../../../plugins/case/common/api';
import { KibanaServices } from '../../lib/kibana';
import { AllCases, Case, Comment, FetchCasesProps, SortFieldCase } from './types';
import { throwIfNotOk } from '../../hooks/api/api';
import { CASES_URL } from './constants';
import { convertToCamelCase, convertAllCasesToCamel } from './utils';
import {
convertToCamelCase,
convertAllCasesToCamel,
decodeCaseResponse,
decodeCasesResponse,
decodeCommentResponse,
} from './utils';

const CaseSavedObjectType = 'cases';

export const getCase = async (caseId: string, includeComments: boolean = true): Promise<Case> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, {
const response = await KibanaServices.get().http.fetch<CaseResponse>(`${CASES_URL}/${caseId}`, {
method: 'GET',
asResponse: true,
query: {
includeComments,
},
});
await throwIfNotOk(response.response);
return convertToCamelCase<CaseSnake, Case>(response.body!);
return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response.body));
};

export const getTags = async (): Promise<string[]> => {
const response = await KibanaServices.get().http.fetch<string[]>(`${CASES_URL}/tags`, {
method: 'GET',
asResponse: true,
});
await throwIfNotOk(response.response);
return response.body ?? [];
};

export const getCases = async ({
Expand All @@ -45,70 +59,74 @@ export const getCases = async ({
sortOrder: 'desc',
},
}: FetchCasesProps): Promise<AllCases> => {
const stateFilter = `case-workflow.attributes.state: ${filterOptions.state}`;
const stateFilter = `${CaseSavedObjectType}.attributes.state: ${filterOptions.state}`;
const tags = [
...(filterOptions.tags?.reduce((acc, t) => [...acc, `case-workflow.attributes.tags: ${t}`], [
stateFilter,
]) ?? [stateFilter]),
...(filterOptions.tags?.reduce(
(acc, t) => [...acc, `${CaseSavedObjectType}.attributes.tags: ${t}`],
[stateFilter]
) ?? [stateFilter]),
];
const query = {
...queryParams,
filter: tags.join(' AND '),
search: filterOptions.search,
...(tags.length > 0 ? { filter: tags.join(' AND ') } : {}),
...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}),
};
const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, {
const response = await KibanaServices.get().http.fetch<CasesResponse>(`${CASES_URL}/_find`, {
method: 'GET',
query,
asResponse: true,
});
await throwIfNotOk(response.response);
return convertAllCasesToCamel(response.body!);
return convertAllCasesToCamel(decodeCasesResponse(response.body));
};

export const createCase = async (newCase: NewCase): Promise<Case> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, {
export const postCase = async (newCase: CaseRequest): Promise<Case> => {
const response = await KibanaServices.get().http.fetch<CaseResponse>(`${CASES_URL}`, {
method: 'POST',
asResponse: true,
body: JSON.stringify(newCase),
});
await throwIfNotOk(response.response);
return convertToCamelCase<CaseSnake, Case>(response.body!);
return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response.body));
};

export const updateCaseProperty = async (
export const patchCase = async (
caseId: string,
updatedCase: Partial<Case>,
updatedCase: Partial<CaseRequest>,
version: string
): Promise<Partial<Case>> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, {
): Promise<Case> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, {
method: 'PATCH',
asResponse: true,
body: JSON.stringify({ case: updatedCase, version }),
body: JSON.stringify({ ...updatedCase, id: caseId, version }),
});
await throwIfNotOk(response.response);
return convertToCamelCase<Partial<CaseSnake>, Partial<Case>>(response.body!);
return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response.body));
};

export const createComment = async (newComment: NewComment, caseId: string): Promise<Comment> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}/comment`, {
method: 'POST',
asResponse: true,
body: JSON.stringify(newComment),
});
export const postComment = async (newComment: CommentRequest, caseId: string): Promise<Comment> => {
const response = await KibanaServices.get().http.fetch<CommentResponse>(
`${CASES_URL}/${caseId}/comments`,
{
method: 'POST',
asResponse: true,
body: JSON.stringify(newComment),
}
);
await throwIfNotOk(response.response);
return convertToCamelCase<CommentSnake, Comment>(response.body!);
return convertToCamelCase<CommentResponse, Comment>(decodeCommentResponse(response.body));
};

export const updateComment = async (
export const patchComment = async (
commentId: string,
commentUpdate: string,
version: string
): Promise<Partial<Comment>> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}/comment/${commentId}`, {
const response = await KibanaServices.get().http.fetch<CommentResponse>(`${CASES_URL}/comments`, {
method: 'PATCH',
asResponse: true,
body: JSON.stringify({ comment: commentUpdate, version }),
body: JSON.stringify({ comment: commentUpdate, id: commentId, version }),
});
await throwIfNotOk(response.response);
return convertToCamelCase<Partial<CommentSnake>, Partial<Comment>>(response.body!);
return convertToCamelCase<CommentResponse, Comment>(decodeCommentResponse(response.body));
};
53 changes: 3 additions & 50 deletions x-pack/legacy/plugins/siem/public/containers/case/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/

interface FormData {
isNew?: boolean;
}

export interface NewCase extends FormData {
description: string;
tags: string[];
title: string;
}

export interface NewComment extends FormData {
comment: string;
}

export interface CommentSnake {
comment_id: string;
created_at: string;
created_by: ElasticUserSnake;
comment: string;
updated_at: string;
version: string;
}

export interface Comment {
commentId: string;
id: string;
createdAt: string;
createdBy: ElasticUser;
comment: string;
updatedAt: string;
version: string;
}

export interface CaseSnake {
case_id: string;
comments: CommentSnake[];
created_at: string;
created_by: ElasticUserSnake;
description: string;
state: string;
tags: string[];
title: string;
updated_at: string;
version: string;
}

export interface Case {
caseId: string;
id: string;
comments: Comment[];
createdAt: string;
createdBy: ElasticUser;
Expand All @@ -75,29 +39,18 @@ export interface FilterOptions {
tags: string[];
}

export interface AllCasesSnake {
cases: CaseSnake[];
page: number;
per_page: number;
total: number;
}

export interface AllCases {
cases: Case[];
page: number;
perPage: number;
total: number;
}

export enum SortFieldCase {
createdAt = 'createdAt',
updatedAt = 'updatedAt',
}

export interface ElasticUserSnake {
readonly username: string;
readonly full_name?: string | null;
}

export interface ElasticUser {
readonly username: string;
readonly fullName?: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
}
};
const initialData: Case = {
caseId: '',
id: '',
createdAt: '',
comments: [],
createdBy: {
Expand Down Expand Up @@ -83,7 +83,11 @@ export const useGetCase = (caseId: string): [CaseState] => {
}
} catch (error) {
if (!didCancel) {
errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster });
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
dispatch({ type: FETCH_FAILURE });
}
}
Expand Down
Loading

0 comments on commit c29ef14

Please sign in to comment.