mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Osquery] Add Osquery results to Case (#139909)
This commit is contained in:
parent
77964bc0f7
commit
b744e7cede
27 changed files with 797 additions and 62 deletions
|
@ -18,6 +18,8 @@
|
|||
export {
|
||||
CASES_URL,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
OBSERVABILITY_OWNER,
|
||||
GENERAL_CASES_OWNER,
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
|
@ -25,7 +27,13 @@ export {
|
|||
UPDATE_CASES_CAPABILITY,
|
||||
} from './constants';
|
||||
|
||||
export { CommentType, CaseStatuses, getCasesFromAlertsUrl, throwErrors } from './api';
|
||||
export {
|
||||
CommentType,
|
||||
CaseStatuses,
|
||||
getCasesFromAlertsUrl,
|
||||
throwErrors,
|
||||
ExternalReferenceStorageType,
|
||||
} from './api';
|
||||
|
||||
export type {
|
||||
Case,
|
||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
|||
CasesMetricsRequest,
|
||||
CasesStatusRequest,
|
||||
CommentRequestAlertType,
|
||||
CommentRequestExternalReferenceNoSOType,
|
||||
CommentRequestPersistableStateType,
|
||||
CommentRequestUserType,
|
||||
} from '../common/api';
|
||||
|
@ -152,7 +153,8 @@ export interface CasesUiStart {
|
|||
export type SupportedCaseAttachment =
|
||||
| CommentRequestAlertType
|
||||
| CommentRequestUserType
|
||||
| CommentRequestPersistableStateType;
|
||||
| CommentRequestPersistableStateType
|
||||
| CommentRequestExternalReferenceNoSOType;
|
||||
|
||||
export type CaseAttachments = SupportedCaseAttachment[];
|
||||
export type CaseAttachmentWithoutOwner = DistributiveOmit<SupportedCaseAttachment, 'owner'>;
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"attributes": {
|
||||
"assignees": [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": {
|
||||
"fields": [],
|
||||
"name": "none",
|
||||
"type": ".none"
|
||||
},
|
||||
"created_at": "2022-09-14T07:51:42.298Z",
|
||||
"created_by": {
|
||||
"email": null,
|
||||
"full_name": null,
|
||||
"username": "elastic"
|
||||
},
|
||||
"description": "Test Observability case",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"owner": "observability",
|
||||
"settings": {
|
||||
"syncAlerts": false
|
||||
},
|
||||
"severity": "low",
|
||||
"status": "open",
|
||||
"tags": [
|
||||
"obs"
|
||||
],
|
||||
"title": "Test Obs case",
|
||||
"updated_at": null,
|
||||
"updated_by": null
|
||||
},
|
||||
"coreMigrationVersion": "8.5.0",
|
||||
"id": "127acc90-3402-11ed-a392-0f81f7d06b71",
|
||||
"migrationVersion": {
|
||||
"cases": "8.5.0"
|
||||
},
|
||||
"references": [],
|
||||
"type": "cases",
|
||||
"updated_at": "2022-09-14T07:51:42.299Z",
|
||||
"version": "WzEwMDIwLDFd"
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"attributes": {
|
||||
"assignees": [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": {
|
||||
"fields": [],
|
||||
"name": "none",
|
||||
"type": ".none"
|
||||
},
|
||||
"created_at": "2022-09-14T08:29:25.376Z",
|
||||
"created_by": {
|
||||
"email": null,
|
||||
"full_name": null,
|
||||
"username": "elastic"
|
||||
},
|
||||
"description": "Test security description",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"owner": "securitySolution",
|
||||
"settings": {
|
||||
"syncAlerts": true
|
||||
},
|
||||
"severity": "low",
|
||||
"status": "open",
|
||||
"tags": [
|
||||
"security"
|
||||
],
|
||||
"title": "Test Security Case",
|
||||
"updated_at": null,
|
||||
"updated_by": null
|
||||
},
|
||||
"coreMigrationVersion": "8.5.0",
|
||||
"id": "5760cad0-3407-11ed-b29d-758c6c3e798d",
|
||||
"migrationVersion": {
|
||||
"cases": "8.5.0"
|
||||
},
|
||||
"references": [],
|
||||
"type": "cases",
|
||||
"updated_at": "2022-09-14T08:29:25.376Z",
|
||||
"version": "WzE0OTk0LDFd"
|
||||
}
|
76
x-pack/plugins/osquery/cypress/integration/all/cases.spec.ts
Normal file
76
x-pack/plugins/osquery/cypress/integration/all/cases.spec.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { checkResults } from '../../tasks/live_query';
|
||||
import { navigateTo } from '../../tasks/navigation';
|
||||
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
|
||||
import { login } from '../../tasks/login';
|
||||
import { ROLES } from '../../test';
|
||||
|
||||
describe('Add to Cases', () => {
|
||||
describe('observability', () => {
|
||||
before(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.LOAD, 'case_observability');
|
||||
login(ROLES.soc_manager);
|
||||
navigateTo('/app/osquery');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'case_observability');
|
||||
});
|
||||
it('should add result a case and not have add to timeline in result', () => {
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 1 },
|
||||
}).click();
|
||||
cy.contains('Live query details');
|
||||
cy.contains('Add to Case').click();
|
||||
cy.contains('Select case');
|
||||
cy.contains(/Select$/).click();
|
||||
cy.contains('Test Obs case has been updated');
|
||||
cy.visit('/app/observability/cases');
|
||||
cy.contains('Test Obs case').click();
|
||||
checkResults();
|
||||
cy.contains('attached Osquery results');
|
||||
cy.contains('SELECT * FROM users;');
|
||||
cy.contains('View in Discover').should('exist');
|
||||
cy.contains('View in Lens').should('exist');
|
||||
cy.contains('Add to Case').should('not.exist');
|
||||
cy.contains('Add to timeline investigation').should('not.exist');
|
||||
});
|
||||
});
|
||||
describe('security', () => {
|
||||
before(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.LOAD, 'case_security');
|
||||
login(ROLES.soc_manager);
|
||||
navigateTo('/app/osquery');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'case_security');
|
||||
});
|
||||
|
||||
it('should add result a case and have add to timeline in result', () => {
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 1 },
|
||||
}).click();
|
||||
cy.contains('Live query details');
|
||||
cy.contains('Add to Case').click();
|
||||
cy.contains('Select case');
|
||||
cy.contains(/Select$/).click();
|
||||
cy.contains('Test Security Case has been updated');
|
||||
cy.visit('/app/security/cases');
|
||||
cy.contains('Test Security Case').click();
|
||||
checkResults();
|
||||
cy.contains('attached Osquery results');
|
||||
cy.contains('SELECT * FROM users;');
|
||||
cy.contains('View in Discover').should('exist');
|
||||
cy.contains('View in Lens').should('exist');
|
||||
cy.contains('Add to Case').should('not.exist');
|
||||
cy.contains('Add to timeline investigation').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,8 @@
|
|||
"githubTeam": "security-asset-management"
|
||||
},
|
||||
"kibanaVersion": "kibana",
|
||||
"optionalPlugins": ["fleet", "home", "usageCollection", "lens", "telemetry"],
|
||||
"requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact", "lens"],
|
||||
"optionalPlugins": ["fleet", "home", "usageCollection", "lens", "telemetry", "cases"],
|
||||
"requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact", "lens", "cases"],
|
||||
"requiredPlugins": [
|
||||
"actions",
|
||||
"data",
|
||||
|
|
|
@ -8,20 +8,17 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { filter } from 'lodash';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import type { ESTermQuery } from '../../common/typed_json';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
|
||||
export interface LiveQueryDetailsArgs {
|
||||
actionDetails: Record<string, string>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface UseLiveQueryDetails {
|
||||
actionId?: string;
|
||||
isLive?: boolean;
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
queryIds?: string[];
|
||||
}
|
||||
|
||||
export interface LiveQueryDetailsItem {
|
||||
|
@ -56,12 +53,13 @@ export const useLiveQueryDetails = ({
|
|||
filterQuery,
|
||||
isLive = false,
|
||||
skip = false,
|
||||
queryIds, // enable finding out specific queries only, eg. in cases
|
||||
}: UseLiveQueryDetails) => {
|
||||
const { http } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useQuery<{ data: LiveQueryDetailsItem }, Error, LiveQueryDetailsItem>(
|
||||
['liveQueries', { actionId, filterQuery }],
|
||||
['liveQueries', { actionId, filterQuery, queryIds }],
|
||||
() => http.get(`/api/osquery/live_queries/${actionId}`),
|
||||
{
|
||||
enabled: !skip && !!actionId,
|
||||
|
@ -73,7 +71,17 @@ export const useLiveQueryDetails = ({
|
|||
defaultMessage: 'Error while fetching action details',
|
||||
}),
|
||||
}),
|
||||
select: (response) => response.data,
|
||||
select: (response) => {
|
||||
if (queryIds) {
|
||||
const filteredQueries = filter(response.data.queries, (query) =>
|
||||
queryIds.includes(query.action_id)
|
||||
);
|
||||
|
||||
return { ...response.data, queries: filteredQueries };
|
||||
}
|
||||
|
||||
return response.data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
retryDelay: 5000,
|
||||
}
|
||||
|
|
86
x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx
Normal file
86
x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { CommentType, ExternalReferenceStorageType } from '@kbn/cases-plugin/common';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
const ADD_TO_CASE = i18n.translate(
|
||||
'xpack.osquery.pack.queriesTable.addToCaseResultsActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Add to Case',
|
||||
}
|
||||
);
|
||||
|
||||
interface IProps {
|
||||
queryId: string;
|
||||
agentIds?: string[];
|
||||
actionId: string;
|
||||
isIcon?: boolean;
|
||||
isDisabled?: boolean;
|
||||
iconProps?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const AddToCaseButton: React.FC<IProps> = ({
|
||||
actionId,
|
||||
agentIds = [],
|
||||
queryId = '',
|
||||
isIcon = false,
|
||||
isDisabled,
|
||||
iconProps,
|
||||
}) => {
|
||||
const { cases } = useKibana().services;
|
||||
|
||||
const casePermissions = cases.helpers.canUseCases();
|
||||
const hasCasesPermissions =
|
||||
casePermissions.read && casePermissions.update && casePermissions.push;
|
||||
const selectCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal({});
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
const attachments: CaseAttachmentsWithoutOwner = [
|
||||
{
|
||||
type: CommentType.externalReference,
|
||||
externalReferenceId: actionId,
|
||||
externalReferenceStorage: {
|
||||
type: ExternalReferenceStorageType.elasticSearchDoc,
|
||||
},
|
||||
externalReferenceAttachmentTypeId: 'osquery',
|
||||
externalReferenceMetadata: { actionId, agentIds, queryId },
|
||||
},
|
||||
];
|
||||
if (hasCasesPermissions) {
|
||||
selectCaseModal.open({ attachments });
|
||||
}
|
||||
}, [actionId, agentIds, hasCasesPermissions, queryId, selectCaseModal]);
|
||||
|
||||
if (isIcon) {
|
||||
return (
|
||||
<EuiToolTip content={<EuiFlexItem>{ADD_TO_CASE}</EuiFlexItem>}>
|
||||
<EuiButtonIcon
|
||||
iconType={'casesApp'}
|
||||
onClick={handleClick}
|
||||
isDisabled={isDisabled || !hasCasesPermissions}
|
||||
{...iconProps}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="casesApp"
|
||||
onClick={handleClick}
|
||||
isDisabled={isDisabled || !hasCasesPermissions}
|
||||
>
|
||||
{ADD_TO_CASE}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
};
|
|
@ -21,6 +21,7 @@ import { useForm as useHookForm, FormProvider } from 'react-hook-form';
|
|||
import { isEmpty, map, find, pickBy } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline';
|
||||
import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form';
|
||||
import type {
|
||||
EcsMappingFormField,
|
||||
|
@ -40,6 +41,7 @@ import { LiveQueryQueryField } from './live_query_query_field';
|
|||
import { AgentsTableField } from './agents_table_field';
|
||||
import { PacksComboBoxField } from './packs_combobox_field';
|
||||
import { savedQueryDataSerializer } from '../../saved_queries/form/use_saved_query_form';
|
||||
import { AddToCaseButton } from '../../cases/add_to_cases_button';
|
||||
|
||||
export interface LiveQueryFormFields {
|
||||
query?: string;
|
||||
|
@ -105,7 +107,7 @@ interface LiveQueryFormProps {
|
|||
formType?: FormType;
|
||||
enabled?: boolean;
|
||||
hideAgentsField?: boolean;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement;
|
||||
}
|
||||
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
||||
|
@ -278,6 +280,26 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
);
|
||||
|
||||
const singleQueryDetails = useMemo(() => liveQueryDetails?.queries?.[0], [liveQueryDetails]);
|
||||
const liveQueryActionId = useMemo(() => liveQueryDetails?.action_id, [liveQueryDetails]);
|
||||
|
||||
const addToCaseButton = useCallback(
|
||||
(payload) => {
|
||||
if (liveQueryActionId) {
|
||||
return (
|
||||
<AddToCaseButton
|
||||
queryId={payload.queryId}
|
||||
agentIds={agentIds}
|
||||
actionId={liveQueryActionId}
|
||||
isIcon={payload.isIcon}
|
||||
isDisabled={payload.isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
[agentIds, liveQueryActionId]
|
||||
);
|
||||
|
||||
const resultsStepContent = useMemo(
|
||||
() =>
|
||||
|
@ -288,6 +310,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
endDate={singleQueryDetails?.expiration}
|
||||
agentIds={singleQueryDetails?.agents}
|
||||
addToTimeline={addToTimeline}
|
||||
addToCase={addToCaseButton}
|
||||
/>
|
||||
) : null,
|
||||
[
|
||||
|
@ -296,6 +319,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
singleQueryDetails?.agents,
|
||||
serializedData.ecs_mapping,
|
||||
addToTimeline,
|
||||
addToCaseButton,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -459,7 +483,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
agentIds={agentIds}
|
||||
// @ts-expect-error version string !+ string[]
|
||||
data={liveQueryDetails?.queries ?? selectedPackData?.attributes?.queries}
|
||||
addToCase={addToCaseButton}
|
||||
addToTimeline={addToTimeline}
|
||||
showResultsHeader
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get, map } from 'lodash';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
|
@ -34,6 +34,9 @@ import type {
|
|||
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 { 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';
|
||||
|
@ -43,6 +46,8 @@ 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[] = [];
|
||||
|
||||
const TruncateTooltipText = styled.div`
|
||||
width: 100%;
|
||||
|
||||
|
@ -526,22 +531,39 @@ type PackQueryStatusItem = Partial<{
|
|||
|
||||
interface PackQueriesStatusTableProps {
|
||||
agentIds?: string[];
|
||||
queryId?: string;
|
||||
actionId?: string;
|
||||
data?: PackQueryStatusItem[];
|
||||
startDate?: string;
|
||||
expirationDate?: string;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => ReactElement;
|
||||
addToTimeline?: (payload: AddToTimelinePayload) => ReactElement;
|
||||
addToCase?: ({
|
||||
actionId,
|
||||
isIcon,
|
||||
isDisabled,
|
||||
}: {
|
||||
actionId?: string;
|
||||
isIcon?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}) => ReactElement;
|
||||
showResultsHeader?: boolean;
|
||||
}
|
||||
|
||||
const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = ({
|
||||
actionId,
|
||||
queryId,
|
||||
agentIds,
|
||||
data,
|
||||
startDate,
|
||||
expirationDate,
|
||||
addToTimeline,
|
||||
addToCase,
|
||||
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) => (
|
||||
|
@ -595,7 +617,18 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
);
|
||||
|
||||
const renderLensResultsAction = useCallback((item) => <PackViewInLensAction item={item} />, []);
|
||||
const handleAddToCase = useCallback(
|
||||
(payload: { actionId: string; isIcon?: boolean }) =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
() => {
|
||||
if (addToCase) {
|
||||
return addToCase({ actionId: payload.actionId, isIcon: payload?.isIcon });
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
[addToCase]
|
||||
);
|
||||
const getHandleErrorsToggle = useCallback(
|
||||
(item) => () => {
|
||||
setItemIdToExpandedRowMap((prevValue) => {
|
||||
|
@ -614,6 +647,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
agentIds={agentIds}
|
||||
failedAgentsCount={item?.failed ?? 0}
|
||||
addToTimeline={addToTimeline}
|
||||
addToCase={addToCase && handleAddToCase({ actionId: item.action_id })}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -623,7 +657,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
return itemIdToExpandedRowMapValues;
|
||||
});
|
||||
},
|
||||
[agentIds, expirationDate, startDate, addToTimeline]
|
||||
[startDate, expirationDate, agentIds, addToTimeline, addToCase, handleAddToCase]
|
||||
);
|
||||
|
||||
const renderToggleResultsAction = useCallback(
|
||||
|
@ -642,8 +676,28 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
|
||||
const getItemId = useCallback((item: PackItem) => get(item, 'id'), []);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
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 }),
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
name: i18n.translate('xpack.osquery.pack.queriesTable.idColumnTitle', {
|
||||
|
@ -679,14 +733,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
defaultMessage: 'View results',
|
||||
}),
|
||||
width: '90px',
|
||||
actions: [
|
||||
{
|
||||
render: renderDiscoverResultsAction,
|
||||
},
|
||||
{
|
||||
render: renderLensResultsAction,
|
||||
},
|
||||
],
|
||||
actions: resultActions,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
@ -699,17 +746,20 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
renderIDColumn,
|
||||
renderQueryColumn,
|
||||
renderDocsColumn,
|
||||
renderAgentsColumn,
|
||||
renderDiscoverResultsAction,
|
||||
renderLensResultsAction,
|
||||
renderToggleResultsAction,
|
||||
]
|
||||
);
|
||||
];
|
||||
}, [
|
||||
renderDiscoverResultsAction,
|
||||
renderLensResultsAction,
|
||||
renderIDColumn,
|
||||
renderQueryColumn,
|
||||
renderDocsColumn,
|
||||
renderAgentsColumn,
|
||||
renderToggleResultsAction,
|
||||
addToCase,
|
||||
addToTimeline,
|
||||
timelines,
|
||||
appName,
|
||||
]);
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
|
@ -724,7 +774,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
useEffect(() => {
|
||||
// reset the expanded row map when the data changes
|
||||
setItemIdToExpandedRowMap({});
|
||||
}, [actionId]);
|
||||
}, [queryId, actionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
@ -737,15 +787,30 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
}
|
||||
}, [agentIds?.length, data, getHandleErrorsToggle, itemIdToExpandedRowMap]);
|
||||
|
||||
const queryIds = useMemo(
|
||||
() =>
|
||||
map(data, (query) => ({
|
||||
value: query.action_id || '',
|
||||
field: 'action_id',
|
||||
})),
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledEuiBasicTable
|
||||
items={data ?? EMPTY_ARRAY}
|
||||
itemId={getItemId}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable
|
||||
/>
|
||||
<CasesContext owner={CASES_OWNER} permissions={casePermissions}>
|
||||
{showResultsHeader && (
|
||||
<PackResultsHeader queryIds={queryIds} actionId={actionId} addToCase={addToCase} />
|
||||
)}
|
||||
|
||||
<StyledEuiBasicTable
|
||||
items={data ?? EMPTY_ARRAY}
|
||||
itemId={getItemId}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable
|
||||
/>
|
||||
</CasesContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface PackResultsHeadersProps {
|
||||
actionId?: string;
|
||||
addToCase?: ({
|
||||
isIcon,
|
||||
iconProps,
|
||||
}: {
|
||||
isIcon: boolean;
|
||||
iconProps: Record<string, string>;
|
||||
}) => ReactElement;
|
||||
queryIds: Array<{ value: string; field: string }>;
|
||||
}
|
||||
|
||||
const StyledResultsHeading = styled(EuiFlexItem)`
|
||||
padding-right: 20px;
|
||||
border-right: 2px solid #d3dae6;
|
||||
`;
|
||||
|
||||
const StyledIconsList = styled(EuiFlexItem)`
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
padding-left: 10px;
|
||||
`;
|
||||
|
||||
export const PackResultsHeader = ({ actionId, addToCase }: PackResultsHeadersProps) => (
|
||||
<>
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<StyledResultsHeading grow={false}>
|
||||
<EuiText>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryActionResults.results"
|
||||
defaultMessage="Results"
|
||||
/>
|
||||
</h2>
|
||||
</EuiText>
|
||||
</StyledResultsHeading>
|
||||
<StyledIconsList grow={false}>
|
||||
<span>
|
||||
{actionId &&
|
||||
addToCase &&
|
||||
addToCase({
|
||||
isIcon: true,
|
||||
iconProps: {
|
||||
color: 'text',
|
||||
size: 'xs',
|
||||
iconSize: 'l',
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
</StyledIconsList>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'l'} />
|
||||
</>
|
||||
);
|
|
@ -10,6 +10,7 @@ import { EuiCode, EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui';
|
|||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { AddToTimelinePayload } from '../timelines/get_add_to_timeline';
|
||||
import type { EcsMappingSerialized } from '../packs/queries/ecs_mapping_editor_field';
|
||||
import { LiveQueryForm } from './form';
|
||||
import { useActionResultsPrivileges } from '../action_results/use_action_privileges';
|
||||
|
@ -33,7 +34,7 @@ interface LiveQueryProps {
|
|||
hideAgentsField?: boolean;
|
||||
packId?: string;
|
||||
agentSelection?: AgentSelection;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement;
|
||||
}
|
||||
|
||||
const LiveQueryComponent: React.FC<LiveQueryProps> = ({
|
||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
|||
OsqueryPluginStart,
|
||||
StartPlugins,
|
||||
AppPluginStartDependencies,
|
||||
SetupPlugins,
|
||||
} from './types';
|
||||
import { OSQUERY_INTEGRATION_NAME, PLUGIN_NAME } from '../common';
|
||||
import {
|
||||
|
@ -30,7 +31,9 @@ import {
|
|||
getLazyOsqueryAction,
|
||||
getLazyLiveQueryField,
|
||||
useIsOsqueryAvailableSimple,
|
||||
getExternalReferenceAttachmentRegular,
|
||||
} from './shared_components';
|
||||
import type { ServicesWrapperProps } from './shared_components/services_wrapper';
|
||||
|
||||
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
|
||||
private kibanaVersion: string;
|
||||
|
@ -40,7 +43,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
this.kibanaVersion = this.initializerContext.env.packageInfo.version;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup): OsqueryPluginSetup {
|
||||
public setup(core: CoreSetup, plugins: SetupPlugins): OsqueryPluginSetup {
|
||||
const storage = this.storage;
|
||||
const kibanaVersion = this.kibanaVersion;
|
||||
// Register an application into the side navigation menu
|
||||
|
@ -67,6 +70,17 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
},
|
||||
});
|
||||
|
||||
core.getStartServices().then(([coreStart, depsStart]) => {
|
||||
plugins.cases?.attachmentFramework.registerExternalReference(
|
||||
getExternalReferenceAttachmentRegular({
|
||||
...coreStart,
|
||||
...depsStart,
|
||||
storage,
|
||||
kibanaVersion,
|
||||
} as unknown as ServicesWrapperProps['services'])
|
||||
);
|
||||
});
|
||||
|
||||
// Return methods that should be available to other plugins
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { pagePathGetters } from '@kbn/fleet-plugin/public';
|
||||
import type { AddToTimelinePayload } from '../timelines/get_add_to_timeline';
|
||||
import type { ECSMapping } from '../../common/schemas/common';
|
||||
import { useAllResults } from './use_all_results';
|
||||
import type { ResultEdges } from '../../common/search_strategy';
|
||||
|
@ -52,7 +53,8 @@ interface ResultsTableComponentProps {
|
|||
ecsMapping?: ECSMapping;
|
||||
endDate?: string;
|
||||
startDate?: string;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement;
|
||||
addToCase?: ({ actionId }: { actionId?: string }) => React.ReactElement;
|
||||
}
|
||||
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
||||
|
@ -62,6 +64,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
startDate,
|
||||
endDate,
|
||||
addToTimeline,
|
||||
addToCase,
|
||||
}) => {
|
||||
const [isLive, setIsLive] = useState(true);
|
||||
const { data: hasActionResultsPrivileges } = useActionResultsPrivileges();
|
||||
|
@ -343,10 +346,11 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
startDate={startDate}
|
||||
/>
|
||||
{addToTimeline && addToTimeline({ query: ['action_id', actionId] })}
|
||||
{addToCase && addToCase({ actionId })}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[actionId, addToTimeline, endDate, startDate]
|
||||
[actionId, addToCase, addToTimeline, endDate, startDate]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { AddToCaseButton } from '../../../cases/add_to_cases_button';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useLiveQueryDetails } from '../../../actions/use_live_query_details';
|
||||
|
@ -52,6 +53,19 @@ const LiveQueryDetailsPageComponent = () => {
|
|||
useLayoutEffect(() => {
|
||||
setIsLive(() => !(data?.status === 'completed'));
|
||||
}, [data?.status]);
|
||||
const addToCaseButton = useCallback(
|
||||
(payload) => (
|
||||
<AddToCaseButton
|
||||
queryId={payload.queryId}
|
||||
actionId={actionId}
|
||||
agentIds={data?.agents}
|
||||
isIcon={payload.isIcon}
|
||||
isDisabled={payload.isDisabled}
|
||||
iconProps={payload.iconProps}
|
||||
/>
|
||||
),
|
||||
[data?.agents, actionId]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumnGrow={false}>
|
||||
|
@ -61,6 +75,8 @@ const LiveQueryDetailsPageComponent = () => {
|
|||
startDate={data?.['@timestamp']}
|
||||
expirationDate={data?.expiration}
|
||||
agentIds={data?.agents}
|
||||
addToCase={addToCaseButton}
|
||||
showResultsHeader
|
||||
/>
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
|
|
|
@ -8,11 +8,15 @@
|
|||
import { EuiTabbedContent, EuiNotificationBadge } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
import type { AddToTimelinePayload } from '../../../timelines/get_add_to_timeline';
|
||||
import type { ECSMapping } from '../../../../common/schemas/common';
|
||||
import { ResultsTable } from '../../../results/results_table';
|
||||
import { ActionResultsSummary } from '../../../action_results/action_results_summary';
|
||||
|
||||
const CASES_OWNER: string[] = [];
|
||||
|
||||
interface ResultTabsProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
|
@ -20,7 +24,8 @@ interface ResultTabsProps {
|
|||
ecsMapping?: ECSMapping;
|
||||
failedAgentsCount?: number;
|
||||
endDate?: string;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => ReactElement;
|
||||
addToTimeline?: (payload: AddToTimelinePayload) => ReactElement;
|
||||
addToCase?: ({ actionId }: { actionId?: string }) => ReactElement;
|
||||
}
|
||||
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
||||
|
@ -31,7 +36,12 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
failedAgentsCount,
|
||||
startDate,
|
||||
addToTimeline,
|
||||
addToCase,
|
||||
}) => {
|
||||
const { cases } = useKibana().services;
|
||||
const casePermissions = cases.helpers.canUseCases();
|
||||
const CasesContext = cases.ui.getCasesContext();
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -45,6 +55,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
addToTimeline={addToTimeline}
|
||||
addToCase={addToCase}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
@ -61,18 +72,29 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
) : null,
|
||||
},
|
||||
],
|
||||
[actionId, agentIds, ecsMapping, startDate, endDate, addToTimeline, failedAgentsCount]
|
||||
[
|
||||
actionId,
|
||||
agentIds,
|
||||
ecsMapping,
|
||||
startDate,
|
||||
endDate,
|
||||
addToTimeline,
|
||||
addToCase,
|
||||
failedAgentsCount,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
// TODO: extend the EuiTabbedContent component to support EuiTabs props
|
||||
// bottomBorder={false}
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
expand={false}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 { EuiAvatar } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import type { ExternalReferenceAttachmentType } from '@kbn/cases-plugin/public/client/attachment_framework/types';
|
||||
import { getLazyExternalContent } from './lazy_external_reference_content';
|
||||
import type { ServicesWrapperProps } from '../services_wrapper';
|
||||
import OsqueryLogo from '../../components/osquery_icon/osquery.svg';
|
||||
|
||||
export const getExternalReferenceAttachmentRegular = (
|
||||
services: ServicesWrapperProps['services']
|
||||
): ExternalReferenceAttachmentType => ({
|
||||
id: 'osquery',
|
||||
displayName: 'Osquery',
|
||||
getAttachmentViewObject: () => ({
|
||||
type: 'regular',
|
||||
event: 'attached Osquery results',
|
||||
timelineAvatar: <EuiAvatar name="osquery" color="subdued" iconType={OsqueryLogo} />,
|
||||
// @ts-expect-error update types
|
||||
children: getLazyExternalContent(services),
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import type { IExternalReferenceMetaDataProps } from './lazy_external_reference_content';
|
||||
import { PackQueriesAttachmentWrapper } from './pack_queries_attachment_wrapper';
|
||||
|
||||
const AttachmentContent = (props: IExternalReferenceMetaDataProps) => {
|
||||
const { externalReferenceMetadata } = props;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="osquery-attachment-content">
|
||||
<EuiFlexItem>
|
||||
<PackQueriesAttachmentWrapper
|
||||
actionId={externalReferenceMetadata.actionId}
|
||||
queryId={externalReferenceMetadata.queryId}
|
||||
agentIds={externalReferenceMetadata.agentIds}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { AttachmentContent as default };
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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, { lazy, Suspense } from 'react';
|
||||
import { EuiCode, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { OsqueryIcon } from '../../components/osquery_icon';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import type { ServicesWrapperProps } from '../services_wrapper';
|
||||
import ServicesWrapper from '../services_wrapper';
|
||||
import { PERMISSION_DENIED } from '../osquery_action/translations';
|
||||
|
||||
export interface IExternalReferenceMetaDataProps {
|
||||
externalReferenceMetadata: {
|
||||
actionId: string;
|
||||
agentIds: string[];
|
||||
queryId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const getLazyExternalContent =
|
||||
// eslint-disable-next-line react/display-name
|
||||
(services: ServicesWrapperProps['services']) => (props: IExternalReferenceMetaDataProps) => {
|
||||
const {
|
||||
services: {
|
||||
application: {
|
||||
capabilities: { osquery },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
if (!osquery.read) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<OsqueryIcon />}
|
||||
title={<h2>{PERMISSION_DENIED}</h2>}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.cases.permissionDenied"
|
||||
defaultMessage=" To access these results, ask your administrator for {osquery} Kibana
|
||||
privileges."
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{
|
||||
osquery: <EuiCode>osquery</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const AttachmentContent = lazy(() => import('./external_references_content'));
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ServicesWrapper services={services}>
|
||||
<AttachmentContent {...props} />
|
||||
</ServicesWrapper>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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, useLayoutEffect, useState } from 'react';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { getAddToTimeline } from '../../timelines/get_add_to_timeline';
|
||||
import { PackQueriesStatusTable } from '../../live_queries/form/pack_queries_status_table';
|
||||
import { useLiveQueryDetails } from '../../actions/use_live_query_details';
|
||||
|
||||
interface PackQueriesAttachmentWrapperProps {
|
||||
actionId?: string;
|
||||
agentIds: string[];
|
||||
queryId: string;
|
||||
}
|
||||
|
||||
export const PackQueriesAttachmentWrapper = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
queryId,
|
||||
}: PackQueriesAttachmentWrapperProps) => {
|
||||
const {
|
||||
services: { timelines, appName },
|
||||
} = useKibana();
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
const addToTimelineButton = getAddToTimeline(timelines, appName);
|
||||
|
||||
const { data } = useLiveQueryDetails({
|
||||
actionId,
|
||||
isLive,
|
||||
...(queryId ? { queryIds: [queryId] } : {}),
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setIsLive(() => !(data?.status === 'completed'));
|
||||
}, [data?.status]);
|
||||
|
||||
const addToTimeline = useCallback(
|
||||
(payload) => {
|
||||
if (!actionId || !addToTimelineButton) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return addToTimelineButton(payload);
|
||||
},
|
||||
[actionId, addToTimelineButton]
|
||||
);
|
||||
|
||||
return (
|
||||
<PackQueriesStatusTable
|
||||
actionId={actionId}
|
||||
queryId={queryId}
|
||||
data={data?.queries}
|
||||
startDate={data?.['@timestamp']}
|
||||
expirationDate={data?.expiration}
|
||||
agentIds={agentIds}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -8,3 +8,4 @@
|
|||
export { getLazyOsqueryAction } from './lazy_osquery_action';
|
||||
export { getLazyLiveQueryField } from './lazy_live_query_field';
|
||||
export { useIsOsqueryAvailableSimple } from './osquery_action/use_is_osquery_available_simple';
|
||||
export { getExternalReferenceAttachmentRegular } from './attachments/external_reference';
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline';
|
||||
import {
|
||||
AGENT_STATUS_ERROR,
|
||||
EMPTY_PROMPT,
|
||||
|
@ -25,7 +26,7 @@ export interface OsqueryActionProps {
|
|||
defaultValues?: {};
|
||||
formType: 'steps' | 'simple';
|
||||
hideAgentsField?: boolean;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement;
|
||||
}
|
||||
|
||||
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty } from '@elastic/eui';
|
||||
import type { ServicesWrapperProps } from '../shared_components/services_wrapper';
|
||||
|
||||
const TimelineComponent = React.memo((props) => <EuiButtonEmpty {...props} size="xs" />);
|
||||
TimelineComponent.displayName = 'TimelineComponent';
|
||||
|
||||
export interface AddToTimelinePayload {
|
||||
query: [string, string];
|
||||
isIcon?: true;
|
||||
}
|
||||
|
||||
export const SECURITY_APP_NAME = 'Security';
|
||||
export const getAddToTimeline = (
|
||||
timelines: ServicesWrapperProps['services']['timelines'],
|
||||
appName?: string
|
||||
) => {
|
||||
if (!timelines || appName !== SECURITY_APP_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { getAddToTimelineButton } = timelines.getHoverActions();
|
||||
|
||||
return (payload: AddToTimelinePayload) => {
|
||||
const {
|
||||
query: [field, value],
|
||||
isIcon,
|
||||
} = payload;
|
||||
|
||||
const providerA = {
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: value,
|
||||
kqlQuery: '',
|
||||
name: value,
|
||||
queryMatch: {
|
||||
field,
|
||||
value,
|
||||
operator: ':' as const,
|
||||
},
|
||||
};
|
||||
|
||||
return getAddToTimelineButton({
|
||||
dataProvider: providerA,
|
||||
field: value,
|
||||
ownFocus: false,
|
||||
...(isIcon ? { showTooltip: true } : { Component: TimelineComponent }),
|
||||
});
|
||||
};
|
||||
};
|
|
@ -16,10 +16,13 @@ import type {
|
|||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { CasesUiStart, CasesUiSetup } from '@kbn/cases-plugin/public';
|
||||
import type { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
||||
import type { getLazyLiveQueryField, getLazyOsqueryAction } from './shared_components';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface OsqueryPluginSetup {}
|
||||
|
||||
export interface OsqueryPluginStart {
|
||||
OsqueryAction?: ReturnType<typeof getLazyOsqueryAction>;
|
||||
LiveQueryField?: ReturnType<typeof getLazyLiveQueryField>;
|
||||
|
@ -37,10 +40,14 @@ export interface StartPlugins {
|
|||
lens?: LensPublicStart;
|
||||
security: SecurityPluginStart;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
cases: CasesUiStart;
|
||||
timelines: TimelinesUIStart;
|
||||
appName?: string;
|
||||
}
|
||||
|
||||
export interface SetupPlugins {
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
|
||||
cases?: CasesUiSetup;
|
||||
}
|
||||
|
||||
export type StartServices = CoreStart & StartPlugins;
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
"feature": {
|
||||
"discover": ["read"],
|
||||
"infrastructure": ["read"],
|
||||
"observabilityCases": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"ml": ["all"],
|
||||
"siem": ["all"],
|
||||
"osquery": ["all"],
|
||||
|
|
|
@ -34,7 +34,7 @@ export const registerFeatures = (features: SetupPlugins['features']) => {
|
|||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['write'],
|
||||
ui: ['read', 'write'],
|
||||
},
|
||||
read: {
|
||||
api: [`${PLUGIN_ID}-read`],
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
// optionalPlugins from ./kibana.json
|
||||
{ "path": "../../../src/plugins/home/tsconfig.json" },
|
||||
{ "path": "../cases/tsconfig.json" },
|
||||
|
||||
// requiredBundles from ./kibana.json
|
||||
{ "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue