diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 82ca4350..d485819f 100755 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -42,7 +42,7 @@ Backend loop phases (see `server/__main__.py` and `server/plugin.py`): `once`, ` ## Conventions & helpers to reuse - Settings: add/modify via `ccd()` in `server/initialise.py` or per‑plugin manifest. Never hardcode ports or secrets; use `get_setting_value()`. - Logging: use `logger.mylog(level, [message])`; levels: none/minimal/verbose/debug/trace. -- Time/MAC/strings: `helper.py` (`timeNowTZ`, `normalize_mac`, sanitizers). Validate MACs before DB writes. +- Time/MAC/strings: `helper.py` (`timeNowDB`, `normalize_mac`, sanitizers). Validate MACs before DB writes. - DB helpers: prefer `server/db/db_helper.py` functions (e.g., `get_table_json`, device condition helpers) over raw SQL in new paths. ## Dev workflow (devcontainer) diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md index 58d50418..3255820a 100755 --- a/docs/NOTIFICATIONS.md +++ b/docs/NOTIFICATIONS.md @@ -58,7 +58,7 @@ You can completely ignore detected devices globally. This could be because your ## Ignoring notifications 🔕 -You can filter out unwanted notifications globally. This could be because of a misbehaving device (GoogleNest/GoogleHub (See also [ARPSAN docs and the `--exclude-broadcast` flag](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/arp_scan#ip-flipping-on-google-nest-devices))) which flips between IP addresses, or because you want to ignore new device notifications of a certian pattern. +You can filter out unwanted notifications globally. This could be because of a misbehaving device (GoogleNest/GoogleHub (See also [ARPSAN docs and the `--exclude-broadcast` flag](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/arp_scan#ip-flipping-on-google-nest-devices))) which flips between IP addresses, or because you want to ignore new device notifications of a certain pattern. 1. Events Filter (`NTFPRCS_event_condition`) - filter out Events from notifications. 2. New Devices Filter (`NTFPRCS_new_dev_condition`) - filter out New Devices from notifications, but log and keep a new device in the system. \ No newline at end of file diff --git a/front/plugins/_publisher_apprise/apprise.py b/front/plugins/_publisher_apprise/apprise.py index 5f1c3c33..a65b1ac0 100755 --- a/front/plugins/_publisher_apprise/apprise.py +++ b/front/plugins/_publisher_apprise/apprise.py @@ -16,7 +16,8 @@ import conf from const import confFileName, logPath from plugin_helper import Plugin_Objects from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from models.notification_instance import NotificationInstance from database import DB from pytz import timezone diff --git a/front/plugins/_publisher_email/email_smtp.py b/front/plugins/_publisher_email/email_smtp.py index 8d738844..9370b03b 100755 --- a/front/plugins/_publisher_email/email_smtp.py +++ b/front/plugins/_publisher_email/email_smtp.py @@ -25,7 +25,8 @@ import conf from const import confFileName, logPath from plugin_helper import Plugin_Objects from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value, hide_email +from helper import get_setting_value, hide_email +from utils.datetime_utils import timeNowDB from models.notification_instance import NotificationInstance from database import DB from pytz import timezone diff --git a/front/plugins/_publisher_mqtt/mqtt.py b/front/plugins/_publisher_mqtt/mqtt.py index 03a441a3..943add6c 100755 --- a/front/plugins/_publisher_mqtt/mqtt.py +++ b/front/plugins/_publisher_mqtt/mqtt.py @@ -23,8 +23,9 @@ from const import confFileName, logPath from plugin_utils import getPluginObject from plugin_helper import Plugin_Objects from logger import mylog, Logger -from helper import timeNowDB, get_setting_value, bytes_to_string, \ +from helper import get_setting_value, bytes_to_string, \ sanitize_string, normalize_string +from utils.datetime_utils import timeNowDB from database import DB, get_device_stats diff --git a/front/plugins/_publisher_ntfy/ntfy.py b/front/plugins/_publisher_ntfy/ntfy.py index 79df681f..ca441f37 100755 --- a/front/plugins/_publisher_ntfy/ntfy.py +++ b/front/plugins/_publisher_ntfy/ntfy.py @@ -19,7 +19,8 @@ import conf from const import confFileName, logPath from plugin_helper import Plugin_Objects, handleEmpty from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from models.notification_instance import NotificationInstance from database import DB from pytz import timezone diff --git a/front/plugins/_publisher_pushover/pushover.py b/front/plugins/_publisher_pushover/pushover.py index 8ebd1dee..e45c76d6 100755 --- a/front/plugins/_publisher_pushover/pushover.py +++ b/front/plugins/_publisher_pushover/pushover.py @@ -11,7 +11,8 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 from logger import mylog, Logger # noqa: E402 -from helper import timeNowDB, get_setting_value, hide_string # noqa: E402 +from helper import get_setting_value, hide_string # noqa: E402 +from utils.datetime_utils import timeNowDB from models.notification_instance import NotificationInstance # noqa: E402 from database import DB # noqa: E402 import conf diff --git a/front/plugins/_publisher_pushsafer/pushsafer.py b/front/plugins/_publisher_pushsafer/pushsafer.py index 366f170a..422ed0f9 100755 --- a/front/plugins/_publisher_pushsafer/pushsafer.py +++ b/front/plugins/_publisher_pushsafer/pushsafer.py @@ -19,7 +19,8 @@ import conf from const import confFileName, logPath from plugin_helper import Plugin_Objects, handleEmpty from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value, hide_string +from helper import get_setting_value, hide_string +from utils.datetime_utils import timeNowDB from models.notification_instance import NotificationInstance from database import DB from pytz import timezone diff --git a/front/plugins/_publisher_telegram/tg.py b/front/plugins/_publisher_telegram/tg.py index c9f92d9d..72f81ed7 100755 --- a/front/plugins/_publisher_telegram/tg.py +++ b/front/plugins/_publisher_telegram/tg.py @@ -16,7 +16,8 @@ import conf from const import confFileName, logPath from plugin_helper import Plugin_Objects from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from models.notification_instance import NotificationInstance from database import DB from pytz import timezone diff --git a/front/plugins/_publisher_webhook/webhook.py b/front/plugins/_publisher_webhook/webhook.py index f1eec9d7..6644d2fe 100755 --- a/front/plugins/_publisher_webhook/webhook.py +++ b/front/plugins/_publisher_webhook/webhook.py @@ -22,7 +22,8 @@ import conf from const import logPath, confFileName from plugin_helper import Plugin_Objects, handleEmpty from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value, hide_string, write_file +from helper import get_setting_value, hide_string, write_file +from utils.datetime_utils import timeNowDB from models.notification_instance import NotificationInstance from database import DB from pytz import timezone diff --git a/front/plugins/internet_ip/script.py b/front/plugins/internet_ip/script.py index f56d2ed7..7f8cb8b9 100755 --- a/front/plugins/internet_ip/script.py +++ b/front/plugins/internet_ip/script.py @@ -20,8 +20,9 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64 from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, check_IP_format, get_setting_value +from helper import check_IP_format, get_setting_value from const import logPath, applicationPath, fullDbPath +from utils.datetime_utils import timeNowDB import conf from pytz import timezone diff --git a/front/plugins/internet_speedtest/script.py b/front/plugins/internet_speedtest/script.py index ef4f5705..4c41e7a3 100755 --- a/front/plugins/internet_speedtest/script.py +++ b/front/plugins/internet_speedtest/script.py @@ -13,7 +13,8 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from plugin_helper import Plugin_Objects from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowDB import conf from pytz import timezone from const import logPath diff --git a/front/plugins/nmap_scan/script.py b/front/plugins/nmap_scan/script.py index 180973bb..6ca65917 100755 --- a/front/plugins/nmap_scan/script.py +++ b/front/plugins/nmap_scan/script.py @@ -14,7 +14,8 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64 from logger import mylog, Logger, append_line_to_file -from helper import timeNowDB, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from const import logPath, applicationPath import conf from pytz import timezone diff --git a/front/plugins/plugin_helper.py b/front/plugins/plugin_helper.py index d95cb795..10023195 100755 --- a/front/plugins/plugin_helper.py +++ b/front/plugins/plugin_helper.py @@ -11,7 +11,8 @@ INSTALL_PATH = "/app" sys.path.append(f"{INSTALL_PATH}/front/plugins") sys.path.append(f'{INSTALL_PATH}/server') -from logger import mylog, Logger, timeNowDB +from logger import mylog, Logger +from utils.datetime_utils import timeNowDB from const import confFileName, default_tz #------------------------------------------------------------------------------- diff --git a/front/plugins/sync/sync.py b/front/plugins/sync/sync.py index 89695bec..3bc584e6 100755 --- a/front/plugins/sync/sync.py +++ b/front/plugins/sync/sync.py @@ -18,7 +18,8 @@ from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64 from plugin_utils import get_plugins_configs, decode_and_rename_files from logger import mylog, Logger from const import pluginsPath, fullDbPath, logPath -from helper import timeNowDB, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from crypto_utils import encrypt_data from messaging.in_app import write_notification import conf diff --git a/server/__main__.py b/server/__main__.py index d623a572..56743876 100755 --- a/server/__main__.py +++ b/server/__main__.py @@ -26,7 +26,8 @@ from pathlib import Path import conf from const import * from logger import mylog -from helper import filePermissions, timeNowTZ, get_setting_value +from helper import filePermissions, get_setting_value +from utils.datetime_utils import timeNowTZ from app_state import updateState from api import update_api from scan.session_events import process_scan diff --git a/server/api.py b/server/api.py index 17d0ee43..4278e9a1 100755 --- a/server/api.py +++ b/server/api.py @@ -7,7 +7,8 @@ import datetime import conf from const import (apiPath, sql_appevents, sql_devices_all, sql_events_pending_alert, sql_settings, sql_plugins_events, sql_plugins_history, sql_plugins_objects,sql_language_strings, sql_notifications_all, sql_online_history, sql_devices_tiles, sql_devices_filters) from logger import mylog -from helper import write_file, get_setting_value, timeNowTZ +from helper import write_file, get_setting_value +from utils.datetime_utils import timeNowTZ from app_state import updateState from models.user_events_queue_instance import UserEventsQueueInstance from messaging.in_app import write_notification diff --git a/server/api_server/device_endpoint.py b/server/api_server/device_endpoint.py index 9c032f28..7633bbd2 100755 --- a/server/api_server/device_endpoint.py +++ b/server/api_server/device_endpoint.py @@ -14,7 +14,8 @@ INSTALL_PATH="/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from database import get_temp_db_connection -from helper import is_random_mac, format_date, get_setting_value, timeNowDB +from helper import is_random_mac, get_setting_value +from utils.datetime_utils import timeNowDB, format_date from db.db_helper import row_to_json, get_date_from_period # -------------------------- diff --git a/server/api_server/devices_endpoint.py b/server/api_server/devices_endpoint.py index eb1960a4..ab298745 100755 --- a/server/api_server/devices_endpoint.py +++ b/server/api_server/devices_endpoint.py @@ -19,8 +19,9 @@ INSTALL_PATH="/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from database import get_temp_db_connection -from helper import is_random_mac, format_date, get_setting_value +from helper import is_random_mac, get_setting_value from db.db_helper import get_table_json, get_device_condition_by_status +from utils.datetime_utils import format_date # -------------------------- diff --git a/server/api_server/events_endpoint.py b/server/api_server/events_endpoint.py index 5d02fcda..c63265bf 100755 --- a/server/api_server/events_endpoint.py +++ b/server/api_server/events_endpoint.py @@ -14,8 +14,9 @@ INSTALL_PATH="/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from database import get_temp_db_connection -from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, ensure_datetime +from helper import is_random_mac, get_setting_value, mylog from db.db_helper import row_to_json, get_date_from_period +from utils.datetime_utils import format_date, format_date_iso, format_event_date, ensure_datetime # -------------------------- diff --git a/server/api_server/history_endpoint.py b/server/api_server/history_endpoint.py index bf719ec2..a08ca476 100755 --- a/server/api_server/history_endpoint.py +++ b/server/api_server/history_endpoint.py @@ -14,7 +14,8 @@ INSTALL_PATH="/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from database import get_temp_db_connection -from helper import is_random_mac, format_date, get_setting_value +from helper import is_random_mac, get_setting_value +from utils.datetime_utils import format_date # -------------------------------------------------- diff --git a/server/api_server/sessions_endpoint.py b/server/api_server/sessions_endpoint.py index 811503be..113a8250 100755 --- a/server/api_server/sessions_endpoint.py +++ b/server/api_server/sessions_endpoint.py @@ -16,8 +16,9 @@ INSTALL_PATH="/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) from database import get_temp_db_connection -from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, format_date_diff, format_ip_long, parse_datetime +from helper import is_random_mac, get_setting_value, mylog, format_ip_long from db.db_helper import row_to_json, get_date_from_period +from utils.datetime_utils import format_date_iso, format_event_date, format_date_diff, parse_datetime, format_date # -------------------------- diff --git a/server/api_server/sync_endpoint.py b/server/api_server/sync_endpoint.py index 59b8095e..424411ab 100755 --- a/server/api_server/sync_endpoint.py +++ b/server/api_server/sync_endpoint.py @@ -2,7 +2,8 @@ import os import base64 from flask import jsonify, request from logger import mylog -from helper import get_setting_value, timeNowDB +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from messaging.in_app import write_notification INSTALL_PATH = "/app" diff --git a/server/app_state.py b/server/app_state.py index d4b33525..ec2ffc1c 100755 --- a/server/app_state.py +++ b/server/app_state.py @@ -4,7 +4,8 @@ import json import conf from const import * from logger import mylog, logResult -from helper import timeNowDB, timeNow, checkNewVersion +from helper import checkNewVersion +from utils.datetime_utils import timeNowDB, timeNow # Register NetAlertX directories INSTALL_PATH="/app" diff --git a/server/helper.py b/server/helper.py index db678ffd..7a89c270 100755 --- a/server/helper.py +++ b/server/helper.py @@ -7,7 +7,6 @@ import os import re import unicodedata import subprocess -from typing import Union import pytz from pytz import timezone import json @@ -29,144 +28,6 @@ from logger import mylog, logResult # Register NetAlertX directories INSTALL_PATH="/app" -#------------------------------------------------------------------------------- -# DateTime -#------------------------------------------------------------------------------- -def timeNowTZ(): - if conf.tz: - return datetime.datetime.now(conf.tz).replace(microsecond=0) - else: - return datetime.datetime.now().replace(microsecond=0) - -def timeNow(): - return datetime.datetime.now().replace(microsecond=0) - -def get_timezone_offset(): - now = datetime.datetime.now(conf.tz) - offset_hours = now.utcoffset().total_seconds() / 3600 - offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60)) - return offset_formatted - -def timeNowDB(local=True): - """ - Return the current time (local or UTC) as ISO 8601 for DB storage. - Safe for SQLite, PostgreSQL, etc. - - Example local: '2025-11-04 18:09:11' - Example UTC: '2025-11-04 07:09:11' - """ - if local: - try: - if isinstance(conf.tz, datetime.tzinfo): - tz = conf.tz - elif conf.tz: - tz = ZoneInfo(conf.tz) - else: - tz = None - except Exception: - tz = None - return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S') - else: - return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S') - - -#------------------------------------------------------------------------------- -# Date and time methods -#------------------------------------------------------------------------------- - -# ------------------------------------------------------------------------------------------- -def format_date_iso(date1: str) -> str: - """Return ISO 8601 string for a date or None if empty""" - if date1 is None: - return None - dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1 - return dt.isoformat() - -# ------------------------------------------------------------------------------------------- -def format_event_date(date_str: str, event_type: str) -> str: - """Format event date with fallback rules.""" - if date_str: - return format_date(date_str) - elif event_type == "": - return "" - else: - return "" - -# ------------------------------------------------------------------------------------------- -def ensure_datetime(dt: Union[str, datetime.datetime, None]) -> datetime.datetime: - if dt is None: - return timeNowTZ() - if isinstance(dt, str): - return datetime.datetime.fromisoformat(dt) - return dt - - -def parse_datetime(dt_str): - if not dt_str: - return None - try: - # Try ISO8601 first - return datetime.datetime.fromisoformat(dt_str) - except ValueError: - # Try RFC1123 / HTTP format - try: - return datetime.datetime.strptime(dt_str, '%a, %d %b %Y %H:%M:%S GMT') - except ValueError: - return None - -def format_date(date_str: str) -> str: - try: - dt = parse_datetime(date_str) - if dt.tzinfo is None: - # Set timezone if missing — change to timezone.utc if you prefer UTC - now = datetime.datetime.now(conf.tz) - dt = dt.replace(tzinfo=now.astimezone().tzinfo) - return dt.astimezone().isoformat() - except Exception: - return "invalid" - -def format_date_diff(date1, date2): - """ - Return difference between two datetimes as 'Xd HH:MM'. - Uses app timezone if datetime is naive. - date2 can be None (uses now). - """ - # Get timezone from settings - tz_name = get_setting_value("TIMEZONE") or "UTC" - tz = pytz.timezone(tz_name) - - def parse_dt(dt): - if dt is None: - return datetime.datetime.now(tz) - if isinstance(dt, str): - try: - dt_parsed = email.utils.parsedate_to_datetime(dt) - except Exception: - # fallback: parse ISO string - dt_parsed = datetime.datetime.fromisoformat(dt) - # convert naive GMT/UTC to app timezone - if dt_parsed.tzinfo is None: - dt_parsed = tz.localize(dt_parsed) - else: - dt_parsed = dt_parsed.astimezone(tz) - return dt_parsed - return dt if dt.tzinfo else tz.localize(dt) - - dt1 = parse_dt(date1) - dt2 = parse_dt(date2) - - delta = dt2 - dt1 - total_minutes = int(delta.total_seconds() // 60) - days, rem_minutes = divmod(total_minutes, 1440) # 1440 mins in a day - hours, minutes = divmod(rem_minutes, 60) - - return { - "text": f"{days}d {hours:02}:{minutes:02}", - "days": days, - "hours": hours, - "minutes": minutes, - "total_minutes": total_minutes - } #------------------------------------------------------------------------------- # File system permission handling diff --git a/server/initialise.py b/server/initialise.py index c16e71ba..8f55476d 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -12,7 +12,8 @@ import re # Register NetAlertX libraries import conf from const import fullConfPath, applicationPath, fullConfFolder, default_tz -from helper import getBuildTimeStamp, fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowDB, get_setting_value, generate_random_string +from helper import getBuildTimeStamp, fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, get_setting_value, generate_random_string +from utils.datetime_utils import timeNowDB from app_state import updateState from logger import mylog from api import update_api diff --git a/server/logger.py b/server/logger.py index 8cd16c9d..0b4a57e2 100755 --- a/server/logger.py +++ b/server/logger.py @@ -7,40 +7,15 @@ import time import logging from zoneinfo import ZoneInfo +# Register NetAlertX directories +INSTALL_PATH="/app" + +sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) + # NetAlertX imports import conf from const import * - -#------------------------------------------------------------------------------- -# duplication from helper to avoid circle -#------------------------------------------------------------------------------- -def timeNowTZ(): - if conf.tz: - return datetime.datetime.now(conf.tz).replace(microsecond=0) - else: - return datetime.datetime.now().replace(microsecond=0) - -def timeNowDB(local=True): - """ - Return the current time (local or UTC) as ISO 8601 for DB storage. - Safe for SQLite, PostgreSQL, etc. - - Example local: '2025-11-04 18:09:11' - Example UTC: '2025-11-04 07:09:11' - """ - if local: - try: - if isinstance(conf.tz, datetime.tzinfo): - tz = conf.tz - elif conf.tz: - tz = ZoneInfo(conf.tz) - else: - tz = None - except Exception: - tz = None - return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S') - else: - return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S') +from utils.datetime_utils import timeNowTZ #------------------------------------------------------------------------------- # Map custom debug levels to Python logging levels diff --git a/server/messaging/in_app.py b/server/messaging/in_app.py index 5246acf4..98cd5a28 100755 --- a/server/messaging/in_app.py +++ b/server/messaging/in_app.py @@ -20,7 +20,8 @@ sys.path.extend([f"{INSTALL_PATH}/server"]) import conf from const import applicationPath, logPath, apiPath, confFileName, reportTemplatesPath from logger import logResult, mylog -from helper import generate_mac_links, removeDuplicateNewLines, timeNowDB, get_file_content, write_file, get_setting_value, get_timezone_offset +from helper import generate_mac_links, removeDuplicateNewLines, get_file_content, write_file, get_setting_value +from utils.datetime_utils import timeNowDB NOTIFICATION_API_FILE = apiPath + 'user_notifications.json' diff --git a/server/messaging/reporting.py b/server/messaging/reporting.py index 2c885ce1..90e16808 100755 --- a/server/messaging/reporting.py +++ b/server/messaging/reporting.py @@ -20,9 +20,10 @@ sys.path.extend([f"{INSTALL_PATH}/server"]) import conf from const import applicationPath, logPath, apiPath, confFileName -from helper import get_file_content, write_file, get_timezone_offset, get_setting_value +from helper import get_file_content, write_file, get_setting_value from logger import logResult, mylog from db.sql_safe_builder import create_safe_condition_builder +from utils.datetime_utils import get_timezone_offset #=============================================================================== # REPORTING diff --git a/server/models/notification_instance.py b/server/models/notification_instance.py index 02832d45..8db9be43 100755 --- a/server/models/notification_instance.py +++ b/server/models/notification_instance.py @@ -16,12 +16,10 @@ from const import applicationPath, logPath, apiPath, reportTemplatesPath from logger import mylog, Logger from helper import generate_mac_links, \ removeDuplicateNewLines, \ - timeNowDB, \ - timeNowTZ, \ write_file, \ - get_setting_value, \ - get_timezone_offset + get_setting_value from messaging.in_app import write_notification +from utils.datetime_utils import timeNowDB, get_timezone_offset # ----------------------------------------------------------------------------- diff --git a/server/plugin.py b/server/plugin.py index ba24b47d..a62ba584 100755 --- a/server/plugin.py +++ b/server/plugin.py @@ -12,7 +12,8 @@ from collections import namedtuple import conf from const import pluginsPath, logPath, applicationPath, reportTemplatesPath from logger import mylog, Logger -from helper import timeNowDB, timeNowTZ, get_file_content, write_file, get_setting, get_setting_value +from helper import get_file_content, write_file, get_setting, get_setting_value +from utils.datetime_utils import timeNowTZ, timeNowDB from app_state import updateState from api import update_api from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting_obj, print_plugin_info, list_to_csv, combine_plugin_objects, resolve_wildcards_arr, handle_empty, custom_plugin_decoder, decode_and_rename_files diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index 48cb84fe..76aca0c5 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -10,7 +10,8 @@ from dateutil import parser INSTALL_PATH="/app" sys.path.extend([f"{INSTALL_PATH}/server"]) -from helper import timeNowDB, timeNowTZ, get_setting_value, check_IP_format +from helper import get_setting_value, check_IP_format +from utils.datetime_utils import timeNowDB from logger import mylog, Logger from const import vendorsPath, vendorsPathNewest, sql_generateGuid from models.device_instance import DeviceInstance diff --git a/server/scan/session_events.py b/server/scan/session_events.py index 2dd1b9fe..88fbb530 100755 --- a/server/scan/session_events.py +++ b/server/scan/session_events.py @@ -6,7 +6,8 @@ sys.path.extend([f"{INSTALL_PATH}/server"]) import conf from scan.device_handling import create_new_devices, print_scan_stats, save_scanned_devices, exclude_ignored_devices, update_devices_data_from_scan -from helper import timeNowDB, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from db.db_helper import print_table_schema from logger import mylog, Logger from messaging.reporting import skip_repeated_notifications diff --git a/server/utils/datetime_utils.py b/server/utils/datetime_utils.py new file mode 100644 index 00000000..b8f7d1dc --- /dev/null +++ b/server/utils/datetime_utils.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python + +import os +import pathlib +import sys +from datetime import datetime +import pytz +from pytz import timezone +import datetime +from typing import Union + +# Register NetAlertX directories +INSTALL_PATH="/app" +sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) + + +# Register NetAlertX directories +INSTALL_PATH="/app" + +import conf +from const import * + + + +#------------------------------------------------------------------------------- +# DateTime +#------------------------------------------------------------------------------- +def timeNowTZ(): + if conf.tz: + return datetime.datetime.now(conf.tz).replace(microsecond=0) + else: + return datetime.datetime.now().replace(microsecond=0) + +def timeNow(): + return datetime.datetime.now().replace(microsecond=0) + +def get_timezone_offset(): + now = datetime.datetime.now(conf.tz) + offset_hours = now.utcoffset().total_seconds() / 3600 + offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60)) + return offset_formatted + +def timeNowDB(local=True): + """ + Return the current time (local or UTC) as ISO 8601 for DB storage. + Safe for SQLite, PostgreSQL, etc. + + Example local: '2025-11-04 18:09:11' + Example UTC: '2025-11-04 07:09:11' + """ + if local: + try: + if isinstance(conf.tz, datetime.tzinfo): + tz = conf.tz + elif conf.tz: + tz = ZoneInfo(conf.tz) + else: + tz = None + except Exception: + tz = None + return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S') + else: + return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S') + + +#------------------------------------------------------------------------------- +# Date and time methods +#------------------------------------------------------------------------------- + +# ------------------------------------------------------------------------------------------- +def format_date_iso(date1: str) -> str: + """Return ISO 8601 string for a date or None if empty""" + if date1 is None: + return None + dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1 + return dt.isoformat() + +# ------------------------------------------------------------------------------------------- +def format_event_date(date_str: str, event_type: str) -> str: + """Format event date with fallback rules.""" + if date_str: + return format_date(date_str) + elif event_type == "": + return "" + else: + return "" + +# ------------------------------------------------------------------------------------------- +def ensure_datetime(dt: Union[str, datetime.datetime, None]) -> datetime.datetime: + if dt is None: + return timeNowTZ() + if isinstance(dt, str): + return datetime.datetime.fromisoformat(dt) + return dt + + +def parse_datetime(dt_str): + if not dt_str: + return None + try: + # Try ISO8601 first + return datetime.datetime.fromisoformat(dt_str) + except ValueError: + # Try RFC1123 / HTTP format + try: + return datetime.datetime.strptime(dt_str, '%a, %d %b %Y %H:%M:%S GMT') + except ValueError: + return None + +def format_date(date_str: str) -> str: + try: + dt = parse_datetime(date_str) + if dt.tzinfo is None: + # Set timezone if missing — change to timezone.utc if you prefer UTC + now = datetime.datetime.now(conf.tz) + dt = dt.replace(tzinfo=now.astimezone().tzinfo) + return dt.astimezone().isoformat() + except Exception: + return "invalid" + +def format_date_diff(date1, date2): + """ + Return difference between two datetimes as 'Xd HH:MM'. + Uses app timezone if datetime is naive. + date2 can be None (uses now). + """ + # Get timezone from settings + tz_name = get_setting_value("TIMEZONE") or "UTC" + tz = pytz.timezone(tz_name) + + def parse_dt(dt): + if dt is None: + return datetime.datetime.now(tz) + if isinstance(dt, str): + try: + dt_parsed = email.utils.parsedate_to_datetime(dt) + except Exception: + # fallback: parse ISO string + dt_parsed = datetime.datetime.fromisoformat(dt) + # convert naive GMT/UTC to app timezone + if dt_parsed.tzinfo is None: + dt_parsed = tz.localize(dt_parsed) + else: + dt_parsed = dt_parsed.astimezone(tz) + return dt_parsed + return dt if dt.tzinfo else tz.localize(dt) + + dt1 = parse_dt(date1) + dt2 = parse_dt(date2) + + delta = dt2 - dt1 + total_minutes = int(delta.total_seconds() // 60) + days, rem_minutes = divmod(total_minutes, 1440) # 1440 mins in a day + hours, minutes = divmod(rem_minutes, 60) + + return { + "text": f"{days}d {hours:02}:{minutes:02}", + "days": days, + "hours": hours, + "minutes": minutes, + "total_minutes": total_minutes + } \ No newline at end of file diff --git a/test/api_endpoints/test_dbquery_endpoints.py b/test/api_endpoints/test_dbquery_endpoints.py index a9f663ad..22ed05f2 100644 --- a/test/api_endpoints/test_dbquery_endpoints.py +++ b/test/api_endpoints/test_dbquery_endpoints.py @@ -6,7 +6,8 @@ import pytest INSTALL_PATH = "/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) -from helper import get_setting_value, timeNowDB +from helper import get_setting_value +from utils.datetime_utils import timeNowDB from api_server.api_server_start import app diff --git a/test/api_endpoints/test_events_endpoints.py b/test/api_endpoints/test_events_endpoints.py index b3060d00..57cc519e 100644 --- a/test/api_endpoints/test_events_endpoints.py +++ b/test/api_endpoints/test_events_endpoints.py @@ -10,7 +10,8 @@ from datetime import datetime, timedelta INSTALL_PATH = "/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) -from helper import timeNowTZ, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowTZ from api_server.api_server_start import app @pytest.fixture(scope="session") diff --git a/test/api_endpoints/test_sessions_endpoints.py b/test/api_endpoints/test_sessions_endpoints.py index 5529ab98..59db6fc4 100644 --- a/test/api_endpoints/test_sessions_endpoints.py +++ b/test/api_endpoints/test_sessions_endpoints.py @@ -10,7 +10,8 @@ from datetime import datetime, timedelta INSTALL_PATH = "/app" sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) -from helper import timeNowDB, timeNowTZ, get_setting_value +from helper import get_setting_value +from utils.datetime_utils import timeNowTZ, timeNowDB from api_server.api_server_start import app @pytest.fixture(scope="session")