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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -0
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/login.py +4 -1
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/core/login.py +21 -4
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +15 -1
- regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/amazon/common.py +204 -0
- regscale/integrations/commercial/amazon/common.py +48 -58
- regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
- regscale/integrations/commercial/aws/cli.py +3093 -55
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +853 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/synqly/query_builder.py +4 -1
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/control_matcher.py +78 -23
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/public/csam/csam.py +572 -763
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +17 -4
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
- regscale/integrations/public/fedramp/poam/scanner.py +74 -7
- regscale/integrations/scanner_integration.py +415 -85
- regscale/models/integration_models/cisa_kev_data.json +80 -20
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
- regscale/models/platform.py +3 -0
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/component.py +1 -1
- regscale/models/regscale_models/control_implementation.py +55 -24
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/issue.py +2 -5
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +17 -5
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/regscale.py +11 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/test_aws.py +55 -56
- tests/regscale/integrations/test_control_matcher.py +24 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
100
|
+
Collect storage resources based on enabled_services configuration.
|
|
87
101
|
|
|
88
|
-
:return: Dictionary containing
|
|
102
|
+
:return: Dictionary containing enabled storage resource information
|
|
89
103
|
:rtype: Dict[str, Any]
|
|
90
104
|
"""
|
|
91
|
-
|
|
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
|