mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* [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:
parent
7774b55c40
commit
95c797c26d
32 changed files with 842 additions and 676 deletions
|
@ -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();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
|
|
@ -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');
|
|
@ -12,6 +12,7 @@
|
|||
"requiredPlugins": [
|
||||
"actions",
|
||||
"data",
|
||||
"licensing",
|
||||
"dataViews",
|
||||
"discover",
|
||||
"features",
|
||||
|
|
27
x-pack/plugins/osquery/public/cases/add_to_cases.tsx
Normal file
27
x-pack/plugins/osquery/public/cases/add_to_cases.tsx
Normal 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';
|
|
@ -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 = '',
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -34,7 +34,7 @@ const DIFFERENTIAL_OPTION = {
|
|||
inputDisplay: (
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.queryFlyoutForm.resultsTypeField.differentialValueLabel"
|
||||
defaultMessage="Diffential"
|
||||
defaultMessage="Differential"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
|
49
x-pack/plugins/osquery/public/lens/pack_view_in_lens.tsx
Normal file
49
x-pack/plugins/osquery/public/lens/pack_view_in_lens.tsx
Normal 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);
|
234
x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx
Normal file
234
x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx
Normal 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);
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -155,7 +155,7 @@ const PackFormComponent: React.FC<PackFormProps> = ({
|
|||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<PolicyIdComboBoxField euiFieldProps={euiFieldProps} />
|
||||
<PolicyIdComboBoxField />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule />
|
||||
|
|
|
@ -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 ?? {} });
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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}</>;
|
||||
|
|
29
x-pack/plugins/osquery/public/shared_components/prompts.tsx
Normal file
29
x-pack/plugins/osquery/public/shared_components/prompts.tsx
Normal 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>}
|
||||
/>
|
||||
);
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
|
@ -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 }>
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue