cartography 0.114.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 +2 -2
- cartography/client/core/tx.py +11 -0
- cartography/intel/aws/config.py +7 -3
- cartography/intel/aws/ecr.py +9 -9
- cartography/intel/aws/identitycenter.py +240 -13
- cartography/intel/aws/lambda_function.py +69 -2
- cartography/intel/aws/organizations.py +3 -1
- cartography/intel/aws/permission_relationships.py +3 -1
- 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 +8 -0
- cartography/intel/azure/logic_apps.py +101 -0
- cartography/intel/create_indexes.py +2 -1
- cartography/intel/dns.py +5 -2
- cartography/intel/gcp/dns.py +2 -1
- cartography/intel/github/repos.py +3 -6
- 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/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/logic_apps.py +56 -0
- cartography/models/entra/user.py +18 -0
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/METADATA +3 -2
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/RECORD +39 -36
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/WHEEL +0 -0
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.114.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,
|
|
@@ -11,6 +11,7 @@ from . import app_service
|
|
|
11
11
|
from . import compute
|
|
12
12
|
from . import cosmosdb
|
|
13
13
|
from . import functions
|
|
14
|
+
from . import logic_apps
|
|
14
15
|
from . import sql
|
|
15
16
|
from . import storage
|
|
16
17
|
from . import subscription
|
|
@@ -56,6 +57,13 @@ def _sync_one_subscription(
|
|
|
56
57
|
update_tag,
|
|
57
58
|
common_job_parameters,
|
|
58
59
|
)
|
|
60
|
+
logic_apps.sync(
|
|
61
|
+
neo4j_session,
|
|
62
|
+
credentials,
|
|
63
|
+
subscription_id,
|
|
64
|
+
update_tag,
|
|
65
|
+
common_job_parameters,
|
|
66
|
+
)
|
|
59
67
|
sql.sync(
|
|
60
68
|
neo4j_session,
|
|
61
69
|
credentials.credential,
|
|
@@ -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,
|
cartography/intel/gcp/dns.py
CHANGED
|
@@ -116,7 +116,8 @@ def transform_dns_rrs(dns_rrs: List[Dict]) -> List[Dict]:
|
|
|
116
116
|
for r in dns_rrs:
|
|
117
117
|
records.append(
|
|
118
118
|
{
|
|
119
|
-
|
|
119
|
+
# Compose a unique ID to avoid collisions across types and zones
|
|
120
|
+
"id": f"{r['name']}|{r.get('type')}|{r.get('zone')}",
|
|
120
121
|
"name": r["name"],
|
|
121
122
|
"type": r.get("type"),
|
|
122
123
|
"ttl": r.get("ttl"),
|
|
@@ -159,8 +159,6 @@ def _get_repo_collaborators_inner_func(
|
|
|
159
159
|
token: str,
|
|
160
160
|
repo_raw_data: list[dict[str, Any]],
|
|
161
161
|
affiliation: str,
|
|
162
|
-
collab_users: list[dict[str, Any]],
|
|
163
|
-
collab_permission: list[str],
|
|
164
162
|
) -> dict[str, list[UserAffiliationAndRepoPermission]]:
|
|
165
163
|
result: dict[str, list[UserAffiliationAndRepoPermission]] = {}
|
|
166
164
|
|
|
@@ -194,6 +192,9 @@ def _get_repo_collaborators_inner_func(
|
|
|
194
192
|
affiliation,
|
|
195
193
|
)
|
|
196
194
|
|
|
195
|
+
collab_users: List[dict[str, Any]] = []
|
|
196
|
+
collab_permission: List[str] = []
|
|
197
|
+
|
|
197
198
|
# nodes and edges are expected to always be present given that we only call for them if totalCount is > 0
|
|
198
199
|
# however sometimes GitHub returns None, as in issue 1334 and 1404.
|
|
199
200
|
for collab in collaborators.nodes or []:
|
|
@@ -230,8 +231,6 @@ def _get_repo_collaborators_for_multiple_repos(
|
|
|
230
231
|
logger.info(
|
|
231
232
|
f'Retrieving repo collaborators for affiliation "{affiliation}" on org "{org}".',
|
|
232
233
|
)
|
|
233
|
-
collab_users: List[dict[str, Any]] = []
|
|
234
|
-
collab_permission: List[str] = []
|
|
235
234
|
|
|
236
235
|
result: dict[str, list[UserAffiliationAndRepoPermission]] = retries_with_backoff(
|
|
237
236
|
_get_repo_collaborators_inner_func,
|
|
@@ -244,8 +243,6 @@ def _get_repo_collaborators_for_multiple_repos(
|
|
|
244
243
|
token=token,
|
|
245
244
|
repo_raw_data=repo_raw_data,
|
|
246
245
|
affiliation=affiliation,
|
|
247
|
-
collab_users=collab_users,
|
|
248
|
-
collab_permission=collab_permission,
|
|
249
246
|
)
|
|
250
247
|
return result
|
|
251
248
|
|
cartography/intel/gsuite/api.py
CHANGED
|
@@ -6,6 +6,7 @@ import neo4j
|
|
|
6
6
|
from googleapiclient.discovery import Resource
|
|
7
7
|
from googleapiclient.errors import HttpError
|
|
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
|
|
|
@@ -171,7 +172,12 @@ def load_gsuite_groups(
|
|
|
171
172
|
g.lastupdated = $UpdateTag
|
|
172
173
|
"""
|
|
173
174
|
logger.info(f"Ingesting {len(groups)} gsuite groups")
|
|
174
|
-
|
|
175
|
+
run_write_query(
|
|
176
|
+
neo4j_session,
|
|
177
|
+
ingestion_qry,
|
|
178
|
+
GroupData=groups,
|
|
179
|
+
UpdateTag=gsuite_update_tag,
|
|
180
|
+
)
|
|
175
181
|
|
|
176
182
|
|
|
177
183
|
@timeit
|
|
@@ -215,7 +221,12 @@ def load_gsuite_users(
|
|
|
215
221
|
u.lastupdated = $UpdateTag
|
|
216
222
|
"""
|
|
217
223
|
logger.info(f"Ingesting {len(users)} gsuite users")
|
|
218
|
-
|
|
224
|
+
run_write_query(
|
|
225
|
+
neo4j_session,
|
|
226
|
+
ingestion_qry,
|
|
227
|
+
UserData=users,
|
|
228
|
+
UpdateTag=gsuite_update_tag,
|
|
229
|
+
)
|
|
219
230
|
|
|
220
231
|
|
|
221
232
|
@timeit
|
|
@@ -234,7 +245,8 @@ def load_gsuite_members(
|
|
|
234
245
|
SET
|
|
235
246
|
r.lastupdated = $UpdateTag
|
|
236
247
|
"""
|
|
237
|
-
|
|
248
|
+
run_write_query(
|
|
249
|
+
neo4j_session,
|
|
238
250
|
ingestion_qry,
|
|
239
251
|
MemberData=members,
|
|
240
252
|
GroupID=group.get("id"),
|
|
@@ -249,7 +261,8 @@ def load_gsuite_members(
|
|
|
249
261
|
SET
|
|
250
262
|
r.lastupdated = $UpdateTag
|
|
251
263
|
"""
|
|
252
|
-
|
|
264
|
+
run_write_query(
|
|
265
|
+
neo4j_session,
|
|
253
266
|
membership_qry,
|
|
254
267
|
MemberData=members,
|
|
255
268
|
GroupID=group.get("id"),
|
|
@@ -10,6 +10,7 @@ import neo4j
|
|
|
10
10
|
from okta.framework.ApiClient import ApiClient
|
|
11
11
|
from okta.framework.OktaError import OktaError
|
|
12
12
|
|
|
13
|
+
from cartography.client.core.tx import run_write_query
|
|
13
14
|
from cartography.intel.okta.utils import check_rate_limit
|
|
14
15
|
from cartography.intel.okta.utils import create_api_client
|
|
15
16
|
from cartography.intel.okta.utils import is_last_page
|
|
@@ -293,7 +294,8 @@ def _load_okta_applications(
|
|
|
293
294
|
SET org_r.lastupdated = $okta_update_tag
|
|
294
295
|
"""
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
run_write_query(
|
|
298
|
+
neo4j_session,
|
|
297
299
|
ingest_statement,
|
|
298
300
|
ORG_ID=okta_org_id,
|
|
299
301
|
APP_LIST=app_list,
|
|
@@ -327,7 +329,8 @@ def _load_application_user(
|
|
|
327
329
|
SET r.lastupdated = $okta_update_tag
|
|
328
330
|
"""
|
|
329
331
|
|
|
330
|
-
|
|
332
|
+
run_write_query(
|
|
333
|
+
neo4j_session,
|
|
331
334
|
ingest,
|
|
332
335
|
APP_ID=app_id,
|
|
333
336
|
USER_LIST=user_list,
|
|
@@ -361,7 +364,8 @@ def _load_application_group(
|
|
|
361
364
|
SET r.lastupdated = $okta_update_tag
|
|
362
365
|
"""
|
|
363
366
|
|
|
364
|
-
|
|
367
|
+
run_write_query(
|
|
368
|
+
neo4j_session,
|
|
365
369
|
ingest,
|
|
366
370
|
APP_ID=app_id,
|
|
367
371
|
GROUP_LIST=group_list,
|
|
@@ -400,7 +404,8 @@ def _load_application_reply_urls(
|
|
|
400
404
|
SET r.lastupdated = $okta_update_tag
|
|
401
405
|
"""
|
|
402
406
|
|
|
403
|
-
|
|
407
|
+
run_write_query(
|
|
408
|
+
neo4j_session,
|
|
404
409
|
ingest,
|
|
405
410
|
APP_ID=app_id,
|
|
406
411
|
URL_LIST=reply_urls,
|
|
@@ -10,6 +10,7 @@ import neo4j
|
|
|
10
10
|
|
|
11
11
|
from cartography.client.core.tx import read_list_of_dicts_tx
|
|
12
12
|
from cartography.client.core.tx import read_single_value_tx
|
|
13
|
+
from cartography.client.core.tx import run_write_query
|
|
13
14
|
from cartography.util import timeit
|
|
14
15
|
|
|
15
16
|
AccountRole = namedtuple("AccountRole", ["account_id", "role_name"])
|
|
@@ -116,7 +117,8 @@ def _load_okta_group_to_aws_roles(
|
|
|
116
117
|
SET r.lastupdated = $okta_update_tag
|
|
117
118
|
"""
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
run_write_query(
|
|
121
|
+
neo4j_session,
|
|
120
122
|
ingest_statement,
|
|
121
123
|
GROUP_TO_ROLE=group_to_role,
|
|
122
124
|
okta_update_tag=okta_update_tag,
|
|
@@ -140,7 +142,8 @@ def _load_human_can_assume_role(
|
|
|
140
142
|
SET r.lastupdated = $okta_update_tag
|
|
141
143
|
"""
|
|
142
144
|
|
|
143
|
-
|
|
145
|
+
run_write_query(
|
|
146
|
+
neo4j_session,
|
|
144
147
|
ingest_statement,
|
|
145
148
|
okta_update_tag=okta_update_tag,
|
|
146
149
|
)
|
|
@@ -8,6 +8,7 @@ from okta import FactorsClient
|
|
|
8
8
|
from okta.framework.OktaError import OktaError
|
|
9
9
|
from okta.models.factor.Factor import Factor
|
|
10
10
|
|
|
11
|
+
from cartography.client.core.tx import run_write_query
|
|
11
12
|
from cartography.intel.okta.sync_state import OktaSyncState
|
|
12
13
|
from cartography.util import timeit
|
|
13
14
|
|
|
@@ -130,7 +131,8 @@ def _load_user_factors(
|
|
|
130
131
|
SET r.lastupdated = $okta_update_tag
|
|
131
132
|
"""
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
run_write_query(
|
|
135
|
+
neo4j_session,
|
|
134
136
|
ingest,
|
|
135
137
|
USER_ID=user_id,
|
|
136
138
|
FACTOR_LIST=factors,
|
cartography/intel/okta/groups.py
CHANGED
|
@@ -11,6 +11,7 @@ from okta.framework.OktaError import OktaError
|
|
|
11
11
|
from okta.framework.PagedResults import PagedResults
|
|
12
12
|
from okta.models.usergroup import UserGroup
|
|
13
13
|
|
|
14
|
+
from cartography.client.core.tx import run_write_query
|
|
14
15
|
from cartography.intel.okta.sync_state import OktaSyncState
|
|
15
16
|
from cartography.intel.okta.utils import check_rate_limit
|
|
16
17
|
from cartography.intel.okta.utils import create_api_client
|
|
@@ -204,7 +205,8 @@ def _load_okta_groups(
|
|
|
204
205
|
SET org_r.lastupdated = $okta_update_tag
|
|
205
206
|
"""
|
|
206
207
|
|
|
207
|
-
|
|
208
|
+
run_write_query(
|
|
209
|
+
neo4j_session,
|
|
208
210
|
ingest_statement,
|
|
209
211
|
ORG_ID=okta_org_id,
|
|
210
212
|
GROUP_LIST=group_list,
|
|
@@ -251,7 +253,8 @@ def load_okta_group_members(
|
|
|
251
253
|
SET r.lastupdated = $okta_update_tag
|
|
252
254
|
"""
|
|
253
255
|
logging.info(f"Loading {len(member_list)} members of group {group_id}")
|
|
254
|
-
|
|
256
|
+
run_write_query(
|
|
257
|
+
neo4j_session,
|
|
255
258
|
ingest,
|
|
256
259
|
GROUP_ID=group_id,
|
|
257
260
|
MEMBER_LIST=member_list,
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
|
|
4
4
|
import neo4j
|
|
5
5
|
|
|
6
|
+
from cartography.client.core.tx import run_write_query
|
|
6
7
|
from cartography.util import timeit
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
@@ -27,7 +28,8 @@ def create_okta_organization(
|
|
|
27
28
|
SET org.lastupdated = $okta_update_tag
|
|
28
29
|
"""
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
run_write_query(
|
|
32
|
+
neo4j_session,
|
|
31
33
|
ingest,
|
|
32
34
|
ORG_NAME=organization,
|
|
33
35
|
okta_update_tag=okta_update_tag,
|
|
@@ -7,6 +7,7 @@ from typing import List
|
|
|
7
7
|
import neo4j
|
|
8
8
|
from okta.framework.ApiClient import ApiClient
|
|
9
9
|
|
|
10
|
+
from cartography.client.core.tx import run_write_query
|
|
10
11
|
from cartography.intel.okta.utils import create_api_client
|
|
11
12
|
from cartography.util import timeit
|
|
12
13
|
|
|
@@ -96,7 +97,8 @@ def _load_trusted_origins(
|
|
|
96
97
|
SET r.lastupdated = $okta_update_tag
|
|
97
98
|
"""
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
run_write_query(
|
|
101
|
+
neo4j_session,
|
|
100
102
|
ingest,
|
|
101
103
|
ORG_ID=okta_org_id,
|
|
102
104
|
TRUSTED_LIST=trusted_list,
|
cartography/intel/okta/roles.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import List
|
|
|
7
7
|
import neo4j
|
|
8
8
|
from okta.framework.ApiClient import ApiClient
|
|
9
9
|
|
|
10
|
+
from cartography.client.core.tx import run_write_query
|
|
10
11
|
from cartography.intel.okta.sync_state import OktaSyncState
|
|
11
12
|
from cartography.intel.okta.utils import check_rate_limit
|
|
12
13
|
from cartography.intel.okta.utils import create_api_client
|
|
@@ -117,7 +118,8 @@ def _load_user_role(
|
|
|
117
118
|
SET r2.lastupdated = $okta_update_tag
|
|
118
119
|
"""
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
run_write_query(
|
|
122
|
+
neo4j_session,
|
|
121
123
|
ingest,
|
|
122
124
|
USER_ID=user_id,
|
|
123
125
|
ROLES_DATA=roles_data,
|
|
@@ -149,7 +151,8 @@ def _load_group_role(
|
|
|
149
151
|
SET r2.lastupdated = $okta_update_tag
|
|
150
152
|
"""
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
run_write_query(
|
|
155
|
+
neo4j_session,
|
|
153
156
|
ingest,
|
|
154
157
|
GROUP_ID=group_id,
|
|
155
158
|
ROLES_DATA=roles_data,
|
cartography/intel/okta/users.py
CHANGED
|
@@ -8,6 +8,7 @@ import neo4j
|
|
|
8
8
|
from okta import UsersClient
|
|
9
9
|
from okta.models.user import User
|
|
10
10
|
|
|
11
|
+
from cartography.client.core.tx import run_write_query
|
|
11
12
|
from cartography.intel.okta.sync_state import OktaSyncState
|
|
12
13
|
from cartography.intel.okta.utils import check_rate_limit
|
|
13
14
|
from cartography.util import timeit
|
|
@@ -174,7 +175,8 @@ def _load_okta_users(
|
|
|
174
175
|
SET h.lastupdated = $okta_update_tag
|
|
175
176
|
"""
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
run_write_query(
|
|
179
|
+
neo4j_session,
|
|
178
180
|
ingest_statement,
|
|
179
181
|
ORG_ID=okta_org_id,
|
|
180
182
|
USER_LIST=user_list,
|
|
@@ -82,7 +82,7 @@ class AWSPermissionSetToAWSAccountRel(CartographyRelSchema):
|
|
|
82
82
|
@dataclass(frozen=True)
|
|
83
83
|
class RoleAssignmentAllowedByRelProperties(CartographyRelProperties):
|
|
84
84
|
"""
|
|
85
|
-
Properties for the ALLOWED_BY relationship between AWSRole and
|
|
85
|
+
Properties for the ALLOWED_BY relationship between AWSRole and AWSSSO principals.
|
|
86
86
|
"""
|
|
87
87
|
|
|
88
88
|
# Mandatory fields for MatchLinks
|
|
@@ -121,6 +121,29 @@ class RoleAssignmentAllowedByMatchLink(CartographyRelSchema):
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class RoleAssignmentAllowedByGroupMatchLink(CartographyRelSchema):
|
|
126
|
+
"""
|
|
127
|
+
MatchLink schema for ALLOWED_BY relationships from group role assignments.
|
|
128
|
+
Creates relationships like: (AWSRole)-[:ALLOWED_BY]->(AWSSSOGroup)
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
source_node_label: str = "AWSRole"
|
|
132
|
+
source_node_matcher: SourceNodeMatcher = make_source_node_matcher(
|
|
133
|
+
{"arn": PropertyRef("RoleArn")},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
target_node_label: str = "AWSSSOGroup"
|
|
137
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
138
|
+
{"id": PropertyRef("GroupId")},
|
|
139
|
+
)
|
|
140
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
141
|
+
rel_label: str = "ALLOWED_BY"
|
|
142
|
+
properties: RoleAssignmentAllowedByRelProperties = (
|
|
143
|
+
RoleAssignmentAllowedByRelProperties()
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
124
147
|
@dataclass(frozen=True)
|
|
125
148
|
class AWSPermissionSetSchema(CartographyNodeSchema):
|
|
126
149
|
label: str = "AWSPermissionSet"
|