cartography 0.110.0rc1__py3-none-any.whl → 0.111.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 +16 -3
- cartography/cli.py +46 -8
- cartography/config.py +16 -9
- cartography/data/indexes.cypher +0 -2
- cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json +2 -2
- cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
- cartography/graph/querybuilder.py +70 -0
- cartography/intel/aws/apigateway.py +113 -4
- cartography/intel/aws/cognito.py +201 -0
- cartography/intel/aws/ec2/vpc.py +140 -124
- cartography/intel/aws/ecs.py +7 -1
- cartography/intel/aws/eventbridge.py +73 -0
- cartography/intel/aws/glue.py +64 -0
- cartography/intel/aws/kms.py +13 -1
- cartography/intel/aws/rds.py +105 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/aws/route53.py +3 -1
- cartography/intel/aws/s3.py +104 -0
- cartography/intel/entra/__init__.py +41 -43
- cartography/intel/entra/applications.py +2 -1
- cartography/intel/entra/ou.py +1 -1
- cartography/intel/github/__init__.py +21 -25
- cartography/intel/github/repos.py +32 -48
- cartography/intel/github/util.py +12 -0
- cartography/intel/keycloak/__init__.py +153 -0
- cartography/intel/keycloak/authenticationexecutions.py +322 -0
- cartography/intel/keycloak/authenticationflows.py +77 -0
- cartography/intel/keycloak/clients.py +187 -0
- cartography/intel/keycloak/groups.py +126 -0
- cartography/intel/keycloak/identityproviders.py +94 -0
- cartography/intel/keycloak/organizations.py +163 -0
- cartography/intel/keycloak/realms.py +61 -0
- cartography/intel/keycloak/roles.py +202 -0
- cartography/intel/keycloak/scopes.py +73 -0
- cartography/intel/keycloak/users.py +70 -0
- cartography/intel/keycloak/util.py +47 -0
- cartography/intel/kubernetes/__init__.py +4 -0
- cartography/intel/kubernetes/rbac.py +464 -0
- cartography/intel/kubernetes/util.py +17 -0
- cartography/models/aws/apigateway/apigatewaydeployment.py +74 -0
- cartography/models/aws/cognito/__init__.py +0 -0
- cartography/models/aws/cognito/identity_pool.py +70 -0
- cartography/models/aws/cognito/user_pool.py +47 -0
- cartography/models/aws/ec2/security_groups.py +1 -1
- cartography/models/aws/ec2/vpc.py +46 -0
- cartography/models/aws/ec2/vpc_cidr.py +102 -0
- cartography/models/aws/ecs/services.py +17 -0
- cartography/models/aws/ecs/tasks.py +1 -0
- cartography/models/aws/eventbridge/target.py +71 -0
- cartography/models/aws/glue/job.py +69 -0
- cartography/models/aws/rds/event_subscription.py +146 -0
- cartography/models/aws/route53/dnsrecord.py +21 -0
- cartography/models/github/dependencies.py +1 -2
- cartography/models/keycloak/__init__.py +0 -0
- cartography/models/keycloak/authenticationexecution.py +160 -0
- cartography/models/keycloak/authenticationflow.py +54 -0
- cartography/models/keycloak/client.py +177 -0
- cartography/models/keycloak/group.py +101 -0
- cartography/models/keycloak/identityprovider.py +89 -0
- cartography/models/keycloak/organization.py +116 -0
- cartography/models/keycloak/organizationdomain.py +73 -0
- cartography/models/keycloak/realm.py +173 -0
- cartography/models/keycloak/role.py +126 -0
- cartography/models/keycloak/scope.py +73 -0
- cartography/models/keycloak/user.py +51 -0
- cartography/models/kubernetes/clusterrolebindings.py +98 -0
- cartography/models/kubernetes/clusterroles.py +52 -0
- cartography/models/kubernetes/rolebindings.py +119 -0
- cartography/models/kubernetes/roles.py +76 -0
- cartography/models/kubernetes/serviceaccounts.py +77 -0
- cartography/models/tailscale/device.py +1 -0
- cartography/sync.py +2 -0
- cartography/util.py +8 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/METADATA +4 -3
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/RECORD +85 -46
- cartography/data/jobs/cleanup/aws_import_vpc_cleanup.json +0 -23
- cartography/intel/entra/resources.py +0 -20
- /cartography/data/jobs/{analysis → scoped_analysis}/aws_s3acl_analysis.json +0 -0
- /cartography/models/aws/{__init__.py → apigateway/__init__.py} +0 -0
- /cartography/models/aws/{apigateway.py → apigateway/apigateway.py} +0 -0
- /cartography/models/aws/{apigatewaycertificate.py → apigateway/apigatewaycertificate.py} +0 -0
- /cartography/models/aws/{apigatewayresource.py → apigateway/apigatewayresource.py} +0 -0
- /cartography/models/aws/{apigatewaystage.py → apigateway/apigatewaystage.py} +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/WHEEL +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/top_level.txt +0 -0
cartography/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.111.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 111, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
cartography/cli.py
CHANGED
|
@@ -254,14 +254,6 @@ class CLI:
|
|
|
254
254
|
"The name of environment variable containing Entra Client Secret for Service Principal Authentication."
|
|
255
255
|
),
|
|
256
256
|
)
|
|
257
|
-
parser.add_argument(
|
|
258
|
-
"--entra-best-effort-mode",
|
|
259
|
-
action="store_true",
|
|
260
|
-
help=(
|
|
261
|
-
"Enable Entra ID sync best effort mode. This will allow cartography to continue "
|
|
262
|
-
"syncing other Entra ID entities and delay raising an exception until the end of the sync."
|
|
263
|
-
),
|
|
264
|
-
)
|
|
265
257
|
parser.add_argument(
|
|
266
258
|
"--aws-requested-syncs",
|
|
267
259
|
type=str,
|
|
@@ -770,6 +762,42 @@ class CLI:
|
|
|
770
762
|
"Required if you are using the SentinelOne intel module. Ignored otherwise."
|
|
771
763
|
),
|
|
772
764
|
)
|
|
765
|
+
parser.add_argument(
|
|
766
|
+
"--keycloak-client-id",
|
|
767
|
+
type=str,
|
|
768
|
+
default=None,
|
|
769
|
+
help=(
|
|
770
|
+
"The Keycloak client ID to sync. "
|
|
771
|
+
"Required if you are using the Keycloak intel module. Ignored otherwise."
|
|
772
|
+
),
|
|
773
|
+
)
|
|
774
|
+
parser.add_argument(
|
|
775
|
+
"--keycloak-client-secret-env-var",
|
|
776
|
+
type=str,
|
|
777
|
+
default="KEYCLOAK_CLIENT_SECRET",
|
|
778
|
+
help=(
|
|
779
|
+
"The name of an environment variable containing the Keycloak client secret. "
|
|
780
|
+
"Required if you are using the Keycloak intel module. Ignored otherwise."
|
|
781
|
+
),
|
|
782
|
+
)
|
|
783
|
+
parser.add_argument(
|
|
784
|
+
"--keycloak-url",
|
|
785
|
+
type=str,
|
|
786
|
+
help=(
|
|
787
|
+
"The base URL for the Keycloak instance. "
|
|
788
|
+
"Required if you are using the Keycloak intel module. Ignored otherwise. "
|
|
789
|
+
),
|
|
790
|
+
)
|
|
791
|
+
parser.add_argument(
|
|
792
|
+
"--keycloak-realm",
|
|
793
|
+
type=str,
|
|
794
|
+
default="master",
|
|
795
|
+
help=(
|
|
796
|
+
"The Keycloak realm used for authentication (note: all available realms will be synced). "
|
|
797
|
+
"Should be `master` (default value) in most of the cases. "
|
|
798
|
+
"Required if you are using the Keycloak intel module. Ignored otherwise. "
|
|
799
|
+
),
|
|
800
|
+
)
|
|
773
801
|
|
|
774
802
|
return parser
|
|
775
803
|
|
|
@@ -1141,6 +1169,16 @@ class CLI:
|
|
|
1141
1169
|
else:
|
|
1142
1170
|
config.sentinelone_api_token = None
|
|
1143
1171
|
|
|
1172
|
+
if config.keycloak_client_secret_env_var:
|
|
1173
|
+
logger.debug(
|
|
1174
|
+
f"Reading Client Secret for Keycloak from environment variable {config.keycloak_client_secret_env_var}",
|
|
1175
|
+
)
|
|
1176
|
+
config.keycloak_client_secret = os.environ.get(
|
|
1177
|
+
config.keycloak_client_secret_env_var
|
|
1178
|
+
)
|
|
1179
|
+
else:
|
|
1180
|
+
config.keycloak_client_secret = None
|
|
1181
|
+
|
|
1144
1182
|
# Run cartography
|
|
1145
1183
|
try:
|
|
1146
1184
|
return cartography.sync.run_with_config(self.sync, config)
|
cartography/config.py
CHANGED
|
@@ -51,9 +51,6 @@ class Config:
|
|
|
51
51
|
:param entra_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
|
|
52
52
|
:type entra_client_secret: str
|
|
53
53
|
:param entra_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
|
|
54
|
-
:type entra_best_effort_mode: bool
|
|
55
|
-
:param entra_best_effort_mode: If True, Entra ID sync will continue on errors and raise an aggregated
|
|
56
|
-
exception at the end of the sync. If False (default), exceptions will be raised immediately.
|
|
57
54
|
:type aws_requested_syncs: str
|
|
58
55
|
:param aws_requested_syncs: Comma-separated list of AWS resources to sync. Optional.
|
|
59
56
|
:type aws_guardduty_severity_threshold: str
|
|
@@ -167,10 +164,16 @@ class Config:
|
|
|
167
164
|
:param sentinelone_api_url: SentinelOne API URL. Optional.
|
|
168
165
|
:type sentinelone_api_token: string
|
|
169
166
|
:param sentinelone_api_token: SentinelOne API token for authentication. Optional.
|
|
170
|
-
:type sentinelone_api_token_env_var: string
|
|
171
|
-
:param sentinelone_api_token_env_var: The name of an environment variable containing the SentinelOne API token. Optional.
|
|
172
167
|
:type sentinelone_account_ids: list[str]
|
|
173
168
|
:param sentinelone_account_ids: List of SentinelOne account IDs to sync. Optional.
|
|
169
|
+
:type keycloak_client_id: str
|
|
170
|
+
:param keycloak_client_id: Keycloak client ID for API authentication. Optional.
|
|
171
|
+
:type keycloak_client_secret: str
|
|
172
|
+
:param keycloak_client_secret: Keycloak client secret for API authentication. Optional.
|
|
173
|
+
:type keycloak_realm: str
|
|
174
|
+
:param keycloak_realm: Keycloak realm for authentication (all realms will be synced). Optional.
|
|
175
|
+
:type keycloak_url: str
|
|
176
|
+
:param keycloak_url: Keycloak base URL, e.g. https://keycloak.example.com. Optional.
|
|
174
177
|
"""
|
|
175
178
|
|
|
176
179
|
def __init__(
|
|
@@ -194,7 +197,6 @@ class Config:
|
|
|
194
197
|
entra_tenant_id=None,
|
|
195
198
|
entra_client_id=None,
|
|
196
199
|
entra_client_secret=None,
|
|
197
|
-
entra_best_effort_mode=False,
|
|
198
200
|
aws_requested_syncs=None,
|
|
199
201
|
aws_guardduty_severity_threshold=None,
|
|
200
202
|
analysis_job_directory=None,
|
|
@@ -257,8 +259,11 @@ class Config:
|
|
|
257
259
|
scaleway_org=None,
|
|
258
260
|
sentinelone_api_url=None,
|
|
259
261
|
sentinelone_api_token=None,
|
|
260
|
-
sentinelone_api_token_env_var=None,
|
|
261
262
|
sentinelone_account_ids=None,
|
|
263
|
+
keycloak_client_id=None,
|
|
264
|
+
keycloak_client_secret=None,
|
|
265
|
+
keycloak_realm=None,
|
|
266
|
+
keycloak_url=None,
|
|
262
267
|
):
|
|
263
268
|
self.neo4j_uri = neo4j_uri
|
|
264
269
|
self.neo4j_user = neo4j_user
|
|
@@ -281,7 +286,6 @@ class Config:
|
|
|
281
286
|
self.entra_tenant_id = entra_tenant_id
|
|
282
287
|
self.entra_client_id = entra_client_id
|
|
283
288
|
self.entra_client_secret = entra_client_secret
|
|
284
|
-
self.entra_best_effort_mode = entra_best_effort_mode
|
|
285
289
|
self.aws_requested_syncs = aws_requested_syncs
|
|
286
290
|
self.aws_guardduty_severity_threshold = aws_guardduty_severity_threshold
|
|
287
291
|
self.analysis_job_directory = analysis_job_directory
|
|
@@ -344,5 +348,8 @@ class Config:
|
|
|
344
348
|
self.scaleway_org = scaleway_org
|
|
345
349
|
self.sentinelone_api_url = sentinelone_api_url
|
|
346
350
|
self.sentinelone_api_token = sentinelone_api_token
|
|
347
|
-
self.sentinelone_api_token_env_var = sentinelone_api_token_env_var
|
|
348
351
|
self.sentinelone_account_ids = sentinelone_account_ids
|
|
352
|
+
self.keycloak_client_id = keycloak_client_id
|
|
353
|
+
self.keycloak_client_secret = keycloak_client_secret
|
|
354
|
+
self.keycloak_realm = keycloak_realm
|
|
355
|
+
self.keycloak_url = keycloak_url
|
cartography/data/indexes.cypher
CHANGED
|
@@ -51,8 +51,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:AWSTransitGatewayAttachment) ON (n.lastupdated
|
|
|
51
51
|
CREATE INDEX IF NOT EXISTS FOR (n:AWSUser) ON (n.arn);
|
|
52
52
|
CREATE INDEX IF NOT EXISTS FOR (n:AWSUser) ON (n.name);
|
|
53
53
|
CREATE INDEX IF NOT EXISTS FOR (n:AWSUser) ON (n.lastupdated);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS FOR (n:AWSVpc) ON (n.id);
|
|
55
|
-
CREATE INDEX IF NOT EXISTS FOR (n:AWSVpc) ON (n.lastupdated);
|
|
56
54
|
CREATE INDEX IF NOT EXISTS FOR (n:AccountAccessKey) ON (n.accesskeyid);
|
|
57
55
|
CREATE INDEX IF NOT EXISTS FOR (n:AccountAccessKey) ON (n.lastupdated);
|
|
58
56
|
CREATE INDEX IF NOT EXISTS FOR (n:AutoScalingGroup) ON (n.arn);
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"iterative": false
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
"__comment__": "Attach EC2KeyPairs with matching fingerprints to
|
|
26
|
-
"query": "MATCH (k1:EC2KeyPair)
|
|
25
|
+
"__comment__": "Attach EC2KeyPairs with matching fingerprints to each other and set duplicate_keyfingerprint = True. Use id(k1) < id(k2) to avoid Cartesian product warning and ensure O(1) comparison.",
|
|
26
|
+
"query": "MATCH (k1:EC2KeyPair) MATCH (k2:EC2KeyPair) WHERE id(k1) < id(k2) AND k1.keyfingerprint = k2.keyfingerprint SET k1.duplicate_keyfingerprint = True, k2.duplicate_keyfingerprint = True MERGE (k1)-[r:MATCHING_FINGERPRINT]-(k2) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG RETURN COUNT(*) as TotalCompleted",
|
|
27
27
|
"iterative": false
|
|
28
28
|
}
|
|
29
29
|
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"statements": [
|
|
3
|
+
{
|
|
4
|
+
"__comment__": "Inherit group memberships from subgroups to parent groups",
|
|
5
|
+
"query": "MATCH (u:KeycloakUser)-[:MEMBER_OF]->(g:KeycloakGroup)-[:SUBGROUP_OF*1..5]->(pg:KeycloakGroup) MERGE (u)-[r:INHERITED_MEMBER_OF]->(pg) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG",
|
|
6
|
+
"iterative": false
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"__comment__": "Assign roles to users based on group memberships",
|
|
10
|
+
"query": "MATCH (u:KeycloakUser)-[:MEMBER_OF|INHERITED_MEMBER_OR]->(g:KeycloakGroup)-[:GRANTS]->(r:KeycloakRole) MERGE (u)-[r0:ASSUME_ROLE]-(r) ON CREATE SET r0.firstseen = $UPDATE_TAG SET r0.lastupdated = $UPDATE_TAG",
|
|
11
|
+
"iterative": false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"__comment__": "Propagate role grants to composite roles",
|
|
15
|
+
"query": "MATCH (r:KeycloakRole)-[:INCLUDES*1..5]->(c:KeycloakRole)-[:GRANTS]->(s:KeycloakScope) MERGE (r)-[r0:INDIRECT_GRANTS]-(s) ON CREATE SET r0.firstseen = $UPDATE_TAG SET r0.lastupdated = $UPDATE_TAG",
|
|
16
|
+
"iterative": false
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"__comment__": "Identify legitimate scopes for users based on assumed roles",
|
|
20
|
+
"query": "MATCH (u:KeycloakUser)-[:ASSUME_ROLE]-(:KeycloakRole)-[:GRANTS|INDIRECT_GRANTS]->(s:KeycloakScope) MERGE (u)-[r:ASSUME_SCOPE]->(s) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG",
|
|
21
|
+
"iterative": false
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"__comment__": "Assign assumed scopes to users for orphan scopes (scopes not granted by any role)",
|
|
25
|
+
"query": "MATCH (s:KeycloakScope)<-[:RESOURCE]-(r:KeycloakRealm) MATCH (u:KeycloakUser)<-[:RESOURCE]-(r) WHERE NOT (s)<-[:GRANTS|INDIRECT_GRANTS]-(:KeycloakRole) MERGE (u)-[r0:ASSUME_SCOPE]->(s) SET r0.firstseen = $UPDATE_TAG SET r0.lastupdated = $UPDATE_TAG",
|
|
26
|
+
"iterative": false
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"name": "Keycloak inheritance analysis"
|
|
30
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from dataclasses import asdict
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version
|
|
3
5
|
from string import Template
|
|
4
6
|
from typing import Dict
|
|
5
7
|
from typing import List
|
|
@@ -223,6 +225,8 @@ def _build_attach_sub_resource_statement(
|
|
|
223
225
|
$RelMergeClause
|
|
224
226
|
ON CREATE SET r.firstseen = timestamp()
|
|
225
227
|
SET
|
|
228
|
+
r._module_name = "$module_name",
|
|
229
|
+
r._module_version = "$module_version",
|
|
226
230
|
$set_rel_properties_statement
|
|
227
231
|
""",
|
|
228
232
|
)
|
|
@@ -244,6 +248,8 @@ def _build_attach_sub_resource_statement(
|
|
|
244
248
|
SubResourceLabel=sub_resource_link.target_node_label,
|
|
245
249
|
MatchClause=_build_match_clause(sub_resource_link.target_node_matcher),
|
|
246
250
|
RelMergeClause=rel_merge_clause,
|
|
251
|
+
module_name=_get_module_from_schema(sub_resource_link),
|
|
252
|
+
module_version=_get_cartography_version(),
|
|
247
253
|
SubResourceRelLabel=sub_resource_link.rel_label,
|
|
248
254
|
set_rel_properties_statement=_build_rel_properties_statement(
|
|
249
255
|
"r",
|
|
@@ -278,6 +284,8 @@ def _build_attach_additional_links_statement(
|
|
|
278
284
|
$RelMerge
|
|
279
285
|
ON CREATE SET $rel_var.firstseen = timestamp()
|
|
280
286
|
SET
|
|
287
|
+
$rel_var._module_name = "$module_name",
|
|
288
|
+
$rel_var._module_version = "$module_version",
|
|
281
289
|
$set_rel_properties_statement
|
|
282
290
|
""",
|
|
283
291
|
)
|
|
@@ -312,6 +320,8 @@ def _build_attach_additional_links_statement(
|
|
|
312
320
|
node_var=node_var,
|
|
313
321
|
rel_var=rel_var,
|
|
314
322
|
RelMerge=rel_merge,
|
|
323
|
+
module_name=_get_module_from_schema(link),
|
|
324
|
+
module_version=_get_cartography_version(),
|
|
315
325
|
set_rel_properties_statement=_build_rel_properties_statement(
|
|
316
326
|
rel_var,
|
|
317
327
|
rel_props_as_dict,
|
|
@@ -453,6 +463,8 @@ def build_ingestion_query(
|
|
|
453
463
|
MERGE (i:$node_label{id: $dict_id_field})
|
|
454
464
|
ON CREATE SET i.firstseen = timestamp()
|
|
455
465
|
SET
|
|
466
|
+
i._module_name = "$module_name",
|
|
467
|
+
i._module_version = "$module_version",
|
|
456
468
|
$set_node_properties_statement
|
|
457
469
|
$attach_relationships_statement
|
|
458
470
|
""",
|
|
@@ -475,6 +487,8 @@ def build_ingestion_query(
|
|
|
475
487
|
ingest_query = query_template.safe_substitute(
|
|
476
488
|
node_label=node_schema.label,
|
|
477
489
|
dict_id_field=node_props.id,
|
|
490
|
+
module_name=_get_module_from_schema(node_schema),
|
|
491
|
+
module_version=_get_cartography_version(),
|
|
478
492
|
set_node_properties_statement=_build_node_properties_statement(
|
|
479
493
|
node_props_as_dict,
|
|
480
494
|
node_schema.extra_node_labels,
|
|
@@ -650,6 +664,8 @@ def build_matchlink_query(rel_schema: CartographyRelSchema) -> str:
|
|
|
650
664
|
MERGE $rel
|
|
651
665
|
ON CREATE SET r.firstseen = timestamp()
|
|
652
666
|
SET
|
|
667
|
+
r._module_name = "$module_name",
|
|
668
|
+
r._module_version = "$module_version",
|
|
653
669
|
$set_rel_properties_statement;
|
|
654
670
|
"""
|
|
655
671
|
)
|
|
@@ -677,8 +693,62 @@ def build_matchlink_query(rel_schema: CartographyRelSchema) -> str:
|
|
|
677
693
|
source_match=source_match,
|
|
678
694
|
target_match=target_match,
|
|
679
695
|
rel=rel,
|
|
696
|
+
module_name=_get_module_from_schema(rel_schema),
|
|
697
|
+
module_version=_get_cartography_version(),
|
|
680
698
|
set_rel_properties_statement=_build_rel_properties_statement(
|
|
681
699
|
"r",
|
|
682
700
|
rel_props_as_dict,
|
|
683
701
|
),
|
|
684
702
|
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _get_cartography_version() -> str:
|
|
706
|
+
"""
|
|
707
|
+
Get the current version of the cartography package.
|
|
708
|
+
|
|
709
|
+
This function attempts to retrieve the version of the installed cartography package
|
|
710
|
+
using importlib.metadata. If the package is not found (typically in development
|
|
711
|
+
or testing environments), it returns 'dev' as a fallback.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
The version string of the cartography package, or 'dev' if not found
|
|
715
|
+
"""
|
|
716
|
+
try:
|
|
717
|
+
return version("cartography")
|
|
718
|
+
except PackageNotFoundError:
|
|
719
|
+
# This can occured if the cartography package is not installed in the environment, typically in development or testing environments.
|
|
720
|
+
logger.warning("cartography package not found. Returning 'dev' version.")
|
|
721
|
+
# Fallback to reading the VERSION file if the package is not found
|
|
722
|
+
return "dev"
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _get_module_from_schema(
|
|
726
|
+
schema, #: "CartographyNodeSchema" | "CartographyRelSchema",
|
|
727
|
+
) -> str:
|
|
728
|
+
"""
|
|
729
|
+
Extract the module name from a Cartography schema object.
|
|
730
|
+
|
|
731
|
+
This function extracts and formats the module name from a CartographyNodeSchema
|
|
732
|
+
or CartographyRelSchema object. It expects schemas to be part of the official
|
|
733
|
+
cartography.models package hierarchy and returns a formatted string indicating
|
|
734
|
+
the specific cartography module.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
schema: A CartographyNodeSchema or CartographyRelSchema object
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
A formatted module name string in the format 'cartography:<module_name>'
|
|
741
|
+
or 'unknown:<full_module_path>' if the schema is not from cartography.models
|
|
742
|
+
"""
|
|
743
|
+
# If the entity schema does not belong to the cartography.models package,
|
|
744
|
+
# we log a warning and return the full module path.
|
|
745
|
+
if not schema.__module__.startswith("cartography.models."):
|
|
746
|
+
logger.warning(
|
|
747
|
+
"The schema %s does not start with 'cartography.models.'. "
|
|
748
|
+
"This may indicate that the schema is not part of the official cartography models.",
|
|
749
|
+
schema.__module__,
|
|
750
|
+
)
|
|
751
|
+
return f"unknown:{schema.__module__}"
|
|
752
|
+
# Otherwise, we return the module path as a string.
|
|
753
|
+
parts = schema.__module__.split(".")
|
|
754
|
+
return f"cartography:{parts[2]}"
|
|
@@ -14,12 +14,18 @@ from policyuniverse.policy import Policy
|
|
|
14
14
|
|
|
15
15
|
from cartography.client.core.tx import load
|
|
16
16
|
from cartography.graph.job import GraphJob
|
|
17
|
-
from cartography.
|
|
18
|
-
from cartography.models.aws.
|
|
17
|
+
from cartography.intel.aws.ec2.util import get_botocore_config
|
|
18
|
+
from cartography.models.aws.apigateway.apigateway import APIGatewayRestAPISchema
|
|
19
|
+
from cartography.models.aws.apigateway.apigatewaycertificate import (
|
|
19
20
|
APIGatewayClientCertificateSchema,
|
|
20
21
|
)
|
|
21
|
-
from cartography.models.aws.
|
|
22
|
-
|
|
22
|
+
from cartography.models.aws.apigateway.apigatewaydeployment import (
|
|
23
|
+
APIGatewayDeploymentSchema,
|
|
24
|
+
)
|
|
25
|
+
from cartography.models.aws.apigateway.apigatewayresource import (
|
|
26
|
+
APIGatewayResourceSchema,
|
|
27
|
+
)
|
|
28
|
+
from cartography.models.aws.apigateway.apigatewaystage import APIGatewayStageSchema
|
|
23
29
|
from cartography.util import aws_handle_regions
|
|
24
30
|
from cartography.util import timeit
|
|
25
31
|
|
|
@@ -40,6 +46,38 @@ def get_apigateway_rest_apis(
|
|
|
40
46
|
return apis
|
|
41
47
|
|
|
42
48
|
|
|
49
|
+
def get_rest_api_ids(
|
|
50
|
+
rest_apis: List[Dict],
|
|
51
|
+
) -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
Extracts the IDs of the REST APIs from the provided list.
|
|
54
|
+
"""
|
|
55
|
+
return [api["id"] for api in rest_apis if "id" in api]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@timeit
|
|
59
|
+
@aws_handle_regions
|
|
60
|
+
def get_rest_api_deployments(
|
|
61
|
+
boto3_session: boto3.session.Session,
|
|
62
|
+
rest_api_ids: List[str],
|
|
63
|
+
region: str,
|
|
64
|
+
) -> List[Dict[str, Any]]:
|
|
65
|
+
"""
|
|
66
|
+
Retrieves the deployments for each REST API in the provided list.
|
|
67
|
+
"""
|
|
68
|
+
client = boto3_session.client(
|
|
69
|
+
"apigateway", region_name=region, config=get_botocore_config()
|
|
70
|
+
)
|
|
71
|
+
deployments: List[Dict[str, Any]] = []
|
|
72
|
+
for api_id in rest_api_ids:
|
|
73
|
+
paginator = client.get_paginator("get_deployments")
|
|
74
|
+
for page in paginator.paginate(restApiId=api_id):
|
|
75
|
+
for deployment in page.get("items", []):
|
|
76
|
+
deployment["api_id"] = api_id
|
|
77
|
+
deployments.append(deployment)
|
|
78
|
+
return deployments
|
|
79
|
+
|
|
80
|
+
|
|
43
81
|
@timeit
|
|
44
82
|
@aws_handle_regions
|
|
45
83
|
def get_rest_api_details(
|
|
@@ -63,6 +101,7 @@ def get_rest_api_details(
|
|
|
63
101
|
|
|
64
102
|
|
|
65
103
|
@timeit
|
|
104
|
+
@aws_handle_regions
|
|
66
105
|
def get_rest_api_stages(api: Dict, client: botocore.client.BaseClient) -> Any:
|
|
67
106
|
"""
|
|
68
107
|
Gets the REST API Stage Resources.
|
|
@@ -104,6 +143,7 @@ def get_rest_api_client_certificate(
|
|
|
104
143
|
|
|
105
144
|
|
|
106
145
|
@timeit
|
|
146
|
+
@aws_handle_regions
|
|
107
147
|
def get_rest_api_resources(api: Dict, client: botocore.client.BaseClient) -> List[Any]:
|
|
108
148
|
"""
|
|
109
149
|
Gets the collection of Resource resources.
|
|
@@ -244,6 +284,25 @@ def transform_rest_api_details(
|
|
|
244
284
|
return stages, certificates, resources
|
|
245
285
|
|
|
246
286
|
|
|
287
|
+
def transform_apigateway_deployments(
|
|
288
|
+
deployments: List[Dict[str, Any]],
|
|
289
|
+
region: str,
|
|
290
|
+
) -> List[Dict[str, Any]]:
|
|
291
|
+
"""
|
|
292
|
+
Transform API Gateway Deployment data for ingestion
|
|
293
|
+
"""
|
|
294
|
+
transformed_deployments = []
|
|
295
|
+
for deployment in deployments:
|
|
296
|
+
transformed_deployment = {
|
|
297
|
+
"id": f"{deployment['api_id']}/{deployment['id']}",
|
|
298
|
+
"api_id": deployment["api_id"],
|
|
299
|
+
"description": deployment.get("description"),
|
|
300
|
+
"region": region,
|
|
301
|
+
}
|
|
302
|
+
transformed_deployments.append(transformed_deployment)
|
|
303
|
+
return transformed_deployments
|
|
304
|
+
|
|
305
|
+
|
|
247
306
|
@timeit
|
|
248
307
|
def load_rest_api_details(
|
|
249
308
|
neo4j_session: neo4j.Session,
|
|
@@ -283,6 +342,30 @@ def load_rest_api_details(
|
|
|
283
342
|
)
|
|
284
343
|
|
|
285
344
|
|
|
345
|
+
@timeit
|
|
346
|
+
def load_apigateway_deployments(
|
|
347
|
+
neo4j_session: neo4j.Session,
|
|
348
|
+
data: List[Dict[str, Any]],
|
|
349
|
+
region: str,
|
|
350
|
+
current_aws_account_id: str,
|
|
351
|
+
aws_update_tag: int,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""
|
|
354
|
+
Load API Gateway Deployment data into neo4j.
|
|
355
|
+
"""
|
|
356
|
+
logger.info(
|
|
357
|
+
f"Loading API Gateway {len(data)} deployments for region '{region}' into graph.",
|
|
358
|
+
)
|
|
359
|
+
load(
|
|
360
|
+
neo4j_session,
|
|
361
|
+
APIGatewayDeploymentSchema(),
|
|
362
|
+
data,
|
|
363
|
+
region=region,
|
|
364
|
+
lastupdated=aws_update_tag,
|
|
365
|
+
AWS_ID=current_aws_account_id,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
286
369
|
@timeit
|
|
287
370
|
def parse_policy(api_id: str, policy: Policy) -> Optional[Dict[Any, Any]]:
|
|
288
371
|
"""
|
|
@@ -345,6 +428,12 @@ def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None:
|
|
|
345
428
|
)
|
|
346
429
|
cleanup_job.run(neo4j_session)
|
|
347
430
|
|
|
431
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
432
|
+
APIGatewayDeploymentSchema(),
|
|
433
|
+
common_job_parameters,
|
|
434
|
+
)
|
|
435
|
+
cleanup_job.run(neo4j_session)
|
|
436
|
+
|
|
348
437
|
|
|
349
438
|
@timeit
|
|
350
439
|
def sync_apigateway_rest_apis(
|
|
@@ -375,6 +464,19 @@ def sync_apigateway_rest_apis(
|
|
|
375
464
|
current_aws_account_id,
|
|
376
465
|
aws_update_tag,
|
|
377
466
|
)
|
|
467
|
+
|
|
468
|
+
api_ids = get_rest_api_ids(rest_apis)
|
|
469
|
+
deployments = get_rest_api_deployments(
|
|
470
|
+
boto3_session,
|
|
471
|
+
api_ids,
|
|
472
|
+
region,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
transformed_deployments = transform_apigateway_deployments(
|
|
476
|
+
deployments,
|
|
477
|
+
region,
|
|
478
|
+
)
|
|
479
|
+
|
|
378
480
|
load_apigateway_rest_apis(
|
|
379
481
|
neo4j_session,
|
|
380
482
|
transformed_apis,
|
|
@@ -388,6 +490,13 @@ def sync_apigateway_rest_apis(
|
|
|
388
490
|
current_aws_account_id,
|
|
389
491
|
aws_update_tag,
|
|
390
492
|
)
|
|
493
|
+
load_apigateway_deployments(
|
|
494
|
+
neo4j_session,
|
|
495
|
+
transformed_deployments,
|
|
496
|
+
region,
|
|
497
|
+
current_aws_account_id,
|
|
498
|
+
aws_update_tag,
|
|
499
|
+
)
|
|
391
500
|
|
|
392
501
|
|
|
393
502
|
@timeit
|