ic-code 1.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.
- ic/__init__.py +29 -0
- ic/cli.py +769 -0
- ic/commands/__init__.py +10 -0
- ic/commands/config.py +699 -0
- ic/compat/__init__.py +241 -0
- ic/compat/cli.py +289 -0
- ic/compat/common.py +243 -0
- ic/config/__init__.py +10 -0
- ic/config/cleanup.py +382 -0
- ic/config/docs_organizer.py +587 -0
- ic/config/external.py +456 -0
- ic/config/manager.py +898 -0
- ic/config/migration.py +628 -0
- ic/config/schema.py +595 -0
- ic/config/secrets.py +437 -0
- ic/config/security.py +462 -0
- ic/core/__init__.py +16 -0
- ic/core/logging.py +311 -0
- ic/core/mcp_manager.py +856 -0
- ic/core/session.py +392 -0
- ic/core/silence_logging.py +67 -0
- ic_code-1.0.0.dist-info/METADATA +354 -0
- ic_code-1.0.0.dist-info/RECORD +27 -0
- ic_code-1.0.0.dist-info/WHEEL +5 -0
- ic_code-1.0.0.dist-info/entry_points.txt +2 -0
- ic_code-1.0.0.dist-info/licenses/LICENSE +21 -0
- ic_code-1.0.0.dist-info/top_level.txt +1 -0
ic/config/secrets.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secrets management module for IC.
|
|
3
|
+
|
|
4
|
+
This module provides secure handling of sensitive configuration data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, List, Optional, Union
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SecretsManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages sensitive configuration data with security validation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config_manager=None):
|
|
22
|
+
"""
|
|
23
|
+
Initialize SecretsManager.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config_manager: Reference to ConfigManager instance
|
|
27
|
+
"""
|
|
28
|
+
self.config_manager = config_manager
|
|
29
|
+
self.secrets_data: Dict[str, Any] = {}
|
|
30
|
+
|
|
31
|
+
# Define sensitive key patterns
|
|
32
|
+
self.sensitive_patterns = [
|
|
33
|
+
r'.*password.*',
|
|
34
|
+
r'.*passwd.*',
|
|
35
|
+
r'.*pwd.*',
|
|
36
|
+
r'.*token.*',
|
|
37
|
+
r'.*key.*',
|
|
38
|
+
r'.*secret.*',
|
|
39
|
+
r'.*credential.*',
|
|
40
|
+
r'.*webhook.*',
|
|
41
|
+
r'.*api_key.*',
|
|
42
|
+
r'.*access_key.*',
|
|
43
|
+
r'.*private_key.*',
|
|
44
|
+
r'.*client_secret.*',
|
|
45
|
+
r'.*tenant_id.*',
|
|
46
|
+
r'.*client_id.*',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Compile patterns for performance
|
|
50
|
+
self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.sensitive_patterns]
|
|
51
|
+
|
|
52
|
+
def load_secrets(self) -> Dict[str, Any]:
|
|
53
|
+
"""
|
|
54
|
+
Load secrets from config/secrets.yaml with environment variable fallback.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dictionary containing sensitive configuration data
|
|
58
|
+
"""
|
|
59
|
+
secrets = {}
|
|
60
|
+
|
|
61
|
+
# Try to load from secrets.yaml file first
|
|
62
|
+
secrets_path = Path("config/secrets.yaml")
|
|
63
|
+
if secrets_path.exists():
|
|
64
|
+
try:
|
|
65
|
+
secrets = self._load_secrets_file(secrets_path)
|
|
66
|
+
logger.debug("Loaded secrets from config/secrets.yaml")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning(f"Failed to load secrets from {secrets_path}: {e}")
|
|
69
|
+
|
|
70
|
+
# Fallback to environment variables
|
|
71
|
+
env_secrets = self._load_secrets_from_env()
|
|
72
|
+
if env_secrets:
|
|
73
|
+
secrets = self._merge_secrets(secrets, env_secrets)
|
|
74
|
+
logger.debug("Merged secrets from environment variables")
|
|
75
|
+
|
|
76
|
+
# Validate that secrets are properly separated (log to file only)
|
|
77
|
+
validation_warnings = self.validate_secrets_separation(secrets)
|
|
78
|
+
if validation_warnings:
|
|
79
|
+
for warning in validation_warnings:
|
|
80
|
+
logger.debug(f"Secrets validation: {warning}") # Changed to debug level
|
|
81
|
+
|
|
82
|
+
self.secrets_data = secrets
|
|
83
|
+
return secrets
|
|
84
|
+
|
|
85
|
+
def _load_secrets_file(self, secrets_path: Path) -> Dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Load secrets from YAML file.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
secrets_path: Path to secrets.yaml file
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dictionary containing secrets
|
|
94
|
+
"""
|
|
95
|
+
import yaml
|
|
96
|
+
|
|
97
|
+
with open(secrets_path, 'r', encoding='utf-8') as f:
|
|
98
|
+
secrets = yaml.safe_load(f) or {}
|
|
99
|
+
|
|
100
|
+
# Validate file permissions
|
|
101
|
+
try:
|
|
102
|
+
file_mode = secrets_path.stat().st_mode & 0o777
|
|
103
|
+
if file_mode != 0o600:
|
|
104
|
+
logger.warning(f"Secrets file {secrets_path} has insecure permissions {oct(file_mode)}. "
|
|
105
|
+
f"Consider setting permissions to 600 (owner read/write only)")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.debug(f"Could not check file permissions: {e}")
|
|
108
|
+
|
|
109
|
+
return secrets
|
|
110
|
+
|
|
111
|
+
def _load_secrets_from_env(self) -> Dict[str, Any]:
|
|
112
|
+
"""
|
|
113
|
+
Load sensitive configuration from environment variables.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dictionary containing environment-based secrets
|
|
117
|
+
"""
|
|
118
|
+
env_secrets = {}
|
|
119
|
+
|
|
120
|
+
# AWS secrets
|
|
121
|
+
aws_accounts = os.getenv('AWS_ACCOUNTS')
|
|
122
|
+
if aws_accounts:
|
|
123
|
+
env_secrets.setdefault('aws', {})['accounts'] = [
|
|
124
|
+
acc.strip() for acc in aws_accounts.split(',') if acc.strip()
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
# CloudFlare secrets
|
|
128
|
+
cf_secrets = {}
|
|
129
|
+
cf_email = os.getenv('CLOUDFLARE_EMAIL')
|
|
130
|
+
cf_token = os.getenv('CLOUDFLARE_API_TOKEN')
|
|
131
|
+
cf_accounts = os.getenv('CLOUDFLARE_ACCOUNTS')
|
|
132
|
+
cf_zones = os.getenv('CLOUDFLARE_ZONES')
|
|
133
|
+
|
|
134
|
+
if cf_email:
|
|
135
|
+
cf_secrets['email'] = cf_email
|
|
136
|
+
if cf_token:
|
|
137
|
+
cf_secrets['api_token'] = cf_token
|
|
138
|
+
if cf_accounts:
|
|
139
|
+
cf_secrets['accounts'] = [acc.strip() for acc in cf_accounts.split(',') if acc.strip()]
|
|
140
|
+
if cf_zones:
|
|
141
|
+
cf_secrets['zones'] = [zone.strip() for zone in cf_zones.split(',') if zone.strip()]
|
|
142
|
+
|
|
143
|
+
if cf_secrets:
|
|
144
|
+
env_secrets['cloudflare'] = cf_secrets
|
|
145
|
+
|
|
146
|
+
# GCP secrets
|
|
147
|
+
gcp_secrets = {}
|
|
148
|
+
gcp_key_path = os.getenv('GCP_SERVICE_ACCOUNT_KEY_PATH') or os.getenv('GOOGLE_APPLICATION_CREDENTIALS')
|
|
149
|
+
gcp_projects = os.getenv('GCP_PROJECTS')
|
|
150
|
+
|
|
151
|
+
if gcp_key_path:
|
|
152
|
+
gcp_secrets['service_account_key_path'] = gcp_key_path
|
|
153
|
+
if gcp_projects:
|
|
154
|
+
gcp_secrets['projects'] = [proj.strip() for proj in gcp_projects.split(',') if proj.strip()]
|
|
155
|
+
|
|
156
|
+
if gcp_secrets:
|
|
157
|
+
env_secrets['gcp'] = gcp_secrets
|
|
158
|
+
|
|
159
|
+
# Azure secrets
|
|
160
|
+
azure_secrets = {}
|
|
161
|
+
azure_tenant = os.getenv('AZURE_TENANT_ID')
|
|
162
|
+
azure_client_id = os.getenv('AZURE_CLIENT_ID')
|
|
163
|
+
azure_client_secret = os.getenv('AZURE_CLIENT_SECRET')
|
|
164
|
+
azure_subscriptions = os.getenv('AZURE_SUBSCRIPTIONS')
|
|
165
|
+
|
|
166
|
+
if azure_tenant:
|
|
167
|
+
azure_secrets['tenant_id'] = azure_tenant
|
|
168
|
+
if azure_client_id:
|
|
169
|
+
azure_secrets['client_id'] = azure_client_id
|
|
170
|
+
if azure_client_secret:
|
|
171
|
+
azure_secrets['client_secret'] = azure_client_secret
|
|
172
|
+
if azure_subscriptions:
|
|
173
|
+
azure_secrets['subscriptions'] = [sub.strip() for sub in azure_subscriptions.split(',') if sub.strip()]
|
|
174
|
+
|
|
175
|
+
if azure_secrets:
|
|
176
|
+
env_secrets['azure'] = azure_secrets
|
|
177
|
+
|
|
178
|
+
# Slack secrets
|
|
179
|
+
slack_webhook = os.getenv('SLACK_WEBHOOK_URL')
|
|
180
|
+
if slack_webhook:
|
|
181
|
+
env_secrets['slack'] = {'webhook_url': slack_webhook}
|
|
182
|
+
|
|
183
|
+
return env_secrets
|
|
184
|
+
|
|
185
|
+
def _merge_secrets(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
186
|
+
"""
|
|
187
|
+
Merge two secrets dictionaries.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
base: Base secrets dictionary
|
|
191
|
+
override: Override secrets dictionary
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Merged secrets dictionary
|
|
195
|
+
"""
|
|
196
|
+
result = base.copy()
|
|
197
|
+
|
|
198
|
+
for key, value in override.items():
|
|
199
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
200
|
+
result[key] = self._merge_secrets(result[key], value)
|
|
201
|
+
else:
|
|
202
|
+
result[key] = value
|
|
203
|
+
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
def validate_secrets_separation(self, config: Dict[str, Any]) -> List[str]:
|
|
207
|
+
"""
|
|
208
|
+
Validate that sensitive information is properly separated from general config.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
config: Configuration dictionary to validate
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of validation warnings
|
|
215
|
+
"""
|
|
216
|
+
warnings = []
|
|
217
|
+
|
|
218
|
+
# Check if any sensitive data appears in the general config
|
|
219
|
+
sensitive_keys_found = self._find_sensitive_keys(config)
|
|
220
|
+
|
|
221
|
+
for key_path in sensitive_keys_found:
|
|
222
|
+
warnings.append(f"Potentially sensitive key '{key_path}' found in general configuration. "
|
|
223
|
+
f"Consider moving to secrets.yaml")
|
|
224
|
+
|
|
225
|
+
return warnings
|
|
226
|
+
|
|
227
|
+
def _find_sensitive_keys(self, data: Any, path: str = "") -> List[str]:
|
|
228
|
+
"""
|
|
229
|
+
Recursively find keys that match sensitive patterns.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
data: Data to search
|
|
233
|
+
path: Current path in the data structure
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of paths to sensitive keys
|
|
237
|
+
"""
|
|
238
|
+
sensitive_keys = []
|
|
239
|
+
|
|
240
|
+
if isinstance(data, dict):
|
|
241
|
+
for key, value in data.items():
|
|
242
|
+
current_path = f"{path}.{key}" if path else key
|
|
243
|
+
|
|
244
|
+
# Check if key matches sensitive patterns
|
|
245
|
+
if self._is_sensitive_key(key):
|
|
246
|
+
sensitive_keys.append(current_path)
|
|
247
|
+
|
|
248
|
+
# Recursively check nested structures
|
|
249
|
+
sensitive_keys.extend(self._find_sensitive_keys(value, current_path))
|
|
250
|
+
|
|
251
|
+
elif isinstance(data, list):
|
|
252
|
+
for i, item in enumerate(data):
|
|
253
|
+
current_path = f"{path}[{i}]" if path else f"[{i}]"
|
|
254
|
+
sensitive_keys.extend(self._find_sensitive_keys(item, current_path))
|
|
255
|
+
|
|
256
|
+
return sensitive_keys
|
|
257
|
+
|
|
258
|
+
def _is_sensitive_key(self, key: str) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
Check if a key matches sensitive patterns.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
key: Key to check
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
True if key is potentially sensitive
|
|
267
|
+
"""
|
|
268
|
+
return any(pattern.match(key) for pattern in self.compiled_patterns)
|
|
269
|
+
|
|
270
|
+
def mask_sensitive_values(self, data: Any) -> Any:
|
|
271
|
+
"""
|
|
272
|
+
Recursively mask sensitive values in data structure.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
data: Data structure to mask
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Data structure with sensitive values masked
|
|
279
|
+
"""
|
|
280
|
+
if isinstance(data, dict):
|
|
281
|
+
masked_data = {}
|
|
282
|
+
for key, value in data.items():
|
|
283
|
+
if self._is_sensitive_key(key) and isinstance(value, str) and value:
|
|
284
|
+
# Mask the value but show first and last few characters for identification
|
|
285
|
+
if len(value) > 8:
|
|
286
|
+
masked_data[key] = f"{value[:3]}***{value[-3:]}"
|
|
287
|
+
else:
|
|
288
|
+
masked_data[key] = "***MASKED***"
|
|
289
|
+
else:
|
|
290
|
+
masked_data[key] = self.mask_sensitive_values(value)
|
|
291
|
+
return masked_data
|
|
292
|
+
|
|
293
|
+
elif isinstance(data, list):
|
|
294
|
+
return [self.mask_sensitive_values(item) for item in data]
|
|
295
|
+
|
|
296
|
+
else:
|
|
297
|
+
return data
|
|
298
|
+
|
|
299
|
+
def get_secret_value(self, key_path: str, default: Any = None) -> Any:
|
|
300
|
+
"""
|
|
301
|
+
Get a secret value using dot notation.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
key_path: Dot-separated path to secret value (e.g., 'aws.accounts')
|
|
305
|
+
default: Default value if key is not found
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Secret value or default
|
|
309
|
+
"""
|
|
310
|
+
keys = key_path.split('.')
|
|
311
|
+
current = self.secrets_data
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
for key in keys:
|
|
315
|
+
current = current[key]
|
|
316
|
+
return current
|
|
317
|
+
except (KeyError, TypeError):
|
|
318
|
+
return default
|
|
319
|
+
|
|
320
|
+
def has_secret(self, key_path: str) -> bool:
|
|
321
|
+
"""
|
|
322
|
+
Check if a secret exists and has a non-empty value.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
key_path: Dot-separated path to secret value
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
True if secret exists and is not empty
|
|
329
|
+
"""
|
|
330
|
+
value = self.get_secret_value(key_path)
|
|
331
|
+
return value is not None and value != "" and value != []
|
|
332
|
+
|
|
333
|
+
def create_secrets_template(self, output_path: Union[str, Path] = "config/secrets.yaml.template") -> bool:
|
|
334
|
+
"""
|
|
335
|
+
Create a template secrets file with empty values.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
output_path: Path where to create the template
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
True if template was created successfully
|
|
342
|
+
"""
|
|
343
|
+
template_content = '''# IC Secrets Configuration Template
|
|
344
|
+
# Copy this file to secrets.yaml and fill in your sensitive values
|
|
345
|
+
# File permissions should be set to 600 (readable only by owner)
|
|
346
|
+
|
|
347
|
+
version: "2.0"
|
|
348
|
+
|
|
349
|
+
# AWS sensitive configuration
|
|
350
|
+
aws:
|
|
351
|
+
accounts: [] # Add your AWS account IDs here, e.g., ["123456789012", "987654321098"]
|
|
352
|
+
|
|
353
|
+
# CloudFlare sensitive configuration
|
|
354
|
+
cloudflare:
|
|
355
|
+
email: "" # Your CloudFlare email
|
|
356
|
+
api_token: "" # Your CloudFlare API token
|
|
357
|
+
accounts: [] # Your CloudFlare account names
|
|
358
|
+
zones: [] # Your CloudFlare zone names
|
|
359
|
+
|
|
360
|
+
# GCP sensitive configuration
|
|
361
|
+
gcp:
|
|
362
|
+
service_account_key_path: "" # Path to your GCP service account key
|
|
363
|
+
projects: [] # Your GCP project IDs
|
|
364
|
+
|
|
365
|
+
# Azure sensitive configuration
|
|
366
|
+
azure:
|
|
367
|
+
tenant_id: "" # Your Azure tenant ID
|
|
368
|
+
client_id: "" # Your Azure client ID
|
|
369
|
+
client_secret: "" # Your Azure client secret
|
|
370
|
+
subscriptions: [] # Your Azure subscription IDs
|
|
371
|
+
|
|
372
|
+
# Slack integration
|
|
373
|
+
slack:
|
|
374
|
+
webhook_url: "" # Your Slack webhook URL
|
|
375
|
+
|
|
376
|
+
# Note: If this file doesn't exist or values are empty,
|
|
377
|
+
# the system will fall back to environment variables
|
|
378
|
+
'''
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
output_path = Path(output_path)
|
|
382
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
383
|
+
|
|
384
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
385
|
+
f.write(template_content)
|
|
386
|
+
|
|
387
|
+
logger.info(f"Created secrets template at {output_path}")
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logger.error(f"Failed to create secrets template: {e}")
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
def validate_secrets_file_security(self, secrets_path: Union[str, Path]) -> List[str]:
|
|
395
|
+
"""
|
|
396
|
+
Validate security aspects of secrets file.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
secrets_path: Path to secrets file
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
List of security warnings
|
|
403
|
+
"""
|
|
404
|
+
warnings = []
|
|
405
|
+
secrets_path = Path(secrets_path)
|
|
406
|
+
|
|
407
|
+
if not secrets_path.exists():
|
|
408
|
+
return warnings
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
# Check file permissions
|
|
412
|
+
file_mode = secrets_path.stat().st_mode & 0o777
|
|
413
|
+
if file_mode & 0o077: # Check if group or others have any permissions
|
|
414
|
+
warnings.append(f"Secrets file {secrets_path} is readable by group/others. "
|
|
415
|
+
f"Current permissions: {oct(file_mode)}. "
|
|
416
|
+
f"Recommended: 600 (owner read/write only)")
|
|
417
|
+
|
|
418
|
+
# Check if file is in version control (basic check for .git directory)
|
|
419
|
+
git_dir = secrets_path.parent
|
|
420
|
+
while git_dir != git_dir.parent:
|
|
421
|
+
if (git_dir / ".git").exists():
|
|
422
|
+
gitignore_path = git_dir / ".gitignore"
|
|
423
|
+
if gitignore_path.exists():
|
|
424
|
+
with open(gitignore_path, 'r') as f:
|
|
425
|
+
gitignore_content = f.read()
|
|
426
|
+
if "secrets.yaml" not in gitignore_content:
|
|
427
|
+
warnings.append("secrets.yaml should be added to .gitignore to prevent "
|
|
428
|
+
"accidental commit of sensitive data")
|
|
429
|
+
else:
|
|
430
|
+
warnings.append("Consider creating .gitignore and adding secrets.yaml to it")
|
|
431
|
+
break
|
|
432
|
+
git_dir = git_dir.parent
|
|
433
|
+
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.debug(f"Could not validate secrets file security: {e}")
|
|
436
|
+
|
|
437
|
+
return warnings
|