diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..ea2b0f2 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1 @@ +globals = {"ngx"} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d2c958..5d1456f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,19 @@ repos: - id: prettier name: Prettier Code Formatter + - repo: https://github.com/JohnnyMorganz/StyLua + rev: 27e6b388796604181e810ef05c9fb15a9f7a7769 # frozen: v0.18.2 + hooks: + - id: stylua-github + exclude: ^crowdsec/lib/ + + - repo: https://github.com/lunarmodules/luacheck + rev: ababb6d403d634eb74d2c541035e9ede966e710d # frozen: v1.1.1 + hooks: + - id: luacheck + exclude: ^crowdsec/lib/ + args: ["--std", "min", "--codes", "--ranges", "--no-cache"] + - repo: https://github.com/pycqa/flake8 rev: 10f4af6dbcf93456ba7df762278ae61ba3120dc6 # frozen: 6.1.0 hooks: diff --git a/clamav/clamav.lua b/clamav/clamav.lua index 1bf933c..fb36596 100644 --- a/clamav/clamav.lua +++ b/clamav/clamav.lua @@ -1,12 +1,35 @@ -local class = require "middleclass" -local plugin = require "bunkerweb.plugin" -local utils = require "bunkerweb.utils" -local cjson = require "cjson" -local upload = require "resty.upload" -local sha512 = require "resty.sha512" -local str = require "resty.string" +local class = require("middleclass") +local plugin = require("bunkerweb.plugin") +local sha512 = require("resty.sha512") +local str = require("resty.string") +local upload = require("resty.upload") +local utils = require("bunkerweb.utils") -local clamav = class("clamav", plugin) +local clamav = class("clamav", plugin) + +local stream_size = function(size) + local floor = math.floor + return ("%c%c%c%c") + :format( + size % 0x100, + floor(size / 0x100) % 0x100, + floor(size / 0x10000) % 0x100, + floor(size / 0x1000000) % 0x100 + ) + :reverse() +end + +local read_all = function(form) + while true do + local typ = form:read() + if not typ then + return + end + if typ == "eof" then + return + end + end +end function clamav:initialize() -- Call parent initialize @@ -30,7 +53,14 @@ function clamav:init_worker() if data ~= "PONG" then return self:ret(false, "wrong data received from ClamAV : " .. data) end - self.logger:log(ngx.NOTICE, "connectivity with " .. self.variables["CLAMAV_HOST"] .. ":" .. self.variables["CLAMAV_PORT"] .. " is successful") + self.logger:log( + ngx.NOTICE, + "connectivity with " + .. self.variables["CLAMAV_HOST"] + .. ":" + .. self.variables["CLAMAV_PORT"] + .. " is successful" + ) return self:ret(true, "success") end @@ -41,7 +71,13 @@ function clamav:access() end -- Check if we have downloads - if not self.ctx.bw.http_content_type or (not self.ctx.bw.http_content_type:match("boundary") or not self.ctx.bw.http_content_type:match("multipart/form%-data")) then + if + not self.ctx.bw.http_content_type + or ( + not self.ctx.bw.http_content_type:match("boundary") + or not self.ctx.bw.http_content_type:match("multipart/form%-data") + ) + then return self:ret(true, "no file upload detected") end @@ -51,7 +87,11 @@ function clamav:access() return self:ret(false, "error while scanning file(s) : " .. detected) end if detected then - return self:ret(true, "file with checksum " .. checksum .. "is detected : " .. detected, utils.get_deny_status(self.ctx)) + return self:ret( + true, + "file with checksum " .. checksum .. "is detected : " .. detected, + utils.get_deny_status(self.ctx) + ) end return self:ret(true, "no file detected") end @@ -63,13 +103,16 @@ function clamav:command(cmd) return false, err end -- Send command - local bytes, err = socket:send("n" .. cmd .. "\n") + local bytes + bytes, err = socket:send("n" .. cmd .. "\n") if not bytes then socket:close() return false, err end -- Receive response - local data, err, partial = socket:receive("*l") + -- luacheck: ignore partial + local data, partial + data, err, partial = socket:receive("*l") if not data then socket:close() return false, err @@ -82,10 +125,7 @@ function clamav:socket() -- Init socket local socket = ngx.socket.tcp() socket:settimeout(tonumber(self.variables["CLAMAV_TIMEOUT"])) - local ok, err = socket:connect( - self.variables["CLAMAV_HOST"], - tonumber(self.variables["CLAMAV_PORT"]) - ) + local ok, err = socket:connect(self.variables["CLAMAV_HOST"], tonumber(self.variables["CLAMAV_PORT"])) if not ok then return false, err end @@ -96,11 +136,10 @@ function clamav:scan() -- Loop on files local form = upload:new(4096, 512, true) if not form then - return false, err + return false, "failed to create upload form" end local sha = sha512:new() - local socket = nil - local err = nil + local socket while true do -- Read part local typ, res, err = form:read() @@ -111,11 +150,14 @@ function clamav:scan() return false, "form:read() failed : " .. err end + local bytes + -- Header case : check if we have a filename if typ == "header" then local found = false + -- luacheck: ignore 213 for i, header in ipairs(res) do - if header:find("^.*filename=\"(.*)\".*$") then + if header:find('^.*filename="(.*)".*$') then found = true break end @@ -126,77 +168,88 @@ function clamav:scan() end socket, err = self:socket() if not socket then - self:read_all(form) + read_all(form) return false, "socket failed : " .. err end - local bytes, err = socket:send("nINSTREAM\n") + bytes, err = socket:send("nINSTREAM\n") if not bytes then socket:close() - self:read_all(form) + read_all(form) return false, "socket:send() failed : " .. err end end - -- Body case : update checksum and send to clamav + -- Body case : update checksum and send to clamav elseif typ == "body" and socket then sha:update(res) - local bytes, err = socket:send(self:stream_size(#res) .. res) + bytes, err = socket:send(stream_size(#res) .. res) if not bytes then socket:close() - self:read_all(form) + read_all(form) return false, "socket:send() failed : " .. err end - -- Part end case : get final checksum and clamav result + -- Part end case : get final checksum and clamav result elseif typ == "part_end" and socket then local checksum = str.to_hex(sha:final()) sha:reset() -- Check if file is in cache local ok, cached = self:is_in_cache(checksum) if not ok then - self.logger:log(ngx.ERR, "can't check if file with checksum " .. checksum .. " is in cache : " .. cached) + self.logger:log( + ngx.ERR, + "can't check if file with checksum " .. checksum .. " is in cache : " .. cached + ) elseif cached then socket:close() socket = nil if cached ~= "clean" then - self:read_all(form) + read_all(form) return true, cached, checksum end else -- End the INSTREAM - local bytes, err = socket:send(self:stream_size(0)) + bytes, err = socket:send(stream_size(0)) if not bytes then socket:close() - self:read_all(form) + read_all(form) return false, "socket:send() failed : " .. err end -- Read result - local data, err, partial = socket:receive("*l") + -- luacheck: ignore partial + local data, partial + data, err, partial = socket:receive("*l") if not data then socket:close() - self:read_all(form) + read_all(form) return false, err end socket:close() socket = nil if data:match("^.*INSTREAM size limit exceeded.*$") then - self.logger:log(ngx.ERR, "can't scan file with checksum " .. checksum .. " because size exceeded StreamMaxLength in clamd.conf") + self.logger:log( + ngx.ERR, + "can't scan file with checksum " + .. checksum + .. " because size exceeded StreamMaxLength in clamd.conf" + ) else - local istart, iend, data = data:find("^stream: (.*) FOUND$") + -- luacheck: ignore iend + local istart + istart, iend, data = data:find("^stream: (.*) FOUND$") local detected = "clean" if istart then detected = data end - local ok, err = self:add_to_cache(checksum, detected) + ok, err = self:add_to_cache(checksum, detected) if not ok then self.logger:log(ngx.ERR, "can't cache result : " .. err) end if detected ~= "clean" then - self:read_all(form) + read_all(form) return true, detected, checksum end end - end - -- End of body case : no file detected + -- End of body case : no file detected elseif typ == "eof" then if socket then socket:close() @@ -204,19 +257,10 @@ function clamav:scan() return true end end + -- luacheck: ignore 511 return false, "malformed content" end -function clamav:stream_size(size) - local floor = math.floor - return ("%c%c%c%c"):format( - size % 0x100, - floor(size / 0x100) % 0x100, - floor(size / 0x10000) % 0x100, - floor(size / 0x1000000) % 0x100 - ):reverse() -end - function clamav:is_in_cache(checksum) local ok, data = self.cachestore:get("plugin_clamav_" .. checksum) if not ok then @@ -233,16 +277,4 @@ function clamav:add_to_cache(checksum, value) return true end -function clamav:read_all(form) - while true do - local typ = form:read() - if not typ then - return - end - if typ == "eof" then - return - end - end -end - return clamav diff --git a/coraza/coraza.lua b/coraza/coraza.lua index 3f3883d..10f2e30 100755 --- a/coraza/coraza.lua +++ b/coraza/coraza.lua @@ -1,184 +1,187 @@ -local class = require "middleclass" -local plugin = require "bunkerweb.plugin" -local utils = require "bunkerweb.utils" -local http = require "resty.http" -local cjson = require "cjson" +local cjson = require("cjson") +local class = require("middleclass") +local http = require("resty.http") +local plugin = require("bunkerweb.plugin") +local utils = require("bunkerweb.utils") local coraza = class("coraza", plugin) function coraza:initialize() - -- Call parent initialize - plugin.initialize(self, "coraza") + -- Call parent initialize + plugin.initialize(self, "coraza") end function coraza:init_worker() - -- Check if needed - if not self:is_needed() then - return self:ret(true, "coraza not activated") - end - -- Send ping request - local ok, data = self:ping() - if not ok then - return self:ret(false, "error while sending ping request to " .. self.variables["CORAZA_API"] .. " : " .. data) - end - return self:ret(true, "ping request to " .. self.variables["CORAZA_API"] .. " is successful") + -- Check if needed + if not self:is_needed() then + return self:ret(true, "coraza not activated") + end + -- Send ping request + local ok, data = self:ping() + if not ok then + return self:ret(false, "error while sending ping request to " .. self.variables["CORAZA_API"] .. " : " .. data) + end + return self:ret(true, "ping request to " .. self.variables["CORAZA_API"] .. " is successful") end function coraza:access() - -- Check if needed - if not self:is_needed() then - return self:ret(true, "coraza not activated") - end - -- Process phases 1 (headers) and 2 (body) + -- Check if needed + if not self:is_needed() then + return self:ret(true, "coraza not activated") + end + -- Process phases 1 (headers) and 2 (body) - local ok, deny, data = self:process_request() - if not ok then - return self:ret(false, "error while processing request : " .. deny ) - end - if deny then - return self:ret(true, "coraza denied request : " .. data, utils.get_deny_status(self.ctx)) - end + local ok, deny, data = self:process_request() + if not ok then + return self:ret(false, "error while processing request : " .. deny) + end + if deny then + return self:ret(true, "coraza denied request : " .. data, utils.get_deny_status(self.ctx)) + end - return self:ret(true, "coraza accepted request") + return self:ret(true, "coraza accepted request") end function coraza:ping() - -- Get http object - local httpc, err = http.new() - if not httpc then - return false, err - end - httpc:set_timeout(1000) - -- Send ping - local res, err = httpc:request_uri(self.variables["CORAZA_API"] .. "/ping", { keepalive = false }) - if not res then - return false, err - end - -- Check status - if res.status ~= 200 then - local err = "received status " .. tostring(res.status) .. " from Coraza API" - local ok, data = pcall(cjson.decode, res.body) - if ok then - err = err .. " with data " .. data - end - return false, err - end - -- Get pong - local ok, data = pcall(cjson.decode, res.body) - if not ok then - return false, data - end - if data.pong == nil then - return false, "malformed json response" - end - return true + -- Get http object + local httpc, err = http.new() + if not httpc then + return false, err + end + httpc:set_timeout(1000) + -- Send ping + local res + res, err = httpc:request_uri(self.variables["CORAZA_API"] .. "/ping", { keepalive = false }) + if not res then + return false, err + end + -- Check status + if res.status ~= 200 then + err = "received status " .. tostring(res.status) .. " from Coraza API" + local ok, data = pcall(cjson.decode, res.body) + if ok then + err = err .. " with data " .. data + end + return false, err + end + -- Get pong + local ok, data = pcall(cjson.decode, res.body) + if not ok then + return false, data + end + if data.pong == nil then + return false, "malformed json response" + end + return true end function coraza:process_request() - -- Instantiate lua-resty-http obj - local httpc, err = http.new() - if not httpc then - return false, err - end - -- Variables to pass to coraza - local data = { - ["X-Coraza-Version"] = ngx.req.http_version(), - ["X-Coraza-Method"] = self.ctx.bw.request_method, - ["X-Coraza-Ip"] = self.ctx.bw.remote_addr, - ["X-Coraza-Id"] = utils.rand(16), - ["X-Coraza-Uri"] = self.ctx.bw.request_uri - } - -- Compute headers - local headers, err = ngx.req.get_headers() - if err == "truncated" then - return true, true, "too many headers" - end - for header, value in pairs(headers) do - data["X-Coraza-Header-" .. header] = value - end - -- Body setup - ngx.req.read_body() - local body = ngx.req.get_body_data() - if not body then - local file = ngx.req.get_body_file() - if file then - local handle, err = io.open(file) - if handle then - data["Content-Length"] = tostring(handle:seek("end")) - handle:close() - end - local fbody = function() - local handle, err = io.open(file) - if not handle then - return nil, err - end - local cbody = function() - while true do - local chunk = handle:read(8192) - if not chunk then - break - end - coroutine.yield(chunk) - end - handle:close() - end - local co = coroutine.create(cbody) - return function(...) - local ok, ret = coroutine.resume(co, ...) - if ok then - return ret - end - return nil, ret - end - end - body = fbody() - end - end - local res, err = httpc:request_uri( - self.variables["CORAZA_API"] .. "/request", - { - method = "POST", - headers = data, - body = body - } - ) - if not res then - return false, err - end - -- Check status - if res.status ~= 200 then - local err = "received status " .. tostring(res.status) .. " from Coraza API" - local ok, data = pcall(cjson.decode, res.body) - if ok then - err = err .. " with data " .. data - end - return false, err - end - -- Get result - local ok, data = pcall(cjson.decode, res.body) - if not ok then - return false, data - end - if data.deny == nil or not data.msg then - return false, "malformed json response" - end - return true, data.deny, data.msg + -- Instantiate lua-resty-http obj + local httpc, err = http.new() + if not httpc then + return false, err + end + -- Variables to pass to coraza + local data = { + ["X-Coraza-Version"] = ngx.req.http_version(), + ["X-Coraza-Method"] = self.ctx.bw.request_method, + ["X-Coraza-Ip"] = self.ctx.bw.remote_addr, + ["X-Coraza-Id"] = utils.rand(16), + ["X-Coraza-Uri"] = self.ctx.bw.request_uri, + } + -- Compute headers + local headers + headers, err = ngx.req.get_headers() + if err == "truncated" then + return true, true, "too many headers" + end + for header, value in pairs(headers) do + data["X-Coraza-Header-" .. header] = value + end + -- Body setup + ngx.req.read_body() + local body = ngx.req.get_body_data() + if not body then + local file = ngx.req.get_body_file() + if file then + local handle + -- luacheck: ignore err + handle, err = io.open(file) + if handle then + data["Content-Length"] = tostring(handle:seek("end")) + handle:close() + end + local fbody = function() + handle, err = io.open(file) + if not handle then + return nil, err + end + local cbody = function() + while true do + local chunk = handle:read(8192) + if not chunk then + break + end + coroutine.yield(chunk) + end + handle:close() + end + local co = coroutine.create(cbody) + return function(...) + local ok, ret = coroutine.resume(co, ...) + if ok then + return ret + end + return nil, ret + end + end + body = fbody() + end + end + local res, err = httpc:request_uri(self.variables["CORAZA_API"] .. "/request", { + method = "POST", + headers = data, + body = body, + }) + if not res then + return false, err + end + -- Check status + if res.status ~= 200 then + local err = "received status " .. tostring(res.status) .. " from Coraza API" + local ok + ok, data = pcall(cjson.decode, res.body) + if ok then + err = err .. " with data " .. data + end + return false, err + end + -- Get result + local ok + ok, data = pcall(cjson.decode, res.body) + if not ok then + return false, data + end + if data.deny == nil or not data.msg then + return false, "malformed json response" + end + return true, data.deny, data.msg end function coraza:is_needed() - -- Loading case - if self.is_loading then - return false - end - -- Request phases (no default) - if self.is_request and (self.ctx.bw.server_name ~= "_") then - return self.variables["USE_CORAZA"] == "yes" and not ngx.req.is_internal() - end - -- Other cases : at least one service uses it - local is_needed, err = utils.has_variable("USE_CORAZA", "yes") - if is_needed == nil then - self.logger:log(ngx.ERR, "can't check USE_CORAZA variable : " .. err) - end - return is_needed + -- Loading case + if self.is_loading then + return false + end + -- Request phases (no default) + if self.is_request and (self.ctx.bw.server_name ~= "_") then + return self.variables["USE_CORAZA"] == "yes" and not ngx.req.is_internal() + end + -- Other cases : at least one service uses it + local is_needed, err = utils.has_variable("USE_CORAZA", "yes") + if is_needed == nil then + self.logger:log(ngx.ERR, "can't check USE_CORAZA variable : " .. err) + end + return is_needed end return coraza diff --git a/crowdsec/crowdsec.lua b/crowdsec/crowdsec.lua index 18c6e27..78df86e 100644 --- a/crowdsec/crowdsec.lua +++ b/crowdsec/crowdsec.lua @@ -1,11 +1,9 @@ -local class = require "middleclass" -local plugin = require "bunkerweb.plugin" -local utils = require "bunkerweb.utils" -local cachestore = require "bunkerweb.cachestore" -local cjson = require "cjson" -local cs = require "crowdsec.lib.bouncer" +local class = require("middleclass") +local cs = require("crowdsec.lib.bouncer") +local plugin = require("bunkerweb.plugin") +local utils = require("bunkerweb.utils") -local crowdsec = class("crowdsec", plugin) +local crowdsec = class("crowdsec", plugin) function crowdsec:initialize() -- Call parent initialize @@ -22,7 +20,8 @@ function crowdsec:init() return self:ret(true, "init not needed") end -- Init CS - local ok, err = cs.init("/var/cache/bunkerweb/crowdsec/crowdsec.conf", "crowdsec-bunkerweb-bouncer/v1.0") + local ok + ok, err = cs.init("/var/cache/bunkerweb/crowdsec/crowdsec.conf", "crowdsec-bunkerweb-bouncer/v1.0") if not ok then self.logger:log(ngx.ERR, "error while initializing bouncer : " .. err) end diff --git a/discord/discord.lua b/discord/discord.lua index 35dc5a3..5a38935 100644 --- a/discord/discord.lua +++ b/discord/discord.lua @@ -1,8 +1,8 @@ -local class = require "middleclass" -local plugin = require "bunkerweb.plugin" -local utils = require "bunkerweb.utils" -local cjson = require "cjson" -local http = require "resty.http" +local cjson = require("cjson") +local class = require("middleclass") +local http = require("resty.http") +local plugin = require("bunkerweb.plugin") +local utils = require("bunkerweb.utils") local discord = class("discord", plugin) @@ -25,8 +25,13 @@ function discord:log(bypass_use_discord) end -- Compute data local data = {} - data.content = "```Denied request for IP " .. - self.ctx.bw.remote_addr .. " (reason = " .. reason .. ").\n\nRequest data :\n\n" .. ngx.var.request .. "\n" + data.content = "```Denied request for IP " + .. self.ctx.bw.remote_addr + .. " (reason = " + .. reason + .. ").\n\nRequest data :\n\n" + .. ngx.var.request + .. "\n" local headers, err = ngx.req.get_headers() if not headers then data.content = data.content .. "error while getting headers : " .. err @@ -37,13 +42,15 @@ function discord:log(bypass_use_discord) end data.content = data.content .. "```" -- Send request - local hdr, err = ngx.timer.at(0, self.send, self, data) + local hdr + hdr, err = ngx.timer.at(0, self.send, self, data) if not hdr then return self:ret(true, "can't create report timer : " .. err) end end -function discord.send(premature, self, data) +-- luacheck: ignore 212 +function discord.send(premature, self, data) -- TODO: premature is not used, remove it if possible local httpc, err = http.new() if not httpc then self.logger:log(ngx.ERR, "can't instantiate http object : " .. err) @@ -53,16 +60,16 @@ function discord.send(premature, self, data) headers = { ["Content-Type"] = "application/json", }, - body = cjson.encode(data) + body = cjson.encode(data), }) httpc:close() if not res then self.logger:log(ngx.ERR, "error while sending request : " .. err_http) end if self.variables["DISCORD_RETRY_IF_LIMITED"] == "yes" and res.status == 429 and res.headers["Retry-After"] then - self.logger:log(ngx.WARN, - "Discord API is rate-limiting us, retrying in " .. res.headers["Retry-After"] .. "s") - local hdr, err = ngx.timer.at(res.headers["Retry-After"], self.send, self, data) + self.logger:log(ngx.WARN, "Discord API is rate-limiting us, retrying in " .. res.headers["Retry-After"] .. "s") + local hdr + hdr, err = ngx.timer.at(res.headers["Retry-After"], self.send, self, data) if not hdr then self.logger:log(ngx.ERR, "can't create report timer : " .. err) return @@ -86,7 +93,7 @@ function discord:log_default() return self:ret(true, "Discord plugin not enabled") end -- Check if default server is disabled - local check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false) + check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false) if check == nil then return self:ret(false, "error while getting variable DISABLE_DEFAULT_SERVER (" .. err .. ")") end diff --git a/slack/slack.lua b/slack/slack.lua index f4d2487..1f37af3 100644 --- a/slack/slack.lua +++ b/slack/slack.lua @@ -1,10 +1,10 @@ -local class = require "middleclass" -local plugin = require "bunkerweb.plugin" -local utils = require "bunkerweb.utils" -local cjson = require "cjson" -local http = require "resty.http" +local cjson = require("cjson") +local class = require("middleclass") +local http = require("resty.http") +local plugin = require("bunkerweb.plugin") +local utils = require("bunkerweb.utils") -local slack = class("slack", plugin) +local slack = class("slack", plugin) function slack:initialize() -- Call parent initialize @@ -25,8 +25,13 @@ function slack:log(bypass_use_slack) end -- Compute data local data = {} - data.content = "```Denied request for IP " .. - self.ctx.bw.remote_addr .. " (reason = " .. reason .. ").\n\nRequest data :\n\n" .. ngx.var.request .. "\n" + data.content = "```Denied request for IP " + .. self.ctx.bw.remote_addr + .. " (reason = " + .. reason + .. ").\n\nRequest data :\n\n" + .. ngx.var.request + .. "\n" local headers, err = ngx.req.get_headers() if not headers then data.content = data.content .. "error while getting headers : " .. err @@ -37,13 +42,15 @@ function slack:log(bypass_use_slack) end data.content = data.content .. "```" -- Send request - local hdr, err = ngx.timer.at(0, self.send, self, data) + local hdr + hdr, err = ngx.timer.at(0, self.send, self, data) if not hdr then return self:ret(true, "can't create report timer : " .. err) end end -function slack.send(premature, self, data) +-- luacheck: ignore 212 +function slack.send(premature, self, data) -- TODO: premature is not used, remove it if possible local httpc, err = http.new() if not httpc then self.logger:log(ngx.ERR, "can't instantiate http object : " .. err) @@ -53,16 +60,16 @@ function slack.send(premature, self, data) headers = { ["Content-Type"] = "application/json", }, - body = cjson.encode(data) + body = cjson.encode(data), }) httpc:close() if not res then self.logger:log(ngx.ERR, "error while sending request : " .. err_http) end if self.variables["SLACK_RETRY_IF_LIMITED"] == "yes" and res.status == 429 and res.headers["Retry-After"] then - self.logger:log(ngx.WARN, - "slack API is rate-limiting us, retrying in " .. res.headers["Retry-After"] .. "s") - local hdr, err = ngx.timer.at(res.headers["Retry-After"], self.send, self, data) + self.logger:log(ngx.WARN, "slack API is rate-limiting us, retrying in " .. res.headers["Retry-After"] .. "s") + local hdr + hdr, err = ngx.timer.at(res.headers["Retry-After"], self.send, self, data) if not hdr then self.logger:log(ngx.ERR, "can't create report timer : " .. err) return @@ -86,7 +93,7 @@ function slack:log_default() return self:ret(true, "slack plugin not enabled") end -- Check if default server is disabled - local check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false) + check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false) if check == nil then return self:ret(false, "error while getting variable DISABLE_DEFAULT_SERVER (" .. err .. ")") end diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..ed2de71 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,2 @@ +[sort_requires] +enabled = true diff --git a/virustotal/virustotal.lua b/virustotal/virustotal.lua index c26108a..d0e2abc 100644 --- a/virustotal/virustotal.lua +++ b/virustotal/virustotal.lua @@ -1,13 +1,25 @@ -local class = require "middleclass" -local plugin = require "bunkerweb.plugin" -local utils = require "bunkerweb.utils" -local cjson = require "cjson" -local upload = require "resty.upload" -local http = require "resty.http" -local sha256 = require "resty.sha256" -local str = require "resty.string" +local cjson = require("cjson") +local class = require("middleclass") +local http = require("resty.http") +local plugin = require("bunkerweb.plugin") +local sha256 = require("resty.sha256") +local str = require("resty.string") +local upload = require("resty.upload") +local utils = require("bunkerweb.utils") -local virustotal = class("virustotal", plugin) +local virustotal = class("virustotal", plugin) + +local read_all = function(form) + while true do + local typ = form:read() + if not typ then + return + end + if typ == "eof" then + return + end + end +end function virustotal:initialize() -- Call parent initialize @@ -20,7 +32,10 @@ end function virustotal:access() -- Check if enabled - if self.variables["USE_VIRUSTOTAL"] ~= "yes" or (self.variables["VIRUSTOTAL_SCAN_IP"] ~= "yes" and self.variables["VIRUSTOTAL_SCAN_FILE"] ~= "yes") then + if + self.variables["USE_VIRUSTOTAL"] ~= "yes" + or (self.variables["VIRUSTOTAL_SCAN_IP"] ~= "yes" and self.variables["VIRUSTOTAL_SCAN_FILE"] ~= "yes") + then return self:ret(true, "virustotal plugin not enabled") end @@ -31,14 +46,24 @@ function virustotal:access() return self:ret(false, "error while checking if IP is malicious : " .. report) end if report and report ~= "clean" then - return self:ret(true, "IP " .. self.ctx.bw.remote_addr .. " is malicious : " .. report, utils.get_deny_status(self.ctx)) + return self:ret( + true, + "IP " .. self.ctx.bw.remote_addr .. " is malicious : " .. report, + utils.get_deny_status(self.ctx) + ) end end -- File check if self.variables["VIRUSTOTAL_SCAN_FILE"] == "yes" then -- Check if we have downloads - if not self.ctx.bw.http_content_type or (not self.ctx.bw.http_content_type:match("boundary") or not self.ctx.bw.http_content_type:match("multipart/form%-data")) then + if + not self.ctx.bw.http_content_type + or ( + not self.ctx.bw.http_content_type:match("boundary") + or not self.ctx.bw.http_content_type:match("multipart/form%-data") + ) + then return self:ret(true, "no file upload detected") end -- Perform the check @@ -48,7 +73,11 @@ function virustotal:access() end -- Malicious case if detected and detected ~= "clean" then - return self:ret(true, "file with checksum " .. checksum .. "is detected : " .. detected, utils.get_deny_status(self.ctx)) + return self:ret( + true, + "file with checksum " .. checksum .. "is detected : " .. detected, + utils.get_deny_status(self.ctx) + ) end end return self:ret(true, "no ip/file detected") @@ -64,7 +93,8 @@ function virustotal:check_ip() return true, report end -- Ask VT API - local ok, found, response = self:request("/ip_addresses/" .. self.ctx.bw.remote_addr) + local found, response + ok, found, response = self:request("/ip_addresses/" .. self.ctx.bw.remote_addr) if not ok then return false, response end @@ -73,7 +103,8 @@ function virustotal:check_ip() result = self:get_result(response, "IP") end -- Add to cache - local ok, err = self:add_to_cache("ip_" .. ngx.ctx.bw.remote_addr, result) + local err + ok, err = self:add_to_cache("ip_" .. ngx.ctx.bw.remote_addr, result) if not ok then return false, err end @@ -90,15 +121,17 @@ function virustotal:check_file() local processing = nil while true do -- Read part - local typ, res, err = form:read() + local typ, res + typ, res, err = form:read() if not typ then return false, "form:read() failed : " .. err end -- Header case : check if we have a filename if typ == "header" then local found = false + -- luacheck: ignore 213 for i, header in ipairs(res) do - if header:find("^.*filename=\"(.*)\".*$") then + if header:find('^.*filename="(.*)".*$') then found = true break end @@ -106,10 +139,10 @@ function virustotal:check_file() if found then processing = true end - -- Body case : update checksum + -- Body case : update checksum elseif typ == "body" and processing then sha:update(res) - -- Part end case : get final checksum and clamav result + -- Part end case : get final checksum and clamav result elseif typ == "part_end" and processing then processing = nil -- Compute checksum @@ -118,17 +151,21 @@ function virustotal:check_file() -- Check if file is in cache local ok, cached = self:is_in_cache("file_" .. checksum) if not ok then - self.logger:log(ngx.ERR, "can't check if file with checksum " .. checksum .. " is in cache : " .. cached) + self.logger:log( + ngx.ERR, + "can't check if file with checksum " .. checksum .. " is in cache : " .. cached + ) elseif cached then if cached ~= "clean" then - self:read_all(form) + read_all(form) return true, cached, checksum end else -- Check if file is already present on VT - local ok, found, response = self:request("/files/" .. checksum) + local found, response + ok, found, response = self:request("/files/" .. checksum) if not ok then - self:read_all(form) + read_all(form) return false, found end local result = "clean" @@ -136,29 +173,36 @@ function virustotal:check_file() result = self:get_result(response, "FILE") end -- Add to cache - local ok, err = self:add_to_cache("file_" .. checksum, result) + ok, err = self:add_to_cache("file_" .. checksum, result) if not ok then - self:read_all(form) + read_all(form) return false, err end -- Stop here if one file is detected if result ~= "clean" then - self:read_all(form) + read_all(form) return true, result, checksum end end - -- End of body case : no file detected + -- End of body case : no file detected elseif typ == "eof" then return true end end + -- luacheck: ignore 511 return false, "malformed content" end function virustotal:get_result(response, type) local result = "clean" - if response["suspicious"] > tonumber(self.variables["VIRUSTOTAL_" .. type .. "_SUSPICIOUS"]) or response["malicious"] > tonumber(self.variables["VIRUSTOTAL_" .. type .. "_MALICIOUS"]) then - result = tostring(response["suspicious"]) .. " suspicious and " .. tostring(response["malicious"]) .. " malicious" + if + response["suspicious"] > tonumber(self.variables["VIRUSTOTAL_" .. type .. "_SUSPICIOUS"]) + or response["malicious"] > tonumber(self.variables["VIRUSTOTAL_" .. type .. "_MALICIOUS"]) + then + result = tostring(response["suspicious"]) + .. " suspicious and " + .. tostring(response["malicious"]) + .. " malicious" end return result end @@ -186,13 +230,12 @@ function virustotal:request(url) return false, err end -- Send request - local res, err = httpc:request_uri("https://www.virustotal.com/api/v3" .. url, - { - headers = { - ["x-apikey"] = self.variables["VIRUSTOTAL_API_KEY"] - } - } - ) + local res + res, err = httpc:request_uri("https://www.virustotal.com/api/v3" .. url, { + headers = { + ["x-apikey"] = self.variables["VIRUSTOTAL_API_KEY"], + }, + }) if not res then return false, err end @@ -201,7 +244,7 @@ function virustotal:request(url) return true, false end if res.status ~= 200 then - local err = "received status " .. tostring(res.status) .. " from VT API" + err = "received status " .. tostring(res.status) .. " from VT API" local ok, data = pcall(cjson.decode, res.body) if ok then err = err .. " with data " .. data @@ -219,16 +262,4 @@ function virustotal:request(url) return true, true, data.data.attributes.last_analysis_stats end -function virustotal:read_all(form) - while true do - local typ = form:read() - if not typ then - return - end - if typ == "eof" then - return - end - end -end - return virustotal diff --git a/webhook/webhook.lua b/webhook/webhook.lua index 180fe38..8ef19ca 100644 --- a/webhook/webhook.lua +++ b/webhook/webhook.lua @@ -1,8 +1,8 @@ -local class = require "middleclass" -local plugin = require "bunkerweb.plugin" -local utils = require "bunkerweb.utils" -local cjson = require "cjson" -local http = require "resty.http" +local cjson = require("cjson") +local class = require("middleclass") +local http = require("resty.http") +local plugin = require("bunkerweb.plugin") +local utils = require("bunkerweb.utils") local webhook = class("webhook", plugin) @@ -25,8 +25,13 @@ function webhook:log(bypass_use_webhook) end -- Compute data local data = {} - data.content = "```Denied request for IP " .. - self.ctx.bw.remote_addr .. " (reason = " .. reason .. ").\n\nRequest data :\n\n" .. ngx.var.request .. "\n" + data.content = "```Denied request for IP " + .. self.ctx.bw.remote_addr + .. " (reason = " + .. reason + .. ").\n\nRequest data :\n\n" + .. ngx.var.request + .. "\n" local headers, err = ngx.req.get_headers() if not headers then data.content = data.content .. "error while getting headers : " .. err @@ -37,13 +42,15 @@ function webhook:log(bypass_use_webhook) end data.content = data.content .. "```" -- Send request - local hdr, err = ngx.timer.at(0, self.send, self, data) + local hdr + hdr, err = ngx.timer.at(0, self.send, self, data) if not hdr then return self:ret(true, "can't create report timer : " .. err) end end -function webhook.send(premature, self, data) +-- luacheck: ignore 212 +function webhook.send(premature, self, data) -- TODO: premature is not used, remove it if possible local httpc, err = http.new() if not httpc then self.logger:log(ngx.ERR, "can't instantiate http object : " .. err) @@ -53,16 +60,19 @@ function webhook.send(premature, self, data) headers = { ["Content-Type"] = "application/json", }, - body = cjson.encode(data) + body = cjson.encode(data), }) httpc:close() if not res then self.logger:log(ngx.ERR, "error while sending request : " .. err_http) end if self.variables["WEBHOOK_RETRY_IF_LIMITED"] == "yes" and res.status == 429 and res.headers["Retry-After"] then - self.logger:log(ngx.WARN, - "HTTP endpoint is rate-limiting us, retrying in " .. res.headers["Retry-After"] .. "s") - local hdr, err = ngx.timer.at(res.headers["Retry-After"], self.send, self, data) + self.logger:log( + ngx.WARN, + "HTTP endpoint is rate-limiting us, retrying in " .. res.headers["Retry-After"] .. "s" + ) + local hdr + hdr, err = ngx.timer.at(res.headers["Retry-After"], self.send, self, data) if not hdr then self.logger:log(ngx.ERR, "can't create report timer : " .. err) return @@ -86,7 +96,7 @@ function webhook:log_default() return self:ret(true, "webhook plugin not enabled") end -- Check if default server is disabled - local check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false) + check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false) if check == nil then return self:ret(false, "error while getting variable DISABLE_DEFAULT_SERVER (" .. err .. ")") end