""" FastAPI Dashboard Backend for SHHH Secrets Sentinel Provides REST API endpoints for quarantine management and system monitoring. """ import asyncio from datetime import datetime from typing import List, Optional from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse import structlog from .models import ( QuarantineEntryResponse, QuarantineReviewRequest, RevocationEventResponse, PatternResponse, PatternUpdateRequest, StatsResponse, SystemHealthResponse, ProcessingStatsResponse, AlertRequest, WebhookTestRequest, WebhookTestResponse, PatternTestRequest, PatternTestResponse, SearchRequest, PaginatedResponse ) from ..core.quarantine import QuarantineManager, QuarantineEntry from ..core.detector import SecretDetector from ..automation.revocation import SecretRevoker from ..pipeline.processor import MessageProcessor logger = structlog.get_logger() # Global components (initialized in lifespan) quarantine_manager: Optional[QuarantineManager] = None detector: Optional[SecretDetector] = None revoker: Optional[SecretRevoker] = None processor: Optional[MessageProcessor] = None @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager""" global quarantine_manager, detector, revoker, processor try: # Initialize components logger.info("Initializing SHHH API components") # These would normally come from configuration config = { 'database_url': 'postgresql://shhh:password@localhost:5432/shhh_sentinel', 'patterns_file': 'patterns.yaml', 'revocation_webhooks': { 'AWS_ACCESS_KEY': 'https://security.chorus.services/hooks/aws-revoke', 'GITHUB_TOKEN': 'https://security.chorus.services/hooks/github-revoke', 'SLACK_TOKEN': 'https://security.chorus.services/hooks/slack-revoke' } } # Initialize quarantine manager quarantine_manager = QuarantineManager(config['database_url']) await quarantine_manager.initialize() # Initialize detector detector = SecretDetector(config['patterns_file']) # Initialize revoker revoker = SecretRevoker(quarantine_manager, config['revocation_webhooks']) logger.info("SHHH API components initialized successfully") yield except Exception as e: logger.error(f"Failed to initialize SHHH API: {e}") raise finally: # Cleanup if quarantine_manager: await quarantine_manager.close() logger.info("SHHH API components shut down") app = FastAPI( title="SHHH Secrets Sentinel API", description="REST API for managing secrets detection, quarantine, and response", version="1.0.0", lifespan=lifespan ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # Configure appropriately for production allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Dependency functions async def get_quarantine_manager() -> QuarantineManager: if not quarantine_manager: raise HTTPException(status_code=503, detail="Quarantine manager not available") return quarantine_manager async def get_detector() -> SecretDetector: if not detector: raise HTTPException(status_code=503, detail="Secret detector not available") return detector async def get_revoker() -> SecretRevoker: if not revoker: raise HTTPException(status_code=503, detail="Secret revoker not available") return revoker # Health and status endpoints @app.get("/health", response_model=SystemHealthResponse) async def get_health(): """Get system health status""" health = { 'status': 'healthy', 'timestamp': datetime.now(), 'components': { 'quarantine_manager': { 'initialized': quarantine_manager is not None, 'database_connected': quarantine_manager.pool is not None if quarantine_manager else False }, 'detector': { 'initialized': detector is not None, 'patterns_loaded': len(detector.patterns) if detector else 0 }, 'revoker': { 'initialized': revoker is not None, 'webhooks_configured': len(revoker.webhook_config) if revoker else 0 } } } return health @app.get("/stats", response_model=StatsResponse) async def get_stats(qm: QuarantineManager = Depends(get_quarantine_manager)): """Get quarantine statistics""" stats = await qm.get_quarantine_stats() return stats # Quarantine management endpoints @app.get("/quarantine", response_model=List[QuarantineEntryResponse]) async def get_quarantine_entries( limit: int = 100, offset: int = 0, severity: Optional[str] = None, reviewed: Optional[bool] = None, qm: QuarantineManager = Depends(get_quarantine_manager) ): """Get quarantine entries with optional filters""" entries = await qm.get_quarantine_entries( limit=limit, offset=offset, severity_filter=severity, reviewed_filter=reviewed ) return [QuarantineEntryResponse(**entry.__dict__) for entry in entries] @app.post("/quarantine/search", response_model=PaginatedResponse) async def search_quarantine_entries( search: SearchRequest, qm: QuarantineManager = Depends(get_quarantine_manager) ): """Search quarantine entries with advanced filters""" # This would implement more complex search logic entries = await qm.get_quarantine_entries( limit=search.limit, offset=search.offset, severity_filter=search.severity, reviewed_filter=search.reviewed ) items = [QuarantineEntryResponse(**entry.__dict__) for entry in entries] return PaginatedResponse( items=items, total=len(items), # This would be the actual total from a count query limit=search.limit, offset=search.offset, has_more=len(items) == search.limit ) @app.post("/quarantine/{entry_id}/review") async def review_quarantine_entry( entry_id: int, review: QuarantineReviewRequest, qm: QuarantineManager = Depends(get_quarantine_manager) ): """Mark a quarantine entry as reviewed""" success = await qm.mark_reviewed(entry_id, review.action, review.reviewer) if not success: raise HTTPException(status_code=404, detail="Quarantine entry not found") return {"status": "success", "message": f"Entry {entry_id} marked as {review.action}"} @app.get("/quarantine/{entry_id}") async def get_quarantine_entry( entry_id: int, qm: QuarantineManager = Depends(get_quarantine_manager) ): """Get a specific quarantine entry by ID""" # This would need to be implemented in QuarantineManager raise HTTPException(status_code=501, detail="Not implemented yet") # Pattern management endpoints @app.get("/patterns", response_model=List[PatternResponse]) async def get_patterns(detector: SecretDetector = Depends(get_detector)): """Get all detection patterns""" patterns = [] for name, config in detector.patterns.items(): patterns.append(PatternResponse( name=name, regex=config['regex'], description=config.get('description', ''), severity=config.get('severity', 'MEDIUM'), confidence=config.get('confidence', 0.8), active=config.get('active', True) )) return patterns @app.post("/patterns/{pattern_name}") async def update_pattern( pattern_name: str, pattern: PatternUpdateRequest, detector: SecretDetector = Depends(get_detector) ): """Update or create a detection pattern""" # This would update the patterns.yaml file # For now, just update in memory detector.patterns[pattern_name] = { 'regex': pattern.regex, 'description': pattern.description, 'severity': pattern.severity, 'confidence': pattern.confidence, 'active': pattern.active } # Recompile regex import re try: detector.patterns[pattern_name]['compiled_regex'] = re.compile( pattern.regex, re.MULTILINE | re.DOTALL ) except re.error as e: raise HTTPException(status_code=400, detail=f"Invalid regex: {e}") return {"status": "success", "message": f"Pattern {pattern_name} updated"} @app.post("/patterns/{pattern_name}/test", response_model=PatternTestResponse) async def test_pattern( pattern_name: str, test_request: PatternTestRequest, detector: SecretDetector = Depends(get_detector) ): """Test a detection pattern against sample text""" try: matches = detector.test_pattern(pattern_name, test_request.test_text) return PatternTestResponse( matches=[match.__dict__ for match in matches], match_count=len(matches) ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @app.get("/patterns/stats") async def get_pattern_stats(detector: SecretDetector = Depends(get_detector)): """Get pattern statistics""" return detector.get_pattern_stats() # Revocation management endpoints @app.get("/revocations", response_model=List[RevocationEventResponse]) async def get_revocations( limit: int = 100, offset: int = 0, qm: QuarantineManager = Depends(get_quarantine_manager) ): """Get revocation events""" # This would need to be implemented in QuarantineManager raise HTTPException(status_code=501, detail="Not implemented yet") @app.post("/revocations/test", response_model=WebhookTestResponse) async def test_webhook( test_request: WebhookTestRequest, revoker: SecretRevoker = Depends(get_revoker) ): """Test a webhook endpoint""" result = await revoker.test_webhook_endpoint(test_request.secret_type) return WebhookTestResponse(**result) @app.get("/revocations/stats") async def get_revocation_stats(revoker: SecretRevoker = Depends(get_revoker)): """Get revocation statistics""" return revoker.get_stats() # Administrative endpoints @app.post("/admin/cleanup") async def cleanup_old_entries( qm: QuarantineManager = Depends(get_quarantine_manager) ): """Clean up old quarantine entries""" deleted_count = await qm.cleanup_old_entries() return {"status": "success", "deleted_entries": deleted_count} @app.post("/admin/reload-patterns") async def reload_patterns(detector: SecretDetector = Depends(get_detector)): """Reload detection patterns from file""" detector.load_patterns() return {"status": "success", "message": "Patterns reloaded"} @app.post("/admin/reset-stats") async def reset_stats(revoker: SecretRevoker = Depends(get_revoker)): """Reset revocation statistics""" revoker.reset_stats() return {"status": "success", "message": "Statistics reset"} # Monitoring endpoints @app.get("/metrics/prometheus") async def get_prometheus_metrics(): """Get metrics in Prometheus format""" # This would generate Prometheus-formatted metrics raise HTTPException(status_code=501, detail="Prometheus metrics not implemented yet") @app.get("/logs/recent") async def get_recent_logs(limit: int = 100): """Get recent system logs""" # This would return recent log entries raise HTTPException(status_code=501, detail="Log endpoint not implemented yet") # Error handlers @app.exception_handler(Exception) async def general_exception_handler(request, exc): logger.error(f"Unhandled exception: {exc}") return JSONResponse( status_code=500, content={"detail": "Internal server error"} ) if __name__ == "__main__": import uvicorn uvicorn.run( "api.main:app", host="127.0.0.1", port=8000, reload=True, log_level="info" )