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.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +19 -4
- regscale/core/app/internal/evidence.py +419 -2
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/jira.py +367 -126
- regscale/integrations/commercial/qualys/__init__.py +7 -8
- regscale/integrations/commercial/qualys/scanner.py +8 -3
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +142 -1
- regscale/integrations/commercial/tenablev2/scanner.py +0 -1
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +64 -79
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
- regscale/integrations/commercial/wizv2/issue.py +1 -1
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +1 -1
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +39 -99
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +60 -41
- regscale/integrations/control_matcher.py +377 -0
- regscale/integrations/due_date_handler.py +14 -8
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +277 -153
- regscale/models/integration_models/cisa_kev_data.json +282 -9
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
- regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +47 -22
- regscale/models/regscale_models/issue.py +256 -95
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +2204 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1365 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +1397 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|