Co-authored-by: Patryk Kopyciński <patryk.kopycinski@elastic.co>
This commit is contained in:
parent
8a55fa4fdc
commit
e982d1023a
|
@ -55,8 +55,9 @@ export type SavedQueryIdOrUndefined = t.TypeOf<typeof savedQueryIdOrUndefined>;
|
|||
|
||||
export const ecsMapping = t.record(
|
||||
t.string,
|
||||
t.type({
|
||||
t.partial({
|
||||
field: t.string,
|
||||
value: t.string,
|
||||
})
|
||||
);
|
||||
export type ECSMapping = t.TypeOf<typeof ecsMapping>;
|
||||
|
|
|
@ -13,7 +13,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import { AgentIdToName } from '../agents/agent_id_to_name';
|
||||
import { useActionResults } from './use_action_results';
|
||||
import { useAllResults } from '../results/use_all_results';
|
||||
import { Direction } from '../../common/search_strategy';
|
||||
import { useActionResultsPrivileges } from './use_action_privileges';
|
||||
|
||||
|
@ -70,38 +69,8 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
});
|
||||
}
|
||||
|
||||
const { data: logsResults } = useAllResults({
|
||||
actionId,
|
||||
activePage: pageIndex,
|
||||
limit: pageSize,
|
||||
sort: [
|
||||
{
|
||||
field: '@timestamp',
|
||||
direction: Direction.asc,
|
||||
},
|
||||
],
|
||||
isLive,
|
||||
skip: !hasActionResultsPrivileges,
|
||||
});
|
||||
|
||||
const renderAgentIdColumn = useCallback((agentId) => <AgentIdToName agentId={agentId} />, []);
|
||||
|
||||
const renderRowsColumn = useCallback(
|
||||
(_, item) => {
|
||||
if (!logsResults) return '-';
|
||||
const agentId = item.fields.agent_id[0];
|
||||
|
||||
return (
|
||||
// @ts-expect-error update types
|
||||
logsResults?.rawResponse?.aggregations?.count_by_agent_id?.buckets?.find(
|
||||
// @ts-expect-error update types
|
||||
(bucket) => bucket.key === agentId
|
||||
)?.doc_count ?? '-'
|
||||
);
|
||||
},
|
||||
[logsResults]
|
||||
);
|
||||
|
||||
const renderRowsColumn = useCallback((rowsCount) => rowsCount ?? '-', []);
|
||||
const renderStatusColumn = useCallback(
|
||||
(_, item) => {
|
||||
if (!item.fields.completed_at) {
|
||||
|
@ -145,7 +114,7 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
render: renderAgentIdColumn,
|
||||
},
|
||||
{
|
||||
field: 'fields.rows[0]',
|
||||
field: '_source.action_response.osquery.count',
|
||||
name: i18n.translate(
|
||||
'xpack.osquery.liveQueryActionResults.table.resultRowsNumberColumnTitle',
|
||||
{
|
||||
|
@ -177,18 +146,9 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
|
||||
const uniqueAgentsRepliedCount =
|
||||
// @ts-expect-error update types
|
||||
logsResults?.rawResponse.aggregations?.unique_agents.value ?? 0;
|
||||
|
||||
return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed);
|
||||
return !!(aggregations.totalResponded !== agentIds?.length);
|
||||
});
|
||||
}, [
|
||||
agentIds?.length,
|
||||
aggregations.failed,
|
||||
expired,
|
||||
logsResults?.rawResponse.aggregations?.unique_agents,
|
||||
]);
|
||||
}, [agentIds?.length, aggregations.totalResponded, expired]);
|
||||
|
||||
return edges.length ? (
|
||||
<EuiInMemoryTable loading={isLive} items={edges} columns={columns} pagination={pagination} />
|
||||
|
|
|
@ -84,6 +84,9 @@ export const useActionResults = ({
|
|||
const totalResponded =
|
||||
// @ts-expect-error update types
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count ?? 0;
|
||||
const totalRowCount =
|
||||
// @ts-expect-error update types
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.rows_count?.value ?? 0;
|
||||
const aggsBuckets =
|
||||
// @ts-expect-error update types
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.responses.buckets;
|
||||
|
@ -100,6 +103,7 @@ export const useActionResults = ({
|
|||
...responseData,
|
||||
edges: reverse(uniqBy('fields.agent_id[0]', flatten([responseData.edges, previousEdges]))),
|
||||
aggregations: {
|
||||
totalRowCount,
|
||||
totalResponded,
|
||||
// @ts-expect-error update types
|
||||
successful: aggsBuckets?.find((bucket) => bucket.key === 'success')?.doc_count ?? 0,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -165,16 +165,6 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
|
|||
defaultValue: {
|
||||
config: JSON.stringify(get(newPolicy, 'inputs[0].config.osquery.value', {}), null, 2),
|
||||
},
|
||||
serializer: (formData) => {
|
||||
let config;
|
||||
try {
|
||||
// @ts-expect-error update types
|
||||
config = JSON.parse(formData.config);
|
||||
} catch (e) {
|
||||
config = {};
|
||||
}
|
||||
return { config };
|
||||
},
|
||||
schema: {
|
||||
config: {
|
||||
label: i18n.translate('xpack.osquery.fleetIntegration.osqueryConfig.configFieldLabel', {
|
||||
|
@ -243,10 +233,16 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
|
|||
if (isValid === undefined) return;
|
||||
|
||||
const updatedPolicy = produce(newPolicy, (draft) => {
|
||||
if (isEmpty(config)) {
|
||||
let parsedConfig;
|
||||
try {
|
||||
parsedConfig = JSON.parse(config);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
|
||||
if (isEmpty(parsedConfig)) {
|
||||
unset(draft, 'inputs[0].config');
|
||||
} else {
|
||||
set(draft, 'inputs[0].config.osquery.value', config);
|
||||
set(draft, 'inputs[0].config.osquery.value', parsedConfig);
|
||||
}
|
||||
return draft;
|
||||
});
|
||||
|
|
|
@ -98,14 +98,17 @@ const PackFormComponent: React.FC<PackFormProps> = ({ defaultValue, editMode = f
|
|||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.pack.form.descriptionFieldLabel', {
|
||||
defaultMessage: 'Description',
|
||||
defaultMessage: 'Description (optional)',
|
||||
}),
|
||||
},
|
||||
policy_ids: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldLabel', {
|
||||
defaultMessage: 'Agent policies',
|
||||
defaultMessage: 'Agent policies (optional)',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldHelpText', {
|
||||
defaultMessage: 'Queries in this pack are scheduled for agents in the selected policies.',
|
||||
}),
|
||||
},
|
||||
enabled: {
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n/react';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {
|
||||
TypedLensByValueInput,
|
||||
|
@ -29,7 +30,7 @@ import {
|
|||
PieVisualizationState,
|
||||
} from '../../../lens/public';
|
||||
import { FilterStateStore, IndexPattern } from '../../../../../src/plugins/data/common';
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
|
||||
import { ScheduledQueryErrorsTable } from './scheduled_query_errors_table';
|
||||
import { usePackQueryLastResults } from './use_pack_query_last_results';
|
||||
|
@ -207,8 +208,6 @@ const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProp
|
|||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event));
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
lensService?.navigateToPrefilledEditor(
|
||||
|
@ -222,7 +221,7 @@ const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProp
|
|||
attributes: getLensAttributes(actionId, agentIds),
|
||||
},
|
||||
{
|
||||
openInNewTab,
|
||||
openInNewTab: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
@ -337,7 +336,7 @@ const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverAction
|
|||
|
||||
if (buttonType === ViewResultsActionButtonType.button) {
|
||||
return (
|
||||
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl}>
|
||||
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl} target="_blank">
|
||||
{VIEW_IN_DISCOVER}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
@ -378,6 +377,7 @@ interface ScheduledQueryLastResultsProps {
|
|||
actionId: string;
|
||||
queryId: string;
|
||||
interval: number;
|
||||
logsIndexPattern: IndexPattern | undefined;
|
||||
toggleErrors: (payload: { queryId: string; interval: number }) => void;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
@ -386,12 +386,10 @@ const ScheduledQueryLastResults: React.FC<ScheduledQueryLastResultsProps> = ({
|
|||
actionId,
|
||||
queryId,
|
||||
interval,
|
||||
logsIndexPattern,
|
||||
toggleErrors,
|
||||
expanded,
|
||||
}) => {
|
||||
const data = useKibana().services.data;
|
||||
const [logsIndexPattern, setLogsIndexPattern] = useState<IndexPattern | undefined>(undefined);
|
||||
|
||||
const { data: lastResultsData, isFetched } = usePackQueryLastResults({
|
||||
actionId,
|
||||
interval,
|
||||
|
@ -409,15 +407,6 @@ const ScheduledQueryLastResults: React.FC<ScheduledQueryLastResultsProps> = ({
|
|||
[queryId, interval, toggleErrors]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLogsIndexPattern = async () => {
|
||||
const indexPattern = await data.indexPatterns.find('logs-*');
|
||||
|
||||
setLogsIndexPattern(indexPattern[0]);
|
||||
};
|
||||
fetchLogsIndexPattern();
|
||||
}, [data.indexPatterns]);
|
||||
|
||||
if (!isFetched || !errorsFetched) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
|
@ -518,6 +507,86 @@ const ScheduledQueryLastResults: React.FC<ScheduledQueryLastResultsProps> = ({
|
|||
|
||||
const getPackActionId = (actionId: string, packName: string) => `pack_${packName}_${actionId}`;
|
||||
|
||||
interface PackViewInActionProps {
|
||||
item: {
|
||||
id: string;
|
||||
interval: number;
|
||||
};
|
||||
logsIndexPattern: IndexPattern | undefined;
|
||||
packName: string;
|
||||
agentIds?: string[];
|
||||
}
|
||||
|
||||
const PackViewInDiscoverActionComponent: React.FC<PackViewInActionProps> = ({
|
||||
item,
|
||||
logsIndexPattern,
|
||||
packName,
|
||||
agentIds,
|
||||
}) => {
|
||||
const { id, interval } = item;
|
||||
const actionId = getPackActionId(id, packName);
|
||||
const { data: lastResultsData } = usePackQueryLastResults({
|
||||
actionId,
|
||||
interval,
|
||||
logsIndexPattern,
|
||||
});
|
||||
|
||||
const startDate = lastResultsData?.['@timestamp']
|
||||
? moment(lastResultsData?.['@timestamp'][0]).subtract(interval, 'seconds').toISOString()
|
||||
: `now-${interval}s`;
|
||||
const endDate = lastResultsData?.['@timestamp']
|
||||
? moment(lastResultsData?.['@timestamp'][0]).toISOString()
|
||||
: 'now';
|
||||
|
||||
return (
|
||||
<ViewResultsInDiscoverAction
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
mode={lastResultsData?.['@timestamp'][0] ? 'absolute' : 'relative'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const PackViewInDiscoverAction = React.memo(PackViewInDiscoverActionComponent);
|
||||
|
||||
const PackViewInLensActionComponent: React.FC<PackViewInActionProps> = ({
|
||||
item,
|
||||
logsIndexPattern,
|
||||
packName,
|
||||
agentIds,
|
||||
}) => {
|
||||
const { id, interval } = item;
|
||||
const actionId = getPackActionId(id, packName);
|
||||
const { data: lastResultsData } = usePackQueryLastResults({
|
||||
actionId,
|
||||
interval,
|
||||
logsIndexPattern,
|
||||
});
|
||||
|
||||
const startDate = lastResultsData?.['@timestamp']
|
||||
? moment(lastResultsData?.['@timestamp'][0]).subtract(interval, 'seconds').toISOString()
|
||||
: `now-${interval}s`;
|
||||
const endDate = lastResultsData?.['@timestamp']
|
||||
? moment(lastResultsData?.['@timestamp'][0]).toISOString()
|
||||
: 'now';
|
||||
|
||||
return (
|
||||
<ViewResultsInLensAction
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
mode={lastResultsData?.['@timestamp'][0] ? 'absolute' : 'relative'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const PackViewInLensAction = React.memo(PackViewInLensActionComponent);
|
||||
|
||||
interface PackQueriesStatusTableProps {
|
||||
agentIds?: string[];
|
||||
data: OsqueryManagerPackagePolicyInputStream[];
|
||||
|
@ -533,6 +602,18 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
Record<string, ReturnType<typeof ScheduledQueryExpandedContent>>
|
||||
>({});
|
||||
|
||||
const indexPatterns = useKibana().services.data.indexPatterns;
|
||||
const [logsIndexPattern, setLogsIndexPattern] = useState<IndexPattern | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLogsIndexPattern = async () => {
|
||||
const indexPattern = await indexPatterns.find('logs-*');
|
||||
|
||||
setLogsIndexPattern(indexPattern[0]);
|
||||
};
|
||||
fetchLogsIndexPattern();
|
||||
}, [indexPatterns]);
|
||||
|
||||
const renderQueryColumn = useCallback(
|
||||
(query: string) => (
|
||||
<EuiCodeBlock language="sql" fontSize="s" paddingSize="none" transparentBackground>
|
||||
|
@ -564,6 +645,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
const renderLastResultsColumn = useCallback(
|
||||
(item) => (
|
||||
<ScheduledQueryLastResults
|
||||
logsIndexPattern={logsIndexPattern}
|
||||
queryId={item.id}
|
||||
actionId={getPackActionId(item.id, packName)}
|
||||
interval={item.interval}
|
||||
|
@ -571,35 +653,31 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
expanded={!!itemIdToExpandedRowMap[item.id]}
|
||||
/>
|
||||
),
|
||||
[itemIdToExpandedRowMap, packName, toggleErrors]
|
||||
[itemIdToExpandedRowMap, packName, toggleErrors, logsIndexPattern]
|
||||
);
|
||||
|
||||
const renderDiscoverResultsAction = useCallback(
|
||||
(item) => (
|
||||
<ViewResultsInDiscoverAction
|
||||
actionId={getPackActionId(item.id, packName)}
|
||||
<PackViewInDiscoverAction
|
||||
item={item}
|
||||
agentIds={agentIds}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
startDate={`now-${item.interval * 2}s`}
|
||||
endDate="now"
|
||||
mode="relative"
|
||||
logsIndexPattern={logsIndexPattern}
|
||||
packName={packName}
|
||||
/>
|
||||
),
|
||||
[agentIds, packName]
|
||||
[agentIds, logsIndexPattern, packName]
|
||||
);
|
||||
|
||||
const renderLensResultsAction = useCallback(
|
||||
(item) => (
|
||||
<ViewResultsInLensAction
|
||||
actionId={getPackActionId(item.id, packName)}
|
||||
<PackViewInLensAction
|
||||
item={item}
|
||||
agentIds={agentIds}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
startDate={`now-${item.interval * 2}s`}
|
||||
endDate="now"
|
||||
mode="relative"
|
||||
logsIndexPattern={logsIndexPattern}
|
||||
packName={packName}
|
||||
/>
|
||||
),
|
||||
[agentIds, packName]
|
||||
[agentIds, logsIndexPattern, packName]
|
||||
);
|
||||
|
||||
const getItemId = useCallback(
|
||||
|
|
|
@ -126,7 +126,7 @@ const PacksTableComponent = () => {
|
|||
{
|
||||
field: 'policy_ids',
|
||||
name: i18n.translate('xpack.osquery.packs.table.policyColumnTitle', {
|
||||
defaultMessage: 'Policies',
|
||||
defaultMessage: 'Scheduled policies',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: renderAgentPolicy,
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
EuiTitle,
|
||||
EuiText,
|
||||
EuiIcon,
|
||||
EuiSuperSelect,
|
||||
} from '@elastic/eui';
|
||||
import sqlParser from 'js-sql-parser';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -54,7 +55,9 @@ import {
|
|||
getUseField,
|
||||
fieldValidators,
|
||||
ValidationFuncArg,
|
||||
UseMultiFields,
|
||||
} from '../../shared_imports';
|
||||
import { OsqueryIcon } from '../../components/osquery_icon';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -77,6 +80,35 @@ const typeMap = {
|
|||
constant_keyword: 'string',
|
||||
};
|
||||
|
||||
const StyledEuiSuperSelect = styled(EuiSuperSelect)`
|
||||
&.euiFormControlLayout__prepend {
|
||||
padding-left: 8px;
|
||||
padding-right: 24px;
|
||||
box-shadow: none;
|
||||
|
||||
.euiIcon {
|
||||
padding: 0;
|
||||
width: 18px;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// @ts-expect-error update types
|
||||
const ResultComboBox = styled(EuiComboBox)`
|
||||
&.euiComboBox--prepended .euiSuperSelect {
|
||||
border-right: 1px solid ${(props) => props.theme.eui.euiBorderColor};
|
||||
|
||||
.euiFormControlLayout__childrenWrapper {
|
||||
border-radius: 6px 0 0 6px;
|
||||
|
||||
.euiFormControlLayoutIcons--right {
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFieldIcon = styled(FieldIcon)`
|
||||
width: 32px;
|
||||
|
||||
|
@ -90,6 +122,11 @@ const StyledFieldSpan = styled.span`
|
|||
padding-bottom: 0 !important;
|
||||
`;
|
||||
|
||||
// align the icon to the inputs
|
||||
const StyledSemicolonWrapper = styled.div`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
// align the icon to the inputs
|
||||
const StyledButtonWrapper = styled.div`
|
||||
margin-top: 11px;
|
||||
|
@ -115,11 +152,10 @@ interface ECSComboboxFieldProps {
|
|||
idAria?: string;
|
||||
}
|
||||
|
||||
export const ECSComboboxField: React.FC<ECSComboboxFieldProps> = ({
|
||||
const ECSComboboxFieldComponent: React.FC<ECSComboboxFieldProps> = ({
|
||||
field,
|
||||
euiFieldProps = {},
|
||||
idAria,
|
||||
...rest
|
||||
}) => {
|
||||
const { setValue } = field;
|
||||
const [selectedOptions, setSelected] = useState<Array<EuiComboBoxOptionOption<ECSSchemaOption>>>(
|
||||
|
@ -179,6 +215,21 @@ export const ECSComboboxField: React.FC<ECSComboboxFieldProps> = ({
|
|||
[selectedOptions]
|
||||
);
|
||||
|
||||
const helpText = useMemo(() => {
|
||||
// @ts-expect-error update types
|
||||
let text = selectedOptions[0]?.value?.description;
|
||||
|
||||
if (!text) return;
|
||||
|
||||
// @ts-expect-error update types
|
||||
const example = selectedOptions[0]?.value?.example;
|
||||
if (example) {
|
||||
text += ` e.g. ${JSON.stringify(example)}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}, [selectedOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error update types
|
||||
setSelected(() => {
|
||||
|
@ -193,14 +244,12 @@ export const ECSComboboxField: React.FC<ECSComboboxFieldProps> = ({
|
|||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
// @ts-expect-error update types
|
||||
helpText={selectedOptions[0]?.value?.description}
|
||||
helpText={helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
describedByIds={describedByIds}
|
||||
isDisabled={euiFieldProps.isDisabled}
|
||||
{...rest}
|
||||
>
|
||||
<EuiComboBox
|
||||
prepend={prepend}
|
||||
|
@ -219,20 +268,65 @@ export const ECSComboboxField: React.FC<ECSComboboxFieldProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const ECSComboboxField = React.memo(ECSComboboxFieldComponent);
|
||||
|
||||
const OSQUERY_COLUMN_VALUE_TYPE_OPTIONS = [
|
||||
{
|
||||
value: 'field',
|
||||
inputDisplay: <OsqueryIcon size="m" />,
|
||||
dropdownDisplay: (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<OsqueryIcon size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.form.ecsMappingSection.osqueryValueOptionLabel"
|
||||
defaultMessage="Osquery value"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'value',
|
||||
inputDisplay: <EuiIcon type="user" size="m" />,
|
||||
dropdownDisplay: (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="user" size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.form.ecsMappingSection.staticValueOptionLabel"
|
||||
defaultMessage="Static value"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
interface OsqueryColumnFieldProps {
|
||||
field: FieldHook<string>;
|
||||
resultType: FieldHook<string>;
|
||||
resultValue: FieldHook<string>;
|
||||
euiFieldProps: EuiComboBoxProps<OsquerySchemaOption>;
|
||||
idAria?: string;
|
||||
}
|
||||
|
||||
export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
||||
field,
|
||||
const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
||||
resultType,
|
||||
resultValue,
|
||||
euiFieldProps = {},
|
||||
idAria,
|
||||
...rest
|
||||
}) => {
|
||||
const { setValue } = field;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const { setValue } = resultValue;
|
||||
const { setValue: setType } = resultType;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(resultValue);
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
const [selectedOptions, setSelected] = useState<
|
||||
Array<EuiComboBoxOptionOption<OsquerySchemaOption>>
|
||||
|
@ -269,19 +363,51 @@ export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
|||
[setValue, setSelected]
|
||||
);
|
||||
|
||||
const onTypeChange = useCallback(
|
||||
(newType) => {
|
||||
if (newType !== resultType.value) {
|
||||
setType(newType);
|
||||
}
|
||||
},
|
||||
[setType, resultType.value]
|
||||
);
|
||||
|
||||
const handleCreateOption = useCallback(
|
||||
(newOption) => {
|
||||
setValue(newOption);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const Prepend = useMemo(
|
||||
() => (
|
||||
<StyledEuiSuperSelect
|
||||
options={OSQUERY_COLUMN_VALUE_TYPE_OPTIONS}
|
||||
valueOfSelected={resultType.value}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
popoverProps={{
|
||||
panelStyle: {
|
||||
minWidth: '250px',
|
||||
},
|
||||
}}
|
||||
onChange={onTypeChange}
|
||||
/>
|
||||
),
|
||||
[onTypeChange, resultType.value]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(() => {
|
||||
if (!field.value.length) return [];
|
||||
if (!resultValue.value.length) return [];
|
||||
|
||||
const selectedOption = find(euiFieldProps?.options, ['label', field.value]);
|
||||
const selectedOption = find(euiFieldProps?.options, ['label', resultValue.value]);
|
||||
|
||||
return selectedOption ? [selectedOption] : [{ label: field.value }];
|
||||
return selectedOption ? [selectedOption] : [{ label: resultValue.value }];
|
||||
});
|
||||
}, [euiFieldProps?.options, setSelected, field.value]);
|
||||
}, [euiFieldProps?.options, setSelected, resultValue.value]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
// @ts-expect-error update types
|
||||
helpText={selectedOptions[0]?.value?.description}
|
||||
error={errorMessage}
|
||||
|
@ -289,13 +415,14 @@ export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
|||
fullWidth
|
||||
describedByIds={describedByIds}
|
||||
isDisabled={euiFieldProps.isDisabled}
|
||||
{...rest}
|
||||
>
|
||||
<EuiComboBox
|
||||
<ResultComboBox
|
||||
fullWidth
|
||||
prepend={Prepend}
|
||||
singleSelection={singleSelection}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
onCreateOption={handleCreateOption}
|
||||
renderOption={renderOsqueryOption}
|
||||
rowHeight={32}
|
||||
isClearable
|
||||
|
@ -305,6 +432,18 @@ export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const OsqueryColumnField = React.memo(
|
||||
OsqueryColumnFieldComponent,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.resultType.value === nextProps.resultType.value &&
|
||||
prevProps.resultType.isChangingValue === nextProps.resultType.isChangingValue &&
|
||||
prevProps.resultType.errors === nextProps.resultType.errors &&
|
||||
prevProps.resultValue.value === nextProps.resultValue.value &&
|
||||
prevProps.resultValue.isChangingValue === nextProps.resultValue.isChangingValue &&
|
||||
prevProps.resultValue.errors === nextProps.resultValue.errors &&
|
||||
deepEqual(prevProps.euiFieldProps, nextProps.euiFieldProps)
|
||||
);
|
||||
|
||||
export interface ECSMappingEditorFieldRef {
|
||||
validate: () => Promise<
|
||||
| Record<
|
||||
|
@ -344,7 +483,7 @@ const getEcsFieldValidator =
|
|||
)(args);
|
||||
|
||||
// @ts-expect-error update types
|
||||
if (fieldRequiredError && ((!editForm && args.formData['value.field'].length) || editForm)) {
|
||||
if (fieldRequiredError && ((!editForm && args.formData['result.value'].length) || editForm)) {
|
||||
return fieldRequiredError;
|
||||
}
|
||||
|
||||
|
@ -354,7 +493,7 @@ const getEcsFieldValidator =
|
|||
const getOsqueryResultFieldValidator =
|
||||
(osquerySchemaOptions: OsquerySchemaOption[], editForm: boolean) =>
|
||||
(
|
||||
args: ValidationFuncArg<ECSMappingEditorFormData, ECSMappingEditorFormData['value']['field']>
|
||||
args: ValidationFuncArg<ECSMappingEditorFormData, ECSMappingEditorFormData['value']['value']>
|
||||
) => {
|
||||
const fieldRequiredError = fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage', {
|
||||
|
@ -366,7 +505,8 @@ const getOsqueryResultFieldValidator =
|
|||
return fieldRequiredError;
|
||||
}
|
||||
|
||||
if (!args.value.length) return;
|
||||
// @ts-expect-error update types
|
||||
if (!args.value?.length || args.formData['result.type'] !== 'field') return;
|
||||
|
||||
const osqueryColumnExists = find(osquerySchemaOptions, ['label', args.value]);
|
||||
|
||||
|
@ -383,6 +523,7 @@ const getOsqueryResultFieldValidator =
|
|||
},
|
||||
}
|
||||
),
|
||||
__isBlocking__: false,
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
@ -395,7 +536,8 @@ const FORM_DEFAULT_VALUE = {
|
|||
interface ECSMappingEditorFormData {
|
||||
key: string;
|
||||
value: {
|
||||
field: string;
|
||||
field?: string;
|
||||
value?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -413,14 +555,20 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
const formSchema = {
|
||||
key: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
fieldsToValidateOnChange: ['value.field'],
|
||||
fieldsToValidateOnChange: ['result.value'],
|
||||
validations: [
|
||||
{
|
||||
validator: getEcsFieldValidator(editForm),
|
||||
},
|
||||
],
|
||||
},
|
||||
'value.field': {
|
||||
result: {
|
||||
type: {
|
||||
defaultValue: OSQUERY_COLUMN_VALUE_TYPE_OPTIONS[0].value,
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
fieldsToValidateOnChange: ['result.value'],
|
||||
},
|
||||
value: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
fieldsToValidateOnChange: ['key'],
|
||||
validations: [
|
||||
|
@ -429,11 +577,22 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { form } = useForm({
|
||||
// @ts-expect-error update types
|
||||
schema: formSchema,
|
||||
defaultValue: defaultValue ?? FORM_DEFAULT_VALUE,
|
||||
deserializer: (data) => ({
|
||||
key: data.key ?? '',
|
||||
result: {
|
||||
type: data.value
|
||||
? Object.keys(data.value)[0]
|
||||
: OSQUERY_COLUMN_VALUE_TYPE_OPTIONS[0].value,
|
||||
value: data.value ? Object.values(data.value)[0] : '',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const { submit, reset, validate, __validateFields } = form;
|
||||
|
@ -442,17 +601,25 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
validate();
|
||||
__validateFields(['value.field']);
|
||||
__validateFields(['result.value']);
|
||||
const { data, isValid } = await submit();
|
||||
|
||||
if (isValid) {
|
||||
const serializedData = {
|
||||
key: data.key,
|
||||
value: {
|
||||
[data.result.type]: data.result.value,
|
||||
},
|
||||
};
|
||||
if (onAdd) {
|
||||
onAdd(data);
|
||||
onAdd(serializedData);
|
||||
}
|
||||
if (onChange) {
|
||||
onChange(serializedData);
|
||||
}
|
||||
reset();
|
||||
}
|
||||
return { data, isValid };
|
||||
}, [validate, __validateFields, submit, onAdd, reset]);
|
||||
}, [validate, __validateFields, submit, onAdd, onChange, reset]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
if (defaultValue?.key && onDelete) {
|
||||
|
@ -460,6 +627,37 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
}
|
||||
}, [defaultValue, onDelete]);
|
||||
|
||||
const MultiFields = useMemo(
|
||||
() => (
|
||||
<UseMultiFields
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
fields={{
|
||||
resultType: {
|
||||
path: 'result.type',
|
||||
},
|
||||
resultValue: {
|
||||
path: 'result.value',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(fields) => (
|
||||
<OsqueryColumnField
|
||||
{...fields}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
// @ts-expect-error update types
|
||||
options: osquerySchemaOptions,
|
||||
isDisabled,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UseMultiFields>
|
||||
),
|
||||
[osquerySchemaOptions, isDisabled]
|
||||
);
|
||||
|
||||
const ecsComboBoxEuiFieldProps = useMemo(() => ({ isDisabled }), [isDisabled]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
|
@ -468,35 +666,37 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
return { data: {}, isValid: true };
|
||||
}
|
||||
|
||||
__validateFields(['value.field']);
|
||||
__validateFields(['result.value']);
|
||||
const isValid = await validate();
|
||||
|
||||
return { data: formData?.key?.length ? { [formData.key]: formData.value } : {}, isValid };
|
||||
return {
|
||||
data: formData?.key?.length
|
||||
? {
|
||||
[formData.key]: {
|
||||
[formData.result.type]: formData.result.value,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
isValid,
|
||||
};
|
||||
},
|
||||
}),
|
||||
[__validateFields, editForm, formData, validate]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (onAdd && !deepEqual(formData, currentFormData.current)) {
|
||||
if (!deepEqual(formData, currentFormData.current)) {
|
||||
currentFormData.current = formData;
|
||||
handleSubmit();
|
||||
}
|
||||
}, [handleSubmit, formData, onAdd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange && !deepEqual(formData, currentFormData.current)) {
|
||||
currentFormData.current = formData;
|
||||
onChange(formData);
|
||||
}
|
||||
}, [defaultValue, formData, handleDeleteClick, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue) {
|
||||
validate();
|
||||
__validateFields(['value.field']);
|
||||
}
|
||||
}, [defaultValue, osquerySchemaOptions, validate, __validateFields]);
|
||||
// useEffect(() => {
|
||||
// if (defaultValue) {
|
||||
// validate();
|
||||
// __validateFields(['result.value']);
|
||||
// }
|
||||
// }, [defaultValue, osquerySchemaOptions, validate, __validateFields]);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
|
@ -507,30 +707,19 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
<CommonUseField
|
||||
path="key"
|
||||
component={ECSComboboxField}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{ isDisabled }}
|
||||
euiFieldProps={ecsComboBoxEuiFieldProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper>
|
||||
<EuiIcon type="arrowLeft" />
|
||||
</StyledButtonWrapper>
|
||||
<StyledSemicolonWrapper>
|
||||
<EuiText>:</EuiText>
|
||||
</StyledSemicolonWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="s" wrap>
|
||||
<ECSFieldWrapper>
|
||||
<CommonUseField
|
||||
path="value.field"
|
||||
component={OsqueryColumnField}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
options: osquerySchemaOptions,
|
||||
isDisabled,
|
||||
}}
|
||||
/>
|
||||
</ECSFieldWrapper>
|
||||
<ECSFieldWrapper>{MultiFields}</ECSFieldWrapper>
|
||||
{!isDisabled && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper>
|
||||
|
@ -578,12 +767,8 @@ interface OsqueryColumn {
|
|||
index: boolean;
|
||||
}
|
||||
|
||||
export const ECSMappingEditorField = ({
|
||||
field,
|
||||
query,
|
||||
fieldRef,
|
||||
euiFieldProps,
|
||||
}: ECSMappingEditorFieldProps) => {
|
||||
export const ECSMappingEditorField = React.memo(
|
||||
({ field, query, fieldRef, euiFieldProps }: ECSMappingEditorFieldProps) => {
|
||||
const { setValue, value = {} } = field;
|
||||
const [osquerySchemaOptions, setOsquerySchemaOptions] = useState<OsquerySchemaOption[]>([]);
|
||||
const formRefs = useRef<Record<string, ECSMappingEditorFormRef>>({});
|
||||
|
@ -869,7 +1054,7 @@ export const ECSMappingEditorField = ({
|
|||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.queryFlyoutForm.ecsFieldLabel"
|
||||
id="xpack.osquery.pack.queryFlyoutForm.mappingEcsFieldLabel"
|
||||
defaultMessage="ECS field"
|
||||
/>
|
||||
</EuiFormLabel>
|
||||
|
@ -877,8 +1062,8 @@ export const ECSMappingEditorField = ({
|
|||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldLabel"
|
||||
defaultMessage="Osquery result"
|
||||
id="xpack.osquery.pack.queryFlyoutForm.mappingValueFieldLabel"
|
||||
defaultMessage="Value"
|
||||
/>
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
|
@ -918,7 +1103,12 @@ export const ECSMappingEditorField = ({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.field.value === nextProps.field.value &&
|
||||
prevProps.query === nextProps.query &&
|
||||
deepEqual(prevProps.euiFieldProps, nextProps.euiFieldProps)
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ECSMappingEditorField;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
import moment from 'moment-timezone';
|
||||
import { IndexPattern } from '../../../../../src/plugins/data/common';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
|
@ -46,13 +47,12 @@ export const usePackQueryLastResults = ({
|
|||
});
|
||||
|
||||
const lastResultsResponse = await lastResultsSearchSource.fetch$().toPromise();
|
||||
const timestamp = lastResultsResponse.rawResponse?.hits?.hits[0]?.fields?.['@timestamp'][0];
|
||||
|
||||
const responseId = lastResultsResponse.rawResponse?.hits?.hits[0]?._source?.response_id;
|
||||
|
||||
if (responseId) {
|
||||
if (timestamp) {
|
||||
const aggsSearchSource = await data.search.searchSource.create({
|
||||
index: logsIndexPattern,
|
||||
size: 0,
|
||||
size: 1,
|
||||
aggs: {
|
||||
unique_agents: { cardinality: { field: 'agent.id' } },
|
||||
},
|
||||
|
@ -61,13 +61,16 @@ export const usePackQueryLastResults = ({
|
|||
bool: {
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
action_id: actionId,
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: moment(timestamp).subtract(interval, 'seconds').format(),
|
||||
lte: moment(timestamp).format(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
match_phrase: {
|
||||
response_id: responseId,
|
||||
action_id: actionId,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -81,7 +84,7 @@ export const usePackQueryLastResults = ({
|
|||
'@timestamp': lastResultsResponse.rawResponse?.hits?.hits[0]?.fields?.['@timestamp'],
|
||||
// @ts-expect-error update types
|
||||
uniqueAgentsCount: aggsResponse.rawResponse.aggregations?.unique_agents?.value,
|
||||
docCount: aggsResponse.rawResponse?.hits?.total,
|
||||
docCount: aggsResponse?.rawResponse?.hits?.total,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -291,19 +291,9 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
|
||||
const uniqueAgentsRepliedCount =
|
||||
// @ts-expect-error-type
|
||||
allResultsData?.rawResponse.aggregations?.unique_agents.value ?? 0;
|
||||
|
||||
return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed);
|
||||
return !!(aggregations.totalResponded !== agentIds?.length);
|
||||
}),
|
||||
[
|
||||
agentIds?.length,
|
||||
aggregations.failed,
|
||||
// @ts-expect-error-type
|
||||
allResultsData?.rawResponse.aggregations?.unique_agents.value,
|
||||
expired,
|
||||
]
|
||||
[agentIds?.length, aggregations.failed, aggregations.totalResponded, expired]
|
||||
);
|
||||
|
||||
if (!hasActionResultsPrivileges) {
|
||||
|
@ -328,7 +318,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
<>
|
||||
{isLive && <EuiProgress color="primary" size="xs" />}
|
||||
|
||||
{isFetched && !allResultsData?.edges.length ? (
|
||||
{isFetched && !allResultsData?.edges.length && !aggregations?.totalRowCount ? (
|
||||
<>
|
||||
<EuiCallOut title={generateEmptyDataMessage(aggregations.totalResponded)} />
|
||||
<EuiSpacer />
|
||||
|
|
|
@ -27,6 +27,16 @@ const PacksPageComponent = () => {
|
|||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.packList.pageSubtitle"
|
||||
defaultMessage="Create packs to organize sets of queries and to schedule queries for agent policies."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
|
|
|
@ -36,6 +36,7 @@ export const useSavedQueries = ({
|
|||
toastMessage: error.body.message,
|
||||
});
|
||||
},
|
||||
refetchOnWindowFocus: !!isLive,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -29,6 +29,7 @@ export const useSavedQuery = ({ savedQueryId }: UseSavedQueryProps) => {
|
|||
() => http.get(`/internal/osquery/saved_query/${savedQueryId}`),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
setErrorToast(data.error, {
|
||||
|
|
|
@ -11,7 +11,7 @@ import path from 'path';
|
|||
|
||||
import { run } from '@kbn/dev-utils';
|
||||
|
||||
const ECS_COLUMN_SCHEMA_FIELDS = ['field', 'type', 'description'];
|
||||
const ECS_COLUMN_SCHEMA_FIELDS = ['field', 'type', 'normalization', 'example', 'description'];
|
||||
|
||||
const RESTRICTED_FIELDS = [
|
||||
'agent.name',
|
||||
|
|
|
@ -53,6 +53,11 @@ export const savedQueryType: SavedObjectsType = {
|
|||
hidden: false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: savedQuerySavedObjectMappings,
|
||||
management: {
|
||||
defaultSearchField: 'id',
|
||||
importableAndExportable: true,
|
||||
getTitle: (savedObject) => savedObject.attributes.id,
|
||||
},
|
||||
};
|
||||
|
||||
export const packSavedObjectMappings: SavedObjectsType['mappings'] = {
|
||||
|
@ -109,4 +114,9 @@ export const packType: SavedObjectsType = {
|
|||
hidden: false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: packSavedObjectMappings,
|
||||
management: {
|
||||
defaultSearchField: 'name',
|
||||
importableAndExportable: true,
|
||||
getTitle: (savedObject) => savedObject.attributes.name,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ export const readSavedQueryRoute = (router: IRouter) => {
|
|||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
const savedQuery = await savedObjectsClient.get<{
|
||||
ecs_mapping: Array<{ field: string; value: string }>;
|
||||
ecs_mapping: Array<{ key: string; value: Record<string, object> }>;
|
||||
}>(savedQuerySavedObjectType, request.params.id);
|
||||
|
||||
if (savedQuery.attributes.ecs_mapping) {
|
||||
|
|
|
@ -34,7 +34,8 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
|
|||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
field: schema.string(),
|
||||
field: schema.maybe(schema.string()),
|
||||
value: schema.maybe(schema.string()),
|
||||
})
|
||||
)
|
||||
),
|
||||
|
|
|
@ -5,22 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { pick, reduce } from 'lodash';
|
||||
import { reduce } from 'lodash';
|
||||
|
||||
export const convertECSMappingToArray = (ecsMapping: Record<string, object> | undefined) =>
|
||||
ecsMapping
|
||||
? Object.entries(ecsMapping).map((item) => ({
|
||||
value: item[0],
|
||||
...item[1],
|
||||
key: item[0],
|
||||
value: item[1],
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
export const convertECSMappingToObject = (ecsMapping: Array<{ field: string; value: string }>) =>
|
||||
export const convertECSMappingToObject = (
|
||||
ecsMapping: Array<{ key: string; value: Record<string, object> }>
|
||||
) =>
|
||||
reduce(
|
||||
ecsMapping,
|
||||
(acc, value) => {
|
||||
acc[value.value] = pick(value, 'field');
|
||||
acc[value.key] = value.value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { field: string }>
|
||||
{} as Record<string, { field?: string; value?: string }>
|
||||
);
|
||||
|
|
|
@ -46,6 +46,11 @@ export const buildActionResultsQuery = ({
|
|||
},
|
||||
},
|
||||
aggs: {
|
||||
rows_count: {
|
||||
sum: {
|
||||
field: 'action_response.osquery.count',
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
terms: {
|
||||
script: {
|
||||
|
|
Loading…
Reference in a new issue