mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Osquery] Global packs (#143948)
This commit is contained in:
parent
2b2e1d19d2
commit
00a7cf6cb8
29 changed files with 1076 additions and 78 deletions
|
@ -113,8 +113,8 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"ml-trained-model": "e39dd10b2da827e194ddcaaf3db141ad1daf0201",
|
||||
"monitoring-telemetry": "af508cea8e22edaa909e462069390650fbbf01b7",
|
||||
"osquery-manager-usage-metric": "fbe3cbea25a96e2ca522ca436878e0162c94dcc2",
|
||||
"osquery-pack": "afb3b46c5e23fc24ad438e9c4317ff37e4e5164a",
|
||||
"osquery-pack-asset": "32421669c87c49dfabd4d3957f044e5eb7f7fb20",
|
||||
"osquery-pack": "a2d675c7af4208e54a5b28d23d324d7c599a5491",
|
||||
"osquery-pack-asset": "de8783298eb33a577bf1fa0caacd42121dcfae91",
|
||||
"osquery-saved-query": "7b213b4b7a3e59350e99c50e8df9948662ed493a",
|
||||
"query": "4640ef356321500a678869f24117b7091a911cb6",
|
||||
"sample-data-telemetry": "8b10336d9efae6f3d5593c4cc89fb4abcdf84e04",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { isEmpty, reduce } from 'lodash';
|
||||
import type { DefaultValues } from 'react-hook-form';
|
||||
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
|
||||
import type { GetAgentPoliciesResponseItem } from '@kbn/fleet-plugin/common';
|
||||
|
||||
export type ECSMappingArray = Array<{
|
||||
key: string;
|
||||
|
@ -52,3 +53,48 @@ export const convertECSMappingToArray = (
|
|||
},
|
||||
[] as ECSMappingArray
|
||||
);
|
||||
|
||||
export type ShardsArray = Array<{
|
||||
policy: {
|
||||
key: string;
|
||||
label: string;
|
||||
};
|
||||
percentage: number;
|
||||
}>;
|
||||
|
||||
export type Shard = Record<string, number>;
|
||||
|
||||
export const convertShardsToObject = (shards: ShardsArray): Shard =>
|
||||
reduce(
|
||||
shards,
|
||||
(acc, value) => {
|
||||
if (!isEmpty(value?.policy)) {
|
||||
acc[value.policy.key] = value.percentage;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Shard
|
||||
);
|
||||
|
||||
export const convertShardsToArray = (
|
||||
shards: DefaultValues<Shard>,
|
||||
policiesById?: Record<string, GetAgentPoliciesResponseItem>
|
||||
): ShardsArray =>
|
||||
reduce(
|
||||
shards,
|
||||
(acc, value, key) => {
|
||||
if (value) {
|
||||
acc.push({
|
||||
policy: {
|
||||
key,
|
||||
label: policiesById?.[key]?.name ?? '',
|
||||
},
|
||||
percentage: value,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as ShardsArray
|
||||
);
|
||||
|
|
|
@ -47,7 +47,9 @@ describe('Alert Event Details', () => {
|
|||
);
|
||||
findAndClickButton('Update pack');
|
||||
closeModalIfVisible();
|
||||
cy.contains(PACK_NAME);
|
||||
cy.contains(`Successfully updated "${PACK_NAME}" pack`);
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
|
||||
cy.visit('/app/security/rules');
|
||||
cy.contains(RULE_NAME);
|
||||
cy.wait(2000);
|
||||
|
@ -109,6 +111,7 @@ describe('Alert Event Details', () => {
|
|||
cy.get('.euiButtonEmpty--flushLeft').contains('Cancel').click();
|
||||
cy.getBySel('add-to-timeline').first().click();
|
||||
cy.getBySel('globalToastList').contains('Added');
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
cy.getBySel(RESULTS_TABLE).within(() => {
|
||||
cy.getBySel(RESULTS_TABLE_BUTTON).should('not.exist');
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('Add to Cases', () => {
|
|||
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'case_observability');
|
||||
});
|
||||
it('should add result a case and not have add to timeline in result', () => {
|
||||
cy.waitForReact();
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 1 },
|
||||
}).click();
|
||||
|
@ -54,6 +55,7 @@ describe('Add to Cases', () => {
|
|||
});
|
||||
|
||||
it('should add result a case and have add to timeline in result', () => {
|
||||
cy.waitForReact();
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 1 },
|
||||
}).click();
|
||||
|
|
|
@ -166,6 +166,7 @@ describe('ALL - Packs', () => {
|
|||
findAndClickButton('Save and deploy changes');
|
||||
cy.contains(PACK_NAME);
|
||||
cy.contains(`Successfully created "${PACK_NAME}" pack`);
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
});
|
||||
|
||||
it('to click the edit button and edit pack', () => {
|
||||
|
@ -186,6 +187,7 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('Save and deploy changes');
|
||||
findAndClickButton('Save and deploy changes');
|
||||
cy.contains(`Successfully updated "${PACK_NAME}" pack`);
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
});
|
||||
|
||||
it('should trigger validation when saved query is being chosen', () => {
|
||||
|
@ -409,4 +411,72 @@ describe('ALL - Packs', () => {
|
|||
cy.react('EuiTableRow').should('have.length.above', 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global packs', () => {
|
||||
beforeEach(() => {
|
||||
login();
|
||||
navigateTo('/app/osquery/packs');
|
||||
});
|
||||
|
||||
it('add global packs to polciies', () => {
|
||||
const globalPack = 'globalPack';
|
||||
cy.contains('Packs').click();
|
||||
findAndClickButton('Add pack');
|
||||
findFormFieldByRowsLabelAndType('Name', globalPack);
|
||||
cy.getBySel('osqueryPackTypeGlobal').click();
|
||||
findAndClickButton('Save pack');
|
||||
|
||||
cy.contains(globalPack);
|
||||
cy.contains(`Successfully created "${globalPack}" pack`);
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
|
||||
cy.visit(FLEET_AGENT_POLICIES);
|
||||
cy.contains('Create agent policy').click();
|
||||
cy.getBySel('createAgentPolicyNameField').type('testGlobal');
|
||||
cy.getBySel('createAgentPolicyFlyoutBtn').click();
|
||||
cy.contains(/^Agent policy 'testGlobal' created$/).click();
|
||||
cy.contains('testGlobal').click();
|
||||
cy.contains('Add integration').click();
|
||||
cy.contains(integration).click();
|
||||
addIntegration('testGlobal');
|
||||
cy.contains('Add Elastic Agent later').click();
|
||||
cy.contains('osquery_manager-');
|
||||
cy.request('/internal/osquery/fleet_wrapper/package_policies').then((response) => {
|
||||
const item = response.body.items[0];
|
||||
|
||||
expect(item.inputs[0].config.osquery.value.packs.globalPack).to.deep.equal({
|
||||
shard: 100,
|
||||
queries: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
it('add proper shard to policies packs config', () => {
|
||||
const shardPack = 'shardPack';
|
||||
cy.contains('Packs').click();
|
||||
findAndClickButton('Add pack');
|
||||
findFormFieldByRowsLabelAndType('Name', shardPack);
|
||||
|
||||
cy.contains('Partial deployment (shards)').click();
|
||||
cy.getBySel('shards-field-policy').type('Default{downArrow}{enter}');
|
||||
cy.get('#shardsPercentage0').type('{backspace}{backspace}5');
|
||||
findAndClickButton('Save pack');
|
||||
|
||||
cy.contains(`Successfully created "${shardPack}" pack`);
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
|
||||
cy.request('/internal/osquery/fleet_wrapper/package_policies').then((response) => {
|
||||
const shardPolicy = response.body.items.find(
|
||||
(policy: { policy_id: string }) => policy.policy_id === 'fleet-server-policy'
|
||||
);
|
||||
|
||||
expect(shardPolicy.inputs[0].config.osquery.value.packs[shardPack]).to.deep.equal({
|
||||
shard: 15,
|
||||
queries: {},
|
||||
});
|
||||
});
|
||||
cy.contains(shardPack).click();
|
||||
cy.contains('Edit').click();
|
||||
cy.get('#shardsPercentage0').should('have.value', '15');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
findFormFieldByRowsLabelAndType,
|
||||
submitQuery,
|
||||
} from '../../tasks/live_query';
|
||||
import { preparePack } from '../../tasks/packs';
|
||||
import { closeModalIfVisible } from '../../tasks/integrations';
|
||||
import { navigateTo } from '../../tasks/navigation';
|
||||
|
||||
|
@ -36,7 +35,9 @@ describe('Alert_Test', () => {
|
|||
const PACK_NAME = 'testpack';
|
||||
const RULE_NAME = 'Test-rule';
|
||||
navigateTo('/app/osquery');
|
||||
preparePack(PACK_NAME);
|
||||
cy.contains('Packs').click();
|
||||
cy.getBySel('pagination-button-next').click();
|
||||
cy.contains(PACK_NAME).click();
|
||||
findAndClickButton('Edit');
|
||||
cy.contains(`Edit ${PACK_NAME}`);
|
||||
findFormFieldByRowsLabelAndType(
|
||||
|
@ -45,7 +46,9 @@ describe('Alert_Test', () => {
|
|||
);
|
||||
findAndClickButton('Update pack');
|
||||
closeModalIfVisible();
|
||||
cy.contains(PACK_NAME);
|
||||
cy.contains(`Successfully updated "${PACK_NAME}" pack`);
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
|
||||
cy.visit('/app/security/rules');
|
||||
cy.contains(RULE_NAME).click();
|
||||
cy.wait(2000);
|
||||
|
@ -73,6 +76,7 @@ describe('Alert_Test', () => {
|
|||
cy.visit('/app/security/alerts');
|
||||
cy.getBySel('expand-event').first().click();
|
||||
|
||||
cy.wait(500);
|
||||
cy.contains('Get processes').click();
|
||||
submitQuery();
|
||||
checkResults();
|
||||
|
@ -86,6 +90,7 @@ describe('Alert_Test', () => {
|
|||
cy.visit('/app/security/alerts');
|
||||
cy.getBySel('expand-event').first().click();
|
||||
|
||||
cy.wait(500);
|
||||
cy.contains('Get processes').click();
|
||||
|
||||
cy.intercept('POST', '/api/osquery/live_queries', (req) => {
|
||||
|
|
|
@ -30,8 +30,18 @@ export const submitQuery = () => {
|
|||
cy.contains('Submit').click();
|
||||
};
|
||||
|
||||
export const checkResults = () =>
|
||||
cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0);
|
||||
// sometimes the results get stuck in the tests, this is a workaround
|
||||
export const checkResults = () => {
|
||||
cy.getBySel('osqueryResultsTable').then(($table) => {
|
||||
if ($table.find('div .euiDataGridRow').length > 0) {
|
||||
cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0);
|
||||
} else {
|
||||
cy.getBySel('osquery-status-tab').click();
|
||||
cy.getBySel('osquery-results-tab').click();
|
||||
cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const typeInECSFieldInput = (text: string) => cy.getBySel('ECS-field-input').type(text);
|
||||
export const typeInOsqueryFieldInput = (text: string) =>
|
||||
|
|
|
@ -81,6 +81,7 @@ export const getSavedQueriesComplexTest = (savedQueryId: string, savedQueryDescr
|
|||
findFormFieldByRowsLabelAndType('Description (optional)', savedQueryDescription);
|
||||
cy.react('EuiButtonDisplay').contains('Save').click();
|
||||
cy.contains('Successfully saved');
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
|
||||
// play saved query
|
||||
cy.contains('Saved queries').click();
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { reduce } from 'lodash';
|
||||
import { filter, isEmpty, map, omit, reduce } from 'lodash';
|
||||
import type { EuiAccordionProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -14,12 +15,15 @@ import {
|
|||
EuiSpacer,
|
||||
EuiBottomBar,
|
||||
EuiHorizontalRule,
|
||||
EuiAccordion,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { FormProvider, useForm as useHookForm } from 'react-hook-form';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { PackShardsField } from './shards/pack_shards_field';
|
||||
import { useRouterNavigate } from '../../common/lib/kibana';
|
||||
import { PolicyIdComboBoxField } from './policy_id_combobox_field';
|
||||
import { QueriesField } from './queries_field';
|
||||
|
@ -32,9 +36,17 @@ import type { PackItem } from '../types';
|
|||
import { NameField } from './name_field';
|
||||
import { DescriptionField } from './description_field';
|
||||
import type { PackQueryFormData } from '../queries/use_pack_query_form';
|
||||
import { PackTypeSelectable } from './shards/pack_type_selectable';
|
||||
|
||||
type PackFormData = Omit<PackItem, 'id' | 'queries'> & { queries: PackQueryFormData[] };
|
||||
|
||||
const StyledEuiAccordion = styled(EuiAccordion)`
|
||||
${({ isDisabled }: { isDisabled?: boolean }) => isDisabled && 'display: none;'}
|
||||
.euiAccordion__button {
|
||||
color: ${({ theme }) => theme.eui.euiColorPrimary};
|
||||
}
|
||||
`;
|
||||
|
||||
interface PackFormProps {
|
||||
defaultValue?: PackItem;
|
||||
editMode?: boolean;
|
||||
|
@ -46,6 +58,13 @@ const PackFormComponent: React.FC<PackFormProps> = ({
|
|||
editMode = false,
|
||||
isReadOnly = false,
|
||||
}) => {
|
||||
const [shardsToggleState, setShardsToggleState] =
|
||||
useState<EuiAccordionProps['forceState']>('closed');
|
||||
const handleToggle = useCallback((isOpen) => {
|
||||
const newState = isOpen ? 'open' : 'closed';
|
||||
setShardsToggleState(newState);
|
||||
}, []);
|
||||
const [packType, setPackType] = useState('policy');
|
||||
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
|
||||
const handleHideConfirmationModal = useCallback(() => setShowConfirmationModal(false), []);
|
||||
|
||||
|
@ -60,16 +79,16 @@ const PackFormComponent: React.FC<PackFormProps> = ({
|
|||
withRedirect: true,
|
||||
});
|
||||
|
||||
const deserializer = (payload: PackItem) => ({
|
||||
...payload,
|
||||
policy_ids: payload.policy_ids ?? [],
|
||||
queries: convertPackQueriesToSO(payload.queries),
|
||||
});
|
||||
const deserializer = (payload: PackItem) => {
|
||||
const defaultPolicyIds = filter(payload.policy_ids, (policyId) => !payload.shards?.[policyId]);
|
||||
|
||||
const serializer = (payload: PackFormData) => ({
|
||||
...payload,
|
||||
queries: convertSOQueriesToPack(payload.queries),
|
||||
});
|
||||
return {
|
||||
...payload,
|
||||
policy_ids: defaultPolicyIds ?? [],
|
||||
queries: convertPackQueriesToSO(payload.queries),
|
||||
shards: omit(payload.shards, '*') ?? {},
|
||||
};
|
||||
};
|
||||
|
||||
const hooksForm = useHookForm({
|
||||
defaultValues: defaultValue
|
||||
|
@ -83,14 +102,68 @@ const PackFormComponent: React.FC<PackFormProps> = ({
|
|||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(defaultValue?.shards)) {
|
||||
if (defaultValue?.shards?.['*']) {
|
||||
setPackType('global');
|
||||
} else {
|
||||
setShardsToggleState('open');
|
||||
}
|
||||
}
|
||||
}, [defaultValue, defaultValue?.shards]);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
} = hooksForm;
|
||||
const { policy_ids: policyIds, shards } = watch();
|
||||
|
||||
const getShards = useCallback(() => {
|
||||
if (packType === 'global') {
|
||||
return { '*': 100 };
|
||||
}
|
||||
|
||||
return reduce(
|
||||
shards,
|
||||
(acc, shard, key) => {
|
||||
if (!isEmpty(key)) {
|
||||
return { ...acc, [key]: shard };
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}, [packType, shards]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: PackFormData) => {
|
||||
const serializer = ({
|
||||
shards: _,
|
||||
policy_ids: payloadPolicyIds,
|
||||
queries,
|
||||
...restPayload
|
||||
}: PackFormData) => {
|
||||
const mappedShards = !isEmpty(shards)
|
||||
? (filter(
|
||||
map(shards, (shard, key) => {
|
||||
if (!isEmpty(key)) {
|
||||
return key;
|
||||
}
|
||||
})
|
||||
) as string[])
|
||||
: [];
|
||||
const policies = [...payloadPolicyIds, ...mappedShards];
|
||||
|
||||
return {
|
||||
...restPayload,
|
||||
policy_ids: policies ?? [],
|
||||
queries: convertSOQueriesToPack(queries),
|
||||
shards: getShards() ?? {},
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
if (editMode && defaultValue?.id) {
|
||||
await updateAsync({ id: defaultValue?.id, ...serializer(values) });
|
||||
|
@ -100,13 +173,11 @@ const PackFormComponent: React.FC<PackFormProps> = ({
|
|||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
},
|
||||
[createAsync, defaultValue?.id, editMode, updateAsync]
|
||||
[createAsync, defaultValue?.id, editMode, getShards, shards, updateAsync]
|
||||
);
|
||||
|
||||
const handleSubmitForm = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]);
|
||||
|
||||
const { policy_ids: policyIds } = watch();
|
||||
|
||||
const agentCount = useMemo(
|
||||
() =>
|
||||
reduce(
|
||||
|
@ -138,6 +209,31 @@ const PackFormComponent: React.FC<PackFormProps> = ({
|
|||
|
||||
const euiFieldProps = useMemo(() => ({ isDisabled: isReadOnly }), [isReadOnly]);
|
||||
|
||||
const changePackType = useCallback(
|
||||
(type: 'global' | 'policy' | 'shards') => {
|
||||
setPackType(type);
|
||||
},
|
||||
[setPackType]
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
Object.entries(agentPoliciesById ?? {}).map(([agentPolicyId, agentPolicy]) => ({
|
||||
key: agentPolicyId,
|
||||
label: agentPolicy.name,
|
||||
})),
|
||||
[agentPoliciesById]
|
||||
);
|
||||
|
||||
const availableOptions = useMemo(() => {
|
||||
const currentShardsFieldValues = map(shards, (shard, key) => key);
|
||||
const currentPolicyIdsFieldValues = map(policyIds, (policy) => policy);
|
||||
|
||||
const currentValues = [...currentShardsFieldValues, ...currentPolicyIdsFieldValues];
|
||||
|
||||
return options.filter(({ key }) => !currentValues.includes(key));
|
||||
}, [shards, policyIds, options]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...hooksForm}>
|
||||
|
@ -154,10 +250,35 @@ const PackFormComponent: React.FC<PackFormProps> = ({
|
|||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<PolicyIdComboBoxField />
|
||||
</EuiFlexItem>
|
||||
<PackTypeSelectable packType={packType} setPackType={changePackType} />
|
||||
</EuiFlexGroup>
|
||||
|
||||
{packType === 'policy' && (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<PolicyIdComboBoxField options={availableOptions} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<StyledEuiAccordion
|
||||
id="shardsToggle"
|
||||
forceState={shardsToggleState}
|
||||
onToggle={handleToggle}
|
||||
buttonContent="Partial deployment (shards)"
|
||||
>
|
||||
<EuiSpacer size="xs" />
|
||||
<PackShardsField options={availableOptions} />
|
||||
</StyledEuiAccordion>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<QueriesField euiFieldProps={euiFieldProps} />
|
||||
|
|
|
@ -32,10 +32,12 @@ const AgentPolicyDescriptionColumn = styled(EuiFlexItem)`
|
|||
|
||||
interface PolicyIdComboBoxFieldProps {
|
||||
euiFieldProps?: EuiComboBoxProps<string>;
|
||||
options: Array<EuiComboBoxOptionOption<string>>;
|
||||
}
|
||||
|
||||
const PolicyIdComboBoxFieldComponent: React.FC<PolicyIdComboBoxFieldProps> = ({
|
||||
euiFieldProps,
|
||||
options,
|
||||
}) => {
|
||||
const { data: { agentPoliciesById } = {} } = useAgentPolicies();
|
||||
|
||||
|
@ -48,15 +50,6 @@ const PolicyIdComboBoxFieldComponent: React.FC<PolicyIdComboBoxFieldProps> = ({
|
|||
rules: {},
|
||||
});
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
Object.entries(agentPoliciesById ?? {}).map(([agentPolicyId, agentPolicy]) => ({
|
||||
key: agentPolicyId,
|
||||
label: agentPolicy.name,
|
||||
})),
|
||||
[agentPoliciesById]
|
||||
);
|
||||
|
||||
const selectedOptions = useMemo(() => {
|
||||
if (agentPoliciesById) {
|
||||
return castArray(value).map((policyId) => ({
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import type { InternalFieldErrors } from 'react-hook-form';
|
||||
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { isEmpty, last, reject } from 'lodash';
|
||||
import { useAgentPolicies } from '../../../agent_policies';
|
||||
import type { ShardsArray } from '../../../../common/schemas/common';
|
||||
import { convertShardsToArray, convertShardsToObject } from '../../../../common/schemas/common';
|
||||
import { ShardsForm } from './shards_form';
|
||||
|
||||
export const defaultShardData = {
|
||||
policy: {
|
||||
label: '',
|
||||
key: '',
|
||||
},
|
||||
percentage: 100,
|
||||
};
|
||||
|
||||
interface PackShardsFieldProps {
|
||||
options: Array<EuiComboBoxOptionOption<string>>;
|
||||
}
|
||||
|
||||
const PackShardsFieldComponent = ({ options }: PackShardsFieldProps) => {
|
||||
const {
|
||||
watch: watchRoot,
|
||||
register: registerRoot,
|
||||
setValue: setValueRoot,
|
||||
formState: { errors: errorsRoot },
|
||||
} = useFormContext();
|
||||
const { data: { agentPoliciesById } = {} } = useAgentPolicies();
|
||||
|
||||
const rootShards = watchRoot('shards');
|
||||
|
||||
const { control, watch, getFieldState, formState, resetField, setValue } = useForm<{
|
||||
shardsArray: ShardsArray;
|
||||
}>({
|
||||
mode: 'all',
|
||||
shouldUnregister: true,
|
||||
defaultValues: {
|
||||
shardsArray: !isEmpty(convertShardsToArray(rootShards, agentPoliciesById))
|
||||
? [...convertShardsToArray(rootShards, agentPoliciesById), defaultShardData]
|
||||
: [defaultShardData],
|
||||
},
|
||||
});
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
control,
|
||||
name: 'shardsArray',
|
||||
});
|
||||
|
||||
const formValue = watch();
|
||||
|
||||
const shardsArrayState = getFieldState('shardsArray', formState);
|
||||
|
||||
useEffect(() => {
|
||||
registerRoot('shards', {
|
||||
validate: () => {
|
||||
const nonEmptyErrors = reject(shardsArrayState.error, isEmpty) as InternalFieldErrors[];
|
||||
|
||||
return !nonEmptyErrors.length;
|
||||
},
|
||||
});
|
||||
}, [shardsArrayState.error, errorsRoot, registerRoot]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch((data, payload) => {
|
||||
if (data?.shardsArray) {
|
||||
const lastShardIndex = data?.shardsArray?.length - 1;
|
||||
if (payload.name?.startsWith(`shardsArray.${lastShardIndex}.`)) {
|
||||
const lastShard = last(data.shardsArray);
|
||||
if (lastShard?.policy?.key) {
|
||||
append(defaultShardData);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [formValue, append, watch]);
|
||||
|
||||
useEffect(() => {
|
||||
const parsedShards = convertShardsToObject(formValue.shardsArray);
|
||||
if (shardsArrayState.isDirty && !deepEqual(parsedShards, rootShards)) {
|
||||
setValueRoot('shards', parsedShards, {
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
}, [setValueRoot, formValue, shardsArrayState.isDirty, rootShards, resetField, setValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{fields.map((item, index, array) => (
|
||||
<div key={item.id}>
|
||||
<ShardsForm
|
||||
index={index}
|
||||
onDelete={remove}
|
||||
isLastItem={index === array.length - 1}
|
||||
control={control}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PackShardsField = React.memo(PackShardsFieldComponent);
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 { EuiCard, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiRadio } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
const StyledEuiCard = styled(EuiCard)`
|
||||
padding: 16px 92px 16px 16px !important;
|
||||
border: ${(props) => {
|
||||
if (props.selectable?.isSelected) {
|
||||
return `1px solid ${props.theme.eui.euiColorPrimary}`;
|
||||
}
|
||||
}};
|
||||
|
||||
.euiTitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.euiText {
|
||||
margin-top: 0;
|
||||
margin-left: 25px;
|
||||
color: ${(props) => props.theme.eui.EuiTextSubduedColor};
|
||||
}
|
||||
|
||||
> button[role='switch'] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
interface PackTypeSelectableProps {
|
||||
packType: string;
|
||||
setPackType: (type: 'global' | 'policy') => void;
|
||||
resetFormFields?: () => void;
|
||||
}
|
||||
|
||||
const PackTypeSelectableComponent = ({
|
||||
packType,
|
||||
setPackType,
|
||||
resetFormFields,
|
||||
}: PackTypeSelectableProps) => {
|
||||
const handleChange = useCallback(
|
||||
(type) => {
|
||||
setPackType(type);
|
||||
if (resetFormFields) {
|
||||
resetFormFields();
|
||||
}
|
||||
},
|
||||
[resetFormFields, setPackType]
|
||||
);
|
||||
const policyCardSelectable = useMemo(
|
||||
() => ({
|
||||
onClick: () => handleChange('policy'),
|
||||
isSelected: packType === 'policy',
|
||||
}),
|
||||
[packType, handleChange]
|
||||
);
|
||||
|
||||
const globalCardSelectable = useMemo(
|
||||
() => ({
|
||||
onClick: () => handleChange('global'),
|
||||
isSelected: packType === 'global',
|
||||
}),
|
||||
[packType, handleChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="Type" fullWidth>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<StyledEuiCard
|
||||
layout="horizontal"
|
||||
title={
|
||||
<EuiRadio
|
||||
id={'osquery_pack_type_policy'}
|
||||
label={i18n.translate('xpack.osquery.pack.form.policyLabel', {
|
||||
defaultMessage: 'Policy',
|
||||
})}
|
||||
onChange={noop}
|
||||
checked={packType === 'policy'}
|
||||
/>
|
||||
}
|
||||
titleSize="xs"
|
||||
hasBorder
|
||||
description={i18n.translate('xpack.osquery.pack.form.policyDescription', {
|
||||
defaultMessage: 'Schedule pack for specific policy.',
|
||||
})}
|
||||
data-test-subj={'osqueryPackTypePolicy'}
|
||||
selectable={policyCardSelectable}
|
||||
{...(packType === 'policy' && { color: 'primary' })}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<StyledEuiCard
|
||||
layout="horizontal"
|
||||
title={
|
||||
<EuiRadio
|
||||
id={'osquery_pack_type_global'}
|
||||
label={i18n.translate('xpack.osquery.pack.form.globalLabel', {
|
||||
defaultMessage: 'Global',
|
||||
})}
|
||||
onChange={noop}
|
||||
checked={packType === 'global'}
|
||||
/>
|
||||
}
|
||||
titleSize="xs"
|
||||
description={i18n.translate('xpack.osquery.pack.form.globalDescription', {
|
||||
defaultMessage: 'Use pack across all policies',
|
||||
})}
|
||||
selectable={globalCardSelectable}
|
||||
data-test-subj={'osqueryPackTypeGlobal'}
|
||||
{...(packType === 'global' && { color: 'primary' })}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const PackTypeSelectable = React.memo(PackTypeSelectableComponent);
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import type { UseFieldArrayRemove, UseFormReturn } from 'react-hook-form';
|
||||
import type { ShardsArray } from '../../../../common/schemas/common/utils';
|
||||
import { ShardsPolicyField } from './shards_policy_field';
|
||||
import { ShardsPercentageField } from './shards_percentage_field';
|
||||
|
||||
const StyledButtonWrapper = styled.div`
|
||||
margin-top: ${(props: { index: number }) => props.index === 0 && '16px'};
|
||||
`;
|
||||
|
||||
export type ShardsFormReturn = UseFormReturn<{ shardsArray: ShardsArray }>;
|
||||
|
||||
interface ShardsFormProps {
|
||||
index: number;
|
||||
isLastItem: boolean;
|
||||
control: ShardsFormReturn['control'];
|
||||
onDelete?: UseFieldArrayRemove;
|
||||
options: Array<EuiComboBoxOptionOption<string>>;
|
||||
}
|
||||
|
||||
const ShardsFormComponent = ({
|
||||
onDelete,
|
||||
index,
|
||||
isLastItem,
|
||||
control,
|
||||
options,
|
||||
}: ShardsFormProps) => {
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
if (onDelete) {
|
||||
onDelete(index);
|
||||
}
|
||||
}, [index, onDelete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup data-test-subj="packShardsForm" alignItems="flexStart" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="s" wrap>
|
||||
<EuiFlexItem>
|
||||
<ShardsPolicyField
|
||||
index={index}
|
||||
control={control}
|
||||
hideLabel={index !== 0}
|
||||
options={options}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={true}>
|
||||
<ShardsPercentageField index={index} control={control} hideLabel={index !== 0} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper index={index}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.osquery.pack.form.deleteShardsRowButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete shards row',
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="text"
|
||||
disabled={isLastItem}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
</StyledButtonWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShardsForm = React.memo(ShardsFormComponent);
|
|
@ -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 React, { useCallback, useMemo } from 'react';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiRange } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ShardsFormReturn } from './shards_form';
|
||||
|
||||
interface ShardsPercentageFieldComponent {
|
||||
index: number;
|
||||
control: ShardsFormReturn['control'];
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
const ShardsPercentageFieldComponent = ({
|
||||
index,
|
||||
control,
|
||||
euiFieldProps,
|
||||
hideLabel,
|
||||
}: ShardsPercentageFieldComponent) => {
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
control,
|
||||
name: `shardsArray.${index}.percentage`,
|
||||
defaultValue: 100,
|
||||
});
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
const numberValue = (e.target as { valueAsNumber: number }).valueAsNumber
|
||||
? (e.target as { valueAsNumber: number }).valueAsNumber
|
||||
: 0;
|
||||
onChange(numberValue);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={
|
||||
hideLabel
|
||||
? ''
|
||||
: i18n.translate('xpack.osquery.pack.form.percentageFieldLabel', {
|
||||
defaultMessage: 'Shard',
|
||||
})
|
||||
}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={10}>
|
||||
<EuiRange
|
||||
data-test-subj="shards-field-percentage"
|
||||
id={'shardsPercentage' + index}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={value}
|
||||
fullWidth={true}
|
||||
showInput={true}
|
||||
showLabels={false}
|
||||
append={'%'}
|
||||
onChange={handleChange}
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShardsPercentageField = React.memo(ShardsPercentageFieldComponent);
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { useController } from 'react-hook-form';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAgentPolicies } from '../../../agent_policies';
|
||||
import type { ShardsFormReturn } from './shards_form';
|
||||
|
||||
interface ShardsPolicyFieldComponent {
|
||||
index: number;
|
||||
control: ShardsFormReturn['control'];
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
hideLabel?: boolean;
|
||||
options: Array<EuiComboBoxOptionOption<string>>;
|
||||
}
|
||||
|
||||
const ShardsPolicyFieldComponent = ({
|
||||
index,
|
||||
control,
|
||||
hideLabel,
|
||||
options,
|
||||
}: ShardsPolicyFieldComponent) => {
|
||||
const { data: { agentPoliciesById } = {} } = useAgentPolicies();
|
||||
|
||||
const policyFieldValidator = useCallback(
|
||||
(policy: { key: string; label: string }) =>
|
||||
!policy
|
||||
? i18n.translate('xpack.osquery.pack.form.shardsPolicyFieldMissingErrorMessage', {
|
||||
defaultMessage: 'Policy is a required field',
|
||||
})
|
||||
: undefined,
|
||||
|
||||
[]
|
||||
);
|
||||
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
control,
|
||||
name: `shardsArray.${index}.policy`,
|
||||
rules: {
|
||||
validate: policyFieldValidator,
|
||||
},
|
||||
});
|
||||
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
const [selectedOptions, setSelected] = useState<EuiComboBoxOptionOption[]>([]);
|
||||
const handleChange = useCallback(
|
||||
(newSelectedOptions: EuiComboBoxOptionOption[]) => {
|
||||
setSelected(newSelectedOptions);
|
||||
onChange(newSelectedOptions[0]);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const foundPolicy = agentPoliciesById?.[value.key];
|
||||
if (value && foundPolicy) {
|
||||
setSelected([{ label: value.label || foundPolicy.name, value: value.key }]);
|
||||
}
|
||||
}, [agentPoliciesById, value]);
|
||||
|
||||
const singleSelectionConfig = useMemo(() => ({ asPlainText: true }), []);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={
|
||||
hideLabel
|
||||
? ''
|
||||
: i18n.translate('xpack.osquery.pack.form.policyFieldLabel', {
|
||||
defaultMessage: 'Policy',
|
||||
})
|
||||
}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
singleSelection={singleSelectionConfig}
|
||||
isInvalid={hasError}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
data-test-subj="shards-field-policy"
|
||||
rowHeight={32}
|
||||
isClearable
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShardsPolicyField = React.memo(ShardsPolicyFieldComponent);
|
|
@ -11,6 +11,7 @@ import type { Draft } from 'immer';
|
|||
import { produce } from 'immer';
|
||||
import { useMemo } from 'react';
|
||||
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
|
||||
import type { Shard } from '../../../common/schemas/common/utils';
|
||||
|
||||
export interface UsePackQueryFormProps {
|
||||
uniqueQueryIds: string[];
|
||||
|
@ -26,6 +27,7 @@ export interface PackSOQueryFormData {
|
|||
platform?: string | undefined;
|
||||
version?: string | undefined;
|
||||
ecs_mapping?: ECSMapping;
|
||||
shards: Shard;
|
||||
}
|
||||
|
||||
export type PackQuerySOECSMapping = Array<{ field: string; value: string }>;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { SavedObject } from '@kbn/core/public';
|
||||
import type { Shard } from '../../common/schemas/common/utils';
|
||||
import type { PackQueryFormData } from './queries/use_pack_query_form';
|
||||
|
||||
export type PackSavedObject = SavedObject<{
|
||||
|
@ -23,4 +24,5 @@ export type PackItem = PackSavedObject['attributes'] & {
|
|||
id: string;
|
||||
policy_ids: string[];
|
||||
read_only?: boolean;
|
||||
shards?: Shard;
|
||||
};
|
||||
|
|
|
@ -43,6 +43,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
{
|
||||
id: 'results',
|
||||
name: 'Results',
|
||||
'data-test-subj': 'osquery-results-tab',
|
||||
content: (
|
||||
<ResultsTable
|
||||
actionId={actionId}
|
||||
|
@ -57,6 +58,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
{
|
||||
id: 'status',
|
||||
name: 'Status',
|
||||
'data-test-subj': 'osquery-status-tab',
|
||||
content: (
|
||||
<ActionResultsSummary actionId={actionId} agentIds={agentIds} expirationDate={endDate} />
|
||||
),
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { CasesUiStart, CasesUiSetup } from '@kbn/cases-plugin/public';
|
||||
import type { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import type {
|
||||
getLazyOsqueryResults,
|
||||
getLazyLiveQueryField,
|
||||
|
@ -47,6 +48,7 @@ export interface StartPlugins {
|
|||
fleet: FleetStart;
|
||||
lens?: LensPublicStart;
|
||||
security: SecurityPluginStart;
|
||||
spaces: SpacesPluginStart;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
cases: CasesUiStart;
|
||||
timelines: TimelinesUIStart;
|
||||
|
|
|
@ -14,6 +14,8 @@ export interface IQueryPayload {
|
|||
};
|
||||
}
|
||||
|
||||
export type SOShard = Array<{ key: string; value: number }>;
|
||||
|
||||
export interface PackSavedObjectAttributes {
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
|
@ -33,6 +35,7 @@ export interface PackSavedObjectAttributes {
|
|||
updated_at: string;
|
||||
updated_by: string | undefined;
|
||||
policy_ids?: string[];
|
||||
shards: SOShard;
|
||||
}
|
||||
|
||||
export type PackSavedObject = SavedObject<PackSavedObjectAttributes>;
|
||||
|
|
|
@ -123,6 +123,10 @@ export const packSavedObjectMappings: SavedObjectsType['mappings'] = {
|
|||
enabled: {
|
||||
type: 'boolean',
|
||||
},
|
||||
shards: {
|
||||
type: 'object',
|
||||
enabled: false,
|
||||
},
|
||||
version: {
|
||||
type: 'long',
|
||||
},
|
||||
|
@ -195,6 +199,10 @@ export const packAssetSavedObjectMappings: SavedObjectsType['mappings'] = {
|
|||
version: {
|
||||
type: 'long',
|
||||
},
|
||||
shards: {
|
||||
type: 'object',
|
||||
enabled: false,
|
||||
},
|
||||
queries: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
|
|
92
x-pack/plugins/osquery/server/lib/update_global_packs.ts
Normal file
92
x-pack/plugins/osquery/server/lib/update_global_packs.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ElasticsearchClient,
|
||||
SavedObjectsClient,
|
||||
SavedObjectsFindResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { has, map, mapKeys, set, unset } from 'lodash';
|
||||
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
|
||||
import produce from 'immer';
|
||||
import { convertShardsToObject } from '../routes/utils';
|
||||
import { packSavedObjectType } from '../../common/types';
|
||||
import type { OsqueryAppContextService } from './osquery_app_context_services';
|
||||
import type { PackSavedObjectAttributes } from '../common/types';
|
||||
import { convertSOQueriesToPackConfig } from '../routes/pack/utils';
|
||||
import type { PackSavedObject } from '../common/types';
|
||||
|
||||
export const updateGlobalPacksCreateCallback = async (
|
||||
packagePolicy: PackagePolicy,
|
||||
packsClient: SavedObjectsClient,
|
||||
allPacks: SavedObjectsFindResponse<PackSavedObjectAttributes>,
|
||||
osqueryContext: OsqueryAppContextService,
|
||||
esClient: ElasticsearchClient
|
||||
) => {
|
||||
const agentPolicyService = osqueryContext.getAgentPolicyService();
|
||||
|
||||
const packagePolicyService = osqueryContext.getPackagePolicyService();
|
||||
const agentPoliciesResult = await agentPolicyService?.getByIds(packsClient, [
|
||||
packagePolicy.policy_id,
|
||||
]);
|
||||
const agentPolicyResultIds = map(agentPoliciesResult, 'id');
|
||||
const agentPolicies = agentPoliciesResult
|
||||
? mapKeys(await agentPolicyService?.getByIds(packsClient, agentPolicyResultIds), 'id')
|
||||
: {};
|
||||
|
||||
const packsContainingShardForPolicy: PackSavedObject[] = [];
|
||||
allPacks.saved_objects.map((pack) => {
|
||||
const shards = convertShardsToObject(pack.attributes.shards);
|
||||
|
||||
return map(shards, (shard, shardName) => {
|
||||
if (shardName === '*') {
|
||||
packsContainingShardForPolicy.push(pack);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
map(packsContainingShardForPolicy, (pack) => {
|
||||
packsClient.update(
|
||||
packSavedObjectType,
|
||||
pack.id,
|
||||
{},
|
||||
{
|
||||
references: [
|
||||
...pack.references,
|
||||
{
|
||||
id: packagePolicy.policy_id,
|
||||
name: agentPolicies[packagePolicy.policy_id]?.name,
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
await packagePolicyService?.update(
|
||||
packsClient,
|
||||
esClient,
|
||||
packagePolicy.id,
|
||||
produce<PackagePolicy>(packagePolicy, (draft) => {
|
||||
unset(draft, 'id');
|
||||
if (!has(draft, 'inputs[0].streams')) {
|
||||
set(draft, 'inputs[0].streams', []);
|
||||
}
|
||||
|
||||
map(packsContainingShardForPolicy, (pack) => {
|
||||
set(draft, `inputs[0].config.osquery.value.packs.${pack.attributes.name}`, {
|
||||
shard: 100,
|
||||
queries: convertSOQueriesToPackConfig(pack.attributes.queries),
|
||||
});
|
||||
});
|
||||
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
};
|
|
@ -17,6 +17,9 @@ import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
|||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||
import type { DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import type { PackSavedObjectAttributes } from './common/types';
|
||||
import { updateGlobalPacksCreateCallback } from './lib/update_global_packs';
|
||||
import { packSavedObjectType } from '../common/types';
|
||||
import type { CreateLiveQueryRequestBodySchema } from '../common/schemas/routes/live_query';
|
||||
import { createConfig } from './create_config';
|
||||
import type { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types';
|
||||
|
@ -118,9 +121,10 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
);
|
||||
const client = new SavedObjectsClient(core.savedObjects.createInternalRepository());
|
||||
|
||||
const esClient = core.elasticsearch.client.asInternalUser;
|
||||
const dataViewsService = await plugins.dataViews.dataViewsServiceFactory(
|
||||
client,
|
||||
core.elasticsearch.client.asInternalUser,
|
||||
esClient,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
@ -136,6 +140,20 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
async (packagePolicy: PackagePolicy): Promise<PackagePolicy> => {
|
||||
if (packagePolicy.package?.name === OSQUERY_INTEGRATION_NAME) {
|
||||
await this.initialize(core, dataViewsService);
|
||||
|
||||
const allPacks = await client.find<PackSavedObjectAttributes>({
|
||||
type: packSavedObjectType,
|
||||
});
|
||||
|
||||
if (allPacks.saved_objects) {
|
||||
await updateGlobalPacksCreateCallback(
|
||||
packagePolicy,
|
||||
client,
|
||||
allPacks,
|
||||
this.osqueryAppContextService,
|
||||
esClient
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return packagePolicy;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { has, mapKeys, set, unset, find, some } from 'lodash';
|
||||
import { has, set, unset, find, some, mapKeys } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { produce } from 'immer';
|
||||
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
|
@ -19,8 +19,13 @@ import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
|
|||
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
|
||||
import { PLUGIN_ID } from '../../../common';
|
||||
import { packSavedObjectType } from '../../../common/types';
|
||||
import { convertPackQueriesToSO, convertSOQueriesToPackConfig } from './utils';
|
||||
import { getInternalSavedObjectsClient } from '../utils';
|
||||
import {
|
||||
convertSOQueriesToPackConfig,
|
||||
convertPackQueriesToSO,
|
||||
findMatchingShards,
|
||||
getInitialPolicies,
|
||||
} from './utils';
|
||||
import { convertShardsToArray, getInternalSavedObjectsClient } from '../utils';
|
||||
import type { PackSavedObjectAttributes } from '../../common/types';
|
||||
|
||||
export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
|
||||
|
@ -34,6 +39,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
description: schema.maybe(schema.string()),
|
||||
enabled: schema.maybe(schema.boolean()),
|
||||
policy_ids: schema.maybe(schema.arrayOf(schema.string())),
|
||||
shards: schema.recordOf(schema.string(), schema.number()),
|
||||
queries: schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
|
@ -75,8 +81,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { name, description, queries, enabled, policy_ids } = request.body;
|
||||
|
||||
const { name, description, queries, enabled, policy_ids, shards } = request.body;
|
||||
const conflictingEntries = await savedObjectsClient.find({
|
||||
type: packSavedObjectType,
|
||||
filter: `${packSavedObjectType}.attributes.name: "${name}"`,
|
||||
|
@ -98,17 +103,22 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
}
|
||||
)) ?? { items: [] };
|
||||
|
||||
const agentPolicies = policy_ids
|
||||
? mapKeys(await agentPolicyService?.getByIds(internalSavedObjectsClient, policy_ids), 'id')
|
||||
: {};
|
||||
const policiesList = getInitialPolicies(packagePolicies, policy_ids, shards);
|
||||
|
||||
const references = policy_ids
|
||||
? policy_ids.map((policyId: string) => ({
|
||||
id: policyId,
|
||||
name: agentPolicies[policyId].name,
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
}))
|
||||
: [];
|
||||
const agentPolicies = await agentPolicyService?.getByIds(
|
||||
internalSavedObjectsClient,
|
||||
policiesList
|
||||
);
|
||||
|
||||
const policyShards = findMatchingShards(agentPolicies, shards);
|
||||
|
||||
const agentPoliciesIdMap = mapKeys(agentPolicies, 'id');
|
||||
|
||||
const references = policiesList.map((id) => ({
|
||||
id,
|
||||
name: agentPoliciesIdMap[id]?.name,
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
}));
|
||||
|
||||
const packSO = await savedObjectsClient.create<PackSavedObjectAttributes>(
|
||||
packSavedObjectType,
|
||||
|
@ -121,6 +131,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
created_by: currentUser,
|
||||
updated_at: moment().toISOString(),
|
||||
updated_by: currentUser,
|
||||
shards: convertShardsToArray(shards),
|
||||
},
|
||||
{
|
||||
references,
|
||||
|
@ -128,9 +139,9 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
}
|
||||
);
|
||||
|
||||
if (enabled && policy_ids?.length) {
|
||||
if (enabled && policiesList.length) {
|
||||
await Promise.all(
|
||||
policy_ids.map((agentPolicyId) => {
|
||||
policiesList.map((agentPolicyId) => {
|
||||
const packagePolicy = find(packagePolicies, ['policy_id', agentPolicyId]);
|
||||
if (packagePolicy) {
|
||||
return packagePolicyService?.update(
|
||||
|
@ -144,6 +155,9 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
}
|
||||
|
||||
set(draft, `inputs[0].config.osquery.value.packs.${packSO.attributes.name}`, {
|
||||
shard: policyShards[packagePolicy.policy_id]
|
||||
? policyShards[packagePolicy.policy_id]
|
||||
: 100,
|
||||
queries: convertSOQueriesToPackConfig(queries),
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { PLUGIN_ID } from '../../../common';
|
|||
|
||||
import { packSavedObjectType } from '../../../common/types';
|
||||
import { convertSOQueriesToPack } from './utils';
|
||||
import { convertShardsToObject } from '../utils';
|
||||
|
||||
export const readPackRoute = (router: IRouter) => {
|
||||
router.get(
|
||||
|
@ -45,6 +46,7 @@ export const readPackRoute = (router: IRouter) => {
|
|||
...rest,
|
||||
...attributes,
|
||||
queries: convertSOQueriesToPack(attributes.queries),
|
||||
shards: convertShardsToObject(attributes.shards),
|
||||
policy_ids: policyIds,
|
||||
read_only: attributes.version !== undefined && osqueryPackAssetReference,
|
||||
},
|
||||
|
|
|
@ -6,7 +6,19 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { set, unset, has, difference, filter, find, map, mapKeys, uniq, some } from 'lodash';
|
||||
import {
|
||||
set,
|
||||
unset,
|
||||
has,
|
||||
difference,
|
||||
filter,
|
||||
find,
|
||||
map,
|
||||
mapKeys,
|
||||
uniq,
|
||||
some,
|
||||
isEmpty,
|
||||
} from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { produce } from 'immer';
|
||||
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
|
@ -24,8 +36,11 @@ import {
|
|||
convertSOQueriesToPack,
|
||||
convertPackQueriesToSO,
|
||||
convertSOQueriesToPackConfig,
|
||||
getInitialPolicies,
|
||||
findMatchingShards,
|
||||
} from './utils';
|
||||
import { getInternalSavedObjectsClient } from '../utils';
|
||||
|
||||
import { convertShardsToArray, getInternalSavedObjectsClient } from '../utils';
|
||||
import type { PackSavedObjectAttributes } from '../../common/types';
|
||||
|
||||
export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
|
||||
|
@ -45,6 +60,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
description: schema.maybe(schema.string()),
|
||||
enabled: schema.maybe(schema.boolean()),
|
||||
policy_ids: schema.maybe(schema.arrayOf(schema.string())),
|
||||
shards: schema.maybe(schema.recordOf(schema.string(), schema.number())),
|
||||
queries: schema.maybe(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
|
@ -87,7 +103,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { name, description, queries, enabled, policy_ids } = request.body;
|
||||
const { name, description, queries, enabled, policy_ids, shards = {} } = request.body;
|
||||
|
||||
const currentPackSO = await savedObjectsClient.get<{ name: string; enabled: boolean }>(
|
||||
packSavedObjectType,
|
||||
|
@ -121,32 +137,40 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
const currentPackagePolicies = filter(packagePolicies, (packagePolicy) =>
|
||||
has(packagePolicy, `inputs[0].config.osquery.value.packs.${currentPackSO.attributes.name}`)
|
||||
);
|
||||
const agentPolicies = policy_ids
|
||||
? mapKeys(await agentPolicyService?.getByIds(internalSavedObjectsClient, policy_ids), 'id')
|
||||
: {};
|
||||
const agentPolicyIds = Object.keys(agentPolicies);
|
||||
|
||||
const policiesList = getInitialPolicies(packagePolicies, policy_ids, shards);
|
||||
|
||||
const agentPolicies = await agentPolicyService?.getByIds(
|
||||
internalSavedObjectsClient,
|
||||
policiesList
|
||||
);
|
||||
|
||||
const policyShards = findMatchingShards(agentPolicies, shards);
|
||||
|
||||
const agentPoliciesIdMap = mapKeys(agentPolicies, 'id');
|
||||
|
||||
const nonAgentPolicyReferences = filter(
|
||||
currentPackSO.references,
|
||||
(reference) => reference.type !== AGENT_POLICY_SAVED_OBJECT_TYPE
|
||||
);
|
||||
|
||||
const getUpdatedReferences = () => {
|
||||
if (policy_ids) {
|
||||
return [
|
||||
...nonAgentPolicyReferences,
|
||||
...policy_ids.map((id) => ({
|
||||
id,
|
||||
name: agentPolicies[id].name,
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
})),
|
||||
];
|
||||
if (!policy_ids && isEmpty(shards)) {
|
||||
return currentPackSO.references;
|
||||
}
|
||||
|
||||
return currentPackSO.references;
|
||||
return [
|
||||
...nonAgentPolicyReferences,
|
||||
...policiesList.map((id) => ({
|
||||
id,
|
||||
name: agentPoliciesIdMap[id]?.name,
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
||||
await savedObjectsClient.update(
|
||||
const references = getUpdatedReferences();
|
||||
|
||||
await savedObjectsClient.update<PackSavedObjectAttributes>(
|
||||
packSavedObjectType,
|
||||
request.params.id,
|
||||
{
|
||||
|
@ -156,10 +180,11 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
queries: queries && convertPackQueriesToSO(queries),
|
||||
updated_at: moment().toISOString(),
|
||||
updated_by: currentUser,
|
||||
shards: convertShardsToArray(shards),
|
||||
},
|
||||
{
|
||||
refresh: 'wait_for',
|
||||
references: getUpdatedReferences(),
|
||||
references,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -167,7 +192,6 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
filter(currentPackSO.references, ['type', AGENT_POLICY_SAVED_OBJECT_TYPE]),
|
||||
'id'
|
||||
);
|
||||
|
||||
const updatedPackSO = await savedObjectsClient.get<{
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
|
@ -182,7 +206,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
|
||||
if (enabled != null && enabled !== currentPackSO.attributes.enabled) {
|
||||
if (enabled) {
|
||||
const policyIds = policy_ids ? agentPolicyIds : currentAgentPolicyIds;
|
||||
const policyIds = policy_ids || !isEmpty(shards) ? policiesList : currentAgentPolicyIds;
|
||||
|
||||
await Promise.all(
|
||||
policyIds.map((agentPolicyId) => {
|
||||
|
@ -237,11 +261,12 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
);
|
||||
}
|
||||
} else {
|
||||
const agentPolicyIdsToRemove = uniq(difference(currentAgentPolicyIds, agentPolicyIds));
|
||||
// TODO double check if policiesList shouldnt be changed into policyIds
|
||||
const agentPolicyIdsToRemove = uniq(difference(currentAgentPolicyIds, policiesList));
|
||||
const agentPolicyIdsToUpdate = uniq(
|
||||
difference(currentAgentPolicyIds, agentPolicyIdsToRemove)
|
||||
);
|
||||
const agentPolicyIdsToAdd = uniq(difference(agentPolicyIds, currentAgentPolicyIds));
|
||||
const agentPolicyIdsToAdd = uniq(difference(policiesList, currentAgentPolicyIds));
|
||||
|
||||
await Promise.all(
|
||||
agentPolicyIdsToRemove.map((agentPolicyId) => {
|
||||
|
@ -287,6 +312,9 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
draft,
|
||||
`inputs[0].config.osquery.value.packs.${updatedPackSO.attributes.name}`,
|
||||
{
|
||||
shard: policyShards[packagePolicy.policy_id]
|
||||
? policyShards[packagePolicy.policy_id]
|
||||
: 100,
|
||||
queries: convertSOQueriesToPackConfig(updatedPackSO.attributes.queries),
|
||||
}
|
||||
);
|
||||
|
@ -317,6 +345,9 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
|
|||
draft,
|
||||
`inputs[0].config.osquery.value.packs.${updatedPackSO.attributes.name}`,
|
||||
{
|
||||
shard: policyShards[packagePolicy.policy_id]
|
||||
? policyShards[packagePolicy.policy_id]
|
||||
: 100,
|
||||
queries: convertSOQueriesToPackConfig(updatedPackSO.attributes.queries),
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty, pick, reduce, isArray } from 'lodash';
|
||||
import { isEmpty, pick, reduce, isArray, filter, uniq, map, mapKeys } from 'lodash';
|
||||
import { satisfies } from 'semver';
|
||||
import type { AgentPolicy, PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import type { Shard } from '../../../common/schemas/common/utils';
|
||||
import { DEFAULT_PLATFORM } from '../../../common/constants';
|
||||
import { removeMultilines } from '../../../common/utils/build_query/remove_multilines';
|
||||
import { convertECSMappingToArray, convertECSMappingToObject } from '../utils';
|
||||
|
@ -88,3 +91,35 @@ export const convertSOQueriesToPackConfig = (
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{} as Record<string, any>
|
||||
);
|
||||
|
||||
export const getInitialPolicies = (
|
||||
packagePolicies: PackagePolicy[] | never[],
|
||||
policyIds: string[] = [],
|
||||
shards?: Shard
|
||||
) => {
|
||||
// we want to find all policies, because this is a global pack
|
||||
if (shards?.['*']) {
|
||||
const supportedPackagePolicyIds = filter(packagePolicies, (packagePolicy) =>
|
||||
satisfies(packagePolicy.package?.version ?? '', '>=0.6.0')
|
||||
);
|
||||
|
||||
return uniq(map(supportedPackagePolicyIds, 'policy_id'));
|
||||
}
|
||||
|
||||
return policyIds;
|
||||
};
|
||||
|
||||
export const findMatchingShards = (agentPolicies: AgentPolicy[] | undefined, shards?: Shard) => {
|
||||
const policyShards: Shard = {};
|
||||
if (!isEmpty(shards)) {
|
||||
const agentPoliciesIdMap = mapKeys(agentPolicies, 'id');
|
||||
|
||||
map(shards, (shard, shardName) => {
|
||||
if (agentPoliciesIdMap[shardName]) {
|
||||
policyShards[agentPoliciesIdMap[shardName].id] = shard;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return policyShards;
|
||||
};
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import type { CoreSetup } from '@kbn/core/server';
|
||||
import { SavedObjectsClient } from '@kbn/core/server';
|
||||
import { reduce } from 'lodash';
|
||||
import type { Shard } from '../../common/schemas/common/utils';
|
||||
import type { SOShard } from '../common/types';
|
||||
|
||||
export const convertECSMappingToArray = (ecsMapping: Record<string, object> | undefined) =>
|
||||
ecsMapping
|
||||
|
@ -30,6 +32,23 @@ export const convertECSMappingToObject = (
|
|||
{} as Record<string, { field?: string; value?: string }>
|
||||
);
|
||||
|
||||
export const convertShardsToArray = (shards: Shard): SOShard =>
|
||||
Object.entries(shards).map((item) => ({
|
||||
key: item[0],
|
||||
value: item[1],
|
||||
}));
|
||||
|
||||
export const convertShardsToObject = (shards: Array<{ key: string; value: number }>) =>
|
||||
reduce(
|
||||
shards,
|
||||
(acc, value) => {
|
||||
acc[value.key] = value.value;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
export const getInternalSavedObjectsClient = async (
|
||||
getStartServices: CoreSetup['getStartServices']
|
||||
) => {
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
*/
|
||||
|
||||
export async function getLatestVersion(): Promise<string> {
|
||||
return '8.6.0-SNAPSHOT';
|
||||
return '8.5.1-SNAPSHOT';
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue