cartography 0.111.0__py3-none-any.whl → 0.112.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (40) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +11 -0
  3. cartography/config.py +8 -0
  4. cartography/data/indexes.cypher +0 -2
  5. cartography/intel/aws/apigateway.py +126 -17
  6. cartography/intel/aws/ec2/instances.py +3 -1
  7. cartography/intel/aws/ec2/network_interfaces.py +1 -1
  8. cartography/intel/aws/ec2/vpc_peerings.py +262 -125
  9. cartography/intel/azure/__init__.py +35 -32
  10. cartography/intel/azure/subscription.py +2 -2
  11. cartography/intel/azure/tenant.py +39 -30
  12. cartography/intel/azure/util/credentials.py +49 -174
  13. cartography/intel/entra/__init__.py +47 -1
  14. cartography/intel/entra/applications.py +220 -170
  15. cartography/intel/entra/groups.py +41 -22
  16. cartography/intel/entra/ou.py +28 -20
  17. cartography/intel/entra/users.py +24 -18
  18. cartography/intel/gcp/__init__.py +25 -8
  19. cartography/intel/gcp/compute.py +47 -12
  20. cartography/intel/kubernetes/__init__.py +26 -0
  21. cartography/intel/kubernetes/eks.py +402 -0
  22. cartography/intel/kubernetes/rbac.py +133 -0
  23. cartography/models/aws/apigateway/apigatewayintegration.py +79 -0
  24. cartography/models/aws/apigateway/apigatewaymethod.py +74 -0
  25. cartography/models/aws/ec2/vpc_peering.py +157 -0
  26. cartography/models/azure/principal.py +44 -0
  27. cartography/models/azure/tenant.py +20 -0
  28. cartography/models/kubernetes/clusterrolebindings.py +40 -0
  29. cartography/models/kubernetes/groups.py +107 -0
  30. cartography/models/kubernetes/oidc.py +51 -0
  31. cartography/models/kubernetes/rolebindings.py +40 -0
  32. cartography/models/kubernetes/users.py +105 -0
  33. cartography/util.py +2 -0
  34. {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/METADATA +8 -5
  35. {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/RECORD +39 -31
  36. cartography/data/jobs/cleanup/aws_import_vpc_peering_cleanup.json +0 -45
  37. {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/WHEEL +0 -0
  38. {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/entry_points.txt +0 -0
  39. {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/licenses/LICENSE +0 -0
  40. {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/top_level.txt +0 -0
@@ -1,58 +1,67 @@
1
1
  import logging
2
2
  from typing import Dict
3
+ from typing import Optional
3
4
 
4
5
  import neo4j
5
6
 
6
- from cartography.util import run_cleanup_job
7
- from cartography.util import timeit
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.models.azure.principal import AzurePrincipalSchema
8
10
 
9
- from .util.credentials import Credentials
11
+ # Import the new, separated schemas
12
+ from cartography.models.azure.tenant import AzureTenantSchema
13
+ from cartography.util import timeit
10
14
 
11
15
  logger = logging.getLogger(__name__)
12
16
 
13
17
 
14
- def get_tenant_id(credentials: Credentials) -> str:
15
- return credentials.get_tenant_id()
16
-
17
-
18
+ @timeit
18
19
  def load_azure_tenant(
19
20
  neo4j_session: neo4j.Session,
20
21
  tenant_id: str,
21
- current_user: str,
22
+ current_user: Optional[str],
22
23
  update_tag: int,
23
24
  ) -> None:
24
- query = """
25
- MERGE (at:AzureTenant{id: $TENANT_ID})
26
- ON CREATE SET at.firstseen = timestamp()
27
- SET at.lastupdated = $update_tag
28
- WITH at
29
- MERGE (ap:AzurePrincipal{id: $CURRENT_USER})
30
- ON CREATE SET ap.email = $CURRENT_USER, ap.firstseen = timestamp()
31
- SET ap.lastupdated = $update_tag
32
- WITH at, ap
33
- MERGE (at)-[r:RESOURCE]->(ap)
34
- ON CREATE SET r.firstseen = timestamp()
35
- SET r.lastupdated = $update_tag;
36
25
  """
37
- neo4j_session.run(
38
- query,
39
- TENANT_ID=tenant_id,
40
- CURRENT_USER=current_user,
41
- update_tag=update_tag,
42
- )
26
+ Ingest the Azure Tenant and, if available, the Azure Principal into Neo4j.
27
+ """
28
+ tenant_data = {"id": tenant_id}
29
+ load(neo4j_session, AzureTenantSchema(), [tenant_data], lastupdated=update_tag)
43
30
 
31
+ if current_user:
32
+ principal_data = {"id": current_user}
33
+ load(
34
+ neo4j_session,
35
+ AzurePrincipalSchema(),
36
+ [principal_data],
37
+ lastupdated=update_tag,
38
+ TENANT_ID=tenant_id,
39
+ )
44
40
 
45
- def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None:
46
- run_cleanup_job("azure_tenant_cleanup.json", neo4j_session, common_job_parameters)
41
+
42
+ @timeit
43
+ def cleanup_azure_tenant(
44
+ neo4j_session: neo4j.Session, common_job_parameters: Dict
45
+ ) -> None:
46
+ """
47
+ Delete stale Azure Tenants and Principals.
48
+ """
49
+ GraphJob.from_node_schema(AzureTenantSchema(), common_job_parameters).run(
50
+ neo4j_session
51
+ )
52
+ GraphJob.from_node_schema(AzurePrincipalSchema(), common_job_parameters).run(
53
+ neo4j_session
54
+ )
47
55
 
48
56
 
49
57
  @timeit
50
58
  def sync(
51
59
  neo4j_session: neo4j.Session,
52
60
  tenant_id: str,
53
- current_user: str,
61
+ current_user: Optional[str],
54
62
  update_tag: int,
55
63
  common_job_parameters: Dict,
56
64
  ) -> None:
65
+ logger.info("Syncing Azure tenant '%s'.", tenant_id)
57
66
  load_azure_tenant(neo4j_session, tenant_id, current_user, update_tag)
58
- cleanup(neo4j_session, common_job_parameters)
67
+ cleanup_azure_tenant(neo4j_session, common_job_parameters)
@@ -1,222 +1,97 @@
1
1
  import logging
2
- from datetime import datetime
3
- from datetime import timedelta
4
2
  from typing import Any
5
3
  from typing import Optional
6
4
 
7
- import adal
8
- import requests
9
- from azure.common.credentials import get_azure_cli_credentials
10
- from azure.common.credentials import get_cli_profile
11
- from azure.core.exceptions import HttpResponseError
5
+ import jwt
6
+ from azure.identity import AzureCliCredential
12
7
  from azure.identity import ClientSecretCredential
13
- from msrestazure.azure_active_directory import AADTokenCredentials
8
+ from azure.mgmt.resource import SubscriptionClient
14
9
 
15
10
  logger = logging.getLogger(__name__)
16
- AUTHORITY_HOST_URI = "https://login.microsoftonline.com"
11
+
12
+
13
+ def _get_tenant_id_from_token(credential: Any) -> str:
14
+ """
15
+ A helper function to get the tenant ID from the claims in an access token.
16
+ """
17
+ token = credential.get_token("https://management.azure.com/.default")
18
+ decoded_token = jwt.decode(token.token, options={"verify_signature": False})
19
+ return decoded_token.get("tid", "")
17
20
 
18
21
 
19
22
  class Credentials:
23
+ """
24
+ A simple data container for the credential object and its associated IDs.
25
+ """
20
26
 
21
27
  def __init__(
22
28
  self,
23
- arm_credentials: Any,
24
- aad_graph_credentials: Any,
29
+ credential: Any,
25
30
  tenant_id: Optional[str] = None,
26
31
  subscription_id: Optional[str] = None,
27
- context: Optional[adal.AuthenticationContext] = None,
28
- current_user: Optional[str] = None,
29
32
  ) -> None:
30
- self.arm_credentials = arm_credentials # Azure Resource Manager API credentials
31
- self.aad_graph_credentials = (
32
- aad_graph_credentials # Azure AD Graph API credentials
33
- )
33
+ self.credential = credential
34
34
  self.tenant_id = tenant_id
35
35
  self.subscription_id = subscription_id
36
- self.context = context
37
- self.current_user = current_user
38
-
39
- def get_current_user(self) -> Optional[str]:
40
- return self.current_user
41
-
42
- def get_tenant_id(self) -> Any:
43
- if self.tenant_id:
44
- return self.tenant_id
45
- elif "tenant_id" in self.aad_graph_credentials.token:
46
- return self.aad_graph_credentials.token["tenant_id"]
47
- else:
48
- # This is a last resort, e.g. for MSI authentication
49
- try:
50
- h = {
51
- "Authorization": "Bearer {}".format(
52
- self.arm_credentials.token["access_token"],
53
- ),
54
- }
55
- r = requests.get(
56
- "https://management.azure.com/tenants?api-version=2020-01-01",
57
- headers=h,
58
- )
59
- r2 = r.json()
60
- return r2.get("value")[0].get("tenantId")
61
- except requests.ConnectionError as e:
62
- logger.error(f"Unable to infer tenant ID: {e}")
63
- return None
64
-
65
- def get_credentials(self, resource: str) -> Any:
66
- if resource == "arm":
67
- self.arm_credentials = self.get_fresh_credentials(self.arm_credentials)
68
- return self.arm_credentials
69
- elif resource == "aad_graph":
70
- self.aad_graph_credentials = self.get_fresh_credentials(
71
- self.aad_graph_credentials,
72
- )
73
- return self.aad_graph_credentials
74
- else:
75
- raise Exception("Invalid credentials resource type")
76
-
77
- def get_fresh_credentials(self, credentials: Any) -> Any:
78
- """
79
- Check if credentials are outdated and if so refresh them.
80
- """
81
- if self.context and hasattr(credentials, "token"):
82
- expiration_datetime = datetime.fromtimestamp(
83
- credentials.token["expires_on"],
84
- )
85
- current_datetime = datetime.now()
86
- expiration_delta = expiration_datetime - current_datetime
87
- if expiration_delta < timedelta(minutes=5):
88
- return self.refresh_credential(credentials)
89
- return credentials
90
-
91
- def refresh_credential(self, credentials: Any) -> Any:
92
- """
93
- Refresh credentials
94
- """
95
- logger.debug("Refreshing credentials")
96
- authority_uri = AUTHORITY_HOST_URI + "/" + self.get_tenant_id()
97
- if self.context:
98
- existing_cache = self.context.cache
99
- context = adal.AuthenticationContext(authority_uri, cache=existing_cache)
100
-
101
- else:
102
- context = adal.AuthenticationContext(authority_uri)
103
-
104
- new_token = context.acquire_token(
105
- credentials.token["resource"],
106
- credentials.token["user_id"],
107
- credentials.token["_client_id"],
108
- )
109
-
110
- new_credentials = AADTokenCredentials(
111
- new_token,
112
- credentials.token.get("_client_id"),
113
- )
114
- return new_credentials
115
36
 
116
37
 
117
38
  class Authenticator:
118
39
 
119
- def authenticate_cli(self) -> Credentials:
40
+ def authenticate_cli(self) -> Optional[Credentials]:
120
41
  """
121
- Implements authentication for the Azure provider
42
+ Implements authentication using the Azure CLI with the modern library.
122
43
  """
44
+ logging.getLogger("urllib3").setLevel(logging.ERROR)
45
+ logging.getLogger(
46
+ "azure.core.pipeline.policies.http_logging_policy",
47
+ ).setLevel(logging.ERROR)
123
48
  try:
49
+ credential = AzureCliCredential()
124
50
 
125
- # Set logging level to error for libraries as otherwise generates a lot of warnings
126
- logging.getLogger("adal-python").setLevel(logging.ERROR)
127
- logging.getLogger("msrest").setLevel(logging.ERROR)
128
- logging.getLogger("msrestazure.azure_active_directory").setLevel(
129
- logging.ERROR,
130
- )
131
- logging.getLogger("urllib3").setLevel(logging.ERROR)
132
- logging.getLogger(
133
- "azure.core.pipeline.policies.http_logging_policy",
134
- ).setLevel(logging.ERROR)
51
+ subscription_client = SubscriptionClient(credential)
52
+ subscription = next(subscription_client.subscriptions.list())
53
+ subscription_id = subscription.subscription_id
135
54
 
136
- arm_credentials, subscription_id, tenant_id = get_azure_cli_credentials(
137
- with_tenant=True,
138
- )
139
- aad_graph_credentials, placeholder_1, placeholder_2 = (
140
- get_azure_cli_credentials(
141
- with_tenant=True,
142
- resource="https://graph.windows.net",
143
- )
144
- )
145
-
146
- profile = get_cli_profile()
55
+ tenant_id = _get_tenant_id_from_token(credential)
147
56
 
148
57
  return Credentials(
149
- arm_credentials,
150
- aad_graph_credentials,
58
+ credential=credential,
151
59
  tenant_id=tenant_id,
152
- current_user=profile.get_current_account_user(),
153
60
  subscription_id=subscription_id,
154
61
  )
155
-
156
- except HttpResponseError as e:
157
- if (
158
- ", AdalError: Unsupported wstrust endpoint version. "
159
- "Current supported version is wstrust2005 or wstrust13." in e.args
160
- ):
161
- logger.error(
162
- f"You are likely authenticating with a Microsoft Account. \
163
- This authentication mode only supports Azure Active Directory principal authentication.\
164
- {e}",
165
- )
166
-
167
- raise e
62
+ except Exception as e:
63
+ logger.error(
64
+ f"Failed to authenticate with Azure CLI. Have you run 'az login'? Details: {e}"
65
+ )
66
+ return None
168
67
 
169
68
  def authenticate_sp(
170
69
  self,
171
70
  tenant_id: Optional[str] = None,
172
71
  client_id: Optional[str] = None,
173
72
  client_secret: Optional[str] = None,
174
- ) -> Credentials:
73
+ subscription_id: Optional[str] = None,
74
+ ) -> Optional[Credentials]:
175
75
  """
176
- Implements authentication for the Azure provider
76
+ Implements authentication using a Service Principal with the modern library.
177
77
  """
178
78
  try:
179
-
180
- # Set logging level to error for libraries as otherwise generates a lot of warnings
181
- logging.getLogger("adal-python").setLevel(logging.ERROR)
182
- logging.getLogger("msrest").setLevel(logging.ERROR)
183
- logging.getLogger("msrestazure.azure_active_directory").setLevel(
184
- logging.ERROR,
185
- )
186
- logging.getLogger("urllib3").setLevel(logging.ERROR)
187
- logging.getLogger(
188
- "azure.core.pipeline.policies.http_logging_policy",
189
- ).setLevel(logging.ERROR)
190
-
191
- arm_credentials = ClientSecretCredential(
192
- client_id=client_id,
193
- client_secret=client_secret,
194
- tenant_id=tenant_id,
195
- )
196
-
197
- aad_graph_credentials = ClientSecretCredential(
79
+ credential = ClientSecretCredential(
198
80
  client_id=client_id,
199
81
  client_secret=client_secret,
200
82
  tenant_id=tenant_id,
201
- resource="https://graph.windows.net",
202
83
  )
203
-
204
84
  return Credentials(
205
- arm_credentials,
206
- aad_graph_credentials,
85
+ credential=credential,
207
86
  tenant_id=tenant_id,
208
- current_user=client_id,
87
+ subscription_id=subscription_id,
209
88
  )
210
-
211
- except HttpResponseError as e:
212
- if (
213
- ", AdalError: Unsupported wstrust endpoint version. "
214
- "Current supported version is wstrust2005 or wstrust13." in e.args
215
- ):
216
- logger.error(
217
- f"You are likely authenticating with a Microsoft Account. \
218
- This authentication mode only supports Azure Active Directory principal authentication.\
219
- {e}",
220
- )
221
-
222
- raise e
89
+ except Exception as e:
90
+ logger.error(
91
+ (
92
+ "Failed to authenticate with Service Principal. "
93
+ "Please ensure the tenant ID, client ID, and client secret are correct. Details: %s"
94
+ ),
95
+ e,
96
+ )
97
+ return None
@@ -2,17 +2,54 @@ import asyncio
2
2
  import logging
3
3
 
4
4
  import neo4j
5
+ from azure.identity import ClientSecretCredential
6
+ from msgraph import GraphServiceClient
5
7
 
6
8
  from cartography.config import Config
7
9
  from cartography.intel.entra.applications import sync_entra_applications
8
10
  from cartography.intel.entra.groups import sync_entra_groups
9
11
  from cartography.intel.entra.ou import sync_entra_ous
12
+ from cartography.intel.entra.users import get_tenant
13
+ from cartography.intel.entra.users import load_tenant
10
14
  from cartography.intel.entra.users import sync_entra_users
15
+ from cartography.intel.entra.users import transform_tenant
11
16
  from cartography.util import timeit
12
17
 
13
18
  logger = logging.getLogger(__name__)
14
19
 
15
20
 
21
+ @timeit
22
+ async def sync_tenant(
23
+ neo4j_session: neo4j.Session,
24
+ tenant_id: str,
25
+ client_id: str,
26
+ client_secret: str,
27
+ update_tag: int,
28
+ ) -> None:
29
+ """
30
+ Sync tenant information as a prerequisite for all other Entra resource syncs.
31
+
32
+ :param neo4j_session: Neo4j session
33
+ :param tenant_id: Entra tenant ID
34
+ :param client_id: Azure application client ID
35
+ :param client_secret: Azure application client secret
36
+ :param update_tag: Update tag for tracking data freshness
37
+ """
38
+ credential = ClientSecretCredential(
39
+ tenant_id=tenant_id,
40
+ client_id=client_id,
41
+ client_secret=client_secret,
42
+ )
43
+ client = GraphServiceClient(
44
+ credential, scopes=["https://graph.microsoft.com/.default"]
45
+ )
46
+
47
+ # Fetch tenant and load it
48
+ tenant = await get_tenant(client)
49
+ transformed_tenant = transform_tenant(tenant, tenant_id)
50
+ load_tenant(neo4j_session, transformed_tenant, update_tag)
51
+
52
+
16
53
  @timeit
17
54
  def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
18
55
  """
@@ -39,6 +76,15 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
39
76
  }
40
77
 
41
78
  async def main() -> None:
79
+ # Load tenant first as a prerequisite for all resource syncs
80
+ await sync_tenant(
81
+ neo4j_session,
82
+ config.entra_tenant_id,
83
+ config.entra_client_id,
84
+ config.entra_client_secret,
85
+ config.update_tag,
86
+ )
87
+
42
88
  # Run user sync
43
89
  await sync_entra_users(
44
90
  neo4j_session,
@@ -79,5 +125,5 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
79
125
  common_job_parameters,
80
126
  )
81
127
 
82
- # Execute both syncs in sequence
128
+ # Execute syncs in sequence
83
129
  asyncio.run(main())