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