[Osquery] Fix 7.16.0 BC3 issues (#117105) (#117157)

Co-authored-by: Patryk Kopyciński <patryk.kopycinski@elastic.co>
This commit is contained in:
Kibana Machine 2021-11-02 16:49:43 -04:00 committed by GitHub
parent 8a55fa4fdc
commit e982d1023a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 741 additions and 486 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
),
[]

View file

@ -36,6 +36,7 @@ export const useSavedQueries = ({
toastMessage: error.body.message,
});
},
refetchOnWindowFocus: !!isLive,
}
);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,6 +46,11 @@ export const buildActionResultsQuery = ({
},
},
aggs: {
rows_count: {
sum: {
field: 'action_response.osquery.count',
},
},
responses: {
terms: {
script: {