diff --git a/package.json b/package.json index 6ec907aeef2c..31daed6021c6 100644 --- a/package.json +++ b/package.json @@ -349,7 +349,7 @@ "react-moment-proptypes": "^1.7.0", "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", - "react-query": "^3.18.1", + "react-query": "^3.21.0", "react-redux": "^7.2.0", "react-resizable": "^1.7.5", "react-resize-detector": "^4.2.0", diff --git a/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx b/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx index 81953135b532..0207963852a5 100644 --- a/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx +++ b/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx @@ -7,12 +7,19 @@ import { EuiLink } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; import { PLUGIN_ID } from '../../../fleet/common'; import { pagePathGetters } from '../../../fleet/public'; import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; import { useAgentPolicy } from './use_agent_policy'; +const StyledEuiLink = styled(EuiLink)` + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +`; + interface AgentsPolicyLinkProps { policyId: string; } @@ -46,10 +53,9 @@ const AgentsPolicyLinkComponent: React.FC = ({ policyId } ); return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - + {data?.name ?? policyId} - + ); }; diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts index 0446b6b2f818..ecd7828cb828 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts @@ -36,6 +36,10 @@ export const useAgentPolicy = ({ policyId, skip, silent }: UseAgentPolicy) => { defaultMessage: 'Error while fetching agent policy details', }), }), + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + staleTime: Infinity, } ); }; diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts b/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts new file mode 100644 index 000000000000..42790e46e0a9 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts @@ -0,0 +1,58 @@ +/* + * 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 { map } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { useQuery } from 'react-query'; + +import { AGENT_SAVED_OBJECT_TYPE, Agent } from '../../../fleet/common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; +import { useKibana } from '../common/lib/kibana'; + +interface UseAgentPolicyAgentIdsProps { + agentPolicyId: string | undefined; + silent?: boolean; + skip?: boolean; +} + +export const useAgentPolicyAgentIds = ({ + agentPolicyId, + silent, + skip, +}: UseAgentPolicyAgentIdsProps) => { + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); + + return useQuery<{ agents: Agent[] }, unknown, string[]>( + ['agentPolicyAgentIds', agentPolicyId], + () => { + const kuery = `${AGENT_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`; + + return http.get(`/internal/osquery/fleet_wrapper/agents`, { + query: { + kuery, + perPage: 10000, + }, + }); + }, + { + select: (data) => map(data?.agents, 'id') || ([] as string[]), + enabled: !skip || !agentPolicyId, + onSuccess: () => setErrorToast(), + onError: (error) => + !silent && + setErrorToast(error as Error, { + title: i18n.translate('xpack.osquery.agents.fetchError', { + defaultMessage: 'Error while fetching agents', + }), + }), + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + } + ); +}; diff --git a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx index 8bf8ed80eb04..7cc561ff7a73 100644 --- a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx +++ b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx @@ -22,5 +22,9 @@ export const useOsqueryIntegrationStatus = () => { defaultMessage: 'Error while fetching osquery integration', }), }), + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + staleTime: Infinity, }); }; diff --git a/x-pack/plugins/osquery/public/editor/index.tsx b/x-pack/plugins/osquery/public/editor/index.tsx index a8079c58e8cb..09e0ccbf7a45 100644 --- a/x-pack/plugins/osquery/public/editor/index.tsx +++ b/x-pack/plugins/osquery/public/editor/index.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useCallback, useRef } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiCodeEditor } from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; import 'brace/theme/tomorrow'; import './osquery_mode.ts'; @@ -26,22 +27,19 @@ interface OsqueryEditorProps { } const OsqueryEditorComponent: React.FC = ({ defaultValue, onChange }) => { - const editorValue = useRef(defaultValue ?? ''); + const [editorValue, setEditorValue] = useState(defaultValue ?? ''); - const handleChange = useCallback((newValue: string) => { - editorValue.current = newValue; - }, []); + useDebounce(() => onChange(editorValue.replaceAll('\n', ' ').replaceAll(' ', ' ')), 500, [ + editorValue, + ]); - const onBlur = useCallback(() => { - onChange(editorValue.current.replaceAll('\n', ' ').replaceAll(' ', ' ')); - }, [onChange]); + useEffect(() => setEditorValue(defaultValue), [defaultValue]); return ( = ({ ), }); - const { submit } = form; + const { setFieldValue, submit } = form; const actionId = useMemo(() => data?.actions[0].action_id, [data?.actions]); const agentIds = useMemo(() => data?.actions[0].agents, [data?.actions]); @@ -253,6 +253,15 @@ const LiveQueryFormComponent: React.FC = ({ [queryFieldStepContent, resultsStepContent] ); + useEffect(() => { + if (defaultValue?.agentSelection) { + setFieldValue('agentSelection', defaultValue?.agentSelection); + } + if (defaultValue?.query) { + setFieldValue('query', defaultValue?.query); + } + }, [defaultValue, setFieldValue]); + return ( <>
{singleAgentMode ? singleAgentForm : } diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 42ac76f2dcf7..c0760b9399ba 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -31,7 +31,7 @@ import { ViewResultsInDiscoverAction, ViewResultsInLensAction, ViewResultsActionButtonType, -} from '../scheduled_query_groups/scheduled_query_group_queries_table'; +} from '../scheduled_query_groups/scheduled_query_group_queries_status_table'; import { useActionResultsPrivileges } from '../action_results/use_action_privileges'; import { OSQUERY_INTEGRATION_NAME } from '../../common'; diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx index e82dcf85780e..205099bb6861 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { EuiInMemoryTable, EuiButton, diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/add/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/add/index.tsx index 90522b537db4..6a4753e7aac9 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/add/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/add/index.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { startCase } from 'lodash'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; @@ -26,7 +27,7 @@ const AddScheduledQueryGroupPageComponent = () => { return { name: osqueryIntegration.name, - title: osqueryIntegration.title, + title: osqueryIntegration.title ?? startCase(osqueryIntegration.name), version: osqueryIntegration.version, }; }, [osqueryIntegration]); diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx index dc6df4961509..35184ec4bcbc 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx @@ -24,10 +24,11 @@ import styled from 'styled-components'; import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group'; -import { ScheduledQueryGroupQueriesTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_table'; +import { ScheduledQueryGroupQueriesStatusTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_status_table'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { AgentsPolicyLink } from '../../../agent_policies/agents_policy_link'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; +import { useAgentPolicyAgentIds } from '../../../agents/use_agent_policy_agent_ids'; const Divider = styled.div` width: 0; @@ -44,6 +45,10 @@ const ScheduledQueryGroupDetailsPageComponent = () => { ); const { data } = useScheduledQueryGroup({ scheduledQueryGroupId }); + const { data: agentIds } = useAgentPolicyAgentIds({ + agentPolicyId: data?.policy_id, + skip: !data, + }); useBreadcrumbs('scheduled_query_group_details', { scheduledQueryGroupName: data?.name ?? '' }); @@ -131,7 +136,13 @@ const ScheduledQueryGroupDetailsPageComponent = () => { return ( - {data && } + {data && ( + + )} ); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx index c940b1f8527b..5bc4bf32bcef 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx @@ -244,7 +244,7 @@ const ScheduledQueryGroupFormComponent: React.FC = ), }); - const { submit } = form; + const { setFieldValue, submit } = form; const policyIdEuiFieldProps = useMemo( () => ({ isDisabled: !!defaultValue, options: agentPolicyOptions }), @@ -276,6 +276,10 @@ const ScheduledQueryGroupFormComponent: React.FC = }; }, [agentPoliciesById, policyId]); + const handleNameChange = useCallback((newName: string) => setFieldValue('name', newName), [ + setFieldValue, + ]); + const handleSaveClick = useCallback(() => { if (currentPolicy.agentCount) { setShowConfirmationModal(true); @@ -343,6 +347,7 @@ const ScheduledQueryGroupFormComponent: React.FC = component={QueriesField} scheduledQueryGroupId={defaultValue?.id ?? null} integrationPackageVersion={integrationPackageVersion} + handleNameChange={handleNameChange} /> diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx index 3cd1b96f12fa..83e64ed6e6f3 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { mapKeys, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; import { EuiLink, EuiFormRow, EuiFilePicker, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; @@ -25,7 +25,7 @@ const ExamplePackLink = React.memo(() => ( ExamplePackLink.displayName = 'ExamplePackLink'; interface OsqueryPackUploaderProps { - onChange: (payload: Record) => void; + onChange: (payload: Record, packName: string) => void; } const OsqueryPackUploaderComponent: React.FC = ({ onChange }) => { @@ -61,12 +61,7 @@ const OsqueryPackUploaderComponent: React.FC = ({ onCh return; } - const queriesJSON = mapKeys( - parsedContent?.queries, - (value, key) => `pack_${packName.current}_${key}` - ); - - onChange(queriesJSON); + onChange(parsedContent?.queries, packName.current); // @ts-expect-error update types filePickerRef.current?.removeFiles(new Event('fake')); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx index 28628f5ebd9a..079b9ddacc9d 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui'; import { produce } from 'immer'; import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { satisfies } from 'semver'; import { OsqueryManagerPackagePolicyInputStream, @@ -23,6 +24,7 @@ import { OsqueryPackUploader } from './pack_uploader'; import { getSupportedPlatforms } from '../queries/platforms/helpers'; interface QueriesFieldProps { + handleNameChange: (name: string) => void; field: FieldHook; integrationPackageVersion?: string | undefined; scheduledQueryGroupId: string; @@ -82,6 +84,7 @@ const getNewStream = (payload: GetNewStreamProps) => const QueriesFieldComponent: React.FC = ({ field, + handleNameChange, integrationPackageVersion, scheduledQueryGroupId, }) => { @@ -208,13 +211,18 @@ const QueriesFieldComponent: React.FC = ({ }, [setValue, tableSelectedItems]); const handlePackUpload = useCallback( - (newQueries) => { + (newQueries, packName) => { + /* Osquery scheduled packs are supported since osquery_manager@0.5.0 */ + const isOsqueryPackSupported = integrationPackageVersion + ? satisfies(integrationPackageVersion, '>=0.5.0') + : false; + setValue( produce((draft) => { forEach(newQueries, (newQuery, newQueryId) => { draft[0].streams.push( getNewStream({ - id: newQueryId, + id: isOsqueryPackSupported ? newQueryId : `pack_${packName}_${newQueryId}`, interval: newQuery.interval, query: newQuery.query, version: newQuery.version, @@ -227,8 +235,12 @@ const QueriesFieldComponent: React.FC = ({ return draft; }) ); + + if (isOsqueryPackSupported) { + handleNameChange(packName); + } }, - [scheduledQueryGroupId, setValue] + [handleNameChange, integrationPackageVersion, scheduledQueryGroupId, setValue] ); const tableData = useMemo(() => (field.value.length ? field.value[0].streams : []), [ @@ -277,7 +289,6 @@ const QueriesFieldComponent: React.FC = ({ {field.value && field.value[0].streams?.length ? ( svg { + padding: 0 6px !important; + } `; const StyledFieldSpan = styled.span` @@ -88,7 +91,15 @@ const StyledFieldSpan = styled.span` // align the icon to the inputs const StyledButtonWrapper = styled.div` - margin-top: 32px; + margin-top: 30px; +`; + +const ECSFieldColumn = styled(EuiFlexGroup)` + max-width: 100%; +`; + +const ECSFieldWrapper = styled(EuiFlexItem)` + max-width: calc(100% - 66px); `; const singleSelection = { asPlainText: true }; @@ -163,7 +174,7 @@ export const ECSComboboxField: React.FC = ({ size="l" type={ // @ts-expect-error update types - selectedOptions[0]?.value?.type === 'keyword' ? 'string' : selectedOptions[0]?.value?.type + typeMap[selectedOptions[0]?.value?.type] ?? selectedOptions[0]?.value?.type } /> ), @@ -220,7 +231,7 @@ export const OsqueryColumnField: React.FC = ({ idAria, ...rest }) => { - const { setErrors, setValue } = field; + const { setValue } = field; const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]); const [selectedOptions, setSelected] = useState< @@ -250,25 +261,6 @@ export const OsqueryColumnField: React.FC = ({ [] ); - const onCreateOsqueryOption = useCallback( - (searchValue = []) => { - const normalizedSearchValue = searchValue.trim().toLowerCase(); - - if (!normalizedSearchValue) { - return; - } - - const newOption = { - label: searchValue, - }; - - // Select the option. - setSelected([newOption]); - setValue(newOption.label); - }, - [setValue, setSelected] - ); - const handleChange = useCallback( (newSelectedOptions) => { setSelected(newSelectedOptions); @@ -285,7 +277,7 @@ export const OsqueryColumnField: React.FC = ({ return selectedOption ? [selectedOption] : [{ label: field.value }]; }); - }, [euiFieldProps?.options, setSelected, field.value, setErrors]); + }, [euiFieldProps?.options, setSelected, field.value]); return ( = ({ singleSelection={singleSelection} selectedOptions={selectedOptions} onChange={handleChange} - onCreateOption={onCreateOsqueryOption} renderOption={renderOsqueryOption} rowHeight={32} isClearable @@ -513,7 +504,7 @@ export const ECSMappingEditorForm = forwardRef - + - + - + - + {defaultValue ? ( @@ -564,7 +555,7 @@ export const ECSMappingEditorForm = forwardRef - + @@ -634,6 +625,11 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit return currentValue; } + const tablesOrderMap = ast?.from?.reduce((acc, table, index) => { + acc[table.as ?? table.table] = index; + return acc; + }, {}); + const astOsqueryTables: Record = ast?.from?.reduce((acc, table) => { const osqueryTable = find(osquerySchema, ['name', table.table]); @@ -665,6 +661,7 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit name: osqueryColumn.name, description: osqueryColumn.description, table: tableName, + tableOrder: tablesOrderMap[tableName], suggestion_label: osqueryColumn.name, }, })); @@ -678,13 +675,14 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit isArray(ast?.columns) && ast?.columns ?.map((column) => { - if (column.expr.column === '*') { + if (column.expr.column === '*' && astOsqueryTables[column.expr.table]) { return astOsqueryTables[column.expr.table].map((osqueryColumn) => ({ label: osqueryColumn.name, value: { name: osqueryColumn.name, description: osqueryColumn.description, table: column.expr.table, + tableOrder: tablesOrderMap[column.expr.table], suggestion_label: `${osqueryColumn.name}`, }, })); @@ -706,6 +704,7 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit name: osqueryColumn.name, description: osqueryColumn.description, table: column.expr.table, + tableOrder: tablesOrderMap[column.expr.table], suggestion_label: `${label}`, }, }, @@ -717,8 +716,12 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit }) .flat(); - // @ts-expect-error update types - return sortBy(suggestions, 'value.suggestion_label'); + // Remove column duplicates by keeping the column from the table that appears last in the query + return sortedUniqBy( + // @ts-expect-error update types + orderBy(suggestions, ['value.suggestion_label', 'value.tableOrder'], ['asc', 'desc']), + 'label' + ); }); }, [query]); @@ -766,6 +769,10 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit return draft; }) ); + + if (formRefs.current[key]) { + delete formRefs.current[key]; + } } }, [setValue] diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx index f0211b049f57..cae9711694f2 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx @@ -13,7 +13,6 @@ import { EuiFlyoutBody, EuiFlyoutHeader, EuiFlyoutFooter, - EuiPortal, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, @@ -117,138 +116,145 @@ const QueryFlyoutComponent: React.FC = ({ [isFieldSupported, setFieldValue, reset] ); + /* Avoids accidental closing of the flyout when the user clicks outside of the flyout */ + const maskProps = useMemo(() => ({ onClick: () => ({}) }), []); + return ( - - - - -

- {isEditMode ? ( - - ) : ( - - )} -

-
-
- -
- {!isEditMode ? ( - <> - - - - ) : null} - - - - - - - - - - - - - - } - // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop - euiFieldProps={{ - isDisabled: !isFieldSupported, - noSuggestions: false, - singleSelection: { asPlainText: true }, - placeholder: i18n.translate( - 'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', - { - defaultMessage: 'ALL', - } - ), - options: ALL_OSQUERY_VERSIONS_OPTIONS, - onCreateOption: undefined, - }} - /> - - - - - - - - - - - - - {!isFieldSupported ? ( - - } - iconType="pin" - > - - - - - - + + + +

+ {isEditMode ? ( + + ) : ( + + )} +

+
+
+ +
+ {!isEditMode ? ( + <> + + + ) : null} - - - - - - - + + + + + + + + + + + + + + } + // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop + euiFieldProps={{ + isDisabled: !isFieldSupported, + noSuggestions: false, + singleSelection: { asPlainText: true }, + placeholder: i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', + { + defaultMessage: 'ALL', + } + ), + options: ALL_OSQUERY_VERSIONS_OPTIONS, + onCreateOption: undefined, + }} + /> - - - - + + - - - + + + + + + + + {!isFieldSupported ? ( + + } + iconType="pin" + > + + + + + + + ) : null} +
+ + + + + + + + + + + + + + +
); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx index 8337d0f91fbf..dc206a6b104d 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx @@ -63,7 +63,23 @@ export const useScheduledQueryGroupQueryForm = ({ options: { stripEmptyFields: false, }, - defaultValue, + defaultValue: defaultValue || { + id: { + type: 'text', + value: '', + }, + query: { + type: 'text', + value: '', + }, + interval: { + type: 'integer', + value: '3600', + }, + ecs_mapping: { + value: {}, + }, + }, // @ts-expect-error update types serializer: (payload) => produce(payload, (draft) => { diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_errors_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_errors_table.tsx new file mode 100644 index 000000000000..71ae34660322 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_errors_table.tsx @@ -0,0 +1,146 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiInMemoryTable, EuiCodeBlock, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { encode } from 'rison-node'; +import { stringify } from 'querystring'; + +import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; +import { AgentIdToName } from '../agents/agent_id_to_name'; +import { useScheduledQueryGroupQueryErrors } from './use_scheduled_query_group_query_errors'; + +const VIEW_IN_LOGS = i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.viewLogsErrorsActionAriaLabel', + { + defaultMessage: 'View in Logs', + } +); + +interface ViewErrorsInLogsActionProps { + actionId: string; + agentId: string; + timestamp?: string; +} + +const ViewErrorsInLogsActionComponent: React.FC = ({ + actionId, + agentId, + timestamp, +}) => { + const navigateToApp = useKibana().services.application.navigateToApp; + + const handleClick = useCallback( + (event) => { + const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event)); + + event.preventDefault(); + const queryString = stringify({ + logPosition: encode({ + end: timestamp, + streamLive: false, + }), + logFilter: encode({ + expression: `elastic_agent.id:${agentId} and (data_stream.dataset:elastic_agent or data_stream.dataset:elastic_agent.osquerybeat) and "${actionId}"`, + kind: 'kuery', + }), + }); + + navigateToApp('logs', { + path: `stream?${queryString}`, + openInNewTab, + }); + }, + [actionId, agentId, navigateToApp, timestamp] + ); + + return ( + + + + ); +}; + +export const ViewErrorsInLogsAction = React.memo(ViewErrorsInLogsActionComponent); + +interface ScheduledQueryErrorsTableProps { + actionId: string; + agentIds?: string[]; + interval: number; +} + +const renderErrorMessage = (error: string) => ( + + {error} + +); + +const ScheduledQueryErrorsTableComponent: React.FC = ({ + actionId, + agentIds, + interval, +}) => { + const { data: lastErrorsData } = useScheduledQueryGroupQueryErrors({ + actionId, + agentIds, + interval, + }); + + const renderAgentIdColumn = useCallback((agentId) => , []); + + const renderLogsErrorsAction = useCallback( + (item) => ( + + ), + [actionId] + ); + + const columns = useMemo( + () => [ + { + field: 'fields.@timestamp', + name: '@timestamp', + width: '220px', + }, + { + field: 'fields["elastic_agent.id"][0]', + name: i18n.translate('xpack.osquery.scheduledQueryErrorsTable.agentIdColumnTitle', { + defaultMessage: 'Agent Id', + }), + truncateText: true, + render: renderAgentIdColumn, + width: '15%', + }, + { + field: 'fields.message[0]', + name: i18n.translate('xpack.osquery.scheduledQueryErrorsTable.errorColumnTitle', { + defaultMessage: 'Error', + }), + render: renderErrorMessage, + }, + { + width: '50px', + actions: [ + { + render: renderLogsErrorsAction, + }, + ], + }, + ], + [renderAgentIdColumn, renderLogsErrorsAction] + ); + + // @ts-expect-error update types + return ; +}; + +export const ScheduledQueryErrorsTable = React.memo(ScheduledQueryErrorsTableComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_status_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_status_table.tsx new file mode 100644 index 000000000000..fe54a46df8c0 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_status_table.tsx @@ -0,0 +1,644 @@ +/* + * 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 { get } from 'lodash/fp'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { + EuiBasicTable, + EuiButtonEmpty, + EuiCodeBlock, + EuiButtonIcon, + EuiToolTip, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiNotificationBadge, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import moment from 'moment-timezone'; +import { + TypedLensByValueInput, + PersistedIndexPatternLayer, + PieVisualizationState, +} from '../../../lens/public'; +import { FilterStateStore } from '../../../../../src/plugins/data/common'; +import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; +import { OsqueryManagerPackagePolicyInputStream } from '../../common/types'; +import { ScheduledQueryErrorsTable } from './scheduled_query_errors_table'; +import { useScheduledQueryGroupQueryLastResults } from './use_scheduled_query_group_query_last_results'; +import { useScheduledQueryGroupQueryErrors } from './use_scheduled_query_group_query_errors'; + +const VIEW_IN_DISCOVER = i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel', + { + defaultMessage: 'View in Discover', + } +); + +const VIEW_IN_LENS = i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel', + { + defaultMessage: 'View in Lens', + } +); + +export enum ViewResultsActionButtonType { + icon = 'icon', + button = 'button', +} + +interface ViewResultsInDiscoverActionProps { + actionId: string; + agentIds?: string[]; + buttonType: ViewResultsActionButtonType; + endDate?: string; + startDate?: string; +} + +function getLensAttributes( + actionId: string, + agentIds?: string[] +): TypedLensByValueInput['attributes'] { + const dataLayer: PersistedIndexPatternLayer = { + columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'], + columns: { + '8690befd-fd69-4246-af4a-dd485d2a3b38': { + sourceField: 'type', + isBucketed: true, + dataType: 'string', + scale: 'ordinal', + operationType: 'terms', + label: 'Top values of type', + params: { + otherBucket: true, + size: 5, + missingBucket: false, + orderBy: { + columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed', + type: 'column', + }, + orderDirection: 'desc', + }, + }, + 'ed999e9d-204c-465b-897f-fe1a125b39ed': { + sourceField: 'Records', + isBucketed: false, + dataType: 'number', + scale: 'ratio', + operationType: 'count', + label: 'Count of records', + }, + }, + incompleteColumns: {}, + }; + + const xyConfig: PieVisualizationState = { + shape: 'pie', + layers: [ + { + layerType: 'data', + legendDisplay: 'default', + nestedLegend: false, + layerId: 'layer1', + metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed', + numberDisplay: 'percent', + groups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'], + categoryDisplay: 'default', + }, + ], + }; + + const agentIdsQuery = { + bool: { + minimum_should_match: 1, + should: agentIds?.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })), + }, + }; + + return { + visualizationType: 'lnsPie', + title: `Action ${actionId} results`, + references: [ + { + id: 'logs-*', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'logs-*', + name: 'indexpattern-datasource-layer-layer1', + type: 'index-pattern', + }, + { + name: 'filter-index-pattern-0', + id: 'logs-*', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: dataLayer, + }, + }, + }, + filters: [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + indexRefName: 'filter-index-pattern-0', + negate: false, + alias: null, + disabled: false, + params: { + query: actionId, + }, + type: 'phrase', + key: 'action_id', + }, + query: { + match_phrase: { + action_id: actionId, + }, + }, + }, + ...(agentIdsQuery + ? [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: 'agent IDs', + disabled: false, + indexRefName: 'filter-index-pattern-0', + key: 'query', + negate: false, + type: 'custom', + value: JSON.stringify(agentIdsQuery), + }, + query: agentIdsQuery, + }, + ] + : []), + ], + query: { language: 'kuery', query: '' }, + visualization: xyConfig, + }, + }; +} + +const ViewResultsInLensActionComponent: React.FC = ({ + actionId, + agentIds, + buttonType, + endDate, + startDate, +}) => { + const lensService = useKibana().services.lens; + + const handleClick = useCallback( + (event) => { + const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event)); + + event.preventDefault(); + + lensService?.navigateToPrefilledEditor( + { + id: '', + timeRange: { + from: startDate ?? 'now-1d', + to: endDate ?? 'now', + mode: startDate || endDate ? 'absolute' : 'relative', + }, + attributes: getLensAttributes(actionId, agentIds), + }, + { + openInNewTab, + } + ); + }, + [actionId, agentIds, endDate, lensService, startDate] + ); + + if (buttonType === ViewResultsActionButtonType.button) { + return ( + + {VIEW_IN_LENS} + + ); + } + + return ( + + + + ); +}; + +export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent); + +const ViewResultsInDiscoverActionComponent: React.FC = ({ + actionId, + agentIds, + buttonType, + endDate, + startDate, +}) => { + const urlGenerator = useKibana().services.discover?.urlGenerator; + const [discoverUrl, setDiscoverUrl] = useState(''); + + useEffect(() => { + const getDiscoverUrl = async () => { + if (!urlGenerator?.createUrl) return; + + const agentIdsQuery = agentIds?.length + ? { + bool: { + minimum_should_match: 1, + should: agentIds.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })), + }, + } + : null; + + const newUrl = await urlGenerator.createUrl({ + indexPatternId: 'logs-*', + filters: [ + { + meta: { + index: 'logs-*', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'action_id', + params: { query: actionId }, + }, + query: { match_phrase: { action_id: actionId } }, + $state: { store: FilterStateStore.APP_STATE }, + }, + ...(agentIdsQuery + ? [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: 'agent IDs', + disabled: false, + index: 'logs-*', + key: 'query', + negate: false, + type: 'custom', + value: JSON.stringify(agentIdsQuery), + }, + query: agentIdsQuery, + }, + ] + : []), + ], + refreshInterval: { + pause: true, + value: 0, + }, + timeRange: + startDate && endDate + ? { + to: endDate, + from: startDate, + mode: 'absolute', + } + : { + to: 'now', + from: 'now-1d', + mode: 'relative', + }, + }); + setDiscoverUrl(newUrl); + }; + getDiscoverUrl(); + }, [actionId, agentIds, endDate, startDate, urlGenerator]); + + if (buttonType === ViewResultsActionButtonType.button) { + return ( + + {VIEW_IN_DISCOVER} + + ); + } + + return ( + + + + ); +}; + +export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent); + +interface ScheduledQueryExpandedContentProps { + actionId: string; + agentIds?: string[]; + interval: number; +} + +const ScheduledQueryExpandedContent = React.memo( + ({ actionId, agentIds, interval }) => ( + + + + + + + + + + ) +); + +ScheduledQueryExpandedContent.displayName = 'ScheduledQueryExpandedContent'; + +interface ScheduledQueryLastResultsProps { + actionId: string; + agentIds: string[]; + queryId: string; + interval: number; + toggleErrors: (payload: { queryId: string; interval: number }) => void; + expanded: boolean; +} + +const ScheduledQueryLastResults: React.FC = ({ + actionId, + agentIds, + queryId, + interval, + toggleErrors, + expanded, +}) => { + const { data: lastResultsData, isFetched } = useScheduledQueryGroupQueryLastResults({ + actionId, + agentIds, + interval, + }); + + const { data: errorsData, isFetched: errorsFetched } = useScheduledQueryGroupQueryErrors({ + actionId, + agentIds, + interval, + }); + + const handleErrorsToggle = useCallback(() => toggleErrors({ queryId, interval }), [ + queryId, + interval, + toggleErrors, + ]); + + if (!isFetched || !errorsFetched) { + return ; + } + + if (!lastResultsData) { + return <>{'-'}; + } + + return ( + + + {lastResultsData.first_event_ingested_time?.value ? ( + + <>{moment(lastResultsData.first_event_ingested_time?.value).fromNow()} + + ) : ( + '-' + )} + + + + + + {lastResultsData?.doc_count ?? 0} + + + {'Documents'} + + + + + + + + {lastResultsData?.unique_agents?.value ?? 0} + + + {'Agents'} + + + + + + + + {errorsData?.total ?? 0} + + + + {'Errors'} + + + + + + + + ); +}; + +const getPackActionId = (actionId: string, packName: string) => `pack_${packName}_${actionId}`; + +interface ScheduledQueryGroupQueriesStatusTableProps { + agentIds?: string[]; + data: OsqueryManagerPackagePolicyInputStream[]; + scheduledQueryGroupName: string; +} + +const ScheduledQueryGroupQueriesStatusTableComponent: React.FC = ({ + agentIds, + data, + scheduledQueryGroupName, +}) => { + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record> + >({}); + + const renderQueryColumn = useCallback( + (query: string) => ( + + {query} + + ), + [] + ); + + const toggleErrors = useCallback( + ({ queryId, interval }: { queryId: string; interval: number }) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[queryId]) { + delete itemIdToExpandedRowMapValues[queryId]; + } else { + itemIdToExpandedRowMapValues[queryId] = ( + + ); + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }, + [agentIds, itemIdToExpandedRowMap, scheduledQueryGroupName] + ); + + const renderLastResultsColumn = useCallback( + (item) => ( + + ), + [agentIds, itemIdToExpandedRowMap, scheduledQueryGroupName, toggleErrors] + ); + + const renderDiscoverResultsAction = useCallback( + (item) => ( + + ), + [agentIds, scheduledQueryGroupName] + ); + + const renderLensResultsAction = useCallback( + (item) => ( + + ), + [agentIds] + ); + + const getItemId = useCallback( + (item: OsqueryManagerPackagePolicyInputStream) => get('vars.id.value', item), + [] + ); + + const columns = useMemo( + () => [ + { + field: 'vars.id.value', + name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.idColumnTitle', { + defaultMessage: 'ID', + }), + width: '15%', + }, + { + field: 'vars.interval.value', + name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.intervalColumnTitle', { + defaultMessage: 'Interval (s)', + }), + width: '80px', + }, + { + field: 'vars.query.value', + name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.queryColumnTitle', { + defaultMessage: 'Query', + }), + render: renderQueryColumn, + width: '20%', + }, + { + name: i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.lastResultsColumnTitle', + { + defaultMessage: 'Last results', + } + ), + render: renderLastResultsColumn, + }, + { + name: i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.viewResultsColumnTitle', + { + defaultMessage: 'View results', + } + ), + width: '90px', + actions: [ + { + render: renderDiscoverResultsAction, + }, + { + render: renderLensResultsAction, + }, + ], + }, + ], + [ + renderQueryColumn, + renderLastResultsColumn, + renderDiscoverResultsAction, + renderLensResultsAction, + ] + ); + + const sorting = useMemo( + () => ({ + sort: { + field: 'vars.id.value' as keyof OsqueryManagerPackagePolicyInputStream, + direction: 'asc' as const, + }, + }), + [] + ); + + return ( + + items={data} + itemId={getItemId} + columns={columns} + sorting={sorting} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + isExpandable + /> + ); +}; + +export const ScheduledQueryGroupQueriesStatusTable = React.memo( + ScheduledQueryGroupQueriesStatusTableComponent +); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx index 1ab87949e349..fb3839a71672 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx @@ -6,288 +6,15 @@ */ import { get } from 'lodash/fp'; -import React, { useCallback, useEffect, useState, useMemo } from 'react'; -import { - EuiBasicTable, - EuiButtonEmpty, - EuiCodeBlock, - EuiButtonIcon, - EuiToolTip, -} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { EuiBasicTable, EuiCodeBlock, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - TypedLensByValueInput, - PersistedIndexPatternLayer, - PieVisualizationState, -} from '../../../lens/public'; -import { FilterStateStore } from '../../../../../src/plugins/data/common'; -import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; import { PlatformIcons } from './queries/platforms'; import { OsqueryManagerPackagePolicyInputStream } from '../../common/types'; -const VIEW_IN_DISCOVER = i18n.translate( - 'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel', - { - defaultMessage: 'View in Discover', - } -); - -const VIEW_IN_LENS = i18n.translate( - 'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel', - { - defaultMessage: 'View in Lens', - } -); - -export enum ViewResultsActionButtonType { - icon = 'icon', - button = 'button', -} - -interface ViewResultsInDiscoverActionProps { - actionId: string; - buttonType: ViewResultsActionButtonType; - endDate?: string; - startDate?: string; -} - -function getLensAttributes(actionId: string): TypedLensByValueInput['attributes'] { - const dataLayer: PersistedIndexPatternLayer = { - columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'], - columns: { - '8690befd-fd69-4246-af4a-dd485d2a3b38': { - sourceField: 'type', - isBucketed: true, - dataType: 'string', - scale: 'ordinal', - operationType: 'terms', - label: 'Top values of type', - params: { - otherBucket: true, - size: 5, - missingBucket: false, - orderBy: { - columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed', - type: 'column', - }, - orderDirection: 'desc', - }, - }, - 'ed999e9d-204c-465b-897f-fe1a125b39ed': { - sourceField: 'Records', - isBucketed: false, - dataType: 'number', - scale: 'ratio', - operationType: 'count', - label: 'Count of records', - }, - }, - incompleteColumns: {}, - }; - - const xyConfig: PieVisualizationState = { - shape: 'pie', - layers: [ - { - legendDisplay: 'default', - nestedLegend: false, - layerId: 'layer1', - layerType: 'data', - metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed', - numberDisplay: 'percent', - groups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'], - categoryDisplay: 'default', - }, - ], - }; - - return { - visualizationType: 'lnsPie', - title: `Action ${actionId} results`, - references: [ - { - id: 'logs-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'logs-*', - name: 'indexpattern-datasource-layer-layer1', - type: 'index-pattern', - }, - { - name: 'filter-index-pattern-0', - id: 'logs-*', - type: 'index-pattern', - }, - ], - state: { - datasourceStates: { - indexpattern: { - layers: { - layer1: dataLayer, - }, - }, - }, - filters: [ - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - indexRefName: 'filter-index-pattern-0', - negate: false, - alias: null, - disabled: false, - params: { - query: actionId, - }, - type: 'phrase', - key: 'action_id', - }, - query: { - match_phrase: { - action_id: actionId, - }, - }, - }, - ], - query: { language: 'kuery', query: '' }, - visualization: xyConfig, - }, - }; -} - -const ViewResultsInLensActionComponent: React.FC = ({ - actionId, - buttonType, - endDate, - startDate, -}) => { - const lensService = useKibana().services.lens; - - const handleClick = useCallback( - (event) => { - const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event)); - - event.preventDefault(); - - lensService?.navigateToPrefilledEditor( - { - id: '', - timeRange: { - from: startDate ?? 'now-1d', - to: endDate ?? 'now', - mode: startDate || endDate ? 'absolute' : 'relative', - }, - attributes: getLensAttributes(actionId), - }, - { - openInNewTab, - } - ); - }, - [actionId, endDate, lensService, startDate] - ); - - if (buttonType === ViewResultsActionButtonType.button) { - return ( - - {VIEW_IN_LENS} - - ); - } - - return ( - - - - ); -}; - -export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent); - -const ViewResultsInDiscoverActionComponent: React.FC = ({ - actionId, - buttonType, - endDate, - startDate, -}) => { - const urlGenerator = useKibana().services.discover?.urlGenerator; - const [discoverUrl, setDiscoverUrl] = useState(''); - - useEffect(() => { - const getDiscoverUrl = async () => { - if (!urlGenerator?.createUrl) return; - - const newUrl = await urlGenerator.createUrl({ - indexPatternId: 'logs-*', - filters: [ - { - meta: { - index: 'logs-*', - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'action_id', - params: { query: actionId }, - }, - query: { match_phrase: { action_id: actionId } }, - $state: { store: FilterStateStore.APP_STATE }, - }, - ], - refreshInterval: { - pause: true, - value: 0, - }, - timeRange: - startDate && endDate - ? { - to: endDate, - from: startDate, - mode: 'absolute', - } - : { - to: 'now', - from: 'now-1d', - mode: 'relative', - }, - }); - setDiscoverUrl(newUrl); - }; - getDiscoverUrl(); - }, [actionId, endDate, startDate, urlGenerator]); - - if (buttonType === ViewResultsActionButtonType.button) { - return ( - - {VIEW_IN_DISCOVER} - - ); - } - - return ( - - - - ); -}; - -export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent); - interface ScheduledQueryGroupQueriesTableProps { data: OsqueryManagerPackagePolicyInputStream[]; - editMode?: boolean; onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; selectedItems?: OsqueryManagerPackagePolicyInputStream[]; @@ -296,7 +23,6 @@ interface ScheduledQueryGroupQueriesTableProps { const ScheduledQueryGroupQueriesTableComponent: React.FC = ({ data, - editMode = false, onDeleteClick, onEditClick, selectedItems, @@ -370,26 +96,6 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC ( - - ), - [] - ); - - const renderLensResultsAction = useCallback( - (item) => ( - - ), - [] - ); - const columns = useMemo( () => [ { @@ -428,42 +134,23 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC ); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_groups_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_groups_table.tsx index 391e20c63653..82fca0020d2b 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_groups_table.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_groups_table.tsx @@ -6,7 +6,7 @@ */ import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; -import moment from 'moment'; +import moment from 'moment-timezone'; import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts index d6bad026a53d..6b6acc30036c 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts @@ -33,6 +33,10 @@ export const useScheduledQueryGroup = ({ keepPreviousData: true, enabled: !skip || !scheduledQueryGroupId, select: (response) => response.item, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + staleTime: Infinity, } ); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_errors.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_errors.ts new file mode 100644 index 000000000000..31d98ee6204e --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_errors.ts @@ -0,0 +1,91 @@ +/* + * 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 { useQuery } from 'react-query'; +import { SortDirection } from '../../../../../src/plugins/data/common'; + +import { useKibana } from '../common/lib/kibana'; + +interface UseScheduledQueryGroupQueryErrorsProps { + actionId: string; + agentIds?: string[]; + interval: number; + skip?: boolean; +} + +export const useScheduledQueryGroupQueryErrors = ({ + actionId, + agentIds, + interval, + skip = false, +}: UseScheduledQueryGroupQueryErrorsProps) => { + const data = useKibana().services.data; + + return useQuery( + ['scheduledQueryErrors', { actionId, interval }], + async () => { + const indexPattern = await data.indexPatterns.find('logs-*'); + const searchSource = await data.search.searchSource.create({ + index: indexPattern[0], + fields: ['*'], + sort: [ + { + '@timestamp': SortDirection.desc, + }, + ], + query: { + // @ts-expect-error update types + bool: { + should: agentIds?.map((agentId) => ({ + match_phrase: { + 'elastic_agent.id': agentId, + }, + })), + minimum_should_match: 1, + filter: [ + { + match_phrase: { + message: 'Error', + }, + }, + { + match_phrase: { + 'data_stream.dataset': 'elastic_agent.osquerybeat', + }, + }, + { + match_phrase: { + message: actionId, + }, + }, + { + range: { + '@timestamp': { + gte: `now-${interval * 2}s`, + lte: 'now', + }, + }, + }, + ], + }, + }, + size: 1000, + }); + + return searchSource.fetch$().toPromise(); + }, + { + keepPreviousData: true, + enabled: !!(!skip && actionId && interval && agentIds?.length), + select: (response) => response.rawResponse.hits ?? [], + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + staleTime: Infinity, + } + ); +}; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts new file mode 100644 index 000000000000..21117a4a0f83 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts @@ -0,0 +1,88 @@ +/* + * 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 { useQuery } from 'react-query'; + +import { useKibana } from '../common/lib/kibana'; + +interface UseScheduledQueryGroupQueryLastResultsProps { + actionId: string; + agentIds?: string[]; + interval: number; + skip?: boolean; +} + +export const useScheduledQueryGroupQueryLastResults = ({ + actionId, + agentIds, + interval, + skip = false, +}: UseScheduledQueryGroupQueryLastResultsProps) => { + const data = useKibana().services.data; + + return useQuery( + ['scheduledQueryLastResults', { actionId }], + async () => { + const indexPattern = await data.indexPatterns.find('logs-*'); + const searchSource = await data.search.searchSource.create({ + index: indexPattern[0], + size: 0, + aggs: { + runs: { + terms: { + field: 'response_id', + order: { first_event_ingested_time: 'desc' }, + size: 1, + }, + aggs: { + first_event_ingested_time: { min: { field: '@timestamp' } }, + unique_agents: { cardinality: { field: 'agent.id' } }, + }, + }, + }, + query: { + // @ts-expect-error update types + bool: { + should: agentIds?.map((agentId) => ({ + match_phrase: { + 'agent.id': agentId, + }, + })), + minimum_should_match: 1, + filter: [ + { + match_phrase: { + action_id: actionId, + }, + }, + { + range: { + '@timestamp': { + gte: `now-${interval * 2}s`, + lte: 'now', + }, + }, + }, + ], + }, + }, + }); + + return searchSource.fetch$().toPromise(); + }, + { + keepPreviousData: true, + enabled: !!(!skip && actionId && interval && agentIds?.length), + // @ts-expect-error update types + select: (response) => response.rawResponse.aggregations?.runs?.buckets[0] ?? [], + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + staleTime: Infinity, + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts index 79c1149675b0..aed6c34d33f9 100644 --- a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts +++ b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts @@ -6,7 +6,7 @@ */ import uuid from 'uuid'; -import moment from 'moment'; +import moment from 'moment-timezone'; import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; diff --git a/yarn.lock b/yarn.lock index 171517cd1d6c..a4d217c0eea2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23291,10 +23291,10 @@ react-popper@^2.2.4: react-fast-compare "^3.0.1" warning "^4.0.2" -react-query@^3.18.1: - version "3.18.1" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.18.1.tgz#893b5475a7b4add099e007105317446f7a2cd310" - integrity sha512-17lv3pQxU9n+cB5acUv0/cxNTjo9q8G+RsedC6Ax4V9D8xEM7Q5xf9xAbCPdEhDrrnzPjTls9fQEABKRSi7OJA== +react-query@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.21.0.tgz#2e099a7906c38eeeb750e8b9b12121a21fa8d9ef" + integrity sha512-5rY5J8OD9f4EdkytjSsdCO+pqbJWKwSIMETfh/UyxqyjLURHE0IhlB+IPNPrzzu/dzK0rRxi5p0IkcCdSfizDQ== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1"