remctl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - Initial release
6
+ - Core crypto: Ed25519 identity keys, X25519 ECDH handshake, AES-256-GCM transport encryption
7
+ - Wire protocol: pydantic-based message schemas, length-prefixed framing, replay protection
8
+ - LAN discovery: UDP broadcast beacon + peer table
9
+ - Node daemon: async TCP server, command dispatch, audit logging, rate limiting
10
+ - CLI client: all commands (lock, shutdown, restart, sleep, logout, closefocused,
11
+ exec, screenshot, push, pull, status, cancel)
12
+ - Two-way pairing: both node and controller must explicitly trust each other
13
+ - Permission tiers: observe, standard, full
14
+ - Platform support: Windows (first-class), Linux (second-class), macOS (stub)
15
+ - Service installation: Windows service (pywin32), Linux systemd unit
remctl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 remctl contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
remctl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: remctl
3
+ Version: 0.1.0
4
+ Summary: Secure LAN-based remote PC control and administration
5
+ Author: remctl contributors
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: administration,lan,remote-control
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: System :: Networking
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: cryptography>=42
21
+ Requires-Dist: mss>=9.0
22
+ Requires-Dist: psutil>=5.9
23
+ Requires-Dist: pydantic>=2
24
+ Requires-Dist: pywin32>=306; sys_platform == 'win32'
25
+ Requires-Dist: rich>=13
26
+ Requires-Dist: typer>=0.12
27
+ Requires-Dist: zeroconf>=0.132
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.8; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
+ Requires-Dist: pytest-mock>=3; extra == 'dev'
32
+ Requires-Dist: pytest>=7; extra == 'dev'
33
+ Requires-Dist: ruff>=0.3; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # remctl
37
+
38
+ **Secure, LAN-based remote PC control and administration.**
39
+
40
+ `remctl` lets you control machines on your local network from the command line —
41
+ lock, shutdown, execute shell commands with streaming output, transfer files,
42
+ take screenshots, and check system status. All communication is authenticated
43
+ using Ed25519 keys and encrypted with AES-256-GCM.
44
+
45
+ > ⚠️ **Intended for your own trusted LAN devices only.** `remctl` is not designed
46
+ > for Internet-facing use. Do not expose it via port forwarding or NAT.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install remctl
52
+ ```
53
+
54
+ ### From source
55
+
56
+ ```bash
57
+ # TODO: user adds PyPI API token via env var / twine config before publishing
58
+ git clone <repo-url>
59
+ cd remctl
60
+ pip install -e ".[dev]"
61
+ ```
62
+
63
+ ## Quick Start: Pairing Walkthrough
64
+
65
+ `remctl` requires two-way trust: both the node and the controller must explicitly
66
+ authorize each other.
67
+
68
+ ### On the target machine (the node)
69
+
70
+ ```bash
71
+ # Show the node's fingerprint
72
+ remctl node pair --show
73
+
74
+ # Start the agent (background via service is recommended)
75
+ remctl node start
76
+ ```
77
+
78
+ ### On the controlling machine
79
+
80
+ ```bash
81
+ # Discover nodes on the LAN
82
+ remctl ctl discover
83
+
84
+ # Pair with a node (verify the fingerprint before confirming)
85
+ remctl ctl pair <node-ip>
86
+
87
+ # Show your own fingerprint for the node to trust you
88
+ remctl ctl pair-show
89
+ ```
90
+
91
+ ### Back on the node (trust the controller)
92
+
93
+ ```bash
94
+ # Trust the controller's public key (copy from the controller's output)
95
+ remctl node trust-controller /path/to/controller_identity.pub
96
+ ```
97
+
98
+ ### Send commands
99
+
100
+ ```bash
101
+ remctl ctl <target> status
102
+ remctl ctl <target> lock
103
+ remctl ctl <target> exec "whoami"
104
+ remctl ctl <target> screenshot -o screenshot.png
105
+ remctl ctl <target> push ./file.txt /remote/path/file.txt
106
+ remctl ctl <target> pull /remote/log.log ./log.log
107
+ ```
108
+
109
+ ## Command Reference
110
+
111
+ | Command | Description |
112
+ |---------|-------------|
113
+ | `remctl node start` | Start the node agent in foreground |
114
+ | `remctl node install-service` | Install as OS service |
115
+ | `remctl ctl discover` | Discover nodes on LAN |
116
+ | `remctl ctl pair <target>` | Pair with a node |
117
+ | `remctl ctl <target> lock` | Lock the remote workstation |
118
+ | `remctl ctl <target> shutdown --delay 60` | Shutdown with optional delay |
119
+ | `remctl ctl <target> restart` | Restart |
120
+ | `remctl ctl <target> sleep` | Sleep/Suspend |
121
+ | `remctl ctl <target> logout` | Log out current user |
122
+ | `remctl ctl <target> closefocused` | Close active window |
123
+ | `remctl ctl <target> exec <command>` | Execute shell command (streamed) |
124
+ | `remctl ctl <target> status` | Show system status (CPU/mem/disk/uptime) |
125
+ | `remctl ctl <target> screenshot -o out.png` | Capture screenshot |
126
+ | `remctl ctl <target> push <local> <remote>` | Push file to node |
127
+ | `remctl ctl <target> pull <remote> <local>` | Pull file from node |
128
+ | `remctl ctl <target> cancel` | Cancel pending shutdown/restart |
129
+
130
+ ## Security Model (Summary)
131
+
132
+ - **Identity:** Ed25519 keypairs generated on first run.
133
+ - **Authentication:** Two-way explicit pairing — both sides must trust each other.
134
+ - **Encryption:** X25519 ECDH key exchange → AES-256-GCM, with forward secrecy.
135
+ - **Replay protection:** Per-message sequence numbers + 30-second timestamp window.
136
+ - **Permission tiers:** `observe` (status/screenshot), `standard` (commands +
137
+ files), `full` (includes `exec`). Default: `standard`.
138
+ - **Audit log:** All commands logged unredacted at `~/.remctl/audit.log`.
139
+
140
+ See [docs/SECURITY.md](docs/SECURITY.md) for the full threat model.
141
+
142
+ ## Configuration
143
+
144
+ All config files live in `~/.remctl/`:
145
+
146
+ | File | Purpose |
147
+ |------|---------|
148
+ | `identity.key` | Private key (keep secure) |
149
+ | `identity.pub` | Public key |
150
+ | `known_nodes.json` | Paired nodes (ctl side) |
151
+ | `known_controllers.json` | Trusted controllers (node side) |
152
+ | `node_config.json` | Node settings (bind address, port, tiers) |
153
+ | `audit.log` | Command audit log |
154
+
155
+ ## Platform Support
156
+
157
+ | Platform | Status |
158
+ |----------|--------|
159
+ | Windows | First-class |
160
+ | Linux | Second-class (best-effort) |
161
+ | macOS | Not yet implemented |
162
+
163
+ ## Publishing to PyPI
164
+
165
+ ```bash
166
+ python -m build
167
+ twine upload dist/*
168
+ ```
169
+
170
+ > The user must configure `~/.pypirc` or set `TWINE_USERNAME`/`TWINE_PASSWORD`
171
+ > environment variables before running `twine upload`. No credentials are
172
+ > included in this repository.
remctl-0.1.0/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # remctl
2
+
3
+ **Secure, LAN-based remote PC control and administration.**
4
+
5
+ `remctl` lets you control machines on your local network from the command line —
6
+ lock, shutdown, execute shell commands with streaming output, transfer files,
7
+ take screenshots, and check system status. All communication is authenticated
8
+ using Ed25519 keys and encrypted with AES-256-GCM.
9
+
10
+ > ⚠️ **Intended for your own trusted LAN devices only.** `remctl` is not designed
11
+ > for Internet-facing use. Do not expose it via port forwarding or NAT.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install remctl
17
+ ```
18
+
19
+ ### From source
20
+
21
+ ```bash
22
+ # TODO: user adds PyPI API token via env var / twine config before publishing
23
+ git clone <repo-url>
24
+ cd remctl
25
+ pip install -e ".[dev]"
26
+ ```
27
+
28
+ ## Quick Start: Pairing Walkthrough
29
+
30
+ `remctl` requires two-way trust: both the node and the controller must explicitly
31
+ authorize each other.
32
+
33
+ ### On the target machine (the node)
34
+
35
+ ```bash
36
+ # Show the node's fingerprint
37
+ remctl node pair --show
38
+
39
+ # Start the agent (background via service is recommended)
40
+ remctl node start
41
+ ```
42
+
43
+ ### On the controlling machine
44
+
45
+ ```bash
46
+ # Discover nodes on the LAN
47
+ remctl ctl discover
48
+
49
+ # Pair with a node (verify the fingerprint before confirming)
50
+ remctl ctl pair <node-ip>
51
+
52
+ # Show your own fingerprint for the node to trust you
53
+ remctl ctl pair-show
54
+ ```
55
+
56
+ ### Back on the node (trust the controller)
57
+
58
+ ```bash
59
+ # Trust the controller's public key (copy from the controller's output)
60
+ remctl node trust-controller /path/to/controller_identity.pub
61
+ ```
62
+
63
+ ### Send commands
64
+
65
+ ```bash
66
+ remctl ctl <target> status
67
+ remctl ctl <target> lock
68
+ remctl ctl <target> exec "whoami"
69
+ remctl ctl <target> screenshot -o screenshot.png
70
+ remctl ctl <target> push ./file.txt /remote/path/file.txt
71
+ remctl ctl <target> pull /remote/log.log ./log.log
72
+ ```
73
+
74
+ ## Command Reference
75
+
76
+ | Command | Description |
77
+ |---------|-------------|
78
+ | `remctl node start` | Start the node agent in foreground |
79
+ | `remctl node install-service` | Install as OS service |
80
+ | `remctl ctl discover` | Discover nodes on LAN |
81
+ | `remctl ctl pair <target>` | Pair with a node |
82
+ | `remctl ctl <target> lock` | Lock the remote workstation |
83
+ | `remctl ctl <target> shutdown --delay 60` | Shutdown with optional delay |
84
+ | `remctl ctl <target> restart` | Restart |
85
+ | `remctl ctl <target> sleep` | Sleep/Suspend |
86
+ | `remctl ctl <target> logout` | Log out current user |
87
+ | `remctl ctl <target> closefocused` | Close active window |
88
+ | `remctl ctl <target> exec <command>` | Execute shell command (streamed) |
89
+ | `remctl ctl <target> status` | Show system status (CPU/mem/disk/uptime) |
90
+ | `remctl ctl <target> screenshot -o out.png` | Capture screenshot |
91
+ | `remctl ctl <target> push <local> <remote>` | Push file to node |
92
+ | `remctl ctl <target> pull <remote> <local>` | Pull file from node |
93
+ | `remctl ctl <target> cancel` | Cancel pending shutdown/restart |
94
+
95
+ ## Security Model (Summary)
96
+
97
+ - **Identity:** Ed25519 keypairs generated on first run.
98
+ - **Authentication:** Two-way explicit pairing — both sides must trust each other.
99
+ - **Encryption:** X25519 ECDH key exchange → AES-256-GCM, with forward secrecy.
100
+ - **Replay protection:** Per-message sequence numbers + 30-second timestamp window.
101
+ - **Permission tiers:** `observe` (status/screenshot), `standard` (commands +
102
+ files), `full` (includes `exec`). Default: `standard`.
103
+ - **Audit log:** All commands logged unredacted at `~/.remctl/audit.log`.
104
+
105
+ See [docs/SECURITY.md](docs/SECURITY.md) for the full threat model.
106
+
107
+ ## Configuration
108
+
109
+ All config files live in `~/.remctl/`:
110
+
111
+ | File | Purpose |
112
+ |------|---------|
113
+ | `identity.key` | Private key (keep secure) |
114
+ | `identity.pub` | Public key |
115
+ | `known_nodes.json` | Paired nodes (ctl side) |
116
+ | `known_controllers.json` | Trusted controllers (node side) |
117
+ | `node_config.json` | Node settings (bind address, port, tiers) |
118
+ | `audit.log` | Command audit log |
119
+
120
+ ## Platform Support
121
+
122
+ | Platform | Status |
123
+ |----------|--------|
124
+ | Windows | First-class |
125
+ | Linux | Second-class (best-effort) |
126
+ | macOS | Not yet implemented |
127
+
128
+ ## Publishing to PyPI
129
+
130
+ ```bash
131
+ python -m build
132
+ twine upload dist/*
133
+ ```
134
+
135
+ > The user must configure `~/.pypirc` or set `TWINE_USERNAME`/`TWINE_PASSWORD`
136
+ > environment variables before running `twine upload`. No credentials are
137
+ > included in this repository.
@@ -0,0 +1,146 @@
1
+ # Security Model
2
+
3
+ ## Overview
4
+
5
+ `remctl` implements a defense-in-depth security model for LAN-based remote PC
6
+ administration. Every communication channel is authenticated, encrypted, and
7
+ protected against replay attacks.
8
+
9
+ ## Identity
10
+
11
+ - Each `remctl` instance (both node and controller) generates an Ed25519 keypair
12
+ on first run, stored in `~/.remctl/identity.key` (private key) and
13
+ `~/.remctl/identity.pub` (public key).
14
+ - A node or controller's **identity** is the SHA-256 fingerprint of its Ed25519
15
+ public key, displayed as a colon-separated hex string (SSH-fingerprint style).
16
+ - Private keys are stored with restrictive permissions (`chmod 600` on Linux,
17
+ equivalent ACL restriction on Windows).
18
+
19
+ ## Pairing (Trust-On-First-Use)
20
+
21
+ - **Two-way trust is required.** Both the node and the controller must explicitly
22
+ trust each other. There is no auto-trust mechanism.
23
+ - When a controller pairs with a node (`remctl ctl pair <target>`):
24
+ 1. The controller opens a direct TCP connection to the node.
25
+ 2. The node sends its public key over this direct connection (never over
26
+ broadcast).
27
+ 3. The controller computes the fingerprint and displays it for visual
28
+ confirmation (mimicking SSH's `known_hosts` behavior).
29
+ 4. Only after the user types `yes` is the node's public key saved.
30
+ - For the node to trust the controller, the user must run
31
+ `remctl node trust-controller <pubkey-file>` on the node, adding the
32
+ controller's public key to the allowlist.
33
+
34
+ ## Wire Protocol Security
35
+
36
+ ### Session Key Exchange (Forward Secrecy)
37
+
38
+ 1. Each connection performs an X25519 ECDH handshake.
39
+ 2. Both sides generate ephemeral X25519 keypairs per connection.
40
+ 3. Each handshake message is signed with the long-term Ed25519 identity key.
41
+ 4. The shared secret is derived via HKDF-SHA256, incorporating:
42
+ - The X25519 ECDH shared secret
43
+ - The peer's long-term public key
44
+ 5. This provides **forward secrecy**: compromising the long-term keys does not
45
+ compromise past session keys.
46
+
47
+ ### Transport Encryption
48
+
49
+ - All post-handshake traffic is encrypted with **AES-256-GCM** (authenticated
50
+ encryption).
51
+ - Frame format: `[4-byte big-endian length][12-byte nonce][ciphertext]`
52
+ - Each frame uses a random nonce (never reused).
53
+
54
+ ### Replay Protection
55
+
56
+ - Every command message includes a **monotonically increasing sequence number**
57
+ and a **timestamp**.
58
+ - The receiver rejects:
59
+ - Messages with a timestamp more than **30 seconds** from the current time.
60
+ - Messages with a sequence number that has already been processed.
61
+ - Sequence numbers are tracked per-connection.
62
+
63
+ ### Network Binding
64
+
65
+ - By default, the node daemon binds to `0.0.0.0` (all interfaces). This is
66
+ suitable for trusted LAN environments.
67
+ - Users can restrict binding to a specific interface via the `--bind` option
68
+ or `node_config.json`.
69
+ - **Port forwarding or WAN exposure is explicitly out of scope.** `remctl` is
70
+ not designed or tested for Internet-facing use, and the documentation does
71
+ not include instructions for exposing it beyond the LAN.
72
+
73
+ ## Authorization
74
+
75
+ ### Controller Allowlist
76
+
77
+ - The node maintains an allowlist of trusted controller public keys in
78
+ `~/.remctl/known_controllers.json`.
79
+ - Any connection whose identity is not in this list is rejected and logged.
80
+
81
+ ### Permission Tiers
82
+
83
+ Each trusted controller is assigned a permission tier (configured in
84
+ `~/.remctl/node_config.json`):
85
+
86
+ | Tier | Permissions |
87
+ |------------|---------------------------------------------------|
88
+ | `observe` | `status`, `screenshot` only |
89
+ | `standard` | All simple commands + file push/pull, no `exec` |
90
+ | `full` | Everything, including arbitrary shell `exec` |
91
+
92
+ - The default tier for newly-paired controllers is **`standard`**, not `full`.
93
+ - Upgrading to `full` requires explicit configuration by the node operator.
94
+
95
+ ## Rate Limiting / Lockout
96
+
97
+ - Failed handshake/auth attempts are tracked per source IP address.
98
+ - After **5 failures within 60 seconds**, the source IP is temporarily banned
99
+ for **5 minutes**.
100
+ - Rate limiting is in-memory only (not persistent across restarts).
101
+
102
+ ## Audit Logging
103
+
104
+ - Every received command is recorded in `~/.remctl/audit.log` as a JSON line.
105
+ - Each log entry includes: timestamp, source fingerprint, command name,
106
+ arguments (unredacted), outcome (accepted/rejected/error), and exit code
107
+ where applicable.
108
+ - Log rotation is size-based (default: 10MB per file, keep 5 backups) using
109
+ `RotatingFileHandler`.
110
+
111
+ ## Threat Model
112
+
113
+ ### What `remctl` protects against:
114
+
115
+ | Threat | Mitigation |
116
+ |--------|-----------|
117
+ | Unauthenticated LAN actor sending commands | Ed25519 identity verification; controller allowlist |
118
+ | Passive eavesdropping on the wire | AES-256-GCM encryption of all traffic |
119
+ | Active man-in-the-middle | X25519 ECDH signed with long-term identity keys |
120
+ | Replay of captured commands | Per-message sequence numbers + timestamp window |
121
+ | Brute-force connection attempts | Per-IP rate limiting with temporary ban |
122
+ | Unauthorized `exec` on a partially trusted controller | Permission tiers (default `standard`) |
123
+ | Controller key compromise (past sessions) | Forward secrecy via ephemeral X25519 keys |
124
+
125
+ ### What `remctl` does NOT protect against:
126
+
127
+ | Non-goal | Reason |
128
+ |----------|--------|
129
+ | A fully compromised endpoint | Any machine where the node daemon runs with root/admin access can be controlled; if an attacker has root/Administrator, all bets are off |
130
+ | Physical access to the machine | Physical access enables bypassing all software security measures |
131
+ | A compromised controller machine | If a trusted controller machine is compromised, the attacker inherits that controller's permissions |
132
+ | WAN/Internet threats | `remctl` is explicitly designed for LAN use only |
133
+ | DoS attacks | While rate limiting mitigates casual DoS, a determined LAN attacker can still flood the network |
134
+ | Malicious `exec` payloads | `exec` runs arbitrary shell commands; the node operator should only grant `full` tier to highly trusted controllers |
135
+
136
+ ## Security Recommendations for Users
137
+
138
+ 1. **Keep your identity keys safe.** The `~/.remctl/identity.key` file grants
139
+ full access to all nodes that trust it. Back it up securely, or generate a
140
+ new keypair if compromised.
141
+ 2. **Use the `observe` tier** for controllers that only need to check status.
142
+ 3. **Grant `full` tier sparingly** — only to controllers that genuinely need
143
+ shell access.
144
+ 4. **Review the audit log** periodically at `~/.remctl/audit.log`.
145
+ 5. **Bind to a specific interface** if your machine has multiple network
146
+ interfaces and you only want `remctl` accessible on one of them.
remctl-0.1.0/py.typed ADDED
File without changes
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "remctl"
7
+ version = "0.1.0"
8
+ description = "Secure LAN-based remote PC control and administration"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ {name = "remctl contributors"},
14
+ ]
15
+ keywords = ["remote-control", "lan", "administration"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: System Administrators",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: System :: Networking",
27
+ ]
28
+
29
+ dependencies = [
30
+ "typer>=0.12",
31
+ "pydantic>=2",
32
+ "cryptography>=42",
33
+ "zeroconf>=0.132",
34
+ "psutil>=5.9",
35
+ "mss>=9.0",
36
+ "rich>=13",
37
+ "pywin32>=306; sys_platform == 'win32'",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "pytest>=7",
43
+ "pytest-asyncio>=0.23",
44
+ "pytest-mock>=3",
45
+ "ruff>=0.3",
46
+ "mypy>=1.8",
47
+ ]
48
+
49
+ [project.scripts]
50
+ remctl = "remctl.cli:app"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/remctl"]
54
+
55
+ [tool.pytest.ini_options]
56
+ asyncio_mode = "auto"
57
+ testpaths = ["tests"]
58
+
59
+ [tool.ruff]
60
+ target-version = "py310"
61
+ line-length = 120
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "N", "W", "UP"]
65
+
66
+ [tool.mypy]
67
+ python_version = "3.11"
68
+ ignore_missing_imports = true
69
+ warn_unused_configs = true
70
+ warn_redundant_casts = true
71
+ warn_unused_ignores = true
72
+ disallow_any_generics = false
73
+ disallow_subclassing_any = false
74
+ exclude = ["tests/"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from remctl.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from logging.handlers import RotatingFileHandler
6
+
7
+ from remctl.crypto import KEY_DIR
8
+
9
+ AUDIT_LOG = KEY_DIR / "audit.log"
10
+
11
+ _logger: logging.Logger | None = None
12
+
13
+
14
+ def get_logger(max_bytes: int = 10 * 1024 * 1024, backup_count: int = 5) -> logging.Logger:
15
+ global _logger
16
+ if _logger is not None:
17
+ return _logger
18
+ KEY_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
19
+ _logger = logging.getLogger("remctl.audit")
20
+ _logger.setLevel(logging.INFO)
21
+ handler = RotatingFileHandler(
22
+ str(AUDIT_LOG),
23
+ maxBytes=max_bytes,
24
+ backupCount=backup_count,
25
+ encoding="utf-8",
26
+ )
27
+ handler.setFormatter(logging.Formatter("%(message)s"))
28
+ _logger.addHandler(handler)
29
+ _logger.propagate = False
30
+ return _logger
31
+
32
+
33
+ def log_command(
34
+ source_fingerprint: str,
35
+ command: str,
36
+ args: dict,
37
+ outcome: str,
38
+ detail: str | None = None,
39
+ ) -> None:
40
+ logger = get_logger()
41
+ record = {
42
+ "timestamp": __import__("time").time(),
43
+ "source": source_fingerprint,
44
+ "command": command,
45
+ "args": args,
46
+ "outcome": outcome,
47
+ }
48
+ if detail:
49
+ record["detail"] = detail
50
+ logger.info(json.dumps(record, default=str))