complio 0.1.1__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 (79) hide show
  1. CHANGELOG.md +208 -0
  2. README.md +343 -0
  3. complio/__init__.py +48 -0
  4. complio/cli/__init__.py +0 -0
  5. complio/cli/banner.py +87 -0
  6. complio/cli/commands/__init__.py +0 -0
  7. complio/cli/commands/history.py +439 -0
  8. complio/cli/commands/scan.py +700 -0
  9. complio/cli/main.py +115 -0
  10. complio/cli/output.py +338 -0
  11. complio/config/__init__.py +17 -0
  12. complio/config/settings.py +333 -0
  13. complio/connectors/__init__.py +9 -0
  14. complio/connectors/aws/__init__.py +0 -0
  15. complio/connectors/aws/client.py +342 -0
  16. complio/connectors/base.py +135 -0
  17. complio/core/__init__.py +10 -0
  18. complio/core/registry.py +228 -0
  19. complio/core/runner.py +351 -0
  20. complio/py.typed +0 -0
  21. complio/reporters/__init__.py +7 -0
  22. complio/reporters/generator.py +417 -0
  23. complio/tests_library/__init__.py +0 -0
  24. complio/tests_library/base.py +492 -0
  25. complio/tests_library/identity/__init__.py +0 -0
  26. complio/tests_library/identity/access_key_rotation.py +302 -0
  27. complio/tests_library/identity/mfa_enforcement.py +327 -0
  28. complio/tests_library/identity/root_account_protection.py +470 -0
  29. complio/tests_library/infrastructure/__init__.py +0 -0
  30. complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
  31. complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
  32. complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
  33. complio/tests_library/infrastructure/ebs_encryption.py +244 -0
  34. complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
  35. complio/tests_library/infrastructure/iam_password_policy.py +460 -0
  36. complio/tests_library/infrastructure/nacl_security.py +356 -0
  37. complio/tests_library/infrastructure/rds_encryption.py +252 -0
  38. complio/tests_library/infrastructure/s3_encryption.py +301 -0
  39. complio/tests_library/infrastructure/s3_public_access.py +369 -0
  40. complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
  41. complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
  42. complio/tests_library/logging/__init__.py +0 -0
  43. complio/tests_library/logging/cloudwatch_alarms.py +354 -0
  44. complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
  45. complio/tests_library/logging/cloudwatch_retention.py +252 -0
  46. complio/tests_library/logging/config_enabled.py +393 -0
  47. complio/tests_library/logging/eventbridge_rules.py +460 -0
  48. complio/tests_library/logging/guardduty_enabled.py +436 -0
  49. complio/tests_library/logging/security_hub_enabled.py +416 -0
  50. complio/tests_library/logging/sns_encryption.py +273 -0
  51. complio/tests_library/network/__init__.py +0 -0
  52. complio/tests_library/network/alb_nlb_security.py +421 -0
  53. complio/tests_library/network/api_gateway_security.py +452 -0
  54. complio/tests_library/network/cloudfront_https.py +332 -0
  55. complio/tests_library/network/direct_connect_security.py +343 -0
  56. complio/tests_library/network/nacl_configuration.py +367 -0
  57. complio/tests_library/network/network_firewall.py +355 -0
  58. complio/tests_library/network/transit_gateway_security.py +318 -0
  59. complio/tests_library/network/vpc_endpoints_security.py +339 -0
  60. complio/tests_library/network/vpn_security.py +333 -0
  61. complio/tests_library/network/waf_configuration.py +428 -0
  62. complio/tests_library/security/__init__.py +0 -0
  63. complio/tests_library/security/kms_key_rotation.py +314 -0
  64. complio/tests_library/storage/__init__.py +0 -0
  65. complio/tests_library/storage/backup_encryption.py +288 -0
  66. complio/tests_library/storage/dynamodb_encryption.py +280 -0
  67. complio/tests_library/storage/efs_encryption.py +257 -0
  68. complio/tests_library/storage/elasticache_encryption.py +370 -0
  69. complio/tests_library/storage/redshift_encryption.py +252 -0
  70. complio/tests_library/storage/s3_versioning.py +264 -0
  71. complio/utils/__init__.py +26 -0
  72. complio/utils/errors.py +179 -0
  73. complio/utils/exceptions.py +151 -0
  74. complio/utils/history.py +243 -0
  75. complio/utils/logger.py +391 -0
  76. complio-0.1.1.dist-info/METADATA +385 -0
  77. complio-0.1.1.dist-info/RECORD +79 -0
  78. complio-0.1.1.dist-info/WHEEL +4 -0
  79. complio-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,243 @@
1
+ """
2
+ Scan history tracking for Complio.
3
+
4
+ This module manages local storage of scan results to allow users to:
5
+ - View recent scans
6
+ - Compare scan results over time
7
+ - Track compliance improvements
8
+
9
+ Example:
10
+ >>> from complio.utils.history import save_scan_to_history, get_scan_history
11
+ >>> save_scan_to_history(scan_id, results)
12
+ >>> history = get_scan_history(limit=10)
13
+ """
14
+
15
+ import json
16
+ import os
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ from complio.core.runner import ScanResults
22
+
23
+
24
+ # History directory location
25
+ SCAN_HISTORY_DIR = Path.home() / ".complio" / "history"
26
+
27
+
28
+ def ensure_history_dir() -> Path:
29
+ """Ensure scan history directory exists with proper permissions.
30
+
31
+ Returns:
32
+ Path to history directory
33
+
34
+ Example:
35
+ >>> history_dir = ensure_history_dir()
36
+ >>> print(history_dir)
37
+ PosixPath('/home/user/.complio/history')
38
+ """
39
+ SCAN_HISTORY_DIR.mkdir(parents=True, exist_ok=True)
40
+ # Set directory permissions to 700 (owner only)
41
+ SCAN_HISTORY_DIR.chmod(0o700)
42
+ return SCAN_HISTORY_DIR
43
+
44
+
45
+ def save_scan_to_history(scan_id: str, results: ScanResults, region: str) -> Path:
46
+ """Save scan results to local history.
47
+
48
+ Args:
49
+ scan_id: Unique scan identifier
50
+ results: Scan results to save
51
+ region: AWS region that was scanned
52
+
53
+ Returns:
54
+ Path to saved history file
55
+
56
+ Example:
57
+ >>> save_scan_to_history("scan_20260107_162335_abc123", results, "eu-west-3")
58
+ PosixPath('/home/user/.complio/history/scan_20260107_162335_abc123.json')
59
+ """
60
+ history_dir = ensure_history_dir()
61
+ filepath = history_dir / f"{scan_id}.json"
62
+
63
+ # Prepare history data
64
+ history_data = {
65
+ "scan_id": scan_id,
66
+ "timestamp": datetime.fromtimestamp(results.timestamp).isoformat(),
67
+ "region": region,
68
+ "summary": {
69
+ "total_tests": results.total_tests,
70
+ "passed_tests": results.passed_tests,
71
+ "failed_tests": results.failed_tests,
72
+ "error_tests": results.error_tests,
73
+ "overall_score": results.overall_score,
74
+ "compliance_status": "COMPLIANT" if results.overall_score >= 90 else "NON_COMPLIANT",
75
+ "execution_time_seconds": round(results.execution_time, 2),
76
+ },
77
+ "test_results": [
78
+ {
79
+ "test_id": tr.test_id,
80
+ "test_name": tr.test_name,
81
+ "status": tr.status,
82
+ "passed": tr.passed,
83
+ "score": tr.score,
84
+ "findings_count": len(tr.findings),
85
+ }
86
+ for tr in results.test_results
87
+ ],
88
+ }
89
+
90
+ # Save to file
91
+ with open(filepath, 'w') as f:
92
+ json.dump(history_data, f, indent=2)
93
+
94
+ # Set file permissions to 600 (owner read/write only)
95
+ filepath.chmod(0o600)
96
+
97
+ return filepath
98
+
99
+
100
+ def get_scan_history(limit: int = 10) -> List[Dict[str, Any]]:
101
+ """Get list of recent scans from history.
102
+
103
+ Args:
104
+ limit: Maximum number of scans to return (default: 10)
105
+
106
+ Returns:
107
+ List of scan history entries, most recent first
108
+
109
+ Example:
110
+ >>> history = get_scan_history(limit=5)
111
+ >>> for scan in history:
112
+ ... print(f"{scan['timestamp']}: {scan['summary']['overall_score']}%")
113
+ """
114
+ if not SCAN_HISTORY_DIR.exists():
115
+ return []
116
+
117
+ # Get all scan files
118
+ scan_files = sorted(
119
+ SCAN_HISTORY_DIR.glob("scan_*.json"),
120
+ key=lambda p: p.stat().st_mtime,
121
+ reverse=True # Most recent first
122
+ )
123
+
124
+ history = []
125
+ for scan_file in scan_files[:limit]:
126
+ try:
127
+ with open(scan_file, 'r') as f:
128
+ data = json.load(f)
129
+ history.append(data)
130
+ except (json.JSONDecodeError, KeyError):
131
+ # Skip corrupted files
132
+ continue
133
+
134
+ return history
135
+
136
+
137
+ def get_scan_by_id(scan_id: str) -> Optional[Dict[str, Any]]:
138
+ """Get a specific scan by its ID.
139
+
140
+ Args:
141
+ scan_id: Scan identifier to retrieve
142
+
143
+ Returns:
144
+ Scan data dictionary, or None if not found
145
+
146
+ Example:
147
+ >>> scan = get_scan_by_id("scan_20260107_162335_abc123")
148
+ >>> if scan:
149
+ ... print(scan['summary']['overall_score'])
150
+ """
151
+ filepath = SCAN_HISTORY_DIR / f"{scan_id}.json"
152
+
153
+ if not filepath.exists():
154
+ return None
155
+
156
+ try:
157
+ with open(filepath, 'r') as f:
158
+ return json.load(f)
159
+ except (json.JSONDecodeError, IOError):
160
+ return None
161
+
162
+
163
+ def compare_scans(scan_id_1: str, scan_id_2: str) -> Dict[str, Any]:
164
+ """Compare two scans and return the differences.
165
+
166
+ Args:
167
+ scan_id_1: First scan ID
168
+ scan_id_2: Second scan ID
169
+
170
+ Returns:
171
+ Dictionary containing comparison data
172
+
173
+ Example:
174
+ >>> diff = compare_scans("scan_20260107_...", "scan_20260106_...")
175
+ >>> print(f"Score change: {diff['score_change']}%")
176
+ """
177
+ scan1 = get_scan_by_id(scan_id_1)
178
+ scan2 = get_scan_by_id(scan_id_2)
179
+
180
+ if not scan1 or not scan2:
181
+ raise ValueError(f"One or both scans not found in history")
182
+
183
+ # Calculate differences
184
+ score1 = scan1['summary']['overall_score']
185
+ score2 = scan2['summary']['overall_score']
186
+ score_change = score1 - score2
187
+
188
+ passed1 = scan1['summary']['passed_tests']
189
+ passed2 = scan2['summary']['passed_tests']
190
+
191
+ failed1 = scan1['summary']['failed_tests']
192
+ failed2 = scan2['summary']['failed_tests']
193
+
194
+ return {
195
+ "scan1": {
196
+ "scan_id": scan1['scan_id'],
197
+ "timestamp": scan1['timestamp'],
198
+ "score": score1,
199
+ "passed": passed1,
200
+ "failed": failed1,
201
+ },
202
+ "scan2": {
203
+ "scan_id": scan2['scan_id'],
204
+ "timestamp": scan2['timestamp'],
205
+ "score": score2,
206
+ "passed": passed2,
207
+ "failed": failed2,
208
+ },
209
+ "differences": {
210
+ "score_change": score_change,
211
+ "score_change_direction": "improved" if score_change > 0 else "declined" if score_change < 0 else "unchanged",
212
+ "passed_change": passed1 - passed2,
213
+ "failed_change": failed1 - failed2,
214
+ }
215
+ }
216
+
217
+
218
+ def clear_old_history(keep_days: int = 30) -> int:
219
+ """Clear scan history older than specified days.
220
+
221
+ Args:
222
+ keep_days: Number of days to keep (default: 30)
223
+
224
+ Returns:
225
+ Number of files deleted
226
+
227
+ Example:
228
+ >>> deleted = clear_old_history(keep_days=30)
229
+ >>> print(f"Deleted {deleted} old scans")
230
+ """
231
+ if not SCAN_HISTORY_DIR.exists():
232
+ return 0
233
+
234
+ from datetime import timedelta
235
+ cutoff_time = datetime.now().timestamp() - (keep_days * 86400)
236
+
237
+ deleted_count = 0
238
+ for scan_file in SCAN_HISTORY_DIR.glob("scan_*.json"):
239
+ if scan_file.stat().st_mtime < cutoff_time:
240
+ scan_file.unlink()
241
+ deleted_count += 1
242
+
243
+ return deleted_count
@@ -0,0 +1,391 @@
1
+ """
2
+ Structured logging with credential filtering.
3
+
4
+ This module provides secure, structured logging for Complio with automatic
5
+ credential redaction. All sensitive data is filtered before being written to logs.
6
+
7
+ Security Features:
8
+ - Automatic credential redaction (AWS keys, secrets, passwords)
9
+ - Structured JSON logging for machine parsing
10
+ - Log rotation to prevent disk fill
11
+ - Separate console and file logging
12
+ - Configurable log levels
13
+ - Context binding for request tracking
14
+
15
+ Example:
16
+ >>> from complio.utils.logger import get_logger
17
+ >>> logger = get_logger(__name__)
18
+ >>> logger.info("test_started", test_name="s3_encryption", region="us-east-1")
19
+ >>> # Credentials automatically redacted:
20
+ >>> logger.info("aws_response", access_key="AKIAIOSFODNN7EXAMPLE")
21
+ >>> # Logged as: access_key="[REDACTED_AWS_ACCESS_KEY]"
22
+ """
23
+
24
+ import logging
25
+ import re
26
+ from logging.handlers import RotatingFileHandler
27
+ from pathlib import Path
28
+ from typing import Any, Dict, Optional
29
+
30
+ import structlog
31
+ from structlog.processors import JSONRenderer
32
+ from structlog.stdlib import add_log_level, filter_by_level
33
+
34
+ from complio.config.settings import get_settings
35
+
36
+ # ============================================================================
37
+ # CREDENTIAL PATTERNS (for redaction)
38
+ # ============================================================================
39
+
40
+ # AWS Access Key patterns
41
+ AWS_ACCESS_KEY_PATTERN = re.compile(r"(AKIA[0-9A-Z]{16})", re.IGNORECASE)
42
+ AWS_SECRET_KEY_PATTERN = re.compile(r"([A-Za-z0-9/+=]{40})", re.IGNORECASE)
43
+ AWS_SESSION_TOKEN_PATTERN = re.compile(r"(FwoGZXIvYXdzE[A-Za-z0-9/+=]{100,})", re.IGNORECASE)
44
+
45
+ # Generic password patterns
46
+ PASSWORD_PATTERN = re.compile(
47
+ r'(password["\s:=]+)([^"\s,}]+)',
48
+ re.IGNORECASE
49
+ )
50
+
51
+ # API keys and tokens
52
+ API_KEY_PATTERN = re.compile(
53
+ r'(["\s](api[_-]?key|token|secret)["\s:=]+)([^"\s,}]+)',
54
+ re.IGNORECASE
55
+ )
56
+
57
+ # Private IP addresses (optional - may want to keep these)
58
+ PRIVATE_IP_PATTERN = re.compile(
59
+ r'\b(?:10|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b'
60
+ )
61
+
62
+ # Email addresses (optional - may want to keep these)
63
+ EMAIL_PATTERN = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
64
+
65
+
66
+ # ============================================================================
67
+ # CREDENTIAL FILTERING PROCESSOR
68
+ # ============================================================================
69
+
70
+
71
+ def filter_credentials(logger: Any, method_name: str, event_dict: Dict[str, Any]) -> Dict[str, Any]:
72
+ """Filter sensitive credentials from log events.
73
+
74
+ This processor scans all log event data and redacts any credentials,
75
+ passwords, API keys, or other sensitive information.
76
+
77
+ Redaction Rules:
78
+ - AWS Access Keys (AKIA...) → [REDACTED_AWS_ACCESS_KEY]
79
+ - AWS Secret Keys (40 chars) → [REDACTED_AWS_SECRET_KEY]
80
+ - AWS Session Tokens → [REDACTED_AWS_SESSION_TOKEN]
81
+ - AWS Account IDs (12 digits) → ****1234 (last 4 digits only)
82
+ - User IDs → ****W53X (last 4 chars only)
83
+ - Passwords → [REDACTED_PASSWORD]
84
+ - API Keys/Tokens → [REDACTED_API_KEY]
85
+
86
+ Args:
87
+ logger: Logger instance
88
+ method_name: Log method name (info, error, etc.)
89
+ event_dict: Log event dictionary
90
+
91
+ Returns:
92
+ Event dictionary with credentials redacted
93
+
94
+ Example:
95
+ >>> event = {"msg": "Connected", "access_key": "AKIAIOSFODNN7EXAMPLE"}
96
+ >>> filtered = filter_credentials(None, "info", event)
97
+ >>> print(filtered["access_key"])
98
+ '[REDACTED_AWS_ACCESS_KEY]'
99
+ """
100
+ # Process all values in the event dict
101
+ for key, value in event_dict.items():
102
+ if isinstance(value, str):
103
+ # Redact AWS Access Keys
104
+ value = AWS_ACCESS_KEY_PATTERN.sub("[REDACTED_AWS_ACCESS_KEY]", value)
105
+
106
+ # Redact AWS Secret Keys (be careful with this - 40 chars is common)
107
+ # Only redact if key name suggests it's a secret
108
+ if any(secret_key in key.lower() for secret_key in ["secret", "password", "token"]):
109
+ if len(value) >= 40:
110
+ value = "[REDACTED_AWS_SECRET_KEY]"
111
+
112
+ # Redact AWS Session Tokens
113
+ value = AWS_SESSION_TOKEN_PATTERN.sub("[REDACTED_AWS_SESSION_TOKEN]", value)
114
+
115
+ # Mask AWS Account IDs (show last 4 digits only)
116
+ if any(acct_key in key.lower() for acct_key in ["account_id", "account"]):
117
+ if value.isdigit() and len(value) == 12:
118
+ value = f"****{value[-4:]}"
119
+
120
+ # Mask User IDs (show last 4 chars only)
121
+ if any(user_key in key.lower() for user_key in ["user_id", "userid"]):
122
+ if len(value) > 4:
123
+ value = f"****{value[-4:]}"
124
+
125
+ # Redact passwords
126
+ value = PASSWORD_PATTERN.sub(r'\1[REDACTED_PASSWORD]', value)
127
+
128
+ # Redact API keys
129
+ value = API_KEY_PATTERN.sub(r'\1[REDACTED_API_KEY]', value)
130
+
131
+ # Update the value
132
+ event_dict[key] = value
133
+
134
+ elif isinstance(value, dict):
135
+ # Recursively filter nested dicts
136
+ event_dict[key] = _filter_dict(value)
137
+
138
+ elif isinstance(value, list):
139
+ # Filter lists
140
+ event_dict[key] = [_filter_value(item) for item in value]
141
+
142
+ return event_dict
143
+
144
+
145
+ def _filter_dict(data: Dict[str, Any]) -> Dict[str, Any]:
146
+ """Recursively filter credentials from dictionary.
147
+
148
+ Args:
149
+ data: Dictionary to filter
150
+
151
+ Returns:
152
+ Filtered dictionary
153
+ """
154
+ filtered = {}
155
+ for key, value in data.items():
156
+ if isinstance(value, str):
157
+ # Check for sensitive keys
158
+ if any(sensitive in key.lower() for sensitive in [
159
+ "password", "secret", "token", "key", "credential"
160
+ ]):
161
+ filtered[key] = "[REDACTED]"
162
+ else:
163
+ # Still scan for patterns
164
+ filtered[key] = AWS_ACCESS_KEY_PATTERN.sub("[REDACTED_AWS_ACCESS_KEY]", value)
165
+ filtered[key] = AWS_SESSION_TOKEN_PATTERN.sub("[REDACTED_AWS_SESSION_TOKEN]", filtered[key])
166
+ elif isinstance(value, dict):
167
+ filtered[key] = _filter_dict(value)
168
+ elif isinstance(value, list):
169
+ filtered[key] = [_filter_value(item) for item in value]
170
+ else:
171
+ filtered[key] = value
172
+ return filtered
173
+
174
+
175
+ def _filter_value(value: Any) -> Any:
176
+ """Filter a single value.
177
+
178
+ Args:
179
+ value: Value to filter
180
+
181
+ Returns:
182
+ Filtered value
183
+ """
184
+ if isinstance(value, str):
185
+ value = AWS_ACCESS_KEY_PATTERN.sub("[REDACTED_AWS_ACCESS_KEY]", value)
186
+ value = AWS_SESSION_TOKEN_PATTERN.sub("[REDACTED_AWS_SESSION_TOKEN]", value)
187
+ return value
188
+ elif isinstance(value, dict):
189
+ return _filter_dict(value)
190
+ elif isinstance(value, list):
191
+ return [_filter_value(item) for item in value]
192
+ else:
193
+ return value
194
+
195
+
196
+ # ============================================================================
197
+ # LOGGER SETUP
198
+ # ============================================================================
199
+
200
+
201
+ def setup_logging() -> None:
202
+ """Configure structured logging for the application.
203
+
204
+ Sets up:
205
+ - Console handler with colored output (if enabled)
206
+ - File handler with rotation (if enabled)
207
+ - Credential filtering processor
208
+ - JSON formatting for file logs
209
+ - Structured event dict format
210
+
211
+ Called automatically when getting a logger.
212
+
213
+ Example:
214
+ >>> setup_logging()
215
+ >>> logger = logging.getLogger("complio")
216
+ >>> logger.info("Application started")
217
+ """
218
+ settings = get_settings()
219
+
220
+ # Get log level
221
+ log_level = getattr(logging, settings.log_level)
222
+
223
+ # Configure stdlib logging
224
+ logging.basicConfig(
225
+ format="%(message)s",
226
+ level=log_level,
227
+ )
228
+
229
+ # Silence noisy libraries
230
+ logging.getLogger("boto3").setLevel(logging.WARNING)
231
+ logging.getLogger("botocore").setLevel(logging.WARNING)
232
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
233
+
234
+ # Build processor chain
235
+ shared_processors = [
236
+ structlog.stdlib.add_logger_name,
237
+ add_log_level,
238
+ structlog.processors.TimeStamper(fmt="iso"),
239
+ filter_credentials, # CRITICAL: Filter credentials before any output
240
+ ]
241
+
242
+ # Configure structlog
243
+ structlog.configure(
244
+ processors=shared_processors + [
245
+ filter_by_level,
246
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
247
+ ],
248
+ context_class=dict,
249
+ logger_factory=structlog.stdlib.LoggerFactory(),
250
+ cache_logger_on_first_use=True,
251
+ )
252
+
253
+ # Set up file logging if enabled
254
+ if settings.log_to_file:
255
+ # Ensure log directory exists
256
+ settings.log_dir.mkdir(parents=True, exist_ok=True)
257
+ settings.log_dir.chmod(0o700)
258
+
259
+ log_file = settings.log_dir / "complio.log"
260
+
261
+ # Create rotating file handler
262
+ file_handler = RotatingFileHandler(
263
+ filename=str(log_file),
264
+ maxBytes=settings.log_max_bytes,
265
+ backupCount=settings.log_backup_count,
266
+ encoding="utf-8",
267
+ )
268
+ file_handler.setLevel(log_level)
269
+
270
+ # Use JSON formatter for file logs
271
+ file_handler.setFormatter(
272
+ structlog.stdlib.ProcessorFormatter(
273
+ processor=JSONRenderer(),
274
+ foreign_pre_chain=shared_processors,
275
+ )
276
+ )
277
+
278
+ # Add to root logger
279
+ root_logger = logging.getLogger()
280
+ root_logger.addHandler(file_handler)
281
+
282
+ # Set file permissions to 600
283
+ log_file.chmod(0o600)
284
+
285
+
286
+ # Track if logging has been set up
287
+ _logging_configured = False
288
+
289
+
290
+ def get_logger(name: str) -> structlog.stdlib.BoundLogger:
291
+ """Get a structured logger instance.
292
+
293
+ Args:
294
+ name: Logger name (usually __name__)
295
+
296
+ Returns:
297
+ Structured logger with credential filtering
298
+
299
+ Example:
300
+ >>> logger = get_logger(__name__)
301
+ >>> logger.info("user_login", user_id=123, ip="10.0.0.1")
302
+ >>> logger.error("auth_failed", error="Invalid password")
303
+ """
304
+ global _logging_configured
305
+ if not _logging_configured:
306
+ setup_logging()
307
+ _logging_configured = True
308
+
309
+ return structlog.get_logger(name)
310
+
311
+
312
+ # ============================================================================
313
+ # CONVENIENCE FUNCTIONS
314
+ # ============================================================================
315
+
316
+
317
+ def log_function_call(logger: structlog.stdlib.BoundLogger, function_name: str, **kwargs: Any) -> None:
318
+ """Log a function call with parameters.
319
+
320
+ Automatically filters sensitive parameters.
321
+
322
+ Args:
323
+ logger: Logger instance
324
+ function_name: Name of function being called
325
+ **kwargs: Function parameters
326
+
327
+ Example:
328
+ >>> logger = get_logger(__name__)
329
+ >>> log_function_call(logger, "connect_to_aws", region="us-east-1", profile="prod")
330
+ """
331
+ logger.debug(
332
+ "function_call",
333
+ function=function_name,
334
+ **kwargs
335
+ )
336
+
337
+
338
+ def log_aws_api_call(
339
+ logger: structlog.stdlib.BoundLogger,
340
+ service: str,
341
+ operation: str,
342
+ region: str,
343
+ **kwargs: Any
344
+ ) -> None:
345
+ """Log an AWS API call.
346
+
347
+ Args:
348
+ logger: Logger instance
349
+ service: AWS service name (e.g., 's3', 'ec2')
350
+ operation: API operation (e.g., 'list_buckets')
351
+ region: AWS region
352
+ **kwargs: Additional context
353
+
354
+ Example:
355
+ >>> logger = get_logger(__name__)
356
+ >>> log_aws_api_call(logger, "s3", "list_buckets", "us-east-1")
357
+ """
358
+ logger.debug(
359
+ "aws_api_call",
360
+ service=service,
361
+ operation=operation,
362
+ region=region,
363
+ **kwargs
364
+ )
365
+
366
+
367
+ def log_test_result(
368
+ logger: structlog.stdlib.BoundLogger,
369
+ test_name: str,
370
+ passed: bool,
371
+ **kwargs: Any
372
+ ) -> None:
373
+ """Log a compliance test result.
374
+
375
+ Args:
376
+ logger: Logger instance
377
+ test_name: Name of compliance test
378
+ passed: Whether test passed
379
+ **kwargs: Additional context (findings, resources, etc.)
380
+
381
+ Example:
382
+ >>> logger = get_logger(__name__)
383
+ >>> log_test_result(logger, "s3_encryption", True, bucket_count=5)
384
+ """
385
+ log_level = "info" if passed else "warning"
386
+ getattr(logger, log_level)(
387
+ "compliance_test_result",
388
+ test_name=test_name,
389
+ passed=passed,
390
+ **kwargs
391
+ )