runbooks 0.6.1__py3-none-any.whl → 0.7.5__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.
- jupyter-agent/.env +2 -0
- jupyter-agent/.gradio/certificate.pem +31 -0
- jupyter-agent/__main__.log +8 -0
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
- jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/README.md +337 -0
- runbooks/finops/__init__.py +2 -4
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/aws_organization.png +0 -0
- 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 +3416 -590
- 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/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_baseline → security}/README.md +191 -68
- runbooks/security/__init__.py +70 -0
- runbooks/{security_baseline → security}/security_baseline_tester.py +5 -3
- runbooks-0.7.5.dist-info/METADATA +606 -0
- {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/RECORD +115 -75
- {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/entry_points.txt +0 -1
- 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/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/security_baseline/requirements.txt +0 -7
- runbooks-0.6.1.dist-info/METADATA +0 -373
- /runbooks/{aws → operate}/tags.json +0 -0
- /runbooks/{security_baseline → remediation/Tests}/__init__.py +0 -0
- /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
- /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
- /runbooks/{security_baseline → security}/checklist/alternate_contacts.py +0 -0
- /runbooks/{security_baseline → security}/checklist/bucket_public_access.py +0 -0
- /runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +0 -0
- /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +0 -0
- /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_access_key.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
- /runbooks/{security_baseline → security}/config-origin.json +0 -0
- /runbooks/{security_baseline → security}/config.json +0 -0
- /runbooks/{security_baseline → security}/permission.json +0 -0
- /runbooks/{security_baseline → security}/report_generator.py +0 -0
- /runbooks/{security_baseline → security}/report_template_en.html +0 -0
- /runbooks/{security_baseline → security}/report_template_jp.html +0 -0
- /runbooks/{security_baseline → security}/report_template_kr.html +0 -0
- /runbooks/{security_baseline → security}/report_template_vn.html +0 -0
- /runbooks/{security_baseline → security}/run_script.py +0 -0
- /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
- /runbooks/{security_baseline → security}/utils/common.py +0 -0
- /runbooks/{security_baseline → security}/utils/enums.py +0 -0
- /runbooks/{security_baseline → security}/utils/language.py +0 -0
- /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
- /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
- {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/WHEEL +0 -0
- {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/top_level.txt +0 -0
runbooks/main.py
CHANGED
@@ -1,10 +1,61 @@
|
|
1
1
|
"""
|
2
|
-
CloudOps Runbooks -
|
2
|
+
CloudOps Runbooks - Enterprise CLI Interface
|
3
3
|
|
4
|
-
|
5
|
-
integrating AWS Cloud Foundations best practices with operational runbooks.
|
4
|
+
## Overview
|
6
5
|
|
7
|
-
|
6
|
+
The `runbooks` command-line interface provides a standardized, enterprise-grade
|
7
|
+
entrypoint for all AWS cloud operations, designed for CloudOps, DevOps, and SRE teams.
|
8
|
+
|
9
|
+
## Design Principles
|
10
|
+
|
11
|
+
- **AI-Agent Friendly**: Predictable command patterns and consistent outputs
|
12
|
+
- **Human-Optimized**: Intuitive syntax with comprehensive help and examples
|
13
|
+
- **KISS Architecture**: Simple commands without legacy complexity
|
14
|
+
- **Enterprise Ready**: Multi-deployment support (CLI, Docker, Lambda, K8s)
|
15
|
+
|
16
|
+
## Command Categories
|
17
|
+
|
18
|
+
### 🔍 Discovery & Assessment
|
19
|
+
- `runbooks inventory` - Resource discovery and inventory operations
|
20
|
+
- `runbooks cfat assess` - Cloud Foundations Assessment Tool
|
21
|
+
- `runbooks security assess` - Security baseline assessment
|
22
|
+
|
23
|
+
### ⚙️ Operations & Automation
|
24
|
+
- `runbooks operate` - AWS resource operations (EC2, S3, DynamoDB, etc.)
|
25
|
+
- `runbooks org` - AWS Organizations management
|
26
|
+
- `runbooks finops` - Cost analysis and financial operations
|
27
|
+
|
28
|
+
## Standardized Options
|
29
|
+
|
30
|
+
All commands support consistent options for enterprise integration:
|
31
|
+
|
32
|
+
- `--profile` - AWS profile selection
|
33
|
+
- `--region` - AWS region targeting
|
34
|
+
- `--dry-run` - Safety mode for testing
|
35
|
+
- `--output` - Format selection (console, json, csv, html, yaml)
|
36
|
+
- `--force` - Override confirmation prompts (for automation)
|
37
|
+
|
38
|
+
## Examples
|
39
|
+
|
40
|
+
```bash
|
41
|
+
# Assessment and Discovery
|
42
|
+
runbooks cfat assess --region us-west-2 --output json
|
43
|
+
runbooks inventory ec2 --profile production --output csv
|
44
|
+
runbooks security assess --output html --output-file security-report.html
|
45
|
+
|
46
|
+
# Operations (with safety)
|
47
|
+
runbooks operate ec2 start --instance-ids i-1234567890abcdef0 --dry-run
|
48
|
+
runbooks operate s3 create-bucket --bucket-name my-bucket --region us-west-2
|
49
|
+
runbooks operate dynamodb create-table --table-name employees
|
50
|
+
|
51
|
+
# Multi-Account Operations
|
52
|
+
runbooks org list-ous --profile management-account
|
53
|
+
runbooks operate ec2 cleanup-unused-volumes --region us-east-1 --force
|
54
|
+
```
|
55
|
+
|
56
|
+
## Documentation
|
57
|
+
|
58
|
+
For comprehensive documentation: https://cloudops.oceansoft.io/cloud-foundation/cfat-assessment-tool.html
|
8
59
|
"""
|
9
60
|
|
10
61
|
import sys
|
@@ -23,84 +74,167 @@ try:
|
|
23
74
|
except ImportError:
|
24
75
|
_HAS_RICH = False
|
25
76
|
|
26
|
-
#
|
77
|
+
# Fallback console implementation
|
27
78
|
class Console:
|
28
79
|
def print(self, *args, **kwargs):
|
29
80
|
print(*args)
|
30
81
|
|
31
|
-
def status(self, message):
|
32
|
-
print(f"Status: {message}")
|
33
|
-
return self
|
34
82
|
|
35
|
-
|
36
|
-
|
83
|
+
from runbooks import __version__
|
84
|
+
from runbooks.cfat.runner import AssessmentRunner
|
85
|
+
from runbooks.config import load_config, save_config
|
86
|
+
from runbooks.inventory.core.collector import InventoryCollector
|
87
|
+
from runbooks.utils import setup_logging
|
37
88
|
|
38
|
-
|
39
|
-
pass
|
89
|
+
console = Console()
|
40
90
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
self.columns = []
|
45
|
-
self.rows = []
|
91
|
+
# ============================================================================
|
92
|
+
# STANDARDIZED CLI OPTIONS (Human & AI-Agent Friendly)
|
93
|
+
# ============================================================================
|
46
94
|
|
47
|
-
def add_column(self, name, style=""):
|
48
|
-
self.columns.append(name)
|
49
95
|
|
50
|
-
|
51
|
-
|
96
|
+
def common_aws_options(f):
|
97
|
+
"""
|
98
|
+
Standard AWS connection and safety options for all commands.
|
52
99
|
|
53
|
-
|
54
|
-
|
55
|
-
return ""
|
100
|
+
Provides consistent AWS configuration across the entire CLI interface,
|
101
|
+
enabling predictable behavior for both human operators and AI agents.
|
56
102
|
|
57
|
-
|
58
|
-
|
59
|
-
output += " | ".join(self.columns) + "\n"
|
60
|
-
output += "-" * (len(" | ".join(self.columns))) + "\n"
|
103
|
+
Args:
|
104
|
+
f: Click command function to decorate
|
61
105
|
|
62
|
-
|
63
|
-
|
106
|
+
Returns:
|
107
|
+
Decorated function with AWS options
|
64
108
|
|
65
|
-
|
109
|
+
Added Options:
|
110
|
+
--profile: AWS profile name (default: 'default')
|
111
|
+
--region: AWS region identifier (default: 'ap-southeast-2')
|
112
|
+
--dry-run: Safety flag to preview operations without execution
|
66
113
|
|
114
|
+
Examples:
|
115
|
+
```bash
|
116
|
+
runbooks inventory ec2 --profile production --region us-west-2 --dry-run
|
117
|
+
runbooks operate s3 create-bucket --profile dev --region eu-west-1
|
118
|
+
```
|
119
|
+
"""
|
120
|
+
f = click.option("--profile", default="default", help="AWS profile (default: 'default')")(f)
|
121
|
+
f = click.option("--region", default="ap-southeast-2", help="AWS region (default: 'ap-southeast-2')")(f)
|
122
|
+
f = click.option("--dry-run", is_flag=True, help="Enable dry-run mode for safety")(f)
|
123
|
+
return f
|
67
124
|
|
68
|
-
from runbooks import __version__
|
69
|
-
from runbooks.cfat.runner import AssessmentRunner
|
70
|
-
from runbooks.config import load_config, save_config
|
71
|
-
from runbooks.inventory.core.collector import InventoryCollector
|
72
|
-
from runbooks.organizations.manager import OUManager
|
73
|
-
from runbooks.utils import setup_logging
|
74
125
|
|
75
|
-
|
126
|
+
def common_output_options(f):
|
127
|
+
"""
|
128
|
+
Standard output formatting options for consistent reporting.
|
129
|
+
|
130
|
+
Enables flexible output formats suitable for different consumption patterns:
|
131
|
+
human readable, automation integration, and data analysis workflows.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
f: Click command function to decorate
|
135
|
+
|
136
|
+
Returns:
|
137
|
+
Decorated function with output options
|
138
|
+
|
139
|
+
Added Options:
|
140
|
+
--output: Format selection (console, json, csv, html, yaml)
|
141
|
+
--output-file: Custom file path for saving results
|
142
|
+
|
143
|
+
Examples:
|
144
|
+
```bash
|
145
|
+
runbooks cfat assess --output json --output-file assessment.json
|
146
|
+
runbooks inventory ec2 --output csv --output-file ec2-inventory.csv
|
147
|
+
runbooks security assess --output html --output-file security-report.html
|
148
|
+
```
|
149
|
+
"""
|
150
|
+
f = click.option(
|
151
|
+
"--output",
|
152
|
+
type=click.Choice(["console", "json", "csv", "html", "yaml"]),
|
153
|
+
default="console",
|
154
|
+
help="Output format",
|
155
|
+
)(f)
|
156
|
+
f = click.option("--output-file", type=click.Path(), help="Output file path (auto-generated if not specified)")(f)
|
157
|
+
return f
|
158
|
+
|
159
|
+
|
160
|
+
def common_filter_options(f):
|
161
|
+
"""
|
162
|
+
Standard resource filtering options for targeted discovery.
|
163
|
+
|
164
|
+
Provides consistent filtering capabilities across all inventory and
|
165
|
+
discovery operations, enabling precise resource targeting for large-scale
|
166
|
+
multi-account AWS environments.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
f: Click command function to decorate
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
Decorated function with filter options
|
173
|
+
|
174
|
+
Added Options:
|
175
|
+
--tags: Tag-based filtering with key=value format (multiple values supported)
|
176
|
+
--accounts: Account ID filtering for multi-account operations
|
177
|
+
--regions: Region-based filtering for multi-region operations
|
178
|
+
|
179
|
+
Examples:
|
180
|
+
```bash
|
181
|
+
runbooks inventory ec2 --tags Environment=production Team=platform
|
182
|
+
runbooks inventory s3 --accounts 123456789012 987654321098
|
183
|
+
runbooks inventory vpc --regions us-east-1 us-west-2 --tags CostCenter=engineering
|
184
|
+
```
|
185
|
+
"""
|
186
|
+
f = click.option("--tags", multiple=True, help="Filter by tags (key=value format)")(f)
|
187
|
+
f = click.option("--accounts", multiple=True, help="Filter by account IDs")(f)
|
188
|
+
f = click.option("--regions", multiple=True, help="Filter by regions")(f)
|
189
|
+
return f
|
190
|
+
|
191
|
+
|
192
|
+
# ============================================================================
|
193
|
+
# MAIN CLI GROUP
|
194
|
+
# ============================================================================
|
76
195
|
|
77
196
|
|
78
197
|
@click.group(invoke_without_command=True)
|
79
198
|
@click.version_option(version=__version__)
|
80
199
|
@click.option("--debug", is_flag=True, help="Enable debug logging")
|
81
|
-
@
|
82
|
-
@click.option("--region", help="AWS region (overrides profile region)")
|
200
|
+
@common_aws_options
|
83
201
|
@click.option("--config", type=click.Path(), help="Configuration file path")
|
84
202
|
@click.pass_context
|
85
|
-
def main(ctx, debug, profile, region, config):
|
203
|
+
def main(ctx, debug, profile, region, dry_run, config):
|
86
204
|
"""
|
87
|
-
CloudOps Runbooks - Enterprise
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
-
|
93
|
-
-
|
94
|
-
-
|
95
|
-
|
205
|
+
CloudOps Runbooks - Enterprise AWS Automation Toolkit v{version}.
|
206
|
+
|
207
|
+
🚀 Unified CLI for comprehensive AWS operations, assessment, and management.
|
208
|
+
|
209
|
+
Quick Commands (New!):
|
210
|
+
• runbooks start i-123456 → Start EC2 instances instantly
|
211
|
+
• runbooks stop i-123456 → Stop EC2 instances instantly
|
212
|
+
• runbooks scan -r ec2,rds → Quick resource discovery
|
213
|
+
|
214
|
+
Full Architecture:
|
215
|
+
• runbooks inventory → Read-only discovery and analysis
|
216
|
+
• runbooks operate → Resource lifecycle operations
|
217
|
+
• runbooks cfat → Cloud Foundations Assessment
|
218
|
+
• runbooks security → Security baseline testing
|
219
|
+
• runbooks org → Organizations management
|
220
|
+
• runbooks finops → Cost and usage analytics
|
221
|
+
|
222
|
+
Safety Features:
|
223
|
+
• --dry-run mode for all operations
|
224
|
+
• Confirmation prompts for destructive actions
|
225
|
+
• Comprehensive logging and audit trails
|
226
|
+
• Type-safe operations with validation
|
227
|
+
|
228
|
+
Examples:
|
229
|
+
runbooks inventory collect --resources ec2,rds --dry-run
|
230
|
+
runbooks operate ec2 start --instance-ids i-123456 --dry-run
|
231
|
+
runbooks cfat assess --categories security --output html
|
232
|
+
runbooks security assess --profile prod --format json
|
233
|
+
""".format(version=__version__)
|
96
234
|
|
97
|
-
|
98
|
-
"""
|
99
|
-
# Initialize context
|
235
|
+
# Initialize context for all subcommands
|
100
236
|
ctx.ensure_object(dict)
|
101
|
-
ctx.obj
|
102
|
-
ctx.obj["profile"] = profile
|
103
|
-
ctx.obj["region"] = region
|
237
|
+
ctx.obj.update({"debug": debug, "profile": profile, "region": region, "dry_run": dry_run})
|
104
238
|
|
105
239
|
# Setup logging
|
106
240
|
setup_logging(debug=debug)
|
@@ -109,676 +243,3368 @@ def main(ctx, debug, profile, region, config):
|
|
109
243
|
config_path = Path(config) if config else Path.home() / ".runbooks" / "config.yaml"
|
110
244
|
ctx.obj["config"] = load_config(config_path)
|
111
245
|
|
112
|
-
# Show help if no command provided
|
113
246
|
if ctx.invoked_subcommand is None:
|
114
247
|
click.echo(ctx.get_help())
|
115
248
|
|
116
249
|
|
117
250
|
# ============================================================================
|
118
|
-
#
|
251
|
+
# INVENTORY COMMANDS (Read-Only Discovery)
|
119
252
|
# ============================================================================
|
120
253
|
|
121
254
|
|
122
|
-
@main.group()
|
255
|
+
@main.group(invoke_without_command=True)
|
256
|
+
@common_aws_options
|
257
|
+
@common_output_options
|
258
|
+
@common_filter_options
|
123
259
|
@click.pass_context
|
124
|
-
def
|
125
|
-
"""
|
126
|
-
|
260
|
+
def inventory(ctx, profile, region, dry_run, output, output_file, tags, accounts, regions):
|
261
|
+
"""
|
262
|
+
Multi-account AWS resource discovery and inventory.
|
127
263
|
|
264
|
+
Read-only operations for comprehensive resource discovery across
|
265
|
+
AWS services, accounts, and regions with advanced filtering.
|
128
266
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
default="console",
|
134
|
-
help="Output format (use 'all' for multiple formats)",
|
135
|
-
)
|
136
|
-
@click.option("--output-file", type=click.Path(), help="Output file path (auto-generated if not specified)")
|
137
|
-
@click.option("--checks", multiple=True, help="Specific checks to run")
|
138
|
-
@click.option("--skip-checks", multiple=True, help="Checks to skip")
|
139
|
-
@click.option("--categories", multiple=True, help="Assessment categories to include")
|
140
|
-
@click.option("--skip-categories", multiple=True, help="Assessment categories to exclude")
|
141
|
-
@click.option("--severity", type=click.Choice(["INFO", "WARNING", "CRITICAL"]), help="Minimum severity level to report")
|
142
|
-
@click.option("--parallel/--sequential", default=True, help="Enable/disable parallel execution")
|
143
|
-
@click.option("--max-workers", type=int, default=10, help="Maximum parallel workers")
|
144
|
-
@click.option("--compliance-framework", help="Target compliance framework (SOC2, PCI-DSS, HIPAA)")
|
145
|
-
@click.option("--export-jira", type=click.Path(), help="Export findings to Jira CSV format")
|
146
|
-
@click.option("--export-asana", type=click.Path(), help="Export findings to Asana CSV format")
|
147
|
-
@click.option("--export-servicenow", type=click.Path(), help="Export findings to ServiceNow JSON format")
|
148
|
-
@click.option("--serve-web", is_flag=True, help="Start web server for interactive reports")
|
149
|
-
@click.option("--web-port", type=int, default=8080, help="Port for web server")
|
150
|
-
@click.pass_context
|
151
|
-
def assess(
|
152
|
-
ctx,
|
153
|
-
output,
|
154
|
-
output_file,
|
155
|
-
checks,
|
156
|
-
skip_checks,
|
157
|
-
categories,
|
158
|
-
skip_categories,
|
159
|
-
severity,
|
160
|
-
parallel,
|
161
|
-
max_workers,
|
162
|
-
compliance_framework,
|
163
|
-
export_jira,
|
164
|
-
export_asana,
|
165
|
-
export_servicenow,
|
166
|
-
serve_web,
|
167
|
-
web_port,
|
168
|
-
):
|
267
|
+
Examples:
|
268
|
+
runbooks inventory collect --resources ec2,rds
|
269
|
+
runbooks inventory collect --accounts 123456789012 --regions us-east-1
|
270
|
+
runbooks inventory collect --tags Environment=prod --output json
|
169
271
|
"""
|
170
|
-
|
272
|
+
# Update context with inventory-specific options
|
273
|
+
ctx.obj.update(
|
274
|
+
{
|
275
|
+
"profile": profile,
|
276
|
+
"region": region,
|
277
|
+
"dry_run": dry_run,
|
278
|
+
"output": output,
|
279
|
+
"output_file": output_file,
|
280
|
+
"tags": tags,
|
281
|
+
"accounts": accounts,
|
282
|
+
"regions": regions,
|
283
|
+
}
|
284
|
+
)
|
171
285
|
|
172
|
-
|
173
|
-
|
174
|
-
features including multi-format reporting, parallel execution,
|
175
|
-
compliance framework alignment, and project management integration.
|
286
|
+
if ctx.invoked_subcommand is None:
|
287
|
+
click.echo(ctx.get_help())
|
176
288
|
|
177
|
-
Examples:
|
178
|
-
# Basic assessment with HTML report
|
179
|
-
runbooks cfat assess --output html --output-file report.html
|
180
289
|
|
181
|
-
|
182
|
-
|
290
|
+
@inventory.command()
|
291
|
+
@click.option("--resources", "-r", multiple=True, help="Resource types (ec2, rds, lambda, s3, etc.)")
|
292
|
+
@click.option("--all-resources", is_flag=True, help="Collect all resource types")
|
293
|
+
@click.option("--all-accounts", is_flag=True, help="Collect from all organization accounts")
|
294
|
+
@click.option("--include-costs", is_flag=True, help="Include cost information")
|
295
|
+
@click.option("--parallel", is_flag=True, default=True, help="Enable parallel collection")
|
296
|
+
@click.pass_context
|
297
|
+
def collect(ctx, resources, all_resources, all_accounts, include_costs, parallel):
|
298
|
+
"""Collect comprehensive AWS resource inventory."""
|
299
|
+
try:
|
300
|
+
console.print(f"[blue]📊 Starting AWS Resource Inventory Collection[/blue]")
|
301
|
+
console.print(f"[dim]Profile: {ctx.obj['profile']} | Region: {ctx.obj['region']} | Parallel: {parallel}[/dim]")
|
183
302
|
|
184
|
-
#
|
185
|
-
|
303
|
+
# Initialize collector
|
304
|
+
collector = InventoryCollector(profile=ctx.obj["profile"], region=ctx.obj["region"], parallel=parallel)
|
186
305
|
|
187
|
-
#
|
188
|
-
|
306
|
+
# Configure resources
|
307
|
+
if all_resources:
|
308
|
+
resource_types = collector.get_all_resource_types()
|
309
|
+
elif resources:
|
310
|
+
resource_types = list(resources)
|
311
|
+
else:
|
312
|
+
resource_types = ["ec2", "rds", "s3", "lambda"]
|
189
313
|
|
190
|
-
#
|
191
|
-
|
314
|
+
# Configure accounts
|
315
|
+
if all_accounts:
|
316
|
+
account_ids = collector.get_organization_accounts()
|
317
|
+
elif ctx.obj.get("accounts"):
|
318
|
+
account_ids = list(ctx.obj["accounts"])
|
319
|
+
else:
|
320
|
+
account_ids = [collector.get_current_account_id()]
|
192
321
|
|
193
|
-
#
|
194
|
-
|
195
|
-
|
196
|
-
|
322
|
+
# Collect inventory
|
323
|
+
with console.status("[bold green]Collecting inventory..."):
|
324
|
+
results = collector.collect_inventory(
|
325
|
+
resource_types=resource_types, account_ids=account_ids, include_costs=include_costs
|
326
|
+
)
|
197
327
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
# Configure categories
|
212
|
-
if categories:
|
213
|
-
runner.assessment_config.included_categories = list(categories)
|
214
|
-
if skip_categories:
|
215
|
-
runner.assessment_config.excluded_categories = list(skip_categories)
|
216
|
-
|
217
|
-
# Configure execution settings
|
218
|
-
runner.assessment_config.parallel_execution = parallel
|
219
|
-
runner.assessment_config.max_workers = max_workers
|
220
|
-
|
221
|
-
# Set compliance framework
|
222
|
-
if compliance_framework:
|
223
|
-
runner.assessment_config.compliance_framework = compliance_framework
|
224
|
-
|
225
|
-
status.update("[bold green]Executing assessment checks...")
|
226
|
-
|
227
|
-
# Run assessment
|
228
|
-
report = runner.run_assessment()
|
328
|
+
# Output results
|
329
|
+
if ctx.obj["output"] == "console":
|
330
|
+
display_inventory_results(results)
|
331
|
+
else:
|
332
|
+
save_inventory_results(results, ctx.obj["output"], ctx.obj["output_file"])
|
333
|
+
|
334
|
+
console.print(f"[green]✅ Inventory collection completed![/green]")
|
335
|
+
|
336
|
+
except Exception as e:
|
337
|
+
console.print(f"[red]❌ Inventory collection failed: {e}[/red]")
|
338
|
+
logger.error(f"Inventory error: {e}")
|
339
|
+
raise click.ClickException(str(e))
|
229
340
|
|
230
|
-
status.update("[bold green]Generating reports...")
|
231
341
|
|
232
|
-
|
233
|
-
|
342
|
+
# ============================================================================
|
343
|
+
# OPERATE COMMANDS (Resource Lifecycle Operations)
|
344
|
+
# ============================================================================
|
234
345
|
|
235
|
-
# Generate output files
|
236
|
-
generated_files = []
|
237
346
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
347
|
+
@main.group(invoke_without_command=True)
|
348
|
+
@common_aws_options
|
349
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompts for destructive operations")
|
350
|
+
@click.pass_context
|
351
|
+
def operate(ctx, profile, region, dry_run, force):
|
352
|
+
"""
|
353
|
+
AWS resource lifecycle operations and automation.
|
242
354
|
|
243
|
-
|
244
|
-
|
355
|
+
Perform operational tasks including creation, modification, and deletion
|
356
|
+
of AWS resources with comprehensive safety features.
|
245
357
|
|
246
|
-
|
247
|
-
|
358
|
+
Safety Features:
|
359
|
+
• Dry-run mode for all operations
|
360
|
+
• Confirmation prompts for destructive actions
|
361
|
+
• Comprehensive logging and audit trails
|
362
|
+
• Operation result tracking and rollback support
|
248
363
|
|
249
|
-
|
250
|
-
|
364
|
+
Examples:
|
365
|
+
runbooks operate ec2 start --instance-ids i-123456 --dry-run
|
366
|
+
runbooks operate s3 create-bucket --bucket-name test --encryption
|
367
|
+
runbooks operate cloudformation deploy --template-file stack.yaml
|
368
|
+
"""
|
369
|
+
ctx.obj.update({"profile": profile, "region": region, "dry_run": dry_run, "force": force})
|
251
370
|
|
252
|
-
|
253
|
-
|
371
|
+
if ctx.invoked_subcommand is None:
|
372
|
+
click.echo(ctx.get_help())
|
254
373
|
|
255
|
-
elif output != "console":
|
256
|
-
# Generate specific format
|
257
|
-
if not output_file:
|
258
|
-
timestamp = report.timestamp.strftime("%Y%m%d_%H%M%S")
|
259
|
-
output_file = f"cfat_report_{timestamp}.{output}"
|
260
374
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
report.to_json(output_file)
|
267
|
-
elif output == "markdown":
|
268
|
-
report.to_markdown(output_file)
|
375
|
+
@operate.group()
|
376
|
+
@click.pass_context
|
377
|
+
def ec2(ctx):
|
378
|
+
"""EC2 instance and resource operations."""
|
379
|
+
pass
|
269
380
|
|
270
|
-
generated_files.append(output_file)
|
271
381
|
|
272
|
-
|
273
|
-
|
274
|
-
|
382
|
+
@ec2.command()
|
383
|
+
@click.option(
|
384
|
+
"--instance-ids",
|
385
|
+
multiple=True,
|
386
|
+
required=True,
|
387
|
+
help="Instance IDs (repeat for multiple). Example: --instance-ids i-1234567890abcdef0",
|
388
|
+
)
|
389
|
+
@click.pass_context
|
390
|
+
def start(ctx, instance_ids):
|
391
|
+
"""Start EC2 instances."""
|
392
|
+
try:
|
393
|
+
from runbooks.inventory.models.account import AWSAccount
|
394
|
+
from runbooks.operate import EC2Operations
|
395
|
+
from runbooks.operate.base import OperationContext
|
396
|
+
|
397
|
+
console.print(f"[blue]⚡ Starting EC2 Instances[/blue]")
|
398
|
+
console.print(f"[dim]Count: {len(instance_ids)} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
399
|
+
|
400
|
+
# Initialize operations
|
401
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
402
|
+
|
403
|
+
# Create context
|
404
|
+
account = AWSAccount(account_id="current", account_name="current")
|
405
|
+
context = OperationContext(
|
406
|
+
account=account,
|
407
|
+
region=ctx.obj["region"],
|
408
|
+
operation_type="start_instances",
|
409
|
+
resource_types=["ec2:instance"],
|
410
|
+
dry_run=ctx.obj["dry_run"],
|
411
|
+
force=ctx.obj.get("force", False),
|
412
|
+
)
|
275
413
|
|
276
|
-
|
277
|
-
|
278
|
-
generated_files.append(export_jira)
|
414
|
+
# Execute operation
|
415
|
+
results = ec2_ops.start_instances(context, list(instance_ids))
|
279
416
|
|
280
|
-
|
281
|
-
|
417
|
+
# Display results
|
418
|
+
successful = sum(1 for r in results if r.success)
|
419
|
+
for result in results:
|
420
|
+
status = "✅" if result.success else "❌"
|
421
|
+
message = result.message if result.success else result.error_message
|
422
|
+
console.print(f"{status} {result.resource_id}: {message}")
|
282
423
|
|
283
|
-
|
284
|
-
exporter.export(report, export_asana)
|
285
|
-
generated_files.append(export_asana)
|
424
|
+
console.print(f"\n[bold]Summary: {successful}/{len(results)} instances started[/bold]")
|
286
425
|
|
287
|
-
|
288
|
-
|
426
|
+
except Exception as e:
|
427
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
428
|
+
console.print(f"[dim]💡 Try: runbooks inventory collect -r ec2 # List available instances[/dim]")
|
429
|
+
console.print(f"[dim]💡 Example: runbooks operate ec2 start --instance-ids i-1234567890abcdef0[/dim]")
|
430
|
+
raise click.ClickException(str(e))
|
289
431
|
|
290
|
-
exporter = ServiceNowExporter()
|
291
|
-
exporter.export(report, export_servicenow)
|
292
|
-
generated_files.append(export_servicenow)
|
293
432
|
|
294
|
-
|
295
|
-
|
296
|
-
|
433
|
+
@ec2.command()
|
434
|
+
@click.option("--instance-ids", multiple=True, required=True, help="Instance IDs (repeat for multiple)")
|
435
|
+
@click.pass_context
|
436
|
+
def stop(ctx, instance_ids):
|
437
|
+
"""Stop EC2 instances."""
|
438
|
+
try:
|
439
|
+
from runbooks.inventory.models.account import AWSAccount
|
440
|
+
from runbooks.operate import EC2Operations
|
441
|
+
from runbooks.operate.base import OperationContext
|
442
|
+
|
443
|
+
console.print(f"[blue]⏹️ Stopping EC2 Instances[/blue]")
|
444
|
+
console.print(f"[dim]Count: {len(instance_ids)} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
445
|
+
|
446
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
447
|
+
|
448
|
+
account = AWSAccount(account_id="current", account_name="current")
|
449
|
+
context = OperationContext(
|
450
|
+
account=account,
|
451
|
+
region=ctx.obj["region"],
|
452
|
+
operation_type="stop_instances",
|
453
|
+
resource_types=["ec2:instance"],
|
454
|
+
dry_run=ctx.obj["dry_run"],
|
455
|
+
force=ctx.obj.get("force", False),
|
456
|
+
)
|
297
457
|
|
298
|
-
|
299
|
-
if generated_files:
|
300
|
-
console.print(f"\n[green]✓ Assessment completed successfully![/green]")
|
301
|
-
console.print(f"[green]✓ Generated files:[/green]")
|
302
|
-
for file in generated_files:
|
303
|
-
console.print(f" • {file}")
|
458
|
+
results = ec2_ops.stop_instances(context, list(instance_ids))
|
304
459
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
console.print(f"
|
310
|
-
console.print(f"• Total Checks: {report.summary.total_checks}")
|
311
|
-
console.print(f"• Pass Rate: {report.summary.pass_rate:.1f}%")
|
460
|
+
successful = sum(1 for r in results if r.success)
|
461
|
+
for result in results:
|
462
|
+
status = "✅" if result.success else "❌"
|
463
|
+
message = result.message if result.success else result.error_message
|
464
|
+
console.print(f"{status} {result.resource_id}: {message}")
|
312
465
|
|
313
|
-
|
314
|
-
logger.error(f"Assessment failed: {e}")
|
315
|
-
console.print(f"[red]✗ Assessment failed: {e}[/red]")
|
316
|
-
sys.exit(1)
|
466
|
+
console.print(f"\n[bold]Summary: {successful}/{len(results)} instances stopped[/bold]")
|
317
467
|
|
468
|
+
except Exception as e:
|
469
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
470
|
+
raise click.ClickException(str(e))
|
318
471
|
|
319
|
-
def start_web_server(report, port: int = 8080):
|
320
|
-
"""
|
321
|
-
Start interactive web server for assessment results.
|
322
472
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
473
|
+
@ec2.command()
|
474
|
+
@click.option("--instance-ids", multiple=True, required=True, help="Instance IDs to terminate (DESTRUCTIVE)")
|
475
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
476
|
+
@click.pass_context
|
477
|
+
def terminate(ctx, instance_ids, confirm):
|
478
|
+
"""Terminate EC2 instances (DESTRUCTIVE - cannot be undone)."""
|
327
479
|
try:
|
328
|
-
import
|
329
|
-
import
|
330
|
-
import
|
331
|
-
|
332
|
-
|
480
|
+
from runbooks.inventory.models.account import AWSAccount
|
481
|
+
from runbooks.operate import EC2Operations
|
482
|
+
from runbooks.operate.base import OperationContext
|
483
|
+
|
484
|
+
console.print(f"[red]💥 Terminating EC2 Instances[/red]")
|
485
|
+
console.print(f"[dim]Count: {len(instance_ids)} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
486
|
+
|
487
|
+
if not ctx.obj["dry_run"] and not confirm and not ctx.obj.get("force", False):
|
488
|
+
console.print("[yellow]⚠️ This action cannot be undone![/yellow]")
|
489
|
+
if not click.confirm("Are you sure you want to terminate these instances?"):
|
490
|
+
console.print("[blue]Operation cancelled[/blue]")
|
491
|
+
return
|
492
|
+
|
493
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
494
|
+
|
495
|
+
account = AWSAccount(account_id="current", account_name="current")
|
496
|
+
context = OperationContext(
|
497
|
+
account=account,
|
498
|
+
region=ctx.obj["region"],
|
499
|
+
operation_type="terminate_instances",
|
500
|
+
resource_types=["ec2:instance"],
|
501
|
+
dry_run=ctx.obj["dry_run"],
|
502
|
+
force=ctx.obj.get("force", False),
|
503
|
+
)
|
504
|
+
|
505
|
+
results = ec2_ops.terminate_instances(context, list(instance_ids))
|
333
506
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
507
|
+
successful = sum(1 for r in results if r.success)
|
508
|
+
for result in results:
|
509
|
+
status = "✅" if result.success else "❌"
|
510
|
+
message = result.message if result.success else result.error_message
|
511
|
+
console.print(f"{status} {result.resource_id}: {message}")
|
338
512
|
|
339
|
-
|
340
|
-
|
513
|
+
console.print(f"\n[bold]Summary: {successful}/{len(results)} instances terminated[/bold]")
|
514
|
+
|
515
|
+
except Exception as e:
|
516
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
517
|
+
raise click.ClickException(str(e))
|
518
|
+
|
519
|
+
|
520
|
+
@ec2.command()
|
521
|
+
@click.option("--image-id", required=True, help="AMI ID to launch")
|
522
|
+
@click.option("--instance-type", default="t2.micro", help="Instance type (default: t2.micro)")
|
523
|
+
@click.option("--count", default=1, help="Number of instances to launch (default: 1)")
|
524
|
+
@click.option("--key-name", help="EC2 key pair name")
|
525
|
+
@click.option("--security-group-ids", multiple=True, help="Security group IDs (repeat for multiple)")
|
526
|
+
@click.option("--subnet-id", help="Subnet ID for VPC placement")
|
527
|
+
@click.option("--user-data", help="User data script")
|
528
|
+
@click.option("--instance-profile", help="IAM instance profile name")
|
529
|
+
@click.option("--tags", multiple=True, help="Instance tags in key=value format")
|
530
|
+
@click.pass_context
|
531
|
+
def run_instances(
|
532
|
+
ctx, image_id, instance_type, count, key_name, security_group_ids, subnet_id, user_data, instance_profile, tags
|
533
|
+
):
|
534
|
+
"""Launch new EC2 instances with comprehensive configuration."""
|
535
|
+
try:
|
536
|
+
from runbooks.inventory.models.account import AWSAccount
|
537
|
+
from runbooks.operate import EC2Operations
|
538
|
+
from runbooks.operate.base import OperationContext
|
341
539
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
console.print(f"[yellow]Press Ctrl+C to stop the server[/yellow]")
|
347
|
-
httpd.serve_forever()
|
540
|
+
console.print(f"[blue]🚀 Launching EC2 Instances[/blue]")
|
541
|
+
console.print(
|
542
|
+
f"[dim]AMI: {image_id} | Type: {instance_type} | Count: {count} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
543
|
+
)
|
348
544
|
|
349
|
-
|
350
|
-
|
545
|
+
# Parse tags
|
546
|
+
tag_dict = {}
|
547
|
+
for tag in tags:
|
548
|
+
if "=" in tag:
|
549
|
+
key, value = tag.split("=", 1)
|
550
|
+
tag_dict[key] = value
|
551
|
+
|
552
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
553
|
+
|
554
|
+
account = AWSAccount(account_id="current", account_name="current")
|
555
|
+
context = OperationContext(
|
556
|
+
account=account,
|
557
|
+
region=ctx.obj["region"],
|
558
|
+
operation_type="run_instances",
|
559
|
+
resource_types=["ec2:instance"],
|
560
|
+
dry_run=ctx.obj["dry_run"],
|
561
|
+
)
|
351
562
|
|
352
|
-
|
353
|
-
|
563
|
+
results = ec2_ops.run_instances(
|
564
|
+
context,
|
565
|
+
image_id=image_id,
|
566
|
+
instance_type=instance_type,
|
567
|
+
min_count=count,
|
568
|
+
max_count=count,
|
569
|
+
key_name=key_name,
|
570
|
+
security_group_ids=list(security_group_ids) if security_group_ids else None,
|
571
|
+
subnet_id=subnet_id,
|
572
|
+
user_data=user_data,
|
573
|
+
instance_profile_name=instance_profile,
|
574
|
+
tags=tag_dict if tag_dict else None,
|
575
|
+
)
|
354
576
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
577
|
+
for result in results:
|
578
|
+
if result.success:
|
579
|
+
console.print(f"[green]✅ Successfully launched {count} instances[/green]")
|
580
|
+
if result.response_data and "Instances" in result.response_data:
|
581
|
+
instance_ids = [inst["InstanceId"] for inst in result.response_data["Instances"]]
|
582
|
+
console.print(f"[green] 📋 Instance IDs: {', '.join(instance_ids)}[/green]")
|
583
|
+
else:
|
584
|
+
console.print(f"[red]❌ Failed to launch instances: {result.error_message}[/red]")
|
360
585
|
|
361
|
-
except ImportError:
|
362
|
-
console.print(f"[red]Web server functionality requires additional dependencies[/red]")
|
363
586
|
except Exception as e:
|
364
|
-
|
365
|
-
|
587
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
588
|
+
raise click.ClickException(str(e))
|
366
589
|
|
367
590
|
|
368
|
-
|
369
|
-
|
370
|
-
|
591
|
+
@ec2.command()
|
592
|
+
@click.option("--source-image-id", required=True, help="Source AMI ID to copy")
|
593
|
+
@click.option("--source-region", required=True, help="Source region")
|
594
|
+
@click.option("--name", required=True, help="Name for the new AMI")
|
595
|
+
@click.option("--description", help="Description for the new AMI")
|
596
|
+
@click.option("--encrypt/--no-encrypt", default=True, help="Enable encryption (default: enabled)")
|
597
|
+
@click.option("--kms-key-id", help="KMS key ID for encryption")
|
598
|
+
@click.pass_context
|
599
|
+
def copy_image(ctx, source_image_id, source_region, name, description, encrypt, kms_key_id):
|
600
|
+
"""Copy AMI across regions with encryption."""
|
601
|
+
try:
|
602
|
+
from runbooks.inventory.models.account import AWSAccount
|
603
|
+
from runbooks.operate import EC2Operations
|
604
|
+
from runbooks.operate.base import OperationContext
|
371
605
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
console.print(f"\n[bold blue]📊 Cloud Foundations Assessment Results[/bold blue]")
|
377
|
-
console.print(f"[dim]Account: {report.account_id} | Region: {report.region} | Profile: {report.profile}[/dim]")
|
378
|
-
|
379
|
-
# Summary metrics table
|
380
|
-
summary_table = Table(title="Assessment Summary", show_header=True, header_style="bold magenta")
|
381
|
-
summary_table.add_column("Metric", style="cyan", width=20)
|
382
|
-
summary_table.add_column("Value", style="bold", width=15)
|
383
|
-
summary_table.add_column("Status", width=15)
|
384
|
-
|
385
|
-
# Add summary rows with enhanced formatting
|
386
|
-
summary_table.add_row(
|
387
|
-
"Compliance Score",
|
388
|
-
f"{report.summary.compliance_score}/100",
|
389
|
-
f"[{'green' if report.summary.compliance_score >= 80 else 'yellow' if report.summary.compliance_score >= 60 else 'red'}]{report.summary.risk_level}[/]",
|
390
|
-
)
|
391
|
-
summary_table.add_row("Total Checks", str(report.summary.total_checks), "✓ Completed")
|
392
|
-
summary_table.add_row(
|
393
|
-
"Pass Rate",
|
394
|
-
f"{report.summary.pass_rate:.1f}%",
|
395
|
-
f"[{'green' if report.summary.pass_rate >= 80 else 'yellow' if report.summary.pass_rate >= 60 else 'red'}]{'Good' if report.summary.pass_rate >= 80 else 'Fair' if report.summary.pass_rate >= 60 else 'Poor'}[/]",
|
396
|
-
)
|
397
|
-
summary_table.add_row(
|
398
|
-
"Critical Issues",
|
399
|
-
str(report.summary.critical_issues),
|
400
|
-
f"[{'red' if report.summary.critical_issues > 0 else 'green'}]{'Action Required' if report.summary.critical_issues > 0 else 'None'}[/]",
|
401
|
-
)
|
402
|
-
summary_table.add_row(
|
403
|
-
"Execution Time",
|
404
|
-
f"{report.summary.total_execution_time:.1f}s",
|
405
|
-
f"[dim]{report.summary.avg_execution_time:.2f}s avg[/dim]",
|
406
|
-
)
|
606
|
+
console.print(f"[blue]📋 Copying AMI Across Regions[/blue]")
|
607
|
+
console.print(
|
608
|
+
f"[dim]Source: {source_image_id} ({source_region}) → {ctx.obj['region']} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
609
|
+
)
|
407
610
|
|
408
|
-
|
409
|
-
|
410
|
-
# Category breakdown
|
411
|
-
if report.results:
|
412
|
-
console.print(f"\n[bold]📋 Results by Category[/bold]")
|
413
|
-
category_summary = report.get_category_summary()
|
414
|
-
|
415
|
-
category_table = Table(show_header=True, header_style="bold magenta")
|
416
|
-
category_table.add_column("Category", style="cyan")
|
417
|
-
category_table.add_column("Total", justify="center")
|
418
|
-
category_table.add_column("Passed", justify="center", style="green")
|
419
|
-
category_table.add_column("Failed", justify="center", style="red")
|
420
|
-
category_table.add_column("Critical", justify="center", style="bold red")
|
421
|
-
category_table.add_column("Pass Rate", justify="center")
|
422
|
-
|
423
|
-
for category, stats in category_summary.items():
|
424
|
-
pass_rate = (stats["passed"] / stats["total"] * 100) if stats["total"] > 0 else 0
|
425
|
-
pass_rate_color = "green" if pass_rate >= 80 else "yellow" if pass_rate >= 60 else "red"
|
426
|
-
|
427
|
-
category_table.add_row(
|
428
|
-
category.upper(),
|
429
|
-
str(stats["total"]),
|
430
|
-
str(stats["passed"]),
|
431
|
-
str(stats["failed"]),
|
432
|
-
str(stats["critical"]),
|
433
|
-
f"[{pass_rate_color}]{pass_rate:.1f}%[/{pass_rate_color}]",
|
434
|
-
)
|
611
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
435
612
|
|
436
|
-
|
613
|
+
account = AWSAccount(account_id="current", account_name="current")
|
614
|
+
context = OperationContext(
|
615
|
+
account=account,
|
616
|
+
region=ctx.obj["region"],
|
617
|
+
operation_type="copy_image",
|
618
|
+
resource_types=["ec2:ami"],
|
619
|
+
dry_run=ctx.obj["dry_run"],
|
620
|
+
)
|
437
621
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
622
|
+
results = ec2_ops.copy_image(
|
623
|
+
context,
|
624
|
+
source_image_id=source_image_id,
|
625
|
+
source_region=source_region,
|
626
|
+
name=name,
|
627
|
+
description=description,
|
628
|
+
encrypted=encrypt,
|
629
|
+
kms_key_id=kms_key_id,
|
630
|
+
)
|
442
631
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
632
|
+
for result in results:
|
633
|
+
if result.success:
|
634
|
+
console.print(f"[green]✅ AMI copy initiated successfully[/green]")
|
635
|
+
if result.response_data and "ImageId" in result.response_data:
|
636
|
+
new_ami_id = result.response_data["ImageId"]
|
637
|
+
console.print(f"[green] 📋 New AMI ID: {new_ami_id}[/green]")
|
638
|
+
console.print(f"[yellow] ⏳ Copy in progress - check console for completion[/yellow]")
|
639
|
+
else:
|
640
|
+
console.print(f"[red]❌ Failed to copy AMI: {result.error_message}[/red]")
|
448
641
|
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
)
|
642
|
+
except Exception as e:
|
643
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
644
|
+
raise click.ClickException(str(e))
|
453
645
|
|
454
|
-
failed_table.add_row(
|
455
|
-
result.finding_id,
|
456
|
-
result.check_name,
|
457
|
-
f"[{severity_color}]{result.severity.value}[/{severity_color}]",
|
458
|
-
result.message[:47] + "..." if len(result.message) > 50 else result.message,
|
459
|
-
)
|
460
646
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
if report.summary.failed_checks > 0:
|
483
|
-
console.print(f"[yellow]• Review and remediate {report.summary.failed_checks} failed checks[/yellow]")
|
484
|
-
if report.summary.pass_rate < 80:
|
485
|
-
console.print(
|
486
|
-
f"[yellow]• Improve overall compliance score (currently {report.summary.compliance_score}/100)[/yellow]"
|
647
|
+
@ec2.command()
|
648
|
+
@click.pass_context
|
649
|
+
def cleanup_unused_volumes(ctx):
|
650
|
+
"""Identify and report unused EBS volumes."""
|
651
|
+
try:
|
652
|
+
from runbooks.inventory.models.account import AWSAccount
|
653
|
+
from runbooks.operate import EC2Operations
|
654
|
+
from runbooks.operate.base import OperationContext
|
655
|
+
|
656
|
+
console.print(f"[blue]🧹 Scanning for Unused EBS Volumes[/blue]")
|
657
|
+
console.print(f"[dim]Region: {ctx.obj['region']} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
658
|
+
|
659
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
660
|
+
|
661
|
+
account = AWSAccount(account_id="current", account_name="current")
|
662
|
+
context = OperationContext(
|
663
|
+
account=account,
|
664
|
+
region=ctx.obj["region"],
|
665
|
+
operation_type="cleanup_unused_volumes",
|
666
|
+
resource_types=["ec2:volume"],
|
667
|
+
dry_run=ctx.obj["dry_run"],
|
487
668
|
)
|
488
|
-
else:
|
489
|
-
console.print(f"[green]• Maintain current security posture and continue monitoring[/green]")
|
490
669
|
|
670
|
+
results = ec2_ops.cleanup_unused_volumes(context)
|
491
671
|
|
492
|
-
|
493
|
-
|
494
|
-
|
672
|
+
for result in results:
|
673
|
+
if result.success:
|
674
|
+
data = result.response_data
|
675
|
+
count = data.get("count", 0)
|
676
|
+
console.print(f"[green]✅ Scan completed[/green]")
|
677
|
+
console.print(f"[yellow]📊 Found {count} unused volumes[/yellow]")
|
678
|
+
|
679
|
+
if count > 0 and "unused_volumes" in data:
|
680
|
+
console.print(
|
681
|
+
f"[dim]Volume IDs: {', '.join(data['unused_volumes'][:5])}{'...' if count > 5 else ''}[/dim]"
|
682
|
+
)
|
683
|
+
console.print(f"[blue]💡 Use AWS Console or additional tools to review and delete[/blue]")
|
684
|
+
else:
|
685
|
+
console.print(f"[red]❌ Scan failed: {result.error_message}[/red]")
|
686
|
+
|
687
|
+
except Exception as e:
|
688
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
689
|
+
raise click.ClickException(str(e))
|
495
690
|
|
496
691
|
|
497
|
-
@
|
692
|
+
@ec2.command()
|
498
693
|
@click.pass_context
|
499
|
-
def
|
500
|
-
"""
|
501
|
-
|
694
|
+
def cleanup_unused_eips(ctx):
|
695
|
+
"""Identify and report unused Elastic IPs."""
|
696
|
+
try:
|
697
|
+
from runbooks.inventory.models.account import AWSAccount
|
698
|
+
from runbooks.operate import EC2Operations
|
699
|
+
from runbooks.operate.base import OperationContext
|
700
|
+
|
701
|
+
console.print(f"[blue]🧹 Scanning for Unused Elastic IPs[/blue]")
|
702
|
+
console.print(f"[dim]Region: {ctx.obj['region']} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
703
|
+
|
704
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
705
|
+
|
706
|
+
account = AWSAccount(account_id="current", account_name="current")
|
707
|
+
context = OperationContext(
|
708
|
+
account=account,
|
709
|
+
region=ctx.obj["region"],
|
710
|
+
operation_type="cleanup_unused_eips",
|
711
|
+
resource_types=["ec2:eip"],
|
712
|
+
dry_run=ctx.obj["dry_run"],
|
713
|
+
)
|
502
714
|
|
715
|
+
results = ec2_ops.cleanup_unused_eips(context)
|
716
|
+
|
717
|
+
for result in results:
|
718
|
+
if result.success:
|
719
|
+
data = result.response_data
|
720
|
+
count = data.get("count", 0)
|
721
|
+
console.print(f"[green]✅ Scan completed[/green]")
|
722
|
+
console.print(f"[yellow]📊 Found {count} unused Elastic IPs[/yellow]")
|
723
|
+
|
724
|
+
if count > 0 and "unused_eips" in data:
|
725
|
+
console.print(
|
726
|
+
f"[dim]Allocation IDs: {', '.join(data['unused_eips'][:3])}{'...' if count > 3 else ''}[/dim]"
|
727
|
+
)
|
728
|
+
console.print(f"[blue]💡 Use AWS Console to review and release unused EIPs[/blue]")
|
729
|
+
console.print(f"[red]⚠️ Unused EIPs incur charges even when not attached[/red]")
|
730
|
+
else:
|
731
|
+
console.print(f"[red]❌ Scan failed: {result.error_message}[/red]")
|
503
732
|
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
@
|
510
|
-
@click.option("--output-file", type=click.Path(), help="Output file path")
|
511
|
-
@click.option("--include-costs", is_flag=True, help="Include cost information")
|
512
|
-
@click.option("--parallel", is_flag=True, default=True, help="Run in parallel")
|
733
|
+
except Exception as e:
|
734
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
735
|
+
raise click.ClickException(str(e))
|
736
|
+
|
737
|
+
|
738
|
+
@operate.group()
|
513
739
|
@click.pass_context
|
514
|
-
def
|
515
|
-
"""
|
516
|
-
|
740
|
+
def s3(ctx):
|
741
|
+
"""S3 bucket and object operations."""
|
742
|
+
pass
|
517
743
|
|
518
|
-
Examples:
|
519
|
-
runbooks inventory collect --all-resources --output excel
|
520
|
-
runbooks inventory collect -r ec2 -r rds --accounts 123456789012
|
521
|
-
runbooks inventory collect --all-accounts --include-costs
|
522
|
-
"""
|
523
|
-
logger.info("Starting resource inventory collection")
|
524
744
|
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
745
|
+
@s3.command()
|
746
|
+
@click.option("--bucket-name", required=True, help="S3 bucket name")
|
747
|
+
@click.option("--encryption/--no-encryption", default=True, help="Enable encryption")
|
748
|
+
@click.option("--versioning/--no-versioning", default=False, help="Enable versioning")
|
749
|
+
@click.option("--public-access-block/--no-public-access-block", default=True, help="Block public access")
|
750
|
+
@click.pass_context
|
751
|
+
def create_bucket(ctx, bucket_name, encryption, versioning, public_access_block):
|
752
|
+
"""Create S3 bucket with security best practices."""
|
753
|
+
try:
|
754
|
+
from runbooks.inventory.models.account import AWSAccount
|
755
|
+
from runbooks.operate import S3Operations
|
756
|
+
from runbooks.operate.base import OperationContext
|
757
|
+
|
758
|
+
console.print(f"[blue]🪣 Creating S3 Bucket[/blue]")
|
759
|
+
console.print(f"[dim]Name: {bucket_name} | Encryption: {encryption} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
760
|
+
|
761
|
+
s3_ops = S3Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
762
|
+
|
763
|
+
account = AWSAccount(account_id="current", account_name="current")
|
764
|
+
context = OperationContext(
|
765
|
+
account=account,
|
766
|
+
region=ctx.obj["region"],
|
767
|
+
operation_type="create_bucket",
|
768
|
+
resource_types=["s3:bucket"],
|
769
|
+
dry_run=ctx.obj["dry_run"],
|
770
|
+
)
|
529
771
|
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
772
|
+
results = s3_ops.create_bucket(
|
773
|
+
context,
|
774
|
+
bucket_name=bucket_name,
|
775
|
+
region=ctx.obj["region"],
|
776
|
+
encryption=encryption,
|
777
|
+
versioning=versioning,
|
778
|
+
public_access_block=public_access_block,
|
779
|
+
)
|
537
780
|
|
538
|
-
|
539
|
-
if
|
540
|
-
|
541
|
-
|
542
|
-
|
781
|
+
for result in results:
|
782
|
+
if result.success:
|
783
|
+
console.print(f"[green]✅ Bucket {bucket_name} created successfully[/green]")
|
784
|
+
if encryption:
|
785
|
+
console.print("[green] 🔒 Encryption enabled[/green]")
|
786
|
+
if versioning:
|
787
|
+
console.print("[green] 📚 Versioning enabled[/green]")
|
788
|
+
if public_access_block:
|
789
|
+
console.print("[green] 🚫 Public access blocked[/green]")
|
543
790
|
else:
|
544
|
-
|
791
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
545
792
|
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
)
|
793
|
+
except Exception as e:
|
794
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
795
|
+
raise click.ClickException(str(e))
|
550
796
|
|
551
|
-
# Generate output
|
552
|
-
if output == "table":
|
553
|
-
display_inventory_results(results)
|
554
|
-
else:
|
555
|
-
from runbooks.inventory.core.formatter import InventoryFormatter
|
556
797
|
|
557
|
-
|
798
|
+
@s3.command()
|
799
|
+
@click.option("--bucket-name", required=True, help="S3 bucket name to delete")
|
800
|
+
@click.option("--force", is_flag=True, help="Skip confirmation and delete all objects")
|
801
|
+
@click.pass_context
|
802
|
+
def delete_bucket_and_objects(ctx, bucket_name, force):
|
803
|
+
"""Delete S3 bucket and all its objects (DESTRUCTIVE)."""
|
804
|
+
try:
|
805
|
+
from runbooks.inventory.models.account import AWSAccount
|
806
|
+
from runbooks.operate import S3Operations
|
807
|
+
from runbooks.operate.base import OperationContext
|
808
|
+
|
809
|
+
console.print(f"[red]🗑️ Deleting S3 Bucket and Objects[/red]")
|
810
|
+
console.print(f"[dim]Bucket: {bucket_name} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
811
|
+
|
812
|
+
if not ctx.obj["dry_run"] and not force and not ctx.obj.get("force", False):
|
813
|
+
console.print("[yellow]⚠️ This will permanently delete the bucket and ALL objects![/yellow]")
|
814
|
+
if not click.confirm(f"Are you sure you want to delete bucket '{bucket_name}' and all its contents?"):
|
815
|
+
console.print("[blue]Operation cancelled[/blue]")
|
816
|
+
return
|
817
|
+
|
818
|
+
s3_ops = S3Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
819
|
+
|
820
|
+
account = AWSAccount(account_id="current", account_name="current")
|
821
|
+
context = OperationContext(
|
822
|
+
account=account,
|
823
|
+
region=ctx.obj["region"],
|
824
|
+
operation_type="delete_bucket_and_objects",
|
825
|
+
resource_types=["s3:bucket"],
|
826
|
+
dry_run=ctx.obj["dry_run"],
|
827
|
+
)
|
558
828
|
|
559
|
-
|
560
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
561
|
-
output_file = f"inventory_{timestamp}.{output}"
|
829
|
+
results = s3_ops.delete_bucket_and_objects(context, bucket_name)
|
562
830
|
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
formatter.to_excel(output_file)
|
831
|
+
for result in results:
|
832
|
+
if result.success:
|
833
|
+
console.print(f"[green]✅ Bucket {bucket_name} and all objects deleted successfully[/green]")
|
834
|
+
else:
|
835
|
+
console.print(f"[red]❌ Failed to delete bucket: {result.error_message}[/red]")
|
569
836
|
|
570
|
-
|
837
|
+
except Exception as e:
|
838
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
839
|
+
raise click.ClickException(str(e))
|
571
840
|
|
572
|
-
except Exception as e:
|
573
|
-
logger.error(f"Inventory collection failed: {e}")
|
574
|
-
console.print(f"[red]✗ Collection failed: {e}[/red]")
|
575
|
-
sys.exit(1)
|
576
841
|
|
842
|
+
@s3.command()
|
843
|
+
@click.option("--account-id", help="AWS account ID (uses current account if not specified)")
|
844
|
+
@click.option("--block-public-acls/--allow-public-acls", default=True, help="Block public ACLs")
|
845
|
+
@click.option("--ignore-public-acls/--honor-public-acls", default=True, help="Ignore public ACLs")
|
846
|
+
@click.option("--block-public-policy/--allow-public-policy", default=True, help="Block public bucket policies")
|
847
|
+
@click.option("--restrict-public-buckets/--allow-public-buckets", default=True, help="Restrict public bucket access")
|
848
|
+
@click.pass_context
|
849
|
+
def set_public_access_block(
|
850
|
+
ctx, account_id, block_public_acls, ignore_public_acls, block_public_policy, restrict_public_buckets
|
851
|
+
):
|
852
|
+
"""Configure account-level S3 public access block settings."""
|
853
|
+
try:
|
854
|
+
from runbooks.inventory.models.account import AWSAccount
|
855
|
+
from runbooks.operate import S3Operations
|
856
|
+
from runbooks.operate.base import OperationContext
|
857
|
+
|
858
|
+
console.print(f"[blue]🔒 Setting S3 Public Access Block[/blue]")
|
859
|
+
console.print(f"[dim]Account: {account_id or 'current'} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
860
|
+
|
861
|
+
s3_ops = S3Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
862
|
+
|
863
|
+
account = AWSAccount(account_id=account_id or "current", account_name="current")
|
864
|
+
context = OperationContext(
|
865
|
+
account=account,
|
866
|
+
region=ctx.obj["region"],
|
867
|
+
operation_type="set_public_access_block",
|
868
|
+
resource_types=["s3:account"],
|
869
|
+
dry_run=ctx.obj["dry_run"],
|
870
|
+
)
|
577
871
|
|
578
|
-
|
579
|
-
|
580
|
-
|
872
|
+
results = s3_ops.set_public_access_block(
|
873
|
+
context,
|
874
|
+
account_id=account_id,
|
875
|
+
block_public_acls=block_public_acls,
|
876
|
+
ignore_public_acls=ignore_public_acls,
|
877
|
+
block_public_policy=block_public_policy,
|
878
|
+
restrict_public_buckets=restrict_public_buckets,
|
879
|
+
)
|
581
880
|
|
881
|
+
for result in results:
|
882
|
+
if result.success:
|
883
|
+
console.print(f"[green]✅ Public access block configured successfully[/green]")
|
884
|
+
console.print(f"[green] 🔒 Block Public ACLs: {block_public_acls}[/green]")
|
885
|
+
console.print(f"[green] 🔒 Ignore Public ACLs: {ignore_public_acls}[/green]")
|
886
|
+
console.print(f"[green] 🔒 Block Public Policy: {block_public_policy}[/green]")
|
887
|
+
console.print(f"[green] 🔒 Restrict Public Buckets: {restrict_public_buckets}[/green]")
|
888
|
+
else:
|
889
|
+
console.print(f"[red]❌ Failed to configure public access block: {result.error_message}[/red]")
|
582
890
|
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
"""AWS Organizations management and automation."""
|
587
|
-
pass
|
891
|
+
except Exception as e:
|
892
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
893
|
+
raise click.ClickException(str(e))
|
588
894
|
|
589
895
|
|
590
|
-
@
|
591
|
-
@click.option(
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
)
|
597
|
-
@click.option("--config-file", type=click.Path(exists=True), help="Custom OU structure configuration file")
|
598
|
-
@click.option("--dry-run", is_flag=True, help="Show what would be created")
|
896
|
+
@s3.command()
|
897
|
+
@click.option("--source-bucket", required=True, help="Source bucket name")
|
898
|
+
@click.option("--destination-bucket", required=True, help="Destination bucket name")
|
899
|
+
@click.option("--source-prefix", help="Source prefix to sync from")
|
900
|
+
@click.option("--destination-prefix", help="Destination prefix to sync to")
|
901
|
+
@click.option("--delete-removed", is_flag=True, help="Delete objects in destination that don't exist in source")
|
902
|
+
@click.option("--exclude-pattern", multiple=True, help="Patterns to exclude from sync (repeat for multiple)")
|
599
903
|
@click.pass_context
|
600
|
-
def
|
601
|
-
"""
|
602
|
-
|
904
|
+
def sync(ctx, source_bucket, destination_bucket, source_prefix, destination_prefix, delete_removed, exclude_pattern):
|
905
|
+
"""Synchronize objects between S3 buckets or prefixes."""
|
906
|
+
try:
|
907
|
+
from runbooks.inventory.models.account import AWSAccount
|
908
|
+
from runbooks.operate import S3Operations
|
909
|
+
from runbooks.operate.base import OperationContext
|
603
910
|
|
604
|
-
|
605
|
-
|
911
|
+
console.print(f"[blue]🔄 Synchronizing S3 Objects[/blue]")
|
912
|
+
console.print(
|
913
|
+
f"[dim]Source: {source_bucket} → Destination: {destination_bucket} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
914
|
+
)
|
606
915
|
|
607
|
-
|
608
|
-
|
609
|
-
runbooks org setup-ous --config-file ou_structure.yaml --dry-run
|
610
|
-
"""
|
611
|
-
logger.info(f"Setting up OU structure with template: {template}")
|
916
|
+
if delete_removed:
|
917
|
+
console.print(f"[yellow]⚠️ Delete removed objects enabled[/yellow]")
|
612
918
|
|
613
|
-
|
614
|
-
manager = OUManager(profile=ctx.obj["profile"], region=ctx.obj["region"])
|
919
|
+
s3_ops = S3Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
615
920
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
921
|
+
account = AWSAccount(account_id="current", account_name="current")
|
922
|
+
context = OperationContext(
|
923
|
+
account=account,
|
924
|
+
region=ctx.obj["region"],
|
925
|
+
operation_type="sync_objects",
|
926
|
+
resource_types=["s3:bucket"],
|
927
|
+
dry_run=ctx.obj["dry_run"],
|
928
|
+
)
|
620
929
|
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
930
|
+
results = s3_ops.sync_objects(
|
931
|
+
context,
|
932
|
+
source_bucket=source_bucket,
|
933
|
+
destination_bucket=destination_bucket,
|
934
|
+
source_prefix=source_prefix,
|
935
|
+
destination_prefix=destination_prefix,
|
936
|
+
delete_removed=delete_removed,
|
937
|
+
exclude_patterns=list(exclude_pattern) if exclude_pattern else None,
|
938
|
+
)
|
939
|
+
|
940
|
+
for result in results:
|
941
|
+
if result.success:
|
942
|
+
data = result.response_data
|
943
|
+
synced = data.get("synced_objects", 0)
|
944
|
+
deleted = data.get("deleted_objects", 0)
|
945
|
+
total = data.get("total_source_objects", 0)
|
946
|
+
console.print(f"[green]✅ S3 sync completed successfully[/green]")
|
947
|
+
console.print(f"[green] 📄 Total source objects: {total}[/green]")
|
948
|
+
console.print(f"[green] 🔄 Objects synced: {synced}[/green]")
|
949
|
+
console.print(f"[green] 🗑️ Objects deleted: {deleted}[/green]")
|
950
|
+
else:
|
951
|
+
console.print(f"[red]❌ Failed to sync objects: {result.error_message}[/red]")
|
628
952
|
|
629
953
|
except Exception as e:
|
630
|
-
|
631
|
-
|
632
|
-
sys.exit(1)
|
954
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
955
|
+
raise click.ClickException(str(e))
|
633
956
|
|
634
957
|
|
635
|
-
|
636
|
-
|
637
|
-
|
958
|
+
@operate.group()
|
959
|
+
@click.pass_context
|
960
|
+
def cloudformation(ctx):
|
961
|
+
"""CloudFormation stack and StackSet operations."""
|
962
|
+
pass
|
638
963
|
|
639
|
-
formatter = InventoryFormatter(results)
|
640
|
-
console_output = formatter.format_console_table()
|
641
|
-
console.print(console_output)
|
642
964
|
|
965
|
+
@cloudformation.command()
|
966
|
+
@click.option("--source-stackset-name", required=True, help="Source StackSet name")
|
967
|
+
@click.option("--target-stackset-name", required=True, help="Target StackSet name")
|
968
|
+
@click.option("--account-ids", multiple=True, required=True, help="Account IDs to move (repeat for multiple)")
|
969
|
+
@click.option("--regions", multiple=True, required=True, help="Regions to move (repeat for multiple)")
|
970
|
+
@click.option("--operation-preferences", help="JSON operation preferences")
|
971
|
+
@click.pass_context
|
972
|
+
def move_stack_instances(ctx, source_stackset_name, target_stackset_name, account_ids, regions, operation_preferences):
|
973
|
+
"""Move CloudFormation stack instances between StackSets."""
|
974
|
+
try:
|
975
|
+
from runbooks.inventory.models.account import AWSAccount
|
976
|
+
from runbooks.operate import CloudFormationOperations
|
977
|
+
from runbooks.operate.base import OperationContext
|
643
978
|
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
table.add_column("Level", style="bold")
|
979
|
+
console.print(f"[blue]📦 Moving CloudFormation Stack Instances[/blue]")
|
980
|
+
console.print(
|
981
|
+
f"[dim]Source: {source_stackset_name} → Target: {target_stackset_name} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
982
|
+
)
|
983
|
+
console.print(f"[dim]Accounts: {len(account_ids)} | Regions: {len(regions)}[/dim]")
|
650
984
|
|
651
|
-
|
652
|
-
|
653
|
-
|
985
|
+
cfn_ops = CloudFormationOperations(
|
986
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
987
|
+
)
|
654
988
|
|
655
|
-
|
656
|
-
|
989
|
+
account = AWSAccount(account_id="current", account_name="current")
|
990
|
+
context = OperationContext(
|
991
|
+
account=account,
|
992
|
+
region=ctx.obj["region"],
|
993
|
+
operation_type="move_stack_instances",
|
994
|
+
resource_types=["cloudformation:stackset"],
|
995
|
+
dry_run=ctx.obj["dry_run"],
|
996
|
+
)
|
657
997
|
|
658
|
-
|
659
|
-
|
998
|
+
# Parse operation preferences if provided
|
999
|
+
preferences = None
|
1000
|
+
if operation_preferences:
|
1001
|
+
import json
|
660
1002
|
|
661
|
-
|
1003
|
+
preferences = json.loads(operation_preferences)
|
662
1004
|
|
1005
|
+
results = cfn_ops.move_stack_instances(
|
1006
|
+
context,
|
1007
|
+
source_stackset_name=source_stackset_name,
|
1008
|
+
target_stackset_name=target_stackset_name,
|
1009
|
+
account_ids=list(account_ids),
|
1010
|
+
regions=list(regions),
|
1011
|
+
operation_preferences=preferences,
|
1012
|
+
)
|
663
1013
|
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
1014
|
+
for result in results:
|
1015
|
+
if result.success:
|
1016
|
+
console.print(f"[green]✅ Stack instance move operation initiated[/green]")
|
1017
|
+
if result.response_data and "OperationId" in result.response_data:
|
1018
|
+
op_id = result.response_data["OperationId"]
|
1019
|
+
console.print(f"[green] 📋 Operation ID: {op_id}[/green]")
|
1020
|
+
console.print(f"[yellow] ⏳ Check AWS Console for progress[/yellow]")
|
1021
|
+
else:
|
1022
|
+
console.print(f"[red]❌ Failed to initiate move: {result.error_message}[/red]")
|
671
1023
|
|
672
|
-
|
673
|
-
|
674
|
-
|
1024
|
+
except Exception as e:
|
1025
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1026
|
+
raise click.ClickException(str(e))
|
675
1027
|
|
676
|
-
for child in ou_result.get("children", []):
|
677
|
-
add_results_to_table(child, level + 1)
|
678
1028
|
|
679
|
-
|
680
|
-
|
1029
|
+
@cloudformation.command()
|
1030
|
+
@click.option("--target-role-name", required=True, help="CloudFormation StackSet execution role name")
|
1031
|
+
@click.option("--management-account-id", required=True, help="Management account ID")
|
1032
|
+
@click.option("--trusted-principals", multiple=True, help="Additional trusted principals (repeat for multiple)")
|
1033
|
+
@click.pass_context
|
1034
|
+
def lockdown_stackset_role(ctx, target_role_name, management_account_id, trusted_principals):
|
1035
|
+
"""Lockdown CloudFormation StackSet execution role to management account."""
|
1036
|
+
try:
|
1037
|
+
from runbooks.inventory.models.account import AWSAccount
|
1038
|
+
from runbooks.operate import CloudFormationOperations
|
1039
|
+
from runbooks.operate.base import OperationContext
|
681
1040
|
|
682
|
-
|
1041
|
+
console.print(f"[blue]🔒 Locking Down StackSet Role[/blue]")
|
1042
|
+
console.print(
|
1043
|
+
f"[dim]Role: {target_role_name} | Management Account: {management_account_id} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
1044
|
+
)
|
683
1045
|
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
console.print(f" [red]✗ {error}[/red]")
|
1046
|
+
cfn_ops = CloudFormationOperations(
|
1047
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
1048
|
+
)
|
688
1049
|
|
1050
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1051
|
+
context = OperationContext(
|
1052
|
+
account=account,
|
1053
|
+
region=ctx.obj["region"],
|
1054
|
+
operation_type="lockdown_stackset_role",
|
1055
|
+
resource_types=["iam:role"],
|
1056
|
+
dry_run=ctx.obj["dry_run"],
|
1057
|
+
)
|
689
1058
|
|
690
|
-
|
691
|
-
|
692
|
-
|
1059
|
+
results = cfn_ops.lockdown_stackset_role(
|
1060
|
+
context,
|
1061
|
+
target_role_name=target_role_name,
|
1062
|
+
management_account_id=management_account_id,
|
1063
|
+
trusted_principals=list(trusted_principals) if trusted_principals else None,
|
1064
|
+
)
|
693
1065
|
|
1066
|
+
for result in results:
|
1067
|
+
if result.success:
|
1068
|
+
console.print(f"[green]✅ StackSet role locked down successfully[/green]")
|
1069
|
+
console.print(f"[green] 🔒 Role: {target_role_name}[/green]")
|
1070
|
+
console.print(f"[green] 🏢 Trusted Account: {management_account_id}[/green]")
|
1071
|
+
if trusted_principals:
|
1072
|
+
console.print(f"[green] 👥 Additional Principals: {len(trusted_principals)}[/green]")
|
1073
|
+
else:
|
1074
|
+
console.print(f"[red]❌ Failed to lockdown role: {result.error_message}[/red]")
|
694
1075
|
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
)
|
702
|
-
@click.option(
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
)
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
)
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
1076
|
+
except Exception as e:
|
1077
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1078
|
+
raise click.ClickException(str(e))
|
1079
|
+
|
1080
|
+
|
1081
|
+
@cloudformation.command()
|
1082
|
+
@click.option("--stackset-name", required=True, help="StackSet name to update")
|
1083
|
+
@click.option("--template-body", help="CloudFormation template body")
|
1084
|
+
@click.option("--template-url", help="CloudFormation template URL")
|
1085
|
+
@click.option("--parameters", multiple=True, help="Parameters in Key=Value format (repeat for multiple)")
|
1086
|
+
@click.option("--capabilities", multiple=True, help="Required capabilities (CAPABILITY_IAM, CAPABILITY_NAMED_IAM)")
|
1087
|
+
@click.option("--description", help="Update description")
|
1088
|
+
@click.option("--operation-preferences", help="JSON operation preferences")
|
1089
|
+
@click.pass_context
|
1090
|
+
def update_stacksets(
|
1091
|
+
ctx, stackset_name, template_body, template_url, parameters, capabilities, description, operation_preferences
|
1092
|
+
):
|
1093
|
+
"""Update CloudFormation StackSet with new template or parameters."""
|
1094
|
+
try:
|
1095
|
+
from runbooks.inventory.models.account import AWSAccount
|
1096
|
+
from runbooks.operate import CloudFormationOperations
|
1097
|
+
from runbooks.operate.base import OperationContext
|
1098
|
+
|
1099
|
+
console.print(f"[blue]🔄 Updating CloudFormation StackSet[/blue]")
|
1100
|
+
console.print(f"[dim]StackSet: {stackset_name} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
1101
|
+
|
1102
|
+
if not template_body and not template_url:
|
1103
|
+
console.print("[red]❌ Either --template-body or --template-url must be specified[/red]")
|
1104
|
+
return
|
1105
|
+
|
1106
|
+
cfn_ops = CloudFormationOperations(
|
1107
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
1108
|
+
)
|
1109
|
+
|
1110
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1111
|
+
context = OperationContext(
|
1112
|
+
account=account,
|
1113
|
+
region=ctx.obj["region"],
|
1114
|
+
operation_type="update_stacksets",
|
1115
|
+
resource_types=["cloudformation:stackset"],
|
1116
|
+
dry_run=ctx.obj["dry_run"],
|
1117
|
+
)
|
1118
|
+
|
1119
|
+
# Parse parameters
|
1120
|
+
param_list = []
|
1121
|
+
for param in parameters:
|
1122
|
+
if "=" in param:
|
1123
|
+
key, value = param.split("=", 1)
|
1124
|
+
param_list.append({"ParameterKey": key, "ParameterValue": value})
|
1125
|
+
|
1126
|
+
# Parse operation preferences
|
1127
|
+
preferences = None
|
1128
|
+
if operation_preferences:
|
1129
|
+
import json
|
1130
|
+
|
1131
|
+
preferences = json.loads(operation_preferences)
|
1132
|
+
|
1133
|
+
results = cfn_ops.update_stacksets(
|
1134
|
+
context,
|
1135
|
+
stackset_name=stackset_name,
|
1136
|
+
template_body=template_body,
|
1137
|
+
template_url=template_url,
|
1138
|
+
parameters=param_list if param_list else None,
|
1139
|
+
capabilities=list(capabilities) if capabilities else None,
|
1140
|
+
description=description,
|
1141
|
+
operation_preferences=preferences,
|
1142
|
+
)
|
1143
|
+
|
1144
|
+
for result in results:
|
1145
|
+
if result.success:
|
1146
|
+
console.print(f"[green]✅ StackSet update operation initiated[/green]")
|
1147
|
+
if result.response_data and "OperationId" in result.response_data:
|
1148
|
+
op_id = result.response_data["OperationId"]
|
1149
|
+
console.print(f"[green] 📋 Operation ID: {op_id}[/green]")
|
1150
|
+
console.print(f"[yellow] ⏳ Check AWS Console for progress[/yellow]")
|
1151
|
+
else:
|
1152
|
+
console.print(f"[red]❌ Failed to initiate update: {result.error_message}[/red]")
|
1153
|
+
|
1154
|
+
except Exception as e:
|
1155
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1156
|
+
raise click.ClickException(str(e))
|
1157
|
+
|
1158
|
+
|
1159
|
+
@operate.group()
|
1160
|
+
@click.pass_context
|
1161
|
+
def iam(ctx):
|
1162
|
+
"""IAM role and policy operations."""
|
1163
|
+
pass
|
1164
|
+
|
1165
|
+
|
1166
|
+
@iam.command()
|
1167
|
+
@click.option("--role-name", required=True, help="IAM role name to update")
|
1168
|
+
@click.option("--trusted-account-ids", multiple=True, required=True, help="Trusted account IDs (repeat for multiple)")
|
1169
|
+
@click.option("--external-id", help="External ID for additional security")
|
1170
|
+
@click.option("--require-mfa", is_flag=True, help="Require MFA for role assumption")
|
1171
|
+
@click.option("--session-duration", type=int, help="Maximum session duration in seconds")
|
1172
|
+
@click.pass_context
|
1173
|
+
def update_roles_cross_accounts(ctx, role_name, trusted_account_ids, external_id, require_mfa, session_duration):
|
1174
|
+
"""Update IAM role trust policy for cross-account access."""
|
1175
|
+
try:
|
1176
|
+
from runbooks.inventory.models.account import AWSAccount
|
1177
|
+
from runbooks.operate import IAMOperations
|
1178
|
+
from runbooks.operate.base import OperationContext
|
1179
|
+
|
1180
|
+
console.print(f"[blue]👥 Updating IAM Role Trust Policy[/blue]")
|
1181
|
+
console.print(
|
1182
|
+
f"[dim]Role: {role_name} | Trusted Accounts: {len(trusted_account_ids)} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
1183
|
+
)
|
1184
|
+
|
1185
|
+
iam_ops = IAMOperations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
1186
|
+
|
1187
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1188
|
+
context = OperationContext(
|
1189
|
+
account=account,
|
1190
|
+
region=ctx.obj["region"],
|
1191
|
+
operation_type="update_roles_cross_accounts",
|
1192
|
+
resource_types=["iam:role"],
|
1193
|
+
dry_run=ctx.obj["dry_run"],
|
1194
|
+
)
|
1195
|
+
|
1196
|
+
results = iam_ops.update_roles_cross_accounts(
|
1197
|
+
context,
|
1198
|
+
role_name=role_name,
|
1199
|
+
trusted_account_ids=list(trusted_account_ids),
|
1200
|
+
external_id=external_id,
|
1201
|
+
require_mfa=require_mfa,
|
1202
|
+
session_duration=session_duration,
|
1203
|
+
)
|
1204
|
+
|
1205
|
+
for result in results:
|
1206
|
+
if result.success:
|
1207
|
+
console.print(f"[green]✅ IAM role trust policy updated successfully[/green]")
|
1208
|
+
console.print(f"[green] 👥 Role: {role_name}[/green]")
|
1209
|
+
console.print(f"[green] 🏢 Trusted Accounts: {', '.join(trusted_account_ids)}[/green]")
|
1210
|
+
if external_id:
|
1211
|
+
console.print(f"[green] 🔑 External ID: {external_id}[/green]")
|
1212
|
+
if require_mfa:
|
1213
|
+
console.print(f"[green] 🛡️ MFA Required: Yes[/green]")
|
1214
|
+
else:
|
1215
|
+
console.print(f"[red]❌ Failed to update role: {result.error_message}[/red]")
|
1216
|
+
|
1217
|
+
except Exception as e:
|
1218
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1219
|
+
raise click.ClickException(str(e))
|
1220
|
+
|
1221
|
+
|
1222
|
+
@operate.group()
|
1223
|
+
@click.pass_context
|
1224
|
+
def cloudwatch(ctx):
|
1225
|
+
"""CloudWatch logs and metrics operations."""
|
1226
|
+
pass
|
1227
|
+
|
1228
|
+
|
1229
|
+
@cloudwatch.command()
|
1230
|
+
@click.option("--retention-days", type=int, required=True, help="Log retention period in days")
|
1231
|
+
@click.option("--log-group-names", multiple=True, help="Specific log group names (repeat for multiple)")
|
1232
|
+
@click.option("--update-all-log-groups", is_flag=True, help="Update all log groups in the region")
|
1233
|
+
@click.option("--log-group-prefix", help="Update log groups with specific prefix")
|
1234
|
+
@click.pass_context
|
1235
|
+
def update_log_retention_policy(ctx, retention_days, log_group_names, update_all_log_groups, log_group_prefix):
|
1236
|
+
"""Update CloudWatch Logs retention policy."""
|
1237
|
+
try:
|
1238
|
+
from runbooks.inventory.models.account import AWSAccount
|
1239
|
+
from runbooks.operate import CloudWatchOperations
|
1240
|
+
from runbooks.operate.base import OperationContext
|
1241
|
+
|
1242
|
+
console.print(f"[blue]📊 Updating CloudWatch Log Retention[/blue]")
|
1243
|
+
console.print(
|
1244
|
+
f"[dim]Retention: {retention_days} days | All Groups: {update_all_log_groups} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
1245
|
+
)
|
1246
|
+
|
1247
|
+
if not log_group_names and not update_all_log_groups and not log_group_prefix:
|
1248
|
+
console.print(
|
1249
|
+
"[red]❌ Must specify log groups, use --update-all-log-groups, or provide --log-group-prefix[/red]"
|
1250
|
+
)
|
1251
|
+
return
|
1252
|
+
|
1253
|
+
cw_ops = CloudWatchOperations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
1254
|
+
|
1255
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1256
|
+
context = OperationContext(
|
1257
|
+
account=account,
|
1258
|
+
region=ctx.obj["region"],
|
1259
|
+
operation_type="update_log_retention_policy",
|
1260
|
+
resource_types=["logs:log-group"],
|
1261
|
+
dry_run=ctx.obj["dry_run"],
|
1262
|
+
)
|
1263
|
+
|
1264
|
+
results = cw_ops.update_log_retention_policy(
|
1265
|
+
context,
|
1266
|
+
retention_days=retention_days,
|
1267
|
+
log_group_names=list(log_group_names) if log_group_names else None,
|
1268
|
+
update_all_log_groups=update_all_log_groups,
|
1269
|
+
log_group_prefix=log_group_prefix,
|
1270
|
+
)
|
1271
|
+
|
1272
|
+
for result in results:
|
1273
|
+
if result.success:
|
1274
|
+
data = result.response_data
|
1275
|
+
updated_count = data.get("updated_log_groups", 0)
|
1276
|
+
console.print(f"[green]✅ Log retention policy updated[/green]")
|
1277
|
+
console.print(f"[green] 📊 Updated {updated_count} log groups[/green]")
|
1278
|
+
console.print(f"[green] ⏰ Retention: {retention_days} days[/green]")
|
1279
|
+
else:
|
1280
|
+
console.print(f"[red]❌ Failed to update retention: {result.error_message}[/red]")
|
1281
|
+
|
1282
|
+
except Exception as e:
|
1283
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1284
|
+
raise click.ClickException(str(e))
|
1285
|
+
|
1286
|
+
|
1287
|
+
# ==============================================================================
|
1288
|
+
# DynamoDB Commands
|
1289
|
+
# ==============================================================================
|
1290
|
+
|
1291
|
+
|
1292
|
+
@operate.group()
|
1293
|
+
@click.pass_context
|
1294
|
+
def dynamodb(ctx):
|
1295
|
+
"""DynamoDB table and data operations."""
|
1296
|
+
pass
|
1297
|
+
|
1298
|
+
|
1299
|
+
@dynamodb.command()
|
1300
|
+
@click.option("--table-name", required=True, help="Name of the DynamoDB table to create")
|
1301
|
+
@click.option("--hash-key", required=True, help="Hash key attribute name")
|
1302
|
+
@click.option("--hash-key-type", default="S", type=click.Choice(["S", "N", "B"]), help="Hash key attribute type")
|
1303
|
+
@click.option("--range-key", help="Range key attribute name (optional)")
|
1304
|
+
@click.option("--range-key-type", default="S", type=click.Choice(["S", "N", "B"]), help="Range key attribute type")
|
745
1305
|
@click.option(
|
746
|
-
"--
|
747
|
-
"
|
748
|
-
|
749
|
-
|
1306
|
+
"--billing-mode",
|
1307
|
+
default="PAY_PER_REQUEST",
|
1308
|
+
type=click.Choice(["PAY_PER_REQUEST", "PROVISIONED"]),
|
1309
|
+
help="Billing mode",
|
750
1310
|
)
|
1311
|
+
@click.option("--read-capacity", type=int, help="Read capacity units (required for PROVISIONED mode)")
|
1312
|
+
@click.option("--write-capacity", type=int, help="Write capacity units (required for PROVISIONED mode)")
|
1313
|
+
@click.option("--tags", multiple=True, help="Tags in format key=value (repeat for multiple)")
|
1314
|
+
@click.pass_context
|
1315
|
+
def create_table(
|
1316
|
+
ctx,
|
1317
|
+
table_name,
|
1318
|
+
hash_key,
|
1319
|
+
hash_key_type,
|
1320
|
+
range_key,
|
1321
|
+
range_key_type,
|
1322
|
+
billing_mode,
|
1323
|
+
read_capacity,
|
1324
|
+
write_capacity,
|
1325
|
+
tags,
|
1326
|
+
):
|
1327
|
+
"""Create a new DynamoDB table."""
|
1328
|
+
try:
|
1329
|
+
from runbooks.inventory.models.account import AWSAccount
|
1330
|
+
from runbooks.operate import DynamoDBOperations
|
1331
|
+
from runbooks.operate.base import OperationContext
|
1332
|
+
|
1333
|
+
console.print(f"[blue]🗃️ Creating DynamoDB Table[/blue]")
|
1334
|
+
console.print(f"[dim]Table: {table_name} | Billing: {billing_mode} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
1335
|
+
|
1336
|
+
# Validate provisioned mode parameters
|
1337
|
+
if billing_mode == "PROVISIONED" and (not read_capacity or not write_capacity):
|
1338
|
+
console.print("[red]❌ Read and write capacity units are required for PROVISIONED billing mode[/red]")
|
1339
|
+
return
|
1340
|
+
|
1341
|
+
# Build key schema
|
1342
|
+
key_schema = [{"AttributeName": hash_key, "KeyType": "HASH"}]
|
1343
|
+
attribute_definitions = [{"AttributeName": hash_key, "AttributeType": hash_key_type}]
|
1344
|
+
|
1345
|
+
if range_key:
|
1346
|
+
key_schema.append({"AttributeName": range_key, "KeyType": "RANGE"})
|
1347
|
+
attribute_definitions.append({"AttributeName": range_key, "AttributeType": range_key_type})
|
1348
|
+
|
1349
|
+
# Build provisioned throughput if needed
|
1350
|
+
provisioned_throughput = None
|
1351
|
+
if billing_mode == "PROVISIONED":
|
1352
|
+
provisioned_throughput = {"ReadCapacityUnits": read_capacity, "WriteCapacityUnits": write_capacity}
|
1353
|
+
|
1354
|
+
# Parse tags
|
1355
|
+
parsed_tags = []
|
1356
|
+
for tag in tags:
|
1357
|
+
if "=" in tag:
|
1358
|
+
key, value = tag.split("=", 1)
|
1359
|
+
parsed_tags.append({"Key": key.strip(), "Value": value.strip()})
|
1360
|
+
|
1361
|
+
dynamodb_ops = DynamoDBOperations(
|
1362
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
1363
|
+
)
|
1364
|
+
|
1365
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1366
|
+
context = OperationContext(
|
1367
|
+
account=account,
|
1368
|
+
region=ctx.obj["region"],
|
1369
|
+
operation_type="create_table",
|
1370
|
+
resource_types=["dynamodb:table"],
|
1371
|
+
dry_run=ctx.obj["dry_run"],
|
1372
|
+
)
|
1373
|
+
|
1374
|
+
results = dynamodb_ops.create_table(
|
1375
|
+
context,
|
1376
|
+
table_name=table_name,
|
1377
|
+
key_schema=key_schema,
|
1378
|
+
attribute_definitions=attribute_definitions,
|
1379
|
+
billing_mode=billing_mode,
|
1380
|
+
provisioned_throughput=provisioned_throughput,
|
1381
|
+
tags=parsed_tags if parsed_tags else None,
|
1382
|
+
)
|
1383
|
+
|
1384
|
+
for result in results:
|
1385
|
+
if result.success:
|
1386
|
+
data = result.response_data
|
1387
|
+
table_arn = data.get("TableDescription", {}).get("TableArn", "")
|
1388
|
+
console.print(f"[green]✅ DynamoDB table created successfully[/green]")
|
1389
|
+
console.print(f"[green] 📊 Table: {table_name}[/green]")
|
1390
|
+
console.print(f"[green] 🔗 ARN: {table_arn}[/green]")
|
1391
|
+
console.print(f"[green] 💰 Billing: {billing_mode}[/green]")
|
1392
|
+
else:
|
1393
|
+
console.print(f"[red]❌ Failed to create table: {result.error_message}[/red]")
|
1394
|
+
|
1395
|
+
except Exception as e:
|
1396
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1397
|
+
raise click.ClickException(str(e))
|
1398
|
+
|
1399
|
+
|
1400
|
+
@dynamodb.command()
|
1401
|
+
@click.option("--table-name", required=True, help="Name of the DynamoDB table to delete")
|
1402
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
1403
|
+
@click.pass_context
|
1404
|
+
def delete_table(ctx, table_name, confirm):
|
1405
|
+
"""
|
1406
|
+
Delete a DynamoDB table.
|
1407
|
+
|
1408
|
+
⚠️ WARNING: This operation is destructive and irreversible!
|
1409
|
+
"""
|
1410
|
+
try:
|
1411
|
+
from runbooks.inventory.models.account import AWSAccount
|
1412
|
+
from runbooks.operate import DynamoDBOperations
|
1413
|
+
from runbooks.operate.base import OperationContext
|
1414
|
+
|
1415
|
+
console.print(f"[blue]🗃️ Deleting DynamoDB Table[/blue]")
|
1416
|
+
console.print(f"[dim]Table: {table_name} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
1417
|
+
|
1418
|
+
if not confirm and not ctx.obj.get("force", False):
|
1419
|
+
console.print(f"[yellow]⚠️ WARNING: You are about to DELETE table '{table_name}'[/yellow]")
|
1420
|
+
console.print("[yellow]This operation is DESTRUCTIVE and IRREVERSIBLE![/yellow]")
|
1421
|
+
if not click.confirm("Do you want to continue?"):
|
1422
|
+
console.print("[yellow]❌ Operation cancelled by user[/yellow]")
|
1423
|
+
return
|
1424
|
+
|
1425
|
+
dynamodb_ops = DynamoDBOperations(
|
1426
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
1427
|
+
)
|
1428
|
+
|
1429
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1430
|
+
context = OperationContext(
|
1431
|
+
account=account,
|
1432
|
+
region=ctx.obj["region"],
|
1433
|
+
operation_type="delete_table",
|
1434
|
+
resource_types=["dynamodb:table"],
|
1435
|
+
dry_run=ctx.obj["dry_run"],
|
1436
|
+
)
|
1437
|
+
|
1438
|
+
results = dynamodb_ops.delete_table(context, table_name)
|
1439
|
+
|
1440
|
+
for result in results:
|
1441
|
+
if result.success:
|
1442
|
+
console.print(f"[green]✅ DynamoDB table deleted successfully[/green]")
|
1443
|
+
console.print(f"[green] 🗑️ Table: {table_name}[/green]")
|
1444
|
+
else:
|
1445
|
+
console.print(f"[red]❌ Failed to delete table: {result.error_message}[/red]")
|
1446
|
+
|
1447
|
+
except Exception as e:
|
1448
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1449
|
+
raise click.ClickException(str(e))
|
1450
|
+
|
1451
|
+
|
1452
|
+
@dynamodb.command()
|
1453
|
+
@click.option("--table-name", required=True, help="Name of the DynamoDB table to backup")
|
1454
|
+
@click.option("--backup-name", help="Custom backup name (defaults to table_name_timestamp)")
|
1455
|
+
@click.pass_context
|
1456
|
+
def backup_table(ctx, table_name, backup_name):
|
1457
|
+
"""Create a backup of a DynamoDB table."""
|
1458
|
+
try:
|
1459
|
+
from runbooks.inventory.models.account import AWSAccount
|
1460
|
+
from runbooks.operate import DynamoDBOperations
|
1461
|
+
from runbooks.operate.base import OperationContext
|
1462
|
+
|
1463
|
+
console.print(f"[blue]🗃️ Creating DynamoDB Table Backup[/blue]")
|
1464
|
+
console.print(
|
1465
|
+
f"[dim]Table: {table_name} | Backup: {backup_name or 'auto-generated'} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
1466
|
+
)
|
1467
|
+
|
1468
|
+
dynamodb_ops = DynamoDBOperations(
|
1469
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
1470
|
+
)
|
1471
|
+
|
1472
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1473
|
+
context = OperationContext(
|
1474
|
+
account=account,
|
1475
|
+
region=ctx.obj["region"],
|
1476
|
+
operation_type="create_backup",
|
1477
|
+
resource_types=["dynamodb:backup"],
|
1478
|
+
dry_run=ctx.obj["dry_run"],
|
1479
|
+
)
|
1480
|
+
|
1481
|
+
results = dynamodb_ops.create_backup(context, table_name=table_name, backup_name=backup_name)
|
1482
|
+
|
1483
|
+
for result in results:
|
1484
|
+
if result.success:
|
1485
|
+
data = result.response_data
|
1486
|
+
backup_details = data.get("BackupDetails", {})
|
1487
|
+
backup_arn = backup_details.get("BackupArn", "")
|
1488
|
+
backup_creation_time = backup_details.get("BackupCreationDateTime", "")
|
1489
|
+
console.print(f"[green]✅ DynamoDB table backup created successfully[/green]")
|
1490
|
+
console.print(f"[green] 📊 Table: {table_name}[/green]")
|
1491
|
+
console.print(f"[green] 💾 Backup: {backup_name or result.resource_id}[/green]")
|
1492
|
+
console.print(f"[green] 🔗 ARN: {backup_arn}[/green]")
|
1493
|
+
console.print(f"[green] 📅 Created: {backup_creation_time}[/green]")
|
1494
|
+
else:
|
1495
|
+
console.print(f"[red]❌ Failed to create backup: {result.error_message}[/red]")
|
1496
|
+
|
1497
|
+
except Exception as e:
|
1498
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1499
|
+
raise click.ClickException(str(e))
|
1500
|
+
|
1501
|
+
|
1502
|
+
# ============================================================================
|
1503
|
+
# CFAT COMMANDS (Cloud Foundations Assessment)
|
1504
|
+
# ============================================================================
|
1505
|
+
|
1506
|
+
|
1507
|
+
@main.group(invoke_without_command=True)
|
1508
|
+
@common_aws_options
|
1509
|
+
@common_output_options
|
1510
|
+
@click.pass_context
|
1511
|
+
def cfat(ctx, profile, region, dry_run, output, output_file):
|
1512
|
+
"""
|
1513
|
+
Cloud Foundations Assessment Tool.
|
1514
|
+
|
1515
|
+
Comprehensive AWS account assessment against Cloud Foundations
|
1516
|
+
best practices with enterprise reporting capabilities.
|
1517
|
+
|
1518
|
+
Examples:
|
1519
|
+
runbooks cfat assess --categories security,cost --output html
|
1520
|
+
runbooks cfat assess --compliance-framework SOC2 --parallel
|
1521
|
+
"""
|
1522
|
+
ctx.obj.update(
|
1523
|
+
{"profile": profile, "region": region, "dry_run": dry_run, "output": output, "output_file": output_file}
|
1524
|
+
)
|
1525
|
+
|
1526
|
+
if ctx.invoked_subcommand is None:
|
1527
|
+
click.echo(ctx.get_help())
|
1528
|
+
|
1529
|
+
|
1530
|
+
@cfat.command()
|
1531
|
+
@click.option("--categories", multiple=True, help="Assessment categories (iam, s3, cloudtrail, etc.)")
|
1532
|
+
@click.option("--severity", type=click.Choice(["INFO", "WARNING", "CRITICAL"]), help="Minimum severity")
|
1533
|
+
@click.option("--compliance-framework", help="Compliance framework (SOC2, PCI-DSS, HIPAA)")
|
1534
|
+
@click.option("--parallel/--sequential", default=True, help="Parallel execution")
|
1535
|
+
@click.option("--max-workers", type=int, default=10, help="Max parallel workers")
|
1536
|
+
@click.pass_context
|
1537
|
+
def assess(ctx, categories, severity, compliance_framework, parallel, max_workers):
|
1538
|
+
"""Run comprehensive Cloud Foundations assessment."""
|
1539
|
+
try:
|
1540
|
+
console.print(f"[blue]🏛️ Starting Cloud Foundations Assessment[/blue]")
|
1541
|
+
console.print(f"[dim]Profile: {ctx.obj['profile']} | Framework: {compliance_framework or 'Default'}[/dim]")
|
1542
|
+
|
1543
|
+
runner = AssessmentRunner(profile=ctx.obj["profile"], region=ctx.obj["region"])
|
1544
|
+
|
1545
|
+
# Configure assessment
|
1546
|
+
if categories:
|
1547
|
+
runner.assessment_config.included_categories = list(categories)
|
1548
|
+
if severity:
|
1549
|
+
runner.set_min_severity(severity)
|
1550
|
+
if compliance_framework:
|
1551
|
+
runner.assessment_config.compliance_framework = compliance_framework
|
1552
|
+
|
1553
|
+
runner.assessment_config.parallel_execution = parallel
|
1554
|
+
runner.assessment_config.max_workers = max_workers
|
1555
|
+
|
1556
|
+
# Run assessment
|
1557
|
+
with console.status("[bold green]Running assessment..."):
|
1558
|
+
report = runner.run_assessment()
|
1559
|
+
|
1560
|
+
# Display results
|
1561
|
+
display_assessment_results(report)
|
1562
|
+
|
1563
|
+
# Save output if requested
|
1564
|
+
if ctx.obj["output"] != "console":
|
1565
|
+
save_assessment_results(report, ctx.obj["output"], ctx.obj["output_file"])
|
1566
|
+
|
1567
|
+
console.print(f"[green]✅ Assessment completed![/green]")
|
1568
|
+
|
1569
|
+
except Exception as e:
|
1570
|
+
console.print(f"[red]❌ Assessment failed: {e}[/red]")
|
1571
|
+
raise click.ClickException(str(e))
|
1572
|
+
|
1573
|
+
|
1574
|
+
# ============================================================================
|
1575
|
+
# SECURITY COMMANDS (Security Baseline Testing)
|
1576
|
+
# ============================================================================
|
1577
|
+
|
1578
|
+
|
1579
|
+
@main.group(invoke_without_command=True)
|
1580
|
+
@common_aws_options
|
1581
|
+
@click.option("--language", type=click.Choice(["EN", "JP", "KR", "VN"]), default="EN", help="Report language")
|
1582
|
+
@common_output_options
|
1583
|
+
@click.pass_context
|
1584
|
+
def security(ctx, profile, region, dry_run, language, output, output_file):
|
1585
|
+
"""
|
1586
|
+
AWS Security Baseline Assessment.
|
1587
|
+
|
1588
|
+
Comprehensive security validation against AWS security best practices
|
1589
|
+
with multi-language reporting capabilities.
|
1590
|
+
|
1591
|
+
Examples:
|
1592
|
+
runbooks security assess --language EN --output html
|
1593
|
+
runbooks security check root-mfa --profile production
|
1594
|
+
"""
|
1595
|
+
ctx.obj.update(
|
1596
|
+
{
|
1597
|
+
"profile": profile,
|
1598
|
+
"region": region,
|
1599
|
+
"dry_run": dry_run,
|
1600
|
+
"language": language,
|
1601
|
+
"output": output,
|
1602
|
+
"output_file": output_file,
|
1603
|
+
}
|
1604
|
+
)
|
1605
|
+
|
1606
|
+
if ctx.invoked_subcommand is None:
|
1607
|
+
click.echo(ctx.get_help())
|
1608
|
+
|
1609
|
+
|
1610
|
+
@security.command()
|
1611
|
+
@click.option("--checks", multiple=True, help="Specific security checks to run")
|
1612
|
+
@click.pass_context
|
1613
|
+
def assess(ctx, checks):
|
1614
|
+
"""Run comprehensive security baseline assessment."""
|
1615
|
+
try:
|
1616
|
+
from runbooks.security.security_baseline_tester import SecurityBaselineTester
|
1617
|
+
|
1618
|
+
console.print(f"[blue]🔒 Starting Security Assessment[/blue]")
|
1619
|
+
console.print(f"[dim]Profile: {ctx.obj['profile']} | Language: {ctx.obj['language']}[/dim]")
|
1620
|
+
|
1621
|
+
tester = SecurityBaselineTester(ctx.obj["profile"], ctx.obj["language"], ctx.obj.get("output_file"))
|
1622
|
+
|
1623
|
+
with console.status("[bold green]Running security checks..."):
|
1624
|
+
tester.run()
|
1625
|
+
|
1626
|
+
console.print(f"[green]✅ Security assessment completed![/green]")
|
1627
|
+
console.print(f"[yellow]📁 Reports generated in: {ctx.obj.get('output_file', './results')}[/yellow]")
|
1628
|
+
|
1629
|
+
except Exception as e:
|
1630
|
+
console.print(f"[red]❌ Security assessment failed: {e}[/red]")
|
1631
|
+
raise click.ClickException(str(e))
|
1632
|
+
|
1633
|
+
|
1634
|
+
# ============================================================================
|
1635
|
+
# ORGANIZATIONS COMMANDS (OU Management)
|
1636
|
+
# ============================================================================
|
1637
|
+
|
1638
|
+
|
1639
|
+
@main.group(invoke_without_command=True)
|
1640
|
+
@common_aws_options
|
1641
|
+
@common_output_options
|
1642
|
+
@click.pass_context
|
1643
|
+
def org(ctx, profile, region, dry_run, output, output_file):
|
1644
|
+
"""
|
1645
|
+
AWS Organizations management and automation.
|
1646
|
+
|
1647
|
+
Manage organizational units (OUs), accounts, and policies with
|
1648
|
+
Cloud Foundations best practices.
|
1649
|
+
|
1650
|
+
Examples:
|
1651
|
+
runbooks org list-ous --output json
|
1652
|
+
runbooks org setup-ous --template security --dry-run
|
1653
|
+
"""
|
1654
|
+
ctx.obj.update(
|
1655
|
+
{"profile": profile, "region": region, "dry_run": dry_run, "output": output, "output_file": output_file}
|
1656
|
+
)
|
1657
|
+
|
1658
|
+
if ctx.invoked_subcommand is None:
|
1659
|
+
click.echo(ctx.get_help())
|
1660
|
+
|
1661
|
+
|
1662
|
+
@org.command()
|
1663
|
+
@click.pass_context
|
1664
|
+
def list_ous(ctx):
|
1665
|
+
"""List all organizational units."""
|
1666
|
+
try:
|
1667
|
+
from runbooks.inventory.collectors.aws_management import OrganizationsManager
|
1668
|
+
|
1669
|
+
console.print(f"[blue]🏢 Listing Organizations Structure[/blue]")
|
1670
|
+
|
1671
|
+
manager = OrganizationsManager(profile=ctx.obj["profile"], region=ctx.obj["region"])
|
1672
|
+
|
1673
|
+
with console.status("[bold green]Retrieving OUs..."):
|
1674
|
+
ous = manager.list_organizational_units()
|
1675
|
+
|
1676
|
+
if ctx.obj["output"] == "console":
|
1677
|
+
display_ou_structure(ous)
|
1678
|
+
else:
|
1679
|
+
save_ou_results(ous, ctx.obj["output"], ctx.obj["output_file"])
|
1680
|
+
|
1681
|
+
console.print(f"[green]✅ Found {len(ous)} organizational units[/green]")
|
1682
|
+
|
1683
|
+
except Exception as e:
|
1684
|
+
console.print(f"[red]❌ Failed to list OUs: {e}[/red]")
|
1685
|
+
raise click.ClickException(str(e))
|
1686
|
+
|
1687
|
+
|
1688
|
+
# ============================================================================
|
1689
|
+
# REMEDIATION COMMANDS (Security & Compliance Fixes)
|
1690
|
+
# ============================================================================
|
1691
|
+
|
1692
|
+
|
1693
|
+
@main.group(invoke_without_command=True)
|
1694
|
+
@common_aws_options
|
1695
|
+
@common_output_options
|
1696
|
+
@click.option("--backup-enabled", is_flag=True, default=True, help="Enable backup creation before changes")
|
1697
|
+
@click.option("--notification-enabled", is_flag=True, help="Enable SNS notifications")
|
1698
|
+
@click.option("--sns-topic-arn", help="SNS topic ARN for notifications")
|
1699
|
+
@click.pass_context
|
1700
|
+
def remediation(
|
1701
|
+
ctx, profile, region, dry_run, output, output_file, backup_enabled, notification_enabled, sns_topic_arn
|
1702
|
+
):
|
1703
|
+
"""
|
1704
|
+
AWS Security & Compliance Remediation - Automated fixes for assessment findings.
|
1705
|
+
|
1706
|
+
Provides comprehensive automated remediation capabilities for security and
|
1707
|
+
compliance findings from security assessments and CFAT evaluations.
|
1708
|
+
|
1709
|
+
## Key Features
|
1710
|
+
|
1711
|
+
- **S3 Security**: Public access controls, encryption, SSL enforcement
|
1712
|
+
- **EC2 Security**: Security group hardening, network security
|
1713
|
+
- **Multi-Account**: Bulk operations across AWS Organizations
|
1714
|
+
- **Safety Features**: Dry-run, backup, rollback capabilities
|
1715
|
+
- **Compliance**: CIS, NIST, SOC2, CloudGuard/Dome9 mapping
|
1716
|
+
|
1717
|
+
Examples:
|
1718
|
+
runbooks remediation s3 block-public-access --bucket-name critical-bucket
|
1719
|
+
runbooks remediation auto-fix --findings security-findings.json --severity critical
|
1720
|
+
runbooks remediation bulk enforce-ssl --accounts 123456789012,987654321098
|
1721
|
+
"""
|
1722
|
+
ctx.obj.update(
|
1723
|
+
{
|
1724
|
+
"profile": profile,
|
1725
|
+
"region": region,
|
1726
|
+
"dry_run": dry_run,
|
1727
|
+
"output": output,
|
1728
|
+
"output_file": output_file,
|
1729
|
+
"backup_enabled": backup_enabled,
|
1730
|
+
"notification_enabled": notification_enabled,
|
1731
|
+
"sns_topic_arn": sns_topic_arn,
|
1732
|
+
}
|
1733
|
+
)
|
1734
|
+
|
1735
|
+
if ctx.invoked_subcommand is None:
|
1736
|
+
click.echo(ctx.get_help())
|
1737
|
+
|
1738
|
+
|
1739
|
+
@remediation.group()
|
1740
|
+
@click.pass_context
|
1741
|
+
def s3(ctx):
|
1742
|
+
"""S3 security and compliance remediation operations."""
|
1743
|
+
pass
|
1744
|
+
|
1745
|
+
|
1746
|
+
@remediation.group()
|
1747
|
+
@click.pass_context
|
1748
|
+
def ec2(ctx):
|
1749
|
+
"""EC2 infrastructure security and compliance remediation operations."""
|
1750
|
+
pass
|
1751
|
+
|
1752
|
+
|
1753
|
+
@remediation.group()
|
1754
|
+
@click.pass_context
|
1755
|
+
def kms(ctx):
|
1756
|
+
"""KMS key management and encryption remediation operations."""
|
1757
|
+
pass
|
1758
|
+
|
1759
|
+
|
1760
|
+
@remediation.group()
|
1761
|
+
@click.pass_context
|
1762
|
+
def dynamodb(ctx):
|
1763
|
+
"""DynamoDB security and optimization remediation operations."""
|
1764
|
+
pass
|
1765
|
+
|
1766
|
+
|
1767
|
+
@remediation.group()
|
1768
|
+
@click.pass_context
|
1769
|
+
def rds(ctx):
|
1770
|
+
"""RDS database security and optimization remediation operations."""
|
1771
|
+
pass
|
1772
|
+
|
1773
|
+
|
1774
|
+
@remediation.group()
|
1775
|
+
@click.pass_context
|
1776
|
+
def lambda_func(ctx):
|
1777
|
+
"""Lambda function security and optimization remediation operations."""
|
1778
|
+
pass
|
1779
|
+
|
1780
|
+
|
1781
|
+
@remediation.group()
|
1782
|
+
@click.pass_context
|
1783
|
+
def acm(ctx):
|
1784
|
+
"""ACM certificate lifecycle and security remediation operations."""
|
1785
|
+
pass
|
1786
|
+
|
1787
|
+
|
1788
|
+
@remediation.group()
|
1789
|
+
@click.pass_context
|
1790
|
+
def cognito(ctx):
|
1791
|
+
"""Cognito user management and authentication security remediation operations."""
|
1792
|
+
pass
|
1793
|
+
|
1794
|
+
|
1795
|
+
@remediation.group()
|
1796
|
+
@click.pass_context
|
1797
|
+
def cloudtrail(ctx):
|
1798
|
+
"""CloudTrail audit trail and policy security remediation operations."""
|
1799
|
+
pass
|
1800
|
+
|
1801
|
+
|
1802
|
+
@s3.command()
|
1803
|
+
@click.option("--bucket-name", required=True, help="Target S3 bucket name")
|
1804
|
+
@click.option("--confirm", is_flag=True, help="Confirm destructive operation")
|
1805
|
+
@click.pass_context
|
1806
|
+
def block_public_access(ctx, bucket_name, confirm):
|
1807
|
+
"""Block all public access to S3 bucket."""
|
1808
|
+
try:
|
1809
|
+
from runbooks.inventory.models.account import AWSAccount
|
1810
|
+
from runbooks.remediation.base import RemediationContext
|
1811
|
+
from runbooks.remediation.s3_remediation import S3SecurityRemediation
|
1812
|
+
|
1813
|
+
console.print(f"[blue]🔒 Blocking Public Access on S3 Bucket[/blue]")
|
1814
|
+
console.print(f"[dim]Bucket: {bucket_name} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
1815
|
+
|
1816
|
+
# Initialize remediation
|
1817
|
+
s3_remediation = S3SecurityRemediation(
|
1818
|
+
profile=ctx.obj["profile"],
|
1819
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
1820
|
+
notification_enabled=ctx.obj["notification_enabled"],
|
1821
|
+
)
|
1822
|
+
|
1823
|
+
# Create remediation context
|
1824
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1825
|
+
context = RemediationContext(
|
1826
|
+
account=account,
|
1827
|
+
region=ctx.obj["region"],
|
1828
|
+
operation_type="block_public_access",
|
1829
|
+
dry_run=ctx.obj["dry_run"],
|
1830
|
+
force=confirm,
|
1831
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
1832
|
+
)
|
1833
|
+
|
1834
|
+
# Execute remediation
|
1835
|
+
results = s3_remediation.block_public_access(context, bucket_name)
|
1836
|
+
|
1837
|
+
# Display results
|
1838
|
+
for result in results:
|
1839
|
+
if result.success:
|
1840
|
+
console.print(f"[green]✅ Successfully blocked public access on bucket: {bucket_name}[/green]")
|
1841
|
+
if result.compliance_evidence:
|
1842
|
+
console.print(
|
1843
|
+
"[dim]Compliance controls satisfied: "
|
1844
|
+
+ ", ".join(result.context.compliance_mapping.cis_controls)
|
1845
|
+
+ "[/dim]"
|
1846
|
+
)
|
1847
|
+
else:
|
1848
|
+
console.print(f"[red]❌ Failed to block public access: {result.error_message}[/red]")
|
1849
|
+
|
1850
|
+
except Exception as e:
|
1851
|
+
console.print(f"[red]❌ Remediation failed: {e}[/red]")
|
1852
|
+
raise click.ClickException(str(e))
|
1853
|
+
|
1854
|
+
|
1855
|
+
@s3.command()
|
1856
|
+
@click.option("--bucket-name", required=True, help="Target S3 bucket name")
|
1857
|
+
@click.option("--confirm", is_flag=True, help="Confirm policy changes")
|
1858
|
+
@click.pass_context
|
1859
|
+
def enforce_ssl(ctx, bucket_name, confirm):
|
1860
|
+
"""Enforce HTTPS-only access to S3 bucket."""
|
1861
|
+
try:
|
1862
|
+
from runbooks.inventory.models.account import AWSAccount
|
1863
|
+
from runbooks.remediation.base import RemediationContext
|
1864
|
+
from runbooks.remediation.s3_remediation import S3SecurityRemediation
|
1865
|
+
|
1866
|
+
console.print(f"[blue]🔐 Enforcing SSL on S3 Bucket[/blue]")
|
1867
|
+
console.print(f"[dim]Bucket: {bucket_name} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
1868
|
+
|
1869
|
+
s3_remediation = S3SecurityRemediation(profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"])
|
1870
|
+
|
1871
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1872
|
+
context = RemediationContext(
|
1873
|
+
account=account,
|
1874
|
+
region=ctx.obj["region"],
|
1875
|
+
operation_type="enforce_ssl",
|
1876
|
+
dry_run=ctx.obj["dry_run"],
|
1877
|
+
force=confirm,
|
1878
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
1879
|
+
)
|
1880
|
+
|
1881
|
+
results = s3_remediation.enforce_ssl(context, bucket_name)
|
1882
|
+
|
1883
|
+
for result in results:
|
1884
|
+
if result.success:
|
1885
|
+
console.print(f"[green]✅ Successfully enforced SSL on bucket: {bucket_name}[/green]")
|
1886
|
+
else:
|
1887
|
+
console.print(f"[red]❌ Failed to enforce SSL: {result.error_message}[/red]")
|
1888
|
+
|
1889
|
+
except Exception as e:
|
1890
|
+
console.print(f"[red]❌ SSL enforcement failed: {e}[/red]")
|
1891
|
+
raise click.ClickException(str(e))
|
1892
|
+
|
1893
|
+
|
1894
|
+
@s3.command()
|
1895
|
+
@click.option("--bucket-name", required=True, help="Target S3 bucket name")
|
1896
|
+
@click.option("--kms-key-id", help="KMS key ID for encryption (uses default if not specified)")
|
1897
|
+
@click.pass_context
|
1898
|
+
def enable_encryption(ctx, bucket_name, kms_key_id):
|
1899
|
+
"""Enable server-side encryption on S3 bucket."""
|
1900
|
+
try:
|
1901
|
+
from runbooks.inventory.models.account import AWSAccount
|
1902
|
+
from runbooks.remediation.base import RemediationContext
|
1903
|
+
from runbooks.remediation.s3_remediation import S3SecurityRemediation
|
1904
|
+
|
1905
|
+
console.print(f"[blue]🔐 Enabling Encryption on S3 Bucket[/blue]")
|
1906
|
+
console.print(f"[dim]Bucket: {bucket_name} | KMS Key: {kms_key_id or 'default'}[/dim]")
|
1907
|
+
|
1908
|
+
s3_remediation = S3SecurityRemediation(profile=ctx.obj["profile"])
|
1909
|
+
|
1910
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1911
|
+
context = RemediationContext(
|
1912
|
+
account=account,
|
1913
|
+
region=ctx.obj["region"],
|
1914
|
+
operation_type="enable_encryption",
|
1915
|
+
dry_run=ctx.obj["dry_run"],
|
1916
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
1917
|
+
)
|
1918
|
+
|
1919
|
+
results = s3_remediation.enable_encryption(context, bucket_name, kms_key_id)
|
1920
|
+
|
1921
|
+
for result in results:
|
1922
|
+
if result.success:
|
1923
|
+
console.print(f"[green]✅ Successfully enabled encryption on bucket: {bucket_name}[/green]")
|
1924
|
+
else:
|
1925
|
+
console.print(f"[red]❌ Failed to enable encryption: {result.error_message}[/red]")
|
1926
|
+
|
1927
|
+
except Exception as e:
|
1928
|
+
console.print(f"[red]❌ Encryption enablement failed: {e}[/red]")
|
1929
|
+
raise click.ClickException(str(e))
|
1930
|
+
|
1931
|
+
|
1932
|
+
@s3.command()
|
1933
|
+
@click.option("--bucket-name", required=True, help="Target S3 bucket name")
|
1934
|
+
@click.pass_context
|
1935
|
+
def secure_comprehensive(ctx, bucket_name):
|
1936
|
+
"""Apply comprehensive S3 security configuration to bucket."""
|
1937
|
+
try:
|
1938
|
+
from runbooks.inventory.models.account import AWSAccount
|
1939
|
+
from runbooks.remediation.base import RemediationContext
|
1940
|
+
from runbooks.remediation.s3_remediation import S3SecurityRemediation
|
1941
|
+
|
1942
|
+
console.print(f"[blue]🛡️ Comprehensive S3 Security Remediation[/blue]")
|
1943
|
+
console.print(f"[dim]Bucket: {bucket_name} | Operations: 5 security controls[/dim]")
|
1944
|
+
|
1945
|
+
s3_remediation = S3SecurityRemediation(profile=ctx.obj["profile"])
|
1946
|
+
|
1947
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1948
|
+
context = RemediationContext(
|
1949
|
+
account=account,
|
1950
|
+
region=ctx.obj["region"],
|
1951
|
+
operation_type="secure_bucket_comprehensive",
|
1952
|
+
dry_run=ctx.obj["dry_run"],
|
1953
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
1954
|
+
)
|
1955
|
+
|
1956
|
+
with console.status("[bold green]Applying comprehensive security controls..."):
|
1957
|
+
results = s3_remediation.secure_bucket_comprehensive(context, bucket_name)
|
1958
|
+
|
1959
|
+
# Display summary
|
1960
|
+
successful = [r for r in results if r.success]
|
1961
|
+
failed = [r for r in results if r.failed]
|
1962
|
+
|
1963
|
+
console.print(f"\n[bold]Security Remediation Summary:[/bold]")
|
1964
|
+
console.print(f"✅ Successful operations: {len(successful)}")
|
1965
|
+
console.print(f"❌ Failed operations: {len(failed)}")
|
1966
|
+
|
1967
|
+
for result in results:
|
1968
|
+
status = "✅" if result.success else "❌"
|
1969
|
+
operation = result.context.operation_type.replace("_", " ").title()
|
1970
|
+
console.print(f" {status} {operation}")
|
1971
|
+
|
1972
|
+
except Exception as e:
|
1973
|
+
console.print(f"[red]❌ Comprehensive remediation failed: {e}[/red]")
|
1974
|
+
raise click.ClickException(str(e))
|
1975
|
+
|
1976
|
+
|
1977
|
+
# ============================================================================
|
1978
|
+
# EC2 REMEDIATION COMMANDS
|
1979
|
+
# ============================================================================
|
1980
|
+
|
1981
|
+
|
1982
|
+
@ec2.command()
|
1983
|
+
@click.option("--exclude-default", is_flag=True, default=True, help="Exclude default security groups")
|
1984
|
+
@click.pass_context
|
1985
|
+
def cleanup_security_groups(ctx, exclude_default):
|
1986
|
+
"""Cleanup unused security groups with dependency analysis."""
|
1987
|
+
try:
|
1988
|
+
from runbooks.inventory.models.account import AWSAccount
|
1989
|
+
from runbooks.remediation.base import RemediationContext
|
1990
|
+
from runbooks.remediation.ec2_remediation import EC2SecurityRemediation
|
1991
|
+
|
1992
|
+
console.print(f"[blue]🖥️ EC2 Security Group Cleanup[/blue]")
|
1993
|
+
console.print(f"[dim]Exclude Default: {exclude_default} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
1994
|
+
|
1995
|
+
ec2_remediation = EC2SecurityRemediation(profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"])
|
1996
|
+
|
1997
|
+
account = AWSAccount(account_id="current", account_name="current")
|
1998
|
+
context = RemediationContext(
|
1999
|
+
account=account,
|
2000
|
+
region=ctx.obj["region"],
|
2001
|
+
operation_type="cleanup_unused_security_groups",
|
2002
|
+
dry_run=ctx.obj["dry_run"],
|
2003
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2004
|
+
)
|
2005
|
+
|
2006
|
+
results = ec2_remediation.cleanup_unused_security_groups(context, exclude_default=exclude_default)
|
2007
|
+
|
2008
|
+
for result in results:
|
2009
|
+
if result.success:
|
2010
|
+
console.print(f"[green]✅ Successfully cleaned up unused security groups[/green]")
|
2011
|
+
data = result.response_data
|
2012
|
+
console.print(f"[dim]Deleted: {data.get('total_deleted', 0)} groups[/dim]")
|
2013
|
+
else:
|
2014
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2015
|
+
|
2016
|
+
except Exception as e:
|
2017
|
+
console.print(f"[red]❌ EC2 security group cleanup failed: {e}[/red]")
|
2018
|
+
raise click.ClickException(str(e))
|
2019
|
+
|
2020
|
+
|
2021
|
+
@ec2.command()
|
2022
|
+
@click.option("--max-age-days", type=int, default=30, help="Maximum age for unattached volumes")
|
2023
|
+
@click.pass_context
|
2024
|
+
def cleanup_ebs_volumes(ctx, max_age_days):
|
2025
|
+
"""Cleanup unattached EBS volumes with CloudTrail analysis."""
|
2026
|
+
try:
|
2027
|
+
from runbooks.inventory.models.account import AWSAccount
|
2028
|
+
from runbooks.remediation.base import RemediationContext
|
2029
|
+
from runbooks.remediation.ec2_remediation import EC2SecurityRemediation
|
2030
|
+
|
2031
|
+
console.print(f"[blue]💽 EC2 EBS Volume Cleanup[/blue]")
|
2032
|
+
console.print(f"[dim]Max Age: {max_age_days} days | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2033
|
+
|
2034
|
+
ec2_remediation = EC2SecurityRemediation(
|
2035
|
+
profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"], cloudtrail_analysis=True
|
2036
|
+
)
|
2037
|
+
|
2038
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2039
|
+
context = RemediationContext(
|
2040
|
+
account=account,
|
2041
|
+
region=ctx.obj["region"],
|
2042
|
+
operation_type="cleanup_unattached_ebs_volumes",
|
2043
|
+
dry_run=ctx.obj["dry_run"],
|
2044
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2045
|
+
)
|
2046
|
+
|
2047
|
+
results = ec2_remediation.cleanup_unattached_ebs_volumes(context, max_age_days=max_age_days)
|
2048
|
+
|
2049
|
+
for result in results:
|
2050
|
+
if result.success:
|
2051
|
+
console.print(f"[green]✅ Successfully cleaned up unattached EBS volumes[/green]")
|
2052
|
+
data = result.response_data
|
2053
|
+
console.print(f"[dim]Deleted: {data.get('total_deleted', 0)} volumes[/dim]")
|
2054
|
+
else:
|
2055
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2056
|
+
|
2057
|
+
except Exception as e:
|
2058
|
+
console.print(f"[red]❌ EBS volume cleanup failed: {e}[/red]")
|
2059
|
+
raise click.ClickException(str(e))
|
2060
|
+
|
2061
|
+
|
2062
|
+
@ec2.command()
|
2063
|
+
@click.pass_context
|
2064
|
+
def audit_public_ips(ctx):
|
2065
|
+
"""Comprehensive public IP auditing and analysis."""
|
2066
|
+
try:
|
2067
|
+
from runbooks.inventory.models.account import AWSAccount
|
2068
|
+
from runbooks.remediation.base import RemediationContext
|
2069
|
+
from runbooks.remediation.ec2_remediation import EC2SecurityRemediation
|
2070
|
+
|
2071
|
+
console.print(f"[blue]🌐 EC2 Public IP Audit[/blue]")
|
2072
|
+
console.print(f"[dim]Region: {ctx.obj['region']}[/dim]")
|
2073
|
+
|
2074
|
+
ec2_remediation = EC2SecurityRemediation(profile=ctx.obj["profile"])
|
2075
|
+
|
2076
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2077
|
+
context = RemediationContext(
|
2078
|
+
account=account,
|
2079
|
+
region=ctx.obj["region"],
|
2080
|
+
operation_type="audit_public_ips",
|
2081
|
+
dry_run=False, # Audit operation, not destructive
|
2082
|
+
backup_enabled=False,
|
2083
|
+
)
|
2084
|
+
|
2085
|
+
results = ec2_remediation.audit_public_ips(context)
|
2086
|
+
|
2087
|
+
for result in results:
|
2088
|
+
if result.success:
|
2089
|
+
console.print(f"[green]✅ Public IP audit completed[/green]")
|
2090
|
+
data = result.response_data
|
2091
|
+
posture = data.get("security_posture", {})
|
2092
|
+
console.print(
|
2093
|
+
f"[dim]Risk Level: {posture.get('security_risk_level', 'UNKNOWN')} | "
|
2094
|
+
f"Public Instances: {posture.get('instances_with_public_access', 0)}[/dim]"
|
2095
|
+
)
|
2096
|
+
else:
|
2097
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2098
|
+
|
2099
|
+
except Exception as e:
|
2100
|
+
console.print(f"[red]❌ Public IP audit failed: {e}[/red]")
|
2101
|
+
raise click.ClickException(str(e))
|
2102
|
+
|
2103
|
+
|
2104
|
+
@ec2.command()
|
2105
|
+
@click.pass_context
|
2106
|
+
def disable_subnet_auto_ip(ctx):
|
2107
|
+
"""Disable automatic public IP assignment on subnets."""
|
2108
|
+
try:
|
2109
|
+
from runbooks.inventory.models.account import AWSAccount
|
2110
|
+
from runbooks.remediation.base import RemediationContext
|
2111
|
+
from runbooks.remediation.ec2_remediation import EC2SecurityRemediation
|
2112
|
+
|
2113
|
+
console.print(f"[blue]🔒 Disable Subnet Auto-Assign Public IP[/blue]")
|
2114
|
+
console.print(f"[dim]Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2115
|
+
|
2116
|
+
ec2_remediation = EC2SecurityRemediation(profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"])
|
2117
|
+
|
2118
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2119
|
+
context = RemediationContext(
|
2120
|
+
account=account,
|
2121
|
+
region=ctx.obj["region"],
|
2122
|
+
operation_type="disable_subnet_auto_public_ip",
|
2123
|
+
dry_run=ctx.obj["dry_run"],
|
2124
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2125
|
+
)
|
2126
|
+
|
2127
|
+
results = ec2_remediation.disable_subnet_auto_public_ip(context)
|
2128
|
+
|
2129
|
+
for result in results:
|
2130
|
+
if result.success:
|
2131
|
+
console.print(f"[green]✅ Successfully disabled subnet auto-assign public IP[/green]")
|
2132
|
+
data = result.response_data
|
2133
|
+
console.print(f"[dim]Modified: {data.get('total_modified', 0)} subnets[/dim]")
|
2134
|
+
else:
|
2135
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2136
|
+
|
2137
|
+
except Exception as e:
|
2138
|
+
console.print(f"[red]❌ Subnet configuration failed: {e}[/red]")
|
2139
|
+
raise click.ClickException(str(e))
|
2140
|
+
|
2141
|
+
|
2142
|
+
# ============================================================================
|
2143
|
+
# KMS REMEDIATION COMMANDS
|
2144
|
+
# ============================================================================
|
2145
|
+
|
2146
|
+
|
2147
|
+
@kms.command()
|
2148
|
+
@click.option("--key-id", required=True, help="KMS key ID to enable rotation for")
|
2149
|
+
@click.option("--rotation-period", type=int, default=365, help="Rotation period in days")
|
2150
|
+
@click.pass_context
|
2151
|
+
def enable_rotation(ctx, key_id, rotation_period):
|
2152
|
+
"""Enable key rotation for a specific KMS key."""
|
2153
|
+
try:
|
2154
|
+
from runbooks.inventory.models.account import AWSAccount
|
2155
|
+
from runbooks.remediation.base import RemediationContext
|
2156
|
+
from runbooks.remediation.kms_remediation import KMSSecurityRemediation
|
2157
|
+
|
2158
|
+
console.print(f"[blue]🔐 KMS Key Rotation Enable[/blue]")
|
2159
|
+
console.print(f"[dim]Key: {key_id} | Period: {rotation_period} days | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2160
|
+
|
2161
|
+
kms_remediation = KMSSecurityRemediation(
|
2162
|
+
profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"], rotation_period_days=rotation_period
|
2163
|
+
)
|
2164
|
+
|
2165
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2166
|
+
context = RemediationContext(
|
2167
|
+
account=account,
|
2168
|
+
region=ctx.obj["region"],
|
2169
|
+
operation_type="enable_key_rotation",
|
2170
|
+
dry_run=ctx.obj["dry_run"],
|
2171
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2172
|
+
)
|
2173
|
+
|
2174
|
+
results = kms_remediation.enable_key_rotation(context, key_id, rotation_period)
|
2175
|
+
|
2176
|
+
for result in results:
|
2177
|
+
if result.success:
|
2178
|
+
console.print(f"[green]✅ Successfully enabled key rotation for: {key_id}[/green]")
|
2179
|
+
elif result.status.value == "skipped":
|
2180
|
+
console.print(f"[yellow]⚠️ Key rotation already enabled or not supported: {key_id}[/yellow]")
|
2181
|
+
else:
|
2182
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2183
|
+
|
2184
|
+
except Exception as e:
|
2185
|
+
console.print(f"[red]❌ KMS key rotation failed: {e}[/red]")
|
2186
|
+
raise click.ClickException(str(e))
|
2187
|
+
|
2188
|
+
|
2189
|
+
@kms.command()
|
751
2190
|
@click.option(
|
752
|
-
"--
|
753
|
-
"-
|
754
|
-
|
755
|
-
help="
|
756
|
-
type=str,
|
2191
|
+
"--key-filter",
|
2192
|
+
type=click.Choice(["customer-managed", "all"]),
|
2193
|
+
default="customer-managed",
|
2194
|
+
help="Filter keys to process",
|
757
2195
|
)
|
2196
|
+
@click.pass_context
|
2197
|
+
def enable_rotation_bulk(ctx, key_filter):
|
2198
|
+
"""Enable key rotation for all eligible KMS keys in bulk."""
|
2199
|
+
try:
|
2200
|
+
from runbooks.inventory.models.account import AWSAccount
|
2201
|
+
from runbooks.remediation.base import RemediationContext
|
2202
|
+
from runbooks.remediation.kms_remediation import KMSSecurityRemediation
|
2203
|
+
|
2204
|
+
console.print(f"[blue]🔐 KMS Bulk Key Rotation Enable[/blue]")
|
2205
|
+
console.print(f"[dim]Filter: {key_filter} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2206
|
+
|
2207
|
+
kms_remediation = KMSSecurityRemediation(profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"])
|
2208
|
+
|
2209
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2210
|
+
context = RemediationContext(
|
2211
|
+
account=account,
|
2212
|
+
region=ctx.obj["region"],
|
2213
|
+
operation_type="enable_key_rotation_bulk",
|
2214
|
+
dry_run=ctx.obj["dry_run"],
|
2215
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2216
|
+
)
|
2217
|
+
|
2218
|
+
with console.status("[bold green]Processing KMS keys..."):
|
2219
|
+
results = kms_remediation.enable_key_rotation_bulk(context, key_filter=key_filter)
|
2220
|
+
|
2221
|
+
for result in results:
|
2222
|
+
if result.success:
|
2223
|
+
console.print(f"[green]✅ Bulk key rotation completed[/green]")
|
2224
|
+
data = result.response_data
|
2225
|
+
console.print(f"[dim]Processed: {data.get('successful_keys', 0)} keys successfully[/dim]")
|
2226
|
+
else:
|
2227
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2228
|
+
|
2229
|
+
except Exception as e:
|
2230
|
+
console.print(f"[red]❌ Bulk KMS key rotation failed: {e}[/red]")
|
2231
|
+
raise click.ClickException(str(e))
|
2232
|
+
|
2233
|
+
|
2234
|
+
@kms.command()
|
2235
|
+
@click.pass_context
|
2236
|
+
def analyze_usage(ctx):
|
2237
|
+
"""Analyze KMS key usage and provide optimization recommendations."""
|
2238
|
+
try:
|
2239
|
+
from runbooks.inventory.models.account import AWSAccount
|
2240
|
+
from runbooks.remediation.base import RemediationContext
|
2241
|
+
from runbooks.remediation.kms_remediation import KMSSecurityRemediation
|
2242
|
+
|
2243
|
+
console.print(f"[blue]📊 KMS Key Usage Analysis[/blue]")
|
2244
|
+
console.print(f"[dim]Region: {ctx.obj['region']}[/dim]")
|
2245
|
+
|
2246
|
+
kms_remediation = KMSSecurityRemediation(profile=ctx.obj["profile"])
|
2247
|
+
|
2248
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2249
|
+
context = RemediationContext(
|
2250
|
+
account=account,
|
2251
|
+
region=ctx.obj["region"],
|
2252
|
+
operation_type="analyze_key_usage",
|
2253
|
+
dry_run=False, # Analysis operation
|
2254
|
+
backup_enabled=False,
|
2255
|
+
)
|
2256
|
+
|
2257
|
+
with console.status("[bold green]Analyzing KMS keys..."):
|
2258
|
+
results = kms_remediation.analyze_key_usage(context)
|
2259
|
+
|
2260
|
+
for result in results:
|
2261
|
+
if result.success:
|
2262
|
+
console.print(f"[green]✅ KMS key analysis completed[/green]")
|
2263
|
+
data = result.response_data
|
2264
|
+
analytics = data.get("usage_analytics", {})
|
2265
|
+
console.print(
|
2266
|
+
f"[dim]Total Keys: {analytics.get('total_keys', 0)} | "
|
2267
|
+
f"Compliance Rate: {analytics.get('rotation_compliance_rate', 0):.1f}%[/dim]"
|
2268
|
+
)
|
2269
|
+
else:
|
2270
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2271
|
+
|
2272
|
+
except Exception as e:
|
2273
|
+
console.print(f"[red]❌ KMS analysis failed: {e}[/red]")
|
2274
|
+
raise click.ClickException(str(e))
|
2275
|
+
|
2276
|
+
|
2277
|
+
# ============================================================================
|
2278
|
+
# DYNAMODB REMEDIATION COMMANDS
|
2279
|
+
# ============================================================================
|
2280
|
+
|
2281
|
+
|
2282
|
+
@dynamodb.command()
|
2283
|
+
@click.option("--table-name", required=True, help="DynamoDB table name")
|
2284
|
+
@click.option("--kms-key-id", help="KMS key ID for encryption (uses default if not specified)")
|
2285
|
+
@click.pass_context
|
2286
|
+
def enable_encryption(ctx, table_name, kms_key_id):
|
2287
|
+
"""Enable server-side encryption for a DynamoDB table."""
|
2288
|
+
try:
|
2289
|
+
from runbooks.inventory.models.account import AWSAccount
|
2290
|
+
from runbooks.remediation.base import RemediationContext
|
2291
|
+
from runbooks.remediation.dynamodb_remediation import DynamoDBRemediation
|
2292
|
+
|
2293
|
+
console.print(f"[blue]🗃️ DynamoDB Table Encryption[/blue]")
|
2294
|
+
console.print(
|
2295
|
+
f"[dim]Table: {table_name} | KMS Key: {kms_key_id or 'default'} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
2296
|
+
)
|
2297
|
+
|
2298
|
+
dynamodb_remediation = DynamoDBRemediation(
|
2299
|
+
profile=ctx.obj["profile"],
|
2300
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2301
|
+
default_kms_key=kms_key_id or "alias/aws/dynamodb",
|
2302
|
+
)
|
2303
|
+
|
2304
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2305
|
+
context = RemediationContext(
|
2306
|
+
account=account,
|
2307
|
+
region=ctx.obj["region"],
|
2308
|
+
operation_type="enable_table_encryption",
|
2309
|
+
dry_run=ctx.obj["dry_run"],
|
2310
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2311
|
+
)
|
2312
|
+
|
2313
|
+
results = dynamodb_remediation.enable_table_encryption(context, table_name, kms_key_id)
|
2314
|
+
|
2315
|
+
for result in results:
|
2316
|
+
if result.success:
|
2317
|
+
console.print(f"[green]✅ Successfully enabled encryption for table: {table_name}[/green]")
|
2318
|
+
elif result.status.value == "skipped":
|
2319
|
+
console.print(f"[yellow]⚠️ Encryption already enabled for table: {table_name}[/yellow]")
|
2320
|
+
else:
|
2321
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2322
|
+
|
2323
|
+
except Exception as e:
|
2324
|
+
console.print(f"[red]❌ DynamoDB encryption failed: {e}[/red]")
|
2325
|
+
raise click.ClickException(str(e))
|
2326
|
+
|
2327
|
+
|
2328
|
+
@dynamodb.command()
|
2329
|
+
@click.pass_context
|
2330
|
+
def enable_encryption_bulk(ctx):
|
2331
|
+
"""Enable server-side encryption for all DynamoDB tables in bulk."""
|
2332
|
+
try:
|
2333
|
+
from runbooks.inventory.models.account import AWSAccount
|
2334
|
+
from runbooks.remediation.base import RemediationContext
|
2335
|
+
from runbooks.remediation.dynamodb_remediation import DynamoDBRemediation
|
2336
|
+
|
2337
|
+
console.print(f"[blue]🗃️ DynamoDB Bulk Table Encryption[/blue]")
|
2338
|
+
console.print(f"[dim]Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2339
|
+
|
2340
|
+
dynamodb_remediation = DynamoDBRemediation(profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"])
|
2341
|
+
|
2342
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2343
|
+
context = RemediationContext(
|
2344
|
+
account=account,
|
2345
|
+
region=ctx.obj["region"],
|
2346
|
+
operation_type="enable_table_encryption_bulk",
|
2347
|
+
dry_run=ctx.obj["dry_run"],
|
2348
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2349
|
+
)
|
2350
|
+
|
2351
|
+
with console.status("[bold green]Processing DynamoDB tables..."):
|
2352
|
+
results = dynamodb_remediation.enable_table_encryption_bulk(context)
|
2353
|
+
|
2354
|
+
for result in results:
|
2355
|
+
if result.success:
|
2356
|
+
console.print(f"[green]✅ Bulk table encryption completed[/green]")
|
2357
|
+
data = result.response_data
|
2358
|
+
console.print(f"[dim]Encrypted: {len(data.get('successful_tables', []))} tables successfully[/dim]")
|
2359
|
+
else:
|
2360
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2361
|
+
|
2362
|
+
except Exception as e:
|
2363
|
+
console.print(f"[red]❌ Bulk DynamoDB encryption failed: {e}[/red]")
|
2364
|
+
raise click.ClickException(str(e))
|
2365
|
+
|
2366
|
+
|
2367
|
+
@dynamodb.command()
|
2368
|
+
@click.option("--table-names", help="Comma-separated list of table names (analyzes all if not specified)")
|
2369
|
+
@click.pass_context
|
2370
|
+
def analyze_usage(ctx, table_names):
|
2371
|
+
"""Analyze DynamoDB table usage and provide optimization recommendations."""
|
2372
|
+
try:
|
2373
|
+
from runbooks.inventory.models.account import AWSAccount
|
2374
|
+
from runbooks.remediation.base import RemediationContext
|
2375
|
+
from runbooks.remediation.dynamodb_remediation import DynamoDBRemediation
|
2376
|
+
|
2377
|
+
console.print(f"[blue]📊 DynamoDB Usage Analysis[/blue]")
|
2378
|
+
console.print(f"[dim]Tables: {table_names or 'all'} | Region: {ctx.obj['region']}[/dim]")
|
2379
|
+
|
2380
|
+
dynamodb_remediation = DynamoDBRemediation(profile=ctx.obj["profile"], analysis_period_days=7)
|
2381
|
+
|
2382
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2383
|
+
context = RemediationContext(
|
2384
|
+
account=account,
|
2385
|
+
region=ctx.obj["region"],
|
2386
|
+
operation_type="analyze_table_usage",
|
2387
|
+
dry_run=False, # Analysis operation
|
2388
|
+
backup_enabled=False,
|
2389
|
+
)
|
2390
|
+
|
2391
|
+
table_list = table_names.split(",") if table_names else None
|
2392
|
+
|
2393
|
+
with console.status("[bold green]Analyzing DynamoDB tables..."):
|
2394
|
+
results = dynamodb_remediation.analyze_table_usage(context, table_names=table_list)
|
2395
|
+
|
2396
|
+
for result in results:
|
2397
|
+
if result.success:
|
2398
|
+
console.print(f"[green]✅ DynamoDB analysis completed[/green]")
|
2399
|
+
data = result.response_data
|
2400
|
+
analytics = data.get("overall_analytics", {})
|
2401
|
+
console.print(
|
2402
|
+
f"[dim]Tables Analyzed: {analytics.get('total_tables', 0)} | "
|
2403
|
+
f"Encryption Rate: {analytics.get('encryption_compliance_rate', 0):.1f}%[/dim]"
|
2404
|
+
)
|
2405
|
+
else:
|
2406
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2407
|
+
|
2408
|
+
except Exception as e:
|
2409
|
+
console.print(f"[red]❌ DynamoDB analysis failed: {e}[/red]")
|
2410
|
+
raise click.ClickException(str(e))
|
2411
|
+
|
2412
|
+
|
2413
|
+
# ============================================================================
|
2414
|
+
# RDS REMEDIATION COMMANDS
|
2415
|
+
# ============================================================================
|
2416
|
+
|
2417
|
+
|
2418
|
+
@rds.command()
|
2419
|
+
@click.option("--db-instance-identifier", required=True, help="RDS instance identifier")
|
2420
|
+
@click.option("--kms-key-id", help="KMS key ID for encryption (uses default if not specified)")
|
2421
|
+
@click.pass_context
|
2422
|
+
def enable_encryption(ctx, db_instance_identifier, kms_key_id):
|
2423
|
+
"""Enable encryption for an RDS instance (creates encrypted snapshot)."""
|
2424
|
+
try:
|
2425
|
+
from runbooks.inventory.models.account import AWSAccount
|
2426
|
+
from runbooks.remediation.base import RemediationContext
|
2427
|
+
from runbooks.remediation.rds_remediation import RDSSecurityRemediation
|
2428
|
+
|
2429
|
+
console.print(f"[blue]🗄️ RDS Instance Encryption[/blue]")
|
2430
|
+
console.print(
|
2431
|
+
f"[dim]Instance: {db_instance_identifier} | KMS Key: {kms_key_id or 'default'} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
2432
|
+
)
|
2433
|
+
|
2434
|
+
rds_remediation = RDSSecurityRemediation(
|
2435
|
+
profile=ctx.obj["profile"],
|
2436
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2437
|
+
default_kms_key=kms_key_id or "alias/aws/rds",
|
2438
|
+
)
|
2439
|
+
|
2440
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2441
|
+
context = RemediationContext(
|
2442
|
+
account=account,
|
2443
|
+
region=ctx.obj["region"],
|
2444
|
+
operation_type="enable_instance_encryption",
|
2445
|
+
dry_run=ctx.obj["dry_run"],
|
2446
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2447
|
+
)
|
2448
|
+
|
2449
|
+
results = rds_remediation.enable_instance_encryption(context, db_instance_identifier, kms_key_id)
|
2450
|
+
|
2451
|
+
for result in results:
|
2452
|
+
if result.success:
|
2453
|
+
console.print(
|
2454
|
+
f"[green]✅ Successfully enabled encryption for instance: {db_instance_identifier}[/green]"
|
2455
|
+
)
|
2456
|
+
data = result.response_data
|
2457
|
+
console.print(f"[dim]Snapshot: {data.get('snapshot_identifier', 'N/A')}[/dim]")
|
2458
|
+
elif result.status.value == "skipped":
|
2459
|
+
console.print(f"[yellow]⚠️ Encryption already enabled for instance: {db_instance_identifier}[/yellow]")
|
2460
|
+
else:
|
2461
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2462
|
+
|
2463
|
+
except Exception as e:
|
2464
|
+
console.print(f"[red]❌ RDS encryption failed: {e}[/red]")
|
2465
|
+
raise click.ClickException(str(e))
|
2466
|
+
|
2467
|
+
|
2468
|
+
@rds.command()
|
2469
|
+
@click.pass_context
|
2470
|
+
def enable_encryption_bulk(ctx):
|
2471
|
+
"""Enable encryption for all unencrypted RDS instances in bulk."""
|
2472
|
+
try:
|
2473
|
+
from runbooks.inventory.models.account import AWSAccount
|
2474
|
+
from runbooks.remediation.base import RemediationContext
|
2475
|
+
from runbooks.remediation.rds_remediation import RDSSecurityRemediation
|
2476
|
+
|
2477
|
+
console.print(f"[blue]🗄️ RDS Bulk Instance Encryption[/blue]")
|
2478
|
+
console.print(f"[dim]Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2479
|
+
|
2480
|
+
rds_remediation = RDSSecurityRemediation(profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"])
|
2481
|
+
|
2482
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2483
|
+
context = RemediationContext(
|
2484
|
+
account=account,
|
2485
|
+
region=ctx.obj["region"],
|
2486
|
+
operation_type="enable_instance_encryption_bulk",
|
2487
|
+
dry_run=ctx.obj["dry_run"],
|
2488
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2489
|
+
)
|
2490
|
+
|
2491
|
+
with console.status("[bold green]Processing RDS instances..."):
|
2492
|
+
results = rds_remediation.enable_instance_encryption_bulk(context)
|
2493
|
+
|
2494
|
+
for result in results:
|
2495
|
+
if result.success:
|
2496
|
+
console.print(f"[green]✅ Bulk instance encryption completed[/green]")
|
2497
|
+
data = result.response_data
|
2498
|
+
console.print(
|
2499
|
+
f"[dim]Snapshots Created: {len(data.get('successful_snapshots', []))} | "
|
2500
|
+
+ f"Success Rate: {data.get('success_rate', 0):.1%}[/dim]"
|
2501
|
+
)
|
2502
|
+
else:
|
2503
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2504
|
+
|
2505
|
+
except Exception as e:
|
2506
|
+
console.print(f"[red]❌ Bulk RDS encryption failed: {e}[/red]")
|
2507
|
+
raise click.ClickException(str(e))
|
2508
|
+
|
2509
|
+
|
2510
|
+
@rds.command()
|
2511
|
+
@click.option("--db-instance-identifier", help="Specific instance identifier (configures all if not specified)")
|
2512
|
+
@click.option("--retention-days", type=int, default=30, help="Backup retention period in days")
|
2513
|
+
@click.pass_context
|
2514
|
+
def configure_backups(ctx, db_instance_identifier, retention_days):
|
2515
|
+
"""Configure backup settings for RDS instances."""
|
2516
|
+
try:
|
2517
|
+
from runbooks.inventory.models.account import AWSAccount
|
2518
|
+
from runbooks.remediation.base import RemediationContext
|
2519
|
+
from runbooks.remediation.rds_remediation import RDSSecurityRemediation
|
2520
|
+
|
2521
|
+
console.print(f"[blue]💾 RDS Backup Configuration[/blue]")
|
2522
|
+
console.print(
|
2523
|
+
f"[dim]Instance: {db_instance_identifier or 'all'} | Retention: {retention_days} days | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
2524
|
+
)
|
2525
|
+
|
2526
|
+
rds_remediation = RDSSecurityRemediation(
|
2527
|
+
profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"], backup_retention_days=retention_days
|
2528
|
+
)
|
2529
|
+
|
2530
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2531
|
+
context = RemediationContext(
|
2532
|
+
account=account,
|
2533
|
+
region=ctx.obj["region"],
|
2534
|
+
operation_type="configure_backup_settings",
|
2535
|
+
dry_run=ctx.obj["dry_run"],
|
2536
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2537
|
+
)
|
2538
|
+
|
2539
|
+
results = rds_remediation.configure_backup_settings(context, db_instance_identifier)
|
2540
|
+
|
2541
|
+
for result in results:
|
2542
|
+
if result.success:
|
2543
|
+
console.print(f"[green]✅ Successfully configured backup settings[/green]")
|
2544
|
+
data = result.response_data
|
2545
|
+
console.print(f"[dim]Configured: {len(data.get('successful_configurations', []))} instances[/dim]")
|
2546
|
+
else:
|
2547
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2548
|
+
|
2549
|
+
except Exception as e:
|
2550
|
+
console.print(f"[red]❌ RDS backup configuration failed: {e}[/red]")
|
2551
|
+
raise click.ClickException(str(e))
|
2552
|
+
|
2553
|
+
|
2554
|
+
@rds.command()
|
2555
|
+
@click.pass_context
|
2556
|
+
def analyze_usage(ctx):
|
2557
|
+
"""Analyze RDS instance usage and provide optimization recommendations."""
|
2558
|
+
try:
|
2559
|
+
from runbooks.inventory.models.account import AWSAccount
|
2560
|
+
from runbooks.remediation.base import RemediationContext
|
2561
|
+
from runbooks.remediation.rds_remediation import RDSSecurityRemediation
|
2562
|
+
|
2563
|
+
console.print(f"[blue]📊 RDS Usage Analysis[/blue]")
|
2564
|
+
console.print(f"[dim]Region: {ctx.obj['region']}[/dim]")
|
2565
|
+
|
2566
|
+
rds_remediation = RDSSecurityRemediation(profile=ctx.obj["profile"], analysis_period_days=7)
|
2567
|
+
|
2568
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2569
|
+
context = RemediationContext(
|
2570
|
+
account=account,
|
2571
|
+
region=ctx.obj["region"],
|
2572
|
+
operation_type="analyze_instance_usage",
|
2573
|
+
dry_run=False, # Analysis operation
|
2574
|
+
backup_enabled=False,
|
2575
|
+
)
|
2576
|
+
|
2577
|
+
with console.status("[bold green]Analyzing RDS instances..."):
|
2578
|
+
results = rds_remediation.analyze_instance_usage(context)
|
2579
|
+
|
2580
|
+
for result in results:
|
2581
|
+
if result.success:
|
2582
|
+
console.print(f"[green]✅ RDS analysis completed[/green]")
|
2583
|
+
data = result.response_data
|
2584
|
+
analytics = data.get("overall_analytics", {})
|
2585
|
+
console.print(
|
2586
|
+
f"[dim]Instances Analyzed: {analytics.get('total_instances', 0)} | "
|
2587
|
+
+ f"Encryption Rate: {analytics.get('encryption_compliance_rate', 0):.1f}% | "
|
2588
|
+
+ f"Avg CPU: {analytics.get('avg_cpu_utilization', 0):.1f}%[/dim]"
|
2589
|
+
)
|
2590
|
+
else:
|
2591
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2592
|
+
|
2593
|
+
except Exception as e:
|
2594
|
+
console.print(f"[red]❌ RDS analysis failed: {e}[/red]")
|
2595
|
+
raise click.ClickException(str(e))
|
2596
|
+
|
2597
|
+
|
2598
|
+
# ============================================================================
|
2599
|
+
# LAMBDA REMEDIATION COMMANDS
|
2600
|
+
# ============================================================================
|
2601
|
+
|
2602
|
+
|
2603
|
+
@lambda_func.command()
|
2604
|
+
@click.option("--function-name", required=True, help="Lambda function name")
|
2605
|
+
@click.option("--kms-key-id", help="KMS key ID for encryption (uses default if not specified)")
|
2606
|
+
@click.pass_context
|
2607
|
+
def encrypt_environment(ctx, function_name, kms_key_id):
|
2608
|
+
"""Enable encryption for Lambda function environment variables."""
|
2609
|
+
try:
|
2610
|
+
from runbooks.inventory.models.account import AWSAccount
|
2611
|
+
from runbooks.remediation.base import RemediationContext
|
2612
|
+
from runbooks.remediation.lambda_remediation import LambdaSecurityRemediation
|
2613
|
+
|
2614
|
+
console.print(f"[blue]🔄 Lambda Environment Encryption[/blue]")
|
2615
|
+
console.print(
|
2616
|
+
f"[dim]Function: {function_name} | KMS Key: {kms_key_id or 'default'} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
2617
|
+
)
|
2618
|
+
|
2619
|
+
lambda_remediation = LambdaSecurityRemediation(
|
2620
|
+
profile=ctx.obj["profile"],
|
2621
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2622
|
+
default_kms_key=kms_key_id or "alias/aws/lambda",
|
2623
|
+
)
|
2624
|
+
|
2625
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2626
|
+
context = RemediationContext(
|
2627
|
+
account=account,
|
2628
|
+
region=ctx.obj["region"],
|
2629
|
+
operation_type="encrypt_environment_variables",
|
2630
|
+
dry_run=ctx.obj["dry_run"],
|
2631
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2632
|
+
)
|
2633
|
+
|
2634
|
+
results = lambda_remediation.encrypt_environment_variables(context, function_name, kms_key_id)
|
2635
|
+
|
2636
|
+
for result in results:
|
2637
|
+
if result.success:
|
2638
|
+
console.print(f"[green]✅ Successfully enabled environment encryption for: {function_name}[/green]")
|
2639
|
+
data = result.response_data
|
2640
|
+
console.print(f"[dim]Variables: {data.get('variables_count', 0)}[/dim]")
|
2641
|
+
elif result.status.value == "skipped":
|
2642
|
+
console.print(
|
2643
|
+
f"[yellow]⚠️ Environment encryption already enabled or no variables: {function_name}[/yellow]"
|
2644
|
+
)
|
2645
|
+
else:
|
2646
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2647
|
+
|
2648
|
+
except Exception as e:
|
2649
|
+
console.print(f"[red]❌ Lambda environment encryption failed: {e}[/red]")
|
2650
|
+
raise click.ClickException(str(e))
|
2651
|
+
|
2652
|
+
|
2653
|
+
@lambda_func.command()
|
2654
|
+
@click.pass_context
|
2655
|
+
def encrypt_environment_bulk(ctx):
|
2656
|
+
"""Enable environment variable encryption for all Lambda functions in bulk."""
|
2657
|
+
try:
|
2658
|
+
from runbooks.inventory.models.account import AWSAccount
|
2659
|
+
from runbooks.remediation.base import RemediationContext
|
2660
|
+
from runbooks.remediation.lambda_remediation import LambdaSecurityRemediation
|
2661
|
+
|
2662
|
+
console.print(f"[blue]🔄 Lambda Bulk Environment Encryption[/blue]")
|
2663
|
+
console.print(f"[dim]Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2664
|
+
|
2665
|
+
lambda_remediation = LambdaSecurityRemediation(
|
2666
|
+
profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"]
|
2667
|
+
)
|
2668
|
+
|
2669
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2670
|
+
context = RemediationContext(
|
2671
|
+
account=account,
|
2672
|
+
region=ctx.obj["region"],
|
2673
|
+
operation_type="encrypt_environment_variables_bulk",
|
2674
|
+
dry_run=ctx.obj["dry_run"],
|
2675
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2676
|
+
)
|
2677
|
+
|
2678
|
+
with console.status("[bold green]Processing Lambda functions..."):
|
2679
|
+
results = lambda_remediation.encrypt_environment_variables_bulk(context)
|
2680
|
+
|
2681
|
+
for result in results:
|
2682
|
+
if result.success:
|
2683
|
+
console.print(f"[green]✅ Bulk environment encryption completed[/green]")
|
2684
|
+
data = result.response_data
|
2685
|
+
console.print(
|
2686
|
+
f"[dim]Encrypted: {len(data.get('successful_functions', []))} functions | "
|
2687
|
+
+ f"Success Rate: {data.get('success_rate', 0):.1%}[/dim]"
|
2688
|
+
)
|
2689
|
+
else:
|
2690
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2691
|
+
|
2692
|
+
except Exception as e:
|
2693
|
+
console.print(f"[red]❌ Bulk Lambda encryption failed: {e}[/red]")
|
2694
|
+
raise click.ClickException(str(e))
|
2695
|
+
|
2696
|
+
|
2697
|
+
@lambda_func.command()
|
2698
|
+
@click.pass_context
|
2699
|
+
def optimize_iam_policies(ctx):
|
2700
|
+
"""Optimize IAM policies for all Lambda functions to follow least privilege."""
|
2701
|
+
try:
|
2702
|
+
from runbooks.inventory.models.account import AWSAccount
|
2703
|
+
from runbooks.remediation.base import RemediationContext
|
2704
|
+
from runbooks.remediation.lambda_remediation import LambdaSecurityRemediation
|
2705
|
+
|
2706
|
+
console.print(f"[blue]🔐 Lambda IAM Policy Optimization[/blue]")
|
2707
|
+
console.print(f"[dim]Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2708
|
+
|
2709
|
+
lambda_remediation = LambdaSecurityRemediation(
|
2710
|
+
profile=ctx.obj["profile"], backup_enabled=ctx.obj["backup_enabled"]
|
2711
|
+
)
|
2712
|
+
|
2713
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2714
|
+
context = RemediationContext(
|
2715
|
+
account=account,
|
2716
|
+
region=ctx.obj["region"],
|
2717
|
+
operation_type="optimize_iam_policies_bulk",
|
2718
|
+
dry_run=ctx.obj["dry_run"],
|
2719
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2720
|
+
)
|
2721
|
+
|
2722
|
+
with console.status("[bold green]Optimizing IAM policies..."):
|
2723
|
+
results = lambda_remediation.optimize_iam_policies_bulk(context)
|
2724
|
+
|
2725
|
+
for result in results:
|
2726
|
+
if result.success:
|
2727
|
+
console.print(f"[green]✅ IAM policy optimization completed[/green]")
|
2728
|
+
data = result.response_data
|
2729
|
+
console.print(
|
2730
|
+
f"[dim]Optimized: {len(data.get('successful_optimizations', []))} functions | "
|
2731
|
+
+ f"Rate: {data.get('optimization_rate', 0):.1%}[/dim]"
|
2732
|
+
)
|
2733
|
+
else:
|
2734
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2735
|
+
|
2736
|
+
except Exception as e:
|
2737
|
+
console.print(f"[red]❌ Lambda IAM optimization failed: {e}[/red]")
|
2738
|
+
raise click.ClickException(str(e))
|
2739
|
+
|
2740
|
+
|
2741
|
+
@lambda_func.command()
|
2742
|
+
@click.pass_context
|
2743
|
+
def analyze_usage(ctx):
|
2744
|
+
"""Analyze Lambda function usage and provide optimization recommendations."""
|
2745
|
+
try:
|
2746
|
+
from runbooks.inventory.models.account import AWSAccount
|
2747
|
+
from runbooks.remediation.base import RemediationContext
|
2748
|
+
from runbooks.remediation.lambda_remediation import LambdaSecurityRemediation
|
2749
|
+
|
2750
|
+
console.print(f"[blue]📊 Lambda Usage Analysis[/blue]")
|
2751
|
+
console.print(f"[dim]Region: {ctx.obj['region']}[/dim]")
|
2752
|
+
|
2753
|
+
lambda_remediation = LambdaSecurityRemediation(profile=ctx.obj["profile"], analysis_period_days=30)
|
2754
|
+
|
2755
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2756
|
+
context = RemediationContext(
|
2757
|
+
account=account,
|
2758
|
+
region=ctx.obj["region"],
|
2759
|
+
operation_type="analyze_function_usage",
|
2760
|
+
dry_run=False, # Analysis operation
|
2761
|
+
backup_enabled=False,
|
2762
|
+
)
|
2763
|
+
|
2764
|
+
with console.status("[bold green]Analyzing Lambda functions..."):
|
2765
|
+
results = lambda_remediation.analyze_function_usage(context)
|
2766
|
+
|
2767
|
+
for result in results:
|
2768
|
+
if result.success:
|
2769
|
+
console.print(f"[green]✅ Lambda analysis completed[/green]")
|
2770
|
+
data = result.response_data
|
2771
|
+
analytics = data.get("overall_analytics", {})
|
2772
|
+
console.print(
|
2773
|
+
f"[dim]Functions Analyzed: {analytics.get('total_functions', 0)} | "
|
2774
|
+
+ f"Encryption Rate: {analytics.get('encryption_compliance_rate', 0):.1f}% | "
|
2775
|
+
+ f"VPC Rate: {analytics.get('vpc_adoption_rate', 0):.1f}%[/dim]"
|
2776
|
+
)
|
2777
|
+
else:
|
2778
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2779
|
+
|
2780
|
+
except Exception as e:
|
2781
|
+
console.print(f"[red]❌ Lambda analysis failed: {e}[/red]")
|
2782
|
+
raise click.ClickException(str(e))
|
2783
|
+
|
2784
|
+
|
2785
|
+
# ============================================================================
|
2786
|
+
# ACM CERTIFICATE REMEDIATION COMMANDS
|
2787
|
+
# ============================================================================
|
2788
|
+
|
2789
|
+
|
2790
|
+
@acm.command()
|
2791
|
+
@click.option("--confirm", is_flag=True, help="Confirm destructive operation")
|
2792
|
+
@click.option("--verify-usage", is_flag=True, default=True, help="Verify certificate usage before deletion")
|
2793
|
+
@click.pass_context
|
2794
|
+
def cleanup_expired_certificates(ctx, confirm, verify_usage):
|
2795
|
+
"""
|
2796
|
+
Clean up expired ACM certificates.
|
2797
|
+
|
2798
|
+
⚠️ WARNING: This operation deletes certificates and can cause service outages!
|
2799
|
+
"""
|
2800
|
+
try:
|
2801
|
+
from runbooks.inventory.models.account import AWSAccount
|
2802
|
+
from runbooks.remediation.acm_remediation import ACMRemediation
|
2803
|
+
from runbooks.remediation.base import RemediationContext
|
2804
|
+
|
2805
|
+
console.print(f"[blue]🏅 Cleaning Up Expired ACM Certificates[/blue]")
|
2806
|
+
console.print(
|
2807
|
+
f"[dim]Region: {ctx.obj['region']} | Verify Usage: {verify_usage} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
2808
|
+
)
|
2809
|
+
|
2810
|
+
acm_remediation = ACMRemediation(
|
2811
|
+
profile=ctx.obj["profile"],
|
2812
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2813
|
+
usage_verification=verify_usage,
|
2814
|
+
require_confirmation=True,
|
2815
|
+
)
|
2816
|
+
|
2817
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2818
|
+
context = RemediationContext(
|
2819
|
+
account=account,
|
2820
|
+
region=ctx.obj["region"],
|
2821
|
+
operation_type="cleanup_expired_certificates",
|
2822
|
+
dry_run=ctx.obj["dry_run"],
|
2823
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2824
|
+
)
|
2825
|
+
|
2826
|
+
with console.status("[bold red]Cleaning up expired certificates..."):
|
2827
|
+
results = acm_remediation.cleanup_expired_certificates(
|
2828
|
+
context, force_delete=confirm, verify_usage=verify_usage
|
2829
|
+
)
|
2830
|
+
|
2831
|
+
for result in results:
|
2832
|
+
if result.success:
|
2833
|
+
console.print(f"[green]✅ Certificate cleanup completed[/green]")
|
2834
|
+
data = result.response_data
|
2835
|
+
deleted_count = data.get("total_deleted", 0)
|
2836
|
+
console.print(f"[green] 🗑️ Deleted: {deleted_count} expired certificates[/green]")
|
2837
|
+
else:
|
2838
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2839
|
+
|
2840
|
+
except Exception as e:
|
2841
|
+
console.print(f"[red]❌ Certificate cleanup failed: {e}[/red]")
|
2842
|
+
raise click.ClickException(str(e))
|
2843
|
+
|
2844
|
+
|
2845
|
+
@acm.command()
|
2846
|
+
@click.pass_context
|
2847
|
+
def analyze_certificate_usage(ctx):
|
2848
|
+
"""Analyze ACM certificate usage and security."""
|
2849
|
+
try:
|
2850
|
+
from runbooks.inventory.models.account import AWSAccount
|
2851
|
+
from runbooks.remediation.acm_remediation import ACMRemediation
|
2852
|
+
from runbooks.remediation.base import RemediationContext
|
2853
|
+
|
2854
|
+
console.print(f"[blue]🏅 ACM Certificate Analysis[/blue]")
|
2855
|
+
console.print(f"[dim]Region: {ctx.obj['region']}[/dim]")
|
2856
|
+
|
2857
|
+
acm_remediation = ACMRemediation(profile=ctx.obj["profile"], usage_verification=True)
|
2858
|
+
|
2859
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2860
|
+
context = RemediationContext(
|
2861
|
+
account=account,
|
2862
|
+
region=ctx.obj["region"],
|
2863
|
+
operation_type="analyze_certificate_usage",
|
2864
|
+
dry_run=False, # Analysis operation
|
2865
|
+
backup_enabled=False,
|
2866
|
+
)
|
2867
|
+
|
2868
|
+
with console.status("[bold green]Analyzing certificates..."):
|
2869
|
+
results = acm_remediation.analyze_certificate_usage(context)
|
2870
|
+
|
2871
|
+
for result in results:
|
2872
|
+
if result.success:
|
2873
|
+
console.print(f"[green]✅ Certificate analysis completed[/green]")
|
2874
|
+
data = result.response_data
|
2875
|
+
analytics = data.get("overall_analytics", {})
|
2876
|
+
console.print(
|
2877
|
+
f"[dim]Certificates: {analytics.get('total_certificates', 0)} | "
|
2878
|
+
+ f"Expired: {analytics.get('expired_certificates', 0)} | "
|
2879
|
+
+ f"Expiring Soon: {analytics.get('expiring_within_30_days', 0)}[/dim]"
|
2880
|
+
)
|
2881
|
+
else:
|
2882
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2883
|
+
|
2884
|
+
except Exception as e:
|
2885
|
+
console.print(f"[red]❌ Certificate analysis failed: {e}[/red]")
|
2886
|
+
raise click.ClickException(str(e))
|
2887
|
+
|
2888
|
+
|
2889
|
+
# ============================================================================
|
2890
|
+
# COGNITO USER REMEDIATION COMMANDS
|
2891
|
+
# ============================================================================
|
2892
|
+
|
2893
|
+
|
2894
|
+
@cognito.command()
|
2895
|
+
@click.option("--user-pool-id", required=True, help="Cognito User Pool ID")
|
2896
|
+
@click.option("--username", required=True, help="Username to reset password for")
|
2897
|
+
@click.option("--new-password", help="New password (will be prompted if not provided)")
|
2898
|
+
@click.option("--permanent", is_flag=True, default=True, help="Set password as permanent")
|
2899
|
+
@click.option("--add-to-group", default="ReadHistorical", help="Group to add user to")
|
2900
|
+
@click.option("--confirm", is_flag=True, help="Confirm destructive operation")
|
2901
|
+
@click.pass_context
|
2902
|
+
def reset_user_password(ctx, user_pool_id, username, new_password, permanent, add_to_group, confirm):
|
2903
|
+
"""
|
2904
|
+
Reset user password in Cognito User Pool.
|
2905
|
+
|
2906
|
+
⚠️ WARNING: This operation can lock users out of applications!
|
2907
|
+
"""
|
2908
|
+
try:
|
2909
|
+
from runbooks.inventory.models.account import AWSAccount
|
2910
|
+
from runbooks.remediation.base import RemediationContext
|
2911
|
+
from runbooks.remediation.cognito_remediation import CognitoRemediation
|
2912
|
+
|
2913
|
+
console.print(f"[blue]👤 Resetting Cognito User Password[/blue]")
|
2914
|
+
console.print(f"[dim]User Pool: {user_pool_id} | Username: {username} | Dry-run: {ctx.obj['dry_run']}[/dim]")
|
2915
|
+
|
2916
|
+
cognito_remediation = CognitoRemediation(
|
2917
|
+
profile=ctx.obj["profile"],
|
2918
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2919
|
+
impact_verification=True,
|
2920
|
+
require_confirmation=True,
|
2921
|
+
)
|
2922
|
+
|
2923
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2924
|
+
context = RemediationContext(
|
2925
|
+
account=account,
|
2926
|
+
region=ctx.obj["region"],
|
2927
|
+
operation_type="reset_user_password",
|
2928
|
+
dry_run=ctx.obj["dry_run"],
|
2929
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
2930
|
+
)
|
2931
|
+
|
2932
|
+
with console.status("[bold red]Resetting user password..."):
|
2933
|
+
results = cognito_remediation.reset_user_password(
|
2934
|
+
context,
|
2935
|
+
user_pool_id=user_pool_id,
|
2936
|
+
username=username,
|
2937
|
+
new_password=new_password,
|
2938
|
+
permanent=permanent,
|
2939
|
+
add_to_group=add_to_group,
|
2940
|
+
force_reset=confirm,
|
2941
|
+
)
|
2942
|
+
|
2943
|
+
for result in results:
|
2944
|
+
if result.success:
|
2945
|
+
console.print(f"[green]✅ Password reset completed[/green]")
|
2946
|
+
data = result.response_data
|
2947
|
+
console.print(f"[green] 👤 User: {username}[/green]")
|
2948
|
+
console.print(f"[green] 🔐 Permanent: {data.get('permanent', permanent)}[/green]")
|
2949
|
+
if data.get("group_assignment"):
|
2950
|
+
console.print(f"[green] 👥 Group: {data.get('group_assignment')}[/green]")
|
2951
|
+
else:
|
2952
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2953
|
+
|
2954
|
+
except Exception as e:
|
2955
|
+
console.print(f"[red]❌ Password reset failed: {e}[/red]")
|
2956
|
+
raise click.ClickException(str(e))
|
2957
|
+
|
2958
|
+
|
2959
|
+
@cognito.command()
|
2960
|
+
@click.option("--user-pool-id", required=True, help="Cognito User Pool ID")
|
2961
|
+
@click.pass_context
|
2962
|
+
def analyze_user_security(ctx, user_pool_id):
|
2963
|
+
"""Analyze Cognito user security and compliance."""
|
2964
|
+
try:
|
2965
|
+
from runbooks.inventory.models.account import AWSAccount
|
2966
|
+
from runbooks.remediation.base import RemediationContext
|
2967
|
+
from runbooks.remediation.cognito_remediation import CognitoRemediation
|
2968
|
+
|
2969
|
+
console.print(f"[blue]👤 Cognito User Security Analysis[/blue]")
|
2970
|
+
console.print(f"[dim]User Pool: {user_pool_id} | Region: {ctx.obj['region']}[/dim]")
|
2971
|
+
|
2972
|
+
cognito_remediation = CognitoRemediation(profile=ctx.obj["profile"], impact_verification=True)
|
2973
|
+
|
2974
|
+
account = AWSAccount(account_id="current", account_name="current")
|
2975
|
+
context = RemediationContext(
|
2976
|
+
account=account,
|
2977
|
+
region=ctx.obj["region"],
|
2978
|
+
operation_type="analyze_user_security",
|
2979
|
+
dry_run=False, # Analysis operation
|
2980
|
+
backup_enabled=False,
|
2981
|
+
)
|
2982
|
+
|
2983
|
+
with console.status("[bold green]Analyzing user security..."):
|
2984
|
+
results = cognito_remediation.analyze_user_security(context, user_pool_id=user_pool_id)
|
2985
|
+
|
2986
|
+
for result in results:
|
2987
|
+
if result.success:
|
2988
|
+
console.print(f"[green]✅ User security analysis completed[/green]")
|
2989
|
+
data = result.response_data
|
2990
|
+
analytics = data.get("security_analytics", {})
|
2991
|
+
console.print(
|
2992
|
+
f"[dim]Users: {analytics.get('total_users', 0)} | "
|
2993
|
+
+ f"MFA Rate: {analytics.get('mfa_compliance_rate', 0):.1f}% | "
|
2994
|
+
+ f"Issues: {analytics.get('users_with_security_issues', 0)}[/dim]"
|
2995
|
+
)
|
2996
|
+
else:
|
2997
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
2998
|
+
|
2999
|
+
except Exception as e:
|
3000
|
+
console.print(f"[red]❌ User security analysis failed: {e}[/red]")
|
3001
|
+
raise click.ClickException(str(e))
|
3002
|
+
|
3003
|
+
|
3004
|
+
# ============================================================================
|
3005
|
+
# CLOUDTRAIL POLICY REMEDIATION COMMANDS
|
3006
|
+
# ============================================================================
|
3007
|
+
|
3008
|
+
|
3009
|
+
@cloudtrail.command()
|
3010
|
+
@click.option("--user-email", required=True, help="Email of user to analyze policy changes for")
|
3011
|
+
@click.option("--days", type=int, default=7, help="Number of days to look back")
|
3012
|
+
@click.pass_context
|
3013
|
+
def analyze_s3_policy_changes(ctx, user_email, days):
|
3014
|
+
"""Analyze S3 policy changes made by specific users via CloudTrail."""
|
3015
|
+
try:
|
3016
|
+
from datetime import datetime, timedelta, timezone
|
3017
|
+
|
3018
|
+
from runbooks.inventory.models.account import AWSAccount
|
3019
|
+
from runbooks.remediation.base import RemediationContext
|
3020
|
+
from runbooks.remediation.cloudtrail_remediation import CloudTrailRemediation
|
3021
|
+
|
3022
|
+
console.print(f"[blue]🕵️ Analyzing S3 Policy Changes[/blue]")
|
3023
|
+
console.print(f"[dim]User: {user_email} | Days: {days} | Region: {ctx.obj['region']}[/dim]")
|
3024
|
+
|
3025
|
+
cloudtrail_remediation = CloudTrailRemediation(
|
3026
|
+
profile=ctx.obj["profile"], impact_verification=True, default_lookback_days=days
|
3027
|
+
)
|
3028
|
+
|
3029
|
+
account = AWSAccount(account_id="current", account_name="current")
|
3030
|
+
context = RemediationContext(
|
3031
|
+
account=account,
|
3032
|
+
region=ctx.obj["region"],
|
3033
|
+
operation_type="analyze_s3_policy_changes",
|
3034
|
+
dry_run=False, # Analysis operation
|
3035
|
+
backup_enabled=False,
|
3036
|
+
)
|
3037
|
+
|
3038
|
+
# Set time range
|
3039
|
+
end_time = datetime.now(tz=timezone.utc)
|
3040
|
+
start_time = end_time - timedelta(days=days)
|
3041
|
+
|
3042
|
+
with console.status("[bold green]Analyzing CloudTrail events..."):
|
3043
|
+
results = cloudtrail_remediation.analyze_s3_policy_changes(
|
3044
|
+
context, user_email=user_email, start_time=start_time, end_time=end_time
|
3045
|
+
)
|
3046
|
+
|
3047
|
+
for result in results:
|
3048
|
+
if result.success:
|
3049
|
+
console.print(f"[green]✅ Policy analysis completed[/green]")
|
3050
|
+
data = result.response_data
|
3051
|
+
assessment = data.get("security_assessment", {})
|
3052
|
+
console.print(
|
3053
|
+
f"[dim]Changes: {assessment.get('total_modifications', 0)} | "
|
3054
|
+
+ f"High Risk: {assessment.get('high_risk_changes', 0)} | "
|
3055
|
+
+ f"Period: {days} days[/dim]"
|
3056
|
+
)
|
3057
|
+
|
3058
|
+
# Show high-risk changes if any
|
3059
|
+
high_risk_changes = data.get("high_risk_changes", [])
|
3060
|
+
if high_risk_changes:
|
3061
|
+
console.print(f"\n[red]⚠️ High-Risk Policy Changes Detected:[/red]")
|
3062
|
+
for change in high_risk_changes[:5]: # Show first 5
|
3063
|
+
bucket = change.get("BucketName", "unknown")
|
3064
|
+
impact = change.get("impact_analysis", {})
|
3065
|
+
security_changes = impact.get("security_changes", [])
|
3066
|
+
console.print(f"[red] 📦 {bucket}: {', '.join(security_changes)}[/red]")
|
3067
|
+
else:
|
3068
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
3069
|
+
|
3070
|
+
except Exception as e:
|
3071
|
+
console.print(f"[red]❌ Policy analysis failed: {e}[/red]")
|
3072
|
+
raise click.ClickException(str(e))
|
3073
|
+
|
3074
|
+
|
3075
|
+
@cloudtrail.command()
|
3076
|
+
@click.option("--bucket-name", required=True, help="S3 bucket name to revert policy for")
|
3077
|
+
@click.option("--target-policy-file", type=click.Path(exists=True), help="JSON file with target policy")
|
3078
|
+
@click.option("--remove-policy", is_flag=True, help="Remove bucket policy entirely")
|
3079
|
+
@click.option("--confirm", is_flag=True, help="Confirm destructive operation")
|
3080
|
+
@click.pass_context
|
3081
|
+
def revert_s3_policy_changes(ctx, bucket_name, target_policy_file, remove_policy, confirm):
|
3082
|
+
"""
|
3083
|
+
Revert S3 bucket policy changes.
|
3084
|
+
|
3085
|
+
⚠️ WARNING: This operation can expose data or break application access!
|
3086
|
+
"""
|
3087
|
+
try:
|
3088
|
+
import json
|
3089
|
+
|
3090
|
+
from runbooks.inventory.models.account import AWSAccount
|
3091
|
+
from runbooks.remediation.base import RemediationContext
|
3092
|
+
from runbooks.remediation.cloudtrail_remediation import CloudTrailRemediation
|
3093
|
+
|
3094
|
+
console.print(f"[blue]🕵️ Reverting S3 Policy Changes[/blue]")
|
3095
|
+
console.print(
|
3096
|
+
f"[dim]Bucket: {bucket_name} | Remove Policy: {remove_policy} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
3097
|
+
)
|
3098
|
+
|
3099
|
+
target_policy = None
|
3100
|
+
if target_policy_file and not remove_policy:
|
3101
|
+
with open(target_policy_file, "r") as f:
|
3102
|
+
target_policy = json.load(f)
|
3103
|
+
|
3104
|
+
cloudtrail_remediation = CloudTrailRemediation(
|
3105
|
+
profile=ctx.obj["profile"],
|
3106
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
3107
|
+
impact_verification=True,
|
3108
|
+
require_confirmation=True,
|
3109
|
+
)
|
3110
|
+
|
3111
|
+
account = AWSAccount(account_id="current", account_name="current")
|
3112
|
+
context = RemediationContext(
|
3113
|
+
account=account,
|
3114
|
+
region=ctx.obj["region"],
|
3115
|
+
operation_type="revert_s3_policy_changes",
|
3116
|
+
dry_run=ctx.obj["dry_run"],
|
3117
|
+
backup_enabled=ctx.obj["backup_enabled"],
|
3118
|
+
)
|
3119
|
+
|
3120
|
+
with console.status("[bold red]Reverting policy changes..."):
|
3121
|
+
results = cloudtrail_remediation.revert_s3_policy_changes(
|
3122
|
+
context, bucket_name=bucket_name, target_policy=target_policy, force_revert=confirm
|
3123
|
+
)
|
3124
|
+
|
3125
|
+
for result in results:
|
3126
|
+
if result.success:
|
3127
|
+
console.print(f"[green]✅ Policy reversion completed[/green]")
|
3128
|
+
data = result.response_data
|
3129
|
+
action = data.get("action_taken", "unknown")
|
3130
|
+
console.print(f"[green] 📦 Bucket: {bucket_name}[/green]")
|
3131
|
+
console.print(f"[green] 🔄 Action: {action}[/green]")
|
3132
|
+
else:
|
3133
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
3134
|
+
|
3135
|
+
except Exception as e:
|
3136
|
+
console.print(f"[red]❌ Policy reversion failed: {e}[/red]")
|
3137
|
+
raise click.ClickException(str(e))
|
3138
|
+
|
3139
|
+
|
3140
|
+
# ============================================================================
|
3141
|
+
# AUTO-FIX COMMAND
|
3142
|
+
# ============================================================================
|
3143
|
+
|
3144
|
+
|
3145
|
+
@remediation.command()
|
3146
|
+
@click.option("--findings-file", required=True, type=click.Path(exists=True), help="Security findings JSON file")
|
758
3147
|
@click.option(
|
759
|
-
"--
|
760
|
-
|
761
|
-
|
3148
|
+
"--severity",
|
3149
|
+
type=click.Choice(["critical", "high", "medium", "low"]),
|
3150
|
+
default="high",
|
3151
|
+
help="Minimum severity to remediate",
|
762
3152
|
)
|
3153
|
+
@click.option("--max-operations", type=int, default=50, help="Maximum operations to execute")
|
3154
|
+
@click.pass_context
|
3155
|
+
def auto_fix(ctx, findings_file, severity, max_operations):
|
3156
|
+
"""Automatically remediate security findings from assessment results."""
|
3157
|
+
try:
|
3158
|
+
import json
|
3159
|
+
|
3160
|
+
console.print(f"[blue]🤖 Auto-Remediation from Security Findings[/blue]")
|
3161
|
+
console.print(f"[dim]File: {findings_file} | Min Severity: {severity}[/dim]")
|
3162
|
+
|
3163
|
+
# Load findings
|
3164
|
+
with open(findings_file, "r") as f:
|
3165
|
+
findings = json.load(f)
|
3166
|
+
|
3167
|
+
# Filter by severity
|
3168
|
+
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
3169
|
+
min_severity = severity_order[severity]
|
3170
|
+
|
3171
|
+
filtered_findings = [f for f in findings if severity_order.get(f.get("severity", "low"), 3) <= min_severity][
|
3172
|
+
:max_operations
|
3173
|
+
]
|
3174
|
+
|
3175
|
+
console.print(f"[yellow]📋 Found {len(filtered_findings)} findings to remediate[/yellow]")
|
3176
|
+
|
3177
|
+
if not filtered_findings:
|
3178
|
+
console.print("[green]✅ No findings requiring remediation[/green]")
|
3179
|
+
return
|
3180
|
+
|
3181
|
+
# Group findings by service for efficient processing
|
3182
|
+
s3_findings = [f for f in filtered_findings if f.get("service") == "s3"]
|
3183
|
+
|
3184
|
+
total_results = []
|
3185
|
+
|
3186
|
+
if s3_findings:
|
3187
|
+
from runbooks.inventory.models.account import AWSAccount
|
3188
|
+
from runbooks.remediation.base import RemediationContext
|
3189
|
+
from runbooks.remediation.s3_remediation import S3SecurityRemediation
|
3190
|
+
|
3191
|
+
console.print(f"[blue]🗄️ Processing {len(s3_findings)} S3 findings[/blue]")
|
3192
|
+
|
3193
|
+
s3_remediation = S3SecurityRemediation(profile=ctx.obj["profile"])
|
3194
|
+
account = AWSAccount(account_id="current", account_name="current")
|
3195
|
+
|
3196
|
+
for finding in s3_findings:
|
3197
|
+
try:
|
3198
|
+
context = RemediationContext.from_security_findings(finding)
|
3199
|
+
context.region = ctx.obj["region"]
|
3200
|
+
context.dry_run = ctx.obj["dry_run"]
|
3201
|
+
|
3202
|
+
check_id = finding.get("check_id", "")
|
3203
|
+
resource = finding.get("resource", "")
|
3204
|
+
|
3205
|
+
if "public-access" in check_id:
|
3206
|
+
results = s3_remediation.block_public_access(context, resource)
|
3207
|
+
elif "ssl" in check_id or "https" in check_id:
|
3208
|
+
results = s3_remediation.enforce_ssl(context, resource)
|
3209
|
+
elif "encryption" in check_id:
|
3210
|
+
results = s3_remediation.enable_encryption(context, resource)
|
3211
|
+
else:
|
3212
|
+
console.print(f"[yellow]⚠️ Unsupported finding type: {check_id}[/yellow]")
|
3213
|
+
continue
|
3214
|
+
|
3215
|
+
total_results.extend(results)
|
3216
|
+
|
3217
|
+
except Exception as e:
|
3218
|
+
console.print(f"[red]❌ Failed to remediate {finding.get('resource', 'unknown')}: {e}[/red]")
|
3219
|
+
|
3220
|
+
# Display final summary
|
3221
|
+
successful = [r for r in total_results if r.success]
|
3222
|
+
failed = [r for r in total_results if r.failed]
|
3223
|
+
|
3224
|
+
console.print(f"\n[bold]Auto-Remediation Summary:[/bold]")
|
3225
|
+
console.print(f"📊 Total findings processed: {len(filtered_findings)}")
|
3226
|
+
console.print(f"✅ Successful remediations: {len(successful)}")
|
3227
|
+
console.print(f"❌ Failed remediations: {len(failed)}")
|
3228
|
+
|
3229
|
+
if ctx.obj["output"] != "console":
|
3230
|
+
# Save results to file
|
3231
|
+
results_data = {
|
3232
|
+
"summary": {
|
3233
|
+
"total_findings": len(filtered_findings),
|
3234
|
+
"successful_remediations": len(successful),
|
3235
|
+
"failed_remediations": len(failed),
|
3236
|
+
},
|
3237
|
+
"results": [
|
3238
|
+
{
|
3239
|
+
"operation_id": r.operation_id,
|
3240
|
+
"resource": r.affected_resources,
|
3241
|
+
"status": r.status.value,
|
3242
|
+
"error": r.error_message,
|
3243
|
+
}
|
3244
|
+
for r in total_results
|
3245
|
+
],
|
3246
|
+
}
|
3247
|
+
|
3248
|
+
output_file = ctx.obj["output_file"] or f"auto_remediation_{severity}.json"
|
3249
|
+
with open(output_file, "w") as f:
|
3250
|
+
json.dump(results_data, f, indent=2, default=str)
|
3251
|
+
|
3252
|
+
console.print(f"[green]💾 Results saved to: {output_file}[/green]")
|
3253
|
+
|
3254
|
+
except Exception as e:
|
3255
|
+
console.print(f"[red]❌ Auto-remediation failed: {e}[/red]")
|
3256
|
+
raise click.ClickException(str(e))
|
3257
|
+
|
3258
|
+
|
3259
|
+
# ============================================================================
|
3260
|
+
# FINOPS COMMANDS (Cost & Usage Analytics)
|
3261
|
+
# ============================================================================
|
3262
|
+
|
3263
|
+
|
3264
|
+
@main.group(invoke_without_command=True)
|
3265
|
+
@common_aws_options
|
3266
|
+
@click.option("--time-range", type=int, help="Time range in days (default: current month)")
|
763
3267
|
@click.option(
|
764
|
-
"--
|
765
|
-
is_flag=True,
|
766
|
-
help="Display an audit report with cost anomalies, stopped EC2 instances, unused EBS volumes, budget alerts, and more",
|
3268
|
+
"--report-type", multiple=True, type=click.Choice(["csv", "json", "pdf"]), default=("csv",), help="Report types"
|
767
3269
|
)
|
768
3270
|
@click.pass_context
|
769
|
-
def finops(ctx,
|
770
|
-
"""
|
3271
|
+
def finops(ctx, profile, region, dry_run, time_range, report_type):
|
3272
|
+
"""
|
3273
|
+
AWS FinOps - Cost and usage analytics.
|
3274
|
+
|
3275
|
+
Comprehensive cost analysis, optimization recommendations,
|
3276
|
+
and resource utilization reporting.
|
3277
|
+
|
3278
|
+
Examples:
|
3279
|
+
runbooks finops dashboard --time-range 30
|
3280
|
+
runbooks finops analyze --report-type json,pdf
|
3281
|
+
"""
|
3282
|
+
ctx.obj.update(
|
3283
|
+
{"profile": profile, "region": region, "dry_run": dry_run, "time_range": time_range, "report_type": report_type}
|
3284
|
+
)
|
3285
|
+
|
771
3286
|
if ctx.invoked_subcommand is None:
|
3287
|
+
# Run default dashboard
|
772
3288
|
import argparse
|
773
3289
|
|
774
3290
|
from runbooks.finops.dashboard_runner import run_dashboard
|
775
3291
|
|
776
|
-
args = argparse.Namespace(**
|
3292
|
+
args = argparse.Namespace(**ctx.obj)
|
777
3293
|
run_dashboard(args)
|
778
3294
|
|
779
3295
|
|
780
3296
|
# ============================================================================
|
781
|
-
#
|
3297
|
+
# HELPER FUNCTIONS
|
3298
|
+
# ============================================================================
|
3299
|
+
|
3300
|
+
|
3301
|
+
def display_inventory_results(results):
|
3302
|
+
"""Display inventory results in formatted tables."""
|
3303
|
+
from runbooks.inventory.core.formatter import InventoryFormatter
|
3304
|
+
|
3305
|
+
formatter = InventoryFormatter(results)
|
3306
|
+
console_output = formatter.format_console_table()
|
3307
|
+
console.print(console_output)
|
3308
|
+
|
3309
|
+
|
3310
|
+
def save_inventory_results(results, output_format, output_file):
|
3311
|
+
"""Save inventory results to file."""
|
3312
|
+
from runbooks.inventory.core.formatter import InventoryFormatter
|
3313
|
+
|
3314
|
+
formatter = InventoryFormatter(results)
|
3315
|
+
|
3316
|
+
if not output_file:
|
3317
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
3318
|
+
output_file = f"inventory_{timestamp}.{output_format}"
|
3319
|
+
|
3320
|
+
if output_format == "csv":
|
3321
|
+
formatter.to_csv(output_file)
|
3322
|
+
elif output_format == "json":
|
3323
|
+
formatter.to_json(output_file)
|
3324
|
+
elif output_format == "html":
|
3325
|
+
formatter.to_html(output_file)
|
3326
|
+
elif output_format == "yaml":
|
3327
|
+
formatter.to_yaml(output_file)
|
3328
|
+
|
3329
|
+
console.print(f"[green]💾 Results saved to: {output_file}[/green]")
|
3330
|
+
|
3331
|
+
|
3332
|
+
def display_assessment_results(report):
|
3333
|
+
"""Display CFAT assessment results."""
|
3334
|
+
console.print(f"\n[bold blue]📊 Cloud Foundations Assessment Results[/bold blue]")
|
3335
|
+
console.print(f"[dim]Score: {report.summary.compliance_score}/100 | Risk: {report.summary.risk_level}[/dim]")
|
3336
|
+
|
3337
|
+
# Summary table
|
3338
|
+
from rich.table import Table
|
3339
|
+
|
3340
|
+
table = Table(title="Assessment Summary")
|
3341
|
+
table.add_column("Metric", style="cyan")
|
3342
|
+
table.add_column("Value", style="bold")
|
3343
|
+
table.add_column("Status", style="green")
|
3344
|
+
|
3345
|
+
table.add_row("Compliance Score", f"{report.summary.compliance_score}/100", report.summary.risk_level)
|
3346
|
+
table.add_row("Total Checks", str(report.summary.total_checks), "✓ Completed")
|
3347
|
+
table.add_row("Pass Rate", f"{report.summary.pass_rate:.1f}%", "📊 Analyzed")
|
3348
|
+
table.add_row(
|
3349
|
+
"Critical Issues",
|
3350
|
+
str(report.summary.critical_issues),
|
3351
|
+
"🚨 Review Required" if report.summary.critical_issues > 0 else "✅ None",
|
3352
|
+
)
|
3353
|
+
|
3354
|
+
console.print(table)
|
3355
|
+
|
3356
|
+
|
3357
|
+
def save_assessment_results(report, output_format, output_file):
|
3358
|
+
"""Save assessment results to file."""
|
3359
|
+
if not output_file:
|
3360
|
+
timestamp = report.timestamp.strftime("%Y%m%d_%H%M%S")
|
3361
|
+
output_file = f"cfat_report_{timestamp}.{output_format}"
|
3362
|
+
|
3363
|
+
if output_format == "html":
|
3364
|
+
report.to_html(output_file)
|
3365
|
+
elif output_format == "json":
|
3366
|
+
report.to_json(output_file)
|
3367
|
+
elif output_format == "csv":
|
3368
|
+
report.to_csv(output_file)
|
3369
|
+
elif output_format == "yaml":
|
3370
|
+
report.to_yaml(output_file)
|
3371
|
+
|
3372
|
+
console.print(f"[green]💾 Assessment saved to: {output_file}[/green]")
|
3373
|
+
|
3374
|
+
|
3375
|
+
def display_ou_structure(ous):
|
3376
|
+
"""Display OU structure in formatted table."""
|
3377
|
+
from rich.table import Table
|
3378
|
+
|
3379
|
+
table = Table(title="AWS Organizations Structure")
|
3380
|
+
table.add_column("Name", style="cyan")
|
3381
|
+
table.add_column("ID", style="green")
|
3382
|
+
table.add_column("Level", justify="center")
|
3383
|
+
table.add_column("Parent ID", style="blue")
|
3384
|
+
|
3385
|
+
for ou in ous:
|
3386
|
+
indent = " " * ou.get("Level", 0)
|
3387
|
+
table.add_row(
|
3388
|
+
f"{indent}{ou.get('Name', 'Unknown')}", ou.get("Id", ""), str(ou.get("Level", 0)), ou.get("ParentId", "")
|
3389
|
+
)
|
3390
|
+
|
3391
|
+
console.print(table)
|
3392
|
+
|
3393
|
+
|
3394
|
+
def save_ou_results(ous, output_format, output_file):
|
3395
|
+
"""Save OU results to file."""
|
3396
|
+
if not output_file:
|
3397
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
3398
|
+
output_file = f"organizations_{timestamp}.{output_format}"
|
3399
|
+
|
3400
|
+
if output_format == "json":
|
3401
|
+
import json
|
3402
|
+
|
3403
|
+
with open(output_file, "w") as f:
|
3404
|
+
json.dump(ous, f, indent=2, default=str)
|
3405
|
+
elif output_format == "yaml":
|
3406
|
+
import yaml
|
3407
|
+
|
3408
|
+
with open(output_file, "w") as f:
|
3409
|
+
yaml.dump(ous, f, default_flow_style=False)
|
3410
|
+
|
3411
|
+
console.print(f"[green]💾 OU structure saved to: {output_file}[/green]")
|
3412
|
+
|
3413
|
+
|
3414
|
+
# ============================================================================
|
3415
|
+
# CLI SHORTCUTS (Common Operations)
|
3416
|
+
# ============================================================================
|
3417
|
+
|
3418
|
+
|
3419
|
+
@main.command()
|
3420
|
+
@click.argument("instance_ids", nargs=-1, required=False)
|
3421
|
+
@common_aws_options
|
3422
|
+
@click.pass_context
|
3423
|
+
def start(ctx, instance_ids, profile, region, dry_run):
|
3424
|
+
"""
|
3425
|
+
🚀 Quick start EC2 instances (shortcut for: runbooks operate ec2 start)
|
3426
|
+
|
3427
|
+
Examples:
|
3428
|
+
runbooks start i-1234567890abcdef0
|
3429
|
+
runbooks start i-123456 i-789012 i-345678
|
3430
|
+
"""
|
3431
|
+
# Interactive prompting for missing instance IDs
|
3432
|
+
if not instance_ids:
|
3433
|
+
console.print("[cyan]⚡ EC2 Start Operation[/cyan]")
|
3434
|
+
|
3435
|
+
# Try to suggest available instances
|
3436
|
+
try:
|
3437
|
+
console.print("[dim]🔍 Discovering stopped instances...[/dim]")
|
3438
|
+
from runbooks.inventory.core.collector import InventoryCollector
|
3439
|
+
|
3440
|
+
collector = InventoryCollector(profile=profile, region=region)
|
3441
|
+
|
3442
|
+
# Quick scan for stopped instances
|
3443
|
+
# Simplified - just provide helpful tip
|
3444
|
+
|
3445
|
+
# Extract stopped instances (this is a simplified version)
|
3446
|
+
console.print("[dim]💡 Found stopped instances - you can specify them manually[/dim]")
|
3447
|
+
|
3448
|
+
except Exception:
|
3449
|
+
pass # Continue without suggestions if discovery fails
|
3450
|
+
|
3451
|
+
# Prompt for instance IDs
|
3452
|
+
instance_input = click.prompt("Instance IDs (comma-separated)", type=str)
|
3453
|
+
|
3454
|
+
if not instance_input.strip():
|
3455
|
+
console.print("[red]❌ No instance IDs provided[/red]")
|
3456
|
+
console.print("[dim]💡 Example: i-1234567890abcdef0,i-0987654321fedcba0[/dim]")
|
3457
|
+
sys.exit(1)
|
3458
|
+
|
3459
|
+
# Parse the input
|
3460
|
+
instance_ids = [id.strip() for id in instance_input.split(",") if id.strip()]
|
3461
|
+
|
3462
|
+
# Confirm the operation
|
3463
|
+
console.print(f"\n[yellow]📋 Will start {len(instance_ids)} instance(s):[/yellow]")
|
3464
|
+
for instance_id in instance_ids:
|
3465
|
+
console.print(f" • {instance_id}")
|
3466
|
+
console.print(f"[yellow]Region: {region}[/yellow]")
|
3467
|
+
console.print(f"[yellow]Profile: {profile}[/yellow]")
|
3468
|
+
console.print(f"[yellow]Dry-run: {dry_run}[/yellow]")
|
3469
|
+
|
3470
|
+
if not click.confirm("\nContinue?", default=True):
|
3471
|
+
console.print("[yellow]❌ Operation cancelled[/yellow]")
|
3472
|
+
sys.exit(0)
|
3473
|
+
|
3474
|
+
console.print(f"[cyan]🚀 Starting {len(instance_ids)} EC2 instance(s)...[/cyan]")
|
3475
|
+
|
3476
|
+
from runbooks.operate.ec2_operations import start_instances
|
3477
|
+
|
3478
|
+
try:
|
3479
|
+
result = start_instances(instance_ids=list(instance_ids), profile=profile, region=region, dry_run=dry_run)
|
3480
|
+
|
3481
|
+
if dry_run:
|
3482
|
+
console.print("[yellow]🧪 DRY RUN - No instances were actually started[/yellow]")
|
3483
|
+
else:
|
3484
|
+
console.print(f"[green]✅ Operation completed successfully[/green]")
|
3485
|
+
|
3486
|
+
except Exception as e:
|
3487
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
3488
|
+
console.print(f"[dim]💡 Try: runbooks inventory collect -r ec2 # List available instances[/dim]")
|
3489
|
+
console.print(f"[dim]💡 Example: runbooks operate ec2 start --instance-ids i-1234567890abcdef0[/dim]")
|
3490
|
+
sys.exit(1)
|
3491
|
+
|
3492
|
+
|
3493
|
+
@main.command()
|
3494
|
+
@click.argument("instance_ids", nargs=-1, required=False)
|
3495
|
+
@common_aws_options
|
3496
|
+
@click.pass_context
|
3497
|
+
def stop(ctx, instance_ids, profile, region, dry_run):
|
3498
|
+
"""
|
3499
|
+
🛑 Quick stop EC2 instances (shortcut for: runbooks operate ec2 stop)
|
3500
|
+
|
3501
|
+
Examples:
|
3502
|
+
runbooks stop i-1234567890abcdef0
|
3503
|
+
runbooks stop i-123456 i-789012 i-345678
|
3504
|
+
"""
|
3505
|
+
# Interactive prompting for missing instance IDs
|
3506
|
+
if not instance_ids:
|
3507
|
+
console.print("[cyan]⚡ EC2 Stop Operation[/cyan]")
|
3508
|
+
|
3509
|
+
# Try to suggest available instances
|
3510
|
+
try:
|
3511
|
+
console.print("[dim]🔍 Discovering running instances...[/dim]")
|
3512
|
+
from runbooks.inventory.core.collector import InventoryCollector
|
3513
|
+
|
3514
|
+
collector = InventoryCollector(profile=profile, region=region)
|
3515
|
+
|
3516
|
+
# Quick scan for running instances
|
3517
|
+
# Simplified - just provide helpful tip
|
3518
|
+
|
3519
|
+
# Extract running instances (this is a simplified version)
|
3520
|
+
console.print("[dim]💡 Found running instances - you can specify them manually[/dim]")
|
3521
|
+
|
3522
|
+
except Exception:
|
3523
|
+
pass # Continue without suggestions if discovery fails
|
3524
|
+
|
3525
|
+
# Prompt for instance IDs
|
3526
|
+
instance_input = click.prompt("Instance IDs (comma-separated)", type=str)
|
3527
|
+
|
3528
|
+
if not instance_input.strip():
|
3529
|
+
console.print("[red]❌ No instance IDs provided[/red]")
|
3530
|
+
console.print("[dim]💡 Example: i-1234567890abcdef0,i-0987654321fedcba0[/dim]")
|
3531
|
+
sys.exit(1)
|
3532
|
+
|
3533
|
+
# Parse the input
|
3534
|
+
instance_ids = [id.strip() for id in instance_input.split(",") if id.strip()]
|
3535
|
+
|
3536
|
+
# Confirm the operation
|
3537
|
+
console.print(f"\n[yellow]📋 Will stop {len(instance_ids)} instance(s):[/yellow]")
|
3538
|
+
for instance_id in instance_ids:
|
3539
|
+
console.print(f" • {instance_id}")
|
3540
|
+
console.print(f"[yellow]Region: {region}[/yellow]")
|
3541
|
+
console.print(f"[yellow]Profile: {profile}[/yellow]")
|
3542
|
+
console.print(f"[yellow]Dry-run: {dry_run}[/yellow]")
|
3543
|
+
|
3544
|
+
if not click.confirm("\nContinue?", default=True):
|
3545
|
+
console.print("[yellow]❌ Operation cancelled[/yellow]")
|
3546
|
+
sys.exit(0)
|
3547
|
+
|
3548
|
+
console.print(f"[yellow]🛑 Stopping {len(instance_ids)} EC2 instance(s)...[/yellow]")
|
3549
|
+
|
3550
|
+
from runbooks.operate.ec2_operations import stop_instances
|
3551
|
+
|
3552
|
+
try:
|
3553
|
+
result = stop_instances(instance_ids=list(instance_ids), profile=profile, region=region, dry_run=dry_run)
|
3554
|
+
|
3555
|
+
if dry_run:
|
3556
|
+
console.print("[yellow]🧪 DRY RUN - No instances were actually stopped[/yellow]")
|
3557
|
+
else:
|
3558
|
+
console.print(f"[green]✅ Operation completed successfully[/green]")
|
3559
|
+
|
3560
|
+
except Exception as e:
|
3561
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
3562
|
+
console.print(f"[dim]💡 Try: runbooks inventory collect -r ec2 # List running instances[/dim]")
|
3563
|
+
console.print(f"[dim]💡 Example: runbooks operate ec2 stop --instance-ids i-1234567890abcdef0[/dim]")
|
3564
|
+
sys.exit(1)
|
3565
|
+
|
3566
|
+
|
3567
|
+
@main.command()
|
3568
|
+
@common_aws_options
|
3569
|
+
@click.option("--resources", "-r", default="ec2", help="Resources to discover (default: ec2)")
|
3570
|
+
@click.pass_context
|
3571
|
+
def scan(ctx, profile, region, dry_run, resources):
|
3572
|
+
"""
|
3573
|
+
🔍 Quick resource discovery (shortcut for: runbooks inventory collect)
|
3574
|
+
|
3575
|
+
Examples:
|
3576
|
+
runbooks scan # Scan EC2 instances
|
3577
|
+
runbooks scan -r ec2,rds # Scan multiple resources
|
3578
|
+
runbooks scan -r s3 # Scan S3 buckets
|
3579
|
+
"""
|
3580
|
+
console.print(f"[cyan]🔍 Scanning {resources} resources...[/cyan]")
|
3581
|
+
|
3582
|
+
from runbooks.inventory.core.collector import InventoryCollector
|
3583
|
+
|
3584
|
+
try:
|
3585
|
+
collector = InventoryCollector(profile=profile, region=region)
|
3586
|
+
|
3587
|
+
# Get current account ID
|
3588
|
+
account_ids = [collector.get_current_account_id()]
|
3589
|
+
|
3590
|
+
# Collect inventory
|
3591
|
+
results = collector.collect_inventory(
|
3592
|
+
resource_types=resources.split(","),
|
3593
|
+
account_ids=account_ids,
|
3594
|
+
include_costs=False
|
3595
|
+
)
|
3596
|
+
|
3597
|
+
console.print(f"[green]✅ Scan completed - Found resources in account {account_ids[0]}[/green]")
|
3598
|
+
|
3599
|
+
except Exception as e:
|
3600
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
3601
|
+
console.print(f"[dim]💡 Available resources: ec2, rds, s3, lambda, iam, vpc[/dim]")
|
3602
|
+
console.print(f"[dim]💡 Example: runbooks scan -r ec2,rds --region us-west-2[/dim]")
|
3603
|
+
sys.exit(1)
|
3604
|
+
|
3605
|
+
|
3606
|
+
# ============================================================================
|
3607
|
+
# MAIN ENTRY POINT
|
782
3608
|
# ============================================================================
|
783
3609
|
|
784
3610
|
if __name__ == "__main__":
|