runbooks 0.7.0__py3-none-any.whl → 0.7.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
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
|
205
|
+
CloudOps Runbooks - Enterprise AWS Automation Toolkit v{version}.
|
88
206
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
-
|
93
|
-
-
|
94
|
-
-
|
95
|
-
|
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,895 +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
|
-
"""Cloud Foundations Assessment Tool - Assess AWS account configuration."""
|
126
|
-
pass
|
127
|
-
|
128
|
-
|
129
|
-
@cfat.command()
|
130
|
-
@click.option(
|
131
|
-
"--output",
|
132
|
-
type=click.Choice(["console", "html", "csv", "json", "markdown", "all"]),
|
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
|
-
):
|
260
|
+
def inventory(ctx, profile, region, dry_run, output, output_file, tags, accounts, regions):
|
169
261
|
"""
|
170
|
-
|
262
|
+
Multi-account AWS resource discovery and inventory.
|
171
263
|
|
172
|
-
|
173
|
-
|
174
|
-
features including multi-format reporting, parallel execution,
|
175
|
-
compliance framework alignment, and project management integration.
|
264
|
+
Read-only operations for comprehensive resource discovery across
|
265
|
+
AWS services, accounts, and regions with advanced filtering.
|
176
266
|
|
177
267
|
Examples:
|
178
|
-
|
179
|
-
runbooks
|
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
|
271
|
+
"""
|
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
|
+
)
|
180
285
|
|
181
|
-
|
182
|
-
|
286
|
+
if ctx.invoked_subcommand is None:
|
287
|
+
click.echo(ctx.get_help())
|
183
288
|
|
184
|
-
# Parallel execution with custom workers
|
185
|
-
runbooks cfat assess --parallel --max-workers 5
|
186
289
|
|
187
|
-
|
188
|
-
|
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]")
|
189
302
|
|
190
|
-
#
|
191
|
-
|
303
|
+
# Initialize collector
|
304
|
+
collector = InventoryCollector(profile=ctx.obj["profile"], region=ctx.obj["region"], parallel=parallel)
|
192
305
|
|
193
|
-
#
|
194
|
-
|
195
|
-
|
196
|
-
|
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"]
|
197
313
|
|
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()
|
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()]
|
321
|
+
|
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
|
+
)
|
229
327
|
|
230
|
-
|
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"])
|
231
333
|
|
232
|
-
|
233
|
-
display_assessment_results(report)
|
334
|
+
console.print(f"[green]✅ Inventory collection completed![/green]")
|
234
335
|
|
235
|
-
|
236
|
-
|
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))
|
237
340
|
|
238
|
-
if output == "all":
|
239
|
-
# Generate all formats
|
240
|
-
timestamp = report.timestamp.strftime("%Y%m%d_%H%M%S")
|
241
|
-
base_name = f"cfat_report_{timestamp}"
|
242
341
|
|
243
|
-
|
244
|
-
|
342
|
+
# ============================================================================
|
343
|
+
# OPERATE COMMANDS (Resource Lifecycle Operations)
|
344
|
+
# ============================================================================
|
245
345
|
|
246
|
-
report.to_json(f"{base_name}.json")
|
247
|
-
generated_files.append(f"{base_name}.json")
|
248
346
|
|
249
|
-
|
250
|
-
|
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.
|
251
354
|
|
252
|
-
|
253
|
-
|
355
|
+
Perform operational tasks including creation, modification, and deletion
|
356
|
+
of AWS resources with comprehensive safety features.
|
254
357
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
260
363
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
elif output == "markdown":
|
268
|
-
report.to_markdown(output_file)
|
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})
|
269
370
|
|
270
|
-
|
371
|
+
if ctx.invoked_subcommand is None:
|
372
|
+
click.echo(ctx.get_help())
|
271
373
|
|
272
|
-
# Export to project management tools
|
273
|
-
if export_jira:
|
274
|
-
from runbooks.cfat.reporting.exporters import JiraExporter
|
275
374
|
|
276
|
-
|
277
|
-
|
278
|
-
|
375
|
+
@operate.group()
|
376
|
+
@click.pass_context
|
377
|
+
def ec2(ctx):
|
378
|
+
"""EC2 instance and resource operations."""
|
379
|
+
pass
|
279
380
|
|
280
|
-
if export_asana:
|
281
|
-
from runbooks.cfat.reporting.exporters import AsanaExporter
|
282
381
|
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
+
)
|
286
413
|
|
287
|
-
|
288
|
-
|
414
|
+
# Execute operation
|
415
|
+
results = ec2_ops.start_instances(context, list(instance_ids))
|
289
416
|
|
290
|
-
|
291
|
-
|
292
|
-
|
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}")
|
293
423
|
|
294
|
-
|
295
|
-
if serve_web:
|
296
|
-
start_web_server(report, web_port)
|
424
|
+
console.print(f"\n[bold]Summary: {successful}/{len(results)} instances started[/bold]")
|
297
425
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
console.print(f" • {file}")
|
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))
|
304
431
|
|
305
|
-
# Display summary statistics
|
306
|
-
console.print(f"\n[bold]Assessment Summary:[/bold]")
|
307
|
-
console.print(f"• Compliance Score: [bold]{report.summary.compliance_score}/100[/bold]")
|
308
|
-
console.print(f"• Risk Level: [bold]{report.summary.risk_level}[/bold]")
|
309
|
-
console.print(f"• Critical Issues: [bold red]{report.summary.critical_issues}[/bold red]")
|
310
|
-
console.print(f"• Total Checks: {report.summary.total_checks}")
|
311
|
-
console.print(f"• Pass Rate: {report.summary.pass_rate:.1f}%")
|
312
432
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
+
)
|
317
457
|
|
458
|
+
results = ec2_ops.stop_instances(context, list(instance_ids))
|
318
459
|
|
319
|
-
|
320
|
-
|
321
|
-
|
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}")
|
322
465
|
|
323
|
-
|
324
|
-
report: Assessment report to serve
|
325
|
-
port: Port number for web server
|
326
|
-
"""
|
327
|
-
try:
|
328
|
-
import os
|
329
|
-
import tempfile
|
330
|
-
import threading
|
331
|
-
import webbrowser
|
332
|
-
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
466
|
+
console.print(f"\n[bold]Summary: {successful}/{len(results)} instances stopped[/bold]")
|
333
467
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
report.to_html(html_file)
|
468
|
+
except Exception as e:
|
469
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
470
|
+
raise click.ClickException(str(e))
|
338
471
|
|
339
|
-
# Change to temp directory for serving
|
340
|
-
os.chdir(temp_dir)
|
341
472
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
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)."""
|
479
|
+
try:
|
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
|
+
)
|
348
504
|
|
349
|
-
|
350
|
-
server_thread.start()
|
505
|
+
results = ec2_ops.terminate_instances(context, list(instance_ids))
|
351
506
|
|
352
|
-
|
353
|
-
|
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}")
|
354
512
|
|
355
|
-
|
356
|
-
try:
|
357
|
-
server_thread.join()
|
358
|
-
except KeyboardInterrupt:
|
359
|
-
console.print(f"\n[yellow]Web server stopped[/yellow]")
|
513
|
+
console.print(f"\n[bold]Summary: {successful}/{len(results)} instances terminated[/bold]")
|
360
514
|
|
361
|
-
except ImportError:
|
362
|
-
console.print(f"[red]Web server functionality requires additional dependencies[/red]")
|
363
515
|
except Exception as e:
|
364
|
-
|
365
|
-
|
516
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
517
|
+
raise click.ClickException(str(e))
|
366
518
|
|
367
519
|
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
371
539
|
|
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
|
-
)
|
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
|
+
)
|
407
544
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
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
|
-
)
|
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
|
+
)
|
435
562
|
|
436
|
-
|
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
|
+
)
|
437
576
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
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]")
|
442
585
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
failed_table.add_column("Severity", width=10)
|
447
|
-
failed_table.add_column("Message", style="dim", width=50)
|
586
|
+
except Exception as e:
|
587
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
588
|
+
raise click.ClickException(str(e))
|
448
589
|
|
449
|
-
for result in failed_results[:10]: # Show first 10 failed checks
|
450
|
-
severity_color = {"INFO": "blue", "WARNING": "yellow", "CRITICAL": "red"}.get(
|
451
|
-
result.severity.value, "white"
|
452
|
-
)
|
453
590
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
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
|
460
605
|
|
461
|
-
|
462
|
-
console.print(f"[dim]... and {len(failed_results) - 10} more failed checks[/dim]")
|
463
|
-
|
464
|
-
console.print(failed_table)
|
465
|
-
|
466
|
-
# Show critical findings if any
|
467
|
-
critical_results = report.get_critical_results()
|
468
|
-
if critical_results:
|
469
|
-
console.print(f"\n[bold red]⚠️ Critical Findings Requiring Immediate Action[/bold red]")
|
470
|
-
for i, result in enumerate(critical_results[:3], 1): # Show first 3 critical
|
471
|
-
console.print(f"[red]{i}. {result.finding_id}[/red]: {result.message}")
|
472
|
-
if result.recommendations:
|
473
|
-
console.print(f" [yellow]→ {result.recommendations[0]}[/yellow]")
|
474
|
-
|
475
|
-
if len(critical_results) > 3:
|
476
|
-
console.print(f" [dim]... and {len(critical_results) - 3} more critical findings[/dim]")
|
477
|
-
|
478
|
-
# Final recommendations
|
479
|
-
console.print(f"\n[bold]📝 Next Steps[/bold]")
|
480
|
-
if report.summary.critical_issues > 0:
|
481
|
-
console.print(f"[red]• Address {report.summary.critical_issues} critical security issues immediately[/red]")
|
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:
|
606
|
+
console.print(f"[blue]📋 Copying AMI Across Regions[/blue]")
|
485
607
|
console.print(
|
486
|
-
f"[
|
608
|
+
f"[dim]Source: {source_image_id} ({source_region}) → {ctx.obj['region']} | Dry-run: {ctx.obj['dry_run']}[/dim]"
|
487
609
|
)
|
488
|
-
else:
|
489
|
-
console.print(f"[green]• Maintain current security posture and continue monitoring[/green]")
|
490
610
|
|
611
|
+
ec2_ops = EC2Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
491
612
|
|
492
|
-
|
493
|
-
|
494
|
-
|
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
|
+
)
|
495
621
|
|
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
|
+
)
|
496
631
|
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
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]")
|
641
|
+
|
642
|
+
except Exception as e:
|
643
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
644
|
+
raise click.ClickException(str(e))
|
502
645
|
|
503
646
|
|
504
|
-
@
|
505
|
-
@click.option("--resources", "-r", multiple=True, help="Resource types to inventory (ec2, rds, lambda, etc.)")
|
506
|
-
@click.option("--all-resources", is_flag=True, help="Inventory all resource types")
|
507
|
-
@click.option("--accounts", "-a", multiple=True, help="Account IDs to inventory")
|
508
|
-
@click.option("--all-accounts", is_flag=True, help="Inventory all organization accounts")
|
509
|
-
@click.option("--output", type=click.Choice(["table", "csv", "json", "excel"]), default="table", help="Output format")
|
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")
|
647
|
+
@ec2.command()
|
513
648
|
@click.pass_context
|
514
|
-
def
|
515
|
-
"""
|
516
|
-
|
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"],
|
668
|
+
)
|
517
669
|
|
518
|
-
|
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")
|
670
|
+
results = ec2_ops.cleanup_unused_volumes(context)
|
524
671
|
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
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]")
|
529
678
|
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
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]")
|
535
684
|
else:
|
536
|
-
|
685
|
+
console.print(f"[red]❌ Scan failed: {result.error_message}[/red]")
|
537
686
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
elif accounts:
|
542
|
-
account_ids = list(accounts)
|
543
|
-
else:
|
544
|
-
account_ids = [collector.get_current_account_id()]
|
687
|
+
except Exception as e:
|
688
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
689
|
+
raise click.ClickException(str(e))
|
545
690
|
|
546
|
-
# Collect inventory
|
547
|
-
results = collector.collect_inventory(
|
548
|
-
resource_types=resource_types, account_ids=account_ids, include_costs=include_costs
|
549
|
-
)
|
550
691
|
|
551
|
-
|
552
|
-
|
553
|
-
|
692
|
+
@ec2.command()
|
693
|
+
@click.pass_context
|
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
|
+
)
|
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]")
|
554
730
|
else:
|
555
|
-
|
731
|
+
console.print(f"[red]❌ Scan failed: {result.error_message}[/red]")
|
556
732
|
|
557
|
-
|
733
|
+
except Exception as e:
|
734
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
735
|
+
raise click.ClickException(str(e))
|
558
736
|
|
559
|
-
if not output_file:
|
560
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
561
|
-
output_file = f"inventory_{timestamp}.{output}"
|
562
737
|
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
formatter.to_excel(output_file)
|
738
|
+
@operate.group()
|
739
|
+
@click.pass_context
|
740
|
+
def s3(ctx):
|
741
|
+
"""S3 bucket and object operations."""
|
742
|
+
pass
|
569
743
|
|
570
|
-
console.print(f"[green]✓ Inventory saved to: {output_file}[/green]")
|
571
744
|
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
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
|
+
)
|
771
|
+
|
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
|
+
)
|
576
780
|
|
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]")
|
790
|
+
else:
|
791
|
+
console.print(f"[red]❌ Failed: {result.error_message}[/red]")
|
577
792
|
|
578
|
-
|
579
|
-
|
580
|
-
|
793
|
+
except Exception as e:
|
794
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
795
|
+
raise click.ClickException(str(e))
|
581
796
|
|
582
797
|
|
583
|
-
@
|
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")
|
584
801
|
@click.pass_context
|
585
|
-
def
|
586
|
-
"""
|
587
|
-
|
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
|
+
)
|
588
828
|
|
829
|
+
results = s3_ops.delete_bucket_and_objects(context, bucket_name)
|
589
830
|
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
help="OU structure template",
|
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")
|
599
|
-
@click.pass_context
|
600
|
-
def setup_ous(ctx, template, config_file, dry_run):
|
601
|
-
"""
|
602
|
-
Set up organizational unit structure.
|
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]")
|
603
836
|
|
604
|
-
|
605
|
-
|
837
|
+
except Exception as e:
|
838
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
839
|
+
raise click.ClickException(str(e))
|
606
840
|
|
607
|
-
Examples:
|
608
|
-
runbooks org setup-ous --template security
|
609
|
-
runbooks org setup-ous --config-file ou_structure.yaml --dry-run
|
610
|
-
"""
|
611
|
-
logger.info(f"Setting up OU structure with template: {template}")
|
612
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."""
|
613
853
|
try:
|
614
|
-
|
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
|
+
)
|
615
871
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
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
|
+
)
|
620
880
|
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
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]")
|
628
890
|
|
629
891
|
except Exception as e:
|
630
|
-
|
631
|
-
|
632
|
-
sys.exit(1)
|
892
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
893
|
+
raise click.ClickException(str(e))
|
633
894
|
|
634
895
|
|
635
|
-
|
636
|
-
|
637
|
-
|
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)")
|
903
|
+
@click.pass_context
|
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
|
638
910
|
|
639
|
-
|
640
|
-
|
641
|
-
|
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
|
+
)
|
642
915
|
|
916
|
+
if delete_removed:
|
917
|
+
console.print(f"[yellow]⚠️ Delete removed objects enabled[/yellow]")
|
643
918
|
|
644
|
-
|
645
|
-
"""Display OU structure in a formatted view."""
|
646
|
-
table = Table(title=f"OU Structure: {structure.get('name', 'Unnamed')}")
|
647
|
-
table.add_column("OU Name", style="cyan")
|
648
|
-
table.add_column("Description", style="dim")
|
649
|
-
table.add_column("Level", style="bold")
|
919
|
+
s3_ops = S3Operations(profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"])
|
650
920
|
|
651
|
-
|
652
|
-
|
653
|
-
|
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
|
+
)
|
654
929
|
|
655
|
-
|
656
|
-
|
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
|
+
)
|
657
939
|
|
658
|
-
|
659
|
-
|
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]")
|
660
952
|
|
661
|
-
|
953
|
+
except Exception as e:
|
954
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
955
|
+
raise click.ClickException(str(e))
|
662
956
|
|
663
957
|
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
table.add_column("Parent ID", style="dim")
|
670
|
-
table.add_column("Status", style="green")
|
958
|
+
@operate.group()
|
959
|
+
@click.pass_context
|
960
|
+
def cloudformation(ctx):
|
961
|
+
"""CloudFormation stack and StackSet operations."""
|
962
|
+
pass
|
671
963
|
|
672
|
-
def add_results_to_table(ou_result, level=0):
|
673
|
-
indent = " " * level
|
674
|
-
table.add_row(f"{indent}{ou_result['name']}", ou_result["id"], ou_result["parent_id"], "✓ Created")
|
675
964
|
|
676
|
-
|
677
|
-
|
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
|
678
978
|
|
679
|
-
|
680
|
-
|
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]")
|
681
984
|
|
682
|
-
|
985
|
+
cfn_ops = CloudFormationOperations(
|
986
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
987
|
+
)
|
683
988
|
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
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
|
+
)
|
688
997
|
|
998
|
+
# Parse operation preferences if provided
|
999
|
+
preferences = None
|
1000
|
+
if operation_preferences:
|
1001
|
+
import json
|
689
1002
|
|
690
|
-
|
691
|
-
# FinOps Commands
|
692
|
-
# ============================================================================
|
1003
|
+
preferences = json.loads(operation_preferences)
|
693
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
|
+
)
|
694
1013
|
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
)
|
702
|
-
|
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
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
)
|
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]")
|
1023
|
+
|
1024
|
+
except Exception as e:
|
1025
|
+
console.print(f"[red]❌ Operation failed: {e}[/red]")
|
1026
|
+
raise click.ClickException(str(e))
|
1027
|
+
|
1028
|
+
|
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
|
1040
|
+
|
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
|
+
)
|
1045
|
+
|
1046
|
+
cfn_ops = CloudFormationOperations(
|
1047
|
+
profile=ctx.obj["profile"], region=ctx.obj["region"], dry_run=ctx.obj["dry_run"]
|
1048
|
+
)
|
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
|
+
)
|
1058
|
+
|
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
|
+
)
|
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]")
|
1075
|
+
|
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")
|
750
1305
|
@click.option(
|
751
|
-
"--
|
752
|
-
"
|
753
|
-
|
754
|
-
help="
|
755
|
-
type=str,
|
1306
|
+
"--billing-mode",
|
1307
|
+
default="PAY_PER_REQUEST",
|
1308
|
+
type=click.Choice(["PAY_PER_REQUEST", "PROVISIONED"]),
|
1309
|
+
help="Billing mode",
|
756
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()
|
757
2190
|
@click.option(
|
758
|
-
"--
|
759
|
-
|
760
|
-
|
2191
|
+
"--key-filter",
|
2192
|
+
type=click.Choice(["customer-managed", "all"]),
|
2193
|
+
default="customer-managed",
|
2194
|
+
help="Filter keys to process",
|
761
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")
|
762
3147
|
@click.option(
|
763
|
-
"--
|
764
|
-
|
765
|
-
|
3148
|
+
"--severity",
|
3149
|
+
type=click.Choice(["critical", "high", "medium", "low"]),
|
3150
|
+
default="high",
|
3151
|
+
help="Minimum severity to remediate",
|
766
3152
|
)
|
3153
|
+
@click.option("--max-operations", type=int, default=50, help="Maximum operations to execute")
|
767
3154
|
@click.pass_context
|
768
|
-
def
|
769
|
-
"""
|
770
|
-
|
771
|
-
import
|
3155
|
+
def auto_fix(ctx, findings_file, severity, max_operations):
|
3156
|
+
"""Automatically remediate security findings from assessment results."""
|
3157
|
+
try:
|
3158
|
+
import json
|
772
3159
|
|
773
|
-
from
|
3160
|
+
console.print(f"[blue]🤖 Auto-Remediation from Security Findings[/blue]")
|
3161
|
+
console.print(f"[dim]File: {findings_file} | Min Severity: {severity}[/dim]")
|
774
3162
|
|
775
|
-
|
776
|
-
|
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))
|
777
3257
|
|
778
3258
|
|
779
3259
|
# ============================================================================
|
780
|
-
#
|
3260
|
+
# FINOPS COMMANDS (Cost & Usage Analytics)
|
781
3261
|
# ============================================================================
|
782
3262
|
|
783
3263
|
|
784
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)")
|
785
3267
|
@click.option(
|
786
|
-
"--
|
787
|
-
default="default",
|
788
|
-
help="AWS IAM profile to use for authentication (default: 'default')"
|
789
|
-
)
|
790
|
-
@click.option(
|
791
|
-
"--language",
|
792
|
-
type=click.Choice(["EN", "JP", "KR", "VN"]),
|
793
|
-
default="EN",
|
794
|
-
help="Language for security reports (default: 'EN')"
|
795
|
-
)
|
796
|
-
@click.option(
|
797
|
-
"--output",
|
798
|
-
help="Custom output directory for reports (default: ./results)"
|
3268
|
+
"--report-type", multiple=True, type=click.Choice(["csv", "json", "pdf"]), default=("csv",), help="Report types"
|
799
3269
|
)
|
800
3270
|
@click.pass_context
|
801
|
-
def
|
802
|
-
"""
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
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
|
+
|
807
3278
|
Examples:
|
808
|
-
runbooks
|
809
|
-
runbooks
|
810
|
-
runbooks security check root-mfa --profile production
|
3279
|
+
runbooks finops dashboard --time-range 30
|
3280
|
+
runbooks finops analyze --report-type json,pdf
|
811
3281
|
"""
|
3282
|
+
ctx.obj.update(
|
3283
|
+
{"profile": profile, "region": region, "dry_run": dry_run, "time_range": time_range, "report_type": report_type}
|
3284
|
+
)
|
3285
|
+
|
812
3286
|
if ctx.invoked_subcommand is None:
|
813
|
-
|
814
|
-
|
815
|
-
# Create mock args namespace for backward compatibility
|
3287
|
+
# Run default dashboard
|
816
3288
|
import argparse
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
3289
|
+
|
3290
|
+
from runbooks.finops.dashboard_runner import run_dashboard
|
3291
|
+
|
3292
|
+
args = argparse.Namespace(**ctx.obj)
|
3293
|
+
run_dashboard(args)
|
3294
|
+
|
3295
|
+
|
3296
|
+
# ============================================================================
|
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", "")
|
821
3389
|
)
|
822
|
-
|
823
|
-
# Import and run the main security function
|
824
|
-
from runbooks.security.security_baseline_tester import SecurityBaselineTester
|
825
|
-
|
826
|
-
try:
|
827
|
-
console.print(f"[blue]🔒 AWS Security Baseline Assessment[/blue]")
|
828
|
-
console.print(f"[dim]Profile: {profile} | Language: {language} | Output: {output or './results'}[/dim]")
|
829
|
-
|
830
|
-
tester = SecurityBaselineTester(profile, language, output)
|
831
|
-
tester.run()
|
832
|
-
|
833
|
-
console.print(f"[green]✅ Security assessment completed successfully![/green]")
|
834
|
-
|
835
|
-
except Exception as e:
|
836
|
-
console.print(f"[red]❌ Error running security assessment: {e}[/red]")
|
837
|
-
raise click.ClickException(str(e))
|
838
3390
|
|
3391
|
+
console.print(table)
|
839
3392
|
|
840
|
-
|
841
|
-
|
842
|
-
"
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
"
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
)
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
)
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
)
|
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
|
867
3422
|
@click.pass_context
|
868
|
-
def
|
869
|
-
"""
|
870
|
-
|
871
|
-
|
872
|
-
detailed reports with findings and remediation guidance.
|
873
|
-
|
3423
|
+
def start(ctx, instance_ids, profile, region, dry_run):
|
3424
|
+
"""
|
3425
|
+
🚀 Quick start EC2 instances (shortcut for: runbooks operate ec2 start)
|
3426
|
+
|
874
3427
|
Examples:
|
875
|
-
runbooks
|
876
|
-
runbooks
|
877
|
-
runbooks security assess --checks root_mfa --checks iam_password_policy
|
3428
|
+
runbooks start i-1234567890abcdef0
|
3429
|
+
runbooks start i-123456 i-789012 i-345678
|
878
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
|
+
|
879
3478
|
try:
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
# Initialize and run security assessment
|
889
|
-
tester = SecurityBaselineTester(profile, language, output)
|
890
|
-
|
891
|
-
# TODO: Add support for specific checks filtering
|
892
|
-
# For now, run all checks
|
893
|
-
tester.run()
|
894
|
-
|
895
|
-
console.print(f"[green]✅ Security assessment completed![/green]")
|
896
|
-
|
897
|
-
# Display results summary
|
898
|
-
console.print(f"\n[bold]📊 Assessment Summary:[/bold]")
|
899
|
-
console.print(f"[green]• Report generated in {format.upper()} format[/green]")
|
900
|
-
console.print(f"[yellow]• Output directory: {output or './results'}[/yellow]")
|
901
|
-
console.print(f"[blue]• Language: {language}[/blue]")
|
902
|
-
|
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
|
+
|
903
3486
|
except Exception as e:
|
904
|
-
console.print(f"[red]❌ Error
|
905
|
-
|
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)
|
906
3491
|
|
907
3492
|
|
908
|
-
@
|
909
|
-
@click.argument("
|
910
|
-
@
|
911
|
-
"--profile",
|
912
|
-
default="default",
|
913
|
-
help="AWS IAM profile to use"
|
914
|
-
)
|
915
|
-
@click.option(
|
916
|
-
"--language",
|
917
|
-
type=click.Choice(["EN", "JP", "KR", "VN"]),
|
918
|
-
default="EN",
|
919
|
-
help="Language for output"
|
920
|
-
)
|
3493
|
+
@main.command()
|
3494
|
+
@click.argument("instance_ids", nargs=-1, required=False)
|
3495
|
+
@common_aws_options
|
921
3496
|
@click.pass_context
|
922
|
-
def
|
923
|
-
"""
|
924
|
-
|
925
|
-
|
926
|
-
root_mfa, root_usage, root_access_key, iam_user_mfa,
|
927
|
-
iam_password_policy, direct_attached_policy, alternate_contacts,
|
928
|
-
trail_enabled, multi_region_trail, account_level_bucket_public_access,
|
929
|
-
bucket_public_access, cloudwatch_alarm_configuration,
|
930
|
-
multi_region_instance_usage, guardduty_enabled, trusted_advisor
|
931
|
-
|
3497
|
+
def stop(ctx, instance_ids, profile, region, dry_run):
|
3498
|
+
"""
|
3499
|
+
🛑 Quick stop EC2 instances (shortcut for: runbooks operate ec2 stop)
|
3500
|
+
|
932
3501
|
Examples:
|
933
|
-
runbooks
|
934
|
-
runbooks
|
3502
|
+
runbooks stop i-1234567890abcdef0
|
3503
|
+
runbooks stop i-123456 i-789012 i-345678
|
935
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
|
+
|
936
3552
|
try:
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
"iam_password_policy", "direct_attached_policy", "alternate_contacts",
|
945
|
-
"trail_enabled", "multi_region_trail", "account_level_bucket_public_access",
|
946
|
-
"bucket_public_access", "cloudwatch_alarm_configuration",
|
947
|
-
"multi_region_instance_usage", "guardduty_enabled", "trusted_advisor"
|
948
|
-
]
|
949
|
-
|
950
|
-
if check_name not in available_checks:
|
951
|
-
console.print(f"[red]❌ Unknown check: {check_name}[/red]")
|
952
|
-
console.print(f"[yellow]Available checks:[/yellow]")
|
953
|
-
for check in available_checks:
|
954
|
-
console.print(f" • {check}")
|
955
|
-
raise click.ClickException(f"Invalid check name: {check_name}")
|
956
|
-
|
957
|
-
console.print(f"[yellow]⚠️ Individual check execution not yet implemented[/yellow]")
|
958
|
-
console.print(f"[blue]💡 Use 'runbooks security assess' to run all checks[/blue]")
|
959
|
-
|
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
|
+
|
960
3560
|
except Exception as e:
|
961
|
-
console.print(f"[red]❌ Error
|
962
|
-
|
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)
|
963
3565
|
|
964
3566
|
|
965
|
-
@
|
3567
|
+
@main.command()
|
3568
|
+
@common_aws_options
|
3569
|
+
@click.option("--resources", "-r", default="ec2", help="Resources to discover (default: ec2)")
|
966
3570
|
@click.pass_context
|
967
|
-
def
|
968
|
-
"""
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
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)
|
997
3604
|
|
998
3605
|
|
999
3606
|
# ============================================================================
|
1000
|
-
#
|
3607
|
+
# MAIN ENTRY POINT
|
1001
3608
|
# ============================================================================
|
1002
3609
|
|
1003
3610
|
if __name__ == "__main__":
|