The docker build contexts and syslog_bridge.py lived at repo root, which meant setuptools (include = ["decnet*"]) never shipped them. Agents installed via `pip install $RELEASE_DIR` got site-packages/decnet/** but no templates/, so every deploy blew up in deployer._sync_logging_helper with FileNotFoundError on templates/syslog_bridge.py. Move templates/ -> decnet/templates/ and declare it as setuptools package-data. Path resolutions in services/*.py and engine/deployer.py drop one .parent since templates now lives beside the code. Test fixtures, bandit exclude path, and coverage omit glob updated to match.
144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MSSQL (TDS)server.
|
|
Reads TDS pre-login and login7 packets, extracts username, responds with
|
|
a login failed error. Logs auth attempts as JSON.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import struct
|
|
from syslog_bridge import syslog_line, write_syslog_file, forward_syslog
|
|
|
|
NODE_NAME = os.environ.get("NODE_NAME", "dbserver")
|
|
SERVICE_NAME = "mssql"
|
|
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
|
|
|
_PRELOGIN_RESP = bytes([
|
|
0x04, 0x01, 0x00, 0x2f, 0x00, 0x00, 0x01, 0x00, # TDS header type=4, status=1, len=47
|
|
# 0. VERSION option
|
|
0x00, 0x00, 0x1a, 0x00, 0x06,
|
|
# 1. ENCRYPTION option
|
|
0x01, 0x00, 0x20, 0x00, 0x01,
|
|
# 2. INSTOPT
|
|
0x02, 0x00, 0x21, 0x00, 0x01,
|
|
# 3. THREADID
|
|
0x03, 0x00, 0x22, 0x00, 0x04,
|
|
# 4. MARS
|
|
0x04, 0x00, 0x26, 0x00, 0x01,
|
|
# TERMINATOR
|
|
0xff,
|
|
# version data: 14.0.2000
|
|
0x0e, 0x00, 0x07, 0xd0, 0x00, 0x00,
|
|
# encryption: NOT_SUP
|
|
0x02,
|
|
# instopt
|
|
0x00,
|
|
# thread id
|
|
0x00, 0x00, 0x00, 0x00,
|
|
# mars
|
|
0x00,
|
|
])
|
|
|
|
|
|
|
|
|
|
def _log(event_type: str, severity: int = 6, **kwargs) -> None:
|
|
line = syslog_line(SERVICE_NAME, NODE_NAME, event_type, severity, **kwargs)
|
|
write_syslog_file(line)
|
|
forward_syslog(line, LOG_TARGET)
|
|
|
|
|
|
def _tds_error_packet(message: str) -> bytes:
|
|
msg_enc = message.encode("utf-16-le")
|
|
# Token type 0xAA = ERROR, followed by length, error number, state, class, msg_len, msg
|
|
token = (
|
|
b"\xaa"
|
|
+ struct.pack("<H", 4 + 1 + 1 + 2 + len(msg_enc) + 1 + 1 + 1 + 1 + 4)
|
|
+ struct.pack("<I", 18456) # SQL error number: login failed
|
|
+ b"\x01" # state
|
|
+ b"\x0e" # class
|
|
+ struct.pack("<H", len(message))
|
|
+ msg_enc
|
|
+ b"\x00" # server name length
|
|
+ b"\x00" # proc name length
|
|
+ struct.pack("<I", 1) # line number
|
|
)
|
|
done = b"\xfd\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
payload = token + done
|
|
header = struct.pack(">BBHBBBB", 0x04, 0x01, len(payload) + 8, 0x00, 0x00, 0x01, 0x00)
|
|
return header + payload
|
|
|
|
|
|
class MSSQLProtocol(asyncio.Protocol):
|
|
def __init__(self):
|
|
self._transport = None
|
|
self._peer = None
|
|
self._buf = b""
|
|
self._prelogin_done = False
|
|
|
|
def connection_made(self, transport):
|
|
self._transport = transport
|
|
self._peer = transport.get_extra_info("peername", ("?", 0))
|
|
_log("connect", src=self._peer[0], src_port=self._peer[1])
|
|
|
|
def data_received(self, data):
|
|
self._buf += data
|
|
while len(self._buf) >= 8:
|
|
pkt_type = self._buf[0]
|
|
pkt_len = struct.unpack(">H", self._buf[2:4])[0]
|
|
if pkt_len < 8:
|
|
_log("unknown_packet", src=self._peer[0], pkt_type=hex(pkt_type))
|
|
self._transport.close()
|
|
self._buf = b""
|
|
return
|
|
if len(self._buf) < pkt_len:
|
|
break
|
|
payload = self._buf[8:pkt_len]
|
|
self._buf = self._buf[pkt_len:]
|
|
self._handle_packet(pkt_type, payload)
|
|
if self._transport.is_closing():
|
|
self._buf = b""
|
|
break
|
|
|
|
def _handle_packet(self, pkt_type: int, payload: bytes):
|
|
if pkt_type == 0x12: # Pre-login
|
|
self._transport.write(_PRELOGIN_RESP)
|
|
self._prelogin_done = True
|
|
elif pkt_type == 0x10: # Login7
|
|
username = self._parse_login7_username(payload)
|
|
_log("auth", src=self._peer[0], username=username)
|
|
self._transport.write(_tds_error_packet("Login failed for user."))
|
|
self._transport.close()
|
|
else:
|
|
_log("unknown_packet", src=self._peer[0], pkt_type=hex(pkt_type))
|
|
self._transport.close()
|
|
|
|
def _parse_login7_username(self, payload: bytes) -> str:
|
|
try:
|
|
# Login7 layout: fixed header 36 bytes, then offsets
|
|
# Username offset at bytes 36-37, length at 38-39
|
|
if len(payload) < 40:
|
|
return "<short_packet>"
|
|
offset = struct.unpack("<H", payload[36:38])[0]
|
|
length = struct.unpack("<H", payload[38:40])[0]
|
|
username = payload[offset:offset + length * 2].decode("utf-16-le", errors="replace")
|
|
return username
|
|
except Exception:
|
|
return "<parse_error>"
|
|
|
|
def connection_lost(self, exc):
|
|
_log("disconnect", src=self._peer[0] if self._peer else "?")
|
|
|
|
|
|
async def main():
|
|
_log("startup", msg=f"MSSQL server starting as {NODE_NAME}")
|
|
loop = asyncio.get_running_loop()
|
|
server = await loop.create_server(MSSQLProtocol, "0.0.0.0", 1433) # nosec B104
|
|
async with server:
|
|
await server.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|