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.
- check_msdefender/__init__.py +5 -0
- check_msdefender/__main__.py +6 -0
- check_msdefender/check_msdefender.py +7 -0
- check_msdefender/cli/__init__.py +15 -0
- check_msdefender/cli/__main__.py +6 -0
- check_msdefender/cli/commands/__init__.py +17 -0
- check_msdefender/cli/commands/detail.py +72 -0
- check_msdefender/cli/commands/lastseen.py +61 -0
- check_msdefender/cli/commands/machines.py +55 -0
- check_msdefender/cli/commands/onboarding.py +61 -0
- check_msdefender/cli/commands/vulnerabilities.py +61 -0
- check_msdefender/cli/decorators.py +18 -0
- check_msdefender/cli/handlers.py +46 -0
- check_msdefender/core/__init__.py +1 -0
- check_msdefender/core/auth.py +46 -0
- check_msdefender/core/config.py +40 -0
- check_msdefender/core/defender.py +176 -0
- check_msdefender/core/exceptions.py +31 -0
- check_msdefender/core/logging_config.py +116 -0
- check_msdefender/core/nagios.py +169 -0
- check_msdefender/services/__init__.py +1 -0
- check_msdefender/services/detail_service.py +77 -0
- check_msdefender/services/lastseen_service.py +70 -0
- check_msdefender/services/machines_service.py +82 -0
- check_msdefender/services/models.py +49 -0
- check_msdefender/services/onboarding_service.py +59 -0
- check_msdefender/services/vulnerabilities_service.py +163 -0
- check_msdefender-1.0.0.dist-info/METADATA +396 -0
- check_msdefender-1.0.0.dist-info/RECORD +33 -0
- check_msdefender-1.0.0.dist-info/WHEEL +5 -0
- check_msdefender-1.0.0.dist-info/entry_points.txt +2 -0
- check_msdefender-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|