[Security Solution][Exceptions] - Make esTypes and subType available to index patterns (#72336)
## Summary This PR updates the following: - `useFetchIndexPatterns` now returns `indexPatterns` whose fields include `esTypes` and `subType` - Why?? The exceptions builder needs these two fields to determine what fields are of ES type `nested` and parent paths - exceptions add and edit modals now use the `rule.index` field to pass into `useFetchindexPatterns` - Before we were using the signals index and alerts index for endpoint, needs to be rule's index patterns - if no index patterns exist on the rule (if rule created via API, it's not required), then uses `DEFAULT_INDEX_PATTERN` - updates the autocomplete validation to use `IField.esTypes` to check type instead of `IField.type`
This commit is contained in:
parent
03fe8c3e89
commit
21977a7e6a
24 changed files with 362 additions and 169 deletions
|
@ -8,14 +8,27 @@ import { ThemeProvider } from 'styled-components';
|
|||
import { mount } from 'enzyme';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
|
||||
import { wait as waitFor } from '@testing-library/react';
|
||||
|
||||
import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts';
|
||||
import { AutocompleteFieldListsComponent } from './field_value_lists';
|
||||
import { ListSchema } from '../../../lists_plugin_deps';
|
||||
import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock';
|
||||
import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock';
|
||||
import { DATE_NOW } from '../../../../../lists/common/constants.mock';
|
||||
|
||||
import { AutocompleteFieldListsComponent } from './field_value_lists';
|
||||
|
||||
const mockStart = jest.fn();
|
||||
const mockResult = getFoundListSchemaMock();
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
const mockStart = jest.fn();
|
||||
const mockKeywordList: ListSchema = {
|
||||
...getListResponseMock(),
|
||||
id: 'keyword_list',
|
||||
type: 'keyword',
|
||||
name: 'keyword list',
|
||||
};
|
||||
const mockResult = { ...getFoundListSchemaMock() };
|
||||
mockResult.data = [...mockResult.data, mockKeywordList];
|
||||
jest.mock('../../../lists_plugin_deps', () => {
|
||||
const originalModule = jest.requireActual('../../../lists_plugin_deps');
|
||||
|
||||
|
@ -31,7 +44,7 @@ jest.mock('../../../lists_plugin_deps', () => {
|
|||
});
|
||||
|
||||
describe('AutocompleteFieldListsComponent', () => {
|
||||
test('it renders disabled if "isDisabled" is true', () => {
|
||||
test('it renders disabled if "isDisabled" is true', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<AutocompleteFieldListsComponent
|
||||
|
@ -46,14 +59,16 @@ describe('AutocompleteFieldListsComponent', () => {
|
|||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
|
||||
.prop('disabled')
|
||||
).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
|
||||
.prop('disabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders loading if "isLoading" is true', () => {
|
||||
test('it renders loading if "isLoading" is true', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<AutocompleteFieldListsComponent
|
||||
|
@ -67,20 +82,23 @@ describe('AutocompleteFieldListsComponent', () => {
|
|||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
wrapper
|
||||
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper
|
||||
.find(
|
||||
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
|
||||
)
|
||||
.prop('isLoading')
|
||||
).toBeTruthy();
|
||||
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(
|
||||
wrapper
|
||||
.find(
|
||||
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
|
||||
)
|
||||
.prop('isLoading')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('it allows user to clear values if "isClearable" is true', () => {
|
||||
test('it allows user to clear values if "isClearable" is true', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<AutocompleteFieldListsComponent
|
||||
|
@ -94,7 +112,6 @@ describe('AutocompleteFieldListsComponent', () => {
|
|||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="comboBoxInput"]`)
|
||||
|
@ -102,7 +119,55 @@ describe('AutocompleteFieldListsComponent', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it correctly displays selected list', () => {
|
||||
test('it correctly displays lists that match the selected "keyword" field esType', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<AutocompleteFieldListsComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('@tags')}
|
||||
selectedValue=""
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
|
||||
.prop('options')
|
||||
).toEqual([{ label: 'keyword list' }]);
|
||||
});
|
||||
|
||||
test('it correctly displays lists that match the selected "ip" field esType', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<AutocompleteFieldListsComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ip')}
|
||||
selectedValue=""
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
|
||||
.prop('options')
|
||||
).toEqual([{ label: 'some name' }]);
|
||||
});
|
||||
|
||||
test('it correctly displays selected list', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<AutocompleteFieldListsComponent
|
||||
|
@ -146,7 +211,7 @@ describe('AutocompleteFieldListsComponent', () => {
|
|||
}).onChange([{ label: 'some name' }]);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
created_at: '2020-04-20T15:25:31.830Z',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'some user',
|
||||
description: 'some description',
|
||||
id: 'some-list-id',
|
||||
|
@ -154,7 +219,7 @@ describe('AutocompleteFieldListsComponent', () => {
|
|||
name: 'some name',
|
||||
tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e',
|
||||
type: 'ip',
|
||||
updated_at: '2020-04-20T15:25:31.830Z',
|
||||
updated_at: DATE_NOW,
|
||||
updated_by: 'some user',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,8 +36,12 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
|
|||
const getLabel = useCallback(({ name }) => name, []);
|
||||
|
||||
const optionsMemo = useMemo(() => {
|
||||
if (selectedField != null) {
|
||||
return lists.filter(({ type }) => type === selectedField.type);
|
||||
if (
|
||||
selectedField != null &&
|
||||
selectedField.esTypes != null &&
|
||||
selectedField.esTypes.length > 0
|
||||
) {
|
||||
return lists.filter(({ type }) => selectedField.esTypes?.includes(type));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -79,10 +79,10 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
|||
});
|
||||
};
|
||||
|
||||
const isValid = useMemo(
|
||||
(): boolean => validateParams(selectedValue, selectedField ? selectedField.type : ''),
|
||||
[selectedField, selectedValue]
|
||||
);
|
||||
const isValid = useMemo((): boolean => validateParams(selectedValue, selectedField), [
|
||||
selectedField,
|
||||
selectedValue,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
|
|
|
@ -76,7 +76,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
|||
|
||||
const isValid = useMemo((): boolean => {
|
||||
const areAnyInvalid = selectedComboOptions.filter(
|
||||
({ label }) => !validateParams(label, selectedField ? selectedField.type : '')
|
||||
({ label }) => !validateParams(label, selectedField)
|
||||
);
|
||||
return areAnyInvalid.length === 0;
|
||||
}, [selectedComboOptions, selectedField]);
|
||||
|
|
|
@ -55,49 +55,25 @@ describe('helpers', () => {
|
|||
|
||||
describe('#validateParams', () => {
|
||||
test('returns true if value is undefined', () => {
|
||||
const isValid = validateParams(undefined, 'date');
|
||||
const isValid = validateParams(undefined, getField('@timestamp'));
|
||||
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns true if value is empty string', () => {
|
||||
const isValid = validateParams('', 'date');
|
||||
const isValid = validateParams('', getField('@timestamp'));
|
||||
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns true if type is "date" and value is valid', () => {
|
||||
const isValid = validateParams('1994-11-05T08:15:30-05:00', 'date');
|
||||
const isValid = validateParams('1994-11-05T08:15:30-05:00', getField('@timestamp'));
|
||||
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns false if type is "date" and value is not valid', () => {
|
||||
const isValid = validateParams('1593478826', 'date');
|
||||
|
||||
expect(isValid).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns true if type is "ip" and value is valid', () => {
|
||||
const isValid = validateParams('126.45.211.34', 'ip');
|
||||
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns false if type is "ip" and value is not valid', () => {
|
||||
const isValid = validateParams('hellooo', 'ip');
|
||||
|
||||
expect(isValid).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns true if type is "number" and value is valid', () => {
|
||||
const isValid = validateParams('123', 'number');
|
||||
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns false if type is "number" and value is not valid', () => {
|
||||
const isValid = validateParams('not a number', 'number');
|
||||
const isValid = validateParams('1593478826', getField('@timestamp'));
|
||||
|
||||
expect(isValid).toBeFalsy();
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import dateMath from '@elastic/datemath';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { IFieldType, Ipv4Address } from '../../../../../../../src/plugins/data/common';
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
|
||||
import {
|
||||
EXCEPTION_OPERATORS,
|
||||
|
@ -30,29 +30,27 @@ export const getOperators = (field: IFieldType | undefined): OperatorOption[] =>
|
|||
}
|
||||
};
|
||||
|
||||
export function validateParams(params: string | undefined, type: string) {
|
||||
export const validateParams = (
|
||||
params: string | undefined,
|
||||
field: IFieldType | undefined
|
||||
): boolean => {
|
||||
// Box would show error state if empty otherwise
|
||||
if (params == null || params === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'date':
|
||||
const moment = dateMath.parse(params);
|
||||
return Boolean(moment && moment.isValid());
|
||||
case 'ip':
|
||||
try {
|
||||
return Boolean(new Ipv4Address(params));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
case 'number':
|
||||
const val = parseFloat(params);
|
||||
return typeof val === 'number' && !isNaN(val);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const types = field != null && field.esTypes != null ? field.esTypes : [];
|
||||
|
||||
return types.reduce<boolean>((acc, type) => {
|
||||
switch (type) {
|
||||
case 'date':
|
||||
const moment = dateMath.parse(params);
|
||||
return Boolean(moment && moment.isValid());
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
}, true);
|
||||
};
|
||||
|
||||
export function getGenericComboBoxProps<T>({
|
||||
options,
|
||||
|
|
|
@ -22,7 +22,6 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { alertsIndexPattern } from '../../../../../common/endpoint/constants';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
|
@ -48,24 +47,18 @@ import {
|
|||
} from '../helpers';
|
||||
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
|
||||
|
||||
export interface AddExceptionOnClick {
|
||||
export interface AddExceptionModalBaseProps {
|
||||
ruleName: string;
|
||||
ruleId: string;
|
||||
exceptionListType: ExceptionListType;
|
||||
ruleIndices: string[];
|
||||
alertData?: {
|
||||
ecsData: Ecs;
|
||||
nonEcsData: TimelineNonEcsData[];
|
||||
};
|
||||
}
|
||||
|
||||
interface AddExceptionModalProps {
|
||||
ruleName: string;
|
||||
ruleId: string;
|
||||
exceptionListType: ExceptionListType;
|
||||
alertData?: {
|
||||
ecsData: Ecs;
|
||||
nonEcsData: TimelineNonEcsData[];
|
||||
};
|
||||
export interface AddExceptionModalProps extends AddExceptionModalBaseProps {
|
||||
onCancel: () => void;
|
||||
onConfirm: (didCloseAlert: boolean) => void;
|
||||
alertStatus?: Status;
|
||||
|
@ -78,10 +71,8 @@ const Modal = styled(EuiModal)`
|
|||
`;
|
||||
|
||||
const ModalHeader = styled(EuiModalHeader)`
|
||||
${({ theme }) => css`
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`}
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
const ModalHeaderSubtitle = styled.div`
|
||||
|
@ -103,6 +94,7 @@ const ModalBodySection = styled.section`
|
|||
export const AddExceptionModal = memo(function AddExceptionModal({
|
||||
ruleName,
|
||||
ruleId,
|
||||
ruleIndices,
|
||||
exceptionListType,
|
||||
alertData,
|
||||
onCancel,
|
||||
|
@ -120,10 +112,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false);
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
|
||||
const [
|
||||
{ isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns },
|
||||
] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []);
|
||||
|
||||
const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns(
|
||||
signalIndexName !== null ? [signalIndexName] : []
|
||||
);
|
||||
const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices);
|
||||
|
||||
const onError = useCallback(
|
||||
(error: Error) => {
|
||||
|
@ -183,19 +176,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
}, [alertData, exceptionListType, ruleExceptionList, ruleName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (indexPatternLoading === false && isSignalIndexLoading === false) {
|
||||
if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) {
|
||||
setShouldDisableBulkClose(
|
||||
entryHasListType(exceptionItemsToAdd) ||
|
||||
entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) ||
|
||||
entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) ||
|
||||
exceptionItemsToAdd.length === 0
|
||||
);
|
||||
}
|
||||
}, [
|
||||
setShouldDisableBulkClose,
|
||||
exceptionItemsToAdd,
|
||||
indexPatternLoading,
|
||||
isSignalIndexPatternLoading,
|
||||
isSignalIndexLoading,
|
||||
indexPatterns,
|
||||
signalIndexPatterns,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -274,15 +267,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
[fetchOrCreateListError, exceptionItemsToAdd]
|
||||
);
|
||||
|
||||
const indexPatternConfig = useCallback(() => {
|
||||
if (exceptionListType === 'endpoint') {
|
||||
return [alertsIndexPattern];
|
||||
}
|
||||
return signalIndexName ? [signalIndexName] : [];
|
||||
}, [exceptionListType, signalIndexName]);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiOverlayMask onClick={onCancel}>
|
||||
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
|
||||
<ModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.ADD_EXCEPTION}</EuiModalHeaderTitle>
|
||||
|
@ -301,8 +287,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
)}
|
||||
{fetchOrCreateListError === false &&
|
||||
!isSignalIndexLoading &&
|
||||
!indexPatternLoading &&
|
||||
!isSignalIndexPatternLoading &&
|
||||
!isLoadingExceptionList &&
|
||||
!isIndexPatternLoading &&
|
||||
ruleExceptionList && (
|
||||
<>
|
||||
<ModalBodySection className="builder-section">
|
||||
|
@ -314,8 +301,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
listId={ruleExceptionList.list_id}
|
||||
listNamespaceType={ruleExceptionList.namespace_type}
|
||||
ruleName={ruleName}
|
||||
indexPatternConfig={indexPatternConfig()}
|
||||
isLoading={false}
|
||||
indexPatterns={indexPatterns}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
data-test-subj="alert-exception-builder"
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
* 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, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ExceptionListItemComponent } from './builder_exception_item';
|
||||
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules/fetch_index_patterns';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
NamespaceType,
|
||||
|
@ -22,7 +22,6 @@ import { AndOrBadge } from '../../and_or_badge';
|
|||
import { BuilderButtonOptions } from './builder_button_options';
|
||||
import { getNewExceptionItem, filterExceptionItems } from '../helpers';
|
||||
import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types';
|
||||
import { Loader } from '../../loader';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import exceptionableFields from '../exceptionable_fields.json';
|
||||
|
||||
|
@ -51,8 +50,7 @@ interface ExceptionBuilderProps {
|
|||
listId: string;
|
||||
listNamespaceType: NamespaceType;
|
||||
ruleName: string;
|
||||
indexPatternConfig: string[];
|
||||
isLoading: boolean;
|
||||
indexPatterns: IIndexPattern;
|
||||
isOrDisabled: boolean;
|
||||
isAndDisabled: boolean;
|
||||
onChange: (arg: OnChangeProps) => void;
|
||||
|
@ -64,8 +62,7 @@ export const ExceptionBuilder = ({
|
|||
listId,
|
||||
listNamespaceType,
|
||||
ruleName,
|
||||
indexPatternConfig,
|
||||
isLoading,
|
||||
indexPatterns,
|
||||
isOrDisabled,
|
||||
isAndDisabled,
|
||||
onChange,
|
||||
|
@ -75,9 +72,6 @@ export const ExceptionBuilder = ({
|
|||
exceptionListItems
|
||||
);
|
||||
const [exceptionsToDelete, setExceptionsToDelete] = useState<ExceptionListItemSchema[]>([]);
|
||||
const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns(
|
||||
indexPatternConfig ?? []
|
||||
);
|
||||
|
||||
const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => {
|
||||
setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0);
|
||||
|
@ -154,7 +148,7 @@ export const ExceptionBuilder = ({
|
|||
}, [setExceptions, listType, listId, listNamespaceType, ruleName]);
|
||||
|
||||
// Filters index pattern fields by exceptionable fields if list type is endpoint
|
||||
const filterIndexPatterns = useCallback(() => {
|
||||
const filterIndexPatterns = useMemo((): IIndexPattern => {
|
||||
if (listType === 'endpoint') {
|
||||
return {
|
||||
...indexPatterns,
|
||||
|
@ -196,9 +190,6 @@ export const ExceptionBuilder = ({
|
|||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
{(isLoading || indexPatternLoading) && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{exceptions.map((exceptionListItem, index) => (
|
||||
<EuiFlexItem grow={1} key={getExceptionListItemId(exceptionListItem, index)}>
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
|
@ -224,8 +215,8 @@ export const ExceptionBuilder = ({
|
|||
key={getExceptionListItemId(exceptionListItem, index)}
|
||||
exceptionItem={exceptionListItem}
|
||||
exceptionId={getExceptionListItemId(exceptionListItem, index)}
|
||||
indexPattern={filterIndexPatterns()}
|
||||
isLoading={indexPatternLoading}
|
||||
indexPattern={filterIndexPatterns}
|
||||
isLoading={indexPatterns.fields.length === 0}
|
||||
exceptionItemIndex={index}
|
||||
andLogicIncluded={andLogicIncluded}
|
||||
isOnlyItem={exceptions.length === 1}
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
EuiFormRow,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { alertsIndexPattern } from '../../../../../common/endpoint/constants';
|
||||
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
|
||||
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import {
|
||||
|
@ -45,6 +44,7 @@ import { Loader } from '../../loader';
|
|||
|
||||
interface EditExceptionModalProps {
|
||||
ruleName: string;
|
||||
ruleIndices: string[];
|
||||
exceptionItem: ExceptionListItemSchema;
|
||||
exceptionListType: ExceptionListType;
|
||||
onCancel: () => void;
|
||||
|
@ -58,10 +58,8 @@ const Modal = styled(EuiModal)`
|
|||
`;
|
||||
|
||||
const ModalHeader = styled(EuiModalHeader)`
|
||||
${({ theme }) => css`
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`}
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
const ModalHeaderSubtitle = styled.div`
|
||||
|
@ -82,6 +80,7 @@ const ModalBodySection = styled.section`
|
|||
|
||||
export const EditExceptionModal = memo(function EditExceptionModal({
|
||||
ruleName,
|
||||
ruleIndices,
|
||||
exceptionItem,
|
||||
exceptionListType,
|
||||
onCancel,
|
||||
|
@ -96,10 +95,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
>([]);
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
|
||||
const [
|
||||
{ isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns },
|
||||
] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []);
|
||||
|
||||
const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns(
|
||||
signalIndexName !== null ? [signalIndexName] : []
|
||||
);
|
||||
const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices);
|
||||
|
||||
const onError = useCallback(
|
||||
(error) => {
|
||||
|
@ -122,19 +122,19 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (indexPatternLoading === false && isSignalIndexLoading === false) {
|
||||
if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) {
|
||||
setShouldDisableBulkClose(
|
||||
entryHasListType(exceptionItemsToAdd) ||
|
||||
entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) ||
|
||||
entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) ||
|
||||
exceptionItemsToAdd.length === 0
|
||||
);
|
||||
}
|
||||
}, [
|
||||
setShouldDisableBulkClose,
|
||||
exceptionItemsToAdd,
|
||||
indexPatternLoading,
|
||||
isSignalIndexPatternLoading,
|
||||
isSignalIndexLoading,
|
||||
indexPatterns,
|
||||
signalIndexPatterns,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -189,15 +189,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
}
|
||||
}, [addOrUpdateExceptionItems, enrichExceptionItems, shouldBulkCloseAlert, signalIndexName]);
|
||||
|
||||
const indexPatternConfig = useCallback(() => {
|
||||
if (exceptionListType === 'endpoint') {
|
||||
return [alertsIndexPattern];
|
||||
}
|
||||
return signalIndexName ? [signalIndexName] : [];
|
||||
}, [exceptionListType, signalIndexName]);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiOverlayMask onClick={onCancel}>
|
||||
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
|
||||
<ModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.EDIT_EXCEPTION_TITLE}</EuiModalHeaderTitle>
|
||||
|
@ -206,11 +199,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
</ModalHeaderSubtitle>
|
||||
</ModalHeader>
|
||||
|
||||
{(addExceptionIsLoading || indexPatternLoading || isSignalIndexLoading) && (
|
||||
{(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && (
|
||||
<Loader data-test-subj="loadingEditExceptionModal" size="xl" />
|
||||
)}
|
||||
|
||||
{!isSignalIndexLoading && !addExceptionIsLoading && !indexPatternLoading && (
|
||||
{!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && (
|
||||
<>
|
||||
<ModalBodySection className="builder-section">
|
||||
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
|
||||
|
@ -221,13 +214,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
listId={exceptionItem.list_id}
|
||||
listNamespaceType={exceptionItem.namespace_type}
|
||||
ruleName={ruleName}
|
||||
isLoading={false}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
data-test-subj="edit-exception-modal-builder"
|
||||
id-aria="edit-exception-modal-builder"
|
||||
onChange={handleBuilderOnChange}
|
||||
indexPatternConfig={indexPatternConfig()}
|
||||
indexPatterns={indexPatterns}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
|
|
@ -67,6 +67,7 @@ describe('ExceptionsViewer', () => {
|
|||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionsViewer
|
||||
ruleId={'123'}
|
||||
ruleIndices={['filebeat-*']}
|
||||
ruleName={ruleName}
|
||||
exceptionListsMeta={[
|
||||
{
|
||||
|
@ -88,6 +89,7 @@ describe('ExceptionsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionsViewer
|
||||
ruleIndices={['filebeat-*']}
|
||||
ruleId={'123'}
|
||||
ruleName={ruleName}
|
||||
exceptionListsMeta={[]}
|
||||
|
@ -116,6 +118,7 @@ describe('ExceptionsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionsViewer
|
||||
ruleIndices={['filebeat-*']}
|
||||
ruleId={'123'}
|
||||
ruleName={ruleName}
|
||||
exceptionListsMeta={[
|
||||
|
|
|
@ -53,6 +53,7 @@ const initialState: State = {
|
|||
interface ExceptionsViewerProps {
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
ruleIndices: string[];
|
||||
exceptionListsMeta: ExceptionIdentifiers[];
|
||||
availableListTypes: ExceptionListTypeEnum[];
|
||||
commentsAccordionId: string;
|
||||
|
@ -61,6 +62,7 @@ interface ExceptionsViewerProps {
|
|||
const ExceptionsViewerComponent = ({
|
||||
ruleId,
|
||||
ruleName,
|
||||
ruleIndices,
|
||||
exceptionListsMeta,
|
||||
availableListTypes,
|
||||
commentsAccordionId,
|
||||
|
@ -104,7 +106,7 @@ const ExceptionsViewerComponent = ({
|
|||
lists: newLists,
|
||||
exceptions: newExceptions,
|
||||
pagination: newPagination,
|
||||
}: UseExceptionListSuccess) => {
|
||||
}: UseExceptionListSuccess): void => {
|
||||
dispatch({
|
||||
type: 'setExceptions',
|
||||
lists: newLists,
|
||||
|
@ -253,10 +255,11 @@ const ExceptionsViewerComponent = ({
|
|||
return (
|
||||
<>
|
||||
{currentModal === 'editModal' &&
|
||||
exceptionToEdit !== null &&
|
||||
exceptionListTypeToEdit !== null && (
|
||||
exceptionToEdit != null &&
|
||||
exceptionListTypeToEdit != null && (
|
||||
<EditExceptionModal
|
||||
ruleName={ruleName}
|
||||
ruleIndices={ruleIndices}
|
||||
exceptionListType={exceptionListTypeToEdit}
|
||||
exceptionItem={exceptionToEdit}
|
||||
onCancel={handleOnCancelExceptionModal}
|
||||
|
@ -267,6 +270,7 @@ const ExceptionsViewerComponent = ({
|
|||
{currentModal === 'addModal' && exceptionListTypeToEdit != null && (
|
||||
<AddExceptionModal
|
||||
ruleName={ruleName}
|
||||
ruleIndices={ruleIndices}
|
||||
ruleId={ruleId}
|
||||
exceptionListType={exceptionListTypeToEdit}
|
||||
onCancel={handleOnCancelExceptionModal}
|
||||
|
|
|
@ -22,6 +22,8 @@ export const sourceQuery = gql`
|
|||
type
|
||||
aggregatable
|
||||
format
|
||||
esTypes
|
||||
subType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export const getIndexFields = memoizeOne(
|
|||
fields && fields.length > 0
|
||||
? {
|
||||
fields: fields.map((field) =>
|
||||
pick(['name', 'searchable', 'type', 'aggregatable'], field)
|
||||
pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field)
|
||||
),
|
||||
title,
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Dispatch } from 'redux';
|
|||
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { RowRendererId } from '../../../../common/types/timeline';
|
||||
import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants';
|
||||
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { Filter } from '../../../../../../../src/plugins/data/common/es_query';
|
||||
import {
|
||||
|
@ -38,7 +39,7 @@ import {
|
|||
UpdateTimelineLoading,
|
||||
} from './types';
|
||||
import { Ecs, TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { AddExceptionOnClick } from '../../../common/components/exceptions/add_exception_modal';
|
||||
import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal';
|
||||
import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers';
|
||||
|
||||
export const buildAlertStatusFilter = (status: Status): Filter[] => [
|
||||
|
@ -225,7 +226,7 @@ interface AlertActionArgs {
|
|||
alertData,
|
||||
ruleName,
|
||||
ruleId,
|
||||
}: AddExceptionOnClick) => void;
|
||||
}: AddExceptionModalBaseProps) => void;
|
||||
}
|
||||
|
||||
export const getAlertActions = ({
|
||||
|
@ -346,10 +347,12 @@ export const getAlertActions = ({
|
|||
onClick: ({ ecsData, data }: TimelineRowActionOnClick) => {
|
||||
const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
|
||||
const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
|
||||
const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' });
|
||||
if (ruleId !== undefined) {
|
||||
openAddExceptionModal({
|
||||
ruleName: ruleName ?? '',
|
||||
ruleId,
|
||||
ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN,
|
||||
exceptionListType: 'endpoint',
|
||||
alertData: {
|
||||
ecsData,
|
||||
|
@ -369,10 +372,12 @@ export const getAlertActions = ({
|
|||
onClick: ({ ecsData, data }: TimelineRowActionOnClick) => {
|
||||
const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
|
||||
const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
|
||||
const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' });
|
||||
if (ruleId !== undefined) {
|
||||
openAddExceptionModal({
|
||||
ruleName: ruleName ?? '',
|
||||
ruleId,
|
||||
ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN,
|
||||
exceptionListType: 'detection',
|
||||
alertData: {
|
||||
ecsData,
|
||||
|
|
|
@ -54,7 +54,7 @@ import {
|
|||
import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
|
||||
import {
|
||||
AddExceptionModal,
|
||||
AddExceptionOnClick,
|
||||
AddExceptionModalBaseProps,
|
||||
} from '../../../common/components/exceptions/add_exception_modal';
|
||||
|
||||
interface OwnProps {
|
||||
|
@ -73,9 +73,10 @@ interface OwnProps {
|
|||
|
||||
type AlertsTableComponentProps = OwnProps & PropsFromRedux;
|
||||
|
||||
const addExceptionModalInitialState: AddExceptionOnClick = {
|
||||
const addExceptionModalInitialState: AddExceptionModalBaseProps = {
|
||||
ruleName: '',
|
||||
ruleId: '',
|
||||
ruleIndices: [],
|
||||
exceptionListType: 'detection',
|
||||
alertData: undefined,
|
||||
};
|
||||
|
@ -112,7 +113,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
|
||||
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
|
||||
const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false);
|
||||
const [addExceptionModalState, setAddExceptionModalState] = useState<AddExceptionOnClick>(
|
||||
const [addExceptionModalState, setAddExceptionModalState] = useState<AddExceptionModalBaseProps>(
|
||||
addExceptionModalInitialState
|
||||
);
|
||||
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns(
|
||||
|
@ -216,12 +217,19 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
);
|
||||
|
||||
const openAddExceptionModalCallback = useCallback(
|
||||
({ ruleName, ruleId, exceptionListType, alertData }: AddExceptionOnClick) => {
|
||||
({
|
||||
ruleName,
|
||||
ruleIndices,
|
||||
ruleId,
|
||||
exceptionListType,
|
||||
alertData,
|
||||
}: AddExceptionModalBaseProps) => {
|
||||
if (alertData !== null && alertData !== undefined) {
|
||||
setShouldShowAddExceptionModal(true);
|
||||
setAddExceptionModalState({
|
||||
ruleName,
|
||||
ruleId,
|
||||
ruleIndices,
|
||||
exceptionListType,
|
||||
alertData,
|
||||
});
|
||||
|
@ -421,12 +429,9 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
closeAddExceptionModal();
|
||||
}, [closeAddExceptionModal]);
|
||||
|
||||
const onAddExceptionConfirm = useCallback(
|
||||
(didCloseAlert: boolean) => {
|
||||
closeAddExceptionModal();
|
||||
},
|
||||
[closeAddExceptionModal]
|
||||
);
|
||||
const onAddExceptionConfirm = useCallback(() => closeAddExceptionModal(), [
|
||||
closeAddExceptionModal,
|
||||
]);
|
||||
|
||||
if (loading || isEmpty(signalsIndex)) {
|
||||
return (
|
||||
|
@ -454,6 +459,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
<AddExceptionModal
|
||||
ruleName={addExceptionModalState.ruleName}
|
||||
ruleId={addExceptionModalState.ruleId}
|
||||
ruleIndices={addExceptionModalState.ruleIndices}
|
||||
exceptionListType={addExceptionModalState.exceptionListType}
|
||||
alertData={addExceptionModalState.alertData}
|
||||
onCancel={onAddExceptionCancel}
|
||||
|
|
|
@ -81,7 +81,7 @@ import { SecurityPageName } from '../../../../../app/types';
|
|||
import { LinkButton } from '../../../../../common/components/links';
|
||||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||
import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer';
|
||||
import { FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants';
|
||||
import { DEFAULT_INDEX_PATTERN, FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants';
|
||||
import { useFullScreen } from '../../../../../common/containers/use_full_screen';
|
||||
import { Display } from '../../../../../hosts/pages/display';
|
||||
import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../lists_plugin_deps';
|
||||
|
@ -515,6 +515,7 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
|
|||
<ExceptionsViewer
|
||||
ruleId={ruleId ?? ''}
|
||||
ruleName={rule?.name ?? ''}
|
||||
ruleIndices={rule?.index ?? DEFAULT_INDEX_PATTERN}
|
||||
availableListTypes={exceptionLists.allowedExceptionListTypes}
|
||||
commentsAccordionId={'ruleDetailsTabExceptions'}
|
||||
exceptionListsMeta={exceptionLists.lists}
|
||||
|
|
|
@ -2669,6 +2669,22 @@
|
|||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "esTypes",
|
||||
"description": "the elastic type as mapped in the index",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "ToStringArrayNoNullable", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "subType",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "ToIFieldSubTypeNonNullable", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
@ -2676,6 +2692,26 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "SCALAR",
|
||||
"name": "ToStringArrayNoNullable",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "SCALAR",
|
||||
"name": "ToIFieldSubTypeNonNullable",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "TimerangeInput",
|
||||
|
|
|
@ -428,6 +428,10 @@ export enum FlowDirection {
|
|||
biDirectional = 'biDirectional',
|
||||
}
|
||||
|
||||
export type ToStringArrayNoNullable = any;
|
||||
|
||||
export type ToIFieldSubTypeNonNullable = any;
|
||||
|
||||
export type ToStringArray = string[];
|
||||
|
||||
export type Date = string;
|
||||
|
@ -627,6 +631,10 @@ export interface IndexField {
|
|||
description?: Maybe<string>;
|
||||
|
||||
format?: Maybe<string>;
|
||||
/** the elastic type as mapped in the index */
|
||||
esTypes?: Maybe<ToStringArrayNoNullable>;
|
||||
|
||||
subType?: Maybe<ToIFieldSubTypeNonNullable>;
|
||||
}
|
||||
|
||||
export interface AuthenticationsData {
|
||||
|
@ -2780,6 +2788,10 @@ export namespace SourceQuery {
|
|||
aggregatable: boolean;
|
||||
|
||||
format: Maybe<string>;
|
||||
|
||||
esTypes: Maybe<ToStringArrayNoNullable>;
|
||||
|
||||
subType: Maybe<ToIFieldSubTypeNonNullable>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -47,9 +47,41 @@ export const toStringArrayScalar = new GraphQLScalarType({
|
|||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const toStringArrayNoNullableScalar = new GraphQLScalarType({
|
||||
name: 'StringArray',
|
||||
description: 'Represents value in detail item from the timeline who wants to more than one type',
|
||||
serialize(value): string[] | undefined {
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
} else if (Array.isArray(value)) {
|
||||
return convertArrayToString(value) as string[];
|
||||
} else if (isBoolean(value) || isNumber(value) || isObject(value)) {
|
||||
return [convertToString(value)];
|
||||
}
|
||||
return [value];
|
||||
},
|
||||
parseValue(value) {
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
switch (ast.kind) {
|
||||
case Kind.INT:
|
||||
return parseInt(ast.value, 10);
|
||||
case Kind.FLOAT:
|
||||
return parseFloat(ast.value);
|
||||
case Kind.STRING:
|
||||
return ast.value;
|
||||
case Kind.LIST:
|
||||
return ast.values;
|
||||
case Kind.OBJECT:
|
||||
return ast.fields;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
export const createScalarToStringArrayValueResolvers = () => ({
|
||||
ToStringArray: toStringArrayScalar,
|
||||
ToStringArrayNoNullable: toStringArrayNoNullableScalar,
|
||||
});
|
||||
|
||||
const convertToString = (value: object | number | boolean | string): string => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import gql from 'graphql-tag';
|
|||
|
||||
export const ecsSchema = gql`
|
||||
scalar ToStringArray
|
||||
scalar ToStringArrayNoNullable
|
||||
|
||||
type EventEcsFields {
|
||||
action: ToStringArray
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { GraphQLScalarType, Kind } from 'graphql';
|
||||
import { SourceStatusResolvers } from '../../graphql/types';
|
||||
import { AppResolverOf, ChildResolverOf } from '../../lib/framework';
|
||||
import { IndexFields } from '../../lib/index_fields';
|
||||
import { SourceStatus } from '../../lib/source_status';
|
||||
import { QuerySourceResolver } from '../sources/resolvers';
|
||||
import { IFieldSubType } from '../../../../../../src/plugins/data/common/index_patterns/types';
|
||||
|
||||
export type SourceStatusIndicesExistResolver = ChildResolverOf<
|
||||
AppResolverOf<SourceStatusResolvers.IndicesExistResolver>,
|
||||
|
@ -50,3 +52,40 @@ export const createSourceStatusResolvers = (libs: {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({
|
||||
name: 'IFieldSubType',
|
||||
description: 'Represents value in index pattern field item',
|
||||
serialize(value): IFieldSubType | undefined {
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
multi: value.multi ?? undefined,
|
||||
nested: value.nested ?? undefined,
|
||||
};
|
||||
},
|
||||
parseValue(value) {
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
switch (ast.kind) {
|
||||
case Kind.INT:
|
||||
return undefined;
|
||||
case Kind.FLOAT:
|
||||
return undefined;
|
||||
case Kind.STRING:
|
||||
return undefined;
|
||||
case Kind.LIST:
|
||||
return undefined;
|
||||
case Kind.OBJECT:
|
||||
return ast;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export const createScalarToIFieldSubTypeNonNullableScalarResolvers = () => ({
|
||||
ToIFieldSubTypeNonNullable: toIFieldSubTypeNonNullableScalar,
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export const sourceStatusSchema = gql`
|
||||
scalar ToIFieldSubTypeNonNullable
|
||||
|
||||
"A descriptor of a field in an index"
|
||||
type IndexField {
|
||||
"Where the field belong"
|
||||
|
@ -26,6 +28,9 @@ export const sourceStatusSchema = gql`
|
|||
"Description of the field"
|
||||
description: String
|
||||
format: String
|
||||
"the elastic type as mapped in the index"
|
||||
esTypes: ToStringArrayNoNullable
|
||||
subType: ToIFieldSubTypeNonNullable
|
||||
}
|
||||
|
||||
extend type SourceStatus {
|
||||
|
|
|
@ -430,6 +430,10 @@ export enum FlowDirection {
|
|||
biDirectional = 'biDirectional',
|
||||
}
|
||||
|
||||
export type ToStringArrayNoNullable = any;
|
||||
|
||||
export type ToIFieldSubTypeNonNullable = any;
|
||||
|
||||
export type ToStringArray = string[] | string;
|
||||
|
||||
export type Date = string;
|
||||
|
@ -629,6 +633,10 @@ export interface IndexField {
|
|||
description?: Maybe<string>;
|
||||
|
||||
format?: Maybe<string>;
|
||||
/** the elastic type as mapped in the index */
|
||||
esTypes?: Maybe<ToStringArrayNoNullable>;
|
||||
|
||||
subType?: Maybe<ToIFieldSubTypeNonNullable>;
|
||||
}
|
||||
|
||||
export interface AuthenticationsData {
|
||||
|
@ -3579,6 +3587,10 @@ export namespace IndexFieldResolvers {
|
|||
description?: DescriptionResolver<Maybe<string>, TypeParent, TContext>;
|
||||
|
||||
format?: FormatResolver<Maybe<string>, TypeParent, TContext>;
|
||||
/** the elastic type as mapped in the index */
|
||||
esTypes?: EsTypesResolver<Maybe<ToStringArrayNoNullable>, TypeParent, TContext>;
|
||||
|
||||
subType?: SubTypeResolver<Maybe<ToIFieldSubTypeNonNullable>, TypeParent, TContext>;
|
||||
}
|
||||
|
||||
export type CategoryResolver<R = string, Parent = IndexField, TContext = SiemContext> = Resolver<
|
||||
|
@ -3626,6 +3638,16 @@ export namespace IndexFieldResolvers {
|
|||
Parent = IndexField,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type EsTypesResolver<
|
||||
R = Maybe<ToStringArrayNoNullable>,
|
||||
Parent = IndexField,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type SubTypeResolver<
|
||||
R = Maybe<ToIFieldSubTypeNonNullable>,
|
||||
Parent = IndexField,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
}
|
||||
|
||||
export namespace AuthenticationsDataResolvers {
|
||||
|
@ -9317,6 +9339,14 @@ export interface DeprecatedDirectiveArgs {
|
|||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ToStringArrayNoNullableScalarConfig
|
||||
extends GraphQLScalarTypeConfig<ToStringArrayNoNullable, any> {
|
||||
name: 'ToStringArrayNoNullable';
|
||||
}
|
||||
export interface ToIFieldSubTypeNonNullableScalarConfig
|
||||
extends GraphQLScalarTypeConfig<ToIFieldSubTypeNonNullable, any> {
|
||||
name: 'ToIFieldSubTypeNonNullable';
|
||||
}
|
||||
export interface ToStringArrayScalarConfig extends GraphQLScalarTypeConfig<ToStringArray, any> {
|
||||
name: 'ToStringArray';
|
||||
}
|
||||
|
@ -9490,6 +9520,8 @@ export type IResolvers<TContext = SiemContext> = {
|
|||
EventsTimelineData?: EventsTimelineDataResolvers.Resolvers<TContext>;
|
||||
OsFields?: OsFieldsResolvers.Resolvers<TContext>;
|
||||
HostFields?: HostFieldsResolvers.Resolvers<TContext>;
|
||||
ToStringArrayNoNullable?: GraphQLScalarType;
|
||||
ToIFieldSubTypeNonNullable?: GraphQLScalarType;
|
||||
ToStringArray?: GraphQLScalarType;
|
||||
Date?: GraphQLScalarType;
|
||||
ToNumberArray?: GraphQLScalarType;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { IndexField } from '../../graphql/types';
|
||||
import { FrameworkRequest } from '../framework';
|
||||
import { IFieldSubType } from '../../../../../../src/plugins/data/common';
|
||||
|
||||
export interface FieldsAdapter {
|
||||
getIndexFields(req: FrameworkRequest, indices: string[]): Promise<IndexField[]>;
|
||||
|
@ -16,4 +17,6 @@ export interface IndexFieldDescriptor {
|
|||
type: string;
|
||||
searchable: boolean;
|
||||
aggregatable: boolean;
|
||||
esTypes?: string[];
|
||||
subType?: IFieldSubType;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue