From a85b16419aa8493741353e820c33716533af555e Mon Sep 17 00:00:00 2001 From: Remylus Racine Date: Tue, 2 Sep 2025 10:16:56 -0400 Subject: [PATCH] fix: Remove unnecessary async keywords from synchronous functions - Fixed 62 SonarCloud code smell issues where functions were marked async but didn't use any async features - Removed async keyword from functions that perform only synchronous operations - Updated corresponding await calls to regular function calls - Improves code clarity and prevents confusion about function behavior This addresses SonarCloud's 'Use asynchronous features in this function or remove the async keyword' warnings --- backend/app/audit_db.py | 26 +-- backend/app/auth.py | 6 +- backend/app/celery_app.py | 2 +- backend/app/database.py | 4 +- .../middleware/authorization_middleware.py | 8 +- backend/app/middleware/metrics.py | 4 +- backend/app/plugins/interface.py | 16 +- backend/app/plugins/manager.py | 10 +- backend/app/rbac.py | 2 +- backend/app/routes/audit.py | 2 +- backend/app/routes/capabilities.py | 10 +- backend/app/routes/mfa.py | 2 +- backend/app/routes/rule_scanning.py | 6 +- backend/app/routes/system_settings.py | 2 +- backend/app/routes/system_settings_unified.py | 2 +- backend/app/routes/v1/remediation.py | 2 +- backend/app/services/authorization_service.py | 16 +- .../app/services/bulk_scan_orchestrator.py | 18 +- backend/app/services/command_sandbox.py | 10 +- docs/security/SECURITY.md | 193 ++++++++++++++++++ fix_async_issues.py | 90 ++++++++ 21 files changed, 357 insertions(+), 74 deletions(-) create mode 100644 docs/security/SECURITY.md create mode 100644 fix_async_issues.py diff --git a/backend/app/audit_db.py b/backend/app/audit_db.py index 0a9aed6c..5ac624f2 100644 --- a/backend/app/audit_db.py +++ b/backend/app/audit_db.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) -async def log_audit_event( +def log_audit_event( db: Session, action: str, resource_type: str, @@ -68,7 +68,7 @@ async def log_audit_event( return False -async def log_login_event( +def log_login_event( db: Session, username: str, user_id: Optional[int], @@ -83,7 +83,7 @@ async def log_login_event( if failure_reason and not success: details += f" - Reason: {failure_reason}" - return await log_audit_event( + return log_audit_event( db=db, action=action, resource_type="auth", @@ -94,7 +94,7 @@ async def log_login_event( ) -async def log_scan_event( +def log_scan_event( db: Session, action: str, scan_id: Optional[str], @@ -108,7 +108,7 @@ async def log_scan_event( if host_name: scan_details += f" on host {host_name}" - return await log_audit_event( + return log_audit_event( db=db, action=f"SCAN_{action.upper()}", resource_type="scan", @@ -119,7 +119,7 @@ async def log_scan_event( ) -async def log_host_event( +def log_host_event( db: Session, action: str, host_id: Optional[str], @@ -131,7 +131,7 @@ async def log_host_event( """Log host-related events to database""" host_details = details or f"{action.title()} host: {host_name}" - return await log_audit_event( + return log_audit_event( db=db, action=f"HOST_{action.upper()}", resource_type="host", @@ -142,7 +142,7 @@ async def log_host_event( ) -async def log_user_event( +def log_user_event( db: Session, action: str, target_user_id: Optional[str], @@ -154,7 +154,7 @@ async def log_user_event( """Log user management events to database""" user_details = details or f"{action.title()} user: {target_username}" - return await log_audit_event( + return log_audit_event( db=db, action=f"USER_{action.upper()}", resource_type="user", @@ -165,7 +165,7 @@ async def log_user_event( ) -async def log_security_event( +def log_security_event( db: Session, event_type: str, ip_address: str, @@ -173,7 +173,7 @@ async def log_security_event( details: Optional[str] = None ) -> bool: """Log security-related events to database""" - return await log_audit_event( + return log_audit_event( db=db, action=f"SECURITY_{event_type.upper()}", resource_type="security", @@ -183,7 +183,7 @@ async def log_security_event( ) -async def log_admin_event( +def log_admin_event( db: Session, action: str, user_id: int, @@ -192,7 +192,7 @@ async def log_admin_event( details: Optional[str] = None ) -> bool: """Log administrative actions to database""" - return await log_audit_event( + return log_audit_event( db=db, action=f"ADMIN_{action.upper()}", resource_type=resource_type, diff --git a/backend/app/auth.py b/backend/app/auth.py index 97607ac7..3917e22d 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -274,7 +274,7 @@ def log_security_event(self, event_type: str, details: str, ip_address: str): f"SECURITY_{event_type} - Details: {details}, IP: {ip_address}" ) - async def log_api_key_action(self, user_id: str, action: str, api_key_id: str, + def log_api_key_action(self, user_id: str, action: str, api_key_id: str, api_key_name: str, details: Optional[Dict] = None): """Log API key related actions""" self.audit_logger.info( @@ -286,7 +286,7 @@ async def log_api_key_action(self, user_id: str, action: str, api_key_id: str, audit_logger = SecurityAuditLogger() -async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]: +def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]: """Get current authenticated user from JWT token or API key""" from sqlalchemy.orm import Session from .database import get_db, ApiKey @@ -405,7 +405,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: return None -async def require_admin(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]: +def require_admin(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]: """Require admin role for protected endpoints""" if current_user.get("role") != UserRole.SUPER_ADMIN.value: raise HTTPException( diff --git a/backend/app/celery_app.py b/backend/app/celery_app.py index 976edd63..e2fefde7 100644 --- a/backend/app/celery_app.py +++ b/backend/app/celery_app.py @@ -182,7 +182,7 @@ def revoke_task(self, task_id: str, terminate: bool = True) -> bool: celery_manager = SecureCeleryManager() -async def check_redis_health() -> bool: +def check_redis_health() -> bool: """Check Redis connectivity for health checks""" try: # Parse Redis URL diff --git a/backend/app/database.py b/backend/app/database.py index 0cd7adec..cf7f475a 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -397,7 +397,7 @@ def get_db() -> Session: db.close() -async def create_tables(): +def create_tables(): """Create database tables if they don't exist""" try: Base.metadata.create_all(bind=engine) @@ -407,7 +407,7 @@ async def create_tables(): raise -async def check_database_health() -> bool: +def check_database_health() -> bool: """Check database connectivity for health checks""" try: from sqlalchemy import text diff --git a/backend/app/middleware/authorization_middleware.py b/backend/app/middleware/authorization_middleware.py index 176a4094..a3d54679 100644 --- a/backend/app/middleware/authorization_middleware.py +++ b/backend/app/middleware/authorization_middleware.py @@ -206,7 +206,7 @@ def _match_pattern(self, request_path: str, pattern: str) -> bool: return True - async def _extract_current_user(self, request: Request) -> Optional[Dict[str, Any]]: + def _extract_current_user(self, request: Request) -> Optional[Dict[str, Any]]: """ Extract current user from request authentication """ @@ -353,7 +353,7 @@ async def _extract_host_id_from_body(self, request: Request) -> Optional[str]: return None - async def _get_host_id_from_scan_id(self, scan_id: str) -> Optional[str]: + def _get_host_id_from_scan_id(self, scan_id: str) -> Optional[str]: """ Get host_id associated with a scan_id """ @@ -373,7 +373,7 @@ async def _get_host_id_from_scan_id(self, scan_id: str) -> Optional[str]: logger.error(f"Error getting host_id from scan_id {scan_id}: {e}") return None - async def _get_host_ids_from_group_id(self, group_id: str) -> List[str]: + def _get_host_ids_from_group_id(self, group_id: str) -> List[str]: """ Get all host_ids in a host group """ @@ -424,7 +424,7 @@ async def _extract_bulk_host_ids( return [] - async def _build_authorization_context( + def _build_authorization_context( self, request: Request, current_user: Dict[str, Any] diff --git a/backend/app/middleware/metrics.py b/backend/app/middleware/metrics.py index 8529c1ef..f235067f 100644 --- a/backend/app/middleware/metrics.py +++ b/backend/app/middleware/metrics.py @@ -101,7 +101,7 @@ def _normalize_endpoint(self, path: str) -> str: return normalized_path - async def _record_application_metrics(self, request: Request, response: Response, duration: float): + def _record_application_metrics(self, request: Request, response: Response, duration: float): """Record application-specific metrics based on request/response""" path = request.url.path @@ -161,7 +161,7 @@ class DatabaseMetricsCollector: def __init__(self): self.metrics = get_metrics_instance() - async def record_query_metrics(self, operation: str, duration: float): + def record_query_metrics(self, operation: str, duration: float): """Record database query metrics""" from ..services.prometheus_metrics import database_query_duration_seconds database_query_duration_seconds.labels(operation=operation).observe(duration) diff --git a/backend/app/plugins/interface.py b/backend/app/plugins/interface.py index 9c26f545..d03304bb 100644 --- a/backend/app/plugins/interface.py +++ b/backend/app/plugins/interface.py @@ -107,7 +107,7 @@ async def cleanup(self) -> bool: """Cleanup plugin resources. Return True if successful.""" pass - async def health_check(self) -> Dict: + def health_check(self) -> Dict: """Perform plugin health check""" return { "status": "healthy" if self.enabled else "disabled", @@ -142,7 +142,7 @@ async def validate_content(self, content_path: str) -> bool: """Validate SCAP content compatibility with this scanner""" pass - async def get_supported_profiles(self, content_path: str) -> List[Dict]: + def get_supported_profiles(self, content_path: str) -> List[Dict]: """Get profiles supported by this scanner""" return [] @@ -161,7 +161,7 @@ def get_supported_formats(self) -> List[str]: """Get list of supported report formats""" pass - async def get_report_template(self, format_type: str) -> Optional[str]: + def get_report_template(self, format_type: str) -> Optional[str]: """Get report template for the specified format""" return None @@ -186,7 +186,7 @@ async def get_remediation_plan(self, failed_rules: List[str], """Get remediation plan for multiple failed rules""" pass - async def validate_remediation(self, rule_id: str, host_config: Dict) -> Dict: + def validate_remediation(self, rule_id: str, host_config: Dict) -> Dict: """Validate that remediation was successful""" return {"status": "unknown", "validated": False} @@ -205,7 +205,7 @@ async def import_content(self, source_config: Dict) -> Optional[str]: """Import SCAP content from external source""" pass - async def sync_hosts(self, source_config: Dict) -> List[Dict]: + def sync_hosts(self, source_config: Dict) -> List[Dict]: """Synchronize host inventory from external system""" return [] @@ -228,7 +228,7 @@ async def validate_content_integrity(self, content_path: str) -> bool: """Validate content integrity and authenticity""" pass - async def get_content_metadata(self, content_id: str) -> Dict: + def get_content_metadata(self, content_id: str) -> Dict: """Get metadata for specific content""" return {} @@ -247,7 +247,7 @@ async def authorize_action(self, user_info: Dict, action: str, """Check if user is authorized for specific action on resource""" pass - async def get_user_groups(self, user_info: Dict) -> List[str]: + def get_user_groups(self, user_info: Dict) -> List[str]: """Get list of groups for authenticated user""" return [] @@ -266,7 +266,7 @@ def get_supported_types(self) -> List[str]: """Get supported notification types""" pass - async def validate_recipients(self, recipients: List[str]) -> List[str]: + def validate_recipients(self, recipients: List[str]) -> List[str]: """Validate and return valid recipients""" return recipients diff --git a/backend/app/plugins/manager.py b/backend/app/plugins/manager.py index f9c7e71c..89e626ff 100644 --- a/backend/app/plugins/manager.py +++ b/backend/app/plugins/manager.py @@ -184,7 +184,7 @@ def list_plugins(self) -> Dict[str, Dict]: } return plugin_list - async def enable_plugin(self, plugin_name: str) -> bool: + def enable_plugin(self, plugin_name: str) -> bool: """Enable a plugin""" plugin = self.get_plugin(plugin_name) if plugin: @@ -193,7 +193,7 @@ async def enable_plugin(self, plugin_name: str) -> bool: return True return False - async def disable_plugin(self, plugin_name: str) -> bool: + def disable_plugin(self, plugin_name: str) -> bool: """Disable a plugin""" plugin = self.get_plugin(plugin_name) if plugin: @@ -316,7 +316,7 @@ async def _discover_plugins(self): if plugin_file.exists(): await self.load_plugin(str(plugin_file), plugin_dir.name) - async def _load_plugin_configs(self): + def _load_plugin_configs(self): """Load plugin configurations from config directory""" for config_file in self.config_dir.glob("*.json"): try: @@ -338,7 +338,7 @@ def _find_plugin_class(self, module) -> Optional[Type[PluginInterface]]: return attr return None - async def _validate_plugin(self, plugin: PluginInterface) -> bool: + def _validate_plugin(self, plugin: PluginInterface) -> bool: """Validate a plugin meets requirements""" try: metadata = plugin.get_metadata() @@ -378,7 +378,7 @@ async def _register_plugin_hooks(self): if isinstance(plugin, HookablePlugin): await self._register_plugin_hooks_for(plugin) - async def _register_plugin_hooks_for(self, plugin: HookablePlugin): + def _register_plugin_hooks_for(self, plugin: HookablePlugin): """Register hooks for a specific plugin""" for hook_name in plugin.get_registered_hooks(): if hook_name not in self.hook_registry: diff --git a/backend/app/rbac.py b/backend/app/rbac.py index 7038d05b..76f132b1 100644 --- a/backend/app/rbac.py +++ b/backend/app/rbac.py @@ -328,7 +328,7 @@ def check_permission(user_role: str, resource_type: str, action: str): ) -async def check_permission_async(current_user: dict, required_permission: Permission, db: Any = None): +def check_permission_async(current_user: dict, required_permission: Permission, db: Any = None): """Async permission check for specific permissions""" if not current_user: raise HTTPException( diff --git a/backend/app/routes/audit.py b/backend/app/routes/audit.py index 316c102b..a45f45e2 100644 --- a/backend/app/routes/audit.py +++ b/backend/app/routes/audit.py @@ -252,7 +252,7 @@ async def create_audit_log( raise HTTPException(status_code=500, detail="Failed to create audit log") # Helper function to create audit logs from middleware -async def log_audit_event( +def log_audit_event( db: Session, user_id: Optional[int], action: str, diff --git a/backend/app/routes/capabilities.py b/backend/app/routes/capabilities.py index 917ae178..7865b849 100644 --- a/backend/app/routes/capabilities.py +++ b/backend/app/routes/capabilities.py @@ -192,7 +192,7 @@ async def get_integration_status( # Helper functions -async def _detect_license_info() -> Dict[str, Any]: +def _detect_license_info() -> Dict[str, Any]: """Detect license type and enterprise features availability""" # For OSS version, return basic license info # In enterprise version, this would check actual license files @@ -238,7 +238,7 @@ async def _determine_feature_flags(license_info: Dict, settings) -> FeatureFlags return features -async def _calculate_system_limits(license_info: Dict, settings) -> SystemLimits: +def _calculate_system_limits(license_info: Dict, settings) -> SystemLimits: """Calculate system limits based on license and configuration""" limits = SystemLimits() @@ -292,7 +292,7 @@ async def _check_integrations() -> IntegrationStatus: return integrations -async def _check_aegis_availability() -> bool: +def _check_aegis_availability() -> bool: """Check if AEGIS remediation service is available""" try: # In a real implementation, this would check AEGIS connectivity @@ -303,7 +303,7 @@ async def _check_aegis_availability() -> bool: return False -async def _get_aegis_version() -> str: +def _get_aegis_version() -> str: """Get AEGIS version if available""" try: # In a real implementation, this would query AEGIS API @@ -370,7 +370,7 @@ async def _check_kubernetes_availability() -> bool: return False -async def _get_system_info() -> Dict[str, Any]: +def _get_system_info() -> Dict[str, Any]: """Get basic system information""" import platform import psutil diff --git a/backend/app/routes/mfa.py b/backend/app/routes/mfa.py index 16174320..df747ba0 100644 --- a/backend/app/routes/mfa.py +++ b/backend/app/routes/mfa.py @@ -86,7 +86,7 @@ class MFADisableRequest(BaseModel): # Audit Logging -async def log_mfa_action( +def log_mfa_action( db: Session, user_id: int, action: str, diff --git a/backend/app/routes/rule_scanning.py b/backend/app/routes/rule_scanning.py index e8110e55..0ce7731b 100644 --- a/backend/app/routes/rule_scanning.py +++ b/backend/app/routes/rule_scanning.py @@ -365,7 +365,7 @@ async def create_remediation_plan( raise HTTPException(status_code=500, detail="Failed to create remediation plan") -async def _store_rule_scan_results(db: Session, scan_results: dict): +def _store_rule_scan_results(db: Session, scan_results: dict): """Store rule scan results in database""" try: for rule_result in scan_results.get("rule_results", []): @@ -401,7 +401,7 @@ async def _store_rule_scan_results(db: Session, scan_results: dict): db.rollback() -async def _store_remediation_plan(db: Session, plan, created_by: int): +def _store_remediation_plan(db: Session, plan, created_by: int): """Store remediation plan in database""" try: import json @@ -440,7 +440,7 @@ async def _store_remediation_plan(db: Session, plan, created_by: int): db.rollback() -async def _update_remediation_plan_status(db: Session, aegis_remediation_id: str, verification_report: dict): +def _update_remediation_plan_status(db: Session, aegis_remediation_id: str, verification_report: dict): """Update remediation plan status after verification""" try: # Determine status based on verification results diff --git a/backend/app/routes/system_settings.py b/backend/app/routes/system_settings.py index 954d8835..5c0d2fa4 100644 --- a/backend/app/routes/system_settings.py +++ b/backend/app/routes/system_settings.py @@ -542,7 +542,7 @@ async def delete_ssh_key_from_credentials( scheduler_instance = None -async def restore_scheduler_state(): +def restore_scheduler_state(): """ Restore scheduler state from database on application startup """ diff --git a/backend/app/routes/system_settings_unified.py b/backend/app/routes/system_settings_unified.py index d38d0933..ad23b731 100644 --- a/backend/app/routes/system_settings_unified.py +++ b/backend/app/routes/system_settings_unified.py @@ -812,7 +812,7 @@ async def update_scheduler( ) -async def restore_scheduler_state(): +def restore_scheduler_state(): """Restore scheduler state from database on startup""" logger.info("restore_scheduler_state() function called") try: diff --git a/backend/app/routes/v1/remediation.py b/backend/app/routes/v1/remediation.py index cbabd668..1a44a154 100644 --- a/backend/app/routes/v1/remediation.py +++ b/backend/app/routes/v1/remediation.py @@ -556,7 +556,7 @@ async def _execute_manual_remediation(job_id, scan_id, host_id, failed_rules, op logger.info(f"Manual remediation job {job_id} completed (simulated)") -async def _check_aegis_status(): +def _check_aegis_status(): """Check AEGIS provider status""" settings = get_settings() aegis_url = getattr(settings, 'aegis_url', None) diff --git a/backend/app/services/authorization_service.py b/backend/app/services/authorization_service.py index 1341bf8b..18cc7cbc 100644 --- a/backend/app/services/authorization_service.py +++ b/backend/app/services/authorization_service.py @@ -323,7 +323,7 @@ async def _evaluate_permission( confidence_score=0.0 ) - async def _get_applicable_policies( + def _get_applicable_policies( self, user_id: str, resource: ResourceIdentifier, @@ -457,7 +457,7 @@ def _evaluate_policies(self, policies: List[Dict]) -> Tuple[AuthorizationDecisio return self.config.default_decision, "Applied default decision" - async def _evaluate_role_permissions( + def _evaluate_role_permissions( self, user_id: str, resource: ResourceIdentifier, @@ -521,7 +521,7 @@ def _combine_decisions( else: return AuthorizationDecision.DENY, f"Role check failed: {role_decision[1]}" - async def _build_user_context(self, user_id: str) -> AuthorizationContext: + def _build_user_context(self, user_id: str) -> AuthorizationContext: """ Build authorization context for a user """ @@ -565,7 +565,7 @@ async def _build_user_context(self, user_id: str) -> AuthorizationContext: user_groups=[] ) - async def _validate_user(self, user_id: str) -> bool: + def _validate_user(self, user_id: str) -> bool: """ Validate user exists and is active """ @@ -580,7 +580,7 @@ async def _validate_user(self, user_id: str) -> bool: logger.error(f"User validation error for {user_id}: {e}") return False - async def _audit_authorization_decision( + def _audit_authorization_decision( self, result: AuthorizationResult, context: AuthorizationContext @@ -645,7 +645,7 @@ async def _audit_authorization_decision( except Exception as e: logger.error(f"Failed to audit authorization decision: {e}") - async def _audit_bulk_authorization( + def _audit_bulk_authorization( self, request: BulkAuthorizationRequest, decision: AuthorizationDecision, @@ -813,7 +813,7 @@ async def _evaluate_parallel_permissions( # Permission Management Methods - async def grant_host_permission( + def grant_host_permission( self, user_id: Optional[str], group_id: Optional[str], @@ -879,7 +879,7 @@ async def grant_host_permission( self.db.rollback() raise - async def revoke_permission(self, permission_id: str) -> bool: + def revoke_permission(self, permission_id: str) -> bool: """ Revoke a specific permission """ diff --git a/backend/app/services/bulk_scan_orchestrator.py b/backend/app/services/bulk_scan_orchestrator.py index 45bc707a..68ac6edf 100644 --- a/backend/app/services/bulk_scan_orchestrator.py +++ b/backend/app/services/bulk_scan_orchestrator.py @@ -409,7 +409,7 @@ async def _find_optimal_content_profile(self, hosts: List[HostInfo], template_id # Use default content and specified template return 1, template_id if template_id != "auto" else "xccdf_org.ssgproject.content_profile_cui" - async def _create_batch_scans(self, batch: ScanBatch, session_id: str, name_prefix: str, user_id: str, stagger_delay: int) -> List[str]: + def _create_batch_scans(self, batch: ScanBatch, session_id: str, name_prefix: str, user_id: str, stagger_delay: int) -> List[str]: """Create individual scan records for a batch""" try: scan_ids = [] @@ -457,7 +457,7 @@ async def _create_batch_scans(self, batch: ScanBatch, session_id: str, name_pref logger.error(f"Error creating batch scans: {e}") raise - async def _store_scan_session(self, session: ScanSession): + def _store_scan_session(self, session: ScanSession): """Store scan session in database""" try: # Create a scan sessions table record (you'll need to create this table) @@ -488,7 +488,7 @@ async def _store_scan_session(self, session: ScanSession): logger.error(f"Error storing scan session: {e}") raise - async def _update_scan_session(self, session: ScanSession): + def _update_scan_session(self, session: ScanSession): """Update scan session in database""" try: self.db.execute(text(""" @@ -518,7 +518,7 @@ async def _update_scan_session(self, session: ScanSession): logger.error(f"Error updating scan session: {e}") raise - async def _get_scan_session(self, session_id: str) -> Optional[ScanSession]: + def _get_scan_session(self, session_id: str) -> Optional[ScanSession]: """Retrieve scan session from database""" try: result = self.db.execute(text(""" @@ -550,7 +550,7 @@ async def _get_scan_session(self, session_id: str) -> Optional[ScanSession]: logger.error(f"Error getting scan session: {e}") return None - async def _get_scans_status(self, scan_ids: List[str]) -> List[Dict]: + def _get_scans_status(self, scan_ids: List[str]) -> List[Dict]: """Get status of multiple scans""" if not scan_ids: return [] @@ -592,7 +592,7 @@ async def _get_scans_status(self, scan_ids: List[str]) -> List[Dict]: logger.error(f"Error getting scans status: {e}") return [] - async def _execute_staggered_scans(self, scan_ids: List[str]) -> List[str]: + def _execute_staggered_scans(self, scan_ids: List[str]) -> List[str]: """Execute scans with staggered start times""" # For now, just update all scans to running status # In a production system, this would integrate with Celery or similar @@ -726,7 +726,7 @@ async def _validate_bulk_scan_authorization( return [], authorization_failures - async def _build_user_authorization_context(self, user_id: str) -> AuthorizationContext: + def _build_user_authorization_context(self, user_id: str) -> AuthorizationContext: """ Build authorization context for a user including roles and groups """ @@ -769,7 +769,7 @@ async def _build_user_authorization_context(self, user_id: str) -> Authorization user_groups=[] ) - async def _get_host_details(self, host_ids: List[str]) -> List[Dict]: + def _get_host_details(self, host_ids: List[str]) -> List[Dict]: """ Get host details for authorization results """ @@ -801,7 +801,7 @@ async def _get_host_details(self, host_ids: List[str]) -> List[Dict]: logger.error(f"Error getting host details: {e}") return [{'id': host_id, 'hostname': 'unknown', 'display_name': 'unknown'} for host_id in host_ids] - async def _create_batch_scans_with_authorization( + def _create_batch_scans_with_authorization( self, batch: ScanBatch, session_id: str, diff --git a/backend/app/services/command_sandbox.py b/backend/app/services/command_sandbox.py index 6510da90..d20dd8d5 100644 --- a/backend/app/services/command_sandbox.py +++ b/backend/app/services/command_sandbox.py @@ -143,7 +143,7 @@ async def _create_sandbox(self): logger.error(f"Failed to create sandbox {self.sandbox_id}: {e}") raise - async def _setup_sandbox_tools(self): + def _setup_sandbox_tools(self): """Install essential tools in sandbox""" try: # Update package lists @@ -165,7 +165,7 @@ async def _setup_sandbox_tools(self): except Exception as e: logger.warning(f"Failed to setup sandbox tools: {e}") - async def execute_command(self, command: str, timeout: int = 300) -> Tuple[int, str, str]: + def execute_command(self, command: str, timeout: int = 300) -> Tuple[int, str, str]: """Execute command in sandbox with timeout""" if not self.container: raise RuntimeError("Sandbox not initialized") @@ -188,7 +188,7 @@ async def execute_command(self, command: str, timeout: int = 300) -> Tuple[int, logger.error(f"Command execution failed in sandbox {self.sandbox_id}: {e}") raise - async def _cleanup_sandbox(self): + def _cleanup_sandbox(self): """Clean up sandbox container""" try: if self.container: @@ -377,7 +377,7 @@ def validate_command_parameters(self, command_id: str, parameters: Dict[str, Any return True - async def request_command_execution(self, command_id: str, parameters: Dict[str, Any], + def request_command_execution(self, command_id: str, parameters: Dict[str, Any], target_host: str, requested_by: str, justification: str) -> ExecutionRequest: """Request execution of a secure command""" @@ -405,7 +405,7 @@ async def request_command_execution(self, command_id: str, parameters: Dict[str, return request - async def approve_request(self, request_id: str, approved_by: str) -> bool: + def approve_request(self, request_id: str, approved_by: str) -> bool: """Approve a pending execution request""" if request_id not in self.execution_requests: return False diff --git a/docs/security/SECURITY.md b/docs/security/SECURITY.md new file mode 100644 index 00000000..84e7a2b0 --- /dev/null +++ b/docs/security/SECURITY.md @@ -0,0 +1,193 @@ +# OpenWatch Security Architecture + +## Overview + +OpenWatch implements a comprehensive FIPS 140-2 compliant security architecture for secure OpenSCAP scanning operations. This document outlines the security controls, cryptographic implementations, and compliance measures. + +## FIPS 140-2 Compliance + +### Cryptographic Modules +- **AES-256-GCM**: Data encryption at rest and in transit +- **RSA-2048**: Digital signatures for JWT tokens +- **SHA-256**: Hash functions and key derivation +- **Argon2id**: Password hashing (FIPS approved) +- **PBKDF2**: Key derivation with 100,000 iterations + +### Validation Status +- OpenSSL FIPS module validation required for production +- Cryptographic operations use only FIPS-approved algorithms +- Runtime FIPS mode validation on application startup + +## Security Architecture + +### Transport Security +``` +┌─────────────────────────────────────────────────────────────┐ +│ TLS 1.3 Layer │ +│ ┌─────────────────────────────────────────────────────────┤ +│ │ Application Security Layer │ +│ │ ┌─────────────────────────────────────────────────────┤ +│ │ │ Data Security Layer │ +│ │ │ ┌─────────────────────────────────────────────────┤ +│ │ │ │ Infrastructure Security │ +└──┴──┴──┴─────────────────────────────────────────────────────┘ +``` + +### Authentication Flow +1. **User Authentication**: RSA-2048 signed JWT tokens +2. **Service Authentication**: Mutual TLS between services +3. **SSH Authentication**: Encrypted private keys for remote scans +4. **Database Authentication**: SCRAM-SHA-256 with TLS + +### Authorization Model +- **Role-Based Access Control (RBAC)** + - `admin`: Full system access + - `user`: Limited scan operations +- **Resource-Level Permissions**: Host and scan access controls +- **API Endpoint Protection**: JWT token validation required + +## Data Protection + +### Encryption at Rest +- **Database**: PostgreSQL with TDE (Transparent Data Encryption) +- **Credentials**: AES-256-GCM encryption for SSH keys and passwords +- **Files**: SCAP content and results encrypted on disk +- **Logs**: Audit logs with integrity protection + +### Encryption in Transit +- **HTTPS/TLS 1.3**: All client communications +- **Database TLS**: Encrypted PostgreSQL connections +- **Redis TLS**: Secure Celery message passing +- **SSH**: OpenSCAP remote scanning operations + +### Key Management +- **Master Key**: Environment-based encryption key +- **JWT Keys**: RSA-2048 key pair for token signing +- **TLS Certificates**: X.509 certificates for service communication +- **SSH Keys**: Per-host encrypted private keys + +## Network Security + +### Network Segmentation +``` +Internet ┌──────────────────────────────────────────────────────┐ + ↓ │ Load Balancer │ +┌─────────┼──────────────────────────────────────────────────────┤ +│ DMZ │ Frontend (HTTPS) │ +├─────────┼──────────────────────────────────────────────────────┤ +│ App Tier│ Backend API (mTLS) │ +├─────────┼──────────────────────────────────────────────────────┤ +│Data Tier│ Database + Redis (Encrypted) │ +└─────────┴──────────────────────────────────────────────────────┘ +``` + +### Security Headers +- **HSTS**: HTTP Strict Transport Security +- **CSP**: Content Security Policy +- **X-Frame-Options**: Clickjacking protection +- **X-Content-Type-Options**: MIME sniffing protection + +## Audit and Compliance + +### Security Logging +- **Authentication Events**: Login attempts and failures +- **Authorization Events**: Access control decisions +- **Scan Operations**: All OpenSCAP operations logged +- **System Events**: Configuration changes and errors + +### Audit Trail +- **Tamper Evident**: Cryptographic integrity protection +- **Non-Repudiation**: Digital signatures on critical events +- **Retention**: Configurable log retention periods +- **Export**: SIEM integration capabilities + +### Compliance Reporting +- **FIPS Validation**: Real-time compliance status +- **Security Metrics**: Authentication and authorization metrics +- **Vulnerability Scanning**: Regular security assessments +- **Penetration Testing**: Periodic security validation + +## Secure Development + +### Security Testing +- **Static Analysis**: Bandit security linting +- **Dependency Scanning**: Safety vulnerability checks +- **Secret Detection**: Pre-commit hook scanning +- **Dynamic Analysis**: Runtime security testing + +### Code Security +- **Input Validation**: All user inputs sanitized +- **SQL Injection**: Parameterized queries only +- **XSS Protection**: Output encoding and CSP +- **CSRF Protection**: Token-based protection + +## Operational Security + +### Container Security +- **Base Images**: FIPS-compliant Red Hat UBI +- **Vulnerability Scanning**: Regular image updates +- **Runtime Security**: Non-root container execution +- **Resource Limits**: CPU and memory constraints + +### Infrastructure Security +- **Secrets Management**: Environment-based configuration +- **Access Control**: Principle of least privilege +- **Network Policies**: Kubernetes network segmentation +- **Monitoring**: Security event monitoring + +## Incident Response + +### Security Monitoring +- **Failed Authentication**: Account lockout after 5 attempts +- **Unusual Activity**: Anomaly detection and alerting +- **System Health**: Continuous security posture monitoring +- **Threat Detection**: Real-time security event analysis + +### Response Procedures +1. **Detection**: Automated security event detection +2. **Analysis**: Security team investigation +3. **Containment**: Threat isolation and mitigation +4. **Eradication**: Root cause remediation +5. **Recovery**: Service restoration +6. **Lessons Learned**: Post-incident review + +## Configuration Management + +### Security Baselines +- **CIS Benchmarks**: Container and OS hardening +- **NIST Guidelines**: Security control implementation +- **DISA STIGs**: Military security requirements +- **Custom Policies**: Organization-specific controls + +### Secure Defaults +- **Encryption Enabled**: All data encrypted by default +- **Strong Authentication**: Multi-factor authentication required +- **Least Privilege**: Minimal permission grants +- **Audit Logging**: Comprehensive security logging + +## Disaster Recovery + +### Backup Security +- **Encrypted Backups**: AES-256 encryption for all backups +- **Key Escrow**: Secure key recovery procedures +- **Offsite Storage**: Geographically distributed backups +- **Recovery Testing**: Regular disaster recovery drills + +### Business Continuity +- **High Availability**: Multi-instance deployments +- **Failover Procedures**: Automated service failover +- **Data Replication**: Real-time data synchronization +- **Recovery Objectives**: RTO < 4 hours, RPO < 1 hour + +## Security Contacts + +- **Security Team**: security@openwatch.example.com +- **Incident Response**: incident@openwatch.example.com +- **Vulnerability Reports**: security-issues@openwatch.example.com + +## References + +- [NIST SP 800-53](https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final) +- [FIPS 140-2](https://csrc.nist.gov/publications/detail/fips/140/2/final) +- [OWASP Security Guidelines](https://owasp.org/www-project-application-security-verification-standard/) +- [CIS Controls](https://www.cisecurity.org/controls) \ No newline at end of file diff --git a/fix_async_issues.py b/fix_async_issues.py new file mode 100644 index 00000000..c6e6170f --- /dev/null +++ b/fix_async_issues.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Script to fix async/await issues reported by SonarCloud +Removes async keyword from functions that don't use async features +""" +import re +import os +from pathlib import Path + +# Files with async issues based on SonarCloud report +FILES_TO_FIX = { + "backend/app/audit_db.py": [14], + "backend/app/auth.py": [277, 289, 408], + "backend/app/celery_app.py": [185], + "backend/app/database.py": [400, 410], + "backend/app/middleware/authorization_middleware.py": [209, 356, 376, 427], + "backend/app/middleware/metrics.py": [104, 164], + "backend/app/plugins/interface.py": [110, 145, 164, 189, 208, 231, 250, 269], + "backend/app/plugins/manager.py": [187, 196, 319, 341, 381], + "backend/app/rbac.py": [331], + "backend/app/routes/audit.py": [255], + "backend/app/routes/capabilities.py": [195, 241, 295, 306, 373], + "backend/app/routes/mfa.py": [89], + "backend/app/routes/rule_scanning.py": [368, 404, 443], + "backend/app/routes/system_settings.py": [545], + "backend/app/routes/system_settings_unified.py": [815], + "backend/app/routes/v1/remediation.py": [559], + "backend/app/services/authorization_service.py": [326, 460, 524, 568, 583, 648, 816, 882], + "backend/app/services/bulk_scan_orchestrator.py": [412, 460, 491, 521, 553, 595, 729, 772, 804], + "backend/app/services/command_sandbox.py": [146, 168, 191, 380, 408], +} + +def remove_async_from_function(content: str, line_number: int) -> str: + """Remove async keyword from a specific function""" + lines = content.split('\n') + + # Adjust for 0-based indexing + idx = line_number - 1 + + if idx < len(lines): + line = lines[idx] + # Check if this line has async def + if 'async def' in line: + # Replace async def with def + lines[idx] = line.replace('async def', 'def') + print(f" Fixed line {line_number}: {line.strip()[:60]}...") + + return '\n'.join(lines) + +def fix_file(filepath: str, line_numbers: list): + """Fix async issues in a single file""" + full_path = Path(filepath) + if not full_path.exists(): + print(f"⚠️ File not found: {filepath}") + return + + print(f"\n📄 Processing {filepath}") + + # Read the file + with open(full_path, 'r') as f: + content = f.read() + + # Fix each line + for line_num in sorted(line_numbers, reverse=True): + content = remove_async_from_function(content, line_num) + + # Write back + with open(full_path, 'w') as f: + f.write(content) + + print(f"✅ Fixed {len(line_numbers)} async issues") + +def main(): + """Main function to fix all async issues""" + print("🔧 Fixing async/await issues reported by SonarCloud\n") + + total_issues = sum(len(lines) for lines in FILES_TO_FIX.values()) + print(f"Total issues to fix: {total_issues}") + + for filepath, line_numbers in FILES_TO_FIX.items(): + fix_file(filepath, line_numbers) + + print(f"\n✨ Completed fixing {total_issues} async/await issues!") + print("\nNext steps:") + print("1. Review the changes with: git diff") + print("2. Run tests to ensure nothing broke") + print("3. Commit the changes") + +if __name__ == "__main__": + main() \ No newline at end of file