cartography 0.101.1rc1__py3-none-any.whl → 0.102.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 +61 -0
- cartography/config.py +16 -0
- cartography/data/indexes.cypher +0 -6
- cartography/data/jobs/cleanup/crowdstrike_import_cleanup.json +0 -5
- cartography/intel/aws/__init__.py +11 -1
- cartography/intel/aws/ec2/launch_templates.py +14 -5
- cartography/intel/aws/ec2/load_balancers.py +126 -148
- cartography/intel/aws/ec2/route_tables.py +287 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/aws/util/common.py +27 -0
- cartography/intel/crowdstrike/__init__.py +17 -5
- cartography/intel/crowdstrike/endpoints.py +12 -44
- cartography/intel/entra/__init__.py +43 -0
- cartography/intel/entra/users.py +205 -0
- cartography/intel/kandji/devices.py +27 -3
- cartography/models/aws/ec2/load_balancer_listeners.py +68 -0
- cartography/models/aws/ec2/load_balancers.py +102 -0
- cartography/models/aws/ec2/route_table_associations.py +87 -0
- cartography/models/aws/ec2/route_tables.py +121 -0
- cartography/models/aws/ec2/routes.py +77 -0
- cartography/models/crowdstrike/__init__.py +0 -0
- cartography/models/crowdstrike/hosts.py +49 -0
- cartography/models/entra/__init__.py +0 -0
- cartography/models/entra/tenant.py +33 -0
- cartography/models/entra/user.py +83 -0
- cartography/stats.py +1 -1
- cartography/sync.py +2 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/METADATA +4 -1
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/RECORD +34 -21
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/WHEEL +1 -1
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/top_level.txt +0 -0
cartography/_version.py
CHANGED
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
20
|
+
__version__ = version = '0.102.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 102, 0)
|
cartography/cli.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import Optional
|
|
|
8
8
|
import cartography.config
|
|
9
9
|
import cartography.sync
|
|
10
10
|
import cartography.util
|
|
11
|
+
from cartography.intel.aws.util.common import parse_and_validate_aws_regions
|
|
11
12
|
from cartography.intel.aws.util.common import parse_and_validate_aws_requested_syncs
|
|
12
13
|
from cartography.intel.semgrep.dependencies import parse_and_validate_semgrep_ecosystems
|
|
13
14
|
|
|
@@ -153,6 +154,23 @@ class CLI:
|
|
|
153
154
|
'respects the AWS CLI/SDK environment variables and does not override them.'
|
|
154
155
|
),
|
|
155
156
|
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
'--aws-regions',
|
|
159
|
+
type=str,
|
|
160
|
+
default=None,
|
|
161
|
+
help=(
|
|
162
|
+
'[EXPERIMENTAL!] Comma-separated list of AWS regions to sync. Example: specify "us-east-1,us-east-2" '
|
|
163
|
+
'to sync US East 1 and 2. Note that this syncs the same regions in ALL accounts and it is currently '
|
|
164
|
+
'not possible to specify different regions per account. '
|
|
165
|
+
'CAUTION: if you previously synced assets from regions that are _not_ included in your current list, '
|
|
166
|
+
'those assets will be _deleted_ during this sync. '
|
|
167
|
+
'This is because cartography\'s cleanup process uses "lastupdated" and "account id" to determine data '
|
|
168
|
+
'freshness and not regions. So, if a previously synced region is missing in the current sync, '
|
|
169
|
+
'Cartography assumes the associated assets are stale and removes them. '
|
|
170
|
+
'Default behavior: If `--aws-regions` is not specified, cartography will _autodiscover_ the '
|
|
171
|
+
'regions supported by each account being synced.'
|
|
172
|
+
),
|
|
173
|
+
)
|
|
156
174
|
parser.add_argument(
|
|
157
175
|
'--aws-best-effort-mode',
|
|
158
176
|
action='store_true',
|
|
@@ -211,6 +229,30 @@ class CLI:
|
|
|
211
229
|
'The name of environment variable containing Azure Client Secret for Service Principal Authentication.'
|
|
212
230
|
),
|
|
213
231
|
)
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
'--entra-tenant-id',
|
|
234
|
+
type=str,
|
|
235
|
+
default=None,
|
|
236
|
+
help=(
|
|
237
|
+
'Entra Tenant Id for Service Principal Authentication.'
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
'--entra-client-id',
|
|
242
|
+
type=str,
|
|
243
|
+
default=None,
|
|
244
|
+
help=(
|
|
245
|
+
'Entra Client Id for Service Principal Authentication.'
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
'--entra-client-secret-env-var',
|
|
250
|
+
type=str,
|
|
251
|
+
default=None,
|
|
252
|
+
help=(
|
|
253
|
+
'The name of environment variable containing Entra Client Secret for Service Principal Authentication.'
|
|
254
|
+
),
|
|
255
|
+
)
|
|
214
256
|
parser.add_argument(
|
|
215
257
|
'--aws-requested-syncs',
|
|
216
258
|
type=str,
|
|
@@ -605,6 +647,11 @@ class CLI:
|
|
|
605
647
|
# No need to store the returned value; we're using this for input validation.
|
|
606
648
|
parse_and_validate_aws_requested_syncs(config.aws_requested_syncs)
|
|
607
649
|
|
|
650
|
+
# AWS regions
|
|
651
|
+
if config.aws_regions:
|
|
652
|
+
# No need to store the returned value; we're using this for input validation.
|
|
653
|
+
parse_and_validate_aws_regions(config.aws_regions)
|
|
654
|
+
|
|
608
655
|
# Azure config
|
|
609
656
|
if config.azure_sp_auth and config.azure_client_secret_env_var:
|
|
610
657
|
logger.debug(
|
|
@@ -615,6 +662,16 @@ class CLI:
|
|
|
615
662
|
else:
|
|
616
663
|
config.azure_client_secret = None
|
|
617
664
|
|
|
665
|
+
# Entra config
|
|
666
|
+
if config.entra_tenant_id and config.entra_client_id and config.entra_client_secret_env_var:
|
|
667
|
+
logger.debug(
|
|
668
|
+
"Reading Client Secret for Entra Authentication from environment variable %s",
|
|
669
|
+
config.entra_client_secret_env_var,
|
|
670
|
+
)
|
|
671
|
+
config.entra_client_secret = os.environ.get(config.entra_client_secret_env_var)
|
|
672
|
+
else:
|
|
673
|
+
config.entra_client_secret = None
|
|
674
|
+
|
|
618
675
|
# Okta config
|
|
619
676
|
if config.okta_org_id and config.okta_api_key_env_var:
|
|
620
677
|
logger.debug(f"Reading API key for Okta from environment variable {config.okta_api_key_env_var}")
|
|
@@ -798,5 +855,9 @@ def main(argv=None):
|
|
|
798
855
|
logging.getLogger('botocore').setLevel(logging.WARNING)
|
|
799
856
|
logging.getLogger('googleapiclient').setLevel(logging.WARNING)
|
|
800
857
|
logging.getLogger('neo4j').setLevel(logging.WARNING)
|
|
858
|
+
logging.getLogger('azure.identity').setLevel(logging.WARNING)
|
|
859
|
+
logging.getLogger('httpx').setLevel(logging.WARNING)
|
|
860
|
+
logging.getLogger('azure.core.pipeline.policies.http_logging_policy').setLevel(logging.WARNING)
|
|
861
|
+
|
|
801
862
|
argv = argv if argv is not None else sys.argv[1:]
|
|
802
863
|
sys.exit(CLI(prog='cartography').main(argv))
|
cartography/config.py
CHANGED
|
@@ -26,6 +26,8 @@ class Config:
|
|
|
26
26
|
:type aws_sync_all_profiles: bool
|
|
27
27
|
:param aws_sync_all_profiles: If True, AWS sync will run for all non-default profiles in the AWS_CONFIG_FILE. If
|
|
28
28
|
False (default), AWS sync will run using the default credentials only. Optional.
|
|
29
|
+
:type aws_regions: str
|
|
30
|
+
:param aws_regions: Comma-separated list of AWS regions to sync. Optional.
|
|
29
31
|
:type aws_best_effort_mode: bool
|
|
30
32
|
:param aws_best_effort_mode: If True, AWS sync will not raise any exceptions, just log. If False (default),
|
|
31
33
|
exceptions will be raised.
|
|
@@ -41,6 +43,12 @@ class Config:
|
|
|
41
43
|
:param azure_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
|
|
42
44
|
:type azure_client_secret: str
|
|
43
45
|
:param azure_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
|
|
46
|
+
:type entra_tenant_id: str
|
|
47
|
+
:param entra_tenant_id: Tenant Id for connecting in a Service Principal Authentication approach. Optional.
|
|
48
|
+
:type entra_client_id: str
|
|
49
|
+
:param entra_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
|
|
50
|
+
:type entra_client_secret: str
|
|
51
|
+
:param entra_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
|
|
44
52
|
:type aws_requested_syncs: str
|
|
45
53
|
:param aws_requested_syncs: Comma-separated list of AWS resources to sync. Optional.
|
|
46
54
|
:type analysis_job_directory: str
|
|
@@ -127,12 +135,16 @@ class Config:
|
|
|
127
135
|
selected_modules=None,
|
|
128
136
|
update_tag=None,
|
|
129
137
|
aws_sync_all_profiles=False,
|
|
138
|
+
aws_regions=None,
|
|
130
139
|
aws_best_effort_mode=False,
|
|
131
140
|
azure_sync_all_subscriptions=False,
|
|
132
141
|
azure_sp_auth=None,
|
|
133
142
|
azure_tenant_id=None,
|
|
134
143
|
azure_client_id=None,
|
|
135
144
|
azure_client_secret=None,
|
|
145
|
+
entra_tenant_id=None,
|
|
146
|
+
entra_client_id=None,
|
|
147
|
+
entra_client_secret=None,
|
|
136
148
|
aws_requested_syncs=None,
|
|
137
149
|
analysis_job_directory=None,
|
|
138
150
|
oci_sync_all_profiles=None,
|
|
@@ -185,12 +197,16 @@ class Config:
|
|
|
185
197
|
self.selected_modules = selected_modules
|
|
186
198
|
self.update_tag = update_tag
|
|
187
199
|
self.aws_sync_all_profiles = aws_sync_all_profiles
|
|
200
|
+
self.aws_regions = aws_regions
|
|
188
201
|
self.aws_best_effort_mode = aws_best_effort_mode
|
|
189
202
|
self.azure_sync_all_subscriptions = azure_sync_all_subscriptions
|
|
190
203
|
self.azure_sp_auth = azure_sp_auth
|
|
191
204
|
self.azure_tenant_id = azure_tenant_id
|
|
192
205
|
self.azure_client_id = azure_client_id
|
|
193
206
|
self.azure_client_secret = azure_client_secret
|
|
207
|
+
self.entra_tenant_id = entra_tenant_id
|
|
208
|
+
self.entra_client_id = entra_client_id
|
|
209
|
+
self.entra_client_secret = entra_client_secret
|
|
194
210
|
self.aws_requested_syncs = aws_requested_syncs
|
|
195
211
|
self.analysis_job_directory = analysis_job_directory
|
|
196
212
|
self.oci_sync_all_profiles = oci_sync_all_profiles
|
cartography/data/indexes.cypher
CHANGED
|
@@ -65,9 +65,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:AccountAccessKey) ON (n.accesskeyid);
|
|
|
65
65
|
CREATE INDEX IF NOT EXISTS FOR (n:AccountAccessKey) ON (n.lastupdated);
|
|
66
66
|
CREATE INDEX IF NOT EXISTS FOR (n:AutoScalingGroup) ON (n.arn);
|
|
67
67
|
CREATE INDEX IF NOT EXISTS FOR (n:AutoScalingGroup) ON (n.lastupdated);
|
|
68
|
-
CREATE INDEX IF NOT EXISTS FOR (n:CrowdstrikeHost) ON (n.id);
|
|
69
|
-
CREATE INDEX IF NOT EXISTS FOR (n:CrowdstrikeHost) ON (n.instance_id);
|
|
70
|
-
CREATE INDEX IF NOT EXISTS FOR (n:CrowdstrikeHost) ON (n.lastupdated);
|
|
71
68
|
CREATE INDEX IF NOT EXISTS FOR (n:CVE) ON (n.id);
|
|
72
69
|
CREATE INDEX IF NOT EXISTS FOR (n:CVE) ON (n.lastupdated);
|
|
73
70
|
CREATE INDEX IF NOT EXISTS FOR (n:Dependency) ON (n.id);
|
|
@@ -194,9 +191,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:KMSGrant) ON (n.lastupdated);
|
|
|
194
191
|
CREATE INDEX IF NOT EXISTS FOR (n:LaunchConfiguration) ON (n.id);
|
|
195
192
|
CREATE INDEX IF NOT EXISTS FOR (n:LaunchConfiguration) ON (n.name);
|
|
196
193
|
CREATE INDEX IF NOT EXISTS FOR (n:LaunchConfiguration) ON (n.lastupdated);
|
|
197
|
-
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancer) ON (n.dnsname);
|
|
198
|
-
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancer) ON (n.id);
|
|
199
|
-
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancer) ON (n.lastupdated);
|
|
200
194
|
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancerV2) ON (n.dnsname);
|
|
201
195
|
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancerV2) ON (n.id);
|
|
202
196
|
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancerV2) ON (n.lastupdated);
|
|
@@ -5,11 +5,6 @@
|
|
|
5
5
|
"iterative": true,
|
|
6
6
|
"iterationsize": 100
|
|
7
7
|
},
|
|
8
|
-
{
|
|
9
|
-
"query": "MATCH (h:CrowdstrikeHost) WHERE h.lastupdated <> $UPDATE_TAG WITH h LIMIT $LIMIT_SIZE DETACH DELETE (h)",
|
|
10
|
-
"iterative": true,
|
|
11
|
-
"iterationsize": 100
|
|
12
|
-
},
|
|
13
8
|
{
|
|
14
9
|
"query": "MATCH (:CrowdstrikeFinding)<-[hc:HAS_CVE]-(:SpotlightVulnerability) WHERE hc.lastupdated <> $UPDATE_TAG WITH hc LIMIT $LIMIT_SIZE DELETE (hc)",
|
|
15
10
|
"iterative": true,
|
|
@@ -14,6 +14,7 @@ from . import ec2
|
|
|
14
14
|
from . import organizations
|
|
15
15
|
from .resources import RESOURCE_FUNCTIONS
|
|
16
16
|
from cartography.config import Config
|
|
17
|
+
from cartography.intel.aws.util.common import parse_and_validate_aws_regions
|
|
17
18
|
from cartography.intel.aws.util.common import parse_and_validate_aws_requested_syncs
|
|
18
19
|
from cartography.stats import get_stats_client
|
|
19
20
|
from cartography.util import merge_module_sync_metadata
|
|
@@ -48,9 +49,10 @@ def _sync_one_account(
|
|
|
48
49
|
current_aws_account_id: str,
|
|
49
50
|
update_tag: int,
|
|
50
51
|
common_job_parameters: Dict[str, Any],
|
|
51
|
-
regions:
|
|
52
|
+
regions: list[str] | None = None,
|
|
52
53
|
aws_requested_syncs: Iterable[str] = RESOURCE_FUNCTIONS.keys(),
|
|
53
54
|
) -> None:
|
|
55
|
+
# Autodiscover the regions supported by the account unless the user has specified the regions to sync.
|
|
54
56
|
if not regions:
|
|
55
57
|
regions = _autodiscover_account_regions(boto3_session, current_aws_account_id)
|
|
56
58
|
|
|
@@ -146,6 +148,7 @@ def _sync_multiple_accounts(
|
|
|
146
148
|
common_job_parameters: Dict[str, Any],
|
|
147
149
|
aws_best_effort_mode: bool,
|
|
148
150
|
aws_requested_syncs: List[str] = [],
|
|
151
|
+
regions: list[str] | None = None,
|
|
149
152
|
) -> bool:
|
|
150
153
|
logger.info("Syncing AWS accounts: %s", ', '.join(accounts.values()))
|
|
151
154
|
organizations.sync(neo4j_session, accounts, sync_tag, common_job_parameters)
|
|
@@ -173,6 +176,7 @@ def _sync_multiple_accounts(
|
|
|
173
176
|
account_id,
|
|
174
177
|
sync_tag,
|
|
175
178
|
common_job_parameters,
|
|
179
|
+
regions=regions,
|
|
176
180
|
aws_requested_syncs=aws_requested_syncs, # Could be replaced later with per-account requested syncs
|
|
177
181
|
)
|
|
178
182
|
except Exception as e:
|
|
@@ -299,6 +303,11 @@ def start_aws_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
299
303
|
if config.aws_requested_syncs:
|
|
300
304
|
requested_syncs = parse_and_validate_aws_requested_syncs(config.aws_requested_syncs)
|
|
301
305
|
|
|
306
|
+
if config.aws_regions:
|
|
307
|
+
regions = parse_and_validate_aws_regions(config.aws_regions)
|
|
308
|
+
else:
|
|
309
|
+
regions = None
|
|
310
|
+
|
|
302
311
|
sync_successful = _sync_multiple_accounts(
|
|
303
312
|
neo4j_session,
|
|
304
313
|
aws_accounts,
|
|
@@ -306,6 +315,7 @@ def start_aws_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
306
315
|
common_job_parameters,
|
|
307
316
|
config.aws_best_effort_mode,
|
|
308
317
|
requested_syncs,
|
|
318
|
+
regions=regions,
|
|
309
319
|
)
|
|
310
320
|
|
|
311
321
|
if sync_successful:
|
|
@@ -69,10 +69,15 @@ def get_launch_template_versions_by_template(
|
|
|
69
69
|
return template_versions
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def transform_launch_templates(templates: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
72
|
+
def transform_launch_templates(templates: list[dict[str, Any]], versions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
73
|
+
valid_template_ids = {v['LaunchTemplateId'] for v in versions}
|
|
73
74
|
result: list[dict[str, Any]] = []
|
|
74
75
|
for template in templates:
|
|
76
|
+
if template['LaunchTemplateId'] not in valid_template_ids:
|
|
77
|
+
continue
|
|
78
|
+
|
|
75
79
|
current = template.copy()
|
|
80
|
+
# Convert CreateTime to timestamp string
|
|
76
81
|
current['CreateTime'] = str(int(current['CreateTime'].timestamp()))
|
|
77
82
|
result.append(current)
|
|
78
83
|
return result
|
|
@@ -165,9 +170,13 @@ def sync_ec2_launch_templates(
|
|
|
165
170
|
logger.info(f"Syncing launch templates for region '{region}' in account '{current_aws_account_id}'.")
|
|
166
171
|
templates = get_launch_templates(boto3_session, region)
|
|
167
172
|
versions = get_launch_template_versions(boto3_session, region, templates)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
|
|
174
|
+
# Transform and load the templates that have versions
|
|
175
|
+
transformed_templates = transform_launch_templates(templates, versions)
|
|
176
|
+
load_launch_templates(neo4j_session, transformed_templates, region, current_aws_account_id, update_tag)
|
|
177
|
+
|
|
178
|
+
# Transform and load the versions
|
|
179
|
+
transformed_versions = transform_launch_template_versions(versions)
|
|
180
|
+
load_launch_template_versions(neo4j_session, transformed_versions, region, current_aws_account_id, update_tag)
|
|
172
181
|
|
|
173
182
|
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -1,190 +1,168 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Dict
|
|
3
|
-
from typing import List
|
|
4
2
|
|
|
5
3
|
import boto3
|
|
6
4
|
import neo4j
|
|
7
5
|
|
|
8
6
|
from .util import get_botocore_config
|
|
7
|
+
from cartography.client.core.tx import load
|
|
8
|
+
from cartography.graph.job import GraphJob
|
|
9
|
+
from cartography.models.aws.ec2.load_balancer_listeners import ELBListenerSchema
|
|
10
|
+
from cartography.models.aws.ec2.load_balancers import LoadBalancerSchema
|
|
9
11
|
from cartography.util import aws_handle_regions
|
|
10
|
-
from cartography.util import run_cleanup_job
|
|
11
12
|
from cartography.util import timeit
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def _get_listener_id(load_balancer_id: str, port: int, protocol: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Generate a unique ID for a load balancer listener.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
load_balancer_id: The ID of the load balancer
|
|
23
|
+
port: The listener port
|
|
24
|
+
protocol: The listener protocol
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A unique ID string for the listener
|
|
28
|
+
"""
|
|
29
|
+
return f"{load_balancer_id}{port}{protocol}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def transform_load_balancer_listener_data(load_balancer_id: str, listener_data: list[dict]) -> list[dict]:
|
|
33
|
+
"""
|
|
34
|
+
Transform load balancer listener data into a format suitable for cartography ingestion.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
load_balancer_id: The ID of the load balancer
|
|
38
|
+
listener_data: List of listener data from AWS API
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of transformed listener data
|
|
42
|
+
"""
|
|
43
|
+
transformed = []
|
|
44
|
+
for listener in listener_data:
|
|
45
|
+
listener_info = listener['Listener']
|
|
46
|
+
transformed_listener = {
|
|
47
|
+
'id': _get_listener_id(load_balancer_id, listener_info['LoadBalancerPort'], listener_info['Protocol']),
|
|
48
|
+
'port': listener_info.get('LoadBalancerPort'),
|
|
49
|
+
'protocol': listener_info.get('Protocol'),
|
|
50
|
+
'instance_port': listener_info.get('InstancePort'),
|
|
51
|
+
'instance_protocol': listener_info.get('InstanceProtocol'),
|
|
52
|
+
'policy_names': listener.get('PolicyNames', []),
|
|
53
|
+
'LoadBalancerId': load_balancer_id,
|
|
54
|
+
}
|
|
55
|
+
transformed.append(transformed_listener)
|
|
56
|
+
return transformed
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def transform_load_balancer_data(load_balancers: list[dict]) -> tuple[list[dict], list[dict]]:
|
|
60
|
+
"""
|
|
61
|
+
Transform load balancer data into a format suitable for cartography ingestion.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
load_balancers: List of load balancer data from AWS API
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (transformed load balancer data, transformed listener data)
|
|
68
|
+
"""
|
|
69
|
+
transformed = []
|
|
70
|
+
listener_data = []
|
|
71
|
+
|
|
72
|
+
for lb in load_balancers:
|
|
73
|
+
load_balancer_id = lb['DNSName']
|
|
74
|
+
transformed_lb = {
|
|
75
|
+
'id': load_balancer_id,
|
|
76
|
+
'name': lb['LoadBalancerName'],
|
|
77
|
+
'dnsname': lb['DNSName'],
|
|
78
|
+
'canonicalhostedzonename': lb.get('CanonicalHostedZoneName'),
|
|
79
|
+
'canonicalhostedzonenameid': lb.get('CanonicalHostedZoneNameID'),
|
|
80
|
+
'scheme': lb.get('Scheme'),
|
|
81
|
+
'createdtime': str(lb['CreatedTime']),
|
|
82
|
+
'GROUP_NAME': lb.get('SourceSecurityGroup', {}).get('GroupName'),
|
|
83
|
+
'GROUP_IDS': [str(group) for group in lb.get('SecurityGroups', [])],
|
|
84
|
+
'INSTANCE_IDS': [instance['InstanceId'] for instance in lb.get('Instances', [])],
|
|
85
|
+
'LISTENER_IDS': [
|
|
86
|
+
_get_listener_id(
|
|
87
|
+
load_balancer_id,
|
|
88
|
+
listener['Listener']['LoadBalancerPort'],
|
|
89
|
+
listener['Listener']['Protocol'],
|
|
90
|
+
) for listener in lb.get('ListenerDescriptions', [])
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
transformed.append(transformed_lb)
|
|
94
|
+
|
|
95
|
+
# Classic ELB listeners are not returned anywhere else in AWS, so we must parse them out
|
|
96
|
+
# of the describe_load_balancers response.
|
|
97
|
+
if lb.get('ListenerDescriptions'):
|
|
98
|
+
listener_data.extend(
|
|
99
|
+
transform_load_balancer_listener_data(
|
|
100
|
+
load_balancer_id,
|
|
101
|
+
lb.get('ListenerDescriptions', []),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return transformed, listener_data
|
|
106
|
+
|
|
107
|
+
|
|
16
108
|
@timeit
|
|
17
109
|
@aws_handle_regions
|
|
18
|
-
def get_loadbalancer_data(boto3_session: boto3.session.Session, region: str) ->
|
|
110
|
+
def get_loadbalancer_data(boto3_session: boto3.session.Session, region: str) -> list[dict]:
|
|
19
111
|
client = boto3_session.client('elb', region_name=region, config=get_botocore_config())
|
|
20
112
|
paginator = client.get_paginator('describe_load_balancers')
|
|
21
|
-
elbs:
|
|
113
|
+
elbs: list[dict] = []
|
|
22
114
|
for page in paginator.paginate():
|
|
23
115
|
elbs.extend(page['LoadBalancerDescriptions'])
|
|
24
116
|
return elbs
|
|
25
117
|
|
|
26
118
|
|
|
27
119
|
@timeit
|
|
28
|
-
def
|
|
29
|
-
neo4j_session: neo4j.Session,
|
|
120
|
+
def load_load_balancers(
|
|
121
|
+
neo4j_session: neo4j.Session, data: list[dict], region: str, current_aws_account_id: str,
|
|
30
122
|
update_tag: int,
|
|
31
123
|
) -> None:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
l.firstseen = timestamp()
|
|
40
|
-
SET l.instance_port = data.Listener.InstancePort, l.instance_protocol = data.Listener.InstanceProtocol,
|
|
41
|
-
l.policy_names = data.PolicyNames,
|
|
42
|
-
l.lastupdated = $update_tag
|
|
43
|
-
WITH l, elb
|
|
44
|
-
MERGE (elb)-[r:ELB_LISTENER]->(l)
|
|
45
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
46
|
-
SET r.lastupdated = $update_tag
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
neo4j_session.run(
|
|
50
|
-
ingest_listener,
|
|
51
|
-
LoadBalancerId=load_balancer_id,
|
|
52
|
-
Listeners=listener_data,
|
|
53
|
-
update_tag=update_tag,
|
|
124
|
+
load(
|
|
125
|
+
neo4j_session,
|
|
126
|
+
LoadBalancerSchema(),
|
|
127
|
+
data,
|
|
128
|
+
Region=region,
|
|
129
|
+
AWS_ID=current_aws_account_id,
|
|
130
|
+
lastupdated=update_tag,
|
|
54
131
|
)
|
|
55
132
|
|
|
56
133
|
|
|
57
134
|
@timeit
|
|
58
|
-
def
|
|
59
|
-
neo4j_session: neo4j.Session,
|
|
60
|
-
update_tag: int,
|
|
61
|
-
) -> None:
|
|
62
|
-
ingest_load_balancer_subnet = """
|
|
63
|
-
MATCH (elb:LoadBalancer{id: $ID}), (subnet:EC2Subnet{subnetid: $SUBNET_ID})
|
|
64
|
-
MERGE (elb)-[r:SUBNET]->(subnet)
|
|
65
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
66
|
-
SET r.lastupdated = $update_tag
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
for subnet_id in subnets_data:
|
|
70
|
-
neo4j_session.run(
|
|
71
|
-
ingest_load_balancer_subnet,
|
|
72
|
-
ID=load_balancer_id,
|
|
73
|
-
SUBNET_ID=subnet_id,
|
|
74
|
-
update_tag=update_tag,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@timeit
|
|
79
|
-
def load_load_balancers(
|
|
80
|
-
neo4j_session: neo4j.Session, data: List[Dict], region: str, current_aws_account_id: str,
|
|
135
|
+
def load_load_balancer_listeners(
|
|
136
|
+
neo4j_session: neo4j.Session, data: list[dict], region: str, current_aws_account_id: str,
|
|
81
137
|
update_tag: int,
|
|
82
138
|
) -> None:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
MERGE (aa)-[r:RESOURCE]->(elb)
|
|
92
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
93
|
-
SET r.lastupdated = $update_tag
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
ingest_load_balancersource_security_group = """
|
|
97
|
-
MATCH (elb:LoadBalancer{id: $ID}),
|
|
98
|
-
(group:EC2SecurityGroup{name: $GROUP_NAME})
|
|
99
|
-
MERGE (elb)-[r:SOURCE_SECURITY_GROUP]->(group)
|
|
100
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
101
|
-
SET r.lastupdated = $update_tag
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
ingest_load_balancer_security_group = """
|
|
105
|
-
MATCH (elb:LoadBalancer{id: $ID}),
|
|
106
|
-
(group:EC2SecurityGroup{groupid: $GROUP_ID})
|
|
107
|
-
MERGE (elb)-[r:MEMBER_OF_EC2_SECURITY_GROUP]->(group)
|
|
108
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
109
|
-
SET r.lastupdated = $update_tag
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
ingest_instances = """
|
|
113
|
-
MATCH (elb:LoadBalancer{id: $ID}), (instance:EC2Instance{instanceid: $INSTANCE_ID})
|
|
114
|
-
MERGE (elb)-[r:EXPOSE]->(instance)
|
|
115
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
116
|
-
SET r.lastupdated = $update_tag
|
|
117
|
-
WITH instance
|
|
118
|
-
MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID})
|
|
119
|
-
MERGE (aa)-[r:RESOURCE]->(instance)
|
|
120
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
121
|
-
SET r.lastupdated = $update_tag
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
for lb in data:
|
|
125
|
-
load_balancer_id = lb["DNSName"]
|
|
126
|
-
|
|
127
|
-
neo4j_session.run(
|
|
128
|
-
ingest_load_balancer,
|
|
129
|
-
ID=load_balancer_id,
|
|
130
|
-
CREATED_TIME=str(lb["CreatedTime"]),
|
|
131
|
-
NAME=lb["LoadBalancerName"],
|
|
132
|
-
DNS_NAME=load_balancer_id,
|
|
133
|
-
HOSTED_ZONE_NAME=lb.get("CanonicalHostedZoneName"),
|
|
134
|
-
HOSTED_ZONE_NAME_ID=lb.get("CanonicalHostedZoneNameID"),
|
|
135
|
-
SCHEME=lb.get("Scheme", ""),
|
|
136
|
-
AWS_ACCOUNT_ID=current_aws_account_id,
|
|
137
|
-
Region=region,
|
|
138
|
-
update_tag=update_tag,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
if lb["Subnets"]:
|
|
142
|
-
load_load_balancer_subnets(neo4j_session, load_balancer_id, lb["Subnets"], update_tag)
|
|
143
|
-
|
|
144
|
-
if lb["SecurityGroups"]:
|
|
145
|
-
for group in lb["SecurityGroups"]:
|
|
146
|
-
neo4j_session.run(
|
|
147
|
-
ingest_load_balancer_security_group,
|
|
148
|
-
ID=load_balancer_id,
|
|
149
|
-
GROUP_ID=str(group),
|
|
150
|
-
update_tag=update_tag,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
if lb["SourceSecurityGroup"]:
|
|
154
|
-
source_group = lb["SourceSecurityGroup"]
|
|
155
|
-
neo4j_session.run(
|
|
156
|
-
ingest_load_balancersource_security_group,
|
|
157
|
-
ID=load_balancer_id,
|
|
158
|
-
GROUP_NAME=source_group["GroupName"],
|
|
159
|
-
update_tag=update_tag,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
if lb["Instances"]:
|
|
163
|
-
for instance in lb["Instances"]:
|
|
164
|
-
neo4j_session.run(
|
|
165
|
-
ingest_instances,
|
|
166
|
-
ID=load_balancer_id,
|
|
167
|
-
INSTANCE_ID=instance["InstanceId"],
|
|
168
|
-
AWS_ACCOUNT_ID=current_aws_account_id,
|
|
169
|
-
update_tag=update_tag,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
if lb["ListenerDescriptions"]:
|
|
173
|
-
load_load_balancer_listeners(neo4j_session, load_balancer_id, lb["ListenerDescriptions"], update_tag)
|
|
139
|
+
load(
|
|
140
|
+
neo4j_session,
|
|
141
|
+
ELBListenerSchema(),
|
|
142
|
+
data,
|
|
143
|
+
Region=region,
|
|
144
|
+
AWS_ID=current_aws_account_id,
|
|
145
|
+
lastupdated=update_tag,
|
|
146
|
+
)
|
|
174
147
|
|
|
175
148
|
|
|
176
149
|
@timeit
|
|
177
|
-
def cleanup_load_balancers(neo4j_session: neo4j.Session, common_job_parameters:
|
|
178
|
-
|
|
150
|
+
def cleanup_load_balancers(neo4j_session: neo4j.Session, common_job_parameters: dict) -> None:
|
|
151
|
+
GraphJob.from_node_schema(ELBListenerSchema(), common_job_parameters).run(neo4j_session)
|
|
152
|
+
GraphJob.from_node_schema(LoadBalancerSchema(), common_job_parameters).run(neo4j_session)
|
|
179
153
|
|
|
180
154
|
|
|
181
155
|
@timeit
|
|
182
156
|
def sync_load_balancers(
|
|
183
|
-
neo4j_session: neo4j.Session, boto3_session: boto3.session.Session, regions:
|
|
184
|
-
update_tag: int, common_job_parameters:
|
|
157
|
+
neo4j_session: neo4j.Session, boto3_session: boto3.session.Session, regions: list[str], current_aws_account_id: str,
|
|
158
|
+
update_tag: int, common_job_parameters: dict,
|
|
185
159
|
) -> None:
|
|
186
160
|
for region in regions:
|
|
187
161
|
logger.info("Syncing EC2 load balancers for region '%s' in account '%s'.", region, current_aws_account_id)
|
|
188
162
|
data = get_loadbalancer_data(boto3_session, region)
|
|
189
|
-
|
|
163
|
+
transformed_data, listener_data = transform_load_balancer_data(data)
|
|
164
|
+
|
|
165
|
+
load_load_balancers(neo4j_session, transformed_data, region, current_aws_account_id, update_tag)
|
|
166
|
+
load_load_balancer_listeners(neo4j_session, listener_data, region, current_aws_account_id, update_tag)
|
|
167
|
+
|
|
190
168
|
cleanup_load_balancers(neo4j_session, common_job_parameters)
|