[Osquery] Fix live packs (#137651)

This commit is contained in:
Patryk Kopyciński 2022-08-01 10:55:19 +02:00 committed by GitHub
parent c26e085c92
commit b4a212b8dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 374 additions and 179 deletions

View file

@ -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', () => {

View file

@ -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}

View file

@ -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',
}),
}),
}
);
};

View file

@ -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',
}),
}),
}
);
};

View file

@ -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,
}
);
};

View file

@ -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}
</>
) : (
<>

View file

@ -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 &&

View file

@ -64,6 +64,7 @@ export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest
(newSelectedOptions) => {
if (!newSelectedOptions.length) {
setSelectedOptions(newSelectedOptions);
field.setValue([]);
return;
}

View file

@ -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>('');

View file

@ -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(

View file

@ -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 ?? '',
},
})
);

View file

@ -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 }],

View file

@ -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 }],

View file

@ -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 />
) : (

View file

@ -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> = ({

View file

@ -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,

View file

@ -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();

View file

@ -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;
}

View file

@ -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' }
)

View file

@ -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);

View file

@ -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.` });
}

View file

@ -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.` });
}

View file

@ -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.` });
}

View file

@ -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.` });
}

View file

@ -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",

View file

@ -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": "パック",

View file

@ -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": "包",