Skip to content

Commit 813a92d

Browse files
committed
BE: SYNC API
2 parents e28ec5c + cc5fc0c commit 813a92d

5 files changed

Lines changed: 69 additions & 47 deletions

File tree

docs/SECURITY.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This includes (but is not limited to):
88
- Running NetAlertX only on networks where you have legal authorization
99
- Keeping your deployment up to date with the latest patches
1010

11-
> 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. Always properly secure and isolate your deployment before exposing it externally.
11+
> 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.
1212
1313
# 🔐 Securing Your NetAlertX Instance
1414

@@ -36,7 +36,7 @@ NetAlertX is designed to be run on **private LANs**, not the open internet.
3636

3737
### ✅ Tailscale (Easy VPN Alternative)
3838

39-
Tailscale sets up a private mesh network between your devices. It's fast to configure and ideal for NetAlertX.
39+
Tailscale sets up a private mesh network between your devices. It's fast to configure and ideal for NetAlertX.
4040
👉 [Get started with Tailscale](https://tailscale.com/)
4141

4242
---
@@ -63,19 +63,19 @@ By default, NetAlertX does **not** require login. Before exposing the UI in any
6363

6464
## 🔥 Additional Security Measures
6565

66-
- **Firewall / Network Rules**
66+
- **Firewall / Network Rules**
6767
Restrict UI/API access to trusted IPs only.
6868

69-
- **Limit Docker Capabilities**
69+
- **Limit Docker Capabilities**
7070
Avoid `--privileged`. Use `--cap-add=NET_RAW` and others **only if required** by your scan method.
7171

72-
- **Keep NetAlertX Updated**
72+
- **Keep NetAlertX Updated**
7373
Regular updates contain bug fixes and security patches.
7474

75-
- **Plugin Permissions**
75+
- **Plugin Permissions**
7676
Disable unused plugins. Only install from trusted sources.
7777

78-
- **Use Read-Only API Keys**
78+
- **Use Read-Only API Keys**
7979
When integrating NetAlertX with other tools, scope keys tightly.
8080

8181
---

front/plugins/sync/sync.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,7 @@ def main():
194194
# last_result.office.encoded.lab.log
195195
# from being incorrectly parsed as PUSH artifacts.
196196
is_push_artifact = (
197-
('.decoded.' in file_name or '.encoded.' in file_name)
198-
and file_name.rsplit('.', 2)[1].isdigit()
197+
('.decoded.' in file_name or '.encoded.' in file_name) and file_name.rsplit('.', 2)[1].isdigit()
199198
)
200199

201200
if is_push_artifact:

server/api_server/sync_endpoint.py

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import base64
34
from flask import jsonify, request
@@ -6,16 +7,14 @@
67
from utils.datetime_utils import timeNowUTC
78
from messaging.in_app import write_notification
89

9-
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
10-
1110
# Make sure log level is initialized correctly
1211
lggr = Logger(get_setting_value('LOG_LEVEL'))
1312

1413

1514
def handle_sync_get():
1615
"""Handle GET requests for SYNC (NODE → HUB)."""
1716

18-
# get all dwevices from the api endpoint
17+
# get all devices from the api endpoint
1918
api_path = os.environ.get('NETALERTX_API', '/tmp/api')
2019

2120
file_path = f"/{api_path}/table_devices.json"
@@ -48,7 +47,7 @@ def handle_sync_get():
4847
def handle_sync_post():
4948
"""Handle POST requests for SYNC (HUB receiving from NODE)."""
5049

51-
mylog("verbose", [
50+
mylog("debug", [
5251
"[SYNC API] ENTER handle_sync_post",
5352
f"method={request.method}",
5453
f"content_type={request.content_type}",
@@ -59,41 +58,42 @@ def handle_sync_post():
5958
# ---- RAW BODY (critical for debugging encoding / encryption issues)
6059
try:
6160
raw = request.get_data(cache=False)
62-
mylog("verbose", [
63-
f"[SYNC API] raw_bytes_len={len(raw)}",
64-
f"[SYNC API] raw_preview={raw[:200]}"
61+
mylog("debug", [
62+
f"[SYNC API] raw_bytes_len={len(raw)} raw_preview={raw[:200]}"
6563
])
6664
except Exception as e:
6765
mylog("none", [f"[SYNC API] FAILED reading raw body: {e}"])
66+
write_notification("[SYNC API] FAILED reading raw body - see app.log", 'alert', timeNowUTC())
6867
return jsonify({"error": "failed reading body"}), 400
6968

70-
# ---- JSON PARSE (this is a very common failure point)
69+
# ---- JSON PARSE (from already-read raw bytes to avoid empty-stream re-read)
7170
try:
72-
body = request.get_json(force=False, silent=False)
73-
mylog("verbose", [f"[SYNC API] parsed_json={body}"])
71+
body = json.loads(raw)
72+
mylog("debug", [f"[SYNC API] parsed_json={body}"])
7473
except Exception as e:
75-
mylog("none", [f"[SYNC API] JSON_PARSE_FAILED={e}"])
74+
msg = f"[SYNC API] JSON_PARSE_FAILED={e}"
75+
mylog("none", [msg])
76+
write_notification(msg, 'alert', timeNowUTC())
7677
return jsonify({"error": "invalid json"}), 400
7778

7879
# ---- EXTRACT FIELDS
7980
data = body.get("data", "")
8081
node_name = body.get("node_name", "")
8182
plugin = body.get("plugin", "")
8283

83-
mylog("verbose", [
84-
f"[SYNC API] node_name={repr(node_name)}",
85-
f"[SYNC API] plugin={repr(plugin)}",
86-
f"[SYNC API] data_type={type(data).__name__}",
87-
f"[SYNC API] data_len={len(data) if isinstance(data, str) else 'non-string'}"
84+
mylog("debug", [
85+
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'}"
8886
])
8987

90-
storage_path = INSTALL_PATH + "/log/plugins"
88+
storage_path = os.getenv("NETALERTX_PLUGINS_LOG", "/tmp/log/plugins")
9189

9290
try:
9391
os.makedirs(storage_path, exist_ok=True)
94-
mylog("verbose", [f"[SYNC API] storage_path_ready={storage_path}"])
92+
mylog("debug", [f"[SYNC API] storage_path_ready={storage_path}"])
9593
except Exception as e:
96-
mylog("none", [f"[SYNC API] MKDIR_FAILED={e}"])
94+
msg = f"[SYNC API] MKDIR_FAILED={e}"
95+
mylog("none", [msg])
96+
write_notification(msg, 'alert', timeNowUTC())
9797
return jsonify({"error": "storage path error"}), 500
9898

9999
# ---- FILE COUNT LOGIC
@@ -108,13 +108,11 @@ def handle_sync_post():
108108
]
109109
file_count = len(encoded_files + decoded_files) + 1
110110

111-
mylog("verbose", [
112-
f"[SYNC API] encoded_files={len(encoded_files)}",
113-
f"[SYNC API] decoded_files={len(decoded_files)}",
114-
f"[SYNC API] file_count={file_count}"
115-
])
111+
mylog("debug", [f"[SYNC API] encoded_files={len(encoded_files)} decoded_files={len(decoded_files)} file_count={file_count}"])
116112
except Exception as e:
117-
mylog("none", [f"[SYNC API] LISTDIR_FAILED={e}"])
113+
msg = f"[SYNC API] LISTDIR_FAILED={e}"
114+
mylog("none", [msg])
115+
write_notification(msg, 'alert', timeNowUTC())
118116
return jsonify({"error": "listdir failed"}), 500
119117

120118
# ---- FILE PATH
@@ -125,7 +123,6 @@ def handle_sync_post():
125123

126124
mylog("verbose", [f"[SYNC API] file_path_new={file_path_new}"])
127125

128-
# ---- WRITE FILE (final critical point)
129126
try:
130127
if not isinstance(data, str):
131128
data = str(data)
@@ -134,15 +131,15 @@ def handle_sync_post():
134131
f.write(data)
135132

136133
except Exception as e:
137-
import traceback
138-
mylog("none", [
139-
f"[SYNC API] WRITE_FAILED={e}",
140-
traceback.format_exc()
141-
])
134+
135+
msg = f"[Plugin: SYNC] Data write failed ({file_path_new}): {e}"
136+
mylog("none", [msg])
137+
write_notification(msg, 'alert', timeNowUTC())
142138
return jsonify({"error": str(e)}), 500
143139

144140
msg = f"[Plugin: SYNC] Data received ({file_path_new})"
145-
write_notification(msg, "info", timeNowUTC())
141+
if lggr.isAbove('verbose'):
142+
write_notification(msg, 'info', timeNowUTC())
146143
mylog("verbose", [msg])
147144

148145
return jsonify({"message": "Data received and stored successfully"}), 200

server/plugin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -824,8 +824,7 @@ def process_plugin_events(db, plugin, plugEventsArr):
824824
# find corresponding object for the event and merge
825825
if plugObj.idsHash == tmpObjFromEvent.idsHash:
826826
if (
827-
plugObj.status == "missing-in-last-scan"
828-
or tmpObjFromEvent.status == "watched-changed"
827+
plugObj.status == "missing-in-last-scan" or tmpObjFromEvent.status == "watched-changed"
829828
):
830829
changed_this_cycle.add(tmpObjFromEvent.idsHash)
831830
pluginObjects[index] = combine_plugin_objects(

test/plugins/test_sync_protocol.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,18 @@ def _node_name_from_filename(file_name: str) -> str:
7171
"""Mirror of the node-name extraction in sync.main() (Mode 3).
7272
7373
Real file formats produced by the system:
74-
PUSH (post-decode): last_result.PLUGIN.decoded.NodeName.N.log → parts[3]
75-
PULL: last_result.NodeName.log → parts[1]
74+
PUSH (post-decode): last_result.PLUGIN.decoded.NodeName.N.log
75+
— split on '.decoded.' marker, strip .N.log with rsplit from the right
76+
PULL: last_result.NodeName.log
77+
— strip 'last_result.' prefix and '.log' suffix
78+
79+
Both forms handle dots anywhere in PLUGIN or NodeName.
7680
"""
77-
parts = file_name.split(".")
78-
return parts[3] if ("decoded" in file_name or "encoded" in file_name) else parts[1]
81+
if '.decoded.' in file_name or '.encoded.' in file_name:
82+
marker = '.decoded.' if '.decoded.' in file_name else '.encoded.'
83+
_, after = file_name.split(marker, 1)
84+
return after.rsplit('.', 2)[0]
85+
return file_name[len('last_result.'):-len('.log')]
7986

8087

8188
def _should_delete_after_process(filename: str) -> bool:
@@ -339,6 +346,26 @@ def test_push_decoded_different_plugins(self):
339346
assert _node_name_from_filename(fname) == "HubNode", \
340347
f"Expected 'HubNode' from {fname}"
341348

349+
# --- dot-in-identifier regression (fragile parts[3] fix) ---
350+
351+
def test_pull_node_name_with_dots(self):
352+
# PULL mode: node name set to e.g. "node.home" or an IP like "192.168.1.82"
353+
assert _node_name_from_filename("last_result.node.home.log") == "node.home"
354+
assert _node_name_from_filename("last_result.192.168.1.82.log") == "192.168.1.82"
355+
356+
def test_push_decoded_node_name_with_dots(self):
357+
# Node name "Node.Vlan01" must survive the filename round-trip intact
358+
assert _node_name_from_filename("last_result.ARPSCAN.decoded.Node.Vlan01.1.log") == "Node.Vlan01"
359+
360+
def test_push_decoded_plugin_name_with_dots(self):
361+
# Hypothetical plugin with a dot in its name must not shift the node index
362+
assert _node_name_from_filename("last_result.MY.PLUGIN.decoded.NodeA.1.log") == "NodeA"
363+
364+
def test_push_both_identifiers_with_dots(self):
365+
assert _node_name_from_filename(
366+
"last_result.A.B.decoded.x.y.z.1.log"
367+
) == "x.y.z"
368+
342369

343370
# ===========================================================================
344371
# CurrentScan candidates filter (Mode 3 – RECEIVE)

0 commit comments

Comments
 (0)