[Osquery] Fix 7.16.0 BC4 issues (#117682) (#117969)

# Conflicts:
#	x-pack/plugins/osquery/public/live_queries/form/index.tsx
This commit is contained in:
Patryk Kopyciński 2021-11-09 08:41:35 +01:00 committed by GitHub
parent b5021e1d02
commit b82a7001e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 365 additions and 228 deletions

View file

@ -57,7 +57,7 @@ export const ecsMapping = t.record(
t.string,
t.partial({
field: t.string,
value: t.string,
value: t.union([t.string, t.array(t.string)]),
})
);
export type ECSMapping = t.TypeOf<typeof ecsMapping>;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { isArray, pickBy } from 'lodash';
import { isArray, isEmpty, pickBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EuiBasicTable, EuiButtonIcon, EuiCodeBlock, formatDate } from '@elastic/eui';
import React, { useState, useCallback, useMemo } from 'react';
@ -72,12 +72,15 @@ const ActionsTableComponent = () => {
const handlePlayClick = useCallback(
(item) =>
push('/live_queries/new', {
form: pickBy({
agentIds: item.fields.agents,
query: item._source.data.query,
ecs_mapping: item._source.data.ecs_mapping,
savedQueryId: item._source.data.saved_query_id,
}),
form: pickBy(
{
agentIds: item.fields.agents,
query: item._source.data.query,
ecs_mapping: item._source.data.ecs_mapping,
savedQueryId: item._source.data.saved_query_id,
},
(value) => !isEmpty(value)
),
}),
[push]
);

View file

@ -5,14 +5,25 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import { EuiIcon, EuiIconProps } from '@elastic/eui';
import OsqueryLogo from './osquery.svg';
export type OsqueryIconProps = Omit<EuiIconProps, 'type'>;
const OsqueryIconComponent: React.FC<OsqueryIconProps> = (props) => (
<EuiIcon size="xl" type={OsqueryLogo} {...props} />
);
const OsqueryIconComponent: React.FC<OsqueryIconProps> = (props) => {
const [Icon, setIcon] = useState<React.ReactElement | null>(null);
// FIXME: This is a hack to force the icon to be loaded asynchronously.
useEffect(() => {
const interval = setInterval(() => {
setIcon(<EuiIcon size="xl" type={OsqueryLogo} {...props} />);
}, 0);
return () => clearInterval(interval);
}, [props, setIcon]);
return Icon;
};
export const OsqueryIcon = React.memo(OsqueryIconComponent);

View file

@ -315,6 +315,16 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
streams: [],
policy_template: 'osquery_manager',
});
} else {
if (!draft.inputs[0].type) {
set(draft, 'inputs[0].type', 'osquery');
}
if (!draft.inputs[0].policy_template) {
set(draft, 'inputs[0].policy_template', 'osquery_manager');
}
if (!draft.inputs[0].enabled) {
set(draft, 'inputs[0].enabled', true);
}
}
});
onChange({

View file

@ -137,11 +137,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
type: FIELD_TYPES.JSON,
validations: [],
},
hidden: {
defaultValue: false,
type: FIELD_TYPES.TOGGLE,
validations: [],
},
};
const { form } = useForm({
@ -152,10 +147,15 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
if (isValid) {
try {
await mutateAsync({
...formData,
...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }),
});
await mutateAsync(
pickBy(
{
...formData,
...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }),
},
(value) => !isEmpty(value)
)
);
// eslint-disable-next-line no-empty
} catch (e) {}
}
@ -163,10 +163,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
options: {
stripEmptyFields: false,
},
serializer: ({ savedQueryId, hidden, ...formData }) => ({
...pickBy({ ...formData, saved_query_id: savedQueryId }),
...(hidden != null && hidden ? { hidden } : {}),
}),
serializer: ({ savedQueryId, ...formData }) =>
pickBy({ ...formData, saved_query_id: savedQueryId }, (value) => !isEmpty(value)),
defaultValue: deepMerge(
{
agentSelection: {
@ -177,7 +175,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
},
query: '',
savedQueryId: null,
hidden: false,
},
defaultValue ?? {}
),
@ -419,9 +416,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
if (defaultValue?.query) {
setFieldValue('query', defaultValue?.query);
}
if (defaultValue?.hidden) {
setFieldValue('hidden', defaultValue?.hidden);
}
// TODO: Set query and ECS mapping from savedQueryId object
if (defaultValue?.savedQueryId) {
setFieldValue('savedQueryId', defaultValue?.savedQueryId);
@ -436,7 +430,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
<Form form={form}>
{formType === 'steps' ? <EuiSteps steps={formSteps} /> : simpleForm}
<UseField path="savedQueryId" component={GhostFormField} />
<UseField path="hidden" component={GhostFormField} />
</Form>
{showSavedQueryFlyout ? (
<SavedQueryFlyout

View file

@ -105,7 +105,7 @@ const PackFormComponent: React.FC<PackFormProps> = ({ defaultValue, editMode = f
defaultValue: [],
type: FIELD_TYPES.COMBO_BOX,
label: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldLabel', {
defaultMessage: 'Agent policies (optional)',
defaultMessage: 'Scheduled agent policies (optional)',
}),
helpText: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldHelpText', {
defaultMessage: 'Queries in this pack are scheduled for agents in the selected policies.',

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { findIndex, forEach, pullAt, pullAllBy, pickBy } from 'lodash';
import { isEmpty, findIndex, forEach, pullAt, pullAllBy, pickBy } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui';
import { produce } from 'immer';
import React, { useCallback, useMemo, useState } from 'react';
@ -133,13 +133,16 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({ field, handleNameC
produce((draft) => {
forEach(parsedContent.queries, (newQuery, newQueryId) => {
draft.push(
pickBy({
id: newQueryId,
interval: newQuery.interval ?? parsedContent.interval,
query: newQuery.query,
version: newQuery.version ?? parsedContent.version,
platform: getSupportedPlatforms(newQuery.platform ?? parsedContent.platform),
})
pickBy(
{
id: newQueryId,
interval: newQuery.interval ?? parsedContent.interval,
query: newQuery.query,
version: newQuery.version ?? parsedContent.version,
platform: getSupportedPlatforms(newQuery.platform ?? parsedContent.platform),
},
(value) => !isEmpty(value)
)
);
});

View file

@ -29,7 +29,7 @@ import {
PersistedIndexPatternLayer,
PieVisualizationState,
} from '../../../lens/public';
import { FilterStateStore, IndexPattern } from '../../../../../src/plugins/data/common';
import { FilterStateStore, DataView } from '../../../../../src/plugins/data/common';
import { useKibana } from '../common/lib/kibana';
import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
import { ScheduledQueryErrorsTable } from './scheduled_query_errors_table';
@ -130,12 +130,12 @@ function getLensAttributes(
references: [
{
id: 'logs-*',
name: 'indexpattern-datasource-current-indexpattern',
name: 'dataView-datasource-current-dataView',
type: 'index-pattern',
},
{
id: 'logs-*',
name: 'indexpattern-datasource-layer-layer1',
name: 'dataView-datasource-layer-layer1',
type: 'index-pattern',
},
{
@ -377,7 +377,7 @@ interface ScheduledQueryLastResultsProps {
actionId: string;
queryId: string;
interval: number;
logsIndexPattern: IndexPattern | undefined;
logsDataView: DataView | undefined;
toggleErrors: (payload: { queryId: string; interval: number }) => void;
expanded: boolean;
}
@ -386,20 +386,20 @@ const ScheduledQueryLastResults: React.FC<ScheduledQueryLastResultsProps> = ({
actionId,
queryId,
interval,
logsIndexPattern,
logsDataView,
toggleErrors,
expanded,
}) => {
const { data: lastResultsData, isFetched } = usePackQueryLastResults({
actionId,
interval,
logsIndexPattern,
logsDataView,
});
const { data: errorsData, isFetched: errorsFetched } = usePackQueryErrors({
actionId,
interval,
logsIndexPattern,
logsDataView,
});
const handleErrorsToggle = useCallback(
@ -512,14 +512,14 @@ interface PackViewInActionProps {
id: string;
interval: number;
};
logsIndexPattern: IndexPattern | undefined;
logsDataView: DataView | undefined;
packName: string;
agentIds?: string[];
}
const PackViewInDiscoverActionComponent: React.FC<PackViewInActionProps> = ({
item,
logsIndexPattern,
logsDataView,
packName,
agentIds,
}) => {
@ -528,7 +528,7 @@ const PackViewInDiscoverActionComponent: React.FC<PackViewInActionProps> = ({
const { data: lastResultsData } = usePackQueryLastResults({
actionId,
interval,
logsIndexPattern,
logsDataView,
});
const startDate = lastResultsData?.['@timestamp']
@ -554,7 +554,7 @@ const PackViewInDiscoverAction = React.memo(PackViewInDiscoverActionComponent);
const PackViewInLensActionComponent: React.FC<PackViewInActionProps> = ({
item,
logsIndexPattern,
logsDataView,
packName,
agentIds,
}) => {
@ -563,7 +563,7 @@ const PackViewInLensActionComponent: React.FC<PackViewInActionProps> = ({
const { data: lastResultsData } = usePackQueryLastResults({
actionId,
interval,
logsIndexPattern,
logsDataView,
});
const startDate = lastResultsData?.['@timestamp']
@ -602,17 +602,17 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
Record<string, ReturnType<typeof ScheduledQueryExpandedContent>>
>({});
const indexPatterns = useKibana().services.data.indexPatterns;
const [logsIndexPattern, setLogsIndexPattern] = useState<IndexPattern | undefined>(undefined);
const dataViews = useKibana().services.data.dataViews;
const [logsDataView, setLogsDataView] = useState<DataView | undefined>(undefined);
useEffect(() => {
const fetchLogsIndexPattern = async () => {
const indexPattern = await indexPatterns.find('logs-*');
const fetchLogsDataView = async () => {
const dataView = await dataViews.find('logs-*');
setLogsIndexPattern(indexPattern[0]);
setLogsDataView(dataView[0]);
};
fetchLogsIndexPattern();
}, [indexPatterns]);
fetchLogsDataView();
}, [dataViews]);
const renderQueryColumn = useCallback(
(query: string) => (
@ -645,7 +645,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
const renderLastResultsColumn = useCallback(
(item) => (
<ScheduledQueryLastResults
logsIndexPattern={logsIndexPattern}
logsDataView={logsDataView}
queryId={item.id}
actionId={getPackActionId(item.id, packName)}
interval={item.interval}
@ -653,7 +653,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
expanded={!!itemIdToExpandedRowMap[item.id]}
/>
),
[itemIdToExpandedRowMap, packName, toggleErrors, logsIndexPattern]
[itemIdToExpandedRowMap, packName, toggleErrors, logsDataView]
);
const renderDiscoverResultsAction = useCallback(
@ -661,11 +661,11 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
<PackViewInDiscoverAction
item={item}
agentIds={agentIds}
logsIndexPattern={logsIndexPattern}
logsDataView={logsDataView}
packName={packName}
/>
),
[agentIds, logsIndexPattern, packName]
[agentIds, logsDataView, packName]
);
const renderLensResultsAction = useCallback(
@ -673,11 +673,11 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
<PackViewInLensAction
item={item}
agentIds={agentIds}
logsIndexPattern={logsIndexPattern}
logsDataView={logsDataView}
packName={packName}
/>
),
[agentIds, logsIndexPattern, packName]
[agentIds, logsDataView, packName]
);
const getItemId = useCallback(

View file

@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n';
import { PlatformIcons } from './queries/platforms';
import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
interface PackQueriesTableProps {
export interface PackQueriesTableProps {
data: OsqueryManagerPackagePolicyInputStream[];
onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
@ -184,3 +184,5 @@ const PackQueriesTableComponent: React.FC<PackQueriesTableProps> = ({
};
export const PackQueriesTable = React.memo(PackQueriesTableComponent);
// eslint-disable-next-line import/no-default-export
export default PackQueriesTable;

View file

@ -52,7 +52,7 @@ export const AgentPoliciesPopover = ({ agentPolicyIds }: { agentPolicyIds: strin
const button = useMemo(
() => (
<EuiButtonEmpty flush="both" onClick={onButtonClick}>
<EuiButtonEmpty size="s" flush="both" onClick={onButtonClick}>
<>{agentPolicyIds?.length ?? 0}</>
</EuiButtonEmpty>
),

View file

@ -6,7 +6,18 @@
*/
import { produce } from 'immer';
import { each, isEmpty, find, orderBy, sortedUniqBy, isArray, map, reduce, get } from 'lodash';
import {
castArray,
each,
isEmpty,
find,
orderBy,
sortedUniqBy,
isArray,
map,
reduce,
get,
} from 'lodash';
import React, {
forwardRef,
useCallback,
@ -81,30 +92,24 @@ const typeMap = {
};
const StyledEuiSuperSelect = styled(EuiSuperSelect)`
&.euiFormControlLayout__prepend {
padding-left: 8px;
padding-right: 24px;
box-shadow: none;
min-width: 70px;
border-radius: 6px 0 0 6px;
.euiIcon {
padding: 0;
width: 18px;
background: 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};
&.euiComboBox {
position: relative;
left: -1px;
.euiFormControlLayout__childrenWrapper {
border-radius: 6px 0 0 6px;
.euiFormControlLayoutIcons--right {
right: 6px;
}
.euiComboBox__inputWrap {
border-radius: 0 6px 6px 0;
}
}
`;
@ -311,9 +316,11 @@ const OSQUERY_COLUMN_VALUE_TYPE_OPTIONS = [
},
];
const EMPTY_ARRAY: EuiComboBoxOptionOption[] = [];
interface OsqueryColumnFieldProps {
resultType: FieldHook<string>;
resultValue: FieldHook<string>;
resultValue: FieldHook<string | string[]>;
euiFieldProps: EuiComboBoxProps<OsquerySchemaOption>;
idAria?: string;
}
@ -324,6 +331,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
euiFieldProps = {},
idAria,
}) => {
const inputRef = useRef<HTMLInputElement>();
const { setValue } = resultValue;
const { setValue: setType } = resultType;
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(resultValue);
@ -367,16 +375,27 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
(newType) => {
if (newType !== resultType.value) {
setType(newType);
setValue(newType === 'value' && euiFieldProps.singleSelection === false ? [] : '');
}
},
[setType, resultType.value]
[resultType.value, setType, setValue, euiFieldProps.singleSelection]
);
const handleCreateOption = useCallback(
(newOption) => {
setValue(newOption);
(newOption: string) => {
if (euiFieldProps.singleSelection === false) {
setValue([newOption]);
if (resultValue.value.length) {
setValue([...castArray(resultValue.value), newOption]);
} else {
setValue([newOption]);
}
inputRef.current?.blur();
} else {
setValue(newOption);
}
},
[setValue]
[euiFieldProps.singleSelection, resultValue.value, setValue]
);
const Prepend = useMemo(
@ -400,6 +419,11 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
setSelected(() => {
if (!resultValue.value.length) return [];
// Static array values
if (isArray(resultValue.value)) {
return resultValue.value.map((value) => ({ label: value }));
}
const selectedOption = find(euiFieldProps?.options, ['label', resultValue.value]);
return selectedOption ? [selectedOption] : [{ label: resultValue.value }];
@ -416,18 +440,26 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
describedByIds={describedByIds}
isDisabled={euiFieldProps.isDisabled}
>
<ResultComboBox
fullWidth
prepend={Prepend}
singleSelection={singleSelection}
selectedOptions={selectedOptions}
onChange={handleChange}
onCreateOption={handleCreateOption}
renderOption={renderOsqueryOption}
rowHeight={32}
isClearable
{...euiFieldProps}
/>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false}>{Prepend}</EuiFlexItem>
<EuiFlexItem>
<ResultComboBox
// eslint-disable-next-line react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop
inputRef={(ref: HTMLInputElement) => {
inputRef.current = ref;
}}
fullWidth
selectedOptions={selectedOptions}
onChange={handleChange}
onCreateOption={handleCreateOption}
renderOption={renderOsqueryOption}
rowHeight={32}
isClearable
{...euiFieldProps}
options={(resultType.value === 'field' && euiFieldProps.options) || EMPTY_ARRAY}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};
@ -497,7 +529,7 @@ const getOsqueryResultFieldValidator =
) => {
const fieldRequiredError = fieldValidators.emptyField(
i18n.translate('xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage', {
defaultMessage: 'Osquery result is required.',
defaultMessage: 'Value is required.',
})
)(args);
@ -551,6 +583,7 @@ interface ECSMappingEditorFormRef {
export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappingEditorFormProps>(
({ isDisabled, osquerySchemaOptions, defaultValue, onAdd, onChange, onDelete }, ref) => {
const editForm = !!defaultValue;
const multipleValuesField = useRef(false);
const currentFormData = useRef(defaultValue);
const formSchema = {
key: {
@ -648,6 +681,8 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
// @ts-expect-error update types
options: osquerySchemaOptions,
isDisabled,
// @ts-expect-error update types
singleSelection: !multipleValuesField.current ? { asPlainText: true } : false,
}}
/>
)}
@ -687,17 +722,13 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
useEffect(() => {
if (!deepEqual(formData, currentFormData.current)) {
currentFormData.current = formData;
const ecsOption = find(ECSSchemaOptions, ['label', formData.key]);
multipleValuesField.current =
ecsOption?.value?.normalization === 'array' && formData.result.type === 'value';
handleSubmit();
}
}, [handleSubmit, formData, onAdd]);
// useEffect(() => {
// if (defaultValue) {
// validate();
// __validateFields(['result.value']);
// }
// }, [defaultValue, osquerySchemaOptions, validate, __validateFields]);
return (
<Form form={form}>
<EuiFlexGroup alignItems="flexStart" gutterSize="s">

View file

@ -6,16 +6,27 @@
*/
import { EuiIcon } from '@elastic/eui';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { getPlatformIconModule } from './helpers';
interface PlatformIconProps {
export interface PlatformIconProps {
platform: string;
}
const PlatformIconComponent: React.FC<PlatformIconProps> = ({ platform }) => {
const platformIconModule = getPlatformIconModule(platform);
return <EuiIcon type={platformIconModule} title={platform} size="l" />;
const [Icon, setIcon] = useState<React.ReactElement | null>(null);
// FIXME: This is a hack to force the icon to be loaded asynchronously.
useEffect(() => {
const interval = setInterval(() => {
const platformIconModule = getPlatformIconModule(platform);
setIcon(<EuiIcon type={platformIconModule} title={platform} size="l" />);
}, 0);
return () => clearInterval(interval);
}, [platform, setIcon]);
return Icon;
};
export const PlatformIcon = React.memo(PlatformIconComponent);

View file

@ -6,21 +6,21 @@
*/
import { useQuery } from 'react-query';
import { IndexPattern, SortDirection } from '../../../../../src/plugins/data/common';
import { DataView, SortDirection } from '../../../../../src/plugins/data/common';
import { useKibana } from '../common/lib/kibana';
interface UsePackQueryErrorsProps {
actionId: string;
interval: number;
logsIndexPattern?: IndexPattern;
logsDataView?: DataView;
skip?: boolean;
}
export const usePackQueryErrors = ({
actionId,
interval,
logsIndexPattern,
logsDataView,
skip = false,
}: UsePackQueryErrorsProps) => {
const data = useKibana().services.data;
@ -29,7 +29,7 @@ export const usePackQueryErrors = ({
['scheduledQueryErrors', { actionId, interval }],
async () => {
const searchSource = await data.search.searchSource.create({
index: logsIndexPattern,
index: logsDataView,
fields: ['*'],
sort: [
{
@ -73,7 +73,7 @@ export const usePackQueryErrors = ({
},
{
keepPreviousData: true,
enabled: !!(!skip && actionId && interval && logsIndexPattern),
enabled: !!(!skip && actionId && interval && logsDataView),
select: (response) => response.rawResponse.hits ?? [],
refetchOnReconnect: false,
refetchOnWindowFocus: false,

View file

@ -7,21 +7,21 @@
import { useQuery } from 'react-query';
import moment from 'moment-timezone';
import { IndexPattern } from '../../../../../src/plugins/data/common';
import { DataView, SortDirection } from '../../../../../src/plugins/data/common';
import { useKibana } from '../common/lib/kibana';
interface UsePackQueryLastResultsProps {
actionId: string;
agentIds?: string[];
interval: number;
logsIndexPattern?: IndexPattern;
logsDataView?: DataView;
skip?: boolean;
}
export const usePackQueryLastResults = ({
actionId,
interval,
logsIndexPattern,
logsDataView,
skip = false,
}: UsePackQueryLastResultsProps) => {
const data = useKibana().services.data;
@ -30,8 +30,9 @@ export const usePackQueryLastResults = ({
['scheduledQueryLastResults', { actionId }],
async () => {
const lastResultsSearchSource = await data.search.searchSource.create({
index: logsIndexPattern,
index: logsDataView,
size: 1,
sort: { '@timestamp': SortDirection.desc },
query: {
// @ts-expect-error update types
bool: {
@ -51,7 +52,7 @@ export const usePackQueryLastResults = ({
if (timestamp) {
const aggsSearchSource = await data.search.searchSource.create({
index: logsIndexPattern,
index: logsDataView,
size: 1,
aggs: {
unique_agents: { cardinality: { field: 'agent.id' } },
@ -92,7 +93,7 @@ export const usePackQueryLastResults = ({
},
{
keepPreviousData: true,
enabled: !!(!skip && actionId && interval && logsIndexPattern),
enabled: !!(!skip && actionId && interval && logsDataView),
refetchOnReconnect: false,
refetchOnWindowFocus: false,
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { get, isEmpty, isEqual, keys, map, reduce } from 'lodash/fp';
import { get, isEmpty, isArray, isObject, isEqual, keys, map, reduce } from 'lodash/fp';
import {
EuiCallOut,
EuiCode,
@ -123,6 +123,11 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
[visibleColumns, setVisibleColumns]
);
const ecsMappingColumns = useMemo(
() => keys(get('actionDetails._source.data.ecs_mapping', actionDetails) || {}),
[actionDetails]
);
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(
() =>
// eslint-disable-next-line react/display-name
@ -140,9 +145,22 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
return <EuiLink href={getFleetAppUrl(agentIdValue)}>{value}</EuiLink>;
}
if (ecsMappingColumns.includes(columnId)) {
const ecsFieldValue = get(columnId, data[rowIndex % pagination.pageSize]?._source);
if (isArray(ecsFieldValue) || isObject(ecsFieldValue)) {
try {
return JSON.stringify(ecsFieldValue, null, 2);
// eslint-disable-next-line no-empty
} catch (e) {}
}
return ecsFieldValue ?? '-';
}
return !isEmpty(value) ? value : '-';
},
[getFleetAppUrl, pagination.pageSize]
[ecsMappingColumns, getFleetAppUrl, pagination.pageSize]
);
const tableSorting = useMemo(
@ -218,12 +236,17 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
return;
}
const newColumns = keys(allResultsData?.edges[0]?.fields)
.sort()
.reduce(
(acc, fieldName) => {
const { data, seen } = acc;
if (fieldName === 'agent.name') {
const fields = [
'agent.name',
...ecsMappingColumns.sort(),
...keys(allResultsData?.edges[0]?.fields || {}).sort(),
];
const newColumns = fields.reduce(
(acc, fieldName) => {
const { data, seen } = acc;
if (fieldName === 'agent.name') {
if (!seen.has(fieldName)) {
data.push({
id: fieldName,
displayAsText: i18n.translate(
@ -234,34 +257,48 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
),
defaultSortDirection: Direction.asc,
});
return acc;
}
if (fieldName.startsWith('osquery.')) {
const displayAsText = fieldName.split('.')[1];
if (!seen.has(displayAsText)) {
data.push({
id: fieldName,
displayAsText,
display: getHeaderDisplay(displayAsText),
defaultSortDirection: Direction.asc,
});
seen.add(displayAsText);
}
return acc;
seen.add(fieldName);
}
return acc;
},
{ data: [], seen: new Set<string>() } as { data: EuiDataGridColumn[]; seen: Set<string> }
).data;
}
if (ecsMappingColumns.includes(fieldName)) {
if (!seen.has(fieldName)) {
data.push({
id: fieldName,
displayAsText: fieldName,
defaultSortDirection: Direction.asc,
});
seen.add(fieldName);
}
return acc;
}
if (fieldName.startsWith('osquery.')) {
const displayAsText = fieldName.split('.')[1];
if (!seen.has(displayAsText)) {
data.push({
id: fieldName,
displayAsText,
display: getHeaderDisplay(displayAsText),
defaultSortDirection: Direction.asc,
});
seen.add(displayAsText);
}
return acc;
}
return acc;
},
{ data: [], seen: new Set<string>() } as { data: EuiDataGridColumn[]; seen: Set<string> }
).data;
setColumns((currentColumns) =>
!isEqual(map('id', currentColumns), map('id', newColumns)) ? newColumns : currentColumns
);
setVisibleColumns(map('id', newColumns));
}, [allResultsData?.edges, getHeaderDisplay]);
}, [allResultsData?.edges, ecsMappingColumns, getHeaderDisplay]);
const toolbarVisibility = useMemo(
() => ({

View file

@ -127,7 +127,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ metadata }) => {
);
}
return <LiveQuery agentId={agentId} />;
return <LiveQuery formType="simple" agentId={agentId} />;
};
export const OsqueryAction = React.memo(OsqueryActionComponent);

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { produce } from 'immer';
import { SavedObjectsType } from '../../../../../../src/core/server';
import { savedQuerySavedObjectType, packSavedObjectType } from '../../../common/types';
export const savedQuerySavedObjectMappings: SavedObjectsType['mappings'] = {
@ -54,9 +54,13 @@ export const savedQueryType: SavedObjectsType = {
namespaceType: 'multiple-isolated',
mappings: savedQuerySavedObjectMappings,
management: {
defaultSearchField: 'id',
importableAndExportable: true,
getTitle: (savedObject) => savedObject.attributes.id,
getEditUrl: (savedObject) => `/saved_queries/${savedObject.id}/edit`,
getInAppUrl: (savedObject) => ({
path: `/app/saved_queries/${savedObject.id}`,
uiCapabilitiesPath: 'osquery.read',
}),
},
};
@ -117,6 +121,19 @@ export const packType: SavedObjectsType = {
management: {
defaultSearchField: 'name',
importableAndExportable: true,
getTitle: (savedObject) => savedObject.attributes.name,
getTitle: (savedObject) => `Pack: ${savedObject.attributes.name}`,
getEditUrl: (savedObject) => `/packs/${savedObject.id}/edit`,
getInAppUrl: (savedObject) => ({
path: `/app/packs/${savedObject.id}`,
uiCapabilitiesPath: 'osquery.read',
}),
onExport: (context, objects) =>
produce(objects, (draft) => {
draft.forEach((packSO) => {
packSO.references = [];
});
return draft;
}),
},
};

View file

@ -227,7 +227,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
security: plugins.security,
};
initSavedObjects(core.savedObjects, osqueryContext);
initSavedObjects(core.savedObjects);
initUsageCollectors({
core,
osqueryContext,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { pickBy } from 'lodash';
import { pickBy, isEmpty } from 'lodash';
import uuid from 'uuid';
import moment from 'moment-timezone';
@ -69,12 +69,15 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
input_type: 'osquery',
agents: selectedAgents,
user_id: currentUser,
data: pickBy({
id: uuid.v4(),
query: request.body.query,
saved_query_id: request.body.saved_query_id,
ecs_mapping: request.body.ecs_mapping,
}),
data: pickBy(
{
id: uuid.v4(),
query: request.body.query,
saved_query_id: request.body.saved_query_id,
ecs_mapping: request.body.ecs_mapping,
},
(value) => !isEmpty(value)
),
};
const actionResponse = await esClient.index<{}, {}>({
index: '.fleet-actions',

View file

@ -43,7 +43,10 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
schema.recordOf(
schema.string(),
schema.object({
field: schema.string(),
field: schema.maybe(schema.string()),
value: schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
),
})
)
),
@ -68,8 +71,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
const conflictingEntries = await savedObjectsClient.find({
type: packSavedObjectType,
search: name,
searchFields: ['name'],
filter: `${packSavedObjectType}.attributes.name: "${name}"`,
});
if (conflictingEntries.saved_objects.length) {

View file

@ -6,7 +6,19 @@
*/
import moment from 'moment-timezone';
import { set, unset, has, difference, filter, find, map, mapKeys, pickBy, uniq } from 'lodash';
import {
isEmpty,
set,
unset,
has,
difference,
filter,
find,
map,
mapKeys,
pickBy,
uniq,
} from 'lodash';
import { schema } from '@kbn/config-schema';
import { produce } from 'immer';
import {
@ -51,7 +63,10 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
schema.recordOf(
schema.string(),
schema.object({
field: schema.string(),
field: schema.maybe(schema.string()),
value: schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
),
})
)
),
@ -82,8 +97,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
if (name) {
const conflictingEntries = await savedObjectsClient.find({
type: packSavedObjectType,
search: name,
searchFields: ['name'],
filter: `${packSavedObjectType}.attributes.name: "${name}"`,
});
if (
@ -112,13 +126,16 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
request.params.id,
{
enabled,
...pickBy({
name,
description,
queries: queries && convertPackQueriesToSO(queries),
updated_at: moment().toISOString(),
updated_by: currentUser,
}),
...pickBy(
{
name,
description,
queries: queries && convertPackQueriesToSO(queries),
updated_at: moment().toISOString(),
updated_by: currentUser,
},
(value) => !isEmpty(value)
),
},
policy_ids
? {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { pickBy } from 'lodash';
import { isEmpty, pickBy } from 'lodash';
import { IRouter } from '../../../../../../src/core/server';
import { PLUGIN_ID } from '../../../common';
import {
@ -39,8 +39,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
const conflictingEntries = await savedObjectsClient.find({
type: savedQuerySavedObjectType,
search: id,
searchFields: ['id'],
filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`,
});
if (conflictingEntries.saved_objects.length) {
@ -49,26 +48,32 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
const savedQuerySO = await savedObjectsClient.create(
savedQuerySavedObjectType,
pickBy({
id,
description,
query,
platform,
version,
interval,
ecs_mapping: convertECSMappingToArray(ecs_mapping),
created_by: currentUser,
created_at: new Date().toISOString(),
updated_by: currentUser,
updated_at: new Date().toISOString(),
})
pickBy(
{
id,
description,
query,
platform,
version,
interval,
ecs_mapping: convertECSMappingToArray(ecs_mapping),
created_by: currentUser,
created_at: new Date().toISOString(),
updated_by: currentUser,
updated_at: new Date().toISOString(),
},
(value) => !isEmpty(value)
)
);
return response.ok({
body: pickBy({
...savedQuerySO,
ecs_mapping,
}),
body: pickBy(
{
...savedQuerySO,
ecs_mapping,
},
(value) => !isEmpty(value)
),
});
}
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { filter, pickBy } from 'lodash';
import { isEmpty, filter, pickBy } from 'lodash';
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
@ -35,7 +35,9 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
schema.string(),
schema.object({
field: schema.maybe(schema.string()),
value: schema.maybe(schema.string()),
value: schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
),
})
)
),
@ -62,8 +64,7 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
const conflictingEntries = await savedObjectsClient.find<{ id: string }>({
type: savedQuerySavedObjectType,
search: id,
searchFields: ['id'],
filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`,
});
if (
@ -76,17 +77,20 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
const updatedSavedQuerySO = await savedObjectsClient.update(
savedQuerySavedObjectType,
request.params.id,
pickBy({
id,
description,
platform,
query,
version,
interval,
ecs_mapping: convertECSMappingToArray(ecs_mapping),
updated_by: currentUser,
updated_at: new Date().toISOString(),
}),
pickBy(
{
id,
description,
platform,
query,
version,
interval,
ecs_mapping: convertECSMappingToArray(ecs_mapping),
updated_by: currentUser,
updated_at: new Date().toISOString(),
},
(value) => !isEmpty(value)
),
{
refresh: 'wait_for',
}

View file

@ -7,24 +7,11 @@
import { CoreSetup } from '../../../../src/core/server';
import { OsqueryAppContext } from './lib/osquery_app_context_services';
import { savedQueryType, packType } from './lib/saved_query/saved_object_mappings';
import { usageMetricType } from './routes/usage/saved_object_mappings';
const types = [savedQueryType, packType];
export const savedObjectTypes = types.map((type) => type.name);
export const initSavedObjects = (
savedObjects: CoreSetup['savedObjects'],
osqueryContext: OsqueryAppContext
) => {
const config = osqueryContext.config();
export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => {
savedObjects.registerType(usageMetricType);
savedObjects.registerType(savedQueryType);
if (config.packs) {
savedObjects.registerType(packType);
}
savedObjects.registerType(packType);
};