feat: add country tracking to ban management; update templates and scripts for country display

This commit is contained in:
Théophile Diot 2024-12-09 13:01:15 +01:00
parent 56432f62c4
commit 756daea931
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
9 changed files with 388 additions and 19 deletions

View file

@ -62,7 +62,7 @@ repos:
- id: codespell
name: Codespell Spell Checker
exclude: (^src/(ui/templates|common/core/.+/files|bw/loading)/.+.html|modsecurity-rules.conf.*|src/ui/app/static/(fonts|libs)/.+)$
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py,src/ui/app/static/json/countries.geojson,src/ui/app/static/js/pages/reports.js,src/ui/app/static/json/periscop.min.json,src/ui/app/static/json/blockhaus.min.json,src/ui/app/routes/reports.py
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py,src/ui/app/static/json/countries.geojson,src/ui/app/static/js/pages/bans.js,src/ui/app/static/json/periscop.min.json,src/ui/app/static/json/blockhaus.min.json,src/ui/app/routes/reports.py
language: python
types: [text]

View file

@ -15,6 +15,7 @@ local api = class("api")
local datastore = cdatastore:new()
local logger = clogger:new("API")
local get_country = utils.get_country
local get_variable = utils.get_variable
local is_ip_in_networks = utils.is_ip_in_networks
-- local run = shell.run
@ -248,6 +249,7 @@ api.global.POST["^/ban$"] = function(self)
exp = 86400,
reason = "manual",
service = "unknown",
country = "local",
}
ban.ip = ip["ip"]
if ip["exp"] then
@ -259,12 +261,19 @@ api.global.POST["^/ban$"] = function(self)
if ip["service"] then
ban.service = ip["service"]
end
local country, err = get_country(ban["ip"])
if not country then
country = "unknown"
logger:log(ERR, "can't get country code " .. err)
end
ban.country = country
datastore:set(
"bans_ip_" .. ban["ip"],
encode({
reason = ban["reason"],
service = ban["service"],
date = os.time(),
country = ban["country"],
}),
ban["exp"]
)
@ -301,6 +310,7 @@ api.global.GET["^/bans$"] = function(self)
reason = ban_data["reason"],
service = ban_data["service"],
date = ban_data["date"],
country = ban_data["country"],
exp = math.floor(ttl),
})
end

View file

@ -735,12 +735,13 @@ utils.is_banned = function(ip)
return false, "not banned"
end
utils.add_ban = function(ip, reason, ttl, service)
utils.add_ban = function(ip, reason, ttl, service, country)
-- Set on local datastore
local ban_data = encode({
reason = reason,
service = service or "unknown",
date = os.time(),
country = country or "local",
})
local ok, err = datastore:set("bans_ip_" .. ip, ban_data, ttl)
if not ok then

View file

@ -257,13 +257,16 @@ class CLI(ApiCaller):
cli_str += "No ban found\n"
for ban in bans:
banned_country = ban.get("country", "unknown")
banned_date = ""
remaining = "for eternity"
if ban["date"] != -1:
banned_date = f"the {datetime.fromtimestamp(ban['date']).strftime('%Y-%m-%d at %H:%M:%S %Z')} "
if ban["exp"] != -1:
remaining = f"for {format_remaining_time(ban['exp'])} remaining"
cli_str += f"- {ban['ip']} ; banned {banned_date}{remaining} with reason \"{ban.get('reason', 'no reason given')}\""
cli_str += (
f"- {ban['ip']} from country \"{banned_country}\" ; banned {banned_date}{remaining} with reason \"{ban.get('reason', 'no reason given')}\""
)
if ban.get("service", "unknown") != "unknown":
cli_str += f" by {ban['service'] if ban['service'] != '_' else 'default server'}"

View file

@ -12,6 +12,7 @@ local timer_at = ngx.timer.at
local add_ban = utils.add_ban
local is_whitelisted = utils.is_whitelisted
local is_banned = utils.is_banned
local get_country = utils.get_country
local get_security_mode = utils.get_security_mode
local tostring = tostring
@ -39,6 +40,16 @@ function badbehavior:log()
end
-- Get security mode
local security_mode = get_security_mode(self.ctx)
-- Get country
local country = "local"
local err
if self.ctx.bw.ip_is_global then
country, err = get_country(self.ctx.bw.remote_addr)
if not country then
country = "unknown"
self.logger:log(ERR, "can't get country code " .. err)
end
end
-- Call increase function later and with cosocket enabled
local ok, err = timer_at(
0,
@ -49,7 +60,8 @@ function badbehavior:log()
tonumber(self.variables["BAD_BEHAVIOR_THRESHOLD"]),
self.use_redis,
self.ctx.bw.server_name,
security_mode
security_mode,
country
)
if not ok then
return self:ret(false, "can't create increase timer : " .. err)
@ -67,7 +79,17 @@ function badbehavior:log_stream()
end
-- luacheck: ignore 212
function badbehavior.increase(premature, ip, count_time, ban_time, threshold, use_redis, server_name, security_mode)
function badbehavior.increase(
premature,
ip,
count_time,
ban_time,
threshold,
use_redis,
server_name,
security_mode,
country
)
-- Instantiate objects
local logger = require "bunkerweb.logger":new("badbehavior")
local datastore = require "bunkerweb.datastore":new()
@ -108,7 +130,7 @@ function badbehavior.increase(premature, ip, count_time, ban_time, threshold, us
-- Store local ban
if counter > threshold then
if security_mode == "block" then
ok, err = add_ban(ip, "bad behavior", ban_time, server_name)
ok, err = add_ban(ip, "bad behavior", ban_time, server_name, country)
if not ok then
logger:log(ERR, "(increase) can't save ban : " .. err)
return

View file

@ -2,8 +2,301 @@ $(document).ready(function () {
var actionLock = false;
let addBanNumber = 1;
const banNumber = parseInt($("#bans_number").val());
const dataCountries = ($("#countries").val() || "")
.split(",")
.filter((code) => code && code !== "local");
const baseFlagsUrl = $("#base_flags_url").val().trim();
const isReadOnly = $("#is-read-only").val().trim() === "True";
const countriesDataNames = {
AD: "Andorra",
AE: "United Arab Emirates",
AF: "Afghanistan",
AG: "Antigua and Barbuda",
AI: "Anguilla",
AL: "Albania",
AM: "Armenia",
AO: "Angola",
AQ: "Antarctica",
AR: "Argentina",
AS: "American Samoa",
AT: "Austria",
AU: "Australia",
AW: "Aruba",
AX: "Åland Islands",
AZ: "Azerbaijan",
BA: "Bosnia and Herzegovina",
BB: "Barbados",
BD: "Bangladesh",
BE: "Belgium",
BF: "Burkina Faso",
BG: "Bulgaria",
BH: "Bahrain",
BI: "Burundi",
BJ: "Benin",
BL: "Saint Barthélemy",
BM: "Bermuda",
BN: "Brunei Darussalam",
BO: "Bolivia, Plurinational State of",
BQ: "Caribbean Netherlands",
BR: "Brazil",
BS: "Bahamas",
BT: "Bhutan",
BV: "Bouvet Island",
BW: "Botswana",
BY: "Belarus",
BZ: "Belize",
CA: "Canada",
CC: "Cocos (Keeling) Islands",
CD: "Congo, the Democratic Republic of the",
CF: "Central African Republic",
CG: "Republic of the Congo",
CH: "Switzerland",
CI: "Côte d'Ivoire",
CK: "Cook Islands",
CL: "Chile",
CM: "Cameroon",
CN: "China (People's Republic of China)",
CO: "Colombia",
CR: "Costa Rica",
CU: "Cuba",
CV: "Cape Verde",
CW: "Curaçao",
CX: "Christmas Island",
CY: "Cyprus",
CZ: "Czech Republic",
DE: "Germany",
DJ: "Djibouti",
DK: "Denmark",
DM: "Dominica",
DO: "Dominican Republic",
DZ: "Algeria",
EC: "Ecuador",
EE: "Estonia",
EG: "Egypt",
EH: "Western Sahara",
ER: "Eritrea",
ES: "Spain",
ET: "Ethiopia",
EU: "Europe",
FI: "Finland",
FJ: "Fiji",
FK: "Falkland Islands (Malvinas)",
FM: "Micronesia, Federated States of",
FO: "Faroe Islands",
FR: "France",
GA: "Gabon",
GB: "United Kingdom",
GD: "Grenada",
GE: "Georgia",
GF: "French Guiana",
GG: "Guernsey",
GH: "Ghana",
GI: "Gibraltar",
GL: "Greenland",
GM: "Gambia",
GN: "Guinea",
GP: "Guadeloupe",
GQ: "Equatorial Guinea",
GR: "Greece",
GS: "South Georgia and the South Sandwich Islands",
GT: "Guatemala",
GU: "Guam",
GW: "Guinea-Bissau",
GY: "Guyana",
HK: "Hong Kong",
HM: "Heard Island and McDonald Islands",
HN: "Honduras",
HR: "Croatia",
HT: "Haiti",
HU: "Hungary",
ID: "Indonesia",
IE: "Ireland",
IL: "Israel",
IM: "Isle of Man",
IN: "India",
IO: "British Indian Ocean Territory",
IQ: "Iraq",
IR: "Iran, Islamic Republic of",
IS: "Iceland",
IT: "Italy",
JE: "Jersey",
JM: "Jamaica",
JO: "Jordan",
JP: "Japan",
KE: "Kenya",
KG: "Kyrgyzstan",
KH: "Cambodia",
KI: "Kiribati",
KM: "Comoros",
KN: "Saint Kitts and Nevis",
KP: "Korea, Democratic People's Republic of",
KR: "Korea, Republic of",
KW: "Kuwait",
KY: "Cayman Islands",
KZ: "Kazakhstan",
LA: "Laos (Lao People's Democratic Republic)",
LB: "Lebanon",
LC: "Saint Lucia",
LI: "Liechtenstein",
LK: "Sri Lanka",
LR: "Liberia",
LS: "Lesotho",
LT: "Lithuania",
LU: "Luxembourg",
LV: "Latvia",
LY: "Libya",
MA: "Morocco",
MC: "Monaco",
MD: "Moldova, Republic of",
ME: "Montenegro",
MF: "Saint Martin",
MG: "Madagascar",
MH: "Marshall Islands",
MK: "North Macedonia",
ML: "Mali",
MM: "Myanmar",
MN: "Mongolia",
MO: "Macao",
MP: "Northern Mariana Islands",
MQ: "Martinique",
MR: "Mauritania",
MS: "Montserrat",
MT: "Malta",
MU: "Mauritius",
MV: "Maldives",
MW: "Malawi",
MX: "Mexico",
MY: "Malaysia",
MZ: "Mozambique",
NA: "Namibia",
NC: "New Caledonia",
NE: "Niger",
NF: "Norfolk Island",
NG: "Nigeria",
NI: "Nicaragua",
NL: "Netherlands",
NO: "Norway",
NP: "Nepal",
NR: "Nauru",
NU: "Niue",
NZ: "New Zealand",
OM: "Oman",
PA: "Panama",
PE: "Peru",
PF: "French Polynesia",
PG: "Papua New Guinea",
PH: "Philippines",
PK: "Pakistan",
PL: "Poland",
PM: "Saint Pierre and Miquelon",
PN: "Pitcairn",
PR: "Puerto Rico",
PS: "Palestine",
PT: "Portugal",
PW: "Palau",
PY: "Paraguay",
QA: "Qatar",
RE: "Réunion",
RO: "Romania",
RS: "Serbia",
RU: "Russian Federation",
RW: "Rwanda",
SA: "Saudi Arabia",
SB: "Solomon Islands",
SC: "Seychelles",
SD: "Sudan",
SE: "Sweden",
SG: "Singapore",
SH: "Saint Helena, Ascension and Tristan da Cunha",
SI: "Slovenia",
SJ: "Svalbard and Jan Mayen Islands",
SK: "Slovakia",
SL: "Sierra Leone",
SM: "San Marino",
SN: "Senegal",
SO: "Somalia",
SR: "Suriname",
SS: "South Sudan",
ST: "Sao Tome and Principe",
SV: "El Salvador",
SX: "Sint Maarten (Dutch part)",
SY: "Syrian Arab Republic",
SZ: "Swaziland",
TC: "Turks and Caicos Islands",
TD: "Chad",
TF: "French Southern Territories",
TG: "Togo",
TH: "Thailand",
TJ: "Tajikistan",
TK: "Tokelau",
TL: "Timor-Leste",
TM: "Turkmenistan",
TN: "Tunisia",
TO: "Tonga",
TR: "Turkey",
TT: "Trinidad and Tobago",
TV: "Tuvalu",
TW: "Taiwan (Republic of China)",
TZ: "Tanzania, United Republic of",
UA: "Ukraine",
UG: "Uganda",
UM: "US Minor Outlying Islands",
US: "United States",
UY: "Uruguay",
UZ: "Uzbekistan",
VA: "Holy See (Vatican City State)",
VC: "Saint Vincent and the Grenadines",
VE: "Venezuela, Bolivarian Republic of",
VG: "Virgin Islands, British",
VI: "Virgin Islands, U.S.",
VN: "Vietnam",
VU: "Vanuatu",
WF: "Wallis and Futuna Islands",
WS: "Samoa",
XK: "Kosovo",
YE: "Yemen",
YT: "Mayotte",
ZA: "South Africa",
ZM: "Zambia",
ZW: "Zimbabwe",
};
// Filter countriesDataNames to include only necessary countries
const filteredCountriesDataNames = dataCountries.reduce((obj, code) => {
if (countriesDataNames[code]) {
obj[code] = countriesDataNames[code];
}
return obj;
}, {});
// Assuming baseFlagsUrl, dataCountries, and countriesDataNames are defined
const countriesSearchPanesOptions = [
{
label: `<img src="${baseFlagsUrl}/zz.svg" class="border border-1 p-0 me-1" height="17" />&nbsp;&nbsp;N/A`,
value: (rowData) => rowData[4].includes("N/A"),
},
...Object.entries(filteredCountriesDataNames).map(([code, name]) => ({
label: `<img src="${baseFlagsUrl}/${code.toLowerCase()}.svg" class="border border-1 p-0 me-1" height="17" />&nbsp;&nbsp;${name}`,
value: (rowData) =>
rowData[4].includes(`data-bs-original-title="${code}"`),
})),
];
// Batch update tooltips
const updateCountryTooltips = () => {
$("[data-bs-original-title]").each(function () {
const $elem = $(this);
const countryCode = $elem.attr("data-bs-original-title");
const countryName = countriesDataNames[countryCode];
if (countryName) {
$elem.attr("data-bs-original-title", countryName);
}
});
// Initialize tooltips once
$('[data-bs-toggle="tooltip"]').tooltip();
};
// Utility functions
function addDays(date, days) {
const result = new Date(date);
@ -142,7 +435,7 @@ $(document).ready(function () {
viewTotal: true,
cascadePanes: true,
collapse: false,
columns: [2, 5, 6],
columns: [2, 4, 6, 7],
},
},
topStart: {},
@ -306,7 +599,7 @@ $(document).ready(function () {
},
};
initializeDataTable({
const bans_table = initializeDataTable({
tableSelector: "#bans",
tableName: "bans",
columnVisibilityCondition: (column) => column > 2 && column < 8,
@ -328,7 +621,7 @@ $(document).ready(function () {
targets: -1,
},
{
targets: [2, 6],
targets: [2, 7],
render: function (data, type, row) {
if (type === "display" || type === "filter") {
const date = new Date(data);
@ -381,6 +674,14 @@ $(document).ready(function () {
},
targets: 2,
},
{
searchPanes: {
show: true,
combiner: "or",
options: countriesSearchPanesOptions,
},
targets: 4,
},
{
searchPanes: {
show: true,
@ -388,7 +689,7 @@ $(document).ready(function () {
{
label: "Next 24 hours",
value: function (rowData, rowIdx) {
const date = new Date(rowData[6]);
const date = new Date(rowData[7]);
const now = new Date();
return date - now < 24 * 60 * 60 * 1000;
},
@ -396,7 +697,7 @@ $(document).ready(function () {
{
label: "Next 7 days",
value: function (rowData, rowIdx) {
const date = new Date(rowData[6]);
const date = new Date(rowData[7]);
const now = new Date();
return date - now < 7 * 24 * 60 * 60 * 1000;
},
@ -404,7 +705,7 @@ $(document).ready(function () {
{
label: "Next 30 days",
value: function (rowData, rowIdx) {
const date = new Date(rowData[6]);
const date = new Date(rowData[7]);
const now = new Date();
return date - now < 30 * 24 * 60 * 60 * 1000;
},
@ -412,7 +713,7 @@ $(document).ready(function () {
{
label: "More than 30 days",
value: function (rowData, rowIdx) {
const date = new Date(rowData[6]);
const date = new Date(rowData[7]);
const now = new Date();
return date - now >= 30 * 24 * 60 * 60 * 1000;
},
@ -421,11 +722,11 @@ $(document).ready(function () {
combiner: "or",
orderable: false,
},
targets: 6,
targets: 7,
},
{
searchPanes: { show: true },
targets: 5,
targets: 6,
},
],
order: [[6, "asc"]],
@ -461,10 +762,14 @@ $(document).ready(function () {
)
.attr("data-bs-placement", "right")
.tooltip();
updateCountryTooltips();
},
},
});
// Update tooltips after table draw
bans_table.on("draw.dt", updateCountryTooltips);
$(document).on("click", ".unban-ip", function () {
if (isReadOnly) {
alert("This action is not allowed in read-only mode.");

View file

@ -2,7 +2,9 @@
{% block content %}
<!-- Content -->
<div class="card table-responsive text-nowrap p-4 pb-8 min-vh-70">
{% set base_flags_url = url_for('static', filename='img/flags') %}
<input type="hidden" id="bans_number" value="{{ bans|length }}" />
<input type="hidden" id="base_flags_url" value="{{ base_flags_url }}" />
<textarea type="hidden"
id="columns_preferences_defaults"
class="visually-hidden">{{ columns_preferences_defaults['bans']|tojson }}</textarea>
@ -29,6 +31,9 @@
<th data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="The banned IP address">IP address</th>
<th data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="The banned IP country">Country</th>
<th data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="The reason why the Report was created">Reason</th>
@ -47,12 +52,32 @@
</tr>
</thead>
<tbody>
{% set ns = namespace(countries=[]) %}
{% for ban in bans %}
{% if ban.get("country", "local") and ban.get('country', 'local') not in ns.countries %}
{% set ns.countries = ns.countries + [ban.get('country', 'local')] %}
{% endif %}
<tr>
<td></td>
<td></td>
<td class="ban-start-date">{{ ban["start_date"] }}</td>
<td>{{ ban["ip"] }}</td>
<td>
<div class="d-flex align-items-center"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-original-title="{% if ban.get('country', 'local') in ("local", "unknown") %}N/A{% else %}{{ ban.get('country', 'local') }}{% endif %}">
<img src="{{ base_flags_url }}/{% if ban.get('country', 'local') in ("local", "unknown") %}zz{% else %}{{ ban.get('country', 'local') |lower }}{% endif %}.svg"
class="border border-1 p-0 me-1"
height="17" />
&nbsp;&nbsp;
{% if ban.get('country', 'local') in ("local", "unknown") %}
N/A
{% else %}
{{ ban["country"] }}
{% endif %}
</div>
</td>
<td>{{ ban["reason"] }}</td>
<td>{{ ban["service"] if ban["service"] != "_" else "default server" }}</td>
<td class="ban-end-date">{{ ban["end_date"] }}</td>
@ -74,6 +99,7 @@
</tr>
{% endfor %}
</tbody>
<input type="hidden" id="countries" value="{{ ns.countries|join(',') }}" />
<span class="position-absolute bottom-0 start-50 translate-middle badge rounded-pill bg-secondary">
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
</span>

View file

@ -124,9 +124,7 @@
nonce="{{ script_nonce }}"></script>
</head>
<body>
{% if not starting %}
<input type="hidden" id="home-path" value="{{ url_for('home') }}" />
{% endif %}
{% if not starting %}<input type="hidden" id="home-path" value="{{ url_for('home') }}" />{% endif %}
<input type="hidden" id="is-read-only" value="{{ is_readonly }}" />
<input type="hidden" id="theme" value="{{ theme }}" />
<input type="hidden"
@ -268,6 +266,9 @@
<script src="{{ url_for('static', filename='js/pages/plugin_page.js') }}"
nonce="{{ script_nonce }}"></script>
{% endif %}
<script async defer src="{{ url_for('static', filename='js/buttons.js') }}" nonce="{{ script_nonce }}"></script>
<script async
defer
src="{{ url_for('static', filename='js/buttons.js') }}"
nonce="{{ script_nonce }}"></script>
</body>
</html>

View file

@ -30,6 +30,7 @@ COLUMNS_PREFERENCES_DEFAULTS = {
"5": True,
"6": True,
"7": True,
"8": True,
},
"configs": {
"3": True,