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.
- cartography/_version.py +2 -2
- cartography/cli.py +97 -1
- cartography/config.py +28 -0
- 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/cloudwatch.py +93 -0
- cartography/intel/aws/ec2/load_balancer_v2s.py +4 -1
- cartography/intel/aws/efs.py +93 -0
- cartography/intel/aws/resources.py +4 -0
- cartography/intel/aws/secretsmanager.py +136 -3
- cartography/intel/aws/ssm.py +71 -0
- cartography/intel/cloudflare/__init__.py +74 -0
- cartography/intel/cloudflare/accounts.py +57 -0
- cartography/intel/cloudflare/dnsrecords.py +64 -0
- cartography/intel/cloudflare/members.py +75 -0
- cartography/intel/cloudflare/roles.py +65 -0
- cartography/intel/cloudflare/zones.py +64 -0
- cartography/intel/openai/__init__.py +86 -0
- cartography/intel/openai/adminapikeys.py +89 -0
- cartography/intel/openai/apikeys.py +96 -0
- cartography/intel/openai/projects.py +97 -0
- cartography/intel/openai/serviceaccounts.py +82 -0
- cartography/intel/openai/users.py +75 -0
- cartography/intel/openai/util.py +45 -0
- cartography/intel/tailscale/__init__.py +77 -0
- cartography/intel/tailscale/acls.py +146 -0
- cartography/intel/tailscale/devices.py +127 -0
- cartography/intel/tailscale/postureintegrations.py +81 -0
- cartography/intel/tailscale/tailnets.py +76 -0
- cartography/intel/tailscale/users.py +80 -0
- cartography/intel/tailscale/utils.py +132 -0
- cartography/models/anthropic/__init__.py +0 -0
- 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/cloudwatch/__init__.py +0 -0
- cartography/models/aws/cloudwatch/loggroup.py +52 -0
- cartography/models/aws/efs/__init__.py +0 -0
- cartography/models/aws/efs/mount_target.py +52 -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/cloudflare/__init__.py +0 -0
- cartography/models/cloudflare/account.py +25 -0
- cartography/models/cloudflare/dnsrecord.py +55 -0
- cartography/models/cloudflare/member.py +82 -0
- cartography/models/cloudflare/role.py +44 -0
- cartography/models/cloudflare/zone.py +59 -0
- cartography/models/openai/__init__.py +0 -0
- cartography/models/openai/adminapikey.py +90 -0
- cartography/models/openai/apikey.py +84 -0
- cartography/models/openai/organization.py +17 -0
- cartography/models/openai/project.py +89 -0
- cartography/models/openai/serviceaccount.py +50 -0
- cartography/models/openai/user.py +49 -0
- cartography/models/tailscale/__init__.py +0 -0
- cartography/models/tailscale/device.py +95 -0
- cartography/models/tailscale/group.py +86 -0
- cartography/models/tailscale/postureintegration.py +58 -0
- cartography/models/tailscale/tag.py +102 -0
- cartography/models/tailscale/tailnet.py +29 -0
- cartography/models/tailscale/user.py +52 -0
- cartography/sync.py +8 -0
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/METADATA +8 -4
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/RECORD +73 -14
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/WHEEL +1 -1
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc1.dist-info}/top_level.txt +0 -0
cartography/_version.py
CHANGED
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
20
|
+
__version__ = version = '0.104.0rc1'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 104, 0, 'rc1')
|
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
|
)
|
|
@@ -577,6 +577,66 @@ class CLI:
|
|
|
577
577
|
default=None,
|
|
578
578
|
help="An ID for the SnipeIT tenant.",
|
|
579
579
|
)
|
|
580
|
+
parser.add_argument(
|
|
581
|
+
"--cloudflare-token-env-var",
|
|
582
|
+
type=str,
|
|
583
|
+
default=None,
|
|
584
|
+
help="The name of an environment variable containing ApiKey with which to authenticate to Cloudflare.",
|
|
585
|
+
)
|
|
586
|
+
parser.add_argument(
|
|
587
|
+
"--tailscale-token-env-var",
|
|
588
|
+
type=str,
|
|
589
|
+
default=None,
|
|
590
|
+
help=(
|
|
591
|
+
"The name of an environment variable containing a Tailscale API token. "
|
|
592
|
+
"Required if you are using the Tailscale intel module. Ignored otherwise."
|
|
593
|
+
),
|
|
594
|
+
)
|
|
595
|
+
parser.add_argument(
|
|
596
|
+
"--tailscale-org",
|
|
597
|
+
type=str,
|
|
598
|
+
default=None,
|
|
599
|
+
help=(
|
|
600
|
+
"The name of the Tailscale organization to sync. "
|
|
601
|
+
"Required if you are using the Tailscale intel module. Ignored otherwise."
|
|
602
|
+
),
|
|
603
|
+
)
|
|
604
|
+
parser.add_argument(
|
|
605
|
+
"--tailscale-base-url",
|
|
606
|
+
type=str,
|
|
607
|
+
default="https://api.tailscale.com/api/v2",
|
|
608
|
+
help=(
|
|
609
|
+
"The base URL for the Tailscale API. "
|
|
610
|
+
"Required if you are using the Tailscale intel module. Ignored otherwise."
|
|
611
|
+
),
|
|
612
|
+
)
|
|
613
|
+
parser.add_argument(
|
|
614
|
+
"--openai-apikey-env-var",
|
|
615
|
+
type=str,
|
|
616
|
+
default=None,
|
|
617
|
+
help=(
|
|
618
|
+
"The name of an environment variable containing a OpenAI API Key. "
|
|
619
|
+
"Required if you are using the OpenAI intel module. Ignored otherwise."
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
parser.add_argument(
|
|
623
|
+
"--openai-org-id",
|
|
624
|
+
type=str,
|
|
625
|
+
default=None,
|
|
626
|
+
help=(
|
|
627
|
+
"The ID of the OpenAI organization to sync. "
|
|
628
|
+
"Required if you are using the OpenAI intel module. Ignored otherwise."
|
|
629
|
+
),
|
|
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
|
+
)
|
|
580
640
|
|
|
581
641
|
return parser
|
|
582
642
|
|
|
@@ -859,6 +919,42 @@ class CLI:
|
|
|
859
919
|
logger.warning("A SnipeIT base URI was not provided.")
|
|
860
920
|
config.snipeit_base_uri = None
|
|
861
921
|
|
|
922
|
+
# Tailscale config
|
|
923
|
+
if config.tailscale_token_env_var:
|
|
924
|
+
logger.debug(
|
|
925
|
+
f"Reading Tailscale API token from environment variable {config.tailscale_token_env_var}",
|
|
926
|
+
)
|
|
927
|
+
config.tailscale_token = os.environ.get(config.tailscale_token_env_var)
|
|
928
|
+
else:
|
|
929
|
+
config.tailscale_token = None
|
|
930
|
+
|
|
931
|
+
# Cloudflare config
|
|
932
|
+
if config.cloudflare_token_env_var:
|
|
933
|
+
logger.debug(
|
|
934
|
+
f"Reading Cloudflare ApiKey from environment variable {config.cloudflare_token_env_var}",
|
|
935
|
+
)
|
|
936
|
+
config.cloudflare_token = os.environ.get(config.cloudflare_token_env_var)
|
|
937
|
+
else:
|
|
938
|
+
config.cloudflare_token = None
|
|
939
|
+
|
|
940
|
+
# OpenAI config
|
|
941
|
+
if config.openai_apikey_env_var:
|
|
942
|
+
logger.debug(
|
|
943
|
+
f"Reading OpenAI API key from environment variable {config.openai_apikey_env_var}",
|
|
944
|
+
)
|
|
945
|
+
config.openai_apikey = os.environ.get(config.openai_apikey_env_var)
|
|
946
|
+
else:
|
|
947
|
+
config.openai_apikey = None
|
|
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
|
+
|
|
862
958
|
# Run cartography
|
|
863
959
|
try:
|
|
864
960
|
return cartography.sync.run_with_config(self.sync, config)
|
cartography/config.py
CHANGED
|
@@ -123,6 +123,20 @@ class Config:
|
|
|
123
123
|
:param snipeit_token: Token used to authenticate to the SnipeIT data provider. Optional.
|
|
124
124
|
:type snipeit_tenant_id: string
|
|
125
125
|
:param snipeit_tenant_id: Token used to authenticate to the SnipeIT data provider. Optional.
|
|
126
|
+
:type tailscale_token: str
|
|
127
|
+
:param tailscale_token: Tailscale API token. Optional.
|
|
128
|
+
:type tailscale_org: str
|
|
129
|
+
:param tailscale_org: Tailscale organization name. Optional.
|
|
130
|
+
:type tailscale_base_url: str
|
|
131
|
+
:param tailscale_base_url: Tailscale API base URL. Optional.
|
|
132
|
+
:type cloudflare_token: string
|
|
133
|
+
:param cloudflare_token: Cloudflare API key. Optional.
|
|
134
|
+
:type openai_apikey: string
|
|
135
|
+
:param openai_apikey: OpenAI API key. Optional.
|
|
136
|
+
:type openai_org_id: string
|
|
137
|
+
:param openai_org_id: OpenAI organization id. Optional.
|
|
138
|
+
:type anthropic_apikey: string
|
|
139
|
+
:param anthropic_apikey: Anthropic API key. Optional.
|
|
126
140
|
"""
|
|
127
141
|
|
|
128
142
|
def __init__(
|
|
@@ -188,6 +202,13 @@ class Config:
|
|
|
188
202
|
snipeit_base_uri=None,
|
|
189
203
|
snipeit_token=None,
|
|
190
204
|
snipeit_tenant_id=None,
|
|
205
|
+
tailscale_token=None,
|
|
206
|
+
tailscale_org=None,
|
|
207
|
+
tailscale_base_url=None,
|
|
208
|
+
cloudflare_token=None,
|
|
209
|
+
openai_apikey=None,
|
|
210
|
+
openai_org_id=None,
|
|
211
|
+
anthropic_apikey=None,
|
|
191
212
|
):
|
|
192
213
|
self.neo4j_uri = neo4j_uri
|
|
193
214
|
self.neo4j_user = neo4j_user
|
|
@@ -250,3 +271,10 @@ class Config:
|
|
|
250
271
|
self.snipeit_base_uri = snipeit_base_uri
|
|
251
272
|
self.snipeit_token = snipeit_token
|
|
252
273
|
self.snipeit_tenant_id = snipeit_tenant_id
|
|
274
|
+
self.tailscale_token = tailscale_token
|
|
275
|
+
self.tailscale_org = tailscale_org
|
|
276
|
+
self.tailscale_base_url = tailscale_base_url
|
|
277
|
+
self.cloudflare_token = cloudflare_token
|
|
278
|
+
self.openai_apikey = openai_apikey
|
|
279
|
+
self.openai_org_id = openai_org_id
|
|
280
|
+
self.anthropic_apikey = anthropic_apikey
|
|
@@ -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
|
|
@@ -0,0 +1,95 @@
|
|
|
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.workspace import AnthropicWorkspaceSchema
|
|
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
|
+
) -> list[dict]:
|
|
25
|
+
org_id, workspaces = get(
|
|
26
|
+
api_session,
|
|
27
|
+
common_job_parameters["BASE_URL"],
|
|
28
|
+
)
|
|
29
|
+
common_job_parameters["ORG_ID"] = org_id
|
|
30
|
+
for workspace in workspaces:
|
|
31
|
+
workspace["users"] = []
|
|
32
|
+
workspace["admins"] = []
|
|
33
|
+
for user in get_workspace_users(
|
|
34
|
+
api_session,
|
|
35
|
+
common_job_parameters["BASE_URL"],
|
|
36
|
+
workspace["id"],
|
|
37
|
+
):
|
|
38
|
+
workspace["users"].append(user["user_id"])
|
|
39
|
+
if user["workspace_role"] == "workspace_admin":
|
|
40
|
+
workspace["admins"].append(user["user_id"])
|
|
41
|
+
load_workspaces(
|
|
42
|
+
neo4j_session, workspaces, org_id, common_job_parameters["UPDATE_TAG"]
|
|
43
|
+
)
|
|
44
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
45
|
+
return workspaces
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@timeit
|
|
49
|
+
def get(
|
|
50
|
+
api_session: requests.Session,
|
|
51
|
+
base_url: str,
|
|
52
|
+
) -> Tuple[str, list[dict[str, Any]]]:
|
|
53
|
+
return paginated_get(
|
|
54
|
+
api_session, f"{base_url}/organizations/workspaces", timeout=_TIMEOUT
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@timeit
|
|
59
|
+
def get_workspace_users(
|
|
60
|
+
api_session: requests.Session,
|
|
61
|
+
base_url: str,
|
|
62
|
+
workspace_id: str,
|
|
63
|
+
) -> list[dict[str, Any]]:
|
|
64
|
+
_, result = paginated_get(
|
|
65
|
+
api_session,
|
|
66
|
+
f"{base_url}/organizations/workspaces/{workspace_id}/members",
|
|
67
|
+
timeout=_TIMEOUT,
|
|
68
|
+
)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@timeit
|
|
73
|
+
def load_workspaces(
|
|
74
|
+
neo4j_session: neo4j.Session,
|
|
75
|
+
data: list[dict[str, Any]],
|
|
76
|
+
ORG_ID: str,
|
|
77
|
+
update_tag: int,
|
|
78
|
+
) -> None:
|
|
79
|
+
logger.info("Loading %d Anthropic workspaces into Neo4j.", len(data))
|
|
80
|
+
load(
|
|
81
|
+
neo4j_session,
|
|
82
|
+
AnthropicWorkspaceSchema(),
|
|
83
|
+
data,
|
|
84
|
+
lastupdated=update_tag,
|
|
85
|
+
ORG_ID=ORG_ID,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@timeit
|
|
90
|
+
def cleanup(
|
|
91
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
92
|
+
) -> None:
|
|
93
|
+
GraphJob.from_node_schema(AnthropicWorkspaceSchema(), common_job_parameters).run(
|
|
94
|
+
neo4j_session
|
|
95
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import neo4j
|
|
8
|
+
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.graph.job import GraphJob
|
|
11
|
+
from cartography.intel.aws.ec2.util import get_botocore_config
|
|
12
|
+
from cartography.models.aws.cloudwatch.loggroup import CloudWatchLogGroupSchema
|
|
13
|
+
from cartography.util import aws_handle_regions
|
|
14
|
+
from cartography.util import timeit
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@timeit
|
|
20
|
+
@aws_handle_regions
|
|
21
|
+
def get_cloudwatch_log_groups(
|
|
22
|
+
boto3_session: boto3.Session, region: str
|
|
23
|
+
) -> List[Dict[str, Any]]:
|
|
24
|
+
client = boto3_session.client(
|
|
25
|
+
"cloudwatch", region_name=region, config=get_botocore_config()
|
|
26
|
+
)
|
|
27
|
+
paginator = client.get_paginator("describe_log_groups")
|
|
28
|
+
logGroups = []
|
|
29
|
+
for page in paginator.paginate():
|
|
30
|
+
logGroups.extend(page["logGroups"])
|
|
31
|
+
return logGroups
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@timeit
|
|
35
|
+
def load_cloudwatch_log_groups(
|
|
36
|
+
neo4j_session: neo4j.Session,
|
|
37
|
+
data: List[Dict[str, Any]],
|
|
38
|
+
region: str,
|
|
39
|
+
current_aws_account_id: str,
|
|
40
|
+
aws_update_tag: int,
|
|
41
|
+
) -> None:
|
|
42
|
+
logger.info(
|
|
43
|
+
f"Loading CloudWatch {len(data)} log groups for region '{region}' into graph.",
|
|
44
|
+
)
|
|
45
|
+
load(
|
|
46
|
+
neo4j_session,
|
|
47
|
+
CloudWatchLogGroupSchema(),
|
|
48
|
+
data,
|
|
49
|
+
lastupdated=aws_update_tag,
|
|
50
|
+
Region=region,
|
|
51
|
+
AWS_ID=current_aws_account_id,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@timeit
|
|
56
|
+
def cleanup(
|
|
57
|
+
neo4j_session: neo4j.Session,
|
|
58
|
+
common_job_parameters: Dict[str, Any],
|
|
59
|
+
) -> None:
|
|
60
|
+
logger.debug("Running CloudWatch cleanup job.")
|
|
61
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
62
|
+
CloudWatchLogGroupSchema(), common_job_parameters
|
|
63
|
+
)
|
|
64
|
+
cleanup_job.run(neo4j_session)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@timeit
|
|
68
|
+
def sync(
|
|
69
|
+
neo4j_session: neo4j.Session,
|
|
70
|
+
boto3_session: boto3.session.Session,
|
|
71
|
+
regions: List[str],
|
|
72
|
+
current_aws_account_id: str,
|
|
73
|
+
update_tag: int,
|
|
74
|
+
common_job_parameters: Dict[str, Any],
|
|
75
|
+
) -> None:
|
|
76
|
+
for region in regions:
|
|
77
|
+
logger.info(
|
|
78
|
+
f"Syncing CloudWatch for region '{region}' in account '{current_aws_account_id}'.",
|
|
79
|
+
)
|
|
80
|
+
logGroups = get_cloudwatch_log_groups(boto3_session, region)
|
|
81
|
+
group_data: List[Dict[str, Any]] = []
|
|
82
|
+
for logGroup in logGroups:
|
|
83
|
+
group_data.append(logGroup)
|
|
84
|
+
|
|
85
|
+
load_cloudwatch_log_groups(
|
|
86
|
+
neo4j_session,
|
|
87
|
+
group_data,
|
|
88
|
+
region,
|
|
89
|
+
current_aws_account_id,
|
|
90
|
+
update_tag,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -99,7 +99,10 @@ def load_load_balancer_v2s(
|
|
|
99
99
|
SET r.lastupdated = $update_tag
|
|
100
100
|
"""
|
|
101
101
|
for lb in data:
|
|
102
|
-
load_balancer_id = lb
|
|
102
|
+
load_balancer_id = lb.get("DNSName")
|
|
103
|
+
if not load_balancer_id:
|
|
104
|
+
logger.warning("Skipping load balancer entry with missing DNSName: %r", lb)
|
|
105
|
+
continue
|
|
103
106
|
|
|
104
107
|
neo4j_session.run(
|
|
105
108
|
ingest_load_balancer_v2,
|