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.
- chronos_mcp/__init__.py +5 -0
- chronos_mcp/__main__.py +9 -0
- chronos_mcp/accounts.py +410 -0
- chronos_mcp/bulk.py +946 -0
- chronos_mcp/caldav_utils.py +149 -0
- chronos_mcp/calendars.py +204 -0
- chronos_mcp/config.py +187 -0
- chronos_mcp/credentials.py +190 -0
- chronos_mcp/events.py +515 -0
- chronos_mcp/exceptions.py +477 -0
- chronos_mcp/journals.py +477 -0
- chronos_mcp/logging_config.py +23 -0
- chronos_mcp/models.py +202 -0
- chronos_mcp/py.typed +0 -0
- chronos_mcp/rrule.py +259 -0
- chronos_mcp/search.py +315 -0
- chronos_mcp/server.py +121 -0
- chronos_mcp/tasks.py +518 -0
- chronos_mcp/tools/__init__.py +29 -0
- chronos_mcp/tools/accounts.py +151 -0
- chronos_mcp/tools/base.py +59 -0
- chronos_mcp/tools/bulk.py +557 -0
- chronos_mcp/tools/calendars.py +142 -0
- chronos_mcp/tools/events.py +698 -0
- chronos_mcp/tools/journals.py +310 -0
- chronos_mcp/tools/tasks.py +414 -0
- chronos_mcp/utils.py +163 -0
- chronos_mcp/validation.py +636 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +91 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_accounts.py +380 -0
- tests/unit/test_accounts_ssrf.py +134 -0
- tests/unit/test_base.py +135 -0
- tests/unit/test_bulk.py +380 -0
- tests/unit/test_bulk_create.py +408 -0
- tests/unit/test_bulk_delete.py +341 -0
- tests/unit/test_bulk_resource_limits.py +74 -0
- tests/unit/test_caldav_utils.py +300 -0
- tests/unit/test_calendars.py +286 -0
- tests/unit/test_config.py +111 -0
- tests/unit/test_config_validation.py +128 -0
- tests/unit/test_credentials_security.py +189 -0
- tests/unit/test_cryptography_security.py +178 -0
- tests/unit/test_events.py +536 -0
- tests/unit/test_exceptions.py +58 -0
- tests/unit/test_journals.py +1097 -0
- tests/unit/test_models.py +95 -0
- tests/unit/test_race_conditions.py +202 -0
- tests/unit/test_recurring_events.py +156 -0
- tests/unit/test_rrule.py +217 -0
- tests/unit/test_search.py +372 -0
- tests/unit/test_search_advanced.py +333 -0
- tests/unit/test_server_input_validation.py +219 -0
- tests/unit/test_ssrf_protection.py +505 -0
- tests/unit/test_tasks.py +918 -0
- tests/unit/test_thread_safety.py +301 -0
- tests/unit/test_tools_journals.py +617 -0
- tests/unit/test_tools_tasks.py +968 -0
- tests/unit/test_url_validation_security.py +234 -0
- tests/unit/test_utils.py +180 -0
- 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!")
|