From 882dc4c1872383640fe42b6f0706e1985f8eb0ce Mon Sep 17 00:00:00 2001 From: Daniel-Eisenberg <59121493+Daniel-Eisenberg@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:58:41 +0300 Subject: [PATCH] Add kong plugin (#36) * add kong plugin to open-appsec * fix url in rockspec file * add the attachment prefix to the paths * check * fix branch --------- Co-authored-by: wiaamm --- attachments/kong/handler.lua | 171 ++++++ attachments/kong/lua_attachment_wrapper.c | 562 ++++++++++++++++++ attachments/kong/nano_ffi.lua | 426 +++++++++++++ ...en-appsec-waf-kong-plugin-1.0.0-1.rockspec | 57 ++ attachments/kong/schema.lua | 16 + 5 files changed, 1232 insertions(+) create mode 100755 attachments/kong/handler.lua create mode 100755 attachments/kong/lua_attachment_wrapper.c create mode 100755 attachments/kong/nano_ffi.lua create mode 100755 attachments/kong/open-appsec-waf-kong-plugin-1.0.0-1.rockspec create mode 100755 attachments/kong/schema.lua diff --git a/attachments/kong/handler.lua b/attachments/kong/handler.lua new file mode 100755 index 0000000..77edbf2 --- /dev/null +++ b/attachments/kong/handler.lua @@ -0,0 +1,171 @@ +local nano = require "kong.plugins.open-appsec-waf-kong-plugin.nano_ffi" +local kong = kong + +local NanoHandler = {} + +NanoHandler.PRIORITY = 3000 +NanoHandler.VERSION = "1.0.0" + +NanoHandler.sessions = {} +NanoHandler.processed_requests = {} -- Track processed requests + +-- **Handles worker initialization** +function NanoHandler.init_worker() + nano.init_attachment() +end + +-- **Handles Request Headers (DecodeHeaders Equivalent)** +function NanoHandler.access(conf) + local headers = kong.request.get_headers() + local session_id = nano.generate_session_id() + kong.service.request.set_header("x-session-id", tostring(session_id)) + + if NanoHandler.processed_requests[session_id] then + kong.ctx.plugin.blocked = true + return + end + + local session_data = nano.init_session(session_id) + if not session_data then + kong.log.err("Failed to initialize session") + kong.ctx.plugin.blocked = true + return kong.response.exit(500, { message = "Session Initialization Failed" }) + end + + kong.ctx.plugin.session_data = session_data + kong.ctx.plugin.session_id = session_id + + local meta_data = nano.handle_start_transaction() + local req_headers = nano.handleHeaders(headers) + + local has_content_length = tonumber(ngx.var.http_content_length) and tonumber(ngx.var.http_content_length) > 0 + local contains_body = has_content_length and 1 or 0 + + local verdict, response = nano.send_data(session_id, session_data, meta_data, req_headers, contains_body, nano.HttpChunkType.HTTP_REQUEST_FILTER) + if verdict == nano.AttachmentVerdict.DROP then + nano.fini_session(session_data) + kong.ctx.plugin.blocked = true + return nano.handle_custom_response(session_data, response) + end + + if contains_body == 1 then + local body = kong.request.get_raw_body() + if body and #body > 0 then + verdict, response = nano.send_body(session_id, session_data, body, nano.HttpChunkType.HTTP_REQUEST_BODY) + if verdict == nano.AttachmentVerdict.DROP then + nano.fini_session(session_data) + kong.ctx.plugin.blocked = true + return nano.handle_custom_response(session_data, response) + end + end + + local ok, verdict, response = pcall(function() + return nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_REQUEST_END) + end) + + if not ok then + kong.log.err("Error ending request inspection: ", verdict) + nano.fini_session(session_data) + kong.ctx.plugin.blocked = true + return kong.response.exit(500, { message = "Error completing request processing" }) + end + + + if verdict == nano.AttachmentVerdict.DROP then + nano.fini_session(session_data) + kong.ctx.plugin.blocked = true + return nano.handle_custom_response(session_data, response) + end + else + verdict, response = nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_REQUEST_END) + if verdict == nano.AttachmentVerdict.DROP then + nano.fini_session(session_data) + kong.ctx.plugin.blocked = true + return nano.handle_custom_response(session_data, response) + end + end + + NanoHandler.processed_requests[session_id] = true +end + +function NanoHandler.header_filter(conf) + local ctx = kong.ctx.plugin + if ctx.blocked then + return + end + + local session_id = ctx.session_id + local session_data = ctx.session_data + + if not session_id or not session_data then + kong.log.err("No session data found in header_filter") + return + end + + local headers = kong.response.get_headers() + local header_data = nano.handleHeaders(headers) + local status_code = kong.response.get_status() + local content_length = tonumber(headers["content-length"]) or 0 + + local verdict, response = nano.send_response_headers(session_id, session_data, header_data, status_code, content_length) + if verdict == nano.AttachmentVerdict.DROP then + kong.ctx.plugin.blocked = true + nano.fini_session(session_data) + return nano.handle_custom_response(session_data, response) + end + + ctx.expect_body = not (status_code == 204 or status_code == 304 or (100 <= status_code and status_code < 200) or content_length == 0) +end + +function NanoHandler.body_filter(conf) + local ctx = kong.ctx.plugin + if ctx.blocked then + return + end + + local session_id = ctx.session_id + local session_data = ctx.session_data + + if not session_id or not session_data or ctx.session_finalized then + return + end + + local body = kong.response.get_raw_body() + + if body then + ctx.body_seen = true + local verdict, response, modifications = nano.send_body(session_id, session_data, body, nano.HttpChunkType.HTTP_RESPONSE_BODY) + + -- Initialize if not exists + ctx.body_buffer_chunk = ctx.body_buffer_chunk or 0 + + -- Handle body modifications if any + if modifications then + body = nano.handle_body_modifications(body, modifications, ctx.body_buffer_chunk) + kong.response.set_raw_body(body) + end + + ctx.body_buffer_chunk = ctx.body_buffer_chunk + 1 + + if verdict == nano.AttachmentVerdict.DROP then + nano.fini_session(session_data) + ctx.session_finalized = true + return nano.handle_custom_response(session_data, response) + end + return + end + + if ctx.body_seen or ctx.expect_body == false then + local verdict, response = nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_RESPONSE_END) + if verdict == nano.AttachmentVerdict.DROP then + nano.fini_session(session_data) + ctx.session_finalized = true + return nano.handle_custom_response(session_data, response) + end + + nano.fini_session(session_data) + ctx.session_finalized = true + end +end + +return NanoHandler diff --git a/attachments/kong/lua_attachment_wrapper.c b/attachments/kong/lua_attachment_wrapper.c new file mode 100755 index 0000000..4624474 --- /dev/null +++ b/attachments/kong/lua_attachment_wrapper.c @@ -0,0 +1,562 @@ +#include +#include +#include +#include +#include +#include "nano_attachment.h" +#include "nano_attachment_common.h" + +#define MAX_HEADERS 10000 + +// Initialize NanoAttachment for worker +static int lua_init_nano_attachment(lua_State *L) { + int worker_id = luaL_checkinteger(L, 1); + int num_workers = luaL_checkinteger(L, 2); + + NanoAttachment* attachment = InitNanoAttachment(0, worker_id, num_workers, fileno(stdout)); + if (!attachment) { + lua_pushnil(L); + lua_pushstring(L, "Failed to initialize NanoAttachment"); + return 2; + } + + lua_pushlightuserdata(L, attachment); + return 1; +} + +static int lua_get_web_response_type(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*)lua_touserdata(L, 1); + HttpSessionData* session_data = (HttpSessionData*)lua_touserdata(L, 2); + AttachmentVerdictResponse* response = (AttachmentVerdictResponse*)lua_touserdata(L, 3); + + if (!attachment || !session_data || !response) { + return luaL_error(L, "invalid args to get_web_response_type"); + } + + NanoWebResponseType type = GetWebResponseType(attachment, session_data, response); + lua_pushinteger(L, type); + return 1; +} + + +static int lua_get_response_code(lua_State *L) { + AttachmentVerdictResponse* response = (AttachmentVerdictResponse*)lua_touserdata(L, 1); + if (!response) { + return luaL_error(L, "invalid response"); + } + + int code = GetResponseCode(response); + lua_pushinteger(L, code); + return 1; +} + +static int lua_get_block_page(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*)lua_touserdata(L, 1); + HttpSessionData* session_data = (HttpSessionData*)lua_touserdata(L, 2); + AttachmentVerdictResponse* response = (AttachmentVerdictResponse*)lua_touserdata(L, 3); + + if (!attachment || !session_data || !response) { + return luaL_error(L, "invalid args to get_block_page"); + } + + BlockPageData page = GetBlockPage(attachment, session_data, response); + size_t size = page.title_prefix.len + page.title.len + + page.body_prefix.len + page.body.len + + page.uuid_prefix.len + page.uuid.len + page.uuid_suffix.len; + + char *result = malloc(size + 1); + if (!result) { + return luaL_error(L, "memory allocation failed"); + } + + int offset = 0; + memcpy(result + offset, page.title_prefix.data, page.title_prefix.len); offset += page.title_prefix.len; + memcpy(result + offset, page.title.data, page.title.len); offset += page.title.len; + memcpy(result + offset, page.body_prefix.data, page.body_prefix.len); offset += page.body_prefix.len; + memcpy(result + offset, page.body.data, page.body.len); offset += page.body.len; + memcpy(result + offset, page.uuid_prefix.data, page.uuid_prefix.len); offset += page.uuid_prefix.len; + memcpy(result + offset, page.uuid.data, page.uuid.len); offset += page.uuid.len; + memcpy(result + offset, page.uuid_suffix.data, page.uuid_suffix.len); offset += page.uuid_suffix.len; + result[size] = '\0'; + + lua_pushlstring(L, result, size); + free(result); + return 1; +} + +static int lua_get_redirect_page(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*)lua_touserdata(L, 1); + HttpSessionData* session_data = (HttpSessionData*)lua_touserdata(L, 2); + AttachmentVerdictResponse* response = (AttachmentVerdictResponse*)lua_touserdata(L, 3); + + if (!attachment || !session_data || !response) { + return luaL_error(L, "invalid args to get_redirect_page"); + } + + RedirectPageData data = GetRedirectPage(attachment, session_data, response); + lua_pushlstring(L, (const char*)data.redirect_location.data, data.redirect_location.len); + return 1; +} + + +// Allocate memory for nano_str_t (long-lived, must be freed later) +static int lua_createNanoStrAlloc(lua_State *L) { + const char* str = luaL_checkstring(L, 1); + if (!str) { + lua_pushnil(L); + lua_pushstring(L, "Invalid string input"); + return 2; + } + + char* c_str = strdup(str); + if (!c_str) { + lua_pushnil(L); + lua_pushstring(L, "Failed to allocate memory for string"); + return 2; +} + + nano_str_t* nanoStr = (nano_str_t*)malloc(sizeof(nano_str_t)); + if (!nanoStr) { + free(c_str); // Clean up already allocated string + lua_pushnil(L); + lua_pushstring(L, "Failed to allocate memory for nano_str_t"); + return 2; + } + + nanoStr->len = strlen(str); + nanoStr->data = (unsigned char*)c_str; + + lua_pushlightuserdata(L, nanoStr); + return 1; +} + +// Free nano_str_t (Memory cleanup) +static int lua_freeNanoStr(lua_State *L) { + nano_str_t* nanoStr = (nano_str_t*)lua_touserdata(L, 1); + if (nanoStr) { + free(nanoStr->data); + free(nanoStr); + } + return 0; +} + +// Allocate memory for HttpHeaders +// Allocate memory for HttpHeaders +static int lua_allocHttpHeaders(lua_State *L) { + size_t max_headers = 10000; + + HttpHeaders* headers = (HttpHeaders*)malloc(sizeof(HttpHeaders)); + if (!headers) { + return luaL_error(L, "Memory allocation failed for HttpHeaders"); + } + + headers->data = (HttpHeaderData*)malloc(max_headers * sizeof(HttpHeaderData)); + if (!headers->data) { + free(headers); + return luaL_error(L, "Memory allocation failed for HttpHeaderData"); + } + + headers->headers_count = 0; + + lua_pushlightuserdata(L, headers); + return 1; +} + +// Free HttpHeaders memory +static int lua_freeHttpHeaders(lua_State *L) { + HttpHeaders* headers = (HttpHeaders*)lua_touserdata(L, 1); + if (headers) { + free(headers->data); + free(headers); + } + return 0; +} + +// Set the headers_count in HttpHeaders +static int lua_setHeaderCount(lua_State *L) { + HttpHeaders* headers = (HttpHeaders*)lua_touserdata(L, 1); + int count = luaL_checkinteger(L, 2); + + if (!headers) { + return 0; + } + + headers->headers_count = count; + return 0; +} + +// Helper function to convert Lua string to nano_str_t +static void lua_fill_nano_str(lua_State *L, int index, nano_str_t *nano_str) { + size_t len; + const char *str = luaL_checklstring(L, index, &len); + + if (!str) { + nano_str->data = NULL; + nano_str->len = 0; + return; + } + + // Allocate memory + 1 for null termination + nano_str->data = (char *)malloc(len + 1); + + if (!nano_str->data) { // Check if allocation failed + nano_str->len = 0; + return; + } + + memcpy(nano_str->data, str, len); // Copy exact `len` bytes + nano_str->data[len] = '\0'; // Manually null-terminate + nano_str->len = len; +} + +static int lua_free_http_metadata(lua_State *L) { + HttpMetaData *metadata = *(HttpMetaData **)lua_touserdata(L, 1); + if (!metadata) return 0; + + free(metadata->http_protocol.data); + free(metadata->method_name.data); + free(metadata->host.data); + free(metadata->listening_ip.data); + free(metadata->uri.data); + free(metadata->client_ip.data); + free(metadata->parsed_host.data); + free(metadata->parsed_uri.data); + free(metadata); + + return 0; +} + +// Set a header element in HttpHeaderData +static int lua_setHeaderElement(lua_State *L) { + HttpHeaders *headers = (HttpHeaders *)lua_touserdata(L, 1); + int index = luaL_checkinteger(L, 2); + + if (!headers || index >= MAX_HEADERS) { + lua_pushboolean(L, 0); + return 1; + } + + // Safely allocate and set header key/value + lua_fill_nano_str(L, 3, &headers->data[index].key); + lua_fill_nano_str(L, 4, &headers->data[index].value); + + lua_pushboolean(L, 1); + return 1; +} + +// Initialize session +static int lua_init_session(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*) lua_touserdata(L, 1); + SessionID session_id = luaL_checkinteger(L, 2); + + if (!attachment) { + lua_pushnil(L); + lua_pushstring(L, "Invalid nano_attachment"); + return 2; + } + + HttpSessionData* session_data = InitSessionData(attachment, session_id); + if (!session_data) { + lua_pushnil(L); + lua_pushstring(L, "Failed to initialize session data"); + return 2; + } + + lua_pushlightuserdata(L, session_data); + return 1; +} + +// Finalize session +static int lua_fini_session(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*) lua_touserdata(L, 1); + HttpSessionData* session_data = lua_touserdata(L, 2); + + if (!attachment || !session_data) { + lua_pushnil(L); + lua_pushstring(L, "Error: Invalid attachment or session_data"); + return 2; + } + + FiniSessionData(attachment, session_data); + lua_pushboolean(L, 1); + return 1; +} + +// Check if session is finalized +static int lua_is_session_finalized(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*) lua_touserdata(L, 1); + HttpSessionData* session_data = (HttpSessionData*) lua_touserdata(L, 2); + + if (!attachment || !session_data) { + lua_pushboolean(L, 0); + return 1; + } + + int result = IsSessionFinalized(attachment, session_data); + lua_pushboolean(L, result); + return 1; +} + +// Function to extract request metadata and create HttpMetaData struct +static int lua_create_http_metadata(lua_State *L) { + HttpMetaData *metadata = (HttpMetaData *)malloc(sizeof(HttpMetaData)); + if (!metadata) { + return luaL_error(L, "Memory allocation failed"); + } + + lua_fill_nano_str(L, 1, &metadata->http_protocol); + lua_fill_nano_str(L, 2, &metadata->method_name); + lua_fill_nano_str(L, 3, &metadata->host); + lua_fill_nano_str(L, 4, &metadata->listening_ip); + metadata->listening_port = (uint16_t)luaL_checkinteger(L, 5); + lua_fill_nano_str(L, 6, &metadata->uri); + lua_fill_nano_str(L, 7, &metadata->client_ip); + metadata->client_port = (uint16_t)luaL_checkinteger(L, 8); + lua_fill_nano_str(L, 9, &metadata->parsed_host); + lua_fill_nano_str(L, 10, &metadata->parsed_uri); + + // Store pointer in Lua + lua_pushlightuserdata(L, metadata); + return 1; // Return userdata +} + +// Send data to NanoAttachment +static int lua_send_data(lua_State *L) { + // Retrieve function arguments from Lua + NanoAttachment* attachment = (NanoAttachment*) lua_touserdata(L, 1); + SessionID session_id = luaL_checkinteger(L, 2); + HttpSessionData *session_data = (HttpSessionData*) lua_touserdata(L, 3); + HttpChunkType chunk_type = luaL_checkinteger(L, 4); + HttpMetaData* meta_data = (HttpMetaData*) lua_touserdata(L, 5); + HttpHeaders* req_headers = (HttpHeaders*) lua_touserdata(L, 6); + int contains_body = luaL_checkinteger(L, 7); // Convert Lua boolean to C int + //int contains_body = 0; + + // Validate inputs + if (!attachment || !session_data || !meta_data || !req_headers) { + lua_pushstring(L, "Error: received NULL data in lua_send_data"); + return lua_error(L); + } + + // Allocate memory for HttpRequestFilterData + HttpRequestFilterData *filter_data = (HttpRequestFilterData *)malloc(sizeof(HttpRequestFilterData)); + if (!filter_data) { + return luaL_error(L, "Memory allocation failed for HttpRequestFilterData"); + } + + // Populate HttpRequestFilterData struct + filter_data->meta_data = meta_data; + filter_data->req_headers = req_headers; + filter_data->contains_body = contains_body; + + // Create attachment data + AttachmentData attachment_data; + attachment_data.session_id = session_id; + attachment_data.session_data = session_data; + attachment_data.chunk_type = chunk_type; + attachment_data.data = (void*)filter_data; + + AttachmentVerdictResponse* res_ptr = malloc(sizeof(AttachmentVerdictResponse)); + *res_ptr = SendDataNanoAttachment(attachment, &attachment_data); + + // Free allocated memory + free(filter_data); + + lua_pushinteger(L, res_ptr->verdict); + lua_pushlightuserdata(L, res_ptr); + return 2; +} + +static int lua_send_body(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*) lua_touserdata(L, 1); + SessionID session_id = luaL_checkinteger(L, 2); + HttpSessionData *session_data = (HttpSessionData*) lua_touserdata(L, 3); + size_t body_len; + const char *body_chunk = luaL_checklstring(L, 4, &body_len); + HttpChunkType chunk_type = luaL_checkinteger(L, 5); + + if (!attachment || !session_data || !body_chunk) { + lua_pushstring(L, "Error: Invalid attachment or session_data"); + return lua_error(L); + } + + // For small bodies, send as a single chunk + if (body_len <= 8 * 1024) { + HttpBody http_chunks; + http_chunks.bodies_count = 1; + + nano_str_t chunk; + chunk.data = (unsigned char*)body_chunk; + chunk.len = body_len; + http_chunks.data = &chunk; + + AttachmentData attachment_data; + attachment_data.session_id = session_id; + attachment_data.session_data = session_data; + attachment_data.chunk_type = chunk_type; + attachment_data.data = &http_chunks; + + AttachmentVerdictResponse* res_ptr = malloc(sizeof(AttachmentVerdictResponse)); + *res_ptr = SendDataNanoAttachment(attachment, &attachment_data); + + // Push verdict + lua_pushinteger(L, res_ptr->verdict); + lua_pushlightuserdata(L, res_ptr); + + // Push modifications if they exist + if (res_ptr->modifications) { + lua_pushlightuserdata(L, res_ptr->modifications); + } else { + lua_pushnil(L); + } + + return 3; + } + + // Calculate number of chunks (8KB each, like Envoy) + const size_t CHUNK_SIZE = 8 * 1024; + size_t num_chunks = ((body_len - 1) / CHUNK_SIZE) + 1; + + // Limit number of chunks like Envoy + if (num_chunks > 10000) { + num_chunks = 10000; + } + + // Create HttpBody structure + HttpBody http_chunks; + http_chunks.bodies_count = num_chunks; + + // Allocate memory for chunks array + http_chunks.data = (nano_str_t*)malloc(num_chunks * sizeof(nano_str_t)); + if (!http_chunks.data) { + lua_pushstring(L, "Error: Failed to allocate memory for chunks"); + return lua_error(L); + } + + // Prepare chunks using pointer arithmetic like Envoy + for (size_t i = 0; i < num_chunks; i++) { + nano_str_t* chunk_ptr = (nano_str_t*)((char*)http_chunks.data + (i * sizeof(nano_str_t))); + size_t chunk_start = i * CHUNK_SIZE; + size_t chunk_len = (i == num_chunks - 1) ? (body_len - chunk_start) : CHUNK_SIZE; + + chunk_ptr->data = (unsigned char*)(body_chunk + chunk_start); + chunk_ptr->len = chunk_len; + } + + // Prepare attachment data + AttachmentData attachment_data; + attachment_data.session_id = session_id; + attachment_data.session_data = session_data; + attachment_data.chunk_type = chunk_type; + attachment_data.data = &http_chunks; + + // Send all chunks at once + AttachmentVerdictResponse* res_ptr = malloc(sizeof(AttachmentVerdictResponse)); + *res_ptr = SendDataNanoAttachment(attachment, &attachment_data); + + // Free allocated memory + free(http_chunks.data); + + // Push verdict + lua_pushinteger(L, res_ptr->verdict); + lua_pushlightuserdata(L, res_ptr); + + // Push modifications if they exist + if (res_ptr->modifications) { + lua_pushlightuserdata(L, res_ptr->modifications); + } else { + lua_pushnil(L); + } + + return 3; +} + +static int lua_end_inspection(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*) lua_touserdata(L, 1); + SessionID session_id = luaL_checkinteger(L, 2); + HttpSessionData* session_data = (HttpSessionData*) lua_touserdata(L, 3); + HttpChunkType chunk_type = luaL_checkinteger(L, 4); + + if (!attachment || !session_data) { + lua_pushstring(L, "Error: Invalid attachment or session_data"); + return lua_error(L); + } + + AttachmentData attachment_data; + attachment_data.session_id = session_id; + attachment_data.session_data = session_data; + attachment_data.chunk_type = chunk_type; + attachment_data.data = NULL; + + // Send NULL to indicate end of data + AttachmentVerdictResponse* res_ptr = malloc(sizeof(AttachmentVerdictResponse)); + *res_ptr = SendDataNanoAttachment(attachment, &attachment_data); + + lua_pushinteger(L, res_ptr->verdict); + lua_pushlightuserdata(L, res_ptr); + + return 2; +} + +// Send response headers to NanoAttachment +static int lua_send_response_headers(lua_State *L) { + NanoAttachment* attachment = (NanoAttachment*) lua_touserdata(L, 1); + SessionID session_id = luaL_checkinteger(L, 2); + HttpSessionData *session_data = (HttpSessionData*) lua_touserdata(L, 3); + HttpHeaders *headers = (HttpHeaders*) lua_touserdata(L, 4); + int status_code = luaL_checkinteger(L, 5); + uint64_t content_length = luaL_checkinteger(L, 6); + + if (!attachment || !session_data || !headers) { + lua_pushstring(L, "Error: Invalid attachment, session_data, or headers"); + return lua_error(L); + } + + // Create ResHttpHeaders structure exactly as in filter.go + ResHttpHeaders res_headers; + res_headers.headers = headers; + res_headers.response_code = status_code; + res_headers.content_length = content_length; + + AttachmentData attachment_data; + attachment_data.session_id = session_id; + attachment_data.session_data = session_data; + attachment_data.chunk_type = HTTP_RESPONSE_HEADER; + attachment_data.data = &res_headers; + + AttachmentVerdictResponse* res_ptr = malloc(sizeof(AttachmentVerdictResponse)); + *res_ptr = SendDataNanoAttachment(attachment, &attachment_data); + lua_pushinteger(L, res_ptr->verdict); + lua_pushlightuserdata(L, res_ptr); + return 2; +} + +// Register functions in Lua +static const struct luaL_Reg nano_attachment_lib[] = { + {"init_nano_attachment", lua_init_nano_attachment}, + {"get_web_response_type", lua_get_web_response_type}, + {"get_response_code", lua_get_response_code}, + {"get_block_page", lua_get_block_page}, + {"get_redirect_page", lua_get_redirect_page}, + {"createNanoStrAlloc", lua_createNanoStrAlloc}, + {"freeNanoStr", lua_freeNanoStr}, + {"setHeaderElement", lua_setHeaderElement}, + {"send_data", lua_send_data}, + {"send_response_headers", lua_send_response_headers}, + {"fini_session", lua_fini_session}, + {"is_session_finalized", lua_is_session_finalized}, + {"init_session", lua_init_session}, + {"allocHttpHeaders", lua_allocHttpHeaders}, + {"freeHttpHeaders", lua_freeHttpHeaders}, + {"setHeaderCount", lua_setHeaderCount}, + {"create_http_metadata", lua_create_http_metadata}, + {"send_body", lua_send_body}, + {"end_inspection", lua_end_inspection}, + {NULL, NULL} +}; + +// Load library +int luaopen_lua_attachment_wrapper(lua_State *L) { + luaL_newlib(L, nano_attachment_lib); + return 1; +} diff --git a/attachments/kong/nano_ffi.lua b/attachments/kong/nano_ffi.lua new file mode 100755 index 0000000..b4fe4cb --- /dev/null +++ b/attachments/kong/nano_ffi.lua @@ -0,0 +1,426 @@ +package.cpath = "/usr/local/kong/lib/?.so;" .. package.cpath +local nano_attachment = require "lua_attachment_wrapper" +local kong = kong +local nano = {} + +nano.session_counter = 0 +nano.attachments = {} -- Store attachments per worker +nano.num_workers = ngx.worker.count() or 1 -- Detect number of workers +nano.allocated_strings = {} +nano.allocate_headers = {} +nano.AttachmentVerdict = { + INSPECT = 0, + ACCEPT = 1, + DROP = 2, -- Matches `ATTACHMENT_VERDICT_DROP` + INJECT = 3 +} +nano.HttpChunkType = { + HTTP_REQUEST_FILTER = 0, + HTTP_REQUEST_METADATA = 1, + HTTP_REQUEST_HEADER = 2, + HTTP_REQUEST_BODY = 3, + HTTP_REQUEST_END = 4, + HTTP_RESPONSE_HEADER = 5, + HTTP_RESPONSE_BODY = 6, + HTTP_RESPONSE_END = 7, + HOLD_DATA = 8 +} + +nano.WebResponseType = { + CUSTOM_WEB_RESPONSE = 0, + RESPONSE_CODE_ONLY = 1, + REDIRECT_WEB_RESPONSE = 2, + NO_WEB_RESPONSE = 3, +} + +local ffi = require "ffi" + +ffi.cdef[[ +typedef enum HttpModificationType +{ + APPEND, + INJECT, + REPLACE +} HttpModificationType; + +typedef enum NanoWebResponseType +{ + CUSTOM_WEB_RESPONSE, + RESPONSE_CODE_ONLY, + REDIRECT_WEB_RESPONSE, + NO_WEB_RESPONSE +} NanoWebResponseType; + +typedef struct __attribute__((__packed__)) HttpInjectData { + int64_t injection_pos; + HttpModificationType mod_type; + uint16_t injection_size; + uint8_t is_header; + uint8_t orig_buff_index; + char data[0]; +} HttpInjectData; + +typedef struct NanoHttpModificationList { + struct NanoHttpModificationList *next; ///< Next node. + HttpInjectData modification; ///< Modification data. + char *modification_buffer; +} NanoHttpModificationList; +]] + +-- Assuming you already defined the C struct somewhere: +-- ffi.cdef[[ +-- typedef struct NanoHttpModificationList { ... } NanoHttpModificationList; +-- ]] + +local NanoHttpModificationListPtr = ffi.typeof("NanoHttpModificationList*") + +function nano.generate_session_id() + nano.session_counter = nano.session_counter + 1 + local worker_id = ngx.worker.id() + -- Compose session_id as "", e.g. "20001" + return tonumber(string.format("%d%05d", worker_id, nano.session_counter)) +end + +function nano.handle_custom_response(session_data, response) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + local response_type = nano_attachment.get_web_response_type(attachment, session_data, response) + + if response_type == nano.WebResponseType.RESPONSE_CODE_ONLY then + local code = nano_attachment.get_response_code(response) + return kong.response.exit(code, "") + end + + if response_type == nano.WebResponseType.REDIRECT_WEB_RESPONSE then + local location = nano_attachment.get_redirect_page(attachment, session_data, response) + return kong.response.exit(307, "", { ["Location"] = location }) + end + + local block_page = nano_attachment.get_block_page(attachment, session_data, response) + if not block_page then + kong.log.err("Failed to retrieve custom block page for session ", session_data) + return kong.response.exit(500, { message = "Internal Server Error" }) + end + local code = nano_attachment.get_response_code(response) -- Get the intended status code + return kong.response.exit(code, block_page, { ["Content-Type"] = "text/html" }) + +end + + + +-- Allocates memory (must be freed later) +function nano.create_nano_str_alloc(str) + if not str then return nil end + + local nano_str = nano_attachment.createNanoStrAlloc(str) + table.insert(nano.allocated_strings, nano_str) -- Track allocation + return nano_str +end + +-- Free nano_str_t to prevent memory leaks +function nano.free_nano_str(nano_str) + if nano_str then + nano_attachment.freeNanoStr(nano_str) + end +end + +-- Free all allocated nano_str_t to prevent memory leaks +function nano.free_all_nano_str() + for _, nano_str in ipairs(nano.allocated_strings) do + nano_attachment.freeNanoStr(nano_str) -- Free memory in C + end + + nano.allocated_strings = {} -- Reset the list +end + +function nano.free_http_headers(header_data) + for _, nano_header in ipairs(nano.allocate_headers) do + nano_attachment.freeNanoStr(nano_header) -- Free memory in C + end + + nano.allocate_headers = {} -- Reset the list +end + +-- Initialize worker attachment +function nano.init_attachment() + local worker_id = ngx.worker.id() + local attachment, err + local retries = 3 + + for attempt = 1, retries do + attachment, err = nano_attachment.init_nano_attachment(worker_id, nano.num_workers) + if attachment then break end + kong.log.err("Worker ", worker_id, " failed to initialize attachment (attempt ", attempt, "): ", err) + ngx.sleep(2) + end + + if not attachment then + kong.log.err("Worker ", worker_id, " failed to initialize attachment after ", retries, " attempts.") + else + nano.attachments[worker_id] = attachment + kong.log.info("Worker ", worker_id, " successfully initialized nano_attachment.") + end +end + +-- Initialize a session for a given request +function nano.init_session(session_id) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + if not attachment then + kong.log.err("Cannot initialize session: Attachment not found for worker ", worker_id) + return nil + end + + local session_data, err = nano_attachment.init_session(attachment, session_id) + if not session_data then + kong.log.err("Failed to initialize session for session_id ", session_id, ": ", err) + return nil + end + + return session_data +end + +-- Check if session is finalized +function nano.is_session_finalized(session_data) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + if not attachment or not session_data then + kong.log.err("Cannot check session finalization: Invalid attachment or session_data") + return false + end + + return nano_attachment.is_session_finalized(attachment, session_data) +end + +-- Extract metadata for request +function nano.handle_start_transaction() + local stream_info = kong.request + + local full_host = stream_info.get_host() + local host = full_host:match("([^:]+)") + + local method = stream_info.get_method() + local uri = stream_info.get_path() + local scheme = stream_info.get_scheme() + local client_ip = kong.client.get_ip() + local client_port = kong.client.get_port() + + local listening_address = kong.request.get_host() + local listening_ip, listening_port = listening_address:match("([^:]+):?(%d*)") + + -- Call the C function with extracted metadata + local metadata = nano_attachment.create_http_metadata( + scheme, method, host, listening_ip, tonumber(listening_port) or 0, + uri, client_ip, tonumber(client_port) or 0, "", "" + ) + + collectgarbage("stop") + + return metadata +end + +-- Handle request headers and convert them to nano_str_t +function nano.handleHeaders(headers) + local envoy_headers_prefix = "x-envoy" + + -- Allocate memory for headers in C + local header_data = nano_attachment.allocHttpHeaders() + table.insert(nano.allocate_headers, header_data) -- Track allocation + local index = 0 + + for key, value in pairs(headers) do + if index > 10000 then break end + + -- Filter out unwanted headers + if key:find("^" .. envoy_headers_prefix) or key == "x-request-id" or + key == ":method" or key == ":path" or key == ":scheme" or + key == "x-forwarded-proto" then + goto continue + end + + -- Convert ":authority" to "Host" + if key == ":authority" then key = "Host" end + + -- Store header data in C memory + nano_attachment.setHeaderElement(header_data, index, key, value) + index = index + 1 + + ::continue:: + end + + -- Store the count + nano_attachment.setHeaderCount(header_data, index) + + return header_data +end + +-- Send data to NanoAttachment +function nano.send_data(session_id, session_data, meta_data, header_data, contains_body, chunk_type) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + if not attachment then + kong.log.err("Attachment not initialized for worker ", worker_id, ". Dropping request.") + return nano.AttachmentVerdict.INSPECT + end + + contains_body = tonumber(contains_body) or 0 -- Ensure it's a number + contains_body = (contains_body > 0) and 1 or 0 -- Force strict 0 or 1 + + local verdict, response = nano_attachment.send_data(attachment, session_id, session_data, chunk_type, meta_data, header_data, contains_body) + return verdict, response +end + +function nano.send_body(session_id, session_data, body_chunk, chunk_type) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + if not attachment then + kong.log.err("Attachment not initialized for worker ", worker_id, ". Dropping request.") + return nano.AttachmentVerdict.INSPECT + end + + -- Send the body chunk directly as a string + local verdict, response, modifications = nano_attachment.send_body(attachment, session_id, session_data, body_chunk, chunk_type) + return verdict, response, modifications +end + +-- Function to inject content into a string at a specific position +function nano.inject_at_position(buffer, injection, pos) + if pos < 0 or pos > #buffer then + kong.log.err("Invalid injection position: ", pos, ", buffer length: ", #buffer) + return buffer + end + return buffer:sub(1, pos) .. injection .. buffer:sub(pos + 1) +end + +-- Function to handle body modifications +function nano.handle_body_modifications(body, modifications, body_buffer_chunk) + if modifications == nil then + return body + end + -- cast the userdata to a pointer + local curr_modification = ffi.cast(NanoHttpModificationListPtr, modifications) + + while curr_modification ~= nil do + if tonumber(curr_modification.modification.orig_buff_index) == body_buffer_chunk then + local injection_pos = tonumber(curr_modification.modification.injection_pos) + local modification_str = ffi.string(curr_modification.modification_buffer) + + kong.log.debug("Injecting modification at pos ", injection_pos, " body buffer chunk ", body_buffer_chunk) + + body = nano.inject_at_position(body, modification_str, injection_pos) + end + + curr_modification = curr_modification.next + end + + return body +end + +-- Add a new function for handling response bodies +function nano.send_response_body(session_id, session_data, body_chunk) + local verdict, response, modifications = nano.send_body(session_id, session_data, body_chunk, nano.HttpChunkType.HTTP_RESPONSE_BODY) + return verdict, response, modifications +end + +function nano.end_inspection(session_id, session_data, chunk_type) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + if not attachment then + kong.log.err("Attachment not initialized for worker ", worker_id, ". Dropping request.") + return nano.AttachmentVerdict.INSPECT + end + + local verdict, response = nano_attachment.end_inspection(attachment, session_id, session_data, chunk_type) + return verdict, response +end + +-- Finalize session cleanup +function nano.fini_session(session_data) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + if not attachment or not session_data then + kong.log.err("Cannot finalize session: Invalid attachment or session_data") + return false + end + + nano_attachment.fini_session(attachment, session_data) + kong.log.info("Successfully finalized session ", session_data, " for worker ", worker_id) + return true +end + +-- Send response headers for inspection +function nano.send_response_headers(session_id, session_data, headers, status_code, content_length) + local worker_id = ngx.worker.id() + local attachment = nano.attachments[worker_id] + + if not attachment then + kong.log.err("Attachment not initialized for worker ", worker_id, ". Dropping request.") + return nano.AttachmentVerdict.INSPECT + end + + local verdict, response = nano_attachment.send_response_headers( + attachment, + session_id, + session_data, + headers, + status_code, + content_length + ) + return verdict, response +end + +-- Function to handle header modifications +function nano.handle_header_modifications(headers, modifications) + if not modifications then + return headers + end + + local curr_modification = modifications + local modified_headers = headers + + while curr_modification do + local mod = curr_modification.modification + if mod.is_header then + local type = mod.mod_type + local key = curr_modification.modification_buffer + local value = curr_modification.next and curr_modification.next.modification_buffer or nil + + if type == 0 then -- APPEND + kong.log.debug("Appending header: ", key, " : ", value) + modified_headers[key] = value + elseif type == 1 then -- INJECT + local header_index = mod.orig_buff_index + local header_name = nil + local header_value = nil + local i = 0 + for k, v in pairs(modified_headers) do + if i == header_index then + header_name = k + header_value = v + break + end + i = i + 1 + end + if header_name then + kong.log.debug("Injecting into header: ", header_name) + modified_headers[header_name] = nano.inject_at_position(header_value, value, mod.injection_pos) + end + elseif type == 2 then -- REPLACE + kong.log.debug("Replacing header: ", key) + modified_headers[key] = value + end + end + curr_modification = curr_modification.next + end + + return modified_headers +end + +return nano diff --git a/attachments/kong/open-appsec-waf-kong-plugin-1.0.0-1.rockspec b/attachments/kong/open-appsec-waf-kong-plugin-1.0.0-1.rockspec new file mode 100755 index 0000000..3f93774 --- /dev/null +++ b/attachments/kong/open-appsec-waf-kong-plugin-1.0.0-1.rockspec @@ -0,0 +1,57 @@ +package = "open-appsec-waf-kong-plugin" +version = "1.0.0-1" + +source = { + url = "git://github.com/openappsec/attachment.git", + tag = "main" +} + +description = { + summary = "Kong plugin for scanning headers", + detailed = [[ + A Kong plugin that scans HTTP request headers using Nano Attachment. + ]], + homepage = "https://github.com/openappsec/attachment", + license = "Apache" +} + +dependencies = { + "lua >= 2.1" +} + +build = { + type = "builtin", + + modules = { + ["kong.plugins.open-appsec-waf-kong-plugin.handler"] = "attachments/kong/handler.lua", + ["kong.plugins.open-appsec-waf-kong-plugin.nano_ffi"] = "attachments/kong/nano_ffi.lua", + ["kong.plugins.open-appsec-waf-kong-plugin.schema"] = "attachments/kong/schema.lua", + ["lua_attachment_wrapper"] = { + sources = { + "attachments/kong/lua_attachment_wrapper.c", + "attachments/nano_attachment/nano_attachment.c", + "attachments/nano_attachment/nano_attachment_io.c", + "attachments/nano_attachment/nano_attachment_metric.c", + "attachments/nano_attachment/nano_attachment_sender.c", + "attachments/nano_attachment/nano_attachment_sender_thread.c", + "attachments/nano_attachment/nano_attachment_thread.c", + "attachments/nano_attachment/nano_compression.c", + "attachments/nano_attachment/nano_configuration.c", + "attachments/nano_attachment/nano_initializer.c", + "attachments/nano_attachment/nano_utils.c", + "attachments/nano_attachment/nano_attachment_util/nano_attachment_util.cc", + "core/attachments/http_configuration/http_configuration.cc", + "core/compression/compression_utils.cc", + "core/shmem_ipc_2/shared_ring_queue.c", + "core/shmem_ipc_2/shmem_ipc.c" + }, + incdirs = { + "core/include/attachments/", + "attachments/nano_attachment/" + }, + defines = { "_GNU_SOURCE", "ZLIB_CONST" }, + libraries = { "pthread", "z", "rt", "stdc++" }, + ldflags = { "-static-libstdc++", "-static-libgcc" } + } + } +} diff --git a/attachments/kong/schema.lua b/attachments/kong/schema.lua new file mode 100755 index 0000000..b044c0c --- /dev/null +++ b/attachments/kong/schema.lua @@ -0,0 +1,16 @@ +local typedefs = require "kong.db.schema.typedefs" + +return { + name = "open-appsec-waf-kong-plugin", + fields = { + { consumer = typedefs.no_consumer }, -- required for Konnect compatibility + { protocols = typedefs.protocols_http }, -- required so Konnect knows when to allow this plugin + { config = { + type = "record", + fields = { + { debug = { type = "boolean", default = false } }, + }, + }, + }, + }, +}