check-msdefender 1.1.0__tar.gz → 1.1.1__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.0 → check_msdefender-1.1.1}/PKG-INFO +37 -2
  2. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/README.md +36 -1
  3. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/PKG-INFO +37 -2
  4. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/SOURCES.txt +3 -0
  5. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/pyproject.toml +1 -1
  6. check_msdefender-1.1.1/tests/fixtures/alerts_data.json +72 -0
  7. check_msdefender-1.1.1/tests/fixtures/test_alerts_service.py +150 -0
  8. check_msdefender-1.1.1/tests/unit/test_alerts_service.py +267 -0
  9. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.claude/settings.local.json +0 -0
  10. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.env.example +0 -0
  11. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.github/workflows/python-package.yml +0 -0
  12. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.github/workflows/python-publish.yml +0 -0
  13. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.gitignore +0 -0
  14. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/.gitignore +0 -0
  15. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/check_msdefender.iml +0 -0
  16. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/dictionaries/project.xml +0 -0
  17. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/encodings.xml +0 -0
  18. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  19. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/misc.xml +0 -0
  20. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/modules.xml +0 -0
  21. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/runConfigurations/Integration_Tests.xml +0 -0
  22. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/vcs.xml +0 -0
  23. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/LICENSE +0 -0
  24. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/__init__.py +0 -0
  25. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/__main__.py +0 -0
  26. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/check_msdefender.py +0 -0
  27. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/__init__.py +0 -0
  28. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/__main__.py +0 -0
  29. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/__init__.py +0 -0
  30. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/alerts.py +0 -0
  31. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/detail.py +0 -0
  32. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/lastseen.py +0 -0
  33. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/machines.py +0 -0
  34. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/onboarding.py +0 -0
  35. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/vulnerabilities.py +0 -0
  36. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/decorators.py +0 -0
  37. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/handlers.py +0 -0
  38. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/__init__.py +0 -0
  39. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/auth.py +0 -0
  40. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/config.py +0 -0
  41. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/defender.py +0 -0
  42. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/exceptions.py +0 -0
  43. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/logging_config.py +0 -0
  44. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/nagios.py +0 -0
  45. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/__init__.py +0 -0
  46. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/alerts_service.py +0 -0
  47. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/detail_service.py +0 -0
  48. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/lastseen_service.py +0 -0
  49. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/machines_service.py +0 -0
  50. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/models.py +0 -0
  51. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/onboarding_service.py +0 -0
  52. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/vulnerabilities_service.py +0 -0
  53. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/dependency_links.txt +0 -0
  54. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/entry_points.txt +0 -0
  55. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/requires.txt +0 -0
  56. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/top_level.txt +0 -0
  57. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.ini.example +0 -0
  58. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Click-Decorators-ErrorHandlers-Formatters.md +0 -0
  59. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Click-Groups.md +0 -0
  60. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Enhance-MsDefender-Vulnerabilities-Output.md +0 -0
  61. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Fixture-Tests.md +0 -0
  62. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Integration-Tests.md +0 -0
  63. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender-Alerts.md +0 -0
  64. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender-DetailMachine.md +0 -0
  65. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender-ListMachines.md +0 -0
  66. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender.md +0 -0
  67. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Nagios-Detailed-Output.md +0 -0
  68. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Nagios-Exit-Code.md +0 -0
  69. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Nagios-Output.md +0 -0
  70. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Pypi-Package.md +0 -0
  71. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Verbose.md +0 -0
  72. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/pytest.ini +0 -0
  73. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/requirements-dev.txt +0 -0
  74. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/requirements.txt +0 -0
  75. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/setup.cfg +0 -0
  76. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/test_verbose.py +0 -0
  77. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/__init__.py +0 -0
  78. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/__init__.py +0 -0
  79. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/machine_data.json +0 -0
  80. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/mock_defender_client.py +0 -0
  81. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_detail_service.py +0 -0
  82. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_lastseen_service.py +0 -0
  83. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_onboarding_service.py +0 -0
  84. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_vulnerabilities_service.py +0 -0
  85. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/vulnerability_data.json +0 -0
  86. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/integration/__init__.py +0 -0
  87. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/integration/test_cli_integration.py +0 -0
  88. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/integration/test_lastseen_integration.py +0 -0
  89. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/unit/__init__.py +0 -0
  90. {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/unit/test_detail_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: check-msdefender
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: A Nagios plugin for monitoring Microsoft Defender API endpoints
5
5
  Author-email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT
@@ -46,7 +46,7 @@ A comprehensive **Nagios plugin** for monitoring Microsoft Defender for Endpoint
46
46
  ## ✨ Features
47
47
 
48
48
  - 🔐 **Dual Authentication** - Support for Client Secret and Certificate-based authentication
49
- - 🎯 **Multiple Endpoints** - Monitor onboarding status, last seen, vulnerabilities, and machine details
49
+ - 🎯 **Multiple Endpoints** - Monitor onboarding status, last seen, vulnerabilities, alerts, and machine details
50
50
  - 📊 **Nagios Compatible** - Standard exit codes and performance data output
51
51
  - 🏗️ **Clean Architecture** - Modular design with testable components
52
52
  - 🔧 **Flexible Configuration** - File-based configuration with sensible defaults
@@ -78,6 +78,9 @@ check_msdefender lastseen -d machine.domain.tld -W 7 -C 30
78
78
  # Check vulnerabilities
79
79
  check_msdefender vulnerabilities -d machine.domain.tld -W 10 -C 100
80
80
 
81
+ # Check alerts
82
+ check_msdefender alerts -d machine.domain.tld -W 1 -C 5
83
+
81
84
  # List all machines
82
85
  check_msdefender machines
83
86
 
@@ -92,6 +95,7 @@ check_msdefender detail -d machine.domain.tld
92
95
  | `onboarding` | Check machine onboarding status | W:1, C:2 |
93
96
  | `lastseen` | Days since machine last seen | W:7, C:30 |
94
97
  | `vulnerabilities` | Vulnerability score calculation | W:10, C:100 |
98
+ | `alerts` | Count of unresolved alerts | W:1, C:0 |
95
99
  | `machines` | List all machines | W:10, C:25 |
96
100
  | `detail` | Get detailed machine information | - |
97
101
 
@@ -103,6 +107,14 @@ The vulnerability score is calculated as:
103
107
  - **Medium vulnerabilities** × 5
104
108
  - **Low vulnerabilities** × 1
105
109
 
110
+ ### Alert Monitoring
111
+
112
+ The alerts command monitors unresolved security alerts for a machine:
113
+ - **Counts only unresolved alerts** (status ≠ "Resolved")
114
+ - **Excludes informational alerts** when critical/warning alerts exist
115
+ - **Shows alert details** including creation time, title, and severity
116
+ - **Default thresholds**: Warning at 1 alert, Critical at 0 (meaning any alert triggers warning)
117
+
106
118
  ### Onboarding Status Values
107
119
 
108
120
  - `0` - Onboarded ✅
@@ -145,6 +157,7 @@ timeout = 5
145
157
  - `Machine.Read.All`
146
158
  - `Vulnerability.Read`
147
159
  - `Vulnerability.Read.All`
160
+ - `Alert.Read.All`
148
161
  3. **Create Authentication** (Secret or Certificate)
149
162
  4. **Note Credentials** (Client ID, Tenant ID, Secret/Certificate)
150
163
 
@@ -182,6 +195,11 @@ define command {
182
195
  command_name check_defender_vulnerabilities
183
196
  command_line $USER1$/check_msdefender/bin/check_msdefender vulnerabilities -d $HOSTALIAS$ -W 10 -C 100
184
197
  }
198
+
199
+ define command {
200
+ command_name check_defender_alerts
201
+ command_line $USER1$/check_msdefender/bin/check_msdefender alerts -d $HOSTALIAS$ -W 1 -C 5
202
+ }
185
203
  ```
186
204
 
187
205
  ### Service Definitions
@@ -208,6 +226,13 @@ define service {
208
226
  check_command check_defender_vulnerabilities
209
227
  hostgroup_name msdefender
210
228
  }
229
+
230
+ define service {
231
+ use generic-service
232
+ service_description DEFENDER_ALERTS
233
+ check_command check_defender_alerts
234
+ hostgroup_name msdefender
235
+ }
211
236
  ```
212
237
 
213
238
  ## 🏗️ Architecture
@@ -221,6 +246,7 @@ check_msdefender/
221
246
  │ │ ├── onboarding.py # Onboarding status command
222
247
  │ │ ├── lastseen.py # Last seen command
223
248
  │ │ ├── vulnerabilities.py # Vulnerabilities command
249
+ │ │ ├── alerts.py # Alerts monitoring command
224
250
  │ │ ├── machines.py # List machines command
225
251
  │ │ └── detail.py # Machine detail command
226
252
  │ ├── decorators.py # Common CLI decorators
@@ -236,6 +262,7 @@ check_msdefender/
236
262
  │ ├── onboarding_service.py # Onboarding business logic
237
263
  │ ├── lastseen_service.py # Last seen business logic
238
264
  │ ├── vulnerabilities_service.py # Vulnerability business logic
265
+ │ ├── alerts_service.py # Alerts monitoring business logic
239
266
  │ ├── machines_service.py # Machines business logic
240
267
  │ ├── detail_service.py # Detail business logic
241
268
  │ └── models.py # Data models
@@ -317,6 +344,14 @@ DEFENDER WARNING - Last seen: 10 days ago | lastseen=10;7;30;0;
317
344
  DEFENDER CRITICAL - Vulnerability score: 150 (1 Critical, 5 High) | vulnerabilities=150;10;100;0;
318
345
  ```
319
346
 
347
+ ### Alerts Warning
348
+ ```
349
+ DEFENDER WARNING - Unresolved alerts for machine.domain.com | alerts=2;1;5;0;
350
+ Unresolved alerts for machine.domain.com
351
+ 2025-09-14T10:22:14.12Z - Suspicious activity detected (New high)
352
+ 2025-09-14T12:00:00.00Z - Malware detection (InProgress medium)
353
+ ```
354
+
320
355
  ## 🔧 Troubleshooting
321
356
 
322
357
  ### Common Issues
@@ -9,7 +9,7 @@ A comprehensive **Nagios plugin** for monitoring Microsoft Defender for Endpoint
9
9
  ## ✨ Features
10
10
 
11
11
  - 🔐 **Dual Authentication** - Support for Client Secret and Certificate-based authentication
12
- - 🎯 **Multiple Endpoints** - Monitor onboarding status, last seen, vulnerabilities, and machine details
12
+ - 🎯 **Multiple Endpoints** - Monitor onboarding status, last seen, vulnerabilities, alerts, and machine details
13
13
  - 📊 **Nagios Compatible** - Standard exit codes and performance data output
14
14
  - 🏗️ **Clean Architecture** - Modular design with testable components
15
15
  - 🔧 **Flexible Configuration** - File-based configuration with sensible defaults
@@ -41,6 +41,9 @@ check_msdefender lastseen -d machine.domain.tld -W 7 -C 30
41
41
  # Check vulnerabilities
42
42
  check_msdefender vulnerabilities -d machine.domain.tld -W 10 -C 100
43
43
 
44
+ # Check alerts
45
+ check_msdefender alerts -d machine.domain.tld -W 1 -C 5
46
+
44
47
  # List all machines
45
48
  check_msdefender machines
46
49
 
@@ -55,6 +58,7 @@ check_msdefender detail -d machine.domain.tld
55
58
  | `onboarding` | Check machine onboarding status | W:1, C:2 |
56
59
  | `lastseen` | Days since machine last seen | W:7, C:30 |
57
60
  | `vulnerabilities` | Vulnerability score calculation | W:10, C:100 |
61
+ | `alerts` | Count of unresolved alerts | W:1, C:0 |
58
62
  | `machines` | List all machines | W:10, C:25 |
59
63
  | `detail` | Get detailed machine information | - |
60
64
 
@@ -66,6 +70,14 @@ The vulnerability score is calculated as:
66
70
  - **Medium vulnerabilities** × 5
67
71
  - **Low vulnerabilities** × 1
68
72
 
73
+ ### Alert Monitoring
74
+
75
+ The alerts command monitors unresolved security alerts for a machine:
76
+ - **Counts only unresolved alerts** (status ≠ "Resolved")
77
+ - **Excludes informational alerts** when critical/warning alerts exist
78
+ - **Shows alert details** including creation time, title, and severity
79
+ - **Default thresholds**: Warning at 1 alert, Critical at 0 (meaning any alert triggers warning)
80
+
69
81
  ### Onboarding Status Values
70
82
 
71
83
  - `0` - Onboarded ✅
@@ -108,6 +120,7 @@ timeout = 5
108
120
  - `Machine.Read.All`
109
121
  - `Vulnerability.Read`
110
122
  - `Vulnerability.Read.All`
123
+ - `Alert.Read.All`
111
124
  3. **Create Authentication** (Secret or Certificate)
112
125
  4. **Note Credentials** (Client ID, Tenant ID, Secret/Certificate)
113
126
 
@@ -145,6 +158,11 @@ define command {
145
158
  command_name check_defender_vulnerabilities
146
159
  command_line $USER1$/check_msdefender/bin/check_msdefender vulnerabilities -d $HOSTALIAS$ -W 10 -C 100
147
160
  }
161
+
162
+ define command {
163
+ command_name check_defender_alerts
164
+ command_line $USER1$/check_msdefender/bin/check_msdefender alerts -d $HOSTALIAS$ -W 1 -C 5
165
+ }
148
166
  ```
149
167
 
150
168
  ### Service Definitions
@@ -171,6 +189,13 @@ define service {
171
189
  check_command check_defender_vulnerabilities
172
190
  hostgroup_name msdefender
173
191
  }
192
+
193
+ define service {
194
+ use generic-service
195
+ service_description DEFENDER_ALERTS
196
+ check_command check_defender_alerts
197
+ hostgroup_name msdefender
198
+ }
174
199
  ```
175
200
 
176
201
  ## 🏗️ Architecture
@@ -184,6 +209,7 @@ check_msdefender/
184
209
  │ │ ├── onboarding.py # Onboarding status command
185
210
  │ │ ├── lastseen.py # Last seen command
186
211
  │ │ ├── vulnerabilities.py # Vulnerabilities command
212
+ │ │ ├── alerts.py # Alerts monitoring command
187
213
  │ │ ├── machines.py # List machines command
188
214
  │ │ └── detail.py # Machine detail command
189
215
  │ ├── decorators.py # Common CLI decorators
@@ -199,6 +225,7 @@ check_msdefender/
199
225
  │ ├── onboarding_service.py # Onboarding business logic
200
226
  │ ├── lastseen_service.py # Last seen business logic
201
227
  │ ├── vulnerabilities_service.py # Vulnerability business logic
228
+ │ ├── alerts_service.py # Alerts monitoring business logic
202
229
  │ ├── machines_service.py # Machines business logic
203
230
  │ ├── detail_service.py # Detail business logic
204
231
  │ └── models.py # Data models
@@ -280,6 +307,14 @@ DEFENDER WARNING - Last seen: 10 days ago | lastseen=10;7;30;0;
280
307
  DEFENDER CRITICAL - Vulnerability score: 150 (1 Critical, 5 High) | vulnerabilities=150;10;100;0;
281
308
  ```
282
309
 
310
+ ### Alerts Warning
311
+ ```
312
+ DEFENDER WARNING - Unresolved alerts for machine.domain.com | alerts=2;1;5;0;
313
+ Unresolved alerts for machine.domain.com
314
+ 2025-09-14T10:22:14.12Z - Suspicious activity detected (New high)
315
+ 2025-09-14T12:00:00.00Z - Malware detection (InProgress medium)
316
+ ```
317
+
283
318
  ## 🔧 Troubleshooting
284
319
 
285
320
  ### Common Issues
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: check-msdefender
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: A Nagios plugin for monitoring Microsoft Defender API endpoints
5
5
  Author-email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT
@@ -46,7 +46,7 @@ A comprehensive **Nagios plugin** for monitoring Microsoft Defender for Endpoint
46
46
  ## ✨ Features
47
47
 
48
48
  - 🔐 **Dual Authentication** - Support for Client Secret and Certificate-based authentication
49
- - 🎯 **Multiple Endpoints** - Monitor onboarding status, last seen, vulnerabilities, and machine details
49
+ - 🎯 **Multiple Endpoints** - Monitor onboarding status, last seen, vulnerabilities, alerts, and machine details
50
50
  - 📊 **Nagios Compatible** - Standard exit codes and performance data output
51
51
  - 🏗️ **Clean Architecture** - Modular design with testable components
52
52
  - 🔧 **Flexible Configuration** - File-based configuration with sensible defaults
@@ -78,6 +78,9 @@ check_msdefender lastseen -d machine.domain.tld -W 7 -C 30
78
78
  # Check vulnerabilities
79
79
  check_msdefender vulnerabilities -d machine.domain.tld -W 10 -C 100
80
80
 
81
+ # Check alerts
82
+ check_msdefender alerts -d machine.domain.tld -W 1 -C 5
83
+
81
84
  # List all machines
82
85
  check_msdefender machines
83
86
 
@@ -92,6 +95,7 @@ check_msdefender detail -d machine.domain.tld
92
95
  | `onboarding` | Check machine onboarding status | W:1, C:2 |
93
96
  | `lastseen` | Days since machine last seen | W:7, C:30 |
94
97
  | `vulnerabilities` | Vulnerability score calculation | W:10, C:100 |
98
+ | `alerts` | Count of unresolved alerts | W:1, C:0 |
95
99
  | `machines` | List all machines | W:10, C:25 |
96
100
  | `detail` | Get detailed machine information | - |
97
101
 
@@ -103,6 +107,14 @@ The vulnerability score is calculated as:
103
107
  - **Medium vulnerabilities** × 5
104
108
  - **Low vulnerabilities** × 1
105
109
 
110
+ ### Alert Monitoring
111
+
112
+ The alerts command monitors unresolved security alerts for a machine:
113
+ - **Counts only unresolved alerts** (status ≠ "Resolved")
114
+ - **Excludes informational alerts** when critical/warning alerts exist
115
+ - **Shows alert details** including creation time, title, and severity
116
+ - **Default thresholds**: Warning at 1 alert, Critical at 0 (meaning any alert triggers warning)
117
+
106
118
  ### Onboarding Status Values
107
119
 
108
120
  - `0` - Onboarded ✅
@@ -145,6 +157,7 @@ timeout = 5
145
157
  - `Machine.Read.All`
146
158
  - `Vulnerability.Read`
147
159
  - `Vulnerability.Read.All`
160
+ - `Alert.Read.All`
148
161
  3. **Create Authentication** (Secret or Certificate)
149
162
  4. **Note Credentials** (Client ID, Tenant ID, Secret/Certificate)
150
163
 
@@ -182,6 +195,11 @@ define command {
182
195
  command_name check_defender_vulnerabilities
183
196
  command_line $USER1$/check_msdefender/bin/check_msdefender vulnerabilities -d $HOSTALIAS$ -W 10 -C 100
184
197
  }
198
+
199
+ define command {
200
+ command_name check_defender_alerts
201
+ command_line $USER1$/check_msdefender/bin/check_msdefender alerts -d $HOSTALIAS$ -W 1 -C 5
202
+ }
185
203
  ```
186
204
 
187
205
  ### Service Definitions
@@ -208,6 +226,13 @@ define service {
208
226
  check_command check_defender_vulnerabilities
209
227
  hostgroup_name msdefender
210
228
  }
229
+
230
+ define service {
231
+ use generic-service
232
+ service_description DEFENDER_ALERTS
233
+ check_command check_defender_alerts
234
+ hostgroup_name msdefender
235
+ }
211
236
  ```
212
237
 
213
238
  ## 🏗️ Architecture
@@ -221,6 +246,7 @@ check_msdefender/
221
246
  │ │ ├── onboarding.py # Onboarding status command
222
247
  │ │ ├── lastseen.py # Last seen command
223
248
  │ │ ├── vulnerabilities.py # Vulnerabilities command
249
+ │ │ ├── alerts.py # Alerts monitoring command
224
250
  │ │ ├── machines.py # List machines command
225
251
  │ │ └── detail.py # Machine detail command
226
252
  │ ├── decorators.py # Common CLI decorators
@@ -236,6 +262,7 @@ check_msdefender/
236
262
  │ ├── onboarding_service.py # Onboarding business logic
237
263
  │ ├── lastseen_service.py # Last seen business logic
238
264
  │ ├── vulnerabilities_service.py # Vulnerability business logic
265
+ │ ├── alerts_service.py # Alerts monitoring business logic
239
266
  │ ├── machines_service.py # Machines business logic
240
267
  │ ├── detail_service.py # Detail business logic
241
268
  │ └── models.py # Data models
@@ -317,6 +344,14 @@ DEFENDER WARNING - Last seen: 10 days ago | lastseen=10;7;30;0;
317
344
  DEFENDER CRITICAL - Vulnerability score: 150 (1 Critical, 5 High) | vulnerabilities=150;10;100;0;
318
345
  ```
319
346
 
347
+ ### Alerts Warning
348
+ ```
349
+ DEFENDER WARNING - Unresolved alerts for machine.domain.com | alerts=2;1;5;0;
350
+ Unresolved alerts for machine.domain.com
351
+ 2025-09-14T10:22:14.12Z - Suspicious activity detected (New high)
352
+ 2025-09-14T12:00:00.00Z - Malware detection (InProgress medium)
353
+ ```
354
+
320
355
  ## 🔧 Troubleshooting
321
356
 
322
357
  ### Common Issues
@@ -71,8 +71,10 @@ doc/Feat-Pypi-Package.md
71
71
  doc/Feat-Verbose.md
72
72
  tests/__init__.py
73
73
  tests/fixtures/__init__.py
74
+ tests/fixtures/alerts_data.json
74
75
  tests/fixtures/machine_data.json
75
76
  tests/fixtures/mock_defender_client.py
77
+ tests/fixtures/test_alerts_service.py
76
78
  tests/fixtures/test_detail_service.py
77
79
  tests/fixtures/test_lastseen_service.py
78
80
  tests/fixtures/test_onboarding_service.py
@@ -82,4 +84,5 @@ tests/integration/__init__.py
82
84
  tests/integration/test_cli_integration.py
83
85
  tests/integration/test_lastseen_integration.py
84
86
  tests/unit/__init__.py
87
+ tests/unit/test_alerts_service.py
85
88
  tests/unit/test_detail_service.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "check-msdefender"
7
- version = "1.1.0"
7
+ version = "1.1.1"
8
8
  authors = [
9
9
  {name = "ldvchosal", email = "ldvchosal@github.com"},
10
10
  ]
@@ -0,0 +1,72 @@
1
+ {
2
+ "alerts": {
3
+ "value": [
4
+ {
5
+ "severity": "High",
6
+ "status": "New",
7
+ "title": "Suspicious activity detected",
8
+ "alertCreationTime": "2025-09-14T10:22:14.12Z",
9
+ "firstEventTime": "2025-09-14T10:22:13.7175652Z",
10
+ "lastEventTime": "2025-09-14T10:22:13.7175652Z",
11
+ "lastUpdateTime": "2025-09-14T10:24:04.42Z",
12
+ "machineId": "test-machine-1",
13
+ "computerDnsName": "test-machine-1.domain.com"
14
+ },
15
+ {
16
+ "severity": "Informational",
17
+ "status": "New",
18
+ "title": "Automated investigation started manually",
19
+ "alertCreationTime": "2025-09-12T21:22:14.12Z",
20
+ "firstEventTime": "2025-09-12T21:22:13.7175652Z",
21
+ "lastEventTime": "2025-09-12T21:22:13.7175652Z",
22
+ "lastUpdateTime": "2025-09-13T01:24:04.42Z",
23
+ "machineId": "test-machine-1",
24
+ "computerDnsName": "test-machine-1.domain.com"
25
+ },
26
+ {
27
+ "severity": "Medium",
28
+ "status": "Resolved",
29
+ "title": "Malware detected and remediated",
30
+ "alertCreationTime": "2025-09-10T15:30:45.67Z",
31
+ "firstEventTime": "2025-09-10T15:30:44.1234567Z",
32
+ "lastEventTime": "2025-09-10T15:30:44.1234567Z",
33
+ "lastUpdateTime": "2025-09-11T09:15:22.89Z",
34
+ "machineId": "test-machine-1",
35
+ "computerDnsName": "test-machine-1.domain.com"
36
+ },
37
+ {
38
+ "severity": "Informational",
39
+ "status": "Resolved",
40
+ "title": "Automated investigation completed",
41
+ "alertCreationTime": "2025-09-11T15:25:38.54Z",
42
+ "firstEventTime": "2025-09-11T15:25:38.1183588Z",
43
+ "lastEventTime": "2025-09-11T15:25:38.1183588Z",
44
+ "lastUpdateTime": "2025-09-12T11:05:46.9966667Z",
45
+ "machineId": "test-machine-2",
46
+ "computerDnsName": "test-machine-2.domain.com"
47
+ },
48
+ {
49
+ "severity": "Low",
50
+ "status": "New",
51
+ "title": "Suspicious file execution",
52
+ "alertCreationTime": "2025-09-13T08:15:22.33Z",
53
+ "firstEventTime": "2025-09-13T08:15:21.9876543Z",
54
+ "lastEventTime": "2025-09-13T08:15:21.9876543Z",
55
+ "lastUpdateTime": "2025-09-13T08:20:11.11Z",
56
+ "machineId": "test-machine-3",
57
+ "computerDnsName": "test-machine-3.domain.com"
58
+ },
59
+ {
60
+ "severity": "Informational",
61
+ "status": "InProgress",
62
+ "title": "Manual investigation in progress",
63
+ "alertCreationTime": "2025-09-14T12:45:30.77Z",
64
+ "firstEventTime": "2025-09-14T12:45:29.1111111Z",
65
+ "lastEventTime": "2025-09-14T12:45:29.1111111Z",
66
+ "lastUpdateTime": "2025-09-14T13:00:15.55Z",
67
+ "machineId": "test-machine-2",
68
+ "computerDnsName": "test-machine-2.domain.com"
69
+ }
70
+ ]
71
+ }
72
+ }
@@ -0,0 +1,150 @@
1
+ """Fixture tests for AlertsService."""
2
+
3
+ import pytest
4
+
5
+ from check_msdefender.services.alerts_service import AlertsService
6
+ from check_msdefender.core.exceptions import ValidationError
7
+ from tests.fixtures.mock_defender_client import MockDefenderClient
8
+
9
+
10
+ class TestAlertsServiceFixtures:
11
+ """Fixture tests for AlertsService."""
12
+
13
+ def setup_method(self):
14
+ """Set up test fixtures."""
15
+ self.mock_client = MockDefenderClient()
16
+ self.service = AlertsService(self.mock_client)
17
+
18
+ def test_get_result_by_machine_id_multiple_alerts(self):
19
+ """Test getting alerts for machine with multiple alerts."""
20
+ # test-machine-1 has 2 unresolved alerts: 1 High (New) + 1 Informational (New)
21
+ result = self.service.get_result(machine_id="test-machine-1")
22
+
23
+ # Should return 2 unresolved alerts
24
+ assert result["value"] == 2
25
+ assert len(result["details"]) == 3 # Summary line + 2 alert details
26
+
27
+ # Check that details include both alerts
28
+ details_text = "\n".join(result["details"])
29
+ assert "Suspicious activity detected" in details_text
30
+ assert "Automated investigation started manually" in details_text
31
+ assert "test-machine-1.domain.com" in details_text
32
+
33
+ def test_get_result_by_machine_id_resolved_alerts_only(self):
34
+ """Test machine with only resolved alerts."""
35
+ # test-machine-2 has 1 resolved alert
36
+ result = self.service.get_result(machine_id="test-machine-2")
37
+
38
+ # Should return 1 unresolved alert (InProgress status)
39
+ assert result["value"] == 1
40
+ assert len(result["details"]) == 2 # Summary line + 1 alert detail
41
+
42
+ details_text = "\n".join(result["details"])
43
+ assert "Manual investigation in progress" in details_text
44
+ assert "test-machine-2.domain.com" in details_text
45
+
46
+ def test_get_result_by_machine_id_low_severity_alert(self):
47
+ """Test machine with low severity unresolved alert."""
48
+ # test-machine-3 has 1 Low severity alert (New status)
49
+ result = self.service.get_result(machine_id="test-machine-3")
50
+
51
+ # Should return 1 unresolved alert
52
+ assert result["value"] == 1
53
+ assert len(result["details"]) == 2 # Summary line + 1 alert detail
54
+
55
+ details_text = "\n".join(result["details"])
56
+ assert "Suspicious file execution" in details_text
57
+ assert "test-machine-3.domain.com" in details_text
58
+
59
+ def test_get_result_by_dns_name(self):
60
+ """Test getting alerts by DNS name."""
61
+ # test-machine-2.domain.com should match alerts
62
+ result = self.service.get_result(dns_name="test-machine-2.domain.com")
63
+
64
+ # Should return 1 unresolved alert (InProgress)
65
+ assert result["value"] == 1
66
+ assert len(result["details"]) == 2 # Summary line + 1 alert detail
67
+
68
+ def test_get_result_no_parameters(self):
69
+ """Test error when no parameters provided."""
70
+ with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
71
+ self.service.get_result()
72
+
73
+ def test_get_result_nonexistent_dns_name(self):
74
+ """Test error when DNS name doesn't exist."""
75
+ with pytest.raises(ValidationError, match="Machine not found with DNS name"):
76
+ self.service.get_result(dns_name="nonexistent.domain.com")
77
+
78
+ def test_get_result_nonexistent_machine_id(self):
79
+ """Test error when machine ID doesn't exist."""
80
+ with pytest.raises(ValidationError, match="Machine not found"):
81
+ self.service.get_result(machine_id="nonexistent-machine")
82
+
83
+ def test_get_result_machine_without_alerts(self):
84
+ """Test machine that exists but has no alerts."""
85
+ # Create a machine that has no matching alerts in fixture data
86
+ result = self.service.get_result(machine_id="test-machine-4")
87
+
88
+ # Should return 0 alerts since test-machine-4 has no alerts in fixture
89
+ assert result["value"] == 0
90
+ assert result["details"] == []
91
+
92
+ def test_alert_severity_categorization(self):
93
+ """Test that alerts are properly categorized by severity."""
94
+ # test-machine-1 has High + Informational alerts
95
+ result = self.service.get_result(machine_id="test-machine-1")
96
+
97
+ # Should count both as unresolved (High=1, Informational=1)
98
+ assert result["value"] == 2
99
+
100
+ # Check that summary mentions unresolved alerts (not specifically informational)
101
+ details_text = "\n".join(result["details"])
102
+ assert "Unresolved alerts for test-machine-1.domain.com" in details_text
103
+
104
+ def test_alert_status_filtering(self):
105
+ """Test that only unresolved alerts are counted."""
106
+ # Based on fixture data, test-machine-1 has:
107
+ # - 1 High/New (unresolved)
108
+ # - 1 Informational/New (unresolved)
109
+ # - 1 Medium/Resolved (resolved - should be filtered out)
110
+ result = self.service.get_result(machine_id="test-machine-1")
111
+
112
+ # Should only count the 2 unresolved alerts
113
+ assert result["value"] == 2
114
+
115
+ # Resolved alert should not appear in details
116
+ details_text = "\n".join(result["details"])
117
+ assert "Malware detected and remediated" not in details_text
118
+
119
+ def test_alert_creation_time_format(self):
120
+ """Test that alert creation times are properly formatted in output."""
121
+ result = self.service.get_result(machine_id="test-machine-1")
122
+
123
+ details_text = "\n".join(result["details"])
124
+
125
+ # Should include timestamps in ISO format
126
+ assert "2025-09-14T10:22:14.12Z" in details_text
127
+ assert "2025-09-12T21:22:14.12Z" in details_text
128
+
129
+ def test_alert_title_and_severity_in_output(self):
130
+ """Test that alert titles and severities appear correctly in output."""
131
+ result = self.service.get_result(machine_id="test-machine-1")
132
+
133
+ details_text = "\n".join(result["details"])
134
+
135
+ # Should include alert titles and severity/status
136
+ assert "suspicious activity detected (new high)" in details_text.lower()
137
+ assert (
138
+ "automated investigation started manually (new informational)" in details_text.lower()
139
+ )
140
+
141
+ def test_dns_name_matching(self):
142
+ """Test that alerts are matched by DNS name when provided."""
143
+ # Use DNS name that should match alerts
144
+ result = self.service.get_result(dns_name="test-machine-3.domain.com")
145
+
146
+ # Should find the Low severity alert for test-machine-3
147
+ assert result["value"] == 1
148
+
149
+ details_text = "\n".join(result["details"])
150
+ assert "Suspicious file execution" in details_text
@@ -0,0 +1,267 @@
1
+ """Unit tests for AlertsService."""
2
+
3
+ import pytest
4
+ from unittest.mock import Mock
5
+
6
+ from check_msdefender.services.alerts_service import AlertsService
7
+ from check_msdefender.core.exceptions import ValidationError
8
+
9
+
10
+ class TestAlertsService:
11
+ """Unit tests for AlertsService."""
12
+
13
+ def setup_method(self):
14
+ """Set up test fixtures."""
15
+ self.mock_client = Mock()
16
+ self.service = AlertsService(self.mock_client)
17
+
18
+ def test_init(self):
19
+ """Test service initialization."""
20
+ assert self.service.defender == self.mock_client
21
+ assert hasattr(self.service, "logger")
22
+
23
+ def test_get_result_by_machine_id_success(self):
24
+ """Test successful retrieval by machine ID."""
25
+ # Mock the alerts API response
26
+ mock_alerts_data = {
27
+ "value": [
28
+ {
29
+ "severity": "High",
30
+ "status": "New",
31
+ "title": "Suspicious activity detected",
32
+ "alertCreationTime": "2025-09-14T10:22:14.12Z",
33
+ "firstEventTime": "2025-09-14T10:22:13.7175652Z",
34
+ "lastEventTime": "2025-09-14T10:22:13.7175652Z",
35
+ "lastUpdateTime": "2025-09-14T10:24:04.42Z",
36
+ "machineId": "test-machine-123",
37
+ "computerDnsName": "test.domain.com",
38
+ },
39
+ {
40
+ "severity": "Informational",
41
+ "status": "Resolved",
42
+ "title": "Investigation completed",
43
+ "alertCreationTime": "2025-09-13T15:30:45.67Z",
44
+ "firstEventTime": "2025-09-13T15:30:44.1234567Z",
45
+ "lastEventTime": "2025-09-13T15:30:44.1234567Z",
46
+ "lastUpdateTime": "2025-09-13T16:15:22.89Z",
47
+ "machineId": "test-machine-123",
48
+ "computerDnsName": "test.domain.com",
49
+ },
50
+ ]
51
+ }
52
+
53
+ # Mock machine details API response
54
+ mock_machine_data = {
55
+ "id": "test-machine-123",
56
+ "computerDnsName": "test.domain.com",
57
+ }
58
+
59
+ self.mock_client.get_alerts.return_value = mock_alerts_data
60
+ self.mock_client.get_machine_by_id.return_value = mock_machine_data
61
+
62
+ result = self.service.get_result(machine_id="test-machine-123")
63
+
64
+ # Should return 1 unresolved alert (High severity, New status)
65
+ assert result["value"] == 1
66
+ assert len(result["details"]) == 2 # Summary line + 1 alert detail
67
+
68
+ # Should call the client methods
69
+ self.mock_client.get_alerts.assert_called_once()
70
+ self.mock_client.get_machine_by_id.assert_called_once_with("test-machine-123")
71
+
72
+ def test_get_result_by_dns_name_success(self):
73
+ """Test successful retrieval by DNS name."""
74
+ # Mock DNS lookup response
75
+ mock_dns_response = {"value": [{"id": "test-machine-456"}]}
76
+
77
+ # Mock alerts API response
78
+ mock_alerts_data = {
79
+ "value": [
80
+ {
81
+ "severity": "Medium",
82
+ "status": "InProgress",
83
+ "title": "Security scan in progress",
84
+ "alertCreationTime": "2025-09-14T12:00:00.00Z",
85
+ "firstEventTime": "2025-09-14T12:00:00.00Z",
86
+ "lastEventTime": "2025-09-14T12:00:00.00Z",
87
+ "lastUpdateTime": "2025-09-14T12:30:00.00Z",
88
+ "machineId": "test-machine-456",
89
+ "computerDnsName": "test.example.com",
90
+ }
91
+ ]
92
+ }
93
+
94
+ self.mock_client.get_machine_by_dns_name.return_value = mock_dns_response
95
+ self.mock_client.get_alerts.return_value = mock_alerts_data
96
+
97
+ result = self.service.get_result(dns_name="test.example.com")
98
+
99
+ # Should return 1 unresolved alert
100
+ assert result["value"] == 1
101
+ assert len(result["details"]) == 2 # Summary line + 1 alert detail
102
+
103
+ # Should call DNS lookup
104
+ self.mock_client.get_machine_by_dns_name.assert_called_once_with("test.example.com")
105
+ self.mock_client.get_alerts.assert_called_once()
106
+
107
+ def test_get_result_by_dns_name_not_found(self):
108
+ """Test error when DNS name doesn't exist."""
109
+ # Mock empty DNS response
110
+ mock_dns_response = {"value": []}
111
+ self.mock_client.get_machine_by_dns_name.return_value = mock_dns_response
112
+
113
+ with pytest.raises(ValidationError, match="Machine not found with DNS name"):
114
+ self.service.get_result(dns_name="nonexistent.domain.com")
115
+
116
+ self.mock_client.get_machine_by_dns_name.assert_called_once_with("nonexistent.domain.com")
117
+ self.mock_client.get_alerts.assert_not_called()
118
+
119
+ def test_get_result_no_parameters(self):
120
+ """Test error when no parameters provided."""
121
+ with pytest.raises(ValidationError, match="Either machine_id or dns_name must be provided"):
122
+ self.service.get_result()
123
+
124
+ self.mock_client.get_machine_by_dns_name.assert_not_called()
125
+ self.mock_client.get_machine_by_id.assert_not_called()
126
+ self.mock_client.get_alerts.assert_not_called()
127
+
128
+ def test_get_result_no_alerts_for_machine(self):
129
+ """Test when no alerts exist for the machine."""
130
+ # Mock alerts API response with no matching alerts
131
+ mock_alerts_data = {
132
+ "value": [
133
+ {
134
+ "severity": "High",
135
+ "status": "New",
136
+ "title": "Alert for different machine",
137
+ "alertCreationTime": "2025-09-14T10:22:14.12Z",
138
+ "machineId": "different-machine",
139
+ "computerDnsName": "other.domain.com",
140
+ }
141
+ ]
142
+ }
143
+
144
+ mock_machine_data = {
145
+ "id": "test-machine-123",
146
+ "computerDnsName": "test.domain.com",
147
+ }
148
+
149
+ self.mock_client.get_alerts.return_value = mock_alerts_data
150
+ self.mock_client.get_machine_by_id.return_value = mock_machine_data
151
+
152
+ result = self.service.get_result(machine_id="test-machine-123")
153
+
154
+ # Should return 0 alerts
155
+ assert result["value"] == 0
156
+ assert result["details"] == []
157
+
158
+ def test_get_result_only_resolved_alerts(self):
159
+ """Test when machine has only resolved alerts."""
160
+ mock_alerts_data = {
161
+ "value": [
162
+ {
163
+ "severity": "High",
164
+ "status": "Resolved",
165
+ "title": "Resolved security issue",
166
+ "alertCreationTime": "2025-09-14T10:22:14.12Z",
167
+ "machineId": "test-machine-123",
168
+ "computerDnsName": "test.domain.com",
169
+ }
170
+ ]
171
+ }
172
+
173
+ mock_machine_data = {
174
+ "id": "test-machine-123",
175
+ "computerDnsName": "test.domain.com",
176
+ }
177
+
178
+ self.mock_client.get_alerts.return_value = mock_alerts_data
179
+ self.mock_client.get_machine_by_id.return_value = mock_machine_data
180
+
181
+ result = self.service.get_result(machine_id="test-machine-123")
182
+
183
+ # Should return 0 unresolved alerts
184
+ assert result["value"] == 0
185
+ assert result["details"] == []
186
+
187
+ def test_get_result_multiple_severity_alerts(self):
188
+ """Test handling multiple alerts with different severities."""
189
+ mock_alerts_data = {
190
+ "value": [
191
+ {
192
+ "severity": "High",
193
+ "status": "New",
194
+ "title": "Critical security threat",
195
+ "alertCreationTime": "2025-09-14T10:22:14.12Z",
196
+ "machineId": "test-machine-123",
197
+ "computerDnsName": "test.domain.com",
198
+ },
199
+ {
200
+ "severity": "Informational",
201
+ "status": "InProgress",
202
+ "title": "System scan running",
203
+ "alertCreationTime": "2025-09-14T11:00:00.00Z",
204
+ "machineId": "test-machine-123",
205
+ "computerDnsName": "test.domain.com",
206
+ },
207
+ {
208
+ "severity": "Medium",
209
+ "status": "New",
210
+ "title": "Suspicious file detected",
211
+ "alertCreationTime": "2025-09-14T12:00:00.00Z",
212
+ "machineId": "test-machine-123",
213
+ "computerDnsName": "test.domain.com",
214
+ },
215
+ ]
216
+ }
217
+
218
+ mock_machine_data = {
219
+ "id": "test-machine-123",
220
+ "computerDnsName": "test.domain.com",
221
+ }
222
+
223
+ self.mock_client.get_alerts.return_value = mock_alerts_data
224
+ self.mock_client.get_machine_by_id.return_value = mock_machine_data
225
+
226
+ result = self.service.get_result(machine_id="test-machine-123")
227
+
228
+ # Should return 3 unresolved alerts
229
+ assert result["value"] == 3
230
+ assert len(result["details"]) == 4 # Summary line + 3 alert details
231
+
232
+ def test_get_result_api_exception_propagation(self):
233
+ """Test that API exceptions are properly propagated."""
234
+ self.mock_client.get_alerts.side_effect = Exception("API Error")
235
+
236
+ mock_machine_data = {
237
+ "id": "test-machine-123",
238
+ "computerDnsName": "test.domain.com",
239
+ }
240
+
241
+ self.mock_client.get_machine_by_id.return_value = mock_machine_data
242
+
243
+ with pytest.raises(Exception, match="API Error"):
244
+ self.service.get_result(machine_id="test-machine-123")
245
+
246
+ def test_logging_calls(self):
247
+ """Test that logging methods are called appropriately."""
248
+ # Mock logger
249
+ mock_logger = Mock()
250
+ self.service.logger = mock_logger
251
+
252
+ # Mock successful response
253
+ mock_alerts_data = {"value": []}
254
+ mock_machine_data = {
255
+ "id": "test-machine-123",
256
+ "computerDnsName": "test.domain.com",
257
+ }
258
+
259
+ self.mock_client.get_alerts.return_value = mock_alerts_data
260
+ self.mock_client.get_machine_by_id.return_value = mock_machine_data
261
+
262
+ self.service.get_result(machine_id="test-machine-123")
263
+
264
+ # Verify logging calls
265
+ mock_logger.method_entry.assert_called_once()
266
+ mock_logger.method_exit.assert_called_once()
267
+ assert mock_logger.info.call_count >= 2 # At least 2 info calls expected