[8.5] [Osquery] Fix small issues (#141083) (#142157)

* [Osquery] Fix small issues (#141083)

(cherry picked from commit ccbac37155)

* Update artifact_manager.ts

change back agent version to 8.5.0

Co-authored-by: Tomasz Ciecierski <tomasz.ciecierski@elastic.co>
This commit is contained in:
Kibana Machine 2022-09-29 03:07:08 -06:00 committed by GitHub
parent 7774b55c40
commit 95c797c26d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 842 additions and 676 deletions

View file

@ -8,7 +8,6 @@
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
import { login } from '../../tasks/login';
import {
checkResults,
findAndClickButton,
findFormFieldByRowsLabelAndType,
inputQuery,
@ -59,6 +58,28 @@ describe('Alert Event Details', () => {
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
});
it('enables to add detection action with osquery', () => {
cy.visit('/app/security/rules');
cy.contains(RULE_NAME).click();
cy.contains('Edit rule settings').click();
cy.getBySel('edit-rule-actions-tab').wait(500).click();
cy.contains('Perform no actions').get('select').select('On each rule execution');
cy.contains('Response actions are run on each rule execution');
cy.getBySel('.osquery-ResponseActionTypeSelectOption').click();
cy.get(LIVE_QUERY_EDITOR);
cy.contains('Save changes').click();
cy.contains('Query is a required field');
inputQuery('select * from uptime');
cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;)
// getSavedQueriesDropdown().type(`users{downArrow}{enter}`);
cy.contains('Save changes').click();
cy.contains(`${RULE_NAME} was saved`).should('exist');
cy.contains('Edit rule settings').click();
cy.getBySel('edit-rule-actions-tab').wait(500).click();
cy.contains('select * from uptime');
});
it('should be able to run live query and add to timeline (-depending on the previous test)', () => {
const TIMELINE_NAME = 'Untitled timeline';
cy.visit('/app/security/alerts');
@ -94,38 +115,20 @@ describe('Alert Event Details', () => {
cy.contains('Cancel').click();
cy.contains(TIMELINE_NAME).click();
cy.getBySel('draggableWrapperKeyboardHandler').contains('action_id: "');
});
it('enables to add detection action with osquery', () => {
cy.visit('/app/security/rules');
cy.contains(RULE_NAME).click();
cy.contains('Edit rule settings').click();
cy.getBySel('edit-rule-actions-tab').wait(500).click();
cy.contains('Perform no actions').get('select').select('On each rule execution');
cy.contains('Response actions are run on each rule execution');
cy.getBySel('.osquery-ResponseActionTypeSelectOption').click();
cy.get(LIVE_QUERY_EDITOR);
cy.contains('Save changes').click();
cy.contains('Query is a required field');
inputQuery('select * from uptime');
cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;)
// getSavedQueriesDropdown().type(`users{downArrow}{enter}`);
cy.contains('Save changes').click();
cy.contains(`${RULE_NAME} was saved`).should('exist');
cy.contains('Edit rule settings').click();
cy.getBySel('edit-rule-actions-tab').wait(500).click();
cy.contains('select * from uptime');
// timeline unsaved changes modal
cy.visit('/app/osquery');
closeModalIfVisible();
});
// TODO think on how to get these actions triggered faster (because now they are not triggered during the test).
it.skip('sees osquery results from last action', () => {
cy.visit('/app/security/alerts');
cy.getBySel('header-page-title').contains('Alerts').should('exist');
cy.getBySel('expand-event').first().click({ force: true });
cy.contains('Osquery Results').click();
cy.getBySel('osquery-results').should('exist');
cy.contains('select * from uptime');
cy.getBySel('osqueryResultsTable').within(() => {
checkResults();
});
});
// it.skip('sees osquery results from last action', () => {
// cy.visit('/app/security/alerts');
// cy.getBySel('header-page-title').contains('Alerts').should('exist');
// cy.getBySel('expand-event').first().click({ force: true });
// cy.contains('Osquery Results').click();
// cy.getBySel('osquery-results').should('exist');
// cy.contains('select * from uptime');
// cy.getBySel('osqueryResultsTable').within(() => {
// checkResults();
// });
// });
});

View file

@ -35,7 +35,7 @@ describe('Add to Cases', () => {
cy.contains('Test Obs case').click();
checkResults();
cy.contains('attached Osquery results');
cy.contains('SELECT * FROM users;');
cy.contains('select * from uptime;');
cy.contains('View in Discover').should('exist');
cy.contains('View in Lens').should('exist');
cy.contains('Add to Case').should('not.exist');
@ -66,7 +66,7 @@ describe('Add to Cases', () => {
cy.contains('Test Security Case').click();
checkResults();
cy.contains('attached Osquery results');
cy.contains('SELECT * FROM users;');
cy.contains('select * from uptime;');
cy.contains('View in Discover').should('exist');
cy.contains('View in Lens').should('exist');
cy.contains('Add to Case').should('not.exist');

View file

@ -12,6 +12,7 @@
"requiredPlugins": [
"actions",
"data",
"licensing",
"dataViews",
"discover",
"features",

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useKibana } from '../common/lib/kibana';
import type { AddToCaseButtonProps } from './add_to_cases_button';
import { AddToCaseButton } from './add_to_cases_button';
const CASES_OWNER: string[] = [];
export const AddToCaseWrapper: React.FC<AddToCaseButtonProps> = React.memo((props) => {
const { cases } = useKibana().services;
const casePermissions = cases.helpers.canUseCases();
const CasesContext = cases.ui.getCasesContext();
return (
<CasesContext owner={CASES_OWNER} permissions={casePermissions}>
<AddToCaseButton {...props} />{' '}
</CasesContext>
);
});
AddToCaseWrapper.displayName = 'AddToCaseWrapper';

View file

@ -19,7 +19,7 @@ const ADD_TO_CASE = i18n.translate(
}
);
interface IProps {
export interface AddToCaseButtonProps {
queryId: string;
agentIds?: string[];
actionId: string;
@ -28,7 +28,7 @@ interface IProps {
iconProps?: Record<string, string>;
}
export const AddToCaseButton: React.FC<IProps> = ({
export const AddToCaseButton: React.FC<AddToCaseButtonProps> = ({
actionId,
agentIds = [],
queryId = '',

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import moment from 'moment-timezone';
import { usePackQueryLastResults } from '../packs/use_pack_query_last_results';
import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table';
import { ViewResultsInDiscoverAction } from './view_results_in_discover';
interface PackViewInActionProps {
item: {
id: string;
interval: number;
action_id?: string;
agents: string[];
};
actionId?: string;
}
const PackViewInDiscoverActionComponent: React.FC<PackViewInActionProps> = ({ item }) => {
const { action_id: actionId, agents: agentIds, interval } = item;
const { data: lastResultsData } = usePackQueryLastResults({
actionId,
interval,
});
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'}
/>
);
};
export const PackViewInDiscoverAction = React.memo(PackViewInDiscoverActionComponent);

View file

@ -0,0 +1,141 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilterStateStore } from '@kbn/es-query';
import { useKibana } from '../common/lib/kibana';
import { useLogsDataView } from '../common/hooks/use_logs_data_view';
import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table';
interface ViewResultsInDiscoverActionProps {
actionId?: string;
agentIds?: string[];
buttonType: ViewResultsActionButtonType;
endDate?: string;
startDate?: string;
mode?: string;
}
const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
actionId,
agentIds,
buttonType,
endDate,
startDate,
}) => {
const { discover, application } = useKibana().services;
const locator = discover?.locator;
const discoverPermissions = application.capabilities.discover;
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
const [discoverUrl, setDiscoverUrl] = useState<string>('');
useEffect(() => {
const getDiscoverUrl = async () => {
if (!locator || !logsDataView) return;
const agentIdsQuery = agentIds?.length
? {
bool: {
minimum_should_match: 1,
should: agentIds.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })),
},
}
: null;
const newUrl = await locator.getUrl({
indexPatternId: logsDataView.id,
filters: [
{
meta: {
index: logsDataView.id,
alias: null,
negate: false,
disabled: false,
type: 'phrase',
key: 'action_id',
params: { query: actionId },
},
query: { match_phrase: { action_id: actionId } },
$state: { store: FilterStateStore.APP_STATE },
},
...(agentIdsQuery
? [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: 'agent IDs',
disabled: false,
index: logsDataView.id,
key: 'query',
negate: false,
type: 'custom',
value: JSON.stringify(agentIdsQuery),
},
query: agentIdsQuery,
},
]
: []),
],
refreshInterval: {
pause: true,
value: 0,
},
timeRange:
startDate && endDate
? {
to: endDate,
from: startDate,
mode: 'absolute',
}
: {
to: 'now',
from: 'now-1d',
mode: 'relative',
},
});
setDiscoverUrl(newUrl);
};
getDiscoverUrl();
}, [actionId, agentIds, endDate, startDate, locator, logsDataView]);
if (!discoverPermissions.show) {
return null;
}
if (buttonType === ViewResultsActionButtonType.button) {
return (
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl} target="_blank">
{VIEW_IN_DISCOVER}
</EuiButtonEmpty>
);
}
return (
<EuiToolTip content={VIEW_IN_DISCOVER}>
<EuiButtonIcon
iconType="discoverApp"
aria-label={VIEW_IN_DISCOVER}
href={discoverUrl}
target="_blank"
isDisabled={!actionId}
/>
</EuiToolTip>
);
};
const VIEW_IN_DISCOVER = i18n.translate(
'xpack.osquery.pack.queriesTable.viewDiscoverResultsActionAriaLabel',
{
defaultMessage: 'View in Discover',
}
);
export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent);

View file

@ -34,7 +34,7 @@ const DIFFERENTIAL_OPTION = {
inputDisplay: (
<FormattedMessage
id="xpack.osquery.pack.queryFlyoutForm.resultsTypeField.differentialValueLabel"
defaultMessage="Diffential"
defaultMessage="Differential"
/>
),
};

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import moment from 'moment-timezone';
import { usePackQueryLastResults } from '../packs/use_pack_query_last_results';
import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table';
import { ViewResultsInLensAction } from './view_results_in_lens';
interface PackViewInActionProps {
item: {
id: string;
interval: number;
action_id?: string;
agents: string[];
};
actionId?: string;
}
const PackViewInLensActionComponent: React.FC<PackViewInActionProps> = ({ item }) => {
const { action_id: actionId, agents: agentIds, interval } = item;
const { data: lastResultsData } = usePackQueryLastResults({
actionId,
interval,
});
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'}
/>
);
};
export const PackViewInLensAction = React.memo(PackViewInLensActionComponent);

View file

@ -0,0 +1,234 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import type {
PersistedIndexPatternLayer,
PieVisualizationState,
TermsIndexPatternColumn,
TypedLensByValueInput,
} from '@kbn/lens-plugin/public';
import { DOCUMENT_FIELD_NAME as RECORDS_FIELD } from '@kbn/lens-plugin/common/constants';
import { FilterStateStore } from '@kbn/es-query';
import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table';
import type { LogsDataView } from '../common/hooks/use_logs_data_view';
import { useKibana } from '../common/lib/kibana';
import { useLogsDataView } from '../common/hooks/use_logs_data_view';
interface ViewResultsInLensActionProps {
actionId?: string;
agentIds?: string[];
buttonType: ViewResultsActionButtonType;
endDate?: string;
startDate?: string;
mode?: string;
}
const ViewResultsInLensActionComponent: React.FC<ViewResultsInLensActionProps> = ({
actionId,
agentIds,
buttonType,
endDate,
startDate,
mode,
}) => {
const lensService = useKibana().services.lens;
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
const handleClick = useCallback(
(event) => {
event.preventDefault();
if (logsDataView) {
lensService?.navigateToPrefilledEditor(
{
id: '',
timeRange: {
from: startDate ?? 'now-1d',
to: endDate ?? 'now',
mode: mode ?? (startDate || endDate) ? 'absolute' : 'relative',
},
attributes: getLensAttributes(logsDataView, actionId, agentIds),
},
{
openInNewTab: true,
skipAppLeave: true,
}
);
}
},
[actionId, agentIds, endDate, lensService, logsDataView, mode, startDate]
);
const isDisabled = useMemo(() => !actionId || !logsDataView, [actionId, logsDataView]);
if (buttonType === ViewResultsActionButtonType.button) {
return (
<EuiButtonEmpty size="xs" iconType="lensApp" onClick={handleClick} isDisabled={isDisabled}>
{VIEW_IN_LENS}
</EuiButtonEmpty>
);
}
return (
<EuiToolTip content={VIEW_IN_LENS}>
<EuiButtonIcon
iconType="lensApp"
disabled={false}
onClick={handleClick}
aria-label={VIEW_IN_LENS}
isDisabled={isDisabled}
/>
</EuiToolTip>
);
};
function getLensAttributes(
logsDataView: LogsDataView,
actionId?: string,
agentIds?: string[]
): TypedLensByValueInput['attributes'] {
const dataLayer: PersistedIndexPatternLayer = {
columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'],
columns: {
'8690befd-fd69-4246-af4a-dd485d2a3b38': {
sourceField: 'type',
isBucketed: true,
dataType: 'string',
scale: 'ordinal',
operationType: 'terms',
label: 'Top values of type',
params: {
otherBucket: true,
size: 5,
missingBucket: false,
orderBy: {
columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
type: 'column',
},
orderDirection: 'desc',
},
} as TermsIndexPatternColumn,
'ed999e9d-204c-465b-897f-fe1a125b39ed': {
sourceField: RECORDS_FIELD,
isBucketed: false,
dataType: 'number',
scale: 'ratio',
operationType: 'count',
label: 'Count of records',
},
},
incompleteColumns: {},
};
const xyConfig: PieVisualizationState = {
shape: 'pie',
layers: [
{
layerType: 'data',
legendDisplay: 'default',
nestedLegend: false,
layerId: 'layer1',
metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
numberDisplay: 'percent',
primaryGroups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'],
categoryDisplay: 'default',
},
],
};
const agentIdsQuery = agentIds?.length
? {
bool: {
minimum_should_match: 1,
should: agentIds?.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })),
},
}
: undefined;
return {
visualizationType: 'lnsPie',
title: `Action ${actionId} results`,
references: [
{
id: logsDataView.id,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: logsDataView.id,
name: 'indexpattern-datasource-layer-layer1',
type: 'index-pattern',
},
{
name: 'filter-index-pattern-0',
id: logsDataView.id,
type: 'index-pattern',
},
],
state: {
datasourceStates: {
indexpattern: {
layers: {
layer1: dataLayer,
},
},
},
filters: [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
index: 'filter-index-pattern-0',
negate: false,
alias: null,
disabled: false,
params: {
query: actionId,
},
type: 'phrase',
key: 'action_id',
},
query: {
match_phrase: {
action_id: actionId,
},
},
},
...(agentIdsQuery
? [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: 'agent IDs',
disabled: false,
index: 'filter-index-pattern-0',
key: 'query',
negate: false,
type: 'custom',
value: JSON.stringify(agentIdsQuery),
},
query: agentIdsQuery,
},
]
: []),
],
query: { language: 'kuery', query: '' },
visualization: xyConfig,
},
};
}
const VIEW_IN_LENS = i18n.translate(
'xpack.osquery.pack.queriesTable.viewLensResultsActionAriaLabel',
{
defaultMessage: 'View in Lens',
}
);
export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent);

View file

@ -12,6 +12,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm as useHookForm, FormProvider } from 'react-hook-form';
import { isEmpty, find, pickBy } from 'lodash';
import { AddToCaseWrapper } from '../../cases/add_to_cases';
import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline';
import { QueryPackSelectable } from './query_pack_selectable';
import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form';
@ -25,7 +26,6 @@ import type { AgentSelection } from '../../agents/types';
import { LiveQueryQueryField } from './live_query_query_field';
import { AgentsTableField } from './agents_table_field';
import { savedQueryDataSerializer } from '../../saved_queries/form/use_saved_query_form';
import { AddToCaseButton } from '../../cases/add_to_cases_button';
import { PackFieldWrapper } from '../../shared_components/osquery_response_action_type/pack_field_wrapper';
export interface LiveQueryFormFields {
@ -213,7 +213,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
(payload) => {
if (liveQueryActionId) {
return (
<AddToCaseButton
<AddToCaseWrapper
queryId={payload.queryId}
agentIds={agentIds}
actionId={liveQueryActionId}

View file

@ -10,7 +10,6 @@ import type { ReactElement } from 'react';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import {
EuiBasicTable,
EuiButtonEmpty,
EuiCodeBlock,
EuiButtonIcon,
EuiToolTip,
@ -23,31 +22,16 @@ import {
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment-timezone';
import type {
TypedLensByValueInput,
PersistedIndexPatternLayer,
PieVisualizationState,
TermsIndexPatternColumn,
} from '@kbn/lens-plugin/public';
import { DOCUMENT_FIELD_NAME as RECORDS_FIELD } from '@kbn/lens-plugin/common/constants';
import { FilterStateStore } from '@kbn/es-query';
import styled from 'styled-components';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { SECURITY_APP_NAME } from '../../timelines/get_add_to_timeline';
import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline';
import { PackResultsHeader } from './pack_results_header';
import { Direction } from '../../../common/search_strategy';
import { removeMultilines } from '../../../common/utils/build_query/remove_multilines';
import { useKibana } from '../../common/lib/kibana';
import { usePackQueryLastResults } from '../../packs/use_pack_query_last_results';
import { ResultTabs } from '../../routes/saved_queries/edit/tabs';
import type { PackItem } from '../../packs/types';
import type { LogsDataView } from '../../common/hooks/use_logs_data_view';
import { useLogsDataView } from '../../common/hooks/use_logs_data_view';
const CASES_OWNER: string[] = [];
import { PackViewInLensAction } from '../../lens/pack_view_in_lens';
import { PackViewInDiscoverAction } from '../../discover/pack_view_in_discover';
const TruncateTooltipText = styled.div`
width: 100%;
@ -68,346 +52,11 @@ const StyledEuiBasicTable = styled(EuiBasicTable)`
}
`;
const VIEW_IN_DISCOVER = i18n.translate(
'xpack.osquery.pack.queriesTable.viewDiscoverResultsActionAriaLabel',
{
defaultMessage: 'View in Discover',
}
);
const VIEW_IN_LENS = i18n.translate(
'xpack.osquery.pack.queriesTable.viewLensResultsActionAriaLabel',
{
defaultMessage: 'View in Lens',
}
);
export enum ViewResultsActionButtonType {
icon = 'icon',
button = 'button',
}
interface ViewResultsInDiscoverActionProps {
actionId?: string;
agentIds?: string[];
buttonType: ViewResultsActionButtonType;
endDate?: string;
startDate?: string;
mode?: string;
}
function getLensAttributes(
logsDataView: LogsDataView,
actionId?: string,
agentIds?: string[]
): TypedLensByValueInput['attributes'] {
const dataLayer: PersistedIndexPatternLayer = {
columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'],
columns: {
'8690befd-fd69-4246-af4a-dd485d2a3b38': {
sourceField: 'type',
isBucketed: true,
dataType: 'string',
scale: 'ordinal',
operationType: 'terms',
label: 'Top values of type',
params: {
otherBucket: true,
size: 5,
missingBucket: false,
orderBy: {
columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
type: 'column',
},
orderDirection: 'desc',
},
} as TermsIndexPatternColumn,
'ed999e9d-204c-465b-897f-fe1a125b39ed': {
sourceField: RECORDS_FIELD,
isBucketed: false,
dataType: 'number',
scale: 'ratio',
operationType: 'count',
label: 'Count of records',
},
},
incompleteColumns: {},
};
const xyConfig: PieVisualizationState = {
shape: 'pie',
layers: [
{
layerType: 'data',
legendDisplay: 'default',
nestedLegend: false,
layerId: 'layer1',
metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
numberDisplay: 'percent',
primaryGroups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'],
categoryDisplay: 'default',
},
],
};
const agentIdsQuery = agentIds?.length
? {
bool: {
minimum_should_match: 1,
should: agentIds?.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })),
},
}
: undefined;
return {
visualizationType: 'lnsPie',
title: `Action ${actionId} results`,
references: [
{
id: logsDataView.id,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: logsDataView.id,
name: 'indexpattern-datasource-layer-layer1',
type: 'index-pattern',
},
{
name: 'filter-index-pattern-0',
id: logsDataView.id,
type: 'index-pattern',
},
],
state: {
datasourceStates: {
indexpattern: {
layers: {
layer1: dataLayer,
},
},
},
filters: [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
index: 'filter-index-pattern-0',
negate: false,
alias: null,
disabled: false,
params: {
query: actionId,
},
type: 'phrase',
key: 'action_id',
},
query: {
match_phrase: {
action_id: actionId,
},
},
},
...(agentIdsQuery
? [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: 'agent IDs',
disabled: false,
index: 'filter-index-pattern-0',
key: 'query',
negate: false,
type: 'custom',
value: JSON.stringify(agentIdsQuery),
},
query: agentIdsQuery,
},
]
: []),
],
query: { language: 'kuery', query: '' },
visualization: xyConfig,
},
};
}
const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
actionId,
agentIds,
buttonType,
endDate,
startDate,
mode,
}) => {
const lensService = useKibana().services.lens;
const isLensAvailable = lensService?.canUseEditor();
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
const handleClick = useCallback(
(event) => {
event.preventDefault();
if (logsDataView) {
lensService?.navigateToPrefilledEditor(
{
id: '',
timeRange: {
from: startDate ?? 'now-1d',
to: endDate ?? 'now',
mode: mode ?? (startDate || endDate) ? 'absolute' : 'relative',
},
attributes: getLensAttributes(logsDataView, actionId, agentIds),
},
{
openInNewTab: true,
skipAppLeave: true,
}
);
}
},
[actionId, agentIds, endDate, lensService, logsDataView, mode, startDate]
);
const isDisabled = useMemo(() => !actionId || !logsDataView, [actionId, logsDataView]);
if (!isLensAvailable) {
return null;
}
if (buttonType === ViewResultsActionButtonType.button) {
return (
<EuiButtonEmpty size="xs" iconType="lensApp" onClick={handleClick} isDisabled={isDisabled}>
{VIEW_IN_LENS}
</EuiButtonEmpty>
);
}
return (
<EuiToolTip content={VIEW_IN_LENS}>
<EuiButtonIcon
iconType="lensApp"
disabled={false}
onClick={handleClick}
aria-label={VIEW_IN_LENS}
isDisabled={isDisabled}
/>
</EuiToolTip>
);
};
export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent);
const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
actionId,
agentIds,
buttonType,
endDate,
startDate,
}) => {
const { discover, application } = useKibana().services;
const locator = discover?.locator;
const discoverPermissions = application.capabilities.discover;
const { data: logsDataView } = useLogsDataView({ skip: !actionId });
const [discoverUrl, setDiscoverUrl] = useState<string>('');
useEffect(() => {
const getDiscoverUrl = async () => {
if (!locator || !logsDataView) return;
const agentIdsQuery = agentIds?.length
? {
bool: {
minimum_should_match: 1,
should: agentIds.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })),
},
}
: null;
const newUrl = await locator.getUrl({
indexPatternId: logsDataView.id,
filters: [
{
meta: {
index: logsDataView.id,
alias: null,
negate: false,
disabled: false,
type: 'phrase',
key: 'action_id',
params: { query: actionId },
},
query: { match_phrase: { action_id: actionId } },
$state: { store: FilterStateStore.APP_STATE },
},
...(agentIdsQuery
? [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: 'agent IDs',
disabled: false,
index: logsDataView.id,
key: 'query',
negate: false,
type: 'custom',
value: JSON.stringify(agentIdsQuery),
},
query: agentIdsQuery,
},
]
: []),
],
refreshInterval: {
pause: true,
value: 0,
},
timeRange:
startDate && endDate
? {
to: endDate,
from: startDate,
mode: 'absolute',
}
: {
to: 'now',
from: 'now-1d',
mode: 'relative',
},
});
setDiscoverUrl(newUrl);
};
getDiscoverUrl();
}, [actionId, agentIds, endDate, startDate, locator, logsDataView]);
if (!discoverPermissions.show) {
return null;
}
if (buttonType === ViewResultsActionButtonType.button) {
return (
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl} target="_blank">
{VIEW_IN_DISCOVER}
</EuiButtonEmpty>
);
}
return (
<EuiToolTip content={VIEW_IN_DISCOVER}>
<EuiButtonIcon
iconType="discoverApp"
aria-label={VIEW_IN_DISCOVER}
href={discoverUrl}
target="_blank"
isDisabled={!actionId}
/>
</EuiToolTip>
);
};
export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent);
interface DocsColumnResultsProps {
count?: number;
isLive?: boolean;
@ -450,72 +99,6 @@ const AgentsColumnResults: React.FC<AgentsColumnResultsProps> = ({
</EuiFlexGroup>
);
interface PackViewInActionProps {
item: {
id: string;
interval: number;
action_id?: string;
agents: string[];
};
actionId?: string;
}
const PackViewInDiscoverActionComponent: React.FC<PackViewInActionProps> = ({ item }) => {
const { action_id: actionId, agents: agentIds, interval } = item;
const { data: lastResultsData } = usePackQueryLastResults({
actionId,
interval,
});
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 }) => {
const { action_id: actionId, agents: agentIds, interval } = item;
const { data: lastResultsData } = usePackQueryLastResults({
actionId,
interval,
});
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);
type PackQueryStatusItem = Partial<{
action_id: string;
id: string;
@ -542,10 +125,12 @@ interface PackQueriesStatusTableProps {
actionId,
isIcon,
isDisabled,
queryId,
}: {
actionId?: string;
isIcon?: boolean;
isDisabled?: boolean;
queryId?: string;
}) => ReactElement;
showResultsHeader?: boolean;
}
@ -562,10 +147,6 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
showResultsHeader,
}) => {
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, unknown>>({});
const { cases, timelines, appName } = useKibana().services;
const casePermissions = cases.helpers.canUseCases();
const CasesContext = cases.ui.getCasesContext();
const renderIDColumn = useCallback(
(id: string) => (
<TruncateTooltipText>
@ -619,11 +200,15 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
const renderLensResultsAction = useCallback((item) => <PackViewInLensAction item={item} />, []);
const handleAddToCase = useCallback(
(payload: { actionId: string; isIcon?: boolean }) =>
(payload: { actionId?: string; isIcon?: boolean; queryId: string }) =>
// eslint-disable-next-line react/display-name
() => {
if (addToCase) {
return addToCase({ actionId: payload.actionId, isIcon: payload?.isIcon });
return addToCase({
actionId: payload.actionId,
isIcon: payload.isIcon,
queryId: payload.queryId,
});
}
return <></>;
@ -648,7 +233,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
agentIds={agentIds}
failedAgentsCount={item?.failed ?? 0}
addToTimeline={addToTimeline}
addToCase={addToCase && handleAddToCase({ actionId: item.action_id })}
addToCase={addToCase && handleAddToCase({ queryId: item.action_id, actionId })}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -658,7 +243,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
return itemIdToExpandedRowMapValues;
});
},
[startDate, expirationDate, agentIds, addToTimeline, addToCase, handleAddToCase]
[startDate, expirationDate, agentIds, addToTimeline, addToCase, handleAddToCase, actionId]
);
const renderToggleResultsAction = useCallback(
@ -677,28 +262,37 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
const getItemId = useCallback((item: PackItem) => get(item, 'id'), []);
const columns = useMemo(() => {
const resultActions = [
{
render: renderDiscoverResultsAction,
},
{
render: renderLensResultsAction,
},
{
available: () => !!addToCase,
render: (item: { action_id: string }) =>
addToCase &&
addToCase({ actionId: item.action_id, isIcon: true, isDisabled: !item.action_id }),
},
{
available: () => addToTimeline && timelines && appName === SECURITY_APP_NAME,
render: (item: { action_id: string }) =>
addToTimeline && addToTimeline({ query: ['action_id', item.action_id], isIcon: true }),
},
];
const renderResultActions = useCallback(
(row: { action_id: string }) => {
const resultActions = [
{
render: renderDiscoverResultsAction,
},
{
render: renderLensResultsAction,
},
{
render: (item: { action_id: string }) =>
addToTimeline && addToTimeline({ query: ['action_id', item.action_id], isIcon: true }),
},
{
render: (item: { action_id: string }) =>
addToCase &&
addToCase({
actionId,
queryId: item.action_id,
isIcon: true,
isDisabled: !item.action_id,
}),
},
];
return [
return resultActions.map((action) => action.render(row));
},
[actionId, addToCase, addToTimeline, renderDiscoverResultsAction, renderLensResultsAction]
);
const columns = useMemo(
() => [
{
field: 'id',
name: i18n.translate('xpack.osquery.pack.queriesTable.idColumnTitle', {
@ -734,7 +328,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
defaultMessage: 'View results',
}),
width: '90px',
actions: resultActions,
render: renderResultActions,
},
{
id: 'actions',
@ -747,21 +341,16 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
},
],
},
];
}, [
renderDiscoverResultsAction,
renderLensResultsAction,
renderIDColumn,
renderQueryColumn,
renderDocsColumn,
renderAgentsColumn,
renderToggleResultsAction,
addToCase,
addToTimeline,
timelines,
appName,
]);
],
[
renderIDColumn,
renderQueryColumn,
renderDocsColumn,
renderAgentsColumn,
renderResultActions,
renderToggleResultsAction,
]
);
const sorting = useMemo(
() => ({
sort: {
@ -798,7 +387,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
);
return (
<CasesContext owner={CASES_OWNER} permissions={casePermissions}>
<>
{showResultsHeader && (
<PackResultsHeader queryIds={queryIds} actionId={actionId} addToCase={addToCase} />
)}
@ -811,7 +400,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
isExpandable
/>
</CasesContext>
</>
);
};

View file

@ -35,6 +35,7 @@ const StyledIconsList = styled(EuiFlexItem)`
export const PackResultsHeader = ({ actionId, addToCase }: PackResultsHeadersProps) => (
<>
<EuiSpacer size={'l'} />
<EuiFlexGroup direction="row" gutterSize="m">
<StyledResultsHeading grow={false}>
<EuiText>

View file

@ -155,7 +155,7 @@ const PackFormComponent: React.FC<PackFormProps> = ({
<EuiFlexGroup>
<EuiFlexItem>
<PolicyIdComboBoxField euiFieldProps={euiFieldProps} />
<PolicyIdComboBoxField />
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule />

View file

@ -79,7 +79,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
resetField('version', { defaultValue: savedQuery.version ? [savedQuery.version] : [] });
resetField('interval', { defaultValue: savedQuery.interval ? savedQuery.interval : 3600 });
resetField('snapshot', { defaultValue: savedQuery.snapshot ?? true });
resetField('removed');
resetField('removed', { defaultValue: savedQuery.removed });
resetField('ecs_mapping', { defaultValue: savedQuery.ecs_mapping ?? {} });
}
},

View file

@ -10,13 +10,18 @@ import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { AddToCaseButton } from '../../../cases/add_to_cases_button';
import styled from 'styled-components';
import { AddToCaseWrapper } from '../../../cases/add_to_cases';
import { useRouterNavigate } from '../../../common/lib/kibana';
import { WithHeaderLayout } from '../../../components/layouts';
import { useLiveQueryDetails } from '../../../actions/use_live_query_details';
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
import { PackQueriesStatusTable } from '../../../live_queries/form/pack_queries_status_table';
const StyledTableWrapper = styled(EuiFlexItem)`
padding-left: 10px;
`;
const LiveQueryDetailsPageComponent = () => {
const { actionId } = useParams<{ actionId: string }>();
useBreadcrumbs('live_query_details', { liveQueryId: actionId });
@ -55,7 +60,7 @@ const LiveQueryDetailsPageComponent = () => {
}, [data?.status]);
const addToCaseButton = useCallback(
(payload) => (
<AddToCaseButton
<AddToCaseWrapper
queryId={payload.queryId}
actionId={actionId}
agentIds={data?.agents}
@ -69,15 +74,17 @@ const LiveQueryDetailsPageComponent = () => {
return (
<WithHeaderLayout leftColumn={LeftColumn} rightColumnGrow={false}>
<PackQueriesStatusTable
actionId={actionId}
data={data?.queries}
startDate={data?.['@timestamp']}
expirationDate={data?.expiration}
agentIds={data?.agents}
addToCase={addToCaseButton}
showResultsHeader
/>
<StyledTableWrapper>
<PackQueriesStatusTable
actionId={actionId}
data={data?.queries}
startDate={data?.['@timestamp']}
expirationDate={data?.expiration}
agentIds={data?.agents}
addToCase={addToCaseButton}
showResultsHeader
/>
</StyledTableWrapper>
</WithHeaderLayout>
);
};

View file

@ -10,13 +10,10 @@ import React, { useMemo } from 'react';
import type { ReactElement } from 'react';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { useKibana } from '../../../common/lib/kibana';
import type { AddToTimelinePayload } from '../../../timelines/get_add_to_timeline';
import { ResultsTable } from '../../../results/results_table';
import { ActionResultsSummary } from '../../../action_results/action_results_summary';
const CASES_OWNER: string[] = [];
interface ResultTabsProps {
actionId: string;
agentIds?: string[];
@ -38,10 +35,6 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
addToTimeline,
addToCase,
}) => {
const { cases } = useKibana().services;
const casePermissions = cases.helpers.canUseCases();
const CasesContext = cases.ui.getCasesContext();
const tabs = useMemo(
() => [
{
@ -85,16 +78,14 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
);
return (
<CasesContext owner={CASES_OWNER} permissions={casePermissions}>
<EuiTabbedContent
// TODO: extend the EuiTabbedContent component to support EuiTabs props
// bottomBorder={false}
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
expand={false}
/>
</CasesContext>
<EuiTabbedContent
// TODO: extend the EuiTabbedContent component to support EuiTabs props
// bottomBorder={false}
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
expand={false}
/>
);
};

View file

@ -72,11 +72,6 @@ export const savedQueryDataSerializer = (payload: SavedQueryFormData): SavedQuer
draft.interval = draft.interval + '';
}
if (draft.snapshot) {
delete draft.snapshot;
delete draft.removed;
}
return draft;
});

View file

@ -6,17 +6,12 @@
*/
import { EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import React, { useMemo } from 'react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { OsqueryEmptyPrompt, OsqueryNotAvailablePrompt } from '../prompts';
import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline';
import {
AGENT_STATUS_ERROR,
EMPTY_PROMPT,
NOT_AVAILABLE,
PERMISSION_DENIED,
SHORT_EMPTY_TITLE,
} from './translations';
import { AGENT_STATUS_ERROR, PERMISSION_DENIED, SHORT_EMPTY_TITLE } from './translations';
import { useKibana } from '../../common/lib/kibana';
import { LiveQuery } from '../../live_queries';
import { OsqueryIcon } from '../../components/osquery_icon';
@ -39,22 +34,11 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
}) => {
const permissions = useKibana().services.application.capabilities.osquery;
const emptyPrompt = useMemo(
() => (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>{SHORT_EMPTY_TITLE}</h2>}
titleSize="xs"
body={<p>{EMPTY_PROMPT}</p>}
/>
),
[]
);
const { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData } =
useIsOsqueryAvailable(agentId);
if (agentId && agentFetched && !agentData) {
return emptyPrompt;
return <OsqueryEmptyPrompt />;
}
if (
@ -91,14 +75,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
}
if (agentId && !osqueryAvailable) {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>{SHORT_EMPTY_TITLE}</h2>}
titleSize="xs"
body={<p>{NOT_AVAILABLE}</p>}
/>
);
return <OsqueryNotAvailablePrompt />;
}
if (agentId && agentData?.status !== 'online') {

View file

@ -20,7 +20,7 @@ import {
DETAILS_ID,
DETAILS_QUERY,
DETAILS_TIMESTAMP,
mockCasesContext,
getMockedKibanaConfig,
} from './test_utils';
jest.mock('../../common/lib/kibana');
@ -43,34 +43,8 @@ const defaultProps = {
queryId: '',
};
const mockKibana = (permissionType: unknown = defaultPermissions) => {
useKibanaMock.mockReturnValue({
services: {
application: {
capabilities: permissionType,
},
cases: {
helpers: {
canUseCases: jest.fn(),
},
ui: {
getCasesContext: jest.fn().mockImplementation(() => mockCasesContext),
},
},
data: {
dataViews: {
getCanSaveSync: jest.fn(),
hasData: {
hasESData: jest.fn(),
hasUserDataView: jest.fn(),
hasDataView: jest.fn(),
},
},
},
notifications: {
toasts: jest.fn(),
},
},
} as unknown as ReturnType<typeof useKibana>);
const mockedKibana = getMockedKibanaConfig(permissionType);
useKibanaMock.mockReturnValue(mockedKibana);
};
const renderWithContext = (Element: React.ReactElement) =>

View file

@ -6,9 +6,10 @@
*/
import { EuiComment, EuiSpacer } from '@elastic/eui';
import React from 'react';
import React, { useCallback } from 'react';
import { FormattedRelative } from '@kbn/i18n-react';
import { AddToCaseWrapper } from '../../cases/add_to_cases';
import type { OsqueryActionResultsProps } from './types';
import { useLiveQueryDetails } from '../../actions/use_live_query_details';
import { ATTACHED_QUERY } from '../../agents/translations';
@ -22,7 +23,6 @@ interface OsqueryResultProps extends Omit<OsqueryActionResultsProps, 'alertId'>
export const OsqueryResult = ({
actionId,
queryId,
ruleName,
addToTimeline,
agentIds,
@ -30,10 +30,22 @@ export const OsqueryResult = ({
}: OsqueryResultProps) => {
const { data } = useLiveQueryDetails({
actionId,
// isLive,
// ...(queryId ? { queryIds: [queryId] } : {}),
});
const addToCaseButton = useCallback(
(payload) => (
<AddToCaseWrapper
queryId={payload.queryId}
actionId={actionId}
agentIds={data?.agents}
isIcon={payload.isIcon}
isDisabled={payload.isDisabled}
iconProps={payload.iconProps}
/>
),
[data?.agents, actionId]
);
return (
<div>
<EuiSpacer size="s" />
@ -51,6 +63,7 @@ export const OsqueryResult = ({
expirationDate={data?.expiration}
agentIds={agentIds}
addToTimeline={addToTimeline}
addToCase={addToCaseButton}
/>
</EuiComment>
<EuiSpacer size="s" />

View file

@ -17,7 +17,7 @@ import * as useAllLiveQueries from '../../actions/use_all_live_queries';
import * as useLiveQueryDetails from '../../actions/use_live_query_details';
import { PERMISSION_DENIED } from '../osquery_action/translations';
import * as privileges from '../../action_results/use_action_privileges';
import { defaultLiveQueryDetails, DETAILS_QUERY, mockCasesContext } from './test_utils';
import { defaultLiveQueryDetails, DETAILS_QUERY, getMockedKibanaConfig } from './test_utils';
jest.mock('../../common/lib/kibana');
@ -49,34 +49,8 @@ const defaultPermissions = {
};
const mockKibana = (permissionType: unknown = defaultPermissions) => {
useKibanaMock.mockReturnValue({
services: {
application: {
capabilities: permissionType,
},
cases: {
helpers: {
canUseCases: jest.fn(),
},
ui: {
getCasesContext: jest.fn().mockImplementation(() => mockCasesContext),
},
},
data: {
dataViews: {
getCanSaveSync: jest.fn(),
hasData: {
hasESData: jest.fn(),
hasUserDataView: jest.fn(),
hasDataView: jest.fn(),
},
},
},
notifications: {
toasts: jest.fn(),
},
},
} as unknown as ReturnType<typeof useKibana>);
const mockedKibana = getMockedKibanaConfig(permissionType);
useKibanaMock.mockReturnValue(mockedKibana);
};
const renderWithContext = (Element: React.ReactElement) =>

View file

@ -6,6 +6,7 @@
*/
import React from 'react';
import type { useKibana } from '../../common/lib/kibana';
export const DETAILS_QUERY = 'select * from uptime';
export const DETAILS_ID = 'test-id';
@ -38,4 +39,41 @@ export const defaultLiveQueryDetails = {
},
} as never;
export const getMockedKibanaConfig = (permissionType: unknown) =>
({
services: {
application: {
capabilities: permissionType,
},
cases: {
helpers: {
canUseCases: jest.fn().mockImplementation(() => ({
read: true,
update: true,
push: true,
})),
},
ui: {
getCasesContext: jest.fn().mockImplementation(() => mockCasesContext),
},
hooks: {
getUseCasesAddToExistingCaseModal: jest.fn(),
},
},
data: {
dataViews: {
getCanSaveSync: jest.fn(),
hasData: {
hasESData: jest.fn(),
hasUserDataView: jest.fn(),
hasDataView: jest.fn(),
},
},
},
notifications: {
toasts: jest.fn(),
},
},
} as unknown as ReturnType<typeof useKibana>);
export const mockCasesContext: React.FC = (props) => <>{props?.children ?? null}</>;

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { OsqueryIcon } from '../components/osquery_icon';
import { EMPTY_PROMPT, NOT_AVAILABLE, SHORT_EMPTY_TITLE } from './osquery_action/translations';
export const OsqueryEmptyPrompt = () => (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>{SHORT_EMPTY_TITLE}</h2>}
titleSize="xs"
body={<p>{EMPTY_PROMPT}</p>}
/>
);
export const OsqueryNotAvailablePrompt = () => (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>{SHORT_EMPTY_TITLE}</h2>}
titleSize="xs"
body={<p>{NOT_AVAILABLE}</p>}
/>
);

View file

@ -144,7 +144,10 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
}
set(draft, `inputs[0].config.osquery.value.packs.${packSO.attributes.name}`, {
queries: convertSOQueriesToPack(queries, { removeMultiLines: true }),
queries: convertSOQueriesToPack(queries, {
removeMultiLines: true,
removeResultType: true,
}),
});
return draft;

View file

@ -285,6 +285,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
{
queries: convertSOQueriesToPack(updatedPackSO.attributes.queries, {
removeMultiLines: true,
removeResultType: true,
}),
}
);
@ -315,7 +316,9 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
draft,
`inputs[0].config.osquery.value.packs.${updatedPackSO.attributes.name}`,
{
queries: updatedPackSO.attributes.queries,
queries: convertSOQueriesToPack(updatedPackSO.attributes.queries, {
removeResultType: true,
}),
}
);

View file

@ -42,15 +42,45 @@ describe('Pack utils', () => {
describe('convertSOQueriesToPack', () => {
test('converts to pack with empty ecs_mapping', () => {
const convertedQueries = convertSOQueriesToPack(getTestQueries());
expect(convertedQueries).toStrictEqual(getTestQueries({}));
expect(convertedQueries).toStrictEqual(getTestQueries());
});
test('converts to pack with converting query to single line', () => {
const convertedQueries = convertSOQueriesToPack(getTestQueries(), { removeMultiLines: true });
expect(convertedQueries).toStrictEqual(oneLiner);
expect(convertedQueries).toStrictEqual({
...oneLiner,
});
});
test('converts to object with pack names after query.id', () => {
const convertedQueries = convertSOQueriesToPack(getTestQueries({ id: 'testId' }));
expect(convertedQueries).toStrictEqual(getTestQueries({}, 'testId'));
});
test('converts with results snapshot set false', () => {
const convertedQueries = convertSOQueriesToPack(
getTestQueries({ snapshot: false, removed: true }),
{ removeResultType: true }
);
expect(convertedQueries).toStrictEqual(getTestQueries({ removed: true, snapshot: false }));
});
test('converts with results snapshot set true and removed false', () => {
const convertedQueries = convertSOQueriesToPack(
getTestQueries({ snapshot: true, removed: true }),
{ removeResultType: true }
);
expect(convertedQueries).toStrictEqual(getTestQueries({}));
});
test('converts with results snapshot set true but removed false', () => {
const convertedQueries = convertSOQueriesToPack(
getTestQueries({ snapshot: true, removed: false }),
{ removeResultType: true }
);
expect(convertedQueries).toStrictEqual(getTestQueries({}));
});
test('converts with both results set to false', () => {
const convertedQueries = convertSOQueriesToPack(
getTestQueries({ snapshot: false, removed: false }),
{ removeResultType: true }
);
expect(convertedQueries).toStrictEqual(getTestQueries({ removed: false, snapshot: false }));
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { isEmpty, pick, reduce } from 'lodash';
import { isEmpty, pick, reduce, isArray } from 'lodash';
import { DEFAULT_PLATFORM } from '../../../common/constants';
import { removeMultilines } from '../../../common/utils/build_query/remove_multilines';
import { convertECSMappingToArray, convertECSMappingToObject } from '../utils';
@ -37,18 +37,29 @@ export const convertPackQueriesToSO = (queries) =>
}>
);
// @ts-expect-error update types
export const convertSOQueriesToPack = (queries, options?: { removeMultiLines?: boolean }) =>
export const convertSOQueriesToPack = (
// @ts-expect-error update types
queries,
options?: { removeMultiLines?: boolean; removeResultType?: boolean }
) =>
reduce(
queries,
// eslint-disable-next-line @typescript-eslint/naming-convention
(acc, { id: queryId, ecs_mapping, query, platform, ...rest }, key) => {
(acc, { id: queryId, ecs_mapping, query, platform, removed, snapshot, ...rest }, key) => {
const resultType = !snapshot ? { removed, snapshot } : {};
const index = queryId ? queryId : key;
acc[index] = {
...rest,
query: options?.removeMultiLines ? removeMultilines(query) : query,
...(!isEmpty(ecs_mapping) ? { ecs_mapping: convertECSMappingToObject(ecs_mapping) } : {}),
...(!isEmpty(ecs_mapping)
? isArray(ecs_mapping)
? { ecs_mapping: convertECSMappingToObject(ecs_mapping) }
: { ecs_mapping }
: {}),
...(platform === DEFAULT_PLATFORM || platform === undefined ? {} : { platform }),
...(options?.removeResultType
? resultType
: { ...(snapshot ? { snapshot } : {}), ...(removed ? { removed } : {}) }),
};
return acc;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { isEmpty, pickBy, some } from 'lodash';
import { isEmpty, pickBy, some, isBoolean } from 'lodash';
import type { IRouter } from '@kbn/core/server';
import { PLUGIN_ID } from '../../../common';
import type { CreateSavedQueryRequestSchemaDecoded } from '../../../common/schemas/routes/saved_query/create_saved_query_request_schema';
@ -76,7 +76,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
updated_by: currentUser,
updated_at: new Date().toISOString(),
},
(value) => !isEmpty(value) || value === false
(value) => !isEmpty(value) || isBoolean(value)
)
);

View file

@ -29,7 +29,7 @@ import { LabelField } from './label_field';
import OsqueryLogo from './osquery_icon/osquery.svg';
import { OsqueryFlyout } from '../../../../../detections/components/osquery/osquery_flyout';
import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view';
import { convertECSMappingToObject } from './utils';
import { OsqueryNotAvailablePrompt } from './not_available_prompt';
const StyledEuiButton = styled(EuiButton)`
> span > img {
@ -49,7 +49,12 @@ const OsqueryEditorComponent = ({
};
}>) => {
const isEditMode = node != null;
const { osquery } = useKibana().services;
const {
osquery,
application: {
capabilities: { osquery: osqueryPermissions },
},
} = useKibana().services;
const formMethods = useForm<{
label: string;
query: string;
@ -70,7 +75,7 @@ const OsqueryEditorComponent = ({
{
query: data.query,
label: data.label,
ecs_mapping: convertECSMappingToObject(data.ecs_mapping),
ecs_mapping: data.ecs_mapping,
},
(value) => !isEmpty(value)
)
@ -83,6 +88,17 @@ const OsqueryEditorComponent = ({
[onSave]
);
const noOsqueryPermissions = useMemo(
() =>
(!osqueryPermissions.runSavedQueries || !osqueryPermissions.readSavedQueries) &&
!osqueryPermissions.writeLiveQueries,
[
osqueryPermissions.readSavedQueries,
osqueryPermissions.runSavedQueries,
osqueryPermissions.writeLiveQueries,
]
);
const OsqueryActionForm = useMemo(() => {
if (osquery?.LiveQueryField) {
const { LiveQueryField } = osquery;
@ -98,6 +114,10 @@ const OsqueryEditorComponent = ({
return null;
}, [formMethods, osquery]);
if (noOsqueryPermissions) {
return <OsqueryNotAvailablePrompt />;
}
return (
<>
<EuiModalHeader>

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiCode, EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
export const PERMISSION_DENIED = i18n.translate(
'xpack.securitySolution.markdown.osquery.permissionDenied',
{
defaultMessage: 'Permission denied',
}
);
export const OsqueryNotAvailablePrompt = () => (
<EuiEmptyPrompt
iconType="logoOsquery"
title={<h2>{PERMISSION_DENIED}</h2>}
titleSize="xs"
body={
<p>
<FormattedMessage
id="xpack.securitySolution.markdown.osquery.missingPrivilleges"
defaultMessage="To access this page, ask your administrator for {osquery} Kibana privileges."
values={{
osquery: <EuiCode>{'osquery'}</EuiCode>,
}}
/>
</p>
}
/>
);

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isEmpty, reduce } from 'lodash';
export const convertECSMappingToObject = (
ecsMapping: Array<{
key: string;
result: {
type: string;
value: string;
};
}>
) =>
reduce(
ecsMapping,
(acc, value) => {
if (!isEmpty(value?.key) && !isEmpty(value.result?.type) && !isEmpty(value.result?.value)) {
acc[value.key] = {
[value.result.type]: value.result.value,
};
}
return acc;
},
{} as Record<string, { field?: string; value?: string }>
);