Skip to content

Commit ea2c97f

Browse files
authored
[azure][feat] Add policies (#2141)
1 parent c710cf6 commit ea2c97f

15 files changed

+453
-95
lines changed

fixlib/fixlib/json_bender.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -368,20 +368,21 @@ def execute(self, source: Any) -> Any:
368368
return bool(source)
369369

370370

371+
def reformat_keys_to_snake(js: JsonElement) -> JsonElement:
372+
if isinstance(js, dict):
373+
return {snakecase(k): reformat_keys_to_snake(v) for k, v in js.items()}
374+
elif isinstance(js, list):
375+
return [reformat_keys_to_snake(v) for v in js]
376+
else:
377+
return js
378+
379+
371380
class ParseJson(Bender):
372381
def __init__(self, keys_to_snake: bool = False, **kwargs: Any):
373382
super().__init__(**kwargs)
374383
self._keys_to_snake = keys_to_snake
375384

376385
def execute(self, source: Any) -> Any:
377-
def reformat_keys_to_snake(js: JsonElement) -> JsonElement:
378-
if isinstance(js, dict):
379-
return {snakecase(k): reformat_keys_to_snake(v) for k, v in js.items()}
380-
elif isinstance(js, list):
381-
return [reformat_keys_to_snake(v) for v in js]
382-
else:
383-
return js
384-
385386
if isinstance(source, str):
386387
try:
387388
result = json.loads(source)

plugins/azure/fix_plugin_azure/collector.py

Lines changed: 47 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ def __init__(
7373
account: BaseAccount,
7474
credentials: AzureCredentials,
7575
core_feedback: CoreFeedback,
76-
global_resources: List[Type[MicrosoftResource]],
77-
regional_resources: List[Type[MicrosoftResource]],
7876
task_data: Optional[Json] = None,
7977
max_resources_per_account: Optional[int] = None,
8078
):
@@ -83,15 +81,9 @@ def __init__(
8381
self.account = account
8482
self.credentials = credentials
8583
self.core_feedback = core_feedback
86-
self.global_resources = global_resources
87-
self.regional_resources = regional_resources
8884
self.graph = Graph(root=account, max_nodes=max_resources_per_account)
8985
self.task_data = task_data
9086

91-
@abstractmethod
92-
def locations(self, builder: GraphBuilder) -> Dict[str, BaseRegion]:
93-
pass
94-
9587
def collect(self) -> None:
9688
with ThreadPoolExecutor(
9789
thread_name_prefix=f"azure_{self.account.id}",
@@ -127,16 +119,15 @@ def get_last_run() -> Optional[datetime]:
127119
config=self.config,
128120
last_run_started_at=last_run,
129121
)
122+
130123
# collect all locations
131124
locations = self.locations(builder)
132125
builder.location_lookup = locations
133-
# collect all global resources
134-
self.collect_resource_list("subscription", builder, self.global_resources)
135-
# collect all regional resources
136-
for location in locations.values():
137-
self.collect_resource_list(location.safe_name, builder.with_location(location), self.regional_resources)
138-
# wait for all work to finish
126+
127+
# collect all resources
128+
self.collect_with(builder, locations)
139129
queue.wait_for_submitted_work()
130+
140131
# connect nodes
141132
log.info(f"[Azure:{self.account.safe_name}] Connect resources and create edges.")
142133
for node, data in list(self.graph.nodes(data=True)):
@@ -146,18 +137,16 @@ def get_last_run() -> Optional[datetime]:
146137
pass
147138
else:
148139
raise Exception(f"Only Azure resources expected, but got {node}")
149-
# wait for all work to finish
150140
queue.wait_for_submitted_work()
151-
# filter nodes
152-
self.filter_nodes()
153141

154-
# post process nodes
142+
# post-process nodes
143+
self.remove_unused()
155144
for node, data in list(self.graph.nodes(data=True)):
156145
if isinstance(node, MicrosoftResource):
157146
node.after_collect(builder, data.get("source", {}))
158147

159148
# delete unnecessary nodes after all work is completed
160-
self.after_collect_filter()
149+
self.after_collect()
161150
# report all accumulated errors
162151
error_accumulator.report_all(self.core_feedback)
163152
self.core_feedback.progress_done(self.account.id, 1, 1, context=[self.cloud.id])
@@ -183,7 +172,37 @@ def work_done(_: Future[None]) -> None:
183172
all_done.add_done_callback(work_done)
184173
return all_done
185174

186-
def filter_nodes(self) -> None:
175+
@abstractmethod
176+
def collect_with(self, builder: GraphBuilder, locations: Dict[str, BaseRegion]) -> None:
177+
pass
178+
179+
@abstractmethod
180+
def locations(self, builder: GraphBuilder) -> Dict[str, BaseRegion]:
181+
pass
182+
183+
def remove_unused(self) -> None:
184+
pass
185+
186+
def after_collect(self) -> None:
187+
pass
188+
189+
190+
class AzureSubscriptionCollector(MicrosoftBaseCollector):
191+
def locations(self, builder: GraphBuilder) -> Dict[str, BaseRegion]:
192+
locations = AzureLocation.collect_resources(builder)
193+
return CaseInsensitiveDict({loc.safe_name: loc for loc in locations}) # type: ignore
194+
195+
def collect_with(self, builder: GraphBuilder, locations: Dict[str, BaseRegion]) -> None:
196+
# add deferred edge to organization
197+
builder.submit_work("azure_all", MicrosoftGraphOrganization.deferred_edge_to_subscription, builder)
198+
# collect all global and regional resources
199+
regional_resources = [r for r in subscription_resources if resource_with_params(r, "location")]
200+
global_resources = list(set(subscription_resources) - set(regional_resources))
201+
self.collect_resource_list("subscription", builder, global_resources)
202+
for location in locations.values():
203+
self.collect_resource_list(location.safe_name, builder.with_location(location), regional_resources)
204+
205+
def remove_unused(self) -> None:
187206
remove_nodes = []
188207

189208
def rm_nodes(cls, ignore_kinds: Optional[Type[Any]] = None) -> None: # type: ignore
@@ -216,7 +235,7 @@ def remove_usage_zero_value() -> None:
216235
rm_nodes(AzureStorageSku, AzureLocation)
217236
remove_usage_zero_value()
218237

219-
def after_collect_filter(self) -> None:
238+
def after_collect(self) -> None:
220239
# Filter unnecessary nodes such as AzureDiskTypePricing
221240
nodes_to_remove = []
222241
node_types = (AzureDiskTypePricing,)
@@ -227,70 +246,23 @@ def after_collect_filter(self) -> None:
227246
nodes_to_remove.append(node)
228247
self._delete_nodes(nodes_to_remove)
229248

230-
def _delete_nodes(self, nodes_to_delte: Any) -> None:
249+
def _delete_nodes(self, nodes_to_delete: Any) -> None:
231250
removed = set()
232-
for node in nodes_to_delte:
251+
for node in nodes_to_delete:
233252
if node in removed:
234253
continue
235254
removed.add(node)
236255
self.graph.remove_node(node)
237-
nodes_to_delte.clear()
238-
239-
240-
class AzureSubscriptionCollector(MicrosoftBaseCollector):
241-
def __init__(
242-
self,
243-
config: AzureConfig,
244-
cloud: Cloud,
245-
subscription: AzureSubscription,
246-
credentials: AzureCredentials,
247-
core_feedback: CoreFeedback,
248-
task_data: Optional[Json] = None,
249-
max_resources_per_account: Optional[int] = None,
250-
):
251-
regional_resources = [r for r in subscription_resources if resource_with_params(r, "location")]
252-
global_resources = list(set(subscription_resources) - set(regional_resources))
253-
super().__init__(
254-
config,
255-
cloud,
256-
subscription,
257-
credentials,
258-
core_feedback,
259-
global_resources,
260-
regional_resources,
261-
task_data=task_data,
262-
max_resources_per_account=max_resources_per_account,
263-
)
264-
265-
def locations(self, builder: GraphBuilder) -> Dict[str, BaseRegion]:
266-
locations = AzureLocation.collect_resources(builder)
267-
return CaseInsensitiveDict({loc.safe_name: loc for loc in locations}) # type: ignore
256+
nodes_to_delete.clear()
268257

269258

270259
class MicrosoftGraphOrganizationCollector(MicrosoftBaseCollector):
271-
def __init__(
272-
self,
273-
config: AzureConfig,
274-
cloud: Cloud,
275-
organization: MicrosoftGraphOrganization,
276-
credentials: AzureCredentials,
277-
core_feedback: CoreFeedback,
278-
task_data: Optional[Json] = None,
279-
max_resources_per_account: Optional[int] = None,
280-
):
281-
super().__init__(
282-
config,
283-
cloud,
284-
organization,
285-
credentials,
286-
core_feedback,
287-
[],
288-
graph_resources, # treat all resources as regional resources, attached to the organization root
289-
task_data=task_data,
290-
max_resources_per_account=max_resources_per_account,
291-
)
292260

293261
def locations(self, builder: GraphBuilder) -> Dict[str, BaseRegion]:
294262
root = MicrosoftGraphOrganizationRoot(id="organization_root")
295263
builder.add_node(root)
296264
return {"organization_root": root}
265+
266+
def collect_with(self, builder: GraphBuilder, locations: Dict[str, BaseRegion]) -> None:
267+
for location in locations.values(): # all resources underneath the organization root
268+
self.collect_resource_list(location.safe_name, builder.with_location(location), graph_resources)

plugins/azure/fix_plugin_azure/resource/microsoft_graph.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
from __future__ import annotations
22

3+
import logging
34
from datetime import datetime
4-
from typing import ClassVar, Dict, Optional, List, Type
5+
from typing import ClassVar, Dict, Optional, List, Type, Any
56

67
from attr import define, field
8+
from isodate import parse_datetime
79

810
from fix_plugin_azure.azure_client import RestApiSpec, MicrosoftRestSpec
911
from fix_plugin_azure.resource.base import GraphBuilder, MicrosoftResource
1012
from fixlib.baseresources import BaseGroup, BaseRole, BaseAccount, BaseRegion, ModelReference, BaseUser
11-
from fixlib.json_bender import Bender, S, ForallBend, Bend, F, MapDict
13+
from fixlib.graph import BySearchCriteria, ByNodeId
14+
from fixlib.json_bender import Bender, S, ForallBend, Bend, F, MapDict, reformat_keys_to_snake
1215
from fixlib.types import Json
1316

17+
log = logging.getLogger("fix.plugins.azure")
18+
1419

1520
@define(eq=False, slots=False)
1621
class MicrosoftGraphEntity(MicrosoftResource):
@@ -1097,12 +1102,97 @@ class MicrosoftGraphOrganization(MicrosoftGraphEntity, BaseAccount):
10971102
tenant_type: Optional[str] = field(default=None, metadata={'description': 'Not nullable. Can be one of the following types: AAD - An enterprise identity access management (IAM) service that serves business-to-employee and business-to-business (B2B) scenarios. AAD B2C An identity access management (IAM) service that serves business-to-consumer (B2C) scenarios. CIAM - A customer identity & access management (CIAM) solution that provides an integrated platform to serve consumers, partners, and citizen scenarios.'}) # fmt: skip
10981103
verified_domains: Optional[List[MicrosoftGraphVerifiedDomain]] = field(default=None, metadata={'description': 'The collection of domains associated with this tenant. Not nullable.'}) # fmt: skip
10991104

1105+
@classmethod
1106+
def deferred_edge_to_subscription(cls, builder: GraphBuilder) -> None:
1107+
for js in builder.client.list(cls.api_spec):
1108+
org = cls.from_api(js)
1109+
builder.add_deferred_edge(
1110+
BySearchCriteria(f'is({cls.kind}) and reported.id=="{org.id}"'), ByNodeId(builder.account.chksum)
1111+
)
1112+
11001113

11011114
@define(eq=False, slots=False)
11021115
class MicrosoftGraphOrganizationRoot(MicrosoftGraphEntity, BaseRegion):
11031116
kind: ClassVar[str] = "microsoft_graph_organization_root"
11041117

11051118

1119+
@define(eq=False, slots=False)
1120+
class MicrosoftGraphPolicy(MicrosoftGraphEntity):
1121+
kind: ClassVar[str] = "microsoft_graph_policy"
1122+
1123+
policy_kind: Optional[str] = field(default=None, metadata={"description": "The kind of policy."})
1124+
enabled: Optional[bool] = field(default=None, metadata={"description": "Indicates whether the policy is enabled."})
1125+
description: Optional[str] = field(default=None, metadata={"description": "Description of the policy."})
1126+
policy: Optional[Json] = field(default=None, metadata={"description": "The policy."})
1127+
1128+
@classmethod
1129+
def collect_resources(cls, builder: GraphBuilder, **kwargs: Any) -> List[MicrosoftGraphPolicy]:
1130+
base = "https://graph.microsoft.com/v1.0/policies"
1131+
policies = {
1132+
"admin_consent request": RestApiSpec("graph", f"{base}/adminConsentRequestPolicy"),
1133+
"authorization": RestApiSpec("graph", f"{base}/authorizationPolicy"),
1134+
"authentication_flow": RestApiSpec("graph", f"{base}/authenticationFlowsPolicy"),
1135+
"authentication_method": RestApiSpec("graph", f"{base}/authenticationMethodsPolicy"),
1136+
"cross_tenant_access": RestApiSpec("graph", f"{base}/crossTenantAccessPolicy"),
1137+
"default_app_management": RestApiSpec("graph", f"{base}/defaultAppManagementPolicy"),
1138+
"device_registration": RestApiSpec("graph", f"{base}/deviceRegistrationPolicy"),
1139+
"identity_security_defaults_enforcement": RestApiSpec(
1140+
"graph", f"{base}/identitySecurityDefaultsEnforcementPolicy"
1141+
),
1142+
"activity_based_timeout": RestApiSpec(
1143+
"graph", f"{base}/activityBasedTimeoutPolicies", expect_array=True, access_path="value"
1144+
),
1145+
"app_management": RestApiSpec(
1146+
"graph", f"{base}/appManagementPolicies", expect_array=True, access_path="value"
1147+
),
1148+
"authentication_strength": RestApiSpec(
1149+
"graph", f"{base}/authenticationStrengthPolicies", expect_array=True, access_path="value"
1150+
),
1151+
"claims_mapping": RestApiSpec(
1152+
"graph", f"{base}/claimsMappingPolicies", expect_array=True, access_path="value"
1153+
),
1154+
"conditional_access": RestApiSpec(
1155+
"graph", f"{base}/conditionalAccessPolicies", expect_array=True, access_path="value"
1156+
),
1157+
"feature_rollout": RestApiSpec(
1158+
"graph", f"{base}/featureRolloutPolicies", expect_array=True, access_path="value"
1159+
),
1160+
"home_realm_discovery": RestApiSpec(
1161+
"graph", f"{base}/homeRealmDiscoveryPolicies", expect_array=True, access_path="value"
1162+
),
1163+
"token_issuance": RestApiSpec(
1164+
"graph", f"{base}/tokenIssuancePolicies", expect_array=True, access_path="value"
1165+
),
1166+
}
1167+
result = []
1168+
for policy_kind, spec in policies.items():
1169+
try:
1170+
for response in builder.client.list(spec, **kwargs):
1171+
rid = response.pop("id", policy_kind)
1172+
name = response.pop("displayName", policy_kind)
1173+
description = response.pop("description", None)
1174+
enabled = response.pop("isEnabled", None) or response.pop("state", None) == "enabled" or True
1175+
created = response.pop("createdDateTime", None)
1176+
updated = response.pop("modifiedDateTime", None)
1177+
policy = reformat_keys_to_snake({k: v for k, v in response.items() if not k.startswith("@odata")})
1178+
gp = MicrosoftGraphPolicy(
1179+
id=rid,
1180+
policy_kind=policy_kind,
1181+
name=name,
1182+
ctime=parse_datetime(created) if created else None,
1183+
mtime=parse_datetime(updated) if updated else None,
1184+
description=description,
1185+
policy=policy, # type: ignore
1186+
enabled=enabled,
1187+
)
1188+
builder.add_node(gp)
1189+
result.append(gp)
1190+
1191+
except Exception as e:
1192+
log.warning(f"Error while collecting policies with service {spec.service}: {e}")
1193+
return result
1194+
1195+
11061196
KindLookup = {
11071197
"#microsoft.graph.user": MicrosoftGraphUser,
11081198
"#microsoft.graph.group": MicrosoftGraphGroup,
@@ -1117,6 +1207,7 @@ class MicrosoftGraphOrganizationRoot(MicrosoftGraphEntity, BaseRegion):
11171207

11181208

11191209
resources: List[Type[MicrosoftResource]] = [
1210+
MicrosoftGraphPolicy,
11201211
MicrosoftGraphDevice,
11211212
MicrosoftGraphServicePrincipal,
11221213
MicrosoftGraphGroup,

plugins/azure/fix_plugin_azure/resource/security.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from fix_plugin_azure.azure_client import AzureResourceSpec
77
from fix_plugin_azure.resource.base import MicrosoftResource, AzureSystemData, GraphBuilder
8-
from fixlib.json_bender import Bender, S, Bend, ForallBend
8+
from fixlib.json_bender import Bender, S, Bend, ForallBend, F
99
from fixlib.types import Json
1010

1111

@@ -170,7 +170,28 @@ class AzureSecuritySetting(MicrosoftResource):
170170
enabled: Optional[bool] = field(default=None, metadata={"description": "Indicates whether the setting is enabled."})
171171

172172

173+
@define(eq=False, slots=False)
174+
class AzureAutoProvisioningSetting(MicrosoftResource):
175+
kind: ClassVar[str] = "azure_auto_provisioning_setting"
176+
api_spec: ClassVar[AzureResourceSpec] = AzureResourceSpec(
177+
service="security",
178+
version="2017-08-01-preview",
179+
path="/subscriptions/{subscriptionId}/providers/Microsoft.Security/autoProvisioningSettings",
180+
path_parameters=["subscriptionId"],
181+
query_parameters=["api-version"],
182+
access_path="value",
183+
expect_array=True,
184+
)
185+
mapping: ClassVar[Dict[str, Bender]] = {
186+
"id": S("id"),
187+
"name": S("name"),
188+
"auto_provision": S("properties", "autoProvision") >> F(lambda x: x == "On"),
189+
}
190+
auto_provision: Optional[bool] = field(default=None, metadata={'description': 'describes properties of an auto provisioning setting'}) # fmt: skip
191+
192+
173193
resources: List[Type[MicrosoftResource]] = [
194+
AzureAutoProvisioningSetting,
174195
AzureSecurityAssessment,
175196
AzureSecurityPricing,
176197
AzureSecurityServerVulnerabilityAssessmentsSetting,

0 commit comments

Comments
 (0)