runbooks 0.2.3__py3-none-any.whl → 0.6.1__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.
Files changed (221) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env.template +2 -0
  3. jupyter-agent/.gitattributes +35 -0
  4. jupyter-agent/README.md +16 -0
  5. jupyter-agent/app.py +256 -0
  6. jupyter-agent/cloudops-agent.png +0 -0
  7. jupyter-agent/ds-system-prompt.txt +154 -0
  8. jupyter-agent/jupyter-agent.png +0 -0
  9. jupyter-agent/llama3_template.jinja +123 -0
  10. jupyter-agent/requirements.txt +9 -0
  11. jupyter-agent/utils.py +409 -0
  12. runbooks/__init__.py +71 -3
  13. runbooks/__main__.py +13 -0
  14. runbooks/aws/ec2_describe_instances.py +1 -1
  15. runbooks/aws/ec2_run_instances.py +8 -2
  16. runbooks/aws/ec2_start_stop_instances.py +17 -4
  17. runbooks/aws/ec2_unused_volumes.py +5 -1
  18. runbooks/aws/s3_create_bucket.py +4 -2
  19. runbooks/aws/s3_list_objects.py +6 -1
  20. runbooks/aws/tagging_lambda_handler.py +13 -2
  21. runbooks/aws/tags.json +12 -0
  22. runbooks/base.py +353 -0
  23. runbooks/cfat/README.md +49 -0
  24. runbooks/cfat/__init__.py +74 -0
  25. runbooks/cfat/app.ts +644 -0
  26. runbooks/cfat/assessment/__init__.py +40 -0
  27. runbooks/cfat/assessment/asana-import.csv +39 -0
  28. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  29. runbooks/cfat/assessment/cfat.txt +520 -0
  30. runbooks/cfat/assessment/collectors.py +200 -0
  31. runbooks/cfat/assessment/jira-import.csv +39 -0
  32. runbooks/cfat/assessment/runner.py +387 -0
  33. runbooks/cfat/assessment/validators.py +290 -0
  34. runbooks/cfat/cli.py +103 -0
  35. runbooks/cfat/docs/asana-import.csv +24 -0
  36. runbooks/cfat/docs/cfat-checks.csv +31 -0
  37. runbooks/cfat/docs/cfat.txt +335 -0
  38. runbooks/cfat/docs/checks-output.png +0 -0
  39. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  40. runbooks/cfat/docs/cloudshell-download.png +0 -0
  41. runbooks/cfat/docs/cloudshell-output.png +0 -0
  42. runbooks/cfat/docs/downloadfile.png +0 -0
  43. runbooks/cfat/docs/jira-import.csv +24 -0
  44. runbooks/cfat/docs/open-cloudshell.png +0 -0
  45. runbooks/cfat/docs/report-header.png +0 -0
  46. runbooks/cfat/models.py +1026 -0
  47. runbooks/cfat/package-lock.json +5116 -0
  48. runbooks/cfat/package.json +38 -0
  49. runbooks/cfat/report.py +496 -0
  50. runbooks/cfat/reporting/__init__.py +46 -0
  51. runbooks/cfat/reporting/exporters.py +337 -0
  52. runbooks/cfat/reporting/formatters.py +496 -0
  53. runbooks/cfat/reporting/templates.py +135 -0
  54. runbooks/cfat/run-assessment.sh +23 -0
  55. runbooks/cfat/runner.py +69 -0
  56. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  57. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  58. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  59. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  60. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  61. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  62. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  63. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  64. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  65. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  66. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  67. runbooks/cfat/src/actions/create-report.ts +616 -0
  68. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  69. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  70. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  71. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  72. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  73. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  74. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  75. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  76. runbooks/cfat/src/actions/get-regions.ts +22 -0
  77. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  78. runbooks/cfat/src/types/index.d.ts +147 -0
  79. runbooks/cfat/tests/__init__.py +141 -0
  80. runbooks/cfat/tests/test_cli.py +340 -0
  81. runbooks/cfat/tests/test_integration.py +290 -0
  82. runbooks/cfat/tests/test_models.py +505 -0
  83. runbooks/cfat/tests/test_reporting.py +354 -0
  84. runbooks/cfat/tsconfig.json +16 -0
  85. runbooks/cfat/webpack.config.cjs +27 -0
  86. runbooks/config.py +260 -0
  87. runbooks/finops/__init__.py +88 -0
  88. runbooks/finops/aws_client.py +245 -0
  89. runbooks/finops/cli.py +151 -0
  90. runbooks/finops/cost_processor.py +410 -0
  91. runbooks/finops/dashboard_runner.py +448 -0
  92. runbooks/finops/helpers.py +355 -0
  93. runbooks/finops/main.py +14 -0
  94. runbooks/finops/profile_processor.py +174 -0
  95. runbooks/finops/types.py +66 -0
  96. runbooks/finops/visualisations.py +80 -0
  97. runbooks/inventory/.gitignore +354 -0
  98. runbooks/inventory/ArgumentsClass.py +261 -0
  99. runbooks/inventory/Inventory_Modules.py +6130 -0
  100. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  101. runbooks/inventory/README.md +1320 -0
  102. runbooks/inventory/__init__.py +62 -0
  103. runbooks/inventory/account_class.py +532 -0
  104. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  105. runbooks/inventory/aws_decorators.py +201 -0
  106. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  107. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  108. runbooks/inventory/check_controltower_readiness.py +1107 -0
  109. runbooks/inventory/check_landingzone_readiness.py +711 -0
  110. runbooks/inventory/cloudtrail.md +727 -0
  111. runbooks/inventory/collectors/__init__.py +20 -0
  112. runbooks/inventory/collectors/aws_compute.py +518 -0
  113. runbooks/inventory/collectors/aws_networking.py +275 -0
  114. runbooks/inventory/collectors/base.py +222 -0
  115. runbooks/inventory/core/__init__.py +19 -0
  116. runbooks/inventory/core/collector.py +303 -0
  117. runbooks/inventory/core/formatter.py +296 -0
  118. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  119. runbooks/inventory/discovery.md +81 -0
  120. runbooks/inventory/draw_org_structure.py +748 -0
  121. runbooks/inventory/ec2_vpc_utils.py +341 -0
  122. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  123. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  124. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  125. runbooks/inventory/find_ec2_security_groups.py +669 -0
  126. runbooks/inventory/find_landingzone_versions.py +201 -0
  127. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  128. runbooks/inventory/inventory.sh +659 -0
  129. runbooks/inventory/list_cfn_stacks.py +558 -0
  130. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  131. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  132. runbooks/inventory/list_cfn_stacksets.py +453 -0
  133. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  134. runbooks/inventory/list_ds_directories.py +354 -0
  135. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  136. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  137. runbooks/inventory/list_ec2_instances.py +425 -0
  138. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  139. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  140. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  141. runbooks/inventory/list_guardduty_detectors.py +568 -0
  142. runbooks/inventory/list_iam_policies.py +404 -0
  143. runbooks/inventory/list_iam_roles.py +518 -0
  144. runbooks/inventory/list_iam_saml_providers.py +359 -0
  145. runbooks/inventory/list_lambda_functions.py +882 -0
  146. runbooks/inventory/list_org_accounts.py +446 -0
  147. runbooks/inventory/list_org_accounts_users.py +354 -0
  148. runbooks/inventory/list_rds_db_instances.py +406 -0
  149. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  150. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  151. runbooks/inventory/list_sns_topics.py +360 -0
  152. runbooks/inventory/list_ssm_parameters.py +402 -0
  153. runbooks/inventory/list_vpc_subnets.py +433 -0
  154. runbooks/inventory/list_vpcs.py +422 -0
  155. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  156. runbooks/inventory/models/__init__.py +24 -0
  157. runbooks/inventory/models/account.py +192 -0
  158. runbooks/inventory/models/inventory.py +309 -0
  159. runbooks/inventory/models/resource.py +247 -0
  160. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  161. runbooks/inventory/requirements.txt +12 -0
  162. runbooks/inventory/run_on_multi_accounts.py +211 -0
  163. runbooks/inventory/tests/common_test_data.py +3661 -0
  164. runbooks/inventory/tests/common_test_functions.py +204 -0
  165. runbooks/inventory/tests/script_test_data.py +0 -0
  166. runbooks/inventory/tests/setup.py +24 -0
  167. runbooks/inventory/tests/src.py +18 -0
  168. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  169. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  170. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  171. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  172. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  173. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  174. runbooks/inventory/update_aws_actions.py +173 -0
  175. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  176. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  177. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  178. runbooks/inventory/update_s3_public_access_block.py +539 -0
  179. runbooks/inventory/utils/__init__.py +23 -0
  180. runbooks/inventory/utils/aws_helpers.py +510 -0
  181. runbooks/inventory/utils/threading_utils.py +493 -0
  182. runbooks/inventory/utils/validation.py +682 -0
  183. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  184. runbooks/main.py +785 -0
  185. runbooks/organizations/__init__.py +12 -0
  186. runbooks/organizations/manager.py +374 -0
  187. runbooks/security_baseline/README.md +324 -0
  188. runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
  189. runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
  190. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
  191. runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
  192. runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
  193. runbooks/security_baseline/checklist/root_access_key.py +6 -1
  194. runbooks/security_baseline/config-origin.json +1 -1
  195. runbooks/security_baseline/config.json +1 -1
  196. runbooks/security_baseline/permission.json +1 -1
  197. runbooks/security_baseline/report_generator.py +10 -2
  198. runbooks/security_baseline/report_template_en.html +8 -8
  199. runbooks/security_baseline/report_template_jp.html +8 -8
  200. runbooks/security_baseline/report_template_kr.html +13 -13
  201. runbooks/security_baseline/report_template_vn.html +8 -8
  202. runbooks/security_baseline/requirements.txt +7 -0
  203. runbooks/security_baseline/run_script.py +8 -2
  204. runbooks/security_baseline/security_baseline_tester.py +10 -2
  205. runbooks/security_baseline/utils/common.py +5 -1
  206. runbooks/utils/__init__.py +204 -0
  207. runbooks-0.6.1.dist-info/METADATA +373 -0
  208. runbooks-0.6.1.dist-info/RECORD +237 -0
  209. {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
  210. runbooks-0.6.1.dist-info/entry_points.txt +7 -0
  211. runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
  212. runbooks-0.6.1.dist-info/top_level.txt +3 -0
  213. runbooks/python101/calculator.py +0 -34
  214. runbooks/python101/config.py +0 -1
  215. runbooks/python101/exceptions.py +0 -16
  216. runbooks/python101/file_manager.py +0 -218
  217. runbooks/python101/toolkit.py +0 -153
  218. runbooks-0.2.3.dist-info/METADATA +0 -435
  219. runbooks-0.2.3.dist-info/RECORD +0 -61
  220. runbooks-0.2.3.dist-info/entry_points.txt +0 -3
  221. runbooks-0.2.3.dist-info/top_level.txt +0 -1
@@ -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}"