From 35cc59b571d19fe52eff17777a4613fd867ff928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Tue, 15 Jun 2021 20:52:20 +0300 Subject: [PATCH] [Osquery] Add support for platform and version fields (#101835) --- .../forms/hook_form_lib/hooks/use_form.ts | 2 +- .../server/services/package_policy.test.ts | 123 ++++++++++++ .../fleet/server/services/package_policy.ts | 11 +- x-pack/plugins/osquery/common/types.ts | 30 +++ .../action_results/action_results_summary.tsx | 3 +- .../agent_policies/agents_policy_link.tsx | 5 +- .../components/manage_integration_link.tsx | 9 +- .../fleet_integration/navigation_buttons.tsx | 15 +- ...managed_policy_create_import_extension.tsx | 21 ++- .../scheduled_query_groups/details/index.tsx | 2 +- .../form/add_query_flyout.tsx | 128 ------------- .../form/edit_query_flyout.tsx | 140 -------------- .../scheduled_query_groups/form/index.tsx | 94 +++++++--- .../form/queries_field.tsx | 166 +++++++++++------ .../queries/constants.ts | 72 +++++++ .../queries/platform_checkbox_group_field.tsx | 134 +++++++++++++ .../queries/platforms/constants.ts | 10 + .../queries/platforms/helpers.tsx | 55 ++++++ .../queries/platforms/index.tsx | 50 +++++ .../queries/platforms/logos/linux.svg | 1 + .../queries/platforms/logos/macos.svg | 1 + .../queries/platforms/logos/windows.svg | 1 + .../queries/platforms/platform_icon.tsx | 21 +++ .../queries/platforms/types.ts | 12 ++ .../queries/query_flyout.tsx | 176 ++++++++++++++++++ .../scheduled_query_groups/queries/schema.tsx | 70 +++++++ .../use_scheduled_query_group_query_form.tsx | 74 ++++++++ .../{form => queries}/validations.ts | 0 .../scheduled_query_group_queries_table.tsx | 59 ++++-- .../use_scheduled_query_group.ts | 13 +- .../plugins/osquery/public/shared_imports.ts | 1 + x-pack/plugins/osquery/tsconfig.json | 3 +- 32 files changed, 1115 insertions(+), 387 deletions(-) delete mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx delete mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx create mode 100644 x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx rename x-pack/plugins/osquery/public/scheduled_query_groups/{form => queries}/validations.ts (100%) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 181bd9959c1bbd..fb334afb22b137 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -18,7 +18,7 @@ const DEFAULT_OPTIONS = { stripEmptyFields: true, }; -interface UseFormReturn { +export interface UseFormReturn { form: FormHook; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index a6958ba88449a0..b3626a83c41d1f 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -688,6 +688,129 @@ describe('Package policy service', () => { expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south'])); expect(modifiedStream.vars!.period.value).toEqual('12mo'); }); + + it('should add new input vars when updating', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const mockPackagePolicy = createPackagePolicyMock(); + const mockInputs = [ + { + config: {}, + enabled: true, + keep_enabled: true, + type: 'endpoint', + vars: { + dog: { + type: 'text', + value: 'dalmatian', + }, + cat: { + type: 'text', + value: 'siamese', + frozen: true, + }, + }, + streams: [ + { + data_stream: { + type: 'birds', + dataset: 'migratory.patterns', + }, + enabled: false, + id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`, + vars: { + paths: { + value: ['north', 'south'], + type: 'text', + frozen: true, + }, + }, + }, + ], + }, + ]; + const inputsUpdate = [ + { + config: {}, + enabled: false, + type: 'endpoint', + vars: { + dog: { + type: 'text', + value: 'labrador', + }, + cat: { + type: 'text', + value: 'tabby', + }, + }, + streams: [ + { + data_stream: { + type: 'birds', + dataset: 'migratory.patterns', + }, + enabled: false, + id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`, + vars: { + paths: { + value: ['east', 'west'], + type: 'text', + }, + period: { + value: '12mo', + type: 'text', + }, + }, + }, + ], + }, + ]; + const attributes = { + ...mockPackagePolicy, + inputs: mockInputs, + }; + + savedObjectsClient.get.mockResolvedValue({ + id: 'test', + type: 'abcd', + references: [], + version: 'test', + attributes, + }); + + savedObjectsClient.update.mockImplementation( + async ( + type: string, + id: string, + attrs: any + ): Promise> => { + savedObjectsClient.get.mockResolvedValue({ + id: 'test', + type: 'abcd', + references: [], + version: 'test', + attributes: attrs, + }); + return attrs; + } + ); + const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + const result = await packagePolicyService.update( + savedObjectsClient, + elasticsearchClient, + 'the-package-policy-id', + { ...mockPackagePolicy, inputs: inputsUpdate } + ); + + const [modifiedInput] = result.inputs; + expect(modifiedInput.enabled).toEqual(true); + expect(modifiedInput.vars!.dog.value).toEqual('labrador'); + expect(modifiedInput.vars!.cat.value).toEqual('siamese'); + const [modifiedStream] = modifiedInput.streams; + expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south'])); + expect(modifiedStream.vars!.period.value).toEqual('12mo'); + }); }); describe('runExternalCallbacks', () => { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 93bcef458279c0..1cda159429984c 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -649,11 +649,16 @@ function _enforceFrozenVars( newVars: Record ) { const resultVars: Record = {}; + for (const [key, val] of Object.entries(newVars)) { + if (oldVars[key]?.frozen) { + resultVars[key] = oldVars[key]; + } else { + resultVars[key] = val; + } + } for (const [key, val] of Object.entries(oldVars)) { - if (val.frozen) { + if (!newVars[key] && val.frozen) { resultVars[key] = val; - } else { - resultVars[key] = newVars[key]; } } return resultVars; diff --git a/x-pack/plugins/osquery/common/types.ts b/x-pack/plugins/osquery/common/types.ts index 11c418a51fc7cb..ab9e36ec335db6 100644 --- a/x-pack/plugins/osquery/common/types.ts +++ b/x-pack/plugins/osquery/common/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../../fleet/common'; + export const savedQuerySavedObjectType = 'osquery-saved-query'; export const packSavedObjectType = 'osquery-pack'; export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack'; @@ -25,3 +27,31 @@ export type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infe ? { [K in keyof U]: U[K][0] } : never : never; + +export interface OsqueryManagerPackagePolicyConfigRecordEntry { + type: string; + value: string; + frozen?: boolean; +} + +export interface OsqueryManagerPackagePolicyConfigRecord { + id: OsqueryManagerPackagePolicyConfigRecordEntry; + query: OsqueryManagerPackagePolicyConfigRecordEntry; + interval: OsqueryManagerPackagePolicyConfigRecordEntry; + platform?: OsqueryManagerPackagePolicyConfigRecordEntry; + version?: OsqueryManagerPackagePolicyConfigRecordEntry; +} + +export interface OsqueryManagerPackagePolicyInputStream + extends Omit { + config?: OsqueryManagerPackagePolicyConfigRecord; + vars?: OsqueryManagerPackagePolicyConfigRecord; +} + +export interface OsqueryManagerPackagePolicyInput extends Omit { + streams: OsqueryManagerPackagePolicyInputStream[]; +} + +export interface OsqueryManagerPackagePolicy extends Omit { + inputs: OsqueryManagerPackagePolicyInput[]; +} diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index ffa86c547656cb..23277976968a98 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -23,6 +23,7 @@ import { import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { PLUGIN_ID } from '../../../fleet/common'; import { pagePathGetters } from '../../../fleet/public'; import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; @@ -130,7 +131,7 @@ const ActionResultsSummaryComponent: React.FC = ({ (agentId) => ( = ({ policyId } const href = useMemo( () => - getUrlForApp('fleet', { + getUrlForApp(PLUGIN_ID, { path: `#` + pagePathGetters.policy_details({ policyId }), }), [getUrlForApp, policyId] @@ -36,7 +37,7 @@ const AgentsPolicyLinkComponent: React.FC = ({ policyId } if (!isModifiedEvent(event) && isLeftClickEvent(event)) { event.preventDefault(); - return navigateToApp('fleet', { + return navigateToApp(PLUGIN_ID, { path: `#` + pagePathGetters.policy_details({ policyId }), }); } diff --git a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx index 8419003f57715b..b28471a907e04c 100644 --- a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx +++ b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../fleet/common'; import { pagePathGetters } from '../../../fleet/public'; import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; @@ -22,12 +23,12 @@ const ManageIntegrationLinkComponent = () => { const integrationHref = useMemo(() => { if (osqueryIntegration) { - return getUrlForApp('fleet', { + return getUrlForApp(INTEGRATIONS_PLUGIN_ID, { path: '#' + pagePathGetters.integration_details_policies({ pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`, - }), + })[1], }); } }, [getUrlForApp, osqueryIntegration]); @@ -37,12 +38,12 @@ const ManageIntegrationLinkComponent = () => { if (!isModifiedEvent(event) && isLeftClickEvent(event)) { event.preventDefault(); if (osqueryIntegration) { - return navigateToApp('fleet', { + return navigateToApp(INTEGRATIONS_PLUGIN_ID, { path: '#' + pagePathGetters.integration_details_policies({ pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`, - }), + })[1], }); } } diff --git a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx index 808718c55d1994..d8169c25ad929c 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx @@ -9,16 +9,17 @@ import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; +import { PLUGIN_ID } from '../../common'; import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; interface NavigationButtonsProps { isDisabled?: boolean; - integrationPolicyId?: string; - agentPolicyId?: string; + integrationPolicyId?: string | undefined; + agentPolicyId?: string | undefined; } const NavigationButtonsComponent: React.FC = ({ - isDisabled, + isDisabled = false, integrationPolicyId, agentPolicyId, }) => { @@ -28,7 +29,7 @@ const NavigationButtonsComponent: React.FC = ({ const liveQueryHref = useMemo( () => - getUrlForApp('osquery', { + getUrlForApp(PLUGIN_ID, { path: agentPolicyId ? `/live_queries/new?agentPolicyId=${agentPolicyId}` : ' `/live_queries/new', @@ -40,7 +41,7 @@ const NavigationButtonsComponent: React.FC = ({ (event) => { if (!isModifiedEvent(event) && isLeftClickEvent(event)) { event.preventDefault(); - navigateToApp('osquery', { + navigateToApp(PLUGIN_ID, { path: agentPolicyId ? `/live_queries/new?agentPolicyId=${agentPolicyId}` : ' `/live_queries/new', @@ -50,7 +51,7 @@ const NavigationButtonsComponent: React.FC = ({ [agentPolicyId, navigateToApp] ); - const scheduleQueryGroupsHref = getUrlForApp('osquery', { + const scheduleQueryGroupsHref = getUrlForApp(PLUGIN_ID, { path: integrationPolicyId ? `/scheduled_query_groups/${integrationPolicyId}/edit` : `/scheduled_query_groups`, @@ -60,7 +61,7 @@ const NavigationButtonsComponent: React.FC = ({ (event) => { if (!isModifiedEvent(event) && isLeftClickEvent(event)) { event.preventDefault(); - navigateToApp('osquery', { + navigateToApp(PLUGIN_ID, { path: integrationPolicyId ? `/scheduled_query_groups/${integrationPolicyId}/edit` : `/scheduled_query_groups`, diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 6dfbc086c394a5..2305df807f1c84 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -15,8 +15,10 @@ import { i18n } from '@kbn/i18n'; import { agentRouteService, agentPolicyRouteService, - PackagePolicy, AgentPolicy, + PLUGIN_ID, + INTEGRATIONS_PLUGIN_ID, + NewPackagePolicy, } from '../../../fleet/common'; import { pagePathGetters, @@ -27,6 +29,7 @@ import { import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table'; import { useKibana } from '../common/lib/kibana'; import { NavigationButtons } from './navigation_buttons'; +import { OsqueryManagerPackagePolicy } from '../../common/types'; /** * Exports Osquery-specific package policy instructions @@ -51,7 +54,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< const agentsLinkHref = useMemo(() => { if (!policy?.policy_id) return '#'; - return getUrlForApp('fleet', { + return getUrlForApp(PLUGIN_ID, { path: `#` + pagePathGetters.policy_details({ policyId: policy?.policy_id }) + @@ -128,13 +131,13 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< replace({ state: { onSaveNavigateTo: (newPackagePolicy) => [ - 'fleet', + INTEGRATIONS_PLUGIN_ID, { path: '#' + pagePathGetters.integration_policy_edit({ packagePolicyId: newPackagePolicy.id, - }), + })[1], state: { forceRefresh: true, }, @@ -146,7 +149,11 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< }, [editMode, replace]); const scheduledQueryGroupTableData = useMemo(() => { - const policyWithoutEmptyQueries = produce(newPolicy, (draft) => { + const policyWithoutEmptyQueries = produce< + NewPackagePolicy, + OsqueryManagerPackagePolicy, + OsqueryManagerPackagePolicy + >(newPolicy, (draft) => { draft.inputs[0].streams = filter(['compiled_stream.id', null], draft.inputs[0].streams); return draft; }); @@ -205,7 +212,9 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< {editMode && scheduledQueryGroupTableData.inputs[0].streams.length ? ( - + ) : null} diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx index a1203542613058..960de043eac6e7 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx @@ -125,7 +125,7 @@ const ScheduledQueryGroupDetailsPageComponent = () => { return ( - {data && } + {data && } ); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx deleted file mode 100644 index 3879a375b857c6..00000000000000 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx +++ /dev/null @@ -1,128 +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 { - EuiFlyout, - EuiTitle, - EuiSpacer, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiFlyoutFooter, - EuiPortal, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, -} from '@elastic/eui'; -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { CodeEditorField } from '../../queries/form/code_editor_field'; -import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations'; -import { Form, useForm, FormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports'; - -const FORM_ID = 'addQueryFlyoutForm'; - -const CommonUseField = getUseField({ component: Field }); - -interface AddQueryFlyoutProps { - onSave: (payload: FormData) => Promise; - onClose: () => void; -} - -const AddQueryFlyoutComponent: React.FC = ({ onSave, onClose }) => { - const { form } = useForm({ - id: FORM_ID, - // @ts-expect-error update types - onSubmit: (payload, isValid) => { - if (isValid) { - onSave(payload); - onClose(); - } - }, - schema: { - id: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', { - defaultMessage: 'ID', - }), - validations: idFieldValidations.map((validator) => ({ validator })), - }, - query: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', { - defaultMessage: 'Query', - }), - validations: [{ validator: queryFieldValidation }], - }, - interval: { - type: FIELD_TYPES.NUMBER, - label: i18n.translate( - 'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel', - { - defaultMessage: 'Interval (s)', - } - ), - validations: [{ validator: intervalFieldValidation }], - }, - }, - }); - - const { submit } = form; - - return ( - - - - -

- -

-
-
- -
- - - - - { - // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop - - } - -
- - - - - - - - - - - - - - -
-
- ); -}; - -export const AddQueryFlyout = React.memo(AddQueryFlyoutComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx deleted file mode 100644 index f44b5e45a26e54..00000000000000 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx +++ /dev/null @@ -1,140 +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 { - EuiFlyout, - EuiTitle, - EuiSpacer, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiFlyoutFooter, - EuiPortal, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, -} from '@elastic/eui'; -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { PackagePolicyInputStream } from '../../../../fleet/common'; -import { CodeEditorField } from '../../queries/form/code_editor_field'; -import { Form, useForm, getUseField, Field, FIELD_TYPES } from '../../shared_imports'; -import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations'; - -const FORM_ID = 'editQueryFlyoutForm'; - -const CommonUseField = getUseField({ component: Field }); - -interface EditQueryFlyoutProps { - defaultValue: PackagePolicyInputStream; - onSave: (payload: FormData) => void; - onClose: () => void; -} - -export const EditQueryFlyout: React.FC = ({ - defaultValue, - onSave, - onClose, -}) => { - const { form } = useForm({ - id: FORM_ID, - // @ts-expect-error update types - onSubmit: (payload, isValid) => { - if (isValid) { - // @ts-expect-error update types - onSave(payload); - onClose(); - } - return; - }, - defaultValue, - deserializer: (payload) => ({ - id: payload.vars.id.value, - query: payload.vars.query.value, - interval: payload.vars.interval.value, - }), - schema: { - id: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', { - defaultMessage: 'ID', - }), - validations: idFieldValidations.map((validator) => ({ validator })), - }, - query: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', { - defaultMessage: 'Query', - }), - validations: [{ validator: queryFieldValidation }], - }, - interval: { - type: FIELD_TYPES.NUMBER, - label: i18n.translate( - 'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel', - { - defaultMessage: 'Interval (s)', - } - ), - validations: [{ validator: intervalFieldValidation }], - }, - }, - }); - - const { submit } = form; - - return ( - - - - -

- -

-
-
- -
- - - - - { - // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop - - } - -
- - - - - - - - - - - - - - -
-
- ); -}; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx index 8924a61d181b6a..64efdf61fc7359 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx @@ -24,13 +24,22 @@ import { produce } from 'immer'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { PLUGIN_ID } from '../../../common'; +import { OsqueryManagerPackagePolicy } from '../../../common/types'; import { AgentPolicy, - PackagePolicy, PackagePolicyPackage, packagePolicyRouteService, } from '../../../../fleet/common'; -import { Form, useForm, useFormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports'; +import { + Form, + useForm, + useFormData, + getUseField, + Field, + FIELD_TYPES, + fieldValidators, +} from '../../shared_imports'; import { useKibana, useRouterNavigate } from '../../common/lib/kibana'; import { PolicyIdComboBoxField } from './policy_id_combobox_field'; import { QueriesField } from './queries_field'; @@ -44,7 +53,7 @@ const FORM_ID = 'scheduledQueryForm'; const CommonUseField = getUseField({ component: Field }); interface ScheduledQueryGroupFormProps { - defaultValue?: PackagePolicy; + defaultValue?: OsqueryManagerPackagePolicy; packageInfo?: PackagePolicyPackage; editMode?: boolean; } @@ -89,7 +98,7 @@ const ScheduledQueryGroupFormComponent: React.FC = { onSuccess: (data) => { if (!editMode) { - navigateToApp('osquery', { path: `scheduled_query_groups/${data.item.id}` }); + navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` }); toasts.addSuccess( i18n.translate('xpack.osquery.scheduledQueryGroup.form.createSuccessToastMessageText', { defaultMessage: 'Successfully scheduled {scheduledQueryGroupName}', @@ -101,7 +110,7 @@ const ScheduledQueryGroupFormComponent: React.FC = return; } - navigateToApp('osquery', { path: `scheduled_query_groups/${data.item.id}` }); + navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` }); toasts.addSuccess( i18n.translate('xpack.osquery.scheduledQueryGroup.form.updateSuccessToastMessageText', { defaultMessage: 'Successfully updated {scheduledQueryGroupName}', @@ -118,7 +127,15 @@ const ScheduledQueryGroupFormComponent: React.FC = } ); - const { form } = useForm({ + const { form } = useForm< + Omit & { + policy_id: string; + }, + Omit & { + policy_id: string[]; + namespace: string[]; + } + >({ id: FORM_ID, schema: { name: { @@ -126,6 +143,18 @@ const ScheduledQueryGroupFormComponent: React.FC = label: i18n.translate('xpack.osquery.scheduledQueryGroup.form.nameFieldLabel', { defaultMessage: 'Name', }), + validations: [ + { + validator: fieldValidators.emptyField( + i18n.translate( + 'xpack.osquery.scheduledQueryGroup.form.nameFieldRequiredErrorMessage', + { + defaultMessage: 'Name is a required field', + } + ) + ), + }, + ], }, description: { type: FIELD_TYPES.TEXT, @@ -144,19 +173,35 @@ const ScheduledQueryGroupFormComponent: React.FC = label: i18n.translate('xpack.osquery.scheduledQueryGroup.form.agentPolicyFieldLabel', { defaultMessage: 'Agent policy', }), + validations: [ + { + validator: fieldValidators.emptyField( + i18n.translate( + 'xpack.osquery.scheduledQueryGroup.form.policyIdFieldRequiredErrorMessage', + { + defaultMessage: 'Agent policy is a required field', + } + ) + ), + }, + ], }, }, - onSubmit: (payload) => { + onSubmit: (payload, isValid) => { + if (!isValid) return Promise.resolve(); const formData = produce(payload, (draft) => { - // @ts-expect-error update types - draft.inputs[0].streams.forEach((stream) => { - delete stream.compiled_stream; + if (draft.inputs?.length) { + draft.inputs[0].streams?.forEach((stream) => { + delete stream.compiled_stream; + + // we don't want to send id as null when creating the policy + if (stream.id == null) { + // @ts-expect-error update types + delete stream.id; + } + }); + } - // we don't want to send id as null when creating the policy - if (stream.id == null) { - delete stream.id; - } - }); return draft; }); return mutateAsync(formData); @@ -164,7 +209,6 @@ const ScheduledQueryGroupFormComponent: React.FC = options: { stripEmptyFields: false, }, - // @ts-expect-error update types deserializer: (payload) => ({ ...payload, policy_id: payload.policy_id.length ? [payload.policy_id] : [], @@ -172,9 +216,7 @@ const ScheduledQueryGroupFormComponent: React.FC = }), serializer: (payload) => ({ ...payload, - // @ts-expect-error update types policy_id: payload.policy_id[0], - // @ts-expect-error update types namespace: payload.namespace[0], }), defaultValue: merge( @@ -182,10 +224,11 @@ const ScheduledQueryGroupFormComponent: React.FC = name: '', description: '', enabled: true, - policy_id: [], + policy_id: '', namespace: 'default', output_id: '', - package: packageInfo, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + package: packageInfo!, inputs: [ { type: 'osquery', @@ -205,7 +248,15 @@ const ScheduledQueryGroupFormComponent: React.FC = [defaultValue, agentPolicyOptions] ); - const [{ policy_id: policyId }] = useFormData({ form, watch: ['policy_id'] }); + const [ + { + package: { version: integrationPackageVersion } = { version: undefined }, + policy_id: policyId, + }, + ] = useFormData({ + form, + watch: ['package', 'policy_id'], + }); const currentPolicy = useMemo(() => { if (!policyId) { @@ -288,6 +339,7 @@ const ScheduledQueryGroupFormComponent: React.FC = path="inputs" component={QueriesField} scheduledQueryGroupId={defaultValue?.id ?? null} + integrationPackageVersion={integrationPackageVersion} /> diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx index 34c6eaea1c2656..0718ff028e0022 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx @@ -11,16 +11,20 @@ import { produce } from 'immer'; import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PackagePolicyInput, PackagePolicyInputStream } from '../../../../fleet/common'; +import { + OsqueryManagerPackagePolicyInputStream, + OsqueryManagerPackagePolicyInput, +} from '../../../common/types'; import { OSQUERY_INTEGRATION_NAME } from '../../../common'; import { FieldHook } from '../../shared_imports'; import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_group_queries_table'; -import { AddQueryFlyout } from './add_query_flyout'; -import { EditQueryFlyout } from './edit_query_flyout'; +import { QueryFlyout } from '../queries/query_flyout'; import { OsqueryPackUploader } from './pack_uploader'; +import { getSupportedPlatforms } from '../queries/platforms/helpers'; interface QueriesFieldProps { - field: FieldHook; + field: FieldHook; + integrationPackageVersion?: string | undefined; scheduledQueryGroupId: string; } @@ -28,29 +32,53 @@ interface GetNewStreamProps { id: string; interval: string; query: string; + platform?: string | undefined; + version?: string | undefined; scheduledQueryGroupId?: string; } -const getNewStream = ({ id, interval, query, scheduledQueryGroupId }: GetNewStreamProps) => ({ - data_stream: { type: 'logs', dataset: `${OSQUERY_INTEGRATION_NAME}.result` }, - enabled: true, - id: scheduledQueryGroupId - ? `osquery-${OSQUERY_INTEGRATION_NAME}.result-${scheduledQueryGroupId}` - : null, - vars: { - id: { type: 'text', value: id }, - interval: { - type: 'integer', - value: interval, +interface GetNewStreamReturn extends Omit { + id?: string | null; +} + +const getNewStream = (payload: GetNewStreamProps) => + produce( + { + data_stream: { type: 'logs', dataset: `${OSQUERY_INTEGRATION_NAME}.result` }, + enabled: true, + id: payload.scheduledQueryGroupId + ? `osquery-${OSQUERY_INTEGRATION_NAME}.result-${payload.scheduledQueryGroupId}` + : null, + vars: { + id: { type: 'text', value: payload.id }, + interval: { + type: 'integer', + value: payload.interval, + }, + query: { type: 'text', value: payload.query }, + }, }, - query: { type: 'text', value: query }, - }, -}); + (draft) => { + if (payload.platform && draft.vars) { + draft.vars.platform = { type: 'text', value: payload.platform }; + } + if (payload.version && draft.vars) { + draft.vars.version = { type: 'text', value: payload.version }; + } + return draft; + } + ); -const QueriesFieldComponent: React.FC = ({ field, scheduledQueryGroupId }) => { +const QueriesFieldComponent: React.FC = ({ + field, + integrationPackageVersion, + scheduledQueryGroupId, +}) => { const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false); const [showEditQueryFlyout, setShowEditQueryFlyout] = useState(-1); - const [tableSelectedItems, setTableSelectedItems] = useState([]); + const [tableSelectedItems, setTableSelectedItems] = useState< + OsqueryManagerPackagePolicyInputStream[] + >([]); const handleShowAddFlyout = useCallback(() => setShowAddQueryFlyout(true), []); const handleHideAddFlyout = useCallback(() => setShowAddQueryFlyout(false), []); @@ -59,7 +87,7 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu const { setValue } = field; const handleDeleteClick = useCallback( - (stream: PackagePolicyInputStream) => { + (stream: OsqueryManagerPackagePolicyInputStream) => { const streamIndex = findIndex(field.value[0].streams, [ 'vars.id.value', stream.vars?.id.value, @@ -79,7 +107,7 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu ); const handleEditClick = useCallback( - (stream: PackagePolicyInputStream) => { + (stream: OsqueryManagerPackagePolicyInputStream) => { const streamIndex = findIndex(field.value[0].streams, [ 'vars.id.value', stream.vars?.id.value, @@ -91,39 +119,61 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu ); const handleEditQuery = useCallback( - (updatedQuery) => { - if (showEditQueryFlyout >= 0) { - setValue( - produce((draft) => { - draft[0].streams[showEditQueryFlyout].vars.id.value = updatedQuery.id; - draft[0].streams[showEditQueryFlyout].vars.interval.value = updatedQuery.interval; - draft[0].streams[showEditQueryFlyout].vars.query.value = updatedQuery.query; + (updatedQuery) => + new Promise((resolve) => { + if (showEditQueryFlyout >= 0) { + setValue( + produce((draft) => { + draft[0].streams[showEditQueryFlyout].vars.id.value = updatedQuery.id; + draft[0].streams[showEditQueryFlyout].vars.interval.value = updatedQuery.interval; + draft[0].streams[showEditQueryFlyout].vars.query.value = updatedQuery.query; - return draft; - }) - ); - } + if (updatedQuery.platform?.length) { + draft[0].streams[showEditQueryFlyout].vars.platform = { + type: 'text', + value: updatedQuery.platform, + }; + } else { + delete draft[0].streams[showEditQueryFlyout].vars.platform; + } - handleHideEditFlyout(); - }, + if (updatedQuery.version?.length) { + draft[0].streams[showEditQueryFlyout].vars.version = { + type: 'text', + value: updatedQuery.version, + }; + } else { + delete draft[0].streams[showEditQueryFlyout].vars.version; + } + + return draft; + }) + ); + } + + handleHideEditFlyout(); + resolve(); + }), [handleHideEditFlyout, setValue, showEditQueryFlyout] ); const handleAddQuery = useCallback( - (newQuery) => { - setValue( - produce((draft) => { - draft[0].streams.push( - getNewStream({ - ...newQuery, - scheduledQueryGroupId, - }) - ); - return draft; - }) - ); - handleHideAddFlyout(); - }, + (newQuery) => + new Promise((resolve) => { + setValue( + produce((draft) => { + draft[0].streams.push( + getNewStream({ + ...newQuery, + scheduledQueryGroupId, + }) + ); + return draft; + }) + ); + handleHideAddFlyout(); + resolve(); + }), [handleHideAddFlyout, scheduledQueryGroupId, setValue] ); @@ -148,6 +198,8 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu id: newQueryId, interval: newQuery.interval, query: newQuery.query, + version: newQuery.version, + platform: getSupportedPlatforms(newQuery.platform), scheduledQueryGroupId, }) ); @@ -160,7 +212,9 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu [scheduledQueryGroupId, setValue] ); - const tableData = useMemo(() => ({ inputs: field.value }), [field.value]); + const tableData = useMemo(() => (field.value.length ? field.value[0].streams : []), [ + field.value, + ]); return ( <> @@ -201,12 +255,16 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu {} {showAddQueryFlyout && ( - // @ts-expect-error update types - + )} {showEditQueryFlyout != null && showEditQueryFlyout >= 0 && ( - diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts new file mode 100644 index 00000000000000..3345c18d07b2cb --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts @@ -0,0 +1,72 @@ +/* + * 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 ALL_OSQUERY_VERSIONS_OPTIONS = [ + { + label: '4.7.0', + }, + { + label: '4.6.0', + }, + { + label: '4.5.1', + }, + { + label: '4.5.0', + }, + { + label: '4.4.0', + }, + { + label: '4.3.0', + }, + { + label: '4.2.0', + }, + { + label: '4.1.2', + }, + { + label: '4.1.1', + }, + { + label: '4.0.2', + }, + { + label: '3.3.2', + }, + { + label: '3.3.0', + }, + { + label: '3.2.6', + }, + { + label: '3.2.4', + }, + { + label: '2.9.0', + }, + { + label: '2.8.0', + }, + { + label: '2.7.0', + }, + { + label: '2.11.2', + }, + { + label: '2.11.0', + }, + { + label: '2.10.2', + }, + { + label: '2.10.0', + }, +]; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx new file mode 100644 index 00000000000000..4e433e9e240b1a --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx @@ -0,0 +1,134 @@ +/* + * 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, pickBy } from 'lodash'; +import React, { useCallback, useMemo, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiCheckboxGroup, + EuiCheckboxGroupOption, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../shared_imports'; +import { PlatformIcon } from './platforms/platform_icon'; + +interface Props { + field: FieldHook; + euiFieldProps?: Record; + idAria?: string; + [key: string]: unknown; +} + +export const PlatformCheckBoxGroupField = ({ + field, + euiFieldProps = {}, + idAria, + ...rest +}: Props) => { + const options = useMemo( + () => [ + { + id: 'linux', + label: ( + + + + + + + + + ), + }, + { + id: 'darwin', + label: ( + + + + + + + + + ), + }, + { + id: 'windows', + label: ( + + + + + + + + + ), + }, + ], + [] + ); + + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState>( + () => + (options as EuiCheckboxGroupOption[]).reduce((acc, option) => { + acc[option.id] = isEmpty(field.value) ? true : field.value?.includes(option.id) ?? false; + return acc; + }, {} as Record) + ); + + const onChange = useCallback( + (optionId: string) => { + const newCheckboxIdToSelectedMap = { + ...checkboxIdToSelectedMap, + [optionId]: !checkboxIdToSelectedMap[optionId], + }; + setCheckboxIdToSelectedMap(newCheckboxIdToSelectedMap); + + field.setValue(() => + Object.keys(pickBy(newCheckboxIdToSelectedMap, (value) => value === true)).join(',') + ); + }, + [checkboxIdToSelectedMap, field] + ); + + const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts new file mode 100644 index 00000000000000..4f81ed73e1e7a7 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 { PlatformType } from './types'; + +export const SUPPORTED_PLATFORMS = [PlatformType.darwin, PlatformType.linux, PlatformType.windows]; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx new file mode 100644 index 00000000000000..362fa5c67e6f90 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx @@ -0,0 +1,55 @@ +/* + * 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 { uniq } from 'lodash'; +import { SUPPORTED_PLATFORMS } from './constants'; + +import linuxSvg from './logos/linux.svg'; +import windowsSvg from './logos/windows.svg'; +import macosSvg from './logos/macos.svg'; +import { PlatformType } from './types'; + +export const getPlatformIconModule = (platform: string) => { + switch (platform) { + case 'darwin': + return macosSvg; + case 'linux': + return linuxSvg; + case 'windows': + return windowsSvg; + default: + return `${platform}`; + } +}; + +export const getSupportedPlatforms = (payload: string) => { + let platformArray: string[]; + try { + platformArray = payload?.split(',').map((platformString) => platformString.trim()); + } catch (e) { + return undefined; + } + + if (!platformArray) return; + + return uniq( + platformArray.reduce((acc, nextPlatform) => { + if (!SUPPORTED_PLATFORMS.includes(nextPlatform as PlatformType)) { + if (nextPlatform === 'posix') { + acc.push(PlatformType.darwin); + acc.push(PlatformType.linux); + } + if (nextPlatform === 'ubuntu') { + acc.push(PlatformType.linux); + } + } else { + acc.push(nextPlatform); + } + return acc; + }, [] as string[]) + ).join(','); +}; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx new file mode 100644 index 00000000000000..b8af2790c6f368 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx @@ -0,0 +1,50 @@ +/* + * 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 } from '@elastic/eui'; +import React, { useEffect, useState, useMemo } from 'react'; + +import { SUPPORTED_PLATFORMS } from './constants'; +import { PlatformIcon } from './platform_icon'; + +interface PlatformIconsProps { + platform: string; +} + +const PlatformIconsComponent: React.FC = ({ platform }) => { + const [platforms, setPlatforms] = useState(SUPPORTED_PLATFORMS); + + useEffect(() => { + setPlatforms((prevValue) => { + if (platform) { + let platformArray: string[]; + try { + platformArray = platform?.split(',').map((platformString) => platformString.trim()); + } catch (e) { + return prevValue; + } + return platformArray; + } else { + return SUPPORTED_PLATFORMS; + } + }); + }, [platform]); + + const content = useMemo( + () => + platforms.map((platformString) => ( + + + + )), + [platforms] + ); + + return {content}; +}; + +export const PlatformIcons = React.memo(PlatformIconsComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg new file mode 100644 index 00000000000000..47358292e08a80 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg new file mode 100644 index 00000000000000..baa5930800aa9c --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg new file mode 100644 index 00000000000000..0872225da3a117 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx new file mode 100644 index 00000000000000..1126dfd690c193 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx @@ -0,0 +1,21 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import { getPlatformIconModule } from './helpers'; + +interface PlatformIconProps { + platform: string; +} + +const PlatformIconComponent: React.FC = ({ platform }) => { + const platformIconModule = getPlatformIconModule(platform); + return ; +}; + +export const PlatformIcon = React.memo(PlatformIconComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts new file mode 100644 index 00000000000000..94953a6a854ea2 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts @@ -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 enum PlatformType { + darwin = 'darwin', + windows = 'windows', + linux = 'linux', +} diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx new file mode 100644 index 00000000000000..62ac3a46a2d773 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx @@ -0,0 +1,176 @@ +/* + * 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 { + EuiCallOut, + EuiFlyout, + EuiTitle, + EuiSpacer, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiPortal, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import React, { useMemo } 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 { Form, getUseField, Field } from '../../shared_imports'; +import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field'; +import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants'; +import { + UseScheduledQueryGroupQueryFormProps, + useScheduledQueryGroupQueryForm, +} from './use_scheduled_query_group_query_form'; +import { ManageIntegrationLink } from '../../components/manage_integration_link'; + +const CommonUseField = getUseField({ component: Field }); + +interface QueryFlyoutProps { + defaultValue?: UseScheduledQueryGroupQueryFormProps['defaultValue'] | undefined; + integrationPackageVersion?: string | undefined; + onSave: (payload: OsqueryManagerPackagePolicyConfigRecord) => Promise; + onClose: () => void; +} + +const QueryFlyoutComponent: React.FC = ({ + defaultValue, + integrationPackageVersion, + onSave, + onClose, +}) => { + const { form } = useScheduledQueryGroupQueryForm({ + defaultValue, + handleSubmit: (payload, isValid) => + new Promise((resolve) => { + if (isValid) { + onSave(payload); + onClose(); + } + resolve(); + }), + }); + + /* Platform and version fields are supported since osquer_manger@0.3.0 */ + const isFieldSupported = useMemo( + () => (integrationPackageVersion ? satisfies(integrationPackageVersion, '>=0.3.0') : false), + [integrationPackageVersion] + ); + + const { submit } = form; + + return ( + + + + +

+ {defaultValue ? ( + + ) : ( + + )} +

+
+
+ +
+ + + + + + + + + + + + + + + + + {!isFieldSupported ? ( + + } + iconType="pin" + > + + + + + + + ) : null} +
+ + + + + + + + + + + + + + +
+
+ ); +}; + +export const QueryFlyout = React.memo(QueryFlyoutComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx new file mode 100644 index 00000000000000..344c33b419dd6c --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx @@ -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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FIELD_TYPES } from '../../shared_imports'; + +import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations'; + +export const formSchema = { + id: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', { + defaultMessage: 'ID', + }), + validations: idFieldValidations.map((validator) => ({ validator })), + }, + query: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', { + defaultMessage: 'Query', + }), + validations: [{ validator: queryFieldValidation }], + }, + interval: { + defaultValue: 3600, + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel', { + defaultMessage: 'Interval (s)', + }), + validations: [{ validator: intervalFieldValidation }], + }, + platform: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.platformFieldLabel', { + defaultMessage: 'Platform', + }), + validations: [], + }, + version: { + defaultValue: [], + type: FIELD_TYPES.COMBO_BOX, + label: (( + + + + + + + + + + + ) as unknown) as string, + validations: [], + }, +}; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx new file mode 100644 index 00000000000000..bcde5f4b970d43 --- /dev/null +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx @@ -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 { isArray } from 'lodash'; +import uuid from 'uuid'; +import { produce } from 'immer'; + +import { FormConfig, useForm } from '../../shared_imports'; +import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types'; +import { formSchema } from './schema'; + +const FORM_ID = 'editQueryFlyoutForm'; + +export interface UseScheduledQueryGroupQueryFormProps { + defaultValue?: OsqueryManagerPackagePolicyConfigRecord | undefined; + handleSubmit: FormConfig< + OsqueryManagerPackagePolicyConfigRecord, + ScheduledQueryGroupFormData + >['onSubmit']; +} + +export interface ScheduledQueryGroupFormData { + id: string; + query: string; + interval: number; + platform?: string | undefined; + version?: string[] | undefined; +} + +export const useScheduledQueryGroupQueryForm = ({ + defaultValue, + handleSubmit, +}: UseScheduledQueryGroupQueryFormProps) => + useForm({ + id: FORM_ID + uuid.v4(), + onSubmit: handleSubmit, + options: { + stripEmptyFields: false, + }, + defaultValue, + // @ts-expect-error update types + serializer: (payload) => + produce(payload, (draft) => { + if (draft.platform?.split(',').length === 3) { + // if all platforms are checked then use undefined + delete draft.platform; + } + if (isArray(draft.version)) { + if (!draft.version.length) { + delete draft.version; + } else { + // @ts-expect-error update types + draft.version = draft.version[0]; + } + } + return draft; + }), + deserializer: (payload) => { + if (!payload) return {} as ScheduledQueryGroupFormData; + + return { + id: payload.id.value, + query: payload.query.value, + interval: parseInt(payload.interval.value, 10), + platform: payload.platform?.value, + version: payload.version?.value ? [payload.version?.value] : [], + }; + }, + schema: formSchema, + }); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/validations.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/validations.ts similarity index 100% rename from x-pack/plugins/osquery/public/scheduled_query_groups/form/validations.ts rename to x-pack/plugins/osquery/public/scheduled_query_groups/queries/validations.ts diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx index 6f78f2c086edf4..36d15587086f2d 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx @@ -22,9 +22,10 @@ import { PersistedIndexPatternLayer, PieVisualizationState, } from '../../../lens/public'; -import { PackagePolicy, PackagePolicyInputStream } from '../../../fleet/common'; import { FilterStateStore } from '../../../../../src/plugins/data/common'; import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana'; +import { PlatformIcons } from './queries/platforms'; +import { OsqueryManagerPackagePolicyInputStream } from '../../common/types'; export enum ViewResultsActionButtonType { icon = 'icon', @@ -303,12 +304,12 @@ const ViewResultsInDiscoverActionComponent: React.FC; + data: OsqueryManagerPackagePolicyInputStream[]; editMode?: boolean; - onDeleteClick?: (item: PackagePolicyInputStream) => void; - onEditClick?: (item: PackagePolicyInputStream) => void; - selectedItems?: PackagePolicyInputStream[]; - setSelectedItems?: (selection: PackagePolicyInputStream[]) => void; + onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; + onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; + selectedItems?: OsqueryManagerPackagePolicyInputStream[]; + setSelectedItems?: (selection: OsqueryManagerPackagePolicyInputStream[]) => void; } const ScheduledQueryGroupQueriesTableComponent: React.FC = ({ @@ -320,7 +321,7 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC { const renderDeleteAction = useCallback( - (item: PackagePolicyInputStream) => ( + (item: OsqueryManagerPackagePolicyInputStream) => ( ( + (item: OsqueryManagerPackagePolicyInputStream) => ( , + [] + ); + + const renderVersionColumn = useCallback( + (version: string) => + version + ? `${version}` + : i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', { + defaultMessage: 'ALL', + }), + [] + ); + const renderDiscoverResultsAction = useCallback( (item) => ( ({ sort: { - field: 'vars.id.value' as keyof PackagePolicyInputStream, + field: 'vars.id.value' as keyof OsqueryManagerPackagePolicyInputStream, direction: 'asc' as const, }, }), [] ); - const itemId = useCallback((item: PackagePolicyInputStream) => get('vars.id.value', item), []); + const itemId = useCallback( + (item: OsqueryManagerPackagePolicyInputStream) => get('vars.id.value', item), + [] + ); const selection = useMemo( () => ({ @@ -477,8 +512,8 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC - items={data.inputs[0].streams} + + items={data} itemId={itemId} columns={columns} sorting={sorting} diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts index e0f892d0302c0c..93d552b3f71f3a 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts @@ -8,11 +8,8 @@ import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; -import { - GetOnePackagePolicyResponse, - PackagePolicy, - packagePolicyRouteService, -} from '../../../fleet/common'; +import { GetOnePackagePolicyResponse, packagePolicyRouteService } from '../../../fleet/common'; +import { OsqueryManagerPackagePolicy } from '../../common/types'; interface UseScheduledQueryGroup { scheduledQueryGroupId: string; @@ -25,7 +22,11 @@ export const useScheduledQueryGroup = ({ }: UseScheduledQueryGroup) => { const { http } = useKibana().services; - return useQuery( + return useQuery< + Omit & { item: OsqueryManagerPackagePolicy }, + unknown, + OsqueryManagerPackagePolicy + >( ['scheduledQueryGroup', { scheduledQueryGroupId }], () => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)), { diff --git a/x-pack/plugins/osquery/public/shared_imports.ts b/x-pack/plugins/osquery/public/shared_imports.ts index 737b4d47357772..8a569a07616567 100644 --- a/x-pack/plugins/osquery/public/shared_imports.ts +++ b/x-pack/plugins/osquery/public/shared_imports.ts @@ -12,6 +12,7 @@ export { FieldValidateResponse, FIELD_TYPES, Form, + FormConfig, FormData, FormDataProvider, FormHook, diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 291b0f7c607cf1..76e26c770cfe02 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -12,7 +12,8 @@ "common/**/*", "public/**/*", "scripts/**/*", - "server/**/*" + "server/**/*", + "../../../typings/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" },