runbooks 0.2.5__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +26 -0
- jupyter-agent/.env +2 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/.gradio/certificate.pem +31 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/__main__.log +8 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
- jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/README.md +337 -0
- runbooks/finops/__init__.py +86 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +619 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +738 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +1004 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security/README.md +447 -0
- runbooks/security/__init__.py +71 -0
- runbooks/{security_baseline → security}/checklist/alternate_contacts.py +8 -1
- runbooks/{security_baseline → security}/checklist/bucket_public_access.py +4 -1
- runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +9 -2
- runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +5 -1
- runbooks/{security_baseline → security}/checklist/root_access_key.py +6 -1
- runbooks/{security_baseline → security}/config-origin.json +1 -1
- runbooks/{security_baseline → security}/config.json +1 -1
- runbooks/{security_baseline → security}/permission.json +1 -1
- runbooks/{security_baseline → security}/report_generator.py +10 -2
- runbooks/{security_baseline → security}/report_template_en.html +7 -7
- runbooks/{security_baseline → security}/report_template_jp.html +7 -7
- runbooks/{security_baseline → security}/report_template_kr.html +12 -12
- runbooks/{security_baseline → security}/report_template_vn.html +7 -7
- runbooks/{security_baseline → security}/run_script.py +8 -2
- runbooks/{security_baseline → security}/security_baseline_tester.py +12 -4
- runbooks/{security_baseline → security}/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.7.0.dist-info/METADATA +375 -0
- runbooks-0.7.0.dist-info/RECORD +249 -0
- {runbooks-0.2.5.dist-info → runbooks-0.7.0.dist-info}/WHEEL +1 -1
- runbooks-0.7.0.dist-info/entry_points.txt +7 -0
- runbooks-0.7.0.dist-info/licenses/LICENSE +201 -0
- runbooks-0.7.0.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.5.dist-info/METADATA +0 -439
- runbooks-0.2.5.dist-info/RECORD +0 -61
- runbooks-0.2.5.dist-info/entry_points.txt +0 -3
- runbooks-0.2.5.dist-info/top_level.txt +0 -1
- /runbooks/{security_baseline/__init__.py → inventory/tests/script_test_data.py} +0 -0
- /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
- /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
- /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
- /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
- /runbooks/{security_baseline → security}/utils/enums.py +0 -0
- /runbooks/{security_baseline → security}/utils/language.py +0 -0
- /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
- /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
"""
|
2
|
+
Enterprise AWS Inventory System.
|
3
|
+
|
4
|
+
This module provides comprehensive AWS resource discovery and inventory
|
5
|
+
capabilities across multiple accounts and regions with enterprise-grade
|
6
|
+
architecture, validation, and automation.
|
7
|
+
|
8
|
+
Architecture:
|
9
|
+
- core/: Main business logic and orchestration
|
10
|
+
- collectors/: Specialized resource collectors by service category
|
11
|
+
- models/: Pydantic data models with validation
|
12
|
+
- utils/: Reusable utilities and helpers
|
13
|
+
- legacy/: Deprecated scripts for backward compatibility
|
14
|
+
|
15
|
+
Components:
|
16
|
+
- InventoryCollector: Main orchestration engine
|
17
|
+
- InventoryFormatter: Multi-format output handling
|
18
|
+
- BaseResourceCollector: Abstract base for all collectors
|
19
|
+
- AWSResource/AWSAccount: Core data models
|
20
|
+
- Validation utilities and AWS helpers
|
21
|
+
"""
|
22
|
+
|
23
|
+
# Core components
|
24
|
+
# Base collector for extending
|
25
|
+
from runbooks.inventory.collectors.base import BaseResourceCollector
|
26
|
+
from runbooks.inventory.core.collector import InventoryCollector
|
27
|
+
from runbooks.inventory.core.formatter import InventoryFormatter
|
28
|
+
|
29
|
+
# Data models
|
30
|
+
from runbooks.inventory.models.account import AWSAccount, OrganizationAccount
|
31
|
+
from runbooks.inventory.models.inventory import InventoryMetadata, InventoryResult
|
32
|
+
from runbooks.inventory.models.resource import AWSResource, ResourceState, ResourceType
|
33
|
+
from runbooks.inventory.utils.aws_helpers import get_boto3_session, validate_aws_credentials
|
34
|
+
|
35
|
+
# Utilities
|
36
|
+
from runbooks.inventory.utils.validation import validate_aws_account_id, validate_resource_types
|
37
|
+
|
38
|
+
__version__ = "1.0.0"
|
39
|
+
|
40
|
+
__all__ = [
|
41
|
+
# Core functionality
|
42
|
+
"InventoryCollector",
|
43
|
+
"InventoryFormatter",
|
44
|
+
# Base classes for extension
|
45
|
+
"BaseResourceCollector",
|
46
|
+
# Data models
|
47
|
+
"AWSAccount",
|
48
|
+
"OrganizationAccount",
|
49
|
+
"AWSResource",
|
50
|
+
"InventoryResult",
|
51
|
+
"InventoryMetadata",
|
52
|
+
# Enums
|
53
|
+
"ResourceState",
|
54
|
+
"ResourceType",
|
55
|
+
# Utilities
|
56
|
+
"validate_aws_account_id",
|
57
|
+
"validate_resource_types",
|
58
|
+
"get_boto3_session",
|
59
|
+
"validate_aws_credentials",
|
60
|
+
# Version
|
61
|
+
"__version__",
|
62
|
+
]
|
@@ -0,0 +1,532 @@
|
|
1
|
+
"""
|
2
|
+
1. Accept either a single profile or multiple profiles
|
3
|
+
2. Determine if a profile (or multiple profiles) was provided
|
4
|
+
3. If a single profile was provided - determine whether it's been provided as an org account, or as a single profile
|
5
|
+
4. If the profile is of a root account and it's supposed to be for the whole Org - **note that**
|
6
|
+
Otherwise - treat it like a standalone account (like anything else)
|
7
|
+
5. If it's a root account, we need to figure out how to find all the child accounts, and the proper roles to access them by
|
8
|
+
5a. Find all the child accounts
|
9
|
+
5b. Find out if any of those children are SUSPENDED and remove them from the list
|
10
|
+
5c. Figure out the right roles to access the children by - which might be a config file, since there might be a mapping for this.
|
11
|
+
5d. Once we have a way to access all the children, we can provide account-credentials to access the children by (but likely not the root account itself)
|
12
|
+
5e. Call the actual target scripts - with the proper credentials (which might be a profile, or might be a session token)
|
13
|
+
6. If it's not a root account - then ... just use it as a profile
|
14
|
+
|
15
|
+
What does a script need to satisfy credentials? It needs a boto3 session. From the session, everything else can derive... yes?
|
16
|
+
|
17
|
+
So if we create a class object that represented the account:
|
18
|
+
Attributes:
|
19
|
+
AccountID: Its 12 digit account number
|
20
|
+
botoClient: Access into the account (profile, or access via a root path)
|
21
|
+
MgmntAccessRoles: The role that the root account uses to get access
|
22
|
+
AccountStatus: Whether it's ACTIVE or SUSPENDED
|
23
|
+
AccountType: Whether it's a root org account, a child account or a standalone account
|
24
|
+
ParentProfile: What its parent profile name is, if available
|
25
|
+
If it's an Org account:
|
26
|
+
ALZ: Whether the Org is running an ALZ
|
27
|
+
CT: Whether the Org is running CT
|
28
|
+
Functions:
|
29
|
+
Which regions and partitions it's enabled for
|
30
|
+
(Could all my inventory items be an attribute of this class?)
|
31
|
+
|
32
|
+
"""
|
33
|
+
|
34
|
+
import logging
|
35
|
+
from json.decoder import JSONDecodeError
|
36
|
+
|
37
|
+
import boto3
|
38
|
+
from botocore.exceptions import (
|
39
|
+
ClientError,
|
40
|
+
ConnectionError,
|
41
|
+
CredentialRetrievalError,
|
42
|
+
EndpointConnectionError,
|
43
|
+
NoCredentialsError,
|
44
|
+
ProfileNotFound,
|
45
|
+
UnknownRegionError,
|
46
|
+
)
|
47
|
+
|
48
|
+
__version__ = "2024.03.22" # (again)
|
49
|
+
|
50
|
+
|
51
|
+
def _validate_region(faws_prelim_session, fRegion=None):
|
52
|
+
# Why are you trying to validate a region, and then didn't supply a region?
|
53
|
+
# Or - common case - you supplied 'us-east-1' which we know to be valid, so we can just immediately return Success
|
54
|
+
if fRegion is None or fRegion == "us-east-1":
|
55
|
+
message = f"Either no region supplied to check or region is 'us-east-1'. Defaulting to 'us-east-1'"
|
56
|
+
logging.info(message)
|
57
|
+
fRegion = "us-east-1"
|
58
|
+
result = {"Success": True, "Message": message, "Region": fRegion}
|
59
|
+
return result
|
60
|
+
else:
|
61
|
+
try:
|
62
|
+
# Since we have to run this command to get a listing of the possible regions, we have to use a region we know will work today...
|
63
|
+
client_region = faws_prelim_session.client("ec2", region_name="us-east-1")
|
64
|
+
# all_regions_list = [region_name['RegionName'] for region_name in client_region.describe_regions(AllRegions=True)['Regions']]
|
65
|
+
matching_regions = client_region.describe_regions(Filters=[{"Name": "region-name", "Values": [fRegion]}])[
|
66
|
+
"Regions"
|
67
|
+
]
|
68
|
+
except Exception as my_Error:
|
69
|
+
message = f"Problem happened.\nError Message: {my_Error}"
|
70
|
+
result = {"Success": False, "Message": message, "Region": fRegion}
|
71
|
+
return result
|
72
|
+
if matching_regions:
|
73
|
+
message = f"{fRegion} is a valid region within AWS"
|
74
|
+
result = {"Success": True, "Message": message, "Region": fRegion}
|
75
|
+
if matching_regions[0]["OptInStatus"] == "not-opted-in":
|
76
|
+
message = f"{fRegion} is a valid region within AWS, but this account hasn't opted into this region"
|
77
|
+
result = {"Success": False, "Message": message, "Region": fRegion}
|
78
|
+
logging.info(message)
|
79
|
+
else:
|
80
|
+
message = f"'{fRegion}' is not valid region within this AWS partition"
|
81
|
+
logging.info(message)
|
82
|
+
result = {"Success": False, "Message": message, "Region": fRegion}
|
83
|
+
return result
|
84
|
+
|
85
|
+
|
86
|
+
class aws_acct_access:
|
87
|
+
"""
|
88
|
+
Class takes a boto3 session object as input
|
89
|
+
Multiple attributes and functions exist within this class to give you information about the account
|
90
|
+
Attributes:
|
91
|
+
AccountStatus: Whether the account is Active or Inactive
|
92
|
+
acct_number: The account number of the account
|
93
|
+
AccountType: Whether the account is a "Root", "Child" or "Standalone" account
|
94
|
+
MgmtAccount: If the account is a child, this is its Management Account
|
95
|
+
OrgID: The Organization the account belongs to, if it does
|
96
|
+
MgmtEmail: The email address of the Management Account, if the account is a "Root" or "Child"
|
97
|
+
creds: The credentials used to get into the account
|
98
|
+
Region: The region used to authenticate into this account. Important to find out if certain regions are allowed (opted-in).
|
99
|
+
ChildAccounts: If the account is a "Root", this is a listing of the child accounts
|
100
|
+
"""
|
101
|
+
|
102
|
+
def __init__(self, fProfile=None, fRegion=None, ocredentials=None):
|
103
|
+
# logging.basicConfig(level=20, format="[%(filename)s:%(lineno)s - %(funcName)s() ] %(message)s")
|
104
|
+
# First thing's first: We need to validate that the region they sent us to use is valid for this account.
|
105
|
+
# Otherwise, all hell will break if it's not.
|
106
|
+
UsingKeys = False
|
107
|
+
UsingSessionToken = False
|
108
|
+
if fRegion is None:
|
109
|
+
fRegion = "us-east-1"
|
110
|
+
account_access_successful = False
|
111
|
+
account_and_region_access_successful = False
|
112
|
+
if ocredentials is not None and ocredentials["Success"]:
|
113
|
+
# Trying to instantiate a class, based on passed in credentials
|
114
|
+
UsingKeys = True
|
115
|
+
UsingSessionToken = False
|
116
|
+
if "SessionToken" in ocredentials:
|
117
|
+
# Using a token-based role
|
118
|
+
UsingSessionToken = True
|
119
|
+
prelim_session = boto3.Session(
|
120
|
+
aws_access_key_id=ocredentials["AccessKeyId"],
|
121
|
+
aws_secret_access_key=ocredentials["SecretAccessKey"],
|
122
|
+
aws_session_token=ocredentials["SessionToken"],
|
123
|
+
region_name="us-east-1",
|
124
|
+
)
|
125
|
+
account_access_successful = True
|
126
|
+
else:
|
127
|
+
# Not using a token-based role
|
128
|
+
prelim_session = boto3.Session(
|
129
|
+
aws_access_key_id=ocredentials["AccessKeyId"],
|
130
|
+
aws_secret_access_key=ocredentials["SecretAccessKey"],
|
131
|
+
region_name="us-east-1",
|
132
|
+
)
|
133
|
+
account_access_successful = True
|
134
|
+
else:
|
135
|
+
# Not trying to use account_key_credentials
|
136
|
+
try:
|
137
|
+
logging.debug("Credentials are using a profile")
|
138
|
+
# Checking to see if a region was included in the profile, if it was, then use it, otherwise - pick a default.
|
139
|
+
prelim_session = boto3.Session(profile_name=fProfile)
|
140
|
+
if prelim_session.region_name is None:
|
141
|
+
prelim_session = boto3.Session(profile_name=fProfile, region_name=fRegion)
|
142
|
+
elif fRegion is None:
|
143
|
+
fRegion = prelim_session.region_name
|
144
|
+
self.session = prelim_session
|
145
|
+
try:
|
146
|
+
result = self.session.client("ec2").describe_regions()
|
147
|
+
account_access_successful = True
|
148
|
+
account_and_region_access_successful = True
|
149
|
+
except JSONDecodeError as my_Error:
|
150
|
+
error_message = (
|
151
|
+
f"Failed to authenticate to AWS using {fProfile}\nProbably a profile that doesn't work..."
|
152
|
+
)
|
153
|
+
logging.error(f"Error: {error_message}")
|
154
|
+
account_access_successful = False
|
155
|
+
account_and_region_access_successful = False
|
156
|
+
except Exception as my_Error:
|
157
|
+
error_message = f"Failed to authenticate to AWS using {fProfile}\nUnknown reason"
|
158
|
+
logging.error(f"Error: {error_message}")
|
159
|
+
account_access_successful = False
|
160
|
+
account_and_region_access_successful = False
|
161
|
+
except ProfileNotFound as my_Error:
|
162
|
+
ErrorMessage = (
|
163
|
+
f"The profile '{fProfile}' wasn't found. Perhaps there was a typo? Error Message: {my_Error}"
|
164
|
+
)
|
165
|
+
account_access_successful = False
|
166
|
+
account_and_region_access_successful = False
|
167
|
+
|
168
|
+
if account_access_successful:
|
169
|
+
result = _validate_region(prelim_session, fRegion)
|
170
|
+
if result["Success"] is True:
|
171
|
+
if UsingSessionToken:
|
172
|
+
logging.debug("Credentials are using SessionToken")
|
173
|
+
self.session = boto3.Session(
|
174
|
+
aws_access_key_id=ocredentials["AccessKeyId"],
|
175
|
+
aws_secret_access_key=ocredentials["SecretAccessKey"],
|
176
|
+
aws_session_token=ocredentials["SessionToken"],
|
177
|
+
region_name=result["Region"],
|
178
|
+
)
|
179
|
+
account_and_region_access_successful = True
|
180
|
+
self.AccountStatus = "ACTIVE"
|
181
|
+
elif UsingKeys:
|
182
|
+
logging.debug("Credentials are using Keys, but no SessionToken")
|
183
|
+
self.session = boto3.Session(
|
184
|
+
aws_access_key_id=ocredentials["AccessKeyId"],
|
185
|
+
aws_secret_access_key=ocredentials["SecretAccessKey"],
|
186
|
+
region_name=result["Region"],
|
187
|
+
)
|
188
|
+
account_and_region_access_successful = True
|
189
|
+
self.AccountStatus = "ACTIVE"
|
190
|
+
else:
|
191
|
+
self.AccountStatus = "ACTIVE"
|
192
|
+
logging.info(f"They're using a profile, which was checked above...")
|
193
|
+
else:
|
194
|
+
logging.error(result["Message"])
|
195
|
+
account_access_successful = False
|
196
|
+
account_and_region_access_successful = False
|
197
|
+
elif account_and_region_access_successful:
|
198
|
+
self.AccountStatus = "ACTIVE"
|
199
|
+
pass
|
200
|
+
|
201
|
+
logging.info(f"Capturing Account Information for profile {fProfile}...")
|
202
|
+
if account_and_region_access_successful:
|
203
|
+
logging.info(f"Successfully validated access to account in region {fRegion}")
|
204
|
+
self.Success = True
|
205
|
+
self.acct_number = self.acct_num()
|
206
|
+
self._AccountAttributes = self.find_account_attr()
|
207
|
+
logging.info(f"Found {len(self._AccountAttributes)} attributes in this account")
|
208
|
+
self.AccountType = self._AccountAttributes["AccountType"]
|
209
|
+
self.MgmtAccount = self._AccountAttributes["MasterAccountId"]
|
210
|
+
self.OrgID = self._AccountAttributes["OrgId"]
|
211
|
+
self.MgmtEmail = self._AccountAttributes["ManagementEmail"]
|
212
|
+
logging.info(f"Account {self.acct_number} is a {self.AccountType} account")
|
213
|
+
self.Region = fRegion
|
214
|
+
self.ErrorType = None
|
215
|
+
self.creds = self.session._session._credentials.get_frozen_credentials()
|
216
|
+
self.credentials = dict()
|
217
|
+
self.credentials.update(
|
218
|
+
{
|
219
|
+
"AccessKeyId": self.creds[0],
|
220
|
+
"SecretAccessKey": self.creds[1],
|
221
|
+
"SessionToken": self.creds[2],
|
222
|
+
"AccountNumber": self.acct_number,
|
223
|
+
"AccountId": self.acct_number,
|
224
|
+
"MgmtAccount": self.MgmtAccount,
|
225
|
+
"Region": fRegion,
|
226
|
+
"Profile": fProfile if fProfile is not None else None,
|
227
|
+
}
|
228
|
+
)
|
229
|
+
if self.AccountType.lower() == "root":
|
230
|
+
logging.info("Enumerating all of the child accounts")
|
231
|
+
self.ChildAccounts = self.find_child_accounts()
|
232
|
+
logging.debug(
|
233
|
+
f"As acct {self.acct_number} is the root account, we found {len(self.ChildAccounts)} accounts in the Org"
|
234
|
+
)
|
235
|
+
|
236
|
+
else:
|
237
|
+
logging.warning("Account isn't a root account, but we're looking for children anyway...")
|
238
|
+
self.ChildAccounts = self.find_child_accounts()
|
239
|
+
elif fProfile is not None and not account_access_successful: # Likely the problem was the profile passed in
|
240
|
+
logging.error(f"Profile {fProfile} failed to successfully access an account")
|
241
|
+
self.AccountType = "Unknown"
|
242
|
+
self.MgmtAccount = "Unknown"
|
243
|
+
self.OrgID = "Unknown"
|
244
|
+
self.MgmtEmail = "Unknown"
|
245
|
+
self.Region = fRegion
|
246
|
+
self.ChildAccounts = [
|
247
|
+
{
|
248
|
+
"AccountEmail": "ProfileFailed@doesnt.work",
|
249
|
+
"AccountId": "012345678912",
|
250
|
+
"AccountStatus": None,
|
251
|
+
"MgmtAccount": "012345678912",
|
252
|
+
}
|
253
|
+
]
|
254
|
+
self.Profile = fProfile if fProfile is not None else None
|
255
|
+
self.creds = "Unknown"
|
256
|
+
self.credentials = "Unknown"
|
257
|
+
self.ErrorType = "Invalid profile"
|
258
|
+
self.Success = False
|
259
|
+
logging.error(f"Profile {fProfile} doesn't seem to work...")
|
260
|
+
# raise NoCredentialsError
|
261
|
+
elif fProfile is not None and account_access_successful: # Likely the problem was the region passed in
|
262
|
+
logging.error(f"Region '{fRegion}' wasn't valid. Please specify a valid region.")
|
263
|
+
self.AccountType = "Unknown"
|
264
|
+
self.MgmtAccount = "Unknown"
|
265
|
+
self.OrgID = "Unknown"
|
266
|
+
self.MgmtEmail = "Unknown"
|
267
|
+
self.Region = fRegion
|
268
|
+
self.creds = "Unknown"
|
269
|
+
self.credentials = "Unknown"
|
270
|
+
self.ErrorType = "Invalid region"
|
271
|
+
self.Success = False
|
272
|
+
# raise UnknownRegionError(region_name=fRegion)
|
273
|
+
elif ocredentials is not None:
|
274
|
+
logging.error(
|
275
|
+
f"Credentials for access_key {ocredentials['AccountNum']} failed to successfully access an account"
|
276
|
+
)
|
277
|
+
self.AccountType = "Unknown"
|
278
|
+
self.MgmtAccount = "Unknown"
|
279
|
+
self.OrgID = "Unknown"
|
280
|
+
self.MgmtEmail = "Unknown"
|
281
|
+
self.Region = fRegion
|
282
|
+
self.creds = "Unknown"
|
283
|
+
self.credentials = "Unknown"
|
284
|
+
self.ErrorType = "Invalid credentials"
|
285
|
+
self.Success = False
|
286
|
+
# raise CredentialRetrievalError
|
287
|
+
else:
|
288
|
+
logging.error(f"Not sure how we got here... Call your admin for help?")
|
289
|
+
self.AccountType = "Unknown"
|
290
|
+
self.MgmtAccount = "Unknown"
|
291
|
+
self.OrgID = "Unknown"
|
292
|
+
self.MgmtEmail = "Unknown"
|
293
|
+
self.Region = fRegion
|
294
|
+
self.creds = "Unknown"
|
295
|
+
self.credentials = "Unknown"
|
296
|
+
self.ErrorType = "Unknown"
|
297
|
+
self.Success = False
|
298
|
+
|
299
|
+
def acct_num(self):
|
300
|
+
"""
|
301
|
+
This function returns a string of the account's 12 digit account number
|
302
|
+
"""
|
303
|
+
import logging
|
304
|
+
|
305
|
+
from botocore.exceptions import ClientError, CredentialRetrievalError
|
306
|
+
|
307
|
+
try:
|
308
|
+
aws_session = self.session
|
309
|
+
logging.info(f"Accessing session object to find its account number")
|
310
|
+
client_sts = aws_session.client("sts")
|
311
|
+
response = client_sts.get_caller_identity()
|
312
|
+
logging.info(f"response: {response}")
|
313
|
+
creds = response["Account"]
|
314
|
+
except JSONDecodeError as my_Error:
|
315
|
+
error_message = (
|
316
|
+
f"There was a JSON Decode Error while using sts to gain access to account\n"
|
317
|
+
f"This is most often associated with a profile that doesn't work to gain access to the account it's made for."
|
318
|
+
)
|
319
|
+
logging.error(f"{error_message}\nError Message: {my_Error}")
|
320
|
+
pass
|
321
|
+
creds = "Failure"
|
322
|
+
except ClientError as my_Error:
|
323
|
+
if str(my_Error).find("UnrecognizedClientException") > 0:
|
324
|
+
logging.info(f"Security Issue")
|
325
|
+
pass
|
326
|
+
elif str(my_Error).find("InvalidClientTokenId") > 0:
|
327
|
+
logging.info(f"Security Token is bad - probably a bad entry in config")
|
328
|
+
pass
|
329
|
+
else:
|
330
|
+
print(my_Error)
|
331
|
+
logging.info(f"Other kind of failure for boto3 access in acct")
|
332
|
+
pass
|
333
|
+
creds = "Failure"
|
334
|
+
except CredentialRetrievalError as my_Error:
|
335
|
+
if str(my_Error).find("custom-process") > 0:
|
336
|
+
logging.info(f"Profile requires custom authentication")
|
337
|
+
pass
|
338
|
+
else:
|
339
|
+
print(my_Error)
|
340
|
+
pass
|
341
|
+
creds = "Failure"
|
342
|
+
return creds
|
343
|
+
|
344
|
+
def find_account_attr(self):
|
345
|
+
import logging
|
346
|
+
|
347
|
+
from botocore.exceptions import ClientError, CredentialRetrievalError
|
348
|
+
|
349
|
+
"""
|
350
|
+
In the case of an Org Root or Child account, I use the response directly from the AWS SDK.
|
351
|
+
You can find the output format here: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/organizations.html#Organizations.Client.describe_organization
|
352
|
+
"""
|
353
|
+
function_response = {
|
354
|
+
"AccountType": "Unknown",
|
355
|
+
"AccountNumber": None,
|
356
|
+
"OrgId": None,
|
357
|
+
"Id": None,
|
358
|
+
"MasterAccountId": None,
|
359
|
+
"MgmtAccountId": None,
|
360
|
+
"ManagementEmail": None,
|
361
|
+
}
|
362
|
+
try:
|
363
|
+
session_org = self.session
|
364
|
+
client_org = session_org.client("organizations")
|
365
|
+
response_pre = client_org.describe_organization()
|
366
|
+
response = response_pre["Organization"]
|
367
|
+
function_response["OrgId"] = response["Id"]
|
368
|
+
function_response["Id"] = self.acct_number
|
369
|
+
function_response["AccountNumber"] = self.acct_number
|
370
|
+
function_response["MasterAccountId"] = response["MasterAccountId"]
|
371
|
+
function_response["MgmtAccountId"] = response["MasterAccountId"]
|
372
|
+
function_response["ManagementEmail"] = response["MasterAccountEmail"]
|
373
|
+
if response["MasterAccountId"] == self.acct_number:
|
374
|
+
function_response["AccountType"] = "Root"
|
375
|
+
else:
|
376
|
+
function_response["AccountType"] = "Child"
|
377
|
+
return function_response
|
378
|
+
except ClientError as my_Error:
|
379
|
+
if str(my_Error).find("UnrecognizedClientException") > 0:
|
380
|
+
logging.error(f"Security Issue with account {self.acct_number}")
|
381
|
+
elif str(my_Error).find("InvalidClientTokenId") > 0:
|
382
|
+
logging.error(f"Security Token is bad - probably a bad entry in config for account {self.acct_number}")
|
383
|
+
elif str(my_Error).find("AccessDenied") > 0:
|
384
|
+
logging.error(f"Access Denied for account {self.acct_number}")
|
385
|
+
elif str(my_Error).find("Organization") > 0:
|
386
|
+
logging.info(f"Org not in use for acct: {self.acct_number}\nError: {my_Error}")
|
387
|
+
function_response["AccountType"] = "StandAlone"
|
388
|
+
function_response["Id"] = self.acct_number
|
389
|
+
function_response["OrgId"] = None
|
390
|
+
function_response["ManagementEmail"] = "Email not available"
|
391
|
+
function_response["AccountNumber"] = self.acct_number
|
392
|
+
function_response["MasterAccountId"] = self.acct_number
|
393
|
+
function_response["MgmtAccountId"] = self.acct_number
|
394
|
+
pass
|
395
|
+
except CredentialRetrievalError as my_Error:
|
396
|
+
logging.error(f"Failure pulling or updating credentials for {self.acct_number}")
|
397
|
+
print(my_Error)
|
398
|
+
pass
|
399
|
+
except Exception as my_Error:
|
400
|
+
print(f"Other kind of failure: {my_Error}")
|
401
|
+
pass
|
402
|
+
except:
|
403
|
+
print("Excepted")
|
404
|
+
pass
|
405
|
+
return function_response
|
406
|
+
|
407
|
+
def find_child_accounts(self):
|
408
|
+
"""
|
409
|
+
This is an example of the list response from this call:
|
410
|
+
[
|
411
|
+
{'MgmtAccount':'<12 digit number>', 'AccountId': 'xxxxxxxxxxxx', 'AccountEmail': 'EmailAddr1@example.com', 'AccountStatus': 'ACTIVE'},
|
412
|
+
{'MgmtAccount':'<12 digit number>', 'AccountId': 'yyyyyyyyyyyy', 'AccountEmail': 'EmailAddr2@example.com', 'AccountStatus': 'ACTIVE'},
|
413
|
+
{'MgmtAccount':'<12 digit number>', 'AccountId': 'zzzzzzzzzzzz', 'AccountEmail': 'EmailAddr3@example.com', 'AccountStatus': 'SUSPENDED'}
|
414
|
+
]
|
415
|
+
This can be convenient for appending and removing.
|
416
|
+
"""
|
417
|
+
import logging
|
418
|
+
|
419
|
+
from botocore.exceptions import ClientError
|
420
|
+
|
421
|
+
child_accounts = []
|
422
|
+
if self.AccountType.lower() == "root":
|
423
|
+
try:
|
424
|
+
session_org = self.session
|
425
|
+
client_org = session_org.client("organizations")
|
426
|
+
response = client_org.list_accounts()
|
427
|
+
theresmore = True
|
428
|
+
logging.info(f"Enumerating Account info for account: {self.acct_number}")
|
429
|
+
while theresmore:
|
430
|
+
for account in response["Accounts"]:
|
431
|
+
child_accounts.append(
|
432
|
+
{
|
433
|
+
"MgmtAccount": self.acct_number,
|
434
|
+
"AccountId": account["Id"],
|
435
|
+
"AccountEmail": account["Email"],
|
436
|
+
"AccountStatus": account["Status"],
|
437
|
+
}
|
438
|
+
)
|
439
|
+
if "NextToken" in response.keys():
|
440
|
+
theresmore = True
|
441
|
+
response = client_org.list_accounts(NextToken=response["NextToken"])
|
442
|
+
else:
|
443
|
+
theresmore = False
|
444
|
+
sorted_child_accounts = sorted(child_accounts, key=lambda d: d["AccountId"])
|
445
|
+
return sorted_child_accounts
|
446
|
+
except ClientError as my_Error:
|
447
|
+
logging.warning(f"Account {self.acct_num()} doesn't represent an Org Root account")
|
448
|
+
logging.debug(my_Error)
|
449
|
+
return ()
|
450
|
+
elif self.find_account_attr()["AccountType"].lower() in ["standalone", "child"]:
|
451
|
+
child_accounts.append(
|
452
|
+
{
|
453
|
+
"MgmtAccount": self.acct_number,
|
454
|
+
"AccountId": self.acct_number,
|
455
|
+
"AccountEmail": "Not an Org Management Account",
|
456
|
+
# We know the account is ACTIVE because if it was SUSPENDED, we wouldn't have gotten a valid response from the org_root check
|
457
|
+
"AccountStatus": "ACTIVE",
|
458
|
+
}
|
459
|
+
)
|
460
|
+
return child_accounts
|
461
|
+
elif self.AccountType.lower() == "unknown":
|
462
|
+
logging.warning(f"Account {self.acct_number} came up as an Unknown Account...")
|
463
|
+
return ()
|
464
|
+
else:
|
465
|
+
logging.warning(f"Account {self.acct_number} suffered a crisis of identity")
|
466
|
+
return ()
|
467
|
+
|
468
|
+
def __str__(self):
|
469
|
+
return f"Account #{self.acct_number} is a {self.AccountType} account with {len(self.ChildAccounts) - 1} child accounts"
|
470
|
+
|
471
|
+
def __repr__(self):
|
472
|
+
return f"Account #{self.acct_number} is a {self.AccountType} account with {len(self.ChildAccounts) - 1} child accounts"
|
473
|
+
|
474
|
+
|
475
|
+
class Aws_Acct_Credentials:
|
476
|
+
"""
|
477
|
+
Description: The definition of the "ocredentials" object
|
478
|
+
"""
|
479
|
+
|
480
|
+
def __init__(self, f_sts_client_obj, f_role_arn: str, f_role_session_name: str, f_region: str = "us-east-1"):
|
481
|
+
"""
|
482
|
+
@Description: The object that will hold the credentials object, to make everything standardized
|
483
|
+
@param f_sts_client_obj: The boto3 client object
|
484
|
+
@param f_role_arn: The role arn you're looking to assume
|
485
|
+
@param f_role_session_name: The text string of the session name you're expecting to use
|
486
|
+
@param f_region: The region you're expecting to authenticate to. This is defaulted to be 'us-east-1'
|
487
|
+
@return credentials: The object containing all the information from the sts_assume_role call
|
488
|
+
"""
|
489
|
+
try:
|
490
|
+
credentials = f_sts_client_obj.assume_role(RoleArn=f_role_arn, RoleSessionName=f_role_session_name)[
|
491
|
+
"Credentials"
|
492
|
+
]
|
493
|
+
self.aws_access_key = credentials["AccessKeyId"]
|
494
|
+
self.AccessKeyId = self.aws_access_key
|
495
|
+
self.aws_secret_access_key = credentials["SecretAccessKey"]
|
496
|
+
self.SecretAccessKey = self.aws_secret_access_key
|
497
|
+
self.aws_session_token = credentials["SessionToken"]
|
498
|
+
self.SessionToken = self.aws_session_token
|
499
|
+
self.region = f_region
|
500
|
+
self.Region = self.region
|
501
|
+
self.AccountId = f_role_arn.split(":")[4]
|
502
|
+
self.AccountNumber = self.AccountId
|
503
|
+
self.AccountNum = self.AccountId
|
504
|
+
self.MgmtAccount = "Unknown"
|
505
|
+
self.Profile = None
|
506
|
+
self.Role = f_role_arn.split(":")[5].split("/")[1]
|
507
|
+
self.Success = True
|
508
|
+
self.ErrorMessage = ""
|
509
|
+
except (
|
510
|
+
ProfileNotFound,
|
511
|
+
ClientError,
|
512
|
+
ConnectionError,
|
513
|
+
EndpointConnectionError,
|
514
|
+
CredentialRetrievalError,
|
515
|
+
UnknownRegionError,
|
516
|
+
NoCredentialsError,
|
517
|
+
) as myError:
|
518
|
+
self.Success = False
|
519
|
+
self.ErrorMessage = str(myError)
|
520
|
+
logging.error(f"Error: {myError}")
|
521
|
+
|
522
|
+
def __str__(self):
|
523
|
+
if self.Profile is None:
|
524
|
+
return f"Account #{self.AccountId} was accessed directly with credentials"
|
525
|
+
else:
|
526
|
+
return f"Account #{self.AccountId} was accessed using {self.Profile}"
|
527
|
+
|
528
|
+
def __repr__(self):
|
529
|
+
if self.Profile is None:
|
530
|
+
return f"Account #{self.AccountId} was accessed directly with credentials"
|
531
|
+
else:
|
532
|
+
return f"Account #{self.AccountId} was accessed using {self.Profile}"
|