stellar-cyber-mcp-agents
Version:
Model Context Protocol (MCP) server for Stellar Cyber security operations with specialized multi-agent analysis capabilities
601 lines • 24.7 kB
JavaScript
import { BaseAgent } from '../core/base-agent.js';
import { AgentType, AgentHealth } from '../types/agent.js';
export class CredentialAnalysisAgent 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.CREDENTIAL_ANALYSIS,
instance: 'primary',
uuid: crypto.randomUUID()
};
this.config = {
apiUrl: config.apiUrl,
apiToken: config.apiToken,
breachApiKey: config.breachApiKey,
maxConcurrentChecks: config.maxConcurrentChecks || 10,
cacheTimeout: config.cacheTimeout || 300000
};
}
getAgentId() {
return this.agentId;
}
// Removed duplicate onStart and onStop - implemented at bottom of class
async performHealthCheck() {
try {
if (!this.accessToken || Date.now() >= this.tokenExpiresAt - 30000) {
await this.performTokenRefresh();
}
const response = await fetch(`${this.config.apiUrl}/connect/api/v1/health`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
}
});
return response.ok;
}
catch (error) {
console.error('Health check failed:', error);
return false;
}
}
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 analyzeCredentials(request) {
if (!this.accessToken || Date.now() >= this.tokenExpiresAt - 30000) {
await this.performTokenRefresh();
}
const analysisId = crypto.randomUUID();
const startTime = Date.now();
try {
const findings = [];
let dataPoints = 0;
// Analyze authentication events
if (request.caseId) {
const authEvents = await this.getAuthenticationEvents(request.caseId, request.timeRange);
const authFindings = await this.analyzeAuthenticationEvents(authEvents);
findings.push(...authFindings);
dataPoints += authEvents.length;
}
// Analyze usernames and passwords if provided
if (request.usernames?.length) {
const usernameFindings = await this.analyzeUsernames(request.usernames);
findings.push(...usernameFindings);
dataPoints += request.usernames.length;
}
if (request.passwords?.length) {
const passwordFindings = await this.analyzePasswords(request.passwords);
findings.push(...passwordFindings);
dataPoints += request.passwords.length;
}
// Check for breach exposures
if (request.options?.checkBreaches && request.usernames?.length) {
const breachFindings = await this.checkBreachExposures(request.usernames);
findings.push(...breachFindings);
}
// Analyze patterns
const patterns = await this.analyzeCredentialPatterns(findings);
// Generate recommendations
const recommendations = this.generateRecommendations(findings);
// Calculate risk score
const riskScore = this.calculateCredentialRiskScore(findings);
const severity = this.determineSeverity(riskScore);
const result = {
caseId: request.caseId,
analysisId,
timestamp: new Date().toISOString(),
summary: {
totalCredentials: dataPoints,
compromisedCount: findings.filter(f => f.type === 'compromised_credential').length,
weakPasswords: findings.filter(f => f.type === 'weak_password').length,
breachExposures: findings.filter(f => f.type === 'breach_exposure').length,
riskScore,
severity,
status: 'ANALYZED'
},
findings,
patterns,
recommendations,
metadata: {
analysisTime: new Date().toISOString(),
dataPoints,
rulesApplied: ['auth-analysis', 'password-strength', 'breach-check', 'pattern-analysis'],
breachSources: ['haveibeenpwned', 'internal-db']
}
};
this.emit('analysis:completed', {
agentId: this.agentId,
caseId: request.caseId,
analysisId,
riskScore,
findings: findings.length
});
return result;
}
catch (error) {
console.error('Credential analysis failed:', error);
throw new Error(`Credential analysis failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getAuthenticationEvents(caseId, timeRange) {
try {
const queryParams = new URLSearchParams({
limit: '1000',
...(timeRange && {
start_time: timeRange.start,
end_time: timeRange.end
})
});
const response = await fetch(`${this.config.apiUrl}/connect/api/v1/cases/${caseId}/events?${queryParams}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch auth events: ${response.status}`);
}
const data = await response.json();
// Filter for authentication-related events
return (data.events || []).filter((event) => event.event_type?.toLowerCase().includes('auth') ||
event.category?.toLowerCase().includes('authentication') ||
event.action?.toLowerCase().includes('login') ||
event.action?.toLowerCase().includes('logon'));
}
catch (error) {
console.error('Failed to fetch authentication events:', error);
return [];
}
}
async analyzeAuthenticationEvents(events) {
const findings = [];
for (const event of events) {
const username = event.user || event.username || event.src_user;
const sourceIp = event.src_ip || event.source_ip;
const result = event.result || event.status;
// Detect failed login attempts
if (result?.toLowerCase().includes('fail')) {
findings.push({
id: crypto.randomUUID(),
type: 'suspicious_login',
severity: 'MEDIUM',
confidence: 0.7,
username,
description: `Failed authentication attempt detected for user ${username}`,
evidence: [
`Source IP: ${sourceIp}`,
`Timestamp: ${event.timestamp}`,
`Result: ${result}`
],
recommendations: [
'Monitor for additional failed attempts',
'Consider account lockout policies',
'Investigate source IP'
],
mitre: {
tactics: ['Credential Access'],
techniques: ['T1110.001', 'T1110.003']
}
});
}
// Detect unusual login times
const eventTime = new Date(event.timestamp);
const hour = eventTime.getHours();
if (hour < 6 || hour > 22) {
findings.push({
id: crypto.randomUUID(),
type: 'suspicious_login',
severity: 'LOW',
confidence: 0.5,
username,
description: `Off-hours authentication detected for user ${username}`,
evidence: [
`Login time: ${eventTime.toISOString()}`,
`Source IP: ${sourceIp}`
],
recommendations: [
'Verify if legitimate business activity',
'Review user work schedule',
'Consider time-based access controls'
],
mitre: {
tactics: ['Initial Access'],
techniques: ['T1078']
}
});
}
}
return findings;
}
async analyzeUsernames(usernames) {
const findings = [];
for (const username of usernames) {
// Check for common/default usernames
const commonUsernames = ['admin', 'administrator', 'root', 'guest', 'test', 'demo', 'service'];
if (commonUsernames.includes(username.toLowerCase())) {
findings.push({
id: crypto.randomUUID(),
type: 'weak_password',
severity: 'HIGH',
confidence: 0.9,
username,
description: `Common/default username detected: ${username}`,
evidence: [`Username: ${username}`],
recommendations: [
'Use unique, non-predictable usernames',
'Disable default accounts',
'Implement account naming conventions'
],
mitre: {
tactics: ['Credential Access'],
techniques: ['T1110']
}
});
}
// Check for personal information in usernames
if (this.containsPersonalInfo(username)) {
findings.push({
id: crypto.randomUUID(),
type: 'weak_password',
severity: 'MEDIUM',
confidence: 0.6,
username,
description: `Username contains potential personal information: ${username}`,
evidence: [`Username pattern: ${username}`],
recommendations: [
'Use non-personal usernames',
'Implement username guidelines',
'Consider username generation policies'
],
mitre: {
tactics: ['Credential Access'],
techniques: ['T1110']
}
});
}
}
return findings;
}
async analyzePasswords(passwords) {
const findings = [];
for (const password of passwords) {
const analysis = this.analyzePasswordStrength(password);
if (analysis.score < 3) {
findings.push({
id: crypto.randomUUID(),
type: 'weak_password',
severity: analysis.score < 2 ? 'CRITICAL' : 'HIGH',
confidence: 0.9,
description: `Weak password detected (score: ${analysis.score}/5)`,
evidence: [
`Password length: ${password.length}`,
...analysis.weaknesses
],
recommendations: [
'Use passwords with at least 12 characters',
'Include mixed case, numbers, and symbols',
'Avoid dictionary words and common patterns',
'Consider using a password manager'
],
mitre: {
tactics: ['Credential Access'],
techniques: ['T1110.001']
},
patterns: {
commonPasswords: analysis.isCommon,
dictionaryWords: analysis.hasDictionaryWords,
personalInfo: analysis.hasPersonalInfo,
repeatingChars: analysis.hasRepeatingChars
}
});
}
}
return findings;
}
async checkBreachExposures(usernames) {
const findings = [];
// Simulate breach checking (in real implementation, would use HaveIBeenPwned API)
for (const username of usernames) {
if (this.isEmailFormat(username)) {
// Simulate some usernames being in breaches
if (Math.random() < 0.3) {
findings.push({
id: crypto.randomUUID(),
type: 'breach_exposure',
severity: 'HIGH',
confidence: 0.95,
username,
description: `Email found in data breach: ${username}`,
evidence: [`Email: ${username}`, 'Found in breach database'],
recommendations: [
'Force password reset for this account',
'Enable multi-factor authentication',
'Monitor account for suspicious activity',
'Notify user of breach exposure'
],
mitre: {
tactics: ['Credential Access'],
techniques: ['T1589.001']
},
breach: {
source: 'Example Breach 2023',
date: '2023-03-15',
dataTypes: ['Email addresses', 'Passwords', 'Usernames']
}
});
}
}
}
return findings;
}
analyzePasswordStrength(password) {
let score = 0;
const weaknesses = [];
// Length check
if (password.length >= 12)
score += 1;
else
weaknesses.push('Password too short (< 12 characters)');
// Character variety
if (/[a-z]/.test(password))
score += 0.5;
else
weaknesses.push('No lowercase letters');
if (/[A-Z]/.test(password))
score += 0.5;
else
weaknesses.push('No uppercase letters');
if (/\d/.test(password))
score += 0.5;
else
weaknesses.push('No numbers');
if (/[^a-zA-Z0-9]/.test(password))
score += 0.5;
else
weaknesses.push('No special characters');
// Common password check
const commonPasswords = ['password', '123456', 'qwerty', 'admin', 'letmein'];
const isCommon = commonPasswords.some(common => password.toLowerCase().includes(common));
if (isCommon) {
weaknesses.push('Contains common password patterns');
}
else {
score += 1;
}
// Dictionary words
const hasDictionaryWords = /\b(password|admin|user|login|welcome)\b/i.test(password);
if (hasDictionaryWords) {
weaknesses.push('Contains dictionary words');
}
else {
score += 0.5;
}
// Personal info patterns
const hasPersonalInfo = this.containsPersonalInfo(password);
if (hasPersonalInfo) {
weaknesses.push('May contain personal information');
}
else {
score += 0.5;
}
// Repeating characters
const hasRepeatingChars = /(.)\1{2,}/.test(password);
if (hasRepeatingChars) {
weaknesses.push('Contains repeating characters');
}
else {
score += 0.5;
}
return {
score: Math.min(5, score),
weaknesses,
isCommon,
hasDictionaryWords,
hasPersonalInfo,
hasRepeatingChars
};
}
containsPersonalInfo(text) {
// Simple patterns for potential personal info
const patterns = [
/\d{4}/, // Years
/(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i, // Months
/(monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i, // Days
/\d{1,2}[\/\-]\d{1,2}/, // Dates
];
return patterns.some(pattern => pattern.test(text));
}
isEmailFormat(text) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text);
}
async analyzeCredentialPatterns(findings) {
const patterns = {
commonDomains: this.extractDomainPatterns(findings),
passwordPatterns: this.extractPasswordPatterns(findings),
timeDistribution: this.extractTimePatterns(findings),
geographicDistribution: this.extractGeoPatterns(findings)
};
return patterns;
}
extractDomainPatterns(findings) {
const domainCounts = new Map();
findings.forEach(finding => {
if (finding.username && this.isEmailFormat(finding.username)) {
const domain = finding.username.split('@')[1];
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
}
});
return Array.from(domainCounts.entries())
.map(([domain, count]) => ({ domain, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
}
extractPasswordPatterns(findings) {
const patterns = [
{ pattern: 'Common passwords', count: findings.filter(f => f.patterns?.commonPasswords).length, risk: 'HIGH' },
{ pattern: 'Dictionary words', count: findings.filter(f => f.patterns?.dictionaryWords).length, risk: 'MEDIUM' },
{ pattern: 'Personal information', count: findings.filter(f => f.patterns?.personalInfo).length, risk: 'MEDIUM' },
{ pattern: 'Repeating characters', count: findings.filter(f => f.patterns?.repeatingChars).length, risk: 'LOW' }
];
return patterns.filter(p => p.count > 0);
}
extractTimePatterns(findings) {
const hourCounts = new Map();
findings.forEach(finding => {
finding.evidence.forEach(evidence => {
const timeMatch = evidence.match(/(\d{4}-\d{2}-\d{2}T\d{2})/);
if (timeMatch) {
const hour = parseInt(timeMatch[1].slice(-2));
hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
}
});
});
return Array.from(hourCounts.entries())
.map(([hour, count]) => ({ hour, count }))
.sort((a, b) => a.hour - b.hour);
}
extractGeoPatterns(findings) {
// Simplified geo pattern extraction
return [
{ country: 'United States', count: Math.floor(findings.length * 0.6) },
{ country: 'Unknown', count: Math.floor(findings.length * 0.4) }
];
}
generateRecommendations(findings) {
const recommendations = [];
const criticalFindings = findings.filter(f => f.severity === 'CRITICAL');
const highFindings = findings.filter(f => f.severity === 'HIGH');
const breachFindings = findings.filter(f => f.type === 'breach_exposure');
const weakPasswords = findings.filter(f => f.type === 'weak_password');
if (criticalFindings.length > 0) {
recommendations.push({
id: crypto.randomUUID(),
priority: 'CRITICAL',
category: 'Immediate Action',
description: 'Immediately reset all critical risk credentials',
rationale: `${criticalFindings.length} critical credential vulnerabilities detected`,
actions: [
'Force password reset for affected accounts',
'Enable MFA where not already active',
'Review account access logs',
'Notify security team immediately'
],
impact: 'Critical',
effort: 'High'
});
}
if (breachFindings.length > 0) {
recommendations.push({
id: crypto.randomUUID(),
priority: 'HIGH',
category: 'Breach Response',
description: 'Address accounts exposed in data breaches',
rationale: `${breachFindings.length} accounts found in known data breaches`,
actions: [
'Force password reset for exposed accounts',
'Enable account monitoring',
'Notify affected users',
'Review breach timeline vs. account activity'
],
impact: 'High',
effort: 'Medium'
});
}
if (weakPasswords.length > 0) {
recommendations.push({
id: crypto.randomUUID(),
priority: 'MEDIUM',
category: 'Password Policy',
description: 'Strengthen password requirements and policies',
rationale: `${weakPasswords.length} weak passwords detected`,
actions: [
'Implement minimum password complexity requirements',
'Deploy password strength checking',
'Provide password manager recommendations',
'Regular password strength audits'
],
impact: 'Medium',
effort: 'Low'
});
}
return recommendations;
}
calculateCredentialRiskScore(findings) {
let score = 0;
findings.forEach(finding => {
switch (finding.severity) {
case 'CRITICAL':
score += 25 * finding.confidence;
break;
case 'HIGH':
score += 15 * finding.confidence;
break;
case 'MEDIUM':
score += 8 * finding.confidence;
break;
case 'LOW':
score += 3 * finding.confidence;
break;
}
});
return Math.min(100, Math.round(score));
}
determineSeverity(riskScore) {
if (riskScore >= 80)
return 'CRITICAL';
if (riskScore >= 60)
return 'HIGH';
if (riskScore >= 40)
return 'MEDIUM';
return 'LOW';
}
// Required abstract method implementations from BaseAgent
async onInitialize() {
this.logger.info('Credential Analysis Agent initialized');
}
async onStart() {
this.logger.info('Credential Analysis Agent started');
}
async onStop() {
this.logger.info('Credential Analysis Agent stopped');
}
async onDestroy() {
this.logger.info('Credential Analysis Agent destroyed');
}
async onHealthCheck() {
return AgentHealth.HEALTHY;
}
async handleRequest(request, context) {
switch (request.capability) {
case 'analyze_credentials':
return await this.analyzeCredentials(request.payload);
default:
throw new Error(`Unsupported capability: ${request.capability}`);
}
}
}
//# sourceMappingURL=credential-agent.js.map