regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.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 (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,3731 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS integration in RegScale CLI."""
4
+
5
+ from unittest.mock import MagicMock, patch, mock_open
6
+
7
+ import pytest
8
+
9
+ from regscale.integrations.commercial.aws.scanner import AWSInventoryIntegration
10
+ from regscale.integrations.scanner_integration import IntegrationAsset
11
+ from regscale.models import regscale_models
12
+ from tests import CLITestFixture
13
+
14
+
15
+ class TestAws(CLITestFixture):
16
+ """Test suite for AWS integration in RegScale CLI."""
17
+
18
+ @staticmethod
19
+ def _build_ec2_instance_data(
20
+ instance_id: str = "i-1234567890abcdef0",
21
+ name: str = "Test Instance",
22
+ state: str = "running",
23
+ instance_type: str = "t3.micro",
24
+ **kwargs,
25
+ ) -> dict:
26
+ """Build test EC2 instance data with sensible defaults."""
27
+ base_data = {
28
+ "InstanceId": instance_id,
29
+ "InstanceType": instance_type,
30
+ "State": state,
31
+ "Region": "us-east-1",
32
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
33
+ "BlockDeviceMappings": [{"DeviceName": "/dev/xvda", "Ebs": {"VolumeId": "vol-12345678"}}],
34
+ "ImageInfo": {
35
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
36
+ "Description": "Amazon Linux 2 AMI",
37
+ "RootDeviceType": "ebs",
38
+ "VirtualizationType": "hvm",
39
+ },
40
+ "PlatformDetails": "Linux/UNIX",
41
+ "Architecture": "x86_64",
42
+ }
43
+
44
+ if name != "Test Instance":
45
+ base_data["Tags"] = [{"Key": "Name", "Value": name}]
46
+
47
+ base_data.update(kwargs)
48
+ return base_data
49
+
50
+ @pytest.fixture
51
+ def mock_aws_integration(self):
52
+ """Create a properly configured MagicMock for AWSInventoryIntegration."""
53
+ mock_self = MagicMock()
54
+ mock_self.collector = None
55
+ mock_self.authenticate = MagicMock()
56
+ mock_self.num_assets_to_process = 0
57
+ return mock_self
58
+
59
+ @patch("regscale.integrations.commercial.aws.scanner.json.load")
60
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.exists", return_value=True)
61
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.getmtime")
62
+ @patch("regscale.integrations.commercial.aws.scanner.time.time")
63
+ @patch("builtins.open", new_callable=mock_open)
64
+ def test_returns_cached_data_when_cache_is_valid(
65
+ self, mock_open, mock_time, mock_getmtime, mock_exists, mock_json_load, mock_aws_integration
66
+ ):
67
+ """Should return cached data when cache exists and is not expired."""
68
+ from regscale.integrations.commercial.aws.scanner import CACHE_TTL_SECONDS
69
+
70
+ cached_data = {"test": "cached_data"}
71
+ mock_json_load.return_value = cached_data
72
+ mock_getmtime.return_value = 0
73
+ mock_time.return_value = CACHE_TTL_SECONDS - 1
74
+
75
+ result = AWSInventoryIntegration.fetch_aws_data_if_needed(
76
+ mock_aws_integration,
77
+ region="us-east-1",
78
+ aws_access_key_id=None,
79
+ aws_secret_access_key=None,
80
+ aws_session_token=None,
81
+ )
82
+
83
+ assert result == cached_data
84
+ mock_open.assert_called_once()
85
+ mock_json_load.assert_called_once()
86
+
87
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.exists", return_value=False)
88
+ @patch("regscale.integrations.commercial.aws.scanner.os.makedirs")
89
+ @patch("regscale.integrations.commercial.aws.scanner.json.dump")
90
+ @patch("builtins.open", new_callable=mock_open)
91
+ @patch("regscale.integrations.commercial.aws.scanner.AWSInventoryCollector")
92
+ def test_fetches_fresh_data_when_cache_missing(
93
+ self, mock_collector_class, mock_open, mock_json_dump, mock_makedirs, mock_exists, mock_aws_integration
94
+ ):
95
+ """Should fetch fresh data when cache doesn't exist."""
96
+ fresh_data = {"fresh": "data"}
97
+ mock_collector = MagicMock()
98
+ mock_collector.collect_all.return_value = fresh_data
99
+ mock_collector_class.return_value = mock_collector
100
+
101
+ def mock_authenticate(*args, **kwargs):
102
+ mock_aws_integration.collector = mock_collector
103
+
104
+ mock_aws_integration.authenticate.side_effect = mock_authenticate
105
+
106
+ result = AWSInventoryIntegration.fetch_aws_data_if_needed(
107
+ mock_aws_integration,
108
+ region="us-east-1",
109
+ aws_access_key_id="test_key",
110
+ aws_secret_access_key="test_secret",
111
+ aws_session_token=None,
112
+ )
113
+
114
+ assert result == fresh_data
115
+ mock_aws_integration.authenticate.assert_called_once_with("test_key", "test_secret", "us-east-1", None)
116
+ mock_collector.collect_all.assert_called_once()
117
+ mock_makedirs.assert_called_once()
118
+ mock_json_dump.assert_called_once()
119
+
120
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.exists", return_value=True)
121
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.getmtime")
122
+ @patch("regscale.integrations.commercial.aws.scanner.time.time")
123
+ @patch("regscale.integrations.commercial.aws.scanner.json.load")
124
+ @patch("builtins.open", new_callable=mock_open)
125
+ def test_raises_error_when_cache_expired_and_authentication_fails(
126
+ self, mock_open, mock_json_load, mock_time, mock_getmtime, mock_exists, mock_aws_integration
127
+ ):
128
+ """Should raise RuntimeError when cache is expired and authentication fails."""
129
+ from regscale.integrations.commercial.aws.scanner import CACHE_TTL_SECONDS
130
+
131
+ mock_getmtime.return_value = 0
132
+ mock_time.return_value = CACHE_TTL_SECONDS + 1
133
+ mock_aws_integration.authenticate.return_value = None
134
+
135
+ with pytest.raises(RuntimeError, match="Failed to initialize AWS inventory collector"):
136
+ AWSInventoryIntegration.fetch_aws_data_if_needed(
137
+ mock_aws_integration,
138
+ region="us-east-1",
139
+ aws_access_key_id=None,
140
+ aws_secret_access_key=None,
141
+ aws_session_token=None,
142
+ )
143
+
144
+ def test_processes_normal_asset_list(self, mock_aws_integration):
145
+ """Should process a normal list of asset dictionaries."""
146
+ assets = [
147
+ {"id": "asset1", "name": "Test Asset 1"},
148
+ {"id": "asset2", "name": "Test Asset 2"},
149
+ ]
150
+ asset_type = "EC2 instance"
151
+
152
+ mock_parser = MagicMock()
153
+ mock_parser.side_effect = [
154
+ IntegrationAsset(name="Asset 1", identifier="asset1", asset_type="EC2", asset_category="Compute"),
155
+ IntegrationAsset(name="Asset 2", identifier="asset2", asset_type="EC2", asset_category="Compute"),
156
+ ]
157
+
158
+ results = list(
159
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
160
+ )
161
+
162
+ assert len(results) == 2
163
+ assert results[0].name == "Asset 1"
164
+ assert results[1].name == "Asset 2"
165
+ assert mock_aws_integration.num_assets_to_process == 2
166
+ assert mock_parser.call_count == 2
167
+
168
+ def test_processes_special_users_structure(self, mock_aws_integration):
169
+ """Should process special Users structure correctly."""
170
+ assets = {
171
+ "Users": [
172
+ {"id": "user1", "name": "User 1"},
173
+ {"id": "user2", "name": "User 2"},
174
+ ]
175
+ }
176
+ asset_type = "IAM Users"
177
+
178
+ mock_parser = MagicMock()
179
+ mock_parser.side_effect = [
180
+ IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity"),
181
+ IntegrationAsset(name="User 2", identifier="user2", asset_type="IAM", asset_category="Identity"),
182
+ ]
183
+
184
+ results = list(
185
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
186
+ )
187
+
188
+ assert len(results) == 2
189
+ assert results[0].name == "User 1"
190
+ assert results[1].name == "User 2"
191
+ assert mock_aws_integration.num_assets_to_process == 2
192
+ assert mock_parser.call_count == 2
193
+
194
+ def test_process_asset_collection_roles_special_case(self, mock_aws_integration):
195
+ """Test processing special 'Roles' case"""
196
+ assets = {
197
+ "Roles": [
198
+ {"id": "role1", "name": "Role 1"},
199
+ {"id": "role2", "name": "Role 2"},
200
+ ]
201
+ }
202
+ asset_type = "IAM Roles"
203
+
204
+ mock_parser = MagicMock()
205
+ mock_parser.side_effect = [
206
+ IntegrationAsset(name="Role 1", identifier="role1", asset_type="IAM", asset_category="Identity"),
207
+ IntegrationAsset(name="Role 2", identifier="role2", asset_type="IAM", asset_category="Identity"),
208
+ ]
209
+
210
+ results = list(
211
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
212
+ )
213
+
214
+ assert len(results) == 2
215
+ assert results[0].name == "Role 1"
216
+ assert results[1].name == "Role 2"
217
+ assert mock_aws_integration.num_assets_to_process == 2
218
+ assert mock_parser.call_count == 2
219
+
220
+ def test_skips_invalid_asset_format(self, mock_aws_integration):
221
+ """Should skip assets with invalid format and log warning."""
222
+ assets = ["invalid_asset", {"id": "valid_asset", "name": "Valid Asset"}]
223
+ asset_type = "EC2 instance"
224
+
225
+ mock_parser = MagicMock()
226
+ mock_parser.return_value = IntegrationAsset(
227
+ name="Valid Asset", identifier="valid_asset", asset_type="EC2", asset_category="Compute"
228
+ )
229
+
230
+ with patch("regscale.integrations.commercial.aws.scanner.logger"):
231
+ results = list(
232
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
233
+ )
234
+
235
+ assert len(results) == 1
236
+ assert results[0].name == "Valid Asset"
237
+ assert mock_aws_integration.num_assets_to_process == 1
238
+ assert mock_parser.call_count == 1
239
+
240
+ def test_process_asset_collection_parser_exception(self, mock_aws_integration):
241
+ """Test handling of parser method exceptions"""
242
+ assets = [
243
+ {"id": "asset1", "name": "Test Asset 1"},
244
+ {"id": "asset2", "name": "Test Asset 2"},
245
+ ]
246
+ asset_type = "EC2 instance"
247
+
248
+ mock_parser = MagicMock()
249
+ mock_parser.side_effect = [
250
+ IntegrationAsset(name="Asset 1", identifier="asset1", asset_type="EC2", asset_category="Compute"),
251
+ Exception("Parser error for asset 2"),
252
+ ]
253
+
254
+ with patch("regscale.integrations.commercial.aws.scanner.logger") as mock_logger:
255
+ results = list(
256
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
257
+ )
258
+
259
+ mock_logger.error.assert_called_once()
260
+ error_call_args = mock_logger.error.call_args
261
+ # The first argument is the message string
262
+ error_message = error_call_args[0][0]
263
+ assert "Error parsing EC2 instance" in error_message
264
+ assert "Parser error for asset 2" in error_message
265
+
266
+ assert len(results) == 1
267
+ assert results[0].name == "Asset 1"
268
+ assert mock_aws_integration.num_assets_to_process == 2 # Counter still increments
269
+ assert mock_parser.call_count == 2
270
+
271
+ def test_process_asset_collection_empty_list(self, mock_aws_integration):
272
+ """Test processing empty asset list"""
273
+ assets = []
274
+ asset_type = "EC2 instance"
275
+
276
+ mock_parser = MagicMock()
277
+
278
+ results = list(
279
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
280
+ )
281
+
282
+ assert len(results) == 0
283
+ assert mock_aws_integration.num_assets_to_process == 0
284
+ assert mock_parser.call_count == 0
285
+
286
+ def test_process_asset_collection_mixed_valid_invalid(self, mock_aws_integration):
287
+ """Test processing mixed valid and invalid assets"""
288
+ assets = [
289
+ {"id": "valid1", "name": "Valid 1"},
290
+ "invalid_string",
291
+ {"id": "valid2", "name": "Valid 2"},
292
+ None,
293
+ {"id": "valid3", "name": "Valid 3"},
294
+ ]
295
+ asset_type = "EC2 instance"
296
+
297
+ mock_parser = MagicMock()
298
+ mock_parser.side_effect = [
299
+ IntegrationAsset(name="Valid 1", identifier="valid1", asset_type="EC2", asset_category="Compute"),
300
+ IntegrationAsset(name="Valid 2", identifier="valid2", asset_type="EC2", asset_category="Compute"),
301
+ IntegrationAsset(name="Valid 3", identifier="valid3", asset_type="EC2", asset_category="Compute"),
302
+ ]
303
+
304
+ results = list(
305
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
306
+ )
307
+
308
+ assert len(results) == 3
309
+ assert results[0].name == "Valid 1"
310
+ assert results[1].name == "Valid 2"
311
+ assert results[2].name == "Valid 3"
312
+ assert mock_aws_integration.num_assets_to_process == 3
313
+ assert mock_parser.call_count == 3
314
+
315
+ def test_process_asset_collection_empty_users_roles(self, mock_aws_integration):
316
+ """Test processing empty Users/Roles collections"""
317
+ assets_users = {"Users": []}
318
+ asset_type = "IAM Users"
319
+
320
+ mock_parser = MagicMock()
321
+
322
+ results = list(
323
+ AWSInventoryIntegration._process_asset_collection(
324
+ mock_aws_integration, assets_users, asset_type, mock_parser
325
+ )
326
+ )
327
+
328
+ assert len(results) == 0
329
+ assert mock_aws_integration.num_assets_to_process == 0
330
+ assert mock_parser.call_count == 0
331
+
332
+ mock_aws_integration.num_assets_to_process = 0
333
+
334
+ assets_roles = {"Roles": []}
335
+ asset_type = "IAM Roles"
336
+
337
+ results = list(
338
+ AWSInventoryIntegration._process_asset_collection(
339
+ mock_aws_integration, assets_roles, asset_type, mock_parser
340
+ )
341
+ )
342
+
343
+ assert len(results) == 0
344
+ assert mock_aws_integration.num_assets_to_process == 0
345
+ assert mock_parser.call_count == 0
346
+
347
+ def test_process_inventory_section_normal_processing(self, mock_aws_integration):
348
+ """Test normal processing of an inventory section"""
349
+ inventory = {
350
+ "EC2Instances": [
351
+ {"id": "i-1234567890", "name": "Test Instance 1"},
352
+ {"id": "i-0987654321", "name": "Test Instance 2"},
353
+ ]
354
+ }
355
+ section_key = "EC2Instances"
356
+ asset_type = "EC2 instance"
357
+
358
+ mock_parser = MagicMock()
359
+ mock_parser.side_effect = [
360
+ IntegrationAsset(name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"),
361
+ IntegrationAsset(name="Instance 2", identifier="i-0987654321", asset_type="EC2", asset_category="Compute"),
362
+ ]
363
+
364
+ mock_aws_integration._process_asset_collection = MagicMock()
365
+ mock_aws_integration._process_asset_collection.return_value = [
366
+ IntegrationAsset(name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"),
367
+ IntegrationAsset(name="Instance 2", identifier="i-0987654321", asset_type="EC2", asset_category="Compute"),
368
+ ]
369
+
370
+ results = list(
371
+ AWSInventoryIntegration._process_inventory_section(
372
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
373
+ )
374
+ )
375
+
376
+ mock_aws_integration._process_asset_collection.assert_called_once_with(
377
+ inventory["EC2Instances"], asset_type, mock_parser
378
+ )
379
+
380
+ assert len(results) == 2
381
+ assert results[0].name == "Instance 1"
382
+ assert results[1].name == "Instance 2"
383
+
384
+ def test_process_inventory_section_missing_key(self, mock_aws_integration):
385
+ """Test processing when section key doesn't exist in inventory"""
386
+ inventory = {
387
+ "S3Buckets": [
388
+ {"name": "test-bucket-1"},
389
+ {"name": "test-bucket-2"},
390
+ ]
391
+ }
392
+ section_key = "EC2Instances"
393
+ asset_type = "EC2 instance"
394
+
395
+ mock_parser = MagicMock()
396
+
397
+ mock_aws_integration._process_asset_collection = MagicMock()
398
+ mock_aws_integration._process_asset_collection.return_value = []
399
+
400
+ results = list(
401
+ AWSInventoryIntegration._process_inventory_section(
402
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
403
+ )
404
+ )
405
+
406
+ mock_aws_integration._process_asset_collection.assert_called_once_with([], asset_type, mock_parser)
407
+
408
+ assert len(results) == 0
409
+
410
+ def test_process_inventory_section_empty_section(self, mock_aws_integration):
411
+ """Test processing when section exists but is empty"""
412
+ inventory = {
413
+ "EC2Instances": [],
414
+ "S3Buckets": [
415
+ {"name": "test-bucket-1"},
416
+ ],
417
+ }
418
+ section_key = "EC2Instances"
419
+ asset_type = "EC2 instance"
420
+
421
+ mock_parser = MagicMock()
422
+
423
+ mock_aws_integration._process_asset_collection = MagicMock()
424
+ mock_aws_integration._process_asset_collection.return_value = []
425
+
426
+ results = list(
427
+ AWSInventoryIntegration._process_inventory_section(
428
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
429
+ )
430
+ )
431
+
432
+ mock_aws_integration._process_asset_collection.assert_called_once_with([], asset_type, mock_parser)
433
+
434
+ assert len(results) == 0
435
+
436
+ def test_process_inventory_section_empty_inventory(self, mock_aws_integration):
437
+ """Test processing with completely empty inventory"""
438
+ inventory = {}
439
+ section_key = "EC2Instances"
440
+ asset_type = "EC2 instance"
441
+
442
+ mock_parser = MagicMock()
443
+
444
+ mock_aws_integration._process_asset_collection = MagicMock()
445
+ mock_aws_integration._process_asset_collection.return_value = []
446
+
447
+ results = list(
448
+ AWSInventoryIntegration._process_inventory_section(
449
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
450
+ )
451
+ )
452
+
453
+ mock_aws_integration._process_asset_collection.assert_called_once_with([], asset_type, mock_parser)
454
+
455
+ assert len(results) == 0
456
+
457
+ def test_process_inventory_section_multiple_sections(self, mock_aws_integration):
458
+ """Test processing when inventory has multiple sections"""
459
+
460
+ inventory = {
461
+ "EC2Instances": [
462
+ {"id": "i-1234567890", "name": "Test Instance 1"},
463
+ ],
464
+ "S3Buckets": [
465
+ {"name": "test-bucket-1"},
466
+ {"name": "test-bucket-2"},
467
+ ],
468
+ "LambdaFunctions": [
469
+ {"name": "test-function-1"},
470
+ ],
471
+ }
472
+ section_key = "S3Buckets"
473
+ asset_type = "S3 bucket"
474
+
475
+ mock_parser = MagicMock()
476
+ mock_parser.side_effect = [
477
+ IntegrationAsset(name="Bucket 1", identifier="test-bucket-1", asset_type="S3", asset_category="Storage"),
478
+ IntegrationAsset(name="Bucket 2", identifier="test-bucket-2", asset_type="S3", asset_category="Storage"),
479
+ ]
480
+
481
+ mock_aws_integration._process_asset_collection = MagicMock()
482
+ mock_aws_integration._process_asset_collection.return_value = [
483
+ IntegrationAsset(name="Bucket 1", identifier="test-bucket-1", asset_type="S3", asset_category="Storage"),
484
+ IntegrationAsset(name="Bucket 2", identifier="test-bucket-2", asset_type="S3", asset_category="Storage"),
485
+ ]
486
+
487
+ results = list(
488
+ AWSInventoryIntegration._process_inventory_section(
489
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
490
+ )
491
+ )
492
+
493
+ mock_aws_integration._process_asset_collection.assert_called_once_with(
494
+ inventory["S3Buckets"], asset_type, mock_parser
495
+ )
496
+
497
+ assert len(results) == 2
498
+ assert results[0].name == "Bucket 1"
499
+ assert results[1].name == "Bucket 2"
500
+
501
+ def test_process_inventory_section_with_special_users_structure(self, mock_aws_integration):
502
+ """Test processing section that contains special Users structure"""
503
+ inventory = {
504
+ "IAM": {
505
+ "Users": [
506
+ {"id": "user1", "name": "User 1"},
507
+ {"id": "user2", "name": "User 2"},
508
+ ]
509
+ }
510
+ }
511
+ section_key = "IAM"
512
+ asset_type = "IAM Users"
513
+
514
+ mock_parser = MagicMock()
515
+ mock_parser.side_effect = [
516
+ IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity"),
517
+ IntegrationAsset(name="User 2", identifier="user2", asset_type="IAM", asset_category="Identity"),
518
+ ]
519
+
520
+ mock_aws_integration._process_asset_collection = MagicMock()
521
+ mock_aws_integration._process_asset_collection.return_value = [
522
+ IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity"),
523
+ IntegrationAsset(name="User 2", identifier="user2", asset_type="IAM", asset_category="Identity"),
524
+ ]
525
+
526
+ results = list(
527
+ AWSInventoryIntegration._process_inventory_section(
528
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
529
+ )
530
+ )
531
+
532
+ mock_aws_integration._process_asset_collection.assert_called_once_with(
533
+ inventory["IAM"], asset_type, mock_parser
534
+ )
535
+
536
+ assert len(results) == 2
537
+ assert results[0].name == "User 1"
538
+ assert results[1].name == "User 2"
539
+
540
+ def test_process_inventory_section_delegates_to_process_asset_collection(self, mock_aws_integration):
541
+ """Test that _process_inventory_section properly delegates to _process_asset_collection"""
542
+ inventory = {
543
+ "EC2Instances": [
544
+ {"id": "i-1234567890", "name": "Test Instance"},
545
+ ]
546
+ }
547
+ section_key = "EC2Instances"
548
+ asset_type = "EC2 instance"
549
+
550
+ mock_parser = MagicMock()
551
+ mock_parser.return_value = IntegrationAsset(
552
+ name="Test Instance", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
553
+ )
554
+
555
+ mock_aws_integration._process_asset_collection = MagicMock()
556
+ mock_aws_integration._process_asset_collection.return_value = [mock_parser.return_value]
557
+
558
+ results = list(
559
+ AWSInventoryIntegration._process_inventory_section(
560
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
561
+ )
562
+ )
563
+
564
+ mock_aws_integration._process_asset_collection.assert_called_once_with(
565
+ inventory["EC2Instances"], asset_type, mock_parser
566
+ )
567
+
568
+ assert len(results) == 1
569
+ assert results[0].name == "Test Instance"
570
+
571
+ def test_fetch_assets_normal_processing(self, mock_aws_integration):
572
+ """Test normal processing of assets from inventory"""
573
+ inventory = {
574
+ "EC2Instances": [
575
+ {"id": "i-1234567890", "name": "Test Instance 1"},
576
+ {"id": "i-0987654321", "name": "Test Instance 2"},
577
+ ],
578
+ "S3Buckets": [
579
+ {"name": "test-bucket-1"},
580
+ {"name": "test-bucket-2"},
581
+ ],
582
+ "IAM": {
583
+ "Users": [
584
+ {"id": "user1", "name": "User 1"},
585
+ ]
586
+ },
587
+ }
588
+
589
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
590
+
591
+ mock_aws_integration.get_asset_configs = MagicMock(
592
+ return_value=[
593
+ ("EC2Instances", "EC2 instance", MagicMock()),
594
+ ("S3Buckets", "S3 bucket", MagicMock()),
595
+ ]
596
+ )
597
+
598
+ mock_aws_integration._process_inventory_section = MagicMock()
599
+ mock_aws_integration._process_inventory_section.side_effect = [
600
+ [
601
+ IntegrationAsset(
602
+ name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
603
+ ),
604
+ IntegrationAsset(
605
+ name="Instance 2", identifier="i-0987654321", asset_type="EC2", asset_category="Compute"
606
+ ),
607
+ ],
608
+ [
609
+ IntegrationAsset(
610
+ name="Bucket 1", identifier="test-bucket-1", asset_type="S3", asset_category="Storage"
611
+ ),
612
+ IntegrationAsset(
613
+ name="Bucket 2", identifier="test-bucket-2", asset_type="S3", asset_category="Storage"
614
+ ),
615
+ ],
616
+ ]
617
+
618
+ results = list(
619
+ AWSInventoryIntegration.fetch_assets(
620
+ mock_aws_integration,
621
+ region="us-east-1",
622
+ aws_access_key_id="test_key",
623
+ aws_secret_access_key="test_secret",
624
+ aws_session_token="test_token",
625
+ )
626
+ )
627
+
628
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once_with(
629
+ "us-east-1", "test_key", "test_secret", "test_token"
630
+ )
631
+
632
+ mock_aws_integration.get_asset_configs.assert_called_once()
633
+
634
+ assert mock_aws_integration._process_inventory_section.call_count == 2
635
+
636
+ assert mock_aws_integration.num_assets_to_process == 0
637
+
638
+ assert len(results) == 4
639
+ assert results[0].name == "Instance 1"
640
+ assert results[1].name == "Instance 2"
641
+ assert results[2].name == "Bucket 1"
642
+ assert results[3].name == "Bucket 2"
643
+
644
+ def test_fetch_assets_empty_inventory(self, mock_aws_integration):
645
+ """Test fetching assets when inventory is empty"""
646
+ inventory = {}
647
+
648
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
649
+
650
+ mock_aws_integration.get_asset_configs = MagicMock(
651
+ return_value=[
652
+ ("EC2Instances", "EC2 instance", MagicMock()),
653
+ ("S3Buckets", "S3 bucket", MagicMock()),
654
+ ]
655
+ )
656
+
657
+ mock_aws_integration._process_inventory_section = MagicMock(return_value=[])
658
+
659
+ results = list(AWSInventoryIntegration.fetch_assets(mock_aws_integration, region="us-east-1"))
660
+
661
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once_with("us-east-1", None, None, None)
662
+
663
+ assert mock_aws_integration._process_inventory_section.call_count == 2
664
+
665
+ assert len(results) == 0
666
+
667
+ def test_fetch_assets_no_asset_configs(self, mock_aws_integration):
668
+ """Test fetching assets when no asset configs are available"""
669
+ inventory = {
670
+ "EC2Instances": [
671
+ {"id": "i-1234567890", "name": "Test Instance"},
672
+ ]
673
+ }
674
+
675
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
676
+
677
+ mock_aws_integration.get_asset_configs = MagicMock(return_value=[])
678
+
679
+ mock_aws_integration._process_inventory_section = MagicMock()
680
+
681
+ results = list(AWSInventoryIntegration.fetch_assets(mock_aws_integration, region="us-east-1"))
682
+
683
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once()
684
+
685
+ mock_aws_integration.get_asset_configs.assert_called_once()
686
+
687
+ mock_aws_integration._process_inventory_section.assert_not_called()
688
+
689
+ assert len(results) == 0
690
+
691
+ def test_fetch_assets_all_asset_types(self, mock_aws_integration):
692
+ """Test fetching assets for all configured asset types"""
693
+ inventory = {
694
+ "IAM": {"Users": [{"id": "user1", "name": "User 1"}]},
695
+ "EC2Instances": [{"id": "i-1234567890", "name": "Test Instance"}],
696
+ "LambdaFunctions": [{"name": "test-function"}],
697
+ "S3Buckets": [{"name": "test-bucket"}],
698
+ "RDSInstances": [{"name": "test-rds"}],
699
+ "DynamoDBTables": [{"name": "test-dynamo"}],
700
+ "VPCs": [{"name": "test-vpc"}],
701
+ "LoadBalancers": [{"name": "test-lb"}],
702
+ "ECRRepositories": [{"name": "test-ecr"}],
703
+ }
704
+
705
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
706
+
707
+ mock_aws_integration.get_asset_configs = MagicMock(
708
+ return_value=[
709
+ ("IAM", "Roles", MagicMock()),
710
+ ("EC2Instances", "EC2 instance", MagicMock()),
711
+ ("LambdaFunctions", "Lambda function", MagicMock()),
712
+ ("S3Buckets", "S3 bucket", MagicMock()),
713
+ ("RDSInstances", "RDS instance", MagicMock()),
714
+ ("DynamoDBTables", "DynamoDB table", MagicMock()),
715
+ ("VPCs", "VPC", MagicMock()),
716
+ ("LoadBalancers", "Load Balancer", MagicMock()),
717
+ ("ECRRepositories", "ECR repository", MagicMock()),
718
+ ]
719
+ )
720
+
721
+ mock_aws_integration._process_inventory_section = MagicMock()
722
+ mock_aws_integration._process_inventory_section.side_effect = [
723
+ [IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity")],
724
+ [
725
+ IntegrationAsset(
726
+ name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
727
+ )
728
+ ],
729
+ [
730
+ IntegrationAsset(
731
+ name="Function 1", identifier="test-function", asset_type="Lambda", asset_category="Compute"
732
+ )
733
+ ],
734
+ [IntegrationAsset(name="Bucket 1", identifier="test-bucket", asset_type="S3", asset_category="Storage")],
735
+ [IntegrationAsset(name="RDS 1", identifier="test-rds", asset_type="RDS", asset_category="Database")],
736
+ [
737
+ IntegrationAsset(
738
+ name="Dynamo 1", identifier="test-dynamo", asset_type="DynamoDB", asset_category="Database"
739
+ )
740
+ ],
741
+ [IntegrationAsset(name="VPC 1", identifier="test-vpc", asset_type="VPC", asset_category="Network")],
742
+ [IntegrationAsset(name="LB 1", identifier="test-lb", asset_type="LoadBalancer", asset_category="Network")],
743
+ [IntegrationAsset(name="ECR 1", identifier="test-ecr", asset_type="ECR", asset_category="Container")],
744
+ ]
745
+
746
+ results = list(AWSInventoryIntegration.fetch_assets(mock_aws_integration, region="us-east-1"))
747
+
748
+ assert mock_aws_integration._process_inventory_section.call_count == 9
749
+
750
+ assert len(results) == 9
751
+ assert results[0].name == "User 1"
752
+ assert results[1].name == "Instance 1"
753
+ assert results[2].name == "Function 1"
754
+ assert results[3].name == "Bucket 1"
755
+ assert results[4].name == "RDS 1"
756
+ assert results[5].name == "Dynamo 1"
757
+ assert results[6].name == "VPC 1"
758
+ assert results[7].name == "LB 1"
759
+ assert results[8].name == "ECR 1"
760
+
761
+ def test_fetch_assets_delegates_to_other_methods(self, mock_aws_integration):
762
+ """Test that fetch_assets properly delegates to other methods"""
763
+ inventory = {
764
+ "EC2Instances": [{"id": "i-1234567890", "name": "Test Instance"}],
765
+ }
766
+
767
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
768
+ mock_aws_integration.get_asset_configs = MagicMock(
769
+ return_value=[
770
+ ("EC2Instances", "EC2 instance", MagicMock()),
771
+ ]
772
+ )
773
+ mock_aws_integration._process_inventory_section = MagicMock(
774
+ return_value=[
775
+ IntegrationAsset(
776
+ name="Test Instance", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
777
+ )
778
+ ]
779
+ )
780
+
781
+ results = list(
782
+ AWSInventoryIntegration.fetch_assets(
783
+ mock_aws_integration,
784
+ region="us-east-1",
785
+ aws_access_key_id="test_key",
786
+ aws_secret_access_key="test_secret",
787
+ )
788
+ )
789
+
790
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once_with(
791
+ "us-east-1", "test_key", "test_secret", None
792
+ )
793
+
794
+ mock_aws_integration.get_asset_configs.assert_called_once()
795
+
796
+ mock_aws_integration._process_inventory_section.assert_called_once_with(
797
+ inventory, "EC2Instances", "EC2 instance", mock_aws_integration.get_asset_configs.return_value[0][2]
798
+ )
799
+
800
+ assert mock_aws_integration.num_assets_to_process == 0
801
+
802
+ assert len(results) == 1
803
+ assert results[0].name == "Test Instance"
804
+
805
+ def test_parses_linux_instance_with_name_tag(self, mock_aws_integration):
806
+ """Should parse Linux EC2 instance with Name tag correctly."""
807
+ instance = self._build_ec2_instance_data(
808
+ instance_id="i-1234567890abcdef0",
809
+ name="Test Linux Server",
810
+ PrivateIpAddress="10.0.1.100",
811
+ PublicIpAddress="52.1.2.3",
812
+ PrivateDnsName="ip-10-0-1-100.ec2.internal",
813
+ PublicDnsName="ec2-52-1-2-3.compute-1.amazonaws.com",
814
+ VpcId="vpc-12345678",
815
+ SubnetId="subnet-12345678",
816
+ ImageId="ami-12345678",
817
+ Architecture="x86_64",
818
+ PlatformDetails="Linux/UNIX",
819
+ CpuOptions={"CoreCount": 2, "ThreadsPerCore": 2},
820
+ Tags=[{"Key": "Name", "Value": "Test Linux Server"}, {"Key": "Environment", "Value": "Production"}],
821
+ )
822
+
823
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
824
+
825
+ assert result.name == "Test Linux Server"
826
+ assert result.identifier == "i-1234567890abcdef0"
827
+ assert result.asset_type == regscale_models.AssetType.VM
828
+ assert result.operating_system == regscale_models.AssetOperatingSystem.Linux
829
+ assert result.is_public_facing is True
830
+ assert result.ip_address == "10.0.1.100"
831
+ assert result.fqdn == "ec2-52-1-2-3.compute-1.amazonaws.com"
832
+ assert result.cpu == 4 # 2 cores * 2 threads
833
+ assert result.ram == 16
834
+ assert result.location == "us-east-1"
835
+ assert result.model == "t3.micro"
836
+ assert result.manufacturer == "AWS"
837
+ assert result.vlan_id == "subnet-12345678"
838
+ assert result.is_virtual is True
839
+ assert result.source_data == instance
840
+
841
+ expected_uri = (
842
+ "https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#InstanceDetails:instanceId=i-1234567890abcdef0"
843
+ )
844
+ assert result.uri == expected_uri
845
+
846
+ def test_parses_windows_instance(self, mock_aws_integration):
847
+ """Should parse Windows EC2 instance correctly."""
848
+ instance = self._build_ec2_instance_data(
849
+ instance_id="i-0987654321fedcba0",
850
+ instance_type="t3.small",
851
+ PrivateIpAddress="10.0.1.101",
852
+ Region="us-west-2",
853
+ Platform="windows",
854
+ PlatformDetails="Windows",
855
+ CpuOptions={"CoreCount": 1, "ThreadsPerCore": 2},
856
+ BlockDeviceMappings=[
857
+ {"DeviceName": "/dev/sda1", "Ebs": {"VolumeId": "vol-87654321"}},
858
+ {"DeviceName": "/dev/sdb", "Ebs": {"VolumeId": "vol-87654322"}},
859
+ ],
860
+ ImageInfo={
861
+ "Name": "Windows_Server-2019-English-Full-Base-2023.12.13",
862
+ "Description": "Microsoft Windows Server 2019 with Full Desktop Experience",
863
+ "RootDeviceType": "ebs",
864
+ "VirtualizationType": "hvm",
865
+ },
866
+ )
867
+
868
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
869
+
870
+ assert result.operating_system == regscale_models.AssetOperatingSystem.WindowsServer
871
+ assert result.asset_type == regscale_models.AssetType.VM
872
+ assert result.cpu == 2 # 1 core * 2 threads
873
+ assert result.disk_storage == 16 # 2 devices * 8GB each
874
+ assert result.fqdn == "i-0987654321fedcba0" # No DNS names, falls back to instance ID
875
+ assert "Windows" in result.description
876
+
877
+ def test_parse_ec2_instance_palo_alto(self, mock_aws_integration):
878
+ """Test parsing a Palo Alto EC2 instance"""
879
+ instance = {
880
+ "InstanceId": "i-paloalto123456",
881
+ "InstanceType": "c5.large",
882
+ "State": "running",
883
+ "PrivateIpAddress": "10.0.1.102",
884
+ "Region": "us-east-1",
885
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
886
+ "BlockDeviceMappings": [{"DeviceName": "/dev/xvda", "Ebs": {"VolumeId": "vol-palo123"}}],
887
+ "ImageInfo": {
888
+ "Name": "pa-vm-aws-10.2.3-h4",
889
+ "Description": "Palo Alto Networks VM-Series Firewall",
890
+ "RootDeviceType": "ebs",
891
+ "VirtualizationType": "hvm",
892
+ },
893
+ }
894
+
895
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
896
+
897
+ assert result.operating_system == regscale_models.AssetOperatingSystem.PaloAlto
898
+ assert result.asset_type == regscale_models.AssetType.Appliance
899
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
900
+ assert result.component_type == regscale_models.ComponentType.Hardware
901
+ assert result.component_names == ["Palo Alto Networks IDPS"]
902
+ assert result.cpu == 2 # 2 cores * 1 thread
903
+ assert "Palo Alto Networks VM-Series Firewall" in result.os_version
904
+
905
+ def test_parse_ec2_instance_no_name_tag(self, mock_aws_integration):
906
+ """Test parsing an EC2 instance without a Name tag"""
907
+ instance = {
908
+ "InstanceId": "i-noname123456",
909
+ "Tags": [{"Key": "Environment", "Value": "Development"}, {"Key": "Project", "Value": "TestProject"}],
910
+ "InstanceType": "t2.micro",
911
+ "State": "stopped",
912
+ "PrivateIpAddress": "10.0.1.103",
913
+ "Region": "us-east-1",
914
+ "CpuOptions": {"CoreCount": 1, "ThreadsPerCore": 1},
915
+ "BlockDeviceMappings": [],
916
+ "ImageInfo": {
917
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
918
+ "Description": "Amazon Linux 2 AMI",
919
+ "RootDeviceType": "ebs",
920
+ "VirtualizationType": "hvm",
921
+ },
922
+ }
923
+
924
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
925
+
926
+ assert result.name == "i-noname123456"
927
+ assert result.status == regscale_models.AssetStatus.Inactive # stopped state
928
+ assert result.cpu == 1 # 1 core * 1 thread
929
+ assert result.disk_storage == 0 # No block devices
930
+
931
+ def test_parse_ec2_instance_no_tags(self, mock_aws_integration):
932
+ """Test parsing an EC2 instance with no tags"""
933
+ instance = {
934
+ "InstanceId": "i-notags123456",
935
+ "InstanceType": "t3.nano",
936
+ "State": "running",
937
+ "Region": "us-east-1",
938
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
939
+ "BlockDeviceMappings": [{"DeviceName": "/dev/xvda", "Ebs": {"VolumeId": "vol-notags123"}}],
940
+ "ImageInfo": {
941
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
942
+ "Description": "Amazon Linux 2 AMI",
943
+ "RootDeviceType": "ebs",
944
+ "VirtualizationType": "hvm",
945
+ },
946
+ }
947
+
948
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
949
+
950
+ assert result.name == "i-notags123456"
951
+ assert result.cpu == 2 # 2 cores * 1 thread
952
+ assert result.disk_storage == 8 # 1 device * 8GB
953
+
954
+ def test_parse_ec2_instance_public_facing(self, mock_aws_integration):
955
+ """Test parsing a public-facing EC2 instance"""
956
+ instance = {
957
+ "InstanceId": "i-public123456",
958
+ "InstanceType": "t3.micro",
959
+ "State": "running",
960
+ "PrivateIpAddress": "10.0.1.104",
961
+ "PublicIpAddress": "54.1.2.3",
962
+ "PrivateDnsName": "ip-10-0-1-104.ec2.internal",
963
+ "PublicDnsName": "ec2-54-1-2-3.compute-1.amazonaws.com",
964
+ "Region": "us-east-1",
965
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
966
+ "BlockDeviceMappings": [],
967
+ "ImageInfo": {
968
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
969
+ "Description": "Amazon Linux 2 AMI",
970
+ "RootDeviceType": "ebs",
971
+ "VirtualizationType": "hvm",
972
+ },
973
+ }
974
+
975
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
976
+
977
+ assert result.is_public_facing is True
978
+ assert result.ip_address == "10.0.1.104" # Prefers private IP
979
+ assert result.fqdn == "ec2-54-1-2-3.compute-1.amazonaws.com" # Prefers public DNS
980
+ assert "Public IP: 54.1.2.3" in result.notes
981
+
982
+ def test_parse_ec2_instance_private_only(self, mock_aws_integration):
983
+ """Test parsing a private-only EC2 instance"""
984
+ instance = {
985
+ "InstanceId": "i-private123456",
986
+ "InstanceType": "t3.micro",
987
+ "State": "running",
988
+ "PrivateIpAddress": "10.0.1.105",
989
+ "PrivateDnsName": "ip-10-0-1-105.ec2.internal",
990
+ "Region": "us-east-1",
991
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
992
+ "BlockDeviceMappings": [],
993
+ "ImageInfo": {
994
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
995
+ "Description": "Amazon Linux 2 AMI",
996
+ "RootDeviceType": "ebs",
997
+ "VirtualizationType": "hvm",
998
+ },
999
+ }
1000
+
1001
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
1002
+
1003
+ assert result.is_public_facing is False
1004
+ assert result.ip_address == "10.0.1.105"
1005
+ assert result.fqdn == "ip-10-0-1-105.ec2.internal"
1006
+ assert "Public IP: N/A" in result.notes
1007
+
1008
+ def test_parse_ec2_instance_minimal_data(self, mock_aws_integration):
1009
+ """Test parsing an EC2 instance with minimal data"""
1010
+ instance = {
1011
+ "InstanceId": "i-minimal123456",
1012
+ "InstanceType": "t3.micro",
1013
+ "State": "running",
1014
+ "Region": "us-east-1",
1015
+ }
1016
+
1017
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
1018
+
1019
+ assert result.name == "i-minimal123456"
1020
+ assert result.identifier == "i-minimal123456"
1021
+ assert result.ip_address is None # No IP addresses provided
1022
+ assert result.fqdn == "i-minimal123456"
1023
+ assert result.cpu == 0 # No CPU options
1024
+ assert result.disk_storage == 0 # No block devices
1025
+ assert result.operating_system == regscale_models.AssetOperatingSystem.Linux # Default
1026
+ assert result.os_version == ""
1027
+ assert result.location == "us-east-1"
1028
+ assert result.model == "t3.micro"
1029
+ assert result.is_public_facing is False
1030
+ assert result.vlan_id is None # No subnet ID provided
1031
+ assert "Private IP: N/A" in result.notes
1032
+ assert "Public IP: N/A" in result.notes
1033
+
1034
+ def test_parse_ec2_instance_edge_cases(self, mock_aws_integration):
1035
+ """Test parsing EC2 instance with edge cases"""
1036
+ instance = {
1037
+ "InstanceId": "i-edge123456",
1038
+ "InstanceType": "t3.micro",
1039
+ "State": "pending",
1040
+ "Region": "us-east-1",
1041
+ "CpuOptions": {}, # Empty CPU options
1042
+ "BlockDeviceMappings": [
1043
+ {"DeviceName": "/dev/xvda"}, # No Ebs field
1044
+ {"DeviceName": "/dev/sdb", "Ebs": {"VolumeId": "vol-edge123"}},
1045
+ ],
1046
+ "ImageInfo": {
1047
+ "Name": "custom-ami-123",
1048
+ "Description": "",
1049
+ "RootDeviceType": "ebs",
1050
+ "VirtualizationType": "hvm",
1051
+ },
1052
+ }
1053
+
1054
+ result = AWSInventoryIntegration.parse_ec2_instance(mock_aws_integration, instance)
1055
+
1056
+ assert result.cpu == 0 # Empty CPU options
1057
+ assert result.disk_storage == 8 # Only one Ebs device
1058
+ assert result.status == regscale_models.AssetStatus.Inactive # pending state
1059
+ assert result.os_version == ""
1060
+ assert result.operating_system == regscale_models.AssetOperatingSystem.Linux # Default
1061
+
1062
+ def test_parse_lambda_function_basic(self, mock_aws_integration):
1063
+ """Test parsing a basic Lambda function"""
1064
+ function = {
1065
+ "FunctionName": "test-function",
1066
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:test-function",
1067
+ "Runtime": "python3.9",
1068
+ "Handler": "index.handler",
1069
+ "MemorySize": 128,
1070
+ "Timeout": 30,
1071
+ "Region": "us-east-1",
1072
+ }
1073
+
1074
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1075
+
1076
+ assert result.name == "test-function"
1077
+ assert result.identifier == "test-function"
1078
+ assert result.asset_type == regscale_models.AssetType.Other
1079
+ assert result.asset_category == regscale_models.AssetCategory.Software
1080
+ assert result.component_type == regscale_models.ComponentType.Software
1081
+ assert result.component_names == ["Lambda Functions"]
1082
+ assert result.parent_id == mock_aws_integration.plan_id
1083
+ assert result.parent_module == "securityplans"
1084
+ assert result.status == regscale_models.AssetStatus.Active
1085
+ assert result.location == "us-east-1"
1086
+ assert result.software_name == "python3.9"
1087
+ assert result.software_version == "9" # The method uses split(".")[-1] to get last part
1088
+ assert result.ram == 128
1089
+ assert result.external_id == "test-function"
1090
+ assert result.aws_identifier == "arn:aws:lambda:us-east-1:123456789012:function:test-function"
1091
+ assert result.manufacturer == "AWS"
1092
+ assert result.is_virtual is True
1093
+ assert result.source_data == function
1094
+
1095
+ assert "AWS Lambda function test-function" in result.description
1096
+ assert "python3.9" in result.description
1097
+ assert "128MB memory" in result.description
1098
+ assert "Function Name: test-function" in result.notes
1099
+ assert "Runtime: python3.9" in result.notes
1100
+ assert "Memory Size: 128 MB" in result.notes
1101
+ assert "Timeout: 30 seconds" in result.notes
1102
+ assert "Handler: index.handler" in result.notes
1103
+
1104
+ def test_parse_lambda_function_with_description(self, mock_aws_integration):
1105
+ """Test parsing a Lambda function with description"""
1106
+ function = {
1107
+ "FunctionName": "api-gateway-function",
1108
+ "FunctionArn": "arn:aws:lambda:us-west-2:123456789012:function:api-gateway-function",
1109
+ "Runtime": "nodejs18.x",
1110
+ "Handler": "app.handler",
1111
+ "MemorySize": 256,
1112
+ "Timeout": 60,
1113
+ "Description": "API Gateway integration function for user authentication",
1114
+ "Region": "us-west-2",
1115
+ }
1116
+
1117
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1118
+
1119
+ assert "API Gateway integration function for user authentication" in result.description
1120
+ assert "Function description: API Gateway integration function for user authentication" in result.description
1121
+ assert "Description: API Gateway integration function for user authentication" in result.notes
1122
+ assert result.software_name == "nodejs18.x"
1123
+ assert result.software_version == "x" # The method uses split(".")[-1] to get last part
1124
+ assert result.ram == 256
1125
+
1126
+ def test_parse_lambda_function_with_function_url(self, mock_aws_integration):
1127
+ """Test parsing a Lambda function with FunctionUrl"""
1128
+ function = {
1129
+ "FunctionName": "webhook-function",
1130
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:webhook-function",
1131
+ "Runtime": "python3.11",
1132
+ "Handler": "lambda_function.lambda_handler",
1133
+ "MemorySize": 512,
1134
+ "Timeout": 120,
1135
+ "FunctionUrl": "https://abc123.lambda-url.us-east-1.on.aws/",
1136
+ "Region": "us-east-1",
1137
+ }
1138
+
1139
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1140
+
1141
+ assert result.uri == "https://abc123.lambda-url.us-east-1.on.aws/"
1142
+ assert result.software_name == "python3.11"
1143
+ assert result.software_version == "11" # The method uses split(".")[-1] to get last part
1144
+ assert result.ram == 512
1145
+
1146
+ @pytest.mark.parametrize(
1147
+ "function_data,expected_software_name,expected_software_version,expected_ram",
1148
+ [
1149
+ (
1150
+ {
1151
+ "FunctionName": "simple-function",
1152
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:simple-function",
1153
+ "Runtime": "python3.8",
1154
+ "Handler": "main.handler",
1155
+ "MemorySize": 64,
1156
+ "Timeout": 15,
1157
+ "Region": "us-east-1",
1158
+ },
1159
+ "python3.8",
1160
+ "8", # The method uses split(".")[-1] to get last part
1161
+ 64,
1162
+ ),
1163
+ (
1164
+ {
1165
+ "FunctionName": "empty-desc-function",
1166
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:empty-desc-function",
1167
+ "Runtime": "java11",
1168
+ "Handler": "com.example.Handler::handleRequest",
1169
+ "MemorySize": 1024,
1170
+ "Timeout": 300,
1171
+ "Description": "",
1172
+ "Region": "us-east-1",
1173
+ },
1174
+ "java11",
1175
+ "java11", # No dots in java11, so full string is used
1176
+ 1024,
1177
+ ),
1178
+ (
1179
+ {
1180
+ "FunctionName": "non-string-desc-function",
1181
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:non-string-desc-function",
1182
+ "Runtime": "dotnet6",
1183
+ "Handler": "MyFunction::FunctionHandler",
1184
+ "MemorySize": 256,
1185
+ "Timeout": 60,
1186
+ "Description": None, # Non-string description
1187
+ "Region": "us-east-1",
1188
+ },
1189
+ "dotnet6",
1190
+ "dotnet6", # No dots in dotnet6, so full string is used
1191
+ 256,
1192
+ ),
1193
+ ],
1194
+ ids=["no_description", "empty_description", "non_string_description"],
1195
+ )
1196
+ def test_parse_lambda_function_description_variations(
1197
+ self, function_data, expected_software_name, expected_software_version, expected_ram, mock_aws_integration
1198
+ ):
1199
+ """Test parsing Lambda functions with various description scenarios."""
1200
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function_data)
1201
+
1202
+ assert "Function description:" not in result.description
1203
+ assert "Description: " in result.notes
1204
+ assert result.software_name == expected_software_name
1205
+ assert result.software_version == expected_software_version
1206
+ assert result.ram == expected_ram
1207
+
1208
+ @pytest.mark.parametrize(
1209
+ "function_data,expected_software_name,expected_software_version,expected_description_contains,expected_notes_contains",
1210
+ [
1211
+ (
1212
+ {
1213
+ "FunctionName": "no-runtime-function",
1214
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:no-runtime-function",
1215
+ "Handler": "index.handler",
1216
+ "MemorySize": 128,
1217
+ "Timeout": 30,
1218
+ "Region": "us-east-1",
1219
+ },
1220
+ None,
1221
+ None,
1222
+ "unknown runtime",
1223
+ "Runtime: unknown",
1224
+ ),
1225
+ (
1226
+ {
1227
+ "FunctionName": "empty-runtime-function",
1228
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:empty-runtime-function",
1229
+ "Runtime": "",
1230
+ "Handler": "index.handler",
1231
+ "MemorySize": 128,
1232
+ "Timeout": 30,
1233
+ "Region": "us-east-1",
1234
+ },
1235
+ "",
1236
+ None,
1237
+ "running with 128MB memory", # Empty runtime shows as empty space
1238
+ "Runtime: ", # Empty runtime shows as empty in notes
1239
+ ),
1240
+ ],
1241
+ ids=["no_runtime", "empty_runtime"],
1242
+ )
1243
+ def test_parse_lambda_function_runtime_variations(
1244
+ self,
1245
+ function_data,
1246
+ expected_software_name,
1247
+ expected_software_version,
1248
+ expected_description_contains,
1249
+ expected_notes_contains,
1250
+ mock_aws_integration,
1251
+ ):
1252
+ """Test parsing Lambda functions with various runtime scenarios."""
1253
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function_data)
1254
+
1255
+ assert result.software_name == expected_software_name
1256
+ assert result.software_version == expected_software_version
1257
+ assert expected_description_contains in result.description
1258
+ assert expected_notes_contains in result.notes
1259
+
1260
+ def test_parse_lambda_function_minimal_data(self, mock_aws_integration):
1261
+ """Test parsing a Lambda function with minimal data"""
1262
+ function = {"FunctionName": "minimal-function"}
1263
+
1264
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1265
+
1266
+ assert result.name == "minimal-function"
1267
+ assert result.identifier == "minimal-function"
1268
+ assert result.asset_type == regscale_models.AssetType.Other
1269
+ assert result.asset_category == regscale_models.AssetCategory.Software
1270
+ assert result.component_type == regscale_models.ComponentType.Software
1271
+ assert result.component_names == ["Lambda Functions"]
1272
+ assert result.status == regscale_models.AssetStatus.Active
1273
+ assert result.location is None
1274
+ assert result.software_name is None
1275
+ assert result.software_version is None
1276
+ assert result.ram is None
1277
+ assert result.external_id == "minimal-function"
1278
+ assert result.aws_identifier is None
1279
+ assert result.uri is None
1280
+ assert result.manufacturer == "AWS"
1281
+ assert result.is_virtual is True
1282
+ assert result.source_data == function
1283
+
1284
+ assert "AWS Lambda function minimal-function" in result.description
1285
+ assert "unknown runtime" in result.description
1286
+ assert "0MB memory" in result.description
1287
+ assert "Function Name: minimal-function" in result.notes
1288
+ assert "Runtime: unknown" in result.notes
1289
+ assert "Memory Size: 0 MB" in result.notes
1290
+ assert "Timeout: 0 seconds" in result.notes
1291
+ assert "Handler: " in result.notes
1292
+ assert "Description: " in result.notes
1293
+
1294
+ def test_parse_lambda_function_edge_cases(self, mock_aws_integration):
1295
+ """Test parsing Lambda function with edge cases"""
1296
+ function = {
1297
+ "FunctionName": "edge-case-function",
1298
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:edge-case-function",
1299
+ "Runtime": "python3.12.1", # Runtime with multiple dots
1300
+ "Handler": "", # Empty handler
1301
+ "MemorySize": 0, # Zero memory
1302
+ "Timeout": 0, # Zero timeout
1303
+ "Description": " ", # Whitespace-only description
1304
+ "Region": "us-east-1",
1305
+ }
1306
+
1307
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1308
+
1309
+ assert result.software_name == "python3.12.1"
1310
+ assert result.software_version == "1" # The method uses split(".")[-1] to get last part
1311
+ assert result.ram == 0
1312
+ assert "0MB memory" in result.description
1313
+ assert "Memory Size: 0 MB" in result.notes
1314
+ assert "Timeout: 0 seconds" in result.notes
1315
+ assert "Handler: " in result.notes
1316
+
1317
+ assert "Function description: " in result.description
1318
+
1319
+ def test_parse_aws_account_basic(self, mock_aws_integration):
1320
+ """Test parsing a basic AWS account with IAM ARN"""
1321
+ iam = {
1322
+ "Arn": "arn:aws:iam::123456789012:user/test-user",
1323
+ "UserName": "test-user",
1324
+ "Path": "/",
1325
+ "CreateDate": "2023-01-01T00:00:00Z",
1326
+ }
1327
+
1328
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1329
+
1330
+ assert result.name == "123456789012"
1331
+ assert result.identifier == "AWS::::Account:123456789012"
1332
+ assert result.asset_type == regscale_models.AssetType.Other
1333
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1334
+ assert result.component_type == regscale_models.ComponentType.Software
1335
+ assert result.component_names == ["AWS Account"]
1336
+ assert result.parent_id == mock_aws_integration.plan_id
1337
+ assert result.parent_module == "securityplans"
1338
+ assert result.status == regscale_models.AssetStatus.Active
1339
+ assert result.location == "Unknown"
1340
+ assert result.external_id == "123456789012"
1341
+ assert result.aws_identifier == "AWS::::Account:123456789012"
1342
+ assert result.manufacturer == "AWS"
1343
+ assert result.source_data == iam
1344
+
1345
+ def test_parse_aws_account_role_arn(self, mock_aws_integration):
1346
+ """Test parsing AWS account from role ARN"""
1347
+ iam = {
1348
+ "Arn": "arn:aws:iam::987654321098:role/EC2Role",
1349
+ "RoleName": "EC2Role",
1350
+ "Path": "/",
1351
+ "CreateDate": "2023-01-01T00:00:00Z",
1352
+ }
1353
+
1354
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1355
+
1356
+ assert result.name == "987654321098"
1357
+ assert result.identifier == "AWS::::Account:987654321098"
1358
+ assert result.external_id == "987654321098"
1359
+ assert result.aws_identifier == "AWS::::Account:987654321098"
1360
+
1361
+ def test_parse_aws_account_group_arn(self, mock_aws_integration):
1362
+ """Test parsing AWS account from group ARN"""
1363
+ iam = {
1364
+ "Arn": "arn:aws:iam::555666777888:group/Developers",
1365
+ "GroupName": "Developers",
1366
+ "Path": "/",
1367
+ "CreateDate": "2023-01-01T00:00:00Z",
1368
+ }
1369
+
1370
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1371
+
1372
+ assert result.name == "555666777888"
1373
+ assert result.identifier == "AWS::::Account:555666777888"
1374
+ assert result.external_id == "555666777888"
1375
+ assert result.aws_identifier == "AWS::::Account:555666777888"
1376
+
1377
+ def test_parse_aws_account_policy_arn(self, mock_aws_integration):
1378
+ """Test parsing AWS account from policy ARN"""
1379
+ iam = {
1380
+ "Arn": "arn:aws:iam::111222333444:policy/AdminPolicy",
1381
+ "PolicyName": "AdminPolicy",
1382
+ "Path": "/",
1383
+ "CreateDate": "2023-01-01T00:00:00Z",
1384
+ }
1385
+
1386
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1387
+
1388
+ assert result.name == "111222333444"
1389
+ assert result.identifier == "AWS::::Account:111222333444"
1390
+ assert result.external_id == "111222333444"
1391
+ assert result.aws_identifier == "AWS::::Account:111222333444"
1392
+
1393
+ def test_parse_aws_account_no_arn(self, mock_aws_integration):
1394
+ """Test parsing AWS account with no ARN"""
1395
+ iam = {"UserName": "test-user", "Path": "/", "CreateDate": "2023-01-01T00:00:00Z"}
1396
+
1397
+ with pytest.raises(IndexError):
1398
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1399
+
1400
+ def test_parse_aws_account_empty_arn(self, mock_aws_integration):
1401
+ """Test parsing AWS account with empty ARN"""
1402
+ iam = {"Arn": "", "UserName": "test-user", "Path": "/", "CreateDate": "2023-01-01T00:00:00Z"}
1403
+
1404
+ with pytest.raises(IndexError):
1405
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1406
+
1407
+ def test_parse_aws_account_invalid_arn_format(self, mock_aws_integration):
1408
+ """Test parsing AWS account with invalid ARN format"""
1409
+ iam = {"Arn": "invalid:arn:format", "UserName": "test-user", "Path": "/", "CreateDate": "2023-01-01T00:00:00Z"}
1410
+
1411
+ with pytest.raises(IndexError):
1412
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1413
+
1414
+ def test_parse_aws_account_short_arn(self, mock_aws_integration):
1415
+ """Test parsing AWS account with ARN that has fewer than 5 parts"""
1416
+ iam = {
1417
+ "Arn": "arn:aws:iam::123456789012", # Only 4 parts
1418
+ "UserName": "test-user",
1419
+ "Path": "/",
1420
+ "CreateDate": "2023-01-01T00:00:00Z",
1421
+ }
1422
+
1423
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1424
+
1425
+ assert result.name == "123456789012" # This is what split(":")[4] would return
1426
+ assert result.identifier == "AWS::::Account:123456789012"
1427
+ assert result.external_id == "123456789012"
1428
+ assert result.aws_identifier == "AWS::::Account:123456789012"
1429
+
1430
+ def test_parse_aws_account_minimal_data(self, mock_aws_integration):
1431
+ """Test parsing AWS account with minimal IAM data"""
1432
+ iam = {}
1433
+
1434
+ with pytest.raises(IndexError):
1435
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1436
+
1437
+ def test_parse_aws_account_edge_cases(self, mock_aws_integration):
1438
+ """Test parsing AWS account with edge cases"""
1439
+ iam = {
1440
+ "Arn": "arn:aws:iam::000000000000:user/root", # Root account
1441
+ "UserName": "root",
1442
+ "Path": "/",
1443
+ "CreateDate": "2023-01-01T00:00:00Z",
1444
+ }
1445
+
1446
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1447
+
1448
+ assert result.name == "000000000000" # Root account ID
1449
+ assert result.identifier == "AWS::::Account:000000000000"
1450
+ assert result.external_id == "000000000000"
1451
+ assert result.aws_identifier == "AWS::::Account:000000000000"
1452
+ assert result.asset_type == regscale_models.AssetType.Other
1453
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1454
+ assert result.component_type == regscale_models.ComponentType.Software
1455
+ assert result.component_names == ["AWS Account"]
1456
+ assert result.status == regscale_models.AssetStatus.Active
1457
+ assert result.location == "Unknown"
1458
+ assert result.manufacturer == "AWS"
1459
+ assert result.source_data == iam
1460
+
1461
+ def test_parse_s3_bucket_basic(self, mock_aws_integration):
1462
+ """Test parsing a basic S3 bucket"""
1463
+ bucket = {"Name": "my-test-bucket", "Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"}
1464
+
1465
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1466
+
1467
+ assert result.name == "my-test-bucket"
1468
+ assert result.identifier == "my-test-bucket"
1469
+ assert result.asset_type == regscale_models.AssetType.Other
1470
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1471
+ assert result.component_type == regscale_models.ComponentType.Hardware
1472
+ assert result.component_names == ["S3 Buckets"]
1473
+ assert result.parent_id == mock_aws_integration.plan_id
1474
+ assert result.parent_module == "securityplans"
1475
+ assert result.status == regscale_models.AssetStatus.Active
1476
+ assert result.location == "us-east-1"
1477
+ assert result.external_id == "my-test-bucket"
1478
+ assert result.aws_identifier == "arn:aws:s3:::my-test-bucket"
1479
+ assert result.uri == "https://my-test-bucket.s3.amazonaws.com"
1480
+ assert result.manufacturer == "AWS"
1481
+ assert result.is_public_facing is False
1482
+ assert result.source_data == bucket
1483
+
1484
+ def test_parse_s3_bucket_public_facing(self, mock_aws_integration):
1485
+ """Test parsing a public-facing S3 bucket"""
1486
+ bucket = {
1487
+ "Name": "public-bucket",
1488
+ "Region": "us-west-2",
1489
+ "CreationDate": "2023-01-01T00:00:00Z",
1490
+ "Grants": [
1491
+ {"Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AllUsers"}, "Permission": "READ"},
1492
+ {"Grantee": {"ID": "123456789012"}, "Permission": "FULL_CONTROL"},
1493
+ ],
1494
+ }
1495
+
1496
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1497
+
1498
+ assert result.name == "public-bucket"
1499
+ assert result.is_public_facing is True
1500
+ assert result.aws_identifier == "arn:aws:s3:::public-bucket"
1501
+ assert result.uri == "https://public-bucket.s3.amazonaws.com"
1502
+ assert result.location == "us-west-2"
1503
+
1504
+ def test_parse_s3_bucket_private_with_grants(self, mock_aws_integration):
1505
+ """Test parsing a private S3 bucket with grants but no public access"""
1506
+
1507
+ bucket = {
1508
+ "Name": "private-bucket",
1509
+ "Region": "us-east-1",
1510
+ "CreationDate": "2023-01-01T00:00:00Z",
1511
+ "Grants": [
1512
+ {"Grantee": {"ID": "123456789012"}, "Permission": "FULL_CONTROL"},
1513
+ {"Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"}, "Permission": "READ"},
1514
+ ],
1515
+ }
1516
+
1517
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1518
+
1519
+ assert result.name == "private-bucket"
1520
+ assert result.is_public_facing is False # Not AllUsers, so private
1521
+ assert result.aws_identifier == "arn:aws:s3:::private-bucket"
1522
+ assert result.uri == "https://private-bucket.s3.amazonaws.com"
1523
+
1524
+ @pytest.mark.parametrize(
1525
+ "bucket_data,expected_name,expected_aws_identifier,expected_uri",
1526
+ [
1527
+ (
1528
+ {"Name": "no-grants-bucket", "Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"},
1529
+ "no-grants-bucket",
1530
+ "arn:aws:s3:::no-grants-bucket",
1531
+ "https://no-grants-bucket.s3.amazonaws.com",
1532
+ ),
1533
+ (
1534
+ {
1535
+ "Name": "empty-grants-bucket",
1536
+ "Region": "us-east-1",
1537
+ "CreationDate": "2023-01-01T00:00:00Z",
1538
+ "Grants": [],
1539
+ },
1540
+ "empty-grants-bucket",
1541
+ "arn:aws:s3:::empty-grants-bucket",
1542
+ "https://empty-grants-bucket.s3.amazonaws.com",
1543
+ ),
1544
+ ],
1545
+ ids=["no_grants", "empty_grants"],
1546
+ )
1547
+ def test_parse_s3_bucket_grants_variations(
1548
+ self, bucket_data, expected_name, expected_aws_identifier, expected_uri, mock_aws_integration
1549
+ ):
1550
+ """Test parsing S3 buckets with various grants scenarios."""
1551
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket_data)
1552
+
1553
+ assert result.name == expected_name
1554
+ assert result.is_public_facing is False # No/empty grants means private
1555
+ assert result.aws_identifier == expected_aws_identifier
1556
+ assert result.uri == expected_uri
1557
+
1558
+ @pytest.mark.parametrize(
1559
+ "bucket_data,expected_name,expected_identifier,expected_external_id,expected_aws_identifier,expected_uri",
1560
+ [
1561
+ (
1562
+ {"Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"},
1563
+ "",
1564
+ "",
1565
+ None,
1566
+ "arn:aws:s3:::None", # bucket.get('Name') returns None
1567
+ "https://None.s3.amazonaws.com", # bucket.get('Name') returns None
1568
+ ),
1569
+ (
1570
+ {"Name": "", "Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"},
1571
+ "",
1572
+ "",
1573
+ "",
1574
+ "arn:aws:s3:::",
1575
+ "https://.s3.amazonaws.com",
1576
+ ),
1577
+ ],
1578
+ ids=["no_name", "empty_name"],
1579
+ )
1580
+ def test_parse_s3_bucket_name_variations(
1581
+ self,
1582
+ bucket_data,
1583
+ expected_name,
1584
+ expected_identifier,
1585
+ expected_external_id,
1586
+ expected_aws_identifier,
1587
+ expected_uri,
1588
+ mock_aws_integration,
1589
+ ):
1590
+ """Test parsing S3 buckets with various name scenarios."""
1591
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket_data)
1592
+
1593
+ assert result.name == expected_name
1594
+ assert result.identifier == expected_identifier
1595
+ assert result.external_id == expected_external_id
1596
+ assert result.aws_identifier == expected_aws_identifier
1597
+ assert result.uri == expected_uri
1598
+ assert result.is_public_facing is False
1599
+
1600
+ def test_parse_s3_bucket_no_region(self, mock_aws_integration):
1601
+ """Test parsing an S3 bucket without region"""
1602
+
1603
+ bucket = {"Name": "no-region-bucket", "CreationDate": "2023-01-01T00:00:00Z"}
1604
+
1605
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1606
+
1607
+ assert result.name == "no-region-bucket"
1608
+ assert result.location is None
1609
+ assert result.aws_identifier == "arn:aws:s3:::no-region-bucket"
1610
+ assert result.uri == "https://no-region-bucket.s3.amazonaws.com"
1611
+ assert result.is_public_facing is False
1612
+
1613
+ def test_parse_s3_bucket_minimal_data(self, mock_aws_integration):
1614
+ """Test parsing an S3 bucket with minimal data"""
1615
+
1616
+ bucket = {}
1617
+
1618
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1619
+
1620
+ assert result.name == ""
1621
+ assert result.identifier == ""
1622
+ assert result.asset_type == regscale_models.AssetType.Other
1623
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1624
+ assert result.component_type == regscale_models.ComponentType.Hardware
1625
+ assert result.component_names == ["S3 Buckets"]
1626
+ assert result.parent_id == mock_aws_integration.plan_id
1627
+ assert result.parent_module == "securityplans"
1628
+ assert result.status == regscale_models.AssetStatus.Active
1629
+ assert result.location is None
1630
+ assert result.external_id is None
1631
+ assert result.aws_identifier == "arn:aws:s3:::None" # bucket.get('Name') returns None
1632
+ assert result.uri == "https://None.s3.amazonaws.com" # bucket.get('Name') returns None
1633
+ assert result.manufacturer == "AWS"
1634
+ assert result.is_public_facing is False
1635
+ assert result.source_data == bucket
1636
+
1637
+ def test_parse_s3_bucket_edge_cases(self, mock_aws_integration):
1638
+ """Test parsing S3 bucket with edge cases"""
1639
+
1640
+ bucket = {
1641
+ "Name": "edge-case-bucket",
1642
+ "Region": "us-east-1",
1643
+ "Grants": [
1644
+ {
1645
+ "Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AllUsers"},
1646
+ "Permission": "WRITE", # Different permission
1647
+ },
1648
+ {
1649
+ "Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AllUsers"},
1650
+ "Permission": "READ_ACP", # Another permission
1651
+ },
1652
+ ],
1653
+ }
1654
+
1655
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1656
+
1657
+ assert result.name == "edge-case-bucket"
1658
+ assert result.is_public_facing is True # Should detect AllUsers regardless of permission
1659
+ assert result.aws_identifier == "arn:aws:s3:::edge-case-bucket"
1660
+ assert result.uri == "https://edge-case-bucket.s3.amazonaws.com"
1661
+ assert result.location == "us-east-1"
1662
+ assert result.asset_type == regscale_models.AssetType.Other
1663
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1664
+ assert result.component_type == regscale_models.ComponentType.Hardware
1665
+ assert result.component_names == ["S3 Buckets"]
1666
+ assert result.status == regscale_models.AssetStatus.Active
1667
+ assert result.manufacturer == "AWS"
1668
+ assert result.source_data == bucket
1669
+
1670
+ def test_parse_rds_instance_basic(self, mock_aws_integration):
1671
+ """Test parsing a basic RDS instance"""
1672
+
1673
+ db = {
1674
+ "DBInstanceIdentifier": "test-db-instance",
1675
+ "DBInstanceClass": "db.t3.micro",
1676
+ "Engine": "mysql",
1677
+ "EngineVersion": "8.0.28",
1678
+ "DBInstanceStatus": "available",
1679
+ "AllocatedStorage": 20,
1680
+ "AvailabilityZone": "us-east-1a",
1681
+ "VpcId": "vpc-12345678",
1682
+ "PubliclyAccessible": False,
1683
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:test-db-instance",
1684
+ "Endpoint": {"Address": "test-db-instance.abc123.us-east-1.rds.amazonaws.com", "Port": 3306},
1685
+ }
1686
+
1687
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1688
+
1689
+ assert result.name == "test-db-instance 8.0.28) - db.t3.micro"
1690
+ assert result.identifier == "test-db-instance"
1691
+ assert result.asset_type == regscale_models.AssetType.VM
1692
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1693
+ assert result.component_type == regscale_models.ComponentType.Hardware
1694
+ assert result.component_names == ["RDS Instances"]
1695
+ assert result.parent_id == mock_aws_integration.plan_id
1696
+ assert result.parent_module == "securityplans"
1697
+ assert result.fqdn == "test-db-instance.abc123.us-east-1.rds.amazonaws.com"
1698
+ assert result.vlan_id == "vpc-12345678"
1699
+ assert result.status == regscale_models.AssetStatus.Active
1700
+ assert result.location == "us-east-1a"
1701
+ assert result.model == "db.t3.micro"
1702
+ assert result.manufacturer == "AWS"
1703
+ assert result.disk_storage == 20
1704
+ assert result.software_name == "mysql"
1705
+ assert result.software_version == "8.0.28"
1706
+ assert result.external_id == "test-db-instance"
1707
+ assert result.aws_identifier == "arn:aws:rds:us-east-1:123456789012:db:test-db-instance"
1708
+ assert result.is_public_facing is False
1709
+ assert result.source_data == db
1710
+
1711
+ def test_parse_rds_instance_no_engine_version(self, mock_aws_integration):
1712
+ """Test parsing RDS instance without engine version"""
1713
+
1714
+ db = {
1715
+ "DBInstanceIdentifier": "simple-db-instance",
1716
+ "DBInstanceClass": "db.r5.large",
1717
+ "Engine": "postgres",
1718
+ "DBInstanceStatus": "available",
1719
+ "AllocatedStorage": 100,
1720
+ "AvailabilityZone": "us-west-2a",
1721
+ "VpcId": "vpc-87654321",
1722
+ "PubliclyAccessible": True,
1723
+ "DBInstanceArn": "arn:aws:rds:us-west-2:123456789012:db:simple-db-instance",
1724
+ "Endpoint": {"Address": "simple-db-instance.def456.us-west-2.rds.amazonaws.com", "Port": 5432},
1725
+ }
1726
+
1727
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1728
+
1729
+ assert result.name == "simple-db-instance) - db.r5.large"
1730
+ assert result.software_version is None
1731
+ assert result.is_public_facing is True
1732
+ assert result.fqdn == "simple-db-instance.def456.us-west-2.rds.amazonaws.com"
1733
+ assert result.vlan_id == "vpc-87654321"
1734
+ assert result.location == "us-west-2a"
1735
+
1736
+ def test_parse_rds_instance_no_instance_class(self, mock_aws_integration):
1737
+ """Test parsing RDS instance without instance class"""
1738
+
1739
+ db = {
1740
+ "DBInstanceIdentifier": "no-class-db-instance",
1741
+ "Engine": "mariadb",
1742
+ "EngineVersion": "10.6.8",
1743
+ "DBInstanceStatus": "available",
1744
+ "AllocatedStorage": 50,
1745
+ "AvailabilityZone": "us-east-1b",
1746
+ "VpcId": "vpc-11111111",
1747
+ "PubliclyAccessible": False,
1748
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-class-db-instance",
1749
+ "Endpoint": {"Address": "no-class-db-instance.ghi789.us-east-1.rds.amazonaws.com", "Port": 3306},
1750
+ }
1751
+
1752
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1753
+
1754
+ assert result.name == "no-class-db-instance 10.6.8) - "
1755
+ assert result.model is None
1756
+ assert result.software_name == "mariadb"
1757
+ assert result.software_version == "10.6.8"
1758
+
1759
+ def test_parse_rds_instance_inactive_status(self, mock_aws_integration):
1760
+ """Test parsing RDS instance with inactive status"""
1761
+
1762
+ db = {
1763
+ "DBInstanceIdentifier": "inactive-db-instance",
1764
+ "DBInstanceClass": "db.t3.small",
1765
+ "Engine": "mysql",
1766
+ "EngineVersion": "8.0.28",
1767
+ "DBInstanceStatus": "stopped",
1768
+ "AllocatedStorage": 30,
1769
+ "AvailabilityZone": "us-east-1c",
1770
+ "VpcId": "vpc-22222222",
1771
+ "PubliclyAccessible": False,
1772
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:inactive-db-instance",
1773
+ }
1774
+
1775
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1776
+
1777
+ assert result.status == regscale_models.AssetStatus.Inactive
1778
+ assert result.name == "inactive-db-instance 8.0.28) - db.t3.small"
1779
+
1780
+ @pytest.mark.parametrize(
1781
+ "db_data,expected_fqdn,expected_status,expected_name",
1782
+ [
1783
+ (
1784
+ {
1785
+ "DBInstanceIdentifier": "no-endpoint-db-instance",
1786
+ "DBInstanceClass": "db.t3.micro",
1787
+ "Engine": "mysql",
1788
+ "EngineVersion": "8.0.28",
1789
+ "DBInstanceStatus": "creating",
1790
+ "AllocatedStorage": 20,
1791
+ "AvailabilityZone": "us-east-1a",
1792
+ "VpcId": "vpc-33333333",
1793
+ "PubliclyAccessible": False,
1794
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-endpoint-db-instance",
1795
+ },
1796
+ None,
1797
+ regscale_models.AssetStatus.Inactive, # creating status
1798
+ "no-endpoint-db-instance 8.0.28) - db.t3.micro",
1799
+ ),
1800
+ (
1801
+ {
1802
+ "DBInstanceIdentifier": "empty-endpoint-db-instance",
1803
+ "DBInstanceClass": "db.t3.micro",
1804
+ "Engine": "mysql",
1805
+ "EngineVersion": "8.0.28",
1806
+ "DBInstanceStatus": "available",
1807
+ "AllocatedStorage": 20,
1808
+ "AvailabilityZone": "us-east-1a",
1809
+ "VpcId": "vpc-44444444",
1810
+ "PubliclyAccessible": False,
1811
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:empty-endpoint-db-instance",
1812
+ "Endpoint": {},
1813
+ },
1814
+ None,
1815
+ regscale_models.AssetStatus.Active,
1816
+ "empty-endpoint-db-instance 8.0.28) - db.t3.micro",
1817
+ ),
1818
+ ],
1819
+ ids=["no_endpoint", "empty_endpoint"],
1820
+ )
1821
+ def test_parse_rds_instance_endpoint_variations(
1822
+ self, db_data, expected_fqdn, expected_status, expected_name, mock_aws_integration
1823
+ ):
1824
+ """Test parsing RDS instances with various endpoint scenarios."""
1825
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db_data)
1826
+
1827
+ assert result.fqdn == expected_fqdn
1828
+ assert result.status == expected_status
1829
+ assert result.name == expected_name
1830
+
1831
+ @pytest.mark.parametrize(
1832
+ "db_data,expected_vlan_id,expected_location,expected_fqdn",
1833
+ [
1834
+ (
1835
+ {
1836
+ "DBInstanceIdentifier": "no-vpc-db-instance",
1837
+ "DBInstanceClass": "db.t3.micro",
1838
+ "Engine": "mysql",
1839
+ "EngineVersion": "8.0.28",
1840
+ "DBInstanceStatus": "available",
1841
+ "AllocatedStorage": 20,
1842
+ "AvailabilityZone": "us-east-1a",
1843
+ "PubliclyAccessible": False,
1844
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-vpc-db-instance",
1845
+ "Endpoint": {"Address": "no-vpc-db-instance.jkl012.us-east-1.rds.amazonaws.com", "Port": 3306},
1846
+ },
1847
+ None,
1848
+ "us-east-1a",
1849
+ "no-vpc-db-instance.jkl012.us-east-1.rds.amazonaws.com",
1850
+ ),
1851
+ (
1852
+ {
1853
+ "DBInstanceIdentifier": "no-az-db-instance",
1854
+ "DBInstanceClass": "db.t3.micro",
1855
+ "Engine": "mysql",
1856
+ "EngineVersion": "8.0.28",
1857
+ "DBInstanceStatus": "available",
1858
+ "AllocatedStorage": 20,
1859
+ "VpcId": "vpc-55555555",
1860
+ "PubliclyAccessible": False,
1861
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-az-db-instance",
1862
+ "Endpoint": {"Address": "no-az-db-instance.mno345.us-east-1.rds.amazonaws.com", "Port": 3306},
1863
+ },
1864
+ "vpc-55555555",
1865
+ None,
1866
+ "no-az-db-instance.mno345.us-east-1.rds.amazonaws.com",
1867
+ ),
1868
+ ],
1869
+ ids=["no_vpc", "no_availability_zone"],
1870
+ )
1871
+ def test_parse_rds_instance_missing_fields(
1872
+ self, db_data, expected_vlan_id, expected_location, expected_fqdn, mock_aws_integration
1873
+ ):
1874
+ """Test parsing RDS instances with missing fields."""
1875
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db_data)
1876
+
1877
+ assert result.vlan_id == expected_vlan_id
1878
+ assert result.location == expected_location
1879
+ assert result.fqdn == expected_fqdn
1880
+
1881
+ def test_parse_rds_instance_minimal_data(self, mock_aws_integration):
1882
+ """Test parsing RDS instance with minimal data"""
1883
+
1884
+ db = {"DBInstanceIdentifier": "minimal-db-instance"}
1885
+
1886
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1887
+
1888
+ assert result.name == "minimal-db-instance) - "
1889
+ assert result.identifier == "minimal-db-instance"
1890
+ assert result.asset_type == regscale_models.AssetType.VM
1891
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1892
+ assert result.component_type == regscale_models.ComponentType.Hardware
1893
+ assert result.component_names == ["RDS Instances"]
1894
+ assert result.parent_id == mock_aws_integration.plan_id
1895
+ assert result.parent_module == "securityplans"
1896
+ assert result.fqdn is None
1897
+ assert result.vlan_id is None
1898
+ assert result.status == regscale_models.AssetStatus.Inactive # No status provided
1899
+ assert result.location is None
1900
+ assert result.model is None
1901
+ assert result.manufacturer == "AWS"
1902
+ assert result.disk_storage is None
1903
+ assert result.software_name is None
1904
+ assert result.software_version is None
1905
+ assert result.external_id == "minimal-db-instance"
1906
+ assert result.aws_identifier is None
1907
+ assert result.is_public_facing is False
1908
+ assert result.source_data == db
1909
+
1910
+ def test_parse_rds_instance_edge_cases(self, mock_aws_integration):
1911
+ """Test parsing RDS instance with edge cases"""
1912
+
1913
+ db = {
1914
+ "DBInstanceIdentifier": "edge-case-db-instance",
1915
+ "DBInstanceClass": "db.r5.24xlarge",
1916
+ "Engine": "oracle-ee",
1917
+ "EngineVersion": "19.0.0.0.ru-2021-10.rur-2021-10.r1",
1918
+ "DBInstanceStatus": "modifying",
1919
+ "AllocatedStorage": 1000,
1920
+ "AvailabilityZone": "us-east-1d",
1921
+ "VpcId": "vpc-66666666",
1922
+ "PubliclyAccessible": True,
1923
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:edge-case-db-instance",
1924
+ "Endpoint": {"Address": "edge-case-db-instance.pqr678.us-east-1.rds.amazonaws.com", "Port": 1521},
1925
+ }
1926
+
1927
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1928
+
1929
+ assert result.name == "edge-case-db-instance 19.0.0.0.ru-2021-10.rur-2021-10.r1) - db.r5.24xlarge"
1930
+ assert result.software_name == "oracle-ee"
1931
+ assert result.software_version == "19.0.0.0.ru-2021-10.rur-2021-10.r1"
1932
+ assert result.status == regscale_models.AssetStatus.Inactive # modifying status
1933
+ assert result.disk_storage == 1000
1934
+ assert result.is_public_facing is True
1935
+ assert result.fqdn == "edge-case-db-instance.pqr678.us-east-1.rds.amazonaws.com"
1936
+ assert result.vlan_id == "vpc-66666666"
1937
+ assert result.location == "us-east-1d"
1938
+ assert result.model == "db.r5.24xlarge"
1939
+ assert result.asset_type == regscale_models.AssetType.VM
1940
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1941
+ assert result.component_type == regscale_models.ComponentType.Hardware
1942
+ assert result.component_names == ["RDS Instances"]
1943
+ assert result.manufacturer == "AWS"
1944
+ assert result.source_data == db
1945
+
1946
+ def test_parse_dynamodb_table_basic(self, mock_aws_integration):
1947
+ """Test parsing a basic DynamoDB table"""
1948
+
1949
+ table = {
1950
+ "TableName": "test-table",
1951
+ "TableStatus": "ACTIVE",
1952
+ "TableSizeBytes": 1024000,
1953
+ "Region": "us-east-1",
1954
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/test-table",
1955
+ "ItemCount": 1000,
1956
+ "CreationDateTime": "2023-01-01T00:00:00Z",
1957
+ }
1958
+
1959
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
1960
+
1961
+ assert result.name == "test-table (ACTIVE)"
1962
+ assert result.identifier == "test-table"
1963
+ assert result.asset_type == regscale_models.AssetType.Other
1964
+ assert result.asset_category == regscale_models.AssetCategory.Software
1965
+ assert result.component_type == regscale_models.ComponentType.Software
1966
+ assert result.component_names == ["DynamoDB Tables"]
1967
+ assert result.parent_id == mock_aws_integration.plan_id
1968
+ assert result.parent_module == "securityplans"
1969
+ assert result.status == regscale_models.AssetStatus.Active
1970
+ assert result.location == "us-east-1"
1971
+ assert result.disk_storage == 1024000
1972
+ assert result.external_id == "test-table"
1973
+ assert result.aws_identifier == "arn:aws:dynamodb:us-east-1:123456789012:table/test-table"
1974
+ assert result.manufacturer == "AWS"
1975
+ assert result.source_data == table
1976
+
1977
+ def test_parse_dynamodb_table_inactive_status(self, mock_aws_integration):
1978
+ """Test parsing DynamoDB table with inactive status"""
1979
+
1980
+ table = {
1981
+ "TableName": "inactive-table",
1982
+ "TableStatus": "CREATING",
1983
+ "TableSizeBytes": 0,
1984
+ "Region": "us-west-2",
1985
+ "TableArn": "arn:aws:dynamodb:us-west-2:123456789012:table/inactive-table",
1986
+ "ItemCount": 0,
1987
+ "CreationDateTime": "2023-01-01T00:00:00Z",
1988
+ }
1989
+
1990
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
1991
+
1992
+ assert result.name == "inactive-table (CREATING)"
1993
+ assert result.status == regscale_models.AssetStatus.Inactive
1994
+ assert result.location == "us-west-2"
1995
+ assert result.disk_storage == 0
1996
+ assert result.aws_identifier == "arn:aws:dynamodb:us-west-2:123456789012:table/inactive-table"
1997
+
1998
+ def test_parse_dynamodb_table_no_status(self, mock_aws_integration):
1999
+ """Test parsing DynamoDB table without status"""
2000
+
2001
+ table = {
2002
+ "TableName": "no-status-table",
2003
+ "TableSizeBytes": 512000,
2004
+ "Region": "us-east-1",
2005
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/no-status-table",
2006
+ "ItemCount": 500,
2007
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2008
+ }
2009
+
2010
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2011
+
2012
+ assert result.name == "no-status-table"
2013
+ assert result.status == regscale_models.AssetStatus.Inactive # No status provided
2014
+ assert result.location == "us-east-1"
2015
+ assert result.disk_storage == 512000
2016
+ assert result.aws_identifier == "arn:aws:dynamodb:us-east-1:123456789012:table/no-status-table"
2017
+
2018
+ def test_parse_dynamodb_table_empty_status(self, mock_aws_integration):
2019
+ """Test parsing DynamoDB table with empty status"""
2020
+
2021
+ table = {
2022
+ "TableName": "empty-status-table",
2023
+ "TableStatus": "",
2024
+ "TableSizeBytes": 256000,
2025
+ "Region": "us-east-1",
2026
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/empty-status-table",
2027
+ "ItemCount": 250,
2028
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2029
+ }
2030
+
2031
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2032
+
2033
+ assert result.name == "empty-status-table" # Empty status is not included in name
2034
+ assert result.status == regscale_models.AssetStatus.Inactive # Empty status is not ACTIVE
2035
+ assert result.location == "us-east-1"
2036
+ assert result.disk_storage == 256000
2037
+
2038
+ def test_parse_dynamodb_table_no_region(self, mock_aws_integration):
2039
+ """Test parsing DynamoDB table without region"""
2040
+
2041
+ table = {
2042
+ "TableName": "no-region-table",
2043
+ "TableStatus": "ACTIVE",
2044
+ "TableSizeBytes": 1024000,
2045
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/no-region-table",
2046
+ "ItemCount": 1000,
2047
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2048
+ }
2049
+
2050
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2051
+
2052
+ assert result.name == "no-region-table (ACTIVE)"
2053
+ assert result.location is None
2054
+ assert result.status == regscale_models.AssetStatus.Active
2055
+ assert result.disk_storage == 1024000
2056
+
2057
+ def test_parse_dynamodb_table_no_size(self, mock_aws_integration):
2058
+ """Test parsing DynamoDB table without size"""
2059
+
2060
+ table = {
2061
+ "TableName": "no-size-table",
2062
+ "TableStatus": "ACTIVE",
2063
+ "Region": "us-east-1",
2064
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/no-size-table",
2065
+ "ItemCount": 1000,
2066
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2067
+ }
2068
+
2069
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2070
+
2071
+ assert result.name == "no-size-table (ACTIVE)"
2072
+ assert result.disk_storage is None
2073
+ assert result.status == regscale_models.AssetStatus.Active
2074
+ assert result.location == "us-east-1"
2075
+
2076
+ def test_parse_dynamodb_table_no_arn(self, mock_aws_integration):
2077
+ """Test parsing DynamoDB table without ARN"""
2078
+
2079
+ table = {
2080
+ "TableName": "no-arn-table",
2081
+ "TableStatus": "ACTIVE",
2082
+ "TableSizeBytes": 1024000,
2083
+ "Region": "us-east-1",
2084
+ "ItemCount": 1000,
2085
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2086
+ }
2087
+
2088
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2089
+
2090
+ assert result.name == "no-arn-table (ACTIVE)"
2091
+ assert result.aws_identifier is None
2092
+ assert result.status == regscale_models.AssetStatus.Active
2093
+ assert result.location == "us-east-1"
2094
+ assert result.disk_storage == 1024000
2095
+
2096
+ def test_parse_dynamodb_table_minimal_data(self, mock_aws_integration):
2097
+ """Test parsing DynamoDB table with minimal data"""
2098
+
2099
+ table = {"TableName": "minimal-table"}
2100
+
2101
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2102
+
2103
+ assert result.name == "minimal-table"
2104
+ assert result.identifier == "minimal-table"
2105
+ assert result.asset_type == regscale_models.AssetType.Other
2106
+ assert result.asset_category == regscale_models.AssetCategory.Software
2107
+ assert result.component_type == regscale_models.ComponentType.Software
2108
+ assert result.component_names == ["DynamoDB Tables"]
2109
+ assert result.parent_id == mock_aws_integration.plan_id
2110
+ assert result.parent_module == "securityplans"
2111
+ assert result.status == regscale_models.AssetStatus.Inactive # No status provided
2112
+ assert result.location is None
2113
+ assert result.disk_storage is None
2114
+ assert result.external_id == "minimal-table"
2115
+ assert result.aws_identifier is None
2116
+ assert result.manufacturer == "AWS"
2117
+ assert result.source_data == table
2118
+
2119
+ def test_parse_dynamodb_table_edge_cases(self, mock_aws_integration):
2120
+ """Test parsing DynamoDB table with edge cases"""
2121
+
2122
+ table = {
2123
+ "TableName": "edge-case-table",
2124
+ "TableStatus": "UPDATING",
2125
+ "TableSizeBytes": 0,
2126
+ "Region": "us-east-1",
2127
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/edge-case-table",
2128
+ "ItemCount": 0,
2129
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2130
+ }
2131
+
2132
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2133
+
2134
+ assert result.name == "edge-case-table (UPDATING)"
2135
+ assert result.status == regscale_models.AssetStatus.Inactive # UPDATING is not ACTIVE
2136
+ assert result.disk_storage == 0
2137
+ assert result.location == "us-east-1"
2138
+ assert result.aws_identifier == "arn:aws:dynamodb:us-east-1:123456789012:table/edge-case-table"
2139
+ assert result.asset_type == regscale_models.AssetType.Other
2140
+ assert result.asset_category == regscale_models.AssetCategory.Software
2141
+ assert result.component_type == regscale_models.ComponentType.Software
2142
+ assert result.component_names == ["DynamoDB Tables"]
2143
+ assert result.manufacturer == "AWS"
2144
+ assert result.source_data == table
2145
+
2146
+ def test_parse_dynamodb_table_large_size(self, mock_aws_integration):
2147
+ """Test parsing DynamoDB table with large size"""
2148
+
2149
+ table = {
2150
+ "TableName": "large-table",
2151
+ "TableStatus": "ACTIVE",
2152
+ "TableSizeBytes": 1073741824, # 1 GB
2153
+ "Region": "us-west-2",
2154
+ "TableArn": "arn:aws:dynamodb:us-west-2:123456789012:table/large-table",
2155
+ "ItemCount": 1000000,
2156
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2157
+ }
2158
+
2159
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2160
+
2161
+ assert result.name == "large-table (ACTIVE)"
2162
+ assert result.disk_storage == 1073741824
2163
+ assert result.status == regscale_models.AssetStatus.Active
2164
+ assert result.location == "us-west-2"
2165
+ assert result.aws_identifier == "arn:aws:dynamodb:us-west-2:123456789012:table/large-table"
2166
+
2167
+ def test_parse_vpc_basic(self, mock_aws_integration):
2168
+ """Test parsing a basic VPC"""
2169
+
2170
+ vpc = {
2171
+ "VpcId": "vpc-12345678",
2172
+ "CidrBlock": "10.0.0.0/16",
2173
+ "State": "available",
2174
+ "Region": "us-east-1",
2175
+ "Tags": [{"Key": "Name", "Value": "Production VPC"}, {"Key": "Environment", "Value": "Production"}],
2176
+ }
2177
+
2178
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2179
+
2180
+ assert result.name == "Production VPC"
2181
+ assert result.identifier == "vpc-12345678"
2182
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2183
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2184
+ assert result.component_type == regscale_models.ComponentType.Hardware
2185
+ assert result.component_names == ["VPCs"]
2186
+ assert result.parent_id == mock_aws_integration.plan_id
2187
+ assert result.parent_module == "securityplans"
2188
+ assert result.status == regscale_models.AssetStatus.Active
2189
+ assert result.location == "us-east-1"
2190
+ assert result.vlan_id == "vpc-12345678"
2191
+ assert result.external_id == "vpc-12345678"
2192
+ assert result.aws_identifier == "vpc-12345678"
2193
+ assert result.manufacturer == "AWS"
2194
+ assert result.notes == "CIDR: 10.0.0.0/16"
2195
+ assert result.source_data == vpc
2196
+
2197
+ def test_parse_vpc_no_name_tag(self, mock_aws_integration):
2198
+ """Test parsing VPC without Name tag"""
2199
+
2200
+ vpc = {
2201
+ "VpcId": "vpc-87654321",
2202
+ "CidrBlock": "172.16.0.0/16",
2203
+ "State": "available",
2204
+ "Region": "us-west-2",
2205
+ "Tags": [{"Key": "Environment", "Value": "Development"}, {"Key": "Project", "Value": "TestProject"}],
2206
+ }
2207
+
2208
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2209
+
2210
+ assert result.name == "vpc-87654321"
2211
+ assert result.identifier == "vpc-87654321"
2212
+ assert result.status == regscale_models.AssetStatus.Active
2213
+ assert result.location == "us-west-2"
2214
+ assert result.notes == "CIDR: 172.16.0.0/16"
2215
+
2216
+ def test_parse_vpc_no_tags(self, mock_aws_integration):
2217
+ """Test parsing VPC with no tags"""
2218
+
2219
+ vpc = {"VpcId": "vpc-notags123", "CidrBlock": "192.168.0.0/16", "State": "available", "Region": "us-east-1"}
2220
+
2221
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2222
+
2223
+ assert result.name == "vpc-notags123"
2224
+ assert result.identifier == "vpc-notags123"
2225
+ assert result.status == regscale_models.AssetStatus.Active
2226
+ assert result.location == "us-east-1"
2227
+ assert result.notes == "CIDR: 192.168.0.0/16"
2228
+
2229
+ def test_parse_vpc_default_vpc(self, mock_aws_integration):
2230
+ """Test parsing a default VPC"""
2231
+
2232
+ vpc = {
2233
+ "VpcId": "vpc-default123",
2234
+ "CidrBlock": "10.0.0.0/16",
2235
+ "State": "available",
2236
+ "Region": "us-east-1",
2237
+ "IsDefault": True,
2238
+ "Tags": [{"Key": "Name", "Value": "Default VPC"}],
2239
+ }
2240
+
2241
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2242
+
2243
+ assert result.name == "Default VPC"
2244
+ assert result.identifier == "vpc-default123"
2245
+ assert result.status == regscale_models.AssetStatus.Active
2246
+ assert result.location == "us-east-1"
2247
+ assert result.notes == "CIDR: 10.0.0.0/16" # IsDefault logic is overwritten by CIDR notes
2248
+
2249
+ def test_parse_vpc_inactive_state(self, mock_aws_integration):
2250
+ """Test parsing VPC with inactive state"""
2251
+
2252
+ vpc = {
2253
+ "VpcId": "vpc-inactive123",
2254
+ "CidrBlock": "10.0.0.0/16",
2255
+ "State": "pending",
2256
+ "Region": "us-east-1",
2257
+ "Tags": [{"Key": "Name", "Value": "Inactive VPC"}],
2258
+ }
2259
+
2260
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2261
+
2262
+ assert result.name == "Inactive VPC"
2263
+ assert result.status == regscale_models.AssetStatus.Inactive
2264
+ assert result.location == "us-east-1"
2265
+ assert result.notes == "CIDR: 10.0.0.0/16"
2266
+
2267
+ def test_parse_vpc_no_cidr(self, mock_aws_integration):
2268
+ """Test parsing VPC without CIDR block"""
2269
+
2270
+ vpc = {
2271
+ "VpcId": "vpc-nocidr123",
2272
+ "State": "available",
2273
+ "Region": "us-east-1",
2274
+ "Tags": [{"Key": "Name", "Value": "No CIDR VPC"}],
2275
+ }
2276
+
2277
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2278
+
2279
+ assert result.name == "No CIDR VPC"
2280
+ assert result.status == regscale_models.AssetStatus.Active
2281
+ assert result.location == "us-east-1"
2282
+ assert result.notes == "CIDR: None"
2283
+
2284
+ def test_parse_vpc_no_region(self, mock_aws_integration):
2285
+ """Test parsing VPC without region"""
2286
+
2287
+ vpc = {
2288
+ "VpcId": "vpc-noregion123",
2289
+ "CidrBlock": "10.0.0.0/16",
2290
+ "State": "available",
2291
+ "Tags": [{"Key": "Name", "Value": "No Region VPC"}],
2292
+ }
2293
+
2294
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2295
+
2296
+ assert result.name == "No Region VPC"
2297
+ assert result.location is None
2298
+ assert result.status == regscale_models.AssetStatus.Active
2299
+ assert result.notes == "CIDR: 10.0.0.0/16"
2300
+
2301
+ def test_parse_vpc_no_vpc_id(self, mock_aws_integration):
2302
+ """Test parsing VPC without VPC ID"""
2303
+
2304
+ vpc = {
2305
+ "CidrBlock": "10.0.0.0/16",
2306
+ "State": "available",
2307
+ "Region": "us-east-1",
2308
+ "Tags": [{"Key": "Name", "Value": "No VPC ID"}],
2309
+ }
2310
+
2311
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2312
+
2313
+ assert result.name == "No VPC ID"
2314
+ assert result.identifier == ""
2315
+ assert result.vlan_id is None
2316
+ assert result.external_id is None
2317
+ assert result.aws_identifier is None
2318
+ assert result.status == regscale_models.AssetStatus.Active
2319
+ assert result.location == "us-east-1"
2320
+ assert result.notes == "CIDR: 10.0.0.0/16"
2321
+
2322
+ def test_parse_vpc_minimal_data(self, mock_aws_integration):
2323
+ """Test parsing VPC with minimal data"""
2324
+
2325
+ vpc = {}
2326
+
2327
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2328
+
2329
+ assert result.name == ""
2330
+ assert result.identifier == ""
2331
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2332
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2333
+ assert result.component_type == regscale_models.ComponentType.Hardware
2334
+ assert result.component_names == ["VPCs"]
2335
+ assert result.parent_id == mock_aws_integration.plan_id
2336
+ assert result.parent_module == "securityplans"
2337
+ assert result.status == regscale_models.AssetStatus.Inactive # No state provided
2338
+ assert result.location is None
2339
+ assert result.vlan_id is None
2340
+ assert result.external_id is None
2341
+ assert result.aws_identifier is None
2342
+ assert result.manufacturer == "AWS"
2343
+ assert result.notes == "CIDR: None"
2344
+ assert result.source_data == vpc
2345
+
2346
+ def test_parse_vpc_edge_cases(self, mock_aws_integration):
2347
+ """Test parsing VPC with edge cases"""
2348
+
2349
+ vpc = {
2350
+ "VpcId": "vpc-edge123",
2351
+ "CidrBlock": "10.0.0.0/8",
2352
+ "State": "available",
2353
+ "Region": "us-east-1",
2354
+ "IsDefault": False,
2355
+ "Tags": [
2356
+ {"Key": "Name", "Value": "Edge Case VPC"},
2357
+ {"Key": "Name", "Value": "Duplicate Name"}, # Duplicate Name tag
2358
+ {"Key": "Description", "Value": "Test VPC"},
2359
+ ],
2360
+ }
2361
+
2362
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2363
+
2364
+ assert result.name == "Edge Case VPC" # First Name tag is used
2365
+ assert result.identifier == "vpc-edge123"
2366
+ assert result.status == regscale_models.AssetStatus.Active
2367
+ assert result.location == "us-east-1"
2368
+ assert result.vlan_id == "vpc-edge123"
2369
+ assert result.external_id == "vpc-edge123"
2370
+ assert result.aws_identifier == "vpc-edge123"
2371
+ assert result.notes == "CIDR: 10.0.0.0/8" # No "Default VPC" prefix since IsDefault is False
2372
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2373
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2374
+ assert result.component_type == regscale_models.ComponentType.Hardware
2375
+ assert result.component_names == ["VPCs"]
2376
+ assert result.manufacturer == "AWS"
2377
+ assert result.source_data == vpc
2378
+
2379
+ def test_parse_vpc_empty_tags(self, mock_aws_integration):
2380
+ """Test parsing VPC with empty tags list"""
2381
+
2382
+ vpc = {
2383
+ "VpcId": "vpc-emptytags123",
2384
+ "CidrBlock": "10.0.0.0/16",
2385
+ "State": "available",
2386
+ "Region": "us-east-1",
2387
+ "Tags": [],
2388
+ }
2389
+
2390
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2391
+
2392
+ assert result.name == "vpc-emptytags123" # Falls back to VPC ID
2393
+ assert result.identifier == "vpc-emptytags123"
2394
+ assert result.status == regscale_models.AssetStatus.Active
2395
+ assert result.location == "us-east-1"
2396
+ assert result.notes == "CIDR: 10.0.0.0/16"
2397
+
2398
+ def test_parse_load_balancer_basic(self, mock_aws_integration):
2399
+ """Test parsing a basic load balancer"""
2400
+
2401
+ lb = {
2402
+ "LoadBalancerName": "my-load-balancer",
2403
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/1234567890",
2404
+ "DNSName": "my-load-balancer-1234567890.us-east-1.elb.amazonaws.com",
2405
+ "VpcId": "vpc-12345678",
2406
+ "State": "active",
2407
+ "Region": "us-east-1",
2408
+ "Scheme": "internet-facing",
2409
+ "Listeners": [{"Port": 80, "Protocol": "HTTP"}, {"Port": 443, "Protocol": "HTTPS"}],
2410
+ }
2411
+
2412
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2413
+
2414
+ assert result.name == "my-load-balancer"
2415
+ assert result.identifier == "my-load-balancer"
2416
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2417
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2418
+ assert result.component_type == regscale_models.ComponentType.Hardware
2419
+ assert result.component_names == ["Load Balancers"]
2420
+ assert result.parent_id == mock_aws_integration.plan_id
2421
+ assert result.parent_module == "securityplans"
2422
+ assert result.fqdn == "my-load-balancer-1234567890.us-east-1.elb.amazonaws.com"
2423
+ assert result.vlan_id == "vpc-12345678"
2424
+ assert result.status == regscale_models.AssetStatus.Active
2425
+ assert result.location == "us-east-1"
2426
+ assert result.external_id == "my-load-balancer"
2427
+ assert (
2428
+ result.aws_identifier
2429
+ == "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/1234567890"
2430
+ )
2431
+ assert result.manufacturer == "AWS"
2432
+ assert result.is_public_facing is True
2433
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2434
+ assert result.source_data == lb
2435
+ assert result.ports_and_protocols == [{"port": 80, "protocol": "HTTP"}, {"port": 443, "protocol": "HTTPS"}]
2436
+
2437
+ def test_parse_load_balancer_internal(self, mock_aws_integration):
2438
+ """Test parsing an internal load balancer"""
2439
+
2440
+ lb = {
2441
+ "LoadBalancerName": "internal-lb",
2442
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/internal-lb/0987654321",
2443
+ "DNSName": "internal-lb-0987654321.us-west-2.elb.amazonaws.com",
2444
+ "VpcId": "vpc-87654321",
2445
+ "State": "active",
2446
+ "Region": "us-west-2",
2447
+ "Scheme": "internal",
2448
+ "Listeners": [{"Port": 8080, "Protocol": "HTTP"}],
2449
+ }
2450
+
2451
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2452
+
2453
+ assert result.name == "internal-lb"
2454
+ assert result.identifier == "internal-lb"
2455
+ assert result.fqdn == "internal-lb-0987654321.us-west-2.elb.amazonaws.com"
2456
+ assert result.vlan_id == "vpc-87654321"
2457
+ assert result.status == regscale_models.AssetStatus.Active
2458
+ assert result.location == "us-west-2"
2459
+ assert result.is_public_facing is False
2460
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2461
+ assert result.ports_and_protocols == [{"port": 8080, "protocol": "HTTP"}]
2462
+
2463
+ def test_parse_load_balancer_inactive_state(self, mock_aws_integration):
2464
+ """Test parsing load balancer with inactive state"""
2465
+
2466
+ lb = {
2467
+ "LoadBalancerName": "inactive-lb",
2468
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/inactive-lb/1111111111",
2469
+ "DNSName": "inactive-lb-1111111111.us-east-1.elb.amazonaws.com",
2470
+ "VpcId": "vpc-11111111",
2471
+ "State": "provisioning",
2472
+ "Region": "us-east-1",
2473
+ "Scheme": "internet-facing",
2474
+ "Listeners": [],
2475
+ }
2476
+
2477
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2478
+
2479
+ assert result.name == "inactive-lb"
2480
+ assert result.status == regscale_models.AssetStatus.Inactive
2481
+ assert result.location == "us-east-1"
2482
+ assert result.is_public_facing is True
2483
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2484
+ assert result.ports_and_protocols == []
2485
+
2486
+ def test_parse_load_balancer_no_scheme(self, mock_aws_integration):
2487
+ """Test parsing load balancer without scheme"""
2488
+
2489
+ lb = {
2490
+ "LoadBalancerName": "no-scheme-lb",
2491
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-scheme-lb/2222222222",
2492
+ "DNSName": "no-scheme-lb-2222222222.us-east-1.elb.amazonaws.com",
2493
+ "VpcId": "vpc-22222222",
2494
+ "State": "active",
2495
+ "Region": "us-east-1",
2496
+ "Listeners": [{"Port": 80, "Protocol": "HTTP"}],
2497
+ }
2498
+
2499
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2500
+
2501
+ assert result.name == "no-scheme-lb"
2502
+ assert result.status == regscale_models.AssetStatus.Active
2503
+ assert result.location == "us-east-1"
2504
+ assert result.is_public_facing is False # No scheme means not public-facing
2505
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2506
+ assert result.ports_and_protocols == [{"port": 80, "protocol": "HTTP"}]
2507
+
2508
+ def test_parse_load_balancer_no_name(self, mock_aws_integration):
2509
+ """Test parsing load balancer without name"""
2510
+
2511
+ lb = {
2512
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/unnamed-lb/3333333333",
2513
+ "DNSName": "unnamed-lb-3333333333.us-east-1.elb.amazonaws.com",
2514
+ "VpcId": "vpc-33333333",
2515
+ "State": "active",
2516
+ "Region": "us-east-1",
2517
+ "Scheme": "internet-facing",
2518
+ "Listeners": [],
2519
+ }
2520
+
2521
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2522
+
2523
+ assert result.name == ""
2524
+ assert result.identifier == ""
2525
+ assert result.external_id is None
2526
+ assert result.status == regscale_models.AssetStatus.Active
2527
+ assert result.location == "us-east-1"
2528
+ assert result.is_public_facing is True
2529
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2530
+ assert result.ports_and_protocols == []
2531
+
2532
+ def test_parse_load_balancer_no_dns(self, mock_aws_integration):
2533
+ """Test parsing load balancer without DNS name"""
2534
+
2535
+ lb = {
2536
+ "LoadBalancerName": "no-dns-lb",
2537
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-dns-lb/4444444444",
2538
+ "VpcId": "vpc-44444444",
2539
+ "State": "active",
2540
+ "Region": "us-east-1",
2541
+ "Scheme": "internal",
2542
+ "Listeners": [{"Port": 8080, "Protocol": "HTTP"}],
2543
+ }
2544
+
2545
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2546
+
2547
+ assert result.name == "no-dns-lb"
2548
+ assert result.fqdn is None
2549
+ assert result.status == regscale_models.AssetStatus.Active
2550
+ assert result.location == "us-east-1"
2551
+ assert result.is_public_facing is False
2552
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2553
+ assert result.ports_and_protocols == [{"port": 8080, "protocol": "HTTP"}]
2554
+
2555
+ def test_parse_load_balancer_no_vpc(self, mock_aws_integration):
2556
+ """Test parsing load balancer without VPC ID"""
2557
+
2558
+ lb = {
2559
+ "LoadBalancerName": "no-vpc-lb",
2560
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-vpc-lb/5555555555",
2561
+ "DNSName": "no-vpc-lb-5555555555.us-east-1.elb.amazonaws.com",
2562
+ "State": "active",
2563
+ "Region": "us-east-1",
2564
+ "Scheme": "internet-facing",
2565
+ "Listeners": [{"Port": 80, "Protocol": "HTTP"}],
2566
+ }
2567
+
2568
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2569
+
2570
+ assert result.name == "no-vpc-lb"
2571
+ assert result.vlan_id is None
2572
+ assert result.status == regscale_models.AssetStatus.Active
2573
+ assert result.location == "us-east-1"
2574
+ assert result.is_public_facing is True
2575
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2576
+ assert result.ports_and_protocols == [{"port": 80, "protocol": "HTTP"}]
2577
+
2578
+ def test_parse_load_balancer_no_region(self, mock_aws_integration):
2579
+ """Test parsing load balancer without region"""
2580
+
2581
+ lb = {
2582
+ "LoadBalancerName": "no-region-lb",
2583
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-region-lb/6666666666",
2584
+ "DNSName": "no-region-lb-6666666666.us-east-1.elb.amazonaws.com",
2585
+ "VpcId": "vpc-66666666",
2586
+ "State": "active",
2587
+ "Scheme": "internet-facing",
2588
+ "Listeners": [{"Port": 443, "Protocol": "HTTPS"}],
2589
+ }
2590
+
2591
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2592
+
2593
+ assert result.name == "no-region-lb"
2594
+ assert result.location is None
2595
+ assert result.status == regscale_models.AssetStatus.Active
2596
+ assert result.is_public_facing is True
2597
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2598
+ assert result.ports_and_protocols == [{"port": 443, "protocol": "HTTPS"}]
2599
+
2600
+ def test_parse_load_balancer_no_listeners(self, mock_aws_integration):
2601
+ """Test parsing load balancer without listeners"""
2602
+
2603
+ lb = {
2604
+ "LoadBalancerName": "no-listeners-lb",
2605
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-listeners-lb/7777777777",
2606
+ "DNSName": "no-listeners-lb-7777777777.us-east-1.elb.amazonaws.com",
2607
+ "VpcId": "vpc-77777777",
2608
+ "State": "active",
2609
+ "Region": "us-east-1",
2610
+ "Scheme": "internal",
2611
+ }
2612
+
2613
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2614
+
2615
+ assert result.name == "no-listeners-lb"
2616
+ assert result.status == regscale_models.AssetStatus.Active
2617
+ assert result.location == "us-east-1"
2618
+ assert result.is_public_facing is False
2619
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2620
+ assert result.ports_and_protocols == []
2621
+
2622
+ def test_parse_load_balancer_minimal_data(self, mock_aws_integration):
2623
+ """Test parsing load balancer with minimal data"""
2624
+
2625
+ lb = {}
2626
+
2627
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2628
+
2629
+ assert result.name == ""
2630
+ assert result.identifier == ""
2631
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2632
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2633
+ assert result.component_type == regscale_models.ComponentType.Hardware
2634
+ assert result.component_names == ["Load Balancers"]
2635
+ assert result.parent_id == mock_aws_integration.plan_id
2636
+ assert result.parent_module == "securityplans"
2637
+ assert result.fqdn is None
2638
+ assert result.vlan_id is None
2639
+ assert result.status == regscale_models.AssetStatus.Inactive # No state provided
2640
+ assert result.location is None
2641
+ assert result.external_id is None
2642
+ assert result.aws_identifier is None
2643
+ assert result.manufacturer == "AWS"
2644
+ assert result.is_public_facing is False
2645
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2646
+ assert result.source_data == lb
2647
+ assert result.ports_and_protocols == []
2648
+
2649
+ def test_parse_load_balancer_edge_cases(self, mock_aws_integration):
2650
+ """Test parsing load balancer with edge cases"""
2651
+
2652
+ lb = {
2653
+ "LoadBalancerName": "edge-case-lb",
2654
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/edge-case-lb/8888888888",
2655
+ "DNSName": "edge-case-lb-8888888888.us-east-1.elb.amazonaws.com",
2656
+ "VpcId": "vpc-88888888",
2657
+ "State": "active",
2658
+ "Region": "us-east-1",
2659
+ "Scheme": "internet-facing",
2660
+ "Listeners": [
2661
+ {"Port": 80, "Protocol": "HTTP"},
2662
+ {"Port": 443, "Protocol": "HTTPS"},
2663
+ {"Port": 8080, "Protocol": "HTTP"},
2664
+ {"Port": 8443, "Protocol": "HTTPS"},
2665
+ ],
2666
+ }
2667
+
2668
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2669
+
2670
+ assert result.name == "edge-case-lb"
2671
+ assert result.identifier == "edge-case-lb"
2672
+ assert result.fqdn == "edge-case-lb-8888888888.us-east-1.elb.amazonaws.com"
2673
+ assert result.vlan_id == "vpc-88888888"
2674
+ assert result.status == regscale_models.AssetStatus.Active
2675
+ assert result.location == "us-east-1"
2676
+ assert result.external_id == "edge-case-lb"
2677
+ assert (
2678
+ result.aws_identifier
2679
+ == "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/edge-case-lb/8888888888"
2680
+ )
2681
+ assert result.manufacturer == "AWS"
2682
+ assert result.is_public_facing is True
2683
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2684
+ assert result.ports_and_protocols == [
2685
+ {"port": 80, "protocol": "HTTP"},
2686
+ {"port": 443, "protocol": "HTTPS"},
2687
+ {"port": 8080, "protocol": "HTTP"},
2688
+ {"port": 8443, "protocol": "HTTPS"},
2689
+ ]
2690
+ assert result.source_data == lb
2691
+
2692
+ def test_parse_ecr_repository_basic(self, mock_aws_integration):
2693
+ """Test parsing a basic ECR repository"""
2694
+
2695
+ repo = {
2696
+ "RepositoryName": "my-app-repo",
2697
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/my-app-repo",
2698
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app-repo",
2699
+ "Region": "us-east-1",
2700
+ "ImageTagMutability": "MUTABLE",
2701
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2702
+ }
2703
+
2704
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2705
+
2706
+ assert result.name == "my-app-repo"
2707
+ assert result.identifier == "my-app-repo"
2708
+ assert result.asset_type == regscale_models.AssetType.Other
2709
+ assert result.asset_category == regscale_models.AssetCategory.Software
2710
+ assert result.component_type == regscale_models.ComponentType.Software
2711
+ assert result.component_names == ["ECR Repositories"]
2712
+ assert result.parent_id == mock_aws_integration.plan_id
2713
+ assert result.parent_module == "securityplans"
2714
+ assert result.status == regscale_models.AssetStatus.Active
2715
+ assert result.location == "us-east-1"
2716
+ assert result.external_id == "my-app-repo"
2717
+ assert result.aws_identifier == "arn:aws:ecr:us-east-1:123456789012:repository/my-app-repo"
2718
+ assert result.uri == "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app-repo"
2719
+ assert result.manufacturer == "AWS"
2720
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2721
+ assert result.source_data == repo
2722
+
2723
+ def test_parse_ecr_repository_immutable_tags(self, mock_aws_integration):
2724
+ """Test parsing ECR repository with immutable tags"""
2725
+
2726
+ repo = {
2727
+ "RepositoryName": "immutable-repo",
2728
+ "RepositoryArn": "arn:aws:ecr:us-west-2:123456789012:repository/immutable-repo",
2729
+ "RepositoryUri": "123456789012.dkr.ecr.us-west-2.amazonaws.com/immutable-repo",
2730
+ "Region": "us-west-2",
2731
+ "ImageTagMutability": "IMMUTABLE",
2732
+ "ImageScanningConfiguration": {"ScanOnPush": False},
2733
+ }
2734
+
2735
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2736
+
2737
+ assert result.name == "immutable-repo"
2738
+ assert result.identifier == "immutable-repo"
2739
+ assert result.status == regscale_models.AssetStatus.Active
2740
+ assert result.location == "us-west-2"
2741
+ assert result.aws_identifier == "arn:aws:ecr:us-west-2:123456789012:repository/immutable-repo"
2742
+ assert result.uri == "123456789012.dkr.ecr.us-west-2.amazonaws.com/immutable-repo"
2743
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2744
+
2745
+ def test_parse_ecr_repository_scan_on_push_enabled(self, mock_aws_integration):
2746
+ """Test parsing ECR repository with scan on push enabled"""
2747
+
2748
+ repo = {
2749
+ "RepositoryName": "scan-enabled-repo",
2750
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/scan-enabled-repo",
2751
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/scan-enabled-repo",
2752
+ "Region": "us-east-1",
2753
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2754
+ }
2755
+
2756
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2757
+
2758
+ assert result.name == "scan-enabled-repo"
2759
+ assert result.identifier == "scan-enabled-repo"
2760
+ assert result.status == regscale_models.AssetStatus.Active
2761
+ assert result.location == "us-east-1"
2762
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2763
+
2764
+ def test_parse_ecr_repository_scan_on_push_disabled(self, mock_aws_integration):
2765
+ """Test parsing ECR repository with scan on push disabled"""
2766
+
2767
+ repo = {
2768
+ "RepositoryName": "scan-disabled-repo",
2769
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/scan-disabled-repo",
2770
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/scan-disabled-repo",
2771
+ "Region": "us-east-1",
2772
+ "ImageScanningConfiguration": {"ScanOnPush": False},
2773
+ }
2774
+
2775
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2776
+
2777
+ assert result.name == "scan-disabled-repo"
2778
+ assert result.identifier == "scan-disabled-repo"
2779
+ assert result.status == regscale_models.AssetStatus.Active
2780
+ assert result.location == "us-east-1"
2781
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2782
+
2783
+ def test_parse_ecr_repository_no_image_tag_mutability(self, mock_aws_integration):
2784
+ """Test parsing ECR repository without image tag mutability"""
2785
+
2786
+ repo = {
2787
+ "RepositoryName": "no-mutability-repo",
2788
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-mutability-repo",
2789
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/no-mutability-repo",
2790
+ "Region": "us-east-1",
2791
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2792
+ }
2793
+
2794
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2795
+
2796
+ assert result.name == "no-mutability-repo"
2797
+ assert result.identifier == "no-mutability-repo"
2798
+ assert result.status == regscale_models.AssetStatus.Active
2799
+ assert result.location == "us-east-1"
2800
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2801
+
2802
+ def test_parse_ecr_repository_no_scanning_config(self, mock_aws_integration):
2803
+ """Test parsing ECR repository without scanning configuration"""
2804
+
2805
+ repo = {
2806
+ "RepositoryName": "no-scanning-repo",
2807
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-scanning-repo",
2808
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/no-scanning-repo",
2809
+ "Region": "us-east-1",
2810
+ "ImageTagMutability": "MUTABLE",
2811
+ }
2812
+
2813
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2814
+
2815
+ assert result.name == "no-scanning-repo"
2816
+ assert result.identifier == "no-scanning-repo"
2817
+ assert result.status == regscale_models.AssetStatus.Active
2818
+ assert result.location == "us-east-1"
2819
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2820
+
2821
+ def test_parse_ecr_repository_no_name(self, mock_aws_integration):
2822
+ """Test parsing ECR repository without name"""
2823
+
2824
+ repo = {
2825
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/unnamed-repo",
2826
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/unnamed-repo",
2827
+ "Region": "us-east-1",
2828
+ "ImageTagMutability": "MUTABLE",
2829
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2830
+ }
2831
+
2832
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2833
+
2834
+ assert result.name == ""
2835
+ assert result.identifier == ""
2836
+ assert result.external_id is None
2837
+ assert result.status == regscale_models.AssetStatus.Active
2838
+ assert result.location == "us-east-1"
2839
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2840
+
2841
+ def test_parse_ecr_repository_no_uri(self, mock_aws_integration):
2842
+ """Test parsing ECR repository without URI"""
2843
+
2844
+ repo = {
2845
+ "RepositoryName": "no-uri-repo",
2846
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-uri-repo",
2847
+ "Region": "us-east-1",
2848
+ "ImageTagMutability": "MUTABLE",
2849
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2850
+ }
2851
+
2852
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2853
+
2854
+ assert result.name == "no-uri-repo"
2855
+ assert result.uri is None
2856
+ assert result.status == regscale_models.AssetStatus.Active
2857
+ assert result.location == "us-east-1"
2858
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2859
+
2860
+ def test_parse_ecr_repository_no_region(self, mock_aws_integration):
2861
+ """Test parsing ECR repository without region"""
2862
+
2863
+ repo = {
2864
+ "RepositoryName": "no-region-repo",
2865
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-region-repo",
2866
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/no-region-repo",
2867
+ "ImageTagMutability": "MUTABLE",
2868
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2869
+ }
2870
+
2871
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2872
+
2873
+ assert result.name == "no-region-repo"
2874
+ assert result.location is None
2875
+ assert result.status == regscale_models.AssetStatus.Active
2876
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2877
+
2878
+ def test_parse_ecr_repository_minimal_data(self, mock_aws_integration):
2879
+ """Test parsing ECR repository with minimal data"""
2880
+
2881
+ repo = {}
2882
+
2883
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2884
+
2885
+ assert result.name == ""
2886
+ assert result.identifier == ""
2887
+ assert result.asset_type == regscale_models.AssetType.Other
2888
+ assert result.asset_category == regscale_models.AssetCategory.Software
2889
+ assert result.component_type == regscale_models.ComponentType.Software
2890
+ assert result.component_names == ["ECR Repositories"]
2891
+ assert result.parent_id == mock_aws_integration.plan_id
2892
+ assert result.parent_module == "securityplans"
2893
+ assert result.status == regscale_models.AssetStatus.Active
2894
+ assert result.location is None
2895
+ assert result.external_id is None
2896
+ assert result.aws_identifier is None
2897
+ assert result.uri is None
2898
+ assert result.manufacturer == "AWS"
2899
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2900
+ assert result.source_data == repo
2901
+
2902
+ def test_parse_ecr_repository_edge_cases(self, mock_aws_integration):
2903
+ """Test parsing ECR repository with edge cases"""
2904
+
2905
+ repo = {
2906
+ "RepositoryName": "edge-case-repo",
2907
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/edge-case-repo",
2908
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/edge-case-repo",
2909
+ "Region": "us-east-1",
2910
+ "ImageTagMutability": "MUTABLE",
2911
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2912
+ }
2913
+
2914
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2915
+
2916
+ assert result.name == "edge-case-repo"
2917
+ assert result.identifier == "edge-case-repo"
2918
+ assert result.status == regscale_models.AssetStatus.Active
2919
+ assert result.location == "us-east-1"
2920
+ assert result.external_id == "edge-case-repo"
2921
+ assert result.aws_identifier == "arn:aws:ecr:us-east-1:123456789012:repository/edge-case-repo"
2922
+ assert result.uri == "123456789012.dkr.ecr.us-east-1.amazonaws.com/edge-case-repo"
2923
+ assert result.manufacturer == "AWS"
2924
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2925
+ assert result.source_data == repo
2926
+
2927
+ @pytest.mark.parametrize(
2928
+ "resource_type,expected_baseline",
2929
+ [
2930
+ ("AwsAccount", "AWS Account"),
2931
+ ("AwsS3Bucket", "S3 Bucket"),
2932
+ ("AwsIamRole", "IAM Role"),
2933
+ ("AwsEc2Instance", "EC2 Instance"),
2934
+ ],
2935
+ )
2936
+ def test_maps_known_resource_types(self, resource_type, expected_baseline, mock_aws_integration):
2937
+ """Should map known resource types to correct baselines."""
2938
+ resource = {"Type": resource_type, "Id": f"arn:aws:test::123456789012:resource/{resource_type.lower()}"}
2939
+
2940
+ result = AWSInventoryIntegration.get_baseline(resource)
2941
+
2942
+ assert result == expected_baseline
2943
+
2944
+ @pytest.mark.parametrize(
2945
+ "resource,expected_baseline",
2946
+ [
2947
+ (
2948
+ {"Type": "AwsUnknownResource", "Id": "arn:aws:unknown::123456789012:resource/unknown"},
2949
+ "AwsUnknownResource",
2950
+ ),
2951
+ ({"Id": "arn:aws:unknown::123456789012:resource/missing"}, ""),
2952
+ ({"Type": "", "Id": "arn:aws:unknown::123456789012:resource/empty"}, ""),
2953
+ ({"Type": "Test", "Id": "arn:aws:unknown::123456789012:resource/none"}, "Test"),
2954
+ ],
2955
+ ids=["unknown_resource", "missing_type", "empty_type", "none_type"],
2956
+ )
2957
+ def test_get_baseline_edge_cases(self, resource, expected_baseline, mock_aws_integration):
2958
+ """Should handle various edge cases for get_baseline."""
2959
+ result = AWSInventoryIntegration.get_baseline(resource)
2960
+ assert result == expected_baseline
2961
+
2962
+ @pytest.mark.parametrize(
2963
+ "resource_type,expected_baseline",
2964
+ [
2965
+ ("awsaccount", "awsaccount"), # Should return original since it doesn't match
2966
+ ("AWSACCOUNT", "AWSACCOUNT"), # Should return original since it doesn't match
2967
+ ],
2968
+ ids=["lowercase", "uppercase"],
2969
+ )
2970
+ def test_get_baseline_case_sensitive(self, resource_type, expected_baseline, mock_aws_integration):
2971
+ """Test get_baseline with case variations."""
2972
+ resource = {"Type": resource_type, "Id": "arn:aws:iam::123456789012:root"}
2973
+ result = AWSInventoryIntegration.get_baseline(resource)
2974
+ assert result == expected_baseline
2975
+
2976
+ def test_get_baseline_with_additional_fields(self, mock_aws_integration):
2977
+ """Test get_baseline with resource containing additional fields"""
2978
+ resource = {
2979
+ "Type": "AwsS3Bucket",
2980
+ "Id": "arn:aws:s3:::test-bucket",
2981
+ "Partition": "aws",
2982
+ "Region": "us-east-1",
2983
+ "AdditionalField": "additional_value",
2984
+ }
2985
+ result = AWSInventoryIntegration.get_baseline(resource)
2986
+ assert result == "S3 Bucket"
2987
+
2988
+ @pytest.mark.parametrize(
2989
+ "resource_type,expected_baseline",
2990
+ [
2991
+ ("AwsAccount", "AWS Account"),
2992
+ ("AwsS3Bucket", "S3 Bucket"),
2993
+ ("AwsIamRole", "IAM Role"),
2994
+ ("AwsEc2Instance", "EC2 Instance"),
2995
+ ],
2996
+ ids=["aws_account", "aws_s3_bucket", "aws_iam_role", "aws_ec2_instance"],
2997
+ )
2998
+ def test_get_baseline_all_mapped_types(self, resource_type, expected_baseline, mock_aws_integration):
2999
+ """Test get_baseline with all mapped resource types."""
3000
+ resource = {"Type": resource_type, "Id": f"arn:aws:test::123456789012:resource/{resource_type.lower()}"}
3001
+ result = AWSInventoryIntegration.get_baseline(resource)
3002
+ assert result == expected_baseline
3003
+
3004
+ @pytest.mark.parametrize(
3005
+ "resource_type,expected_baseline",
3006
+ [
3007
+ (" AwsAccount ", " AwsAccount "), # Should return original with whitespace
3008
+ ("AwsAccount@#$%", "AwsAccount@#$%"), # Should return original with special chars
3009
+ ("AwsAccount123", "AwsAccount123"), # Should return original with numbers
3010
+ ],
3011
+ ids=["whitespace", "special_chars", "numbers"],
3012
+ )
3013
+ def test_get_baseline_special_characters(self, resource_type, expected_baseline, mock_aws_integration):
3014
+ """Test get_baseline with various special character cases."""
3015
+ resource = {"Type": resource_type, "Id": "arn:aws:iam::123456789012:root"}
3016
+ result = AWSInventoryIntegration.get_baseline(resource)
3017
+ assert result == expected_baseline
3018
+
3019
+ def test_get_baseline_empty_resource(self, mock_aws_integration):
3020
+ """Test get_baseline with empty resource dictionary"""
3021
+ resource = {}
3022
+ result = AWSInventoryIntegration.get_baseline(resource)
3023
+ assert result == ""
3024
+
3025
+ @pytest.mark.parametrize(
3026
+ "arn,expected_name",
3027
+ [
3028
+ ("arn:aws:iam::123456789012:role/test-role", "test-role"),
3029
+ ("arn:aws:iam::123456789012:role/path/to/test-role", "test-role"),
3030
+ ("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0", "i-1234567890abcdef0"),
3031
+ ("arn:aws:iam::123456789012:user/test-user", "test-user"),
3032
+ ("arn:aws:iam::123456789012:role/MyRole", "MyRole"),
3033
+ ],
3034
+ )
3035
+ def test_extracts_name_from_arn_with_slash(self, arn, expected_name, mock_aws_integration):
3036
+ """Should extract name from ARN containing slash."""
3037
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3038
+
3039
+ assert result == expected_name
3040
+
3041
+ @pytest.mark.parametrize(
3042
+ "arn",
3043
+ [
3044
+ "arn:aws:s3:::test-bucket",
3045
+ "arn:aws:s3:::my-test-bucket",
3046
+ "arn:aws:lambda:us-east-1:123456789012:function:my-function",
3047
+ "AWS::::Account:123456789012",
3048
+ ],
3049
+ )
3050
+ def test_returns_full_arn_when_no_slash(self, arn, mock_aws_integration):
3051
+ """Should return full ARN when no slash is present."""
3052
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3053
+
3054
+ assert result == arn
3055
+
3056
+ def test_returns_empty_string_for_empty_input(self, mock_aws_integration):
3057
+ """Should return empty string for empty input."""
3058
+ result = AWSInventoryIntegration.extract_name_from_arn("")
3059
+
3060
+ assert result == ""
3061
+
3062
+ @pytest.mark.parametrize(
3063
+ "test_input",
3064
+ [
3065
+ " ", # whitespace only
3066
+ "simple-string", # no slashes or colons
3067
+ "AWS::::Account:123456789012", # AWS account format
3068
+ ],
3069
+ ids=["whitespace", "simple_string", "aws_account_format"],
3070
+ )
3071
+ def test_returns_original_string_for_non_arn_inputs(self, test_input, mock_aws_integration):
3072
+ """Should return original string for non-ARN inputs."""
3073
+ result = AWSInventoryIntegration.extract_name_from_arn(test_input)
3074
+ assert result == test_input
3075
+
3076
+ def test_extract_name_from_arn_complex_path(self, mock_aws_integration):
3077
+ """Test extract_name_from_arn with complex path structure"""
3078
+ arn = "arn:aws:iam::123456789012:role/path/to/subpath/MyComplexRole"
3079
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3080
+ assert result == "MyComplexRole"
3081
+
3082
+ @pytest.mark.parametrize(
3083
+ "arn,expected_name",
3084
+ [
3085
+ ("arn:aws:iam::123456789012:role/test-role@#$%", "test-role@#$%"),
3086
+ ("arn:aws:iam::123456789012:role/role-123-test", "role-123-test"),
3087
+ ("arn:aws:iam::123456789012:role/test_role_name", "test_role_name"),
3088
+ ("arn:aws:iam::123456789012:role/test.role.name", "test.role.name"),
3089
+ ],
3090
+ ids=["special_chars", "numbers", "underscores", "dots"],
3091
+ )
3092
+ def test_extract_name_from_arn_with_characters(self, arn, expected_name, mock_aws_integration):
3093
+ """Test extract_name_from_arn with various character types in the name."""
3094
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3095
+ assert result == expected_name
3096
+
3097
+ def test_extract_name_from_arn_static_method(self, mock_aws_integration):
3098
+ """Test that extract_name_from_arn is a static method and can be called without instance"""
3099
+ arn = "arn:aws:iam::123456789012:role/test-role"
3100
+
3101
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3102
+ assert result == "test-role"
3103
+
3104
+ @pytest.mark.parametrize(
3105
+ "arn,expected_name",
3106
+ [
3107
+ ("arn:aws:iam::123456789012:role/test-role/", ""),
3108
+ ("arn:aws:iam::123456789012:/role/test-role", "test-role"),
3109
+ ("arn:aws:iam::123456789012:role//test-role", "test-role"),
3110
+ ],
3111
+ ids=["trailing_slash", "leading_slash", "multiple_slashes"],
3112
+ )
3113
+ def test_extract_name_from_arn_slash_edge_cases(self, arn, expected_name, mock_aws_integration):
3114
+ """Test extract_name_from_arn with various slash edge cases."""
3115
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3116
+ assert result == expected_name
3117
+
3118
+ @pytest.mark.parametrize(
3119
+ "arn,expected_name",
3120
+ [
3121
+ ("arn:aws:iam::123456789012:root", "arn:aws:iam::123456789012:root"), # No slashes, returns whole string
3122
+ ("arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket"), # No slashes, returns whole string
3123
+ ("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0", "i-1234567890abcdef0"),
3124
+ ("arn:aws:iam::123456789012:user/JohnDoe", "JohnDoe"),
3125
+ ("arn:aws:iam::123456789012:role/MyRole", "MyRole"),
3126
+ (
3127
+ "arn:aws:lambda:us-east-1:123456789012:function:my-function",
3128
+ "arn:aws:lambda:us-east-1:123456789012:function:my-function",
3129
+ ), # No slashes, returns whole string
3130
+ (
3131
+ "arn:aws:rds:us-east-1:123456789012:db:my-database",
3132
+ "arn:aws:rds:us-east-1:123456789012:db:my-database",
3133
+ ), # No slashes, returns whole string
3134
+ (
3135
+ "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef0",
3136
+ "1234567890abcdef0",
3137
+ ),
3138
+ ],
3139
+ ids=[
3140
+ "iam_root",
3141
+ "s3_bucket",
3142
+ "ec2_instance",
3143
+ "iam_user",
3144
+ "iam_role",
3145
+ "lambda_function",
3146
+ "rds_database",
3147
+ "load_balancer",
3148
+ ],
3149
+ )
3150
+ def test_extract_name_from_arn_real_aws_examples(self, arn, expected_name, mock_aws_integration):
3151
+ """Test extract_name_from_arn with real AWS ARN examples."""
3152
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3153
+ assert result == expected_name
3154
+
3155
+ @pytest.mark.parametrize(
3156
+ "arn,expected_name",
3157
+ [
3158
+ ("test:value", "test:value"), # No slashes, so returns whole string
3159
+ ("test/value", "value"),
3160
+ ("a", "a"),
3161
+ ],
3162
+ ids=["colon_only", "slash_only", "single_char"],
3163
+ )
3164
+ def test_extract_name_from_arn_minimal_arns(self, arn, expected_name, mock_aws_integration):
3165
+ """Test extract_name_from_arn with minimal ARN structures."""
3166
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3167
+ assert result == expected_name
3168
+
3169
+ def test_extract_name_from_arn_mixed_separators(self, mock_aws_integration):
3170
+ """Test extract_name_from_arn with mixed slash and colon separators"""
3171
+
3172
+ arn_mixed = "arn:aws:iam::123456789012:role/path:to:role"
3173
+ result_mixed = AWSInventoryIntegration.extract_name_from_arn(arn_mixed)
3174
+ assert result_mixed == "path:to:role" # Gets the last part after the last slash
3175
+
3176
+ def test_parse_finding_basic_success(self):
3177
+ """Test parse_finding with basic successful finding"""
3178
+ # Create a real instance of AWSInventoryIntegration
3179
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3180
+
3181
+ finding = {
3182
+ "Title": "Test Security Finding",
3183
+ "Description": "This is a test security finding description",
3184
+ "CreatedAt": "2023-01-01T00:00:00Z",
3185
+ "Types": ["Software and Configuration Checks"],
3186
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3187
+ "Compliance": {"Status": "FAILED"},
3188
+ "Remediation": {"Recommendation": {"Text": "Fix this security issue", "Url": "https://example.com/fix"}},
3189
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3190
+ }
3191
+
3192
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3193
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3194
+ ) as mock_comments, patch(
3195
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3196
+ ) as mock_severity, patch(
3197
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3198
+ ) as mock_due_date, patch(
3199
+ "regscale.integrations.commercial.aws.scanner.date_str"
3200
+ ) as mock_date_str, patch(
3201
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3202
+ ) as mock_datetime_str:
3203
+
3204
+ mock_status.return_value = ("Fail", "Test results")
3205
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3206
+ mock_severity.return_value = "HIGH"
3207
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3208
+ mock_date_str.return_value = "2023-01-01"
3209
+ mock_datetime_str.return_value = "2023-02-01"
3210
+
3211
+ aws_integration.app = MagicMock()
3212
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3213
+
3214
+ results = aws_integration.parse_finding(finding)
3215
+
3216
+ assert len(results) == 1
3217
+ finding_result = results[0]
3218
+ assert (
3219
+ finding_result.asset_identifier == "arn:aws:iam::123456789012:root"
3220
+ ) # extract_name_from_arn returns full ARN when no slashes
3221
+ assert finding_result.external_id == "" # No finding ID provided in test data
3222
+ assert finding_result.title == "Test Security Finding"
3223
+ assert finding_result.category == "SecurityHub"
3224
+ assert finding_result.issue_title == "Test Security Finding"
3225
+ assert finding_result.severity == regscale_models.IssueSeverity.High
3226
+ assert finding_result.description == "This is a test security finding description"
3227
+ assert finding_result.status == regscale_models.IssueStatus.Open
3228
+ assert finding_result.checklist_status == regscale_models.ChecklistStatus.FAIL
3229
+ assert finding_result.results == "Test results"
3230
+ assert finding_result.recommendation_for_mitigation == "Fix this security issue"
3231
+ assert finding_result.comments == "Test comments with Finding Severity: HIGH"
3232
+ assert finding_result.poam_comments == "Test comments with Finding Severity: HIGH"
3233
+ assert finding_result.date_created == "2023-01-01"
3234
+ assert finding_result.due_date == "2023-02-01"
3235
+ assert finding_result.plugin_name == "Software and Configuration Checks"
3236
+ assert finding_result.baseline == "AWS Account"
3237
+ assert finding_result.observations == "Test comments with Finding Severity: HIGH"
3238
+ assert finding_result.vulnerability_type == "Vulnerability Scan"
3239
+
3240
+ def test_parse_finding_multiple_resources(self):
3241
+ """Test parse_finding with multiple resources"""
3242
+ # Create a real instance of AWSInventoryIntegration
3243
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3244
+
3245
+ finding = {
3246
+ "Title": "Multi-Resource Finding",
3247
+ "Description": "Finding affecting multiple resources",
3248
+ "CreatedAt": "2023-01-01T00:00:00Z",
3249
+ "Types": ["Software and Configuration Checks"],
3250
+ "Resources": [
3251
+ {"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"},
3252
+ {"Type": "AwsS3Bucket", "Id": "arn:aws:s3:::test-bucket"},
3253
+ ],
3254
+ "Compliance": {"Status": "PASSED"},
3255
+ "Remediation": {"Recommendation": {"Text": "No action needed", "Url": "https://example.com/info"}},
3256
+ "FindingProviderFields": {"Severity": {"Label": "LOW"}},
3257
+ }
3258
+
3259
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3260
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3261
+ ) as mock_comments, patch(
3262
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3263
+ ) as mock_severity, patch(
3264
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3265
+ ) as mock_due_date, patch(
3266
+ "regscale.integrations.commercial.aws.scanner.date_str"
3267
+ ) as mock_date_str, patch(
3268
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3269
+ ) as mock_datetime_str:
3270
+
3271
+ mock_status.return_value = ("Pass", "Passed test")
3272
+ mock_comments.return_value = "Test comments with Finding Severity: LOW"
3273
+ mock_severity.return_value = "LOW"
3274
+ mock_due_date.return_value = "2023-04-01T00:00:00Z"
3275
+ mock_date_str.return_value = "2023-01-01"
3276
+ mock_datetime_str.return_value = "2023-04-01"
3277
+
3278
+ aws_integration.app = MagicMock()
3279
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3280
+
3281
+ results = aws_integration.parse_finding(finding)
3282
+
3283
+ # Should create one finding per resource
3284
+ assert len(results) == 2
3285
+
3286
+ # Check first resource (AWS Account)
3287
+ assert (
3288
+ results[0].asset_identifier == "arn:aws:iam::123456789012:root"
3289
+ ) # extract_name_from_arn returns full ARN when no slashes
3290
+ assert results[0].baseline == "AWS Account"
3291
+ assert results[0].status == regscale_models.IssueStatus.Open # Default status when no config
3292
+
3293
+ # Check second resource (S3 Bucket)
3294
+ assert (
3295
+ results[1].asset_identifier == "arn:aws:s3:::test-bucket"
3296
+ ) # extract_name_from_arn returns full ARN when no slashes
3297
+ assert results[1].baseline == "S3 Bucket" # get_baseline maps AwsS3Bucket to "S3 Bucket"
3298
+ assert results[1].status == regscale_models.IssueStatus.Open # Default status when no config
3299
+
3300
+ def test_parse_finding_missing_severity_config(self):
3301
+ """Test parse_finding when severity config is missing"""
3302
+ # Create a real instance of AWSInventoryIntegration
3303
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3304
+
3305
+ finding = {
3306
+ "Title": "Test Finding",
3307
+ "Description": "Test description",
3308
+ "CreatedAt": "2023-01-01T00:00:00Z",
3309
+ "Types": ["Software and Configuration Checks"],
3310
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3311
+ "Compliance": {"Status": "FAILED"},
3312
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3313
+ "FindingProviderFields": {"Severity": {"Label": "UNKNOWN"}},
3314
+ }
3315
+
3316
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3317
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3318
+ ) as mock_comments, patch(
3319
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3320
+ ) as mock_severity, patch(
3321
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3322
+ ) as mock_due_date, patch(
3323
+ "regscale.integrations.commercial.aws.scanner.date_str"
3324
+ ) as mock_date_str, patch(
3325
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3326
+ ) as mock_datetime_str:
3327
+
3328
+ mock_status.return_value = ("Fail", "Test results")
3329
+ mock_comments.return_value = "Test comments with Finding Severity: UNKNOWN"
3330
+ mock_severity.return_value = "UNKNOWN"
3331
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3332
+ mock_date_str.return_value = "2023-01-01"
3333
+ mock_datetime_str.return_value = "2023-02-01"
3334
+
3335
+ aws_integration.app = MagicMock()
3336
+ aws_integration.app.config = {
3337
+ "issues": {
3338
+ "amazon": {
3339
+ "high": 30,
3340
+ "moderate": 60,
3341
+ # Missing "low" mapping
3342
+ }
3343
+ }
3344
+ }
3345
+
3346
+ results = aws_integration.parse_finding(finding)
3347
+
3348
+ # Should still create a finding with default 30 days
3349
+ assert len(results) == 1
3350
+ assert results[0].due_date == "2023-02-01"
3351
+
3352
+ def test_parse_finding_missing_remediation(self):
3353
+ """Test parse_finding when remediation information is missing"""
3354
+ # Create a real instance of AWSInventoryIntegration
3355
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3356
+
3357
+ finding = {
3358
+ "Title": "Test Finding",
3359
+ "Description": "Test description",
3360
+ "CreatedAt": "2023-01-01T00:00:00Z",
3361
+ "Types": ["Software and Configuration Checks"],
3362
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3363
+ "Compliance": {"Status": "FAILED"},
3364
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3365
+ # Missing Remediation field
3366
+ }
3367
+
3368
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3369
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3370
+ ) as mock_comments, patch(
3371
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3372
+ ) as mock_severity, patch(
3373
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3374
+ ) as mock_due_date, patch(
3375
+ "regscale.integrations.commercial.aws.scanner.date_str"
3376
+ ) as mock_date_str, patch(
3377
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3378
+ ) as mock_datetime_str:
3379
+
3380
+ mock_status.return_value = ("Fail", "Test results")
3381
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3382
+ mock_severity.return_value = "HIGH"
3383
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3384
+ mock_date_str.return_value = "2023-01-01"
3385
+ mock_datetime_str.return_value = "2023-02-01"
3386
+
3387
+ aws_integration.app = MagicMock()
3388
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3389
+
3390
+ results = aws_integration.parse_finding(finding)
3391
+
3392
+ assert len(results) == 1
3393
+ # Should handle missing remediation gracefully
3394
+ assert results[0].recommendation_for_mitigation == ""
3395
+
3396
+ def test_parse_finding_missing_types(self):
3397
+ """Test parse_finding when Types field is missing"""
3398
+ # Create a real instance of AWSInventoryIntegration
3399
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3400
+
3401
+ finding = {
3402
+ "Title": "Test Finding",
3403
+ "Description": "Test description",
3404
+ "CreatedAt": "2023-01-01T00:00:00Z",
3405
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3406
+ "Compliance": {"Status": "FAILED"},
3407
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3408
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3409
+ # Missing Types field
3410
+ }
3411
+
3412
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3413
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3414
+ ) as mock_comments, patch(
3415
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3416
+ ) as mock_severity, patch(
3417
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3418
+ ) as mock_due_date, patch(
3419
+ "regscale.integrations.commercial.aws.scanner.date_str"
3420
+ ) as mock_date_str, patch(
3421
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3422
+ ) as mock_datetime_str:
3423
+
3424
+ mock_status.return_value = ("Fail", "Test results")
3425
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3426
+ mock_severity.return_value = "HIGH"
3427
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3428
+ mock_date_str.return_value = "2023-01-01"
3429
+ mock_datetime_str.return_value = "2023-02-01"
3430
+
3431
+ aws_integration.app = MagicMock()
3432
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3433
+
3434
+ # This should handle the missing Types gracefully and return empty list
3435
+ results = aws_integration.parse_finding(finding)
3436
+ assert len(results) == 0
3437
+
3438
+ def test_parse_finding_empty_types(self):
3439
+ """Test parse_finding when Types field is empty"""
3440
+ # Create a real instance of AWSInventoryIntegration
3441
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3442
+
3443
+ finding = {
3444
+ "Title": "Test Finding",
3445
+ "Description": "Test description",
3446
+ "CreatedAt": "2023-01-01T00:00:00Z",
3447
+ "Types": [], # Empty types list
3448
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3449
+ "Compliance": {"Status": "FAILED"},
3450
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3451
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3452
+ }
3453
+
3454
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3455
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3456
+ ) as mock_comments, patch(
3457
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3458
+ ) as mock_severity, patch(
3459
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3460
+ ) as mock_due_date, patch(
3461
+ "regscale.integrations.commercial.aws.scanner.date_str"
3462
+ ) as mock_date_str, patch(
3463
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3464
+ ) as mock_datetime_str:
3465
+
3466
+ mock_status.return_value = ("Fail", "Test results")
3467
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3468
+ mock_severity.return_value = "HIGH"
3469
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3470
+ mock_date_str.return_value = "2023-01-01"
3471
+ mock_datetime_str.return_value = "2023-02-01"
3472
+
3473
+ aws_integration.app = MagicMock()
3474
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3475
+
3476
+ # This should handle the empty Types gracefully and return empty list
3477
+ results = aws_integration.parse_finding(finding)
3478
+ assert len(results) == 0
3479
+
3480
+ def test_parse_finding_exception_handling(self):
3481
+ """Test parse_finding exception handling"""
3482
+ # Create a real instance of AWSInventoryIntegration
3483
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3484
+
3485
+ finding = {
3486
+ "Title": "Test Finding",
3487
+ "Description": "Test description",
3488
+ "CreatedAt": "2023-01-01T00:00:00Z",
3489
+ "Types": ["Software and Configuration Checks"],
3490
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3491
+ "Compliance": {"Status": "FAILED"},
3492
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3493
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3494
+ }
3495
+
3496
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status:
3497
+
3498
+ # Make determine_status_and_results raise an exception
3499
+ mock_status.side_effect = Exception("Test exception")
3500
+
3501
+ # Should handle exception gracefully and return empty list
3502
+ results = aws_integration.parse_finding(finding)
3503
+
3504
+ assert len(results) == 0
3505
+
3506
+ @pytest.mark.parametrize(
3507
+ "severity_label,friendly_sev,expected_severity",
3508
+ [
3509
+ ("CRITICAL", "high", regscale_models.IssueSeverity.High),
3510
+ ("HIGH", "high", regscale_models.IssueSeverity.High),
3511
+ ("MEDIUM", "moderate", regscale_models.IssueSeverity.Moderate),
3512
+ ("MODERATE", "moderate", None), # MODERATE is not in the mapping, so it returns None
3513
+ ("LOW", "low", regscale_models.IssueSeverity.Low),
3514
+ ("UNKNOWN", "low", None), # UNKNOWN is not in the mapping, so it returns None
3515
+ ],
3516
+ ids=["critical", "high", "medium", "moderate", "low", "unknown"],
3517
+ )
3518
+ def test_parse_finding_different_severities(self, severity_label, friendly_sev, expected_severity):
3519
+ """Test parse_finding with different severity levels."""
3520
+ # Create a real instance of AWSInventoryIntegration
3521
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3522
+
3523
+ finding = {
3524
+ "Title": f"Test {severity_label} Finding",
3525
+ "Description": "Test description",
3526
+ "CreatedAt": "2023-01-01T00:00:00Z",
3527
+ "Types": ["Software and Configuration Checks"],
3528
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3529
+ "Compliance": {"Status": "FAILED"},
3530
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3531
+ "FindingProviderFields": {"Severity": {"Label": severity_label}},
3532
+ }
3533
+
3534
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3535
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3536
+ ) as mock_comments, patch(
3537
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3538
+ ) as mock_severity, patch(
3539
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3540
+ ) as mock_due_date, patch(
3541
+ "regscale.integrations.commercial.aws.scanner.date_str"
3542
+ ) as mock_date_str, patch(
3543
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3544
+ ) as mock_datetime_str:
3545
+
3546
+ mock_status.return_value = ("Fail", "Test results")
3547
+ mock_comments.return_value = f"Test comments with Finding Severity: {severity_label}"
3548
+ mock_severity.return_value = severity_label
3549
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3550
+ mock_date_str.return_value = "2023-01-01"
3551
+ mock_datetime_str.return_value = "2023-02-01"
3552
+
3553
+ aws_integration.app = MagicMock()
3554
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3555
+
3556
+ results = aws_integration.parse_finding(finding)
3557
+
3558
+ assert len(results) == 1
3559
+ assert results[0].severity == expected_severity
3560
+
3561
+ @pytest.mark.parametrize(
3562
+ "status,expected_issue_status,expected_checklist_status",
3563
+ [
3564
+ ("Fail", regscale_models.IssueStatus.Open, regscale_models.ChecklistStatus.FAIL),
3565
+ (
3566
+ "Pass",
3567
+ regscale_models.IssueStatus.Open,
3568
+ regscale_models.ChecklistStatus.PASS,
3569
+ ), # Defaults to Open when no config
3570
+ (
3571
+ "Unknown",
3572
+ regscale_models.IssueStatus.Open,
3573
+ regscale_models.ChecklistStatus.NOT_REVIEWED,
3574
+ ), # Defaults to Open when no config
3575
+ ],
3576
+ ids=["fail", "pass", "unknown"],
3577
+ )
3578
+ def test_parse_finding_different_statuses(self, status, expected_issue_status, expected_checklist_status):
3579
+ """Test parse_finding with different status values."""
3580
+ # Create a real instance of AWSInventoryIntegration
3581
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3582
+
3583
+ finding = {
3584
+ "Title": f"Test {status} Finding",
3585
+ "Description": "Test description",
3586
+ "CreatedAt": "2023-01-01T00:00:00Z",
3587
+ "Types": ["Software and Configuration Checks"],
3588
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3589
+ "Compliance": {"Status": "FAILED" if status == "Fail" else "PASSED"},
3590
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3591
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3592
+ }
3593
+
3594
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3595
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3596
+ ) as mock_comments, patch(
3597
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3598
+ ) as mock_severity, patch(
3599
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3600
+ ) as mock_due_date, patch(
3601
+ "regscale.integrations.commercial.aws.scanner.date_str"
3602
+ ) as mock_date_str, patch(
3603
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3604
+ ) as mock_datetime_str:
3605
+
3606
+ mock_status.return_value = (status, f"{status} results")
3607
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3608
+ mock_severity.return_value = "HIGH"
3609
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3610
+ mock_date_str.return_value = "2023-01-01"
3611
+ mock_datetime_str.return_value = "2023-02-01"
3612
+
3613
+ aws_integration.app = MagicMock()
3614
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3615
+
3616
+ results = aws_integration.parse_finding(finding)
3617
+
3618
+ assert len(results) == 1
3619
+
3620
+ assert results[0].status == expected_issue_status
3621
+ assert results[0].checklist_status == expected_checklist_status
3622
+
3623
+ def test_parse_finding_real_aws_finding_structure(self):
3624
+ """Test parse_finding with real AWS Security Hub finding structure"""
3625
+ # Create a real instance of AWSInventoryIntegration
3626
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3627
+
3628
+ finding = {
3629
+ "SchemaVersion": "2018-10-08",
3630
+ "Id": "arn:aws:securityhub:us-east-1:132360893372:security-control/Config.1/finding/6e568eb7-ea14-46ca-87f3-e8a6efbe805f",
3631
+ "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub",
3632
+ "ProductName": "Security Hub",
3633
+ "CompanyName": "AWS",
3634
+ "Region": "us-east-1",
3635
+ "GeneratorId": "security-control/Config.1",
3636
+ "AwsAccountId": "132360893372",
3637
+ "Types": ["Software and Configuration Checks/Industry and Regulatory Standards"],
3638
+ "FirstObservedAt": "2023-04-26T13:21:29.696Z",
3639
+ "LastObservedAt": "2023-05-02T08:13:56.971Z",
3640
+ "CreatedAt": "2023-04-26T13:21:29.696Z",
3641
+ "UpdatedAt": "2023-05-02T08:13:51.803Z",
3642
+ "Severity": {"Label": "MEDIUM", "Normalized": 40, "Original": "MEDIUM"},
3643
+ "Title": "AWS Config should be enabled",
3644
+ "Description": "This AWS control checks whether the Config service is enabled in the account for the local region and is recording all resources.",
3645
+ "Remediation": {
3646
+ "Recommendation": {
3647
+ "Text": "For information on how to correct this issue, consult the AWS Security Hub controls documentation.",
3648
+ "Url": "https://docs.aws.amazon.com/console/securityhub/Config.1/remediation",
3649
+ }
3650
+ },
3651
+ "ProductFields": {
3652
+ "aws/securityhub/ProductName": "Security Hub",
3653
+ "aws/securityhub/CompanyName": "AWS",
3654
+ "Resources:0/Id": "arn:aws:iam::132360893372:root",
3655
+ "aws/securityhub/FindingId": "arn:aws:securityhub:us-east-1::product/aws/securityhub/arn:aws:securityhub:us-east-1:132360893372:security-control/Config.1/finding/6e568eb7-ea14-46ca-87f3-e8a6efbe805f",
3656
+ },
3657
+ "Resources": [
3658
+ {"Type": "AwsAccount", "Id": "AWS::::Account:132360893372", "Partition": "aws", "Region": "us-east-1"}
3659
+ ],
3660
+ "Compliance": {
3661
+ "Status": "FAILED",
3662
+ "RelatedRequirements": [
3663
+ "NIST.800-53.r5 CM-3",
3664
+ "NIST.800-53.r5 CM-6(1)",
3665
+ "NIST.800-53.r5 CM-8",
3666
+ "NIST.800-53.r5 CM-8(2)",
3667
+ "CIS AWS Foundations Benchmark v1.2.0/2.5",
3668
+ ],
3669
+ "SecurityControlId": "Config.1",
3670
+ "AssociatedStandards": [
3671
+ {"StandardsId": "standards/nist-800-53/v/5.0.0"},
3672
+ {"StandardsId": "ruleset/cis-aws-foundations-benchmark/v/1.2.0"},
3673
+ {"StandardsId": "standards/aws-foundational-security-best-practices/v/1.0.0"},
3674
+ ],
3675
+ },
3676
+ "WorkflowState": "NEW",
3677
+ "Workflow": {"Status": "NEW"},
3678
+ "RecordState": "ACTIVE",
3679
+ "FindingProviderFields": {
3680
+ "Severity": {"Label": "MEDIUM", "Original": "MEDIUM"},
3681
+ "Types": ["Software and Configuration Checks/Industry and Regulatory Standards"],
3682
+ },
3683
+ }
3684
+
3685
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3686
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3687
+ ) as mock_comments, patch(
3688
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3689
+ ) as mock_severity, patch(
3690
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3691
+ ) as mock_due_date, patch(
3692
+ "regscale.integrations.commercial.aws.scanner.date_str"
3693
+ ) as mock_date_str, patch(
3694
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3695
+ ) as mock_datetime_str:
3696
+
3697
+ mock_status.return_value = (
3698
+ "Fail",
3699
+ "NIST.800-53.r5 CM-3, NIST.800-53.r5 CM-6(1), NIST.800-53.r5 CM-8, NIST.800-53.r5 CM-8(2), CIS AWS Foundations Benchmark v1.2.0/2.5",
3700
+ )
3701
+ mock_comments.return_value = "For information on how to correct this issue, consult the AWS Security Hub controls documentation.<br></br>https://docs.aws.amazon.com/console/securityhub/Config.1/remediation<br></br>Finding Severity: MEDIUM"
3702
+ mock_severity.return_value = "MEDIUM"
3703
+ mock_due_date.return_value = "2023-06-25T00:00:00Z"
3704
+ mock_date_str.return_value = "2023-04-26"
3705
+ mock_datetime_str.return_value = "2023-06-25"
3706
+
3707
+ aws_integration.app = MagicMock()
3708
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3709
+
3710
+ results = aws_integration.parse_finding(finding)
3711
+
3712
+ assert len(results) == 1
3713
+ finding_result = results[0]
3714
+ assert (
3715
+ finding_result.asset_identifier == "AWS::::Account:132360893372"
3716
+ ) # extract_name_from_arn returns full ARN when no slashes
3717
+ assert (
3718
+ finding_result.external_id
3719
+ == "arn:aws:securityhub:us-east-1:132360893372:security-control/Config.1/finding/6e568eb7-ea14-46ca-87f3-e8a6efbe805f"
3720
+ )
3721
+ assert finding_result.title == "AWS Config should be enabled"
3722
+ assert finding_result.category == "SecurityHub"
3723
+ assert finding_result.severity == regscale_models.IssueSeverity.Moderate
3724
+ assert finding_result.status == regscale_models.IssueStatus.Open
3725
+ assert finding_result.checklist_status == regscale_models.ChecklistStatus.FAIL
3726
+ assert finding_result.plugin_name == "Software and Configuration Checks/Industry and Regulatory Standards"
3727
+ assert finding_result.baseline == "AWS Account" # get_baseline maps AwsAccount to "AWS Account"
3728
+ assert (
3729
+ finding_result.results
3730
+ == "NIST.800-53.r5 CM-3, NIST.800-53.r5 CM-6(1), NIST.800-53.r5 CM-8, NIST.800-53.r5 CM-8(2), CIS AWS Foundations Benchmark v1.2.0/2.5"
3731
+ )