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
@@ -18,28 +18,33 @@ from cartography.util import timeit
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
20
  # A team's permission on a repo: https://docs.github.com/en/graphql/reference/enums#repositorypermission
21
- RepoPermission = namedtuple('RepoPermission', ['repo_url', 'permission'])
21
+ RepoPermission = namedtuple("RepoPermission", ["repo_url", "permission"])
22
22
  # A team member's role: https://docs.github.com/en/graphql/reference/enums#teammemberrole
23
- UserRole = namedtuple('UserRole', ['user_url', 'role'])
23
+ UserRole = namedtuple("UserRole", ["user_url", "role"])
24
24
  # Unlike the other tuples here, there is no qualification (like 'role' or 'permission') to the relationship.
25
25
  # A child team is just a child team: https://docs.github.com/en/graphql/reference/objects#teamconnection
26
- ChildTeam = namedtuple('ChildTeam', ['team_url'])
26
+ ChildTeam = namedtuple("ChildTeam", ["team_url"])
27
27
 
28
28
 
29
29
  def backoff_handler(details: Dict) -> None:
30
30
  """
31
31
  Custom backoff handler for GitHub calls in this module.
32
32
  """
33
- team_name = details['kwargs'].get('team_name') or 'not present in kwargs'
34
- updated_details = {**details, 'team_name': team_name}
33
+ team_name = details["kwargs"].get("team_name") or "not present in kwargs"
34
+ updated_details = {**details, "team_name": team_name}
35
35
  logger.warning(
36
- "Backing off {wait:0.1f} seconds after {tries} tries. Calling function {target} for team {team_name}"
37
- .format(**updated_details),
36
+ "Backing off {wait:0.1f} seconds after {tries} tries. Calling function {target} for team {team_name}".format(
37
+ **updated_details,
38
+ ),
38
39
  )
39
40
 
40
41
 
41
42
  @timeit
42
- def get_teams(org: str, api_url: str, token: str) -> Tuple[PaginatedGraphqlData, Dict[str, Any]]:
43
+ def get_teams(
44
+ org: str,
45
+ api_url: str,
46
+ token: str,
47
+ ) -> Tuple[PaginatedGraphqlData, Dict[str, Any]]:
43
48
  org_teams_gql = """
44
49
  query($login: String!, $cursor: String) {
45
50
  organization(login: $login) {
@@ -68,16 +73,16 @@ def get_teams(org: str, api_url: str, token: str) -> Tuple[PaginatedGraphqlData,
68
73
  }
69
74
  }
70
75
  """
71
- return fetch_all(token, api_url, org, org_teams_gql, 'teams')
76
+ return fetch_all(token, api_url, org, org_teams_gql, "teams")
72
77
 
73
78
 
74
79
  def _get_teams_repos_inner_func(
75
- org: str,
76
- api_url: str,
77
- token: str,
78
- team_name: str,
79
- repo_urls: list[str],
80
- repo_permissions: list[str],
80
+ org: str,
81
+ api_url: str,
82
+ token: str,
83
+ team_name: str,
84
+ repo_urls: list[str],
85
+ repo_permissions: list[str],
81
86
  ) -> None:
82
87
  logger.info(f"Loading team repos for {team_name}.")
83
88
  team_repos = _get_team_repos(org, api_url, token, team_name)
@@ -85,23 +90,23 @@ def _get_teams_repos_inner_func(
85
90
  # The `or []` is because `.nodes` can be None. See:
86
91
  # https://docs.github.com/en/graphql/reference/objects#teamrepositoryconnection
87
92
  for repo in team_repos.nodes or []:
88
- repo_urls.append(repo['url'])
93
+ repo_urls.append(repo["url"])
89
94
  # The `or []` is because `.edges` can be None.
90
95
  for edge in team_repos.edges or []:
91
- repo_permissions.append(edge['permission'])
96
+ repo_permissions.append(edge["permission"])
92
97
 
93
98
 
94
99
  @timeit
95
100
  def _get_team_repos_for_multiple_teams(
96
- team_raw_data: list[dict[str, Any]],
97
- org: str,
98
- api_url: str,
99
- token: str,
101
+ team_raw_data: list[dict[str, Any]],
102
+ org: str,
103
+ api_url: str,
104
+ token: str,
100
105
  ) -> dict[str, list[RepoPermission]]:
101
106
  result: dict[str, list[RepoPermission]] = {}
102
107
  for team in team_raw_data:
103
- team_name = team['slug']
104
- repo_count = team['repositories']['totalCount']
108
+ team_name = team["slug"]
109
+ repo_count = team["repositories"]["totalCount"]
105
110
 
106
111
  if repo_count == 0:
107
112
  # This team has access to no repos so let's move on
@@ -125,12 +130,19 @@ def _get_team_repos_for_multiple_teams(
125
130
  repo_permissions=repo_permissions,
126
131
  )
127
132
  # Shape = [(repo_url, 'WRITE'), ...]]
128
- result[team_name] = [RepoPermission(url, perm) for url, perm in zip(repo_urls, repo_permissions)]
133
+ result[team_name] = [
134
+ RepoPermission(url, perm) for url, perm in zip(repo_urls, repo_permissions)
135
+ ]
129
136
  return result
130
137
 
131
138
 
132
139
  @timeit
133
- def _get_team_repos(org: str, api_url: str, token: str, team: str) -> PaginatedGraphqlData:
140
+ def _get_team_repos(
141
+ org: str,
142
+ api_url: str,
143
+ token: str,
144
+ team: str,
145
+ ) -> PaginatedGraphqlData:
134
146
  team_repos_gql = """
135
147
  query($login: String!, $team: String!, $cursor: String) {
136
148
  organization(login: $login) {
@@ -165,38 +177,42 @@ def _get_team_repos(org: str, api_url: str, token: str, team: str) -> PaginatedG
165
177
  api_url,
166
178
  org,
167
179
  team_repos_gql,
168
- 'team',
169
- resource_inner_type='repositories',
180
+ "team",
181
+ resource_inner_type="repositories",
170
182
  team=team,
171
183
  )
172
184
  return team_repos
173
185
 
174
186
 
175
187
  def _get_teams_users_inner_func(
176
- org: str, api_url: str, token: str, team_name: str,
177
- user_urls: List[str], user_roles: List[str],
188
+ org: str,
189
+ api_url: str,
190
+ token: str,
191
+ team_name: str,
192
+ user_urls: List[str],
193
+ user_roles: List[str],
178
194
  ) -> None:
179
195
  logger.info(f"Loading team users for {team_name}.")
180
196
  team_users = _get_team_users(org, api_url, token, team_name)
181
197
  # The `or []` is because `.nodes` can be None. See:
182
198
  # https://docs.github.com/en/graphql/reference/objects#teammemberconnection
183
199
  for user in team_users.nodes or []:
184
- user_urls.append(user['url'])
200
+ user_urls.append(user["url"])
185
201
  # The `or []` is because `.edges` can be None.
186
202
  for edge in team_users.edges or []:
187
- user_roles.append(edge['role'])
203
+ user_roles.append(edge["role"])
188
204
 
189
205
 
190
206
  def _get_team_users_for_multiple_teams(
191
- team_raw_data: list[dict[str, Any]],
192
- org: str,
193
- api_url: str,
194
- token: str,
207
+ team_raw_data: list[dict[str, Any]],
208
+ org: str,
209
+ api_url: str,
210
+ token: str,
195
211
  ) -> dict[str, list[UserRole]]:
196
212
  result: dict[str, list[UserRole]] = {}
197
213
  for team in team_raw_data:
198
- team_name = team['slug']
199
- user_count = team['members']['totalCount']
214
+ team_name = team["slug"]
215
+ user_count = team["members"]["totalCount"]
200
216
 
201
217
  if user_count == 0:
202
218
  # This team has no users so let's move on
@@ -206,17 +222,34 @@ def _get_team_users_for_multiple_teams(
206
222
  user_urls: List[str] = []
207
223
  user_roles: List[str] = []
208
224
 
209
- retries_with_backoff(_get_teams_users_inner_func, TypeError, 5, backoff_handler)(
210
- org=org, api_url=api_url, token=token, team_name=team_name, user_urls=user_urls, user_roles=user_roles,
225
+ retries_with_backoff(
226
+ _get_teams_users_inner_func,
227
+ TypeError,
228
+ 5,
229
+ backoff_handler,
230
+ )(
231
+ org=org,
232
+ api_url=api_url,
233
+ token=token,
234
+ team_name=team_name,
235
+ user_urls=user_urls,
236
+ user_roles=user_roles,
211
237
  )
212
238
 
213
239
  # Shape = [(user_url, 'MAINTAINER'), ...]]
214
- result[team_name] = [UserRole(url, role) for url, role in zip(user_urls, user_roles)]
240
+ result[team_name] = [
241
+ UserRole(url, role) for url, role in zip(user_urls, user_roles)
242
+ ]
215
243
  return result
216
244
 
217
245
 
218
246
  @timeit
219
- def _get_team_users(org: str, api_url: str, token: str, team: str) -> PaginatedGraphqlData:
247
+ def _get_team_users(
248
+ org: str,
249
+ api_url: str,
250
+ token: str,
251
+ team: str,
252
+ ) -> PaginatedGraphqlData:
220
253
  team_users_gql = """
221
254
  query($login: String!, $team: String!, $cursor: String) {
222
255
  organization(login: $login) {
@@ -252,35 +285,39 @@ def _get_team_users(org: str, api_url: str, token: str, team: str) -> PaginatedG
252
285
  api_url,
253
286
  org,
254
287
  team_users_gql,
255
- 'team',
256
- resource_inner_type='members',
288
+ "team",
289
+ resource_inner_type="members",
257
290
  team=team,
258
291
  )
259
292
  return team_users
260
293
 
261
294
 
262
295
  def _get_child_teams_inner_func(
263
- org: str, api_url: str, token: str, team_name: str, team_urls: List[str],
296
+ org: str,
297
+ api_url: str,
298
+ token: str,
299
+ team_name: str,
300
+ team_urls: List[str],
264
301
  ) -> None:
265
302
  logger.info(f"Loading child teams for {team_name}.")
266
303
  child_teams = _get_child_teams(org, api_url, token, team_name)
267
304
  # The `or []` is because `.nodes` can be None. See:
268
305
  # https://docs.github.com/en/graphql/reference/objects#teammemberconnection
269
306
  for cteam in child_teams.nodes or []:
270
- team_urls.append(cteam['url'])
307
+ team_urls.append(cteam["url"])
271
308
  # No edges to process here, the GitHub response for child teams has no relevant info in edges.
272
309
 
273
310
 
274
311
  def _get_child_teams_for_multiple_teams(
275
- team_raw_data: list[dict[str, Any]],
276
- org: str,
277
- api_url: str,
278
- token: str,
312
+ team_raw_data: list[dict[str, Any]],
313
+ org: str,
314
+ api_url: str,
315
+ token: str,
279
316
  ) -> dict[str, list[ChildTeam]]:
280
317
  result: dict[str, list[ChildTeam]] = {}
281
318
  for team in team_raw_data:
282
- team_name = team['slug']
283
- team_count = team['childTeams']['totalCount']
319
+ team_name = team["slug"]
320
+ team_count = team["childTeams"]["totalCount"]
284
321
 
285
322
  if team_count == 0:
286
323
  # This team has no child teams so let's move on
@@ -289,15 +326,29 @@ def _get_child_teams_for_multiple_teams(
289
326
 
290
327
  team_urls: List[str] = []
291
328
 
292
- retries_with_backoff(_get_child_teams_inner_func, TypeError, 5, backoff_handler)(
293
- org=org, api_url=api_url, token=token, team_name=team_name, team_urls=team_urls,
329
+ retries_with_backoff(
330
+ _get_child_teams_inner_func,
331
+ TypeError,
332
+ 5,
333
+ backoff_handler,
334
+ )(
335
+ org=org,
336
+ api_url=api_url,
337
+ token=token,
338
+ team_name=team_name,
339
+ team_urls=team_urls,
294
340
  )
295
341
 
296
342
  result[team_name] = [ChildTeam(url) for url in team_urls]
297
343
  return result
298
344
 
299
345
 
300
- def _get_child_teams(org: str, api_url: str, token: str, team: str) -> PaginatedGraphqlData:
346
+ def _get_child_teams(
347
+ org: str,
348
+ api_url: str,
349
+ token: str,
350
+ team: str,
351
+ ) -> PaginatedGraphqlData:
301
352
  team_users_gql = """
302
353
  query($login: String!, $team: String!, $cursor: String) {
303
354
  organization(login: $login) {
@@ -330,32 +381,32 @@ def _get_child_teams(org: str, api_url: str, token: str, team: str) -> Paginated
330
381
  api_url,
331
382
  org,
332
383
  team_users_gql,
333
- 'team',
334
- resource_inner_type='childTeams',
384
+ "team",
385
+ resource_inner_type="childTeams",
335
386
  team=team,
336
387
  )
337
388
  return team_users
338
389
 
339
390
 
340
391
  def transform_teams(
341
- team_paginated_data: PaginatedGraphqlData,
342
- org_data: Dict[str, Any],
343
- team_repo_data: dict[str, list[RepoPermission]],
344
- team_user_data: dict[str, list[UserRole]],
345
- team_child_team_data: dict[str, list[ChildTeam]],
392
+ team_paginated_data: PaginatedGraphqlData,
393
+ org_data: Dict[str, Any],
394
+ team_repo_data: dict[str, list[RepoPermission]],
395
+ team_user_data: dict[str, list[UserRole]],
396
+ team_child_team_data: dict[str, list[ChildTeam]],
346
397
  ) -> list[dict[str, Any]]:
347
398
  result = []
348
399
  for team in team_paginated_data.nodes:
349
- team_name = team['slug']
400
+ team_name = team["slug"]
350
401
  repo_info = {
351
- 'name': team_name,
352
- 'url': team['url'],
353
- 'description': team['description'],
354
- 'repo_count': team['repositories']['totalCount'],
355
- 'member_count': team['members']['totalCount'],
356
- 'child_team_count': team['childTeams']['totalCount'],
357
- 'org_url': org_data['url'],
358
- 'org_login': org_data['login'],
402
+ "name": team_name,
403
+ "url": team["url"],
404
+ "description": team["description"],
405
+ "repo_count": team["repositories"]["totalCount"],
406
+ "member_count": team["members"]["totalCount"],
407
+ "child_team_count": team["childTeams"]["totalCount"],
408
+ "org_url": org_data["url"],
409
+ "org_login": org_data["login"],
359
410
  }
360
411
  repo_permissions = team_repo_data[team_name]
361
412
  user_roles = team_user_data[team_name]
@@ -384,17 +435,17 @@ def transform_teams(
384
435
  # or here: https://docs.github.com/en/graphql/reference/enums#teammembershiptype
385
436
  # We label the relationship as 'MEMBER_OF_TEAM' here because it is in line with
386
437
  # other similar relationships in Cartography.
387
- repo_info_copy['MEMBER_OF_TEAM'] = team_url
438
+ repo_info_copy["MEMBER_OF_TEAM"] = team_url
388
439
  result.append(repo_info_copy)
389
440
  return result
390
441
 
391
442
 
392
443
  @timeit
393
444
  def load_team_repos(
394
- neo4j_session: neo4j.Session,
395
- data: List[Dict[str, Any]],
396
- update_tag: int,
397
- organization_url: str,
445
+ neo4j_session: neo4j.Session,
446
+ data: List[Dict[str, Any]],
447
+ update_tag: int,
448
+ organization_url: str,
398
449
  ) -> None:
399
450
  logger.info(f"Loading {len(data)} GitHub team-repos to the graph")
400
451
  load(
@@ -407,23 +458,54 @@ def load_team_repos(
407
458
 
408
459
 
409
460
  @timeit
410
- def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
411
- GraphJob.from_node_schema(GitHubTeamSchema(), common_job_parameters).run(neo4j_session)
461
+ def cleanup(
462
+ neo4j_session: neo4j.Session,
463
+ common_job_parameters: Dict[str, Any],
464
+ ) -> None:
465
+ GraphJob.from_node_schema(GitHubTeamSchema(), common_job_parameters).run(
466
+ neo4j_session,
467
+ )
412
468
 
413
469
 
414
470
  @timeit
415
471
  def sync_github_teams(
416
- neo4j_session: neo4j.Session,
417
- common_job_parameters: Dict[str, Any],
418
- github_api_key: str,
419
- github_url: str,
420
- organization: str,
472
+ neo4j_session: neo4j.Session,
473
+ common_job_parameters: Dict[str, Any],
474
+ github_api_key: str,
475
+ github_url: str,
476
+ organization: str,
421
477
  ) -> None:
422
478
  teams_paginated, org_data = get_teams(organization, github_url, github_api_key)
423
- team_repos = _get_team_repos_for_multiple_teams(teams_paginated.nodes, organization, github_url, github_api_key)
424
- team_users = _get_team_users_for_multiple_teams(teams_paginated.nodes, organization, github_url, github_api_key)
425
- team_children = _get_child_teams_for_multiple_teams(teams_paginated.nodes, organization, github_url, github_api_key)
426
- processed_data = transform_teams(teams_paginated, org_data, team_repos, team_users, team_children)
427
- load_team_repos(neo4j_session, processed_data, common_job_parameters['UPDATE_TAG'], org_data['url'])
428
- common_job_parameters['org_url'] = org_data['url']
479
+ team_repos = _get_team_repos_for_multiple_teams(
480
+ teams_paginated.nodes,
481
+ organization,
482
+ github_url,
483
+ github_api_key,
484
+ )
485
+ team_users = _get_team_users_for_multiple_teams(
486
+ teams_paginated.nodes,
487
+ organization,
488
+ github_url,
489
+ github_api_key,
490
+ )
491
+ team_children = _get_child_teams_for_multiple_teams(
492
+ teams_paginated.nodes,
493
+ organization,
494
+ github_url,
495
+ github_api_key,
496
+ )
497
+ processed_data = transform_teams(
498
+ teams_paginated,
499
+ org_data,
500
+ team_repos,
501
+ team_users,
502
+ team_children,
503
+ )
504
+ load_team_repos(
505
+ neo4j_session,
506
+ processed_data,
507
+ common_job_parameters["UPDATE_TAG"],
508
+ org_data["url"],
509
+ )
510
+ common_job_parameters["org_url"] = org_data["url"]
429
511
  cleanup(neo4j_session, common_job_parameters)
@@ -96,12 +96,16 @@ def get_users(token: str, api_url: str, organization: str) -> Tuple[List[Dict],
96
96
  api_url,
97
97
  organization,
98
98
  GITHUB_ORG_USERS_PAGINATED_GRAPHQL,
99
- 'membersWithRole',
99
+ "membersWithRole",
100
100
  )
101
101
  return users.edges, org
102
102
 
103
103
 
104
- def get_enterprise_owners(token: str, api_url: str, organization: str) -> Tuple[List[Dict], Dict]:
104
+ def get_enterprise_owners(
105
+ token: str,
106
+ api_url: str,
107
+ organization: str,
108
+ ) -> Tuple[List[Dict], Dict]:
105
109
  """
106
110
  Retrieve a list of enterprise owners from the given GitHub organization as described in
107
111
  https://docs.github.com/en/graphql/reference/objects#organizationenterpriseowneredge.
@@ -119,13 +123,17 @@ def get_enterprise_owners(token: str, api_url: str, organization: str) -> Tuple[
119
123
  api_url,
120
124
  organization,
121
125
  GITHUB_ENTERPRISE_OWNER_USERS_PAGINATED_GRAPHQL,
122
- 'enterpriseOwners',
126
+ "enterpriseOwners",
123
127
  )
124
128
  return owners.edges, org
125
129
 
126
130
 
127
131
  @timeit
128
- def transform_users(user_data: List[Dict], owners_data: List[Dict], org_data: Dict) -> Tuple[List[Dict], List[Dict]]:
132
+ def transform_users(
133
+ user_data: List[Dict],
134
+ owners_data: List[Dict],
135
+ org_data: Dict,
136
+ ) -> Tuple[List[Dict], List[Dict]]:
129
137
  """
130
138
  Taking raw user and owner data, return two lists of processed user data:
131
139
  * organization users aka affiliated users (users directly affiliated with an organization)
@@ -145,27 +153,27 @@ def transform_users(user_data: List[Dict], owners_data: List[Dict], org_data: Di
145
153
  users_dict = {}
146
154
  for user in user_data:
147
155
  # all members get the 'MEMBER_OF' relationship
148
- processed_user = deepcopy(user['node'])
149
- processed_user['hasTwoFactorEnabled'] = user['hasTwoFactorEnabled']
150
- processed_user['MEMBER_OF'] = org_data['url']
156
+ processed_user = deepcopy(user["node"])
157
+ processed_user["hasTwoFactorEnabled"] = user["hasTwoFactorEnabled"]
158
+ processed_user["MEMBER_OF"] = org_data["url"]
151
159
  # admins get a second relationship expressing them as such
152
- if user['role'] == 'ADMIN':
153
- processed_user['ADMIN_OF'] = org_data['url']
154
- users_dict[processed_user['url']] = processed_user
160
+ if user["role"] == "ADMIN":
161
+ processed_user["ADMIN_OF"] = org_data["url"]
162
+ users_dict[processed_user["url"]] = processed_user
155
163
 
156
164
  owners_dict = {}
157
165
  for owner in owners_data:
158
- processed_owner = deepcopy(owner['node'])
159
- processed_owner['isEnterpriseOwner'] = True
160
- if owner['organizationRole'] == 'UNAFFILIATED':
161
- processed_owner['UNAFFILIATED'] = org_data['url']
166
+ processed_owner = deepcopy(owner["node"])
167
+ processed_owner["isEnterpriseOwner"] = True
168
+ if owner["organizationRole"] == "UNAFFILIATED":
169
+ processed_owner["UNAFFILIATED"] = org_data["url"]
162
170
  else:
163
- processed_owner['MEMBER_OF'] = org_data['url']
164
- owners_dict[processed_owner['url']] = processed_owner
171
+ processed_owner["MEMBER_OF"] = org_data["url"]
172
+ owners_dict[processed_owner["url"]] = processed_owner
165
173
 
166
174
  affiliated_users = [] # users affiliated with the target org
167
175
  for url, user in users_dict.items():
168
- user['isEnterpriseOwner'] = url in owners_dict
176
+ user["isEnterpriseOwner"] = url in owners_dict
169
177
  affiliated_users.append(user)
170
178
 
171
179
  unaffiliated_users = [] # users not affiliated with the target org
@@ -190,7 +198,7 @@ def load_users(
190
198
  node_schema,
191
199
  user_data,
192
200
  lastupdated=update_tag,
193
- org_url=org_data['url'],
201
+ org_url=org_data["url"],
194
202
  )
195
203
 
196
204
 
@@ -211,45 +219,68 @@ def load_organization(
211
219
 
212
220
 
213
221
  @timeit
214
- def cleanup(neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]) -> None:
222
+ def cleanup(
223
+ neo4j_session: neo4j.Session,
224
+ common_job_parameters: dict[str, Any],
225
+ ) -> None:
215
226
  logger.info("Cleaning up GitHub users")
216
- GraphJob.from_node_schema(GitHubOrganizationUserSchema(), common_job_parameters).run(neo4j_session)
217
- GraphJob.from_node_schema(GitHubUnaffiliatedUserSchema(), common_job_parameters).run(neo4j_session)
227
+ GraphJob.from_node_schema(
228
+ GitHubOrganizationUserSchema(),
229
+ common_job_parameters,
230
+ ).run(neo4j_session)
231
+ GraphJob.from_node_schema(
232
+ GitHubUnaffiliatedUserSchema(),
233
+ common_job_parameters,
234
+ ).run(neo4j_session)
218
235
 
219
236
 
220
237
  @timeit
221
238
  def sync(
222
- neo4j_session: neo4j.Session,
223
- common_job_parameters: Dict,
224
- github_api_key: str,
225
- github_url: str,
226
- organization: str,
239
+ neo4j_session: neo4j.Session,
240
+ common_job_parameters: Dict,
241
+ github_api_key: str,
242
+ github_url: str,
243
+ organization: str,
227
244
  ) -> None:
228
245
  logger.info("Syncing GitHub users")
229
246
  user_data, org_data = get_users(github_api_key, github_url, organization)
230
- owners_data, org_data = get_enterprise_owners(github_api_key, github_url, organization)
231
- processed_affiliated_user_data, processed_unaffiliated_user_data = (
232
- transform_users(user_data, owners_data, org_data)
247
+ owners_data, org_data = get_enterprise_owners(
248
+ github_api_key,
249
+ github_url,
250
+ organization,
251
+ )
252
+ processed_affiliated_user_data, processed_unaffiliated_user_data = transform_users(
253
+ user_data,
254
+ owners_data,
255
+ org_data,
233
256
  )
234
257
  load_organization(
235
- neo4j_session, GitHubOrganizationSchema(), [org_data],
236
- common_job_parameters['UPDATE_TAG'],
258
+ neo4j_session,
259
+ GitHubOrganizationSchema(),
260
+ [org_data],
261
+ common_job_parameters["UPDATE_TAG"],
237
262
  )
238
263
  load_users(
239
- neo4j_session, GitHubOrganizationUserSchema(), processed_affiliated_user_data, org_data,
240
- common_job_parameters['UPDATE_TAG'],
264
+ neo4j_session,
265
+ GitHubOrganizationUserSchema(),
266
+ processed_affiliated_user_data,
267
+ org_data,
268
+ common_job_parameters["UPDATE_TAG"],
241
269
  )
242
270
  load_users(
243
- neo4j_session, GitHubUnaffiliatedUserSchema(), processed_unaffiliated_user_data, org_data,
244
- common_job_parameters['UPDATE_TAG'],
271
+ neo4j_session,
272
+ GitHubUnaffiliatedUserSchema(),
273
+ processed_unaffiliated_user_data,
274
+ org_data,
275
+ common_job_parameters["UPDATE_TAG"],
245
276
  )
246
277
  cleanup(neo4j_session, common_job_parameters)
247
278
 
248
279
  merge_module_sync_metadata(
249
280
  neo4j_session,
250
- group_type='GitHubOrganization',
251
- group_id=org_data['url'],
252
- synced_type='GitHubOrganization',
253
- update_tag=common_job_parameters['UPDATE_TAG'],
281
+ group_type="GitHubOrganization",
282
+ group_id=org_data["url"],
283
+ synced_type="GitHubOrganization",
284
+ update_tag=common_job_parameters["UPDATE_TAG"],
254
285
  stat_handler=stat_handler,
255
286
  )