cartography 0.102.0rc2__py3-none-any.whl → 0.103.0rc1__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 (251) hide show
  1. cartography/__main__.py +1 -2
  2. cartography/_version.py +2 -2
  3. cartography/cli.py +302 -253
  4. cartography/client/core/tx.py +39 -18
  5. cartography/config.py +4 -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/config.py +56 -20
  23. cartography/intel/aws/dynamodb.py +108 -40
  24. cartography/intel/aws/ec2/__init__.py +2 -2
  25. cartography/intel/aws/ec2/auto_scaling_groups.py +181 -78
  26. cartography/intel/aws/ec2/elastic_ip_addresses.py +41 -13
  27. cartography/intel/aws/ec2/images.py +49 -20
  28. cartography/intel/aws/ec2/instances.py +234 -136
  29. cartography/intel/aws/ec2/internet_gateways.py +40 -11
  30. cartography/intel/aws/ec2/key_pairs.py +44 -20
  31. cartography/intel/aws/ec2/launch_templates.py +101 -59
  32. cartography/intel/aws/ec2/load_balancer_v2s.py +104 -39
  33. cartography/intel/aws/ec2/load_balancers.py +82 -42
  34. cartography/intel/aws/ec2/network_acls.py +89 -65
  35. cartography/intel/aws/ec2/network_interfaces.py +146 -87
  36. cartography/intel/aws/ec2/reserved_instances.py +45 -16
  37. cartography/intel/aws/ec2/route_tables.py +138 -98
  38. cartography/intel/aws/ec2/security_groups.py +71 -21
  39. cartography/intel/aws/ec2/snapshots.py +61 -22
  40. cartography/intel/aws/ec2/subnets.py +54 -18
  41. cartography/intel/aws/ec2/tgw.py +100 -34
  42. cartography/intel/aws/ec2/util.py +1 -1
  43. cartography/intel/aws/ec2/volumes.py +69 -41
  44. cartography/intel/aws/ec2/vpc.py +37 -12
  45. cartography/intel/aws/ec2/vpc_peerings.py +83 -24
  46. cartography/intel/aws/ecr.py +88 -32
  47. cartography/intel/aws/ecs.py +83 -47
  48. cartography/intel/aws/eks.py +55 -29
  49. cartography/intel/aws/elasticache.py +42 -18
  50. cartography/intel/aws/elasticsearch.py +57 -20
  51. cartography/intel/aws/emr.py +61 -23
  52. cartography/intel/aws/iam.py +401 -145
  53. cartography/intel/aws/iam_instance_profiles.py +22 -22
  54. cartography/intel/aws/identitycenter.py +71 -37
  55. cartography/intel/aws/inspector.py +159 -89
  56. cartography/intel/aws/kms.py +92 -38
  57. cartography/intel/aws/lambda_function.py +103 -34
  58. cartography/intel/aws/organizations.py +30 -10
  59. cartography/intel/aws/permission_relationships.py +133 -51
  60. cartography/intel/aws/rds.py +249 -85
  61. cartography/intel/aws/redshift.py +107 -46
  62. cartography/intel/aws/resourcegroupstaggingapi.py +120 -66
  63. cartography/intel/aws/resources.py +53 -46
  64. cartography/intel/aws/route53.py +108 -61
  65. cartography/intel/aws/s3.py +168 -83
  66. cartography/intel/aws/s3accountpublicaccessblock.py +157 -0
  67. cartography/intel/aws/secretsmanager.py +24 -12
  68. cartography/intel/aws/securityhub.py +20 -9
  69. cartography/intel/aws/sns.py +166 -0
  70. cartography/intel/aws/sqs.py +60 -28
  71. cartography/intel/aws/ssm.py +70 -30
  72. cartography/intel/aws/util/arns.py +7 -7
  73. cartography/intel/aws/util/common.py +31 -4
  74. cartography/intel/azure/__init__.py +78 -19
  75. cartography/intel/azure/compute.py +101 -27
  76. cartography/intel/azure/cosmosdb.py +496 -170
  77. cartography/intel/azure/sql.py +296 -105
  78. cartography/intel/azure/storage.py +322 -113
  79. cartography/intel/azure/subscription.py +39 -23
  80. cartography/intel/azure/tenant.py +13 -4
  81. cartography/intel/azure/util/credentials.py +95 -55
  82. cartography/intel/bigfix/__init__.py +2 -2
  83. cartography/intel/bigfix/computers.py +93 -65
  84. cartography/intel/create_indexes.py +3 -2
  85. cartography/intel/crowdstrike/__init__.py +11 -9
  86. cartography/intel/crowdstrike/endpoints.py +5 -1
  87. cartography/intel/crowdstrike/spotlight.py +8 -3
  88. cartography/intel/cve/__init__.py +46 -13
  89. cartography/intel/cve/feed.py +48 -12
  90. cartography/intel/digitalocean/__init__.py +22 -13
  91. cartography/intel/digitalocean/compute.py +75 -108
  92. cartography/intel/digitalocean/management.py +44 -80
  93. cartography/intel/digitalocean/platform.py +48 -43
  94. cartography/intel/dns.py +36 -10
  95. cartography/intel/duo/__init__.py +21 -16
  96. cartography/intel/duo/api_host.py +14 -9
  97. cartography/intel/duo/endpoints.py +50 -45
  98. cartography/intel/duo/groups.py +18 -14
  99. cartography/intel/duo/phones.py +37 -34
  100. cartography/intel/duo/tokens.py +26 -23
  101. cartography/intel/duo/users.py +54 -50
  102. cartography/intel/duo/web_authn_credentials.py +30 -25
  103. cartography/intel/entra/__init__.py +25 -7
  104. cartography/intel/entra/ou.py +112 -0
  105. cartography/intel/entra/users.py +69 -63
  106. cartography/intel/gcp/__init__.py +185 -49
  107. cartography/intel/gcp/compute.py +418 -231
  108. cartography/intel/gcp/crm.py +96 -43
  109. cartography/intel/gcp/dns.py +60 -19
  110. cartography/intel/gcp/gke.py +72 -38
  111. cartography/intel/gcp/iam.py +61 -41
  112. cartography/intel/gcp/storage.py +84 -55
  113. cartography/intel/github/__init__.py +13 -11
  114. cartography/intel/github/repos.py +270 -137
  115. cartography/intel/github/teams.py +170 -88
  116. cartography/intel/github/users.py +70 -39
  117. cartography/intel/github/util.py +36 -34
  118. cartography/intel/gsuite/__init__.py +47 -26
  119. cartography/intel/gsuite/api.py +73 -30
  120. cartography/intel/jamf/__init__.py +19 -1
  121. cartography/intel/jamf/computers.py +30 -7
  122. cartography/intel/jamf/util.py +7 -2
  123. cartography/intel/kandji/__init__.py +6 -3
  124. cartography/intel/kandji/devices.py +14 -8
  125. cartography/intel/kubernetes/namespaces.py +7 -4
  126. cartography/intel/kubernetes/pods.py +7 -4
  127. cartography/intel/kubernetes/services.py +8 -4
  128. cartography/intel/lastpass/__init__.py +2 -2
  129. cartography/intel/lastpass/users.py +23 -12
  130. cartography/intel/oci/__init__.py +44 -11
  131. cartography/intel/oci/iam.py +134 -38
  132. cartography/intel/oci/organizations.py +13 -6
  133. cartography/intel/oci/utils.py +43 -20
  134. cartography/intel/okta/__init__.py +66 -15
  135. cartography/intel/okta/applications.py +42 -20
  136. cartography/intel/okta/awssaml.py +93 -33
  137. cartography/intel/okta/factors.py +16 -4
  138. cartography/intel/okta/groups.py +56 -29
  139. cartography/intel/okta/organization.py +5 -1
  140. cartography/intel/okta/origins.py +6 -2
  141. cartography/intel/okta/roles.py +15 -5
  142. cartography/intel/okta/users.py +20 -8
  143. cartography/intel/okta/utils.py +6 -4
  144. cartography/intel/pagerduty/__init__.py +8 -7
  145. cartography/intel/pagerduty/escalation_policies.py +18 -6
  146. cartography/intel/pagerduty/schedules.py +12 -4
  147. cartography/intel/pagerduty/services.py +11 -4
  148. cartography/intel/pagerduty/teams.py +8 -3
  149. cartography/intel/pagerduty/users.py +3 -1
  150. cartography/intel/pagerduty/vendors.py +3 -1
  151. cartography/intel/semgrep/__init__.py +24 -6
  152. cartography/intel/semgrep/dependencies.py +50 -28
  153. cartography/intel/semgrep/deployment.py +3 -1
  154. cartography/intel/semgrep/findings.py +42 -18
  155. cartography/intel/snipeit/__init__.py +17 -3
  156. cartography/intel/snipeit/asset.py +12 -6
  157. cartography/intel/snipeit/user.py +8 -5
  158. cartography/intel/snipeit/util.py +9 -4
  159. cartography/models/aws/apigateway.py +21 -17
  160. cartography/models/aws/apigatewaycertificate.py +28 -22
  161. cartography/models/aws/apigatewayresource.py +28 -20
  162. cartography/models/aws/apigatewaystage.py +33 -25
  163. cartography/models/aws/cloudtrail/__init__.py +0 -0
  164. cartography/models/aws/cloudtrail/trail.py +61 -0
  165. cartography/models/aws/dynamodb/gsi.py +30 -22
  166. cartography/models/aws/dynamodb/tables.py +25 -17
  167. cartography/models/aws/ec2/auto_scaling_groups.py +102 -82
  168. cartography/models/aws/ec2/images.py +36 -34
  169. cartography/models/aws/ec2/instances.py +51 -45
  170. cartography/models/aws/ec2/keypair.py +21 -16
  171. cartography/models/aws/ec2/keypair_instance.py +28 -21
  172. cartography/models/aws/ec2/launch_configurations.py +30 -26
  173. cartography/models/aws/ec2/launch_template_versions.py +48 -38
  174. cartography/models/aws/ec2/launch_templates.py +21 -17
  175. cartography/models/aws/ec2/load_balancer_listeners.py +27 -23
  176. cartography/models/aws/ec2/load_balancers.py +47 -37
  177. cartography/models/aws/ec2/network_acl_rules.py +38 -30
  178. cartography/models/aws/ec2/network_acls.py +38 -29
  179. cartography/models/aws/ec2/networkinterface_instance.py +52 -39
  180. cartography/models/aws/ec2/networkinterfaces.py +53 -37
  181. cartography/models/aws/ec2/privateip_networkinterface.py +32 -22
  182. cartography/models/aws/ec2/reservations.py +18 -14
  183. cartography/models/aws/ec2/route_table_associations.py +44 -34
  184. cartography/models/aws/ec2/route_tables.py +50 -43
  185. cartography/models/aws/ec2/routes.py +45 -37
  186. cartography/models/aws/ec2/securitygroup_instance.py +29 -20
  187. cartography/models/aws/ec2/securitygroup_networkinterface.py +24 -15
  188. cartography/models/aws/ec2/subnet_instance.py +24 -19
  189. cartography/models/aws/ec2/subnet_networkinterface.py +40 -31
  190. cartography/models/aws/ec2/volumes.py +47 -40
  191. cartography/models/aws/eks/clusters.py +23 -21
  192. cartography/models/aws/emr.py +32 -30
  193. cartography/models/aws/iam/instanceprofile.py +33 -24
  194. cartography/models/aws/identitycenter/awsidentitycenter.py +18 -14
  195. cartography/models/aws/identitycenter/awspermissionset.py +37 -29
  196. cartography/models/aws/identitycenter/awsssouser.py +23 -21
  197. cartography/models/aws/inspector/findings.py +77 -65
  198. cartography/models/aws/inspector/packages.py +35 -29
  199. cartography/models/aws/s3/__init__.py +0 -0
  200. cartography/models/aws/s3/account_public_access_block.py +51 -0
  201. cartography/models/aws/sns/__init__.py +0 -0
  202. cartography/models/aws/sns/topic.py +50 -0
  203. cartography/models/aws/ssm/instance_information.py +51 -39
  204. cartography/models/aws/ssm/instance_patch.py +32 -26
  205. cartography/models/bigfix/bigfix_computer.py +42 -38
  206. cartography/models/bigfix/bigfix_root.py +3 -3
  207. cartography/models/core/common.py +12 -10
  208. cartography/models/core/nodes.py +5 -2
  209. cartography/models/core/relationships.py +14 -6
  210. cartography/models/crowdstrike/hosts.py +37 -35
  211. cartography/models/cve/cve.py +34 -32
  212. cartography/models/cve/cve_feed.py +6 -6
  213. cartography/models/digitalocean/__init__.py +0 -0
  214. cartography/models/digitalocean/account.py +21 -0
  215. cartography/models/digitalocean/droplet.py +56 -0
  216. cartography/models/digitalocean/project.py +48 -0
  217. cartography/models/duo/api_host.py +3 -3
  218. cartography/models/duo/endpoint.py +43 -41
  219. cartography/models/duo/group.py +14 -14
  220. cartography/models/duo/phone.py +27 -27
  221. cartography/models/duo/token.py +16 -16
  222. cartography/models/duo/user.py +46 -44
  223. cartography/models/duo/web_authn_credential.py +27 -19
  224. cartography/models/entra/ou.py +48 -0
  225. cartography/models/entra/tenant.py +24 -18
  226. cartography/models/entra/user.py +64 -48
  227. cartography/models/gcp/iam.py +23 -23
  228. cartography/models/github/orgs.py +5 -4
  229. cartography/models/github/teams.py +37 -31
  230. cartography/models/github/users.py +34 -23
  231. cartography/models/kandji/device.py +22 -16
  232. cartography/models/kandji/tenant.py +6 -4
  233. cartography/models/lastpass/tenant.py +3 -3
  234. cartography/models/lastpass/user.py +32 -28
  235. cartography/models/semgrep/dependencies.py +36 -24
  236. cartography/models/semgrep/deployment.py +5 -5
  237. cartography/models/semgrep/findings.py +58 -42
  238. cartography/models/semgrep/locations.py +27 -21
  239. cartography/models/snipeit/asset.py +30 -21
  240. cartography/models/snipeit/tenant.py +6 -4
  241. cartography/models/snipeit/user.py +19 -12
  242. cartography/stats.py +3 -3
  243. cartography/sync.py +107 -31
  244. cartography/util.py +84 -62
  245. {cartography-0.102.0rc2.dist-info → cartography-0.103.0rc1.dist-info}/METADATA +3 -14
  246. cartography-0.103.0rc1.dist-info/RECORD +396 -0
  247. {cartography-0.102.0rc2.dist-info → cartography-0.103.0rc1.dist-info}/WHEEL +1 -1
  248. cartography-0.102.0rc2.dist-info/RECORD +0 -381
  249. {cartography-0.102.0rc2.dist-info → cartography-0.103.0rc1.dist-info}/entry_points.txt +0 -0
  250. {cartography-0.102.0rc2.dist-info → cartography-0.103.0rc1.dist-info}/licenses/LICENSE +0 -0
  251. {cartography-0.102.0rc2.dist-info → cartography-0.103.0rc1.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,6 @@ from cartography.graph.job import GraphJob
11
11
  from cartography.models.duo.group import DuoGroupSchema
12
12
  from cartography.util import timeit
13
13
 
14
-
15
14
  logger = logging.getLogger(__name__)
16
15
 
17
16
 
@@ -21,9 +20,9 @@ def sync_duo_groups(
21
20
  neo4j_session: neo4j.Session,
22
21
  common_job_parameters: Dict[str, Any],
23
22
  ) -> None:
24
- '''
23
+ """
25
24
  Sync Duo groups
26
- '''
25
+ """
27
26
  groups = _get_groups(client)
28
27
  _load_groups(neo4j_session, groups, common_job_parameters)
29
28
  _cleanup_groups(neo4j_session, common_job_parameters)
@@ -31,10 +30,10 @@ def sync_duo_groups(
31
30
 
32
31
  @timeit
33
32
  def _get_groups(client: duo_client.Admin) -> List[Dict[str, Any]]:
34
- '''
33
+ """
35
34
  Fetch all group data
36
35
  https://duo.com/docs/adminapi#users
37
- '''
36
+ """
38
37
  logger.info("Fetching Duo groups")
39
38
  return client.get_groups()
40
39
 
@@ -45,22 +44,27 @@ def _load_groups(
45
44
  groups: List[Dict[str, Any]],
46
45
  common_job_parameters: Dict[str, Any],
47
46
  ) -> None:
48
- '''
47
+ """
49
48
  Load the groups into the graph
50
- '''
51
- logger.info(f'Loading {len(groups)} duo groups')
49
+ """
50
+ logger.info(f"Loading {len(groups)} duo groups")
52
51
  load(
53
52
  neo4j_session,
54
53
  DuoGroupSchema(),
55
54
  groups,
56
- DUO_API_HOSTNAME=common_job_parameters['DUO_API_HOSTNAME'],
57
- lastupdated=common_job_parameters['UPDATE_TAG'],
55
+ DUO_API_HOSTNAME=common_job_parameters["DUO_API_HOSTNAME"],
56
+ lastupdated=common_job_parameters["UPDATE_TAG"],
58
57
  )
59
58
 
60
59
 
61
60
  @timeit
62
- def _cleanup_groups(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
63
- '''
61
+ def _cleanup_groups(
62
+ neo4j_session: neo4j.Session,
63
+ common_job_parameters: Dict[str, Any],
64
+ ) -> None:
65
+ """
64
66
  Cleanup endpoints
65
- '''
66
- GraphJob.from_node_schema(DuoGroupSchema(), common_job_parameters).run(neo4j_session)
67
+ """
68
+ GraphJob.from_node_schema(DuoGroupSchema(), common_job_parameters).run(
69
+ neo4j_session,
70
+ )
@@ -20,9 +20,9 @@ def sync(
20
20
  neo4j_session: neo4j.Session,
21
21
  common_job_parameters: Dict[str, Any],
22
22
  ) -> None:
23
- '''
23
+ """
24
24
  Sync
25
- '''
25
+ """
26
26
  data = _get(client)
27
27
  transformed_data = _transform(data)
28
28
  _load(neo4j_session, transformed_data, common_job_parameters)
@@ -31,45 +31,45 @@ def sync(
31
31
 
32
32
  @timeit
33
33
  def _get(client: duo_client.Admin) -> List[Dict[str, Any]]:
34
- '''
34
+ """
35
35
  Fetch all data
36
36
  https://duo.com/docs/adminapi#endpoints
37
- '''
38
- logger.info(f'Fetching data for {Schema.label}')
37
+ """
38
+ logger.info(f"Fetching data for {Schema.label}")
39
39
  return client.get_phones()
40
40
 
41
41
 
42
42
  @timeit
43
43
  def _transform(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
44
- '''
44
+ """
45
45
  Reformat the data before loading
46
- '''
47
- logger.info(f'Transforming {len(data)} items for {Schema.label}')
46
+ """
47
+ logger.info(f"Transforming {len(data)} items for {Schema.label}")
48
48
  transformed_data = []
49
49
  for datum in data:
50
50
  transformed_datum = {
51
- 'activated': datum['activated'],
52
- 'capabilities': datum['capabilities'],
53
- 'encrypted': datum['encrypted'],
54
- 'extension': datum['extension'],
55
- 'fingerprint': datum['fingerprint'],
56
- 'last_seen': datum['last_seen'],
57
- 'model': datum['model'],
58
- 'name': datum['name'],
59
- 'phone_id': datum['phone_id'],
60
- 'platform': datum['platform'],
61
- 'postdelay': datum['postdelay'],
62
- 'predelay': datum['predelay'],
63
- 'screenlock': datum['screenlock'],
64
- 'sms_passcodes_sent': datum['sms_passcodes_sent'],
65
- 'tampered': datum['tampered'],
66
- 'type': datum['type'],
51
+ "activated": datum["activated"],
52
+ "capabilities": datum["capabilities"],
53
+ "encrypted": datum["encrypted"],
54
+ "extension": datum["extension"],
55
+ "fingerprint": datum["fingerprint"],
56
+ "last_seen": datum["last_seen"],
57
+ "model": datum["model"],
58
+ "name": datum["name"],
59
+ "phone_id": datum["phone_id"],
60
+ "platform": datum["platform"],
61
+ "postdelay": datum["postdelay"],
62
+ "predelay": datum["predelay"],
63
+ "screenlock": datum["screenlock"],
64
+ "sms_passcodes_sent": datum["sms_passcodes_sent"],
65
+ "tampered": datum["tampered"],
66
+ "type": datum["type"],
67
67
  }
68
68
  transformed_data.append(transformed_datum)
69
- for user in datum['users']:
69
+ for user in datum["users"]:
70
70
  match_datum = {
71
71
  **transformed_datum,
72
- 'user_id': user['user_id'],
72
+ "user_id": user["user_id"],
73
73
  }
74
74
  transformed_data.append(match_datum)
75
75
  return transformed_data
@@ -81,22 +81,25 @@ def _load(
81
81
  data: List[Dict[str, Any]],
82
82
  common_job_parameters: Dict[str, Any],
83
83
  ) -> None:
84
- '''
84
+ """
85
85
  Load the data into the database
86
- '''
87
- logger.info(f'Loading {len(data)} items for {Schema.label}')
86
+ """
87
+ logger.info(f"Loading {len(data)} items for {Schema.label}")
88
88
  load(
89
89
  neo4j_session,
90
90
  Schema(),
91
91
  data,
92
- DUO_API_HOSTNAME=common_job_parameters['DUO_API_HOSTNAME'],
93
- lastupdated=common_job_parameters['UPDATE_TAG'],
92
+ DUO_API_HOSTNAME=common_job_parameters["DUO_API_HOSTNAME"],
93
+ lastupdated=common_job_parameters["UPDATE_TAG"],
94
94
  )
95
95
 
96
96
 
97
97
  @timeit
98
- def _cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
99
- '''
98
+ def _cleanup(
99
+ neo4j_session: neo4j.Session,
100
+ common_job_parameters: Dict[str, Any],
101
+ ) -> None:
102
+ """
100
103
  Cleanup nodes
101
- '''
104
+ """
102
105
  GraphJob.from_node_schema(Schema(), common_job_parameters).run(neo4j_session)
@@ -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())