"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.AlertsClient = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _ruleDataUtils = require("@kbn/rule-data-utils"); var _lodash = require("lodash"); var _coreSavedObjectsUtilsServer = require("@kbn/core-saved-objects-utils-server"); var _legacy_alerts_client = require("./legacy_alerts_client"); var _resource_installer_utils = require("../alerts_service/resource_installer_utils"); var _lib = require("./lib"); /* * 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. */ // Term queries can take up to 10,000 terms const CHUNK_SIZE = 10000; class AlertsClient { // Query for alerts from the previous execution in order to identify the // correct index to use if and when we need to make updates to existing active or // recovered alerts constructor(options) { var _this$options$ruleTyp, _this$options$ruleTyp2; (0, _defineProperty2.default)(this, "legacyAlertsClient", void 0); (0, _defineProperty2.default)(this, "fetchedAlerts", void 0); (0, _defineProperty2.default)(this, "rule", {}); (0, _defineProperty2.default)(this, "ruleType", void 0); (0, _defineProperty2.default)(this, "indexTemplateAndPattern", void 0); (0, _defineProperty2.default)(this, "reportedAlerts", {}); this.options = options; this.legacyAlertsClient = new _legacy_alerts_client.LegacyAlertsClient({ logger: this.options.logger, ruleType: this.options.ruleType }); this.indexTemplateAndPattern = (0, _resource_installer_utils.getIndexTemplateAndPattern)({ context: (_this$options$ruleTyp = this.options.ruleType.alerts) === null || _this$options$ruleTyp === void 0 ? void 0 : _this$options$ruleTyp.context, namespace: (_this$options$ruleTyp2 = this.options.ruleType.alerts) !== null && _this$options$ruleTyp2 !== void 0 && _this$options$ruleTyp2.isSpaceAware ? this.options.namespace : _coreSavedObjectsUtilsServer.DEFAULT_NAMESPACE_STRING }); this.fetchedAlerts = { indices: {}, data: {} }; this.rule = (0, _lib.formatRule)({ rule: this.options.rule, ruleType: this.options.ruleType }); this.ruleType = options.ruleType; } async initializeExecution(opts) { var _this$ruleType$alerts; await this.legacyAlertsClient.initializeExecution(opts); if (!((_this$ruleType$alerts = this.ruleType.alerts) !== null && _this$ruleType$alerts !== void 0 && _this$ruleType$alerts.shouldWrite)) { return; } // Get tracked alert UUIDs to query for // TODO - we can consider refactoring to store the previous execution UUID and query // for active and recovered alerts from the previous execution using that UUID const trackedAlerts = this.legacyAlertsClient.getTrackedAlerts(); const uuidsToFetch = []; (0, _lodash.keys)(trackedAlerts).forEach(key => { const tkey = key; (0, _lodash.keys)(trackedAlerts[tkey]).forEach(alertId => { uuidsToFetch.push(trackedAlerts[tkey][alertId].getUuid()); }); }); if (!uuidsToFetch.length) { return; } const queryByUuid = async uuids => { const result = await this.search({ size: uuids.length, query: { bool: { filter: [{ term: { [_ruleDataUtils.ALERT_RULE_UUID]: this.options.rule.id } }, { terms: { [_ruleDataUtils.ALERT_UUID]: uuids } }] } } }); return result.hits; }; try { const results = await Promise.all((0, _lodash.chunk)(uuidsToFetch, CHUNK_SIZE).map(uuidChunk => queryByUuid(uuidChunk))); for (const hit of results.flat()) { const alertHit = hit._source; const alertUuid = alertHit.kibana.alert.uuid; const alertId = alertHit.kibana.alert.instance.id; // Keep track of existing alert document so we can copy over data if alert is ongoing this.fetchedAlerts.data[alertId] = alertHit; // Keep track of index so we can update the correct document this.fetchedAlerts.indices[alertUuid] = hit._index; } } catch (err) { this.options.logger.error(`Error searching for tracked alerts by UUID - ${err.message}`); } } async search(queryBody) { const esClient = await this.options.elasticsearchClientPromise; const { hits: { hits, total } } = await esClient.search({ index: this.indexTemplateAndPattern.pattern, body: queryBody }); return { hits, total }; } report(alert) { const context = alert.context ? alert.context : {}; const state = !(0, _lodash.isEmpty)(alert.state) ? alert.state : null; // Create a legacy alert const legacyAlert = this.legacyAlertsClient.factory().create(alert.id).scheduleActions(alert.actionGroup, context); if (state) { legacyAlert.replaceState(state); } // Save the alert payload if (alert.payload) { this.reportedAlerts[alert.id] = alert.payload; } return { uuid: legacyAlert.getUuid(), start: legacyAlert.getStart() }; } setAlertData(alert) { const context = alert.context ? alert.context : {}; // Allow setting context and payload on known alerts only // Alerts are known if they have been reported in this execution or are recovered const alertToUpdate = this.legacyAlertsClient.getAlert(alert.id); if (!alertToUpdate) { throw new Error(`Cannot set alert data for alert ${alert.id} because it has not been reported and it is not recovered.`); } // Set the alert context alertToUpdate.setContext(context); // Save the alert payload if (alert.payload) { this.reportedAlerts[alert.id] = alert.payload; } } hasReachedAlertLimit() { return this.legacyAlertsClient.hasReachedAlertLimit(); } checkLimitUsage() { return this.legacyAlertsClient.checkLimitUsage(); } processAndLogAlerts(opts) { this.legacyAlertsClient.processAndLogAlerts(opts); } getProcessedAlerts(type) { return this.legacyAlertsClient.getProcessedAlerts(type); } async persistAlerts() { var _this$ruleType$alerts2; if (!((_this$ruleType$alerts2 = this.ruleType.alerts) !== null && _this$ruleType$alerts2 !== void 0 && _this$ruleType$alerts2.shouldWrite)) { var _this$ruleType$alerts3; this.options.logger.debug(`Resources registered and installed for ${(_this$ruleType$alerts3 = this.ruleType.alerts) === null || _this$ruleType$alerts3 === void 0 ? void 0 : _this$ruleType$alerts3.context} context but "shouldWrite" is set to false.`); return; } const currentTime = new Date().toISOString(); const esClient = await this.options.elasticsearchClientPromise; const { alertsToReturn, recoveredAlertsToReturn } = this.legacyAlertsClient.getAlertsToSerialize(false); const activeAlerts = this.legacyAlertsClient.getProcessedAlerts('active'); const recoveredAlerts = this.legacyAlertsClient.getProcessedAlerts('recovered'); // TODO - Lifecycle alerts set some other fields based on alert status // Example: workflow status - default to 'open' if not set // event action: new alert = 'new', active alert: 'active', otherwise 'close' const activeAlertsToIndex = []; for (const id of (0, _lodash.keys)(alertsToReturn)) { // See if there's an existing active alert document if (!!activeAlerts[id]) { if (this.fetchedAlerts.data.hasOwnProperty(id) && this.fetchedAlerts.data[id].kibana.alert.status === 'active') { activeAlertsToIndex.push((0, _lib.buildOngoingAlert)({ alert: this.fetchedAlerts.data[id], legacyAlert: activeAlerts[id], rule: this.rule, timestamp: currentTime, payload: this.reportedAlerts[id], kibanaVersion: this.options.kibanaVersion })); } else { activeAlertsToIndex.push((0, _lib.buildNewAlert)({ legacyAlert: activeAlerts[id], rule: this.rule, timestamp: currentTime, payload: this.reportedAlerts[id], kibanaVersion: this.options.kibanaVersion })); } } else { this.options.logger.error(`Error writing alert(${id}) to ${this.indexTemplateAndPattern.alias} - alert(${id}) doesn't exist in active alerts`); } } const recoveredAlertsToIndex = []; for (const id of (0, _lodash.keys)(recoveredAlertsToReturn)) { // See if there's an existing alert document // If there is not, log an error because there should be if (this.fetchedAlerts.data.hasOwnProperty(id)) { recoveredAlertsToIndex.push(recoveredAlerts[id] ? (0, _lib.buildRecoveredAlert)({ alert: this.fetchedAlerts.data[id], legacyAlert: recoveredAlerts[id], rule: this.rule, timestamp: currentTime, payload: this.reportedAlerts[id], recoveryActionGroup: this.options.ruleType.recoveryActionGroup.id, kibanaVersion: this.options.kibanaVersion }) : (0, _lib.buildUpdatedRecoveredAlert)({ alert: this.fetchedAlerts.data[id], legacyRawAlert: recoveredAlertsToReturn[id], timestamp: currentTime, rule: this.rule })); } else { this.options.logger.warn(`Could not find alert document to update for recovered alert with id ${id} and uuid ${recoveredAlerts[id].getUuid()}`); } } const alertsToIndex = [...activeAlertsToIndex, ...recoveredAlertsToIndex]; if (alertsToIndex.length > 0) { try { const response = await esClient.bulk({ refresh: 'wait_for', index: this.indexTemplateAndPattern.alias, require_alias: true, body: (0, _lodash.flatMap)([...activeAlertsToIndex, ...recoveredAlertsToIndex].map(alert => [{ index: { _id: alert.kibana.alert.uuid, // If we know the concrete index for this alert, specify it ...(this.fetchedAlerts.indices[alert.kibana.alert.uuid] ? { _index: this.fetchedAlerts.indices[alert.kibana.alert.uuid], require_alias: false } : {}) } }, alert])) }); // If there were individual indexing errors, they will be returned in the success response if (response && response.errors) { var _response$items; const errorsInResponse = ((_response$items = response.items) !== null && _response$items !== void 0 ? _response$items : []).map(item => item && item.index && item.index.error ? item.index.error : null).filter(item => item != null); this.options.logger.error(`Error writing ${errorsInResponse.length} out of ${alertsToIndex.length} alerts - ${JSON.stringify(errorsInResponse)}`); } } catch (err) { this.options.logger.error(`Error writing ${alertsToIndex.length} alerts to ${this.indexTemplateAndPattern.alias} - ${err.message}`); } } } getAlertsToSerialize() { // The flapping value that is persisted inside the task manager state (and used in the next execution) // is different than the value that should be written to the alert document. For this reason, we call // getAlertsToSerialize() twice, once before building and bulk indexing alert docs and once after to return // the value for task state serialization // This will be a blocker if ever we want to stop serializing alert data inside the task state and just use // the fetched alert document. return this.legacyAlertsClient.getAlertsToSerialize(); } factory() { return this.legacyAlertsClient.factory(); } async getSummarizedAlerts({ ruleId, spaceId, excludedAlertInstanceIds, alertsFilter, start, end, executionUuid }) { var _this$ruleType$alerts4, _this$ruleType$autoRe; if (!ruleId || !spaceId) { throw new Error(`Must specify both rule ID and space ID for AAD alert query.`); } const queryByExecutionUuid = !!executionUuid; const queryByTimeRange = !!start && !!end; // Either executionUuid or start/end dates must be specified, but not both if (!queryByExecutionUuid && !queryByTimeRange || queryByExecutionUuid && queryByTimeRange) { throw new Error(`Must specify either execution UUID or time range for AAD alert query.`); } const getQueryParams = { executionUuid, start, end, ruleId, excludedAlertInstanceIds, alertsFilter }; const formatAlert = (_this$ruleType$alerts4 = this.ruleType.alerts) === null || _this$ruleType$alerts4 === void 0 ? void 0 : _this$ruleType$alerts4.formatAlert; const isLifecycleAlert = (_this$ruleType$autoRe = this.ruleType.autoRecoverAlerts) !== null && _this$ruleType$autoRe !== void 0 ? _this$ruleType$autoRe : false; if (isLifecycleAlert) { const queryBodies = (0, _lib.getLifecycleAlertsQueries)(getQueryParams); const responses = await Promise.all(queryBodies.map(queryBody => this.search(queryBody))); return { new: (0, _lib.getHitsWithCount)(responses[0], formatAlert), ongoing: (0, _lib.getHitsWithCount)(responses[1], formatAlert), recovered: (0, _lib.getHitsWithCount)(responses[2], formatAlert) }; } const response = await this.search((0, _lib.getContinualAlertsQuery)(getQueryParams)); return { new: (0, _lib.getHitsWithCount)(response, formatAlert), ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] } }; } client() { return { report: alert => this.report(alert), setAlertData: alert => this.setAlertData(alert), getAlertLimitValue: () => this.factory().alertLimit.getValue(), setAlertLimitReached: reached => this.factory().alertLimit.setLimitReached(reached), getRecoveredAlerts: () => { var _getRecoveredAlerts; const { getRecoveredAlerts } = this.factory().done(); const recoveredLegacyAlerts = (_getRecoveredAlerts = getRecoveredAlerts()) !== null && _getRecoveredAlerts !== void 0 ? _getRecoveredAlerts : []; return recoveredLegacyAlerts.map(alert => ({ alert, hit: this.fetchedAlerts.data[alert.getId()] })); } }; } } exports.AlertsClient = AlertsClient;