chore: enforce strict typing and internal naming conventions across web components

This commit is contained in:
2026-04-07 19:56:15 -04:00
parent 950280a97b
commit ba2faba5d5
52 changed files with 4967 additions and 934 deletions

103
GEMINI.md Normal file
View File

@@ -0,0 +1,103 @@
# DECNET (Deception Network) Project Context
DECNET is a high-fidelity honeypot framework designed to deploy heterogeneous fleets of fake machines (called **deckies**) that appear as real hosts on a local network.
## Project Overview
- **Core Purpose:** To lure, profile, and log attacker interactions within a controlled, deceptive environment.
- **Key Technology:** Linux-native container networking (MACVLAN/IPvlan) combined with Docker to give each decoy its own MAC address, IP, and realistic TCP/IP stack behavior.
- **Main Components:**
- **Deckies:** Group of containers sharing a network namespace (one base container + multiple service containers).
- **Archetypes:** Pre-defined machine profiles (e.g., `windows-workstation`, `linux-server`) that bundle services and OS fingerprints.
- **Services:** Modular honeypot plugins (SSH, SMB, RDP, etc.) built as `BaseService` subclasses.
- **OS Fingerprinting:** Sysctl-based TCP/IP stack tuning to spoof OS detection (nmap).
- **Logging Pipeline:** RFC 5424 syslog forwarding to an isolated SIEM/ELK stack.
## Technical Stack
- **Language:** Python 3.11+
- **CLI Framework:** [Typer](https://typer.tiangolo.com/)
- **Data Validation:** [Pydantic v2](https://docs.pydantic.dev/)
- **Orchestration:** Docker Engine 24+ (via Docker SDK for Python)
- **Networking:** MACVLAN (default) or IPvlan L2 (for WiFi/restricted environments).
- **Testing:** Pytest (100% pass requirement).
- **Formatting/Linting:** Ruff, Bandit (SAST), pip-audit.
## Architecture
```text
Host NIC (eth0)
└── MACVLAN Bridge
├── Decky-01 (192.168.1.10) -> [Base] + [SSH] + [HTTP]
├── Decky-02 (192.168.1.11) -> [Base] + [SMB] + [RDP]
└── ...
```
- **Base Container:** Owns the IP/MAC, sets `sysctls` for OS spoofing, and runs `sleep infinity`.
- **Service Containers:** Use `network_mode: service:<base>` to share the identity and networking of the base container.
- **Isolation:** Decoy traffic is strictly separated from the logging network.
## Key Commands
### Development & Maintenance
- **Install (Dev):**
- `rm .venv -rf`
- `python3 -m venv .venv`
- `source .venv/bin/activate`
- `pip install -e .`
- **Run Tests:** `pytest` (Run before any commit)
- **Linting:** `ruff check .`
- **Security Scan:** `bandit -r decnet/`
- **Web Git:** git.resacachile.cl (Gitea)
### CLI Usage
- **List Services:** `decnet services`
- **List Archetypes:** `decnet archetypes`
- **Dry Run (Compose Gen):** `decnet deploy --deckies 3 --randomize-services --dry-run`
- **Deploy (Full):** `sudo .venv/bin/decnet deploy --interface eth0 --deckies 5 --randomize-services`
- **Status:** `decnet status`
- **Teardown:** `sudo .venv/bin/decnet teardown --all`
## Development Conventions
- **Code Style:**
- Strict adherence to Ruff/PEP8.
- **Always use typed variables**. If any non-types variables are found, they must be corrected.
- The correct way is `x: int = 1`, never `x : int = 1`.
- If assignment is present, always use a space between the type and the equal sign `x: int = 1`.
- **Never** use lowercase L (l), uppercase o (O) or uppercase i (i) in single-character names.
- **Internal vars are to be declared with an underscore** (_internal_variable_name).
- **Internal to internal vars are to be declared with double underscore** (__internal_variable_name).
- Always use snake_case for code.
- Always use PascalCase for classes and generics.
- **Testing:** New features MUST include a `pytest` case. 100% test pass rate is mandatory before merging.
- **Plugin System:**
- New services go in `decnet/services/<name>.py`.
- Subclass `decnet.services.base.BaseService`.
- The registry uses auto-discovery; no manual registration required.
- **Configuration:**
- Use Pydantic models in `decnet/config.py` for any new settings.
- INI file parsing is handled in `decnet/ini_loader.py`.
- **State Management:**
- Runtime state is persisted in `decnet-state.json`.
- Do not modify this file manually.
- **General Development Guidelines**:
- **Never** commit broken code.
- **No matter how small** the changes, they must be committed.
- **If new features are addedd** new tests must be added, too.
- **Never present broken code to the user**. Test, validate, then present.
- **Extensive testing** for every function must be created.
- **Always develop in the `dev` branch, never in `main`.**
- **Test in the `testing` branch.**
## Directory Structure
- `decnet/`: Main source code.
- `services/`: Honeypot service implementations.
- `logging/`: Syslog formatting and forwarding logic.
- `correlation/`: (In Progress) Logic for grouping attacker events.
- `templates/`: Dockerfiles and entrypoint scripts for services.
- `tests/`: Pytest suite.
- `pyproject.toml`: Dependency and entry point definitions.
- `CLAUDE.md`: Claude-specific environment guidance.
- `DEVELOPMENT.md`: Roadmap and TODOs.

BIN
decnet.db Normal file

Binary file not shown.

31
decnet.json Normal file
View File

@@ -0,0 +1,31 @@
{"timestamp": "2026-04-07 19:48:29", "decky": "decky-webmail", "service": "smtp", "event_type": "startup", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:48:29.520153+00:00 decky-webmail smtp - startup - SMTP server starting as decky-webmail"}
{"timestamp": "2026-04-07 19:48:29", "decky": "decky-webmail", "service": "imap", "event_type": "startup", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:48:29.525953+00:00 decky-webmail imap - startup - IMAP server starting as decky-webmail"}
{"timestamp": "2026-04-07 19:48:29", "decky": "decky-webmail", "service": "pop3", "event_type": "startup", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:48:29.531525+00:00 decky-webmail pop3 - startup - POP3 server starting as decky-webmail"}
{"timestamp": "2026-04-07 19:48:29", "decky": "decky-webmail", "service": "http", "event_type": "startup", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:48:29.562070+00:00 decky-webmail http - startup - HTTP server starting as decky-webmail"}
{"timestamp": "2026-04-07 19:53:05", "decky": "decky-webmail", "service": "pop3", "event_type": "connect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:05.202133+00:00 decky-webmail pop3 - connect [decnet@55555 src=\"192.168.1.5\" src_port=\"56394\"]"}
{"timestamp": "2026-04-07 19:53:05", "decky": "decky-webmail", "service": "smtp", "event_type": "connect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:05.202095+00:00 decky-webmail smtp - connect [decnet@55555 src=\"192.168.1.5\" src_port=\"44836\"]"}
{"timestamp": "2026-04-07 19:53:05", "decky": "decky-webmail", "service": "imap", "event_type": "connect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:05.202120+00:00 decky-webmail imap - connect [decnet@55555 src=\"192.168.1.5\" src_port=\"49892\"]"}
{"timestamp": "2026-04-07 19:53:05", "decky": "decky-webmail", "service": "smtp", "event_type": "disconnect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:05.204537+00:00 decky-webmail smtp - disconnect [decnet@55555 src=\"192.168.1.5\"]"}
{"timestamp": "2026-04-07 19:53:11", "decky": "decky-webmail", "service": "imap", "event_type": "command", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:11.208384+00:00 decky-webmail imap - command [decnet@55555 src=\"192.168.1.5\" cmd=\"GET / HTTP/1.0\"]"}
{"timestamp": "2026-04-07 19:53:11", "decky": "decky-webmail", "service": "pop3", "event_type": "command", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:11.208384+00:00 decky-webmail pop3 - command [decnet@55555 src=\"192.168.1.5\" cmd=\"\"]"}
{"timestamp": "2026-04-07 19:53:11", "decky": "decky-webmail", "service": "pop3", "event_type": "command", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:11.208646+00:00 decky-webmail pop3 - command [decnet@55555 src=\"192.168.1.5\" cmd=\"\"]"}
{"timestamp": "2026-04-07 19:53:11", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:11.208787+00:00 decky-webmail http - request [decnet@55555 method=\"GET\" path=\"/\" remote_addr=\"192.168.1.5\" headers=\"{}\" body=\"\"]"}
{"timestamp": "2026-04-07 19:53:16", "decky": "decky-webmail", "service": "pop3", "event_type": "disconnect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:16.213731+00:00 decky-webmail pop3 - disconnect [decnet@55555 src=\"192.168.1.5\"]"}
{"timestamp": "2026-04-07 19:53:16", "decky": "decky-webmail", "service": "imap", "event_type": "disconnect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:16.213827+00:00 decky-webmail imap - disconnect [decnet@55555 src=\"192.168.1.5\"]"}
{"timestamp": "2026-04-07 19:53:16", "decky": "decky-webmail", "service": "pop3", "event_type": "connect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:16.214094+00:00 decky-webmail pop3 - connect [decnet@55555 src=\"192.168.1.5\" src_port=\"51296\"]"}
{"timestamp": "2026-04-07 19:53:16", "decky": "decky-webmail", "service": "imap", "event_type": "connect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:16.214133+00:00 decky-webmail imap - connect [decnet@55555 src=\"192.168.1.5\" src_port=\"50426\"]"}
{"timestamp": "2026-04-07 19:53:16", "decky": "decky-webmail", "service": "pop3", "event_type": "command", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:16.214228+00:00 decky-webmail pop3 - command [decnet@55555 src=\"192.168.1.5\" cmd=\"OPTIONS / HTTP/1.0\"]"}
{"timestamp": "2026-04-07 19:53:16", "decky": "decky-webmail", "service": "pop3", "event_type": "command", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:16.214301+00:00 decky-webmail pop3 - command [decnet@55555 src=\"192.168.1.5\" cmd=\"\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "imap", "event_type": "disconnect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:21.219340+00:00 decky-webmail imap - disconnect [decnet@55555 src=\"192.168.1.5\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "pop3", "event_type": "disconnect", "attacker_ip": "192.168.1.5", "raw_line": "<134>1 2026-04-07T19:53:21.219334+00:00 decky-webmail pop3 - disconnect [decnet@55555 src=\"192.168.1.5\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:21.222956+00:00 decky-webmail http - request [decnet@55555 method=\"GET\" path=\"/\" remote_addr=\"192.168.1.5\" headers=\"{}\" body=\"\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:21.223266+00:00 decky-webmail http - request [decnet@55555 method=\"POST\" path=\"/sdk\" remote_addr=\"192.168.1.5\" headers=\"{'Host': '192.168.1.110', 'Connection': 'close', 'Content-Length': '441', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}\" body=\"<soap:Envelope xmlns:xsd=\\\"http://www.w3.org/2001/XMLSchema\\\" xmlns:xsi=\\\"http://www.w3.org/2001/XMLSchema-instance\\\" xmlns:soap=\\\"http://schemas.xmlsoap.org/soap/envelope/\\\"><soap:Header><operationID>00000001-00000001</operationID></soap:Header><soap:Body><RetrieveServiceContent xmlns=\\\"urn:internalvim25\\\"><_this xsi:type=\\\"ManagedObjectReference\\\" type=\\\"ServiceInstance\\\">ServiceInstance</_this></RetrieveServiceContent></soap:Body></soap:Envelope>\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:21.223437+00:00 decky-webmail http - request [decnet@55555 method=\"GET\" path=\"/nmaplowercheck1775591601\" remote_addr=\"192.168.1.5\" headers=\"{'Host': '192.168.1.110', 'Connection': 'close', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}\" body=\"\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:21.224651+00:00 decky-webmail http - request [decnet@55555 method=\"GET\" path=\"/NmapUpperCheck1775591601\" remote_addr=\"192.168.1.5\" headers=\"{'Host': '192.168.1.110', 'Connection': 'close', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}\" body=\"\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:21.225177+00:00 decky-webmail http - request [decnet@55555 method=\"GET\" path=\"/Nmap/folder/check1775591601\" remote_addr=\"192.168.1.5\" headers=\"{'Host': '192.168.1.110', 'Connection': 'close', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}\" body=\"\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:21.225909+00:00 decky-webmail http - request [decnet@55555 method=\"GET\" path=\"/\" remote_addr=\"192.168.1.5\" headers=\"{}\" body=\"\"]"}
{"timestamp": "2026-04-07 19:53:21", "decky": "decky-webmail", "service": "http", "event_type": "request", "attacker_ip": "Unknown", "raw_line": "<134>1 2026-04-07T19:53:21.226287+00:00 decky-webmail http - request [decnet@55555 method=\"GET\" path=\"/\" remote_addr=\"192.168.1.5\" headers=\"{'Host': '192.168.1.110'}\" body=\"\"]"}
{"timestamp": "2026-04-07 20:24:03", "decky": "decky-webmail", "service": "smtp", "event_type": "startup", "attacker_ip": "Unknown", "fields": "{}", "msg": "SMTP server starting as decky-webmail", "raw_line": "<134>1 2026-04-07T20:24:03.279897+00:00 decky-webmail smtp - startup - SMTP server starting as decky-webmail"}
{"timestamp": "2026-04-07 20:24:03", "decky": "decky-webmail", "service": "imap", "event_type": "startup", "attacker_ip": "Unknown", "fields": "{}", "msg": "IMAP server starting as decky-webmail", "raw_line": "<134>1 2026-04-07T20:24:03.279954+00:00 decky-webmail imap - startup - IMAP server starting as decky-webmail"}
{"timestamp": "2026-04-07 20:24:03", "decky": "decky-webmail", "service": "pop3", "event_type": "startup", "attacker_ip": "Unknown", "fields": "{}", "msg": "POP3 server starting as decky-webmail", "raw_line": "<134>1 2026-04-07T20:24:03.283256+00:00 decky-webmail pop3 - startup - POP3 server starting as decky-webmail"}
{"timestamp": "2026-04-07 20:24:03", "decky": "decky-webmail", "service": "http", "event_type": "startup", "attacker_ip": "Unknown", "fields": "{}", "msg": "HTTP server starting as decky-webmail", "raw_line": "<134>1 2026-04-07T20:24:03.297543+00:00 decky-webmail http - startup - HTTP server starting as decky-webmail"}

View File

@@ -157,3 +157,69 @@
<134>1 2026-04-04T07:41:33.751197+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd="/"] <134>1 2026-04-04T07:41:33.751197+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd="/"]
<134>1 2026-04-04T07:41:33.751245+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"] <134>1 2026-04-04T07:41:33.751245+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-04T07:41:33.751285+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"] <134>1 2026-04-04T07:41:33.751285+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-04T07:41:33.751337+00:00 decky-fileserv ftp - connection [decnet@55555 src_ip="192.168.1.5" src_port="44606"]
<134>1 2026-04-04T07:41:33.751704+00:00 decky-fileserv ftp - disconnect [decnet@55555 src_ip="192.168.1.5" src_port="44606"]
<134>1 2026-04-04T07:41:33.751814+00:00 decky-fileserv ftp - connection [decnet@55555 src_ip="192.168.1.5" src_port="44614"]
<134>1 2026-04-04T07:41:33.751968+00:00 decky-fileserv ftp - connection [decnet@55555 src_ip="192.168.1.5" src_port="44630"]
<134>1 2026-04-04T07:41:33.752086+00:00 decky-fileserv ftp - disconnect [decnet@55555 src_ip="192.168.1.5" src_port="44630"]
<134>1 2026-04-04T07:41:33.752162+00:00 decky-fileserv ftp - disconnect [decnet@55555 src_ip="192.168.1.5" src_port="44614"]
<134>1 2026-04-07T19:34:47.857994+00:00 decky-webmail imap - startup - IMAP server starting as decky-webmail
<134>1 2026-04-07T19:34:47.861786+00:00 decky-webmail smtp - startup - SMTP server starting as decky-webmail
<134>1 2026-04-07T19:34:47.863785+00:00 decky-webmail pop3 - startup - POP3 server starting as decky-webmail
<134>1 2026-04-07T19:34:47.928625+00:00 decky-webmail http - startup - HTTP server starting as decky-webmail
<134>1 2026-04-07T19:35:17.828230+00:00 decky-webmail imap - startup - IMAP server starting as decky-webmail
<134>1 2026-04-07T19:35:17.855831+00:00 decky-webmail smtp - startup - SMTP server starting as decky-webmail
<134>1 2026-04-07T19:35:17.860387+00:00 decky-webmail pop3 - startup - POP3 server starting as decky-webmail
<134>1 2026-04-07T19:35:17.879879+00:00 decky-webmail http - startup - HTTP server starting as decky-webmail
<134>1 2026-04-07T19:40:42.159239+00:00 decky-webmail imap - startup - IMAP server starting as decky-webmail
<134>1 2026-04-07T19:40:42.170590+00:00 decky-webmail smtp - startup - SMTP server starting as decky-webmail
<134>1 2026-04-07T19:40:42.174154+00:00 decky-webmail pop3 - startup - POP3 server starting as decky-webmail
<134>1 2026-04-07T19:40:42.219612+00:00 decky-webmail http - startup - HTTP server starting as decky-webmail
<134>1 2026-04-07T19:41:33.471561+00:00 decky-webmail imap - connect [decnet@55555 src="192.168.1.5" src_port="37206"]
<134>1 2026-04-07T19:41:33.471553+00:00 decky-webmail pop3 - connect [decnet@55555 src="192.168.1.5" src_port="47186"]
<134>1 2026-04-07T19:41:33.471534+00:00 decky-webmail smtp - connect [decnet@55555 src="192.168.1.5" src_port="34592"]
<134>1 2026-04-07T19:41:33.474811+00:00 decky-webmail smtp - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:41:39.477713+00:00 decky-webmail imap - command [decnet@55555 src="192.168.1.5" cmd="GET / HTTP/1.0"]
<134>1 2026-04-07T19:41:39.477712+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd=""]
<134>1 2026-04-07T19:41:39.477920+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd=""]
<134>1 2026-04-07T19:41:39.477994+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/" remote_addr="192.168.1.5" headers="{}" body=""]
<134>1 2026-04-07T19:41:44.483244+00:00 decky-webmail imap - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:41:44.483352+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:41:44.483592+00:00 decky-webmail pop3 - connect [decnet@55555 src="192.168.1.5" src_port="47662"]
<134>1 2026-04-07T19:41:44.483583+00:00 decky-webmail imap - connect [decnet@55555 src="192.168.1.5" src_port="33210"]
<134>1 2026-04-07T19:41:44.483686+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd="OPTIONS / HTTP/1.0"]
<134>1 2026-04-07T19:41:44.483727+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd=""]
<134>1 2026-04-07T19:41:49.488857+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:41:49.488842+00:00 decky-webmail imap - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:41:49.492686+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/" remote_addr="192.168.1.5" headers="{}" body=""]
<134>1 2026-04-07T19:41:49.493022+00:00 decky-webmail http - request [decnet@55555 method="POST" path="/sdk" remote_addr="192.168.1.5" headers="{'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)', 'Connection': 'close', 'Content-Length': '441', 'Host': '192.168.1.110'}" body="<soap:Envelope xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Header><operationID>00000001-00000001</operationID></soap:Header><soap:Body><RetrieveServiceContent xmlns=\"urn:internalvim25\"><_this xsi:type=\"ManagedObjectReference\" type=\"ServiceInstance\">ServiceInstance</_this></RetrieveServiceContent></soap:Body></soap:Envelope>"]
<134>1 2026-04-07T19:41:49.493181+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/nmaplowercheck1775590909" remote_addr="192.168.1.5" headers="{'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)', 'Connection': 'close', 'Host': '192.168.1.110'}" body=""]
<134>1 2026-04-07T19:41:49.494290+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/NmapUpperCheck1775590909" remote_addr="192.168.1.5" headers="{'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)', 'Connection': 'close', 'Host': '192.168.1.110'}" body=""]
<134>1 2026-04-07T19:41:49.494718+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/Nmap/folder/check1775590909" remote_addr="192.168.1.5" headers="{'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)', 'Connection': 'close', 'Host': '192.168.1.110'}" body=""]
<134>1 2026-04-07T19:41:49.495656+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/" remote_addr="192.168.1.5" headers="{}" body=""]
<134>1 2026-04-07T19:41:49.496032+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/" remote_addr="192.168.1.5" headers="{'Host': '192.168.1.110'}" body=""]
<134>1 2026-04-07T19:48:29.520153+00:00 decky-webmail smtp - startup - SMTP server starting as decky-webmail
<134>1 2026-04-07T19:48:29.525953+00:00 decky-webmail imap - startup - IMAP server starting as decky-webmail
<134>1 2026-04-07T19:48:29.531525+00:00 decky-webmail pop3 - startup - POP3 server starting as decky-webmail
<134>1 2026-04-07T19:48:29.562070+00:00 decky-webmail http - startup - HTTP server starting as decky-webmail
<134>1 2026-04-07T19:53:05.202133+00:00 decky-webmail pop3 - connect [decnet@55555 src="192.168.1.5" src_port="56394"]
<134>1 2026-04-07T19:53:05.202095+00:00 decky-webmail smtp - connect [decnet@55555 src="192.168.1.5" src_port="44836"]
<134>1 2026-04-07T19:53:05.202120+00:00 decky-webmail imap - connect [decnet@55555 src="192.168.1.5" src_port="49892"]
<134>1 2026-04-07T19:53:05.204537+00:00 decky-webmail smtp - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:53:11.208384+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd=""]
<134>1 2026-04-07T19:53:11.208384+00:00 decky-webmail imap - command [decnet@55555 src="192.168.1.5" cmd="GET / HTTP/1.0"]
<134>1 2026-04-07T19:53:11.208646+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd=""]
<134>1 2026-04-07T19:53:11.208787+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/" remote_addr="192.168.1.5" headers="{}" body=""]
<134>1 2026-04-07T19:53:16.213731+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:53:16.213827+00:00 decky-webmail imap - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:53:16.214094+00:00 decky-webmail pop3 - connect [decnet@55555 src="192.168.1.5" src_port="51296"]
<134>1 2026-04-07T19:53:16.214133+00:00 decky-webmail imap - connect [decnet@55555 src="192.168.1.5" src_port="50426"]
<134>1 2026-04-07T19:53:16.214228+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd="OPTIONS / HTTP/1.0"]
<134>1 2026-04-07T19:53:16.214301+00:00 decky-webmail pop3 - command [decnet@55555 src="192.168.1.5" cmd=""]
<134>1 2026-04-07T19:53:21.219340+00:00 decky-webmail imap - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:53:21.219334+00:00 decky-webmail pop3 - disconnect [decnet@55555 src="192.168.1.5"]
<134>1 2026-04-07T19:53:21.222956+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/" remote_addr="192.168.1.5" headers="{}" body=""]
<134>1 2026-04-07T19:53:21.223266+00:00 decky-webmail http - request [decnet@55555 method="POST" path="/sdk" remote_addr="192.168.1.5" headers="{'Host': '192.168.1.110', 'Connection': 'close', 'Content-Length': '441', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}" body="<soap:Envelope xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Header><operationID>00000001-00000001</operationID></soap:Header><soap:Body><RetrieveServiceContent xmlns=\"urn:internalvim25\"><_this xsi:type=\"ManagedObjectReference\" type=\"ServiceInstance\">ServiceInstance</_this></RetrieveServiceContent></soap:Body></soap:Envelope>"]
<134>1 2026-04-07T19:53:21.223437+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/nmaplowercheck1775591601" remote_addr="192.168.1.5" headers="{'Host': '192.168.1.110', 'Connection': 'close', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}" body=""]
<134>1 2026-04-07T19:53:21.224651+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/NmapUpperCheck1775591601" remote_addr="192.168.1.5" headers="{'Host': '192.168.1.110', 'Connection': 'close', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}" body=""]
<134>1 2026-04-07T19:53:21.225177+00:00 decky-webmail http - request [decnet@55555 method="GET" path="/Nmap/folder/check1775591601" remote_addr="192.168.1.5" headers="{'Host': '192.168.1.110', 'Connection': 'close', 'User-Agent': 'Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)'}" body=""]

View File

@@ -353,12 +353,12 @@ def deploy(
import subprocess import subprocess
import sys import sys
console.print(f"[green]Starting DECNET API on port {api_port}...[/]") console.print(f"[green]Starting DECNET API on port {api_port}...[/]")
env = os.environ.copy() _env: dict[str, str] = os.environ.copy()
env["DECNET_INGEST_LOG_FILE"] = effective_log_file _env["DECNET_INGEST_LOG_FILE"] = str(effective_log_file)
try: try:
subprocess.Popen( subprocess.Popen(
[sys.executable, "-m", "uvicorn", "decnet.web.api:app", "--host", "0.0.0.0", "--port", str(api_port)], [sys.executable, "-m", "uvicorn", "decnet.web.api:app", "--host", "0.0.0.0", "--port", str(api_port)],
env=env, env=_env,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT stderr=subprocess.STDOUT
) )

View File

@@ -1,7 +1,7 @@
import uuid import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import timedelta from datetime import timedelta
from typing import Any, AsyncGenerator from typing import Any, AsyncGenerator, Optional
import jwt import jwt
from fastapi import Depends, FastAPI, HTTPException, Query, status from fastapi import Depends, FastAPI, HTTPException, Query, status
@@ -22,7 +22,7 @@ from decnet.web.ingester import log_ingestion_worker
import asyncio import asyncio
repo: SQLiteRepository = SQLiteRepository() repo: SQLiteRepository = SQLiteRepository()
ingestion_task: asyncio.Task | None = None ingestion_task: Optional[asyncio.Task[Any]] = None
@asynccontextmanager @asynccontextmanager
@@ -30,8 +30,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
global ingestion_task global ingestion_task
await repo.initialize() await repo.initialize()
# Create default admin if no users exist # Create default admin if no users exist
admin_user: dict[str, Any] | None = await repo.get_user_by_username("admin") _admin_user: Optional[dict[str, Any]] = await repo.get_user_by_username("admin")
if not admin_user: if not _admin_user:
await repo.create_user( await repo.create_user(
{ {
"uuid": str(uuid.uuid4()), "uuid": str(uuid.uuid4()),
@@ -71,19 +71,19 @@ oauth2_scheme: OAuth2PasswordBearer = OAuth2PasswordBearer(tokenUrl="/api/v1/aut
async def get_current_user(token: str = Depends(oauth2_scheme)) -> str: async def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
credentials_exception = HTTPException( _credentials_exception: HTTPException = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials", detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
payload: dict[str, Any] = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) _payload: dict[str, Any] = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_uuid: str | None = payload.get("uuid") _user_uuid: Optional[str] = _payload.get("uuid")
if user_uuid is None: if _user_uuid is None:
raise credentials_exception raise _credentials_exception
return _user_uuid
except jwt.PyJWTError: except jwt.PyJWTError:
raise credentials_exception raise _credentials_exception
return user_uuid
class Token(BaseModel): class Token(BaseModel):
@@ -111,37 +111,37 @@ class LogsResponse(BaseModel):
@app.post("/api/v1/auth/login", response_model=Token) @app.post("/api/v1/auth/login", response_model=Token)
async def login(request: LoginRequest) -> dict[str, Any]: async def login(request: LoginRequest) -> dict[str, Any]:
user: dict[str, Any] | None = await repo.get_user_by_username(request.username) _user: Optional[dict[str, Any]] = await repo.get_user_by_username(request.username)
if not user or not verify_password(request.password, user["password_hash"]): if not _user or not verify_password(request.password, _user["password_hash"]):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token_expires: timedelta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) _access_token_expires: timedelta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# Token uses uuid instead of sub # Token uses uuid instead of sub
access_token: str = create_access_token( _access_token: str = create_access_token(
data={"uuid": user["uuid"]}, expires_delta=access_token_expires data={"uuid": _user["uuid"]}, expires_delta=_access_token_expires
) )
return { return {
"access_token": access_token, "access_token": _access_token,
"token_type": "bearer", "token_type": "bearer",
"must_change_password": bool(user.get("must_change_password", False)) "must_change_password": bool(_user.get("must_change_password", False))
} }
@app.post("/api/v1/auth/change-password") @app.post("/api/v1/auth/change-password")
async def change_password(request: ChangePasswordRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: async def change_password(request: ChangePasswordRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]:
user: dict[str, Any] | None = await repo.get_user_by_uuid(current_user) _user: Optional[dict[str, Any]] = await repo.get_user_by_uuid(current_user)
if not user or not verify_password(request.old_password, user["password_hash"]): if not _user or not verify_password(request.old_password, _user["password_hash"]):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect old password", detail="Incorrect old password",
) )
new_hash = get_password_hash(request.new_password) _new_hash: str = get_password_hash(request.new_password)
await repo.update_user_password(current_user, new_hash, must_change_password=False) await repo.update_user_password(current_user, _new_hash, must_change_password=False)
return {"message": "Password updated successfully"} return {"message": "Password updated successfully"}
@@ -149,16 +149,16 @@ async def change_password(request: ChangePasswordRequest, current_user: str = De
async def get_logs( async def get_logs(
limit: int = Query(50, ge=1, le=1000), limit: int = Query(50, ge=1, le=1000),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
search: str | None = None, search: Optional[str] = None,
current_user: str = Depends(get_current_user) current_user: str = Depends(get_current_user)
) -> dict[str, Any]: ) -> dict[str, Any]:
logs: list[dict[str, Any]] = await repo.get_logs(limit=limit, offset=offset, search=search) _logs: list[dict[str, Any]] = await repo.get_logs(limit=limit, offset=offset, search=search)
total: int = await repo.get_total_logs(search=search) _total: int = await repo.get_total_logs(search=search)
return { return {
"total": total, "total": _total,
"limit": limit, "limit": limit,
"offset": offset, "offset": offset,
"data": logs "data": _logs
} }

View File

@@ -18,20 +18,20 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
# Use a cost factor of 12 (default for passlib/bcrypt) # Use a cost factor of 12 (default for passlib/bcrypt)
salt = bcrypt.gensalt(rounds=12) _salt: bytes = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode("utf-8"), salt) _hashed: bytes = bcrypt.hashpw(password.encode("utf-8"), _salt)
return hashed.decode("utf-8") return _hashed.decode("utf-8")
def create_access_token(data: dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: def create_access_token(data: dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
to_encode: dict[str, Any] = data.copy() _to_encode: dict[str, Any] = data.copy()
expire: datetime _expire: datetime
if expires_delta: if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta _expire = datetime.now(timezone.utc) + expires_delta
else: else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15) _expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire}) _to_encode.update({"exp": _expire})
to_encode.update({"iat": datetime.now(timezone.utc)}) _to_encode.update({"iat": datetime.now(timezone.utc)})
encoded_jwt: str = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) _encoded_jwt: str = jwt.encode(_to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt return _encoded_jwt

View File

@@ -7,62 +7,62 @@ from pathlib import Path
from decnet.web.repository import BaseRepository from decnet.web.repository import BaseRepository
logger = logging.getLogger("decnet.web.ingester") logger: logging.Logger = logging.getLogger("decnet.web.ingester")
async def log_ingestion_worker(repo: BaseRepository) -> None: async def log_ingestion_worker(repo: BaseRepository) -> None:
""" """
Background task that tails the DECNET_INGEST_LOG_FILE.json and Background task that tails the DECNET_INGEST_LOG_FILE.json and
inserts structured JSON logs into the SQLite repository. inserts structured JSON logs into the SQLite repository.
""" """
base_log_file = os.environ.get("DECNET_INGEST_LOG_FILE") _base_log_file: str | None = os.environ.get("DECNET_INGEST_LOG_FILE")
if not base_log_file: if not _base_log_file:
logger.warning("DECNET_INGEST_LOG_FILE not set. Log ingestion disabled.") logger.warning("DECNET_INGEST_LOG_FILE not set. Log ingestion disabled.")
return return
json_log_path = Path(base_log_file).with_suffix(".json") _json_log_path: Path = Path(_base_log_file).with_suffix(".json")
position = 0 _position: int = 0
logger.info(f"Starting JSON log ingestion from {json_log_path}") logger.info(f"Starting JSON log ingestion from {_json_log_path}")
while True: while True:
try: try:
if not json_log_path.exists(): if not _json_log_path.exists():
await asyncio.sleep(2) await asyncio.sleep(2)
continue continue
stat = json_log_path.stat() _stat: os.stat_result = _json_log_path.stat()
if stat.st_size < position: if _stat.st_size < _position:
# File rotated or truncated # File rotated or truncated
position = 0 _position = 0
if stat.st_size == position: if _stat.st_size == _position:
# No new data # No new data
await asyncio.sleep(1) await asyncio.sleep(1)
continue continue
with open(json_log_path, "r", encoding="utf-8", errors="replace") as f: with open(_json_log_path, "r", encoding="utf-8", errors="replace") as _f:
f.seek(position) _f.seek(_position)
while True: while True:
line = f.readline() _line: str = _f.readline()
if not line: if not _line:
break # EOF reached break # EOF reached
if not line.endswith('\n'): if not _line.endswith('\n'):
# Partial line read, don't process yet, don't advance position # Partial line read, don't process yet, don't advance position
break break
try: try:
log_data = json.loads(line.strip()) _log_data: dict[str, Any] = json.loads(_line.strip())
await repo.add_log(log_data) await repo.add_log(_log_data)
except json.JSONDecodeError: except json.JSONDecodeError:
logger.error(f"Failed to decode JSON log line: {line}") logger.error(f"Failed to decode JSON log line: {_line}")
continue continue
# Update position after successful line read # Update position after successful line read
position = f.tell() _position = _f.tell()
except Exception as e: except Exception as _e:
logger.error(f"Error in log ingestion worker: {e}") logger.error(f"Error in log ingestion worker: {_e}")
await asyncio.sleep(5) await asyncio.sleep(5)
await asyncio.sleep(1) await asyncio.sleep(1)

View File

@@ -10,9 +10,9 @@ class SQLiteRepository(BaseRepository):
self.db_path: str = db_path self.db_path: str = db_path
async def initialize(self) -> None: async def initialize(self) -> None:
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
# Logs table # Logs table
await db.execute(""" await _db.execute("""
CREATE TABLE IF NOT EXISTS logs ( CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -26,15 +26,15 @@ class SQLiteRepository(BaseRepository):
) )
""") """)
try: try:
await db.execute("ALTER TABLE logs ADD COLUMN fields TEXT") await _db.execute("ALTER TABLE logs ADD COLUMN fields TEXT")
except aiosqlite.OperationalError: except aiosqlite.OperationalError:
pass pass
try: try:
await db.execute("ALTER TABLE logs ADD COLUMN msg TEXT") await _db.execute("ALTER TABLE logs ADD COLUMN msg TEXT")
except aiosqlite.OperationalError: except aiosqlite.OperationalError:
pass pass
# Users table (internal RBAC) # Users table (internal RBAC)
await db.execute(""" await _db.execute("""
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
uuid TEXT PRIMARY KEY, uuid TEXT PRIMARY KEY,
username TEXT UNIQUE, username TEXT UNIQUE,
@@ -44,19 +44,19 @@ class SQLiteRepository(BaseRepository):
) )
""") """)
try: try:
await db.execute("ALTER TABLE users ADD COLUMN must_change_password BOOLEAN DEFAULT 0") await _db.execute("ALTER TABLE users ADD COLUMN must_change_password BOOLEAN DEFAULT 0")
except aiosqlite.OperationalError: except aiosqlite.OperationalError:
pass # Column already exists pass # Column already exists
await db.commit() await _db.commit()
async def add_log(self, log_data: dict[str, Any]) -> None: async def add_log(self, log_data: dict[str, Any]) -> None:
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
timestamp = log_data.get("timestamp") _timestamp: Any = log_data.get("timestamp")
if timestamp: if _timestamp:
await db.execute( await _db.execute(
"INSERT INTO logs (timestamp, decky, service, event_type, attacker_ip, raw_line, fields, msg) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "INSERT INTO logs (timestamp, decky, service, event_type, attacker_ip, raw_line, fields, msg) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
( (
timestamp, _timestamp,
log_data.get("decky"), log_data.get("decky"),
log_data.get("service"), log_data.get("service"),
log_data.get("event_type"), log_data.get("event_type"),
@@ -67,7 +67,7 @@ class SQLiteRepository(BaseRepository):
) )
) )
else: else:
await db.execute( await _db.execute(
"INSERT INTO logs (decky, service, event_type, attacker_ip, raw_line, fields, msg) VALUES (?, ?, ?, ?, ?, ?, ?)", "INSERT INTO logs (decky, service, event_type, attacker_ip, raw_line, fields, msg) VALUES (?, ?, ?, ?, ?, ?, ?)",
( (
log_data.get("decky"), log_data.get("decky"),
@@ -79,7 +79,7 @@ class SQLiteRepository(BaseRepository):
log_data.get("msg") log_data.get("msg")
) )
) )
await db.commit() await _db.commit()
async def get_logs( async def get_logs(
self, self,
@@ -87,74 +87,74 @@ class SQLiteRepository(BaseRepository):
offset: int = 0, offset: int = 0,
search: Optional[str] = None search: Optional[str] = None
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
query: str = "SELECT * FROM logs" _query: str = "SELECT * FROM logs"
params: list[Any] = [] _params: list[Any] = []
if search: if search:
query += " WHERE raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?" _query += " WHERE raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?"
like_val = f"%{search}%" _like_val: str = f"%{search}%"
params.extend([like_val, like_val, like_val, like_val]) _params.extend([_like_val, _like_val, _like_val, _like_val])
query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?" _query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?"
params.extend([limit, offset]) _params.extend([limit, offset])
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
db.row_factory = aiosqlite.Row _db.row_factory = aiosqlite.Row
async with db.execute(query, params) as cursor: async with _db.execute(_query, _params) as _cursor:
rows = await cursor.fetchall() _rows: list[aiosqlite.Row] = await _cursor.fetchall()
return [dict(row) for row in rows] return [dict(_row) for _row in _rows]
async def get_total_logs(self, search: Optional[str] = None) -> int: async def get_total_logs(self, search: Optional[str] = None) -> int:
query: str = "SELECT COUNT(*) as total FROM logs" _query: str = "SELECT COUNT(*) as total FROM logs"
params: list[Any] = [] _params: list[Any] = []
if search: if search:
query += " WHERE raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?" _query += " WHERE raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?"
like_val = f"%{search}%" _like_val: str = f"%{search}%"
params.extend([like_val, like_val, like_val, like_val]) _params.extend([_like_val, _like_val, _like_val, _like_val])
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
db.row_factory = aiosqlite.Row _db.row_factory = aiosqlite.Row
async with db.execute(query, params) as cursor: async with _db.execute(_query, _params) as _cursor:
row = await cursor.fetchone() _row: Optional[aiosqlite.Row] = await _cursor.fetchone()
return row["total"] if row else 0 return _row["total"] if _row else 0
async def get_stats_summary(self) -> dict[str, Any]: async def get_stats_summary(self) -> dict[str, Any]:
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
db.row_factory = aiosqlite.Row _db.row_factory = aiosqlite.Row
async with db.execute("SELECT COUNT(*) as total_logs FROM logs") as cursor: async with _db.execute("SELECT COUNT(*) as total_logs FROM logs") as _cursor:
row = await cursor.fetchone() _row: Optional[aiosqlite.Row] = await _cursor.fetchone()
total_logs: int = row["total_logs"] if row else 0 _total_logs: int = _row["total_logs"] if _row else 0
async with db.execute("SELECT COUNT(DISTINCT attacker_ip) as unique_attackers FROM logs") as cursor: async with _db.execute("SELECT COUNT(DISTINCT attacker_ip) as unique_attackers FROM logs") as _cursor:
row = await cursor.fetchone() _row = await _cursor.fetchone()
unique_attackers: int = row["unique_attackers"] if row else 0 _unique_attackers: int = _row["unique_attackers"] if _row else 0
async with db.execute("SELECT COUNT(DISTINCT decky) as active_deckies FROM logs") as cursor: async with _db.execute("SELECT COUNT(DISTINCT decky) as active_deckies FROM logs") as _cursor:
row = await cursor.fetchone() _row = await _cursor.fetchone()
active_deckies: int = row["active_deckies"] if row else 0 _active_deckies: int = _row["active_deckies"] if _row else 0
return { return {
"total_logs": total_logs, "total_logs": _total_logs,
"unique_attackers": unique_attackers, "unique_attackers": _unique_attackers,
"active_deckies": active_deckies "active_deckies": _active_deckies
} }
async def get_user_by_username(self, username: str) -> Optional[dict[str, Any]]: async def get_user_by_username(self, username: str) -> Optional[dict[str, Any]]:
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
db.row_factory = aiosqlite.Row _db.row_factory = aiosqlite.Row
async with db.execute("SELECT * FROM users WHERE username = ?", (username,)) as cursor: async with _db.execute("SELECT * FROM users WHERE username = ?", (username,)) as _cursor:
row = await cursor.fetchone() _row: Optional[aiosqlite.Row] = await _cursor.fetchone()
return dict(row) if row else None return dict(_row) if _row else None
async def get_user_by_uuid(self, uuid: str) -> Optional[dict[str, Any]]: async def get_user_by_uuid(self, uuid: str) -> Optional[dict[str, Any]]:
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
db.row_factory = aiosqlite.Row _db.row_factory = aiosqlite.Row
async with db.execute("SELECT * FROM users WHERE uuid = ?", (uuid,)) as cursor: async with _db.execute("SELECT * FROM users WHERE uuid = ?", (uuid,)) as _cursor:
row = await cursor.fetchone() _row: Optional[aiosqlite.Row] = await _cursor.fetchone()
return dict(row) if row else None return dict(_row) if _row else None
async def create_user(self, user_data: dict[str, Any]) -> None: async def create_user(self, user_data: dict[str, Any]) -> None:
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
await db.execute( await _db.execute(
"INSERT INTO users (uuid, username, password_hash, role, must_change_password) VALUES (?, ?, ?, ?, ?)", "INSERT INTO users (uuid, username, password_hash, role, must_change_password) VALUES (?, ?, ?, ?, ?)",
( (
user_data["uuid"], user_data["uuid"],
@@ -164,12 +164,12 @@ class SQLiteRepository(BaseRepository):
user_data.get("must_change_password", False) user_data.get("must_change_password", False)
) )
) )
await db.commit() await _db.commit()
async def update_user_password(self, uuid: str, password_hash: str, must_change_password: bool = False) -> None: async def update_user_password(self, uuid: str, password_hash: str, must_change_password: bool = False) -> None:
async with aiosqlite.connect(self.db_path) as db: async with aiosqlite.connect(self.db_path) as _db:
await db.execute( await _db.execute(
"UPDATE users SET password_hash = ?, must_change_password = ? WHERE uuid = ?", "UPDATE users SET password_hash = ?, must_change_password = ? WHERE uuid = ?",
(password_hash, must_change_password, uuid) (password_hash, must_change_password, uuid)
) )
await db.commit() await _db.commit()

24
temp_web/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
temp_web/README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
temp_web/eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
temp_web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>temp_web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2969
temp_web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
temp_web/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "temp_web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.4"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
temp_web/public/icons.svg Normal file
View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

184
temp_web/src/App.css Normal file
View File

@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

121
temp_web/src/App.tsx Normal file
View File

@@ -0,0 +1,121 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
</p>
</div>
<button
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div className="ticks"></div>
<section id="spacer"></section>
</>
)
}
export default App

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

111
temp_web/src/index.css Normal file
View File

@@ -0,0 +1,111 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}

10
temp_web/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
temp_web/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
temp_web/vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

View File

@@ -150,6 +150,7 @@ def _get_json_logger() -> logging.Logger:
def write_syslog_file(line: str) -> None: def write_syslog_file(line: str) -> None:
"""Append a syslog line to the rotating log file.""" """Append a syslog line to the rotating log file."""
try: try:
@@ -159,8 +160,9 @@ def write_syslog_file(line: str) -> None:
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
_RFC5424_RE = re.compile( _RFC5424_RE: re.Pattern = re.compile(
r"^<\d+>1 " r"^<\d+>1 "
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
@@ -169,55 +171,61 @@ def write_syslog_file(line: str) -> None:
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG r"(.+)$", # 5: SD element + optional MSG
) )
_SD_BLOCK_RE = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
m = _RFC5424_RE.match(line) _m: Optional[re.Match] = _RFC5424_RE.match(line)
if m: if _m:
ts_raw, decky, service, event_type, sd_rest = m.groups() _ts_raw: str
_decky: str
_service: str
_event_type: str
_sd_rest: str
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
fields = {} _fields: dict[str, str] = {}
msg = "" _msg: str = ""
if sd_rest.startswith("-"): if _sd_rest.startswith("-"):
msg = sd_rest[1:].lstrip() _msg = _sd_rest[1:].lstrip()
elif sd_rest.startswith("["): elif _sd_rest.startswith("["):
block = _SD_BLOCK_RE.search(sd_rest) _block: Optional[re.Match] = _SD_BLOCK_RE.search(_sd_rest)
if block: if _block:
for k, v in _PARAM_RE.findall(block.group(1)): for _k, _v in _PARAM_RE.findall(_block.group(1)):
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
# extract msg after the block # extract msg after the block
msg_match = re.search(r'\]\s+(.+)$', sd_rest) _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
if msg_match: if _msg_match:
msg = msg_match.group(1).strip() _msg = _msg_match.group(1).strip()
else: else:
msg = sd_rest _msg = _sd_rest
attacker_ip = "Unknown" _attacker_ip: str = "Unknown"
for fname in _IP_FIELDS: for _fname in _IP_FIELDS:
if fname in fields: if _fname in _fields:
attacker_ip = fields[fname] _attacker_ip = _fields[_fname]
break break
# Parse timestamp to normalize it # Parse timestamp to normalize it
_ts_formatted: str
try: try:
ts = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S") _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
ts = ts_raw _ts_formatted = _ts_raw
payload = { _payload: dict[str, Any] = {
"timestamp": ts, "timestamp": _ts_formatted,
"decky": decky, "decky": _decky,
"service": service, "service": _service,
"event_type": event_type, "event_type": _event_type,
"attacker_ip": attacker_ip, "attacker_ip": _attacker_ip,
"fields": json.dumps(fields), "fields": json.dumps(_fields),
"msg": msg, "msg": _msg,
"raw_line": line "raw_line": line
} }
_get_json_logger().info(json.dumps(payload)) _get_json_logger().info(json.dumps(_payload))
except Exception: except Exception:
pass pass

1
test.log Normal file
View File

@@ -0,0 +1 @@
<134>1 2026-04-07T19:44:50.140680+00:00 decky-01 ssh - login_attempt [decnet@55555 src_ip="192.168.1.100"] Auth failed