regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- 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/commercial/synqly/ticketing.py +27 -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 +65 -5
- 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.1.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
- 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.1.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
"""AWS container resource collectors."""
|
|
2
2
|
|
|
3
|
-
from typing import Dict, List, Any
|
|
3
|
+
from typing import Dict, List, Any, Optional
|
|
4
4
|
|
|
5
5
|
from ..base import BaseCollector
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ContainerCollector(BaseCollector):
|
|
9
|
-
"""Collector for AWS container resources."""
|
|
9
|
+
"""Collector for AWS container resources with filtering support."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
session: Any,
|
|
14
|
+
region: str,
|
|
15
|
+
account_id: Optional[str] = None,
|
|
16
|
+
tags: Optional[Dict[str, str]] = None,
|
|
17
|
+
enabled_services: Optional[Dict[str, bool]] = None,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Initialize container collector with filtering support.
|
|
21
|
+
|
|
22
|
+
:param session: AWS session to use for API calls
|
|
23
|
+
:param str region: AWS region to collect from
|
|
24
|
+
:param str account_id: Optional AWS account ID to filter resources
|
|
25
|
+
:param dict tags: Optional tag filters (AND logic)
|
|
26
|
+
:param dict enabled_services: Optional dict of service names to boolean flags for enabling/disabling collection
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(session, region, account_id, tags)
|
|
29
|
+
self.enabled_services = enabled_services or {}
|
|
10
30
|
|
|
11
31
|
@staticmethod
|
|
12
32
|
def _get_repository_policy(ecr, repository_name: str) -> Dict[str, Any]:
|
|
@@ -78,9 +98,45 @@ class ContainerCollector(BaseCollector):
|
|
|
78
98
|
"Images": images,
|
|
79
99
|
}
|
|
80
100
|
|
|
101
|
+
def _should_include_repository(self, ecr, repo_arn: str) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if repository should be included based on account and tag filters.
|
|
104
|
+
|
|
105
|
+
:param ecr: ECR client
|
|
106
|
+
:param str repo_arn: Repository ARN
|
|
107
|
+
:return: True if repository should be included, False otherwise
|
|
108
|
+
:rtype: bool
|
|
109
|
+
"""
|
|
110
|
+
if not self._matches_account(repo_arn):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
if self.tags:
|
|
114
|
+
try:
|
|
115
|
+
tags_response = ecr.list_tags_for_resource(resourceArn=repo_arn)
|
|
116
|
+
repo_tags = tags_response.get("tags", [])
|
|
117
|
+
return self._matches_tags(repo_tags)
|
|
118
|
+
except Exception:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def _process_single_repository(self, ecr, repo: Dict[str, Any]) -> Dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Process a single repository, retrieving policy, images, and building data.
|
|
126
|
+
|
|
127
|
+
:param ecr: ECR client
|
|
128
|
+
:param repo: Repository data
|
|
129
|
+
:return: Processed repository data
|
|
130
|
+
:rtype: Dict[str, Any]
|
|
131
|
+
"""
|
|
132
|
+
repo_name = repo["repositoryName"]
|
|
133
|
+
policy = self._get_repository_policy(ecr, repo_name)
|
|
134
|
+
images = self._get_repository_images(ecr, repo_name)
|
|
135
|
+
return self._build_repository_data(repo, policy, images)
|
|
136
|
+
|
|
81
137
|
def get_ecr_repositories(self) -> List[Dict[str, Any]]:
|
|
82
138
|
"""
|
|
83
|
-
Get information about ECR repositories.
|
|
139
|
+
Get information about ECR repositories with filtering.
|
|
84
140
|
|
|
85
141
|
:return: List of ECR repository information
|
|
86
142
|
:rtype: List[Dict[str, Any]]
|
|
@@ -93,9 +149,12 @@ class ContainerCollector(BaseCollector):
|
|
|
93
149
|
for page in paginator.paginate():
|
|
94
150
|
for repo in page.get("repositories", []):
|
|
95
151
|
try:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
repo_arn = repo.get("repositoryArn", "")
|
|
153
|
+
|
|
154
|
+
if not self._should_include_repository(ecr, repo_arn):
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
repo_data = self._process_single_repository(ecr, repo)
|
|
99
158
|
repositories.append(repo_data)
|
|
100
159
|
except Exception as e:
|
|
101
160
|
self._handle_error(e, f"ECR repository {repo['repositoryName']}")
|
|
@@ -105,9 +164,15 @@ class ContainerCollector(BaseCollector):
|
|
|
105
164
|
|
|
106
165
|
def collect(self) -> Dict[str, Any]:
|
|
107
166
|
"""
|
|
108
|
-
Collect
|
|
167
|
+
Collect container resources based on enabled_services configuration.
|
|
109
168
|
|
|
110
|
-
:return: Dictionary containing
|
|
169
|
+
:return: Dictionary containing enabled container resource information
|
|
111
170
|
:rtype: Dict[str, Any]
|
|
112
171
|
"""
|
|
113
|
-
|
|
172
|
+
result = {}
|
|
173
|
+
|
|
174
|
+
# ECR Repositories
|
|
175
|
+
if self.enabled_services.get("ecr", True):
|
|
176
|
+
result["ECRRepositories"] = self.get_ecr_repositories()
|
|
177
|
+
|
|
178
|
+
return result
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
"""AWS database resource collectors."""
|
|
2
2
|
|
|
3
|
-
from typing import Dict, List, Any
|
|
3
|
+
from typing import Dict, List, Any, Optional
|
|
4
4
|
|
|
5
5
|
from ..base import BaseCollector
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class DatabaseCollector(BaseCollector):
|
|
9
|
-
"""Collector for AWS database resources."""
|
|
9
|
+
"""Collector for AWS database resources with filtering support."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
session: Any,
|
|
14
|
+
region: str,
|
|
15
|
+
account_id: Optional[str] = None,
|
|
16
|
+
tags: Optional[Dict[str, str]] = None,
|
|
17
|
+
enabled_services: Optional[Dict[str, bool]] = None,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Initialize database collector with filtering support.
|
|
21
|
+
|
|
22
|
+
:param session: AWS session to use for API calls
|
|
23
|
+
:param str region: AWS region to collect from
|
|
24
|
+
:param str account_id: Optional AWS account ID to filter resources
|
|
25
|
+
:param dict tags: Optional tag filters (AND logic)
|
|
26
|
+
:param dict enabled_services: Optional dict of service names to boolean flags for enabling/disabling collection
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(session, region, account_id, tags)
|
|
29
|
+
self.enabled_services = enabled_services or {}
|
|
10
30
|
|
|
11
31
|
def get_rds_instances(self) -> List[Dict[str, Any]]:
|
|
12
32
|
"""
|
|
13
|
-
Get information about RDS instances.
|
|
33
|
+
Get information about RDS instances with filtering.
|
|
14
34
|
|
|
15
35
|
:return: List of RDS instance information
|
|
16
36
|
:rtype: List[Dict[str, Any]]
|
|
@@ -22,10 +42,20 @@ class DatabaseCollector(BaseCollector):
|
|
|
22
42
|
|
|
23
43
|
for page in paginator.paginate():
|
|
24
44
|
for instance in page.get("DBInstances", []):
|
|
45
|
+
# Apply tag filtering
|
|
46
|
+
if self.tags and not self._matches_tags(instance.get("TagList", [])):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# Apply account filtering using DBInstanceArn
|
|
50
|
+
instance_arn = instance.get("DBInstanceArn", "")
|
|
51
|
+
if not self._matches_account(instance_arn):
|
|
52
|
+
continue
|
|
53
|
+
|
|
25
54
|
instances.append(
|
|
26
55
|
{
|
|
27
56
|
"Region": self.region,
|
|
28
57
|
"DBInstanceIdentifier": instance.get("DBInstanceIdentifier"),
|
|
58
|
+
"DBInstanceArn": instance.get("DBInstanceArn"),
|
|
29
59
|
"DBInstanceClass": instance.get("DBInstanceClass"),
|
|
30
60
|
"Engine": instance.get("Engine"),
|
|
31
61
|
"EngineVersion": instance.get("EngineVersion"),
|
|
@@ -34,6 +64,7 @@ class DatabaseCollector(BaseCollector):
|
|
|
34
64
|
"AllocatedStorage": instance.get("AllocatedStorage"),
|
|
35
65
|
"InstanceCreateTime": str(instance.get("InstanceCreateTime")),
|
|
36
66
|
"VpcId": instance.get("DBSubnetGroup", {}).get("VpcId"),
|
|
67
|
+
"AvailabilityZone": instance.get("AvailabilityZone"),
|
|
37
68
|
"MultiAZ": instance.get("MultiAZ"),
|
|
38
69
|
"PubliclyAccessible": instance.get("PubliclyAccessible"),
|
|
39
70
|
"StorageEncrypted": instance.get("StorageEncrypted"),
|
|
@@ -45,9 +76,60 @@ class DatabaseCollector(BaseCollector):
|
|
|
45
76
|
self._handle_error(e, "RDS instances")
|
|
46
77
|
return instances
|
|
47
78
|
|
|
79
|
+
def _should_include_table(self, dynamodb, table_arn: str, table_name: str) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Check if table should be included based on account and tag filters.
|
|
82
|
+
|
|
83
|
+
:param dynamodb: DynamoDB client
|
|
84
|
+
:param str table_arn: Table ARN
|
|
85
|
+
:param str table_name: Table name
|
|
86
|
+
:return: True if table should be included, False otherwise
|
|
87
|
+
:rtype: bool
|
|
88
|
+
"""
|
|
89
|
+
if not self._matches_account(table_arn):
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
if self.tags:
|
|
93
|
+
try:
|
|
94
|
+
tags_response = dynamodb.list_tags_of_resource(ResourceArn=table_arn)
|
|
95
|
+
table_tags = tags_response.get("Tags", [])
|
|
96
|
+
return self._matches_tags(table_tags)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
self._handle_error(e, f"DynamoDB table tags for {table_name}")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
def _build_table_data(self, table: Dict[str, Any]) -> Dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Build table data dictionary.
|
|
106
|
+
|
|
107
|
+
:param table: Raw table data
|
|
108
|
+
:return: Processed table data
|
|
109
|
+
:rtype: Dict[str, Any]
|
|
110
|
+
"""
|
|
111
|
+
return {
|
|
112
|
+
"Region": self.region,
|
|
113
|
+
"TableName": table.get("TableName"),
|
|
114
|
+
"TableStatus": table.get("TableStatus"),
|
|
115
|
+
"CreationDateTime": str(table.get("CreationDateTime")),
|
|
116
|
+
"TableSizeBytes": table.get("TableSizeBytes"),
|
|
117
|
+
"ItemCount": table.get("ItemCount"),
|
|
118
|
+
"TableArn": table.get("TableArn"),
|
|
119
|
+
"ProvisionedThroughput": {
|
|
120
|
+
"ReadCapacityUnits": table.get("ProvisionedThroughput", {}).get("ReadCapacityUnits"),
|
|
121
|
+
"WriteCapacityUnits": table.get("ProvisionedThroughput", {}).get("WriteCapacityUnits"),
|
|
122
|
+
},
|
|
123
|
+
"BillingModeSummary": table.get("BillingModeSummary", {}),
|
|
124
|
+
"GlobalSecondaryIndexes": table.get("GlobalSecondaryIndexes", []),
|
|
125
|
+
"LocalSecondaryIndexes": table.get("LocalSecondaryIndexes", []),
|
|
126
|
+
"StreamSpecification": table.get("StreamSpecification", {}),
|
|
127
|
+
"SSEDescription": table.get("SSEDescription", {}),
|
|
128
|
+
}
|
|
129
|
+
|
|
48
130
|
def get_dynamodb_tables(self) -> List[Dict[str, Any]]:
|
|
49
131
|
"""
|
|
50
|
-
Get information about DynamoDB tables.
|
|
132
|
+
Get information about DynamoDB tables with filtering.
|
|
51
133
|
|
|
52
134
|
:return: List of DynamoDB table information
|
|
53
135
|
:rtype: List[Dict[str, Any]]
|
|
@@ -61,30 +143,13 @@ class DatabaseCollector(BaseCollector):
|
|
|
61
143
|
for table_name in page.get("TableNames", []):
|
|
62
144
|
try:
|
|
63
145
|
table = dynamodb.describe_table(TableName=table_name)["Table"]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"ItemCount": table.get("ItemCount"),
|
|
72
|
-
"TableArn": table.get("TableArn"),
|
|
73
|
-
"ProvisionedThroughput": {
|
|
74
|
-
"ReadCapacityUnits": table.get("ProvisionedThroughput", {}).get(
|
|
75
|
-
"ReadCapacityUnits"
|
|
76
|
-
),
|
|
77
|
-
"WriteCapacityUnits": table.get("ProvisionedThroughput", {}).get(
|
|
78
|
-
"WriteCapacityUnits"
|
|
79
|
-
),
|
|
80
|
-
},
|
|
81
|
-
"BillingModeSummary": table.get("BillingModeSummary", {}),
|
|
82
|
-
"GlobalSecondaryIndexes": table.get("GlobalSecondaryIndexes", []),
|
|
83
|
-
"LocalSecondaryIndexes": table.get("LocalSecondaryIndexes", []),
|
|
84
|
-
"StreamSpecification": table.get("StreamSpecification", {}),
|
|
85
|
-
"SSEDescription": table.get("SSEDescription", {}),
|
|
86
|
-
}
|
|
87
|
-
)
|
|
146
|
+
table_arn = table.get("TableArn", "")
|
|
147
|
+
|
|
148
|
+
if not self._should_include_table(dynamodb, table_arn, table_name):
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
table_data = self._build_table_data(table)
|
|
152
|
+
tables.append(table_data)
|
|
88
153
|
except Exception as e:
|
|
89
154
|
self._handle_error(e, f"DynamoDB table {table_name}")
|
|
90
155
|
except Exception as e:
|
|
@@ -93,9 +158,19 @@ class DatabaseCollector(BaseCollector):
|
|
|
93
158
|
|
|
94
159
|
def collect(self) -> Dict[str, Any]:
|
|
95
160
|
"""
|
|
96
|
-
Collect
|
|
161
|
+
Collect database resources based on enabled_services configuration.
|
|
97
162
|
|
|
98
|
-
:return: Dictionary containing
|
|
163
|
+
:return: Dictionary containing enabled database resource information
|
|
99
164
|
:rtype: Dict[str, Any]
|
|
100
165
|
"""
|
|
101
|
-
|
|
166
|
+
result = {}
|
|
167
|
+
|
|
168
|
+
# RDS Instances
|
|
169
|
+
if self.enabled_services.get("rds", True):
|
|
170
|
+
result["RDSInstances"] = self.get_rds_instances()
|
|
171
|
+
|
|
172
|
+
# DynamoDB Tables
|
|
173
|
+
if self.enabled_services.get("dynamodb", True):
|
|
174
|
+
result["DynamoDBTables"] = self.get_dynamodb_tables()
|
|
175
|
+
|
|
176
|
+
return result
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""AWS GuardDuty 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 GuardDutyCollector(BaseCollector):
|
|
14
|
+
"""Collector for AWS GuardDuty 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 GuardDuty 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 GuardDuty 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 GuardDuty resources.
|
|
41
|
+
|
|
42
|
+
:return: Dictionary containing GuardDuty detectors, findings, and members
|
|
43
|
+
:rtype: Dict[str, Any]
|
|
44
|
+
"""
|
|
45
|
+
result = {"Detectors": [], "Findings": [], "Members": []}
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
client = self._get_client("guardduty")
|
|
49
|
+
detector_ids = self._list_detectors(client)
|
|
50
|
+
|
|
51
|
+
for detector_id in detector_ids:
|
|
52
|
+
self._process_detector(client, detector_id, result)
|
|
53
|
+
|
|
54
|
+
if self.collect_findings:
|
|
55
|
+
logger.info(
|
|
56
|
+
f"Collected {len(result['Detectors'])} GuardDuty detector(s), "
|
|
57
|
+
f"{len(result['Findings'])} finding(s) from {self.region}"
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
logger.info(f"Collected {len(result['Detectors'])} GuardDuty detector(s) from {self.region}")
|
|
61
|
+
|
|
62
|
+
except ClientError as e:
|
|
63
|
+
self._handle_error(e, "GuardDuty resources")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Unexpected error collecting GuardDuty resources: {e}", exc_info=True)
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def _process_detector(self, client: Any, detector_id: str, result: Dict[str, Any]) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Process a single detector and add its details to result.
|
|
72
|
+
|
|
73
|
+
:param client: GuardDuty client
|
|
74
|
+
:param str detector_id: Detector ID to process
|
|
75
|
+
:param dict result: Result dictionary to populate
|
|
76
|
+
"""
|
|
77
|
+
detector_info = self._get_detector(client, detector_id)
|
|
78
|
+
if not detector_info:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
if not self._should_include_detector(detector_info, detector_id):
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
detector_info = self._enrich_detector_info(client, detector_id, detector_info)
|
|
85
|
+
result["Detectors"].append(detector_info)
|
|
86
|
+
|
|
87
|
+
if self.collect_findings:
|
|
88
|
+
findings = self._list_and_get_findings(client, detector_id)
|
|
89
|
+
result["Findings"].extend(findings)
|
|
90
|
+
else:
|
|
91
|
+
logger.debug(f"Skipping GuardDuty findings collection for detector {detector_id} (collect_findings=False)")
|
|
92
|
+
|
|
93
|
+
members = self._list_members(client, detector_id)
|
|
94
|
+
result["Members"].extend(members)
|
|
95
|
+
|
|
96
|
+
def _should_include_detector(self, detector_info: Dict[str, Any], detector_id: str) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
Check if detector should be included based on filters.
|
|
99
|
+
|
|
100
|
+
:param dict detector_info: Detector information
|
|
101
|
+
:param str detector_id: Detector ID
|
|
102
|
+
:return: True if detector should be included
|
|
103
|
+
:rtype: bool
|
|
104
|
+
"""
|
|
105
|
+
if self.account_id and not self._matches_account_id(detector_info.get("AccountId", "")):
|
|
106
|
+
logger.debug(f"Skipping detector {detector_id} - does not match account ID {self.account_id}")
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
if self.tags:
|
|
110
|
+
detector_tags = self._get_detector_tags(
|
|
111
|
+
self._get_client("guardduty"), detector_id, detector_info.get("AccountId", "")
|
|
112
|
+
)
|
|
113
|
+
if not self._matches_tags(detector_tags):
|
|
114
|
+
logger.debug(f"Skipping detector {detector_id} - does not match tag filters")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def _enrich_detector_info(self, client: Any, detector_id: str, detector_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Enrich detector info with additional metadata.
|
|
122
|
+
|
|
123
|
+
:param client: GuardDuty client
|
|
124
|
+
:param str detector_id: Detector ID
|
|
125
|
+
:param dict detector_info: Detector information to enrich
|
|
126
|
+
:return: Enriched detector information
|
|
127
|
+
:rtype: Dict[str, Any]
|
|
128
|
+
"""
|
|
129
|
+
if self.tags:
|
|
130
|
+
detector_tags = self._get_detector_tags(client, detector_id, detector_info.get("AccountId", ""))
|
|
131
|
+
detector_info["Tags"] = detector_tags
|
|
132
|
+
|
|
133
|
+
detector_info["DetectorId"] = detector_id
|
|
134
|
+
detector_info["Region"] = self.region
|
|
135
|
+
return detector_info
|
|
136
|
+
|
|
137
|
+
def _list_detectors(self, client: Any) -> List[str]:
|
|
138
|
+
"""
|
|
139
|
+
List GuardDuty detectors.
|
|
140
|
+
|
|
141
|
+
:param client: GuardDuty client
|
|
142
|
+
:return: List of detector IDs
|
|
143
|
+
:rtype: List[str]
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
response = client.list_detectors()
|
|
147
|
+
return response.get("DetectorIds", [])
|
|
148
|
+
except ClientError as e:
|
|
149
|
+
if e.response["Error"]["Code"] == "AccessDeniedException":
|
|
150
|
+
logger.warning(f"Access denied to list GuardDuty detectors in {self.region}")
|
|
151
|
+
return []
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
def _get_detector(self, client: Any, detector_id: str) -> Optional[Dict[str, Any]]:
|
|
155
|
+
"""
|
|
156
|
+
Get details about a specific GuardDuty detector.
|
|
157
|
+
|
|
158
|
+
:param client: GuardDuty client
|
|
159
|
+
:param str detector_id: Detector ID
|
|
160
|
+
:return: Detector details or None
|
|
161
|
+
:rtype: Optional[Dict[str, Any]]
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
response = client.get_detector(DetectorId=detector_id)
|
|
165
|
+
# Remove ResponseMetadata
|
|
166
|
+
response.pop("ResponseMetadata", None)
|
|
167
|
+
return response
|
|
168
|
+
except ClientError as e:
|
|
169
|
+
logger.error(f"Error getting detector {detector_id}: {e}")
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def _list_and_get_findings(self, client: Any, detector_id: str, max_findings: int = 50) -> List[Dict[str, Any]]:
|
|
173
|
+
"""
|
|
174
|
+
List and get detailed information about GuardDuty findings.
|
|
175
|
+
|
|
176
|
+
:param client: GuardDuty client
|
|
177
|
+
:param str detector_id: Detector ID
|
|
178
|
+
:param int max_findings: Maximum number of findings to retrieve
|
|
179
|
+
:return: List of findings with details
|
|
180
|
+
:rtype: List[Dict[str, Any]]
|
|
181
|
+
"""
|
|
182
|
+
findings = []
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# List finding IDs
|
|
186
|
+
list_response = client.list_findings(DetectorId=detector_id, MaxResults=max_findings)
|
|
187
|
+
finding_ids = list_response.get("FindingIds", [])
|
|
188
|
+
|
|
189
|
+
if not finding_ids:
|
|
190
|
+
return findings
|
|
191
|
+
|
|
192
|
+
# Get detailed information for findings
|
|
193
|
+
get_response = client.get_findings(DetectorId=detector_id, FindingIds=finding_ids)
|
|
194
|
+
findings = get_response.get("Findings", [])
|
|
195
|
+
|
|
196
|
+
# Add region information
|
|
197
|
+
for finding in findings:
|
|
198
|
+
finding["Region"] = self.region
|
|
199
|
+
finding["DetectorId"] = detector_id
|
|
200
|
+
|
|
201
|
+
except ClientError as e:
|
|
202
|
+
if e.response["Error"]["Code"] == "AccessDeniedException":
|
|
203
|
+
logger.warning(f"Access denied to list findings for detector {detector_id}")
|
|
204
|
+
else:
|
|
205
|
+
logger.error(f"Error listing findings for detector {detector_id}: {e}")
|
|
206
|
+
|
|
207
|
+
return findings
|
|
208
|
+
|
|
209
|
+
def _list_members(self, client: Any, detector_id: str) -> List[Dict[str, Any]]:
|
|
210
|
+
"""
|
|
211
|
+
List member accounts for a GuardDuty detector.
|
|
212
|
+
|
|
213
|
+
:param client: GuardDuty client
|
|
214
|
+
:param str detector_id: Detector ID
|
|
215
|
+
:return: List of member accounts
|
|
216
|
+
:rtype: List[Dict[str, Any]]
|
|
217
|
+
"""
|
|
218
|
+
members = []
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
response = client.list_members(DetectorId=detector_id)
|
|
222
|
+
members = response.get("Members", [])
|
|
223
|
+
|
|
224
|
+
# Add region and detector information
|
|
225
|
+
for member in members:
|
|
226
|
+
member["Region"] = self.region
|
|
227
|
+
member["DetectorId"] = detector_id
|
|
228
|
+
|
|
229
|
+
except ClientError as e:
|
|
230
|
+
if e.response["Error"]["Code"] == "AccessDeniedException":
|
|
231
|
+
logger.debug(f"Access denied to list members for detector {detector_id}")
|
|
232
|
+
else:
|
|
233
|
+
logger.error(f"Error listing members for detector {detector_id}: {e}")
|
|
234
|
+
|
|
235
|
+
return members
|
|
236
|
+
|
|
237
|
+
def _matches_account_id(self, detector_account_id: str) -> bool:
|
|
238
|
+
"""
|
|
239
|
+
Check if detector account ID matches the specified account ID.
|
|
240
|
+
|
|
241
|
+
:param str detector_account_id: Account ID from detector
|
|
242
|
+
:return: True if matches or no account_id filter specified
|
|
243
|
+
:rtype: bool
|
|
244
|
+
"""
|
|
245
|
+
if not self.account_id:
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
return detector_account_id == self.account_id
|
|
249
|
+
|
|
250
|
+
def _get_detector_tags(self, client: Any, detector_id: str, account_id: str) -> Dict[str, str]:
|
|
251
|
+
"""
|
|
252
|
+
Get tags for a GuardDuty detector.
|
|
253
|
+
|
|
254
|
+
:param client: GuardDuty client
|
|
255
|
+
:param str detector_id: Detector ID
|
|
256
|
+
:param str account_id: AWS account ID
|
|
257
|
+
:return: Dictionary of tags (TagKey -> TagValue)
|
|
258
|
+
:rtype: Dict[str, str]
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
# Construct the detector ARN
|
|
262
|
+
detector_arn = f"arn:aws:guardduty:{self.region}:{account_id}:detector/{detector_id}"
|
|
263
|
+
response = client.list_tags_for_resource(ResourceArn=detector_arn)
|
|
264
|
+
tags = response.get("Tags", {})
|
|
265
|
+
return tags
|
|
266
|
+
except ClientError as e:
|
|
267
|
+
logger.debug(f"Error getting tags for detector {detector_id}: {e}")
|
|
268
|
+
return {}
|
|
269
|
+
|
|
270
|
+
def _matches_tags(self, resource_tags: Dict[str, str]) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
Check if resource tags match the specified filter tags.
|
|
273
|
+
|
|
274
|
+
:param dict resource_tags: Tags on the resource
|
|
275
|
+
:return: True if all filter tags match
|
|
276
|
+
:rtype: bool
|
|
277
|
+
"""
|
|
278
|
+
if not self.tags:
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
# All filter tags must match
|
|
282
|
+
for key, value in self.tags.items():
|
|
283
|
+
if resource_tags.get(key) != value:
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
return True
|