cartography 0.103.0rc1__py3-none-any.whl → 0.104.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.

Files changed (73) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +97 -1
  3. cartography/config.py +28 -0
  4. cartography/intel/anthropic/__init__.py +62 -0
  5. cartography/intel/anthropic/apikeys.py +72 -0
  6. cartography/intel/anthropic/users.py +75 -0
  7. cartography/intel/anthropic/util.py +51 -0
  8. cartography/intel/anthropic/workspaces.py +95 -0
  9. cartography/intel/aws/cloudwatch.py +93 -0
  10. cartography/intel/aws/ec2/load_balancer_v2s.py +4 -1
  11. cartography/intel/aws/efs.py +93 -0
  12. cartography/intel/aws/resources.py +4 -0
  13. cartography/intel/aws/secretsmanager.py +136 -3
  14. cartography/intel/aws/ssm.py +71 -0
  15. cartography/intel/cloudflare/__init__.py +74 -0
  16. cartography/intel/cloudflare/accounts.py +57 -0
  17. cartography/intel/cloudflare/dnsrecords.py +64 -0
  18. cartography/intel/cloudflare/members.py +75 -0
  19. cartography/intel/cloudflare/roles.py +65 -0
  20. cartography/intel/cloudflare/zones.py +64 -0
  21. cartography/intel/openai/__init__.py +86 -0
  22. cartography/intel/openai/adminapikeys.py +89 -0
  23. cartography/intel/openai/apikeys.py +96 -0
  24. cartography/intel/openai/projects.py +97 -0
  25. cartography/intel/openai/serviceaccounts.py +82 -0
  26. cartography/intel/openai/users.py +75 -0
  27. cartography/intel/openai/util.py +45 -0
  28. cartography/intel/tailscale/__init__.py +77 -0
  29. cartography/intel/tailscale/acls.py +146 -0
  30. cartography/intel/tailscale/devices.py +127 -0
  31. cartography/intel/tailscale/postureintegrations.py +81 -0
  32. cartography/intel/tailscale/tailnets.py +76 -0
  33. cartography/intel/tailscale/users.py +80 -0
  34. cartography/intel/tailscale/utils.py +132 -0
  35. cartography/models/anthropic/__init__.py +0 -0
  36. cartography/models/anthropic/apikey.py +90 -0
  37. cartography/models/anthropic/organization.py +19 -0
  38. cartography/models/anthropic/user.py +48 -0
  39. cartography/models/anthropic/workspace.py +90 -0
  40. cartography/models/aws/cloudwatch/__init__.py +0 -0
  41. cartography/models/aws/cloudwatch/loggroup.py +52 -0
  42. cartography/models/aws/efs/__init__.py +0 -0
  43. cartography/models/aws/efs/mount_target.py +52 -0
  44. cartography/models/aws/secretsmanager/__init__.py +0 -0
  45. cartography/models/aws/secretsmanager/secret_version.py +116 -0
  46. cartography/models/aws/ssm/parameters.py +84 -0
  47. cartography/models/cloudflare/__init__.py +0 -0
  48. cartography/models/cloudflare/account.py +25 -0
  49. cartography/models/cloudflare/dnsrecord.py +55 -0
  50. cartography/models/cloudflare/member.py +82 -0
  51. cartography/models/cloudflare/role.py +44 -0
  52. cartography/models/cloudflare/zone.py +59 -0
  53. cartography/models/openai/__init__.py +0 -0
  54. cartography/models/openai/adminapikey.py +90 -0
  55. cartography/models/openai/apikey.py +84 -0
  56. cartography/models/openai/organization.py +17 -0
  57. cartography/models/openai/project.py +89 -0
  58. cartography/models/openai/serviceaccount.py +50 -0
  59. cartography/models/openai/user.py +49 -0
  60. cartography/models/tailscale/__init__.py +0 -0
  61. cartography/models/tailscale/device.py +95 -0
  62. cartography/models/tailscale/group.py +86 -0
  63. cartography/models/tailscale/postureintegration.py +58 -0
  64. cartography/models/tailscale/tag.py +102 -0
  65. cartography/models/tailscale/tailnet.py +29 -0
  66. cartography/models/tailscale/user.py +52 -0
  67. cartography/sync.py +8 -0
  68. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/METADATA +8 -4
  69. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/RECORD +73 -14
  70. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/WHEEL +1 -1
  71. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/entry_points.txt +0 -0
  72. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/licenses/LICENSE +0 -0
  73. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,77 @@
1
+ import logging
2
+
3
+ import neo4j
4
+ import requests
5
+
6
+ import cartography.intel.tailscale.acls
7
+ import cartography.intel.tailscale.devices
8
+ import cartography.intel.tailscale.postureintegrations
9
+ import cartography.intel.tailscale.tailnets
10
+ import cartography.intel.tailscale.users
11
+ from cartography.config import Config
12
+ from cartography.util import timeit
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @timeit
18
+ def start_tailscale_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
19
+ """
20
+ If this module is configured, perform ingestion of Tailscale data. Otherwise warn and exit
21
+ :param neo4j_session: Neo4J session for database interface
22
+ :param config: A cartography.config object
23
+ :return: None
24
+ """
25
+
26
+ if not config.tailscale_token or not config.tailscale_org:
27
+ logger.info(
28
+ "Tailscale import is not configured - skipping this module. "
29
+ "See docs to configure.",
30
+ )
31
+ return
32
+
33
+ # Create requests sessions
34
+ api_session = requests.session()
35
+ api_session.headers.update({"Authorization": f"Bearer {config.tailscale_token}"})
36
+
37
+ common_job_parameters = {
38
+ "UPDATE_TAG": config.update_tag,
39
+ "BASE_URL": config.tailscale_base_url,
40
+ "org": config.tailscale_org,
41
+ }
42
+
43
+ cartography.intel.tailscale.tailnets.sync(
44
+ neo4j_session,
45
+ api_session,
46
+ common_job_parameters,
47
+ org=config.tailscale_org,
48
+ )
49
+
50
+ users = cartography.intel.tailscale.users.sync(
51
+ neo4j_session,
52
+ api_session,
53
+ common_job_parameters,
54
+ org=config.tailscale_org,
55
+ )
56
+
57
+ cartography.intel.tailscale.devices.sync(
58
+ neo4j_session,
59
+ api_session,
60
+ common_job_parameters,
61
+ org=config.tailscale_org,
62
+ )
63
+
64
+ cartography.intel.tailscale.postureintegrations.sync(
65
+ neo4j_session,
66
+ api_session,
67
+ common_job_parameters,
68
+ org=config.tailscale_org,
69
+ )
70
+
71
+ cartography.intel.tailscale.acls.sync(
72
+ neo4j_session,
73
+ api_session,
74
+ common_job_parameters,
75
+ org=config.tailscale_org,
76
+ users=users,
77
+ )
@@ -0,0 +1,146 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+ from typing import Tuple
6
+
7
+ import neo4j
8
+ import requests
9
+
10
+ from cartography.client.core.tx import load
11
+ from cartography.graph.job import GraphJob
12
+ from cartography.intel.tailscale.utils import ACLParser
13
+ from cartography.intel.tailscale.utils import role_to_group
14
+ from cartography.models.tailscale.group import TailscaleGroupSchema
15
+ from cartography.models.tailscale.tag import TailscaleTagSchema
16
+ from cartography.util import timeit
17
+
18
+ logger = logging.getLogger(__name__)
19
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
20
+ _TIMEOUT = (60, 60)
21
+
22
+
23
+ @timeit
24
+ def sync(
25
+ neo4j_session: neo4j.Session,
26
+ api_session: requests.Session,
27
+ common_job_parameters: Dict[str, Any],
28
+ org: str,
29
+ users: List[Dict[str, Any]],
30
+ ) -> None:
31
+ raw_acl = get(
32
+ api_session,
33
+ common_job_parameters["BASE_URL"],
34
+ org,
35
+ )
36
+ groups, tags = transform(raw_acl, users)
37
+ load_groups(
38
+ neo4j_session,
39
+ groups,
40
+ common_job_parameters["UPDATE_TAG"],
41
+ org,
42
+ )
43
+ load_tags(
44
+ neo4j_session,
45
+ tags,
46
+ org,
47
+ common_job_parameters["UPDATE_TAG"],
48
+ )
49
+ cleanup(neo4j_session, common_job_parameters)
50
+
51
+
52
+ @timeit
53
+ def get(
54
+ api_session: requests.Session,
55
+ base_url: str,
56
+ org: str,
57
+ ) -> str:
58
+ req = api_session.get(
59
+ f"{base_url}/tailnet/{org}/acl",
60
+ timeout=_TIMEOUT,
61
+ )
62
+ req.raise_for_status()
63
+ return req.text
64
+
65
+
66
+ def transform(
67
+ raw_acl: str,
68
+ users: List[Dict[str, Any]],
69
+ ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
70
+ transformed_groups: Dict[str, Dict[str, Any]] = {}
71
+ transformed_tags: Dict[str, Dict[str, Any]] = {}
72
+
73
+ parser = ACLParser(raw_acl)
74
+ # Extract groups from the ACL
75
+ for group in parser.get_groups():
76
+ for dom in group["domain_members"]:
77
+ for user in users:
78
+ if user["loginName"].endswith(f"@{dom}"):
79
+ group["members"].append(user["loginName"])
80
+ # Ensure domain members are unique
81
+ group["domain_members"] = list(set(group["domain_members"]))
82
+ transformed_groups[group["id"]] = group
83
+ # Extract tags from the ACL
84
+ for tag in parser.get_tags():
85
+ for dom in tag["domain_owners"]:
86
+ for user in users:
87
+ if user["loginName"].endswith(f"@{dom}"):
88
+ tag["owners"].append(user["loginName"])
89
+ # Ensure domain owners are unique
90
+ tag["owners"] = list(set(tag["owners"]))
91
+ transformed_tags[tag["id"]] = tag
92
+
93
+ # Add autogroups based on user roles
94
+ for user in users:
95
+ for g in role_to_group(user["role"]):
96
+ if g not in transformed_groups:
97
+ transformed_groups[g] = {
98
+ "id": g,
99
+ "name": g.split(":")[-1],
100
+ "members": [],
101
+ "sub_groups": [],
102
+ "domain_members": [],
103
+ }
104
+ transformed_groups[g]["members"].append(user["loginName"])
105
+
106
+ return list(transformed_groups.values()), list(transformed_tags.values())
107
+
108
+
109
+ @timeit
110
+ def load_groups(
111
+ neo4j_session: neo4j.Session,
112
+ groups: List[Dict[str, Any]],
113
+ update_tag: str,
114
+ org: str,
115
+ ) -> None:
116
+ logger.info(f"Loading {len(groups)} Tailscale Groups to the graph")
117
+ load(neo4j_session, TailscaleGroupSchema(), groups, lastupdated=update_tag, org=org)
118
+
119
+
120
+ @timeit
121
+ def load_tags(
122
+ neo4j_session: neo4j.Session,
123
+ data: List[Dict[str, Any]],
124
+ org: str,
125
+ update_tag: int,
126
+ ) -> None:
127
+ logger.info(f"Loading {len(data)} Tailscale Tags to the graph")
128
+ load(
129
+ neo4j_session,
130
+ TailscaleTagSchema(),
131
+ data,
132
+ lastupdated=update_tag,
133
+ org=org,
134
+ )
135
+
136
+
137
+ @timeit
138
+ def cleanup(
139
+ neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]
140
+ ) -> None:
141
+ GraphJob.from_node_schema(TailscaleGroupSchema(), common_job_parameters).run(
142
+ neo4j_session
143
+ )
144
+ GraphJob.from_node_schema(TailscaleTagSchema(), common_job_parameters).run(
145
+ neo4j_session
146
+ )
@@ -0,0 +1,127 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import neo4j
7
+ import requests
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.models.tailscale.device import TailscaleDeviceSchema
12
+ from cartography.models.tailscale.tag import TailscaleTagSchema
13
+ from cartography.util import timeit
14
+
15
+ logger = logging.getLogger(__name__)
16
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
17
+ _TIMEOUT = (60, 60)
18
+
19
+
20
+ @timeit
21
+ def sync(
22
+ neo4j_session: neo4j.Session,
23
+ api_session: requests.Session,
24
+ common_job_parameters: Dict[str, Any],
25
+ org: str,
26
+ ) -> List[Dict]:
27
+ devices = get(
28
+ api_session,
29
+ common_job_parameters["BASE_URL"],
30
+ org,
31
+ )
32
+ tags = transform(devices)
33
+ load_devices(
34
+ neo4j_session,
35
+ devices,
36
+ org,
37
+ common_job_parameters["UPDATE_TAG"],
38
+ )
39
+ load_tags(
40
+ neo4j_session,
41
+ tags,
42
+ org,
43
+ common_job_parameters["UPDATE_TAG"],
44
+ )
45
+ cleanup(neo4j_session, common_job_parameters)
46
+ return devices
47
+
48
+
49
+ @timeit
50
+ def get(
51
+ api_session: requests.Session,
52
+ base_url: str,
53
+ org: str,
54
+ ) -> List[Dict[str, Any]]:
55
+ results: List[Dict[str, Any]] = []
56
+ req = api_session.get(
57
+ f"{base_url}/tailnet/{org}/devices",
58
+ timeout=_TIMEOUT,
59
+ )
60
+ req.raise_for_status()
61
+ results = req.json()["devices"]
62
+ return results
63
+
64
+
65
+ def transform(
66
+ raw_data: List[Dict[str, Any]],
67
+ ) -> List[Dict[str, Any]]:
68
+ """Extracts tags from the raw data and returns a list of dictionaries"""
69
+ transformed_tags: Dict[str, Dict[str, Any]] = {}
70
+ # Transform the raw data into the format expected by the load function
71
+ for device in raw_data:
72
+ for raw_tag in device.get("tags", []):
73
+ if raw_tag not in transformed_tags:
74
+ transformed_tags[raw_tag] = {
75
+ "id": raw_tag,
76
+ "name": raw_tag.split(":")[-1],
77
+ "devices": [device["nodeId"]],
78
+ }
79
+ else:
80
+ transformed_tags[raw_tag]["devices"].append(device["nodeId"])
81
+ return list(transformed_tags.values())
82
+
83
+
84
+ @timeit
85
+ def load_devices(
86
+ neo4j_session: neo4j.Session,
87
+ data: List[Dict[str, Any]],
88
+ org: str,
89
+ update_tag: int,
90
+ ) -> None:
91
+ logger.info(f"Loading {len(data)} Tailscale Devices to the graph")
92
+ load(
93
+ neo4j_session,
94
+ TailscaleDeviceSchema(),
95
+ data,
96
+ lastupdated=update_tag,
97
+ org=org,
98
+ )
99
+
100
+
101
+ @timeit
102
+ def load_tags(
103
+ neo4j_session: neo4j.Session,
104
+ data: List[Dict[str, Any]],
105
+ org: str,
106
+ update_tag: int,
107
+ ) -> None:
108
+ logger.info(f"Loading {len(data)} Tailscale Tags to the graph")
109
+ load(
110
+ neo4j_session,
111
+ TailscaleTagSchema(),
112
+ data,
113
+ lastupdated=update_tag,
114
+ org=org,
115
+ )
116
+
117
+
118
+ @timeit
119
+ def cleanup(
120
+ neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]
121
+ ) -> None:
122
+ GraphJob.from_node_schema(TailscaleDeviceSchema(), common_job_parameters).run(
123
+ neo4j_session
124
+ )
125
+ GraphJob.from_node_schema(TailscaleTagSchema(), common_job_parameters).run(
126
+ neo4j_session
127
+ )
@@ -0,0 +1,81 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import neo4j
7
+ import requests
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.models.tailscale.postureintegration import (
12
+ TailscalePostureIntegrationSchema,
13
+ )
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
18
+ _TIMEOUT = (60, 60)
19
+
20
+
21
+ @timeit
22
+ def sync(
23
+ neo4j_session: neo4j.Session,
24
+ api_session: requests.Session,
25
+ common_job_parameters: Dict[str, Any],
26
+ org: str,
27
+ ) -> None:
28
+ postureintegrations = get(
29
+ api_session,
30
+ common_job_parameters["BASE_URL"],
31
+ org,
32
+ )
33
+ load_postureintegrations(
34
+ neo4j_session,
35
+ postureintegrations,
36
+ org,
37
+ common_job_parameters["UPDATE_TAG"],
38
+ )
39
+ cleanup(neo4j_session, common_job_parameters)
40
+
41
+
42
+ @timeit
43
+ def get(
44
+ api_session: requests.Session,
45
+ base_url: str,
46
+ org: str,
47
+ ) -> List[Dict[str, Any]]:
48
+ results: List[Dict[str, Any]] = []
49
+ req = api_session.get(
50
+ f"{base_url}/tailnet/{org}/posture/integrations",
51
+ timeout=_TIMEOUT,
52
+ )
53
+ req.raise_for_status()
54
+ results = req.json()["integrations"]
55
+ return results
56
+
57
+
58
+ @timeit
59
+ def load_postureintegrations(
60
+ neo4j_session: neo4j.Session,
61
+ data: List[Dict[str, Any]],
62
+ org: str,
63
+ update_tag: int,
64
+ ) -> None:
65
+ logger.info(f"Loading {len(data)} Tailscale PostureIntegrations to the graph")
66
+ load(
67
+ neo4j_session,
68
+ TailscalePostureIntegrationSchema(),
69
+ data,
70
+ lastupdated=update_tag,
71
+ org=org,
72
+ )
73
+
74
+
75
+ @timeit
76
+ def cleanup(
77
+ neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]
78
+ ) -> None:
79
+ GraphJob.from_node_schema(
80
+ TailscalePostureIntegrationSchema(), common_job_parameters
81
+ ).run(neo4j_session)
@@ -0,0 +1,76 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import neo4j
7
+ import requests
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.models.tailscale.tailnet import TailscaleTailnetSchema
12
+ from cartography.util import timeit
13
+
14
+ logger = logging.getLogger(__name__)
15
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
16
+ _TIMEOUT = (60, 60)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ api_session: requests.Session,
23
+ common_job_parameters: Dict[str, Any],
24
+ org: str,
25
+ ) -> None:
26
+ tailnet = get(
27
+ api_session,
28
+ common_job_parameters["BASE_URL"],
29
+ org,
30
+ )
31
+ load_tailnets(
32
+ neo4j_session,
33
+ [tailnet],
34
+ org,
35
+ common_job_parameters["UPDATE_TAG"],
36
+ )
37
+ cleanup(neo4j_session, common_job_parameters)
38
+
39
+
40
+ @timeit
41
+ def get(
42
+ api_session: requests.Session,
43
+ base_url: str,
44
+ org: str,
45
+ ) -> Dict[str, Any]:
46
+ req = api_session.get(
47
+ f"{base_url}/tailnet/{org}/settings",
48
+ timeout=_TIMEOUT,
49
+ )
50
+ req.raise_for_status()
51
+ return req.json()
52
+
53
+
54
+ @timeit
55
+ def load_tailnets(
56
+ neo4j_session: neo4j.Session,
57
+ data: List[Dict[str, Any]],
58
+ org: str,
59
+ update_tag: int,
60
+ ) -> None:
61
+ load(
62
+ neo4j_session,
63
+ TailscaleTailnetSchema(),
64
+ data,
65
+ lastupdated=update_tag,
66
+ org=org,
67
+ )
68
+
69
+
70
+ @timeit
71
+ def cleanup(
72
+ neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]
73
+ ) -> None:
74
+ GraphJob.from_node_schema(TailscaleTailnetSchema(), common_job_parameters).run(
75
+ neo4j_session
76
+ )
@@ -0,0 +1,80 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import neo4j
7
+ import requests
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.models.tailscale.user import TailscaleUserSchema
12
+ from cartography.util import timeit
13
+
14
+ logger = logging.getLogger(__name__)
15
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
16
+ _TIMEOUT = (60, 60)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ api_session: requests.Session,
23
+ common_job_parameters: Dict[str, Any],
24
+ org: str,
25
+ ) -> List[Dict]:
26
+ users = get(
27
+ api_session,
28
+ common_job_parameters["BASE_URL"],
29
+ org,
30
+ )
31
+ load_users(
32
+ neo4j_session,
33
+ users,
34
+ org,
35
+ common_job_parameters["UPDATE_TAG"],
36
+ )
37
+ cleanup(neo4j_session, common_job_parameters)
38
+ return users
39
+
40
+
41
+ @timeit
42
+ def get(
43
+ api_session: requests.Session,
44
+ base_url: str,
45
+ org: str,
46
+ ) -> List[Dict[str, Any]]:
47
+ results: List[Dict[str, Any]] = []
48
+ req = api_session.get(
49
+ f"{base_url}/tailnet/{org}/users",
50
+ timeout=_TIMEOUT,
51
+ )
52
+ req.raise_for_status()
53
+ results = req.json()["users"]
54
+ return results
55
+
56
+
57
+ @timeit
58
+ def load_users(
59
+ neo4j_session: neo4j.Session,
60
+ data: List[Dict[str, Any]],
61
+ org: str,
62
+ update_tag: int,
63
+ ) -> None:
64
+ logger.info(f"Loading {len(data)} Tailscale Users to the graph")
65
+ load(
66
+ neo4j_session,
67
+ TailscaleUserSchema(),
68
+ data,
69
+ lastupdated=update_tag,
70
+ org=org,
71
+ )
72
+
73
+
74
+ @timeit
75
+ def cleanup(
76
+ neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]
77
+ ) -> None:
78
+ GraphJob.from_node_schema(TailscaleUserSchema(), common_job_parameters).run(
79
+ neo4j_session
80
+ )