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.

Files changed (112) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/utils/app_utils.py +11 -2
  3. regscale/dev/cli.py +26 -0
  4. regscale/dev/version.py +72 -0
  5. regscale/integrations/commercial/__init__.py +15 -1
  6. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  7. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  8. regscale/integrations/commercial/amazon/common.py +48 -58
  9. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  10. regscale/integrations/commercial/aws/cli.py +3093 -55
  11. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  12. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  13. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  14. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  15. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  16. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  17. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  18. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  19. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  20. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  21. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  22. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  23. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  24. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  25. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  26. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  27. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  28. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  29. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  30. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  31. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  32. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  33. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  34. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  35. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  36. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  37. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  38. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  39. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  40. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  41. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  42. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  43. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  44. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  45. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  46. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  47. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  48. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  49. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  50. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  51. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  52. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  53. regscale/integrations/commercial/aws/scanner.py +851 -206
  54. regscale/integrations/commercial/aws/security_hub.py +319 -0
  55. regscale/integrations/commercial/aws/session_manager.py +282 -0
  56. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  57. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  58. regscale/integrations/compliance_integration.py +308 -38
  59. regscale/integrations/due_date_handler.py +3 -0
  60. regscale/integrations/scanner_integration.py +399 -84
  61. regscale/models/integration_models/cisa_kev_data.json +34 -4
  62. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  63. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  64. regscale/models/regscale_models/assessment.py +2 -1
  65. regscale/models/regscale_models/control_objective.py +74 -5
  66. regscale/models/regscale_models/file.py +2 -0
  67. regscale/models/regscale_models/issue.py +2 -5
  68. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
  70. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  71. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  73. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  86. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  87. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  89. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  90. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  91. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  92. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  94. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  96. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  98. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  99. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  100. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  101. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  103. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  104. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  105. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  106. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  108. tests/regscale/integrations/commercial/test_aws.py +55 -56
  109. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.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
- policy = self._get_repository_policy(ecr, repo["repositoryName"])
97
- images = self._get_repository_images(ecr, repo["repositoryName"])
98
- repo_data = self._build_repository_data(repo, policy, images)
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 all container resources.
167
+ Collect container resources based on enabled_services configuration.
109
168
 
110
- :return: Dictionary containing all container resource information
169
+ :return: Dictionary containing enabled container resource information
111
170
  :rtype: Dict[str, Any]
112
171
  """
113
- return {"ECRRepositories": self.get_ecr_repositories()}
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
- tables.append(
65
- {
66
- "Region": self.region,
67
- "TableName": table.get("TableName"),
68
- "TableStatus": table.get("TableStatus"),
69
- "CreationDateTime": str(table.get("CreationDateTime")),
70
- "TableSizeBytes": table.get("TableSizeBytes"),
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 all database resources.
161
+ Collect database resources based on enabled_services configuration.
97
162
 
98
- :return: Dictionary containing all database resource information
163
+ :return: Dictionary containing enabled database resource information
99
164
  :rtype: Dict[str, Any]
100
165
  """
101
- return {"RDSInstances": self.get_rds_instances(), "DynamoDBTables": self.get_dynamodb_tables()}
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