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,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security-focused tests for server input validation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from chronos_mcp.exceptions import ValidationError
|
|
8
|
+
from chronos_mcp.validation import InputValidator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestServerInputValidation:
|
|
12
|
+
"""Test input validation that should be applied in MCP server tools"""
|
|
13
|
+
|
|
14
|
+
def test_url_validation_enforces_https(self):
|
|
15
|
+
"""Test that URL validation enforces HTTPS"""
|
|
16
|
+
validator = InputValidator()
|
|
17
|
+
|
|
18
|
+
# Valid HTTPS URLs should pass
|
|
19
|
+
assert validator.PATTERNS["url"].match("https://caldav.example.com/")
|
|
20
|
+
assert validator.PATTERNS["url"].match("https://localhost:8443/caldav")
|
|
21
|
+
assert validator.PATTERNS["url"].match("https://192.168.1.100:8443/caldav")
|
|
22
|
+
|
|
23
|
+
# HTTP URLs should be rejected
|
|
24
|
+
assert not validator.PATTERNS["url"].match("http://caldav.example.com/")
|
|
25
|
+
assert not validator.PATTERNS["url"].match("http://localhost:8080/caldav")
|
|
26
|
+
|
|
27
|
+
def test_text_field_validation_rejects_dangerous_content(self):
|
|
28
|
+
"""Test that text field validation rejects dangerous content"""
|
|
29
|
+
validator = InputValidator()
|
|
30
|
+
|
|
31
|
+
# Valid text should pass
|
|
32
|
+
result = validator.validate_text_field(
|
|
33
|
+
"Valid Account Name", "alias", required=True
|
|
34
|
+
)
|
|
35
|
+
assert result == "Valid Account Name"
|
|
36
|
+
|
|
37
|
+
# Dangerous script content should be rejected
|
|
38
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
39
|
+
validator.validate_text_field(
|
|
40
|
+
"<script>alert('xss')</script>", "alias", required=True
|
|
41
|
+
)
|
|
42
|
+
assert "dangerous content" in str(exc_info.value)
|
|
43
|
+
|
|
44
|
+
# JavaScript protocol should be rejected
|
|
45
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
46
|
+
validator.validate_text_field("javascript:alert(1)", "alias", required=True)
|
|
47
|
+
assert "dangerous content" in str(exc_info.value)
|
|
48
|
+
|
|
49
|
+
def test_text_field_validation_enforces_length_limits(self):
|
|
50
|
+
"""Test that text field validation enforces length limits"""
|
|
51
|
+
validator = InputValidator()
|
|
52
|
+
|
|
53
|
+
# Text within limits should pass
|
|
54
|
+
short_text = "a" * 49 # Within alias limit of 50
|
|
55
|
+
result = validator.validate_text_field(short_text, "alias", required=True)
|
|
56
|
+
assert result == short_text
|
|
57
|
+
|
|
58
|
+
# Text exceeding limits should be rejected
|
|
59
|
+
long_text = "a" * 1000 # Exceeds alias limit
|
|
60
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
61
|
+
validator.validate_text_field(long_text, "alias", required=True)
|
|
62
|
+
assert "exceeds maximum length" in str(exc_info.value)
|
|
63
|
+
|
|
64
|
+
def test_required_field_validation(self):
|
|
65
|
+
"""Test that required fields are validated"""
|
|
66
|
+
validator = InputValidator()
|
|
67
|
+
|
|
68
|
+
# Empty required field should be rejected
|
|
69
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
70
|
+
validator.validate_text_field("", "alias", required=True)
|
|
71
|
+
assert "required" in str(exc_info.value)
|
|
72
|
+
|
|
73
|
+
# None for required field should be rejected
|
|
74
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
75
|
+
validator.validate_text_field(None, "alias", required=True)
|
|
76
|
+
assert "required" in str(exc_info.value)
|
|
77
|
+
|
|
78
|
+
# Empty optional field should return empty string
|
|
79
|
+
result = validator.validate_text_field("", "description", required=False)
|
|
80
|
+
assert result == ""
|
|
81
|
+
|
|
82
|
+
def test_uid_validation_rejects_invalid_characters(self):
|
|
83
|
+
"""Test that UID validation rejects invalid characters"""
|
|
84
|
+
validator = InputValidator()
|
|
85
|
+
|
|
86
|
+
# Valid UID should pass
|
|
87
|
+
result = validator.validate_uid("valid-uid-123_test.example@domain")
|
|
88
|
+
assert result == "valid-uid-123_test.example@domain"
|
|
89
|
+
|
|
90
|
+
# UID with dangerous characters should be rejected
|
|
91
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
92
|
+
validator.validate_uid("<script>alert('xss')</script>")
|
|
93
|
+
assert "invalid characters" in str(exc_info.value)
|
|
94
|
+
|
|
95
|
+
# UID with spaces should be rejected
|
|
96
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
97
|
+
validator.validate_uid("uid with spaces")
|
|
98
|
+
assert "invalid characters" in str(exc_info.value)
|
|
99
|
+
|
|
100
|
+
def test_color_validation_enforces_hex_format(self):
|
|
101
|
+
"""Test that color validation enforces hex format"""
|
|
102
|
+
validator = InputValidator()
|
|
103
|
+
|
|
104
|
+
# Valid hex colors should pass
|
|
105
|
+
valid_colors = ["#FF0000", "#00FF00", "#0000FF", "#123456", "#ABCDEF"]
|
|
106
|
+
for color in valid_colors:
|
|
107
|
+
assert validator.PATTERNS["color"].match(
|
|
108
|
+
color
|
|
109
|
+
), f"Valid color should match: {color}"
|
|
110
|
+
|
|
111
|
+
# Invalid colors should be rejected
|
|
112
|
+
invalid_colors = ["FF0000", "#GG0000", "#12345", "#1234567", "red"]
|
|
113
|
+
for color in invalid_colors:
|
|
114
|
+
assert not validator.PATTERNS["color"].match(
|
|
115
|
+
color
|
|
116
|
+
), f"Invalid color should be rejected: {color}"
|
|
117
|
+
|
|
118
|
+
# Note: #ff0000 is actually valid (lowercase hex is allowed)
|
|
119
|
+
|
|
120
|
+
def test_unicode_normalization(self):
|
|
121
|
+
"""Test that Unicode text is properly normalized"""
|
|
122
|
+
validator = InputValidator()
|
|
123
|
+
|
|
124
|
+
# Unicode text should be normalized
|
|
125
|
+
unicode_text = "Tést Àccount with ünicode"
|
|
126
|
+
result = validator.validate_text_field(
|
|
127
|
+
unicode_text, "display_name", required=True
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Should not raise an error and should normalize the text
|
|
131
|
+
assert result is not None
|
|
132
|
+
assert len(result) > 0
|
|
133
|
+
|
|
134
|
+
def test_dangerous_pattern_detection(self):
|
|
135
|
+
"""Test that various dangerous patterns are detected"""
|
|
136
|
+
validator = InputValidator()
|
|
137
|
+
|
|
138
|
+
dangerous_inputs = [
|
|
139
|
+
"<script>alert('xss')</script>",
|
|
140
|
+
"javascript:alert(1)",
|
|
141
|
+
"data:text/html,<script>alert(1)</script>",
|
|
142
|
+
"expression(alert(1))",
|
|
143
|
+
'<iframe src="evil.com"></iframe>',
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
for dangerous_input in dangerous_inputs:
|
|
147
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
148
|
+
validator.validate_text_field(
|
|
149
|
+
dangerous_input, "description", required=False
|
|
150
|
+
)
|
|
151
|
+
assert "dangerous content" in str(
|
|
152
|
+
exc_info.value
|
|
153
|
+
), f"Should reject: {dangerous_input}"
|
|
154
|
+
|
|
155
|
+
# Some patterns might not match exactly - that's OK as long as major threats are caught
|
|
156
|
+
potentially_dangerous = [
|
|
157
|
+
"on<event>=handler", # Might not match our specific patterns
|
|
158
|
+
"\\u003cscript\\u003ealert(1)\\u003c/script\\u003e", # Unicode escapes
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# These should ideally be caught, but if not, it's not a critical failure for this test
|
|
162
|
+
|
|
163
|
+
def test_validation_preserves_safe_content(self):
|
|
164
|
+
"""Test that validation preserves safe content"""
|
|
165
|
+
validator = InputValidator()
|
|
166
|
+
|
|
167
|
+
safe_inputs = [
|
|
168
|
+
"Normal text content",
|
|
169
|
+
"Email: user@example.com",
|
|
170
|
+
"URL: https://example.com/path",
|
|
171
|
+
"Special chars: !@#$%^&*()",
|
|
172
|
+
"Numbers: 12345",
|
|
173
|
+
"Mixed: Test123!@#",
|
|
174
|
+
"Unicode: café résumé naïve",
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
for safe_input in safe_inputs:
|
|
178
|
+
result = validator.validate_text_field(
|
|
179
|
+
safe_input, "description", required=False
|
|
180
|
+
)
|
|
181
|
+
assert result is not None, f"Should preserve safe content: {safe_input}"
|
|
182
|
+
assert len(result) > 0
|
|
183
|
+
|
|
184
|
+
def test_url_validation_allows_common_caldav_patterns(self):
|
|
185
|
+
"""Test that URL validation allows common CalDAV server patterns"""
|
|
186
|
+
validator = InputValidator()
|
|
187
|
+
|
|
188
|
+
common_caldav_urls = [
|
|
189
|
+
"https://caldav.fastmail.com/dav/calendars/user/",
|
|
190
|
+
"https://calendar.google.com/calendar/dav/",
|
|
191
|
+
"https://outlook.office365.com/EWS/Exchange.asmx",
|
|
192
|
+
"https://dav.mailbox.org/caldav/",
|
|
193
|
+
"https://caldav.icloud.com/",
|
|
194
|
+
"https://server.example.com:8443/caldav/",
|
|
195
|
+
"https://192.168.1.100:8080/radicale/",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
for url in common_caldav_urls:
|
|
199
|
+
assert validator.PATTERNS["url"].match(
|
|
200
|
+
url
|
|
201
|
+
), f"Common CalDAV URL should be allowed: {url}"
|
|
202
|
+
|
|
203
|
+
def test_validation_error_sanitization(self):
|
|
204
|
+
"""Test that validation errors don't leak sensitive information"""
|
|
205
|
+
validator = InputValidator()
|
|
206
|
+
|
|
207
|
+
# Test with potentially sensitive information
|
|
208
|
+
try:
|
|
209
|
+
validator.validate_text_field(
|
|
210
|
+
"password123secret<script>", "alias", required=True
|
|
211
|
+
)
|
|
212
|
+
assert False, "Should have raised ValidationError"
|
|
213
|
+
except ValidationError as e:
|
|
214
|
+
error_msg = str(e)
|
|
215
|
+
# Error message should not contain the original sensitive content
|
|
216
|
+
assert "password123secret" not in error_msg
|
|
217
|
+
assert "<script>" not in error_msg
|
|
218
|
+
# Should contain generic message
|
|
219
|
+
assert "dangerous content" in error_msg
|