check-msdefender 1.0.0__tar.gz → 1.1.0__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 (87) hide show
  1. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.claude/settings.local.json +2 -1
  2. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/PKG-INFO +1 -1
  3. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/commands/__init__.py +2 -0
  4. check_msdefender-1.1.0/check_msdefender/cli/commands/alerts.py +61 -0
  5. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/core/defender.py +38 -0
  6. check_msdefender-1.1.0/check_msdefender/services/alerts_service.py +98 -0
  7. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender.egg-info/PKG-INFO +1 -1
  8. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender.egg-info/SOURCES.txt +3 -0
  9. check_msdefender-1.1.0/doc/Feat-MsDefender-Alerts.md +116 -0
  10. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/pyproject.toml +1 -1
  11. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/machine_data.json +22 -0
  12. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/mock_defender_client.py +8 -0
  13. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.env.example +0 -0
  14. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.github/workflows/python-package.yml +0 -0
  15. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.github/workflows/python-publish.yml +0 -0
  16. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.gitignore +0 -0
  17. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/.gitignore +0 -0
  18. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/check_msdefender.iml +0 -0
  19. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/dictionaries/project.xml +0 -0
  20. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/encodings.xml +0 -0
  21. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  22. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/misc.xml +0 -0
  23. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/modules.xml +0 -0
  24. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/runConfigurations/Integration_Tests.xml +0 -0
  25. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/.idea/vcs.xml +0 -0
  26. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/LICENSE +0 -0
  27. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/README.md +0 -0
  28. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/__init__.py +0 -0
  29. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/__main__.py +0 -0
  30. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/check_msdefender.py +0 -0
  31. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/__init__.py +0 -0
  32. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/__main__.py +0 -0
  33. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/commands/detail.py +0 -0
  34. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/commands/lastseen.py +0 -0
  35. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/commands/machines.py +0 -0
  36. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/commands/onboarding.py +0 -0
  37. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/commands/vulnerabilities.py +0 -0
  38. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/decorators.py +0 -0
  39. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/cli/handlers.py +0 -0
  40. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/core/__init__.py +0 -0
  41. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/core/auth.py +0 -0
  42. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/core/config.py +0 -0
  43. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/core/exceptions.py +0 -0
  44. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/core/logging_config.py +0 -0
  45. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/core/nagios.py +0 -0
  46. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/services/__init__.py +0 -0
  47. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/services/detail_service.py +0 -0
  48. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/services/lastseen_service.py +0 -0
  49. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/services/machines_service.py +0 -0
  50. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/services/models.py +0 -0
  51. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/services/onboarding_service.py +0 -0
  52. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender/services/vulnerabilities_service.py +0 -0
  53. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender.egg-info/dependency_links.txt +0 -0
  54. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender.egg-info/entry_points.txt +0 -0
  55. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender.egg-info/requires.txt +0 -0
  56. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender.egg-info/top_level.txt +0 -0
  57. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/check_msdefender.ini.example +0 -0
  58. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Click-Decorators-ErrorHandlers-Formatters.md +0 -0
  59. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Click-Groups.md +0 -0
  60. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Enhance-MsDefender-Vulnerabilities-Output.md +0 -0
  61. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Fixture-Tests.md +0 -0
  62. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Integration-Tests.md +0 -0
  63. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-MsDefender-DetailMachine.md +0 -0
  64. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-MsDefender-ListMachines.md +0 -0
  65. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-MsDefender.md +0 -0
  66. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Nagios-Detailed-Output.md +0 -0
  67. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Nagios-Exit-Code.md +0 -0
  68. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Nagios-Output.md +0 -0
  69. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Pypi-Package.md +0 -0
  70. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/doc/Feat-Verbose.md +0 -0
  71. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/pytest.ini +0 -0
  72. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/requirements-dev.txt +0 -0
  73. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/requirements.txt +0 -0
  74. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/setup.cfg +0 -0
  75. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/test_verbose.py +0 -0
  76. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/__init__.py +0 -0
  77. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/__init__.py +0 -0
  78. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/test_detail_service.py +0 -0
  79. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/test_lastseen_service.py +0 -0
  80. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/test_onboarding_service.py +0 -0
  81. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/test_vulnerabilities_service.py +0 -0
  82. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/fixtures/vulnerability_data.json +0 -0
  83. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/integration/__init__.py +0 -0
  84. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/integration/test_cli_integration.py +0 -0
  85. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/integration/test_lastseen_integration.py +0 -0
  86. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/unit/__init__.py +0 -0
  87. {check_msdefender-1.0.0 → check_msdefender-1.1.0}/tests/unit/test_detail_service.py +0 -0
@@ -10,7 +10,8 @@
10
10
  "Bash(python:*)",
11
11
  "Bash(find:*)",
12
12
  "Bash(echo \"Exit code: $?\")",
13
- "Bash(mypy:*)"
13
+ "Bash(mypy:*)",
14
+ "Bash(black:*)"
14
15
  ],
15
16
  "deny": [],
16
17
  "ask": []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: check-msdefender
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: A Nagios plugin for monitoring Microsoft Defender API endpoints
5
5
  Author-email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT
@@ -6,6 +6,7 @@ from .vulnerabilities import register_vulnerability_commands
6
6
  from .onboarding import register_onboarding_commands
7
7
  from .machines import register_machines_commands
8
8
  from .detail import register_detail_commands
9
+ from .alerts import register_alerts_commands
9
10
 
10
11
 
11
12
  def register_all_commands(main_group: Any) -> None:
@@ -15,3 +16,4 @@ def register_all_commands(main_group: Any) -> None:
15
16
  register_onboarding_commands(main_group)
16
17
  register_machines_commands(main_group)
17
18
  register_detail_commands(main_group)
19
+ register_alerts_commands(main_group)
@@ -0,0 +1,61 @@
1
+ """Alerts commands for CLI."""
2
+
3
+ import sys
4
+ import click
5
+ from typing import Optional, Any
6
+
7
+ from check_msdefender.core.auth import get_authenticator
8
+ from check_msdefender.core.config import load_config
9
+ from check_msdefender.core.defender import DefenderClient
10
+ from check_msdefender.core.nagios import NagiosPlugin
11
+ from check_msdefender.services.alerts_service import AlertsService
12
+ from ..decorators import common_options
13
+
14
+
15
+ def register_alerts_commands(main_group: Any) -> None:
16
+ """Register alerts commands with the main CLI group."""
17
+
18
+ @main_group.command("alerts")
19
+ @common_options
20
+ def alerts_cmd(
21
+ config: str,
22
+ verbose: int,
23
+ machine_id: Optional[str],
24
+ dns_name: Optional[str],
25
+ warning: Optional[float],
26
+ critical: Optional[float],
27
+ ) -> None:
28
+ """Check alerts for Microsoft Defender."""
29
+ warning = warning if warning is not None else 1
30
+ critical = critical if critical is not None else 0
31
+
32
+ try:
33
+ # Load configuration
34
+ cfg = load_config(config)
35
+
36
+ # Get authenticator
37
+ authenticator = get_authenticator(cfg)
38
+
39
+ # Create Defender client
40
+ client = DefenderClient(authenticator, verbose_level=verbose)
41
+
42
+ # Create the alerts service
43
+ service = AlertsService(client, verbose_level=verbose)
44
+
45
+ # Create Nagios plugin
46
+ plugin = NagiosPlugin(service, "alerts")
47
+
48
+ # Execute check
49
+ result = plugin.check(
50
+ machine_id=machine_id,
51
+ dns_name=dns_name,
52
+ warning=warning,
53
+ critical=critical,
54
+ verbose=verbose,
55
+ )
56
+
57
+ sys.exit(result or 0)
58
+
59
+ except Exception as e:
60
+ print(f"UNKNOWN: {str(e)}")
61
+ sys.exit(3)
@@ -167,6 +167,44 @@ class DefenderClient:
167
167
  self.logger.debug(f"Response: {str(e.response.content)}")
168
168
  raise DefenderAPIError(f"Failed to query MS Defender API: {str(e)}")
169
169
 
170
+ def get_alerts(self) -> Dict[str, Any]:
171
+ """Get alerts from Microsoft Defender."""
172
+ self.logger.method_entry("get_alerts")
173
+
174
+ token = self._get_token()
175
+
176
+ url = f"{self.base_url}/api/alerts"
177
+ headers = {
178
+ "Authorization": f"Bearer {token}",
179
+ "Content-Type": DefenderClient.application_json,
180
+ }
181
+
182
+ params = {
183
+ "$top": "100",
184
+ "$expand": "evidence",
185
+ "$orderby": "alertCreationTime desc",
186
+ "$select": "status,title,machineId,computerDnsName,alertCreationTime,firstEventTime,lastEventTime,lastUpdateTime,severity",
187
+ }
188
+
189
+ try:
190
+ start_time = time.time()
191
+ self.logger.info("Querying alerts")
192
+ response = requests.get(url, headers=headers, params=params, timeout=self.timeout)
193
+ elapsed = time.time() - start_time
194
+
195
+ self.logger.api_call("GET", url, response.status_code, elapsed)
196
+ response.raise_for_status()
197
+
198
+ result = cast(Dict[str, Any], response.json())
199
+ self.logger.json_response(str(result))
200
+ self.logger.method_exit("get_alerts", result)
201
+ return result
202
+ except requests.RequestException as e:
203
+ self.logger.debug(f"API request failed: {str(e)}")
204
+ if hasattr(e, "response") and e.response is not None:
205
+ self.logger.debug(f"Response: {str(e.response.content)}")
206
+ raise DefenderAPIError(f"Failed to query MS Defender API: {str(e)}")
207
+
170
208
  def _get_token(self) -> str:
171
209
  """Get access token from authenticator."""
172
210
  self.logger.trace("Getting access token from authenticator")
@@ -0,0 +1,98 @@
1
+ """Alerts service implementation."""
2
+
3
+ from datetime import datetime
4
+ from typing import Dict, Optional, Any, List
5
+ from check_msdefender.core.exceptions import ValidationError
6
+ from check_msdefender.core.logging_config import get_verbose_logger
7
+
8
+
9
+ class AlertsService:
10
+ """Service for checking machine alerts."""
11
+
12
+ def __init__(self, defender_client: Any, verbose_level: int = 0) -> None:
13
+ """Initialize with Defender client."""
14
+ self.defender = defender_client
15
+ self.logger = get_verbose_logger(__name__, verbose_level)
16
+
17
+ def get_result(
18
+ self, machine_id: Optional[str] = None, dns_name: Optional[str] = None
19
+ ) -> Dict[str, Any]:
20
+ """Get alerts result with value and details for a machine."""
21
+ self.logger.method_entry("get_result", machine_id=machine_id, dns_name=dns_name)
22
+
23
+ if not machine_id and not dns_name:
24
+ raise ValidationError("Either machine_id or dns_name must be provided")
25
+
26
+ # Get machine information
27
+ target_dns_name = dns_name
28
+ target_machine_id = machine_id
29
+
30
+ if machine_id:
31
+ # Get DNS name from machine_id
32
+ machine_details = self.defender.get_machine_by_id(machine_id)
33
+ target_dns_name = machine_details.get("computerDnsName", "Unknown")
34
+ elif dns_name:
35
+ # Get machine_id from dns_name
36
+ dns_response = self.defender.get_machine_by_dns_name(dns_name)
37
+ machines = dns_response.get("value", [])
38
+ if not machines:
39
+ raise ValidationError(f"Machine not found with DNS name: {dns_name}")
40
+ target_machine_id = machines[0].get("id")
41
+ target_dns_name = dns_name
42
+
43
+ # Get all alerts
44
+ self.logger.info("Fetching alerts from Microsoft Defender")
45
+ alerts_data = self.defender.get_alerts()
46
+ all_alerts = alerts_data.get("value", [])
47
+
48
+ # Filter alerts for the specific machine
49
+ machine_alerts = [
50
+ alert
51
+ for alert in all_alerts
52
+ if alert.get("machineId") == target_machine_id
53
+ or alert.get("computerDnsName") == target_dns_name
54
+ ]
55
+
56
+ self.logger.info(f"Found {len(machine_alerts)} alerts for machine {target_dns_name}")
57
+
58
+ # Categorize alerts by status and severity
59
+ unresolved_alerts = [alert for alert in machine_alerts if alert.get("status") != "Resolved"]
60
+ informational_alerts = [
61
+ alert for alert in unresolved_alerts if alert.get("severity") == "Informational"
62
+ ]
63
+ critical_warning_alerts = [
64
+ alert
65
+ for alert in unresolved_alerts
66
+ if alert.get("severity") in ["High", "Medium", "Low"]
67
+ ]
68
+
69
+ # Create details for output
70
+ details = []
71
+ if unresolved_alerts:
72
+ summary_line = f"Unresolved alerts for {target_dns_name}"
73
+ if informational_alerts and not critical_warning_alerts:
74
+ summary_line = f"Unresolved informational alerts for {target_dns_name}"
75
+ elif critical_warning_alerts:
76
+ summary_line = f"Unresolved alerts for {target_dns_name}"
77
+ details.append(summary_line)
78
+
79
+ # Add individual alerts (limit to 10)
80
+ for alert in unresolved_alerts[:10]:
81
+ creation_time = alert.get("alertCreationTime", "Unknown")
82
+ title = alert.get("title", "Unknown alert")
83
+ status = alert.get("status", "Unknown")
84
+ severity = alert.get("severity", "Unknown")
85
+ details.append(f"{creation_time} - {title} ({status} {severity.lower()})")
86
+
87
+ # Return the number of unresolved alerts as the value
88
+ # This will be used by Nagios plugin for determining status based on thresholds
89
+ value = len(unresolved_alerts)
90
+
91
+ result = {
92
+ "value": value,
93
+ "details": details,
94
+ }
95
+
96
+ self.logger.info(f"Alert analysis complete: {len(unresolved_alerts)} unresolved alerts")
97
+ self.logger.method_exit("get_result", result)
98
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: check-msdefender
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: A Nagios plugin for monitoring Microsoft Defender API endpoints
5
5
  Author-email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT
@@ -34,6 +34,7 @@ check_msdefender/cli/__main__.py
34
34
  check_msdefender/cli/decorators.py
35
35
  check_msdefender/cli/handlers.py
36
36
  check_msdefender/cli/commands/__init__.py
37
+ check_msdefender/cli/commands/alerts.py
37
38
  check_msdefender/cli/commands/detail.py
38
39
  check_msdefender/cli/commands/lastseen.py
39
40
  check_msdefender/cli/commands/machines.py
@@ -47,6 +48,7 @@ check_msdefender/core/exceptions.py
47
48
  check_msdefender/core/logging_config.py
48
49
  check_msdefender/core/nagios.py
49
50
  check_msdefender/services/__init__.py
51
+ check_msdefender/services/alerts_service.py
50
52
  check_msdefender/services/detail_service.py
51
53
  check_msdefender/services/lastseen_service.py
52
54
  check_msdefender/services/machines_service.py
@@ -58,6 +60,7 @@ doc/Feat-Click-Groups.md
58
60
  doc/Feat-Enhance-MsDefender-Vulnerabilities-Output.md
59
61
  doc/Feat-Fixture-Tests.md
60
62
  doc/Feat-Integration-Tests.md
63
+ doc/Feat-MsDefender-Alerts.md
61
64
  doc/Feat-MsDefender-DetailMachine.md
62
65
  doc/Feat-MsDefender-ListMachines.md
63
66
  doc/Feat-MsDefender.md
@@ -0,0 +1,116 @@
1
+ # Alerts Machine Feature
2
+
3
+ Get detailed machine alerts from Microsoft Defender.
4
+
5
+ ## API Endpoint
6
+ - Base URL: `https://api.securitycenter.microsoft.com`
7
+ - Endpoint: `/api/alerts`
8
+ - Method: GET
9
+ - Authentication: Bearer token (Azure AD)
10
+
11
+ ## Implementation
12
+
13
+ ### Command Structure
14
+ ```bash
15
+ check_msdefender alerts -i <machine_id>
16
+ check_msdefender alerts -d <dns_name>
17
+ ```
18
+
19
+ ### Service Class: `AlertsService`
20
+ Location: `check_msdefender/services/alerts_service.py`
21
+
22
+ **Methods:**
23
+ - `get_alerts(machine_id=None, dns_name=None)` - Returns machine alerts
24
+ - Inherits from base service pattern like `LastSeenService`
25
+
26
+ ### CLI Command: `alerts`
27
+ Location: `check_msdefender/cli/commands/alerts.py`
28
+
29
+ **Pattern follows existing commands:**
30
+ - Uses `@common_options` decorator
31
+ - Creates `DefenderClient` and `AlertsService`
32
+ - Uses `NagiosPlugin` for output formatting
33
+
34
+ ### API Client Method
35
+ Location: `check_msdefender/core/defender.py`
36
+ - `get_alerts()`
37
+ - Returns full alerts details JSON
38
+
39
+ https://api.securitycenter.microsoft.com/api/alerts?$top=100&$expand=evidence&$orderby=alertCreationTime desc&$select=status,title,machineId,computerDnsName,alertCreationTime,firstEventTime,lastEventTime,lastUpdateTime
40
+
41
+ https://api.securitycenter.microsoft.com
42
+ /api/alerts
43
+ $top=100
44
+ $expand=evidence
45
+ $orderby=alertCreationTime desc
46
+ $select=status,title,machineId,computerDnsName,alertCreationTime,firstEventTime,lastEventTime,lastUpdateTime,severity
47
+
48
+ ```json
49
+ {
50
+ "@odata.context": "https://api-eu3.securitycenter.microsoft.com/api/$metadata#Alerts(status,title,machineId,computerDnsName,alertCreationTime,firstEventTime,lastEventTime,lastUpdateTime,severity)",
51
+ "value": [
52
+ {
53
+ "severity": "Informational",
54
+ "status": "New",
55
+ "title": "Automated investigation started manually",
56
+ "alertCreationTime": "2025-09-12T21:22:14.12Z",
57
+ "firstEventTime": "2025-09-12T21:22:13.7175652Z",
58
+ "lastEventTime": "2025-09-12T21:22:13.7175652Z",
59
+ "lastUpdateTime": "2025-09-13T01:24:04.42Z",
60
+ "machineId": "89xxxxxxxxxxxxxxxxxxxxxxx41f",
61
+ "computerDnsName": "machine.domain.tld"
62
+ },
63
+ {
64
+ "severity": "Informational",
65
+ "status": "Resolved",
66
+ "title": "Automated investigation started manually",
67
+ "alertCreationTime": "2025-09-11T15:25:38.54Z",
68
+ "firstEventTime": "2025-09-11T15:25:38.1183588Z",
69
+ "lastEventTime": "2025-09-11T15:25:38.1183588Z",
70
+ "lastUpdateTime": "2025-09-12T11:05:46.9966667Z",
71
+ "machineId": "89xxxxxxxxxxxxxxxxxxxxxxx41f",
72
+ "computerDnsName": "machine.domain.tld"
73
+ }
74
+ ]
75
+ }
76
+ ```
77
+
78
+ ## Output Format
79
+
80
+ ### Success (Machine Found)
81
+ ```
82
+ DEFENDER WARNING - Unresolved informational alerts for machine.domain.tld | alerts=3;0;0
83
+ 2025-09-12T21:22:14.12Z - Automated investigation started manually (New informational)
84
+ alertCreationTime - Alert title (status severity)
85
+ alertCreationTime - Alert title (status severity)
86
+ ```
87
+
88
+ ### Failure States
89
+ - **Warning**: `DEFENDER WARNING - Unresolved informational alerts | alerts=0;0;1`
90
+ - **Critical**: `DEFENDER CRITICAL - Unresolved alerts | alerts=0;1;0`
91
+ - **Unknown**: `DEFENDER UNKNOWN - API error: <message>` (API failures)
92
+
93
+ ## Nagios Integration
94
+ - **OK**: No unresolved or alerts
95
+ - **WARNING**: Unresolved informational alerts
96
+ - **CRITICAL**: Unresolved alerts
97
+ - **UNKNOWN**: API errors, authentication failures, network issues
98
+
99
+ ## File Structure
100
+ ```
101
+ check_msdefender/
102
+ ├── cli/commands/alerts.py # CLI command implementation
103
+ ├── services/alerts_service.py # Business logic service
104
+ └── core/defender.py # API client (get_machine_by_id exists)
105
+ ```
106
+
107
+ ## Tests
108
+ - Test units
109
+ - Integration tests
110
+ - Fixture tests
111
+
112
+ ## validation
113
+ - black
114
+ - flake8
115
+ - mypy
116
+ - pytest
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "check-msdefender"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  authors = [
9
9
  {name = "ldvchosal", email = "ldvchosal@github.com"},
10
10
  ]
@@ -44,6 +44,21 @@
44
44
  "rbacGroupId": 789,
45
45
  "riskScore": "High",
46
46
  "onboardingStatus": "Unknown"
47
+ },
48
+ "test-machine-4": {
49
+ "id": "test-machine-4",
50
+ "computerDnsName": "test-machine-4.domain.com",
51
+ "lastSeen": "2024-01-08T12:00:00Z",
52
+ "osPlatform": "Windows11",
53
+ "version": "22H2",
54
+ "osProcessor": "x64",
55
+ "lastIpAddress": "192.168.1.103",
56
+ "lastExternalIpAddress": "203.0.113.4",
57
+ "healthStatus": "Active",
58
+ "deviceValue": "Normal",
59
+ "rbacGroupId": 999,
60
+ "riskScore": "Low",
61
+ "onboardingStatus": "Onboarded"
47
62
  }
48
63
  },
49
64
  "machine_by_dns": {
@@ -61,6 +76,13 @@
61
76
  }
62
77
  ]
63
78
  },
79
+ "test-machine-3.domain.com": {
80
+ "value": [
81
+ {
82
+ "id": "test-machine-3"
83
+ }
84
+ ]
85
+ },
64
86
  "nonexistent.domain.com": {
65
87
  "value": []
66
88
  }
@@ -21,6 +21,10 @@ class MockDefenderClient:
21
21
  with open(fixtures_dir / "vulnerability_data.json") as f:
22
22
  self.vulnerability_data = json.load(f)
23
23
 
24
+ # Load alerts data
25
+ with open(fixtures_dir / "alerts_data.json") as f:
26
+ self.alerts_data = json.load(f)
27
+
24
28
  def get_machine_by_id(self, machine_id):
25
29
  """Get machine by ID from fixtures."""
26
30
  machine = self.machine_data["machine_by_id"].get(machine_id)
@@ -35,3 +39,7 @@ class MockDefenderClient:
35
39
  def get_machine_vulnerabilities(self, machine_id):
36
40
  """Get vulnerabilities for machine from fixtures."""
37
41
  return self.vulnerability_data["vulnerabilities_by_machine"].get(machine_id, {"value": []})
42
+
43
+ def get_alerts(self):
44
+ """Get all alerts from fixtures."""
45
+ return self.alerts_data["alerts"]