cartography 0.90.0rc2__py3-none-any.whl → 0.92.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.
cartography/cli.py CHANGED
@@ -325,6 +325,30 @@ class CLI:
325
325
  default=None,
326
326
  help='The name of an environment variable containing a password with which to authenticate to Jamf.',
327
327
  )
328
+ parser.add_argument(
329
+ '--kandji-base-uri',
330
+ type=str,
331
+ default=None,
332
+ help=(
333
+ 'Your Kandji base URI, e.g. https://company.api.kandji.io.'
334
+ 'Required if you are using the Kandji intel module. Ignored otherwise.'
335
+ ),
336
+ )
337
+ parser.add_argument(
338
+ '--kandji-tenant-id',
339
+ type=str,
340
+ default=None,
341
+ help=(
342
+ 'Your Kandji tenant id e.g. company.'
343
+ 'Required using the Kandji intel module. Ignored otherwise.'
344
+ ),
345
+ )
346
+ parser.add_argument(
347
+ '--kandji-token-env-var',
348
+ type=str,
349
+ default=None,
350
+ help='The name of an environment variable containing token with which to authenticate to Kandji.',
351
+ )
328
352
  parser.add_argument(
329
353
  '--k8s-kubeconfig',
330
354
  default=None,
@@ -620,6 +644,26 @@ class CLI:
620
644
  config.jamf_user = None
621
645
  config.jamf_password = None
622
646
 
647
+ # Kandji config
648
+ if config.kandji_base_uri:
649
+ if config.kandji_token_env_var:
650
+ logger.debug(
651
+ "Reading Kandji API token from environment variable '%s'.",
652
+ config.kandji_token_env_var,
653
+ )
654
+ config.kandji_token = os.environ.get(config.kandji_token_env_var)
655
+ elif os.environ.get('KANDJI_TOKEN'):
656
+ logger.debug(
657
+ "Reading Kandji API token from environment variable 'KANDJI_TOKEN'.",
658
+ )
659
+ config.kandji_token = os.environ.get('KANDJI_TOKEN')
660
+ else:
661
+ logger.warning("A Kandji base URI was provided but a token was not.")
662
+ config.kandji_token = None
663
+ else:
664
+ logger.warning("A Kandji base URI was not provided.")
665
+ config.kandji_base_uri = None
666
+
623
667
  if config.statsd_enabled:
624
668
  logger.debug(
625
669
  f'statsd enabled. Sending metrics to server {config.statsd_host}:{config.statsd_port}. '
cartography/config.py CHANGED
@@ -69,6 +69,12 @@ class Config:
69
69
  :param jamf_user: User name used to authenticate to the Jamf data provider. Optional.
70
70
  :type jamf_password: string
71
71
  :param jamf_password: Password used to authenticate to the Jamf data provider. Optional.
72
+ :type kandji_base_uri: string
73
+ :param kandji_base_uri: Kandji data provider base URI, e.g. https://company.api.kandji.io. Optional.
74
+ :type kandji_tenant_id: string
75
+ :param kandji_tenant_id: Kandji tenant id. e.g. company Optional.
76
+ :type kandji_token: string
77
+ :param kandji_token: Token used to authenticate to the Kandji data provider. Optional.
72
78
  :type statsd_enabled: bool
73
79
  :param statsd_enabled: Whether to collect statsd metrics such as sync execution times. Optional.
74
80
  :type statsd_host: str
@@ -137,6 +143,9 @@ class Config:
137
143
  jamf_base_uri=None,
138
144
  jamf_user=None,
139
145
  jamf_password=None,
146
+ kandji_base_uri=None,
147
+ kandji_tenant_id=None,
148
+ kandji_token=None,
140
149
  k8s_kubeconfig=None,
141
150
  statsd_enabled=False,
142
151
  statsd_prefix=None,
@@ -190,6 +199,9 @@ class Config:
190
199
  self.jamf_base_uri = jamf_base_uri
191
200
  self.jamf_user = jamf_user
192
201
  self.jamf_password = jamf_password
202
+ self.kandji_base_uri = kandji_base_uri
203
+ self.kandji_tenant_id = kandji_tenant_id
204
+ self.kandji_token = kandji_token
193
205
  self.k8s_kubeconfig = k8s_kubeconfig
194
206
  self.statsd_enabled = statsd_enabled
195
207
  self.statsd_prefix = statsd_prefix
@@ -19,23 +19,23 @@ logger = logging.getLogger(__name__)
19
19
 
20
20
  @timeit
21
21
  def get_images_in_use(neo4j_session: neo4j.Session, region: str, current_aws_account_id: str) -> List[str]:
22
- # We use OPTIONAL here to allow query chaining with queries that may not match.
23
22
  get_images_query = """
24
- OPTIONAL MATCH (:AWSAccount{id: $AWS_ACCOUNT_ID})-[:RESOURCE]->(i:EC2Instance)
23
+ MATCH (:AWSAccount{id: $AWS_ACCOUNT_ID})-[:RESOURCE]->(i:EC2Instance)
25
24
  WHERE i.region = $Region
26
- WITH collect(DISTINCT i.imageid) AS images
27
- OPTIONAL MATCH (:AWSAccount{id: $AWS_ACCOUNT_ID})-[:RESOURCE]->(lc:LaunchConfiguration)
25
+ RETURN DISTINCT(i.imageid) as image
26
+ UNION
27
+ MATCH (:AWSAccount{id: $AWS_ACCOUNT_ID})-[:RESOURCE]->(lc:LaunchConfiguration)
28
28
  WHERE lc.region = $Region
29
- WITH collect(DISTINCT lc.image_id)+images AS images
30
- OPTIONAL MATCH (:AWSAccount{id: $AWS_ACCOUNT_ID})-[:RESOURCE]->(ltv:LaunchTemplateVersion)
29
+ RETURN DISTINCT(lc.image_id) as image
30
+ UNION
31
+ MATCH (:AWSAccount{id: $AWS_ACCOUNT_ID})-[:RESOURCE]->(ltv:LaunchTemplateVersion)
31
32
  WHERE ltv.region = $Region
32
- WITH collect(DISTINCT ltv.image_id)+images AS images
33
- RETURN images
33
+ RETURN DISTINCT(ltv.image_id) as image
34
34
  """
35
35
  results = neo4j_session.run(get_images_query, AWS_ACCOUNT_ID=current_aws_account_id, Region=region)
36
36
  images = []
37
37
  for r in results:
38
- images.extend(r['images'])
38
+ images.append(r['image'])
39
39
  return images
40
40
 
41
41
 
@@ -42,8 +42,10 @@ def get_snapshots(boto3_session: boto3.session.Session, region: str, in_use_snap
42
42
  snapshots.extend(page['Snapshots'])
43
43
  except ClientError as e:
44
44
  if e.response['Error']['Code'] == 'InvalidSnapshot.NotFound':
45
- logger.warning(f"Failed to retrieve page of in-use, \
46
- not owned snapshots. Continuing anyway. Error - {e}")
45
+ logger.warning(
46
+ f"Failed to retrieve page of in-use, \
47
+ not owned snapshots. Continuing anyway. Error - {e}",
48
+ )
47
49
  else:
48
50
  raise
49
51
 
@@ -57,10 +57,13 @@ def _get_team_repos_for_multiple_teams(
57
57
 
58
58
  team_repos = _get_team_repos(org, api_url, token, team_name) if repo_count > 0 else None
59
59
 
60
- # Shape = [(repo_url, 'WRITE'), ...]]
61
- repo_urls = [t['url'] for t in team_repos.nodes] if team_repos else []
62
- repo_permissions = [t['permission'] for t in team_repos.edges] if team_repos else []
60
+ repo_urls = []
61
+ repo_permissions = []
62
+ if team_repos:
63
+ repo_urls = [t['url'] for t in team_repos.nodes] if team_repos.nodes else []
64
+ repo_permissions = [t['permission'] for t in team_repos.edges] if team_repos.edges else []
63
65
 
66
+ # Shape = [(repo_url, 'WRITE'), ...]]
64
67
  result[team_name] = list(zip(repo_urls, repo_permissions))
65
68
  return result
66
69
 
@@ -81,12 +81,12 @@ def call_github_api(query: str, variables: str, token: str, api_url: str) -> Dic
81
81
 
82
82
 
83
83
  def fetch_page(
84
- token: str,
85
- api_url: str,
86
- organization: str,
87
- query: str,
88
- cursor: Optional[str] = None,
89
- **kwargs: Any,
84
+ token: str,
85
+ api_url: str,
86
+ organization: str,
87
+ query: str,
88
+ cursor: Optional[str] = None,
89
+ **kwargs: Any,
90
90
  ) -> Dict[str, Any]:
91
91
  """
92
92
  Return a single page of max size 100 elements from the Github api_url using the given `query` and `cursor` params.
@@ -139,6 +139,7 @@ def fetch_all(
139
139
  """
140
140
  cursor = None
141
141
  has_next_page = True
142
+ org_data: Dict[str, Any] = {}
142
143
  data: PaginatedGraphqlData = PaginatedGraphqlData(nodes=[], edges=[])
143
144
  retry = 0
144
145
 
@@ -170,6 +171,15 @@ def fetch_all(
170
171
  time.sleep(2 ** retry)
171
172
  continue
172
173
 
174
+ if 'data' not in resp:
175
+ logger.warning(
176
+ f'Got no "data" attribute in response: {resp}. '
177
+ f'Stopping requests for organization: {organization} and '
178
+ f'resource_type: {resource_type}',
179
+ )
180
+ has_next_page = False
181
+ continue
182
+
173
183
  resource = resp['data']['organization'][resource_type]
174
184
  if resource_inner_type:
175
185
  resource = resp['data']['organization'][resource_type][resource_inner_type]
@@ -180,6 +190,14 @@ def fetch_all(
180
190
 
181
191
  cursor = resource['pageInfo']['endCursor']
182
192
  has_next_page = resource['pageInfo']['hasNextPage']
183
-
184
- org_data = {'url': resp['data']['organization']['url'], 'login': resp['data']['organization']['login']}
193
+ if not org_data:
194
+ org_data = {
195
+ 'url': resp['data']['organization']['url'],
196
+ 'login': resp['data']['organization']['login'],
197
+ }
198
+
199
+ if not org_data:
200
+ raise ValueError(
201
+ f"Didn't get any organization data for organization: {organization} and resource_type: {resource_type}",
202
+ )
185
203
  return data, org_data
@@ -0,0 +1,39 @@
1
+ import logging
2
+
3
+ import neo4j
4
+
5
+ import cartography.intel.kandji.devices
6
+ from cartography.config import Config
7
+ from cartography.util import timeit
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @timeit
13
+ def start_kandji_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
14
+ """
15
+ If this module is configured, perform ingestion of Kandji devices. Otherwise warn and exit
16
+
17
+ :param neo4j_session: Neo4J session for database interface
18
+ :param config: A cartography.config object
19
+
20
+ :return: None
21
+ """
22
+ if config.kandji_base_uri is None or config.kandji_token is None or config.kandji_tenant_id is None:
23
+ logger.warning(
24
+ 'Required parameter(s) missing. Skipping sync.',
25
+ 'See docs to configure.',
26
+ )
27
+ return
28
+
29
+ common_job_parameters = {
30
+ "UPDATE_TAG": config.update_tag,
31
+ "TENANT_ID": config.kandji_tenant_id,
32
+ }
33
+
34
+ cartography.intel.kandji.devices.sync(
35
+ neo4j_session,
36
+ config.kandji_base_uri,
37
+ config.kandji_token,
38
+ common_job_parameters=common_job_parameters,
39
+ )
@@ -0,0 +1,84 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import neo4j
7
+ from requests import Session
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.models.kandji.device import KandjiDeviceSchema
12
+ from cartography.models.kandji.tenant import KandjiTenantSchema
13
+ from cartography.util import timeit
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+ _TIMEOUT = (60, 60)
18
+
19
+
20
+ @timeit
21
+ def get(kandji_base_uri: str, kandji_token: str) -> List[Dict[str, Any]]:
22
+ api_endpoint = f"{kandji_base_uri}/api/v1/devices"
23
+ headers = {
24
+ 'Accept': 'application/json',
25
+ 'Authorization': f'Bearer {kandji_token}',
26
+ }
27
+
28
+ session = Session()
29
+ req = session.get(api_endpoint, headers=headers, timeout=_TIMEOUT)
30
+ req.raise_for_status()
31
+ return req.json()
32
+
33
+
34
+ @timeit
35
+ def transform(api_result: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
36
+ result: List[Dict[str, Any]] = []
37
+ for device in api_result:
38
+ n_device = device
39
+ n_device['id'] = device['device_id']
40
+ result.append(n_device)
41
+ return result
42
+
43
+
44
+ @timeit
45
+ def load_devices(
46
+ neo4j_session: neo4j.Session,
47
+ common_job_parameters: Dict[str, Any],
48
+ data: List[Dict[str, Any]],
49
+ ) -> None:
50
+
51
+ tenant_id = common_job_parameters["TENANT_ID"]
52
+ update_tag = common_job_parameters["UPDATE_TAG"]
53
+
54
+ load(
55
+ neo4j_session,
56
+ KandjiTenantSchema(),
57
+ [{'id': tenant_id}],
58
+ lastupdated=update_tag,
59
+ )
60
+
61
+ load(
62
+ neo4j_session,
63
+ KandjiDeviceSchema(),
64
+ data,
65
+ lastupdated=update_tag,
66
+ TENANT_ID=tenant_id,
67
+ )
68
+
69
+
70
+ def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
71
+ GraphJob.from_node_schema(KandjiDeviceSchema(), common_job_parameters).run(neo4j_session)
72
+
73
+
74
+ @timeit
75
+ def sync(
76
+ neo4j_session: neo4j.Session,
77
+ kandji_base_uri: str,
78
+ kandji_token: str,
79
+ common_job_parameters: Dict[str, Any],
80
+ ) -> None:
81
+ devices = get(kandji_base_uri=kandji_base_uri, kandji_token=kandji_token)
82
+ formatted_devices = transform(devices)
83
+ load_devices(neo4j_session, common_job_parameters, formatted_devices)
84
+ cleanup(neo4j_session, common_job_parameters)
@@ -68,7 +68,7 @@ def start_okta_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
68
68
  applications.sync_okta_applications(neo4j_session, config.okta_org_id, config.update_tag, config.okta_api_key)
69
69
  factors.sync_users_factors(neo4j_session, config.okta_org_id, config.update_tag, config.okta_api_key, state)
70
70
  origins.sync_trusted_origins(neo4j_session, config.okta_org_id, config.update_tag, config.okta_api_key)
71
- awssaml.sync_okta_aws_saml(neo4j_session, config.okta_saml_role_regex, config.update_tag)
71
+ awssaml.sync_okta_aws_saml(neo4j_session, config.okta_saml_role_regex, config.update_tag, config.okta_org_id)
72
72
 
73
73
  # need creds with permission
74
74
  # soft fail as some won't be able to get such high priv token
@@ -1,15 +1,22 @@
1
1
  # Okta intel module - AWS SAML
2
2
  import logging
3
3
  import re
4
+ from collections import namedtuple
4
5
  from typing import Dict
5
6
  from typing import List
6
7
  from typing import Optional
7
8
 
8
9
  import neo4j
9
10
 
11
+ from cartography.client.core.tx import read_list_of_dicts_tx
12
+ from cartography.client.core.tx import read_single_value_tx
10
13
  from cartography.util import timeit
11
14
 
12
15
 
16
+ AccountRole = namedtuple('AccountRole', ['account_id', 'role_name'])
17
+ OktaGroup = namedtuple('OktaGroup', ['group_id', 'group_name'])
18
+ GroupRole = namedtuple('GroupRole', ['okta_group_id', 'aws_role_arn'])
19
+
13
20
  logger = logging.getLogger(__name__)
14
21
 
15
22
 
@@ -17,17 +24,25 @@ def _parse_regex(regex_string: str) -> str:
17
24
  return regex_string.replace("{{accountid}}", "P<accountid>").replace("{{role}}", "P<role>").strip()
18
25
 
19
26
 
20
- @timeit
21
- def transform_okta_group_to_aws_role(group_id: str, group_name: str, mapping_regex: str) -> Optional[Dict]:
27
+ def _parse_okta_group_name(okta_group_name: str, mapping_regex: str) -> AccountRole | None:
28
+ """
29
+ Extract AWS account id and AWS role name from the given Okta group name using the given mapping regex.
30
+ """
22
31
  regex = _parse_regex(mapping_regex)
23
- matches = re.search(regex, group_name)
32
+ matches = re.search(regex, okta_group_name)
24
33
  if matches:
25
- accountid = matches.group("accountid")
26
- role = matches.group("role")
27
- role_arn = f"arn:aws:iam::{accountid}:role/{role}"
34
+ account_id = matches.group("accountid")
35
+ role_name = matches.group("role")
36
+ return AccountRole(account_id, role_name)
37
+ return None
38
+
39
+
40
+ def transform_okta_group_to_aws_role(group_id: str, group_name: str, mapping_regex: str) -> Optional[Dict]:
41
+ account_role = _parse_okta_group_name(group_name, mapping_regex)
42
+ if account_role:
43
+ role_arn = f"arn:aws:iam::{account_role.account_id}:role/{account_role.role_name}"
28
44
  return {"groupid": group_id, "role": role_arn}
29
- else:
30
- return None
45
+ return None
31
46
 
32
47
 
33
48
  @timeit
@@ -45,6 +60,7 @@ def query_for_okta_to_aws_role_mapping(neo4j_session: neo4j.Session, mapping_reg
45
60
 
46
61
  for res in results:
47
62
  has_results = True
63
+ # input: okta group id, okta group name. output: aws role arn.
48
64
  mapping = transform_okta_group_to_aws_role(res["group.id"], res["group.name"], mapping_regex)
49
65
  if mapping:
50
66
  group_to_role_mapping.append(mapping)
@@ -107,8 +123,96 @@ def _load_human_can_assume_role(neo4j_session: neo4j.Session, okta_update_tag: i
107
123
  )
108
124
 
109
125
 
126
+ def get_awssso_okta_groups(neo4j_session: neo4j.Session, okta_org_id: str) -> list[OktaGroup]:
127
+ """
128
+ Return list of all Okta group ids in the current Okta organization tied to Okta Applications with name
129
+ "amazon_aws_sso".
130
+ """
131
+ query = """
132
+ MATCH (g:OktaGroup)-[:APPLICATION]->(a:OktaApplication{name:"amazon_aws_sso"})
133
+ <-[:RESOURCE]-(:OktaOrganization{id: $okta_org_id})
134
+ RETURN g.id as group_id, g.name as group_name
135
+ """
136
+ result = neo4j_session.read_transaction(read_list_of_dicts_tx, query, okta_org_id=okta_org_id)
137
+ return [OktaGroup(group_name=og['group_name'], group_id=og['group_id']) for og in result]
138
+
139
+
140
+ def get_awssso_role_arn(account_id: str, role_hint: str, neo4j_session: neo4j.Session) -> str | None:
141
+ """
142
+ Attempt to return the AWS role ARN for the given AWS account ID and role hint string.
143
+ This function exists to handle that AWS SSO roles have a 'AWSReservedSSO' prefix and a hashed suffix
144
+ Input:
145
+ - account_id: AWS account ID
146
+ - role_hint (str): The `AccountRole.role_name` returned by _parse_okta_group_name(). This is the part of the Okta
147
+ group name that refers to the AWS role name.
148
+ Output:
149
+ - If we are able to find it, returns the matching AWS role ARN.
150
+ """
151
+ query = """
152
+ MATCH (:AWSAccount{id:$account_id})-[:RESOURCE]->(role:AWSRole{path:"/aws-reserved/sso.amazonaws.com/"})
153
+ WHERE SPLIT(role.name, '_')[1..-1][0] = $role_hint
154
+ RETURN role.arn AS role_arn
155
+ """
156
+ return neo4j_session.read_transaction(read_single_value_tx, query, account_id=account_id, role_hint=role_hint)
157
+
158
+
159
+ def query_for_okta_to_awssso_role_mapping(
160
+ neo4j_session: neo4j.Session,
161
+ awssso_okta_groups: list[OktaGroup],
162
+ mapping_regex: str,
163
+ ) -> list[GroupRole]:
164
+ """
165
+ Input:
166
+ - neo4j session
167
+ - str list of Okta group names
168
+ - str regex that tells us how to find the AWS role name and account when given an Okta group name
169
+ Output:
170
+ - list of OktaGroup id to AWSRole arn pairs.
171
+ """
172
+ result = []
173
+ for group in awssso_okta_groups:
174
+ account_role = _parse_okta_group_name(group.group_name, mapping_regex)
175
+ if not account_role:
176
+ logger.info(f"Okta group {group.group_name} has no associated AWS SSO role")
177
+ continue
178
+
179
+ role_arn = get_awssso_role_arn(account_role.account_id, account_role.role_name, neo4j_session)
180
+ if role_arn:
181
+ result.append(GroupRole(group.group_id, role_arn))
182
+ return result
183
+
184
+
185
+ def _load_awssso_tx(tx: neo4j.Transaction, group_to_role: list[GroupRole], okta_update_tag: int) -> None:
186
+ ingest_statement = """
187
+ UNWIND $GROUP_TO_ROLE as app_data
188
+ MATCH (role:AWSRole{arn: app_data.aws_role_arn})
189
+ MATCH (group:OktaGroup{id: app_data.okta_group_id})
190
+ MERGE (role)<-[r:ALLOWED_BY]-(group)
191
+ ON CREATE SET r.firstseen = timestamp()
192
+ SET r.lastupdated = $okta_update_tag
193
+ """
194
+ tx.run(
195
+ ingest_statement,
196
+ GROUP_TO_ROLE=[g._asdict() for g in group_to_role],
197
+ okta_update_tag=okta_update_tag,
198
+ )
199
+
200
+
201
+ def _load_okta_group_to_awssso_roles(
202
+ neo4j_session: neo4j.Session,
203
+ group_to_role: list[GroupRole],
204
+ okta_update_tag: int,
205
+ ) -> None:
206
+ neo4j_session.write_transaction(_load_awssso_tx, group_to_role, okta_update_tag)
207
+
208
+
110
209
  @timeit
111
- def sync_okta_aws_saml(neo4j_session: neo4j.Session, mapping_regex: str, okta_update_tag: int) -> None:
210
+ def sync_okta_aws_saml(
211
+ neo4j_session: neo4j.Session,
212
+ mapping_regex: str,
213
+ okta_update_tag: int,
214
+ okta_org_id: str,
215
+ ) -> None:
112
216
  """
113
217
  Sync okta integration with saml. This will link OktaGroups to the AWSRoles they enable.
114
218
  This is for people who use the okta saml provider for AWS
@@ -127,3 +231,7 @@ def sync_okta_aws_saml(neo4j_session: neo4j.Session, mapping_regex: str, okta_up
127
231
  group_to_role_mapping = query_for_okta_to_aws_role_mapping(neo4j_session, mapping_regex)
128
232
  _load_okta_group_to_aws_roles(neo4j_session, group_to_role_mapping, okta_update_tag)
129
233
  _load_human_can_assume_role(neo4j_session, okta_update_tag)
234
+
235
+ sso_okta_groups = get_awssso_okta_groups(neo4j_session, okta_org_id)
236
+ group_to_ssorole_mapping = query_for_okta_to_awssso_role_mapping(neo4j_session, sso_okta_groups, mapping_regex)
237
+ _load_okta_group_to_awssso_roles(neo4j_session, group_to_ssorole_mapping, okta_update_tag)
File without changes
@@ -0,0 +1,48 @@
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 KandjiDeviceNodeProperties(CartographyNodeProperties):
15
+ id: PropertyRef = PropertyRef('id')
16
+ lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
17
+
18
+ device_id: PropertyRef = PropertyRef('device_id')
19
+ device_name: PropertyRef = PropertyRef('device_name')
20
+ last_check_in: PropertyRef = PropertyRef('last_check_in')
21
+ model: PropertyRef = PropertyRef('model')
22
+ os_version: PropertyRef = PropertyRef('os_version')
23
+ platform: PropertyRef = PropertyRef('platform')
24
+ serial_number: PropertyRef = PropertyRef('serial_number', extra_index=True)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class KandjiTenantToKandjiDeviceRelProperties(CartographyRelProperties):
29
+ lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ # (:KandjiDevice)-[:ENROLLED_TO]->(:KandjiTenant)
34
+ class KandjiTenantToKandjiDeviceRel(CartographyRelSchema):
35
+ target_node_label: str = 'KandjiTenant'
36
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
37
+ {'id': PropertyRef('TENANT_ID', set_in_kwargs=True)},
38
+ )
39
+ direction: LinkDirection = LinkDirection.OUTWARD
40
+ rel_label: str = "ENROLLED_TO"
41
+ properties: KandjiTenantToKandjiDeviceRelProperties = KandjiTenantToKandjiDeviceRelProperties()
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class KandjiDeviceSchema(CartographyNodeSchema):
46
+ label: str = 'KandjiDevice' # The label of the node
47
+ properties: KandjiDeviceNodeProperties = KandjiDeviceNodeProperties() # An object representing all properties
48
+ sub_resource_relationship: KandjiTenantToKandjiDeviceRel = KandjiTenantToKandjiDeviceRel()
@@ -0,0 +1,17 @@
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 KandjiTenantNodeProperties(CartographyNodeProperties):
10
+ id: PropertyRef = PropertyRef('id')
11
+ lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class KandjiTenantSchema(CartographyNodeSchema):
16
+ label: str = 'KandjiTenant' # The label of the node
17
+ properties: KandjiTenantNodeProperties = KandjiTenantNodeProperties() # An object representing all properties
cartography/sync.py CHANGED
@@ -24,6 +24,7 @@ import cartography.intel.duo
24
24
  import cartography.intel.gcp
25
25
  import cartography.intel.github
26
26
  import cartography.intel.gsuite
27
+ import cartography.intel.kandji
27
28
  import cartography.intel.kubernetes
28
29
  import cartography.intel.lastpass
29
30
  import cartography.intel.oci
@@ -50,6 +51,7 @@ TOP_LEVEL_MODULES = OrderedDict({ # preserve order so that the default sync alw
50
51
  'okta': cartography.intel.okta.start_okta_ingestion,
51
52
  'github': cartography.intel.github.start_github_ingestion,
52
53
  'digitalocean': cartography.intel.digitalocean.start_digitalocean_ingestion,
54
+ 'kandji': cartography.intel.kandji.start_kandji_ingestion,
53
55
  'kubernetes': cartography.intel.kubernetes.start_k8s_ingestion,
54
56
  'lastpass': cartography.intel.lastpass.start_lastpass_ingestion,
55
57
  'bigfix': cartography.intel.bigfix.start_bigfix_ingestion,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cartography
3
- Version: 0.90.0rc2
3
+ Version: 0.92.0rc1
4
4
  Summary: Explore assets and their relationships across your technical infrastructure.
5
5
  Home-page: https://www.github.com/lyft/cartography
6
6
  Maintainer: Lyft
@@ -1,10 +1,10 @@
1
1
  cartography/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cartography/__main__.py,sha256=JftXT_nUPkqcEh8uxCCT4n-OyHYqbldEgrDS-4ygy0U,101
3
- cartography/cli.py,sha256=2_kLxdCSIb0nLo2vRVnIR_5XnylonvoWehfvXZElX1o,30059
4
- cartography/config.py,sha256=3X70Vx94T0Sam5qCdln-R_FnGmJGY-SY6Ok32A38nbE,10777
3
+ cartography/cli.py,sha256=ot9_gMxw5_irVS7KYfWf5HIr2Xkb10RDEbOTY1nzUcw,31787
4
+ cartography/config.py,sha256=rL1zgxZO47_R7S6E9e0CwxmhzRSN0X_q93NtcPR1G00,11368
5
5
  cartography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  cartography/stats.py,sha256=dbybb9V2FuvSuHjjNwz6Vjwnd1hap2C7h960rLoKcl8,4406
7
- cartography/sync.py,sha256=1y7nzaNSpxND1CRHpP6pFyOSw0DVDn_kox3zuvw3Uzo,9635
7
+ cartography/sync.py,sha256=a80r_IzrZcWGSmRDRrxkesNYPiOuLte5YHvDQT3L-Lw,9730
8
8
  cartography/util.py,sha256=F3FPMJl1KDW0x_5cvt2ZGI0Dv1LVrHU7Az4OleAANBI,14474
9
9
  cartography/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  cartography/client/aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -172,7 +172,7 @@ cartography/intel/aws/ssm.py,sha256=IDOYa8v2FgziU8nBOZ7wyDG4o_nFYshbB-si9Ut_9Zc,
172
172
  cartography/intel/aws/ec2/__init__.py,sha256=IDK2Yap7mllK_ab6yVMLXatJ94znIkn-szv5RJP5fbo,346
173
173
  cartography/intel/aws/ec2/auto_scaling_groups.py,sha256=4erjP31KSVW-Pp2ASmDox_VLp_AQUAin4KYxfZKZcSM,9223
174
174
  cartography/intel/aws/ec2/elastic_ip_addresses.py,sha256=0k4NwS73VyWbEj5jXvSkaq2RNvmAlBlrN-UKa_Bj0uk,3957
175
- cartography/intel/aws/ec2/images.py,sha256=KZJft6EB-EbILzzjbamRkOgvOmPBqarKUFzMKgpJdXc,3842
175
+ cartography/intel/aws/ec2/images.py,sha256=DcGHJwZf8_K5iRCDJ2QOdP837TTJTwST02IRswfk9Mc,3697
176
176
  cartography/intel/aws/ec2/instances.py,sha256=mnTjdBY-4D-TGhH29UrSaLUW0Uft0JApDIJkkLz4zPc,12170
177
177
  cartography/intel/aws/ec2/internet_gateways.py,sha256=dI-4-85_3DGGZZBcY_DN6XqESx9P26S6jKok314lcnQ,2883
178
178
  cartography/intel/aws/ec2/key_pairs.py,sha256=SvRgd56vE4eouvTSNoFK8PP8HYoECO91goxc36oq_FY,2508
@@ -182,7 +182,7 @@ cartography/intel/aws/ec2/load_balancers.py,sha256=1GwErzGqi3BKCARqfGJcD_r_D84rF
182
182
  cartography/intel/aws/ec2/network_interfaces.py,sha256=CzF8PooCYUQ2pk8DR8JDAhkWRUQSBj_27OsIfkL_-Cs,9199
183
183
  cartography/intel/aws/ec2/reserved_instances.py,sha256=jv8-VLI5KL8jN1QRI20yim8lzZ7I7wR8a5EF8DckahA,3122
184
184
  cartography/intel/aws/ec2/security_groups.py,sha256=vxLeaCpCowkbl-YpON1UdbjtPolMfj_reOEuKujN80Y,6060
185
- cartography/intel/aws/ec2/snapshots.py,sha256=HSeK8COoc1p399Y0LSg6Jo0bTiooCIh66jCv4DPsJsA,5393
185
+ cartography/intel/aws/ec2/snapshots.py,sha256=R3U6ZwE4bQPy5yikLCRcUHyXN1dD7TzS-3jULQO-F0g,5432
186
186
  cartography/intel/aws/ec2/subnets.py,sha256=wdv9TXI1BR_iilOCYmYXL2yok8qef49-I77_DPlyheQ,3694
187
187
  cartography/intel/aws/ec2/tgw.py,sha256=lTFPlRNoDHNklR38alSywXlSiiTyg86vJNth7Pc4pZQ,9114
188
188
  cartography/intel/aws/ec2/util.py,sha256=Pv-x1QEAAmyxcpEl6y8M24ija3ERjXFE36fswuKXHDs,226
@@ -231,14 +231,16 @@ cartography/intel/gcp/gke.py,sha256=qaTwsVaxkwNhW5_Mw4bedOk7fgJK8y0LwwcYlUABXDg,
231
231
  cartography/intel/gcp/storage.py,sha256=oO_ayEhkXlj2Gn7T5MU41ZXiqwRwe6Ud4wzqyRTsyf4,9075
232
232
  cartography/intel/github/__init__.py,sha256=y876JJGTDJZEOFCDiNCJfcLNxN24pVj4s2N0YmuuoaE,1914
233
233
  cartography/intel/github/repos.py,sha256=YPDdBMk6NkZjwPcqPW5LlCy_OS9tKcrZD6ygiUG93J0,21766
234
- cartography/intel/github/teams.py,sha256=aX1dj7HHVjw0hv_VvG1dee3WzTfmVMraP1khlwvN7Xs,5287
234
+ cartography/intel/github/teams.py,sha256=mofyJeJVOD7Umh9Rq6QnAwom9bBHBx18kyvFMvQX5YE,5383
235
235
  cartography/intel/github/users.py,sha256=kQp0dxzP08DVrdvfVeCciQbrKPbbFvwbR_p_I_XGt7s,3826
236
- cartography/intel/github/util.py,sha256=trigWITP7C44RjKpBT12fCcxtqo5WFqUOcB9VqtzifA,7465
236
+ cartography/intel/github/util.py,sha256=K6hbxypy4luKhIE1Uh5VWZc9OyjMK2OuO00vBAQfloA,8049
237
237
  cartography/intel/gsuite/__init__.py,sha256=AGIUskGlLCVGHbnQicNpNWi9AvmV7_7hUKTt-hsB2J8,4306
238
238
  cartography/intel/gsuite/api.py,sha256=J0dkNdfBVMrEv8vvStQu7YKVxXSyV45WueFhUS4aOG4,10310
239
239
  cartography/intel/jamf/__init__.py,sha256=Nof-LrUeevoieo6oP2GyfTwx8k5TUIgreW6hSj53YjQ,419
240
240
  cartography/intel/jamf/computers.py,sha256=EfjlupQ-9HYTjOrmuwrGuJDy9ApAnJvk8WrYcp6_Jkk,1673
241
241
  cartography/intel/jamf/util.py,sha256=EAyP8VpOY2uAvW3HtX6r7qORNjGa1Tr3fuqezuLQ0j4,1017
242
+ cartography/intel/kandji/__init__.py,sha256=OHZJNzuNibIfJ51OkL3XL2EdA_ZmvPHPeWCQUld4J64,1079
243
+ cartography/intel/kandji/devices.py,sha256=j_rP6rQ5VPT_XEcGXx7Yt6eCOm1Oe3I2qWIxXODXEcA,2224
242
244
  cartography/intel/kubernetes/__init__.py,sha256=jaOTEanWnTrYvcBN1XUC5oqBhz1AJbFmzoT9uu_VBSg,1481
243
245
  cartography/intel/kubernetes/namespaces.py,sha256=6o-FgAX_Ai5NCj2xOWM-RNWEvn0gZjVQnZSGCJlcIhw,2710
244
246
  cartography/intel/kubernetes/pods.py,sha256=aX3pP_vs6icMe2vK4vgMak6HZ64okhRzoihpkPHscGU,4502
@@ -251,9 +253,9 @@ cartography/intel/oci/__init__.py,sha256=AZmRX6EO4LUnynDtIKHxtZ_Ab2-CYPPc2u5d0Q2
251
253
  cartography/intel/oci/iam.py,sha256=zPrJeoMoO3ZkjBfWbTttjrcUvxxMuWquLTmsDH5MgOI,17712
252
254
  cartography/intel/oci/organizations.py,sha256=tzQkZfE4LPoS-6lXBRQGyhq8aJLZUJ1_q75Q9eTBke0,4086
253
255
  cartography/intel/oci/utils.py,sha256=UbX9jib4sWEdKeAt2CeCo4k9shUiWY08oTfQz_nDvjA,3223
254
- cartography/intel/okta/__init__.py,sha256=HYw9wlE27dHJ2fwSlHgbJyHcxhdFzbYWBcZdQ6bqfIo,3813
256
+ cartography/intel/okta/__init__.py,sha256=i5YY9mIDQ2-IBnCSWf4rToYMa9fQQIxucCnl0TXK2Uc,3833
255
257
  cartography/intel/okta/applications.py,sha256=ZqUn-bru6Kh75vpUeRnMurUBh0rGBRpI2b2V09HOOQw,12866
256
- cartography/intel/okta/awssaml.py,sha256=uJFasoyQmABoC5xAjlXak51KuAjT2H6AA2f47N3h6gw,4651
258
+ cartography/intel/okta/awssaml.py,sha256=Rw0mrJ7NY5xjfEO_ijMqi1VEbr0FSasfrvGtoCPy1aU,9136
257
259
  cartography/intel/okta/factors.py,sha256=1bLnF4MRf0MYzzhT2tfM4jdfkjE1bkQn6_WuOqED2K4,4955
258
260
  cartography/intel/okta/groups.py,sha256=GxaixbY5KWkalj2rY6nWwe_IskVVowOAPo88OZIGcPY,10172
259
261
  cartography/intel/okta/organization.py,sha256=YLQc7ETdtf8Vc-CRCYivV_xmVl2Oz0Px53anJHYp-p8,821
@@ -319,6 +321,9 @@ cartography/models/duo/user.py,sha256=ih3DH_QveAve4cX9dmIwC5gVN6_RNnuLK3bfJ5I9u6
319
321
  cartography/models/duo/web_authn_credential.py,sha256=OcZnfG5zCMlphxSltRcAXQ12hHYJjxrBt6A9L28g7Vk,2920
320
322
  cartography/models/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
321
323
  cartography/models/github/teams.py,sha256=mk3OFGTDqWkLz7aX7Q9AtpOMOkZDDGH0MWoVeevK2-k,4376
324
+ cartography/models/kandji/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
325
+ cartography/models/kandji/device.py,sha256=C3zPhLi1oPNysbSUr4H2u8b-Xy14sb3FE7YcjCwlntw,2214
326
+ cartography/models/kandji/tenant.py,sha256=KhcbahNBemny3coQPiadIY8B-yDMg_ejYB2BR6vqBfw,674
322
327
  cartography/models/lastpass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
323
328
  cartography/models/lastpass/tenant.py,sha256=TG-9LFo9Sfzb9UgcTt_gFVTKocLItbgQMMPkN_iprXU,618
324
329
  cartography/models/lastpass/user.py,sha256=SMTTYN6jgccc9k76hY3rVImElJOhHhZ9f1aZ6JzcrHw,3487
@@ -326,10 +331,10 @@ cartography/models/semgrep/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
326
331
  cartography/models/semgrep/deployment.py,sha256=or5qZDuR51MXzINpH15jZrqmSUvXQevCNYWJ7D6v-JI,745
327
332
  cartography/models/semgrep/findings.py,sha256=xrn8sgXpNMrNJbKQagaAVxaCG9bVjTATSRR2XRBR4rg,5386
328
333
  cartography/models/semgrep/locations.py,sha256=kSk7Nn5Mn4Ob84MVZOo2GR0YFi-9Okq9pgA3FfC6_bk,3061
329
- cartography-0.90.0rc2.dist-info/LICENSE,sha256=489ZXeW9G90up6ep-D1n-lJgk9ciNT2yxXpFgRSidtk,11341
330
- cartography-0.90.0rc2.dist-info/METADATA,sha256=DHVlVtAVX0wLA23rAP2on60QcEvswp8wivDIIw5LQgE,1991
331
- cartography-0.90.0rc2.dist-info/NOTICE,sha256=YOGAsjFtbyKj5tslYIg6V5jEYRuEvnSsIuDOUKj0Qj4,97
332
- cartography-0.90.0rc2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
333
- cartography-0.90.0rc2.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
334
- cartography-0.90.0rc2.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
335
- cartography-0.90.0rc2.dist-info/RECORD,,
334
+ cartography-0.92.0rc1.dist-info/LICENSE,sha256=489ZXeW9G90up6ep-D1n-lJgk9ciNT2yxXpFgRSidtk,11341
335
+ cartography-0.92.0rc1.dist-info/METADATA,sha256=Ascw5OW3AX47QCK7O2ftYRY4BRVycgrTVfIZqMe0O6g,1991
336
+ cartography-0.92.0rc1.dist-info/NOTICE,sha256=YOGAsjFtbyKj5tslYIg6V5jEYRuEvnSsIuDOUKj0Qj4,97
337
+ cartography-0.92.0rc1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
338
+ cartography-0.92.0rc1.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
339
+ cartography-0.92.0rc1.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
340
+ cartography-0.92.0rc1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5