runbooks 0.2.5__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 +7 -7
  199. runbooks/security_baseline/report_template_jp.html +7 -7
  200. runbooks/security_baseline/report_template_kr.html +12 -12
  201. runbooks/security_baseline/report_template_vn.html +7 -7
  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.5.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.5.dist-info/METADATA +0 -439
  219. runbooks-0.2.5.dist-info/RECORD +0 -61
  220. runbooks-0.2.5.dist-info/entry_points.txt +0 -3
  221. runbooks-0.2.5.dist-info/top_level.txt +0 -1
@@ -0,0 +1,20 @@
1
+ """
2
+ AWS resource collectors organized by service category.
3
+
4
+ This module provides specialized collectors for different AWS service categories,
5
+ each implementing a common interface for resource discovery and inventory.
6
+
7
+ Categories:
8
+ - aws_compute: EC2, Lambda, ECS, Batch, Fargate
9
+ - aws_storage: S3, EBS, EFS, FSx
10
+ - aws_database: RDS, DynamoDB, ElastiCache, Redshift
11
+ - aws_network: VPC, ELB, CloudFront, Route53, API Gateway
12
+ - aws_security: IAM, GuardDuty, Config, Security Hub, WAF
13
+ - aws_management: CloudFormation, Organizations, Control Tower, SSM
14
+ """
15
+
16
+ from runbooks.inventory.collectors.base import BaseResourceCollector
17
+
18
+ __all__ = [
19
+ "BaseResourceCollector",
20
+ ]
@@ -0,0 +1,518 @@
1
+ """
2
+ AWS compute resource collector.
3
+
4
+ This module provides specialized collection of compute resources including
5
+ EC2 instances, Lambda functions, ECS clusters/services, and related components.
6
+ """
7
+
8
+ from datetime import datetime
9
+ from typing import Any, Dict, List, Optional, Set
10
+
11
+ import boto3
12
+ from botocore.exceptions import ClientError
13
+ from loguru import logger
14
+
15
+ from runbooks.inventory.collectors.base import BaseResourceCollector, CollectionContext
16
+ from runbooks.inventory.models.resource import AWSResource, ResourceCost, ResourceState
17
+ from runbooks.inventory.utils.aws_helpers import aws_api_retry
18
+
19
+
20
+ class ComputeResourceCollector(BaseResourceCollector):
21
+ """
22
+ Collector for AWS compute resources.
23
+
24
+ Handles discovery and inventory of:
25
+ - EC2 instances, images, snapshots, volumes
26
+ - Lambda functions and layers
27
+ - ECS clusters, services, and tasks
28
+ - Auto Scaling groups
29
+ - Elastic Beanstalk applications
30
+ """
31
+
32
+ service_category = "compute"
33
+ supported_resources = {
34
+ "ec2:instance",
35
+ "ec2:image",
36
+ "ec2:snapshot",
37
+ "ec2:volume",
38
+ "lambda:function",
39
+ "lambda:layer",
40
+ "ecs:cluster",
41
+ "ecs:service",
42
+ "ecs:task",
43
+ "autoscaling:group",
44
+ "elasticbeanstalk:application",
45
+ }
46
+ requires_org_access = False
47
+
48
+ def collect_resources(
49
+ self, context: CollectionContext, resource_filters: Optional[Dict[str, Any]] = None
50
+ ) -> List[AWSResource]:
51
+ """
52
+ Collect compute resources from AWS account/region.
53
+
54
+ Args:
55
+ context: Collection context with account, region, and options
56
+ resource_filters: Optional filters to apply during collection
57
+
58
+ Returns:
59
+ List of discovered compute resources
60
+ """
61
+ resources = []
62
+ resource_filters = resource_filters or {}
63
+
64
+ logger.info(
65
+ f"Starting compute resource collection in {context.region} for account {context.account.account_id}"
66
+ )
67
+
68
+ # Collect each supported resource type
69
+ for resource_type in context.resource_types.intersection(self.supported_resources):
70
+ try:
71
+ if resource_type.startswith("ec2:"):
72
+ resources.extend(self._collect_ec2_resources(context, resource_type, resource_filters))
73
+ elif resource_type.startswith("lambda:"):
74
+ resources.extend(self._collect_lambda_resources(context, resource_type, resource_filters))
75
+ elif resource_type.startswith("ecs:"):
76
+ resources.extend(self._collect_ecs_resources(context, resource_type, resource_filters))
77
+ elif resource_type.startswith("autoscaling:"):
78
+ resources.extend(self._collect_autoscaling_resources(context, resource_type, resource_filters))
79
+ elif resource_type.startswith("elasticbeanstalk:"):
80
+ resources.extend(self._collect_beanstalk_resources(context, resource_type, resource_filters))
81
+
82
+ except ClientError as e:
83
+ error_code = e.response["Error"]["Code"]
84
+ logger.error(f"Failed to collect {resource_type} in {context.region}: {error_code} - {e}")
85
+ if error_code in ["UnauthorizedOperation", "AccessDenied"]:
86
+ logger.warning(f"Insufficient permissions for {resource_type}")
87
+ continue
88
+ except Exception as e:
89
+ logger.error(f"Unexpected error collecting {resource_type}: {e}")
90
+ continue
91
+
92
+ logger.info(f"Collected {len(resources)} compute resources from {context.region}")
93
+ return resources
94
+
95
+ @aws_api_retry(max_retries=3)
96
+ def _collect_ec2_resources(
97
+ self, context: CollectionContext, resource_type: str, filters: Dict[str, Any]
98
+ ) -> List[AWSResource]:
99
+ """Collect EC2 resources (instances, volumes, snapshots, images)."""
100
+ resources = []
101
+ ec2_client = self.get_client("ec2", context.region)
102
+
103
+ if resource_type == "ec2:instance":
104
+ resources.extend(self._collect_ec2_instances(ec2_client, context, filters))
105
+ elif resource_type == "ec2:volume":
106
+ resources.extend(self._collect_ebs_volumes(ec2_client, context, filters))
107
+ elif resource_type == "ec2:snapshot":
108
+ resources.extend(self._collect_ebs_snapshots(ec2_client, context, filters))
109
+ elif resource_type == "ec2:image":
110
+ resources.extend(self._collect_ec2_images(ec2_client, context, filters))
111
+
112
+ return resources
113
+
114
+ def _collect_ec2_instances(
115
+ self, ec2_client: boto3.client, context: CollectionContext, filters: Dict[str, Any]
116
+ ) -> List[AWSResource]:
117
+ """Collect EC2 instances."""
118
+ resources = []
119
+
120
+ try:
121
+ paginator = ec2_client.get_paginator("describe_instances")
122
+
123
+ for page in paginator.paginate():
124
+ for reservation in page["Reservations"]:
125
+ for instance in reservation["Instances"]:
126
+ resource = self._create_ec2_instance_resource(instance, context)
127
+ if resource:
128
+ resources.append(resource)
129
+
130
+ logger.debug(f"Collected {len(resources)} EC2 instances")
131
+
132
+ except ClientError as e:
133
+ logger.error(f"Failed to collect EC2 instances: {e}")
134
+ raise
135
+
136
+ return resources
137
+
138
+ def _create_ec2_instance_resource(
139
+ self, instance_data: Dict[str, Any], context: CollectionContext
140
+ ) -> Optional[AWSResource]:
141
+ """Create AWSResource from EC2 instance data."""
142
+ try:
143
+ instance_id = instance_data["InstanceId"]
144
+ instance_type = instance_data["InstanceType"]
145
+ state = instance_data["State"]["Name"]
146
+
147
+ # Map EC2 state to ResourceState
148
+ state_mapping = {
149
+ "pending": ResourceState.PENDING,
150
+ "running": ResourceState.RUNNING,
151
+ "shutting-down": ResourceState.PENDING,
152
+ "terminated": ResourceState.TERMINATED,
153
+ "stopping": ResourceState.PENDING,
154
+ "stopped": ResourceState.STOPPED,
155
+ }
156
+
157
+ resource_state = state_mapping.get(state, ResourceState.UNKNOWN)
158
+
159
+ # Extract tags
160
+ tags = {}
161
+ for tag in instance_data.get("Tags", []):
162
+ tags[tag["Key"]] = tag["Value"]
163
+
164
+ # Get instance name
165
+ instance_name = tags.get("Name", instance_id)
166
+
167
+ # Extract security groups
168
+ security_groups = [sg["GroupId"] for sg in instance_data.get("SecurityGroups", [])]
169
+
170
+ # Determine public access
171
+ public_access = bool(instance_data.get("PublicIpAddress"))
172
+
173
+ # Create resource configuration
174
+ configuration = {
175
+ "instance_type": instance_type,
176
+ "image_id": instance_data.get("ImageId"),
177
+ "launch_time": instance_data.get("LaunchTime"),
178
+ "platform": instance_data.get("Platform"),
179
+ "subnet_id": instance_data.get("SubnetId"),
180
+ "vpc_id": instance_data.get("VpcId"),
181
+ "private_ip": instance_data.get("PrivateIpAddress"),
182
+ "public_ip": instance_data.get("PublicIpAddress"),
183
+ "monitoring": instance_data.get("Monitoring", {}).get("State"),
184
+ "ebs_optimized": instance_data.get("EbsOptimized", False),
185
+ }
186
+
187
+ # Create cost estimate (rough approximation)
188
+ cost_info = None
189
+ if context.include_costs:
190
+ # Simple cost estimation based on instance type
191
+ # In production, use AWS Cost Explorer API
192
+ cost_info = self._estimate_ec2_cost(instance_type, resource_state)
193
+
194
+ # Create resource metadata
195
+ metadata = self._create_resource_metadata(context, instance_data)
196
+
197
+ return AWSResource(
198
+ resource_id=instance_id,
199
+ resource_type="ec2:instance",
200
+ resource_arn=f"arn:aws:ec2:{context.region}:{context.account.account_id}:instance/{instance_id}",
201
+ resource_name=instance_name,
202
+ state=resource_state,
203
+ creation_date=instance_data.get("LaunchTime"),
204
+ account_id=context.account.account_id,
205
+ region=context.region,
206
+ availability_zone=instance_data.get("Placement", {}).get("AvailabilityZone"),
207
+ configuration=configuration,
208
+ tags=tags,
209
+ security_groups=security_groups,
210
+ public_access=public_access,
211
+ cost_info=cost_info,
212
+ metadata=metadata,
213
+ )
214
+
215
+ except Exception as e:
216
+ logger.error(f"Error creating EC2 instance resource: {e}")
217
+ return None
218
+
219
+ def _collect_ebs_volumes(
220
+ self, ec2_client: boto3.client, context: CollectionContext, filters: Dict[str, Any]
221
+ ) -> List[AWSResource]:
222
+ """Collect EBS volumes."""
223
+ resources = []
224
+
225
+ try:
226
+ paginator = ec2_client.get_paginator("describe_volumes")
227
+
228
+ for page in paginator.paginate():
229
+ for volume in page["Volumes"]:
230
+ resource = self._create_ebs_volume_resource(volume, context)
231
+ if resource:
232
+ resources.append(resource)
233
+
234
+ logger.debug(f"Collected {len(resources)} EBS volumes")
235
+
236
+ except ClientError as e:
237
+ logger.error(f"Failed to collect EBS volumes: {e}")
238
+ raise
239
+
240
+ return resources
241
+
242
+ def _create_ebs_volume_resource(
243
+ self, volume_data: Dict[str, Any], context: CollectionContext
244
+ ) -> Optional[AWSResource]:
245
+ """Create AWSResource from EBS volume data."""
246
+ try:
247
+ volume_id = volume_data["VolumeId"]
248
+ state = volume_data["State"]
249
+
250
+ # Map EBS state to ResourceState
251
+ state_mapping = {
252
+ "creating": ResourceState.CREATING,
253
+ "available": ResourceState.AVAILABLE,
254
+ "in-use": ResourceState.IN_USE,
255
+ "deleting": ResourceState.DELETING,
256
+ "deleted": ResourceState.TERMINATED,
257
+ "error": ResourceState.UNKNOWN,
258
+ }
259
+
260
+ resource_state = state_mapping.get(state, ResourceState.UNKNOWN)
261
+
262
+ # Extract tags
263
+ tags = {}
264
+ for tag in volume_data.get("Tags", []):
265
+ tags[tag["Key"]] = tag["Value"]
266
+
267
+ volume_name = tags.get("Name", volume_id)
268
+
269
+ # Configuration details
270
+ configuration = {
271
+ "volume_type": volume_data.get("VolumeType"),
272
+ "size": volume_data.get("Size"),
273
+ "iops": volume_data.get("Iops"),
274
+ "encrypted": volume_data.get("Encrypted", False),
275
+ "kms_key_id": volume_data.get("KmsKeyId"),
276
+ "throughput": volume_data.get("Throughput"),
277
+ }
278
+
279
+ # Check attachments
280
+ attachments = volume_data.get("Attachments", [])
281
+ attached_to = [att["InstanceId"] for att in attachments if att.get("State") == "attached"]
282
+
283
+ # Cost estimation
284
+ cost_info = None
285
+ if context.include_costs:
286
+ cost_info = self._estimate_ebs_cost(volume_data)
287
+
288
+ metadata = self._create_resource_metadata(context, volume_data)
289
+
290
+ return AWSResource(
291
+ resource_id=volume_id,
292
+ resource_type="ec2:volume",
293
+ resource_arn=f"arn:aws:ec2:{context.region}:{context.account.account_id}:volume/{volume_id}",
294
+ resource_name=volume_name,
295
+ state=resource_state,
296
+ creation_date=volume_data.get("CreateTime"),
297
+ account_id=context.account.account_id,
298
+ region=context.region,
299
+ availability_zone=volume_data.get("AvailabilityZone"),
300
+ configuration=configuration,
301
+ tags=tags,
302
+ encryption_status="encrypted" if volume_data.get("Encrypted") else "not-encrypted",
303
+ cost_info=cost_info,
304
+ metadata=metadata,
305
+ dependencies=[
306
+ f"arn:aws:ec2:{context.region}:{context.account.account_id}:instance/{instance_id}"
307
+ for instance_id in attached_to
308
+ ],
309
+ )
310
+
311
+ except Exception as e:
312
+ logger.error(f"Error creating EBS volume resource: {e}")
313
+ return None
314
+
315
+ @aws_api_retry(max_retries=3)
316
+ def _collect_lambda_resources(
317
+ self, context: CollectionContext, resource_type: str, filters: Dict[str, Any]
318
+ ) -> List[AWSResource]:
319
+ """Collect Lambda resources (functions, layers)."""
320
+ resources = []
321
+ lambda_client = self.get_client("lambda", context.region)
322
+
323
+ if resource_type == "lambda:function":
324
+ resources.extend(self._collect_lambda_functions(lambda_client, context, filters))
325
+ elif resource_type == "lambda:layer":
326
+ resources.extend(self._collect_lambda_layers(lambda_client, context, filters))
327
+
328
+ return resources
329
+
330
+ def _collect_lambda_functions(
331
+ self, lambda_client: boto3.client, context: CollectionContext, filters: Dict[str, Any]
332
+ ) -> List[AWSResource]:
333
+ """Collect Lambda functions."""
334
+ resources = []
335
+
336
+ try:
337
+ paginator = lambda_client.get_paginator("list_functions")
338
+
339
+ for page in paginator.paginate():
340
+ for function in page["Functions"]:
341
+ resource = self._create_lambda_function_resource(function, context)
342
+ if resource:
343
+ resources.append(resource)
344
+
345
+ logger.debug(f"Collected {len(resources)} Lambda functions")
346
+
347
+ except ClientError as e:
348
+ logger.error(f"Failed to collect Lambda functions: {e}")
349
+ raise
350
+
351
+ return resources
352
+
353
+ def _create_lambda_function_resource(
354
+ self, function_data: Dict[str, Any], context: CollectionContext
355
+ ) -> Optional[AWSResource]:
356
+ """Create AWSResource from Lambda function data."""
357
+ try:
358
+ function_name = function_data["FunctionName"]
359
+ function_arn = function_data["FunctionArn"]
360
+
361
+ # Lambda functions are always "available" when listed
362
+ resource_state = ResourceState.AVAILABLE
363
+
364
+ # Configuration details
365
+ configuration = {
366
+ "runtime": function_data.get("Runtime"),
367
+ "handler": function_data.get("Handler"),
368
+ "code_size": function_data.get("CodeSize"),
369
+ "memory_size": function_data.get("MemorySize"),
370
+ "timeout": function_data.get("Timeout"),
371
+ "environment": function_data.get("Environment", {}).get("Variables", {}),
372
+ "role": function_data.get("Role"),
373
+ "vpc_config": function_data.get("VpcConfig"),
374
+ "last_modified": function_data.get("LastModified"),
375
+ }
376
+
377
+ # Cost estimation for Lambda
378
+ cost_info = None
379
+ if context.include_costs:
380
+ cost_info = self._estimate_lambda_cost(function_data)
381
+
382
+ metadata = self._create_resource_metadata(context, function_data)
383
+
384
+ return AWSResource(
385
+ resource_id=function_name,
386
+ resource_type="lambda:function",
387
+ resource_arn=function_arn,
388
+ resource_name=function_name,
389
+ state=resource_state,
390
+ creation_date=datetime.fromisoformat(function_data.get("LastModified", "").replace("Z", "+00:00")),
391
+ account_id=context.account.account_id,
392
+ region=context.region,
393
+ configuration=configuration,
394
+ tags={}, # Lambda tags require separate API call
395
+ cost_info=cost_info,
396
+ metadata=metadata,
397
+ )
398
+
399
+ except Exception as e:
400
+ logger.error(f"Error creating Lambda function resource: {e}")
401
+ return None
402
+
403
+ def _collect_ecs_resources(
404
+ self, context: CollectionContext, resource_type: str, filters: Dict[str, Any]
405
+ ) -> List[AWSResource]:
406
+ """Collect ECS resources (clusters, services, tasks)."""
407
+ # Implementation for ECS resources
408
+ return []
409
+
410
+ def _collect_autoscaling_resources(
411
+ self, context: CollectionContext, resource_type: str, filters: Dict[str, Any]
412
+ ) -> List[AWSResource]:
413
+ """Collect Auto Scaling resources."""
414
+ # Implementation for Auto Scaling groups
415
+ return []
416
+
417
+ def _collect_beanstalk_resources(
418
+ self, context: CollectionContext, resource_type: str, filters: Dict[str, Any]
419
+ ) -> List[AWSResource]:
420
+ """Collect Elastic Beanstalk resources."""
421
+ # Implementation for Beanstalk applications
422
+ return []
423
+
424
+ def get_resource_costs(self, resources: List[AWSResource], context: CollectionContext) -> Dict[str, float]:
425
+ """Get cost information for compute resources."""
426
+ costs = {}
427
+
428
+ if not context.include_costs:
429
+ return costs
430
+
431
+ # Group resources by type for batch cost calculation
432
+ ec2_instances = [r for r in resources if r.resource_type == "ec2:instance"]
433
+ ebs_volumes = [r for r in resources if r.resource_type == "ec2:volume"]
434
+ lambda_functions = [r for r in resources if r.resource_type == "lambda:function"]
435
+
436
+ # Calculate costs for each resource type
437
+ # In production, integrate with AWS Cost Explorer API
438
+ for instance in ec2_instances:
439
+ if instance.cost_info:
440
+ costs[instance.resource_arn] = instance.cost_info.monthly_cost or 0.0
441
+
442
+ for volume in ebs_volumes:
443
+ if volume.cost_info:
444
+ costs[volume.resource_arn] = volume.cost_info.monthly_cost or 0.0
445
+
446
+ for function in lambda_functions:
447
+ if function.cost_info:
448
+ costs[function.resource_arn] = function.cost_info.monthly_cost or 0.0
449
+
450
+ return costs
451
+
452
+ def _estimate_ec2_cost(self, instance_type: str, state: ResourceState) -> Optional[ResourceCost]:
453
+ """Estimate monthly cost for EC2 instance."""
454
+ if state not in [ResourceState.RUNNING, ResourceState.PENDING]:
455
+ return None
456
+
457
+ # Simplified cost estimation (replace with actual pricing API)
458
+ base_costs = {
459
+ "t2.micro": 8.5,
460
+ "t2.small": 17.0,
461
+ "t2.medium": 34.0,
462
+ "t3.micro": 7.5,
463
+ "t3.small": 15.0,
464
+ "t3.medium": 30.0,
465
+ "m5.large": 70.0,
466
+ "m5.xlarge": 140.0,
467
+ "c5.large": 62.0,
468
+ "r5.large": 91.0,
469
+ }
470
+
471
+ monthly_cost = base_costs.get(instance_type, 50.0) # Default estimate
472
+
473
+ return ResourceCost(monthly_cost=monthly_cost, currency="USD", cost_breakdown={"compute": monthly_cost})
474
+
475
+ def _estimate_ebs_cost(self, volume_data: Dict[str, Any]) -> Optional[ResourceCost]:
476
+ """Estimate monthly cost for EBS volume."""
477
+ volume_type = volume_data.get("VolumeType", "gp2")
478
+ size_gb = volume_data.get("Size", 0)
479
+
480
+ # Simplified EBS pricing per GB per month
481
+ prices_per_gb = {"gp2": 0.10, "gp3": 0.08, "io1": 0.125, "io2": 0.125, "st1": 0.045, "sc1": 0.025}
482
+
483
+ price_per_gb = prices_per_gb.get(volume_type, 0.10)
484
+ monthly_cost = size_gb * price_per_gb
485
+
486
+ cost_breakdown = {"storage": monthly_cost}
487
+
488
+ # Add IOPS cost for provisioned IOPS volumes
489
+ if volume_type in ["io1", "io2"] and volume_data.get("Iops"):
490
+ iops_cost = volume_data["Iops"] * 0.065 # $0.065 per IOPS per month
491
+ cost_breakdown["iops"] = iops_cost
492
+ monthly_cost += iops_cost
493
+
494
+ return ResourceCost(monthly_cost=monthly_cost, currency="USD", cost_breakdown=cost_breakdown)
495
+
496
+ def _estimate_lambda_cost(self, function_data: Dict[str, Any]) -> Optional[ResourceCost]:
497
+ """Estimate monthly cost for Lambda function."""
498
+ memory_mb = function_data.get("MemorySize", 128)
499
+
500
+ # Lambda pricing is based on requests and GB-seconds
501
+ # This is a very rough estimate - actual costs depend on usage
502
+ estimated_monthly_requests = 10000 # Assumption
503
+ estimated_avg_duration_ms = 1000 # Assumption
504
+
505
+ # Calculate GB-seconds
506
+ gb_seconds = (memory_mb / 1024) * (estimated_avg_duration_ms / 1000) * estimated_monthly_requests
507
+
508
+ # Lambda pricing (simplified)
509
+ request_cost = estimated_monthly_requests * 0.0000002 # $0.20 per 1M requests
510
+ compute_cost = gb_seconds * 0.0000166667 # $0.0000166667 per GB-second
511
+
512
+ monthly_cost = request_cost + compute_cost
513
+
514
+ return ResourceCost(
515
+ monthly_cost=monthly_cost,
516
+ currency="USD",
517
+ cost_breakdown={"requests": request_cost, "compute": compute_cost},
518
+ )