├── admin ├── conf │ ├── sites.conf │ ├── waf.conf │ ├── waf.conf.default │ ├── admin.conf │ └── admin.conf.default ├── ssl-certs │ └── .gitkeep ├── component │ ├── pear │ │ ├── css │ │ │ ├── module │ │ │ │ ├── label.css │ │ │ │ ├── topBar.css │ │ │ │ ├── link.css │ │ │ │ ├── fullscreen.css │ │ │ │ ├── tag.css │ │ │ │ ├── form.css │ │ │ │ ├── nprogress.css │ │ │ │ ├── table.css │ │ │ │ ├── message.css │ │ │ │ ├── frame.css │ │ │ │ ├── button.css │ │ │ │ └── menu.css │ │ │ └── pear.css │ │ ├── font │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ ├── module │ │ │ ├── topBar.js │ │ │ ├── context.js │ │ │ ├── count.js │ │ │ ├── convert.js │ │ │ ├── popup.js │ │ │ ├── button.js │ │ │ ├── fullscreen.js │ │ │ ├── common.js │ │ │ ├── frame.js │ │ │ ├── tag.js │ │ │ ├── echartsTheme.js │ │ │ ├── message.js │ │ │ └── drawer.js │ │ └── pear.js │ └── layui │ │ └── font │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 ├── favicon.ico ├── admin │ ├── images │ │ └── logo.png │ ├── data │ │ ├── user.json │ │ └── menu.json │ └── css │ │ ├── other │ │ ├── error.css │ │ └── login.css │ │ └── loader.css ├── view │ ├── error │ │ ├── 500.html │ │ └── 404.html │ ├── system │ │ └── password.html │ └── ip-blocking.html ├── js │ ├── action.js │ ├── severityLevel.js │ ├── attackType.js │ └── validator.js ├── lua │ ├── lib │ │ └── pager.lua │ ├── system.lua │ ├── website.lua │ ├── ip_group.lua │ ├── ip_blocking.lua │ ├── dashboard.lua │ ├── events.lua │ └── cc_defense.lua ├── config │ └── pear.config.yml ├── login.html └── index.html ├── conf ├── global_rules │ ├── ipWhiteList │ ├── sensitiveWords │ ├── acl.json │ ├── whiteUrl.json │ ├── headers.json │ ├── cc.json │ ├── fileExt.json │ ├── sensitive.json │ ├── httpMethod.json │ ├── blackUrl.json │ ├── post.json │ ├── cookie.json │ └── args.json ├── website.json ├── certificate.json ├── ipgroup.json ├── system.json └── global.json ├── images ├── dashboard.png └── donate_wechat.png ├── init.lua ├── lib ├── logger_factory.lua ├── aes.lua ├── decoder.lua ├── constants.lua ├── time.lua ├── arrays.lua ├── utils.lua ├── mysql_cli.lua ├── ip_utils.lua ├── logger.lua ├── geoip.lua ├── request.lua ├── stringutf8.lua ├── file_utils.lua └── action.lua ├── header_filter.lua ├── waf.lua ├── html └── redirect.html ├── body_filter.lua ├── init_worker.lua └── install.sh /admin/conf/sites.conf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/ssl-certs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/global_rules/ipWhiteList: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/global_rules/sensitiveWords: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/component/pear/css/module/label.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/website.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 1, 3 | "rules": [] 4 | } -------------------------------------------------------------------------------- /conf/certificate.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 1, 3 | "rules": [] 4 | } -------------------------------------------------------------------------------- /admin/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/favicon.ico -------------------------------------------------------------------------------- /conf/ipgroup.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 1, 3 | "moduleName": "ipgroup", 4 | "rules": [] 5 | } -------------------------------------------------------------------------------- /conf/global_rules/acl.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleName": "ACL", 3 | "nextId": 1, 4 | "rules": [] 5 | } -------------------------------------------------------------------------------- /images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/images/dashboard.png -------------------------------------------------------------------------------- /images/donate_wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/images/donate_wechat.png -------------------------------------------------------------------------------- /admin/admin/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/admin/images/logo.png -------------------------------------------------------------------------------- /conf/global_rules/whiteUrl.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 1, 3 | "moduleName": "URL白名单检测", 4 | "rules": [] 5 | } -------------------------------------------------------------------------------- /admin/component/layui/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/component/layui/font/iconfont.eot -------------------------------------------------------------------------------- /admin/component/layui/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/component/layui/font/iconfont.ttf -------------------------------------------------------------------------------- /admin/component/pear/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/component/pear/font/iconfont.ttf -------------------------------------------------------------------------------- /admin/component/pear/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/component/pear/font/iconfont.woff -------------------------------------------------------------------------------- /admin/component/layui/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/component/layui/font/iconfont.woff -------------------------------------------------------------------------------- /admin/component/layui/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/component/layui/font/iconfont.woff2 -------------------------------------------------------------------------------- /admin/component/pear/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukaleyang/zhongkui-waf/HEAD/admin/component/pear/font/iconfont.woff2 -------------------------------------------------------------------------------- /admin/component/pear/css/module/topBar.css: -------------------------------------------------------------------------------- 1 | .layui-fixbar li { 2 | border-radius: 4px; 3 | background-color: #5FB878; 4 | color: white; 5 | } 6 | -------------------------------------------------------------------------------- /admin/admin/data/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "admin", 3 | "password": "133F21A9ACCE9D8F36A302F9140F3EAD", 4 | "salt": "XRKUfOvTAJhd8Rk8rvDQ" 5 | } -------------------------------------------------------------------------------- /admin/component/pear/module/topBar.js: -------------------------------------------------------------------------------- 1 | layui.define(["jquery","element","util"],function(e){"use strict";layui.jquery;var i=layui.util;layui.element;e("topBar",new function(){i.fixbar({})})}); -------------------------------------------------------------------------------- /admin/component/pear/module/context.js: -------------------------------------------------------------------------------- 1 | layui.define(["jquery","element"],function(e){"use strict";layui.jquery,layui.element;e("context",new function(){this.put=function(e,t){localStorage.setItem(e,t)},this.get=function(e){return localStorage.getItem(e)}})}); -------------------------------------------------------------------------------- /admin/component/pear/css/module/link.css: -------------------------------------------------------------------------------- 1 | .pear-link{ 2 | font-size: 15px!important; 3 | } 4 | 5 | .pear-link.pear-link-primary{ 6 | color : #5FB878 ; 7 | } 8 | 9 | .pear-link.pear-link-success{ 10 | color : #5FB878 ; 11 | } 12 | 13 | .pear-link .pear-link-warming{ 14 | 15 | 16 | } 17 | 18 | .pear-link .pear-link-danger{ 19 | 20 | } -------------------------------------------------------------------------------- /admin/component/pear/module/count.js: -------------------------------------------------------------------------------- 1 | layui.define(["jquery","element"],function(e){"use strict";layui.jquery,layui.element;e("count",new function(){this.up=function(e,t){t=t||{};var n=document.getElementById(e),i=t.time,u=t.num,r=t.regulator,l=u/(i/r),a=0,c=0,o=setInterval(function(){(a+=l)>=u&&(clearInterval(o),a=u);var e=a.toFixed(t.bit?t.bit:0);e!=c&&(c=e,n.innerHTML=c)},30)}})}); -------------------------------------------------------------------------------- /admin/component/pear/module/convert.js: -------------------------------------------------------------------------------- 1 | layui.define(["jquery","element"],function(e){"use strict";layui.jquery,layui.element;e("convert",new function(){this.imageToBase64=function(e){var t=document.createElement("canvas");t.width=e.width,t.height=e.height,t.getContext("2d").drawImage(e,0,0,e.width,e.height);var i=e.src.substring(e.src.lastIndexOf(".")+1).toLowerCase();return t.toDataURL("image/"+i)}})}); -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local config = require "config" 5 | 6 | local script_path = debug.getinfo(1, 'S').source:sub(2) 7 | local script_dir = script_path:match("(.*[/\\])") 8 | config.ZHONGKUI_PATH = script_dir:sub(1, -2) or "/usr/local/openresty/zhongkui-waf" 9 | config.CONF_PATH = config.ZHONGKUI_PATH .. "/conf" 10 | 11 | config.load_config_file() 12 | -------------------------------------------------------------------------------- /admin/component/pear/module/popup.js: -------------------------------------------------------------------------------- 1 | layui.define(["layer","jquery","element"],function(i){"use strict";layui.jquery;var e=layui.layer;layui.element;i("popup",new function(){this.success=function(i){e.msg(i,{icon:1,time:1e3})},this.failure=function(i){e.msg(i,{icon:2,time:1e3})},this.warning=function(i){e.msg(i,{icon:3,time:1e3})},this.success=function(i,n){e.msg(i,{icon:1,time:1e3},n)},this.failure=function(i,n){e.msg(i,{icon:2,time:1e3},n)},this.warming=function(i,n){e.msg(i,{icon:3,time:1e3},n)}})}); -------------------------------------------------------------------------------- /admin/component/pear/css/module/fullscreen.css: -------------------------------------------------------------------------------- 1 | html:-moz-full-screen { 2 | background: grey; 3 | } 4 | html:-webkit-full-screen { 5 | background: grey; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | html:fullscreen{ 10 | background: grey; 11 | width: 100% !important; 12 | height: 100% !important; 13 | } 14 | 15 | :not(:root):fullscreen::backdrop{ 16 | background:whitesmoke; 17 | } 18 | 19 | .pear-full-screen { 20 | width: 100% !important; 21 | height: 100% !important; 22 | } -------------------------------------------------------------------------------- /lib/logger_factory.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local logger = require "logger" 5 | 6 | local loggers = {} 7 | 8 | local _M = {} 9 | 10 | function _M.get_logger(logPath, host, rolling) 11 | local host_logger = loggers[host] 12 | if not host_logger then 13 | host_logger = logger:new(logPath, host, rolling) 14 | loggers[host] = host_logger 15 | end 16 | return host_logger 17 | end 18 | 19 | return _M -------------------------------------------------------------------------------- /conf/global_rules/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 3, 3 | "moduleName": "Headers检测", 4 | "rules": [ 5 | { 6 | "id": 1, 7 | "state": "on", 8 | "action": "deny", 9 | "rule": "/TomcatBypass/Command/Base64", 10 | "attackType": "rce", 11 | "severityLevel": "high" 12 | }, 13 | { 14 | "id": 2, 15 | "state": "on", 16 | "action": "deny", 17 | "rule": "j\\S*ndi\\S*:\\S*(?:dap|dns)\\S+", 18 | "attackType": "rce", 19 | "severityLevel": "high" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /admin/component/pear/module/button.js: -------------------------------------------------------------------------------- 1 | layui.define(["jquery"],function(t){"use strict";var e=layui.jquery,i=function(t){this.option=t};i.prototype.load=function(t){var o={elem:t.elem,time:!!t.time&&t.time,done:t.done?t.done:function(){}},n=e(o.elem).html();e(o.elem).html(""),e(o.elem).attr("disabled","disabled");var l=e(o.elem);return""==o.time&&0==o.time||setTimeout(function(){e(o.elem).attr("disabled",!1),l.html(n),o.done()},o.time),o.text=n,new i(o)},i.prototype.stop=function(t){e(this.option.elem).attr("disabled",!1),e(this.option.elem).html(this.option.text),t&&t()},t("button",new i)}); -------------------------------------------------------------------------------- /admin/view/error/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |

500

14 |

抱歉,服务器出错了

15 | 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /admin/view/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |

404

14 |

抱歉,你访问的页面不存在或仍在开发中

15 | 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /admin/component/pear/css/pear.css: -------------------------------------------------------------------------------- 1 | @import url("../../layui/css/layui.css"); 2 | @import url("../font/iconfont.css"); 3 | 4 | @import url("module/nprogress.css"); 5 | @import url("module/message.css"); 6 | @import url("module/loading.css"); 7 | @import url("module/topBar.css"); 8 | @import url("module/layout.css"); 9 | @import url("module/button.css"); 10 | @import url("module/frame.css"); 11 | @import url("module/layer.css"); 12 | @import url("module/toast.css"); 13 | @import url("module/menu.css"); 14 | @import url("module/link.css"); 15 | @import url("module/tab.css"); 16 | @import url("module/tag.css"); 17 | @import url("module/fullscreen.css"); 18 | @import url("module/popover.min.css"); -------------------------------------------------------------------------------- /header_filter.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local config = require "config" 5 | 6 | local get_site_config = config.get_site_config 7 | local is_site_option_on = config.is_site_option_on 8 | 9 | if is_site_option_on("waf") and get_site_config("waf").mode == "protection" then 10 | if ngx.status ~= 403 then 11 | if is_site_option_on("sensitiveDataFilter") or (is_site_option_on("bot") and get_site_config("bot").trap.state == "on") then 12 | ngx.header.content_length = nil 13 | end 14 | else 15 | ngx.header.server = "ZhongKui WAF" 16 | end 17 | end -------------------------------------------------------------------------------- /admin/admin/css/other/error.css: -------------------------------------------------------------------------------- 1 | * {padding: 0;margin: 0;font-size: 0.38rem;}ul {list-style: none;}a {text-decoration: none;-webkit-tap-highlight-color: transparent }.clearfix:after {content: '';width: 0;height: 0;display: block;clear: both;}html {height: 100%;width: 100%;}body {font-size: 0.28rem;height: 100%;width: 100%;display: flex;flex-direction: column;position: relative;background-color: white !important;}.content {position: absolute;top: 50%;transform: translateY(-50%);width: 100%;text-align: center;}.content>img {height: 300px;max-width: 370px;margin-right: 180px;}.content>* {display: inline-block;}.content-r {vertical-align: top;}.content-r>h1 {font-size: 72px;color: #434e59;margin-bottom: 24px;font-weight: 600;}.content-r>p {font-size: 20px;color: rgba(0, 0, 0, .45);margin-bottom: 16px;}button {margin-top: 20px;} -------------------------------------------------------------------------------- /lib/aes.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local aes = require "resty.aes" 5 | local str = require "resty.string" 6 | 7 | local tonumber = tonumber 8 | local gsub = string.gsub 9 | local char = string.char 10 | 11 | 12 | local _M = {} 13 | 14 | local function from_hex(s) 15 | return (gsub(s, '..', function(cc) 16 | return char(tonumber(cc, 16)) 17 | end)) 18 | end 19 | 20 | function _M.encrypt(key, msg, salt) 21 | local aes_128_cbc_md5 = aes:new(key, salt) 22 | local encrypted = aes_128_cbc_md5:encrypt(msg) 23 | return str.to_hex(encrypted) 24 | end 25 | 26 | function _M.decrypt(key, msg, salt) 27 | local aes_128_cbc_md5 = aes:new(key, salt) 28 | local encrypted = from_hex(msg) 29 | return aes_128_cbc_md5:decrypt(encrypted) 30 | end 31 | 32 | return _M 33 | -------------------------------------------------------------------------------- /admin/js/action.js: -------------------------------------------------------------------------------- 1 | let action = { 2 | "deny": "拒绝访问", 3 | "allow": "允许访问", 4 | "redirect": "拒绝访问并返回拦截页面", 5 | "captcha": "人机验证", 6 | "coding": "打码" 7 | } 8 | 9 | function initActionSelect(id, exclude, success) { 10 | var mySelect = document.getElementById(id); 11 | 12 | for (let key in action) { 13 | var option = document.createElement('option'); 14 | if (exclude && exclude === key) { 15 | continue; 16 | } 17 | option.text = action[key]; 18 | option.value = key; 19 | 20 | mySelect.appendChild(option); 21 | } 22 | 23 | if (success) { 24 | success(); 25 | } 26 | } 27 | 28 | function getActionText(actionType) { 29 | actionType = actionType.toLowerCase(); 30 | for (let key in action) { 31 | if (actionType === key) { 32 | return action[key]; 33 | } 34 | } 35 | return; 36 | } -------------------------------------------------------------------------------- /lib/decoder.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local _M = {} 5 | 6 | function _M.decode_base64(str) 7 | local str_new = str 8 | for t = 1, 2 do 9 | local temp = ngx.decode_base64(str_new) 10 | if not temp then 11 | break 12 | end 13 | str_new = temp 14 | end 15 | return str_new 16 | end 17 | 18 | function _M.unescape_uri(str) 19 | local str_new = str 20 | for t = 1, 2 do 21 | local temp = ngx.unescape_uri(str_new) 22 | if not temp then 23 | break 24 | end 25 | str_new = temp 26 | end 27 | return str_new 28 | end 29 | 30 | function _M.remove_comment(str) 31 | if str == nil then return nil end 32 | local str_new, n, err = ngx.re.gsub(str, "/\\*[\\s\\S]*\\*/", " ", "ijo") 33 | return str_new 34 | end 35 | 36 | 37 | return _M 38 | -------------------------------------------------------------------------------- /admin/conf/waf.conf: -------------------------------------------------------------------------------- 1 | lua_shared_dict dict_cclimit 10m; 2 | lua_shared_dict dict_accesstoken 5m; 3 | lua_shared_dict dict_blackip 10m; 4 | lua_shared_dict dict_locks 100k; 5 | lua_shared_dict dict_config 100k; 6 | lua_shared_dict dict_config_rules_hits 100k; 7 | lua_shared_dict dict_req_count 5m; 8 | lua_shared_dict dict_req_count_citys 10m; 9 | lua_shared_dict dict_sql_queue 10m; 10 | 11 | lua_package_path "/usr/local/openresty/zhongkui-waf/?.lua;/usr/local/openresty/zhongkui-waf/lib/?.lua;/usr/local/openresty/zhongkui-waf/admin/lua/?.lua;;"; 12 | init_by_lua_file /usr/local/openresty/zhongkui-waf/init.lua; 13 | init_worker_by_lua_file /usr/local/openresty/zhongkui-waf/init_worker.lua; 14 | access_by_lua_file /usr/local/openresty/zhongkui-waf/waf.lua; 15 | body_filter_by_lua_file /usr/local/openresty/zhongkui-waf/body_filter.lua; 16 | header_filter_by_lua_file /usr/local/openresty/zhongkui-waf/header_filter.lua; 17 | log_by_lua_file /usr/local/openresty/zhongkui-waf/log_and_traffic.lua; -------------------------------------------------------------------------------- /conf/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "attackLog": { 3 | "state": "off", 4 | "logPath": "/usr/local/openresty/nginx/logs/hack/", 5 | "jsonFormat": "off" 6 | }, 7 | "geoip": { 8 | "file": "/usr/local/share/GeoIP/GeoLite2-City.mmdb", 9 | "language": "zh-CN" 10 | }, 11 | "html": "", 12 | "secret": "2215D605B798A5CCEB6D5C900EE3585B", 13 | "mysql": { 14 | "state": "off", 15 | "host": "127.0.0.1", 16 | "port": "3306", 17 | "user": "", 18 | "password": "", 19 | "database": "zhongkui_waf", 20 | "poolSize": "10", 21 | "timeout": 5000 22 | }, 23 | "redis": { 24 | "state": "off", 25 | "host": "127.0.0.1", 26 | "password": "", 27 | "timeouts": "1000,1000,1000", 28 | "poolSize": 10, 29 | "ssl": "off", 30 | "port": 6379 31 | }, 32 | "rulesSort": { 33 | "state": "off", 34 | "period": 10 35 | } 36 | } -------------------------------------------------------------------------------- /admin/conf/waf.conf.default: -------------------------------------------------------------------------------- 1 | lua_shared_dict dict_cclimit 10m; 2 | lua_shared_dict dict_accesstoken 5m; 3 | lua_shared_dict dict_blackip 10m; 4 | lua_shared_dict dict_locks 100k; 5 | lua_shared_dict dict_config 100k; 6 | lua_shared_dict dict_config_rules_hits 100k; 7 | lua_shared_dict dict_req_count 5m; 8 | lua_shared_dict dict_req_count_citys 10m; 9 | lua_shared_dict dict_sql_queue 10m; 10 | 11 | lua_package_path "/usr/local/openresty/zhongkui-waf/?.lua;/usr/local/openresty/zhongkui-waf/lib/?.lua;/usr/local/openresty/zhongkui-waf/admin/lua/?.lua;;"; 12 | init_by_lua_file /usr/local/openresty/zhongkui-waf/init.lua; 13 | init_worker_by_lua_file /usr/local/openresty/zhongkui-waf/init_worker.lua; 14 | access_by_lua_file /usr/local/openresty/zhongkui-waf/waf.lua; 15 | body_filter_by_lua_file /usr/local/openresty/zhongkui-waf/body_filter.lua; 16 | header_filter_by_lua_file /usr/local/openresty/zhongkui-waf/header_filter.lua; 17 | log_by_lua_file /usr/local/openresty/zhongkui-waf/log_and_traffic.lua; -------------------------------------------------------------------------------- /lib/constants.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2024 bukale bukale2022@163.com 3 | 4 | local _M = {} 5 | 6 | _M.KEY_HTTP_4XX = 'http4x' 7 | _M.KEY_HTTP_5XX = 'http5x' 8 | _M.KEY_REQUEST_TIMES = 'request_times' 9 | _M.KEY_ATTACK_TIMES = 'attack_times' 10 | _M.KEY_BLOCK_TIMES_ATTACK = 'block_times_attack' 11 | _M.KEY_BLOCK_TIMES_CAPTCHA = 'block_times_captcha' 12 | _M.KEY_BLOCK_TIMES_CC = 'block_times_cc' 13 | _M.KEY_CAPTCHA_PASS_TIMES = 'captcha_pass_times' 14 | _M.KEY_ATTACK_PREFIX = 'attack_' 15 | _M.KEY_ATTACK_TYPE_PREFIX = 'attack_type_' 16 | _M.KEY_BLOCKED_PREFIX = 'blocked_' 17 | _M.KEY_ATTACK_LOG = 'attack_log' 18 | _M.KEY_IP_BLOCK_LOG = 'ip_block_log' 19 | _M.KEY_BLACKIP_PREFIX = 'black_ip:' 20 | _M.KEY_IP_GROUPS_WHITELIST = 'ipWhiteList' 21 | _M.KEY_IP_GROUPS_BLACKLIST = 'ipBlackList' 22 | _M.KEY_CAPTCHA_PREFIX = 'captcha:' 23 | _M.KEY_CAPTCHA_ACCESSTOKEN_REDIS_PREFIX = 'captcha_accesstoken:' 24 | 25 | return _M 26 | -------------------------------------------------------------------------------- /conf/global_rules/cc.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 3, 3 | "moduleName": "CC检测", 4 | "rules": [ 5 | { 6 | "id": 1, 7 | "attackType": "cc-url", 8 | "rule": "cc-url", 9 | "state": "off", 10 | "severityLevel": "medium", 11 | "countType": "url", 12 | "pattern": "", 13 | "autoIpBlock": "on", 14 | "ipBlockExpireInSeconds": 600, 15 | "action": "captcha", 16 | "duration": 60, 17 | "threshold": 1000, 18 | "description": "统计单个IP访问单个URL次数" 19 | }, 20 | { 21 | "id": 2, 22 | "attackType": "cc-ip", 23 | "rule": "cc-ip", 24 | "state": "off", 25 | "severityLevel": "medium", 26 | "countType": "ip", 27 | "pattern": "", 28 | "autoIpBlock": "on", 29 | "ipBlockExpireInSeconds": 600, 30 | "action": "captcha", 31 | "duration": 60, 32 | "threshold": 1000, 33 | "description": "统计单个IP访问次数" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /admin/js/severityLevel.js: -------------------------------------------------------------------------------- 1 | let severityLevelArray = [ 2 | { "type": "low", "name_en": "low", "name_cn": "低危" }, 3 | { "type": "medium", "name_en": "medium", "name_cn": "中危" }, 4 | { "type": "high", "name_en": "high", "name_cn": "高危" }, 5 | { "type": "critical", "name_en": "critical", "name_cn": "严重" } 6 | ] 7 | 8 | function initSeverityLevelSelect(id, success) { 9 | var mySelect = document.getElementById(id); 10 | 11 | for (let i in severityLevelArray) { 12 | let obj = severityLevelArray[i]; 13 | var option = document.createElement('option'); 14 | option.text = obj.name_cn; 15 | option.value = obj.type; 16 | 17 | mySelect.appendChild(option); 18 | } 19 | 20 | if (success) { 21 | success(); 22 | } 23 | } 24 | 25 | function getSeverityLevelText(severityLevel) { 26 | severityLevel = severityLevel.toLowerCase(); 27 | for (let i in severityLevelArray) { 28 | let obj = severityLevelArray[i]; 29 | if (severityLevel === obj.type) { 30 | return obj.name_cn; 31 | } 32 | } 33 | return severityLevel; 34 | } -------------------------------------------------------------------------------- /lib/time.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local ngxmatch = ngx.re.match 5 | local sub = string.sub 6 | local tonumber = tonumber 7 | 8 | local _M = {} 9 | 10 | function _M.calculate_seconds_to_next_midnight() 11 | local localtime = ngx.localtime() 12 | 13 | local m, err = ngxmatch(localtime, "(\\d+):(\\d+):(\\d+)", "jo") 14 | if not m then 15 | ngx.log(ngx.ERR, "failed to calculate ttl ", err) 16 | return nil 17 | end 18 | 19 | return 86400 - tonumber(m[1]) * 3600 - tonumber(m[2]) * 60 - tonumber(m[3]) 20 | end 21 | 22 | function _M.get_date_hour() 23 | local localtime = ngx.localtime() 24 | local hour = sub(localtime, 1, 13) 25 | return hour 26 | end 27 | 28 | function _M.get_hours() 29 | local hours = {} 30 | local today = ngx.today() 31 | local hour = nil 32 | for i = 0, 23 do 33 | if i < 10 then 34 | hour = today .. ' 0' .. i 35 | else 36 | hour = today .. ' ' .. i 37 | end 38 | hours[i + 1] = hour 39 | end 40 | 41 | return hours 42 | end 43 | 44 | return _M -------------------------------------------------------------------------------- /admin/component/pear/module/fullscreen.js: -------------------------------------------------------------------------------- 1 | layui.define(["message","table","jquery","element","yaml","form","tab","menu","frame","theme","convert"],function(e){"use strict";var n=layui.jquery.Deferred();e("fullscreen",new function(){this.func=null,this.onFullchange=function(e){this.func=e;for(var n=["fullscreenchange","webkitfullscreenchange","mozfullscreenchange","MSFullscreenChange"],l=0;l`;t("#"+e.elem).html("
"+n+'
\n\t\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t
')}(i),t("#"+i.elem).width(i.width),t("#"+i.elem).height(i.height),new n(i)},n.prototype.changePage=function(e,n){var a=t("#"+this.option.elem).find(".pear-frame-loading"),r=t("#"+this.option.elem+" iframe");r.attr("src",e),i(r,a,n)},n.prototype.changePageByElement=function(e,n,a,r){var o=t("#"+e).find(".pear-frame-loading"),l=t("#"+e+" iframe");l.attr("src",n),t("#"+e+" .title").html(a),i(l,o,r)},n.prototype.refresh=function(e){var n=t("#"+this.option.elem).find(".pear-frame-loading"),a=t("#"+this.option.elem).find("iframe");a.attr("src",a.attr("src")),i(a,n,e)},e("frame",new n)}); -------------------------------------------------------------------------------- /admin/admin/css/loader.css: -------------------------------------------------------------------------------- 1 | .loader-main{position: fixed;width: 100%;height: 100%;background-color: whitesmoke;z-index: 9999999;}.loader {width: 50px;height: 50px;margin: 30px auto 40px;margin-top: 20%;position: relative;z-index: 999999;background-color: whitesmoke;}.loader:before {content: "";width: 50px;height: 7px;border-radius: 50%;background: #000;opacity: 0.1;position: absolute;top: 59px;left: 0;animation: shadow .5s linear infinite;}.loader:after {content: "";width: 50px;height: 50px;border-radius: 3px;background-color: #5FB878;position: absolute;top: 0;left: 0;animation: loading .5s linear infinite;}@-webkit-keyframes loading {17% {border-bottom-right-radius: 3px;}25% {transform: translateY(9px) rotate(22.5deg);}50% {transform: translateY(18px) scale(1, 0.9) rotate(45deg);border-bottom-right-radius: 40px;}75% {transform: translateY(9px) rotate(67.5deg);}100% {transform: translateY(0) rotate(90deg);}}@keyframes loading {17% {border-bottom-right-radius: 3px;}25% {transform: translateY(9px) rotate(22.5deg);}50% {transform: translateY(18px) scale(1, 0.9) rotate(45deg);border-bottom-right-radius: 40px;}75% {transform: translateY(9px) rotate(67.5deg);}100% {transform: translateY(0) rotate(90deg);}}@-webkit-keyframes shadow {0%, 100% {transform: scale(1, 1);}50% {transform: scale(1.2, 1);}}@keyframes shadow {0%, 100% {transform: scale(1, 1);}50% {transform: scale(1.2, 1);}} -------------------------------------------------------------------------------- /lib/arrays.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local bit = require "bit" 5 | local nkeys = require "table.nkeys" 6 | local isempty = require "table.isempty" 7 | 8 | local rshift = bit.rshift 9 | 10 | local _M = {} 11 | 12 | local INDEX_OUT_OF_RANGE = "String index out of range: " 13 | 14 | function _M.binary_search(array, from_index, ro_index, item) 15 | if isempty(array) then 16 | return -1 17 | end 18 | 19 | if from_index > ro_index then 20 | error("out of range: " .. from_index .. "," .. ro_index) 21 | end 22 | 23 | if from_index < 1 then 24 | error(INDEX_OUT_OF_RANGE .. from_index) 25 | end 26 | 27 | local array_length = nkeys(array) 28 | if ro_index > array_length then 29 | error(INDEX_OUT_OF_RANGE .. ro_index) 30 | end 31 | 32 | local low = from_index 33 | local high = ro_index 34 | 35 | while low <= high do 36 | local mid = rshift(low + high, 1) 37 | --local mid = math.ceil((low + high) / 2) 38 | 39 | local midval = array[mid] 40 | if midval < item then 41 | low = mid + 1 42 | elseif midval > item then 43 | high = mid - 1 44 | else 45 | return mid 46 | end 47 | end 48 | 49 | return -low 50 | end 51 | 52 | return _M -------------------------------------------------------------------------------- /waf.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local geoip = require "geoip" 5 | local config = require "config" 6 | local lib = require "lib" 7 | local ip_utils = require "ip_utils" 8 | local request = require "request" 9 | local stringutf8 = require "stringutf8" 10 | 11 | local default_if_blank = stringutf8.default_if_blank 12 | local generate_id = request.generate_id 13 | local is_site_option_on = config.is_site_option_on 14 | local get_client_ip = ip_utils.get_client_ip 15 | 16 | local function init() 17 | local ctx = ngx.ctx 18 | 19 | local ip = get_client_ip() 20 | ctx.ip = ip 21 | 22 | ctx.ua = default_if_blank(ngx.var.http_user_agent, '') 23 | 24 | ctx.geoip = geoip.lookup(ip) 25 | 26 | ctx.request_id = generate_id() 27 | 28 | ctx.server_name = default_if_blank(ngx.var.server_name, 'unknown') 29 | end 30 | 31 | if is_site_option_on("waf") then 32 | 33 | init() 34 | 35 | lib.is_white_ip() 36 | 37 | lib.is_black_ip() 38 | 39 | lib.is_unsafe_http_method() 40 | 41 | lib.is_bot() 42 | 43 | lib.is_acl() 44 | 45 | lib.is_cc() 46 | 47 | lib.is_white_url() 48 | 49 | lib.is_black_url() 50 | 51 | lib.is_evil_args() 52 | 53 | lib.is_evil_headers() 54 | 55 | lib.is_evil_cookies() 56 | 57 | lib.is_evil_request_body() 58 | 59 | end 60 | -------------------------------------------------------------------------------- /admin/component/pear/pear.js: -------------------------------------------------------------------------------- 1 | window.rootPath = (function (src) { 2 | src = document.currentScript 3 | ? document.currentScript.src 4 | : document.scripts[document.scripts.length - 1].src; 5 | return src.substring(0, src.lastIndexOf("/") + 1); 6 | })(); 7 | 8 | layui.config({ 9 | base: rootPath + "module/", 10 | version: "3.40.0" 11 | }).extend({ 12 | admin: "admin", // 框架布局组件 13 | common: "common", // 公共方法封装 14 | menu: "menu", // 数据菜单组件 15 | frame: "frame", // 内容页面组件 16 | tab: "tab", // 多选项卡组件 17 | echarts: "echarts", // 数据图表组件 18 | echartsTheme: "echartsTheme",// 数据图表主题 19 | drawer: "drawer", // 抽屉弹层组件 20 | tag:"tag", // 多标签页组件 21 | popup:"popup", // 弹层封装 22 | count:"count", // 数字滚动 23 | topBar: "topBar", // 置顶组件 24 | button: "button", // 加载按钮 25 | loading: "loading", // 加载组件 26 | convert:"convert", // 数据转换 27 | yaml:"yaml", // yaml 解析组件 28 | context: "context", // 上下文组件 29 | theme: "theme", // 主题转换 30 | message: "message", // 通知组件 31 | toast: "toast", // 消息通知 32 | fullscreen:"fullscreen" //全屏组件 33 | }).use(['layer', 'theme', 'jquery'], function () { 34 | layui.theme.changeTheme(window, false); 35 | var $ = layui.$; 36 | $.ajaxSetup({ 37 | complete(xhr, status) { 38 | if (xhr.status == 401) { 39 | window.parent.location.href = '/login.html'; 40 | } 41 | }}); 42 | }); -------------------------------------------------------------------------------- /admin/component/pear/css/module/tag.css: -------------------------------------------------------------------------------- 1 | .input-new-tag { 2 | width: 90px; 3 | } 4 | 5 | .input-new-tag input { 6 | height: 100%!important; 7 | border: none; 8 | padding-left: 0px; 9 | } 10 | 11 | .tag .layui-btn .tag-close:hover { 12 | border-radius: 2px; 13 | color: #fff; 14 | } 15 | 16 | .tag .layui-btn .tag-close { 17 | margin-left: 8px; 18 | transition: all .2s; 19 | -webkit-transition: all .2s; 20 | } 21 | .tag-item { 22 | background-color: #5FB878; 23 | color: white; 24 | border: none; 25 | } 26 | 27 | .tag-item:hover { 28 | 29 | color: white; 30 | 31 | } 32 | .tag-item-normal { 33 | background-color: #5FB878; 34 | color: white; 35 | border: none; 36 | } 37 | 38 | .tag-item-warm { 39 | background-color: #f6ad55; 40 | color: white; 41 | border: none; 42 | } 43 | 44 | .tag-item-danger { 45 | background-color: #f56c6c; 46 | color: white; 47 | border: none; 48 | } 49 | 50 | .tag-item-dark { 51 | background-color: #525252; 52 | color: white; 53 | border: none; 54 | } 55 | 56 | .tag-item-primary { 57 | background-color: white !important; 58 | color: dimgray; 59 | border: 1px solid dimgray; 60 | } 61 | 62 | .tag-item-normal:hover { 63 | 64 | color: white !important; 65 | } 66 | 67 | .tag-item-warm:hover { 68 | 69 | color: white; 70 | } 71 | 72 | .tag-item-danger:hover { 73 | 74 | color: white; 75 | } 76 | 77 | .tag-item-dark:hover { 78 | 79 | color: white; 80 | } 81 | 82 | .tag-item-primary:hover { 83 | color: dimgray; 84 | border: 1px solid dimgray; 85 | } -------------------------------------------------------------------------------- /lib/utils.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2024 bukale bukale2022@163.com 3 | 4 | local timerat = ngx.timer.at 5 | local every = ngx.timer.every 6 | 7 | local _M = {} 8 | 9 | function _M.start_timer(delay, callback, ...) 10 | local ok, err = timerat(delay, callback, ...) 11 | if not ok then 12 | ngx.log(ngx.ERR, "failed to create timer: ", err) 13 | return 14 | end 15 | 16 | return ok, err 17 | end 18 | 19 | function _M.start_timer_every(delay, callback, ...) 20 | local ok, err = every(delay, callback, ...) 21 | if not ok then 22 | ngx.log(ngx.ERR, "failed to create the timer: ", err) 23 | return 24 | end 25 | 26 | return ok, err 27 | end 28 | 29 | function _M.dict_incr(dict, key, ttl) 30 | local newval, err = dict:incr(key, 1) 31 | if not newval then 32 | if ttl then 33 | local t = type(ttl) 34 | if t == 'number' then 35 | dict:set(key, 1, ttl) 36 | elseif t == 'function' then 37 | dict:set(key, 1, ttl()) 38 | end 39 | else 40 | dict:set(key, 1) 41 | end 42 | 43 | return 1 44 | end 45 | 46 | return newval, err 47 | end 48 | 49 | function _M.dict_set(dict, key, value, ttl) 50 | return dict:set(key, value, ttl) 51 | end 52 | 53 | function _M.dict_get(dict, key) 54 | return dict:get(key) 55 | end 56 | 57 | return _M 58 | -------------------------------------------------------------------------------- /admin/admin/css/other/login.css: -------------------------------------------------------------------------------- 1 | .layui-form {width: 320px !important;margin: auto !important;margin-top: 160px !important;}.layui-form button {width: 100% !important;height: 44px !important;line-height: 44px !important;font-size: 16px !important;background-color: #5FB878 !important;font-weight: 550 !important;}.layui-form-checked[lay-skin=primary] i {border-color: #5FB878 !important;background-color: #5FB878 !important;color: #fff !important;}.layui-tab-content {margin-top: 15px !important;padding-left: 0px !important;padding-right: 0px !important;}.layui-form-item {margin-top: 20px !important;}.layui-input:focus {box-shadow: 0px 0px 2px 1px #5FB878 !important;}.layui-form-danger:focus{box-shadow: 0px 0px 2px 1px #f56c6c !important;}.logo {width: 60px !important;border-radius: 12px;margin-top: 10px !important;margin-left: 8px !important;}.title {font-size: 30px !important;font-weight: 550 !important;margin-left: 20px !important;color: #5FB878 !important;display: inline-block !important;height: 60px !important;line-height: 60px !important;margin-top: 10px !important;position: absolute !important;}.desc {width: 100% !important;text-align: center !important;color: gray !important;height: 50px !important;line-height: 50px !important;}body {background-repeat:no-repeat;background-color: whitesmoke;background-size: 100%;height: 100%;}.code {float: left;margin-right: 13px;margin: 0px !important;border: #e6e6e6 1px solid;display: inline-block!important;}.codeImage {float: right;height: 42px;border: #e6e6e6 1px solid;cursor: pointer;}@media (max-width:768px){body{background-position:center;}} -------------------------------------------------------------------------------- /admin/component/pear/css/module/form.css: -------------------------------------------------------------------------------- 1 | input::-webkit-input-placeholder, 2 | textarea::-webkit-input-placeholder { 3 | color: #ccc; 4 | } 5 | 6 | .layui-input:hover, 7 | .layui-textarea:hover, 8 | .layui-input:focus, 9 | .layui-textarea:focus { 10 | border-color: #eee; 11 | } 12 | 13 | .layui-input:focus, 14 | .layui-textarea:focus { 15 | border-color: #5FB878 !important; 16 | box-shadow: 0 0 0 3px #f0f9eb !important; 17 | } 18 | 19 | .layui-input[success] { 20 | box-shadow: 0px 0px 0px 3px #f0f9eb !important; 21 | border: #5FB878 1px solid!important; 22 | } 23 | 24 | .layui-input[failure], 25 | .layui-form-item .layui-form-danger:focus { 26 | box-shadow: 0px 0px 0px 3px #fef0f0 !important; 27 | border: #F56C6C 1px solid!important; 28 | } 29 | 30 | .layui-input, 31 | .layui-select, 32 | .layui-textarea { 33 | border-radius: 4px; 34 | border-color: #eee; 35 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 36 | } 37 | 38 | .layui-form-select dl::-webkit-scrollbar { 39 | width: 0px; 40 | height: 0px; 41 | } 42 | 43 | .layui-form-select dl::-webkit-scrollbar { 44 | width: 6px; 45 | height: 6px; 46 | } 47 | 48 | .layui-form-select dl::-webkit-scrollbar-track { 49 | background: white; 50 | border-radius: 3px; 51 | } 52 | 53 | .layui-form-select dl::-webkit-scrollbar-thumb { 54 | background: #E6E6E6; 55 | border-radius: 3px; 56 | } 57 | 58 | .layui-form-select dl::-webkit-scrollbar-thumb:hover { 59 | background: #E6E6E6; 60 | } 61 | 62 | .layui-form-select dl::-webkit-scrollbar-corner { 63 | background: #f6f6f6; 64 | } 65 | 66 | /* layui 2.6.9 样式变化 */ 67 | .layui-form-select dl dd.layui-this{ 68 | background-color: #F6F6F6; 69 | font-weight: 700; 70 | } 71 | -------------------------------------------------------------------------------- /admin/lua/lib/pager.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local tonumber = tonumber 5 | 6 | local _M = {} 7 | 8 | local mt = { __index = _M } 9 | 10 | function _M:new(page, limit) 11 | page = tonumber(page) or 1 -- 第几页 12 | if page < 1 then 13 | page = 1 14 | end 15 | 16 | local t = { 17 | page = tonumber(page) or 1, -- 第几页 18 | limit = tonumber(limit) or 10, -- 每页大小 19 | totalPages = 0, -- 总页数 20 | totalSize = 0 -- 总记录数 21 | } 22 | 23 | setmetatable(t, mt) 24 | return t 25 | end 26 | 27 | local function get_page(page) 28 | page = tonumber(page) or 1 29 | if page < 1 then 30 | page = 1 31 | end 32 | return page 33 | end 34 | 35 | local function get_limit(limit) 36 | limit = tonumber(limit) or 10 37 | if limit < 1 then 38 | limit = 1 39 | end 40 | return limit 41 | end 42 | 43 | -- 获取起始下标,从0开始 44 | function _M.get_begin(page, limit) 45 | page = get_page(page) 46 | limit = get_limit(limit) 47 | return (page - 1) * limit 48 | end 49 | 50 | -- 获取截止下标,从0开始 51 | function _M.get_end(page, limit) 52 | page = get_page(page) 53 | limit = get_limit(limit) 54 | return (page - 1) * limit + limit - 1 55 | end 56 | 57 | -- 获取起始下标,从1开始 58 | function _M.get_lua_begin(page, limit) 59 | page = get_page(page) 60 | limit = get_limit(limit) 61 | return (page - 1) * limit + 1 62 | end 63 | 64 | -- 获取截止下标,从1开始 65 | function _M.get_lua_end(page, limit) 66 | page = get_page(page) 67 | limit = get_limit(limit) 68 | return (page - 1) * limit + limit 69 | end 70 | 71 | return _M 72 | -------------------------------------------------------------------------------- /html/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ZhongKui WAF 10 | 47 | 48 | 49 |
50 |
403
51 |
Your request has been blocked by ZhongKui WAF!
52 |
53 |

请求ID: $request_id

54 |

拦截时间: $blocked_time

55 |

客户端IP: $remote_addr

56 |

客户端标识: $user_agent

57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /admin/component/pear/css/module/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #29d; 8 | 9 | position: fixed; 10 | z-index: 999999; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d; 26 | opacity: 1.0; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | 33 | /* Remove these to get rid of the spinner */ 34 | #nprogress .spinner { 35 | display: block; 36 | position: fixed; 37 | z-index: 1031; 38 | top: 15px; 39 | right: 15px; 40 | } 41 | 42 | #nprogress .spinner-icon { 43 | width: 18px; 44 | height: 18px; 45 | box-sizing: border-box; 46 | 47 | border: solid 2px transparent; 48 | border-top-color: #29d; 49 | border-left-color: #29d; 50 | border-radius: 50%; 51 | 52 | -webkit-animation: nprogress-spinner 400ms linear infinite; 53 | animation: nprogress-spinner 400ms linear infinite; 54 | } 55 | 56 | .nprogress-custom-parent { 57 | overflow: hidden; 58 | position: relative; 59 | } 60 | 61 | .nprogress-custom-parent #nprogress .spinner, 62 | .nprogress-custom-parent #nprogress .bar { 63 | position: absolute; 64 | } 65 | 66 | @-webkit-keyframes nprogress-spinner { 67 | 0% { -webkit-transform: rotate(0deg); } 68 | 100% { -webkit-transform: rotate(360deg); } 69 | } 70 | @keyframes nprogress-spinner { 71 | 0% { transform: rotate(0deg); } 72 | 100% { transform: rotate(360deg); } 73 | } -------------------------------------------------------------------------------- /admin/config/pear.config.yml: -------------------------------------------------------------------------------- 1 | ## 网站配置 2 | logo: 3 | ## 网站名称 4 | title: "ZhongKui WAF" 5 | ## 网站图标 6 | image: "admin/images/logo.png" 7 | ## 菜单配置 8 | menu: 9 | ## 菜单数据来源 10 | data: "admin/data/menu.json" 11 | ## 菜单接口的请求方式 GET / POST 12 | method: "GET" 13 | ## 是否同时只打开一个菜单目录 14 | accordion: true 15 | ## 侧边默认折叠状态 16 | collapse: false 17 | ## 是否开启多系统菜单模式 18 | control: false 19 | ## 顶部菜单宽度 PX 20 | controlWidth: 500 21 | ## 默认选中的菜单项 22 | select: "1" 23 | ## 是否开启异步菜单,false 时 data 属性设置为静态数据,true 时为后端接口 24 | async: true 25 | ## 视图内容配置 26 | tab: 27 | ## 是否开启多选项卡 28 | enable: false 29 | ## 保持视图状态 30 | keepState: true 31 | ## 开启选项卡记忆 32 | session: true 33 | ## 浏览器刷新时是否预加载非激活标签页 34 | preload: true 35 | ## 可打开的数量, false 不限制极值 36 | max: "30" 37 | ## 首页 38 | index: 39 | id: "1" ## 标识 ID , 建议与菜单项中的 ID 一致 40 | href: "view/dashboard.html" ## 页面地址 41 | title: "数据统计" ## 标题 42 | ## 主题配置 43 | theme: 44 | ## 默认主题色,对应 colors 配置中的 ID 标识 45 | defaultColor: "2" 46 | ## 默认的菜单主题 dark-theme 黑 / light-theme 白 47 | defaultMenu: "dark-theme" 48 | ## 默认的顶部主题 dark-theme 黑 / light-theme 白 49 | defaultHeader: "light-theme" 50 | ## 是否允许用户切换主题,false 时关闭自定义主题面板 51 | allowCustom: true 52 | ## 通栏配置 53 | banner: false 54 | ## 主题色配置列表 55 | colors: 56 | - id: "1" 57 | color: "#2d8cf0" 58 | second: "#ecf5ff" 59 | - id: "2" 60 | color: "#36b368" 61 | second: "#f0f9eb" 62 | - id: "3" 63 | color: "#f6ad55" 64 | second: "#fdf6ec" 65 | - id: "4" 66 | color: "#f56c6c" 67 | second: "#fef0f0" 68 | - id: "5" 69 | color: "#3963bc" 70 | second: "#ecf5ff" 71 | ## 其他配置 72 | other: 73 | ## 主页动画时长 74 | keepLoad: "120" 75 | ## 布局顶部主题 76 | autoHead: false 77 | ## 页脚 78 | footer: true 79 | ## 头部配置 80 | header: 81 | ## 站内消息,通过 false 设置关闭 82 | message: false -------------------------------------------------------------------------------- /conf/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "waf": { 3 | "state": "on", 4 | "mode": "protection" 5 | }, 6 | "args": { 7 | "state": "on" 8 | }, 9 | "acl": { 10 | "state": "off" 11 | }, 12 | "blackIP": { 13 | "state": "off" 14 | }, 15 | "blackUrl": { 16 | "state": "on" 17 | }, 18 | "bot": { 19 | "state": "off", 20 | "trap": { 21 | "state": "off", 22 | "action": "deny", 23 | "autoIpBlock": "off", 24 | "ipBlockExpireInSeconds": "600", 25 | "uri": "/zhongkuiwaf/honey/trap" 26 | }, 27 | "captcha": { 28 | "state": "off", 29 | "action": "captcha", 30 | "verifyInSeconds": 60, 31 | "maxFailTimes": 3, 32 | "autoIpBlock": "off", 33 | "ipBlockExpireInSeconds": 600, 34 | "expireInSeconds": 1800, 35 | "type": "js_challenge" 36 | } 37 | }, 38 | "cc": { 39 | "state": "off" 40 | }, 41 | "cookie": { 42 | "state": "on" 43 | }, 44 | "fileExt": { 45 | "state": "on" 46 | }, 47 | "fileContentCheck": { 48 | "state": "on" 49 | }, 50 | "geoip": { 51 | "disallowCountrys": [], 52 | "language": "zh-CN" 53 | }, 54 | "headers": { 55 | "state": "on" 56 | }, 57 | "httpMethod": { 58 | "state": "off" 59 | }, 60 | "post": { 61 | "state": "on" 62 | }, 63 | "sensitiveDataFilter": { 64 | "state": "off" 65 | }, 66 | "sqli": { 67 | "state": "on" 68 | }, 69 | "whiteIP": { 70 | "state": "off" 71 | }, 72 | "whiteUrl": { 73 | "state": "off" 74 | }, 75 | "xss": { 76 | "state": "on" 77 | } 78 | } -------------------------------------------------------------------------------- /admin/js/attackType.js: -------------------------------------------------------------------------------- 1 | let attackTypeArray = [ 2 | {"type":"sqli","name_en":"SQL Injection","name_cn":"SQL注入"}, 3 | {"type":"xss","name_en":"XSS","name_cn":"XSS"}, 4 | {"type":"acl","name_en":"ACL","name_cn":"ACL(访问控制列表)"}, 5 | {"type":"file_ext","name_en":"file ext","name_cn":"上传文件类型黑名单"}, 6 | {"type":"blackurl","name_en":"URL Blacklist","name_cn":"URL黑名单"}, 7 | {"type":"blackip","name_en":"IP Blacklist","name_cn":"IP黑名单"}, 8 | {"type":"unsafe_method","name_en":"unsafe http method","name_cn":"不允许的HTTP方法"}, 9 | {"type":"bot","name_en":"Bot","name_cn":"Bot"}, 10 | {"type":"bot_trap","name_en":"bot trap","name_cn":"Bot陷阱"}, 11 | {"type":"directory_traversal","name_en":"Directory Traversal","name_cn":"目录穿越"}, 12 | {"type":"commandi","name_en":"Command Injection","name_cn":"命令注入"}, 13 | {"type":"rce","name_en":"Remote Code Exec","name_cn":"代码执行"}, 14 | {"type":"codei","name_en":"Code Injection","name_cn":"代码注入"}, 15 | {"type":"backdoor","name_en":"backdoor","name_cn":"后门"}, 16 | {"type":"data_leak","name_en":"Data Leak","name_cn":"信息泄露"}, 17 | {"type":"read_file","name_en":"Read File","name_cn":"文件读取"}, 18 | {"type":"unknown","name_en":"unknown","name_cn":"未知"} 19 | ] 20 | 21 | function initAttackTypeSelect(id, success) { 22 | var mySelect = document.getElementById(id); 23 | 24 | for (let i in attackTypeArray) { 25 | let obj = attackTypeArray[i]; 26 | var option = document.createElement('option'); 27 | option.text = obj.name_cn; 28 | option.value = obj.type; 29 | 30 | mySelect.appendChild(option); 31 | } 32 | 33 | if (success) { 34 | success(); 35 | } 36 | } 37 | 38 | function getAttackTypeText(attackType) { 39 | attackType = attackType.toLowerCase(); 40 | for (let i in attackTypeArray) { 41 | let obj = attackTypeArray[i]; 42 | if (attackType === obj.type) { 43 | return obj.name_cn; 44 | } 45 | } 46 | return attackType; 47 | } -------------------------------------------------------------------------------- /lib/mysql_cli.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local mysql = require "resty.mysql" 5 | local config = require "config" 6 | 7 | local _M = {} 8 | 9 | local mysql_config = config.get_system_config("mysql") 10 | local host = mysql_config.host 11 | local port = mysql_config.port 12 | local user = mysql_config.user 13 | local password = mysql_config.password 14 | local database = mysql_config.database 15 | local poolSize = mysql_config.poolSize 16 | local timeout = mysql_config.timeout or 1000 17 | 18 | function _M.get_connection() 19 | local db, err = mysql:new() 20 | if not db then 21 | ngx.log(ngx.ERR, "failed to instantiate mysql: ", err) 22 | return nil, err 23 | end 24 | 25 | db:set_timeout(timeout) 26 | 27 | local ok, err, errcode, sql_state = db:connect{ 28 | host = host, 29 | port = port or 3306, 30 | database = database, 31 | user = user, 32 | password = password, 33 | charset = "utf8mb4", 34 | max_packet_size = 1024 * 1024, 35 | pool_size = poolSize or 10 36 | } 37 | 38 | if not ok then 39 | ngx.log(ngx.ERR, "failed to connect: ", err, ": ", errcode, " ", sql_state) 40 | return nil, err 41 | end 42 | 43 | return db, err 44 | end 45 | 46 | function _M.query(sql, rows) 47 | local res, err, errcode, sql_state 48 | local db = _M.get_connection() 49 | if db then 50 | res, err, errcode, sql_state = db:query(sql, rows) 51 | if not res then 52 | ngx.log(ngx.ERR, "bad result: ", err, ": ", errcode, ": ", sql_state, ".") 53 | return 54 | end 55 | 56 | _M.close_connection(db) 57 | end 58 | 59 | return res 60 | end 61 | 62 | 63 | function _M.close_connection(db) 64 | -- put it into the connection pool of size 100, 65 | -- with 10 seconds max idle timeout 66 | local ok, err = db:set_keepalive(10000, poolSize or 10) 67 | if not ok then 68 | ngx.log(ngx.ERR, "failed to set keepalive: ", err) 69 | end 70 | 71 | return ok, err 72 | end 73 | 74 | 75 | return _M 76 | -------------------------------------------------------------------------------- /conf/global_rules/httpMethod.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 10, 3 | "moduleName": "HTTP方法检测", 4 | "rules": [ 5 | { 6 | "id": 1, 7 | "state": "off", 8 | "action": "DENY", 9 | "rule": "GET", 10 | "attackType": "unsafe_method", 11 | "severityLevel": "high" 12 | }, 13 | { 14 | "id": 2, 15 | "state": "off", 16 | "action": "DENY", 17 | "rule": "POST", 18 | "attackType": "unsafe_method", 19 | "severityLevel": "high" 20 | }, 21 | { 22 | "id": 3, 23 | "state": "off", 24 | "action": "DENY", 25 | "rule": "HEAD", 26 | "attackType": "unsafe_method", 27 | "severityLevel": "high" 28 | }, 29 | { 30 | "id": 4, 31 | "state": "off", 32 | "action": "DENY", 33 | "rule": "PUT", 34 | "attackType": "unsafe_method", 35 | "severityLevel": "high" 36 | }, 37 | { 38 | "id": 5, 39 | "state": "off", 40 | "action": "DENY", 41 | "rule": "DELETE", 42 | "attackType": "unsafe_method", 43 | "severityLevel": "high" 44 | }, 45 | { 46 | "id": 6, 47 | "state": "off", 48 | "action": "DENY", 49 | "rule": "CONNECT", 50 | "attackType": "unsafe_method", 51 | "severityLevel": "high" 52 | }, 53 | { 54 | "id": 7, 55 | "state": "off", 56 | "action": "DENY", 57 | "rule": "OPTIONS", 58 | "attackType": "unsafe_method", 59 | "severityLevel": "high" 60 | }, 61 | { 62 | "id": 8, 63 | "state": "off", 64 | "action": "DENY", 65 | "rule": "TRACE", 66 | "attackType": "unsafe_method", 67 | "severityLevel": "high" 68 | }, 69 | { 70 | "id": 9, 71 | "state": "off", 72 | "action": "DENY", 73 | "rule": "PATCH", 74 | "attackType": "unsafe_method", 75 | "severityLevel": "high" 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /lib/ip_utils.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local _M = {} 5 | 6 | local stringutf8 = require "stringutf8" 7 | 8 | local pairs = pairs 9 | local ipairs = ipairs 10 | local tonumber = tonumber 11 | local find = string.find 12 | local sub = string.sub 13 | local trim = stringutf8.trim 14 | local ngxmatch = ngx.re.match 15 | 16 | local insert = table.insert 17 | 18 | function _M.get_client_ip() 19 | local var = ngx.var 20 | local ips = { 21 | var.http_x_forwarded_for, 22 | var.http_proxy_client_ip, 23 | var.http_wl_proxy_client_ip, 24 | var.http_http_client_ip, 25 | var.http_http_x_forwarded_for, 26 | var.remote_addr 27 | } 28 | 29 | for _, ip in pairs(ips) do 30 | if ip and #ip > 0 then 31 | local idx = find(ip, ",") 32 | if idx and idx > 0 then 33 | ip = sub(ip, 1, idx - 1) 34 | end 35 | 36 | return trim(ip) 37 | end 38 | end 39 | 40 | return "unknown" 41 | end 42 | 43 | -- 是否内网IP 44 | function _M.is_private_ip(ip) 45 | if not ip then 46 | return false 47 | end 48 | 49 | if ip == '127.0.0.1' then 50 | return true 51 | end 52 | 53 | local m, err = ngxmatch(ip, '(\\d{1,3})\\.(\\d{1,3})\\.(?:\\d{1,3})\\.(?:\\d{1,3})', 'isjo') 54 | if m then 55 | local a, b = tonumber(m[1]), tonumber(m[2]) 56 | if a == 10 then 57 | return true 58 | elseif a == 172 and b >= 16 and b <= 31 then 59 | return true 60 | elseif a == 192 and b == 168 then 61 | return true 62 | end 63 | else 64 | if err then 65 | ngx.log(ngx.ERR, "error: ", err) 66 | return 67 | end 68 | end 69 | 70 | return false 71 | end 72 | 73 | -- 把配置中混合在一起的单ip和ip网段区分开,{ip网段table},{ips} 74 | function _M.filter_ip_list(ips) 75 | local t1, t2 = {}, {} 76 | 77 | if ips and #ips > 0 then 78 | for _, v in ipairs(ips) do 79 | if find(v, '/') then 80 | insert(t1, v) 81 | else 82 | insert(t2, v) 83 | end 84 | end 85 | end 86 | 87 | return t1, t2 88 | end 89 | 90 | return _M 91 | -------------------------------------------------------------------------------- /admin/component/pear/module/tag.js: -------------------------------------------------------------------------------- 1 | layui.define("jquery",function(t){"use strict";var e=layui.$,n="layui-btn layui-btn-primary layui-btn-sm";(i=function(){this.config={skin:n,tagText:"+ New Tag"},this.configs={}}).prototype.set=function(t){return e.extend(!0,this.config,t),i.render(),this},i.prototype.on=function(t,e){return layui.onevent.call(this,"tag",t,e)},i.prototype.add=function(t,n){var i=e(".tag[lay-filter="+t+"]");return a.add(null,i,n),a.tagAuto(t),this},i.prototype.delete=function(t,n){var i=e(".tag[lay-filter="+t+"]").find('>.tag-item[lay-id="'+n+'"]');return a.delete(null,i),this};var a={tagClick:function(t,n,a,i){i=i||{};var l=a||e(this),o=(n=n||l.index(l),l.parents(".tag").eq(0)),r=o.attr("lay-filter");layui.event.call(this,"tag","click("+r+")",{elem:o,index:n})},add:function(t,e,n){var a=e.attr("lay-filter"),i=e.children(".button-new-tag"),l=i.index(),o='";!1!==layui.event.call(this,"tag","add("+a+")",{elem:e,index:l,othis:o})&&(i[0]?i.before(o):e.append(o))},input:function(t,n){var l=n||e(this),o=l.parents(".tag").eq(0),r=o.attr("lay-filter"),u=i.configs[r]=e.extend({},i.config,i.configs[r]||{},u),s=e('
');s.addClass(u.skin),l.after(s).remove(),s.children(".layui-input").on("blur",function(){if(this.value){var t={text:this.value};a.add(null,o,t)}s.remove(),a.tagAuto(r)}).focus()},delete:function(t,n){var a=n||e(this).parent(),i=a.index(),l=a.parents(".tag").eq(0),o=l.attr("lay-filter");!1!==layui.event.call(this,"tag","delete("+o+")",{elem:l,index:i})&&a.remove()},tagAuto:function(t){var l=(t=t||"")&&i.configs[t]||i.config;e(".tag"+(t?'[lay-filter="'+t+'"]':"")).each(function(){var t=e(this),i=t.children(".tag-item"),o=t.children(".button-new-tag");i.removeClass(n).addClass(l.skin),t.attr("lay-allowClose")&&i.length&&i.each(function(){var t=e(this);if(!t.find(".tag-close")[0]){var n=e('');n.on("click",a.delete),t.append(n)}}),t.attr("lay-newTag")&&0===o.length&&((o=e('')).on("click",a.input),t.append(o)),o.html(l.tagText),o.removeClass(n).addClass(l.skin)})}};i.prototype.init=function(t,n){return t&&(i.configs[t]=e.extend({},i.config,i.configs[t]||{},n)),a.tagAuto.call(this,t)},i.prototype.render=i.prototype.init;var i=new i,l=e(document);i.render(),l.on("click",".tag-item",a.tagClick),t("tag",i)}); -------------------------------------------------------------------------------- /body_filter.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local sensitive = require "sensitive_data_filter" 5 | local config = require "config" 6 | local stringutf8 = require "stringutf8" 7 | 8 | local ngxfind = ngx.re.find 9 | local gsub = string.gsub 10 | local format = string.format 11 | local default_if_blank = stringutf8.default_if_blank 12 | 13 | local get_site_config = config.get_site_config 14 | local is_site_option_on = config.is_site_option_on 15 | 16 | local CONTENT_TYPE_REGEX = "^(?:text/html|text/plain|text/xml|application/json|application/xml|application/xhtml\\+xml)" 17 | local HTML_CONTENT_TYPE_REGEX = "^(?:text/html|application/xhtml\\+xml)" 18 | local TRAP_HTML = 'come-here' 19 | 20 | local content = ngx.arg[1] 21 | 22 | if is_site_option_on("waf") then 23 | if get_site_config("waf").mode == "protection" then 24 | if ngx.status ~= 403 then 25 | local content_type = ngx.header.content_type or '' 26 | if is_site_option_on("sensitiveDataFilter") then 27 | if content_type then 28 | local from = ngxfind(content_type, CONTENT_TYPE_REGEX, "isjo") 29 | if from then 30 | if content then 31 | content = sensitive.data_filter(content) 32 | end 33 | end 34 | end 35 | end 36 | 37 | if is_site_option_on("bot") then 38 | local trap = get_site_config("bot").trap 39 | local trap_uri = trap.uri or '' 40 | if trap.state == "on" then 41 | if content_type then 42 | local from = ngxfind(content_type, HTML_CONTENT_TYPE_REGEX, "isjo") 43 | if from then 44 | if content then 45 | content = gsub(content, '', format(TRAP_HTML, trap_uri)) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | local is_attack = ngx.ctx.is_attack 55 | if is_attack then 56 | local response_body = ngx.ctx.response_body 57 | if content then 58 | ngx.ctx.response_body = default_if_blank(response_body, "") .. content 59 | end 60 | end 61 | 62 | end 63 | 64 | ngx.arg[1] = content 65 | -------------------------------------------------------------------------------- /admin/component/pear/css/module/table.css: -------------------------------------------------------------------------------- 1 | .layui-table-tool-panel { 2 | margin-top: 10px !important; 3 | } 4 | 5 | .layui-table-tool { 6 | background-color: white !important; 7 | border-bottom: none !important; 8 | padding-bottom: 10px !important; 9 | } 10 | 11 | .layui-table-header, 12 | .layui-table-header th { 13 | background-color: white !important; 14 | } 15 | 16 | .layui-table-view { 17 | border: none !important; 18 | } 19 | 20 | /** 兼容 layui 2.7.0 升级 table cell 单元格边距的调整 */ 21 | .layui-table-view .layui-table td, .layui-table-view .layui-table th { 22 | padding: 5px 0px; 23 | } 24 | 25 | .layui-table-cell { 26 | height: 34px; 27 | line-height: 34px; 28 | } 29 | 30 | .layui-table .layui-laypage .layui-laypage-curr .layui-laypage-em { 31 | border-radius: 50px !important; 32 | border-radius: 4px!important; 33 | background-color: #5FB878 !important; 34 | } 35 | 36 | .layui-table tr { 37 | height: 34px; 38 | line-height: 34px; 39 | } 40 | 41 | .layui-table-cell { 42 | padding-top: 1px !important; 43 | } 44 | 45 | .layui-table-box * { 46 | font-size: 13px !important; 47 | } 48 | 49 | .layui-table-page .layui-laypage input { 50 | width: 40px; 51 | height: 26.5px!important; 52 | } 53 | 54 | .layui-table-box button { 55 | font-size: 15px !important; 56 | } 57 | 58 | .layui-table-cell .pear-btn { 59 | margin-right: 5px; 60 | } 61 | 62 | .layui-table-cell .pear-btn:last-child { 63 | margin-right: 0px; 64 | } 65 | 66 | .layui-table-page { 67 | height: 45px !important; 68 | padding-top: 10px !important; 69 | } 70 | 71 | .layui-table-tool .layui-inline { 72 | border-radius: 3px !important; 73 | width: 30px !important; 74 | height: 30px !important; 75 | line-height: 20px !important; 76 | } 77 | 78 | .layui-table-view .layui-table[lay-skin=line] { 79 | border: none !important; 80 | } 81 | 82 | .layui-table-init .layui-icon{ 83 | font-size: 40px !important; 84 | margin: -15px 0 0 -15px; 85 | } 86 | 87 | .layui-table-body::-webkit-scrollbar { 88 | width: 0px; 89 | height: 0px; 90 | } 91 | 92 | .layui-table-body::-webkit-scrollbar { 93 | width: 6px; 94 | height: 6px; 95 | } 96 | .layui-table-body::-webkit-scrollbar-track { 97 | background: white; 98 | border-radius: 2px; 99 | } 100 | 101 | .layui-table-body::-webkit-scrollbar-thumb { 102 | background: #E6E6E6; 103 | border-radius: 2px; 104 | } 105 | 106 | .layui-table-body::-webkit-scrollbar-thumb:hover { 107 | background: #E6E6E6; 108 | } 109 | 110 | .layui-table-body::-webkit-scrollbar-corner { 111 | background: #f6f6f6; 112 | } 113 | -------------------------------------------------------------------------------- /conf/global_rules/blackUrl.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleName": "URL黑名单检测", 3 | "nextId": 11, 4 | "rules": [ 5 | { 6 | "attackType": "data_leak", 7 | "rule": "\\.(svn|htaccess|bash_history)", 8 | "state": "on", 9 | "severityLevel": "high", 10 | "action": "redirect", 11 | "id": 1 12 | }, 13 | { 14 | "attackType": "read_file", 15 | "rule": "\\.(bak|inc|old|mdb|sql|backup|java|class)$", 16 | "state": "on", 17 | "severityLevel": "high", 18 | "action": "redirect", 19 | "id": 2 20 | }, 21 | { 22 | "attackType": "data_leak", 23 | "rule": "(vhost|bbs|host|wwwroot|www|site|root|hytop|flashfxp).*\\.rar", 24 | "state": "on", 25 | "severityLevel": "high", 26 | "action": "redirect", 27 | "id": 3 28 | }, 29 | { 30 | "attackType": "backdoor", 31 | "rule": "(phpmyadmin|jmx-console|jmxinvokerservlet)", 32 | "state": "on", 33 | "severityLevel": "high", 34 | "action": "redirect", 35 | "id": 4 36 | }, 37 | { 38 | "attackType": "backdoor", 39 | "rule": "(?:phpMyAdmin2|phpMyAdmin|phpmyadmin|dbadmin|pma|myadmin|admin|mysql)\/scripts\/setup%.php", 40 | "state": "on", 41 | "severityLevel": "high", 42 | "action": "redirect", 43 | "id": 5 44 | }, 45 | { 46 | "attackType": "rce", 47 | "rule": "java\\.lang", 48 | "state": "on", 49 | "severityLevel": "high", 50 | "action": "redirect", 51 | "id": 6 52 | }, 53 | { 54 | "attackType": "rce", 55 | "rule": "\/(attachments|upimg|images|css|uploadfiles|html|uploads|templets|static|template|data|inc|forumdata|upload|includes|cache|avatar)\/(\\\\w+).(php|jsp)", 56 | "state": "on", 57 | "severityLevel": "high", 58 | "action": "redirect", 59 | "id": 7 60 | }, 61 | { 62 | "attackType": "data_leak", 63 | "rule": "wp-includes\/wlwmanifest.xml", 64 | "state": "on", 65 | "severityLevel": "high", 66 | "action": "redirect", 67 | "id": 8 68 | }, 69 | { 70 | "attackType": "rce", 71 | "rule": "die(@md5(HelloThinkCMF))<\/php>", 72 | "state": "on", 73 | "severityLevel": "high", 74 | "action": "redirect", 75 | "id": 9 76 | }, 77 | { 78 | "attackType": "backdoor", 79 | "rule": "\/boaform\/admin\/formLogin", 80 | "state": "on", 81 | "severityLevel": "high", 82 | "action": "redirect", 83 | "id": 10 84 | } 85 | ] 86 | } -------------------------------------------------------------------------------- /admin/lua/system.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local config = require "config" 6 | local file = require "file_utils" 7 | local user = require "user" 8 | local request = require "request" 9 | 10 | local get_post_args = request.get_post_args 11 | local cjson_encode = cjson.encode 12 | local cjson_decode = cjson.decode 13 | local read_file_to_string = file.read_file_to_string 14 | local write_string_to_file = file.write_string_to_file 15 | 16 | local type = type 17 | 18 | local _M = {} 19 | 20 | local SYSTEM_PATH = config.CONF_PATH .. '/system.json' 21 | 22 | function _M.do_request() 23 | local response = {code = 200, data = {}, msg = ""} 24 | local uri = ngx.var.uri 25 | local reload = false 26 | 27 | if user.check_auth_token() == false then 28 | response.code = 401 29 | response.msg = 'User not logged in' 30 | ngx.status = 401 31 | ngx.say(cjson_encode(response)) 32 | ngx.exit(401) 33 | return 34 | end 35 | 36 | if uri == "/system/get" then 37 | -- 查询配置信息 38 | local system = config.get_system_config() 39 | if system then 40 | -- 清空用户名和密码,避免返回给前端 41 | local redis = system.redis 42 | local mysql = system.mysql 43 | redis.user = nil 44 | redis.password = nil 45 | mysql.user = nil 46 | mysql.password = nil 47 | response.data = cjson_encode(system) 48 | end 49 | elseif uri == "/system/update" then 50 | local args, err = get_post_args() 51 | if args then 52 | local json = read_file_to_string(SYSTEM_PATH) 53 | local system = cjson_decode(json) 54 | 55 | for key, val in pairs(args) do 56 | local option = system[key] 57 | 58 | if key == 'secret' then 59 | option = val 60 | else 61 | local t = cjson_decode(val) 62 | if type(t) == 'table' and type(option) == 'table' then 63 | for k, v in pairs(t) do 64 | option[k] = v 65 | end 66 | else 67 | option = val 68 | end 69 | end 70 | 71 | system[key] = option 72 | end 73 | 74 | write_string_to_file(SYSTEM_PATH, cjson_encode(system)) 75 | reload = true 76 | else 77 | response.code = 500 78 | response.msg = err 79 | end 80 | end 81 | 82 | ngx.say(cjson_encode(response)) 83 | 84 | -- 如果没有错误且需要重载配置文件则重载配置文件 85 | if (response.code == 200 or response.code == 0) and reload == true then 86 | config.reload_config_file() 87 | end 88 | end 89 | 90 | _M.do_request() 91 | 92 | return _M 93 | -------------------------------------------------------------------------------- /admin/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ZhongKui WAF登录 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
ZhongKui WAF
17 |
18 | 妖 魔 鬼 怪 快 离 开 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 41 |
42 |
43 | 44 | 45 | 46 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /admin/component/pear/css/module/message.css: -------------------------------------------------------------------------------- 1 | .pear-notice .layui-this { 2 | color: #5FB878 !important; 3 | font-weight: 500; 4 | } 5 | 6 | .pear-notice { 7 | box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%), 0 12px 48px 16px rgb(0 0 0 / 3%)!important; 8 | } 9 | 10 | .pear-notice .layui-tab-title { 11 | display: flex; 12 | text-align: center; 13 | border-right: 1px solid whitesmoke; 14 | } 15 | 16 | .pear-notice .layui-tab-title li { 17 | flex: 1; 18 | text-align: center; 19 | border-right: 1px solid whitesmoke; 20 | } 21 | /*排除最后一个 li 右边框*/ 22 | .pear-notice .layui-tab-title li:last-child { 23 | border-right: none; 24 | } 25 | 26 | .pear-notice * { 27 | color: dimgray !important; 28 | } 29 | 30 | .pear-notice { 31 | width: 360px !important; 32 | } 33 | 34 | .pear-notice img { 35 | margin-left: 8px; 36 | width: 33px !important; 37 | height: 33px !important; 38 | border-radius: 50px; 39 | margin-right: 15px; 40 | } 41 | 42 | .pear-notice-item { 43 | height: 45px !important; 44 | line-height: 45px !important; 45 | padding-right: 20px; 46 | padding-left: 20px; 47 | border-bottom: 1px solid whitesmoke; 48 | padding-top: 10px; 49 | padding-bottom: 15px; 50 | } 51 | .pear-notice-end { 52 | float: right; 53 | right: 10px; 54 | } 55 | 56 | .pear-notice-item span{ 57 | height: 40px; 58 | line-height: 40px; 59 | } 60 | 61 | /** 滚动条样式 */ 62 | .pear-notice *::-webkit-scrollbar { 63 | width: 0px; 64 | height: 0px; 65 | } 66 | 67 | .pear-notice *::-webkit-scrollbar-track { 68 | background: white; 69 | border-radius: 2px; 70 | } 71 | 72 | .pear-notice *::-webkit-scrollbar-thumb { 73 | background: #E6E6E6; 74 | border-radius: 2px; 75 | } 76 | 77 | .pear-notice *::-webkit-scrollbar-thumb:hover { 78 | background: #E6E6E6; 79 | } 80 | 81 | .pear-notice *::-webkit-scrollbar-corner { 82 | background: #f6f6f6; 83 | } 84 | /** 增加 empty 样式 */ 85 | .pear-empty { 86 | font-size: 14px; 87 | line-height: 1.5715; 88 | min-height: 200px; 89 | display: flex; 90 | flex-direction: column; 91 | justify-content: center; 92 | align-items: center; 93 | } 94 | .pear-empty-normal { 95 | margin: 32px 0; 96 | color: #00000040; 97 | } 98 | .pear-empty-normal .pear-empty-image { 99 | height: 40px; 100 | } 101 | 102 | .pear-empty-image { 103 | height: 100px; 104 | margin-bottom: 8px; 105 | } 106 | .pear-empty-image svg { 107 | height: 100%; 108 | margin: auto; 109 | } 110 | 111 | .pear-empty-img-simple-g { 112 | stroke: #d9d9d9; 113 | } 114 | .pear-empty-img-default-g { 115 | fill: #fff; 116 | } 117 | .pear-empty-img-simple-path { 118 | fill: #fafafa; 119 | } 120 | .pear-empty-img-default-path-1 { 121 | fill: #aeb8c2; 122 | } 123 | .pear-empty-img-default-path-2 { 124 | fill: url(#linearGradient-1); 125 | } 126 | .pear-empty-img-default-path-3 { 127 | fill: #f5f5f7; 128 | } 129 | .pear-empty-img-default-path-4, .pear-empty-img-default-path-5 { 130 | fill: #dce0e6; 131 | } 132 | 133 | 134 | -------------------------------------------------------------------------------- /admin/admin/data/menu.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "title": "数据统计", 5 | "icon": "layui-icon layui-icon-chart", 6 | "type": 1, 7 | "href": "view/dashboard.html" 8 | }, 9 | { 10 | "id": 2, 11 | "title": "攻击事件", 12 | "icon": "layui-icon layui-icon-list", 13 | "type": 1, 14 | "href": "view/events.html" 15 | }, 16 | { 17 | "id": 3, 18 | "title": "IP封禁日志", 19 | "icon": "layui-icon layui-icon-disabled", 20 | "type": 1, 21 | "href": "view/ip-blocking.html" 22 | }, 23 | { 24 | "id": 4, 25 | "title": "防护站点", 26 | "icon": "layui-icon layui-icon-website", 27 | "type": 1, 28 | "href": "view/website.html" 29 | }, 30 | { 31 | "id": 5, 32 | "title": "防御配置", 33 | "icon": "layui-icon layui-icon-auz", 34 | "type": 0, 35 | "href": "", 36 | "children": [ 37 | { 38 | "id": 51, 39 | "title": "网站防护", 40 | "icon": "layui-icon layui-icon-console", 41 | "type": 1, 42 | "openType": "_iframe", 43 | "href": "view/defense/web.html" 44 | }, 45 | { 46 | "id": 52, 47 | "title": "IP黑白名单", 48 | "icon": "layui-icon layui-icon-list", 49 | "type": 1, 50 | "openType": "_iframe", 51 | "href": "view/defense/ip-filter.html" 52 | }, 53 | { 54 | "id": 53, 55 | "title": "Bot管理", 56 | "icon": "layui-icon layui-icon-console", 57 | "type": 1, 58 | "openType": "_iframe", 59 | "href": "view/defense/bot.html" 60 | }, 61 | { 62 | "id": 54, 63 | "title": "CC防护", 64 | "icon": "layui-icon layui-icon-console", 65 | "type": 1, 66 | "openType": "_iframe", 67 | "href": "view/defense/cc.html" 68 | }, 69 | { 70 | "id": 55, 71 | "title": "访问控制(ACL)", 72 | "icon": "layui-icon layui-icon-console", 73 | "type": 1, 74 | "openType": "_iframe", 75 | "href": "view/defense/acl.html" 76 | }, 77 | { 78 | "id": 56, 79 | "title": "敏感数据过滤", 80 | "icon": "layui-icon layui-icon-console", 81 | "type": 1, 82 | "openType": "_iframe", 83 | "href": "view/defense/sensitive.html" 84 | } 85 | ] 86 | }, 87 | { 88 | "id": 6, 89 | "title": "通用配置", 90 | "icon": "layui-icon layui-icon-set-fill", 91 | "type": 0, 92 | "href": "", 93 | "children": [ 94 | { 95 | "id": 62, 96 | "title": "证书管理", 97 | "icon": "layui-icon layui-icon-file", 98 | "type": 1, 99 | "openType": "_iframe", 100 | "href": "view/common/certificate.html" 101 | }, 102 | { 103 | "id": 63, 104 | "title": "IP组", 105 | "icon": "layui-icon layui-icon-list", 106 | "type": 1, 107 | "openType": "_iframe", 108 | "href": "view/common/ip-group.html" 109 | } 110 | ] 111 | }, 112 | { 113 | "id": 7, 114 | "title": "系统设置", 115 | "icon": "layui-icon layui-icon-util", 116 | "type": 0, 117 | "href": "", 118 | "children": [ 119 | { 120 | "id": 71, 121 | "title": "系统设置", 122 | "icon": "layui-icon layui-icon-template-1", 123 | "type": 1, 124 | "openType": "_iframe", 125 | "href": "view/system/system.html" 126 | }, 127 | { 128 | "id": 72, 129 | "title": "密码修改", 130 | "icon": "layui-icon layui-icon-console", 131 | "type": 1, 132 | "openType": "_iframe", 133 | "href": "view/system/password.html" 134 | } 135 | ] 136 | } 137 | ] -------------------------------------------------------------------------------- /admin/view/system/password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 用户密码修改 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
密码修改
15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /admin/component/pear/css/module/frame.css: -------------------------------------------------------------------------------- 1 | .pear-frame { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; 5 | overflow: hidden; 6 | } 7 | 8 | .pear-frame .dot { 9 | width: 5px; 10 | height: 24px; 11 | background-color: #5FB878; 12 | margin-top: 8px; 13 | margin-left: 15px; 14 | border-radius: 2px; 15 | display: inline-block; 16 | } 17 | 18 | .pear-frame .title { 19 | position: absolute; 20 | margin-top: 0px; 21 | margin-left: 12px; 22 | color: dimgray; 23 | display: inline-block; 24 | letter-spacing: 2px; 25 | } 26 | 27 | .pear-frame .pear-frame-title { 28 | height: 40px; 29 | line-height: 40px; 30 | background-color: white; 31 | border: whitesmoke 1px solid; 32 | } 33 | 34 | .pear-frame .pear-frame-content { 35 | width: 100%; 36 | height: calc(100% - 0px) !important; 37 | } 38 | 39 | .pear-frame-loading { 40 | position: absolute; 41 | display: none; 42 | width: 100%; 43 | height: calc(100% - 0px) !important; 44 | top: 0px; 45 | z-index: 19; 46 | background-color: #fff 47 | } 48 | 49 | .pear-frame-loading.close { 50 | animation: close 1s; 51 | -webkit-animation: close 1s; 52 | animation-fill-mode: forwards; 53 | } 54 | 55 | .ball-loader { 56 | position: absolute; 57 | left: 50%; 58 | top: 50%; 59 | transform: translate(-50%, -50%); 60 | -ms-transform: translate(-50%, -50%); 61 | -webkit-transform: translate(-50%, -50%) 62 | } 63 | 64 | .ball-loader>span, 65 | .signal-loader>span { 66 | background-color: #4aca85; 67 | display: inline-block 68 | } 69 | 70 | .ball-loader>span:nth-child(1), 71 | .ball-loader.sm>span:nth-child(1), 72 | .signal-loader>span:nth-child(1), 73 | .signal-loader.sm>span:nth-child(1) { 74 | -webkit-animation-delay: 0s; 75 | animation-delay: 0s 76 | } 77 | 78 | .ball-loader>span:nth-child(2), 79 | .ball-loader.sm>span:nth-child(2), 80 | .signal-loader>span:nth-child(2), 81 | .signal-loader.sm>span:nth-child(2) { 82 | -webkit-animation-delay: .1s; 83 | animation-delay: .1s 84 | } 85 | 86 | .ball-loader>span:nth-child(3), 87 | .ball-loader.sm>span:nth-child(3), 88 | .signal-loader>span:nth-child(3), 89 | .signal-loader.sm>span:nth-child(3) { 90 | -webkit-animation-delay: .15s; 91 | animation-delay: .15s 92 | } 93 | 94 | .ball-loader>span:nth-child(4), 95 | .ball-loader.sm>span:nth-child(4), 96 | .signal-loader>span:nth-child(4), 97 | .signal-loader.sm>span:nth-child(4) { 98 | -webkit-animation-delay: .2s; 99 | animation-delay: .2s 100 | } 101 | 102 | .ball-loader>span { 103 | width: 20px; 104 | height: 20px; 105 | margin: 0 3px; 106 | border-radius: 50%; 107 | transform: scale(0); 108 | -ms-transform: scale(0); 109 | -webkit-transform: scale(0); 110 | animation: ball-load 1s ease-in-out infinite; 111 | -webkit-animation: 1s ball-load ease-in-out infinite 112 | } 113 | 114 | @-webkit-keyframes ball-load { 115 | 0% { 116 | transform: scale(0); 117 | -webkit-transform: scale(0) 118 | } 119 | 120 | 50% { 121 | transform: scale(1); 122 | -webkit-transform: scale(1) 123 | } 124 | 125 | 100% { 126 | transform: scale(0); 127 | -webkit-transform: scale(0) 128 | } 129 | } 130 | 131 | @keyframes ball-load { 132 | 0% { 133 | transform: scale(0); 134 | -webkit-transform: scale(0) 135 | } 136 | 137 | 50% { 138 | transform: scale(1); 139 | -webkit-transform: scale(1) 140 | } 141 | 142 | 100% { 143 | transform: scale(0); 144 | -webkit-transform: scale(0) 145 | } 146 | } 147 | 148 | @-webkit-keyframes close { 149 | 0% { 150 | opacity: 1; 151 | /*display: block;*/ 152 | } 153 | 154 | 100% { 155 | opacity: 0; 156 | /*display: none;*/ 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /admin/js/validator.js: -------------------------------------------------------------------------------- 1 | const REG_IPV4 = /^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/ 2 | const REG_IPV6 = /(^(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$)|(^\[(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))\]$)/i 3 | const REG_IP_CIDR = /^(?:(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,3})?|(?:(?:(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4})|(?:::)(?::|(?:[A-Fa-f0-9]{1,4}:){1,7})?|([A-Fa-f0-9]{1,4}:){1,7}:|([A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}|([A-Fa-f0-9]{1,4}:){1,5}(:[A-Fa-f0-9]{1,4}){1,2}|([A-Fa-f0-9]{1,4}:){1,4}(:[A-Fa-f0-9]{1,4}){1,3}|([A-Fa-f0-9]{1,4}:){1,3}(:[A-Fa-f0-9]{1,4}){1,4}|([A-Fa-f0-9]{1,4}:){1,2}(:[A-Fa-f0-9]{1,4}){1,5}|[A-Fa-f0-9]{1,4}:((:[A-Fa-f0-9]{1,4}){1,6})|:((:[A-Fa-f0-9]{1,4}){1,7}|:))(?:\/\d{1,3})?)$/ 4 | 5 | var validator = {} 6 | 7 | validator.isIPv4 = function(value) { 8 | if (value.trim().length > 0) { 9 | return REG_IPV4.test(value); 10 | } else { 11 | return false; 12 | } 13 | } 14 | 15 | validator.isIPv6 = function(value) { 16 | if (value.trim().length > 0) { 17 | return REG_IPV6.test(value); 18 | } else { 19 | return false; 20 | } 21 | } 22 | 23 | validator.isIP = function(value) { 24 | if (value.trim().length > 0) { 25 | return REG_IPV4.test(value) || REG_IPV6.test(value); 26 | } else { 27 | return false; 28 | } 29 | } 30 | 31 | validator.isIPWithCIDR = function(value) { 32 | const parts = value.split('/'); 33 | const ipAddress = parts[0]; 34 | const cidr = parts[1] ? parseInt(parts[1], 10) : null; 35 | let version = 0; 36 | 37 | if (this.isIPv4(ipAddress)) { 38 | version = 4; 39 | } else if (this.isIPv6(ipAddress)) { 40 | version = 6; 41 | } 42 | 43 | if (version == 0) { 44 | return false; 45 | } 46 | 47 | // 如果提供了CIDR,验证其有效性(对于IPv4为0-32,IPv6为0-128) 48 | if (cidr !== null) { 49 | if (version == 4) { 50 | if (cidr < 0 || cidr > 32) { 51 | return false; 52 | } 53 | } else if (version == 6) { 54 | if (cidr < 0 || cidr > 128) { 55 | return false; 56 | } 57 | } else { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | -------------------------------------------------------------------------------- /admin/component/pear/css/module/button.css: -------------------------------------------------------------------------------- 1 | .pear-btn { 2 | display: inline-block; 3 | line-height: 38px; 4 | white-space: nowrap; 5 | cursor: pointer; 6 | text-align: center; 7 | box-sizing: border-box; 8 | outline: none; 9 | transition: 0.1s; 10 | font-weight: 500; 11 | padding: 0 18px; 12 | height: 38px; 13 | font-size: 14px; 14 | background-color: white; 15 | border: 1px solid #dcdfe6; 16 | border-radius: 2px; 17 | } 18 | 19 | .pear-btn i { 20 | font-size: 13px; 21 | } 22 | 23 | .pear-btn:hover { 24 | opacity: .8; 25 | filter: alpha(opacity=80); 26 | color: #409eff; 27 | background-color: #ECF5FF; 28 | } 29 | 30 | .pear-btn-danger, 31 | .pear-btn-warming, 32 | .pear-btn-success, 33 | .pear-btn-primary { 34 | height: 37px; 35 | line-height: 37px; 36 | color: #fff !important 37 | } 38 | 39 | /** Button 主题 */ 40 | .pear-btn-primary { 41 | border: 1px solid #2D8CF0; 42 | background-color: #2D8CF0 !important; 43 | } 44 | .pear-btn-danger { 45 | border: 1px solid #f56c6c; 46 | background-color: #f56c6c !important; 47 | } 48 | .pear-btn-warming { 49 | border: 1px solid #f6ad55; 50 | background-color: #f6ad55 !important; 51 | } 52 | .pear-btn-success { 53 | border: 1px solid #36b368; 54 | background-color: #36b368 !important; 55 | } 56 | 57 | .pear-btn[round] { 58 | border-radius: 50px; 59 | } 60 | 61 | .pear-btn-primary[plain] { 62 | color: #409eff !important; 63 | background: #ecf5ff 10% !important; 64 | } 65 | 66 | .pear-btn-primary[plain]:hover { 67 | color: #fff !important; 68 | background-color: #2d8cf0!important 69 | } 70 | 71 | .pear-btn-success[plain] { 72 | color: #36b368 !important; 73 | background: #f0f9eb !important; 74 | } 75 | 76 | .pear-btn-success[plain]:hover { 77 | color: white !important; 78 | background-color: #36b368 !important 79 | } 80 | 81 | .pear-btn-warming[plain] { 82 | color: #e6a23c !important; 83 | background: #fdf6ec !important; 84 | } 85 | 86 | .pear-btn-warming[plain]:hover { 87 | color: white !important; 88 | background-color: #e6a23c !important 89 | } 90 | 91 | .pear-btn-danger[plain] { 92 | color: #f56c6c !important; 93 | background: #fef0f0 !important; 94 | } 95 | 96 | .pear-btn-danger[plain]:hover { 97 | color: white !important; 98 | background-color: #f56c6c !important 99 | } 100 | 101 | /** Button Group */ 102 | .pear-btn-group { 103 | display: inline-block; 104 | vertical-align: middle; 105 | } 106 | 107 | .pear-btn-group .pear-btn { 108 | float: left; 109 | position: relative; 110 | border-radius: 0px; 111 | margin-left: 1px; 112 | margin-right: 1px; 113 | } 114 | 115 | .pear-btn-md { 116 | height: 34px; 117 | line-height: 34px; 118 | padding: 0 10px; 119 | font-size: 12.5px; 120 | } 121 | 122 | .pear-btn-group .pear-btn:first-child { 123 | border-top-left-radius: 4px !important; 124 | border-bottom-left-radius: 4px !important; 125 | } 126 | 127 | .pear-btn-group .pear-btn:last-child { 128 | border-top-right-radius: 4px !important; 129 | border-bottom-right-radius: 4px !important; 130 | } 131 | 132 | .pear-btn-group .pear-btn[round]:first-child { 133 | border-top-left-radius: 50px !important; 134 | border-bottom-left-radius: 50px !important; 135 | } 136 | 137 | .pear-btn-group .pear-btn[round]:last-child { 138 | border-top-right-radius: 50px !important; 139 | border-bottom-right-radius: 50px !important; 140 | } 141 | 142 | /** Button Size*/ 143 | .pear-btn-sm { 144 | height: 32px; 145 | line-height: 32px; 146 | padding: 0 10px; 147 | font-size: 12px; 148 | } 149 | 150 | .pear-btn-xs { 151 | height: 28px; 152 | line-height: 28px; 153 | padding: 0 8px; 154 | font-size: 12px; 155 | } 156 | 157 | .pear-btn-md { 158 | height: 34px; 159 | line-height: 34px; 160 | padding: 0 10px; 161 | font-size: 12.5px; 162 | } 163 | 164 | .pear-btn-lg { 165 | height: 44px; 166 | line-height: 44px; 167 | padding: 0 25px; 168 | font-size: 16px; 169 | } 170 | -------------------------------------------------------------------------------- /lib/logger.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local concat = table.concat 5 | local newtab = table.new 6 | local timerat = ngx.timer.at 7 | local setmetatable = setmetatable 8 | local io_open = io.open 9 | 10 | local _M = {} 11 | 12 | local mt = {__index = _M} 13 | 14 | function _M:new(logPath, host, rolling) 15 | local t = { 16 | flush_limit = 4096,-- 4kb 17 | flush_timeout = 1, 18 | 19 | buffered_size = 0, 20 | buffer_index = 0, 21 | buffer_data = newtab(20000, 0), 22 | 23 | logPath = logPath, 24 | prefix = logPath .. host .. '_', 25 | rolling = rolling or false, 26 | host = host, 27 | timer = nil} 28 | 29 | setmetatable(t, mt) 30 | return t 31 | end 32 | 33 | local function need_flush(self) 34 | if self.buffered_size > 0 then 35 | return true 36 | end 37 | 38 | return false 39 | end 40 | 41 | local function flush_lock(self) 42 | local dic_lock = ngx.shared.dict_locks 43 | local locked = dic_lock:get(self.host) 44 | if not locked then 45 | local succ, err = dic_lock:set(self.host, true) 46 | if not succ then 47 | ngx.log(ngx.ERR, "failed to lock logfile " .. self.host .. ": ", err) 48 | end 49 | return succ 50 | end 51 | return false 52 | end 53 | 54 | local function flush_unlock(self) 55 | local dic_lock = ngx.shared.dict_locks 56 | local succ, err = dic_lock:set(self.host, false) 57 | if not succ then 58 | ngx.log(ngx.ERR, "failed to unlock logfile " .. self.host .. ": ", err) 59 | end 60 | return succ 61 | end 62 | 63 | local function write_file(self, value) 64 | local file_name = '' 65 | if self.rolling then 66 | file_name = self.prefix .. ngx.today() .. ".log" 67 | else 68 | file_name = self.logPath 69 | end 70 | 71 | local file = io_open(file_name, "a+") 72 | 73 | if file == nil or value == nil then 74 | return 75 | end 76 | 77 | file:write(value) 78 | file:flush() 79 | file:close() 80 | 81 | return 82 | end 83 | 84 | local function flush_buffer(self) 85 | if not need_flush(self) then 86 | return true 87 | end 88 | 89 | if not flush_lock(self) then 90 | return true 91 | end 92 | 93 | local buffer = concat(self.buffer_data, "", 1, self.buffer_index) 94 | write_file(self, buffer) 95 | 96 | self.buffered_size = 0 97 | self.buffer_index = 0 98 | self.buffer_data = newtab(20000, 0) 99 | 100 | flush_unlock(self) 101 | end 102 | 103 | local function flushPeriod(premature, self) 104 | flush_buffer(self) 105 | self.timer = false 106 | end 107 | 108 | local function write_buffer(self, msg, msg_len) 109 | self.buffer_index = self.buffer_index + 1 110 | 111 | self.buffer_data[self.buffer_index] = msg 112 | 113 | self.buffered_size = self.buffered_size + msg_len 114 | 115 | return self.buffered_size 116 | end 117 | 118 | local function start_timer(self) 119 | if not self.timer then 120 | local ok, err = timerat(self.flush_timeout, flushPeriod, self) 121 | if not ok then 122 | ngx.log(ngx.ERR, "failed to create the timer: ", err) 123 | return 124 | end 125 | if ok then 126 | self.timer = true 127 | end 128 | end 129 | return self.timer 130 | end 131 | 132 | function _M:log(msg) 133 | if type(msg) ~= "string" then 134 | msg = tostring(msg) 135 | end 136 | 137 | local msg_len = #msg 138 | local len = msg_len + self.buffered_size 139 | 140 | if len < self.flush_limit then 141 | write_buffer(self, msg, msg_len) 142 | start_timer(self) 143 | elseif len >= self.flush_limit then 144 | write_buffer(self, msg, msg_len) 145 | flush_buffer(self) 146 | end 147 | end 148 | 149 | 150 | return _M -------------------------------------------------------------------------------- /admin/component/pear/module/echartsTheme.js: -------------------------------------------------------------------------------- 1 | layui.define(function(e){e("echartsTheme",{color:["#3fb1e3","#6be6c1","#626c91","#a0a7e6","#c4ebad","#96dee8"],backgroundColor:"rgba(252,252,252,0)",textStyle:{},title:{textStyle:{color:"#666666"},subtextStyle:{color:"#999999"}},line:{itemStyle:{borderWidth:"3"},lineStyle:{width:"4"},symbolSize:"10",symbol:"emptyCircle",smooth:!0},radar:{itemStyle:{borderWidth:"3"},lineStyle:{width:"4"},symbolSize:"10",symbol:"emptyCircle",smooth:!0},bar:{itemStyle:{barBorderWidth:0,barBorderColor:"#ccc"},emphasis:{itemStyle:{barBorderWidth:0,barBorderColor:"#ccc"}}},pie:{itemStyle:{borderWidth:0,borderColor:"#ccc"},emphasis:{itemStyle:{borderWidth:0,borderColor:"#ccc"}}},scatter:{itemStyle:{borderWidth:0,borderColor:"#ccc"},emphasis:{itemStyle:{borderWidth:0,borderColor:"#ccc"}}},boxplot:{itemStyle:{borderWidth:0,borderColor:"#ccc"},emphasis:{itemStyle:{borderWidth:0,borderColor:"#ccc"}}},parallel:{itemStyle:{borderWidth:0,borderColor:"#ccc"},emphasis:{itemStyle:{borderWidth:0,borderColor:"#ccc"}}},sankey:{itemStyle:{borderWidth:0,borderColor:"#ccc"},emphasis:{itemStyle:{borderWidth:0,borderColor:"#ccc"}}},funnel:{itemStyle:{borderWidth:0,borderColor:"#ccc"},emphasis:{itemStyle:{borderWidth:0,borderColor:"#ccc"}}},gauge:{itemStyle:{borderWidth:0,borderColor:"#ccc"},emphasis:{itemStyle:{borderWidth:0,borderColor:"#ccc"}}},candlestick:{itemStyle:{color:"#e6a0d2",color0:"transparent",borderColor:"#e6a0d2",borderColor0:"#3fb1e3",borderWidth:"2"}},graph:{itemStyle:{borderWidth:0,borderColor:"#ccc"},lineStyle:{width:"1",color:"#cccccc"},symbolSize:"10",symbol:"emptyCircle",smooth:!0,color:["#3fb1e3","#6be6c1","#626c91","#a0a7e6","#c4ebad","#96dee8"],label:{color:"#ffffff"}},map:{itemStyle:{areaColor:"#eeeeee",borderColor:"#aaaaaa",borderWidth:.5},label:{color:"#ffffff"},emphasis:{itemStyle:{areaColor:"rgba(63,177,227,0.25)",borderColor:"#3fb1e3",borderWidth:1},label:{color:"rgb(63,177,227)"}}},geo:{itemStyle:{areaColor:"#eeeeee",borderColor:"#aaaaaa",borderWidth:.5},label:{color:"#ffffff"},emphasis:{itemStyle:{areaColor:"rgba(63,177,227,0.25)",borderColor:"#3fb1e3",borderWidth:1},label:{color:"rgb(63,177,227)"}}},categoryAxis:{axisLine:{show:!0,lineStyle:{color:"#cccccc"}},axisTick:{show:!1,lineStyle:{color:"#333"}},axisLabel:{show:!0,color:"#999999"},splitLine:{show:!0,lineStyle:{color:["#eeeeee"]}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.05)","rgba(200,200,200,0.02)"]}}},valueAxis:{axisLine:{show:!0,lineStyle:{color:"#cccccc"}},axisTick:{show:!1,lineStyle:{color:"#333"}},axisLabel:{show:!0,color:"#999999"},splitLine:{show:!0,lineStyle:{color:["#eeeeee"]}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.05)","rgba(200,200,200,0.02)"]}}},logAxis:{axisLine:{show:!0,lineStyle:{color:"#cccccc"}},axisTick:{show:!1,lineStyle:{color:"#333"}},axisLabel:{show:!0,color:"#999999"},splitLine:{show:!0,lineStyle:{color:["#eeeeee"]}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.05)","rgba(200,200,200,0.02)"]}}},timeAxis:{axisLine:{show:!0,lineStyle:{color:"#cccccc"}},axisTick:{show:!1,lineStyle:{color:"#333"}},axisLabel:{show:!0,color:"#999999"},splitLine:{show:!0,lineStyle:{color:["#eeeeee"]}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.05)","rgba(200,200,200,0.02)"]}}},toolbox:{iconStyle:{borderColor:"#999999"},emphasis:{iconStyle:{borderColor:"#666666"}}},legend:{textStyle:{color:"#999999"}},tooltip:{axisPointer:{lineStyle:{color:"#cccccc",width:1},crossStyle:{color:"#cccccc",width:1}}},timeline:{lineStyle:{color:"#626c91",width:1},itemStyle:{color:"#626c91",borderWidth:1},controlStyle:{color:"#626c91",borderColor:"#626c91",borderWidth:.5},checkpointStyle:{color:"#3fb1e3",borderColor:"rgba(63,177,227,0.15)"},label:{color:"#626c91"},emphasis:{itemStyle:{color:"#626c91"},controlStyle:{color:"#626c91",borderColor:"#626c91",borderWidth:.5},label:{color:"#626c91"}}},visualMap:{color:["#2a99c9","#afe8ff"]},dataZoom:{backgroundColor:"rgba(255,255,255,0)",dataBackgroundColor:"rgba(222,222,222,1)",fillerColor:"rgba(114,230,212,0.25)",handleColor:"#cccccc",handleSize:"100%",textStyle:{color:"#999999"}},markPoint:{label:{color:"#ffffff"},emphasis:{label:{color:"#ffffff"}}}})}); -------------------------------------------------------------------------------- /init_worker.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local config = require "config" 6 | local redis_cli = require "redis_cli" 7 | local isarray = require "table.isarray" 8 | local sql = require "sql" 9 | local utils = require "utils" 10 | local constants = require "constants" 11 | 12 | local md5 = ngx.md5 13 | local pairs = pairs 14 | local tonumber = tonumber 15 | 16 | local cjson_decode = cjson.decode 17 | local cjson_encode = cjson.encode 18 | 19 | local dict_config = ngx.shared.dict_config 20 | local dict_hits = ngx.shared.dict_config_rules_hits 21 | 22 | local is_global_option_on = config.is_global_option_on 23 | local is_system_option_on = config.is_system_option_on 24 | local get_system_config = config.get_system_config 25 | 26 | local prefix = "waf_rules_hits:" 27 | 28 | local function sort(key_str, t) 29 | for _, rt in pairs(t) do 30 | local rule_md5 = md5(rt.rule) 31 | local key = key_str .. '_' .. rule_md5 32 | local key_total = key_str .. '_total_' .. rule_md5 33 | 34 | local hits = nil 35 | local totalHits = nil 36 | 37 | if is_system_option_on("redis") then 38 | hits = redis_cli.get(prefix .. key) 39 | totalHits = redis_cli.get(prefix .. key_total) 40 | else 41 | hits = dict_hits:get(key) 42 | totalHits = dict_hits:get(key_total) 43 | end 44 | 45 | rt.hits = tonumber(hits) or 0 46 | rt.totalHits = tonumber(totalHits) or 0 47 | end 48 | 49 | table.sort(t, function(a, b) 50 | if a.hits > b.hits then 51 | return true 52 | elseif a.hits == b.hits then 53 | if a.totalHits > b.totalHits then 54 | return true 55 | end 56 | end 57 | return false 58 | end) 59 | return t 60 | end 61 | 62 | local sort_timer_handler = function(premature) 63 | if premature then 64 | return 65 | end 66 | 67 | local config_table = config.get_config_table() 68 | if config_table then 69 | for server_name, _ in pairs(config_table) do 70 | local json = dict_config:get(server_name) 71 | if json then 72 | local security_modules = cjson_decode(json) 73 | for _, module in pairs(security_modules) do 74 | local rules = module.rules 75 | if isarray(rules) then 76 | rules = sort(server_name .. module['moduleName'], rules) 77 | end 78 | end 79 | 80 | local json_new = cjson_encode(security_modules) 81 | dict_config:set(server_name, json_new) 82 | end 83 | end 84 | end 85 | end 86 | 87 | local get_rules_timer_handler = function(premature) 88 | if premature then 89 | return 90 | end 91 | 92 | local config_table = config.get_config_table() 93 | if config_table then 94 | for key, conf in pairs(config_table) do 95 | local json = dict_config:get(key) 96 | if json then 97 | local security_modules = cjson_decode(json) 98 | conf.security_modules = security_modules 99 | end 100 | end 101 | end 102 | end 103 | 104 | if is_global_option_on("waf") then 105 | local worker_id = ngx.worker.id() 106 | 107 | if is_system_option_on('rulesSort') then 108 | local delay = get_system_config('rulesSort').period 109 | 110 | if worker_id == 0 then 111 | utils.start_timer_every(delay, sort_timer_handler) 112 | end 113 | 114 | utils.start_timer_every(delay, get_rules_timer_handler) 115 | end 116 | 117 | if is_system_option_on("mysql") then 118 | if worker_id == 0 then 119 | utils.start_timer(0, sql.check_table) 120 | utils.start_timer_every(2, sql.write_sql_queue_to_mysql, constants.KEY_ATTACK_LOG) 121 | utils.start_timer_every(2, sql.write_sql_queue_to_mysql, constants.KEY_IP_BLOCK_LOG) 122 | utils.start_timer_every(2, sql.update_waf_status) 123 | utils.start_timer_every(2, sql.update_traffic_stats) 124 | end 125 | end 126 | 127 | end 128 | -------------------------------------------------------------------------------- /admin/component/pear/module/message.js: -------------------------------------------------------------------------------- 1 | layui.define(["table","jquery","element"],function(t){"use strict";var e=layui.jquery,a=layui.element,i=function(t){this.option=t};i.prototype.render=function(t){var l={elem:t.elem,url:!!t.url&&t.url,height:t.height,data:t.data};if(0!=l.url){l.data=function(t){e.ajaxSettings.async=!1;var a=null;return e.get(t,function(t){a=t}),e.ajaxSettings.async=!0,a}(l.url);var n=function(t){var a,i=0,l='
    ',n='
    ';e.each(t.data,function(t,a){0===t?(l+='
  • '+a.title+"
  • ",n+='
    '):(l+="
  • "+a.title+"
  • ",n+='
    '),e.each(a.children,function(t,e){i++,n+='
    ',e.avatar&&(n+=''),n+='
    '+e.title+'
    '+e.time+"
    "}),0==a.children.length&&(n+='

    暂无数据

    '),n+="
    "}),a=i>0?'
  • ':'
  • ';return a+=l+="
",a+=n+="",a+=""}(l);e(l.elem).html(n);var s=document.querySelector(l.elem+" .pear-notice");new MutationObserver(function(t,a){if("none"!==getComputedStyle(s).display&&s.getBoundingClientRect().right>e(window).width()){var i=document.querySelector(l.elem).getBoundingClientRect().right;s.style.right=i-e(window).width()+20+"px",s.style.left="unset"}}).observe(s,{attributes:!0,childList:!1,subtree:!1,attributeOldValue:!1,attributeFilter:["class"]})}return setTimeout(function(){a.init(),e(t.elem+" li").click(function(t){e(this).siblings().removeClass("pear-this"),e(this).addClass("pear-this")})},300),new i(l)},i.prototype.click=function(t){e("*[notice-id]").click(function(a){a.preventDefault();var i=e(this).attr("notice-id"),l=e(this).attr("notice-title"),n=e(this).attr("notice-context"),s=e(this).attr("notice-form");t(i,l,n,s)})},i.prototype.reload=function(){},t("message",new i)}); -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo " 4 | ______ _ __ _ 5 | |__ / |__ ___ _ __ __ _| |/ / _(_) 6 | / /| '_ \ / _ \| '_ \ / _\` | ' / | | | | 7 | / /_| | | | (_) | | | | (_| | . \ |_| | | 8 | /____|_| |_|\___/|_| |_|\__, |_|\_\__,_|_| 9 | |___/ 10 | " 11 | 12 | OPENRESTY_PATH=/usr/local/openresty 13 | ZHONGKUI_PATH=$OPENRESTY_PATH/zhongkui-waf 14 | GEOIP_DATABASE_PATH=/usr/local/share/GeoIP 15 | 16 | cd /usr/local/src 17 | if [ ! -x "openresty-1.25.3.2.tar.gz" ]; then 18 | wget https://openresty.org/download/openresty-1.25.3.2.tar.gz 19 | fi 20 | tar zxf openresty-1.25.3.2.tar.gz 21 | cd openresty-1.25.3.2 22 | 23 | ./configure --prefix=$OPENRESTY_PATH \ 24 | --with-http_ssl_module \ 25 | --with-http_v2_module \ 26 | --with-http_realip_module \ 27 | --with-http_sub_module \ 28 | --with-http_stub_status_module \ 29 | --with-http_auth_request_module \ 30 | --with-http_secure_link_module \ 31 | --with-stream \ 32 | --with-stream_ssl_module \ 33 | --with-stream_realip_module \ 34 | --without-http_fastcgi_module \ 35 | --without-mail_pop3_module \ 36 | --without-mail_imap_module \ 37 | --without-mail_smtp_module 38 | 39 | make && make install 40 | echo -e "\033[34m[openresty安装成功]\033[0m" 41 | 42 | 43 | cd /usr/local/src 44 | if [ ! -x "zhongkui-waf-master.zip" ]; then 45 | wget -O /usr/local/src/zhongkui-waf-master.zip https://github.com/bukaleyang/zhongkui-waf/archive/refs/heads/master.zip --no-check-certificate 46 | fi 47 | unzip zhongkui-waf-master.zip 48 | mv ./zhongkui-waf-master $OPENRESTY_PATH/zhongkui-waf 49 | 50 | mkdir -p $OPENRESTY_PATH/nginx/logs/hack 51 | echo -e "\033[34m[hack目录已创建]\033[0m" 52 | echo -e "\033[34m[zhongkui-waf安装成功]\033[0m" 53 | 54 | 55 | cd /usr/local/src 56 | if [ ! -x "libmaxminddb-1.7.1.tar.gz" ]; then 57 | wget https://github.com/maxmind/libmaxminddb/releases/download/1.7.1/libmaxminddb-1.7.1.tar.gz 58 | fi 59 | tar -zxf libmaxminddb-1.7.1.tar.gz 60 | cd ./libmaxminddb-1.7.1 61 | ./configure 62 | make && make install 63 | echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf 64 | ldconfig 65 | echo -e "\033[34m[libmaxminddb安装成功]\033[0m" 66 | 67 | 68 | cd /usr/local/src 69 | if [ ! -x "libinjection-master.zip" ]; then 70 | wget -O /usr/local/src/libinjection-master.zip https://github.com/client9/libinjection/archive/refs/heads/master.zip 71 | fi 72 | unzip libinjection-master.zip 73 | cd ./libinjection-master 74 | make all 75 | mv ./src/libinjection.so $OPENRESTY_PATH/lualib/libinjection.so 76 | echo -e "\033[34m[libinjection安装成功]\033[0m" 77 | 78 | 79 | cd /usr/local/src 80 | if [ ! -x "luaossl-rel-20220711.tar.gz" ]; then 81 | wget -O /usr/local/src/luaossl-rel-20220711.tar.gz https://github.com/wahern/luaossl/archive/refs/tags/rel-20220711.tar.gz 82 | fi 83 | tar -zxf luaossl-rel-20220711.tar.gz 84 | cd ./luaossl-rel-20220711 85 | make all5.1 includedir=$OPENRESTY_PATH/luajit/include/luajit-2.1 && make install5.1 86 | echo -e "\033[34m[luaossl安装成功]\033[0m" 87 | 88 | 89 | cd /usr/local/src 90 | if [ ! -x "luafilesystem-master.zip" ]; then 91 | wget -O /usr/local/src/luafilesystem-master.zip https://github.com/lunarmodules/luafilesystem/archive/refs/heads/master.zip 92 | fi 93 | unzip luafilesystem-master.zip 94 | cd ./luafilesystem-master 95 | make INCS=-I$OPENRESTY_PATH/luajit/include/luajit-2.1 96 | mv ./src/lfs.so $OPENRESTY_PATH/lualib/lfs.so 97 | echo -e "\033[34m[luafilesystem安装成功]\033[0m" 98 | 99 | 100 | # =================maxminddb数据库文件自动更新start================= 101 | cd /usr/local/src 102 | if [ ! -x "geoipupdate_6.0.0_linux_386.tar.gz" ]; then 103 | wget https://github.com/maxmind/geoipupdate/releases/download/v6.0.0/geoipupdate_6.0.0_linux_386.tar.gz 104 | fi 105 | tar -zxf geoipupdate_6.0.0_linux_386.tar.gz 106 | mv ./geoipupdate_6.0.0_linux_386/geoipupdate /usr/local/bin/geoipupdate 107 | 108 | 109 | if [ -x "/usr/local/bin/geoipupdate" ]; then 110 | # 将配置文件GeoIP.conf写入到/usr/local/etc/目录 111 | echo " 112 | AccountID your AccountID 113 | LicenseKey your LicenseKey 114 | #EditionIDs GeoLite2-ASN GeoLite2-City GeoLite2-Country 115 | EditionIDs GeoLite2-City 116 | DatabaseDirectory $GEOIP_DATABASE_PATH 117 | " >> /usr/local/etc/GeoIP.conf 118 | 119 | echo -e "\033[34m[GeoIP.conf安装成功]\033[0m" 120 | 121 | echo "32 8 * * 1,3 /usr/local/bin/geoipupdate" | crontab - 122 | echo -e "\033[34m[geoipupdate安装成功]\033[0m" 123 | 124 | mkdir -p $GEOIP_DATABASE_PATH 125 | /usr/local/bin/geoipupdate 126 | fi 127 | # =================maxminddb数据库文件自动更新end================= 128 | -------------------------------------------------------------------------------- /lib/geoip.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local geo = require "resty.maxminddb" 5 | local config = require "config" 6 | local ip_utils = require "ip_utils" 7 | 8 | local _M = {} 9 | 10 | local pcall = pcall 11 | local next = next 12 | local ipairs = ipairs 13 | 14 | local get_site_config = config.get_site_config 15 | local get_system_config = config.get_system_config 16 | local is_private_ip = ip_utils.is_private_ip 17 | 18 | local db_file = get_system_config().geoip.file 19 | 20 | local unknown = {['iso_code'] = ''} 21 | local unknownNames = {en = 'unknown', ['zh-CN'] = '未知'} 22 | unknown.names = unknownNames 23 | 24 | local default = {is_allowed = true, country = unknown, province = unknown, city = unknown, longitude = 0, latitude = 0} 25 | 26 | local intranet = {is_allowed = true, longitude = 0, latitude = 0, 27 | country = {names = {['iso_code'] = '', en = 'intranet', ['zh-CN'] = '内网'}}, 28 | province = {names = {['iso_code'] = '', en = 'intranet', ['zh-CN'] = '内网'}}, 29 | city = {names = {['iso_code'] = '', en = 'intranet', ['zh-CN'] = '内网'}}} 30 | 31 | function _M.lookup(ip) 32 | if is_private_ip(ip) then 33 | return intranet 34 | end 35 | 36 | if not geo.initted() then 37 | geo.init(db_file) 38 | end 39 | 40 | local disallow_country_list = get_site_config("geoip")['disallowCountrys'] 41 | local disallow_country_table = nil 42 | 43 | if next(disallow_country_list) ~= nil then 44 | disallow_country_table = {} 45 | for _, code in ipairs(disallow_country_list) do 46 | disallow_country_table[code] = 1 47 | end 48 | end 49 | 50 | local is_allowed = true 51 | local country = nil 52 | local province = nil 53 | local city = nil 54 | local longitude = nil 55 | local latitude = nil 56 | 57 | --support ipv6 e.g. 2001:4860:0:1001::3004:ef68 58 | local pass, res, err = pcall(geo.lookup, ip) 59 | if not pass or not res then 60 | ngx.log(ngx.ERR, 'failed to lookup by ip,reason:', err) 61 | return default 62 | else 63 | country = res['country'] 64 | if country then 65 | local names = country['names'] 66 | if not names then 67 | country['names'] = unknownNames 68 | end 69 | else 70 | country = unknown 71 | country['iso_code'] = '' 72 | end 73 | 74 | local subdivisions = res['subdivisions'] 75 | if subdivisions then 76 | province = subdivisions[1] 77 | end 78 | 79 | if province then 80 | local names = province['names'] 81 | if not names then 82 | province['names'] = unknownNames 83 | end 84 | else 85 | province = unknown 86 | end 87 | 88 | city = res['city'] 89 | if city then 90 | local names = city['names'] 91 | if not names then 92 | city['names'] = unknownNames 93 | end 94 | else 95 | city = unknown 96 | end 97 | 98 | local location = res['location'] 99 | if location then 100 | longitude = location['longitude'] 101 | latitude = location['latitude'] 102 | end 103 | 104 | local iso_code = country.iso_code 105 | 106 | if disallow_country_table then 107 | if disallow_country_table[iso_code] then 108 | is_allowed = false 109 | end 110 | end 111 | 112 | if iso_code == 'TW' or iso_code == 'HK' or iso_code == 'MO' then 113 | local name_cn = country.names['zh-CN'] 114 | local name_en = country.names['en'] 115 | 116 | province.iso_code = iso_code 117 | province.names['zh-CN'] = name_cn 118 | province.names['en'] = name_en 119 | 120 | if iso_code ~= 'TW' then 121 | city.iso_code = '' 122 | city.names['zh-CN'] = name_cn 123 | city.names['en'] = name_en 124 | end 125 | country.iso_code = 'CN' 126 | country.names['zh-CN'] = '中国' 127 | country.names['en'] = 'China' 128 | end 129 | end 130 | 131 | return { is_allowed = is_allowed, country = country, province = province, city = city, longitude = longitude or 0, latitude = latitude or 0 } 132 | end 133 | 134 | return _M 135 | -------------------------------------------------------------------------------- /lib/request.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2024 bukale bukale2022@163.com 3 | 4 | local file_utils = require "file_utils" 5 | 6 | local ngxfind = ngx.re.find 7 | local ngxmatch = ngx.re.match 8 | local ngxgmatch = ngx.re.gmatch 9 | local sub = string.sub 10 | local randomseed = math.randomseed 11 | local random = math.random 12 | local ostime = os.time 13 | local osdate = os.date 14 | local osclock = os.clock 15 | 16 | local read_file_to_string = file_utils.read_file_to_string 17 | 18 | local _M = {} 19 | 20 | -- 生成一个随机的id 21 | function _M.generate_id() 22 | local now = ostime() 23 | randomseed(now + (ngx.worker.id() + 1) * osclock() + random()) 24 | local num = random(100000, 999999) 25 | 26 | return osdate("%Y%m%d%H%M%S", now) .. num 27 | end 28 | 29 | function _M.get_boundary() 30 | local content_type = ngx.var.http_content_type 31 | if not content_type then 32 | return nil, "no Content-Type" 33 | end 34 | local boundary = nil 35 | 36 | if content_type then 37 | local from, to = ngxfind(content_type, "\\s*boundary\\s*=\\s*(\\S+)", "isjo", nil, 1) 38 | if from then 39 | boundary = sub(content_type, from, to) 40 | end 41 | end 42 | 43 | return boundary 44 | end 45 | 46 | function _M.get_request_body() 47 | local body_data = ngx.ctx.request_body 48 | if not body_data then 49 | ngx.req.read_body() 50 | body_data = ngx.req.get_body_data() 51 | if not body_data then 52 | local body_file = ngx.req.get_body_file() 53 | if body_file then 54 | body_data = read_file_to_string(body_file, true) 55 | end 56 | end 57 | ngx.ctx.request_body = body_data 58 | end 59 | 60 | return body_data 61 | end 62 | 63 | function _M.get_post_args() 64 | ngx.req.read_body() 65 | return ngx.req.get_post_args() 66 | end 67 | 68 | function _M.get_upload_files() 69 | local boundary = _M.get_boundary() 70 | if not boundary then 71 | return nil, "no boundary" 72 | end 73 | 74 | local delimiter = '--' .. boundary .. '\r\n' 75 | local delimiter_end = '--' .. boundary .. '--' .. '\r\n' 76 | 77 | local content = '' 78 | local is_file = false 79 | 80 | local body_raw = _M.get_request_body() 81 | local it, err = ngxgmatch(body_raw, ".+?(?:\n|$)", "isjo") 82 | if not it then 83 | ngx.log(ngx.ERR, "error: ", err) 84 | return nil, err 85 | end 86 | 87 | local files = {} 88 | local name = nil 89 | local fileName = nil 90 | local ext = nil 91 | 92 | while true do 93 | local m, err = it() 94 | if err then 95 | ngx.log(ngx.ERR, "error: ", err) 96 | return nil, err 97 | end 98 | 99 | if not m then 100 | break 101 | end 102 | 103 | local line = m[0] 104 | if line == nil then 105 | break 106 | end 107 | 108 | if line == delimiter or line == delimiter_end then 109 | if content ~= '' then 110 | if is_file then 111 | is_file = false 112 | files[name] = {content = content, fileName = fileName, ext = ext} 113 | end 114 | content = '' 115 | end 116 | elseif line ~= '\r\n' then 117 | if is_file then 118 | if content == '' then 119 | local from = ngxfind(line, "Content-Type:\\s*\\S+/\\S+", "ijo") 120 | if not from then 121 | content = content .. line 122 | end 123 | else 124 | content = content .. line 125 | end 126 | else 127 | local from, to = ngxfind(line, [[Content-Disposition:\s*form-data;\s*name=["|']\w+["|'];\s*filename=["|'][\s\S]+\.\w+(?:"|')]], "ijo") 128 | if from then 129 | name = sub(line, from, to) 130 | 131 | local ma, _ = ngxmatch(line, [[Content-Disposition:\s*form-data;\s*name=["|'](\w+)["|'];\s*filename=["|']([\s\S]+)(\.\w+)(?:"|')]], "ijo") 132 | if ma then 133 | name = ma[1] 134 | ext = ma[3] 135 | fileName = ma[2] .. ext 136 | end 137 | is_file = true 138 | end 139 | end 140 | end 141 | end 142 | 143 | return files 144 | end 145 | 146 | return _M 147 | -------------------------------------------------------------------------------- /admin/lua/website.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2024 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local config = require "config" 6 | local file = require "file_utils" 7 | local user = require "user" 8 | local rule_utils = require "lib.rule_utils" 9 | local nkeys = require "table.nkeys" 10 | 11 | local pairs = pairs 12 | local concat = table.concat 13 | local format = string.format 14 | local write_string_to_file = file.write_string_to_file 15 | local cjson_encode = cjson.encode 16 | 17 | local _M = {} 18 | 19 | local WEBSITES_PATH = config.CONF_PATH .. '/website.json' 20 | local CERTIFICATE_PATH = config.CONF_PATH .. "/certificate.json" 21 | local SITES_CONF_PATH = config.ZHONGKUI_PATH .. '/admin/conf/sites.conf' 22 | 23 | -- nginx server 24 | local NGINX_SERVER_CONFIG = [[ 25 | server { 26 | %s 27 | server_name %s; 28 | 29 | charset utf-8; 30 | 31 | %s 32 | location / { 33 | proxy_pass %s; 34 | proxy_set_header Host $host; 35 | proxy_set_header X-Real-IP $remote_addr; 36 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 37 | } 38 | } 39 | ]] 40 | 41 | -- ssl certificate 42 | local SSL_CERT_CONFIG = [[ 43 | ssl_certificate %s; 44 | ssl_certificate_key %s; 45 | 46 | ssl_session_cache shared:SSL:1m; 47 | ssl_session_timeout 5m; 48 | 49 | ssl_ciphers HIGH:!aNULL:!MD5; 50 | ssl_prefer_server_ciphers on; 51 | ]] 52 | 53 | -- 生成站点配置文件 54 | local function generate_nginx_config_file() 55 | local response = rule_utils.list_rules(WEBSITES_PATH) 56 | local sites = response.data 57 | local ngxConfig = '' 58 | 59 | if sites and nkeys(sites) > 0 then 60 | for _, site in pairs(sites) do 61 | local serverNames = site.serverNames 62 | local serverNameStr = concat(serverNames, ' ') 63 | local upstream = site.upstream 64 | local listenPorts = site.listenPorts 65 | local listenPortsStr = '' 66 | local sslCertConfigStr = '' 67 | local isSSL = nil 68 | 69 | if listenPorts then 70 | for _, p in pairs(listenPorts) do 71 | local port = p.port 72 | local sslStr = '' 73 | if p.ssl == 'on' then 74 | sslStr = ' ssl' 75 | isSSL = true 76 | end 77 | listenPortsStr = listenPortsStr .. ' listen ' .. port .. sslStr .. ';\n' 78 | end 79 | end 80 | 81 | if isSSL then 82 | local certId = site.certId 83 | if certId then 84 | local resp = rule_utils.get_rule(CERTIFICATE_PATH, certId) 85 | if resp.code == 200 and resp.data then 86 | local cert = resp.data 87 | if cert then 88 | local certPath = cert.certPath 89 | local keyPath = cert.keyPath 90 | sslCertConfigStr = format(SSL_CERT_CONFIG, certPath, keyPath) 91 | end 92 | end 93 | end 94 | end 95 | 96 | ngxConfig = ngxConfig .. format(NGINX_SERVER_CONFIG, listenPortsStr, serverNameStr, sslCertConfigStr, upstream) 97 | end 98 | end 99 | 100 | write_string_to_file(SITES_CONF_PATH, ngxConfig) 101 | end 102 | 103 | function _M.do_request() 104 | local response = {code = 200, data = {}, msg = ""} 105 | local uri = ngx.var.uri 106 | local reload = false 107 | 108 | if user.check_auth_token() == false then 109 | response.code = 401 110 | response.msg = 'User not logged in' 111 | ngx.status = 401 112 | ngx.say(cjson_encode(response)) 113 | ngx.exit(401) 114 | return 115 | end 116 | 117 | if uri == "/sites/list" then 118 | -- 查询站点列表 119 | response = rule_utils.list_rules(WEBSITES_PATH) 120 | elseif uri == "/sites/save" then 121 | -- 修改或新增站点 122 | local newRule = rule_utils.get_rule_from_request() 123 | newRule.mode = 'protection' 124 | 125 | response = rule_utils.save_or_update_rule(WEBSITES_PATH, newRule) 126 | reload = true 127 | elseif uri == "/sites/remove" then 128 | -- 删除站点 129 | response = rule_utils.delete_rule(WEBSITES_PATH) 130 | reload = true 131 | end 132 | 133 | ngx.say(cjson_encode(response)) 134 | 135 | -- 如果没有错误且需要重载配置文件则重载配置文件 136 | if response.code == 200 and reload == true then 137 | generate_nginx_config_file() 138 | config.reload_config_file() 139 | end 140 | end 141 | 142 | _M.do_request() 143 | 144 | return _M 145 | -------------------------------------------------------------------------------- /admin/lua/ip_group.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local config = require "config" 6 | local file_utils = require "file_utils" 7 | local user = require "user" 8 | local rule_utils = require "lib.rule_utils" 9 | 10 | local tonumber = tonumber 11 | local insert = table.insert 12 | 13 | local cjson_decode = cjson.decode 14 | local cjson_encode = cjson.encode 15 | 16 | local _M = {} 17 | 18 | local IP_GROUP_PATH = config.CONF_PATH .. '/ipgroup.json' 19 | local ACL_PATH = config.CONF_PATH .. '/global_rules/acl.json' 20 | 21 | function _M.do_request() 22 | local response = {code = 200, data = {}, msg = ""} 23 | local uri = ngx.var.uri 24 | local reload = false 25 | 26 | if user.check_auth_token() == false then 27 | response.code = 401 28 | response.msg = 'User not logged in' 29 | ngx.status = 401 30 | ngx.say(cjson_encode(response)) 31 | return ngx.exit(401) 32 | end 33 | 34 | if uri == "/common/ipgroups/list" then 35 | -- ip组列表 36 | response = rule_utils.list_rules(IP_GROUP_PATH) 37 | elseif uri == "/common/ipgroups/listnames" then 38 | local json = file_utils.read_file_to_string(IP_GROUP_PATH) 39 | if json then 40 | local ruleTable = cjson_decode(json) 41 | local rules = ruleTable.rules 42 | 43 | local groups = {} 44 | 45 | if rules then 46 | for _, r in pairs(rules) do 47 | local group = {id = r.id, groupName = r.groupName } 48 | insert(groups, group) 49 | end 50 | end 51 | 52 | response.data = groups 53 | end 54 | elseif uri == "/common/ipgroups/get" then 55 | -- ip组内容 56 | local args, err = ngx.req.get_uri_args() 57 | if args then 58 | local id = tonumber(args['id']) 59 | if id then 60 | response = rule_utils.get_rule(IP_GROUP_PATH, id) 61 | end 62 | else 63 | response.code = 500 64 | response.msg = err 65 | ngx.log(ngx.ERR, err) 66 | end 67 | elseif uri == "/common/ipgroups/update" then 68 | -- 修改ip组内容 69 | local newRule = rule_utils.get_rule_from_request() 70 | if newRule then 71 | newRule.id = tonumber(newRule.id) 72 | local ips = newRule.ips 73 | 74 | if not ips or #ips == 0 then 75 | response.code = 500 76 | response.msg = 'param content is empty' 77 | end 78 | 79 | response = rule_utils.save_or_update_rule(IP_GROUP_PATH, newRule) 80 | reload = true 81 | else 82 | response.code = 500 83 | response.msg = 'param is empty' 84 | end 85 | elseif uri == "/common/ipgroups/remove" then 86 | -- 删除IP组 87 | ngx.req.read_body() 88 | local args, err = ngx.req.get_post_args() 89 | 90 | if args then 91 | local id = tonumber(args['id']) 92 | if id then 93 | local flag = false 94 | local json = file_utils.read_file_to_string(ACL_PATH) 95 | if json then 96 | local ruleTable = cjson_decode(json) 97 | local rules = ruleTable.rules 98 | 99 | if rules then 100 | for _, r in pairs(rules) do 101 | local conditions = r.conditions or {} 102 | for _, c in pairs(conditions) do 103 | if c.ipGroupId == id then 104 | flag = true 105 | break 106 | end 107 | end 108 | 109 | if flag then 110 | break 111 | end 112 | end 113 | end 114 | end 115 | 116 | if flag then 117 | response.code = 500 118 | response.msg = '该IP组被其他规则引用,不能删除' 119 | else 120 | response = rule_utils.delete_rule(IP_GROUP_PATH) 121 | reload = true 122 | end 123 | end 124 | else 125 | response.code = 500 126 | response.msg = err 127 | end 128 | end 129 | 130 | ngx.say(cjson_encode(response)) 131 | 132 | -- 如果没有错误且需要重载配置文件则重载配置文件 133 | if (response.code == 200 or response.code == 0) and reload == true then 134 | config.reload_config_file() 135 | end 136 | end 137 | 138 | _M.do_request() 139 | 140 | return _M 141 | -------------------------------------------------------------------------------- /admin/lua/ip_blocking.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2024 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local user = require "user" 6 | local pager = require "lib.pager" 7 | local mysql = require "mysql_cli" 8 | local action = require "action" 9 | 10 | local tonumber = tonumber 11 | local format = string.format 12 | local quote_sql_str = ngx.quote_sql_str 13 | local cjson_encode = cjson.encode 14 | 15 | local _M = {} 16 | 17 | local SQL_COUNT_IP_BLOCK_LOG = 'SELECT COUNT(*) AS total FROM ip_block_log ' 18 | 19 | local SQL_SELECT_IP_BLOCK_LOG = [[ 20 | SELECT id, request_id, ip, ip_country_code, ip_country_cn, ip_country_en, ip_province_code, ip_province_cn, ip_province_en, ip_city_code, ip_city_cn, ip_city_en, 21 | ip_longitude, ip_latitude, block_reason, start_time, block_duration, end_time, unblock_time, action, block_times FROM ip_block_log 22 | ]] 23 | 24 | local SQL_IP_BLOCK_LOG_UNBLOCK = [[ 25 | UPDATE ip_block_log SET unblock_time=NOW() WHERE id=%u; 26 | ]] 27 | 28 | -- 查询日志列表数据 29 | local function listLogs() 30 | local response = {code = 200, data = {}, msg = ""} 31 | 32 | local args, err = ngx.req.get_uri_args() 33 | if args then 34 | local page = tonumber(args['page']) 35 | local limit = tonumber(args['limit']) 36 | local offset = pager.get_begin(page, limit) 37 | 38 | local ip = args['ip'] 39 | local block_reason = args['block_reason'] 40 | 41 | local where = ' WHERE 1=1 ' 42 | 43 | if ip and #ip > 0 then 44 | where = where .. ' AND ip=' .. quote_sql_str(ip) .. ' ' 45 | end 46 | 47 | if block_reason and #block_reason > 0 then 48 | where = where .. ' AND block_reason=' .. quote_sql_str(block_reason) .. ' ' 49 | end 50 | 51 | local res, err = mysql.query(SQL_COUNT_IP_BLOCK_LOG .. where) 52 | 53 | if res and res[1] then 54 | local total = tonumber(res[1].total) 55 | if total > 0 then 56 | res, err = mysql.query(SQL_SELECT_IP_BLOCK_LOG .. where .. ' ORDER BY id DESC LIMIT ' .. offset .. ',' .. limit) 57 | if res then 58 | response.data = res 59 | else 60 | response.code = 500 61 | response.msg = 'query database error' 62 | ngx.log(ngx.ERR, err) 63 | end 64 | end 65 | 66 | response.code = 0 67 | response.count = total 68 | else 69 | response.code = 500 70 | response.msg = 'query database error' 71 | ngx.log(ngx.ERR, err) 72 | end 73 | else 74 | response.code = 500 75 | response.msg = err 76 | end 77 | 78 | if response.code ~= 0 then 79 | ngx.log(ngx.ERR, response.msg) 80 | end 81 | 82 | return response 83 | end 84 | 85 | -- 根据id解封ip 86 | local function unblock() 87 | local response = {code = 200, data = {}, msg = ""} 88 | 89 | local args, err = ngx.req.get_post_args() 90 | if args and args['id'] then 91 | local id = tonumber(args['id']) 92 | 93 | local res, err = mysql.query(format(SQL_SELECT_IP_BLOCK_LOG .. ' WHERE id=%u;', id)) 94 | if res then 95 | local data = res[1] 96 | local ip = data.ip 97 | 98 | local ok = action.unblock_ip(ip) 99 | if ok then 100 | res, err = mysql.query(format(SQL_IP_BLOCK_LOG_UNBLOCK, id)) 101 | if res then 102 | response.data = res[1] 103 | else 104 | response.code = 500 105 | response.msg = 'query database error' 106 | ngx.log(ngx.ERR, err) 107 | end 108 | end 109 | else 110 | response.code = 500 111 | response.msg = 'query database error' 112 | ngx.log(ngx.ERR, err) 113 | end 114 | else 115 | response.code = 500 116 | response.msg = err 117 | ngx.log(ngx.ERR, err) 118 | end 119 | 120 | return response 121 | end 122 | 123 | function _M.do_request() 124 | local response = {code = 200, data = {}, msg = ""} 125 | local uri = ngx.var.uri 126 | 127 | if user.check_auth_token() == false then 128 | response.code = 401 129 | response.msg = 'User not logged in' 130 | ngx.status = 401 131 | ngx.say(cjson_encode(response)) 132 | ngx.exit(401) 133 | return 134 | end 135 | 136 | if uri == "/ipblocking/list" then 137 | -- 查询事件数据列表 138 | response = listLogs() 139 | elseif uri == "/ipblocking/unblock" then 140 | -- ip解封 141 | response = unblock() 142 | end 143 | 144 | ngx.say(cjson_encode(response)) 145 | end 146 | 147 | _M.do_request() 148 | 149 | return _M 150 | -------------------------------------------------------------------------------- /lib/stringutf8.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local len = string.len 5 | local sub = string.sub 6 | local match = string.match 7 | local byte = string.byte 8 | 9 | local concat = table.concat 10 | local insert = table.insert 11 | local newtab = table.new 12 | local abs = math.abs 13 | 14 | local ngxfind = ngx.re.find 15 | 16 | local error = error 17 | 18 | local _M = {} 19 | 20 | local INDEX_OUT_OF_RANGE = "String index out of range: " 21 | local NOT_NUMBER = "number expected, got " 22 | local NOT_STRING = "string expected, got " 23 | local NOT_STRING_NIL = "string expected, got nil" 24 | 25 | function _M.to_char_array(str) 26 | local array 27 | if str then 28 | local length = len(str) 29 | array = newtab(length, 0) 30 | 31 | local byteLength = 1 32 | local i, j = 1, 1 33 | while i <= length do 34 | local first_byte = byte(str, i) 35 | if first_byte >= 0 and first_byte < 128 then 36 | byteLength = 1 37 | elseif first_byte > 191 and first_byte < 224 then 38 | byteLength = 2 39 | elseif first_byte > 223 and first_byte < 240 then 40 | byteLength = 3 41 | elseif first_byte > 239 and first_byte < 248 then 42 | byteLength = 4 43 | end 44 | 45 | j = i + byteLength 46 | local char = sub(str, i, j - 1) 47 | i = j 48 | insert(array, char) 49 | end 50 | end 51 | 52 | return array 53 | end 54 | 55 | function _M.sub(str, i, j) 56 | local sub_str 57 | if str then 58 | if i == nil then 59 | i = 1 60 | end 61 | 62 | if type(i) ~= "number" then 63 | error(NOT_NUMBER .. type(i)) 64 | end 65 | 66 | if i < 1 then 67 | error(INDEX_OUT_OF_RANGE .. i) 68 | end 69 | 70 | if j then 71 | if type(j) ~= "number" then 72 | error(NOT_NUMBER .. type(j)) 73 | end 74 | end 75 | 76 | local array = _M.to_char_array(str) 77 | if array then 78 | local length = #array 79 | local subLen = length - i 80 | if subLen < 0 then 81 | error(INDEX_OUT_OF_RANGE .. subLen) 82 | end 83 | 84 | if not j then 85 | sub_str = concat(array, "", i) 86 | else 87 | if abs(j) > length then 88 | error(INDEX_OUT_OF_RANGE .. j) 89 | end 90 | if j < 0 then 91 | j = length + j + 1 92 | end 93 | sub_str = concat(array, "", i, j) 94 | end 95 | end 96 | end 97 | 98 | return sub_str 99 | end 100 | 101 | function _M.trim(str) 102 | if str then 103 | str = ngx.re.gsub(str, "^\\s*|\\s*$", "", "jo") 104 | end 105 | 106 | return str 107 | end 108 | 109 | function _M.len(str) 110 | local str_len = 0 111 | if str then 112 | if type(str) ~= "string" then 113 | error(NOT_STRING .. type(str)) 114 | end 115 | 116 | local length = len(str) 117 | 118 | local i = 1 119 | while i <= length do 120 | local first_byte = byte(str, i) 121 | if first_byte >= 0 and first_byte < 128 then 122 | i = i + 1 123 | elseif first_byte > 191 and first_byte < 224 then 124 | i = i + 2 125 | elseif first_byte > 223 and first_byte < 240 then 126 | i = i + 3 127 | elseif first_byte > 239 and first_byte < 248 then 128 | i = i + 4 129 | end 130 | 131 | str_len = str_len + 1 132 | end 133 | else 134 | error(NOT_STRING_NIL) 135 | end 136 | 137 | return str_len 138 | end 139 | 140 | function _M.default_if_blank(str, default_str) 141 | if default_str == nil then 142 | default_str = "" 143 | end 144 | 145 | if str == nil or match(str, "^%s*$") then 146 | return default_str 147 | end 148 | 149 | return str 150 | end 151 | 152 | function _M.split(input_str, delimiter) 153 | if not input_str then 154 | return nil 155 | end 156 | 157 | local length = len(input_str) 158 | local result = {} 159 | 160 | if length == 0 then 161 | return result 162 | end 163 | 164 | local ctx = { pos = 1 } 165 | local start = 1 166 | 167 | while ctx.pos < length do 168 | local from = ngxfind(input_str, delimiter, "jo", ctx) 169 | 170 | if from then 171 | insert(result, sub(input_str, start, from - 1)) 172 | start = ctx.pos 173 | else 174 | insert(result, sub(input_str, start, length)) 175 | break 176 | end 177 | end 178 | 179 | return result 180 | end 181 | 182 | return _M 183 | -------------------------------------------------------------------------------- /admin/conf/admin.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 1226; 3 | server_name localhost; 4 | 5 | charset utf-8; 6 | 7 | #access_log logs/zhongkui.access.log main; 8 | 9 | # path-to-zhongkui-waf/admin 10 | set $root_path /usr/local/openresty/zhongkui-waf/admin; 11 | 12 | location = /admin/data/user.json { 13 | deny all; 14 | return 403; 15 | } 16 | 17 | location ^~ /admin/lua/ { 18 | deny all; 19 | return 403; 20 | } 21 | 22 | location ^~ /ssl-certs/ { 23 | deny all; 24 | return 403; 25 | } 26 | 27 | location / { 28 | root $root_path; 29 | proxy_set_header Host $host; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 32 | } 33 | 34 | location /user/ { 35 | content_by_lua_block { 36 | local user = require "user" 37 | user.do_request() 38 | } 39 | proxy_set_header X-Real-IP $remote_addr; 40 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 41 | } 42 | 43 | location ~* \.(html)$ { 44 | access_by_lua_block { 45 | local user = require "user" 46 | if user.check_auth_token() == false then 47 | ngx.header.content_type = 'text/html' 48 | ngx.say('') 49 | --ngx.redirect("/login") 50 | end 51 | } 52 | 53 | root $root_path; 54 | proxy_set_header Host $host; 55 | proxy_set_header X-Real-IP $remote_addr; 56 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 57 | } 58 | 59 | location /login { 60 | default_type 'text/html'; 61 | alias $root_path/login.html; 62 | proxy_set_header Host $host; 63 | proxy_set_header X-Real-IP $remote_addr; 64 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 65 | } 66 | 67 | location /sites/ { 68 | content_by_lua_file $root_path/lua/website.lua; 69 | proxy_set_header X-Real-IP $remote_addr; 70 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 | } 72 | 73 | location /defense/ { 74 | content_by_lua_file $root_path/lua/defense.lua; 75 | proxy_set_header X-Real-IP $remote_addr; 76 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 77 | } 78 | 79 | location /sensitive/ { 80 | content_by_lua_file $root_path/lua/sensitive.lua; 81 | proxy_set_header X-Real-IP $remote_addr; 82 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 83 | } 84 | 85 | location /dashboard { 86 | content_by_lua_file $root_path/lua/dashboard.lua; 87 | proxy_set_header X-Real-IP $remote_addr; 88 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 89 | } 90 | 91 | location /events/ { 92 | content_by_lua_file $root_path/lua/events.lua; 93 | proxy_set_header X-Real-IP $remote_addr; 94 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 95 | } 96 | 97 | location /ipblocking/ { 98 | content_by_lua_file $root_path/lua/ip_blocking.lua; 99 | proxy_set_header X-Real-IP $remote_addr; 100 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 101 | } 102 | 103 | location /ip/ { 104 | content_by_lua_file $root_path/lua/ip_filter.lua; 105 | proxy_set_header X-Real-IP $remote_addr; 106 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 107 | } 108 | 109 | location /bot/ { 110 | content_by_lua_file $root_path/lua/bot.lua; 111 | proxy_set_header X-Real-IP $remote_addr; 112 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 113 | } 114 | 115 | location /cc/ { 116 | content_by_lua_file $root_path/lua/cc_defense.lua; 117 | proxy_set_header X-Real-IP $remote_addr; 118 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 119 | } 120 | 121 | location /acl/ { 122 | content_by_lua_file $root_path/lua/acl.lua; 123 | proxy_set_header X-Real-IP $remote_addr; 124 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 125 | } 126 | 127 | location /common/certificate/ { 128 | content_by_lua_file $root_path/lua/certificate.lua; 129 | proxy_set_header X-Real-IP $remote_addr; 130 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 131 | } 132 | 133 | location /common/ipgroups/ { 134 | content_by_lua_file $root_path/lua/ip_group.lua; 135 | proxy_set_header X-Real-IP $remote_addr; 136 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 137 | } 138 | 139 | location /system/ { 140 | content_by_lua_file $root_path/lua/system.lua; 141 | proxy_set_header X-Real-IP $remote_addr; 142 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /admin/conf/admin.conf.default: -------------------------------------------------------------------------------- 1 | server { 2 | listen 1226; 3 | server_name localhost; 4 | 5 | charset utf-8; 6 | 7 | #access_log logs/zhongkui.access.log main; 8 | 9 | # path-to-zhongkui-waf/admin 10 | set $root_path /usr/local/openresty/zhongkui-waf/admin; 11 | 12 | location = /admin/data/user.json { 13 | deny all; 14 | return 403; 15 | } 16 | 17 | location ^~ /admin/lua/ { 18 | deny all; 19 | return 403; 20 | } 21 | 22 | location ^~ /ssl-certs/ { 23 | deny all; 24 | return 403; 25 | } 26 | 27 | location / { 28 | root $root_path; 29 | proxy_set_header Host $host; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 32 | } 33 | 34 | location /user/ { 35 | content_by_lua_block { 36 | local user = require "user" 37 | user.do_request() 38 | } 39 | proxy_set_header X-Real-IP $remote_addr; 40 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 41 | } 42 | 43 | location ~* \.(html)$ { 44 | access_by_lua_block { 45 | local user = require "user" 46 | if user.check_auth_token() == false then 47 | ngx.header.content_type = 'text/html' 48 | ngx.say('') 49 | --ngx.redirect("/login") 50 | end 51 | } 52 | 53 | root $root_path; 54 | proxy_set_header Host $host; 55 | proxy_set_header X-Real-IP $remote_addr; 56 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 57 | } 58 | 59 | location /login { 60 | default_type 'text/html'; 61 | alias $root_path/login.html; 62 | proxy_set_header Host $host; 63 | proxy_set_header X-Real-IP $remote_addr; 64 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 65 | } 66 | 67 | location /sites/ { 68 | content_by_lua_file $root_path/lua/website.lua; 69 | proxy_set_header X-Real-IP $remote_addr; 70 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 | } 72 | 73 | location /defense/ { 74 | content_by_lua_file $root_path/lua/defense.lua; 75 | proxy_set_header X-Real-IP $remote_addr; 76 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 77 | } 78 | 79 | location /sensitive/ { 80 | content_by_lua_file $root_path/lua/sensitive.lua; 81 | proxy_set_header X-Real-IP $remote_addr; 82 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 83 | } 84 | 85 | location /dashboard { 86 | content_by_lua_file $root_path/lua/dashboard.lua; 87 | proxy_set_header X-Real-IP $remote_addr; 88 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 89 | } 90 | 91 | location /events/ { 92 | content_by_lua_file $root_path/lua/events.lua; 93 | proxy_set_header X-Real-IP $remote_addr; 94 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 95 | } 96 | 97 | location /ipblocking/ { 98 | content_by_lua_file $root_path/lua/ip_blocking.lua; 99 | proxy_set_header X-Real-IP $remote_addr; 100 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 101 | } 102 | 103 | location /ip/ { 104 | content_by_lua_file $root_path/lua/ip_filter.lua; 105 | proxy_set_header X-Real-IP $remote_addr; 106 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 107 | } 108 | 109 | location /bot/ { 110 | content_by_lua_file $root_path/lua/bot.lua; 111 | proxy_set_header X-Real-IP $remote_addr; 112 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 113 | } 114 | 115 | location /cc/ { 116 | content_by_lua_file $root_path/lua/cc_defense.lua; 117 | proxy_set_header X-Real-IP $remote_addr; 118 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 119 | } 120 | 121 | location /acl/ { 122 | content_by_lua_file $root_path/lua/acl.lua; 123 | proxy_set_header X-Real-IP $remote_addr; 124 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 125 | } 126 | 127 | location /common/certificate/ { 128 | content_by_lua_file $root_path/lua/certificate.lua; 129 | proxy_set_header X-Real-IP $remote_addr; 130 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 131 | } 132 | 133 | location /common/ipgroups/ { 134 | content_by_lua_file $root_path/lua/ip_group.lua; 135 | proxy_set_header X-Real-IP $remote_addr; 136 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 137 | } 138 | 139 | location /system/ { 140 | content_by_lua_file $root_path/lua/system.lua; 141 | proxy_set_header X-Real-IP $remote_addr; 142 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /admin/lua/dashboard.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local config = require "config" 5 | local cjson = require "cjson" 6 | local user = require "user" 7 | local time = require "time" 8 | local sql = require "sql" 9 | local utils = require "utils" 10 | local constants = require "constants" 11 | 12 | local ipairs = ipairs 13 | local concat = table.concat 14 | local ngxfind = ngx.re.find 15 | 16 | local cjson_encode = cjson.encode 17 | local is_system_option_on = config.is_system_option_on 18 | 19 | local _M = {} 20 | 21 | local function getRequestTraffic() 22 | local hours = time.get_hours() 23 | local dict = ngx.shared.dict_req_count 24 | local dataStr = '[["hour", "traffic","attack_traffic","blocked_traffic"],' 25 | for _, hour in ipairs(hours) do 26 | local count = dict:get(hour) or 0 27 | local attack_count = dict:get(constants.KEY_ATTACK_PREFIX .. hour) or 0 28 | local blocked_count = dict:get(constants.KEY_BLOCKED_PREFIX .. hour) or 0 29 | dataStr = concat({ dataStr, '["', hour, '", ', count, ',', attack_count, ',', blocked_count, '],' }) 30 | end 31 | 32 | dataStr = string.sub(dataStr, 1, -2) .. ']' 33 | return dataStr 34 | end 35 | 36 | local function getAttackTypeTraffic() 37 | local dict = ngx.shared.dict_req_count 38 | local keys = dict:get_keys() 39 | local dataStr = '' 40 | 41 | if keys then 42 | local today = ngx.today() 43 | local prefix = constants.KEY_ATTACK_TYPE_PREFIX .. today 44 | 45 | for _, key in ipairs(keys) do 46 | local from = ngxfind(key, prefix) 47 | if from then 48 | local count = dict:get(key) or 0 49 | dataStr = concat({ dataStr, '{"name":"', key, '","value": ', count, '},' }) 50 | end 51 | end 52 | end 53 | 54 | if #dataStr > 0 then 55 | dataStr = '[' .. string.sub(dataStr, 1, -2) .. ']' 56 | else 57 | dataStr = '[]' 58 | end 59 | 60 | return dataStr 61 | end 62 | 63 | function _M.do_request() 64 | local response = {code = 200, data = {}, msg = ""} 65 | local uri = ngx.var.uri 66 | 67 | if user.check_auth_token() == false then 68 | response.code = 401 69 | response.msg = 'User not logged in' 70 | ngx.status = 401 71 | ngx.say(cjson_encode(response)) 72 | ngx.exit(401) 73 | return 74 | end 75 | 76 | if uri == "/dashboard" then 77 | local trafficDataStr = getRequestTraffic() 78 | local attackTypeDataStr = getAttackTypeTraffic() 79 | 80 | local data = {} 81 | data.trafficData = trafficDataStr 82 | data.attackTypeData = attackTypeDataStr 83 | 84 | local wafStatus = {} 85 | local world = {} 86 | local china = {} 87 | 88 | if is_system_option_on("mysql") then 89 | local res, err = sql.get_today_waf_status() 90 | if res then 91 | wafStatus = res[1] 92 | else 93 | ngx.log(ngx.ERR, err) 94 | end 95 | 96 | res, err = sql.get_30days_world_traffic_stats() 97 | if res then 98 | world = res 99 | else 100 | ngx.log(ngx.ERR, err) 101 | end 102 | 103 | res, err = sql.get_30days_china_traffic_stats() 104 | if res then 105 | china = res 106 | else 107 | ngx.log(ngx.ERR, err) 108 | end 109 | else 110 | local dict = ngx.shared.dict_req_count 111 | 112 | local http4xx = utils.dict_get(dict, constants.KEY_HTTP_4XX) or 0 113 | local http5xx = utils.dict_get(dict, constants.KEY_HTTP_5XX) or 0 114 | local request_times = utils.dict_get(dict, constants.KEY_REQUEST_TIMES) or 0 115 | local attack_times = utils.dict_get(dict, constants.KEY_ATTACK_TIMES) or 0 116 | local block_times_attack = utils.dict_get(dict, constants.KEY_BLOCK_TIMES_ATTACK) or 0 117 | local block_times_captcha = utils.dict_get(dict, constants.KEY_BLOCK_TIMES_CAPTCHA) or 0 118 | local block_times_cc = utils.dict_get(dict, constants.KEY_BLOCK_TIMES_CC) or 0 119 | local captcha_pass_times = utils.dict_get(dict, constants.KEY_CAPTCHA_PASS_TIMES) or 0 120 | 121 | local block_times = block_times_attack + block_times_captcha + block_times_cc 122 | 123 | wafStatus = {http4xx = http4xx, http5xx = http5xx, request_times = request_times, 124 | attack_times = attack_times, block_times = block_times, block_times_attack = block_times_attack, 125 | block_times_captcha = block_times_captcha, block_times_cc = block_times_cc, captcha_pass_times = captcha_pass_times} 126 | end 127 | 128 | data.sourceRegion = {world = world, china = china} 129 | data.wafStatus = wafStatus 130 | response.data = data 131 | end 132 | 133 | ngx.say(cjson_encode(response)) 134 | end 135 | 136 | _M.do_request() 137 | 138 | return _M 139 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 主页 7 | 8 | 9 | 10 | 11 | 12 | 13 | 22 | 23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 36 | 37 |
    38 |
  • 39 |
  • 40 |
41 | 42 |
43 | 44 |
    45 |
  • 46 |
  • 47 |
  • 48 |
  • 49 | 50 | 51 | 52 |
    53 |
    退出登录
    54 |
    55 |
  • 56 | 57 |
  • 58 |
59 |
60 | 61 |
62 | 63 | 69 | 70 |
71 |
72 |
73 |
74 |
Version 1.4.0
75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | 83 |
84 | 85 |
86 |
87 | 88 | 97 | 98 |
99 | 100 |
101 | 102 |
103 |
104 |
105 | 106 |
107 | 108 |
109 | 110 | 111 | 112 | 113 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /admin/lua/events.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2024 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local user = require "user" 6 | local pager = require "lib.pager" 7 | local mysql = require "mysql_cli" 8 | 9 | local tonumber = tonumber 10 | local quote_sql_str = ngx.quote_sql_str 11 | local cjson_encode = cjson.encode 12 | 13 | local _M = {} 14 | 15 | local SQL_COUNT_ATTACK_LOG = 'SELECT COUNT(*) AS total FROM attack_log ' 16 | 17 | local SQL_SELECT_ATTACK_LOG = [[ 18 | SELECT id, request_id, ip, ip_country_code, ip_country_cn, ip_country_en, ip_province_code, ip_province_cn, ip_province_en, ip_city_code, ip_city_cn, ip_city_en, 19 | ip_longitude, ip_latitude, http_method, server_name, user_agent, referer, request_protocol, request_uri, 20 | http_status, request_time, attack_type, severity_level, security_module, hit_rule, action FROM attack_log 21 | ]] 22 | 23 | local SQL_SELECT_ATTACK_LOG_DETAIL = [[ 24 | SELECT id, request_id, ip, ip_country_code, ip_country_cn, ip_country_en, ip_province_code, ip_province_cn, ip_province_en, ip_city_code, ip_city_cn, ip_city_en, 25 | ip_longitude, ip_latitude, http_method, server_name, user_agent, referer, request_protocol, request_uri, 26 | request_body, http_status, response_body, request_time, attack_type, severity_level, security_module, hit_rule, action FROM attack_log 27 | ]] 28 | 29 | -- 查询日志列表数据 30 | local function listLogs() 31 | local response = {code = 200, data = {}, msg = ""} 32 | 33 | local args, err = ngx.req.get_uri_args() 34 | if args then 35 | local page = tonumber(args['page']) 36 | local limit = tonumber(args['limit']) 37 | local offset = pager.get_begin(page, limit) 38 | 39 | local serverName = args['serverName'] 40 | local ip = args['ip'] 41 | local action = args['action'] 42 | local attackType = args['attackType'] 43 | 44 | local where = ' WHERE 1=1 ' 45 | 46 | if serverName and #serverName > 0 then 47 | where = where .. ' AND server_name LIKE ' .. quote_sql_str('%' .. serverName .. '%') 48 | end 49 | 50 | if ip and #ip > 0 then 51 | where = where .. ' AND ip=' .. quote_sql_str(ip) .. ' ' 52 | end 53 | 54 | if attackType and #attackType > 0 then 55 | where = where .. ' AND attack_type=' .. quote_sql_str(attackType) .. ' ' 56 | end 57 | 58 | if action and #action > 0 then 59 | where = where .. ' AND action=' .. quote_sql_str(action) .. ' ' 60 | end 61 | 62 | local res, err = mysql.query(SQL_COUNT_ATTACK_LOG .. where) 63 | 64 | if res and res[1] then 65 | local total = tonumber(res[1].total) 66 | if total > 0 then 67 | res, err = mysql.query(SQL_SELECT_ATTACK_LOG .. where .. ' ORDER BY id DESC LIMIT ' .. offset .. ',' .. limit) 68 | if res then 69 | response.data = res 70 | else 71 | response.code = 500 72 | response.msg = 'query database error' 73 | ngx.log(ngx.ERR, err) 74 | end 75 | end 76 | 77 | response.code = 0 78 | response.count = total 79 | else 80 | response.code = 500 81 | response.msg = 'query database error' 82 | ngx.log(ngx.ERR, err) 83 | end 84 | else 85 | response.code = 500 86 | response.msg = err 87 | end 88 | 89 | if response.code ~= 0 then 90 | ngx.log(ngx.ERR, response.msg) 91 | end 92 | 93 | return response 94 | end 95 | 96 | -- 根据id查询日志详情 97 | local function getLog() 98 | local response = {code = 200, data = {}, msg = ""} 99 | 100 | local args, err = ngx.req.get_uri_args() 101 | if args and args['id'] then 102 | local id = tonumber(args['id']) 103 | local where = ' WHERE id=' .. id 104 | 105 | local res, err = mysql.query(SQL_SELECT_ATTACK_LOG_DETAIL .. where) 106 | if res then 107 | response.data = res[1] 108 | else 109 | response.code = 500 110 | response.msg = 'query database error' 111 | ngx.log(ngx.ERR, err) 112 | end 113 | else 114 | response.code = 500 115 | response.msg = err 116 | ngx.log(ngx.ERR, err) 117 | end 118 | 119 | return response 120 | end 121 | 122 | function _M.do_request() 123 | local response = {code = 200, data = {}, msg = ""} 124 | local uri = ngx.var.uri 125 | 126 | if user.check_auth_token() == false then 127 | response.code = 401 128 | response.msg = 'User not logged in' 129 | ngx.status = 401 130 | ngx.say(cjson_encode(response)) 131 | ngx.exit(401) 132 | return 133 | end 134 | 135 | if uri == "/events/list" then 136 | -- 查询事件数据列表 137 | response = listLogs() 138 | elseif uri == "/events/get" then 139 | -- 查询事件详情 140 | response = getLog() 141 | end 142 | 143 | ngx.say(cjson_encode(response)) 144 | end 145 | 146 | _M.do_request() 147 | 148 | return _M 149 | -------------------------------------------------------------------------------- /conf/global_rules/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 20, 3 | "moduleName": "Post检测", 4 | "rules": [ 5 | { 6 | "id": 1, 7 | "state": "on", 8 | "action": "deny", 9 | "rule": "select.+(from|limit)", 10 | "attackType": "sqli", 11 | "severityLevel": "high" 12 | }, 13 | { 14 | "id": 2, 15 | "state": "on", 16 | "action": "deny", 17 | "rule": "(?:(union(.*?)select))", 18 | "attackType": "sqli", 19 | "severityLevel": "high" 20 | }, 21 | { 22 | "id": 3, 23 | "state": "on", 24 | "action": "deny", 25 | "rule": "having|rongjitest", 26 | "attackType": "sqli", 27 | "severityLevel": "high" 28 | }, 29 | { 30 | "id": 4, 31 | "state": "on", 32 | "action": "deny", 33 | "rule": "sleep\\((\\s*)(\\d*)(\\s*)\\)", 34 | "attackType": "sqli", 35 | "severityLevel": "high" 36 | }, 37 | { 38 | "id": 5, 39 | "state": "on", 40 | "action": "deny", 41 | "rule": "benchmark\\((.*)\\,(.*)\\)", 42 | "attackType": "sqli", 43 | "severityLevel": "high" 44 | }, 45 | { 46 | "id": 6, 47 | "state": "on", 48 | "action": "deny", 49 | "rule": "base64_decode\\(", 50 | "attackType": "sqli", 51 | "severityLevel": "medium" 52 | }, 53 | { 54 | "id": 7, 55 | "state": "on", 56 | "action": "deny", 57 | "rule": "(?:from\\W+information_schema\\W)", 58 | "attackType": "sqli", 59 | "severityLevel": "high" 60 | }, 61 | { 62 | "id": 8, 63 | "state": "on", 64 | "action": "deny", 65 | "rule": "(?:(?:current_)user|database|schema|connection_id)\\s*\\(", 66 | "attackType": "sqli", 67 | "severityLevel": "high" 68 | }, 69 | { 70 | "id": 9, 71 | "state": "on", 72 | "action": "deny", 73 | "rule": "(?:etc\\/\\W*passwd)", 74 | "attackType": "directory_traversal", 75 | "severityLevel": "critical" 76 | }, 77 | { 78 | "id": 10, 79 | "state": "on", 80 | "action": "deny", 81 | "rule": "into(\\s+)+(?:dump|out)file\\s*", 82 | "attackType": "sqli", 83 | "severityLevel": "high" 84 | }, 85 | { 86 | "id": 11, 87 | "state": "on", 88 | "action": "deny", 89 | "rule": "group\\s+by.+\\(", 90 | "attackType": "sqli", 91 | "severityLevel": "high" 92 | }, 93 | { 94 | "id": 12, 95 | "state": "on", 96 | "action": "deny", 97 | "rule": "(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\\(", 98 | "attackType": "rce", 99 | "severityLevel": "high" 100 | }, 101 | { 102 | "id": 13, 103 | "state": "on", 104 | "action": "deny", 105 | "rule": "xwork.MethodAccessor", 106 | "attackType": "rce", 107 | "severityLevel": "high" 108 | }, 109 | { 110 | "id": 14, 111 | "state": "on", 112 | "action": "deny", 113 | "rule": "xwork\\.MethodAccessor", 114 | "attackType": "rce", 115 | "severityLevel": "high" 116 | }, 117 | { 118 | "id": 15, 119 | "state": "on", 120 | "action": "deny", 121 | "rule": "java\\.lang", 122 | "attackType": "rce", 123 | "severityLevel": "high" 124 | }, 125 | { 126 | "id": 16, 127 | "state": "on", 128 | "action": "deny", 129 | "rule": "(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\\:\\/", 130 | "attackType": "rce", 131 | "severityLevel": "low" 132 | }, 133 | { 134 | "id": 17, 135 | "state": "on", 136 | "action": "deny", 137 | "rule": "\\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\\[", 138 | "attackType": "codei", 139 | "severityLevel": "medium" 140 | }, 141 | { 142 | "id": 18, 143 | "state": "on", 144 | "action": "deny", 145 | "rule": "\\<(iframe|script|body|img|layer|div|meta|style|base|object|input)", 146 | "attackType": "xss", 147 | "severityLevel": "low" 148 | }, 149 | { 150 | "id": 19, 151 | "state": "on", 152 | "action": "deny", 153 | "rule": "(onmouseover|onerror|onload)\\=", 154 | "attackType": "xss", 155 | "severityLevel": "low" 156 | } 157 | ] 158 | } -------------------------------------------------------------------------------- /lib/file_utils.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local lfs = require "lfs" 6 | 7 | local lower = string.lower 8 | local insert = table.insert 9 | local pairs = pairs 10 | local pcall = pcall 11 | local cjson_decode = cjson.decode 12 | local io_open = io.open 13 | 14 | local _M = {} 15 | 16 | function _M.read_rule(file_path, file_name) 17 | local file, err = io_open(file_path .. file_name .. ".json", "r") 18 | if not file then 19 | -- ngx.log(ngx.ERR, "Failed to open file ", err) 20 | return 21 | end 22 | 23 | local moduleName = nil 24 | local modules = {} 25 | local table_rules = {} 26 | local table_other = {} 27 | local text = file:read('*a') 28 | 29 | file:close() 30 | 31 | if #text > 0 then 32 | local result = cjson_decode(text) 33 | 34 | if result then 35 | moduleName = result.moduleName 36 | for key, value in pairs(result) do 37 | if key == "rules" then 38 | for _, r in pairs(value) do 39 | if lower(r.state) == 'on' then 40 | r.hits = 0 41 | r.totalHits = 0 42 | insert(table_rules, r) 43 | end 44 | end 45 | else 46 | table_other[key] = value 47 | end 48 | end 49 | end 50 | end 51 | 52 | modules.moduleName = moduleName or '' 53 | modules.rules = table_rules 54 | 55 | return modules, table_other 56 | end 57 | 58 | function _M.read_file_to_table(file_path) 59 | local file, err = io_open(file_path, "r") 60 | if not file then 61 | ngx.log(ngx.ERR, "Failed to open file ", err) 62 | return 63 | end 64 | 65 | local t = {} 66 | 67 | for line in file:lines() do 68 | line = string.gsub(line, "[\r\n]", "") 69 | table.insert(t, line) 70 | end 71 | 72 | file:close() 73 | 74 | return t 75 | end 76 | 77 | function _M.read_file_to_string(file_path, binary) 78 | if not file_path then 79 | ngx.log(ngx.ERR, "No file found ", file_path) 80 | return 81 | end 82 | 83 | local mode = "r" 84 | if binary == true then 85 | mode = "rb" 86 | end 87 | 88 | local file, err = io_open(file_path, mode) 89 | if not file then 90 | -- ngx.log(ngx.ERR, "Failed to open file ", err) 91 | return 92 | end 93 | 94 | local content = "" 95 | repeat 96 | local chunk = file:read(8192) -- 读取 8KB 的块 97 | if chunk then 98 | content = content .. chunk 99 | else 100 | break 101 | end 102 | until not chunk 103 | 104 | file:close() 105 | return content 106 | end 107 | 108 | function _M.write_string_to_file(file_path, str, append) 109 | if str == nil then 110 | return 111 | end 112 | 113 | local mode = 'w' 114 | if append == true then 115 | mode = 'a' 116 | end 117 | local file, err = io_open(file_path, mode) 118 | if not file then 119 | ngx.log(ngx.ERR, "Failed to open file ", err) 120 | return 121 | end 122 | 123 | file:write(str) 124 | file:flush() 125 | file:close() 126 | end 127 | 128 | function _M.remove_file(file_path) 129 | if not file_path then 130 | ngx.log(ngx.ERR, "No file found ", file_path) 131 | return 132 | end 133 | 134 | local success, err = os.remove(file_path) 135 | if success then 136 | ngx.log(ngx.INFO, file_path .. " has been successfully removed.") 137 | else 138 | ngx.log(ngx.ERR, "failed to remove file " .. file_path .. " " .. err) 139 | end 140 | end 141 | 142 | function _M.mkdir(path) 143 | local res, err = lfs.mkdir(path) 144 | if not res then 145 | ngx.log(ngx.ERR, err) 146 | end 147 | return res, err 148 | end 149 | 150 | function _M.is_directory(path) 151 | local attr = lfs.attributes(path) 152 | return attr and attr.mode == "directory" 153 | end 154 | 155 | function _M.rmdir(path) 156 | if not _M.is_directory(path) then 157 | return false, "failed to remove directory " .. path .. " is not a directory" 158 | end 159 | 160 | for entry in lfs.dir(path) do 161 | if entry ~= "." and entry ~= ".." then 162 | local e = path .. '/' .. entry 163 | 164 | local mode = lfs.attributes(e, "mode") 165 | 166 | if mode == "directory" then 167 | _M.rmdir(e) 168 | else 169 | _M.remove_file(e) 170 | end 171 | end 172 | end 173 | 174 | local res, err = lfs.rmdir(path) 175 | if not res then 176 | ngx.log(ngx.ERR, "failed to remove directory " .. path, err) 177 | end 178 | 179 | return res, err 180 | end 181 | 182 | function _M.is_file_exists(file_path) 183 | if not file_path then 184 | return false 185 | end 186 | 187 | local res, attr = pcall(lfs.attributes, file_path) 188 | if res and attr then 189 | return true 190 | end 191 | 192 | return false 193 | end 194 | 195 | return _M -------------------------------------------------------------------------------- /conf/global_rules/cookie.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 21, 3 | "moduleName": "Cookie检测", 4 | "rules": [ 5 | { 6 | "id": 1, 7 | "state": "on", 8 | "action": "deny", 9 | "rule": "\\.\\./", 10 | "attackType": "directory_traversal", 11 | "severityLevel": "low" 12 | }, 13 | { 14 | "id": 2, 15 | "state": "on", 16 | "action": "deny", 17 | "rule": "\\:\\$", 18 | "attackType": "codei", 19 | "severityLevel": "low" 20 | }, 21 | { 22 | "id": 3, 23 | "state": "on", 24 | "action": "deny", 25 | "rule": "\\$\\{", 26 | "attackType": "codei", 27 | "severityLevel": "low" 28 | }, 29 | { 30 | "id": 4, 31 | "state": "on", 32 | "action": "deny", 33 | "rule": "select.+(from|limit)", 34 | "attackType": "sqli", 35 | "severityLevel": "high" 36 | }, 37 | { 38 | "id": 5, 39 | "state": "on", 40 | "action": "deny", 41 | "rule": "(?:(union(.*?)select))", 42 | "attackType": "sqli", 43 | "severityLevel": "high" 44 | }, 45 | { 46 | "id": 6, 47 | "state": "on", 48 | "action": "deny", 49 | "rule": "having|rongjitest", 50 | "attackType": "sqli", 51 | "severityLevel": "high" 52 | }, 53 | { 54 | "id": 7, 55 | "state": "on", 56 | "action": "deny", 57 | "rule": "sleep\\((\\s*)(\\d*)(\\s*)\\)", 58 | "attackType": "sqli", 59 | "severityLevel": "high" 60 | }, 61 | { 62 | "id": 8, 63 | "state": "on", 64 | "action": "deny", 65 | "rule": "benchmark\\((.*)\\,(.*)\\)", 66 | "attackType": "sqli", 67 | "severityLevel": "high" 68 | }, 69 | { 70 | "id": 9, 71 | "state": "on", 72 | "action": "deny", 73 | "rule": "base64_decode\\(", 74 | "attackType": "sqli", 75 | "severityLevel": "medium" 76 | }, 77 | { 78 | "id": 10, 79 | "state": "on", 80 | "action": "deny", 81 | "rule": "(?:from\\W+information_schema\\W)", 82 | "attackType": "sqli", 83 | "severityLevel": "high" 84 | }, 85 | { 86 | "id": 11, 87 | "state": "on", 88 | "action": "deny", 89 | "rule": "(?:etc\\/\\W*passwd)", 90 | "attackType": "directory_traversal", 91 | "severityLevel": "critical" 92 | }, 93 | { 94 | "id": 12, 95 | "state": "on", 96 | "action": "deny", 97 | "rule": "(?:(?:current_)user|database|schema|connection_id)\\s*\\(", 98 | "attackType": "sqli", 99 | "severityLevel": "high" 100 | }, 101 | { 102 | "id": 13, 103 | "state": "on", 104 | "action": "deny", 105 | "rule": "into(\\s+)+(?:dump|out)file\\s*", 106 | "attackType": "sqli", 107 | "severityLevel": "high" 108 | }, 109 | { 110 | "id": 14, 111 | "state": "on", 112 | "action": "deny", 113 | "rule": "group\\s+by.+\\(", 114 | "attackType": "sqli", 115 | "severityLevel": "high" 116 | }, 117 | { 118 | "id": 15, 119 | "state": "on", 120 | "action": "deny", 121 | "rule": "(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\\:\\/", 122 | "attackType": "rce", 123 | "severityLevel": "low" 124 | }, 125 | { 126 | "id": 16, 127 | "state": "on", 128 | "action": "deny", 129 | "rule": "(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\\(", 130 | "attackType": "rce", 131 | "severityLevel": "high" 132 | }, 133 | { 134 | "id": 17, 135 | "state": "on", 136 | "action": "deny", 137 | "rule": "xwork.MethodAccessor", 138 | "attackType": "rce", 139 | "severityLevel": "medium" 140 | }, 141 | { 142 | "id": 18, 143 | "state": "on", 144 | "action": "deny", 145 | "rule": "xwork\\.MethodAccessor", 146 | "attackType": "rce", 147 | "severityLevel": "medium" 148 | }, 149 | { 150 | "id": 19, 151 | "state": "on", 152 | "action": "deny", 153 | "rule": "java\\.lang", 154 | "attackType": "rce", 155 | "severityLevel": "medium" 156 | }, 157 | { 158 | "id": 20, 159 | "state": "on", 160 | "action": "deny", 161 | "rule": "\\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\\[", 162 | "attackType": "codei", 163 | "severityLevel": "medium" 164 | } 165 | ] 166 | } -------------------------------------------------------------------------------- /lib/action.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local config = require "config" 5 | local redis_cli = require "redis_cli" 6 | local captcha = require "captcha" 7 | local constants = require "constants" 8 | local request = require "request" 9 | 10 | local md5 = ngx.md5 11 | local ngxsub = ngx.re.sub 12 | local upper = string.upper 13 | local ostime = os.time 14 | local osdate = os.date 15 | 16 | local get_site_config = config.get_site_config 17 | local is_system_option_on = config.is_system_option_on 18 | local get_system_config = config.get_system_config 19 | 20 | local _M = {} 21 | 22 | local dict_hits = ngx.shared.dict_config_rules_hits 23 | local RULES_HIT_PREFIX = "waf_rules_hits:" 24 | local RULES_HIT_EXPTIME = 60 25 | local REDIRECT_HTML = get_system_config().html 26 | local REGEX_OPTION = "jo" 27 | 28 | local function deny(status) 29 | if get_site_config("waf").mode == "protection" then 30 | ngx.ctx.is_blocked = true 31 | 32 | return ngx.exit(status or ngx.HTTP_FORBIDDEN) 33 | else 34 | ngx.ctx.action = "ALLOW" 35 | end 36 | end 37 | 38 | local function redirect() 39 | if get_site_config("waf").mode == "protection" then 40 | ngx.ctx.is_blocked = true 41 | ngx.header.content_type = "text/html; charset=UTF-8" 42 | ngx.status = ngx.HTTP_FORBIDDEN 43 | local ctx = ngx.ctx 44 | local html = REDIRECT_HTML 45 | 46 | html = ngxsub(html, "\\$remote_addr", ctx.ip, REGEX_OPTION) 47 | html = ngxsub(html, "\\$request_id", ctx.request_id, REGEX_OPTION) 48 | html = ngxsub(html, "\\$blocked_time", osdate("%Y-%m-%d %H:%M:%S", ostime()), REGEX_OPTION) 49 | html = ngxsub(html, "\\$user_agent", ctx.ua, REGEX_OPTION) 50 | 51 | ngx.say(html) 52 | return ngx.exit(ngx.status) 53 | end 54 | end 55 | 56 | -- block ip 57 | function _M.block_ip(ip, rule_table) 58 | if upper(rule_table.autoIpBlock) == "ON" and ip then 59 | local ok, err = nil, nil 60 | 61 | if is_system_option_on("redis") then 62 | local key = constants.KEY_BLACKIP_PREFIX .. ip 63 | 64 | ok, err = redis_cli.set(key, 1, rule_table.ipBlockExpireInSeconds) 65 | if ok then 66 | ngx.ctx.ip_blocked = true 67 | else 68 | ngx.log(ngx.ERR, "failed to block ip " .. ip, err) 69 | end 70 | else 71 | local blackip = ngx.shared.dict_blackip 72 | ok, err = blackip:set(ip, 1, rule_table.ipBlockExpireInSeconds) 73 | if ok then 74 | ngx.ctx.ip_blocked = true 75 | else 76 | ngx.log(ngx.ERR, "failed to block ip " .. ip, err) 77 | end 78 | end 79 | 80 | return ok 81 | end 82 | end 83 | 84 | function _M.unblock_ip(ip) 85 | local ok, err = nil, nil 86 | 87 | if is_system_option_on("redis") then 88 | local key = constants.KEY_BLACKIP_PREFIX .. ip 89 | ok, err = redis_cli.del(key) 90 | else 91 | local blackip = ngx.shared.dict_blackip 92 | 93 | ok, err = blackip:delete(ip) 94 | if not ok then 95 | ngx.log(ngx.ERR, "failed to delete key " .. ip, err) 96 | end 97 | end 98 | 99 | return ok 100 | end 101 | 102 | local function hit(module_name, rule_table) 103 | if is_system_option_on('rulesSort') then 104 | local ruleMd5Str = md5(rule_table.rule) 105 | local server_name = ngx.ctx.server_name 106 | local attackType = server_name .. module_name 107 | local key = RULES_HIT_PREFIX .. attackType .. '_' .. ruleMd5Str 108 | local key_total = RULES_HIT_PREFIX .. attackType .. '_total_' .. ruleMd5Str 109 | local newHits = nil 110 | local newTotalHits = nil 111 | 112 | if is_system_option_on("redis") then 113 | local count = redis_cli.get(key) 114 | if not count then 115 | redis_cli.set(key, 1, RULES_HIT_EXPTIME) 116 | else 117 | newHits = redis_cli.incr(key) 118 | end 119 | newTotalHits = redis_cli.incr(key_total) 120 | else 121 | newHits = dict_hits:incr(key, 1, 0, RULES_HIT_EXPTIME) 122 | newTotalHits = dict_hits:incr(key_total, 1, 0) 123 | end 124 | 125 | rule_table.hits = newHits or 1 126 | rule_table.totalHits = newTotalHits or 1 127 | end 128 | end 129 | 130 | function _M.do_action(module_name, rule_table, data, attackType, status) 131 | local action = upper(rule_table.action) 132 | if attackType == nil then 133 | attackType = rule_table.attackType 134 | else 135 | rule_table.attackType = attackType 136 | end 137 | 138 | hit(module_name, rule_table) 139 | ngx.ctx.module_name = module_name 140 | ngx.ctx.rule_table = rule_table 141 | ngx.ctx.action = action 142 | ngx.ctx.hit_data = data 143 | ngx.ctx.is_attack = true 144 | 145 | request.get_request_body() 146 | 147 | if action == "ALLOW" then 148 | return ngx.exit(ngx.OK) 149 | elseif action == "DENY" then 150 | deny(status) 151 | elseif action == "REDIRECT" then 152 | redirect() 153 | elseif action == "CAPTCHA" then 154 | ngx.ctx.is_attack = false 155 | captcha.trigger_captcha() 156 | else 157 | redirect() 158 | end 159 | end 160 | 161 | return _M 162 | -------------------------------------------------------------------------------- /admin/view/ip-blocking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IP封禁日志 7 | 8 | 9 | 10 |
11 |
12 |
13 |
IP封禁日志
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /conf/global_rules/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "nextId": 21, 3 | "moduleName": "Args检测", 4 | "rules": [ 5 | { 6 | "id": 1, 7 | "state": "on", 8 | "action": "redirect", 9 | "rule": "select.+(from|limit)", 10 | "attackType": "sqli", 11 | "severityLevel": "high" 12 | }, 13 | { 14 | "id": 2, 15 | "state": "on", 16 | "action": "redirect", 17 | "rule": "(?:(union(.*?)select))", 18 | "attackType": "sqli", 19 | "severityLevel": "high" 20 | }, 21 | { 22 | "id": 3, 23 | "state": "on", 24 | "action": "redirect", 25 | "rule": "having|rongjitest", 26 | "attackType": "sqli", 27 | "severityLevel": "high" 28 | }, 29 | { 30 | "id": 4, 31 | "state": "on", 32 | "action": "redirect", 33 | "rule": "sleep\\((\\s*)(\\d*)(\\s*)\\)", 34 | "attackType": "sqli", 35 | "severityLevel": "high" 36 | }, 37 | { 38 | "id": 5, 39 | "state": "on", 40 | "action": "redirect", 41 | "rule": "benchmark\\((.*)\\,(.*)\\)", 42 | "attackType": "sqli", 43 | "severityLevel": "high" 44 | }, 45 | { 46 | "id": 6, 47 | "state": "on", 48 | "action": "redirect", 49 | "rule": "(?:from\\W+information_schema\\W)", 50 | "attackType": "sqli", 51 | "severityLevel": "medium" 52 | }, 53 | { 54 | "id": 7, 55 | "state": "on", 56 | "action": "redirect", 57 | "rule": "(?:(?:current_)user|database|schema|connection_id)\\s*\\(", 58 | "attackType": "sqli", 59 | "severityLevel": "high" 60 | }, 61 | { 62 | "id": 8, 63 | "state": "on", 64 | "action": "redirect", 65 | "rule": "(?:etc\\/\\W*passwd)", 66 | "attackType": "directory_traversal", 67 | "severityLevel": "critical" 68 | }, 69 | { 70 | "id": 9, 71 | "state": "on", 72 | "action": "redirect", 73 | "rule": "into(\\s+)+(?:dump|out)file\\s*", 74 | "attackType": "sqli", 75 | "severityLevel": "high" 76 | }, 77 | { 78 | "id": 10, 79 | "state": "on", 80 | "action": "redirect", 81 | "rule": "group\\s+by.+\\(", 82 | "attackType": "sqli", 83 | "severityLevel": "high" 84 | }, 85 | { 86 | "id": 11, 87 | "state": "on", 88 | "action": "redirect", 89 | "rule": "\\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\\[", 90 | "attackType": "codei", 91 | "severityLevel": "medium" 92 | }, 93 | { 94 | "id": 12, 95 | "state": "on", 96 | "action": "redirect", 97 | "rule": "\\<(iframe|script|body|img|layer|div|meta|style|base|object|input)", 98 | "attackType": "xss", 99 | "severityLevel": "low" 100 | }, 101 | { 102 | "id": 13, 103 | "state": "on", 104 | "action": "redirect", 105 | "rule": "(onmouseover|onerror|onload)\\=", 106 | "attackType": "xss", 107 | "severityLevel": "low" 108 | }, 109 | { 110 | "id": 14, 111 | "state": "on", 112 | "action": "redirect", 113 | "rule": "/shell?cd+/tmp;\\s*rm+-rf\\+\\*;\\s*wget", 114 | "attackType": "commandi", 115 | "severityLevel": "critical" 116 | }, 117 | { 118 | "id": 15, 119 | "state": "on", 120 | "action": "redirect", 121 | "rule": "(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\\:\\/", 122 | "attackType": "rce", 123 | "severityLevel": "low" 124 | }, 125 | { 126 | "id": 16, 127 | "state": "on", 128 | "action": "redirect", 129 | "rule": "(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\\(", 130 | "attackType": "rce", 131 | "severityLevel": "high" 132 | }, 133 | { 134 | "id": 17, 135 | "state": "on", 136 | "action": "redirect", 137 | "rule": "xwork.MethodAccessor", 138 | "attackType": "rce", 139 | "severityLevel": "medium" 140 | }, 141 | { 142 | "id": 18, 143 | "state": "on", 144 | "action": "redirect", 145 | "rule": "xwork\\.MethodAccessor", 146 | "attackType": "rce", 147 | "severityLevel": "medium" 148 | }, 149 | { 150 | "id": 19, 151 | "state": "on", 152 | "action": "redirect", 153 | "rule": "java\\.lang", 154 | "attackType": "rce", 155 | "severityLevel": "medium" 156 | }, 157 | { 158 | "id": 20, 159 | "state": "on", 160 | "action": "redirect", 161 | "rule": "/systembc/password.php", 162 | "attackType": "backdoor", 163 | "severityLevel": "high" 164 | } 165 | ] 166 | } -------------------------------------------------------------------------------- /admin/component/pear/module/drawer.js: -------------------------------------------------------------------------------- 1 | layui.define(["jquery","element","layer","loading"],function(t){"use strict";var e=layui.jquery,n=(layui.element,layui.layer),i=layui.loading;function s(t,e){var n="pear-drawer pear-drawer-anim layui-anim layer-anim-",i="rl";return e&&(n="position-absolute "+n),"l"===t?i="lr":"r"===t?i="rl":"t"===t?i="tb":"b"===t&&(i="bt"),n+i}function a(t,e,n){return function(){e&&"function"==typeof e&&e.apply(this,arguments),t.apply(this,arguments),n&&"function"==typeof n&&n.apply(this,arguments)}}t("drawer",new function(){this.open=function(t){if(void 0===t.legacy&&(t.legacy=!0),t.legacy){var o=new mSlider({target:t.target,dom:t.dom,direction:t.direction,distance:t.distance,time:t.time?t.time:0,maskClose:t.maskClose,callback:t.success});return o.open(),o}return function(t){var o=function(t){t.direction&&!t.offset&&("right"===t.direction?t.offset="r":"left"===t.direction?t.offset="l":"top"===t.direction?t.offset="t":"bottom"===t.direction?t.offset="b":t.offset="r");t.distance&&!t.area&&(t.area=t.distance);t.dom&&!t.content&&(t.content=e(t.dom));t.maskClose&&void 0===t.shadeClose&&(t.shadeClose="false"!==(t.maskClose+"").toString());t.type=1,t.anim=-1,t.move=!1,t.fixed=!0,t.iframe&&(t.type=2,t.content=t.iframe);void 0===t.offset&&(t.offset="r");t.area=(n=t.offset,i=t.area,i instanceof Array?i:(void 0!==i&&"auto"!==i||(i="30%"),"l"===n||"r"===n?[i,"100%"]:"t"===n||"b"===n?["100%",i]:[i,"100%"])),void 0===t.title&&(t.title=!1);var n,i;void 0===t.closeBtn&&(t.closeBtn=!1);void 0===t.shade&&(t.shade=.3);void 0===t.shadeClose&&(t.shadeClose=!0);void 0===t.skin&&(t.skin=s(t.offset));void 0===t.resize&&(t.resize=!1);void 0===t.success&&(t.success=function(){});void 0===t.end&&(t.end=function(){});return t}(t);o.target&&function(t){var n=e(t.target),i=e(t.content);i.appendTo(n),t.skin=s(t.offset,!0),t.offset=function(t,e,n){if(void 0===t||"l"===t||"t"===t)t="lt";else if("r"===t){var i;e instanceof Array&&(e=e[0]),i=-1!=e.indexOf("%")?n.innerWidth()*(1-e.replace("%","")/100):n.innerWidth()-e,t=[0,i]}else if("b"===t){var s;e instanceof Array&&(e=e[1]),s=-1!=e.indexOf("%")?n.innerHeight()*(1-e.replace("%","")/100):n.innerHeight()-e,t=[s,0]}return t}(t.offset,t.area,n),t.end=a(t.end,function(){i.css("display","none")}),t.shade&&(t.success=a(t.success,function(t,n){var i=e("#layui-layer-shade"+n);i.css("position","absolute"),i.appendTo(t.parent())}))}(o);o.url&&function(t){t.success=a(t.success,function(n,s){var a="#"+n.attr("id");i.block({type:1,elem:a,msg:""}),e.ajax({url:t.url,dataType:"html",success:function(t){n.children(".layui-layer-content").html(t),i.blockRemove(a)}})})}(o);return n.open(o)}(t)},this.title=n.title,this.style=n.style,this.close=n.close,this.closeAll=n.closeAll})}),function(t,e){function n(t){this.opts={target:t.target||"body",direction:t.direction||"left",distance:t.distance||"60%",dom:this.Q(t.dom),time:t.time||"",maskClose:"false"!==(t.maskClose+"").toString(),callback:t.callback||""},this.rnd=this.rnd(),this.target=this.opts.target,this.dom=this.opts.dom[0],this.wrap="",this.inner="",this.mask="",this.init()}n.prototype={Q:function(t){return document.querySelectorAll(t)},isMobile:function(){return!!navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)},addEvent:function(t,e,n){t.attachEvent?t.attachEvent("on"+e,n):t.addEventListener(e,n,!1)},rnd:function(){return Math.random().toString(36).substr(2,6)},init:function(){var t=this;if(t.dom){t.dom.style.display="block";var e=document.createElement("div"),n=document.createElement("div"),i=document.createElement("div");switch(e.setAttribute("class","mSlider-main ms-"+t.rnd),n.setAttribute("class","mSlider-inner"),i.setAttribute("class","mSlider-mask"),t.Q(t.target)[0].appendChild(e),t.Q(".ms-"+t.rnd)[0].appendChild(n),t.Q(".ms-"+t.rnd)[0].appendChild(i),t.wrap=t.Q(".ms-"+t.rnd)[0],t.inner=t.Q(".ms-"+t.rnd+" .mSlider-inner")[0],t.mask=t.Q(".ms-"+t.rnd+" .mSlider-mask")[0],t.inner.appendChild(t.dom),t.opts.direction){case"top":t.top="0",t.left="0",t.width="100%",t.height=t.opts.distance,t.translate="0,-100%,0";break;case"bottom":t.bottom="0",t.left="0",t.width="100%",t.height=t.opts.distance,t.translate="0,100%,0";break;case"right":t.top="0",t.right="0",t.width=t.opts.distance,t.height=document.documentElement.clientHeight+"px",t.translate="100%,0,0";break;default:t.top="0",t.left="0",t.width=t.opts.distance,t.height=document.documentElement.clientHeight+"px",t.translate="-100%,0,0"}t.wrap.style.display="none",t.wrap.style.position="body"===t.target?"fixed":"absolute",t.wrap.style.top="0",t.wrap.style.left="0",t.wrap.style.width="100%",t.wrap.style.height="100%",t.wrap.style.zIndex=9999999,t.inner.style.position="absolute",t.inner.style.top=t.top,t.inner.style.bottom=t.bottom,t.inner.style.left=t.left,t.inner.style.right=t.right,t.inner.style.width=t.width,t.inner.style.height="body"===t.target?t.height:"100%",t.inner.style.backgroundColor="#fff",t.inner.style.transform="translate3d("+t.translate+")",t.inner.style.webkitTransition="all .2s ease-out",t.inner.style.transition="all .2s ease-out",t.inner.style.zIndex=1e7,t.mask.style.width="100%",t.mask.style.height="100%",t.mask.style.opacity="0.1",t.mask.style.backgroundColor="black",t.mask.style.zIndex="9999998",t.mask.style.webkitBackfaceVisibility="hidden",t.events()}else console.log("未正确绑定弹窗容器")},open:function(){var t=this;t.wrap.style.display="block",setTimeout(function(){t.inner.style.transform="translate3d(0,0,0)",t.inner.style.webkitTransform="translate3d(0,0,0)",t.mask.style.opacity=.1},30),t.opts.time&&(t.timer=setTimeout(function(){t.close()},t.opts.time))},close:function(){var t=this;t.timer&&clearTimeout(t.timer),t.inner.style.webkitTransform="translate3d("+t.translate+")",t.inner.style.transform="translate3d("+t.translate+")",t.mask.style.opacity=0,setTimeout(function(){t.wrap.style.display="none",t.timer=null,t.opts.callback&&t.opts.callback()},300)},events:function(){var t=this;t.addEvent(t.mask,"touchmove",function(t){t.preventDefault()}),t.addEvent(t.mask,t.isMobile()?"touchend":"click",function(e){t.opts.maskClose&&t.close()})}},t.mSlider=n}(window); -------------------------------------------------------------------------------- /admin/lua/cc_defense.lua: -------------------------------------------------------------------------------- 1 | -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 2 | -- Copyright (c) 2023 bukale bukale2022@163.com 3 | 4 | local cjson = require "cjson" 5 | local config = require "config" 6 | local user = require "user" 7 | local rule_utils = require "lib.rule_utils" 8 | 9 | local tonumber = tonumber 10 | 11 | local get_site_config_file = config.get_site_config_file 12 | local get_site_module_rule_file = config.get_site_module_rule_file 13 | local update_site_config_file = config.update_site_config_file 14 | 15 | local cjson_decode = cjson.decode 16 | local cjson_encode = cjson.encode 17 | 18 | local _M = {} 19 | 20 | local MODULE_ID = 'cc' 21 | 22 | function _M.do_request() 23 | local response = {code = 200, data = {}, msg = ""} 24 | local uri = ngx.var.uri 25 | local reload = false 26 | 27 | if user.check_auth_token() == false then 28 | response.code = 401 29 | response.msg = 'User not logged in' 30 | ngx.status = 401 31 | ngx.say(cjson_encode(response)) 32 | ngx.exit(401) 33 | return 34 | end 35 | 36 | if uri == "/cc/config/get" then 37 | local args, err = ngx.req.get_uri_args() 38 | if args then 39 | local site_id = tostring(args['siteId']) 40 | local _, content = get_site_config_file(site_id) 41 | if content then 42 | local config_table = cjson_decode(content) 43 | local cc = config_table.cc 44 | response.data = cjson_encode(cc) 45 | else 46 | response.code = 500 47 | response.msg = 'no config file found' 48 | end 49 | else 50 | response.code = 500 51 | response.msg = err 52 | end 53 | elseif uri == "/cc/config/state/update" then 54 | -- 修改配置 55 | ngx.req.read_body() 56 | local args, err = ngx.req.get_post_args() 57 | if args then 58 | local site_id = tostring(args['siteId']) 59 | local state = args.state 60 | local _, content = get_site_config_file(site_id) 61 | 62 | if state and content then 63 | local config_table = cjson_decode(content) 64 | if config_table then 65 | config_table.cc.state = state 66 | local new_config_json = cjson_encode(config_table) 67 | update_site_config_file(site_id, new_config_json) 68 | reload = true 69 | end 70 | else 71 | response.code = 500 72 | response.msg = 'param error' 73 | end 74 | else 75 | response.code = 500 76 | response.msg = err 77 | end 78 | elseif uri == "/cc/rule/list" then 79 | local args, err = ngx.req.get_uri_args() 80 | if args then 81 | local site_id = tostring(args['siteId']) 82 | if site_id then 83 | local file_path = get_site_module_rule_file(site_id, MODULE_ID) 84 | response = rule_utils.list_rules(file_path) 85 | else 86 | response.code = 500 87 | response.msg = 'param error' 88 | end 89 | else 90 | response.code = 500 91 | response.msg = err 92 | end 93 | elseif uri == "/cc/rule/save" then 94 | -- 修改或新增cc规则 95 | ngx.req.read_body() 96 | local args, err = ngx.req.get_post_args() 97 | if args then 98 | local site_id = tostring(args['siteId']) 99 | if site_id then 100 | local rule_new = rule_utils.get_rule_from_request() 101 | if rule_new then 102 | rule_new.id = tonumber(rule_new.id) 103 | rule_new.duration = tonumber(rule_new.duration) 104 | rule_new.threshold = tonumber(rule_new.threshold) 105 | rule_new.ipBlockExpireInSeconds = tonumber(rule_new.ipBlockExpireInSeconds) 106 | rule_new.autoIpBlock = rule_new.autoIpBlock or 'off' 107 | rule_new.attackType = 'cc-' .. rule_new.countType 108 | rule_new.severityLevel = 'medium' 109 | 110 | response = rule_utils.save_or_update_site_rule(site_id, MODULE_ID, rule_new) 111 | reload = true 112 | else 113 | response.code = 500 114 | response.msg = 'param error' 115 | end 116 | else 117 | response.code = 500 118 | response.msg = 'param error' 119 | end 120 | else 121 | response.code = 500 122 | response.msg = err 123 | end 124 | elseif uri == "/cc/rule/state/update" then 125 | -- 修改cc规则状态 126 | ngx.req.read_body() 127 | local args, err = ngx.req.get_post_args() 128 | if args then 129 | local site_id = tostring(args['siteId']) 130 | local rule_id = tonumber(args['ruleId']) 131 | local state = tostring(args['state']) 132 | 133 | response = rule_utils.update_site_rule_state(site_id, MODULE_ID, rule_id, state) 134 | if response and response.code == 200 then 135 | reload = true 136 | end 137 | else 138 | response.code = 500 139 | response.msg = err 140 | end 141 | elseif uri == "/cc/rule/remove" then 142 | -- 删除cc规则 143 | ngx.req.read_body() 144 | local args, err = ngx.req.get_post_args() 145 | if args then 146 | local site_id = tostring(args['siteId']) 147 | local rule_id = tonumber(args['ruleId']) 148 | 149 | response = rule_utils.delete_site_rule(site_id, MODULE_ID, rule_id) 150 | if response and response.code == 200 then 151 | reload = true 152 | end 153 | else 154 | response.code = 500 155 | response.msg = err 156 | end 157 | reload = true 158 | end 159 | 160 | ngx.say(cjson_encode(response)) 161 | 162 | -- 如果没有错误且需要重载配置文件则重载配置文件 163 | if (response.code == 200 or response.code == 0) and reload == true then 164 | config.reload_config_file() 165 | end 166 | end 167 | 168 | _M.do_request() 169 | 170 | return _M 171 | -------------------------------------------------------------------------------- /admin/component/pear/css/module/menu.css: -------------------------------------------------------------------------------- 1 | .pear-nav-tree { 2 | width: 230px !important; 3 | border-radius: 0px; 4 | background-color: #28333E; 5 | } 6 | 7 | .pear-nav-tree .layui-nav-item>a { 8 | height: 56px; 9 | line-height: 56px; 10 | padding-top: 0px; 11 | padding-bottom: 0px; 12 | } 13 | 14 | .pear-nav-tree .layui-nav-item dd a { 15 | height: 48px; 16 | line-height: 48px; 17 | } 18 | 19 | .pear-nav-tree .layui-nav-item>a .layui-nav-more { 20 | padding: 0px; 21 | } 22 | 23 | .pear-side-scroll::-webkit-scrollbar { 24 | width: 0px; 25 | height: 0px; 26 | } 27 | .pear-side-scroll{ 28 | width: 230px; 29 | } 30 | 31 | .pear-nav-tree .layui-nav-child dd.layui-this, 32 | .layui-nav-tree .layui-nav-child dd.layui-this a, 33 | .layui-nav-tree .layui-this, 34 | .layui-nav-tree .layui-this>a, 35 | .layui-nav-tree .layui-this>a:hover { 36 | background-color: #5FB878; 37 | } 38 | 39 | .pear-nav-tree .toast { 40 | font-size: 14px; 41 | margin: 5px; 42 | margin-right: 8px; 43 | text-align: center; 44 | height: 40px; 45 | line-height: 40px; 46 | color: lightgray; 47 | } 48 | 49 | 50 | .pear-nav-tree .layui-nav-item a i { 51 | margin-right: 12px; 52 | } 53 | 54 | .pear-nav-tree .layui-nav-item a span { 55 | letter-spacing: 2px; 56 | font-size: 13.5px; 57 | } 58 | 59 | .pear-nav-tree .layui-nav-item a:hover { 60 | background-color: transparent; 61 | } 62 | 63 | .pear-nav-tree .layui-nav-more { 64 | margin-right: 5px; 65 | } 66 | 67 | .pear-nav-tree .layui-nav-bar { 68 | display: none; 69 | } 70 | 71 | .pear-nav-tree .layui-nav-item a .layui-badge-dot { 72 | float: right; 73 | right: 13px; 74 | } 75 | 76 | .pear-nav-tree .layui-nav-item a .layui-badge { 77 | float: right; 78 | right: 10px; 79 | } 80 | 81 | /** 实 现 菜 单 隐 藏 */ 82 | .pear-nav-mini { 83 | overflow: hidden; 84 | } 85 | 86 | .pear-nav-mini .layui-nav-item a span { 87 | display: none; 88 | } 89 | 90 | .pear-nav-mini .layui-nav-child { 91 | display: none; 92 | } 93 | 94 | .pear-nav-mini .layui-nav-more { 95 | display: none !important; 96 | } 97 | 98 | .pear-nav-control.pc a { 99 | font-weight: 500; 100 | font-size: 14px; 101 | } 102 | 103 | .pear-nav-control.pc li{ 104 | display: inline-block; 105 | } 106 | 107 | .pear-nav-control.pc .layui-nav-bar { 108 | top: 0px !important; 109 | } 110 | 111 | .pear-nav-control.pc .layui-this * { 112 | background-color: whitesmoke; 113 | } 114 | 115 | .pear-nav-control.pc *{ 116 | color: darkslategray!important; 117 | } 118 | 119 | .pear-nav-control.pc .layui-nav-bar{ 120 | display: none!important; 121 | } 122 | 123 | .pear-nav-control .layui-nav-child{ 124 | border: 1px solid whitesmoke; 125 | border-radius: 6px; 126 | width: 150px; 127 | } 128 | 129 | /** 隐 藏 后 子 级 悬 浮 菜 单 */ 130 | .pear-nav-tree .layui-nav-hover { 131 | position: fixed; 132 | min-width: 130px; 133 | padding: 4px; 134 | display: block !important; 135 | background: transparent !important; 136 | } 137 | .pear-nav-tree .layui-nav-hover:before { 138 | content: ''; 139 | position: absolute; 140 | right: 4px; 141 | left: 4px; 142 | bottom: 0; 143 | top: 0; 144 | border-radius: 4px; 145 | overflow: hidden; 146 | background-color: #28333E; 147 | display: block; 148 | box-shadow: 0px 0px 3px lightgray; 149 | } 150 | .pear-nav-tree .layui-nav-hover a span { 151 | display: inline-block !important; 152 | } 153 | .pear-nav-tree .layui-nav-hover a i { 154 | display: none; 155 | } 156 | .pear-nav-tree .layui-nav-child dd a span { 157 | margin-left: 26px !important; 158 | } 159 | .pear-nav-tree .layui-nav-child dd a i { 160 | display: none; 161 | } 162 | .pear-nav-tree .layui-nav-hover dd a span { 163 | margin-left: 0px !important; 164 | } 165 | .pear-nav-tree dl { 166 | padding-top: 0; 167 | padding-bottom: 0; 168 | } 169 | /** 亮 样 式*/ 170 | .dark-theme .layui-nav-tree{ 171 | background-color: #28333E!important; 172 | } 173 | 174 | .light-theme{ 175 | background-color: white!important; 176 | } 177 | 178 | .light-theme .pear-nav-tree, 179 | .light-theme .pear-nav-tree .layui-nav-hover:before, 180 | .light-theme .pear-nav-tree .layui-nav-child{ 181 | background-color: white!important; 182 | } 183 | 184 | .light-theme .pear-nav-tree a, 185 | .light-theme .pear-nav-tree .layui-nav-more{ 186 | color: dimgray!important; 187 | border-top-color: dimgray; 188 | } 189 | 190 | .light-theme .pear-nav-tree .layui-nav-itemed>a>.layui-nav-more{ 191 | border-top-color: white!important; 192 | border-bottom-color: dimgray!important; 193 | } 194 | 195 | .light-theme .pear-nav-tree .layui-this a, 196 | .light-theme .pear-nav-tree .layui-this{ 197 | color: white!important; 198 | background-color: #5FB878!important; 199 | 200 | } 201 | 202 | .light-theme .pear-nav-tree .layui-this a:hover{ 203 | background-color: #5FB878!important; 204 | 205 | } 206 | 207 | .light-theme .pear-nav-tree .layui-nav-bar{ 208 | display: none; 209 | 210 | } 211 | 212 | /** 下 拉 图 标 */ 213 | .pear-nav-tree.arrow .layui-nav-more { 214 | font-family: layui-icon !important; 215 | font-size: 10px; 216 | font-style: normal; 217 | -webkit-font-smoothing: antialiased; 218 | -moz-osx-font-smoothing: grayscale; 219 | overflow: hidden; 220 | width: auto; 221 | height: auto; 222 | line-height: normal; 223 | border: none; 224 | top: 23px; 225 | margin-right: 2px !important; 226 | margin: 0; 227 | padding: 0; 228 | display: inline-block; 229 | transition: all .2s; 230 | -webkit-transition: all .2s; 231 | } 232 | 233 | .pear-nav-tree.arrow .layui-nav-child .layui-nav-more { 234 | top: 17px; 235 | } 236 | 237 | .pear-nav-tree.arrow .layui-nav-more:before { 238 | content: "\e61a"; 239 | } 240 | 241 | .pear-nav-tree.arrow .layui-nav-itemed>a>.layui-nav-more { 242 | transform: rotate(180deg); 243 | -ms-transform: rotate(180deg); 244 | -moz-transform: rotate(180deg); 245 | -webkit-transform: rotate(180deg); 246 | -o-transform: rotate(180deg); 247 | width: 12px; 248 | text-align: center; 249 | } 250 | .pear-nav-tree.arrow .layui-nav-child.layui-nav-hover>dd>a>.layui-nav-more { 251 | display: inline-block !important; 252 | transform: rotate(270deg); 253 | -ms-transform: rotate(270deg); 254 | -moz-transform: rotate(270deg); 255 | -webkit-transform: rotate(270deg); 256 | -o-transform: rotate(270deg); 257 | width: 12px; 258 | text-align: center; 259 | background-color: transparent !important; 260 | } 261 | 262 | .pear-nav-tree.arrow .layui-nav-child.layui-nav-hover>a>.layui-nav-more:before, 263 | .pear-nav-tree.arrow .layui-nav-itemed>a>.layui-nav-more:before { 264 | content: '\e61a'; 265 | display: inline-block; 266 | vertical-align: middle; 267 | } 268 | --------------------------------------------------------------------------------