From 3140d745203b06c5c68167a8d0c841af8767e0e7 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 18 Jul 2024 17:24:43 -0700 Subject: [PATCH 1/3] fixed ui issues Signed-off-by: Amardeepsingh Siglani --- common/helpers.ts | 18 ++ public/app.scss | 1 + public/components/DeleteModal/DeleteModal.tsx | 4 +- .../Notifications/NotificationForm.tsx | 10 +- .../components/DetectorSchedule/Interval.tsx | 2 +- .../ThreatIntelFindingsTable.tsx | 2 + .../Findings/containers/Findings/Findings.tsx | 26 +- .../ConfigureThreatIntelAlertTriggers.tsx | 5 +- .../SelectThreatIntelLogSourcesForm.tsx | 83 +++--- .../ThreatIntelLogSourcesFlyout.tsx | 3 +- .../ThreatIntelSourceDetails.tsx | 27 +- .../AddThreatIntelSource.scss | 3 + .../AddThreatIntelSource.tsx | 250 ++++++++++++++++-- .../ThreatIntelScanConfigForm.tsx | 60 ++++- .../ThreatIntelSource/ThreatIntelSource.tsx | 5 +- public/pages/ThreatIntel/utils/constants.ts | 4 +- public/pages/ThreatIntel/utils/helpers.ts | 15 +- public/utils/helpers.tsx | 2 +- public/utils/validation.ts | 8 + server/clusters/addFieldMappingMethods.ts | 2 +- server/services/FieldMappingService.ts | 8 +- 21 files changed, 420 insertions(+), 118 deletions(-) create mode 100644 public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.scss diff --git a/common/helpers.ts b/common/helpers.ts index af8677885..1c635f64c 100644 --- a/common/helpers.ts +++ b/common/helpers.ts @@ -43,3 +43,21 @@ export const setSecurityAnalyticsPluginConfig = (config: SecurityAnalyticsPlugin export const getSecurityAnalyticsPluginConfig = (): SecurityAnalyticsPluginConfigType | undefined => securityAnalyticsPluginConfig; + +export function extractFieldsFromMappings( + properties: any, + fields: string[], + parentField: string = '' +) { + Object.keys(properties).forEach((field) => { + if (properties[field].hasOwnProperty('properties')) { + extractFieldsFromMappings( + properties[field]['properties'], + fields, + parentField ? `${parentField}.${field}` : field + ); + } else { + fields.push(parentField ? `${parentField}.${field}` : field); + } + }); +} diff --git a/public/app.scss b/public/app.scss index d721e5522..51faa952d 100644 --- a/public/app.scss +++ b/public/app.scss @@ -21,6 +21,7 @@ $euiTextColor: $euiColorDarkestShade !default; @import "./pages/Rules/components/RuleEditor/RuleEditorForm.scss"; @import "./pages/Rules/components/RuleEditor/DetectionVisualEditor.scss"; @import "./pages/Rules/components/RuleEditor/components/SelectionExpField.scss"; +@import "./pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.scss"; .selected-radio-panel { background-color: tintOrShade($euiColorPrimary, 90%, 70%); diff --git a/public/components/DeleteModal/DeleteModal.tsx b/public/components/DeleteModal/DeleteModal.tsx index d062873f6..4456dde74 100644 --- a/public/components/DeleteModal/DeleteModal.tsx +++ b/public/components/DeleteModal/DeleteModal.tsx @@ -20,6 +20,7 @@ interface DeleteModalProps { ids: string; onClickDelete: (event?: any) => void; type: string; + confirmButtonText?: string; } interface DeleteModalState { @@ -49,6 +50,7 @@ export default class DeleteModal extends Component = ({ onNotificationToggle, }) => { const hasNotificationPlugin = getIsNotificationPluginInstalled(); - const [isActionRemoved, setIsActionRemoved] = useState(true); + const [shouldSendNotification, setShouldSendNotification] = useState(!!action?.destination_id); const selectedNotificationChannelOption: NotificationChannelOption[] = []; - if (!isActionRemoved && action?.destination_id) { + if (shouldSendNotification && action?.destination_id) { allNotificationChannels.forEach((typeOption) => { const matchingChannel = typeOption.options.find( (option) => option.value === action.destination_id @@ -66,14 +66,14 @@ export const NotificationForm: React.FC = ({ <> { - setIsActionRemoved(e.target.checked); + setShouldSendNotification(e.target.checked); onNotificationToggle?.(e.target.checked); }} /> - {isActionRemoved && ( + {shouldSendNotification && ( <> diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx index c3fb3930d..8bd7008d4 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx @@ -29,7 +29,7 @@ export interface IntervalState { export class Interval extends React.Component { state = { - isIntervalValid: true, + isIntervalValid: Number.isInteger(this.props.schedule.period.interval), }; onTimeIntervalChange = (event: React.ChangeEvent) => { diff --git a/public/pages/Findings/components/FindingsTable/ThreatIntelFindingsTable.tsx b/public/pages/Findings/components/FindingsTable/ThreatIntelFindingsTable.tsx index 3b09e641e..2b16b5b44 100644 --- a/public/pages/Findings/components/FindingsTable/ThreatIntelFindingsTable.tsx +++ b/public/pages/Findings/components/FindingsTable/ThreatIntelFindingsTable.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { renderTime } from '../../../../utils/helpers'; import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants'; import { DataStore } from '../../../../store/DataStore'; +import { IocLabel, ThreatIntelIocType } from '../../../../../common/constants'; export interface ThreatIntelFindingsTableProps { findingItems: ThreatIntelFinding[]; @@ -30,6 +31,7 @@ export const ThreatIntelFindingsTable: React.FC = { name: 'Indicator type', field: 'ioc_type', + render: (iocType: ThreatIntelIocType) => IocLabel[iocType], }, { name: 'Threat intel source', diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx index b9ea211e0..fa8b54c6a 100644 --- a/public/pages/Findings/containers/Findings/Findings.tsx +++ b/public/pages/Findings/containers/Findings/Findings.tsx @@ -83,12 +83,14 @@ interface DetectionRulesFindingsState { rules: { [id: string]: RuleSource }; groupBy: FindingsGroupByType; filteredFindings: FindingItemType[]; + emptyPromptBody: React.ReactNode; } interface ThreatIntelFindingsState { findings: ThreatIntelFinding[]; groupBy: ThreatIntelFindingsGroupByType; filteredFindings: ThreatIntelFinding[]; + emptyPromptBody: React.ReactNode; } interface FindingsState { @@ -153,11 +155,25 @@ class Findings extends Component { rules: {}, filteredFindings: [], groupBy: 'logType', + emptyPromptBody: ( +

+ Adjust the time range to see more results or{' '} + create a detector to + generate findings. +

+ ), }, [FindingTabId.ThreatIntel]: { findings: [], filteredFindings: [], groupBy: 'indicatorType', + emptyPromptBody: ( +

+ Adjust the time range to see more results or{' '} + setup threat intel to + generate findings. +

+ ), }, }, recentlyUsedRanges: [DEFAULT_DATE_RANGE], @@ -600,15 +616,7 @@ class Findings extends Component { {!findings || findings.length === 0 ? ( No findings} - body={ -

- Adjust the time range to see more results or{' '} - - create a detector - {' '} - to generate findings. -

- } + body={this.state.findingStateByTabId[this.state.selectedTabId].emptyPromptBody} /> ) : ( diff --git a/public/pages/ThreatIntel/components/ConfigureThreatIntelAlertTriggers/ConfigureThreatIntelAlertTriggers.tsx b/public/pages/ThreatIntel/components/ConfigureThreatIntelAlertTriggers/ConfigureThreatIntelAlertTriggers.tsx index 63dd0f986..024a24c86 100644 --- a/public/pages/ThreatIntel/components/ConfigureThreatIntelAlertTriggers/ConfigureThreatIntelAlertTriggers.tsx +++ b/public/pages/ThreatIntel/components/ConfigureThreatIntelAlertTriggers/ConfigureThreatIntelAlertTriggers.tsx @@ -88,7 +88,10 @@ export const ConfigureThreatIntelAlertTriggers: React.FC { - updateTriggers([...alertTriggers, getEmptyThreatIntelAlertTrigger(getNextTriggerName())]); + updateTriggers([ + ...alertTriggers, + getEmptyThreatIntelAlertTrigger(getNextTriggerName(), false), + ]); }} > Add another alert trigger diff --git a/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx b/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx index bbcc38a0c..77741d8da 100644 --- a/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx +++ b/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx @@ -8,12 +8,12 @@ import { EuiBadge, EuiButton, EuiButtonEmpty, - EuiCheckbox, EuiComboBox, EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiIcon, EuiPanel, EuiPopover, EuiSpacer, @@ -49,23 +49,25 @@ export const SelectThreatIntelLogSources: React.FC[]>([]); - const [logSourceMappingByName, setLogSourceMappingByName] = useState>({}); + const [fieldsByIndexName, setFieldsByIndexName] = useState< + Record + >({}); const [iocInfoWithAddFieldOpen, setIocInfoWithAddFieldOpen] = useState< { sourceName: string; ioc: string; selectedFields: string[] } | undefined >(undefined); const getLogFields = useCallback( async (indexName: string) => { - if (saContext && !logSourceMappingByName[indexName]) { + if (saContext && !fieldsByIndexName[indexName]) { getFieldsForIndex(saContext.services.fieldMappingService, indexName).then((fields) => { - setLogSourceMappingByName({ - ...logSourceMappingByName, + setFieldsByIndexName({ + ...fieldsByIndexName, [indexName]: fields, }); }); } }, - [saContext, logSourceMappingByName] + [saContext, fieldsByIndexName] ); const [selectedSourcesMap, setSelectedSourcesMap] = useState(() => { const selectedSourcesByName: Map = new Map(); @@ -150,6 +152,7 @@ export const SelectThreatIntelLogSources: React.FC 0); updateSources?.(Array.from(newSelectedSourcesMap.values())); }; @@ -200,10 +203,7 @@ export const SelectThreatIntelLogSources: React.FCSelect fields to scan -

- To perform detection the IoC from threat intelligence feeds have to be matched against - selected fields in your data. -

+

Add log fields that map to at least one IoC type to perform threat intel scan.

@@ -216,6 +216,14 @@ export const SelectThreatIntelLogSources: React.FC { const { name, iocConfigMap } = source; + const fieldOptionsSet = new Set( + (fieldsByIndexName[name] || []).map(({ label }) => label) + ); + Object.values(ThreatIntelIocType).forEach((iocType) => { + const selectedFields = iocConfigMap[iocType as ThreatIntelIocType]?.fieldAliases || []; + selectedFields.forEach((f) => fieldOptionsSet.delete(f)); + }); + return ( <> - onIocToggle(source, ioc, event.target.checked)} - /> + + + + + + {IocLabel[ioc]} + + - + {config.fieldAliases.map((alias) => ( } panelPaddingSize="s" - closePopover={() => setIocInfoWithAddFieldOpen(undefined)} + closePopover={() => onFieldAliasesAdd(source, ioc)} isOpen={ iocInfoWithAddFieldOpen && iocInfoWithAddFieldOpen.sourceName === name && iocInfoWithAddFieldOpen.ioc === ioc } > - - onFieldAliasesSelect(options.map(({ label }) => label)) - } - selectedOptions={iocInfoWithAddFieldOpen?.selectedFields.map( - (field) => ({ label: field }) - )} - /> - - - - Cancel + + + ({ + label: f, + value: f, + }))} + onChange={(options) => + onFieldAliasesSelect(options.map(({ label }) => label)) + } + selectedOptions={iocInfoWithAddFieldOpen?.selectedFields.map( + (field) => ({ label: field }) + )} + /> - onFieldAliasesAdd(source, ioc)}> - Add fields + onFieldAliasesAdd(source, ioc)}> + Done diff --git a/public/pages/ThreatIntel/components/ThreatIntelLogSourcesFlyout/ThreatIntelLogSourcesFlyout.tsx b/public/pages/ThreatIntel/components/ThreatIntelLogSourcesFlyout/ThreatIntelLogSourcesFlyout.tsx index 72a1f3666..a894dc15e 100644 --- a/public/pages/ThreatIntel/components/ThreatIntelLogSourcesFlyout/ThreatIntelLogSourcesFlyout.tsx +++ b/public/pages/ThreatIntel/components/ThreatIntelLogSourcesFlyout/ThreatIntelLogSourcesFlyout.tsx @@ -58,8 +58,7 @@ export const ThreatIntelLogSourcesFlyout: React.FC = - To perform detection the IoC from threat intelligence feeds have to be matched against - selected fields in your data. + Select log fields that map to at least one IoC type to perform threat intel scan. diff --git a/public/pages/ThreatIntel/components/ThreatIntelSourceDetails/ThreatIntelSourceDetails.tsx b/public/pages/ThreatIntel/components/ThreatIntelSourceDetails/ThreatIntelSourceDetails.tsx index 9525e4482..2f1d7c482 100644 --- a/public/pages/ThreatIntel/components/ThreatIntelSourceDetails/ThreatIntelSourceDetails.tsx +++ b/public/pages/ThreatIntel/components/ThreatIntelSourceDetails/ThreatIntelSourceDetails.tsx @@ -366,18 +366,21 @@ export const ThreatIntelSourceDetails: React.FC = {!isReadOnly && ( - - - - Discard - - - - Save - - - - + <> + + + + + Discard + + + + Save + + + + + )} ); diff --git a/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.scss b/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.scss new file mode 100644 index 000000000..d4776bf85 --- /dev/null +++ b/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.scss @@ -0,0 +1,3 @@ +.label--danger { + color: $ouiColorDanger !important; +} \ No newline at end of file diff --git a/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx b/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx index 2d00eb388..0da0682de 100644 --- a/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx +++ b/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx @@ -41,6 +41,42 @@ import { import { ThreatIntelIocType } from '../../../../../common/constants'; import { PeriodSchedule } from '../../../../../models/interfaces'; import { checkboxes } from '../../utils/constants'; +import { + THREAT_INTEL_SOURCE_DESCRIPTION_REGEX, + THREAT_INTEL_SOURCE_NAME_REGEX, + validateDescription, + validateName, +} from '../../../../utils/validation'; + +interface AddThreatIntelSourceFormInputErrors { + name?: string; + description?: string; + s3?: Partial< + { + [field in keyof S3ConnectionSource['s3']]: string; + } + >; + fileUpload?: { + file?: string; + }; + schedule?: string; + ioc_types?: string; +} + +interface AddThreatIntelSourceFormInputTouched { + name?: boolean; + description?: boolean; + s3?: Partial< + { + [field in keyof S3ConnectionSource['s3']]: boolean; + } + >; + fileUpload?: { + file?: boolean; + }; + schedule?: boolean; + ioc_types?: boolean; +} export interface AddThreatIntelSourceProps extends RouteComponentProps { threatIntelService: ThreatIntelService; @@ -68,11 +104,25 @@ export const AddThreatIntelSource: React.FC = ({ const [fileUploadSource, setFileUploadSource] = useState( getEmptyIocFileUploadSource() ); - const [fileError, setFileError] = useState(''); const [submitInProgress, setSubmitInProgress] = useState(false); const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState>( {} ); + const [inputTouched, setInputTouched] = useState({}); + const [inputErrors, setInputErrors] = useState({}); + + const setFieldError = (fieldErrors: AddThreatIntelSourceFormInputErrors) => { + setInputErrors({ + ...inputErrors, + ...fieldErrors, + }); + }; + const setFieldTouched = (fieldsTouched: AddThreatIntelSourceFormInputTouched) => { + setInputTouched({ + ...inputTouched, + ...fieldsTouched, + }); + }; useEffect(() => { context?.chrome.setBreadcrumbs([ @@ -82,6 +132,11 @@ export const AddThreatIntelSource: React.FC = ({ ]); }, []); + const validateIocTypes = (iocTypeMap: Record) => { + return !Object.values(iocTypeMap).some((val) => val) + ? 'At least one ioc type should be selected' + : ''; + }; const onIocTypesChange = (optionId: string) => { const newCheckboxIdToSelectedMap = { ...checkboxIdToSelectedMap, @@ -89,6 +144,10 @@ export const AddThreatIntelSource: React.FC = ({ [optionId]: !checkboxIdToSelectedMap[optionId], }, }; + setFieldError({ + ioc_types: validateIocTypes(newCheckboxIdToSelectedMap), + }); + setFieldTouched({ ioc_types: true }); setCheckboxIdToSelectedMap(newCheckboxIdToSelectedMap); }; @@ -99,30 +158,89 @@ export const AddThreatIntelSource: React.FC = ({ }); }; + const validateSourceName = (name: string) => { + let error; + if (!name) { + error = 'Name is required.'; + } else if (name.length > 128) { + error = 'Max length can be 128.'; + } else { + const isValid = validateName(name, THREAT_INTEL_SOURCE_NAME_REGEX); + if (!isValid) { + error = 'Invalid name.'; + } + } + + return error; + }; const onNameChange = (event: React.ChangeEvent) => { + const name = event.target.value; + const nameError = validateSourceName(name); + setFieldError({ + name: nameError || '', + }); + setSource({ ...source, - name: event.target.value, + name, }); + setFieldTouched({ name: true }); }; + const validateSourceDescription = (description: string) => { + let error; + const isValid = validateDescription(description, THREAT_INTEL_SOURCE_DESCRIPTION_REGEX); + if (!isValid) { + error = 'Invalid name.'; + } + + return error; + }; const onDescriptionChange = (event: React.ChangeEvent) => { + const descriptionError = validateSourceDescription(event.target.value); + setFieldError({ + description: descriptionError || '', + }); + setSource({ ...source, description: event.target.value, }); + setFieldTouched({ description: true }); }; + const validateS3ConfigField = (value: string) => { + if (!value) { + return `Required.`; + } + }; const onS3DataChange = (field: keyof S3ConnectionSource['s3'], value: string) => { + const error = validateS3ConfigField(value); + setFieldError({ + s3: { + ...inputErrors.s3, + [field]: error || '', + }, + }); setS3ConnectionDetails({ s3: { ...s3ConnectionDetails.s3, [field]: value, }, }); + setFieldTouched({ s3: { ...inputTouched.s3, [field]: true } }); }; + const validateSchedule = (schedule: PeriodSchedule) => { + setFieldError({ + schedule: + !schedule.period.interval || Number.isNaN(schedule.period.interval) + ? 'Invalid schedule' + : '', + }); + }; const onIntervalChange = (schedule: PeriodSchedule) => { + validateSchedule(schedule); setSchedule({ interval: { start_time: Date.now(), @@ -130,19 +248,65 @@ export const AddThreatIntelSource: React.FC = ({ unit: schedule.period.unit, }, }); + setFieldTouched({ schedule: true }); }; const onFileChange = (files: FileList | null) => { - setFileError(''); + setFieldError({ + fileUpload: { + ...inputErrors.fileUpload, + file: files?.length === 0 ? 'File required.' : '', + }, + }); if (!!files?.item(0)) { readIocsFromFile(files[0], (response) => { if (response.ok) { setFileUploadSource(response.sourceData); } else { - setFileError(response.errorMessage); + setFieldError({ + fileUpload: { + ...inputErrors.fileUpload, + file: response.errorMessage, + }, + }); } }); } + setFieldTouched({ fileUpload: { file: true } }); + }; + + const isThereAnError = (errors: any): boolean => { + for (let key of Object.keys(errors)) { + if ( + (sourceType !== 'S3_CUSTOM' && key === 's3') || + (sourceType !== 'IOC_UPLOAD' && key === 'fileUpload') || + (!source.enabled && key === 'schedule') + ) { + continue; + } + + if (typeof errors[key] === 'string' && !!errors[key]) { + return true; + } + + if (typeof errors[key] === 'object') { + if (isThereAnError(errors[key])) { + return true; + } + } + } + + return false; + }; + + const shouldEnableSubmit = () => { + const { name, s3, fileUpload, ioc_types } = inputTouched; + const reqFieldsTouched = + name && + ioc_types && + ((sourceType === 'IOC_UPLOAD' && fileUpload?.file) || + (sourceType === 'S3_CUSTOM' && s3 && Object.values(s3).every((val) => val))); + return reqFieldsTouched && !isThereAnError(inputErrors); }; const onSubmit = () => { @@ -203,8 +367,18 @@ export const AddThreatIntelSource: React.FC = ({

Details

- - + + = ({ @@ -280,7 +455,9 @@ export const AddThreatIntelSource: React.FC = ({ - IAM Role ARN + + IAM Role ARN + The Amazon Resource Name for an IAM role in Amazon Web Services (AWS) @@ -288,40 +465,56 @@ export const AddThreatIntelSource: React.FC = ({ } + isInvalid={!!inputErrors.s3?.role_arn} + error={inputErrors.s3?.role_arn} > onS3DataChange('role_arn', event.target.value)} + onBlur={(event) => onS3DataChange('role_arn', event.target.value)} value={s3ConnectionDetails.s3.role_arn} /> - + onS3DataChange('bucket_name', event.target.value)} + onBlur={(event) => onS3DataChange('bucket_name', event.target.value)} value={s3ConnectionDetails.s3.bucket_name} /> - + onS3DataChange('object_key', event.target.value)} + onBlur={(event) => onS3DataChange('object_key', event.target.value)} value={s3ConnectionDetails.s3.object_key} /> - + onS3DataChange('region', event.target.value)} + onBlur={(event) => onS3DataChange('region', event.target.value)} value={s3ConnectionDetails.s3.region} /> - {/* Test connection - */}

Download schedule

@@ -364,8 +557,8 @@ export const AddThreatIntelSource: React.FC = ({

Maximum size: 500 kB.

} - isInvalid={!!fileError} - error={fileError} + isInvalid={!!inputErrors.fileUpload?.file} + error={inputErrors.fileUpload?.file} > = ({ display={'large'} multiple={false} aria-label="ioc file picker" - isInvalid={!!fileError} + isInvalid={!!inputErrors.fileUpload?.file} data-test-subj="import_ioc_file" />
@@ -385,12 +578,20 @@ export const AddThreatIntelSource: React.FC = ({

Types of malicious indicators

- - + +

+ Select atleast one IOC type to select from the{' '} + {sourceType === 'IOC_UPLOAD' ? 'uploaded file' : 'S3 bucket'}. +

+
+ + + + @@ -399,7 +600,12 @@ export const AddThreatIntelSource: React.FC = ({ history.push(ROUTES.THREAT_INTEL_OVERVIEW)}>Cancel
- + Add threat intel source diff --git a/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx b/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx index bca60e069..6b69520b0 100644 --- a/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx +++ b/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx @@ -34,6 +34,7 @@ import { ConfigureThreatIntelScanStep } from '../../utils/constants'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { PeriodSchedule } from '../../../../../models/interfaces'; import { errorNotificationToast } from '../../../../utils/helpers'; +import { validateName } from '../../../../utils/validation'; export interface ThreatIntelScanConfigFormProps extends RouteComponentProps< @@ -46,13 +47,17 @@ export interface ThreatIntelScanConfigFormProps notifications: NotificationsStart; } +interface TriggerErrors { + nameError?: string; + notificationChannelError?: string; +} + interface FormErrors { logSourceError?: string; fieldAliasError?: string; - triggersErrors?: { - nameError?: string; - notificationChannelError?: string; - }[]; + + scheduleError?: string; + triggersErrors?: TriggerErrors[]; } export const ThreatIntelScanConfigForm: React.FC = ({ @@ -71,8 +76,8 @@ export const ThreatIntelScanConfigForm: React.FC const [formErrors, setFormErrors] = useState({}); const [configureInProgress, setConfigureInProgress] = useState(false); const [stepDataValid, setStepDataValid] = useState({ - [ConfigureThreatIntelScanStep.SelectLogSources]: true, - [ConfigureThreatIntelScanStep.SetupAlertTriggers]: true, + [ConfigureThreatIntelScanStep.SelectLogSources]: false, + [ConfigureThreatIntelScanStep.SetupAlertTriggers]: false, }); const threatIntelTriggerCounter = useRef(0); const getNextTriggerName = () => { @@ -91,6 +96,10 @@ export const ThreatIntelScanConfigForm: React.FC return getEmptyScanConfigFormModel(getNextTriggerName()); }); + useEffect(() => { + updateStepValidity(formErrors); + }, [formErrors]); + useEffect(() => { context?.chrome.setBreadcrumbs([ BREADCRUMBS.SECURITY_ANALYTICS, @@ -131,7 +140,6 @@ export const ThreatIntelScanConfigForm: React.FC ...errors, }; setFormErrors(newErrors); - updateStepValidity(newErrors); }; const validateLogSources = (logSources: ThreatIntelScanConfigFormModel['logSources']) => { @@ -142,7 +150,9 @@ export const ThreatIntelScanConfigForm: React.FC const validateFieldAliases = (logSources: ThreatIntelScanConfigFormModel['logSources']) => { const iocEnabled = logSources.every((logSource) => { - return Object.keys(logSource.iocConfigMap).length !== 0; + return Object.values(logSource.iocConfigMap).some( + (o) => o.enabled && o.fieldAliases.length > 0 + ); }); if (!iocEnabled) { @@ -160,8 +170,31 @@ export const ThreatIntelScanConfigForm: React.FC return ''; }; + const validateSchedule = (schedule: PeriodSchedule) => { + return !schedule.period.interval || Number.isNaN(schedule.period.interval) + ? 'Invalid schedule' + : ''; + }; + + const validateTriggers = (triggers: ThreatIntelAlertTrigger[]) => { + const triggersErrors: TriggerErrors[] = []; + triggers.forEach((t, idx) => { + const errors: TriggerErrors = {}; + errors.nameError = validateName(t.name) ? '' : 'Invalid trigger name'; + errors.notificationChannelError = t.actions.every((a) => !!a.destination_id) + ? '' + : 'Select a channel'; + triggersErrors.push(errors); + }); + setFormErrors({ + ...formErrors, + triggersErrors, + }); + }; + const updateStepValidity = (errors: FormErrors) => { - const stepOneDataValid = !errors.logSourceError && !errors.fieldAliasError; + const stepOneDataValid = + !errors.logSourceError && !errors.fieldAliasError && !errors.scheduleError; const stepTwoDataValid = !errors.triggersErrors?.some( (errors) => errors.nameError || errors.notificationChannelError ); @@ -172,11 +205,6 @@ export const ThreatIntelScanConfigForm: React.FC }); }; - const validateFormData = (formModel: ThreatIntelScanConfigFormModel) => { - validateLogSources(formModel.logSources); - validateFieldAliases(formModel.logSources); - }; - const updatePayload = (formModel: ThreatIntelScanConfigFormModel) => { setConfigureScanFormInputs(formModel); }; @@ -200,6 +228,7 @@ export const ThreatIntelScanConfigForm: React.FC ...configureScanFormInputs, triggers, }); + validateTriggers(triggers); }; const onScheduleChange = (schedule: PeriodSchedule) => { @@ -207,6 +236,9 @@ export const ThreatIntelScanConfigForm: React.FC ...configureScanFormInputs, schedule, }); + updateFormErrors({ + scheduleError: validateSchedule(schedule), + }); }; const getStepConent = (step: ConfigureThreatIntelScanStep) => { diff --git a/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx b/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx index c836c1661..5f068dec6 100644 --- a/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx +++ b/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx @@ -244,7 +244,7 @@ export const ThreatIntelSource: React.FC = ({ }, }) : 'Download on demand' - : 'File uploaded', + : 'N/A', }, { title: 'Last updated', @@ -265,6 +265,9 @@ export const ThreatIntelSource: React.FC = ({ ids={source.name} onClickDelete={onDeleteConfirmed} confirmation + confirmButtonText="Delete" + additionalWarning="You can also deactivate this source temporarily and reactivate later. + Cancel to exit and then choose Deactivate." /> )} diff --git a/public/pages/ThreatIntel/utils/constants.ts b/public/pages/ThreatIntel/utils/constants.ts index 152e71464..fd2c48d81 100644 --- a/public/pages/ThreatIntel/utils/constants.ts +++ b/public/pages/ThreatIntel/utils/constants.ts @@ -21,10 +21,10 @@ export const checkboxes: { id: ThreatIntelIocType; label: string }[] = [ }, { id: ThreatIntelIocType.Domain, - label: 'Domains', + label: IocLabel[ThreatIntelIocType.Domain], }, { id: ThreatIntelIocType.FileHash, - label: 'File hash', + label: IocLabel[ThreatIntelIocType.FileHash], }, ]; diff --git a/public/pages/ThreatIntel/utils/helpers.ts b/public/pages/ThreatIntel/utils/helpers.ts index 1a84aac1a..7b42785c0 100644 --- a/public/pages/ThreatIntel/utils/helpers.ts +++ b/public/pages/ThreatIntel/utils/helpers.ts @@ -35,7 +35,7 @@ export function getEmptyScanConfigFormModel(triggerName: string): ThreatIntelSca unit: 'MINUTES', }, }, - triggers: [getEmptyThreatIntelAlertTrigger(triggerName)], + triggers: [getEmptyThreatIntelAlertTrigger(triggerName, false)], }; } @@ -60,13 +60,16 @@ export function getEmptyThreatIntelAlertTriggerAction(): ThreatIntelAlertTrigger }; } -export function getEmptyThreatIntelAlertTrigger(triggerName: string): ThreatIntelAlertTrigger { +export function getEmptyThreatIntelAlertTrigger( + triggerName: string, + includeEmptyAction: boolean = true +): ThreatIntelAlertTrigger { return { name: triggerName, data_sources: [], ioc_types: [], severity: AlertSeverity.ONE, - actions: [getEmptyThreatIntelAlertTriggerAction()], + actions: includeEmptyAction ? [getEmptyThreatIntelAlertTriggerAction()] : [], }; } @@ -161,12 +164,6 @@ export function deriveFormModelFromConfig( const configClone: any = _.cloneDeep(scanConfig); delete configClone.per_ioc_type_scan_input_list; - configClone.triggers.forEach((trigger: ThreatIntelAlertTrigger) => { - if (trigger.actions.length === 0) { - trigger.actions.push(getEmptyThreatIntelAlertTriggerAction()); - } - }); - const formModel: ThreatIntelScanConfigFormModel = { ...configClone, logSources: Object.values(logSourcesByName), diff --git a/public/utils/helpers.tsx b/public/utils/helpers.tsx index c452aa84b..451ca59d3 100644 --- a/public/utils/helpers.tsx +++ b/public/utils/helpers.tsx @@ -580,7 +580,7 @@ export function getIsNotificationPluginInstalled(): boolean { export async function getFieldsForIndex( fieldMappingService: FieldMappingService, indexName: string -) { +): Promise<{ label: string; value: string }[]> { let fields: { label: string; value: string; diff --git a/public/utils/validation.ts b/public/utils/validation.ts index a565ac34a..aca8c316c 100644 --- a/public/utils/validation.ts +++ b/public/utils/validation.ts @@ -19,6 +19,10 @@ export const LOG_TYPE_NAME_REGEX = new RegExp(/^[a-z0-9_-]{2,50}$/); // numbers 0-9, hyphens, dot, and underscores. export const DETECTION_NAME_REGEX = new RegExp(/^[a-zA-Z0-9_.-]{1,50}$/); +// This regex pattern support MIN to MAX character limit, capital and lowercase letters, +// numbers 0-9, hyphens, dot, and underscores. +export const THREAT_INTEL_SOURCE_NAME_REGEX = new RegExp(/^[a-zA-Z0-9 _-]{1,128}$/); + export const DETECTION_CONDITION_REGEX = new RegExp( /^((not )?.+)?( (and|or|and not|or not|not) ?(.+))*(?/_mapping/field/*`, + fmt: `/<%=indexName%>/_mapping`, req: { indexName: { type: 'string', diff --git a/server/services/FieldMappingService.ts b/server/services/FieldMappingService.ts index a3871b9c5..5047dbacf 100644 --- a/server/services/FieldMappingService.ts +++ b/server/services/FieldMappingService.ts @@ -22,6 +22,7 @@ import { import { ServerResponse } from '../models/types'; import { CLIENT_FIELD_MAPPINGS_METHODS } from '../utils/constants'; import { MDSEnabledClientService } from './MDSEnabledClientService'; +import { extractFieldsFromMappings } from '../../common/helpers'; export default class FieldMappingService extends MDSEnabledClientService { /** @@ -157,9 +158,10 @@ export default class FieldMappingService extends MDSEnabledClientService { } ); - const fieldMappings = Object.values(mappingsResponse)[0]?.mappings; - const fields = Object.keys(fieldMappings || {}).filter( - (field) => Object.keys(fieldMappings[field].mapping).length > 0 + const fields: string[] = []; + extractFieldsFromMappings( + Object.values(mappingsResponse)[0]?.mappings?.properties || {}, + fields ); return response.custom({ From 9dd34570c767fb516b925b80d7499f138db8b59d Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 18 Jul 2024 23:18:27 -0700 Subject: [PATCH 2/3] update notification form; fixed edit scan Signed-off-by: Amardeepsingh Siglani --- .../Notifications/NotificationForm.tsx | 25 ++++++++++--------- .../SelectThreatIntelLogSourcesForm.tsx | 20 +++++++++------ .../ThreatIntelAlertTriggerForm.tsx | 3 --- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/public/components/Notifications/NotificationForm.tsx b/public/components/Notifications/NotificationForm.tsx index 0946dfb05..4377d4013 100644 --- a/public/components/Notifications/NotificationForm.tsx +++ b/public/components/Notifications/NotificationForm.tsx @@ -31,7 +31,7 @@ export interface NotificationFormProps { allNotificationChannels: NotificationChannelTypeOptions[]; loadingNotifications: boolean; action?: TriggerAction; - prepareMessage: (updateMessage?: boolean, onMount?: boolean) => void; + prepareMessage?: (updateMessage?: boolean, onMount?: boolean) => void; refreshNotificationChannels: () => void; onChannelsChange: (selectedOptions: EuiComboBoxOptionOption[]) => void; onMessageBodyChange: (message: string) => void; @@ -168,17 +168,18 @@ export const NotificationForm: React.FC = ({ />
- - - - prepareMessage(true /* updateMessage */)} - > - Generate message - - - + {prepareMessage && ( + + + prepareMessage(true /* updateMessage */)} + > + Generate message + + + + )}
diff --git a/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx b/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx index 77741d8da..4c074ca88 100644 --- a/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx +++ b/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx @@ -69,14 +69,9 @@ export const SelectThreatIntelLogSources: React.FC { - const selectedSourcesByName: Map = new Map(); - sources.forEach((source) => { - selectedSourcesByName.set(source.name, source); - getLogFields(source.name); - }); - return selectedSourcesByName; - }); + const [selectedSourcesMap, setSelectedSourcesMap] = useState>( + new Map() + ); useEffect(() => { const getLogSourceOptions = async () => { @@ -93,6 +88,15 @@ export const SelectThreatIntelLogSources: React.FC { + const selectedSourcesByName: Map = new Map(); + sources.forEach((source) => { + selectedSourcesByName.set(source.name, source); + getLogFields(source.name); + }); + setSelectedSourcesMap(selectedSourcesByName); + }, [sources]); + const onIocToggle = ( source: ThreatIntelLogSource, toggledIoc: ThreatIntelIocType, diff --git a/public/pages/ThreatIntel/components/ThreatIntelAlertTriggerForm/ThreatIntelAlertTriggerForm.tsx b/public/pages/ThreatIntel/components/ThreatIntelAlertTriggerForm/ThreatIntelAlertTriggerForm.tsx index 3d7808b30..6661e17bb 100644 --- a/public/pages/ThreatIntel/components/ThreatIntelAlertTriggerForm/ThreatIntelAlertTriggerForm.tsx +++ b/public/pages/ThreatIntel/components/ThreatIntelAlertTriggerForm/ThreatIntelAlertTriggerForm.tsx @@ -92,8 +92,6 @@ export const ThreatIntelAlertTriggerForm: React.FC }); }; - const prepareMessage = () => {}; - return ( onChannelsChange={onChannelsChange} onMessageBodyChange={onMessageBodyChange} onMessageSubjectChange={onMessageSubjectChange} - prepareMessage={prepareMessage} refreshNotificationChannels={refreshNotificationChannels} onNotificationToggle={onNotificationToggle} /> From 768a133204e8d31697a80ff385c40e7135e6984b Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Fri, 19 Jul 2024 14:36:23 -0700 Subject: [PATCH 3/3] addressed PR comments Signed-off-by: Amardeepsingh Siglani --- .../AlertTriggerView/AlertTriggerView.tsx | 2 +- .../components/FindingDetailsFlyout.tsx | 2 +- .../AddThreatIntelSource.tsx | 32 +++++++++++-------- .../ThreatIntelScanConfigForm.tsx | 2 +- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx index 271293997..bae763a94 100644 --- a/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx +++ b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx @@ -88,7 +88,7 @@ export const AlertTriggerView: React.FC = ({ {createTextDetailsGroup([ - { label: 'IOC match', content: 'Any match in threat intelligence feed' }, + { label: 'IoC match', content: 'Any match in threat intelligence feed' }, ])} )} diff --git a/public/pages/Findings/components/FindingDetailsFlyout.tsx b/public/pages/Findings/components/FindingDetailsFlyout.tsx index 81c385fd0..61c446da6 100644 --- a/public/pages/Findings/components/FindingDetailsFlyout.tsx +++ b/public/pages/Findings/components/FindingDetailsFlyout.tsx @@ -596,7 +596,7 @@ export default class FindingDetailsFlyout extends Component< -

This finding is generated from a threat intelligence feed IOCs.

+

This finding is generated from a threat intelligence feed IoCs.

{createTextDetailsGroup([ diff --git a/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx b/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx index 0da0682de..dbe661d12 100644 --- a/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx +++ b/public/pages/ThreatIntel/containers/AddThreatIntelSource/AddThreatIntelSource.tsx @@ -48,18 +48,24 @@ import { validateName, } from '../../../../utils/validation'; +enum ErrorKeys { + s3 = 's3', + fileUpload = 'fileUpload', + schedule = 'schedule', +} + interface AddThreatIntelSourceFormInputErrors { name?: string; description?: string; - s3?: Partial< + [ErrorKeys.s3]?: Partial< { [field in keyof S3ConnectionSource['s3']]: string; } >; - fileUpload?: { + [ErrorKeys.fileUpload]?: { file?: string; }; - schedule?: string; + [ErrorKeys.schedule]?: string; ioc_types?: string; } @@ -134,7 +140,7 @@ export const AddThreatIntelSource: React.FC = ({ const validateIocTypes = (iocTypeMap: Record) => { return !Object.values(iocTypeMap).some((val) => val) - ? 'At least one ioc type should be selected' + ? 'At least one ioc type should be selected.' : ''; }; const onIocTypesChange = (optionId: string) => { @@ -235,7 +241,7 @@ export const AddThreatIntelSource: React.FC = ({ setFieldError({ schedule: !schedule.period.interval || Number.isNaN(schedule.period.interval) - ? 'Invalid schedule' + ? 'Invalid schedule.' : '', }); }; @@ -275,12 +281,12 @@ export const AddThreatIntelSource: React.FC = ({ setFieldTouched({ fileUpload: { file: true } }); }; - const isThereAnError = (errors: any): boolean => { + const hasError = (errors: { [key: string]: any }): boolean => { for (let key of Object.keys(errors)) { if ( - (sourceType !== 'S3_CUSTOM' && key === 's3') || - (sourceType !== 'IOC_UPLOAD' && key === 'fileUpload') || - (!source.enabled && key === 'schedule') + (sourceType !== 'S3_CUSTOM' && key === ErrorKeys.s3) || + (sourceType !== 'IOC_UPLOAD' && key === ErrorKeys.fileUpload) || + (!source.enabled && key === ErrorKeys.schedule) ) { continue; } @@ -290,7 +296,7 @@ export const AddThreatIntelSource: React.FC = ({ } if (typeof errors[key] === 'object') { - if (isThereAnError(errors[key])) { + if (hasError(errors[key])) { return true; } } @@ -306,7 +312,7 @@ export const AddThreatIntelSource: React.FC = ({ ioc_types && ((sourceType === 'IOC_UPLOAD' && fileUpload?.file) || (sourceType === 'S3_CUSTOM' && s3 && Object.values(s3).every((val) => val))); - return reqFieldsTouched && !isThereAnError(inputErrors); + return reqFieldsTouched && !hasError(inputErrors); }; const onSubmit = () => { @@ -369,7 +375,7 @@ export const AddThreatIntelSource: React.FC = ({ @@ -580,7 +586,7 @@ export const AddThreatIntelSource: React.FC = ({

- Select atleast one IOC type to select from the{' '} + Select at least one IoC type to select from the{' '} {sourceType === 'IOC_UPLOAD' ? 'uploaded file' : 'S3 bucket'}.

diff --git a/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx b/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx index 6b69520b0..3f0531c2e 100644 --- a/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx +++ b/public/pages/ThreatIntel/containers/ScanConfiguration/ThreatIntelScanConfigForm.tsx @@ -180,7 +180,7 @@ export const ThreatIntelScanConfigForm: React.FC const triggersErrors: TriggerErrors[] = []; triggers.forEach((t, idx) => { const errors: TriggerErrors = {}; - errors.nameError = validateName(t.name) ? '' : 'Invalid trigger name'; + errors.nameError = validateName(t.name) ? '' : 'Invalid trigger name.'; errors.notificationChannelError = t.actions.every((a) => !!a.destination_id) ? '' : 'Select a channel';