UNPKG

@openzeppelin/defender-serverless

Version:
401 lines (400 loc) 19.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.formatABI = exports.isUnauthorisedError = exports.validateAdditionalPermissionsOrThrow = exports.validateListPermissions = exports.constructSentinel = exports.constructNotificationCategory = exports.constructNotification = exports.getBlockExplorerApiKeyClient = exports.getDeploymentConfigClient = exports.getAdminClient = exports.getRelayClient = exports.getAutotaskClient = exports.getSentinelClient = exports.getTeamAPIkeysOrThrow = exports.isSSOT = exports.getStackName = exports.getResourceID = exports.isTemplateResource = exports.getConsolidatedSecrets = exports.getEquivalentResourceByKey = exports.validateTypesAndSanitise = exports.getEquivalentResource = void 0; const lodash_1 = __importDefault(require("lodash")); const defender_autotask_client_1 = require("@openzeppelin/defender-autotask-client"); const defender_sentinel_client_1 = require("@openzeppelin/defender-sentinel-client"); const defender_relay_client_1 = require("@openzeppelin/defender-relay-client"); const defender_admin_client_1 = require("@openzeppelin/defender-admin-client"); const platform_deploy_client_1 = require("@openzeppelin/platform-deploy-client"); const sanitise_1 = require("./sanitise"); const logger_1 = __importDefault(require("./logger")); /** * @dev this function retrieves the Defender equivalent object of the provided template resource * This will not work for resources that do not have the stackResourceId property, ie. secrets and contracts */ const getEquivalentResource = (context, resource, resources, currentResources) => { if (resource) { const [key, value] = Object.entries(resources ?? []).find(a => lodash_1.default.isEqual(a[1], resource)); return currentResources.find((e) => e.stackResourceId === (0, exports.getResourceID)((0, exports.getStackName)(context), key)); } }; exports.getEquivalentResource = getEquivalentResource; const validateTypesAndSanitise = (o) => { return (0, sanitise_1.sanitise)(o); }; exports.validateTypesAndSanitise = validateTypesAndSanitise; const getEquivalentResourceByKey = (resourceKey, currentResources) => { return currentResources.find((e) => e.stackResourceId === resourceKey); }; exports.getEquivalentResourceByKey = getEquivalentResourceByKey; /** * @dev returns both a list of consolidated secrets for both global and stack, where the latter will be preceded with the stack name. * */ const getConsolidatedSecrets = (context) => { const globalSecrets = context.service.resources?.Resources?.secrets?.global ?? {}; const stackSecrets = context.service.resources?.Resources?.secrets?.stack ?? {}; const stackSecretsPrecededWithStackName = Object.entries(stackSecrets).map(([ssk, ssv]) => { return { [`${(0, exports.getStackName)(context)}_${ssk}`]: ssv, }; }); return lodash_1.default.map(lodash_1.default.entries(Object.assign(globalSecrets, ...stackSecretsPrecededWithStackName)), ([k, v]) => ({ [k]: v, })); }; exports.getConsolidatedSecrets = getConsolidatedSecrets; const isTemplateResource = (context, resource, resourceType, resources) => { return !!Object.entries(resources).find(a => resourceType === 'Secrets' ? // if secret, just compare key Object.keys(a[1])[0] === resource : resourceType === 'Contracts' ? // if contracts, compare network and address a[1].network === resource.network && a[1].address === resource.address : // anything else, compare stackResourceId (0, exports.getResourceID)((0, exports.getStackName)(context), a[0]) === resource.stackResourceId); }; exports.isTemplateResource = isTemplateResource; const getResourceID = (stackName, resourceName) => { return `${stackName}.${resourceName}`; }; exports.getResourceID = getResourceID; const getStackName = (context) => { if (context.service.provider.stackName && typeof context.service.provider.stackName === 'string') { return context.service.provider.stackName; } if (context.service.provider.stage) return `${context.service.service}-${context.service.provider.stage}`; throw new Error(`Unable to get stack name. Missing "provider.stage" property. Alternatively, define "stackName" under "provider" in your serverless.yaml file.`); }; exports.getStackName = getStackName; const isSSOT = (context) => { return !!context.service.provider.ssot; }; exports.isSSOT = isSSOT; const getTeamAPIkeysOrThrow = (context) => { const defenderConfig = context.service.initialServerlessConfig.defender; if (!defenderConfig) throw new Error(`Missing "defender" top-level property in configuration. Please define "defender" with the "key" and "secret" properties in your serverless.yaml file.`); if (!defenderConfig.key || !defenderConfig.secret) throw new Error(`Missing "defender" key or secret properties in configuration. Please define a "key" and "secret" property under "defender" in your serverless.yaml file.`); return { apiKey: defenderConfig.key, apiSecret: defenderConfig.secret }; }; exports.getTeamAPIkeysOrThrow = getTeamAPIkeysOrThrow; const getSentinelClient = (key) => { return new defender_sentinel_client_1.SentinelClient(key); }; exports.getSentinelClient = getSentinelClient; const getAutotaskClient = (key) => { return new defender_autotask_client_1.AutotaskClient(key); }; exports.getAutotaskClient = getAutotaskClient; const getRelayClient = (key) => { return new defender_relay_client_1.RelayClient(key); }; exports.getRelayClient = getRelayClient; const getAdminClient = (key) => { return new defender_admin_client_1.AdminClient(key); }; exports.getAdminClient = getAdminClient; const getDeploymentConfigClient = (key) => { return new platform_deploy_client_1.DeploymentConfigClient(key); }; exports.getDeploymentConfigClient = getDeploymentConfigClient; const getBlockExplorerApiKeyClient = (key) => { return new platform_deploy_client_1.BlockExplorerApiKeyClient(key); }; exports.getBlockExplorerApiKeyClient = getBlockExplorerApiKeyClient; const constructNotification = (notification, stackResourceId) => { const commonNotification = { type: notification.type, name: notification.name, paused: notification.paused, stackResourceId, }; let currentConfig; let config; switch (notification.type) { case 'datadog': currentConfig = notification.config; config = { apiKey: currentConfig['api-key'], metricPrefix: currentConfig['metric-prefix'], }; return { ...commonNotification, config }; case 'discord': currentConfig = notification.config; config = { url: currentConfig.url, }; return { ...commonNotification, config }; case 'email': currentConfig = notification.config; config = { emails: currentConfig.emails, }; return { ...commonNotification, config }; case 'slack': currentConfig = notification.config; config = { url: currentConfig.url, }; return { ...commonNotification, config }; case 'telegram': currentConfig = notification.config; config = { botToken: currentConfig['bot-token'], chatId: currentConfig['chat-id'], }; return { ...commonNotification, config }; case 'opsgenie': currentConfig = notification.config; config = currentConfig; return { ...commonNotification, config }; case 'pager-duty': currentConfig = notification.config; config = currentConfig; return { ...commonNotification, config }; default: throw new Error(`Incompatible notification type ${notification.type}`); } }; exports.constructNotification = constructNotification; const constructNotificationCategory = (context, category, stackResourceId, notifications) => { return { name: category.name, description: category.description, notificationIds: (category['notification-ids'] ? category['notification-ids'] .map(notification => { const maybeNotification = (0, exports.getEquivalentResource)(context, notification, context.service.resources?.Resources?.notifications, notifications); if (maybeNotification) return { notificationId: maybeNotification.notificationId, type: maybeNotification.type, }; }) .filter(isResource) : []), stackResourceId, }; }; exports.constructNotificationCategory = constructNotificationCategory; const isResource = (item) => { return !!item; }; const constructSentinel = (context, stackResourceId, sentinel, notifications, autotasks, blockwatchers, categories) => { const autotaskCondition = sentinel['autotask-condition'] && autotasks.find(a => a.name === sentinel['autotask-condition'].name); const autotaskTrigger = sentinel['autotask-trigger'] && autotasks.find(a => a.name === sentinel['autotask-trigger'].name); const notificationChannels = sentinel['notify-config'].channels .map(notification => { const maybeNotification = (0, exports.getEquivalentResource)(context, notification, context.service.resources?.Resources?.notifications, notifications); return maybeNotification?.notificationId; }) .filter(isResource); const sentinelCategory = sentinel['notify-config'].category; const notificationCategoryId = sentinelCategory && categories.find(c => c.name === sentinelCategory.name)?.categoryId; const commonSentinel = { type: sentinel.type, name: sentinel.name, network: sentinel.network, addresses: sentinel.addresses, abi: sentinel.abi && JSON.stringify(typeof sentinel.abi === 'string' ? JSON.parse(sentinel.abi) : sentinel.abi), paused: sentinel.paused, autotaskCondition: autotaskCondition && autotaskCondition.autotaskId, autotaskTrigger: autotaskTrigger && autotaskTrigger.autotaskId, alertThreshold: sentinel['alert-threshold'] && { amount: sentinel['alert-threshold'].amount, windowSeconds: sentinel['alert-threshold']['window-seconds'], }, alertMessageBody: sentinel['notify-config'].message, alertMessageSubject: sentinel['notify-config']['message-subject'], alertTimeoutMs: sentinel['notify-config'].timeout, notificationChannels, notificationCategoryId: lodash_1.default.isEmpty(notificationChannels) ? notificationCategoryId : undefined, riskCategory: sentinel['risk-category'], stackResourceId: stackResourceId, }; if (sentinel.type === 'FORTA') { const fortaSentinel = { ...commonSentinel, type: 'FORTA', privateFortaNodeId: sentinel['forta-node-id'], agentIDs: sentinel['agent-ids'], fortaConditions: { alertIDs: sentinel.conditions && sentinel.conditions['alert-ids'], minimumScannerCount: (sentinel.conditions && sentinel.conditions['min-scanner-count']) || 1, severity: sentinel.conditions?.severity, }, fortaLastProcessedTime: sentinel['forta-last-processed-time'], }; return fortaSentinel; } if (sentinel.type === 'BLOCK') { const compatibleBlockWatcher = blockwatchers.find(b => b.confirmLevel === sentinel['confirm-level']); if (!compatibleBlockWatcher) { throw new Error(`A blockwatcher with confirmation level (${sentinel['confirm-level']}) does not exist on ${sentinel.network}. Choose another confirmation level.`); } const blockSentinel = { ...commonSentinel, type: 'BLOCK', network: sentinel.network, addresses: sentinel.addresses, confirmLevel: compatibleBlockWatcher.confirmLevel, eventConditions: sentinel.conditions && sentinel.conditions.event && sentinel.conditions.event.map(c => { return { eventSignature: c.signature, expression: c.expression, }; }), functionConditions: sentinel.conditions && sentinel.conditions.function && sentinel.conditions.function.map(c => { return { functionSignature: c.signature, expression: c.expression, }; }), txCondition: sentinel.conditions && sentinel.conditions.transaction, }; return blockSentinel; } throw new Error('Incompatible sentinel type. Type must be either FORTA or BLOCK'); }; exports.constructSentinel = constructSentinel; const validateListPermissions = async (client, resources, resourceType) => { if (!resources) return true; try { switch (resourceType) { case 'Sentinels': await client.list(); break; case 'Autotasks': await client.list(); break; case 'Deployment Configs': await client.list(); break; case 'Relayers': await client.list(); break; case 'Notifications': await client.listNotificationChannels(); break; case 'Categories': await client.listNotificationCategories(); break; case 'Contracts': await client.listContracts(); break; case 'Secrets': await client.listSecrets(); break; case 'Block Explorer Api Keys': await client.list(); break; default: return true; } return true; } catch (e) { // catch the error and verify it is an unauthorised access error if ((0, exports.isUnauthorisedError)(e)) { logger_1.default.getInstance().error(`Unauthorised to access ${resourceType}. Additional API key permissions are required to access this resource.`); return false; } // we throw with original error if its not a permission issue throw e; } }; exports.validateListPermissions = validateListPermissions; const validateAdditionalPermissionsOrThrow = async (context, resources, resourceType) => { if (!resources) return; const teamApiKey = (0, exports.getTeamAPIkeysOrThrow)(context); switch (resourceType) { case 'Sentinels': // Check for access to Autotasks // Enumerate all sentinels, and check if any sentinel has an autotask associated const sentinelsWithAutotasks = Object.values(resources).filter(r => !!r['autotask-condition'] || !!r['autotask-trigger']); // If there are sentinels with autotasks associated, then try to list autotasks if (!lodash_1.default.isEmpty(sentinelsWithAutotasks)) { try { await (0, exports.getAutotaskClient)(teamApiKey).list(); return; } catch (e) { // catch the error and verify it is an unauthorised access error if ((0, exports.isUnauthorisedError)(e)) { // if this fails (due to permissions issue), we re-throw the error with more context throw new Error('At least one Sentinel is associated with an Autotask. Additional API key permissions are required to access Autotasks. Alternatively, remove the association with the autotask to complete this action.'); } // also throw with original error if its not a permission issue throw e; } } case 'Autotasks': // Check for access to Relayers // Enumerate all autotasks, and check if any autotask has a relayer associated const autotasksWithRelayers = Object.values(resources).filter(r => !!r.relayer); // If there are autotasks with relayers associated, then try to list relayers if (!lodash_1.default.isEmpty(autotasksWithRelayers)) { try { await (0, exports.getRelayClient)(teamApiKey).list(); return; } catch (e) { // catch the error and verify it is an unauthorised access error if ((0, exports.isUnauthorisedError)(e)) { // if this fails (due to permissions issue), we re-throw the error with more context throw new Error('At least one Autotask is associated with a Relayer. Additional API key permissions are required to access Relayers. Alternatively, remove the association with the relayer to complete this action.'); } // also throw with original error if its not a permission issue throw e; } } case 'Deployment Configs': // Check for access to Deployment Config try { await (0, exports.getDeploymentConfigClient)(teamApiKey).list(); return; } catch (e) { // catch the error and verify it is an unauthorised access error if ((0, exports.isUnauthorisedError)(e)) { // if this fails (due to permissions issue), we re-throw the error with more context throw new Error('Additional API key permissions ("Manage Deployments") are required to access Deployment Configurations.'); } // also throw with original error if its not a permission issue throw e; } // No other resources require additional permissions default: return; } }; exports.validateAdditionalPermissionsOrThrow = validateAdditionalPermissionsOrThrow; const isUnauthorisedError = (e) => { try { const defenderErrorStatus = e.response.status; return defenderErrorStatus === 403; } catch { // if it is not a DefenderApiError, // the error is most likely caused due to something else return false; } }; exports.isUnauthorisedError = isUnauthorisedError; const formatABI = (abi) => { return abi && JSON.stringify(typeof abi === 'string' ? JSON.parse(abi) : abi); }; exports.formatABI = formatABI;