[Osquery] Add live query to alerts (#128142)

[Osquery] Add osquery to alerts and timeline
This commit is contained in:
Tomasz Ciecierski 2022-03-23 08:19:14 +01:00 committed by GitHub
parent 829517bb64
commit fb71a2d66e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 673 additions and 108 deletions

View file

@ -48,7 +48,7 @@ const TabComponent = (props: TabProps) => {
return (
<TabContent>
<OsqueryAction metadata={metadata} />
<OsqueryAction agentId={metadata?.info?.agent?.id} />
</TabContent>
);
}, [OsqueryAction, loading, metadata]);

View file

@ -0,0 +1,28 @@
{
"attributes": {
"created_at": "2022-01-28T09:01:46.147Z",
"created_by": "elastic",
"description": "gfd",
"enabled": true,
"name": "testpack",
"queries": [
{
"id": "fds",
"interval": 10,
"query": "select * from uptime;"
}
],
"updated_at": "2022-01-28T09:01:46.147Z",
"updated_by": "elastic"
},
"coreMigrationVersion": "8.1.0",
"id": "eb92a730-8018-11ec-88ce-bd5b5e3a7526",
"references": [],
"sort": [
1643360506152,
9062
],
"type": "osquery-pack",
"updated_at": "2022-01-28T09:01:46.152Z",
"version": "WzgzOTksMV0="
}

View file

@ -0,0 +1,99 @@
{
"id": "c8ca6100-802e-11ec-952d-cf6018da8e2b",
"type": "alert",
"namespaces": [
"default"
],
"updated_at": "2022-01-28T11:38:23.009Z",
"version": "WzE5MjksMV0=",
"attributes": {
"name": "Test-rule",
"tags": [
"__internal_rule_id:22308402-5e0e-421b-8d22-a47ddc4b0188",
"__internal_immutable:false"
],
"alertTypeId": "siem.queryRule",
"consumer": "siem",
"params": {
"author": [],
"description": "asd",
"ruleId": "22308402-5e0e-421b-8d22-a47ddc4b0188",
"falsePositives": [],
"from": "now-360s",
"immutable": false,
"license": "",
"outputIndex": ".siem-signals-default",
"meta": {
"from": "1m",
"kibana_siem_app_url": "http://localhost:5601/app/security"
},
"maxSignals": 100,
"riskScore": 21,
"riskScoreMapping": [],
"severity": "low",
"severityMapping": [],
"threat": [],
"to": "now",
"references": [],
"version": 1,
"exceptionsList": [],
"type": "query",
"language": "kuery",
"index": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*"
],
"query": "_id:*",
"filters": []
},
"schedule": {
"interval": "5m"
},
"enabled": true,
"actions": [],
"throttle": null,
"notifyWhen": "onActiveAlert",
"apiKeyOwner": "elastic",
"legacyId": null,
"createdBy": "elastic",
"updatedBy": "elastic",
"createdAt": "2022-01-28T11:38:17.540Z",
"updatedAt": "2022-01-28T11:38:19.894Z",
"muteAll": true,
"mutedInstanceIds": [],
"executionStatus": {
"status": "ok",
"lastExecutionDate": "2022-01-28T11:38:21.638Z",
"error": null,
"lastDuration": 1369
},
"monitoring": {
"execution": {
"history": [
{
"success": true,
"timestamp": 1643369903007
}
],
"calculated_metrics": {
"success_ratio": 1
}
}
},
"meta": {
"versionApiKeyLastmodified": "8.1.0"
},
"scheduledTaskId": "c8ca6100-802e-11ec-952d-cf6018da8e2b"
},
"references": [],
"migrationVersion": {
"alert": "8.0.0"
},
"coreMigrationVersion": "8.1.0"
}

View file

@ -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 { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
import { login } from '../../tasks/login';
import {
checkResults,
findAndClickButton,
findFormFieldByRowsLabelAndType,
inputQuery,
submitQuery,
} from '../../tasks/live_query';
import { preparePack } from '../../tasks/packs';
import { closeModalIfVisible } from '../../tasks/integrations';
import { navigateTo } from '../../tasks/navigation';
describe('Alert Event Details', () => {
before(() => {
runKbnArchiverScript(ArchiverMethod.LOAD, 'pack');
runKbnArchiverScript(ArchiverMethod.LOAD, 'rule');
});
beforeEach(() => {
login();
});
after(() => {
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'pack');
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'rule');
});
it('should be able to run live query', () => {
const PACK_NAME = 'testpack';
const RULE_NAME = 'Test-rule';
navigateTo('/app/osquery/packs');
preparePack(PACK_NAME);
findAndClickButton('Edit');
cy.contains(`Edit ${PACK_NAME}`);
findFormFieldByRowsLabelAndType(
'Scheduled agent policies (optional)',
'fleet server {downArrow}{enter}'
);
findAndClickButton('Update pack');
closeModalIfVisible();
cy.contains(PACK_NAME);
cy.visit('/app/security/rules');
cy.contains(RULE_NAME).click();
cy.wait(2000);
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
cy.getBySel('ruleSwitch').click();
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false');
cy.getBySel('ruleSwitch').click();
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
cy.visit('/app/security/alerts');
cy.getBySel('expand-event').first().click();
cy.getBySel('take-action-dropdown-btn').click();
cy.getBySel('osquery-action-item').click();
inputQuery('select * from uptime;');
submitQuery();
checkResults();
});
});

View file

@ -45,7 +45,6 @@ describe('Super User - Metrics', () => {
cy.getBySel('comboBoxInput').first().click();
cy.wait(500);
cy.get('div[role=listBox]').should('have.lengthOf.above', 0);
cy.getBySel('comboBoxInput').first().type('{downArrow}{enter}');
submitQuery();

View file

@ -71,7 +71,7 @@ describe('SuperUser - Packs', () => {
});
it('to click the edit button and edit pack', () => {
preparePack(PACK_NAME, SAVED_QUERY_ID);
preparePack(PACK_NAME);
findAndClickButton('Edit');
cy.contains(`Edit ${PACK_NAME}`);
findAndClickButton('Add query');
@ -89,7 +89,7 @@ describe('SuperUser - Packs', () => {
});
it('should trigger validation when saved query is being chosen', () => {
preparePack(PACK_NAME, SAVED_QUERY_ID);
preparePack(PACK_NAME);
findAndClickButton('Edit');
findAndClickButton('Add query');
cy.contains('Attach next query');
@ -103,7 +103,7 @@ describe('SuperUser - Packs', () => {
});
// THIS TESTS TAKES TOO LONG FOR NOW - LET ME THINK IT THROUGH
it.skip('to click the icon and visit discover', () => {
preparePack(PACK_NAME, SAVED_QUERY_ID);
preparePack(PACK_NAME);
cy.react('CustomItemAction', {
props: { index: 0, item: { id: SAVED_QUERY_ID } },
}).click();
@ -124,7 +124,7 @@ describe('SuperUser - Packs', () => {
lensUrl = url;
});
});
preparePack(PACK_NAME, SAVED_QUERY_ID);
preparePack(PACK_NAME);
cy.react('CustomItemAction', {
props: { index: 1, item: { id: SAVED_QUERY_ID } },
}).click();
@ -154,7 +154,7 @@ describe('SuperUser - Packs', () => {
});
it('delete all queries in the pack', () => {
preparePack(PACK_NAME, SAVED_QUERY_ID);
preparePack(PACK_NAME);
cy.contains(/^Edit$/).click();
cy.getBySel('checkboxSelectAll').click();
@ -170,7 +170,7 @@ describe('SuperUser - Packs', () => {
});
it('enable changing saved queries and ecs_mappings', () => {
preparePack(PACK_NAME, SAVED_QUERY_ID);
preparePack(PACK_NAME);
cy.contains(/^Edit$/).click();
findAndClickButton('Add query');
@ -210,7 +210,7 @@ describe('SuperUser - Packs', () => {
});
it('to click delete button', () => {
preparePack(PACK_NAME, SAVED_QUERY_ID);
preparePack(PACK_NAME);
findAndClickButton('Edit');
deleteAndConfirm('pack');
});

View file

@ -6,9 +6,10 @@
*/
import { navigateTo } from '../../tasks/navigation';
import { RESULTS_TABLE_BUTTON } from '../../screens/live_query';
import {
checkResults,
DEFAULT_QUERY,
BIG_QUERY,
deleteAndConfirm,
findFormFieldByRowsLabelAndType,
inputQuery,
@ -34,18 +35,18 @@ describe('Super User - Saved queries', () => {
() => {
cy.contains('New live query').click();
selectAllAgents();
inputQuery(DEFAULT_QUERY);
inputQuery(BIG_QUERY);
submitQuery();
checkResults();
// enter fullscreen
cy.getBySel('dataGridFullScreenButton').trigger('mouseover');
cy.contains(/Full screen$/).should('exist');
cy.contains('Exit full screen').should('not.exist');
cy.getBySel('dataGridFullScreenButton').click();
cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover');
cy.contains(/Enter fullscreen$/).should('exist');
cy.contains('Exit fullscreen').should('not.exist');
cy.getBySel(RESULTS_TABLE_BUTTON).click();
cy.getBySel('dataGridFullScreenButton').trigger('mouseover');
cy.contains(/Full screen$/).should('not.exist');
cy.contains('Exit full screen').should('exist');
cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover');
cy.contains(/Enter Fullscreen$/).should('not.exist');
cy.contains('Exit fullscreen').should('exist');
// hidden columns
cy.react('EuiDataGridHeaderCellWrapper', { props: { id: 'osquery.cmdline' } }).click();
@ -59,10 +60,10 @@ describe('Super User - Saved queries', () => {
cy.getBySel('pagination-button-next').click().wait(500).click();
cy.contains('2 columns hidden').should('exist');
cy.getBySel('dataGridFullScreenButton').trigger('mouseover');
cy.contains(/Full screen$/).should('not.exist');
cy.contains('Exit full screen').should('exist');
cy.getBySel('dataGridFullScreenButton').click();
cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover');
cy.contains(/Enter fullscreen$/).should('not.exist');
cy.contains('Exit fullscreen').should('exist');
cy.getBySel(RESULTS_TABLE_BUTTON).click();
// sorting
cy.react('EuiDataGridHeaderCellWrapper', {
@ -70,8 +71,8 @@ describe('Super User - Saved queries', () => {
}).click();
cy.contains(/Sort A-Z$/).click();
cy.contains('2 columns hidden').should('exist');
cy.getBySel('dataGridFullScreenButton').trigger('mouseover');
cy.contains(/Full screen$/).should('exist');
cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover');
cy.contains(/Enter fullscreen$/).should('exist');
// save new query
cy.contains('Exit full screen').should('not.exist');
@ -111,8 +112,8 @@ describe('Super User - Saved queries', () => {
props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } },
}).click();
deleteAndConfirm('query');
cy.contains(SAVED_QUERY_ID);
cy.contains(/^No items found/);
cy.contains(SAVED_QUERY_ID).should('exist');
cy.contains(SAVED_QUERY_ID).should('not.exist');
}
);
});

View file

@ -9,3 +9,4 @@ export const AGENT_FIELD = '[data-test-subj="comboBoxInput"]';
export const ALL_AGENTS_OPTION = '[title="All agents"]';
export const LIVE_QUERY_EDITOR = '#osquery_editor';
export const SUBMIT_BUTTON = '#submit-button';
export const RESULTS_TABLE_BUTTON = 'dataGridFullScreenButton';

View file

@ -16,7 +16,7 @@ import {
export const addIntegration = (agentPolicy = 'Default Fleet Server policy') => {
cy.getBySel(ADD_POLICY_BTN).click();
cy.getBySel(DATA_COLLECTION_SETUP_STEP).find('.euiLoadingSpinner').should('not.exist');
cy.getBySel('agentPolicySelect').select(agentPolicy);
cy.getBySel('agentPolicySelect').should('have.text', agentPolicy);
cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click();
// sometimes agent is assigned to default policy, sometimes not
closeModalIfVisible();

View file

@ -7,13 +7,14 @@
import { LIVE_QUERY_EDITOR } from '../screens/live_query';
export const DEFAULT_QUERY = 'select * from processes, users;';
export const DEFAULT_QUERY = 'select * from processes;';
export const BIG_QUERY = 'select * from processes, users;';
export const selectAllAgents = () => {
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type('All agents');
cy.react('EuiFilterSelectItem').contains('All agents').should('exist');
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type(
'{downArrow}{enter}'
'{downArrow}{enter}{esc}'
);
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export const preparePack = (packName: string, savedQueryId: string) => {
export const preparePack = (packName: string) => {
cy.contains('Packs').click();
const createdPack = cy.contains(packName);
createdPack.click();

View file

@ -16,7 +16,7 @@ export interface IQueryPayload {
export type PackSavedObject = SavedObject<{
name: string;
description: string | undefined;
queries: Array<Record<string, any>>;
queries: Array<Record<string, unknown>>;
enabled: boolean | undefined;
created_at: string;
created_by: string | undefined;

View file

@ -26,7 +26,7 @@ import {
LazyOsqueryManagedPolicyEditExtension,
LazyOsqueryManagedCustomButtonExtension,
} from './fleet_integration';
import { getLazyOsqueryAction } from './shared_components';
import { getLazyOsqueryAction, useIsOsqueryAvailableSimple } from './shared_components';
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
private kibanaVersion: string;
@ -95,6 +95,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
storage: this.storage,
kibanaVersion: this.kibanaVersion,
}),
isOsqueryAvailable: useIsOsqueryAvailableSimple,
};
}

View file

@ -6,3 +6,4 @@
*/
export { getLazyOsqueryAction } from './lazy_osquery_action';
export { useIsOsqueryAvailableSimple } from './osquery_action/use_is_osquery_available_simple';

View file

@ -5,66 +5,68 @@
* 2.0.
*/
import { find } from 'lodash';
import { EuiErrorBoundary, EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import React, { useMemo } from 'react';
import { QueryClientProvider } from 'react-query';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { useAgentDetails } from '../../agents/use_agent_details';
import { useAgentPolicy } from '../../agent_policies';
import { i18n } from '@kbn/i18n';
import { KibanaContextProvider, useKibana } from '../../common/lib/kibana';
import { LiveQuery } from '../../live_queries';
import { queryClient } from '../../query_client';
import { OsqueryIcon } from '../../components/osquery_icon';
import { KibanaThemeProvider } from '../../shared_imports';
import { useIsOsqueryAvailable } from './use_is_osquery_available';
interface OsqueryActionProps {
metadata?: {
info: {
agent: { id: string };
};
};
agentId?: string;
formType: 'steps' | 'simple';
}
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ metadata }) => {
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ agentId, formType = 'simple' }) => {
const permissions = useKibana().services.application.capabilities.osquery;
const agentId = metadata?.info?.agent?.id ?? undefined;
const {
data: agentData,
isFetched: agentFetched,
isLoading,
} = useAgentDetails({
agentId,
silent: true,
skip: !agentId,
});
const {
data: agentPolicyData,
isFetched: policyFetched,
isError: policyError,
isLoading: policyLoading,
} = useAgentPolicy({
policyId: agentData?.policy_id,
skip: !agentData,
silent: true,
});
const osqueryAvailable = useMemo(() => {
if (policyError) return false;
const emptyPrompt = useMemo(
() => (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={
<h2>
{i18n.translate('xpack.osquery.action.shortEmptyTitle', {
defaultMessage: 'Osquery is not available',
})}
</h2>
}
titleSize="xs"
body={
<p>
{i18n.translate('xpack.osquery.action.empty', {
defaultMessage:
'An Elastic Agent is not installed on this host. To run queries, install Elastic Agent on the host, and then add the Osquery Manager integration to the agent policy in Fleet.',
})}
</p>
}
/>
),
[]
);
const { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData } =
useIsOsqueryAvailable(agentId);
const osqueryPackageInstalled = find(agentPolicyData?.package_policies, [
'package.name',
OSQUERY_INTEGRATION_NAME,
]);
return osqueryPackageInstalled?.enabled;
}, [agentPolicyData?.package_policies, policyError]);
if (!agentId || (agentFetched && !agentData)) {
return emptyPrompt;
}
if (!(permissions.runSavedQueries || permissions.writeLiveQueries)) {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Permissions denied</h2>}
title={
<h2>
{i18n.translate('xpack.osquery.action.permissionDenied', {
defaultMessage: 'Permission denied',
})}
</h2>
}
titleSize="xs"
body={
<p>
@ -80,22 +82,6 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ metadata }) => {
return <EuiLoadingContent lines={10} />;
}
if (!agentId || (agentFetched && !agentData)) {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Osquery is not available</h2>}
titleSize="xs"
body={
<p>
An Elastic Agent is not installed on this host. To run queries, install Elastic Agent on
the host, and then add the Osquery Manager integration to the agent policy in Fleet.
</p>
}
/>
);
}
if (!policyFetched && policyLoading) {
return <EuiLoadingContent lines={10} />;
}
@ -104,12 +90,20 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ metadata }) => {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Osquery is not available</h2>}
title={
<h2>
{i18n.translate('xpack.osquery.action.shortEmptyTitle', {
defaultMessage: 'Osquery is not available',
})}
</h2>
}
titleSize="xs"
body={
<p>
The Osquery Manager integration is not added to the agent policy. To run queries on the
host, add the Osquery Manager integration to the agent policy in Fleet.
{i18n.translate('xpack.osquery.action.unavailable', {
defaultMessage:
'The Osquery Manager integration is not added to the agent policy. To run queries on the host, add the Osquery Manager integration to the agent policy in Fleet.',
})}
</p>
}
/>
@ -120,30 +114,38 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ metadata }) => {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Osquery is not available</h2>}
title={
<h2>
{i18n.translate('xpack.osquery.action.shortEmptyTitle', {
defaultMessage: 'Osquery is not available',
})}
</h2>
}
titleSize="xs"
body={
<p>
To run queries on this host, the Elastic Agent must be active. Check the status of this
agent in Fleet.
{i18n.translate('xpack.osquery.action.agentStatus', {
defaultMessage:
'To run queries on this host, the Elastic Agent must be active. Check the status of this agent in Fleet.',
})}
</p>
}
/>
);
}
return <LiveQuery formType="simple" agentId={agentId} />;
return <LiveQuery formType={formType} agentId={agentId} />;
};
export const OsqueryAction = React.memo(OsqueryActionComponent);
const OsqueryAction = React.memo(OsqueryActionComponent);
// @ts-expect-error update types
const OsqueryActionWrapperComponent = ({ services, ...props }) => (
const OsqueryActionWrapperComponent = ({ services, agentId, formType }) => (
<KibanaThemeProvider theme$={services.theme.theme$}>
<KibanaContextProvider services={services}>
<EuiErrorBoundary>
<QueryClientProvider client={queryClient}>
<OsqueryAction {...props} />
<OsqueryAction agentId={agentId} formType={formType} />
</QueryClientProvider>
</EuiErrorBoundary>
</KibanaContextProvider>

View file

@ -0,0 +1,46 @@
/*
* 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 { useMemo } from 'react';
import { find } from 'lodash';
import { useAgentDetails } from '../../agents/use_agent_details';
import { useAgentPolicy } from '../../agent_policies';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
export const useIsOsqueryAvailable = (agentId?: string) => {
const {
data: agentData,
isFetched: agentFetched,
isLoading,
} = useAgentDetails({
agentId,
silent: true,
skip: !agentId,
});
const {
data: agentPolicyData,
isFetched: policyFetched,
isError: policyError,
isLoading: policyLoading,
} = useAgentPolicy({
policyId: agentData?.policy_id,
skip: !agentData,
silent: true,
});
const osqueryAvailable = useMemo(() => {
if (policyError) return false;
const osqueryPackageInstalled = find(agentPolicyData?.package_policies, [
'package.name',
OSQUERY_INTEGRATION_NAME,
]);
return osqueryPackageInstalled?.enabled;
}, [agentPolicyData?.package_policies, policyError]);
return { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData };
};

View file

@ -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 { useKibana } from '../../common/lib/kibana';
import { useIsOsqueryAvailableSimple } from './use_is_osquery_available_simple';
import { renderHook } from '@testing-library/react-hooks';
import { createStartServicesMock } from '../../../../triggers_actions_ui/public/common/lib/kibana/kibana_react.mock';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { httpServiceMock } from '../../../../../../src/core/public/mocks';
jest.mock('../../common/lib/kibana');
const response = {
item: {
policy_id: '4234234234',
package_policies: [
{
package: { name: OSQUERY_INTEGRATION_NAME },
enabled: true,
},
],
},
};
describe('UseIsOsqueryAvailableSimple', () => {
const mockedHttp = httpServiceMock.createStartContract();
mockedHttp.get.mockResolvedValue(response);
beforeAll(() => {
(useKibana as jest.Mock).mockImplementation(() => {
const mockStartServicesMock = createStartServicesMock();
return {
services: {
...mockStartServicesMock,
http: mockedHttp,
},
};
});
});
it('should expect response from API and return enabled flag', async () => {
const { result, waitForValueToChange } = renderHook(() =>
useIsOsqueryAvailableSimple({
agentId: '3242332',
})
);
expect(result.current).toBe(false);
await waitForValueToChange(() => result.current);
expect(result.current).toBe(true);
});
});

View file

@ -0,0 +1,43 @@
/*
* 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 { useEffect, useState } from 'react';
import { find } from 'lodash';
import { useKibana } from '../../common/lib/kibana';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { AgentPolicy, FleetServerAgent, NewPackagePolicy } from '../../../../fleet/common';
interface IProps {
agentId: string;
}
export const useIsOsqueryAvailableSimple = ({ agentId }: IProps) => {
const { http } = useKibana().services;
const [isAvailable, setIsAvailable] = useState(false);
useEffect(() => {
(async () => {
try {
const { item: agentInfo }: { item: FleetServerAgent } = await http.get(
`/internal/osquery/fleet_wrapper/agents/${agentId}`
);
const { item: packageInfo }: { item: AgentPolicy } = await http.get(
`/internal/osquery/fleet_wrapper/agent_policies/${agentInfo.policy_id}/`
);
const osqueryPackageInstalled = find(packageInfo?.package_policies, [
'package.name',
OSQUERY_INTEGRATION_NAME,
]) as NewPackagePolicy;
setIsAvailable(osqueryPackageInstalled.enabled);
} catch (err) {
return;
}
})();
}, [agentId, http]);
return isAvailable;
};

View file

@ -22,6 +22,7 @@ import { getLazyOsqueryAction } from './shared_components';
export interface OsqueryPluginSetup {}
export interface OsqueryPluginStart {
OsqueryAction?: ReturnType<typeof getLazyOsqueryAction>;
isOsqueryAvailable: (props: { agentId: string }) => boolean;
}
export interface AppPluginStartDependencies {

View file

@ -39,7 +39,8 @@
"lists",
"home",
"telemetry",
"dataViewFieldEditor"
"dataViewFieldEditor",
"osquery"
],
"server": true,
"ui": true,

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { ACTION_OSQUERY } from './translations';
interface IProps {
handleClick: () => void;
}
export const OsqueryActionItem = ({ handleClick }: IProps) => {
return (
<EuiContextMenuItem
key="osquery-action-item"
data-test-subj="osquery-action-item"
onClick={handleClick}
>
{ACTION_OSQUERY}
</EuiContextMenuItem>
);
};

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import styled from 'styled-components';
import { EuiFlyout, EuiFlyoutFooter, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
import { OsqueryEventDetailsFooter } from './osquery_flyout_footer';
import { OsqueryEventDetailsHeader } from './osquery_flyout_header';
import { ACTION_OSQUERY } from './translations';
const OsqueryActionWrapper = styled.div`
padding: 8px;
`;
export interface OsqueryFlyoutProps {
agentId: string;
onClose: () => void;
}
export const OsqueryFlyout: React.FC<OsqueryFlyoutProps> = ({ agentId, onClose }) => {
const {
services: { osquery },
} = useKibana();
// @ts-expect-error
const { OsqueryAction } = osquery;
return (
<EuiFlyout
ownFocus
maskProps={{ style: 'z-index: 5000' }} // For an edge case to display above the timeline flyout
size="m"
onClose={onClose}
>
<EuiFlyoutHeader hasBorder>
<OsqueryEventDetailsHeader
primaryText={<h2>{ACTION_OSQUERY}</h2>}
handleClick={onClose}
data-test-subj="flyout-header-osquery"
/>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<OsqueryActionWrapper data-test-subj="flyout-body-osquery">
<OsqueryAction agentId={agentId} formType="steps" />
</OsqueryActionWrapper>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<OsqueryEventDetailsFooter handleClick={onClose} data-test-subj="flyout-footer-osquery" />
</EuiFlyoutFooter>
</EuiFlyout>
);
};
OsqueryFlyout.displayName = 'OsqueryFlyout';

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
interface EventDetailsFooterProps {
handleClick: () => void;
}
export const OsqueryEventDetailsFooterComponent = ({ handleClick }: EventDetailsFooterProps) => {
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={handleClick} data-test-subj="osquery-empty-button">
<FormattedMessage id="xpack.securitySolution.footer.cancel" defaultMessage="Cancel" />
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
};
export const OsqueryEventDetailsFooter = React.memo(OsqueryEventDetailsFooterComponent);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButtonEmpty, EuiText, EuiTitle } from '@elastic/eui';
import { BACK_TO_ALERT_DETAILS } from './translations';
interface IProps {
primaryText: React.ReactElement;
handleClick: () => void;
}
const OsqueryEventDetailsHeaderComponent: React.FC<IProps> = ({ primaryText, handleClick }) => {
return (
<>
<EuiButtonEmpty iconType="arrowLeft" iconSide="left" flush="left" onClick={handleClick}>
<EuiText size="xs">
<p>{BACK_TO_ALERT_DETAILS}</p>
</EuiText>
</EuiButtonEmpty>
<EuiTitle>{primaryText}</EuiTitle>
</>
);
};
export const OsqueryEventDetailsHeader = React.memo(OsqueryEventDetailsHeaderComponent);

View file

@ -0,0 +1,22 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const BACK_TO_ALERT_DETAILS = i18n.translate(
'xpack.securitySolution.alertsView.osqueryBackToAlertDetails',
{
defaultMessage: 'Alert Details',
}
);
export const ACTION_OSQUERY = i18n.translate(
'xpack.securitySolution.alertsView.osqueryAlertTitle',
{
defaultMessage: 'Run Osquery',
}
);

View file

@ -78,6 +78,7 @@ describe('take action dropdown', () => {
refetch: jest.fn(),
refetchFlyoutData: jest.fn(),
timelineId: TimelineId.active,
onOsqueryClick: jest.fn(),
};
beforeAll(() => {
@ -89,8 +90,11 @@ describe('take action dropdown', () => {
...mockStartServicesMock,
timelines: { ...mockTimelines },
cases: mockCasesContract(),
osquery: {
isOsqueryAvailable: jest.fn().mockReturnValue(true),
},
application: {
capabilities: { siem: { crud_alerts: true, read_alerts: true } },
capabilities: { siem: { crud_alerts: true, read_alerts: true }, osquery: true },
},
},
};
@ -190,6 +194,13 @@ describe('take action dropdown', () => {
).toEqual('Investigate in timeline');
});
});
test('should render "Run Osquery"', async () => {
await waitFor(() => {
expect(wrapper.find('[data-test-subj="osquery-action-item"]').first().text()).toEqual(
'Run Osquery'
);
});
});
});
describe('should correctly enable/disable the "Add Endpoint event filter" button', () => {

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import React, { useState, useCallback, useMemo } from 'react';
import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { EuiButton, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations';
@ -23,6 +23,8 @@ import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_c
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { useUserPrivileges } from '../../../common/components/user_privileges';
import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions';
import { useKibana } from '../../../common/lib/kibana';
import { OsqueryActionItem } from '../osquery/osquery_action_item';
interface ActionsData {
alertStatus: Status;
@ -45,6 +47,7 @@ export interface TakeActionDropdownProps {
refetch: (() => void) | undefined;
refetchFlyoutData: () => Promise<void>;
timelineId: string;
onOsqueryClick: (id: string) => void;
}
export const TakeActionDropdown = React.memo(
@ -61,6 +64,7 @@ export const TakeActionDropdown = React.memo(
refetch,
refetchFlyoutData,
timelineId,
onOsqueryClick,
}: TakeActionDropdownProps) => {
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
const { loading: canAccessEndpointManagementLoading, canAccessEndpointManagement } =
@ -70,6 +74,7 @@ export const TakeActionDropdown = React.memo(
() => !canAccessEndpointManagementLoading && canAccessEndpointManagement,
[canAccessEndpointManagement, canAccessEndpointManagementLoading]
);
const { osquery } = useKibana().services;
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@ -97,6 +102,11 @@ export const TakeActionDropdown = React.memo(
const isEndpointEvent = useMemo(() => isEvent && isAgentEndpoint, [isEvent, isAgentEndpoint]);
const agentId = useMemo(
() => getFieldValue({ category: 'agent', field: 'agent.id' }, detailsData),
[detailsData]
);
const togglePopoverHandler = useCallback(() => {
setIsPopoverOpen(!isPopoverOpen);
}, [isPopoverOpen]);
@ -166,6 +176,23 @@ export const TakeActionDropdown = React.memo(
onInvestigateInTimelineAlertClick: closePopoverHandler,
});
const osqueryAvailable = osquery?.isOsqueryAvailable({
agentId,
});
const handleOnOsqueryClick = useCallback(() => {
onOsqueryClick(agentId);
setIsPopoverOpen(false);
}, [onOsqueryClick, setIsPopoverOpen, agentId]);
const osqueryActionItem = useMemo(
() =>
OsqueryActionItem({
handleClick: handleOnOsqueryClick,
}),
[handleOnOsqueryClick]
);
const alertsActionItems = useMemo(
() =>
!isEvent && actionsData.ruleId
@ -196,13 +223,16 @@ export const TakeActionDropdown = React.memo(
...(tGridEnabled ? addToCaseActionItems : []),
...alertsActionItems,
...hostIsolationActionItems,
...(osqueryAvailable ? [osqueryActionItem] : []),
...investigateInTimelineActionItems,
],
[
tGridEnabled,
alertsActionItems,
addToCaseActionItems,
alertsActionItems,
hostIsolationActionItems,
osqueryAvailable,
osqueryActionItem,
investigateInTimelineActionItems,
]
);
@ -220,7 +250,6 @@ export const TakeActionDropdown = React.memo(
</EuiButton>
);
}, [togglePopoverHandler]);
return items.length && !loadingEventDetails && ecsData ? (
<EuiPopover
id="AlertTakeActionPanel"

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { find } from 'lodash/fp';
import { connect, ConnectedProps } from 'react-redux';
@ -20,6 +20,7 @@ import { getFieldValue } from '../../../../detections/components/host_isolation/
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
import { Ecs } from '../../../../../common/ecs';
import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { OsqueryFlyout } from '../../../../detections/components/osquery/osquery_flyout';
interface EventDetailsFooterProps {
detailsData: TimelineEventsDetailsItem[] | null;
@ -109,6 +110,14 @@ export const EventDetailsFooterComponent = React.memo(
const { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick } =
useEventFilterModal();
const [isOsqueryFlyoutOpenWithAgentId, setOsqueryFlyoutOpenWithAgentId] = useState<
null | string
>(null);
const closeOsqueryFlyout = useCallback(() => {
setOsqueryFlyoutOpenWithAgentId(null);
}, [setOsqueryFlyoutOpenWithAgentId]);
return (
<>
<EuiFlyoutFooter data-test-subj="side-panel-flyout-footer">
@ -128,6 +137,7 @@ export const EventDetailsFooterComponent = React.memo(
refetch={refetchAll}
indexName={expandedEvent.indexName}
timelineId={timelineId}
onOsqueryClick={setOsqueryFlyoutOpenWithAgentId}
/>
)}
</EuiFlexItem>
@ -154,6 +164,9 @@ export const EventDetailsFooterComponent = React.memo(
maskProps={{ style: 'z-index: 5000' }}
/>
)}
{isOsqueryFlyoutOpenWithAgentId && detailsEcsData != null && (
<OsqueryFlyout agentId={isOsqueryFlyoutOpenWithAgentId} onClose={closeOsqueryFlyout} />
)}
</>
);
}

View file

@ -28,6 +28,7 @@ import type { TimelinesUIStart } from '../../timelines/public';
import type { ResolverPluginSetup } from './resolver/types';
import type { Inspect } from '../common/search_strategy';
import type { MlPluginSetup, MlPluginStart } from '../../ml/public';
import type { OsqueryPluginStart } from '../../osquery/public';
import type { Detections } from './detections';
import type { Cases } from './cases';
@ -69,6 +70,7 @@ export interface StartPlugins {
ml?: MlPluginStart;
spaces?: SpacesPluginStart;
dataViewFieldEditor: IndexPatternFieldEditorStart;
osquery?: OsqueryPluginStart;
}
export type StartServices = CoreStart &

View file

@ -39,6 +39,7 @@
{ "path": "../lists/tsconfig.json" },
{ "path": "../maps/tsconfig.json" },
{ "path": "../ml/tsconfig.json" },
{ "path": "../osquery/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
{ "path": "../security/tsconfig.json" },
{ "path": "../timelines/tsconfig.json" }

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import axios from 'axios';
import { last } from 'lodash';
// import axios from 'axios';
// import { last } from 'lodash';
export async function getLatestVersion(): Promise<string> {
const response: any = await axios('https://artifacts-api.elastic.co/v1/versions');
return last(response.data.versions as string[]) || '8.2.0-SNAPSHOT';
return '8.1.0-SNAPSHOT';
// const response: any = await axios('https://artifacts-api.elastic.co/v1/versions');
// return last(response.data.versions as string[]) || '8.2.0-SNAPSHOT';
}