[Osquery] Global packs (#143948)

This commit is contained in:
Tomasz Ciecierski 2022-11-14 17:18:04 +01:00 committed by GitHub
parent 2b2e1d19d2
commit 00a7cf6cb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1076 additions and 78 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import 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);

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import type { Draft } from 'immer';
import { produce } from 'immer';
import { useMemo } from 'react';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import 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 }>;

View file

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

View file

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

View file

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

View file

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

View file

@ -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: {

View 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;
})
);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,5 +6,5 @@
*/
export async function getLatestVersion(): Promise<string> {
return '8.6.0-SNAPSHOT';
return '8.5.1-SNAPSHOT';
}