[Osquery] Fix Live query form errors handling (#99015)

This commit is contained in:
Patryk Kopyciński 2021-05-03 18:50:07 +02:00 committed by GitHub
parent 85b78711c6
commit 3f39f5e275
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 506 additions and 298 deletions

View file

@ -8,7 +8,7 @@
import { estypes } from '@elastic/elasticsearch';
import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common';
import { Inspect, Maybe, PageInfoPaginated } from '../../common';
import { Inspect, Maybe, PageInfoPaginated, SortField } from '../../common';
import { RequestOptionsPaginated } from '../..';
export type ResultEdges = estypes.SearchResponse<unknown>['hits']['hits'];
@ -20,7 +20,8 @@ export interface ResultsStrategyResponse extends IEsSearchResponse {
inspect?: Maybe<Inspect>;
}
export interface ResultsRequestOptions extends RequestOptionsPaginated {
export interface ResultsRequestOptions extends Omit<RequestOptionsPaginated, 'sort'> {
actionId: string;
agentId?: string;
sort: SortField[];
}

View file

@ -9,7 +9,8 @@
"id": "osquery",
"kibanaVersion": "kibana",
"optionalPlugins": [
"home"
"home",
"lens"
],
"requiredBundles": [
"esUiShared",

View file

@ -18,8 +18,10 @@ import {
EuiDescriptionList,
EuiInMemoryTable,
EuiCodeBlock,
EuiProgress,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { pagePathGetters } from '../../../fleet/public';
import { useActionResults } from './use_action_results';
@ -27,6 +29,10 @@ import { useAllResults } from '../results/use_all_results';
import { Direction } from '../../common/search_strategy';
import { useKibana } from '../common/lib/kibana';
const StyledEuiCard = styled(EuiCard)`
position: relative;
`;
interface ActionResultsSummaryProps {
actionId: string;
agentIds?: string[];
@ -66,8 +72,12 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
actionId,
activePage: pageIndex,
limit: pageSize,
direction: Direction.asc,
sortField: '@timestamp',
sort: [
{
field: '@timestamp',
direction: Direction.asc,
},
],
isLive,
});
@ -215,14 +225,15 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
<>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiCard title="" description="" textAlign="left">
<StyledEuiCard title="" description="" textAlign="left">
{notRespondedCount ? <EuiProgress size="xs" position="absolute" /> : null}
<EuiDescriptionList
compressed
textStyle="reverse"
type="responsiveColumn"
listItems={listItems}
/>
</EuiCard>
</StyledEuiCard>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -15,8 +15,8 @@ import {
ResultEdges,
PageInfoPaginated,
OsqueryQueries,
ResultsRequestOptions,
ResultsStrategyResponse,
ActionResultsRequestOptions,
ActionResultsStrategyResponse,
Direction,
} from '../../common/search_strategy';
import { ESTermQuery } from '../../common/typed_json';
@ -65,7 +65,7 @@ export const useActionResults = ({
['actionResults', { actionId }],
async () => {
const responseData = await data.search
.search<ResultsRequestOptions, ResultsStrategyResponse>(
.search<ActionResultsRequestOptions, ActionResultsStrategyResponse>(
{
actionId,
factoryQueryType: OsqueryQueries.actionResults,

View file

@ -8,7 +8,7 @@
import { filter } from 'lodash/fp';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { produce } from 'immer';
import { i18n } from '@kbn/i18n';
@ -45,7 +45,8 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
application: { getUrlForApp },
http,
} = useKibana().services;
const { replace } = useHistory();
const { state: locationState } = useLocation();
const { replace, go } = useHistory();
const agentsLinkHref = useMemo(() => {
if (!policy?.policy_id) return '#';
@ -93,6 +94,16 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
}
}, [editMode, http, policy?.policy_id, policyAgentsCount]);
useEffect(() => {
/*
in order to enable Osquery side nav we need to refresh the whole Kibana
TODO: Find a better solution
*/
if (editMode && locationState?.forceRefresh) {
go(0);
}
}, [editMode, go, locationState]);
useEffect(() => {
/*
by default Fleet set up streams with an empty scheduled query,
@ -124,6 +135,9 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
pagePathGetters.integration_policy_edit({
packagePolicyId: newPackagePolicy.id,
}),
state: {
forceRefresh: true,
},
},
],
} as CreatePackagePolicyRouteState,

View file

@ -35,7 +35,10 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
// onSubmit,
onSuccess,
}) => {
const { http } = useKibana().services;
const {
http,
notifications: { toasts },
} = useKibana().services;
const {
data,
@ -51,6 +54,10 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
}),
{
onSuccess,
onError: (error) => {
// @ts-expect-error update types
toasts.addError(error, { title: error.body.error, toastMessage: error.body.message });
},
}
);

View file

@ -15,9 +15,17 @@ interface ResultTabsProps {
actionId: string;
agentIds?: string[];
isLive?: boolean;
startDate?: string;
endDate?: string;
}
const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId, agentIds, isLive }) => {
const ResultTabsComponent: React.FC<ResultTabsProps> = ({
actionId,
agentIds,
endDate,
isLive,
startDate,
}) => {
const tabs = useMemo(
() => [
{
@ -36,12 +44,18 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId, agentIds, is
content: (
<>
<EuiSpacer />
<ResultsTable actionId={actionId} agentIds={agentIds} isLive={isLive} />
<ResultsTable
actionId={actionId}
agentIds={agentIds}
isLive={isLive}
startDate={startDate}
endDate={endDate}
/>
</>
),
},
],
[actionId, agentIds, isLive]
[actionId, agentIds, endDate, isLive, startDate]
);
return (

View file

@ -7,15 +7,13 @@
import { isEmpty, isEqual, keys, map } from 'lodash/fp';
import {
EuiCallOut,
EuiDataGrid,
EuiDataGridSorting,
EuiDataGridProps,
EuiDataGridColumn,
EuiLink,
EuiTextColor,
EuiBasicTable,
EuiBasicTableColumn,
EuiSpacer,
EuiLoadingContent,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
@ -26,6 +24,11 @@ import { Direction, ResultEdges } from '../../common/search_strategy';
import { useKibana } from '../common/lib/kibana';
import { useActionResults } from '../action_results/use_action_results';
import { generateEmptyDataMessage } from './translations';
import {
ViewResultsInDiscoverAction,
ViewResultsInLensAction,
ViewResultsActionButtonType,
} from '../scheduled_query_groups/scheduled_query_group_queries_table';
const DataContext = createContext<ResultEdges>([]);
@ -33,20 +36,17 @@ interface ResultsTableComponentProps {
actionId: string;
selectedAgent?: string;
agentIds?: string[];
endDate?: string;
isLive?: boolean;
}
interface SummaryTableValue {
total: number | string;
pending: number | string;
responded: number;
failed: number;
startDate?: string;
}
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
actionId,
agentIds,
isLive,
startDate,
endDate,
}) => {
const {
// @ts-expect-error update types
@ -61,52 +61,6 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
isLive,
});
const notRespondedCount = useMemo(() => {
if (!agentIds || !aggregations.totalResponded) {
return '-';
}
return agentIds.length - aggregations.totalResponded;
}, [aggregations.totalResponded, agentIds]);
const summaryColumns: Array<EuiBasicTableColumn<SummaryTableValue>> = useMemo(
() => [
{
field: 'total',
name: 'Agents queried',
},
{
field: 'responded',
name: 'Successful',
},
{
field: 'pending',
name: 'Not yet responded',
},
{
field: 'failed',
name: 'Failed',
// eslint-disable-next-line react/display-name
render: (failed: number) => (
<EuiTextColor color={failed ? 'danger' : 'default'}>{failed}</EuiTextColor>
),
},
],
[]
);
const summaryItems = useMemo(
() => [
{
total: agentIds?.length ?? '-',
pending: notRespondedCount,
responded: aggregations.totalResponded,
failed: aggregations.failed,
},
],
[aggregations, agentIds, notRespondedCount]
);
const { getUrlForApp } = useKibana().services.application;
const getFleetAppUrl = useCallback(
@ -132,17 +86,23 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
[setPagination]
);
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([
{
id: 'agent.name',
direction: Direction.asc,
},
]);
const [columns, setColumns] = useState<EuiDataGridColumn[]>([]);
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
const { data: allResultsData } = useAllResults({
const { data: allResultsData, isFetched } = useAllResults({
actionId,
activePage: pagination.pageIndex,
limit: pagination.pageSize,
direction: Direction.asc,
sortField: '@timestamp',
isLive,
sort: sortingColumns.map((sortedColumn) => ({
field: sortedColumn.id,
direction: sortedColumn.direction as Direction,
})),
});
const [visibleColumns, setVisibleColumns] = useState<string[]>([]);
@ -234,27 +194,50 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
}
}, [columns, allResultsData?.edges]);
const toolbarVisibility = useMemo(
() => ({
additionalControls: (
<>
<ViewResultsInDiscoverAction
actionId={actionId}
buttonType={ViewResultsActionButtonType.button}
endDate={endDate}
startDate={startDate}
/>
<ViewResultsInLensAction
actionId={actionId}
buttonType={ViewResultsActionButtonType.button}
endDate={endDate}
startDate={startDate}
/>
</>
),
}),
[actionId, endDate, startDate]
);
if (!aggregations.totalResponded) {
return <EuiLoadingContent lines={5} />;
}
if (aggregations.totalResponded && isFetched && !allResultsData?.edges.length) {
return <EuiCallOut title={generateEmptyDataMessage(aggregations.totalResponded)} />;
}
return (
// @ts-expect-error update types
<DataContext.Provider value={allResultsData?.edges}>
<EuiBasicTable items={summaryItems} rowHeader="total" columns={summaryColumns} />
<EuiSpacer />
{columns.length > 0 ? (
<EuiDataGrid
aria-label="Osquery results"
columns={columns}
columnVisibility={columnVisibility}
rowCount={allResultsData?.totalCount ?? 0}
renderCellValue={renderCellValue}
sorting={tableSorting}
pagination={tablePagination}
height="500px"
/>
) : (
<div className={'eui-textCenter'}>
{generateEmptyDataMessage(aggregations.totalResponded)}
</div>
)}
<EuiDataGrid
aria-label="Osquery results"
columns={columns}
columnVisibility={columnVisibility}
rowCount={allResultsData?.totalCount ?? 0}
renderCellValue={renderCellValue}
sorting={tableSorting}
pagination={tablePagination}
height="500px"
toolbarVisibility={toolbarVisibility}
/>
</DataContext.Provider>
);
};

View file

@ -34,9 +34,8 @@ export interface ResultsArgs {
interface UseAllResults {
actionId: string;
activePage: number;
direction: Direction;
limit: number;
sortField: string;
sort: Array<{ field: string; direction: Direction }>;
filterQuery?: ESTermQuery | string;
skip?: boolean;
isLive?: boolean;
@ -45,9 +44,8 @@ interface UseAllResults {
export const useAllResults = ({
actionId,
activePage,
direction,
limit,
sortField,
sort,
filterQuery,
skip = false,
isLive = false,
@ -58,7 +56,7 @@ export const useAllResults = ({
} = useKibana().services;
return useQuery(
['allActionResults', { actionId, activePage, direction, limit, sortField }],
['allActionResults', { actionId, activePage, limit, sort }],
async () => {
const responseData = await data.search
.search<ResultsRequestOptions, ResultsStrategyResponse>(
@ -67,10 +65,7 @@ export const useAllResults = ({
factoryQueryType: OsqueryQueries.results,
filterQuery: createFilter(filterQuery),
pagination: generateTablePaginationOptions(activePage, limit),
sort: {
direction,
field: sortField,
},
sort,
},
{
strategy: 'osquerySearchStrategy',

View file

@ -1,68 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { useDiscoverLink } from '../../../common/hooks';
interface LiveQueryDetailsActionsMenuProps {
actionId: string;
}
const LiveQueryDetailsActionsMenuComponent: React.FC<LiveQueryDetailsActionsMenuProps> = ({
actionId,
}) => {
const discoverLinkProps = useDiscoverLink({ filters: [{ key: 'action_id', value: actionId }] });
const [isPopoverOpen, setPopover] = useState(false);
const onButtonClick = useCallback(() => {
setPopover((currentIsPopoverOpen) => !currentIsPopoverOpen);
}, []);
const closePopover = useCallback(() => {
setPopover(false);
}, []);
const items = useMemo(
() => [
<EuiContextMenuItem key="copy" icon="copy" {...discoverLinkProps}>
<FormattedMessage
id="xpack.osquery.liveQueryResults.viewResultsInDiscoverLabel"
defaultMessage="View results in Discover"
/>
</EuiContextMenuItem>,
],
[discoverLinkProps]
);
const button = useMemo(
() => (
<EuiButton iconType="arrowDown" iconSide="right" onClick={onButtonClick}>
<FormattedMessage
id="xpack.osquery.liveQueryResults.actionsMenuButtonLabel"
defaultMessage="Actions"
/>
</EuiButton>
),
[onButtonClick]
);
return (
<EuiPopover
id="liveQueryDetailsActionsMenu"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
>
<EuiContextMenuPanel size="s" items={items} />
</EuiPopover>
);
};
export const LiveQueryDetailsActionsMenu = React.memo(LiveQueryDetailsActionsMenuComponent);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { get } from 'lodash';
import {
EuiButtonEmpty,
EuiTextColor,
@ -27,7 +28,6 @@ import { WithHeaderLayout } from '../../../components/layouts';
import { useActionResults } from '../../../action_results/use_action_results';
import { useActionDetails } from '../../../actions/use_action_details';
import { ResultTabs } from '../../../queries/edit/tabs';
import { LiveQueryDetailsActionsMenu } from './actions_menu';
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
@ -120,15 +120,9 @@ const LiveQueryDetailsPageComponent = () => {
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem grow={false} key="agents_failed_count_divider">
<Divider />
</EuiFlexItem>
<EuiFlexItem grow={false} key="actions_menu">
<LiveQueryDetailsActionsMenu actionId={actionId} />
</EuiFlexItem>
</EuiFlexGroup>
),
[actionId, actionResultsData?.aggregations.failed, data?.actionDetails?.fields?.agents?.length]
[actionResultsData?.aggregations.failed, data?.actionDetails?.fields?.agents?.length]
);
return (
@ -137,7 +131,12 @@ const LiveQueryDetailsPageComponent = () => {
{data?.actionDetails._source?.data?.query}
</EuiCodeBlock>
<EuiSpacer />
<ResultTabs actionId={actionId} agentIds={data?.actionDetails?.fields?.agents} />
<ResultTabs
actionId={actionId}
agentIds={data?.actionDetails?.fields?.agents}
startDate={get(data, ['actionDetails', 'fields', '@timestamp', '0'])}
endDate={get(data, 'actionDetails.fields.expiration[0]')}
/>
</WithHeaderLayout>
);
};

View file

@ -1,69 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { useDiscoverLink } from '../../../common/hooks';
interface LiveQueryDetailsActionsMenuProps {
actionId: string;
}
const LiveQueryDetailsActionsMenuComponent: React.FC<LiveQueryDetailsActionsMenuProps> = ({
actionId,
}) => {
const discoverLinkProps = useDiscoverLink({ filters: [{ key: 'action_id', value: actionId }] });
const [isPopoverOpen, setPopover] = useState(false);
const onButtonClick = useCallback(() => {
setPopover((currentIsPopoverOpen) => !currentIsPopoverOpen);
}, []);
const closePopover = useCallback(() => {
setPopover(false);
}, []);
const items = useMemo(
() => [
<EuiContextMenuItem key="copy" icon="copy" {...discoverLinkProps}>
<FormattedMessage
id="xpack.osquery.liveQueryResults.viewResultsInDiscoverLabel"
defaultMessage="View results in Discover"
/>
</EuiContextMenuItem>,
],
[discoverLinkProps]
);
const button = useMemo(
() => (
<EuiButton iconType="arrowDown" iconSide="right" onClick={onButtonClick}>
<FormattedMessage
id="xpack.osquery.liveQueryResults.actionsMenuButtonLabel"
defaultMessage="Actions"
/>
</EuiButton>
),
[onButtonClick]
);
return (
<EuiPopover
id="liveQueryDetailsActionsMenu"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
>
<EuiContextMenuPanel size="s" items={items} />
</EuiPopover>
);
};
export const LiveQueryDetailsActionsMenu = React.memo(LiveQueryDetailsActionsMenuComponent);

View file

@ -13,6 +13,8 @@ import {
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
@ -75,9 +77,17 @@ const ScheduledQueryGroupDetailsPageComponent = () => {
<BetaBadge />
</BetaBadgeRowWrapper>
</EuiFlexItem>
{data?.description && (
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{data.description}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
),
[data?.name, scheduledQueryGroupsListProps]
[data?.description, data?.name, scheduledQueryGroupsListProps]
);
const RightColumn = useMemo(

View file

@ -73,6 +73,10 @@ const ActiveStateSwitchComponent: React.FC<ActiveStateSwitchProps> = ({ item })
)
);
},
onError: (error) => {
// @ts-expect-error update types
toasts.addError(error, { title: error.body.error, toastMessage: error.body.message });
},
}
);

View file

@ -77,7 +77,7 @@ const AddQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({ onSave, onClos
return (
<EuiPortal>
<EuiFlyout size="s" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
<EuiFlyout size="m" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 id="flyoutTitle">

View file

@ -91,7 +91,7 @@ export const EditQueryFlyout: React.FC<EditQueryFlyoutProps> = ({
return (
<EuiPortal>
<EuiFlyout size="s" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
<EuiFlyout size="m" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 id="flyoutTitle">

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import { findIndex, forEach, pullAt } from 'lodash';
import { findIndex, forEach, pullAt, pullAllBy } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui';
import { produce } from 'immer';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { PackagePolicyInput, PackagePolicyInputStream } from '../../../../fleet/common';
@ -50,6 +50,7 @@ const getNewStream = ({ id, interval, query, scheduledQueryGroupId }: GetNewStre
const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({ field, scheduledQueryGroupId }) => {
const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false);
const [showEditQueryFlyout, setShowEditQueryFlyout] = useState<number>(-1);
const [tableSelectedItems, setTableSelectedItems] = useState<PackagePolicyInputStream[]>([]);
const handleShowAddFlyout = useCallback(() => setShowAddQueryFlyout(true), []);
const handleHideAddFlyout = useCallback(() => setShowAddQueryFlyout(false), []);
@ -126,6 +127,17 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({ field, scheduledQu
[handleHideAddFlyout, scheduledQueryGroupId, setValue]
);
const handleDeleteQueries = useCallback(() => {
setValue(
produce((draft) => {
pullAllBy(draft[0].streams, tableSelectedItems, 'vars.id.value');
return draft;
})
);
setTableSelectedItems([]);
}, [setValue, tableSelectedItems]);
const handlePackUpload = useCallback(
(newQueries) => {
setValue(
@ -148,26 +160,42 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({ field, scheduledQu
[scheduledQueryGroupId, setValue]
);
const tableData = useMemo(() => ({ inputs: field.value }), [field.value]);
return (
<>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton fill onClick={handleShowAddFlyout} iconType="plusInCircle">
<FormattedMessage
id="xpack.osquery.scheduledQueryGroup.queriesForm.addQueryButtonLabel"
defaultMessage="Add query"
/>
</EuiButton>
{!tableSelectedItems.length ? (
<EuiButton fill onClick={handleShowAddFlyout} iconType="plusInCircle">
<FormattedMessage
id="xpack.osquery.scheduledQueryGroup.queriesForm.addQueryButtonLabel"
defaultMessage="Add query"
/>
</EuiButton>
) : (
<EuiButton color="danger" onClick={handleDeleteQueries} iconType="trash">
<FormattedMessage
id="xpack.osquery.scheduledQueryGroup.table.deleteQueriesButtonLabel"
defaultMessage="Delete {queriesCount, plural, one {# query} other {# queries}}"
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
values={{
queriesCount: tableSelectedItems.length,
}}
/>
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{field.value && field.value[0].streams?.length ? (
<ScheduledQueryGroupQueriesTable
editMode={true}
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
data={{ inputs: field.value }}
data={tableData}
onEditClick={handleEditClick}
onDeleteClick={handleDeleteClick}
selectedItems={tableSelectedItems}
setSelectedItems={setTableSelectedItems}
/>
) : null}
<EuiSpacer />

View file

@ -5,19 +5,221 @@
* 2.0.
*/
import { get } from 'lodash/fp';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { EuiInMemoryTable, EuiCodeBlock, EuiButtonIcon } from '@elastic/eui';
import {
EuiBasicTable,
EuiButtonEmpty,
EuiCodeBlock,
EuiButtonIcon,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
TypedLensByValueInput,
PersistedIndexPatternLayer,
PieVisualizationState,
} from '../../../lens/public';
import { PackagePolicy, PackagePolicyInputStream } from '../../../fleet/common';
import { FilterStateStore } from '../../../../../src/plugins/data/common';
import { useKibana } from '../common/lib/kibana';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
interface ViewResultsInDiscoverActionProps {
item: PackagePolicyInputStream;
export enum ViewResultsActionButtonType {
icon = 'icon',
button = 'button',
}
const ViewResultsInDiscoverAction: React.FC<ViewResultsInDiscoverActionProps> = ({ item }) => {
interface ViewResultsInDiscoverActionProps {
actionId: string;
buttonType: ViewResultsActionButtonType;
endDate?: string;
startDate?: string;
}
function getLensAttributes(actionId: string): TypedLensByValueInput['attributes'] {
const dataLayer: PersistedIndexPatternLayer = {
columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'],
columns: {
'8690befd-fd69-4246-af4a-dd485d2a3b38': {
sourceField: 'type',
isBucketed: true,
dataType: 'string',
scale: 'ordinal',
operationType: 'terms',
label: 'Top values of type',
params: {
otherBucket: true,
size: 5,
missingBucket: false,
orderBy: {
columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
type: 'column',
},
orderDirection: 'desc',
},
},
'ed999e9d-204c-465b-897f-fe1a125b39ed': {
sourceField: 'Records',
isBucketed: false,
dataType: 'number',
scale: 'ratio',
operationType: 'count',
label: 'Count of records',
},
},
incompleteColumns: {},
};
const xyConfig: PieVisualizationState = {
shape: 'pie',
layers: [
{
legendDisplay: 'default',
nestedLegend: false,
layerId: 'layer1',
metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
numberDisplay: 'percent',
groups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'],
categoryDisplay: 'default',
},
],
};
return {
visualizationType: 'lnsPie',
title: `Action ${actionId} results`,
references: [
{
id: 'logs-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'logs-*',
name: 'indexpattern-datasource-layer-layer1',
type: 'index-pattern',
},
{
name: 'filter-index-pattern-0',
id: 'logs-*',
type: 'index-pattern',
},
],
state: {
datasourceStates: {
indexpattern: {
layers: {
layer1: dataLayer,
},
},
},
filters: [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
indexRefName: 'filter-index-pattern-0',
negate: false,
alias: null,
disabled: false,
params: {
query: actionId,
},
type: 'phrase',
key: 'action_id',
},
query: {
match_phrase: {
action_id: actionId,
},
},
},
],
query: { language: 'kuery', query: '' },
visualization: xyConfig,
},
};
}
const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
actionId,
buttonType,
endDate,
startDate,
}) => {
const lensService = useKibana().services.lens;
const handleClick = useCallback(
(event) => {
const openInNewWindow = !(!isModifiedEvent(event) && isLeftClickEvent(event));
event.preventDefault();
lensService?.navigateToPrefilledEditor(
{
id: '',
timeRange: {
from: startDate ?? 'now-1d',
to: endDate ?? 'now',
mode: startDate || endDate ? 'absolute' : 'relative',
},
attributes: getLensAttributes(actionId),
},
openInNewWindow
);
},
[actionId, endDate, lensService, startDate]
);
if (buttonType === ViewResultsActionButtonType.button) {
return (
<EuiButtonEmpty
size="xs"
iconType="lensApp"
onClick={handleClick}
disabled={!lensService?.canUseEditor()}
>
<FormattedMessage
id="xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel"
defaultMessage="View results in Lens"
/>
</EuiButtonEmpty>
);
}
return (
<EuiToolTip
content={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel',
{
defaultMessage: 'View results in Lens',
}
)}
>
<EuiButtonIcon
iconType="lensApp"
disabled={!lensService?.canUseEditor()}
onClick={handleClick}
aria-label={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel',
{
defaultMessage: 'View results in Lens',
}
)}
/>
</EuiToolTip>
);
};
export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent);
const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
actionId,
buttonType,
endDate,
startDate,
}) => {
const urlGenerator = useKibana().services.discover?.urlGenerator;
const [discoverUrl, setDiscoverUrl] = useState<string>('');
@ -36,40 +238,77 @@ const ViewResultsInDiscoverAction: React.FC<ViewResultsInDiscoverActionProps> =
disabled: false,
type: 'phrase',
key: 'action_id',
params: { query: item.vars?.id.value },
params: { query: actionId },
},
query: { match_phrase: { action_id: item.vars?.id.value } },
query: { match_phrase: { action_id: actionId } },
$state: { store: FilterStateStore.APP_STATE },
},
],
refreshInterval: {
pause: true,
value: 0,
},
timeRange:
startDate && endDate
? {
to: endDate,
from: startDate,
mode: 'absolute',
}
: {
to: 'now',
from: 'now-15m',
mode: 'relative',
},
});
setDiscoverUrl(newUrl);
};
getDiscoverUrl();
}, [item.vars?.id.value, urlGenerator]);
}, [actionId, endDate, startDate, urlGenerator]);
if (buttonType === ViewResultsActionButtonType.button) {
return (
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl}>
<FormattedMessage
id="xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel"
defaultMessage="View results in Discover"
/>
</EuiButtonEmpty>
);
}
return (
<EuiButtonIcon
iconType="visTable"
href={discoverUrl}
aria-label={i18n.translate(
<EuiToolTip
content={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel',
{
defaultMessage: 'Check results of {queryName} in Discover',
values: {
queryName: item.vars?.id.value,
},
defaultMessage: 'View results in Discover',
}
)}
/>
>
<EuiButtonIcon
iconType="discoverApp"
href={discoverUrl}
aria-label={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel',
{
defaultMessage: 'View results in Discover',
}
)}
/>
</EuiToolTip>
);
};
export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent);
interface ScheduledQueryGroupQueriesTableProps {
data: Pick<PackagePolicy, 'inputs'>;
editMode?: boolean;
onDeleteClick?: (item: PackagePolicyInputStream) => void;
onEditClick?: (item: PackagePolicyInputStream) => void;
selectedItems?: PackagePolicyInputStream[];
setSelectedItems?: (selection: PackagePolicyInputStream[]) => void;
}
const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQueriesTableProps> = ({
@ -77,6 +316,8 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQuer
editMode = false,
onDeleteClick,
onEditClick,
selectedItems,
setSelectedItems,
}) => {
const renderDeleteAction = useCallback(
(item: PackagePolicyInputStream) => (
@ -132,7 +373,22 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQuer
);
const renderDiscoverResultsAction = useCallback(
(item) => <ViewResultsInDiscoverAction item={item} />,
(item) => (
<ViewResultsInDiscoverAction
actionId={item.vars?.id.value}
buttonType={ViewResultsActionButtonType.icon}
/>
),
[]
);
const renderLensResultsAction = useCallback(
(item) => (
<ViewResultsInLensAction
actionId={item.vars?.id.value}
buttonType={ViewResultsActionButtonType.icon}
/>
),
[]
);
@ -184,29 +440,50 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQuer
{
render: renderDiscoverResultsAction,
},
{
render: renderLensResultsAction,
},
],
},
],
[editMode, renderDeleteAction, renderDiscoverResultsAction, renderEditAction, renderQueryColumn]
[
editMode,
renderDeleteAction,
renderDiscoverResultsAction,
renderEditAction,
renderLensResultsAction,
renderQueryColumn,
]
);
const sorting = useMemo(
() => ({
sort: {
field: 'vars.id.value',
field: 'vars.id.value' as keyof PackagePolicyInputStream,
direction: 'asc' as const,
},
}),
[]
);
const itemId = useCallback((item: PackagePolicyInputStream) => get('vars.id.value', item), []);
const selection = useMemo(
() => ({
onSelectionChange: setSelectedItems,
initialSelected: selectedItems,
}),
[selectedItems, setSelectedItems]
);
return (
<EuiInMemoryTable<PackagePolicyInputStream>
<EuiBasicTable<PackagePolicyInputStream>
items={data.inputs[0].streams}
itemId="vars.id.value"
isExpandable={true}
itemId={itemId}
columns={columns}
sorting={sorting}
selection={editMode ? selection : undefined}
isSelectable={editMode}
/>
);
};

View file

@ -8,6 +8,7 @@
import { DiscoverStart } from '../../../../src/plugins/discover/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { FleetStart } from '../../fleet/public';
import { LensPublicStart } from '../../../plugins/lens/public';
import { CoreStart } from '../../../../src/core/public';
import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public';
import {
@ -28,6 +29,7 @@ export interface StartPlugins {
discover: DiscoverStart;
data: DataPublicPluginStart;
fleet: FleetStart;
lens?: LensPublicStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}

View file

@ -44,7 +44,7 @@ export const parseAgentSelection = async (
const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection;
const agentService = context.service.getAgentService();
const packagePolicyService = context.service.getPackagePolicyService();
const kueryFragments = ['active:true'];
const kueryFragments = [];
if (agentService && packagePolicyService) {
const osqueryPolicies = await aggregateResults(async (page, perPage) => {
@ -55,7 +55,7 @@ export const parseAgentSelection = async (
});
return { results: items.map((it) => it.policy_id), total };
});
kueryFragments.push(`policy_id:(${uniq(osqueryPolicies).join(',')})`);
kueryFragments.push(`policy_id:(${uniq(osqueryPolicies).join(' or ')})`);
if (allAgentsSelected) {
const kuery = kueryFragments.join(' and ');
const fetchedAgents = await aggregateResults(async (page, perPage) => {
@ -72,10 +72,10 @@ export const parseAgentSelection = async (
if (platformsSelected.length > 0 || policiesSelected.length > 0) {
const groupFragments = [];
if (platformsSelected.length) {
groupFragments.push(`local_metadata.os.platform:(${platformsSelected.join(',')})`);
groupFragments.push(`local_metadata.os.platform:(${platformsSelected.join(' or ')})`);
}
if (policiesSelected.length) {
groupFragments.push(`policy_id:(${policiesSelected.join(',')})`);
groupFragments.push(`policy_id:(${policiesSelected.join(' or ')})`);
}
kueryFragments.push(`(${groupFragments.join(' or ')})`);
const kuery = kueryFragments.join(' and ');

View file

@ -41,7 +41,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
);
if (!selectedAgents.length) {
throw new Error('No agents found for selection, aborting.');
return response.badRequest({ body: new Error('No agents found for selection') });
}
const action = {

View file

@ -53,13 +53,12 @@ export const buildResultsQuery = ({
size: querySize,
track_total_hits: true,
fields: agentId ? ['osquery.*'] : ['agent.*', 'osquery.*'],
sort: [
{
[sort.field]: {
order: sort.direction,
sort:
sort?.map((sortConfig) => ({
[sortConfig.field]: {
order: sortConfig.direction,
},
},
],
})) ?? [],
},
};