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/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
+ ]