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,192 @@
1
+ """
2
+ AWS account models for inventory operations.
3
+
4
+ This module provides Pydantic models representing AWS accounts and
5
+ organization structures with proper validation and type safety.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import Dict, List, Optional, Set
12
+
13
+ from pydantic import BaseModel, Field, validator
14
+
15
+
16
+ class AccountStatus(str, Enum):
17
+ """AWS account status enumeration."""
18
+
19
+ ACTIVE = "ACTIVE"
20
+ SUSPENDED = "SUSPENDED"
21
+ PENDING_CLOSURE = "PENDING_CLOSURE"
22
+
23
+
24
+ class OrganizationUnitType(str, Enum):
25
+ """Organization unit type enumeration."""
26
+
27
+ ROOT = "ROOT"
28
+ ORGANIZATIONAL_UNIT = "ORGANIZATIONAL_UNIT"
29
+
30
+
31
+ @dataclass
32
+ class AWSCredentials:
33
+ """AWS credentials for account access."""
34
+
35
+ access_key_id: Optional[str] = None
36
+ secret_access_key: Optional[str] = None
37
+ session_token: Optional[str] = None
38
+ profile_name: Optional[str] = None
39
+ role_arn: Optional[str] = None
40
+ external_id: Optional[str] = None
41
+
42
+ def __post_init__(self):
43
+ """Validate credentials configuration."""
44
+ has_keys = self.access_key_id and self.secret_access_key
45
+ has_profile = self.profile_name
46
+ has_role = self.role_arn
47
+
48
+ if not any([has_keys, has_profile, has_role]):
49
+ raise ValueError("Must provide either access keys, profile name, or role ARN")
50
+
51
+
52
+ class AWSAccount(BaseModel):
53
+ """
54
+ Represents an AWS account with metadata and access information.
55
+
56
+ This model provides a comprehensive representation of an AWS account
57
+ including organization context, contact information, and access credentials.
58
+ """
59
+
60
+ account_id: str = Field(..., pattern=r"^\d{12}$", description="12-digit AWS account ID")
61
+
62
+ account_name: Optional[str] = Field(None, description="Human-readable account name")
63
+
64
+ email: Optional[str] = Field(None, description="Root email address for the account")
65
+
66
+ status: AccountStatus = Field(AccountStatus.ACTIVE, description="Current account status")
67
+
68
+ creation_date: Optional[datetime] = Field(None, description="Account creation timestamp")
69
+
70
+ # Organization information
71
+ organization_id: Optional[str] = Field(None, description="AWS Organizations ID if account is in an organization")
72
+
73
+ organizational_unit_id: Optional[str] = Field(None, description="Organizational Unit ID containing this account")
74
+
75
+ organizational_unit_name: Optional[str] = Field(None, description="Organizational Unit name")
76
+
77
+ is_management_account: bool = Field(False, description="Whether this is the organization management account")
78
+
79
+ # Access and permissions
80
+ default_region: str = Field("us-east-1", description="Default AWS region for operations")
81
+
82
+ available_regions: Set[str] = Field(default_factory=set, description="Set of available/enabled regions")
83
+
84
+ # Metadata
85
+ tags: Dict[str, str] = Field(default_factory=dict, description="Account-level tags")
86
+
87
+ discovered_services: Set[str] = Field(default_factory=set, description="AWS services discovered in this account")
88
+
89
+ last_inventory_date: Optional[datetime] = Field(None, description="Last successful inventory collection timestamp")
90
+
91
+ # Cost and billing (if available)
92
+ monthly_cost_estimate: Optional[float] = Field(None, description="Monthly cost estimate in USD")
93
+
94
+ cost_center: Optional[str] = Field(None, description="Cost center or billing code")
95
+
96
+ class Config:
97
+ """Pydantic configuration."""
98
+
99
+ use_enum_values = True
100
+ extra = "forbid"
101
+ json_encoders = {datetime: lambda v: v.isoformat(), set: lambda v: list(v)}
102
+
103
+ @validator("account_id")
104
+ def validate_account_id(cls, v):
105
+ """Validate account ID format."""
106
+ if not v.isdigit() or len(v) != 12:
107
+ raise ValueError("Account ID must be exactly 12 digits")
108
+ return v
109
+
110
+ @validator("available_regions")
111
+ def validate_regions(cls, v):
112
+ """Validate region format."""
113
+ valid_region_pattern = r"^[a-z]{2,3}-[a-z]+-\d+$"
114
+ import re
115
+
116
+ for region in v:
117
+ if not re.match(valid_region_pattern, region):
118
+ raise ValueError(f"Invalid region format: {region}")
119
+ return v
120
+
121
+ def is_in_organization(self) -> bool:
122
+ """Check if account is part of an AWS Organization."""
123
+ return self.organization_id is not None
124
+
125
+ def add_discovered_service(self, service: str) -> None:
126
+ """Add a service to the discovered services set."""
127
+ self.discovered_services.add(service)
128
+
129
+ def get_region_count(self) -> int:
130
+ """Get the number of available regions."""
131
+ return len(self.available_regions)
132
+
133
+ def to_dict(self) -> Dict:
134
+ """Convert to dictionary representation."""
135
+ return self.dict()
136
+
137
+ def __str__(self) -> str:
138
+ """String representation."""
139
+ name = self.account_name or "Unnamed"
140
+ return f"AWSAccount({self.account_id}, {name})"
141
+
142
+
143
+ class OrganizationAccount(AWSAccount):
144
+ """
145
+ Extended account model for organization management accounts.
146
+
147
+ Provides additional capabilities for managing organization-wide
148
+ operations and cross-account resource discovery.
149
+ """
150
+
151
+ child_accounts: List[AWSAccount] = Field(
152
+ default_factory=list, description="List of child accounts in this organization"
153
+ )
154
+
155
+ organizational_units: Dict[str, str] = Field(default_factory=dict, description="Mapping of OU IDs to OU names")
156
+
157
+ service_control_policies: List[str] = Field(
158
+ default_factory=list, description="List of SCP IDs attached to the organization"
159
+ )
160
+
161
+ feature_set: str = Field("ALL", description="Organization feature set (ALL or CONSOLIDATED_BILLING)")
162
+
163
+ def add_child_account(self, account: AWSAccount) -> None:
164
+ """Add a child account to the organization."""
165
+ account.organization_id = self.organization_id
166
+ self.child_accounts.append(account)
167
+
168
+ def get_child_account(self, account_id: str) -> Optional[AWSAccount]:
169
+ """Get a specific child account by ID."""
170
+ for account in self.child_accounts:
171
+ if account.account_id == account_id:
172
+ return account
173
+ return None
174
+
175
+ def get_accounts_by_ou(self, ou_id: str) -> List[AWSAccount]:
176
+ """Get all accounts in a specific organizational unit."""
177
+ return [account for account in self.child_accounts if account.organizational_unit_id == ou_id]
178
+
179
+ def get_total_accounts(self) -> int:
180
+ """Get total number of accounts in organization (including management)."""
181
+ return len(self.child_accounts) + 1
182
+
183
+ def get_organization_summary(self) -> Dict:
184
+ """Get organization summary statistics."""
185
+ return {
186
+ "management_account": self.account_id,
187
+ "total_accounts": self.get_total_accounts(),
188
+ "organizational_units": len(self.organizational_units),
189
+ "service_control_policies": len(self.service_control_policies),
190
+ "feature_set": self.feature_set,
191
+ "active_accounts": sum(1 for acc in self.child_accounts if acc.status == AccountStatus.ACTIVE),
192
+ }
@@ -0,0 +1,309 @@
1
+ """
2
+ Inventory result models for collection aggregation.
3
+
4
+ This module provides models for representing the results of inventory
5
+ collection operations, including metadata, statistics, and aggregations.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime, timedelta
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional, Set
12
+
13
+ from pydantic import BaseModel, Field, validator
14
+
15
+ from runbooks.inventory.models.account import AWSAccount
16
+ from runbooks.inventory.models.resource import AWSResource
17
+
18
+
19
+ class InventoryStatus(str, Enum):
20
+ """Inventory collection status."""
21
+
22
+ SUCCESS = "success"
23
+ PARTIAL_SUCCESS = "partial_success"
24
+ FAILED = "failed"
25
+ IN_PROGRESS = "in_progress"
26
+ CANCELLED = "cancelled"
27
+
28
+
29
+ class CollectionScope(str, Enum):
30
+ """Scope of inventory collection."""
31
+
32
+ SINGLE_ACCOUNT = "single_account"
33
+ ORGANIZATION = "organization"
34
+ MULTI_ACCOUNT = "multi_account"
35
+ CUSTOM = "custom"
36
+
37
+
38
+ @dataclass
39
+ class CollectionError:
40
+ """Error information from collection operations."""
41
+
42
+ account_id: str
43
+ region: str
44
+ service: str
45
+ error_code: str
46
+ error_message: str
47
+ timestamp: datetime
48
+ retry_count: int = 0
49
+
50
+ def is_retryable(self) -> bool:
51
+ """Check if this error type is retryable."""
52
+ retryable_codes = {"Throttling", "RequestLimitExceeded", "ServiceUnavailable", "InternalError", "NetworkError"}
53
+ return self.error_code in retryable_codes
54
+
55
+
56
+ @dataclass
57
+ class InventoryStatistics:
58
+ """Statistical summary of inventory collection."""
59
+
60
+ # Resource counts
61
+ total_resources: int = 0
62
+ resources_by_type: Dict[str, int] = field(default_factory=dict)
63
+ resources_by_account: Dict[str, int] = field(default_factory=dict)
64
+ resources_by_region: Dict[str, int] = field(default_factory=dict)
65
+ resources_by_service: Dict[str, int] = field(default_factory=dict)
66
+
67
+ # State distribution
68
+ active_resources: int = 0
69
+ inactive_resources: int = 0
70
+ billable_resources: int = 0
71
+
72
+ # Cost information
73
+ total_estimated_cost: float = 0.0
74
+ cost_by_account: Dict[str, float] = field(default_factory=dict)
75
+ cost_by_service: Dict[str, float] = field(default_factory=dict)
76
+
77
+ # Collection metrics
78
+ accounts_scanned: int = 0
79
+ regions_scanned: int = 0
80
+ services_scanned: int = 0
81
+ api_calls_made: int = 0
82
+
83
+ # Error tracking
84
+ total_errors: int = 0
85
+ errors_by_service: Dict[str, int] = field(default_factory=dict)
86
+
87
+ def add_resource(self, resource: AWSResource) -> None:
88
+ """Add a resource to the statistics."""
89
+ self.total_resources += 1
90
+
91
+ # Update type count
92
+ resource_type = resource.resource_type
93
+ self.resources_by_type[resource_type] = self.resources_by_type.get(resource_type, 0) + 1
94
+
95
+ # Update account count
96
+ account_id = resource.account_id
97
+ self.resources_by_account[account_id] = self.resources_by_account.get(account_id, 0) + 1
98
+
99
+ # Update region count
100
+ region = resource.region
101
+ self.resources_by_region[region] = self.resources_by_region.get(region, 0) + 1
102
+
103
+ # Update service count
104
+ service = resource.get_service_name()
105
+ self.resources_by_service[service] = self.resources_by_service.get(service, 0) + 1
106
+
107
+ # Update state counts
108
+ if resource.is_active():
109
+ self.active_resources += 1
110
+ else:
111
+ self.inactive_resources += 1
112
+
113
+ if resource.is_billable():
114
+ self.billable_resources += 1
115
+
116
+ # Update cost information
117
+ cost = resource.get_cost_estimate()
118
+ if cost > 0:
119
+ self.total_estimated_cost += cost
120
+
121
+ self.cost_by_account[account_id] = self.cost_by_account.get(account_id, 0.0) + cost
122
+
123
+ self.cost_by_service[service] = self.cost_by_service.get(service, 0.0) + cost
124
+
125
+ def add_error(self, error: CollectionError) -> None:
126
+ """Add an error to the statistics."""
127
+ self.total_errors += 1
128
+
129
+ service = error.service
130
+ self.errors_by_service[service] = self.errors_by_service.get(service, 0) + 1
131
+
132
+ def get_success_rate(self) -> float:
133
+ """Calculate collection success rate."""
134
+ total_operations = self.total_resources + self.total_errors
135
+ if total_operations == 0:
136
+ return 0.0
137
+ return (self.total_resources / total_operations) * 100
138
+
139
+ def get_top_resource_types(self, limit: int = 10) -> List[tuple]:
140
+ """Get top resource types by count."""
141
+ return sorted(self.resources_by_type.items(), key=lambda x: x[1], reverse=True)[:limit]
142
+
143
+ def get_most_expensive_services(self, limit: int = 10) -> List[tuple]:
144
+ """Get most expensive services by cost."""
145
+ return sorted(self.cost_by_service.items(), key=lambda x: x[1], reverse=True)[:limit]
146
+
147
+
148
+ class InventoryMetadata(BaseModel):
149
+ """
150
+ Metadata about an inventory collection operation.
151
+
152
+ Provides comprehensive information about when, how, and what
153
+ was collected during an inventory operation.
154
+ """
155
+
156
+ # Collection identification
157
+ collection_id: str = Field(..., description="Unique identifier for this collection")
158
+
159
+ collection_name: Optional[str] = Field(None, description="Human-readable name for this collection")
160
+
161
+ # Timing information
162
+ start_time: datetime = Field(..., description="Collection start timestamp")
163
+
164
+ end_time: Optional[datetime] = Field(None, description="Collection completion timestamp")
165
+
166
+ duration: Optional[timedelta] = Field(None, description="Total collection duration")
167
+
168
+ # Scope and configuration
169
+ scope: CollectionScope = Field(..., description="Scope of this collection")
170
+
171
+ target_accounts: List[str] = Field(default_factory=list, description="Account IDs that were targeted")
172
+
173
+ target_regions: List[str] = Field(default_factory=list, description="Regions that were scanned")
174
+
175
+ target_resource_types: Set[str] = Field(default_factory=set, description="Resource types that were collected")
176
+
177
+ # Collection parameters
178
+ include_costs: bool = Field(False, description="Whether cost information was collected")
179
+
180
+ parallel_execution: bool = Field(True, description="Whether collection used parallel processing")
181
+
182
+ max_workers: Optional[int] = Field(None, description="Maximum number of worker threads used")
183
+
184
+ # Collector information
185
+ collector_version: str = Field(..., description="Version of collector used")
186
+
187
+ collector_config: Dict[str, Any] = Field(default_factory=dict, description="Collector configuration parameters")
188
+
189
+ # Status and results
190
+ status: InventoryStatus = Field(InventoryStatus.IN_PROGRESS, description="Current collection status")
191
+
192
+ status_message: Optional[str] = Field(None, description="Additional status information")
193
+
194
+ class Config:
195
+ """Pydantic configuration."""
196
+
197
+ use_enum_values = True
198
+ json_encoders = {
199
+ datetime: lambda v: v.isoformat(),
200
+ timedelta: lambda v: v.total_seconds(),
201
+ set: lambda v: list(v),
202
+ }
203
+
204
+ def mark_completed(self, status: InventoryStatus = InventoryStatus.SUCCESS) -> None:
205
+ """Mark collection as completed."""
206
+ self.end_time = datetime.utcnow()
207
+ self.duration = self.end_time - self.start_time
208
+ self.status = status
209
+
210
+ def get_duration_seconds(self) -> float:
211
+ """Get collection duration in seconds."""
212
+ if self.duration:
213
+ return self.duration.total_seconds()
214
+ if self.end_time:
215
+ return (self.end_time - self.start_time).total_seconds()
216
+ return (datetime.utcnow() - self.start_time).total_seconds()
217
+
218
+
219
+ class InventoryResult(BaseModel):
220
+ """
221
+ Complete results from an inventory collection operation.
222
+
223
+ Aggregates all resources discovered, statistics, errors, and metadata
224
+ from a complete inventory collection run.
225
+ """
226
+
227
+ # Core data
228
+ metadata: InventoryMetadata = Field(..., description="Collection metadata and configuration")
229
+
230
+ resources: List[AWSResource] = Field(default_factory=list, description="All discovered resources")
231
+
232
+ accounts: List[AWSAccount] = Field(default_factory=list, description="Account information for scanned accounts")
233
+
234
+ # Statistics and analytics
235
+ statistics: InventoryStatistics = Field(
236
+ default_factory=InventoryStatistics, description="Statistical summary of collection"
237
+ )
238
+
239
+ # Error tracking
240
+ errors: List[CollectionError] = Field(default_factory=list, description="Errors encountered during collection")
241
+
242
+ # Performance metrics
243
+ performance_metrics: Dict[str, Any] = Field(default_factory=dict, description="Performance and timing metrics")
244
+
245
+ class Config:
246
+ """Pydantic configuration."""
247
+
248
+ json_encoders = {datetime: lambda v: v.isoformat(), timedelta: lambda v: v.total_seconds()}
249
+
250
+ def add_resource(self, resource: AWSResource) -> None:
251
+ """Add a resource to the results."""
252
+ self.resources.append(resource)
253
+ self.statistics.add_resource(resource)
254
+
255
+ def add_error(self, error: CollectionError) -> None:
256
+ """Add an error to the results."""
257
+ self.errors.append(error)
258
+ self.statistics.add_error(error)
259
+
260
+ def get_resources_by_type(self, resource_type: str) -> List[AWSResource]:
261
+ """Get all resources of a specific type."""
262
+ return [r for r in self.resources if r.resource_type == resource_type]
263
+
264
+ def get_resources_by_account(self, account_id: str) -> List[AWSResource]:
265
+ """Get all resources from a specific account."""
266
+ return [r for r in self.resources if r.account_id == account_id]
267
+
268
+ def get_resources_by_region(self, region: str) -> List[AWSResource]:
269
+ """Get all resources from a specific region."""
270
+ return [r for r in self.resources if r.region == region]
271
+
272
+ def get_active_resources(self) -> List[AWSResource]:
273
+ """Get all active resources."""
274
+ return [r for r in self.resources if r.is_active()]
275
+
276
+ def get_billable_resources(self) -> List[AWSResource]:
277
+ """Get all billable resources."""
278
+ return [r for r in self.resources if r.is_billable()]
279
+
280
+ def get_resources_with_tag(self, key: str, value: Optional[str] = None) -> List[AWSResource]:
281
+ """Get resources that have a specific tag."""
282
+ return [r for r in self.resources if r.has_tag(key, value)]
283
+
284
+ def get_total_cost_estimate(self) -> float:
285
+ """Get total estimated monthly cost."""
286
+ return self.statistics.total_estimated_cost
287
+
288
+ def get_collection_summary(self) -> Dict[str, Any]:
289
+ """Get a summary of the collection results."""
290
+ return {
291
+ "collection_id": self.metadata.collection_id,
292
+ "status": self.metadata.status,
293
+ "duration_seconds": self.metadata.get_duration_seconds(),
294
+ "total_resources": self.statistics.total_resources,
295
+ "total_accounts": len(self.accounts),
296
+ "total_regions": self.statistics.regions_scanned,
297
+ "total_errors": self.statistics.total_errors,
298
+ "success_rate": self.statistics.get_success_rate(),
299
+ "total_cost_estimate": self.statistics.total_estimated_cost,
300
+ "scope": self.metadata.scope,
301
+ }
302
+
303
+ def to_dict(self) -> Dict[str, Any]:
304
+ """Convert to dictionary representation."""
305
+ return self.dict()
306
+
307
+ def __len__(self) -> int:
308
+ """Return number of resources collected."""
309
+ return len(self.resources)