server-management-lib 0.2.0__tar.gz → 0.2.1__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.
Files changed (37) hide show
  1. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/PKG-INFO +1 -1
  2. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/pyproject.toml +1 -1
  3. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/__init__.py +1 -1
  4. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/redaction.py +45 -11
  5. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib.egg-info/PKG-INFO +1 -1
  6. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_redaction.py +66 -0
  7. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/README.md +0 -0
  8. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/setup.cfg +0 -0
  9. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/audit.py +0 -0
  10. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/base.py +0 -0
  11. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/config.py +0 -0
  12. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/executor.py +0 -0
  13. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/http_clients.py +0 -0
  14. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/local_executor.py +0 -0
  15. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/operations.py +0 -0
  16. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/output.py +0 -0
  17. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/py.typed +0 -0
  18. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/rate_limit.py +0 -0
  19. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/responses.py +0 -0
  20. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/security.py +0 -0
  21. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/ssh_manager.py +0 -0
  22. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/transport.py +0 -0
  23. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib/tunnel.py +0 -0
  24. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib.egg-info/SOURCES.txt +0 -0
  25. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib.egg-info/dependency_links.txt +0 -0
  26. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib.egg-info/requires.txt +0 -0
  27. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/src/server_management_lib.egg-info/top_level.txt +0 -0
  28. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_audit.py +0 -0
  29. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_config.py +0 -0
  30. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_http_clients.py +0 -0
  31. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_local_executor.py +0 -0
  32. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_operations.py +0 -0
  33. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_output.py +0 -0
  34. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_security.py +0 -0
  35. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_ssh_manager.py +0 -0
  36. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_tooling.py +0 -0
  37. {server_management_lib-0.2.0 → server_management_lib-0.2.1}/tests/test_tunnel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: server-management-lib
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Shared security, SSH/local execution, configuration and HTTP clients for the Server MCP suite.
5
5
  Author-email: Alessio Elmi <alessio.elmi@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "server-management-lib"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Shared security, SSH/local execution, configuration and HTTP clients for the Server MCP suite."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -121,4 +121,4 @@ __all__ = [
121
121
  "validate_http_binding",
122
122
  ]
123
123
 
124
- __version__ = "0.2.0"
124
+ __version__ = "0.2.1"
@@ -63,16 +63,37 @@ def redact_env_content(text: str) -> str:
63
63
  # General secret redaction (service config files and host status/log output)
64
64
  # ============================================================================
65
65
 
66
- # Key names whose values are secrets. Matching is on the key itself, so
67
- # ``POSTGRES_PASSWORD``, ``GITHUB_TOKEN`` and ``api_key`` are covered without
68
- # treating unrelated ``*.key`` file names as secrets (that policy lives in
69
- # ``security.BLOCKED_PATH_*``).
70
- _SENSITIVE_KEY_WORD = re.compile(
71
- r"(?:password|passwd|pwd|secret|token|api[_-]?key|private[_-]?key|"
72
- r"authorization|access[_-]?key|client[_-]?secret|credential)",
73
- re.IGNORECASE,
66
+ # Key names whose values are secrets. Keys are split into words on separators
67
+ # (``_``, ``-``, ``.``, ...) and each word is checked, so ``POSTGRES_PASSWORD``,
68
+ # ``GITHUB_TOKEN``, ``api_key``, ``db-pass`` and a bare INI ``pass`` are covered
69
+ # without treating unrelated ``*.key`` file names as secrets (that policy lives
70
+ # in ``security.BLOCKED_PATH_*``).
71
+ #
72
+ # Short generic names are matched as word suffixes rather than substrings:
73
+ # ``dbpass``/``smtpPass`` are credentials, while ``tokenizer`` and ``keyboard``
74
+ # are not. The trade-off is deliberate: ``bypass`` is masked too, because a
75
+ # spurious mask is harmless and a missed credential is not.
76
+ _SENSITIVE_KEY_WORDS: tuple[str, ...] = (
77
+ "auth",
78
+ "authorization",
79
+ "credential",
80
+ "credentials",
81
+ "key",
82
+ "pass",
83
+ "passphrase",
84
+ "passwd",
85
+ "password",
86
+ "pw",
87
+ "pwd",
88
+ "secret",
89
+ "secrets",
90
+ "token",
91
+ "tokens",
74
92
  )
75
93
 
94
+ # Word separators used when normalizing a key (``db_pass`` -> ``db``, ``pass``).
95
+ _KEY_SEPARATOR = re.compile(r"[^A-Za-z0-9]+")
96
+
76
97
  # JSON object member: "key": "value" or "key": value
77
98
  _JSON_MEMBER = re.compile(
78
99
  r'(?P<prefix>"(?P<key>[^"\n]+)"\s*:\s*)'
@@ -108,10 +129,23 @@ _URL_PASSWORD = re.compile(
108
129
  )
109
130
 
110
131
 
132
+ def _key_words(key: str) -> list[str]:
133
+ """Split a normalized key into lowercase words on non-alphanumerics."""
134
+ return [word.lower() for word in _KEY_SEPARATOR.split(key) if word]
135
+
136
+
111
137
  def _is_sensitive_key(key: str) -> bool:
112
- """Return True when an assignment key names a credential."""
113
- cleaned = key.strip().strip("\"'").lstrip("-")
114
- return bool(cleaned) and _SENSITIVE_KEY_WORD.search(cleaned) is not None
138
+ """Return True when an assignment key names a credential.
139
+
140
+ The key is normalized (quotes and leading CLI dashes removed) and split
141
+ into words. A word matches when it equals a sensitive name or ends with
142
+ one, so separators and concatenation (``api_key``, ``apikey``,
143
+ ``db-pass``, ``dbpass``) are all recognized.
144
+ """
145
+ cleaned = key.strip().strip("\"'").lstrip("-").strip()
146
+ if not cleaned:
147
+ return False
148
+ return any(word.endswith(_SENSITIVE_KEY_WORDS) for word in _key_words(cleaned))
115
149
 
116
150
 
117
151
  def _redact_line(line: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: server-management-lib
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Shared security, SSH/local execution, configuration and HTTP clients for the Server MCP suite.
5
5
  Author-email: Alessio Elmi <alessio.elmi@gmail.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,7 @@
1
1
  """Tests for the file-content redaction helpers."""
2
2
 
3
+ import pytest
4
+
3
5
  from server_management_lib.redaction import REDACTED_VALUE, redact_env_content
4
6
 
5
7
 
@@ -130,3 +132,67 @@ class TestRedactSecrets:
130
132
  text = "# token=comment\nmonkey.txt is readable\n"
131
133
 
132
134
  assert redact_secrets(text) == text
135
+
136
+ def test_ini_short_password_key_is_masked(self):
137
+ from server_management_lib.redaction import redact_secrets
138
+
139
+ text = "[mail]\npass = hunter2\nuser = bob\n"
140
+
141
+ result = redact_secrets(text)
142
+
143
+ assert "hunter2" not in result
144
+ assert "[mail]" in result
145
+ assert "user = bob" in result
146
+ assert result.splitlines()[1] == f"pass = {REDACTED_VALUE}"
147
+
148
+ @pytest.mark.parametrize(
149
+ "key",
150
+ [
151
+ "pass",
152
+ "PASS",
153
+ "Pass",
154
+ "pw",
155
+ "pwd",
156
+ "passphrase",
157
+ "passwd",
158
+ "db_pass",
159
+ "db-pass",
160
+ "dbpass",
161
+ "smtpPass",
162
+ "api_key",
163
+ "apikey",
164
+ "apiKey",
165
+ "auth_token",
166
+ "authToken",
167
+ "secret_key",
168
+ "client_secret",
169
+ "credentials",
170
+ ],
171
+ )
172
+ def test_credential_key_spellings_are_masked(self, key):
173
+ from server_management_lib.redaction import redact_secrets
174
+
175
+ result = redact_secrets(f"{key} = hunter2")
176
+
177
+ assert "hunter2" not in result, f"leaked value for key {key!r}: {result}"
178
+ assert result == f"{key} = {REDACTED_VALUE}"
179
+
180
+ @pytest.mark.parametrize(
181
+ "key",
182
+ ["user", "username", "host", "port", "tokenizer", "keyboard", "mode"],
183
+ )
184
+ def test_non_credential_keys_stay_readable(self, key):
185
+ from server_management_lib.redaction import redact_secrets
186
+
187
+ assert redact_secrets(f"{key} = visible") == f"{key} = visible"
188
+
189
+ def test_short_password_key_is_masked_in_yaml_and_json(self):
190
+ from server_management_lib.redaction import redact_secrets
191
+
192
+ yaml_result = redact_secrets(" pass: hunter2\n")
193
+ json_result = redact_secrets('{"pass": "hunter2"}')
194
+
195
+ assert "hunter2" not in yaml_result
196
+ assert f"pass: {REDACTED_VALUE}" in yaml_result
197
+ assert "hunter2" not in json_result
198
+ assert f'"pass": "{REDACTED_VALUE}"' in json_result