mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Asset management] Text updates (#98192)
* updated scheduled query activation toggle text and interval header in query group * added id validation for schedule queries * fixed up agent resolution to ignore inactive agents, and properly pull all agents * nixed unused file * more validation for query fields * added status table to the results data tab, added more validation * updated wording * added error notifications for failed queries * pr feedback and cleanup * fix up last hook * use the pluralize macro, removed rbac tags Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f690c60517
commit
b94f712f8c
32 changed files with 537 additions and 168 deletions
|
@ -12,6 +12,16 @@ export type Name = t.TypeOf<typeof name>;
|
|||
export const nameOrUndefined = t.union([name, t.undefined]);
|
||||
export type NameOrUndefined = t.TypeOf<typeof nameOrUndefined>;
|
||||
|
||||
export const agentSelection = t.type({
|
||||
agents: t.array(t.string),
|
||||
allAgentsSelected: t.boolean,
|
||||
platformsSelected: t.array(t.string),
|
||||
policiesSelected: t.array(t.string),
|
||||
});
|
||||
export type AgentSelection = t.TypeOf<typeof agentSelection>;
|
||||
export const agentSelectionOrUndefined = t.union([agentSelection, t.undefined]);
|
||||
export type AgentSelectionOrUndefined = t.TypeOf<typeof agentSelectionOrUndefined>;
|
||||
|
||||
export const description = t.string;
|
||||
export type Description = t.TypeOf<typeof description>;
|
||||
export const descriptionOrUndefined = t.union([description, t.undefined]);
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
|
||||
import { query, agentSelection } from '../../common/schemas';
|
||||
|
||||
export const createActionRequestBodySchema = t.type({
|
||||
agentSelection,
|
||||
query,
|
||||
});
|
||||
|
||||
export type CreateActionRequestBodySchema = t.OutputOf<typeof createActionRequestBodySchema>;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './create_action_request_body_schema';
|
|
@ -8,6 +8,7 @@
|
|||
import { flatten, reverse, uniqBy } from 'lodash/fp';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
|
@ -32,7 +33,7 @@ export interface ResultsArgs {
|
|||
totalCount: number;
|
||||
}
|
||||
|
||||
interface UseActionResults {
|
||||
export interface UseActionResults {
|
||||
actionId: string;
|
||||
activePage: number;
|
||||
agentIds?: string[];
|
||||
|
@ -55,7 +56,10 @@ export const useActionResults = ({
|
|||
skip = false,
|
||||
isLive = false,
|
||||
}: UseActionResults) => {
|
||||
const { data } = useKibana().services;
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
['actionResults', { actionId }],
|
||||
|
@ -120,6 +124,12 @@ export const useActionResults = ({
|
|||
refetchInterval: isLive ? 1000 : false,
|
||||
keepPreviousData: true,
|
||||
enabled: !skip && !!agentIds?.length,
|
||||
onError: (error: Error) =>
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.osquery.action_results.fetchError', {
|
||||
defaultMessage: 'Error while fetching action results',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
|
@ -32,7 +33,10 @@ interface UseActionDetails {
|
|||
}
|
||||
|
||||
export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseActionDetails) => {
|
||||
const { data } = useKibana().services;
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
['actionDetails', { actionId, filterQuery }],
|
||||
|
@ -57,6 +61,12 @@ export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseAct
|
|||
},
|
||||
{
|
||||
enabled: !skip,
|
||||
onError: (error: Error) =>
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.osquery.action_details.fetchError', {
|
||||
defaultMessage: 'Error while fetching action details',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
|
@ -47,7 +48,10 @@ export const useAllActions = ({
|
|||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllActions) => {
|
||||
const { data } = useKibana().services;
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
['actions', { activePage, direction, limit, sortField }],
|
||||
|
@ -78,6 +82,12 @@ export const useAllActions = ({
|
|||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !skip,
|
||||
onError: (error: Error) =>
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.osquery.all_actions.fetchError', {
|
||||
defaultMessage: 'Error while fetching actions',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
agentPolicyRouteService,
|
||||
|
@ -15,7 +16,10 @@ import {
|
|||
} from '../../../fleet/common';
|
||||
|
||||
export const useAgentPolicies = () => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery<GetAgentPoliciesResponse, unknown, GetAgentPoliciesResponseItem[]>(
|
||||
['agentPolicies'],
|
||||
|
@ -30,6 +34,12 @@ export const useAgentPolicies = () => {
|
|||
placeholderData: [],
|
||||
keepPreviousData: true,
|
||||
select: (response) => response.items,
|
||||
onError: (error) =>
|
||||
toasts.addError(error as Error, {
|
||||
title: i18n.translate('xpack.osquery.agent_policies.fetchError', {
|
||||
defaultMessage: 'Error while fetching agent policies',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { agentPolicyRouteService } from '../../../fleet/common';
|
||||
|
||||
|
@ -16,7 +17,10 @@ interface UseAgentPolicy {
|
|||
}
|
||||
|
||||
export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
['agentPolicy', { policyId }],
|
||||
|
@ -25,6 +29,12 @@ export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => {
|
|||
enabled: !skip,
|
||||
keepPreviousData: true,
|
||||
select: (response) => response.item,
|
||||
onError: (error: Error) =>
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.osquery.agent_policy_details.fetchError', {
|
||||
defaultMessage: 'Error while fetching agent policy details',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { useAgentPolicies } from './use_agent_policies';
|
||||
|
||||
|
@ -24,7 +25,10 @@ interface UseAgentGroups {
|
|||
}
|
||||
|
||||
export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => {
|
||||
const { data } = useKibana().services;
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const { agentPoliciesLoading, agentPolicyById } = useAgentPolicies(osqueryPolicies);
|
||||
const [platforms, setPlatforms] = useState<Group[]>([]);
|
||||
|
@ -96,6 +100,12 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA
|
|||
},
|
||||
{
|
||||
enabled: !osqueryPoliciesLoading && !agentPoliciesLoading,
|
||||
onError: (error) =>
|
||||
toasts.addError(error as Error, {
|
||||
title: i18n.translate('xpack.osquery.agent_groups.fetchError', {
|
||||
defaultMessage: 'Error while fetching agent groups',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -7,17 +7,27 @@
|
|||
|
||||
import { mapKeys } from 'lodash';
|
||||
import { useQueries, UseQueryResult } from 'react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { agentPolicyRouteService, GetOneAgentPolicyResponse } from '../../../fleet/common';
|
||||
|
||||
export const useAgentPolicies = (policyIds: string[] = []) => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const agentResponse = useQueries(
|
||||
policyIds.map((policyId) => ({
|
||||
queryKey: ['agentPolicy', policyId],
|
||||
queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)),
|
||||
enabled: policyIds.length > 0,
|
||||
onError: (error) =>
|
||||
toasts.addError(error as Error, {
|
||||
title: i18n.translate('xpack.osquery.action_policy_details.fetchError', {
|
||||
defaultMessage: 'Error while fetching policy details',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
) as Array<UseQueryResult<GetOneAgentPolicyResponse>>;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { GetAgentStatusResponse, agentRouteService } from '../../../fleet/common';
|
||||
|
@ -16,7 +17,10 @@ interface UseAgentStatus {
|
|||
}
|
||||
|
||||
export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery<GetAgentStatusResponse, unknown, GetAgentStatusResponse['results']>(
|
||||
['agentStatus', policyId],
|
||||
|
@ -34,6 +38,12 @@ export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => {
|
|||
{
|
||||
enabled: !skip,
|
||||
select: (response) => response.results,
|
||||
onError: (error) =>
|
||||
toasts.addError(error as Error, {
|
||||
title: i18n.translate('xpack.osquery.agent_status.fetchError', {
|
||||
defaultMessage: 'Error while fetching agent status',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { GetAgentsResponse, agentRouteService } from '../../../fleet/common';
|
||||
|
@ -27,7 +28,10 @@ export const useAllAgents = (
|
|||
opts: RequestOptions = { perPage: 9000 }
|
||||
) => {
|
||||
const { perPage } = opts;
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const { isLoading: agentsLoading, data: agentData } = useQuery<GetAgentsResponse>(
|
||||
['agents', osqueryPolicies, searchValue, perPage],
|
||||
() => {
|
||||
|
@ -52,6 +56,12 @@ export const useAllAgents = (
|
|||
},
|
||||
{
|
||||
enabled: !osqueryPoliciesLoading,
|
||||
onError: (error) =>
|
||||
toasts.addError(error as Error, {
|
||||
title: i18n.translate('xpack.osquery.agents.fetchError', {
|
||||
defaultMessage: 'Error while fetching agents',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -5,15 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { packagePolicyRouteService, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common';
|
||||
import { OSQUERY_INTEGRATION_NAME } from '../../common';
|
||||
|
||||
export const useOsqueryPolicies = () => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies } = useQuery(
|
||||
const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies = [] } = useQuery(
|
||||
['osqueryPolicies'],
|
||||
() =>
|
||||
http.get(packagePolicyRouteService.getListPath(), {
|
||||
|
@ -21,8 +27,19 @@ export const useOsqueryPolicies = () => {
|
|||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
|
||||
},
|
||||
}),
|
||||
{ select: (data) => data.items.map((p: { policy_id: string }) => p.policy_id) }
|
||||
{
|
||||
select: (response) =>
|
||||
uniq<string>(response.items.map((p: { policy_id: string }) => p.policy_id)),
|
||||
onError: (error: Error) =>
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.osquery.osquery_policies.fetchError', {
|
||||
defaultMessage: 'Error while fetching osquery policies',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return { osqueryPoliciesLoading, osqueryPolicies };
|
||||
return useMemo(() => ({ osqueryPoliciesLoading, osqueryPolicies }), [
|
||||
osqueryPoliciesLoading,
|
||||
osqueryPolicies,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { find } from 'lodash/fp';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
|
@ -13,7 +14,10 @@ import { OSQUERY_INTEGRATION_NAME } from '../../../common';
|
|||
import { useKibana } from '../lib/kibana';
|
||||
|
||||
export const useOsqueryIntegration = () => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
'integrations',
|
||||
|
@ -26,6 +30,12 @@ export const useOsqueryIntegration = () => {
|
|||
{
|
||||
select: ({ response }: GetPackagesResponse) =>
|
||||
find(['name', OSQUERY_INTEGRATION_NAME], response),
|
||||
onError: (error: Error) =>
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.osquery.osquery_integration.fetchError', {
|
||||
defaultMessage: 'Error while fetching osquery integration',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
17
x-pack/plugins/osquery/public/common/validations.ts
Normal file
17
x-pack/plugins/osquery/public/common/validations.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ValidationFunc, fieldValidators } from '../shared_imports';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const queryFieldValidation: ValidationFunc<any, string, string> = fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.emptyQueryError', {
|
||||
defaultMessage: 'Query is a required field',
|
||||
})
|
||||
);
|
|
@ -22,7 +22,7 @@ const QueryAgentResultsComponent = () => {
|
|||
{data?.actionDetails._source?.data?.query}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} agentId={agentId} />
|
||||
<ResultsTable actionId={actionId} selectedAgent={agentId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,14 +12,18 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import React, { useMemo } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { UseField, Form, FormData, useForm, useFormData } from '../../shared_imports';
|
||||
import { UseField, Form, FormData, useForm, useFormData, FIELD_TYPES } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { ResultTabs } from '../../queries/edit/tabs';
|
||||
import { queryFieldValidation } from '../../common/validations';
|
||||
import { fieldValidators } from '../../shared_imports';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
|
||||
export const MAX_QUERY_LENGTH = 2000;
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
defaultValue?: Partial<FormData> | undefined;
|
||||
onSubmit?: (payload: Record<string, string>) => Promise<void>;
|
||||
|
@ -50,9 +54,27 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
}
|
||||
);
|
||||
|
||||
const formSchema = {
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.maxLengthField({
|
||||
length: MAX_QUERY_LENGTH,
|
||||
message: i18n.translate('xpack.osquery.liveQuery.queryForm.largeQueryError', {
|
||||
defaultMessage: 'Query is too large (max {maxLength} characters)',
|
||||
values: { maxLength: MAX_QUERY_LENGTH },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{ validator: queryFieldValidation },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
// schema: formSchema,
|
||||
schema: formSchema,
|
||||
onSubmit: (payload) => {
|
||||
return mutateAsync(payload);
|
||||
},
|
||||
|
@ -60,10 +82,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
stripEmptyFields: false,
|
||||
},
|
||||
defaultValue: defaultValue ?? {
|
||||
query: {
|
||||
id: null,
|
||||
query: '',
|
||||
},
|
||||
query: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -85,16 +104,16 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[agentSelection]
|
||||
);
|
||||
|
||||
const queryValueProvided = useMemo(() => !!query?.query?.length, [query]);
|
||||
const queryValueProvided = useMemo(() => !!query?.length, [query]);
|
||||
|
||||
const queryStatus = useMemo(() => {
|
||||
if (!agentSelected) return 'disabled';
|
||||
if (isError) return 'danger';
|
||||
if (isError || !form.getFields().query.isValid) return 'danger';
|
||||
if (isLoading) return 'loading';
|
||||
if (isSuccess) return 'complete';
|
||||
|
||||
return 'incomplete';
|
||||
}, [agentSelected, isError, isLoading, isSuccess]);
|
||||
}, [agentSelected, isError, isLoading, isSuccess, form]);
|
||||
|
||||
const resultsStatus = useMemo(() => (queryStatus === 'complete' ? 'incomplete' : 'disabled'), [
|
||||
queryStatus,
|
||||
|
|
|
@ -5,86 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// import { find } from 'lodash/fp';
|
||||
// import { EuiCodeBlock, EuiSuperSelect, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
// import { useQuery } from 'react-query';
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { FieldHook } from '../../shared_imports';
|
||||
// import { useKibana } from '../../common/lib/kibana';
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
|
||||
interface LiveQueryQueryFieldProps {
|
||||
disabled?: boolean;
|
||||
field: FieldHook<{
|
||||
id: string | null;
|
||||
query: string;
|
||||
}>;
|
||||
field: FieldHook<string>;
|
||||
}
|
||||
|
||||
const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disabled, field }) => {
|
||||
// const { http } = useKibana().services;
|
||||
// const { data } = useQuery('savedQueryList', () =>
|
||||
// http.get('/internal/osquery/saved_query', {
|
||||
// query: {
|
||||
// pageIndex: 0,
|
||||
// pageSize: 100,
|
||||
// sortField: 'updated_at',
|
||||
// sortDirection: 'desc',
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
// const queryOptions =
|
||||
// // @ts-expect-error update types
|
||||
// data?.saved_objects.map((savedQuery) => ({
|
||||
// value: savedQuery,
|
||||
// inputDisplay: savedQuery.attributes.name,
|
||||
// dropdownDisplay: (
|
||||
// <>
|
||||
// <strong>{savedQuery.attributes.name}</strong>
|
||||
// <EuiText size="s" color="subdued">
|
||||
// <p className="euiTextColor--subdued">{savedQuery.attributes.description}</p>
|
||||
// </EuiText>
|
||||
// <EuiCodeBlock language="sql" fontSize="s" paddingSize="s">
|
||||
// {savedQuery.attributes.query}
|
||||
// </EuiCodeBlock>
|
||||
// </>
|
||||
// ),
|
||||
// })) ?? [];
|
||||
|
||||
const { value, setValue } = field;
|
||||
|
||||
// const handleSavedQueryChange = useCallback(
|
||||
// (newValue) => {
|
||||
// setValue({
|
||||
// id: newValue.id,
|
||||
// query: newValue.attributes.query,
|
||||
// });
|
||||
// },
|
||||
// [setValue]
|
||||
// );
|
||||
const { value, setValue, errors } = field;
|
||||
const error = errors[0]?.message;
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(newValue) => {
|
||||
setValue({
|
||||
id: null,
|
||||
query: newValue,
|
||||
});
|
||||
setValue(newValue);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <EuiSuperSelect
|
||||
valueOfSelected={find(['id', value.id], data?.saved_objects)}
|
||||
options={queryOptions}
|
||||
onChange={handleSavedQueryChange}
|
||||
/>
|
||||
<EuiSpacer /> */}
|
||||
<OsqueryEditor defaultValue={value.query} disabled={disabled} onChange={handleEditorChange} />
|
||||
</>
|
||||
<EuiFormRow isInvalid={typeof error === 'string'} error={error} fullWidth>
|
||||
<OsqueryEditor defaultValue={value} disabled={disabled} onChange={handleEditorChange} />
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId, agentIds, is
|
|||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} isLive={isLive} />
|
||||
<ResultsTable actionId={actionId} agentIds={agentIds} isLive={isLive} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -31,15 +31,16 @@ const OsquerySchemaLink = React.memo(() => (
|
|||
OsquerySchemaLink.displayName = 'OsquerySchemaLink';
|
||||
|
||||
const CodeEditorFieldComponent: React.FC<CodeEditorFieldProps> = ({ field }) => {
|
||||
const { value, label, labelAppend, helpText, setValue } = field;
|
||||
const { value, label, labelAppend, helpText, setValue, errors } = field;
|
||||
const error = errors[0]?.message;
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={label}
|
||||
labelAppend={!isEmpty(labelAppend) ? labelAppend : <OsquerySchemaLink />}
|
||||
helpText={helpText}
|
||||
// isInvalid={typeof error === 'string'}
|
||||
// error={error}
|
||||
isInvalid={typeof error === 'string'}
|
||||
error={error}
|
||||
fullWidth
|
||||
>
|
||||
<OsqueryEditor defaultValue={value} onChange={setValue} />
|
||||
|
|
|
@ -12,6 +12,10 @@ import {
|
|||
EuiDataGridProps,
|
||||
EuiDataGridColumn,
|
||||
EuiLink,
|
||||
EuiTextColor,
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
|
||||
|
@ -20,16 +24,89 @@ import { pagePathGetters } from '../../../fleet/public';
|
|||
import { useAllResults } from './use_all_results';
|
||||
import { Direction, ResultEdges } from '../../common/search_strategy';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { useActionResults } from '../action_results/use_action_results';
|
||||
import { generateEmptyDataMessage } from './translations';
|
||||
|
||||
const DataContext = createContext<ResultEdges>([]);
|
||||
|
||||
interface ResultsTableComponentProps {
|
||||
actionId: string;
|
||||
agentId?: string;
|
||||
selectedAgent?: string;
|
||||
agentIds?: string[];
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId, isLive }) => {
|
||||
interface SummaryTableValue {
|
||||
total: number | string;
|
||||
pending: number | string;
|
||||
responded: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
isLive,
|
||||
}) => {
|
||||
const {
|
||||
// @ts-expect-error update types
|
||||
data: { aggregations },
|
||||
} = useActionResults({
|
||||
actionId,
|
||||
activePage: 0,
|
||||
agentIds,
|
||||
limit: 0,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
isLive,
|
||||
});
|
||||
|
||||
const notRespondedCount = useMemo(() => {
|
||||
if (!agentIds || !aggregations.totalResponded) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return agentIds.length - aggregations.totalResponded;
|
||||
}, [aggregations.totalResponded, agentIds]);
|
||||
|
||||
const summaryColumns: Array<EuiBasicTableColumn<SummaryTableValue>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'total',
|
||||
name: 'Agents queried',
|
||||
},
|
||||
{
|
||||
field: 'responded',
|
||||
name: 'Successful',
|
||||
},
|
||||
{
|
||||
field: 'pending',
|
||||
name: 'Not yet responded',
|
||||
},
|
||||
{
|
||||
field: 'failed',
|
||||
name: 'Failed',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (failed: number) => (
|
||||
<EuiTextColor color={failed ? 'danger' : 'default'}>{failed}</EuiTextColor>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const summaryItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
total: agentIds?.length ?? '-',
|
||||
pending: notRespondedCount,
|
||||
responded: aggregations.totalResponded,
|
||||
failed: aggregations.failed,
|
||||
},
|
||||
],
|
||||
[aggregations, agentIds, notRespondedCount]
|
||||
);
|
||||
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
|
||||
const getFleetAppUrl = useCallback(
|
||||
|
@ -115,30 +192,41 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId,
|
|||
|
||||
const newColumns = keys(allResultsData?.edges[0]?.fields)
|
||||
.sort()
|
||||
.reduce((acc, fieldName) => {
|
||||
if (fieldName === 'agent.name') {
|
||||
acc.push({
|
||||
id: fieldName,
|
||||
displayAsText: i18n.translate('xpack.osquery.liveQueryResults.table.agentColumnTitle', {
|
||||
defaultMessage: 'agent',
|
||||
}),
|
||||
defaultSortDirection: Direction.asc,
|
||||
});
|
||||
.reduce(
|
||||
(acc, fieldName) => {
|
||||
const { data, seen } = acc;
|
||||
if (fieldName === 'agent.name') {
|
||||
data.push({
|
||||
id: fieldName,
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.osquery.liveQueryResults.table.agentColumnTitle',
|
||||
{
|
||||
defaultMessage: 'agent',
|
||||
}
|
||||
),
|
||||
defaultSortDirection: Direction.asc,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (fieldName.startsWith('osquery.')) {
|
||||
const displayAsText = fieldName.split('.')[1];
|
||||
if (!seen.has(displayAsText)) {
|
||||
data.push({
|
||||
id: fieldName,
|
||||
displayAsText,
|
||||
defaultSortDirection: Direction.asc,
|
||||
});
|
||||
seen.add(displayAsText);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (fieldName.startsWith('osquery.')) {
|
||||
acc.push({
|
||||
id: fieldName,
|
||||
displayAsText: fieldName.split('.')[1],
|
||||
defaultSortDirection: Direction.asc,
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as EuiDataGridColumn[]);
|
||||
},
|
||||
{ data: [], seen: new Set<string>() } as { data: EuiDataGridColumn[]; seen: Set<string> }
|
||||
).data;
|
||||
|
||||
if (!isEqual(columns, newColumns)) {
|
||||
setColumns(newColumns);
|
||||
|
@ -149,16 +237,24 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId,
|
|||
return (
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={allResultsData?.edges}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rowCount={allResultsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="500px"
|
||||
/>
|
||||
<EuiBasicTable items={summaryItems} rowHeader="total" columns={summaryColumns} />
|
||||
<EuiSpacer />
|
||||
{columns.length > 0 ? (
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rowCount={allResultsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="500px"
|
||||
/>
|
||||
) : (
|
||||
<div className={'eui-textCenter'}>
|
||||
{generateEmptyDataMessage(aggregations.totalResponded)}
|
||||
</div>
|
||||
)}
|
||||
</DataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,14 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const generateEmptyDataMessage = (agentsResponded: number): string => {
|
||||
return i18n.translate('xpack.osquery.results.multipleAgentsResponded', {
|
||||
defaultMessage:
|
||||
'{agentsResponded, plural, one {# agent has} other {# agents have}} responded, but no osquery data has been reported.',
|
||||
values: { agentsResponded },
|
||||
});
|
||||
};
|
||||
|
||||
export const ERROR_ALL_RESULTS = i18n.translate('xpack.osquery.results.errorSearchDescription', {
|
||||
defaultMessage: `An error has occurred on all results search`,
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
|
@ -51,7 +52,10 @@ export const useAllResults = ({
|
|||
skip = false,
|
||||
isLive = false,
|
||||
}: UseAllResults) => {
|
||||
const { data } = useKibana().services;
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
['allActionResults', { actionId, activePage, direction, limit, sortField }],
|
||||
|
@ -82,6 +86,12 @@ export const useAllResults = ({
|
|||
{
|
||||
refetchInterval: isLive ? 1000 : false,
|
||||
enabled: !skip,
|
||||
onError: (error: Error) =>
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.osquery.results.fetchError', {
|
||||
defaultMessage: 'Error while fetching results',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CodeEditorField } from '../../queries/form/code_editor_field';
|
||||
import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
|
||||
import { Form, useForm, FormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
const FORM_ID = 'addQueryFlyoutForm';
|
||||
|
@ -50,12 +51,14 @@ const AddQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({ onSave, onClos
|
|||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
validations: idFieldValidations.map((validator) => ({ validator })),
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
|
||||
defaultMessage: 'Query',
|
||||
}),
|
||||
validations: [{ validator: queryFieldValidation }],
|
||||
},
|
||||
interval: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
|
@ -65,6 +68,7 @@ const AddQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({ onSave, onClos
|
|||
defaultMessage: 'Interval (s)',
|
||||
}
|
||||
),
|
||||
validations: [{ validator: intervalFieldValidation }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -74,7 +74,7 @@ const ConfirmDeployAgentPolicyModalComponent: React.FC<ConfirmDeployAgentPolicyM
|
|||
<EuiSpacer size="l" />
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.agentPolicy.confirmModalDescription"
|
||||
defaultMessage="This action can not be undone. Are you sure you wish to continue?"
|
||||
defaultMessage="Are you sure you wish to continue?"
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
|
|
|
@ -25,6 +25,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { PackagePolicyInputStream } from '../../../../fleet/common';
|
||||
import { CodeEditorField } from '../../queries/form/code_editor_field';
|
||||
import { Form, useForm, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
|
||||
import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
|
||||
|
||||
const FORM_ID = 'editQueryFlyoutForm';
|
||||
|
||||
|
@ -64,12 +65,14 @@ export const EditQueryFlyout: React.FC<EditQueryFlyoutProps> = ({
|
|||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
validations: idFieldValidations.map((validator) => ({ validator })),
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
|
||||
defaultMessage: 'Query',
|
||||
}),
|
||||
validations: [{ validator: queryFieldValidation }],
|
||||
},
|
||||
interval: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
|
@ -79,6 +82,7 @@ export const EditQueryFlyout: React.FC<EditQueryFlyoutProps> = ({
|
|||
defaultMessage: 'Interval (s)',
|
||||
}
|
||||
),
|
||||
validations: [{ validator: intervalFieldValidation }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const INVALID_ID_ERROR = i18n.translate('xpack.osquery.agents.failSearchDescription', {
|
||||
defaultMessage: `Failed to fetch agents`,
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ValidationFunc, fieldValidators } from '../../shared_imports';
|
||||
export { queryFieldValidation } from '../../common/validations';
|
||||
|
||||
const idPattern = /^[a-zA-Z0-9-_]+$/;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const idSchemaValidation: ValidationFunc<any, string, string> = ({ value }) => {
|
||||
const valueIsValid = idPattern.test(value);
|
||||
if (!valueIsValid) {
|
||||
return {
|
||||
message: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.invalidIdError', {
|
||||
defaultMessage: 'Characters must be alphanumeric, _, or -',
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const idFieldValidations = [
|
||||
fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.emptyIdError', {
|
||||
defaultMessage: 'ID is required',
|
||||
})
|
||||
),
|
||||
idSchemaValidation,
|
||||
];
|
||||
|
||||
export const intervalFieldValidation: ValidationFunc<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any,
|
||||
string,
|
||||
number
|
||||
> = fieldValidators.numberGreaterThanField({
|
||||
than: 0,
|
||||
message: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.invalidIntervalField',
|
||||
{
|
||||
defaultMessage: 'A positive interval value is required',
|
||||
}
|
||||
),
|
||||
});
|
|
@ -148,7 +148,7 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQuer
|
|||
{
|
||||
field: 'vars.interval.value',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.intervalColumnTitle', {
|
||||
defaultMessage: 'Interval',
|
||||
defaultMessage: 'Interval (s)',
|
||||
}),
|
||||
width: '100px',
|
||||
},
|
||||
|
|
|
@ -26,6 +26,7 @@ export {
|
|||
ValidationFunc,
|
||||
VALIDATION_TYPES,
|
||||
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
|
||||
export {
|
||||
Field,
|
||||
ComboBoxField,
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from 'src/core/server';
|
||||
import { uniq } from 'lodash';
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common';
|
||||
import { OSQUERY_INTEGRATION_NAME } from '../../common';
|
||||
import { OsqueryAppContext } from './osquery_app_context_services';
|
||||
|
||||
export interface AgentSelection {
|
||||
|
@ -15,45 +18,82 @@ export interface AgentSelection {
|
|||
policiesSelected: string[];
|
||||
}
|
||||
|
||||
const PER_PAGE = 9000;
|
||||
|
||||
const aggregateResults = async (
|
||||
generator: (page: number, perPage: number) => Promise<{ results: string[]; total: number }>
|
||||
) => {
|
||||
const { results, total } = await generator(1, PER_PAGE);
|
||||
const totalPages = Math.ceil(total / PER_PAGE);
|
||||
let currPage = 2;
|
||||
while (currPage <= totalPages) {
|
||||
const { results: additionalResults } = await generator(currPage++, PER_PAGE);
|
||||
results.push(...additionalResults);
|
||||
}
|
||||
return uniq<string>(results);
|
||||
};
|
||||
|
||||
export const parseAgentSelection = async (
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
context: OsqueryAppContext,
|
||||
agentSelection: AgentSelection
|
||||
) => {
|
||||
let selectedAgents: string[] = [];
|
||||
const selectedAgents: Set<string> = new Set();
|
||||
const addAgent = selectedAgents.add.bind(selectedAgents);
|
||||
const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection;
|
||||
const agentService = context.service.getAgentService();
|
||||
if (agentService) {
|
||||
if (allAgentsSelected) {
|
||||
// TODO: actually fetch all the agents
|
||||
const { agents: fetchedAgents } = await agentService.listAgents(esClient, {
|
||||
perPage: 9000,
|
||||
showInactive: true,
|
||||
const packagePolicyService = context.service.getPackagePolicyService();
|
||||
const kueryFragments = ['active:true'];
|
||||
|
||||
if (agentService && packagePolicyService) {
|
||||
const osqueryPolicies = await aggregateResults(async (page, perPage) => {
|
||||
const { items, total } = await packagePolicyService.list(soClient, {
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
|
||||
perPage,
|
||||
page,
|
||||
});
|
||||
selectedAgents.push(...fetchedAgents.map((a) => a.id));
|
||||
} else {
|
||||
if (platformsSelected.length > 0 || policiesSelected.length > 0) {
|
||||
const kueryFragments = [];
|
||||
if (platformsSelected.length) {
|
||||
kueryFragments.push(
|
||||
...platformsSelected.map((platform) => `local_metadata.os.platform:${platform}`)
|
||||
);
|
||||
}
|
||||
if (policiesSelected.length) {
|
||||
kueryFragments.push(...policiesSelected.map((policy) => `policy_id:${policy}`));
|
||||
}
|
||||
const kuery = kueryFragments.join(' or ');
|
||||
// TODO: actually fetch all the agents
|
||||
const { agents: fetchedAgents } = await agentService.listAgents(esClient, {
|
||||
return { results: items.map((it) => it.policy_id), total };
|
||||
});
|
||||
kueryFragments.push(`policy_id:(${uniq(osqueryPolicies).join(',')})`);
|
||||
if (allAgentsSelected) {
|
||||
const kuery = kueryFragments.join(' and ');
|
||||
const fetchedAgents = await aggregateResults(async (page, perPage) => {
|
||||
const res = await agentService.listAgents(esClient, {
|
||||
perPage,
|
||||
page,
|
||||
kuery,
|
||||
perPage: 9000,
|
||||
showInactive: true,
|
||||
});
|
||||
selectedAgents.push(...fetchedAgents.map((a) => a.id));
|
||||
return { results: res.agents.map((agent) => agent.id), total: res.total };
|
||||
});
|
||||
fetchedAgents.forEach(addAgent);
|
||||
} else {
|
||||
if (platformsSelected.length > 0 || policiesSelected.length > 0) {
|
||||
const groupFragments = [];
|
||||
if (platformsSelected.length) {
|
||||
groupFragments.push(`local_metadata.os.platform:(${platformsSelected.join(',')})`);
|
||||
}
|
||||
if (policiesSelected.length) {
|
||||
groupFragments.push(`policy_id:(${policiesSelected.join(',')})`);
|
||||
}
|
||||
kueryFragments.push(`(${groupFragments.join(' or ')})`);
|
||||
const kuery = kueryFragments.join(' and ');
|
||||
const fetchedAgents = await aggregateResults(async (page, perPage) => {
|
||||
const res = await agentService.listAgents(esClient, {
|
||||
perPage,
|
||||
page,
|
||||
kuery,
|
||||
showInactive: true,
|
||||
});
|
||||
return { results: res.agents.map((agent) => agent.id), total: res.total };
|
||||
});
|
||||
fetchedAgents.forEach(addAgent);
|
||||
}
|
||||
selectedAgents.push(...agents);
|
||||
selectedAgents = Array.from(new Set(selectedAgents));
|
||||
}
|
||||
}
|
||||
return selectedAgents;
|
||||
|
||||
agents.forEach(addAgent);
|
||||
|
||||
return Array.from(selectedAgents);
|
||||
};
|
||||
|
|
|
@ -7,29 +7,42 @@
|
|||
|
||||
import uuid from 'uuid';
|
||||
import moment from 'moment';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { IRouter } from '../../../../../../src/core/server';
|
||||
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
|
||||
|
||||
import { parseAgentSelection, AgentSelection } from '../../lib/parse_agent_groups';
|
||||
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
|
||||
import {
|
||||
createActionRequestBodySchema,
|
||||
CreateActionRequestBodySchema,
|
||||
} from '../../../common/schemas/routes/action/create_action_request_body_schema';
|
||||
|
||||
export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
|
||||
router.post(
|
||||
{
|
||||
path: '/internal/osquery/action',
|
||||
validate: {
|
||||
params: schema.object({}, { unknowns: 'allow' }),
|
||||
body: schema.object({}, { unknowns: 'allow' }),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:osquery', 'access:osquery_write'],
|
||||
body: buildRouteValidation<
|
||||
typeof createActionRequestBodySchema,
|
||||
CreateActionRequestBodySchema
|
||||
>(createActionRequestBodySchema),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const soClient = context.core.savedObjects.client;
|
||||
const { agentSelection } = request.body as { agentSelection: AgentSelection };
|
||||
const selectedAgents = await parseAgentSelection(esClient, osqueryContext, agentSelection);
|
||||
const selectedAgents = await parseAgentSelection(
|
||||
esClient,
|
||||
soClient,
|
||||
osqueryContext,
|
||||
agentSelection
|
||||
);
|
||||
|
||||
if (!selectedAgents.length) {
|
||||
throw new Error('No agents found for selection, aborting.');
|
||||
}
|
||||
|
||||
const action = {
|
||||
action_id: uuid.v4(),
|
||||
|
@ -39,10 +52,8 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
|
|||
input_type: 'osquery',
|
||||
agents: selectedAgents,
|
||||
data: {
|
||||
// @ts-expect-error update validation
|
||||
id: request.body.query.id ?? uuid.v4(),
|
||||
// @ts-expect-error update validation
|
||||
query: request.body.query.query,
|
||||
id: uuid.v4(),
|
||||
query: request.body.query,
|
||||
},
|
||||
};
|
||||
const actionResponse = await esClient.index<{}, {}>({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue