Skip to content

Commit

Permalink
[Security Solution] FinalEdit: Add fields that are common for all r…
Browse files Browse the repository at this point in the history
…ule types (elastic#196642)

**Partially addresses: elastic#171520
**Is a follow-up to: elastic#196326

This PR enables editing of common fields in the new "Updates" tab of the rule upgrade flyout. The common fields are fields applicable to all rule types.

## Summary
These fields are editable now:
 - `building_block`
 - `description`
 - `false_positives`
 - `investigation_fields`
 - `max_signals`
 - `note`
 - `references`
 - `related_integrations`
 - `required_fields`
 - `risk_score`
 - `risk_score_mapping`
 - `rule_name_override`
 - `rule_schedule`
 - `setup`
 - `severity`
 - `severity_mapping`
 - `tags`
 - `threat`
 - `timeline_template`
 - `timestamp_override`

<img width="2672" alt="Scherm­afbeelding 2024-10-16 om 17 32 06" src="https://github.com/user-attachments/assets/6dd615e2-6e84-4e1f-b674-f42d03f575e7">

### Testing
 - Ensure the `prebuiltRulesCustomizationEnabled` feature flag is enabled.
 - To simulate the availability of prebuilt rule upgrades, downgrade a currently installed prebuilt rule using the `PATCH api/detection_engine/rules` API. 
   - Set `version: 1` in the request body to downgrade it to version 1.
   - Modify other rule fields in the request body as needed to test the changes.
  • Loading branch information
nikitaindik authored and CAWilson94 committed Nov 18, 2024
1 parent babee9a commit b61b77c
Show file tree
Hide file tree
Showing 69 changed files with 2,248 additions and 534 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-securitysolution-autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

export * from './src/check_empty_value';
export * from './src/field';
export * from './src/es_field_selector';
export * from './src/field_value_exists';
export * from './src/field_value_lists';
export * from './src/field_value_match';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import React from 'react';
import { fireEvent, render, waitFor, within } from '@testing-library/react';
import '@testing-library/jest-dom';

import { FieldComponent } from '..';
import { EsFieldSelector } from '..';
import { fields, getField } from '../../fields/index.mock';

describe('FieldComponent', () => {
it('should render the component enabled and displays the selected field correctly', () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={false}
isLoading={false}
Expand All @@ -38,7 +38,7 @@ describe('FieldComponent', () => {
});
it('should render the component disabled if isDisabled is true', () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={true}
isLoading={false}
Expand All @@ -57,7 +57,7 @@ describe('FieldComponent', () => {
});
it('should render the loading spinner if isLoading is true when clicked', () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={true}
isLoading={true}
Expand All @@ -78,7 +78,7 @@ describe('FieldComponent', () => {
});
it('should allow user to clear values if isClearable is true', () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
indexPattern={{
fields,
id: '1234',
Expand All @@ -97,7 +97,7 @@ describe('FieldComponent', () => {
});
it('should change the selected value', async () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={true}
isLoading={false}
Expand All @@ -119,7 +119,7 @@ describe('FieldComponent', () => {
it('it allows custom user input if "acceptsCustomOptions" is "true"', async () => {
const mockOnChange = jest.fn();
const wrapper = render(
<FieldComponent
<EsFieldSelector
indexPattern={{
fields,
id: '1234',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import TestRenderer from 'react-test-renderer';
const { act: actTestRenderer } = TestRenderer;

import { fields } from '../../fields/index.mock';
import { useField } from '../use_field';
import { useEsField } from '../use_es_field';

jest.mock('../../translations', () => ({
BINARY_TYPE_NOT_SUPPORTED: 'Binary fields are currently unsupported',
Expand All @@ -33,7 +33,7 @@ describe('useField', () => {

describe('comboOptions and selectedComboOptions', () => {
it('should return default values', () => {
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
const { isInvalid, comboOptions, selectedComboOptions, fieldWidth } = result.current;
expect(isInvalid).toBeFalsy();
expect(comboOptions.length).toEqual(30);
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('useField', () => {
};

const { result } = renderHook(() =>
useField({ indexPattern: newIndexPattern, onChange: onChangeMock })
useEsField({ indexPattern: newIndexPattern, onChange: onChangeMock })
);
const { comboOptions, selectedComboOptions } = result.current;
expect(comboOptions).toEqual([{ label: 'bytes' }, { label: 'ssl' }, { label: '@timestamp' }]);
Expand Down Expand Up @@ -124,7 +124,7 @@ describe('useField', () => {
};

const { result } = renderHook(() =>
useField({
useEsField({
indexPattern: newIndexPattern,
onChange: onChangeMock,
selectedField: { name: '', type: 'keyword' },
Expand Down Expand Up @@ -173,7 +173,7 @@ describe('useField', () => {
};

const { result } = renderHook(() =>
useField({
useEsField({
indexPattern: newIndexPattern,
onChange: onChangeMock,
selectedField: { name: ' ', type: 'keyword' },
Expand Down Expand Up @@ -222,7 +222,7 @@ describe('useField', () => {
};

const { result } = renderHook(() =>
useField({ indexPattern: newIndexPattern, onChange: onChangeMock, selectedField })
useEsField({ indexPattern: newIndexPattern, onChange: onChangeMock, selectedField })
);
const { comboOptions, selectedComboOptions } = result.current;
expect(comboOptions).toEqual([{ label: 'bytes' }, { label: 'ssl' }, { label: '@timestamp' }]);
Expand Down Expand Up @@ -273,7 +273,7 @@ describe('useField', () => {
readFromDocValues: true,
},
] as unknown as DataViewFieldBase[];
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
const { comboOptions, renderFields } = result.current;
expect(comboOptions).toEqual([
{ label: 'blob' },
Expand Down Expand Up @@ -328,7 +328,7 @@ describe('useField', () => {
readFromDocValues: true,
},
] as unknown as DataViewFieldBase[];
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
const { comboOptions, renderFields } = result.current;
expect(comboOptions).toEqual([
{ label: 'blob' },
Expand Down Expand Up @@ -374,7 +374,7 @@ describe('useField', () => {
readFromDocValues: true,
},
] as unknown as DataViewFieldBase[];
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
const { comboOptions, renderFields } = result.current;
expect(comboOptions).toEqual([{ label: 'bytes' }, { label: 'ssl' }, { label: '@timestamp' }]);
act(() => {
Expand All @@ -389,7 +389,7 @@ describe('useField', () => {
jest.resetModules();
});
it('should invoke onChange with one value if one option is sent', () => {
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
act(() => {
result.current.handleValuesChange([
{
Expand All @@ -411,7 +411,7 @@ describe('useField', () => {
});
});
it('should invoke onChange with array value if more than an option', () => {
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
act(() => {
result.current.handleValuesChange([
{
Expand Down Expand Up @@ -446,7 +446,7 @@ describe('useField', () => {
});
});
it('should invoke onChange with custom option if one is sent', () => {
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
act(() => {
result.current.handleCreateCustomOption('madeUpField');
expect(onChangeMock).toHaveBeenCalledWith([
Expand All @@ -462,13 +462,13 @@ describe('useField', () => {
describe('fieldWidth', () => {
it('should return object has width prop', () => {
const { result } = renderHook(() =>
useField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 100 })
useEsField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 100 })
);
expect(result.current.fieldWidth).toEqual({ width: '100px' });
});
it('should return empty object', () => {
const { result } = renderHook(() =>
useField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 0 })
useEsField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 0 })
);
expect(result.current.fieldWidth).toEqual({});
});
Expand All @@ -477,7 +477,7 @@ describe('useField', () => {
describe('isInvalid with handleTouch', () => {
it('should return isInvalid equals true when calling with no selectedField and isRequired is true', () => {
const { result } = renderHook(() =>
useField({ indexPattern, onChange: onChangeMock, isRequired: true })
useEsField({ indexPattern, onChange: onChangeMock, isRequired: true })
);

actTestRenderer(() => {
Expand All @@ -487,7 +487,7 @@ describe('useField', () => {
});
it('should return isInvalid equals false with selectedField and isRequired is true', () => {
const { result } = renderHook(() =>
useField({ indexPattern, onChange: onChangeMock, isRequired: true, selectedField })
useEsField({ indexPattern, onChange: onChangeMock, isRequired: true, selectedField })
);

actTestRenderer(() => {
Expand All @@ -496,7 +496,7 @@ describe('useField', () => {
expect(result.current.isInvalid).toBeFalsy();
});
it('should return isInvalid equals false when isRequired is false', () => {
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));

actTestRenderer(() => {
result.current.handleTouch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ import React from 'react';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { FieldProps } from './types';
import { useField } from './use_field';
import { FieldBaseProps } from './types';
import { useEsField } from './use_es_field';

const AS_PLAIN_TEXT = { asPlainText: true };

export const FieldComponent: React.FC<FieldProps> = ({
interface EsFieldSelectorProps extends FieldBaseProps {
isClearable?: boolean;
isDisabled?: boolean;
isLoading?: boolean;
placeholder: string;
acceptsCustomOptions?: boolean;
showMappingConflicts?: boolean;
'aria-label'?: string;
}

export function EsFieldSelector({
fieldInputWidth,
fieldTypeFilter = [],
indexPattern,
Expand All @@ -30,18 +40,17 @@ export const FieldComponent: React.FC<FieldProps> = ({
acceptsCustomOptions = false,
showMappingConflicts = false,
'aria-label': ariaLabel,
}): JSX.Element => {
}: EsFieldSelectorProps): JSX.Element {
const {
isInvalid,
comboOptions,
selectedComboOptions,
fieldWidth,

renderFields,
handleTouch,
handleValuesChange,
handleCreateCustomOption,
} = useField({
} = useEsField({
indexPattern,
fieldTypeFilter,
isRequired,
Expand Down Expand Up @@ -97,6 +106,4 @@ export const FieldComponent: React.FC<FieldProps> = ({
aria-label={ariaLabel}
/>
);
};

FieldComponent.displayName = 'Field';
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ import { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
import { FieldConflictsInfo } from '@kbn/securitysolution-list-utils';
import { GetGenericComboBoxPropsReturn } from '../get_generic_combo_box_props';

export interface FieldProps extends FieldBaseProps {
isClearable: boolean;
isDisabled: boolean;
isLoading: boolean;
placeholder: string;
acceptsCustomOptions?: boolean;
showMappingConflicts?: boolean;
'aria-label'?: string;
}
export interface FieldBaseProps {
indexPattern: DataViewBase | undefined;
fieldTypeFilter?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const getComboBoxProps = (fields: ComboBoxFields): GetFieldComboBoxPropsReturn =
};
};

export const useField = ({
export const useEsField = ({
indexPattern,
fieldTypeFilter,
isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ interface AutocompleteFieldMatchProps {
selectedField: DataViewFieldBase | undefined;
selectedValue: string | undefined;
indexPattern: DataViewBase | undefined;
isLoading: boolean;
isDisabled: boolean;
isClearable: boolean;
isLoading?: boolean;
isDisabled?: boolean;
isClearable?: boolean;
isRequired?: boolean;
fieldInputWidth?: number;
rowLabel?: string;
Expand All @@ -68,7 +68,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
selectedField,
selectedValue,
indexPattern,
isLoading,
isLoading = false,
isDisabled = false,
isClearable = false,
isRequired = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
AutocompleteFieldMatchAnyComponent,
AutocompleteFieldMatchComponent,
AutocompleteFieldWildcardComponent,
FieldComponent,
EsFieldSelector,
OperatorComponent,
} from '@kbn/securitysolution-autocomplete';
import {
Expand Down Expand Up @@ -207,7 +207,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
(isFirst: boolean): JSX.Element => {
const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry);
const comboBox = (
<FieldComponent
<EsFieldSelector
placeholder={
entry.nested != null
? i18n.EXCEPTION_FIELD_NESTED_PLACEHOLDER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';

import { FieldComponent } from '@kbn/securitysolution-autocomplete';
import { EsFieldSelector } from '@kbn/securitysolution-autocomplete';
import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
import type { FormattedEntry, Entry } from './types';
import * as i18n from './translations';
Expand Down Expand Up @@ -57,7 +57,7 @@ export const EntryItem: React.FC<EntryItemProps> = ({

const renderFieldInput = useMemo(() => {
const comboBox = (
<FieldComponent
<EsFieldSelector
placeholder={i18n.FIELD_PLACEHOLDER}
indexPattern={indexPattern}
selectedField={entry.field}
Expand Down Expand Up @@ -87,7 +87,7 @@ export const EntryItem: React.FC<EntryItemProps> = ({

const renderThreatFieldInput = useMemo(() => {
const comboBox = (
<FieldComponent
<EsFieldSelector
placeholder={i18n.FIELD_PLACEHOLDER}
indexPattern={threatIndexPatterns}
selectedField={entry.value}
Expand Down
Loading

0 comments on commit b61b77c

Please sign in to comment.