libre-devops-helpers 0.4.1__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.
- libre_devops_helpers/__init__.py +23 -0
- libre_devops_helpers/__main__.py +5 -0
- libre_devops_helpers/cli/__init__.py +5 -0
- libre_devops_helpers/cli/app.py +147 -0
- libre_devops_helpers/cli/commands/__init__.py +1 -0
- libre_devops_helpers/cli/commands/automation.py +321 -0
- libre_devops_helpers/cli/commands/az.py +93 -0
- libre_devops_helpers/cli/commands/azure.py +258 -0
- libre_devops_helpers/cli/commands/config.py +54 -0
- libre_devops_helpers/cli/commands/devices.py +548 -0
- libre_devops_helpers/cli/commands/entra.py +554 -0
- libre_devops_helpers/cli/commands/graph.py +358 -0
- libre_devops_helpers/cli/commands/incidents.py +420 -0
- libre_devops_helpers/cli/commands/intune.py +79 -0
- libre_devops_helpers/cli/commands/keyvault.py +142 -0
- libre_devops_helpers/cli/commands/logicapp.py +489 -0
- libre_devops_helpers/cli/commands/logs.py +69 -0
- libre_devops_helpers/cli/commands/pim.py +381 -0
- libre_devops_helpers/cli/commands/pretty.py +141 -0
- libre_devops_helpers/cli/commands/profiles.py +153 -0
- libre_devops_helpers/cli/commands/snow.py +268 -0
- libre_devops_helpers/cli/commands/token.py +222 -0
- libre_devops_helpers/cli/commands/welcome.py +43 -0
- libre_devops_helpers/cli/commands/xdr.py +353 -0
- libre_devops_helpers/cli/exits.py +12 -0
- libre_devops_helpers/cli/options.py +146 -0
- libre_devops_helpers/cli/render.py +360 -0
- libre_devops_helpers/cli/runtime.py +348 -0
- libre_devops_helpers/cli/servicenow_runtime.py +170 -0
- libre_devops_helpers/core/__init__.py +94 -0
- libre_devops_helpers/core/auth.py +103 -0
- libre_devops_helpers/core/brand.py +67 -0
- libre_devops_helpers/core/browser.py +21 -0
- libre_devops_helpers/core/config.py +159 -0
- libre_devops_helpers/core/dpapi.py +60 -0
- libre_devops_helpers/core/errors.py +90 -0
- libre_devops_helpers/core/http.py +412 -0
- libre_devops_helpers/core/inputs.py +193 -0
- libre_devops_helpers/core/log.py +246 -0
- libre_devops_helpers/core/poll.py +88 -0
- libre_devops_helpers/core/process.py +106 -0
- libre_devops_helpers/core/sheets.py +330 -0
- libre_devops_helpers/core/tables.py +49 -0
- libre_devops_helpers/core/timewindow.py +127 -0
- libre_devops_helpers/core/token_store.py +266 -0
- libre_devops_helpers/core/util.py +120 -0
- libre_devops_helpers/core/yaml_text.py +142 -0
- libre_devops_helpers/microsoft/__init__.py +94 -0
- libre_devops_helpers/microsoft/auth/__init__.py +43 -0
- libre_devops_helpers/microsoft/auth/azure_cli.py +91 -0
- libre_devops_helpers/microsoft/auth/delegated.py +391 -0
- libre_devops_helpers/microsoft/auth/entra.py +241 -0
- libre_devops_helpers/microsoft/auth/factory.py +114 -0
- libre_devops_helpers/microsoft/auth/lapse.py +42 -0
- libre_devops_helpers/microsoft/auth/managed_identity.py +84 -0
- libre_devops_helpers/microsoft/automation/__init__.py +24 -0
- libre_devops_helpers/microsoft/automation/client.py +241 -0
- libre_devops_helpers/microsoft/automation/models.py +131 -0
- libre_devops_helpers/microsoft/azcli/__init__.py +27 -0
- libre_devops_helpers/microsoft/azcli/client.py +81 -0
- libre_devops_helpers/microsoft/azcli/context.py +95 -0
- libre_devops_helpers/microsoft/azure/__init__.py +34 -0
- libre_devops_helpers/microsoft/azure/client.py +260 -0
- libre_devops_helpers/microsoft/azure/models.py +198 -0
- libre_devops_helpers/microsoft/clouds.py +83 -0
- libre_devops_helpers/microsoft/config.py +244 -0
- libre_devops_helpers/microsoft/devices/__init__.py +45 -0
- libre_devops_helpers/microsoft/devices/antivirus.py +149 -0
- libre_devops_helpers/microsoft/devices/check.py +286 -0
- libre_devops_helpers/microsoft/devices/inspect.py +146 -0
- libre_devops_helpers/microsoft/devices/models.py +148 -0
- libre_devops_helpers/microsoft/entra/__init__.py +41 -0
- libre_devops_helpers/microsoft/entra/client.py +422 -0
- libre_devops_helpers/microsoft/entra/models.py +334 -0
- libre_devops_helpers/microsoft/entra/permissions.py +51 -0
- libre_devops_helpers/microsoft/graph/__init__.py +38 -0
- libre_devops_helpers/microsoft/graph/client.py +292 -0
- libre_devops_helpers/microsoft/incidents/__init__.py +51 -0
- libre_devops_helpers/microsoft/incidents/client.py +217 -0
- libre_devops_helpers/microsoft/incidents/models.py +165 -0
- libre_devops_helpers/microsoft/incidents/permissions.py +15 -0
- libre_devops_helpers/microsoft/intune/__init__.py +18 -0
- libre_devops_helpers/microsoft/intune/client.py +94 -0
- libre_devops_helpers/microsoft/intune/models.py +63 -0
- libre_devops_helpers/microsoft/intune/permissions.py +16 -0
- libre_devops_helpers/microsoft/keyvault/__init__.py +34 -0
- libre_devops_helpers/microsoft/keyvault/client.py +185 -0
- libre_devops_helpers/microsoft/loganalytics/__init__.py +17 -0
- libre_devops_helpers/microsoft/loganalytics/client.py +117 -0
- libre_devops_helpers/microsoft/logicapps/__init__.py +79 -0
- libre_devops_helpers/microsoft/logicapps/checks.py +432 -0
- libre_devops_helpers/microsoft/logicapps/client.py +162 -0
- libre_devops_helpers/microsoft/logicapps/document.py +202 -0
- libre_devops_helpers/microsoft/pim/__init__.py +39 -0
- libre_devops_helpers/microsoft/pim/azure.py +238 -0
- libre_devops_helpers/microsoft/pim/entra.py +294 -0
- libre_devops_helpers/microsoft/pim/models.py +93 -0
- libre_devops_helpers/microsoft/pim/permissions.py +98 -0
- libre_devops_helpers/microsoft/pim/rules.py +70 -0
- libre_devops_helpers/microsoft/process.py +70 -0
- libre_devops_helpers/microsoft/resources.py +137 -0
- libre_devops_helpers/microsoft/tokens.py +269 -0
- libre_devops_helpers/microsoft/xdr/__init__.py +33 -0
- libre_devops_helpers/microsoft/xdr/client.py +246 -0
- libre_devops_helpers/microsoft/xdr/models.py +181 -0
- libre_devops_helpers/microsoft/xdr/permissions.py +24 -0
- libre_devops_helpers/py.typed +0 -0
- libre_devops_helpers/servicenow/__init__.py +50 -0
- libre_devops_helpers/servicenow/auth.py +409 -0
- libre_devops_helpers/servicenow/config.py +261 -0
- libre_devops_helpers/servicenow/instance/__init__.py +32 -0
- libre_devops_helpers/servicenow/instance/client.py +91 -0
- libre_devops_helpers/servicenow/instance/models.py +131 -0
- libre_devops_helpers/servicenow/roles.py +23 -0
- libre_devops_helpers/servicenow/tables.py +133 -0
- libre_devops_helpers-0.4.1.dist-info/METADATA +153 -0
- libre_devops_helpers-0.4.1.dist-info/RECORD +120 -0
- libre_devops_helpers-0.4.1.dist-info/WHEEL +4 -0
- libre_devops_helpers-0.4.1.dist-info/entry_points.txt +2 -0
- libre_devops_helpers-0.4.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Entra ID objects as returned by Microsoft Graph, trimmed to the fields used here.
|
|
2
|
+
|
|
3
|
+
Every model keeps the full Graph record in ``raw`` for JSON output.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, ClassVar, Literal
|
|
12
|
+
|
|
13
|
+
from libre_devops_helpers.core.util import parse_datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _text(data: Mapping[str, Any], key: str) -> str:
|
|
17
|
+
return str(data.get(key) or "")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _bool(value: object) -> bool | None:
|
|
21
|
+
return value if isinstance(value, bool) else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _strings(value: object) -> tuple[str, ...]:
|
|
25
|
+
return tuple(str(item) for item in value) if isinstance(value, list) else ()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class EntraDevice:
|
|
30
|
+
"""An Entra device object."""
|
|
31
|
+
|
|
32
|
+
SELECT: ClassVar[str] = (
|
|
33
|
+
"id,displayName,deviceId,operatingSystem,operatingSystemVersion,"
|
|
34
|
+
"accountEnabled,trustType,approximateLastSignInDateTime"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
display_name: str
|
|
39
|
+
device_id: str
|
|
40
|
+
operating_system: str
|
|
41
|
+
os_version: str
|
|
42
|
+
enabled: bool | None
|
|
43
|
+
trust_type: str
|
|
44
|
+
last_sign_in: datetime | None
|
|
45
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_json(cls, data: Mapping[str, Any]) -> EntraDevice:
|
|
49
|
+
return cls(
|
|
50
|
+
id=str(data.get("id", "")),
|
|
51
|
+
display_name=_text(data, "displayName"),
|
|
52
|
+
device_id=_text(data, "deviceId"),
|
|
53
|
+
operating_system=_text(data, "operatingSystem"),
|
|
54
|
+
os_version=_text(data, "operatingSystemVersion"),
|
|
55
|
+
enabled=_bool(data.get("accountEnabled")),
|
|
56
|
+
trust_type=_text(data, "trustType"),
|
|
57
|
+
last_sign_in=parse_datetime(data.get("approximateLastSignInDateTime")),
|
|
58
|
+
raw=dict(data),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class EntraGroup:
|
|
64
|
+
"""An Entra group. ``dynamic`` is true for rule-based membership."""
|
|
65
|
+
|
|
66
|
+
SELECT: ClassVar[str] = (
|
|
67
|
+
"id,displayName,description,groupTypes,securityEnabled,mailEnabled,membershipRule"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
id: str
|
|
71
|
+
display_name: str
|
|
72
|
+
description: str
|
|
73
|
+
dynamic: bool
|
|
74
|
+
security_enabled: bool | None
|
|
75
|
+
membership_rule: str
|
|
76
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_json(cls, data: Mapping[str, Any]) -> EntraGroup:
|
|
80
|
+
group_types = data.get("groupTypes")
|
|
81
|
+
return cls(
|
|
82
|
+
id=str(data.get("id", "")),
|
|
83
|
+
display_name=_text(data, "displayName"),
|
|
84
|
+
description=_text(data, "description"),
|
|
85
|
+
dynamic=isinstance(group_types, list) and "DynamicMembership" in group_types,
|
|
86
|
+
security_enabled=_bool(data.get("securityEnabled")),
|
|
87
|
+
membership_rule=_text(data, "membershipRule"),
|
|
88
|
+
raw=dict(data),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class EntraUser:
|
|
94
|
+
"""An Entra user."""
|
|
95
|
+
|
|
96
|
+
SELECT: ClassVar[str] = (
|
|
97
|
+
"id,displayName,userPrincipalName,mail,accountEnabled,userType,onPremisesSyncEnabled"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
id: str
|
|
101
|
+
display_name: str
|
|
102
|
+
user_principal_name: str
|
|
103
|
+
mail: str
|
|
104
|
+
enabled: bool | None
|
|
105
|
+
user_type: str
|
|
106
|
+
synced: bool | None
|
|
107
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_json(cls, data: Mapping[str, Any]) -> EntraUser:
|
|
111
|
+
return cls(
|
|
112
|
+
id=str(data.get("id", "")),
|
|
113
|
+
display_name=_text(data, "displayName"),
|
|
114
|
+
user_principal_name=_text(data, "userPrincipalName"),
|
|
115
|
+
mail=_text(data, "mail"),
|
|
116
|
+
enabled=_bool(data.get("accountEnabled")),
|
|
117
|
+
user_type=_text(data, "userType"),
|
|
118
|
+
synced=_bool(data.get("onPremisesSyncEnabled")),
|
|
119
|
+
raw=dict(data),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class DirectoryObject:
|
|
125
|
+
"""A group member of any type. ``kind`` is ``user``, ``device``, ``group``, ...
|
|
126
|
+
|
|
127
|
+
``detail`` is the most useful extra field for the kind: a user's UPN, a device's
|
|
128
|
+
operating system, a service principal's app id.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
id: str
|
|
132
|
+
kind: str
|
|
133
|
+
display_name: str
|
|
134
|
+
detail: str
|
|
135
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_json(cls, data: Mapping[str, Any], kind: str | None = None) -> DirectoryObject:
|
|
139
|
+
odata_type = _text(data, "@odata.type").removeprefix("#microsoft.graph.")
|
|
140
|
+
kind = kind or odata_type or "object"
|
|
141
|
+
detail = {
|
|
142
|
+
"user": _text(data, "userPrincipalName"),
|
|
143
|
+
"device": _text(data, "operatingSystem"),
|
|
144
|
+
"servicePrincipal": _text(data, "appId"),
|
|
145
|
+
}.get(kind, "")
|
|
146
|
+
return cls(
|
|
147
|
+
id=str(data.get("id", "")),
|
|
148
|
+
kind=kind,
|
|
149
|
+
display_name=_text(data, "displayName"),
|
|
150
|
+
detail=detail,
|
|
151
|
+
raw=dict(data),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class RoleAssignment:
|
|
157
|
+
"""A directory role a principal holds: ``active`` now, or ``eligible`` through PIM."""
|
|
158
|
+
|
|
159
|
+
role_name: str
|
|
160
|
+
role_template_id: str
|
|
161
|
+
state: Literal["active", "eligible"]
|
|
162
|
+
scope: str
|
|
163
|
+
ends: datetime | None
|
|
164
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_directory_role(cls, data: Mapping[str, Any]) -> RoleAssignment:
|
|
168
|
+
"""From a ``directoryRole`` the principal is a member of (tenant-wide, active)."""
|
|
169
|
+
return cls(
|
|
170
|
+
role_name=_text(data, "displayName"),
|
|
171
|
+
role_template_id=_text(data, "roleTemplateId"),
|
|
172
|
+
state="active",
|
|
173
|
+
scope="/",
|
|
174
|
+
ends=None,
|
|
175
|
+
raw=dict(data),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_eligibility(cls, data: Mapping[str, Any]) -> RoleAssignment:
|
|
180
|
+
"""From a PIM ``unifiedRoleEligibilitySchedule`` expanded with its role definition."""
|
|
181
|
+
definition = data.get("roleDefinition")
|
|
182
|
+
definition = definition if isinstance(definition, Mapping) else {}
|
|
183
|
+
schedule = data.get("scheduleInfo")
|
|
184
|
+
expiration = schedule.get("expiration") if isinstance(schedule, Mapping) else None
|
|
185
|
+
ends = expiration.get("endDateTime") if isinstance(expiration, Mapping) else None
|
|
186
|
+
return cls(
|
|
187
|
+
role_name=_text(definition, "displayName") or _text(data, "roleDefinitionId"),
|
|
188
|
+
role_template_id=_text(definition, "templateId") or _text(data, "roleDefinitionId"),
|
|
189
|
+
state="eligible",
|
|
190
|
+
scope=_text(data, "directoryScopeId") or "/",
|
|
191
|
+
ends=parse_datetime(ends),
|
|
192
|
+
raw=dict(data),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(frozen=True)
|
|
197
|
+
class RoleReport:
|
|
198
|
+
"""A principal's directory roles. ``eligible`` is None when PIM could not be read."""
|
|
199
|
+
|
|
200
|
+
active: tuple[RoleAssignment, ...]
|
|
201
|
+
eligible: tuple[RoleAssignment, ...] | None
|
|
202
|
+
eligible_error: str | None = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class SignIn:
|
|
207
|
+
"""One Entra sign-in event. ``error_code`` 0 is a success."""
|
|
208
|
+
|
|
209
|
+
id: str
|
|
210
|
+
created: datetime | None
|
|
211
|
+
user: str
|
|
212
|
+
app: str
|
|
213
|
+
ip_address: str
|
|
214
|
+
client_app: str
|
|
215
|
+
conditional_access: str
|
|
216
|
+
error_code: int
|
|
217
|
+
failure_reason: str
|
|
218
|
+
device_name: str
|
|
219
|
+
operating_system: str
|
|
220
|
+
location: str
|
|
221
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def succeeded(self) -> bool:
|
|
225
|
+
return self.error_code == 0
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def from_json(cls, data: Mapping[str, Any]) -> SignIn:
|
|
229
|
+
status = data.get("status")
|
|
230
|
+
status = status if isinstance(status, Mapping) else {}
|
|
231
|
+
device = data.get("deviceDetail")
|
|
232
|
+
device = device if isinstance(device, Mapping) else {}
|
|
233
|
+
location = data.get("location")
|
|
234
|
+
location = location if isinstance(location, Mapping) else {}
|
|
235
|
+
code = status.get("errorCode")
|
|
236
|
+
place = ", ".join(
|
|
237
|
+
part for part in (_text(location, "city"), _text(location, "countryOrRegion")) if part
|
|
238
|
+
)
|
|
239
|
+
return cls(
|
|
240
|
+
id=str(data.get("id", "")),
|
|
241
|
+
created=parse_datetime(data.get("createdDateTime")),
|
|
242
|
+
user=_text(data, "userPrincipalName"),
|
|
243
|
+
app=_text(data, "appDisplayName"),
|
|
244
|
+
ip_address=_text(data, "ipAddress"),
|
|
245
|
+
client_app=_text(data, "clientAppUsed"),
|
|
246
|
+
conditional_access=_text(data, "conditionalAccessStatus"),
|
|
247
|
+
error_code=code if isinstance(code, int) and not isinstance(code, bool) else 0,
|
|
248
|
+
failure_reason=_text(status, "failureReason"),
|
|
249
|
+
device_name=_text(device, "displayName"),
|
|
250
|
+
operating_system=_text(device, "operatingSystem"),
|
|
251
|
+
location=place,
|
|
252
|
+
raw=dict(data),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True)
|
|
257
|
+
class AppCredential:
|
|
258
|
+
"""A client secret or certificate on an app registration or a service principal."""
|
|
259
|
+
|
|
260
|
+
owner_kind: Literal["application", "service principal"]
|
|
261
|
+
owner_name: str
|
|
262
|
+
owner_id: str
|
|
263
|
+
app_id: str
|
|
264
|
+
kind: Literal["secret", "certificate"]
|
|
265
|
+
name: str
|
|
266
|
+
key_id: str
|
|
267
|
+
starts: datetime | None
|
|
268
|
+
ends: datetime | None
|
|
269
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
270
|
+
|
|
271
|
+
def days_left(self, now: datetime) -> int | None:
|
|
272
|
+
"""Whole days until expiry (negative once expired), or None with no end date."""
|
|
273
|
+
if self.ends is None:
|
|
274
|
+
return None
|
|
275
|
+
return (self.ends - now).days
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass(frozen=True)
|
|
279
|
+
class ConditionalAccessPolicy:
|
|
280
|
+
"""A Conditional Access policy, flattened to who and what it targets and what it requires."""
|
|
281
|
+
|
|
282
|
+
id: str
|
|
283
|
+
display_name: str
|
|
284
|
+
state: str
|
|
285
|
+
include_users: tuple[str, ...]
|
|
286
|
+
exclude_users: tuple[str, ...]
|
|
287
|
+
include_groups: tuple[str, ...]
|
|
288
|
+
exclude_groups: tuple[str, ...]
|
|
289
|
+
include_roles: tuple[str, ...]
|
|
290
|
+
include_applications: tuple[str, ...]
|
|
291
|
+
exclude_applications: tuple[str, ...]
|
|
292
|
+
client_app_types: tuple[str, ...]
|
|
293
|
+
grant_controls: tuple[str, ...]
|
|
294
|
+
grant_operator: str
|
|
295
|
+
session_controls: tuple[str, ...]
|
|
296
|
+
modified: datetime | None
|
|
297
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def from_json(cls, data: Mapping[str, Any]) -> ConditionalAccessPolicy:
|
|
301
|
+
conditions = data.get("conditions")
|
|
302
|
+
conditions = conditions if isinstance(conditions, Mapping) else {}
|
|
303
|
+
users = conditions.get("users")
|
|
304
|
+
users = users if isinstance(users, Mapping) else {}
|
|
305
|
+
apps = conditions.get("applications")
|
|
306
|
+
apps = apps if isinstance(apps, Mapping) else {}
|
|
307
|
+
grant = data.get("grantControls")
|
|
308
|
+
grant = grant if isinstance(grant, Mapping) else {}
|
|
309
|
+
session = data.get("sessionControls")
|
|
310
|
+
session = session if isinstance(session, Mapping) else {}
|
|
311
|
+
return cls(
|
|
312
|
+
id=str(data.get("id", "")),
|
|
313
|
+
display_name=_text(data, "displayName"),
|
|
314
|
+
state=_text(data, "state"),
|
|
315
|
+
include_users=_strings(users.get("includeUsers")),
|
|
316
|
+
exclude_users=_strings(users.get("excludeUsers")),
|
|
317
|
+
include_groups=_strings(users.get("includeGroups")),
|
|
318
|
+
exclude_groups=_strings(users.get("excludeGroups")),
|
|
319
|
+
include_roles=_strings(users.get("includeRoles")),
|
|
320
|
+
include_applications=_strings(apps.get("includeApplications")),
|
|
321
|
+
exclude_applications=_strings(apps.get("excludeApplications")),
|
|
322
|
+
client_app_types=_strings(conditions.get("clientAppTypes")),
|
|
323
|
+
grant_controls=(
|
|
324
|
+
*_strings(grant.get("builtInControls")),
|
|
325
|
+
*_strings(grant.get("customAuthenticationFactors")),
|
|
326
|
+
*(("terms of use",) if grant.get("termsOfUse") else ()),
|
|
327
|
+
),
|
|
328
|
+
grant_operator=_text(grant, "operator"),
|
|
329
|
+
session_controls=tuple(
|
|
330
|
+
name for name, value in session.items() if value and name != "@odata.type"
|
|
331
|
+
),
|
|
332
|
+
modified=parse_datetime(data.get("modifiedDateTime") or data.get("createdDateTime")),
|
|
333
|
+
raw=dict(data),
|
|
334
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""What each Entra feature needs from a Graph token.
|
|
2
|
+
|
|
3
|
+
``Directory.AccessAsUser.All`` appears in most groups because the Azure CLI's delegated
|
|
4
|
+
Graph token carries it, and it lets the call do whatever the signed-in user can.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from libre_devops_helpers.microsoft.resources import Requirement
|
|
8
|
+
|
|
9
|
+
_DIRECTORY = ("Directory.Read.All", "Directory.ReadWrite.All", "Directory.AccessAsUser.All")
|
|
10
|
+
|
|
11
|
+
REQUIREMENTS = (
|
|
12
|
+
Requirement("entra devices", "graph", (("Device.Read.All", *_DIRECTORY),)),
|
|
13
|
+
Requirement(
|
|
14
|
+
"entra groups",
|
|
15
|
+
"graph",
|
|
16
|
+
(("GroupMember.Read.All", "Group.Read.All", "Group.ReadWrite.All", *_DIRECTORY),),
|
|
17
|
+
),
|
|
18
|
+
Requirement("entra users", "graph", (("User.Read.All", "User.ReadWrite.All", *_DIRECTORY),)),
|
|
19
|
+
# Active roles are read through the user's memberships, which directory read covers.
|
|
20
|
+
Requirement(
|
|
21
|
+
"entra roles",
|
|
22
|
+
"graph",
|
|
23
|
+
(("RoleManagement.Read.Directory", "RoleManagement.Read.All", *_DIRECTORY),),
|
|
24
|
+
),
|
|
25
|
+
# PIM eligibility is not covered by directory read, not even Directory.AccessAsUser.All.
|
|
26
|
+
Requirement(
|
|
27
|
+
"entra pim eligibility",
|
|
28
|
+
"graph",
|
|
29
|
+
(
|
|
30
|
+
(
|
|
31
|
+
"RoleEligibilitySchedule.Read.Directory",
|
|
32
|
+
"RoleEligibilitySchedule.ReadWrite.Directory",
|
|
33
|
+
"RoleManagement.Read.Directory",
|
|
34
|
+
"RoleManagement.Read.All",
|
|
35
|
+
"RoleManagement.ReadWrite.Directory",
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
Requirement(
|
|
40
|
+
"entra apps",
|
|
41
|
+
"graph",
|
|
42
|
+
(("Application.Read.All", "Application.ReadWrite.All", *_DIRECTORY),),
|
|
43
|
+
),
|
|
44
|
+
# Graph asks for both: the audit log itself, and directory read to resolve names.
|
|
45
|
+
Requirement("entra sign-ins", "graph", (("AuditLog.Read.All",), _DIRECTORY)),
|
|
46
|
+
Requirement(
|
|
47
|
+
"entra conditional access",
|
|
48
|
+
"graph",
|
|
49
|
+
(("Policy.Read.All", "Policy.ReadWrite.ConditionalAccess"),),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Microsoft Graph, directly: any GET, objects by name or id, and Advanced Hunting.
|
|
2
|
+
|
|
3
|
+
Public API::
|
|
4
|
+
|
|
5
|
+
from libre_devops_helpers.microsoft.graph import GraphClient
|
|
6
|
+
|
|
7
|
+
page = GraphClient.for_profile(profile, tokens).page("users", params={"$top": "5"})
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from libre_devops_helpers.microsoft.graph.client import (
|
|
11
|
+
HUNT_HINT,
|
|
12
|
+
KINDS,
|
|
13
|
+
VERSIONS,
|
|
14
|
+
GraphClient,
|
|
15
|
+
GraphPage,
|
|
16
|
+
graph_path,
|
|
17
|
+
iso_duration,
|
|
18
|
+
not_found,
|
|
19
|
+
)
|
|
20
|
+
from libre_devops_helpers.microsoft.resources import Requirement
|
|
21
|
+
|
|
22
|
+
REQUIREMENTS = (
|
|
23
|
+
Requirement(
|
|
24
|
+
"hunting (xdr hunt, devices av-signature)", "graph", (("ThreatHunting.Read.All",),)
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"HUNT_HINT",
|
|
30
|
+
"KINDS",
|
|
31
|
+
"REQUIREMENTS",
|
|
32
|
+
"VERSIONS",
|
|
33
|
+
"GraphClient",
|
|
34
|
+
"GraphPage",
|
|
35
|
+
"graph_path",
|
|
36
|
+
"iso_duration",
|
|
37
|
+
"not_found",
|
|
38
|
+
]
|