Pre-cleanup snapshot - all current files

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tony
2025-08-05 02:32:45 +10:00
parent 26079aa8da
commit 4511f4c801
32 changed files with 5072 additions and 0 deletions

View File

@@ -0,0 +1,474 @@
"""
Automated Secret Revocation System for SHHH Secrets Sentinel
Provides automated response capabilities for different types of detected secrets.
"""
import asyncio
import aiohttp
import json
import time
from typing import Dict, Any, Optional, List
from dataclasses import dataclass
from datetime import datetime
import structlog
from ..core.quarantine import QuarantineEntry, RevocationEvent, QuarantineManager
from ..core.detector import SecretMatch
logger = structlog.get_logger()
@dataclass
class RevocationRequest:
"""Represents a request to revoke a secret"""
quarantine_id: int
secret_type: str
redacted_secret: str
urgency: str # 'immediate', 'high', 'medium', 'low'
metadata: Dict[str, Any]
@dataclass
class RevocationResponse:
"""Represents the response from a revocation attempt"""
success: bool
method: str
response_data: Dict[str, Any]
error_message: Optional[str] = None
revocation_id: Optional[str] = None
class SecretRevoker:
"""
Automated secret revocation system that integrates with various cloud providers
and services to automatically disable compromised credentials.
"""
def __init__(self, quarantine_manager: QuarantineManager, webhook_config: Dict[str, str] = None):
self.quarantine = quarantine_manager
self.webhook_config = webhook_config or {}
# Revocation timeouts and retry settings
self.request_timeout = 10 # seconds
self.max_retries = 3
self.retry_delay = 2 # seconds
# Statistics
self.stats = {
'total_revocations': 0,
'successful_revocations': 0,
'failed_revocations': 0,
'revocations_by_type': {},
'last_reset': datetime.now()
}
logger.info("Initialized SecretRevoker")
async def trigger_revocation(self, quarantine_entry: QuarantineEntry) -> Optional[RevocationResponse]:
"""Trigger automatic revocation for a quarantined secret"""
try:
revocation_request = RevocationRequest(
quarantine_id=quarantine_entry.id,
secret_type=quarantine_entry.secret_type,
redacted_secret=self._extract_redacted_from_metadata(quarantine_entry),
urgency=self._determine_urgency(quarantine_entry.severity),
metadata={
'source_agent': quarantine_entry.source_agent,
'detection_timestamp': quarantine_entry.timestamp.isoformat(),
'confidence': quarantine_entry.confidence,
'hypercore_position': quarantine_entry.hypercore_position
}
)
# Determine revocation method
revocation_method = self._get_revocation_method(quarantine_entry.secret_type)
if not revocation_method:
logger.warning(f"No revocation method configured for {quarantine_entry.secret_type}")
return None
# Attempt revocation
response = await self._execute_revocation(revocation_request, revocation_method)
# Record the revocation event
await self._record_revocation_event(quarantine_entry, response)
# Update statistics
self._update_stats(quarantine_entry.secret_type, response.success)
return response
except Exception as e:
logger.error(f"Failed to trigger revocation for quarantine {quarantine_entry.id}: {e}")
return None
def _extract_redacted_from_metadata(self, quarantine_entry: QuarantineEntry) -> str:
"""Extract redacted secret from quarantine metadata"""
try:
matches = quarantine_entry.metadata.get('matches', [])
if matches:
# Get the first match's redacted text
return matches[0].get('redacted_text', 'REDACTED')
except:
pass
return 'REDACTED'
def _determine_urgency(self, severity: str) -> str:
"""Determine revocation urgency based on severity"""
urgency_map = {
'CRITICAL': 'immediate',
'HIGH': 'high',
'MEDIUM': 'medium',
'LOW': 'low'
}
return urgency_map.get(severity, 'medium')
def _get_revocation_method(self, secret_type: str) -> Optional[str]:
"""Get the appropriate revocation method for a secret type"""
method_map = {
'AWS_ACCESS_KEY': 'aws_iam_revocation',
'AWS_SECRET_KEY': 'aws_iam_revocation',
'GITHUB_TOKEN': 'github_token_revocation',
'GITHUB_OAUTH': 'github_token_revocation',
'SLACK_TOKEN': 'slack_token_revocation',
'GOOGLE_API_KEY': 'google_api_revocation',
'DOCKER_TOKEN': 'docker_token_revocation'
}
return method_map.get(secret_type)
async def _execute_revocation(self, request: RevocationRequest, method: str) -> RevocationResponse:
"""Execute the actual revocation based on the method"""
method_handlers = {
'aws_iam_revocation': self._revoke_aws_credentials,
'github_token_revocation': self._revoke_github_token,
'slack_token_revocation': self._revoke_slack_token,
'google_api_revocation': self._revoke_google_api_key,
'docker_token_revocation': self._revoke_docker_token,
'webhook_revocation': self._revoke_via_webhook
}
handler = method_handlers.get(method, self._revoke_via_webhook)
for attempt in range(self.max_retries):
try:
response = await handler(request)
if response.success:
logger.info(
f"Successfully revoked {request.secret_type}",
quarantine_id=request.quarantine_id,
method=method,
attempt=attempt + 1
)
return response
# Log failure and retry if not successful
logger.warning(
f"Revocation attempt {attempt + 1} failed",
quarantine_id=request.quarantine_id,
method=method,
error=response.error_message
)
if attempt < self.max_retries - 1:
await asyncio.sleep(self.retry_delay * (attempt + 1)) # Exponential backoff
except Exception as e:
logger.error(f"Revocation attempt {attempt + 1} error: {e}")
if attempt < self.max_retries - 1:
await asyncio.sleep(self.retry_delay * (attempt + 1))
# All attempts failed
return RevocationResponse(
success=False,
method=method,
response_data={},
error_message=f"All {self.max_retries} revocation attempts failed"
)
async def _revoke_aws_credentials(self, request: RevocationRequest) -> RevocationResponse:
"""Revoke AWS credentials via webhook"""
webhook_url = self.webhook_config.get('AWS_ACCESS_KEY')
if not webhook_url:
return RevocationResponse(
success=False,
method='aws_iam_revocation',
response_data={},
error_message="No AWS revocation webhook configured"
)
payload = {
'event': 'secret_leak_detected',
'secret_type': request.secret_type,
'redacted_key': request.redacted_secret,
'urgency': request.urgency,
'quarantine_id': request.quarantine_id,
'timestamp': datetime.now().isoformat(),
'recommended_action': 'Revoke IAM access key immediately',
'metadata': request.metadata
}
return await self._send_webhook_request(webhook_url, payload, 'aws_iam_revocation')
async def _revoke_github_token(self, request: RevocationRequest) -> RevocationResponse:
"""Revoke GitHub token via webhook"""
webhook_url = self.webhook_config.get('GITHUB_TOKEN')
if not webhook_url:
return RevocationResponse(
success=False,
method='github_token_revocation',
response_data={},
error_message="No GitHub revocation webhook configured"
)
payload = {
'event': 'secret_leak_detected',
'secret_type': request.secret_type,
'redacted_key': request.redacted_secret,
'urgency': request.urgency,
'quarantine_id': request.quarantine_id,
'timestamp': datetime.now().isoformat(),
'recommended_action': 'Revoke GitHub token via API or settings',
'metadata': request.metadata
}
return await self._send_webhook_request(webhook_url, payload, 'github_token_revocation')
async def _revoke_slack_token(self, request: RevocationRequest) -> RevocationResponse:
"""Revoke Slack token via webhook"""
webhook_url = self.webhook_config.get('SLACK_TOKEN')
if not webhook_url:
return RevocationResponse(
success=False,
method='slack_token_revocation',
response_data={},
error_message="No Slack revocation webhook configured"
)
payload = {
'event': 'secret_leak_detected',
'secret_type': request.secret_type,
'redacted_key': request.redacted_secret,
'urgency': request.urgency,
'quarantine_id': request.quarantine_id,
'timestamp': datetime.now().isoformat(),
'recommended_action': 'Revoke Slack token via Admin API',
'metadata': request.metadata
}
return await self._send_webhook_request(webhook_url, payload, 'slack_token_revocation')
async def _revoke_google_api_key(self, request: RevocationRequest) -> RevocationResponse:
"""Revoke Google API key via webhook"""
webhook_url = self.webhook_config.get('GOOGLE_API_KEY')
if not webhook_url:
return RevocationResponse(
success=False,
method='google_api_revocation',
response_data={},
error_message="No Google API revocation webhook configured"
)
payload = {
'event': 'secret_leak_detected',
'secret_type': request.secret_type,
'redacted_key': request.redacted_secret,
'urgency': request.urgency,
'quarantine_id': request.quarantine_id,
'timestamp': datetime.now().isoformat(),
'recommended_action': 'Revoke API key via Google Cloud Console',
'metadata': request.metadata
}
return await self._send_webhook_request(webhook_url, payload, 'google_api_revocation')
async def _revoke_docker_token(self, request: RevocationRequest) -> RevocationResponse:
"""Revoke Docker token via webhook"""
webhook_url = self.webhook_config.get('DOCKER_TOKEN')
if not webhook_url:
return RevocationResponse(
success=False,
method='docker_token_revocation',
response_data={},
error_message="No Docker revocation webhook configured"
)
payload = {
'event': 'secret_leak_detected',
'secret_type': request.secret_type,
'redacted_key': request.redacted_secret,
'urgency': request.urgency,
'quarantine_id': request.quarantine_id,
'timestamp': datetime.now().isoformat(),
'recommended_action': 'Revoke Docker token via Hub settings',
'metadata': request.metadata
}
return await self._send_webhook_request(webhook_url, payload, 'docker_token_revocation')
async def _revoke_via_webhook(self, request: RevocationRequest) -> RevocationResponse:
"""Generic webhook revocation for unknown secret types"""
# Try to find a generic webhook endpoint
webhook_url = self.webhook_config.get('GENERIC',
self.webhook_config.get('DEFAULT'))
if not webhook_url:
return RevocationResponse(
success=False,
method='webhook_revocation',
response_data={},
error_message=f"No webhook configured for {request.secret_type}"
)
payload = {
'event': 'secret_leak_detected',
'secret_type': request.secret_type,
'redacted_key': request.redacted_secret,
'urgency': request.urgency,
'quarantine_id': request.quarantine_id,
'timestamp': datetime.now().isoformat(),
'recommended_action': 'Manual review and revocation required',
'metadata': request.metadata
}
return await self._send_webhook_request(webhook_url, payload, 'webhook_revocation')
async def _send_webhook_request(self, url: str, payload: Dict[str, Any], method: str) -> RevocationResponse:
"""Send webhook request and handle response"""
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.request_timeout)) as session:
async with session.post(url, json=payload) as response:
response_data = {}
try:
response_data = await response.json()
except:
response_data = {'text': await response.text()}
if response.status == 200:
return RevocationResponse(
success=True,
method=method,
response_data=response_data,
revocation_id=response_data.get('revocation_id')
)
else:
return RevocationResponse(
success=False,
method=method,
response_data=response_data,
error_message=f"HTTP {response.status}: {response_data}"
)
except asyncio.TimeoutError:
return RevocationResponse(
success=False,
method=method,
response_data={},
error_message=f"Webhook request timed out after {self.request_timeout}s"
)
except Exception as e:
return RevocationResponse(
success=False,
method=method,
response_data={},
error_message=f"Webhook request failed: {str(e)}"
)
async def _record_revocation_event(self, quarantine_entry: QuarantineEntry, response: RevocationResponse):
"""Record revocation event in the database"""
try:
revocation_event = RevocationEvent(
quarantine_id=quarantine_entry.id,
secret_type=quarantine_entry.secret_type,
revocation_method=response.method,
status='success' if response.success else 'failed',
response_data=response.response_data,
timestamp=datetime.now()
)
await self.quarantine.record_revocation(revocation_event)
except Exception as e:
logger.error(f"Failed to record revocation event: {e}")
def _update_stats(self, secret_type: str, success: bool):
"""Update revocation statistics"""
self.stats['total_revocations'] += 1
if success:
self.stats['successful_revocations'] += 1
else:
self.stats['failed_revocations'] += 1
# Update by-type stats
if secret_type not in self.stats['revocations_by_type']:
self.stats['revocations_by_type'][secret_type] = {
'total': 0,
'successful': 0,
'failed': 0
}
type_stats = self.stats['revocations_by_type'][secret_type]
type_stats['total'] += 1
if success:
type_stats['successful'] += 1
else:
type_stats['failed'] += 1
async def test_webhook_endpoint(self, secret_type: str) -> Dict[str, Any]:
"""Test a webhook endpoint with a test payload"""
webhook_url = self.webhook_config.get(secret_type)
if not webhook_url:
return {
'success': False,
'error': f'No webhook configured for {secret_type}'
}
test_payload = {
'event': 'webhook_test',
'secret_type': secret_type,
'test': True,
'timestamp': datetime.now().isoformat()
}
try:
response = await self._send_webhook_request(webhook_url, test_payload, 'test')
return {
'success': response.success,
'method': response.method,
'response_data': response.response_data,
'error': response.error_message
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
def get_stats(self) -> Dict[str, Any]:
"""Get revocation statistics"""
current_time = datetime.now()
uptime_hours = (current_time - self.stats['last_reset']).total_seconds() / 3600
stats = self.stats.copy()
stats.update({
'uptime_hours': round(uptime_hours, 2),
'success_rate': (
self.stats['successful_revocations'] / max(1, self.stats['total_revocations'])
) * 100,
'configured_webhooks': list(self.webhook_config.keys())
})
return stats
def reset_stats(self):
"""Reset statistics counters"""
self.stats = {
'total_revocations': 0,
'successful_revocations': 0,
'failed_revocations': 0,
'revocations_by_type': {},
'last_reset': datetime.now()
}
logger.info("SecretRevoker statistics reset")