mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Defend Workflows] Osquery Take Action against all agents (#152188)
fixes https://github.com/elastic/security-team/issues/6125 --------- Co-authored-by: Tomasz Ciecierski <ciecierskitomek@gmail.com> Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
This commit is contained in:
parent
2e171759ca
commit
808dca8574
22 changed files with 255 additions and 247 deletions
|
@ -10,6 +10,10 @@ import { each, get } from 'lodash';
|
|||
const CONTAINS_DYNAMIC_PARAMETER_REGEX = /\{{([^}]+)\}}/g; // when there are 2 opening and 2 closing curly brackets (including brackets)
|
||||
|
||||
export const replaceParamsQuery = (query: string, data: object) => {
|
||||
if (!containsDynamicQuery(query)) {
|
||||
return { result: query, skipped: false };
|
||||
}
|
||||
|
||||
const matchedBrackets = query.match(new RegExp(CONTAINS_DYNAMIC_PARAMETER_REGEX));
|
||||
let resultQuery = query;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { addIntegration, closeModalIfVisible, closeToastIfVisible } from '../../
|
|||
import { login } from '../../tasks/login';
|
||||
import { findAndClickButton, findFormFieldByRowsLabelAndType } from '../../tasks/live_query';
|
||||
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
|
||||
import { DEFAULT_POLICY } from '../../screens/fleet';
|
||||
import { DEFAULT_POLICY, OSQUERY_POLICY } from '../../screens/fleet';
|
||||
|
||||
describe('ALL - Add Integration', () => {
|
||||
const integration = 'Osquery Manager';
|
||||
|
@ -43,11 +43,10 @@ describe('ALL - Add Integration', () => {
|
|||
cy.get('[title="Osquery Manager • Integration"]').should('exist').click();
|
||||
});
|
||||
|
||||
it('should add the old integration and be able to upgrade it', () => {
|
||||
it.skip('should add the old integration and be able to upgrade it', () => {
|
||||
const oldVersion = '0.7.4';
|
||||
|
||||
cy.visit(OLD_OSQUERY_MANAGER);
|
||||
cy.contains(integration).click();
|
||||
addIntegration();
|
||||
cy.contains('osquery_manager-1');
|
||||
cy.visit('app/fleet/policies');
|
||||
|
@ -94,11 +93,19 @@ describe('ALL - Add Integration', () => {
|
|||
addIntegration();
|
||||
cy.contains('osquery_manager-');
|
||||
closeToastIfVisible();
|
||||
cy.getBySel('nav-search-input').type('Osquery');
|
||||
cy.get('[title="Osquery • Management"]').should('exist').click();
|
||||
cy.visit(OSQUERY);
|
||||
cy.contains('Live queries history');
|
||||
});
|
||||
|
||||
it(`add integration to ${OSQUERY_POLICY}`, () => {
|
||||
cy.visit(FLEET_AGENT_POLICIES);
|
||||
cy.contains(OSQUERY_POLICY).click();
|
||||
cy.contains('Add integration').click();
|
||||
cy.contains(integration).click();
|
||||
addIntegration(OSQUERY_POLICY);
|
||||
cy.contains('osquery_manager-');
|
||||
});
|
||||
|
||||
it('should have integration and packs copied when upgrading integration', () => {
|
||||
const packageName = 'osquery_manager';
|
||||
const oldVersion = '1.2.0';
|
||||
|
@ -110,12 +117,12 @@ describe('ALL - Add Integration', () => {
|
|||
cy.contains('Upgrade');
|
||||
cy.contains('Agent policy 1').click();
|
||||
cy.get('tr')
|
||||
.should('contain', 'osquery_manager-2')
|
||||
.should('contain', 'osquery_manager-3')
|
||||
.and('contain', 'Osquery Manager')
|
||||
.and('contain', `v${oldVersion}`);
|
||||
cy.contains('Actions').click();
|
||||
cy.contains('View policy').click();
|
||||
cy.contains('name: osquery_manager-2');
|
||||
cy.contains('name: osquery_manager-3');
|
||||
cy.contains(`version: ${oldVersion}`);
|
||||
cy.get('.euiFlyoutFooter').within(() => {
|
||||
cy.contains('Close').click();
|
||||
|
@ -142,19 +149,19 @@ describe('ALL - Add Integration', () => {
|
|||
cy.contains(/^Advanced$/).click();
|
||||
cy.contains('"Integration":');
|
||||
cy.contains(/^Upgrade integration$/).click();
|
||||
cy.contains(/^osquery_manager-2$/).click();
|
||||
cy.contains(/^osquery_manager-3$/).click();
|
||||
cy.contains(/^Advanced$/).click();
|
||||
cy.contains('"Integration":');
|
||||
cy.contains('Cancel').click();
|
||||
closeModalIfVisible();
|
||||
cy.get('tr')
|
||||
.should('contain', 'osquery_manager-2')
|
||||
.should('contain', 'osquery_manager-3')
|
||||
.and('contain', 'Osquery Manager')
|
||||
.and('contain', 'v')
|
||||
.and('not.contain', `v${oldVersion}`);
|
||||
cy.contains('Actions').click();
|
||||
cy.contains('View policy').click();
|
||||
cy.contains('name: osquery_manager-2');
|
||||
cy.contains('name: osquery_manager-3');
|
||||
|
||||
// test list of prebuilt queries
|
||||
navigateTo('/app/osquery/saved_queries');
|
||||
|
|
|
@ -58,7 +58,7 @@ describe('Alert Event Details', () => {
|
|||
|
||||
it('should prepare packs and alert rules', () => {
|
||||
const PACK_NAME = 'testpack';
|
||||
navigateTo('/app/osquery/packs');
|
||||
navigateTo('/app/osquery/live_queries');
|
||||
preparePack(PACK_NAME);
|
||||
findAndClickButton('Edit');
|
||||
cy.contains(`Edit ${PACK_NAME}`);
|
||||
|
@ -439,6 +439,27 @@ describe('Alert Event Details', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should be able to run take action query against all enrolled agents', () => {
|
||||
loadAlertsEvents();
|
||||
cy.getBySel('expand-event').first().click({ force: true });
|
||||
cy.getBySel('take-action-dropdown-btn').click();
|
||||
cy.getBySel('osquery-action-item').click();
|
||||
cy.getBySel('agentSelection').within(() => {
|
||||
cy.getBySel('comboBoxClearButton').click();
|
||||
cy.getBySel('comboBoxInput').type('All{downArrow}{enter}{esc}');
|
||||
cy.contains('All agents');
|
||||
});
|
||||
inputQuery("SELECT * FROM os_version where name='{{host.os.name}}';", {
|
||||
parseSpecialCharSequences: false,
|
||||
});
|
||||
cy.wait(1000);
|
||||
submitQuery();
|
||||
cy.getBySel('flyout-body-osquery').within(() => {
|
||||
// at least 2 agents should have responded
|
||||
cy.get('[data-grid-row-index]').should('have.length.at.least', 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should substitute params in osquery ran from timelines alerts', () => {
|
||||
loadAlertsEvents();
|
||||
cy.getBySel('send-alert-to-timeline-button').first().click({ force: true });
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('ALL - Custom space', () => {
|
|||
id: CUSTOM_SPACE,
|
||||
name: CUSTOM_SPACE,
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
headers: { 'kbn-xsrf': 'create-space' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,6 +68,7 @@ describe('ALL - Live Query', () => {
|
|||
checkResults();
|
||||
cy.react('Cell', { props: { columnIndex: 0 } })
|
||||
.should('exist')
|
||||
.first()
|
||||
.click();
|
||||
cy.url().should('include', 'app/fleet/agents/');
|
||||
});
|
||||
|
|
|
@ -29,19 +29,20 @@ describe('ALL - Inventory', () => {
|
|||
|
||||
cy.wait(1000);
|
||||
|
||||
cy.getBySel('nodeContainer').click();
|
||||
cy.getBySel('nodeContainer').first().click();
|
||||
cy.contains('Osquery').click();
|
||||
inputQuery('select * from uptime;');
|
||||
|
||||
submitQuery();
|
||||
checkResults();
|
||||
});
|
||||
|
||||
it('should be able to run the previously saved query', () => {
|
||||
cy.getBySel('toggleNavButton').click();
|
||||
cy.getBySel('collapsibleNavAppLink').contains('Infrastructure').click();
|
||||
|
||||
cy.wait(500);
|
||||
cy.getBySel('nodeContainer').click();
|
||||
cy.getBySel('nodeContainer').first().click();
|
||||
cy.contains('Osquery').click();
|
||||
|
||||
cy.getBySel('comboBoxInput').first().click();
|
||||
|
|
|
@ -476,9 +476,8 @@ describe('ALL - Packs', () => {
|
|||
navigateTo('/app/osquery/packs');
|
||||
});
|
||||
|
||||
it('add global packs to polciies', () => {
|
||||
it('add global packs to policies', () => {
|
||||
const globalPack = 'globalPack';
|
||||
cy.contains('Packs').click();
|
||||
findAndClickButton('Add pack');
|
||||
findFormFieldByRowsLabelAndType('Name', globalPack);
|
||||
cy.getBySel('policyIdsComboBox').should('exist');
|
||||
|
@ -517,9 +516,9 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('rev. 2').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('add proper shard to policies packs config', () => {
|
||||
const shardPack = 'shardPack';
|
||||
cy.contains('Packs').click();
|
||||
cy.getBySel('pagination-button-next').click();
|
||||
|
||||
findAndClickButton('Add pack');
|
||||
|
|
|
@ -64,10 +64,12 @@ describe('ALL - Saved queries', () => {
|
|||
cy.contains('Snapshot');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prebuilt ', () => {
|
||||
before(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.LOAD, 'pack_with_prebuilt_saved_queries');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
navigateTo('/app/osquery/saved_queries');
|
||||
});
|
||||
|
@ -77,7 +79,6 @@ describe('ALL - Saved queries', () => {
|
|||
});
|
||||
|
||||
it('checks result type on prebuilt saved query', () => {
|
||||
cy.contains('Saved queries').click();
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 1, item: { attributes: { id: 'users_elastic' } } },
|
||||
}).click();
|
||||
|
@ -85,6 +86,7 @@ describe('ALL - Saved queries', () => {
|
|||
cy.contains('Snapshot');
|
||||
});
|
||||
});
|
||||
|
||||
it('user can run prebuilt saved query and add to case', () => {
|
||||
cy.react('PlayButtonComponent', {
|
||||
props: { savedQuery: { attributes: { id: 'users_elastic' } } },
|
||||
|
@ -98,7 +100,6 @@ describe('ALL - Saved queries', () => {
|
|||
});
|
||||
|
||||
it('user cant delete prebuilt saved query', () => {
|
||||
cy.contains('Saved queries').click();
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 1, item: { attributes: { id: 'users_elastic' } } },
|
||||
}).click();
|
||||
|
@ -114,6 +115,7 @@ describe('ALL - Saved queries', () => {
|
|||
}).click();
|
||||
deleteAndConfirm('query');
|
||||
});
|
||||
|
||||
it('user can edit prebuilt saved query under pack', () => {
|
||||
const PACK_NAME = 'pack_with_prebuilt_sq';
|
||||
preparePack(PACK_NAME);
|
||||
|
|
|
@ -10,3 +10,4 @@ export const ADD_AGENT_BUTTON = 'addAgentButton';
|
|||
export const AGENT_POLICIES_TAB = 'fleet-agent-policies-tab';
|
||||
export const ENROLLMENT_TOKENS_TAB = 'fleet-enrollment-tokens-tab';
|
||||
export const DEFAULT_POLICY = 'Default Fleet Server policy';
|
||||
export const OSQUERY_POLICY = 'Osquery policy';
|
||||
|
|
|
@ -17,7 +17,7 @@ export const selectAllAgents = () => {
|
|||
}).click();
|
||||
cy.react('EuiFilterSelectItem').contains('All agents').should('exist');
|
||||
cy.react('AgentsTable EuiComboBox').type('{downArrow}{enter}{esc}');
|
||||
cy.contains('1 agent selected.');
|
||||
cy.contains('2 agents selected.');
|
||||
};
|
||||
|
||||
export const clearInputQuery = () =>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation';
|
||||
import { closeToastIfVisible } from './integrations';
|
||||
|
||||
export const INTEGRATIONS = 'app/integrations#/';
|
||||
export const FLEET = 'app/fleet/';
|
||||
|
@ -20,7 +21,7 @@ export const navigateTo = (page: string, opts?: Partial<Cypress.VisitOptions>) =
|
|||
cy.contains('Loading Elastic').should('not.exist');
|
||||
|
||||
// There's a security warning toast that seemingly makes ui elements in the bottom right unavailable, so we close it
|
||||
cy.get('[data-test-subj="toastCloseButton"]', { timeout: 30000 }).click();
|
||||
closeToastIfVisible();
|
||||
cy.waitForReact();
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { closeModalIfVisible } from './integrations';
|
||||
import { closeModalIfVisible, closeToastIfVisible } from './integrations';
|
||||
|
||||
export const preparePack = (packName: string) => {
|
||||
cy.contains('Packs').click();
|
||||
|
@ -21,7 +21,7 @@ export const deactivatePack = (packName: string) => {
|
|||
|
||||
cy.contains(`Successfully deactivated "${packName}" pack`).should('not.exist');
|
||||
cy.contains(`Successfully deactivated "${packName}" pack`).should('exist');
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
closeToastIfVisible();
|
||||
};
|
||||
|
||||
export const activatePack = (packName: string) => {
|
||||
|
@ -32,5 +32,5 @@ export const activatePack = (packName: string) => {
|
|||
|
||||
cy.contains(`Successfully activated "${packName}" pack`).should('not.exist');
|
||||
cy.contains(`Successfully activated "${packName}" pack`).should('exist');
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
closeToastIfVisible();
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { RESULTS_TABLE_BUTTON } from '../screens/live_query';
|
||||
import { closeToastIfVisible } from './integrations';
|
||||
import {
|
||||
checkResults,
|
||||
BIG_QUERY,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
selectAllAgents,
|
||||
submitQuery,
|
||||
} from './live_query';
|
||||
import { navigateTo } from './navigation';
|
||||
|
||||
export const getSavedQueriesComplexTest = (savedQueryId: string, savedQueryDescription: string) =>
|
||||
it(
|
||||
|
@ -71,7 +73,7 @@ export const getSavedQueriesComplexTest = (savedQueryId: string, savedQueryDescr
|
|||
|
||||
// visit Status results
|
||||
cy.react('EuiTab', { props: { id: 'status' } }).click();
|
||||
cy.react('EuiTableRow').should('have.lengthOf', 1);
|
||||
cy.react('EuiTableRow').should('have.lengthOf', 2);
|
||||
|
||||
// save new query
|
||||
cy.contains('Exit full screen').should('not.exist');
|
||||
|
@ -81,10 +83,10 @@ export const getSavedQueriesComplexTest = (savedQueryId: string, savedQueryDescr
|
|||
findFormFieldByRowsLabelAndType('Description (optional)', savedQueryDescription);
|
||||
cy.react('EuiButtonDisplay').contains('Save').click();
|
||||
cy.contains('Successfully saved');
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
closeToastIfVisible();
|
||||
|
||||
// play saved query
|
||||
cy.contains('Saved queries').click();
|
||||
navigateTo('/app/osquery/saved_queries');
|
||||
cy.contains(savedQueryId);
|
||||
cy.react('PlayButtonComponent', {
|
||||
props: { savedQuery: { attributes: { id: savedQueryId } } },
|
||||
|
|
|
@ -11,11 +11,7 @@ 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 { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
|
||||
import {
|
||||
containsDynamicQuery,
|
||||
replaceParamsQuery,
|
||||
} from '../../../common/utils/replace_params_query';
|
||||
import { createDynamicQueries, createQueries } from './create_queries';
|
||||
import { createDynamicQueries, replacedQueries } from './create_queries';
|
||||
import { getInternalSavedObjectsClient } from '../../routes/utils';
|
||||
import { parseAgentSelection } from '../../lib/parse_agent_groups';
|
||||
import { packSavedObjectType } from '../../../common/types';
|
||||
|
@ -94,16 +90,13 @@ export const createActionHandler = async (
|
|||
: undefined,
|
||||
queries: packSO
|
||||
? map(convertSOQueriesToPack(packSO.attributes.queries), (packQuery, packQueryId) => {
|
||||
const dynamicQueryPresent = packQuery.query && containsDynamicQuery(packQuery.query);
|
||||
const replacedQuery = replacedQueries(packQuery.query, alertData);
|
||||
|
||||
return pickBy(
|
||||
{
|
||||
action_id: uuidv4(),
|
||||
id: packQueryId,
|
||||
query:
|
||||
dynamicQueryPresent && alertData
|
||||
? replaceParamsQuery(packQuery.query, alertData).result
|
||||
: packQuery.query,
|
||||
...replacedQuery,
|
||||
ecs_mapping: packQuery.ecs_mapping,
|
||||
version: packQuery.version,
|
||||
platform: packQuery.platform,
|
||||
|
@ -112,9 +105,7 @@ export const createActionHandler = async (
|
|||
(value) => !isEmpty(value)
|
||||
);
|
||||
})
|
||||
: alertData
|
||||
? await createDynamicQueries(params, alertData, osqueryContext)
|
||||
: await createQueries(params, selectedAgents, osqueryContext),
|
||||
: await createDynamicQueries({ params, alertData, agents: selectedAgents, osqueryContext }),
|
||||
};
|
||||
|
||||
const fleetActions = map(
|
||||
|
@ -130,31 +121,33 @@ export const createActionHandler = async (
|
|||
data: pick(query, ['id', 'query', 'ecs_mapping', 'version', 'platform']),
|
||||
})
|
||||
);
|
||||
|
||||
await esClientInternal.bulk({
|
||||
refresh: 'wait_for',
|
||||
body: flatten(
|
||||
fleetActions.map((action) => [{ index: { _index: AGENT_ACTIONS_INDEX } }, action])
|
||||
),
|
||||
});
|
||||
|
||||
const actionsComponentTemplateExists = await esClientInternal.indices.exists({
|
||||
index: `${ACTIONS_INDEX}*`,
|
||||
});
|
||||
|
||||
if (actionsComponentTemplateExists) {
|
||||
if (fleetActions.length) {
|
||||
await esClientInternal.bulk({
|
||||
refresh: 'wait_for',
|
||||
body: [{ index: { _index: `${ACTIONS_INDEX}-default` } }, osqueryAction],
|
||||
body: flatten(
|
||||
fleetActions.map((action) => [{ index: { _index: AGENT_ACTIONS_INDEX } }, action])
|
||||
),
|
||||
});
|
||||
|
||||
const actionsComponentTemplateExists = await esClientInternal.indices.exists({
|
||||
index: `${ACTIONS_INDEX}*`,
|
||||
});
|
||||
|
||||
if (actionsComponentTemplateExists) {
|
||||
await esClientInternal.bulk({
|
||||
refresh: 'wait_for',
|
||||
body: [{ index: { _index: `${ACTIONS_INDEX}-default` } }, osqueryAction],
|
||||
});
|
||||
}
|
||||
|
||||
osqueryContext.telemetryEventsSender.reportEvent(TELEMETRY_EBT_LIVE_QUERY_EVENT, {
|
||||
...omit(osqueryAction, ['type', 'input_type', 'user_id']),
|
||||
agents: osqueryAction.agents.length,
|
||||
});
|
||||
}
|
||||
|
||||
osqueryContext.telemetryEventsSender.reportEvent(TELEMETRY_EBT_LIVE_QUERY_EVENT, {
|
||||
...omit(osqueryAction, ['type', 'input_type', 'user_id']),
|
||||
agents: osqueryAction.agents.length,
|
||||
});
|
||||
|
||||
return {
|
||||
response: osqueryAction,
|
||||
fleetActionsCount: fleetActions.length,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createDynamicQueries, createQueries } from './create_queries';
|
||||
import { createDynamicQueries, PARAMETER_NOT_FOUND } from './create_queries';
|
||||
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
|
||||
|
||||
|
@ -18,6 +18,8 @@ describe('create queries', () => {
|
|||
removed: false,
|
||||
snapshot: true,
|
||||
};
|
||||
const TEST_AGENT = 'test-agent';
|
||||
|
||||
const mockedQueriesParams = {
|
||||
queries: [
|
||||
{
|
||||
|
@ -49,69 +51,74 @@ describe('create queries', () => {
|
|||
// Info: getting queries by index (eg. [1], [0]) because can't compare whole query object due to unique action_id generated.
|
||||
describe('dynamic', () => {
|
||||
const pid = 123;
|
||||
it('if queries length it should return replaced list of queries', async () => {
|
||||
const queries = await createDynamicQueries(
|
||||
mockedQueriesParams,
|
||||
{
|
||||
it('alertData, multi queries, should replace queries and show errors', async () => {
|
||||
const queries = await createDynamicQueries({
|
||||
params: mockedQueriesParams,
|
||||
agents: [TEST_AGENT],
|
||||
alertData: {
|
||||
process: {
|
||||
pid,
|
||||
},
|
||||
} as unknown as ParsedTechnicalFields,
|
||||
{} as OsqueryAppContext
|
||||
);
|
||||
osqueryContext: {} as OsqueryAppContext,
|
||||
});
|
||||
expect(queries[0].query).toBe(`SELECT * FROM processes where pid=${pid};`);
|
||||
expect(queries[0].error).toBe(undefined);
|
||||
expect(queries[1].query).toBe('SELECT * FROM processes where pid={{process.not-existing}};');
|
||||
expect(queries[1].error).toBe(
|
||||
"This query hasn't been called due to parameter used and its value not found in the alert."
|
||||
);
|
||||
expect(queries[1].query).toBe('SELECT * FROM processes where pid={{process.not-existing}};');
|
||||
expect(queries[2].query).toBe('SELECT * FROM processes;');
|
||||
expect(queries[2].error).toBe(undefined);
|
||||
});
|
||||
it('if single query it should return one replaced query ', async () => {
|
||||
const queries = await createDynamicQueries(
|
||||
mockedSingleQueryParams,
|
||||
{
|
||||
process: {
|
||||
pid,
|
||||
},
|
||||
|
||||
it('alertData, single query, existing param should return changed query', async () => {
|
||||
const queries = await createDynamicQueries({
|
||||
params: mockedSingleQueryParams,
|
||||
agents: [TEST_AGENT],
|
||||
alertData: {
|
||||
process: { pid },
|
||||
} as unknown as ParsedTechnicalFields,
|
||||
{} as OsqueryAppContext
|
||||
);
|
||||
osqueryContext: {} as OsqueryAppContext,
|
||||
});
|
||||
expect(queries[0].query).toBe(`SELECT * FROM processes where pid=${pid};`);
|
||||
expect(queries[0].error).toBe(undefined);
|
||||
});
|
||||
it('if single query with not existing parameter it should return query as it is', async () => {
|
||||
const queries = await createDynamicQueries(
|
||||
mockedSingleQueryParams,
|
||||
{
|
||||
it('alertData, single query, not existing param should return error', async () => {
|
||||
const queries = await createDynamicQueries({
|
||||
params: mockedSingleQueryParams,
|
||||
agents: [TEST_AGENT],
|
||||
alertData: {
|
||||
process: {},
|
||||
} as unknown as ParsedTechnicalFields,
|
||||
{} as OsqueryAppContext
|
||||
);
|
||||
osqueryContext: {} as OsqueryAppContext,
|
||||
});
|
||||
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
|
||||
expect(queries[0].error).toBe(PARAMETER_NOT_FOUND);
|
||||
});
|
||||
it('no alert data, multi query, return unchanged queries no error', async () => {
|
||||
const queries = await createDynamicQueries({
|
||||
params: mockedQueriesParams,
|
||||
agents: [TEST_AGENT],
|
||||
osqueryContext: {} as OsqueryAppContext,
|
||||
});
|
||||
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
|
||||
expect(queries[0].agents).toContain(TEST_AGENT);
|
||||
expect(queries[0].error).toBe(undefined);
|
||||
expect(queries[2].query).toBe('SELECT * FROM processes;');
|
||||
expect(queries[2].agents).toContain(TEST_AGENT);
|
||||
expect(queries[2].error).toBe(undefined);
|
||||
});
|
||||
|
||||
it('no alert data, single query, return unchanged query and no error', async () => {
|
||||
const queries = await createDynamicQueries({
|
||||
params: mockedSingleQueryParams,
|
||||
agents: [TEST_AGENT],
|
||||
osqueryContext: {} as OsqueryAppContext,
|
||||
});
|
||||
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
|
||||
expect(queries[0].agents).toContain(TEST_AGENT);
|
||||
expect(queries[0].error).toBe(undefined);
|
||||
});
|
||||
});
|
||||
describe('normal', () => {
|
||||
const TEST_AGENT = 'test-agent';
|
||||
it('if queries length it should return not replaced list of queries with agents', async () => {
|
||||
const queries = await createQueries(
|
||||
mockedQueriesParams,
|
||||
[TEST_AGENT],
|
||||
{} as OsqueryAppContext
|
||||
);
|
||||
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
|
||||
expect(queries[0].agents).toContain(TEST_AGENT);
|
||||
expect(queries[2].query).toBe('SELECT * FROM processes;');
|
||||
expect(queries[2].agents).toContain(TEST_AGENT);
|
||||
});
|
||||
|
||||
it('if single query should return not replaced query with agents', async () => {
|
||||
const queries = await createQueries(
|
||||
mockedSingleQueryParams,
|
||||
[TEST_AGENT],
|
||||
{} as OsqueryAppContext
|
||||
);
|
||||
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
|
||||
expect(queries[0].agents).toContain(TEST_AGENT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,47 +15,26 @@ import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/r
|
|||
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: uuidv4(),
|
||||
agents,
|
||||
},
|
||||
(value) => !isEmpty(value) || value === true
|
||||
)
|
||||
)
|
||||
: [
|
||||
pickBy(
|
||||
{
|
||||
action_id: uuidv4(),
|
||||
id: uuidv4(),
|
||||
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 PARAMETER_NOT_FOUND = 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.",
|
||||
}
|
||||
);
|
||||
|
||||
export const createDynamicQueries = async (
|
||||
params: CreateLiveQueryRequestBodySchema,
|
||||
alertData: ParsedTechnicalFields,
|
||||
osqueryContext: OsqueryAppContext
|
||||
) =>
|
||||
interface CreateDynamicQueriesParams {
|
||||
params: CreateLiveQueryRequestBodySchema;
|
||||
alertData?: ParsedTechnicalFields;
|
||||
agents: string[];
|
||||
osqueryContext: OsqueryAppContext;
|
||||
}
|
||||
export const createDynamicQueries = async ({
|
||||
params,
|
||||
alertData,
|
||||
agents,
|
||||
osqueryContext,
|
||||
}: CreateDynamicQueriesParams) =>
|
||||
params.queries?.length
|
||||
? map(params.queries, ({ query, ...restQuery }) => {
|
||||
const replacedQuery = replacedQueries(query, alertData);
|
||||
|
@ -66,7 +45,7 @@ export const createDynamicQueries = async (
|
|||
...restQuery,
|
||||
action_id: uuidv4(),
|
||||
alert_ids: params.alert_ids,
|
||||
agents: params.agent_ids,
|
||||
agents,
|
||||
},
|
||||
(value) => !isEmpty(value) || value === true
|
||||
);
|
||||
|
@ -77,8 +56,6 @@ export const createDynamicQueries = async (
|
|||
action_id: uuidv4(),
|
||||
id: uuidv4(),
|
||||
...replacedQueries(params.query, alertData),
|
||||
// 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(
|
||||
|
@ -88,13 +65,13 @@ export const createDynamicQueries = async (
|
|||
: undefined,
|
||||
ecs_mapping: params.ecs_mapping,
|
||||
alert_ids: params.alert_ids,
|
||||
agents: params.agent_ids,
|
||||
agents,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
),
|
||||
];
|
||||
|
||||
const replacedQueries = (
|
||||
export const replacedQueries = (
|
||||
query: string | undefined,
|
||||
alertData?: ParsedTechnicalFields
|
||||
): { query: string | undefined; error?: string } => {
|
||||
|
@ -105,10 +82,7 @@ const replacedQueries = (
|
|||
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.",
|
||||
}),
|
||||
error: PARAMETER_NOT_FOUND,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import markdown from 'remark-parse-no-trim';
|
|||
import { some, filter } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import type { ECSMappingOrUndefined } from '@kbn/osquery-io-ts-types';
|
||||
import { PARAMETER_NOT_FOUND } from '../../handlers/action/create_queries';
|
||||
import { replaceParamsQuery } from '../../../common/utils/replace_params_query';
|
||||
import { createLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
|
||||
import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
|
||||
|
@ -93,7 +94,7 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
|
|||
|
||||
try {
|
||||
const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username;
|
||||
const { response: osqueryAction } = await createActionHandler(
|
||||
const { response: osqueryAction, fleetActionsCount } = await createActionHandler(
|
||||
osqueryContext,
|
||||
request.body,
|
||||
{
|
||||
|
@ -102,6 +103,11 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
|
|||
alertData,
|
||||
}
|
||||
);
|
||||
if (!fleetActionsCount) {
|
||||
return response.badRequest({
|
||||
body: PARAMETER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
return response.ok({
|
||||
body: { data: osqueryAction },
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import execa from 'execa';
|
||||
import { getLatestVersion } from './artifact_manager';
|
||||
import { Manager } from './resource_manager';
|
||||
|
||||
|
@ -22,7 +22,7 @@ export interface AgentManagerParams {
|
|||
export class AgentManager extends Manager {
|
||||
private params: AgentManagerParams;
|
||||
private log: ToolingLog;
|
||||
private agentProcess?: ChildProcess;
|
||||
private agentContainerId?: string;
|
||||
private requestOptions: AxiosRequestConfig;
|
||||
constructor(params: AgentManagerParams, log: ToolingLog, requestOptions: AxiosRequestConfig) {
|
||||
super();
|
||||
|
@ -33,34 +33,40 @@ export class AgentManager extends Manager {
|
|||
|
||||
public async setup() {
|
||||
this.log.info('Running agent preconfig');
|
||||
return await axios.post(
|
||||
`${this.params.kibanaUrl}/api/fleet/agents/setup`,
|
||||
{},
|
||||
|
||||
await axios.post(
|
||||
`${this.params.kibanaUrl}/api/fleet/agent_policies?sys_monitoring=true`,
|
||||
{
|
||||
name: 'Osquery policy',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
inactivity_timeout: 1209600,
|
||||
},
|
||||
this.requestOptions
|
||||
);
|
||||
}
|
||||
|
||||
public async startAgent() {
|
||||
this.log.info('Getting agent enrollment key');
|
||||
const { data: apiKeys } = await axios.get(
|
||||
this.params.kibanaUrl + '/api/fleet/enrollment_api_keys',
|
||||
this.requestOptions
|
||||
);
|
||||
const policy = apiKeys.items[1];
|
||||
const policy = apiKeys.items[0];
|
||||
|
||||
this.log.info('Running the agent');
|
||||
|
||||
const artifact = `docker.elastic.co/beats/elastic-agent:${await getLatestVersion()}`;
|
||||
this.log.info(artifact);
|
||||
|
||||
const args = [
|
||||
const dockerArgs = [
|
||||
'run',
|
||||
'--detach',
|
||||
'--add-host',
|
||||
'host.docker.internal:host-gateway',
|
||||
'--env',
|
||||
'FLEET_ENROLL=1',
|
||||
'--env',
|
||||
`FLEET_URL=http://host.docker.internal:8220`,
|
||||
`FLEET_URL=https://host.docker.internal:8220`,
|
||||
'--env',
|
||||
`FLEET_ENROLLMENT_TOKEN=${policy.api_key}`,
|
||||
'--env',
|
||||
|
@ -69,7 +75,7 @@ export class AgentManager extends Manager {
|
|||
artifact,
|
||||
];
|
||||
|
||||
this.agentProcess = spawn('docker', args, { stdio: 'inherit' });
|
||||
this.agentContainerId = (await execa('docker', dockerArgs)).stdout;
|
||||
|
||||
// Wait til we see the agent is online
|
||||
let done = false;
|
||||
|
@ -92,15 +98,11 @@ export class AgentManager extends Manager {
|
|||
|
||||
protected _cleanup() {
|
||||
this.log.info('Cleaning up the agent process');
|
||||
if (this.agentProcess) {
|
||||
if (!this.agentProcess.kill(9)) {
|
||||
this.log.warning('Unable to kill agent process');
|
||||
}
|
||||
if (this.agentContainerId) {
|
||||
this.log.info('Closing agent process');
|
||||
|
||||
this.agentProcess.on('close', () => {
|
||||
this.log.info('Agent process closed');
|
||||
});
|
||||
delete this.agentProcess;
|
||||
execa.sync('docker', ['kill', this.agentContainerId]);
|
||||
this.log.info('Agent process closed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
*/
|
||||
|
||||
export async function getLatestVersion(): Promise<string> {
|
||||
return '8.6.0-SNAPSHOT';
|
||||
return '8.8.0-SNAPSHOT';
|
||||
}
|
||||
|
|
|
@ -35,9 +35,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
...xpackFunctionalTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--csp.warnLegacyBrowsers=false',
|
||||
'--csp.strict=false',
|
||||
// define custom kibana server args here
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
`--xpack.fleet.agents.fleet_server.hosts=["https://host.docker.internal:8220"]`,
|
||||
`--xpack.fleet.agents.elasticsearch.host=http://host.docker.internal:${kibanaCommonTestsConfig.get(
|
||||
'servers.elasticsearch.port'
|
||||
)}`,
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import execa from 'execa';
|
||||
import { Manager } from './resource_manager';
|
||||
import { getLatestVersion } from './artifact_manager';
|
||||
import { AgentManagerParams } from './agent';
|
||||
|
||||
export class FleetManager extends Manager {
|
||||
private fleetProcess?: ChildProcess;
|
||||
private fleetContainerId?: string;
|
||||
private config: AgentManagerParams;
|
||||
private log: ToolingLog;
|
||||
private requestOptions: AxiosRequestConfig;
|
||||
|
@ -25,77 +25,65 @@ export class FleetManager extends Manager {
|
|||
}
|
||||
public async setup(): Promise<void> {
|
||||
this.log.info('Setting fleet up');
|
||||
return new Promise(async (res, rej) => {
|
||||
try {
|
||||
// default fleet server policy no longer created by default
|
||||
const {
|
||||
data: {
|
||||
item: { id: policyId },
|
||||
},
|
||||
} = await axios.post(
|
||||
`${this.config.kibanaUrl}/api/fleet/agent_policies`,
|
||||
{
|
||||
name: 'Default Fleet Server policy',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
has_fleet_server: true,
|
||||
},
|
||||
this.requestOptions
|
||||
);
|
||||
|
||||
const response = await axios.post(
|
||||
`${this.config.kibanaUrl}/api/fleet/service_tokens`,
|
||||
{},
|
||||
this.requestOptions
|
||||
);
|
||||
const serviceToken = response.data.value;
|
||||
const artifact = `docker.elastic.co/beats/elastic-agent:${await getLatestVersion()}`;
|
||||
this.log.info(artifact);
|
||||
// default fleet server policy no longer created by default
|
||||
const {
|
||||
data: {
|
||||
item: { id: policyId },
|
||||
},
|
||||
} = await axios.post(
|
||||
`${this.config.kibanaUrl}/api/fleet/agent_policies`,
|
||||
{
|
||||
name: 'Default Fleet Server policy',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
has_fleet_server: true,
|
||||
},
|
||||
this.requestOptions
|
||||
);
|
||||
|
||||
const host = 'host.docker.internal';
|
||||
const response = await axios.post(
|
||||
`${this.config.kibanaUrl}/api/fleet/service_tokens`,
|
||||
{},
|
||||
this.requestOptions
|
||||
);
|
||||
const serviceToken = response.data.value;
|
||||
const artifact = `docker.elastic.co/beats/elastic-agent:${await getLatestVersion()}`;
|
||||
this.log.info(artifact);
|
||||
|
||||
const args = [
|
||||
'run',
|
||||
'-p',
|
||||
`8220:8220`,
|
||||
'--add-host',
|
||||
'host.docker.internal:host-gateway',
|
||||
'--env',
|
||||
'FLEET_SERVER_ENABLE=true',
|
||||
'--env',
|
||||
`FLEET_SERVER_ELASTICSEARCH_HOST=http://${host}:${this.config.esPort}`,
|
||||
'--env',
|
||||
`FLEET_SERVER_SERVICE_TOKEN=${serviceToken}`,
|
||||
'--env',
|
||||
`FLEET_SERVER_POLICY=${policyId}`,
|
||||
'--rm',
|
||||
artifact,
|
||||
];
|
||||
this.log.info('docker ' + args.join(' '));
|
||||
this.fleetProcess = spawn('docker', args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
this.fleetProcess.on('error', rej);
|
||||
setTimeout(res, 15000);
|
||||
} catch (error) {
|
||||
rej(error);
|
||||
}
|
||||
});
|
||||
const host = 'host.docker.internal';
|
||||
|
||||
const dockerArgs = [
|
||||
'run',
|
||||
'--detach',
|
||||
'-p',
|
||||
`8220:8220`,
|
||||
'--add-host',
|
||||
'host.docker.internal:host-gateway',
|
||||
'--env',
|
||||
'FLEET_SERVER_ENABLE=true',
|
||||
'--env',
|
||||
`FLEET_SERVER_ELASTICSEARCH_HOST=http://${host}:${this.config.esPort}`,
|
||||
'--env',
|
||||
`FLEET_SERVER_SERVICE_TOKEN=${serviceToken}`,
|
||||
'--env',
|
||||
`FLEET_SERVER_POLICY=${policyId}`,
|
||||
'--rm',
|
||||
artifact,
|
||||
];
|
||||
|
||||
this.log.info('docker ' + dockerArgs.join(' '));
|
||||
this.fleetContainerId = (await execa('docker', dockerArgs)).stdout;
|
||||
}
|
||||
|
||||
protected _cleanup() {
|
||||
this.log.info('Removing old fleet config');
|
||||
if (this.fleetProcess) {
|
||||
if (this.fleetContainerId) {
|
||||
this.log.info('Closing fleet process');
|
||||
if (!this.fleetProcess.kill(9)) {
|
||||
this.log.warning('Unable to kill fleet server process');
|
||||
}
|
||||
|
||||
this.fleetProcess.on('close', () => {
|
||||
this.log.info('Fleet server process closed');
|
||||
});
|
||||
delete this.fleetProcess;
|
||||
execa.sync('docker', ['kill', this.fleetContainerId]);
|
||||
this.log.info('Fleet server process closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,7 @@ import { withProcRunner } from '@kbn/dev-proc-runner';
|
|||
|
||||
import { FtrProviderContext } from './ftr_provider_context';
|
||||
|
||||
import {
|
||||
// AgentManager,
|
||||
AgentManagerParams,
|
||||
} from './agent';
|
||||
import { AgentManager, AgentManagerParams } from './agent';
|
||||
import { FleetManager } from './fleet_server';
|
||||
|
||||
async function withFleetAgent(
|
||||
|
@ -47,8 +44,7 @@ async function withFleetAgent(
|
|||
},
|
||||
};
|
||||
const fleetManager = new FleetManager(params, log, requestOptions);
|
||||
|
||||
// const agentManager = new AgentManager(params, log, requestOptions);
|
||||
const agentManager = new AgentManager(params, log, requestOptions);
|
||||
|
||||
// Since the managers will create uncaughtException event handlers we need to exit manually
|
||||
process.on('uncaughtException', (err) => {
|
||||
|
@ -57,13 +53,13 @@ async function withFleetAgent(
|
|||
process.exit(1);
|
||||
});
|
||||
|
||||
// await agentManager.setup();
|
||||
await fleetManager.setup();
|
||||
await agentManager.setup();
|
||||
try {
|
||||
await runner({});
|
||||
} finally {
|
||||
agentManager.cleanup();
|
||||
fleetManager.cleanup();
|
||||
// agentManager.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue