UNPKG

@aikidosec/firewall

Version:

Zen by Aikido is an embedded Application Firewall that autonomously protects Node.js apps against common and critical attacks, provides rate limiting, detects malicious traffic (including bots), and more.

197 lines (196 loc) 7.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getFlushEveryMS = getFlushEveryMS; exports.getTimeoutInMS = getTimeoutInMS; exports.createLambdaWrapper = createLambdaWrapper; const AgentSingleton_1 = require("../agent/AgentSingleton"); const Context_1 = require("../agent/Context"); const envToBool_1 = require("../helpers/envToBool"); const isJsonContentType_1 = require("../helpers/isJsonContentType"); const isPlainObject_1 = require("../helpers/isPlainObject"); const parseCookies_1 = require("../helpers/parseCookies"); const shouldDiscoverRoute_1 = require("./http-server/shouldDiscoverRoute"); function isAsyncHandler(handler) { return handler.length <= 2; } function convertToAsyncFunction(originalHandler) { // oxlint-disable-next-line require-await return async (event, context) => { if (isAsyncHandler(originalHandler)) { return originalHandler(event, context); } return new Promise((resolve, reject) => { try { originalHandler(event, context, (error, result) => { if (error) { reject(error); } else { resolve(result); } }); } catch (error) { reject(error); } }); }; } function normalizeHeaders(headers) { const normalized = {}; for (const key in headers) { normalized[key.toLowerCase()] = headers[key]; } return normalized; } function tryParseAsJSON(json) { try { return JSON.parse(json); } catch { return undefined; } } function parseBody(event) { const headers = event.headers ? normalizeHeaders(event.headers) : {}; if (!event.body || !(0, isJsonContentType_1.isJsonContentType)(headers["content-type"] || "")) { return undefined; } return tryParseAsJSON(event.body); } function isGatewayEvent(event) { return (0, isPlainObject_1.isPlainObject)(event) && "httpMethod" in event && "headers" in event; } function isGatewayResponse(event) { return ((0, isPlainObject_1.isPlainObject)(event) && "statusCode" in event && typeof event.statusCode === "number"); } function isSQSEvent(event) { return (0, isPlainObject_1.isPlainObject)(event) && "Records" in event; } function getFlushEveryMS() { if (process.env.AIKIDO_LAMBDA_FLUSH_EVERY_MS) { const parsed = parseInt(process.env.AIKIDO_LAMBDA_FLUSH_EVERY_MS, 10); // Minimum is 1 minute if (!isNaN(parsed) && parsed >= 60 * 1000) { return parsed; } } return 10 * 60 * 1000; // 10 minutes } function getTimeoutInMS() { if (process.env.AIKIDO_LAMBDA_TIMEOUT_MS) { const parsed = parseInt(process.env.AIKIDO_LAMBDA_TIMEOUT_MS, 10); // Minimum is 1 second if (!isNaN(parsed) && parsed >= 1000) { return parsed; } } return 1000; // 1 second } // eslint-disable-next-line max-lines-per-function function createLambdaWrapper(handler) { const asyncHandler = convertToAsyncFunction(handler); const agent = (0, AgentSingleton_1.getInstance)(); let lastFlushStatsAt = undefined; let startupEventSent = false; // eslint-disable-next-line max-lines-per-function return async (event, context) => { var _a, _b, _c; // Send startup event on first invocation if (agent && !startupEventSent) { startupEventSent = true; try { await agent.onStart(getTimeoutInMS()); } catch (err) { // eslint-disable-next-line no-console console.error(`Aikido: Failed to start agent: ${err.message}`); } } let agentContext = undefined; if (isSQSEvent(event)) { const body = event.Records.map((record) => tryParseAsJSON(record.body)).filter((body) => body); agentContext = { url: undefined, method: undefined, remoteAddress: undefined, body: { Records: body.map((record) => ({ body: record, })), }, routeParams: {}, headers: {}, query: {}, cookies: {}, source: "lambda/sqs", route: undefined, }; } else if (isGatewayEvent(event)) { agentContext = { url: undefined, method: event.httpMethod, remoteAddress: (_b = (_a = event.requestContext) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.sourceIp, body: parseBody(event), headers: event.headers, routeParams: event.pathParameters ? event.pathParameters : {}, query: event.queryStringParameters ? event.queryStringParameters : {}, cookies: ((_c = event.headers) === null || _c === void 0 ? void 0 : _c.cookie) ? (0, parseCookies_1.parse)(event.headers.cookie) : {}, source: "lambda/gateway", route: event.resource ? event.resource : undefined, }; } if (!agentContext) { // We don't know what the type of the event is // We can't provide any context for the underlying sinks // So we just run the handler without any context logWarningUnsupportedTrigger(); return await asyncHandler(event, context); } let result; try { result = await (0, Context_1.runWithContext)(agentContext, async () => { return await asyncHandler(event, context); }); return result; } finally { if (agent) { if (isGatewayEvent(event) && isGatewayResponse(result) && agentContext.route && agentContext.method) { const shouldDiscover = (0, shouldDiscoverRoute_1.shouldDiscoverRoute)({ statusCode: result.statusCode, method: agentContext.method, route: agentContext.route, }); if (shouldDiscover) { agent.onRouteExecute(agentContext); } } const stats = agent.getInspectionStatistics(); stats.onRequest(); await agent.getPendingEvents().waitUntilSent(getTimeoutInMS()); if (lastFlushStatsAt === undefined || lastFlushStatsAt + getFlushEveryMS() < performance.now()) { await agent.flushStats(getTimeoutInMS()); lastFlushStatsAt = performance.now(); } } } }; } let loggedWarningUnsupportedTrigger = false; function logWarningUnsupportedTrigger() { if (loggedWarningUnsupportedTrigger || (0, envToBool_1.envToBool)(process.env.AIKIDO_LAMBDA_IGNORE_UNSUPPORTED_TRIGGER_WARNING)) { return; } // eslint-disable-next-line no-console console.warn("Zen detected a lambda function call with an unsupported trigger. Only API Gateway and SQS triggers are currently supported."); loggedWarningUnsupportedTrigger = true; }