mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] Add Osquery markdown plugin (#95149)
This commit is contained in:
parent
95beca7d72
commit
a171f93995
21 changed files with 722 additions and 304 deletions
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiAccordionProps } from '@elastic/eui';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
|
@ -13,16 +12,15 @@ import {
|
|||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiAccordion,
|
||||
EuiCard,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useForm as useHookForm, FormProvider } from 'react-hook-form';
|
||||
|
||||
import { isEmpty, map, find, pickBy } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form';
|
||||
import type {
|
||||
EcsMappingFormField,
|
||||
|
@ -33,8 +31,6 @@ import { convertECSMappingToObject } from '../../../common/schemas/common/utils'
|
|||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { ResultTabs } from '../../routes/saved_queries/edit/tabs';
|
||||
import { SavedQueryFlyout } from '../../saved_queries';
|
||||
import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
import { usePacks } from '../../packs/use_packs';
|
||||
import { PackQueriesStatusTable } from './pack_queries_status_table';
|
||||
import { useCreateLiveQuery } from '../use_create_live_query_action';
|
||||
|
@ -99,13 +95,6 @@ const StyledEuiCard = styled(EuiCard)`
|
|||
}
|
||||
`;
|
||||
|
||||
const StyledEuiAccordion = styled(EuiAccordion)`
|
||||
${({ isDisabled }: { isDisabled?: boolean }) => isDisabled && 'display: none;'}
|
||||
.euiAccordion__button {
|
||||
color: ${({ theme }) => theme.eui.euiColorPrimary};
|
||||
}
|
||||
`;
|
||||
|
||||
type FormType = 'simple' | 'steps';
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
|
@ -123,7 +112,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
defaultValue,
|
||||
onSuccess,
|
||||
queryField = true,
|
||||
ecsMappingField = true,
|
||||
formType = 'steps',
|
||||
enabled = true,
|
||||
hideAgentsField = false,
|
||||
|
@ -161,8 +149,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[permissions]
|
||||
);
|
||||
|
||||
const [advancedContentState, setAdvancedContentState] =
|
||||
useState<EuiAccordionProps['forceState']>('closed');
|
||||
const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false);
|
||||
const [queryType, setQueryType] = useState<string>('query');
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
|
@ -208,43 +194,14 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[queryStatus]
|
||||
);
|
||||
|
||||
const handleSavedQueryChange = useCallback(
|
||||
(savedQuery) => {
|
||||
if (savedQuery) {
|
||||
setValue('query', savedQuery.query);
|
||||
setValue('savedQueryId', savedQuery.savedQueryId);
|
||||
setValue(
|
||||
'ecs_mapping',
|
||||
!isEmpty(savedQuery.ecs_mapping)
|
||||
? map(savedQuery.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0] as string,
|
||||
},
|
||||
}))
|
||||
: [defaultEcsFormData]
|
||||
);
|
||||
|
||||
if (!isEmpty(savedQuery.ecs_mapping)) {
|
||||
setAdvancedContentState('open');
|
||||
}
|
||||
} else {
|
||||
setValue('savedQueryId', null);
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
// not sure why, but submitOnCmdEnter doesn't have proper form values so I am passing them in manually
|
||||
async (values: LiveQueryFormFields = watchedValues) => {
|
||||
async (values: LiveQueryFormFields) => {
|
||||
const serializedData = pickBy(
|
||||
{
|
||||
agentSelection: values.agentSelection,
|
||||
saved_query_id: values.savedQueryId,
|
||||
query: values.query,
|
||||
pack_id: packId?.length ? packId[0] : undefined,
|
||||
pack_id: values?.packId?.length ? values?.packId[0] : undefined,
|
||||
...(values.ecs_mapping
|
||||
? { ecs_mapping: convertECSMappingToObject(values.ecs_mapping) }
|
||||
: {}),
|
||||
|
@ -259,25 +216,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
[errors, mutateAsync, packId, watchedValues]
|
||||
);
|
||||
const commands = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'submitOnCmdEnter',
|
||||
bindKey: { win: 'ctrl+enter', mac: 'cmd+enter' },
|
||||
// @ts-expect-error update types - explanation in onSubmit()
|
||||
exec: () => handleSubmit(onSubmit)(watchedValues),
|
||||
},
|
||||
],
|
||||
[handleSubmit, onSubmit, watchedValues]
|
||||
);
|
||||
|
||||
const queryComponentProps = useMemo(
|
||||
() => ({
|
||||
commands,
|
||||
}),
|
||||
[commands]
|
||||
[errors, mutateAsync]
|
||||
);
|
||||
|
||||
const serializedData: SavedQuerySOFormData = useMemo(
|
||||
|
@ -285,23 +224,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[watchedValues]
|
||||
);
|
||||
|
||||
const handleToggle = useCallback((isOpen) => {
|
||||
const newState = isOpen ? 'open' : 'closed';
|
||||
setAdvancedContentState(newState);
|
||||
}, []);
|
||||
|
||||
const ecsFieldProps = useMemo(
|
||||
() => ({
|
||||
isDisabled: !permissions.writeLiveQueries,
|
||||
}),
|
||||
[permissions.writeLiveQueries]
|
||||
);
|
||||
|
||||
const isSavedQueryDisabled = useMemo(
|
||||
() => !permissions.runSavedQueries || !permissions.readSavedQueries,
|
||||
[permissions.readSavedQueries, permissions.runSavedQueries]
|
||||
);
|
||||
|
||||
const { data: packsData, isFetched: isPackDataFetched } = usePacks({});
|
||||
|
||||
const selectedPackData = useMemo(
|
||||
|
@ -309,6 +231,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[packId, packsData]
|
||||
);
|
||||
|
||||
const handleSubmitForm = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]);
|
||||
|
||||
const submitButtonContent = useMemo(
|
||||
() => (
|
||||
<EuiFlexItem>
|
||||
|
@ -330,7 +254,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
<EuiButton
|
||||
id="submit-button"
|
||||
disabled={!enabled || isSubmitting}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
onClick={handleSubmitForm}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.submitButtonLabel"
|
||||
|
@ -349,53 +273,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
handleShowSaveQueryFlyout,
|
||||
enabled,
|
||||
isSubmitting,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
]
|
||||
);
|
||||
|
||||
const queryFieldStepContent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{queryField && (
|
||||
<>
|
||||
{!isSavedQueryDisabled && (
|
||||
<>
|
||||
<SavedQueriesDropdown
|
||||
disabled={isSavedQueryDisabled}
|
||||
onChange={handleSavedQueryChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<LiveQueryQueryField {...queryComponentProps} queryType={queryType} />
|
||||
</>
|
||||
)}
|
||||
{ecsMappingField && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<StyledEuiAccordion
|
||||
id="advanced"
|
||||
forceState={advancedContentState}
|
||||
onToggle={handleToggle}
|
||||
buttonContent="Advanced"
|
||||
>
|
||||
<EuiSpacer size="xs" />
|
||||
<ECSMappingEditorField euiFieldProps={ecsFieldProps} />
|
||||
</StyledEuiAccordion>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[
|
||||
queryField,
|
||||
isSavedQueryDisabled,
|
||||
handleSavedQueryChange,
|
||||
queryComponentProps,
|
||||
queryType,
|
||||
ecsMappingField,
|
||||
advancedContentState,
|
||||
handleToggle,
|
||||
ecsFieldProps,
|
||||
handleSubmitForm,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -589,7 +467,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem>{queryFieldStepContent}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<LiveQueryQueryField handleSubmitForm={handleSubmitForm} />
|
||||
</EuiFlexItem>
|
||||
{submitButtonContent}
|
||||
<EuiFlexItem>{resultsStepContent}</EuiFlexItem>
|
||||
</>
|
||||
|
|
|
@ -5,33 +5,45 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCodeBlock, EuiFormRow } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import type { EuiAccordionProps } from '@elastic/eui';
|
||||
import { EuiCodeBlock, EuiFormRow, EuiAccordion, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useController } from 'react-hook-form';
|
||||
import { useController, useFormContext } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiCodeEditorProps } from '../../shared_imports';
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { MAX_QUERY_LENGTH } from '../../packs/queries/validations';
|
||||
import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import type { SavedQueriesDropdownProps } from '../../saved_queries/saved_queries_dropdown';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
|
||||
const StyledEuiAccordion = styled(EuiAccordion)`
|
||||
${({ isDisabled }: { isDisabled?: boolean }) => isDisabled && 'display: none;'}
|
||||
.euiAccordion__button {
|
||||
color: ${({ theme }) => theme.eui.euiColorPrimary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEuiCodeBlock = styled(EuiCodeBlock)`
|
||||
min-height: 100px;
|
||||
`;
|
||||
|
||||
interface LiveQueryQueryFieldProps {
|
||||
export interface LiveQueryQueryFieldProps {
|
||||
disabled?: boolean;
|
||||
commands?: EuiCodeEditorProps['commands'];
|
||||
queryType: string;
|
||||
handleSubmitForm?: () => void;
|
||||
}
|
||||
|
||||
const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
|
||||
disabled,
|
||||
commands,
|
||||
queryType,
|
||||
handleSubmitForm,
|
||||
}) => {
|
||||
const formContext = useFormContext();
|
||||
const [advancedContentState, setAdvancedContentState] =
|
||||
useState<EuiAccordionProps['forceState']>('closed');
|
||||
const permissions = useKibana().services.application.capabilities.osquery;
|
||||
const queryType = formContext?.watch('queryType', 'query');
|
||||
|
||||
const {
|
||||
field: { onChange, value },
|
||||
|
@ -43,7 +55,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
|
|||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.emptyQueryError', {
|
||||
defaultMessage: 'Query is a required field',
|
||||
}),
|
||||
value: queryType === 'query',
|
||||
value: queryType !== 'pack',
|
||||
},
|
||||
maxLength: {
|
||||
message: i18n.translate('xpack.osquery.liveQuery.queryForm.largeQueryError', {
|
||||
|
@ -56,27 +68,108 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
|
|||
defaultValue: '',
|
||||
});
|
||||
|
||||
const handleSavedQueryChange: SavedQueriesDropdownProps['onChange'] = useCallback(
|
||||
(savedQuery) => {
|
||||
if (savedQuery) {
|
||||
formContext?.setValue('query', savedQuery.query);
|
||||
formContext?.setValue('savedQueryId', savedQuery.savedQueryId);
|
||||
if (!isEmpty(savedQuery.ecs_mapping)) {
|
||||
formContext?.setValue(
|
||||
'ecs_mapping',
|
||||
map(savedQuery.ecs_mapping, (ecsValue, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(ecsValue)[0],
|
||||
value: Object.values(ecsValue)[0] as string,
|
||||
},
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
formContext?.resetField('ecs_mapping');
|
||||
}
|
||||
|
||||
if (!isEmpty(savedQuery.ecs_mapping)) {
|
||||
setAdvancedContentState('open');
|
||||
}
|
||||
} else {
|
||||
formContext?.setValue('savedQueryId', null);
|
||||
}
|
||||
},
|
||||
[formContext]
|
||||
);
|
||||
|
||||
const handleToggle = useCallback((isOpen) => {
|
||||
const newState = isOpen ? 'open' : 'closed';
|
||||
setAdvancedContentState(newState);
|
||||
}, []);
|
||||
|
||||
const ecsFieldProps = useMemo(
|
||||
() => ({
|
||||
isDisabled: !permissions.writeLiveQueries,
|
||||
}),
|
||||
[permissions.writeLiveQueries]
|
||||
);
|
||||
|
||||
const isSavedQueryDisabled = useMemo(
|
||||
() => !permissions.runSavedQueries || !permissions.readSavedQueries,
|
||||
[permissions.readSavedQueries, permissions.runSavedQueries]
|
||||
);
|
||||
|
||||
const commands = useMemo(
|
||||
() =>
|
||||
handleSubmitForm
|
||||
? [
|
||||
{
|
||||
name: 'submitOnCmdEnter',
|
||||
bindKey: { win: 'ctrl+enter', mac: 'cmd+enter' },
|
||||
exec: handleSubmitForm,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[handleSubmitForm]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
isInvalid={!!error?.message}
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
isDisabled={!permissions.writeLiveQueries || disabled}
|
||||
>
|
||||
{!permissions.writeLiveQueries || disabled ? (
|
||||
<StyledEuiCodeBlock
|
||||
language="sql"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
transparentBackground={!value.length}
|
||||
>
|
||||
{value}
|
||||
</StyledEuiCodeBlock>
|
||||
) : (
|
||||
<OsqueryEditor defaultValue={value} onChange={onChange} commands={commands} />
|
||||
<>
|
||||
{!isSavedQueryDisabled && (
|
||||
<SavedQueriesDropdown disabled={isSavedQueryDisabled} onChange={handleSavedQueryChange} />
|
||||
)}
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
isInvalid={!!error?.message}
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
isDisabled={!permissions.writeLiveQueries || disabled}
|
||||
>
|
||||
{!permissions.writeLiveQueries || disabled ? (
|
||||
<StyledEuiCodeBlock
|
||||
language="sql"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
transparentBackground={!value.length}
|
||||
>
|
||||
{value}
|
||||
</StyledEuiCodeBlock>
|
||||
) : (
|
||||
<OsqueryEditor defaultValue={value} onChange={onChange} commands={commands} />
|
||||
)}
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<StyledEuiAccordion
|
||||
id="advanced"
|
||||
forceState={advancedContentState}
|
||||
onToggle={handleToggle}
|
||||
buttonContent="Advanced"
|
||||
>
|
||||
<EuiSpacer size="xs" />
|
||||
<ECSMappingEditorField euiFieldProps={ecsFieldProps} />
|
||||
</StyledEuiAccordion>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryQueryField = React.memo(LiveQueryQueryFieldComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { LiveQueryQueryField as default };
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
trim,
|
||||
get,
|
||||
} from 'lodash';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import {
|
||||
EuiFormLabel,
|
||||
|
@ -625,25 +625,6 @@ export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({
|
|||
defaultValue: '',
|
||||
});
|
||||
|
||||
const MultiFields = useMemo(
|
||||
() => (
|
||||
<div>
|
||||
<OsqueryColumnField
|
||||
item={item}
|
||||
index={index}
|
||||
isLastItem={isLastItem}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
// @ts-expect-error update types
|
||||
options: osquerySchemaOptions,
|
||||
isDisabled,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[item, index, isLastItem, osquerySchemaOptions, isDisabled]
|
||||
);
|
||||
|
||||
const ecsComboBoxEuiFieldProps = useMemo(() => ({ isDisabled }), [isDisabled]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
|
@ -676,7 +657,19 @@ export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="s" wrap>
|
||||
<ECSFieldWrapper>{MultiFields}</ECSFieldWrapper>
|
||||
<ECSFieldWrapper>
|
||||
<OsqueryColumnField
|
||||
item={item}
|
||||
index={index}
|
||||
isLastItem={isLastItem}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
// @ts-expect-error update types
|
||||
options: osquerySchemaOptions,
|
||||
isDisabled,
|
||||
}}
|
||||
/>
|
||||
</ECSFieldWrapper>
|
||||
{!isDisabled && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper>
|
||||
|
@ -742,7 +735,7 @@ export const ECSMappingEditorField = React.memo(
|
|||
const fieldsToValidate = prepareEcsFieldsToValidate(fields);
|
||||
// it is always at least 2 - empty fields
|
||||
if (fieldsToValidate.length > 2) {
|
||||
setTimeout(async () => await trigger('ecs_mapping'), 0);
|
||||
setTimeout(() => trigger('ecs_mapping'), 0);
|
||||
}
|
||||
}, [fields, query, trigger]);
|
||||
|
||||
|
@ -977,7 +970,7 @@ export const ECSMappingEditorField = React.memo(
|
|||
);
|
||||
}, [query]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
const ecsList = formData?.ecs_mapping;
|
||||
const lastEcs = formData?.ecs_mapping?.[itemsList?.current.length - 1];
|
||||
|
||||
|
@ -986,15 +979,16 @@ export const ECSMappingEditorField = React.memo(
|
|||
return;
|
||||
}
|
||||
|
||||
// // list contains ecs already, and the last item has values provided
|
||||
// list contains ecs already, and the last item has values provided
|
||||
if (
|
||||
ecsList?.length === itemsList.current.length &&
|
||||
lastEcs?.key?.length &&
|
||||
lastEcs?.result?.value?.length
|
||||
(ecsList?.length === itemsList.current.length &&
|
||||
lastEcs?.key?.length &&
|
||||
lastEcs?.result?.value?.length) ||
|
||||
!fields?.length
|
||||
) {
|
||||
return append(defaultEcsFormData);
|
||||
}
|
||||
}, [append, euiFieldProps?.isDisabled, formData]);
|
||||
}, [append, fields, formData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -26,7 +26,11 @@ import {
|
|||
LazyOsqueryManagedPolicyEditExtension,
|
||||
LazyOsqueryManagedCustomButtonExtension,
|
||||
} from './fleet_integration';
|
||||
import { getLazyOsqueryAction, useIsOsqueryAvailableSimple } from './shared_components';
|
||||
import {
|
||||
getLazyOsqueryAction,
|
||||
getLazyLiveQueryField,
|
||||
useIsOsqueryAvailableSimple,
|
||||
} from './shared_components';
|
||||
|
||||
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
|
||||
private kibanaVersion: string;
|
||||
|
@ -94,8 +98,10 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
OsqueryAction: getLazyOsqueryAction({
|
||||
...core,
|
||||
...plugins,
|
||||
storage: this.storage,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
}),
|
||||
LiveQueryField: getLazyLiveQueryField({
|
||||
...core,
|
||||
...plugins,
|
||||
}),
|
||||
isOsqueryAvailable: useIsOsqueryAvailableSimple,
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ const StyledEuiCodeBlock = styled(EuiCodeBlock)`
|
|||
}
|
||||
`;
|
||||
|
||||
interface SavedQueriesDropdownProps {
|
||||
export interface SavedQueriesDropdownProps {
|
||||
disabled?: boolean;
|
||||
onChange: (
|
||||
value:
|
||||
|
|
|
@ -6,4 +6,5 @@
|
|||
*/
|
||||
|
||||
export { getLazyOsqueryAction } from './lazy_osquery_action';
|
||||
export { getLazyLiveQueryField } from './lazy_live_query_field';
|
||||
export { useIsOsqueryAvailableSimple } from './osquery_action/use_is_osquery_available_simple';
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import type { LiveQueryQueryFieldProps } from '../live_queries/form/live_query_query_field';
|
||||
import type { ServicesWrapperProps } from './services_wrapper';
|
||||
import ServicesWrapper from './services_wrapper';
|
||||
|
||||
export const getLazyLiveQueryField =
|
||||
(services: ServicesWrapperProps['services']) =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
({
|
||||
formMethods,
|
||||
...props
|
||||
}: LiveQueryQueryFieldProps & {
|
||||
formMethods: UseFormReturn<{
|
||||
label: string;
|
||||
query: string;
|
||||
ecs_mapping: Record<string, unknown>;
|
||||
}>;
|
||||
}) => {
|
||||
const LiveQueryField = lazy(() => import('../live_queries/form/live_query_query_field'));
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ServicesWrapper services={services}>
|
||||
<FormProvider {...formMethods}>
|
||||
<LiveQueryField {...props} />
|
||||
</FormProvider>
|
||||
</ServicesWrapper>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
|
@ -6,15 +6,20 @@
|
|||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import ServicesWrapper from './services_wrapper';
|
||||
import type { ServicesWrapperProps } from './services_wrapper';
|
||||
import type { OsqueryActionProps } from './osquery_action';
|
||||
|
||||
// @ts-expect-error update types
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const getLazyOsqueryAction = (services) => (props) => {
|
||||
const OsqueryAction = lazy(() => import('./osquery_action'));
|
||||
export const getLazyOsqueryAction =
|
||||
// eslint-disable-next-line react/display-name
|
||||
(services: ServicesWrapperProps['services']) => (props: OsqueryActionProps) => {
|
||||
const OsqueryAction = lazy(() => import('./osquery_action'));
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<OsqueryAction services={services} {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ServicesWrapper services={services}>
|
||||
<OsqueryAction {...props} />
|
||||
</ServicesWrapper>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiErrorBoundary, EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
|
||||
import { EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import {
|
||||
AGENT_STATUS_ERROR,
|
||||
EMPTY_PROMPT,
|
||||
|
@ -16,17 +15,14 @@ import {
|
|||
PERMISSION_DENIED,
|
||||
SHORT_EMPTY_TITLE,
|
||||
} from './translations';
|
||||
import { KibanaContextProvider, useKibana } from '../../common/lib/kibana';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { LiveQuery } from '../../live_queries';
|
||||
import { queryClient } from '../../query_client';
|
||||
import { OsqueryIcon } from '../../components/osquery_icon';
|
||||
import { KibanaThemeProvider } from '../../shared_imports';
|
||||
import { useIsOsqueryAvailable } from './use_is_osquery_available';
|
||||
import type { StartPlugins } from '../../types';
|
||||
|
||||
interface OsqueryActionProps {
|
||||
export interface OsqueryActionProps {
|
||||
agentId?: string;
|
||||
defaultValues?: {};
|
||||
formType: 'steps' | 'simple';
|
||||
hideAgentsField?: boolean;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
|
@ -35,6 +31,7 @@ interface OsqueryActionProps {
|
|||
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
||||
agentId,
|
||||
formType = 'simple',
|
||||
defaultValues,
|
||||
hideAgentsField,
|
||||
addToTimeline,
|
||||
}) => {
|
||||
|
@ -54,7 +51,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
|||
const { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData } =
|
||||
useIsOsqueryAvailable(agentId);
|
||||
|
||||
if (!agentId || (agentFetched && !agentData)) {
|
||||
if (agentId && agentFetched && !agentData) {
|
||||
return emptyPrompt;
|
||||
}
|
||||
|
||||
|
@ -77,15 +74,15 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
if (agentId && isLoading) {
|
||||
return <EuiLoadingContent lines={10} />;
|
||||
}
|
||||
|
||||
if (!policyFetched && policyLoading) {
|
||||
if (agentId && !policyFetched && policyLoading) {
|
||||
return <EuiLoadingContent lines={10} />;
|
||||
}
|
||||
|
||||
if (!osqueryAvailable) {
|
||||
if (agentId && !osqueryAvailable) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<OsqueryIcon />}
|
||||
|
@ -96,7 +93,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (agentData?.status !== 'online') {
|
||||
if (agentId && agentData?.status !== 'online') {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<OsqueryIcon />}
|
||||
|
@ -113,38 +110,14 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
|||
agentId={agentId}
|
||||
hideAgentsField={hideAgentsField}
|
||||
addToTimeline={addToTimeline}
|
||||
{...defaultValues}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
OsqueryActionComponent.displayName = 'OsqueryAction';
|
||||
|
||||
export const OsqueryAction = React.memo(OsqueryActionComponent);
|
||||
|
||||
type OsqueryActionWrapperProps = { services: CoreStart & StartPlugins } & OsqueryActionProps;
|
||||
|
||||
const OsqueryActionWrapperComponent: React.FC<OsqueryActionWrapperProps> = ({
|
||||
services,
|
||||
agentId,
|
||||
formType,
|
||||
hideAgentsField = false,
|
||||
addToTimeline,
|
||||
}) => (
|
||||
<KibanaThemeProvider theme$={services.theme.theme$}>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EuiErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OsqueryAction
|
||||
agentId={agentId}
|
||||
formType={formType}
|
||||
hideAgentsField={hideAgentsField}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</EuiErrorBoundary>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
);
|
||||
|
||||
const OsqueryActionWrapper = React.memo(OsqueryActionWrapperComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { OsqueryActionWrapper as default };
|
||||
export { OsqueryAction as default };
|
||||
|
|
|
@ -81,13 +81,6 @@ describe('Osquery Action', () => {
|
|||
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
|
||||
expect(getByText(EMPTY_PROMPT)).toBeInTheDocument();
|
||||
});
|
||||
it('should return empty prompt when no agentId', async () => {
|
||||
spyOsquery();
|
||||
mockKibana();
|
||||
|
||||
const { getByText } = renderWithContext(<OsqueryAction agentId={''} formType={'steps'} />);
|
||||
expect(getByText(EMPTY_PROMPT)).toBeInTheDocument();
|
||||
});
|
||||
it('should return permission denied when agentFetched and agentData available', async () => {
|
||||
spyOsquery({ agentData: {} });
|
||||
mockKibana();
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '../common/lib/kibana';
|
||||
|
||||
import { queryClient } from '../query_client';
|
||||
import { KibanaThemeProvider } from '../shared_imports';
|
||||
import type { StartPlugins } from '../types';
|
||||
|
||||
export interface ServicesWrapperProps {
|
||||
services: CoreStart & StartPlugins;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ServicesWrapperComponent: React.FC<ServicesWrapperProps> = ({ services, children }) => (
|
||||
<KibanaThemeProvider theme$={services.theme.theme$}>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EuiErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</EuiErrorBoundary>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
);
|
||||
|
||||
const ServicesWrapper = React.memo(ServicesWrapperComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ServicesWrapper as default };
|
|
@ -16,12 +16,13 @@ import type {
|
|||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { getLazyOsqueryAction } from './shared_components';
|
||||
import type { getLazyLiveQueryField, getLazyOsqueryAction } from './shared_components';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface OsqueryPluginSetup {}
|
||||
export interface OsqueryPluginStart {
|
||||
OsqueryAction?: ReturnType<typeof getLazyOsqueryAction>;
|
||||
LiveQueryField?: ReturnType<typeof getLazyLiveQueryField>;
|
||||
isOsqueryAvailable: (props: { agentId: string }) => boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
|
||||
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { createContext, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { GetBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
|
||||
import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
|
||||
import * as i18n from './translations';
|
||||
import { useRuleWithFallback } from '../../../detections/containers/detection_engine/rules/use_rule_with_fallback';
|
||||
import { MarkdownRenderer } from '../markdown_editor';
|
||||
|
@ -22,6 +23,8 @@ export const Indent = styled.div`
|
|||
word-break: break-word;
|
||||
`;
|
||||
|
||||
export const BasicAlertDataContext = createContext<Partial<GetBasicDataFromDetailsData>>({});
|
||||
|
||||
const InvestigationGuideViewComponent: React.FC<{
|
||||
data: TimelineEventsDetailsItem[];
|
||||
}> = ({ data }) => {
|
||||
|
@ -32,13 +35,14 @@ const InvestigationGuideViewComponent: React.FC<{
|
|||
: item?.originalValue ?? null;
|
||||
}, [data]);
|
||||
const { rule: maybeRule } = useRuleWithFallback(ruleId);
|
||||
const basicAlertData = useBasicDataFromDetailsData(data);
|
||||
|
||||
if (!maybeRule?.note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BasicAlertDataContext.Provider value={basicAlertData}>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xxxs" data-test-subj="summary-view-guide">
|
||||
<h5>{i18n.INVESTIGATION_GUIDE}</h5>
|
||||
|
@ -51,7 +55,7 @@ const InvestigationGuideViewComponent: React.FC<{
|
|||
</LineClamp>
|
||||
</EuiText>
|
||||
</Indent>
|
||||
</>
|
||||
</BasicAlertDataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,37 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiLinkAnchorProps } from '@elastic/eui';
|
||||
import {
|
||||
getDefaultEuiMarkdownParsingPlugins,
|
||||
getDefaultEuiMarkdownProcessingPlugins,
|
||||
getDefaultEuiMarkdownUiPlugins,
|
||||
} from '@elastic/eui';
|
||||
// Remove after this issue is resolved: https://github.com/elastic/eui/issues/4688
|
||||
import type { Options as Remark2RehypeOptions } from 'mdast-util-to-hast';
|
||||
import type { FunctionComponent } from 'react';
|
||||
import type rehype2react from 'rehype-react';
|
||||
import type { Plugin, PluggableList } from 'unified';
|
||||
|
||||
import * as timelineMarkdownPlugin from './timeline';
|
||||
import * as osqueryMarkdownPlugin from './osquery';
|
||||
|
||||
export const { uiPlugins, parsingPlugins, processingPlugins } = {
|
||||
uiPlugins: getDefaultEuiMarkdownUiPlugins(),
|
||||
parsingPlugins: getDefaultEuiMarkdownParsingPlugins(),
|
||||
processingPlugins: getDefaultEuiMarkdownProcessingPlugins() as [
|
||||
[Plugin, Remark2RehypeOptions],
|
||||
[
|
||||
typeof rehype2react,
|
||||
Parameters<typeof rehype2react>[0] & {
|
||||
components: { a: FunctionComponent<EuiLinkAnchorProps>; timeline: unknown };
|
||||
}
|
||||
],
|
||||
...PluggableList
|
||||
],
|
||||
processingPlugins: getDefaultEuiMarkdownProcessingPlugins(),
|
||||
};
|
||||
|
||||
uiPlugins.push(timelineMarkdownPlugin.plugin);
|
||||
uiPlugins.push(osqueryMarkdownPlugin.plugin);
|
||||
|
||||
parsingPlugins.push(timelineMarkdownPlugin.parser);
|
||||
parsingPlugins.push(osqueryMarkdownPlugin.parser);
|
||||
|
||||
// This line of code is TS-compatible and it will break if [1][1] change in the future.
|
||||
processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
|
||||
processingPlugins[1][1].components.osquery = osqueryMarkdownPlugin.renderer;
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { pickBy, isEmpty } from 'lodash';
|
||||
import type { Plugin } from 'unified';
|
||||
import React, { useContext, useMemo, useState, useCallback } from 'react';
|
||||
import type { RemarkTokenizer } from '@elastic/eui';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiCodeBlock,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import styled from 'styled-components';
|
||||
import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '../../../../lib/kibana';
|
||||
import { LabelField } from './label_field';
|
||||
import OsqueryLogo from './osquery_icon/osquery.svg';
|
||||
import { OsqueryFlyout } from '../../../../../detections/components/osquery/osquery_flyout';
|
||||
import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view';
|
||||
import { convertECSMappingToObject } from './utils';
|
||||
|
||||
const StyledEuiButton = styled(EuiButton)`
|
||||
> span > img {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const OsqueryEditorComponent = ({
|
||||
node,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: EuiMarkdownEditorUiPluginEditorProps<{
|
||||
configuration: {
|
||||
label?: string;
|
||||
query: string;
|
||||
ecs_mapping: { [key: string]: {} };
|
||||
};
|
||||
}>) => {
|
||||
const isEditMode = node != null;
|
||||
const { osquery } = useKibana().services;
|
||||
const formMethods = useForm<{
|
||||
label: string;
|
||||
query: string;
|
||||
ecs_mapping: Record<string, unknown>;
|
||||
}>({
|
||||
defaultValues: {
|
||||
label: node?.configuration?.label,
|
||||
query: node?.configuration?.query,
|
||||
ecs_mapping: node?.configuration?.ecs_mapping,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data) => {
|
||||
onSave(
|
||||
`!{osquery${JSON.stringify(
|
||||
pickBy(
|
||||
{
|
||||
query: data.query,
|
||||
label: data.label,
|
||||
ecs_mapping: convertECSMappingToObject(data.ecs_mapping),
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
)
|
||||
)}}`,
|
||||
{
|
||||
block: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
[onSave]
|
||||
);
|
||||
|
||||
const OsqueryActionForm = useMemo(() => {
|
||||
if (osquery?.LiveQueryField) {
|
||||
const { LiveQueryField } = osquery;
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<LabelField />
|
||||
<EuiSpacer size="m" />
|
||||
<LiveQueryField formMethods={formMethods} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [formMethods, osquery]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.editModalTitle"
|
||||
defaultMessage="Edit query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.addModalTitle"
|
||||
defaultMessage="Add query"
|
||||
/>
|
||||
)}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<>{OsqueryActionForm}</>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel}>
|
||||
{i18n.translate('xpack.securitySolution.markdown.osquery.modalCancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton onClick={formMethods.handleSubmit(onSubmit)} fill>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel"
|
||||
defaultMessage="Add query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.editModalConfirmButtonLabel"
|
||||
defaultMessage="Save changes"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OsqueryEditor = React.memo(OsqueryEditorComponent);
|
||||
|
||||
export const plugin = {
|
||||
name: 'osquery',
|
||||
button: {
|
||||
label: 'Osquery',
|
||||
iconType: 'logoOsquery',
|
||||
},
|
||||
helpText: (
|
||||
<div>
|
||||
<EuiCodeBlock language="md" fontSize="l" paddingSize="s" isCopyable>
|
||||
{'!{osquery{options}}'}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
),
|
||||
editor: OsqueryEditor,
|
||||
};
|
||||
|
||||
export const parser: Plugin = function () {
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.blockTokenizers;
|
||||
const methods = Parser.prototype.blockMethods;
|
||||
|
||||
const tokenizeOsquery: RemarkTokenizer = function (eat, value, silent) {
|
||||
if (value.startsWith('!{osquery') === false) return false;
|
||||
|
||||
const nextChar = value[9];
|
||||
|
||||
if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a osquery
|
||||
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// is there a configuration?
|
||||
const hasConfiguration = nextChar === '{';
|
||||
|
||||
let match = '!{osquery';
|
||||
let configuration = {};
|
||||
|
||||
if (hasConfiguration) {
|
||||
let configurationString = '';
|
||||
|
||||
let openObjects = 0;
|
||||
|
||||
for (let i = 9; i < value.length; i++) {
|
||||
const char = value[i];
|
||||
if (char === '{') {
|
||||
openObjects++;
|
||||
configurationString += char;
|
||||
} else if (char === '}') {
|
||||
openObjects--;
|
||||
if (openObjects === -1) {
|
||||
break;
|
||||
}
|
||||
configurationString += char;
|
||||
} else {
|
||||
configurationString += char;
|
||||
}
|
||||
}
|
||||
|
||||
match += configurationString;
|
||||
try {
|
||||
configuration = JSON.parse(configurationString);
|
||||
} catch (e) {
|
||||
const now = eat.now();
|
||||
this.file.fail(`Unable to parse osquery JSON configuration: ${e}`, {
|
||||
line: now.line,
|
||||
column: now.column + 9,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
match += '}';
|
||||
|
||||
return eat(match)({
|
||||
type: 'osquery',
|
||||
configuration,
|
||||
});
|
||||
};
|
||||
|
||||
tokenizers.osquery = tokenizeOsquery;
|
||||
methods.splice(methods.indexOf('text'), 0, 'osquery');
|
||||
};
|
||||
|
||||
// receives the configuration from the parser and renders
|
||||
const RunOsqueryButtonRenderer = ({
|
||||
configuration,
|
||||
}: {
|
||||
configuration: {
|
||||
label?: string;
|
||||
query: string;
|
||||
ecs_mapping: { [key: string]: {} };
|
||||
};
|
||||
}) => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const { agentId } = useContext(BasicAlertDataContext);
|
||||
|
||||
const handleOpen = useCallback(() => setShowFlyout(true), [setShowFlyout]);
|
||||
|
||||
const handleClose = useCallback(() => setShowFlyout(false), [setShowFlyout]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledEuiButton iconType={OsqueryLogo} onClick={handleOpen}>
|
||||
{configuration.label ??
|
||||
i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', {
|
||||
defaultMessage: 'Run Osquery',
|
||||
})}
|
||||
</StyledEuiButton>
|
||||
{showFlyout && (
|
||||
<OsqueryFlyout
|
||||
defaultValues={{
|
||||
query: configuration.query,
|
||||
ecs_mapping: configuration.ecs_mapping,
|
||||
queryField: false,
|
||||
}}
|
||||
agentId={agentId}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { RunOsqueryButtonRenderer as renderer };
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface QueryDescriptionFieldProps {
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const LabelFieldComponent = ({ euiFieldProps }: QueryDescriptionFieldProps) => {
|
||||
const {
|
||||
field: { onChange, value, name: fieldName },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'label',
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.securitySolution.markdown.osquery.labelFieldText', {
|
||||
defaultMessage: 'Label',
|
||||
})}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={hasError}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
name={fieldName}
|
||||
fullWidth
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const LabelField = React.memo(LabelFieldComponent);
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { EuiIconProps } from '@elastic/eui';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import OsqueryLogo from './osquery.svg';
|
||||
|
||||
export type OsqueryIconProps = Omit<EuiIconProps, 'type'>;
|
||||
|
||||
const OsqueryIconComponent: React.FC<OsqueryIconProps> = (props) => (
|
||||
<EuiIcon size="xl" type={OsqueryLogo} {...props} />
|
||||
);
|
||||
|
||||
export const OsqueryIcon = React.memo(OsqueryIconComponent);
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="256px" height="255px" viewBox="0 0 256 255" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M255.214617,0.257580247 L255.214617,63.993679 L191.609679,127.598617 L191.609679,63.7297778 L255.214617,0.257580247" fill="#A596FF"></path>
|
||||
<path d="M128.006321,0.257580247 L128.006321,63.993679 L191.611259,127.598617 L191.611259,63.7297778 L128.006321,0.257580247" fill="#000000"></path>
|
||||
<path d="M255.345778,254.803753 L191.609679,254.803753 L128.004741,191.198815 L191.872,191.198815 L255.345778,254.803753" fill="#A596FF"></path>
|
||||
<path d="M255.345778,127.595457 L191.609679,127.595457 L128.004741,191.200395 L191.872,191.200395 L255.345778,127.595457" fill="#000000"></path>
|
||||
<path d="M0.801185185,254.936494 L0.801185185,191.198815 L64.4061235,127.593877 L64.4061235,191.462716 L0.801185185,254.936494" fill="#A596FF"></path>
|
||||
<path d="M128.009481,254.936494 L128.009481,191.198815 L64.4045432,127.593877 L64.4045432,191.462716 L128.009481,254.936494" fill="#000000"></path>
|
||||
<path d="M0.671604938,0.385580247 L64.4077037,0.385580247 L128.012642,63.9905185 L64.1453827,63.9905185 L0.671604938,0.385580247" fill="#A596FF"></path>
|
||||
<path d="M0.671604938,127.593877 L64.4077037,127.593877 L128.012642,63.9889383 L64.1453827,63.9889383 L0.671604938,127.593877" fill="#000000"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty, reduce } from 'lodash';
|
||||
|
||||
export const convertECSMappingToObject = (
|
||||
ecsMapping: Array<{
|
||||
key: string;
|
||||
result: {
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
}>
|
||||
) =>
|
||||
reduce(
|
||||
ecsMapping,
|
||||
(acc, value) => {
|
||||
if (!isEmpty(value?.key) && !isEmpty(value.result?.type) && !isEmpty(value.result?.value)) {
|
||||
acc[value.key] = {
|
||||
[value.result.type]: value.result.value,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { field?: string; value?: string }>
|
||||
);
|
|
@ -25,16 +25,19 @@ const OsqueryActionWrapper = styled.div`
|
|||
`;
|
||||
|
||||
export interface OsqueryFlyoutProps {
|
||||
agentId: string;
|
||||
agentId?: string;
|
||||
defaultValues?: {};
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TimelineComponent = React.memo((props) => {
|
||||
return <EuiButtonEmpty {...props} size="xs" />;
|
||||
});
|
||||
const TimelineComponent = React.memo((props) => <EuiButtonEmpty {...props} size="xs" />);
|
||||
TimelineComponent.displayName = 'TimelineComponent';
|
||||
|
||||
export const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({ agentId, onClose }) => {
|
||||
export const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({
|
||||
agentId,
|
||||
defaultValues,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
services: { osquery, timelines },
|
||||
} = useKibana();
|
||||
|
@ -70,30 +73,38 @@ export const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({ agentId,
|
|||
},
|
||||
[getAddToTimelineButton]
|
||||
);
|
||||
// @ts-expect-error
|
||||
const { OsqueryAction } = osquery;
|
||||
return (
|
||||
<EuiFlyout
|
||||
ownFocus
|
||||
maskProps={{ style: 'z-index: 5000' }} // For an edge case to display above the timeline flyout
|
||||
size="m"
|
||||
onClose={onClose}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder data-test-subj="flyout-header-osquery">
|
||||
<EuiTitle>
|
||||
<h2>{ACTION_OSQUERY}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<OsqueryActionWrapper data-test-subj="flyout-body-osquery">
|
||||
<OsqueryAction agentId={agentId} formType="steps" addToTimeline={handleAddToTimeline} />
|
||||
</OsqueryActionWrapper>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<OsqueryEventDetailsFooter handleClick={onClose} data-test-subj="flyout-footer-osquery" />
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
||||
if (osquery?.OsqueryAction) {
|
||||
return (
|
||||
<EuiFlyout
|
||||
ownFocus
|
||||
maskProps={{ style: 'z-index: 5000' }} // For an edge case to display above the timeline flyout
|
||||
size="m"
|
||||
onClose={onClose}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder data-test-subj="flyout-header-osquery">
|
||||
<EuiTitle>
|
||||
<h2>{ACTION_OSQUERY}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<OsqueryActionWrapper data-test-subj="flyout-body-osquery">
|
||||
<osquery.OsqueryAction
|
||||
agentId={agentId}
|
||||
formType="steps"
|
||||
defaultValues={defaultValues}
|
||||
addToTimeline={handleAddToTimeline}
|
||||
/>
|
||||
</OsqueryActionWrapper>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<OsqueryEventDetailsFooter handleClick={onClose} data-test-subj="flyout-footer-osquery" />
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const OsqueryFlyout = React.memo(OsqueryFlyoutComponent);
|
||||
|
|
|
@ -11,8 +11,9 @@ import type { TimelineEventsDetailsItem } from '../../../../../common/search_str
|
|||
import { getFieldValue } from '../../../../detections/components/host_isolation/helpers';
|
||||
import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
|
||||
|
||||
interface GetBasicDataFromDetailsData {
|
||||
export interface GetBasicDataFromDetailsData {
|
||||
alertId: string;
|
||||
agentId?: string;
|
||||
isAlert: boolean;
|
||||
hostName: string;
|
||||
ruleName: string;
|
||||
|
@ -31,6 +32,11 @@ export const useBasicDataFromDetailsData = (
|
|||
|
||||
const alertId = useMemo(() => getFieldValue({ category: '_id', field: '_id' }, data), [data]);
|
||||
|
||||
const agentId = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.id' }, data),
|
||||
[data]
|
||||
);
|
||||
|
||||
const hostName = useMemo(
|
||||
() => getFieldValue({ category: 'host', field: 'host.name' }, data),
|
||||
[data]
|
||||
|
@ -44,17 +50,18 @@ export const useBasicDataFromDetailsData = (
|
|||
return useMemo(
|
||||
() => ({
|
||||
alertId,
|
||||
agentId,
|
||||
isAlert,
|
||||
hostName,
|
||||
ruleName,
|
||||
timestamp,
|
||||
}),
|
||||
[alertId, hostName, isAlert, ruleName, timestamp]
|
||||
[agentId, alertId, hostName, isAlert, ruleName, timestamp]
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
The referenced alert _index in the flyout uses the `.internal.` such as
|
||||
The referenced alert _index in the flyout uses the `.internal.` such as
|
||||
`.internal.alerts-security.alerts-spaceId` in the alert page flyout and
|
||||
.internal.preview.alerts-security.alerts-spaceId` in the rule creation preview flyout
|
||||
but we always want to use their respective aliase indices rather than accessing their backing .internal. indices.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue