mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Osquery] Substitute Event Data in place of {{parameter}} in Osquery run from Alerts (#146598)
This commit is contained in:
parent
ee3869b0cd
commit
ed0e6b2acb
53 changed files with 1401 additions and 595 deletions
|
@ -99,6 +99,8 @@ export const arrayQueries = t.array(
|
|||
ecs_mapping: ecsMappingOrUndefined,
|
||||
version: versionOrUndefined,
|
||||
platform: platformOrUndefined,
|
||||
removed: removedOrUndefined,
|
||||
snapshot: snapshotOrUndefined,
|
||||
})
|
||||
);
|
||||
export type ArrayQueries = t.TypeOf<typeof arrayQueries>;
|
||||
|
@ -111,10 +113,12 @@ export const objectQueries = t.record(
|
|||
version: versionOrUndefined,
|
||||
platform: platformOrUndefined,
|
||||
saved_query_id: savedQueryIdOrUndefined,
|
||||
removed: removedOrUndefined,
|
||||
snapshot: snapshotOrUndefined,
|
||||
})
|
||||
);
|
||||
export type ObjectQueries = t.TypeOf<typeof objectQueries>;
|
||||
export const queries = t.union([arrayQueries, objectQueries]);
|
||||
export type Queries = t.TypeOf<typeof queries>;
|
||||
export const queriesOrUndefined = t.union([queries, t.undefined]);
|
||||
export const queriesOrUndefined = t.union([arrayQueries, t.undefined]); // in the future we might need to support `objectQueries` so use `queries` instead of `arrayQueries` - now removing this because of strange type issue where query is a number
|
||||
export type QueriesOrUndefined = t.TypeOf<typeof queriesOrUndefined>;
|
||||
|
|
|
@ -10,7 +10,7 @@ export interface CodeSignature {
|
|||
trusted: string[];
|
||||
}
|
||||
export interface Ext {
|
||||
code_signature: CodeSignature[] | CodeSignature;
|
||||
code_signature?: CodeSignature[] | CodeSignature;
|
||||
}
|
||||
export interface Hash {
|
||||
sha256: string[];
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface RuleEcs {
|
|||
id?: string[];
|
||||
rule_id?: string[];
|
||||
name?: string[];
|
||||
false_positives: string[];
|
||||
false_positives?: string[];
|
||||
saved_id?: string[];
|
||||
timeline_id?: string[];
|
||||
timeline_title?: string[];
|
||||
|
@ -27,10 +27,7 @@ export interface RuleEcs {
|
|||
severity?: string[];
|
||||
tags?: string[];
|
||||
threat?: unknown;
|
||||
threshold?: {
|
||||
field: string | string[];
|
||||
value: number;
|
||||
};
|
||||
threshold?: unknown;
|
||||
type?: string[];
|
||||
size?: string[];
|
||||
to?: string[];
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
savedQueryIdOrUndefined,
|
||||
packIdOrUndefined,
|
||||
queryOrUndefined,
|
||||
queriesOrUndefined,
|
||||
arrayQueries,
|
||||
} from '@kbn/osquery-io-ts-types';
|
||||
|
||||
export const createLiveQueryRequestBodySchema = t.partial({
|
||||
|
@ -21,7 +21,7 @@ export const createLiveQueryRequestBodySchema = t.partial({
|
|||
agent_platforms: t.array(t.string),
|
||||
agent_policy_ids: t.array(t.string),
|
||||
query: queryOrUndefined,
|
||||
queries: queriesOrUndefined,
|
||||
queries: arrayQueries,
|
||||
saved_query_id: savedQueryIdOrUndefined,
|
||||
ecs_mapping: ecsMappingOrUndefined,
|
||||
pack_id: packIdOrUndefined,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { replaceParamsQuery } from './replace_params_query';
|
||||
|
||||
describe('replaceParamsQuery', () => {
|
||||
it('should return unchanged query, and skipped true', () => {
|
||||
const query = 'SELECT * FROM processes WHERE version = {{params.version}}';
|
||||
const { result, skipped } = replaceParamsQuery(query, {});
|
||||
expect(result).toBe(query);
|
||||
expect(skipped).toBe(true);
|
||||
});
|
||||
it('should return proper value instead of params if field is found', () => {
|
||||
const query = 'SELECT * FROM processes WHERE version = {{kibana.version}}';
|
||||
const { result, skipped } = replaceParamsQuery(query, { kibana: { version: '8.7.0' } });
|
||||
const expectedQuery = 'SELECT * FROM processes WHERE version = 8.7.0';
|
||||
expect(result).toBe(expectedQuery);
|
||||
expect(skipped).toBe(false);
|
||||
});
|
||||
it('should return proper value instead of params with multiple params', () => {
|
||||
const query =
|
||||
'SELECT * FROM processes WHERE version = {{kibana.version}} and pid = {{kibana.pid}}';
|
||||
const { result, skipped } = replaceParamsQuery(query, {
|
||||
kibana: { version: '8.7.0', pid: '123' },
|
||||
});
|
||||
const expectedQuery = 'SELECT * FROM processes WHERE version = 8.7.0 and pid = 123';
|
||||
expect(result).toBe(expectedQuery);
|
||||
expect(skipped).toBe(false);
|
||||
});
|
||||
it('should return proper value if param has white spaces inside', () => {
|
||||
const query = 'SELECT * FROM processes WHERE version = {{ kibana.version }}';
|
||||
const { result, skipped } = replaceParamsQuery(query, { kibana: { version: '8.7.0' } });
|
||||
const expectedQuery = 'SELECT * FROM processes WHERE version = 8.7.0';
|
||||
expect(result).toBe(expectedQuery);
|
||||
expect(skipped).toBe(false);
|
||||
});
|
||||
|
||||
it('should not change query if there are no opening curly braces but still skipped false', () => {
|
||||
const query = 'SELECT * FROM processes WHERE version = kibana.version }}';
|
||||
const { result, skipped } = replaceParamsQuery(query, { kibana: { version: '8.7.0' } });
|
||||
const expectedQuery = 'SELECT * FROM processes WHERE version = kibana.version }}';
|
||||
expect(result).toBe(expectedQuery);
|
||||
expect(skipped).toBe(false);
|
||||
});
|
||||
it('should return skipped true if {{params}} field not found', () => {
|
||||
const query =
|
||||
'SELECT * FROM processes WHERE version = {{kibana.version}} {{not.existing}} {{agent.name}}';
|
||||
const { result, skipped } = replaceParamsQuery(query, {
|
||||
kibana: { version: '8.7.0' },
|
||||
agent: { name: 'testAgent' },
|
||||
});
|
||||
const expectedQuery =
|
||||
'SELECT * FROM processes WHERE version = 8.7.0 {{not.existing}} testAgent';
|
||||
expect(result).toBe(expectedQuery);
|
||||
expect(skipped).toBe(true);
|
||||
});
|
||||
it('should return replaced values even if params are duplicated, but also return skip true', () => {
|
||||
const query =
|
||||
'SELECT * FROM processes WHERE version = {{ kibana.version}} {{not.existing }} {{kibana.version}} {{kibana.version}} {{agent.name}}';
|
||||
const { result, skipped } = replaceParamsQuery(query, {
|
||||
kibana: { version: '8.7.0' },
|
||||
agent: { name: 'testAgent' },
|
||||
});
|
||||
const expectedQuery =
|
||||
'SELECT * FROM processes WHERE version = 8.7.0 {{not.existing }} 8.7.0 8.7.0 testAgent';
|
||||
expect(result).toBe(expectedQuery);
|
||||
expect(skipped).toBe(true);
|
||||
});
|
||||
|
||||
it('handle complex windows query with registry as param', () => {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const query = `select * FROM registry WHERE key LIKE 'HKEY_USERS\{{user.id}}\Software\Microsoft\IdentityCRL\Immersive\production\Token\{0CB4A94A-6E8C-477B-88C8-A3799FC97414}'`;
|
||||
const { result, skipped } = replaceParamsQuery(query, {
|
||||
user: { id: 'S-1-5-20' },
|
||||
});
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const expectedQuery = `select * FROM registry WHERE key LIKE 'HKEY_USERS\S-1-5-20\Software\Microsoft\IdentityCRL\Immersive\production\Token\{0CB4A94A-6E8C-477B-88C8-A3799FC97414}'`;
|
||||
expect(result).toBe(expectedQuery);
|
||||
expect(skipped).toBe(false);
|
||||
});
|
||||
});
|
33
x-pack/plugins/osquery/common/utils/replace_params_query.ts
Normal file
33
x-pack/plugins/osquery/common/utils/replace_params_query.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { each, get } from 'lodash';
|
||||
|
||||
export const replaceParamsQuery = (query: string, data: object) => {
|
||||
const regex = /\{{([^}]+)\}}/g; // when there are 2 opening and 2 closing curly brackets (including brackets)
|
||||
const matchedBrackets = query.match(regex);
|
||||
let resultQuery = query;
|
||||
|
||||
if (matchedBrackets) {
|
||||
each(matchedBrackets, (bracesText: string) => {
|
||||
const field = bracesText.replace(/{{|}}/g, '').trim();
|
||||
if (resultQuery.includes(bracesText)) {
|
||||
const foundFieldValue = get(data, field);
|
||||
if (foundFieldValue) {
|
||||
resultQuery = resultQuery.replace(bracesText, foundFieldValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const skipped = regex.test(resultQuery);
|
||||
|
||||
return {
|
||||
result: resultQuery,
|
||||
skipped,
|
||||
};
|
||||
};
|
|
@ -10,6 +10,7 @@ import {
|
|||
RESPONSE_ACTIONS_ITEM_1,
|
||||
RESPONSE_ACTIONS_ITEM_2,
|
||||
OSQUERY_RESPONSE_ACTION_ADD_BUTTON,
|
||||
RESPONSE_ACTIONS_ITEM_3,
|
||||
} from '../../tasks/response_actions';
|
||||
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
|
||||
import { login } from '../../tasks/login';
|
||||
|
@ -196,6 +197,40 @@ describe('Alert Event Details', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should be able to add investigation guides to response actions', () => {
|
||||
const investigationGuideNote =
|
||||
'It seems that you have suggested queries in investigation guide, would you like to add them as response actions?';
|
||||
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.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
|
||||
cy.contains('Example');
|
||||
});
|
||||
cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => {
|
||||
cy.contains('select * from uptime');
|
||||
});
|
||||
cy.getBySel(RESPONSE_ACTIONS_ITEM_2).should('not.exist');
|
||||
cy.getBySel(RESPONSE_ACTIONS_ITEM_3).should('not.exist');
|
||||
cy.contains(investigationGuideNote);
|
||||
cy.getBySel('osqueryAddInvestigationGuideQueries').click();
|
||||
cy.contains(investigationGuideNote).should('not.exist');
|
||||
|
||||
cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
|
||||
cy.contains('Example');
|
||||
});
|
||||
cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => {
|
||||
cy.contains('select * from uptime');
|
||||
});
|
||||
cy.getBySel(RESPONSE_ACTIONS_ITEM_2).within(() => {
|
||||
cy.contains('SELECT * FROM processes;');
|
||||
});
|
||||
cy.getBySel(RESPONSE_ACTIONS_ITEM_3).within(() => {
|
||||
cy.contains('select * from users');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
export const RESPONSE_ACTIONS_ITEM_0 = 'response-actions-list-item-0';
|
||||
export const RESPONSE_ACTIONS_ITEM_1 = 'response-actions-list-item-1';
|
||||
export const RESPONSE_ACTIONS_ITEM_2 = 'response-actions-list-item-2';
|
||||
export const RESPONSE_ACTIONS_ITEM_3 = 'response-actions-list-item-3';
|
||||
|
||||
export const OSQUERY_RESPONSE_ACTION_ADD_BUTTON = 'osquery-response-action-type-selection-option';
|
||||
|
|
|
@ -18,6 +18,7 @@ interface ActionResultsSummaryProps {
|
|||
actionId: string;
|
||||
expirationDate?: string;
|
||||
agentIds?: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const renderErrorMessage = (error: string) => (
|
||||
|
@ -30,6 +31,7 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
actionId,
|
||||
expirationDate,
|
||||
agentIds,
|
||||
error,
|
||||
}) => {
|
||||
const [pageIndex] = useState(0);
|
||||
const [pageSize] = useState(50);
|
||||
|
@ -52,22 +54,42 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
isLive,
|
||||
skip: !hasActionResultsPrivileges,
|
||||
});
|
||||
if (expired) {
|
||||
edges.forEach((edge) => {
|
||||
if (!edge.fields?.completed_at && edge.fields) {
|
||||
edge.fields['error.keyword'] = edge.fields.error = [
|
||||
i18n.translate('xpack.osquery.liveQueryActionResults.table.expiredErrorText', {
|
||||
defaultMessage: 'The action request timed out.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
edges.forEach((edge) => {
|
||||
if (edge.fields) {
|
||||
edge.fields['error.skipped'] = edge.fields.error = [
|
||||
i18n.translate('xpack.osquery.liveQueryActionResults.table.skippedErrorText', {
|
||||
defaultMessage:
|
||||
"This query hasn't been called due to parameter used and its value not found in the alert.",
|
||||
}),
|
||||
];
|
||||
}
|
||||
});
|
||||
} else if (expired) {
|
||||
edges.forEach((edge) => {
|
||||
if (!edge.fields?.completed_at && edge.fields) {
|
||||
edge.fields['error.keyword'] = edge.fields.error = [
|
||||
i18n.translate('xpack.osquery.liveQueryActionResults.table.expiredErrorText', {
|
||||
defaultMessage: 'The action request timed out.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [edges, error, expired]);
|
||||
|
||||
const renderAgentIdColumn = useCallback((agentId) => <AgentIdToName agentId={agentId} />, []);
|
||||
const renderRowsColumn = useCallback((rowsCount) => rowsCount ?? '-', []);
|
||||
const renderStatusColumn = useCallback(
|
||||
(_, item) => {
|
||||
if (item.fields['error.skipped']) {
|
||||
return i18n.translate('xpack.osquery.liveQueryActionResults.table.skippedStatusText', {
|
||||
defaultMessage: 'skipped',
|
||||
});
|
||||
}
|
||||
|
||||
if (!item.fields.completed_at) {
|
||||
return expired
|
||||
? i18n.translate('xpack.osquery.liveQueryActionResults.table.expiredStatusText', {
|
||||
|
@ -139,11 +161,11 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
if (!agentIds?.length || expired || error) return false;
|
||||
|
||||
return !!(aggregations.totalResponded !== agentIds?.length);
|
||||
});
|
||||
}, [agentIds?.length, aggregations.totalResponded, expired]);
|
||||
}, [agentIds?.length, aggregations.totalResponded, error, expired]);
|
||||
|
||||
return edges.length ? (
|
||||
<EuiInMemoryTable loading={isLive} items={edges} columns={columns} pagination={pagination} />
|
||||
|
|
|
@ -10,33 +10,42 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import type { ActionEdges, ActionsStrategyResponse, Direction } from '../../common/search_strategy';
|
||||
import type { ActionEdges, ActionsStrategyResponse } from '../../common/search_strategy';
|
||||
import type { ESTermQuery, ESExistsQuery } from '../../common/typed_json';
|
||||
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
import { Direction } from '../../common/search_strategy';
|
||||
|
||||
interface UseAllLiveQueries {
|
||||
export interface UseAllLiveQueriesConfig {
|
||||
activePage: number;
|
||||
direction: Direction;
|
||||
direction?: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
filterQuery?: ESTermQuery | ESExistsQuery | string;
|
||||
skip?: boolean;
|
||||
alertId?: string;
|
||||
}
|
||||
|
||||
// Make sure we keep this and ACTIONS_QUERY_KEY in osquery_flyout.tsx in sync.
|
||||
const ACTIONS_QUERY_KEY = 'actions';
|
||||
|
||||
export const useAllLiveQueries = ({
|
||||
activePage,
|
||||
direction,
|
||||
direction = Direction.desc,
|
||||
limit,
|
||||
sortField,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllLiveQueries) => {
|
||||
alertId,
|
||||
}: UseAllLiveQueriesConfig) => {
|
||||
const { http } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useQuery(
|
||||
['actions', { activePage, direction, limit, sortField }],
|
||||
[
|
||||
ACTIONS_QUERY_KEY,
|
||||
{ activePage, direction, limit, sortField, ...(alertId ? { alertId } : {}) },
|
||||
],
|
||||
() =>
|
||||
http.get<{ data: Omit<ActionsStrategyResponse, 'edges'> & { items: ActionEdges } }>(
|
||||
'/api/osquery/live_queries',
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Ecs } from '../../common/ecs';
|
||||
|
||||
export interface AlertEcsData {
|
||||
_id: string;
|
||||
_index?: string;
|
||||
}
|
||||
|
||||
export const AlertAttachmentContext = React.createContext<AlertEcsData | null>(null);
|
||||
export const AlertAttachmentContext = React.createContext<Ecs | null>(null);
|
||||
|
|
|
@ -86,7 +86,7 @@ const DocsColumnResults: React.FC<DocsColumnResultsProps> = ({ count, isLive })
|
|||
<EuiFlexItem grow={false}>
|
||||
{count ? <EuiNotificationBadge color="subdued">{count}</EuiNotificationBadge> : '-'}
|
||||
</EuiFlexItem>
|
||||
{isLive ? (
|
||||
{!isLive ? (
|
||||
<EuiFlexItem grow={false} data-test-subj={'live-query-loading'}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
|
@ -130,6 +130,7 @@ type PackQueryStatusItem = Partial<{
|
|||
status?: string;
|
||||
pending?: number;
|
||||
docs?: number;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
interface PackQueriesStatusTableProps {
|
||||
|
@ -192,15 +193,12 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
[handleQueryFlyoutOpen]
|
||||
);
|
||||
|
||||
const renderDocsColumn = useCallback(
|
||||
(item: PackQueryStatusItem) => (
|
||||
<DocsColumnResults
|
||||
count={item?.docs ?? 0}
|
||||
isLive={item?.status === 'running' && item?.pending !== 0}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
const renderDocsColumn = useCallback((item: PackQueryStatusItem) => {
|
||||
const isLive =
|
||||
!item?.status || !!item.error || (item?.status !== 'running' && item?.pending === 0);
|
||||
|
||||
return <DocsColumnResults count={item?.docs ?? 0} isLive={isLive} />;
|
||||
}, []);
|
||||
|
||||
const renderAgentsColumn = useCallback((item) => {
|
||||
if (!item.action_id) return;
|
||||
|
@ -239,6 +237,7 @@ const PackQueriesStatusTableComponent: React.FC<PackQueriesStatusTableProps> = (
|
|||
endDate={expirationDate}
|
||||
agentIds={agentIds}
|
||||
failedAgentsCount={item?.failed ?? 0}
|
||||
error={item.error}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -12,22 +12,29 @@ import styled from 'styled-components';
|
|||
import { useController } from 'react-hook-form';
|
||||
|
||||
const StyledEuiCard = styled(EuiCard)`
|
||||
padding: 16px 92px 16px 16px !important;
|
||||
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: ${(props) => {
|
||||
if (props.selectable?.isSelected) {
|
||||
return `1px solid ${props.theme.eui.euiColorSuccess}`;
|
||||
}
|
||||
}};
|
||||
.euiCard__content {
|
||||
padding: 16px 92px 16px 16px !important;
|
||||
}
|
||||
.euiTitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.euiText {
|
||||
margin-top: 0;
|
||||
color: ${(props) => props.theme.eui.euiTextSubduedColor};
|
||||
}
|
||||
|
||||
> button[role='switch'] {
|
||||
left: auto;
|
||||
min-inline-size: 80px;
|
||||
height: 100% !important;
|
||||
width: 80px;
|
||||
right: 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
|
||||
> span {
|
||||
|
@ -43,12 +50,7 @@ const StyledEuiCard = styled(EuiCard)`
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
button[aria-checked='false'] > span > svg {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
interface QueryPackSelectableProps {
|
||||
canRunSingleQuery: boolean;
|
||||
canRunPacks: boolean;
|
||||
|
@ -79,6 +81,7 @@ export const QueryPackSelectable = ({
|
|||
onClick: () => handleChange('query'),
|
||||
isSelected: queryType === 'query',
|
||||
iconType: 'check',
|
||||
textProps: {}, // this is needed for the text to get wrapped in span
|
||||
}),
|
||||
[queryType, handleChange]
|
||||
);
|
||||
|
@ -88,6 +91,7 @@ export const QueryPackSelectable = ({
|
|||
onClick: () => handleChange('pack'),
|
||||
isSelected: queryType === 'pack',
|
||||
iconType: 'check',
|
||||
textProps: {}, // this is needed for the text to get wrapped in span
|
||||
}),
|
||||
[queryType, handleChange]
|
||||
);
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
|
||||
import { castArray, isEmpty, pickBy } from 'lodash';
|
||||
import { EuiCode, EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
|
||||
import { replaceParamsQuery } from '../../common/utils/replace_params_query';
|
||||
import { AlertAttachmentContext } from '../common/contexts';
|
||||
import { LiveQueryForm } from './form';
|
||||
import { useActionResultsPrivileges } from '../action_results/use_action_privileges';
|
||||
import { OSQUERY_INTEGRATION_NAME } from '../../common';
|
||||
|
@ -72,19 +74,30 @@ const LiveQueryComponent: React.FC<LiveQueryProps> = ({
|
|||
|
||||
return null;
|
||||
}, [agentId, agentIds, agentPolicyIds, agentSelection]);
|
||||
const ecsData = useContext(AlertAttachmentContext);
|
||||
|
||||
const initialQuery = useMemo(() => {
|
||||
if (ecsData && query) {
|
||||
const { result } = replaceParamsQuery(query, ecsData);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return query;
|
||||
}, [ecsData, query]);
|
||||
|
||||
const defaultValue = useMemo(() => {
|
||||
const initialValue = {
|
||||
...(initialAgentSelection ? { agentSelection: initialAgentSelection } : {}),
|
||||
alertIds,
|
||||
query,
|
||||
query: initialQuery,
|
||||
savedQueryId,
|
||||
ecs_mapping,
|
||||
packId,
|
||||
};
|
||||
|
||||
return !isEmpty(pickBy(initialValue, (value) => !isEmpty(value))) ? initialValue : undefined;
|
||||
}, [alertIds, ecs_mapping, initialAgentSelection, packId, query, savedQueryId]);
|
||||
}, [alertIds, ecs_mapping, initialAgentSelection, initialQuery, packId, savedQueryId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <EuiLoadingContent lines={10} />;
|
||||
|
|
|
@ -23,6 +23,10 @@ const StyledEuiCard = styled(EuiCard)`
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.euiSpacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.euiText {
|
||||
margin-top: 0;
|
||||
margin-left: 25px;
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
} from '@kbn/core/public';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { useAllLiveQueries } from './actions/use_all_live_queries';
|
||||
import { getLazyOsqueryResponseActionTypeForm } from './shared_components/lazy_osquery_action_params_form';
|
||||
import { useFetchStatus } from './fleet_integration/use_fetch_status';
|
||||
import { getLazyOsqueryResults } from './shared_components/lazy_osquery_results';
|
||||
|
@ -128,6 +129,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
kibanaVersion: this.kibanaVersion,
|
||||
}),
|
||||
OsqueryResponseActionTypeForm: getLazyOsqueryResponseActionTypeForm(),
|
||||
fetchAllLiveQueries: useAllLiveQueries,
|
||||
fetchInstallationStatus: useFetchStatus,
|
||||
isOsqueryAvailable: useIsOsqueryAvailableSimple,
|
||||
};
|
||||
|
|
|
@ -61,6 +61,7 @@ export interface ResultsTableComponentProps {
|
|||
endDate?: string;
|
||||
startDate?: string;
|
||||
liveQueryActionId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
||||
|
@ -70,6 +71,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
startDate,
|
||||
endDate,
|
||||
liveQueryActionId,
|
||||
error,
|
||||
}) => {
|
||||
const [isLive, setIsLive] = useState(true);
|
||||
const { data: hasActionResultsPrivileges } = useActionResultsPrivileges();
|
||||
|
@ -367,7 +369,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
useEffect(
|
||||
() =>
|
||||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
if (!agentIds?.length || expired || error) return false;
|
||||
|
||||
return !!(
|
||||
aggregations.totalResponded !== agentIds?.length ||
|
||||
|
@ -381,6 +383,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
aggregations?.totalRowCount,
|
||||
allResultsData?.edges.length,
|
||||
allResultsData?.total,
|
||||
error,
|
||||
expired,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ interface ResultTabsProps {
|
|||
failedAgentsCount?: number;
|
||||
endDate?: string;
|
||||
liveQueryActionId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
||||
|
@ -37,6 +38,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
failedAgentsCount,
|
||||
startDate,
|
||||
liveQueryActionId,
|
||||
error,
|
||||
}) => {
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
|
@ -52,6 +54,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
liveQueryActionId={liveQueryActionId}
|
||||
error={error}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
@ -60,7 +63,12 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
name: 'Status',
|
||||
'data-test-subj': 'osquery-status-tab',
|
||||
content: (
|
||||
<ActionResultsSummary actionId={actionId} agentIds={agentIds} expirationDate={endDate} />
|
||||
<ActionResultsSummary
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
expirationDate={endDate}
|
||||
error={error}
|
||||
/>
|
||||
),
|
||||
append: failedAgentsCount ? (
|
||||
<EuiNotificationBadge className="eui-alignCenter" size="m">
|
||||
|
@ -69,7 +77,16 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
) : null,
|
||||
},
|
||||
],
|
||||
[actionId, agentIds, ecsMapping, startDate, endDate, liveQueryActionId, failedAgentsCount]
|
||||
[
|
||||
actionId,
|
||||
agentIds,
|
||||
ecsMapping,
|
||||
startDate,
|
||||
endDate,
|
||||
liveQueryActionId,
|
||||
error,
|
||||
failedAgentsCount,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,6 +12,8 @@ import type { LiveQueryQueryFieldProps } from '../live_queries/form/live_query_q
|
|||
import type { ServicesWrapperProps } from './services_wrapper';
|
||||
import ServicesWrapper from './services_wrapper';
|
||||
|
||||
const LiveQueryField = lazy(() => import('../live_queries/form/live_query_query_field'));
|
||||
|
||||
export const getLazyLiveQueryField =
|
||||
(services: ServicesWrapperProps['services']) =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
|
@ -24,10 +26,8 @@ export const getLazyLiveQueryField =
|
|||
query: string;
|
||||
ecs_mapping: Record<string, unknown>;
|
||||
}>;
|
||||
}) => {
|
||||
const LiveQueryField = lazy(() => import('../live_queries/form/live_query_query_field'));
|
||||
|
||||
return (
|
||||
}) =>
|
||||
(
|
||||
<Suspense fallback={null}>
|
||||
<ServicesWrapper services={services}>
|
||||
<FormProvider {...formMethods}>
|
||||
|
@ -36,4 +36,3 @@ export const getLazyLiveQueryField =
|
|||
</ServicesWrapper>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,18 +6,17 @@
|
|||
*/
|
||||
|
||||
import React, { lazy, Suspense, useMemo } from 'react';
|
||||
import type { Ecs } from '../../common/ecs';
|
||||
import ServicesWrapper from './services_wrapper';
|
||||
import type { ServicesWrapperProps } from './services_wrapper';
|
||||
import type { OsqueryActionProps } from './osquery_action';
|
||||
import type { AlertEcsData } from '../common/contexts';
|
||||
import { AlertAttachmentContext } from '../common/contexts';
|
||||
|
||||
const OsqueryAction = lazy(() => import('./osquery_action'));
|
||||
export const getLazyOsqueryAction =
|
||||
(services: ServicesWrapperProps['services']) =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
(props: OsqueryActionProps & { ecsData?: AlertEcsData }) => {
|
||||
const OsqueryAction = lazy(() => import('./osquery_action'));
|
||||
|
||||
(props: OsqueryActionProps & { ecsData?: Ecs }) => {
|
||||
const { ecsData, ...restProps } = props;
|
||||
const renderAction = useMemo(() => {
|
||||
if (ecsData && ecsData?._id) {
|
||||
|
@ -29,7 +28,7 @@ export const getLazyOsqueryAction =
|
|||
}
|
||||
|
||||
return <OsqueryAction {...restProps} />;
|
||||
}, [OsqueryAction, ecsData, restProps]);
|
||||
}, [ecsData, restProps]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
|
|
|
@ -14,14 +14,13 @@ interface BigServices extends StartServices {
|
|||
storage: unknown;
|
||||
}
|
||||
|
||||
const OsqueryResults = lazy(() => import('./osquery_results'));
|
||||
|
||||
export const getLazyOsqueryResults =
|
||||
// eslint-disable-next-line react/display-name
|
||||
(services: BigServices) => (props: OsqueryActionResultsProps) => {
|
||||
const OsqueryResults = lazy(() => import('./osquery_results'));
|
||||
|
||||
return (
|
||||
(services: BigServices) => (props: OsqueryActionResultsProps) =>
|
||||
(
|
||||
<Suspense fallback={null}>
|
||||
<OsqueryResults services={services} {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface OsqueryActionProps {
|
|||
defaultValues?: {};
|
||||
formType: 'steps' | 'simple';
|
||||
hideAgentsField?: boolean;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
||||
|
@ -28,6 +29,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
|||
formType = 'simple',
|
||||
defaultValues,
|
||||
hideAgentsField,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const permissions = useKibana().services.application.capabilities.osquery;
|
||||
|
||||
|
@ -91,6 +93,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
|
|||
formType={formType}
|
||||
agentId={agentId}
|
||||
hideAgentsField={hideAgentsField}
|
||||
onSuccess={onSuccess}
|
||||
{...defaultValues}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import uuid from 'uuid';
|
||||
import type { FieldErrors } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { useForm as useHookForm, FormProvider } from 'react-hook-form';
|
||||
|
@ -35,7 +34,6 @@ interface OsqueryResponseActionsValues {
|
|||
|
||||
interface OsqueryResponseActionsParamsFormFields {
|
||||
savedQueryId: string | null;
|
||||
id: string;
|
||||
ecs_mapping: ECSMapping;
|
||||
query: string;
|
||||
packId?: string[];
|
||||
|
@ -58,7 +56,6 @@ const OsqueryResponseActionParamsFormComponent = ({
|
|||
onError,
|
||||
onChange,
|
||||
}: OsqueryResponseActionsParamsFormProps) => {
|
||||
const uniqueId = useMemo(() => uuid.v4(), []);
|
||||
const hooksForm = useHookForm<OsqueryResponseActionsParamsFormFields>({
|
||||
mode: 'all',
|
||||
defaultValues: defaultValues
|
||||
|
@ -70,14 +67,13 @@ const OsqueryResponseActionParamsFormComponent = ({
|
|||
}
|
||||
: {
|
||||
ecs_mapping: {},
|
||||
id: uniqueId,
|
||||
queryType: 'query',
|
||||
},
|
||||
});
|
||||
|
||||
const { watch, register, formState, control } = hooksForm;
|
||||
|
||||
const [packId, queryType, queries, id] = watch(['packId', 'queryType', 'queries', 'id']);
|
||||
const [packId, queryType, queries] = watch(['packId', 'queryType', 'queries']);
|
||||
const { data: packData } = usePack({
|
||||
packId: packId?.[0],
|
||||
skip: !packId?.[0],
|
||||
|
@ -105,7 +101,6 @@ const OsqueryResponseActionParamsFormComponent = ({
|
|||
|
||||
useEffect(() => {
|
||||
register('savedQueryId');
|
||||
register('id');
|
||||
}, [register]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -114,12 +109,10 @@ const OsqueryResponseActionParamsFormComponent = ({
|
|||
// @ts-expect-error update types
|
||||
formData.queryType === 'pack'
|
||||
? {
|
||||
id: formData.id,
|
||||
packId: formData?.packId?.length ? formData?.packId[0] : undefined,
|
||||
queries: formData.queries,
|
||||
}
|
||||
: {
|
||||
id: formData.id,
|
||||
savedQueryId: formData.savedQueryId,
|
||||
query: formData.query,
|
||||
ecsMapping: formData.ecs_mapping,
|
||||
|
@ -150,10 +143,9 @@ const OsqueryResponseActionParamsFormComponent = ({
|
|||
const queryDetails = useMemo(
|
||||
() => ({
|
||||
queries,
|
||||
action_id: id,
|
||||
agents: [],
|
||||
}),
|
||||
[id, queries]
|
||||
[queries]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -10,9 +10,7 @@ import React from 'react';
|
|||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import { useAllLiveQueries } from '../../actions/use_all_live_queries';
|
||||
import { KibanaContextProvider } from '../../common/lib/kibana';
|
||||
import { Direction } from '../../../common/search_strategy';
|
||||
|
||||
import { queryClient } from '../../query_client';
|
||||
import { KibanaThemeProvider } from '../../shared_imports';
|
||||
|
@ -23,41 +21,30 @@ import { OsqueryResult } from './osquery_result';
|
|||
const OsqueryActionResultsComponent: React.FC<OsqueryActionResultsProps> = ({
|
||||
agentIds,
|
||||
ruleName,
|
||||
alertId,
|
||||
actionItems,
|
||||
ecsData,
|
||||
}) => {
|
||||
const { data: actionsData } = useAllLiveQueries({
|
||||
filterQuery: { term: { alert_ids: alertId } },
|
||||
activePage: 0,
|
||||
limit: 100,
|
||||
direction: Direction.desc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
}) => (
|
||||
<div data-test-subj={'osquery-results'}>
|
||||
{actionItems?.map((item) => {
|
||||
const actionId = item.fields?.action_id?.[0];
|
||||
const queryId = item.fields?.['queries.action_id']?.[0];
|
||||
const startDate = item.fields?.['@timestamp'][0];
|
||||
|
||||
return (
|
||||
<div data-test-subj={'osquery-results'}>
|
||||
{actionsData?.data.items.map((item, index) => {
|
||||
const actionId = item.fields?.action_id?.[0];
|
||||
const queryId = item.fields?.['queries.action_id']?.[0];
|
||||
// const query = item.fields?.['queries.query']?.[0];
|
||||
const startDate = item.fields?.['@timestamp'][0];
|
||||
|
||||
return (
|
||||
<OsqueryResult
|
||||
key={actionId + index}
|
||||
actionId={actionId}
|
||||
queryId={queryId}
|
||||
startDate={startDate}
|
||||
ruleName={ruleName}
|
||||
agentIds={agentIds}
|
||||
ecsData={ecsData}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<OsqueryResult
|
||||
key={actionId}
|
||||
actionId={actionId}
|
||||
queryId={queryId}
|
||||
startDate={startDate}
|
||||
ruleName={ruleName}
|
||||
agentIds={agentIds}
|
||||
ecsData={ecsData}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const OsqueryActionResults = React.memo(OsqueryActionResultsComponent);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { ATTACHED_QUERY } from '../../agents/translations';
|
|||
import { PackQueriesStatusTable } from '../../live_queries/form/pack_queries_status_table';
|
||||
import { AlertAttachmentContext } from '../../common/contexts';
|
||||
|
||||
interface OsqueryResultProps extends Omit<OsqueryActionResultsProps, 'alertId'> {
|
||||
interface OsqueryResultProps extends OsqueryActionResultsProps {
|
||||
actionId: string;
|
||||
queryId: string;
|
||||
startDate: string;
|
||||
|
|
|
@ -13,11 +13,11 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|||
import { OsqueryActionResults } from '.';
|
||||
import { queryClient } from '../../query_client';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
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, getMockedKibanaConfig } from './test_utils';
|
||||
import type { OsqueryActionResultsProps } from './types';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
|
@ -31,11 +31,21 @@ const enablePrivileges = () => {
|
|||
}));
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
const defaultProps: OsqueryActionResultsProps = {
|
||||
agentIds: ['agent1'],
|
||||
ruleName: ['Test-rule'],
|
||||
ruleActions: [{ action_type_id: 'action1' }, { action_type_id: 'action2' }],
|
||||
alertId: 'test-alert-id',
|
||||
actionItems: [
|
||||
{
|
||||
_id: 'test',
|
||||
_index: 'test',
|
||||
fields: {
|
||||
action_id: ['testActionId'],
|
||||
'queries.action_id': ['queriesActionId'],
|
||||
'queries.query': [DETAILS_QUERY],
|
||||
'@timestamp': ['2022-09-08T18:16:30.256Z'],
|
||||
},
|
||||
},
|
||||
],
|
||||
ecsData: {
|
||||
_id: 'test',
|
||||
},
|
||||
|
@ -66,23 +76,6 @@ const renderWithContext = (Element: React.ReactElement) =>
|
|||
describe('Osquery Results', () => {
|
||||
beforeAll(() => {
|
||||
mockKibana();
|
||||
// @ts-expect-error update types
|
||||
jest.spyOn(useAllLiveQueries, 'useAllLiveQueries').mockImplementation(() => ({
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
fields: {
|
||||
action_id: ['sfdsfds'],
|
||||
'queries.action_id': ['dsadas'],
|
||||
'queries.query': [DETAILS_QUERY],
|
||||
'@timestamp': ['2022-09-08T18:16:30.256Z'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest
|
||||
.spyOn(useLiveQueryDetails, 'useLiveQueryDetails')
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { AlertEcsData } from '../../common/contexts';
|
||||
import type { Ecs } from '../../../common/ecs';
|
||||
import type { ActionEdges } from '../../../common/search_strategy';
|
||||
|
||||
export interface OsqueryActionResultsProps {
|
||||
agentIds?: string[];
|
||||
ruleName?: string[];
|
||||
alertId: string;
|
||||
ecsData: AlertEcsData;
|
||||
ecsData: Ecs;
|
||||
actionItems?: ActionEdges;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import type {
|
|||
getLazyOsqueryAction,
|
||||
getLazyOsqueryResponseActionTypeForm,
|
||||
} from './shared_components';
|
||||
import type { useAllLiveQueries, UseAllLiveQueriesConfig } from './actions/use_all_live_queries';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface OsqueryPluginSetup {}
|
||||
|
@ -36,6 +37,7 @@ export interface OsqueryPluginStart {
|
|||
isOsqueryAvailable: (props: { agentId: string }) => boolean;
|
||||
fetchInstallationStatus: () => { loading: boolean; disabled: boolean; permissionDenied: boolean };
|
||||
OsqueryResponseActionTypeForm: ReturnType<typeof getLazyOsqueryResponseActionTypeForm>;
|
||||
fetchAllLiveQueries: (config: UseAllLiveQueriesConfig) => ReturnType<typeof useAllLiveQueries>;
|
||||
}
|
||||
|
||||
export interface AppPluginStartDependencies {
|
||||
|
|
|
@ -7,16 +7,16 @@
|
|||
|
||||
import uuid from 'uuid';
|
||||
import moment from 'moment';
|
||||
import { flatten, isEmpty, map, omit, pick, pickBy, some } from 'lodash';
|
||||
import { filter, flatten, isEmpty, map, omit, pick, pickBy, some } from 'lodash';
|
||||
import { AGENT_ACTIONS_INDEX } from '@kbn/fleet-plugin/common';
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type { Ecs, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { createDynamicQueries, createQueries } from './create_queries';
|
||||
import { getInternalSavedObjectsClient } from '../../routes/utils';
|
||||
import { parseAgentSelection } from '../../lib/parse_agent_groups';
|
||||
import { packSavedObjectType } from '../../../common/types';
|
||||
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
|
||||
import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
|
||||
import { convertSOQueriesToPack } from '../../routes/pack/utils';
|
||||
import { isSavedQueryPrebuilt } from '../../routes/saved_query/utils';
|
||||
import { ACTIONS_INDEX } from '../../../common/constants';
|
||||
import { TELEMETRY_EBT_LIVE_QUERY_EVENT } from '../../lib/telemetry/constants';
|
||||
import type { PackSavedObjectAttributes } from '../../common/types';
|
||||
|
@ -25,17 +25,23 @@ interface Metadata {
|
|||
currentUser: string | undefined;
|
||||
}
|
||||
|
||||
interface CreateActionHandlerOptions {
|
||||
soClient?: SavedObjectsClientContract;
|
||||
metadata?: Metadata;
|
||||
ecsData?: Ecs;
|
||||
}
|
||||
|
||||
export const createActionHandler = async (
|
||||
osqueryContext: OsqueryAppContext,
|
||||
params: CreateLiveQueryRequestBodySchema,
|
||||
soClient?: SavedObjectsClientContract,
|
||||
metadata?: Metadata
|
||||
options: CreateActionHandlerOptions
|
||||
) => {
|
||||
const [coreStartServices] = await osqueryContext.getStartServices();
|
||||
const esClientInternal = coreStartServices.elasticsearch.client.asInternalUser;
|
||||
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
|
||||
osqueryContext.getStartServices
|
||||
);
|
||||
const { soClient, metadata, ecsData } = options;
|
||||
const savedObjectsClient = soClient ?? coreStartServices.savedObjects.createInternalRepository();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
|
@ -46,6 +52,7 @@ export const createActionHandler = async (
|
|||
platformsSelected: agent_platforms,
|
||||
policiesSelected: agent_policy_ids,
|
||||
});
|
||||
|
||||
if (!selectedAgents.length) {
|
||||
throw new Error('No agents found for selection');
|
||||
}
|
||||
|
@ -95,49 +102,24 @@ export const createActionHandler = async (
|
|||
(value) => !isEmpty(value)
|
||||
)
|
||||
)
|
||||
: params.queries?.length
|
||||
? map(params.queries, (query) =>
|
||||
pickBy(
|
||||
{
|
||||
// @ts-expect-error where does type 'number' comes from?
|
||||
...query,
|
||||
action_id: uuid.v4(),
|
||||
agents: selectedAgents,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
)
|
||||
)
|
||||
: [
|
||||
pickBy(
|
||||
{
|
||||
action_id: uuid.v4(),
|
||||
id: uuid.v4(),
|
||||
query: params.query,
|
||||
saved_query_id: params.saved_query_id,
|
||||
saved_query_prebuilt: params.saved_query_id
|
||||
? await isSavedQueryPrebuilt(
|
||||
osqueryContext.service.getPackageService()?.asInternalUser,
|
||||
params.saved_query_id
|
||||
)
|
||||
: undefined,
|
||||
ecs_mapping: params.ecs_mapping,
|
||||
agents: selectedAgents,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
),
|
||||
],
|
||||
: ecsData
|
||||
? await createDynamicQueries(params, ecsData, osqueryContext)
|
||||
: await createQueries(params, selectedAgents, osqueryContext),
|
||||
};
|
||||
|
||||
const fleetActions = map(osqueryAction.queries, (query) => ({
|
||||
action_id: query.action_id,
|
||||
'@timestamp': moment().toISOString(),
|
||||
expiration: moment().add(5, 'minutes').toISOString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'osquery',
|
||||
agents: query.agents,
|
||||
user_id: metadata?.currentUser,
|
||||
data: pick(query, ['id', 'query', 'ecs_mapping', 'version', 'platform']),
|
||||
}));
|
||||
const fleetActions = map(
|
||||
filter(osqueryAction.queries, (query) => !query.error),
|
||||
(query) => ({
|
||||
action_id: query.action_id,
|
||||
'@timestamp': moment().toISOString(),
|
||||
expiration: moment().add(5, 'minutes').toISOString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'osquery',
|
||||
agents: query.agents,
|
||||
user_id: metadata?.currentUser,
|
||||
data: pick(query, ['id', 'query', 'ecs_mapping', 'version', 'platform']),
|
||||
})
|
||||
);
|
||||
|
||||
await esClientInternal.bulk({
|
||||
refresh: 'wait_for',
|
||||
|
|
118
x-pack/plugins/osquery/server/handlers/action/create_queries.ts
Normal file
118
x-pack/plugins/osquery/server/handlers/action/create_queries.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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, map, pickBy } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import type { Ecs } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
|
||||
import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
|
||||
import { replaceParamsQuery } from '../../../common/utils/replace_params_query';
|
||||
import { isSavedQueryPrebuilt } from '../../routes/saved_query/utils';
|
||||
|
||||
export const createQueries = async (
|
||||
params: CreateLiveQueryRequestBodySchema,
|
||||
agents: string[],
|
||||
osqueryContext: OsqueryAppContext
|
||||
) =>
|
||||
params.queries?.length
|
||||
? map(params.queries, (query) =>
|
||||
pickBy(
|
||||
{
|
||||
...query,
|
||||
action_id: uuid.v4(),
|
||||
agents,
|
||||
},
|
||||
(value) => !isEmpty(value) || value === true
|
||||
)
|
||||
)
|
||||
: [
|
||||
pickBy(
|
||||
{
|
||||
action_id: uuid.v4(),
|
||||
id: uuid.v4(),
|
||||
query: params.query,
|
||||
saved_query_id: params.saved_query_id,
|
||||
saved_query_prebuilt: params.saved_query_id
|
||||
? await isSavedQueryPrebuilt(
|
||||
osqueryContext.service.getPackageService()?.asInternalUser,
|
||||
params.saved_query_id
|
||||
)
|
||||
: undefined,
|
||||
ecs_mapping: params.ecs_mapping,
|
||||
agents,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
),
|
||||
];
|
||||
|
||||
export const createDynamicQueries = async (
|
||||
params: CreateLiveQueryRequestBodySchema,
|
||||
alert: Ecs,
|
||||
osqueryContext: OsqueryAppContext
|
||||
) =>
|
||||
params.queries?.length
|
||||
? map(params.queries, ({ query, ...restQuery }) => {
|
||||
const replacedQuery = replacedQueries(query, alert);
|
||||
|
||||
return pickBy(
|
||||
{
|
||||
...replacedQuery,
|
||||
...restQuery,
|
||||
action_id: uuid.v4(),
|
||||
alert_ids: params.alert_ids,
|
||||
agents: params.agent_ids,
|
||||
},
|
||||
(value) => !isEmpty(value) || value === true
|
||||
);
|
||||
})
|
||||
: [
|
||||
pickBy(
|
||||
{
|
||||
action_id: uuid.v4(),
|
||||
id: uuid.v4(),
|
||||
...replacedQueries(params.query, alert),
|
||||
// just for single queries - we need to overwrite the error property
|
||||
error: undefined,
|
||||
saved_query_id: params.saved_query_id,
|
||||
saved_query_prebuilt: params.saved_query_id
|
||||
? await isSavedQueryPrebuilt(
|
||||
osqueryContext.service.getPackageService()?.asInternalUser,
|
||||
params.saved_query_id
|
||||
)
|
||||
: undefined,
|
||||
ecs_mapping: params.ecs_mapping,
|
||||
alert_ids: params.alert_ids,
|
||||
agents: params.agent_ids,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
),
|
||||
];
|
||||
|
||||
const replacedQueries = (
|
||||
query: string | undefined,
|
||||
ecsData?: Ecs
|
||||
): { query: string | undefined; error?: string } => {
|
||||
if (ecsData && query) {
|
||||
const { result, skipped } = replaceParamsQuery(query, ecsData);
|
||||
|
||||
return {
|
||||
query: result,
|
||||
...(skipped
|
||||
? {
|
||||
error: i18n.translate('xpack.osquery.liveQueryActions.error.notFoundParameters', {
|
||||
defaultMessage:
|
||||
"This query hasn't been called due to parameter used and its value not found in the alert.",
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
return { query };
|
||||
};
|
|
@ -11,6 +11,7 @@ import type {
|
|||
CoreStart,
|
||||
Plugin,
|
||||
Logger,
|
||||
Ecs,
|
||||
} from '@kbn/core/server';
|
||||
import { SavedObjectsClient } from '@kbn/core/server';
|
||||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||
|
@ -92,8 +93,8 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
this.telemetryEventsSender.setup(this.telemetryReceiver, plugins.taskManager, core.analytics);
|
||||
|
||||
return {
|
||||
osqueryCreateAction: (params: CreateLiveQueryRequestBodySchema) =>
|
||||
createActionHandler(osqueryContext, params),
|
||||
osqueryCreateAction: (params: CreateLiveQueryRequestBodySchema, ecsData?: Ecs) =>
|
||||
createActionHandler(osqueryContext, params, { ecsData }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -86,8 +86,7 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
|
|||
const { response: osqueryAction } = await createActionHandler(
|
||||
osqueryContext,
|
||||
request.body,
|
||||
soClient,
|
||||
{ currentUser }
|
||||
{ soClient, metadata: { currentUser } }
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
|
|
|
@ -11,6 +11,8 @@ import type {
|
|||
PluginSetup as DataPluginSetup,
|
||||
PluginStart as DataPluginStart,
|
||||
} from '@kbn/data-plugin/server';
|
||||
import type { Ecs } from '@kbn/ecs';
|
||||
|
||||
import type { FleetStartContract } from '@kbn/fleet-plugin/server';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import type { PluginSetupContract } from '@kbn/features-plugin/server';
|
||||
|
@ -24,7 +26,7 @@ import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/
|
|||
import type { CreateLiveQueryRequestBodySchema } from '../common/schemas/routes/live_query';
|
||||
|
||||
export interface OsqueryPluginSetup {
|
||||
osqueryCreateAction: (payload: CreateLiveQueryRequestBodySchema) => void;
|
||||
osqueryCreateAction: (payload: CreateLiveQueryRequestBodySchema, ecsData?: Ecs) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
|
|
@ -65,5 +65,6 @@
|
|||
"@kbn/securitysolution-es-utils",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
"@kbn/std",
|
||||
"@kbn/ecs",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import * as t from 'io-ts';
|
|||
import { ecsMapping, arrayQueries } from '@kbn/osquery-io-ts-types';
|
||||
|
||||
export const OsqueryParams = t.type({
|
||||
id: t.string,
|
||||
query: t.union([t.string, t.undefined]),
|
||||
ecs_mapping: t.union([ecsMapping, t.undefined]),
|
||||
queries: t.union([arrayQueries, t.undefined]),
|
||||
|
@ -18,7 +17,6 @@ export const OsqueryParams = t.type({
|
|||
});
|
||||
|
||||
export const OsqueryParamsCamelCase = t.type({
|
||||
id: t.string,
|
||||
query: t.union([t.string, t.undefined]),
|
||||
ecsMapping: t.union([ecsMapping, t.undefined]),
|
||||
queries: t.union([arrayQueries, t.undefined]),
|
||||
|
|
|
@ -26,7 +26,7 @@ import { mockAlertDetailsData } from './__mocks__';
|
|||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment';
|
||||
import { useGetUserCasesPermissions } from '../../lib/kibana';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana';
|
||||
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
|
||||
|
||||
jest.mock('../../../timelines/components/timeline/body/renderers', () => {
|
||||
|
@ -43,6 +43,7 @@ jest.mock('../../../timelines/components/timeline/body/renderers', () => {
|
|||
|
||||
jest.mock('../../lib/kibana');
|
||||
const originalKibanaLib = jest.requireActual('../../lib/kibana');
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
|
||||
// The returned permissions object will indicate that the user does not have permissions by default
|
||||
|
@ -202,6 +203,30 @@ describe('EventDetails', () => {
|
|||
});
|
||||
|
||||
it('render osquery tab', async () => {
|
||||
const {
|
||||
services: { osquery },
|
||||
} = useKibanaMock();
|
||||
if (osquery) {
|
||||
jest.spyOn(osquery, 'fetchAllLiveQueries').mockReturnValue({
|
||||
data: {
|
||||
// @ts-expect-error - we don't need all the response details to test the functionality
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
_id: 'testId',
|
||||
_index: 'testIndex',
|
||||
fields: {
|
||||
action_id: ['testActionId'],
|
||||
'queries.action_id': ['testQueryActionId'],
|
||||
'queries.query': ['select * from users'],
|
||||
'@timestamp': ['2022-09-08T18:16:30.256Z'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
rawEventData: {
|
||||
|
|
|
@ -5,24 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiCode,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiNotificationBadge,
|
||||
} from '@elastic/eui';
|
||||
import { EuiCode, EuiEmptyPrompt, EuiNotificationBadge, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Ecs } from '../../../../common/ecs';
|
||||
import { PERMISSION_DENIED } from '../../../detection_engine/rule_response_actions/osquery/translations';
|
||||
import { expandDottedObject } from '../../../../common/utils/expand_dotted';
|
||||
import { RESPONSE_ACTION_TYPES } from '../../../../common/detection_engine/rule_response_actions/schemas';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { EventsViewType } from './event_details';
|
||||
import * as i18n from './translations';
|
||||
import type { RESPONSE_ACTION_TYPES } from '../../../../common/detection_engine/rule_response_actions/schemas/response_actions';
|
||||
|
||||
const TabContentWrapper = styled.div`
|
||||
height: 100%;
|
||||
|
@ -30,7 +24,7 @@ const TabContentWrapper = styled.div`
|
|||
`;
|
||||
type RuleParameters = Array<{
|
||||
response_actions: Array<{
|
||||
action_type_id: string;
|
||||
action_type_id: RESPONSE_ACTION_TYPES.OSQUERY;
|
||||
params: Record<string, unknown>;
|
||||
}>;
|
||||
}>;
|
||||
|
@ -98,45 +92,41 @@ export const useOsqueryTab = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const { OsqueryResults } = osquery;
|
||||
const expandedEventFieldsObject = expandDottedObject(
|
||||
rawEventData.fields
|
||||
) as ExpandedEventFieldsObject;
|
||||
|
||||
const parameters = expandedEventFieldsObject.kibana?.alert?.rule?.parameters;
|
||||
const responseActions = parameters?.[0].response_actions;
|
||||
const responseActions =
|
||||
expandedEventFieldsObject?.kibana?.alert?.rule?.parameters?.[0].response_actions;
|
||||
|
||||
const osqueryActionsLength = responseActions?.filter(
|
||||
(action: { action_type_id: string }) => action.action_type_id === RESPONSE_ACTION_TYPES.OSQUERY
|
||||
)?.length;
|
||||
|
||||
if (!osqueryActionsLength) {
|
||||
if (!responseActions?.length) {
|
||||
return;
|
||||
}
|
||||
const ruleName = expandedEventFieldsObject.kibana?.alert?.rule?.name;
|
||||
const agentIds = expandedEventFieldsObject.agent?.id;
|
||||
|
||||
const { OsqueryResults, fetchAllLiveQueries } = osquery;
|
||||
|
||||
const alertId = rawEventData._id;
|
||||
|
||||
const { data: actionsData } = fetchAllLiveQueries({
|
||||
filterQuery: { term: { alert_ids: alertId } },
|
||||
activePage: 0,
|
||||
limit: 100,
|
||||
sortField: '@timestamp',
|
||||
alertId,
|
||||
});
|
||||
const actionItems = actionsData?.data.items || [];
|
||||
|
||||
const ruleName = expandedEventFieldsObject.kibana?.alert?.rule?.name;
|
||||
const agentIds = expandedEventFieldsObject.agent?.id;
|
||||
|
||||
return {
|
||||
id: EventsViewType.osqueryView,
|
||||
'data-test-subj': 'osqueryViewTab',
|
||||
name: (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
alignItems={'center'}
|
||||
justifyContent={'spaceAround'}
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<span>{i18n.OSQUERY_VIEW}</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiNotificationBadge data-test-subj="osquery-actions-notification">
|
||||
{osqueryActionsLength}
|
||||
</EuiNotificationBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
name: i18n.OSQUERY_VIEW,
|
||||
append: (
|
||||
<EuiNotificationBadge data-test-subj="osquery-actions-notification">
|
||||
{actionItems.length}
|
||||
</EuiNotificationBadge>
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
|
@ -144,12 +134,15 @@ export const useOsqueryTab = ({
|
|||
{!application?.capabilities?.osquery?.read ? (
|
||||
emptyPrompt
|
||||
) : (
|
||||
<OsqueryResults
|
||||
agentIds={agentIds}
|
||||
ruleName={ruleName}
|
||||
alertId={alertId}
|
||||
ecsData={ecsData}
|
||||
/>
|
||||
<>
|
||||
<OsqueryResults
|
||||
agentIds={agentIds}
|
||||
ruleName={ruleName}
|
||||
actionItems={actionItems}
|
||||
ecsData={ecsData}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
</TabContentWrapper>
|
||||
</>
|
||||
|
|
|
@ -5,291 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { pickBy, isEmpty } from 'lodash';
|
||||
import type { Plugin } from 'unified';
|
||||
import React, { useContext, useMemo, useState, useCallback } from 'react';
|
||||
import type { RemarkTokenizer } from '@elastic/eui';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiCodeBlock,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import styled from 'styled-components';
|
||||
import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '../../../../lib/kibana';
|
||||
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 { OsqueryNotAvailablePrompt } from './not_available_prompt';
|
||||
import { plugin } from './plugin';
|
||||
import { OsqueryParser } from './parser';
|
||||
import { OsqueryRenderer } from './renderer';
|
||||
|
||||
const StyledEuiButton = styled(EuiButton)`
|
||||
> span > img {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const OsqueryEditorComponent = ({
|
||||
node,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: EuiMarkdownEditorUiPluginEditorProps<{
|
||||
configuration: {
|
||||
label?: string;
|
||||
query: string;
|
||||
ecs_mapping: { [key: string]: {} };
|
||||
};
|
||||
}>) => {
|
||||
const isEditMode = node != null;
|
||||
const {
|
||||
osquery,
|
||||
application: {
|
||||
capabilities: { osquery: osqueryPermissions },
|
||||
},
|
||||
} = useKibana().services;
|
||||
const formMethods = useForm<{
|
||||
label: string;
|
||||
query: string;
|
||||
ecs_mapping: Record<string, unknown>;
|
||||
}>({
|
||||
defaultValues: {
|
||||
label: node?.configuration?.label,
|
||||
query: node?.configuration?.query,
|
||||
ecs_mapping: node?.configuration?.ecs_mapping,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data) => {
|
||||
onSave(
|
||||
`!{osquery${JSON.stringify(
|
||||
pickBy(
|
||||
{
|
||||
query: data.query,
|
||||
label: data.label,
|
||||
ecs_mapping: data.ecs_mapping,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
)
|
||||
)}}`,
|
||||
{
|
||||
block: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
[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;
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<LabelField />
|
||||
<EuiSpacer size="m" />
|
||||
<LiveQueryField formMethods={formMethods} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [formMethods, osquery]);
|
||||
|
||||
if (noOsqueryPermissions) {
|
||||
return <OsqueryNotAvailablePrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.editModalTitle"
|
||||
defaultMessage="Edit query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.addModalTitle"
|
||||
defaultMessage="Add query"
|
||||
/>
|
||||
)}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<>{OsqueryActionForm}</>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel}>
|
||||
{i18n.translate('xpack.securitySolution.markdown.osquery.modalCancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton onClick={formMethods.handleSubmit(onSubmit)} fill>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel"
|
||||
defaultMessage="Add query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.editModalConfirmButtonLabel"
|
||||
defaultMessage="Save changes"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OsqueryEditor = React.memo(OsqueryEditorComponent);
|
||||
|
||||
export const plugin = {
|
||||
name: 'osquery',
|
||||
button: {
|
||||
label: 'Osquery',
|
||||
iconType: 'logoOsquery',
|
||||
},
|
||||
helpText: (
|
||||
<div>
|
||||
<EuiCodeBlock language="md" fontSize="l" paddingSize="s" isCopyable>
|
||||
{'!{osquery{options}}'}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
),
|
||||
editor: OsqueryEditor,
|
||||
};
|
||||
|
||||
export const parser: Plugin = function () {
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.blockTokenizers;
|
||||
const methods = Parser.prototype.blockMethods;
|
||||
|
||||
const tokenizeOsquery: RemarkTokenizer = function (eat, value, silent) {
|
||||
if (value.startsWith('!{osquery') === false) return false;
|
||||
|
||||
const nextChar = value[9];
|
||||
|
||||
if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a osquery
|
||||
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// is there a configuration?
|
||||
const hasConfiguration = nextChar === '{';
|
||||
|
||||
let match = '!{osquery';
|
||||
let configuration = {};
|
||||
|
||||
if (hasConfiguration) {
|
||||
let configurationString = '';
|
||||
|
||||
let openObjects = 0;
|
||||
|
||||
for (let i = 9; i < value.length; i++) {
|
||||
const char = value[i];
|
||||
if (char === '{') {
|
||||
openObjects++;
|
||||
configurationString += char;
|
||||
} else if (char === '}') {
|
||||
openObjects--;
|
||||
if (openObjects === -1) {
|
||||
break;
|
||||
}
|
||||
configurationString += char;
|
||||
} else {
|
||||
configurationString += char;
|
||||
}
|
||||
}
|
||||
|
||||
match += configurationString;
|
||||
try {
|
||||
configuration = JSON.parse(configurationString);
|
||||
} catch (e) {
|
||||
const now = eat.now();
|
||||
this.file.fail(`Unable to parse osquery JSON configuration: ${e}`, {
|
||||
line: now.line,
|
||||
column: now.column + 9,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
match += '}';
|
||||
|
||||
return eat(match)({
|
||||
type: 'osquery',
|
||||
configuration,
|
||||
});
|
||||
};
|
||||
|
||||
tokenizers.osquery = tokenizeOsquery;
|
||||
methods.splice(methods.indexOf('text'), 0, 'osquery');
|
||||
};
|
||||
|
||||
// receives the configuration from the parser and renders
|
||||
const RunOsqueryButtonRenderer = ({
|
||||
configuration,
|
||||
}: {
|
||||
configuration: {
|
||||
label?: string;
|
||||
query: string;
|
||||
ecs_mapping: { [key: string]: {} };
|
||||
test: [];
|
||||
};
|
||||
}) => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const { agentId, alertId } = useContext(BasicAlertDataContext);
|
||||
|
||||
const handleOpen = useCallback(() => setShowFlyout(true), [setShowFlyout]);
|
||||
|
||||
const handleClose = useCallback(() => setShowFlyout(false), [setShowFlyout]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledEuiButton iconType={OsqueryLogo} onClick={handleOpen}>
|
||||
{configuration.label ??
|
||||
i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', {
|
||||
defaultMessage: 'Run Osquery',
|
||||
})}
|
||||
</StyledEuiButton>
|
||||
{showFlyout && (
|
||||
<OsqueryFlyout
|
||||
defaultValues={{
|
||||
...(alertId ? { alertIds: [alertId] } : {}),
|
||||
query: configuration.query,
|
||||
ecs_mapping: configuration.ecs_mapping,
|
||||
queryField: false,
|
||||
}}
|
||||
agentId={agentId}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { RunOsqueryButtonRenderer as renderer };
|
||||
export { plugin, OsqueryParser as parser, OsqueryRenderer as renderer };
|
||||
|
|
|
@ -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 type { Plugin } from 'unified';
|
||||
import type { RemarkTokenizer } from '@elastic/eui';
|
||||
|
||||
export const OsqueryParser: Plugin = function () {
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.blockTokenizers;
|
||||
const methods = Parser.prototype.blockMethods;
|
||||
|
||||
const tokenizeOsquery: RemarkTokenizer = function (eat, value, silent) {
|
||||
if (value.startsWith('!{osquery') === false) return false;
|
||||
|
||||
const nextChar = value[9];
|
||||
|
||||
if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a osquery
|
||||
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// is there a configuration?
|
||||
const hasConfiguration = nextChar === '{';
|
||||
|
||||
let match = '!{osquery';
|
||||
let configuration = {};
|
||||
|
||||
if (hasConfiguration) {
|
||||
let configurationString = '';
|
||||
|
||||
let openObjects = 0;
|
||||
|
||||
for (let i = 9; i < value.length; i++) {
|
||||
const char = value[i];
|
||||
if (char === '{') {
|
||||
openObjects++;
|
||||
configurationString += char;
|
||||
} else if (char === '}') {
|
||||
openObjects--;
|
||||
if (openObjects === -1) {
|
||||
break;
|
||||
}
|
||||
configurationString += char;
|
||||
} else {
|
||||
configurationString += char;
|
||||
}
|
||||
}
|
||||
|
||||
match += configurationString;
|
||||
try {
|
||||
configuration = JSON.parse(configurationString);
|
||||
} catch (e) {
|
||||
const now = eat.now();
|
||||
this.file.fail(`Unable to parse osquery JSON configuration: ${e}`, {
|
||||
line: now.line,
|
||||
column: now.column + 9,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
match += '}';
|
||||
|
||||
return eat(match)({
|
||||
type: 'osquery',
|
||||
configuration,
|
||||
});
|
||||
};
|
||||
|
||||
tokenizers.osquery = tokenizeOsquery;
|
||||
methods.splice(methods.indexOf('text'), 0, 'osquery');
|
||||
};
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCodeBlock,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { isEmpty, pickBy } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LabelField } from './label_field';
|
||||
import { OsqueryNotAvailablePrompt } from './not_available_prompt';
|
||||
import { useKibana } from '../../../../lib/kibana';
|
||||
|
||||
const OsqueryEditorComponent = ({
|
||||
node,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: EuiMarkdownEditorUiPluginEditorProps<{
|
||||
configuration: {
|
||||
label?: string;
|
||||
query: string;
|
||||
ecs_mapping: { [key: string]: {} };
|
||||
};
|
||||
}>) => {
|
||||
const isEditMode = node != null;
|
||||
const {
|
||||
osquery,
|
||||
application: {
|
||||
capabilities: { osquery: osqueryPermissions },
|
||||
},
|
||||
} = useKibana().services;
|
||||
const formMethods = useForm<{
|
||||
label: string;
|
||||
query: string;
|
||||
ecs_mapping: Record<string, unknown>;
|
||||
}>({
|
||||
defaultValues: {
|
||||
label: node?.configuration?.label,
|
||||
query: node?.configuration?.query,
|
||||
ecs_mapping: node?.configuration?.ecs_mapping,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data) => {
|
||||
onSave(
|
||||
`!{osquery${JSON.stringify(
|
||||
pickBy(
|
||||
{
|
||||
query: data.query,
|
||||
label: data.label,
|
||||
ecs_mapping: data.ecs_mapping,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
)
|
||||
)}}`,
|
||||
{
|
||||
block: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
[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;
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<LabelField />
|
||||
<EuiSpacer size="m" />
|
||||
<LiveQueryField formMethods={formMethods} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [formMethods, osquery]);
|
||||
|
||||
if (noOsqueryPermissions) {
|
||||
return <OsqueryNotAvailablePrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.editModalTitle"
|
||||
defaultMessage="Edit query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.addModalTitle"
|
||||
defaultMessage="Add query"
|
||||
/>
|
||||
)}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<>{OsqueryActionForm}</>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel}>
|
||||
{i18n.translate('xpack.securitySolution.markdown.osquery.modalCancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton onClick={formMethods.handleSubmit(onSubmit)} fill>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel"
|
||||
defaultMessage="Add query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.markdown.osquery.editModalConfirmButtonLabel"
|
||||
defaultMessage="Save changes"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OsqueryEditor = React.memo(OsqueryEditorComponent);
|
||||
|
||||
export const plugin = {
|
||||
name: 'osquery',
|
||||
button: {
|
||||
label: 'Osquery',
|
||||
iconType: 'logoOsquery',
|
||||
},
|
||||
helpText: (
|
||||
<div>
|
||||
<EuiCodeBlock language="md" fontSize="l" paddingSize="s" isCopyable>
|
||||
{'!{osquery{options}}'}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
),
|
||||
editor: OsqueryEditor,
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// receives the configuration from the parser and renders
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { reduce } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view';
|
||||
import { expandDottedObject } from '../../../../../../common/utils/expand_dotted';
|
||||
import type { Ecs } from '../../../../../../common/ecs';
|
||||
import OsqueryLogo from './osquery_icon/osquery.svg';
|
||||
import { OsqueryFlyout } from '../../../../../detections/components/osquery/osquery_flyout';
|
||||
|
||||
const StyledEuiButton = styled(EuiButton)`
|
||||
> span > img {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const OsqueryRenderer = ({
|
||||
configuration,
|
||||
}: {
|
||||
configuration: {
|
||||
label?: string;
|
||||
query: string;
|
||||
ecs_mapping: { [key: string]: {} };
|
||||
test: [];
|
||||
};
|
||||
}) => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const { agentId, alertId, data } = useContext(BasicAlertDataContext);
|
||||
|
||||
const handleOpen = useCallback(() => setShowFlyout(true), [setShowFlyout]);
|
||||
|
||||
const handleClose = useCallback(() => setShowFlyout(false), [setShowFlyout]);
|
||||
|
||||
const ecsData = useMemo(() => {
|
||||
const fieldsMap: Record<string, string> = reduce(
|
||||
data,
|
||||
(acc, eventDetailItem) => ({
|
||||
...acc,
|
||||
[eventDetailItem.field]: eventDetailItem?.values?.[0],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
return expandDottedObject(fieldsMap) as Ecs;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledEuiButton iconType={OsqueryLogo} onClick={handleOpen}>
|
||||
{configuration.label ??
|
||||
i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', {
|
||||
defaultMessage: 'Run Osquery',
|
||||
})}
|
||||
</StyledEuiButton>
|
||||
{showFlyout && (
|
||||
<OsqueryFlyout
|
||||
defaultValues={{
|
||||
...(alertId ? { alertIds: [alertId] } : {}),
|
||||
query: configuration.query,
|
||||
ecs_mapping: configuration.ecs_mapping,
|
||||
queryField: false,
|
||||
}}
|
||||
agentId={agentId}
|
||||
onClose={handleClose}
|
||||
ecsData={ecsData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -70,6 +70,7 @@ export const useKibana = jest.fn().mockReturnValue({
|
|||
},
|
||||
osquery: {
|
||||
OsqueryResults: jest.fn().mockReturnValue(null),
|
||||
fetchAllLiveQueries: jest.fn().mockReturnValue({ data: { data: { items: [] } } }),
|
||||
},
|
||||
timelines: createTGridMocks(),
|
||||
savedObjectsTagging: {
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
interface OsqueryInvestigationGuidePanelProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const panelCss = {
|
||||
marginBottom: '16px',
|
||||
};
|
||||
const flexGroupCss = { padding: `0 24px` };
|
||||
|
||||
export const OsqueryInvestigationGuidePanel = React.memo<OsqueryInvestigationGuidePanelProps>(
|
||||
({ onClick }) => {
|
||||
const [hideInvestigationGuideSuggestion, setHideInvestigationGuideSuggestion] = useState(false);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick();
|
||||
setHideInvestigationGuideSuggestion(true);
|
||||
}, [onClick]);
|
||||
|
||||
if (hideInvestigationGuideSuggestion) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiPanel color={'primary'} paddingSize={'xs'} css={panelCss}>
|
||||
<EuiFlexGroup direction={'row'} alignItems={'center'} css={flexGroupCss}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActionsList.investigationGuideSuggestion"
|
||||
defaultMessage="It seems that you have suggested queries in investigation guide, would you like to add them as response actions?"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size={'m'}
|
||||
color={'primary'}
|
||||
onClick={handleClick}
|
||||
data-test-subj={'osqueryAddInvestigationGuideQueries'}
|
||||
>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActionsList.addButton"
|
||||
defaultMessage="Add"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
OsqueryInvestigationGuidePanel.displayName = 'OsqueryInvestigationGuidePanel';
|
|
@ -15,6 +15,29 @@ import type { ArrayItem } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_
|
|||
import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { getMockTheme } from '../../common/lib/kibana/kibana_react.mock';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
|
||||
useParams: () => ({
|
||||
detailName: 'testId',
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../common/lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useToasts: jest.fn().mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import * as rules from '../rule_management/logic/use_rule';
|
||||
// @ts-expect-error we don't really care about thr useRule return value
|
||||
jest.spyOn(rules, 'useRule').mockReturnValue({});
|
||||
|
||||
const renderWithContext = (Element: React.ReactElement) => {
|
||||
const mockTheme = getMockTheme({ eui: { euiColorLightestShade: '#F5F7FA' } });
|
||||
|
||||
|
@ -38,7 +61,8 @@ describe('ResponseActionsForm', () => {
|
|||
const { getByTestId, queryByTestId } = renderWithContext(<Component items={[]} />);
|
||||
expect(getByTestId('response-actions-form'));
|
||||
expect(getByTestId('response-actions-header'));
|
||||
expect(getByTestId('response-actions-list'));
|
||||
expect(getByTestId('response-actions-wrapper'));
|
||||
expect(queryByTestId('response-actions-list'));
|
||||
expect(queryByTestId('response-actions-list-item-0')).toEqual(null);
|
||||
});
|
||||
it('renders list of elements', async () => {
|
||||
|
|
|
@ -10,9 +10,9 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
|||
import { map, reduce, upperFirst } from 'lodash';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { css } from '@emotion/react';
|
||||
import { ResponseActionsWrapper } from './response_actions_wrapper';
|
||||
import { FORM_ERRORS_TITLE } from '../../detections/components/rules/rule_actions_field/translations';
|
||||
import { ResponseActionsHeader } from './response_actions_header';
|
||||
import { ResponseActionsList } from './response_actions_list';
|
||||
import type { ArrayItem, FormHook } from '../../shared_imports';
|
||||
import { useSupportedResponseActionTypes } from './use_supported_response_action_types';
|
||||
|
||||
|
@ -44,7 +44,7 @@ export const ResponseActionsForm = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<ResponseActionsList
|
||||
<ResponseActionsWrapper
|
||||
items={items}
|
||||
removeItem={removeItem}
|
||||
supportedResponseActionTypes={supportedResponseActionTypes}
|
||||
|
|
|
@ -5,67 +5,59 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import type { ResponseActionType } from './get_supported_response_actions';
|
||||
import { ResponseActionAddButton } from './response_action_add_button';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { OsqueryInvestigationGuidePanel } from './osquery/osquery_investigation_guide_panel';
|
||||
import { useRule } from '../rule_management/logic';
|
||||
import { ResponseActionTypeForm } from './response_action_type_form';
|
||||
import type { ArrayItem } from '../../shared_imports';
|
||||
import { UseField, useFormContext } from '../../shared_imports';
|
||||
import { UseField, useFormContext, useFormData } from '../../shared_imports';
|
||||
import { getResponseActionsFromNote, getOsqueryQueriesFromNote } from './utils';
|
||||
|
||||
interface ResponseActionsListProps {
|
||||
items: ArrayItem[];
|
||||
removeItem: (id: number) => void;
|
||||
addItem: () => void;
|
||||
supportedResponseActionTypes: ResponseActionType[];
|
||||
}
|
||||
|
||||
const GhostFormField = () => <></>;
|
||||
|
||||
export const ResponseActionsList = React.memo(
|
||||
({ items, removeItem, supportedResponseActionTypes, addItem }: ResponseActionsListProps) => {
|
||||
const actionTypeIdRef = useRef<string | null>(null);
|
||||
const updateActionTypeId = useCallback((id) => {
|
||||
actionTypeIdRef.current = id;
|
||||
}, []);
|
||||
export const ResponseActionsList = React.memo<ResponseActionsListProps>(({ items, removeItem }) => {
|
||||
const { detailName: ruleId } = useParams<{ detailName: string }>();
|
||||
const { data: rule } = useRule(ruleId);
|
||||
|
||||
const context = useFormContext();
|
||||
const renderButton = useMemo(() => {
|
||||
return (
|
||||
<ResponseActionAddButton
|
||||
supportedResponseActionTypes={supportedResponseActionTypes}
|
||||
addActionType={addItem}
|
||||
updateActionTypeId={updateActionTypeId}
|
||||
/>
|
||||
);
|
||||
}, [addItem, updateActionTypeId, supportedResponseActionTypes]);
|
||||
const osqueryNoteQueries = useMemo(
|
||||
() => (rule?.note ? getOsqueryQueriesFromNote(rule.note) : []),
|
||||
[rule?.note]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionTypeIdRef.current) {
|
||||
const index = items.length - 1;
|
||||
const path = `responseActions[${index}].actionTypeId`;
|
||||
context.setFieldValue(path, actionTypeIdRef.current);
|
||||
actionTypeIdRef.current = null;
|
||||
}
|
||||
}, [context, items.length]);
|
||||
const context = useFormContext();
|
||||
const [formData] = useFormData();
|
||||
|
||||
return (
|
||||
<div data-test-subj={'response-actions-list'}>
|
||||
{items.map((actionItem, index) => {
|
||||
return (
|
||||
<div key={actionItem.id} data-test-subj={`response-actions-list-item-${index}`}>
|
||||
<EuiSpacer size="m" />
|
||||
<ResponseActionTypeForm item={actionItem} onDeleteAction={removeItem} />
|
||||
const handleInvestigationGuideClick = useCallback(() => {
|
||||
const values = getResponseActionsFromNote(osqueryNoteQueries, formData.responseActions);
|
||||
context.updateFieldValues(values);
|
||||
}, [context, formData?.responseActions, osqueryNoteQueries]);
|
||||
|
||||
<UseField path={`${actionItem.path}.actionTypeId`} component={GhostFormField} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<EuiSpacer size="m" />
|
||||
{renderButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div data-test-subj={'response-actions-list'}>
|
||||
{items.map((actionItem, index) => {
|
||||
return (
|
||||
<div key={actionItem.id} data-test-subj={`response-actions-list-item-${index}`}>
|
||||
<EuiSpacer size="m" />
|
||||
<ResponseActionTypeForm item={actionItem} onDeleteAction={removeItem} />
|
||||
|
||||
<UseField path={`${actionItem.path}.actionTypeId`} component={GhostFormField} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<EuiSpacer size="m" />
|
||||
{osqueryNoteQueries.length ? (
|
||||
<OsqueryInvestigationGuidePanel onClick={handleInvestigationGuideClick} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ResponseActionsList.displayName = 'ResponseActionsList';
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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, useEffect, useRef, useMemo } from 'react';
|
||||
|
||||
import { ResponseActionsList } from './response_actions_list';
|
||||
import type { ResponseActionType } from './get_supported_response_actions';
|
||||
import { ResponseActionAddButton } from './response_action_add_button';
|
||||
import type { ArrayItem } from '../../shared_imports';
|
||||
import { useFormContext } from '../../shared_imports';
|
||||
|
||||
interface ResponseActionsWrapperProps {
|
||||
items: ArrayItem[];
|
||||
removeItem: (id: number) => void;
|
||||
addItem: () => void;
|
||||
supportedResponseActionTypes: ResponseActionType[];
|
||||
}
|
||||
|
||||
export const ResponseActionsWrapper = React.memo<ResponseActionsWrapperProps>(
|
||||
({ items, removeItem, supportedResponseActionTypes, addItem }) => {
|
||||
const actionTypeIdRef = useRef<string | null>(null);
|
||||
const updateActionTypeId = useCallback((id) => {
|
||||
actionTypeIdRef.current = id;
|
||||
}, []);
|
||||
|
||||
const context = useFormContext();
|
||||
|
||||
const renderButton = useMemo(() => {
|
||||
return (
|
||||
<ResponseActionAddButton
|
||||
supportedResponseActionTypes={supportedResponseActionTypes}
|
||||
addActionType={addItem}
|
||||
updateActionTypeId={updateActionTypeId}
|
||||
/>
|
||||
);
|
||||
}, [addItem, updateActionTypeId, supportedResponseActionTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionTypeIdRef.current) {
|
||||
const index = items.length - 1;
|
||||
const path = `responseActions[${index}].actionTypeId`;
|
||||
context.setFieldValue(path, actionTypeIdRef.current);
|
||||
actionTypeIdRef.current = null;
|
||||
}
|
||||
}, [context, items.length]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="response-actions-wrapper">
|
||||
<ResponseActionsList items={items} removeItem={removeItem} />
|
||||
{renderButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ResponseActionsWrapper.displayName = 'ResponseActionsWrapper';
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { getOsqueryQueriesFromNote } from './utils';
|
||||
|
||||
describe('getOsqueryQueriesFromNote', () => {
|
||||
it('should transform investigation guide note into osquery queries', () => {
|
||||
const note =
|
||||
'!{osquery{"query":"SELECT * FROM processes where pid = {{ process.pid }};","label":"Get processes","ecs_mapping":{"process.pid":{"field":"pid"},"process.name":{"field":"name"},"process.executable":{"field":"path"},"process.args":{"field":"cmdline"},"process.working_directory":{"field":"cwd"},"user.id":{"field":"uid"},"group.id":{"field":"gid"},"process.parent.pid":{"field":"parent"},"process.pgid":{"field":"pgroup"}}}}\n\n!{osquery{"query":"select * from users;","label":"Get users"}}';
|
||||
const queries = getOsqueryQueriesFromNote(note);
|
||||
const expectedQueries = [
|
||||
{
|
||||
type: 'osquery',
|
||||
configuration: {
|
||||
query: 'SELECT * FROM processes where pid = {{ process.pid }};',
|
||||
label: 'Get processes',
|
||||
ecs_mapping: {
|
||||
'process.pid': {
|
||||
field: 'pid',
|
||||
},
|
||||
'process.name': {
|
||||
field: 'name',
|
||||
},
|
||||
'process.executable': {
|
||||
field: 'path',
|
||||
},
|
||||
'process.args': {
|
||||
field: 'cmdline',
|
||||
},
|
||||
'process.working_directory': {
|
||||
field: 'cwd',
|
||||
},
|
||||
'user.id': {
|
||||
field: 'uid',
|
||||
},
|
||||
'group.id': {
|
||||
field: 'gid',
|
||||
},
|
||||
'process.parent.pid': {
|
||||
field: 'parent',
|
||||
},
|
||||
'process.pgid': {
|
||||
field: 'pgroup',
|
||||
},
|
||||
},
|
||||
},
|
||||
position: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 1,
|
||||
offset: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 423,
|
||||
offset: 422,
|
||||
},
|
||||
indent: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'osquery',
|
||||
configuration: {
|
||||
query: 'select * from users;',
|
||||
label: 'Get users',
|
||||
},
|
||||
position: {
|
||||
start: {
|
||||
line: 3,
|
||||
column: 1,
|
||||
offset: 424,
|
||||
},
|
||||
end: {
|
||||
line: 3,
|
||||
column: 63,
|
||||
offset: 486,
|
||||
},
|
||||
indent: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(queries).toEqual(expectedQueries);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 unified from 'unified';
|
||||
import markdown from 'remark-parse';
|
||||
import { filter, reduce } from 'lodash';
|
||||
|
||||
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
|
||||
import type { RuleResponseAction } from '../../../common/detection_engine/rule_response_actions/schemas';
|
||||
import { RESPONSE_ACTION_TYPES } from '../../../common/detection_engine/rule_response_actions/schemas';
|
||||
import { OsqueryParser } from '../../common/components/markdown_editor/plugins/osquery/parser';
|
||||
|
||||
interface OsqueryNoteQuery {
|
||||
configuration: {
|
||||
label: string;
|
||||
query: string;
|
||||
ecs_mapping: ECSMapping;
|
||||
};
|
||||
}
|
||||
|
||||
export const getOsqueryQueriesFromNote = (note: string): OsqueryNoteQuery[] => {
|
||||
const parsedAlertInvestigationGuide = unified()
|
||||
.use([[markdown, {}], OsqueryParser])
|
||||
.parse(note);
|
||||
return filter(parsedAlertInvestigationGuide?.children as object, ['type', 'osquery']);
|
||||
};
|
||||
|
||||
export const getResponseActionsFromNote = (
|
||||
osqueryQueries: OsqueryNoteQuery[],
|
||||
defaultResponseActions: RuleResponseAction[] = []
|
||||
) => {
|
||||
return reduce(
|
||||
osqueryQueries,
|
||||
(acc: { responseActions: RuleResponseAction[] }, { configuration }: OsqueryNoteQuery) => {
|
||||
const responseActionPath = 'responseActions';
|
||||
acc[responseActionPath].push({
|
||||
actionTypeId: RESPONSE_ACTION_TYPES.OSQUERY,
|
||||
params: {
|
||||
savedQueryId: undefined,
|
||||
packId: undefined,
|
||||
queries: undefined,
|
||||
query: configuration.query,
|
||||
ecsMapping: configuration.ecs_mapping,
|
||||
},
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ responseActions: defaultResponseActions }
|
||||
);
|
||||
};
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlyout, EuiFlyoutFooter, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { Ecs } from '../../../../common/ecs';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { OsqueryEventDetailsFooter } from './osquery_flyout_footer';
|
||||
|
@ -19,11 +20,19 @@ const OsqueryActionWrapper = styled.div`
|
|||
|
||||
export interface OsqueryFlyoutProps {
|
||||
agentId?: string;
|
||||
defaultValues?: {};
|
||||
defaultValues?: {
|
||||
alertIds?: string[];
|
||||
query?: string;
|
||||
ecs_mapping?: { [key: string]: {} };
|
||||
queryField?: boolean;
|
||||
};
|
||||
onClose: () => void;
|
||||
ecsData?: Ecs;
|
||||
}
|
||||
|
||||
// Make sure we keep this and ACTIONS_QUERY_KEY in use_all_live_queries.ts in sync.
|
||||
const ACTIONS_QUERY_KEY = 'actions';
|
||||
|
||||
const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({
|
||||
agentId,
|
||||
defaultValues,
|
||||
|
@ -33,6 +42,13 @@ const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({
|
|||
const {
|
||||
services: { osquery },
|
||||
} = useKibana();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateQueries = useCallback(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ACTIONS_QUERY_KEY, { alertId: defaultValues?.alertIds?.[0] }],
|
||||
});
|
||||
}, [defaultValues?.alertIds, queryClient]);
|
||||
|
||||
if (osquery?.OsqueryAction) {
|
||||
return (
|
||||
|
@ -54,6 +70,7 @@ const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({
|
|||
formType="steps"
|
||||
defaultValues={defaultValues}
|
||||
ecsData={ecsData}
|
||||
onSuccess={invalidateQueries}
|
||||
/>
|
||||
</OsqueryActionWrapper>
|
||||
</EuiFlyoutBody>
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { scheduleNotificationResponseActions } from './schedule_notification_response_actions';
|
||||
import { RESPONSE_ACTION_TYPES } from '../../../../common/detection_engine/rule_response_actions/schemas';
|
||||
|
||||
describe('ScheduleNotificationResponseActions', () => {
|
||||
const signalOne = { agent: { id: 'agent-id-1' }, _id: 'alert-id-1', user: { id: 'S-1-5-20' } };
|
||||
const signalTwo = { agent: { id: 'agent-id-2' }, _id: 'alert-id-2' };
|
||||
const signals = [signalOne, signalTwo];
|
||||
const defaultQueryParams = {
|
||||
ecsMapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
savedQueryId: 'testSavedQueryId',
|
||||
query: undefined,
|
||||
queries: [],
|
||||
packId: undefined,
|
||||
};
|
||||
const defaultPackParams = {
|
||||
packId: 'testPackId',
|
||||
queries: [],
|
||||
query: undefined,
|
||||
ecsMapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
savedQueryId: undefined,
|
||||
};
|
||||
const defaultQueries = {
|
||||
ecs_mapping: undefined,
|
||||
platform: 'windows',
|
||||
version: '1.0.0',
|
||||
snapshot: true,
|
||||
removed: false,
|
||||
};
|
||||
|
||||
const defaultResultParams = {
|
||||
agent_ids: ['agent-id-1', 'agent-id-2'],
|
||||
alert_ids: ['alert-id-1', 'alert-id-2'],
|
||||
};
|
||||
const defaultQueryResultParams = {
|
||||
...defaultResultParams,
|
||||
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
ecsMapping: undefined,
|
||||
saved_query_id: 'testSavedQueryId',
|
||||
savedQueryId: undefined,
|
||||
queries: [],
|
||||
};
|
||||
const defaultPackResultParams = {
|
||||
...defaultResultParams,
|
||||
query: undefined,
|
||||
saved_query_id: undefined,
|
||||
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
};
|
||||
|
||||
const simpleQuery = 'select * from uptime';
|
||||
it('should handle osquery response actions with query', async () => {
|
||||
const osqueryActionMock = jest.fn();
|
||||
|
||||
const responseActions = [
|
||||
{
|
||||
actionTypeId: RESPONSE_ACTION_TYPES.OSQUERY,
|
||||
params: {
|
||||
...defaultQueryParams,
|
||||
query: simpleQuery,
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({ signals, responseActions }, osqueryActionMock);
|
||||
|
||||
expect(osqueryActionMock).toHaveBeenCalledWith({
|
||||
...defaultQueryResultParams,
|
||||
query: simpleQuery,
|
||||
});
|
||||
//
|
||||
});
|
||||
it('should handle osquery response actions with packs', async () => {
|
||||
const osqueryActionMock = jest.fn();
|
||||
|
||||
const responseActions = [
|
||||
{
|
||||
actionTypeId: RESPONSE_ACTION_TYPES.OSQUERY,
|
||||
params: {
|
||||
...defaultPackParams,
|
||||
queries: [
|
||||
{
|
||||
...defaultQueries,
|
||||
id: 'query-1',
|
||||
query: simpleQuery,
|
||||
},
|
||||
],
|
||||
packId: 'testPackId',
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({ signals, responseActions }, osqueryActionMock);
|
||||
|
||||
expect(osqueryActionMock).toHaveBeenCalledWith({
|
||||
...defaultPackResultParams,
|
||||
queries: [{ ...defaultQueries, id: 'query-1', query: simpleQuery }],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map, uniq } from 'lodash';
|
||||
import type { Ecs } from '@kbn/ecs';
|
||||
import { uniq, reduce, some, each } from 'lodash';
|
||||
import type { RuleResponseAction } from '../../../../common/detection_engine/rule_response_actions/schemas';
|
||||
import { RESPONSE_ACTION_TYPES } from '../../../../common/detection_engine/rule_response_actions/schemas';
|
||||
import type { SetupPlugins } from '../../../plugin_contract';
|
||||
|
@ -15,31 +16,68 @@ interface ScheduleNotificationActions {
|
|||
responseActions: RuleResponseAction[];
|
||||
}
|
||||
|
||||
interface IAlert {
|
||||
agent: {
|
||||
id: string;
|
||||
};
|
||||
interface AlertsWithAgentType {
|
||||
alerts: Ecs[];
|
||||
agents: string[];
|
||||
alertIds: string[];
|
||||
}
|
||||
const CONTAINS_DYNAMIC_PARAMETER_REGEX = /\{{([^}]+)\}}/g; // when there are 2 opening and 2 closing curly brackets (including brackets)
|
||||
|
||||
export const scheduleNotificationResponseActions = (
|
||||
{ signals, responseActions }: ScheduleNotificationActions,
|
||||
osqueryCreateAction?: SetupPlugins['osquery']['osqueryCreateAction']
|
||||
) => {
|
||||
const filteredAlerts = (signals as IAlert[]).filter((alert) => alert.agent?.id);
|
||||
const agentIds = uniq(filteredAlerts.map((alert: IAlert) => alert.agent?.id));
|
||||
const alertIds = map(filteredAlerts, '_id');
|
||||
const filteredAlerts = (signals as Ecs[]).filter((alert) => alert.agent?.id);
|
||||
|
||||
responseActions.forEach((responseAction) => {
|
||||
const { alerts, agents, alertIds }: AlertsWithAgentType = reduce(
|
||||
filteredAlerts,
|
||||
(acc, alert) => {
|
||||
const agentId = alert.agent?.id;
|
||||
if (agentId !== undefined) {
|
||||
return {
|
||||
alerts: [...acc.alerts, alert],
|
||||
agents: [...acc.agents, agentId],
|
||||
alertIds: [...acc.alertIds, (alert as unknown as { _id: string })._id],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ alerts: [], agents: [], alertIds: [] } as AlertsWithAgentType
|
||||
);
|
||||
const agentIds = uniq(agents);
|
||||
|
||||
each(responseActions, (responseAction) => {
|
||||
if (responseAction.actionTypeId === RESPONSE_ACTION_TYPES.OSQUERY && osqueryCreateAction) {
|
||||
const temporaryQueries = responseAction.params.queries?.length
|
||||
? responseAction.params.queries
|
||||
: [{ query: responseAction.params.query }];
|
||||
const containsDynamicQueries = some(temporaryQueries, (query) => {
|
||||
return query.query ? CONTAINS_DYNAMIC_PARAMETER_REGEX.test(query.query) : false;
|
||||
});
|
||||
const { savedQueryId, packId, queries, ecsMapping, ...rest } = responseAction.params;
|
||||
|
||||
return osqueryCreateAction({
|
||||
...rest,
|
||||
queries,
|
||||
ecs_mapping: ecsMapping,
|
||||
saved_query_id: savedQueryId,
|
||||
agent_ids: agentIds,
|
||||
alert_ids: alertIds,
|
||||
if (!containsDynamicQueries) {
|
||||
return osqueryCreateAction({
|
||||
...rest,
|
||||
queries,
|
||||
ecs_mapping: ecsMapping,
|
||||
saved_query_id: savedQueryId,
|
||||
agent_ids: agentIds,
|
||||
alert_ids: alertIds,
|
||||
});
|
||||
}
|
||||
each(alerts, (alert) => {
|
||||
return osqueryCreateAction(
|
||||
{
|
||||
...rest,
|
||||
queries,
|
||||
ecs_mapping: ecsMapping,
|
||||
saved_query_id: savedQueryId,
|
||||
agent_ids: alert.agent?.id ? [alert.agent.id] : [],
|
||||
alert_ids: [(alert as unknown as { _id: string })._id],
|
||||
},
|
||||
alert
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
"@kbn/core-status-common-internal",
|
||||
"@kbn/repo-info",
|
||||
"@kbn/storybook",
|
||||
"@kbn/ecs",
|
||||
"@kbn/cypress-config",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/shared-ux-utility",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue