mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Osquery] Refactor ECS editor field (#130582)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d124a4d17c
commit
e1bd7da1d1
38 changed files with 914 additions and 1115 deletions
|
@ -48,7 +48,7 @@ const TabComponent = (props: TabProps) => {
|
|||
|
||||
return (
|
||||
<TabContent>
|
||||
<OsqueryAction agentId={metadata?.info?.agent?.id} />
|
||||
<OsqueryAction agentId={metadata?.info?.agent?.id} hideAgentsField />
|
||||
</TabContent>
|
||||
);
|
||||
}, [OsqueryAction, loading, metadata]);
|
||||
|
|
39
x-pack/plugins/osquery/common/schemas/common/utils.ts
Normal file
39
x-pack/plugins/osquery/common/schemas/common/utils.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty, reduce } from 'lodash';
|
||||
|
||||
export const convertECSMappingToArray = (ecsMapping: Record<string, object> | undefined) =>
|
||||
ecsMapping
|
||||
? Object.entries(ecsMapping).map((item) => ({
|
||||
key: item[0],
|
||||
value: item[1],
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
export const convertECSMappingToObject = (
|
||||
ecsMapping: Array<{
|
||||
key: string;
|
||||
result: {
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
}>
|
||||
): Record<string, { field?: string; value?: string }> =>
|
||||
reduce(
|
||||
ecsMapping,
|
||||
(acc, value) => {
|
||||
if (!isEmpty(value?.key) && !isEmpty(value.result?.type) && !isEmpty(value.result?.value)) {
|
||||
acc[value.key] = {
|
||||
[value.result.type]: value.result.value,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { field?: string; value?: string }>
|
||||
);
|
|
@ -31,7 +31,10 @@ describe('ALL - Delete ECS Mappings', () => {
|
|||
}).click();
|
||||
cy.contains('Custom key/value pairs.').should('exist');
|
||||
cy.contains('Hours of uptime').should('exist');
|
||||
cy.react('EuiButtonIcon', { props: { id: 'labels-trash' } }).click();
|
||||
cy.react('ECSComboboxFieldComponent', { props: { field: { value: 'labels' } } })
|
||||
.parents('[data-test-subj="ECSMappingEditorForm"]')
|
||||
.react('EuiButtonIcon', { props: { iconType: 'trash' } })
|
||||
.click();
|
||||
cy.react('EuiButton').contains('Update query').click();
|
||||
cy.wait(5000);
|
||||
|
||||
|
|
|
@ -61,10 +61,8 @@ describe('ALL - Live Query', () => {
|
|||
}).should('exist');
|
||||
cy.react(RESULTS_TABLE_CELL_WRRAPER, {
|
||||
props: { id: 'osquery.days.number', index: 2 },
|
||||
}).within(() => {
|
||||
cy.get('.euiToolTipAnchor').within(() => {
|
||||
cy.get('svg').should('exist');
|
||||
});
|
||||
});
|
||||
})
|
||||
.react('EuiIconTip', { props: { type: 'indexMapping' } })
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -55,7 +55,7 @@ describe('ALL - Packs', () => {
|
|||
cy.react('List').first().click();
|
||||
findAndClickButton('Add query');
|
||||
cy.contains('Attach next query');
|
||||
getSavedQueriesDropdown().click().type(`${SAVED_QUERY_ID}{downArrow}{enter}`);
|
||||
getSavedQueriesDropdown().type(`${SAVED_QUERY_ID}{downArrow}{enter}`);
|
||||
cy.react('EuiFormRow', { props: { label: 'Interval (s)' } })
|
||||
.click()
|
||||
.clear()
|
||||
|
@ -92,7 +92,7 @@ describe('ALL - Packs', () => {
|
|||
findAndClickButton('Add query');
|
||||
cy.contains('Attach next query');
|
||||
cy.contains('ID must be unique').should('not.exist');
|
||||
getSavedQueriesDropdown().click().type(`${SAVED_QUERY_ID}{downArrow}{enter}`);
|
||||
getSavedQueriesDropdown().type(`${SAVED_QUERY_ID}{downArrow}{enter}`);
|
||||
cy.contains('ID must be unique').should('exist');
|
||||
cy.react('EuiFlyoutFooter').react('EuiButtonEmpty').contains('Cancel').click();
|
||||
});
|
||||
|
@ -170,7 +170,7 @@ describe('ALL - Packs', () => {
|
|||
|
||||
findAndClickButton('Add query');
|
||||
|
||||
getSavedQueriesDropdown().click().type('Multiple {downArrow} {enter}');
|
||||
getSavedQueriesDropdown().type('Multiple {downArrow} {enter}');
|
||||
cy.contains('Custom key/value pairs');
|
||||
cy.contains('Days of uptime');
|
||||
cy.contains('List of keywords used to tag each');
|
||||
|
@ -178,7 +178,7 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('Client network address.');
|
||||
cy.contains('Total uptime seconds');
|
||||
|
||||
getSavedQueriesDropdown().click().type('NOMAPPING {downArrow} {enter}');
|
||||
getSavedQueriesDropdown().type('NOMAPPING {downArrow} {enter}');
|
||||
cy.contains('Custom key/value pairs').should('not.exist');
|
||||
cy.contains('Days of uptime').should('not.exist');
|
||||
cy.contains('List of keywords used to tag each').should('not.exist');
|
||||
|
@ -186,7 +186,7 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('Client network address.').should('not.exist');
|
||||
cy.contains('Total uptime seconds').should('not.exist');
|
||||
|
||||
getSavedQueriesDropdown().click().type('ONE_MAPPING {downArrow} {enter}');
|
||||
getSavedQueriesDropdown().type('ONE_MAPPING {downArrow} {enter}');
|
||||
cy.contains('Name of the continent');
|
||||
cy.contains('Seconds of uptime');
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('T1 Analyst - READ + runSavedQueries ', () => {
|
|||
cy.waitForReact(1000);
|
||||
cy.contains('New live query').should('not.be.disabled').click();
|
||||
selectAllAgents();
|
||||
getSavedQueriesDropdown().click().type(`${SAVED_QUERY_ID}{downArrow} {enter}`);
|
||||
getSavedQueriesDropdown().type(`${SAVED_QUERY_ID}{downArrow} {enter}`);
|
||||
cy.contains('select * from uptime');
|
||||
submitQuery();
|
||||
checkResults();
|
||||
|
|
|
@ -104,7 +104,10 @@ describe('T2 Analyst - READ + Write Live/Saved + runSavedQueries ', () => {
|
|||
}).click();
|
||||
cy.contains('Custom key/value pairs.').should('exist');
|
||||
cy.contains('Hours of uptime').should('exist');
|
||||
cy.react('EuiButtonIcon', { props: { id: 'labels-trash' } }).click();
|
||||
cy.react('ECSComboboxFieldComponent', { props: { field: { value: 'labels' } } })
|
||||
.parents('[data-test-subj="ECSMappingEditorForm"]')
|
||||
.react('EuiButtonIcon', { props: { iconType: 'trash' } })
|
||||
.click();
|
||||
cy.react('EuiButton').contains('Update query').click();
|
||||
cy.wait(5000);
|
||||
|
||||
|
|
|
@ -8,17 +8,15 @@
|
|||
import { LIVE_QUERY_EDITOR } from '../screens/live_query';
|
||||
|
||||
export const DEFAULT_QUERY = 'select * from processes;';
|
||||
export const BIG_QUERY = 'select * from processes, users;';
|
||||
export const BIG_QUERY = 'select * from processes, users limit 200;';
|
||||
|
||||
export const selectAllAgents = () => {
|
||||
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } })
|
||||
.find('input')
|
||||
.should('not.be.disabled');
|
||||
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).click();
|
||||
cy.react('AgentsTable').find('input').should('not.be.disabled');
|
||||
cy.react('AgentsTable EuiComboBox', {
|
||||
props: { placeholder: 'Select agents or groups' },
|
||||
}).click();
|
||||
cy.react('EuiFilterSelectItem').contains('All agents').should('exist');
|
||||
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type(
|
||||
'{downArrow}{enter}{esc}'
|
||||
);
|
||||
cy.react('AgentsTable EuiComboBox').type('{downArrow}{enter}{esc}');
|
||||
cy.contains('1 agent selected.');
|
||||
};
|
||||
|
||||
|
@ -27,12 +25,11 @@ export const inputQuery = (query: string) => cy.get(LIVE_QUERY_EDITOR).type(quer
|
|||
export const submitQuery = () => cy.contains('Submit').click();
|
||||
|
||||
export const checkResults = () =>
|
||||
cy.getBySel('dataGridRowCell', { timeout: 60000 }).should('have.lengthOf.above', 0);
|
||||
cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0);
|
||||
|
||||
export const typeInECSFieldInput = (text: string) =>
|
||||
cy.getBySel('ECS-field-input').click().type(text);
|
||||
export const typeInECSFieldInput = (text: string) => cy.getBySel('ECS-field-input').type(text);
|
||||
export const typeInOsqueryFieldInput = (text: string) =>
|
||||
cy.react('OsqueryColumnFieldComponent').first().react('ResultComboBox').click().type(text);
|
||||
cy.react('OsqueryColumnFieldComponent').first().react('ResultComboBox').type(text);
|
||||
|
||||
export const findFormFieldByRowsLabelAndType = (label: string, text: string) => {
|
||||
cy.react('EuiFormRow', { props: { label } }).type(text);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { flatten, reverse, uniqBy } from 'lodash/fp';
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import {
|
||||
createFilter,
|
||||
getInspectResponse,
|
||||
|
@ -68,7 +68,7 @@ export const useActionResults = ({
|
|||
return useQuery(
|
||||
['actionResults', { actionId }],
|
||||
async () => {
|
||||
const responseData = await firstValueFrom(
|
||||
const responseData = await lastValueFrom(
|
||||
data.search.search<ActionResultsRequestOptions, ActionResultsStrategyResponse>(
|
||||
{
|
||||
actionId,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
|
@ -37,7 +37,7 @@ export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseAct
|
|||
return useQuery(
|
||||
['actionDetails', { actionId, filterQuery }],
|
||||
async () => {
|
||||
const responseData = await firstValueFrom(
|
||||
const responseData = await lastValueFrom(
|
||||
data.search.search<ActionDetailsRequestOptions, ActionDetailsStrategyResponse>(
|
||||
{
|
||||
actionId,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import {
|
||||
createFilter,
|
||||
generateTablePaginationOptions,
|
||||
|
@ -60,7 +60,7 @@ export const useAllActions = ({
|
|||
return useQuery(
|
||||
['actions', { activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
const responseData = await firstValueFrom(
|
||||
const responseData = await lastValueFrom(
|
||||
data.search.search<ActionsRequestOptions, ActionsStrategyResponse>(
|
||||
{
|
||||
factoryQueryType: OsqueryQueries.actions,
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
|
||||
import { find } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EuiComboBox, EuiHealth, EuiHighlight, EuiSpacer } from '@elastic/eui';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { useAllAgents } from './use_all_agents';
|
||||
import { useAgentGroups } from './use_agent_groups';
|
||||
import { useOsqueryPolicies } from './use_osquery_policies';
|
||||
import { AgentGrouper } from './agent_grouper';
|
||||
import {
|
||||
getNumAgentsInGrouping,
|
||||
|
@ -61,19 +61,16 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
|
|||
);
|
||||
|
||||
// grouping related
|
||||
const osqueryPolicyData = useOsqueryPolicies();
|
||||
const {
|
||||
loading: groupsLoading,
|
||||
totalCount: totalNumAgents,
|
||||
groups,
|
||||
isLoading: groupsLoading,
|
||||
data: agentGroupsData,
|
||||
isFetched: groupsFetched,
|
||||
} = useAgentGroups(osqueryPolicyData);
|
||||
const grouper = useMemo(() => new AgentGrouper(), []);
|
||||
} = useAgentGroups();
|
||||
const {
|
||||
isLoading: agentsLoading,
|
||||
data: agents,
|
||||
isFetched: agentsFetched,
|
||||
} = useAllAgents(osqueryPolicyData, debouncedSearchValue, {
|
||||
} = useAllAgents(debouncedSearchValue, {
|
||||
perPage,
|
||||
});
|
||||
|
||||
|
@ -96,7 +93,7 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
|
|||
selectedGroups: SelectedGroups;
|
||||
} = generateAgentSelection(selection);
|
||||
if (newAgentSelection.allAgentsSelected) {
|
||||
setNumAgentsSelected(totalNumAgents);
|
||||
setNumAgentsSelected(agentGroupsData?.totalCount ?? 0);
|
||||
} else {
|
||||
const checkAgent = generateAgentCheck(selectedGroups);
|
||||
setNumAgentsSelected(
|
||||
|
@ -105,14 +102,14 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
|
|||
// add the number of agents added via policy and platform groups
|
||||
getNumAgentsInGrouping(selectedGroups) -
|
||||
// subtract the number of agents double counted by policy/platform selections
|
||||
getNumOverlapped(selectedGroups, groups.overlap)
|
||||
getNumOverlapped(selectedGroups, agentGroupsData?.groups?.overlap ?? {})
|
||||
);
|
||||
}
|
||||
|
||||
onChange(newAgentSelection);
|
||||
setSelectedOptions(selection);
|
||||
},
|
||||
[groups, onChange, totalNumAgents]
|
||||
[agentGroupsData, onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -154,26 +151,18 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
|
|||
}, [agentSelection, onSelection, options, selectedOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (agentsFetched && groupsFetched) {
|
||||
if (agentsFetched && groupsFetched && agentGroupsData) {
|
||||
const grouper = new AgentGrouper();
|
||||
// update the groups when groups or agents have changed
|
||||
grouper.setTotalAgents(totalNumAgents);
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Platform, groups.platforms);
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Policy, groups.policies);
|
||||
grouper.setTotalAgents(agentGroupsData?.totalCount);
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Platform, agentGroupsData?.groups.platforms);
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Policy, agentGroupsData?.groups.policies);
|
||||
// @ts-expect-error update types
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents);
|
||||
const newOptions = grouper.generateOptions();
|
||||
setOptions(newOptions);
|
||||
setOptions((prevOptions) => (!deepEqual(prevOptions, newOptions) ? newOptions : prevOptions));
|
||||
}
|
||||
}, [
|
||||
groups.platforms,
|
||||
groups.policies,
|
||||
totalNumAgents,
|
||||
groupsLoading,
|
||||
agents,
|
||||
agentsFetched,
|
||||
groupsFetched,
|
||||
grouper,
|
||||
]);
|
||||
}, [groupsLoading, agents, agentsFetched, groupsFetched, agentGroupsData]);
|
||||
|
||||
const renderOption = useCallback((option, searchVal, contentClassName) => {
|
||||
const { label, value } = option;
|
||||
|
@ -202,6 +191,7 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
|
|||
return (
|
||||
<div>
|
||||
<EuiComboBox
|
||||
data-test-subj="agentSelection"
|
||||
placeholder={SELECT_AGENT_LABEL}
|
||||
isLoading={modifyingSearch || groupsLoading || agentsLoading}
|
||||
options={options}
|
||||
|
@ -218,4 +208,6 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
|
|||
);
|
||||
};
|
||||
|
||||
AgentsTableComponent.displayName = 'AgentsTable';
|
||||
|
||||
export const AgentsTable = React.memo(AgentsTableComponent);
|
||||
|
|
|
@ -38,7 +38,17 @@ interface Aggs extends estypes.AggregationsTermsAggregateBase {
|
|||
buckets: AggregationDataPoint[];
|
||||
}
|
||||
|
||||
export const processAggregations = (aggs: Record<string, estypes.AggregationsAggregate>) => {
|
||||
export const processAggregations = (
|
||||
aggs: Record<string, estypes.AggregationsAggregate> | undefined
|
||||
) => {
|
||||
if (!aggs) {
|
||||
return {
|
||||
platforms: [],
|
||||
overlap: {},
|
||||
policies: [],
|
||||
};
|
||||
}
|
||||
|
||||
const platforms: Group[] = [];
|
||||
const overlap: Overlap = {};
|
||||
const platformTerms = aggs.platforms as Aggs;
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { useAgentPolicies } from './use_agent_policies';
|
||||
|
||||
|
@ -19,28 +18,27 @@ import {
|
|||
|
||||
import { processAggregations } from './helpers';
|
||||
import { generateTablePaginationOptions } from '../common/helpers';
|
||||
import { Overlap, Group } from './types';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
import { useOsqueryPolicies } from './use_osquery_policies';
|
||||
|
||||
interface UseAgentGroups {
|
||||
osqueryPolicies: string[];
|
||||
osqueryPoliciesLoading: boolean;
|
||||
}
|
||||
|
||||
export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => {
|
||||
export const useAgentGroups = () => {
|
||||
const { data } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
const { data: osqueryPolicies, isFetched: isOsqueryPoliciesFetched } = useOsqueryPolicies();
|
||||
const { agentPoliciesLoading, agentPolicyById } = useAgentPolicies(osqueryPolicies);
|
||||
const [platforms, setPlatforms] = useState<Group[]>([]);
|
||||
const [policies, setPolicies] = useState<Group[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [overlap, setOverlap] = useState<Overlap>(() => ({}));
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
const { isFetched } = useQuery(
|
||||
|
||||
return useQuery<
|
||||
AgentsStrategyResponse,
|
||||
unknown,
|
||||
{
|
||||
totalCount: number;
|
||||
groups: ReturnType<typeof processAggregations>;
|
||||
}
|
||||
>(
|
||||
['agentGroups'],
|
||||
async () => {
|
||||
const responseData = await firstValueFrom(
|
||||
const responseData = await lastValueFrom(
|
||||
data.search.search<AgentsRequestOptions, AgentsStrategyResponse>(
|
||||
{
|
||||
filterQuery: { terms: { policy_id: osqueryPolicies } },
|
||||
|
@ -76,32 +74,54 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA
|
|||
)
|
||||
);
|
||||
|
||||
if (responseData.rawResponse.aggregations) {
|
||||
const {
|
||||
platforms: newPlatforms,
|
||||
overlap: newOverlap,
|
||||
policies: newPolicies,
|
||||
} = processAggregations(responseData.rawResponse.aggregations);
|
||||
|
||||
setPlatforms(newPlatforms);
|
||||
setOverlap(newOverlap);
|
||||
setPolicies(
|
||||
newPolicies.map((p) => {
|
||||
const name = agentPolicyById[p.id]?.name ?? p.name;
|
||||
|
||||
return {
|
||||
...p,
|
||||
name,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setTotalCount(responseData.totalCount);
|
||||
return responseData;
|
||||
},
|
||||
{
|
||||
enabled: !osqueryPoliciesLoading && !agentPoliciesLoading,
|
||||
select: (response) => {
|
||||
const { platforms, overlap, policies } = processAggregations(
|
||||
response.rawResponse.aggregations
|
||||
);
|
||||
|
||||
return {
|
||||
totalCount: response.totalCount,
|
||||
groups: {
|
||||
platforms,
|
||||
overlap,
|
||||
policies: policies.map((p) => {
|
||||
const name = agentPolicyById[p.id]?.name ?? p.name;
|
||||
|
||||
return {
|
||||
...p,
|
||||
name,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
placeholderData: {
|
||||
totalCount: 0,
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
activePage: 1,
|
||||
fakeTotalCount: 100,
|
||||
showMorePagesIndicator: true,
|
||||
},
|
||||
rawResponse: {
|
||||
took: 0,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
failed: 0,
|
||||
successful: 0,
|
||||
total: 0,
|
||||
},
|
||||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
keepPreviousData: true,
|
||||
enabled: isOsqueryPoliciesFetched && !agentPoliciesLoading,
|
||||
onSuccess: () => setErrorToast(),
|
||||
onError: (error) =>
|
||||
setErrorToast(error as Error, {
|
||||
|
@ -111,15 +131,4 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA
|
|||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
isFetched,
|
||||
loading,
|
||||
totalCount,
|
||||
groups: {
|
||||
platforms,
|
||||
policies,
|
||||
overlap,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -11,11 +11,7 @@ import { useQuery } from 'react-query';
|
|||
import { GetAgentsResponse } from '@kbn/fleet-plugin/common';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
interface UseAllAgents {
|
||||
osqueryPolicies: string[];
|
||||
osqueryPoliciesLoading: boolean;
|
||||
}
|
||||
import { useOsqueryPolicies } from './use_osquery_policies';
|
||||
|
||||
interface RequestOptions {
|
||||
perPage?: number;
|
||||
|
@ -23,22 +19,24 @@ interface RequestOptions {
|
|||
}
|
||||
|
||||
// TODO: break out the paginated vs all cases into separate hooks
|
||||
export const useAllAgents = (
|
||||
{ osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents,
|
||||
searchValue = '',
|
||||
opts: RequestOptions = { perPage: 9000 }
|
||||
) => {
|
||||
export const useAllAgents = (searchValue = '', opts: RequestOptions = { perPage: 9000 }) => {
|
||||
const { perPage } = opts;
|
||||
const { http } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
const { data: osqueryPolicies, isFetched } = useOsqueryPolicies();
|
||||
|
||||
return useQuery<GetAgentsResponse>(
|
||||
['agents', osqueryPolicies, searchValue, perPage],
|
||||
() => {
|
||||
let kuery = `(${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')})`;
|
||||
let kuery = '';
|
||||
|
||||
if (searchValue) {
|
||||
kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`;
|
||||
if (osqueryPolicies?.length) {
|
||||
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}*)`;
|
||||
}
|
||||
}
|
||||
|
||||
return http.get(`/internal/osquery/fleet_wrapper/agents`, {
|
||||
|
@ -51,7 +49,7 @@ export const useAllAgents = (
|
|||
{
|
||||
// @ts-expect-error update types
|
||||
select: (data) => data?.agents || [],
|
||||
enabled: !osqueryPoliciesLoading && osqueryPolicies.length > 0,
|
||||
enabled: isFetched && !!osqueryPolicies?.length,
|
||||
onSuccess: () => setErrorToast(),
|
||||
onError: (error) =>
|
||||
// @ts-expect-error update types
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
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 { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
@ -16,7 +15,7 @@ export const useOsqueryPolicies = () => {
|
|||
const { http } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies = [] } = useQuery(
|
||||
return useQuery(
|
||||
['osqueryPolicies'],
|
||||
() =>
|
||||
http.get<{ items: Array<{ policy_id: string }> }>(
|
||||
|
@ -33,9 +32,4 @@ export const useOsqueryPolicies = () => {
|
|||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({ osqueryPoliciesLoading, osqueryPolicies }),
|
||||
[osqueryPoliciesLoading, osqueryPolicies]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,22 +8,21 @@
|
|||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiSteps,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiAccordion,
|
||||
EuiAccordionProps,
|
||||
} from '@elastic/eui';
|
||||
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import deepMerge from 'deepmerge';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { pickBy, isEmpty } from 'lodash';
|
||||
import { pickBy, isEmpty, map } from 'lodash';
|
||||
import { convertECSMappingToObject } from '../../../common/schemas/common/utils';
|
||||
import { UseField, Form, FormData, useForm, useFormData, FIELD_TYPES } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
|
@ -33,16 +32,13 @@ import { queryFieldValidation } from '../../common/validations';
|
|||
import { fieldValidators } from '../../shared_imports';
|
||||
import { SavedQueryFlyout } from '../../saved_queries';
|
||||
import { useErrorToast } from '../../common/hooks/use_error_toast';
|
||||
import {
|
||||
ECSMappingEditorField,
|
||||
ECSMappingEditorFieldRef,
|
||||
} from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
|
||||
const StyledEuiAccordion = styled(EuiAccordion)`
|
||||
${({ isDisabled }: { isDisabled: boolean }) => isDisabled && 'display: none;'}
|
||||
${({ isDisabled }: { isDisabled?: boolean }) => isDisabled && 'display: none;'}
|
||||
.euiAccordion__button {
|
||||
color: ${({ theme }) => theme.eui.euiColorPrimary};
|
||||
}
|
||||
|
@ -55,27 +51,26 @@ const GhostFormField = () => <></>;
|
|||
type FormType = 'simple' | 'steps';
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
defaultValue?: Partial<FormData> | undefined;
|
||||
defaultValue?: Partial<FormData>;
|
||||
onSuccess?: () => void;
|
||||
agentsField?: boolean;
|
||||
queryField?: boolean;
|
||||
ecsMappingField?: boolean;
|
||||
formType?: FormType;
|
||||
enabled?: boolean;
|
||||
hideAgentsField?: boolean;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
}
|
||||
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
||||
defaultValue,
|
||||
onSuccess,
|
||||
agentsField = true,
|
||||
queryField = true,
|
||||
ecsMappingField = true,
|
||||
formType = 'steps',
|
||||
enabled = true,
|
||||
hideAgentsField = false,
|
||||
addToTimeline,
|
||||
}) => {
|
||||
const ecsFieldRef = useRef<ECSMappingEditorFieldRef>();
|
||||
const permissions = useKibana().services.application.capabilities.osquery;
|
||||
const { http } = useKibana().services;
|
||||
const [advancedContentState, setAdvancedContentState] =
|
||||
|
@ -136,7 +131,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
],
|
||||
},
|
||||
ecs_mapping: {
|
||||
defaultValue: {},
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.JSON,
|
||||
validations: [],
|
||||
},
|
||||
|
@ -146,18 +141,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
id: FORM_ID,
|
||||
schema: formSchema,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
const ecsFieldValue = await ecsFieldRef?.current?.validate();
|
||||
if (isValid && (!ecsMappingField || !!ecsFieldValue)) {
|
||||
if (isValid) {
|
||||
try {
|
||||
await mutateAsync(
|
||||
pickBy(
|
||||
{
|
||||
...formData,
|
||||
...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }),
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
)
|
||||
);
|
||||
await mutateAsync(pickBy(formData, (value) => !isEmpty(value)));
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
|
@ -165,8 +151,16 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
serializer: ({ savedQueryId, ...formData }) =>
|
||||
pickBy({ ...formData, saved_query_id: savedQueryId }, (value) => !isEmpty(value)),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
serializer: ({ savedQueryId, ecs_mapping, ...formData }) =>
|
||||
pickBy(
|
||||
{
|
||||
...formData,
|
||||
saved_query_id: savedQueryId,
|
||||
ecs_mapping: convertECSMappingToObject(ecs_mapping),
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
),
|
||||
defaultValue: deepMerge(
|
||||
{
|
||||
agentSelection: {
|
||||
|
@ -177,12 +171,13 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
},
|
||||
query: '',
|
||||
savedQueryId: null,
|
||||
ecs_mapping: [],
|
||||
},
|
||||
defaultValue ?? {}
|
||||
),
|
||||
});
|
||||
|
||||
const { setFieldValue, submit, isSubmitting } = form;
|
||||
const { updateFieldValues, setFieldValue, submit, isSubmitting } = form;
|
||||
|
||||
const actionId = useMemo(() => data?.actions[0].action_id, [data?.actions]);
|
||||
const agentIds = useMemo(() => data?.actions[0].agents, [data?.actions]);
|
||||
|
@ -207,13 +202,12 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
const queryValueProvided = useMemo(() => !!query?.length, [query]);
|
||||
|
||||
const queryStatus = useMemo(() => {
|
||||
if (!agentSelected) return 'disabled';
|
||||
if (isError || !form.getFields().query.isValid) return 'danger';
|
||||
if (isError || !form.getFields().query?.isValid) return 'danger';
|
||||
if (isLoading) return 'loading';
|
||||
if (isSuccess) return 'complete';
|
||||
|
||||
return 'incomplete';
|
||||
}, [agentSelected, isError, isLoading, isSuccess, form]);
|
||||
}, [isError, isLoading, isSuccess, form]);
|
||||
|
||||
const resultsStatus = useMemo(
|
||||
() => (queryStatus === 'complete' ? 'incomplete' : 'disabled'),
|
||||
|
@ -223,19 +217,28 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
const handleSavedQueryChange = useCallback(
|
||||
(savedQuery) => {
|
||||
if (savedQuery) {
|
||||
setFieldValue('query', savedQuery.query);
|
||||
setFieldValue('savedQueryId', savedQuery.savedQueryId);
|
||||
updateFieldValues({
|
||||
query: savedQuery.query,
|
||||
savedQueryId: savedQuery.savedQueryId,
|
||||
ecs_mapping: savedQuery.ecs_mapping
|
||||
? map(savedQuery.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
});
|
||||
|
||||
if (!isEmpty(savedQuery.ecs_mapping)) {
|
||||
setFieldValue('ecs_mapping', savedQuery.ecs_mapping);
|
||||
setAdvancedContentState('open');
|
||||
} else {
|
||||
setFieldValue('ecs_mapping', {});
|
||||
}
|
||||
} else {
|
||||
setFieldValue('savedQueryId', null);
|
||||
}
|
||||
},
|
||||
[setFieldValue]
|
||||
[setFieldValue, updateFieldValues]
|
||||
);
|
||||
|
||||
const commands = useMemo(
|
||||
|
@ -251,10 +254,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
|
||||
const queryComponentProps = useMemo(
|
||||
() => ({
|
||||
disabled: queryStatus === 'disabled',
|
||||
commands,
|
||||
}),
|
||||
[queryStatus, commands]
|
||||
[commands]
|
||||
);
|
||||
|
||||
const flyoutFormDefaultValue = useMemo(
|
||||
|
@ -275,9 +277,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
);
|
||||
|
||||
const isSavedQueryDisabled = useMemo(
|
||||
() =>
|
||||
queryComponentProps.disabled || !permissions.runSavedQueries || !permissions.readSavedQueries,
|
||||
[permissions.readSavedQueries, permissions.runSavedQueries, queryComponentProps.disabled]
|
||||
() => !permissions.runSavedQueries || !permissions.readSavedQueries,
|
||||
[permissions.readSavedQueries, permissions.runSavedQueries]
|
||||
);
|
||||
|
||||
const queryFieldStepContent = useMemo(
|
||||
|
@ -314,16 +315,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
forceState={advancedContentState}
|
||||
onToggle={handleToggle}
|
||||
buttonContent="Advanced"
|
||||
isDisabled={queryComponentProps.disabled}
|
||||
>
|
||||
<EuiSpacer size="xs" />
|
||||
<UseField
|
||||
path="ecs_mapping"
|
||||
component={ECSMappingEditorField}
|
||||
query={query}
|
||||
fieldRef={ecsFieldRef}
|
||||
euiFieldProps={ecsFieldProps}
|
||||
/>
|
||||
<ECSMappingEditorField euiFieldProps={ecsFieldProps} />
|
||||
</StyledEuiAccordion>
|
||||
</>
|
||||
) : (
|
||||
|
@ -372,7 +366,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
ecsMappingField,
|
||||
advancedContentState,
|
||||
handleToggle,
|
||||
query,
|
||||
ecsFieldProps,
|
||||
formType,
|
||||
agentSelected,
|
||||
|
@ -399,70 +392,38 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[actionId, agentIds, data?.actions, addToTimeline]
|
||||
);
|
||||
|
||||
const formSteps: EuiContainedStepProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryForm.steps.agentsStepHeading', {
|
||||
defaultMessage: 'Select agents',
|
||||
}),
|
||||
children: <UseField path="agentSelection" component={AgentsTableField} />,
|
||||
status: agentSelected ? 'complete' : 'incomplete',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryForm.steps.queryStepHeading', {
|
||||
defaultMessage: 'Enter query',
|
||||
}),
|
||||
children: queryFieldStepContent,
|
||||
status: queryStatus,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryForm.steps.resultsStepHeading', {
|
||||
defaultMessage: 'Check results',
|
||||
}),
|
||||
children: resultsStepContent,
|
||||
status: resultsStatus,
|
||||
},
|
||||
],
|
||||
[agentSelected, queryFieldStepContent, queryStatus, resultsStepContent, resultsStatus]
|
||||
);
|
||||
|
||||
const simpleForm = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup direction="column">
|
||||
<UseField
|
||||
path="agentSelection"
|
||||
component={agentsField ? AgentsTableField : GhostFormField}
|
||||
/>
|
||||
<EuiFlexItem>{queryFieldStepContent}</EuiFlexItem>
|
||||
<EuiFlexItem>{resultsStepContent}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[agentsField, queryFieldStepContent, resultsStepContent]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue?.agentSelection) {
|
||||
setFieldValue('agentSelection', defaultValue?.agentSelection);
|
||||
if (defaultValue) {
|
||||
updateFieldValues({
|
||||
agentSelection: defaultValue.agentSelection,
|
||||
query: defaultValue.query,
|
||||
savedQueryId: defaultValue.savedQueryId,
|
||||
ecs_mapping: defaultValue.ecs_mapping
|
||||
? map(defaultValue.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
}))
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultValue?.query) {
|
||||
setFieldValue('query', defaultValue?.query);
|
||||
}
|
||||
|
||||
// TODO: Set query and ECS mapping from savedQueryId object
|
||||
if (defaultValue?.savedQueryId) {
|
||||
setFieldValue('savedQueryId', defaultValue?.savedQueryId);
|
||||
}
|
||||
|
||||
if (!isEmpty(defaultValue?.ecs_mapping)) {
|
||||
setFieldValue('ecs_mapping', defaultValue?.ecs_mapping);
|
||||
}
|
||||
}, [defaultValue, setFieldValue]);
|
||||
}, [defaultValue, updateFieldValues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
{formType === 'steps' ? <EuiSteps steps={formSteps} /> : simpleForm}
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="agentSelection"
|
||||
component={!hideAgentsField ? AgentsTableField : GhostFormField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{queryFieldStepContent}</EuiFlexItem>
|
||||
<EuiFlexItem>{resultsStepContent}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<UseField path="savedQueryId" component={GhostFormField} />
|
||||
</Form>
|
||||
{showSavedQueryFlyout ? (
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FIELD_TYPES, FormSchema } from '../../shared_imports';
|
||||
|
||||
export const formSchema: FormSchema = {
|
||||
agents: {
|
||||
type: FIELD_TYPES.MULTI_SELECT,
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXTAREA,
|
||||
validations: [],
|
||||
},
|
||||
};
|
|
@ -28,6 +28,7 @@ interface LiveQueryProps {
|
|||
ecsMappingField?: boolean;
|
||||
enabled?: boolean;
|
||||
formType?: 'steps' | 'simple';
|
||||
hideAgentsField?: boolean;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
}
|
||||
|
||||
|
@ -40,11 +41,11 @@ const LiveQueryComponent: React.FC<LiveQueryProps> = ({
|
|||
savedQueryId,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ecs_mapping,
|
||||
agentsField,
|
||||
queryField,
|
||||
ecsMappingField,
|
||||
formType,
|
||||
enabled,
|
||||
hideAgentsField,
|
||||
addToTimeline,
|
||||
}) => {
|
||||
const { data: hasActionResultsPrivileges, isLoading } = useActionResultsPrivileges();
|
||||
|
@ -108,13 +109,13 @@ const LiveQueryComponent: React.FC<LiveQueryProps> = ({
|
|||
|
||||
return (
|
||||
<LiveQueryForm
|
||||
agentsField={agentId ? !agentId : agentsField}
|
||||
queryField={queryField}
|
||||
ecsMappingField={ecsMappingField}
|
||||
defaultValue={defaultValue}
|
||||
onSuccess={onSuccess}
|
||||
formType={formType}
|
||||
enabled={enabled}
|
||||
hideAgentsField={hideAgentsField}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,14 +6,11 @@
|
|||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import type {
|
||||
ECSMappingEditorFieldProps,
|
||||
ECSMappingEditorFieldRef,
|
||||
} from './ecs_mapping_editor_field';
|
||||
import type { ECSMappingEditorFieldProps } from './ecs_mapping_editor_field';
|
||||
|
||||
const LazyECSMappingEditorField = lazy(() => import('./ecs_mapping_editor_field'));
|
||||
|
||||
export type { ECSMappingEditorFieldProps, ECSMappingEditorFieldRef };
|
||||
export type { ECSMappingEditorFieldProps };
|
||||
export const ECSMappingEditorField = (props: ECSMappingEditorFieldProps) => (
|
||||
<Suspense fallback={null}>
|
||||
<LazyECSMappingEditorField {...props} />
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { map } from 'lodash';
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiTitle,
|
||||
|
@ -19,17 +19,17 @@ import {
|
|||
EuiButton,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState, useRef } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { CodeEditorField } from '../../saved_queries/form/code_editor_field';
|
||||
import { Form, getUseField, Field, useFormData } from '../../shared_imports';
|
||||
import { Form, getUseField, Field } from '../../shared_imports';
|
||||
import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field';
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants';
|
||||
import { UsePackQueryFormProps, PackFormData, usePackQueryForm } from './use_pack_query_form';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
import { ECSMappingEditorField, ECSMappingEditorFieldRef } from './lazy_ecs_mapping_editor_field';
|
||||
import { ECSMappingEditorField } from './lazy_ecs_mapping_editor_field';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -46,70 +46,46 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
onSave,
|
||||
onClose,
|
||||
}) => {
|
||||
const ecsFieldRef = useRef<ECSMappingEditorFieldRef>();
|
||||
const [isEditMode] = useState(!!defaultValue);
|
||||
const { form } = usePackQueryForm({
|
||||
uniqueQueryIds,
|
||||
defaultValue,
|
||||
handleSubmit: async (payload, isValid) => {
|
||||
const ecsFieldValue = await ecsFieldRef?.current?.validate();
|
||||
const isEcsFieldValueValid =
|
||||
ecsFieldValue &&
|
||||
Object.values(ecsFieldValue).every((field) => !isEmpty(Object.values(field)[0]));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (isValid && isEcsFieldValueValid) {
|
||||
onSave({
|
||||
...payload,
|
||||
...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }),
|
||||
});
|
||||
handleSubmit: async (payload, isValid) =>
|
||||
new Promise((resolve) => {
|
||||
if (isValid) {
|
||||
onSave(payload);
|
||||
onClose();
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const { submit, setFieldValue, reset, isSubmitting, validate } = form;
|
||||
|
||||
const [{ query }] = useFormData({
|
||||
form,
|
||||
watch: ['query'],
|
||||
});
|
||||
const { submit, isSubmitting, updateFieldValues } = form;
|
||||
|
||||
const handleSetQueryValue = useCallback(
|
||||
(savedQuery) => {
|
||||
reset();
|
||||
|
||||
if (savedQuery) {
|
||||
setFieldValue('id', savedQuery.id);
|
||||
setFieldValue('query', savedQuery.query);
|
||||
|
||||
if (savedQuery.description) {
|
||||
setFieldValue('description', savedQuery.description);
|
||||
}
|
||||
|
||||
if (savedQuery.interval) {
|
||||
setFieldValue('interval', savedQuery.interval);
|
||||
}
|
||||
|
||||
if (savedQuery.platform) {
|
||||
setFieldValue('platform', savedQuery.platform);
|
||||
}
|
||||
|
||||
if (savedQuery.version) {
|
||||
setFieldValue('version', [savedQuery.version]);
|
||||
}
|
||||
|
||||
if (savedQuery.ecs_mapping) {
|
||||
setFieldValue('ecs_mapping', savedQuery.ecs_mapping);
|
||||
}
|
||||
updateFieldValues({
|
||||
id: savedQuery.id,
|
||||
query: savedQuery.query,
|
||||
description: savedQuery.description,
|
||||
platform: savedQuery.platform,
|
||||
version: savedQuery.version,
|
||||
interval: savedQuery.interval,
|
||||
// @ts-expect-error update types
|
||||
ecs_mapping:
|
||||
map(savedQuery.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
})) ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
validate();
|
||||
},
|
||||
[reset, validate, setFieldValue]
|
||||
[updateFieldValues]
|
||||
);
|
||||
/* Avoids accidental closing of the flyout when the user clicks outside of the flyout */
|
||||
const maskProps = useMemo(() => ({ onClick: () => ({}) }), []);
|
||||
|
@ -190,12 +166,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="ecs_mapping"
|
||||
component={ECSMappingEditorField}
|
||||
query={query}
|
||||
fieldRef={ecsFieldRef}
|
||||
/>
|
||||
<ECSMappingEditorField />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
|
|
|
@ -71,7 +71,7 @@ export const createFormSchema = (ids: Set<string>) => ({
|
|||
validations: [],
|
||||
},
|
||||
ecs_mapping: {
|
||||
defaultValue: {},
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.JSON,
|
||||
validations: [],
|
||||
},
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isArray, isEmpty, xor } from 'lodash';
|
||||
import { isArray, isEmpty, xor, map } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { convertECSMappingToObject } from '../../../common/schemas/common/utils';
|
||||
import { FormConfig, useForm } from '../../shared_imports';
|
||||
import { createFormSchema } from './schema';
|
||||
|
||||
|
@ -37,11 +38,14 @@ export interface PackFormData {
|
|||
platform?: string | undefined;
|
||||
version?: string | undefined;
|
||||
ecs_mapping?:
|
||||
| Record<
|
||||
string,
|
||||
{
|
||||
field: string;
|
||||
}
|
||||
| Array<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
field?: string;
|
||||
value?: string;
|
||||
}
|
||||
>
|
||||
>
|
||||
| undefined;
|
||||
}
|
||||
|
@ -76,7 +80,7 @@ export const usePackQueryForm = ({
|
|||
id: '',
|
||||
query: '',
|
||||
interval: 3600,
|
||||
ecs_mapping: {},
|
||||
ecs_mapping: [],
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) =>
|
||||
|
@ -100,6 +104,9 @@ export const usePackQueryForm = ({
|
|||
|
||||
if (isEmpty(draft.ecs_mapping)) {
|
||||
delete draft.ecs_mapping;
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
draft.ecs_mapping = convertECSMappingToObject(payload.ecs_mapping);
|
||||
}
|
||||
|
||||
return draft;
|
||||
|
@ -114,7 +121,17 @@ export const usePackQueryForm = ({
|
|||
interval: payload.interval,
|
||||
platform: payload.platform,
|
||||
version: payload.version ? [payload.version] : [],
|
||||
ecs_mapping: payload.ecs_mapping ?? {},
|
||||
ecs_mapping: !isArray(payload.ecs_mapping)
|
||||
? map(payload.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
// @ts-expect-error update types
|
||||
type: Object.keys(value)[0],
|
||||
// @ts-expect-error update types
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
}))
|
||||
: payload.ecs_mapping,
|
||||
};
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
|
|
|
@ -109,11 +109,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
]);
|
||||
const [columns, setColumns] = useState<EuiDataGridColumn[]>([]);
|
||||
|
||||
const {
|
||||
data: allResultsData,
|
||||
isFetched,
|
||||
isLoading,
|
||||
} = useAllResults({
|
||||
const { data: allResultsData, isLoading } = useAllResults({
|
||||
actionId,
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
|
@ -361,20 +357,44 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
|
||||
return !!(aggregations.totalResponded !== agentIds?.length);
|
||||
return !!(
|
||||
aggregations.totalResponded !== agentIds?.length ||
|
||||
allResultsData?.totalCount !== aggregations?.totalRowCount ||
|
||||
(allResultsData?.totalCount && !allResultsData?.edges.length)
|
||||
);
|
||||
}),
|
||||
[agentIds?.length, aggregations.failed, aggregations.totalResponded, expired]
|
||||
[
|
||||
agentIds?.length,
|
||||
aggregations.totalResponded,
|
||||
aggregations?.totalRowCount,
|
||||
allResultsData?.edges.length,
|
||||
allResultsData?.totalCount,
|
||||
expired,
|
||||
]
|
||||
);
|
||||
|
||||
if (!hasActionResultsPrivileges) {
|
||||
return (
|
||||
<EuiCallOut title="Missing privileges" color="danger" iconType="alert">
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQuery.permissionDeniedPromptTitle"
|
||||
defaultMessage="Permission denied"
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<p>
|
||||
{'Your user role doesn’t have index read permissions on the '}
|
||||
<EuiCode>logs-{OSQUERY_INTEGRATION_NAME}.result*</EuiCode>
|
||||
{
|
||||
'index. Access to this index is required to view osquery results. Administrators can update role permissions in Stack Management > Roles.'
|
||||
}
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQuery.permissionDeniedPromptBody"
|
||||
defaultMessage="To view query results, ask your administrator to update your user role to have index {read} privileges on the {logs} index."
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{
|
||||
read: <EuiCode>read</EuiCode>,
|
||||
logs: <EuiCode>logs-{OSQUERY_INTEGRATION_NAME}.result*</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
@ -388,13 +408,12 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
<>
|
||||
{isLive && <EuiProgress color="primary" size="xs" />}
|
||||
|
||||
{isFetched && !allResultsData?.edges.length && !aggregations?.totalRowCount ? (
|
||||
{!allResultsData?.edges.length ? (
|
||||
<>
|
||||
<EuiCallOut title={generateEmptyDataMessage(aggregations.totalResponded)} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : (
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={allResultsData?.edges}>
|
||||
<EuiDataGrid
|
||||
data-test-subj="osqueryResultsTable"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import {
|
||||
createFilter,
|
||||
generateTablePaginationOptions,
|
||||
|
@ -62,7 +62,7 @@ export const useAllResults = ({
|
|||
return useQuery(
|
||||
['allActionResults', { actionId, activePage, limit, sort }],
|
||||
async () => {
|
||||
const responseData = await firstValueFrom(
|
||||
const responseData = await lastValueFrom(
|
||||
data.search.search<ResultsRequestOptions, ResultsStrategyResponse>(
|
||||
{
|
||||
actionId,
|
||||
|
|
|
@ -13,12 +13,12 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { Form } from '../../../shared_imports';
|
||||
import { SavedQueryForm, SavedQueryFormRefObject } from '../../../saved_queries/form';
|
||||
import { SavedQueryForm } from '../../../saved_queries/form';
|
||||
import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_form';
|
||||
|
||||
interface EditSavedQueryFormProps {
|
||||
|
@ -32,19 +32,17 @@ const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
|
|||
handleSubmit,
|
||||
viewMode,
|
||||
}) => {
|
||||
const savedQueryFormRef = useRef<SavedQueryFormRefObject>(null);
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
defaultValue,
|
||||
savedQueryFormRef,
|
||||
handleSubmit,
|
||||
});
|
||||
const { submit, isSubmitting } = form;
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<SavedQueryForm ref={savedQueryFormRef} viewMode={viewMode} hasPlayground />
|
||||
<SavedQueryForm viewMode={viewMode} hasPlayground />
|
||||
{!viewMode && (
|
||||
<>
|
||||
<EuiBottomBar>
|
||||
|
|
|
@ -13,12 +13,12 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { Form } from '../../../shared_imports';
|
||||
import { SavedQueryForm, SavedQueryFormRefObject } from '../../../saved_queries/form';
|
||||
import { SavedQueryForm } from '../../../saved_queries/form';
|
||||
import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_form';
|
||||
|
||||
interface NewSavedQueryFormProps {
|
||||
|
@ -30,19 +30,17 @@ const NewSavedQueryFormComponent: React.FC<NewSavedQueryFormProps> = ({
|
|||
defaultValue,
|
||||
handleSubmit,
|
||||
}) => {
|
||||
const savedQueryFormRef = useRef<SavedQueryFormRefObject>(null);
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
defaultValue,
|
||||
savedQueryFormRef,
|
||||
handleSubmit,
|
||||
});
|
||||
const { submit, isSubmitting, isValid } = form;
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<SavedQueryForm ref={savedQueryFormRef} hasPlayground isValid={isValid} />
|
||||
<SavedQueryForm hasPlayground isValid={isValid} />
|
||||
<EuiBottomBar>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -13,25 +13,15 @@ import {
|
|||
EuiText,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import React, {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../packs/queries/constants';
|
||||
import { PlatformCheckBoxGroupField } from '../../packs/queries/platform_checkbox_group_field';
|
||||
import { Field, getUseField, UseField, useFormData } from '../../shared_imports';
|
||||
import { Field, getUseField, UseField } from '../../shared_imports';
|
||||
import { CodeEditorField } from './code_editor_field';
|
||||
import {
|
||||
ECSMappingEditorField,
|
||||
ECSMappingEditorFieldRef,
|
||||
} from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { PlaygroundFlyout } from './playground_flyout';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
@ -41,131 +31,112 @@ interface SavedQueryFormProps {
|
|||
hasPlayground?: boolean;
|
||||
isValid?: boolean;
|
||||
}
|
||||
export interface SavedQueryFormRefObject {
|
||||
validateEcsMapping: ECSMappingEditorFieldRef['validate'];
|
||||
}
|
||||
|
||||
const SavedQueryFormComponent = forwardRef<SavedQueryFormRefObject, SavedQueryFormProps>(
|
||||
({ viewMode, hasPlayground, isValid }, ref) => {
|
||||
const [playgroundVisible, setPlaygroundVisible] = useState(false);
|
||||
const ecsFieldRef = useRef<ECSMappingEditorFieldRef>();
|
||||
const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({
|
||||
viewMode,
|
||||
hasPlayground,
|
||||
isValid,
|
||||
}) => {
|
||||
const [playgroundVisible, setPlaygroundVisible] = useState(false);
|
||||
|
||||
const euiFieldProps = useMemo(
|
||||
() => ({
|
||||
isDisabled: !!viewMode,
|
||||
const euiFieldProps = useMemo(
|
||||
() => ({
|
||||
isDisabled: !!viewMode,
|
||||
}),
|
||||
[viewMode]
|
||||
);
|
||||
|
||||
const handleHidePlayground = useCallback(() => setPlaygroundVisible(false), []);
|
||||
|
||||
const handleTogglePlayground = useCallback(
|
||||
() => setPlaygroundVisible((prevValue) => !prevValue),
|
||||
[]
|
||||
);
|
||||
|
||||
const intervalEuiFieldProps = useMemo(
|
||||
() => ({
|
||||
append: 's',
|
||||
...euiFieldProps,
|
||||
}),
|
||||
[euiFieldProps]
|
||||
);
|
||||
|
||||
const versionEuiFieldProps = useMemo(
|
||||
() => ({
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
placeholder: i18n.translate('xpack.osquery.pack.queriesTable.osqueryVersionAllLabel', {
|
||||
defaultMessage: 'ALL',
|
||||
}),
|
||||
[viewMode]
|
||||
);
|
||||
options: ALL_OSQUERY_VERSIONS_OPTIONS,
|
||||
onCreateOption: undefined,
|
||||
...euiFieldProps,
|
||||
}),
|
||||
[euiFieldProps]
|
||||
);
|
||||
|
||||
const [{ query }] = useFormData({ watch: ['query'] });
|
||||
|
||||
const handleHidePlayground = useCallback(() => setPlaygroundVisible(false), []);
|
||||
|
||||
const handleTogglePlayground = useCallback(
|
||||
() => setPlaygroundVisible((prevValue) => !prevValue),
|
||||
[]
|
||||
);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
validateEcsMapping: () => {
|
||||
if (ecsFieldRef.current) {
|
||||
return ecsFieldRef.current.validate();
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonUseField path="id" euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer />
|
||||
<UseField path="query" component={CodeEditorField} euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer size="xl" />
|
||||
return (
|
||||
<>
|
||||
<CommonUseField path="id" euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer />
|
||||
<UseField path="query" component={CodeEditorField} euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<ECSMappingEditorField />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!viewMode && hasPlayground && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="ecs_mapping"
|
||||
component={ECSMappingEditorField}
|
||||
query={query}
|
||||
fieldRef={ecsFieldRef}
|
||||
/>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="play" onClick={handleTogglePlayground}>
|
||||
Test configuration
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!viewMode && hasPlayground && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="play" onClick={handleTogglePlayground}>
|
||||
Test configuration
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueries.form.packConfigSection.title"
|
||||
defaultMessage="Pack configuration"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued">
|
||||
)}
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueries.form.packConfigSection.description"
|
||||
defaultMessage="The options listed below are optional and are only applied when the query is assigned to a pack."
|
||||
id="xpack.osquery.savedQueries.form.packConfigSection.title"
|
||||
defaultMessage="Pack configuration"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="interval"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{ append: 's', ...euiFieldProps }}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.savedQueries.form.packConfigSection.description"
|
||||
defaultMessage="The options listed below are optional and are only applied when the query is assigned to a pack."
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<CommonUseField
|
||||
path="version"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
placeholder: i18n.translate(
|
||||
'xpack.osquery.pack.queriesTable.osqueryVersionAllLabel',
|
||||
{
|
||||
defaultMessage: 'ALL',
|
||||
}
|
||||
),
|
||||
options: ALL_OSQUERY_VERSIONS_OPTIONS,
|
||||
onCreateOption: undefined,
|
||||
...euiFieldProps,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField path="platform" component={PlatformCheckBoxGroupField} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{playgroundVisible && (
|
||||
<PlaygroundFlyout
|
||||
enabled={isValid !== undefined ? isValid : true}
|
||||
onClose={handleHidePlayground}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField path="interval" euiFieldProps={intervalEuiFieldProps} />
|
||||
<EuiSpacer size="m" />
|
||||
<CommonUseField path="version" euiFieldProps={versionEuiFieldProps} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField path="platform" component={PlatformCheckBoxGroupField} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{playgroundVisible && (
|
||||
<PlaygroundFlyout
|
||||
enabled={isValid !== undefined ? isValid : true}
|
||||
onClose={handleHidePlayground}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SavedQueryFormComponent.displayName = 'SavedQueryForm';
|
||||
|
||||
export const SavedQueryForm = React.memo(SavedQueryFormComponent);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
|
@ -26,11 +26,14 @@ interface PlaygroundFlyoutProps {
|
|||
}
|
||||
|
||||
const PlaygroundFlyoutComponent: React.FC<PlaygroundFlyoutProps> = ({ enabled, onClose }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const [{ query, ecs_mapping, id }] = useFormData({
|
||||
const [{ query, ecs_mapping: ecsMapping, id }, formDataSerializer] = useFormData({
|
||||
watch: ['query', 'ecs_mapping', 'savedQueryId'],
|
||||
});
|
||||
|
||||
/* recalculate the form data when ecs_mapping changes */
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const serializedFormData = useMemo(() => formDataSerializer(), [ecsMapping, formDataSerializer]);
|
||||
|
||||
return (
|
||||
<EuiFlyout type="push" size="m" onClose={onClose}>
|
||||
<StyledEuiFlyoutHeader hasBorder>
|
||||
|
@ -48,7 +51,7 @@ const PlaygroundFlyoutComponent: React.FC<PlaygroundFlyoutProps> = ({ enabled, o
|
|||
enabled={enabled && query !== ''}
|
||||
formType="simple"
|
||||
query={query}
|
||||
ecs_mapping={ecs_mapping}
|
||||
ecs_mapping={serializedFormData.ecs_mapping}
|
||||
savedQueryId={id}
|
||||
queryField={false}
|
||||
ecsMappingField={false}
|
||||
|
|
|
@ -8,27 +8,22 @@
|
|||
import { isArray, isEmpty, map } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { convertECSMappingToObject } from '../../../common/schemas/common/utils';
|
||||
import { useForm } from '../../shared_imports';
|
||||
import { createFormSchema } from '../../packs/queries/schema';
|
||||
import { PackFormData } from '../../packs/queries/use_pack_query_form';
|
||||
import { useSavedQueries } from '../use_saved_queries';
|
||||
import { SavedQueryFormRefObject } from '.';
|
||||
|
||||
const SAVED_QUERY_FORM_ID = 'savedQueryForm';
|
||||
|
||||
interface UseSavedQueryFormProps {
|
||||
defaultValue?: unknown;
|
||||
handleSubmit: (payload: unknown) => Promise<void>;
|
||||
savedQueryFormRef: RefObject<SavedQueryFormRefObject>;
|
||||
}
|
||||
|
||||
export const useSavedQueryForm = ({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
savedQueryFormRef,
|
||||
}: UseSavedQueryFormProps) => {
|
||||
export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryFormProps) => {
|
||||
const { data } = useSavedQueries({});
|
||||
const ids: string[] = useMemo<string[]>(
|
||||
() => map(data?.saved_objects, 'attributes.id') ?? [],
|
||||
|
@ -50,14 +45,9 @@ export const useSavedQueryForm = ({
|
|||
id: SAVED_QUERY_FORM_ID + uuid.v4(),
|
||||
schema: formSchema,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
const ecsFieldValue = await savedQueryFormRef?.current?.validateEcsMapping();
|
||||
|
||||
if (isValid && !!ecsFieldValue) {
|
||||
if (isValid) {
|
||||
try {
|
||||
await handleSubmit({
|
||||
...formData,
|
||||
ecs_mapping: ecsFieldValue,
|
||||
});
|
||||
await handleSubmit(formData);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
|
@ -82,9 +72,12 @@ export const useSavedQueryForm = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (isEmpty(draft.ecs_mapping)) {
|
||||
if (isEmpty(payload.ecs_mapping)) {
|
||||
// @ts-expect-error update types
|
||||
delete draft.ecs_mapping;
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
draft.ecs_mapping = convertECSMappingToObject(payload.ecs_mapping);
|
||||
}
|
||||
|
||||
// @ts-expect-error update types
|
||||
|
@ -103,7 +96,16 @@ export const useSavedQueryForm = ({
|
|||
interval: payload.interval ?? 3600,
|
||||
platform: payload.platform,
|
||||
version: payload.version ? [payload.version] : [],
|
||||
ecs_mapping: payload.ecs_mapping ?? {},
|
||||
ecs_mapping:
|
||||
(!isEmpty(payload.ecs_mapping) &&
|
||||
map(payload.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
}))) ??
|
||||
[],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ import { SimpleSavedObject } from '@kbn/core/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { useSavedQueries } from './use_saved_queries';
|
||||
import { useFormData } from '../shared_imports';
|
||||
|
@ -47,10 +46,7 @@ const SavedQueriesDropdownComponent: React.FC<SavedQueriesDropdownProps> = ({
|
|||
}) => {
|
||||
const [selectedOptions, setSelectedOptions] = useState([]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const [{ query, ecs_mapping, savedQueryId }] = useFormData({
|
||||
watch: ['ecs_mapping', 'query', 'savedQueryId'],
|
||||
});
|
||||
const [{ savedQueryId }] = useFormData();
|
||||
|
||||
const { data } = useSavedQueries({});
|
||||
|
||||
|
@ -122,15 +118,11 @@ const SavedQueriesDropdownComponent: React.FC<SavedQueriesDropdownProps> = ({
|
|||
if (
|
||||
selectedOptions.length &&
|
||||
// @ts-expect-error update types
|
||||
(selectedOptions[0].value.savedQueryId !== savedQueryId ||
|
||||
// @ts-expect-error update types
|
||||
selectedOptions[0].value.query !== query ||
|
||||
// @ts-expect-error update types
|
||||
!deepEqual(selectedOptions[0].value.ecs_mapping, ecs_mapping))
|
||||
selectedOptions[0].value.savedQueryId !== savedQueryId
|
||||
) {
|
||||
setSelectedOptions([]);
|
||||
}
|
||||
}, [ecs_mapping, query, savedQueryId, selectedOptions]);
|
||||
}, [savedQueryId, selectedOptions]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
|
|
|
@ -17,12 +17,12 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { Form } from '../shared_imports';
|
||||
import { useSavedQueryForm } from './form/use_saved_query_form';
|
||||
import { SavedQueryForm, SavedQueryFormRefObject } from './form';
|
||||
import { SavedQueryForm } from './form';
|
||||
import { useCreateSavedQuery } from './use_create_saved_query';
|
||||
|
||||
interface AddQueryFlyoutProps {
|
||||
|
@ -38,7 +38,6 @@ const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({
|
|||
onClose,
|
||||
isExternal,
|
||||
}) => {
|
||||
const savedQueryFormRef = useRef<SavedQueryFormRefObject>(null);
|
||||
const createSavedQueryMutation = useCreateSavedQuery({ withRedirect: false });
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
|
@ -48,7 +47,6 @@ const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({
|
|||
|
||||
const { form } = useSavedQueryForm({
|
||||
defaultValue,
|
||||
savedQueryFormRef,
|
||||
handleSubmit,
|
||||
});
|
||||
const { submit, isSubmitting } = form;
|
||||
|
@ -74,7 +72,7 @@ const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({
|
|||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
<SavedQueryForm ref={savedQueryFormRef} />
|
||||
<SavedQueryForm />
|
||||
</Form>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { EuiErrorBoundary, EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import {
|
||||
AGENT_STATUS_ERROR,
|
||||
EMPTY_PROMPT,
|
||||
|
@ -22,16 +23,19 @@ import { queryClient } from '../../query_client';
|
|||
import { OsqueryIcon } from '../../components/osquery_icon';
|
||||
import { KibanaThemeProvider } from '../../shared_imports';
|
||||
import { useIsOsqueryAvailable } from './use_is_osquery_available';
|
||||
import { StartPlugins } from '../../types';
|
||||
|
||||
interface OsqueryActionProps {
|
||||
agentId?: string;
|
||||
formType: 'steps' | 'simple';
|
||||
hideAgentsField?: boolean;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
}
|
||||
|
||||
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
||||
agentId,
|
||||
formType = 'simple',
|
||||
hideAgentsField,
|
||||
addToTimeline,
|
||||
}) => {
|
||||
const permissions = useKibana().services.application.capabilities.osquery;
|
||||
|
@ -103,18 +107,37 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
return <LiveQuery formType={formType} agentId={agentId} addToTimeline={addToTimeline} />;
|
||||
return (
|
||||
<LiveQuery
|
||||
formType={formType}
|
||||
agentId={agentId}
|
||||
hideAgentsField={hideAgentsField}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const OsqueryAction = React.memo(OsqueryActionComponent);
|
||||
|
||||
// @ts-expect-error update types
|
||||
const OsqueryActionWrapperComponent = ({ services, agentId, formType, addToTimeline }) => (
|
||||
type OsqueryActionWrapperProps = { services: CoreStart & StartPlugins } & OsqueryActionProps;
|
||||
|
||||
const OsqueryActionWrapperComponent: React.FC<OsqueryActionWrapperProps> = ({
|
||||
services,
|
||||
agentId,
|
||||
formType,
|
||||
hideAgentsField = false,
|
||||
addToTimeline,
|
||||
}) => (
|
||||
<KibanaThemeProvider theme$={services.theme.theme$}>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EuiErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OsqueryAction agentId={agentId} formType={formType} addToTimeline={addToTimeline} />
|
||||
<OsqueryAction
|
||||
agentId={agentId}
|
||||
formType={formType}
|
||||
hideAgentsField={hideAgentsField}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</EuiErrorBoundary>
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -23,6 +23,8 @@ export {
|
|||
Form,
|
||||
FormDataProvider,
|
||||
UseArray,
|
||||
ArrayItem,
|
||||
FormArrayField,
|
||||
UseField,
|
||||
UseMultiFields,
|
||||
useForm,
|
||||
|
|
|
@ -21965,9 +21965,6 @@
|
|||
"xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "Afficher l'historique des recherches en direct",
|
||||
"xpack.osquery.liveQueryForm.form.saveForLaterButtonLabel": "Enregistrer pour plus tard",
|
||||
"xpack.osquery.liveQueryForm.form.submitButtonLabel": "Envoyer",
|
||||
"xpack.osquery.liveQueryForm.steps.agentsStepHeading": "Sélectionner les agents",
|
||||
"xpack.osquery.liveQueryForm.steps.queryStepHeading": "Entrer la recherche",
|
||||
"xpack.osquery.liveQueryForm.steps.resultsStepHeading": "Vérifier les résultats",
|
||||
"xpack.osquery.liveQueryResults.table.agentColumnTitle": "agent",
|
||||
"xpack.osquery.liveQueryResults.table.fieldMappedLabel": "Le champ est mappé à",
|
||||
"xpack.osquery.newLiveQuery.pageTitle": "Nouvelle recherche en direct",
|
||||
|
|
|
@ -22118,9 +22118,6 @@
|
|||
"xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "ライブクエリ履歴を表示",
|
||||
"xpack.osquery.liveQueryForm.form.saveForLaterButtonLabel": "後で使用するために保存する",
|
||||
"xpack.osquery.liveQueryForm.form.submitButtonLabel": "送信",
|
||||
"xpack.osquery.liveQueryForm.steps.agentsStepHeading": "エージェントを選択",
|
||||
"xpack.osquery.liveQueryForm.steps.queryStepHeading": "クエリを入力",
|
||||
"xpack.osquery.liveQueryForm.steps.resultsStepHeading": "結果を確認",
|
||||
"xpack.osquery.liveQueryResults.table.agentColumnTitle": "エージェント",
|
||||
"xpack.osquery.liveQueryResults.table.fieldMappedLabel": "フィールドは以下にマッピングされています",
|
||||
"xpack.osquery.newLiveQuery.pageTitle": "新しいライブクエリ",
|
||||
|
|
|
@ -22149,9 +22149,6 @@
|
|||
"xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "查看实时查询历史记录",
|
||||
"xpack.osquery.liveQueryForm.form.saveForLaterButtonLabel": "保存,以后继续",
|
||||
"xpack.osquery.liveQueryForm.form.submitButtonLabel": "提交",
|
||||
"xpack.osquery.liveQueryForm.steps.agentsStepHeading": "选择代理",
|
||||
"xpack.osquery.liveQueryForm.steps.queryStepHeading": "输入查询",
|
||||
"xpack.osquery.liveQueryForm.steps.resultsStepHeading": "检查结果",
|
||||
"xpack.osquery.liveQueryResults.table.agentColumnTitle": "代理",
|
||||
"xpack.osquery.liveQueryResults.table.fieldMappedLabel": "字段已映射到",
|
||||
"xpack.osquery.newLiveQuery.pageTitle": "新建实时查询",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue