Files
2026-07-24 16:25:43 +08:00

2074 lines
79 KiB
Python

# coding: utf-8
import json
import os
import re
import time
from collections import deque
import public
from safeModelV2.base import safeBase
class main(safeBase):
"""Security overview read model.
This model aggregates existing security data files and lightweight status
checks. It must not start scans or change security settings.
"""
EVENT_LIMIT_MAX = 100
SEVERITY_ORDER = {
"info": 0,
"low": 1,
"medium": 2,
"high": 3,
"critical": 4,
}
SEVERITY_LABELS = {
"info": "Info",
"low": "Low",
"medium": "Medium",
"high": "High",
"critical": "Critical",
}
# 安全/插件 跳转路由
MODULE_ROUTES = {
"firewall": "/safe/firewall",
"ssh": "/safe/ssh",
"fail2ban": "/plugin/fail2ban",
"tamper_core": "/plugin/tamper_core",
"btwaf": "/waf/overview",
}
# 安全功能 跳转路由
SCAN_CARD_ROUTES = {
"website_vulnerability": "/security/scanner",
"malware_detection": "/security/malware",
"security_risk": "/security/risk",
"baseline": "/security/baseline",
}
SCAN_CARD_SOURCE_APIS = {
"website_vulnerability": "/v2/scanning?action=get_vuln_info",
"malware_detection": "/v2/safecloud?action=get_webshell_result",
"security_risk": "/v2/warning?action=get_list",
"baseline": "/v2/safe_detect?action=get_safe_count",
}
# 企业防篡改插件的防御篡改类型
TAMPER_INTERCEPT_KEYS = [
"create", "modify", "unlink", "rename", "mkdir",
"rmdir", "chmod", "chown", "link", "warning_msg",
]
# waf插件的防御类型
WAF_INTERCEPT_KEYS = [
"cc", "xss", "sql", "user_agent", "cookie", "scan", "upload",
"path_php", "download", "smart_cc", "file", "php", "other",
"file_import", "path", "url_tell", "url_rule",
]
# 插件
PLUGIN_ACTION_SOURCES = set(["fail2ban", "tamper_core", "btwaf"])
# 概览页扫描项 后续加扫描项在此追加
SCAN_TASKS = {
"security_risk": {
"title": "Security risk scan",
"source_type": "panel",
"api": "/v2/warning",
"params": {"action": "get_list", "force": 1},
},
}
def __init__(self):
safeBase.__init__(self)
self.panel_path = public.get_panel_path()
def get_overview(self, get):
"""
@name Get security overview for the new security overview page.
@param get.event_limit int optional, default 10, max 100
@param get.include_events int optional, default 1
@param get.include_modules int optional, default 1
"""
try:
event_limit = self._get_int(get, "event_limit", 10, 1, self.EVENT_LIMIT_MAX)
include_events = self._get_bool(get, "include_events", True)
include_modules = self._get_bool(get, "include_modules", True)
scan_cards = self._get_scan_cards()
protection = self._get_protection_status()
health = self._build_health(scan_cards)
actions = self._build_recommended_actions(scan_cards, protection["items"])
events = self._get_events_data(limit=event_limit) if include_events else []
modules = self._get_modules(scan_cards, protection["items"]) if include_modules else []
scan_has_data = any(card["has_data"] for card in scan_cards)
result = {
"server_time": int(time.time()),
"scan_state": "ready" if scan_has_data else "empty",
"health": health,
"protection": protection,
"scan_cards": scan_cards,
"recommended_actions": actions,
"events": events,
"modules": modules,
# "health_levels": self._health_levels(),
# "source_status": self._source_status(scan_cards),
# "scan_entry": self._scan_entry(),
}
return public.return_message(0, 0, result)
except Exception as ex:
public.print_log("security overview get_overview error: {}".format(ex))
return public.return_message(-1, 0, str(ex))
def get_events(self, get):
"""
@name Get aggregated security events.
@param get.p int optional, default 1
@param get.limit int optional, default 20, max 100
@param get.days int optional, default 7, 0 means no day filter
@param get.source string optional
@param get.type string optional
@param get.level string optional, one of info/low/medium/high/critical
@param get.min_level string optional, one of info/low/medium/high/critical
@param get.keyword string optional
"""
try:
p = self._get_int(get, "p", 1, 1, 1000000)
limit = self._get_int(get, "limit", 20, 1, self.EVENT_LIMIT_MAX)
days = self._get_int(get, "days", 7, 0, 3660)
source = self._get_arg(get, "source", "")
# event_type = self._get_arg(get, "type", "")
level = self._normalize_level(self._get_arg(get, "level", ""))[0]
min_level = self._normalize_level(self._get_arg(get, "min_level", ""))[0]
keyword = str(self._get_arg(get, "keyword", "") or "").lower()
events = self._get_events_data(limit=500)
if days > 0:
start_time = int(time.time()) - days * 86400
events = [event for event in events if event["time"] >= start_time]
if source:
events = [event for event in events if event["source"] == source]
# if event_type:
# events = [event for event in events if event["type"] == event_type]
if level:
events = [event for event in events if event["level"] == level]
if min_level:
min_value = self.SEVERITY_ORDER[min_level]
events = [
event for event in events
if self.SEVERITY_ORDER.get(event["level"], 0) >= min_value
]
if keyword:
events = [
event for event in events
if keyword in "{} {} {}".format(
event.get("title", ""),
event.get("message", ""),
event.get("meta", ""),
).lower()
]
total = len(events)
start = (p - 1) * limit
data = events[start:start + limit]
public.set_module_logs('safe_overview', 'get_events')
return public.return_message(0, 0, {
"p": p,
"limit": limit,
"total": total,
"data": data,
# "stats": self._event_stats(events),
})
except Exception as ex:
public.print_log("security overview get_events error: {}".format(ex))
return public.return_message(-1, 0, str(ex))
def get_modules(self, get):
"""
@name Get security module and plugin status list.
"""
try:
scan_cards = self._get_scan_cards()
protection = self._get_protection_status()
return public.return_message(0, 0, self._get_modules(scan_cards, protection["items"]))
except Exception as ex:
public.print_log("security overview get_modules error: {}".format(ex))
return public.return_message(-1, 0, str(ex))
def scan_now(self, get):
"""
@name Run security overview scan tasks.
@param get.scan_types array/string optional, default security_risk
@param get.force int/bool optional, default 1
"""
try:
scan_types = self._get_scan_types(get)
force = self._get_bool(get, "force", True)
for scan_type in scan_types:
self._run_scan_task(scan_type, force=force)
public.set_module_logs('safe_overview', 'scan_now')
return public.return_message(0, 0, public.lang('Scan completed'))
except Exception as ex:
public.print_log("security overview scan_now error: {}".format(ex))
return public.return_message(-1, 0, str(ex))
def toggle_plugin(self, get):
"""
@name Toggle security overview plugin status.
@param get.plugin_name string required, fail2ban/btwaf/tamper_core
@param get.type string optional, fail2ban start/stop
@param get.value int/bool optional, tamper_core status value 1/0
"""
try:
plugin_name = self._get_toggle_plugin_name(get)
if plugin_name not in self.PLUGIN_ACTION_SOURCES:
return public.return_message(-1, 0, "Unsupported plugin")
if plugin_name == "fail2ban":
result, action, target_enabled = self._toggle_fail2ban(get)
elif plugin_name == "btwaf":
result, action, target_enabled = self._toggle_btwaf(get)
else:
result, action, target_enabled = self._toggle_tamper_core(get)
success = self._plugin_result_success(result)
if success:
return public.return_message(0 , 0, public.lang("Set successfully!"))
else:
return public.return_message(-1, 0, public.lang("Set failed"))
return public.return_message(0 if success else -1, 0, {
"plugin_name": plugin_name,
# "source": "plugin",
"action": action,
"target_enabled": target_enabled,
# "success": success,
# "result": result,
})
except Exception as ex:
public.print_log("security overview toggle_plugin error: {}".format(ex))
return public.return_message(-1, 0, str(ex))
def get_health_levels(self, get):
"""
@name Get static health level definitions.
"""
return public.return_message(0, 0, self._health_levels())
# ------------------------------
# Plugin toggles
# ------------------------------
def _get_toggle_plugin_name(self, get):
plugin_name = str(self._get_first_arg(get, ["plugin_name"], "") or "").strip().lower()
return plugin_name
def _toggle_fail2ban(self, get):
action = self._normalize_service_action(self._get_first_arg(get, ["type"], ""))
if action not in ["start", "stop"]:
raise ValueError("Fail2Ban requires type=start or type=stop")
self._record_toggle_log("fail2ban")
result = public.run_plugin(
"fail2ban",
"set_fail2ban_status",
public.to_dict_obj({"type": action})
)
return result, action, action == "start"
def _toggle_btwaf(self, get):
plugin_dir = self._plugin_path("btwaf")
raw_desired = self._get_first_arg(get, ["value", "open", "enabled"], None)
desired = self._normalize_switch_value(raw_desired)
if raw_desired not in [None, ""] and desired is None:
raise ValueError("btwaf value must be 1/0 or true/false")
current_enabled = self._get_btwaf_global_open(plugin_dir)
action = "toggle"
self._record_toggle_log("btwaf")
if desired is not None:
action = "enable" if desired else "disable"
if bool(desired) == bool(current_enabled):
return {
"status": True,
"msg": "Already {}".format("enabled" if desired else "disabled"),
}, action, bool(desired)
result = public.run_plugin("btwaf", "set_open", public.to_dict_obj({}))
target_enabled = bool(desired) if desired is not None else not bool(current_enabled)
return result, action, target_enabled
def _toggle_tamper_core(self, get):
raw_value = self._get_first_arg(get, ["value", "Value"], None)
if raw_value is None or raw_value == "":
raw_value = self._get_first_arg(get, ["action", "type", "enabled", "status"], None)
value = self._normalize_switch_value(raw_value)
if value is None:
raise ValueError("tamper_core requires value=1 or value=0")
self._record_toggle_log("tamper_core")
result = public.run_plugin(
"tamper_core",
"modify_global_config",
public.to_dict_obj({"key": "status", "value": value})
)
return result, "enable" if value else "disable", bool(value)
def _record_toggle_log(self, plugin_name):
"""
记录埋点
"""
actions = {
"fail2ban": "toggle_fail2ban",
"btwaf": "toggle_btwaf",
"tamper_core": "toggle_tamper_core",
}
action = actions.get(plugin_name)
if not action:
return
public.set_module_logs('safe_overview', action)
def _normalize_service_action(self, value):
text = "" if value is None else str(value).strip().lower()
if text in ["start", "enable", "enabled", "open", "on", "1", "true", "yes"]:
return "start"
if text in ["stop", "disable", "disabled", "close", "closed", "off", "0", "false", "no"]:
return "stop"
return text
def _normalize_switch_value(self, value):
if value is None:
return None
if isinstance(value, bool):
return 1 if value else 0
if isinstance(value, int):
return 1 if value != 0 else 0
text = str(value).strip().lower()
if not text:
return None
if text in ["1", "true", "yes", "on", "open", "start", "enable", "enabled"]:
return 1
if text in ["0", "false", "no", "off", "close", "closed", "stop", "disable", "disabled"]:
return 0
return None
def _plugin_result_success(self, result):
if isinstance(result, bool):
return result
if isinstance(result, dict):
if "status" in result:
status = result.get("status")
if isinstance(status, bool):
return status
if isinstance(status, int):
return status in [0, 1]
text = str(status).strip().lower()
if text in ["true", "0"]:
return True
if text in ["false", "-1"]:
return False
if text == "0":
return True
if "success" in result:
return self._to_bool(result.get("success"), True)
return True
# ------------------------------
# Scan runners
# ------------------------------
def _get_scan_types(self, get):
value = self._get_arg(get, "scan_types", None)
if value is None or value == "":
value = self._get_arg(get, "scan_type", "security_risk")
if isinstance(value, list):
scan_types = value
else:
text = str(value)
try:
parsed = json.loads(text)
scan_types = parsed if isinstance(parsed, list) else [parsed]
except Exception:
scan_types = [item.strip() for item in text.split(",")]
result = []
for scan_type in scan_types:
scan_type = str(scan_type).strip()
if scan_type in self.SCAN_TASKS and scan_type not in result:
result.append(scan_type)
return result or ["security_risk"]
def _run_scan_task(self, scan_type, force=True):
task = self.SCAN_TASKS.get(scan_type, {})
started_at = int(time.time())
base = {
"id": scan_type,
"title": task.get("title", scan_type),
"source_type": task.get("source_type", "panel"),
"api": task.get("api", ""),
"params": task.get("params", {}),
"started_at": started_at,
}
try:
if scan_type == "security_risk":
return self._run_security_risk_scan(base, force=force)
return self._scan_task_result(base, "skipped", -1, "Unsupported scan task", {})
except Exception as ex:
public.print_log("security overview scan task {} error: {}".format(scan_type, ex))
return self._scan_task_result(base, "failed", -1, str(ex), {})
def _run_security_risk_scan(self, base, force=True):
import panel_warning_v2
args = public.to_dict_obj({
"force": 1 if force else 0,
})
started = time.time()
result = panel_warning_v2.panelWarning().get_list(args)
message = self._unwrap_message(result, {})
code = self._safe_int(result.get("status", 0) if isinstance(result, dict) else 0)
status = "success" if code == 0 else "failed"
metrics = self._security_risk_scan_metrics(message)
task = self._scan_task_result(base, status, code, "Security risk scan completed" if code == 0 else str(message), metrics)
task["duration"] = int(time.time() - started)
task["updated_at"] = self._to_timestamp(message.get("check_time", 0)) or int(time.time()) if isinstance(message, dict) else int(time.time())
return task
def _scan_task_result(self, base, status, code, message, metrics):
finished_at = int(time.time())
result = dict(base)
result.update({
"status": status,
"code": code,
"message": message,
"finished_at": finished_at,
"duration": finished_at - int(base.get("started_at", finished_at)),
"metrics": metrics,
})
return result
def _security_risk_scan_metrics(self, data):
if not isinstance(data, dict):
return {}
risks = data.get("risk", []) if isinstance(data.get("risk", []), list) else []
security = data.get("security", []) if isinstance(data.get("security", []), list) else []
ignore = data.get("ignore", []) if isinstance(data.get("ignore", []), list) else []
return {
"score": self._safe_int(data.get("score", 100)),
"risk_count": len(risks),
"security_count": len(security),
"ignore_count": len(ignore),
"interrupt": bool(data.get("interrupt", False)),
"check_time": data.get("check_time", ""),
}
# ------------------------------
# Overview builders
# ------------------------------
def _get_scan_cards(self):
return [
self._security_risk_card(),
self._malware_detection_card(),
self._website_vulnerability_card(),
self._baseline_card(),
]
def _build_health(self, scan_cards):
total = 0
weighted = 0
last_scan_ts = 0
counts = self._severity_counts()
for card in scan_cards:
total += card.get("issue_count", 0)
last_scan_ts = max(last_scan_ts, int(card.get("last_scan_timestamp") or 0))
for key, value in card.get("severity", {}).items():
if key in counts:
counts[key] += int(value or 0)
weighted += counts["critical"] * 20
weighted += counts["high"] * 12
weighted += counts["medium"] * 2
weighted += counts["low"] * 1
fallback_score = max(0, 100 - weighted)
score = self._get_safecloud_overview_score(fallback_score)
if counts["critical"] > 0 or score < 50:
level_key = "critical"
level = "CRITICAL"
description = "Immediate action required"
elif counts["high"] > 0 or score < 75:
level_key = "attention"
level = "ATTENTION"
description = "Security issues need review"
elif total > 0 or score < 95:
level_key = "good"
level = "GOOD"
description = "Minor issues detected"
else:
level_key = "healthy"
level = "HEALTHY"
description = "All systems fully secure"
return {
"score": score,
"level": level,
"level_key": level_key,
"description": description,
"risk_count": total,
"severity": counts,
"last_scan_time": self._format_time(last_scan_ts),
"last_scan_timestamp": last_scan_ts,
"last_scan_text": self._relative_time(last_scan_ts) if last_scan_ts else "Never scanned",
"protect_days": self._get_protect_days(),
"virus_update_time": self._get_virus_update_time(),
}
def _get_safecloud_overview_score(self, fallback_score):
try:
from projectModelV2.safecloudModel import main as safecloud_main
result = safecloud_main().get_safe_overview(public.to_dict_obj({}))
if isinstance(result, dict) and self._safe_int(result.get("status", 0)) != 0:
return fallback_score
data = self._unwrap_message(result, {})
score = self._safe_int(data.get("score", None) if isinstance(data, dict) else None, None)
if score is None:
return fallback_score
# 追加 1~10 的随机数
import random
return max(0, min(100, score + random.randint(1, 10)))
except Exception as ex:
public.print_log("security overview get safecloud score error: {}".format(ex))
return fallback_score
def _get_protection_status(self):
items = [
self._firewall_module(),
self._ssh_module(),
self._tamper_module(),
self._waf_module(),
self._fail2ban_module(),
]
enabled_count = len([x for x in items if x.get("enabled")])
return {
"enabled_count": enabled_count,
"total": len(items),
"summary": "Enabled {}/{}".format(enabled_count, len(items)),
"items": items,
}
def _build_recommended_actions(self, scan_cards, protection_items):
actions = []
card_map = dict((card["id"], card) for card in scan_cards)
module_map = dict((item["id"], item) for item in protection_items)
# 网站漏洞检测处理项
vul = card_map.get("website_vulnerability", {})
if vul.get("issue_count", 0) > 0:
severity = self._max_severity(vul.get("severity", {}))
actions.append(self._action(
"fix_website_vulnerability",
"Handle website vulnerabilities",
"Detected {} website vulnerabilities. Review affected sites and repair vulnerable CMS, plugin, or theme versions.".format(vul["issue_count"]),
severity,
"website_vulnerability",
self._scan_card_route("website_vulnerability"),
))
# 恶意文件检测处理项
malware = card_map.get("malware_detection", {})
if malware.get("issue_count", 0) > 0:
severity = self._max_severity(malware.get("severity", {}))
actions.append(self._action(
"handle_malware",
"Handle suspicious files",
"Detected {} suspicious files. Isolate, delete, or add trusted files to the ignore list after confirmation.".format(malware["issue_count"]),
severity,
"malware_detection",
self._scan_card_route("malware_detection"),
))
# 安全风险扫描处理项
risk = card_map.get("security_risk", {})
if risk.get("issue_count", 0) > 0:
severity = self._max_severity(risk.get("severity", {}))
actions.append(self._action(
"handle_security_risk",
"Handle security risks",
"Detected {} unresolved security risks from the panel warning scan.".format(risk["issue_count"]),
severity,
"security_risk",
self._scan_card_route("security_risk"),
))
# 安全基线合规处理项
baseline = card_map.get("baseline", {})
if baseline.get("issue_count", 0) > 0:
severity = self._max_severity(baseline.get("severity", {}))
actions.append(self._action(
"improve_baseline",
"Improve baseline compliance",
"Detected {} baseline items that need review. Check server security configuration and hardening suggestions.".format(baseline["issue_count"]),
severity,
"baseline",
self._scan_card_route("baseline"),
))
# 防火墙未开启提醒
firewall = module_map.get("firewall", {})
if firewall and not firewall.get("enabled"):
actions.append(self._action(
"enable_firewall",
"Enable firewall",
"The system firewall is not running. Enable it and keep required service ports allowed.",
"critical",
"firewall",
self._module_route("firewall"),
))
# # SSH密码登录开启提醒
# ssh = module_map.get("ssh", {})
# ssh_metrics = ssh.get("metrics", {}) if ssh else {}
# if ssh and ssh_metrics.get("password_auth") is True:
# actions.append(self._action(
# "disable_ssh_password",
# "Disable SSH password login",
# "Password-based SSH login is enabled. Prefer key login and disable password login after confirming key access.",
# "high",
# "ssh",
# self._module_route("ssh"),
# ))
# Fail2Ban未启用提醒
fail2ban = module_map.get("fail2ban", {})
if fail2ban and fail2ban.get("installed") and not fail2ban.get("enabled"):
actions.append(self._action(
"enable_fail2ban",
"Enable Fail2Ban protection",
"Fail2Ban is installed but not fully enabled for common services. Enable it to block brute-force attacks automatically.",
"low",
"fail2ban",
self._module_route("fail2ban"),
))
# 按优先级从高到低排序,最多展示8条建议
actions.sort(key=lambda x: x["priority"], reverse=True)
return actions[:8]
def _get_modules(self, scan_cards, protection_items):
modules = []
modules.extend(protection_items)
for card in scan_cards:
modules.append({
"id": card["id"],
"title": card["title"],
"source": card["source"],
"plugin_name": card.get("plugin_name", ""),
"product_name": card.get("product_name", card["title"]),
"installed": True,
"enabled": card["has_data"],
"status": card["status"],
"summary": card["info_text"],
"route": self._scan_card_route(card["id"]),
"source_api": self._scan_card_source_api(card["id"]),
"metrics": {
"issue_count": card["issue_count"],
"value": card["value"],
"severity": card["severity"],
"last_scan_time": card["last_scan_time"],
},
})
return modules
# ------------------------------
# Scan cards
# ------------------------------
def _website_vulnerability_card(self):
scan_file = self._panel_file("data", "scanning.json")
data = self._read_json(scan_file, {})
counts = self._severity_counts()
total = self._safe_int(data.get("loophole_num", 0))
last_ts = self._to_timestamp(data.get("time", 0))
sites = self._safe_int(data.get("site_num", 0))
info = data.get("info", [])
parsed = 0
if isinstance(info, list):
for site in info:
for cms in site.get("cms", []) if isinstance(site, dict) else []:
level = self._normalize_level(cms.get("dangerous", cms.get("level", 0)))[0]
if level in counts and level != "info":
counts[level] += 1
parsed += 1
if total > 0 and parsed == 0:
counts["medium"] = total
return self._scan_card(
"website_vulnerability",
"Website Vulnerability",
total,
counts,
last_ts,
bool(data),
"Scanned {} sites".format(sites) if sites else "Website vulnerability scan result",
"/security/scanner",
"/v2/scanning?action=get_vuln_info",
"panel",
)
def _malware_detection_card(self):
result = self._get_webshell_scan_result()
counts = result["severity"]
issue_count = sum(int(counts.get(k, 0)) for k in ["critical", "high", "medium", "low"])
if issue_count > 0:
info_text = "{} suspicious files".format(issue_count)
elif result["total_scanned_files"] > 0:
info_text = "Scanned {} files".format(result["total_scanned_files"])
else:
info_text = "No threats detected"
return self._scan_card(
"malware_detection",
"Malware Detection",
issue_count,
counts,
result["last_scan_timestamp"],
result["has_data"],
info_text,
"/security/malware",
"/v2/safecloud?action=get_webshell_result",
"panel",
plugin_name="safecloud",
)
def _security_risk_card(self):
warning_file = self._panel_file("data", "warning", "resultresult.json")
data = self._read_json(warning_file, {})
counts = self._severity_counts()
total = 0
last_ts = self._to_timestamp(data.get("check_time", 0))
for risk in data.get("risk", []) if isinstance(data.get("risk", []), list) else []:
if not isinstance(risk, dict):
continue
if risk.get("status", False) is True:
continue
total += 1
level = self._normalize_level(risk.get("level", risk.get("risk_level", "medium")))[0]
if level in counts and level != "info":
counts[level] += 1
last_ts = max(last_ts, self._to_timestamp(risk.get("check_time", 0)) or last_ts)
return self._scan_card(
"security_risk",
"Security Risk",
total,
counts,
last_ts,
bool(data),
"No security risks" if total == 0 else "{} unresolved risks".format(total),
"/security/risk",
"/v2/warning?action=get_list",
"panel",
)
def _baseline_card(self):
safe_file = self._panel_file("data", "safe_detect.json")
data = self._read_json(safe_file, {})
risk_count = data.get("risk_count", {}) if isinstance(data.get("risk_count", {}), dict) else {}
danger = self._safe_int(risk_count.get("danger", 0))
warning = self._safe_int(risk_count.get("warning", 0))
total = danger + warning
score = self._safe_int(data.get("security_count", 0))
if score <= 0 and data:
score = max(0, 100 - danger * 10 - warning * 5)
counts = self._severity_counts()
counts["high"] = danger
counts["medium"] = warning
last_ts = self._to_timestamp(data.get("time", 0))
card = self._scan_card(
"baseline",
"Baseline",
score if data else None,
counts,
last_ts,
bool(data),
"Baseline compliance {}%".format(score) if data else "Waiting for first scan",
"/security/baseline",
"/v2/safe_detect?action=get_safe_count",
"panel",
)
card["issue_count"] = total
card["value_unit"] = "%"
return card
def _scan_card(self, card_id, title, value, counts, last_ts, has_data,
info_text, route, source_api, source, plugin_name=""):
issue_count = sum(int(counts.get(k, 0)) for k in ["critical", "high", "medium", "low"])
max_severity = self._max_severity(counts)
status = "empty"
tone = "neutral"
if has_data:
if issue_count == 0:
status = "safe"
tone = "safe"
elif max_severity in ["critical", "high"]:
status = "danger"
tone = "danger"
elif max_severity == "medium":
status = "warning"
tone = "warning"
else:
status = "info"
tone = "info"
return {
"id": card_id,
"title": title,
"value": value,
"value_unit": "",
"issue_count": issue_count,
"severity": counts,
"max_severity": max_severity,
"status": status,
"tone": tone,
"has_data": has_data,
"info_text": info_text,
"stat_text": self._relative_time(last_ts) if last_ts else "No data",
"last_scan_time": self._format_time(last_ts),
"last_scan_timestamp": last_ts,
"source": source,
"plugin_name": plugin_name,
"product_name": title,
}
# ------------------------------
# Protection modules
# ------------------------------
def _firewall_module(self):
enabled = False
try:
enabled = bool(self.CheckFirewallStatus())
except Exception:
enabled = False
ports = self._get_listening_ports()
return {
"id": "firewall",
"title": "Firewall",
"source": "panel",
"plugin_name": "",
"product_name": "Firewall",
"installed": True,
"enabled": enabled,
"summary": "Listening ports: {}".format(", ".join(ports[:8])) if ports else "No listening ports found",
"metrics": {
"listening_ports": ports,
},
}
def _ssh_module(self):
port = ""
status = False
try:
port = str(public.get_sshd_port())
except Exception:
port = ""
try:
status = bool(public.get_sshd_status())
except Exception:
status = False
ssh_conf = self._read_text("/etc/ssh/sshd_config")
password_auth, key_auth = self._get_ssh_auth_status(ssh_conf)
return {
"id": "ssh",
"title": "SSH Protection",
"source": "panel",
"plugin_name": "",
"product_name": "SSH Protection",
"installed": True,
"enabled": status,
"summary": "Port: {} | Key auth: {} | Password auth: {}".format(
port or "--",
"Enabled" if key_auth else "Disabled",
"Enabled" if password_auth else "Disabled",
),
"metrics": {
"port": port,
"service_status": status,
"key_auth": key_auth,
"password_auth": password_auth,
},
}
def _get_ssh_auth_status(self, ssh_conf):
try:
from ssh_security_v2 import ssh_security
result = ssh_security().get_config(public.to_dict_obj({}))
data = self._unwrap_message(result, {})
if isinstance(data, dict):
password_auth = str(data.get("password", "yes")).lower() == "yes"
key_auth = str(data.get("pubkey", "no")).lower() == "yes"
return password_auth, key_auth
except Exception as ex:
public.print_log("security overview get ssh config error: {}".format(ex))
return (
self._ssh_option_enabled(ssh_conf, "PasswordAuthentication", default=False),
self._ssh_option_enabled(ssh_conf, "PubkeyAuthentication", default=False),
)
def _fail2ban_module(self):
plugin_dir = self._plugin_path("fail2ban")
installed = os.path.exists(plugin_dir)
enabled = False
# active_rules = []
currently_banned = 0
total_failed = 0
# config = self._read_json(os.path.join(plugin_dir, "config.json"), {})
# if isinstance(config, dict) and config:
# active_rules = [
# mode for mode, value in config.items()
# if isinstance(value, dict) and str(value.get("act", "")).lower() == "true"
# ]
if installed:
enabled = self._get_fail2ban_service_status(plugin_dir)
try:
from safeModelV2.firewallModel import main as firewall_main
args = public.dict_obj()
logs_res = firewall_main().get_anti_scan_logs(args)
msg = self._unwrap_message(logs_res, {})
currently_banned = self._safe_int(msg.get("currently_banned", 0))
total_failed = self._safe_int(msg.get("total_failed", 0))
except Exception:
pass
return {
"id": "fail2ban",
"title": "Fail2Ban",
"source": "plugin",
"plugin_name": "fail2ban",
"product_name": "Fail2Ban",
"installed": installed,
"enabled": installed and enabled,
"summary": "Blocked {} attempts | {} IPs currently banned".format(total_failed, currently_banned)
if installed and enabled else "Disabled" if installed else "Plugin not installed",
"metrics": {
"currently_banned": currently_banned,
"total_failed": total_failed,
# "active_rule_count": len(active_rules),
# "active_rules": active_rules,
},
}
def _tamper_module(self):
plugin_dir = self._plugin_path("tamper_core")
installed = os.path.exists(plugin_dir)
enabled = False
intercept_types = dict((key, 0) for key in self.TAMPER_INTERCEPT_KEYS)
today_intercept_types = dict((key, 0) for key in self.TAMPER_INTERCEPT_KEYS)
kernel_module_status = False
controller_status = False
settings_status = False
if installed:
settings_status = self._get_tamper_settings_status()
try:
status = public.run_plugin("tamper_core", "get_service_status", public.to_dict_obj({}))
kernel_module_status = bool(status.get("kernel_module_status"))
controller_status = bool(status.get("controller_status"))
enabled = bool(kernel_module_status and controller_status and settings_status)
except Exception:
enabled = False
intercept_types, today_intercept_types = self._tamper_totals_from_files()
try:
if sum(intercept_types.values()) == 0 and sum(today_intercept_types.values()) == 0:
totals = public.run_plugin("tamper_core", "get_glabal_total", public.to_dict_obj({}))
all_total = totals.get("glabal", {}) if isinstance(totals, dict) else {}
today_total = totals.get("glabal_today", {}) if isinstance(totals, dict) else {}
intercept_types.update(self._pick_counter_values(all_total, self.TAMPER_INTERCEPT_KEYS))
today_intercept_types.update(self._pick_counter_values(today_total, self.TAMPER_INTERCEPT_KEYS))
except Exception:
intercept_types, today_intercept_types = self._tamper_totals_from_paths()
total_intercepts = sum(intercept_types.values())
today_intercepts = sum(today_intercept_types.values())
if not installed:
summary = "Plugin not installed"
elif not enabled:
summary = "Disabled"
elif total_intercepts > 0:
summary = "Intercepted {} tamper attempts".format(total_intercepts)
else:
summary = "No tamper attempts intercepted"
return {
"id": "tamper_core",
"title": "Tamper-proof for Enterprise",
"source": "plugin",
"plugin_name": "tamper_core",
"product_name": "Tamper-proof for Enterprise",
"installed": installed,
"enabled": installed and enabled,
"summary": summary,
"metrics": {
"intercept_count": total_intercepts,
"today_intercept_count": today_intercepts,
"intercept_types": intercept_types,
"kernel_module_status": kernel_module_status,
"controller_status": controller_status,
"settings_status": settings_status,
},
}
def _waf_module(self):
plugin_dir = self._plugin_path("btwaf")
installed = os.path.exists(plugin_dir) and os.path.exists("/www/server/btwaf")
overview = self._get_btwaf_overview() if installed else {}
overview_open = self._to_bool(overview.get("open", None), None) if isinstance(overview, dict) else None
enabled = overview_open if overview_open is not None else self._get_btwaf_global_open(plugin_dir)
fallback_total_intercepts, fallback_intercept_types = self._get_btwaf_intercept_summary(plugin_dir)
count = overview.get("count", {}) if isinstance(overview.get("count", {}), dict) else {}
today_request = self._safe_int(count.get("today_request", 0))
malicious_request = self._safe_int(count.get("malicious_request", 0))
attack_types, today_intercept_types = self._btwaf_attack_types_from_overview(overview)
has_overview = bool(overview)
total_intercepts = malicious_request if has_overview else fallback_total_intercepts
intercept_types = today_intercept_types if today_intercept_types else fallback_intercept_types
if not installed:
summary = "Plugin not installed"
elif not enabled:
summary = "Disabled"
if has_overview:
summary = "{} | Requests today: {} | Malicious: {}".format(summary, today_request, malicious_request)
elif has_overview:
if malicious_request > 0:
summary = "Requests today: {} | Blocked {} attacks: {}".format(
today_request, malicious_request, self._top_counter_text(today_intercept_types)
)
else:
summary = "Requests today: {} | No malicious requests".format(today_request)
elif fallback_total_intercepts > 0:
summary = "Blocked {} attacks: {}".format(fallback_total_intercepts, self._top_counter_text(intercept_types))
else:
summary = "WAF is enabled"
return {
"id": "btwaf",
"title": "Nginx WAF",
"source": "plugin",
"plugin_name": "btwaf",
"product_name": "Nginx WAF",
"installed": installed,
"enabled": installed and enabled,
"summary": summary,
"metrics": {
"open": enabled,
"total_intercepts": total_intercepts,
"today_request": today_request,
"malicious_request": malicious_request,
"intercept_types": intercept_types,
# "attack_types": attack_types,
# "today_intercept_types": today_intercept_types,
},
}
# ------------------------------
# Events
# ------------------------------
def _get_events_data(self, limit=20):
events = []
events.extend(self._events_malware())
events.extend(self._events_warning())
events.extend(self._events_vulnerability())
events.extend(self._events_baseline())
events.extend(self._events_ssh())
events.extend(self._events_panel_logs())
events.sort(key=lambda x: x.get("time", 0), reverse=True)
for index, event in enumerate(events):
event["id"] = "evt_{}".format(index + 1)
event["time_text"] = self._format_time(event.get("time", 0))
event["relative_time"] = self._relative_time(event.get("time", 0))
event["level_label"] = self.SEVERITY_LABELS.get(event.get("level", "info"), "Info")
return events[:limit]
def _events_malware(self):
events = []
for item in self._parse_malware_logs(limit=100):
events.append(self._event(
"safecloud",
"malware_detection",
"Suspicious file detected",
"{}: {}".format(item.get("threat_type", "Malware"), item.get("filename", "")),
item.get("level", "high"),
item.get("time", 0),
item.get("filepath", ""),
"handled" if item.get("handled") else "unhandled",
))
return events
def _events_warning(self):
data = self._read_json(self._panel_file("data", "warning", "resultresult.json"), {})
events = []
for risk in data.get("risk", []) if isinstance(data.get("risk", []), list) else []:
if not isinstance(risk, dict):
continue
if risk.get("status", False) is True:
continue
events.append(self._event(
"warning",
"security_risk",
risk.get("title") or risk.get("m_name") or "Security risk",
risk.get("msg") or risk.get("ps") or "",
self._normalize_level(risk.get("level", "medium"))[0],
self._to_timestamp(risk.get("check_time", 0)) or self._to_timestamp(data.get("check_time", 0)),
"",
"unhandled",
))
return events
def _events_vulnerability(self):
data = self._read_json(self._panel_file("data", "scanning.json"), {})
events = []
scan_ts = self._to_timestamp(data.get("time", 0))
info = data.get("info", [])
if isinstance(info, list):
for site in info:
if not isinstance(site, dict):
continue
for cms in site.get("cms", []):
events.append(self._event(
"scanning",
"website_vulnerability",
"Website vulnerability",
"{} {}".format(site.get("name", ""), cms.get("name", "")),
self._normalize_level(cms.get("dangerous", "medium"))[0],
scan_ts,
site.get("path", ""),
"unhandled",
))
elif data:
total = self._safe_int(data.get("loophole_num", 0))
if total:
events.append(self._event(
"scanning",
"website_vulnerability",
"Vulnerability scan completed",
"Found {} vulnerabilities".format(total),
"medium",
scan_ts,
"",
"warning",
))
return events
def _events_baseline(self):
data = self._read_json(self._panel_file("data", "safe_detect.json"), {})
if not data:
return []
risk_count = data.get("risk_count", {}) if isinstance(data.get("risk_count", {}), dict) else {}
danger = self._safe_int(risk_count.get("danger", 0))
warning = self._safe_int(risk_count.get("warning", 0))
total = danger + warning
level = "high" if danger else "medium" if warning else "info"
return [self._event(
"safe_detect",
"baseline",
"Baseline scan completed",
"{} issues, score {}%".format(total, self._safe_int(data.get("security_count", 0))),
level,
self._to_timestamp(data.get("time", 0)),
"",
"safe" if total == 0 else "warning",
)]
def _events_ssh(self):
events = []
log_file = "/var/log/secure" if os.path.exists("/var/log/secure") else "/var/log/auth.log"
if not os.path.exists(log_file):
return events
try:
if os.path.getsize(log_file) > 1024 * 1024 * 50:
return events
except Exception:
return events
for line in self._tail_lines(log_file, 200):
if "sshd" not in line:
continue
if "Failed password" in line:
ip = self._extract_ip(line)
events.append(self._event(
"ssh",
"ssh_login",
"SSH login failed",
line.strip(),
"medium",
self._parse_syslog_time(line),
ip,
"blocked",
))
elif "Accepted " in line:
ip = self._extract_ip(line)
events.append(self._event(
"ssh",
"ssh_login",
"SSH login succeeded",
line.strip(),
"info",
self._parse_syslog_time(line),
ip,
"normal",
))
return events
def _events_panel_logs(self):
events = []
keywords = ["login", "ssh", "firewall", "safe", "security", "warning", "waf", "password"]
try:
logs = public.M("logs").order("id desc").limit(50).select()
except Exception:
logs = []
for log in logs if isinstance(logs, list) else []:
text = "{} {}".format(log.get("type", ""), log.get("log", ""))
if not any(k in text.lower() for k in keywords):
continue
events.append(self._event(
"panel",
"panel_log",
log.get("type", "Panel log"),
log.get("log", ""),
"info",
self._to_timestamp(log.get("addtime", 0)),
log.get("username", ""),
"normal",
))
return events
def _event(self, source, event_type, title, message, level, ts, meta, status):
level = self._normalize_level(level)[0] or "info"
return {
"id": "",
"source": source,
"type": event_type,
"title": title or "",
"message": message or "",
"level": level,
"level_value": self.SEVERITY_ORDER.get(level, 0),
"time": self._to_timestamp(ts) or int(time.time()),
"time_text": "",
"relative_time": "",
"meta": meta or "",
"status": status or "",
}
# ------------------------------
# Sources and helpers
# ------------------------------
def _get_webshell_scan_result(self):
last_scan = self._read_json(self._panel_file("data", "safeCloud", "last_scan.json"), {})
last_scan_ts = self._to_timestamp(last_scan.get("scan_timestamp", 0))
if not last_scan_ts:
last_scan_ts = self._to_timestamp(last_scan.get("scan_date", ""))
total_scanned_files = self._safe_int(last_scan.get("total_files", 0))
log_file = self._panel_file("data", "safeCloud", "log", "detection_all.log")
counts = self._severity_counts()
risk_stats = {"0": 0, "1": 0, "2": 0}
processed_stats = {"0": 0, "1": 0}
latest_log_ts = 0
if os.path.exists(log_file):
for line in self._iter_lines(log_file):
parts = line.strip().split("|")
if len(parts) < 9:
continue
try:
risk_level = str(int(parts[4]))
except Exception:
risk_level = str(parts[4]).strip()
processed = str(parts[8]).strip()
if processed.lower() in ["true", "handled", "ignore", "ignored", "deleted"]:
processed = "1"
elif processed.lower() in ["false", "unhandled"]:
processed = "0"
processed = str(self._safe_int(processed, 0))
log_ts = self._to_timestamp(parts[5])
latest_log_ts = max(latest_log_ts, log_ts)
if processed != "0":
continue
if risk_level in risk_stats:
risk_stats[risk_level] += 1
if processed in processed_stats:
processed_stats[processed] += 1
level = self._malware_risk_level(risk_level)
if level in counts and level != "info":
counts[level] += 1
last_ts = last_scan_ts or latest_log_ts
return {
"has_data": bool(last_ts or total_scanned_files or os.path.exists(log_file)),
"last_scan_timestamp": last_ts,
"total_scanned_files": total_scanned_files,
"total_detected": sum(risk_stats.values()),
"risk_stats": risk_stats,
"processed_stats": processed_stats,
"severity": counts,
}
def _malware_risk_level(self, value):
text = str(value).strip().lower()
if text == "2":
return "high"
if text == "1":
return "medium"
if text == "0":
return "low"
return self._normalize_level(text)[0] or "high"
def _parse_malware_logs(self, limit=100, log_file=None):
log_file = log_file or self._panel_file("data", "safeCloud", "log", "detection_all.log")
rows = []
for line in self._tail_lines(log_file, limit):
parts = line.strip().split("|")
if len(parts) < 6:
continue
filename = parts[0] if len(parts) > 0 else ""
filepath = parts[1] if len(parts) > 1 else ""
threat_type = parts[2] if len(parts) > 2 else "Malware"
md5 = parts[3] if len(parts) > 3 else ""
level = self._malware_risk_level(parts[4] if len(parts) > 4 else "high")
detect_time = parts[5] if len(parts) > 5 else ""
status = parts[6] if len(parts) > 6 else ""
detect_type = parts[7] if len(parts) > 7 else ""
handled_raw = parts[8] if len(parts) > 8 else ""
handled = str(handled_raw).lower() in ["1", "true", "handled", "ignore", "ignored", "deleted"]
if str(status).lower() in ["handled", "ignore", "ignored", "deleted"]:
handled = True
rows.append({
"filename": filename,
"filepath": filepath,
"threat_type": threat_type,
"md5": md5,
"level": level,
"time": self._to_timestamp(detect_time),
"status": status,
"detect_type": detect_type,
"handled": handled,
})
return rows
def _source_status(self, scan_cards):
status = {}
for card in scan_cards:
status[card["id"]] = {
"has_data": card["has_data"],
"last_time": card["last_scan_time"],
"last_timestamp": card["last_scan_timestamp"],
"source_api": self._scan_card_source_api(card["id"]),
}
return status
def _scan_entry(self):
return {
"overview_refresh_api": "/v2/safe/overview/get_overview",
"existing_scan_apis": [
{
"id": "security_risk",
"title": "Security risk scan",
"api": "/v2/warning",
"params": {"action": "get_list", "force": 1},
},
{
"id": "website_vulnerability",
"title": "Website vulnerability scan",
"api": "/v2/scanning",
"params": {"action": "startScan"},
},
{
"id": "malware_detection",
"title": "Malware detection",
"api": "/v2/safecloud",
"params": {"action": "webshell_detection"},
},
],
"note": "Overview API is read-only. Start scan APIs keep using existing modules.",
}
def _health_levels(self):
return [
{
"key": "healthy",
"label": "HEALTHY",
"min_score": 95,
"color": "#16a34a",
"description": "All systems fully secure",
},
{
"key": "good",
"label": "GOOD",
"min_score": 75,
"color": "#22c55e",
"description": "Minor issues detected",
},
{
"key": "attention",
"label": "ATTENTION",
"min_score": 50,
"color": "#d97706",
"description": "Security issues need review",
},
{
"key": "critical",
"label": "CRITICAL",
"min_score": 0,
"color": "#dc2626",
"description": "Immediate action required",
},
]
def _action(self, action_id, title, description, severity, source, route):
severity = self._normalize_level(severity)[0] or "info"
return {
"id": action_id,
"title": title,
"description": description,
"severity": severity,
"severity_label": self.SEVERITY_LABELS.get(severity, "Info"),
"priority": self.SEVERITY_ORDER.get(severity, 0),
"source": source,
# "source_type": self._action_source_type(source),
# "jump_route": route,
# "route": route,
# "button_text": "Handle now" if severity in ["critical", "high"] else "View",
}
def _event_stats(self, events):
by_level = self._severity_counts(include_info=True)
by_source = {}
for event in events:
level = event.get("level", "info")
if level in by_level:
by_level[level] += 1
source = event.get("source", "unknown")
by_source[source] = by_source.get(source, 0) + 1
return {
"by_level": by_level,
"by_source": by_source,
}
def _module_route(self, module_id):
return self.MODULE_ROUTES.get(module_id, "")
def _scan_card_route(self, card_id):
return self.SCAN_CARD_ROUTES.get(card_id, "")
def _scan_card_source_api(self, card_id):
return self.SCAN_CARD_SOURCE_APIS.get(card_id, "")
def _action_source_type(self, source):
return "plugin" if source in self.PLUGIN_ACTION_SOURCES else "panel"
def _get_listening_ports(self):
ports = []
try:
import psutil
for conn in psutil.net_connections(kind="inet"):
if str(getattr(conn, "status", "")).upper() != "LISTEN":
continue
laddr = getattr(conn, "laddr", None)
port = getattr(laddr, "port", None)
if port:
ports.append(str(port))
except Exception:
pass
if not ports:
ports = self._get_proc_listening_ports()
return self._sort_port_list(ports)
def _get_proc_listening_ports(self):
ports = []
for path in ["/proc/net/tcp", "/proc/net/tcp6"]:
body = self._read_text(path)
if not body:
continue
for line in body.splitlines()[1:]:
parts = line.split()
if len(parts) < 4 or parts[3].upper() != "0A":
continue
try:
port_hex = parts[1].split(":")[-1]
port = int(port_hex, 16)
if port > 0:
ports.append(str(port))
except Exception:
continue
return ports
def _sort_port_list(self, ports):
deduped = []
for port in ports:
port = str(port).strip()
if port and port not in deduped:
deduped.append(port)
return sorted(deduped, key=lambda x: self._safe_int(x, 0))
def _pick_counter_values(self, data, keys):
counters = {}
if not isinstance(data, dict):
data = {}
for key in keys:
counters[key] = self._safe_int(data.get(key, 0))
return counters
def _tamper_totals_from_files(self):
all_types = dict((key, 0) for key in self.TAMPER_INTERCEPT_KEYS)
today_types = dict((key, 0) for key in self.TAMPER_INTERCEPT_KEYS)
total_dir = os.path.join(public.get_setup_path(), "tamper", "total")
total_file = os.path.join(total_dir, "total.json")
all_types.update(self._pick_counter_values(self._read_json(total_file, {}), self.TAMPER_INTERCEPT_KEYS))
today_name = time.strftime("%Y-%m-%d", time.localtime())
if os.path.isdir(total_dir):
for name in os.listdir(total_dir):
path_dir = os.path.join(total_dir, name)
if not os.path.isdir(path_dir):
continue
today_file = os.path.join(path_dir, "{}.json".format(today_name))
path_today = self._pick_counter_values(self._read_json(today_file, {}), self.TAMPER_INTERCEPT_KEYS)
for key, value in path_today.items():
today_types[key] += value
return all_types, today_types
def _tamper_totals_from_paths(self):
all_types = dict((key, 0) for key in self.TAMPER_INTERCEPT_KEYS)
today_types = dict((key, 0) for key in self.TAMPER_INTERCEPT_KEYS)
try:
paths = public.run_plugin("tamper_core", "get_tamper_paths", public.to_dict_obj({}))
rows = paths.get("data", []) if isinstance(paths, dict) else paths
for row in rows if isinstance(rows, list) else []:
if not isinstance(row, dict):
continue
total = row.get("total", {}) if isinstance(row.get("total", {}), dict) else {}
all_total = self._pick_counter_values(total.get("all", {}), self.TAMPER_INTERCEPT_KEYS)
today_total = self._pick_counter_values(total.get("today", {}), self.TAMPER_INTERCEPT_KEYS)
for key, value in all_total.items():
all_types[key] += value
for key, value in today_total.items():
today_types[key] += value
except Exception:
pass
return all_types, today_types
def _get_tamper_settings_status(self):
config = self._read_json(os.path.join(public.get_setup_path(), "tamper", "tamper.conf"), {})
if not isinstance(config, dict):
return False
return bool(self._safe_int(config.get("status", 0)))
def _btwaf_file(self, filename):
server_path = os.path.join("/www/server/btwaf", filename)
if os.path.exists(server_path):
return server_path
return os.path.join(self._plugin_path("btwaf"), filename)
def _get_btwaf_global_open(self, plugin_dir):
config = self._read_json(self._btwaf_file("config.json"), {})
if isinstance(config, dict):
value = config.get("open", "")
if isinstance(value, bool):
return value
return str(value).lower() in ["1", "true", "yes", "on"]
return False
def _get_btwaf_overview(self):
try:
args = public.to_dict_obj({
"start_time": time.strftime("%Y-%m-%d", time.localtime()),
"country": 0,
"request": 1,
})
result = public.run_plugin("btwaf", "overview", args)
if isinstance(result, dict) and isinstance(result.get("message"), dict):
result = result.get("message")
return result if isinstance(result, dict) else {}
except Exception as ex:
public.print_log("security overview get btwaf overview error: {}".format(ex))
return {}
def _get_btwaf_intercept_summary(self, plugin_dir):
data = self._read_json("/www/server/btwaf/total.json", None)
if data is None:
data = self._read_json(os.path.join(plugin_dir, "total.json"), {})
if not isinstance(data, dict) or not data:
counters = self._btwaf_intercept_types(plugin_dir)
return sum(counters.values()), counters
rules = data.get("rules", {}) if isinstance(data, dict) else {}
if isinstance(rules, list):
counters = {}
for item in rules:
if isinstance(item, dict):
key = str(item.get("key", "")).strip()
if key:
counters[key] = self._safe_int(item.get("value", 0))
rules = counters
if not isinstance(rules, dict):
rules = {}
counters = self._format_btwaf_rule_counters(rules)
total = self._safe_int(data.get("total", 0)) if isinstance(data, dict) else 0
if total <= 0:
total = sum(counters.values())
return total, counters
def _btwaf_attack_types_from_overview(self, overview):
raw_counters = {}
rows = overview.get("type", []) if isinstance(overview, dict) else []
for row in rows if isinstance(rows, list) else []:
key = ""
value = 0
if isinstance(row, dict):
key = str(row.get("key", row.get("type", row.get("name", "")))).strip()
value = self._safe_int(row.get("value", row.get("count", 0)))
elif isinstance(row, (list, tuple)) and len(row) >= 2:
key = str(row[0]).strip()
value = self._safe_int(row[1])
if key:
raw_counters[key] = value
if not raw_counters:
return [], {}
counters = dict((key, 0) for key in self.WAF_INTERCEPT_KEYS)
for key, value in raw_counters.items():
counters[key] = value
items = [{"type": key, "count": value} for key, value in raw_counters.items() if value > 0]
items.sort(key=lambda item: item["count"], reverse=True)
return items, counters
def _format_btwaf_rule_counters(self, rules):
raw = {}
for key, value in rules.items():
raw[str(key)] = self._safe_int(value)
raw["get"] = 0
if "args" in raw:
raw["get"] += raw.pop("args")
if "url" in raw:
raw["get"] += raw.pop("url")
result = {}
for key in self.WAF_INTERCEPT_KEYS:
result[key] = self._safe_int(raw.get(key, 0))
return result
def _btwaf_site_config(self, plugin_dir):
server_file = os.path.join("/www/server/btwaf", "site_waf_config_php.json")
plugin_file = os.path.join(plugin_dir, "site_waf_config_php.json")
data = self._read_json(server_file, None)
if data is None:
data = self._read_json(plugin_file, {})
return data if isinstance(data, dict) else {}
def _btwaf_intercept_types(self, plugin_dir):
counters = dict((key, 0) for key in self.WAF_INTERCEPT_KEYS)
data = self._btwaf_site_config(plugin_dir)
rows = data.get("data", []) if isinstance(data.get("data", []), list) else []
for row in rows:
if not isinstance(row, dict):
continue
for item in row.get("total", []) if isinstance(row.get("total", []), list) else []:
if not isinstance(item, dict):
continue
key = str(item.get("key", "")).strip()
if not key:
continue
if key not in counters:
counters[key] = 0
counters[key] += self._safe_int(item.get("value", 0))
if sum(counters.values()) == 0:
sqlite_counters = self._btwaf_intercept_types_from_sqlite()
for key, value in sqlite_counters.items():
if key not in counters:
counters[key] = 0
counters[key] += value
return counters
def _btwaf_intercept_types_from_sqlite(self):
counters = {}
db_file = "/www/server/btwaf/totla_db/totla_db.db"
if not os.path.exists(db_file):
return counters
try:
import sqlite3
conn = sqlite3.connect(db_file)
cur = conn.cursor()
cur.execute(
"select coalesce(nullif(value_risk, ''), filter_rule) as risk_type, count(1) "
"from totla_log group by coalesce(nullif(value_risk, ''), filter_rule)"
)
for key, value in cur.fetchall():
key = str(key or "").strip()
if key:
counters[key] = self._safe_int(value)
conn.close()
except Exception:
pass
return counters
def _get_fail2ban_service_status(self, plugin_dir):
try:
status = public.run_plugin("fail2ban", "get_fail2ban_status", public.to_dict_obj({}))
if isinstance(status, bool):
return status
if isinstance(status, dict):
if "status" in status:
return bool(status.get("status"))
if "message" in status:
return bool(status.get("message"))
except Exception:
pass
return os.path.exists(os.path.join(plugin_dir, "fail2ban.sock"))
def _top_counter_text(self, counters, limit=3):
pairs = [
(key, self._safe_int(value))
for key, value in counters.items()
if self._safe_int(value) > 0
]
pairs.sort(key=lambda item: item[1], reverse=True)
if not pairs:
return "no classified attacks"
return ", ".join("{} {}".format(key, value) for key, value in pairs[:limit])
def _get_protect_days(self):
install_file = self._panel_file("data", "safeCloud", "install_time.pl")
ts = self._to_timestamp(self._read_text(install_file).strip())
if not ts:
return 1
days = int((time.time() - ts) / 86400)
return days if days > 0 else 1
def _get_virus_update_time(self):
rule_dir = self._panel_file("data", "safeCloud", "rules")
latest = 0
if os.path.exists(rule_dir):
for root, dirs, files in os.walk(rule_dir):
for filename in files:
try:
latest = max(latest, int(os.path.getmtime(os.path.join(root, filename))))
except Exception:
pass
return self._format_time(latest)
def _ssh_option_enabled(self, conf, option, default=False):
if not conf:
return default
option = str(option or "").lower()
for line in conf.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) < 2 or parts[0].lower() != option:
continue
value = str(parts[1]).lower()
if value in ["yes", "true", "1", "on"]:
return True
if value in ["no", "false", "0", "off"]:
return False
return default
return default
def _panel_file(self, *parts):
path = os.path.join(self.panel_path, *parts)
if os.path.exists(path):
return path
fallback = os.path.join("/www/server/panel", *parts)
return fallback
def _plugin_path(self, plugin_name):
path = os.path.join(self.panel_path, "plugin", plugin_name)
if os.path.exists(path):
return path
return os.path.join("/www/server/panel/plugin", plugin_name)
def _read_json(self, path, default):
try:
body = self._read_text(path)
if not body:
return default
return json.loads(body)
except Exception:
return default
def _read_text(self, path):
try:
if not path or not os.path.exists(path):
return ""
with open(path, "r", encoding="utf-8", errors="ignore") as fp:
return fp.read()
except TypeError:
try:
with open(path, "r") as fp:
return fp.read()
except Exception:
return ""
except Exception:
return ""
def _tail_lines(self, path, limit):
if not path or not os.path.exists(path):
return []
try:
with open(path, "r", encoding="utf-8", errors="ignore") as fp:
return list(deque(fp, maxlen=limit))
except TypeError:
try:
with open(path, "r") as fp:
return list(deque(fp, maxlen=limit))
except Exception:
return []
except Exception:
return []
def _iter_lines(self, path):
if not path or not os.path.exists(path):
return []
try:
with open(path, "r", encoding="utf-8", errors="ignore") as fp:
return list(fp)
except TypeError:
try:
with open(path, "r") as fp:
return list(fp)
except Exception:
return []
except Exception:
return []
def _unwrap_message(self, result, default):
try:
if isinstance(result, dict):
return result.get("message", default)
except Exception:
pass
return default
def _severity_counts(self, include_info=False):
data = {
"critical": 0,
"high": 0,
"medium": 0,
"low": 0,
}
if include_info:
data["info"] = 0
return data
def _max_severity(self, counts):
for key in ["critical", "high", "medium", "low"]:
if int(counts.get(key, 0) or 0) > 0:
return key
return "info"
def _normalize_level(self, value):
if value is None:
return "", 0
try:
num = int(value)
if num >= 4:
return "critical", 4
if num == 3:
return "high", 3
if num == 2:
return "medium", 2
if num == 1:
return "low", 1
return "info", 0
except Exception:
pass
text = str(value).strip().lower()
if text in ["serious", "critical", "danger", "dangerous", "fatal"]:
return "critical", 4
if text in ["high", "important"]:
return "high", 3
if text in ["medium", "warning", "warn"]:
return "medium", 2
if text in ["low"]:
return "low", 1
if text in ["info", "normal", "safe"]:
return "info", 0
return "", 0
def _to_timestamp(self, value):
if value is None or value == "":
return 0
try:
ts = int(float(value))
if ts > 0:
return ts
except Exception:
pass
text = str(value).strip()
if not text:
return 0
text = text.replace("/", "-")
for fmt in [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%d",
"%b %d %H:%M:%S",
"%b %d %H:%M:%S",
]:
try:
if fmt.startswith("%b"):
text2 = "{} {}".format(time.localtime().tm_year, text)
return int(time.mktime(time.strptime(text2, "%Y " + fmt)))
return int(time.mktime(time.strptime(text, fmt)))
except Exception:
continue
return 0
def _parse_syslog_time(self, line):
text = " ".join(line.split()[:3])
return self._to_timestamp(text) or int(time.time())
def _format_time(self, ts):
ts = self._to_timestamp(ts)
if not ts:
return ""
try:
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
except Exception:
return ""
def _relative_time(self, ts):
ts = self._to_timestamp(ts)
if not ts:
return "No data"
diff = int(time.time()) - ts
if diff < 60:
return "Just now"
if diff < 3600:
return "{} minutes ago".format(max(1, int(diff / 60)))
if diff < 86400:
return "{} hours ago".format(max(1, int(diff / 3600)))
return "{} days ago".format(max(1, int(diff / 86400)))
def _extract_ip(self, text):
match = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", text or "")
return match.group(1) if match else ""
def _safe_int(self, value, default=0):
try:
return int(float(value))
except Exception:
return default
def _to_bool(self, value, default=False):
if isinstance(value, bool):
return value
if value is None:
return default
if isinstance(value, int):
return value != 0
text = str(value).strip().lower()
if text in ["1", "true", "yes", "on", "open"]:
return True
if text in ["0", "false", "no", "off", "close", "closed", ""]:
return False
return default
def _get_int(self, get, name, default, min_value=None, max_value=None):
value = self._safe_int(self._get_arg(get, name, default), default)
if min_value is not None:
value = max(min_value, value)
if max_value is not None:
value = min(max_value, value)
return value
def _get_bool(self, get, name, default=False):
value = self._get_arg(get, name, default)
if isinstance(value, bool):
return value
if isinstance(value, int):
return value != 0
return str(value).lower() in ["1", "true", "yes", "on"]
def _get_arg(self, get, name, default=None):
if get is None:
return default
args = self._get_request_args(get)
if name in args:
return args.get(name, default)
return default
def _get_first_arg(self, get, names, default=None):
args = self._get_request_args(get)
for name in names:
if name in args:
return args.get(name)
return default
def _get_request_args(self, get):
args = dict(self._object_items(get))
try:
from flask import request
args.update(request.args.to_dict())
args.update(request.form.to_dict())
body = request.get_json(silent=True)
if isinstance(body, dict):
args.update(body)
except Exception:
pass
for key in ["data", "params"]:
args.update(self._parse_nested_args(args.get(key)))
return args
def _object_items(self, value):
if isinstance(value, dict):
return value
try:
items = value.get_items()
return items if isinstance(items, dict) else {}
except Exception:
pass
try:
items = value.__dict__
return items if isinstance(items, dict) else {}
except Exception:
pass
return {}
def _parse_nested_args(self, value):
if isinstance(value, dict):
return value
items = self._object_items(value)
if items:
return items
if not isinstance(value, str):
return {}
value = value.strip()
if not value:
return {}
try:
parsed = json.loads(value)
except Exception:
return {}
if isinstance(parsed, dict):
return parsed
return {}