check-msdefender 1.1.2__tar.gz → 1.1.4__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 (50) hide show
  1. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/PKG-INFO +2 -2
  2. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/README.md +0 -1
  3. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/decorators.py +3 -1
  4. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/core/auth.py +3 -1
  5. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/core/defender.py +19 -9
  6. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/core/logging_config.py +5 -2
  7. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/core/nagios.py +3 -1
  8. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/alerts_service.py +15 -5
  9. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/detail_service.py +9 -3
  10. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/lastseen_service.py +3 -1
  11. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/machines_service.py +9 -2
  12. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/onboarding_service.py +3 -1
  13. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/vulnerabilities_service.py +16 -6
  14. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/pyproject.toml +14 -17
  15. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/mock_defender_client.py +3 -1
  16. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/test_alerts_service.py +5 -2
  17. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/test_detail_service.py +9 -3
  18. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/test_lastseen_service.py +3 -1
  19. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/test_onboarding_service.py +3 -1
  20. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/test_vulnerabilities_service.py +3 -1
  21. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/integration/test_cli_integration.py +60 -16
  22. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/integration/test_lastseen_integration.py +17 -3
  23. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/unit/test_alerts_service.py +9 -3
  24. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/unit/test_detail_service.py +22 -7
  25. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/LICENSE +0 -0
  26. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/__init__.py +0 -0
  27. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/__main__.py +0 -0
  28. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/check_msdefender.py +0 -0
  29. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/__init__.py +0 -0
  30. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/__main__.py +0 -0
  31. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/commands/__init__.py +0 -0
  32. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/commands/alerts.py +0 -0
  33. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/commands/detail.py +0 -0
  34. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/commands/lastseen.py +0 -0
  35. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/commands/machines.py +0 -0
  36. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/commands/onboarding.py +0 -0
  37. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/commands/vulnerabilities.py +0 -0
  38. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/cli/handlers.py +0 -0
  39. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/core/__init__.py +0 -0
  40. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/core/config.py +0 -0
  41. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/core/exceptions.py +0 -0
  42. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/__init__.py +0 -0
  43. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/check_msdefender/services/models.py +0 -0
  44. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/__init__.py +0 -0
  45. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/__init__.py +0 -0
  46. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/alerts_data.json +0 -0
  47. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/machine_data.json +0 -0
  48. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/fixtures/vulnerability_data.json +0 -0
  49. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/integration/__init__.py +0 -0
  50. {check_msdefender-1.1.2 → check_msdefender-1.1.4}/tests/unit/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: check-msdefender
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: A Nagios plugin for monitoring Microsoft Defender API endpoints
5
5
  Keywords: nagios,monitoring,microsoft,graph,api,azure
6
6
  Author-Email: ldvchosal <ldvchosal@github.com>
@@ -32,6 +32,7 @@ Requires-Dist: flake8>=3.8; extra == "dev"
32
32
  Requires-Dist: mypy>=0.800; extra == "dev"
33
33
  Requires-Dist: twine>=6.2.0; extra == "dev"
34
34
  Requires-Dist: pdm>=2.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.13.0; extra == "dev"
35
36
  Description-Content-Type: text/markdown
36
37
 
37
38
  # 🛡️ Check MS Defender
@@ -294,7 +295,6 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
294
295
 
295
296
  # Install in development mode
296
297
  pip install -e .
297
- pip install -r requirements-dev.txt
298
298
  ```
299
299
 
300
300
  ### Code Quality Tools
@@ -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
@@ -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", "-i", "--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)
@@ -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 = "eu",
20
+ verbose_level: int = 0,
17
21
  ) -> None:
18
22
  """Initialize with authenticator and optional region.
19
23
 
@@ -32,12 +36,12 @@ class DefenderClient:
32
36
  def _get_base_url(self, region: str) -> str:
33
37
  """Get base URL for the specified region."""
34
38
  endpoints = {
35
- "eu": "https://api-eu.securitycenter.microsoft.com",
36
- "eu3": "https://api-eu3.securitycenter.microsoft.com",
37
- "us": "https://api.securitycenter.microsoft.com",
38
- "uk": "https://api-uk.securitycenter.microsoft.com",
39
+ "eu": "https://eu.api.security.microsoft.com",
40
+ "us": "https://us.api.security.microsoft.com",
41
+ "uk": "https://uk.api.security.microsoft.com",
42
+ "api": "https://api.security.microsoft.com",
39
43
  }
40
- return endpoints.get(region, endpoints["eu3"])
44
+ return endpoints.get(region, endpoints["eu"])
41
45
 
42
46
  def get_machine_by_dns_name(self, dns_name: str) -> Dict[str, Any]:
43
47
  """Get machine information by DNS name."""
@@ -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),
@@ -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:
@@ -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 "✗"
@@ -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
  )
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "check-msdefender"
9
- version = "1.1.2"
9
+ version = "1.1.4"
10
10
  authors = [
11
11
  { name = "ldvchosal", email = "ldvchosal@github.com" },
12
12
  ]
@@ -52,6 +52,7 @@ dev = [
52
52
  "mypy>=0.800",
53
53
  "twine>=6.2.0",
54
54
  "pdm>=2.0.0",
55
+ "ruff>=0.13.0",
55
56
  ]
56
57
 
57
58
  [project.urls]
@@ -90,31 +91,27 @@ python_functions = "test_*"
90
91
  addopts = "-v"
91
92
 
92
93
  [tool.pdm.scripts]
93
- format = "black ."
94
+ format = "ruff format"
94
95
  typecheck = "mypy check_msdefender/"
95
96
  lint = "flake8 check_msdefender/"
96
97
  build = "python -m build"
97
98
  publish = "python -m twine upload dist/* --verbose"
98
99
  test = "pytest -v tests/"
99
- msdhelp = "check_msdefender --help"
100
- msdmachines = "check_msdefender machines"
101
- msdlastseen = "check_msdefender lastseen -d $MACHINE"
102
- msddetail = "check_msdefender detail -d $MACHINE"
103
- msdalerts = "check_msdefender alerts -d $MACHINE"
104
- msdvulnerabilities = "check_msdefender vulnerabilities -d $MACHINE"
105
- msdonboarding = "check_msdefender onboarding -d $MACHINE"
100
+ install = "pdm install"
101
+ clihelp = "check_msdefender --help"
102
+ climachines = "check_msdefender machines"
103
+
104
+ [tool.pdm.scripts.cli]
105
+ composite = [
106
+ "clihelp",
107
+ "climachines",
108
+ ]
106
109
 
107
110
  [tool.pdm.scripts.all]
108
111
  composite = [
112
+ "install",
109
113
  "format",
110
114
  "build",
111
115
  "test",
112
- "typecheck",
113
- "lint",
114
- ]
115
-
116
- [tool.pdm.scripts.msdall]
117
- composite = [
118
- "msdhelp",
119
- "msdmachines",
116
+ "cli",
120
117
  ]
@@ -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"
@@ -44,7 +44,9 @@ class TestLastSeenServiceFixtures:
44
44
 
45
45
  def test_get_result_no_parameters(self):
46
46
  """Test error when no parameters provided."""
47
- with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
47
+ with pytest.raises(
48
+ ValidationError, match="Either machine_id or dns_name must be provided"
49
+ ):
48
50
  self.service.get_result()
49
51
 
50
52
  def test_get_result_nonexistent_dns_name(self):
@@ -43,7 +43,9 @@ class TestOnboardingServiceFixtures:
43
43
 
44
44
  def test_get_result_no_parameters(self):
45
45
  """Test error when no parameters provided."""
46
- with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
46
+ with pytest.raises(
47
+ ValidationError, match="Either machine_id or dns_name must be provided"
48
+ ):
47
49
  self.service.get_result()
48
50
 
49
51
  def test_get_result_nonexistent_dns_name(self):
@@ -41,7 +41,9 @@ class TestVulnerabilitiesServiceFixtures:
41
41
 
42
42
  def test_get_result_no_parameters(self):
43
43
  """Test error when no parameters provided."""
44
- with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
44
+ with pytest.raises(
45
+ ValidationError, match="Either machine_id or dns_name must be provided"
46
+ ):
45
47
  self.service.get_result()
46
48
 
47
49
  def test_get_result_nonexistent_dns_name(self):
@@ -21,7 +21,10 @@ class TestHelpCommand:
21
21
  result = cli_runner.invoke(main, ["--help"])
22
22
 
23
23
  assert result.exit_code == 0
24
- assert "Check Microsoft Defender API endpoints and validate values." in result.output
24
+ assert (
25
+ "Check Microsoft Defender API endpoints and validate values."
26
+ in result.output
27
+ )
25
28
  assert "Commands:" in result.output
26
29
  assert "lastseen" in result.output
27
30
  assert "onboarding" in result.output
@@ -33,7 +36,10 @@ class TestHelpCommand:
33
36
  result = cli_runner.invoke(main, ["--help"])
34
37
 
35
38
  assert result.exit_code == 0
36
- assert "Check Microsoft Defender API endpoints and validate values." in result.output
39
+ assert (
40
+ "Check Microsoft Defender API endpoints and validate values."
41
+ in result.output
42
+ )
37
43
 
38
44
 
39
45
  class TestLastSeenCommand:
@@ -88,7 +94,11 @@ class TestLastSeenCommand:
88
94
 
89
95
  assert result.exit_code == 0
90
96
  mock_plugin.check.assert_called_once_with(
91
- machine_id=None, dns_name="machine.domain.tld", warning=7, critical=30, verbose=0
97
+ machine_id=None,
98
+ dns_name="machine.domain.tld",
99
+ warning=7,
100
+ critical=30,
101
+ verbose=0,
92
102
  )
93
103
 
94
104
  @patch("check_msdefender.cli.commands.lastseen.load_config")
@@ -129,7 +139,11 @@ class TestOnboardingCommand:
129
139
 
130
140
  assert result.exit_code == 0
131
141
  mock_plugin.check.assert_called_once_with(
132
- machine_id=None, dns_name="machine.domain.tld", warning=1, critical=2, verbose=0
142
+ machine_id=None,
143
+ dns_name="machine.domain.tld",
144
+ warning=1,
145
+ critical=2,
146
+ verbose=0,
133
147
  )
134
148
 
135
149
  @patch("check_msdefender.cli.commands.onboarding.load_config")
@@ -165,11 +179,17 @@ class TestVulnerabilitiesCommand:
165
179
  mock_nagios.return_value = mock_plugin
166
180
  mock_plugin.check.return_value = 0
167
181
 
168
- result = cli_runner.invoke(main, ["vulnerabilities", "-d", "machine.domain.tld"])
182
+ result = cli_runner.invoke(
183
+ main, ["vulnerabilities", "-d", "machine.domain.tld"]
184
+ )
169
185
 
170
186
  assert result.exit_code == 0
171
187
  mock_plugin.check.assert_called_once_with(
172
- machine_id=None, dns_name="machine.domain.tld", warning=10, critical=100, verbose=0
188
+ machine_id=None,
189
+ dns_name="machine.domain.tld",
190
+ warning=10,
191
+ critical=100,
192
+ verbose=0,
173
193
  )
174
194
 
175
195
  @patch("check_msdefender.cli.commands.vulnerabilities.load_config")
@@ -191,11 +211,17 @@ class TestVulnerabilitiesCommand:
191
211
  mock_nagios.return_value = mock_plugin
192
212
  mock_plugin.check.return_value = 0
193
213
 
194
- result = cli_runner.invoke(main, ["vulnerabilities", "-d", "machine.domain.tld", "-v"])
214
+ result = cli_runner.invoke(
215
+ main, ["vulnerabilities", "-d", "machine.domain.tld", "-v"]
216
+ )
195
217
 
196
218
  assert result.exit_code == 0
197
219
  mock_plugin.check.assert_called_once_with(
198
- machine_id=None, dns_name="machine.domain.tld", warning=10, critical=100, verbose=1
220
+ machine_id=None,
221
+ dns_name="machine.domain.tld",
222
+ warning=10,
223
+ critical=100,
224
+ verbose=1,
199
225
  )
200
226
 
201
227
  @patch("check_msdefender.cli.commands.vulnerabilities.load_config")
@@ -217,11 +243,17 @@ class TestVulnerabilitiesCommand:
217
243
  mock_nagios.return_value = mock_plugin
218
244
  mock_plugin.check.return_value = 0
219
245
 
220
- result = cli_runner.invoke(main, ["vulnerabilities", "-d", "machine.domain.tld", "-vvvv"])
246
+ result = cli_runner.invoke(
247
+ main, ["vulnerabilities", "-d", "machine.domain.tld", "-vvvv"]
248
+ )
221
249
 
222
250
  assert result.exit_code == 0
223
251
  mock_plugin.check.assert_called_once_with(
224
- machine_id=None, dns_name="machine.domain.tld", warning=10, critical=100, verbose=4
252
+ machine_id=None,
253
+ dns_name="machine.domain.tld",
254
+ warning=10,
255
+ critical=100,
256
+ verbose=4,
225
257
  )
226
258
 
227
259
  @patch("check_msdefender.cli.commands.vulnerabilities.load_config")
@@ -229,7 +261,9 @@ class TestVulnerabilitiesCommand:
229
261
  """Test vulnerabilities command error handling."""
230
262
  mock_config.side_effect = Exception("Service unavailable")
231
263
 
232
- result = cli_runner.invoke(main, ["vulnerabilities", "-d", "machine.domain.tld"])
264
+ result = cli_runner.invoke(
265
+ main, ["vulnerabilities", "-d", "machine.domain.tld"]
266
+ )
233
267
 
234
268
  assert result.exit_code == 3
235
269
  assert "UNKNOWN: Service unavailable" in result.output
@@ -257,7 +291,9 @@ class TestDetailCommand:
257
291
  "value": 1,
258
292
  "details": ["Machine ID: test-machine", "Computer Name: test-pc"],
259
293
  }
260
- mock_service_instance.get_machine_details_json.return_value = '{"id": "test-machine"}'
294
+ mock_service_instance.get_machine_details_json.return_value = (
295
+ '{"id": "test-machine"}'
296
+ )
261
297
 
262
298
  result = cli_runner.invoke(main, ["detail", "-i", "test-machine-123"])
263
299
 
@@ -287,7 +323,9 @@ class TestDetailCommand:
287
323
  "value": 1,
288
324
  "details": ["Machine ID: test-machine", "Computer Name: test-pc"],
289
325
  }
290
- mock_service_instance.get_machine_details_json.return_value = '{"id": "test-machine"}'
326
+ mock_service_instance.get_machine_details_json.return_value = (
327
+ '{"id": "test-machine"}'
328
+ )
291
329
 
292
330
  result = cli_runner.invoke(main, ["detail", "-m", "test-machine-456"])
293
331
 
@@ -349,7 +387,9 @@ class TestDetailCommand:
349
387
  "details": ["Machine not found with DNS name: nonexistent.domain.com"],
350
388
  }
351
389
 
352
- result = cli_runner.invoke(main, ["detail", "-d", "nonexistent.domain.com", "-W", "0"])
390
+ result = cli_runner.invoke(
391
+ main, ["detail", "-d", "nonexistent.domain.com", "-W", "0"]
392
+ )
353
393
 
354
394
  assert result.exit_code == 1 # Warning
355
395
  assert "DEFENDER WARNING - Machine not found" in result.output
@@ -375,7 +415,9 @@ class TestDetailCommand:
375
415
  "details": ["Machine not found with DNS name: nonexistent.domain.com"],
376
416
  }
377
417
 
378
- result = cli_runner.invoke(main, ["detail", "-d", "nonexistent.domain.com", "-C", "0"])
418
+ result = cli_runner.invoke(
419
+ main, ["detail", "-d", "nonexistent.domain.com", "-C", "0"]
420
+ )
379
421
 
380
422
  assert result.exit_code == 2 # Critical
381
423
  assert "DEFENDER CRITICAL - Machine not found" in result.output
@@ -396,6 +438,8 @@ class TestDetailCommand:
396
438
  result = cli_runner.invoke(main, ["detail", "--help"])
397
439
 
398
440
  assert result.exit_code == 0
399
- assert "Get detailed machine information from Microsoft Defender." in result.output
441
+ assert (
442
+ "Get detailed machine information from Microsoft Defender." in result.output
443
+ )
400
444
  assert "-m, -i, --machine-id, --id TEXT" in result.output
401
445
  assert "-d, --dns-name TEXT" in result.output
@@ -48,14 +48,26 @@ def test_lastseen_command_success(
48
48
  # Execute command
49
49
  result = cli_runner.invoke(
50
50
  mock_app,
51
- ["lastseen", "--machine-id", "test-machine-id", "--warning", "5", "--critical", "10"],
51
+ [
52
+ "lastseen",
53
+ "--machine-id",
54
+ "test-machine-id",
55
+ "--warning",
56
+ "5",
57
+ "--critical",
58
+ "10",
59
+ ],
52
60
  )
53
61
 
54
62
  # Verify result
55
63
  assert result.exit_code == 0
56
64
  mock_nagios.assert_called_once_with(mock_service_instance, "lastseen")
57
65
  mock_plugin.check.assert_called_once_with(
58
- machine_id="test-machine-id", dns_name=None, warning=5, critical=10, verbose=False
66
+ machine_id="test-machine-id",
67
+ dns_name=None,
68
+ warning=5,
69
+ critical=10,
70
+ verbose=False,
59
71
  )
60
72
 
61
73
 
@@ -66,7 +78,9 @@ def test_lastseen_command_exception(mock_config, cli_runner, mock_app):
66
78
  mock_config.side_effect = Exception("Test error")
67
79
 
68
80
  # Execute command
69
- result = cli_runner.invoke(mock_app, ["lastseen", "--machine-id", "test-machine-id"])
81
+ result = cli_runner.invoke(
82
+ mock_app, ["lastseen", "--machine-id", "test-machine-id"]
83
+ )
70
84
 
71
85
  # Verify error handling
72
86
  assert result.exit_code == 3 # Now properly returns exit code 3 for UNKNOWN
@@ -101,7 +101,9 @@ class TestAlertsService:
101
101
  assert len(result["details"]) == 2 # Summary line + 1 alert detail
102
102
 
103
103
  # Should call DNS lookup
104
- self.mock_client.get_machine_by_dns_name.assert_called_once_with("test.example.com")
104
+ self.mock_client.get_machine_by_dns_name.assert_called_once_with(
105
+ "test.example.com"
106
+ )
105
107
  self.mock_client.get_alerts.assert_called_once()
106
108
 
107
109
  def test_get_result_by_dns_name_not_found(self):
@@ -113,12 +115,16 @@ class TestAlertsService:
113
115
  with pytest.raises(ValidationError, match="Machine not found with DNS name"):
114
116
  self.service.get_result(dns_name="nonexistent.domain.com")
115
117
 
116
- self.mock_client.get_machine_by_dns_name.assert_called_once_with("nonexistent.domain.com")
118
+ self.mock_client.get_machine_by_dns_name.assert_called_once_with(
119
+ "nonexistent.domain.com"
120
+ )
117
121
  self.mock_client.get_alerts.assert_not_called()
118
122
 
119
123
  def test_get_result_no_parameters(self):
120
124
  """Test error when no parameters provided."""
121
- with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
125
+ with pytest.raises(
126
+ ValidationError, match="Either machine_id or dns_name must be provided"
127
+ ):
122
128
  self.service.get_result()
123
129
 
124
130
  self.mock_client.get_machine_by_dns_name.assert_not_called()
@@ -49,7 +49,10 @@ class TestDetailService:
49
49
  """Test successful retrieval by DNS name."""
50
50
  # Mock DNS lookup response
51
51
  mock_dns_response = {"value": [{"id": "test-machine-456"}]}
52
- mock_machine_data = {"id": "test-machine-456", "computerDnsName": "test.domain.com"}
52
+ mock_machine_data = {
53
+ "id": "test-machine-456",
54
+ "computerDnsName": "test.domain.com",
55
+ }
53
56
 
54
57
  self.mock_client.get_machine_by_dns_name.return_value = mock_dns_response
55
58
  self.mock_client.get_machine_by_id.return_value = mock_machine_data
@@ -60,7 +63,9 @@ class TestDetailService:
60
63
  assert result["value"] == 1
61
64
 
62
65
  # Should call both DNS lookup and machine ID retrieval
63
- self.mock_client.get_machine_by_dns_name.assert_called_once_with("test.domain.com")
66
+ self.mock_client.get_machine_by_dns_name.assert_called_once_with(
67
+ "test.domain.com"
68
+ )
64
69
  self.mock_client.get_machine_by_id.assert_called_once_with("test-machine-456")
65
70
 
66
71
  def test_get_result_by_dns_name_not_found(self):
@@ -75,12 +80,16 @@ class TestDetailService:
75
80
  assert result["value"] == 0
76
81
 
77
82
  # Should not call get_machine_by_id since DNS lookup failed
78
- self.mock_client.get_machine_by_dns_name.assert_called_once_with("nonexistent.domain.com")
83
+ self.mock_client.get_machine_by_dns_name.assert_called_once_with(
84
+ "nonexistent.domain.com"
85
+ )
79
86
  self.mock_client.get_machine_by_id.assert_not_called()
80
87
 
81
88
  def test_get_result_no_parameters(self):
82
89
  """Test error when no parameters provided."""
83
- with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
90
+ with pytest.raises(
91
+ ValidationError, match="Either machine_id or dns_name must be provided"
92
+ ):
84
93
  self.service.get_result()
85
94
 
86
95
  # Should not make any API calls
@@ -152,9 +161,15 @@ class TestDetailService:
152
161
  self.mock_client.get_machine_by_id.return_value = mock_machine_data
153
162
 
154
163
  # Call with both parameters - DNS name should be used first
155
- result = self.service.get_result(machine_id="direct-machine", dns_name="test.domain.com")
164
+ result = self.service.get_result(
165
+ machine_id="direct-machine", dns_name="test.domain.com"
166
+ )
156
167
 
157
168
  assert result["value"] == 1
158
169
  # Should resolve via DNS first
159
- self.mock_client.get_machine_by_dns_name.assert_called_once_with("test.domain.com")
160
- self.mock_client.get_machine_by_id.assert_called_once_with("dns-resolved-machine")
170
+ self.mock_client.get_machine_by_dns_name.assert_called_once_with(
171
+ "test.domain.com"
172
+ )
173
+ self.mock_client.get_machine_by_id.assert_called_once_with(
174
+ "dns-resolved-machine"
175
+ )