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