cartography 0.103.0__py3-none-any.whl → 0.104.0__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 +21 -3
- cartography/config.py +4 -0
- cartography/graph/cleanupbuilder.py +151 -41
- cartography/intel/anthropic/__init__.py +62 -0
- cartography/intel/anthropic/apikeys.py +72 -0
- cartography/intel/anthropic/users.py +75 -0
- cartography/intel/anthropic/util.py +51 -0
- cartography/intel/anthropic/workspaces.py +95 -0
- cartography/intel/aws/cloudtrail.py +3 -38
- cartography/intel/aws/cloudwatch.py +1 -1
- cartography/intel/aws/ec2/load_balancer_v2s.py +4 -1
- cartography/intel/aws/resources.py +0 -2
- cartography/intel/aws/secretsmanager.py +150 -3
- cartography/intel/aws/ssm.py +71 -0
- cartography/intel/entra/ou.py +21 -5
- cartography/intel/openai/adminapikeys.py +1 -2
- cartography/intel/openai/apikeys.py +1 -1
- cartography/intel/openai/projects.py +4 -1
- cartography/intel/openai/serviceaccounts.py +1 -1
- cartography/intel/openai/users.py +0 -3
- cartography/intel/openai/util.py +17 -1
- cartography/models/anthropic/apikey.py +90 -0
- cartography/models/anthropic/organization.py +19 -0
- cartography/models/anthropic/user.py +48 -0
- cartography/models/anthropic/workspace.py +90 -0
- cartography/models/aws/cloudtrail/trail.py +24 -0
- cartography/models/aws/secretsmanager/__init__.py +0 -0
- cartography/models/aws/secretsmanager/secret_version.py +116 -0
- cartography/models/aws/ssm/parameters.py +84 -0
- cartography/models/core/nodes.py +15 -2
- cartography/models/openai/project.py +20 -1
- cartography/sync.py +2 -0
- {cartography-0.103.0.dist-info → cartography-0.104.0.dist-info}/METADATA +4 -4
- {cartography-0.103.0.dist-info → cartography-0.104.0.dist-info}/RECORD +40 -30
- {cartography-0.103.0.dist-info → cartography-0.104.0.dist-info}/WHEEL +1 -1
- cartography/intel/aws/efs.py +0 -93
- cartography/models/aws/efs/mount_target.py +0 -52
- /cartography/models/{aws/efs → anthropic}/__init__.py +0 -0
- {cartography-0.103.0.dist-info → cartography-0.104.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.103.0.dist-info → cartography-0.104.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.103.0.dist-info → cartography-0.104.0.dist-info}/top_level.txt +0 -0
cartography/_version.py
CHANGED
cartography/cli.py
CHANGED
|
@@ -561,7 +561,7 @@ class CLI:
|
|
|
561
561
|
type=str,
|
|
562
562
|
default=None,
|
|
563
563
|
help=(
|
|
564
|
-
"Your SnipeIT base URI"
|
|
564
|
+
"Your SnipeIT base URI. "
|
|
565
565
|
"Required if you are using the SnipeIT intel module. Ignored otherwise."
|
|
566
566
|
),
|
|
567
567
|
)
|
|
@@ -588,7 +588,7 @@ class CLI:
|
|
|
588
588
|
type=str,
|
|
589
589
|
default=None,
|
|
590
590
|
help=(
|
|
591
|
-
"The name of an environment variable containing a Tailscale API token."
|
|
591
|
+
"The name of an environment variable containing a Tailscale API token. "
|
|
592
592
|
"Required if you are using the Tailscale intel module. Ignored otherwise."
|
|
593
593
|
),
|
|
594
594
|
)
|
|
@@ -615,7 +615,7 @@ class CLI:
|
|
|
615
615
|
type=str,
|
|
616
616
|
default=None,
|
|
617
617
|
help=(
|
|
618
|
-
"The name of an environment variable containing a OpenAI API Key."
|
|
618
|
+
"The name of an environment variable containing a OpenAI API Key. "
|
|
619
619
|
"Required if you are using the OpenAI intel module. Ignored otherwise."
|
|
620
620
|
),
|
|
621
621
|
)
|
|
@@ -628,6 +628,15 @@ class CLI:
|
|
|
628
628
|
"Required if you are using the OpenAI intel module. Ignored otherwise."
|
|
629
629
|
),
|
|
630
630
|
)
|
|
631
|
+
parser.add_argument(
|
|
632
|
+
"--anthropic-apikey-env-var",
|
|
633
|
+
type=str,
|
|
634
|
+
default=None,
|
|
635
|
+
help=(
|
|
636
|
+
"The name of an environment variable containing an Anthropic API Key. "
|
|
637
|
+
"Required if you are using the Anthropic intel module. Ignored otherwise."
|
|
638
|
+
),
|
|
639
|
+
)
|
|
631
640
|
|
|
632
641
|
return parser
|
|
633
642
|
|
|
@@ -937,6 +946,15 @@ class CLI:
|
|
|
937
946
|
else:
|
|
938
947
|
config.openai_apikey = None
|
|
939
948
|
|
|
949
|
+
# Anthropic config
|
|
950
|
+
if config.anthropic_apikey_env_var:
|
|
951
|
+
logger.debug(
|
|
952
|
+
f"Reading Anthropic API key from environment variable {config.anthropic_apikey_env_var}",
|
|
953
|
+
)
|
|
954
|
+
config.anthropic_apikey = os.environ.get(config.anthropic_apikey_env_var)
|
|
955
|
+
else:
|
|
956
|
+
config.anthropic_apikey = None
|
|
957
|
+
|
|
940
958
|
# Run cartography
|
|
941
959
|
try:
|
|
942
960
|
return cartography.sync.run_with_config(self.sync, config)
|
cartography/config.py
CHANGED
|
@@ -135,6 +135,8 @@ class Config:
|
|
|
135
135
|
:param openai_apikey: OpenAI API key. Optional.
|
|
136
136
|
:type openai_org_id: string
|
|
137
137
|
:param openai_org_id: OpenAI organization id. Optional.
|
|
138
|
+
:type anthropic_apikey: string
|
|
139
|
+
:param anthropic_apikey: Anthropic API key. Optional.
|
|
138
140
|
"""
|
|
139
141
|
|
|
140
142
|
def __init__(
|
|
@@ -206,6 +208,7 @@ class Config:
|
|
|
206
208
|
cloudflare_token=None,
|
|
207
209
|
openai_apikey=None,
|
|
208
210
|
openai_org_id=None,
|
|
211
|
+
anthropic_apikey=None,
|
|
209
212
|
):
|
|
210
213
|
self.neo4j_uri = neo4j_uri
|
|
211
214
|
self.neo4j_user = neo4j_user
|
|
@@ -274,3 +277,4 @@ class Config:
|
|
|
274
277
|
self.cloudflare_token = cloudflare_token
|
|
275
278
|
self.openai_apikey = openai_apikey
|
|
276
279
|
self.openai_org_id = openai_org_id
|
|
280
|
+
self.anthropic_apikey = anthropic_apikey
|
|
@@ -15,33 +15,40 @@ from cartography.models.core.relationships import TargetNodeMatcher
|
|
|
15
15
|
def build_cleanup_queries(node_schema: CartographyNodeSchema) -> List[str]:
|
|
16
16
|
"""
|
|
17
17
|
Generates queries to clean up stale nodes and relationships from the given CartographyNodeSchema.
|
|
18
|
+
Properly handles cases where a node schema has a scoped cleanup or not.
|
|
18
19
|
Note that auto-cleanups for a node with no relationships is not currently supported.
|
|
19
|
-
|
|
20
|
-
Algorithm:
|
|
21
|
-
1. If node_schema has no relationships at all, return empty.
|
|
22
|
-
|
|
23
|
-
Otherwise,
|
|
24
|
-
|
|
25
|
-
1. If node_schema doesn't have a sub_resource relationship, generate queries only to clean up its other
|
|
26
|
-
relationships. No nodes will be cleaned up.
|
|
27
|
-
|
|
28
|
-
Otherwise,
|
|
29
|
-
|
|
30
|
-
1. First delete all stale nodes attached to the node_schema's sub resource
|
|
31
|
-
2. Delete all stale node to sub resource relationships
|
|
32
|
-
- We don't expect this to be very common (never for AWS resources, at least), but in case it is possible for an
|
|
33
|
-
asset to change sub resources, we want to handle it properly.
|
|
34
|
-
3. For all relationships defined on the node schema, delete all stale ones.
|
|
35
20
|
:param node_schema: The given CartographyNodeSchema
|
|
36
21
|
:return: A list of Neo4j queries to clean up nodes and relationships.
|
|
37
22
|
"""
|
|
23
|
+
# If the node has no relationships, do not delete the node. Leave this behind for the user to manage.
|
|
24
|
+
# Oftentimes these are SyncMetadata nodes.
|
|
38
25
|
if (
|
|
39
26
|
not node_schema.sub_resource_relationship
|
|
40
27
|
and not node_schema.other_relationships
|
|
41
28
|
):
|
|
42
29
|
return []
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
# Case 1 [Standard]: the node has a sub resource and scoped cleanup is true => clean up stale nodes
|
|
32
|
+
# of this type, scoped to the sub resource. Continue on to clean up the other_relationships too.
|
|
33
|
+
if node_schema.sub_resource_relationship and node_schema.scoped_cleanup:
|
|
34
|
+
queries = _build_cleanup_node_and_rel_queries(
|
|
35
|
+
node_schema,
|
|
36
|
+
node_schema.sub_resource_relationship,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Case 2: The node has a sub resource but scoped cleanup is false => this does not make sense
|
|
40
|
+
# because if have a sub resource, we are implying that we are doing scoped cleanup.
|
|
41
|
+
elif node_schema.sub_resource_relationship and not node_schema.scoped_cleanup:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"This is not expected: {node_schema.label} has a sub_resource_relationship but scoped_cleanup=False."
|
|
44
|
+
"Please check the class definition for this node schema. It doesn't make sense for a node to have a "
|
|
45
|
+
"sub resource relationship and an unscoped cleanup. Doing this will cause all stale nodes of this type "
|
|
46
|
+
"to be deleted regardless of the sub resource they are attached to."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Case 3: The node has no sub resource but scoped cleanup is true => do not delete any nodes, but clean up stale relationships.
|
|
50
|
+
# Return early.
|
|
51
|
+
elif not node_schema.sub_resource_relationship and node_schema.scoped_cleanup:
|
|
45
52
|
queries = []
|
|
46
53
|
other_rels = (
|
|
47
54
|
node_schema.other_relationships.rels
|
|
@@ -53,17 +60,20 @@ def build_cleanup_queries(node_schema: CartographyNodeSchema) -> List[str]:
|
|
|
53
60
|
queries.append(query)
|
|
54
61
|
return queries
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
node_schema
|
|
59
|
-
|
|
63
|
+
# Case 4: The node has no sub resource and scoped cleanup is false => clean up the stale nodes. Continue on to clean up the other_relationships too.
|
|
64
|
+
else:
|
|
65
|
+
queries = [_build_cleanup_node_query_unscoped(node_schema)]
|
|
66
|
+
|
|
60
67
|
if node_schema.other_relationships:
|
|
61
68
|
for rel in node_schema.other_relationships.rels:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
if node_schema.scoped_cleanup:
|
|
70
|
+
# [0] is the delete node query, [1] is the delete relationship query. We only want the latter.
|
|
71
|
+
_, rel_query = _build_cleanup_node_and_rel_queries(node_schema, rel)
|
|
72
|
+
queries.append(rel_query)
|
|
73
|
+
else:
|
|
74
|
+
queries.append(_build_cleanup_rel_queries_unscoped(node_schema, rel))
|
|
65
75
|
|
|
66
|
-
return
|
|
76
|
+
return queries
|
|
67
77
|
|
|
68
78
|
|
|
69
79
|
def _build_cleanup_rel_query_no_sub_resource(
|
|
@@ -94,6 +104,46 @@ def _build_cleanup_rel_query_no_sub_resource(
|
|
|
94
104
|
)
|
|
95
105
|
|
|
96
106
|
|
|
107
|
+
def _build_match_statement_for_cleanup(node_schema: CartographyNodeSchema) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Helper function to build a MATCH statement for a given node schema for cleanup.
|
|
110
|
+
"""
|
|
111
|
+
if not node_schema.sub_resource_relationship and not node_schema.scoped_cleanup:
|
|
112
|
+
template = Template("MATCH (n:$node_label)")
|
|
113
|
+
return template.safe_substitute(
|
|
114
|
+
node_label=node_schema.label,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# if it has a sub resource relationship defined, we need to match on the sub resource to make sure we only delete
|
|
118
|
+
# nodes that are attached to the sub resource.
|
|
119
|
+
template = Template(
|
|
120
|
+
"MATCH (n:$node_label)$sub_resource_link(:$sub_resource_label{$match_sub_res_clause})"
|
|
121
|
+
)
|
|
122
|
+
sub_resource_link = ""
|
|
123
|
+
sub_resource_label = ""
|
|
124
|
+
match_sub_res_clause = ""
|
|
125
|
+
|
|
126
|
+
if node_schema.sub_resource_relationship:
|
|
127
|
+
# Draw sub resource rel with correct direction
|
|
128
|
+
if node_schema.sub_resource_relationship.direction == LinkDirection.INWARD:
|
|
129
|
+
sub_resource_link_template = Template("<-[s:$SubResourceRelLabel]-")
|
|
130
|
+
else:
|
|
131
|
+
sub_resource_link_template = Template("-[s:$SubResourceRelLabel]->")
|
|
132
|
+
sub_resource_link = sub_resource_link_template.safe_substitute(
|
|
133
|
+
SubResourceRelLabel=node_schema.sub_resource_relationship.rel_label,
|
|
134
|
+
)
|
|
135
|
+
sub_resource_label = node_schema.sub_resource_relationship.target_node_label
|
|
136
|
+
match_sub_res_clause = _build_match_clause(
|
|
137
|
+
node_schema.sub_resource_relationship.target_node_matcher,
|
|
138
|
+
)
|
|
139
|
+
return template.safe_substitute(
|
|
140
|
+
node_label=node_schema.label,
|
|
141
|
+
sub_resource_link=sub_resource_link,
|
|
142
|
+
sub_resource_label=sub_resource_label,
|
|
143
|
+
match_sub_res_clause=match_sub_res_clause,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
97
147
|
def _build_cleanup_node_and_rel_queries(
|
|
98
148
|
node_schema: CartographyNodeSchema,
|
|
99
149
|
selected_relationship: CartographyRelSchema,
|
|
@@ -120,15 +170,6 @@ def _build_cleanup_node_and_rel_queries(
|
|
|
120
170
|
"verify the node class definition for the relationships that it has.",
|
|
121
171
|
)
|
|
122
172
|
|
|
123
|
-
# Draw sub resource rel with correct direction
|
|
124
|
-
if node_schema.sub_resource_relationship.direction == LinkDirection.INWARD:
|
|
125
|
-
sub_resource_link_template = Template("<-[s:$SubResourceRelLabel]-")
|
|
126
|
-
else:
|
|
127
|
-
sub_resource_link_template = Template("-[s:$SubResourceRelLabel]->")
|
|
128
|
-
sub_resource_link = sub_resource_link_template.safe_substitute(
|
|
129
|
-
SubResourceRelLabel=node_schema.sub_resource_relationship.rel_label,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
173
|
# The cleanup node query must always be before the cleanup rel query
|
|
133
174
|
delete_action_clauses = [
|
|
134
175
|
"""
|
|
@@ -161,19 +202,14 @@ def _build_cleanup_node_and_rel_queries(
|
|
|
161
202
|
# Ensure the node is attached to the sub resource and delete the node
|
|
162
203
|
query_template = Template(
|
|
163
204
|
"""
|
|
164
|
-
|
|
205
|
+
$match_statement
|
|
165
206
|
$selected_rel_clause
|
|
166
207
|
$delete_action_clause
|
|
167
208
|
""",
|
|
168
209
|
)
|
|
169
210
|
return [
|
|
170
211
|
query_template.safe_substitute(
|
|
171
|
-
|
|
172
|
-
sub_resource_link=sub_resource_link,
|
|
173
|
-
sub_resource_label=node_schema.sub_resource_relationship.target_node_label,
|
|
174
|
-
match_sub_res_clause=_build_match_clause(
|
|
175
|
-
node_schema.sub_resource_relationship.target_node_matcher,
|
|
176
|
-
),
|
|
212
|
+
match_statement=_build_match_statement_for_cleanup(node_schema),
|
|
177
213
|
selected_rel_clause=(
|
|
178
214
|
""
|
|
179
215
|
if selected_relationship == node_schema.sub_resource_relationship
|
|
@@ -185,6 +221,80 @@ def _build_cleanup_node_and_rel_queries(
|
|
|
185
221
|
]
|
|
186
222
|
|
|
187
223
|
|
|
224
|
+
def _build_cleanup_node_query_unscoped(
|
|
225
|
+
node_schema: CartographyNodeSchema,
|
|
226
|
+
) -> str:
|
|
227
|
+
"""
|
|
228
|
+
Generates a cleanup query for a node_schema to allow unscoped cleanup.
|
|
229
|
+
"""
|
|
230
|
+
if node_schema.scoped_cleanup:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"_build_cleanup_node_query_for_unscoped_cleanup() failed: '{node_schema.label}' does not have "
|
|
233
|
+
"scoped_cleanup=False, so we cannot generate a query to clean it up. Please verify that the class "
|
|
234
|
+
"definition is what you expect.",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# The cleanup node query must always be before the cleanup rel query
|
|
238
|
+
delete_action_clause = """
|
|
239
|
+
WHERE n.lastupdated <> $UPDATE_TAG
|
|
240
|
+
WITH n LIMIT $LIMIT_SIZE
|
|
241
|
+
DETACH DELETE n;
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
# Ensure the node is attached to the sub resource and delete the node
|
|
245
|
+
query_template = Template(
|
|
246
|
+
"""
|
|
247
|
+
$match_statement
|
|
248
|
+
$delete_action_clause
|
|
249
|
+
""",
|
|
250
|
+
)
|
|
251
|
+
return query_template.safe_substitute(
|
|
252
|
+
match_statement=_build_match_statement_for_cleanup(node_schema),
|
|
253
|
+
delete_action_clause=delete_action_clause,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _build_cleanup_rel_queries_unscoped(
|
|
258
|
+
node_schema: CartographyNodeSchema,
|
|
259
|
+
selected_relationship: CartographyRelSchema,
|
|
260
|
+
) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Generates relationship cleanup query for a node_schema with scoped_cleanup=False.
|
|
263
|
+
"""
|
|
264
|
+
if node_schema.scoped_cleanup:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"_build_cleanup_node_and_rel_queries_unscoped() failed: '{node_schema.label}' does not have "
|
|
267
|
+
"scoped_cleanup=False, so we cannot generate a query to clean it up. Please verify that the class "
|
|
268
|
+
"definition is what you expect.",
|
|
269
|
+
)
|
|
270
|
+
if not rel_present_on_node_schema(node_schema, selected_relationship):
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"_build_cleanup_node_query(): Attempted to build cleanup query for node '{node_schema.label}' and "
|
|
273
|
+
f"relationship {selected_relationship.rel_label} but that relationship is not present on the node. Please "
|
|
274
|
+
"verify the node class definition for the relationships that it has.",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# The cleanup node query must always be before the cleanup rel query
|
|
278
|
+
delete_action_clause = """WHERE r.lastupdated <> $UPDATE_TAG
|
|
279
|
+
WITH r LIMIT $LIMIT_SIZE
|
|
280
|
+
DELETE r;
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
# Ensure the node is attached to the sub resource and delete the node
|
|
284
|
+
query_template = Template(
|
|
285
|
+
"""
|
|
286
|
+
$match_statement
|
|
287
|
+
$selected_rel_clause
|
|
288
|
+
$delete_action_clause
|
|
289
|
+
""",
|
|
290
|
+
)
|
|
291
|
+
return query_template.safe_substitute(
|
|
292
|
+
match_statement=_build_match_statement_for_cleanup(node_schema),
|
|
293
|
+
selected_rel_clause=_build_selected_rel_clause(selected_relationship),
|
|
294
|
+
delete_action_clause=delete_action_clause,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
188
298
|
def _build_selected_rel_clause(selected_relationship: CartographyRelSchema) -> str:
|
|
189
299
|
"""
|
|
190
300
|
Draw selected relationship with correct direction. Returns a string that looks like either
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import neo4j
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
import cartography.intel.anthropic.apikeys
|
|
7
|
+
import cartography.intel.anthropic.users
|
|
8
|
+
import cartography.intel.anthropic.workspaces
|
|
9
|
+
from cartography.config import Config
|
|
10
|
+
from cartography.util import timeit
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@timeit
|
|
16
|
+
def start_anthropic_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
17
|
+
"""
|
|
18
|
+
If this module is configured, perform ingestion of Anthropic data. Otherwise warn and exit
|
|
19
|
+
:param neo4j_session: Neo4J session for database interface
|
|
20
|
+
:param config: A cartography.config object
|
|
21
|
+
:return: None
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if not config.anthropic_apikey:
|
|
25
|
+
logger.info(
|
|
26
|
+
"Anthropic import is not configured - skipping this module. "
|
|
27
|
+
"See docs to configure.",
|
|
28
|
+
)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Create requests sessions
|
|
32
|
+
api_session = requests.session()
|
|
33
|
+
api_session.headers.update(
|
|
34
|
+
{
|
|
35
|
+
"X-Api-Key": config.anthropic_apikey,
|
|
36
|
+
"anthropic-version": "2023-06-01",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
common_job_parameters = {
|
|
41
|
+
"UPDATE_TAG": config.update_tag,
|
|
42
|
+
"BASE_URL": "https://api.anthropic.com/v1",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Organization node is created during the users sync
|
|
46
|
+
cartography.intel.anthropic.users.sync(
|
|
47
|
+
neo4j_session,
|
|
48
|
+
api_session,
|
|
49
|
+
common_job_parameters,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
cartography.intel.anthropic.workspaces.sync(
|
|
53
|
+
neo4j_session,
|
|
54
|
+
api_session,
|
|
55
|
+
common_job_parameters,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
cartography.intel.anthropic.apikeys.sync(
|
|
59
|
+
neo4j_session,
|
|
60
|
+
api_session,
|
|
61
|
+
common_job_parameters,
|
|
62
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import neo4j
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from cartography.client.core.tx import load
|
|
9
|
+
from cartography.graph.job import GraphJob
|
|
10
|
+
from cartography.intel.anthropic.util import paginated_get
|
|
11
|
+
from cartography.models.anthropic.apikey import AnthropicApiKeySchema
|
|
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
|
+
) -> None:
|
|
25
|
+
org_id, apikeys = get(
|
|
26
|
+
api_session,
|
|
27
|
+
common_job_parameters["BASE_URL"],
|
|
28
|
+
)
|
|
29
|
+
common_job_parameters["ORG_ID"] = org_id
|
|
30
|
+
load_apikeys(
|
|
31
|
+
neo4j_session,
|
|
32
|
+
apikeys,
|
|
33
|
+
org_id,
|
|
34
|
+
common_job_parameters["UPDATE_TAG"],
|
|
35
|
+
)
|
|
36
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@timeit
|
|
40
|
+
def get(
|
|
41
|
+
api_session: requests.Session,
|
|
42
|
+
base_url: str,
|
|
43
|
+
) -> Tuple[str, list[dict[str, Any]]]:
|
|
44
|
+
return paginated_get(
|
|
45
|
+
api_session, f"{base_url}/organizations/api_keys", timeout=_TIMEOUT
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@timeit
|
|
50
|
+
def load_apikeys(
|
|
51
|
+
neo4j_session: neo4j.Session,
|
|
52
|
+
data: list[dict[str, Any]],
|
|
53
|
+
ORG_ID: str,
|
|
54
|
+
update_tag: int,
|
|
55
|
+
) -> None:
|
|
56
|
+
logger.info("Loading %d Anthropic ApiKey into Neo4j.", len(data))
|
|
57
|
+
load(
|
|
58
|
+
neo4j_session,
|
|
59
|
+
AnthropicApiKeySchema(),
|
|
60
|
+
data,
|
|
61
|
+
lastupdated=update_tag,
|
|
62
|
+
ORG_ID=ORG_ID,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@timeit
|
|
67
|
+
def cleanup(
|
|
68
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
69
|
+
) -> None:
|
|
70
|
+
GraphJob.from_node_schema(AnthropicApiKeySchema(), common_job_parameters).run(
|
|
71
|
+
neo4j_session
|
|
72
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import neo4j
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from cartography.client.core.tx import load
|
|
9
|
+
from cartography.graph.job import GraphJob
|
|
10
|
+
from cartography.intel.anthropic.util import paginated_get
|
|
11
|
+
from cartography.models.anthropic.organization import AnthropicOrganizationSchema
|
|
12
|
+
from cartography.models.anthropic.user import AnthropicUserSchema
|
|
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
|
+
) -> str:
|
|
26
|
+
org_id, users = get(
|
|
27
|
+
api_session,
|
|
28
|
+
common_job_parameters["BASE_URL"],
|
|
29
|
+
)
|
|
30
|
+
common_job_parameters["ORG_ID"] = org_id
|
|
31
|
+
load_users(neo4j_session, users, org_id, common_job_parameters["UPDATE_TAG"])
|
|
32
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
33
|
+
return org_id
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@timeit
|
|
37
|
+
def get(
|
|
38
|
+
api_session: requests.Session,
|
|
39
|
+
base_url: str,
|
|
40
|
+
) -> Tuple[str, list[dict[str, Any]]]:
|
|
41
|
+
return paginated_get(
|
|
42
|
+
api_session, f"{base_url}/organizations/users", timeout=_TIMEOUT
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@timeit
|
|
47
|
+
def load_users(
|
|
48
|
+
neo4j_session: neo4j.Session,
|
|
49
|
+
data: list[dict[str, Any]],
|
|
50
|
+
ORG_ID: str,
|
|
51
|
+
update_tag: int,
|
|
52
|
+
) -> None:
|
|
53
|
+
load(
|
|
54
|
+
neo4j_session,
|
|
55
|
+
AnthropicOrganizationSchema(),
|
|
56
|
+
[{"id": ORG_ID}],
|
|
57
|
+
lastupdated=update_tag,
|
|
58
|
+
)
|
|
59
|
+
logger.info("Loading %d Anthropic User into Neo4j.", len(data))
|
|
60
|
+
load(
|
|
61
|
+
neo4j_session,
|
|
62
|
+
AnthropicUserSchema(),
|
|
63
|
+
data,
|
|
64
|
+
lastupdated=update_tag,
|
|
65
|
+
ORG_ID=ORG_ID,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@timeit
|
|
70
|
+
def cleanup(
|
|
71
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
72
|
+
) -> None:
|
|
73
|
+
GraphJob.from_node_schema(AnthropicUserSchema(), common_job_parameters).run(
|
|
74
|
+
neo4j_session
|
|
75
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def paginated_get(
|
|
7
|
+
api_session: requests.Session,
|
|
8
|
+
url: str,
|
|
9
|
+
timeout: tuple[int, int],
|
|
10
|
+
after: str | None = None,
|
|
11
|
+
) -> tuple[str, list[dict[str, Any]]]:
|
|
12
|
+
"""Helper function to get paginated data from the Anthropic API.
|
|
13
|
+
|
|
14
|
+
This function handles the pagination of the API requests and returns
|
|
15
|
+
the results in a list. It also retrieves the organization ID from the
|
|
16
|
+
response headers. The function will continue to make requests until
|
|
17
|
+
all pages of data have been retrieved. The results are returned as a
|
|
18
|
+
list of dictionaries, where each dictionary represents a single
|
|
19
|
+
entity.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
api_session (requests.Session): The requests session to use for making API calls.
|
|
23
|
+
url (str): The URL to make the API call to.
|
|
24
|
+
timeout (tuple[int, int]): The timeout for the API call.
|
|
25
|
+
after (str | None): The ID of the last item retrieved in the previous request.
|
|
26
|
+
If None, the first page of results will be retrieved.
|
|
27
|
+
Returns:
|
|
28
|
+
tuple[str, list[dict[str, Any]]]: A tuple containing the organization ID and a list of
|
|
29
|
+
dictionaries representing the results.
|
|
30
|
+
"""
|
|
31
|
+
results: list[dict[str, Any]] = []
|
|
32
|
+
params = {"after_id": after} if after else {}
|
|
33
|
+
req = api_session.get(
|
|
34
|
+
url,
|
|
35
|
+
params=params,
|
|
36
|
+
timeout=timeout,
|
|
37
|
+
)
|
|
38
|
+
req.raise_for_status()
|
|
39
|
+
# Get organization_id from the headers
|
|
40
|
+
organization_id = req.headers.get("anthropic-organization-id", "")
|
|
41
|
+
result = req.json()
|
|
42
|
+
results.extend(result.get("data", []))
|
|
43
|
+
if result.get("has_more"):
|
|
44
|
+
_, next_results = paginated_get(
|
|
45
|
+
api_session,
|
|
46
|
+
url,
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
after=result.get("last_id"),
|
|
49
|
+
)
|
|
50
|
+
results.extend(next_results)
|
|
51
|
+
return organization_id, results
|