cartography 0.113.0__py3-none-any.whl → 0.115.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/_version.py +2 -2
- cartography/cli.py +10 -2
- cartography/client/core/tx.py +11 -0
- cartography/config.py +4 -0
- cartography/data/indexes.cypher +0 -27
- cartography/intel/aws/config.py +7 -3
- cartography/intel/aws/ecr.py +9 -9
- cartography/intel/aws/iam.py +741 -492
- cartography/intel/aws/identitycenter.py +240 -13
- cartography/intel/aws/lambda_function.py +69 -2
- cartography/intel/aws/organizations.py +10 -9
- cartography/intel/aws/permission_relationships.py +7 -17
- cartography/intel/aws/redshift.py +9 -4
- cartography/intel/aws/route53.py +53 -3
- cartography/intel/aws/securityhub.py +3 -1
- cartography/intel/azure/__init__.py +24 -0
- cartography/intel/azure/app_service.py +105 -0
- cartography/intel/azure/functions.py +124 -0
- cartography/intel/azure/logic_apps.py +101 -0
- cartography/intel/create_indexes.py +2 -1
- cartography/intel/dns.py +5 -2
- cartography/intel/entra/__init__.py +31 -0
- cartography/intel/entra/app_role_assignments.py +277 -0
- cartography/intel/entra/applications.py +4 -238
- cartography/intel/entra/federation/__init__.py +0 -0
- cartography/intel/entra/federation/aws_identity_center.py +77 -0
- cartography/intel/entra/service_principals.py +217 -0
- cartography/intel/gcp/__init__.py +136 -440
- cartography/intel/gcp/clients.py +65 -0
- cartography/intel/gcp/compute.py +18 -44
- cartography/intel/gcp/crm/__init__.py +0 -0
- cartography/intel/gcp/crm/folders.py +108 -0
- cartography/intel/gcp/crm/orgs.py +65 -0
- cartography/intel/gcp/crm/projects.py +109 -0
- cartography/intel/gcp/dns.py +2 -1
- cartography/intel/gcp/gke.py +72 -113
- cartography/intel/github/__init__.py +41 -0
- cartography/intel/github/commits.py +423 -0
- cartography/intel/github/repos.py +76 -45
- cartography/intel/gsuite/api.py +17 -4
- cartography/intel/okta/applications.py +9 -4
- cartography/intel/okta/awssaml.py +5 -2
- cartography/intel/okta/factors.py +3 -1
- cartography/intel/okta/groups.py +5 -2
- cartography/intel/okta/organization.py +3 -1
- cartography/intel/okta/origins.py +3 -1
- cartography/intel/okta/roles.py +5 -2
- cartography/intel/okta/users.py +3 -1
- cartography/models/aws/iam/access_key.py +103 -0
- cartography/models/aws/iam/account_role.py +24 -0
- cartography/models/aws/iam/federated_principal.py +60 -0
- cartography/models/aws/iam/group.py +60 -0
- cartography/models/aws/iam/group_membership.py +26 -0
- cartography/models/aws/iam/inline_policy.py +78 -0
- cartography/models/aws/iam/managed_policy.py +51 -0
- cartography/models/aws/iam/policy_statement.py +57 -0
- cartography/models/aws/iam/role.py +83 -0
- cartography/models/aws/iam/root_principal.py +52 -0
- cartography/models/aws/iam/service_principal.py +30 -0
- cartography/models/aws/iam/sts_assumerole_allow.py +38 -0
- cartography/models/aws/iam/user.py +54 -0
- cartography/models/aws/identitycenter/awspermissionset.py +24 -1
- cartography/models/aws/identitycenter/awssogroup.py +70 -0
- cartography/models/aws/identitycenter/awsssouser.py +37 -1
- cartography/models/aws/lambda_function/lambda_function.py +2 -0
- cartography/models/azure/__init__.py +0 -0
- cartography/models/azure/app_service.py +59 -0
- cartography/models/azure/function_app.py +59 -0
- cartography/models/azure/logic_apps.py +56 -0
- cartography/models/entra/entra_user_to_aws_sso.py +41 -0
- cartography/models/entra/service_principal.py +104 -0
- cartography/models/entra/user.py +18 -0
- cartography/models/gcp/compute/subnet.py +74 -0
- cartography/models/gcp/crm/__init__.py +0 -0
- cartography/models/gcp/crm/folders.py +98 -0
- cartography/models/gcp/crm/organizations.py +21 -0
- cartography/models/gcp/crm/projects.py +100 -0
- cartography/models/gcp/gke.py +69 -0
- cartography/models/github/commits.py +63 -0
- {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/METADATA +8 -5
- {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/RECORD +85 -56
- cartography/data/jobs/cleanup/aws_import_account_access_key_cleanup.json +0 -17
- cartography/data/jobs/cleanup/aws_import_groups_cleanup.json +0 -13
- cartography/data/jobs/cleanup/aws_import_principals_cleanup.json +0 -30
- cartography/data/jobs/cleanup/aws_import_roles_cleanup.json +0 -13
- cartography/data/jobs/cleanup/aws_import_users_cleanup.json +0 -8
- cartography/data/jobs/cleanup/gcp_compute_vpc_subnet_cleanup.json +0 -35
- cartography/data/jobs/cleanup/gcp_crm_folder_cleanup.json +0 -23
- cartography/data/jobs/cleanup/gcp_crm_organization_cleanup.json +0 -17
- cartography/data/jobs/cleanup/gcp_crm_project_cleanup.json +0 -23
- cartography/data/jobs/cleanup/gcp_gke_cluster_cleanup.json +0 -17
- cartography/intel/gcp/crm.py +0 -355
- {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/WHEEL +0 -0
- {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/top_level.txt +0 -0
cartography/intel/aws/route53.py
CHANGED
|
@@ -24,6 +24,7 @@ DnsData = namedtuple(
|
|
|
24
24
|
[
|
|
25
25
|
"zones",
|
|
26
26
|
"a_records",
|
|
27
|
+
"aaaa_records",
|
|
27
28
|
"alias_records",
|
|
28
29
|
"cname_records",
|
|
29
30
|
"ns_records",
|
|
@@ -73,7 +74,7 @@ def get_zones(
|
|
|
73
74
|
def transform_record_set(
|
|
74
75
|
record_set: dict[str, Any], zone_id: str, name: str
|
|
75
76
|
) -> dict[str, Any] | None:
|
|
76
|
-
# process CNAME, ALIAS and
|
|
77
|
+
# process CNAME, ALIAS, A, and AAAA records
|
|
77
78
|
if record_set["Type"] == "CNAME":
|
|
78
79
|
if "AliasTarget" in record_set:
|
|
79
80
|
# this is a weighted CNAME record
|
|
@@ -127,6 +128,31 @@ def transform_record_set(
|
|
|
127
128
|
"value": value,
|
|
128
129
|
"id": _create_dns_record_id(zone_id, name, "A"),
|
|
129
130
|
}
|
|
131
|
+
elif record_set["Type"] == "AAAA":
|
|
132
|
+
if "AliasTarget" in record_set:
|
|
133
|
+
# AAAA alias records follow the same pattern as A aliases but map to IPv6 targets
|
|
134
|
+
value = record_set["AliasTarget"]["DNSName"]
|
|
135
|
+
if value.endswith("."):
|
|
136
|
+
value = value[:-1]
|
|
137
|
+
return {
|
|
138
|
+
"name": name,
|
|
139
|
+
"type": "ALIAS",
|
|
140
|
+
"zoneid": zone_id,
|
|
141
|
+
"value": value,
|
|
142
|
+
"id": _create_dns_record_id(zone_id, name, "ALIAS_AAAA"),
|
|
143
|
+
}
|
|
144
|
+
else:
|
|
145
|
+
ip_addresses = [record["Value"] for record in record_set["ResourceRecords"]]
|
|
146
|
+
value = ",".join(ip_addresses)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"name": name,
|
|
150
|
+
"type": "AAAA",
|
|
151
|
+
"zoneid": zone_id,
|
|
152
|
+
"ip_addresses": ip_addresses,
|
|
153
|
+
"value": value,
|
|
154
|
+
"id": _create_dns_record_id(zone_id, name, "AAAA"),
|
|
155
|
+
}
|
|
130
156
|
# This should never happen since we only call this for A and CNAME records,
|
|
131
157
|
# but we'll log it and return None.
|
|
132
158
|
logger.warning(f"Unsupported record type: {record_set['Type']}")
|
|
@@ -179,10 +205,11 @@ def transform_all_dns_data(
|
|
|
179
205
|
) -> DnsData:
|
|
180
206
|
"""
|
|
181
207
|
Transform all DNS data into flat lists for loading.
|
|
182
|
-
Returns: (zones, a_records, alias_records, cname_records, ns_records)
|
|
208
|
+
Returns: (zones, a_records, aaaa_records, alias_records, cname_records, ns_records)
|
|
183
209
|
"""
|
|
184
210
|
transformed_zones = []
|
|
185
211
|
all_a_records = []
|
|
212
|
+
all_aaaa_records = []
|
|
186
213
|
all_alias_records = []
|
|
187
214
|
all_cname_records = []
|
|
188
215
|
all_ns_records = []
|
|
@@ -196,7 +223,7 @@ def transform_all_dns_data(
|
|
|
196
223
|
zone_name = parsed_zone["name"]
|
|
197
224
|
|
|
198
225
|
for rs in zone_record_sets:
|
|
199
|
-
if rs["Type"]
|
|
226
|
+
if rs["Type"] in {"A", "AAAA", "CNAME"}:
|
|
200
227
|
transformed_rs = transform_record_set(
|
|
201
228
|
rs,
|
|
202
229
|
zone_id,
|
|
@@ -209,6 +236,8 @@ def transform_all_dns_data(
|
|
|
209
236
|
all_a_records.append(transformed_rs)
|
|
210
237
|
# TODO consider creating IPs as a first-class node from here.
|
|
211
238
|
# Right now we just match on them from the A record.
|
|
239
|
+
elif transformed_rs["type"] == "AAAA":
|
|
240
|
+
all_aaaa_records.append(transformed_rs)
|
|
212
241
|
elif transformed_rs["type"] == "ALIAS":
|
|
213
242
|
all_alias_records.append(transformed_rs)
|
|
214
243
|
elif transformed_rs["type"] == "CNAME":
|
|
@@ -232,6 +261,7 @@ def transform_all_dns_data(
|
|
|
232
261
|
return DnsData(
|
|
233
262
|
zones=transformed_zones,
|
|
234
263
|
a_records=all_a_records,
|
|
264
|
+
aaaa_records=all_aaaa_records,
|
|
235
265
|
alias_records=all_alias_records,
|
|
236
266
|
cname_records=all_cname_records,
|
|
237
267
|
ns_records=all_ns_records,
|
|
@@ -244,6 +274,7 @@ def _load_dns_details_flat(
|
|
|
244
274
|
neo4j_session: neo4j.Session,
|
|
245
275
|
zones: list[dict[str, Any]],
|
|
246
276
|
a_records: list[dict[str, Any]],
|
|
277
|
+
aaaa_records: list[dict[str, Any]],
|
|
247
278
|
alias_records: list[dict[str, Any]],
|
|
248
279
|
cname_records: list[dict[str, Any]],
|
|
249
280
|
ns_records: list[dict[str, Any]],
|
|
@@ -253,6 +284,7 @@ def _load_dns_details_flat(
|
|
|
253
284
|
) -> None:
|
|
254
285
|
load_zones(neo4j_session, zones, current_aws_id, update_tag)
|
|
255
286
|
load_a_records(neo4j_session, a_records, update_tag, current_aws_id)
|
|
287
|
+
load_aaaa_records(neo4j_session, aaaa_records, update_tag, current_aws_id)
|
|
256
288
|
load_alias_records(neo4j_session, alias_records, update_tag, current_aws_id)
|
|
257
289
|
load_cname_records(neo4j_session, cname_records, update_tag, current_aws_id)
|
|
258
290
|
load_name_servers(neo4j_session, name_servers, update_tag, current_aws_id)
|
|
@@ -274,6 +306,7 @@ def load_dns_details(
|
|
|
274
306
|
neo4j_session,
|
|
275
307
|
transformed_data.zones,
|
|
276
308
|
transformed_data.a_records,
|
|
309
|
+
transformed_data.aaaa_records,
|
|
277
310
|
transformed_data.alias_records,
|
|
278
311
|
transformed_data.cname_records,
|
|
279
312
|
transformed_data.ns_records,
|
|
@@ -299,6 +332,22 @@ def load_a_records(
|
|
|
299
332
|
)
|
|
300
333
|
|
|
301
334
|
|
|
335
|
+
@timeit
|
|
336
|
+
def load_aaaa_records(
|
|
337
|
+
neo4j_session: neo4j.Session,
|
|
338
|
+
records: list[dict[str, Any]],
|
|
339
|
+
update_tag: int,
|
|
340
|
+
current_aws_id: str,
|
|
341
|
+
) -> None:
|
|
342
|
+
load(
|
|
343
|
+
neo4j_session,
|
|
344
|
+
AWSDNSRecordSchema(),
|
|
345
|
+
records,
|
|
346
|
+
lastupdated=update_tag,
|
|
347
|
+
AWS_ID=current_aws_id,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
302
351
|
@timeit
|
|
303
352
|
def load_alias_records(
|
|
304
353
|
neo4j_session: neo4j.Session,
|
|
@@ -468,6 +517,7 @@ def sync(
|
|
|
468
517
|
neo4j_session,
|
|
469
518
|
transformed_data.zones,
|
|
470
519
|
transformed_data.a_records,
|
|
520
|
+
transformed_data.aaaa_records,
|
|
471
521
|
transformed_data.alias_records,
|
|
472
522
|
transformed_data.cname_records,
|
|
473
523
|
transformed_data.ns_records,
|
|
@@ -6,6 +6,7 @@ import boto3
|
|
|
6
6
|
import neo4j
|
|
7
7
|
from dateutil import parser
|
|
8
8
|
|
|
9
|
+
from cartography.client.core.tx import run_write_query
|
|
9
10
|
from cartography.util import run_cleanup_job
|
|
10
11
|
from cartography.util import timeit
|
|
11
12
|
|
|
@@ -50,7 +51,8 @@ def load_hub(
|
|
|
50
51
|
ON CREATE SET r.firstseen = timestamp()
|
|
51
52
|
SET r.lastupdated = $aws_update_tag
|
|
52
53
|
"""
|
|
53
|
-
|
|
54
|
+
run_write_query(
|
|
55
|
+
neo4j_session,
|
|
54
56
|
ingest_hub,
|
|
55
57
|
Hub=data,
|
|
56
58
|
AWS_ACCOUNT_ID=current_aws_account_id,
|
|
@@ -7,8 +7,11 @@ import neo4j
|
|
|
7
7
|
from cartography.config import Config
|
|
8
8
|
from cartography.util import timeit
|
|
9
9
|
|
|
10
|
+
from . import app_service
|
|
10
11
|
from . import compute
|
|
11
12
|
from . import cosmosdb
|
|
13
|
+
from . import functions
|
|
14
|
+
from . import logic_apps
|
|
12
15
|
from . import sql
|
|
13
16
|
from . import storage
|
|
14
17
|
from . import subscription
|
|
@@ -40,6 +43,27 @@ def _sync_one_subscription(
|
|
|
40
43
|
update_tag,
|
|
41
44
|
common_job_parameters,
|
|
42
45
|
)
|
|
46
|
+
app_service.sync(
|
|
47
|
+
neo4j_session,
|
|
48
|
+
credentials,
|
|
49
|
+
subscription_id,
|
|
50
|
+
update_tag,
|
|
51
|
+
common_job_parameters,
|
|
52
|
+
)
|
|
53
|
+
functions.sync(
|
|
54
|
+
neo4j_session,
|
|
55
|
+
credentials,
|
|
56
|
+
subscription_id,
|
|
57
|
+
update_tag,
|
|
58
|
+
common_job_parameters,
|
|
59
|
+
)
|
|
60
|
+
logic_apps.sync(
|
|
61
|
+
neo4j_session,
|
|
62
|
+
credentials,
|
|
63
|
+
subscription_id,
|
|
64
|
+
update_tag,
|
|
65
|
+
common_job_parameters,
|
|
66
|
+
)
|
|
43
67
|
sql.sync(
|
|
44
68
|
neo4j_session,
|
|
45
69
|
credentials.credential,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import neo4j
|
|
7
|
+
from azure.core.exceptions import ClientAuthenticationError
|
|
8
|
+
from azure.core.exceptions import HttpResponseError
|
|
9
|
+
from azure.mgmt.web import WebSiteManagementClient
|
|
10
|
+
|
|
11
|
+
from cartography.client.core.tx import load
|
|
12
|
+
from cartography.graph.job import GraphJob
|
|
13
|
+
from cartography.models.azure.app_service import AzureAppServiceSchema
|
|
14
|
+
from cartography.util import timeit
|
|
15
|
+
|
|
16
|
+
from .util.credentials import Credentials
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@timeit
|
|
22
|
+
def get_app_services(credentials: Credentials, subscription_id: str) -> List[Dict]:
|
|
23
|
+
"""
|
|
24
|
+
Get a list of App Services from the given Azure subscription.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
client = WebSiteManagementClient(credentials.credential, subscription_id)
|
|
28
|
+
# NOTE: This is the same API call as Functions. We get all web apps
|
|
29
|
+
# and then filter them in the transform stage.
|
|
30
|
+
return [app.as_dict() for app in client.web_apps.list()]
|
|
31
|
+
except (ClientAuthenticationError, HttpResponseError) as e:
|
|
32
|
+
logger.warning(
|
|
33
|
+
f"Failed to get app services for subscription {subscription_id}: {str(e)}"
|
|
34
|
+
)
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@timeit
|
|
39
|
+
def transform_app_services(app_services_response: List[Dict]) -> List[Dict]:
|
|
40
|
+
"""
|
|
41
|
+
Transform the raw API response to the dictionary structure that the model expects.
|
|
42
|
+
"""
|
|
43
|
+
transformed_apps: List[Dict[str, Any]] = []
|
|
44
|
+
for app in app_services_response:
|
|
45
|
+
if "functionapp" not in app.get("kind", ""):
|
|
46
|
+
transformed_app = {
|
|
47
|
+
"id": app.get("id"),
|
|
48
|
+
"name": app.get("name"),
|
|
49
|
+
"kind": app.get("kind"),
|
|
50
|
+
"location": app.get("location"),
|
|
51
|
+
"state": app.get("state"),
|
|
52
|
+
"default_host_name": app.get("default_host_name"),
|
|
53
|
+
"https_only": app.get("https_only"),
|
|
54
|
+
}
|
|
55
|
+
transformed_apps.append(transformed_app)
|
|
56
|
+
return transformed_apps
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@timeit
|
|
60
|
+
def load_app_services(
|
|
61
|
+
neo4j_session: neo4j.Session,
|
|
62
|
+
data: List[Dict[str, Any]],
|
|
63
|
+
subscription_id: str,
|
|
64
|
+
update_tag: int,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Load the transformed Azure App Service data to Neo4j.
|
|
68
|
+
"""
|
|
69
|
+
load(
|
|
70
|
+
neo4j_session,
|
|
71
|
+
AzureAppServiceSchema(),
|
|
72
|
+
data,
|
|
73
|
+
lastupdated=update_tag,
|
|
74
|
+
AZURE_SUBSCRIPTION_ID=subscription_id,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@timeit
|
|
79
|
+
def cleanup_app_services(
|
|
80
|
+
neo4j_session: neo4j.Session, common_job_parameters: Dict
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Run the cleanup job for Azure App Services.
|
|
84
|
+
"""
|
|
85
|
+
GraphJob.from_node_schema(AzureAppServiceSchema(), common_job_parameters).run(
|
|
86
|
+
neo4j_session
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@timeit
|
|
91
|
+
def sync(
|
|
92
|
+
neo4j_session: neo4j.Session,
|
|
93
|
+
credentials: Credentials,
|
|
94
|
+
subscription_id: str,
|
|
95
|
+
update_tag: int,
|
|
96
|
+
common_job_parameters: Dict,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""
|
|
99
|
+
The main sync function for Azure App Services.
|
|
100
|
+
"""
|
|
101
|
+
logger.info(f"Syncing Azure App Services for subscription {subscription_id}.")
|
|
102
|
+
raw_apps = get_app_services(credentials, subscription_id)
|
|
103
|
+
transformed_apps = transform_app_services(raw_apps)
|
|
104
|
+
load_app_services(neo4j_session, transformed_apps, subscription_id, update_tag)
|
|
105
|
+
cleanup_app_services(neo4j_session, common_job_parameters)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import neo4j
|
|
7
|
+
from azure.core.exceptions import ClientAuthenticationError
|
|
8
|
+
from azure.core.exceptions import HttpResponseError
|
|
9
|
+
from azure.mgmt.web import WebSiteManagementClient
|
|
10
|
+
|
|
11
|
+
from cartography.client.core.tx import load
|
|
12
|
+
from cartography.graph.job import GraphJob
|
|
13
|
+
from cartography.models.azure.function_app import AzureFunctionAppSchema
|
|
14
|
+
from cartography.util import timeit
|
|
15
|
+
|
|
16
|
+
from .util.credentials import Credentials
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@timeit
|
|
22
|
+
def get_function_apps(credentials: Credentials, subscription_id: str) -> List[Dict]:
|
|
23
|
+
"""
|
|
24
|
+
Get a list of Function Apps from the given Azure subscription.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
client = WebSiteManagementClient(credentials.credential, subscription_id)
|
|
28
|
+
# Note: Function Apps are a type of Web App, so we list all web apps
|
|
29
|
+
# and then filter them in the transform stage.
|
|
30
|
+
return [app.as_dict() for app in client.web_apps.list()]
|
|
31
|
+
|
|
32
|
+
except ClientAuthenticationError as e:
|
|
33
|
+
logger.warning(
|
|
34
|
+
(
|
|
35
|
+
"Failed to authenticate to get function apps for subscription '%s'. "
|
|
36
|
+
"Please check your credentials. Error: %s"
|
|
37
|
+
),
|
|
38
|
+
subscription_id,
|
|
39
|
+
e,
|
|
40
|
+
)
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
except HttpResponseError as e:
|
|
44
|
+
logger.warning(
|
|
45
|
+
(
|
|
46
|
+
"Failed to get function apps for subscription '%s' due to an API error. "
|
|
47
|
+
"Status code: %s. Message: %s"
|
|
48
|
+
),
|
|
49
|
+
subscription_id,
|
|
50
|
+
e.status_code,
|
|
51
|
+
str(e),
|
|
52
|
+
)
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@timeit
|
|
57
|
+
def transform_function_apps(function_apps_response: List[Dict]) -> List[Dict]:
|
|
58
|
+
"""
|
|
59
|
+
Transform the raw API response to the dictionary structure that the model expects.
|
|
60
|
+
"""
|
|
61
|
+
transformed_apps: List[Dict[str, Any]] = []
|
|
62
|
+
for app in function_apps_response:
|
|
63
|
+
# We only want to ingest resources that are explicitly function apps.
|
|
64
|
+
if "functionapp" in app.get("kind", ""):
|
|
65
|
+
transformed_app = {
|
|
66
|
+
"id": app.get("id"),
|
|
67
|
+
"name": app.get("name"),
|
|
68
|
+
"kind": app.get("kind"),
|
|
69
|
+
"location": app.get("location"),
|
|
70
|
+
"state": app.get("state"),
|
|
71
|
+
"default_host_name": app.get("default_host_name"),
|
|
72
|
+
"https_only": app.get("https_only"),
|
|
73
|
+
}
|
|
74
|
+
transformed_apps.append(transformed_app)
|
|
75
|
+
return transformed_apps
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@timeit
|
|
79
|
+
def load_function_apps(
|
|
80
|
+
neo4j_session: neo4j.Session,
|
|
81
|
+
data: List[Dict[str, Any]],
|
|
82
|
+
subscription_id: str,
|
|
83
|
+
update_tag: int,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Load the transformed Azure Function App data to Neo4j.
|
|
87
|
+
"""
|
|
88
|
+
load(
|
|
89
|
+
neo4j_session,
|
|
90
|
+
AzureFunctionAppSchema(),
|
|
91
|
+
data,
|
|
92
|
+
lastupdated=update_tag,
|
|
93
|
+
AZURE_SUBSCRIPTION_ID=subscription_id,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@timeit
|
|
98
|
+
def cleanup_function_apps(
|
|
99
|
+
neo4j_session: neo4j.Session, common_job_parameters: Dict
|
|
100
|
+
) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Run the cleanup job for Azure Function Apps.
|
|
103
|
+
"""
|
|
104
|
+
GraphJob.from_node_schema(AzureFunctionAppSchema(), common_job_parameters).run(
|
|
105
|
+
neo4j_session
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@timeit
|
|
110
|
+
def sync(
|
|
111
|
+
neo4j_session: neo4j.Session,
|
|
112
|
+
credentials: Credentials,
|
|
113
|
+
subscription_id: str,
|
|
114
|
+
update_tag: int,
|
|
115
|
+
common_job_parameters: Dict,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""
|
|
118
|
+
The main sync function for Azure Function Apps.
|
|
119
|
+
"""
|
|
120
|
+
logger.info(f"Syncing Azure Function Apps for subscription {subscription_id}.")
|
|
121
|
+
raw_apps = get_function_apps(credentials, subscription_id)
|
|
122
|
+
transformed_apps = transform_function_apps(raw_apps)
|
|
123
|
+
load_function_apps(neo4j_session, transformed_apps, subscription_id, update_tag)
|
|
124
|
+
cleanup_function_apps(neo4j_session, common_job_parameters)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
from azure.core.exceptions import ClientAuthenticationError
|
|
6
|
+
from azure.core.exceptions import HttpResponseError
|
|
7
|
+
from azure.mgmt.logic import LogicManagementClient
|
|
8
|
+
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.graph.job import GraphJob
|
|
11
|
+
from cartography.models.azure.logic_apps import AzureLogicAppSchema
|
|
12
|
+
from cartography.util import timeit
|
|
13
|
+
|
|
14
|
+
from .util.credentials import Credentials
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@timeit
|
|
20
|
+
def get_logic_apps(credentials: Credentials, subscription_id: str) -> list[dict]:
|
|
21
|
+
"""
|
|
22
|
+
Get a list of Logic Apps from the given Azure subscription.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
client = LogicManagementClient(credentials.credential, subscription_id)
|
|
26
|
+
# NOTE: The resource for a Logic App is called a "Workflow" in the SDK.
|
|
27
|
+
return [w.as_dict() for w in client.workflows.list_by_subscription()]
|
|
28
|
+
except (ClientAuthenticationError, HttpResponseError) as e:
|
|
29
|
+
logger.warning(
|
|
30
|
+
f"Failed to get logic apps for subscription {subscription_id}: {str(e)}"
|
|
31
|
+
)
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def transform_logic_apps(logic_apps_response: list[dict]) -> list[dict]:
|
|
36
|
+
"""
|
|
37
|
+
Transform the raw API response to the dictionary structure that the model expects.
|
|
38
|
+
"""
|
|
39
|
+
transformed_apps: list[dict[str, Any]] = []
|
|
40
|
+
for app in logic_apps_response:
|
|
41
|
+
transformed_app = {
|
|
42
|
+
"id": app.get("id"),
|
|
43
|
+
"name": app.get("name"),
|
|
44
|
+
"location": app.get("location"),
|
|
45
|
+
"state": app.get("properties", {}).get("state"),
|
|
46
|
+
"created_time": app.get("properties", {}).get("created_time"),
|
|
47
|
+
"changed_time": app.get("properties", {}).get("changed_time"),
|
|
48
|
+
"version": app.get("properties", {}).get("version"),
|
|
49
|
+
"access_endpoint": app.get("properties", {}).get("access_endpoint"),
|
|
50
|
+
}
|
|
51
|
+
transformed_apps.append(transformed_app)
|
|
52
|
+
return transformed_apps
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@timeit
|
|
56
|
+
def load_logic_apps(
|
|
57
|
+
neo4j_session: neo4j.Session,
|
|
58
|
+
data: list[dict[str, Any]],
|
|
59
|
+
subscription_id: str,
|
|
60
|
+
update_tag: int,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Load the transformed Azure Logic App data to Neo4j.
|
|
64
|
+
"""
|
|
65
|
+
load(
|
|
66
|
+
neo4j_session,
|
|
67
|
+
AzureLogicAppSchema(),
|
|
68
|
+
data,
|
|
69
|
+
lastupdated=update_tag,
|
|
70
|
+
AZURE_SUBSCRIPTION_ID=subscription_id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@timeit
|
|
75
|
+
def cleanup_logic_apps(
|
|
76
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Run the cleanup job for Azure Logic Apps.
|
|
80
|
+
"""
|
|
81
|
+
GraphJob.from_node_schema(AzureLogicAppSchema(), common_job_parameters).run(
|
|
82
|
+
neo4j_session
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@timeit
|
|
87
|
+
def sync(
|
|
88
|
+
neo4j_session: neo4j.Session,
|
|
89
|
+
credentials: Credentials,
|
|
90
|
+
subscription_id: str,
|
|
91
|
+
update_tag: int,
|
|
92
|
+
common_job_parameters: dict,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
The main sync function for Azure Logic Apps.
|
|
96
|
+
"""
|
|
97
|
+
logger.info(f"Syncing Azure Logic Apps for subscription {subscription_id}.")
|
|
98
|
+
raw_apps = get_logic_apps(credentials, subscription_id)
|
|
99
|
+
transformed_apps = transform_logic_apps(raw_apps)
|
|
100
|
+
load_logic_apps(neo4j_session, transformed_apps, subscription_id, update_tag)
|
|
101
|
+
cleanup_logic_apps(neo4j_session, common_job_parameters)
|
|
@@ -3,6 +3,7 @@ from typing import List
|
|
|
3
3
|
|
|
4
4
|
import neo4j
|
|
5
5
|
|
|
6
|
+
from cartography.client.core.tx import run_write_query
|
|
6
7
|
from cartography.config import Config
|
|
7
8
|
from cartography.util import load_resource_binary
|
|
8
9
|
|
|
@@ -23,4 +24,4 @@ def run(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
23
24
|
logger.info("Creating indexes for cartography node types.")
|
|
24
25
|
for statement in get_index_statements():
|
|
25
26
|
logger.debug("Executing statement: %s", statement)
|
|
26
|
-
neo4j_session
|
|
27
|
+
run_write_query(neo4j_session, statement)
|
cartography/intel/dns.py
CHANGED
|
@@ -8,6 +8,7 @@ import dns.rdatatype
|
|
|
8
8
|
import dns.resolver
|
|
9
9
|
import neo4j
|
|
10
10
|
|
|
11
|
+
from cartography.client.core.tx import run_write_query
|
|
11
12
|
from cartography.util import timeit
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
@@ -104,7 +105,8 @@ def _link_ip_to_A_record(
|
|
|
104
105
|
SET r.lastupdated = $update_tag
|
|
105
106
|
"""
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
run_write_query(
|
|
109
|
+
neo4j_session,
|
|
108
110
|
ingest,
|
|
109
111
|
ParentId=parent_record,
|
|
110
112
|
IP_LIST=ip_list,
|
|
@@ -151,7 +153,8 @@ def ingest_dns_record(
|
|
|
151
153
|
|
|
152
154
|
record_id = f"{name}+{type}"
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
run_write_query(
|
|
157
|
+
neo4j_session,
|
|
155
158
|
template.safe_substitute(
|
|
156
159
|
record_label=record_label,
|
|
157
160
|
dns_node_additional_label=dns_node_additional_label,
|
|
@@ -6,9 +6,12 @@ from azure.identity import ClientSecretCredential
|
|
|
6
6
|
from msgraph import GraphServiceClient
|
|
7
7
|
|
|
8
8
|
from cartography.config import Config
|
|
9
|
+
from cartography.intel.entra.app_role_assignments import sync_app_role_assignments
|
|
9
10
|
from cartography.intel.entra.applications import sync_entra_applications
|
|
11
|
+
from cartography.intel.entra.federation.aws_identity_center import sync_entra_federation
|
|
10
12
|
from cartography.intel.entra.groups import sync_entra_groups
|
|
11
13
|
from cartography.intel.entra.ou import sync_entra_ous
|
|
14
|
+
from cartography.intel.entra.service_principals import sync_service_principals
|
|
12
15
|
from cartography.intel.entra.users import get_tenant
|
|
13
16
|
from cartography.intel.entra.users import load_tenant
|
|
14
17
|
from cartography.intel.entra.users import sync_entra_users
|
|
@@ -125,5 +128,33 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
125
128
|
common_job_parameters,
|
|
126
129
|
)
|
|
127
130
|
|
|
131
|
+
# Run service principals sync
|
|
132
|
+
await sync_service_principals(
|
|
133
|
+
neo4j_session,
|
|
134
|
+
config.entra_tenant_id,
|
|
135
|
+
config.entra_client_id,
|
|
136
|
+
config.entra_client_secret,
|
|
137
|
+
config.update_tag,
|
|
138
|
+
common_job_parameters,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Run app role assignments sync
|
|
142
|
+
await sync_app_role_assignments(
|
|
143
|
+
neo4j_session,
|
|
144
|
+
config.entra_tenant_id,
|
|
145
|
+
config.entra_client_id,
|
|
146
|
+
config.entra_client_secret,
|
|
147
|
+
config.update_tag,
|
|
148
|
+
common_job_parameters,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Run federation sync (after all resources are synced)
|
|
152
|
+
await sync_entra_federation(
|
|
153
|
+
neo4j_session,
|
|
154
|
+
config.update_tag,
|
|
155
|
+
config.entra_tenant_id,
|
|
156
|
+
common_job_parameters,
|
|
157
|
+
)
|
|
158
|
+
|
|
128
159
|
# Execute syncs in sequence
|
|
129
160
|
asyncio.run(main())
|