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/security.py
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security management module for IC.
|
|
3
|
+
|
|
4
|
+
This module provides security utilities including sensitive data detection,
|
|
5
|
+
masking, and Git security validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Dict, List, Any, Optional, Union
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SecurityManager:
|
|
20
|
+
"""
|
|
21
|
+
Manages security features including sensitive data detection and masking.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
25
|
+
"""
|
|
26
|
+
Initialize SecurityManager with configuration.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config: Security configuration dictionary
|
|
30
|
+
"""
|
|
31
|
+
self.config = config or {}
|
|
32
|
+
self.sensitive_keys = self.config.get('sensitive_keys', [
|
|
33
|
+
"password", "passwd", "pwd",
|
|
34
|
+
"token", "access_token", "refresh_token", "auth_token",
|
|
35
|
+
"key", "api_key", "access_key", "secret_key", "private_key",
|
|
36
|
+
"secret", "client_secret", "webhook_secret",
|
|
37
|
+
"webhook_url", "webhook",
|
|
38
|
+
"credential", "credentials",
|
|
39
|
+
"cert", "certificate",
|
|
40
|
+
"session", "session_token",
|
|
41
|
+
])
|
|
42
|
+
self.mask_pattern = self.config.get('mask_pattern', '***MASKED***')
|
|
43
|
+
self.warn_on_sensitive = self.config.get('warn_on_sensitive_in_config', True)
|
|
44
|
+
|
|
45
|
+
def mask_sensitive_data(self, data: Any) -> Any:
|
|
46
|
+
"""
|
|
47
|
+
Recursively mask sensitive data in dictionaries, lists, and strings.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
data: Data to mask (dict, list, str, or other)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Data with sensitive information masked
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(data, dict):
|
|
56
|
+
masked = {}
|
|
57
|
+
for key, value in data.items():
|
|
58
|
+
if self._is_sensitive_key(key):
|
|
59
|
+
masked[key] = self.mask_pattern
|
|
60
|
+
else:
|
|
61
|
+
masked[key] = self.mask_sensitive_data(value)
|
|
62
|
+
return masked
|
|
63
|
+
elif isinstance(data, list):
|
|
64
|
+
return [self.mask_sensitive_data(item) for item in data]
|
|
65
|
+
elif isinstance(data, str):
|
|
66
|
+
if self._looks_like_secret(data):
|
|
67
|
+
return self.mask_pattern
|
|
68
|
+
else:
|
|
69
|
+
return data
|
|
70
|
+
else:
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
def _is_sensitive_key(self, key: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Check if a key name indicates sensitive data.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
key: Key name to check
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if key appears to contain sensitive data
|
|
82
|
+
"""
|
|
83
|
+
key_lower = key.lower()
|
|
84
|
+
return any(sensitive in key_lower for sensitive in self.sensitive_keys)
|
|
85
|
+
|
|
86
|
+
def _looks_like_secret(self, value: str) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Heuristic to detect secret-like strings.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
value: String value to check
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if value looks like a secret
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(value, str) or len(value) < 10:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
# Check for common secret patterns
|
|
100
|
+
secret_patterns = [
|
|
101
|
+
r'^[A-Za-z0-9+/]{40,}={0,2}$', # Base64-like
|
|
102
|
+
r'^[A-Fa-f0-9]{32,}$', # Hex strings
|
|
103
|
+
r'^[A-Za-z0-9_-]{20,}$', # API keys
|
|
104
|
+
r'^sk-[A-Za-z0-9]{32,}$', # OpenAI-style keys
|
|
105
|
+
r'^xoxb-[A-Za-z0-9-]{50,}$', # Slack bot tokens
|
|
106
|
+
r'^ghp_[A-Za-z0-9]{36}$', # GitHub personal access tokens
|
|
107
|
+
r'^gho_[A-Za-z0-9]{36}$', # GitHub OAuth tokens
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
return any(re.match(pattern, value) for pattern in secret_patterns)
|
|
111
|
+
|
|
112
|
+
def validate_config_security(self, config_data: Dict[str, Any]) -> List[str]:
|
|
113
|
+
"""
|
|
114
|
+
Validate configuration for security issues.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
config_data: Configuration data to validate
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of security warnings
|
|
121
|
+
"""
|
|
122
|
+
warnings = []
|
|
123
|
+
|
|
124
|
+
def check_node(data: Any, path: str = "") -> None:
|
|
125
|
+
if isinstance(data, dict):
|
|
126
|
+
for key, value in data.items():
|
|
127
|
+
current_path = f"{path}.{key}" if path else key
|
|
128
|
+
if self._is_sensitive_key(key) and isinstance(value, str) and value:
|
|
129
|
+
if not self._is_placeholder_value(value):
|
|
130
|
+
warnings.append(
|
|
131
|
+
f"Sensitive data found in config at '{current_path}'. "
|
|
132
|
+
f"Consider using environment variables instead."
|
|
133
|
+
)
|
|
134
|
+
check_node(value, current_path)
|
|
135
|
+
elif isinstance(data, list):
|
|
136
|
+
for i, item in enumerate(data):
|
|
137
|
+
check_node(item, f"{path}[{i}]")
|
|
138
|
+
elif isinstance(data, str) and self._looks_like_secret(data):
|
|
139
|
+
warnings.append(
|
|
140
|
+
f"Potential secret found at '{path}'. "
|
|
141
|
+
f"Consider using environment variables instead."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
check_node(config_data)
|
|
145
|
+
return warnings
|
|
146
|
+
|
|
147
|
+
def _is_placeholder_value(self, value: str) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Check if a value is a placeholder (safe for config files).
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
value: Value to check
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
True if value appears to be a placeholder
|
|
156
|
+
"""
|
|
157
|
+
placeholder_patterns = [
|
|
158
|
+
r'^your-.*-here$',
|
|
159
|
+
r'^<.*>$',
|
|
160
|
+
r'^\[.*\]$',
|
|
161
|
+
r'^REPLACE_.*$',
|
|
162
|
+
r'^TODO:.*$',
|
|
163
|
+
r'^CHANGE_.*$',
|
|
164
|
+
r'^example.*$',
|
|
165
|
+
r'^placeholder.*$',
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
value_lower = value.lower()
|
|
169
|
+
return any(re.match(pattern, value_lower) for pattern in placeholder_patterns)
|
|
170
|
+
|
|
171
|
+
def create_gitignore_entries(self) -> List[str]:
|
|
172
|
+
"""
|
|
173
|
+
Generate .gitignore entries for security.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
List of .gitignore entries
|
|
177
|
+
"""
|
|
178
|
+
return [
|
|
179
|
+
"# IC Configuration - Security",
|
|
180
|
+
"config.yaml",
|
|
181
|
+
"config.yml",
|
|
182
|
+
".env",
|
|
183
|
+
".env.*",
|
|
184
|
+
"*.key",
|
|
185
|
+
"*.pem",
|
|
186
|
+
"**/credentials.json",
|
|
187
|
+
"**/service-account*.json",
|
|
188
|
+
"logs/",
|
|
189
|
+
".ic/",
|
|
190
|
+
"",
|
|
191
|
+
"# AWS credentials",
|
|
192
|
+
".aws/credentials",
|
|
193
|
+
"aws-key/",
|
|
194
|
+
"",
|
|
195
|
+
"# GCP credentials",
|
|
196
|
+
"gcp-key/",
|
|
197
|
+
"**/gcp-*.json",
|
|
198
|
+
"service-account*.json",
|
|
199
|
+
"*-key.json",
|
|
200
|
+
"",
|
|
201
|
+
"# Azure credentials",
|
|
202
|
+
".azure/",
|
|
203
|
+
"*.pfx",
|
|
204
|
+
"*.p12",
|
|
205
|
+
"",
|
|
206
|
+
"# OCI credentials",
|
|
207
|
+
".oci/config",
|
|
208
|
+
".oci/sessions/",
|
|
209
|
+
"",
|
|
210
|
+
"# SSH keys",
|
|
211
|
+
"id_rsa*",
|
|
212
|
+
"*.ppk",
|
|
213
|
+
"",
|
|
214
|
+
"# CloudFlare credentials",
|
|
215
|
+
".cloudflare/",
|
|
216
|
+
"",
|
|
217
|
+
"# Temporary files",
|
|
218
|
+
"*.tmp",
|
|
219
|
+
"*.temp",
|
|
220
|
+
"*.bak",
|
|
221
|
+
".DS_Store",
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
def mask_log_message(self, message: str) -> str:
|
|
225
|
+
"""
|
|
226
|
+
Mask sensitive data in log messages.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
message: Log message to mask
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Masked log message
|
|
233
|
+
"""
|
|
234
|
+
# Mask common credential patterns in log messages
|
|
235
|
+
patterns = [
|
|
236
|
+
(r'(password|passwd|pwd)[\s=:]+[^\s]+', r'\1=' + self.mask_pattern),
|
|
237
|
+
(r'(token|key)[\s=:]+[^\s]+', r'\1=' + self.mask_pattern),
|
|
238
|
+
(r'(secret)[\s=:]+[^\s]+', r'\1=' + self.mask_pattern),
|
|
239
|
+
(r'Bearer\s+[A-Za-z0-9\-._~+/]+=*', f'Bearer {self.mask_pattern}'),
|
|
240
|
+
(r'Basic\s+[A-Za-z0-9+/]+=*', f'Basic {self.mask_pattern}'),
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
masked_message = message
|
|
244
|
+
for pattern, replacement in patterns:
|
|
245
|
+
masked_message = re.sub(pattern, replacement, masked_message, flags=re.IGNORECASE)
|
|
246
|
+
|
|
247
|
+
return masked_message
|
|
248
|
+
|
|
249
|
+
def mask_sensitive_in_text(self, text: str) -> str:
|
|
250
|
+
"""
|
|
251
|
+
Mask sensitive data in text strings (alias for mask_log_message).
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
text: Text to mask
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Text with sensitive information masked
|
|
258
|
+
"""
|
|
259
|
+
return self.mask_log_message(text)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class GitSecurityChecker:
|
|
263
|
+
"""
|
|
264
|
+
Git security validation and pre-commit hooks.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def __init__(self, security_manager: SecurityManager):
|
|
268
|
+
"""
|
|
269
|
+
Initialize GitSecurityChecker.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
security_manager: SecurityManager instance
|
|
273
|
+
"""
|
|
274
|
+
self.security = security_manager
|
|
275
|
+
|
|
276
|
+
def check_staged_files(self) -> List[str]:
|
|
277
|
+
"""
|
|
278
|
+
Check staged files for sensitive data before commit.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of security warnings
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
# Get staged files
|
|
285
|
+
result = subprocess.run(
|
|
286
|
+
['git', 'diff', '--cached', '--name-only'],
|
|
287
|
+
capture_output=True, text=True, check=True
|
|
288
|
+
)
|
|
289
|
+
staged_files = [f for f in result.stdout.strip().split('\n') if f]
|
|
290
|
+
|
|
291
|
+
warnings = []
|
|
292
|
+
for file_path in staged_files:
|
|
293
|
+
if file_path and self._should_check_file(file_path):
|
|
294
|
+
file_warnings = self._check_file_content(file_path)
|
|
295
|
+
warnings.extend(file_warnings)
|
|
296
|
+
|
|
297
|
+
return warnings
|
|
298
|
+
except subprocess.CalledProcessError:
|
|
299
|
+
logger.debug("Could not check staged files (not in git repository)")
|
|
300
|
+
return []
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.warning(f"Could not check staged files: {e}")
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
def _should_check_file(self, file_path: str) -> bool:
|
|
306
|
+
"""
|
|
307
|
+
Determine if file should be checked for sensitive data.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
file_path: Path to file
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
True if file should be checked
|
|
314
|
+
"""
|
|
315
|
+
# Skip binary files and certain extensions
|
|
316
|
+
skip_extensions = {'.pyc', '.pyo', '.so', '.dylib', '.dll', '.exe', '.jpg', '.png', '.gif', '.pdf'}
|
|
317
|
+
skip_dirs = {'__pycache__', '.git', 'node_modules', '.pytest_cache', 'logs'}
|
|
318
|
+
|
|
319
|
+
if any(file_path.endswith(ext) for ext in skip_extensions):
|
|
320
|
+
return False
|
|
321
|
+
if any(skip_dir in file_path for skip_dir in skip_dirs):
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
def _check_file_content(self, file_path: str) -> List[str]:
|
|
327
|
+
"""
|
|
328
|
+
Check file content for sensitive data.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
file_path: Path to file to check
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
List of warnings for this file
|
|
335
|
+
"""
|
|
336
|
+
warnings = []
|
|
337
|
+
try:
|
|
338
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
339
|
+
content = f.read()
|
|
340
|
+
|
|
341
|
+
# Check for common secret patterns
|
|
342
|
+
if self._contains_secrets(content):
|
|
343
|
+
warnings.append(f"Potential secrets found in {file_path}")
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.debug(f"Could not check file {file_path}: {e}")
|
|
347
|
+
|
|
348
|
+
return warnings
|
|
349
|
+
|
|
350
|
+
def _contains_secrets(self, content: str) -> bool:
|
|
351
|
+
"""
|
|
352
|
+
Check if content contains potential secrets.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
content: File content to check
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
True if content appears to contain secrets
|
|
359
|
+
"""
|
|
360
|
+
secret_patterns = [
|
|
361
|
+
r'(?i)(password|passwd|pwd)\s*[=:]\s*["\']?[^"\'\s]{8,}',
|
|
362
|
+
r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']?[^"\'\s]{20,}',
|
|
363
|
+
r'(?i)(secret|token)\s*[=:]\s*["\']?[^"\'\s]{20,}',
|
|
364
|
+
r'(?i)(access[_-]?key)\s*[=:]\s*["\']?[A-Z0-9]{20}',
|
|
365
|
+
r'(?i)(private[_-]?key)\s*[=:]\s*["\']?[^"\'\s]{40,}',
|
|
366
|
+
r'sk-[A-Za-z0-9]{32,}', # OpenAI keys
|
|
367
|
+
r'xoxb-[A-Za-z0-9-]{50,}', # Slack bot tokens
|
|
368
|
+
r'ghp_[A-Za-z0-9]{36}', # GitHub tokens
|
|
369
|
+
r'AKIA[0-9A-Z]{16}', # AWS access keys
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
return any(re.search(pattern, content) for pattern in secret_patterns)
|
|
373
|
+
|
|
374
|
+
def install_pre_commit_hook(self) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Install Git pre-commit hook for security validation.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
True if hook was installed successfully
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
git_dir = Path('.git')
|
|
383
|
+
if not git_dir.exists():
|
|
384
|
+
logger.warning("Not in a git repository")
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
hooks_dir = git_dir / 'hooks'
|
|
388
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
389
|
+
|
|
390
|
+
pre_commit_hook = hooks_dir / 'pre-commit'
|
|
391
|
+
hook_content = self._generate_pre_commit_hook()
|
|
392
|
+
|
|
393
|
+
with open(pre_commit_hook, 'w') as f:
|
|
394
|
+
f.write(hook_content)
|
|
395
|
+
|
|
396
|
+
# Make hook executable
|
|
397
|
+
os.chmod(pre_commit_hook, 0o755)
|
|
398
|
+
|
|
399
|
+
logger.info("Pre-commit security hook installed successfully")
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f"Failed to install pre-commit hook: {e}")
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
def _generate_pre_commit_hook(self) -> str:
|
|
407
|
+
"""
|
|
408
|
+
Generate pre-commit hook script content.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Pre-commit hook script content
|
|
412
|
+
"""
|
|
413
|
+
return '''#!/bin/bash
|
|
414
|
+
# IC Security Pre-commit Hook
|
|
415
|
+
# This hook checks for sensitive data before commits
|
|
416
|
+
|
|
417
|
+
echo "Running IC security checks..."
|
|
418
|
+
|
|
419
|
+
# Check for sensitive files
|
|
420
|
+
if git diff --cached --name-only | grep -E "\\.(key|pem|p12|pfx)$|credentials|service-account"; then
|
|
421
|
+
echo "ERROR: Attempting to commit sensitive files!"
|
|
422
|
+
echo "Please remove these files from the commit:"
|
|
423
|
+
git diff --cached --name-only | grep -E "\\.(key|pem|p12|pfx)$|credentials|service-account"
|
|
424
|
+
exit 1
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
# Check for common secret patterns in staged content
|
|
428
|
+
if git diff --cached | grep -E "(password|token|secret|key)\\s*[=:]\\s*[^\\s]{10,}"; then
|
|
429
|
+
echo "WARNING: Potential secrets found in staged content!"
|
|
430
|
+
echo "Please review your changes and remove any sensitive data."
|
|
431
|
+
echo "Consider using environment variables or secure configuration files."
|
|
432
|
+
# Uncomment the next line to block commits with potential secrets
|
|
433
|
+
# exit 1
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
echo "Security checks passed."
|
|
437
|
+
exit 0
|
|
438
|
+
'''
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def create_security_config() -> Dict[str, Any]:
|
|
442
|
+
"""
|
|
443
|
+
Create default security configuration.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Default security configuration
|
|
447
|
+
"""
|
|
448
|
+
return {
|
|
449
|
+
"sensitive_keys": [
|
|
450
|
+
"password", "passwd", "pwd",
|
|
451
|
+
"token", "access_token", "refresh_token", "auth_token",
|
|
452
|
+
"key", "api_key", "access_key", "secret_key", "private_key",
|
|
453
|
+
"secret", "client_secret", "webhook_secret",
|
|
454
|
+
"webhook_url", "webhook",
|
|
455
|
+
"credential", "credentials",
|
|
456
|
+
"cert", "certificate",
|
|
457
|
+
"session", "session_token",
|
|
458
|
+
],
|
|
459
|
+
"mask_pattern": "***MASKED***",
|
|
460
|
+
"warn_on_sensitive_in_config": True,
|
|
461
|
+
"git_hooks_enabled": True,
|
|
462
|
+
}
|
ic/core/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core utilities for IC (Infra Resource Management CLI).
|
|
3
|
+
|
|
4
|
+
This package contains core functionality including:
|
|
5
|
+
- Enhanced logging system with security features
|
|
6
|
+
- Session management utilities
|
|
7
|
+
- Common utilities and helpers
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .logging import ICLogger, get_logger, init_logger
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'ICLogger',
|
|
14
|
+
'get_logger',
|
|
15
|
+
'init_logger',
|
|
16
|
+
]
|