Merge branch 'next_release' of github.com:netalertx/NetAlertX into next_release

This commit is contained in:
jokob-sk committed 2026-08-15 12:21:24 +10:00
commit 9adb39e0b3
4 files changed
+111 -19

No files matched your search

+1
View File
@@ -24,6 +24,7 @@ description: NetAlertX coding standards and conventions. Use this when writing c
- follow existing code style and structure, and ensure backward compatibility with existing installations when submitting PRs
- all code needs to be scalable to handle large networks with thousands of devices (10k+) without performance degradation
- no inline imports, all imports must be at the top of the file
- when using `server/logger.py` `mylog()`, only use valid levels: `none`, `minimal`, `verbose`, `debug`, `trace`; invalid levels silently degrade to `none`
## File Length
+4 -4
View File
@@ -207,13 +207,13 @@
"Device_MultiEdit_Backup": "Cuidado, inserir valores errados abaixo interromperá sua configuração. Faça backup do seu banco de dados ou da configuração dos dispositivos primeiro (<a href=\"#\" onclick=\"ExportCSV()\">clique para baixar <i class=\"fa-solid fa-download fa-bounce\"></i> </a>). Leia como recuperar dispositivos deste arquivo no <a href=\"https://docs.netalertx.com/BACKUPS#scenario-2-corrupted-database\" target=\" _blank\">Documentação de backups</a>.",
"Device_MultiEdit_Fields": "Editar campos:",
"Device_MultiEdit_MassActions": "Ações em massa:",
"Device_MultiEdit_No_Devices": "",
"Device_MultiEdit_No_Devices": "Nenhum dispositivo selecionado.",
"Device_MultiEdit_Tooltip": "Cuidadoso. Clicar aqui aplicará o valor à esquerda a todos os dispositivos selecionados acima.",
"Device_NextScan_Imminent": "",
"Device_NextScan_In": "",
"Device_NextScan_In": "Próxima varredura em aproximadamente ",
"Device_NoData_Help": "",
"Device_NoData_Scanning": "",
"Device_NoData_Title": "",
"Device_NoData_Scanning": "Aguarde a primeira varredura - isso pode levar vários minutos após a configuração inicial.",
"Device_NoData_Title": "Ainda não foram encontrados nenhum dispositivo",
"Device_NoMatch_Title": "",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
+44 -15
View File
@@ -1,8 +1,10 @@
import html
import json
import re
import uuid
import socket
from yattag import indent
from yattag.indentation import XMLTokenError
from json2table import convert
# Register NetAlertX modules
@@ -146,20 +148,7 @@ class NotificationInstance:
mail_html, conf.REPORT_DASHBOARD_URL + "/deviceDetails.php?mac="
)
# Add preheader for inbox preview after all links have been generated.
# Invisible padding prevents email clients from showing the start of the email body.
preheader = "".join(preheaders)
padding = (" &zwnj;&#8199;" * 47)
mail_html = mail_html.replace(
"PREHEADER",
preheader + padding,
)
final_html = indent(
mail_html, indentation=" ", newline="\r\n", indent_text=True
)
final_html = finalize_html(mail_html, preheaders)
send_api(self.JSON, final_text, final_html)
@@ -335,8 +324,9 @@ def construct_notifications(JSON, section):
text = tableTitle + "\n---------\n"
# Convert a JSON into an HTML table
html_data = escape_html_rows(jsn)
html = convert(
{"data": jsn},
{"data": html_data},
build_direction=build_direction,
table_attributes=table_attributes,
)
@@ -398,6 +388,45 @@ def format_table(html, thValue, props, newThValue=""):
)
# -----------------------------------------------------------------------------
# Escape free-text values before embedding them into notification HTML
def escape_html_rows(rows):
"""Return a copy of notification rows with only string values HTML-escaped."""
return [
{
key: html.escape(value) if isinstance(value, str) else value
for key, value in row.items()
}
for row in rows
]
# -----------------------------------------------------------------------------
# Finalize HTML and tolerate pretty-print failures
def finalize_html(mail_html, preheaders):
"""Insert an escaped preheader and pretty-print HTML, falling back to raw HTML on XML errors."""
# Add preheader for inbox preview after all links have been generated.
# Invisible padding prevents email clients from showing the start of the email body.
preheader = "".join(html.escape(entry) for entry in preheaders)
padding = (" &zwnj;&#8199;" * 47)
mail_html = mail_html.replace(
"PREHEADER",
preheader + padding,
)
try:
return indent(
mail_html, indentation=" ", newline="\r\n", indent_text=True
)
except XMLTokenError as err:
mylog(
"none",
f"[Notification] Failed to pretty-print HTML report, sending unindented HTML instead: {err}",
)
return mail_html
# -----------------------------------------------------------------------------
# Pre-header Preview
def build_preheader(tableTitle, jsn, headers):
@@ -295,6 +295,68 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
self.assertEqual(html_without, html_with)
# -----------------------------------------------------------------
# HTML output escapes free-text device values while text stays raw
# -----------------------------------------------------------------
@patch("models.notification_instance.get_setting_value")
def test_html_escapes_free_text_values(self, mock_setting):
from models.notification_instance import construct_notifications
mock_setting.side_effect = self._setting_factory({
"NTFPRCS_TEXT_SECTION_HEADERS": True,
"NTFPRCS_TEXT_TEMPLATE_new_devices": "",
})
devices = [
{
"devName": "Meta Quest <Pro",
"eveMac": "aa:bb:cc:dd:ee:ff",
"devVendor": "Meta",
"eveIp": "192.168.1.42",
"eveDateTime": "2025-01-15 10:30:00",
"eveEventType": "New Device",
"devComments": "values <=2 break things",
}
]
json_data = _make_json(
"new_devices", devices, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
html, text, _ = construct_notifications(json_data, "new_devices")
self.assertIn("Meta Quest &lt;Pro", html)
self.assertIn("values &lt;=2 break things", html)
self.assertNotIn("Meta Quest <Pro", html)
self.assertNotIn("values <=2 break things", html)
self.assertIn("Meta Quest <Pro", text)
self.assertIn("values <=2 break things", text)
# -----------------------------------------------------------------
# Final HTML escapes preheaders and tolerates indent failures
# -----------------------------------------------------------------
@patch("models.notification_instance.mylog")
@patch("models.notification_instance.indent")
def test_finalize_html_escapes_preheader_and_falls_back(self, mock_indent, mock_mylog):
from models.notification_instance import finalize_html, XMLTokenError
mock_indent.side_effect = XMLTokenError("broken html")
# The mock forces the pretty-print failure so we can assert the raw
# fallback HTML returned after PREHEADER replacement.
template = "<html><body><span>PREHEADER</span>broken < content</body></html>"
expected_html = template.replace(
"PREHEADER",
"Meta Quest &lt;Pro" + (" &zwnj;&#8199;" * 47),
)
final_html = finalize_html(template, ["Meta Quest <Pro"])
self.assertEqual(final_html, expected_html)
mock_indent.assert_called_once()
mock_mylog.assert_called_once_with(
"none",
"[Notification] Failed to pretty-print HTML report, sending unindented HTML instead: broken html",
)
if __name__ == "__main__":
unittest.main()