diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index e5ddfbe4dd037..6b3db9460eb7c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -22,6 +22,10 @@ export const stackManagementSchema: MakeSchemaFrom = { _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.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 2acb487e7ed08..92076ebc302e2 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -183,5 +183,6 @@ export interface UsageStats { 'aiAssistant:preferredAIAssistantType': string; 'observability:profilingFetchTopNFunctionsFromStacktraces': boolean; 'securitySolution:excludedDataTiersForRuleExecution': string[]; + 'securitySolution:maxUnassociatedNotes': number; 'observability:searchExcludedDataTiers': string[]; } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 616a037fbd5d1..a3e46f5684135 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -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." diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 35f2b8ae177c0..e7bb823c04ec8 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -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; diff --git a/x-pack/plugins/security_solution/public/notes/api/api.test.ts b/x-pack/plugins/security_solution/public/notes/api/api.test.ts new file mode 100644 index 0000000000000..bc53b7bd78ac5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/notes/api/api.test.ts @@ -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(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/notes/api/api.ts b/x-pack/plugins/security_solution/public/notes/api/api.ts index eb25eed9f2816..4bda803950b84 100644 --- a/x-pack/plugins/security_solution/public/notes/api/api.ts +++ b/x-pack/plugins/security_solution/public/notes/api/api.ts @@ -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(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(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(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(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(NOTE_URL, { body: JSON.stringify({ noteIds }), version: '2023-10-31', }); diff --git a/x-pack/plugins/security_solution/public/notes/components/add_note.test.tsx b/x-pack/plugins/security_solution/public/notes/components/add_note.test.tsx index 9b2e0596d5357..e195339c28a52 100644 --- a/x-pack/plugins/security_solution/public/notes/components/add_note.test.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/add_note.test.tsx @@ -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', () => { ); - 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 () => { diff --git a/x-pack/plugins/security_solution/public/notes/components/add_note.tsx b/x-pack/plugins/security_solution/public/notes/components/add_note.tsx index d31aaaa028b56..b3b226550b66f 100644 --- a/x-pack/plugins/security_solution/public/notes/components/add_note.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/add_note.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts index 1d2d197705fef..3ab0333dc1abb 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts @@ -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' }; diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts index 2d24ab838ee06..979e984b5719b 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts @@ -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, { 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; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/notes/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/notes/api.ts index 25cb18f574260..b1506c17a7cbe 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/notes/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/notes/api.ts @@ -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_URL, { + const response = await KibanaServices.get().http.patch(NOTE_URL, { method: 'PATCH', body: requestBody, version: '2023-10-31', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts index 2794fd5d8cd7d..0f3440d8ed13a 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts @@ -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(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 ?? {}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.test.ts index ed5b198932768..227c336d9a19a 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.test.ts @@ -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); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts index 300903f8b22ee..ff353efe0fb53 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts @@ -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 => { - 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(MAX_UNASSOCIATED_NOTES); const notesCount = await savedObjectsClient.find({ 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(), diff --git a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts index 259719a18bdf0..a3de26000d8a0 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts @@ -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( - 'user', - user, - set( - 'context.core.savedObjects.client', - savedObjectsClient, - request + 'context.core.uiSettings', + uiSettings, + set( + 'user', + user, + set( + 'context.core.savedObjects.client', + savedObjectsClient, + request + ) ) ); }; diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index 36b6a5a6582c8..ecf3629b54831 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -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. -
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. +
This might help to improve rule performance or reduce execution time.
If you specify multiple data tiers, separate values with commas. For example: data_frozen,data_cold`, } ),