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.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -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/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/__init__.py +88 -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/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -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/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/script_test_data.py +0 -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 +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +8 -8
- runbooks/security_baseline/report_template_jp.html +8 -8
- runbooks/security_baseline/report_template_kr.html +13 -13
- runbooks/security_baseline/report_template_vn.html +8 -8
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.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.3.dist-info/METADATA +0 -435
- runbooks-0.2.3.dist-info/RECORD +0 -61
- runbooks-0.2.3.dist-info/entry_points.txt +0 -3
- runbooks-0.2.3.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
|
+
)
|