cartography 0.102.0rc1__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.
- cartography/__main__.py +1 -2
- cartography/_version.py +2 -2
- cartography/cli.py +376 -249
- cartography/client/core/tx.py +39 -18
- cartography/config.py +28 -0
- cartography/driftdetect/__main__.py +1 -2
- cartography/driftdetect/add_shortcut.py +10 -2
- cartography/driftdetect/cli.py +71 -75
- cartography/driftdetect/detect_deviations.py +7 -3
- cartography/driftdetect/get_states.py +20 -8
- cartography/driftdetect/model.py +5 -5
- cartography/driftdetect/serializers.py +8 -6
- cartography/driftdetect/storage.py +2 -2
- cartography/graph/cleanupbuilder.py +35 -15
- cartography/graph/job.py +46 -17
- cartography/graph/querybuilder.py +165 -80
- cartography/graph/statement.py +35 -26
- cartography/intel/analysis.py +4 -1
- cartography/intel/aws/__init__.py +114 -55
- cartography/intel/aws/apigateway.py +134 -63
- cartography/intel/aws/cloudtrail.py +127 -0
- cartography/intel/aws/cloudwatch.py +93 -0
- cartography/intel/aws/config.py +56 -20
- cartography/intel/aws/dynamodb.py +108 -40
- cartography/intel/aws/ec2/__init__.py +2 -2
- cartography/intel/aws/ec2/auto_scaling_groups.py +181 -78
- cartography/intel/aws/ec2/elastic_ip_addresses.py +41 -13
- cartography/intel/aws/ec2/images.py +49 -20
- cartography/intel/aws/ec2/instances.py +234 -136
- cartography/intel/aws/ec2/internet_gateways.py +40 -11
- cartography/intel/aws/ec2/key_pairs.py +44 -20
- cartography/intel/aws/ec2/launch_templates.py +101 -59
- cartography/intel/aws/ec2/load_balancer_v2s.py +104 -39
- cartography/intel/aws/ec2/load_balancers.py +82 -42
- cartography/intel/aws/ec2/network_acls.py +89 -65
- cartography/intel/aws/ec2/network_interfaces.py +146 -87
- cartography/intel/aws/ec2/reserved_instances.py +45 -16
- cartography/intel/aws/ec2/route_tables.py +327 -0
- cartography/intel/aws/ec2/security_groups.py +71 -21
- cartography/intel/aws/ec2/snapshots.py +61 -22
- cartography/intel/aws/ec2/subnets.py +54 -18
- cartography/intel/aws/ec2/tgw.py +100 -34
- cartography/intel/aws/ec2/util.py +1 -1
- cartography/intel/aws/ec2/volumes.py +69 -41
- cartography/intel/aws/ec2/vpc.py +37 -12
- cartography/intel/aws/ec2/vpc_peerings.py +83 -24
- cartography/intel/aws/ecr.py +88 -32
- cartography/intel/aws/ecs.py +83 -47
- cartography/intel/aws/efs.py +93 -0
- cartography/intel/aws/eks.py +55 -29
- cartography/intel/aws/elasticache.py +42 -18
- cartography/intel/aws/elasticsearch.py +57 -20
- cartography/intel/aws/emr.py +61 -23
- cartography/intel/aws/iam.py +401 -145
- cartography/intel/aws/iam_instance_profiles.py +22 -22
- cartography/intel/aws/identitycenter.py +71 -37
- cartography/intel/aws/inspector.py +159 -89
- cartography/intel/aws/kms.py +92 -38
- cartography/intel/aws/lambda_function.py +103 -34
- cartography/intel/aws/organizations.py +30 -10
- cartography/intel/aws/permission_relationships.py +133 -51
- cartography/intel/aws/rds.py +249 -85
- cartography/intel/aws/redshift.py +107 -46
- cartography/intel/aws/resourcegroupstaggingapi.py +120 -66
- cartography/intel/aws/resources.py +57 -44
- cartography/intel/aws/route53.py +108 -61
- cartography/intel/aws/s3.py +168 -83
- cartography/intel/aws/s3accountpublicaccessblock.py +157 -0
- cartography/intel/aws/secretsmanager.py +24 -12
- cartography/intel/aws/securityhub.py +20 -9
- cartography/intel/aws/sns.py +166 -0
- cartography/intel/aws/sqs.py +60 -28
- cartography/intel/aws/ssm.py +70 -30
- cartography/intel/aws/util/arns.py +7 -7
- cartography/intel/aws/util/common.py +31 -4
- cartography/intel/azure/__init__.py +78 -19
- cartography/intel/azure/compute.py +101 -27
- cartography/intel/azure/cosmosdb.py +496 -170
- cartography/intel/azure/sql.py +296 -105
- cartography/intel/azure/storage.py +322 -113
- cartography/intel/azure/subscription.py +39 -23
- cartography/intel/azure/tenant.py +13 -4
- cartography/intel/azure/util/credentials.py +95 -55
- cartography/intel/bigfix/__init__.py +2 -2
- cartography/intel/bigfix/computers.py +93 -65
- cartography/intel/cloudflare/__init__.py +74 -0
- cartography/intel/cloudflare/accounts.py +57 -0
- cartography/intel/cloudflare/dnsrecords.py +64 -0
- cartography/intel/cloudflare/members.py +75 -0
- cartography/intel/cloudflare/roles.py +65 -0
- cartography/intel/cloudflare/zones.py +64 -0
- cartography/intel/create_indexes.py +3 -2
- cartography/intel/crowdstrike/__init__.py +11 -9
- cartography/intel/crowdstrike/endpoints.py +5 -1
- cartography/intel/crowdstrike/spotlight.py +8 -3
- cartography/intel/cve/__init__.py +46 -13
- cartography/intel/cve/feed.py +48 -12
- cartography/intel/digitalocean/__init__.py +22 -13
- cartography/intel/digitalocean/compute.py +75 -108
- cartography/intel/digitalocean/management.py +44 -80
- cartography/intel/digitalocean/platform.py +48 -43
- cartography/intel/dns.py +36 -10
- cartography/intel/duo/__init__.py +21 -16
- cartography/intel/duo/api_host.py +14 -9
- cartography/intel/duo/endpoints.py +50 -45
- cartography/intel/duo/groups.py +18 -14
- cartography/intel/duo/phones.py +37 -34
- cartography/intel/duo/tokens.py +26 -23
- cartography/intel/duo/users.py +54 -50
- cartography/intel/duo/web_authn_credentials.py +30 -25
- cartography/intel/entra/__init__.py +25 -7
- cartography/intel/entra/ou.py +112 -0
- cartography/intel/entra/users.py +69 -63
- cartography/intel/gcp/__init__.py +185 -49
- cartography/intel/gcp/compute.py +418 -231
- cartography/intel/gcp/crm.py +96 -43
- cartography/intel/gcp/dns.py +60 -19
- cartography/intel/gcp/gke.py +72 -38
- cartography/intel/gcp/iam.py +61 -41
- cartography/intel/gcp/storage.py +84 -55
- cartography/intel/github/__init__.py +13 -11
- cartography/intel/github/repos.py +270 -137
- cartography/intel/github/teams.py +170 -88
- cartography/intel/github/users.py +70 -39
- cartography/intel/github/util.py +36 -34
- cartography/intel/gsuite/__init__.py +47 -26
- cartography/intel/gsuite/api.py +73 -30
- cartography/intel/jamf/__init__.py +19 -1
- cartography/intel/jamf/computers.py +30 -7
- cartography/intel/jamf/util.py +7 -2
- cartography/intel/kandji/__init__.py +6 -3
- cartography/intel/kandji/devices.py +14 -8
- cartography/intel/kubernetes/namespaces.py +7 -4
- cartography/intel/kubernetes/pods.py +7 -4
- cartography/intel/kubernetes/services.py +8 -4
- cartography/intel/lastpass/__init__.py +2 -2
- cartography/intel/lastpass/users.py +23 -12
- cartography/intel/oci/__init__.py +44 -11
- cartography/intel/oci/iam.py +134 -38
- cartography/intel/oci/organizations.py +13 -6
- cartography/intel/oci/utils.py +43 -20
- cartography/intel/okta/__init__.py +66 -15
- cartography/intel/okta/applications.py +42 -20
- cartography/intel/okta/awssaml.py +93 -33
- cartography/intel/okta/factors.py +16 -4
- cartography/intel/okta/groups.py +56 -29
- cartography/intel/okta/organization.py +5 -1
- cartography/intel/okta/origins.py +6 -2
- cartography/intel/okta/roles.py +15 -5
- cartography/intel/okta/users.py +20 -8
- cartography/intel/okta/utils.py +6 -4
- cartography/intel/openai/__init__.py +86 -0
- cartography/intel/openai/adminapikeys.py +90 -0
- cartography/intel/openai/apikeys.py +96 -0
- cartography/intel/openai/projects.py +94 -0
- cartography/intel/openai/serviceaccounts.py +82 -0
- cartography/intel/openai/users.py +78 -0
- cartography/intel/openai/util.py +29 -0
- cartography/intel/pagerduty/__init__.py +8 -7
- cartography/intel/pagerduty/escalation_policies.py +18 -6
- cartography/intel/pagerduty/schedules.py +12 -4
- cartography/intel/pagerduty/services.py +11 -4
- cartography/intel/pagerduty/teams.py +8 -3
- cartography/intel/pagerduty/users.py +3 -1
- cartography/intel/pagerduty/vendors.py +3 -1
- cartography/intel/semgrep/__init__.py +24 -6
- cartography/intel/semgrep/dependencies.py +50 -28
- cartography/intel/semgrep/deployment.py +3 -1
- cartography/intel/semgrep/findings.py +42 -18
- cartography/intel/snipeit/__init__.py +17 -3
- cartography/intel/snipeit/asset.py +12 -6
- cartography/intel/snipeit/user.py +8 -5
- cartography/intel/snipeit/util.py +9 -4
- cartography/intel/tailscale/__init__.py +77 -0
- cartography/intel/tailscale/acls.py +146 -0
- cartography/intel/tailscale/devices.py +127 -0
- cartography/intel/tailscale/postureintegrations.py +81 -0
- cartography/intel/tailscale/tailnets.py +76 -0
- cartography/intel/tailscale/users.py +80 -0
- cartography/intel/tailscale/utils.py +132 -0
- cartography/models/aws/apigateway.py +21 -17
- cartography/models/aws/apigatewaycertificate.py +28 -22
- cartography/models/aws/apigatewayresource.py +28 -20
- cartography/models/aws/apigatewaystage.py +33 -25
- cartography/models/aws/cloudtrail/__init__.py +0 -0
- cartography/models/aws/cloudtrail/trail.py +61 -0
- cartography/models/aws/cloudwatch/__init__.py +0 -0
- cartography/models/aws/cloudwatch/loggroup.py +52 -0
- cartography/models/aws/dynamodb/gsi.py +30 -22
- cartography/models/aws/dynamodb/tables.py +25 -17
- cartography/models/aws/ec2/auto_scaling_groups.py +102 -82
- cartography/models/aws/ec2/images.py +36 -34
- cartography/models/aws/ec2/instances.py +51 -45
- cartography/models/aws/ec2/keypair.py +21 -16
- cartography/models/aws/ec2/keypair_instance.py +28 -21
- cartography/models/aws/ec2/launch_configurations.py +30 -26
- cartography/models/aws/ec2/launch_template_versions.py +48 -38
- cartography/models/aws/ec2/launch_templates.py +21 -17
- cartography/models/aws/ec2/load_balancer_listeners.py +27 -23
- cartography/models/aws/ec2/load_balancers.py +47 -37
- cartography/models/aws/ec2/network_acl_rules.py +38 -30
- cartography/models/aws/ec2/network_acls.py +38 -29
- cartography/models/aws/ec2/networkinterface_instance.py +52 -39
- cartography/models/aws/ec2/networkinterfaces.py +53 -37
- cartography/models/aws/ec2/privateip_networkinterface.py +32 -22
- cartography/models/aws/ec2/reservations.py +18 -14
- cartography/models/aws/ec2/route_table_associations.py +97 -0
- cartography/models/aws/ec2/route_tables.py +128 -0
- cartography/models/aws/ec2/routes.py +85 -0
- cartography/models/aws/ec2/securitygroup_instance.py +29 -20
- cartography/models/aws/ec2/securitygroup_networkinterface.py +24 -15
- cartography/models/aws/ec2/subnet_instance.py +24 -19
- cartography/models/aws/ec2/subnet_networkinterface.py +40 -31
- cartography/models/aws/ec2/volumes.py +47 -40
- cartography/models/aws/efs/__init__.py +0 -0
- cartography/models/aws/efs/mount_target.py +52 -0
- cartography/models/aws/eks/clusters.py +23 -21
- cartography/models/aws/emr.py +32 -30
- cartography/models/aws/iam/instanceprofile.py +33 -24
- cartography/models/aws/identitycenter/awsidentitycenter.py +18 -14
- cartography/models/aws/identitycenter/awspermissionset.py +37 -29
- cartography/models/aws/identitycenter/awsssouser.py +23 -21
- cartography/models/aws/inspector/findings.py +77 -65
- cartography/models/aws/inspector/packages.py +35 -29
- cartography/models/aws/s3/__init__.py +0 -0
- cartography/models/aws/s3/account_public_access_block.py +51 -0
- cartography/models/aws/sns/__init__.py +0 -0
- cartography/models/aws/sns/topic.py +50 -0
- cartography/models/aws/ssm/instance_information.py +51 -39
- cartography/models/aws/ssm/instance_patch.py +32 -26
- cartography/models/bigfix/bigfix_computer.py +42 -38
- cartography/models/bigfix/bigfix_root.py +3 -3
- cartography/models/cloudflare/__init__.py +0 -0
- cartography/models/cloudflare/account.py +25 -0
- cartography/models/cloudflare/dnsrecord.py +55 -0
- cartography/models/cloudflare/member.py +82 -0
- cartography/models/cloudflare/role.py +44 -0
- cartography/models/cloudflare/zone.py +59 -0
- cartography/models/core/common.py +12 -10
- cartography/models/core/nodes.py +5 -2
- cartography/models/core/relationships.py +14 -6
- cartography/models/crowdstrike/hosts.py +37 -35
- cartography/models/cve/cve.py +34 -32
- cartography/models/cve/cve_feed.py +6 -6
- cartography/models/digitalocean/__init__.py +0 -0
- cartography/models/digitalocean/account.py +21 -0
- cartography/models/digitalocean/droplet.py +56 -0
- cartography/models/digitalocean/project.py +48 -0
- cartography/models/duo/api_host.py +3 -3
- cartography/models/duo/endpoint.py +43 -41
- cartography/models/duo/group.py +14 -14
- cartography/models/duo/phone.py +27 -27
- cartography/models/duo/token.py +16 -16
- cartography/models/duo/user.py +46 -44
- cartography/models/duo/web_authn_credential.py +27 -19
- cartography/models/entra/ou.py +48 -0
- cartography/models/entra/tenant.py +24 -18
- cartography/models/entra/user.py +64 -48
- cartography/models/gcp/iam.py +23 -23
- cartography/models/github/orgs.py +5 -4
- cartography/models/github/teams.py +37 -31
- cartography/models/github/users.py +34 -23
- cartography/models/kandji/device.py +22 -16
- cartography/models/kandji/tenant.py +6 -4
- cartography/models/lastpass/tenant.py +3 -3
- cartography/models/lastpass/user.py +32 -28
- cartography/models/openai/__init__.py +0 -0
- cartography/models/openai/adminapikey.py +90 -0
- cartography/models/openai/apikey.py +84 -0
- cartography/models/openai/organization.py +17 -0
- cartography/models/openai/project.py +70 -0
- cartography/models/openai/serviceaccount.py +50 -0
- cartography/models/openai/user.py +49 -0
- cartography/models/semgrep/dependencies.py +36 -24
- cartography/models/semgrep/deployment.py +5 -5
- cartography/models/semgrep/findings.py +58 -42
- cartography/models/semgrep/locations.py +27 -21
- cartography/models/snipeit/asset.py +30 -21
- cartography/models/snipeit/tenant.py +6 -4
- cartography/models/snipeit/user.py +19 -12
- cartography/models/tailscale/__init__.py +0 -0
- cartography/models/tailscale/device.py +95 -0
- cartography/models/tailscale/group.py +86 -0
- cartography/models/tailscale/postureintegration.py +58 -0
- cartography/models/tailscale/tag.py +102 -0
- cartography/models/tailscale/tailnet.py +29 -0
- cartography/models/tailscale/user.py +52 -0
- cartography/stats.py +3 -3
- cartography/sync.py +113 -31
- cartography/util.py +84 -62
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0.dist-info}/METADATA +8 -15
- cartography-0.103.0.dist-info/RECORD +442 -0
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0.dist-info}/WHEEL +1 -1
- cartography-0.102.0rc1.dist-info/RECORD +0 -377
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0.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(
|
|
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(
|
|
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(
|
|
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[
|
|
34
|
-
updated_details = {**details,
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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[
|
|
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[
|
|
96
|
+
repo_permissions.append(edge["permission"])
|
|
92
97
|
|
|
93
98
|
|
|
94
99
|
@timeit
|
|
95
100
|
def _get_team_repos_for_multiple_teams(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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[
|
|
104
|
-
repo_count = team[
|
|
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] = [
|
|
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(
|
|
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
|
-
|
|
169
|
-
resource_inner_type=
|
|
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
|
-
|
|
177
|
-
|
|
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[
|
|
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[
|
|
203
|
+
user_roles.append(edge["role"])
|
|
188
204
|
|
|
189
205
|
|
|
190
206
|
def _get_team_users_for_multiple_teams(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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[
|
|
199
|
-
user_count = team[
|
|
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(
|
|
210
|
-
|
|
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] = [
|
|
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(
|
|
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
|
-
|
|
256
|
-
resource_inner_type=
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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[
|
|
283
|
-
team_count = team[
|
|
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(
|
|
293
|
-
|
|
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(
|
|
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
|
-
|
|
334
|
-
resource_inner_type=
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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[
|
|
400
|
+
team_name = team["slug"]
|
|
350
401
|
repo_info = {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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[
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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(
|
|
411
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
99
|
+
"membersWithRole",
|
|
100
100
|
)
|
|
101
101
|
return users.edges, org
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def get_enterprise_owners(
|
|
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
|
-
|
|
126
|
+
"enterpriseOwners",
|
|
123
127
|
)
|
|
124
128
|
return owners.edges, org
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
@timeit
|
|
128
|
-
def transform_users(
|
|
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[
|
|
149
|
-
processed_user[
|
|
150
|
-
processed_user[
|
|
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[
|
|
153
|
-
processed_user[
|
|
154
|
-
users_dict[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[
|
|
159
|
-
processed_owner[
|
|
160
|
-
if owner[
|
|
161
|
-
processed_owner[
|
|
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[
|
|
164
|
-
owners_dict[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[
|
|
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[
|
|
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(
|
|
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(
|
|
217
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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(
|
|
231
|
-
|
|
232
|
-
|
|
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,
|
|
236
|
-
|
|
258
|
+
neo4j_session,
|
|
259
|
+
GitHubOrganizationSchema(),
|
|
260
|
+
[org_data],
|
|
261
|
+
common_job_parameters["UPDATE_TAG"],
|
|
237
262
|
)
|
|
238
263
|
load_users(
|
|
239
|
-
neo4j_session,
|
|
240
|
-
|
|
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,
|
|
244
|
-
|
|
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=
|
|
251
|
-
group_id=org_data[
|
|
252
|
-
synced_type=
|
|
253
|
-
update_tag=common_job_parameters[
|
|
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
|
)
|