From 2157345aa93dd0fa2383945032856bab7d4ec860 Mon Sep 17 00:00:00 2001 From: bryan Date: Thu, 28 Jan 2021 23:22:31 -0800 Subject: [PATCH 01/16] first pass of osquery host picker --- .../search_strategy/osquery/agents/index.ts | 11 + .../common/search_strategy/osquery/index.ts | 1 + .../osquery/public/agents/agents_table.tsx | 221 +++++++++++++++--- .../osquery/public/agents/translations.ts | 8 + .../osquery/public/agents/use_agent_groups.ts | 92 ++++++++ .../live_query/form/agents_table_field.tsx | 6 +- .../osquery/public/live_query/form/index.tsx | 2 +- .../routes/action/create_action_route.ts | 62 ++++- .../osquery/factory/agents/index.ts | 1 + .../factory/agents/query.all_agents.dsl.ts | 13 ++ 10 files changed, 379 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugins/osquery/public/agents/use_agent_groups.ts diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts index 57c7a42f2a481..83625897bc96c 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts @@ -11,9 +11,20 @@ import { Inspect, Maybe, PageInfoPaginated } from '../../common'; import { RequestOptionsPaginated } from '../..'; import { Agent } from '../../../shared_imports'; +export interface AggregationDataPoint { + key: string; +} + +export interface AgentAggregation { + [key: string]: { + buckets: AggregationDataPoint[]; + }; +} + export interface AgentsStrategyResponse extends IEsSearchResponse { edges: Agent[]; totalCount: number; + aggregations?: AgentAggregation; pageInfo: PageInfoPaginated; inspect?: Maybe; } diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts index 567990aca0537..4bf06472125f5 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts @@ -36,6 +36,7 @@ export type FactoryQueryTypes = OsqueryQueries; export interface RequestBasicOptions extends IEsSearchRequest { filterQuery: ESQuery | string | undefined; + aggregations?: { [key: string]: string }; docValueFields?: DocValueFields[]; factoryQueryType?: FactoryQueryTypes; } diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index e41b74c672e9b..0a2faca32aa3e 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -6,25 +6,45 @@ */ import { find } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import React, { Fragment, useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { EuiBasicTable, EuiBasicTableColumn, EuiBasicTableProps, EuiTableSelectionType, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiOverlayMask, + EuiSelectable, + EuiSelectableOption, + EuiButton, + EuiButtonEmpty, EuiHealth, } from '@elastic/eui'; import { useAllAgents } from './use_all_agents'; +import { useAgentGroups, ALL_AGENTS_GROUP_KEY } from './use_agent_groups'; import { Direction } from '../../common/search_strategy'; import { Agent } from '../../common/shared_imports'; +export interface AgentsSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; +} + interface AgentsTableProps { - selectedAgents: string[]; - onChange: (payload: string[]) => void; + agentSelection: AgentsSelection; + onChange: (payload: AgentsSelection) => void; } -const AgentsTableComponent: React.FC = ({ selectedAgents, onChange }) => { +type GroupOption = EuiSelectableOption<{ type: string }>; + +const AgentsTableComponent: React.FC = ({ agentSelection, onChange }) => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(5); const [sortField, setSortField] = useState('upgraded_at'); @@ -32,6 +52,10 @@ const AgentsTableComponent: React.FC = ({ selectedAgents, onCh const [selectedItems, setSelectedItems] = useState([]); const tableRef = useRef>(null); + const [isModalVisible, setIsModalVisible] = useState(false); + const closeModal = useCallback(() => setIsModalVisible(false), [setIsModalVisible]); + const showModal = useCallback(() => setIsModalVisible(true), [setIsModalVisible]); + const onTableChange: EuiBasicTableProps['onChange'] = useCallback( ({ page = {}, sort = {} }) => { const { index: newPageIndex, size: newPageSize } = page; @@ -46,24 +70,75 @@ const AgentsTableComponent: React.FC = ({ selectedAgents, onCh [] ); - const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback( - (newSelectedItems) => { - setSelectedItems(newSelectedItems); - - if (onChange) { - // @ts-expect-error update types - onChange(newSelectedItems.map((item) => item._id)); - } - }, - [onChange] - ); - const renderStatus = (online: string) => { const color = online ? 'success' : 'danger'; const label = online ? 'Online' : 'Offline'; return {label}; }; + const { loading: groupsLoading, groups } = useAgentGroups(); + const [selectedGroups, setSelectedGroups] = useState([]); + const [allAgentsSelected, setAllAgentsSelected] = useState(false); + const [groupOptions, setGroupOptions] = useState([]); + useEffect(() => { + const opts: GroupOption[] = [{ label: ALL_AGENTS_GROUP_KEY, type: 'all' }]; + if (!allAgentsSelected) { + const selectedSet = new Set(selectedGroups); + const generateOption = (type: string) => (name: string) => { + const option: GroupOption = { label: name, type }; + if (selectedSet.has(name)) { + option.checked = 'on'; + } + return option; + }; + + const platformOptions = groups.platforms.map(generateOption('platform')); + if (platformOptions.length > 0) { + opts.push({ label: 'Platform', isGroupLabel: true }, ...platformOptions); + } + + const policyOptions = groups.policies.map(generateOption('policy')); + if (policyOptions.length > 0) { + opts.push({ label: 'Policy', isGroupLabel: true }, ...policyOptions); + } + } else { + opts[0].checked = 'on'; + } + setGroupOptions(opts); + }, [groups.policies, groups.platforms, selectedGroups, allAgentsSelected]); + + const onGroupChange = useCallback( + (newOptions: GroupOption[]) => { + const selectedPlatforms: string[] = []; + const selectedPolicies: string[] = []; + newOptions.forEach((opt) => { + if (opt.checked === 'on') { + switch (opt.type) { + case 'platform': + selectedPlatforms.push(opt.label); + break; + case 'policy': + selectedPolicies.push(opt.label); + break; + default: + break; + } + } + }); + const selected = newOptions.filter((el) => el.checked === 'on').map((el) => el.label); + setSelectedGroups(selected); + const allSelected = selected.some((el) => el === ALL_AGENTS_GROUP_KEY); + setAllAgentsSelected(allSelected); + onChange({ + ...agentSelection, + allAgentsSelected: allSelected, + platformsSelected: selectedPlatforms, + policiesSelected: selectedPolicies, + }); + }, + [onChange, agentSelection] + ); + const { data = {} } = useAllAgents({ activePage: pageIndex, limit: pageSize, @@ -71,6 +146,17 @@ const AgentsTableComponent: React.FC = ({ selectedAgents, onCh sortField, }); + const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback( + (newSelectedItems) => { + setSelectedItems(newSelectedItems); + onChange({ + ...agentSelection, + agents: newSelectedItems.map((item: { _id: string }) => item._id), + }); + }, + [onChange, agentSelection] + ); + const columns: Array> = useMemo( () => [ { @@ -79,6 +165,12 @@ const AgentsTableComponent: React.FC = ({ selectedAgents, onCh sortable: true, truncateText: true, }, + { + field: 'local_metadata.os.family', + name: 'platform', + sortable: true, + truncateText: true, + }, { field: 'local_metadata.host.name', name: 'hostname', @@ -94,6 +186,12 @@ const AgentsTableComponent: React.FC = ({ selectedAgents, onCh ], [] ); + const searchProps = useMemo( + () => ({ + 'data-test-subj': 'selectableSearchHere', + }), + [] + ); const pagination = useMemo( () => ({ @@ -128,6 +226,7 @@ const AgentsTableComponent: React.FC = ({ selectedAgents, onCh ); useEffect(() => { + const selectedAgents = agentSelection?.agents; if ( selectedAgents?.length && // @ts-expect-error update types @@ -140,23 +239,83 @@ const AgentsTableComponent: React.FC = ({ selectedAgents, onCh ); } // @ts-expect-error update types - }, [selectedAgents, data.agents, selectedItems.length]); + }, [agentSelection, data.agents, selectedItems.length]); + + let modal; + + if (isModalVisible) { + modal = ( + + + + Select Agents + + + + {groupsLoading ? null : ( + + {(list, search) => ( + + {search} + {list} + + )} + + )} + {allAgentsSelected || selectedGroups?.length ? null : ( + + ref={tableRef} + // @ts-expect-error update types + // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop + items={data.agents ?? []} + itemId="_id" + columns={columns} + pagination={pagination} + sorting={sorting} + isSelectable={true} + selection={selection} + onChange={onTableChange} + rowHeader="firstName" + /> + )} + + + + Cancel + + + Save + + + + + ); + } + + let buttonText; + if (allAgentsSelected) { + buttonText = 'All Agents Selected'; + } else if (selectedGroups.length) { + const numGroups = selectedGroups.length; + buttonText = `${numGroups} Agent Group${numGroups > 1 ? 's' : ''} Selected`; + } else if (agentSelection?.agents?.length) { + const numAgents = agentSelection.agents.length; + buttonText = `${numAgents} Agent${numAgents > 1 ? 's' : ''} Selected`; + } else { + buttonText = 'Select Agents'; + } return ( - - ref={tableRef} - // @ts-expect-error update types - // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop - items={data.agents ?? []} - itemId="_id" - columns={columns} - pagination={pagination} - sorting={sorting} - isSelectable={true} - selection={selection} - onChange={onTableChange} - rowHeader="firstName" - /> +
+ {buttonText} + {modal} +
); }; diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index 0d9d9a8a12b8f..2499bd1652bff 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -14,3 +14,11 @@ export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearch export const FAIL_ALL_AGENTS = i18n.translate('xpack.osquery.agents.failSearchDescription', { defaultMessage: `Failed to fetch agents`, }); + +export const ERROR_AGENT_GROUPS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { + defaultMessage: `An error has occurred while fetching agent groups.`, +}); + +export const FAIL_AGENT_GROUPS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { + defaultMessage: `An error has occurred while fetching agent groups.`, +}); diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts new file mode 100644 index 0000000000000..adfbe6cc28379 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -0,0 +1,92 @@ +/* + * 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 { useEffect, useState, useRef } from 'react'; +import { useKibana } from '../common/lib/kibana'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; + +import { + OsqueryQueries, + AgentsRequestOptions, + AgentsStrategyResponse, +} from '../../common/search_strategy'; + +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { generateTablePaginationOptions } from './helpers'; +import * as i18n from './translations'; + +export const ALL_AGENTS_GROUP_KEY = 'All agents'; +export const useAgentGroups = () => { + const { data, notifications } = useKibana().services; + const [loading, setLoading] = useState(true); + const [platforms, setPlatforms] = useState([]); + const [policies, setPolicies] = useState([]); + + const abortCtrl = useRef(new AbortController()); + useEffect(() => { + let didCancel = false; + const searchSubscription$ = data.search + .search( + { + filterQuery: undefined, + factoryQueryType: OsqueryQueries.agents, + aggregations: { + platforms: 'local_metadata.os.platform', + policies: 'policy_id', + }, + pagination: generateTablePaginationOptions(0, 9000), + sort: { + direction: 'asc', + field: 'local_metadata.os.platform', + }, + } as AgentsRequestOptions, + { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + } + ) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + if (response.aggregations) { + const aggs = response.aggregations; + setPlatforms(aggs.platforms.buckets.map((o) => o.key)); + setPolicies(aggs.policies.buckets.map((o) => o.key)); + } + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_AGENT_GROUPS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_AGENT_GROUPS, text: msg.message }); + } + }, + }); + const abort = abortCtrl.current; + return () => { + didCancel = true; + abort.abort(); + }; + }, [setPolicies, setPlatforms, data.search, notifications.toasts]); + return { + loading, + groups: { + [ALL_AGENTS_GROUP_KEY]: [], + platforms, + policies, + }, + }; +}; diff --git a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx index 7a93b5d2491db..4bc9262af7613 100644 --- a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx +++ b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx @@ -7,10 +7,10 @@ import React, { useCallback } from 'react'; import { FieldHook } from '../../shared_imports'; -import { AgentsTable } from '../../agents/agents_table'; +import { AgentsTable, AgentsSelection } from '../../agents/agents_table'; interface AgentsTableFieldProps { - field: FieldHook; + field: FieldHook; } const AgentsTableFieldComponent: React.FC = ({ field }) => { @@ -24,7 +24,7 @@ const AgentsTableFieldComponent: React.FC = ({ field }) = [value, setValue] ); - return ; + return ; }; export const AgentsTableField = React.memo(AgentsTableFieldComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/index.tsx b/x-pack/plugins/osquery/public/live_query/form/index.tsx index 4a69e2fc0e76d..7e19bee530ec5 100644 --- a/x-pack/plugins/osquery/public/live_query/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_query/form/index.tsx @@ -40,7 +40,7 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on return (
- + 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 25212bc3bf5cc..55dfbaa514bc8 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 @@ -13,6 +13,13 @@ import moment from 'moment'; import { IRouter } from '../../../../../../src/core/server'; import { packSavedObjectType, savedQuerySavedObjectType } from '../../../common/types'; +export interface AgentsSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; +} + export const createActionRoute = (router: IRouter) => { router.post( { @@ -24,6 +31,56 @@ export const createActionRoute = (router: IRouter) => { }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asInternalUser; + const selectedAgents: string[] = []; + const { + agentSelection: { allAgentsSelected, platformsSelected, policiesSelected, agents }, + } = request.body as { agentSelection: AgentsSelection }; + const extractIds = ({ body }) => + body.hits.hits.map((o) => o._source.local_metadata?.elastic.agent.id).filter((e) => e); + if (allAgentsSelected) { + // make a query for all agent ids + const idRes = await esClient.search<{}, {}>({ + index: '.fleet-agents', + body: { + _source: 'local_metadata.elastic.agent.id', + size: 9000, + query: { + match_all: {}, + }, + }, + }); + const ids = extractIds(idRes); + selectedAgents.push(...ids); + } else if (platformsSelected.length > 0 || policiesSelected.length > 0) { + const filters: Array<{ + term: { [key: string]: string }; + }> = platformsSelected.map((platform) => ({ + term: { 'local_metadata.os.platform': platform }, + })); + filters.push(...policiesSelected.map((policyId) => ({ term: { policyId } }))); + const query = { + index: '.fleet-agents', + body: { + _source: 'local_metadata.elastic.agent.id', + size: 9000, + query: { + bool: { + filter: [ + { + bool: { + should: filters, + }, + }, + ], + }, + }, + }, + }; + const ids = extractIds(await esClient.search<{}, {}>(query)); + selectedAgents.push(...ids); + } else { + selectedAgents.push(...agents); + } // @ts-expect-error update validation if (request.body.pack_id) { @@ -73,7 +130,7 @@ export const createActionRoute = (router: IRouter) => { type: 'INPUT_ACTION', input_type: 'osquery', // @ts-expect-error update validation - agents: request.body.agents, + agents: selectedAgents, data: { id: query.id, // @ts-expect-error update validation @@ -103,8 +160,7 @@ export const createActionRoute = (router: IRouter) => { expiration: moment().add(2, 'days').toISOString(), type: 'INPUT_ACTION', input_type: 'osquery', - // @ts-expect-error update validation - agents: request.body.agents, + agents: selectedAgents, data: { // @ts-expect-error update validation id: request.body.query.id ?? uuid.v4(), diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts index 1f7fbccb68682..395b3d6504fa8 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts @@ -38,6 +38,7 @@ export const allAgents: OsqueryFactory = { ...response, inspect, edges: response.rawResponse.hits.hits.map((hit) => ({ _id: hit._id, ...hit._source })), + aggregations: response.rawResponse.aggregations, totalCount: response.rawResponse.hits.total, pageInfo: { activePage: activePage ?? 0, diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts index 4ad6022017966..1b1e296abece9 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts @@ -14,6 +14,7 @@ export const buildAgentsQuery = ({ filterQuery, pagination: { cursorStart, querySize }, sort, + aggregations, }: AgentsRequestOptions): ISearchRequestParams => { // const filter = [...createQueryFilterClauses(filterQuery)]; @@ -29,6 +30,7 @@ export const buildAgentsQuery = ({ }, }, }, + aggs: {}, track_total_hits: true, sort: [ { @@ -42,5 +44,16 @@ export const buildAgentsQuery = ({ }, }; + if (aggregations) { + Object.keys(aggregations).reduce((acc, aggKey) => { + acc[aggKey] = { + terms: { + field: aggregations[aggKey], + }, + }; + return acc; + }, dslQuery.body.aggs as { [key: string]: { terms: { field: string } } }); + } + return dslQuery; }; From 0ee193b538bf881bdb974e9aa3761c4c45fd2902 Mon Sep 17 00:00:00 2001 From: bryan Date: Sun, 14 Mar 2021 22:10:00 -0700 Subject: [PATCH 02/16] passes type checking and linting --- .../plugins/osquery/public/agents/agents_table.tsx | 2 +- .../server/routes/action/create_action_route.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 0a2faca32aa3e..180d8abbe7394 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -42,7 +42,7 @@ interface AgentsTableProps { onChange: (payload: AgentsSelection) => void; } -type GroupOption = EuiSelectableOption<{ type: string }>; +type GroupOption = EuiSelectableOption<{ type?: string }>; const AgentsTableComponent: React.FC = ({ agentSelection, onChange }) => { const [pageIndex, setPageIndex] = useState(0); 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 55dfbaa514bc8..187928831ba3a 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 @@ -35,11 +35,16 @@ export const createActionRoute = (router: IRouter) => { const { agentSelection: { allAgentsSelected, platformsSelected, policiesSelected, agents }, } = request.body as { agentSelection: AgentsSelection }; - const extractIds = ({ body }) => - body.hits.hits.map((o) => o._source.local_metadata?.elastic.agent.id).filter((e) => e); + // TODO: fix up the types here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extractIds = ({ body }: Record) => + body.hits.hits + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((o: Record) => o._source.local_metadata?.elastic.agent.id) + .filter((e: string) => e); if (allAgentsSelected) { // make a query for all agent ids - const idRes = await esClient.search<{}, {}>({ + const idRes = await esClient.search({ index: '.fleet-agents', body: { _source: 'local_metadata.elastic.agent.id', @@ -129,7 +134,6 @@ export const createActionRoute = (router: IRouter) => { expiration: moment().add(2, 'days').toISOString(), type: 'INPUT_ACTION', input_type: 'osquery', - // @ts-expect-error update validation agents: selectedAgents, data: { id: query.id, From 6cd9a3ceff0d50bb0bb6a7ea7925f75319b1e033 Mon Sep 17 00:00:00 2001 From: bryan Date: Mon, 22 Mar 2021 23:56:16 -0700 Subject: [PATCH 03/16] reworked everything --- .../search_strategy/osquery/agents/index.ts | 5 +- .../common/search_strategy/osquery/index.ts | 8 +- .../osquery/public/agents/agents_table.tsx | 452 +++++++----------- .../plugins/osquery/public/agents/helpers.ts | 46 ++ x-pack/plugins/osquery/public/agents/types.ts | 35 ++ .../osquery/public/agents/use_agent_groups.ts | 101 ++-- .../public/agents/use_osquery_policies.ts | 26 + .../factory/agents/query.all_agents.dsl.ts | 24 +- .../server/search_strategy/osquery/helpers.ts | 42 ++ 9 files changed, 398 insertions(+), 341 deletions(-) create mode 100644 x-pack/plugins/osquery/public/agents/types.ts create mode 100644 x-pack/plugins/osquery/public/agents/use_osquery_policies.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts index 83625897bc96c..092d781ddd923 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts @@ -11,10 +11,13 @@ import { Inspect, Maybe, PageInfoPaginated } from '../../common'; import { RequestOptionsPaginated } from '../..'; import { Agent } from '../../../shared_imports'; -export interface AggregationDataPoint { +interface BaseDataPoint { key: string; + doc_count: number; } +export type AggregationDataPoint = BaseDataPoint & AgentAggregation; + export interface AgentAggregation { [key: string]: { buckets: AggregationDataPoint[]; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts index 4bf06472125f5..9e12bae807454 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts @@ -34,9 +34,15 @@ export enum OsqueryQueries { export type FactoryQueryTypes = OsqueryQueries; +export type AggregationValue = string | { field: string; subaggs: Aggregation }; + +export interface Aggregation { + [key: string]: AggregationValue; +} + export interface RequestBasicOptions extends IEsSearchRequest { filterQuery: ESQuery | string | undefined; - aggregations?: { [key: string]: string }; + aggregations?: Aggregation; docValueFields?: DocValueFields[]; factoryQueryType?: FactoryQueryTypes; } diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 180d8abbe7394..4b883b293af51 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -5,30 +5,21 @@ * 2.0. */ -import { find } from 'lodash/fp'; -import React, { Fragment, useCallback, useEffect, useMemo, useState, useRef } from 'react'; -import { - EuiBasicTable, - EuiBasicTableColumn, - EuiBasicTableProps, - EuiTableSelectionType, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiOverlayMask, - EuiSelectable, - EuiSelectableOption, - EuiButton, - EuiButtonEmpty, - EuiHealth, -} from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiHealth, EuiHighlight } from '@elastic/eui'; import { useAllAgents } from './use_all_agents'; -import { useAgentGroups, ALL_AGENTS_GROUP_KEY } from './use_agent_groups'; +import { useAgentGroups } from './use_agent_groups'; import { Direction } from '../../common/search_strategy'; import { Agent } from '../../common/shared_imports'; +import { + getNumAgentsInGrouping, + generateAgentCheck, + getNumOverlapped, + generateColorPicker, +} from './helpers'; + +import { AGENT_GROUP_KEY, SelectedGroups, AgentOptionValue, GroupOptionValue } from './types'; export interface AgentsSelection { agents: string[]; @@ -42,279 +33,194 @@ interface AgentsTableProps { onChange: (payload: AgentsSelection) => void; } -type GroupOption = EuiSelectableOption<{ type?: string }>; - -const AgentsTableComponent: React.FC = ({ agentSelection, onChange }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('upgraded_at'); - const [sortDirection, setSortDirection] = useState(Direction.asc); - const [selectedItems, setSelectedItems] = useState([]); - const tableRef = useRef>(null); - - const [isModalVisible, setIsModalVisible] = useState(false); - const closeModal = useCallback(() => setIsModalVisible(false), [setIsModalVisible]); - const showModal = useCallback(() => setIsModalVisible(true), [setIsModalVisible]); - - const onTableChange: EuiBasicTableProps['onChange'] = useCallback( - ({ page = {}, sort = {} }) => { - const { index: newPageIndex, size: newPageSize } = page; - - const { field: newSortField, direction: newSortDirection } = sort; - - setPageIndex(newPageIndex); - setPageSize(newPageSize); - setSortField(newSortField); - setSortDirection(newSortDirection); - }, - [] - ); +type GroupOption = EuiComboBoxOptionOption; - const renderStatus = (online: string) => { - const color = online ? 'success' : 'danger'; - const label = online ? 'Online' : 'Offline'; - return {label}; - }; +const getColor = generateColorPicker(); - const { loading: groupsLoading, groups } = useAgentGroups(); - const [selectedGroups, setSelectedGroups] = useState([]); - const [allAgentsSelected, setAllAgentsSelected] = useState(false); - const [groupOptions, setGroupOptions] = useState([]); - useEffect(() => { - const opts: GroupOption[] = [{ label: ALL_AGENTS_GROUP_KEY, type: 'all' }]; - if (!allAgentsSelected) { - const selectedSet = new Set(selectedGroups); - const generateOption = (type: string) => (name: string) => { - const option: GroupOption = { label: name, type }; - if (selectedSet.has(name)) { - option.checked = 'on'; - } - return option; - }; +const AgentsTableComponent: React.FC = ({ onChange }) => { + // handle paged fetching of agents + const [pageIndex /* , setPageIndex*/] = useState(0); + const [pageSize /* , setPageSize*/] = useState(1000); + const [sortField /* , setSortField*/] = useState('upgraded_at'); + const [sortDirection /* , setSortDirection*/] = useState(Direction.asc); - const platformOptions = groups.platforms.map(generateOption('platform')); - if (platformOptions.length > 0) { - opts.push({ label: 'Platform', isGroupLabel: true }, ...platformOptions); - } + const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups(); + const [loading, setLoading] = useState(true); + const [options, setOptions] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); + const [numAgentsSelected, setNumAgentsSelected] = useState(0); - const policyOptions = groups.policies.map(generateOption('policy')); - if (policyOptions.length > 0) { - opts.push({ label: 'Policy', isGroupLabel: true }, ...policyOptions); - } - } else { - opts[0].checked = 'on'; - } - setGroupOptions(opts); - }, [groups.policies, groups.platforms, selectedGroups, allAgentsSelected]); - - const onGroupChange = useCallback( - (newOptions: GroupOption[]) => { - const selectedPlatforms: string[] = []; - const selectedPolicies: string[] = []; - newOptions.forEach((opt) => { - if (opt.checked === 'on') { - switch (opt.type) { - case 'platform': - selectedPlatforms.push(opt.label); - break; - case 'policy': - selectedPolicies.push(opt.label); - break; - default: - break; - } - } - }); - const selected = newOptions.filter((el) => el.checked === 'on').map((el) => el.label); - setSelectedGroups(selected); - const allSelected = selected.some((el) => el === ALL_AGENTS_GROUP_KEY); - setAllAgentsSelected(allSelected); - onChange({ - ...agentSelection, - allAgentsSelected: allSelected, - platformsSelected: selectedPlatforms, - policiesSelected: selectedPolicies, - }); - }, - [onChange, agentSelection] - ); - - const { data = {} } = useAllAgents({ + const { data: agentData = { agents: [] } } = useAllAgents({ activePage: pageIndex, limit: pageSize, direction: sortDirection, sortField, }); - const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback( - (newSelectedItems) => { - setSelectedItems(newSelectedItems); - onChange({ - ...agentSelection, - agents: newSelectedItems.map((item: { _id: string }) => item._id), - }); - }, - [onChange, agentSelection] - ); - - const columns: Array> = useMemo( - () => [ - { - field: 'local_metadata.elastic.agent.id', - name: 'id', - sortable: true, - truncateText: true, - }, - { - field: 'local_metadata.os.family', - name: 'platform', - sortable: true, - truncateText: true, - }, - { - field: 'local_metadata.host.name', - name: 'hostname', - truncateText: true, - }, - + useEffect(() => { + const allAgentsLabel = 'All agents'; + const opts: GroupOption[] = [ { - field: 'active', - name: 'Online', - dataType: 'boolean', - render: (active: string) => renderStatus(active), + label: allAgentsLabel, + options: [ + { + label: allAgentsLabel, + value: { groupType: AGENT_GROUP_KEY.All, size: totalNumAgents }, + color: getColor(AGENT_GROUP_KEY.All), + }, + ], }, - ], - [] - ); - const searchProps = useMemo( - () => ({ - 'data-test-subj': 'selectableSearchHere', - }), - [] - ); - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - // @ts-expect-error update types - totalItemCount: data.totalCount ?? 0, - pageSizeOptions: [3, 5, 8], - }), - // @ts-expect-error update types - [pageIndex, pageSize, data.totalCount] - ); - - const sorting = useMemo( - () => ({ - sort: { - field: sortField, - direction: sortDirection, - }, - }), - [sortDirection, sortField] - ); - - const selection: EuiBasicTableProps['selection'] = useMemo( - () => ({ - selectable: (agent: Agent) => agent.active, - selectableMessage: (selectable: boolean) => (!selectable ? 'User is currently offline' : ''), - onSelectionChange, - initialSelected: selectedItems, - }), - [onSelectionChange, selectedItems] - ); - - useEffect(() => { - const selectedAgents = agentSelection?.agents; - if ( - selectedAgents?.length && - // @ts-expect-error update types - data.agents?.length && - selectedItems.length !== selectedAgents.length - ) { - tableRef?.current?.setSelection( - // @ts-expect-error update types - selectedAgents.map((agentId) => find({ _id: agentId }, data.agents)) - ); + ]; + + if (groups.platforms.length > 0) { + const groupType = AGENT_GROUP_KEY.Platform; + opts.push({ + label: 'Platform', + options: groups.platforms.map(({ name, size }) => ({ + label: name, + value: { color: getColor(groupType), groupType, size }, + })), + }); } - // @ts-expect-error update types - }, [agentSelection, data.agents, selectedItems.length]); - - let modal; - if (isModalVisible) { - modal = ( - - - - Select Agents - + if (groups.policies.length > 0) { + const groupType = AGENT_GROUP_KEY.Policy; + opts.push({ + label: 'Policy', + options: groups.policies.map(({ name, size }) => ({ + label: name, + value: { color: getColor(groupType), groupType, size }, + })), + }); + } - - {groupsLoading ? null : ( - - {(list, search) => ( - - {search} - {list} - - )} - - )} - {allAgentsSelected || selectedGroups?.length ? null : ( - - ref={tableRef} - // @ts-expect-error update types - // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop - items={data.agents ?? []} - itemId="_id" - columns={columns} - pagination={pagination} - sorting={sorting} - isSelectable={true} - selection={selection} - onChange={onTableChange} - rowHeader="firstName" - /> - )} - + if (agentData.agents.length > 0) { + const groupType = AGENT_GROUP_KEY.Agent; + opts.push({ + label: 'Agents', + options: (agentData.agents as Agent[]).map((agent: Agent) => ({ + label: agent.local_metadata.host.hostname, + value: { + groupType, + color: getColor(groupType), + groups: { policy: agent.policy_id ?? '', platform: agent.local_metadata.os.platform }, + id: agent.id, + online: agent.active, + }, + })), + }); + } + setLoading(false); + setOptions(opts); + }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agentData]); + + const onSelection = useCallback( + (selection: GroupOption[]) => { + // TODO?: optimize this by making it incremental + const newAgentSelection: AgentsSelection = { + agents: [], + allAgentsSelected: false, + platformsSelected: [], + policiesSelected: [], + }; + // parse through the selections to be able to determine how many are actually selected + const selectedAgents = []; + const selectedGroups: SelectedGroups = { + policy: {}, + platform: {}, + }; - - Cancel + // TODO: clean this up, make it less awkward + for (const opt of selection) { + const groupType = opt.value?.groupType; + let value; + switch (groupType) { + case AGENT_GROUP_KEY.All: + newAgentSelection.allAgentsSelected = true; + break; + case AGENT_GROUP_KEY.Platform: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.platform[opt.label] = value.size; + } + newAgentSelection.platformsSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Policy: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.policy[opt.label] = value.size ?? 0; + } + newAgentSelection.policiesSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Agent: + value = opt.value as AgentOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to count how many agents are selected if they are all selected + selectedAgents.push(opt.value); + } + // TODO: fix this casting by updating the opt type to be a union + newAgentSelection.agents.push(value.id as string); + break; + default: + // this should never happen! + // eslint-disable-next-line no-console + console.error(`unknown group type ${groupType}`); + } + } + if (newAgentSelection.allAgentsSelected) { + setNumAgentsSelected(totalNumAgents); + } else { + const checkAgent = generateAgentCheck(selectedGroups); + setNumAgentsSelected( + // filter out all the agents counted by selected policies and platforms + selectedAgents.filter((a) => checkAgent(a as AgentOptionValue)).length + + // add the number of agents added via policy and platform groups + getNumAgentsInGrouping(selectedGroups) - + // subtract the number of agents double counted by policy/platform selections + getNumOverlapped(selectedGroups, groups.overlap) + ); + } + onChange(newAgentSelection); + setSelectedOptions(selection); + }, + [groups, onChange, totalNumAgents] + ); - - Save - - - - + const renderOption = useCallback((option, searchValue, contentClassName) => { + const { label, value } = option; + return value?.groupType === AGENT_GROUP_KEY.Agent ? ( + + + {label} + + + ) : ( + + {label} +   + ({value?.size}) + ); - } - - let buttonText; - if (allAgentsSelected) { - buttonText = 'All Agents Selected'; - } else if (selectedGroups.length) { - const numGroups = selectedGroups.length; - buttonText = `${numGroups} Agent Group${numGroups > 1 ? 's' : ''} Selected`; - } else if (agentSelection?.agents?.length) { - const numAgents = agentSelection.agents.length; - buttonText = `${numAgents} Agent${numAgents > 1 ? 's' : ''} Selected`; - } else { - buttonText = 'Select Agents'; - } - + }, []); + const selectedAgentsText = `${numAgentsSelected} agent${ + numAgentsSelected === 1 ? '' : 's' + } selected.`; return (
- {buttonText} - {modal} +

Select Agents

+ {numAgentsSelected > 0 ? {selectedAgentsText} : ''} +   +
); }; diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index fef17aadb62be..ad0edbfc902c4 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -5,15 +5,61 @@ * 2.0. */ +import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { PaginationInputPaginated, FactoryQueryTypes, StrategyResponseType, Inspect, } from '../../common/search_strategy'; +import { AGENT_GROUP_KEY, SelectedGroups, Overlap, AgentOptionValue } from './types'; export type InspectResponse = Inspect & { response: string[] }; +export const getNumOverlapped = ({ policy, platform }: SelectedGroups, overlap: Overlap) => { + let sum = 0; + Object.keys(platform).forEach((plat) => { + const policies = overlap[plat]; + Object.keys(policy).forEach((pol) => { + sum += policies[pol] ?? 0; + }); + }); + return sum; +}; + +export const generateColorPicker = () => { + const visColorsBehindText = euiPaletteColorBlindBehindText(); + const typeColors = new Map(); + return (type: AGENT_GROUP_KEY) => { + if (!typeColors.has(type)) { + typeColors.set(type, visColorsBehindText[typeColors.size]); + } + return typeColors.get(type); + }; +}; + +export const getNumAgentsInGrouping = (selectedGroups: SelectedGroups) => { + let sum = 0; + Object.keys(selectedGroups).forEach((g) => { + const group = selectedGroups[g]; + sum += Object.keys(group).reduce((acc, k) => acc + group[k], 0); + }); + return sum; +}; + +export const generateAgentCheck = (selectedGroups: SelectedGroups) => { + return ({ groups }: AgentOptionValue) => { + return Object.keys(groups) + .map((group) => { + const selectedGroup = selectedGroups[group]; + const agentGroup = groups[group]; + // check if the agent platform/policy is selected + return selectedGroup[agentGroup]; + }) + .every((a) => !a); + }; +}; + export const generateTablePaginationOptions = ( activePage: number, limit: number, diff --git a/x-pack/plugins/osquery/public/agents/types.ts b/x-pack/plugins/osquery/public/agents/types.ts new file mode 100644 index 0000000000000..4aa245f2b5be6 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/types.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +export interface Overlap { + [platform: string]: { [policy: string]: number }; +} + +export interface SelectedGroups { + [groupType: string]: { [groupName: string]: number }; +} + +interface BaseGroupOption { + groupType: AGENT_GROUP_KEY; +} + +export type AgentOptionValue = BaseGroupOption & { + groups: { [groupType: string]: string }; + online: boolean; + id: string; +}; + +export type GroupOptionValue = BaseGroupOption & { + size: number; +}; + +export enum AGENT_GROUP_KEY { + All, + Platform, + Policy, + Agent, +} diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts index adfbe6cc28379..4b07ca16c5d07 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -4,9 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useEffect, useState, useRef } from 'react'; +import { useState } from 'react'; +import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; -import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { useOsqueryPolicies } from './use_osquery_policies'; import { OsqueryQueries, @@ -14,27 +15,35 @@ import { AgentsStrategyResponse, } from '../../common/search_strategy'; -import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; import { generateTablePaginationOptions } from './helpers'; -import * as i18n from './translations'; +import { Overlap } from './types'; + +interface Group { + name: string; + size: number; +} -export const ALL_AGENTS_GROUP_KEY = 'All agents'; export const useAgentGroups = () => { - const { data, notifications } = useKibana().services; - const [loading, setLoading] = useState(true); - const [platforms, setPlatforms] = useState([]); - const [policies, setPolicies] = useState([]); + const { data } = useKibana().services; + const { osqueryPolicies, osqueryPoliciesLoading } = useOsqueryPolicies(); - const abortCtrl = useRef(new AbortController()); - useEffect(() => { - let didCancel = false; - const searchSubscription$ = data.search + const [platforms, setPlatforms] = useState([]); + const [policyOptions, setPolicyOptions] = useState([]); + const [loading, setLoading] = useState(true); + const [overlap, setOverlap] = useState(() => ({})); + const [totalCount, setTotalCount] = useState(0); + useQuery(['agentGroups', osqueryPoliciesLoading], async () => { + if (osqueryPoliciesLoading) return null; + const responseData = await data.search .search( { - filterQuery: undefined, + filterQuery: { terms: { policy_id: osqueryPolicies } }, factoryQueryType: OsqueryQueries.agents, aggregations: { - platforms: 'local_metadata.os.platform', + platforms: { + field: 'local_metadata.os.platform', + subaggs: { policies: 'policy_id' }, + }, policies: 'policy_id', }, pagination: generateTablePaginationOptions(0, 9000), @@ -45,48 +54,38 @@ export const useAgentGroups = () => { } as AgentsRequestOptions, { strategy: 'osquerySearchStrategy', - abortSignal: abortCtrl.current.signal, } ) - .subscribe({ - next: (response) => { - if (isCompleteResponse(response)) { - if (!didCancel) { - setLoading(false); - if (response.aggregations) { - const aggs = response.aggregations; - setPlatforms(aggs.platforms.buckets.map((o) => o.key)); - setPolicies(aggs.policies.buckets.map((o) => o.key)); - } - } - searchSubscription$.unsubscribe(); - } else if (isErrorResponse(response)) { - if (!didCancel) { - setLoading(false); - } - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_AGENT_GROUPS); - searchSubscription$.unsubscribe(); - } - }, - error: (msg) => { - if (!(msg instanceof AbortError)) { - notifications.toasts.addDanger({ title: i18n.FAIL_AGENT_GROUPS, text: msg.message }); - } - }, - }); - const abort = abortCtrl.current; - return () => { - didCancel = true; - abort.abort(); - }; - }, [setPolicies, setPlatforms, data.search, notifications.toasts]); + .toPromise(); + setLoading(false); + if (responseData.aggregations) { + const aggs = responseData.aggregations; + const newPlatforms: Group[] = []; + const newOverlap: Overlap = {}; + for (const { key, doc_count: docCount, policies } of aggs.platforms.buckets) { + newPlatforms.push({ name: key, size: docCount }); + newOverlap[key] = policies.buckets.reduce( + (acc: { [key: string]: number }, pol: { key: string; doc_count: number }) => { + acc[pol.key] = pol.doc_count; + return acc; + }, + {} as { [key: string]: number } + ); + } + setPlatforms(newPlatforms); + setOverlap(newOverlap); + setPolicyOptions(aggs.policies.buckets.map((o) => ({ name: o.key, size: o.doc_count }))); + } + setTotalCount(responseData.totalCount); + }); + return { loading, + totalCount, groups: { - [ALL_AGENTS_GROUP_KEY]: [], platforms, - policies, + policies: policyOptions, + overlap, }, }; }; diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts new file mode 100644 index 0000000000000..58377d7de0d5b --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -0,0 +1,26 @@ +/* + * 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 { useState } from 'react'; +import { useQuery } from 'react-query'; +import { useKibana } from '../common/lib/kibana'; + +export const useOsqueryPolicies = () => { + const [osqueryPolicies, setPolicies] = useState([]); + const { http } = useKibana().services; + + const { isLoading: osqueryPoliciesLoading } = useQuery(['osqueryPolicies'], async () => { + const policyResponse = await http.get('/api/fleet/package_policies', { + query: { + kuery: 'ingest-package-policies.package.name:osquery_elastic_managed', + }, + }); + setPolicies(policyResponse.items.map((p: { policy_id: string }) => p.policy_id)); + }); + + return { osqueryPoliciesLoading, osqueryPolicies }; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts index 1b1e296abece9..ba3df094634a0 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts @@ -7,16 +7,19 @@ import { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; import { AgentsRequestOptions } from '../../../../../common/search_strategy'; -// import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; +import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; +import { parseAggregator } from '../../helpers'; export const buildAgentsQuery = ({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars filterQuery, pagination: { cursorStart, querySize }, sort, aggregations, }: AgentsRequestOptions): ISearchRequestParams => { - // const filter = [...createQueryFilterClauses(filterQuery)]; + const filter = [ + { term: { active: { value: 'true' } } }, + ...createQueryFilterClauses(filterQuery), + ]; const dslQuery = { allowNoIndices: true, @@ -24,10 +27,8 @@ export const buildAgentsQuery = ({ ignoreUnavailable: true, body: { query: { - term: { - active: { - value: 'true', - }, + bool: { + filter, }, }, aggs: {}, @@ -45,14 +46,7 @@ export const buildAgentsQuery = ({ }; if (aggregations) { - Object.keys(aggregations).reduce((acc, aggKey) => { - acc[aggKey] = { - terms: { - field: aggregations[aggKey], - }, - }; - return acc; - }, dslQuery.body.aggs as { [key: string]: { terms: { field: string } } }); + dslQuery.body.aggs = parseAggregator(aggregations); } return dslQuery; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts new file mode 100644 index 0000000000000..3b80188540cd9 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts @@ -0,0 +1,42 @@ +/* + * 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 { Aggregation, AggregationValue } from '../../../common/search_strategy/osquery'; + +interface AggregationResult { + [key: string]: { + terms: { + field: string; + }; + aggs?: AggregationResult; + }; +} + +function isPrimative(a: AggregationValue): a is string { + return typeof a === 'string'; +} + +export function parseAggregator(aggs: Aggregation) { + return Object.keys(aggs).reduce((acc, aggKey) => { + const value = aggs[aggKey]; + if (isPrimative(value)) { + acc[aggKey] = { + terms: { + field: aggs[aggKey] as string, + }, + }; + } else { + acc[aggKey] = { + terms: { + field: value.field, + }, + aggs: parseAggregator(value.subaggs), + }; + } + return acc; + }, {} as AggregationResult); +} From 2bab6a733e37db714d143c2694a8b45c68dd19b5 Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 23 Mar 2021 00:51:51 -0700 Subject: [PATCH 04/16] fixed the policy query and pill colors --- .../osquery/public/agents/agents_table.tsx | 10 ++-- .../routes/action/create_action_route.ts | 54 ++++++++++--------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 4b883b293af51..cf4ae296f384c 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -78,7 +78,8 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { label: 'Platform', options: groups.platforms.map(({ name, size }) => ({ label: name, - value: { color: getColor(groupType), groupType, size }, + color: getColor(groupType), + value: { groupType, size }, })), }); } @@ -89,7 +90,8 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { label: 'Policy', options: groups.policies.map(({ name, size }) => ({ label: name, - value: { color: getColor(groupType), groupType, size }, + color: getColor(groupType), + value: { groupType, size }, })), }); } @@ -100,11 +102,11 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { label: 'Agents', options: (agentData.agents as Agent[]).map((agent: Agent) => ({ label: agent.local_metadata.host.hostname, + color: getColor(groupType), value: { groupType, - color: getColor(groupType), groups: { policy: agent.policy_id ?? '', platform: agent.local_metadata.os.platform }, - id: agent.id, + id: agent.local_metadata.elastic.agent.id, online: agent.active, }, })), 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 187928831ba3a..9e5387bd2cfee 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 @@ -31,7 +31,7 @@ export const createActionRoute = (router: IRouter) => { }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asInternalUser; - const selectedAgents: string[] = []; + let selectedAgents: string[] = []; const { agentSelection: { allAgentsSelected, platformsSelected, policiesSelected, agents }, } = request.body as { agentSelection: AgentsSelection }; @@ -56,35 +56,37 @@ export const createActionRoute = (router: IRouter) => { }); const ids = extractIds(idRes); selectedAgents.push(...ids); - } else if (platformsSelected.length > 0 || policiesSelected.length > 0) { - const filters: Array<{ - term: { [key: string]: string }; - }> = platformsSelected.map((platform) => ({ - term: { 'local_metadata.os.platform': platform }, - })); - filters.push(...policiesSelected.map((policyId) => ({ term: { policyId } }))); - const query = { - index: '.fleet-agents', - body: { - _source: 'local_metadata.elastic.agent.id', - size: 9000, - query: { - bool: { - filter: [ - { - bool: { - should: filters, + } else { + if (platformsSelected.length > 0 || policiesSelected.length > 0) { + const filters: Array<{ + term: { [key: string]: string }; + }> = platformsSelected.map((platform) => ({ + term: { 'local_metadata.os.platform': platform }, + })); + filters.push(...policiesSelected.map((policyId) => ({ term: { policy_id: policyId } }))); + const query = { + index: '.fleet-agents', + body: { + _source: 'local_metadata.elastic.agent.id', + size: 9000, + query: { + bool: { + filter: [ + { + bool: { + should: filters, + }, }, - }, - ], + ], + }, }, }, - }, - }; - const ids = extractIds(await esClient.search<{}, {}>(query)); - selectedAgents.push(...ids); - } else { + }; + const ids = extractIds(await esClient.search<{}, {}>(query)); + selectedAgents.push(...ids); + } selectedAgents.push(...agents); + selectedAgents = Array.from(new Set(selectedAgents)); } // @ts-expect-error update validation From a745ee1a0f8167c53772443f0e8a9c735e880516 Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 23 Mar 2021 09:38:00 -0700 Subject: [PATCH 05/16] fix i18n test failures --- x-pack/plugins/osquery/public/agents/translations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index 2499bd1652bff..672c83a7e2fc2 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -15,10 +15,10 @@ export const FAIL_ALL_AGENTS = i18n.translate('xpack.osquery.agents.failSearchDe defaultMessage: `Failed to fetch agents`, }); -export const ERROR_AGENT_GROUPS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { +export const ERROR_AGENT_GROUPS = i18n.translate('xpack.osquery.agentGroups.errorSearchDescription', { defaultMessage: `An error has occurred while fetching agent groups.`, }); -export const FAIL_AGENT_GROUPS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { - defaultMessage: `An error has occurred while fetching agent groups.`, +export const FAIL_AGENT_GROUPS = i18n.translate('xpack.osquery.agentGroups.failSearchDescription', { + defaultMessage: `Failed to fetch agent groups.`, }); From 200a98f38d416e691b223ace3293c26b14871c34 Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 23 Mar 2021 12:07:34 -0700 Subject: [PATCH 06/16] refactor server side logic --- .../public/agents/use_osquery_policies.ts | 21 +++--- .../osquery/server/lib/parse_agent_groups.ts | 64 +++++++++++++++++ .../routes/action/create_action_route.ts | 70 ++----------------- .../osquery/server/routes/action/index.ts | 5 +- x-pack/plugins/osquery/server/routes/index.ts | 2 +- 5 files changed, 84 insertions(+), 78 deletions(-) create mode 100644 x-pack/plugins/osquery/server/lib/parse_agent_groups.ts diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts index 58377d7de0d5b..3553628ba3bbc 100644 --- a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -5,22 +5,23 @@ * 2.0. */ -import { useState } from 'react'; import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; export const useOsqueryPolicies = () => { - const [osqueryPolicies, setPolicies] = useState([]); const { http } = useKibana().services; - const { isLoading: osqueryPoliciesLoading } = useQuery(['osqueryPolicies'], async () => { - const policyResponse = await http.get('/api/fleet/package_policies', { - query: { - kuery: 'ingest-package-policies.package.name:osquery_elastic_managed', - }, - }); - setPolicies(policyResponse.items.map((p: { policy_id: string }) => p.policy_id)); - }); + const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies } = useQuery( + ['osqueryPolicies'], + async () => { + return await http.get('/api/fleet/package_policies', { + query: { + kuery: 'ingest-package-policies.package.name:osquery_elastic_managed', + }, + }); + }, + { select: (data) => data.items.map((p: { policy_id: string }) => p.policy_id) } + ); return { osqueryPoliciesLoading, osqueryPolicies }; }; diff --git a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts new file mode 100644 index 0000000000000..74c934e460be1 --- /dev/null +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts @@ -0,0 +1,64 @@ +/* + * 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 type { ElasticsearchClient } from 'src/core/server'; +import { OsqueryAppContext } from './osquery_app_context_services'; +import { AgentService } from '../../../fleet/server'; + +export interface AgentSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; +} + +function checkService(service: AgentService | undefined): service is AgentService { + return service !== undefined; +} + +export const parseAgentSelection = async ( + esClient: ElasticsearchClient, + context: OsqueryAppContext, + agentSelection: AgentSelection +) => { + let selectedAgents: string[] = []; + const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection; + const agentService = context.service.getAgentService(); + if (checkService(agentService)) { + if (allAgentsSelected) { + // TODO: actually fetch all the agents + const { agents: fetchedAgents } = await agentService.listAgents(esClient, { + perPage: 9000, + showInactive: false, + }); + selectedAgents.push(...fetchedAgents.map((a) => a.id)); + } else { + if (platformsSelected.length > 0 || policiesSelected.length > 0) { + const kueryFragments = []; + if (platformsSelected.length) { + kueryFragments.push( + ...platformsSelected.map((platform) => `local_metadata.os.platform:${platform}`) + ); + } + if (policiesSelected.length) { + kueryFragments.push(...policiesSelected.map((policy) => `policy_id:${policy}`)); + } + const kuery = kueryFragments.join(' or '); + // TODO: actually fetch all the agents + const { agents: fetchedAgents } = await agentService.listAgents(esClient, { + kuery, + perPage: 9000, + showInactive: false, + }); + selectedAgents.push(...fetchedAgents.map((a) => a.id)); + } + selectedAgents.push(...agents); + selectedAgents = Array.from(new Set(selectedAgents)); + } + } + return selectedAgents; +}; 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 9e5387bd2cfee..5cbabf53cab3a 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 @@ -12,15 +12,11 @@ import moment from 'moment'; import { IRouter } from '../../../../../../src/core/server'; import { packSavedObjectType, savedQuerySavedObjectType } from '../../../common/types'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export interface AgentsSelection { - agents: string[]; - allAgentsSelected: boolean; - platformsSelected: string[]; - policiesSelected: string[]; -} +import { parseAgentSelection, AgentSelection } from '../../lib/parse_agent_groups'; -export const createActionRoute = (router: IRouter) => { +export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( { path: '/internal/osquery/action', @@ -31,64 +27,8 @@ export const createActionRoute = (router: IRouter) => { }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asInternalUser; - let selectedAgents: string[] = []; - const { - agentSelection: { allAgentsSelected, platformsSelected, policiesSelected, agents }, - } = request.body as { agentSelection: AgentsSelection }; - // TODO: fix up the types here - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const extractIds = ({ body }: Record) => - body.hits.hits - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((o: Record) => o._source.local_metadata?.elastic.agent.id) - .filter((e: string) => e); - if (allAgentsSelected) { - // make a query for all agent ids - const idRes = await esClient.search({ - index: '.fleet-agents', - body: { - _source: 'local_metadata.elastic.agent.id', - size: 9000, - query: { - match_all: {}, - }, - }, - }); - const ids = extractIds(idRes); - selectedAgents.push(...ids); - } else { - if (platformsSelected.length > 0 || policiesSelected.length > 0) { - const filters: Array<{ - term: { [key: string]: string }; - }> = platformsSelected.map((platform) => ({ - term: { 'local_metadata.os.platform': platform }, - })); - filters.push(...policiesSelected.map((policyId) => ({ term: { policy_id: policyId } }))); - const query = { - index: '.fleet-agents', - body: { - _source: 'local_metadata.elastic.agent.id', - size: 9000, - query: { - bool: { - filter: [ - { - bool: { - should: filters, - }, - }, - ], - }, - }, - }, - }; - const ids = extractIds(await esClient.search<{}, {}>(query)); - selectedAgents.push(...ids); - } - selectedAgents.push(...agents); - selectedAgents = Array.from(new Set(selectedAgents)); - } - + const { agentSelection } = request.body as { agentSelection: AgentSelection }; + const selectedAgents = parseAgentSelection(esClient, osqueryContext, agentSelection); // @ts-expect-error update validation if (request.body.pack_id) { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/action/index.ts b/x-pack/plugins/osquery/server/routes/action/index.ts index 37e04fac5b986..fcf89d79dd0ee 100644 --- a/x-pack/plugins/osquery/server/routes/action/index.ts +++ b/x-pack/plugins/osquery/server/routes/action/index.ts @@ -7,7 +7,8 @@ import { IRouter } from '../../../../../../src/core/server'; import { createActionRoute } from './create_action_route'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const initActionRoutes = (router: IRouter) => { - createActionRoute(router); +export const initActionRoutes = (router: IRouter, context: OsqueryAppContext) => { + createActionRoute(router, context); }; diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts index 29df227583992..59d4085a77be1 100644 --- a/x-pack/plugins/osquery/server/routes/index.ts +++ b/x-pack/plugins/osquery/server/routes/index.ts @@ -13,7 +13,7 @@ import { OsqueryAppContext } from '../lib/osquery_app_context_services'; import { initPackRoutes } from './pack'; export const defineRoutes = (router: IRouter, context: OsqueryAppContext) => { - initActionRoutes(router); + initActionRoutes(router, context); initPackRoutes(router); initSavedQueryRoutes(router); initScheduledQueryRoutes(router, context); From 191fd63e393e74863003fac7c4f165f359a27531 Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 23 Mar 2021 12:23:13 -0700 Subject: [PATCH 07/16] more clean up --- x-pack/plugins/osquery/public/agents/translations.ts | 8 -------- x-pack/plugins/osquery/server/lib/parse_agent_groups.ts | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index 672c83a7e2fc2..0d9d9a8a12b8f 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -14,11 +14,3 @@ export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearch export const FAIL_ALL_AGENTS = i18n.translate('xpack.osquery.agents.failSearchDescription', { defaultMessage: `Failed to fetch agents`, }); - -export const ERROR_AGENT_GROUPS = i18n.translate('xpack.osquery.agentGroups.errorSearchDescription', { - defaultMessage: `An error has occurred while fetching agent groups.`, -}); - -export const FAIL_AGENT_GROUPS = i18n.translate('xpack.osquery.agentGroups.failSearchDescription', { - defaultMessage: `Failed to fetch agent groups.`, -}); 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 74c934e460be1..6059a0ed51e9a 100644 --- a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts @@ -33,7 +33,7 @@ export const parseAgentSelection = async ( // TODO: actually fetch all the agents const { agents: fetchedAgents } = await agentService.listAgents(esClient, { perPage: 9000, - showInactive: false, + showInactive: true, }); selectedAgents.push(...fetchedAgents.map((a) => a.id)); } else { @@ -52,7 +52,7 @@ export const parseAgentSelection = async ( const { agents: fetchedAgents } = await agentService.listAgents(esClient, { kuery, perPage: 9000, - showInactive: false, + showInactive: true, }); selectedAgents.push(...fetchedAgents.map((a) => a.id)); } From fe354f51f42f468684622f04464d96febeb17807 Mon Sep 17 00:00:00 2001 From: bryan Date: Wed, 24 Mar 2021 19:32:28 -0700 Subject: [PATCH 08/16] address pr comments --- .../search_strategy/osquery/agents/index.ts | 14 -- .../action_results/action_results_table.tsx | 18 +-- .../osquery/public/agents/agents_table.tsx | 40 +++--- .../osquery/public/agents/helpers.test.ts | 131 ++++++++++++++++++ .../plugins/osquery/public/agents/helpers.ts | 27 +++- .../osquery/public/agents/translations.ts | 16 +++ x-pack/plugins/osquery/public/agents/types.ts | 17 +++ .../osquery/public/agents/use_agent_groups.ts | 102 +++++++------- .../osquery/public/agents/use_all_agents.ts | 92 ++---------- .../public/agents/use_osquery_policies.ts | 3 +- .../osquery/server/lib/parse_agent_groups.ts | 7 +- .../osquery/factory/agents/index.ts | 1 - 12 files changed, 282 insertions(+), 186 deletions(-) create mode 100644 x-pack/plugins/osquery/public/agents/helpers.test.ts diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts index 092d781ddd923..57c7a42f2a481 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts @@ -11,23 +11,9 @@ import { Inspect, Maybe, PageInfoPaginated } from '../../common'; import { RequestOptionsPaginated } from '../..'; import { Agent } from '../../../shared_imports'; -interface BaseDataPoint { - key: string; - doc_count: number; -} - -export type AggregationDataPoint = BaseDataPoint & AgentAggregation; - -export interface AgentAggregation { - [key: string]: { - buckets: AggregationDataPoint[]; - }; -} - export interface AgentsStrategyResponse extends IEsSearchResponse { edges: Agent[]; totalCount: number; - aggregations?: AgentAggregation; pageInfo: PageInfoPaginated; inspect?: Maybe; } diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx index 1dd5b63eedc23..7c17d80dd5f22 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -22,6 +22,7 @@ import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; import { Direction, ResultEdges } from '../../common/search_strategy'; import { useRouterNavigate } from '../common/lib/kibana'; +import { useOsqueryPolicies } from '../agents/use_osquery_policies'; const DataContext = createContext([]); @@ -91,12 +92,8 @@ const ActionResultsTableComponent: React.FC = ({ action setVisibleColumns, ]); - const { data: agentsData } = useAllAgents({ - activePage: 0, - limit: 1000, - direction: Direction.desc, - sortField: 'updated_at', - }); + const osqueryPolicyData = useOsqueryPolicies(); + const { agentData } = useAllAgents(osqueryPolicyData); const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( () => ({ rowIndex, columnId }) => { @@ -134,8 +131,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent_status') { const agentIdValue = value.fields.agent_id[0]; - // @ts-expect-error update types - const agent = find(['_id', agentIdValue], agentsData?.agents); + const agent = find(['_id', agentIdValue], agentData?.agents); const online = agent?.active; const color = online ? 'success' : 'danger'; const label = online ? 'Online' : 'Offline'; @@ -144,8 +140,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent') { const agentIdValue = value.fields.agent_id[0]; - // @ts-expect-error update types - const agent = find(['_id', agentIdValue], agentsData?.agents); + const agent = find(['_id', agentIdValue], agentData?.agents); const agentName = agent?.local_metadata.host.name; // eslint-disable-next-line react-hooks/rules-of-hooks @@ -161,8 +156,7 @@ const ActionResultsTableComponent: React.FC = ({ action return '-'; }, - // @ts-expect-error update types - [actionId, agentsData?.agents, pagination.pageIndex, pagination.pageSize] + [actionId, agentData?.agents, pagination.pageIndex, pagination.pageSize] ); const tableSorting: EuiDataGridSorting = useMemo( diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index cf4ae296f384c..05179b9a8086c 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -10,7 +10,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiHealth, EuiHighlight } from '@ import { useAllAgents } from './use_all_agents'; import { useAgentGroups } from './use_agent_groups'; -import { Direction } from '../../common/search_strategy'; +import { useOsqueryPolicies } from './use_osquery_policies'; import { Agent } from '../../common/shared_imports'; import { getNumAgentsInGrouping, @@ -19,6 +19,13 @@ import { generateColorPicker, } from './helpers'; +import { + ALL_AGENTS_LABEL, + AGENT_PLATFORMS_LABEL, + AGENT_POLICY_LABEL, + AGENT_SELECTION_LABEL, +} from './translations'; + import { AGENT_GROUP_KEY, SelectedGroups, AgentOptionValue, GroupOptionValue } from './types'; export interface AgentsSelection { @@ -38,27 +45,18 @@ type GroupOption = EuiComboBoxOptionOption; const getColor = generateColorPicker(); const AgentsTableComponent: React.FC = ({ onChange }) => { - // handle paged fetching of agents - const [pageIndex /* , setPageIndex*/] = useState(0); - const [pageSize /* , setPageSize*/] = useState(1000); - const [sortField /* , setSortField*/] = useState('upgraded_at'); - const [sortDirection /* , setSortDirection*/] = useState(Direction.asc); - - const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups(); + const osqueryPolicyData = useOsqueryPolicies(); + const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups( + osqueryPolicyData + ); + const { agentData } = useAllAgents(osqueryPolicyData); const [loading, setLoading] = useState(true); const [options, setOptions] = useState([]); const [selectedOptions, setSelectedOptions] = useState([]); const [numAgentsSelected, setNumAgentsSelected] = useState(0); - const { data: agentData = { agents: [] } } = useAllAgents({ - activePage: pageIndex, - limit: pageSize, - direction: sortDirection, - sortField, - }); - useEffect(() => { - const allAgentsLabel = 'All agents'; + const allAgentsLabel = ALL_AGENTS_LABEL; const opts: GroupOption[] = [ { label: allAgentsLabel, @@ -75,7 +73,7 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { if (groups.platforms.length > 0) { const groupType = AGENT_GROUP_KEY.Platform; opts.push({ - label: 'Platform', + label: AGENT_PLATFORMS_LABEL, options: groups.platforms.map(({ name, size }) => ({ label: name, color: getColor(groupType), @@ -87,7 +85,7 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { if (groups.policies.length > 0) { const groupType = AGENT_GROUP_KEY.Policy; opts.push({ - label: 'Policy', + label: AGENT_POLICY_LABEL, options: groups.policies.map(({ name, size }) => ({ label: name, color: getColor(groupType), @@ -96,11 +94,11 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { }); } - if (agentData.agents.length > 0) { + if (agentData && agentData.list.length > 0) { const groupType = AGENT_GROUP_KEY.Agent; opts.push({ - label: 'Agents', - options: (agentData.agents as Agent[]).map((agent: Agent) => ({ + label: AGENT_SELECTION_LABEL, + options: (agentData.list as Agent[]).map((agent: Agent) => ({ label: agent.local_metadata.host.hostname, color: getColor(groupType), value: { diff --git a/x-pack/plugins/osquery/public/agents/helpers.test.ts b/x-pack/plugins/osquery/public/agents/helpers.test.ts new file mode 100644 index 0000000000000..48c5e86a80901 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/helpers.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { getNumOverlapped, getNumAgentsInGrouping, processAggregations } from './helpers'; +import { Overlap, SelectedGroups, AgentAggregation } from './types'; + +describe('processAggregations', () => { + it('should parse aggregation responses down into metadata objects', () => { + const input = { + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + platforms: { + buckets: [ + { + key: 'darwin', + doc_count: 200, + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + }, + ], + }, + }; + // TODO: make this type check + const { platforms, policies, overlap } = processAggregations( + (input as unknown) as AgentAggregation + ); + expect(platforms).toEqual([ + { + name: 'darwin', + size: 200, + }, + ]); + expect(policies).toEqual([ + { + name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + { + name: '8cd06880-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + ]); + expect(overlap).toEqual({ + darwin: { + '8cd06880-8a74-11eb-86cb-c58693443a4f': 100, + '8cd01a60-8a74-11eb-86cb-c58693443a4f': 100, + }, + }); + }); +}); + +describe('getNumAgentsInGrouping', () => { + it('should add up the quantities for the selected groups', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + }, + policy: { + policy_id1: 40, + }, + }; + expect(getNumAgentsInGrouping(selectedGroups)).toEqual(75); + }); +}); + +describe('getNumOverlapped', () => { + const overlap: Overlap = { + darwin: { + policy_id1: 15, + policy_id2: 35, + }, + linux: { + policy_id1: 25, + policy_id2: 10, + }, + }; + + it('should add up the quantities associated with a platform/policy selection', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + }, + policy: { + policy_id1: 40, + }, + }; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(25); + }); + + it('should gracefully handle missing group selections', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + windows: 40, + }, + policy: { + policy_id1: 40, + policy_id3: 40, + }, + }; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(25); + }); +}); diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index ad0edbfc902c4..72fdaa8273384 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -12,14 +12,21 @@ import { StrategyResponseType, Inspect, } from '../../common/search_strategy'; -import { AGENT_GROUP_KEY, SelectedGroups, Overlap, AgentOptionValue } from './types'; +import { + AGENT_GROUP_KEY, + SelectedGroups, + Overlap, + Group, + AgentOptionValue, + AgentAggregation, +} from './types'; export type InspectResponse = Inspect & { response: string[] }; export const getNumOverlapped = ({ policy, platform }: SelectedGroups, overlap: Overlap) => { let sum = 0; Object.keys(platform).forEach((plat) => { - const policies = overlap[plat]; + const policies = overlap[plat] ?? {}; Object.keys(policy).forEach((pol) => { sum += policies[pol] ?? 0; }); @@ -27,6 +34,22 @@ export const getNumOverlapped = ({ policy, platform }: SelectedGroups, overlap: return sum; }; +export const processAggregations = (aggs: AgentAggregation) => { + const platforms: Group[] = []; + const overlap: Overlap = {}; + for (const { key, doc_count: size, policies } of aggs.platforms.buckets) { + platforms.push({ name: key, size }); + overlap[key] = policies.buckets.reduce((acc: { [key: string]: number }, pol) => { + acc[pol.key] = pol.doc_count; + return acc; + }, {} as { [key: string]: number }); + } + return { + platforms, + overlap, + policies: aggs.policies.buckets.map((o) => ({ name: o.key, size: o.doc_count })), + }; +}; export const generateColorPicker = () => { const visColorsBehindText = euiPaletteColorBlindBehindText(); const typeColors = new Map(); diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index 0d9d9a8a12b8f..934e7075b212e 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -7,6 +7,22 @@ import { i18n } from '@kbn/i18n'; +export const ALL_AGENTS_LABEL = i18n.translate('xpack.osquery.agents.allAgentsLabel', { + defaultMessage: `All agents`, +}); + +export const AGENT_PLATFORMS_LABEL = i18n.translate('xpack.osquery.agents.platformLabel', { + defaultMessage: `Platform`, +}); + +export const AGENT_POLICY_LABEL = i18n.translate('xpack.osquery.agents.policyLabel', { + defaultMessage: `Policy`, +}); + +export const AGENT_SELECTION_LABEL = i18n.translate('xpack.osquery.agents.selectionLabel', { + defaultMessage: `Agents`, +}); + export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { defaultMessage: `An error has occurred on all agents search`, }); diff --git a/x-pack/plugins/osquery/public/agents/types.ts b/x-pack/plugins/osquery/public/agents/types.ts index 4aa245f2b5be6..3d283e05a9f9e 100644 --- a/x-pack/plugins/osquery/public/agents/types.ts +++ b/x-pack/plugins/osquery/public/agents/types.ts @@ -5,6 +5,23 @@ * 2.0. */ +interface BaseDataPoint { + key: string; + doc_count: number; +} + +export type AggregationDataPoint = AgentAggregation & BaseDataPoint; + +export interface AgentAggregation { + [aggregationKey: string]: { + buckets: AggregationDataPoint[]; + }; +} + +export interface Group { + name: string; + size: number; +} export interface Overlap { [platform: string]: { [policy: string]: number }; } diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts index 4b07ca16c5d07..ce10894303f1c 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -7,7 +7,6 @@ import { useState } from 'react'; import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; -import { useOsqueryPolicies } from './use_osquery_policies'; import { OsqueryQueries, @@ -15,76 +14,75 @@ import { AgentsStrategyResponse, } from '../../common/search_strategy'; -import { generateTablePaginationOptions } from './helpers'; -import { Overlap } from './types'; +import { generateTablePaginationOptions, processAggregations } from './helpers'; +import { Overlap, Group } from './types'; -interface Group { - name: string; - size: number; +interface UseAgentGroups { + osqueryPolicies: string[]; + osqueryPoliciesLoading: boolean; } -export const useAgentGroups = () => { +export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => { const { data } = useKibana().services; - const { osqueryPolicies, osqueryPoliciesLoading } = useOsqueryPolicies(); const [platforms, setPlatforms] = useState([]); - const [policyOptions, setPolicyOptions] = useState([]); + const [policies, setPolicies] = useState([]); const [loading, setLoading] = useState(true); const [overlap, setOverlap] = useState(() => ({})); const [totalCount, setTotalCount] = useState(0); - useQuery(['agentGroups', osqueryPoliciesLoading], async () => { - if (osqueryPoliciesLoading) return null; - const responseData = await data.search - .search( - { - filterQuery: { terms: { policy_id: osqueryPolicies } }, - factoryQueryType: OsqueryQueries.agents, - aggregations: { - platforms: { + useQuery( + ['agentGroups'], + async () => { + const responseData = await data.search + .search( + { + filterQuery: { terms: { policy_id: osqueryPolicies } }, + factoryQueryType: OsqueryQueries.agents, + aggregations: { + platforms: { + field: 'local_metadata.os.platform', + subaggs: { policies: 'policy_id' }, + }, + policies: 'policy_id', + }, + pagination: generateTablePaginationOptions(0, 9000), + sort: { + direction: 'asc', field: 'local_metadata.os.platform', - subaggs: { policies: 'policy_id' }, }, - policies: 'policy_id', - }, - pagination: generateTablePaginationOptions(0, 9000), - sort: { - direction: 'asc', - field: 'local_metadata.os.platform', - }, - } as AgentsRequestOptions, - { - strategy: 'osquerySearchStrategy', - } - ) - .toPromise(); - setLoading(false); - if (responseData.aggregations) { - const aggs = responseData.aggregations; - const newPlatforms: Group[] = []; - const newOverlap: Overlap = {}; - for (const { key, doc_count: docCount, policies } of aggs.platforms.buckets) { - newPlatforms.push({ name: key, size: docCount }); - newOverlap[key] = policies.buckets.reduce( - (acc: { [key: string]: number }, pol: { key: string; doc_count: number }) => { - acc[pol.key] = pol.doc_count; - return acc; - }, - {} as { [key: string]: number } - ); + } as AgentsRequestOptions, + { + strategy: 'osquerySearchStrategy', + } + ) + .toPromise(); + + if (responseData.rawResponse.aggregations) { + const { + platforms: newPlatforms, + overlap: newOverlap, + policies: newPolicies, + } = processAggregations(responseData.rawResponse.aggregations); + + setPlatforms(newPlatforms); + setOverlap(newOverlap); + setPolicies(newPolicies); } - setPlatforms(newPlatforms); - setOverlap(newOverlap); - setPolicyOptions(aggs.policies.buckets.map((o) => ({ name: o.key, size: o.doc_count }))); + + setLoading(false); + setTotalCount(responseData.totalCount); + }, + { + enabled: !osqueryPoliciesLoading, } - setTotalCount(responseData.totalCount); - }); + ); return { loading, totalCount, groups: { platforms, - policies: policyOptions, + policies, overlap, }, }; diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index 663c6936fe55b..daf66609ce23f 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -5,94 +5,32 @@ * 2.0. */ -import deepEqual from 'fast-deep-equal'; -import { useEffect, useState } from 'react'; import { useQuery } from 'react-query'; -import { createFilter } from '../common/helpers'; import { useKibana } from '../common/lib/kibana'; -import { - PageInfoPaginated, - OsqueryQueries, - AgentsRequestOptions, - AgentsStrategyResponse, - Direction, -} from '../../common/search_strategy'; -import { ESTermQuery } from '../../common/typed_json'; -import { Agent } from '../../common/shared_imports'; - -import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; - -export interface AgentsArgs { - agents: Agent[]; - id: string; - inspect: InspectResponse; - isInspected: boolean; - pageInfo: PageInfoPaginated; - totalCount: number; -} interface UseAllAgents { - activePage: number; - direction: Direction; - limit: number; - sortField: string; - filterQuery?: ESTermQuery | string; - skip?: boolean; + osqueryPolicies: string[]; + osqueryPoliciesLoading: boolean; } -export const useAllAgents = ({ - activePage, - direction, - limit, - sortField, - filterQuery, - skip = false, -}: UseAllAgents) => { - const { data } = useKibana().services; - - const [agentsRequest, setHostRequest] = useState(null); - - const response = useQuery( - ['agents', { activePage, direction, limit, sortField }], +export const useAllAgents = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents) => { + // TODO: properly fetch these in an async manner + const { http } = useKibana().services; + const { isLoading: agentsLoading, data: agentData } = useQuery( + ['agents', osqueryPolicies], async () => { - if (!agentsRequest) return Promise.resolve(); - - const responseData = await data.search - .search(agentsRequest, { - strategy: 'osquerySearchStrategy', - }) - .toPromise(); - - return { - ...responseData, - agents: responseData.edges, - inspect: getInspectResponse(responseData), - }; + return await http.get('/api/fleet/agents', { + query: { + kuery: osqueryPolicies.map((p) => `policy_id:${p}`).join(' or '), + perPage: 9000, + }, + }); }, { - enabled: !skip && !!agentsRequest, + enabled: !osqueryPoliciesLoading, } ); - useEffect(() => { - setHostRequest((prevRequest) => { - const myRequest = { - ...(prevRequest ?? {}), - factoryQueryType: OsqueryQueries.agents, - filterQuery: createFilter(filterQuery), - pagination: generateTablePaginationOptions(activePage, limit), - sort: { - direction, - field: sortField, - }, - }; - if (!deepEqual(prevRequest, myRequest)) { - return myRequest; - } - return prevRequest; - }); - }, [activePage, direction, filterQuery, limit, sortField]); - - return response; + return { agentsLoading, agentData }; }; diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts index 3553628ba3bbc..89f54e9f85c7e 100644 --- a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -7,6 +7,7 @@ import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; export const useOsqueryPolicies = () => { const { http } = useKibana().services; @@ -16,7 +17,7 @@ export const useOsqueryPolicies = () => { async () => { return await http.get('/api/fleet/package_policies', { query: { - kuery: 'ingest-package-policies.package.name:osquery_elastic_managed', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:osquery_elastic_managed`, }, }); }, 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 6059a0ed51e9a..975770e594367 100644 --- a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts @@ -7,7 +7,6 @@ import type { ElasticsearchClient } from 'src/core/server'; import { OsqueryAppContext } from './osquery_app_context_services'; -import { AgentService } from '../../../fleet/server'; export interface AgentSelection { agents: string[]; @@ -16,10 +15,6 @@ export interface AgentSelection { policiesSelected: string[]; } -function checkService(service: AgentService | undefined): service is AgentService { - return service !== undefined; -} - export const parseAgentSelection = async ( esClient: ElasticsearchClient, context: OsqueryAppContext, @@ -28,7 +23,7 @@ export const parseAgentSelection = async ( let selectedAgents: string[] = []; const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection; const agentService = context.service.getAgentService(); - if (checkService(agentService)) { + if (agentService) { if (allAgentsSelected) { // TODO: actually fetch all the agents const { agents: fetchedAgents } = await agentService.listAgents(esClient, { diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts index 395b3d6504fa8..1f7fbccb68682 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts @@ -38,7 +38,6 @@ export const allAgents: OsqueryFactory = { ...response, inspect, edges: response.rawResponse.hits.hits.map((hit) => ({ _id: hit._id, ...hit._source })), - aggregations: response.rawResponse.aggregations, totalCount: response.rawResponse.hits.total, pageInfo: { activePage: activePage ?? 0, From dbd24f98cae4c43653ffd8d461dd9105ed2d5681 Mon Sep 17 00:00:00 2001 From: bryan Date: Thu, 25 Mar 2021 10:53:09 -0700 Subject: [PATCH 09/16] fix types, await the parsed agents on the server side --- x-pack/plugins/osquery/public/agents/helpers.test.ts | 7 ++----- x-pack/plugins/osquery/public/agents/helpers.ts | 12 +++++++----- x-pack/plugins/osquery/public/agents/types.ts | 12 +++++------- .../server/routes/action/create_action_route.ts | 2 +- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/osquery/public/agents/helpers.test.ts b/x-pack/plugins/osquery/public/agents/helpers.test.ts index 48c5e86a80901..b16cb7ef84e21 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.test.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.test.ts @@ -6,7 +6,7 @@ */ import { getNumOverlapped, getNumAgentsInGrouping, processAggregations } from './helpers'; -import { Overlap, SelectedGroups, AgentAggregation } from './types'; +import { Overlap, SelectedGroups } from './types'; describe('processAggregations', () => { it('should parse aggregation responses down into metadata objects', () => { @@ -44,10 +44,7 @@ describe('processAggregations', () => { ], }, }; - // TODO: make this type check - const { platforms, policies, overlap } = processAggregations( - (input as unknown) as AgentAggregation - ); + const { platforms, policies, overlap } = processAggregations(input); expect(platforms).toEqual([ { name: 'darwin', diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index 72fdaa8273384..ded17f5a399a0 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Aggregate, TermsAggregate } from '@elastic/elasticsearch/api/types'; import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { PaginationInputPaginated, @@ -18,7 +19,7 @@ import { Overlap, Group, AgentOptionValue, - AgentAggregation, + AggregationDataPoint, } from './types'; export type InspectResponse = Inspect & { response: string[] }; @@ -33,11 +34,12 @@ export const getNumOverlapped = ({ policy, platform }: SelectedGroups, overlap: }); return sum; }; - -export const processAggregations = (aggs: AgentAggregation) => { +export const processAggregations = (aggs: Record) => { const platforms: Group[] = []; const overlap: Overlap = {}; - for (const { key, doc_count: size, policies } of aggs.platforms.buckets) { + const platformTerms = aggs.platforms as TermsAggregate; + const policyTerms = aggs.policies as TermsAggregate; + for (const { key, doc_count: size, policies } of platformTerms.buckets) { platforms.push({ name: key, size }); overlap[key] = policies.buckets.reduce((acc: { [key: string]: number }, pol) => { acc[pol.key] = pol.doc_count; @@ -47,7 +49,7 @@ export const processAggregations = (aggs: AgentAggregation) => { return { platforms, overlap, - policies: aggs.policies.buckets.map((o) => ({ name: o.key, size: o.doc_count })), + policies: policyTerms.buckets.map((o) => ({ name: o.key, size: o.doc_count })), }; }; export const generateColorPicker = () => { diff --git a/x-pack/plugins/osquery/public/agents/types.ts b/x-pack/plugins/osquery/public/agents/types.ts index 3d283e05a9f9e..2fa8ddaf345cd 100644 --- a/x-pack/plugins/osquery/public/agents/types.ts +++ b/x-pack/plugins/osquery/public/agents/types.ts @@ -5,18 +5,16 @@ * 2.0. */ +import { TermsAggregate } from '@elastic/elasticsearch/api/types'; + interface BaseDataPoint { key: string; doc_count: number; } -export type AggregationDataPoint = AgentAggregation & BaseDataPoint; - -export interface AgentAggregation { - [aggregationKey: string]: { - buckets: AggregationDataPoint[]; - }; -} +export type AggregationDataPoint = BaseDataPoint & { + [key: string]: TermsAggregate; +}; export interface Group { name: string; 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 5cbabf53cab3a..7068243cc0fb7 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 @@ -28,7 +28,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon async (context, request, response) => { const esClient = context.core.elasticsearch.client.asInternalUser; const { agentSelection } = request.body as { agentSelection: AgentSelection }; - const selectedAgents = parseAgentSelection(esClient, osqueryContext, agentSelection); + const selectedAgents = await parseAgentSelection(esClient, osqueryContext, agentSelection); // @ts-expect-error update validation if (request.body.pack_id) { const savedObjectsClient = context.core.savedObjects.client; From 7111ac5a7313478826a0f6a3f6ea3919afa80957 Mon Sep 17 00:00:00 2001 From: bryan Date: Thu, 25 Mar 2021 11:41:46 -0700 Subject: [PATCH 10/16] primitive is spelled with an i --- .../plugins/osquery/server/search_strategy/osquery/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts index 3b80188540cd9..8c68a42ad1091 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts @@ -16,14 +16,14 @@ interface AggregationResult { }; } -function isPrimative(a: AggregationValue): a is string { +function isPrimitive(a: AggregationValue): a is string { return typeof a === 'string'; } export function parseAggregator(aggs: Aggregation) { return Object.keys(aggs).reduce((acc, aggKey) => { const value = aggs[aggKey]; - if (isPrimative(value)) { + if (isPrimitive(value)) { acc[aggKey] = { terms: { field: aggs[aggKey] as string, From 8db9196798f96b5e50e62495f0d4aafe203f8e99 Mon Sep 17 00:00:00 2001 From: bryan Date: Thu, 25 Mar 2021 11:55:37 -0700 Subject: [PATCH 11/16] agents come in as list rather than agents --- .../osquery/public/action_results/action_results_table.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx index b35fe3b5265e6..0c09cbce2a2c7 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -131,7 +131,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent_status') { const agentIdValue = value.fields?.agent_id[0]; - const agent = find(['_id', agentIdValue], agentData?.agents); + const agent = find(['_id', agentIdValue], agentData?.list); const online = agent?.active; const color = online ? 'success' : 'danger'; const label = online ? 'Online' : 'Offline'; @@ -140,7 +140,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent') { const agentIdValue = value.fields?.agent_id[0]; - const agent = find(['_id', agentIdValue], agentData?.agents); + const agent = find(['_id', agentIdValue], agentData?.list); const agentName = agent?.local_metadata.host.name; // eslint-disable-next-line react-hooks/rules-of-hooks @@ -157,7 +157,7 @@ const ActionResultsTableComponent: React.FC = ({ action return '-'; }, - [actionId, agentData?.agents, pagination.pageIndex, pagination.pageSize] + [actionId, agentData?.list, pagination.pageIndex, pagination.pageSize] ); const tableSorting: EuiDataGridSorting = useMemo( From cd26dba4479e5eeac4d04aa6f6ffae7e6556a510 Mon Sep 17 00:00:00 2001 From: bryan Date: Thu, 25 Mar 2021 17:29:55 -0700 Subject: [PATCH 12/16] added more helpers tests, added some edge case guarding --- .../osquery/public/agents/helpers.test.ts | 94 +++++++++++++++++++ .../plugins/osquery/public/agents/helpers.ts | 27 ++++-- 2 files changed, 113 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/osquery/public/agents/helpers.test.ts b/x-pack/plugins/osquery/public/agents/helpers.test.ts index b16cb7ef84e21..3efd1b877d1a0 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.test.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.test.ts @@ -9,6 +9,66 @@ import { getNumOverlapped, getNumAgentsInGrouping, processAggregations } from '. import { Overlap, SelectedGroups } from './types'; describe('processAggregations', () => { + it('should handle empty inputs properly', () => { + const input = {}; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([]); + expect(policies).toEqual([]); + expect(overlap).toEqual({}); + }); + it('should handle platforms with no policies', () => { + const input = { + platforms: { + buckets: [ + { + key: 'darwin', + doc_count: 200, + policies: { + buckets: [], + }, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([ + { + name: 'darwin', + size: 200, + }, + ]); + expect(policies).toEqual([]); + expect(overlap).toEqual({}); + }); + it('should handle policies with no platforms', () => { + const input = { + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([]); + expect(policies).toEqual([ + { + name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + { + name: '8cd06880-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + ]); + expect(overlap).toEqual({}); + }); it('should parse aggregation responses down into metadata objects', () => { const input = { policies: { @@ -71,6 +131,11 @@ describe('processAggregations', () => { }); describe('getNumAgentsInGrouping', () => { + it('should handle empty objects', () => { + const selectedGroups: SelectedGroups = {}; + expect(getNumAgentsInGrouping(selectedGroups)).toEqual(0); + }); + it('should add up the quantities for the selected groups', () => { const selectedGroups: SelectedGroups = { platform: { @@ -110,6 +175,35 @@ describe('getNumOverlapped', () => { expect(computedOverlap).toBe(25); }); + it('should gracefully handle empty objects', () => { + const selectedGroups: SelectedGroups = {}; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing platforms', () => { + const selectedGroups: SelectedGroups = { + policy: { + policy_id1: 40, + policy_id3: 40, + }, + }; + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing policies', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + windows: 40, + }, + }; + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + it('should gracefully handle missing group selections', () => { const selectedGroups: SelectedGroups = { platform: { diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index ded17f5a399a0..830fca5f57caa 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -24,7 +24,10 @@ import { export type InspectResponse = Inspect & { response: string[] }; -export const getNumOverlapped = ({ policy, platform }: SelectedGroups, overlap: Overlap) => { +export const getNumOverlapped = ( + { policy = {}, platform = {} }: SelectedGroups, + overlap: Overlap +) => { let sum = 0; Object.keys(platform).forEach((plat) => { const policies = overlap[plat] ?? {}; @@ -39,17 +42,25 @@ export const processAggregations = (aggs: Record) => { const overlap: Overlap = {}; const platformTerms = aggs.platforms as TermsAggregate; const policyTerms = aggs.policies as TermsAggregate; - for (const { key, doc_count: size, policies } of platformTerms.buckets) { - platforms.push({ name: key, size }); - overlap[key] = policies.buckets.reduce((acc: { [key: string]: number }, pol) => { - acc[pol.key] = pol.doc_count; - return acc; - }, {} as { [key: string]: number }); + + const policies = policyTerms?.buckets.map((o) => ({ name: o.key, size: o.doc_count })) ?? []; + + if (platformTerms?.buckets) { + for (const { key, doc_count: size, policies: platformPolicies } of platformTerms.buckets) { + platforms.push({ name: key, size }); + if (platformPolicies?.buckets && policies.length > 0) { + overlap[key] = platformPolicies.buckets.reduce((acc: { [key: string]: number }, pol) => { + acc[pol.key] = pol.doc_count; + return acc; + }, {} as { [key: string]: number }); + } + } } + return { platforms, overlap, - policies: policyTerms.buckets.map((o) => ({ name: o.key, size: o.doc_count })), + policies, }; }; export const generateColorPicker = () => { From 6c00d9e99863e4c0ed1067bd45b89264ea6fb781 Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 30 Mar 2021 12:31:24 -0700 Subject: [PATCH 13/16] unwrap agents off response in useAllAgents --- .../public/action_results/action_results_table.tsx | 8 ++++---- x-pack/plugins/osquery/public/agents/agents_table.tsx | 8 ++++---- x-pack/plugins/osquery/public/agents/use_all_agents.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx index 0c09cbce2a2c7..660b837da6d93 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -93,7 +93,7 @@ const ActionResultsTableComponent: React.FC = ({ action ]); const osqueryPolicyData = useOsqueryPolicies(); - const { agentData } = useAllAgents(osqueryPolicyData); + const { agents } = useAllAgents(osqueryPolicyData); const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( () => ({ rowIndex, columnId }) => { @@ -131,7 +131,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent_status') { const agentIdValue = value.fields?.agent_id[0]; - const agent = find(['_id', agentIdValue], agentData?.list); + const agent = find(['_id', agentIdValue], agents); const online = agent?.active; const color = online ? 'success' : 'danger'; const label = online ? 'Online' : 'Offline'; @@ -140,7 +140,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent') { const agentIdValue = value.fields?.agent_id[0]; - const agent = find(['_id', agentIdValue], agentData?.list); + const agent = find(['_id', agentIdValue], agents); const agentName = agent?.local_metadata.host.name; // eslint-disable-next-line react-hooks/rules-of-hooks @@ -157,7 +157,7 @@ const ActionResultsTableComponent: React.FC = ({ action return '-'; }, - [actionId, agentData?.list, pagination.pageIndex, pagination.pageSize] + [actionId, agents, pagination.pageIndex, pagination.pageSize] ); const tableSorting: EuiDataGridSorting = useMemo( diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 05179b9a8086c..f4103dc2227e9 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -49,7 +49,7 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups( osqueryPolicyData ); - const { agentData } = useAllAgents(osqueryPolicyData); + const { agents } = useAllAgents(osqueryPolicyData); const [loading, setLoading] = useState(true); const [options, setOptions] = useState([]); const [selectedOptions, setSelectedOptions] = useState([]); @@ -94,11 +94,11 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { }); } - if (agentData && agentData.list.length > 0) { + if (agents && agents.length > 0) { const groupType = AGENT_GROUP_KEY.Agent; opts.push({ label: AGENT_SELECTION_LABEL, - options: (agentData.list as Agent[]).map((agent: Agent) => ({ + options: (agents as Agent[]).map((agent: Agent) => ({ label: agent.local_metadata.host.hostname, color: getColor(groupType), value: { @@ -112,7 +112,7 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { } setLoading(false); setOptions(opts); - }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agentData]); + }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents]); const onSelection = useCallback( (selection: GroupOption[]) => { diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index daf66609ce23f..607f9ae007692 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -32,5 +32,5 @@ export const useAllAgents = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAll } ); - return { agentsLoading, agentData }; + return { agentsLoading, agents: agentData?.list }; }; From 4914692ab892b1f29e3407452c1a09ebf494f939 Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 30 Mar 2021 14:37:05 -0700 Subject: [PATCH 14/16] used proper types for aggregations --- .../common/search_strategy/osquery/index.ts | 9 +--- .../osquery/public/agents/use_agent_groups.ts | 18 ++++++-- .../public/agents/use_osquery_policies.ts | 2 +- .../factory/agents/query.all_agents.dsl.ts | 7 +--- .../server/search_strategy/osquery/helpers.ts | 42 ------------------- 5 files changed, 20 insertions(+), 58 deletions(-) delete mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts index 9e12bae807454..331edc3603fe4 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts @@ -19,6 +19,7 @@ import { AgentsStrategyResponse, AgentsRequestOptions } from './agents'; import { ResultsStrategyResponse, ResultsRequestOptions } from './results'; import { DocValueFields, SortField, PaginationInputPaginated } from '../common'; +import type { estypes } from '@elastic/elasticsearch'; export * from './actions'; export * from './agents'; @@ -34,15 +35,9 @@ export enum OsqueryQueries { export type FactoryQueryTypes = OsqueryQueries; -export type AggregationValue = string | { field: string; subaggs: Aggregation }; - -export interface Aggregation { - [key: string]: AggregationValue; -} - export interface RequestBasicOptions extends IEsSearchRequest { filterQuery: ESQuery | string | undefined; - aggregations?: Aggregation; + aggregations?: Record; docValueFields?: DocValueFields[]; factoryQueryType?: FactoryQueryTypes; } diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts index ce10894303f1c..9bd80d220b313 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -40,10 +40,22 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA factoryQueryType: OsqueryQueries.agents, aggregations: { platforms: { - field: 'local_metadata.os.platform', - subaggs: { policies: 'policy_id' }, + terms: { + field: 'local_metadata.os.platform', + }, + aggs: { + policies: { + terms: { + field: 'policy_id' + } + } + }, }, - policies: 'policy_id', + policies: { + terms: { + field: 'policy_id', + } + } }, pagination: generateTablePaginationOptions(0, 9000), sort: { diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts index 89f54e9f85c7e..f786e9167d2f8 100644 --- a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -17,7 +17,7 @@ export const useOsqueryPolicies = () => { async () => { return await http.get('/api/fleet/package_policies', { query: { - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:osquery_elastic_managed`, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:osquery_manager`, }, }); }, diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts index ba3df094634a0..9e369e3df08fc 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts @@ -8,7 +8,6 @@ import { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; import { AgentsRequestOptions } from '../../../../../common/search_strategy'; import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; -import { parseAggregator } from '../../helpers'; export const buildAgentsQuery = ({ filterQuery, @@ -31,7 +30,7 @@ export const buildAgentsQuery = ({ filter, }, }, - aggs: {}, + aggs: aggregations, track_total_hits: true, sort: [ { @@ -45,9 +44,7 @@ export const buildAgentsQuery = ({ }, }; - if (aggregations) { - dslQuery.body.aggs = parseAggregator(aggregations); - } + console.log(JSON.stringify(dslQuery, null, 2)) return dslQuery; }; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts deleted file mode 100644 index 8c68a42ad1091..0000000000000 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/helpers.ts +++ /dev/null @@ -1,42 +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 { Aggregation, AggregationValue } from '../../../common/search_strategy/osquery'; - -interface AggregationResult { - [key: string]: { - terms: { - field: string; - }; - aggs?: AggregationResult; - }; -} - -function isPrimitive(a: AggregationValue): a is string { - return typeof a === 'string'; -} - -export function parseAggregator(aggs: Aggregation) { - return Object.keys(aggs).reduce((acc, aggKey) => { - const value = aggs[aggKey]; - if (isPrimitive(value)) { - acc[aggKey] = { - terms: { - field: aggs[aggKey] as string, - }, - }; - } else { - acc[aggKey] = { - terms: { - field: value.field, - }, - aggs: parseAggregator(value.subaggs), - }; - } - return acc; - }, {} as AggregationResult); -} From 41e5945701b01e8996ecfb5d40582a4a29d8a6f0 Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 30 Mar 2021 15:42:56 -0700 Subject: [PATCH 15/16] translations for more ui text --- .../osquery/public/agents/agents_table.tsx | 9 ++++----- .../osquery/public/agents/translations.ts | 20 +++++++++++++++++++ .../factory/agents/query.all_agents.dsl.ts | 2 -- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index f4103dc2227e9..5f1b6a0d2f0b1 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -23,7 +23,9 @@ import { ALL_AGENTS_LABEL, AGENT_PLATFORMS_LABEL, AGENT_POLICY_LABEL, + SELECT_AGENT_LABEL, AGENT_SELECTION_LABEL, + generateSelectedAgentsMessage, } from './translations'; import { AGENT_GROUP_KEY, SelectedGroups, AgentOptionValue, GroupOptionValue } from './types'; @@ -204,13 +206,10 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { ); }, []); - const selectedAgentsText = `${numAgentsSelected} agent${ - numAgentsSelected === 1 ? '' : 's' - } selected.`; return (
-

Select Agents

- {numAgentsSelected > 0 ? {selectedAgentsText} : ''} +

{SELECT_AGENT_LABEL}

+ {numAgentsSelected > 0 ? {generateSelectedAgentsMessage(numAgentsSelected)} : ''}   { + if (numAgents === 0) { + return ''; + } else if (numAgents === 1) { + return i18n.translate('xpack.osquery.agents.oneSelectedAgentText', { + defaultMessage: '{numAgents} agent selected.', + values: { numAgents }, + }); + } else { + return i18n.translate('xpack.osquery.agents.mulitpleSelectedAgentsText', { + defaultMessage: '{numAgents} agents selected.', + values: { numAgents }, + }); + } +}; + export const ALL_AGENTS_LABEL = i18n.translate('xpack.osquery.agents.allAgentsLabel', { defaultMessage: `All agents`, }); @@ -23,6 +39,10 @@ export const AGENT_SELECTION_LABEL = i18n.translate('xpack.osquery.agents.select defaultMessage: `Agents`, }); +export const SELECT_AGENT_LABEL = i18n.translate('xpack.osquery.agents.selectAgentLabel', { + defaultMessage: `Select Agents`, +}); + export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { defaultMessage: `An error has occurred on all agents search`, }); diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts index 9e369e3df08fc..52101462270c7 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts @@ -44,7 +44,5 @@ export const buildAgentsQuery = ({ }, }; - console.log(JSON.stringify(dslQuery, null, 2)) - return dslQuery; }; From 8d5aa8d495de2660c33c9043f3571f3eb0d1c38f Mon Sep 17 00:00:00 2001 From: bryan Date: Tue, 30 Mar 2021 22:59:17 -0700 Subject: [PATCH 16/16] fix linting errors --- .../osquery/common/search_strategy/osquery/index.ts | 2 +- .../plugins/osquery/public/agents/use_agent_groups.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts index 331edc3603fe4..d07af8c878c51 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { @@ -19,7 +20,6 @@ import { AgentsStrategyResponse, AgentsRequestOptions } from './agents'; import { ResultsStrategyResponse, ResultsRequestOptions } from './results'; import { DocValueFields, SortField, PaginationInputPaginated } from '../common'; -import type { estypes } from '@elastic/elasticsearch'; export * from './actions'; export * from './agents'; diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts index 9bd80d220b313..0eaca65d02d4b 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -46,16 +46,16 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA aggs: { policies: { terms: { - field: 'policy_id' - } - } + field: 'policy_id', + }, + }, }, }, policies: { terms: { field: 'policy_id', - } - } + }, + }, }, pagination: generateTablePaginationOptions(0, 9000), sort: {