UNPKG

@openzeppelin/defender-serverless

Version:
889 lines 53.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_1 = __importDefault(require("lodash")); const prompt_1 = __importDefault(require("prompt")); const keccak256_1 = __importDefault(require("keccak256")); const logger_1 = __importDefault(require("../utils/logger")); const utils_1 = require("../utils"); class DefenderDeploy { constructor(serverless, options, logging) { this.serverless = serverless; this.options = options; this.logging = logging; this.log = logger_1.default.getInstance(); this.hooks = { 'before:deploy:deploy': () => this.validateKeys(), 'deploy:deploy': this.requestConfirmation.bind(this), }; } validateKeys() { this.teamKey = (0, utils_1.getTeamAPIkeysOrThrow)(this.serverless); } async getSSOTDifference() { const difference = { sentinels: [], autotasks: [], notifications: [], categories: [], contracts: [], relayerApiKeys: [], secrets: [], deploymentConfigs: [], blockExplorerApiKeys: [], }; let hasPermission = false; // Contracts const contracts = this.serverless.service.resources?.Resources?.contracts ?? []; const adminClient = (0, utils_1.getAdminClient)(this.teamKey); let contractDifference = []; this.log.progress('ssot-difference', `Validating permissions for Contracts`); hasPermission = await (0, utils_1.validateListPermissions)(adminClient, contracts, 'Contracts'); if (hasPermission) { const dContracts = await adminClient.listContracts(); contractDifference = lodash_1.default.differenceWith(dContracts, Object.entries(contracts ?? []), (a, b) => `${a.network}-${a.address}` === `${b[1].network}-${b[1].address}`); } else { this.log.warn(`Skipping validation for Contracts`); } // Sentinels const sentinels = this.serverless.service.resources?.Resources?.sentinels ?? []; const sentinelClient = (0, utils_1.getSentinelClient)(this.teamKey); let sentinelDifference = []; this.log.progress('ssot-difference', `Validating permissions for Sentinels`); hasPermission = await (0, utils_1.validateListPermissions)(sentinelClient, sentinels, 'Sentinels'); if (hasPermission) { const dSentinels = (await sentinelClient.list()).items; sentinelDifference = lodash_1.default.differenceWith(dSentinels, Object.entries(sentinels ?? []), (a, b) => a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0])); } else { this.log.warn(`Skipping validation for Sentinels`); } // Relayers const relayers = this.serverless.service.resources?.Resources?.relayers ?? []; const relayerClient = (0, utils_1.getRelayClient)(this.teamKey); this.log.progress('ssot-difference', `Validating permissions for Relayers`); hasPermission = await (0, utils_1.validateListPermissions)(relayerClient, relayers, 'Relayers'); if (hasPermission) { const dRelayers = (await relayerClient.list()).items; // Relayers API keys await Promise.all(Object.entries(relayers).map(async ([id, relayer]) => { const dRelayer = (0, utils_1.getEquivalentResourceByKey)((0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), id), dRelayers); if (dRelayer) { const dRelayerApiKeys = await relayerClient.listKeys(dRelayer.relayerId); const configuredKeys = relayer['api-keys']; const relayerApiKeyDifference = lodash_1.default.differenceWith(dRelayerApiKeys, configuredKeys, (a, b) => a.stackResourceId === (0, utils_1.getResourceID)(dRelayer.stackResourceId, b)); difference.relayerApiKeys.push(...relayerApiKeyDifference); } })); } else { this.log.warn(`Skipping validation for Relayer API Keys`); } // Notifications const notifications = this.serverless.service.resources?.Resources?.notifications ?? []; let notificationDifference = []; this.log.progress('ssot-difference', `Validating permissions for Notifications`); hasPermission = await (0, utils_1.validateListPermissions)(sentinelClient, notifications, 'Notifications'); if (hasPermission) { const dNotifications = await sentinelClient.listNotificationChannels(); notificationDifference = lodash_1.default.differenceWith(dNotifications, Object.entries(notifications ?? []), (a, b) => a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0])); } else { this.log.warn(`Skipping validation for Notifications`); } // Notification Categories const categories = this.serverless.service.resources?.Resources?.categories ?? []; let categoryDifference = []; this.log.progress('ssot-difference', `Validating permissions for Categories`); hasPermission = await (0, utils_1.validateListPermissions)(sentinelClient, categories, 'Categories'); if (hasPermission) { const dCategories = await sentinelClient.listNotificationCategories(); categoryDifference = lodash_1.default.differenceWith(dCategories, Object.entries(categories ?? []), (a, b) => a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0])); } else { this.log.warn(`Skipping validation for Categories`); } // Autotasks const autotasks = this.serverless.service.functions; const autotaskClient = (0, utils_1.getAutotaskClient)(this.teamKey); let autotaskDifference = []; this.log.progress('ssot-difference', `Validating permissions for Autotasks`); hasPermission = await (0, utils_1.validateListPermissions)(autotaskClient, autotasks, 'Autotasks'); if (hasPermission) { const dAutotasks = (await autotaskClient.list()).items; autotaskDifference = lodash_1.default.differenceWith(dAutotasks, Object.entries(autotasks ?? []), (a, b) => a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0])); } else { this.log.warn(`Skipping validation for Autotasks`); } // Secrets const allSecrets = (0, utils_1.getConsolidatedSecrets)(this.serverless); const dSecrets = (await autotaskClient.listSecrets()).secretNames ?? []; const secretsDifference = lodash_1.default.differenceWith(dSecrets, Object.values(allSecrets).map((k, _) => Object.keys(k)[0] ?? ''), (a, b) => a === b); // Deployment Configs const deploymentConfigs = this.serverless.service.resources?.Resources?.['deployment-configs'] ?? []; const deploymentConfigClient = (0, utils_1.getDeploymentConfigClient)(this.teamKey); let deploymentConfigDifference = []; this.log.progress('ssot-difference', `Validating permissions for Deployment Configs`); hasPermission = await (0, utils_1.validateListPermissions)(deploymentConfigClient, deploymentConfigs, 'Deployment Configs'); if (hasPermission) { const dDeploymentConfigs = await deploymentConfigClient.list(); deploymentConfigDifference = lodash_1.default.differenceWith(dDeploymentConfigs, Object.entries(deploymentConfigs ?? []), (a, b) => a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0])); } else { this.log.warn(`Skipping validation for Deployment Configs`); } // Block Explorer Api Keys const blockExplorerApiKeys = this.serverless.service.resources?.Resources?.['block-explorer-api-keys'] ?? []; const blockExplorerApiKeysClient = (0, utils_1.getBlockExplorerApiKeyClient)(this.teamKey); let blockExplorerApiKeyDifference = []; this.log.progress('ssot-difference', `Validating permissions for Block Explorer Api Keys`); hasPermission = await (0, utils_1.validateListPermissions)(blockExplorerApiKeysClient, blockExplorerApiKeys, 'Block Explorer Api Keys'); if (hasPermission) { const dBlockExplorerApiKeys = await blockExplorerApiKeysClient.list(); blockExplorerApiKeyDifference = lodash_1.default.differenceWith(dBlockExplorerApiKeys, Object.entries(blockExplorerApiKeys ?? []), (a, b) => a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0])); } else { this.log.warn(`Skipping validation for Block Explorer Api Keys`); } this.log.removeProgress('ssot-difference'); difference.contracts = contractDifference; difference.sentinels = sentinelDifference; difference.notifications = notificationDifference; difference.categories = categoryDifference; difference.autotasks = autotaskDifference; difference.secrets = secretsDifference; difference.deploymentConfigs = deploymentConfigDifference; difference.blockExplorerApiKeys = blockExplorerApiKeyDifference; return difference; } async constructConfirmationMessage(withResources) { const start = `You have SSOT enabled. This might remove resources from Defender permanently.\nHaving SSOT enabled will interpret the resources defined in the serverless.yml file as the Single Source Of Truth, and will remove any existing Defender resource which is not defined in the YAML file (with the exception of Relayers).\nIf you continue, the following resources will be removed from Defender:`; const end = `Are you sure you wish to continue [y/n]?`; const formattedResources = { autotasks: withResources.autotasks.length > 0 ? withResources.autotasks.map(a => `${a.stackResourceId ?? a.name} (${a.autotaskId})`) : undefined, sentinels: withResources.sentinels.length > 0 ? withResources.sentinels.map(a => `${a.stackResourceId ?? a.name} (${a.subscriberId})`) : undefined, notifications: withResources.notifications.length > 0 ? withResources.notifications.map(a => `${a.stackResourceId ?? a.name} (${a.notificationId})`) : undefined, contracts: withResources.contracts.length > 0 ? withResources.contracts.map(a => `${a.network}-${a.address} (${a.name})`) : undefined, relayerApiKeys: withResources.relayerApiKeys.length > 0 ? withResources.relayerApiKeys.map(a => `${a.stackResourceId ?? a.apiKey} (${a.keyId})`) : undefined, secrets: withResources.secrets.length > 0 ? withResources.secrets.map(a => `${a}`) : undefined, }; return `${start}\n${lodash_1.default.isEmpty((0, utils_1.validateTypesAndSanitise)(formattedResources)) ? 'None. No differences found.' : JSON.stringify(formattedResources, null, 2)}\n\n${end}`; } async requestConfirmation() { if ((0, utils_1.isSSOT)(this.serverless) && process.stdout.isTTY) { const properties = [ { name: 'confirm', validator: /^(y|n){1}$/i, warning: 'Confirmation must be `y` (yes) or `n` (no)', }, ]; this.log.progress('component-deploy', `Retrieving list of resources`); this.ssotDifference = await this.getSSOTDifference(); this.log.progress('component-deploy', `Awaiting confirmation from user`); prompt_1.default.start({ message: await this.constructConfirmationMessage(this.ssotDifference), }); const { confirm } = await prompt_1.default.get(properties); if (confirm.toString().toLowerCase() !== 'y') { this.log.error('Confirmation not acquired. Terminating command'); return; } this.log.success('Confirmation acquired'); } await this.deploy(); } async deploySecrets(output) { const allSecrets = (0, utils_1.getConsolidatedSecrets)(this.serverless); const client = (0, utils_1.getAutotaskClient)(this.teamKey); const retrieveExisting = () => client.listSecrets().then(r => r.secretNames ?? []); await this.wrapper(this.serverless, 'Secrets', allSecrets, retrieveExisting, // on update async (secret, match) => { await client.createSecrets({ deletes: [], secrets: secret, }); return { name: `Secret`, id: `${match}`, success: true, response: secret, }; }, // on create async (secret, _) => { await client.createSecrets({ deletes: [], secrets: secret, }); return { name: `Secret`, id: `${Object.keys(secret)[0]}`, success: true, response: secret, }; }, // on remove async (secrets) => { await client.createSecrets({ deletes: secrets, secrets: {}, }); }, // overrideMatchDefinition (a, b) => !!b[a], output, this.ssotDifference?.secrets); } async deployContracts(output) { const contracts = this.serverless.service.resources?.Resources?.contracts ?? []; const client = (0, utils_1.getAdminClient)(this.teamKey); const retrieveExisting = () => client.listContracts(); await this.wrapper(this.serverless, 'Contracts', contracts, retrieveExisting, // on update async (contract, match) => { const mappedMatch = { name: match.name, network: match.network, address: match.address, abi: match.abi && JSON.stringify(JSON.parse(match.abi)), 'nat-spec': match.natSpec ? match.natSpec : undefined, }; // in reality this will never be called as long as defender-client does not return ABI as part of the list response if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(contract), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.name, id: `${match.network}-${match.address}`, success: false, response: match, notice: `Skipped import - contract ${match.address} already exists on ${match.network}`, }; } this.log.notice(`Contracts will always update regardless of changes due to certain limitations in Defender API clients.`); const updatedContract = await client.addContract({ name: contract.name, network: match.network, address: match.address, abi: (0, utils_1.formatABI)(contract.abi), natSpec: contract['nat-spec'] ? contract['nat-spec'] : undefined, }); return { name: updatedContract.name, id: `${match.network}-${match.address}`, success: true, response: updatedContract, }; }, // on create async (contract, _) => { const importedContract = await client.addContract({ name: contract.name, network: contract.network, address: contract.address, abi: (0, utils_1.formatABI)(contract.abi), natSpec: contract['nat-spec'] ? contract['nat-spec'] : undefined, }); return { name: importedContract.name, id: `${importedContract.network}-${importedContract.address}`, success: true, response: importedContract, }; }, // on remove async (contracts) => { await Promise.all(contracts.map(async (c) => await client.deleteContract(`${c.network}-${c.address}`))); }, // overrideMatchDefinition (a, b) => { return a.address === b.address && a.network === b.network; }, output, this.ssotDifference?.contracts); } async deployRelayers(output) { const relayers = this.serverless.service.resources?.Resources?.relayers ?? []; const client = (0, utils_1.getRelayClient)(this.teamKey); const retrieveExisting = () => client.list().then(r => r.items); await this.wrapper(this.serverless, 'Relayers', relayers, retrieveExisting, // on update async (relayer, match) => { // Warn users when they try to change the relayer network if (match.network !== relayer.network) { this.log.warn(`Detected a network change from ${match.network} to ${relayer.network} for Relayer: ${match.stackResourceId}. Defender does not currently allow updates to the network once a Relayer is created. This change will be ignored. To enforce this change, remove this relayer and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the relayer. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this relayer.`); relayer.network = match.network; } const mappedMatch = { name: match.name, network: match.network, 'min-balance': parseInt(match.minBalance.toString()), policy: { 'gas-price-cap': match.policies.gasPriceCap, 'whitelist-receivers': match.policies.whitelistReceivers, 'eip1559-pricing': match.policies.EIP1559Pricing, 'private-transactions': match.policies.privateTransactions, }, // currently not supported by defender-client // paused: match.paused }; let updatedRelayer = undefined; if (!lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(lodash_1.default.omit(relayer, ['api-keys', 'address-from-relayer'])), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { updatedRelayer = await client.update(match.relayerId, { name: relayer.name, minBalance: relayer['min-balance'], policies: relayer.policy && { whitelistReceivers: relayer.policy['whitelist-receivers'], gasPriceCap: relayer.policy['gas-price-cap'], EIP1559Pricing: relayer.policy['eip1559-pricing'], privateTransactions: relayer.policy['private-transactions'], }, }); } // check existing keys and remove / create accordingly const existingRelayerKeys = await client.listKeys(match.relayerId); const configuredKeys = relayer['api-keys']; const inDefender = lodash_1.default.differenceWith(existingRelayerKeys, configuredKeys, (a, b) => a.stackResourceId === (0, utils_1.getResourceID)(match.stackResourceId, b)); // delete key in Defender thats not defined in template if ((0, utils_1.isSSOT)(this.serverless) && inDefender.length > 0) { this.log.info(`Unused resources found on Defender:`); this.log.info(JSON.stringify(inDefender, null, 2)); this.log.progress('component-deploy-extra', `Removing resources from Defender`); await Promise.all(inDefender.map(async (key) => await client.deleteKey(match.relayerId, key.keyId))); this.log.success(`Removed resources from Defender`); output.relayerKeys.removed.push(...inDefender); } const inTemplate = lodash_1.default.differenceWith(configuredKeys, existingRelayerKeys, (a, b) => (0, utils_1.getResourceID)(match.stackResourceId, a) === b.stackResourceId); // create key in Defender thats defined in template if (inTemplate) { await Promise.all(inTemplate.map(async (key) => { const keyStackResource = (0, utils_1.getResourceID)(match.stackResourceId, key); const createdKey = await client.createKey(match.relayerId, keyStackResource); this.log.success(`Created API Key (${keyStackResource}) for Relayer (${match.relayerId})`); const keyPath = `${process.cwd()}/.defender/relayer-keys/${keyStackResource}.json`; await this.serverless.utils.writeFile(keyPath, JSON.stringify({ ...createdKey }, null, 2)); this.log.info(`API Key details stored in ${keyPath}`, 1); output.relayerKeys.created.push(createdKey); })); } return { name: match.stackResourceId, id: match.relayerId, success: !!updatedRelayer, response: updatedRelayer ?? match, notice: !updatedRelayer ? `Skipped ${match.stackResourceId} - no changes detected` : undefined, }; }, // on create async (relayer, stackResourceId) => { const relayers = this.serverless.service.resources?.Resources?.relayers ?? []; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, relayer['address-from-relayer'], relayers, existingRelayers); const createdRelayer = await client.create({ name: relayer.name, network: relayer.network, minBalance: relayer['min-balance'], useAddressFromRelayerId: maybeRelayer?.relayerId, policies: relayer.policy && { whitelistReceivers: relayer.policy['whitelist-receivers'], gasPriceCap: relayer.policy['gas-price-cap'], EIP1559Pricing: relayer.policy['eip1559-pricing'], privateTransactions: relayer.policy['private-transactions'], }, stackResourceId, }); const relayerKeys = relayer['api-keys']; if (relayerKeys) { await Promise.all(relayerKeys.map(async (key) => { const keyStackResource = (0, utils_1.getResourceID)(stackResourceId, key); const createdKey = await client.createKey(createdRelayer.relayerId, keyStackResource); this.log.success(`Created API Key (${keyStackResource}) for Relayer (${createdRelayer.relayerId})`); const keyPath = `${process.cwd()}/.defender/relayer-keys/${keyStackResource}.json`; await this.serverless.utils.writeFile(keyPath, JSON.stringify({ ...createdKey }, null, 2)); this.log.info(`API Key details stored in ${keyPath}`, 1); output.relayerKeys.created.push(createdKey); })); } return { name: stackResourceId, id: createdRelayer.relayerId, success: true, response: createdRelayer, }; }, // on remove requires manual interaction undefined, undefined, output); } async deployNotifications(output) { const notifications = this.serverless.service.resources?.Resources?.notifications ?? []; const client = (0, utils_1.getSentinelClient)(this.teamKey); const retrieveExisting = () => client.listNotificationChannels(); await this.wrapper(this.serverless, 'Notifications', notifications, retrieveExisting, // on update async (notification, match) => { const mappedMatch = { type: match.type, name: match.name, config: match.config, paused: match.paused, }; if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(notification), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.stackResourceId, id: match.notificationId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatedNotification = await client.updateNotificationChannel({ ...(0, utils_1.constructNotification)(notification, match.stackResourceId), notificationId: match.notificationId, }); return { name: updatedNotification.stackResourceId, id: updatedNotification.notificationId, success: true, response: updatedNotification, }; }, // on create async (notification, stackResourceId) => { const createdNotification = await client.createNotificationChannel((0, utils_1.constructNotification)(notification, stackResourceId)); return { name: stackResourceId, id: createdNotification.notificationId, success: true, response: createdNotification, }; }, // on remove async (notifications) => { await Promise.all(notifications.map(async (n) => await client.deleteNotificationChannel(n))); }, undefined, output, this.ssotDifference?.notifications); } async deployCategories(output) { const categories = this.serverless.service.resources?.Resources?.categories ?? []; const client = (0, utils_1.getSentinelClient)(this.teamKey); const notifications = await client.listNotificationChannels(); const retrieveExisting = () => client.listNotificationCategories(); await this.wrapper(this.serverless, 'Categories', categories, retrieveExisting, // on update async (category, match) => { const newCategory = (0, utils_1.constructNotificationCategory)(this.serverless, category, match.stackResourceId, notifications); const mappedMatch = { name: match.name, description: match.description, notificationIds: match.notificationIds, stackResourceId: match.stackResourceId, }; if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(newCategory), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.stackResourceId, id: match.categoryId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatedCategory = await client.updateNotificationCategory({ ...newCategory, categoryId: match.categoryId, }); return { name: updatedCategory.stackResourceId, id: updatedCategory.categoryId, success: true, response: updatedCategory, }; }, // on create async (category, stackResourceId) => { return { name: stackResourceId, id: '', success: false, notice: 'Creating custom notification categories is not yet supported', }; // const createdCategory = await client.createNotificationCategory( // constructNotificationCategory(this.serverless, category, stackResourceId, notifications), // ); // return { // name: stackResourceId, // id: createdCategory.categoryId, // success: true, // response: createdCategory, // }; }, // on remove async (categories) => { this.log.warn(`Deleting notification categories is not yet supported.`); // await Promise.all(categories.map(async (n) => await client.deleteNotificationCategory(n.categoryId))); }, // overrideMatchDefinition // TODO: remove this when we allow creating new categories (a, b) => { return a.name === b.name; }, output, this.ssotDifference?.categories); } async deploySentinels(output) { try { const sentinels = this.serverless.service.resources?.Resources?.sentinels ?? []; const client = (0, utils_1.getSentinelClient)(this.teamKey); const autotasks = await (0, utils_1.getAutotaskClient)(this.teamKey).list(); const notifications = await client.listNotificationChannels(); const categories = await client.listNotificationCategories(); const retrieveExisting = () => client.list().then(r => r.items); await this.wrapper(this.serverless, 'Sentinels', sentinels, retrieveExisting, // on update async (sentinel, match) => { const isForta = (o) => o.type === 'FORTA'; const isBlock = (o) => o.type === 'BLOCK'; // Warn users when they try to change the sentinel network if (match.network !== sentinel.network) { this.log.warn(`Detected a network change from ${match.network} to ${sentinel.network} for Sentinel: ${match.stackResourceId}. Defender does not currently allow updates to the network once a Sentinel is created. This change will be ignored. To enforce this change, remove this sentinel and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the sentinel. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this sentinel.`); sentinel.network = match.network; } // Warn users when they try to change the sentinel type if (sentinel.type !== match.type) { this.log.warn(`Detected a type change from ${match.type} to ${sentinel.type} for Sentinel: ${match.stackResourceId}. Defender does not currently allow updates to the type once a Sentinel is created. This change will be ignored. To enforce this change, remove this sentinel and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the sentinel. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this sentinel.`); sentinel.type = match.type; } const blockwatchersForNetwork = (await client.listBlockwatchers()).filter(b => b.network === sentinel.network); const newSentinel = (0, utils_1.constructSentinel)(this.serverless, match.stackResourceId, sentinel, notifications, autotasks.items, blockwatchersForNetwork, categories); // Map match "response" object to that of a "create" object const addressRule = (isBlock(match) && match.addressRules.length > 0 && lodash_1.default.first(match.addressRules)) || undefined; const blockConditions = (addressRule && addressRule.conditions.length > 0 && addressRule.conditions) || undefined; const confirmLevel = (isBlock(match) && match.blockWatcherId.split('-').length > 0 && lodash_1.default.last(match.blockWatcherId.split('-'))) || undefined; const mappedMatch = { name: match.name, abi: addressRule && addressRule.abi, paused: match.paused, alertThreshold: match.alertThreshold, autotaskTrigger: match.notifyConfig?.autotaskId, alertTimeoutMs: match.notifyConfig?.timeoutMs, alertMessageBody: match.notifyConfig?.messageBody, alertMessageSubject: match.notifyConfig?.messageSubject, notificationChannels: match.notifyConfig?.notifications.map((n) => n.notificationId), notificationCategoryId: lodash_1.default.isEmpty(match.notifyConfig?.notifications) ? match.notifyConfig?.notificationCategoryId : undefined, type: match.type, stackResourceId: match.stackResourceId, network: match.network, confirmLevel: (confirmLevel && parseInt(confirmLevel)) || confirmLevel, eventConditions: blockConditions && blockConditions.flatMap((c) => c.eventConditions), functionConditions: blockConditions && blockConditions.flatMap((c) => c.functionConditions), txCondition: blockConditions && blockConditions[0].txConditions.length > 0 && blockConditions[0].txConditions[0].expression, privateFortaNodeId: (isForta(match) && match.privateFortaNodeId) || undefined, addresses: isBlock(match) ? addressRule && addressRule.addresses : match.fortaRule?.addresses, autotaskCondition: isBlock(match) ? addressRule && addressRule.autotaskCondition?.autotaskId : match.fortaRule?.autotaskCondition?.autotaskId, fortaLastProcessedTime: (isForta(match) && match.fortaLastProcessedTime) || undefined, agentIDs: (isForta(match) && match.fortaRule?.agentIDs) || undefined, fortaConditions: (isForta(match) && match.fortaRule.conditions) || undefined, riskCategory: match.riskCategory, }; if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(newSentinel), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.stackResourceId, id: match.subscriberId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatedSentinel = await client.update(match.subscriberId, // Do not allow to update network of (existing) sentinels lodash_1.default.omit(newSentinel, ['network'])); return { name: updatedSentinel.stackResourceId, id: updatedSentinel.subscriberId, success: true, response: updatedSentinel, }; }, // on create async (sentinel, stackResourceId) => { const blockwatchersForNetwork = (await client.listBlockwatchers()).filter(b => b.network === sentinel.network); const createdSentinel = await client.create((0, utils_1.constructSentinel)(this.serverless, stackResourceId, sentinel, notifications, autotasks.items, blockwatchersForNetwork, categories)); return { name: stackResourceId, id: createdSentinel.subscriberId, success: true, response: createdSentinel, }; }, // on remove async (sentinels) => { await Promise.all(sentinels.map(async (s) => await client.delete(s.subscriberId))); }, undefined, output, this.ssotDifference?.sentinels); } catch (e) { this.log.tryLogDefenderError(e); } } async deployAutotasks(output) { const autotasks = this.serverless.service.functions; const client = (0, utils_1.getAutotaskClient)(this.teamKey); const retrieveExisting = () => client.list().then(r => r.items); await this.wrapper(this.serverless, 'Autotasks', autotasks, retrieveExisting, // on update async (autotask, match) => { const relayers = this.serverless.service.resources?.Resources?.relayers ?? []; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, autotask.relayer, relayers, existingRelayers); // Get new code digest const code = await client.getEncodedZippedCodeFromFolder(autotask.path); const newDigest = client.getCodeDigest(code); const { codeDigest } = await client.get(match.autotaskId); const isSchedule = (o) => o.type === 'schedule'; const mappedMatch = { name: match.name, trigger: { type: match.trigger.type, frequency: (isSchedule(match.trigger) && match.trigger.frequencyMinutes) || undefined, cron: (isSchedule(match.trigger) && match.trigger.cron) || undefined, }, paused: match.paused, relayerId: match.relayerId, codeDigest: match.codeDigest, }; if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)({ ...lodash_1.default.omit(autotask, ['events', 'package', 'relayer', 'path']), relayerId: maybeRelayer?.relayerId, codeDigest: newDigest, }), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.stackResourceId, id: match.autotaskId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatesAutotask = await client.update({ autotaskId: match.autotaskId, name: autotask.name, paused: autotask.paused, trigger: { type: autotask.trigger.type, frequencyMinutes: autotask.trigger.frequency ?? undefined, cron: autotask.trigger.cron ?? undefined, }, relayerId: maybeRelayer?.relayerId, }); if (newDigest === codeDigest) { return { name: match.stackResourceId, id: match.autotaskId, success: true, notice: `Skipped code upload - no changes detected for ${match.stackResourceId}`, response: updatesAutotask, }; } else { await client.updateCodeFromFolder(match.autotaskId, autotask.path); return { name: match.stackResourceId, id: match.autotaskId, success: true, response: updatesAutotask, }; } }, // on create async (autotask, stackResourceId) => { const autotaskRelayer = autotask.relayer; const relayers = this.serverless.service.resources?.Resources?.relayers ?? []; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, autotaskRelayer, relayers, existingRelayers); const createdAutotask = await client.create({ name: autotask.name, trigger: { type: autotask.trigger.type, frequencyMinutes: autotask.trigger.frequency ?? undefined, cron: autotask.trigger.cron ?? undefined, }, encodedZippedCode: await client.getEncodedZippedCodeFromFolder(autotask.path), paused: autotask.paused, relayerId: maybeRelayer?.relayerId, stackResourceId: stackResourceId, }); return { name: stackResourceId, id: createdAutotask.autotaskId, success: true, response: createdAutotask, }; }, // on remove async (autotasks) => { await Promise.all(autotasks.map(async (a) => await client.delete(a.autotaskId))); }, undefined, output, this.ssotDifference?.autotasks); } async deployDeploymentConfig(output) { const deploymentConfigs = this.serverless.service.resources?.Resources?.['deployment-configs'] ?? []; const client = (0, utils_1.getDeploymentConfigClient)(this.teamKey); const retrieveExisting = () => client.list(); await this.wrapper(this.serverless, 'Deployment Configs', deploymentConfigs, retrieveExisting, // on update async (deploymentConfig, match) => { const deploymentConfigRelayer = deploymentConfig.relayer; const relayers = this.serverless.service.resources?.Resources?.relayers ?? []; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, deploymentConfigRelayer, relayers, existingRelayers); if (!maybeRelayer) throw new Error(`Cannot find relayer ${deploymentConfigRelayer} in ${match.stackResourceId}`); if (lodash_1.default.isEqual(maybeRelayer.relayerId, match.relayerId)) { return { name: match.stackResourceId, id: match.deploymentConfigId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatedDeploymentConfig = await client.update(match.deploymentConfigId, { relayerId: maybeRelayer.relayerId, stackResourceId: match.stackResourceId, }); return { name: updatedDeploymentConfig.stackResourceId, id: updatedDeploymentConfig.deploymentConfigId, success: true, response: updatedDeploymentConfig, }; }, // on create async (deploymentConfig, stackResourceId) => { const deploymentConfigRelayer = deploymentConfig.relayer; const relayers = this.serverless.service.resources?.Resources?.relayers ?? []; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, deploymentConfigRelayer, relayers, existingRelayers); if (!maybeRelayer) throw new Error(`Cannot find relayer ${deploymentConfigRelayer} in ${stackResourceId}`); const importedDeployment = await client.create({ relayerId: maybeRelayer.relayerId, stackResourceId }); return { name: stackResourceId, id: importedDeployment.deploymentConfigId, success: true, response: importedDeployment, }; }, // on remove async (deploymentConfigs) => { await Promise.all(deploymentConfigs.map(async (c) => await client.remove(c.deploymentConfigId))); }, undefined, output, this.ssotDifference?.deploymentConfigs); } async deployBlockExplorerApiKey(output) { const blockExplorerApiKeys = this.serverless.service.resources?.Resources?.['block-explorer-api-keys'] ?? []; const client = (0, utils_1.getBlockExplorerApiKeyClient)(this.teamKey); const retrieveExisting = () => client.list(); await this.wrapper(this.serverless, 'Block Explorer Api Keys', blockExplorerApiKeys, retrieveExisting, // on update async (blockExplorerApiKey, match) => { if (lodash_1.default.isEqual((0, keccak256_1.default)(blockExplorerApiKey.key).toString('hex'), match.keyHash)) { return { name: match.stackResourceId, id: match.blockExplorerApiKeyId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatedBlockExplorerApiKey = await client.update(match.blockExplorerApiKeyId, { ...blockExplorerApiKey, stackResourceId: match.stackResourceId, }); return { name: updatedBlockExplorerApiKey.stackResourceId, id: updatedBlockExplorerApiKey.blockExplorerApiKeyId, success: true, response: updatedBlockExplorerApiKey, }; }, // on create async (blockExplorerApiKey, stackResourceId) => { const importedBlockExplorerApiKey = await client.create({ ...blockExplorerApiKey, stackResourceId }); return { name: stackResourceId, id: importedBlockExplorerApiKey.blockExplorerApiKeyId, success: true, response: importedBlockExplorerApiKey, }; }, // on remove async (blockExplorerApiKeys) => { await Promise.all(blockExplorerApiKeys.map(async (c) => await client.remove(c.blockExplorerApiKeyId))); }, undefined, output, this.ssotDifference?.blockExplorerApiKeys); } async wrapper(context, resourceType, resources, retrieveExistingResources, onUpdate, onCreate, onRemove, overrideMatchDefinition, output = { removed: [], created: [], updated: [] }, ssotDifference = []) { try { const stackName = (0, utils_1.getStackName)(context); this.log.notice(`${resourceType}`); this.log.progress('component-deploy', `Validating permissions for ${resourceType}`); await (0, utils_1.validateAdditionalPermissionsOrThrow)(context, resources, resourceType); this.log.progress('component-deploy', `Initialising deployment of ${resourceType}`); // only remove if template is considered single source of truth if ((0, utils_1.isSSOT)(context) && onRemove) { if (ssotDifference.length > 0) { this.log.info(`Unused resources found on Defender:`); this.log.info(JSON.stringify(ssotDifference, null, 2)); this.log.progress('component-deploy-extra', `Removing resources from Defender`); await onRemove(ssotDifference); this.log.success(`Removed resources from Defender`); output.removed.push(...ssotDifference); } } for (const [id, resource] of Object.entries(resources ?? [])) { // always refresh list after each addition as some resources rely on the previous one const existing = await retrieveExistingResources(); const entryStackResourceId = (0, utils_1.getResourceID)(stackName, id); let match; if (overrideMatchDefinition) { match = existing.find((e) => overrideMatchDefinition(e, resource)); } else { match = existing.find((e) => e.stackResourceId === entryStackResourceId); } if (match) { this.log.progress('component-deploy-extra', `Updating ${resourceType === 'Contracts' ? match.name : resourceType === 'Secrets' ? match : match.stackResourceId}`); try { const result = await onUpdate(resource, match); if (result.success) { this.log.success(`Updated ${result.name} (${result.id})`); output.updated.push(result.response); } // notice logs requires the --verbose flag if (result.notice) this.log.info(`${result.notice}`, 1); if (result.error) this.log.error(`${result.error}`); } catch (e) { this.log.tryLogDefenderError(e); } } else { this.log.progress('component-deploy-extra', `Creating ${resourceType === 'Secrets'