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.

@@ -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
- req = session.get(api_endpoint, headers=headers, timeout=_TIMEOUT)
30
- req.raise_for_status()
31
- return req.json()
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.101.1rc1
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