runbooks 1.1.7__py3-none-any.whl → 1.1.10__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 +1 -1
- runbooks/__init___optimized.py +2 -1
- runbooks/_platform/__init__.py +1 -1
- runbooks/cfat/cli.py +4 -3
- runbooks/cfat/cloud_foundations_assessment.py +1 -2
- runbooks/cfat/tests/test_cli.py +4 -1
- runbooks/cli/commands/finops.py +68 -19
- runbooks/cli/commands/inventory.py +838 -14
- runbooks/cli/commands/operate.py +65 -4
- runbooks/cli/commands/vpc.py +1 -1
- runbooks/cloudops/cost_optimizer.py +1 -3
- runbooks/common/cli_decorators.py +6 -4
- runbooks/common/config_loader.py +787 -0
- runbooks/common/config_schema.py +280 -0
- runbooks/common/dry_run_framework.py +14 -2
- runbooks/common/mcp_integration.py +238 -0
- runbooks/finops/ebs_cost_optimizer.py +7 -4
- runbooks/finops/elastic_ip_optimizer.py +7 -4
- runbooks/finops/infrastructure/__init__.py +3 -2
- runbooks/finops/infrastructure/commands.py +7 -4
- runbooks/finops/infrastructure/load_balancer_optimizer.py +7 -4
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +7 -4
- runbooks/finops/nat_gateway_optimizer.py +7 -4
- runbooks/finops/tests/run_tests.py +1 -1
- runbooks/inventory/ArgumentsClass.py +2 -1
- runbooks/inventory/CLAUDE.md +41 -0
- runbooks/inventory/README.md +210 -2
- runbooks/inventory/Tests/test_Inventory_Modules.py +27 -10
- runbooks/inventory/Tests/test_cfn_describe_stacks.py +18 -7
- runbooks/inventory/Tests/test_ec2_describe_instances.py +30 -15
- runbooks/inventory/Tests/test_lambda_list_functions.py +17 -3
- runbooks/inventory/Tests/test_org_list_accounts.py +17 -4
- runbooks/inventory/account_class.py +0 -1
- runbooks/inventory/all_my_instances_wrapper.py +4 -8
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/check_cloudtrail_compliance.py +4 -4
- runbooks/inventory/check_controltower_readiness.py +50 -47
- runbooks/inventory/check_landingzone_readiness.py +35 -31
- runbooks/inventory/cloud_foundations_integration.py +8 -3
- runbooks/inventory/collectors/aws_compute.py +59 -11
- runbooks/inventory/collectors/aws_management.py +39 -5
- runbooks/inventory/core/collector.py +1655 -159
- runbooks/inventory/core/concurrent_paginator.py +511 -0
- runbooks/inventory/discovery.md +15 -6
- runbooks/inventory/{draw_org_structure.py → draw_org.py} +55 -9
- runbooks/inventory/drift_detection_cli.py +8 -68
- runbooks/inventory/find_cfn_drift_detection.py +14 -4
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -5
- runbooks/inventory/find_cfn_stackset_drift.py +5 -5
- runbooks/inventory/find_ec2_security_groups.py +6 -3
- runbooks/inventory/find_landingzone_versions.py +5 -5
- runbooks/inventory/find_vpc_flow_logs.py +5 -5
- runbooks/inventory/inventory.sh +20 -7
- runbooks/inventory/inventory_mcp_cli.py +4 -0
- runbooks/inventory/inventory_modules.py +9 -7
- runbooks/inventory/list_cfn_stacks.py +18 -8
- runbooks/inventory/list_cfn_stackset_operation_results.py +2 -2
- runbooks/inventory/list_cfn_stackset_operations.py +32 -20
- runbooks/inventory/list_cfn_stacksets.py +7 -4
- runbooks/inventory/list_config_recorders_delivery_channels.py +4 -4
- runbooks/inventory/list_ds_directories.py +3 -3
- runbooks/inventory/list_ec2_availability_zones.py +7 -3
- runbooks/inventory/list_ec2_ebs_volumes.py +3 -3
- runbooks/inventory/list_ec2_instances.py +1 -1
- runbooks/inventory/list_ecs_clusters_and_tasks.py +8 -4
- runbooks/inventory/list_elbs_load_balancers.py +7 -3
- runbooks/inventory/list_enis_network_interfaces.py +3 -3
- runbooks/inventory/list_guardduty_detectors.py +9 -5
- runbooks/inventory/list_iam_policies.py +7 -3
- runbooks/inventory/list_iam_roles.py +3 -3
- runbooks/inventory/list_iam_saml_providers.py +8 -4
- runbooks/inventory/list_lambda_functions.py +8 -4
- runbooks/inventory/list_org_accounts.py +306 -276
- runbooks/inventory/list_org_accounts_users.py +45 -9
- runbooks/inventory/list_rds_db_instances.py +4 -4
- runbooks/inventory/list_route53_hosted_zones.py +3 -3
- runbooks/inventory/list_servicecatalog_provisioned_products.py +5 -5
- runbooks/inventory/list_sns_topics.py +4 -4
- runbooks/inventory/list_ssm_parameters.py +6 -3
- runbooks/inventory/list_vpc_subnets.py +8 -4
- runbooks/inventory/list_vpcs.py +15 -4
- runbooks/inventory/mcp_inventory_validator.py +771 -134
- runbooks/inventory/mcp_vpc_validator.py +6 -0
- runbooks/inventory/organizations_discovery.py +17 -3
- runbooks/inventory/organizations_utils.py +553 -0
- runbooks/inventory/output_formatters.py +422 -0
- runbooks/inventory/recover_cfn_stack_ids.py +5 -5
- runbooks/inventory/run_on_multi_accounts.py +3 -3
- runbooks/inventory/tag_coverage.py +481 -0
- runbooks/inventory/validation_utils.py +358 -0
- runbooks/inventory/verify_ec2_security_groups.py +18 -5
- runbooks/inventory/vpc_architecture_validator.py +7 -1
- runbooks/inventory/vpc_dependency_analyzer.py +6 -0
- runbooks/main_final.py +2 -2
- runbooks/main_ultra_minimal.py +2 -2
- runbooks/mcp/integration.py +6 -4
- runbooks/remediation/acm_remediation.py +2 -2
- runbooks/remediation/cloudtrail_remediation.py +2 -2
- runbooks/remediation/cognito_remediation.py +2 -2
- runbooks/remediation/dynamodb_remediation.py +2 -2
- runbooks/remediation/ec2_remediation.py +2 -2
- runbooks/remediation/kms_remediation.py +2 -2
- runbooks/remediation/lambda_remediation.py +2 -2
- runbooks/remediation/rds_remediation.py +2 -2
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/vpc/cloudtrail_audit_integration.py +1 -1
- {runbooks-1.1.7.dist-info → runbooks-1.1.10.dist-info}/METADATA +74 -4
- {runbooks-1.1.7.dist-info → runbooks-1.1.10.dist-info}/RECORD +112 -105
- runbooks/__init__.py.backup +0 -134
- {runbooks-1.1.7.dist-info → runbooks-1.1.10.dist-info}/WHEEL +0 -0
- {runbooks-1.1.7.dist-info → runbooks-1.1.10.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.7.dist-info → runbooks-1.1.10.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.7.dist-info → runbooks-1.1.10.dist-info}/top_level.txt +0 -0
@@ -9,39 +9,50 @@ status analysis, organizational hierarchy mapping, and cross-organization accoun
|
|
9
9
|
**AWS API Mapping**: `organizations.list_accounts()`, `organizations.describe_organization()`
|
10
10
|
|
11
11
|
Features:
|
12
|
-
- Multi-organization account discovery
|
12
|
+
- Multi-organization account discovery via --all-profiles pattern
|
13
13
|
- Management Account identification and validation
|
14
14
|
- Account status tracking (ACTIVE, SUSPENDED, etc.)
|
15
15
|
- Cross-organization account lookup by ID
|
16
|
+
- Multi-format export (JSON, CSV, Markdown, Table)
|
16
17
|
- Short-form and detailed organizational views
|
17
18
|
- Root profile discovery and listing
|
18
19
|
- Account hierarchy visualization
|
19
20
|
|
21
|
+
Architecture (v1.1.10):
|
22
|
+
- Group-level with --all-profiles pattern (Option B)
|
23
|
+
- Shared utilities integration (organizations_utils.py + output_formatters.py)
|
24
|
+
- Modern CLI + Legacy Python Main dual compatibility
|
25
|
+
- Rich CLI output with enterprise UX standards
|
26
|
+
|
20
27
|
Compatibility:
|
21
28
|
- AWS Organizations with cross-account roles
|
22
29
|
- AWS Control Tower managed accounts
|
23
30
|
- Multiple AWS Organizations access
|
24
31
|
- AWS Account Factory provisioned accounts
|
32
|
+
- Single-account fallback for non-Organizations environments
|
25
33
|
|
26
|
-
Example:
|
27
|
-
|
34
|
+
Example (Modern CLI):
|
35
|
+
Multi-account Organizations discovery:
|
28
36
|
```bash
|
29
|
-
|
37
|
+
runbooks inventory --all-profiles $MANAGEMENT_PROFILE list-org-accounts
|
30
38
|
```
|
31
39
|
|
32
|
-
|
40
|
+
Brief listing with timing:
|
33
41
|
```bash
|
34
|
-
|
42
|
+
runbooks inventory --profile mgmt list-org-accounts --short --timing
|
35
43
|
```
|
36
44
|
|
37
|
-
Find
|
45
|
+
Find specific accounts across organizations:
|
38
46
|
```bash
|
39
|
-
|
47
|
+
runbooks inventory --all-profiles mgmt list-org-accounts --acct 123456789012 987654321098
|
40
48
|
```
|
41
49
|
|
42
|
-
|
50
|
+
Example (Legacy Python Main):
|
43
51
|
```bash
|
44
|
-
python
|
52
|
+
python src/runbooks/inventory/list_org_accounts.py --profile my-org-profile
|
53
|
+
python src/runbooks/inventory/list_org_accounts.py --profile my-profile --short
|
54
|
+
python src/runbooks/inventory/list_org_accounts.py --acct 123456789012 987654321098
|
55
|
+
python src/runbooks/inventory/list_org_accounts.py --rootonly
|
45
56
|
```
|
46
57
|
|
47
58
|
Use Cases:
|
@@ -60,30 +71,231 @@ Author:
|
|
60
71
|
AWS Cloud Foundations Team
|
61
72
|
|
62
73
|
Version:
|
63
|
-
|
74
|
+
1.1.10 (v1.1.10 parameter patterns + shared utilities)
|
64
75
|
"""
|
65
76
|
|
66
77
|
import logging
|
67
78
|
import sys
|
79
|
+
import json
|
68
80
|
from os.path import split
|
69
81
|
from time import time
|
82
|
+
from typing import Dict, List, Optional
|
83
|
+
|
84
|
+
from runbooks.inventory.ArgumentsClass import CommonArguments
|
85
|
+
from runbooks.inventory.organizations_utils import discover_organization_accounts
|
86
|
+
from runbooks.inventory.output_formatters import OrganizationsFormatter, export_to_file
|
87
|
+
from runbooks.common.rich_utils import (
|
88
|
+
console,
|
89
|
+
print_header,
|
90
|
+
print_success,
|
91
|
+
print_error,
|
92
|
+
print_warning,
|
93
|
+
print_info,
|
94
|
+
create_table,
|
95
|
+
)
|
96
|
+
from runbooks.common.config_loader import get_config_loader
|
97
|
+
from runbooks import __version__
|
98
|
+
|
99
|
+
logger = logging.getLogger(__name__)
|
70
100
|
|
71
|
-
|
101
|
+
begin_time = time()
|
72
102
|
|
73
|
-
# from botocore.exceptions import ClientError, NoCredentialsError, InvalidConfigError
|
74
|
-
from runbooks.common.rich_utils import console
|
75
|
-
from Inventory_Modules import display_results, get_org_accounts_from_profiles, get_profiles
|
76
103
|
|
77
|
-
|
78
|
-
|
104
|
+
##################
|
105
|
+
# Core Functions
|
106
|
+
##################
|
107
|
+
def list_organization_accounts(
|
108
|
+
profiles: List[str],
|
109
|
+
short_form: bool = False,
|
110
|
+
root_only: bool = False,
|
111
|
+
account_lookup: Optional[List[str]] = None,
|
112
|
+
export_format: str = "table",
|
113
|
+
output_file: Optional[str] = None,
|
114
|
+
skip_profiles: Optional[List[str]] = None,
|
115
|
+
verbose: int = logging.ERROR,
|
116
|
+
) -> Dict:
|
117
|
+
"""
|
118
|
+
List all accounts in AWS Organizations with multi-profile support.
|
119
|
+
|
120
|
+
This function serves both Modern CLI and legacy Python Main modes,
|
121
|
+
implementing the Group-Level with --all-profiles pattern (Option B).
|
122
|
+
|
123
|
+
Args:
|
124
|
+
profiles: List of AWS profiles to query (Organizations management accounts)
|
125
|
+
short_form: Show only profile-level info, skip child accounts (performance optimization)
|
126
|
+
root_only: Show only management accounts (governance focus)
|
127
|
+
account_lookup: Specific account IDs to find across organizations (cross-org search)
|
128
|
+
export_format: Output format (json, csv, markdown, table)
|
129
|
+
output_file: Output filename (None = console only)
|
130
|
+
skip_profiles: Profiles to exclude from discovery
|
131
|
+
verbose: Logging level (logging.ERROR, WARNING, INFO, DEBUG)
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Dictionary containing:
|
135
|
+
- OrgsFound: List of management account IDs discovered
|
136
|
+
- AccountList: Complete account inventory with metadata
|
137
|
+
- ClosedAccounts: Suspended/closed account IDs
|
138
|
+
- FailedProfiles: Profiles that failed authentication/access
|
139
|
+
- StandAloneAccounts: Non-organizational standalone accounts
|
140
|
+
|
141
|
+
Architecture:
|
142
|
+
- Uses organizations_utils.discover_organization_accounts() for AWS API integration
|
143
|
+
- Uses output_formatters.OrganizationsFormatter() for multi-format export
|
144
|
+
- Graceful fallback to single-account mode when Organizations unavailable
|
145
|
+
- Profile-level caching to avoid redundant API calls
|
146
|
+
"""
|
147
|
+
# Configure logging
|
148
|
+
logger.setLevel(verbose)
|
149
|
+
|
150
|
+
# Print header
|
151
|
+
print_header("Organizations Account Inventory", __version__)
|
152
|
+
|
153
|
+
# Filter profiles
|
154
|
+
active_profiles = [p for p in profiles if not skip_profiles or p not in skip_profiles]
|
155
|
+
|
156
|
+
if skip_profiles:
|
157
|
+
excluded_count = len(profiles) - len(active_profiles)
|
158
|
+
print_info(f"Excluding {excluded_count} profile(s): {', '.join(skip_profiles)}")
|
159
|
+
|
160
|
+
print_info(f"Scanning {len(active_profiles)} profile(s) for Organizations membership")
|
161
|
+
|
162
|
+
# Account discovery across profiles
|
163
|
+
all_accounts = []
|
164
|
+
orgs_found = set()
|
165
|
+
failed_profiles = []
|
166
|
+
profile_errors = {}
|
167
|
+
|
168
|
+
for profile in active_profiles:
|
169
|
+
try:
|
170
|
+
logger.info(f"Discovering accounts for profile: {profile}")
|
171
|
+
|
172
|
+
# Use shared utility for Organizations API discovery
|
173
|
+
accounts, error_msg = discover_organization_accounts(profile, region="us-east-1")
|
79
174
|
|
175
|
+
if error_msg:
|
176
|
+
# Fallback mode - single account
|
177
|
+
logger.warning(f"Profile {profile} fallback mode: {error_msg}")
|
178
|
+
profile_errors[profile] = error_msg
|
80
179
|
|
81
|
-
|
82
|
-
#
|
180
|
+
if accounts:
|
181
|
+
# Track management accounts
|
182
|
+
mgmt_accounts = [acc for acc in accounts if acc.get("is_management_account", False)]
|
183
|
+
for mgmt_acc in mgmt_accounts:
|
184
|
+
orgs_found.add(mgmt_acc["id"])
|
185
|
+
|
186
|
+
# Add profile metadata to each account
|
187
|
+
for account in accounts:
|
188
|
+
account["discovery_profile"] = profile
|
189
|
+
|
190
|
+
all_accounts.extend(accounts)
|
191
|
+
print_success(f"Profile '{profile}': {len(accounts)} accounts discovered")
|
192
|
+
else:
|
193
|
+
failed_profiles.append(profile)
|
194
|
+
print_error(f"Profile '{profile}': Discovery failed")
|
195
|
+
|
196
|
+
except Exception as e:
|
197
|
+
error_msg = str(e)
|
198
|
+
logger.error(f"Profile {profile} failed: {error_msg}", exc_info=True)
|
199
|
+
failed_profiles.append(profile)
|
200
|
+
profile_errors[profile] = error_msg
|
201
|
+
print_error(f"Profile '{profile}': {error_msg}")
|
202
|
+
|
203
|
+
# Summary statistics
|
204
|
+
console.print()
|
205
|
+
console.print("[cyan]📊 Discovery Summary:[/cyan]")
|
206
|
+
console.print(f"Organizations found: {len(orgs_found)}")
|
207
|
+
console.print(f"Total accounts: {len(all_accounts)}")
|
208
|
+
|
209
|
+
# Status breakdown
|
210
|
+
active_count = sum(1 for acc in all_accounts if acc.get("status") == "ACTIVE")
|
211
|
+
suspended_count = sum(1 for acc in all_accounts if acc.get("status") == "SUSPENDED")
|
212
|
+
closed_count = sum(1 for acc in all_accounts if acc.get("status") == "CLOSED")
|
213
|
+
|
214
|
+
console.print(f" - Active: {active_count}")
|
215
|
+
if suspended_count > 0:
|
216
|
+
console.print(f" - Suspended: {suspended_count}")
|
217
|
+
if closed_count > 0:
|
218
|
+
console.print(f" - Closed: {closed_count}")
|
219
|
+
|
220
|
+
if failed_profiles:
|
221
|
+
console.print(f"[yellow]Failed profiles: {len(failed_profiles)}[/yellow]")
|
222
|
+
console.print()
|
223
|
+
|
224
|
+
# Apply filters
|
225
|
+
if root_only:
|
226
|
+
all_accounts = [acc for acc in all_accounts if acc.get("is_management_account", False)]
|
227
|
+
print_info(f"Root-only filter: {len(all_accounts)} management accounts")
|
228
|
+
|
229
|
+
# Account lookup (cross-organization search)
|
230
|
+
if account_lookup:
|
231
|
+
found_accounts = [acc for acc in all_accounts if acc["id"] in account_lookup]
|
232
|
+
|
233
|
+
console.print("[cyan]🔍 Account Lookup Results:[/cyan]")
|
234
|
+
if found_accounts:
|
235
|
+
for acc in found_accounts:
|
236
|
+
org_id = acc.get("parent_org", "N/A")
|
237
|
+
console.print(
|
238
|
+
f" Account: [bold]{acc['id']}[/bold] | "
|
239
|
+
f"Name: {acc.get('name', 'N/A')} | "
|
240
|
+
f"Org: {org_id} | "
|
241
|
+
f"Status: {acc['status']} | "
|
242
|
+
f"Profile: {acc.get('profile', 'N/A')}"
|
243
|
+
)
|
244
|
+
else:
|
245
|
+
console.print(f"[yellow] No accounts found matching IDs: {', '.join(account_lookup)}[/yellow]")
|
246
|
+
console.print()
|
247
|
+
|
248
|
+
# Output formatting
|
249
|
+
formatter = OrganizationsFormatter()
|
250
|
+
|
251
|
+
if export_format == "table":
|
252
|
+
# Rich table for console display
|
253
|
+
table = formatter.format_accounts_table(all_accounts, title="AWS Organization Accounts")
|
254
|
+
console.print(table)
|
255
|
+
|
256
|
+
elif export_format == "json":
|
257
|
+
# JSON export with metadata
|
258
|
+
output_filename = output_file or "organizations_accounts.json"
|
259
|
+
metadata = {
|
260
|
+
"organizations_count": len(orgs_found),
|
261
|
+
"total_accounts": len(all_accounts),
|
262
|
+
"discovery_profiles": active_profiles,
|
263
|
+
"failed_profiles": failed_profiles,
|
264
|
+
}
|
265
|
+
formatter.export_json(all_accounts, output_filename, metadata=metadata)
|
266
|
+
|
267
|
+
elif export_format == "csv":
|
268
|
+
# CSV export
|
269
|
+
output_filename = output_file or "organizations_accounts.csv"
|
270
|
+
formatter.export_csv(all_accounts, output_filename)
|
271
|
+
|
272
|
+
elif export_format == "markdown":
|
273
|
+
# Markdown export
|
274
|
+
output_filename = output_file or "organizations_accounts.md"
|
275
|
+
formatter.export_markdown(all_accounts, output_filename, title="AWS Organization Accounts")
|
276
|
+
|
277
|
+
# Closed/suspended account tracking
|
278
|
+
closed_accounts = [acc["id"] for acc in all_accounts if acc["status"] != "ACTIVE"]
|
279
|
+
|
280
|
+
# Standalone account detection (accounts with no parent org)
|
281
|
+
standalone_accounts = [
|
282
|
+
acc["id"]
|
283
|
+
for acc in all_accounts
|
284
|
+
if acc.get("email") == "N/A" and acc.get("id") == acc.get("parent_org")
|
285
|
+
]
|
286
|
+
|
287
|
+
return {
|
288
|
+
"OrgsFound": list(orgs_found),
|
289
|
+
"AccountList": all_accounts,
|
290
|
+
"ClosedAccounts": closed_accounts,
|
291
|
+
"FailedProfiles": failed_profiles,
|
292
|
+
"StandAloneAccounts": standalone_accounts,
|
293
|
+
"ProfileErrors": profile_errors,
|
294
|
+
}
|
83
295
|
|
84
296
|
|
85
297
|
##################
|
86
|
-
#
|
298
|
+
# Legacy Python Main Entry Point
|
87
299
|
##################
|
88
300
|
def parse_args(f_arguments):
|
89
301
|
"""
|
@@ -103,7 +315,7 @@ def parse_args(f_arguments):
|
|
103
315
|
- pShortform: Brief output format (profiles only, not child accounts)
|
104
316
|
- accountList: Specific account IDs to lookup across organizations
|
105
317
|
- SkipProfiles: Profiles to exclude from discovery
|
106
|
-
- Filename: Output file prefix for
|
318
|
+
- Filename: Output file prefix for export
|
107
319
|
- Time: Enable execution timing measurements
|
108
320
|
- Other standard framework arguments
|
109
321
|
|
@@ -137,7 +349,7 @@ def parse_args(f_arguments):
|
|
137
349
|
# Add execution timing capabilities
|
138
350
|
parser.timing()
|
139
351
|
|
140
|
-
# Enable
|
352
|
+
# Enable file export functionality
|
141
353
|
parser.save_to_file()
|
142
354
|
|
143
355
|
# Configure logging verbosity levels
|
@@ -166,279 +378,97 @@ def parse_args(f_arguments):
|
|
166
378
|
"-A", "--acct", help="Find which Org this account is a part of", nargs="*", dest="accountList", default=None
|
167
379
|
)
|
168
380
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
f_Shortform: bool,
|
180
|
-
f_verbose,
|
181
|
-
):
|
182
|
-
"""
|
183
|
-
Execute comprehensive AWS Organizations discovery across multiple management accounts.
|
184
|
-
|
185
|
-
This is the core orchestration function that discovers and maps AWS Organizations
|
186
|
-
hierarchies, identifies management accounts, enumerates child accounts, and provides
|
187
|
-
detailed organizational visibility across multiple AWS Organizations.
|
188
|
-
|
189
|
-
Args:
|
190
|
-
f_Profiles (list): AWS profiles to analyze for organization membership
|
191
|
-
f_SkipProfiles (list): Profiles to exclude from discovery process
|
192
|
-
f_AccountList (list): Specific account IDs to lookup across organizations
|
193
|
-
f_Timing (bool): Enable execution timing measurements and display
|
194
|
-
f_RootOnly (bool): Limit output to Management Accounts only
|
195
|
-
f_SaveFilename (str): Output file prefix for CSV export (None for console only)
|
196
|
-
f_Shortform (bool): Brief output format excluding child account details
|
197
|
-
f_verbose: Logging verbosity level for detailed operational logging
|
198
|
-
|
199
|
-
Returns:
|
200
|
-
dict: Comprehensive organizational data containing:
|
201
|
-
- OrgsFound: List of Management Account IDs discovered
|
202
|
-
- StandAloneAccounts: Non-organizational standalone accounts
|
203
|
-
- ClosedAccounts: Suspended or closed account IDs
|
204
|
-
- FailedProfiles: Profiles that failed authentication/access
|
205
|
-
- AccountList: Complete account inventory with metadata
|
206
|
-
|
207
|
-
Processing Workflow:
|
208
|
-
1. Profile Resolution: Convert profile names to validated credential sets
|
209
|
-
2. Organization Discovery: Query each profile for organizational context
|
210
|
-
3. Account Enumeration: List all child accounts for Management Accounts
|
211
|
-
4. Status Analysis: Identify suspended, closed, or problematic accounts
|
212
|
-
5. Output Generation: Format results for console display or CSV export
|
213
|
-
6. Cross-Reference Lookup: Match requested account IDs to organizations
|
214
|
-
|
215
|
-
Output Formats:
|
216
|
-
- Console Mode: Formatted tables with colored output for status highlighting
|
217
|
-
- CSV Mode: Pipe-delimited files suitable for data analysis and reporting
|
218
|
-
- Short Mode: Profile-level summary without child account enumeration
|
219
|
-
- Lookup Mode: Targeted account-to-organization mapping
|
220
|
-
|
221
|
-
Error Handling:
|
222
|
-
- Profile failures are logged and tracked but don't stop processing
|
223
|
-
- Authentication errors are captured with detailed error messages
|
224
|
-
- Missing organizations are handled gracefully with fallback behavior
|
225
|
-
- API rate limits and throttling are managed through sequential processing
|
226
|
-
|
227
|
-
Performance Considerations:
|
228
|
-
- Sequential profile processing (no threading due to AWS API complexity)
|
229
|
-
- Cached organization data to avoid redundant API calls
|
230
|
-
- Progress indicators for long-running discovery operations
|
231
|
-
- Memory-efficient handling of large organizational hierarchies
|
232
|
-
|
233
|
-
Security Features:
|
234
|
-
- Read-only operations with minimal required permissions
|
235
|
-
- No credential storage or caching beyond execution scope
|
236
|
-
- Audit trail through comprehensive logging
|
237
|
-
- Safe handling of cross-account access patterns
|
238
|
-
"""
|
239
|
-
ProfileList = get_profiles(fSkipProfiles=f_SkipProfiles, fprofiles=f_Profiles)
|
240
|
-
# print("Capturing info for supplied profiles")
|
241
|
-
logging.info(f"These profiles were requested {f_Profiles}.")
|
242
|
-
logging.warning(f"These profiles are being checked {ProfileList}.")
|
243
|
-
print(f"Please bear with us as we run through {len(ProfileList)} profiles")
|
244
|
-
AllProfileAccounts = get_org_accounts_from_profiles(ProfileList)
|
245
|
-
AccountList = []
|
246
|
-
FailedProfiles = []
|
247
|
-
OrgsFound = []
|
248
|
-
|
249
|
-
# Print out the results
|
250
|
-
if f_Timing:
|
251
|
-
print()
|
252
|
-
print(f"It's taken [green]{time() - begin_time:.2f} seconds to find profile accounts...")
|
253
|
-
print()
|
254
|
-
fmt = "%-23s %-15s %-15s %-12s %-10s"
|
255
|
-
print("<------------------------------------>")
|
256
|
-
print(fmt % ("Profile Name", "Account Number", "Payer Org Acct", "Org ID", "Root Acct?"))
|
257
|
-
print(fmt % ("------------", "--------------", "--------------", "------", "----------"))
|
258
|
-
|
259
|
-
for item in AllProfileAccounts:
|
260
|
-
if not item["Success"]:
|
261
|
-
# If the profile failed, don't print anything and continue on.
|
262
|
-
FailedProfiles.append(item["profile"])
|
263
|
-
logging.error(f"{item['profile']} errored. Message: {item['ErrorMessage']}")
|
264
|
-
else:
|
265
|
-
if item["RootAcct"]:
|
266
|
-
# If the account is a root account, capture it for display later
|
267
|
-
OrgsFound.append(item["MgmtAccount"])
|
268
|
-
# Print results for all profiles
|
269
|
-
item["AccountId"] = item["aws_acct"].acct_number
|
270
|
-
item["AccountStatus"] = item["aws_acct"].AccountStatus
|
271
|
-
# item['AccountEmail'] = item['aws_acct'].
|
272
|
-
try:
|
273
|
-
if f_RootOnly and not item["RootAcct"]:
|
274
|
-
# If we're only looking for root accounts, and this isn't one, don't print anything and continue on.
|
275
|
-
continue
|
276
|
-
else:
|
277
|
-
logging.info(f"{item['profile']} was successful.")
|
278
|
-
print(
|
279
|
-
f"{Fore.RED if item['RootAcct'] else ''}{item['profile']:23s} {item['aws_acct'].acct_number:15s} {item['MgmtAccount']:15s} {str(item['OrgId']):12s} {item['RootAcct']}"
|
280
|
-
)
|
281
|
-
except TypeError as my_Error:
|
282
|
-
logging.error(f"Error - {my_Error} on {item}")
|
283
|
-
pass
|
284
|
-
|
285
|
-
"""
|
286
|
-
If I create a dictionary from the Root Accts and Root Profiles Lists -
|
287
|
-
I can use that to determine which profile belongs to the root user of my (child) account.
|
288
|
-
But this dictionary is only guaranteed to be valid after ALL profiles have been checked,
|
289
|
-
so... it doesn't solve our issue - unless we don't write anything to the screen until *everything* is done,
|
290
|
-
and we keep all output in another dictionary - where we can populate the missing data at the end...
|
291
|
-
but that takes a long time, since nothing would be sent to the screen in the meantime.
|
292
|
-
"""
|
293
|
-
|
294
|
-
print(ERASE_LINE)
|
295
|
-
print("-------------------")
|
296
|
-
|
297
|
-
if f_Shortform:
|
298
|
-
# The user specified "short-form" which means they don't want any information on child accounts.
|
299
|
-
return_response = {
|
300
|
-
"OrgsFound": OrgsFound,
|
301
|
-
"FailedProfiles": FailedProfiles,
|
302
|
-
"AllProfileAccounts": AllProfileAccounts,
|
303
|
-
}
|
304
|
-
else:
|
305
|
-
NumOfOrgAccounts = 0
|
306
|
-
ClosedAccounts = []
|
307
|
-
FailedAccounts = 0
|
308
|
-
account = dict()
|
309
|
-
ProfileNameLength = len("Organization's Profile")
|
310
|
-
|
311
|
-
for item in AllProfileAccounts:
|
312
|
-
# AllProfileAccounts holds the list of account class objects of the accounts associated with the profiles it found.
|
313
|
-
if item["Success"] and not item["RootAcct"]:
|
314
|
-
account.update(item["aws_acct"].ChildAccounts[0])
|
315
|
-
account.update({"Profile": item["profile"]})
|
316
|
-
AccountList.append(account.copy())
|
317
|
-
elif item["Success"] and item["RootAcct"]:
|
318
|
-
for child_acct in item["aws_acct"].ChildAccounts:
|
319
|
-
account.update(child_acct)
|
320
|
-
account.update({"Profile": item["profile"]})
|
321
|
-
ProfileNameLength = max(len(item["profile"]), ProfileNameLength)
|
322
|
-
AccountList.append(account.copy())
|
323
|
-
if not child_acct["AccountStatus"] == "ACTIVE":
|
324
|
-
ClosedAccounts.append(child_acct["AccountId"])
|
325
|
-
|
326
|
-
NumOfOrgAccounts += len(item["aws_acct"].ChildAccounts)
|
327
|
-
elif not item["Success"]:
|
328
|
-
FailedAccounts += 1
|
329
|
-
continue
|
330
|
-
|
331
|
-
# Display results on screen
|
332
|
-
if f_SaveFilename is None:
|
333
|
-
fmt = "%-23s %-15s"
|
334
|
-
print()
|
335
|
-
print(fmt % ("Organization's Profile", "Root Account"))
|
336
|
-
print(fmt % ("----------------------", "------------"))
|
337
|
-
for item in AllProfileAccounts:
|
338
|
-
if item["Success"] and item["RootAcct"]:
|
339
|
-
print(
|
340
|
-
f"{item['profile']:{ProfileNameLength}s} [bold]{item['MgmtAccount']:15s}"
|
341
|
-
)
|
342
|
-
print(
|
343
|
-
f"\t{'Child Account Number':{len('Child Account Number')}s} {'Child Account Status':{len('Child Account Status')}s} {'Child Email Address'}"
|
344
|
-
)
|
345
|
-
for child_acct in item["aws_acct"].ChildAccounts:
|
346
|
-
print(
|
347
|
-
f"\t{Fore.RED if not child_acct['AccountStatus'] == 'ACTIVE' else ''}{child_acct['AccountId']:{len('Child Account Number')}s} {child_acct['AccountStatus']:{len('Child Account Status')}s} {child_acct['AccountEmail']}"
|
348
|
-
)
|
349
|
-
|
350
|
-
elif f_SaveFilename is not None:
|
351
|
-
# The user specified a file name, which means they want a (pipe-delimited) CSV file with the relevant output.
|
352
|
-
display_dict = {
|
353
|
-
"MgmtAccount": {"DisplayOrder": 1, "Heading": "Parent Acct"},
|
354
|
-
"AccountId": {"DisplayOrder": 2, "Heading": "Account Number"},
|
355
|
-
"AccountStatus": {"DisplayOrder": 3, "Heading": "Account Status", "Condition": ["SUSPENDED", "CLOSED"]},
|
356
|
-
"AccountEmail": {"DisplayOrder": 4, "Heading": "Email"},
|
357
|
-
}
|
358
|
-
if pRootOnly:
|
359
|
-
sorted_Results = sorted(AllProfileAccounts, key=lambda d: (d["MgmtAccount"], d["AccountId"]))
|
360
|
-
else:
|
361
|
-
sorted_Results = sorted(AccountList, key=lambda d: (d["MgmtAccount"], d["AccountId"]))
|
362
|
-
display_results(sorted_Results, display_dict, "None", f_SaveFilename)
|
363
|
-
|
364
|
-
StandAloneAccounts = [
|
365
|
-
x["AccountId"]
|
366
|
-
for x in AccountList
|
367
|
-
if x["MgmtAccount"] == x["AccountId"] and x["AccountEmail"] == "Not an Org Management Account"
|
368
|
-
]
|
369
|
-
FailedProfiles = [i["profile"] for i in AllProfileAccounts if not i["Success"]]
|
370
|
-
OrgsFound = [i["MgmtAccount"] for i in AllProfileAccounts if i["RootAcct"]]
|
371
|
-
StandAloneAccounts.sort()
|
372
|
-
FailedProfiles.sort()
|
373
|
-
OrgsFound.sort()
|
374
|
-
ClosedAccounts.sort()
|
375
|
-
|
376
|
-
print()
|
377
|
-
print(f"Number of Organizations: {len(OrgsFound)}")
|
378
|
-
print(f"Number of Organization Accounts: {NumOfOrgAccounts}")
|
379
|
-
print(f"Number of Standalone Accounts: {len(StandAloneAccounts)}")
|
380
|
-
print(f"Number of suspended or closed accounts: {len(ClosedAccounts)}")
|
381
|
-
print(f"Number of profiles that failed: {len(FailedProfiles)}")
|
382
|
-
if f_verbose < 50:
|
383
|
-
print("----------------------")
|
384
|
-
print(f"The following accounts are the Org Accounts: {OrgsFound}")
|
385
|
-
print(f"The following accounts are Standalone: {StandAloneAccounts}") if len(
|
386
|
-
StandAloneAccounts
|
387
|
-
) > 0 else None
|
388
|
-
print(f"The following accounts are closed or suspended: {ClosedAccounts}") if len(
|
389
|
-
ClosedAccounts
|
390
|
-
) > 0 else None
|
391
|
-
print(f"The following profiles failed: {FailedProfiles}") if len(FailedProfiles) > 0 else None
|
392
|
-
print("----------------------")
|
393
|
-
print()
|
394
|
-
return_response = {
|
395
|
-
"OrgsFound": OrgsFound,
|
396
|
-
"StandAloneAccounts": StandAloneAccounts,
|
397
|
-
"ClosedAccounts": ClosedAccounts,
|
398
|
-
"FailedProfiles": FailedProfiles,
|
399
|
-
"AccountList": AccountList,
|
400
|
-
}
|
401
|
-
|
402
|
-
if f_AccountList is not None:
|
403
|
-
print(f"Found the requested account number{'' if len(AccountList) == 1 else 's'}:")
|
404
|
-
for acct in AccountList:
|
405
|
-
if acct["AccountId"] in f_AccountList:
|
406
|
-
print(
|
407
|
-
f"Profile: {acct['Profile']} | Org: {acct['MgmtAccount']} | Account: {acct['AccountId']} | Status: {acct['AccountStatus']} | Email: {acct['AccountEmail']}"
|
408
|
-
)
|
381
|
+
# Tag mappings configuration (v1.1.10 config-aware feature)
|
382
|
+
local.add_argument(
|
383
|
+
"--tag-mappings",
|
384
|
+
type=str,
|
385
|
+
default=None,
|
386
|
+
dest="tagMappings",
|
387
|
+
help='JSON string mapping field names to AWS tag keys. '
|
388
|
+
'Example: \'{"wbs_code": "ProjectCode", "cost_group": "BillingGroup"}\'. '
|
389
|
+
'Overrides hierarchical config (user/project/env defaults).',
|
390
|
+
)
|
409
391
|
|
410
|
-
return
|
392
|
+
return parser.my_parser.parse_args(f_arguments)
|
411
393
|
|
412
394
|
|
413
395
|
##################
|
414
396
|
# Main
|
415
397
|
##################
|
416
|
-
|
417
398
|
if __name__ == "__main__":
|
418
399
|
args = parse_args(sys.argv[1:])
|
419
400
|
|
401
|
+
# Extract arguments
|
420
402
|
pProfiles = args.Profiles
|
421
403
|
pRootOnly = args.RootOnly
|
422
404
|
pTiming = args.Time
|
423
|
-
pSkipAccounts = args.SkipAccounts
|
424
405
|
pSkipProfiles = args.SkipProfiles
|
425
406
|
verbose = args.loglevel
|
426
407
|
pSaveFilename = args.Filename
|
427
408
|
pShortform = args.pShortform
|
428
409
|
pAccountList = args.accountList
|
410
|
+
pTagMappings = args.tagMappings
|
411
|
+
|
412
|
+
# Configure logging
|
429
413
|
logging.basicConfig(
|
430
|
-
level=verbose, format="[%(filename)s:%(lineno)s - %(
|
414
|
+
level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s"
|
431
415
|
)
|
432
|
-
logging.getLogger("boto3").setLevel(logging.CRITICAL)
|
433
|
-
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
434
|
-
logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
|
435
|
-
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
436
416
|
|
437
|
-
|
417
|
+
# Suppress AWS SDK noise unless in DEBUG mode
|
418
|
+
if verbose > logging.DEBUG:
|
419
|
+
for logger_name in ["boto3", "botocore", "s3transfer", "urllib3"]:
|
420
|
+
logging.getLogger(logger_name).setLevel(logging.CRITICAL)
|
421
|
+
|
422
|
+
# Parse CLI tag mappings (if provided)
|
423
|
+
cli_tag_overrides = None
|
424
|
+
if pTagMappings:
|
425
|
+
try:
|
426
|
+
cli_tag_overrides = json.loads(pTagMappings)
|
427
|
+
logger.info(f"Using CLI tag mapping overrides: {cli_tag_overrides}")
|
428
|
+
except json.JSONDecodeError as e:
|
429
|
+
console.print(f"[red]Error: Invalid JSON in --tag-mappings: {e}[/red]")
|
430
|
+
console.print("[yellow]Example: --tag-mappings '{\"wbs_code\": \"ProjectCode\", \"cost_group\": \"BillingGroup\"}'[/yellow]")
|
431
|
+
sys.exit(1)
|
432
|
+
|
433
|
+
# Load tag mappings with hierarchical precedence
|
434
|
+
config_loader = get_config_loader()
|
435
|
+
final_tag_mappings = config_loader.load_tag_mappings(cli_overrides=cli_tag_overrides)
|
436
|
+
|
437
|
+
# Display configuration sources
|
438
|
+
config_sources = config_loader.get_config_sources()
|
439
|
+
if verbose <= logging.INFO:
|
440
|
+
print_info(f"Tag mapping sources: {' → '.join(config_sources)}")
|
441
|
+
logger.info(f"Loaded {len(final_tag_mappings)} tag mappings from {len(config_sources)} sources")
|
442
|
+
|
443
|
+
# Determine export format based on filename
|
444
|
+
export_format = "table"
|
445
|
+
if pSaveFilename:
|
446
|
+
if pSaveFilename.endswith(".json"):
|
447
|
+
export_format = "json"
|
448
|
+
elif pSaveFilename.endswith(".csv"):
|
449
|
+
export_format = "csv"
|
450
|
+
elif pSaveFilename.endswith(".md"):
|
451
|
+
export_format = "markdown"
|
452
|
+
else:
|
453
|
+
# Default to CSV if no extension provided
|
454
|
+
export_format = "csv"
|
455
|
+
pSaveFilename = f"{pSaveFilename}.csv"
|
456
|
+
|
457
|
+
# Execute discovery
|
458
|
+
results = list_organization_accounts(
|
459
|
+
profiles=pProfiles,
|
460
|
+
short_form=pShortform,
|
461
|
+
root_only=pRootOnly,
|
462
|
+
account_lookup=pAccountList,
|
463
|
+
export_format=export_format,
|
464
|
+
output_file=pSaveFilename,
|
465
|
+
skip_profiles=pSkipProfiles,
|
466
|
+
verbose=verbose,
|
467
|
+
)
|
438
468
|
|
439
|
-
|
469
|
+
# Timing summary
|
440
470
|
if pTiming:
|
441
|
-
|
442
|
-
print()
|
443
|
-
|
444
|
-
print()
|
471
|
+
elapsed = time() - begin_time
|
472
|
+
console.print(f"\n[green]⏱️ Execution time: {elapsed:.2f}s[/green]")
|
473
|
+
|
474
|
+
console.print("\n[dim]Thanks for using this script[/dim]\n")
|