This commit is contained in:
Jokob @NetAlertX
2026-05-24 22:08:06 +00:00
5 changed files with 154 additions and 47 deletions

View File

@@ -8,7 +8,7 @@ This includes (but is not limited to):
- Running NetAlertX only on networks where you have legal authorization
- Keeping your deployment up to date with the latest patches
> NetAlertX is not responsible for misuse, misconfiguration, or unsecure deployments. Always test and secure your setup before exposing it to the outside world.
> NetAlertX is not responsible for misuse, misconfiguration, or insecure deployments. Always test and secure your setup before exposing it to the outside world. Users interacting with the UI are treated as trusted actors within the deployment model.
# 🔐 Securing Your NetAlertX Instance
@@ -36,7 +36,7 @@ NetAlertX is designed to be run on **private LANs**, not the open internet.
### ✅ Tailscale (Easy VPN Alternative)
Tailscale sets up a private mesh network between your devices. It's fast to configure and ideal for NetAlertX.
Tailscale sets up a private mesh network between your devices. It's fast to configure and ideal for NetAlertX.
👉 [Get started with Tailscale](https://tailscale.com/)
---
@@ -63,19 +63,19 @@ By default, NetAlertX does **not** require login. Before exposing the UI in any
## 🔥 Additional Security Measures
- **Firewall / Network Rules**
- **Firewall / Network Rules**
Restrict UI/API access to trusted IPs only.
- **Limit Docker Capabilities**
- **Limit Docker Capabilities**
Avoid `--privileged`. Use `--cap-add=NET_RAW` and others **only if required** by your scan method.
- **Keep NetAlertX Updated**
- **Keep NetAlertX Updated**
Regular updates contain bug fixes and security patches.
- **Plugin Permissions**
- **Plugin Permissions**
Disable unused plugins. Only install from trusted sources.
- **Use Read-Only API Keys**
- **Use Read-Only API Keys**
When integrating NetAlertX with other tools, scope keys tightly.
---

View File

@@ -284,7 +284,6 @@ def main():
return 0
# ------------------------------------------------------------------
# Data retrieval methods
api_endpoints = [
"/sync", # New Python-based endpoint
@@ -293,39 +292,87 @@ api_endpoints = [
# send data to the HUB
def send_data(api_token, file_content, encryption_key, file_path, node_name, pref, hub_url):
"""Send encrypted data to HUB, preferring /sync endpoint and falling back to PHP version."""
encrypted_data = encrypt_data(file_content, encryption_key)
mylog('verbose', [f'[{pluginName}] Sending encrypted_data: "{encrypted_data}"'])
"""
Sends encrypted plugin output from NODE → HUB.
Flow:
1. Encrypt plugin output locally
2. Build payload (data + metadata)
3. Try each configured HUB endpoint in order
4. On success (200) → stop immediately
5. On failure → log HUB response + continue fallback
6. If all endpoints fail → alert user
"""
# STEP 1: Encrypt raw plugin output before transmission
encrypted_data = encrypt_data(file_content, encryption_key)
mylog('verbose', [f"[{pluginName}] Encrypted payload prepared type={type(encrypted_data).__name__}"])
# STEP 2: Build request payload for HUB sync API
data = {
'data': encrypted_data,
'file_path': file_path,
'plugin': pref,
'node_name': node_name
}
headers = {'Authorization': f'Bearer {api_token}'}
headers = {
'Authorization': f'Bearer {api_token}'
}
# STEP 3: Attempt delivery to each configured endpoint
for endpoint in api_endpoints:
final_endpoint = hub_url + endpoint
try:
response = requests.post(final_endpoint, json=data, headers=headers, timeout=5)
mylog('verbose', [f'[{pluginName}] Tried endpoint: {final_endpoint}, status: {response.status_code}'])
# STEP 4: Send request to HUB sync endpoint
response = requests.post(
final_endpoint,
json=data,
headers=headers,
timeout=5
)
# STEP 5a: Success path (HUB accepted payload)
if response.status_code == 200:
message = f'[{pluginName}] Data for "{file_path}" sent successfully via {final_endpoint}'
message = (f'[{pluginName}] Sync success for "{file_path}" via {final_endpoint}')
mylog('verbose', [message])
write_notification(message, 'info', timeNowUTC())
return True
except requests.RequestException as e:
mylog('verbose', [f'[{pluginName}] Error calling {final_endpoint}: {e}'])
# STEP 5b: HUB returned error (e.g. 500, 400)
try:
response_json = response.json()
except Exception:
response_json = {}
# If all endpoints fail
message = f'[{pluginName}] Failed to send data for "{file_path}" via all endpoints'
mylog('verbose', [message])
# Extract best available error message
error_msg = (
response_json.get("error") or response_json.get("message") or response.text
)
msg = (f'[{pluginName}] HUB error on {final_endpoint} [{response.status_code}]: {error_msg}')
mylog('none', [msg])
write_notification(msg, 'alert', timeNowUTC())
mylog('verbose', [f'[{pluginName}] Endpoint attempted: {final_endpoint} status={response.status_code}'])
except requests.RequestException as e:
# STEP 5c: Network-level failure (timeout, DNS, etc.)
mylog('verbose', [f'[{pluginName}] Request exception calling {final_endpoint} error={type(e).__name__}: {e}'])
# STEP 6: All endpoints failed → final fallback alert
message = (
f'[{pluginName}] All HUB endpoints failed for "{file_path}"'
)
mylog('none', [message])
write_notification(message, 'alert', timeNowUTC())
return False

View File

@@ -1,3 +1,4 @@
import json
import os
import base64
from flask import jsonify, request
@@ -13,7 +14,7 @@ lggr = Logger(get_setting_value('LOG_LEVEL'))
def handle_sync_get():
"""Handle GET requests for SYNC (NODE → HUB)."""
# get all dwevices from the api endpoint
# get all devices from the api endpoint
api_path = os.environ.get('NETALERTX_API', '/tmp/api')
file_path = f"/{api_path}/table_devices.json"
@@ -45,40 +46,100 @@ def handle_sync_get():
def handle_sync_post():
"""Handle POST requests for SYNC (HUB receiving from NODE)."""
body = request.get_json(silent=True) or {}
mylog("debug", [
"[SYNC API] ENTER handle_sync_post",
f"method={request.method}",
f"content_type={request.content_type}",
f"content_length={request.content_length}",
f"remote_addr={request.remote_addr}"
])
# ---- RAW BODY (critical for debugging encoding / encryption issues)
try:
raw = request.get_data(cache=False)
mylog("debug", [
f"[SYNC API] raw_bytes_len={len(raw)} raw_preview={raw[:200]}"
])
except Exception as e:
mylog("none", [f"[SYNC API] FAILED reading raw body: {e}"])
write_notification("[SYNC API] FAILED reading raw body - see app.log", 'alert', timeNowUTC())
return jsonify({"error": "failed reading body"}), 400
# ---- JSON PARSE (from already-read raw bytes to avoid empty-stream re-read)
try:
body = json.loads(raw)
mylog("debug", [f"[SYNC API] parsed_json={body}"])
except Exception as e:
msg = f"[SYNC API] JSON_PARSE_FAILED={e}"
mylog("none", [msg])
write_notification(msg, 'alert', timeNowUTC())
return jsonify({"error": "invalid json"}), 400
# ---- EXTRACT FIELDS
data = body.get("data", "")
node_name = body.get("node_name", "")
plugin = body.get("plugin", "")
mylog("debug", [
f"[SYNC API] node_name={repr(node_name)} plugin={repr(plugin)} data_type={type(data).__name__} data_len={len(data) if isinstance(data, str) else 'non-string'}"
])
storage_path = os.getenv("NETALERTX_PLUGINS_LOG", "/tmp/log/plugins")
os.makedirs(storage_path, exist_ok=True)
encoded_files = [
f
for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.encoded.{node_name}")
]
decoded_files = [
f
for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.decoded.{node_name}")
]
file_count = len(encoded_files + decoded_files) + 1
file_path_new = os.path.join(
storage_path, f"last_result.{plugin}.encoded.{node_name}.{file_count}.log"
)
try:
os.makedirs(storage_path, exist_ok=True)
mylog("debug", [f"[SYNC API] storage_path_ready={storage_path}"])
except Exception as e:
msg = f"[SYNC API] MKDIR_FAILED={e}"
mylog("none", [msg])
write_notification(msg, 'alert', timeNowUTC())
return jsonify({"error": "storage path error"}), 500
# ---- FILE COUNT LOGIC
try:
encoded_files = [
f for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.encoded.{node_name}")
]
decoded_files = [
f for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.decoded.{node_name}")
]
file_count = len(encoded_files + decoded_files) + 1
mylog("debug", [f"[SYNC API] encoded_files={len(encoded_files)} decoded_files={len(decoded_files)} file_count={file_count}"])
except Exception as e:
msg = f"[SYNC API] LISTDIR_FAILED={e}"
mylog("none", [msg])
write_notification(msg, 'alert', timeNowUTC())
return jsonify({"error": "listdir failed"}), 500
# ---- FILE PATH
file_path_new = os.path.join(
storage_path,
f"last_result.{plugin}.encoded.{node_name}.{file_count}.log"
)
mylog("verbose", [f"[SYNC API] file_path_new={file_path_new}"])
try:
if not isinstance(data, str):
data = str(data)
with open(file_path_new, "w") as f:
f.write(data)
except Exception as e:
msg = f"[Plugin: SYNC] Failed to store data: {e}"
write_notification(msg, "alert", timeNowUTC())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
msg = f"[Plugin: SYNC] Data write failed ({file_path_new}): {e}"
mylog("none", [msg])
write_notification(msg, 'alert', timeNowUTC())
return jsonify({"error": str(e)}), 500
msg = f"[Plugin: SYNC] Data received ({file_path_new})"
write_notification(msg, "info", timeNowUTC())
if lggr.isAbove('verbose'):
write_notification(msg, 'info', timeNowUTC())
mylog("verbose", [msg])
return jsonify({"message": "Data received and stored successfully"}), 200

View File

@@ -824,8 +824,7 @@ def process_plugin_events(db, plugin, plugEventsArr):
# find corresponding object for the event and merge
if plugObj.idsHash == tmpObjFromEvent.idsHash:
if (
plugObj.status == "missing-in-last-scan"
or tmpObjFromEvent.status == "watched-changed"
plugObj.status == "missing-in-last-scan" or tmpObjFromEvent.status == "watched-changed"
):
changed_this_cycle.add(tmpObjFromEvent.idsHash)
pluginObjects[index] = combine_plugin_objects(

View File

@@ -6,7 +6,7 @@ import datetime
import re
import pytz
from typing import Union, Optional
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from zoneinfo import ZoneInfo
import email.utils
import conf
# from const import *