🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
374 lines
12 KiB
Python
374 lines
12 KiB
Python
"""
|
|
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"
|
|
) |