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.
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/PKG-INFO +37 -2
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/README.md +36 -1
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/PKG-INFO +37 -2
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/SOURCES.txt +3 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/pyproject.toml +1 -1
- check_msdefender-1.1.1/tests/fixtures/alerts_data.json +72 -0
- check_msdefender-1.1.1/tests/fixtures/test_alerts_service.py +150 -0
- check_msdefender-1.1.1/tests/unit/test_alerts_service.py +267 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.claude/settings.local.json +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.env.example +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.github/workflows/python-package.yml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.github/workflows/python-publish.yml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.gitignore +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/.gitignore +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/check_msdefender.iml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/dictionaries/project.xml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/encodings.xml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/misc.xml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/modules.xml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/runConfigurations/Integration_Tests.xml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/vcs.xml +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/LICENSE +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/__main__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/check_msdefender.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/__main__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/alerts.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/detail.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/lastseen.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/machines.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/onboarding.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/vulnerabilities.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/decorators.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/handlers.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/auth.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/config.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/defender.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/exceptions.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/logging_config.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/core/nagios.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/alerts_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/detail_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/lastseen_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/machines_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/models.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/onboarding_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/vulnerabilities_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/dependency_links.txt +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/entry_points.txt +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/requires.txt +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/top_level.txt +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.ini.example +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Click-Decorators-ErrorHandlers-Formatters.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Click-Groups.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Enhance-MsDefender-Vulnerabilities-Output.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Fixture-Tests.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Integration-Tests.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender-Alerts.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender-DetailMachine.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender-ListMachines.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-MsDefender.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Nagios-Detailed-Output.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Nagios-Exit-Code.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Nagios-Output.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Pypi-Package.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/doc/Feat-Verbose.md +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/pytest.ini +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/requirements-dev.txt +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/requirements.txt +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/setup.cfg +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/test_verbose.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/machine_data.json +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/mock_defender_client.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_detail_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_lastseen_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_onboarding_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_vulnerabilities_service.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/vulnerability_data.json +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/integration/__init__.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/integration/test_cli_integration.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/integration/test_lastseen_integration.py +0 -0
- {check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/unit/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/inspectionProfiles/profiles_settings.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/.idea/runConfigurations/Integration_Tests.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/onboarding.py
RENAMED
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/cli/commands/vulnerabilities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/alerts_service.py
RENAMED
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/detail_service.py
RENAMED
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/lastseen_service.py
RENAMED
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/machines_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender/services/onboarding_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/check_msdefender.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/fixtures/test_vulnerabilities_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_msdefender-1.1.0 → check_msdefender-1.1.1}/tests/integration/test_lastseen_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|