[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:
Konrad Szwarc 2023-02-28 15:10:12 +01:00 committed by GitHub
parent 2e171759ca
commit 808dca8574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 255 additions and 247 deletions

View file

@ -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;

View file

@ -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');

View file

@ -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 });

View file

@ -29,6 +29,7 @@ describe('ALL - Custom space', () => {
id: CUSTOM_SPACE,
name: CUSTOM_SPACE,
},
failOnStatusCode: false,
headers: { 'kbn-xsrf': 'create-space' },
});
});

View file

@ -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/');
});

View file

@ -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();

View file

@ -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');

View file

@ -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);

View file

@ -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';

View file

@ -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 = () =>

View file

@ -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();
};

View file

@ -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();
};

View file

@ -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 } } },

View file

@ -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,
};
};

View file

@ -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);
});
});
});

View file

@ -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,
}
: {}),
};

View file

@ -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 },

View file

@ -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;
}

View file

@ -6,5 +6,5 @@
*/
export async function getLatestVersion(): Promise<string> {
return '8.6.0-SNAPSHOT';
return '8.8.0-SNAPSHOT';
}

View file

@ -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'
)}`,

View file

@ -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');
}
}
}

View file

@ -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();
}
}