regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (112) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/utils/app_utils.py +11 -2
  3. regscale/dev/cli.py +26 -0
  4. regscale/dev/version.py +72 -0
  5. regscale/integrations/commercial/__init__.py +15 -1
  6. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  7. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  8. regscale/integrations/commercial/amazon/common.py +48 -58
  9. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  10. regscale/integrations/commercial/aws/cli.py +3093 -55
  11. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  12. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  13. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  14. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  15. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  16. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  17. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  18. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  19. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  20. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  21. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  22. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  23. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  24. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  25. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  26. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  27. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  28. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  29. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  30. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  31. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  32. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  33. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  34. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  35. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  36. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  37. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  38. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  39. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  40. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  41. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  42. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  43. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  44. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  45. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  46. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  47. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  48. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  49. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  50. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  51. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  52. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  53. regscale/integrations/commercial/aws/scanner.py +851 -206
  54. regscale/integrations/commercial/aws/security_hub.py +319 -0
  55. regscale/integrations/commercial/aws/session_manager.py +282 -0
  56. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  57. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  58. regscale/integrations/compliance_integration.py +308 -38
  59. regscale/integrations/due_date_handler.py +3 -0
  60. regscale/integrations/scanner_integration.py +399 -84
  61. regscale/models/integration_models/cisa_kev_data.json +34 -4
  62. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  63. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  64. regscale/models/regscale_models/assessment.py +2 -1
  65. regscale/models/regscale_models/control_objective.py +74 -5
  66. regscale/models/regscale_models/file.py +2 -0
  67. regscale/models/regscale_models/issue.py +2 -5
  68. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
  70. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  71. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  73. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  86. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  87. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  89. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  90. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  91. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  92. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  94. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  96. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  98. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  99. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  100. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  101. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  103. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  104. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  105. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  106. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  108. tests/regscale/integrations/commercial/test_aws.py +55 -56
  109. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS Session Token Manager."""
4
+
5
+ import json
6
+ import os
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+ from unittest.mock import MagicMock, Mock, patch, mock_open
10
+
11
+ import pytest
12
+
13
+ from regscale.integrations.commercial.aws.session_manager import AWSSessionManager
14
+
15
+
16
+ class TestAWSSessionManager:
17
+ """Test suite for AWS Session Manager."""
18
+
19
+ @pytest.fixture
20
+ def temp_cache_dir(self, tmp_path):
21
+ """Create a temporary cache directory for testing."""
22
+ cache_dir = tmp_path / "aws_sessions"
23
+ cache_dir.mkdir()
24
+ return str(cache_dir)
25
+
26
+ @pytest.fixture
27
+ def session_manager(self, temp_cache_dir):
28
+ """Create a session manager with temporary cache directory."""
29
+ return AWSSessionManager(cache_dir=temp_cache_dir)
30
+
31
+ @pytest.fixture
32
+ def mock_credentials(self):
33
+ """Create mock AWS credentials response."""
34
+ expiration = datetime.now(timezone.utc) + timedelta(hours=1)
35
+ return {
36
+ "aws_access_key_id": "ASIAIOSFODNN7EXAMPLE",
37
+ "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
38
+ "aws_session_token": "FwoGZXIvYXdzEBYaD...",
39
+ "expiration": expiration.isoformat(),
40
+ }
41
+
42
+ def test_init_creates_cache_directory(self, tmp_path):
43
+ """Test that __init__ creates cache directory if it doesn't exist."""
44
+ cache_dir = tmp_path / "test_cache"
45
+ assert not cache_dir.exists()
46
+
47
+ manager = AWSSessionManager(cache_dir=str(cache_dir))
48
+
49
+ assert cache_dir.exists()
50
+ assert manager.cache_dir == cache_dir
51
+
52
+ def test_init_default_cache_directory(self):
53
+ """Test that __init__ uses default cache directory."""
54
+ manager = AWSSessionManager()
55
+
56
+ expected_path = Path.home() / ".regscale" / "aws_sessions"
57
+ assert manager.cache_dir == expected_path
58
+
59
+ @patch("os.chmod")
60
+ @patch("os.name", "posix")
61
+ def test_init_sets_directory_permissions_unix(self, mock_chmod, temp_cache_dir):
62
+ """Test that __init__ sets restrictive permissions on Unix systems."""
63
+ manager = AWSSessionManager(cache_dir=temp_cache_dir)
64
+
65
+ # Verify chmod was called with owner-only permissions (0o700)
66
+ mock_chmod.assert_called_with(manager.cache_dir, 0o700)
67
+
68
+ @patch("os.name", "nt")
69
+ def test_init_skips_permissions_windows(self, temp_cache_dir):
70
+ """Test that __init__ skips chmod on Windows."""
71
+ # Should not raise an error on Windows
72
+ manager = AWSSessionManager(cache_dir=temp_cache_dir)
73
+ assert manager.cache_dir == Path(temp_cache_dir)
74
+
75
+ def test_get_session_token_with_profile(self, session_manager):
76
+ """Test getting session token using AWS profile."""
77
+ expiration = datetime.now(timezone.utc) + timedelta(hours=1)
78
+ mock_response = {
79
+ "Credentials": {
80
+ "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
81
+ "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
82
+ "SessionToken": "FwoGZXIvYXdzEBYaD...",
83
+ "Expiration": expiration,
84
+ }
85
+ }
86
+
87
+ with patch("boto3.Session") as mock_session_class:
88
+ mock_session = MagicMock()
89
+ mock_sts_client = MagicMock()
90
+ mock_sts_client.get_session_token.return_value = mock_response
91
+ mock_session.client.return_value = mock_sts_client
92
+ mock_session_class.return_value = mock_session
93
+
94
+ credentials = session_manager.get_session_token(profile="test-profile", duration_seconds=3600)
95
+
96
+ # Verify boto3 Session was created with profile
97
+ mock_session_class.assert_called_once_with(profile_name="test-profile")
98
+
99
+ # Verify credentials were returned correctly
100
+ assert credentials["aws_access_key_id"] == "ASIAIOSFODNN7EXAMPLE"
101
+ assert credentials["aws_secret_access_key"] == "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
102
+ assert credentials["aws_session_token"] == "FwoGZXIvYXdzEBYaD..."
103
+ assert "expiration" in credentials
104
+
105
+ def test_get_session_token_with_explicit_credentials(self, session_manager):
106
+ """Test getting session token using explicit AWS credentials."""
107
+ expiration = datetime.now(timezone.utc) + timedelta(hours=1)
108
+ mock_response = {
109
+ "Credentials": {
110
+ "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
111
+ "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
112
+ "SessionToken": "FwoGZXIvYXdzEBYaD...",
113
+ "Expiration": expiration,
114
+ }
115
+ }
116
+
117
+ with patch("boto3.Session") as mock_session_class:
118
+ mock_session = MagicMock()
119
+ mock_sts_client = MagicMock()
120
+ mock_sts_client.get_session_token.return_value = mock_response
121
+ mock_session.client.return_value = mock_sts_client
122
+ mock_session_class.return_value = mock_session
123
+
124
+ credentials = session_manager.get_session_token(
125
+ aws_access_key_id="AKIAIOSFODNN7EXAMPLE",
126
+ aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
127
+ duration_seconds=7200,
128
+ )
129
+
130
+ # Verify boto3 Session was created with explicit credentials
131
+ mock_session_class.assert_called_once_with(
132
+ aws_access_key_id="AKIAIOSFODNN7EXAMPLE",
133
+ aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
134
+ )
135
+
136
+ # Verify get_session_token was called with correct duration
137
+ mock_sts_client.get_session_token.assert_called_once_with(DurationSeconds=7200)
138
+
139
+ assert credentials["aws_access_key_id"] == "ASIAIOSFODNN7EXAMPLE"
140
+
141
+ def test_get_session_token_with_mfa(self, session_manager):
142
+ """Test getting session token with MFA."""
143
+ expiration = datetime.now(timezone.utc) + timedelta(hours=1)
144
+ mock_response = {
145
+ "Credentials": {
146
+ "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
147
+ "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
148
+ "SessionToken": "FwoGZXIvYXdzEBYaD...",
149
+ "Expiration": expiration,
150
+ }
151
+ }
152
+
153
+ with patch("boto3.Session") as mock_session_class:
154
+ mock_session = MagicMock()
155
+ mock_sts_client = MagicMock()
156
+ mock_sts_client.get_session_token.return_value = mock_response
157
+ mock_session.client.return_value = mock_sts_client
158
+ mock_session_class.return_value = mock_session
159
+
160
+ credentials = session_manager.get_session_token(
161
+ profile="test-profile",
162
+ mfa_serial="arn:aws:iam::123456789012:mfa/user",
163
+ mfa_code="123456",
164
+ duration_seconds=3600,
165
+ )
166
+
167
+ # Verify get_session_token was called with MFA parameters
168
+ mock_sts_client.get_session_token.assert_called_once_with(
169
+ SerialNumber="arn:aws:iam::123456789012:mfa/user", TokenCode="123456", DurationSeconds=3600
170
+ )
171
+
172
+ assert credentials["aws_access_key_id"] == "ASIAIOSFODNN7EXAMPLE"
173
+
174
+ def test_get_session_token_assume_role(self, session_manager):
175
+ """Test assuming a role to get session token."""
176
+ expiration = datetime.now(timezone.utc) + timedelta(hours=1)
177
+ mock_response = {
178
+ "Credentials": {
179
+ "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
180
+ "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
181
+ "SessionToken": "FwoGZXIvYXdzEBYaD...",
182
+ "Expiration": expiration,
183
+ }
184
+ }
185
+
186
+ with patch("boto3.Session") as mock_session_class:
187
+ mock_session = MagicMock()
188
+ mock_sts_client = MagicMock()
189
+ mock_sts_client.assume_role.return_value = mock_response
190
+ mock_session.client.return_value = mock_sts_client
191
+ mock_session_class.return_value = mock_session
192
+
193
+ credentials = session_manager.get_session_token(
194
+ profile="test-profile",
195
+ role_arn="arn:aws:iam::987654321098:role/TestRole",
196
+ role_session_name="test-session",
197
+ duration_seconds=3600,
198
+ )
199
+
200
+ # Verify assume_role was called instead of get_session_token
201
+ mock_sts_client.assume_role.assert_called_once()
202
+ call_args = mock_sts_client.assume_role.call_args[1]
203
+ assert call_args["RoleArn"] == "arn:aws:iam::987654321098:role/TestRole"
204
+ assert call_args["RoleSessionName"] == "test-session"
205
+ assert call_args["DurationSeconds"] == 3600
206
+
207
+ assert credentials["aws_access_key_id"] == "ASIAIOSFODNN7EXAMPLE"
208
+
209
+ def test_get_session_token_assume_role_with_mfa(self, session_manager):
210
+ """Test assuming a role with MFA."""
211
+ expiration = datetime.now(timezone.utc) + timedelta(hours=1)
212
+ mock_response = {
213
+ "Credentials": {
214
+ "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
215
+ "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
216
+ "SessionToken": "FwoGZXIvYXdzEBYaD...",
217
+ "Expiration": expiration,
218
+ }
219
+ }
220
+
221
+ with patch("boto3.Session") as mock_session_class:
222
+ mock_session = MagicMock()
223
+ mock_sts_client = MagicMock()
224
+ mock_sts_client.assume_role.return_value = mock_response
225
+ mock_session.client.return_value = mock_sts_client
226
+ mock_session_class.return_value = mock_session
227
+
228
+ credentials = session_manager.get_session_token(
229
+ profile="test-profile",
230
+ role_arn="arn:aws:iam::987654321098:role/TestRole",
231
+ role_session_name="test-session",
232
+ mfa_serial="arn:aws:iam::123456789012:mfa/user",
233
+ mfa_code="123456",
234
+ duration_seconds=3600,
235
+ )
236
+
237
+ # Verify assume_role was called with MFA parameters
238
+ call_args = mock_sts_client.assume_role.call_args[1]
239
+ assert call_args["SerialNumber"] == "arn:aws:iam::123456789012:mfa/user"
240
+ assert call_args["TokenCode"] == "123456"
241
+
242
+ assert credentials["aws_access_key_id"] == "ASIAIOSFODNN7EXAMPLE"
243
+
244
+ def test_get_session_token_error_handling(self, session_manager):
245
+ """Test error handling when getting session token fails."""
246
+ with patch("boto3.Session") as mock_session_class:
247
+ mock_session = MagicMock()
248
+ mock_sts_client = MagicMock()
249
+ mock_sts_client.get_session_token.side_effect = Exception("AWS API Error")
250
+ mock_session.client.return_value = mock_sts_client
251
+ mock_session_class.return_value = mock_session
252
+
253
+ with pytest.raises(Exception) as exc_info:
254
+ session_manager.get_session_token(profile="test-profile")
255
+
256
+ assert "AWS API Error" in str(exc_info.value)
257
+
258
+ def test_cache_session(self, session_manager, mock_credentials, temp_cache_dir):
259
+ """Test caching session credentials to file."""
260
+ session_manager.cache_session("test-session", mock_credentials, region="us-east-1")
261
+
262
+ cache_file = Path(temp_cache_dir) / "test-session.json"
263
+ assert cache_file.exists()
264
+
265
+ with open(cache_file, "r") as f:
266
+ cached_data = json.load(f)
267
+
268
+ assert cached_data["session_name"] == "test-session"
269
+ assert cached_data["region"] == "us-east-1"
270
+ assert cached_data["credentials"] == mock_credentials
271
+ assert "cached_at" in cached_data
272
+
273
+ @patch("os.chmod")
274
+ @patch("os.name", "posix")
275
+ def test_cache_session_sets_file_permissions_unix(
276
+ self, mock_chmod, session_manager, mock_credentials, temp_cache_dir
277
+ ):
278
+ """Test that cache_session sets restrictive permissions on Unix."""
279
+ session_manager.cache_session("test-session", mock_credentials)
280
+
281
+ # Verify chmod was called with owner-only permissions (0o600)
282
+ cache_file = Path(temp_cache_dir) / "test-session.json"
283
+ mock_chmod.assert_called_with(cache_file, 0o600)
284
+
285
+ def test_get_cached_session_valid(self, session_manager, mock_credentials, temp_cache_dir):
286
+ """Test retrieving a valid cached session."""
287
+ # Cache a session first
288
+ session_manager.cache_session("test-session", mock_credentials, region="us-east-1")
289
+
290
+ # Retrieve it
291
+ cached_data = session_manager.get_cached_session("test-session")
292
+
293
+ assert cached_data is not None
294
+ assert cached_data["session_name"] == "test-session"
295
+ assert cached_data["region"] == "us-east-1"
296
+ assert cached_data["credentials"] == mock_credentials
297
+
298
+ def test_get_cached_session_not_found(self, session_manager):
299
+ """Test retrieving a non-existent cached session."""
300
+ cached_data = session_manager.get_cached_session("nonexistent")
301
+
302
+ assert cached_data is None
303
+
304
+ def test_get_cached_session_expired(self, session_manager, temp_cache_dir):
305
+ """Test retrieving an expired cached session."""
306
+ # Create expired credentials (expired 1 hour ago)
307
+ expiration = datetime.now(timezone.utc) - timedelta(hours=1)
308
+ expired_credentials = {
309
+ "aws_access_key_id": "ASIAIOSFODNN7EXAMPLE",
310
+ "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
311
+ "aws_session_token": "FwoGZXIvYXdzEBYaD...",
312
+ "expiration": expiration.isoformat(),
313
+ }
314
+
315
+ session_manager.cache_session("expired-session", expired_credentials)
316
+
317
+ # Try to retrieve it
318
+ cached_data = session_manager.get_cached_session("expired-session")
319
+
320
+ assert cached_data is None
321
+
322
+ # Verify cache file was deleted
323
+ cache_file = Path(temp_cache_dir) / "expired-session.json"
324
+ assert not cache_file.exists()
325
+
326
+ def test_get_cached_session_expiring_soon(self, session_manager, temp_cache_dir):
327
+ """Test retrieving a session that expires in less than 5 minutes."""
328
+ # Create credentials that expire in 2 minutes
329
+ expiration = datetime.now(timezone.utc) + timedelta(minutes=2)
330
+ expiring_credentials = {
331
+ "aws_access_key_id": "ASIAIOSFODNN7EXAMPLE",
332
+ "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
333
+ "aws_session_token": "FwoGZXIvYXdzEBYaD...",
334
+ "expiration": expiration.isoformat(),
335
+ }
336
+
337
+ session_manager.cache_session("expiring-session", expiring_credentials)
338
+
339
+ # Try to retrieve it
340
+ cached_data = session_manager.get_cached_session("expiring-session")
341
+
342
+ # Should be treated as expired (5 minute buffer)
343
+ assert cached_data is None
344
+
345
+ def test_get_cached_session_corrupted_file(self, session_manager, temp_cache_dir):
346
+ """Test retrieving a session with corrupted cache file."""
347
+ # Create a corrupted cache file
348
+ cache_file = Path(temp_cache_dir) / "corrupted-session.json"
349
+ with open(cache_file, "w") as f:
350
+ f.write("not valid json{{{")
351
+
352
+ cached_data = session_manager.get_cached_session("corrupted-session")
353
+
354
+ assert cached_data is None
355
+
356
+ def test_clear_session_exists(self, session_manager, mock_credentials, temp_cache_dir):
357
+ """Test clearing an existing cached session."""
358
+ session_manager.cache_session("test-session", mock_credentials)
359
+
360
+ result = session_manager.clear_session("test-session")
361
+
362
+ assert result is True
363
+
364
+ cache_file = Path(temp_cache_dir) / "test-session.json"
365
+ assert not cache_file.exists()
366
+
367
+ def test_clear_session_not_found(self, session_manager):
368
+ """Test clearing a non-existent cached session."""
369
+ result = session_manager.clear_session("nonexistent")
370
+
371
+ assert result is False
372
+
373
+ def test_clear_all_sessions(self, session_manager, mock_credentials, temp_cache_dir):
374
+ """Test clearing all cached sessions."""
375
+ # Create multiple sessions
376
+ session_manager.cache_session("session-1", mock_credentials)
377
+ session_manager.cache_session("session-2", mock_credentials)
378
+ session_manager.cache_session("session-3", mock_credentials)
379
+
380
+ count = session_manager.clear_all_sessions()
381
+
382
+ assert count == 3
383
+
384
+ # Verify all cache files are deleted
385
+ cache_dir = Path(temp_cache_dir)
386
+ assert len(list(cache_dir.glob("*.json"))) == 0
387
+
388
+ def test_clear_all_sessions_empty(self, session_manager):
389
+ """Test clearing all sessions when none exist."""
390
+ count = session_manager.clear_all_sessions()
391
+
392
+ assert count == 0
393
+
394
+ def test_list_sessions(self, session_manager, mock_credentials, temp_cache_dir):
395
+ """Test listing all cached sessions."""
396
+ # Create multiple sessions
397
+ session_manager.cache_session("session-1", mock_credentials, region="us-east-1")
398
+ session_manager.cache_session("session-2", mock_credentials, region="us-west-2")
399
+
400
+ sessions = session_manager.list_sessions()
401
+
402
+ assert len(sessions) == 2
403
+
404
+ # Verify session data
405
+ session_names = {s["name"] for s in sessions}
406
+ assert "session-1" in session_names
407
+ assert "session-2" in session_names
408
+
409
+ # Verify each session has required fields
410
+ for session in sessions:
411
+ assert "name" in session
412
+ assert "region" in session
413
+ assert "expiration" in session
414
+ assert "expired" in session
415
+ assert "cached_at" in session
416
+ assert isinstance(session["expired"], bool)
417
+
418
+ def test_list_sessions_with_expired(self, session_manager, temp_cache_dir):
419
+ """Test listing sessions includes expired status."""
420
+ # Create one valid and one expired session
421
+ valid_expiration = datetime.now(timezone.utc) + timedelta(hours=1)
422
+ valid_credentials = {
423
+ "aws_access_key_id": "ASIAIOSFODNN7EXAMPLE",
424
+ "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
425
+ "aws_session_token": "FwoGZXIvYXdzEBYaD...",
426
+ "expiration": valid_expiration.isoformat(),
427
+ }
428
+
429
+ expired_expiration = datetime.now(timezone.utc) - timedelta(hours=1)
430
+ expired_credentials = {
431
+ "aws_access_key_id": "ASIAIOSFODNN7EXAMPLE",
432
+ "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
433
+ "aws_session_token": "FwoGZXIvYXdzEBYaD...",
434
+ "expiration": expired_expiration.isoformat(),
435
+ }
436
+
437
+ session_manager.cache_session("valid-session", valid_credentials)
438
+ session_manager.cache_session("expired-session", expired_credentials)
439
+
440
+ sessions = session_manager.list_sessions()
441
+
442
+ assert len(sessions) == 2
443
+
444
+ # Find the sessions and check their expired status
445
+ valid_session = next(s for s in sessions if s["name"] == "valid-session")
446
+ expired_session = next(s for s in sessions if s["name"] == "expired-session")
447
+
448
+ assert valid_session["expired"] is False
449
+ assert expired_session["expired"] is True
450
+
451
+ def test_list_sessions_empty(self, session_manager):
452
+ """Test listing sessions when none exist."""
453
+ sessions = session_manager.list_sessions()
454
+
455
+ assert sessions == []
456
+
457
+ def test_list_sessions_corrupted_file(self, session_manager, temp_cache_dir):
458
+ """Test listing sessions skips corrupted files."""
459
+ # Create one valid and one corrupted session
460
+ valid_expiration = datetime.now(timezone.utc) + timedelta(hours=1)
461
+ valid_credentials = {
462
+ "aws_access_key_id": "ASIAIOSFODNN7EXAMPLE",
463
+ "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
464
+ "aws_session_token": "FwoGZXIvYXdzEBYaD...",
465
+ "expiration": valid_expiration.isoformat(),
466
+ }
467
+
468
+ session_manager.cache_session("valid-session", valid_credentials)
469
+
470
+ # Create corrupted file
471
+ corrupted_file = Path(temp_cache_dir) / "corrupted.json"
472
+ with open(corrupted_file, "w") as f:
473
+ f.write("not valid json{{{")
474
+
475
+ sessions = session_manager.list_sessions()
476
+
477
+ # Should only return the valid session
478
+ assert len(sessions) == 1
479
+ assert sessions[0]["name"] == "valid-session"
480
+
481
+ def test_get_credentials_for_session(self, session_manager, mock_credentials):
482
+ """Test getting credentials tuple for use in CLI commands."""
483
+ session_manager.cache_session("test-session", mock_credentials, region="us-east-1")
484
+
485
+ credentials_tuple = session_manager.get_credentials_for_session("test-session")
486
+
487
+ assert credentials_tuple is not None
488
+ access_key_id, secret_access_key, session_token, region = credentials_tuple
489
+
490
+ assert access_key_id == mock_credentials["aws_access_key_id"]
491
+ assert secret_access_key == mock_credentials["aws_secret_access_key"]
492
+ assert session_token == mock_credentials["aws_session_token"]
493
+ assert region == "us-east-1"
494
+
495
+ def test_get_credentials_for_session_not_found(self, session_manager):
496
+ """Test getting credentials for non-existent session."""
497
+ credentials_tuple = session_manager.get_credentials_for_session("nonexistent")
498
+
499
+ assert credentials_tuple is None
500
+
501
+ def test_get_credentials_for_session_expired(self, session_manager):
502
+ """Test getting credentials for expired session."""
503
+ # Create expired credentials
504
+ expiration = datetime.now(timezone.utc) - timedelta(hours=1)
505
+ expired_credentials = {
506
+ "aws_access_key_id": "ASIAIOSFODNN7EXAMPLE",
507
+ "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
508
+ "aws_session_token": "FwoGZXIvYXdzEBYaD...",
509
+ "expiration": expiration.isoformat(),
510
+ }
511
+
512
+ session_manager.cache_session("expired-session", expired_credentials)
513
+
514
+ credentials_tuple = session_manager.get_credentials_for_session("expired-session")
515
+
516
+ assert credentials_tuple is None