diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 6e43bd645fd7b..c1749bf475172 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -255,6 +255,7 @@ export const severity_mapping_item = t.exact( severity, }) ); +export type SeverityMappingItem = t.TypeOf; export const severity_mapping = t.array(severity_mapping_item); export type SeverityMapping = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx index 8a6f049c96037..ed844b5130c77 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx @@ -17,6 +17,7 @@ interface OperatorProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + fieldTypeFilter?: string[]; fieldInputWidth?: number; onChange: (a: IFieldType[]) => void; } @@ -28,13 +29,22 @@ export const FieldComponent: React.FC = ({ isLoading = false, isDisabled = false, isClearable = false, + fieldTypeFilter = [], fieldInputWidth = 190, onChange, }): JSX.Element => { const getLabel = useCallback((field): string => field.name, []); - const optionsMemo = useMemo((): IFieldType[] => (indexPattern ? indexPattern.fields : []), [ - indexPattern, - ]); + const optionsMemo = useMemo((): IFieldType[] => { + if (indexPattern != null) { + if (fieldTypeFilter.length > 0) { + return indexPattern.fields.filter((f) => fieldTypeFilter.includes(f.type)); + } else { + return indexPattern.fields; + } + } else { + return []; + } + }, [fieldTypeFilter, indexPattern]); const selectedOptionsMemo = useMemo((): IFieldType[] => (selectedField ? [selectedField] : []), [ selectedField, ]); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index 7c884d773209a..cbbe43cc03568 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -24,6 +24,8 @@ describe('AlertsUtilityBar', () => { currentFilter="closed" selectAll={jest.fn()} showClearSelection={true} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateAlertsStatus={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index f99a0256c0b3f..9056c05beed3e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -37,6 +37,8 @@ describe('AlertsTableComponent', () => { clearEventsLoading={jest.fn()} setEventsDeleted={jest.fn()} clearEventsDeleted={jest.fn()} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateTimelineIsLoading={jest.fn()} updateTimeline={jest.fn()} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx new file mode 100644 index 0000000000000..0346511874104 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; + +interface AutocompleteFieldProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + indices: IIndexPattern; + isDisabled: boolean; + fieldType: string; + placeholder?: string; +} + +export const AutocompleteField = ({ + dataTestSubj, + field, + idAria, + indices, + isDisabled, + fieldType, + placeholder, +}: AutocompleteFieldProps) => { + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { + // TODO: Update onChange type in FieldComponent as newField can be undefined + field.setValue(newField?.name ?? ''); + }, + [field] + ); + + const selectedField = useMemo(() => { + const existingField = (field.value as string) ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const fieldTypeFilter = useMemo(() => [fieldType], [fieldType]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index bdf1ac600faef..334dde9abe7d8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -6,7 +6,6 @@ import { EuiFormRow, - EuiFieldText, EuiCheckbox, EuiText, EuiFlexGroup, @@ -15,12 +14,15 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; const NestedContent = styled.div` margin-left: 24px; @@ -38,20 +40,47 @@ interface RiskScoreFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; + placeholder?: string; } -export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskScoreFieldProps) => { +export const RiskScoreField = ({ + dataTestSubj, + field, + idAria, + indices, + placeholder, +}: RiskScoreFieldProps) => { const [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); - const updateRiskScoreMapping = useCallback( - (event) => { + const fieldTypeFilter = useMemo(() => ['number'], []); + + useEffect(() => { + if ( + !isRiskScoreMappingSelected && + initialFieldCheck && + (field.value as AboutStepRiskScore).mapping?.length > 0 + ) { + setIsRiskScoreMappingSelected(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isRiskScoreMappingSelected, + setIsRiskScoreMappingSelected, + setInitialFieldCheck, + ]); + + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { const values = field.value as AboutStepRiskScore; field.setValue({ value: values.value, mapping: [ { - field: event.target.value, + field: newField?.name ?? '', operator: 'equals', value: '', }, @@ -61,7 +90,19 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco [field] ); - const severityLabel = useMemo(() => { + const handleRiskScoreMappingSelected = useCallback(() => { + setIsRiskScoreMappingSelected(!isRiskScoreMappingSelected); + }, [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected]); + + const selectedField = useMemo(() => { + const existingField = (field.value as AboutStepRiskScore).mapping?.[0]?.field ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const riskScoreLabel = useMemo(() => { return (
@@ -73,19 +114,15 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco ); }, []); - const severityMappingLabel = useMemo(() => { + const riskScoreMappingLabel = useMemo(() => { return (
- setIsRiskScoreMappingSelected(!isRiskScoreMappingSelected)} - > + setIsRiskScoreMappingSelected(e.target.checked)} + onChange={handleRiskScoreMappingSelected} /> {i18n.RISK_SCORE_MAPPING} @@ -96,13 +133,13 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco
); - }, [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected]); + }, [handleRiskScoreMappingSelected, isRiskScoreMappingSelected]); return ( - diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 47c45a6bdf88d..cc884c1338f30 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -15,13 +15,14 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; const NestedContent = styled.div` margin-left: 24px; @@ -39,7 +40,7 @@ interface SeverityFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; options: SeverityOptionItem[]; } @@ -47,10 +48,28 @@ export const SeverityField = ({ dataTestSubj, field, idAria, - indices, // TODO: To be used with autocomplete fields once https://github.com/elastic/kibana/pull/67013 is merged + indices, options, }: SeverityFieldProps) => { const [isSeverityMappingChecked, setIsSeverityMappingChecked] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); + + useEffect(() => { + if ( + !isSeverityMappingChecked && + initialFieldCheck && + (field.value as AboutStepSeverity).mapping?.length > 0 + ) { + setIsSeverityMappingChecked(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isSeverityMappingChecked, + setIsSeverityMappingChecked, + setInitialFieldCheck, + ]); const updateSeverityMapping = useCallback( (index: number, severity: string, mappingField: string, event) => { @@ -168,7 +187,7 @@ export const SeverityField = ({ - {i18n.SEVERITY} + {i18n.DEFAULT_SEVERITY} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx index 9c9784bac6b63..f0bfc5f4637ab 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx @@ -13,6 +13,13 @@ export const SEVERITY = i18n.translate( } ); +export const DEFAULT_SEVERITY = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.defaultSeverityTitle', + { + defaultMessage: 'Severity', + } +); + export const SOURCE_FIELD = i18n.translate( 'xpack.securitySolution.alerts.severityMapping.sourceFieldTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 7f7ee94ed85b7..3616643874a0a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -38,6 +38,8 @@ import { MarkdownEditorForm } from '../../../../common/components/markdown_edito import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; +import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; +import { AutocompleteField } from '../autocomplete_field'; const CommonUseField = getUseField({ component: Field }); @@ -90,6 +92,9 @@ const StepAboutRuleComponent: FC = ({ setStepData, }) => { const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); + const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( + defineRuleData?.index ?? [] + ); const { form } = useForm({ defaultValue: myStepData, @@ -149,7 +154,6 @@ const StepAboutRuleComponent: FC = ({ }} /> - = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleSeverityField', idAria: 'detectionEngineStepAboutRuleSeverityField', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, options: severityOptions, - indices: defineRuleData?.index ?? [], + indices: indexPatterns, }} /> @@ -184,7 +188,8 @@ const StepAboutRuleComponent: FC = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleRiskScore', idAria: 'detectionEngineStepAboutRuleRiskScore', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, + indices: indexPatterns, }} /> @@ -196,7 +201,7 @@ const StepAboutRuleComponent: FC = ({ 'data-test-subj': 'detectionEngineStepAboutRuleTags', euiFieldProps: { fullWidth: true, - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, placeholder: '', }, }} @@ -277,7 +282,7 @@ const StepAboutRuleComponent: FC = ({ }} /> - + = ({ /> - - - + - - - + {({ severity }) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index c179128c56d92..3a5aa3c56c3df 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -26,6 +26,12 @@ export const ADD_FALSE_POSITIVE = i18n.translate( defaultMessage: 'Add false positive example', } ); +export const BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.buildingBlockLabel', + { + defaultMessage: 'Building block', + } +); export const LOW = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 777f7766993d0..fca45adcbb7d3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -154,12 +154,13 @@ const EditRulePageComponent: FC = () => { <> - {myAboutRuleForm.data != null && ( + {myAboutRuleForm.data != null && myDefineRuleForm.data != null && ( )} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 75c4d75cedf1d..218750ac30a2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -51,6 +51,7 @@ export const buildBulkBody = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index ed632ee2576dc..7257e5952ff05 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -5,7 +5,7 @@ */ import { buildRule } from './build_rule'; -import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; +import { sampleDocNoSortId, sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; @@ -29,6 +29,7 @@ describe('buildRule', () => { ]; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -97,6 +98,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -154,6 +156,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index fc8b26450c852..a24390a6e6928 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -8,6 +8,10 @@ import { pickBy } from 'lodash/fp'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams } from '../types'; +import { buildRiskScoreFromMapping } from './mappings/build_risk_score_from_mapping'; +import { SignalSourceHit } from './types'; +import { buildSeverityFromMapping } from './mappings/build_severity_from_mapping'; +import { buildRuleNameFromMapping } from './mappings/build_rule_name_from_mapping'; interface BuildRuleParams { ruleParams: RuleTypeParams; @@ -17,6 +21,7 @@ interface BuildRuleParams { enabled: boolean; createdAt: string; createdBy: string; + doc: SignalSourceHit; updatedAt: string; updatedBy: string; interval: string; @@ -32,12 +37,33 @@ export const buildRule = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, tags, throttle, }: BuildRuleParams): Partial => { + const { riskScore, riskScoreMeta } = buildRiskScoreFromMapping({ + doc, + riskScore: ruleParams.riskScore, + riskScoreMapping: ruleParams.riskScoreMapping, + }); + + const { severity, severityMeta } = buildSeverityFromMapping({ + doc, + severity: ruleParams.severity, + severityMapping: ruleParams.severityMapping, + }); + + const { ruleName, ruleNameMeta } = buildRuleNameFromMapping({ + doc, + ruleName: name, + ruleNameMapping: ruleParams.ruleNameOverride, + }); + + const meta = { ...ruleParams.meta, ...riskScoreMeta, ...severityMeta, ...ruleNameMeta }; + return pickBy((value: unknown) => value != null, { id, rule_id: ruleParams.ruleId ?? '(unknown rule_id)', @@ -48,9 +74,9 @@ export const buildRule = ({ saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, timeline_title: ruleParams.timelineTitle, - meta: ruleParams.meta, + meta: Object.keys(meta).length > 0 ? meta : undefined, max_signals: ruleParams.maxSignals, - risk_score: ruleParams.riskScore, // TODO: Risk Score Override via risk_score_mapping + risk_score: riskScore, risk_score_mapping: ruleParams.riskScoreMapping ?? [], output_index: ruleParams.outputIndex, description: ruleParams.description, @@ -61,11 +87,11 @@ export const buildRule = ({ interval, language: ruleParams.language, license: ruleParams.license, - name, // TODO: Rule Name Override via rule_name_override + name: ruleName, query: ruleParams.query, references: ruleParams.references, rule_name_override: ruleParams.ruleNameOverride, - severity: ruleParams.severity, // TODO: Severity Override via severity_mapping + severity, severity_mapping: ruleParams.severityMapping ?? [], tags, type: ruleParams.type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts new file mode 100644 index 0000000000000..e1d9c7f7c8a5c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRiskScoreFromMapping } from './build_risk_score_from_mapping'; + +describe('buildRiskScoreFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('risk score defaults to provided if mapping is incomplete', () => { + const riskScore = buildRiskScoreFromMapping({ + doc: sampleDocNoSortId(), + riskScore: 57, + riskScoreMapping: undefined, + }); + + expect(riskScore).toEqual({ riskScore: 57, riskScoreMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts new file mode 100644 index 0000000000000..356cf95fc0d24 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + RiskScore, + RiskScoreMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; +import { RiskScore as RiskScoreIOTS } from '../../../../../common/detection_engine/schemas/types'; + +interface BuildRiskScoreFromMappingProps { + doc: SignalSourceHit; + riskScore: RiskScore; + riskScoreMapping: RiskScoreMappingOrUndefined; +} + +interface BuildRiskScoreFromMappingReturn { + riskScore: RiskScore; + riskScoreMeta: Meta; // TODO: Stricter types +} + +export const buildRiskScoreFromMapping = ({ + doc, + riskScore, + riskScoreMapping, +}: BuildRiskScoreFromMappingProps): BuildRiskScoreFromMappingReturn => { + // MVP support is for mapping from a single field + if (riskScoreMapping != null && riskScoreMapping.length > 0) { + const mappedField = riskScoreMapping[0].field; + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mappedField, doc._source); + // TODO: This doesn't seem to validate...identified riskScore > 100 😬 + if (RiskScoreIOTS.is(mappedValue)) { + return { riskScore: mappedValue, riskScoreMeta: { riskScoreOverridden: true } }; + } + } + return { riskScore, riskScoreMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts new file mode 100644 index 0000000000000..b509020646d1b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRuleNameFromMapping } from './build_rule_name_from_mapping'; + +describe('buildRuleNameFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('rule name defaults to provided if mapping is incomplete', () => { + const ruleName = buildRuleNameFromMapping({ + doc: sampleDocNoSortId(), + ruleName: 'rule-name', + ruleNameMapping: 'message', + }); + + expect(ruleName).toEqual({ ruleName: 'rule-name', ruleNameMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts new file mode 100644 index 0000000000000..af540ed1454ad --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { get } from 'lodash/fp'; +import { + Meta, + Name, + RuleNameOverrideOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildRuleNameFromMappingProps { + doc: SignalSourceHit; + ruleName: Name; + ruleNameMapping: RuleNameOverrideOrUndefined; +} + +interface BuildRuleNameFromMappingReturn { + ruleName: Name; + ruleNameMeta: Meta; // TODO: Stricter types +} + +export const buildRuleNameFromMapping = ({ + doc, + ruleName, + ruleNameMapping, +}: BuildRuleNameFromMappingProps): BuildRuleNameFromMappingReturn => { + if (ruleNameMapping != null) { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(ruleNameMapping, doc._source); + if (t.string.is(mappedValue)) { + return { ruleName: mappedValue, ruleNameMeta: { ruleNameOverridden: true } }; + } + } + + return { ruleName, ruleNameMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts new file mode 100644 index 0000000000000..80950335934f4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildSeverityFromMapping } from './build_severity_from_mapping'; + +describe('buildSeverityFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('severity defaults to provided if mapping is incomplete', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocNoSortId(), + severity: 'low', + severityMapping: undefined, + }); + + expect(severity).toEqual({ severity: 'low', severityMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts new file mode 100644 index 0000000000000..a3c4f47b491be --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + Severity, + SeverityMappingItem, + severity as SeverityIOTS, + SeverityMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildSeverityFromMappingProps { + doc: SignalSourceHit; + severity: Severity; + severityMapping: SeverityMappingOrUndefined; +} + +interface BuildSeverityFromMappingReturn { + severity: Severity; + severityMeta: Meta; // TODO: Stricter types +} + +export const buildSeverityFromMapping = ({ + doc, + severity, + severityMapping, +}: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { + if (severityMapping != null && severityMapping.length > 0) { + let severityMatch: SeverityMappingItem | undefined; + severityMapping.forEach((mapping) => { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mapping.field, doc._source); + if (mapping.value === mappedValue) { + severityMatch = { ...mapping }; + } + }); + + if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { + return { + severity: severityMatch.severity, + severityMeta: { severityOverrideField: severityMatch.field }, + }; + } + } + return { severity, severityMeta: {} }; +};