iflow-mcp_democratize-technology-chronos-mcp 2.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.
Files changed (68) hide show
  1. chronos_mcp/__init__.py +5 -0
  2. chronos_mcp/__main__.py +9 -0
  3. chronos_mcp/accounts.py +410 -0
  4. chronos_mcp/bulk.py +946 -0
  5. chronos_mcp/caldav_utils.py +149 -0
  6. chronos_mcp/calendars.py +204 -0
  7. chronos_mcp/config.py +187 -0
  8. chronos_mcp/credentials.py +190 -0
  9. chronos_mcp/events.py +515 -0
  10. chronos_mcp/exceptions.py +477 -0
  11. chronos_mcp/journals.py +477 -0
  12. chronos_mcp/logging_config.py +23 -0
  13. chronos_mcp/models.py +202 -0
  14. chronos_mcp/py.typed +0 -0
  15. chronos_mcp/rrule.py +259 -0
  16. chronos_mcp/search.py +315 -0
  17. chronos_mcp/server.py +121 -0
  18. chronos_mcp/tasks.py +518 -0
  19. chronos_mcp/tools/__init__.py +29 -0
  20. chronos_mcp/tools/accounts.py +151 -0
  21. chronos_mcp/tools/base.py +59 -0
  22. chronos_mcp/tools/bulk.py +557 -0
  23. chronos_mcp/tools/calendars.py +142 -0
  24. chronos_mcp/tools/events.py +698 -0
  25. chronos_mcp/tools/journals.py +310 -0
  26. chronos_mcp/tools/tasks.py +414 -0
  27. chronos_mcp/utils.py +163 -0
  28. chronos_mcp/validation.py +636 -0
  29. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
  30. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
  31. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
  32. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
  33. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
  34. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
  35. tests/__init__.py +0 -0
  36. tests/conftest.py +91 -0
  37. tests/unit/__init__.py +0 -0
  38. tests/unit/test_accounts.py +380 -0
  39. tests/unit/test_accounts_ssrf.py +134 -0
  40. tests/unit/test_base.py +135 -0
  41. tests/unit/test_bulk.py +380 -0
  42. tests/unit/test_bulk_create.py +408 -0
  43. tests/unit/test_bulk_delete.py +341 -0
  44. tests/unit/test_bulk_resource_limits.py +74 -0
  45. tests/unit/test_caldav_utils.py +300 -0
  46. tests/unit/test_calendars.py +286 -0
  47. tests/unit/test_config.py +111 -0
  48. tests/unit/test_config_validation.py +128 -0
  49. tests/unit/test_credentials_security.py +189 -0
  50. tests/unit/test_cryptography_security.py +178 -0
  51. tests/unit/test_events.py +536 -0
  52. tests/unit/test_exceptions.py +58 -0
  53. tests/unit/test_journals.py +1097 -0
  54. tests/unit/test_models.py +95 -0
  55. tests/unit/test_race_conditions.py +202 -0
  56. tests/unit/test_recurring_events.py +156 -0
  57. tests/unit/test_rrule.py +217 -0
  58. tests/unit/test_search.py +372 -0
  59. tests/unit/test_search_advanced.py +333 -0
  60. tests/unit/test_server_input_validation.py +219 -0
  61. tests/unit/test_ssrf_protection.py +505 -0
  62. tests/unit/test_tasks.py +918 -0
  63. tests/unit/test_thread_safety.py +301 -0
  64. tests/unit/test_tools_journals.py +617 -0
  65. tests/unit/test_tools_tasks.py +968 -0
  66. tests/unit/test_url_validation_security.py +234 -0
  67. tests/unit/test_utils.py +180 -0
  68. tests/unit/test_validation.py +983 -0
@@ -0,0 +1,111 @@
1
+ """
2
+ Unit tests for configuration management
3
+ """
4
+
5
+ import pytest
6
+
7
+ from chronos_mcp.config import ChronosConfig, ConfigManager
8
+ from chronos_mcp.models import Account
9
+
10
+
11
+ class TestConfigManager:
12
+ def test_config_init(self, mock_config_manager):
13
+ """Test ConfigManager initialization"""
14
+ # The config_dir is set but not necessarily created until save
15
+ assert mock_config_manager.config_dir.name == ".chronos"
16
+ assert mock_config_manager.config_file.name == "accounts.json"
17
+ assert isinstance(mock_config_manager.config, ChronosConfig)
18
+
19
+ def test_add_account(self, mock_config_manager, sample_account):
20
+ """Test adding an account"""
21
+ mock_config_manager.add_account(sample_account)
22
+ assert "test_account" in mock_config_manager.config.accounts
23
+ assert (
24
+ mock_config_manager.config.accounts["test_account"].username == "testuser"
25
+ )
26
+ # Should be set as default if it's the first account
27
+ assert mock_config_manager.config.default_account == "test_account"
28
+
29
+ def test_remove_account(self, mock_config_manager, sample_account):
30
+ """Test removing an account"""
31
+ mock_config_manager.add_account(sample_account)
32
+ mock_config_manager.remove_account("test_account")
33
+ assert "test_account" not in mock_config_manager.config.accounts
34
+ assert mock_config_manager.config.default_account is None
35
+
36
+ def test_get_account(self, mock_config_manager, sample_account):
37
+ """Test getting an account"""
38
+ mock_config_manager.add_account(sample_account)
39
+ account = mock_config_manager.get_account("test_account")
40
+ assert account.username == "testuser"
41
+
42
+ def test_get_default_account(self, mock_config_manager, sample_account):
43
+ """Test getting default account when no alias specified"""
44
+ mock_config_manager.add_account(sample_account)
45
+ account = mock_config_manager.get_account() # No alias
46
+ assert account.username == "testuser"
47
+
48
+ def test_save_and_load_config(self, tmp_path, sample_account):
49
+ """Test saving and loading configuration"""
50
+ # Create a config manager with a real temp directory
51
+ config_dir = tmp_path / ".chronos"
52
+ config_dir.mkdir(exist_ok=True)
53
+
54
+ # Override the config_dir for this test
55
+ mgr = ConfigManager()
56
+ mgr.config_dir = config_dir
57
+ mgr.config_file = config_dir / "accounts.json"
58
+
59
+ # Add account and save
60
+ mgr.add_account(sample_account)
61
+ mgr.save_config() # Changed from _save_config to save_config
62
+
63
+ # Verify file was created
64
+ assert mgr.config_file.exists()
65
+
66
+ # Create new manager instance to test loading
67
+ new_mgr = ConfigManager()
68
+ new_mgr.config_dir = config_dir
69
+ new_mgr.config_file = config_dir / "accounts.json"
70
+ new_mgr._load_config()
71
+
72
+ assert "test_account" in new_mgr.config.accounts
73
+ assert new_mgr.config.accounts["test_account"].username == "testuser"
74
+
75
+ def test_list_accounts(self, mock_config_manager, sample_account):
76
+ """Test listing all accounts"""
77
+ mock_config_manager.add_account(sample_account)
78
+ accounts = mock_config_manager.list_accounts()
79
+ assert len(accounts) == 1
80
+ assert "test_account" in accounts
81
+
82
+ def test_add_duplicate_account_raises_error(
83
+ self, mock_config_manager, sample_account
84
+ ):
85
+ """Test that adding an account with duplicate alias raises AccountAlreadyExistsError"""
86
+ from chronos_mcp.exceptions import AccountAlreadyExistsError
87
+
88
+ # Add the first account
89
+ mock_config_manager.add_account(sample_account)
90
+
91
+ # Create a second account with the same alias but different URL
92
+ duplicate_account = Account(
93
+ alias="test_account", # Same alias
94
+ url="https://different.caldav.com", # Different URL
95
+ username="different_user",
96
+ password="different_pass",
97
+ )
98
+
99
+ # Attempt to add duplicate should raise error
100
+ with pytest.raises(AccountAlreadyExistsError) as exc_info:
101
+ mock_config_manager.add_account(duplicate_account)
102
+
103
+ # Verify error details
104
+ assert "test_account" in str(exc_info.value)
105
+ assert exc_info.value.error_code == "ACCOUNT_EXISTS"
106
+
107
+ # Verify original account was not modified
108
+ accounts = mock_config_manager.list_accounts()
109
+ assert len(accounts) == 1
110
+ assert str(accounts["test_account"].url) == "https://caldav.example.com/"
111
+ assert accounts["test_account"].username == "testuser"
@@ -0,0 +1,128 @@
1
+ """
2
+ Tests for configuration validation
3
+ """
4
+
5
+ import os
6
+ from unittest.mock import patch, Mock
7
+ import pytest
8
+
9
+ from chronos_mcp.config import ConfigManager
10
+ from chronos_mcp.exceptions import ValidationError
11
+
12
+
13
+ class TestConfigValidation:
14
+ """Test configuration input validation"""
15
+
16
+ @patch('os.getenv')
17
+ @patch('chronos_mcp.config.get_credential_manager')
18
+ def test_environment_password_validation(self, mock_cred_manager, mock_getenv):
19
+ """Test that environment variable passwords are validated"""
20
+ # Mock environment variables with control character in password
21
+ def getenv_side_effect(key, default=None):
22
+ env_vars = {
23
+ 'CALDAV_BASE_URL': 'https://example.com',
24
+ 'CALDAV_USERNAME': 'valid_user',
25
+ 'CALDAV_PASSWORD': 'a' * 11000 # Exceeds max length
26
+ }
27
+ return env_vars.get(key, default)
28
+
29
+ mock_getenv.side_effect = getenv_side_effect
30
+ mock_cred_manager.return_value.keyring_available = False
31
+
32
+ # Should skip environment account due to validation failure
33
+ config_mgr = ConfigManager()
34
+
35
+ # Default account should not be created due to validation failure
36
+ assert "default" not in config_mgr.config.accounts
37
+
38
+ @patch('os.getenv')
39
+ @patch('chronos_mcp.config.get_credential_manager')
40
+ def test_environment_username_validation(self, mock_cred_manager, mock_getenv):
41
+ """Test that environment variable usernames are validated"""
42
+ # Mock environment variables with XSS in username
43
+ def getenv_side_effect(key, default=None):
44
+ env_vars = {
45
+ 'CALDAV_BASE_URL': 'https://example.com',
46
+ 'CALDAV_USERNAME': '<script>alert("xss")</script>', # XSS - should be rejected
47
+ 'CALDAV_PASSWORD': 'ValidPassword123'
48
+ }
49
+ return env_vars.get(key, default)
50
+
51
+ mock_getenv.side_effect = getenv_side_effect
52
+ mock_cred_manager.return_value.keyring_available = False
53
+
54
+ # Should skip environment account due to validation failure
55
+ config_mgr = ConfigManager()
56
+
57
+ # Default account should not be created
58
+ assert "default" not in config_mgr.config.accounts
59
+
60
+ @patch.dict(os.environ, {
61
+ 'CALDAV_BASE_URL': 'https://example.com',
62
+ 'CALDAV_USERNAME': 'valid_user',
63
+ 'CALDAV_PASSWORD': 'ValidP@ssw0rd!'
64
+ })
65
+ @patch('chronos_mcp.config.get_credential_manager')
66
+ def test_environment_valid_credentials(self, mock_cred_manager):
67
+ """Test that valid environment credentials are accepted"""
68
+ mock_cred_mgr = Mock()
69
+ mock_cred_mgr.keyring_available = False
70
+ mock_cred_manager.return_value = mock_cred_mgr
71
+
72
+ config_mgr = ConfigManager()
73
+
74
+ # Default account should be created with valid inputs
75
+ assert "default" in config_mgr.config.accounts
76
+ assert config_mgr.config.accounts["default"].username == "valid_user"
77
+ assert config_mgr.config.accounts["default"].password == "ValidP@ssw0rd!"
78
+
79
+
80
+ class TestModelValidation:
81
+ """Test Pydantic model-level validation (defense-in-depth)"""
82
+
83
+ def test_account_model_password_validation(self):
84
+ """Test that Account model validates password field"""
85
+ from chronos_mcp.models import Account
86
+ from pydantic import ValidationError
87
+
88
+ # Test with oversized password - should be rejected by validator
89
+ with pytest.raises(ValidationError) as exc_info:
90
+ Account(
91
+ alias="test",
92
+ url="https://example.com",
93
+ username="user",
94
+ password="a" * 11000, # Exceeds validation limit
95
+ display_name="Test Account"
96
+ )
97
+
98
+ assert "CALDAV_PASSWORD" in str(exc_info.value) or "password" in str(exc_info.value).lower()
99
+
100
+ def test_account_model_valid_password(self):
101
+ """Test that Account model accepts valid passwords"""
102
+ from chronos_mcp.models import Account
103
+
104
+ # Valid password should pass
105
+ account = Account(
106
+ alias="test",
107
+ url="https://example.com",
108
+ username="user",
109
+ password="ValidP@ssw0rd!123",
110
+ display_name="Test Account"
111
+ )
112
+
113
+ assert account.password == "ValidP@ssw0rd!123"
114
+
115
+ def test_account_model_none_password(self):
116
+ """Test that Account model accepts None password (keyring scenario)"""
117
+ from chronos_mcp.models import Account
118
+
119
+ # None password should pass (for keyring usage)
120
+ account = Account(
121
+ alias="test",
122
+ url="https://example.com",
123
+ username="user",
124
+ password=None,
125
+ display_name="Test Account"
126
+ )
127
+
128
+ assert account.password is None
@@ -0,0 +1,189 @@
1
+ """
2
+ Security-focused tests for credential management
3
+ """
4
+
5
+ import io
6
+ import logging
7
+ import sys
8
+ from unittest.mock import MagicMock, Mock, patch
9
+
10
+ import pytest
11
+
12
+ from chronos_mcp.credentials import CredentialManager, get_credential_manager
13
+
14
+
15
+ class TestCredentialSecurity:
16
+ """Test security aspects of credential management"""
17
+
18
+ def test_no_password_info_in_debug_logs(self, caplog):
19
+ """Test that password information is redacted in debug logs"""
20
+ # Test with keyring available
21
+ with patch("chronos_mcp.credentials.keyring") as mock_keyring:
22
+ mock_keyring.get_password.return_value = "test_password"
23
+
24
+ manager = CredentialManager()
25
+ manager.keyring_available = True
26
+
27
+ with caplog.at_level(logging.DEBUG):
28
+ password = manager.get_password("test_alias")
29
+
30
+ # Check that debug log doesn't contain actual alias
31
+ debug_logs = [
32
+ record.message
33
+ for record in caplog.records
34
+ if record.levelname == "DEBUG"
35
+ ]
36
+ assert any("[REDACTED]" in log for log in debug_logs)
37
+ assert not any("test_alias" in log for log in debug_logs)
38
+ assert password == "test_password"
39
+
40
+ def test_no_password_info_in_fallback_logs(self, caplog):
41
+ """Test that fallback password logs are redacted"""
42
+ manager = CredentialManager()
43
+ manager.keyring_available = False
44
+
45
+ with caplog.at_level(logging.DEBUG):
46
+ password = manager.get_password(
47
+ "test_alias", fallback_password="fallback_pass"
48
+ )
49
+
50
+ # Check that debug log doesn't contain actual alias
51
+ debug_logs = [
52
+ record.message for record in caplog.records if record.levelname == "DEBUG"
53
+ ]
54
+ assert any("[REDACTED]" in log for log in debug_logs)
55
+ assert not any("test_alias" in log for log in debug_logs)
56
+ assert password == "fallback_pass"
57
+
58
+ def test_no_password_info_in_store_logs(self, caplog):
59
+ """Test that password storage logs are redacted"""
60
+ with patch("chronos_mcp.credentials.keyring") as mock_keyring:
61
+ mock_keyring.set_password.return_value = None
62
+
63
+ manager = CredentialManager()
64
+ manager.keyring_available = True
65
+
66
+ with caplog.at_level(logging.INFO):
67
+ result = manager.set_password("test_alias", "secret_password")
68
+
69
+ # Check that info log doesn't contain actual alias
70
+ info_logs = [
71
+ record.message
72
+ for record in caplog.records
73
+ if record.levelname == "INFO"
74
+ ]
75
+ assert any("[REDACTED]" in log for log in info_logs)
76
+ assert not any("test_alias" in log for log in info_logs)
77
+ assert result is True
78
+
79
+ def test_no_password_info_in_delete_logs(self, caplog):
80
+ """Test that password deletion logs are redacted"""
81
+ with patch("chronos_mcp.credentials.keyring") as mock_keyring:
82
+ mock_keyring.delete_password.return_value = None
83
+
84
+ manager = CredentialManager()
85
+ manager.keyring_available = True
86
+
87
+ with caplog.at_level(logging.INFO):
88
+ result = manager.delete_password("test_alias")
89
+
90
+ # Check logs don't contain actual alias
91
+ info_logs = [
92
+ record.message
93
+ for record in caplog.records
94
+ if record.levelname == "INFO"
95
+ ]
96
+ assert any("[REDACTED]" in log for log in info_logs)
97
+ assert not any("test_alias" in log for log in info_logs)
98
+ assert result is True
99
+
100
+ def test_no_password_info_in_debug_delete_logs(self, caplog):
101
+ """Test that debug deletion logs are redacted"""
102
+ with (
103
+ patch("chronos_mcp.credentials.keyring") as mock_keyring,
104
+ patch("chronos_mcp.credentials.keyring.errors") as mock_errors,
105
+ ):
106
+
107
+ # Create a proper PasswordDeleteError exception
108
+ class MockPasswordDeleteError(Exception):
109
+ pass
110
+
111
+ mock_errors.PasswordDeleteError = MockPasswordDeleteError
112
+
113
+ # Make delete_password raise the exception
114
+ mock_keyring.delete_password.side_effect = MockPasswordDeleteError()
115
+
116
+ manager = CredentialManager()
117
+ manager.keyring_available = True
118
+
119
+ with caplog.at_level(logging.DEBUG):
120
+ result = manager.delete_password("test_alias")
121
+
122
+ # Check that debug log doesn't contain actual alias
123
+ debug_logs = [
124
+ record.message
125
+ for record in caplog.records
126
+ if record.levelname == "DEBUG"
127
+ ]
128
+ assert any("[REDACTED]" in log for log in debug_logs)
129
+ assert not any("test_alias" in log for log in debug_logs)
130
+ assert result is False
131
+
132
+ def test_no_password_info_when_keyring_unavailable(self, caplog):
133
+ """Test redacted logs when keyring is unavailable"""
134
+ manager = CredentialManager()
135
+ manager.keyring_available = False
136
+
137
+ with caplog.at_level(logging.DEBUG):
138
+ result = manager.set_password("test_alias", "secret_password")
139
+
140
+ # Check that debug log doesn't contain actual alias
141
+ debug_logs = [
142
+ record.message for record in caplog.records if record.levelname == "DEBUG"
143
+ ]
144
+ assert any("[REDACTED]" in log for log in debug_logs)
145
+ assert not any("test_alias" in log for log in debug_logs)
146
+ assert result is False
147
+
148
+ def test_password_never_logged_directly(self, caplog):
149
+ """Test that actual passwords are never logged anywhere"""
150
+ with patch("chronos_mcp.credentials.keyring") as mock_keyring:
151
+ mock_keyring.get_password.return_value = "supersecret123"
152
+ mock_keyring.set_password.return_value = None
153
+ mock_keyring.delete_password.return_value = None
154
+
155
+ manager = CredentialManager()
156
+ manager.keyring_available = True
157
+
158
+ # Capture all log levels
159
+ with caplog.at_level(logging.DEBUG):
160
+ manager.get_password("test_alias")
161
+ manager.set_password("test_alias", "supersecret123")
162
+ manager.delete_password("test_alias")
163
+
164
+ # Check that password never appears in any log
165
+ all_logs = [record.message for record in caplog.records]
166
+ assert not any("supersecret123" in log for log in all_logs)
167
+ assert not any(
168
+ "secret" in log.lower()
169
+ for log in all_logs
170
+ if "supersecret123" not in log
171
+ )
172
+
173
+ def test_alias_never_logged_in_password_context(self, caplog):
174
+ """Test that aliases never appear in password-related logs"""
175
+ sensitive_alias = "production_admin_account"
176
+
177
+ with patch("chronos_mcp.credentials.keyring") as mock_keyring:
178
+ mock_keyring.get_password.return_value = "password123"
179
+
180
+ manager = CredentialManager()
181
+ manager.keyring_available = True
182
+
183
+ with caplog.at_level(logging.DEBUG):
184
+ manager.get_password(sensitive_alias)
185
+
186
+ # Check that sensitive alias never appears in logs
187
+ all_logs = [record.message for record in caplog.records]
188
+ assert not any(sensitive_alias in log for log in all_logs)
189
+ assert any("[REDACTED]" in log for log in all_logs)
@@ -0,0 +1,178 @@
1
+ """
2
+ Test suite for cryptography security and version compatibility.
3
+
4
+ This test verifies that the cryptography library functions correctly for
5
+ our use case and helps detect any security vulnerabilities or compatibility
6
+ issues when updating to newer versions.
7
+ """
8
+
9
+ import pytest
10
+ import sys
11
+ from unittest.mock import patch, MagicMock
12
+
13
+
14
+ # Test version information
15
+ def test_cryptography_version_check():
16
+ """Test that cryptography version is appropriate for security requirements."""
17
+ try:
18
+ import cryptography
19
+ from packaging import version
20
+
21
+ current_version = version.parse(cryptography.__version__)
22
+
23
+ # Version 45.0.5 is our current version
24
+ # We should verify it's at least this version to ensure known vulnerabilities are patched
25
+ min_version = version.parse("42.0.4") # Known vulnerable versions are < 42.0.4
26
+
27
+ assert (
28
+ current_version >= min_version
29
+ ), f"Cryptography version {current_version} is below minimum secure version {min_version}"
30
+
31
+ print(f"Current cryptography version: {current_version}")
32
+
33
+ except ImportError:
34
+ pytest.fail("Cryptography library not available")
35
+
36
+
37
+ def test_keyring_cryptography_integration():
38
+ """Test that keyring works with current cryptography version."""
39
+ try:
40
+ import keyring
41
+ from chronos_mcp.credentials import CredentialManager
42
+
43
+ # Test that credential manager can be instantiated
44
+ credential_manager = CredentialManager()
45
+
46
+ # Test basic functionality - this should not fail with cryptography issues
47
+ status = credential_manager.get_status()
48
+ assert isinstance(status, dict)
49
+ assert "keyring_available" in status
50
+
51
+ # If keyring is available, test basic operations
52
+ if status["keyring_available"]:
53
+ # Test storing and retrieving a test credential
54
+ test_alias = "test_crypto_security"
55
+ test_password = "test_password_123"
56
+
57
+ try:
58
+ # Store password
59
+ result = credential_manager.set_password(test_alias, test_password)
60
+
61
+ if result: # Only test retrieval if storage succeeded
62
+ # Retrieve password
63
+ retrieved = credential_manager.get_password(test_alias)
64
+ assert retrieved == test_password
65
+
66
+ # Clean up
67
+ credential_manager.delete_password(test_alias)
68
+
69
+ except Exception as e:
70
+ # Log the error but don't fail the test if it's a platform-specific keyring issue
71
+ print(f"Keyring operation failed (may be platform-specific): {e}")
72
+
73
+ except ImportError as e:
74
+ pytest.skip(f"Keyring not available: {e}")
75
+
76
+
77
+ def test_cryptography_vulnerable_version_detection():
78
+ """Test that we can detect if we're running a vulnerable cryptography version."""
79
+ try:
80
+ import cryptography
81
+ from packaging import version
82
+
83
+ current_version = version.parse(cryptography.__version__)
84
+
85
+ # Known vulnerable versions
86
+ vulnerable_versions = [
87
+ "38.0.0",
88
+ "38.0.1",
89
+ "38.0.2",
90
+ "38.0.3",
91
+ "38.0.4",
92
+ "39.0.0",
93
+ "40.0.0",
94
+ "40.0.1",
95
+ "40.0.2",
96
+ "41.0.0",
97
+ "41.0.1",
98
+ "41.0.2",
99
+ "41.0.3",
100
+ "41.0.4",
101
+ "41.0.5",
102
+ "41.0.6",
103
+ "41.0.7",
104
+ "42.0.0",
105
+ "42.0.1",
106
+ "42.0.2",
107
+ "42.0.3", # CVE-2023-23931, CVE-2023-0286
108
+ ]
109
+
110
+ # This test should PASS with our current version (45.0.5)
111
+ # but would FAIL if we were running a vulnerable version
112
+ for vuln_version in vulnerable_versions:
113
+ assert current_version > version.parse(vuln_version), (
114
+ f"Current version {current_version} is vulnerable! "
115
+ f"Known vulnerable version: {vuln_version}"
116
+ )
117
+
118
+ except ImportError:
119
+ pytest.fail("Cryptography library not available")
120
+
121
+
122
+ def test_future_cryptography_version_compatibility():
123
+ """Test that updating to newer cryptography versions doesn't break our usage."""
124
+ try:
125
+ import cryptography
126
+ from packaging import version
127
+
128
+ current_version = version.parse(cryptography.__version__)
129
+
130
+ # Test that we're not on a version that's too old
131
+ # Version 46.0.1 is the latest as of this test
132
+ recommended_min = version.parse("45.0.0")
133
+
134
+ assert (
135
+ current_version >= recommended_min
136
+ ), f"Cryptography version {current_version} is older than recommended minimum {recommended_min}"
137
+
138
+ # This test will help us detect if a future update breaks compatibility
139
+ # by testing basic cryptographic operations that keyring might use
140
+ from cryptography.fernet import Fernet
141
+ from cryptography.hazmat.primitives import hashes
142
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
143
+ import os
144
+ import base64
145
+
146
+ # Test basic encryption/decryption (similar to what keyring backends might do)
147
+ password = b"test_password"
148
+ salt = os.urandom(16)
149
+
150
+ kdf = PBKDF2HMAC(
151
+ algorithm=hashes.SHA256(),
152
+ length=32,
153
+ salt=salt,
154
+ iterations=100000,
155
+ )
156
+ key = base64.urlsafe_b64encode(kdf.derive(password))
157
+ f = Fernet(key)
158
+
159
+ # Test encryption/decryption
160
+ test_data = b"sensitive credential data"
161
+ encrypted = f.encrypt(test_data)
162
+ decrypted = f.decrypt(encrypted)
163
+
164
+ assert decrypted == test_data, "Basic cryptography operations failed"
165
+
166
+ except ImportError:
167
+ pytest.fail("Cryptography library not available")
168
+ except Exception as e:
169
+ pytest.fail(f"Cryptography compatibility test failed: {e}")
170
+
171
+
172
+ if __name__ == "__main__":
173
+ # Run tests directly for debugging
174
+ test_cryptography_version_check()
175
+ test_keyring_cryptography_integration()
176
+ test_cryptography_vulnerable_version_detection()
177
+ test_future_cryptography_version_compatibility()
178
+ print("All cryptography security tests passed!")