cartography 0.101.1rc1__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 -6
- cartography/data/jobs/cleanup/crowdstrike_import_cleanup.json +0 -5
- cartography/intel/aws/ec2/launch_templates.py +14 -5
- cartography/intel/aws/ec2/load_balancers.py +126 -148
- 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/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.0rc1.dist-info}/METADATA +4 -1
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0rc1.dist-info}/RECORD +27 -18
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0rc1.dist-info}/WHEEL +0 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
|
@@ -25,10 +25,34 @@ def get(kandji_base_uri: str, kandji_token: str) -> List[Dict[str, Any]]:
|
|
|
25
25
|
'Authorization': f'Bearer {kandji_token}',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
offset = 0
|
|
29
|
+
limit = 300
|
|
30
|
+
params: dict[str, str | int] = {
|
|
31
|
+
"sort": "serial_number",
|
|
32
|
+
"limit": limit,
|
|
33
|
+
"offset": offset,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
devices: List[Dict[str, Any]] = []
|
|
28
37
|
session = Session()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
while True:
|
|
39
|
+
logger.debug("Kandji device offset: %s", offset)
|
|
40
|
+
|
|
41
|
+
params["offset"] = offset
|
|
42
|
+
response = session.get(api_endpoint, headers=headers, timeout=_TIMEOUT, params=params)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
|
|
45
|
+
result = response.json()
|
|
46
|
+
# If no more result, we are done
|
|
47
|
+
if len(result) == 0:
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
devices.extend(result)
|
|
51
|
+
|
|
52
|
+
offset += limit
|
|
53
|
+
|
|
54
|
+
logger.debug("Kandji device count: %d", len(devices))
|
|
55
|
+
return devices
|
|
32
56
|
|
|
33
57
|
|
|
34
58
|
@timeit
|
|
@@ -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,49 @@
|
|
|
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
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class CrowdstrikeHostNodeProperties(CartographyNodeProperties):
|
|
10
|
+
id: PropertyRef = PropertyRef('device_id')
|
|
11
|
+
cid: PropertyRef = PropertyRef('cid')
|
|
12
|
+
instance_id: PropertyRef = PropertyRef('instance_id', extra_index=True)
|
|
13
|
+
serial_number: PropertyRef = PropertyRef('serial_number', extra_index=True)
|
|
14
|
+
status: PropertyRef = PropertyRef('status')
|
|
15
|
+
hostname: PropertyRef = PropertyRef('hostname')
|
|
16
|
+
machine_domain: PropertyRef = PropertyRef('machine_domain')
|
|
17
|
+
crowdstrike_first_seen: PropertyRef = PropertyRef('first_seen')
|
|
18
|
+
crowdstrike_last_seen: PropertyRef = PropertyRef('last_seen')
|
|
19
|
+
local_ip: PropertyRef = PropertyRef('local_ip')
|
|
20
|
+
external_ip: PropertyRef = PropertyRef('external_ip')
|
|
21
|
+
cpu_signature: PropertyRef = PropertyRef('cpu_signature')
|
|
22
|
+
bios_manufacturer: PropertyRef = PropertyRef('bios_manufacturer')
|
|
23
|
+
bios_version: PropertyRef = PropertyRef('bios_version')
|
|
24
|
+
mac_address: PropertyRef = PropertyRef('mac_address')
|
|
25
|
+
os_version: PropertyRef = PropertyRef('os_version')
|
|
26
|
+
os_build: PropertyRef = PropertyRef('os_build')
|
|
27
|
+
platform_id: PropertyRef = PropertyRef('platform_id')
|
|
28
|
+
platform_name: PropertyRef = PropertyRef('platform_name')
|
|
29
|
+
service_provider: PropertyRef = PropertyRef('service_provider')
|
|
30
|
+
service_provider_account_id: PropertyRef = PropertyRef('service_provider_account_id')
|
|
31
|
+
agent_version: PropertyRef = PropertyRef('agent_version')
|
|
32
|
+
system_manufacturer: PropertyRef = PropertyRef('system_manufacturer')
|
|
33
|
+
system_product_name: PropertyRef = PropertyRef('system_product_name')
|
|
34
|
+
product_type: PropertyRef = PropertyRef('product_type')
|
|
35
|
+
product_type_desc: PropertyRef = PropertyRef('product_type_desc')
|
|
36
|
+
provision_status: PropertyRef = PropertyRef('provision_status')
|
|
37
|
+
reduced_functionality_mode: PropertyRef = PropertyRef('reduced_functionality_mode')
|
|
38
|
+
kernel_version: PropertyRef = PropertyRef('kernel_version')
|
|
39
|
+
major_version: PropertyRef = PropertyRef('major_version')
|
|
40
|
+
minor_version: PropertyRef = PropertyRef('minor_version')
|
|
41
|
+
tags: PropertyRef = PropertyRef('tags')
|
|
42
|
+
modified_timestamp: PropertyRef = PropertyRef('modified_timestamp')
|
|
43
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class CrowdstrikeHostSchema(CartographyNodeSchema):
|
|
48
|
+
label: str = 'CrowdstrikeHost'
|
|
49
|
+
properties: CrowdstrikeHostNodeProperties = CrowdstrikeHostNodeProperties()
|
|
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/stats.py
CHANGED
|
@@ -97,7 +97,7 @@ def set_stats_client(stats_client: StatsClient) -> None:
|
|
|
97
97
|
"""
|
|
98
98
|
This is used to set the module level stats client configured to talk with a statsd host
|
|
99
99
|
"""
|
|
100
|
-
global _scoped_stats_client
|
|
100
|
+
global _scoped_stats_client # noqa: F824
|
|
101
101
|
_scoped_stats_client.set_stats_client(stats_client)
|
|
102
102
|
|
|
103
103
|
|
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
|