check-msdefender 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. check_msdefender/__init__.py +5 -0
  2. check_msdefender/__main__.py +6 -0
  3. check_msdefender/check_msdefender.py +7 -0
  4. check_msdefender/cli/__init__.py +15 -0
  5. check_msdefender/cli/__main__.py +6 -0
  6. check_msdefender/cli/commands/__init__.py +17 -0
  7. check_msdefender/cli/commands/detail.py +72 -0
  8. check_msdefender/cli/commands/lastseen.py +61 -0
  9. check_msdefender/cli/commands/machines.py +55 -0
  10. check_msdefender/cli/commands/onboarding.py +61 -0
  11. check_msdefender/cli/commands/vulnerabilities.py +61 -0
  12. check_msdefender/cli/decorators.py +18 -0
  13. check_msdefender/cli/handlers.py +46 -0
  14. check_msdefender/core/__init__.py +1 -0
  15. check_msdefender/core/auth.py +46 -0
  16. check_msdefender/core/config.py +40 -0
  17. check_msdefender/core/defender.py +176 -0
  18. check_msdefender/core/exceptions.py +31 -0
  19. check_msdefender/core/logging_config.py +116 -0
  20. check_msdefender/core/nagios.py +169 -0
  21. check_msdefender/services/__init__.py +1 -0
  22. check_msdefender/services/detail_service.py +77 -0
  23. check_msdefender/services/lastseen_service.py +70 -0
  24. check_msdefender/services/machines_service.py +82 -0
  25. check_msdefender/services/models.py +49 -0
  26. check_msdefender/services/onboarding_service.py +59 -0
  27. check_msdefender/services/vulnerabilities_service.py +163 -0
  28. check_msdefender-1.0.0.dist-info/METADATA +396 -0
  29. check_msdefender-1.0.0.dist-info/RECORD +33 -0
  30. check_msdefender-1.0.0.dist-info/WHEEL +5 -0
  31. check_msdefender-1.0.0.dist-info/entry_points.txt +2 -0
  32. check_msdefender-1.0.0.dist-info/licenses/LICENSE +21 -0
  33. check_msdefender-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,82 @@
1
+ """Machines service implementation."""
2
+
3
+ from typing import Dict, List, Any
4
+ from check_msdefender.core.exceptions import ValidationError
5
+ from check_msdefender.core.logging_config import get_verbose_logger
6
+
7
+
8
+ class MachinesService:
9
+ """Service for listing machines."""
10
+
11
+ def __init__(self, defender_client: Any, verbose_level: int = 0) -> None:
12
+ """Initialize with Defender client."""
13
+ self.defender = defender_client
14
+ self.logger = get_verbose_logger(__name__, verbose_level)
15
+
16
+ def get_result(self, **kwargs: Any) -> Dict[str, Any]:
17
+ """Get machine count result with value and details."""
18
+ self.logger.method_entry("get_result")
19
+
20
+ # Get all machines
21
+ self.logger.info("Fetching all machines from Defender API")
22
+ machines_data = self.defender.list_machines()
23
+
24
+ if not machines_data.get("value"):
25
+ self.logger.info("No machines found")
26
+ result = {"value": 0, "details": ["No machines found in Microsoft Defender"]}
27
+ self.logger.method_exit("get_result", result)
28
+ return result
29
+
30
+ machines = machines_data["value"]
31
+ machine_count = len(machines)
32
+
33
+ # Create detailed output
34
+ details = [f"Total machines: {machine_count}"]
35
+
36
+ # Liat machines
37
+ # Define the sort order
38
+ status_priority = {"Onboarded": 1, "InsufficientInfo": 2, "Unsupported": 3}
39
+
40
+ # Sort by priority
41
+ sorted_machines = sorted(
42
+ machines, key=lambda x: (status_priority[x["onboardingStatus"]], x["computerDnsName"])
43
+ )
44
+ for machine in sorted_machines:
45
+ onboarded = "✓" if machine["onboardingStatus"] == "Onboarded" else "✗"
46
+ details.append(
47
+ f"{machine['id']}: {machine['computerDnsName']} ({machine['osPlatform']}) {onboarded}"
48
+ )
49
+
50
+ result = {"value": machine_count, "details": details}
51
+
52
+ self.logger.info(f"Found {machine_count} machines")
53
+ self.logger.method_exit("get_result", result)
54
+ return result
55
+
56
+ def get_details(self, **kwargs: Any) -> List[str]:
57
+ """Get detailed machine information."""
58
+ self.logger.method_entry("get_details")
59
+
60
+ # Get all machines
61
+ self.logger.info("Fetching all machines from Defender API")
62
+ machines_data = self.defender.list_machines()
63
+
64
+ if not machines_data.get("value"):
65
+ self.logger.info("No machines found")
66
+ self.logger.method_exit("get_details", [])
67
+ return []
68
+
69
+ machines = machines_data["value"]
70
+ details = []
71
+
72
+ for machine in machines:
73
+ machine_id = machine.get("id", "unknown")[:10] # Truncate ID for display
74
+ dns_name = machine.get("computerDnsName", "unknown")
75
+ status = machine.get("onboardingStatus", "unknown")
76
+ platform = machine.get("osPlatform", "unknown")
77
+
78
+ details.append(f"{machine_id} {dns_name} {status} {platform}")
79
+
80
+ self.logger.info(f"Prepared details for {len(details)} machines")
81
+ self.logger.method_exit("get_details", details)
82
+ return details
@@ -0,0 +1,49 @@
1
+ """Data models for check_msdefender."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Optional, List
6
+ from enum import Enum
7
+
8
+
9
+ class OnboardingStatus(Enum):
10
+ """Onboarding status enumeration."""
11
+
12
+ ONBOARDED = 0
13
+ INSUFFICIENT_INFO = 1
14
+ UNKNOWN = 2
15
+
16
+
17
+ @dataclass
18
+ class Machine:
19
+ """Machine data model."""
20
+
21
+ id: str
22
+ computer_dns_name: str
23
+ last_seen: Optional[datetime] = None
24
+ onboarding_status: Optional[OnboardingStatus] = None
25
+
26
+
27
+ @dataclass
28
+ class Vulnerability:
29
+ """Vulnerability data model."""
30
+
31
+ id: str
32
+ severity: str
33
+ title: str
34
+ description: Optional[str] = None
35
+
36
+
37
+ @dataclass
38
+ class VulnerabilityScore:
39
+ """Vulnerability score calculation."""
40
+
41
+ critical: int = 0
42
+ high: int = 0
43
+ medium: int = 0
44
+ low: int = 0
45
+
46
+ @property
47
+ def total_score(self) -> int:
48
+ """Calculate total weighted score."""
49
+ return self.critical * 100 + self.high * 10 + self.medium * 5 + self.low * 1
@@ -0,0 +1,59 @@
1
+ """Onboarding status service implementation."""
2
+
3
+ from typing import Dict, Optional, Any
4
+ from check_msdefender.services.models import OnboardingStatus
5
+ from check_msdefender.core.exceptions import ValidationError
6
+ from check_msdefender.core.logging_config import get_verbose_logger
7
+
8
+
9
+ class OnboardingService:
10
+ """Service for checking onboarding status."""
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 onboarding status 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
+ if dns_name:
28
+ self.logger.info(f"Fetching machine data by DNS name: {dns_name}")
29
+ machines_data = self.defender.get_machine_by_dns_name(dns_name)
30
+ if not machines_data.get("value"):
31
+ raise ValidationError(f"Machine not found with DNS name: {dns_name}")
32
+ machine_data = machines_data["value"][0]
33
+ self.logger.debug(f"Found machine: {machine_data.get('id', 'unknown')}")
34
+ machine_id = machine_data.get("id")
35
+
36
+ # Extract onboarding status
37
+ machine_details = self.defender.get_machine_by_id(machine_id)
38
+ onboarding_state = machine_details.get("onboardingStatus")
39
+ self.logger.debug(f"Raw onboarding status from API: {onboarding_state}")
40
+
41
+ if onboarding_state == "Onboarded":
42
+ result_value = OnboardingStatus.ONBOARDED.value
43
+ status_text = "Fully onboarded"
44
+ elif onboarding_state == "InsufficientInfo":
45
+ result_value = OnboardingStatus.INSUFFICIENT_INFO.value
46
+ status_text = "Insufficient information for onboarding"
47
+ else:
48
+ result_value = OnboardingStatus.UNKNOWN.value
49
+ status_text = f"Unknown onboarding status: {onboarding_state}"
50
+
51
+ # Create detailed output
52
+ computer_name = machine_details.get("computerDnsName", "Unknown")
53
+ details = [f"Machine: {computer_name} - {status_text}"]
54
+
55
+ result = {"value": result_value, "details": details}
56
+
57
+ self.logger.info(f"Machine onboarding status: {onboarding_state} -> {result_value}")
58
+ self.logger.method_exit("get_result", result)
59
+ return result
@@ -0,0 +1,163 @@
1
+ """Vulnerabilities service implementation."""
2
+
3
+ from typing import Dict, List, Optional, Any
4
+ from check_msdefender.services.models import VulnerabilityScore, Vulnerability
5
+ from check_msdefender.core.exceptions import ValidationError
6
+ from check_msdefender.core.logging_config import get_verbose_logger
7
+
8
+
9
+ class VulnerabilitiesService:
10
+ """Service for checking vulnerabilities."""
11
+
12
+ def __init__(self, defender_client: Any, verbose_level: int = 0) -> None:
13
+ """Initialize with Defender client."""
14
+ self.client = defender_client
15
+ self.logger = get_verbose_logger(__name__, verbose_level)
16
+ self._severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
17
+
18
+ def get_result(
19
+ self, machine_id: Optional[str] = None, dns_name: Optional[str] = None
20
+ ) -> Dict[str, Any]:
21
+ """Get vulnerability result with value and details for a machine."""
22
+ self.logger.method_entry("get_result", machine_id=machine_id, dns_name=dns_name)
23
+
24
+ if not machine_id and not dns_name:
25
+ raise ValidationError("Either machine_id or dns_name must be provided")
26
+
27
+ # Get machine ID if DNS name provided
28
+ if dns_name:
29
+ self.logger.info(f"Resolving machine ID for DNS name: {dns_name}")
30
+ machines_data = self.client.get_machine_by_dns_name(dns_name)
31
+ if not machines_data.get("value"):
32
+ raise ValidationError(f"Machine not found with DNS name: {dns_name}")
33
+ machine_id = machines_data["value"][0]["id"]
34
+ self.logger.debug(f"Resolved machine ID: {machine_id}")
35
+
36
+ # Get vulnerabilities for the machine
37
+ self.logger.info(f"Fetching vulnerabilities for machine: {machine_id}")
38
+ vulnerabilities_data = self.client.get_machine_vulnerabilities(machine_id)
39
+ raw_vulnerabilities = vulnerabilities_data.get("value", [])
40
+ self.logger.info(f"Found {len(raw_vulnerabilities)} raw vulnerabilities")
41
+
42
+ # Process and deduplicate vulnerabilities
43
+ vulnerabilities = self._process_vulnerabilities(raw_vulnerabilities)
44
+ self.logger.info(f"Found {len(vulnerabilities)} unique vulnerabilities after deduplication")
45
+
46
+ # Calculate vulnerability score
47
+ score = VulnerabilityScore()
48
+
49
+ # Create detailed output
50
+ details = []
51
+
52
+ # Sort vulnerabilities by severity for consistent output
53
+ sorted_vulnerabilities = self._sort_by_severity(vulnerabilities)
54
+
55
+ for vuln in sorted_vulnerabilities:
56
+ severity = vuln.severity.lower()
57
+ self.logger.debug(f"Processing vulnerability {vuln.id} with severity: {severity}")
58
+
59
+ if severity == "critical":
60
+ score.critical += 1
61
+ elif severity == "high":
62
+ score.high += 1
63
+ elif severity == "medium":
64
+ score.medium += 1
65
+ elif severity == "low":
66
+ score.low += 1
67
+
68
+ description = self.clean_and_truncate(vuln.description)
69
+ # Add to details list
70
+ details.append(f"{vuln.id}: {description} {vuln.severity.upper()}")
71
+
72
+ self.logger.info(
73
+ f"Vulnerability score breakdown - Critical: {score.critical}, High: {score.high}, Medium: {score.medium}, Low: {score.low}"
74
+ )
75
+ self.logger.info(f"Total vulnerability score: {score.total_score}")
76
+
77
+ details.insert(
78
+ 0, f"Vulnerabilities: {len(raw_vulnerabilities)}, score: {score.total_score}"
79
+ )
80
+
81
+ result = {"value": score.total_score, "details": details}
82
+
83
+ self.logger.method_exit("get_result", result)
84
+ return result
85
+
86
+ def clean_and_truncate(
87
+ self, text: Optional[str], prefix: str = "Summary: ", word_count: int = 10
88
+ ) -> str:
89
+ # Handle None text
90
+ if text is None:
91
+ return ""
92
+ # Remove prefix and get first N words
93
+ cleaned = text.replace(prefix, "", 1) # Remove only first occurrence
94
+ words = cleaned.split()[:word_count]
95
+ return " ".join(words)
96
+
97
+ def get_detailed_vulnerabilities(
98
+ self, machine_id: Optional[str] = None, dns_name: Optional[str] = None
99
+ ) -> List[Vulnerability]:
100
+ """Get detailed vulnerability information for a machine."""
101
+ self.logger.method_entry(
102
+ "get_detailed_vulnerabilities", machine_id=machine_id, dns_name=dns_name
103
+ )
104
+
105
+ if not machine_id and not dns_name:
106
+ raise ValidationError("Either machine_id or dns_name must be provided")
107
+
108
+ # Get machine ID if DNS name provided
109
+ if dns_name:
110
+ self.logger.info(f"Resolving machine ID for DNS name: {dns_name}")
111
+ machines_data = self.client.get_machine_by_dns_name(dns_name)
112
+ if not machines_data.get("value"):
113
+ raise ValidationError(f"Machine not found with DNS name: {dns_name}")
114
+ machine_id = machines_data["value"][0]["id"]
115
+ self.logger.debug(f"Resolved machine ID: {machine_id}")
116
+
117
+ # Get vulnerabilities for the machine
118
+ self.logger.info(f"Fetching vulnerabilities for machine: {machine_id}")
119
+ vulnerabilities_data = self.client.get_machine_vulnerabilities(machine_id)
120
+ raw_vulnerabilities = vulnerabilities_data.get("value", [])
121
+
122
+ # Process and deduplicate vulnerabilities
123
+ vulnerabilities = self._process_vulnerabilities(raw_vulnerabilities)
124
+
125
+ # Sort by severity
126
+ sorted_vulnerabilities = self._sort_by_severity(vulnerabilities)
127
+
128
+ self.logger.method_exit("get_detailed_vulnerabilities", len(sorted_vulnerabilities))
129
+ return sorted_vulnerabilities
130
+
131
+ def _process_vulnerabilities(
132
+ self, raw_vulnerabilities: List[Dict[str, Any]]
133
+ ) -> List[Vulnerability]:
134
+ """Process and deduplicate vulnerabilities."""
135
+ seen_cves = set()
136
+ unique_vulnerabilities = []
137
+
138
+ for vuln_data in raw_vulnerabilities:
139
+ vuln_id = vuln_data.get("id", "")
140
+
141
+ # Skip if we've already seen this CVE
142
+ if vuln_id in seen_cves:
143
+ continue
144
+
145
+ seen_cves.add(vuln_id)
146
+
147
+ # Create Vulnerability object
148
+ vulnerability = Vulnerability(
149
+ id=vuln_id,
150
+ severity=vuln_data.get("severity", "unknown"),
151
+ title=vuln_data.get("name", "Unknown vulnerability"),
152
+ description=vuln_data.get("description"),
153
+ )
154
+
155
+ unique_vulnerabilities.append(vulnerability)
156
+
157
+ return unique_vulnerabilities
158
+
159
+ def _sort_by_severity(self, vulnerabilities: List[Vulnerability]) -> List[Vulnerability]:
160
+ """Sort vulnerabilities by severity (Critical > High > Medium > Low)."""
161
+ return sorted(
162
+ vulnerabilities, key=lambda v: self._severity_order.get(v.severity.lower(), 999)
163
+ )