mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Osquery] Fix live packs (#137651)
This commit is contained in:
parent
c26e085c92
commit
b4a212b8dc
27 changed files with 374 additions and 179 deletions
|
@ -66,6 +66,7 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('Save and deploy changes');
|
||||
findAndClickButton('Save and deploy changes');
|
||||
cy.contains(PACK_NAME);
|
||||
cy.contains(`Successfully created "${PACK_NAME}" pack`);
|
||||
});
|
||||
|
||||
it('to click the edit button and edit pack', () => {
|
||||
|
|
|
@ -19,10 +19,13 @@ import {
|
|||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { useAllActions } from './use_all_actions';
|
||||
import { useAllLiveQueries } from './use_all_live_queries';
|
||||
import type { SearchHit } from '../../common/search_strategy';
|
||||
import { Direction } from '../../common/search_strategy';
|
||||
import { useRouterNavigate, useKibana } from '../common/lib/kibana';
|
||||
|
||||
const EMPTY_ARRAY: SearchHit[] = [];
|
||||
|
||||
interface ActionTableResultsButtonProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
@ -41,7 +44,7 @@ const ActionsTableComponent = () => {
|
|||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
|
||||
const { data: actionsData } = useAllActions({
|
||||
const { data: actionsData } = useAllLiveQueries({
|
||||
activePage: pageIndex,
|
||||
limit: pageSize,
|
||||
direction: Direction.desc,
|
||||
|
@ -129,7 +132,7 @@ const ActionsTableComponent = () => {
|
|||
[push]
|
||||
);
|
||||
const isPlayButtonAvailable = useCallback(
|
||||
() => permissions.runSavedQueries || permissions.writeLiveQueries,
|
||||
() => !!(permissions.runSavedQueries || permissions.writeLiveQueries),
|
||||
[permissions.runSavedQueries, permissions.writeLiveQueries]
|
||||
);
|
||||
|
||||
|
@ -199,16 +202,15 @@ const ActionsTableComponent = () => {
|
|||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: actionsData?.total ?? 0,
|
||||
totalItemCount: actionsData?.data?.total ?? 0,
|
||||
pageSizeOptions: [20, 50, 100],
|
||||
}),
|
||||
[actionsData?.total, pageIndex, pageSize]
|
||||
[actionsData, pageIndex, pageSize]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
|
||||
items={actionsData?.actions ?? []}
|
||||
items={actionsData?.data?.items ?? EMPTY_ARRAY}
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
|
|
|
@ -1,95 +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 { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import type { InspectResponse } from '../common/helpers';
|
||||
import {
|
||||
createFilter,
|
||||
generateTablePaginationOptions,
|
||||
getInspectResponse,
|
||||
} from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import type {
|
||||
ActionEdges,
|
||||
ActionsRequestOptions,
|
||||
ActionsStrategyResponse,
|
||||
Direction,
|
||||
} from '../../common/search_strategy';
|
||||
import { OsqueryQueries } from '../../common/search_strategy';
|
||||
import type { ESTermQuery } from '../../common/typed_json';
|
||||
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
||||
export interface ActionsArgs {
|
||||
actions: ActionEdges;
|
||||
id: string;
|
||||
inspect: InspectResponse;
|
||||
isInspected: boolean;
|
||||
}
|
||||
|
||||
interface UseAllActions {
|
||||
activePage: number;
|
||||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useAllActions = ({
|
||||
activePage,
|
||||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllActions) => {
|
||||
const { data } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useQuery(
|
||||
['actions', { activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
const responseData = await lastValueFrom(
|
||||
data.search.search<ActionsRequestOptions, ActionsStrategyResponse>(
|
||||
{
|
||||
factoryQueryType: OsqueryQueries.actions,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
sort: {
|
||||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: 'osquerySearchStrategy',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
...responseData,
|
||||
actions: responseData.edges,
|
||||
inspect: getInspectResponse(responseData, {} as InspectResponse),
|
||||
};
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !skip,
|
||||
onSuccess: () => setErrorToast(),
|
||||
onError: (error: Error) =>
|
||||
setErrorToast(error, {
|
||||
title: i18n.translate('xpack.osquery.all_actions.fetchError', {
|
||||
defaultMessage: 'Error while fetching actions',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import type { ActionEdges, ActionsStrategyResponse, Direction } from '../../common/search_strategy';
|
||||
import type { ESTermQuery } from '../../common/typed_json';
|
||||
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
||||
interface UseAllLiveQueries {
|
||||
activePage: number;
|
||||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useAllLiveQueries = ({
|
||||
activePage,
|
||||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllLiveQueries) => {
|
||||
const { http } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useQuery(
|
||||
['actions', { activePage, direction, limit, sortField }],
|
||||
() =>
|
||||
http.get<{ data: Omit<ActionsStrategyResponse, 'edges'> & { items: ActionEdges } }>(
|
||||
'/api/osquery/live_queries',
|
||||
{
|
||||
query: {
|
||||
filterQuery: createFilter(filterQuery),
|
||||
page: activePage,
|
||||
pageSize: limit,
|
||||
sort: sortField,
|
||||
sortOrder: direction,
|
||||
},
|
||||
}
|
||||
),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !skip,
|
||||
onSuccess: () => setErrorToast(),
|
||||
onError: (error: Error) =>
|
||||
setErrorToast(error, {
|
||||
title: i18n.translate('xpack.osquery.live_queries_all.fetchError', {
|
||||
defaultMessage: 'Error while fetching live queries',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -14,18 +14,44 @@ export interface LogsDataView extends DataView {
|
|||
id: string;
|
||||
}
|
||||
|
||||
export const useLogsDataView = () => {
|
||||
interface UseLogsDataView {
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useLogsDataView = (payload?: UseLogsDataView) => {
|
||||
const dataViews = useKibana().services.data.dataViews;
|
||||
|
||||
return useQuery<LogsDataView>(['logsDataView'], async () => {
|
||||
let dataView = (await dataViews.find('logs-osquery_manager.result*', 1))[0];
|
||||
if (!dataView && dataViews.getCanSaveSync()) {
|
||||
dataView = await dataViews.createAndSave({
|
||||
title: 'logs-osquery_manager.result*',
|
||||
timeFieldName: '@timestamp',
|
||||
});
|
||||
}
|
||||
return useQuery<LogsDataView | undefined>(
|
||||
['logsDataView'],
|
||||
async () => {
|
||||
try {
|
||||
await dataViews.getFieldsForWildcard({
|
||||
pattern: 'logs-osquery_manager.result*',
|
||||
});
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return dataView as LogsDataView;
|
||||
});
|
||||
let dataView;
|
||||
try {
|
||||
const data = await dataViews.find('logs-osquery_manager.result*', 1);
|
||||
if (data.length) {
|
||||
dataView = data[0];
|
||||
}
|
||||
} catch (e) {
|
||||
if (dataViews.getCanSaveSync()) {
|
||||
dataView = await dataViews.createAndSave({
|
||||
title: 'logs-osquery_manager.result*',
|
||||
timeFieldName: '@timestamp',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dataView as LogsDataView;
|
||||
},
|
||||
{
|
||||
enabled: !payload?.skip,
|
||||
retry: 1,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
EuiCard,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { pickBy, isEmpty, map, find } from 'lodash';
|
||||
|
@ -122,7 +122,14 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
const handleShowSaveQueryFlyout = useCallback(() => setShowSavedQueryFlyout(true), []);
|
||||
const handleCloseSaveQueryFlyout = useCallback(() => setShowSavedQueryFlyout(false), []);
|
||||
|
||||
const { data, isLoading, mutateAsync, isError, isSuccess } = useCreateLiveQuery({ onSuccess });
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
mutateAsync,
|
||||
isError,
|
||||
isSuccess,
|
||||
reset: cleanupLiveQuery,
|
||||
} = useCreateLiveQuery({ onSuccess });
|
||||
|
||||
const { data: liveQueryDetails } = useLiveQueryDetails({
|
||||
actionId: data?.action_id,
|
||||
|
@ -271,6 +278,13 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[permissions.readSavedQueries, permissions.runSavedQueries]
|
||||
);
|
||||
|
||||
const { data: packsData } = usePacks({});
|
||||
|
||||
const selectedPackData = useMemo(
|
||||
() => (packId?.length ? find(packsData?.data, { id: packId[0] }) : null),
|
||||
[packId, packsData]
|
||||
);
|
||||
|
||||
const submitButtonContent = useMemo(
|
||||
() => (
|
||||
<EuiFlexItem>
|
||||
|
@ -300,7 +314,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
!enabled ||
|
||||
!agentSelected ||
|
||||
(queryType === 'query' && !queryValueProvided) ||
|
||||
(queryType === 'pack' && !packId) ||
|
||||
(queryType === 'pack' &&
|
||||
(!packId || !selectedPackData?.attributes.queries.length)) ||
|
||||
isSubmitting
|
||||
}
|
||||
onClick={submit}
|
||||
|
@ -325,6 +340,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
queryType,
|
||||
queryValueProvided,
|
||||
resultsStatus,
|
||||
selectedPackData,
|
||||
submit,
|
||||
]
|
||||
);
|
||||
|
@ -426,13 +442,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
}
|
||||
}, [defaultValue, updateFieldValues]);
|
||||
|
||||
const { data: packsData } = usePacks({});
|
||||
|
||||
const selectedPackData = useMemo(
|
||||
() => (packId?.length ? find(packsData?.data, { id: packId[0] }) : null),
|
||||
[packId, packsData]
|
||||
);
|
||||
|
||||
const queryCardSelectable = useMemo(
|
||||
() => ({
|
||||
onClick: () => setQueryType('query'),
|
||||
|
@ -452,11 +461,12 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
);
|
||||
|
||||
const canRunPacks = useMemo(
|
||||
() => !!(permissions.runSavedQueries && permissions.readPacks),
|
||||
() =>
|
||||
!!((permissions.runSavedQueries || permissions.writeLiveQueries) && permissions.readPacks),
|
||||
[permissions]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
if (defaultValue?.packId) {
|
||||
setQueryType('pack');
|
||||
const selectedPackOption = find(packsData?.data, ['id', defaultValue.packId]);
|
||||
|
@ -468,10 +478,12 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
}
|
||||
}, [defaultValue, packsData, updateFieldValues]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
setIsLive(() => !(liveQueryDetails?.status === 'completed'));
|
||||
}, [liveQueryDetails?.status]);
|
||||
|
||||
useEffect(() => cleanupLiveQuery(), [queryType, packId, cleanupLiveQuery]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
|
@ -544,18 +556,19 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
</EuiFlexItem>
|
||||
{submitButtonContent}
|
||||
<EuiSpacer />
|
||||
{(liveQueryDetails?.queries?.length ||
|
||||
selectedPackData?.attributes?.queries?.length) && (
|
||||
{liveQueryDetails?.queries?.length ||
|
||||
selectedPackData?.attributes?.queries?.length ? (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<PackQueriesStatusTable
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
data={liveQueryDetails?.queries ?? selectedPackData?.attributes?.queries}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useState, useMemo } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonEmpty,
|
||||
|
@ -42,6 +43,16 @@ import type { PackItem } from '../../packs/types';
|
|||
import type { LogsDataView } from '../../common/hooks/use_logs_data_view';
|
||||
import { useLogsDataView } from '../../common/hooks/use_logs_data_view';
|
||||
|
||||
const TruncateTooltipText = styled.div`
|
||||
width: 100%;
|
||||
|
||||
> span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const EMPTY_ARRAY: PackQueryStatusItem[] = [];
|
||||
|
||||
// @ts-expect-error TS2769
|
||||
|
@ -224,7 +235,7 @@ const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProp
|
|||
}) => {
|
||||
const lensService = useKibana().services.lens;
|
||||
const isLensAvailable = lensService?.canUseEditor();
|
||||
const { data: logsDataView } = useLogsDataView();
|
||||
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
|
@ -290,7 +301,7 @@ const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverAction
|
|||
const { discover, application } = useKibana().services;
|
||||
const locator = discover?.locator;
|
||||
const discoverPermissions = application.capabilities.discover;
|
||||
const { data: logsDataView } = useLogsDataView();
|
||||
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
|
||||
|
||||
const [discoverUrl, setDiscoverUrl] = useState<string>('');
|
||||
|
||||
|
@ -519,16 +530,30 @@ interface PackQueriesStatusTableProps {
|
|||
data?: PackQueryStatusItem[];
|
||||
startDate?: string;
|
||||
expirationDate?: string;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => ReactElement;
|
||||
}
|
||||
|
||||
const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
data,
|
||||
startDate,
|
||||
expirationDate,
|
||||
addToTimeline,
|
||||
}) => {
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, unknown>>({});
|
||||
|
||||
const renderIDColumn = useCallback(
|
||||
(id: string) => (
|
||||
<TruncateTooltipText>
|
||||
<EuiToolTip content={id} display="block">
|
||||
<>{id}</>
|
||||
</EuiToolTip>
|
||||
</TruncateTooltipText>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderQueryColumn = useCallback((query: string, item) => {
|
||||
const singleLine = removeMultilines(query);
|
||||
const content = singleLine.length > 55 ? `${singleLine.substring(0, 55)}...` : singleLine;
|
||||
|
@ -588,6 +613,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
endDate={expirationDate}
|
||||
agentIds={agentIds}
|
||||
failedAgentsCount={item?.failed ?? 0}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -597,12 +623,12 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
return itemIdToExpandedRowMapValues;
|
||||
});
|
||||
},
|
||||
[agentIds, expirationDate, startDate]
|
||||
[agentIds, expirationDate, startDate, addToTimeline]
|
||||
);
|
||||
|
||||
const renderToggleResultsAction = useCallback(
|
||||
(item) =>
|
||||
item?.action_id ? (
|
||||
item?.action_id && data?.length && data.length > 1 ? (
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`toggleIcon-${item.id}`}
|
||||
onClick={getHandleErrorsToggle(item)}
|
||||
|
@ -611,7 +637,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
) : (
|
||||
<></>
|
||||
),
|
||||
[getHandleErrorsToggle, itemIdToExpandedRowMap]
|
||||
[data, getHandleErrorsToggle, itemIdToExpandedRowMap]
|
||||
);
|
||||
|
||||
const getItemId = useCallback((item: PackItem) => get(item, 'id'), []);
|
||||
|
@ -624,7 +650,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
defaultMessage: 'ID',
|
||||
}),
|
||||
width: '15%',
|
||||
truncateText: true,
|
||||
render: renderIDColumn,
|
||||
},
|
||||
{
|
||||
field: 'query',
|
||||
|
@ -638,12 +664,14 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
name: i18n.translate('xpack.osquery.pack.queriesTable.docsResultsColumnTitle', {
|
||||
defaultMessage: 'Docs',
|
||||
}),
|
||||
width: '80px',
|
||||
render: renderDocsColumn,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.osquery.pack.queriesTable.agentsResultsColumnTitle', {
|
||||
defaultMessage: 'Agents',
|
||||
}),
|
||||
width: '160px',
|
||||
render: renderAgentsColumn,
|
||||
},
|
||||
{
|
||||
|
@ -673,6 +701,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
},
|
||||
],
|
||||
[
|
||||
renderIDColumn,
|
||||
renderQueryColumn,
|
||||
renderDocsColumn,
|
||||
renderAgentsColumn,
|
||||
|
@ -692,7 +721,12 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
[]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
// reset the expanded row map when the data changes
|
||||
setItemIdToExpandedRowMap({});
|
||||
}, [actionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
data?.length === 1 &&
|
||||
agentIds?.length &&
|
||||
|
|
|
@ -64,6 +64,7 @@ export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest
|
|||
(newSelectedOptions) => {
|
||||
if (!newSelectedOptions.length) {
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
field.setValue([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProp
|
|||
}) => {
|
||||
const lensService = useKibana().services.lens;
|
||||
const isLensAvailable = lensService?.canUseEditor();
|
||||
const { data: logsDataView } = useLogsDataView();
|
||||
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
|
@ -274,7 +274,7 @@ const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverAction
|
|||
const { discover, application } = useKibana().services;
|
||||
const locator = discover?.locator;
|
||||
const discoverPermissions = application.capabilities.discover;
|
||||
const { data: logsDataView } = useLogsDataView();
|
||||
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
|
||||
|
||||
const [discoverUrl, setDiscoverUrl] = useState<string>('');
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import type { EuiBasicTableColumn, EuiTableActionsColumnType } from '@elastic/eui';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiText,
|
||||
|
@ -20,7 +21,8 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useKibana, useRouterNavigate } from '../common/lib/kibana';
|
||||
import { usePacks } from './use_packs';
|
||||
import { ActiveStateSwitch } from './active_state_switch';
|
||||
import { AgentsPolicyLink } from '../agent_policies/agents_policy_link';
|
||||
|
@ -85,6 +87,8 @@ export const AgentPoliciesPopover = ({ agentPolicyIds = [] }: { agentPolicyIds?:
|
|||
};
|
||||
|
||||
const PacksTableComponent = () => {
|
||||
const permissions = useKibana().services.application.capabilities.osquery;
|
||||
const { push } = useHistory();
|
||||
const { data, isLoading } = usePacks({});
|
||||
|
||||
const renderAgentPolicy = useCallback(
|
||||
|
@ -116,6 +120,23 @@ const PacksTableComponent = () => {
|
|||
);
|
||||
}, []);
|
||||
|
||||
const handlePlayClick = useCallback<(item: PackSavedObject) => () => void>(
|
||||
(item) => () =>
|
||||
push('/live_queries/new', {
|
||||
form: {
|
||||
packId: item.id,
|
||||
},
|
||||
}),
|
||||
[push]
|
||||
);
|
||||
|
||||
const renderPlayAction = useCallback(
|
||||
(item, enabled) => (
|
||||
<EuiButtonIcon iconType="play" onClick={handlePlayClick(item)} isDisabled={!enabled} />
|
||||
),
|
||||
[handlePlayClick]
|
||||
);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<PackSavedObject>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -167,8 +188,28 @@ const PacksTableComponent = () => {
|
|||
width: '80px',
|
||||
render: renderActive,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.osquery.pack.queriesTable.actionsColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
width: '80px',
|
||||
actions: [
|
||||
{
|
||||
render: renderPlayAction,
|
||||
enabled: () => permissions.writeLiveQueries || permissions.runSavedQueries,
|
||||
},
|
||||
],
|
||||
} as EuiTableActionsColumnType<PackSavedObject>,
|
||||
],
|
||||
[renderActive, renderAgentPolicy, renderQueries, renderUpdatedAt]
|
||||
[
|
||||
permissions.runSavedQueries,
|
||||
permissions.writeLiveQueries,
|
||||
renderActive,
|
||||
renderAgentPolicy,
|
||||
renderPlayAction,
|
||||
renderQueries,
|
||||
renderUpdatedAt,
|
||||
]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
|
|
|
@ -28,7 +28,11 @@ export const useCreatePack = ({ withRedirect }: UseCreatePackProps) => {
|
|||
} = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useMutation<PackSavedObject, { body: { error: string; message: string } }>(
|
||||
return useMutation<
|
||||
{ data: PackSavedObject },
|
||||
{ body: { error: string; message: string } },
|
||||
PackSavedObject
|
||||
>(
|
||||
(payload) =>
|
||||
http.post('/api/osquery/packs', {
|
||||
body: JSON.stringify(payload),
|
||||
|
@ -47,7 +51,7 @@ export const useCreatePack = ({ withRedirect }: UseCreatePackProps) => {
|
|||
i18n.translate('xpack.osquery.newPack.successToastMessageText', {
|
||||
defaultMessage: 'Successfully created "{packName}" pack',
|
||||
values: {
|
||||
packName: payload.attributes?.name ?? '',
|
||||
packName: payload.data.attributes?.name ?? '',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -26,7 +26,7 @@ export const usePackQueryErrors = ({
|
|||
skip = false,
|
||||
}: UsePackQueryErrorsProps) => {
|
||||
const data = useKibana().services.data;
|
||||
const { data: logsDataView } = useLogsDataView();
|
||||
const { data: logsDataView } = useLogsDataView({ skip });
|
||||
|
||||
return useQuery(
|
||||
['scheduledQueryErrors', { actionId, interval }],
|
||||
|
|
|
@ -29,7 +29,7 @@ export const usePackQueryLastResults = ({
|
|||
skip = false,
|
||||
}: UsePackQueryLastResultsProps) => {
|
||||
const data = useKibana().services.data;
|
||||
const { data: logsDataView } = useLogsDataView();
|
||||
const { data: logsDataView } = useLogsDataView({ skip });
|
||||
|
||||
return useQuery(
|
||||
['scheduledQueryLastResults', { actionId }],
|
||||
|
|
|
@ -27,7 +27,7 @@ const LiveQueriesComponent = () => {
|
|||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/new`}>
|
||||
{(permissions.runSavedQueries && permissions.readSavedQueries) ||
|
||||
{(permissions.runSavedQueries && (permissions.readSavedQueries || permissions.readPacks)) ||
|
||||
permissions.writeLiveQueries ? (
|
||||
<NewLiveQueryPage />
|
||||
) : (
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiTabbedContent, EuiNotificationBadge } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { ResultsTable } from '../../../results/results_table';
|
||||
import { ActionResultsSummary } from '../../../action_results/action_results_summary';
|
||||
|
@ -18,7 +19,7 @@ interface ResultTabsProps {
|
|||
ecsMapping?: Record<string, string>;
|
||||
failedAgentsCount?: number;
|
||||
endDate?: string;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => ReactElement;
|
||||
}
|
||||
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { filter, omit } from 'lodash';
|
||||
import { filter, omit, some } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
@ -102,9 +102,14 @@ export const updateAssetsRoute = (router: IRouter, osqueryContext: OsqueryAppCon
|
|||
filter: `${packSavedObjectType}.attributes.name: "${packAssetSavedObject.attributes.name}"`,
|
||||
});
|
||||
|
||||
const name = conflictingEntries.saved_objects.length
|
||||
? `${packAssetSavedObject.attributes.name}-elastic`
|
||||
: packAssetSavedObject.attributes.name;
|
||||
const name =
|
||||
conflictingEntries.saved_objects.length &&
|
||||
some(conflictingEntries.saved_objects, [
|
||||
'attributes.name',
|
||||
packAssetSavedObject.attributes.name,
|
||||
])
|
||||
? `${packAssetSavedObject.attributes.name}-elastic`
|
||||
: packAssetSavedObject.attributes.name;
|
||||
|
||||
await savedObjectsClient.create(
|
||||
packSavedObjectType,
|
||||
|
|
|
@ -51,7 +51,10 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
|
|||
osquery: { writeLiveQueries, runSavedQueries },
|
||||
} = await coreStartServices.capabilities.resolveCapabilities(request);
|
||||
|
||||
const isInvalid = !(writeLiveQueries || (runSavedQueries && request.body.saved_query_id));
|
||||
const isInvalid = !(
|
||||
writeLiveQueries ||
|
||||
(runSavedQueries && (request.body.saved_query_id || request.body.pack_id))
|
||||
);
|
||||
|
||||
if (isInvalid) {
|
||||
return response.forbidden();
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import { omit } from 'lodash';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||
import { PLUGIN_ID } from '../../../common';
|
||||
|
||||
import type {
|
||||
ActionsRequestOptions,
|
||||
ActionsStrategyResponse,
|
||||
Direction,
|
||||
} from '../../../common/search_strategy';
|
||||
import { OsqueryQueries } from '../../../common/search_strategy';
|
||||
import { createFilter, generateTablePaginationOptions } from '../../../common/utils/build_query';
|
||||
|
||||
export const findLiveQueryRoute = (router: IRouter<DataRequestHandlerContext>) => {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/osquery/live_queries',
|
||||
validate: {
|
||||
query: schema.object(
|
||||
{
|
||||
filterQuery: schema.maybe(schema.string()),
|
||||
page: schema.maybe(schema.number()),
|
||||
pageSize: schema.maybe(schema.number()),
|
||||
sort: schema.maybe(schema.string()),
|
||||
sortOrder: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
},
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
|
||||
|
||||
try {
|
||||
const search = await context.search;
|
||||
const res = await lastValueFrom(
|
||||
search.search<ActionsRequestOptions, ActionsStrategyResponse>(
|
||||
{
|
||||
factoryQueryType: OsqueryQueries.actions,
|
||||
filterQuery: createFilter(request.query.filterQuery),
|
||||
pagination: generateTablePaginationOptions(
|
||||
request.query.page ?? 0,
|
||||
request.query.pageSize ?? 100
|
||||
),
|
||||
sort: {
|
||||
direction: (request.query.sortOrder ?? 'desc') as Direction,
|
||||
field: request.query.sort ?? 'created_at',
|
||||
},
|
||||
},
|
||||
{ abortSignal, strategy: 'osquerySearchStrategy' }
|
||||
)
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
data: {
|
||||
...omit(res, 'edges'),
|
||||
items: res.edges,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return response.customError({
|
||||
statusCode: e.statusCode ?? 500,
|
||||
body: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getRequestAbortedSignal(aborted$: Observable<void>): AbortSignal {
|
||||
const controller = new AbortController();
|
||||
aborted$.subscribe(() => controller.abort());
|
||||
|
||||
return controller.signal;
|
||||
}
|
|
@ -28,10 +28,10 @@ export const getLiveQueryResultsRoute = (router: IRouter<DataRequestHandlerConte
|
|||
query: schema.object(
|
||||
{
|
||||
filterQuery: schema.maybe(schema.string()),
|
||||
pageIndex: schema.maybe(schema.number()),
|
||||
page: schema.maybe(schema.number()),
|
||||
pageSize: schema.maybe(schema.number()),
|
||||
sortField: schema.maybe(schema.string()),
|
||||
sortOrder: schema.maybe(schema.string()),
|
||||
sort: schema.maybe(schema.string()),
|
||||
sortOrder: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
|
@ -78,15 +78,13 @@ export const getLiveQueryResultsRoute = (router: IRouter<DataRequestHandlerConte
|
|||
factoryQueryType: OsqueryQueries.results,
|
||||
filterQuery: createFilter(request.query.filterQuery),
|
||||
pagination: generateTablePaginationOptions(
|
||||
request.query.pageIndex ?? 0,
|
||||
request.query.page ?? 0,
|
||||
request.query.pageSize ?? 100
|
||||
),
|
||||
sort: [
|
||||
{
|
||||
direction: request.query.sortOrder ?? 'desc',
|
||||
field: request.query.sortField ?? '@timestamp',
|
||||
},
|
||||
],
|
||||
sort: {
|
||||
direction: request.query.sortOrder ?? 'desc',
|
||||
field: request.query.sort ?? '@timestamp',
|
||||
},
|
||||
},
|
||||
{ abortSignal, strategy: 'osquerySearchStrategy' }
|
||||
)
|
||||
|
|
|
@ -11,11 +11,13 @@ import { createLiveQueryRoute } from './create_live_query_route';
|
|||
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
|
||||
import { getLiveQueryDetailsRoute } from './get_live_query_details_route';
|
||||
import { getLiveQueryResultsRoute } from './get_live_query_results_route';
|
||||
import { findLiveQueryRoute } from './find_live_query_route';
|
||||
|
||||
export const initLiveQueryRoutes = (
|
||||
router: IRouter<DataRequestHandlerContext>,
|
||||
context: OsqueryAppContext
|
||||
) => {
|
||||
findLiveQueryRoute(router);
|
||||
createLiveQueryRoute(router, context);
|
||||
getLiveQueryDetailsRoute(router);
|
||||
getLiveQueryResultsRoute(router);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { has, mapKeys, set, unset, find } from 'lodash';
|
||||
import { has, mapKeys, set, unset, find, some } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { produce } from 'immer';
|
||||
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
|
@ -80,7 +80,10 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
filter: `${packSavedObjectType}.attributes.name: "${name}"`,
|
||||
});
|
||||
|
||||
if (conflictingEntries.saved_objects.length) {
|
||||
if (
|
||||
conflictingEntries.saved_objects.length &&
|
||||
some(conflictingEntries.saved_objects, ['attributes.name', name])
|
||||
) {
|
||||
return response.conflict({ body: `Pack with name "${name}" already exists.` });
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { set, unset, has, difference, filter, find, map, mapKeys, uniq } from 'lodash';
|
||||
import { set, unset, has, difference, filter, find, map, mapKeys, uniq, some } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { produce } from 'immer';
|
||||
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
|
@ -95,11 +95,10 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
});
|
||||
|
||||
if (
|
||||
filter(
|
||||
conflictingEntries.saved_objects,
|
||||
(packSO) =>
|
||||
packSO.id !== currentPackSO.id && packSO.attributes.name.length === name.length
|
||||
).length
|
||||
some(
|
||||
filter(conflictingEntries.saved_objects, (packSO) => packSO.id !== currentPackSO.id),
|
||||
['attributes.name', name]
|
||||
)
|
||||
) {
|
||||
return response.conflict({ body: `Pack with name "${name}" already exists.` });
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty, pickBy } from 'lodash';
|
||||
import { isEmpty, pickBy, some } from 'lodash';
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import { PLUGIN_ID } from '../../../common';
|
||||
import type { CreateSavedQueryRequestSchemaDecoded } from '../../../common/schemas/routes/saved_query/create_saved_query_request_schema';
|
||||
|
@ -41,7 +41,10 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
|
|||
filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`,
|
||||
});
|
||||
|
||||
if (conflictingEntries.saved_objects.length) {
|
||||
if (
|
||||
conflictingEntries.saved_objects.length &&
|
||||
some(conflictingEntries.saved_objects, ['attributes.id', id])
|
||||
) {
|
||||
return response.conflict({ body: `Saved query with id "${id}" already exists.` });
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { filter } from 'lodash';
|
||||
import { filter, some } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
|
@ -76,8 +76,10 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
|
|||
});
|
||||
|
||||
if (
|
||||
filter(conflictingEntries.saved_objects, (soObject) => soObject.id !== request.params.id)
|
||||
.length
|
||||
some(
|
||||
filter(conflictingEntries.saved_objects, (soObject) => soObject.id !== request.params.id),
|
||||
['attributes.id', id]
|
||||
)
|
||||
) {
|
||||
return response.conflict({ body: `Saved query with id "${id}" already exists.` });
|
||||
}
|
||||
|
|
|
@ -22462,7 +22462,6 @@
|
|||
"xpack.osquery.agents.policyLabel": "Politique",
|
||||
"xpack.osquery.agents.selectAgentLabel": "Sélectionner les agents ou les groupes à interroger",
|
||||
"xpack.osquery.agents.selectionLabel": "Agents",
|
||||
"xpack.osquery.all_actions.fetchError": "Erreur lors de la récupération des actions",
|
||||
"xpack.osquery.appNavigation.liveQueriesLinkText": "Recherches en direct",
|
||||
"xpack.osquery.appNavigation.manageIntegrationButton": "Gérer l'intégration",
|
||||
"xpack.osquery.appNavigation.packsLinkText": "Packs",
|
||||
|
|
|
@ -22541,7 +22541,6 @@
|
|||
"xpack.osquery.agents.policyLabel": "ポリシー",
|
||||
"xpack.osquery.agents.selectAgentLabel": "クエリを実行するエージェントまたはグループを選択",
|
||||
"xpack.osquery.agents.selectionLabel": "エージェント",
|
||||
"xpack.osquery.all_actions.fetchError": "アクションの取得中にエラーが発生しました",
|
||||
"xpack.osquery.appNavigation.liveQueriesLinkText": "ライブクエリ",
|
||||
"xpack.osquery.appNavigation.manageIntegrationButton": "統合を管理",
|
||||
"xpack.osquery.appNavigation.packsLinkText": "パック",
|
||||
|
|
|
@ -22565,7 +22565,6 @@
|
|||
"xpack.osquery.agents.policyLabel": "策略",
|
||||
"xpack.osquery.agents.selectAgentLabel": "选择要查询的代理或组",
|
||||
"xpack.osquery.agents.selectionLabel": "代理",
|
||||
"xpack.osquery.all_actions.fetchError": "提取操作时出错",
|
||||
"xpack.osquery.appNavigation.liveQueriesLinkText": "实时查询",
|
||||
"xpack.osquery.appNavigation.manageIntegrationButton": "管理集成",
|
||||
"xpack.osquery.appNavigation.packsLinkText": "包",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue