@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
JavaScript
;
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;
}