aws-inventory-manager 0.17.12__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.
- aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
- aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
- aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/aws/__init__.py +11 -0
- src/aws/client.py +128 -0
- src/aws/credentials.py +191 -0
- src/aws/rate_limiter.py +177 -0
- src/cli/__init__.py +12 -0
- src/cli/config.py +130 -0
- src/cli/main.py +4046 -0
- src/cloudtrail/__init__.py +5 -0
- src/cloudtrail/query.py +642 -0
- src/config_service/__init__.py +21 -0
- src/config_service/collector.py +346 -0
- src/config_service/detector.py +256 -0
- src/config_service/resource_type_mapping.py +328 -0
- src/cost/__init__.py +5 -0
- src/cost/analyzer.py +226 -0
- src/cost/explorer.py +209 -0
- src/cost/reporter.py +237 -0
- src/delta/__init__.py +5 -0
- src/delta/calculator.py +206 -0
- src/delta/differ.py +185 -0
- src/delta/formatters.py +272 -0
- src/delta/models.py +154 -0
- src/delta/reporter.py +234 -0
- src/matching/__init__.py +6 -0
- src/matching/config.py +52 -0
- src/matching/normalizer.py +450 -0
- src/matching/prompts.py +33 -0
- src/models/__init__.py +21 -0
- src/models/config_diff.py +135 -0
- src/models/cost_report.py +87 -0
- src/models/deletion_operation.py +104 -0
- src/models/deletion_record.py +97 -0
- src/models/delta_report.py +122 -0
- src/models/efs_resource.py +80 -0
- src/models/elasticache_resource.py +90 -0
- src/models/group.py +318 -0
- src/models/inventory.py +133 -0
- src/models/protection_rule.py +123 -0
- src/models/report.py +288 -0
- src/models/resource.py +111 -0
- src/models/security_finding.py +102 -0
- src/models/snapshot.py +122 -0
- src/restore/__init__.py +20 -0
- src/restore/audit.py +175 -0
- src/restore/cleaner.py +461 -0
- src/restore/config.py +209 -0
- src/restore/deleter.py +976 -0
- src/restore/dependency.py +254 -0
- src/restore/safety.py +115 -0
- src/security/__init__.py +0 -0
- src/security/checks/__init__.py +0 -0
- src/security/checks/base.py +56 -0
- src/security/checks/ec2_checks.py +88 -0
- src/security/checks/elasticache_checks.py +149 -0
- src/security/checks/iam_checks.py +102 -0
- src/security/checks/rds_checks.py +140 -0
- src/security/checks/s3_checks.py +95 -0
- src/security/checks/secrets_checks.py +96 -0
- src/security/checks/sg_checks.py +142 -0
- src/security/cis_mapper.py +97 -0
- src/security/models.py +53 -0
- src/security/reporter.py +174 -0
- src/security/scanner.py +87 -0
- src/snapshot/__init__.py +6 -0
- src/snapshot/capturer.py +453 -0
- src/snapshot/filter.py +259 -0
- src/snapshot/inventory_storage.py +236 -0
- src/snapshot/report_formatter.py +250 -0
- src/snapshot/reporter.py +189 -0
- src/snapshot/resource_collectors/__init__.py +5 -0
- src/snapshot/resource_collectors/apigateway.py +140 -0
- src/snapshot/resource_collectors/backup.py +136 -0
- src/snapshot/resource_collectors/base.py +81 -0
- src/snapshot/resource_collectors/cloudformation.py +55 -0
- src/snapshot/resource_collectors/cloudwatch.py +109 -0
- src/snapshot/resource_collectors/codebuild.py +69 -0
- src/snapshot/resource_collectors/codepipeline.py +82 -0
- src/snapshot/resource_collectors/dynamodb.py +65 -0
- src/snapshot/resource_collectors/ec2.py +240 -0
- src/snapshot/resource_collectors/ecs.py +215 -0
- src/snapshot/resource_collectors/efs_collector.py +102 -0
- src/snapshot/resource_collectors/eks.py +200 -0
- src/snapshot/resource_collectors/elasticache_collector.py +79 -0
- src/snapshot/resource_collectors/elb.py +126 -0
- src/snapshot/resource_collectors/eventbridge.py +156 -0
- src/snapshot/resource_collectors/glue.py +199 -0
- src/snapshot/resource_collectors/iam.py +188 -0
- src/snapshot/resource_collectors/kms.py +111 -0
- src/snapshot/resource_collectors/lambda_func.py +139 -0
- src/snapshot/resource_collectors/rds.py +109 -0
- src/snapshot/resource_collectors/route53.py +86 -0
- src/snapshot/resource_collectors/s3.py +105 -0
- src/snapshot/resource_collectors/secretsmanager.py +70 -0
- src/snapshot/resource_collectors/sns.py +68 -0
- src/snapshot/resource_collectors/sqs.py +82 -0
- src/snapshot/resource_collectors/ssm.py +160 -0
- src/snapshot/resource_collectors/stepfunctions.py +74 -0
- src/snapshot/resource_collectors/vpcendpoints.py +79 -0
- src/snapshot/resource_collectors/waf.py +159 -0
- src/snapshot/storage.py +351 -0
- src/storage/__init__.py +21 -0
- src/storage/audit_store.py +419 -0
- src/storage/database.py +294 -0
- src/storage/group_store.py +763 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +416 -0
- src/storage/schema.py +339 -0
- src/storage/snapshot_store.py +363 -0
- src/utils/__init__.py +12 -0
- src/utils/export.py +305 -0
- src/utils/hash.py +60 -0
- src/utils/logging.py +63 -0
- src/utils/pagination.py +41 -0
- src/utils/paths.py +51 -0
- src/utils/progress.py +41 -0
- src/utils/unsupported_resources.py +306 -0
- src/web/__init__.py +5 -0
- src/web/app.py +97 -0
- src/web/dependencies.py +69 -0
- src/web/routes/__init__.py +1 -0
- src/web/routes/api/__init__.py +18 -0
- src/web/routes/api/charts.py +156 -0
- src/web/routes/api/cleanup.py +186 -0
- src/web/routes/api/filters.py +253 -0
- src/web/routes/api/groups.py +305 -0
- src/web/routes/api/inventories.py +80 -0
- src/web/routes/api/queries.py +202 -0
- src/web/routes/api/resources.py +393 -0
- src/web/routes/api/snapshots.py +314 -0
- src/web/routes/api/views.py +260 -0
- src/web/routes/pages.py +198 -0
- src/web/services/__init__.py +1 -0
- src/web/templates/base.html +955 -0
- src/web/templates/components/navbar.html +31 -0
- src/web/templates/components/sidebar.html +104 -0
- src/web/templates/pages/audit_logs.html +86 -0
- src/web/templates/pages/cleanup.html +279 -0
- src/web/templates/pages/dashboard.html +227 -0
- src/web/templates/pages/diff.html +175 -0
- src/web/templates/pages/error.html +30 -0
- src/web/templates/pages/groups.html +721 -0
- src/web/templates/pages/queries.html +246 -0
- src/web/templates/pages/resources.html +2429 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- src/web/templates/pages/snapshots.html +429 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""AWS Glue resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GlueCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS Glue resources (databases, tables, crawlers, jobs)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "glue"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect AWS Glue resources.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of Glue resources (databases, tables, crawlers, jobs)
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
client = self._create_client()
|
|
25
|
+
account_id = self._get_account_id()
|
|
26
|
+
|
|
27
|
+
# Collect databases and tables
|
|
28
|
+
resources.extend(self._collect_databases(client, account_id))
|
|
29
|
+
|
|
30
|
+
# Collect crawlers
|
|
31
|
+
resources.extend(self._collect_crawlers(client, account_id))
|
|
32
|
+
|
|
33
|
+
# Collect jobs
|
|
34
|
+
resources.extend(self._collect_jobs(client, account_id))
|
|
35
|
+
|
|
36
|
+
# Collect connections
|
|
37
|
+
resources.extend(self._collect_connections(client, account_id))
|
|
38
|
+
|
|
39
|
+
self.logger.debug(f"Collected {len(resources)} Glue resources in {self.region}")
|
|
40
|
+
return resources
|
|
41
|
+
|
|
42
|
+
def _collect_databases(self, client, account_id: str) -> List[Resource]:
|
|
43
|
+
"""Collect Glue databases and their tables."""
|
|
44
|
+
resources = []
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
paginator = client.get_paginator("get_databases")
|
|
48
|
+
for page in paginator.paginate():
|
|
49
|
+
for db in page.get("DatabaseList", []):
|
|
50
|
+
db_name = db.get("Name")
|
|
51
|
+
db_arn = f"arn:aws:glue:{self.region}:{account_id}:database/{db_name}"
|
|
52
|
+
|
|
53
|
+
resource = Resource(
|
|
54
|
+
arn=db_arn,
|
|
55
|
+
resource_type="AWS::Glue::Database",
|
|
56
|
+
name=db_name,
|
|
57
|
+
region=self.region,
|
|
58
|
+
tags={}, # Glue databases don't support tags directly
|
|
59
|
+
config_hash=compute_config_hash(db),
|
|
60
|
+
created_at=db.get("CreateTime"),
|
|
61
|
+
raw_config=db,
|
|
62
|
+
)
|
|
63
|
+
resources.append(resource)
|
|
64
|
+
|
|
65
|
+
# Collect tables for this database
|
|
66
|
+
resources.extend(self._collect_tables(client, account_id, db_name))
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
self.logger.error(f"Error collecting Glue databases in {self.region}: {e}")
|
|
70
|
+
|
|
71
|
+
return resources
|
|
72
|
+
|
|
73
|
+
def _collect_tables(self, client, account_id: str, database_name: str) -> List[Resource]:
|
|
74
|
+
"""Collect tables for a specific database."""
|
|
75
|
+
resources = []
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
paginator = client.get_paginator("get_tables")
|
|
79
|
+
for page in paginator.paginate(DatabaseName=database_name):
|
|
80
|
+
for table in page.get("TableList", []):
|
|
81
|
+
table_name = table.get("Name")
|
|
82
|
+
table_arn = f"arn:aws:glue:{self.region}:{account_id}:table/{database_name}/{table_name}"
|
|
83
|
+
|
|
84
|
+
resource = Resource(
|
|
85
|
+
arn=table_arn,
|
|
86
|
+
resource_type="AWS::Glue::Table",
|
|
87
|
+
name=f"{database_name}/{table_name}",
|
|
88
|
+
region=self.region,
|
|
89
|
+
tags={}, # Glue tables don't support tags directly
|
|
90
|
+
config_hash=compute_config_hash(table),
|
|
91
|
+
created_at=table.get("CreateTime"),
|
|
92
|
+
raw_config=table,
|
|
93
|
+
)
|
|
94
|
+
resources.append(resource)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.debug(f"Error collecting tables for database {database_name}: {e}")
|
|
98
|
+
|
|
99
|
+
return resources
|
|
100
|
+
|
|
101
|
+
def _collect_crawlers(self, client, account_id: str) -> List[Resource]:
|
|
102
|
+
"""Collect Glue crawlers."""
|
|
103
|
+
resources = []
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
paginator = client.get_paginator("get_crawlers")
|
|
107
|
+
for page in paginator.paginate():
|
|
108
|
+
for crawler in page.get("Crawlers", []):
|
|
109
|
+
crawler_name = crawler.get("Name")
|
|
110
|
+
crawler_arn = f"arn:aws:glue:{self.region}:{account_id}:crawler/{crawler_name}"
|
|
111
|
+
|
|
112
|
+
# Get tags for crawler
|
|
113
|
+
tags = {}
|
|
114
|
+
try:
|
|
115
|
+
tag_response = client.get_tags(ResourceArn=crawler_arn)
|
|
116
|
+
tags = tag_response.get("Tags", {})
|
|
117
|
+
except Exception as e:
|
|
118
|
+
self.logger.debug(f"Could not get tags for crawler {crawler_name}: {e}")
|
|
119
|
+
|
|
120
|
+
resource = Resource(
|
|
121
|
+
arn=crawler_arn,
|
|
122
|
+
resource_type="AWS::Glue::Crawler",
|
|
123
|
+
name=crawler_name,
|
|
124
|
+
region=self.region,
|
|
125
|
+
tags=tags,
|
|
126
|
+
config_hash=compute_config_hash(crawler),
|
|
127
|
+
created_at=crawler.get("CreationTime"),
|
|
128
|
+
raw_config=crawler,
|
|
129
|
+
)
|
|
130
|
+
resources.append(resource)
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self.logger.error(f"Error collecting Glue crawlers in {self.region}: {e}")
|
|
134
|
+
|
|
135
|
+
return resources
|
|
136
|
+
|
|
137
|
+
def _collect_jobs(self, client, account_id: str) -> List[Resource]:
|
|
138
|
+
"""Collect Glue jobs."""
|
|
139
|
+
resources = []
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
paginator = client.get_paginator("get_jobs")
|
|
143
|
+
for page in paginator.paginate():
|
|
144
|
+
for job in page.get("Jobs", []):
|
|
145
|
+
job_name = job.get("Name")
|
|
146
|
+
job_arn = f"arn:aws:glue:{self.region}:{account_id}:job/{job_name}"
|
|
147
|
+
|
|
148
|
+
# Get tags for job
|
|
149
|
+
tags = {}
|
|
150
|
+
try:
|
|
151
|
+
tag_response = client.get_tags(ResourceArn=job_arn)
|
|
152
|
+
tags = tag_response.get("Tags", {})
|
|
153
|
+
except Exception as e:
|
|
154
|
+
self.logger.debug(f"Could not get tags for job {job_name}: {e}")
|
|
155
|
+
|
|
156
|
+
resource = Resource(
|
|
157
|
+
arn=job_arn,
|
|
158
|
+
resource_type="AWS::Glue::Job",
|
|
159
|
+
name=job_name,
|
|
160
|
+
region=self.region,
|
|
161
|
+
tags=tags,
|
|
162
|
+
config_hash=compute_config_hash(job),
|
|
163
|
+
created_at=job.get("CreatedOn"),
|
|
164
|
+
raw_config=job,
|
|
165
|
+
)
|
|
166
|
+
resources.append(resource)
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
self.logger.error(f"Error collecting Glue jobs in {self.region}: {e}")
|
|
170
|
+
|
|
171
|
+
return resources
|
|
172
|
+
|
|
173
|
+
def _collect_connections(self, client, account_id: str) -> List[Resource]:
|
|
174
|
+
"""Collect Glue connections."""
|
|
175
|
+
resources = []
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
paginator = client.get_paginator("get_connections")
|
|
179
|
+
for page in paginator.paginate():
|
|
180
|
+
for conn in page.get("ConnectionList", []):
|
|
181
|
+
conn_name = conn.get("Name")
|
|
182
|
+
conn_arn = f"arn:aws:glue:{self.region}:{account_id}:connection/{conn_name}"
|
|
183
|
+
|
|
184
|
+
resource = Resource(
|
|
185
|
+
arn=conn_arn,
|
|
186
|
+
resource_type="AWS::Glue::Connection",
|
|
187
|
+
name=conn_name,
|
|
188
|
+
region=self.region,
|
|
189
|
+
tags={}, # Connections don't support tags
|
|
190
|
+
config_hash=compute_config_hash(conn),
|
|
191
|
+
created_at=conn.get("CreationTime"),
|
|
192
|
+
raw_config=conn,
|
|
193
|
+
)
|
|
194
|
+
resources.append(resource)
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self.logger.error(f"Error collecting Glue connections in {self.region}: {e}")
|
|
198
|
+
|
|
199
|
+
return resources
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""IAM resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IAMCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS IAM resources (roles, users, groups, policies)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "iam"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def is_global_service(self) -> bool:
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
def collect(self) -> List[Resource]:
|
|
22
|
+
"""Collect IAM resources.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
List of IAM resources (roles, users, groups, policies)
|
|
26
|
+
"""
|
|
27
|
+
resources = []
|
|
28
|
+
account_id = self._get_account_id()
|
|
29
|
+
|
|
30
|
+
# Collect roles
|
|
31
|
+
resources.extend(self._collect_roles(account_id))
|
|
32
|
+
|
|
33
|
+
# Collect users
|
|
34
|
+
resources.extend(self._collect_users(account_id))
|
|
35
|
+
|
|
36
|
+
# Collect groups
|
|
37
|
+
resources.extend(self._collect_groups(account_id))
|
|
38
|
+
|
|
39
|
+
# Collect policies (customer-managed only)
|
|
40
|
+
resources.extend(self._collect_policies(account_id))
|
|
41
|
+
|
|
42
|
+
self.logger.debug(f"Collected {len(resources)} IAM resources")
|
|
43
|
+
return resources
|
|
44
|
+
|
|
45
|
+
def _collect_roles(self, account_id: str) -> List[Resource]:
|
|
46
|
+
"""Collect IAM roles."""
|
|
47
|
+
resources = []
|
|
48
|
+
client = self._create_client()
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
paginator = client.get_paginator("list_roles")
|
|
52
|
+
for page in paginator.paginate():
|
|
53
|
+
for role in page["Roles"]:
|
|
54
|
+
# Build ARN
|
|
55
|
+
arn = role["Arn"]
|
|
56
|
+
|
|
57
|
+
# Extract tags
|
|
58
|
+
tags = {}
|
|
59
|
+
try:
|
|
60
|
+
tag_response = client.list_role_tags(RoleName=role["RoleName"])
|
|
61
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("Tags", [])}
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.logger.debug(f"Could not get tags for role {role['RoleName']}: {e}")
|
|
64
|
+
|
|
65
|
+
# Create resource
|
|
66
|
+
resource = Resource(
|
|
67
|
+
arn=arn,
|
|
68
|
+
resource_type="AWS::IAM::Role",
|
|
69
|
+
name=role["RoleName"],
|
|
70
|
+
region="global",
|
|
71
|
+
tags=tags,
|
|
72
|
+
config_hash=compute_config_hash(role),
|
|
73
|
+
created_at=role.get("CreateDate"),
|
|
74
|
+
raw_config=role,
|
|
75
|
+
)
|
|
76
|
+
resources.append(resource)
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
self.logger.error(f"Error collecting IAM roles: {e}")
|
|
80
|
+
|
|
81
|
+
return resources
|
|
82
|
+
|
|
83
|
+
def _collect_users(self, account_id: str) -> List[Resource]:
|
|
84
|
+
"""Collect IAM users."""
|
|
85
|
+
resources = []
|
|
86
|
+
client = self._create_client()
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
paginator = client.get_paginator("list_users")
|
|
90
|
+
for page in paginator.paginate():
|
|
91
|
+
for user in page["Users"]:
|
|
92
|
+
# Build ARN
|
|
93
|
+
arn = user["Arn"]
|
|
94
|
+
|
|
95
|
+
# Extract tags
|
|
96
|
+
tags = {}
|
|
97
|
+
try:
|
|
98
|
+
tag_response = client.list_user_tags(UserName=user["UserName"])
|
|
99
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("Tags", [])}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self.logger.debug(f"Could not get tags for user {user['UserName']}: {e}")
|
|
102
|
+
|
|
103
|
+
# Create resource
|
|
104
|
+
resource = Resource(
|
|
105
|
+
arn=arn,
|
|
106
|
+
resource_type="AWS::IAM::User",
|
|
107
|
+
name=user["UserName"],
|
|
108
|
+
region="global",
|
|
109
|
+
tags=tags,
|
|
110
|
+
config_hash=compute_config_hash(user),
|
|
111
|
+
created_at=user.get("CreateDate"),
|
|
112
|
+
raw_config=user,
|
|
113
|
+
)
|
|
114
|
+
resources.append(resource)
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
self.logger.error(f"Error collecting IAM users: {e}")
|
|
118
|
+
|
|
119
|
+
return resources
|
|
120
|
+
|
|
121
|
+
def _collect_groups(self, account_id: str) -> List[Resource]:
|
|
122
|
+
"""Collect IAM groups."""
|
|
123
|
+
resources = []
|
|
124
|
+
client = self._create_client()
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
paginator = client.get_paginator("list_groups")
|
|
128
|
+
for page in paginator.paginate():
|
|
129
|
+
for group in page["Groups"]:
|
|
130
|
+
# Build ARN
|
|
131
|
+
arn = group["Arn"]
|
|
132
|
+
|
|
133
|
+
# Create resource (groups don't support tags)
|
|
134
|
+
resource = Resource(
|
|
135
|
+
arn=arn,
|
|
136
|
+
resource_type="AWS::IAM::Group",
|
|
137
|
+
name=group["GroupName"],
|
|
138
|
+
region="global",
|
|
139
|
+
tags={},
|
|
140
|
+
config_hash=compute_config_hash(group),
|
|
141
|
+
created_at=group.get("CreateDate"),
|
|
142
|
+
raw_config=group,
|
|
143
|
+
)
|
|
144
|
+
resources.append(resource)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
self.logger.error(f"Error collecting IAM groups: {e}")
|
|
148
|
+
|
|
149
|
+
return resources
|
|
150
|
+
|
|
151
|
+
def _collect_policies(self, account_id: str) -> List[Resource]:
|
|
152
|
+
"""Collect customer-managed IAM policies."""
|
|
153
|
+
resources = []
|
|
154
|
+
client = self._create_client()
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
paginator = client.get_paginator("list_policies")
|
|
158
|
+
# Only get customer-managed policies (not AWS-managed)
|
|
159
|
+
for page in paginator.paginate(Scope="Local"):
|
|
160
|
+
for policy in page["Policies"]:
|
|
161
|
+
# Build ARN
|
|
162
|
+
arn = policy["Arn"]
|
|
163
|
+
|
|
164
|
+
# Extract tags
|
|
165
|
+
tags = {}
|
|
166
|
+
try:
|
|
167
|
+
tag_response = client.list_policy_tags(PolicyArn=arn)
|
|
168
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("Tags", [])}
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self.logger.debug(f"Could not get tags for policy {policy['PolicyName']}: {e}")
|
|
171
|
+
|
|
172
|
+
# Create resource
|
|
173
|
+
resource = Resource(
|
|
174
|
+
arn=arn,
|
|
175
|
+
resource_type="AWS::IAM::Policy",
|
|
176
|
+
name=policy["PolicyName"],
|
|
177
|
+
region="global",
|
|
178
|
+
tags=tags,
|
|
179
|
+
config_hash=compute_config_hash(policy),
|
|
180
|
+
created_at=policy.get("CreateDate"),
|
|
181
|
+
raw_config=policy,
|
|
182
|
+
)
|
|
183
|
+
resources.append(resource)
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
self.logger.error(f"Error collecting IAM policies: {e}")
|
|
187
|
+
|
|
188
|
+
return resources
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""KMS resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KMSCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS KMS (Key Management Service) resources."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "kms"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect KMS keys.
|
|
19
|
+
|
|
20
|
+
Collects customer-managed KMS keys (not AWS-managed keys).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of KMS key resources
|
|
24
|
+
"""
|
|
25
|
+
resources = []
|
|
26
|
+
client = self._create_client()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
paginator = client.get_paginator("list_keys")
|
|
30
|
+
for page in paginator.paginate():
|
|
31
|
+
for key_item in page.get("Keys", []):
|
|
32
|
+
key_id = key_item["KeyId"]
|
|
33
|
+
key_arn = key_item["KeyArn"]
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Get key metadata
|
|
37
|
+
key_metadata = client.describe_key(KeyId=key_id)["KeyMetadata"]
|
|
38
|
+
|
|
39
|
+
# Skip AWS-managed keys (we only want customer-managed keys)
|
|
40
|
+
if key_metadata.get("KeyManager") == "AWS":
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# Skip keys that are pending deletion
|
|
44
|
+
if key_metadata.get("KeyState") == "PendingDeletion":
|
|
45
|
+
self.logger.debug(f"Skipping key {key_id} - pending deletion")
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
key_alias = None
|
|
49
|
+
|
|
50
|
+
# Get key aliases
|
|
51
|
+
try:
|
|
52
|
+
aliases_response = client.list_aliases(KeyId=key_id)
|
|
53
|
+
aliases = aliases_response.get("Aliases", [])
|
|
54
|
+
if aliases:
|
|
55
|
+
# Use first alias as the name
|
|
56
|
+
key_alias = aliases[0]["AliasName"]
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.logger.debug(f"Could not get aliases for key {key_id}: {e}")
|
|
59
|
+
|
|
60
|
+
# Get tags
|
|
61
|
+
tags = {}
|
|
62
|
+
try:
|
|
63
|
+
tag_response = client.list_resource_tags(KeyId=key_id)
|
|
64
|
+
for tag in tag_response.get("Tags", []):
|
|
65
|
+
tags[tag["TagKey"]] = tag["TagValue"]
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.debug(f"Could not get tags for key {key_id}: {e}")
|
|
68
|
+
|
|
69
|
+
# Get key rotation status
|
|
70
|
+
rotation_enabled = False
|
|
71
|
+
try:
|
|
72
|
+
rotation_response = client.get_key_rotation_status(KeyId=key_id)
|
|
73
|
+
rotation_enabled = rotation_response.get("KeyRotationEnabled", False)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.logger.debug(f"Could not get rotation status for key {key_id}: {e}")
|
|
76
|
+
|
|
77
|
+
# Build config for hash
|
|
78
|
+
config = {
|
|
79
|
+
**key_metadata,
|
|
80
|
+
"RotationEnabled": rotation_enabled,
|
|
81
|
+
"Aliases": [key_alias] if key_alias else [],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Extract creation date
|
|
85
|
+
created_at = key_metadata.get("CreationDate")
|
|
86
|
+
|
|
87
|
+
# Use alias as name if available, otherwise use key ID
|
|
88
|
+
name = key_alias if key_alias else key_id
|
|
89
|
+
|
|
90
|
+
# Create resource
|
|
91
|
+
resource = Resource(
|
|
92
|
+
arn=key_arn,
|
|
93
|
+
resource_type="AWS::KMS::Key",
|
|
94
|
+
name=name,
|
|
95
|
+
region=self.region,
|
|
96
|
+
tags=tags,
|
|
97
|
+
config_hash=compute_config_hash(config),
|
|
98
|
+
created_at=created_at,
|
|
99
|
+
raw_config=config,
|
|
100
|
+
)
|
|
101
|
+
resources.append(resource)
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.logger.debug(f"Error processing key {key_id}: {e}")
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
self.logger.error(f"Error collecting KMS keys in {self.region}: {e}")
|
|
109
|
+
|
|
110
|
+
self.logger.debug(f"Collected {len(resources)} KMS keys in {self.region}")
|
|
111
|
+
return resources
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Lambda resource collector."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from ...models.resource import Resource
|
|
7
|
+
from ...utils.hash import compute_config_hash
|
|
8
|
+
from .base import BaseResourceCollector
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_lambda_timestamp(timestamp_str: Optional[str]) -> Optional[datetime]:
|
|
12
|
+
"""Parse Lambda's ISO-8601 timestamp format.
|
|
13
|
+
|
|
14
|
+
Lambda returns timestamps like: 2024-01-15T10:30:00.000+0000
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
timestamp_str: ISO-8601 formatted timestamp string
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Parsed datetime or None if parsing fails
|
|
21
|
+
"""
|
|
22
|
+
if not timestamp_str:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
# Lambda format: 2024-01-15T10:30:00.000+0000
|
|
26
|
+
# Python's fromisoformat doesn't handle +0000 format, need to normalize
|
|
27
|
+
normalized = timestamp_str.replace("+0000", "+00:00").replace("-0000", "+00:00")
|
|
28
|
+
return datetime.fromisoformat(normalized)
|
|
29
|
+
except (ValueError, AttributeError):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LambdaCollector(BaseResourceCollector):
|
|
34
|
+
"""Collector for AWS Lambda functions and layers."""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def service_name(self) -> str:
|
|
38
|
+
return "lambda"
|
|
39
|
+
|
|
40
|
+
def collect(self) -> List[Resource]:
|
|
41
|
+
"""Collect Lambda resources.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of Lambda functions and layers
|
|
45
|
+
"""
|
|
46
|
+
resources = []
|
|
47
|
+
account_id = self._get_account_id()
|
|
48
|
+
|
|
49
|
+
# Collect functions
|
|
50
|
+
resources.extend(self._collect_functions(account_id))
|
|
51
|
+
|
|
52
|
+
# Collect layers
|
|
53
|
+
resources.extend(self._collect_layers(account_id))
|
|
54
|
+
|
|
55
|
+
self.logger.debug(f"Collected {len(resources)} Lambda resources in {self.region}")
|
|
56
|
+
return resources
|
|
57
|
+
|
|
58
|
+
def _collect_functions(self, account_id: str) -> List[Resource]:
|
|
59
|
+
"""Collect Lambda functions."""
|
|
60
|
+
resources = []
|
|
61
|
+
client = self._create_client()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
paginator = client.get_paginator("list_functions")
|
|
65
|
+
for page in paginator.paginate():
|
|
66
|
+
for function in page["Functions"]:
|
|
67
|
+
function_name = function["FunctionName"]
|
|
68
|
+
function_arn = function["FunctionArn"]
|
|
69
|
+
|
|
70
|
+
# Get full function configuration (includes tags)
|
|
71
|
+
try:
|
|
72
|
+
full_config = client.get_function(FunctionName=function_name)
|
|
73
|
+
tags = full_config.get("Tags", {})
|
|
74
|
+
config_data = full_config.get("Configuration", function)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self.logger.debug(f"Could not get full config for {function_name}: {e}")
|
|
77
|
+
tags = {}
|
|
78
|
+
config_data = function
|
|
79
|
+
|
|
80
|
+
# Parse LastModified timestamp (set on creation and updates)
|
|
81
|
+
last_modified = _parse_lambda_timestamp(config_data.get("LastModified"))
|
|
82
|
+
|
|
83
|
+
# Create resource
|
|
84
|
+
resource = Resource(
|
|
85
|
+
arn=function_arn,
|
|
86
|
+
resource_type="AWS::Lambda::Function",
|
|
87
|
+
name=function_name,
|
|
88
|
+
region=self.region,
|
|
89
|
+
tags=tags,
|
|
90
|
+
config_hash=compute_config_hash(config_data),
|
|
91
|
+
created_at=last_modified,
|
|
92
|
+
raw_config=config_data,
|
|
93
|
+
)
|
|
94
|
+
resources.append(resource)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.error(f"Error collecting Lambda functions in {self.region}: {e}")
|
|
98
|
+
|
|
99
|
+
return resources
|
|
100
|
+
|
|
101
|
+
def _collect_layers(self, account_id: str) -> List[Resource]:
|
|
102
|
+
"""Collect Lambda layers."""
|
|
103
|
+
resources = []
|
|
104
|
+
client = self._create_client()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
paginator = client.get_paginator("list_layers")
|
|
108
|
+
for page in paginator.paginate():
|
|
109
|
+
for layer in page["Layers"]:
|
|
110
|
+
layer_name = layer["LayerName"]
|
|
111
|
+
layer_arn = layer["LayerArn"]
|
|
112
|
+
|
|
113
|
+
# Get latest version info
|
|
114
|
+
try:
|
|
115
|
+
latest_version = layer.get("LatestMatchingVersion", {})
|
|
116
|
+
layer_version_arn = latest_version.get("LayerVersionArn", layer_arn)
|
|
117
|
+
# Parse CreatedDate timestamp (same format as function LastModified)
|
|
118
|
+
created_at = _parse_lambda_timestamp(latest_version.get("CreatedDate"))
|
|
119
|
+
|
|
120
|
+
# Create resource
|
|
121
|
+
resource = Resource(
|
|
122
|
+
arn=layer_version_arn,
|
|
123
|
+
resource_type="AWS::Lambda::LayerVersion",
|
|
124
|
+
name=layer_name,
|
|
125
|
+
region=self.region,
|
|
126
|
+
tags={}, # Layers don't support tags
|
|
127
|
+
config_hash=compute_config_hash(layer),
|
|
128
|
+
created_at=created_at,
|
|
129
|
+
raw_config=layer,
|
|
130
|
+
)
|
|
131
|
+
resources.append(resource)
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.logger.debug(f"Could not get layer version for {layer_name}: {e}")
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
self.logger.error(f"Error collecting Lambda layers in {self.region}: {e}")
|
|
138
|
+
|
|
139
|
+
return resources
|