cartography 0.103.0rc1__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 +97 -1
- cartography/config.py +28 -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 +93 -0
- cartography/intel/aws/ec2/load_balancer_v2s.py +4 -1
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/aws/secretsmanager.py +150 -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/entra/ou.py +21 -5
- 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/cloudtrail/trail.py +24 -0
- cartography/models/aws/cloudwatch/__init__.py +0 -0
- cartography/models/aws/cloudwatch/loggroup.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/core/nodes.py +15 -2
- 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.0.dist-info}/METADATA +8 -4
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0.dist-info}/RECORD +75 -19
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0.dist-info}/WHEEL +1 -1
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.103.0rc1.dist-info → cartography-0.104.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|
|
@@ -4,7 +4,6 @@ from typing import Dict
|
|
|
4
4
|
from typing import List
|
|
5
5
|
|
|
6
6
|
import boto3
|
|
7
|
-
import botocore.exceptions
|
|
8
7
|
import neo4j
|
|
9
8
|
|
|
10
9
|
from cartography.client.core.tx import load
|
|
@@ -25,10 +24,8 @@ def get_cloudtrail_trails(
|
|
|
25
24
|
client = boto3_session.client(
|
|
26
25
|
"cloudtrail", region_name=region, config=get_botocore_config()
|
|
27
26
|
)
|
|
28
|
-
|
|
29
|
-
trails = []
|
|
30
|
-
for page in paginator.paginate():
|
|
31
|
-
trails.extend(page["Trails"])
|
|
27
|
+
|
|
28
|
+
trails = client.describe_trails()["trailList"]
|
|
32
29
|
|
|
33
30
|
# CloudTrail multi-region trails are shown in list_trails,
|
|
34
31
|
# but the get_trail call only works in the home region
|
|
@@ -36,28 +33,6 @@ def get_cloudtrail_trails(
|
|
|
36
33
|
return trails_filtered
|
|
37
34
|
|
|
38
35
|
|
|
39
|
-
@timeit
|
|
40
|
-
def get_cloudtrail_trail(
|
|
41
|
-
boto3_session: boto3.Session,
|
|
42
|
-
region: str,
|
|
43
|
-
trail_name: str,
|
|
44
|
-
) -> Dict[str, Any]:
|
|
45
|
-
client = boto3_session.client(
|
|
46
|
-
"cloudtrail", region_name=region, config=get_botocore_config()
|
|
47
|
-
)
|
|
48
|
-
trail_details: Dict[str, Any] = {}
|
|
49
|
-
try:
|
|
50
|
-
response = client.get_trail(Name=trail_name)
|
|
51
|
-
trail_details = response["Trail"]
|
|
52
|
-
except botocore.exceptions.ClientError as e:
|
|
53
|
-
code = e.response["Error"]["Code"]
|
|
54
|
-
msg = e.response["Error"]["Message"]
|
|
55
|
-
logger.warning(
|
|
56
|
-
f"Could not run CloudTrail get_trail due to boto3 error {code}: {msg}. Skipping.",
|
|
57
|
-
)
|
|
58
|
-
return trail_details
|
|
59
|
-
|
|
60
|
-
|
|
61
36
|
@timeit
|
|
62
37
|
def load_cloudtrail_trails(
|
|
63
38
|
neo4j_session: neo4j.Session,
|
|
@@ -105,20 +80,10 @@ def sync(
|
|
|
105
80
|
f"Syncing CloudTrail for region '{region}' in account '{current_aws_account_id}'.",
|
|
106
81
|
)
|
|
107
82
|
trails = get_cloudtrail_trails(boto3_session, region)
|
|
108
|
-
trail_data: List[Dict[str, Any]] = []
|
|
109
|
-
for trail in trails:
|
|
110
|
-
trail_name = trail["Name"]
|
|
111
|
-
trail_details = get_cloudtrail_trail(
|
|
112
|
-
boto3_session,
|
|
113
|
-
region,
|
|
114
|
-
trail_name,
|
|
115
|
-
)
|
|
116
|
-
if trail_details:
|
|
117
|
-
trail_data.append(trail_details)
|
|
118
83
|
|
|
119
84
|
load_cloudtrail_trails(
|
|
120
85
|
neo4j_session,
|
|
121
|
-
|
|
86
|
+
trails,
|
|
122
87
|
region,
|
|
123
88
|
current_aws_account_id,
|
|
124
89
|
update_tag,
|
|
@@ -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
|
+
"logs", 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,
|
|
@@ -5,6 +5,7 @@ from cartography.intel.aws.ec2.route_tables import sync_route_tables
|
|
|
5
5
|
|
|
6
6
|
from . import apigateway
|
|
7
7
|
from . import cloudtrail
|
|
8
|
+
from . import cloudwatch
|
|
8
9
|
from . import config
|
|
9
10
|
from . import dynamodb
|
|
10
11
|
from . import ecr
|
|
@@ -102,4 +103,5 @@ RESOURCE_FUNCTIONS: Dict[str, Callable[..., None]] = {
|
|
|
102
103
|
"config": config.sync,
|
|
103
104
|
"identitycenter": identitycenter.sync_identity_center_instances,
|
|
104
105
|
"cloudtrail": cloudtrail.sync,
|
|
106
|
+
"cloudwatch": cloudwatch.sync,
|
|
105
107
|
}
|
|
@@ -5,12 +5,20 @@ from typing import List
|
|
|
5
5
|
import boto3
|
|
6
6
|
import neo4j
|
|
7
7
|
|
|
8
|
+
from cartography.client.core.tx import load
|
|
9
|
+
from cartography.graph.job import GraphJob
|
|
10
|
+
from cartography.models.aws.secretsmanager.secret_version import (
|
|
11
|
+
SecretsManagerSecretVersionSchema,
|
|
12
|
+
)
|
|
13
|
+
from cartography.stats import get_stats_client
|
|
8
14
|
from cartography.util import aws_handle_regions
|
|
9
15
|
from cartography.util import dict_date_to_epoch
|
|
16
|
+
from cartography.util import merge_module_sync_metadata
|
|
10
17
|
from cartography.util import run_cleanup_job
|
|
11
18
|
from cartography.util import timeit
|
|
12
19
|
|
|
13
20
|
logger = logging.getLogger(__name__)
|
|
21
|
+
stat_handler = get_stats_client(__name__)
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
@timeit
|
|
@@ -76,6 +84,107 @@ def cleanup_secrets(neo4j_session: neo4j.Session, common_job_parameters: Dict) -
|
|
|
76
84
|
)
|
|
77
85
|
|
|
78
86
|
|
|
87
|
+
@timeit
|
|
88
|
+
@aws_handle_regions
|
|
89
|
+
def get_secret_versions(
|
|
90
|
+
boto3_session: boto3.session.Session, region: str, secret_arn: str
|
|
91
|
+
) -> List[Dict]:
|
|
92
|
+
"""
|
|
93
|
+
Get all versions of a secret from AWS Secrets Manager.
|
|
94
|
+
|
|
95
|
+
Note: list_secret_version_ids is not paginatable through boto3's paginator,
|
|
96
|
+
so we implement manual pagination.
|
|
97
|
+
"""
|
|
98
|
+
client = boto3_session.client("secretsmanager", region_name=region)
|
|
99
|
+
next_token = None
|
|
100
|
+
versions = []
|
|
101
|
+
|
|
102
|
+
while True:
|
|
103
|
+
params = {"SecretId": secret_arn, "IncludeDeprecated": True}
|
|
104
|
+
if next_token:
|
|
105
|
+
params["NextToken"] = next_token
|
|
106
|
+
|
|
107
|
+
response = client.list_secret_version_ids(**params)
|
|
108
|
+
|
|
109
|
+
for version in response.get("Versions", []):
|
|
110
|
+
version["SecretId"] = secret_arn
|
|
111
|
+
version["ARN"] = f"{secret_arn}:version:{version['VersionId']}"
|
|
112
|
+
|
|
113
|
+
versions.extend(response.get("Versions", []))
|
|
114
|
+
|
|
115
|
+
next_token = response.get("NextToken")
|
|
116
|
+
if not next_token:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
return versions
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def transform_secret_versions(
|
|
123
|
+
versions: List[Dict],
|
|
124
|
+
region: str,
|
|
125
|
+
aws_account_id: str,
|
|
126
|
+
) -> List[Dict]:
|
|
127
|
+
"""
|
|
128
|
+
Transform AWS Secrets Manager Secret Versions to match the data model.
|
|
129
|
+
"""
|
|
130
|
+
transformed_data = []
|
|
131
|
+
for version in versions:
|
|
132
|
+
transformed = {
|
|
133
|
+
"ARN": version["ARN"],
|
|
134
|
+
"SecretId": version["SecretId"],
|
|
135
|
+
"VersionId": version["VersionId"],
|
|
136
|
+
"VersionStages": version.get("VersionStages"),
|
|
137
|
+
"CreatedDate": dict_date_to_epoch(version, "CreatedDate"),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if "KmsKeyId" in version and version["KmsKeyId"]:
|
|
141
|
+
transformed["KmsKeyId"] = version["KmsKeyId"]
|
|
142
|
+
|
|
143
|
+
if "Tags" in version and version["Tags"]:
|
|
144
|
+
transformed["Tags"] = version["Tags"]
|
|
145
|
+
|
|
146
|
+
transformed_data.append(transformed)
|
|
147
|
+
|
|
148
|
+
return transformed_data
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@timeit
|
|
152
|
+
def load_secret_versions(
|
|
153
|
+
neo4j_session: neo4j.Session,
|
|
154
|
+
data: List[Dict],
|
|
155
|
+
region: str,
|
|
156
|
+
aws_account_id: str,
|
|
157
|
+
update_tag: int,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Load secret versions into Neo4j using the data model.
|
|
161
|
+
"""
|
|
162
|
+
logger.info(f"Loading {len(data)} Secret Versions for region {region} into graph.")
|
|
163
|
+
|
|
164
|
+
load(
|
|
165
|
+
neo4j_session,
|
|
166
|
+
SecretsManagerSecretVersionSchema(),
|
|
167
|
+
data,
|
|
168
|
+
lastupdated=update_tag,
|
|
169
|
+
Region=region,
|
|
170
|
+
AWS_ID=aws_account_id,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@timeit
|
|
175
|
+
def cleanup_secret_versions(
|
|
176
|
+
neo4j_session: neo4j.Session, common_job_parameters: Dict
|
|
177
|
+
) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Run Secret Versions cleanup job.
|
|
180
|
+
"""
|
|
181
|
+
logger.debug("Running Secret Versions cleanup job.")
|
|
182
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
183
|
+
SecretsManagerSecretVersionSchema(), common_job_parameters
|
|
184
|
+
)
|
|
185
|
+
cleanup_job.run(neo4j_session)
|
|
186
|
+
|
|
187
|
+
|
|
79
188
|
@timeit
|
|
80
189
|
def sync(
|
|
81
190
|
neo4j_session: neo4j.Session,
|
|
@@ -85,12 +194,50 @@ def sync(
|
|
|
85
194
|
update_tag: int,
|
|
86
195
|
common_job_parameters: Dict,
|
|
87
196
|
) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Sync AWS Secrets Manager resources.
|
|
199
|
+
"""
|
|
88
200
|
for region in regions:
|
|
89
201
|
logger.info(
|
|
90
|
-
"Syncing Secrets Manager for region '
|
|
91
|
-
region,
|
|
92
|
-
current_aws_account_id,
|
|
202
|
+
f"Syncing Secrets Manager for region '{region}' in account '{current_aws_account_id}'."
|
|
93
203
|
)
|
|
94
204
|
secrets = get_secret_list(boto3_session, region)
|
|
205
|
+
|
|
95
206
|
load_secrets(neo4j_session, secrets, region, current_aws_account_id, update_tag)
|
|
207
|
+
|
|
208
|
+
all_versions = []
|
|
209
|
+
for secret in secrets:
|
|
210
|
+
logger.info(
|
|
211
|
+
f"Getting versions for secret {secret.get('Name', 'unnamed')} ({secret['ARN']})"
|
|
212
|
+
)
|
|
213
|
+
versions = get_secret_versions(boto3_session, region, secret["ARN"])
|
|
214
|
+
logger.info(
|
|
215
|
+
f"Found {len(versions)} versions for secret {secret.get('Name', 'unnamed')}"
|
|
216
|
+
)
|
|
217
|
+
all_versions.extend(versions)
|
|
218
|
+
|
|
219
|
+
transformed_data = transform_secret_versions(
|
|
220
|
+
all_versions,
|
|
221
|
+
region,
|
|
222
|
+
current_aws_account_id,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
load_secret_versions(
|
|
226
|
+
neo4j_session,
|
|
227
|
+
transformed_data,
|
|
228
|
+
region,
|
|
229
|
+
current_aws_account_id,
|
|
230
|
+
update_tag,
|
|
231
|
+
)
|
|
232
|
+
|
|
96
233
|
cleanup_secrets(neo4j_session, common_job_parameters)
|
|
234
|
+
cleanup_secret_versions(neo4j_session, common_job_parameters)
|
|
235
|
+
|
|
236
|
+
merge_module_sync_metadata(
|
|
237
|
+
neo4j_session,
|
|
238
|
+
group_type="AWSAccount",
|
|
239
|
+
group_id=current_aws_account_id,
|
|
240
|
+
synced_type="SecretsManagerSecretVersion",
|
|
241
|
+
update_tag=update_tag,
|
|
242
|
+
stat_handler=stat_handler,
|
|
243
|
+
)
|
cartography/intel/aws/ssm.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
3
|
+
import re
|
|
2
4
|
from typing import Any
|
|
3
5
|
from typing import Dict
|
|
4
6
|
from typing import List
|
|
@@ -10,6 +12,7 @@ from cartography.client.core.tx import load
|
|
|
10
12
|
from cartography.graph.job import GraphJob
|
|
11
13
|
from cartography.models.aws.ssm.instance_information import SSMInstanceInformationSchema
|
|
12
14
|
from cartography.models.aws.ssm.instance_patch import SSMInstancePatchSchema
|
|
15
|
+
from cartography.models.aws.ssm.parameters import SSMParameterSchema
|
|
13
16
|
from cartography.util import aws_handle_regions
|
|
14
17
|
from cartography.util import dict_date_to_epoch
|
|
15
18
|
from cartography.util import timeit
|
|
@@ -107,6 +110,42 @@ def transform_instance_patches(data_list: List[Dict[str, Any]]) -> List[Dict[str
|
|
|
107
110
|
return data_list
|
|
108
111
|
|
|
109
112
|
|
|
113
|
+
@timeit
|
|
114
|
+
@aws_handle_regions
|
|
115
|
+
def get_ssm_parameters(
|
|
116
|
+
boto3_session: boto3.session.Session,
|
|
117
|
+
region: str,
|
|
118
|
+
) -> List[Dict[str, Any]]:
|
|
119
|
+
client = boto3_session.client("ssm", region_name=region)
|
|
120
|
+
paginator = client.get_paginator("describe_parameters")
|
|
121
|
+
ssm_parameters_data: List[Dict[str, Any]] = []
|
|
122
|
+
for page in paginator.paginate(PaginationConfig={"PageSize": 50}):
|
|
123
|
+
ssm_parameters_data.extend(page.get("Parameters", []))
|
|
124
|
+
return ssm_parameters_data
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def transform_ssm_parameters(
|
|
128
|
+
raw_parameters_data: List[Dict[str, Any]],
|
|
129
|
+
) -> List[Dict[str, Any]]:
|
|
130
|
+
transformed_list: List[Dict[str, Any]] = []
|
|
131
|
+
for param in raw_parameters_data:
|
|
132
|
+
param["LastModifiedDate"] = dict_date_to_epoch(param, "LastModifiedDate")
|
|
133
|
+
param["PoliciesJson"] = json.dumps(param.get("Policies", []))
|
|
134
|
+
# KMSKey uses shorter UUID as their primary id
|
|
135
|
+
# SSM Parameters, when encrypted, reference KMS keys using their full ARNs in the KeyId field
|
|
136
|
+
# Adding a param to match on the id property of the target node
|
|
137
|
+
if param.get("Type") == "SecureString" and param.get("KeyId") is not None:
|
|
138
|
+
match = re.match(r".*key/(.*)$", param["KeyId"])
|
|
139
|
+
if match:
|
|
140
|
+
param["KMSKeyIdShort"] = match.group(1)
|
|
141
|
+
else:
|
|
142
|
+
param["KMSKeyIdShort"] = None
|
|
143
|
+
else:
|
|
144
|
+
param["KMSKeyIdShort"] = None
|
|
145
|
+
transformed_list.append(param)
|
|
146
|
+
return transformed_list
|
|
147
|
+
|
|
148
|
+
|
|
110
149
|
@timeit
|
|
111
150
|
def load_instance_information(
|
|
112
151
|
neo4j_session: neo4j.Session,
|
|
@@ -143,6 +182,24 @@ def load_instance_patches(
|
|
|
143
182
|
)
|
|
144
183
|
|
|
145
184
|
|
|
185
|
+
@timeit
|
|
186
|
+
def load_ssm_parameters(
|
|
187
|
+
neo4j_session: neo4j.Session,
|
|
188
|
+
data: List[Dict[str, Any]],
|
|
189
|
+
region: str,
|
|
190
|
+
current_aws_account_id: str,
|
|
191
|
+
aws_update_tag: int,
|
|
192
|
+
) -> None:
|
|
193
|
+
load(
|
|
194
|
+
neo4j_session,
|
|
195
|
+
SSMParameterSchema(),
|
|
196
|
+
data,
|
|
197
|
+
lastupdated=aws_update_tag,
|
|
198
|
+
Region=region,
|
|
199
|
+
AWS_ID=current_aws_account_id,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
146
203
|
@timeit
|
|
147
204
|
def cleanup_ssm(
|
|
148
205
|
neo4j_session: neo4j.Session,
|
|
@@ -156,6 +213,9 @@ def cleanup_ssm(
|
|
|
156
213
|
GraphJob.from_node_schema(SSMInstancePatchSchema(), common_job_parameters).run(
|
|
157
214
|
neo4j_session,
|
|
158
215
|
)
|
|
216
|
+
GraphJob.from_node_schema(SSMParameterSchema(), common_job_parameters).run(
|
|
217
|
+
neo4j_session,
|
|
218
|
+
)
|
|
159
219
|
|
|
160
220
|
|
|
161
221
|
@timeit
|
|
@@ -193,4 +253,15 @@ def sync(
|
|
|
193
253
|
current_aws_account_id,
|
|
194
254
|
update_tag,
|
|
195
255
|
)
|
|
256
|
+
|
|
257
|
+
data = get_ssm_parameters(boto3_session, region)
|
|
258
|
+
data = transform_ssm_parameters(data)
|
|
259
|
+
load_ssm_parameters(
|
|
260
|
+
neo4j_session,
|
|
261
|
+
data,
|
|
262
|
+
region,
|
|
263
|
+
current_aws_account_id,
|
|
264
|
+
update_tag,
|
|
265
|
+
)
|
|
266
|
+
|
|
196
267
|
cleanup_ssm(neo4j_session, common_job_parameters)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import neo4j
|
|
4
|
+
from cloudflare import Cloudflare
|
|
5
|
+
|
|
6
|
+
import cartography.intel.cloudflare.accounts
|
|
7
|
+
import cartography.intel.cloudflare.dnsrecords
|
|
8
|
+
import cartography.intel.cloudflare.members
|
|
9
|
+
import cartography.intel.cloudflare.roles
|
|
10
|
+
import cartography.intel.cloudflare.zones
|
|
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_cloudflare_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
19
|
+
"""
|
|
20
|
+
If this module is configured, perform ingestion of Cloudflare 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.cloudflare_token:
|
|
27
|
+
logger.info(
|
|
28
|
+
"Cloudflare import is not configured - skipping this module. "
|
|
29
|
+
"See docs to configure.",
|
|
30
|
+
)
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# Create client
|
|
34
|
+
client = Cloudflare(api_token=config.cloudflare_token)
|
|
35
|
+
|
|
36
|
+
common_job_parameters = {
|
|
37
|
+
"UPDATE_TAG": config.update_tag,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for account in cartography.intel.cloudflare.accounts.sync(
|
|
41
|
+
neo4j_session,
|
|
42
|
+
client,
|
|
43
|
+
common_job_parameters,
|
|
44
|
+
):
|
|
45
|
+
account_job_parameters = common_job_parameters.copy()
|
|
46
|
+
account_job_parameters["account_id"] = account["id"]
|
|
47
|
+
cartography.intel.cloudflare.roles.sync(
|
|
48
|
+
neo4j_session,
|
|
49
|
+
client,
|
|
50
|
+
account_job_parameters,
|
|
51
|
+
account_id=account["id"],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
cartography.intel.cloudflare.members.sync(
|
|
55
|
+
neo4j_session,
|
|
56
|
+
client,
|
|
57
|
+
account_job_parameters,
|
|
58
|
+
account_id=account["id"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for zone in cartography.intel.cloudflare.zones.sync(
|
|
62
|
+
neo4j_session,
|
|
63
|
+
client,
|
|
64
|
+
account_job_parameters,
|
|
65
|
+
account_id=account["id"],
|
|
66
|
+
):
|
|
67
|
+
zone_job_parameters = account_job_parameters.copy()
|
|
68
|
+
zone_job_parameters["zone_id"] = zone["id"]
|
|
69
|
+
cartography.intel.cloudflare.dnsrecords.sync(
|
|
70
|
+
neo4j_session,
|
|
71
|
+
client,
|
|
72
|
+
zone_job_parameters,
|
|
73
|
+
zone_id=zone["id"],
|
|
74
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import neo4j
|
|
7
|
+
from cloudflare import Cloudflare
|
|
8
|
+
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.graph.job import GraphJob
|
|
11
|
+
from cartography.models.cloudflare.account import CloudflareAccountSchema
|
|
12
|
+
from cartography.util import timeit
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@timeit
|
|
18
|
+
def sync(
|
|
19
|
+
neo4j_session: neo4j.Session,
|
|
20
|
+
client: Cloudflare,
|
|
21
|
+
common_job_parameters: Dict[str, Any],
|
|
22
|
+
) -> List[Dict]:
|
|
23
|
+
accounts = get(client)
|
|
24
|
+
load_accounts(
|
|
25
|
+
neo4j_session,
|
|
26
|
+
accounts,
|
|
27
|
+
common_job_parameters["UPDATE_TAG"],
|
|
28
|
+
)
|
|
29
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
30
|
+
return accounts
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@timeit
|
|
34
|
+
def get(client: Cloudflare) -> List[Dict[str, Any]]:
|
|
35
|
+
return [account.to_dict() for account in client.accounts.list()]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_accounts(
|
|
39
|
+
neo4j_session: neo4j.Session,
|
|
40
|
+
data: List[Dict[str, Any]],
|
|
41
|
+
update_tag: int,
|
|
42
|
+
) -> None:
|
|
43
|
+
logger.info("Loading %d Cloudflare accounts into Neo4j.", len(data))
|
|
44
|
+
load(
|
|
45
|
+
neo4j_session,
|
|
46
|
+
CloudflareAccountSchema(),
|
|
47
|
+
data,
|
|
48
|
+
lastupdated=update_tag,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cleanup(
|
|
53
|
+
neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]
|
|
54
|
+
) -> None:
|
|
55
|
+
GraphJob.from_node_schema(CloudflareAccountSchema(), common_job_parameters).run(
|
|
56
|
+
neo4j_session
|
|
57
|
+
)
|