runbooks 0.7.0__py3-none-any.whl → 0.7.6__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.
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,296 @@
|
|
1
|
+
"""
|
2
|
+
🚨 HIGH-RISK: CloudTrail S3 Policy Modifications - Audit trail security operations.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
|
9
|
+
import click
|
10
|
+
from botocore.exceptions import ClientError
|
11
|
+
|
12
|
+
from .commons import display_aws_account_info, get_client
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
def apply_policy(policy_json: dict, bucket_name: str, backup_enabled: bool = True):
|
18
|
+
"""Apply S3 bucket policy with backup and validation."""
|
19
|
+
try:
|
20
|
+
s3 = get_client("s3")
|
21
|
+
|
22
|
+
# Backup existing policy before applying new one
|
23
|
+
backup_policy = None
|
24
|
+
if backup_enabled:
|
25
|
+
try:
|
26
|
+
backup_response = s3.get_bucket_policy(Bucket=bucket_name)
|
27
|
+
backup_policy = backup_response["Policy"]
|
28
|
+
logger.info(f"✅ Backed up existing policy for bucket: {bucket_name}")
|
29
|
+
except ClientError as e:
|
30
|
+
if e.response.get("Error", {}).get("Code") != "NoSuchBucketPolicy":
|
31
|
+
logger.warning(f"Could not backup policy: {e}")
|
32
|
+
|
33
|
+
# Apply the new policy
|
34
|
+
s3.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy_json))
|
35
|
+
logger.info(f"✅ Applied new policy to bucket: {bucket_name}")
|
36
|
+
|
37
|
+
return backup_policy
|
38
|
+
|
39
|
+
except ClientError as e:
|
40
|
+
logger.error(f"❌ Failed to apply policy to bucket '{bucket_name}': {e}")
|
41
|
+
raise
|
42
|
+
|
43
|
+
|
44
|
+
def get_s3_policy_modifications(user_email, start_time=None, end_time=None):
|
45
|
+
"""Search CloudTrail for S3 policy modifications by a specific user."""
|
46
|
+
try:
|
47
|
+
cloudtrail = get_client("cloudtrail")
|
48
|
+
config = get_client("config")
|
49
|
+
|
50
|
+
# Set default time range if not provided (last 30 days)
|
51
|
+
if not end_time:
|
52
|
+
end_time = datetime.utcnow()
|
53
|
+
if not start_time:
|
54
|
+
start_time = end_time - timedelta(days=30)
|
55
|
+
|
56
|
+
logger.info(f"🔍 Searching CloudTrail events from {start_time} to {end_time}")
|
57
|
+
|
58
|
+
# Define CloudTrail LookupEvent parameters
|
59
|
+
event_selector = {
|
60
|
+
"LookupAttributes": [
|
61
|
+
{"AttributeKey": "EventName", "AttributeValue": "PutBucketPolicy"},
|
62
|
+
{"AttributeKey": "ResourceType", "AttributeValue": "AWS::S3::Bucket"},
|
63
|
+
],
|
64
|
+
"StartTime": start_time,
|
65
|
+
"EndTime": end_time,
|
66
|
+
}
|
67
|
+
|
68
|
+
response = cloudtrail.lookup_events(**event_selector)
|
69
|
+
events = response.get("Events", [])
|
70
|
+
|
71
|
+
logger.info(f"Found {len(events)} S3 policy modification events")
|
72
|
+
|
73
|
+
modifications = []
|
74
|
+
|
75
|
+
for event in events:
|
76
|
+
try:
|
77
|
+
cloudtrail_event = json.loads(event["CloudTrailEvent"])
|
78
|
+
user_identity = cloudtrail_event.get("userIdentity", {})
|
79
|
+
|
80
|
+
# Check for modifications by the specified user (multiple ways to match)
|
81
|
+
user_matches = False
|
82
|
+
principal_id = user_identity.get("principalId", "")
|
83
|
+
user_name = user_identity.get("userName", "")
|
84
|
+
arn = user_identity.get("arn", "")
|
85
|
+
|
86
|
+
if user_email in principal_id or user_email in user_name or user_email in arn:
|
87
|
+
user_matches = True
|
88
|
+
|
89
|
+
if user_matches:
|
90
|
+
# Extract bucket name and policy changes
|
91
|
+
request_params = cloudtrail_event.get("requestParameters", {})
|
92
|
+
bucket_name = request_params.get("bucketName")
|
93
|
+
new_policy = request_params.get("bucketPolicy")
|
94
|
+
|
95
|
+
if not bucket_name:
|
96
|
+
logger.warning(f"No bucket name found in event: {event.get('EventId', 'Unknown')}")
|
97
|
+
continue
|
98
|
+
|
99
|
+
logger.debug(f"Found modification to bucket: {bucket_name}")
|
100
|
+
|
101
|
+
# Try to get previous policy from AWS Config
|
102
|
+
old_policy = None
|
103
|
+
try:
|
104
|
+
config_response = config.get_resource_config_history(
|
105
|
+
resourceType="AWS::S3::Bucket",
|
106
|
+
resourceId=bucket_name,
|
107
|
+
laterTime=event["EventTime"],
|
108
|
+
limit=1,
|
109
|
+
)
|
110
|
+
|
111
|
+
if config_response.get("configurationItems"):
|
112
|
+
old_config = config_response["configurationItems"][0]
|
113
|
+
bucket_policy_config = old_config.get("supplementaryConfiguration", {}).get("BucketPolicy")
|
114
|
+
|
115
|
+
if bucket_policy_config:
|
116
|
+
policy_data = json.loads(bucket_policy_config)
|
117
|
+
if policy_data.get("policyText"):
|
118
|
+
old_policy = json.loads(policy_data["policyText"])
|
119
|
+
|
120
|
+
except ClientError as e:
|
121
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
122
|
+
if error_code == "ResourceNotDiscoveredException":
|
123
|
+
logger.debug(f"Bucket {bucket_name} not tracked in Config")
|
124
|
+
else:
|
125
|
+
logger.warning(f"Config query failed for {bucket_name}: {e}")
|
126
|
+
|
127
|
+
modification = {
|
128
|
+
"BucketName": bucket_name,
|
129
|
+
"NewPolicy": new_policy,
|
130
|
+
"OldPolicy": old_policy,
|
131
|
+
"EventTime": event["EventTime"],
|
132
|
+
"EventId": event.get("EventId"),
|
133
|
+
"UserIdentity": user_identity,
|
134
|
+
}
|
135
|
+
|
136
|
+
modifications.append(modification)
|
137
|
+
|
138
|
+
except (json.JSONDecodeError, KeyError) as e:
|
139
|
+
logger.warning(f"Failed to parse CloudTrail event: {e}")
|
140
|
+
continue
|
141
|
+
|
142
|
+
logger.info(f"Found {len(modifications)} policy modifications by user: {user_email}")
|
143
|
+
return modifications
|
144
|
+
|
145
|
+
except ClientError as e:
|
146
|
+
logger.error(f"Failed to query CloudTrail: {e}")
|
147
|
+
raise
|
148
|
+
|
149
|
+
|
150
|
+
@click.command()
|
151
|
+
@click.option("--email", required=True, help="User email to check for S3 policy modifications")
|
152
|
+
@click.option("--days", default=30, help="Number of days to look back for modifications")
|
153
|
+
@click.option("--dry-run", is_flag=True, default=True, help="Preview mode - show analysis without reverting policies")
|
154
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompts (dangerous!)")
|
155
|
+
def cloudtrail_s3_modifications(email, days, dry_run, confirm):
|
156
|
+
"""🚨 HIGH-RISK: Analyze and potentially revert S3 policy modifications from CloudTrail."""
|
157
|
+
|
158
|
+
# HIGH-RISK OPERATION WARNING
|
159
|
+
if not dry_run and not confirm:
|
160
|
+
logger.warning("🚨 HIGH-RISK OPERATION: S3 Policy Reversion")
|
161
|
+
logger.warning("This operation can modify S3 bucket policies based on CloudTrail analysis")
|
162
|
+
if not click.confirm("Do you want to continue?"):
|
163
|
+
logger.info("Operation cancelled by user")
|
164
|
+
return
|
165
|
+
|
166
|
+
logger.info(f"🔍 CloudTrail S3 policy analysis in {display_aws_account_info()}")
|
167
|
+
logger.info(f"Analyzing modifications by user: {email}")
|
168
|
+
logger.info(f"Looking back {days} days")
|
169
|
+
|
170
|
+
try:
|
171
|
+
# Set time range
|
172
|
+
end_time = datetime.utcnow()
|
173
|
+
start_time = end_time - timedelta(days=days)
|
174
|
+
|
175
|
+
# Get policy modifications
|
176
|
+
policy_changes = get_s3_policy_modifications(email, start_time, end_time)
|
177
|
+
|
178
|
+
if not policy_changes:
|
179
|
+
logger.info(f"✅ No S3 bucket policy modifications found for user: {email}")
|
180
|
+
return
|
181
|
+
|
182
|
+
logger.warning(f"⚠ Found {len(policy_changes)} policy modifications")
|
183
|
+
|
184
|
+
# Analyze each modification
|
185
|
+
reversion_candidates = []
|
186
|
+
|
187
|
+
for i, change in enumerate(policy_changes, 1):
|
188
|
+
bucket_name = change["BucketName"]
|
189
|
+
event_time = change["EventTime"]
|
190
|
+
event_id = change.get("EventId", "Unknown")
|
191
|
+
|
192
|
+
logger.info(f"\n📋 Modification {i}/{len(policy_changes)}:")
|
193
|
+
logger.info(f" Bucket: {bucket_name}")
|
194
|
+
logger.info(f" Event Time: {event_time}")
|
195
|
+
logger.info(f" Event ID: {event_id}")
|
196
|
+
|
197
|
+
# Show user identity details
|
198
|
+
user_identity = change.get("UserIdentity", {})
|
199
|
+
logger.info(f" User Type: {user_identity.get('type', 'Unknown')}")
|
200
|
+
logger.info(f" Principal ID: {user_identity.get('principalId', 'Unknown')}")
|
201
|
+
|
202
|
+
# Analyze policy changes
|
203
|
+
new_policy = change.get("NewPolicy")
|
204
|
+
old_policy = change.get("OldPolicy")
|
205
|
+
|
206
|
+
if old_policy:
|
207
|
+
logger.info(f" ✅ Previous policy found - reversion possible")
|
208
|
+
|
209
|
+
# Check if current policy still matches the problematic one
|
210
|
+
try:
|
211
|
+
s3 = get_client("s3")
|
212
|
+
current_response = s3.get_bucket_policy(Bucket=bucket_name)
|
213
|
+
current_policy = json.loads(current_response["Policy"])
|
214
|
+
|
215
|
+
# Simple comparison - check if old policy statements are missing
|
216
|
+
old_statements = old_policy.get("Statement", [])
|
217
|
+
current_statements = current_policy.get("Statement", [])
|
218
|
+
|
219
|
+
missing_statements = []
|
220
|
+
for old_stmt in old_statements:
|
221
|
+
if old_stmt not in current_statements:
|
222
|
+
missing_statements.append(old_stmt)
|
223
|
+
|
224
|
+
if missing_statements:
|
225
|
+
logger.warning(f" ⚠ {len(missing_statements)} policy statements appear to be missing")
|
226
|
+
reversion_candidates.append(
|
227
|
+
{
|
228
|
+
"bucket": bucket_name,
|
229
|
+
"old_policy": old_policy,
|
230
|
+
"current_policy": current_policy,
|
231
|
+
"missing_statements": missing_statements,
|
232
|
+
"event_time": event_time,
|
233
|
+
}
|
234
|
+
)
|
235
|
+
else:
|
236
|
+
logger.info(f" ✓ Current policy appears to include previous statements")
|
237
|
+
|
238
|
+
except ClientError as e:
|
239
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
240
|
+
if error_code == "NoSuchBucketPolicy":
|
241
|
+
logger.warning(f" ⚠ Bucket currently has no policy")
|
242
|
+
else:
|
243
|
+
logger.error(f" ❌ Failed to get current policy: {e}")
|
244
|
+
|
245
|
+
else:
|
246
|
+
logger.warning(f" ⚠ No previous policy found - cannot revert")
|
247
|
+
|
248
|
+
# Show policy details if verbose
|
249
|
+
logger.debug(f" New Policy: {json.dumps(new_policy, indent=2) if new_policy else 'None'}")
|
250
|
+
logger.debug(f" Old Policy: {json.dumps(old_policy, indent=2) if old_policy else 'None'}")
|
251
|
+
|
252
|
+
# Summary and reversion
|
253
|
+
logger.info(f"\n=== ANALYSIS SUMMARY ===")
|
254
|
+
logger.info(f"Total modifications found: {len(policy_changes)}")
|
255
|
+
logger.info(f"Buckets eligible for reversion: {len(reversion_candidates)}")
|
256
|
+
|
257
|
+
if reversion_candidates:
|
258
|
+
logger.warning(f"⚠ {len(reversion_candidates)} buckets have potential policy issues")
|
259
|
+
|
260
|
+
if dry_run:
|
261
|
+
logger.info("DRY-RUN: Would attempt to revert the following buckets:")
|
262
|
+
for candidate in reversion_candidates:
|
263
|
+
logger.info(f" - {candidate['bucket']} (modified: {candidate['event_time']})")
|
264
|
+
logger.info("To perform actual reversion, run with --no-dry-run")
|
265
|
+
else:
|
266
|
+
# Perform reversions with confirmation
|
267
|
+
for candidate in reversion_candidates:
|
268
|
+
bucket_name = candidate["bucket"]
|
269
|
+
old_policy = candidate["old_policy"]
|
270
|
+
|
271
|
+
if not confirm:
|
272
|
+
logger.warning(f"\n🚨 REVERT BUCKET POLICY:")
|
273
|
+
logger.warning(f" Bucket: {bucket_name}")
|
274
|
+
logger.warning(f" Event Time: {candidate['event_time']}")
|
275
|
+
if not click.confirm(f"Revert policy for bucket {bucket_name}?"):
|
276
|
+
logger.info(f"Skipped reversion for {bucket_name}")
|
277
|
+
continue
|
278
|
+
|
279
|
+
logger.info(f"🔄 Reverting policy for bucket: {bucket_name}")
|
280
|
+
try:
|
281
|
+
backup_policy = apply_policy(old_policy, bucket_name, backup_enabled=True)
|
282
|
+
logger.info(f"✅ Successfully reverted policy for: {bucket_name}")
|
283
|
+
|
284
|
+
# Log the reversion for audit
|
285
|
+
logger.info(f"🔍 Audit: Policy reversion completed")
|
286
|
+
logger.info(f" Bucket: {bucket_name}")
|
287
|
+
logger.info(f" Original Event Time: {candidate['event_time']}")
|
288
|
+
|
289
|
+
except Exception as e:
|
290
|
+
logger.error(f"❌ Failed to revert policy for {bucket_name}: {e}")
|
291
|
+
else:
|
292
|
+
logger.info("✅ No policy reversions needed")
|
293
|
+
|
294
|
+
except Exception as e:
|
295
|
+
logger.error(f"❌ CloudTrail analysis failed: {e}")
|
296
|
+
raise
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"""
|
2
|
+
Cognito User Analysis - List active users in Cognito User Pools.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
|
7
|
+
import click
|
8
|
+
from botocore.exceptions import ClientError
|
9
|
+
|
10
|
+
from .commons import get_client, write_to_csv
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
@click.command()
|
16
|
+
@click.option("--user-pool-id", prompt="User Pool ID", help="The ID of the Cognito User Pool")
|
17
|
+
@click.option("--output-file", default="cognito_active_users.csv", help="Output CSV file path")
|
18
|
+
def get_active_users_in_cognito_pool(user_pool_id, output_file):
|
19
|
+
"""Get active users in a Cognito User Pool and export to CSV."""
|
20
|
+
logger.info(f"Getting active users from Cognito User Pool: {user_pool_id}")
|
21
|
+
|
22
|
+
try:
|
23
|
+
client_cognito = get_client("cognito-idp")
|
24
|
+
|
25
|
+
# Get active users (enabled status)
|
26
|
+
response = client_cognito.list_users(UserPoolId=user_pool_id, Filter='status="Enabled"')
|
27
|
+
|
28
|
+
users = response.get("Users", [])
|
29
|
+
|
30
|
+
if not users:
|
31
|
+
logger.info("No active users found in the user pool")
|
32
|
+
return
|
33
|
+
|
34
|
+
logger.info(f"Found {len(users)} active users")
|
35
|
+
|
36
|
+
# Process user data
|
37
|
+
user_data = []
|
38
|
+
for user in users:
|
39
|
+
user_info = {
|
40
|
+
"username": user.get("Username", ""),
|
41
|
+
"status": user.get("UserStatus", ""),
|
42
|
+
"created_at": user["UserCreateDate"].isoformat() if "UserCreateDate" in user else "",
|
43
|
+
"last_modified_at": user["UserLastModifiedDate"].isoformat() if "UserLastModifiedDate" in user else "",
|
44
|
+
"enabled": user.get("Enabled", False),
|
45
|
+
"email_verified": False,
|
46
|
+
"phone_verified": False,
|
47
|
+
}
|
48
|
+
|
49
|
+
# Extract email and phone verification status from attributes
|
50
|
+
for attr in user.get("Attributes", []):
|
51
|
+
if attr.get("Name") == "email_verified":
|
52
|
+
user_info["email_verified"] = attr.get("Value") == "true"
|
53
|
+
elif attr.get("Name") == "phone_number_verified":
|
54
|
+
user_info["phone_verified"] = attr.get("Value") == "true"
|
55
|
+
|
56
|
+
user_data.append(user_info)
|
57
|
+
logger.debug(f"Processed user: {user_info['username']}")
|
58
|
+
|
59
|
+
# Export to CSV
|
60
|
+
write_to_csv(user_data, output_file)
|
61
|
+
logger.info(f"User data exported to: {output_file}")
|
62
|
+
|
63
|
+
# Summary
|
64
|
+
enabled_count = sum(1 for u in user_data if u["enabled"])
|
65
|
+
email_verified_count = sum(1 for u in user_data if u["email_verified"])
|
66
|
+
|
67
|
+
logger.info(f"Summary: {enabled_count} enabled users, {email_verified_count} with verified emails")
|
68
|
+
|
69
|
+
except ClientError as e:
|
70
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
71
|
+
if error_code == "ResourceNotFoundException":
|
72
|
+
logger.error(f"User pool not found: {user_pool_id}")
|
73
|
+
else:
|
74
|
+
logger.error(f"Failed to get Cognito users: {e}")
|
75
|
+
raise
|
76
|
+
except Exception as e:
|
77
|
+
logger.error(f"Unexpected error: {e}")
|
78
|
+
raise
|