mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Notes] Make MAX_UNASSOCIATED_NOTES an advanced Kibana setting (#194947)
## Summary Fixes: https://github.com/elastic/kibana/issues/193097 Adds a new Kibana advanced setting that allows users to limit the maximum amount of unassociated notes. The max value for that used to be hard coded before. https://github.com/user-attachments/assets/34af7f67-9109-4251-a5a3-a1af68f123fe ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
424ffbaffc
commit
925329ec84
16 changed files with 220 additions and 77 deletions
|
@ -22,6 +22,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
},
|
||||
'securitySolution:maxUnassociatedNotes': {
|
||||
type: 'integer',
|
||||
_meta: { description: 'The maximum number of allowed unassociated notes' },
|
||||
},
|
||||
'securitySolution:defaultThreatIndex': {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'Default value of the setting was changed.' },
|
||||
|
|
|
@ -183,5 +183,6 @@ export interface UsageStats {
|
|||
'aiAssistant:preferredAIAssistantType': string;
|
||||
'observability:profilingFetchTopNFunctionsFromStacktraces': boolean;
|
||||
'securitySolution:excludedDataTiersForRuleExecution': string[];
|
||||
'securitySolution:maxUnassociatedNotes': number;
|
||||
'observability:searchExcludedDataTiers': string[];
|
||||
}
|
||||
|
|
|
@ -9888,6 +9888,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"securitySolution:maxUnassociatedNotes": {
|
||||
"type": "integer",
|
||||
"_meta": {
|
||||
"description": "The maximum number of allowed unassociated notes"
|
||||
}
|
||||
},
|
||||
"securitySolution:defaultThreatIndex": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
|
@ -10050,7 +10056,7 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"securitySolution:enableVisualizationsInFlyout":{
|
||||
"securitySolution:enableVisualizationsInFlyout": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
|
|
|
@ -71,6 +71,7 @@ export const SECURITY_TAG_NAME = 'Security Solution' as const;
|
|||
export const SECURITY_TAG_DESCRIPTION = 'Security Solution auto-generated tag' as const;
|
||||
export const DEFAULT_SPACE_ID = 'default' as const;
|
||||
export const DEFAULT_RELATIVE_DATE_THRESHOLD = 24 as const;
|
||||
export const DEFAULT_MAX_UNASSOCIATED_NOTES = 1000 as const;
|
||||
|
||||
// Document path where threat indicator fields are expected. Fields are used
|
||||
// to enrich signals, and are copied to threat.enrichments.
|
||||
|
@ -200,6 +201,9 @@ export const ENABLE_ASSET_CRITICALITY_SETTING = 'securitySolution:enableAssetCri
|
|||
export const EXCLUDED_DATA_TIERS_FOR_RULE_EXECUTION =
|
||||
'securitySolution:excludedDataTiersForRuleExecution' as const;
|
||||
|
||||
/** This Kibana Advances setting allows users to define the maximum amount of unassociated notes (notes without a `timelineId`) */
|
||||
export const MAX_UNASSOCIATED_NOTES = 'securitySolution:maxUnassociatedNotes' as const;
|
||||
|
||||
/** This Kibana Advanced Setting allows users to enable/disable the Visualizations in Flyout feature */
|
||||
export const ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING =
|
||||
'securitySolution:enableVisualizationsInFlyout' as const;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 type { PersistNoteRouteResponse } from '../../../common/api/timeline';
|
||||
import { KibanaServices } from '../../common/lib/kibana';
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => {
|
||||
return {
|
||||
KibanaServices: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('Notes API client', () => {
|
||||
beforeAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
describe('create note', () => {
|
||||
it('should throw an error when a response code other than 200 is returned', async () => {
|
||||
const errorResponse: PersistNoteRouteResponse = {
|
||||
data: {
|
||||
persistNote: {
|
||||
code: 500,
|
||||
message: 'Internal server error',
|
||||
note: {
|
||||
timelineId: '1',
|
||||
noteId: '2',
|
||||
version: '3',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
(KibanaServices.get as jest.Mock).mockReturnValue({
|
||||
http: {
|
||||
patch: jest.fn().mockReturnValue(errorResponse),
|
||||
},
|
||||
});
|
||||
|
||||
expect(async () =>
|
||||
api.createNote({
|
||||
note: {
|
||||
timelineId: '1',
|
||||
},
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,7 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { BareNote, Note } from '../../../common/api/timeline';
|
||||
import type {
|
||||
BareNote,
|
||||
DeleteNoteResponse,
|
||||
GetNotesResponse,
|
||||
PersistNoteRouteResponse,
|
||||
} from '../../../common/api/timeline';
|
||||
import { KibanaServices } from '../../common/lib/kibana';
|
||||
import { NOTE_URL } from '../../../common/constants';
|
||||
|
||||
|
@ -16,16 +21,18 @@ import { NOTE_URL } from '../../../common/constants';
|
|||
*/
|
||||
export const createNote = async ({ note }: { note: BareNote }) => {
|
||||
try {
|
||||
const response = await KibanaServices.get().http.patch<{
|
||||
data: { persistNote: { code: number; message: string; note: Note } };
|
||||
}>(NOTE_URL, {
|
||||
const response = await KibanaServices.get().http.patch<PersistNoteRouteResponse>(NOTE_URL, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ note }),
|
||||
version: '2023-10-31',
|
||||
});
|
||||
return response.data.persistNote.note;
|
||||
const noteResponse = response.data.persistNote;
|
||||
if (noteResponse.code !== 200) {
|
||||
throw new Error(noteResponse.message);
|
||||
}
|
||||
return noteResponse.note;
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to stringify query: ${JSON.stringify(err)}`);
|
||||
throw new Error(('message' in err && err.message) || 'Request failed');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -44,20 +51,17 @@ export const fetchNotes = async ({
|
|||
filter: string;
|
||||
search: string;
|
||||
}) => {
|
||||
const response = await KibanaServices.get().http.get<{ totalCount: number; notes: Note[] }>(
|
||||
NOTE_URL,
|
||||
{
|
||||
query: {
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
filter,
|
||||
search,
|
||||
},
|
||||
version: '2023-10-31',
|
||||
}
|
||||
);
|
||||
const response = await KibanaServices.get().http.get<GetNotesResponse>(NOTE_URL, {
|
||||
query: {
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
filter,
|
||||
search,
|
||||
},
|
||||
version: '2023-10-31',
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
|
@ -65,13 +69,10 @@ export const fetchNotes = async ({
|
|||
* Fetches all the notes for an array of document ids
|
||||
*/
|
||||
export const fetchNotesByDocumentIds = async (documentIds: string[]) => {
|
||||
const response = await KibanaServices.get().http.get<{ notes: Note[]; totalCount: number }>(
|
||||
NOTE_URL,
|
||||
{
|
||||
query: { documentIds },
|
||||
version: '2023-10-31',
|
||||
}
|
||||
);
|
||||
const response = await KibanaServices.get().http.get<GetNotesResponse>(NOTE_URL, {
|
||||
query: { documentIds },
|
||||
version: '2023-10-31',
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
|
@ -79,13 +80,10 @@ export const fetchNotesByDocumentIds = async (documentIds: string[]) => {
|
|||
* Fetches all the notes for an array of saved object ids
|
||||
*/
|
||||
export const fetchNotesBySaveObjectIds = async (savedObjectIds: string[]) => {
|
||||
const response = await KibanaServices.get().http.get<{ notes: Note[]; totalCount: number }>(
|
||||
NOTE_URL,
|
||||
{
|
||||
query: { savedObjectIds },
|
||||
version: '2023-10-31',
|
||||
}
|
||||
);
|
||||
const response = await KibanaServices.get().http.get<GetNotesResponse>(NOTE_URL, {
|
||||
query: { savedObjectIds },
|
||||
version: '2023-10-31',
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
|
@ -93,7 +91,7 @@ export const fetchNotesBySaveObjectIds = async (savedObjectIds: string[]) => {
|
|||
* Deletes multiple notes
|
||||
*/
|
||||
export const deleteNotes = async (noteIds: string[]) => {
|
||||
const response = await KibanaServices.get().http.delete<{ data: unknown }>(NOTE_URL, {
|
||||
const response = await KibanaServices.get().http.delete<DeleteNoteResponse>(NOTE_URL, {
|
||||
body: JSON.stringify({ noteIds }),
|
||||
version: '2023-10-31',
|
||||
});
|
||||
|
|
|
@ -102,6 +102,7 @@ describe('AddNote', () => {
|
|||
});
|
||||
|
||||
it('should render error toast if create a note fails', () => {
|
||||
const createNoteError = new Error('This error comes from the backend');
|
||||
const store = createMockStore({
|
||||
...mockGlobalState,
|
||||
notes: {
|
||||
|
@ -112,7 +113,7 @@ describe('AddNote', () => {
|
|||
},
|
||||
error: {
|
||||
...mockGlobalState.notes.error,
|
||||
createNote: { type: 'http', status: 500 },
|
||||
createNote: createNoteError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -123,9 +124,12 @@ describe('AddNote', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockAddError).toHaveBeenCalledWith(null, {
|
||||
title: CREATE_NOTE_ERROR,
|
||||
});
|
||||
expect(mockAddError).toHaveBeenCalledWith(
|
||||
createNoteError,
|
||||
expect.objectContaining({
|
||||
title: CREATE_NOTE_ERROR,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onNodeAdd callback when it is available', async () => {
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
ReqStatus,
|
||||
selectCreateNoteError,
|
||||
selectCreateNoteStatus,
|
||||
userClosedCreateErrorToast,
|
||||
} from '../store/notes.slice';
|
||||
import { MarkdownEditor } from '../../common/components/markdown_editor';
|
||||
|
||||
|
@ -101,14 +102,19 @@ export const AddNote = memo(
|
|||
setEditorValue('');
|
||||
}, [dispatch, editorValue, eventId, telemetry, timelineId, onNoteAdd]);
|
||||
|
||||
const resetError = useCallback(() => {
|
||||
dispatch(userClosedCreateErrorToast());
|
||||
}, [dispatch]);
|
||||
|
||||
// show a toast if the create note call fails
|
||||
useEffect(() => {
|
||||
if (createStatus === ReqStatus.Failed && createError) {
|
||||
addErrorToast(null, {
|
||||
addErrorToast(createError, {
|
||||
title: CREATE_NOTE_ERROR,
|
||||
});
|
||||
resetError();
|
||||
}
|
||||
}, [addErrorToast, createError, createStatus]);
|
||||
}, [addErrorToast, createError, createStatus, resetError]);
|
||||
|
||||
const buttonDisabled = useMemo(
|
||||
() => disableButton || editorValue.trim().length === 0 || isMarkdownInvalid,
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
fetchNotesBySavedObjectIds,
|
||||
selectNotesBySavedObjectId,
|
||||
selectSortedNotesBySavedObjectId,
|
||||
userClosedCreateErrorToast,
|
||||
} from './notes.slice';
|
||||
import type { NotesState } from './notes.slice';
|
||||
import { mockGlobalState } from '../../common/mock';
|
||||
|
@ -533,6 +534,25 @@ describe('notesSlice', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('userClosedCreateErrorToast', () => {
|
||||
it('should reset create note error', () => {
|
||||
const action = { type: userClosedCreateErrorToast.type };
|
||||
|
||||
expect(
|
||||
notesReducer(
|
||||
{
|
||||
...initalEmptyState,
|
||||
error: {
|
||||
...initalEmptyState.error,
|
||||
createNote: new Error(),
|
||||
},
|
||||
},
|
||||
action
|
||||
).error.createNote
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('userSelectedNotesForDeletion', () => {
|
||||
it('should set correct id when user selects a note to delete', () => {
|
||||
const action = { type: userSelectedNotesForDeletion.type, payload: '1' };
|
||||
|
|
|
@ -103,7 +103,7 @@ export const fetchNotesByDocumentIds = createAsyncThunk<
|
|||
>('notes/fetchNotesByDocumentIds', async (args) => {
|
||||
const { documentIds } = args;
|
||||
const res = await fetchNotesByDocumentIdsApi(documentIds);
|
||||
return normalizeEntities(res.notes);
|
||||
return normalizeEntities('notes' in res ? res.notes : []);
|
||||
});
|
||||
|
||||
export const fetchNotesBySavedObjectIds = createAsyncThunk<
|
||||
|
@ -113,7 +113,7 @@ export const fetchNotesBySavedObjectIds = createAsyncThunk<
|
|||
>('notes/fetchNotesBySavedObjectIds', async (args) => {
|
||||
const { savedObjectIds } = args;
|
||||
const res = await fetchNotesBySaveObjectIdsApi(savedObjectIds);
|
||||
return normalizeEntities(res.notes);
|
||||
return normalizeEntities('notes' in res ? res.notes : []);
|
||||
});
|
||||
|
||||
export const fetchNotes = createAsyncThunk<
|
||||
|
@ -130,7 +130,10 @@ export const fetchNotes = createAsyncThunk<
|
|||
>('notes/fetchNotes', async (args) => {
|
||||
const { page, perPage, sortField, sortOrder, filter, search } = args;
|
||||
const res = await fetchNotesApi({ page, perPage, sortField, sortOrder, filter, search });
|
||||
return { ...normalizeEntities(res.notes), totalCount: res.totalCount };
|
||||
return {
|
||||
...normalizeEntities('notes' in res ? res.notes : []),
|
||||
totalCount: 'totalCount' in res ? res.totalCount : 0,
|
||||
};
|
||||
});
|
||||
|
||||
export const createNote = createAsyncThunk<NormalizedEntity<Note>, { note: BareNote }, {}>(
|
||||
|
@ -199,6 +202,9 @@ const notesSlice = createSlice({
|
|||
userSelectedBulkDelete: (state) => {
|
||||
state.pendingDeleteIds = state.selectedIds;
|
||||
},
|
||||
userClosedCreateErrorToast: (state) => {
|
||||
state.error.createNote = null;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
|
@ -308,12 +314,12 @@ export const selectFetchNotesError = (state: State) => state.notes.error.fetchNo
|
|||
export const selectFetchNotesStatus = (state: State) => state.notes.status.fetchNotes;
|
||||
|
||||
export const selectNotesByDocumentId = createSelector(
|
||||
[selectAllNotes, (state: State, documentId: string) => documentId],
|
||||
[selectAllNotes, (_: State, documentId: string) => documentId],
|
||||
(notes, documentId) => notes.filter((note) => note.eventId === documentId)
|
||||
);
|
||||
|
||||
export const selectNotesBySavedObjectId = createSelector(
|
||||
[selectAllNotes, (state: State, savedObjectId: string) => savedObjectId],
|
||||
[selectAllNotes, (_: State, savedObjectId: string) => savedObjectId],
|
||||
(notes, savedObjectId) =>
|
||||
savedObjectId.length > 0 ? notes.filter((note) => note.timelineId === savedObjectId) : []
|
||||
);
|
||||
|
@ -321,10 +327,10 @@ export const selectNotesBySavedObjectId = createSelector(
|
|||
export const selectDocumentNotesBySavedObjectId = createSelector(
|
||||
[
|
||||
selectAllNotes,
|
||||
(
|
||||
state: State,
|
||||
{ documentId, savedObjectId }: { documentId: string; savedObjectId: string }
|
||||
) => ({ documentId, savedObjectId }),
|
||||
(_: State, { documentId, savedObjectId }: { documentId: string; savedObjectId: string }) => ({
|
||||
documentId,
|
||||
savedObjectId,
|
||||
}),
|
||||
],
|
||||
(notes, { documentId, savedObjectId }) =>
|
||||
notes.filter((note) => note.eventId === documentId && note.timelineId === savedObjectId)
|
||||
|
@ -334,7 +340,7 @@ export const selectSortedNotesByDocumentId = createSelector(
|
|||
[
|
||||
selectAllNotes,
|
||||
(
|
||||
state: State,
|
||||
_: State,
|
||||
{
|
||||
documentId,
|
||||
sort,
|
||||
|
@ -359,7 +365,7 @@ export const selectSortedNotesBySavedObjectId = createSelector(
|
|||
[
|
||||
selectAllNotes,
|
||||
(
|
||||
state: State,
|
||||
_: State,
|
||||
{
|
||||
savedObjectId,
|
||||
sort,
|
||||
|
@ -391,6 +397,7 @@ export const {
|
|||
userSearchedNotes,
|
||||
userSelectedRow,
|
||||
userClosedDeleteModal,
|
||||
userClosedCreateErrorToast,
|
||||
userSelectedNotesForDeletion,
|
||||
userSelectedBulkDelete,
|
||||
} = notesSlice.actions;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { NOTE_URL } from '../../../../common/constants';
|
||||
import type { BareNote, Note } from '../../../../common/api/timeline';
|
||||
import type { BareNote, PersistNoteRouteResponse } from '../../../../common/api/timeline';
|
||||
import { KibanaServices } from '../../../common/lib/kibana';
|
||||
|
||||
export const persistNote = async ({
|
||||
|
@ -27,7 +27,7 @@ export const persistNote = async ({
|
|||
} catch (err) {
|
||||
return Promise.reject(new Error(`Failed to stringify query: ${JSON.stringify(err)}`));
|
||||
}
|
||||
const response = await KibanaServices.get().http.patch<Note[]>(NOTE_URL, {
|
||||
const response = await KibanaServices.get().http.patch<PersistNoteRouteResponse>(NOTE_URL, {
|
||||
method: 'PATCH',
|
||||
body: requestBody,
|
||||
version: '2023-10-31',
|
||||
|
|
|
@ -11,11 +11,11 @@ import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'
|
|||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
import { timelineSavedObjectType } from '../../saved_object_mappings';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../../types';
|
||||
import { NOTE_URL } from '../../../../../common/constants';
|
||||
import { MAX_UNASSOCIATED_NOTES, NOTE_URL } from '../../../../../common/constants';
|
||||
|
||||
import { buildSiemResponse } from '../../../detection_engine/routes/utils';
|
||||
import { buildFrameworkRequest } from '../../utils/common';
|
||||
import { getAllSavedNote, MAX_UNASSOCIATED_NOTES } from '../../saved_object/notes';
|
||||
import { getAllSavedNote } from '../../saved_object/notes';
|
||||
import { noteSavedObjectType } from '../../saved_object_mappings/notes';
|
||||
import { GetNotesRequestQuery, type GetNotesResponse } from '../../../../../common/api/timeline';
|
||||
|
||||
|
@ -39,6 +39,10 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
try {
|
||||
const queryParams = request.query;
|
||||
const frameworkRequest = await buildFrameworkRequest(context, request);
|
||||
const {
|
||||
uiSettings: { client: uiSettingsClient },
|
||||
} = await frameworkRequest.context.core;
|
||||
const maxUnassociatedNotes = await uiSettingsClient.get<number>(MAX_UNASSOCIATED_NOTES);
|
||||
const documentIds = queryParams.documentIds ?? null;
|
||||
const savedObjectIds = queryParams.savedObjectIds ?? null;
|
||||
if (documentIds != null) {
|
||||
|
@ -48,7 +52,7 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
type: noteSavedObjectType,
|
||||
search: docIdSearchString,
|
||||
page: 1,
|
||||
perPage: MAX_UNASSOCIATED_NOTES,
|
||||
perPage: maxUnassociatedNotes,
|
||||
};
|
||||
const res = await getAllSavedNote(frameworkRequest, options);
|
||||
const body: GetNotesResponse = res ?? {};
|
||||
|
@ -58,7 +62,7 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
type: noteSavedObjectType,
|
||||
search: documentIds,
|
||||
page: 1,
|
||||
perPage: MAX_UNASSOCIATED_NOTES,
|
||||
perPage: maxUnassociatedNotes,
|
||||
};
|
||||
const res = await getAllSavedNote(frameworkRequest, options);
|
||||
return response.ok({ body: res ?? {} });
|
||||
|
@ -73,7 +77,7 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
id: soIdSearchString,
|
||||
},
|
||||
page: 1,
|
||||
perPage: MAX_UNASSOCIATED_NOTES,
|
||||
perPage: maxUnassociatedNotes,
|
||||
};
|
||||
const res = await getAllSavedNote(frameworkRequest, options);
|
||||
const body: GetNotesResponse = res ?? {};
|
||||
|
@ -85,7 +89,7 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
type: timelineSavedObjectType,
|
||||
id: savedObjectIds,
|
||||
},
|
||||
perPage: MAX_UNASSOCIATED_NOTES,
|
||||
perPage: maxUnassociatedNotes,
|
||||
};
|
||||
const res = await getAllSavedNote(frameworkRequest, options);
|
||||
const body: GetNotesResponse = res ?? {};
|
||||
|
|
|
@ -187,6 +187,10 @@ describe('persistNote', () => {
|
|||
created_at: '2024-06-25T22:56:01.354Z',
|
||||
created_by: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
|
||||
};
|
||||
const mockUiSettingsClientGet = jest.fn();
|
||||
const mockUiSettingsClient = {
|
||||
get: mockUiSettingsClientGet,
|
||||
};
|
||||
const mockSavedObjectClient = savedObjectsClientMock.create();
|
||||
const core = coreMock.createRequestHandlerContext();
|
||||
const context = {
|
||||
|
@ -197,6 +201,10 @@ describe('persistNote', () => {
|
|||
...core.savedObjects,
|
||||
client: mockSavedObjectClient,
|
||||
},
|
||||
uiSettings: {
|
||||
...core.uiSettings,
|
||||
client: mockUiSettingsClient,
|
||||
},
|
||||
},
|
||||
resolve: jest.fn(),
|
||||
} as unknown as RequestHandlerContext;
|
||||
|
@ -304,7 +312,7 @@ describe('persistNote', () => {
|
|||
message: 'Cannot create more than 1000 notes without associating them to a timeline',
|
||||
note: mockNote,
|
||||
});
|
||||
|
||||
mockUiSettingsClientGet.mockResolvedValue(1000);
|
||||
const result = await persistNote({ request: mockRequest, noteId: null, note: mockNote });
|
||||
|
||||
expect(result.code).toBe(403);
|
||||
|
|
|
@ -16,7 +16,7 @@ import { identity } from 'fp-ts/lib/function';
|
|||
import type { SavedObjectsFindOptions } from '@kbn/core/server';
|
||||
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
|
||||
import { getUserDisplayName } from '@kbn/user-profile-components';
|
||||
import { UNAUTHENTICATED_USER } from '../../../../../common/constants';
|
||||
import { MAX_UNASSOCIATED_NOTES, UNAUTHENTICATED_USER } from '../../../../../common/constants';
|
||||
import type {
|
||||
Note,
|
||||
BareNote,
|
||||
|
@ -31,8 +31,6 @@ import { noteSavedObjectType } from '../../saved_object_mappings/notes';
|
|||
import { timelineSavedObjectType } from '../../saved_object_mappings';
|
||||
import { noteFieldsMigrator } from './field_migrator';
|
||||
|
||||
export const MAX_UNASSOCIATED_NOTES = 1000;
|
||||
|
||||
export const deleteNotesByTimelineId = async (request: FrameworkRequest, timelineId: string) => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
|
@ -135,7 +133,10 @@ export const createNote = async ({
|
|||
note: BareNote | BareNoteWithoutExternalRefs;
|
||||
overrideOwner?: boolean;
|
||||
}): Promise<ResponseNote> => {
|
||||
const savedObjectsClient = (await request.context.core).savedObjects.client;
|
||||
const {
|
||||
savedObjects: { client: savedObjectsClient },
|
||||
uiSettings: { client: uiSettingsClient },
|
||||
} = await request.context.core;
|
||||
const userInfo = request.user;
|
||||
|
||||
const noteWithCreator = overrideOwner ? pickSavedNote(noteId, { ...note }, userInfo) : note;
|
||||
|
@ -145,15 +146,15 @@ export const createNote = async ({
|
|||
data: noteWithCreator,
|
||||
});
|
||||
if (references.length === 1 && references[0].id === '') {
|
||||
// Limit unassociated events to 1000
|
||||
const maxUnassociatedNotes = await uiSettingsClient.get<number>(MAX_UNASSOCIATED_NOTES);
|
||||
const notesCount = await savedObjectsClient.find<SavedObjectNoteWithoutExternalRefs>({
|
||||
type: noteSavedObjectType,
|
||||
hasReference: { type: timelineSavedObjectType, id: '' },
|
||||
});
|
||||
if (notesCount.total >= MAX_UNASSOCIATED_NOTES) {
|
||||
if (notesCount.total >= maxUnassociatedNotes) {
|
||||
return {
|
||||
code: 403,
|
||||
message: `Cannot create more than ${MAX_UNASSOCIATED_NOTES} notes without associating them to a timeline`,
|
||||
message: `Cannot create more than ${maxUnassociatedNotes} notes without associating them to a timeline`,
|
||||
note: {
|
||||
...note,
|
||||
noteId: uuidv1(),
|
||||
|
|
|
@ -23,14 +23,19 @@ export const buildFrameworkRequest = async (
|
|||
const coreContext = await context.core;
|
||||
const savedObjectsClient = coreContext.savedObjects.client;
|
||||
const user = coreContext.security.authc.getCurrentUser();
|
||||
const uiSettings = coreContext.uiSettings;
|
||||
|
||||
return set<FrameworkRequest>(
|
||||
'user',
|
||||
user,
|
||||
set<KibanaRequest & { context: RequestHandlerContext }>(
|
||||
'context.core.savedObjects.client',
|
||||
savedObjectsClient,
|
||||
request
|
||||
'context.core.uiSettings',
|
||||
uiSettings,
|
||||
set<FrameworkRequest>(
|
||||
'user',
|
||||
user,
|
||||
set<KibanaRequest & { context: RequestHandlerContext }>(
|
||||
'context.core.savedObjects.client',
|
||||
savedObjectsClient,
|
||||
request
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
DEFAULT_INDEX_PATTERN,
|
||||
DEFAULT_INTERVAL_PAUSE,
|
||||
DEFAULT_INTERVAL_VALUE,
|
||||
DEFAULT_MAX_UNASSOCIATED_NOTES,
|
||||
DEFAULT_RULE_REFRESH_INTERVAL_ON,
|
||||
DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
|
||||
DEFAULT_RULES_TABLE_REFRESH_SETTING,
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
ENABLE_NEWS_FEED_SETTING,
|
||||
IP_REPUTATION_LINKS_SETTING,
|
||||
IP_REPUTATION_LINKS_SETTING_DEFAULT,
|
||||
MAX_UNASSOCIATED_NOTES,
|
||||
NEWS_FEED_URL_SETTING,
|
||||
NEWS_FEED_URL_SETTING_DEFAULT,
|
||||
ENABLE_CCS_READ_WARNING_SETTING,
|
||||
|
@ -342,6 +344,26 @@ export const initUiSettings = (
|
|||
requiresPageReload: true,
|
||||
schema: schema.arrayOf(schema.string()),
|
||||
},
|
||||
[MAX_UNASSOCIATED_NOTES]: {
|
||||
name: i18n.translate('xpack.securitySolution.uiSettings.maxUnassociatedNotesLabel', {
|
||||
defaultMessage: 'Maximum amount of unassociated notes',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.uiSettings.maxUnassociatedNotesDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Defines the maximum amount of unassociated notes (notes that are not assigned to a timeline) that can be created.',
|
||||
}
|
||||
),
|
||||
type: 'number',
|
||||
value: DEFAULT_MAX_UNASSOCIATED_NOTES,
|
||||
schema: schema.number({
|
||||
min: 1,
|
||||
max: 1000,
|
||||
defaultValue: DEFAULT_MAX_UNASSOCIATED_NOTES,
|
||||
}),
|
||||
requiresPageReload: false,
|
||||
},
|
||||
[EXCLUDED_DATA_TIERS_FOR_RULE_EXECUTION]: {
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.uiSettings.excludedDataTiersForRuleExecutionLabel',
|
||||
|
@ -353,8 +375,8 @@ export const initUiSettings = (
|
|||
'xpack.securitySolution.uiSettings.excludedDataTiersForRuleExecutionDescription',
|
||||
{
|
||||
defaultMessage: `
|
||||
When configured, events from the specified data tiers are not searched during rules executions.
|
||||
<br/>This might help to improve rule performance or reduce execution time.
|
||||
When configured, events from the specified data tiers are not searched during rules executions.
|
||||
<br/>This might help to improve rule performance or reduce execution time.
|
||||
<br/>If you specify multiple data tiers, separate values with commas. For example: data_frozen,data_cold`,
|
||||
}
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue