cartography 0.101.1rc2__py3-none-any.whl → 0.102.0rc1__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 +38 -0
- cartography/config.py +12 -0
- cartography/data/indexes.cypher +0 -3
- cartography/intel/aws/ec2/launch_templates.py +27 -32
- cartography/intel/aws/ec2/load_balancers.py +126 -148
- cartography/intel/entra/__init__.py +43 -0
- cartography/intel/entra/users.py +205 -0
- cartography/models/aws/ec2/load_balancer_listeners.py +68 -0
- cartography/models/aws/ec2/load_balancers.py +102 -0
- cartography/models/entra/__init__.py +0 -0
- cartography/models/entra/tenant.py +33 -0
- cartography/models/entra/user.py +83 -0
- cartography/sync.py +2 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc1.dist-info}/METADATA +4 -1
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc1.dist-info}/RECORD +20 -13
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc1.dist-info}/WHEEL +0 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc1.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.0rc1'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 102, 0)
|
cartography/cli.py
CHANGED
|
@@ -211,6 +211,30 @@ class CLI:
|
|
|
211
211
|
'The name of environment variable containing Azure Client Secret for Service Principal Authentication.'
|
|
212
212
|
),
|
|
213
213
|
)
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
'--entra-tenant-id',
|
|
216
|
+
type=str,
|
|
217
|
+
default=None,
|
|
218
|
+
help=(
|
|
219
|
+
'Entra Tenant Id for Service Principal Authentication.'
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
'--entra-client-id',
|
|
224
|
+
type=str,
|
|
225
|
+
default=None,
|
|
226
|
+
help=(
|
|
227
|
+
'Entra Client Id for Service Principal Authentication.'
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
'--entra-client-secret-env-var',
|
|
232
|
+
type=str,
|
|
233
|
+
default=None,
|
|
234
|
+
help=(
|
|
235
|
+
'The name of environment variable containing Entra Client Secret for Service Principal Authentication.'
|
|
236
|
+
),
|
|
237
|
+
)
|
|
214
238
|
parser.add_argument(
|
|
215
239
|
'--aws-requested-syncs',
|
|
216
240
|
type=str,
|
|
@@ -615,6 +639,16 @@ class CLI:
|
|
|
615
639
|
else:
|
|
616
640
|
config.azure_client_secret = None
|
|
617
641
|
|
|
642
|
+
# Entra config
|
|
643
|
+
if config.entra_tenant_id and config.entra_client_id and config.entra_client_secret_env_var:
|
|
644
|
+
logger.debug(
|
|
645
|
+
"Reading Client Secret for Entra Authentication from environment variable %s",
|
|
646
|
+
config.entra_client_secret_env_var,
|
|
647
|
+
)
|
|
648
|
+
config.entra_client_secret = os.environ.get(config.entra_client_secret_env_var)
|
|
649
|
+
else:
|
|
650
|
+
config.entra_client_secret = None
|
|
651
|
+
|
|
618
652
|
# Okta config
|
|
619
653
|
if config.okta_org_id and config.okta_api_key_env_var:
|
|
620
654
|
logger.debug(f"Reading API key for Okta from environment variable {config.okta_api_key_env_var}")
|
|
@@ -798,5 +832,9 @@ def main(argv=None):
|
|
|
798
832
|
logging.getLogger('botocore').setLevel(logging.WARNING)
|
|
799
833
|
logging.getLogger('googleapiclient').setLevel(logging.WARNING)
|
|
800
834
|
logging.getLogger('neo4j').setLevel(logging.WARNING)
|
|
835
|
+
logging.getLogger('azure.identity').setLevel(logging.WARNING)
|
|
836
|
+
logging.getLogger('httpx').setLevel(logging.WARNING)
|
|
837
|
+
logging.getLogger('azure.core.pipeline.policies.http_logging_policy').setLevel(logging.WARNING)
|
|
838
|
+
|
|
801
839
|
argv = argv if argv is not None else sys.argv[1:]
|
|
802
840
|
sys.exit(CLI(prog='cartography').main(argv))
|
cartography/config.py
CHANGED
|
@@ -41,6 +41,12 @@ class Config:
|
|
|
41
41
|
:param azure_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
|
|
42
42
|
:type azure_client_secret: str
|
|
43
43
|
:param azure_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
|
|
44
|
+
:type entra_tenant_id: str
|
|
45
|
+
:param entra_tenant_id: Tenant Id for connecting in a Service Principal Authentication approach. Optional.
|
|
46
|
+
:type entra_client_id: str
|
|
47
|
+
:param entra_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
|
|
48
|
+
:type entra_client_secret: str
|
|
49
|
+
:param entra_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
|
|
44
50
|
:type aws_requested_syncs: str
|
|
45
51
|
:param aws_requested_syncs: Comma-separated list of AWS resources to sync. Optional.
|
|
46
52
|
:type analysis_job_directory: str
|
|
@@ -133,6 +139,9 @@ class Config:
|
|
|
133
139
|
azure_tenant_id=None,
|
|
134
140
|
azure_client_id=None,
|
|
135
141
|
azure_client_secret=None,
|
|
142
|
+
entra_tenant_id=None,
|
|
143
|
+
entra_client_id=None,
|
|
144
|
+
entra_client_secret=None,
|
|
136
145
|
aws_requested_syncs=None,
|
|
137
146
|
analysis_job_directory=None,
|
|
138
147
|
oci_sync_all_profiles=None,
|
|
@@ -191,6 +200,9 @@ class Config:
|
|
|
191
200
|
self.azure_tenant_id = azure_tenant_id
|
|
192
201
|
self.azure_client_id = azure_client_id
|
|
193
202
|
self.azure_client_secret = azure_client_secret
|
|
203
|
+
self.entra_tenant_id = entra_tenant_id
|
|
204
|
+
self.entra_client_id = entra_client_id
|
|
205
|
+
self.entra_client_secret = entra_client_secret
|
|
194
206
|
self.aws_requested_syncs = aws_requested_syncs
|
|
195
207
|
self.analysis_job_directory = analysis_job_directory
|
|
196
208
|
self.oci_sync_all_profiles = oci_sync_all_profiles
|
cartography/data/indexes.cypher
CHANGED
|
@@ -191,9 +191,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:KMSGrant) ON (n.lastupdated);
|
|
|
191
191
|
CREATE INDEX IF NOT EXISTS FOR (n:LaunchConfiguration) ON (n.id);
|
|
192
192
|
CREATE INDEX IF NOT EXISTS FOR (n:LaunchConfiguration) ON (n.name);
|
|
193
193
|
CREATE INDEX IF NOT EXISTS FOR (n:LaunchConfiguration) ON (n.lastupdated);
|
|
194
|
-
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancer) ON (n.dnsname);
|
|
195
|
-
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancer) ON (n.id);
|
|
196
|
-
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancer) ON (n.lastupdated);
|
|
197
194
|
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancerV2) ON (n.dnsname);
|
|
198
195
|
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancerV2) ON (n.id);
|
|
199
196
|
CREATE INDEX IF NOT EXISTS FOR (n:LoadBalancerV2) ON (n.lastupdated);
|
|
@@ -37,37 +37,18 @@ def get_launch_template_versions(
|
|
|
37
37
|
boto3_session: boto3.session.Session,
|
|
38
38
|
region: str,
|
|
39
39
|
launch_templates: list[dict[str, Any]],
|
|
40
|
-
) ->
|
|
41
|
-
|
|
42
|
-
found_templates: list[dict[str, Any]] = []
|
|
40
|
+
) -> list[dict[str, Any]]:
|
|
41
|
+
template_versions: list[dict[str, Any]] = []
|
|
43
42
|
for template in launch_templates:
|
|
44
43
|
launch_template_id = template['LaunchTemplateId']
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
region,
|
|
50
|
-
)
|
|
51
|
-
# If the call succeeded, the template still exists.
|
|
52
|
-
# Add it and its versions (list might be empty if no versions exist).
|
|
53
|
-
found_templates.append(template)
|
|
54
|
-
found_versions.extend(versions)
|
|
55
|
-
except botocore.exceptions.ClientError as e:
|
|
56
|
-
if e.response['Error']['Code'] == 'InvalidLaunchTemplateId.NotFound':
|
|
57
|
-
logger.warning(
|
|
58
|
-
"Launch template %s no longer exists in region %s, skipping.",
|
|
59
|
-
launch_template_id, region,
|
|
60
|
-
)
|
|
61
|
-
# Skip this template, don't add it or its versions
|
|
62
|
-
continue
|
|
63
|
-
else:
|
|
64
|
-
# Re-raise any other client error
|
|
65
|
-
raise
|
|
66
|
-
|
|
67
|
-
return found_versions, found_templates
|
|
44
|
+
versions = get_launch_template_versions_by_template(boto3_session, launch_template_id, region)
|
|
45
|
+
template_versions.extend(versions)
|
|
46
|
+
|
|
47
|
+
return template_versions
|
|
68
48
|
|
|
69
49
|
|
|
70
50
|
@timeit
|
|
51
|
+
@aws_handle_regions
|
|
71
52
|
def get_launch_template_versions_by_template(
|
|
72
53
|
boto3_session: boto3.session.Session,
|
|
73
54
|
launch_template_id: str,
|
|
@@ -76,15 +57,27 @@ def get_launch_template_versions_by_template(
|
|
|
76
57
|
client = boto3_session.client('ec2', region_name=region, config=get_botocore_config())
|
|
77
58
|
v_paginator = client.get_paginator('describe_launch_template_versions')
|
|
78
59
|
template_versions = []
|
|
79
|
-
|
|
80
|
-
|
|
60
|
+
try:
|
|
61
|
+
for versions in v_paginator.paginate(LaunchTemplateId=launch_template_id):
|
|
62
|
+
template_versions.extend(versions['LaunchTemplateVersions'])
|
|
63
|
+
except botocore.exceptions.ClientError as e:
|
|
64
|
+
error_code = e.response['Error']['Code']
|
|
65
|
+
if error_code == 'InvalidLaunchTemplateId.NotFound':
|
|
66
|
+
logger.warning("Launch template %s no longer exists in region %s", launch_template_id, region)
|
|
67
|
+
else:
|
|
68
|
+
raise
|
|
81
69
|
return template_versions
|
|
82
70
|
|
|
83
71
|
|
|
84
|
-
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}
|
|
85
74
|
result: list[dict[str, Any]] = []
|
|
86
75
|
for template in templates:
|
|
76
|
+
if template['LaunchTemplateId'] not in valid_template_ids:
|
|
77
|
+
continue
|
|
78
|
+
|
|
87
79
|
current = template.copy()
|
|
80
|
+
# Convert CreateTime to timestamp string
|
|
88
81
|
current['CreateTime'] = str(int(current['CreateTime'].timestamp()))
|
|
89
82
|
result.append(current)
|
|
90
83
|
return result
|
|
@@ -176,11 +169,13 @@ def sync_ec2_launch_templates(
|
|
|
176
169
|
for region in regions:
|
|
177
170
|
logger.info(f"Syncing launch templates for region '{region}' in account '{current_aws_account_id}'.")
|
|
178
171
|
templates = get_launch_templates(boto3_session, region)
|
|
179
|
-
versions
|
|
172
|
+
versions = get_launch_template_versions(boto3_session, region, templates)
|
|
180
173
|
|
|
181
|
-
# Transform and load
|
|
182
|
-
transformed_templates = transform_launch_templates(
|
|
174
|
+
# Transform and load the templates that have versions
|
|
175
|
+
transformed_templates = transform_launch_templates(templates, versions)
|
|
183
176
|
load_launch_templates(neo4j_session, transformed_templates, region, current_aws_account_id, update_tag)
|
|
177
|
+
|
|
178
|
+
# Transform and load the versions
|
|
184
179
|
transformed_versions = transform_launch_template_versions(versions)
|
|
185
180
|
load_launch_template_versions(neo4j_session, transformed_versions, region, current_aws_account_id, update_tag)
|
|
186
181
|
|
|
@@ -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)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
|
|
6
|
+
from cartography.config import Config
|
|
7
|
+
from cartography.intel.entra.users import sync_entra_users
|
|
8
|
+
from cartography.util import timeit
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@timeit
|
|
14
|
+
def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
15
|
+
"""
|
|
16
|
+
If this module is configured, perform ingestion of Entra data. Otherwise warn and exit
|
|
17
|
+
:param neo4j_session: Neo4J session for database interface
|
|
18
|
+
:param config: A cartography.config object
|
|
19
|
+
:return: None
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if not config.entra_tenant_id or not config.entra_client_id or not config.entra_client_secret:
|
|
23
|
+
logger.info(
|
|
24
|
+
'Entra import is not configured - skipping this module. '
|
|
25
|
+
'See docs to configure.',
|
|
26
|
+
)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
common_job_parameters = {
|
|
30
|
+
"UPDATE_TAG": config.update_tag,
|
|
31
|
+
"TENANT_ID": config.entra_tenant_id,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
asyncio.run(
|
|
35
|
+
sync_entra_users(
|
|
36
|
+
neo4j_session,
|
|
37
|
+
config.entra_tenant_id,
|
|
38
|
+
config.entra_client_id,
|
|
39
|
+
config.entra_client_secret,
|
|
40
|
+
config.update_tag,
|
|
41
|
+
common_job_parameters,
|
|
42
|
+
),
|
|
43
|
+
)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
from azure.identity import ClientSecretCredential
|
|
6
|
+
from msgraph import GraphServiceClient
|
|
7
|
+
from msgraph.generated.models.organization import Organization
|
|
8
|
+
from msgraph.generated.models.user import User
|
|
9
|
+
|
|
10
|
+
from cartography.client.core.tx import load
|
|
11
|
+
from cartography.graph.job import GraphJob
|
|
12
|
+
from cartography.models.entra.tenant import EntraTenantSchema
|
|
13
|
+
from cartography.models.entra.user import EntraUserSchema
|
|
14
|
+
from cartography.util import timeit
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@timeit
|
|
20
|
+
async def get_tenant(client: GraphServiceClient) -> Organization:
|
|
21
|
+
"""
|
|
22
|
+
Get tenant information from Microsoft Graph API
|
|
23
|
+
"""
|
|
24
|
+
org = await client.organization.get()
|
|
25
|
+
return org.value[0] # Get the first (and typically only) tenant
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@timeit
|
|
29
|
+
async def get_users(client: GraphServiceClient) -> list[User]:
|
|
30
|
+
"""
|
|
31
|
+
Get all users from Microsoft Graph API with pagination support
|
|
32
|
+
"""
|
|
33
|
+
all_users: list[User] = []
|
|
34
|
+
request_configuration = client.users.UsersRequestBuilderGetRequestConfiguration(
|
|
35
|
+
query_parameters=client.users.UsersRequestBuilderGetQueryParameters(
|
|
36
|
+
# Request more items per page to reduce number of API calls
|
|
37
|
+
top=999,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
page = await client.users.get(request_configuration=request_configuration)
|
|
42
|
+
while page:
|
|
43
|
+
all_users.extend(page.value)
|
|
44
|
+
if not page.odata_next_link:
|
|
45
|
+
break
|
|
46
|
+
page = await client.users.with_url(page.odata_next_link).get()
|
|
47
|
+
|
|
48
|
+
return all_users
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@timeit
|
|
52
|
+
def transform_users(users: list[User]) -> list[dict[str, Any]]:
|
|
53
|
+
"""
|
|
54
|
+
Transform the API response into the format expected by our schema
|
|
55
|
+
"""
|
|
56
|
+
result: list[dict[str, Any]] = []
|
|
57
|
+
for user in users:
|
|
58
|
+
transformed_user = {
|
|
59
|
+
'id': user.id,
|
|
60
|
+
'user_principal_name': user.user_principal_name,
|
|
61
|
+
'display_name': user.display_name,
|
|
62
|
+
'given_name': user.given_name,
|
|
63
|
+
'surname': user.surname,
|
|
64
|
+
'mail': user.mail,
|
|
65
|
+
'other_mails': user.other_mails,
|
|
66
|
+
'preferred_language': user.preferred_language,
|
|
67
|
+
'preferred_name': user.preferred_name,
|
|
68
|
+
'state': user.state,
|
|
69
|
+
'usage_location': user.usage_location,
|
|
70
|
+
'user_type': user.user_type,
|
|
71
|
+
'show_in_address_list': user.show_in_address_list,
|
|
72
|
+
'sign_in_sessions_valid_from_date_time': user.sign_in_sessions_valid_from_date_time,
|
|
73
|
+
'security_identifier': user.on_premises_security_identifier,
|
|
74
|
+
'account_enabled': user.account_enabled,
|
|
75
|
+
'age_group': user.age_group,
|
|
76
|
+
'business_phones': user.business_phones,
|
|
77
|
+
'city': user.city,
|
|
78
|
+
'company_name': user.company_name,
|
|
79
|
+
'consent_provided_for_minor': user.consent_provided_for_minor,
|
|
80
|
+
'country': user.country,
|
|
81
|
+
'created_date_time': user.created_date_time,
|
|
82
|
+
'creation_type': user.creation_type,
|
|
83
|
+
'deleted_date_time': user.deleted_date_time,
|
|
84
|
+
'department': user.department,
|
|
85
|
+
'employee_id': user.employee_id,
|
|
86
|
+
'employee_type': user.employee_type,
|
|
87
|
+
'external_user_state': user.external_user_state,
|
|
88
|
+
'external_user_state_change_date_time': user.external_user_state_change_date_time,
|
|
89
|
+
'hire_date': user.hire_date,
|
|
90
|
+
'is_management_restricted': user.is_management_restricted,
|
|
91
|
+
'is_resource_account': user.is_resource_account,
|
|
92
|
+
'job_title': user.job_title,
|
|
93
|
+
'last_password_change_date_time': user.last_password_change_date_time,
|
|
94
|
+
'mail_nickname': user.mail_nickname,
|
|
95
|
+
'office_location': user.office_location,
|
|
96
|
+
'on_premises_distinguished_name': user.on_premises_distinguished_name,
|
|
97
|
+
'on_premises_domain_name': user.on_premises_domain_name,
|
|
98
|
+
'on_premises_immutable_id': user.on_premises_immutable_id,
|
|
99
|
+
'on_premises_last_sync_date_time': user.on_premises_last_sync_date_time,
|
|
100
|
+
'on_premises_sam_account_name': user.on_premises_sam_account_name,
|
|
101
|
+
'on_premises_security_identifier': user.on_premises_security_identifier,
|
|
102
|
+
'on_premises_sync_enabled': user.on_premises_sync_enabled,
|
|
103
|
+
'on_premises_user_principal_name': user.on_premises_user_principal_name,
|
|
104
|
+
}
|
|
105
|
+
result.append(transformed_user)
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@timeit
|
|
110
|
+
def transform_tenant(tenant: Organization, tenant_id: str) -> dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Transform the tenant data into the format expected by our schema
|
|
113
|
+
"""
|
|
114
|
+
return {
|
|
115
|
+
'id': tenant_id,
|
|
116
|
+
'created_date_time': tenant.created_date_time,
|
|
117
|
+
'default_usage_location': tenant.default_usage_location,
|
|
118
|
+
'deleted_date_time': tenant.deleted_date_time,
|
|
119
|
+
'display_name': tenant.display_name,
|
|
120
|
+
'marketing_notification_emails': tenant.marketing_notification_emails,
|
|
121
|
+
'mobile_device_management_authority': tenant.mobile_device_management_authority,
|
|
122
|
+
'on_premises_last_sync_date_time': tenant.on_premises_last_sync_date_time,
|
|
123
|
+
'on_premises_sync_enabled': tenant.on_premises_sync_enabled,
|
|
124
|
+
'partner_tenant_type': tenant.partner_tenant_type,
|
|
125
|
+
'postal_code': tenant.postal_code,
|
|
126
|
+
'preferred_language': tenant.preferred_language,
|
|
127
|
+
'state': tenant.state,
|
|
128
|
+
'street': tenant.street,
|
|
129
|
+
'tenant_type': tenant.tenant_type,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@timeit
|
|
134
|
+
def load_tenant(
|
|
135
|
+
neo4j_session: neo4j.Session,
|
|
136
|
+
tenant: dict[str, Any],
|
|
137
|
+
update_tag: int,
|
|
138
|
+
) -> None:
|
|
139
|
+
load(
|
|
140
|
+
neo4j_session,
|
|
141
|
+
EntraTenantSchema(),
|
|
142
|
+
[tenant],
|
|
143
|
+
lastupdated=update_tag,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@timeit
|
|
148
|
+
def load_users(
|
|
149
|
+
neo4j_session: neo4j.Session,
|
|
150
|
+
users: list[dict[str, Any]],
|
|
151
|
+
tenant_id: str,
|
|
152
|
+
update_tag: int,
|
|
153
|
+
) -> None:
|
|
154
|
+
logger.info(f"Loading {len(users)} Entra users")
|
|
155
|
+
load(
|
|
156
|
+
neo4j_session,
|
|
157
|
+
EntraUserSchema(),
|
|
158
|
+
users,
|
|
159
|
+
lastupdated=update_tag,
|
|
160
|
+
TENANT_ID=tenant_id,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cleanup(neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]) -> None:
|
|
165
|
+
GraphJob.from_node_schema(EntraUserSchema(), common_job_parameters).run(neo4j_session)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@timeit
|
|
169
|
+
async def sync_entra_users(
|
|
170
|
+
neo4j_session: neo4j.Session,
|
|
171
|
+
tenant_id: str,
|
|
172
|
+
client_id: str,
|
|
173
|
+
client_secret: str,
|
|
174
|
+
update_tag: int,
|
|
175
|
+
common_job_parameters: dict[str, Any],
|
|
176
|
+
) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Sync Entra users and tenant information
|
|
179
|
+
:param neo4j_session: Neo4J session for database interface
|
|
180
|
+
:param tenant_id: Entra tenant ID
|
|
181
|
+
:param client_id: Entra application client ID
|
|
182
|
+
:param client_secret: Entra application client secret
|
|
183
|
+
:param update_tag: Timestamp used to determine data freshness
|
|
184
|
+
:param common_job_parameters: dict of other job parameters to carry to sub-jobs
|
|
185
|
+
:return: None
|
|
186
|
+
"""
|
|
187
|
+
# Initialize Graph client
|
|
188
|
+
credential = ClientSecretCredential(
|
|
189
|
+
tenant_id=tenant_id,
|
|
190
|
+
client_id=client_id,
|
|
191
|
+
client_secret=client_secret,
|
|
192
|
+
)
|
|
193
|
+
client = GraphServiceClient(credential, scopes=['https://graph.microsoft.com/.default'])
|
|
194
|
+
|
|
195
|
+
# Get tenant information
|
|
196
|
+
tenant = await get_tenant(client)
|
|
197
|
+
users = await get_users(client)
|
|
198
|
+
|
|
199
|
+
transformed_users = transform_users(users)
|
|
200
|
+
transformed_tenant = transform_tenant(tenant, tenant_id)
|
|
201
|
+
|
|
202
|
+
load_tenant(neo4j_session, transformed_tenant, update_tag)
|
|
203
|
+
load_users(neo4j_session, transformed_users, tenant_id, update_tag)
|
|
204
|
+
|
|
205
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cartography.models.core.common import PropertyRef
|
|
4
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
5
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
6
|
+
from cartography.models.core.nodes import ExtraNodeLabels
|
|
7
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
8
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
9
|
+
from cartography.models.core.relationships import LinkDirection
|
|
10
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
11
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
12
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ELBListenerNodeProperties(CartographyNodeProperties):
|
|
17
|
+
id: PropertyRef = PropertyRef('id')
|
|
18
|
+
port: PropertyRef = PropertyRef('port')
|
|
19
|
+
protocol: PropertyRef = PropertyRef('protocol')
|
|
20
|
+
instance_port: PropertyRef = PropertyRef('instance_port')
|
|
21
|
+
instance_protocol: PropertyRef = PropertyRef('instance_protocol')
|
|
22
|
+
policy_names: PropertyRef = PropertyRef('policy_names')
|
|
23
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ELBListenerToLoadBalancerRelProperties(CartographyRelProperties):
|
|
28
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ELBListenerToLoadBalancer(CartographyRelSchema):
|
|
33
|
+
target_node_label: str = 'LoadBalancer'
|
|
34
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
35
|
+
{'id': PropertyRef('LoadBalancerId')},
|
|
36
|
+
)
|
|
37
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
38
|
+
rel_label: str = "ELB_LISTENER"
|
|
39
|
+
properties: ELBListenerToLoadBalancerRelProperties = ELBListenerToLoadBalancerRelProperties()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ELBListenerToAWSAccountRelProperties(CartographyRelProperties):
|
|
44
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ELBListenerToAWSAccount(CartographyRelSchema):
|
|
49
|
+
target_node_label: str = 'AWSAccount'
|
|
50
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
51
|
+
{'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
|
|
52
|
+
)
|
|
53
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
54
|
+
rel_label: str = "RESOURCE"
|
|
55
|
+
properties: ELBListenerToAWSAccountRelProperties = ELBListenerToAWSAccountRelProperties()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class ELBListenerSchema(CartographyNodeSchema):
|
|
60
|
+
label: str = 'ELBListener'
|
|
61
|
+
properties: ELBListenerNodeProperties = ELBListenerNodeProperties()
|
|
62
|
+
extra_node_labels: ExtraNodeLabels = ExtraNodeLabels(['Endpoint'])
|
|
63
|
+
sub_resource_relationship: ELBListenerToAWSAccount = ELBListenerToAWSAccount()
|
|
64
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
65
|
+
[
|
|
66
|
+
ELBListenerToLoadBalancer(),
|
|
67
|
+
],
|
|
68
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cartography.models.core.common import PropertyRef
|
|
4
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
5
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
6
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
7
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
8
|
+
from cartography.models.core.relationships import LinkDirection
|
|
9
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
10
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
11
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class LoadBalancerNodeProperties(CartographyNodeProperties):
|
|
16
|
+
id: PropertyRef = PropertyRef('id')
|
|
17
|
+
name: PropertyRef = PropertyRef('name')
|
|
18
|
+
dnsname: PropertyRef = PropertyRef('dnsname', extra_index=True)
|
|
19
|
+
canonicalhostedzonename: PropertyRef = PropertyRef('canonicalhostedzonename')
|
|
20
|
+
canonicalhostedzonenameid: PropertyRef = PropertyRef('canonicalhostedzonenameid')
|
|
21
|
+
scheme: PropertyRef = PropertyRef('scheme')
|
|
22
|
+
region: PropertyRef = PropertyRef('Region', set_in_kwargs=True)
|
|
23
|
+
createdtime: PropertyRef = PropertyRef('createdtime')
|
|
24
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class LoadBalancerToAWSAccountRelProperties(CartographyRelProperties):
|
|
29
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class LoadBalancerToAWSAccount(CartographyRelSchema):
|
|
34
|
+
target_node_label: str = 'AWSAccount'
|
|
35
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
36
|
+
{'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
|
|
37
|
+
)
|
|
38
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
39
|
+
rel_label: str = "RESOURCE"
|
|
40
|
+
properties: LoadBalancerToAWSAccountRelProperties = LoadBalancerToAWSAccountRelProperties()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class LoadBalancerToSecurityGroupRelProperties(CartographyRelProperties):
|
|
45
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class LoadBalancerToSourceSecurityGroup(CartographyRelSchema):
|
|
50
|
+
target_node_label: str = 'EC2SecurityGroup'
|
|
51
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
52
|
+
{'name': PropertyRef('GROUP_NAME')},
|
|
53
|
+
)
|
|
54
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
55
|
+
rel_label: str = "SOURCE_SECURITY_GROUP"
|
|
56
|
+
properties: LoadBalancerToSecurityGroupRelProperties = LoadBalancerToSecurityGroupRelProperties()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class LoadBalancerToEC2SecurityGroupRelProperties(CartographyRelProperties):
|
|
61
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class LoadBalancerToEC2SecurityGroup(CartographyRelSchema):
|
|
66
|
+
target_node_label: str = 'EC2SecurityGroup'
|
|
67
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
68
|
+
{'groupid': PropertyRef('GROUP_IDS', one_to_many=True)},
|
|
69
|
+
)
|
|
70
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
71
|
+
rel_label: str = "MEMBER_OF_EC2_SECURITY_GROUP"
|
|
72
|
+
properties: LoadBalancerToEC2SecurityGroupRelProperties = LoadBalancerToEC2SecurityGroupRelProperties()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class LoadBalancerToEC2InstanceRelProperties(CartographyRelProperties):
|
|
77
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class LoadBalancerToEC2Instance(CartographyRelSchema):
|
|
82
|
+
target_node_label: str = 'EC2Instance'
|
|
83
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
84
|
+
{'instanceid': PropertyRef('INSTANCE_IDS', one_to_many=True)},
|
|
85
|
+
)
|
|
86
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
87
|
+
rel_label: str = "EXPOSE"
|
|
88
|
+
properties: LoadBalancerToEC2InstanceRelProperties = LoadBalancerToEC2InstanceRelProperties()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class LoadBalancerSchema(CartographyNodeSchema):
|
|
93
|
+
label: str = 'LoadBalancer'
|
|
94
|
+
properties: LoadBalancerNodeProperties = LoadBalancerNodeProperties()
|
|
95
|
+
sub_resource_relationship: LoadBalancerToAWSAccount = LoadBalancerToAWSAccount()
|
|
96
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
97
|
+
[
|
|
98
|
+
LoadBalancerToSourceSecurityGroup(),
|
|
99
|
+
LoadBalancerToEC2SecurityGroup(),
|
|
100
|
+
LoadBalancerToEC2Instance(),
|
|
101
|
+
],
|
|
102
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cartography.models.core.common import PropertyRef
|
|
4
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
5
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
6
|
+
from cartography.models.core.nodes import ExtraNodeLabels
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class EntraTenantNodeProperties(CartographyNodeProperties):
|
|
11
|
+
id: PropertyRef = PropertyRef('id')
|
|
12
|
+
created_date_time: PropertyRef = PropertyRef('created_date_time')
|
|
13
|
+
default_usage_location: PropertyRef = PropertyRef('default_usage_location')
|
|
14
|
+
deleted_date_time: PropertyRef = PropertyRef('deleted_date_time')
|
|
15
|
+
display_name: PropertyRef = PropertyRef('display_name')
|
|
16
|
+
marketing_notification_emails: PropertyRef = PropertyRef('marketing_notification_emails')
|
|
17
|
+
mobile_device_management_authority: PropertyRef = PropertyRef('mobile_device_management_authority')
|
|
18
|
+
on_premises_last_sync_date_time: PropertyRef = PropertyRef('on_premises_last_sync_date_time')
|
|
19
|
+
on_premises_sync_enabled: PropertyRef = PropertyRef('on_premises_sync_enabled')
|
|
20
|
+
partner_tenant_type: PropertyRef = PropertyRef('partner_tenant_type')
|
|
21
|
+
postal_code: PropertyRef = PropertyRef('postal_code')
|
|
22
|
+
preferred_language: PropertyRef = PropertyRef('preferred_language')
|
|
23
|
+
state: PropertyRef = PropertyRef('state')
|
|
24
|
+
street: PropertyRef = PropertyRef('street')
|
|
25
|
+
tenant_type: PropertyRef = PropertyRef('tenant_type')
|
|
26
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class EntraTenantSchema(CartographyNodeSchema):
|
|
31
|
+
label: str = 'AzureTenant'
|
|
32
|
+
properties: EntraTenantNodeProperties = EntraTenantNodeProperties()
|
|
33
|
+
extra_node_labels: ExtraNodeLabels = ExtraNodeLabels(['EntraTenant'])
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cartography.models.core.common import PropertyRef
|
|
4
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
5
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
6
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
7
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
8
|
+
from cartography.models.core.relationships import LinkDirection
|
|
9
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
10
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class EntraUserNodeProperties(CartographyNodeProperties):
|
|
15
|
+
id: PropertyRef = PropertyRef('id')
|
|
16
|
+
user_principal_name: PropertyRef = PropertyRef('user_principal_name')
|
|
17
|
+
display_name: PropertyRef = PropertyRef('display_name')
|
|
18
|
+
given_name: PropertyRef = PropertyRef('given_name')
|
|
19
|
+
surname: PropertyRef = PropertyRef('surname')
|
|
20
|
+
# The underlying datatype calls this 'mail' but everything else in cartography uses 'email'
|
|
21
|
+
email: PropertyRef = PropertyRef('mail', extra_index=True)
|
|
22
|
+
other_mails: PropertyRef = PropertyRef('other_mails')
|
|
23
|
+
preferred_language: PropertyRef = PropertyRef('preferred_language')
|
|
24
|
+
preferred_name: PropertyRef = PropertyRef('preferred_name')
|
|
25
|
+
state: PropertyRef = PropertyRef('state')
|
|
26
|
+
usage_location: PropertyRef = PropertyRef('usage_location')
|
|
27
|
+
user_type: PropertyRef = PropertyRef('user_type')
|
|
28
|
+
show_in_address_list: PropertyRef = PropertyRef('show_in_address_list')
|
|
29
|
+
sign_in_sessions_valid_from_date_time: PropertyRef = PropertyRef('sign_in_sessions_valid_from_date_time')
|
|
30
|
+
security_identifier: PropertyRef = PropertyRef('security_identifier')
|
|
31
|
+
account_enabled: PropertyRef = PropertyRef('account_enabled')
|
|
32
|
+
city: PropertyRef = PropertyRef('city')
|
|
33
|
+
company_name: PropertyRef = PropertyRef('company_name')
|
|
34
|
+
consent_provided_for_minor: PropertyRef = PropertyRef('consent_provided_for_minor')
|
|
35
|
+
country: PropertyRef = PropertyRef('country')
|
|
36
|
+
created_date_time: PropertyRef = PropertyRef('created_date_time')
|
|
37
|
+
creation_type: PropertyRef = PropertyRef('creation_type')
|
|
38
|
+
deleted_date_time: PropertyRef = PropertyRef('deleted_date_time')
|
|
39
|
+
department: PropertyRef = PropertyRef('department')
|
|
40
|
+
employee_id: PropertyRef = PropertyRef('employee_id')
|
|
41
|
+
employee_type: PropertyRef = PropertyRef('employee_type')
|
|
42
|
+
external_user_state: PropertyRef = PropertyRef('external_user_state')
|
|
43
|
+
external_user_state_change_date_time: PropertyRef = PropertyRef('external_user_state_change_date_time')
|
|
44
|
+
hire_date: PropertyRef = PropertyRef('hire_date')
|
|
45
|
+
is_management_restricted: PropertyRef = PropertyRef('is_management_restricted')
|
|
46
|
+
is_resource_account: PropertyRef = PropertyRef('is_resource_account')
|
|
47
|
+
job_title: PropertyRef = PropertyRef('job_title')
|
|
48
|
+
last_password_change_date_time: PropertyRef = PropertyRef('last_password_change_date_time')
|
|
49
|
+
mail_nickname: PropertyRef = PropertyRef('mail_nickname')
|
|
50
|
+
office_location: PropertyRef = PropertyRef('office_location')
|
|
51
|
+
on_premises_distinguished_name: PropertyRef = PropertyRef('on_premises_distinguished_name')
|
|
52
|
+
on_premises_domain_name: PropertyRef = PropertyRef('on_premises_domain_name')
|
|
53
|
+
on_premises_immutable_id: PropertyRef = PropertyRef('on_premises_immutable_id')
|
|
54
|
+
on_premises_last_sync_date_time: PropertyRef = PropertyRef('on_premises_last_sync_date_time')
|
|
55
|
+
on_premises_sam_account_name: PropertyRef = PropertyRef('on_premises_sam_account_name')
|
|
56
|
+
on_premises_security_identifier: PropertyRef = PropertyRef('on_premises_security_identifier')
|
|
57
|
+
on_premises_sync_enabled: PropertyRef = PropertyRef('on_premises_sync_enabled')
|
|
58
|
+
on_premises_user_principal_name: PropertyRef = PropertyRef('on_premises_user_principal_name')
|
|
59
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class EntraTenantToUserRelProperties(CartographyRelProperties):
|
|
64
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
# (:EntraUser)<-[:RESOURCE]-(:AzureTenant)
|
|
69
|
+
class EntraUserToTenantRel(CartographyRelSchema):
|
|
70
|
+
target_node_label: str = 'AzureTenant'
|
|
71
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
72
|
+
{'id': PropertyRef('TENANT_ID', set_in_kwargs=True)},
|
|
73
|
+
)
|
|
74
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
75
|
+
rel_label: str = "RESOURCE"
|
|
76
|
+
properties: EntraTenantToUserRelProperties = EntraTenantToUserRelProperties()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class EntraUserSchema(CartographyNodeSchema):
|
|
81
|
+
label: str = 'EntraUser'
|
|
82
|
+
properties: EntraUserNodeProperties = EntraUserNodeProperties()
|
|
83
|
+
sub_resource_relationship: EntraUserToTenantRel = EntraUserToTenantRel()
|
cartography/sync.py
CHANGED
|
@@ -20,6 +20,7 @@ import cartography.intel.crowdstrike
|
|
|
20
20
|
import cartography.intel.cve
|
|
21
21
|
import cartography.intel.digitalocean
|
|
22
22
|
import cartography.intel.duo
|
|
23
|
+
import cartography.intel.entra
|
|
23
24
|
import cartography.intel.gcp
|
|
24
25
|
import cartography.intel.github
|
|
25
26
|
import cartography.intel.gsuite
|
|
@@ -42,6 +43,7 @@ TOP_LEVEL_MODULES = OrderedDict({ # preserve order so that the default sync alw
|
|
|
42
43
|
'create-indexes': cartography.intel.create_indexes.run,
|
|
43
44
|
'aws': cartography.intel.aws.start_aws_ingestion,
|
|
44
45
|
'azure': cartography.intel.azure.start_azure_ingestion,
|
|
46
|
+
'entra': cartography.intel.entra.start_entra_ingestion,
|
|
45
47
|
'crowdstrike': cartography.intel.crowdstrike.start_crowdstrike_ingestion,
|
|
46
48
|
'gcp': cartography.intel.gcp.start_gcp_ingestion,
|
|
47
49
|
'gsuite': cartography.intel.gsuite.start_gsuite_ingestion,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cartography
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.102.0rc1
|
|
4
4
|
Summary: Explore assets and their relationships across your technical infrastructure.
|
|
5
5
|
Maintainer: Cartography Contributors
|
|
6
6
|
License: apache2
|
|
@@ -47,6 +47,7 @@ Requires-Dist: msrestazure>=0.6.4
|
|
|
47
47
|
Requires-Dist: azure-mgmt-storage>=16.0.0
|
|
48
48
|
Requires-Dist: azure-mgmt-sql<=1.0.0
|
|
49
49
|
Requires-Dist: azure-identity>=1.5.0
|
|
50
|
+
Requires-Dist: msgraph-sdk
|
|
50
51
|
Requires-Dist: kubernetes>=22.6.0
|
|
51
52
|
Requires-Dist: pdpyras>=4.3.0
|
|
52
53
|
Requires-Dist: crowdstrike-falconpy>=0.5.1
|
|
@@ -61,6 +62,7 @@ Requires-Dist: pytest>=6.2.4; extra == "dev"
|
|
|
61
62
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
62
63
|
Requires-Dist: pytest-cov==6.1.1; extra == "dev"
|
|
63
64
|
Requires-Dist: pytest-rerunfailures; extra == "dev"
|
|
65
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
64
66
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
65
67
|
Requires-Dist: types-requests<2.32.0.20250329; extra == "dev"
|
|
66
68
|
Dynamic: license-file
|
|
@@ -97,6 +99,7 @@ You can learn more about the story behind Cartography in our [presentation at BS
|
|
|
97
99
|
- [GitHub](https://cartography-cncf.github.io/cartography/modules/github/index.html) - repos, branches, users, teams
|
|
98
100
|
- [DigitalOcean](https://cartography-cncf.github.io/cartography/modules/digitalocean/index.html)
|
|
99
101
|
- [Microsoft Azure](https://cartography-cncf.github.io/cartography/modules/azure/index.html) - CosmosDB, SQL, Storage, Virtual Machine
|
|
102
|
+
- [Microsoft Entra ID](https://cartography-cncf.github.io/cartography/modules/entra/index.html) - Users
|
|
100
103
|
- [Kubernetes](https://cartography-cncf.github.io/cartography/modules/kubernetes/index.html) - Cluster, Namespace, Service, Pod, Container
|
|
101
104
|
- [PagerDuty](https://cartography-cncf.github.io/cartography/modules/pagerduty/index.html) - Users, teams, services, schedules, escalation policies, integrations, vendors
|
|
102
105
|
- [Crowdstrike Falcon](https://cartography-cncf.github.io/cartography/modules/crowdstrike/index.html) - Hosts, Spotlight vulnerabilities, CVEs
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
cartography/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
cartography/__main__.py,sha256=JftXT_nUPkqcEh8uxCCT4n-OyHYqbldEgrDS-4ygy0U,101
|
|
3
|
-
cartography/_version.py,sha256=
|
|
4
|
-
cartography/cli.py,sha256=-
|
|
5
|
-
cartography/config.py,sha256=
|
|
3
|
+
cartography/_version.py,sha256=q6TYvfOuC2Uhe9vDriWNPGMCELT7yrGBm93mUcBnafo,518
|
|
4
|
+
cartography/cli.py,sha256=-fGIdBx3IwauUeYojsb-NnQPma2wtS7mFRgOETyfCg4,34796
|
|
5
|
+
cartography/config.py,sha256=xXM0OqsDl5Du55C-hr2LgjdtpU1_znPsmAgujrPGPgo,12553
|
|
6
6
|
cartography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
cartography/stats.py,sha256=-KiqrNfUe_39z9TKAQamJwKs5XePnzXscEJocAuNiJs,4420
|
|
8
|
-
cartography/sync.py,sha256=
|
|
8
|
+
cartography/sync.py,sha256=LSDvK2vaMXhuNHvUkdncpDHAGdiJ7eP7-uDVhwLFjKM,9799
|
|
9
9
|
cartography/util.py,sha256=VZgiHcAprn3nGzItee4_TggfsGWxWPTkLN-2MIhYUqM,14999
|
|
10
10
|
cartography/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
cartography/client/aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -13,7 +13,7 @@ cartography/client/aws/iam.py,sha256=dYsGikc36DEsSeR2XVOVFFUDwuU9yWj_EVkpgVYCFgM
|
|
|
13
13
|
cartography/client/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
cartography/client/core/tx.py,sha256=55Cf9DJGHHXQk4HmPOdFwr1eh9Pr1nzmIvs4XoCVr0g,10892
|
|
15
15
|
cartography/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
cartography/data/indexes.cypher,sha256=
|
|
16
|
+
cartography/data/indexes.cypher,sha256=aUHMiLPsEt09W61GyjJHfpkRJ07S2sGcpU9IReYxKC0,26551
|
|
17
17
|
cartography/data/permission_relationships.yaml,sha256=RuKGGc_3ZUQ7ag0MssB8k_zaonCkVM5E8I_svBWTmGc,969
|
|
18
18
|
cartography/data/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
cartography/data/jobs/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -174,9 +174,9 @@ cartography/intel/aws/ec2/images.py,sha256=SLoxcy_PQgNomVMDMdutm0zXJCOLosiHJlN63
|
|
|
174
174
|
cartography/intel/aws/ec2/instances.py,sha256=uI8eVJmeEybS8y_T8CVKAkwxJyVDCH7sbuEJYeWGSWY,12468
|
|
175
175
|
cartography/intel/aws/ec2/internet_gateways.py,sha256=dI-4-85_3DGGZZBcY_DN6XqESx9P26S6jKok314lcnQ,2883
|
|
176
176
|
cartography/intel/aws/ec2/key_pairs.py,sha256=g4imIo_5jk8upq9J4--erg-OZXG2i3cJMe6SnNCYj9s,2635
|
|
177
|
-
cartography/intel/aws/ec2/launch_templates.py,sha256=
|
|
177
|
+
cartography/intel/aws/ec2/launch_templates.py,sha256=JNCT7WKvHcM8Z6D1MDR65GsBiqmd6bMuRQAMu9BWRKY,6634
|
|
178
178
|
cartography/intel/aws/ec2/load_balancer_v2s.py,sha256=95FfQQn740gexINIHDJizOM4OKzRtQT_y2XQMipQ5Dg,8661
|
|
179
|
-
cartography/intel/aws/ec2/load_balancers.py,sha256=
|
|
179
|
+
cartography/intel/aws/ec2/load_balancers.py,sha256=ah9-lXvipzVDjGFqfpNCrEyBfdu-BdDeV2ZcPwJM78M,6013
|
|
180
180
|
cartography/intel/aws/ec2/network_acls.py,sha256=_UiOx79OxcqH0ecRjcVMglAzz5XJ4aVYLlv6dl_ism4,6809
|
|
181
181
|
cartography/intel/aws/ec2/network_interfaces.py,sha256=CzF8PooCYUQ2pk8DR8JDAhkWRUQSBj_27OsIfkL_-Cs,9199
|
|
182
182
|
cartography/intel/aws/ec2/reserved_instances.py,sha256=jv8-VLI5KL8jN1QRI20yim8lzZ7I7wR8a5EF8DckahA,3122
|
|
@@ -220,6 +220,8 @@ cartography/intel/duo/phones.py,sha256=ueJheqSLD2xYcMus5eOiixPYS3_xVjgQzeomjV2a6
|
|
|
220
220
|
cartography/intel/duo/tokens.py,sha256=bEEnjfc4waQnkRHVSnZLAeGE8wHOOZL7FA9m80GGQdQ,2396
|
|
221
221
|
cartography/intel/duo/users.py,sha256=lc7ly_XKeUjJ50szw31WT_GiCrZfGKJv1zVUpmTchh4,4097
|
|
222
222
|
cartography/intel/duo/web_authn_credentials.py,sha256=IbDf3CWqfEyI7f9zJugUvoDd6vZOECfb_7ANZaRYzuk,2636
|
|
223
|
+
cartography/intel/entra/__init__.py,sha256=Qtn2-ZTZA-_3IzopJG1r2F8fkLd2DJ3b1H1ZbeF4xUA,1185
|
|
224
|
+
cartography/intel/entra/users.py,sha256=Tv7LutCEZ4zo3E9MNT8Z8jwiyV2TTzCfvflGP3bz9c0,7523
|
|
223
225
|
cartography/intel/gcp/__init__.py,sha256=sZHPfDCPZFCE5d6aj20Ow4AC0vrFxV7RCn_cMinCDmI,17650
|
|
224
226
|
cartography/intel/gcp/compute.py,sha256=CH2cBdOwbLZCAbkfRJkkI-sFybXVKRWEUGDJANQmvyA,48333
|
|
225
227
|
cartography/intel/gcp/crm.py,sha256=Uw5PILhVFhpM8gq7uu2v7F_YikDW3gsTZ3d7-e8Z1_k,12324
|
|
@@ -296,6 +298,8 @@ cartography/models/aws/ec2/keypair_instance.py,sha256=M1Ru8Z_2izW0cADAnQVVHaKsT_
|
|
|
296
298
|
cartography/models/aws/ec2/launch_configurations.py,sha256=zdfWJEx93HXDXd_IzSEkhvcztkJI7_v_TCE_d8ZNAyI,2764
|
|
297
299
|
cartography/models/aws/ec2/launch_template_versions.py,sha256=RitfnAuAj0XpFsCXkRbtUhHMAi8Vsvmtury231eKvGU,3897
|
|
298
300
|
cartography/models/aws/ec2/launch_templates.py,sha256=GqiwFuMp72LNSt2eQlp2WfdU_vHsom-xKV5AaUewSHQ,2157
|
|
301
|
+
cartography/models/aws/ec2/load_balancer_listeners.py,sha256=M7oYOinOQkEUijJjhs1oCffB5VmLWYSa92tVWKwMSJQ,2879
|
|
302
|
+
cartography/models/aws/ec2/load_balancers.py,sha256=qJbPWePdO2vuyKzcVcSvKtHlEFMBkkUovZ826BaAcwg,4347
|
|
299
303
|
cartography/models/aws/ec2/loadbalancerv2.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
300
304
|
cartography/models/aws/ec2/network_acl_rules.py,sha256=4Rq2J-Dce8J6y9J6YIalmYtuQRWLp652LXO1Xg6XGPE,3951
|
|
301
305
|
cartography/models/aws/ec2/network_acls.py,sha256=pJKsXdMLB8L79lmTYpLJfFJ6p7PWpf3rBN6eW6y-5hY,3419
|
|
@@ -342,6 +346,9 @@ cartography/models/duo/phone.py,sha256=oxgMmwKLRiCWbAhqrTKE4ILseu0j96GugEIV_hchR
|
|
|
342
346
|
cartography/models/duo/token.py,sha256=BS_AvF-TAGzCY9Owtqxr8g_s6716dnzFOO1IwkckmVA,2668
|
|
343
347
|
cartography/models/duo/user.py,sha256=ih3DH_QveAve4cX9dmIwC5gVN6_RNnuLK3bfJ5I9u6g,6554
|
|
344
348
|
cartography/models/duo/web_authn_credential.py,sha256=OcZnfG5zCMlphxSltRcAXQ12hHYJjxrBt6A9L28g7Vk,2920
|
|
349
|
+
cartography/models/entra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
350
|
+
cartography/models/entra/tenant.py,sha256=wxF6DTsLs653ObzPrEOj2xQ-YGtNJhg-E1desPQmmU0,1751
|
|
351
|
+
cartography/models/entra/user.py,sha256=Y6am84AEYdVJHIFUHHF5XSgwCQ-aOekeu4ZABJhnfp0,4765
|
|
345
352
|
cartography/models/gcp/iam.py,sha256=N7OGmnRlkIFZOv0rh3QGGBmYV7WYy3-xeE4Wv7StGOE,3071
|
|
346
353
|
cartography/models/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
347
354
|
cartography/models/github/orgs.py,sha256=EcUmkeyoCJmkmzLsfKdUwwTE0N2IIwyaUrIK32dQybo,1106
|
|
@@ -362,9 +369,9 @@ cartography/models/snipeit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
362
369
|
cartography/models/snipeit/asset.py,sha256=FyRAaeXuZjMy0eUQcSDFcgEAF5lbLMlvqp1Tv9d3Lv4,3238
|
|
363
370
|
cartography/models/snipeit/tenant.py,sha256=p4rFnpNNuF1W5ilGBbexDaETWTwavfb38RcQGoImkQI,679
|
|
364
371
|
cartography/models/snipeit/user.py,sha256=MsB4MiCVNTH6JpESime7cOkB89autZOXQpL6Z0l7L6o,2113
|
|
365
|
-
cartography-0.
|
|
366
|
-
cartography-0.
|
|
367
|
-
cartography-0.
|
|
368
|
-
cartography-0.
|
|
369
|
-
cartography-0.
|
|
370
|
-
cartography-0.
|
|
372
|
+
cartography-0.102.0rc1.dist-info/licenses/LICENSE,sha256=kvLEBRYaQ1RvUni6y7Ti9uHeooqnjPoo6n_-0JO1ETc,11351
|
|
373
|
+
cartography-0.102.0rc1.dist-info/METADATA,sha256=GkNEwQom09u69HGJrgQGXtYl7pBv1k6uTjsu9bmjUdQ,12087
|
|
374
|
+
cartography-0.102.0rc1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
375
|
+
cartography-0.102.0rc1.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
|
|
376
|
+
cartography-0.102.0rc1.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
|
|
377
|
+
cartography-0.102.0rc1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|