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.
- aiconfigkit/__init__.py +0 -0
- aiconfigkit/__main__.py +6 -0
- aiconfigkit/ai_tools/__init__.py +0 -0
- aiconfigkit/ai_tools/base.py +236 -0
- aiconfigkit/ai_tools/capability_registry.py +262 -0
- aiconfigkit/ai_tools/claude.py +91 -0
- aiconfigkit/ai_tools/claude_desktop.py +97 -0
- aiconfigkit/ai_tools/cline.py +92 -0
- aiconfigkit/ai_tools/copilot.py +92 -0
- aiconfigkit/ai_tools/cursor.py +109 -0
- aiconfigkit/ai_tools/detector.py +169 -0
- aiconfigkit/ai_tools/kiro.py +85 -0
- aiconfigkit/ai_tools/mcp_syncer.py +291 -0
- aiconfigkit/ai_tools/roo.py +110 -0
- aiconfigkit/ai_tools/translator.py +390 -0
- aiconfigkit/ai_tools/winsurf.py +102 -0
- aiconfigkit/cli/__init__.py +0 -0
- aiconfigkit/cli/delete.py +118 -0
- aiconfigkit/cli/download.py +274 -0
- aiconfigkit/cli/install.py +237 -0
- aiconfigkit/cli/install_new.py +937 -0
- aiconfigkit/cli/list.py +275 -0
- aiconfigkit/cli/main.py +454 -0
- aiconfigkit/cli/mcp_configure.py +232 -0
- aiconfigkit/cli/mcp_install.py +166 -0
- aiconfigkit/cli/mcp_sync.py +165 -0
- aiconfigkit/cli/package.py +383 -0
- aiconfigkit/cli/package_create.py +323 -0
- aiconfigkit/cli/package_install.py +472 -0
- aiconfigkit/cli/template.py +19 -0
- aiconfigkit/cli/template_backup.py +261 -0
- aiconfigkit/cli/template_init.py +499 -0
- aiconfigkit/cli/template_install.py +261 -0
- aiconfigkit/cli/template_list.py +172 -0
- aiconfigkit/cli/template_uninstall.py +146 -0
- aiconfigkit/cli/template_update.py +225 -0
- aiconfigkit/cli/template_validate.py +234 -0
- aiconfigkit/cli/tools.py +47 -0
- aiconfigkit/cli/uninstall.py +125 -0
- aiconfigkit/cli/update.py +309 -0
- aiconfigkit/core/__init__.py +0 -0
- aiconfigkit/core/checksum.py +211 -0
- aiconfigkit/core/component_detector.py +905 -0
- aiconfigkit/core/conflict_resolution.py +329 -0
- aiconfigkit/core/git_operations.py +539 -0
- aiconfigkit/core/mcp/__init__.py +1 -0
- aiconfigkit/core/mcp/credentials.py +279 -0
- aiconfigkit/core/mcp/manager.py +308 -0
- aiconfigkit/core/mcp/set_manager.py +1 -0
- aiconfigkit/core/mcp/validator.py +1 -0
- aiconfigkit/core/models.py +1661 -0
- aiconfigkit/core/package_creator.py +743 -0
- aiconfigkit/core/package_manifest.py +248 -0
- aiconfigkit/core/repository.py +298 -0
- aiconfigkit/core/secret_detector.py +438 -0
- aiconfigkit/core/template_manifest.py +283 -0
- aiconfigkit/core/version.py +201 -0
- aiconfigkit/storage/__init__.py +0 -0
- aiconfigkit/storage/library.py +429 -0
- aiconfigkit/storage/mcp_tracker.py +1 -0
- aiconfigkit/storage/package_tracker.py +234 -0
- aiconfigkit/storage/template_library.py +229 -0
- aiconfigkit/storage/template_tracker.py +296 -0
- aiconfigkit/storage/tracker.py +416 -0
- aiconfigkit/tui/__init__.py +5 -0
- aiconfigkit/tui/installer.py +511 -0
- aiconfigkit/utils/__init__.py +0 -0
- aiconfigkit/utils/atomic_write.py +90 -0
- aiconfigkit/utils/backup.py +169 -0
- aiconfigkit/utils/dotenv.py +128 -0
- aiconfigkit/utils/git_helpers.py +187 -0
- aiconfigkit/utils/logging.py +60 -0
- aiconfigkit/utils/namespace.py +134 -0
- aiconfigkit/utils/paths.py +205 -0
- aiconfigkit/utils/project.py +109 -0
- aiconfigkit/utils/streaming.py +216 -0
- aiconfigkit/utils/ui.py +194 -0
- aiconfigkit/utils/validation.py +187 -0
- devsync-0.5.5.dist-info/LICENSE +21 -0
- devsync-0.5.5.dist-info/METADATA +477 -0
- devsync-0.5.5.dist-info/RECORD +84 -0
- devsync-0.5.5.dist-info/WHEEL +5 -0
- devsync-0.5.5.dist-info/entry_points.txt +2 -0
- 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
|