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
@@ -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 aws_access_key_id: Optional AWS access key ID
31
- :param str aws_secret_access_key: Optional AWS secret access key
32
- :param str aws_session_token: Optional AWS session ID
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.session = boto3.Session(
38
- aws_access_key_id=aws_access_key_id,
39
- aws_secret_access_key=aws_secret_access_key,
40
- region_name=region,
41
- aws_session_token=aws_session_token,
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
- # Initialize collectors
45
- self.compute = ComputeCollector(self.session, self.region)
46
- self.storage = StorageCollector(self.session, self.region)
47
- self.database = DatabaseCollector(self.session, self.region)
48
- self.networking = NetworkingCollector(self.session, self.region)
49
- self.security = SecurityCollector(self.session, self.region)
50
- self.integration = IntegrationCollector(self.session, self.region)
51
- self.containers = ContainerCollector(self.session, self.region)
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
- for collector in collectors:
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 aws_access_key_id: Optional AWS access key ID
95
- :param str aws_secret_access_key: Optional AWS secret access key
96
- :param str aws_session_token: Optional AWS session ID
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(region, aws_access_key_id, aws_secret_access_key, aws_session_token)
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__(self, session: "boto3.Session", region: str):
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
  """