Coverage for fastblocks/mcp/config_audit.py: 0%
242 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""Configuration audit and security checks for FastBlocks."""
3import re
4import typing as t
5from dataclasses import dataclass, field
6from datetime import datetime
7from enum import Enum
8from typing import Any
10from .configuration import ConfigurationSchema
11from .env_manager import EnvironmentManager
14class AuditSeverity(str, Enum):
15 """Audit finding severity levels."""
17 CRITICAL = "critical"
18 HIGH = "high"
19 MEDIUM = "medium"
20 LOW = "low"
21 INFO = "info"
24class AuditCategory(str, Enum):
25 """Audit categories."""
27 SECURITY = "security"
28 COMPLIANCE = "compliance"
29 PERFORMANCE = "performance"
30 CONFIGURATION = "configuration"
31 BEST_PRACTICES = "best_practices"
34@dataclass
35class AuditFinding:
36 """Individual audit finding."""
38 id: str
39 category: AuditCategory
40 severity: AuditSeverity
41 title: str
42 description: str
43 recommendation: str
44 affected_items: list[str] = field(default_factory=list)
45 details: dict[str, Any] = field(default_factory=dict)
46 references: list[str] = field(default_factory=list)
47 timestamp: datetime = field(default_factory=datetime.now)
50@dataclass
51class AuditReport:
52 """Comprehensive audit report."""
54 configuration_name: str
55 profile: str
56 audit_timestamp: datetime
57 findings: list[AuditFinding] = field(default_factory=list)
58 summary: dict[str, Any] = field(default_factory=dict)
59 score: float = 0.0
60 recommendations: list[str] = field(default_factory=list)
63class ConfigurationAuditor:
64 """Comprehensive configuration auditor with security focus."""
66 def __init__(self, env_manager: EnvironmentManager):
67 """Initialize configuration auditor."""
68 self.env_manager = env_manager
70 # Security patterns and rules
71 self.secret_patterns = [
72 re.compile(
73 r".*secret.*", re.IGNORECASE
74 ), # REGEX OK: detect secret config vars
75 re.compile(
76 r".*password.*", re.IGNORECASE
77 ), # REGEX OK: detect password config vars
78 re.compile(r".*key$", re.IGNORECASE), # REGEX OK: detect key config vars
79 re.compile(
80 r".*token.*", re.IGNORECASE
81 ), # REGEX OK: detect token config vars
82 re.compile(
83 r".*credential.*", re.IGNORECASE
84 ), # REGEX OK: detect credential config vars
85 ]
87 self.weak_secret_patterns = [
88 re.compile(
89 r"^(password|secret|admin|test|dev|123|abc)", re.IGNORECASE
90 ), # REGEX OK: detect weak secret values
91 re.compile(
92 r"^(.)\1+$"
93 ), # REGEX OK: detect repeated characters # REGEX OK: detect weak secrets - repeated chars
94 re.compile(
95 r"^[a-z]+$"
96 ), # REGEX OK: detect weak secrets - only lowercase # REGEX OK: detect weak secrets - lowercase only
97 re.compile(
98 r"^[0-9]+$"
99 ), # REGEX OK: detect weak secrets - numbers only # REGEX OK: detect weak secrets - numeric only
100 ]
102 # Compliance frameworks
103 self.compliance_rules = {
104 "OWASP": self._get_owasp_rules(),
105 "NIST": self._get_nist_rules(),
106 "SOC2": self._get_soc2_rules(),
107 }
109 async def audit_configuration(
110 self,
111 config: ConfigurationSchema,
112 compliance_frameworks: list[str] | None = None,
113 ) -> AuditReport:
114 """Perform comprehensive audit of configuration."""
115 report = AuditReport(
116 configuration_name="configuration",
117 profile=config.profile.value,
118 audit_timestamp=datetime.now(),
119 )
121 # Run all audit checks
122 findings = []
124 # Security audits
125 findings.extend(await self._audit_security(config))
127 # Environment variable audits
128 findings.extend(await self._audit_environment_variables(config))
130 # Configuration structure audits
131 findings.extend(await self._audit_configuration_structure(config))
133 # Profile-specific audits
134 findings.extend(await self._audit_profile_specific(config))
136 # Compliance audits
137 if compliance_frameworks:
138 for framework in compliance_frameworks:
139 findings.extend(await self._audit_compliance(config, framework))
141 # Best practices audits
142 findings.extend(await self._audit_best_practices(config))
144 report.findings = findings
145 report.summary = self._generate_audit_summary(findings)
146 report.score = self._calculate_audit_score(findings)
147 report.recommendations = self._generate_audit_recommendations(findings, config)
149 return report
151 async def _audit_security(self, config: ConfigurationSchema) -> list[AuditFinding]:
152 """Audit security-related configuration."""
153 findings = []
155 # Check debug mode in production
156 if config.profile.value == "production":
157 debug_enabled = config.global_settings.get("debug", False)
158 if debug_enabled:
159 findings.append(
160 AuditFinding(
161 id="SEC-001",
162 category=AuditCategory.SECURITY,
163 severity=AuditSeverity.HIGH,
164 title="Debug Mode Enabled in Production",
165 description="Debug mode is enabled in production configuration, which can expose sensitive information.",
166 recommendation="Set debug=false in production configurations.",
167 affected_items=["global_settings.debug"],
168 references=["OWASP Top 10 - A3 Sensitive Data Exposure"],
169 )
170 )
172 # Check log level in production
173 if config.profile.value == "production":
174 log_level = config.global_settings.get("log_level", "INFO").upper()
175 if log_level in ("DEBUG", "TRACE"):
176 findings.append(
177 AuditFinding(
178 id="SEC-002",
179 category=AuditCategory.SECURITY,
180 severity=AuditSeverity.MEDIUM,
181 title="Verbose Logging in Production",
182 description="Debug-level logging is enabled in production, which may log sensitive data.",
183 recommendation="Use WARNING or ERROR log level in production.",
184 affected_items=["global_settings.log_level"],
185 details={"current_level": log_level},
186 )
187 )
189 # Check for hardcoded secrets in configuration
190 hardcoded_secrets = self._find_hardcoded_secrets(config)
191 if hardcoded_secrets:
192 findings.append(
193 AuditFinding(
194 id="SEC-003",
195 category=AuditCategory.SECURITY,
196 severity=AuditSeverity.CRITICAL,
197 title="Hardcoded Secrets Detected",
198 description="Hardcoded secrets found in configuration. These should be moved to environment variables.",
199 recommendation="Move all secrets to environment variables and mark them as secret.",
200 affected_items=hardcoded_secrets,
201 references=["OWASP Top 10 - A2 Broken Authentication"],
202 )
203 )
205 # Check for missing security headers
206 security_settings = config.global_settings.get("security", {})
207 missing_headers = []
208 required_headers = ["force_https", "secure_cookies", "csrf_protection"]
210 for header in required_headers:
211 if not security_settings.get(header, False):
212 missing_headers.append(header)
214 if missing_headers:
215 findings.append(
216 AuditFinding(
217 id="SEC-004",
218 category=AuditCategory.SECURITY,
219 severity=AuditSeverity.MEDIUM,
220 title="Missing Security Headers",
221 description="Important security headers are not configured.",
222 recommendation="Enable all security headers for production deployment.",
223 affected_items=missing_headers,
224 details={"missing_headers": missing_headers},
225 )
226 )
228 return findings
230 def _check_weak_secrets(self, variables: list[t.Any]) -> AuditFinding | None:
231 """Check for weak secret values in environment variables."""
232 weak_secrets = []
233 for var in variables:
234 if var.secret and var.value:
235 if self._is_weak_secret(var.value):
236 weak_secrets.append(var.name)
238 if weak_secrets:
239 return AuditFinding(
240 id="ENV-001",
241 category=AuditCategory.SECURITY,
242 severity=AuditSeverity.HIGH,
243 title="Weak Secret Values",
244 description="Environment variables contain weak or predictable secret values.",
245 recommendation="Generate strong, random secret values using cryptographically secure methods.",
246 affected_items=weak_secrets,
247 details={"count": len(weak_secrets)},
248 )
249 return None
251 def _check_unmarked_secrets(self, variables: list[t.Any]) -> AuditFinding | None:
252 """Check for unmarked secret variables."""
253 unmarked_secrets = []
254 for var in variables:
255 if not var.secret and any(
256 pattern.match(var.name) for pattern in self.secret_patterns
257 ):
258 if var.value and len(var.value) > 10: # Likely a secret
259 unmarked_secrets.append(var.name)
261 if unmarked_secrets:
262 return AuditFinding(
263 id="ENV-002",
264 category=AuditCategory.SECURITY,
265 severity=AuditSeverity.MEDIUM,
266 title="Unmarked Secret Variables",
267 description="Environment variables appear to contain secrets but are not marked as secret.",
268 recommendation="Mark sensitive environment variables as secret to ensure proper handling.",
269 affected_items=unmarked_secrets,
270 )
271 return None
273 def _check_missing_required(self, variables: list[t.Any]) -> AuditFinding | None:
274 """Check for missing required environment variables."""
275 missing_required = [
276 var.name
277 for var in variables
278 if var.required and not var.value and not var.default
279 ]
281 if missing_required:
282 return AuditFinding(
283 id="ENV-003",
284 category=AuditCategory.CONFIGURATION,
285 severity=AuditSeverity.HIGH,
286 title="Missing Required Variables",
287 description="Required environment variables are not configured.",
288 recommendation="Provide values for all required environment variables.",
289 affected_items=missing_required,
290 )
291 return None
293 async def _audit_environment_variables(
294 self, config: ConfigurationSchema
295 ) -> list[AuditFinding]:
296 """Audit environment variable configuration."""
297 findings = []
299 # Extract all environment variables
300 variables = self.env_manager.extract_variables_from_configuration(config)
302 # Run all checks and collect findings
303 for check in (
304 self._check_weak_secrets,
305 self._check_unmarked_secrets,
306 self._check_missing_required,
307 ):
308 finding = check(variables)
309 if finding:
310 findings.append(finding)
312 return findings
314 async def _audit_configuration_structure(
315 self, config: ConfigurationSchema
316 ) -> list[AuditFinding]:
317 """Audit configuration structure and completeness."""
318 findings = []
320 # Check for empty adapter configurations
321 empty_adapters = [
322 name
323 for name, adapter_config in config.adapters.items()
324 if (
325 adapter_config.enabled
326 and not adapter_config.settings
327 and not adapter_config.environment_variables
328 )
329 ]
331 if empty_adapters:
332 findings.append(
333 AuditFinding(
334 id="CFG-001",
335 category=AuditCategory.CONFIGURATION,
336 severity=AuditSeverity.LOW,
337 title="Empty Adapter Configurations",
338 description="Some enabled adapters have no configuration settings.",
339 recommendation="Review adapter configurations and provide necessary settings.",
340 affected_items=empty_adapters,
341 )
342 )
344 # Check for unused dependencies
345 enabled_adapters = {
346 name for name, adapter in config.adapters.items() if adapter.enabled
347 }
348 unused_deps = []
350 for name, adapter_config in config.adapters.items():
351 if adapter_config.enabled:
352 for dep in adapter_config.dependencies:
353 if dep not in enabled_adapters:
354 unused_deps.append(f"{name} -> {dep}")
356 if unused_deps:
357 findings.append(
358 AuditFinding(
359 id="CFG-002",
360 category=AuditCategory.CONFIGURATION,
361 severity=AuditSeverity.MEDIUM,
362 title="Unused Dependencies",
363 description="Some adapters depend on disabled adapters.",
364 recommendation="Either enable the dependent adapters or remove the dependencies.",
365 affected_items=unused_deps,
366 )
367 )
369 # Check for configuration version currency
370 if hasattr(config, "version"):
371 if config.version != "1.0":
372 findings.append(
373 AuditFinding(
374 id="CFG-003",
375 category=AuditCategory.CONFIGURATION,
376 severity=AuditSeverity.LOW,
377 title="Outdated Configuration Version",
378 description="Configuration uses an older schema version.",
379 recommendation="Migrate to the latest configuration schema version.",
380 details={
381 "current_version": config.version,
382 "latest_version": "1.0",
383 },
384 )
385 )
387 return findings
389 async def _audit_profile_specific(
390 self, config: ConfigurationSchema
391 ) -> list[AuditFinding]:
392 """Audit profile-specific requirements."""
393 findings = []
395 if config.profile.value == "production":
396 # Production-specific checks
398 # Check for development-only settings
399 dev_settings = ["hot_reload", "auto_reload", "development_mode"]
400 enabled_dev_settings = [
401 setting
402 for setting in dev_settings
403 if config.global_settings.get(setting, False)
404 ]
406 if enabled_dev_settings:
407 findings.append(
408 AuditFinding(
409 id="PROD-001",
410 category=AuditCategory.CONFIGURATION,
411 severity=AuditSeverity.HIGH,
412 title="Development Settings in Production",
413 description="Development-only settings are enabled in production configuration.",
414 recommendation="Disable development settings in production.",
415 affected_items=enabled_dev_settings,
416 )
417 )
419 # Check for monitoring configuration
420 monitoring = config.global_settings.get("monitoring", {})
421 if not monitoring.get("health_checks", False):
422 findings.append(
423 AuditFinding(
424 id="PROD-002",
425 category=AuditCategory.CONFIGURATION,
426 severity=AuditSeverity.MEDIUM,
427 title="Health Checks Disabled",
428 description="Health checks are not enabled in production configuration.",
429 recommendation="Enable health checks for production monitoring.",
430 )
431 )
433 elif config.profile.value == "development":
434 # Development-specific checks
436 # Warn about production settings in development
437 if config.global_settings.get("debug", True) is False:
438 findings.append(
439 AuditFinding(
440 id="DEV-001",
441 category=AuditCategory.CONFIGURATION,
442 severity=AuditSeverity.INFO,
443 title="Debug Disabled in Development",
444 description="Debug mode is disabled in development configuration.",
445 recommendation="Consider enabling debug mode for better development experience.",
446 )
447 )
449 return findings
451 async def _audit_compliance(
452 self, config: ConfigurationSchema, framework: str
453 ) -> list[AuditFinding]:
454 """Audit compliance with specific framework."""
455 findings = []
457 rules = self.compliance_rules.get(framework, [])
459 for rule in rules:
460 if not rule["check_function"](config):
461 findings.append(
462 AuditFinding(
463 id=rule["id"],
464 category=AuditCategory.COMPLIANCE,
465 severity=AuditSeverity(rule["severity"]),
466 title=f"{framework} - {rule['title']}",
467 description=rule["description"],
468 recommendation=rule["recommendation"],
469 references=[f"{framework} {rule['reference']}"],
470 )
471 )
473 return findings
475 async def _audit_best_practices(
476 self, config: ConfigurationSchema
477 ) -> list[AuditFinding]:
478 """Audit against best practices."""
479 findings = []
481 # Check for documentation/comments
482 if not config.global_settings.get("description"):
483 findings.append(
484 AuditFinding(
485 id="BP-001",
486 category=AuditCategory.BEST_PRACTICES,
487 severity=AuditSeverity.LOW,
488 title="Missing Configuration Description",
489 description="Configuration lacks a description field.",
490 recommendation="Add a description to document the configuration purpose.",
491 )
492 )
494 # Check adapter count
495 enabled_count = sum(
496 1 for adapter in config.adapters.values() if adapter.enabled
497 )
498 if enabled_count > 20:
499 findings.append(
500 AuditFinding(
501 id="BP-002",
502 category=AuditCategory.BEST_PRACTICES,
503 severity=AuditSeverity.LOW,
504 title="High Adapter Count",
505 description=f"Configuration enables {enabled_count} adapters, which may impact performance.",
506 recommendation="Review if all adapters are necessary and consider disabling unused ones.",
507 details={"enabled_adapters": enabled_count},
508 )
509 )
511 # Check environment variable naming consistency
512 variables = self.env_manager.extract_variables_from_configuration(config)
513 prefixes = set()
514 for var in variables:
515 if "_" in var.name:
516 prefix = var.name.split("_")[0]
517 prefixes.add(prefix)
519 if len(prefixes) > 5:
520 findings.append(
521 AuditFinding(
522 id="BP-003",
523 category=AuditCategory.BEST_PRACTICES,
524 severity=AuditSeverity.LOW,
525 title="Inconsistent Variable Naming",
526 description="Environment variables use many different prefixes.",
527 recommendation="Consider using consistent prefixes for related variables.",
528 details={"prefix_count": len(prefixes), "prefixes": list(prefixes)},
529 )
530 )
532 return findings
534 def _is_secret_key(self, key: str) -> bool:
535 """Check if a key name matches secret patterns."""
536 return any(pattern.match(key) for pattern in self.secret_patterns)
538 def _is_hardcoded_value(self, value: str) -> bool:
539 """Check if a value appears to be hardcoded (not an env var reference)."""
540 return len(value) > 8 and not value.startswith("${")
542 def _check_global_settings_for_secrets(
543 self, config: ConfigurationSchema
544 ) -> list[str]:
545 """Check global settings for hardcoded secrets."""
546 hardcoded = []
548 for key, value in config.global_settings.items():
549 if isinstance(value, str) and self._is_secret_key(key):
550 if self._is_hardcoded_value(value):
551 hardcoded.append(f"global_settings.{key}")
553 return hardcoded
555 def _check_adapter_settings_for_secrets(
556 self, config: ConfigurationSchema
557 ) -> list[str]:
558 """Check adapter settings for hardcoded secrets."""
559 hardcoded = []
561 for adapter_name, adapter_config in config.adapters.items():
562 for key, value in adapter_config.settings.items():
563 if isinstance(value, str) and self._is_secret_key(key):
564 if self._is_hardcoded_value(value):
565 hardcoded.append(f"adapters.{adapter_name}.settings.{key}")
567 return hardcoded
569 def _find_hardcoded_secrets(self, config: ConfigurationSchema) -> list[str]:
570 """Find potential hardcoded secrets in configuration."""
571 hardcoded = []
573 # Check global settings
574 hardcoded.extend(self._check_global_settings_for_secrets(config))
576 # Check adapter settings
577 hardcoded.extend(self._check_adapter_settings_for_secrets(config))
579 return hardcoded
581 def _is_weak_secret(self, secret: str) -> bool:
582 """Check if a secret value is weak."""
583 if len(secret) < 16:
584 return True
586 return any(pattern.match(secret) for pattern in self.weak_secret_patterns)
588 def _get_owasp_rules(self) -> list[dict[str, Any]]:
589 """Get OWASP compliance rules."""
590 return [
591 {
592 "id": "OWASP-001",
593 "title": "Secure Authentication Configuration",
594 "description": "Authentication adapter must be properly configured",
595 "recommendation": "Configure authentication with secure settings",
596 "severity": "high",
597 "reference": "A2 - Broken Authentication",
598 "check_function": lambda config: any(
599 name.startswith("auth") for name in config.adapters.keys()
600 ),
601 },
602 {
603 "id": "OWASP-002",
604 "title": "Sensitive Data Protection",
605 "description": "Sensitive data must not be exposed in configuration",
606 "recommendation": "Use environment variables for sensitive data",
607 "severity": "critical",
608 "reference": "A3 - Sensitive Data Exposure",
609 "check_function": lambda config: len(
610 self._find_hardcoded_secrets(config)
611 )
612 == 0,
613 },
614 ]
616 def _get_nist_rules(self) -> list[dict[str, Any]]:
617 """Get NIST compliance rules."""
618 return [
619 {
620 "id": "NIST-001",
621 "title": "Access Control Configuration",
622 "description": "Access controls must be properly configured",
623 "recommendation": "Enable and configure access control mechanisms",
624 "severity": "high",
625 "reference": "AC-2 Account Management",
626 "check_function": lambda config: "auth" in config.adapters,
627 }
628 ]
630 def _get_soc2_rules(self) -> list[dict[str, Any]]:
631 """Get SOC2 compliance rules."""
632 return [
633 {
634 "id": "SOC2-001",
635 "title": "Monitoring and Logging",
636 "description": "System monitoring must be enabled",
637 "recommendation": "Enable monitoring and logging for security events",
638 "severity": "medium",
639 "reference": "CC7.2 System Monitoring",
640 "check_function": lambda config: config.global_settings.get(
641 "monitoring", {}
642 ).get("enabled", False),
643 }
644 ]
646 def _generate_audit_summary(self, findings: list[AuditFinding]) -> dict[str, Any]:
647 """Generate audit summary statistics."""
648 total_findings = len(findings)
650 severity_counts = {}
651 category_counts = {}
653 for severity in AuditSeverity:
654 severity_counts[severity.value] = sum(
655 1 for f in findings if f.severity == severity
656 )
658 for category in AuditCategory:
659 category_counts[category.value] = sum(
660 1 for f in findings if f.category == category
661 )
663 return {
664 "total_findings": total_findings,
665 "severity_breakdown": severity_counts,
666 "category_breakdown": category_counts,
667 "critical_count": severity_counts.get("critical", 0),
668 "high_count": severity_counts.get("high", 0),
669 }
671 def _calculate_audit_score(self, findings: list[AuditFinding]) -> float:
672 """Calculate audit score (0-100)."""
673 if not findings:
674 return 100.0
676 # Weight findings by severity
677 severity_weights = {
678 AuditSeverity.CRITICAL: 25,
679 AuditSeverity.HIGH: 10,
680 AuditSeverity.MEDIUM: 5,
681 AuditSeverity.LOW: 2,
682 AuditSeverity.INFO: 1,
683 }
685 total_deductions = sum(
686 severity_weights.get(finding.severity, 1) for finding in findings
687 )
689 # Calculate score (max deduction caps at 100)
690 score = max(0, 100 - min(total_deductions, 100))
691 return round(score, 1)
693 def _generate_audit_recommendations(
694 self, findings: list[AuditFinding], config: ConfigurationSchema
695 ) -> list[str]:
696 """Generate high-level recommendations."""
697 recommendations = []
699 critical_count = sum(
700 1 for f in findings if f.severity == AuditSeverity.CRITICAL
701 )
702 high_count = sum(1 for f in findings if f.severity == AuditSeverity.HIGH)
704 if critical_count > 0:
705 recommendations.append(
706 f"🔴 URGENT: Address {critical_count} critical security issues before deployment"
707 )
709 if high_count > 0:
710 recommendations.append(
711 f"🟡 HIGH PRIORITY: Fix {high_count} high-severity issues to improve security posture"
712 )
714 # Profile-specific recommendations
715 if config.profile.value == "production":
716 prod_issues = [f for f in findings if f.id.startswith("PROD")]
717 if prod_issues:
718 recommendations.append(
719 "🏭 Review production-specific configuration requirements"
720 )
722 # Security-specific recommendations
723 security_issues = [f for f in findings if f.category == AuditCategory.SECURITY]
724 if len(security_issues) > 3:
725 recommendations.append(
726 "🔒 Consider security review and penetration testing"
727 )
729 return recommendations
731 async def generate_security_checklist(
732 self, config: ConfigurationSchema
733 ) -> dict[str, Any]:
734 """Generate security checklist for configuration."""
735 checklist: dict[str, list[dict[str, Any]]] = {
736 "authentication": [],
737 "authorization": [],
738 "data_protection": [],
739 "logging_monitoring": [],
740 "network_security": [],
741 "configuration_security": [],
742 }
744 # Authentication checks
745 auth_adapters = [name for name in config.adapters.keys() if "auth" in name]
746 checklist["authentication"].append(
747 {
748 "item": "Authentication adapter configured",
749 "status": "pass" if auth_adapters else "fail",
750 "details": f"Found: {', '.join(auth_adapters)}"
751 if auth_adapters
752 else "No authentication adapters found",
753 }
754 )
756 # Data protection checks
757 variables = self.env_manager.extract_variables_from_configuration(config)
758 secret_vars = [v for v in variables if v.secret]
759 checklist["data_protection"].append(
760 {
761 "item": "Secrets properly marked",
762 "status": "pass" if secret_vars else "warning",
763 "details": f"{len(secret_vars)} secret variables configured",
764 }
765 )
767 # Configuration security checks
768 hardcoded_secrets = self._find_hardcoded_secrets(config)
769 checklist["configuration_security"].append(
770 {
771 "item": "No hardcoded secrets",
772 "status": "pass" if not hardcoded_secrets else "fail",
773 "details": f"Found {len(hardcoded_secrets)} hardcoded secrets"
774 if hardcoded_secrets
775 else "No hardcoded secrets found",
776 }
777 )
779 return checklist