check-msdefender 1.1.2__py3-none-any.whl → 1.1.4__py3-none-any.whl

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.
@@ -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
  )
@@ -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
@@ -1,7 +1,7 @@
1
- check_msdefender-1.1.2.dist-info/METADATA,sha256=fJ9nhfz8Hx61xVoQ0KR-l7rNaJOCcqxU9YqBB3wrC1g,14071
2
- check_msdefender-1.1.2.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- check_msdefender-1.1.2.dist-info/entry_points.txt,sha256=OqVzHI1PaD9V22g0K7BhA2nYv4O-pH8mcLzuGdsk5rM,79
4
- check_msdefender-1.1.2.dist-info/licenses/LICENSE,sha256=kW3DwIsKc9HVYdS4f4tI6sLo-EPqBQbz-WmuvHU4Nak,1065
1
+ check_msdefender-1.1.4.dist-info/METADATA,sha256=XGMBO_Ya5rReVoyq3pbxitdBA4S2J1yC_MF9S1XBb_Y,14079
2
+ check_msdefender-1.1.4.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ check_msdefender-1.1.4.dist-info/entry_points.txt,sha256=OqVzHI1PaD9V22g0K7BhA2nYv4O-pH8mcLzuGdsk5rM,79
4
+ check_msdefender-1.1.4.dist-info/licenses/LICENSE,sha256=kW3DwIsKc9HVYdS4f4tI6sLo-EPqBQbz-WmuvHU4Nak,1065
5
5
  check_msdefender/__init__.py,sha256=HDn1Ub7Ohqkfko0kUPT8w7HqU52jXwo-leSTImO1x_k,161
6
6
  check_msdefender/__main__.py,sha256=TuNsRSdnkQm9OdBTAwD5aB2zV_Irc50WgylVWhrfnLY,124
7
7
  check_msdefender/check_msdefender.py,sha256=OO4Tg2DBW28AT-2LOH-qJM2pE5TPcF615BF7HjyZsmA,137
@@ -14,21 +14,21 @@ check_msdefender/cli/commands/lastseen.py,sha256=my-kW00ioaFdmec3zjqrLk12kt9Pld8
14
14
  check_msdefender/cli/commands/machines.py,sha256=uyQal7P4VI4a3dECFWgXKBiUPcdxhUrpWFOyKHmpORU,1724
15
15
  check_msdefender/cli/commands/onboarding.py,sha256=5QSP75uyrX0MQ1ABiGFSDKIzVszLF8U3uQ4bqFF9F2g,1912
16
16
  check_msdefender/cli/commands/vulnerabilities.py,sha256=fl8NYAO4Ug2Yk5NmQ6CT9TuibhFHWE01R6YjDIPgYl4,1931
17
- check_msdefender/cli/decorators.py,sha256=wRUv4vY6SL3nFjpYW9h1M1xDO_pzA6--gCtg3y6MmQM,786
17
+ check_msdefender/cli/decorators.py,sha256=EWgchw56-_ZIYEdwZNZsc5LncSw6obFp1RXKGTknU2E,800
18
18
  check_msdefender/cli/handlers.py,sha256=hp_CX_3qPoQGrPPVeiojb2j7tuFMva4ebWg9CxVUiPg,1395
19
19
  check_msdefender/core/__init__.py,sha256=naBiEkixiWTuHU3GENk8fqC8H3p_hkzRsmSY2uiM_TQ,47
20
- check_msdefender/core/auth.py,sha256=7mkGmhGHy4t38O0e4Rz7dQ52xfMbK3IUXMlw3u83aB4,1585
20
+ check_msdefender/core/auth.py,sha256=TDolCrwQoo37poaNUYJC2kgp4QpeH0W9AOathYCIUgw,1607
21
21
  check_msdefender/core/config.py,sha256=IoWBL_DB110F4i6hFfli6iFDBXx57dHh32lCuLkcgNk,1170
22
- check_msdefender/core/defender.py,sha256=XicGQrWuc9UCBr_Vc0SZxNSoZ_OwFXiFwfP_c9-eRtc,8838
22
+ check_msdefender/core/defender.py,sha256=bP0oB_Y9uu1Rx6gZeYc6KDJDcrmUQdgPaHRaswHXa5U,8934
23
23
  check_msdefender/core/exceptions.py,sha256=X4s_XM64SEVSs-4mGKqnF8xXwGFY3E0buvkgRNuCCX4,600
24
- check_msdefender/core/logging_config.py,sha256=27gLjvbP_AgedDQWZQEFfn_CGn5y6HcJQlI5jlxQHow,4067
25
- check_msdefender/core/nagios.py,sha256=nZSo-1VV57WFSieyRp456tw_OqpjXOoM_MEjnLkgxlE,6600
24
+ check_msdefender/core/logging_config.py,sha256=J4vdxbJBM4LDpbNvjRsVPZcsvW0yfRz7TnkJmOwyjpE,4122
25
+ check_msdefender/core/nagios.py,sha256=uIp_WRkr-27gyjL1YKgzMhfcBmJt-0aSbm5XjtYyAN8,6632
26
26
  check_msdefender/services/__init__.py,sha256=_fiKXxcz263IghXn9BnUWDKPgedhUPoSakEN3tBd2SU,44
27
- check_msdefender/services/alerts_service.py,sha256=poKZw1WKphmtPPnuMDrGRuPQbRLjLDZpo2rhFCh7TDc,4034
28
- check_msdefender/services/detail_service.py,sha256=i-jXubNfsNf-fS6ba2MQecN886GzU0UC40DwS3HrnnY,3382
29
- check_msdefender/services/lastseen_service.py,sha256=LiNVeUbAoMzowMvE90P7zCtKFHBLbIDp5mmkVHRLwqs,3128
30
- check_msdefender/services/machines_service.py,sha256=XTiEctJ2EmZrjPu3xBlFbC_UkkPbUROrPXmqX2bUYss,3059
27
+ check_msdefender/services/alerts_service.py,sha256=XxtP31Zt0WcfkW65YlJgoSVRxRX3yAJ4LTpgx9er-xc,4162
28
+ check_msdefender/services/detail_service.py,sha256=PMoBv_m0udwWdPus5SvDx49V6iP9ERGWg5pX5cGARvA,3472
29
+ check_msdefender/services/lastseen_service.py,sha256=T98Axx4ziUEavibXuG5FB2eHTyAcxktNM1DI4Y5fo1c,3158
30
+ check_msdefender/services/machines_service.py,sha256=XYWnAliNo1DyIvdQ5xcPh133_CaW8U39WGdmiATwZps,3166
31
31
  check_msdefender/services/models.py,sha256=CDmQ5vU0-GawIalqXjXNk3rry6gsyjv6eSlW2NiXwQ0,979
32
- check_msdefender/services/onboarding_service.py,sha256=RIOsvALCoKV0YqnCHKYRkelSPrO-F-6vNBLlto4MpiI,2686
33
- check_msdefender/services/vulnerabilities_service.py,sha256=ikD6E-hg7LtvCiTg7cTCqGSTly6Wgtql82NJD81D2n0,6812
34
- check_msdefender-1.1.2.dist-info/RECORD,,
32
+ check_msdefender/services/onboarding_service.py,sha256=5ekrDlt6Vc__28O7b8niY-KPA14Dnw0UbuSgdx7_Lyg,2708
33
+ check_msdefender/services/vulnerabilities_service.py,sha256=DfU6NaAkm-X5BVL3Zfm3lDe21vE-r1ErYh3r8nZLA6I,6926
34
+ check_msdefender-1.1.4.dist-info/RECORD,,