stellar-cyber-mcp-agents
Version:
Model Context Protocol (MCP) server for Stellar Cyber security operations with specialized multi-agent analysis capabilities
1,059 lines • 46.5 kB
JavaScript
import { BaseAgent } from '../core/base-agent.js';
import { AgentHealth } from '../types/agent.js';
export class InvestigationAgent extends BaseAgent {
config;
accessToken = null;
tokenExpiresAt = 0;
refreshPromise = null;
constructor(metadata, registry, channel, logger, metrics, config) {
super(metadata, registry, channel, logger, metrics);
this.config = {
requestTimeout: 30000,
maxRetries: 3,
retryDelay: 1000,
...config
};
}
async onInitialize() {
this.logger.info('Initializing Investigation Agent');
// Initialize authentication
await this.refreshToken();
this.logger.info('Investigation Agent initialized successfully');
}
async onStart() {
this.logger.info('Starting Investigation Agent');
// Start token refresh monitoring
this.startTokenRefreshMonitoring();
this.logger.info('Investigation Agent started successfully');
}
async onStop() {
this.logger.info('Stopping Investigation Agent');
// Clean up any ongoing operations
this.refreshPromise = null;
this.logger.info('Investigation Agent stopped successfully');
}
async onDestroy() {
this.logger.info('Destroying Investigation Agent');
// Clear tokens and state
this.accessToken = null;
this.tokenExpiresAt = 0;
this.refreshPromise = null;
this.logger.info('Investigation 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 Investigation Agent request', {
capability,
requestId: request.id,
sourceAgent: request.sourceAgentId
});
switch (capability) {
case 'investigate_case':
return await this.investigateCase(payload.caseId, payload.options);
case 'get_case_details':
return await this.getCaseDetails(payload.caseId);
case 'analyze_case_observables':
return await this.analyzeCaseObservables(payload.caseId);
case 'get_case_timeline':
return await this.getCaseTimeline(payload.caseId);
case 'get_case_artifacts':
return await this.getCaseArtifacts(payload.caseId);
case 'suggest_workflow':
return await this.suggestWorkflow(payload.caseId);
case 'search_cases':
return await this.searchCases(payload.query, payload.filters);
case 'get_related_cases':
return await this.getRelatedCases(payload.caseId, payload.method);
case 'update_case_status':
return await this.updateCaseStatus(payload.caseId, payload.status, payload.comment);
case 'add_case_comment':
return await this.addCaseComment(payload.caseId, payload.comment);
case 'get_case_activities':
return await this.getCaseActivities(payload.caseId);
case 'get_case_observables':
return await this.getCaseObservables(payload.caseId);
case 'get_case_alerts':
return await this.getCaseAlerts(payload.caseId);
case 'get_case_comments':
return await this.getCaseComments(payload.caseId);
case 'get_case_scores':
return await this.getCaseScores(payload.caseId);
case 'get_threat_intelligence':
return await this.getThreatIntelligence(payload.caseId);
default:
throw new Error(`Unknown capability: ${capability}`);
}
}
async investigateCase(caseId, options = {}) {
const investigationId = crypto.randomUUID();
const timestamp = new Date().toISOString();
this.logger.info('Starting case investigation', { caseId, investigationId });
try {
// Fetch comprehensive case data in parallel
const [caseDetails, caseScores, observables, activities, comments, alerts, threatIntel] = await Promise.allSettled([
this.getCaseDetails(caseId),
this.getCaseScores(caseId),
this.getCaseObservables(caseId),
this.getCaseActivities(caseId),
this.getCaseComments(caseId),
this.getCaseAlerts(caseId),
this.getThreatIntelligence(caseId)
]);
// Combine all available data
const caseData = this.combineInvestigationData({
caseDetails: this.getSettledValue(caseDetails),
caseScores: this.getSettledValue(caseScores),
observables: this.getSettledValue(observables),
activities: this.getSettledValue(activities),
comments: this.getSettledValue(comments),
alerts: this.getSettledValue(alerts),
threatIntel: this.getSettledValue(threatIntel)
});
// Perform analysis
const findings = await this.analyzeFindings(caseData);
const recommendations = await this.generateRecommendations(caseData, findings);
const nextSteps = await this.generateNextSteps(caseData, findings);
const relatedCases = await this.findRelatedCases(caseId, caseData);
const timeline = await this.buildTimeline(caseData);
const artifacts = await this.extractArtifacts(caseData);
const workflow = await this.suggestWorkflow(caseId);
// Generate summary
const summary = this.generateInvestigationSummary(caseData, findings);
const result = {
caseId,
investigationId,
timestamp,
summary,
findings,
recommendations,
nextSteps,
relatedCases,
timeline,
artifacts,
workflow
};
this.logger.info('Case investigation completed', {
caseId,
investigationId,
findingsCount: findings.length,
recommendationsCount: recommendations.length
});
return result;
}
catch (error) {
this.logger.error('Case investigation failed', { caseId, investigationId, error });
throw error;
}
}
async getCaseDetails(caseId) {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}`);
if (!response.ok) {
throw new Error(`Failed to fetch case details: ${response.status}`);
}
return await response.json();
}
async getCaseScores(caseId) {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/scores`);
if (!response.ok) {
throw new Error(`Failed to fetch case scores: ${response.status}`);
}
return await response.json();
}
async getCaseObservables(caseId) {
try {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/observables`);
if (!response.ok) {
this.logger.warn(`Failed to fetch case observables: ${response.status}`, { caseId });
return [];
}
const data = await response.json();
// Handle the actual API response structure
if (data.data && data.data.observables) {
// Convert the grouped observables format to a flat array
const observables = [];
const observableTypes = data.data.observables;
// Process each observable type (host, user, process, etc.)
Object.keys(observableTypes).forEach(type => {
const typeData = observableTypes[type];
if (typeData.values && Array.isArray(typeData.values)) {
typeData.values.forEach((item, index) => {
observables.push({
id: `${type}_${index}`,
type: type,
value: item.value,
tlp: 'WHITE', // Default TLP
confidence: 0.8, // Default confidence
tags: [type],
firstSeen: new Date().toISOString(), // Default to current time
lastSeen: new Date().toISOString(), // Default to current time
count: 1, // Default count
metadata: {
count: typeData.count,
source: 'stellar-cyber'
}
});
});
}
});
this.logger.info(`Converted ${observables.length} observables from grouped format`, { caseId });
return observables;
}
// Fallback for direct array format
return data.observables || data.data || [];
}
catch (error) {
this.logger.warn('Case observables endpoint not available, returning empty array', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return [];
}
}
async getCaseActivities(caseId) {
try {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/activities`);
if (!response.ok) {
this.logger.warn(`Failed to fetch case activities: ${response.status}`, { caseId });
return [];
}
const data = await response.json();
this.logger.info(`Retrieved ${data.data?.length || 0} activities from API`, { caseId });
return data.data || data.activities || [];
}
catch (error) {
this.logger.warn('Case activities endpoint not available, returning empty array', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return [];
}
}
async getCaseComments(caseId) {
try {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/comments`);
if (!response.ok) {
this.logger.warn(`Failed to fetch case comments: ${response.status}`, { caseId });
return [];
}
const data = await response.json();
this.logger.info(`Retrieved ${data.data?.length || 0} comments from API`, { caseId });
return data.data || data.comments || [];
}
catch (error) {
this.logger.warn('Case comments endpoint not available, returning empty array', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return [];
}
}
async getCaseAlerts(caseId) {
try {
// First try to get alerts from main case details
const caseDetails = await this.getCaseDetails(caseId);
// Check if alerts are embedded in case details
if (caseDetails.alerts && Array.isArray(caseDetails.alerts)) {
this.logger.info(`Found ${caseDetails.alerts.length} alerts in case details`, { caseId });
return caseDetails.alerts;
}
// If case details contain alert_ids or similar, extract those
if (caseDetails.alert_ids && Array.isArray(caseDetails.alert_ids)) {
this.logger.info(`Found ${caseDetails.alert_ids.length} alert IDs in case details`, { caseId });
// Convert alert IDs to basic alert objects
return caseDetails.alert_ids.map((id, index) => ({
id,
name: `Alert ${index + 1}`,
severity: caseDetails.severity || 'medium',
timestamp: caseDetails.created_at || caseDetails.createdAt || new Date().toISOString(),
source: 'stellar-cyber',
description: `Alert associated with case ${caseId}`
}));
}
// Fall back to separate endpoint with required parameters
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/alerts?skip=0&limit=50`);
if (!response.ok) {
this.logger.warn(`Failed to fetch case alerts from separate endpoint: ${response.status}`, { caseId });
return [];
}
const data = await response.json();
// Handle the actual API response structure for alerts
if (data.data && data.data.docs && Array.isArray(data.data.docs)) {
this.logger.info(`Retrieved ${data.data.docs.length} alerts from API`, { caseId });
// Convert Elasticsearch docs to Alert objects
return data.data.docs.map((doc, index) => ({
id: doc._id,
name: `Alert ${index + 1}`,
severity: 'medium', // Default since not in alert doc
timestamp: new Date().toISOString(), // Default since not in alert doc
source: doc._index,
description: `Alert from ${doc._index}`,
details: doc._source,
found: doc.found
}));
}
// Fallback for other possible formats
this.logger.info(`Retrieved ${data.data?.length || 0} alerts from API (fallback format)`, { caseId });
return data.data || [];
}
catch (error) {
this.logger.warn('Unable to fetch case alerts, returning empty array', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return [];
}
}
async getThreatIntelligence(caseId) {
try {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/threat-intel`);
if (!response.ok) {
this.logger.warn(`Failed to fetch threat intelligence: ${response.status}`, { caseId });
return {
indicators: [],
campaigns: [],
actors: [],
recommendations: [],
sources: []
};
}
return await response.json();
}
catch (error) {
this.logger.warn('Threat intelligence endpoint not available, returning empty data', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return {
indicators: [],
campaigns: [],
actors: [],
recommendations: [],
sources: []
};
}
}
async analyzeCaseObservables(caseId) {
try {
const observables = await this.getCaseObservables(caseId);
// Ensure observables is an array
if (!Array.isArray(observables)) {
this.logger.warn('Observables is not an array, treating as empty', { caseId, observables: typeof observables });
return {
totalObservables: 0,
byType: {},
highConfidence: [],
suspicious: [],
malicious: [],
recommendations: [],
error: 'Observables data is not in expected array format'
};
}
this.logger.info(`Analyzing ${observables.length} observables for case`, { caseId });
const analysis = {
totalObservables: observables.length,
byType: this.groupObservablesByType(observables),
highConfidence: observables.filter(o => o.confidence && o.confidence > 0.8),
suspicious: observables.filter(o => o.tags && Array.isArray(o.tags) && o.tags.includes('suspicious')),
malicious: observables.filter(o => o.tags && Array.isArray(o.tags) && o.tags.includes('malicious')),
recommendations: this.generateObservableRecommendations(observables)
};
return analysis;
}
catch (error) {
this.logger.error('Error analyzing case observables', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return {
totalObservables: 0,
byType: {},
highConfidence: [],
suspicious: [],
malicious: [],
recommendations: [],
error: error instanceof Error ? error.message : 'Unknown error during analysis'
};
}
}
async getCaseTimeline(caseId) {
try {
const activities = await this.getCaseActivities(caseId);
const alerts = await this.getCaseAlerts(caseId);
const timeline = [];
// Add activities to timeline - ensure activities is an array
if (Array.isArray(activities)) {
activities.forEach(activity => {
timeline.push({
timestamp: activity.timestamp,
type: 'activity',
description: `${activity.actor} performed ${activity.action}`,
severity: activity.severity,
source: 'activities',
details: activity
});
});
}
// Add alerts to timeline - ensure alerts is an array
if (Array.isArray(alerts)) {
alerts.forEach(alert => {
timeline.push({
timestamp: alert.timestamp,
type: 'alert',
description: alert.name,
severity: alert.severity,
source: alert.source,
details: alert
});
});
}
// Sort by timestamp
timeline.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
return timeline;
}
catch (error) {
this.logger.error('Error building case timeline', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return [];
}
}
async getCaseArtifacts(caseId) {
try {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/artifacts`);
if (!response.ok) {
this.logger.warn(`Failed to fetch case artifacts: ${response.status}`, { caseId });
return [];
}
const data = await response.json();
this.logger.info(`Retrieved ${data.data?.length || 0} artifacts from API`, { caseId });
return data.data || data.artifacts || [];
}
catch (error) {
this.logger.warn('Case artifacts endpoint not available, returning empty array', { caseId, error: error instanceof Error ? error.message : 'Unknown error' });
return [];
}
}
async suggestWorkflow(caseId) {
const caseDetails = await this.getCaseDetails(caseId);
// Analyze case characteristics to suggest appropriate workflow
const threatType = this.identifyThreatType(caseDetails);
const severity = caseDetails.severity;
const workflows = this.getWorkflowDefinitions();
const suggestedWorkflow = this.selectWorkflow(workflows, threatType, severity);
return suggestedWorkflow;
}
async searchCases(query, filters = {}) {
const params = new URLSearchParams();
// Only use basic filtering parameters that we know work
// Based on get-latest-case.js example: limit and sort are supported
if (filters.limit) {
// Enforce API limit cap to prevent 500 errors
const limit = Math.min(filters.limit, 20);
params.append('limit', limit.toString());
}
else {
params.append('limit', '20'); // Default limit - API fails with 500 error above 20
}
if (filters.sort) {
params.append('sort', filters.sort);
}
else {
params.append('sort', '-created_at'); // Default to newest first
}
const queryString = params.toString();
const endpoint = `/connect/api/v1/cases?${queryString}`;
const response = await this.makeRequest('GET', endpoint);
if (!response.ok) {
throw new Error(`Failed to search cases: ${response.status}`);
}
const result = await response.json();
// Apply client-side filtering if query or other filters are provided
if (result.data && result.data.cases) {
let cases = result.data.cases;
// Apply query filter on case name, description, etc.
if (query && query.trim()) {
const searchTerm = query.toLowerCase();
cases = cases.filter((caseItem) => {
return ((caseItem.name && caseItem.name.toLowerCase().includes(searchTerm)) ||
(caseItem.description && caseItem.description.toLowerCase().includes(searchTerm)) ||
(caseItem.severity && caseItem.severity.toLowerCase().includes(searchTerm)) ||
(caseItem.status && caseItem.status.toLowerCase().includes(searchTerm)));
});
}
// Apply status filter
if (filters.status) {
cases = cases.filter((caseItem) => caseItem.status && caseItem.status.toLowerCase() === filters.status.toLowerCase());
}
// Apply severity filter
if (filters.severity) {
cases = cases.filter((caseItem) => caseItem.severity && caseItem.severity.toLowerCase() === filters.severity.toLowerCase());
}
// Apply assignee filter
if (filters.assignee) {
cases = cases.filter((caseItem) => (caseItem.assignee_name && caseItem.assignee_name.toLowerCase().includes(filters.assignee.toLowerCase())) ||
(caseItem.assignee_email && caseItem.assignee_email.toLowerCase().includes(filters.assignee.toLowerCase())));
}
// Return filtered results in same format
return {
...result,
data: {
...result.data,
cases: cases,
total: cases.length
}
};
}
return result;
}
async getRelatedCases(caseId, method = 'observables') {
try {
const response = await this.makeRequest('GET', `/connect/api/v1/cases/${caseId}/related?method=${method}`);
if (!response.ok) {
// If related cases endpoint doesn't exist, return empty array instead of failing
this.logger.warn(`Related cases endpoint returned ${response.status}, skipping related case lookup`);
return [];
}
const data = await response.json();
return data.relatedCases || [];
}
catch (error) {
this.logger.warn('Failed to fetch related cases, continuing without', { error });
return [];
}
}
async updateCaseStatus(caseId, status, comment) {
const payload = { status };
if (comment)
payload.comment = comment;
const response = await this.makeRequest('PUT', `/connect/api/v1/cases/${caseId}/status`, payload);
if (!response.ok) {
throw new Error(`Failed to update case status: ${response.status}`);
}
return await response.json();
}
async addCaseComment(caseId, comment) {
const payload = { content: comment };
const response = await this.makeRequest('POST', `/connect/api/v1/cases/${caseId}/comments`, payload);
if (!response.ok) {
throw new Error(`Failed to add case comment: ${response.status}`);
}
return await response.json();
}
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);
}
isTokenValid() {
return this.accessToken !== null && Date.now() < this.tokenExpiresAt - 30000; // 30 second buffer
}
async getAccessToken() {
if (this.isTokenValid()) {
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;
}
startTokenRefreshMonitoring() {
// Skip background timers in MCP mode to prevent EPIPE errors
if (process.env.MCP_MODE === 'true')
return;
setInterval(() => {
if (!this.isTokenValid()) {
this.refreshToken().catch(error => {
this.logger.error('Token refresh failed', { error });
});
}
}, 60000); // Check every minute
}
getSettledValue(result) {
return result.status === 'fulfilled' ? result.value : null;
}
combineInvestigationData(data) {
// Combine all investigation data into a unified structure
return {
case: data.caseDetails,
scores: data.caseScores,
observables: data.observables || [],
activities: data.activities || [],
comments: data.comments || [],
alerts: data.alerts || [],
threatIntel: data.threatIntel
};
}
async analyzeFindings(caseData) {
const findings = [];
// Analyze observables
if (caseData.observables && Array.isArray(caseData.observables) && caseData.observables.length > 0) {
const observableFindings = this.analyzeObservables(caseData.observables);
findings.push(...observableFindings);
}
// Analyze activities
if (caseData.activities.length > 0) {
const activityFindings = this.analyzeActivities(caseData.activities);
findings.push(...activityFindings);
}
// Analyze alerts
if (caseData.alerts.length > 0) {
const alertFindings = this.analyzeAlerts(caseData.alerts);
findings.push(...alertFindings);
}
return findings;
}
analyzeObservables(observables) {
const findings = [];
const maliciousObservables = observables.filter(o => o.tags && o.tags.includes('malicious'));
const suspiciousObservables = observables.filter(o => o.tags && o.tags.includes('suspicious'));
if (maliciousObservables.length > 0) {
findings.push({
id: crypto.randomUUID(),
category: 'Malicious Indicators',
description: `Found ${maliciousObservables.length} malicious indicators`,
severity: 'CRITICAL',
confidence: 0.9,
evidence: maliciousObservables.map(o => `${o.type}: ${o.value}`),
indicators: maliciousObservables.map(o => o.value),
mitre: {
tactics: ['Initial Access', 'Execution'],
techniques: ['T1566', 'T1204']
}
});
}
if (suspiciousObservables.length > 0) {
findings.push({
id: crypto.randomUUID(),
category: 'Suspicious Indicators',
description: `Found ${suspiciousObservables.length} suspicious indicators`,
severity: 'HIGH',
confidence: 0.7,
evidence: suspiciousObservables.map(o => `${o.type}: ${o.value}`),
indicators: suspiciousObservables.map(o => o.value),
mitre: {
tactics: ['Reconnaissance', 'Discovery'],
techniques: ['T1595', 'T1082']
}
});
}
return findings;
}
analyzeActivities(activities) {
const findings = [];
const criticalActivities = activities.filter(a => a.severity === 'CRITICAL');
const highActivities = activities.filter(a => a.severity === 'HIGH');
if (criticalActivities.length > 0) {
findings.push({
id: crypto.randomUUID(),
category: 'Critical Activities',
description: `Found ${criticalActivities.length} critical activities`,
severity: 'CRITICAL',
confidence: 0.9,
evidence: criticalActivities.map(a => `${a.timestamp}: ${a.action}`),
indicators: criticalActivities.map(a => a.id),
mitre: {
tactics: ['Persistence', 'Privilege Escalation'],
techniques: ['T1053', 'T1055']
}
});
}
return findings;
}
analyzeAlerts(alerts) {
const findings = [];
const criticalAlerts = alerts.filter(a => a.severity === 'CRITICAL');
if (criticalAlerts.length > 0) {
findings.push({
id: crypto.randomUUID(),
category: 'Critical Alerts',
description: `Found ${criticalAlerts.length} critical alerts`,
severity: 'CRITICAL',
confidence: 0.95,
evidence: criticalAlerts.map(a => `${a.timestamp}: ${a.name}`),
indicators: criticalAlerts.map(a => a.id),
mitre: {
tactics: ['Defense Evasion', 'Impact'],
techniques: ['T1562', 'T1485']
}
});
}
return findings;
}
async generateRecommendations(caseData, findings) {
const recommendations = [];
const criticalFindings = findings.filter(f => f.severity === 'CRITICAL');
const highFindings = findings.filter(f => f.severity === 'HIGH');
if (criticalFindings.length > 0) {
recommendations.push({
id: crypto.randomUUID(),
priority: 'IMMEDIATE',
category: 'Incident Response',
description: 'Activate incident response team immediately',
rationale: `Found ${criticalFindings.length} critical findings requiring immediate attention`,
effort: 'High',
impact: 'High',
resources: ['SOC Team', 'Incident Response Team', 'CISO']
});
}
if (caseData.observables && Array.isArray(caseData.observables) && caseData.observables.some((o) => o.tags && o.tags.includes('malicious'))) {
recommendations.push({
id: crypto.randomUUID(),
priority: 'HIGH',
category: 'Containment',
description: 'Block malicious indicators at network perimeter',
rationale: 'Malicious indicators detected that could be blocked',
effort: 'Medium',
impact: 'High',
resources: ['Network Security Team', 'Firewall', 'Proxy']
});
}
return recommendations;
}
async generateNextSteps(caseData, findings) {
const nextSteps = [];
if (findings.some(f => f.severity === 'CRITICAL')) {
nextSteps.push('Escalate to incident response team');
nextSteps.push('Implement immediate containment measures');
}
if (caseData.observables && Array.isArray(caseData.observables) && caseData.observables.length > 0) {
nextSteps.push('Enrich observables with threat intelligence');
nextSteps.push('Check for additional IOCs in environment');
}
if (caseData.activities.length > 0) {
nextSteps.push('Analyze activity timeline for attack progression');
nextSteps.push('Identify affected systems and accounts');
}
nextSteps.push('Document findings and evidence');
nextSteps.push('Update case status and notify stakeholders');
return nextSteps;
}
async findRelatedCases(caseId, caseData) {
// Use existing correlation logic to find related cases
const relatedCases = await this.getRelatedCases(caseId, 'observables');
return relatedCases;
}
async buildTimeline(caseData) {
const timeline = [];
// Add activities - ensure it's an array
if (Array.isArray(caseData.activities)) {
caseData.activities.forEach((activity) => {
timeline.push({
timestamp: activity.timestamp,
type: 'activity',
description: `${activity.actor}: ${activity.action}`,
severity: activity.severity,
source: 'activities',
details: activity
});
});
}
// Add alerts - ensure it's an array
if (Array.isArray(caseData.alerts)) {
caseData.alerts.forEach((alert) => {
timeline.push({
timestamp: alert.timestamp,
type: 'alert',
description: alert.name,
severity: alert.severity,
source: alert.source,
details: alert
});
});
}
// Sort chronologically
timeline.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
return timeline;
}
async extractArtifacts(caseData) {
const artifacts = [];
try {
// Fetch observables from the dedicated API endpoint if case ID is available
let observables = [];
if (caseData.case && caseData.case._id) {
observables = await this.getCaseObservables(caseData.case._id);
}
else if (caseData._id) {
observables = await this.getCaseObservables(caseData._id);
}
else {
// Fallback to observables in case data if available
observables = caseData.observables || [];
}
// Extract artifacts from observables - handle cases where observables might not be an array
if (Array.isArray(observables)) {
observables.forEach((observable) => {
if (observable.type === 'file' || observable.type === 'hash') {
artifacts.push({
id: crypto.randomUUID(),
type: observable.type,
name: observable.value,
hash: observable.type === 'hash' ? observable.value : undefined,
extractedAt: new Date().toISOString(),
analysis: {
verdict: observable.tags && observable.tags.includes('malicious') ? 'MALICIOUS' : 'UNKNOWN',
confidence: observable.confidence || 0,
signatures: observable.tags ? observable.tags.filter(tag => tag.startsWith('sig:')) : [],
behaviors: observable.tags ? observable.tags.filter(tag => tag.startsWith('behavior:')) : [],
family: observable.tags ? observable.tags.find(tag => tag.startsWith('family:'))?.substring(7) : undefined,
variant: observable.tags ? observable.tags.find(tag => tag.startsWith('variant:'))?.substring(8) : undefined
}
});
}
});
}
}
catch (error) {
this.logger.warn('Failed to fetch observables for artifact extraction, continuing without artifacts', { error });
}
return artifacts;
}
generateInvestigationSummary(caseData, findings) {
const threatType = this.identifyThreatType(caseData.case);
const severity = this.calculateOverallSeverity(findings);
const confidence = this.calculateOverallConfidence(findings);
const riskScore = this.calculateRiskScore(caseData, findings);
return {
threatType,
severity,
confidence,
status: findings.length > 0 ? 'ACTIVE' : 'RESOLVED',
riskScore
};
}
identifyThreatType(caseDetails) {
const name = caseDetails.name?.toLowerCase() || '';
const description = caseDetails.description?.toLowerCase() || '';
if (name.includes('malware') || description.includes('malware')) {
return 'Malware';
}
if (name.includes('phishing') || description.includes('phishing')) {
return 'Phishing';
}
if (name.includes('network') || description.includes('network')) {
return 'Network Intrusion';
}
if (name.includes('credential') || description.includes('credential')) {
return 'Credential Compromise';
}
return 'Unknown';
}
calculateOverallSeverity(findings) {
if (findings.some(f => f.severity === 'CRITICAL'))
return 'CRITICAL';
if (findings.some(f => f.severity === 'HIGH'))
return 'HIGH';
if (findings.some(f => f.severity === 'MEDIUM'))
return 'MEDIUM';
return 'LOW';
}
calculateOverallConfidence(findings) {
if (findings.length === 0)
return 0;
const totalConfidence = findings.reduce((sum, f) => sum + f.confidence, 0);
return totalConfidence / findings.length;
}
calculateRiskScore(caseData, findings) {
let score = 0;
// Base score from case score
score += caseData.case?.score || 0;
// Add points for critical findings
score += findings.filter(f => f.severity === 'CRITICAL').length * 30;
score += findings.filter(f => f.severity === 'HIGH').length * 20;
score += findings.filter(f => f.severity === 'MEDIUM').length * 10;
// Add points for malicious observables
if (caseData.observables && Array.isArray(caseData.observables)) {
const maliciousObservables = caseData.observables.filter((o) => o.tags && o.tags.includes('malicious'));
score += maliciousObservables.length * 25;
}
return Math.min(score, 100);
}
groupObservablesByType(observables) {
const groups = {};
observables.forEach(observable => {
groups[observable.type] = (groups[observable.type] || 0) + 1;
});
return groups;
}
generateObservableRecommendations(observables) {
const recommendations = [];
const maliciousObservables = observables.filter(o => o.tags && o.tags.includes('malicious'));
if (maliciousObservables.length > 0) {
recommendations.push('Block malicious indicators at network perimeter');
recommendations.push('Hunt for additional instances of malicious indicators');
}
const suspiciousObservables = observables.filter(o => o.tags && o.tags.includes('suspicious'));
if (suspiciousObservables.length > 0) {
recommendations.push('Monitor suspicious indicators for additional activity');
recommendations.push('Enrich suspicious indicators with threat intelligence');
}
return recommendations;
}
getWorkflowDefinitions() {
return [
{
type: 'malware',
name: 'Malware Investigation Workflow',
description: 'Comprehensive malware analysis and containment',
steps: [
'Isolate affected systems',
'Collect malware samples',
'Perform static analysis',
'Perform dynamic analysis',
'Identify indicators of compromise',
'Hunt for additional infections',
'Implement containment measures',
'Document findings and lessons learned'
],
estimatedTime: '4-8 hours',
requiredSkills: ['Malware Analysis', 'Incident Response', 'Forensics'],
tools: ['Sandbox', 'Disassembler', 'Network Monitoring', 'EDR']
},
{
type: 'network',
name: 'Network Intrusion Investigation',
description: 'Network-based threat investigation and response',
steps: [
'Analyze network traffic patterns',
'Identify lateral movement',
'Check for data exfiltration',
'Identify compromised systems',
'Implement network segmentation',
'Monitor for persistence mechanisms',
'Collect forensic evidence',
'Prepare incident report'
],
estimatedTime: '6-12 hours',
requiredSkills: ['Network Security', 'Incident Response', 'Forensics'],
tools: ['SIEM', 'Network Monitoring', 'Packet Capture', 'Forensics Tools']
}
];
}
selectWorkflow(workflows, threatType, severity) {
const matchingWorkflow = workflows.find(w => w.type === threatType.toLowerCase());
return matchingWorkflow || workflows[0];
}
}
export function createInvestigationAgentMetadata() {
const capabilities = [
{
name: 'investigate_case',
description: 'Perform comprehensive case investigation',
inputSchema: {
type: 'object',
properties: {
caseId: { type: 'string' },
options: { type: 'object' }
},
required: ['caseId']
},
outputSchema: {
type: 'object',
properties: {
investigationId: { type: 'string' },
summary: { type: 'object' },
findings: { type: 'array' },
recommendations: { type: 'array' }
}
}
},
{
name: 'get_case_details',
description: 'Retrieve detailed case information',
inputSchema: {
type: 'object',
properties: {
caseId: { type: 'string' }
},
required: ['caseId']
},
outputSchema: {
type: 'object'
}
},
{
name: 'analyze_case_observables',
description: 'Analyze case observables and IOCs',
inputSchema: {
type: 'object',
properties: {
caseId: { type: 'string' }
},
required: ['caseId']
},
outputSchema: {
type: 'object',
properties: {
analysis: { type: 'object' },
recommendations: { type: 'array' }
}
}
},
{
name: 'search_cases',
description: 'Search for cases based on criteria',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string' },
filters: { type: 'object' }
},
required: ['query']
},
outputSchema: {
type: 'object',
properties: {
cases: { type: 'array' },
total: { type: 'number' }
}
}
}
];
return {
id: {
type: 'investigation',
instance: 'primary',
uuid: crypto.randomUUID()
},
name: 'Investigation Agent',
description: 'Comprehensive case investigation and analysis agent',
version: '1.0.0',
capabilities,
dependencies: [],
resources: {
memory: 256,
cpu: 1
}
};
}
//# sourceMappingURL=investigation-agent.js.map