UNPKG

stellar-cyber-mcp-agents

Version:

Model Context Protocol (MCP) server for Stellar Cyber security operations with specialized multi-agent analysis capabilities

889 lines 34.2 kB
import { BaseAgent } from '../core/base-agent.js'; import { AgentHealth } from '../types/agent.js'; export class CorrelationAgent extends BaseAgent { config; correlationRules = new Map(); correlationCache = new Map(); campaignCache = new Map(); accessToken = null; tokenExpiresAt = 0; refreshPromise = null; constructor(metadata, registry, channel, logger, metrics, config) { super(metadata, registry, channel, logger, metrics); this.config = { correlationThreshold: 0.7, timeWindowHours: 168, // 7 days maxCasesPerCorrelation: 50, enableCampaignDetection: true, ...config }; } async onInitialize() { this.logger.info('Initializing Correlation Agent'); // Initialize authentication await this.refreshToken(); // Initialize correlation rules await this.initializeCorrelationRules(); // Start cache cleanup (only if not in MCP mode) if (process.env.MCP_MODE !== 'true') { this.startCacheCleanup(); } this.logger.info('Correlation Agent initialized successfully'); } async onStart() { this.logger.info('Starting Correlation Agent'); // Start periodic campaign detection if (this.config.enableCampaignDetection) { this.startCampaignDetection(); } this.logger.info('Correlation Agent started successfully'); } async onStop() { this.logger.info('Stopping Correlation Agent'); // Stop all periodic tasks // (intervals would be cleared here) this.logger.info('Correlation Agent stopped successfully'); } async onDestroy() { this.logger.info('Destroying Correlation Agent'); // Clear all caches and state this.correlationCache.clear(); this.campaignCache.clear(); this.correlationRules.clear(); this.logger.info('Correlation Agent destroyed successfully'); } async onHealthCheck() { try { // Check API connectivity const response = await this.makeRequest('GET', '/connect/api/v1/health'); if (response.ok) { return AgentHealth.HEALTHY; } else { return AgentHealth.DEGRADED; } } catch (error) { this.logger.error('Health check failed', { error }); return AgentHealth.UNHEALTHY; } } async handleRequest(request, context) { const { capability, payload } = request; this.logger.debug('Handling Correlation Agent request', { capability, requestId: request.id, sourceAgent: request.sourceAgentId }); switch (capability) { case 'correlate_cases': return await this.correlateCases(payload.query); case 'correlate_case_data': return await this.correlateCaseData(payload.caseId, payload.data); case 'find_related_cases': return await this.findRelatedCases(payload.caseId, payload.method, payload.threshold); case 'detect_campaigns': return await this.detectCampaigns(payload.query); case 'analyze_case_similarity': return await this.analyzeCaseSimilarity(payload.caseId1, payload.caseId2); case 'get_correlation_timeline': return await this.getCorrelationTimeline(payload.caseIds, payload.timeRange); case 'create_correlation_rule': return await this.createCorrelationRule(payload.rule); case 'update_correlation_rule': return await this.updateCorrelationRule(payload.ruleId, payload.updates); case 'get_correlation_rules': return await this.getCorrelationRules(); case 'get_correlation_statistics': return await this.getCorrelationStatistics(); default: throw new Error(`Unknown capability: ${capability}`); } } async correlateCases(query) { const correlationId = crypto.randomUUID(); this.logger.info('Starting case correlation', { correlationId, query }); try { // Check cache first const cacheKey = this.getCacheKey(query); const cachedResult = this.correlationCache.get(cacheKey); if (cachedResult) { this.logger.debug('Returning cached correlation result', { correlationId, cacheKey }); return [cachedResult]; } const results = []; // Get cases to correlate const cases = await this.getCasesForCorrelation(query); if (cases.length < 2) { this.logger.info('Not enough cases for correlation', { correlationId, caseCount: cases.length }); return results; } // Apply correlation rules for (const rule of this.correlationRules.values()) { if (!rule.enabled) continue; const ruleResults = await this.applyCorrelationRule(rule, cases, query); results.push(...ruleResults); } // Merge and deduplicate results const mergedResults = this.mergeCorrelationResults(results); // Cache results mergedResults.forEach(result => { this.correlationCache.set(this.getCacheKey({ caseId: result.primaryCaseId }), result); }); this.logger.info('Case correlation completed', { correlationId, resultsCount: mergedResults.length, casesAnalyzed: cases.length }); return mergedResults; } catch (error) { this.logger.error('Case correlation failed', { correlationId, error }); throw error; } } async correlateCaseData(caseId, data) { const correlationId = crypto.randomUUID(); this.logger.info('Starting case data correlation', { correlationId, caseId }); try { // Extract observables from data const observables = this.extractObservables(data); // Find cases with similar observables const relatedCases = await this.findCasesByObservables(observables); // Calculate correlation score const correlationScore = this.calculateCorrelationScore(data, relatedCases); // Build common elements const commonElements = this.buildCommonElements(data, relatedCases); // Generate recommendations const recommendations = this.generateCorrelationRecommendations(caseId, relatedCases, commonElements); const result = { id: correlationId, type: 'case_similarity', score: correlationScore, confidence: this.calculateConfidence(correlationScore, relatedCases.length), primaryCaseId: caseId, relatedCases, commonElements, timeline: await this.buildCorrelationTimeline([caseId, ...relatedCases.map(c => c.caseId)]), recommendations, metadata: { analysisTime: new Date().toISOString(), rulesApplied: Array.from(this.correlationRules.keys()), dataPoints: Object.keys(data).length, correlationMethod: 'case_data' } }; this.logger.info('Case data correlation completed', { correlationId, caseId, relatedCasesCount: relatedCases.length, score: correlationScore }); return result; } catch (error) { this.logger.error('Case data correlation failed', { correlationId, caseId, error }); throw error; } } async findRelatedCases(caseId, method = 'observables', threshold = 0.7) { this.logger.info('Finding related cases', { caseId, method, threshold }); try { let relatedCases = []; switch (method) { case 'observables': relatedCases = await this.findRelatedCasesByObservables(caseId, threshold); break; case 'temporal': relatedCases = await this.findRelatedCasesByTime(caseId, threshold); break; case 'behavioral': relatedCases = await this.findRelatedCasesByBehavior(caseId, threshold); break; case 'all': const observableRelated = await this.findRelatedCasesByObservables(caseId, threshold); const temporalRelated = await this.findRelatedCasesByTime(caseId, threshold); const behavioralRelated = await this.findRelatedCasesByBehavior(caseId, threshold); relatedCases = this.mergeRelatedCases([ ...observableRelated, ...temporalRelated, ...behavioralRelated ]); break; default: throw new Error(`Unknown correlation method: ${method}`); } this.logger.info('Related cases found', { caseId, method, relatedCasesCount: relatedCases.length }); return relatedCases; } catch (error) { this.logger.error('Failed to find related cases', { caseId, method, error }); throw error; } } async detectCampaigns(query = {}) { const detectionId = crypto.randomUUID(); this.logger.info('Starting campaign detection', { detectionId, query }); try { // Get cases for analysis const cases = await this.getCasesForCampaignDetection(query); if (cases.length < 3) { this.logger.info('Not enough cases for campaign detection', { detectionId, caseCount: cases.length }); return []; } const campaigns = []; // Group cases by potential campaigns const campaignGroups = await this.groupCasesIntoCampaigns(cases); for (const group of campaignGroups) { const campaign = await this.analyzeCampaignGroup(group); if (campaign.confidence >= this.config.correlationThreshold) { campaigns.push(campaign); } } // Cache campaign results campaigns.forEach(campaign => { this.campaignCache.set(campaign.campaignId, campaign); }); this.logger.info('Campaign detection completed', { detectionId, campaignsDetected: campaigns.length, casesAnalyzed: cases.length }); return campaigns; } catch (error) { this.logger.error('Campaign detection failed', { detectionId, error }); throw error; } } async analyzeCaseSimilarity(caseId1, caseId2) { this.logger.info('Analyzing case similarity', { caseId1, caseId2 }); try { // Get case data const [case1Data, case2Data] = await Promise.all([ this.getCaseData(caseId1), this.getCaseData(caseId2) ]); // Calculate similarity metrics const similarity = { overall: this.calculateOverallSimilarity(case1Data, case2Data), observables: this.calculateObservableSimilarity(case1Data.observables, case2Data.observables), temporal: this.calculateTemporalSimilarity(case1Data.timeline, case2Data.timeline), behavioral: this.calculateBehavioralSimilarity(case1Data.activities, case2Data.activities), metadata: this.calculateMetadataSimilarity(case1Data.metadata, case2Data.metadata) }; // Identify common elements const commonElements = this.findCommonElements(case1Data, case2Data); // Generate similarity report const report = { caseId1, caseId2, similarity, commonElements, recommendations: this.generateSimilarityRecommendations(similarity, commonElements), confidence: this.calculateSimilarityConfidence(similarity), analysisTime: new Date().toISOString() }; this.logger.info('Case similarity analysis completed', { caseId1, caseId2, overallSimilarity: similarity.overall, confidence: report.confidence }); return report; } catch (error) { this.logger.error('Case similarity analysis failed', { caseId1, caseId2, error }); throw error; } } async getCorrelationTimeline(caseIds, timeRange) { this.logger.info('Building correlation timeline', { caseIds, timeRange }); try { const timeline = []; // Get events for each case for (const caseId of caseIds) { const caseEvents = await this.getCaseEvents(caseId, timeRange); timeline.push(...caseEvents); } // Sort chronologically timeline.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); // Add correlation information const correlatedTimeline = this.addCorrelationInfo(timeline); this.logger.info('Correlation timeline built', { caseIds, timelineEvents: correlatedTimeline.length }); return correlatedTimeline; } catch (error) { this.logger.error('Failed to build correlation timeline', { caseIds, error }); throw error; } } async createCorrelationRule(rule) { this.logger.info('Creating correlation rule', { ruleId: rule.id, ruleName: rule.name }); try { // Validate rule this.validateCorrelationRule(rule); // Store rule this.correlationRules.set(rule.id, rule); // Persist to storage if needed await this.persistCorrelationRule(rule); this.logger.info('Correlation rule created successfully', { ruleId: rule.id, ruleName: rule.name }); } catch (error) { this.logger.error('Failed to create correlation rule', { ruleId: rule.id, error }); throw error; } } async updateCorrelationRule(ruleId, updates) { this.logger.info('Updating correlation rule', { ruleId, updates }); try { const existingRule = this.correlationRules.get(ruleId); if (!existingRule) { throw new Error(`Correlation rule not found: ${ruleId}`); } const updatedRule = { ...existingRule, ...updates }; // Validate updated rule this.validateCorrelationRule(updatedRule); // Store updated rule this.correlationRules.set(ruleId, updatedRule); // Persist to storage await this.persistCorrelationRule(updatedRule); this.logger.info('Correlation rule updated successfully', { ruleId }); } catch (error) { this.logger.error('Failed to update correlation rule', { ruleId, error }); throw error; } } async getCorrelationRules() { return Array.from(this.correlationRules.values()); } async getCorrelationStatistics() { const stats = { totalRules: this.correlationRules.size, activeRules: Array.from(this.correlationRules.values()).filter(r => r.enabled).length, cacheSize: this.correlationCache.size, campaignCacheSize: this.campaignCache.size, recentCorrelations: this.getRecentCorrelations(), topCorrelationTypes: this.getTopCorrelationTypes() }; return stats; } // Private helper methods async refreshToken() { if (this.refreshPromise) { return this.refreshPromise; } this.refreshPromise = this.performTokenRefresh(); try { await this.refreshPromise; } finally { this.refreshPromise = null; } } async performTokenRefresh() { const response = await fetch(`${this.config.apiUrl}/connect/api/v1/access_token`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.config.apiToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`Token refresh failed: ${response.status}`); } const data = await response.json(); this.accessToken = data.access_token; this.tokenExpiresAt = Date.now() + (data.exp * 1000); } async getAccessToken() { if (this.accessToken && Date.now() < this.tokenExpiresAt - 30000) { return this.accessToken; } await this.refreshToken(); return this.accessToken; } async makeRequest(method, endpoint, body) { const token = await this.getAccessToken(); const options = { method, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', } }; if (body) { options.body = JSON.stringify(body); } const response = await fetch(`${this.config.apiUrl}${endpoint}`, options); return response; } async initializeCorrelationRules() { // Initialize default correlation rules const defaultRules = [ { id: 'observable-overlap', name: 'Observable Overlap', description: 'Correlate cases with shared observables', type: 'observable', weight: 1.0, threshold: 0.3, enabled: true, conditions: [ { field: 'observables', operator: 'similarity', value: 0.3, weight: 1.0 } ] }, { id: 'temporal-proximity', name: 'Temporal Proximity', description: 'Correlate cases occurring close in time', type: 'temporal', weight: 0.8, threshold: 0.5, enabled: true, conditions: [ { field: 'timestamp', operator: 'range', value: { hours: 24 }, weight: 1.0 } ] }, { id: 'behavioral-similarity', name: 'Behavioral Similarity', description: 'Correlate cases with similar attack patterns', type: 'behavioral', weight: 0.9, threshold: 0.6, enabled: true, conditions: [ { field: 'tactics', operator: 'similarity', value: 0.4, weight: 0.8 }, { field: 'techniques', operator: 'similarity', value: 0.3, weight: 0.9 } ] } ]; defaultRules.forEach(rule => { this.correlationRules.set(rule.id, rule); }); } getCacheKey(query) { return JSON.stringify(query); } async getCasesForCorrelation(query) { const params = new URLSearchParams(); if (query.severity) { query.severity.forEach(s => params.append('severity', s)); } if (query.status) { query.status.forEach(s => params.append('status', s)); } if (query.timeRange) { params.append('start', query.timeRange.start); params.append('end', query.timeRange.end); } if (query.limit) { params.append('limit', query.limit.toString()); } const response = await this.makeRequest('GET', `/api/v1/cases?${params}`); if (!response.ok) { throw new Error(`Failed to get cases: ${response.status}`); } const data = await response.json(); return data.cases || []; } async applyCorrelationRule(rule, cases, query) { const results = []; // Apply rule logic based on type switch (rule.type) { case 'observable': return await this.applyObservableCorrelation(rule, cases); case 'temporal': return await this.applyTemporalCorrelation(rule, cases); case 'behavioral': return await this.applyBehavioralCorrelation(rule, cases); default: return results; } } async applyObservableCorrelation(rule, cases) { const results = []; for (let i = 0; i < cases.length; i++) { const primaryCase = cases[i]; const relatedCases = []; for (let j = i + 1; j < cases.length; j++) { const candidateCase = cases[j]; const similarity = this.calculateObservableSimilarity(primaryCase.observables, candidateCase.observables); if (similarity >= rule.threshold) { relatedCases.push({ caseId: candidateCase.id, name: candidateCase.name, severity: candidateCase.severity, status: candidateCase.status, createdAt: candidateCase.createdAt, score: similarity, confidence: similarity * rule.weight, relationshipType: 'observable_overlap', sharedElements: this.getSharedObservables(primaryCase.observables, candidateCase.observables) }); } } if (relatedCases.length > 0) { results.push({ id: crypto.randomUUID(), type: 'observable_overlap', score: Math.max(...relatedCases.map(r => r.score)), confidence: Math.max(...relatedCases.map(r => r.confidence)), primaryCaseId: primaryCase.id, relatedCases, commonElements: this.buildCommonElementsFromCases([primaryCase, ...relatedCases]), timeline: [], recommendations: this.generateCorrelationRecommendations(primaryCase.id, relatedCases, []), metadata: { analysisTime: new Date().toISOString(), rulesApplied: [rule.id], dataPoints: relatedCases.length, correlationMethod: 'observable' } }); } } return results; } async applyTemporalCorrelation(rule, cases) { // Similar implementation for temporal correlation return []; } async applyBehavioralCorrelation(rule, cases) { // Similar implementation for behavioral correlation return []; } mergeCorrelationResults(results) { // Merge and deduplicate correlation results const merged = new Map(); results.forEach(result => { const key = `${result.primaryCaseId}-${result.type}`; const existing = merged.get(key); if (!existing || result.score > existing.score) { merged.set(key, result); } }); return Array.from(merged.values()); } calculateObservableSimilarity(observables1, observables2) { if (!observables1 || !observables2 || observables1.length === 0 || observables2.length === 0) { return 0; } const set1 = new Set(observables1.map(o => o.value)); const set2 = new Set(observables2.map(o => o.value)); const intersection = new Set([...set1].filter(x => set2.has(x))); const union = new Set([...set1, ...set2]); return intersection.size / union.size; } getSharedObservables(observables1, observables2) { if (!observables1 || !observables2) return []; const set1 = new Set(observables1.map(o => o.value)); const set2 = new Set(observables2.map(o => o.value)); return [...set1].filter(x => set2.has(x)); } buildCommonElementsFromCases(cases) { // Build common elements from multiple cases return []; } generateCorrelationRecommendations(primaryCaseId, relatedCases, commonElements) { const recommendations = []; if (relatedCases.length > 0) { recommendations.push({ id: crypto.randomUUID(), priority: 'HIGH', category: 'Investigation', description: 'Investigate related cases for potential campaign activity', rationale: `Found ${relatedCases.length} related cases that may be part of a coordinated attack`, actions: [ 'Review all related cases for common patterns', 'Consolidate indicators of compromise', 'Assess overall threat impact', 'Consider threat hunting across environment' ], evidence: relatedCases.map(c => c.caseId), impact: 'High', effort: 'Medium' }); } return recommendations; } extractObservables(data) { return data.observables || []; } async findCasesByObservables(observables) { // Find cases that share observables return []; } calculateCorrelationScore(data, relatedCases) { if (relatedCases.length === 0) return 0; const scores = relatedCases.map(c => c.score); return scores.reduce((sum, score) => sum + score, 0) / scores.length; } buildCommonElements(data, relatedCases) { return []; } calculateConfidence(score, caseCount) { let confidence = score; // Adjust confidence based on number of related cases if (caseCount > 5) { confidence *= 1.2; } else if (caseCount < 2) { confidence *= 0.8; } return Math.min(confidence, 1.0); } async buildCorrelationTimeline(caseIds) { return []; } validateCorrelationRule(rule) { if (!rule.id || !rule.name || !rule.type) { throw new Error('Invalid correlation rule: missing required fields'); } if (rule.weight < 0 || rule.weight > 1) { throw new Error('Invalid correlation rule: weight must be between 0 and 1'); } if (rule.threshold < 0 || rule.threshold > 1) { throw new Error('Invalid correlation rule: threshold must be between 0 and 1'); } } async persistCorrelationRule(rule) { // Persist rule to storage (database, file, etc.) // For now, just log the action this.logger.debug('Persisting correlation rule', { ruleId: rule.id }); } getRecentCorrelations() { return Array.from(this.correlationCache.values()).slice(-10); } getTopCorrelationTypes() { const types = {}; for (const result of this.correlationCache.values()) { types[result.type] = (types[result.type] || 0) + 1; } return types; } startCacheCleanup() { // Skip background timers in MCP mode to prevent EPIPE errors if (process.env.MCP_MODE === 'true') return; setInterval(() => { this.cleanupCache(); }, 60000); // Clean up every minute } cleanupCache() { const maxAge = 24 * 60 * 60 * 1000; // 24 hours const cutoffTime = Date.now() - maxAge; // Clean correlation cache for (const [key, result] of this.correlationCache) { const resultTime = new Date(result.metadata.analysisTime).getTime(); if (resultTime < cutoffTime) { this.correlationCache.delete(key); } } // Clean campaign cache for (const [key, campaign] of this.campaignCache) { const campaignTime = new Date(campaign.timeframe.end).getTime(); if (campaignTime < cutoffTime) { this.campaignCache.delete(key); } } } startCampaignDetection() { // Skip background timers in MCP mode to prevent EPIPE errors if (process.env.MCP_MODE === 'true') return; setInterval(() => { this.detectCampaigns().catch(error => { this.logger.error('Periodic campaign detection failed', { error }); }); }, 3600000); // Run every hour } // Placeholder methods for complex implementations async findRelatedCasesByObservables(caseId, threshold) { return []; } async findRelatedCasesByTime(caseId, threshold) { return []; } async findRelatedCasesByBehavior(caseId, threshold) { return []; } mergeRelatedCases(cases) { return cases; } async getCasesForCampaignDetection(query) { return []; } async groupCasesIntoCampaigns(cases) { return []; } async analyzeCampaignGroup(group) { return { campaignId: crypto.randomUUID(), name: 'Campaign', description: 'Campaign description', confidence: 0.8, timeframe: { start: new Date().toISOString(), end: new Date().toISOString(), duration: '1 day' }, cases: [], indicators: [], tactics: [], techniques: [], targets: [], progression: [] }; } async getCaseData(caseId) { return {}; } calculateOverallSimilarity(case1, case2) { return 0.5; } calculateTemporalSimilarity(timeline1, timeline2) { return 0.5; } calculateBehavioralSimilarity(activities1, activities2) { return 0.5; } calculateMetadataSimilarity(metadata1, metadata2) { return 0.5; } findCommonElements(case1, case2) { return []; } generateSimilarityRecommendations(similarity, commonElements) { return []; } calculateSimilarityConfidence(similarity) { return 0.8; } async getCaseEvents(caseId, timeRange) { return []; } addCorrelationInfo(timeline) { return timeline; } } export function createCorrelationAgentMetadata() { const capabilities = [ { name: 'correlate_cases', description: 'Correlate cases based on various criteria', inputSchema: { type: 'object', properties: { query: { type: 'object' } }, required: ['query'] }, outputSchema: { type: 'array', items: { type: 'object' } } }, { name: 'correlate_case_data', description: 'Correlate specific case data with other cases', inputSchema: { type: 'object', properties: { caseId: { type: 'string' }, data: { type: 'object' } }, required: ['caseId', 'data'] }, outputSchema: { type: 'object' } }, { name: 'find_related_cases', description: 'Find cases related to a specific case', inputSchema: { type: 'object', properties: { caseId: { type: 'string' }, method: { type: 'string' }, threshold: { type: 'number' } }, required: ['caseId'] }, outputSchema: { type: 'array', items: { type: 'object' } } }, { name: 'detect_campaigns', description: 'Detect threat campaigns across multiple cases', inputSchema: { type: 'object', properties: { query: { type: 'object' } } }, outputSchema: { type: 'array', items: { type: 'object' } } } ]; return { id: { type: 'correlation', instance: 'primary', uuid: crypto.randomUUID() }, name: 'Correlation Agent', description: 'Advanced case correlation and campaign detection agent', version: '1.0.0', capabilities, dependencies: [], resources: { memory: 512, cpu: 2 } }; } //# sourceMappingURL=correlation-agent.js.map