From 3f39f5e27558f6facdd65d48fbe3de9965c663f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Mon, 3 May 2021 18:50:07 +0200 Subject: [PATCH] [Osquery] Fix Live query form errors handling (#99015) --- .../search_strategy/osquery/results/index.ts | 5 +- x-pack/plugins/osquery/kibana.json | 3 +- .../action_results/action_results_summary.tsx | 19 +- .../action_results/use_action_results.ts | 6 +- ...managed_policy_create_import_extension.tsx | 18 +- .../public/live_queries/form/index.tsx | 9 +- .../osquery/public/queries/edit/tabs.tsx | 20 +- .../osquery/public/results/results_table.tsx | 143 ++++---- .../osquery/public/results/use_all_results.ts | 13 +- .../live_queries/details/actions_menu.tsx | 68 ---- .../routes/live_queries/details/index.tsx | 17 +- .../details/actions_menu.tsx | 69 ---- .../scheduled_query_groups/details/index.tsx | 12 +- .../active_state_switch.tsx | 4 + .../form/add_query_flyout.tsx | 2 +- .../form/edit_query_flyout.tsx | 2 +- .../form/queries_field.tsx | 48 ++- .../scheduled_query_group_queries_table.tsx | 323 ++++++++++++++++-- x-pack/plugins/osquery/public/types.ts | 2 + .../osquery/server/lib/parse_agent_groups.ts | 8 +- .../routes/action/create_action_route.ts | 2 +- .../factory/results/query.all_results.dsl.ts | 11 +- 22 files changed, 506 insertions(+), 298 deletions(-) delete mode 100644 x-pack/plugins/osquery/public/routes/live_queries/details/actions_menu.tsx delete mode 100644 x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/actions_menu.tsx diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts index 035774aaffe3..ca85f4342c9c 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts @@ -8,7 +8,7 @@ import { estypes } from '@elastic/elasticsearch'; import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { Inspect, Maybe, PageInfoPaginated } from '../../common'; +import { Inspect, Maybe, PageInfoPaginated, SortField } from '../../common'; import { RequestOptionsPaginated } from '../..'; export type ResultEdges = estypes.SearchResponse['hits']['hits']; @@ -20,7 +20,8 @@ export interface ResultsStrategyResponse extends IEsSearchResponse { inspect?: Maybe; } -export interface ResultsRequestOptions extends RequestOptionsPaginated { +export interface ResultsRequestOptions extends Omit { actionId: string; agentId?: string; + sort: SortField[]; } diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json index 17d74b124f45..2c5c0708dc68 100644 --- a/x-pack/plugins/osquery/kibana.json +++ b/x-pack/plugins/osquery/kibana.json @@ -9,7 +9,8 @@ "id": "osquery", "kibanaVersion": "kibana", "optionalPlugins": [ - "home" + "home", + "lens" ], "requiredBundles": [ "esUiShared", diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index 9542286c860f..ffa86c547656 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -18,8 +18,10 @@ import { EuiDescriptionList, EuiInMemoryTable, EuiCodeBlock, + EuiProgress, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; import { pagePathGetters } from '../../../fleet/public'; import { useActionResults } from './use_action_results'; @@ -27,6 +29,10 @@ import { useAllResults } from '../results/use_all_results'; import { Direction } from '../../common/search_strategy'; import { useKibana } from '../common/lib/kibana'; +const StyledEuiCard = styled(EuiCard)` + position: relative; +`; + interface ActionResultsSummaryProps { actionId: string; agentIds?: string[]; @@ -66,8 +72,12 @@ const ActionResultsSummaryComponent: React.FC = ({ actionId, activePage: pageIndex, limit: pageSize, - direction: Direction.asc, - sortField: '@timestamp', + sort: [ + { + field: '@timestamp', + direction: Direction.asc, + }, + ], isLive, }); @@ -215,14 +225,15 @@ const ActionResultsSummaryComponent: React.FC = ({ <> - + + {notRespondedCount ? : null} - + diff --git a/x-pack/plugins/osquery/public/action_results/use_action_results.ts b/x-pack/plugins/osquery/public/action_results/use_action_results.ts index 1f6da0b3a2a0..d686a5a4364e 100644 --- a/x-pack/plugins/osquery/public/action_results/use_action_results.ts +++ b/x-pack/plugins/osquery/public/action_results/use_action_results.ts @@ -15,8 +15,8 @@ import { ResultEdges, PageInfoPaginated, OsqueryQueries, - ResultsRequestOptions, - ResultsStrategyResponse, + ActionResultsRequestOptions, + ActionResultsStrategyResponse, Direction, } from '../../common/search_strategy'; import { ESTermQuery } from '../../common/typed_json'; @@ -65,7 +65,7 @@ export const useActionResults = ({ ['actionResults', { actionId }], async () => { const responseData = await data.search - .search( + .search( { actionId, factoryQueryType: OsqueryQueries.actionResults, diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 3b99e1d46855..6dfbc086c394 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -8,7 +8,7 @@ import { filter } from 'lodash/fp'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui'; import React, { useEffect, useMemo, useState } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { produce } from 'immer'; import { i18n } from '@kbn/i18n'; @@ -45,7 +45,8 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< application: { getUrlForApp }, http, } = useKibana().services; - const { replace } = useHistory(); + const { state: locationState } = useLocation(); + const { replace, go } = useHistory(); const agentsLinkHref = useMemo(() => { if (!policy?.policy_id) return '#'; @@ -93,6 +94,16 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< } }, [editMode, http, policy?.policy_id, policyAgentsCount]); + useEffect(() => { + /* + in order to enable Osquery side nav we need to refresh the whole Kibana + TODO: Find a better solution + */ + if (editMode && locationState?.forceRefresh) { + go(0); + } + }, [editMode, go, locationState]); + useEffect(() => { /* by default Fleet set up streams with an empty scheduled query, @@ -124,6 +135,9 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< pagePathGetters.integration_policy_edit({ packagePolicyId: newPackagePolicy.id, }), + state: { + forceRefresh: true, + }, }, ], } as CreatePackagePolicyRouteState, diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 5d1b616c7d88..4cf2d4aa4fe9 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -35,7 +35,10 @@ const LiveQueryFormComponent: React.FC = ({ // onSubmit, onSuccess, }) => { - const { http } = useKibana().services; + const { + http, + notifications: { toasts }, + } = useKibana().services; const { data, @@ -51,6 +54,10 @@ const LiveQueryFormComponent: React.FC = ({ }), { onSuccess, + onError: (error) => { + // @ts-expect-error update types + toasts.addError(error, { title: error.body.error, toastMessage: error.body.message }); + }, } ); diff --git a/x-pack/plugins/osquery/public/queries/edit/tabs.tsx b/x-pack/plugins/osquery/public/queries/edit/tabs.tsx index f86762e76834..978c3f938f1d 100644 --- a/x-pack/plugins/osquery/public/queries/edit/tabs.tsx +++ b/x-pack/plugins/osquery/public/queries/edit/tabs.tsx @@ -15,9 +15,17 @@ interface ResultTabsProps { actionId: string; agentIds?: string[]; isLive?: boolean; + startDate?: string; + endDate?: string; } -const ResultTabsComponent: React.FC = ({ actionId, agentIds, isLive }) => { +const ResultTabsComponent: React.FC = ({ + actionId, + agentIds, + endDate, + isLive, + startDate, +}) => { const tabs = useMemo( () => [ { @@ -36,12 +44,18 @@ const ResultTabsComponent: React.FC = ({ actionId, agentIds, is content: ( <> - + ), }, ], - [actionId, agentIds, isLive] + [actionId, agentIds, endDate, isLive, startDate] ); return ( diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 8b613a336ae7..affc60084728 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -7,15 +7,13 @@ import { isEmpty, isEqual, keys, map } from 'lodash/fp'; import { + EuiCallOut, EuiDataGrid, EuiDataGridSorting, EuiDataGridProps, EuiDataGridColumn, EuiLink, - EuiTextColor, - EuiBasicTable, - EuiBasicTableColumn, - EuiSpacer, + EuiLoadingContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; @@ -26,6 +24,11 @@ import { Direction, ResultEdges } from '../../common/search_strategy'; import { useKibana } from '../common/lib/kibana'; import { useActionResults } from '../action_results/use_action_results'; import { generateEmptyDataMessage } from './translations'; +import { + ViewResultsInDiscoverAction, + ViewResultsInLensAction, + ViewResultsActionButtonType, +} from '../scheduled_query_groups/scheduled_query_group_queries_table'; const DataContext = createContext([]); @@ -33,20 +36,17 @@ interface ResultsTableComponentProps { actionId: string; selectedAgent?: string; agentIds?: string[]; + endDate?: string; isLive?: boolean; -} - -interface SummaryTableValue { - total: number | string; - pending: number | string; - responded: number; - failed: number; + startDate?: string; } const ResultsTableComponent: React.FC = ({ actionId, agentIds, isLive, + startDate, + endDate, }) => { const { // @ts-expect-error update types @@ -61,52 +61,6 @@ const ResultsTableComponent: React.FC = ({ isLive, }); - const notRespondedCount = useMemo(() => { - if (!agentIds || !aggregations.totalResponded) { - return '-'; - } - - return agentIds.length - aggregations.totalResponded; - }, [aggregations.totalResponded, agentIds]); - - const summaryColumns: Array> = useMemo( - () => [ - { - field: 'total', - name: 'Agents queried', - }, - { - field: 'responded', - name: 'Successful', - }, - { - field: 'pending', - name: 'Not yet responded', - }, - { - field: 'failed', - name: 'Failed', - // eslint-disable-next-line react/display-name - render: (failed: number) => ( - {failed} - ), - }, - ], - [] - ); - - const summaryItems = useMemo( - () => [ - { - total: agentIds?.length ?? '-', - pending: notRespondedCount, - responded: aggregations.totalResponded, - failed: aggregations.failed, - }, - ], - [aggregations, agentIds, notRespondedCount] - ); - const { getUrlForApp } = useKibana().services.application; const getFleetAppUrl = useCallback( @@ -132,17 +86,23 @@ const ResultsTableComponent: React.FC = ({ [setPagination] ); + const [sortingColumns, setSortingColumns] = useState([ + { + id: 'agent.name', + direction: Direction.asc, + }, + ]); const [columns, setColumns] = useState([]); - const [sortingColumns, setSortingColumns] = useState([]); - - const { data: allResultsData } = useAllResults({ + const { data: allResultsData, isFetched } = useAllResults({ actionId, activePage: pagination.pageIndex, limit: pagination.pageSize, - direction: Direction.asc, - sortField: '@timestamp', isLive, + sort: sortingColumns.map((sortedColumn) => ({ + field: sortedColumn.id, + direction: sortedColumn.direction as Direction, + })), }); const [visibleColumns, setVisibleColumns] = useState([]); @@ -234,27 +194,50 @@ const ResultsTableComponent: React.FC = ({ } }, [columns, allResultsData?.edges]); + const toolbarVisibility = useMemo( + () => ({ + additionalControls: ( + <> + + + + ), + }), + [actionId, endDate, startDate] + ); + + if (!aggregations.totalResponded) { + return ; + } + + if (aggregations.totalResponded && isFetched && !allResultsData?.edges.length) { + return ; + } + return ( // @ts-expect-error update types - - - {columns.length > 0 ? ( - - ) : ( -
- {generateEmptyDataMessage(aggregations.totalResponded)} -
- )} +
); }; diff --git a/x-pack/plugins/osquery/public/results/use_all_results.ts b/x-pack/plugins/osquery/public/results/use_all_results.ts index afeb7dadb030..d5e2bbc88694 100644 --- a/x-pack/plugins/osquery/public/results/use_all_results.ts +++ b/x-pack/plugins/osquery/public/results/use_all_results.ts @@ -34,9 +34,8 @@ export interface ResultsArgs { interface UseAllResults { actionId: string; activePage: number; - direction: Direction; limit: number; - sortField: string; + sort: Array<{ field: string; direction: Direction }>; filterQuery?: ESTermQuery | string; skip?: boolean; isLive?: boolean; @@ -45,9 +44,8 @@ interface UseAllResults { export const useAllResults = ({ actionId, activePage, - direction, limit, - sortField, + sort, filterQuery, skip = false, isLive = false, @@ -58,7 +56,7 @@ export const useAllResults = ({ } = useKibana().services; return useQuery( - ['allActionResults', { actionId, activePage, direction, limit, sortField }], + ['allActionResults', { actionId, activePage, limit, sort }], async () => { const responseData = await data.search .search( @@ -67,10 +65,7 @@ export const useAllResults = ({ factoryQueryType: OsqueryQueries.results, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), - sort: { - direction, - field: sortField, - }, + sort, }, { strategy: 'osquerySearchStrategy', diff --git a/x-pack/plugins/osquery/public/routes/live_queries/details/actions_menu.tsx b/x-pack/plugins/osquery/public/routes/live_queries/details/actions_menu.tsx deleted file mode 100644 index 5e7c6082fef5..000000000000 --- a/x-pack/plugins/osquery/public/routes/live_queries/details/actions_menu.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; - -import { useDiscoverLink } from '../../../common/hooks'; -interface LiveQueryDetailsActionsMenuProps { - actionId: string; -} - -const LiveQueryDetailsActionsMenuComponent: React.FC = ({ - actionId, -}) => { - const discoverLinkProps = useDiscoverLink({ filters: [{ key: 'action_id', value: actionId }] }); - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover((currentIsPopoverOpen) => !currentIsPopoverOpen); - }, []); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const items = useMemo( - () => [ - - - , - ], - [discoverLinkProps] - ); - - const button = useMemo( - () => ( - - - - ), - [onButtonClick] - ); - - return ( - - - - ); -}; - -export const LiveQueryDetailsActionsMenu = React.memo(LiveQueryDetailsActionsMenuComponent); diff --git a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx index 9f759f847f4c..5a80e12d0fef 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { get } from 'lodash'; import { EuiButtonEmpty, EuiTextColor, @@ -27,7 +28,6 @@ import { WithHeaderLayout } from '../../../components/layouts'; import { useActionResults } from '../../../action_results/use_action_results'; import { useActionDetails } from '../../../actions/use_action_details'; import { ResultTabs } from '../../../queries/edit/tabs'; -import { LiveQueryDetailsActionsMenu } from './actions_menu'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; @@ -120,15 +120,9 @@ const LiveQueryDetailsPageComponent = () => { - - - - - - ), - [actionId, actionResultsData?.aggregations.failed, data?.actionDetails?.fields?.agents?.length] + [actionResultsData?.aggregations.failed, data?.actionDetails?.fields?.agents?.length] ); return ( @@ -137,7 +131,12 @@ const LiveQueryDetailsPageComponent = () => { {data?.actionDetails._source?.data?.query} - + ); }; diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/actions_menu.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/actions_menu.tsx deleted file mode 100644 index ccfb933afdad..000000000000 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/actions_menu.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; - -import { useDiscoverLink } from '../../../common/hooks'; - -interface LiveQueryDetailsActionsMenuProps { - actionId: string; -} - -const LiveQueryDetailsActionsMenuComponent: React.FC = ({ - actionId, -}) => { - const discoverLinkProps = useDiscoverLink({ filters: [{ key: 'action_id', value: actionId }] }); - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover((currentIsPopoverOpen) => !currentIsPopoverOpen); - }, []); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const items = useMemo( - () => [ - - - , - ], - [discoverLinkProps] - ); - - const button = useMemo( - () => ( - - - - ), - [onButtonClick] - ); - - return ( - - - - ); -}; - -export const LiveQueryDetailsActionsMenu = React.memo(LiveQueryDetailsActionsMenuComponent); 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 d27dcfe19436..a12035426130 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 @@ -13,6 +13,8 @@ import { EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiSpacer, + EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; @@ -75,9 +77,17 @@ const ScheduledQueryGroupDetailsPageComponent = () => { + {data?.description && ( + + + + {data.description} + + + )} ), - [data?.name, scheduledQueryGroupsListProps] + [data?.description, data?.name, scheduledQueryGroupsListProps] ); const RightColumn = useMemo( diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx index 578cd4654e6b..56dbe721c31a 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx @@ -73,6 +73,10 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) ) ); }, + onError: (error) => { + // @ts-expect-error update types + toasts.addError(error, { title: error.body.error, toastMessage: error.body.message }); + }, } ); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx index 808431b68c4b..3879a375b857 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx @@ -77,7 +77,7 @@ const AddQueryFlyoutComponent: React.FC = ({ onSave, onClos return ( - +

diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx index 767eda01c06d..f44b5e45a26e 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx @@ -91,7 +91,7 @@ export const EditQueryFlyout: React.FC = ({ return ( - +

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 7d5a2c5ac99c..34c6eaea1c26 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 @@ -5,10 +5,10 @@ * 2.0. */ -import { findIndex, forEach, pullAt } from 'lodash'; +import { findIndex, forEach, pullAt, pullAllBy } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui'; import { produce } from 'immer'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { PackagePolicyInput, PackagePolicyInputStream } from '../../../../fleet/common'; @@ -50,6 +50,7 @@ const getNewStream = ({ id, interval, query, scheduledQueryGroupId }: GetNewStre const QueriesFieldComponent: React.FC = ({ field, scheduledQueryGroupId }) => { const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false); const [showEditQueryFlyout, setShowEditQueryFlyout] = useState(-1); + const [tableSelectedItems, setTableSelectedItems] = useState([]); const handleShowAddFlyout = useCallback(() => setShowAddQueryFlyout(true), []); const handleHideAddFlyout = useCallback(() => setShowAddQueryFlyout(false), []); @@ -126,6 +127,17 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu [handleHideAddFlyout, scheduledQueryGroupId, setValue] ); + const handleDeleteQueries = useCallback(() => { + setValue( + produce((draft) => { + pullAllBy(draft[0].streams, tableSelectedItems, 'vars.id.value'); + + return draft; + }) + ); + setTableSelectedItems([]); + }, [setValue, tableSelectedItems]); + const handlePackUpload = useCallback( (newQueries) => { setValue( @@ -148,26 +160,42 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu [scheduledQueryGroupId, setValue] ); + const tableData = useMemo(() => ({ inputs: field.value }), [field.value]); + return ( <> - - - + {!tableSelectedItems.length ? ( + + + + ) : ( + + + + )} {field.value && field.value[0].streams?.length ? ( ) : null} 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 90ec7e0c2717..6f78f2c086ed 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 @@ -5,19 +5,221 @@ * 2.0. */ +import { get } from 'lodash/fp'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; -import { EuiInMemoryTable, EuiCodeBlock, EuiButtonIcon } from '@elastic/eui'; +import { + EuiBasicTable, + EuiButtonEmpty, + EuiCodeBlock, + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + TypedLensByValueInput, + PersistedIndexPatternLayer, + PieVisualizationState, +} from '../../../lens/public'; import { PackagePolicy, PackagePolicyInputStream } from '../../../fleet/common'; import { FilterStateStore } from '../../../../../src/plugins/data/common'; -import { useKibana } from '../common/lib/kibana'; +import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; -interface ViewResultsInDiscoverActionProps { - item: PackagePolicyInputStream; +export enum ViewResultsActionButtonType { + icon = 'icon', + button = 'button', } -const ViewResultsInDiscoverAction: React.FC = ({ item }) => { +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', + 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 openInNewWindow = !(!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), + }, + openInNewWindow + ); + }, + [actionId, endDate, lensService, startDate] + ); + + if (buttonType === ViewResultsActionButtonType.button) { + return ( + + + + ); + } + + 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(''); @@ -36,40 +238,77 @@ const ViewResultsInDiscoverAction: React.FC = disabled: false, type: 'phrase', key: 'action_id', - params: { query: item.vars?.id.value }, + params: { query: actionId }, }, - query: { match_phrase: { action_id: item.vars?.id.value } }, + 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-15m', + mode: 'relative', + }, }); setDiscoverUrl(newUrl); }; getDiscoverUrl(); - }, [item.vars?.id.value, urlGenerator]); + }, [actionId, endDate, startDate, urlGenerator]); + + if (buttonType === ViewResultsActionButtonType.button) { + return ( + + + + ); + } return ( - + > + + ); }; +export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent); + interface ScheduledQueryGroupQueriesTableProps { data: Pick; editMode?: boolean; onDeleteClick?: (item: PackagePolicyInputStream) => void; onEditClick?: (item: PackagePolicyInputStream) => void; + selectedItems?: PackagePolicyInputStream[]; + setSelectedItems?: (selection: PackagePolicyInputStream[]) => void; } const ScheduledQueryGroupQueriesTableComponent: React.FC = ({ @@ -77,6 +316,8 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC { const renderDeleteAction = useCallback( (item: PackagePolicyInputStream) => ( @@ -132,7 +373,22 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC , + (item) => ( + + ), + [] + ); + + const renderLensResultsAction = useCallback( + (item) => ( + + ), [] ); @@ -184,29 +440,50 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC ({ sort: { - field: 'vars.id.value', + field: 'vars.id.value' as keyof PackagePolicyInputStream, direction: 'asc' as const, }, }), [] ); + const itemId = useCallback((item: PackagePolicyInputStream) => get('vars.id.value', item), []); + + const selection = useMemo( + () => ({ + onSelectionChange: setSelectedItems, + initialSelected: selectedItems, + }), + [selectedItems, setSelectedItems] + ); + return ( - + items={data.inputs[0].streams} - itemId="vars.id.value" - isExpandable={true} + itemId={itemId} columns={columns} sorting={sorting} + selection={editMode ? selection : undefined} + isSelectable={editMode} /> ); }; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index f1dbec045dac..441c00f2d096 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -8,6 +8,7 @@ import { DiscoverStart } from '../../../../src/plugins/discover/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { FleetStart } from '../../fleet/public'; +import { LensPublicStart } from '../../../plugins/lens/public'; import { CoreStart } from '../../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; import { @@ -28,6 +29,7 @@ export interface StartPlugins { discover: DiscoverStart; data: DataPublicPluginStart; fleet: FleetStart; + lens?: LensPublicStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } diff --git a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts index f6cbdf4ec51f..a120d7deddf5 100644 --- a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts @@ -44,7 +44,7 @@ export const parseAgentSelection = async ( const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection; const agentService = context.service.getAgentService(); const packagePolicyService = context.service.getPackagePolicyService(); - const kueryFragments = ['active:true']; + const kueryFragments = []; if (agentService && packagePolicyService) { const osqueryPolicies = await aggregateResults(async (page, perPage) => { @@ -55,7 +55,7 @@ export const parseAgentSelection = async ( }); return { results: items.map((it) => it.policy_id), total }; }); - kueryFragments.push(`policy_id:(${uniq(osqueryPolicies).join(',')})`); + kueryFragments.push(`policy_id:(${uniq(osqueryPolicies).join(' or ')})`); if (allAgentsSelected) { const kuery = kueryFragments.join(' and '); const fetchedAgents = await aggregateResults(async (page, perPage) => { @@ -72,10 +72,10 @@ export const parseAgentSelection = async ( if (platformsSelected.length > 0 || policiesSelected.length > 0) { const groupFragments = []; if (platformsSelected.length) { - groupFragments.push(`local_metadata.os.platform:(${platformsSelected.join(',')})`); + groupFragments.push(`local_metadata.os.platform:(${platformsSelected.join(' or ')})`); } if (policiesSelected.length) { - groupFragments.push(`policy_id:(${policiesSelected.join(',')})`); + groupFragments.push(`policy_id:(${policiesSelected.join(' or ')})`); } kueryFragments.push(`(${groupFragments.join(' or ')})`); const kuery = kueryFragments.join(' and '); 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 9dcd020f0734..970a786b930b 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 @@ -41,7 +41,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon ); if (!selectedAgents.length) { - throw new Error('No agents found for selection, aborting.'); + return response.badRequest({ body: new Error('No agents found for selection') }); } const action = { diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts index ac36f4b31e5f..6ef00b0ea305 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts @@ -53,13 +53,12 @@ export const buildResultsQuery = ({ size: querySize, track_total_hits: true, fields: agentId ? ['osquery.*'] : ['agent.*', 'osquery.*'], - sort: [ - { - [sort.field]: { - order: sort.direction, + sort: + sort?.map((sortConfig) => ({ + [sortConfig.field]: { + order: sortConfig.direction, }, - }, - ], + })) ?? [], }, };