@openzeppelin/defender-as-code
Version:
Configure your Defender environment via code
851 lines • 72.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const prompt_1 = __importDefault(require("prompt"));
const lodash_1 = __importDefault(require("lodash"));
const logger_1 = __importDefault(require("../utils/logger"));
const utils_1 = require("../utils");
const keccak256_1 = __importDefault(require("keccak256"));
class DefenderDeploy {
constructor(serverless, options, logging) {
this.serverless = serverless;
this.options = options;
this.logging = logging;
this.resources = this.serverless.service.resources;
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 = {
monitors: [],
actions: [],
notifications: [],
contracts: [],
relayerApiKeys: [],
relayerGroupApiKeys: [],
secrets: [],
blockExplorerApiKeys: [],
forkedNetworks: [],
privateNetworks: [],
};
// Contracts
const contracts = this.resources?.contracts ?? {};
const adminClient = (0, utils_1.getProposalClient)(this.teamKey);
const dContracts = await adminClient.listContracts();
const contractDifference = lodash_1.default.differenceWith(dContracts, Object.entries(contracts), (a, b) => {
const contractId = `${a.network}-${a.address}`;
if ((0, utils_1.isDefenderId)(b[1])) {
return contractId === b[1];
}
return contractId === `${b[1].network}-${b[1].address}`;
});
// Forked Networks
const forkedNetworks = this.resources?.['forked-networks'] ?? {};
const networkClient = (0, utils_1.getNetworkClient)(this.teamKey);
const forkedNetworkItems = await networkClient.listForkedNetworks();
const forkedNetworkDifference = lodash_1.default.differenceWith(forkedNetworkItems, Object.entries(forkedNetworks), (a, b) => {
if ((0, utils_1.isDefenderId)(b[1])) {
return a.tenantNetworkId === b[1];
}
return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]);
});
// Private Networks
const privateNetworks = this.resources?.['private-networks'] ?? {};
const privateNetworkItems = await networkClient.listPrivateNetworks();
const privateNetworkDifference = lodash_1.default.differenceWith(privateNetworkItems, Object.entries(privateNetworks), (a, b) => {
if ((0, utils_1.isDefenderId)(b[1])) {
return a.tenantNetworkId === b[1];
}
return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]);
});
// Monitors
const monitors = this.resources?.monitors ?? {};
const monitorClient = (0, utils_1.getMonitorClient)(this.teamKey);
const monitorItems = (await monitorClient.list()).items;
const monitorDifference = lodash_1.default.differenceWith(monitorItems, Object.entries(monitors), (a, b) => {
if ((0, utils_1.isDefenderId)(b[1])) {
return a.monitorId === b[1];
}
return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]);
});
// Relayers
const relayers = this.resources?.relayers ?? {};
const relayerClient = (0, utils_1.getRelayClient)(this.teamKey);
const dRelayers = (await relayerClient.list()).items;
// Relayers API keys
await Promise.all(Object.entries(relayers).map(async ([id, relayer]) => {
if ((0, utils_1.isDefenderId)(relayer))
return;
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);
}
}));
// Relayer Groups
const relayerGroups = this.resources?.['relayer-groups'] ?? {};
const relayerGroupClient = (0, utils_1.getRelayGroupClient)(this.teamKey);
const dRelayerGroups = await relayerGroupClient.list();
// Relayer Group API keys
await Promise.all(Object.entries(relayerGroups).map(async ([id, relayerGroup]) => {
if ((0, utils_1.isDefenderId)(relayerGroup))
return;
const dRelayerGroup = (0, utils_1.getEquivalentResourceByKey)((0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), id), dRelayerGroups);
if (dRelayerGroup) {
const dRelayerGroupApiKeys = await relayerGroupClient.listKeys(dRelayerGroup.relayerGroupId);
const configuredKeys = relayerGroup['api-keys'] ?? [];
const relayerGroupApiKeyDifference = lodash_1.default.differenceWith(dRelayerGroupApiKeys, configuredKeys, (a, b) => a.stackResourceId === (0, utils_1.getResourceID)(dRelayerGroup.stackResourceId, b));
difference.relayerGroupApiKeys.push(...relayerGroupApiKeyDifference);
}
}));
// Notifications
const notifications = this.resources?.notifications ?? {};
const dNotifications = await monitorClient.listNotificationChannels();
const notificationDifference = lodash_1.default.differenceWith(dNotifications, Object.entries(notifications), (a, b) => {
if ((0, utils_1.isDefenderId)(b[1])) {
return a.notificationId === b[1];
}
return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]);
});
// Actions
const actions = this.resources.actions ?? {};
const actionClient = (0, utils_1.getActionClient)(this.teamKey);
const dActions = (await actionClient.list()).items;
const actionDifference = lodash_1.default.differenceWith(dActions, Object.entries(actions), (a, b) => {
if ((0, utils_1.isDefenderId)(b[1])) {
return a.actionId === b[1];
}
return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]);
});
// Secrets
const allSecrets = (0, utils_1.getConsolidatedSecrets)(this.serverless, this.resources);
const dSecrets = (await actionClient.listSecrets()).secretNames ?? [];
const secretsDifference = lodash_1.default.differenceWith(dSecrets, Object.values(allSecrets).map((k, _) => Object.keys(k)[0] ?? ''), (a, b) => a === b);
// Block Explorer Api Keys
const blockExplorerApiKeys = this.resources?.['block-explorer-api-keys'] ?? {};
const deployClient = (0, utils_1.getDeployClient)(this.teamKey);
const dBlockExplorerApiKeys = await deployClient.listBlockExplorerApiKeys();
const blockExplorerApiKeyDifference = lodash_1.default.differenceWith(dBlockExplorerApiKeys, Object.entries(blockExplorerApiKeys ?? []), (a, b) => {
if ((0, utils_1.isDefenderId)(b[1])) {
return a.blockExplorerApiKeyId === b[1];
}
return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]);
});
difference.forkedNetworks = forkedNetworkDifference;
difference.privateNetworks = privateNetworkDifference;
difference.contracts = contractDifference;
difference.monitors = monitorDifference;
difference.notifications = notificationDifference;
difference.actions = actionDifference;
difference.secrets = secretsDifference;
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 = {
actions: withResources.actions.length > 0
? withResources.actions.map((a) => `${a.stackResourceId ?? a.name} (${a.actionId})`)
: undefined,
monitors: withResources.monitors.length > 0
? withResources.monitors.map((a) => `${a.stackResourceId ?? a.name} (${a.monitorId})`)
: 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,
relayerGroupApiKeys: withResources.relayerGroupApiKeys.length > 0
? withResources.relayerGroupApiKeys.map((a) => `${a.stackResourceId ?? a.apiKey} (${a.keyId})`)
: undefined,
secrets: withResources.secrets.length > 0 ? withResources.secrets.map((a) => `${a}`) : undefined,
forkedNetworks: withResources.forkedNetworks.length > 0
? withResources.forkedNetworks.map((a) => `${a.stackResourceId ?? a.name} (${a.tenantNetworkId})`)
: undefined,
privateNetworks: withResources.privateNetworks.length > 0
? withResources.privateNetworks.map((a) => `${a.stackResourceId ?? a.name} (${a.tenantNetworkId})`)
: 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, this.resources);
const client = (0, utils_1.getActionClient)(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.resources?.contracts ?? {};
const client = (0, utils_1.getProposalClient)(this.teamKey);
const retrieveExisting = () => client.listContracts();
await this.wrapper(this.serverless, 'Contracts', (0, utils_1.removeDefenderIdReferences)(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 platform-sdk 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 SDK.`);
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, _stackResourceId) => {
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.resources?.relayers ?? {};
const client = (0, utils_1.getRelayClient)(this.teamKey);
const retrieveExisting = () => client.list().then((r) => r.items);
await this.wrapper(this.serverless, 'Relayers', (0, utils_1.removeDefenderIdReferences)(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-sdk
// 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, {
stackResourceId: 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.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'], // typing address-from-relayer causes issues with schema generation due to circular dependancies so we cast it
relayers, existingRelayers, 'Relayers');
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, {
stackResourceId: 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 deployRelayerGroups(output) {
const relayerGroups = this.resources?.['relayer-groups'] ?? {};
const client = (0, utils_1.getRelayGroupClient)(this.teamKey);
const retrieveExisting = () => client.list();
await this.wrapper(this.serverless, 'Relayer Groups', (0, utils_1.removeDefenderIdReferences)(relayerGroups), retrieveExisting,
// on update
async (relayerGroup, match) => {
// Warn users when they try to change the relayer group network
if (match.network !== relayerGroup.network) {
this.log.warn(`Detected a network change from ${match.network} to ${relayerGroup.network} for Relayer Group: ${match.stackResourceId}. Defender does not currently allow updates to the network once a Relayer Group is created. This change will be ignored. To enforce this change, remove this relayer group and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the relayer group. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this relayer group.`);
relayerGroup.network = match.network;
}
if (match.relayers.length !== relayerGroup.relayers) {
this.log.warn(`Detected a change in the number of relayers from ${match.relayers.length} to ${relayerGroup.relayers} for Relayer Group: ${match.stackResourceId}. Defender does not currently allow updates to the number of relayers once a Relayer Group is created. This change will be ignored. To enforce this change, remove this relayer group and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the relayer group. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this relayer group.`);
relayerGroup.relayers = match.relayers.length;
}
const monitorClient = (0, utils_1.getMonitorClient)(this.teamKey);
const notifications = await monitorClient.listNotificationChannels();
const notificationChannelIds = relayerGroup['notification-channels']?.['notification-ids']
.map((notification) => {
const maybeNotification = (0, utils_1.getEquivalentResource)(this.serverless, notification, this.resources?.notifications, notifications, 'Notifications');
return maybeNotification?.notificationId;
})
.filter(utils_1.isResource);
if (relayerGroup['notification-channels']) {
relayerGroup['notification-channels'] = {
'events': relayerGroup['notification-channels']?.events,
'notification-ids': notificationChannelIds,
};
}
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,
},
'relayers': match.relayers.length,
'notification-channels': match.notificationChannels && {
'events': match.notificationChannels.events,
'notification-ids': match.notificationChannels.notificationIds,
},
// Not yet supported in SDK
// 'user-weight-caps': match.userWeightCaps,
};
let updatedRelayerGroup = undefined;
if (!lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(lodash_1.default.omit(relayerGroup, ['api-keys'])), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) {
updatedRelayerGroup = await client.update(match.relayerGroupId, {
name: relayerGroup.name,
minBalance: relayerGroup['min-balance'],
policies: relayerGroup.policy && {
whitelistReceivers: relayerGroup.policy['whitelist-receivers'],
gasPriceCap: relayerGroup.policy['gas-price-cap'],
EIP1559Pricing: relayerGroup.policy['eip1559-pricing'],
privateTransactions: relayerGroup.policy['private-transactions'],
},
notificationChannels: relayerGroup['notification-channels'] && {
events: relayerGroup['notification-channels'].events,
notificationIds: notificationChannelIds,
},
// Not yet supported in SDK
// userWeightCaps: relayerGroup['user-weight-caps'],
});
}
// check existing keys and remove / create accordingly
const existingRelayerGroupKeys = await client.listKeys(match.relayerGroupId);
const configuredKeys = relayerGroup['api-keys'] ?? [];
const inDefender = lodash_1.default.differenceWith(existingRelayerGroupKeys, 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.relayerGroupId, key.keyId)));
this.log.success(`Removed resources from Defender`);
output.relayerGroupKeys.removed.push(...inDefender);
}
const inTemplate = lodash_1.default.differenceWith(configuredKeys, existingRelayerGroupKeys, (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.relayerGroupId, {
stackResourceId: keyStackResource,
});
this.log.success(`Created API Key (${keyStackResource}) for Relayer Group (${match.relayerGroupId})`);
const keyPath = `${process.cwd()}/.defender/relayer-group-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.relayerGroupKeys.created.push(createdKey);
}));
}
return {
name: match.stackResourceId,
id: match.relayerGroupId,
success: !!updatedRelayerGroup,
response: updatedRelayerGroup ?? match,
notice: !updatedRelayerGroup ? `Skipped ${match.stackResourceId} - no changes detected` : undefined,
};
},
// on create
async (relayerGroup, stackResourceId) => {
const createdRelayerGroup = await client.create({
name: relayerGroup.name,
network: relayerGroup.network,
minBalance: relayerGroup['min-balance'],
policies: relayerGroup.policy && {
whitelistReceivers: relayerGroup.policy['whitelist-receivers'],
gasPriceCap: relayerGroup.policy['gas-price-cap'],
EIP1559Pricing: relayerGroup.policy['eip1559-pricing'],
privateTransactions: relayerGroup.policy['private-transactions'],
},
relayers: relayerGroup.relayers,
stackResourceId,
});
const relayerGroupKeys = relayerGroup['api-keys'];
if (relayerGroupKeys) {
await Promise.all(relayerGroupKeys.map(async (key) => {
const keyStackResource = (0, utils_1.getResourceID)(stackResourceId, key);
const createdKey = await client.createKey(createdRelayerGroup.relayerGroupId, {
stackResourceId: keyStackResource,
});
this.log.success(`Created API Key (${keyStackResource}) for Relayer Group (${createdRelayerGroup.relayerGroupId})`);
const keyPath = `${process.cwd()}/.defender/relayer-group-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.relayerGroupKeys.created.push(createdKey);
}));
}
return {
name: stackResourceId,
id: createdRelayerGroup.relayerGroupId,
success: true,
response: createdRelayerGroup,
};
},
// on remove requires manual interaction
undefined, undefined, output);
}
async deployNotifications(output) {
const notifications = this.resources?.notifications ?? {};
const client = (0, utils_1.getMonitorClient)(this.teamKey);
const retrieveExisting = () => client.listNotificationChannels();
await this.wrapper(this.serverless, 'Notifications', (0, utils_1.removeDefenderIdReferences)(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(match.notificationId, {
...(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.notificationId, n.type)));
}, undefined, output, this.ssotDifference?.notifications);
}
async deployMonitors(output) {
try {
const monitors = this.resources?.monitors ?? {};
const client = (0, utils_1.getMonitorClient)(this.teamKey);
const actions = await (0, utils_1.getActionClient)(this.teamKey).list();
const notifications = await client.listNotificationChannels();
const contracts = await (0, utils_1.getProposalClient)(this.teamKey).listContracts({ includeAbi: true });
const retrieveExisting = () => client.list().then((r) => r.items);
await this.wrapper(this.serverless, 'Monitors', (0, utils_1.removeDefenderIdReferences)(monitors), retrieveExisting,
// on update
async (monitor, match) => {
const isForta = (o) => o.type === 'FORTA';
const isBlock = (o) => o.type === 'BLOCK';
// Warn users when they try to change the monitor network
if (match.network !== monitor.network) {
this.log.warn(`Detected a network change from ${match.network} to ${monitor.network} for Monitor: ${match.stackResourceId}. Defender does not currently allow updates to the network once a Monitor is created. This change will be ignored. To enforce this change, remove this monitor and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the monitor. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this monitor.`);
monitor.network = match.network;
}
// Warn users when they try to change the monitor type
if (monitor.type !== match.type) {
this.log.warn(`Detected a type change from ${match.type} to ${monitor.type} for Monitor: ${match.stackResourceId}. Defender does not currently allow updates to the type once a Monitor is created. This change will be ignored. To enforce this change, remove this monitor and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the monitor. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this monitor.`);
monitor.type = match.type;
}
let blockwatchersForNetwork = [];
// Check if network is forked network
if ((0, utils_1.isTenantNetwork)(monitor.network)) {
blockwatchersForNetwork = (await client.listTenantBlockwatchers()).filter((b) => b.network === monitor.network);
}
else {
blockwatchersForNetwork = (await client.listBlockwatchers()).filter((b) => b.network === monitor.network);
}
const newMonitor = (0, utils_1.constructMonitor)(this.serverless, this.resources, match.stackResourceId, monitor, notifications, actions.items, blockwatchersForNetwork, contracts);
// 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,
skipABIValidation: match.skipABIValidation,
alertThreshold: match.alertThreshold,
actionTrigger: match.notifyConfig?.actionId,
alertTimeoutMs: match.notifyConfig?.timeoutMs,
alertMessageBody: match.notifyConfig?.messageBody,
alertMessageSubject: match.notifyConfig?.messageSubject,
notificationChannels: match.notifyConfig?.notifications.map((n) => n.notificationId),
severityLevel: match.notifyConfig?.severityLevel,
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,
actionCondition: isBlock(match)
? addressRule && addressRule.actionCondition?.actionId
: match.fortaRule?.actionCondition?.actionId,
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)(newMonitor), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) {
return {
name: match.stackResourceId,
id: match.monitorId,
success: false,
response: match,
notice: `Skipped ${match.stackResourceId} - no changes detected`,
};
}
const updatedMonitor = await client.update(match.monitorId, {
// Do not allow to update network of (existing) monitors
...lodash_1.default.omit(newMonitor, ['network']),
monitorId: match.monitorId,
});
return {
name: updatedMonitor.stackResourceId,
id: updatedMonitor.monitorId,
success: true,
response: updatedMonitor,
};
},
// on create
async (monitor, stackResourceId) => {
let blockwatchersForNetwork = [];
// Check if network is forked network
if ((0, utils_1.isTenantNetwork)(monitor.network)) {
blockwatchersForNetwork = (await client.listTenantBlockwatchers()).filter((b) => b.network === monitor.network);
}
else {
blockwatchersForNetwork = (await client.listBlockwatchers()).filter((b) => b.network === monitor.network);
}
const createdMonitor = await client.create((0, utils_1.constructMonitor)(this.serverless, this.resources, stackResourceId, monitor, notifications, actions.items, blockwatchersForNetwork, contracts));
return {
name: stackResourceId,
id: createdMonitor.monitorId,
success: true,
response: createdMonitor,
};
},
// on remove
async (monitors) => {
await Promise.all(monitors.map(async (s) => await client.delete(s.monitorId)));
}, undefined, output, this.ssotDifference?.monitors);
}
catch (e) {
this.log.tryLogDefenderError(e);
}
}
async deployActions(output) {
const actions = this.resources.actions ?? {};
const client = (0, utils_1.getActionClient)(this.teamKey);
const retrieveExisting = () => client.list().then((r) => r.items);
await this.wrapper(this.serverless, 'Actions', (0, utils_1.removeDefenderIdReferences)(actions), retrieveExisting,
// on update
async (action, match) => {
const relayers = this.resources?.relayers ?? {};
const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items;
const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, action.relayer, relayers, existingRelayers, 'Relayers');
// Get new code digest
const code = await client.getEncodedZippedCodeFromFolder(action.path);
const newDigest = client.getCodeDigest({
encodedZippedCode: code,
});
const { codeDigest } = await client.get(match.actionId);
const environmentVariables = await client.getEnvironmentVariables(match.actionId);
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,
'environment-variables': environmentVariables,
};
if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)({
...lodash_1.default.omit(action, ['events', 'package', 'relayer', 'path']),
relayerId: maybeRelayer?.relayerId,
codeDigest: newDigest,
}), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) {
return {
name: match.stackResourceId,
id: match.actionId,
success: false,
response: match,
notice: `Skipped ${match.stackResourceId} - no changes detected`,
};
}
const updatesAction = await client.update({
actionId: match.actionId,
name: action.name,
paused: action.paused,
trigger: {
type: action.trigger.type,
frequencyMinutes: action.trigger.frequency ?? undefined,
cron: action.trigger.cron ?? undefined,
},
relayerId: maybeRelayer?.relayerId,
});
await client.updateEnvironmentVariables(match.actionId, { variables: action['environment-variables'] ?? {} });
if (newDigest === codeDigest) {
return {
name: match.stackResourceId,
id: match.actionId,
success: true,
notice: `Skipped code upload - no code changes detected for ${match.stackResourceId}`,
response: updatesAction,
};
}
else {
await client.updateCodeFromFolder(match.actionId, {
path: action.path,
});
return {
name: match.stackResourceId,
id: match.actionId,
success: true,
response: updatesAction,
};
}
},
// on create
async (action, stackResourceId) => {
const actionRelayer = action.relayer;
const relayers = this.resources?.relayers ?? {};
const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items;
const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, actionRelayer, relayers, existingRelayers, 'Relayers');
const createdAction = await client.create({
name: action.name,
trigger: {
type: action.trigger.type,
frequencyMinutes: action.trigger.frequency ?? undefined,
cron: action.trigger.cron ?? undefined,
},
encodedZippedCode: await client.getEncodedZippedCodeFromFolder(action.path),
paused: action.paused,
relayerId: maybeRelayer?.relayerId,
stackResourceId: stackResourceId,
environmentVariables: action['environment-variables'],
});
return {
name: stackResourceId,
id: createdAction.actionId,
success: true,
response: createdAction,
};
},
// on remove
async (actions) => {
await Promise.all(actions.map(async (a) => await client.delete(a.actionId)));
}, undefined, output, this.ssotDifference?.actions);
}
async deployBlockExplorerApiKey(output) {
const blockExplorerApiKeys = this.resources?.['block-explorer-api-keys'] ?? {};
const client = (0, utils_1.getDeployClient)(this.teamKey);
const retrieveExisting = () => client.listBlockExplorerApiKeys();
await this.wrapper(this.serverless, 'Block Explorer Api Keys', (0, utils_1.removeDefenderIdRe