check-msdefender 1.1.1__tar.gz → 1.1.3__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 (90) hide show
  1. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/PKG-INFO +12 -13
  2. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/README.md +0 -1
  3. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/check_msdefender.py +0 -1
  4. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/commands/alerts.py +0 -1
  5. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/commands/detail.py +1 -6
  6. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/commands/lastseen.py +0 -1
  7. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/commands/machines.py +0 -1
  8. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/commands/onboarding.py +0 -1
  9. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/commands/vulnerabilities.py +0 -1
  10. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/decorators.py +3 -1
  11. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/handlers.py +0 -1
  12. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/core/auth.py +3 -1
  13. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/core/defender.py +14 -4
  14. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/core/logging_config.py +5 -2
  15. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/core/nagios.py +3 -1
  16. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/alerts_service.py +17 -7
  17. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/detail_service.py +9 -3
  18. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/lastseen_service.py +3 -1
  19. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/machines_service.py +10 -3
  20. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/models.py +1 -1
  21. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/onboarding_service.py +3 -1
  22. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/vulnerabilities_service.py +16 -6
  23. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/pyproject.toml +45 -11
  24. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/mock_defender_client.py +4 -2
  25. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/test_alerts_service.py +5 -2
  26. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/test_detail_service.py +9 -3
  27. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/test_lastseen_service.py +6 -4
  28. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/test_onboarding_service.py +3 -1
  29. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/test_vulnerabilities_service.py +3 -1
  30. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/integration/test_cli_integration.py +61 -18
  31. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/integration/test_lastseen_integration.py +17 -3
  32. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/unit/test_alerts_service.py +9 -3
  33. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/unit/test_detail_service.py +26 -10
  34. check_msdefender-1.1.1/.claude/settings.local.json +0 -19
  35. check_msdefender-1.1.1/.env.example +0 -10
  36. check_msdefender-1.1.1/.github/workflows/python-package.yml +0 -40
  37. check_msdefender-1.1.1/.github/workflows/python-publish.yml +0 -70
  38. check_msdefender-1.1.1/.gitignore +0 -6
  39. check_msdefender-1.1.1/.idea/.gitignore +0 -5
  40. check_msdefender-1.1.1/.idea/check_msdefender.iml +0 -13
  41. check_msdefender-1.1.1/.idea/dictionaries/project.xml +0 -7
  42. check_msdefender-1.1.1/.idea/encodings.xml +0 -4
  43. check_msdefender-1.1.1/.idea/inspectionProfiles/profiles_settings.xml +0 -6
  44. check_msdefender-1.1.1/.idea/misc.xml +0 -7
  45. check_msdefender-1.1.1/.idea/modules.xml +0 -8
  46. check_msdefender-1.1.1/.idea/runConfigurations/Integration_Tests.xml +0 -23
  47. check_msdefender-1.1.1/.idea/vcs.xml +0 -6
  48. check_msdefender-1.1.1/check_msdefender.egg-info/PKG-INFO +0 -431
  49. check_msdefender-1.1.1/check_msdefender.egg-info/SOURCES.txt +0 -88
  50. check_msdefender-1.1.1/check_msdefender.egg-info/dependency_links.txt +0 -1
  51. check_msdefender-1.1.1/check_msdefender.egg-info/entry_points.txt +0 -2
  52. check_msdefender-1.1.1/check_msdefender.egg-info/requires.txt +0 -12
  53. check_msdefender-1.1.1/check_msdefender.egg-info/top_level.txt +0 -1
  54. check_msdefender-1.1.1/check_msdefender.ini.example +0 -18
  55. check_msdefender-1.1.1/doc/Feat-Click-Decorators-ErrorHandlers-Formatters.md +0 -66
  56. check_msdefender-1.1.1/doc/Feat-Click-Groups.md +0 -126
  57. check_msdefender-1.1.1/doc/Feat-Enhance-MsDefender-Vulnerabilities-Output.md +0 -35
  58. check_msdefender-1.1.1/doc/Feat-Fixture-Tests.md +0 -40
  59. check_msdefender-1.1.1/doc/Feat-Integration-Tests.md +0 -21
  60. check_msdefender-1.1.1/doc/Feat-MsDefender-Alerts.md +0 -116
  61. check_msdefender-1.1.1/doc/Feat-MsDefender-DetailMachine.md +0 -78
  62. check_msdefender-1.1.1/doc/Feat-MsDefender-ListMachines.md +0 -87
  63. check_msdefender-1.1.1/doc/Feat-MsDefender.md +0 -110
  64. check_msdefender-1.1.1/doc/Feat-Nagios-Detailed-Output.md +0 -31
  65. check_msdefender-1.1.1/doc/Feat-Nagios-Exit-Code.md +0 -21
  66. check_msdefender-1.1.1/doc/Feat-Nagios-Output.md +0 -53
  67. check_msdefender-1.1.1/doc/Feat-Pypi-Package.md +0 -306
  68. check_msdefender-1.1.1/doc/Feat-Verbose.md +0 -39
  69. check_msdefender-1.1.1/pytest.ini +0 -6
  70. check_msdefender-1.1.1/requirements-dev.txt +0 -9
  71. check_msdefender-1.1.1/requirements.txt +0 -3
  72. check_msdefender-1.1.1/setup.cfg +0 -4
  73. check_msdefender-1.1.1/test_verbose.py +0 -29
  74. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/LICENSE +0 -0
  75. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/__init__.py +0 -0
  76. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/__main__.py +0 -0
  77. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/__init__.py +0 -0
  78. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/__main__.py +0 -0
  79. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/cli/commands/__init__.py +0 -0
  80. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/core/__init__.py +0 -0
  81. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/core/config.py +0 -0
  82. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/core/exceptions.py +0 -0
  83. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/check_msdefender/services/__init__.py +0 -0
  84. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/__init__.py +0 -0
  85. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/__init__.py +0 -0
  86. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/alerts_data.json +0 -0
  87. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/machine_data.json +0 -0
  88. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/fixtures/vulnerability_data.json +0 -0
  89. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/integration/__init__.py +0 -0
  90. {check_msdefender-1.1.1 → check_msdefender-1.1.3}/tests/unit/__init__.py +0 -0
@@ -1,14 +1,10 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: check-msdefender
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: A Nagios plugin for monitoring Microsoft Defender API endpoints
5
- Author-email: ldvchosal <ldvchosal@github.com>
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/lduchosal/check_msdefender
8
- Project-URL: Bug Reports, https://github.com/lduchosal/check_msdefender/issues
9
- Project-URL: Source, https://github.com/lduchosal/check_msdefender
10
- Project-URL: Documentation, https://github.com/lduchosal/check_msdefender/blob/main/README.md
11
5
  Keywords: nagios,monitoring,microsoft,graph,api,azure
6
+ Author-Email: ldvchosal <ldvchosal@github.com>
7
+ License: MIT
12
8
  Classifier: Development Status :: 5 - Production/Stable
13
9
  Classifier: Intended Audience :: System Administrators
14
10
  Classifier: License :: OSI Approved :: MIT License
@@ -19,9 +15,11 @@ Classifier: Programming Language :: Python :: 3.10
19
15
  Classifier: Programming Language :: Python :: 3.11
20
16
  Classifier: Topic :: System :: Monitoring
21
17
  Classifier: Topic :: System :: Systems Administration
18
+ Project-URL: Homepage, https://github.com/lduchosal/check_msdefender
19
+ Project-URL: Bug Reports, https://github.com/lduchosal/check_msdefender/issues
20
+ Project-URL: Source, https://github.com/lduchosal/check_msdefender
21
+ Project-URL: Documentation, https://github.com/lduchosal/check_msdefender/blob/main/README.md
22
22
  Requires-Python: >=3.9
23
- Description-Content-Type: text/markdown
24
- License-File: LICENSE
25
23
  Requires-Dist: nagiosplugin>=1.4.0
26
24
  Requires-Dist: azure-identity>=1.12.0
27
25
  Requires-Dist: click<9.0,>=8.0
@@ -33,7 +31,9 @@ Requires-Dist: black>=21.0; extra == "dev"
33
31
  Requires-Dist: flake8>=3.8; extra == "dev"
34
32
  Requires-Dist: mypy>=0.800; extra == "dev"
35
33
  Requires-Dist: twine>=6.2.0; extra == "dev"
36
- Dynamic: license-file
34
+ Requires-Dist: pdm>=2.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.13.0; extra == "dev"
36
+ Description-Content-Type: text/markdown
37
37
 
38
38
  # 🛡️ Check MS Defender
39
39
 
@@ -295,7 +295,6 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
295
295
 
296
296
  # Install in development mode
297
297
  pip install -e .
298
- pip install -r requirements-dev.txt
299
298
  ```
300
299
 
301
300
  ### Code Quality Tools
@@ -428,4 +427,4 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
428
427
 
429
428
  [🐛 Report Bug](https://github.com/lduchosal/check_msdefender/issues) • [💡 Request Feature](https://github.com/lduchosal/check_msdefender/issues) • [📖 Documentation](https://github.com/lduchosal/check_msdefender/blob/main/README.md)
430
429
 
431
- </div>
430
+ </div>
@@ -258,7 +258,6 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
258
258
 
259
259
  # Install in development mode
260
260
  pip install -e .
261
- pip install -r requirements-dev.txt
262
261
  ```
263
262
 
264
263
  ### Code Quality Tools
@@ -1,6 +1,5 @@
1
1
  """Main entry point for check_msdefender Nagios plugin."""
2
2
 
3
- import sys
4
3
  from check_msdefender.cli import main
5
4
 
6
5
  if __name__ == "__main__":
@@ -1,7 +1,6 @@
1
1
  """Alerts commands for CLI."""
2
2
 
3
3
  import sys
4
- import click
5
4
  from typing import Optional, Any
6
5
 
7
6
  from check_msdefender.core.auth import get_authenticator
@@ -16,7 +16,6 @@ def register_detail_commands(main_group: Any) -> None:
16
16
  """Register detail commands with the main CLI group."""
17
17
 
18
18
  @main_group.command("detail")
19
- @click.option("-i", "--id", "machine_id_alt", help="Machine ID (GUID)")
20
19
  @common_options
21
20
  def detail_cmd(
22
21
  config: str,
@@ -25,7 +24,6 @@ def register_detail_commands(main_group: Any) -> None:
25
24
  dns_name: Optional[str],
26
25
  warning: Optional[float],
27
26
  critical: Optional[float],
28
- machine_id_alt: Optional[str],
29
27
  ) -> None:
30
28
  """Get detailed machine information from Microsoft Defender."""
31
29
  try:
@@ -44,9 +42,6 @@ def register_detail_commands(main_group: Any) -> None:
44
42
  # Create custom Nagios plugin for detail output
45
43
  plugin = NagiosPlugin(service, "detail")
46
44
 
47
- # Use -i option if provided, otherwise fallback to -m
48
- final_machine_id = machine_id_alt or machine_id
49
-
50
45
  # Set default thresholds for detail command to show proper performance data
51
46
  # Based on expected test output patterns
52
47
  if warning is not None and critical is None:
@@ -58,7 +53,7 @@ def register_detail_commands(main_group: Any) -> None:
58
53
 
59
54
  # Execute check
60
55
  result = plugin.check(
61
- machine_id=final_machine_id,
56
+ machine_id=machine_id,
62
57
  dns_name=dns_name,
63
58
  warning=warning,
64
59
  critical=critical,
@@ -1,7 +1,6 @@
1
1
  """Last seen commands for CLI."""
2
2
 
3
3
  import sys
4
- import click
5
4
  from typing import Optional, Any
6
5
 
7
6
  from check_msdefender.core.auth import get_authenticator
@@ -1,7 +1,6 @@
1
1
  """List machines commands for CLI."""
2
2
 
3
3
  import sys
4
- import click
5
4
  from typing import Optional, Any
6
5
 
7
6
  from check_msdefender.core.auth import get_authenticator
@@ -1,7 +1,6 @@
1
1
  """Onboarding status commands for CLI."""
2
2
 
3
3
  import sys
4
- import click
5
4
  from typing import Optional, Any
6
5
 
7
6
  from check_msdefender.core.auth import get_authenticator
@@ -1,7 +1,6 @@
1
1
  """Vulnerability commands for CLI."""
2
2
 
3
3
  import sys
4
- import click
5
4
  from typing import Optional, Any
6
5
 
7
6
  from check_msdefender.core.auth import get_authenticator
@@ -10,7 +10,9 @@ def common_options(func: Callable[..., Any]) -> Callable[..., Any]:
10
10
  "-c", "--config", default="check_msdefender.ini", help="Configuration file path"
11
11
  )(func)
12
12
  func = click.option("-v", "--verbose", count=True, help="Increase verbosity")(func)
13
- func = click.option("-m", "--machine-id", help="Machine ID (GUID)")(func)
13
+ func = click.option("-m", "--machine-id", "-i", "--id", help="Machine ID (GUID)")(
14
+ func
15
+ )
14
16
  func = click.option("-d", "--dns-name", help="Computer DNS Name (FQDN)")(func)
15
17
  func = click.option("-W", "--warning", type=float, help="Warning threshold")(func)
16
18
  func = click.option("-C", "--critical", type=float, help="Critical threshold")(func)
@@ -1,7 +1,6 @@
1
1
  """Error handlers and formatters for click CLI."""
2
2
 
3
3
  import click
4
- from typing import Any
5
4
 
6
5
 
7
6
  class ClickErrorHandler:
@@ -20,7 +20,9 @@ def get_authenticator(
20
20
  tenant_id = auth_section.get("tenant_id")
21
21
 
22
22
  if not client_id or not tenant_id:
23
- raise ConfigurationError("client_id and tenant_id are required in [auth] section")
23
+ raise ConfigurationError(
24
+ "client_id and tenant_id are required in [auth] section"
25
+ )
24
26
 
25
27
  # Check for client secret authentication
26
28
  client_secret = auth_section.get("client_secret")
@@ -13,7 +13,11 @@ class DefenderClient:
13
13
  application_json = "application/json"
14
14
 
15
15
  def __init__(
16
- self, authenticator: Any, timeout: int = 5, region: str = "eu3", verbose_level: int = 0
16
+ self,
17
+ authenticator: Any,
18
+ timeout: int = 5,
19
+ region: str = "eu3",
20
+ verbose_level: int = 0,
17
21
  ) -> None:
18
22
  """Initialize with authenticator and optional region.
19
23
 
@@ -56,7 +60,9 @@ class DefenderClient:
56
60
  try:
57
61
  start_time = time.time()
58
62
  self.logger.info(f"Querying machine by DNS name: {dns_name}")
59
- response = requests.get(url, headers=headers, params=params, timeout=self.timeout)
63
+ response = requests.get(
64
+ url, headers=headers, params=params, timeout=self.timeout
65
+ )
60
66
  elapsed = time.time() - start_time
61
67
 
62
68
  self.logger.api_call("GET", url, response.status_code, elapsed)
@@ -151,7 +157,9 @@ class DefenderClient:
151
157
  try:
152
158
  start_time = time.time()
153
159
  self.logger.info("Querying all machines")
154
- response = requests.get(url, headers=headers, params=params, timeout=self.timeout)
160
+ response = requests.get(
161
+ url, headers=headers, params=params, timeout=self.timeout
162
+ )
155
163
  elapsed = time.time() - start_time
156
164
 
157
165
  self.logger.api_call("GET", url, response.status_code, elapsed)
@@ -189,7 +197,9 @@ class DefenderClient:
189
197
  try:
190
198
  start_time = time.time()
191
199
  self.logger.info("Querying alerts")
192
- response = requests.get(url, headers=headers, params=params, timeout=self.timeout)
200
+ response = requests.get(
201
+ url, headers=headers, params=params, timeout=self.timeout
202
+ )
193
203
  elapsed = time.time() - start_time
194
204
 
195
205
  self.logger.api_call("GET", url, response.status_code, elapsed)
@@ -36,7 +36,8 @@ class VerboseLogger:
36
36
  if self.verbose_level >= 3:
37
37
  # Full trace format
38
38
  formatter = logging.Formatter(
39
- "[%(levelname)s] %(asctime)s %(name)s:%(lineno)d - %(message)s", datefmt="%H:%M:%S"
39
+ "[%(levelname)s] %(asctime)s %(name)s:%(lineno)d - %(message)s",
40
+ datefmt="%H:%M:%S",
40
41
  )
41
42
  elif self.verbose_level >= 2:
42
43
  # Debug format
@@ -81,7 +82,9 @@ class VerboseLogger:
81
82
  """Log API call details if verbose >= 2."""
82
83
  if self.verbose_level >= 2:
83
84
  if status_code and response_time:
84
- self.logger.debug(f"API {method} {url} -> {status_code} ({response_time:.3f}s)")
85
+ self.logger.debug(
86
+ f"API {method} {url} -> {status_code} ({response_time:.3f}s)"
87
+ )
85
88
  else:
86
89
  self.logger.debug(f"API {method} {url}")
87
90
 
@@ -128,7 +128,9 @@ class NagiosPlugin:
128
128
 
129
129
  # Create Nagios check with custom summary
130
130
  # Use 'found' as context name for detail command, otherwise use command name
131
- context_name = "found" if self.command_name == "detail" else self.command_name
131
+ context_name = (
132
+ "found" if self.command_name == "detail" else self.command_name
133
+ )
132
134
  check = nagiosplugin.Check(
133
135
  DefenderResource(self.command_name, value),
134
136
  DefenderScalarContext(context_name, warning, critical),
@@ -1,7 +1,7 @@
1
1
  """Alerts service implementation."""
2
2
 
3
- from datetime import datetime
4
- from typing import Dict, Optional, Any, List
3
+ from typing import Dict, Optional, Any
4
+
5
5
  from check_msdefender.core.exceptions import ValidationError
6
6
  from check_msdefender.core.logging_config import get_verbose_logger
7
7
 
@@ -53,12 +53,18 @@ class AlertsService:
53
53
  or alert.get("computerDnsName") == target_dns_name
54
54
  ]
55
55
 
56
- self.logger.info(f"Found {len(machine_alerts)} alerts for machine {target_dns_name}")
56
+ self.logger.info(
57
+ f"Found {len(machine_alerts)} alerts for machine {target_dns_name}"
58
+ )
57
59
 
58
60
  # Categorize alerts by status and severity
59
- unresolved_alerts = [alert for alert in machine_alerts if alert.get("status") != "Resolved"]
61
+ unresolved_alerts = [
62
+ alert for alert in machine_alerts if alert.get("status") != "Resolved"
63
+ ]
60
64
  informational_alerts = [
61
- alert for alert in unresolved_alerts if alert.get("severity") == "Informational"
65
+ alert
66
+ for alert in unresolved_alerts
67
+ if alert.get("severity") == "Informational"
62
68
  ]
63
69
  critical_warning_alerts = [
64
70
  alert
@@ -82,7 +88,9 @@ class AlertsService:
82
88
  title = alert.get("title", "Unknown alert")
83
89
  status = alert.get("status", "Unknown")
84
90
  severity = alert.get("severity", "Unknown")
85
- details.append(f"{creation_time} - {title} ({status} {severity.lower()})")
91
+ details.append(
92
+ f"{creation_time} - {title} ({status} {severity.lower()})"
93
+ )
86
94
 
87
95
  # Return the number of unresolved alerts as the value
88
96
  # This will be used by Nagios plugin for determining status based on thresholds
@@ -93,6 +101,8 @@ class AlertsService:
93
101
  "details": details,
94
102
  }
95
103
 
96
- self.logger.info(f"Alert analysis complete: {len(unresolved_alerts)} unresolved alerts")
104
+ self.logger.info(
105
+ f"Alert analysis complete: {len(unresolved_alerts)} unresolved alerts"
106
+ )
97
107
  self.logger.method_exit("get_result", result)
98
108
  return result
@@ -54,10 +54,16 @@ class DetailService:
54
54
  # Create detailed output
55
55
  details = []
56
56
  details.append(f"Machine ID: {machine_details.get('id', 'Unknown')}")
57
- details.append(f"Computer Name: {machine_details.get('computerDnsName', 'Unknown')}")
58
- details.append(f"OS Platform: {machine_details.get('osPlatform', 'Unknown')}")
57
+ details.append(
58
+ f"Computer Name: {machine_details.get('computerDnsName', 'Unknown')}"
59
+ )
60
+ details.append(
61
+ f"OS Platform: {machine_details.get('osPlatform', 'Unknown')}"
62
+ )
59
63
  details.append(f"OS Version: {machine_details.get('osVersion', 'Unknown')}")
60
- details.append(f"Health Status: {machine_details.get('healthStatus', 'Unknown')}")
64
+ details.append(
65
+ f"Health Status: {machine_details.get('healthStatus', 'Unknown')}"
66
+ )
61
67
  details.append(f"Risk Score: {machine_details.get('riskScore', 'Unknown')}")
62
68
 
63
69
  result = {"value": 1, "details": details}
@@ -62,7 +62,9 @@ class LastSeenService:
62
62
 
63
63
  result = {"value": days_diff, "details": details}
64
64
 
65
- self.logger.info(f"Machine last seen {days_diff} days ago ({last_seen_str})")
65
+ self.logger.info(
66
+ f"Machine last seen {days_diff} days ago ({last_seen_str})"
67
+ )
66
68
  self.logger.method_exit("get_result", result)
67
69
  return result
68
70
  except (ValueError, TypeError) as e:
@@ -1,7 +1,7 @@
1
1
  """Machines service implementation."""
2
2
 
3
3
  from typing import Dict, List, Any
4
- from check_msdefender.core.exceptions import ValidationError
4
+
5
5
  from check_msdefender.core.logging_config import get_verbose_logger
6
6
 
7
7
 
@@ -23,7 +23,10 @@ class MachinesService:
23
23
 
24
24
  if not machines_data.get("value"):
25
25
  self.logger.info("No machines found")
26
- result = {"value": 0, "details": ["No machines found in Microsoft Defender"]}
26
+ result = {
27
+ "value": 0,
28
+ "details": ["No machines found in Microsoft Defender"],
29
+ }
27
30
  self.logger.method_exit("get_result", result)
28
31
  return result
29
32
 
@@ -39,7 +42,11 @@ class MachinesService:
39
42
 
40
43
  # Sort by priority
41
44
  sorted_machines = sorted(
42
- machines, key=lambda x: (status_priority[x["onboardingStatus"]], x["computerDnsName"])
45
+ machines,
46
+ key=lambda x: (
47
+ status_priority[x["onboardingStatus"]],
48
+ x["computerDnsName"],
49
+ ),
43
50
  )
44
51
  for machine in sorted_machines:
45
52
  onboarded = "✓" if machine["onboardingStatus"] == "Onboarded" else "✗"
@@ -2,8 +2,8 @@
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from datetime import datetime
5
- from typing import Optional, List
6
5
  from enum import Enum
6
+ from typing import Optional
7
7
 
8
8
 
9
9
  class OnboardingStatus(Enum):
@@ -54,6 +54,8 @@ class OnboardingService:
54
54
 
55
55
  result = {"value": result_value, "details": details}
56
56
 
57
- self.logger.info(f"Machine onboarding status: {onboarding_state} -> {result_value}")
57
+ self.logger.info(
58
+ f"Machine onboarding status: {onboarding_state} -> {result_value}"
59
+ )
58
60
  self.logger.method_exit("get_result", result)
59
61
  return result
@@ -41,7 +41,9 @@ class VulnerabilitiesService:
41
41
 
42
42
  # Process and deduplicate vulnerabilities
43
43
  vulnerabilities = self._process_vulnerabilities(raw_vulnerabilities)
44
- self.logger.info(f"Found {len(vulnerabilities)} unique vulnerabilities after deduplication")
44
+ self.logger.info(
45
+ f"Found {len(vulnerabilities)} unique vulnerabilities after deduplication"
46
+ )
45
47
 
46
48
  # Calculate vulnerability score
47
49
  score = VulnerabilityScore()
@@ -54,7 +56,9 @@ class VulnerabilitiesService:
54
56
 
55
57
  for vuln in sorted_vulnerabilities:
56
58
  severity = vuln.severity.lower()
57
- self.logger.debug(f"Processing vulnerability {vuln.id} with severity: {severity}")
59
+ self.logger.debug(
60
+ f"Processing vulnerability {vuln.id} with severity: {severity}"
61
+ )
58
62
 
59
63
  if severity == "critical":
60
64
  score.critical += 1
@@ -75,7 +79,8 @@ class VulnerabilitiesService:
75
79
  self.logger.info(f"Total vulnerability score: {score.total_score}")
76
80
 
77
81
  details.insert(
78
- 0, f"Vulnerabilities: {len(raw_vulnerabilities)}, score: {score.total_score}"
82
+ 0,
83
+ f"Vulnerabilities: {len(raw_vulnerabilities)}, score: {score.total_score}",
79
84
  )
80
85
 
81
86
  result = {"value": score.total_score, "details": details}
@@ -125,7 +130,9 @@ class VulnerabilitiesService:
125
130
  # Sort by severity
126
131
  sorted_vulnerabilities = self._sort_by_severity(vulnerabilities)
127
132
 
128
- self.logger.method_exit("get_detailed_vulnerabilities", len(sorted_vulnerabilities))
133
+ self.logger.method_exit(
134
+ "get_detailed_vulnerabilities", len(sorted_vulnerabilities)
135
+ )
129
136
  return sorted_vulnerabilities
130
137
 
131
138
  def _process_vulnerabilities(
@@ -156,8 +163,11 @@ class VulnerabilitiesService:
156
163
 
157
164
  return unique_vulnerabilities
158
165
 
159
- def _sort_by_severity(self, vulnerabilities: List[Vulnerability]) -> List[Vulnerability]:
166
+ def _sort_by_severity(
167
+ self, vulnerabilities: List[Vulnerability]
168
+ ) -> List[Vulnerability]:
160
169
  """Sort vulnerabilities by severity (Critical > High > Medium > Low)."""
161
170
  return sorted(
162
- vulnerabilities, key=lambda v: self._severity_order.get(v.severity.lower(), 999)
171
+ vulnerabilities,
172
+ key=lambda v: self._severity_order.get(v.severity.lower(), 999),
163
173
  )
@@ -1,16 +1,17 @@
1
1
  [build-system]
2
- requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
3
- build-backend = "setuptools.build_meta"
2
+ requires = [
3
+ "pdm-backend",
4
+ ]
5
+ build-backend = "pdm.backend"
4
6
 
5
7
  [project]
6
8
  name = "check-msdefender"
7
- version = "1.1.1"
9
+ version = "1.1.3"
8
10
  authors = [
9
- {name = "ldvchosal", email = "ldvchosal@github.com"},
11
+ { name = "ldvchosal", email = "ldvchosal@github.com" },
10
12
  ]
11
13
  description = "A Nagios plugin for monitoring Microsoft Defender API endpoints"
12
14
  readme = "README.md"
13
- license = {text = "MIT"}
14
15
  requires-python = ">=3.9"
15
16
  classifiers = [
16
17
  "Development Status :: 5 - Production/Stable",
@@ -24,13 +25,23 @@ classifiers = [
24
25
  "Topic :: System :: Monitoring",
25
26
  "Topic :: System :: Systems Administration",
26
27
  ]
27
- keywords = ["nagios", "monitoring", "microsoft", "graph", "api", "azure"]
28
+ keywords = [
29
+ "nagios",
30
+ "monitoring",
31
+ "microsoft",
32
+ "graph",
33
+ "api",
34
+ "azure",
35
+ ]
28
36
  dependencies = [
29
37
  "nagiosplugin>=1.4.0",
30
38
  "azure-identity>=1.12.0",
31
- "click>=8.0,<9.0"
39
+ "click>=8.0,<9.0",
32
40
  ]
33
41
 
42
+ [project.license]
43
+ text = "MIT"
44
+
34
45
  [project.optional-dependencies]
35
46
  dev = [
36
47
  "pytest>=6.0",
@@ -40,6 +51,8 @@ dev = [
40
51
  "flake8>=3.8",
41
52
  "mypy>=0.800",
42
53
  "twine>=6.2.0",
54
+ "pdm>=2.0.0",
55
+ "ruff>=0.13.0",
43
56
  ]
44
57
 
45
58
  [project.urls]
@@ -52,11 +65,15 @@ Documentation = "https://github.com/lduchosal/check_msdefender/blob/main/README.
52
65
  check_msdefender = "check_msdefender.cli:main"
53
66
 
54
67
  [tool.setuptools.package-data]
55
- "*" = ["*.ini"]
68
+ "*" = [
69
+ "*.ini",
70
+ ]
56
71
 
57
72
  [tool.black]
58
73
  line-length = 100
59
- target-version = ['py39']
74
+ target-version = [
75
+ "py39",
76
+ ]
60
77
 
61
78
  [tool.mypy]
62
79
  python_version = "3.9"
@@ -65,8 +82,25 @@ warn_unused_configs = true
65
82
  disallow_untyped_defs = true
66
83
 
67
84
  [tool.pytest.ini_options]
68
- testpaths = ["tests"]
85
+ testpaths = [
86
+ "tests",
87
+ ]
69
88
  python_files = "test_*.py"
70
89
  python_classes = "Test*"
71
90
  python_functions = "test_*"
72
- addopts = "-v"
91
+ addopts = "-v"
92
+
93
+ [tool.pdm.scripts]
94
+ format = "ruff format"
95
+ typecheck = "mypy check_msdefender/"
96
+ lint = "flake8 check_msdefender/"
97
+ build = "python -m build"
98
+ publish = "python -m twine upload dist/* --verbose"
99
+ test = "pytest -v tests/"
100
+
101
+ [tool.pdm.scripts.all]
102
+ composite = [
103
+ "format",
104
+ "build",
105
+ "test",
106
+ ]
@@ -1,8 +1,8 @@
1
1
  """Mock Defender client for fixture tests."""
2
2
 
3
3
  import json
4
- import os
5
4
  from pathlib import Path
5
+
6
6
  from check_msdefender.core.exceptions import ValidationError
7
7
 
8
8
 
@@ -38,7 +38,9 @@ class MockDefenderClient:
38
38
 
39
39
  def get_machine_vulnerabilities(self, machine_id):
40
40
  """Get vulnerabilities for machine from fixtures."""
41
- return self.vulnerability_data["vulnerabilities_by_machine"].get(machine_id, {"value": []})
41
+ return self.vulnerability_data["vulnerabilities_by_machine"].get(
42
+ machine_id, {"value": []}
43
+ )
42
44
 
43
45
  def get_alerts(self):
44
46
  """Get all alerts from fixtures."""
@@ -67,7 +67,9 @@ class TestAlertsServiceFixtures:
67
67
 
68
68
  def test_get_result_no_parameters(self):
69
69
  """Test error when no parameters provided."""
70
- with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
70
+ with pytest.raises(
71
+ ValidationError, match="Either machine_id or dns_name must be provided"
72
+ ):
71
73
  self.service.get_result()
72
74
 
73
75
  def test_get_result_nonexistent_dns_name(self):
@@ -135,7 +137,8 @@ class TestAlertsServiceFixtures:
135
137
  # Should include alert titles and severity/status
136
138
  assert "suspicious activity detected (new high)" in details_text.lower()
137
139
  assert (
138
- "automated investigation started manually (new informational)" in details_text.lower()
140
+ "automated investigation started manually (new informational)"
141
+ in details_text.lower()
139
142
  )
140
143
 
141
144
  def test_dns_name_matching(self):
@@ -56,7 +56,9 @@ class TestDetailServiceFixtures:
56
56
 
57
57
  def test_get_result_no_parameters(self):
58
58
  """Test error when no parameters provided."""
59
- with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
59
+ with pytest.raises(
60
+ ValidationError, match="Either machine_id or dns_name must be provided"
61
+ ):
60
62
  self.service.get_result()
61
63
 
62
64
  def test_get_result_nonexistent_dns_name(self):
@@ -70,7 +72,9 @@ class TestDetailServiceFixtures:
70
72
 
71
73
  def test_get_result_nonexistent_machine_id(self):
72
74
  """Test error when machine ID doesn't exist."""
73
- with pytest.raises(ValidationError, match="Machine not found: nonexistent-machine"):
75
+ with pytest.raises(
76
+ ValidationError, match="Machine not found: nonexistent-machine"
77
+ ):
74
78
  self.service.get_result(machine_id="nonexistent-machine")
75
79
 
76
80
  def test_machine_details_comprehensive(self):
@@ -96,7 +100,9 @@ class TestDetailServiceFixtures:
96
100
  ]
97
101
 
98
102
  for field in expected_fields:
99
- assert field in details_dict, f"Field '{field}' missing from machine details"
103
+ assert field in details_dict, (
104
+ f"Field '{field}' missing from machine details"
105
+ )
100
106
 
101
107
  # Verify specific values for test-machine-3
102
108
  assert details_dict["id"] == "test-machine-3"