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.
Files changed (85) hide show
  1. {ipmg-3.1.1/src/ipmg.egg-info → ipmg-3.1.2}/PKG-INFO +1 -1
  2. {ipmg-3.1.1 → ipmg-3.1.2}/pyproject.toml +1 -1
  3. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/__init__.py +1 -1
  4. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/notify.py +21 -0
  5. {ipmg-3.1.1 → ipmg-3.1.2/src/ipmg.egg-info}/PKG-INFO +1 -1
  6. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_notify.py +50 -0
  7. {ipmg-3.1.1 → ipmg-3.1.2}/LICENSE +0 -0
  8. {ipmg-3.1.1 → ipmg-3.1.2}/README.md +0 -0
  9. {ipmg-3.1.1 → ipmg-3.1.2}/setup.cfg +0 -0
  10. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/__main__.py +0 -0
  11. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/__init__.py +0 -0
  12. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/commands.py +0 -0
  13. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/config.py +0 -0
  14. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/cli/parser.py +0 -0
  15. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/__init__.py +0 -0
  16. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/diff.py +0 -0
  17. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/discovery.py +0 -0
  18. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/engine.py +0 -0
  19. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/health.py +0 -0
  20. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/ping.py +0 -0
  21. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/portscan.py +0 -0
  22. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/core/security.py +0 -0
  23. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/exceptions.py +0 -0
  24. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/__init__.py +0 -0
  25. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/database.py +0 -0
  26. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/file_io.py +0 -0
  27. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/infrastructure/incremental.py +0 -0
  28. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/__init__.py +0 -0
  29. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/diff_report.py +0 -0
  30. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/frames.py +0 -0
  31. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/live.py +0 -0
  32. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/machine.py +0 -0
  33. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/metrics.py +0 -0
  34. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/summary.py +0 -0
  35. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/reporting/ui.py +0 -0
  36. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/services/__init__.py +0 -0
  37. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/services/history_service.py +0 -0
  38. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/services/scan_service.py +0 -0
  39. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/utils/__init__.py +0 -0
  40. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/utils/helpers.py +0 -0
  41. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/__init__.py +0 -0
  42. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/app.py +0 -0
  43. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/db.py +0 -0
  44. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/manager.py +0 -0
  45. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/schemas.py +0 -0
  46. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/server.py +0 -0
  47. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/css/app.css +0 -0
  48. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/index.html +0 -0
  49. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/api.js +0 -0
  50. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/app.js +0 -0
  51. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/charts.js +0 -0
  52. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/demo.js +0 -0
  53. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg/web/static/js/views.js +0 -0
  54. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/SOURCES.txt +0 -0
  55. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/dependency_links.txt +0 -0
  56. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/entry_points.txt +0 -0
  57. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/requires.txt +0 -0
  58. {ipmg-3.1.1 → ipmg-3.1.2}/src/ipmg.egg-info/top_level.txt +0 -0
  59. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_api_contract.py +0 -0
  60. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_commands.py +0 -0
  61. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_config.py +0 -0
  62. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_database_history.py +0 -0
  63. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_diff.py +0 -0
  64. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_diff_report.py +0 -0
  65. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_discover.py +0 -0
  66. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_engine.py +0 -0
  67. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_file_io.py +0 -0
  68. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_health.py +0 -0
  69. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_history_service.py +0 -0
  70. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_incremental.py +0 -0
  71. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ipv6.py +0 -0
  72. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_live.py +0 -0
  73. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_machine_output.py +0 -0
  74. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_metrics.py +0 -0
  75. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_parser.py +0 -0
  76. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ping.py +0 -0
  77. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ping_command.py +0 -0
  78. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_portscan.py +0 -0
  79. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_scan_service.py +0 -0
  80. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_ui.py +0 -0
  81. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_utils.py +0 -0
  82. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_api.py +0 -0
  83. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_db.py +0 -0
  84. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_manager.py +0 -0
  85. {ipmg-3.1.1 → ipmg-3.1.2}/tests/test_web_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipmg
3
- Version: 3.1.1
3
+ Version: 3.1.2
4
4
  Summary: IP Management & Ping Monitoring CLI Tool
5
5
  Author: Sameer Alam
6
6
  Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
 
14
14
  [project]
15
15
  name = "ipmg"
16
- version = "3.1.1" # Managed automatically by semantic-release
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"
@@ -2,4 +2,4 @@
2
2
  ipmg - IP Management & Ping Monitoring Tool
3
3
  """
4
4
 
5
- __version__ = "3.1.1"
5
+ __version__ = "3.1.2"
@@ -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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipmg
3
- Version: 3.1.1
3
+ Version: 3.1.2
4
4
  Summary: IP Management & Ping Monitoring CLI Tool
5
5
  Author: Sameer Alam
6
6
  Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
@@ -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