mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Osquery] Add Saved queries (#100965)
This commit is contained in:
parent
c05588f077
commit
ccf42c0b80
62 changed files with 1678 additions and 551 deletions
|
@ -7,10 +7,10 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const name = t.string;
|
||||
export type Name = t.TypeOf<typeof name>;
|
||||
export const nameOrUndefined = t.union([name, t.undefined]);
|
||||
export type NameOrUndefined = t.TypeOf<typeof nameOrUndefined>;
|
||||
export const id = t.string;
|
||||
export type Id = t.TypeOf<typeof id>;
|
||||
export const idOrUndefined = t.union([id, t.undefined]);
|
||||
export type IdOrUndefined = t.TypeOf<typeof idOrUndefined>;
|
||||
|
||||
export const agentSelection = t.type({
|
||||
agents: t.array(t.string),
|
||||
|
@ -18,6 +18,7 @@ export const agentSelection = t.type({
|
|||
platformsSelected: t.array(t.string),
|
||||
policiesSelected: t.array(t.string),
|
||||
});
|
||||
|
||||
export type AgentSelection = t.TypeOf<typeof agentSelection>;
|
||||
export const agentSelectionOrUndefined = t.union([agentSelection, t.undefined]);
|
||||
export type AgentSelectionOrUndefined = t.TypeOf<typeof agentSelectionOrUndefined>;
|
||||
|
@ -36,3 +37,13 @@ export const query = t.string;
|
|||
export type Query = t.TypeOf<typeof query>;
|
||||
export const queryOrUndefined = t.union([query, t.undefined]);
|
||||
export type QueryOrUndefined = t.TypeOf<typeof queryOrUndefined>;
|
||||
|
||||
export const version = t.string;
|
||||
export type Version = t.TypeOf<typeof query>;
|
||||
export const versionOrUndefined = t.union([version, t.undefined]);
|
||||
export type VersionOrUndefined = t.TypeOf<typeof versionOrUndefined>;
|
||||
|
||||
export const interval = t.string;
|
||||
export type Interval = t.TypeOf<typeof query>;
|
||||
export const intervalOrUndefined = t.union([interval, t.undefined]);
|
||||
export type IntervalOrUndefined = t.TypeOf<typeof intervalOrUndefined>;
|
||||
|
|
|
@ -7,14 +7,24 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { name, description, Description, platform, query } from '../../common/schemas';
|
||||
import {
|
||||
id,
|
||||
description,
|
||||
Description,
|
||||
platform,
|
||||
query,
|
||||
version,
|
||||
interval,
|
||||
} from '../../common/schemas';
|
||||
import { RequiredKeepUndefined } from '../../../types';
|
||||
|
||||
export const createSavedQueryRequestSchema = t.type({
|
||||
name,
|
||||
id,
|
||||
description,
|
||||
platform,
|
||||
query,
|
||||
version,
|
||||
interval,
|
||||
});
|
||||
|
||||
export type CreateSavedQueryRequestSchema = t.OutputOf<typeof createSavedQueryRequestSchema>;
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
"features",
|
||||
"fleet",
|
||||
"navigation",
|
||||
"triggersActionsUi"
|
||||
"triggersActionsUi",
|
||||
"security"
|
||||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isArray } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTable, EuiButtonIcon, EuiCodeBlock, formatDate } from '@elastic/eui';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
|
@ -54,6 +55,8 @@ const ActionsTableComponent = () => {
|
|||
|
||||
const renderAgentsColumn = useCallback((_, item) => <>{item.fields.agents?.length ?? 0}</>, []);
|
||||
|
||||
const renderCreatedByColumn = useCallback((userId) => (isArray(userId) ? userId[0] : '-'), []);
|
||||
|
||||
const renderTimestampColumn = useCallback(
|
||||
(_, item) => <>{formatDate(item.fields['@timestamp'][0])}</>,
|
||||
[]
|
||||
|
@ -90,6 +93,14 @@ const ActionsTableComponent = () => {
|
|||
width: '200px',
|
||||
render: renderTimestampColumn,
|
||||
},
|
||||
{
|
||||
field: 'fields.user_id',
|
||||
name: i18n.translate('xpack.osquery.liveQueryActions.table.createdByColumnTitle', {
|
||||
defaultMessage: 'Run by',
|
||||
}),
|
||||
width: '200px',
|
||||
render: renderCreatedByColumn,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle', {
|
||||
defaultMessage: 'View details',
|
||||
|
@ -101,7 +112,13 @@ const ActionsTableComponent = () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
[renderActionsColumn, renderAgentsColumn, renderQueryColumn, renderTimestampColumn]
|
||||
[
|
||||
renderActionsColumn,
|
||||
renderAgentsColumn,
|
||||
renderCreatedByColumn,
|
||||
renderQueryColumn,
|
||||
renderTimestampColumn,
|
||||
]
|
||||
);
|
||||
|
||||
const pagination = useMemo(
|
||||
|
|
|
@ -67,6 +67,40 @@ const breadcrumbGetters: {
|
|||
text: liveQueryId,
|
||||
},
|
||||
],
|
||||
saved_queries: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.savedQueriesPageTitle', {
|
||||
defaultMessage: 'Saved queries',
|
||||
}),
|
||||
},
|
||||
],
|
||||
saved_query_new: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
href: pagePathGetters.saved_queries(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.savedQueriesPageTitle', {
|
||||
defaultMessage: 'Saved queries',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.newSavedQueryPageTitle', {
|
||||
defaultMessage: 'New',
|
||||
}),
|
||||
},
|
||||
],
|
||||
saved_query_edit: ({ savedQueryName }) => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
href: pagePathGetters.saved_queries(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.savedQueriesPageTitle', {
|
||||
defaultMessage: 'Saved queries',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: savedQueryName,
|
||||
},
|
||||
],
|
||||
scheduled_query_groups: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
|
|
|
@ -11,12 +11,15 @@ export type StaticPage =
|
|||
| 'live_queries'
|
||||
| 'live_query_new'
|
||||
| 'scheduled_query_groups'
|
||||
| 'scheduled_query_group_add';
|
||||
| 'scheduled_query_group_add'
|
||||
| 'saved_queries'
|
||||
| 'saved_query_new';
|
||||
|
||||
export type DynamicPage =
|
||||
| 'live_query_details'
|
||||
| 'scheduled_query_group_details'
|
||||
| 'scheduled_query_group_edit';
|
||||
| 'scheduled_query_group_edit'
|
||||
| 'saved_query_edit';
|
||||
|
||||
export type Page = StaticPage | DynamicPage;
|
||||
|
||||
|
@ -50,6 +53,9 @@ export const pagePathGetters: {
|
|||
live_queries: () => '/live_queries',
|
||||
live_query_new: () => '/live_queries/new',
|
||||
live_query_details: ({ liveQueryId }) => `/live_queries/${liveQueryId}`,
|
||||
saved_queries: () => '/saved_queries',
|
||||
saved_query_new: () => '/saved_queries/new',
|
||||
saved_query_edit: ({ savedQueryId }) => `/saved_queries/${savedQueryId}`,
|
||||
scheduled_query_groups: () => '/scheduled_query_groups',
|
||||
scheduled_query_group_add: () => '/scheduled_query_groups/add',
|
||||
scheduled_query_group_details: ({ scheduledQueryGroupId }) =>
|
||||
|
|
|
@ -50,6 +50,15 @@ const OsqueryAppComponent = () => {
|
|||
defaultMessage="Scheduled query groups"
|
||||
/>
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={section === 'saved_queries'}
|
||||
{...useRouterNavigate('saved_queries')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.savedQueriesLinkText"
|
||||
defaultMessage="Saved queries"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { EuiText, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
export const OsquerySchemaLink = React.memo(() => (
|
||||
<EuiText size="xs">
|
||||
<EuiLink href="https://osquery.io/schema/4.8.0" target="_blank">
|
||||
<FormattedMessage id="xpack.osquery.osquerySchemaLinkLabel" defaultMessage="Osquery schema" />
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
));
|
||||
|
||||
OsquerySchemaLink.displayName = 'OsquerySchemaLink';
|
|
@ -40,7 +40,7 @@ const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({
|
|||
name="osquery_editor"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
editorProps={EDITOR_PROPS}
|
||||
height="200px"
|
||||
height="150px"
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -20,7 +20,7 @@ let osqueryTables: TablesJSON | null = null;
|
|||
export const getOsqueryTables = () => {
|
||||
if (!osqueryTables) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
osqueryTables = normalizeTables(require('./osquery_schema/v4.7.0.json'));
|
||||
osqueryTables = normalizeTables(require('./osquery_schema/v4.8.0.json'));
|
||||
}
|
||||
return osqueryTables;
|
||||
};
|
||||
|
|
|
@ -207,7 +207,8 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
|
|||
integrationPolicyId={policy?.id}
|
||||
agentPolicyId={policy?.policy_id}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
|
||||
{editMode && scheduledQueryGroupTableData.inputs[0].streams.length ? (
|
||||
<EuiFlexGroup>
|
||||
|
|
|
@ -5,20 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiSteps, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiSteps,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { UseField, Form, FormData, useForm, useFormData, FIELD_TYPES } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { ResultTabs } from '../../queries/edit/tabs';
|
||||
import { ResultTabs } from '../../routes/saved_queries/edit/tabs';
|
||||
import { queryFieldValidation } from '../../common/validations';
|
||||
import { fieldValidators } from '../../shared_imports';
|
||||
import { SavedQueryFlyout } from '../../saved_queries';
|
||||
import { useErrorToast } from '../../common/hooks/use_error_toast';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
|
@ -27,19 +35,17 @@ export const MAX_QUERY_LENGTH = 2000;
|
|||
|
||||
interface LiveQueryFormProps {
|
||||
defaultValue?: Partial<FormData> | undefined;
|
||||
onSubmit?: (payload: Record<string, string>) => Promise<void>;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
||||
defaultValue,
|
||||
// onSubmit,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, onSuccess }) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false);
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
const handleShowSaveQueryFlout = useCallback(() => setShowSavedQueryFlyout(true), []);
|
||||
const handleCloseSaveQueryFlout = useCallback(() => setShowSavedQueryFlyout(false), []);
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
|
@ -139,6 +145,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[queryStatus]
|
||||
);
|
||||
|
||||
const flyoutFormDefaultValue = useMemo(() => ({ query }), [query]);
|
||||
|
||||
const formSteps: EuiContainedStepProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -161,6 +169,17 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
disabled={!agentSelected || !queryValueProvided || resultsStatus === 'disabled'}
|
||||
onClick={handleShowSaveQueryFlout}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.saveForLaterButtonLabel"
|
||||
defaultMessage="Save for later"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton disabled={!agentSelected || !queryValueProvided} onClick={submit}>
|
||||
<FormattedMessage
|
||||
|
@ -193,6 +212,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
actionId,
|
||||
agentIds,
|
||||
agentSelected,
|
||||
handleShowSaveQueryFlout,
|
||||
queryComponentProps,
|
||||
queryStatus,
|
||||
queryValueProvided,
|
||||
|
@ -203,9 +223,17 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiSteps steps={formSteps} />
|
||||
</Form>
|
||||
<>
|
||||
<Form form={form}>
|
||||
<EuiSteps steps={formSteps} />
|
||||
</Form>
|
||||
{showSavedQueryFlyout ? (
|
||||
<SavedQueryFlyout
|
||||
onClose={handleCloseSaveQueryFlout}
|
||||
defaultValue={flyoutFormDefaultValue}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { OsquerySchemaLink } from '../../components/osquery_schema_link';
|
||||
import { FieldHook } from '../../shared_imports';
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
|
||||
interface LiveQueryQueryFieldProps {
|
||||
disabled?: boolean;
|
||||
|
@ -20,6 +22,13 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disa
|
|||
const { value, setValue, errors } = field;
|
||||
const error = errors[0]?.message;
|
||||
|
||||
const handleSavedQueryChange = useCallback(
|
||||
(savedQuery) => {
|
||||
setValue(savedQuery.query);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(newValue) => {
|
||||
setValue(newValue);
|
||||
|
@ -29,7 +38,13 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disa
|
|||
|
||||
return (
|
||||
<EuiFormRow isInvalid={typeof error === 'string'} error={error} fullWidth>
|
||||
<OsqueryEditor defaultValue={value} disabled={disabled} onChange={handleEditorChange} />
|
||||
<>
|
||||
<SavedQueriesDropdown disabled={disabled} onChange={handleSavedQueryChange} />
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth labelAppend={<OsquerySchemaLink />}>
|
||||
<OsqueryEditor defaultValue={value} disabled={disabled} onChange={handleEditorChange} />
|
||||
</EuiFormRow>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { SavedQueryForm } from '../../queries/form';
|
||||
import { SavedQueryForm } from '../../saved_queries/form';
|
||||
|
||||
// @ts-expect-error update types
|
||||
const AddNewPackQueryFlyoutComponent = ({ handleClose, handleSubmit }) => (
|
||||
|
@ -19,7 +19,10 @@ const AddNewPackQueryFlyoutComponent = ({ handleClose, handleSubmit }) => (
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<SavedQueryForm handleSubmit={handleSubmit} />
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
<SavedQueryForm handleSubmit={handleSubmit} />
|
||||
}
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
|
|
@ -1,53 +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 { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { SavedQueryForm } from '../form';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface EditSavedQueryPageProps {
|
||||
onSuccess: () => void;
|
||||
savedQueryId: string;
|
||||
}
|
||||
|
||||
const EditSavedQueryPageComponent: React.FC<EditSavedQueryPageProps> = ({
|
||||
onSuccess,
|
||||
savedQueryId,
|
||||
}) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { isLoading, data: savedQueryDetails } = useQuery(['savedQuery', { savedQueryId }], () =>
|
||||
http.get(`/internal/osquery/saved_query/${savedQueryId}`)
|
||||
);
|
||||
const updateSavedQueryMutation = useMutation(
|
||||
(payload) =>
|
||||
http.put(`/internal/osquery/saved_query/${savedQueryId}`, { body: JSON.stringify(payload) }),
|
||||
{ onSuccess }
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <>{'Loading...'}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isEmpty(savedQueryDetails) && (
|
||||
<SavedQueryForm
|
||||
defaultValue={savedQueryDetails}
|
||||
// @ts-expect-error update types
|
||||
handleSubmit={updateSavedQueryMutation.mutate}
|
||||
type="edit"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditSavedQueryPage = React.memo(EditSavedQueryPageComponent);
|
|
@ -1,72 +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 { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { Field, getUseField, useForm, UseField, Form } from '../../shared_imports';
|
||||
import { CodeEditorField } from './code_editor_field';
|
||||
import { formSchema } from './schema';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const SAVED_QUERY_FORM_ID = 'savedQueryForm';
|
||||
|
||||
interface SavedQueryFormProps {
|
||||
defaultValue?: unknown;
|
||||
handleSubmit: () => Promise<void>;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
type,
|
||||
}) => {
|
||||
const { form } = useForm({
|
||||
// @ts-expect-error update types
|
||||
id: defaultValue ? SAVED_QUERY_FORM_ID + defaultValue.id : SAVED_QUERY_FORM_ID,
|
||||
schema: formSchema,
|
||||
onSubmit: handleSubmit,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
defaultValue,
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="name" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField
|
||||
path="platform"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
options: [
|
||||
{ value: 'darwin', text: 'macOS' },
|
||||
{ value: 'freebsd', text: 'FreeBSD' },
|
||||
{ value: 'linux', text: 'Linux' },
|
||||
{ value: 'posix', text: 'Posix' },
|
||||
{ value: 'windows', text: 'Windows' },
|
||||
{ value: 'all', text: 'All' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<UseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
<EuiButton onClick={submit}>{type === 'edit' ? 'Update' : 'Save'}</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const SavedQueryForm = React.memo(SavedQueryFormComponent);
|
|
@ -1,30 +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 { FIELD_TYPES, FormSchema } from '../../shared_imports';
|
||||
|
||||
export const formSchema: FormSchema = {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Query name',
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXTAREA,
|
||||
label: 'Description',
|
||||
validations: [],
|
||||
},
|
||||
platform: {
|
||||
type: FIELD_TYPES.SELECT,
|
||||
label: 'Platform',
|
||||
defaultValue: 'all',
|
||||
},
|
||||
query: {
|
||||
label: 'Query',
|
||||
type: FIELD_TYPES.TEXTAREA,
|
||||
validations: [],
|
||||
},
|
||||
};
|
|
@ -1,36 +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 React, { useCallback, useState } from 'react';
|
||||
|
||||
import { QueriesPage } from './queries';
|
||||
import { NewSavedQueryPage } from './new';
|
||||
import { EditSavedQueryPage } from './edit';
|
||||
|
||||
const QueriesComponent = () => {
|
||||
const [showNewSavedQueryForm, setShowNewSavedQueryForm] = useState(false);
|
||||
const [editSavedQueryId, setEditSavedQueryId] = useState<string | null>(null);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
setShowNewSavedQueryForm(false);
|
||||
setEditSavedQueryId(null);
|
||||
}, []);
|
||||
|
||||
const handleNewQueryClick = useCallback(() => setShowNewSavedQueryForm(true), []);
|
||||
|
||||
if (showNewSavedQueryForm) {
|
||||
return <NewSavedQueryPage onSuccess={goBack} />;
|
||||
}
|
||||
|
||||
if (editSavedQueryId?.length) {
|
||||
return <EditSavedQueryPage onSuccess={goBack} savedQueryId={editSavedQueryId} />;
|
||||
}
|
||||
|
||||
return <QueriesPage onNewClick={handleNewQueryClick} onEditClick={setEditSavedQueryId} />;
|
||||
};
|
||||
|
||||
export const Queries = React.memo(QueriesComponent);
|
|
@ -1,32 +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 React from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { SavedQueryForm } from '../form';
|
||||
|
||||
interface NewSavedQueryPageProps {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const NewSavedQueryPageComponent: React.FC<NewSavedQueryPageProps> = ({ onSuccess }) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const createSavedQueryMutation = useMutation(
|
||||
(payload) => http.post(`/internal/osquery/saved_query`, { body: JSON.stringify(payload) }),
|
||||
{
|
||||
onSuccess,
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error update types
|
||||
return <SavedQueryForm handleSubmit={createSavedQueryMutation.mutate} />;
|
||||
};
|
||||
|
||||
export const NewSavedQueryPage = React.memo(NewSavedQueryPageComponent);
|
|
@ -1,244 +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 { map } from 'lodash/fp';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery, useQueryClient, useMutation } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface QueriesPageProps {
|
||||
onEditClick: (savedQueryId: string) => void;
|
||||
onNewClick: () => void;
|
||||
}
|
||||
|
||||
const QueriesPageComponent: React.FC<QueriesPageProps> = ({ onEditClick, onNewClick }) => {
|
||||
const { push } = useHistory();
|
||||
const queryClient = useQueryClient();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [sortField, setSortField] = useState('updated_at');
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, unknown>>({});
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const deleteSavedQueriesMutation = useMutation(
|
||||
(payload) => http.delete(`/internal/osquery/saved_query`, { body: JSON.stringify(payload) }),
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries('savedQueryList'),
|
||||
}
|
||||
);
|
||||
|
||||
const { data = {} } = useQuery(
|
||||
['savedQueryList', { pageIndex, pageSize, sortField, sortDirection }],
|
||||
() =>
|
||||
http.get('/internal/osquery/saved_query', {
|
||||
query: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
},
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
// Refetch the data every 10 seconds
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
);
|
||||
const { total = 0, saved_objects: savedQueries } = data;
|
||||
|
||||
const toggleDetails = useCallback(
|
||||
(item) => () => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
itemIdToExpandedRowMapValues[item.id] = (
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{item.attributes.query}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
},
|
||||
[itemIdToExpandedRowMap]
|
||||
);
|
||||
|
||||
const renderExtendedItemToggle = useCallback(
|
||||
(item) => (
|
||||
<EuiButtonIcon
|
||||
onClick={toggleDetails(item)}
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
[itemIdToExpandedRowMap, toggleDetails]
|
||||
);
|
||||
|
||||
const handleEditClick = useCallback((item) => onEditClick(item.id), [onEditClick]);
|
||||
|
||||
const handlePlayClick = useCallback(
|
||||
(item) =>
|
||||
push({
|
||||
search: qs.stringify({
|
||||
tab: 'live_query',
|
||||
}),
|
||||
state: {
|
||||
query: {
|
||||
id: item.id,
|
||||
query: item.attributes.query,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[push]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'attributes.name',
|
||||
name: 'Query name',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'attributes.description',
|
||||
name: 'Description',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'updated_at',
|
||||
name: 'Last updated at',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Live query',
|
||||
description: 'Run live query',
|
||||
type: 'icon',
|
||||
icon: 'play',
|
||||
onClick: handlePlayClick,
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
description: 'Edit or run this query',
|
||||
type: 'icon',
|
||||
icon: 'documentEdit',
|
||||
onClick: handleEditClick,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: renderExtendedItemToggle,
|
||||
},
|
||||
],
|
||||
[handleEditClick, handlePlayClick, renderExtendedItemToggle]
|
||||
);
|
||||
|
||||
const onTableChange = useCallback(({ page = {}, sort = {} }) => {
|
||||
setPageIndex(page.index);
|
||||
setPageSize(page.size);
|
||||
setSortField(sort.field);
|
||||
setSortDirection(sort.direction);
|
||||
}, []);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: total,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
}),
|
||||
[total, pageIndex, pageSize]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
}),
|
||||
[sortDirection, sortField]
|
||||
);
|
||||
|
||||
const selection = useMemo(
|
||||
() => ({
|
||||
selectable: () => true,
|
||||
onSelectionChange: setSelectedItems,
|
||||
initialSelected: [],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
const selectedItemsIds = map<string>('id', selectedItems);
|
||||
// @ts-expect-error update types
|
||||
deleteSavedQueriesMutation.mutate({ savedQueryIds: selectedItemsIds });
|
||||
}, [deleteSavedQueriesMutation, selectedItems]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
{!selectedItems.length ? (
|
||||
<EuiButton fill onClick={onNewClick}>
|
||||
{'New query'}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton color="danger" iconType="trash" onClick={handleDeleteClick}>
|
||||
{`Delete ${selectedItems.length} Queries`}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{savedQueries && (
|
||||
<EuiBasicTable
|
||||
items={savedQueries}
|
||||
itemId="id"
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
// @ts-expect-error update types
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
selection={selection}
|
||||
onChange={onTableChange}
|
||||
// @ts-expect-error update types
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
rowHeader="id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueriesPage = React.memo(QueriesPageComponent);
|
|
@ -11,12 +11,16 @@ import { Switch, Redirect, Route } from 'react-router-dom';
|
|||
import { useBreadcrumbs } from '../common/hooks/use_breadcrumbs';
|
||||
import { LiveQueries } from './live_queries';
|
||||
import { ScheduledQueryGroups } from './scheduled_query_groups';
|
||||
import { SavedQueries } from './saved_queries';
|
||||
|
||||
const OsqueryAppRoutesComponent = () => {
|
||||
useBreadcrumbs('base');
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`/saved_queries`}>
|
||||
<SavedQueries />
|
||||
</Route>
|
||||
<Route path={`/scheduled_query_groups`}>
|
||||
<ScheduledQueryGroups />
|
||||
</Route>
|
||||
|
|
|
@ -27,7 +27,7 @@ import { useRouterNavigate } from '../../../common/lib/kibana';
|
|||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useActionResults } from '../../../action_results/use_action_results';
|
||||
import { useActionDetails } from '../../../actions/use_action_details';
|
||||
import { ResultTabs } from '../../../queries/edit/tabs';
|
||||
import { ResultTabs } from '../../saved_queries/edit/tabs';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBottomBar,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { Form } from '../../../shared_imports';
|
||||
import { SavedQueryForm } from '../../../saved_queries/form';
|
||||
import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_form';
|
||||
|
||||
interface EditSavedQueryFormProps {
|
||||
defaultValue?: unknown;
|
||||
handleSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
}) => {
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<SavedQueryForm />
|
||||
<EuiBottomBar>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty color="ghost" {...savedQueryListProps}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editSavedQuery.form.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
// isLoading={isLoading}
|
||||
color="primary"
|
||||
fill
|
||||
size="m"
|
||||
iconType="save"
|
||||
onClick={form.submit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editSavedQuery.form.updateQueryButtonLabel"
|
||||
defaultMessage="Update query"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiBottomBar>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditSavedQueryForm = React.memo(EditSavedQueryFormComponent);
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiConfirmModal,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
import { EditSavedQueryForm } from './form';
|
||||
import { useDeleteSavedQuery, useUpdateSavedQuery, useSavedQuery } from '../../../saved_queries';
|
||||
|
||||
const EditSavedQueryPageComponent = () => {
|
||||
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||
const { savedQueryId } = useParams<{ savedQueryId: string }>();
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const { isLoading, data: savedQueryDetails } = useSavedQuery({ savedQueryId });
|
||||
const updateSavedQueryMutation = useUpdateSavedQuery({ savedQueryId });
|
||||
const deleteSavedQueryMutation = useDeleteSavedQuery({ savedQueryId });
|
||||
|
||||
useBreadcrumbs('saved_query_edit', { savedQueryId: savedQueryDetails?.attributes?.id ?? '' });
|
||||
|
||||
const handleCloseDeleteConfirmationModal = useCallback(() => {
|
||||
setIsDeleteModalVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
setIsDeleteModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteConfirmClick = useCallback(() => {
|
||||
deleteSavedQueryMutation.mutateAsync().then(() => {
|
||||
handleCloseDeleteConfirmationModal();
|
||||
});
|
||||
}, [deleteSavedQueryMutation, handleCloseDeleteConfirmationModal]);
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...savedQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editSavedQuery.viewSavedQueriesListTitle"
|
||||
defaultMessage="View all saved queries"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editSavedQuery.pageTitle"
|
||||
defaultMessage='Edit "{savedQueryId}"'
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{
|
||||
savedQueryId: savedQueryDetails?.attributes?.id ?? '',
|
||||
}}
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[savedQueryDetails?.attributes?.id, savedQueryListProps]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiButton color="danger" onClick={handleDeleteClick} iconType="trash">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editSavedQuery.deleteSavedQueryButtonLabel"
|
||||
defaultMessage="Delete query"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
[handleDeleteClick]
|
||||
);
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
{!isLoading && !isEmpty(savedQueryDetails) && (
|
||||
<EditSavedQueryForm
|
||||
defaultValue={savedQueryDetails?.attributes}
|
||||
// @ts-expect-error update types
|
||||
handleSubmit={updateSavedQueryMutation.mutateAsync}
|
||||
/>
|
||||
)}
|
||||
{isDeleteModalVisible ? (
|
||||
<EuiConfirmModal
|
||||
title={`Are you sure you want to delete this query?`}
|
||||
onCancel={handleCloseDeleteConfirmationModal}
|
||||
onConfirm={handleDeleteConfirmClick}
|
||||
cancelButtonText="No, don't do it"
|
||||
confirmButtonText="Yes, do it"
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<p>You’re about to delete this query.</p>
|
||||
<p>Are you sure you want to do this?</p>
|
||||
</EuiConfirmModal>
|
||||
) : null}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditSavedQueryPage = React.memo(EditSavedQueryPageComponent);
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { EuiTabbedContent, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { ResultsTable } from '../../../results/results_table';
|
||||
import { ActionResultsSummary } from '../../../action_results/action_results_summary';
|
||||
|
||||
interface ResultTabsProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
expirationDate: Date;
|
||||
isLive?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
endDate,
|
||||
expirationDate,
|
||||
isLive,
|
||||
startDate,
|
||||
}) => {
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'results',
|
||||
name: 'Results',
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ResultsTable
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
isLive={isLive}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
name: 'Status',
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ActionResultsSummary
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
expirationDate={expirationDate}
|
||||
isLive={isLive}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[actionId, agentIds, endDate, expirationDate, isLive, startDate]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
expand={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResultTabs = React.memo(ResultTabsComponent);
|
35
x-pack/plugins/osquery/public/routes/saved_queries/index.tsx
Normal file
35
x-pack/plugins/osquery/public/routes/saved_queries/index.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { QueriesPage } from './list';
|
||||
import { NewSavedQueryPage } from './new';
|
||||
import { EditSavedQueryPage } from './edit';
|
||||
import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs';
|
||||
|
||||
const SavedQueriesComponent = () => {
|
||||
useBreadcrumbs('saved_queries');
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/new`}>
|
||||
<NewSavedQueryPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/:savedQueryId`}>
|
||||
<EditSavedQueryPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}`}>
|
||||
<QueriesPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export const SavedQueries = React.memo(SavedQueriesComponent);
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { SavedObject } from 'kibana/public';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
import { useSavedQueries } from '../../../saved_queries/use_saved_queries';
|
||||
|
||||
interface EditButtonProps {
|
||||
savedQueryId: string;
|
||||
savedQueryName: string;
|
||||
}
|
||||
|
||||
const EditButtonComponent: React.FC<EditButtonProps> = ({ savedQueryId, savedQueryName }) => {
|
||||
const buttonProps = useRouterNavigate(`saved_queries/${savedQueryId}`);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
color="primary"
|
||||
{...buttonProps}
|
||||
iconType="pencil"
|
||||
aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.editActionAriaLabel', {
|
||||
defaultMessage: 'Edit {savedQueryName}',
|
||||
values: {
|
||||
savedQueryName,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const EditButton = React.memo(EditButtonComponent);
|
||||
|
||||
const SavedQueriesPageComponent = () => {
|
||||
useBreadcrumbs('saved_queries');
|
||||
const newQueryLinkProps = useRouterNavigate('saved_queries/new');
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [sortField, setSortField] = useState('updated_at');
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
|
||||
const { data } = useSavedQueries({ isLive: true });
|
||||
|
||||
// const handlePlayClick = useCallback(
|
||||
// (item) =>
|
||||
// push({
|
||||
// search: qs.stringify({
|
||||
// tab: 'live_query',
|
||||
// }),
|
||||
// state: {
|
||||
// query: {
|
||||
// id: item.id,
|
||||
// query: item.attributes.query,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// [push]
|
||||
// );
|
||||
|
||||
const renderEditAction = useCallback(
|
||||
(item: SavedObject<{ name: string }>) => (
|
||||
<EditButton savedQueryId={item.id} savedQueryName={item.attributes.name} />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderUpdatedAt = useCallback((updatedAt, item) => {
|
||||
if (!updatedAt) return '-';
|
||||
|
||||
const updatedBy =
|
||||
item.attributes.updated_by !== item.attributes.created_by
|
||||
? ` @ ${item.attributes.updated_by}`
|
||||
: '';
|
||||
return updatedAt ? `${moment(updatedAt).fromNow()}${updatedBy}` : '-';
|
||||
}, []);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'attributes.id',
|
||||
name: 'Query ID',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'attributes.description',
|
||||
name: 'Description',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'attributes.created_by',
|
||||
name: 'Created by',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'attributes.updated_at',
|
||||
name: 'Last updated at',
|
||||
sortable: (item: SavedObject<{ updated_at: string }>) =>
|
||||
item.attributes.updated_at ? Date.parse(item.attributes.updated_at) : 0,
|
||||
truncateText: true,
|
||||
render: renderUpdatedAt,
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
// {
|
||||
// name: 'Live query',
|
||||
// description: 'Run live query',
|
||||
// type: 'icon',
|
||||
// icon: 'play',
|
||||
// onClick: handlePlayClick,
|
||||
// },
|
||||
{ render: renderEditAction },
|
||||
],
|
||||
},
|
||||
],
|
||||
[renderEditAction, renderUpdatedAt]
|
||||
);
|
||||
|
||||
const onTableChange = useCallback(({ page = {}, sort = {} }) => {
|
||||
setPageIndex(page.index);
|
||||
setPageSize(page.size);
|
||||
setSortField(sort.field);
|
||||
setSortDirection(sort.direction);
|
||||
}, []);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: data?.total ?? 0,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
}),
|
||||
[pageIndex, pageSize, data?.total]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
}),
|
||||
[sortDirection, sortField]
|
||||
);
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueryList.pageTitle"
|
||||
defaultMessage="Saved queries"
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiButton fill {...newQueryLinkProps} iconType="plusInCircle">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueryList.addSavedQueryButtonLabel"
|
||||
defaultMessage="Add saved query"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
[newQueryLinkProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
{data?.savedObjects && (
|
||||
<EuiInMemoryTable
|
||||
items={data?.savedObjects}
|
||||
itemId="id"
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
// @ts-expect-error update types
|
||||
sorting={sorting}
|
||||
onChange={onTableChange}
|
||||
rowHeader="id"
|
||||
/>
|
||||
)}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueriesPage = React.memo(SavedQueriesPageComponent);
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBottomBar,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { Form } from '../../../shared_imports';
|
||||
import { SavedQueryForm } from '../../../saved_queries/form';
|
||||
import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_form';
|
||||
|
||||
interface NewSavedQueryFormProps {
|
||||
defaultValue?: unknown;
|
||||
handleSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const NewSavedQueryFormComponent: React.FC<NewSavedQueryFormProps> = ({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
}) => {
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<SavedQueryForm />
|
||||
<EuiBottomBar>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty color="ghost" {...savedQueryListProps}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.addSavedQuery.form.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
// isLoading={isLoading}
|
||||
color="primary"
|
||||
fill
|
||||
size="m"
|
||||
iconType="save"
|
||||
onClick={form.submit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.addSavedQuery.form.saveQueryButtonLabel"
|
||||
defaultMessage="Save query"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiBottomBar>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewSavedQueryForm = React.memo(NewSavedQueryFormComponent);
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
import { NewSavedQueryForm } from './form';
|
||||
import { useCreateSavedQuery } from '../../../saved_queries/use_create_saved_query';
|
||||
|
||||
const NewSavedQueryPageComponent = () => {
|
||||
useBreadcrumbs('saved_query_new');
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const createSavedQueryMutation = useCreateSavedQuery({ withRedirect: true });
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...savedQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.addSavedQuery.viewSavedQueriesListTitle"
|
||||
defaultMessage="View all saved queries"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.addSavedQuery.pageTitle"
|
||||
defaultMessage="Add saved query"
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[savedQueryListProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn}>
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
<NewSavedQueryForm handleSubmit={createSavedQueryMutation.mutateAsync} />
|
||||
}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewSavedQueryPage = React.memo(NewSavedQueryPageComponent);
|
8
x-pack/plugins/osquery/public/saved_queries/constants.ts
Normal file
8
x-pack/plugins/osquery/public/saved_queries/constants.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const SAVED_QUERIES_ID = 'savedQueryList';
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { EuiFormRow, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { OsquerySchemaLink } from '../../components/osquery_schema_link';
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
import { FieldHook } from '../../shared_imports';
|
||||
|
||||
|
@ -17,19 +17,6 @@ interface CodeEditorFieldProps {
|
|||
field: FieldHook<string>;
|
||||
}
|
||||
|
||||
const OsquerySchemaLink = React.memo(() => (
|
||||
<EuiText size="xs">
|
||||
<EuiLink href="https://osquery.io/schema/4.7.0" target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.codeEditorField.osquerySchemaLinkLabel"
|
||||
defaultMessage="Osquery schema"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
));
|
||||
|
||||
OsquerySchemaLink.displayName = 'OsquerySchemaLink';
|
||||
|
||||
const CodeEditorFieldComponent: React.FC<CodeEditorFieldProps> = ({ field }) => {
|
||||
const { value, label, labelAppend, helpText, setValue, errors } = field;
|
||||
const error = errors[0]?.message;
|
74
x-pack/plugins/osquery/public/saved_queries/form/index.tsx
Normal file
74
x-pack/plugins/osquery/public/saved_queries/form/index.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../scheduled_query_groups/queries/constants';
|
||||
import { PlatformCheckBoxGroupField } from '../../scheduled_query_groups/queries/platform_checkbox_group_field';
|
||||
import { Field, getUseField, UseField } from '../../shared_imports';
|
||||
import { CodeEditorField } from './code_editor_field';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const SavedQueryFormComponent = () => (
|
||||
<>
|
||||
<CommonUseField path="id" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" />
|
||||
<EuiSpacer />
|
||||
<UseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueries.form.scheduledQueryGroupConfigSection.title"
|
||||
defaultMessage="Scheduled query group configuration"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueries.form.scheduledQueryGroupConfigSection.description"
|
||||
defaultMessage="The options listed below are optional and are only applied when the query is assigned to a scheduled query group."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="interval"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{ append: 's' }}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<CommonUseField
|
||||
path="version"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
placeholder: ALL_OSQUERY_VERSIONS_OPTIONS[0].label,
|
||||
options: ALL_OSQUERY_VERSIONS_OPTIONS,
|
||||
onCreateOption: undefined,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField path="platform" component={PlatformCheckBoxGroupField} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
|
||||
export const SavedQueryForm = React.memo(SavedQueryFormComponent);
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isArray } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { useForm } from '../../shared_imports';
|
||||
import { formSchema } from '../../scheduled_query_groups/queries/schema';
|
||||
import { ScheduledQueryGroupFormData } from '../../scheduled_query_groups/queries/use_scheduled_query_group_query_form';
|
||||
|
||||
const SAVED_QUERY_FORM_ID = 'savedQueryForm';
|
||||
|
||||
interface UseSavedQueryFormProps {
|
||||
defaultValue?: unknown;
|
||||
handleSubmit: (payload: unknown) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryFormProps) =>
|
||||
useForm({
|
||||
id: SAVED_QUERY_FORM_ID + uuid.v4(),
|
||||
schema: formSchema,
|
||||
onSubmit: handleSubmit,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
defaultValue,
|
||||
serializer: (payload) =>
|
||||
produce(payload, (draft) => {
|
||||
// @ts-expect-error update types
|
||||
if (draft.platform?.split(',').length === 3) {
|
||||
// if all platforms are checked then use undefined
|
||||
// @ts-expect-error update types
|
||||
delete draft.platform;
|
||||
}
|
||||
if (isArray(draft.version)) {
|
||||
if (!draft.version.length) {
|
||||
// @ts-expect-error update types
|
||||
delete draft.version;
|
||||
} else {
|
||||
draft.version = draft.version[0];
|
||||
}
|
||||
}
|
||||
return draft;
|
||||
}),
|
||||
// @ts-expect-error update types
|
||||
deserializer: (payload) => {
|
||||
if (!payload) return {} as ScheduledQueryGroupFormData;
|
||||
|
||||
return {
|
||||
id: payload.id,
|
||||
description: payload.description,
|
||||
query: payload.query,
|
||||
interval: payload.interval ? parseInt(payload.interval, 10) : undefined,
|
||||
platform: payload.platform,
|
||||
version: payload.version ? [payload.version] : [],
|
||||
};
|
||||
},
|
||||
});
|
12
x-pack/plugins/osquery/public/saved_queries/index.tsx
Normal file
12
x-pack/plugins/osquery/public/saved_queries/index.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './saved_query_flyout';
|
||||
export * from './use_saved_query';
|
||||
export * from './use_saved_queries';
|
||||
export * from './use_update_saved_query';
|
||||
export * from './use_delete_saved_query';
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { find } from 'lodash/fp';
|
||||
import { EuiCodeBlock, EuiFormRow, EuiComboBox, EuiText } from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { SimpleSavedObject } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { useSavedQueries } from './use_saved_queries';
|
||||
|
||||
interface SavedQueriesDropdownProps {
|
||||
disabled?: boolean;
|
||||
onChange: (
|
||||
value: SimpleSavedObject<{
|
||||
id: string;
|
||||
description?: string | undefined;
|
||||
query: string;
|
||||
}>['attributes']
|
||||
) => void;
|
||||
}
|
||||
|
||||
const SavedQueriesDropdownComponent: React.FC<SavedQueriesDropdownProps> = ({
|
||||
disabled,
|
||||
onChange,
|
||||
}) => {
|
||||
const [selectedOptions, setSelectedOptions] = useState([]);
|
||||
|
||||
const { data } = useSavedQueries({});
|
||||
|
||||
const queryOptions =
|
||||
data?.savedObjects?.map((savedQuery) => ({
|
||||
label: savedQuery.attributes.id ?? '',
|
||||
value: {
|
||||
id: savedQuery.attributes.id,
|
||||
description: savedQuery.attributes.description,
|
||||
query: savedQuery.attributes.query,
|
||||
},
|
||||
})) ?? [];
|
||||
|
||||
const handleSavedQueryChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
const selectedSavedQuery = find(
|
||||
['attributes.id', newSelectedOptions[0].value.id],
|
||||
data?.savedObjects
|
||||
);
|
||||
|
||||
if (selectedSavedQuery) {
|
||||
onChange(selectedSavedQuery.attributes);
|
||||
}
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
},
|
||||
[data?.savedObjects, onChange]
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
({ value }) => (
|
||||
<>
|
||||
<strong>{value.id}</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">{value.description}</p>
|
||||
</EuiText>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="s">
|
||||
{value.query}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueries.dropdown.searchFieldLabel"
|
||||
defaultMessage="Build from a saved query (optional)"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
isDisabled={disabled}
|
||||
fullWidth
|
||||
placeholder={i18n.translate('xpack.osquery.savedQueries.dropdown.searchFieldPlaceholder', {
|
||||
defaultMessage: 'Search for saved queries',
|
||||
})}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={queryOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleSavedQueryChange}
|
||||
renderOption={renderOption}
|
||||
rowHeight={90}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const SavedQueriesDropdown = React.memo(SavedQueriesDropdownComponent);
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlyout,
|
||||
EuiTitle,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiPortal,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { Form } from '../shared_imports';
|
||||
import { useSavedQueryForm } from './form/use_saved_query_form';
|
||||
import { SavedQueryForm } from './form';
|
||||
import { useCreateSavedQuery } from './use_create_saved_query';
|
||||
|
||||
interface AddQueryFlyoutProps {
|
||||
defaultValue: unknown;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({ defaultValue, onClose }) => {
|
||||
const createSavedQueryMutation = useCreateSavedQuery({ withRedirect: false });
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(payload) => createSavedQueryMutation.mutateAsync(payload).then(() => onClose()),
|
||||
[createSavedQueryMutation, onClose]
|
||||
);
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout size="m" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQuery.saveQueryFlyoutForm.addFormTitle"
|
||||
defaultMessage="Save query"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
<SavedQueryForm />
|
||||
</Form>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={form.submit} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
||||
|
||||
export const SavedQueryFlyout = React.memo(SavedQueryFlyoutComponent);
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { useMutation, useQueryClient } from 'react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { savedQuerySavedObjectType } from '../../common/types';
|
||||
import { PLUGIN_ID } from '../../common';
|
||||
import { pagePathGetters } from '../common/page_paths';
|
||||
import { SAVED_QUERIES_ID } from './constants';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
||||
interface UseCreateSavedQueryProps {
|
||||
withRedirect?: boolean;
|
||||
}
|
||||
|
||||
export const useCreateSavedQuery = ({ withRedirect }: UseCreateSavedQueryProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
application: { navigateToApp },
|
||||
savedObjects,
|
||||
security,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useMutation(
|
||||
async (payload) => {
|
||||
const currentUser = await security.authc.getCurrentUser();
|
||||
|
||||
if (!currentUser) {
|
||||
throw new Error('CurrentUser is missing');
|
||||
}
|
||||
|
||||
return savedObjects.client.create(savedQuerySavedObjectType, {
|
||||
// @ts-expect-error update types
|
||||
...payload,
|
||||
created_by: currentUser.username,
|
||||
created_at: new Date(Date.now()).toISOString(),
|
||||
updated_by: currentUser.username,
|
||||
updated_at: new Date(Date.now()).toISOString(),
|
||||
});
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
// @ts-expect-error update types
|
||||
setErrorToast(error, { title: error.body.error, toastMessage: error.body.message });
|
||||
},
|
||||
onSuccess: (payload) => {
|
||||
queryClient.invalidateQueries(SAVED_QUERIES_ID);
|
||||
if (withRedirect) {
|
||||
navigateToApp(PLUGIN_ID, { path: pagePathGetters.saved_queries() });
|
||||
}
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.osquery.newSavedQuery.successToastMessageText', {
|
||||
defaultMessage: 'Successfully saved "{savedQueryId}" query',
|
||||
values: {
|
||||
savedQueryId: payload.attributes?.id ?? '',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { useMutation, useQueryClient } from 'react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { savedQuerySavedObjectType } from '../../common/types';
|
||||
import { PLUGIN_ID } from '../../common';
|
||||
import { pagePathGetters } from '../common/page_paths';
|
||||
import { SAVED_QUERIES_ID } from './constants';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
||||
interface UseDeleteSavedQueryProps {
|
||||
savedQueryId: string;
|
||||
}
|
||||
|
||||
export const useDeleteSavedQuery = ({ savedQueryId }: UseDeleteSavedQueryProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
application: { navigateToApp },
|
||||
savedObjects,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useMutation(() => savedObjects.client.delete(savedQuerySavedObjectType, savedQueryId), {
|
||||
onError: (error) => {
|
||||
// @ts-expect-error update types
|
||||
setErrorToast(error, { title: error.body.error, toastMessage: error.body.message });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(SAVED_QUERIES_ID);
|
||||
navigateToApp(PLUGIN_ID, { path: pagePathGetters.saved_queries() });
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.osquery.editSavedQuery.deleteSuccessToastMessageText', {
|
||||
defaultMessage: 'Successfully deleted saved query',
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../common/lib/kibana';
|
||||
import { savedQuerySavedObjectType } from '../../common/types';
|
||||
import { SAVED_QUERIES_ID } from './constants';
|
||||
|
||||
export const useSavedQueries = ({
|
||||
isLive = false,
|
||||
pageIndex = 0,
|
||||
pageSize = 10000,
|
||||
sortField = 'updated_at',
|
||||
sortDirection = 'desc',
|
||||
}) => {
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
[SAVED_QUERIES_ID, { pageIndex, pageSize, sortField, sortDirection }],
|
||||
async () =>
|
||||
savedObjects.client.find<{
|
||||
id: string;
|
||||
description?: string;
|
||||
query: string;
|
||||
updated_at: string;
|
||||
updated_by: string;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
}>({
|
||||
type: savedQuerySavedObjectType,
|
||||
page: pageIndex + 1,
|
||||
perPage: pageSize,
|
||||
sortField,
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
// Refetch the data every 10 seconds
|
||||
refetchInterval: isLive ? 10000 : false,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { PLUGIN_ID } from '../../common';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { savedQuerySavedObjectType } from '../../common/types';
|
||||
import { pagePathGetters } from '../common/page_paths';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
||||
export const SAVED_QUERY_ID = 'savedQuery';
|
||||
|
||||
interface UseSavedQueryProps {
|
||||
savedQueryId: string;
|
||||
}
|
||||
|
||||
export const useSavedQuery = ({ savedQueryId }: UseSavedQueryProps) => {
|
||||
const {
|
||||
application: { navigateToApp },
|
||||
savedObjects,
|
||||
} = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useQuery(
|
||||
[SAVED_QUERY_ID, { savedQueryId }],
|
||||
async () =>
|
||||
savedObjects.client.get<{
|
||||
id: string;
|
||||
description?: string;
|
||||
query: string;
|
||||
}>(savedQuerySavedObjectType, savedQueryId),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
setErrorToast(data.error, {
|
||||
title: data.error.error,
|
||||
toastMessage: data.error.message,
|
||||
});
|
||||
navigateToApp(PLUGIN_ID, { path: pagePathGetters.saved_queries() });
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
// @ts-expect-error update types
|
||||
setErrorToast(error, { title: error.body.error, toastMessage: error.body.message });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../common/lib/kibana';
|
||||
import { GetOnePackagePolicyResponse, packagePolicyRouteService } from '../../../fleet/common';
|
||||
import { OsqueryManagerPackagePolicy } from '../../common/types';
|
||||
|
||||
interface UseScheduledQueryGroup {
|
||||
scheduledQueryGroupId: string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useScheduledQueryGroup = ({
|
||||
scheduledQueryGroupId,
|
||||
skip = false,
|
||||
}: UseScheduledQueryGroup) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery<
|
||||
Omit<GetOnePackagePolicyResponse, 'item'> & { item: OsqueryManagerPackagePolicy },
|
||||
unknown,
|
||||
OsqueryManagerPackagePolicy
|
||||
>(
|
||||
['scheduledQueryGroup', { scheduledQueryGroupId }],
|
||||
() => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !skip,
|
||||
select: (response) => response.item,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { useMutation, useQueryClient } from 'react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { savedQuerySavedObjectType } from '../../common/types';
|
||||
import { PLUGIN_ID } from '../../common';
|
||||
import { pagePathGetters } from '../common/page_paths';
|
||||
import { SAVED_QUERIES_ID } from './constants';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
||||
interface UseUpdateSavedQueryProps {
|
||||
savedQueryId: string;
|
||||
}
|
||||
|
||||
export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
application: { navigateToApp },
|
||||
savedObjects,
|
||||
security,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useMutation(
|
||||
async (payload) => {
|
||||
const currentUser = await security.authc.getCurrentUser();
|
||||
|
||||
if (!currentUser) {
|
||||
throw new Error('CurrentUser is missing');
|
||||
}
|
||||
|
||||
return savedObjects.client.update(savedQuerySavedObjectType, savedQueryId, {
|
||||
// @ts-expect-error update types
|
||||
...payload,
|
||||
updated_by: currentUser.username,
|
||||
updated_at: new Date(Date.now()).toISOString(),
|
||||
});
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
// @ts-expect-error update types
|
||||
setErrorToast(error, { title: error.body.error, toastMessage: error.body.message });
|
||||
},
|
||||
onSuccess: (payload) => {
|
||||
queryClient.invalidateQueries(SAVED_QUERIES_ID);
|
||||
navigateToApp(PLUGIN_ID, { path: pagePathGetters.saved_queries() });
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.osquery.editSavedQuery.successToastMessageText', {
|
||||
defaultMessage: 'Successfully updated "{savedQueryName}" query',
|
||||
values: {
|
||||
savedQueryName: payload.attributes?.name ?? '',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -6,6 +6,9 @@
|
|||
*/
|
||||
|
||||
export const ALL_OSQUERY_VERSIONS_OPTIONS = [
|
||||
{
|
||||
label: '4.8.0',
|
||||
},
|
||||
{
|
||||
label: '4.7.0',
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { isEmpty, pickBy } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -112,6 +112,15 @@ export const PlatformCheckBoxGroupField = ({
|
|||
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
|
||||
useEffect(() => {
|
||||
setCheckboxIdToSelectedMap(() =>
|
||||
(options as EuiCheckboxGroupOption[]).reduce((acc, option) => {
|
||||
acc[option.id] = isEmpty(field.value) ? true : field.value?.includes(option.id) ?? false;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>)
|
||||
);
|
||||
}, [field.value, options]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
|
|
|
@ -18,13 +18,14 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiDescribedFormGroup,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { satisfies } from 'semver';
|
||||
|
||||
import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
|
||||
import { CodeEditorField } from '../../queries/form/code_editor_field';
|
||||
import { CodeEditorField } from '../../saved_queries/form/code_editor_field';
|
||||
import { Form, getUseField, Field } from '../../shared_imports';
|
||||
import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field';
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants';
|
||||
|
@ -33,6 +34,7 @@ import {
|
|||
useScheduledQueryGroupQueryForm,
|
||||
} from './use_scheduled_query_group_query_form';
|
||||
import { ManageIntegrationLink } from '../../components/manage_integration_link';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -49,6 +51,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
onSave,
|
||||
onClose,
|
||||
}) => {
|
||||
const [isEditMode] = useState(!!defaultValue);
|
||||
const { form } = useScheduledQueryGroupQueryForm({
|
||||
defaultValue,
|
||||
handleSubmit: (payload, isValid) =>
|
||||
|
@ -67,7 +70,31 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
[integrationPackageVersion]
|
||||
);
|
||||
|
||||
const { submit } = form;
|
||||
const { submit, setFieldValue } = form;
|
||||
|
||||
const handleSetQueryValue = useCallback(
|
||||
(savedQuery) => {
|
||||
setFieldValue('id', savedQuery.id);
|
||||
setFieldValue('query', savedQuery.query);
|
||||
|
||||
if (savedQuery.description) {
|
||||
setFieldValue('description', savedQuery.description);
|
||||
}
|
||||
|
||||
if (savedQuery.interval) {
|
||||
setFieldValue('interval', savedQuery.interval);
|
||||
}
|
||||
|
||||
if (isFieldSupported && savedQuery.platform) {
|
||||
setFieldValue('platform', savedQuery.platform);
|
||||
}
|
||||
|
||||
if (isFieldSupported && savedQuery.version) {
|
||||
setFieldValue('version', [savedQuery.version]);
|
||||
}
|
||||
},
|
||||
[isFieldSupported, setFieldValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
|
@ -75,7 +102,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">
|
||||
{defaultValue ? (
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.editFormTitle"
|
||||
defaultMessage="Edit query"
|
||||
|
@ -91,11 +118,20 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
{!isEditMode ? (
|
||||
<>
|
||||
<SavedQueriesDropdown onChange={handleSetQueryValue} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
<CommonUseField path="id" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>Set heading level based on context</h3>}
|
||||
description={'Will be wrapped in a small, subdued EuiText block.'}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="interval"
|
||||
|
@ -124,7 +160,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
euiFieldProps={{ disabled: !isFieldSupported }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescribedFormGroup>
|
||||
<EuiSpacer />
|
||||
</Form>
|
||||
{!isFieldSupported ? (
|
||||
|
|
|
@ -22,6 +22,16 @@ export const formSchema = {
|
|||
}),
|
||||
validations: idFieldValidations.map((validator) => ({ validator })),
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.descriptionFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
),
|
||||
validations: [],
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
|
||||
|
|
|
@ -45,6 +45,9 @@ export const useScheduledQueryGroupQueryForm = ({
|
|||
// @ts-expect-error update types
|
||||
serializer: (payload) =>
|
||||
produce(payload, (draft) => {
|
||||
if (isArray(draft.platform)) {
|
||||
draft.platform.join(',');
|
||||
}
|
||||
if (draft.platform?.split(',').length === 3) {
|
||||
// if all platforms are checked then use undefined
|
||||
delete draft.platform;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -37,6 +38,13 @@ const ScheduledQueryGroupsTableComponent = () => {
|
|||
|
||||
const renderActive = useCallback((_, item) => <ActiveStateSwitch item={item} />, []);
|
||||
|
||||
const renderUpdatedAt = useCallback((updatedAt, item) => {
|
||||
if (!updatedAt) return '-';
|
||||
|
||||
const updatedBy = item.updated_by !== item.created_by ? ` @ ${item.updated_by}` : '';
|
||||
return updatedAt ? `${moment(updatedAt).fromNow()}${updatedBy}` : '-';
|
||||
}, []);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<PackagePolicy>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -66,6 +74,21 @@ const ScheduledQueryGroupsTableComponent = () => {
|
|||
render: renderQueries,
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
field: 'created_by',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryGroups.table.createdByColumnTitle', {
|
||||
defaultMessage: 'Created by',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'updated_at',
|
||||
name: 'Last updated',
|
||||
sortable: (item) => (item.updated_at ? Date.parse(item.updated_at) : 0),
|
||||
truncateText: true,
|
||||
render: renderUpdatedAt,
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryGroups.table.activeColumnTitle', {
|
||||
|
@ -77,7 +100,7 @@ const ScheduledQueryGroupsTableComponent = () => {
|
|||
render: renderActive,
|
||||
},
|
||||
],
|
||||
[renderActive, renderAgentPolicy, renderQueries]
|
||||
[renderActive, renderAgentPolicy, renderQueries, renderUpdatedAt]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
import { DiscoverStart } from '../../../../src/plugins/discover/public';
|
||||
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
|
||||
import { FleetStart } from '../../fleet/public';
|
||||
import { LensPublicStart } from '../../../plugins/lens/public';
|
||||
import { LensPublicStart } from '../../lens/public';
|
||||
import { SecurityPluginStart } from '../../security/public';
|
||||
import { CoreStart } from '../../../../src/core/public';
|
||||
import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public';
|
||||
import {
|
||||
|
@ -30,6 +31,7 @@ export interface StartPlugins {
|
|||
data: DataPublicPluginStart;
|
||||
fleet: FleetStart;
|
||||
lens?: LensPublicStart;
|
||||
security: SecurityPluginStart;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ run(
|
|||
const mapFunc = pullFields.bind(null, { name: true });
|
||||
const formattedSchema = schemaData.map(mapFunc);
|
||||
await fs.writeFile(
|
||||
path.join(schemaPath, `${flags.schema_version}-formatted`),
|
||||
path.join(schemaPath, `${flags.schema_version}-formatted.json`),
|
||||
JSON.stringify(formattedSchema)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import { TypeOf, schema } from '@kbn/config-schema';
|
|||
export const ConfigSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
actionEnabled: schema.boolean({ defaultValue: false }),
|
||||
savedQueries: schema.boolean({ defaultValue: false }),
|
||||
savedQueries: schema.boolean({ defaultValue: true }),
|
||||
packs: schema.boolean({ defaultValue: false }),
|
||||
});
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Logger, LoggerFactory } from 'src/core/server';
|
||||
import { SecurityPluginStart } from '../../../security/server';
|
||||
import {
|
||||
AgentService,
|
||||
FleetStartContract,
|
||||
|
@ -69,7 +70,7 @@ export class OsqueryAppContextService {
|
|||
export interface OsqueryAppContext {
|
||||
logFactory: LoggerFactory;
|
||||
config(): ConfigType;
|
||||
|
||||
security: SecurityPluginStart;
|
||||
/**
|
||||
* Object readiness is tied to plugin start method
|
||||
*/
|
||||
|
|
|
@ -14,34 +14,40 @@ export const savedQuerySavedObjectMappings: SavedObjectsType['mappings'] = {
|
|||
description: {
|
||||
type: 'text',
|
||||
},
|
||||
name: {
|
||||
type: 'text',
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
query: {
|
||||
type: 'text',
|
||||
},
|
||||
created: {
|
||||
created_at: {
|
||||
type: 'date',
|
||||
},
|
||||
createdBy: {
|
||||
created_by: {
|
||||
type: 'text',
|
||||
},
|
||||
platform: {
|
||||
type: 'keyword',
|
||||
},
|
||||
updated: {
|
||||
version: {
|
||||
type: 'keyword',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
updatedBy: {
|
||||
updated_by: {
|
||||
type: 'text',
|
||||
},
|
||||
interval: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const savedQueryType: SavedObjectsType = {
|
||||
name: savedQuerySavedObjectType,
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: savedQuerySavedObjectMappings,
|
||||
};
|
||||
|
||||
|
@ -53,16 +59,16 @@ export const packSavedObjectMappings: SavedObjectsType['mappings'] = {
|
|||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
created: {
|
||||
created_at: {
|
||||
type: 'date',
|
||||
},
|
||||
createdBy: {
|
||||
created_by: {
|
||||
type: 'text',
|
||||
},
|
||||
updated: {
|
||||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
updatedBy: {
|
||||
updated_by: {
|
||||
type: 'text',
|
||||
},
|
||||
queries: {
|
||||
|
@ -81,6 +87,6 @@ export const packSavedObjectMappings: SavedObjectsType['mappings'] = {
|
|||
export const packType: SavedObjectsType = {
|
||||
name: packSavedObjectType,
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: packSavedObjectMappings,
|
||||
};
|
||||
|
|
|
@ -46,6 +46,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
logFactory: this.context.logger,
|
||||
service: this.osqueryAppContextService,
|
||||
config: (): ConfigType => config,
|
||||
security: plugins.security,
|
||||
};
|
||||
|
||||
initSavedObjects(core.savedObjects, osqueryContext);
|
||||
|
|
|
@ -48,13 +48,15 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
|
|||
}
|
||||
|
||||
try {
|
||||
const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username;
|
||||
const action = {
|
||||
action_id: uuid.v4(),
|
||||
'@timestamp': moment().toISOString(),
|
||||
expiration: moment().add(1, 'days').toISOString(),
|
||||
expiration: moment().add(5, 'minutes').toISOString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'osquery',
|
||||
agents: selectedAgents,
|
||||
user_id: currentUser,
|
||||
data: {
|
||||
id: uuid.v4(),
|
||||
query: request.body.query,
|
||||
|
@ -75,7 +77,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
|
|||
incrementCount(soClient, 'live_query', 'errors');
|
||||
return response.customError({
|
||||
statusCode: 500,
|
||||
body: new Error(`Error occurred whlie processing ${error}`),
|
||||
body: new Error(`Error occurred while processing ${error}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,15 @@ export const createSavedQueryRoute = (router: IRouter) => {
|
|||
async (context, request, response) => {
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
const { name, description, platform, query } = request.body;
|
||||
const { id, description, platform, query, version, interval } = request.body;
|
||||
|
||||
const savedQuerySO = await savedObjectsClient.create(savedQuerySavedObjectType, {
|
||||
name,
|
||||
id,
|
||||
description,
|
||||
query,
|
||||
platform,
|
||||
version,
|
||||
interval,
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
|
|
|
@ -21,18 +21,14 @@ export const readSavedQueryRoute = (router: IRouter) => {
|
|||
async (context, request, response) => {
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
const { attributes, ...savedQuery } = await savedObjectsClient.get(
|
||||
const savedQuery = await savedObjectsClient.get(
|
||||
savedQuerySavedObjectType,
|
||||
// @ts-expect-error update types
|
||||
request.params.id
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
...savedQuery,
|
||||
// @ts-expect-error update types
|
||||
...attributes,
|
||||
},
|
||||
body: savedQuery,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -23,17 +23,19 @@ export const updateSavedQueryRoute = (router: IRouter) => {
|
|||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
// @ts-expect-error update types
|
||||
const { name, description, platform, query } = request.body;
|
||||
const { id, description, platform, query, version, interval } = request.body;
|
||||
|
||||
const savedQuerySO = await savedObjectsClient.update(
|
||||
savedQuerySavedObjectType,
|
||||
// @ts-expect-error update types
|
||||
request.params.id,
|
||||
{
|
||||
name,
|
||||
id,
|
||||
description,
|
||||
platform,
|
||||
query,
|
||||
version,
|
||||
interval,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { FleetStartContract } from '../../fleet/server';
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
|
||||
import { PluginSetupContract } from '../../features/server';
|
||||
import { SecurityPluginStart } from '../../security/server';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface OsqueryPluginSetup {}
|
||||
|
@ -24,6 +25,7 @@ export interface SetupPlugins {
|
|||
actions: ActionsPlugin['setup'];
|
||||
data: DataPluginSetup;
|
||||
features: PluginSetupContract;
|
||||
security: SecurityPluginStart;
|
||||
}
|
||||
|
||||
export interface StartPlugins {
|
||||
|
|
|
@ -17336,7 +17336,7 @@
|
|||
"xpack.osquery.breadcrumbs.newLiveQueryPageTitle": "新規",
|
||||
"xpack.osquery.breadcrumbs.overviewPageTitle": "概要",
|
||||
"xpack.osquery.breadcrumbs.scheduledQueryGroupsPageTitle": "スケジュールされたクエリグループ",
|
||||
"xpack.osquery.codeEditorField.osquerySchemaLinkLabel": "Osqueryスキーマ",
|
||||
"xpack.osquery.osquerySchemaLinkLabel": "Osqueryスキーマ",
|
||||
"xpack.osquery.common.tabBetaBadgeLabel": "ベータ",
|
||||
"xpack.osquery.common.tabBetaBadgeTooltipContent": "この機能は現在開発中です。他にも機能が追加され、機能によっては変更されるものもあります。",
|
||||
"xpack.osquery.editScheduledQuery.pageTitle": "{queryName}を編集",
|
||||
|
|
|
@ -17573,7 +17573,7 @@
|
|||
"xpack.osquery.breadcrumbs.newLiveQueryPageTitle": "新建",
|
||||
"xpack.osquery.breadcrumbs.overviewPageTitle": "概览",
|
||||
"xpack.osquery.breadcrumbs.scheduledQueryGroupsPageTitle": "已计划查询组",
|
||||
"xpack.osquery.codeEditorField.osquerySchemaLinkLabel": "Osquery 架构",
|
||||
"xpack.osquery.osquerySchemaLinkLabel": "Osquery 架构",
|
||||
"xpack.osquery.common.tabBetaBadgeLabel": "公测版",
|
||||
"xpack.osquery.common.tabBetaBadgeTooltipContent": "我们正在开发此功能。将会有更多的功能,某些功能可能有变更。",
|
||||
"xpack.osquery.createScheduledQuery.agentPolicyAgentsCountText": "{count, plural, other {# 个代理}}已注册",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue