@openzeppelin/defender-serverless
Version:
Configure your Defender environment via code
401 lines (400 loc) • 19.3 kB
JavaScript
;
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;