devsync 0.5.5__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 (84) hide show
  1. aiconfigkit/__init__.py +0 -0
  2. aiconfigkit/__main__.py +6 -0
  3. aiconfigkit/ai_tools/__init__.py +0 -0
  4. aiconfigkit/ai_tools/base.py +236 -0
  5. aiconfigkit/ai_tools/capability_registry.py +262 -0
  6. aiconfigkit/ai_tools/claude.py +91 -0
  7. aiconfigkit/ai_tools/claude_desktop.py +97 -0
  8. aiconfigkit/ai_tools/cline.py +92 -0
  9. aiconfigkit/ai_tools/copilot.py +92 -0
  10. aiconfigkit/ai_tools/cursor.py +109 -0
  11. aiconfigkit/ai_tools/detector.py +169 -0
  12. aiconfigkit/ai_tools/kiro.py +85 -0
  13. aiconfigkit/ai_tools/mcp_syncer.py +291 -0
  14. aiconfigkit/ai_tools/roo.py +110 -0
  15. aiconfigkit/ai_tools/translator.py +390 -0
  16. aiconfigkit/ai_tools/winsurf.py +102 -0
  17. aiconfigkit/cli/__init__.py +0 -0
  18. aiconfigkit/cli/delete.py +118 -0
  19. aiconfigkit/cli/download.py +274 -0
  20. aiconfigkit/cli/install.py +237 -0
  21. aiconfigkit/cli/install_new.py +937 -0
  22. aiconfigkit/cli/list.py +275 -0
  23. aiconfigkit/cli/main.py +454 -0
  24. aiconfigkit/cli/mcp_configure.py +232 -0
  25. aiconfigkit/cli/mcp_install.py +166 -0
  26. aiconfigkit/cli/mcp_sync.py +165 -0
  27. aiconfigkit/cli/package.py +383 -0
  28. aiconfigkit/cli/package_create.py +323 -0
  29. aiconfigkit/cli/package_install.py +472 -0
  30. aiconfigkit/cli/template.py +19 -0
  31. aiconfigkit/cli/template_backup.py +261 -0
  32. aiconfigkit/cli/template_init.py +499 -0
  33. aiconfigkit/cli/template_install.py +261 -0
  34. aiconfigkit/cli/template_list.py +172 -0
  35. aiconfigkit/cli/template_uninstall.py +146 -0
  36. aiconfigkit/cli/template_update.py +225 -0
  37. aiconfigkit/cli/template_validate.py +234 -0
  38. aiconfigkit/cli/tools.py +47 -0
  39. aiconfigkit/cli/uninstall.py +125 -0
  40. aiconfigkit/cli/update.py +309 -0
  41. aiconfigkit/core/__init__.py +0 -0
  42. aiconfigkit/core/checksum.py +211 -0
  43. aiconfigkit/core/component_detector.py +905 -0
  44. aiconfigkit/core/conflict_resolution.py +329 -0
  45. aiconfigkit/core/git_operations.py +539 -0
  46. aiconfigkit/core/mcp/__init__.py +1 -0
  47. aiconfigkit/core/mcp/credentials.py +279 -0
  48. aiconfigkit/core/mcp/manager.py +308 -0
  49. aiconfigkit/core/mcp/set_manager.py +1 -0
  50. aiconfigkit/core/mcp/validator.py +1 -0
  51. aiconfigkit/core/models.py +1661 -0
  52. aiconfigkit/core/package_creator.py +743 -0
  53. aiconfigkit/core/package_manifest.py +248 -0
  54. aiconfigkit/core/repository.py +298 -0
  55. aiconfigkit/core/secret_detector.py +438 -0
  56. aiconfigkit/core/template_manifest.py +283 -0
  57. aiconfigkit/core/version.py +201 -0
  58. aiconfigkit/storage/__init__.py +0 -0
  59. aiconfigkit/storage/library.py +429 -0
  60. aiconfigkit/storage/mcp_tracker.py +1 -0
  61. aiconfigkit/storage/package_tracker.py +234 -0
  62. aiconfigkit/storage/template_library.py +229 -0
  63. aiconfigkit/storage/template_tracker.py +296 -0
  64. aiconfigkit/storage/tracker.py +416 -0
  65. aiconfigkit/tui/__init__.py +5 -0
  66. aiconfigkit/tui/installer.py +511 -0
  67. aiconfigkit/utils/__init__.py +0 -0
  68. aiconfigkit/utils/atomic_write.py +90 -0
  69. aiconfigkit/utils/backup.py +169 -0
  70. aiconfigkit/utils/dotenv.py +128 -0
  71. aiconfigkit/utils/git_helpers.py +187 -0
  72. aiconfigkit/utils/logging.py +60 -0
  73. aiconfigkit/utils/namespace.py +134 -0
  74. aiconfigkit/utils/paths.py +205 -0
  75. aiconfigkit/utils/project.py +109 -0
  76. aiconfigkit/utils/streaming.py +216 -0
  77. aiconfigkit/utils/ui.py +194 -0
  78. aiconfigkit/utils/validation.py +187 -0
  79. devsync-0.5.5.dist-info/LICENSE +21 -0
  80. devsync-0.5.5.dist-info/METADATA +477 -0
  81. devsync-0.5.5.dist-info/RECORD +84 -0
  82. devsync-0.5.5.dist-info/WHEEL +5 -0
  83. devsync-0.5.5.dist-info/entry_points.txt +2 -0
  84. devsync-0.5.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,438 @@
1
+ """Secret detection engine for identifying and templating sensitive values."""
2
+
3
+ import math
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Optional
7
+
8
+ from aiconfigkit.core.models import SecretConfidence
9
+
10
+
11
+ @dataclass
12
+ class SecretDetectionResult:
13
+ """Result of secret detection analysis.
14
+
15
+ Attributes:
16
+ confidence: Confidence level that this is a secret
17
+ reason: Human-readable explanation of detection
18
+ original_value: Original value analyzed
19
+ templated_value: Suggested template replacement (e.g., ${API_KEY})
20
+ """
21
+
22
+ confidence: SecretConfidence
23
+ reason: str
24
+ original_value: str
25
+ templated_value: Optional[str] = None
26
+
27
+
28
+ @dataclass
29
+ class SecretDetector:
30
+ """Heuristic-based secret detector with three confidence levels.
31
+
32
+ Detection rules:
33
+ - HIGH: Keywords (*_TOKEN, *_KEY, *_SECRET, *_PASSWORD, API_*, AUTH_*),
34
+ high entropy (>4.5 bits/char), API key patterns (20+ alphanumeric)
35
+ - MEDIUM: Ambiguous keywords (*_URL with credentials, *_ID with entropy)
36
+ - SAFE: URLs without credentials, booleans, version strings, short values (<8 chars)
37
+ """
38
+
39
+ high_entropy_threshold: float = 4.5
40
+ min_secret_length: int = 8
41
+
42
+ secret_keywords: list[str] = field(
43
+ default_factory=lambda: [
44
+ "TOKEN",
45
+ "KEY",
46
+ "SECRET",
47
+ "PASSWORD",
48
+ "PASSWD",
49
+ "CREDENTIAL",
50
+ "AUTH",
51
+ "PRIVATE",
52
+ "API",
53
+ ]
54
+ )
55
+
56
+ safe_keywords: list[str] = field(
57
+ default_factory=lambda: [
58
+ "PATH",
59
+ "DIR",
60
+ "NAME",
61
+ "TYPE",
62
+ "MODE",
63
+ "DEBUG",
64
+ "LEVEL",
65
+ "HOST",
66
+ "PORT",
67
+ "VERSION",
68
+ "ENABLED",
69
+ "DISABLED",
70
+ ]
71
+ )
72
+
73
+ ambiguous_keywords: list[str] = field(default_factory=lambda: ["URL", "ID", "ENDPOINT", "URI"])
74
+
75
+ def detect(self, value: str, key_name: str = "") -> SecretDetectionResult:
76
+ """Analyze a value to determine if it's likely a secret.
77
+
78
+ Args:
79
+ value: The value to analyze
80
+ key_name: Optional environment variable or key name (e.g., "API_KEY")
81
+
82
+ Returns:
83
+ SecretDetectionResult with confidence level and explanation
84
+ """
85
+ if not value or not value.strip():
86
+ return SecretDetectionResult(
87
+ confidence=SecretConfidence.SAFE,
88
+ reason="Empty or whitespace-only value",
89
+ original_value=value,
90
+ )
91
+
92
+ key_upper = key_name.upper() if key_name else ""
93
+
94
+ keyword_result = self._keyword_match(key_upper, value)
95
+ if keyword_result:
96
+ return keyword_result
97
+
98
+ pattern_result = self._pattern_match(value, key_upper)
99
+ if pattern_result:
100
+ return pattern_result
101
+
102
+ entropy_result = self._entropy_analysis(value, key_upper)
103
+ if entropy_result:
104
+ return entropy_result
105
+
106
+ return SecretDetectionResult(
107
+ confidence=SecretConfidence.SAFE,
108
+ reason="No secret indicators detected",
109
+ original_value=value,
110
+ )
111
+
112
+ def template_value(self, key: str) -> str:
113
+ """Convert a key name to a template placeholder.
114
+
115
+ Args:
116
+ key: The key name (e.g., "API_KEY", "github_token")
117
+
118
+ Returns:
119
+ Template placeholder (e.g., "${API_KEY}")
120
+ """
121
+ normalized_key = key.upper().replace("-", "_")
122
+ if not normalized_key.startswith("$"):
123
+ return f"${{{normalized_key}}}"
124
+ return normalized_key
125
+
126
+ def _keyword_match(self, key_upper: str, value: str) -> Optional[SecretDetectionResult]:
127
+ """Check for secret-related keywords in the key name.
128
+
129
+ Args:
130
+ key_upper: Uppercase key name
131
+ value: The value being analyzed
132
+
133
+ Returns:
134
+ SecretDetectionResult if keyword matched, None otherwise
135
+ """
136
+ if not key_upper:
137
+ return None
138
+
139
+ for safe_kw in self.safe_keywords:
140
+ if safe_kw in key_upper:
141
+ parts = key_upper.split("_")
142
+ if safe_kw in parts:
143
+ is_only_safe = True
144
+ for part in parts:
145
+ if part and part not in self.safe_keywords and any(sk in part for sk in self.secret_keywords):
146
+ is_only_safe = False
147
+ break
148
+ if is_only_safe:
149
+ return SecretDetectionResult(
150
+ confidence=SecretConfidence.SAFE,
151
+ reason=f"Key contains safe keyword '{safe_kw}'",
152
+ original_value=value,
153
+ )
154
+
155
+ for secret_kw in self.secret_keywords:
156
+ if secret_kw in key_upper:
157
+ return SecretDetectionResult(
158
+ confidence=SecretConfidence.HIGH,
159
+ reason=f"Key contains secret keyword '{secret_kw}'",
160
+ original_value=value,
161
+ templated_value=self.template_value(key_upper),
162
+ )
163
+
164
+ for ambig_kw in self.ambiguous_keywords:
165
+ if ambig_kw in key_upper:
166
+ if self._contains_credentials_in_url(value):
167
+ return SecretDetectionResult(
168
+ confidence=SecretConfidence.HIGH,
169
+ reason=f"Key contains '{ambig_kw}' with embedded credentials",
170
+ original_value=value,
171
+ templated_value=self.template_value(key_upper),
172
+ )
173
+ entropy = self._calculate_entropy(value)
174
+ if entropy > self.high_entropy_threshold:
175
+ return SecretDetectionResult(
176
+ confidence=SecretConfidence.MEDIUM,
177
+ reason=f"Key contains '{ambig_kw}' with high entropy value",
178
+ original_value=value,
179
+ templated_value=self.template_value(key_upper),
180
+ )
181
+
182
+ return None
183
+
184
+ def _pattern_match(self, value: str, key_upper: str) -> Optional[SecretDetectionResult]:
185
+ """Check for known secret patterns in the value.
186
+
187
+ Args:
188
+ value: The value to analyze
189
+ key_upper: Uppercase key name for templating
190
+
191
+ Returns:
192
+ SecretDetectionResult if pattern matched, None otherwise
193
+ """
194
+ if self._is_boolean_value(value):
195
+ return SecretDetectionResult(
196
+ confidence=SecretConfidence.SAFE,
197
+ reason="Boolean value",
198
+ original_value=value,
199
+ )
200
+
201
+ if self._is_numeric_value(value):
202
+ return SecretDetectionResult(
203
+ confidence=SecretConfidence.SAFE,
204
+ reason="Numeric value",
205
+ original_value=value,
206
+ )
207
+
208
+ if self._is_version_string(value):
209
+ return SecretDetectionResult(
210
+ confidence=SecretConfidence.SAFE,
211
+ reason="Version string",
212
+ original_value=value,
213
+ )
214
+
215
+ if len(value) < self.min_secret_length:
216
+ return SecretDetectionResult(
217
+ confidence=SecretConfidence.SAFE,
218
+ reason=f"Value too short ({len(value)} chars < {self.min_secret_length})",
219
+ original_value=value,
220
+ )
221
+
222
+ if self._matches_api_key_pattern(value):
223
+ return SecretDetectionResult(
224
+ confidence=SecretConfidence.HIGH,
225
+ reason="Matches API key pattern (20+ alphanumeric characters)",
226
+ original_value=value,
227
+ templated_value=self.template_value(key_upper) if key_upper else None,
228
+ )
229
+
230
+ if self._matches_jwt_pattern(value):
231
+ return SecretDetectionResult(
232
+ confidence=SecretConfidence.HIGH,
233
+ reason="Matches JWT token pattern",
234
+ original_value=value,
235
+ templated_value=self.template_value(key_upper) if key_upper else None,
236
+ )
237
+
238
+ if self._matches_base64_secret_pattern(value):
239
+ entropy = self._calculate_entropy(value)
240
+ if entropy > self.high_entropy_threshold:
241
+ return SecretDetectionResult(
242
+ confidence=SecretConfidence.HIGH,
243
+ reason="Matches base64 encoded secret pattern with high entropy",
244
+ original_value=value,
245
+ templated_value=self.template_value(key_upper) if key_upper else None,
246
+ )
247
+
248
+ url_analysis = self._analyze_url(value)
249
+ if url_analysis:
250
+ return url_analysis
251
+
252
+ return None
253
+
254
+ def _entropy_analysis(self, value: str, key_upper: str) -> Optional[SecretDetectionResult]:
255
+ """Analyze value entropy to detect potential secrets.
256
+
257
+ Args:
258
+ value: The value to analyze
259
+ key_upper: Uppercase key name for templating
260
+
261
+ Returns:
262
+ SecretDetectionResult if high entropy detected, None otherwise
263
+ """
264
+ if len(value) < self.min_secret_length:
265
+ return None
266
+
267
+ entropy = self._calculate_entropy(value)
268
+
269
+ if entropy > self.high_entropy_threshold:
270
+ return SecretDetectionResult(
271
+ confidence=SecretConfidence.MEDIUM,
272
+ reason=f"High entropy value ({entropy:.2f} bits/char)",
273
+ original_value=value,
274
+ templated_value=self.template_value(key_upper) if key_upper else None,
275
+ )
276
+
277
+ return None
278
+
279
+ def _calculate_entropy(self, value: str) -> float:
280
+ """Calculate Shannon entropy of a string.
281
+
282
+ Args:
283
+ value: String to analyze
284
+
285
+ Returns:
286
+ Entropy in bits per character
287
+ """
288
+ if not value:
289
+ return 0.0
290
+
291
+ char_counts: dict[str, int] = {}
292
+ for char in value:
293
+ char_counts[char] = char_counts.get(char, 0) + 1
294
+
295
+ entropy = 0.0
296
+ length = len(value)
297
+ for count in char_counts.values():
298
+ if count > 0:
299
+ probability = count / length
300
+ entropy -= probability * math.log2(probability)
301
+
302
+ return entropy
303
+
304
+ def _is_boolean_value(self, value: str) -> bool:
305
+ """Check if value is a boolean."""
306
+ return value.lower() in ("true", "false", "yes", "no", "1", "0", "on", "off")
307
+
308
+ def _is_numeric_value(self, value: str) -> bool:
309
+ """Check if value is purely numeric."""
310
+ try:
311
+ float(value)
312
+ return True
313
+ except ValueError:
314
+ return False
315
+
316
+ def _is_version_string(self, value: str) -> bool:
317
+ """Check if value looks like a version string."""
318
+ version_pattern = r"^v?\d+(\.\d+){0,3}(-[\w.]+)?(\+[\w.]+)?$"
319
+ return bool(re.match(version_pattern, value))
320
+
321
+ def _matches_api_key_pattern(self, value: str) -> bool:
322
+ """Check if value matches common API key patterns."""
323
+ if len(value) < 20:
324
+ return False
325
+ api_key_pattern = r"^[A-Za-z0-9_-]{20,}$"
326
+ return bool(re.match(api_key_pattern, value))
327
+
328
+ def _matches_jwt_pattern(self, value: str) -> bool:
329
+ """Check if value matches JWT token pattern."""
330
+ jwt_pattern = r"^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$"
331
+ return bool(re.match(jwt_pattern, value))
332
+
333
+ def _matches_base64_secret_pattern(self, value: str) -> bool:
334
+ """Check if value looks like a base64-encoded secret."""
335
+ if len(value) < 16:
336
+ return False
337
+ base64_pattern = r"^[A-Za-z0-9+/=]{16,}$"
338
+ if re.match(base64_pattern, value):
339
+ if value.endswith("==") or value.endswith("="):
340
+ return True
341
+ alphanumeric_ratio = sum(c.isalnum() for c in value) / len(value)
342
+ return alphanumeric_ratio > 0.9
343
+ return False
344
+
345
+ def _analyze_url(self, value: str) -> Optional[SecretDetectionResult]:
346
+ """Analyze URL values for embedded credentials.
347
+
348
+ Args:
349
+ value: The value to analyze
350
+
351
+ Returns:
352
+ SecretDetectionResult if URL analysis applies
353
+ """
354
+ url_pattern = r"^https?://"
355
+ if not re.match(url_pattern, value, re.IGNORECASE):
356
+ return None
357
+
358
+ if self._contains_credentials_in_url(value):
359
+ return SecretDetectionResult(
360
+ confidence=SecretConfidence.HIGH,
361
+ reason="URL contains embedded credentials",
362
+ original_value=value,
363
+ )
364
+
365
+ return SecretDetectionResult(
366
+ confidence=SecretConfidence.SAFE,
367
+ reason="URL without embedded credentials",
368
+ original_value=value,
369
+ )
370
+
371
+ def _contains_credentials_in_url(self, value: str) -> bool:
372
+ """Check if URL contains embedded credentials (user:pass@)."""
373
+ cred_pattern = r"https?://[^/]+:[^@]+@"
374
+ return bool(re.match(cred_pattern, value, re.IGNORECASE))
375
+
376
+
377
+ def template_secrets_in_config(
378
+ config: dict[str, Any], detector: Optional[SecretDetector] = None
379
+ ) -> tuple[dict[str, Any], list[str]]:
380
+ """Process a configuration dict and template detected secrets.
381
+
382
+ Args:
383
+ config: Configuration dictionary (e.g., MCP server config)
384
+ detector: SecretDetector instance (creates default if None)
385
+
386
+ Returns:
387
+ Tuple of (templated config, list of templated key names)
388
+ """
389
+ if detector is None:
390
+ detector = SecretDetector()
391
+
392
+ templated_keys: list[str] = []
393
+ result = _template_dict_recursive(config, detector, templated_keys, "")
394
+
395
+ # We know the result is a dict since config is a dict
396
+ assert isinstance(result, dict)
397
+ return result, templated_keys
398
+
399
+
400
+ def _template_dict_recursive(
401
+ obj: Any,
402
+ detector: SecretDetector,
403
+ templated_keys: list[str],
404
+ current_path: str,
405
+ ) -> Any:
406
+ """Recursively process nested structures for secret templating.
407
+
408
+ Args:
409
+ obj: Object to process
410
+ detector: SecretDetector instance
411
+ templated_keys: List to append templated key names
412
+ current_path: Current key path for context
413
+
414
+ Returns:
415
+ Processed object with secrets templated
416
+ """
417
+ if isinstance(obj, dict):
418
+ result: dict[str, Any] = {}
419
+ for key, value in obj.items():
420
+ path = f"{current_path}.{key}" if current_path else key
421
+ if isinstance(value, str):
422
+ detection = detector.detect(value, key)
423
+ if detection.confidence in (SecretConfidence.HIGH, SecretConfidence.MEDIUM):
424
+ if detection.templated_value:
425
+ result[key] = detection.templated_value
426
+ templated_keys.append(key)
427
+ else:
428
+ result[key] = detector.template_value(key)
429
+ templated_keys.append(key)
430
+ else:
431
+ result[key] = value
432
+ else:
433
+ result[key] = _template_dict_recursive(value, detector, templated_keys, path)
434
+ return result
435
+ elif isinstance(obj, list):
436
+ return [_template_dict_recursive(item, detector, templated_keys, current_path) for item in obj]
437
+ else:
438
+ return obj