regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.1.0__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.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (113) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/utils/app_utils.py +11 -2
  3. regscale/dev/cli.py +26 -0
  4. regscale/dev/version.py +72 -0
  5. regscale/integrations/commercial/__init__.py +15 -1
  6. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  7. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  8. regscale/integrations/commercial/amazon/common.py +48 -58
  9. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  10. regscale/integrations/commercial/aws/cli.py +3093 -55
  11. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  12. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  13. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  14. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  15. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  16. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  17. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  18. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  19. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  20. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  21. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  22. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  23. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  24. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  25. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  26. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  27. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  28. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  29. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  30. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  31. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  32. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  33. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  34. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  35. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  36. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  37. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  38. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  39. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  40. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  41. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  42. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  43. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  44. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  45. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  46. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  47. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  48. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  49. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  50. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  51. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  52. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  53. regscale/integrations/commercial/aws/scanner.py +851 -206
  54. regscale/integrations/commercial/aws/security_hub.py +319 -0
  55. regscale/integrations/commercial/aws/session_manager.py +282 -0
  56. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  57. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  58. regscale/integrations/commercial/synqly/ticketing.py +27 -0
  59. regscale/integrations/compliance_integration.py +308 -38
  60. regscale/integrations/due_date_handler.py +3 -0
  61. regscale/integrations/scanner_integration.py +399 -84
  62. regscale/models/integration_models/cisa_kev_data.json +65 -5
  63. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  64. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  65. regscale/models/regscale_models/assessment.py +2 -1
  66. regscale/models/regscale_models/control_objective.py +74 -5
  67. regscale/models/regscale_models/file.py +2 -0
  68. regscale/models/regscale_models/issue.py +2 -5
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/METADATA +1 -1
  70. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
  71. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  73. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  86. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  87. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  89. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  90. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  91. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  92. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  94. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  96. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  98. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  99. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  100. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  101. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  103. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  104. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  105. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  106. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  108. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  109. tests/regscale/integrations/commercial/test_aws.py +55 -56
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/LICENSE +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
  113. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,394 @@
1
+ """AWS S3 resource collection."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from botocore.exceptions import ClientError
7
+
8
+ from regscale.integrations.commercial.aws.inventory.base import BaseCollector
9
+
10
+ logger = logging.getLogger("regscale")
11
+
12
+
13
+ class S3Collector(BaseCollector):
14
+ """Collector for AWS S3 resources."""
15
+
16
+ def __init__(
17
+ self, session: Any, region: str, account_id: Optional[str] = None, tags: Optional[Dict[str, str]] = None
18
+ ):
19
+ """
20
+ Initialize S3 collector.
21
+
22
+ :param session: AWS session to use for API calls
23
+ :param str region: AWS region to collect from
24
+ :param str account_id: Optional AWS account ID to filter resources
25
+ :param dict tags: Optional tags to filter resources (key-value pairs)
26
+ """
27
+ super().__init__(session, region, account_id, tags)
28
+
29
+ def collect(self) -> Dict[str, Any]:
30
+ """
31
+ Collect AWS S3 resources.
32
+
33
+ :return: Dictionary containing S3 bucket information
34
+ :rtype: Dict[str, Any]
35
+ """
36
+ result = {"Buckets": []}
37
+
38
+ try:
39
+ client = self._get_client("s3")
40
+
41
+ # Get all buckets
42
+ buckets = self._list_buckets(client)
43
+ result["Buckets"] = buckets
44
+
45
+ logger.info(f"Collected {len(buckets)} S3 bucket(s) from {self.region}")
46
+
47
+ except ClientError as e:
48
+ self._handle_error(e, "S3 buckets")
49
+ except Exception as e:
50
+ logger.error(f"Unexpected error collecting S3 resources: {e}", exc_info=True)
51
+
52
+ return result
53
+
54
+ def _list_buckets(self, client: Any) -> List[Dict[str, Any]]:
55
+ """
56
+ List S3 buckets with enhanced details.
57
+
58
+ :param client: S3 client
59
+ :return: List of bucket information
60
+ :rtype: List[Dict[str, Any]]
61
+ """
62
+ buckets = []
63
+ try:
64
+ response = client.list_buckets()
65
+
66
+ for bucket in response.get("Buckets", []):
67
+ bucket_name = bucket["Name"]
68
+ bucket_dict = self._process_bucket(client, bucket, bucket_name)
69
+
70
+ if bucket_dict:
71
+ buckets.append(bucket_dict)
72
+
73
+ except ClientError as e:
74
+ self._handle_list_buckets_error(e)
75
+
76
+ return buckets
77
+
78
+ def _process_bucket(self, client: Any, bucket: Dict[str, Any], bucket_name: str) -> Optional[Dict[str, Any]]:
79
+ """
80
+ Process a single bucket and return its details if it passes filters.
81
+
82
+ :param client: S3 client
83
+ :param dict bucket: Bucket information from list_buckets
84
+ :param str bucket_name: Bucket name
85
+ :return: Bucket details dictionary or None if bucket should be skipped
86
+ :rtype: Optional[Dict[str, Any]]
87
+ """
88
+ try:
89
+ # Get bucket location
90
+ location = self._get_bucket_location(client, bucket_name)
91
+
92
+ # Only include buckets in the target region
93
+ if location != self.region:
94
+ return None
95
+
96
+ # Build bucket details dictionary
97
+ bucket_dict = self._build_bucket_details(client, bucket, bucket_name, location)
98
+
99
+ # Apply tag filtering if configured
100
+ if not self._should_include_bucket(bucket_dict):
101
+ return None
102
+
103
+ return bucket_dict
104
+
105
+ except ClientError as e:
106
+ self._handle_bucket_processing_error(e, bucket_name)
107
+ return None
108
+
109
+ def _build_bucket_details(
110
+ self, client: Any, bucket: Dict[str, Any], bucket_name: str, location: str
111
+ ) -> Dict[str, Any]:
112
+ """
113
+ Build complete details dictionary for a bucket.
114
+
115
+ :param client: S3 client
116
+ :param dict bucket: Bucket information from list_buckets
117
+ :param str bucket_name: Bucket name
118
+ :param str location: Bucket location/region
119
+ :return: Complete bucket details dictionary
120
+ :rtype: Dict[str, Any]
121
+ """
122
+ bucket_dict = {
123
+ "Region": self.region,
124
+ "Name": bucket_name,
125
+ "CreationDate": str(bucket["CreationDate"]),
126
+ "Location": location,
127
+ }
128
+
129
+ # Get encryption configuration
130
+ encryption = self._get_bucket_encryption(client, bucket_name)
131
+ bucket_dict["Encryption"] = encryption
132
+
133
+ # Get versioning configuration
134
+ versioning = self._get_bucket_versioning(client, bucket_name)
135
+ bucket_dict["Versioning"] = versioning
136
+
137
+ # Get public access block configuration
138
+ public_access_block = self._get_public_access_block(client, bucket_name)
139
+ bucket_dict["PublicAccessBlock"] = public_access_block
140
+
141
+ # Get bucket policy status
142
+ policy_status = self._get_bucket_policy_status(client, bucket_name)
143
+ bucket_dict["PolicyStatus"] = policy_status
144
+
145
+ # Get bucket ACL
146
+ acl = self._get_bucket_acl(client, bucket_name)
147
+ bucket_dict["ACL"] = acl
148
+
149
+ # Get bucket tagging
150
+ tags = self._get_bucket_tagging(client, bucket_name)
151
+ bucket_dict["Tags"] = tags
152
+
153
+ # Get bucket logging
154
+ logging_config = self._get_bucket_logging(client, bucket_name)
155
+ bucket_dict["Logging"] = logging_config
156
+
157
+ return bucket_dict
158
+
159
+ def _should_include_bucket(self, bucket_dict: Dict[str, Any]) -> bool:
160
+ """
161
+ Check if bucket should be included based on tag filters.
162
+
163
+ :param dict bucket_dict: Bucket details dictionary
164
+ :return: True if bucket should be included, False otherwise
165
+ :rtype: bool
166
+ """
167
+ if not self.tags:
168
+ return True
169
+
170
+ tags = bucket_dict.get("Tags", [])
171
+ bucket_tags_dict = self._convert_tags_to_dict(tags)
172
+
173
+ if not self._matches_tags(bucket_tags_dict):
174
+ bucket_name = bucket_dict.get("Name", "unknown")
175
+ logger.debug("Skipping bucket %s - does not match tag filters", bucket_name)
176
+ return False
177
+
178
+ return True
179
+
180
+ def _handle_list_buckets_error(self, error: ClientError) -> None:
181
+ """
182
+ Handle errors from list_buckets operation.
183
+
184
+ :param ClientError error: The client error to handle
185
+ """
186
+ error_code = error.response["Error"]["Code"]
187
+ if error_code == "AccessDenied":
188
+ logger.warning("Access denied to list S3 buckets")
189
+ else:
190
+ logger.error("Error listing S3 buckets: %s", error)
191
+
192
+ def _handle_bucket_processing_error(self, error: ClientError, bucket_name: str) -> None:
193
+ """
194
+ Handle errors during bucket detail processing.
195
+
196
+ :param ClientError error: The client error to handle
197
+ :param str bucket_name: Name of the bucket being processed
198
+ """
199
+ error_code = error.response["Error"]["Code"]
200
+ if error_code not in ["NoSuchBucket", "AccessDenied"]:
201
+ logger.error("Error getting details for bucket %s: %s", bucket_name, error)
202
+
203
+ def _get_bucket_location(self, client: Any, bucket_name: str) -> str:
204
+ """
205
+ Get bucket location.
206
+
207
+ :param client: S3 client
208
+ :param str bucket_name: Bucket name
209
+ :return: Bucket region
210
+ :rtype: str
211
+ """
212
+ try:
213
+ response = client.get_bucket_location(Bucket=bucket_name)
214
+ location = response.get("LocationConstraint") or "us-east-1"
215
+ return location
216
+ except ClientError:
217
+ return "unknown"
218
+
219
+ def _get_bucket_encryption(self, client: Any, bucket_name: str) -> Dict[str, Any]:
220
+ """
221
+ Get bucket encryption configuration.
222
+
223
+ :param client: S3 client
224
+ :param str bucket_name: Bucket name
225
+ :return: Encryption configuration
226
+ :rtype: Dict[str, Any]
227
+ """
228
+ try:
229
+ response = client.get_bucket_encryption(Bucket=bucket_name)
230
+ rules = response.get("ServerSideEncryptionConfiguration", {}).get("Rules", [])
231
+ if rules:
232
+ return {
233
+ "Enabled": True,
234
+ "Algorithm": rules[0].get("ApplyServerSideEncryptionByDefault", {}).get("SSEAlgorithm"),
235
+ "KMSMasterKeyID": rules[0].get("ApplyServerSideEncryptionByDefault", {}).get("KMSMasterKeyID"),
236
+ }
237
+ return {"Enabled": False}
238
+ except ClientError as e:
239
+ if e.response["Error"]["Code"] == "ServerSideEncryptionConfigurationNotFoundError":
240
+ return {"Enabled": False}
241
+ logger.debug(f"Error getting encryption for bucket {bucket_name}: {e}")
242
+ return {}
243
+
244
+ def _get_bucket_versioning(self, client: Any, bucket_name: str) -> Dict[str, Any]:
245
+ """
246
+ Get bucket versioning configuration.
247
+
248
+ :param client: S3 client
249
+ :param str bucket_name: Bucket name
250
+ :return: Versioning configuration
251
+ :rtype: Dict[str, Any]
252
+ """
253
+ try:
254
+ response = client.get_bucket_versioning(Bucket=bucket_name)
255
+ return {"Status": response.get("Status", "Disabled"), "MFADelete": response.get("MFADelete", "Disabled")}
256
+ except ClientError as e:
257
+ logger.debug(f"Error getting versioning for bucket {bucket_name}: {e}")
258
+ return {}
259
+
260
+ def _get_public_access_block(self, client: Any, bucket_name: str) -> Dict[str, Any]:
261
+ """
262
+ Get public access block configuration.
263
+
264
+ :param client: S3 client
265
+ :param str bucket_name: Bucket name
266
+ :return: Public access block configuration
267
+ :rtype: Dict[str, Any]
268
+ """
269
+ try:
270
+ response = client.get_public_access_block(Bucket=bucket_name)
271
+ config = response.get("PublicAccessBlockConfiguration", {})
272
+ return {
273
+ "BlockPublicAcls": config.get("BlockPublicAcls", False),
274
+ "IgnorePublicAcls": config.get("IgnorePublicAcls", False),
275
+ "BlockPublicPolicy": config.get("BlockPublicPolicy", False),
276
+ "RestrictPublicBuckets": config.get("RestrictPublicBuckets", False),
277
+ }
278
+ except ClientError as e:
279
+ if e.response["Error"]["Code"] == "NoSuchPublicAccessBlockConfiguration":
280
+ return {
281
+ "BlockPublicAcls": False,
282
+ "IgnorePublicAcls": False,
283
+ "BlockPublicPolicy": False,
284
+ "RestrictPublicBuckets": False,
285
+ }
286
+ logger.debug(f"Error getting public access block for bucket {bucket_name}: {e}")
287
+ return {}
288
+
289
+ def _get_bucket_policy_status(self, client: Any, bucket_name: str) -> Dict[str, Any]:
290
+ """
291
+ Get bucket policy status.
292
+
293
+ :param client: S3 client
294
+ :param str bucket_name: Bucket name
295
+ :return: Policy status
296
+ :rtype: Dict[str, Any]
297
+ """
298
+ try:
299
+ response = client.get_bucket_policy_status(Bucket=bucket_name)
300
+ policy_status = response.get("PolicyStatus", {})
301
+ return {"IsPublic": policy_status.get("IsPublic", False)}
302
+ except ClientError as e:
303
+ if e.response["Error"]["Code"] == "NoSuchBucketPolicy":
304
+ return {"IsPublic": False}
305
+ logger.debug(f"Error getting policy status for bucket {bucket_name}: {e}")
306
+ return {}
307
+
308
+ def _get_bucket_acl(self, client: Any, bucket_name: str) -> Dict[str, Any]:
309
+ """
310
+ Get bucket ACL.
311
+
312
+ :param client: S3 client
313
+ :param str bucket_name: Bucket name
314
+ :return: ACL information
315
+ :rtype: Dict[str, Any]
316
+ """
317
+ try:
318
+ response = client.get_bucket_acl(Bucket=bucket_name)
319
+ return {"Owner": response.get("Owner", {}), "GrantCount": len(response.get("Grants", []))}
320
+ except ClientError as e:
321
+ logger.debug(f"Error getting ACL for bucket {bucket_name}: {e}")
322
+ return {}
323
+
324
+ def _get_bucket_tagging(self, client: Any, bucket_name: str) -> List[Dict[str, str]]:
325
+ """
326
+ Get bucket tags.
327
+
328
+ :param client: S3 client
329
+ :param str bucket_name: Bucket name
330
+ :return: List of tags
331
+ :rtype: List[Dict[str, str]]
332
+ """
333
+ try:
334
+ response = client.get_bucket_tagging(Bucket=bucket_name)
335
+ return response.get("TagSet", [])
336
+ except ClientError as e:
337
+ if e.response["Error"]["Code"] == "NoSuchTagSet":
338
+ return []
339
+ logger.debug(f"Error getting tags for bucket {bucket_name}: {e}")
340
+ return []
341
+
342
+ def _get_bucket_logging(self, client: Any, bucket_name: str) -> Dict[str, Any]:
343
+ """
344
+ Get bucket logging configuration.
345
+
346
+ :param client: S3 client
347
+ :param str bucket_name: Bucket name
348
+ :return: Logging configuration
349
+ :rtype: Dict[str, Any]
350
+ """
351
+ try:
352
+ response = client.get_bucket_logging(Bucket=bucket_name)
353
+ logging_enabled = response.get("LoggingEnabled", {})
354
+ if logging_enabled:
355
+ return {
356
+ "Enabled": True,
357
+ "TargetBucket": logging_enabled.get("TargetBucket"),
358
+ "TargetPrefix": logging_enabled.get("TargetPrefix"),
359
+ }
360
+ return {"Enabled": False}
361
+ except ClientError as e:
362
+ logger.debug(f"Error getting logging for bucket {bucket_name}: {e}")
363
+ return {}
364
+
365
+ def _convert_tags_to_dict(self, tags_list: List[Dict[str, str]]) -> Dict[str, str]:
366
+ """
367
+ Convert S3 tags list format to dictionary format.
368
+
369
+ S3 returns tags as list of dicts: [{"Key": "k1", "Value": "v1"}]
370
+ Convert to dict format: {"k1": "v1"}
371
+
372
+ :param list tags_list: List of tag dictionaries
373
+ :return: Dictionary of tags (Key -> Value)
374
+ :rtype: Dict[str, str]
375
+ """
376
+ return {tag.get("Key", ""): tag.get("Value", "") for tag in tags_list}
377
+
378
+ def _matches_tags(self, resource_tags: Dict[str, str]) -> bool:
379
+ """
380
+ Check if resource tags match the specified filter tags.
381
+
382
+ :param dict resource_tags: Tags on the resource
383
+ :return: True if all filter tags match
384
+ :rtype: bool
385
+ """
386
+ if not self.tags:
387
+ return True
388
+
389
+ # All filter tags must match
390
+ for key, value in self.tags.items():
391
+ if resource_tags.get(key) != value:
392
+ return False
393
+
394
+ return True