regscale-cli 6.27.2.0__py3-none-any.whl → 6.28.0.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 (140) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +11 -2
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/dev/cli.py +26 -0
  10. regscale/dev/version.py +72 -0
  11. regscale/integrations/commercial/__init__.py +15 -1
  12. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  13. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  14. regscale/integrations/commercial/amazon/common.py +48 -58
  15. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  16. regscale/integrations/commercial/aws/cli.py +3093 -55
  17. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  18. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  19. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  20. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  21. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  22. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  23. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  24. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  25. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  26. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  27. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  28. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  29. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  30. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  31. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  32. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  33. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  34. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  35. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  36. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  37. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  38. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  39. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  40. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  41. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  42. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  43. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  44. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  45. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  46. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  47. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  48. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  49. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  50. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  51. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  52. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  53. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  54. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  55. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  56. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  57. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  58. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  59. regscale/integrations/commercial/aws/scanner.py +853 -205
  60. regscale/integrations/commercial/aws/security_hub.py +319 -0
  61. regscale/integrations/commercial/aws/session_manager.py +282 -0
  62. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  63. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  64. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  65. regscale/integrations/compliance_integration.py +308 -38
  66. regscale/integrations/control_matcher.py +78 -23
  67. regscale/integrations/due_date_handler.py +3 -0
  68. regscale/integrations/public/csam/csam.py +572 -763
  69. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  70. regscale/integrations/public/csam/csam_common.py +154 -0
  71. regscale/integrations/public/csam/csam_controls.py +432 -0
  72. regscale/integrations/public/csam/csam_poam.py +124 -0
  73. regscale/integrations/public/fedramp/click.py +17 -4
  74. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  75. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  76. regscale/integrations/scanner_integration.py +415 -85
  77. regscale/models/integration_models/cisa_kev_data.json +80 -20
  78. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  79. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
  80. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  81. regscale/models/platform.py +3 -0
  82. regscale/models/regscale_models/__init__.py +5 -0
  83. regscale/models/regscale_models/assessment.py +2 -1
  84. regscale/models/regscale_models/component.py +1 -1
  85. regscale/models/regscale_models/control_implementation.py +55 -24
  86. regscale/models/regscale_models/control_objective.py +74 -5
  87. regscale/models/regscale_models/file.py +2 -0
  88. regscale/models/regscale_models/issue.py +2 -5
  89. regscale/models/regscale_models/organization.py +3 -0
  90. regscale/models/regscale_models/regscale_model.py +17 -5
  91. regscale/models/regscale_models/security_plan.py +1 -0
  92. regscale/regscale.py +11 -1
  93. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  94. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
  95. tests/regscale/core/test_login.py +171 -4
  96. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  97. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  98. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  99. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  100. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  101. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  102. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  103. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  104. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  105. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  106. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  107. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  108. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  109. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  110. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  111. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  112. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  113. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  114. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  115. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  116. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  117. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  118. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  119. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  120. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  121. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  122. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  123. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  124. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  125. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  126. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  127. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  128. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  129. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  130. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  131. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  132. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  133. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  134. tests/regscale/integrations/commercial/test_aws.py +55 -56
  135. tests/regscale/integrations/test_control_matcher.py +24 -0
  136. tests/regscale/models/test_control_implementation.py +118 -3
  137. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  138. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  139. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  140. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,473 @@
1
+ """AWS Security Hub 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 SecurityHubCollector(BaseCollector):
14
+ """Collector for AWS Security Hub resources."""
15
+
16
+ def __init__(
17
+ self,
18
+ session: Any,
19
+ region: str,
20
+ account_id: Optional[str] = None,
21
+ tags: Optional[Dict[str, str]] = None,
22
+ collect_findings: bool = True,
23
+ ):
24
+ """
25
+ Initialize Security Hub collector.
26
+
27
+ :param session: AWS session to use for API calls
28
+ :param str region: AWS region to collect from
29
+ :param str account_id: Optional AWS account ID to filter resources
30
+ :param dict tags: Optional tags to filter resources (key-value pairs)
31
+ :param bool collect_findings: Whether to collect Security Hub findings. Default True.
32
+ """
33
+ super().__init__(session, region)
34
+ self.account_id = account_id
35
+ self.tags = tags or {}
36
+ self.collect_findings = collect_findings
37
+
38
+ def collect(self) -> Dict[str, Any]:
39
+ """
40
+ Collect AWS Security Hub resources.
41
+
42
+ :return: Dictionary containing Security Hub findings and configuration
43
+ :rtype: Dict[str, Any]
44
+ """
45
+ result = {
46
+ "Findings": [],
47
+ "Standards": [],
48
+ "EnabledStandards": [],
49
+ "SecurityControls": [],
50
+ "HubConfiguration": {},
51
+ "Members": [],
52
+ "Insights": [],
53
+ }
54
+
55
+ try:
56
+ client = self._get_client("securityhub")
57
+
58
+ # Get hub configuration
59
+ hub_config = self._describe_hub(client)
60
+ result["HubConfiguration"] = hub_config
61
+
62
+ # Get enabled standards
63
+ enabled_standards = self._get_enabled_standards(client)
64
+ result["EnabledStandards"] = enabled_standards
65
+
66
+ # Get standards
67
+ standards = self._describe_standards(client)
68
+ result["Standards"] = standards
69
+
70
+ # Get security controls
71
+ controls = self._list_security_controls(client)
72
+ result["SecurityControls"] = controls
73
+
74
+ # Get findings only if requested
75
+ if self.collect_findings:
76
+ findings = self._get_findings(client)
77
+ result["Findings"] = findings
78
+ else:
79
+ findings = []
80
+ logger.debug("Skipping Security Hub findings collection (collect_findings=False)")
81
+
82
+ # Get insights
83
+ insights = self._get_insights(client)
84
+ result["Insights"] = insights
85
+
86
+ # Get member accounts
87
+ members = self._list_members(client)
88
+ result["Members"] = members
89
+
90
+ if self.collect_findings:
91
+ logger.info(
92
+ f"Collected {len(findings)} Security Hub finding(s), "
93
+ f"{len(enabled_standards)} enabled standard(s) from {self.region}"
94
+ )
95
+ else:
96
+ logger.info(f"Collected {len(enabled_standards)} enabled standard(s) from {self.region}")
97
+
98
+ except ClientError as e:
99
+ self._handle_error(e, "AWS Security Hub resources")
100
+ except Exception as e:
101
+ logger.error(f"Unexpected error collecting AWS Security Hub resources: {e}", exc_info=True)
102
+
103
+ return result
104
+
105
+ def _describe_hub(self, client: Any) -> Dict[str, Any]:
106
+ """
107
+ Describe Security Hub configuration.
108
+
109
+ :param client: Security Hub client
110
+ :return: Hub configuration
111
+ :rtype: Dict[str, Any]
112
+ """
113
+ try:
114
+ response = client.describe_hub()
115
+ hub_config = {
116
+ "Region": self.region,
117
+ "HubArn": response.get("HubArn"),
118
+ "SubscribedAt": str(response.get("SubscribedAt")),
119
+ "AutoEnableControls": response.get("AutoEnableControls"),
120
+ "ControlFindingGenerator": response.get("ControlFindingGenerator"),
121
+ }
122
+ return hub_config
123
+ except ClientError as e:
124
+ if e.response["Error"]["Code"] in ["InvalidAccessException", "ResourceNotFoundException"]:
125
+ logger.warning(f"Security Hub not enabled or access denied in {self.region}")
126
+ else:
127
+ logger.error(f"Error describing Security Hub: {e}")
128
+ return {}
129
+
130
+ def _get_enabled_standards(self, client: Any) -> List[Dict[str, Any]]:
131
+ """
132
+ Get enabled Security Hub standards.
133
+
134
+ :param client: Security Hub client
135
+ :return: List of enabled standards
136
+ :rtype: List[Dict[str, Any]]
137
+ """
138
+ standards = []
139
+ next_token = None
140
+
141
+ try:
142
+ while True:
143
+ params = {}
144
+ if next_token:
145
+ params["NextToken"] = next_token
146
+
147
+ response = client.get_enabled_standards(**params)
148
+ standards_subscriptions = response.get("StandardsSubscriptions", [])
149
+
150
+ for standard in standards_subscriptions:
151
+ standard_dict = {
152
+ "Region": self.region,
153
+ "StandardsSubscriptionArn": standard.get("StandardsSubscriptionArn"),
154
+ "StandardsArn": standard.get("StandardsArn"),
155
+ "StandardsInput": standard.get("StandardsInput", {}),
156
+ "StandardsStatus": standard.get("StandardsStatus"),
157
+ }
158
+ standards.append(standard_dict)
159
+
160
+ next_token = response.get("NextToken")
161
+ if not next_token:
162
+ break
163
+
164
+ except ClientError as e:
165
+ if e.response["Error"]["Code"] == "InvalidAccessException":
166
+ logger.warning(f"Access denied to get enabled standards in {self.region}")
167
+ else:
168
+ logger.error(f"Error getting enabled standards: {e}")
169
+
170
+ return standards
171
+
172
+ def _describe_standards(self, client: Any) -> List[Dict[str, Any]]:
173
+ """
174
+ Describe available Security Hub standards.
175
+
176
+ :param client: Security Hub client
177
+ :return: List of available standards
178
+ :rtype: List[Dict[str, Any]]
179
+ """
180
+ standards = []
181
+ next_token = None
182
+
183
+ try:
184
+ while True:
185
+ params = {}
186
+ if next_token:
187
+ params["NextToken"] = next_token
188
+
189
+ response = client.describe_standards(**params)
190
+ standards_list = response.get("Standards", [])
191
+
192
+ for standard in standards_list:
193
+ standard_dict = {
194
+ "Region": self.region,
195
+ "StandardsArn": standard.get("StandardsArn"),
196
+ "Name": standard.get("Name"),
197
+ "Description": standard.get("Description"),
198
+ "EnabledByDefault": standard.get("EnabledByDefault"),
199
+ }
200
+ standards.append(standard_dict)
201
+
202
+ next_token = response.get("NextToken")
203
+ if not next_token:
204
+ break
205
+
206
+ except ClientError as e:
207
+ if e.response["Error"]["Code"] == "InvalidAccessException":
208
+ logger.warning(f"Access denied to describe standards in {self.region}")
209
+ else:
210
+ logger.error(f"Error describing standards: {e}")
211
+
212
+ return standards
213
+
214
+ def _list_security_controls(self, client: Any) -> List[Dict[str, Any]]:
215
+ """
216
+ List Security Hub security controls.
217
+
218
+ :param client: Security Hub client
219
+ :return: List of security controls
220
+ :rtype: List[Dict[str, Any]]
221
+ """
222
+ controls = []
223
+ next_token = None
224
+
225
+ try:
226
+ while True:
227
+ params = {"MaxResults": 100}
228
+ if next_token:
229
+ params["NextToken"] = next_token
230
+
231
+ response = client.list_security_control_definitions(**params)
232
+ control_list = response.get("SecurityControlDefinitions", [])
233
+
234
+ for control in control_list:
235
+ control_dict = {
236
+ "Region": self.region,
237
+ "SecurityControlId": control.get("SecurityControlId"),
238
+ "Title": control.get("Title"),
239
+ "Description": control.get("Description"),
240
+ "RemediationUrl": control.get("RemediationUrl"),
241
+ "SeverityRating": control.get("SeverityRating"),
242
+ "CurrentRegionAvailability": control.get("CurrentRegionAvailability"),
243
+ }
244
+ controls.append(control_dict)
245
+
246
+ next_token = response.get("NextToken")
247
+ if not next_token:
248
+ break
249
+
250
+ except ClientError as e:
251
+ if e.response["Error"]["Code"] == "InvalidAccessException":
252
+ logger.warning(f"Access denied to list security controls in {self.region}")
253
+ else:
254
+ logger.error(f"Error listing security controls: {e}")
255
+
256
+ return controls
257
+
258
+ def _build_findings_filters(self) -> Dict[str, Any]:
259
+ """
260
+ Build filters for Security Hub findings query.
261
+
262
+ :return: Dictionary of filters
263
+ :rtype: Dict[str, Any]
264
+ """
265
+ filters = {}
266
+
267
+ if self.account_id:
268
+ filters["AwsAccountId"] = [{"Value": self.account_id, "Comparison": "EQUALS"}]
269
+
270
+ if self.tags:
271
+ filters["ResourceTags"] = self._build_tag_filters()
272
+
273
+ return filters
274
+
275
+ def _build_tag_filters(self) -> List[Dict[str, str]]:
276
+ """
277
+ Build tag filters for Security Hub findings query.
278
+
279
+ :return: List of tag filter dictionaries
280
+ :rtype: List[Dict[str, str]]
281
+ """
282
+ tag_filters = []
283
+ for key, value in self.tags.items():
284
+ tag_filters.append({"Key": key, "Value": value, "Comparison": "EQUALS"})
285
+ return tag_filters
286
+
287
+ def _add_region_to_findings(self, finding_list: List[Dict[str, Any]]) -> None:
288
+ """
289
+ Add region information to each finding.
290
+
291
+ :param finding_list: List of findings to modify
292
+ """
293
+ for finding in finding_list:
294
+ finding["Region"] = self.region
295
+
296
+ def _get_findings(self, client: Any, max_results: int = 100) -> List[Dict[str, Any]]:
297
+ """
298
+ Get Security Hub findings with pagination.
299
+
300
+ :param client: Security Hub client
301
+ :param int max_results: Maximum number of results per page
302
+ :return: List of findings
303
+ :rtype: List[Dict[str, Any]]
304
+ """
305
+ findings = []
306
+ next_token = None
307
+
308
+ try:
309
+ while True:
310
+ params = {"MaxResults": max_results}
311
+ filters = self._build_findings_filters()
312
+
313
+ if filters:
314
+ params["Filters"] = filters
315
+
316
+ if next_token:
317
+ params["NextToken"] = next_token
318
+
319
+ response = client.get_findings(**params)
320
+ finding_list = response.get("Findings", [])
321
+
322
+ self._add_region_to_findings(finding_list)
323
+ findings.extend(finding_list)
324
+
325
+ next_token = response.get("NextToken")
326
+ if not next_token:
327
+ break
328
+
329
+ except ClientError as e:
330
+ self._handle_findings_error(e)
331
+
332
+ return findings
333
+
334
+ def _handle_findings_error(self, error: ClientError) -> None:
335
+ """
336
+ Handle errors when getting findings.
337
+
338
+ :param error: ClientError exception
339
+ """
340
+ if error.response["Error"]["Code"] == "InvalidAccessException":
341
+ logger.warning(f"Access denied to get findings in {self.region}")
342
+ else:
343
+ logger.error(f"Error getting findings: {error}")
344
+
345
+ def _get_insights(self, client: Any) -> List[Dict[str, Any]]:
346
+ """
347
+ Get Security Hub insights.
348
+
349
+ :param client: Security Hub client
350
+ :return: List of insights
351
+ :rtype: List[Dict[str, Any]]
352
+ """
353
+ insights = []
354
+ next_token = None
355
+
356
+ try:
357
+ while True:
358
+ params = {"MaxResults": 100}
359
+ if next_token:
360
+ params["NextToken"] = next_token
361
+
362
+ response = client.get_insights(**params)
363
+ insight_list = response.get("Insights", [])
364
+
365
+ for insight in insight_list:
366
+ insight_dict = {
367
+ "Region": self.region,
368
+ "InsightArn": insight.get("InsightArn"),
369
+ "Name": insight.get("Name"),
370
+ "Filters": insight.get("Filters", {}),
371
+ "GroupByAttribute": insight.get("GroupByAttribute"),
372
+ }
373
+ insights.append(insight_dict)
374
+
375
+ next_token = response.get("NextToken")
376
+ if not next_token:
377
+ break
378
+
379
+ except ClientError as e:
380
+ if e.response["Error"]["Code"] == "InvalidAccessException":
381
+ logger.warning(f"Access denied to get insights in {self.region}")
382
+ else:
383
+ logger.error(f"Error getting insights: {e}")
384
+
385
+ return insights
386
+
387
+ def _should_include_member(self, member: Dict[str, Any]) -> bool:
388
+ """
389
+ Determine if a member should be included based on account ID filter.
390
+
391
+ :param member: Member account dictionary
392
+ :return: True if member should be included, False otherwise
393
+ :rtype: bool
394
+ """
395
+ if not self.account_id:
396
+ return True
397
+ return member.get("AccountId") == self.account_id
398
+
399
+ def _format_member_dict(self, member: Dict[str, Any]) -> Dict[str, Any]:
400
+ """
401
+ Format member account data into standardized dictionary.
402
+
403
+ :param member: Raw member data from API
404
+ :return: Formatted member dictionary
405
+ :rtype: Dict[str, Any]
406
+ """
407
+ return {
408
+ "Region": self.region,
409
+ "AccountId": member.get("AccountId"),
410
+ "Email": member.get("Email"),
411
+ "MasterId": member.get("MasterId"),
412
+ "AdministratorId": member.get("AdministratorId"),
413
+ "MemberStatus": member.get("MemberStatus"),
414
+ "InvitedAt": str(member.get("InvitedAt")) if member.get("InvitedAt") else None,
415
+ "UpdatedAt": str(member.get("UpdatedAt")) if member.get("UpdatedAt") else None,
416
+ }
417
+
418
+ def _process_member_list(self, member_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
419
+ """
420
+ Process and filter member list.
421
+
422
+ :param member_list: Raw list of members from API
423
+ :return: Processed and filtered list of members
424
+ :rtype: List[Dict[str, Any]]
425
+ """
426
+ processed_members = []
427
+ for member in member_list:
428
+ if self._should_include_member(member):
429
+ processed_members.append(self._format_member_dict(member))
430
+ return processed_members
431
+
432
+ def _list_members(self, client: Any) -> List[Dict[str, Any]]:
433
+ """
434
+ List Security Hub member accounts.
435
+
436
+ :param client: Security Hub client
437
+ :return: List of member accounts
438
+ :rtype: List[Dict[str, Any]]
439
+ """
440
+ members = []
441
+ next_token = None
442
+
443
+ try:
444
+ while True:
445
+ params = {"MaxResults": 50}
446
+ if next_token:
447
+ params["NextToken"] = next_token
448
+
449
+ response = client.list_members(**params)
450
+ member_list = response.get("Members", [])
451
+
452
+ processed_members = self._process_member_list(member_list)
453
+ members.extend(processed_members)
454
+
455
+ next_token = response.get("NextToken")
456
+ if not next_token:
457
+ break
458
+
459
+ except ClientError as e:
460
+ self._handle_list_members_error(e)
461
+
462
+ return members
463
+
464
+ def _handle_list_members_error(self, error: ClientError) -> None:
465
+ """
466
+ Handle errors when listing members.
467
+
468
+ :param error: ClientError exception
469
+ """
470
+ if error.response["Error"]["Code"] == "InvalidAccessException":
471
+ logger.debug(f"Access denied to list members in {self.region}")
472
+ else:
473
+ logger.error(f"Error listing members: {error}")
@@ -1,49 +1,52 @@
1
1
  """AWS storage resource collectors."""
2
2
 
3
- from typing import Dict, List, Any
3
+ from typing import Dict, List, Any, Optional
4
4
 
5
+ from regscale.integrations.commercial.aws.inventory.resources.s3 import S3Collector
5
6
  from ..base import BaseCollector
6
7
 
7
8
 
8
9
  class StorageCollector(BaseCollector):
9
- """Collector for AWS storage resources."""
10
+ """Collector for AWS storage resources with filtering support."""
11
+
12
+ def __init__(
13
+ self,
14
+ session: Any,
15
+ region: str,
16
+ account_id: Optional[str] = None,
17
+ tags: Optional[Dict[str, str]] = None,
18
+ enabled_services: Optional[Dict[str, bool]] = None,
19
+ ):
20
+ """
21
+ Initialize storage collector with filtering support.
22
+
23
+ :param session: AWS session to use for API calls
24
+ :param str region: AWS region to collect from
25
+ :param str account_id: Optional AWS account ID to filter resources
26
+ :param dict tags: Optional tag filters (AND logic)
27
+ :param dict enabled_services: Optional dict of service names to boolean flags for enabling/disabling collection
28
+ """
29
+ super().__init__(session, region, account_id, tags)
30
+ self.enabled_services = enabled_services or {}
10
31
 
11
32
  def get_s3_buckets(self) -> List[Dict[str, Any]]:
12
33
  """
13
- Get information about S3 buckets.
34
+ Get information about S3 buckets with filtering.
14
35
 
15
36
  :return: List of S3 bucket information
16
37
  :rtype: List[Dict[str, Any]]
17
38
  """
18
- buckets = []
19
39
  try:
20
- s3 = self._get_client("s3")
21
- response = s3.list_buckets()
22
-
23
- for bucket in response.get("Buckets", []):
24
- try:
25
- location = s3.get_bucket_location(Bucket=bucket["Name"])
26
- region = location.get("LocationConstraint") or "us-east-1"
27
-
28
- # Only include buckets in the target region
29
- if region == self.region:
30
- buckets.append(
31
- {
32
- "Region": self.region,
33
- "Name": bucket["Name"],
34
- "CreationDate": str(bucket["CreationDate"]),
35
- "Location": region,
36
- }
37
- )
38
- except Exception as e:
39
- self._handle_error(e, f"S3 bucket {bucket['Name']}")
40
+ s3_collector = S3Collector(self.session, self.region, self.account_id, self.tags)
41
+ result = s3_collector.collect()
42
+ return result.get("Buckets", [])
40
43
  except Exception as e:
41
44
  self._handle_error(e, "S3 buckets")
42
- return buckets
45
+ return []
43
46
 
44
47
  def get_ebs_volumes(self) -> List[Dict[str, Any]]:
45
48
  """
46
- Get information about EBS volumes.
49
+ Get information about EBS volumes with tag filtering.
47
50
 
48
51
  :return: List of EBS volume information
49
52
  :rtype: List[Dict[str, Any]]
@@ -55,6 +58,17 @@ class StorageCollector(BaseCollector):
55
58
 
56
59
  for page in paginator.paginate():
57
60
  for volume in page.get("Volumes", []):
61
+ # Apply tag filtering
62
+ if self.tags and not self._matches_tags(volume.get("Tags", [])):
63
+ continue
64
+
65
+ # Apply account filtering if ARN available
66
+ volume_arn = (
67
+ f"arn:aws:ec2:{self.region}:{volume.get('OwnerId', 'unknown')}:volume/{volume.get('VolumeId')}"
68
+ )
69
+ if not self._matches_account(volume_arn):
70
+ continue
71
+
58
72
  attachments = volume.get("Attachments", [])
59
73
  volumes.append(
60
74
  {
@@ -83,9 +97,19 @@ class StorageCollector(BaseCollector):
83
97
 
84
98
  def collect(self) -> Dict[str, Any]:
85
99
  """
86
- Collect all storage resources.
100
+ Collect storage resources based on enabled_services configuration.
87
101
 
88
- :return: Dictionary containing all storage resource information
102
+ :return: Dictionary containing enabled storage resource information
89
103
  :rtype: Dict[str, Any]
90
104
  """
91
- return {"S3Buckets": self.get_s3_buckets(), "EBSVolumes": self.get_ebs_volumes()}
105
+ result = {}
106
+
107
+ # S3 Buckets
108
+ if self.enabled_services.get("s3", True):
109
+ result["S3Buckets"] = self.get_s3_buckets()
110
+
111
+ # EBS Volumes
112
+ if self.enabled_services.get("ebs", True):
113
+ result["EBSVolumes"] = self.get_ebs_volumes()
114
+
115
+ return result