[EDR Workflows] Osquery Timeout (#169925)

https://github.com/elastic/security-team/issues/7775

This PR adds `Timeout` form field to all the places where Osquery query
can be configured. Behind the scenes all the scenarios lead to the value
being passed down to the fleet.

Live Query
![Screenshot 2023-11-06 at 16 20
05](e87dd693-0f29-42a3-affb-20b0b42ff8ab)
Live Pack Query
![Screenshot 2023-11-06 at 17 12
41](35424508-b97c-43fa-befd-01f494b61ffd)
Saved Query
![Screenshot 2023-11-06 at 17 11
58](e5100fa6-7251-46a2-b83c-f843d157e889)
Osquery Response Action Query
![Screenshot 2023-11-06 at 17 13
40](b6ac4dd6-0d85-4ed5-9d4d-c1c485182a70)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Konrad Szwarc 2023-11-15 13:37:11 +01:00 committed by GitHub
parent 1b6822cb38
commit e7cf51b63c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 389 additions and 71 deletions

View file

@ -42,10 +42,6 @@
}
}
},
"metrics-data-source": {
"dynamic": false,
"properties": {}
},
"url": {
"dynamic": false,
"properties": {
@ -1473,6 +1469,10 @@
}
}
},
"metrics-data-source": {
"dynamic": false,
"properties": {}
},
"canvas-element": {
"dynamic": false,
"properties": {
@ -2168,6 +2168,9 @@
"interval": {
"type": "keyword"
},
"timeout": {
"type": "short"
},
"ecs_mapping": {
"dynamic": false,
"properties": {}
@ -2216,6 +2219,9 @@
"interval": {
"type": "text"
},
"timeout": {
"type": "short"
},
"platform": {
"type": "keyword"
},
@ -2258,6 +2264,9 @@
"interval": {
"type": "text"
},
"timeout": {
"type": "short"
},
"platform": {
"type": "keyword"
},
@ -2679,50 +2688,6 @@
}
}
},
"apm-telemetry": {
"dynamic": false,
"properties": {}
},
"apm-server-schema": {
"properties": {
"schemaJson": {
"type": "text",
"index": false
}
}
},
"apm-service-group": {
"properties": {
"groupName": {
"type": "keyword"
},
"kuery": {
"type": "text"
},
"description": {
"type": "text"
},
"color": {
"type": "text"
}
}
},
"apm-custom-dashboards": {
"properties": {
"dashboardSavedObjectId": {
"type": "keyword"
},
"kuery": {
"type": "text"
},
"serviceEnvironmentFilterEnabled": {
"type": "boolean"
},
"serviceNameFilterEnabled": {
"type": "boolean"
}
}
},
"enterprise_search_telemetry": {
"dynamic": false,
"properties": {}
@ -3196,5 +3161,49 @@
"index": false
}
}
},
"apm-telemetry": {
"dynamic": false,
"properties": {}
},
"apm-server-schema": {
"properties": {
"schemaJson": {
"type": "text",
"index": false
}
}
},
"apm-service-group": {
"properties": {
"groupName": {
"type": "keyword"
},
"kuery": {
"type": "text"
},
"description": {
"type": "text"
},
"color": {
"type": "text"
}
}
},
"apm-custom-dashboards": {
"properties": {
"dashboardSavedObjectId": {
"type": "keyword"
},
"kuery": {
"type": "text"
},
"serviceEnvironmentFilterEnabled": {
"type": "boolean"
},
"serviceNameFilterEnabled": {
"type": "boolean"
}
}
}
}

View file

@ -7,6 +7,7 @@
*/
import * as t from 'io-ts';
import { inRangeRt } from '@kbn/io-ts-utils';
export const id = t.string;
export type Id = t.TypeOf<typeof id>;
@ -49,6 +50,11 @@ export type Interval = t.TypeOf<typeof interval>;
export const intervalOrUndefined = t.union([interval, t.undefined]);
export type IntervalOrUndefined = t.TypeOf<typeof intervalOrUndefined>;
export const timeout = inRangeRt(60, 60 * 15);
export type Timeout = t.TypeOf<typeof timeout>;
export const timeoutOrUndefined = t.union([timeout, t.undefined]);
export type TimeoutOrUndefined = t.TypeOf<typeof timeoutOrUndefined>;
export const snapshot = t.boolean;
export type Snapshot = t.TypeOf<typeof snapshot>;
export const snapshotOrUndefined = t.union([snapshot, t.undefined]);

View file

@ -12,5 +12,8 @@
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/io-ts-utils",
]
}

View file

@ -125,9 +125,9 @@ describe('checking migration metadata changes on all registered SO types', () =>
"monitoring-telemetry": "5d91bf75787d9d4dd2fae954d0b3f76d33d2e559",
"observability-onboarding-state": "b16064c516aac64ae699c737d7d10b6e199bfded",
"osquery-manager-usage-metric": "983bcbc3b7dda0aad29b20907db233abba709bcc",
"osquery-pack": "6ab4358ca4304a12dcfc1777c8135b75cffb4397",
"osquery-pack-asset": "b14101d3172c4b60eb5404696881ce5275c84152",
"osquery-saved-query": "44f1161e165defe3f9b6ad643c68c542a765fcdb",
"osquery-pack": "702e86b1a936153b39f65b0781bdc136e186e123",
"osquery-pack-asset": "cd140bc2e4b092e93692b587bf6e38051ef94c75",
"osquery-saved-query": "6095e288750aa3164dfe186c74bc5195c2bf2bd4",
"policy-settings-protection-updates-note": "33924bb246f9e5bcb876109cc83e3c7a28308352",
"query": "21cbbaa09abb679078145ce90087b1e88b7eae95",
"risk-engine-configuration": "b105d4a3c6adce40708d729d12e5ef3c8fbd9508",

View file

@ -13,6 +13,7 @@ import {
packIdOrUndefined,
queryOrUndefined,
arrayQueries,
timeoutOrUndefined,
} from '@kbn/osquery-io-ts-types';
export const createLiveQueryRequestBodySchema = t.partial({
@ -23,6 +24,7 @@ export const createLiveQueryRequestBodySchema = t.partial({
query: queryOrUndefined,
queries: arrayQueries,
saved_query_id: savedQueryIdOrUndefined,
timeout: timeoutOrUndefined,
ecs_mapping: ecsMappingOrUndefined,
pack_id: packIdOrUndefined,
alert_ids: t.array(t.string),

View file

@ -18,6 +18,7 @@ import {
snapshotOrUndefined,
removedOrUndefined,
ecsMappingOrUndefined,
timeoutOrUndefined,
} from '@kbn/osquery-io-ts-types';
export const createSavedQueryRequestSchema = t.type({
@ -27,6 +28,7 @@ export const createSavedQueryRequestSchema = t.type({
query,
version: versionOrUndefined,
interval,
timeout: timeoutOrUndefined,
snapshot: snapshotOrUndefined,
removed: removedOrUndefined,
ecs_mapping: ecsMappingOrUndefined,

View file

@ -7,12 +7,14 @@
import * as t from 'io-ts';
import { toNumberRt } from '@kbn/io-ts-utils';
import { timeoutOrUndefined } from '@kbn/osquery-io-ts-types';
export const updateSavedQueryRequestBodySchema = t.type({
id: t.string,
query: t.string,
description: t.union([t.string, t.undefined]),
interval: t.union([toNumberRt, t.undefined]),
timeout: timeoutOrUndefined,
snapshot: t.union([t.boolean, t.undefined]),
removed: t.union([t.boolean, t.undefined]),
platform: t.union([t.string, t.undefined]),

View file

@ -29,3 +29,8 @@ export const API_VERSIONS = {
v1: '1',
},
};
export enum QUERY_TIMEOUT {
DEFAULT = 60, // 60 seconds
MAX = 60 * 15,
}

View file

@ -8,6 +8,7 @@
import { navigateTo } from '../../tasks/navigation';
import {
checkResults,
fillInQueryTimeout,
inputQuery,
selectAllAgents,
submitQuery,
@ -36,13 +37,26 @@ describe('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => {
cy.contains('Query is a required field').should('not.exist');
checkResults();
getAdvancedButton().click();
fillInQueryTimeout('91');
submitQuery();
cy.contains('Timeout value must be lower than 900 seconds.');
fillInQueryTimeout('89');
submitQuery();
cy.contains('Timeout value must be lower than 900 seconds.').should('not.exist');
typeInOsqueryFieldInput('days{downArrow}{enter}');
submitQuery();
cy.contains('ECS field is required.');
typeInECSFieldInput('message{downArrow}{enter}');
cy.intercept('POST', '/api/osquery/live_queries').as('postQuery');
submitQuery();
cy.contains('ECS field is required.').should('not.exist');
cy.wait('@postQuery').then((interception) => {
expect(interception.request.body).to.have.property('query', 'select * from uptime;');
expect(interception.request.body).to.have.property('timeout', 890);
expect(interception.response?.statusCode).to.eq(200);
expect(interception.response?.body.data.queries[0]).to.have.property('timeout', 890);
});
checkResults();
cy.get('[data-gridcell-column-index="0"][data-gridcell-row-index="0"]').should('exist').click();
cy.url().should('include', 'app/fleet/agents/');

View file

@ -29,18 +29,21 @@ describe('ALL - Live Query Packs', { tags: ['@ess', '@serverless'] }, () => {
system_memory_linux_elastic: {
ecs_mapping: {},
interval: 3600,
timeout: 700,
platform: 'linux',
query: 'SELECT * FROM memory_info;',
},
system_info_elastic: {
ecs_mapping: {},
interval: 3600,
timeout: 200,
platform: 'linux,windows,darwin',
query: 'SELECT * FROM system_info;',
},
failingQuery: {
ecs_mapping: {},
interval: 10,
timeout: 90,
query: 'select opera_extensions.* from users join opera_extensions using (uid);',
},
},

View file

@ -197,18 +197,21 @@ describe('Packs - Create and Edit', { tags: ['@ess', '@serverless'] }, () => {
const queries = {
Query1: {
interval: 3600,
timeout: 60,
query: 'select * from uptime;',
removed: true,
snapshot: false,
},
Query2: {
interval: 3600,
timeout: 60,
query: 'select * from uptime;',
removed: false,
snapshot: false,
},
Query3: {
interval: 3600,
timeout: 60,
query: 'select * from uptime;',
},
};

View file

@ -40,6 +40,12 @@ export const submitQuery = () => {
cy.contains('Submit').click();
};
export const fillInQueryTimeout = (timeout: string) => {
cy.getBySel('advanced-accordion-content').within(() => {
cy.getBySel('timeout-input').clear().type(timeout);
});
};
// sometimes the results get stuck in the tests, this is a workaround
export const checkResults = () => {
cy.getBySel('osqueryResultsTable').then(($table) => {

View file

@ -10,3 +10,4 @@ export { QueryDescriptionField } from './query_description_field';
export { IntervalField } from './interval_field';
export { QueryIdField } from './query_id_field';
export { ResultsTypeField } from './results_type_field';
export { TimeoutField } from './timeout_field';

View file

@ -0,0 +1,114 @@
/*
* 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 React, { useCallback, useMemo } from 'react';
import deepEqual from 'fast-deep-equal';
import { useController } from 'react-hook-form';
import type { EuiFieldNumberProps } from '@elastic/eui';
import {
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiIconTip,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { QUERY_TIMEOUT } from '../../common/constants';
const timeoutFieldValidations = {
min: {
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.timeoutFieldMinNumberError', {
defaultMessage: 'Timeout value must be greater than {than} seconds.',
values: { than: QUERY_TIMEOUT.DEFAULT },
}),
value: QUERY_TIMEOUT.DEFAULT,
},
max: {
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.timeoutFieldMaxNumberError', {
defaultMessage: 'Timeout value must be lower than {than} seconds.',
values: { than: QUERY_TIMEOUT.MAX },
}),
value: QUERY_TIMEOUT.MAX,
},
};
interface TimeoutFieldProps {
euiFieldProps?: Record<string, unknown>;
}
const TimeoutFieldComponent = ({ euiFieldProps }: TimeoutFieldProps) => {
const {
field: { onChange, value },
fieldState: { error },
} = useController({
name: 'timeout',
defaultValue: QUERY_TIMEOUT.DEFAULT,
rules: {
...timeoutFieldValidations,
},
});
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const numberValue = e.target.valueAsNumber ? e.target.valueAsNumber : 0;
onChange(numberValue);
},
[onChange]
);
const hasError = useMemo(() => !!error?.message, [error?.message]);
return (
<EuiFormRow
label={
<EuiFlexGroup gutterSize="xs" alignItems="flexEnd">
<EuiFlexItem grow={false}>
<FormattedMessage id="xpack.osquery.liveQuery.timeout" defaultMessage="Timeout" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
content={i18n.translate('xpack.osquery.liveQuery.timeoutHint', {
defaultMessage: 'Maximum time to wait for query results, default is 60 seconds.',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
}
fullWidth
error={error?.message}
isInvalid={hasError}
labelAppend={
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.osquery.osquery.liveQuery.timeoutFieldOptionalLabel"
defaultMessage="(optional)"
/>
</EuiText>
</EuiFlexItem>
}
>
<EuiFieldNumber
isInvalid={hasError}
value={value as EuiFieldNumberProps['value']}
onChange={handleChange}
fullWidth
type="number"
data-test-subj="timeout-input"
name="timeout"
min={QUERY_TIMEOUT.DEFAULT}
max={QUERY_TIMEOUT.MAX}
defaultValue={QUERY_TIMEOUT.DEFAULT}
step={1}
append="seconds"
{...euiFieldProps}
/>
</EuiFormRow>
);
};
export const TimeoutField = React.memo(TimeoutFieldComponent, deepEqual);

View file

@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useForm as useHookForm, FormProvider } from 'react-hook-form';
import { isEmpty, find, pickBy } from 'lodash';
import { isEmpty, find, pickBy, isNumber } from 'lodash';
import {
containsDynamicQuery,
@ -39,6 +39,7 @@ export interface LiveQueryFormFields {
savedQueryId?: string | null;
ecs_mapping: ECSMapping;
packId: string[];
timeout?: number;
queryType: 'query' | 'pack';
}
@ -151,10 +152,10 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
alert_ids: values.alertIds,
pack_id: queryType === 'pack' && values?.packId?.length ? values?.packId[0] : undefined,
ecs_mapping: values.ecs_mapping,
timeout: values.timeout,
},
(value) => !isEmpty(value)
(value) => !isEmpty(value) || isNumber(value)
) as unknown as LiveQueryFormFields;
await mutateAsync(serializedData);
},
[alertAttachmentContext, mutateAsync, queryType]

View file

@ -11,6 +11,8 @@ import { EuiCodeBlock, EuiFormRow, EuiAccordion, EuiSpacer } from '@elastic/eui'
import React, { useCallback, useMemo, useState } from 'react';
import { useController, useFormContext } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
import { QUERY_TIMEOUT } from '../../../common/constants';
import { TimeoutField } from '../../form/timeout_field';
import type { LiveQueryFormFields } from '.';
import { OsqueryEditor } from '../../editor';
import { useKibana } from '../../common/lib/kibana';
@ -68,6 +70,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
resetField('query', { defaultValue: savedQuery.query });
resetField('savedQueryId', { defaultValue: savedQuery.savedQueryId });
resetField('ecs_mapping', { defaultValue: savedQuery.ecs_mapping ?? {} });
resetField('timeout', { defaultValue: savedQuery.timeout ?? QUERY_TIMEOUT.DEFAULT });
if (!isEmpty(savedQuery.ecs_mapping)) {
setAdvancedContentState('open');
@ -122,6 +125,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
{!isSavedQueryDisabled && (
<SavedQueriesDropdown disabled={isSavedQueryDisabled} onChange={handleSavedQueryChange} />
)}
<EuiFormRow
isInvalid={!!error?.message}
error={error?.message}
@ -155,6 +159,8 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
data-test-subj="advanced-accordion-content"
>
<EuiSpacer size="xs" />
<TimeoutField />
<EuiSpacer size="s" />
<ECSMappingEditorField euiFieldProps={ecsFieldProps} />
</EuiAccordion>
)}

View file

@ -14,6 +14,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import deepEqual from 'fast-deep-equal';
import { useController, useFormContext, useWatch, useFieldArray } from 'react-hook-form';
import { QUERY_TIMEOUT } from '../../../common/constants';
import { PackQueriesTable } from '../pack_queries_table';
import { QueryFlyout } from '../queries/query_flyout';
import { OsqueryPackUploader } from './pack_uploader';
@ -84,6 +85,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({ euiFieldProps }) =
draft.id = updatedQuery.id;
draft.interval = updatedQuery.interval;
draft.query = updatedQuery.query;
draft.timeout = updatedQuery.timeout;
if (updatedQuery.platform?.length) {
draft.platform = updatedQuery.platform;
@ -137,6 +139,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({ euiFieldProps }) =
{
id: newQueryId,
interval: newQuery.interval ?? parsedContent.interval ?? '3600',
timeout: newQuery.timeout ?? parsedContent.timeout ?? QUERY_TIMEOUT.DEFAULT,
query: newQuery.query,
version: newQuery.version ?? parsedContent.version,
snapshot: newQuery.snapshot ?? parsedContent.snapshot,

View file

@ -17,6 +17,7 @@ export const convertPackQueriesToSO = (queries: Record<string, Omit<PackQueryFor
...pick(value, [
'query',
'interval',
'timeout',
'snapshot',
'removed',
'platform',

View file

@ -22,8 +22,14 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { FormProvider } from 'react-hook-form';
import { DEFAULT_PLATFORM } from '../../../common/constants';
import { QueryIdField, IntervalField, VersionField, ResultsTypeField } from '../../form';
import { DEFAULT_PLATFORM, QUERY_TIMEOUT } from '../../../common/constants';
import {
QueryIdField,
IntervalField,
VersionField,
ResultsTypeField,
TimeoutField,
} from '../../form';
import { CodeEditorField } from '../../saved_queries/form/code_editor_field';
import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field';
import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants';
@ -79,6 +85,9 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
});
resetField('version', { defaultValue: savedQuery.version ? [savedQuery.version] : [] });
resetField('interval', { defaultValue: savedQuery.interval ? savedQuery.interval : 3600 });
resetField('timeout', {
defaultValue: savedQuery.timeout ? savedQuery.timeout : QUERY_TIMEOUT.DEFAULT,
});
resetField('snapshot', { defaultValue: savedQuery.snapshot ?? true });
resetField('removed', { defaultValue: savedQuery.removed });
resetField('ecs_mapping', { defaultValue: savedQuery.ecs_mapping ?? {} });
@ -146,9 +155,14 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
<EuiSpacer />
<ResultsTypeField />
</EuiFlexItem>
<EuiFlexItem>
<PlatformCheckBoxGroupField />
</EuiFlexItem>
<EuiFlexGroup direction={'column'} justifyContent={'spaceBetween'}>
<EuiFlexItem>
<PlatformCheckBoxGroupField />
</EuiFlexItem>
<EuiFlexItem grow={0}>
<TimeoutField />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>

View file

@ -11,6 +11,7 @@ import type { Draft } from 'immer';
import { produce } from 'immer';
import { useMemo } from 'react';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { QUERY_TIMEOUT } from '../../../common/constants';
import type { Shard } from '../../../common/utils/converters';
export interface UsePackQueryFormProps {
@ -22,6 +23,7 @@ export interface PackSOQueryFormData {
id: string;
query: string;
interval: string;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
platform?: string | undefined;
@ -37,6 +39,7 @@ export interface PackQueryFormData {
description?: string;
query: string;
interval: number;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
platform?: string | undefined;
@ -48,6 +51,7 @@ const deserializer = (payload: PackSOQueryFormData): PackQueryFormData => ({
id: payload.id,
query: payload.query,
interval: payload.interval ? parseInt(payload.interval, 10) : 3600,
timeout: payload.timeout || QUERY_TIMEOUT.DEFAULT,
snapshot: payload.snapshot,
removed: payload.removed,
platform: payload.platform,

View file

@ -34,6 +34,7 @@ export interface SavedQuerySO {
saved_object_id: string;
description?: string;
query: string;
timeout?: number;
ecs_mapping: ECSMapping;
updated_at: string;
prebuilt?: boolean;

View file

@ -23,6 +23,7 @@ import {
QueryDescriptionField,
VersionField,
ResultsTypeField,
TimeoutField,
} from '../../form';
import { PlatformCheckBoxGroupField } from '../../packs/queries/platform_checkbox_group_field';
import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../packs/queries/constants';
@ -89,12 +90,20 @@ const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({
<QueryDescriptionField euiFieldProps={euiFieldProps} />
<EuiSpacer />
<CodeEditorField euiFieldProps={euiFieldProps} />
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent={'spaceBetween'}>
<EuiFlexItem>
<TimeoutField />
</EuiFlexItem>
<EuiFlexItem />
</EuiFlexGroup>
<EuiSpacer size="xl" />
<EuiFlexGroup>
<EuiFlexItem css={overflowCss}>
<ECSMappingEditorField euiFieldProps={euiFieldProps} />
</EuiFlexItem>
</EuiFlexGroup>
{!viewMode && hasPlayground && (
<EuiFlexGroup>
<EuiFlexItem grow={false}>

View file

@ -11,6 +11,7 @@ import type { Draft } from 'immer';
import produce from 'immer';
import { useMemo } from 'react';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { QUERY_TIMEOUT } from '../../../common/constants';
import { useSavedQueries } from '../use_saved_queries';
export interface SavedQuerySOFormData {
@ -18,6 +19,7 @@ export interface SavedQuerySOFormData {
description?: string;
query?: string;
interval?: string;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
platform?: string;
@ -30,6 +32,7 @@ export interface SavedQueryFormData {
description?: string;
query?: string;
interval?: number;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
platform?: string;
@ -46,6 +49,7 @@ const deserializer = (payload: SavedQuerySOFormData): SavedQueryFormData => ({
description: payload.description,
query: payload.query,
interval: payload.interval ? parseInt(payload.interval, 10) : 3600,
timeout: payload.timeout ?? QUERY_TIMEOUT.DEFAULT,
snapshot: payload.snapshot ?? true,
removed: payload.removed ?? false,
platform: payload.platform,
@ -95,6 +99,7 @@ export const useSavedQueryForm = ({ defaultValue }: UseSavedQueryFormProps) => {
id: '',
query: '',
interval: 3600,
timeout: QUERY_TIMEOUT.DEFAULT,
ecs_mapping: {},
snapshot: true,
},

View file

@ -25,7 +25,7 @@ export interface SavedQueriesDropdownProps {
disabled?: boolean;
onChange: (
value:
| (Pick<SavedQuerySO, 'id' | 'description' | 'query' | 'ecs_mapping'> & {
| (Pick<SavedQuerySO, 'id' | 'description' | 'query' | 'ecs_mapping' | 'timeout'> & {
savedQueryId: string;
})
| null

View file

@ -24,23 +24,27 @@ interface OsqueryResponseActionsValues {
id?: string;
ecsMapping?: ECSMapping;
query?: string;
timeout: number;
packId?: string;
queries?: Array<{
id: string;
ecs_mapping: ECSMapping;
query: string;
timeout?: number;
}>;
}
interface OsqueryResponseActionsParamsFormFields {
savedQueryId: string | null;
ecs_mapping: ECSMapping;
timeout: number;
query: string;
packId?: string[];
queries: Array<{
id: string;
ecs_mapping: ECSMapping;
query: string;
timeout?: number;
}>;
queryType: 'query' | 'pack';
}
@ -115,6 +119,7 @@ const OsqueryResponseActionParamsFormComponent = ({
: {
savedQueryId: formData.savedQueryId,
query: formData.query,
timeout: formData.timeout,
ecsMapping: formData.ecs_mapping,
}
);

View file

@ -21,6 +21,7 @@ export interface PackSavedObject {
name: string;
query: string;
interval: number;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
ecs_mapping?: Record<string, unknown>;
@ -41,6 +42,7 @@ export interface SavedQuerySavedObject {
description: string | undefined;
query: string;
interval: number | string;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
platform: string;

View file

@ -7,7 +7,7 @@
import { v4 as uuidv4 } from 'uuid';
import moment from 'moment';
import { filter, isEmpty, map, omit, pick, pickBy, some } from 'lodash';
import { filter, isEmpty, isNumber, map, omit, pick, pickBy, some } from 'lodash';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import type { CreateLiveQueryRequestBodySchema } from '../../../common/api';
@ -17,7 +17,7 @@ import { parseAgentSelection } from '../../lib/parse_agent_groups';
import { packSavedObjectType } from '../../../common/types';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { convertSOQueriesToPack } from '../../routes/pack/utils';
import { ACTIONS_INDEX } from '../../../common/constants';
import { ACTIONS_INDEX, QUERY_TIMEOUT } from '../../../common/constants';
import { TELEMETRY_EBT_LIVE_QUERY_EVENT } from '../../lib/telemetry/constants';
import type { PackSavedObject } from '../../common/types';
import { CustomHttpRequestError } from '../../common/error';
@ -100,9 +100,10 @@ export const createActionHandler = async (
ecs_mapping: packQuery.ecs_mapping,
version: packQuery.version,
platform: packQuery.platform,
timeout: packQuery.timeout,
agents: selectedAgents,
},
(value) => !isEmpty(value)
(value) => !isEmpty(value) || isNumber(value)
);
})
: await createDynamicQueries({
@ -125,6 +126,7 @@ export const createActionHandler = async (
input_type: 'osquery',
agents: query.agents,
user_id: metadata?.currentUser,
...(query.timeout !== QUERY_TIMEOUT.DEFAULT ? { timeout: query.timeout } : {}),
data: pick(query, ['id', 'query', 'ecs_mapping', 'version', 'platform']),
})
)

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { isEmpty, map, pickBy } from 'lodash';
import { isEmpty, isNumber, map, pickBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
@ -43,7 +43,7 @@ export const createDynamicQueries = async ({
alert_ids: params.alert_ids,
agents,
},
(value) => !isEmpty(value) || value === true
(value) => !isEmpty(value) || value === true || isNumber(value)
);
})
: [
@ -61,10 +61,11 @@ export const createDynamicQueries = async ({
: undefined,
ecs_mapping: params.ecs_mapping,
alert_ids: params.alert_ids,
timeout: params.timeout,
agents,
...(error ? { error } : {}),
},
(value) => !isEmpty(value)
(value) => !isEmpty(value) || isNumber(value)
),
];

View file

@ -8,6 +8,11 @@
import { produce } from 'immer';
import type { SavedObjectsType } from '@kbn/core/server';
import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import {
packAssetSavedObjectModelVersion1,
packSavedObjectModelVersion1,
savedQueryModelVersion1,
} from './saved_object_model_versions';
import {
savedQuerySavedObjectType,
packSavedObjectType,
@ -67,6 +72,9 @@ export const savedQuerySavedObjectMappings: SavedObjectsType['mappings'] = {
interval: {
type: 'keyword',
},
timeout: {
type: 'short',
},
ecs_mapping: {
dynamic: false,
properties: {},
@ -80,6 +88,9 @@ export const savedQueryType: SavedObjectsType = {
hidden: false,
namespaceType: 'multiple-isolated',
mappings: savedQuerySavedObjectMappings,
modelVersions: {
1: savedQueryModelVersion1,
},
management: {
importableAndExportable: true,
getTitle: (savedObject) => savedObject.attributes.id,
@ -145,6 +156,9 @@ export const packSavedObjectMappings: SavedObjectsType['mappings'] = {
interval: {
type: 'text',
},
timeout: {
type: 'short',
},
platform: {
type: 'keyword',
},
@ -166,6 +180,9 @@ export const packType: SavedObjectsType = {
hidden: false,
namespaceType: 'multiple-isolated',
mappings: packSavedObjectMappings,
modelVersions: {
1: packSavedObjectModelVersion1,
},
management: {
defaultSearchField: 'name',
importableAndExportable: true,
@ -219,6 +236,9 @@ export const packAssetSavedObjectMappings: SavedObjectsType['mappings'] = {
interval: {
type: 'text',
},
timeout: {
type: 'short',
},
platform: {
type: 'keyword',
},
@ -242,6 +262,9 @@ export const packAssetType: SavedObjectsType = {
importableAndExportable: true,
visibleInManagement: false,
},
modelVersions: {
1: packAssetSavedObjectModelVersion1,
},
namespaceType: 'agnostic',
mappings: packAssetSavedObjectMappings,
};

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { SavedObjectsModelVersion } from '@kbn/core-saved-objects-server';
export const savedQueryModelVersion1: SavedObjectsModelVersion = {
changes: [
{
type: 'mappings_addition',
addedMappings: {
timeout: { type: 'short' },
},
},
],
};
export const packSavedObjectModelVersion1: SavedObjectsModelVersion = {
changes: [
{
type: 'mappings_addition',
addedMappings: {
queries: {
properties: {
timeout: { type: 'short' },
},
},
},
},
],
};
export const packAssetSavedObjectModelVersion1: SavedObjectsModelVersion = {
changes: [
{
type: 'mappings_addition',
addedMappings: {
queries: {
properties: {
timeout: { type: 'short' },
},
},
},
},
],
};

View file

@ -21,7 +21,16 @@ export const convertPackQueriesToSO = (queries) =>
const ecsMapping = value.ecs_mapping && convertECSMappingToArray(value.ecs_mapping);
acc.push({
id: key,
...pick(value, ['name', 'query', 'interval', 'platform', 'version', 'snapshot', 'removed']),
...pick(value, [
'name',
'query',
'interval',
'platform',
'version',
'snapshot',
'removed',
'timeout',
]),
...(ecsMapping ? { ecs_mapping: ecsMapping } : {}),
});
@ -32,6 +41,7 @@ export const convertPackQueriesToSO = (queries) =>
name: string;
query: string;
interval: number;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
ecs_mapping?: Record<string, unknown>;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { isEmpty, pickBy, some, isBoolean } from 'lodash';
import { isEmpty, pickBy, some, isBoolean, isNumber } from 'lodash';
import type { IRouter } from '@kbn/core/server';
import type { CreateSavedQueryRequestSchemaDecoded } from '../../../common/api';
import { API_VERSIONS } from '../../../common/constants';
@ -50,6 +50,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
interval,
snapshot,
removed,
timeout,
// eslint-disable-next-line @typescript-eslint/naming-convention
ecs_mapping,
} = request.body;
@ -80,13 +81,14 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
interval,
snapshot,
removed,
timeout,
ecs_mapping: convertECSMappingToArray(ecs_mapping),
created_by: currentUser,
created_at: new Date().toISOString(),
updated_by: currentUser,
updated_at: new Date().toISOString(),
},
(value) => !isEmpty(value) || isBoolean(value)
(value) => !isEmpty(value) || isBoolean(value) || isNumber(value)
)
);
@ -102,6 +104,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
snapshot: attributes.snapshot,
version: attributes.version,
interval: attributes.interval,
timeout: attributes.timeout,
platform: attributes.platform,
query: attributes.query,
updated_at: attributes.updated_at,
@ -109,7 +112,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
saved_object_id: savedQuerySO.id,
ecs_mapping,
},
(value) => !isEmpty(value)
(value) => !isEmpty(value) || isNumber(value)
);
return response.ok({

View file

@ -73,6 +73,7 @@ export const findSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
description,
id,
interval,
timeout,
platform,
query,
removed,
@ -94,6 +95,7 @@ export const findSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
version,
ecs_mapping: ecsMapping,
interval,
timeout,
platform,
query,
updated_at: updatedAt,

View file

@ -64,6 +64,7 @@ export const readSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
description,
id,
interval,
timeout,
platform,
query,
removed,
@ -85,6 +86,7 @@ export const readSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
version,
ecs_mapping: ecsMapping,
interval,
timeout,
platform,
query,
updated_at: updatedAt,

View file

@ -11,6 +11,7 @@ export interface SavedQueryResponse {
description: string | undefined;
query: string;
interval: number | string;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
platform?: string;
@ -29,6 +30,7 @@ export interface UpdateSavedQueryResponse {
description: string | undefined;
query: string;
interval: number | string;
timeout?: number;
snapshot?: boolean;
removed?: boolean;
platform?: string;

View file

@ -60,6 +60,7 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
query,
version,
interval,
timeout,
snapshot,
removed,
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -102,6 +103,7 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
query,
version,
interval,
timeout,
snapshot,
removed,
ecs_mapping: convertECSMappingToArray(ecs_mapping),
@ -133,6 +135,7 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
version: attributes.version,
ecs_mapping: attributes.ecs_mapping,
interval: attributes.interval,
timeout: attributes.timeout,
platform: attributes.platform,
query: attributes.query,
updated_at: attributes.updated_at,