diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f468f828501b3..4f57048f2bbbb 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -115,8 +115,8 @@ export const BLOCKLIST_PATH = `${MANAGEMENT_PATH}/blocklist` as const; export const RESPONSE_ACTIONS_HISTORY_PATH = `${MANAGEMENT_PATH}/response_actions_history` as const; export const ENTITY_ANALYTICS_PATH = '/entity_analytics' as const; export const ENTITY_ANALYTICS_MANAGEMENT_PATH = `/entity_analytics_management` as const; -export const ENTITY_ANALYTICS_ASSET_CLASSIFICATION_PATH = - `/entity_analytics_asset_classification` as const; +export const ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH = + `/entity_analytics_asset_criticality` as const; export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}` as const; export const APP_LANDING_PATH = `${APP_PATH}${LANDING_PATH}` as const; export const APP_DETECTION_RESPONSE_PATH = `${APP_PATH}${DETECTION_RESPONSE_PATH}` as const; diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 516239d164632..cd7f9b1e7c102 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -25,6 +25,13 @@ export const ENTITY_ANALYTICS_RISK_SCORE = i18n.translate( } ); +export const ASSET_CRITICALITY = i18n.translate( + 'xpack.securitySolution.navigation.entityRiskScore', + { + defaultMessage: 'Asset criticality', + } +); + export const DETECTION_RESPONSE = i18n.translate( 'xpack.securitySolution.navigation.detectionResponse', { diff --git a/x-pack/plugins/security_solution/public/common/icons/asset_criticality.tsx b/x-pack/plugins/security_solution/public/common/icons/asset_criticality.tsx new file mode 100644 index 0000000000000..2f8442658e99a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/icons/asset_criticality.tsx @@ -0,0 +1,43 @@ +/* + * 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 { SVGProps } from 'react'; +import React from 'react'; +export const IconAssetCriticality: React.FC> = ({ ...props }) => ( + + + + + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx index 6417180b8fd70..05bc70589a4b3 100644 --- a/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx @@ -62,6 +62,7 @@ const coreMock = { settings: { client: { get: () => {}, + get$: () => new Subject(), set: () => {}, }, }, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index d5d7b8f1097d6..0bd6ff4f24f2f 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -156,7 +156,6 @@ export const useEntityAnalyticsRoutes = () => { version: '1', method: 'POST', headers: { - // 'Content-Type': 'text/csv', 'Content-Type': undefined, }, body, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.stories.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.stories.tsx index 83c146e51712d..00dd1e567aa3c 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.stories.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.stories.tsx @@ -11,13 +11,20 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { euiLightVars } from '@kbn/ui-theme'; import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { StorybookProviders } from '../../../common/mock/storybook_providers'; import { AssetCriticalityFileUploader } from './asset_criticality_file_uploader'; +import { AssetCriticalityResultStep } from './components/result_step'; +import { AssetCriticalityValidationStep } from './components/validation_step'; +import { AssetCriticalityFilePickerStep } from './components/file_picker_step'; addDecorator((storyFn) => ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} )); +const validLinesAsText = `user-001,low_impact\nuser-002,medium_impact,user\nuser-003,medium_impact,user\nhost-001,extreme_impact,host\nhost-002,extreme_impact,host`; +const invalidLinesAsText = `user,user-001,wow_impact\nbleh,user-002,medium_impact\nuser,user-003,medium_impact,extra_column`; + export default { component: AssetCriticalityFileUploader, title: 'Entity Analytics/AssetCriticalityFileUploader', @@ -27,8 +34,117 @@ export const Default: Story = () => { return ( -
- +
+ + + +
+ + + ); +}; + +export const FilePickerStep: Story = () => { + return ( + + +
+ + {}} isLoading={true} /> + +
+
+
+ ); +}; + +export const ValidationStep: Story = () => { + return ( + + +
+ + {}} + onReturn={() => {}} + validLinesAsText={validLinesAsText} + invalidLinesAsText={invalidLinesAsText} + invalidLinesErrors={[ + { error: 'error message 1', index: 1 }, + { error: 'error message 2', index: 2 }, + { error: 'error message 3', index: 3 }, + ]} + /> + +
+
+
+ ); +}; + +export const ResultsStep: Story = () => { + return ( + + +
+ {'Results Step - Success'} + + + + {}} + validLinesAsText={validLinesAsText} + result={{ + errors: [], + stats: { + total: 10, + updated: 5, + created: 5, + errors: 0, + }, + }} + /> + + + + {'Results Step - Partial error'} + + + + {}} + validLinesAsText={validLinesAsText} + result={{ + errors: [ + { message: 'error message 1', index: 1 }, + { message: 'error message 2', index: 3 }, + { message: 'error message 3', index: 5 }, + ], + stats: { + total: 5, + updated: 0, + created: 2, + errors: 3, + }, + }} + /> + + + + + {'Results Step - Complete failure'} + + + + {}} + validLinesAsText="" + errorMessage="Something went wrong" + /> +
diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx index 71755592808e4..9bc2d0f05383e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx @@ -26,15 +26,6 @@ import { getStepStatus } from './helpers'; import { AssetCriticalityResultStep } from './components/result_step'; import { useEntityAnalyticsRoutes } from '../../api/api'; -// TODO -// Should we support headers? -// result step UI -// Add the info icon with the error message - -// WHAT DO I NEED FROM DESIGN? -// 3. serverless navigation -// 5. Rename steps - export const AssetCriticalityFileUploader: React.FC = () => { const [state, dispatch] = useReducer(reducer, { isLoading: false, step: 1 }); const formatBytes = useFormatBytes(); @@ -43,8 +34,7 @@ export const AssetCriticalityFileUploader: React.FC = () => { const onFileChange = (fileList: FileList | null) => { if (fileList?.length === 0) { dispatch({ - type: 'goToStep', - payload: { step: 1 }, + type: 'resetState', }); } @@ -71,13 +61,13 @@ export const AssetCriticalityFileUploader: React.FC = () => { dynamicTyping: true, skipEmptyLines: true, complete(parsedFile, returnedFile) { - const { invalid, valid, error } = validateParsedContent(parsedFile.data); - - if (error) { - dispatch({ type: 'fileError', payload: { message: error, file } }); + if (parsedFile.data.length === 0) { + dispatch({ type: 'fileError', payload: { message: 'The file is empty', file } }); // TODO i18n return; } + const { invalid, valid, errors } = validateParsedContent(parsedFile.data); + const validLinesAsText = Papa.unparse(valid); const invalidLinesAsText = Papa.unparse(invalid); @@ -87,6 +77,7 @@ export const AssetCriticalityFileUploader: React.FC = () => { fileName: returnedFile?.name ?? '', validLinesAsText, invalidLinesAsText, + invalidLinesErrors: errors, validLinesCount: valid.length, invalidLinesCount: invalid.length, }, @@ -115,7 +106,9 @@ export const AssetCriticalityFileUploader: React.FC = () => { ), status: getStepStatus(1, state.step), onClick: () => { - dispatch({ type: 'goToStep', payload: { step: 1 } }); + if (state.step === 2) { + dispatch({ type: 'resetState' }); // User can only go back to the first step from the second step + } }, }, { @@ -126,9 +119,7 @@ export const AssetCriticalityFileUploader: React.FC = () => { } ), status: getStepStatus(2, state.step), - onClick: () => { - dispatch({ type: 'goToStep', payload: { step: 2 } }); - }, + onClick: () => {}, // This prevents the user from going back to the previous step }, { title: i18n.translate( @@ -163,9 +154,10 @@ export const AssetCriticalityFileUploader: React.FC = () => { invalidLinesCount={state.invalidLinesCount} validLinesAsText={state.validLinesAsText} invalidLinesAsText={state.invalidLinesAsText} + invalidLinesErrors={state.invalidLinesErrors ?? []} fileName={state.fileName ?? ''} onReturn={() => { - dispatch({ type: 'goToStep', payload: { step: 1 } }); + dispatch({ type: 'resetState' }); }} onConfirm={async () => { if (state.validLinesAsText) { @@ -173,22 +165,36 @@ export const AssetCriticalityFileUploader: React.FC = () => { type: 'uploadingFile', }); - const result = await uploadAssetCriticalityFile( - state.validLinesAsText, - state.fileName - ); - - dispatch({ - type: 'fileUploaded', - payload: result, - }); + try { + const result = await uploadAssetCriticalityFile( + state.validLinesAsText, + state.fileName + ); + + dispatch({ + type: 'fileUploaded', + payload: { response: result }, + }); + } catch (e) { + dispatch({ + type: 'fileUploaded', + payload: { errorMessage: e.message }, + }); + } } }} /> )} - {state.step === 3 && state.fileUploadResponse && ( - + {state.step === 3 && ( + { + dispatch({ type: 'resetState' }); + }} + /> )}
diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/file_picker_step.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/file_picker_step.tsx index 9a76661475ba8..9d0aac5556c49 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/file_picker_step.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/file_picker_step.tsx @@ -18,13 +18,12 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { euiThemeVars } from '@kbn/ui-theme'; -import { useFormatBytes } from '../../../../common/components/formatted_bytes'; import { - MAX_FILE_SIZE, - SUPPORTED_FILE_EXTENSIONS, - SUPPORTED_FILE_TYPES, - VALID_CRITICALITY_LEVELS, -} from '../constants'; + CRITICALITY_CSV_MAX_SIZE_BYTES, + ValidCriticalityLevels, +} from '../../../../../common/entity_analytics/asset_criticality'; +import { useFormatBytes } from '../../../../common/components/formatted_bytes'; +import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPES } from '../constants'; interface AssetCriticalityFilePickerStepProps { onFileChange: (fileList: FileList | null) => void; @@ -32,7 +31,7 @@ interface AssetCriticalityFilePickerStepProps { errorMessage?: string; } -const sampleCSVContent = `identifier,criticality,type\nuser-001,low_impact,user\nuser-002,medium_impact,user\nhost-001,extreme_impact,host`; +const sampleCSVContent = `type,identifier,criticality\nuser,user-001,low_impact\nuser,user-002,medium_impact\nhost,host-001,extreme_impact`; const listStyle = css` list-style-type: disc; @@ -81,7 +80,7 @@ export const AssetCriticalityFilePickerStep: React.FC @@ -121,7 +120,7 @@ export const AssetCriticalityFilePickerStep: React.FC{VALID_CRITICALITY_LEVELS.join(', ')}, + labels: {ValidCriticalityLevels.join(', ')}, }} /> diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx index 6e778fef4dbc8..7fe8a1f333be4 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx @@ -5,31 +5,133 @@ * 2.0. */ -import { EuiCallOut } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiCallOut, + EuiCodeBlock, + EuiFlexGroup, + EuiHorizontalRule, + EuiSpacer, + useEuiTheme, +} from '@elastic/eui'; import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; import type { AssetCriticalityCsvUploadResponse } from '../../../../../common/entity_analytics/asset_criticality/types'; export const AssetCriticalityResultStep: React.FC<{ - result: AssetCriticalityCsvUploadResponse; -}> = ({ result }) => { - if (result.stats.total > 0) { + result?: AssetCriticalityCsvUploadResponse; + validLinesAsText: string; + errorMessage?: string; + onReturn: () => void; +}> = ({ result, validLinesAsText, errorMessage, onReturn }) => { + const { euiTheme } = useEuiTheme(); + + if (result === undefined || errorMessage !== undefined) { return ( - -

{'Asset criticalities has been successfully mapped.'}

- {result.stats.created ?

{`Created: ${result.stats.created} `}

: ''} - {result.stats.updated ?

{`Updated: ${result.stats.updated}`}

: ''} - {result.stats.errors ?

{`Error: ${result.stats.errors}`}

: ''} -

{`Total: ${result.stats.total}`}

-
+ <> + + } + color="danger" + iconType="error" + > + {errorMessage && errorMessage} + + + ); } + if (result.stats.errors === 0) { + return ( + <> + + + + + + ); + } + + const annotations = result.errors.reduce>((acc, e) => { + acc[e.index] = e.message; + return acc; + }, {}); + return ( <> - -

{'There was an error while mapping asset criticalities.'}

-

{`Errors: ${result.errors.join(', ')}`}

+ + } + color="warning" + iconType="warning" + > + +

+ +

+

+ +

+ + + {validLinesAsText} +
+ ); }; + +const ResultStepFooter = ({ onReturn }: { onReturn: () => void }) => ( + <> + + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx index 65ff5062bdfa7..8f9365eac7d2f 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx @@ -11,6 +11,7 @@ import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, + EuiHorizontalRule, EuiIcon, EuiSpacer, useEuiTheme, @@ -19,6 +20,7 @@ import { css } from '@emotion/react'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { downloadBlob } from '../../../../common/utils/download_blob'; +import type { RowValidationErrors } from '../validations'; interface AssetCriticalityValidationStepProps { validLinesCount: number; @@ -28,8 +30,11 @@ interface AssetCriticalityValidationStepProps { fileName: string; onConfirm: () => void; onReturn: () => void; + invalidLinesErrors: RowValidationErrors[]; } +const CODE_BLOCK_HEIGHT = 250; + export const AssetCriticalityValidationStep: React.FC = ({ validLinesCount, invalidLinesCount, @@ -38,9 +43,15 @@ export const AssetCriticalityValidationStep: React.FC { const { euiTheme } = useEuiTheme(); + const annotations = invalidLinesErrors.reduce>((acc, e) => { + acc[e.index] = e.error; + return acc; + }, {}); + return ( <> @@ -65,7 +76,7 @@ export const AssetCriticalityValidationStep: React.FC {validLinesCount} }} /> @@ -86,7 +97,7 @@ export const AssetCriticalityValidationStep: React.FC {validLinesAsText} - )} @@ -113,7 +123,7 @@ export const AssetCriticalityValidationStep: React.FC {invalidLinesCount}, @@ -145,8 +155,8 @@ export const AssetCriticalityValidationStep: React.FC {invalidLinesAsText} + )} + - - - - - - - + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts index 492e8696cb7c9..c64128274aa3d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts @@ -5,10 +5,5 @@ * 2.0. */ -import { CriticalityLevels } from '../../../../common/entity_analytics/asset_criticality'; - export const SUPPORTED_FILE_TYPES = ['text/csv', 'text/plain', 'text/tab-separated-values']; export const SUPPORTED_FILE_EXTENSIONS = ['CSV', 'TXT', 'TSV']; -export const VALID_CRITICALITY_LEVELS = Object.values(CriticalityLevels); -export const MAX_FILE_SIZE = 1024 * 1024; // 1MB -export const FILE_SIZE_TOLERANCE = 1024 * 50; // ~= 50kb diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts index 34f534098d597..bc4225a90871e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts @@ -6,6 +6,7 @@ */ import type { AssetCriticalityCsvUploadResponse } from '../../../../common/entity_analytics/asset_criticality/types'; +import type { RowValidationErrors } from './validations'; interface ReducerState { fileError?: string; @@ -18,18 +19,23 @@ interface ReducerState { // invalidLines?: string[][]; step: number; fileUploadResponse?: AssetCriticalityCsvUploadResponse; - + fileUploadError?: string; fileName?: string; invalidLinesAsText?: string; validLinesAsText?: string; validLinesCount?: number; invalidLinesCount?: number; + invalidLinesErrors?: RowValidationErrors[]; } type ReducerAction = | { type: 'uploadingFile' } - | { type: 'fileUploaded'; payload: AssetCriticalityCsvUploadResponse } + | { + type: 'fileUploaded'; + payload: { response?: AssetCriticalityCsvUploadResponse; errorMessage?: string }; + } | { type: 'goToStep'; payload: { step: number } } + | { type: 'resetState' } | { type: 'loadingFile'; payload: { fileName: string } } | { type: 'fileValidated'; @@ -38,41 +44,22 @@ type ReducerAction = invalidLinesAsText: string; validLinesAsText: string; validLinesCount: number; + invalidLinesErrors: RowValidationErrors[]; invalidLinesCount: number; }; } | { type: 'fileError'; payload: { message: string; file: File } }; +const initialState = { + isLoading: false, + step: 1, +}; + export const reducer = (state: ReducerState, action: ReducerAction): ReducerState => { switch (action.type) { - case 'goToStep': - if (action.payload.step > state.step || state.step === 3) { - return state; - } + case 'resetState': + return initialState; - if (action.payload.step === 1) { - return { - ...action.payload, - isLoading: false, - step: 1, - }; - } - - if (action.payload.step === 2) { - return { - isLoading: false, - step: 2, - }; - } - - if (action.payload.step === 3) { - return { - isLoading: false, - step: 3, - }; - } - - return state; case 'loadingFile': return { isLoading: true, @@ -103,7 +90,8 @@ export const reducer = (state: ReducerState, action: ReducerAction): ReducerStat case 'fileUploaded': return { ...state, - fileUploadResponse: action.payload, + fileUploadResponse: action.payload.response, + fileUploadError: action.payload.errorMessage, isLoading: false, step: 3, }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts index 69ec2f5eafa62..fde96a1a68d47 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts @@ -6,20 +6,47 @@ */ import { i18n } from '@kbn/i18n'; -import { partition } from 'lodash/fp'; -import { parseAssetCriticalityCsvRow } from '../../../../common/entity_analytics/asset_criticality'; -import { MAX_FILE_SIZE, SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPES } from './constants'; +import { + CRITICALITY_CSV_MAX_SIZE_BYTES, + CRITICALITY_CSV_MAX_SIZE_BYTES_WITH_TOLERANCE, + parseAssetCriticalityCsvRow, +} from '../../../../common/entity_analytics/asset_criticality'; +import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPES } from './constants'; + +export interface RowValidationErrors { + error: string; + index: number; +} export const validateParsedContent = ( data: string[][] -): { valid: string[][]; invalid: string[][]; error?: string } => { +): { valid: string[][]; invalid: string[][]; errors: RowValidationErrors[] } => { if (data.length === 0) { - return { valid: [], invalid: [], error: 'The file is empty' }; + return { valid: [], invalid: [], errors: [] }; } - const [valid, invalid] = partition((row) => parseAssetCriticalityCsvRow(row).valid, data); + let errorIndex = 1; + const { valid, invalid, errors } = data.reduce<{ + valid: string[][]; + invalid: string[][]; + errors: RowValidationErrors[]; + }>( + (acc, row) => { + const parsedRow = parseAssetCriticalityCsvRow(row); + if (parsedRow.valid) { + acc.valid.push(row); + } else { + acc.invalid.push(row); + acc.errors.push({ error: parsedRow.error, index: errorIndex }); + errorIndex++; + } + + return acc; + }, + { valid: [], invalid: [], errors: [] } + ); - return { valid, invalid }; + return { valid, invalid, errors }; }; export const validateFile = ( @@ -51,7 +78,7 @@ export const validateFile = ( }; } - if (file.size > MAX_FILE_SIZE) { + if (file.size > CRITICALITY_CSV_MAX_SIZE_BYTES_WITH_TOLERANCE) { return { valid: false, errorMessage: i18n.translate( @@ -60,7 +87,7 @@ export const validateFile = ( defaultMessage: 'File size {fileSize} exceeds maximum file size of {maxFileSize}', values: { fileSize: formatBytes(file.size), - maxFileSize: formatBytes(MAX_FILE_SIZE), + maxFileSize: formatBytes(CRITICALITY_CSV_MAX_SIZE_BYTES), }, } ), diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/asset_criticality_upload_page.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/asset_criticality_upload_page.tsx index 309914f89d951..8a46b96ed3788 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/pages/asset_criticality_upload_page.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/asset_criticality_upload_page.tsx @@ -28,7 +28,7 @@ export const AssetCriticalityUploadPage = () => { pageTitle={ } /> @@ -88,7 +88,7 @@ export const AssetCriticalityUploadPage = () => { diff --git a/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx b/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx index b3168a1b26472..bc9583e8355c1 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx @@ -14,7 +14,7 @@ import { SpyRoute } from '../common/utils/route/spy_routes'; import { NotFoundPage } from '../app/404'; import { - ENTITY_ANALYTICS_ASSET_CLASSIFICATION_PATH, + ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH, ENTITY_ANALYTICS_MANAGEMENT_PATH, SecurityPageName, } from '../../common/constants'; @@ -58,7 +58,7 @@ const EntityAnalyticsAssetClassificationContainer: React.FC = React.memo(() => { return ( @@ -76,7 +76,7 @@ export const routes = [ component: EntityAnalyticsManagementContainer, }, { - path: ENTITY_ANALYTICS_ASSET_CLASSIFICATION_PATH, + path: ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH, component: EntityAnalyticsAssetClassificationContainer, }, ]; diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 6429b39eda161..0732438fca418 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -16,7 +16,7 @@ import { import { BLOCKLIST_PATH, ENDPOINTS_PATH, - ENTITY_ANALYTICS_ASSET_CLASSIFICATION_PATH, + ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH, ENTITY_ANALYTICS_MANAGEMENT_PATH, EVENT_FILTERS_PATH, HOST_ISOLATION_EXCEPTIONS_PATH, @@ -37,6 +37,7 @@ import { RESPONSE_ACTIONS_HISTORY, TRUSTED_APPLICATIONS, ENTITY_ANALYTICS_RISK_SCORE, + ASSET_CRITICALITY, } from '../app/translations'; import { licenseService } from '../common/hooks/use_license'; import type { LinkItem } from '../common/links/types'; @@ -51,6 +52,7 @@ import { IconSavedObject } from '../common/icons/saved_object'; import { IconDashboards } from '../common/icons/dashboards'; import { IconEntityAnalytics } from '../common/icons/entity_analytics'; import { HostIsolationExceptionsApiClient } from './pages/host_isolation_exceptions/host_isolation_exceptions_api_client'; +import { IconAssetCriticality } from '../common/icons/asset_criticality'; const categories = [ { @@ -186,12 +188,15 @@ export const links: LinkItem = { }, { id: SecurityPageName.entityAnalyticsAssetClassification, - title: 'Asset classification', // TODO i18n - description: i18n.translate('xpack.securitySolution.appLinks.entityRiskScoringDescription', { - defaultMessage: 'Description test 1 2 3', - }), - landingIcon: IconEntityAnalytics, - path: ENTITY_ANALYTICS_ASSET_CLASSIFICATION_PATH, + title: ASSET_CRITICALITY, + description: i18n.translate( + 'xpack.securitySolution.appLinks.assetClassificationDescription', + { + defaultMessage: 'Represents the criticality of an asset to your business infrastructure.', + } + ), + landingIcon: IconAssetCriticality, + path: ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH, skipUrlState: true, hideTimeline: true, capabilities: [`${SERVER_APP_ID}.entity-analytics`],