"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.bulkUpdateAgents = bulkUpdateAgents; exports.closePointInTime = closePointInTime; exports.deleteAgent = deleteAgent; exports.getAgentByAccessAPIKeyId = getAgentByAccessAPIKeyId; exports.getAgentById = getAgentById; exports.getAgentPolicyForAgent = getAgentPolicyForAgent; exports.getAgentTags = getAgentTags; exports.getAgentVersionsForAgentPolicyIds = getAgentVersionsForAgentPolicyIds; exports.getAgents = getAgents; exports.getAgentsById = getAgentsById; exports.getAgentsByKuery = getAgentsByKuery; exports.getAllAgentsByKuery = getAllAgentsByKuery; exports.getElasticsearchQuery = getElasticsearchQuery; exports.openPointInTime = openPointInTime; exports.removeSOAttributes = removeSOAttributes; exports.updateAgent = updateAgent; var _boom = _interopRequireDefault(require("@hapi/boom")); var _esQuery = require("@kbn/es-query"); var _ = require(".."); var _constants = require("../../../common/constants"); var _services = require("../../../common/services"); var _constants2 = require("../../constants"); var _errors = require("../../errors"); var _audit_logging = require("../audit_logging"); var _helpers = require("./helpers"); var _build_status_runtime_field = require("./build_status_runtime_field"); /* * 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. */ const INACTIVE_AGENT_CONDITION = `status:inactive OR status:unenrolled`; const ACTIVE_AGENT_CONDITION = `NOT (${INACTIVE_AGENT_CONDITION})`; function _joinFilters(filters) { try { return filters.filter(filter => filter !== undefined).reduce((acc, kuery) => { if (kuery === undefined) { return acc; } const kueryNode = typeof kuery === 'string' ? (0, _esQuery.fromKueryExpression)(removeSOAttributes(kuery)) : kuery; if (!acc) { return kueryNode; } return { type: 'function', function: 'and', arguments: [acc, kueryNode] }; }, undefined); } catch (err) { throw new _errors.FleetError(`Kuery is malformed: ${err.message}`); } } function removeSOAttributes(kuery) { return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); } async function getAgents(esClient, soClient, options) { let agents = []; if ('agentIds' in options) { agents = (await getAgentsById(esClient, soClient, options.agentIds)).filter(maybeAgent => !('notFound' in maybeAgent)); } else if ('kuery' in options) { var _options$showInactive; agents = (await getAllAgentsByKuery(esClient, soClient, { kuery: options.kuery, showInactive: (_options$showInactive = options.showInactive) !== null && _options$showInactive !== void 0 ? _options$showInactive : false })).agents; } else { throw new _errors.FleetError('Either options.agentIds or options.kuery are required to get agents'); } return agents; } async function openPointInTime(esClient, index = _constants2.AGENTS_INDEX) { const pitKeepAlive = '10m'; const pitRes = await esClient.openPointInTime({ index, keep_alive: pitKeepAlive }); _audit_logging.auditLoggingService.writeCustomAuditLog({ message: `User opened point in time query [index=${index}] [pitId=${pitRes.id}]` }); return pitRes.id; } async function closePointInTime(esClient, pitId) { _audit_logging.auditLoggingService.writeCustomAuditLog({ message: `User closing point in time query [pitId=${pitId}]` }); try { await esClient.closePointInTime({ id: pitId }); } catch (error) { _.appContextService.getLogger().warn(`Error closing point in time with id: ${pitId}. Error: ${error.message}`); } } async function getAgentTags(soClient, esClient, options) { const { kuery, showInactive = false } = options; const filters = []; if (kuery && kuery !== '') { filters.push(kuery); } if (showInactive === false) { filters.push(ACTIVE_AGENT_CONDITION); } const kueryNode = _joinFilters(filters); const body = kueryNode ? { query: (0, _esQuery.toElasticsearchQuery)(kueryNode) } : {}; const runtimeFields = await (0, _build_status_runtime_field.buildAgentStatusRuntimeField)(soClient); try { var _result$aggregations, _buckets$map; const result = await esClient.search({ index: _constants2.AGENTS_INDEX, size: 0, body, fields: Object.keys(runtimeFields), runtime_mappings: runtimeFields, aggs: { tags: { terms: { field: 'tags', size: _constants.SO_SEARCH_LIMIT } } } }); const buckets = (_result$aggregations = result.aggregations) === null || _result$aggregations === void 0 ? void 0 : _result$aggregations.tags.buckets; return (_buckets$map = buckets === null || buckets === void 0 ? void 0 : buckets.map(bucket => bucket.key)) !== null && _buckets$map !== void 0 ? _buckets$map : []; } catch (err) { if ((0, _errors.isESClientError)(err) && err.meta.statusCode === 404) { return []; } throw err; } } function getElasticsearchQuery(kuery, showInactive = false, includeHosted = false, hostedPolicies = [], extraFilters = []) { const filters = []; if (kuery && kuery !== '') { filters.push(kuery); } if (showInactive === false) { filters.push(ACTIVE_AGENT_CONDITION); } if (!includeHosted && hostedPolicies.length > 0) { filters.push('NOT (policy_id:{policyIds})'.replace('{policyIds}', hostedPolicies.join(','))); } filters.push(...extraFilters); const kueryNode = _joinFilters(filters); return kueryNode ? (0, _esQuery.toElasticsearchQuery)(kueryNode) : undefined; } async function getAgentsByKuery(esClient, soClient, options) { var _options$sortField, _options$sortOrder; const { page = 1, perPage = 20, sortField = (_options$sortField = options.sortField) !== null && _options$sortField !== void 0 ? _options$sortField : 'enrolled_at', sortOrder = (_options$sortOrder = options.sortOrder) !== null && _options$sortOrder !== void 0 ? _options$sortOrder : 'desc', kuery, showInactive = false, getStatusSummary = false, showUpgradeable, searchAfter, pitId } = options; const filters = []; if (kuery && kuery !== '') { filters.push(kuery); } if (showInactive === false) { filters.push(ACTIVE_AGENT_CONDITION); } const kueryNode = _joinFilters(filters); const runtimeFields = await (0, _build_status_runtime_field.buildAgentStatusRuntimeField)(soClient); const isDefaultSort = sortField === 'enrolled_at' && sortOrder === 'desc'; // if using default sorting (enrolled_at), adding a secondary sort on hostname, so that the results are not changing randomly in case many agents were enrolled at the same time const secondarySort = isDefaultSort ? [{ 'local_metadata.host.hostname.keyword': { order: 'asc' } }] : []; const statusSummary = { online: 0, error: 0, inactive: 0, offline: 0, updating: 0, unenrolled: 0, degraded: 0, enrolling: 0, unenrolling: 0 }; const queryAgents = async (from, size) => esClient.search({ from, size, track_total_hits: true, rest_total_hits_as_int: true, runtime_mappings: runtimeFields, fields: Object.keys(runtimeFields), sort: [{ [sortField]: { order: sortOrder } }, ...secondarySort], query: kueryNode ? (0, _esQuery.toElasticsearchQuery)(kueryNode) : undefined, ...(pitId ? { pit: { id: pitId, keep_alive: '1m' } } : { index: _constants2.AGENTS_INDEX, ignore_unavailable: true }), ...(pitId && searchAfter ? { search_after: searchAfter, from: 0 } : {}), ...(getStatusSummary && { aggs: { status: { terms: { field: 'status' } } } }) }); let res; try { res = await queryAgents((page - 1) * perPage, perPage); } catch (err) { _.appContextService.getLogger().error(`Error getting agents by kuery: ${JSON.stringify(err)}`); throw err; } let agents = res.hits.hits.map(_helpers.searchHitToAgent); let total = res.hits.total; // filtering for a range on the version string will not work, // nor does filtering on a flattened field (local_metadata), so filter here if (showUpgradeable) { // fixing a bug where upgradeable filter was not returning right results https://github.com/elastic/kibana/issues/117329 // query all agents, then filter upgradeable, and return the requested page and correct total // if there are more than SO_SEARCH_LIMIT agents, the logic falls back to same as before if (total < _constants.SO_SEARCH_LIMIT) { const response = await queryAgents(0, _constants.SO_SEARCH_LIMIT); agents = response.hits.hits.map(_helpers.searchHitToAgent).filter(agent => (0, _services.isAgentUpgradeable)(agent, _.appContextService.getKibanaVersion())); total = agents.length; const start = (page - 1) * perPage; agents = agents.slice(start, start + perPage); } else { agents = agents.filter(agent => (0, _services.isAgentUpgradeable)(agent, _.appContextService.getKibanaVersion())); } } if (getStatusSummary) { var _res$aggregations; (_res$aggregations = res.aggregations) === null || _res$aggregations === void 0 ? void 0 : _res$aggregations.status.buckets.forEach(bucket => { statusSummary[bucket.key] = bucket.doc_count; }); } return { agents, total, page, perPage, ...(getStatusSummary ? { statusSummary } : {}) }; } async function getAllAgentsByKuery(esClient, soClient, options) { const res = await getAgentsByKuery(esClient, soClient, { ...options, page: 1, perPage: _constants.SO_SEARCH_LIMIT }); return { agents: res.agents, total: res.total }; } async function getAgentById(esClient, soClient, agentId) { const [agentHit] = await getAgentsById(esClient, soClient, [agentId]); if ('notFound' in agentHit) { throw new _errors.AgentNotFoundError(`Agent ${agentId} not found`); } return agentHit; } async function _filterAgents(esClient, soClient, query, options = {}) { const { page = 1, perPage = 20, sortField = 'enrolled_at', sortOrder = 'desc' } = options; const runtimeFields = await (0, _build_status_runtime_field.buildAgentStatusRuntimeField)(soClient); let res; try { res = await esClient.search({ from: (page - 1) * perPage, size: perPage, track_total_hits: true, rest_total_hits_as_int: true, runtime_mappings: runtimeFields, fields: Object.keys(runtimeFields), sort: [{ [sortField]: { order: sortOrder } }], query: { bool: { filter: query } }, index: _constants2.AGENTS_INDEX, ignore_unavailable: true }); } catch (err) { _.appContextService.getLogger().error(`Error querying agents: ${JSON.stringify(err)}`); throw err; } const agents = res.hits.hits.map(_helpers.searchHitToAgent); const total = res.hits.total; return { agents, total, page, perPage }; } async function getAgentsById(esClient, soClient, agentIds) { if (!agentIds.length) { return []; } const idsQuery = { terms: { _id: agentIds } }; const { agents } = await _filterAgents(esClient, soClient, idsQuery, { perPage: agentIds.length }); // return agents in the same order as agentIds return agentIds.map(agentId => agents.find(agent => agent.id === agentId) || { id: agentId, notFound: true }); } // given a list of agentPolicyIds, return a map of agent version => count of agents // this is used to get all fleet server versions async function getAgentVersionsForAgentPolicyIds(esClient, agentPolicyIds) { const versionCount = {}; if (!agentPolicyIds.length) { return versionCount; } try { const res = esClient.search({ size: 0, track_total_hits: false, body: { query: { bool: { filter: [{ terms: { policy_id: agentPolicyIds } }] } }, aggs: { agent_versions: { terms: { field: 'local_metadata.elastic.agent.version.keyword', size: 1000 } } } }, index: _constants2.AGENTS_INDEX, ignore_unavailable: true }); const { aggregations } = await res; if (aggregations && aggregations.agent_versions) { aggregations.agent_versions.buckets.forEach(bucket => { versionCount[bucket.key] = bucket.doc_count; }); } } catch (error) { if (error.statusCode !== 404) { throw error; } } return versionCount; } async function getAgentByAccessAPIKeyId(esClient, soClient, accessAPIKeyId) { const query = { term: { access_api_key_id: accessAPIKeyId } }; const { agents } = await _filterAgents(esClient, soClient, query); const agent = agents.length ? agents[0] : null; if (!agent) { throw new _errors.AgentNotFoundError('Agent not found'); } if (agent.access_api_key_id !== accessAPIKeyId) { throw new Error('Agent api key id is not matching'); } if (!agent.active) { throw _boom.default.forbidden('Agent inactive'); } return agent; } async function updateAgent(esClient, agentId, data) { _audit_logging.auditLoggingService.writeCustomAuditLog({ message: `User updated agent [id=${agentId}]` }); await esClient.update({ id: agentId, index: _constants2.AGENTS_INDEX, body: { doc: (0, _helpers.agentSOAttributesToFleetServerAgentDoc)(data) }, refresh: 'wait_for' }); } async function bulkUpdateAgents(esClient, updateData, errors) { if (updateData.length === 0) { return; } const body = updateData.flatMap(({ agentId, data }) => [{ update: { _id: agentId, retry_on_conflict: 5 } }, { doc: { ...(0, _helpers.agentSOAttributesToFleetServerAgentDoc)(data) } }]); const res = await esClient.bulk({ body, index: _constants2.AGENTS_INDEX, refresh: 'wait_for' }); res.items.filter(item => item.update.error).forEach(item => { // @ts-expect-error it not assignable to ErrorCause errors[item.update._id] = item.update.error; }); } async function deleteAgent(esClient, agentId) { try { await esClient.update({ id: agentId, index: _constants2.AGENTS_INDEX, body: { doc: { active: false } } }); } catch (err) { if ((0, _errors.isESClientError)(err) && err.meta.statusCode === 404) { throw new _errors.AgentNotFoundError('Agent not found'); } throw err; } } async function _getAgentDocById(esClient, agentId) { try { const res = await esClient.get({ id: agentId, index: _constants2.AGENTS_INDEX }); if (!res._source) { throw new _errors.AgentNotFoundError(`Agent ${agentId} not found`); } return res._source; } catch (err) { if ((0, _errors.isESClientError)(err) && err.meta.statusCode === 404) { throw new _errors.AgentNotFoundError(`Agent ${agentId} not found`); } throw err; } } async function getAgentPolicyForAgent(soClient, esClient, agentId) { const agent = await _getAgentDocById(esClient, agentId); if (!agent.policy_id) { return; } const agentPolicy = await _.agentPolicyService.get(soClient, agent.policy_id, false); if (agentPolicy) { return agentPolicy; } }