cartography 0.102.0rc2__py3-none-any.whl → 0.103.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.

Potentially problematic release.


This version of cartography might be problematic. Click here for more details.

Files changed (297) hide show
  1. cartography/__main__.py +1 -2
  2. cartography/_version.py +2 -2
  3. cartography/cli.py +376 -249
  4. cartography/client/core/tx.py +39 -18
  5. cartography/config.py +28 -0
  6. cartography/driftdetect/__main__.py +1 -2
  7. cartography/driftdetect/add_shortcut.py +10 -2
  8. cartography/driftdetect/cli.py +71 -75
  9. cartography/driftdetect/detect_deviations.py +7 -3
  10. cartography/driftdetect/get_states.py +20 -8
  11. cartography/driftdetect/model.py +5 -5
  12. cartography/driftdetect/serializers.py +8 -6
  13. cartography/driftdetect/storage.py +2 -2
  14. cartography/graph/cleanupbuilder.py +35 -15
  15. cartography/graph/job.py +46 -17
  16. cartography/graph/querybuilder.py +165 -80
  17. cartography/graph/statement.py +35 -26
  18. cartography/intel/analysis.py +4 -1
  19. cartography/intel/aws/__init__.py +114 -55
  20. cartography/intel/aws/apigateway.py +134 -63
  21. cartography/intel/aws/cloudtrail.py +127 -0
  22. cartography/intel/aws/cloudwatch.py +93 -0
  23. cartography/intel/aws/config.py +56 -20
  24. cartography/intel/aws/dynamodb.py +108 -40
  25. cartography/intel/aws/ec2/__init__.py +2 -2
  26. cartography/intel/aws/ec2/auto_scaling_groups.py +181 -78
  27. cartography/intel/aws/ec2/elastic_ip_addresses.py +41 -13
  28. cartography/intel/aws/ec2/images.py +49 -20
  29. cartography/intel/aws/ec2/instances.py +234 -136
  30. cartography/intel/aws/ec2/internet_gateways.py +40 -11
  31. cartography/intel/aws/ec2/key_pairs.py +44 -20
  32. cartography/intel/aws/ec2/launch_templates.py +101 -59
  33. cartography/intel/aws/ec2/load_balancer_v2s.py +104 -39
  34. cartography/intel/aws/ec2/load_balancers.py +82 -42
  35. cartography/intel/aws/ec2/network_acls.py +89 -65
  36. cartography/intel/aws/ec2/network_interfaces.py +146 -87
  37. cartography/intel/aws/ec2/reserved_instances.py +45 -16
  38. cartography/intel/aws/ec2/route_tables.py +138 -98
  39. cartography/intel/aws/ec2/security_groups.py +71 -21
  40. cartography/intel/aws/ec2/snapshots.py +61 -22
  41. cartography/intel/aws/ec2/subnets.py +54 -18
  42. cartography/intel/aws/ec2/tgw.py +100 -34
  43. cartography/intel/aws/ec2/util.py +1 -1
  44. cartography/intel/aws/ec2/volumes.py +69 -41
  45. cartography/intel/aws/ec2/vpc.py +37 -12
  46. cartography/intel/aws/ec2/vpc_peerings.py +83 -24
  47. cartography/intel/aws/ecr.py +88 -32
  48. cartography/intel/aws/ecs.py +83 -47
  49. cartography/intel/aws/efs.py +93 -0
  50. cartography/intel/aws/eks.py +55 -29
  51. cartography/intel/aws/elasticache.py +42 -18
  52. cartography/intel/aws/elasticsearch.py +57 -20
  53. cartography/intel/aws/emr.py +61 -23
  54. cartography/intel/aws/iam.py +401 -145
  55. cartography/intel/aws/iam_instance_profiles.py +22 -22
  56. cartography/intel/aws/identitycenter.py +71 -37
  57. cartography/intel/aws/inspector.py +159 -89
  58. cartography/intel/aws/kms.py +92 -38
  59. cartography/intel/aws/lambda_function.py +103 -34
  60. cartography/intel/aws/organizations.py +30 -10
  61. cartography/intel/aws/permission_relationships.py +133 -51
  62. cartography/intel/aws/rds.py +249 -85
  63. cartography/intel/aws/redshift.py +107 -46
  64. cartography/intel/aws/resourcegroupstaggingapi.py +120 -66
  65. cartography/intel/aws/resources.py +57 -46
  66. cartography/intel/aws/route53.py +108 -61
  67. cartography/intel/aws/s3.py +168 -83
  68. cartography/intel/aws/s3accountpublicaccessblock.py +157 -0
  69. cartography/intel/aws/secretsmanager.py +24 -12
  70. cartography/intel/aws/securityhub.py +20 -9
  71. cartography/intel/aws/sns.py +166 -0
  72. cartography/intel/aws/sqs.py +60 -28
  73. cartography/intel/aws/ssm.py +70 -30
  74. cartography/intel/aws/util/arns.py +7 -7
  75. cartography/intel/aws/util/common.py +31 -4
  76. cartography/intel/azure/__init__.py +78 -19
  77. cartography/intel/azure/compute.py +101 -27
  78. cartography/intel/azure/cosmosdb.py +496 -170
  79. cartography/intel/azure/sql.py +296 -105
  80. cartography/intel/azure/storage.py +322 -113
  81. cartography/intel/azure/subscription.py +39 -23
  82. cartography/intel/azure/tenant.py +13 -4
  83. cartography/intel/azure/util/credentials.py +95 -55
  84. cartography/intel/bigfix/__init__.py +2 -2
  85. cartography/intel/bigfix/computers.py +93 -65
  86. cartography/intel/cloudflare/__init__.py +74 -0
  87. cartography/intel/cloudflare/accounts.py +57 -0
  88. cartography/intel/cloudflare/dnsrecords.py +64 -0
  89. cartography/intel/cloudflare/members.py +75 -0
  90. cartography/intel/cloudflare/roles.py +65 -0
  91. cartography/intel/cloudflare/zones.py +64 -0
  92. cartography/intel/create_indexes.py +3 -2
  93. cartography/intel/crowdstrike/__init__.py +11 -9
  94. cartography/intel/crowdstrike/endpoints.py +5 -1
  95. cartography/intel/crowdstrike/spotlight.py +8 -3
  96. cartography/intel/cve/__init__.py +46 -13
  97. cartography/intel/cve/feed.py +48 -12
  98. cartography/intel/digitalocean/__init__.py +22 -13
  99. cartography/intel/digitalocean/compute.py +75 -108
  100. cartography/intel/digitalocean/management.py +44 -80
  101. cartography/intel/digitalocean/platform.py +48 -43
  102. cartography/intel/dns.py +36 -10
  103. cartography/intel/duo/__init__.py +21 -16
  104. cartography/intel/duo/api_host.py +14 -9
  105. cartography/intel/duo/endpoints.py +50 -45
  106. cartography/intel/duo/groups.py +18 -14
  107. cartography/intel/duo/phones.py +37 -34
  108. cartography/intel/duo/tokens.py +26 -23
  109. cartography/intel/duo/users.py +54 -50
  110. cartography/intel/duo/web_authn_credentials.py +30 -25
  111. cartography/intel/entra/__init__.py +25 -7
  112. cartography/intel/entra/ou.py +112 -0
  113. cartography/intel/entra/users.py +69 -63
  114. cartography/intel/gcp/__init__.py +185 -49
  115. cartography/intel/gcp/compute.py +418 -231
  116. cartography/intel/gcp/crm.py +96 -43
  117. cartography/intel/gcp/dns.py +60 -19
  118. cartography/intel/gcp/gke.py +72 -38
  119. cartography/intel/gcp/iam.py +61 -41
  120. cartography/intel/gcp/storage.py +84 -55
  121. cartography/intel/github/__init__.py +13 -11
  122. cartography/intel/github/repos.py +270 -137
  123. cartography/intel/github/teams.py +170 -88
  124. cartography/intel/github/users.py +70 -39
  125. cartography/intel/github/util.py +36 -34
  126. cartography/intel/gsuite/__init__.py +47 -26
  127. cartography/intel/gsuite/api.py +73 -30
  128. cartography/intel/jamf/__init__.py +19 -1
  129. cartography/intel/jamf/computers.py +30 -7
  130. cartography/intel/jamf/util.py +7 -2
  131. cartography/intel/kandji/__init__.py +6 -3
  132. cartography/intel/kandji/devices.py +14 -8
  133. cartography/intel/kubernetes/namespaces.py +7 -4
  134. cartography/intel/kubernetes/pods.py +7 -4
  135. cartography/intel/kubernetes/services.py +8 -4
  136. cartography/intel/lastpass/__init__.py +2 -2
  137. cartography/intel/lastpass/users.py +23 -12
  138. cartography/intel/oci/__init__.py +44 -11
  139. cartography/intel/oci/iam.py +134 -38
  140. cartography/intel/oci/organizations.py +13 -6
  141. cartography/intel/oci/utils.py +43 -20
  142. cartography/intel/okta/__init__.py +66 -15
  143. cartography/intel/okta/applications.py +42 -20
  144. cartography/intel/okta/awssaml.py +93 -33
  145. cartography/intel/okta/factors.py +16 -4
  146. cartography/intel/okta/groups.py +56 -29
  147. cartography/intel/okta/organization.py +5 -1
  148. cartography/intel/okta/origins.py +6 -2
  149. cartography/intel/okta/roles.py +15 -5
  150. cartography/intel/okta/users.py +20 -8
  151. cartography/intel/okta/utils.py +6 -4
  152. cartography/intel/openai/__init__.py +86 -0
  153. cartography/intel/openai/adminapikeys.py +90 -0
  154. cartography/intel/openai/apikeys.py +96 -0
  155. cartography/intel/openai/projects.py +94 -0
  156. cartography/intel/openai/serviceaccounts.py +82 -0
  157. cartography/intel/openai/users.py +78 -0
  158. cartography/intel/openai/util.py +29 -0
  159. cartography/intel/pagerduty/__init__.py +8 -7
  160. cartography/intel/pagerduty/escalation_policies.py +18 -6
  161. cartography/intel/pagerduty/schedules.py +12 -4
  162. cartography/intel/pagerduty/services.py +11 -4
  163. cartography/intel/pagerduty/teams.py +8 -3
  164. cartography/intel/pagerduty/users.py +3 -1
  165. cartography/intel/pagerduty/vendors.py +3 -1
  166. cartography/intel/semgrep/__init__.py +24 -6
  167. cartography/intel/semgrep/dependencies.py +50 -28
  168. cartography/intel/semgrep/deployment.py +3 -1
  169. cartography/intel/semgrep/findings.py +42 -18
  170. cartography/intel/snipeit/__init__.py +17 -3
  171. cartography/intel/snipeit/asset.py +12 -6
  172. cartography/intel/snipeit/user.py +8 -5
  173. cartography/intel/snipeit/util.py +9 -4
  174. cartography/intel/tailscale/__init__.py +77 -0
  175. cartography/intel/tailscale/acls.py +146 -0
  176. cartography/intel/tailscale/devices.py +127 -0
  177. cartography/intel/tailscale/postureintegrations.py +81 -0
  178. cartography/intel/tailscale/tailnets.py +76 -0
  179. cartography/intel/tailscale/users.py +80 -0
  180. cartography/intel/tailscale/utils.py +132 -0
  181. cartography/models/aws/apigateway.py +21 -17
  182. cartography/models/aws/apigatewaycertificate.py +28 -22
  183. cartography/models/aws/apigatewayresource.py +28 -20
  184. cartography/models/aws/apigatewaystage.py +33 -25
  185. cartography/models/aws/cloudtrail/__init__.py +0 -0
  186. cartography/models/aws/cloudtrail/trail.py +61 -0
  187. cartography/models/aws/cloudwatch/__init__.py +0 -0
  188. cartography/models/aws/cloudwatch/loggroup.py +52 -0
  189. cartography/models/aws/dynamodb/gsi.py +30 -22
  190. cartography/models/aws/dynamodb/tables.py +25 -17
  191. cartography/models/aws/ec2/auto_scaling_groups.py +102 -82
  192. cartography/models/aws/ec2/images.py +36 -34
  193. cartography/models/aws/ec2/instances.py +51 -45
  194. cartography/models/aws/ec2/keypair.py +21 -16
  195. cartography/models/aws/ec2/keypair_instance.py +28 -21
  196. cartography/models/aws/ec2/launch_configurations.py +30 -26
  197. cartography/models/aws/ec2/launch_template_versions.py +48 -38
  198. cartography/models/aws/ec2/launch_templates.py +21 -17
  199. cartography/models/aws/ec2/load_balancer_listeners.py +27 -23
  200. cartography/models/aws/ec2/load_balancers.py +47 -37
  201. cartography/models/aws/ec2/network_acl_rules.py +38 -30
  202. cartography/models/aws/ec2/network_acls.py +38 -29
  203. cartography/models/aws/ec2/networkinterface_instance.py +52 -39
  204. cartography/models/aws/ec2/networkinterfaces.py +53 -37
  205. cartography/models/aws/ec2/privateip_networkinterface.py +32 -22
  206. cartography/models/aws/ec2/reservations.py +18 -14
  207. cartography/models/aws/ec2/route_table_associations.py +44 -34
  208. cartography/models/aws/ec2/route_tables.py +50 -43
  209. cartography/models/aws/ec2/routes.py +45 -37
  210. cartography/models/aws/ec2/securitygroup_instance.py +29 -20
  211. cartography/models/aws/ec2/securitygroup_networkinterface.py +24 -15
  212. cartography/models/aws/ec2/subnet_instance.py +24 -19
  213. cartography/models/aws/ec2/subnet_networkinterface.py +40 -31
  214. cartography/models/aws/ec2/volumes.py +47 -40
  215. cartography/models/aws/efs/__init__.py +0 -0
  216. cartography/models/aws/efs/mount_target.py +52 -0
  217. cartography/models/aws/eks/clusters.py +23 -21
  218. cartography/models/aws/emr.py +32 -30
  219. cartography/models/aws/iam/instanceprofile.py +33 -24
  220. cartography/models/aws/identitycenter/awsidentitycenter.py +18 -14
  221. cartography/models/aws/identitycenter/awspermissionset.py +37 -29
  222. cartography/models/aws/identitycenter/awsssouser.py +23 -21
  223. cartography/models/aws/inspector/findings.py +77 -65
  224. cartography/models/aws/inspector/packages.py +35 -29
  225. cartography/models/aws/s3/__init__.py +0 -0
  226. cartography/models/aws/s3/account_public_access_block.py +51 -0
  227. cartography/models/aws/sns/__init__.py +0 -0
  228. cartography/models/aws/sns/topic.py +50 -0
  229. cartography/models/aws/ssm/instance_information.py +51 -39
  230. cartography/models/aws/ssm/instance_patch.py +32 -26
  231. cartography/models/bigfix/bigfix_computer.py +42 -38
  232. cartography/models/bigfix/bigfix_root.py +3 -3
  233. cartography/models/cloudflare/__init__.py +0 -0
  234. cartography/models/cloudflare/account.py +25 -0
  235. cartography/models/cloudflare/dnsrecord.py +55 -0
  236. cartography/models/cloudflare/member.py +82 -0
  237. cartography/models/cloudflare/role.py +44 -0
  238. cartography/models/cloudflare/zone.py +59 -0
  239. cartography/models/core/common.py +12 -10
  240. cartography/models/core/nodes.py +5 -2
  241. cartography/models/core/relationships.py +14 -6
  242. cartography/models/crowdstrike/hosts.py +37 -35
  243. cartography/models/cve/cve.py +34 -32
  244. cartography/models/cve/cve_feed.py +6 -6
  245. cartography/models/digitalocean/__init__.py +0 -0
  246. cartography/models/digitalocean/account.py +21 -0
  247. cartography/models/digitalocean/droplet.py +56 -0
  248. cartography/models/digitalocean/project.py +48 -0
  249. cartography/models/duo/api_host.py +3 -3
  250. cartography/models/duo/endpoint.py +43 -41
  251. cartography/models/duo/group.py +14 -14
  252. cartography/models/duo/phone.py +27 -27
  253. cartography/models/duo/token.py +16 -16
  254. cartography/models/duo/user.py +46 -44
  255. cartography/models/duo/web_authn_credential.py +27 -19
  256. cartography/models/entra/ou.py +48 -0
  257. cartography/models/entra/tenant.py +24 -18
  258. cartography/models/entra/user.py +64 -48
  259. cartography/models/gcp/iam.py +23 -23
  260. cartography/models/github/orgs.py +5 -4
  261. cartography/models/github/teams.py +37 -31
  262. cartography/models/github/users.py +34 -23
  263. cartography/models/kandji/device.py +22 -16
  264. cartography/models/kandji/tenant.py +6 -4
  265. cartography/models/lastpass/tenant.py +3 -3
  266. cartography/models/lastpass/user.py +32 -28
  267. cartography/models/openai/__init__.py +0 -0
  268. cartography/models/openai/adminapikey.py +90 -0
  269. cartography/models/openai/apikey.py +84 -0
  270. cartography/models/openai/organization.py +17 -0
  271. cartography/models/openai/project.py +70 -0
  272. cartography/models/openai/serviceaccount.py +50 -0
  273. cartography/models/openai/user.py +49 -0
  274. cartography/models/semgrep/dependencies.py +36 -24
  275. cartography/models/semgrep/deployment.py +5 -5
  276. cartography/models/semgrep/findings.py +58 -42
  277. cartography/models/semgrep/locations.py +27 -21
  278. cartography/models/snipeit/asset.py +30 -21
  279. cartography/models/snipeit/tenant.py +6 -4
  280. cartography/models/snipeit/user.py +19 -12
  281. cartography/models/tailscale/__init__.py +0 -0
  282. cartography/models/tailscale/device.py +95 -0
  283. cartography/models/tailscale/group.py +86 -0
  284. cartography/models/tailscale/postureintegration.py +58 -0
  285. cartography/models/tailscale/tag.py +102 -0
  286. cartography/models/tailscale/tailnet.py +29 -0
  287. cartography/models/tailscale/user.py +52 -0
  288. cartography/stats.py +3 -3
  289. cartography/sync.py +113 -31
  290. cartography/util.py +84 -62
  291. {cartography-0.102.0rc2.dist-info → cartography-0.103.0.dist-info}/METADATA +8 -15
  292. cartography-0.103.0.dist-info/RECORD +442 -0
  293. {cartography-0.102.0rc2.dist-info → cartography-0.103.0.dist-info}/WHEEL +1 -1
  294. cartography-0.102.0rc2.dist-info/RECORD +0 -381
  295. {cartography-0.102.0rc2.dist-info → cartography-0.103.0.dist-info}/entry_points.txt +0 -0
  296. {cartography-0.102.0rc2.dist-info → cartography-0.103.0.dist-info}/licenses/LICENSE +0 -0
  297. {cartography-0.102.0rc2.dist-info → cartography-0.103.0.dist-info}/top_level.txt +0 -0
@@ -21,9 +21,9 @@ def sync(
21
21
  neo4j_session: neo4j.Session,
22
22
  common_job_parameters: Dict[str, Any],
23
23
  ) -> None:
24
- '''
24
+ """
25
25
  Sync
26
- '''
26
+ """
27
27
  data = _get(client)
28
28
  transformed_data = _transform(data)
29
29
  _load(neo4j_session, transformed_data, common_job_parameters)
@@ -32,34 +32,34 @@ def sync(
32
32
 
33
33
  @timeit
34
34
  def _get(client: duo_client.Admin) -> List[Dict[str, Any]]:
35
- '''
35
+ """
36
36
  Fetch all data
37
37
  https://duo.com/docs/adminapi#endpoints
38
- '''
39
- logger.info(f'Fetching data for {Schema.label}')
38
+ """
39
+ logger.info(f"Fetching data for {Schema.label}")
40
40
  return client.get_tokens()
41
41
 
42
42
 
43
43
  @timeit
44
44
  def _transform(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
45
- '''
45
+ """
46
46
  Reformat the data before loading
47
- '''
48
- logger.info(f'Transforming {len(data)} items for {Schema.label}')
47
+ """
48
+ logger.info(f"Transforming {len(data)} items for {Schema.label}")
49
49
  transformed_data = []
50
50
  for datum in data:
51
51
  transformed_datum = {
52
- 'admins': json.dumps(datum['admins']),
53
- 'serial': datum['serial'],
54
- 'token_id': datum['token_id'],
55
- 'totp_step': datum['totp_step'],
56
- 'type': datum['type'],
52
+ "admins": json.dumps(datum["admins"]),
53
+ "serial": datum["serial"],
54
+ "token_id": datum["token_id"],
55
+ "totp_step": datum["totp_step"],
56
+ "type": datum["type"],
57
57
  }
58
58
  transformed_data.append(transformed_datum)
59
- for user in datum['users']:
59
+ for user in datum["users"]:
60
60
  match_datum = {
61
61
  **transformed_datum,
62
- 'user_id': user['user_id'],
62
+ "user_id": user["user_id"],
63
63
  }
64
64
  transformed_data.append(match_datum)
65
65
  return transformed_data
@@ -71,22 +71,25 @@ def _load(
71
71
  data: List[Dict[str, Any]],
72
72
  common_job_parameters: Dict[str, Any],
73
73
  ) -> None:
74
- '''
74
+ """
75
75
  Load the data into the database
76
- '''
77
- logger.info(f'Loading {len(data)} items for {Schema.label}')
76
+ """
77
+ logger.info(f"Loading {len(data)} items for {Schema.label}")
78
78
  load(
79
79
  neo4j_session,
80
80
  Schema(),
81
81
  data,
82
- DUO_API_HOSTNAME=common_job_parameters['DUO_API_HOSTNAME'],
83
- lastupdated=common_job_parameters['UPDATE_TAG'],
82
+ DUO_API_HOSTNAME=common_job_parameters["DUO_API_HOSTNAME"],
83
+ lastupdated=common_job_parameters["UPDATE_TAG"],
84
84
  )
85
85
 
86
86
 
87
87
  @timeit
88
- def _cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
89
- '''
88
+ def _cleanup(
89
+ neo4j_session: neo4j.Session,
90
+ common_job_parameters: Dict[str, Any],
91
+ ) -> None:
92
+ """
90
93
  Cleanup nodes
91
- '''
94
+ """
92
95
  GraphJob.from_node_schema(Schema(), common_job_parameters).run(neo4j_session)
@@ -21,9 +21,9 @@ def sync_duo_users(
21
21
  neo4j_session: neo4j.Session,
22
22
  common_job_parameters: Dict[str, Any],
23
23
  ) -> None:
24
- '''
24
+ """
25
25
  Sync Duo Users
26
- '''
26
+ """
27
27
  users = _get_users(client)
28
28
  transformed_users = _transform_users(users)
29
29
  _load_users(neo4j_session, transformed_users, common_job_parameters)
@@ -32,77 +32,78 @@ def sync_duo_users(
32
32
 
33
33
  @timeit
34
34
  def _get_users(client: duo_client.Admin) -> List[Dict[str, Any]]:
35
- '''
35
+ """
36
36
  Fetch all users data
37
37
  https://duo.com/docs/adminapi#users
38
- '''
38
+ """
39
39
  logger.info("Fetching Duo users")
40
40
  return client.get_users()
41
41
 
42
42
 
43
43
  @timeit
44
44
  def _transform_users(users: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
45
- '''
45
+ """
46
46
  Reformat the data before loading
47
- '''
48
- logger.info(f'Transforming {len(users)} duo users')
47
+ """
48
+ logger.info(f"Transforming {len(users)} duo users")
49
49
  transformed_users = []
50
50
  for user in users:
51
51
  transformed_user = {
52
- 'alias1': user['alias1'],
53
- 'alias2': user['alias2'],
54
- 'alias3': user['alias3'],
55
- 'alias4': user['alias4'],
56
- 'created': user['created'],
57
- 'email': user['email'],
58
- 'firstname': user['firstname'],
59
- 'is_enrolled': user['is_enrolled'],
60
- 'last_directory_sync': user['last_directory_sync'],
61
- 'last_login': user['last_login'],
62
- 'lastname': user['lastname'],
63
- 'notes': user['notes'],
64
- 'phones': [
65
- dumps({
66
- **phone,
67
- 'number': None,
68
- })
69
- for phone in user['phones']
52
+ "alias1": user["alias1"],
53
+ "alias2": user["alias2"],
54
+ "alias3": user["alias3"],
55
+ "alias4": user["alias4"],
56
+ "created": user["created"],
57
+ "email": user["email"],
58
+ "firstname": user["firstname"],
59
+ "is_enrolled": user["is_enrolled"],
60
+ "last_directory_sync": user["last_directory_sync"],
61
+ "last_login": user["last_login"],
62
+ "lastname": user["lastname"],
63
+ "notes": user["notes"],
64
+ "phones": [
65
+ dumps(
66
+ {
67
+ **phone,
68
+ "number": None,
69
+ },
70
+ )
71
+ for phone in user["phones"]
70
72
  ],
71
- 'realname': user['realname'],
72
- 'status': user['status'],
73
- 'tokens': [dumps(token) for token in user['tokens']],
74
- 'u2ftokens': [dumps(u2ftoken) for u2ftoken in user['u2ftokens']],
75
- 'user_id': user['user_id'],
76
- 'username': user['username'],
77
- 'webauthncredentials': [
73
+ "realname": user["realname"],
74
+ "status": user["status"],
75
+ "tokens": [dumps(token) for token in user["tokens"]],
76
+ "u2ftokens": [dumps(u2ftoken) for u2ftoken in user["u2ftokens"]],
77
+ "user_id": user["user_id"],
78
+ "username": user["username"],
79
+ "webauthncredentials": [
78
80
  dumps(webauthncredential)
79
- for webauthncredential
80
- in user['webauthncredentials']
81
+ for webauthncredential in user["webauthncredentials"]
81
82
  ],
82
83
  }
83
84
  transformed_users.append(transformed_user)
84
- for group in user['groups']:
85
+ for group in user["groups"]:
85
86
  match_user = {
86
87
  **transformed_user,
87
- 'group_id': group['group_id'],
88
+ "group_id": group["group_id"],
88
89
  }
89
90
  transformed_users.append(match_user)
90
- for phone in user['phones']:
91
+ for phone in user["phones"]:
91
92
  match_user = {
92
93
  **transformed_user,
93
- 'phone_id': phone['phone_id'],
94
+ "phone_id": phone["phone_id"],
94
95
  }
95
96
  transformed_users.append(match_user)
96
- for token in user['tokens']:
97
+ for token in user["tokens"]:
97
98
  match_user = {
98
99
  **transformed_user,
99
- 'token_id': token['token_id'],
100
+ "token_id": token["token_id"],
100
101
  }
101
102
  transformed_users.append(match_user)
102
- for webauthncredential in user['webauthncredentials']:
103
+ for webauthncredential in user["webauthncredentials"]:
103
104
  match_user = {
104
105
  **transformed_user,
105
- 'webauthnkey': webauthncredential['webauthnkey'],
106
+ "webauthnkey": webauthncredential["webauthnkey"],
106
107
  }
107
108
  transformed_users.append(match_user)
108
109
  return transformed_users
@@ -114,22 +115,25 @@ def _load_users(
114
115
  users: List[Dict[str, Any]],
115
116
  common_job_parameters: Dict[str, Any],
116
117
  ) -> None:
117
- '''
118
+ """
118
119
  Load the users into the database
119
- '''
120
- logger.info(f'Loading {len(users)} duo users')
120
+ """
121
+ logger.info(f"Loading {len(users)} duo users")
121
122
  load(
122
123
  neo4j_session,
123
124
  DuoUserSchema(),
124
125
  users,
125
- DUO_API_HOSTNAME=common_job_parameters['DUO_API_HOSTNAME'],
126
- lastupdated=common_job_parameters['UPDATE_TAG'],
126
+ DUO_API_HOSTNAME=common_job_parameters["DUO_API_HOSTNAME"],
127
+ lastupdated=common_job_parameters["UPDATE_TAG"],
127
128
  )
128
129
 
129
130
 
130
131
  @timeit
131
- def _cleanup_users(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
132
- '''
132
+ def _cleanup_users(
133
+ neo4j_session: neo4j.Session,
134
+ common_job_parameters: Dict[str, Any],
135
+ ) -> None:
136
+ """
133
137
  Cleanup endpoints
134
- '''
138
+ """
135
139
  GraphJob.from_node_schema(DuoUserSchema(), common_job_parameters).run(neo4j_session)
@@ -8,7 +8,9 @@ import neo4j
8
8
 
9
9
  from cartography.client.core.tx import load
10
10
  from cartography.graph.job import GraphJob
11
- from cartography.models.duo.web_authn_credential import DuoWebAuthnCredentialSchema as Schema
11
+ from cartography.models.duo.web_authn_credential import (
12
+ DuoWebAuthnCredentialSchema as Schema,
13
+ )
12
14
  from cartography.util import timeit
13
15
 
14
16
  logger = logging.getLogger(__name__)
@@ -20,9 +22,9 @@ def sync(
20
22
  neo4j_session: neo4j.Session,
21
23
  common_job_parameters: Dict[str, Any],
22
24
  ) -> None:
23
- '''
25
+ """
24
26
  Sync
25
- '''
27
+ """
26
28
  data = _get(client)
27
29
  transformed_data = _transform(data)
28
30
  _load(neo4j_session, transformed_data, common_job_parameters)
@@ -31,37 +33,37 @@ def sync(
31
33
 
32
34
  @timeit
33
35
  def _get(client: duo_client.Admin) -> List[Dict[str, Any]]:
34
- '''
36
+ """
35
37
  Fetch all data
36
38
  https://duo.com/docs/adminapi#endpoints
37
- '''
38
- logger.info(f'Fetching data for {Schema.label}')
39
+ """
40
+ logger.info(f"Fetching data for {Schema.label}")
39
41
  return client.get_webauthncredentials()
40
42
 
41
43
 
42
44
  @timeit
43
45
  def _transform(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
44
- '''
46
+ """
45
47
  Reformat the data before loading
46
- '''
47
- logger.info(f'Transforming {len(data)} items for {Schema.label}')
48
+ """
49
+ logger.info(f"Transforming {len(data)} items for {Schema.label}")
48
50
  transformed_data = []
49
51
  for datum in data:
50
52
  transformed_datum = {
51
53
  # The admin property may be null if the cred is attached to a user
52
- 'admin': datum.get('admin'),
53
- 'credential_name': datum['credential_name'],
54
- 'date_added': datum['date_added'],
55
- 'label': datum['label'],
56
- 'user': datum['user'],
57
- 'webauthnkey': datum['webauthnkey'],
54
+ "admin": datum.get("admin"),
55
+ "credential_name": datum["credential_name"],
56
+ "date_added": datum["date_added"],
57
+ "label": datum["label"],
58
+ "user": datum["user"],
59
+ "webauthnkey": datum["webauthnkey"],
58
60
  }
59
61
  transformed_data.append(transformed_datum)
60
62
  # The user property may be null if the cred is attached to an admin
61
- if datum.get('user'):
63
+ if datum.get("user"):
62
64
  match_datum = {
63
65
  **transformed_datum,
64
- 'user_id': datum['user']['user_id'],
66
+ "user_id": datum["user"]["user_id"],
65
67
  }
66
68
  transformed_data.append(match_datum)
67
69
  return transformed_data
@@ -73,22 +75,25 @@ def _load(
73
75
  data: List[Dict[str, Any]],
74
76
  common_job_parameters: Dict[str, Any],
75
77
  ) -> None:
76
- '''
78
+ """
77
79
  Load the data into the database
78
- '''
79
- logger.info(f'Loading {len(data)} items for {Schema.label}')
80
+ """
81
+ logger.info(f"Loading {len(data)} items for {Schema.label}")
80
82
  load(
81
83
  neo4j_session,
82
84
  Schema(),
83
85
  data,
84
- DUO_API_HOSTNAME=common_job_parameters['DUO_API_HOSTNAME'],
85
- lastupdated=common_job_parameters['UPDATE_TAG'],
86
+ DUO_API_HOSTNAME=common_job_parameters["DUO_API_HOSTNAME"],
87
+ lastupdated=common_job_parameters["UPDATE_TAG"],
86
88
  )
87
89
 
88
90
 
89
91
  @timeit
90
- def _cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
91
- '''
92
+ def _cleanup(
93
+ neo4j_session: neo4j.Session,
94
+ common_job_parameters: Dict[str, Any],
95
+ ) -> None:
96
+ """
92
97
  Cleanup nodes
93
- '''
98
+ """
94
99
  GraphJob.from_node_schema(Schema(), common_job_parameters).run(neo4j_session)
@@ -4,6 +4,7 @@ import logging
4
4
  import neo4j
5
5
 
6
6
  from cartography.config import Config
7
+ from cartography.intel.entra.ou import sync_entra_ous
7
8
  from cartography.intel.entra.users import sync_entra_users
8
9
  from cartography.util import timeit
9
10
 
@@ -19,10 +20,14 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
19
20
  :return: None
20
21
  """
21
22
 
22
- if not config.entra_tenant_id or not config.entra_client_id or not config.entra_client_secret:
23
+ if (
24
+ not config.entra_tenant_id
25
+ or not config.entra_client_id
26
+ or not config.entra_client_secret
27
+ ):
23
28
  logger.info(
24
- 'Entra import is not configured - skipping this module. '
25
- 'See docs to configure.',
29
+ "Entra import is not configured - skipping this module. "
30
+ "See docs to configure.",
26
31
  )
27
32
  return
28
33
 
@@ -31,13 +36,26 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
31
36
  "TENANT_ID": config.entra_tenant_id,
32
37
  }
33
38
 
34
- asyncio.run(
35
- sync_entra_users(
39
+ async def main() -> None:
40
+ # Run user sync
41
+ await sync_entra_users(
36
42
  neo4j_session,
37
43
  config.entra_tenant_id,
38
44
  config.entra_client_id,
39
45
  config.entra_client_secret,
40
46
  config.update_tag,
41
47
  common_job_parameters,
42
- ),
43
- )
48
+ )
49
+
50
+ # Run OU sync
51
+ await sync_entra_ous(
52
+ neo4j_session,
53
+ config.entra_tenant_id,
54
+ config.entra_client_id,
55
+ config.entra_client_secret,
56
+ config.update_tag,
57
+ common_job_parameters,
58
+ )
59
+
60
+ # Execute both syncs in sequence
61
+ asyncio.run(main())
@@ -0,0 +1,112 @@
1
+ # cartography/intel/entra/ou.py
2
+ import logging
3
+ from typing import Any
4
+
5
+ import neo4j
6
+ from azure.identity import ClientSecretCredential
7
+ from msgraph import GraphServiceClient
8
+ from msgraph.generated.models.administrative_unit import AdministrativeUnit
9
+
10
+ from cartography.client.core.tx import load
11
+ from cartography.graph.job import GraphJob
12
+ from cartography.intel.entra.users import load_tenant
13
+ from cartography.models.entra.ou import EntraOUSchema
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ async def get_entra_ous(client: GraphServiceClient) -> list[AdministrativeUnit]:
21
+ """
22
+ Get all OUs from Microsoft Graph API with pagination support
23
+ """
24
+ all_units: list[AdministrativeUnit] = []
25
+ request = client.directory.administrative_units.request()
26
+
27
+ while request:
28
+ response = await request.get()
29
+ all_units.extend(response.value)
30
+ request = response.odata_next_link if response.odata_next_link else None
31
+
32
+ return all_units
33
+
34
+
35
+ def transform_ous(
36
+ units: list[AdministrativeUnit], tenant_id: str
37
+ ) -> list[dict[str, Any]]:
38
+ """
39
+ Transform the API response into the format expected by our schema
40
+ """
41
+ result: list[dict[str, Any]] = []
42
+ for unit in units:
43
+ transformed_unit = {
44
+ "id": unit.id,
45
+ "display_name": unit.display_name,
46
+ "description": unit.description,
47
+ "visibility": unit.visibility,
48
+ "membership_type": unit.membership_type,
49
+ "is_member_management_restricted": unit.is_member_management_restricted,
50
+ "deleted_date_time": unit.deleted_date_time,
51
+ "tenant_id": tenant_id,
52
+ }
53
+ result.append(transformed_unit)
54
+ return result
55
+
56
+
57
+ @timeit
58
+ def load_ous(
59
+ neo4j_session: neo4j.Session,
60
+ units: list[dict[str, Any]],
61
+ update_tag: int,
62
+ common_job_parameters: dict[str, Any],
63
+ ) -> None:
64
+ logger.info(f"Loading {len(units)} Entra OUs")
65
+ load(
66
+ neo4j_session,
67
+ EntraOUSchema(),
68
+ units,
69
+ lastupdated=update_tag,
70
+ TENANT_ID=common_job_parameters["TENANT_ID"],
71
+ UPDATE_TAG=common_job_parameters["UPDATE_TAG"],
72
+ )
73
+
74
+
75
+ def cleanup_ous(
76
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
77
+ ) -> None:
78
+ GraphJob.from_node_schema(EntraOUSchema(), common_job_parameters).run(neo4j_session)
79
+
80
+
81
+ @timeit
82
+ async def sync_entra_ous(
83
+ neo4j_session: neo4j.Session,
84
+ tenant_id: str,
85
+ client_id: str,
86
+ client_secret: str,
87
+ update_tag: int,
88
+ common_job_parameters: dict[str, Any],
89
+ ) -> None:
90
+ """
91
+ Sync Entra OUs
92
+ """
93
+ # Initialize Graph client
94
+ credential = ClientSecretCredential(
95
+ tenant_id=tenant_id,
96
+ client_id=client_id,
97
+ client_secret=client_secret,
98
+ )
99
+ client = GraphServiceClient(
100
+ credential, scopes=["https://graph.microsoft.com/.default"]
101
+ )
102
+
103
+ # Get OUs
104
+ units = await get_entra_ous(client)
105
+ transformed_units = transform_ous(units, tenant_id)
106
+
107
+ # Load data
108
+ load_tenant(neo4j_session, {"id": tenant_id}, update_tag)
109
+ load_ous(neo4j_session, transformed_units, update_tag, common_job_parameters)
110
+
111
+ # Cleanup stale data
112
+ cleanup_ous(neo4j_session, common_job_parameters)