regscale-cli 6.27.3.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/utils/app_utils.py +11 -2
- 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 +851 -206
- 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/compliance_integration.py +308 -38
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/scanner_integration.py +399 -84
- regscale/models/integration_models/cisa_kev_data.json +34 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
- regscale/models/regscale_models/assessment.py +2 -1
- 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_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
- 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
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""AWS resource inventory collection module."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
from typing import Dict, Any, Optional
|
|
5
6
|
|
|
6
7
|
from regscale.integrations.commercial.aws.inventory.base import BaseCollector
|
|
7
8
|
from regscale.integrations.commercial.aws.inventory.resources.compute import ComputeCollector
|
|
9
|
+
from regscale.integrations.commercial.aws.inventory.resources.config import ConfigCollector
|
|
8
10
|
from regscale.integrations.commercial.aws.inventory.resources.containers import ContainerCollector
|
|
9
11
|
from regscale.integrations.commercial.aws.inventory.resources.database import DatabaseCollector
|
|
10
12
|
from regscale.integrations.commercial.aws.inventory.resources.integration import IntegrationCollector
|
|
@@ -12,6 +14,8 @@ from regscale.integrations.commercial.aws.inventory.resources.networking import
|
|
|
12
14
|
from regscale.integrations.commercial.aws.inventory.resources.security import SecurityCollector
|
|
13
15
|
from regscale.integrations.commercial.aws.inventory.resources.storage import StorageCollector
|
|
14
16
|
|
|
17
|
+
logger = logging.getLogger("regscale")
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
class AWSInventoryCollector:
|
|
17
21
|
"""Collects inventory of AWS resources."""
|
|
@@ -19,40 +23,211 @@ class AWSInventoryCollector:
|
|
|
19
23
|
def __init__(
|
|
20
24
|
self,
|
|
21
25
|
region: str = os.getenv("AWS_REGION", "us-east-1"),
|
|
26
|
+
profile: Optional[str] = None,
|
|
22
27
|
aws_access_key_id: Optional[str] = None,
|
|
23
28
|
aws_secret_access_key: Optional[str] = None,
|
|
24
29
|
aws_session_token: Optional[str] = None,
|
|
30
|
+
account_id: Optional[str] = None,
|
|
31
|
+
tags: Optional[Dict[str, str]] = None,
|
|
32
|
+
enabled_services: Optional[Dict[str, bool]] = None,
|
|
33
|
+
collect_findings: bool = True,
|
|
25
34
|
):
|
|
26
35
|
"""
|
|
27
36
|
Initialize the AWS inventory collector.
|
|
28
37
|
|
|
29
38
|
:param str region: AWS region to collect inventory from
|
|
30
|
-
:param str
|
|
31
|
-
:param str
|
|
32
|
-
:param str
|
|
39
|
+
:param str profile: Optional AWS profile name from ~/.aws/credentials
|
|
40
|
+
:param str aws_access_key_id: Optional AWS access key ID (overrides profile)
|
|
41
|
+
:param str aws_secret_access_key: Optional AWS secret access key (overrides profile)
|
|
42
|
+
:param str aws_session_token: Optional AWS session token (overrides profile)
|
|
43
|
+
:param str account_id: Optional AWS account ID to filter resources
|
|
44
|
+
:param dict tags: Optional dictionary of tag key-value pairs to filter resources
|
|
45
|
+
:param dict enabled_services: Optional dictionary of service names to boolean flags for enabling/disabling collection
|
|
46
|
+
:param bool collect_findings: Whether to collect security findings (GuardDuty, Security Hub, Inspector). Default True.
|
|
33
47
|
"""
|
|
34
48
|
import boto3
|
|
35
49
|
|
|
36
50
|
self.region = region
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
self.account_id = account_id
|
|
52
|
+
self.tags = tags
|
|
53
|
+
self.enabled_services = self._get_enabled_services(enabled_services)
|
|
54
|
+
|
|
55
|
+
# If explicit credentials are provided, use them; otherwise use profile
|
|
56
|
+
if aws_access_key_id or aws_secret_access_key:
|
|
57
|
+
self.session = boto3.Session(
|
|
58
|
+
aws_access_key_id=aws_access_key_id,
|
|
59
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
60
|
+
region_name=region,
|
|
61
|
+
aws_session_token=aws_session_token,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
# Use profile or default credential chain
|
|
65
|
+
self.session = boto3.Session(
|
|
66
|
+
profile_name=profile,
|
|
67
|
+
region_name=region,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Initialize collectors based on enabled services
|
|
71
|
+
compute_config = self.enabled_services.get("compute", {"enabled": True, "services": {}})
|
|
72
|
+
self.compute = (
|
|
73
|
+
ComputeCollector(self.session, self.region, account_id, tags, compute_config.get("services", {}))
|
|
74
|
+
if compute_config.get("enabled", True)
|
|
75
|
+
else None
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
storage_config = self.enabled_services.get("storage", {"enabled": True, "services": {}})
|
|
79
|
+
self.storage = (
|
|
80
|
+
StorageCollector(self.session, self.region, account_id, tags, storage_config.get("services", {}))
|
|
81
|
+
if storage_config.get("enabled", True)
|
|
82
|
+
else None
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
database_config = self.enabled_services.get("database", {"enabled": True, "services": {}})
|
|
86
|
+
self.database = (
|
|
87
|
+
DatabaseCollector(self.session, self.region, account_id, tags, database_config.get("services", {}))
|
|
88
|
+
if database_config.get("enabled", True)
|
|
89
|
+
else None
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
networking_config = self.enabled_services.get("networking", {"enabled": True, "services": {}})
|
|
93
|
+
self.networking = (
|
|
94
|
+
NetworkingCollector(self.session, self.region, account_id, tags, networking_config.get("services", {}))
|
|
95
|
+
if networking_config.get("enabled", True)
|
|
96
|
+
else None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
security_config = self.enabled_services.get("security", {"enabled": True, "services": {}})
|
|
100
|
+
self.security = (
|
|
101
|
+
SecurityCollector(
|
|
102
|
+
self.session, self.region, account_id, tags, security_config.get("services", {}), collect_findings
|
|
103
|
+
)
|
|
104
|
+
if security_config.get("enabled", True)
|
|
105
|
+
else None
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
integration_config = self.enabled_services.get("integration", {"enabled": True, "services": {}})
|
|
109
|
+
self.integration = (
|
|
110
|
+
IntegrationCollector(self.session, self.region, account_id, tags, integration_config.get("services", {}))
|
|
111
|
+
if integration_config.get("enabled", True)
|
|
112
|
+
else None
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
containers_config = self.enabled_services.get("containers", {"enabled": True, "services": {}})
|
|
116
|
+
self.containers = (
|
|
117
|
+
ContainerCollector(self.session, self.region, account_id, tags, containers_config.get("services", {}))
|
|
118
|
+
if containers_config.get("enabled", True)
|
|
119
|
+
else None
|
|
42
120
|
)
|
|
43
121
|
|
|
44
|
-
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
122
|
+
config_config = self.enabled_services.get("config", {"enabled": True, "services": {}})
|
|
123
|
+
self.config = (
|
|
124
|
+
ConfigCollector(self.session, self.region, account_id, tags) if config_config.get("enabled", True) else None
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _get_enabled_services(self, enabled_services: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
128
|
+
"""
|
|
129
|
+
Get enabled services configuration with support for nested structure.
|
|
130
|
+
Supports both simple (category: bool) and nested (category: {enabled: bool, services: {...}}) formats.
|
|
131
|
+
|
|
132
|
+
:param dict enabled_services: Optional dictionary of service names to boolean flags or nested config
|
|
133
|
+
:return: Dictionary of service configurations
|
|
134
|
+
:rtype: Dict[str, Any]
|
|
135
|
+
"""
|
|
136
|
+
# Default all services to enabled with all sub-services enabled
|
|
137
|
+
default_services = {
|
|
138
|
+
"compute": {
|
|
139
|
+
"enabled": True,
|
|
140
|
+
"services": {"ec2": True, "lambda": True, "ecs": True, "systems_manager": True},
|
|
141
|
+
},
|
|
142
|
+
"storage": {"enabled": True, "services": {"s3": True, "ebs": True}},
|
|
143
|
+
"database": {"enabled": True, "services": {"rds": True, "dynamodb": True}},
|
|
144
|
+
"networking": {
|
|
145
|
+
"enabled": True,
|
|
146
|
+
"services": {
|
|
147
|
+
"vpc": True,
|
|
148
|
+
"elastic_ips": True,
|
|
149
|
+
"load_balancers": True,
|
|
150
|
+
"cloudfront": True,
|
|
151
|
+
"route53": True,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
"security": {
|
|
155
|
+
"enabled": True,
|
|
156
|
+
"services": {
|
|
157
|
+
"iam": True,
|
|
158
|
+
"kms": True,
|
|
159
|
+
"secrets_manager": True,
|
|
160
|
+
"waf": True,
|
|
161
|
+
"acm": True,
|
|
162
|
+
"cloudtrail": True,
|
|
163
|
+
"config": True,
|
|
164
|
+
"guardduty": True,
|
|
165
|
+
"securityhub": True,
|
|
166
|
+
"inspector": True,
|
|
167
|
+
"audit_manager": True,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
"integration": {
|
|
171
|
+
"enabled": True,
|
|
172
|
+
"services": {"api_gateway": True, "sns": True, "sqs": True, "eventbridge": True},
|
|
173
|
+
},
|
|
174
|
+
"containers": {"enabled": True, "services": {"ecr": True}},
|
|
175
|
+
"config": {"enabled": True, "services": {}},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# If no config provided, return defaults
|
|
179
|
+
if enabled_services is None:
|
|
180
|
+
return default_services
|
|
181
|
+
|
|
182
|
+
# Process configuration - support both simple and nested formats
|
|
183
|
+
merged_config = {}
|
|
184
|
+
for category, default_value in default_services.items():
|
|
185
|
+
if category not in enabled_services:
|
|
186
|
+
# Category not specified, use default
|
|
187
|
+
merged_config[category] = default_value
|
|
188
|
+
elif isinstance(enabled_services[category], bool):
|
|
189
|
+
# Simple format: security: true/false
|
|
190
|
+
# Convert to nested format with all services matching the category setting
|
|
191
|
+
merged_config[category] = {
|
|
192
|
+
"enabled": enabled_services[category],
|
|
193
|
+
"services": {
|
|
194
|
+
service: enabled_services[category] for service in default_value.get("services", {}).keys()
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
elif isinstance(enabled_services[category], dict):
|
|
198
|
+
# Nested format: security: {enabled: true, services: {...}}
|
|
199
|
+
category_config = enabled_services[category]
|
|
200
|
+
enabled_flag = category_config.get("enabled", True)
|
|
201
|
+
provided_services = category_config.get("services", {})
|
|
202
|
+
|
|
203
|
+
# Merge provided services with defaults
|
|
204
|
+
merged_services = default_value.get("services", {}).copy()
|
|
205
|
+
merged_services.update(provided_services)
|
|
206
|
+
|
|
207
|
+
merged_config[category] = {"enabled": enabled_flag, "services": merged_services}
|
|
208
|
+
else:
|
|
209
|
+
# Invalid format, use default
|
|
210
|
+
logger.warning(f"Invalid configuration format for category '{category}', using defaults")
|
|
211
|
+
merged_config[category] = default_value
|
|
212
|
+
|
|
213
|
+
# Log disabled categories and services
|
|
214
|
+
disabled_categories = [name for name, config in merged_config.items() if not config.get("enabled", True)]
|
|
215
|
+
if disabled_categories:
|
|
216
|
+
logger.info(f"AWS inventory collection disabled for categories: {', '.join(disabled_categories)}")
|
|
217
|
+
|
|
218
|
+
for category, config in merged_config.items():
|
|
219
|
+
if config.get("enabled", True):
|
|
220
|
+
disabled_services = [service for service, enabled in config.get("services", {}).items() if not enabled]
|
|
221
|
+
if disabled_services:
|
|
222
|
+
logger.info(
|
|
223
|
+
f"AWS inventory collection disabled for {category} services: {', '.join(disabled_services)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return merged_config
|
|
52
227
|
|
|
53
228
|
def collect_all(self) -> Dict[str, Any]:
|
|
54
229
|
"""
|
|
55
|
-
Collect all AWS resources.
|
|
230
|
+
Collect all AWS resources from enabled collectors.
|
|
56
231
|
|
|
57
232
|
:return: Dictionary containing all AWS resource information
|
|
58
233
|
:rtype: Dict[str, Any]
|
|
@@ -66,9 +241,15 @@ class AWSInventoryCollector:
|
|
|
66
241
|
self.security,
|
|
67
242
|
self.integration,
|
|
68
243
|
self.containers,
|
|
244
|
+
self.config,
|
|
69
245
|
]
|
|
70
246
|
|
|
71
|
-
|
|
247
|
+
# Filter out None collectors (disabled services)
|
|
248
|
+
active_collectors = [c for c in collectors if c is not None]
|
|
249
|
+
|
|
250
|
+
logger.info(f"Collecting AWS inventory from {len(active_collectors)} enabled service(s)")
|
|
251
|
+
|
|
252
|
+
for collector in active_collectors:
|
|
72
253
|
try:
|
|
73
254
|
resources = collector.collect()
|
|
74
255
|
inventory.update(resources)
|
|
@@ -83,21 +264,41 @@ class AWSInventoryCollector:
|
|
|
83
264
|
|
|
84
265
|
def collect_all_inventory(
|
|
85
266
|
region: str = os.getenv("AWS_REGION", "us-east-1"),
|
|
267
|
+
profile: Optional[str] = None,
|
|
86
268
|
aws_access_key_id: Optional[str] = None,
|
|
87
269
|
aws_secret_access_key: Optional[str] = None,
|
|
88
270
|
aws_session_token: Optional[str] = None,
|
|
271
|
+
account_id: Optional[str] = None,
|
|
272
|
+
tags: Optional[Dict[str, str]] = None,
|
|
273
|
+
enabled_services: Optional[Dict[str, bool]] = None,
|
|
274
|
+
collect_findings: bool = True,
|
|
89
275
|
) -> Dict[str, Any]:
|
|
90
276
|
"""
|
|
91
277
|
Collect inventory of all AWS resources.
|
|
92
278
|
|
|
93
279
|
:param str region: AWS region to collect inventory from
|
|
94
|
-
:param str
|
|
95
|
-
:param str
|
|
96
|
-
:param str
|
|
280
|
+
:param str profile: Optional AWS profile name from ~/.aws/credentials
|
|
281
|
+
:param str aws_access_key_id: Optional AWS access key ID (overrides profile)
|
|
282
|
+
:param str aws_secret_access_key: Optional AWS secret access key (overrides profile)
|
|
283
|
+
:param str aws_session_token: Optional AWS session token (overrides profile)
|
|
284
|
+
:param str account_id: Optional AWS account ID to filter resources
|
|
285
|
+
:param dict tags: Optional dictionary of tag key-value pairs to filter resources
|
|
286
|
+
:param dict enabled_services: Optional dictionary of service names to boolean flags for enabling/disabling collection
|
|
287
|
+
:param bool collect_findings: Whether to collect security findings (GuardDuty, Security Hub, Inspector). Default True.
|
|
97
288
|
:return: Dictionary containing all AWS resource information
|
|
98
289
|
:rtype: Dict[str, Any]
|
|
99
290
|
"""
|
|
100
|
-
collector = AWSInventoryCollector(
|
|
291
|
+
collector = AWSInventoryCollector(
|
|
292
|
+
region,
|
|
293
|
+
profile,
|
|
294
|
+
aws_access_key_id,
|
|
295
|
+
aws_secret_access_key,
|
|
296
|
+
aws_session_token,
|
|
297
|
+
account_id,
|
|
298
|
+
tags,
|
|
299
|
+
enabled_services,
|
|
300
|
+
collect_findings,
|
|
301
|
+
)
|
|
101
302
|
return collector.collect_all()
|
|
102
303
|
|
|
103
304
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Base classes for AWS resource collection."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Any, Dict, TYPE_CHECKING
|
|
4
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from botocore.exceptions import ClientError
|
|
7
7
|
|
|
@@ -12,17 +12,27 @@ logger = logging.getLogger("regscale")
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class BaseCollector:
|
|
15
|
-
"""Base class for AWS resource collectors."""
|
|
15
|
+
"""Base class for AWS resource collectors with universal filtering support."""
|
|
16
16
|
|
|
17
|
-
def __init__(
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
session: "boto3.Session",
|
|
20
|
+
region: str,
|
|
21
|
+
account_id: Optional[str] = None,
|
|
22
|
+
tags: Optional[Dict[str, str]] = None,
|
|
23
|
+
):
|
|
18
24
|
"""
|
|
19
|
-
Initialize the base collector.
|
|
25
|
+
Initialize the base collector with filtering support.
|
|
20
26
|
|
|
21
27
|
:param boto3.Session session: AWS session to use for API calls
|
|
22
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 dictionary of tag key-value pairs to filter resources (AND logic)
|
|
23
31
|
"""
|
|
24
32
|
self.session = session
|
|
25
33
|
self.region = region
|
|
34
|
+
self.account_id = account_id
|
|
35
|
+
self.tags = tags or {}
|
|
26
36
|
|
|
27
37
|
def _get_client(self, service_name: str) -> Any:
|
|
28
38
|
"""
|
|
@@ -32,7 +42,99 @@ class BaseCollector:
|
|
|
32
42
|
:return: Boto3 client for the service
|
|
33
43
|
:rtype: Any
|
|
34
44
|
"""
|
|
35
|
-
return self.session.client(service_name)
|
|
45
|
+
return self.session.client(service_name, region_name=self.region)
|
|
46
|
+
|
|
47
|
+
def _matches_account(self, resource_arn: str) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Check if resource belongs to target account.
|
|
50
|
+
|
|
51
|
+
:param str resource_arn: AWS Resource ARN
|
|
52
|
+
:return: True if resource matches account filter or no filter specified
|
|
53
|
+
:rtype: bool
|
|
54
|
+
|
|
55
|
+
ARN format: arn:partition:service:region:account-id:resource
|
|
56
|
+
"""
|
|
57
|
+
if not self.account_id:
|
|
58
|
+
return True # No filter, include all
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# ARN format: arn:aws:service:region:account-id:resource-id
|
|
62
|
+
arn_parts = resource_arn.split(":")
|
|
63
|
+
if len(arn_parts) >= 5:
|
|
64
|
+
arn_account = arn_parts[4]
|
|
65
|
+
match = arn_account == self.account_id
|
|
66
|
+
if not match:
|
|
67
|
+
logger.debug(f"Filtering out resource {resource_arn} - account {arn_account} != {self.account_id}")
|
|
68
|
+
return match
|
|
69
|
+
except (IndexError, AttributeError) as e:
|
|
70
|
+
logger.debug(f"Could not parse account from ARN {resource_arn}: {e}")
|
|
71
|
+
return True # Can't parse, include by default
|
|
72
|
+
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def _matches_tags(self, resource_tags: Any) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Check if all filter tags match resource tags (AND logic).
|
|
78
|
+
|
|
79
|
+
Supports multiple AWS tag formats:
|
|
80
|
+
- Dict: {'Key': 'Value', ...}
|
|
81
|
+
- List of dicts: [{'Key': 'k', 'Value': 'v'}, ...]
|
|
82
|
+
- List of Tags: [{'key': 'k', 'value': 'v'}, ...] (lowercase)
|
|
83
|
+
|
|
84
|
+
:param resource_tags: Resource tags in any AWS format
|
|
85
|
+
:return: True if all filter tags match (or no filter), False otherwise
|
|
86
|
+
:rtype: bool
|
|
87
|
+
"""
|
|
88
|
+
if not self.tags:
|
|
89
|
+
return True # No filter, include all
|
|
90
|
+
|
|
91
|
+
# Normalize tags to dict format
|
|
92
|
+
normalized_tags = self._normalize_tags(resource_tags)
|
|
93
|
+
|
|
94
|
+
# All filter tags must match (AND logic)
|
|
95
|
+
for key, value in self.tags.items():
|
|
96
|
+
if normalized_tags.get(key) != value:
|
|
97
|
+
logger.debug(
|
|
98
|
+
f"Resource does not match tag filter: expected {key}={value}, got {normalized_tags.get(key)}"
|
|
99
|
+
)
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def _normalize_tags(self, tags: Any) -> Dict[str, str]:
|
|
105
|
+
"""
|
|
106
|
+
Normalize various AWS tag formats to simple dict.
|
|
107
|
+
|
|
108
|
+
Handles:
|
|
109
|
+
- Dict format: {'Key': 'Value'}
|
|
110
|
+
- List format: [{'Key': 'k', 'Value': 'v'}] (uppercase)
|
|
111
|
+
- List format: [{'key': 'k', 'value': 'v'}] (lowercase)
|
|
112
|
+
- None or empty
|
|
113
|
+
|
|
114
|
+
:param tags: Tags in any AWS format
|
|
115
|
+
:return: Normalized dict of tags
|
|
116
|
+
:rtype: Dict[str, str]
|
|
117
|
+
"""
|
|
118
|
+
if not tags:
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
if isinstance(tags, dict):
|
|
122
|
+
return tags
|
|
123
|
+
|
|
124
|
+
if isinstance(tags, list):
|
|
125
|
+
result = {}
|
|
126
|
+
for tag in tags:
|
|
127
|
+
if isinstance(tag, dict):
|
|
128
|
+
# Handle uppercase format (most common)
|
|
129
|
+
if "Key" in tag and "Value" in tag:
|
|
130
|
+
result[tag["Key"]] = tag["Value"]
|
|
131
|
+
# Handle lowercase format
|
|
132
|
+
elif "key" in tag and "value" in tag:
|
|
133
|
+
result[tag["key"]] = tag["value"]
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
logger.warning(f"Unexpected tag format: {type(tags)}")
|
|
137
|
+
return {}
|
|
36
138
|
|
|
37
139
|
def _handle_error(self, error: Exception, resource_type: str) -> None:
|
|
38
140
|
"""
|