ipmg 3.1.1__tar.gz → 3.1.2__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.
- {ipmg-3.1.1/src/ipmg.egg-info → ipmg-3.1.2}/PKG-INFO +1 -1
- {ipmg-3.1.1 → ipmg-3.1.2}/pyproject.toml +1 -1
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/__init__.py +1 -1
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/notify.py +21 -0
- {ipmg-3.1.1 → ipmg-3.1.2/src/ipmg.egg-info}/PKG-INFO +1 -1
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_notify.py +50 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/LICENSE +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/README.md +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/setup.cfg +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/__main__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/__init__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/commands.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/config.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/parser.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/__init__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/diff.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/discovery.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/engine.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/health.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/ping.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/portscan.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/security.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/exceptions.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/__init__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/database.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/file_io.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/incremental.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/__init__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/diff_report.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/frames.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/live.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/machine.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/metrics.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/summary.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/ui.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/services/__init__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/services/history_service.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/services/scan_service.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/utils/__init__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/utils/helpers.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/__init__.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/app.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/db.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/manager.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/schemas.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/server.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/css/app.css +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/index.html +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/api.js +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/app.js +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/charts.js +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/demo.js +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/views.js +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/SOURCES.txt +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/dependency_links.txt +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/entry_points.txt +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/requires.txt +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/top_level.txt +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_api_contract.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_commands.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_config.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_database_history.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_diff.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_diff_report.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_discover.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_engine.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_file_io.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_health.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_history_service.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_incremental.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ipv6.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_live.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_machine_output.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_metrics.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_parser.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ping.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ping_command.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_portscan.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_scan_service.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ui.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_utils.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_api.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_db.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_manager.py +0 -0
- {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_server.py +0 -0
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "ipmg"
|
|
16
|
-
version = "3.1.
|
|
16
|
+
version = "3.1.2" # Managed automatically by semantic-release
|
|
17
17
|
description = "IP Management & Ping Monitoring CLI Tool"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
requires-python = ">=3.9"
|
|
@@ -8,6 +8,7 @@ never fails the scan or costs it its reports.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import ipaddress
|
|
11
12
|
import json
|
|
12
13
|
import logging
|
|
13
14
|
import os
|
|
@@ -92,6 +93,16 @@ def _flag_or_env(value: Optional[str], environ: Mapping[str, str], name: str) ->
|
|
|
92
93
|
return environ.get(name, "").strip()
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
def _is_loopback(host: str) -> bool:
|
|
97
|
+
candidate = host.strip().strip("[]")
|
|
98
|
+
if candidate.lower() == "localhost":
|
|
99
|
+
return True
|
|
100
|
+
try:
|
|
101
|
+
return ipaddress.ip_address(candidate).is_loopback
|
|
102
|
+
except ValueError:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
95
106
|
def _url(value: Optional[str], environ: Mapping[str, str], flag: str, env: str) -> str:
|
|
96
107
|
"""The URL for a ``--notify-*`` flag. None means the flag was not given."""
|
|
97
108
|
if value is None:
|
|
@@ -140,6 +151,11 @@ def _smtp_settings(args, environ: Mapping[str, str]) -> Optional[SmtpSettings]:
|
|
|
140
151
|
raise NotifyError(f"SMTP port out of range (1-65535): {port}")
|
|
141
152
|
|
|
142
153
|
username = _flag_or_env(getattr(args, "smtp_user", None), environ, ENV_SMTP_USER)
|
|
154
|
+
if username and security == "none" and not _is_loopback(host):
|
|
155
|
+
raise NotifyError(
|
|
156
|
+
"--smtp-user cannot be used with --smtp-security none unless the SMTP host is "
|
|
157
|
+
"loopback; use starttls or ssl instead."
|
|
158
|
+
)
|
|
143
159
|
sender = _flag_or_env(getattr(args, "smtp_from", None), environ, ENV_SMTP_FROM)
|
|
144
160
|
return SmtpSettings(
|
|
145
161
|
host=host,
|
|
@@ -316,6 +332,11 @@ def post_json(url: str, payload: Mapping[str, Any]) -> None:
|
|
|
316
332
|
|
|
317
333
|
|
|
318
334
|
def send_email(settings: SmtpSettings, message: EmailMessage) -> None:
|
|
335
|
+
if settings.username and settings.security == "none" and not _is_loopback(settings.host):
|
|
336
|
+
raise NotifyError(
|
|
337
|
+
"--smtp-user cannot be used with --smtp-security none unless the SMTP host is "
|
|
338
|
+
"loopback; use starttls or ssl instead."
|
|
339
|
+
)
|
|
319
340
|
context = ssl.create_default_context()
|
|
320
341
|
if settings.security == "ssl":
|
|
321
342
|
client = smtplib.SMTP_SSL(
|
|
@@ -223,6 +223,38 @@ def test_incomplete_email_settings_are_errors(overrides, environ, message):
|
|
|
223
223
|
notify_options(notify_args(**overrides), environ=environ)
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
def test_smtp_user_with_security_none_is_refused_for_non_loopback_hosts():
|
|
227
|
+
with pytest.raises(NotifyError, match=r"starttls.*ssl"):
|
|
228
|
+
notify_options(
|
|
229
|
+
notify_args(
|
|
230
|
+
notify_email=["ops@example.test"],
|
|
231
|
+
smtp_host="mail.example.test",
|
|
232
|
+
smtp_security="none",
|
|
233
|
+
smtp_user="ipmg@example.test",
|
|
234
|
+
),
|
|
235
|
+
environ={},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@pytest.mark.parametrize("loopback_host", ["localhost", "127.0.0.1", "::1"])
|
|
240
|
+
def test_smtp_user_with_security_none_is_allowed_for_loopback_hosts(loopback_host):
|
|
241
|
+
settings = notify_options(
|
|
242
|
+
notify_args(
|
|
243
|
+
notify_email=["ops@example.test"],
|
|
244
|
+
smtp_host=loopback_host,
|
|
245
|
+
smtp_security="none",
|
|
246
|
+
smtp_user="relay-user",
|
|
247
|
+
),
|
|
248
|
+
environ={"IPMG_SMTP_PASSWORD": "relay-password"}, # pragma: allowlist secret
|
|
249
|
+
).email
|
|
250
|
+
|
|
251
|
+
assert settings.host == loopback_host
|
|
252
|
+
assert settings.security == "none"
|
|
253
|
+
assert settings.username == "relay-user"
|
|
254
|
+
assert settings.port == 25
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
226
258
|
# ---------------------------------------------------------------- messages
|
|
227
259
|
|
|
228
260
|
|
|
@@ -518,3 +550,21 @@ def test_send_email_follows_the_security_setting(diff, fake_smtp, security, user
|
|
|
518
550
|
|
|
519
551
|
assert fake_smtp[0].calls == expected
|
|
520
552
|
assert (fake_smtp[0].context is not None) is (security == "ssl")
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_send_email_refuses_cleartext_auth_on_remote_host(diff, fake_smtp):
|
|
556
|
+
settings = SmtpSettings("mail.example.test", 25, "none", "ipmg@x.test", ("ops@x.test",), "u", "pw")
|
|
557
|
+
|
|
558
|
+
with pytest.raises(NotifyError, match=r"starttls.*ssl"):
|
|
559
|
+
send_email(settings, email_message(diff, settings))
|
|
560
|
+
|
|
561
|
+
assert fake_smtp == []
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def test_send_email_allows_cleartext_auth_on_loopback_host(diff, fake_smtp):
|
|
565
|
+
settings = SmtpSettings("127.0.0.1", 25, "none", "ipmg@x.test", ("ops@x.test",), "u", "pw")
|
|
566
|
+
|
|
567
|
+
send_email(settings, email_message(diff, settings))
|
|
568
|
+
|
|
569
|
+
assert fake_smtp[0].calls == [("login", "u", "pw"), ("send", "ops@x.test"), "quit"]
|
|
570
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|