mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Osquery] 7.14 bug squash (#105387)
This commit is contained in:
parent
32206b9284
commit
dd159f1c9e
16 changed files with 162 additions and 63 deletions
|
@ -53,6 +53,18 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
sortField: '@timestamp',
|
||||
isLive,
|
||||
});
|
||||
if (expired) {
|
||||
// @ts-expect-error update types
|
||||
edges.forEach((edge) => {
|
||||
if (!edge.fields.completed_at) {
|
||||
edge.fields['error.keyword'] = edge.fields.error = [
|
||||
i18n.translate('xpack.osquery.liveQueryActionResults.table.expiredErrorText', {
|
||||
defaultMessage: 'The action request timed out.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { data: logsResults } = useAllResults({
|
||||
actionId,
|
||||
|
|
|
@ -34,8 +34,7 @@ export const useAllAgents = (
|
|||
const { isLoading: agentsLoading, data: agentData } = useQuery<GetAgentsResponse>(
|
||||
['agents', osqueryPolicies, searchValue, perPage],
|
||||
() => {
|
||||
const policyFragment = osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ');
|
||||
let kuery = `last_checkin_status: online and (${policyFragment})`;
|
||||
let kuery = `${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')}`;
|
||||
|
||||
if (searchValue) {
|
||||
kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`;
|
||||
|
|
|
@ -55,8 +55,8 @@ const SavedQueriesPageComponent = () => {
|
|||
const { push } = useHistory();
|
||||
const newQueryLinkProps = useRouterNavigate('saved_queries/new');
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [sortField, setSortField] = useState('updated_at');
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [sortField, setSortField] = useState('attributes.updated_at');
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
|
||||
const { data } = useSavedQueries({ isLive: true });
|
||||
|
|
|
@ -9,9 +9,11 @@ import { isArray } from 'lodash';
|
|||
import uuid from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useForm } from '../../shared_imports';
|
||||
import { formSchema } from '../../scheduled_query_groups/queries/schema';
|
||||
import { createFormSchema } from '../../scheduled_query_groups/queries/schema';
|
||||
import { ScheduledQueryGroupFormData } from '../../scheduled_query_groups/queries/use_scheduled_query_group_query_form';
|
||||
import { useSavedQueries } from '../use_saved_queries';
|
||||
|
||||
const SAVED_QUERY_FORM_ID = 'savedQueryForm';
|
||||
|
||||
|
@ -20,11 +22,29 @@ interface UseSavedQueryFormProps {
|
|||
handleSubmit: (payload: unknown) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryFormProps) =>
|
||||
useForm({
|
||||
export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryFormProps) => {
|
||||
const { data } = useSavedQueries({});
|
||||
const ids: string[] = useMemo<string[]>(
|
||||
() => data?.savedObjects.map((obj) => obj.attributes.id) ?? [],
|
||||
[data]
|
||||
);
|
||||
const idSet = useMemo<Set<string>>(() => {
|
||||
const res = new Set<string>(ids);
|
||||
// @ts-expect-error update types
|
||||
if (defaultValue && defaultValue.id) res.delete(defaultValue.id);
|
||||
return res;
|
||||
}, [ids, defaultValue]);
|
||||
const formSchema = useMemo<ReturnType<typeof createFormSchema>>(() => createFormSchema(idSet), [
|
||||
idSet,
|
||||
]);
|
||||
return useForm({
|
||||
id: SAVED_QUERY_FORM_ID + uuid.v4(),
|
||||
schema: formSchema,
|
||||
onSubmit: handleSubmit,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (isValid) {
|
||||
return handleSubmit(formData);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
|
@ -62,3 +82,4 @@ export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryF
|
|||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -37,6 +37,16 @@ export const useCreateSavedQuery = ({ withRedirect }: UseCreateSavedQueryProps)
|
|||
throw new Error('CurrentUser is missing');
|
||||
}
|
||||
|
||||
const conflictingEntries = await savedObjects.client.find({
|
||||
type: savedQuerySavedObjectType,
|
||||
// @ts-expect-error update types
|
||||
search: payload.id,
|
||||
searchFields: ['id'],
|
||||
});
|
||||
if (conflictingEntries.savedObjects.length) {
|
||||
// @ts-expect-error update types
|
||||
throw new Error(`Saved query with id ${payload.id} already exists.`);
|
||||
}
|
||||
return savedObjects.client.create(savedQuerySavedObjectType, {
|
||||
// @ts-expect-error update types
|
||||
...payload,
|
||||
|
|
|
@ -37,6 +37,17 @@ export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps)
|
|||
throw new Error('CurrentUser is missing');
|
||||
}
|
||||
|
||||
const conflictingEntries = await savedObjects.client.find({
|
||||
type: savedQuerySavedObjectType,
|
||||
// @ts-expect-error update types
|
||||
search: payload.id,
|
||||
searchFields: ['id'],
|
||||
});
|
||||
if (conflictingEntries.savedObjects.length) {
|
||||
// @ts-expect-error update types
|
||||
throw new Error(`Saved query with id ${payload.id} already exists.`);
|
||||
}
|
||||
|
||||
return savedObjects.client.update(savedQuerySavedObjectType, savedQueryId, {
|
||||
// @ts-expect-error update types
|
||||
...payload,
|
||||
|
|
|
@ -216,6 +216,20 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
field.value,
|
||||
]);
|
||||
|
||||
const uniqueQueryIds = useMemo<string[]>(
|
||||
() =>
|
||||
field.value && field.value[0].streams.length
|
||||
? field.value[0].streams.reduce((acc, stream) => {
|
||||
if (stream.vars?.id.value) {
|
||||
acc.push(stream.vars?.id.value);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as string[])
|
||||
: [],
|
||||
[field.value]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
|
@ -256,6 +270,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
{<OsqueryPackUploader onChange={handlePackUpload} />}
|
||||
{showAddQueryFlyout && (
|
||||
<QueryFlyout
|
||||
uniqueQueryIds={uniqueQueryIds}
|
||||
integrationPackageVersion={integrationPackageVersion}
|
||||
onSave={handleAddQuery}
|
||||
onClose={handleHideAddFlyout}
|
||||
|
@ -263,6 +278,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
)}
|
||||
{showEditQueryFlyout != null && showEditQueryFlyout >= 0 && (
|
||||
<QueryFlyout
|
||||
uniqueQueryIds={uniqueQueryIds}
|
||||
defaultValue={field.value[0].streams[showEditQueryFlyout]?.vars}
|
||||
integrationPackageVersion={integrationPackageVersion}
|
||||
onSave={handleEditQuery}
|
||||
|
|
|
@ -40,6 +40,7 @@ import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown
|
|||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface QueryFlyoutProps {
|
||||
uniqueQueryIds: string[];
|
||||
defaultValue?: UseScheduledQueryGroupQueryFormProps['defaultValue'] | undefined;
|
||||
integrationPackageVersion?: string | undefined;
|
||||
onSave: (payload: OsqueryManagerPackagePolicyConfigRecord) => Promise<void>;
|
||||
|
@ -47,6 +48,7 @@ interface QueryFlyoutProps {
|
|||
}
|
||||
|
||||
const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
||||
uniqueQueryIds,
|
||||
defaultValue,
|
||||
integrationPackageVersion,
|
||||
onSave,
|
||||
|
@ -54,6 +56,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
}) => {
|
||||
const [isEditMode] = useState(!!defaultValue);
|
||||
const { form } = useScheduledQueryGroupQueryForm({
|
||||
uniqueQueryIds,
|
||||
defaultValue,
|
||||
handleSubmit: (payload, isValid) =>
|
||||
new Promise((resolve) => {
|
||||
|
@ -65,7 +68,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
}),
|
||||
});
|
||||
|
||||
/* Platform and version fields are supported since osquer_manger@0.3.0 */
|
||||
/* Platform and version fields are supported since osquery_manager@0.3.0 */
|
||||
const isFieldSupported = useMemo(
|
||||
() => (integrationPackageVersion ? satisfies(integrationPackageVersion, '>=0.3.0') : false),
|
||||
[integrationPackageVersion]
|
||||
|
|
|
@ -12,15 +12,19 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
|
||||
import { FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
|
||||
import {
|
||||
createIdFieldValidations,
|
||||
intervalFieldValidation,
|
||||
queryFieldValidation,
|
||||
} from './validations';
|
||||
|
||||
export const formSchema = {
|
||||
export const createFormSchema = (ids: Set<string>) => ({
|
||||
id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
validations: idFieldValidations.map((validator) => ({ validator })),
|
||||
validations: createIdFieldValidations(ids).map((validator) => ({ validator })),
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
|
@ -69,4 +73,4 @@ export const formSchema = {
|
|||
) as unknown) as string,
|
||||
validations: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -5,17 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isArray } from 'lodash';
|
||||
import { isArray, xor } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { FormConfig, useForm } from '../../shared_imports';
|
||||
import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
|
||||
import { formSchema } from './schema';
|
||||
import { createFormSchema } from './schema';
|
||||
|
||||
const FORM_ID = 'editQueryFlyoutForm';
|
||||
|
||||
export interface UseScheduledQueryGroupQueryFormProps {
|
||||
uniqueQueryIds: string[];
|
||||
defaultValue?: OsqueryManagerPackagePolicyConfigRecord | undefined;
|
||||
handleSubmit: FormConfig<
|
||||
OsqueryManagerPackagePolicyConfigRecord,
|
||||
|
@ -32,12 +34,26 @@ export interface ScheduledQueryGroupFormData {
|
|||
}
|
||||
|
||||
export const useScheduledQueryGroupQueryForm = ({
|
||||
uniqueQueryIds,
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
}: UseScheduledQueryGroupQueryFormProps) =>
|
||||
useForm<OsqueryManagerPackagePolicyConfigRecord, ScheduledQueryGroupFormData>({
|
||||
}: UseScheduledQueryGroupQueryFormProps) => {
|
||||
const idSet = useMemo<Set<string>>(
|
||||
() =>
|
||||
new Set<string>(xor(uniqueQueryIds, defaultValue?.id.value ? [defaultValue.id.value] : [])),
|
||||
[uniqueQueryIds, defaultValue]
|
||||
);
|
||||
const formSchema = useMemo<ReturnType<typeof createFormSchema>>(() => createFormSchema(idSet), [
|
||||
idSet,
|
||||
]);
|
||||
|
||||
return useForm<OsqueryManagerPackagePolicyConfigRecord, ScheduledQueryGroupFormData>({
|
||||
id: FORM_ID + uuid.v4(),
|
||||
onSubmit: handleSubmit,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (isValid && handleSubmit) {
|
||||
return handleSubmit(formData, isValid);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
|
@ -75,3 +91,4 @@ export const useScheduledQueryGroupQueryForm = ({
|
|||
},
|
||||
schema: formSchema,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -23,13 +23,28 @@ const idSchemaValidation: ValidationFunc<any, string, string> = ({ value }) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const idFieldValidations = [
|
||||
const createUniqueIdValidation = (ids: Set<string>) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const uniqueIdCheck: ValidationFunc<any, string, string> = ({ value }) => {
|
||||
if (ids.has(value)) {
|
||||
return {
|
||||
message: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.uniqueIdError', {
|
||||
defaultMessage: 'ID must be unique',
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
return uniqueIdCheck;
|
||||
};
|
||||
|
||||
export const createIdFieldValidations = (ids: Set<string>) => [
|
||||
fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.emptyIdError', {
|
||||
defaultMessage: 'ID is required',
|
||||
})
|
||||
),
|
||||
idSchemaValidation,
|
||||
createUniqueIdValidation(ids),
|
||||
];
|
||||
|
||||
export const intervalFieldValidation: ValidationFunc<
|
||||
|
|
|
@ -31,7 +31,7 @@ export const useScheduledQueryGroup = ({
|
|||
() => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !skip,
|
||||
enabled: !skip || !scheduledQueryGroupId,
|
||||
select: (response) => response.item,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@ import { usageMetricSavedObjectType } from '../../../common/types';
|
|||
|
||||
import {
|
||||
CounterValue,
|
||||
createMetricObjects,
|
||||
getOrCreateMetricObject,
|
||||
getRouteMetric,
|
||||
incrementCount,
|
||||
RouteString,
|
||||
|
@ -45,31 +45,22 @@ describe('Usage metric recorder', () => {
|
|||
get.mockClear();
|
||||
create.mockClear();
|
||||
});
|
||||
it('should seed route metrics objects', async () => {
|
||||
it('should create metrics that do not exist', async () => {
|
||||
get.mockRejectedValueOnce('stub value');
|
||||
create.mockReturnValueOnce('stub value');
|
||||
const result = await createMetricObjects(savedObjectsClient);
|
||||
const result = await getOrCreateMetricObject(savedObjectsClient, 'live_query');
|
||||
checkGetCalls(get.mock.calls);
|
||||
checkCreateCalls(create.mock.calls);
|
||||
expect(result).toBe(true);
|
||||
expect(result).toBe('stub value');
|
||||
});
|
||||
|
||||
it('should handle previously seeded objects properly', async () => {
|
||||
it('should handle previously created objects properly', async () => {
|
||||
get.mockReturnValueOnce('stub value');
|
||||
create.mockRejectedValueOnce('stub value');
|
||||
const result = await createMetricObjects(savedObjectsClient);
|
||||
const result = await getOrCreateMetricObject(savedObjectsClient, 'live_query');
|
||||
checkGetCalls(get.mock.calls);
|
||||
checkCreateCalls(create.mock.calls, []);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should report failure to create the metrics object', async () => {
|
||||
get.mockRejectedValueOnce('stub value');
|
||||
create.mockRejectedValueOnce('stub value');
|
||||
const result = await createMetricObjects(savedObjectsClient);
|
||||
checkGetCalls(get.mock.calls);
|
||||
checkCreateCalls(create.mock.calls);
|
||||
expect(result).toBe(false);
|
||||
expect(result).toBe('stub value');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -18,30 +18,28 @@ export type RouteString = 'live_query';
|
|||
|
||||
export const routeStrings: RouteString[] = ['live_query'];
|
||||
|
||||
export async function createMetricObjects(soClient: SavedObjectsClientContract) {
|
||||
const res = await Promise.allSettled(
|
||||
routeStrings.map(async (route) => {
|
||||
try {
|
||||
await soClient.get(usageMetricSavedObjectType, route);
|
||||
} catch (e) {
|
||||
await soClient.create(
|
||||
usageMetricSavedObjectType,
|
||||
{
|
||||
errors: 0,
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
id: route,
|
||||
}
|
||||
);
|
||||
export async function getOrCreateMetricObject<T>(
|
||||
soClient: SavedObjectsClientContract,
|
||||
route: string
|
||||
) {
|
||||
try {
|
||||
return await soClient.get<T>(usageMetricSavedObjectType, route);
|
||||
} catch (e) {
|
||||
return await soClient.create(
|
||||
usageMetricSavedObjectType,
|
||||
{
|
||||
errors: 0,
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
id: route,
|
||||
}
|
||||
})
|
||||
);
|
||||
return !res.some((e) => e.status === 'rejected');
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCount(soClient: SavedObjectsClientContract, route: RouteString) {
|
||||
return await soClient.get<LiveQuerySessionUsage>(usageMetricSavedObjectType, route);
|
||||
return await getOrCreateMetricObject<LiveQuerySessionUsage>(soClient, route);
|
||||
}
|
||||
|
||||
export interface CounterValue {
|
||||
|
@ -55,7 +53,7 @@ export async function incrementCount(
|
|||
key: keyof CounterValue = 'count',
|
||||
increment = 1
|
||||
) {
|
||||
const metric = await soClient.get<CounterValue>(usageMetricSavedObjectType, route);
|
||||
const metric = await getOrCreateMetricObject<CounterValue>(soClient, route);
|
||||
metric.attributes[key] += increment;
|
||||
await soClient.update(usageMetricSavedObjectType, route, metric.attributes);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { CoreSetup, SavedObjectsClient } from '../../../../../src/core/server';
|
||||
import { CollectorFetchContext } from '../../../../../src/plugins/usage_collection/server';
|
||||
import { createMetricObjects } from '../routes/usage';
|
||||
import { getBeatUsage, getLiveQueryUsage, getPolicyLevelUsage } from './fetchers';
|
||||
import { CollectorDependencies, usageSchema, UsageData } from './types';
|
||||
|
||||
|
@ -25,10 +24,7 @@ export const registerCollector: RegisterCollector = ({ core, osqueryContext, usa
|
|||
const collector = usageCollection.makeUsageCollector<UsageData>({
|
||||
type: 'osquery',
|
||||
schema: usageSchema,
|
||||
isReady: async () => {
|
||||
const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core));
|
||||
return await createMetricObjects(savedObjectsClient);
|
||||
},
|
||||
isReady: () => true,
|
||||
fetch: async ({ esClient }: CollectorFetchContext): Promise<UsageData> => {
|
||||
const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core));
|
||||
return {
|
||||
|
|
|
@ -45,6 +45,11 @@ export async function getPolicyLevelUsage(
|
|||
const agentResponse = await esClient.search({
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
match: {
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
policied: {
|
||||
filter: {
|
||||
|
@ -87,7 +92,8 @@ export function getScheduledQueryUsage(packagePolicies: ListResult<PackagePolicy
|
|||
return packagePolicies.items.reduce(
|
||||
(acc, item) => {
|
||||
++acc.queryGroups.total;
|
||||
if (item.inputs.length === 0) {
|
||||
const policyAgents = item.inputs.reduce((sum, input) => sum + input.streams.length, 0);
|
||||
if (policyAgents === 0) {
|
||||
++acc.queryGroups.empty;
|
||||
}
|
||||
return acc;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue