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
JavaScript
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