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.
Files changed (120) hide show
  1. libre_devops_helpers/__init__.py +23 -0
  2. libre_devops_helpers/__main__.py +5 -0
  3. libre_devops_helpers/cli/__init__.py +5 -0
  4. libre_devops_helpers/cli/app.py +147 -0
  5. libre_devops_helpers/cli/commands/__init__.py +1 -0
  6. libre_devops_helpers/cli/commands/automation.py +321 -0
  7. libre_devops_helpers/cli/commands/az.py +93 -0
  8. libre_devops_helpers/cli/commands/azure.py +258 -0
  9. libre_devops_helpers/cli/commands/config.py +54 -0
  10. libre_devops_helpers/cli/commands/devices.py +548 -0
  11. libre_devops_helpers/cli/commands/entra.py +554 -0
  12. libre_devops_helpers/cli/commands/graph.py +358 -0
  13. libre_devops_helpers/cli/commands/incidents.py +420 -0
  14. libre_devops_helpers/cli/commands/intune.py +79 -0
  15. libre_devops_helpers/cli/commands/keyvault.py +142 -0
  16. libre_devops_helpers/cli/commands/logicapp.py +489 -0
  17. libre_devops_helpers/cli/commands/logs.py +69 -0
  18. libre_devops_helpers/cli/commands/pim.py +381 -0
  19. libre_devops_helpers/cli/commands/pretty.py +141 -0
  20. libre_devops_helpers/cli/commands/profiles.py +153 -0
  21. libre_devops_helpers/cli/commands/snow.py +268 -0
  22. libre_devops_helpers/cli/commands/token.py +222 -0
  23. libre_devops_helpers/cli/commands/welcome.py +43 -0
  24. libre_devops_helpers/cli/commands/xdr.py +353 -0
  25. libre_devops_helpers/cli/exits.py +12 -0
  26. libre_devops_helpers/cli/options.py +146 -0
  27. libre_devops_helpers/cli/render.py +360 -0
  28. libre_devops_helpers/cli/runtime.py +348 -0
  29. libre_devops_helpers/cli/servicenow_runtime.py +170 -0
  30. libre_devops_helpers/core/__init__.py +94 -0
  31. libre_devops_helpers/core/auth.py +103 -0
  32. libre_devops_helpers/core/brand.py +67 -0
  33. libre_devops_helpers/core/browser.py +21 -0
  34. libre_devops_helpers/core/config.py +159 -0
  35. libre_devops_helpers/core/dpapi.py +60 -0
  36. libre_devops_helpers/core/errors.py +90 -0
  37. libre_devops_helpers/core/http.py +412 -0
  38. libre_devops_helpers/core/inputs.py +193 -0
  39. libre_devops_helpers/core/log.py +246 -0
  40. libre_devops_helpers/core/poll.py +88 -0
  41. libre_devops_helpers/core/process.py +106 -0
  42. libre_devops_helpers/core/sheets.py +330 -0
  43. libre_devops_helpers/core/tables.py +49 -0
  44. libre_devops_helpers/core/timewindow.py +127 -0
  45. libre_devops_helpers/core/token_store.py +266 -0
  46. libre_devops_helpers/core/util.py +120 -0
  47. libre_devops_helpers/core/yaml_text.py +142 -0
  48. libre_devops_helpers/microsoft/__init__.py +94 -0
  49. libre_devops_helpers/microsoft/auth/__init__.py +43 -0
  50. libre_devops_helpers/microsoft/auth/azure_cli.py +91 -0
  51. libre_devops_helpers/microsoft/auth/delegated.py +391 -0
  52. libre_devops_helpers/microsoft/auth/entra.py +241 -0
  53. libre_devops_helpers/microsoft/auth/factory.py +114 -0
  54. libre_devops_helpers/microsoft/auth/lapse.py +42 -0
  55. libre_devops_helpers/microsoft/auth/managed_identity.py +84 -0
  56. libre_devops_helpers/microsoft/automation/__init__.py +24 -0
  57. libre_devops_helpers/microsoft/automation/client.py +241 -0
  58. libre_devops_helpers/microsoft/automation/models.py +131 -0
  59. libre_devops_helpers/microsoft/azcli/__init__.py +27 -0
  60. libre_devops_helpers/microsoft/azcli/client.py +81 -0
  61. libre_devops_helpers/microsoft/azcli/context.py +95 -0
  62. libre_devops_helpers/microsoft/azure/__init__.py +34 -0
  63. libre_devops_helpers/microsoft/azure/client.py +260 -0
  64. libre_devops_helpers/microsoft/azure/models.py +198 -0
  65. libre_devops_helpers/microsoft/clouds.py +83 -0
  66. libre_devops_helpers/microsoft/config.py +244 -0
  67. libre_devops_helpers/microsoft/devices/__init__.py +45 -0
  68. libre_devops_helpers/microsoft/devices/antivirus.py +149 -0
  69. libre_devops_helpers/microsoft/devices/check.py +286 -0
  70. libre_devops_helpers/microsoft/devices/inspect.py +146 -0
  71. libre_devops_helpers/microsoft/devices/models.py +148 -0
  72. libre_devops_helpers/microsoft/entra/__init__.py +41 -0
  73. libre_devops_helpers/microsoft/entra/client.py +422 -0
  74. libre_devops_helpers/microsoft/entra/models.py +334 -0
  75. libre_devops_helpers/microsoft/entra/permissions.py +51 -0
  76. libre_devops_helpers/microsoft/graph/__init__.py +38 -0
  77. libre_devops_helpers/microsoft/graph/client.py +292 -0
  78. libre_devops_helpers/microsoft/incidents/__init__.py +51 -0
  79. libre_devops_helpers/microsoft/incidents/client.py +217 -0
  80. libre_devops_helpers/microsoft/incidents/models.py +165 -0
  81. libre_devops_helpers/microsoft/incidents/permissions.py +15 -0
  82. libre_devops_helpers/microsoft/intune/__init__.py +18 -0
  83. libre_devops_helpers/microsoft/intune/client.py +94 -0
  84. libre_devops_helpers/microsoft/intune/models.py +63 -0
  85. libre_devops_helpers/microsoft/intune/permissions.py +16 -0
  86. libre_devops_helpers/microsoft/keyvault/__init__.py +34 -0
  87. libre_devops_helpers/microsoft/keyvault/client.py +185 -0
  88. libre_devops_helpers/microsoft/loganalytics/__init__.py +17 -0
  89. libre_devops_helpers/microsoft/loganalytics/client.py +117 -0
  90. libre_devops_helpers/microsoft/logicapps/__init__.py +79 -0
  91. libre_devops_helpers/microsoft/logicapps/checks.py +432 -0
  92. libre_devops_helpers/microsoft/logicapps/client.py +162 -0
  93. libre_devops_helpers/microsoft/logicapps/document.py +202 -0
  94. libre_devops_helpers/microsoft/pim/__init__.py +39 -0
  95. libre_devops_helpers/microsoft/pim/azure.py +238 -0
  96. libre_devops_helpers/microsoft/pim/entra.py +294 -0
  97. libre_devops_helpers/microsoft/pim/models.py +93 -0
  98. libre_devops_helpers/microsoft/pim/permissions.py +98 -0
  99. libre_devops_helpers/microsoft/pim/rules.py +70 -0
  100. libre_devops_helpers/microsoft/process.py +70 -0
  101. libre_devops_helpers/microsoft/resources.py +137 -0
  102. libre_devops_helpers/microsoft/tokens.py +269 -0
  103. libre_devops_helpers/microsoft/xdr/__init__.py +33 -0
  104. libre_devops_helpers/microsoft/xdr/client.py +246 -0
  105. libre_devops_helpers/microsoft/xdr/models.py +181 -0
  106. libre_devops_helpers/microsoft/xdr/permissions.py +24 -0
  107. libre_devops_helpers/py.typed +0 -0
  108. libre_devops_helpers/servicenow/__init__.py +50 -0
  109. libre_devops_helpers/servicenow/auth.py +409 -0
  110. libre_devops_helpers/servicenow/config.py +261 -0
  111. libre_devops_helpers/servicenow/instance/__init__.py +32 -0
  112. libre_devops_helpers/servicenow/instance/client.py +91 -0
  113. libre_devops_helpers/servicenow/instance/models.py +131 -0
  114. libre_devops_helpers/servicenow/roles.py +23 -0
  115. libre_devops_helpers/servicenow/tables.py +133 -0
  116. libre_devops_helpers-0.4.1.dist-info/METADATA +153 -0
  117. libre_devops_helpers-0.4.1.dist-info/RECORD +120 -0
  118. libre_devops_helpers-0.4.1.dist-info/WHEEL +4 -0
  119. libre_devops_helpers-0.4.1.dist-info/entry_points.txt +2 -0
  120. 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
+ ]