UNPKG

stellar-cyber-mcp-agents

Version:

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

509 lines 21.4 kB
import { BaseAgent } from '../core/base-agent.js'; import { AgentType, AgentHealth } from '../types/agent.js'; export class CampaignDetectionAgent extends BaseAgent { agentId; accessToken = null; tokenExpiresAt = 0; config; constructor(metadata, registry, channel, logger, metrics, config) { super(metadata, registry, channel, logger, metrics); this.agentId = { type: AgentType.CAMPAIGN_DETECTION, instance: 'primary', uuid: crypto.randomUUID() }; this.config = { apiUrl: config.apiUrl, apiToken: config.apiToken, minClusterSize: config.minClusterSize || 3, maxClusterDistance: config.maxClusterDistance || 0.7, historicalDays: config.historicalDays || 90 }; } async performTokenRefresh() { try { 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); } catch (error) { console.error('Token refresh failed:', error); throw error; } } async detectCampaigns(request) { if (!this.accessToken || Date.now() >= this.tokenExpiresAt - 30000) { await this.performTokenRefresh(); } const analysisId = crypto.randomUUID(); const startTime = Date.now(); try { // Step 1: Gather cases for analysis const cases = await this.gatherCasesForAnalysis(request); // Step 2: Extract indicators from cases const indicators = await this.extractIndicators(cases); // Step 3: Perform clustering analysis const clusters = await this.performClustering(cases, indicators, request.options); // Step 4: Analyze TTPs for each cluster const enrichedClusters = await this.enrichClustersWithTTPs(clusters, cases); // Step 5: Infrastructure analysis const finalClusters = await this.analyzeInfrastructure(enrichedClusters); // Step 6: Global indicator analysis const globalIndicators = this.analyzeGlobalIndicators(indicators); // Step 7: Generate recommendations const recommendations = this.generateCampaignRecommendations(finalClusters); const result = { analysisId, timestamp: new Date().toISOString(), summary: { totalCases: cases.length, clustersFound: finalClusters.length, highConfidenceClusters: finalClusters.filter(c => c.confidence >= 0.8).length, uniqueIndicators: indicators.length, timespan: this.calculateTimespan(request.timeRange), status: 'COMPLETED' }, clusters: finalClusters, globalIndicators, recommendations, metadata: { analysisTime: new Date().toISOString(), casesAnalyzed: cases.length, correlationMethod: 'hierarchical-clustering', algorithmVersion: '2.1.0' } }; this.emit('campaign:detected', { agentId: this.agentId, analysisId, clustersFound: finalClusters.length, highConfidenceClusters: result.summary.highConfidenceClusters }); return result; } catch (error) { console.error('Campaign detection failed:', error); throw new Error(`Campaign detection failed: ${error instanceof Error ? error.message : String(error)}`); } } async gatherCasesForAnalysis(request) { try { const queryParams = new URLSearchParams({ limit: '1000', start_time: request.timeRange.start, end_time: request.timeRange.end }); const response = await fetch(`${this.config.apiUrl}/connect/api/v1/cases?${queryParams}`, { method: 'GET', headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`Failed to fetch cases: ${response.status}`); } const data = await response.json(); return data.cases || []; } catch (error) { console.error('Failed to gather cases:', error); return []; } } async extractIndicators(cases) { const indicatorMap = new Map(); for (const caseData of cases) { try { // Fetch observables for each case const observablesResponse = await fetch(`${this.config.apiUrl}/connect/api/v1/cases/${caseData.id}/observables`, { method: 'GET', headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' } }); if (observablesResponse.ok) { const observablesData = await observablesResponse.json(); const observables = observablesData.observables || []; for (const observable of observables) { const key = `${observable.type}:${observable.value}`; if (indicatorMap.has(key)) { const existing = indicatorMap.get(key); existing.frequency++; existing.cases.push(caseData.id); existing.lastSeen = new Date(Math.max(new Date(existing.lastSeen).getTime(), new Date(caseData.created_at || Date.now()).getTime())).toISOString(); } else { indicatorMap.set(key, { type: this.mapObservableType(observable.type), value: observable.value, frequency: 1, firstSeen: caseData.created_at || new Date().toISOString(), lastSeen: caseData.created_at || new Date().toISOString(), confidence: this.calculateIndicatorConfidence(observable), cases: [caseData.id], tags: observable.tags || [], sources: [observable.source || 'unknown'] }); } } } } catch (error) { console.warn(`Failed to fetch observables for case ${caseData.id}:`, error); } } return Array.from(indicatorMap.values()); } mapObservableType(type) { const typeMap = { 'ip': 'ip', 'domain': 'domain', 'url': 'domain', 'hash': 'hash', 'md5': 'hash', 'sha1': 'hash', 'sha256': 'hash', 'email': 'email' }; return typeMap[type.toLowerCase()] || 'infrastructure'; } calculateIndicatorConfidence(observable) { let confidence = 0.5; // Base confidence // Increase confidence based on observable properties if (observable.threat_intel) confidence += 0.3; if (observable.reputation && observable.reputation.score < 50) confidence += 0.2; if (observable.tags && observable.tags.length > 0) confidence += 0.1; return Math.min(1.0, confidence); } async performClustering(cases, indicators, options) { const clusters = []; const processedCases = new Set(); // Create similarity matrix const similarityMatrix = this.calculateSimilarityMatrix(cases, indicators); // Hierarchical clustering for (let i = 0; i < cases.length; i++) { if (processedCases.has(cases[i].id)) continue; const cluster = this.buildCluster(i, cases, similarityMatrix, processedCases); if (cluster.cases.length >= this.config.minClusterSize) { clusters.push(cluster); } } return clusters; } calculateSimilarityMatrix(cases, indicators) { const matrix = []; for (let i = 0; i < cases.length; i++) { matrix[i] = []; for (let j = 0; j < cases.length; j++) { if (i === j) { matrix[i][j] = 1.0; } else { matrix[i][j] = this.calculateCaseSimilarity(cases[i], cases[j], indicators); } } } return matrix; } calculateCaseSimilarity(case1, case2, indicators) { // Get indicators for each case const case1Indicators = indicators.filter(ind => ind.cases.includes(case1.id)); const case2Indicators = indicators.filter(ind => ind.cases.includes(case2.id)); if (case1Indicators.length === 0 || case2Indicators.length === 0) { return 0; } // Calculate Jaccard similarity const case1Values = new Set(case1Indicators.map(ind => ind.value)); const case2Values = new Set(case2Indicators.map(ind => ind.value)); const intersection = new Set([...case1Values].filter(x => case2Values.has(x))); const union = new Set([...case1Values, ...case2Values]); const jaccardSimilarity = intersection.size / union.size; // Weight by indicator confidence let weightedSimilarity = 0; let totalWeight = 0; for (const indicator of indicators) { if (indicator.cases.includes(case1.id) && indicator.cases.includes(case2.id)) { weightedSimilarity += indicator.confidence; totalWeight += indicator.confidence; } } const confidenceWeightedSimilarity = totalWeight > 0 ? weightedSimilarity / totalWeight : 0; // Combine similarities return (jaccardSimilarity * 0.7) + (confidenceWeightedSimilarity * 0.3); } buildCluster(seedIndex, cases, similarityMatrix, processedCases) { const clusterCases = []; const queue = [seedIndex]; const visited = new Set(); while (queue.length > 0) { const currentIndex = queue.shift(); if (visited.has(currentIndex)) continue; visited.add(currentIndex); const currentCase = cases[currentIndex]; processedCases.add(currentCase.id); clusterCases.push({ caseId: currentCase.id, similarity: currentIndex === seedIndex ? 1.0 : similarityMatrix[seedIndex][currentIndex], sharedIndicators: 0, // Will be calculated later timeline: currentCase.created_at || new Date().toISOString() }); // Find similar cases for (let i = 0; i < cases.length; i++) { if (!visited.has(i) && similarityMatrix[currentIndex][i] > this.config.maxClusterDistance) { queue.push(i); } } } // Calculate cluster confidence const avgSimilarity = clusterCases.reduce((sum, c) => sum + c.similarity, 0) / clusterCases.length; return { id: crypto.randomUUID(), name: `Campaign Cluster ${seedIndex + 1}`, description: `Detected campaign cluster with ${clusterCases.length} related cases`, confidence: avgSimilarity, cases: clusterCases.sort((a, b) => new Date(a.timeline).getTime() - new Date(b.timeline).getTime()), indicators: [], ttps: [], timeline: [], infrastructure: { domains: [], ips: [], registrationPatterns: [] } }; } async enrichClustersWithTTPs(clusters, cases) { for (const cluster of clusters) { const clusterCases = cases.filter(c => cluster.cases.some(cc => cc.caseId === c.id)); cluster.ttps = this.extractTTPs(clusterCases); cluster.timeline = this.buildTimeline(clusterCases); } return clusters; } extractTTPs(cases) { const ttpMap = new Map(); for (const caseData of cases) { // Extract TTPs from case data (simplified) const techniques = this.identifyTechniques(caseData); for (const technique of techniques) { const key = technique.technique; if (ttpMap.has(key)) { const existing = ttpMap.get(key); existing.frequency++; existing.cases.push(caseData.id); } else { ttpMap.set(key, { id: crypto.randomUUID(), name: technique.name, description: technique.description, mitre: technique.mitre, frequency: 1, cases: [caseData.id], confidence: 0.7, evidence: technique.evidence }); } } } return Array.from(ttpMap.values()); } identifyTechniques(caseData) { // Simplified technique identification based on case properties const techniques = []; // Common techniques based on case type and observables if (caseData.category?.toLowerCase().includes('malware')) { techniques.push({ name: 'Malware Execution', technique: 'T1204', description: 'User execution of malicious content', mitre: { tactic: 'Execution', technique: 'T1204' }, evidence: [`Case type: ${caseData.category}`] }); } if (caseData.category?.toLowerCase().includes('phishing')) { techniques.push({ name: 'Spearphishing Link', technique: 'T1566.002', description: 'Spearphishing with malicious links', mitre: { tactic: 'Initial Access', technique: 'T1566', subtechnique: '002' }, evidence: [`Case type: ${caseData.category}`] }); } return techniques; } buildTimeline(cases) { const timeline = []; for (const caseData of cases) { timeline.push({ timestamp: caseData.created_at || new Date().toISOString(), event: 'Case Created', caseId: caseData.id, details: caseData.title || 'Security incident detected' }); } return timeline.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); } async analyzeInfrastructure(clusters) { for (const cluster of clusters) { // Extract infrastructure patterns (simplified) const domains = new Set(); const ips = new Set(); for (const indicator of cluster.indicators) { if (indicator.type === 'domain') { domains.add(indicator.value); } else if (indicator.type === 'ip') { ips.add(indicator.value); } } cluster.infrastructure.domains = Array.from(domains).map(domain => ({ domain, role: 'C2', // Simplified firstSeen: new Date().toISOString() })); cluster.infrastructure.ips = Array.from(ips).map(ip => ({ ip, role: 'Infrastructure', firstSeen: new Date().toISOString() })); // Analyze registration patterns cluster.infrastructure.registrationPatterns = this.analyzeRegistrationPatterns(cluster.infrastructure.domains.map(d => d.domain)); } return clusters; } analyzeRegistrationPatterns(domains) { const patterns = []; // Simplified pattern analysis if (domains.some(d => d.includes('temp') || d.includes('tmp'))) { patterns.push('Temporary domain names'); } if (domains.some(d => /\d{4,}/.test(d))) { patterns.push('Domains with long numeric sequences'); } return patterns; } analyzeGlobalIndicators(indicators) { return indicators .filter(ind => ind.frequency > 1) // Only multi-case indicators .map(indicator => ({ indicator, prevalence: indicator.frequency / indicators.length, riskScore: this.calculateIndicatorRiskScore(indicator) })) .sort((a, b) => b.riskScore - a.riskScore) .slice(0, 50); // Top 50 indicators } calculateIndicatorRiskScore(indicator) { let score = indicator.confidence * 50; // Base score // Frequency factor score += Math.min(indicator.frequency * 10, 30); // Type-specific scoring switch (indicator.type) { case 'hash': score += 20; // Hashes are high-confidence indicators break; case 'domain': score += 15; break; case 'ip': score += 10; break; case 'email': score += 5; break; } return Math.min(100, score); } generateCampaignRecommendations(clusters) { const recommendations = []; const highConfidenceClusters = clusters.filter(c => c.confidence >= 0.8); const largeClusters = clusters.filter(c => c.cases.length >= 5); if (highConfidenceClusters.length > 0) { recommendations.push({ id: crypto.randomUUID(), priority: 'CRITICAL', category: 'Active Campaign', description: 'High-confidence campaign clusters detected requiring immediate attention', rationale: `${highConfidenceClusters.length} high-confidence campaign clusters identified`, actions: [ 'Escalate to incident response team', 'Block identified infrastructure', 'Hunt for additional related activity', 'Notify relevant stakeholders' ] }); } if (largeClusters.length > 0) { recommendations.push({ id: crypto.randomUUID(), priority: 'HIGH', category: 'Campaign Scale', description: 'Large-scale campaign activity detected', rationale: `${largeClusters.length} clusters with 5+ related cases`, actions: [ 'Conduct comprehensive threat hunting', 'Review security controls effectiveness', 'Implement additional monitoring', 'Share indicators with threat intel teams' ] }); } return recommendations; } calculateTimespan(timeRange) { const start = new Date(timeRange.start); const end = new Date(timeRange.end); const diffMs = end.getTime() - start.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 1) return '1 day'; if (diffDays < 7) return `${diffDays} days`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks`; return `${Math.floor(diffDays / 30)} months`; } // Required abstract method implementations from BaseAgent async onInitialize() { this.logger.info('Campaign Detection Agent initialized'); } async onStart() { this.logger.info('Campaign Detection Agent started'); } async onStop() { this.logger.info('Campaign Detection Agent stopped'); } async onDestroy() { this.logger.info('Campaign Detection Agent destroyed'); } async onHealthCheck() { return AgentHealth.HEALTHY; } async handleRequest(request, context) { switch (request.capability) { case 'detect_campaigns': return await this.detectCampaigns(request.payload); default: throw new Error(`Unsupported capability: ${request.capability}`); } } } //# sourceMappingURL=campaign-agent.js.map