WIP: Save current state - security subsystems, migrations, logging
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
RefreshCw,
|
||||
@@ -20,7 +20,7 @@ import { agentApi, securityApi } from '@/lib/api';
|
||||
import toast from 'react-hot-toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AgentSubsystem } from '@/types';
|
||||
import { AgentUpdate } from './AgentUpdate';
|
||||
import { AgentUpdatesModal } from './AgentUpdatesModal';
|
||||
|
||||
interface AgentScannersProps {
|
||||
agentId: string;
|
||||
@@ -241,23 +241,21 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
|
||||
const autoRunCount = subsystems.filter(s => s.auto_run && s.enabled).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Subsystem Configuration Table */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h3 className="text-sm font-medium text-gray-900">Subsystem Configuration</h3>
|
||||
<div className="flex items-center space-x-3 text-xs text-gray-600">
|
||||
<span>{enabledCount} enabled</span>
|
||||
<span>{autoRunCount} auto-running</span>
|
||||
<span className="text-gray-500">• {subsystems.length} total</span>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{/* Subsystems Section - Continuous Surface */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-900">Subsystems</h3>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
{enabledCount} enabled • {autoRunCount} auto-running • {subsystems.length} total
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowUpdateModal(true)}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-xs text-blue-600 hover:text-blue-800 border border-blue-300 hover:bg-blue-50 rounded-md transition-colors"
|
||||
className="text-sm text-primary-600 hover:text-primary-800 flex items-center space-x-1 border border-primary-300 px-2 py-1 rounded"
|
||||
>
|
||||
<Upload className="h-3 w-3" />
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>Update Agent</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -404,17 +402,12 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<div className="text-xs text-gray-500 text-center py-2">
|
||||
Subsystems report specific metrics on scheduled intervals. Enable auto-run to schedule automatic scans, or use Actions to trigger manual scans.
|
||||
</div>
|
||||
|
||||
{/* Security Health */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200/50">
|
||||
{/* Security Health Section - Continuous Surface */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Shield className="h-5 w-5 text-blue-600" />
|
||||
<h3 className="text-sm font-semibold text-gray-900">Security Health</h3>
|
||||
<h3 className="text-base font-semibold text-gray-900">Security Health</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => queryClient.invalidateQueries({ queryKey: ['security-overview'] })}
|
||||
@@ -427,243 +420,205 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
|
||||
</div>
|
||||
|
||||
{securityLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<RefreshCw className="h-5 w-5 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-sm text-gray-600">Loading security status...</span>
|
||||
</div>
|
||||
) : securityOverview ? (
|
||||
<div className="divide-y divide-gray-200/50">
|
||||
{/* Overall Security Status */}
|
||||
<div className="p-4 hover:bg-gray-50/50 transition-colors duration-150" title={`Last check: ${new Date(securityOverview.timestamp).toLocaleString()}. No issues in past 24h.`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={cn(
|
||||
'w-3 h-3 rounded-full',
|
||||
securityOverview.overall_status === 'healthy' ? 'bg-green-500' :
|
||||
securityOverview.overall_status === 'degraded' ? 'bg-amber-500' : 'bg-red-500'
|
||||
)}></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">Overall Security Status</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
{securityOverview.overall_status === 'healthy' ? 'All systems nominal' :
|
||||
securityOverview.overall_status === 'degraded' ? `${securityOverview.alerts.length} active issue(s)` :
|
||||
'Critical issues detected'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{/* Overall Status - Compact */}
|
||||
<div className="flex items-center justify-between p-3 bg-white/70 backdrop-blur-sm rounded-lg border border-gray-200/30">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={cn(
|
||||
'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium',
|
||||
securityOverview.overall_status === 'healthy' ? 'bg-green-100 text-green-700' :
|
||||
securityOverview.overall_status === 'degraded' ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
)}>
|
||||
{securityOverview.overall_status === 'healthy' && <CheckCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status === 'degraded' && <AlertCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status === 'unhealthy' && <XCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status.toUpperCase()}
|
||||
'w-3 h-3 rounded-full',
|
||||
securityOverview.overall_status === 'healthy' ? 'bg-green-500' :
|
||||
securityOverview.overall_status === 'degraded' ? 'bg-amber-500' : 'bg-red-500'
|
||||
)}></div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-900">Overall Status</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
{securityOverview.overall_status === 'healthy' ? 'All systems nominal' :
|
||||
securityOverview.overall_status === 'degraded' ? `${securityOverview.alerts.length} issue(s)` :
|
||||
'Critical issues'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium border',
|
||||
securityOverview.overall_status === 'healthy' ? 'bg-green-100 text-green-700 border-green-200' :
|
||||
securityOverview.overall_status === 'degraded' ? 'bg-amber-100 text-amber-700 border-amber-200' :
|
||||
'bg-red-100 text-red-700 border-red-200'
|
||||
)}>
|
||||
{securityOverview.overall_status === 'healthy' && <CheckCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status === 'degraded' && <AlertCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status === 'unhealthy' && <XCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Security Metrics */}
|
||||
<div className="p-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(securityOverview.subsystems).map(([key, subsystem]) => {
|
||||
const display = getSecurityStatusDisplay(subsystem.status);
|
||||
const getEnhancedTooltip = (subsystemType: string, status: string) => {
|
||||
switch (subsystemType) {
|
||||
case 'command_validation':
|
||||
const cmdSubsystem = securityOverview.subsystems.command_validation || {};
|
||||
const cmdMetrics = cmdSubsystem.metrics || {};
|
||||
return `Commands processed: ${cmdMetrics.commands_last_hour || 0}. Failures: 0 (last 24h). Pending: ${cmdMetrics.total_pending_commands || 0}.`;
|
||||
case 'ed25519_signing':
|
||||
const signingSubsystem = securityOverview.subsystems.ed25519_signing || {};
|
||||
const signingChecks = signingSubsystem.checks || {};
|
||||
return `Fingerprint: ${signingChecks.public_key_fingerprint || 'Not available'}. Algorithm: ${signingChecks.algorithm || 'Ed25519'}. Valid since: ${new Date(securityOverview.timestamp).toLocaleDateString()}.`;
|
||||
case 'machine_binding':
|
||||
const bindingSubsystem = securityOverview.subsystems.machine_binding || {};
|
||||
const bindingChecks = bindingSubsystem.checks || {};
|
||||
return `Bound agents: ${bindingChecks.bound_agents || 'Unknown'}. Violations (24h): ${bindingChecks.recent_violations || 0}. Enforcement: Hardware fingerprint. Min version: ${bindingChecks.min_agent_version || 'v0.1.22'}.`;
|
||||
case 'nonce_validation':
|
||||
const nonceSubsystem = securityOverview.subsystems.nonce_validation || {};
|
||||
const nonceChecks = nonceSubsystem.checks || {};
|
||||
return `Max age: ${nonceChecks.max_age_minutes || 5}min. Replays blocked (24h): ${nonceChecks.validation_failures || 0}. Format: ${nonceChecks.nonce_format || 'UUID:Timestamp'}.`;
|
||||
default:
|
||||
return `Status: ${status}. Enabled: ${subsystem.enabled}`;
|
||||
}
|
||||
};
|
||||
{/* Security Grid - 2x2 Layout */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{Object.entries(securityOverview.subsystems).map(([key, subsystem]) => {
|
||||
const statusColors = {
|
||||
healthy: 'bg-green-100 text-green-700 border-green-200',
|
||||
enforced: 'bg-blue-100 text-blue-700 border-blue-200',
|
||||
degraded: 'bg-amber-100 text-amber-700 border-amber-200',
|
||||
unhealthy: 'bg-red-100 text-red-700 border-red-200'
|
||||
};
|
||||
|
||||
const getEnhancedSubtitle = (subsystemType: string, status: string) => {
|
||||
switch (subsystemType) {
|
||||
case 'command_validation':
|
||||
const cmdSubsystem = securityOverview.subsystems.command_validation || {};
|
||||
const cmdMetrics = cmdSubsystem.metrics || {};
|
||||
const pendingCount = cmdMetrics.total_pending_commands || 0;
|
||||
return pendingCount > 0 ? `Operational - ${pendingCount} pending` : 'Operational - 0 failures';
|
||||
case 'ed25519_signing':
|
||||
const signingSubsystem = securityOverview.subsystems.ed25519_signing || {};
|
||||
const signingChecks = signingSubsystem.checks || {};
|
||||
return signingChecks.signing_operational ? 'Enabled - Key valid' : 'Disabled - Invalid key';
|
||||
case 'machine_binding':
|
||||
const bindingSubsystem = securityOverview.subsystems.machine_binding || {};
|
||||
const bindingChecks = bindingSubsystem.checks || {};
|
||||
const violations = bindingChecks.recent_violations || 0;
|
||||
return status === 'healthy' || status === 'enforced' ? `Enforced - ${violations} violations` : 'Violations detected';
|
||||
case 'nonce_validation':
|
||||
const nonceSubsystem = securityOverview.subsystems.nonce_validation || {};
|
||||
const nonceChecks = nonceSubsystem.checks || {};
|
||||
const maxAge = nonceChecks.max_age_minutes || 5;
|
||||
const failures = nonceChecks.validation_failures || 0;
|
||||
return `Enabled - ${maxAge}min window, ${failures} blocked`;
|
||||
default:
|
||||
return `${subsystem.enabled ? 'Enabled' : 'Disabled'} - ${status}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getDetailedSecurityInfo = (subsystemType: string, subsystem: any) => {
|
||||
if (!securityOverview?.subsystems[subsystemType]) return '';
|
||||
|
||||
const subsystemData = securityOverview.subsystems[subsystemType];
|
||||
const checks = subsystemData.checks || {};
|
||||
const metrics = subsystemData.metrics || {};
|
||||
|
||||
switch (subsystemType) {
|
||||
case 'nonce_validation':
|
||||
return `Nonces: ${metrics.total_pending_commands || 0} pending. Max age: ${checks.max_age_minutes || 5}min. Failures: ${checks.validation_failures || 0}. Format: ${checks.nonce_format || 'UUID:Timestamp'}`;
|
||||
case 'machine_binding':
|
||||
return `Machine ID: ${checks.machine_id_type || 'Hardware fingerprint'}. Bound agents: ${checks.bound_agents || 'Unknown'}. Violations: ${checks.recent_violations || 0}. Min version: ${checks.min_agent_version || 'v0.1.22'}`;
|
||||
case 'ed25519_signing':
|
||||
return `Key: ${checks.public_key_fingerprint?.substring(0, 16) || 'Not available'}... Algorithm: ${checks.algorithm || 'Ed25519'}. Valid since: ${new Date(securityOverview.timestamp).toLocaleDateString()}`;
|
||||
default:
|
||||
return `Status: ${subsystem.status}. Last check: ${new Date(securityOverview.timestamp).toLocaleString()}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between p-3 bg-white/50 backdrop-blur-sm rounded-lg border border-gray-200/30 hover:bg-white/70 transition-all duration-150"
|
||||
title={getEnhancedTooltip(key, subsystem.status)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-lg bg-gray-50/80">
|
||||
<div className="text-gray-600">
|
||||
return (
|
||||
<div key={key} className="group relative">
|
||||
<div className={cn(
|
||||
'p-3 bg-white/70 backdrop-blur-sm rounded-lg border border-gray-200/30',
|
||||
'hover:bg-white/90 hover:shadow-sm transition-all duration-150 cursor-pointer'
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-1.5 rounded-md bg-gray-50/80 group-hover:bg-gray-100 transition-colors">
|
||||
{getSecurityIcon(key)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-medium text-gray-900 truncate">
|
||||
{getSecurityDisplayName(key)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 truncate">
|
||||
{key === 'command_validation' ?
|
||||
`${subsystem.metrics?.total_pending_commands || 0} pending` :
|
||||
key === 'ed25519_signing' ?
|
||||
'Key valid' :
|
||||
key === 'machine_binding' ?
|
||||
`${subsystem.checks?.recent_violations || 0} violations` :
|
||||
key === 'nonce_validation' ?
|
||||
`${subsystem.checks?.validation_failures || 0} blocked` :
|
||||
subsystem.status}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
||||
{getSecurityDisplayName(key)}
|
||||
<CheckCircle className="w-3 h-3 text-gray-400" />
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
{getEnhancedSubtitle(key, subsystem.status)}
|
||||
</p>
|
||||
{(key === 'nonce_validation' || key === 'machine_binding' || key === 'ed25519_signing') && (
|
||||
<div className="mt-2 p-2 bg-gray-50/70 rounded border border-gray-200/50">
|
||||
<p className="text-xs text-gray-700 font-mono break-all">
|
||||
{getDetailedSecurityInfo(key, subsystem)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(
|
||||
'px-1.5 py-0.5 rounded text-[10px] font-medium border',
|
||||
statusColors[subsystem.status as keyof typeof statusColors] || statusColors.unhealthy
|
||||
)}>
|
||||
{subsystem.status === 'healthy' && <CheckCircle className="w-3 h-3 inline" />}
|
||||
{subsystem.status === 'enforced' && <Shield className="w-3 h-3 inline" />}
|
||||
{subsystem.status === 'degraded' && <AlertCircle className="w-3 h-3 inline" />}
|
||||
{subsystem.status === 'unhealthy' && <XCircle className="w-3 h-3 inline" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium border',
|
||||
subsystem.status === 'healthy' ? 'bg-green-100 text-green-700 border-green-200' :
|
||||
subsystem.status === 'enforced' ? 'bg-blue-100 text-blue-700 border-blue-200' :
|
||||
subsystem.status === 'degraded' ? 'bg-amber-100 text-amber-700 border-amber-200' :
|
||||
'bg-red-100 text-red-700 border-red-200'
|
||||
)}>
|
||||
{subsystem.status === 'healthy' && <CheckCircle className="w-3 h-3" />}
|
||||
{subsystem.status === 'enforced' && <Shield className="w-3 h-3" />}
|
||||
{subsystem.status === 'degraded' && <AlertCircle className="w-3 h-3" />}
|
||||
{subsystem.status === 'unhealthy' && <XCircle className="w-3 h-3" />}
|
||||
{subsystem.status.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Security Alerts - Frosted Glass Style */}
|
||||
{/* Detailed Info Panel */}
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{Object.entries(securityOverview.subsystems).map(([key, subsystem]) => {
|
||||
const checks = subsystem.checks || {};
|
||||
|
||||
return (
|
||||
<div key={`${key}-details`} className="opacity-70 hover:opacity-100 transition-opacity">
|
||||
<div className="p-2 bg-gray-50/70 rounded border border-gray-200/50">
|
||||
<p className="text-[10px] text-gray-700 font-mono truncate">
|
||||
{key === 'nonce_validation' ?
|
||||
`Nonces: ${subsystem.metrics?.total_pending_commands || 0} | Max: ${checks.max_age_minutes || 5}m | Failures: ${checks.validation_failures || 0}` :
|
||||
key === 'machine_binding' ?
|
||||
`Bound: ${checks.bound_agents || 'N/A'} | Violations: ${checks.recent_violations || 0} | Method: Hardware` :
|
||||
key === 'ed25519_signing' ?
|
||||
`Key: ${checks.public_key_fingerprint?.substring(0, 16) || 'N/A'}... | Algo: ${checks.algorithm || 'Ed25519'}` :
|
||||
key === 'command_validation' ?
|
||||
`Processed: ${subsystem.metrics?.commands_last_hour || 0}/hr | Pending: ${subsystem.metrics?.total_pending_commands || 0}` :
|
||||
`Status: ${subsystem.status}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Security Alerts & Recommendations */}
|
||||
{(securityOverview.alerts.length > 0 || securityOverview.recommendations.length > 0) && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex gap-2">
|
||||
{securityOverview.alerts.length > 0 && (
|
||||
<div className="p-3 bg-red-50/80 backdrop-blur-sm rounded-lg border border-red-200/50">
|
||||
<p className="text-sm font-medium text-red-800 mb-2">Security Alerts</p>
|
||||
<ul className="text-xs text-red-700 space-y-1">
|
||||
{securityOverview.alerts.map((alert, index) => (
|
||||
<li key={index} className="flex items-start space-x-2">
|
||||
<XCircle className="h-3 w-3 text-red-500 mt-0.5 flex-shrink-0" />
|
||||
<span>{alert}</span>
|
||||
</li>
|
||||
<div className="flex-1 p-2 bg-red-50 rounded border border-red-200">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<XCircle className="h-3 w-3 text-red-500" />
|
||||
<p className="text-xs font-medium text-red-800">Alerts ({securityOverview.alerts.length})</p>
|
||||
</div>
|
||||
<ul className="text-[10px] text-red-700 space-y-0.5">
|
||||
{securityOverview.alerts.slice(0, 1).map((alert, index) => (
|
||||
<li key={index} className="truncate">• {alert}</li>
|
||||
))}
|
||||
{securityOverview.alerts.length > 1 && (
|
||||
<li className="text-red-600">+{securityOverview.alerts.length - 1} more</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{securityOverview.recommendations.length > 0 && (
|
||||
<div className="p-3 bg-amber-50/80 backdrop-blur-sm rounded-lg border border-amber-200/50">
|
||||
<p className="text-sm font-medium text-amber-800 mb-2">Recommendations</p>
|
||||
<ul className="text-xs text-amber-700 space-y-1">
|
||||
{securityOverview.recommendations.map((recommendation, index) => (
|
||||
<li key={index} className="flex items-start space-x-2">
|
||||
<AlertCircle className="h-3 w-3 text-amber-500 mt-0.5 flex-shrink-0" />
|
||||
<span>{recommendation}</span>
|
||||
</li>
|
||||
<div className="flex-1 p-2 bg-amber-50 rounded border border-amber-200">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<AlertCircle className="h-3 w-3 text-amber-500" />
|
||||
<p className="text-xs font-medium text-amber-800">Recs ({securityOverview.recommendations.length})</p>
|
||||
</div>
|
||||
<ul className="text-[10px] text-amber-700 space-y-0.5">
|
||||
{securityOverview.recommendations.slice(0, 1).map((rec, index) => (
|
||||
<li key={index} className="truncate">• {rec}</li>
|
||||
))}
|
||||
{securityOverview.recommendations.length > 1 && (
|
||||
<li className="text-amber-600">+{securityOverview.recommendations.length - 1} more</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Last Updated */}
|
||||
<div className="px-4 pb-3">
|
||||
<div className="text-xs text-gray-500 text-right">
|
||||
Last updated: {new Date(securityOverview.timestamp).toLocaleString()}
|
||||
{/* Stats Row */}
|
||||
<div className="flex justify-between pt-2 border-t border-gray-200/50">
|
||||
<div className="text-center">
|
||||
<p className="text-[11px] font-medium text-gray-900">{Object.keys(securityOverview.subsystems).length}</p>
|
||||
<p className="text-[10px] text-gray-600">Systems</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[11px] font-medium text-gray-900">
|
||||
{Object.values(securityOverview.subsystems).filter(s => s.status === 'healthy' || s.status === 'enforced').length}
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-600">Healthy</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[11px] font-medium text-gray-900">{securityOverview.alerts.length}</p>
|
||||
<p className="text-[10px] text-gray-600">Alerts</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[11px] font-medium text-gray-600">
|
||||
{new Date(securityOverview.timestamp).toLocaleTimeString()}
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-600">Updated</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<Shield className="mx-auto h-8 w-8 text-gray-400" />
|
||||
<p className="mt-2 text-sm text-gray-600">Unable to load security status</p>
|
||||
<p className="text-xs text-gray-500">Security monitoring may be unavailable</p>
|
||||
<div className="text-center py-6">
|
||||
<Shield className="mx-auto h-6 w-6 text-gray-400" />
|
||||
<p className="mt-1 text-xs text-gray-600">Unable to load security status</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Update Agent Modal */}
|
||||
{showUpdateModal && agent && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowUpdateModal(false)} />
|
||||
<div className="relative z-10 w-full max-w-lg mx-4">
|
||||
<div className="bg-white rounded-lg shadow-xl border border-gray-200">
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Update Agent: {agent.hostname}</h3>
|
||||
<button
|
||||
onClick={() => setShowUpdateModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<XCircle className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<AgentUpdate
|
||||
agent={agent}
|
||||
onUpdateComplete={() => {
|
||||
setShowUpdateModal(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['agent', agentId] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Agent Updates Modal */}
|
||||
<AgentUpdatesModal
|
||||
isOpen={showUpdateModal}
|
||||
onClose={() => {
|
||||
setShowUpdateModal(false);
|
||||
}}
|
||||
selectedAgentIds={[agentId]} // Single agent for this scanner view
|
||||
onAgentsUpdated={() => {
|
||||
// Refresh agent and subsystems data after update
|
||||
queryClient.invalidateQueries({ queryKey: ['agent', agentId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['subsystems', agentId] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,12 +172,36 @@ export function AgentUpdate({ agent, onUpdateComplete, className }: AgentUpdateP
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-900">
|
||||
Update Agent: {agent.hostname}
|
||||
</h3>
|
||||
<p className="mb-4 text-gray-600">
|
||||
Update agent from <strong>{currentVersion}</strong> to <strong>{availableVersion}</strong>?
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-gray-500">
|
||||
This will temporarily take the agent offline during the update process.
|
||||
</p>
|
||||
|
||||
{/* Warning for same-version updates */}
|
||||
{currentVersion === availableVersion ? (
|
||||
<>
|
||||
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded">
|
||||
<p className="text-amber-800 font-medium mb-2">
|
||||
⚠️ Version appears identical
|
||||
</p>
|
||||
<p className="text-sm text-amber-700 mb-2">
|
||||
Current: <strong>{currentVersion}</strong> → Target: <strong>{availableVersion}</strong>
|
||||
</p>
|
||||
<p className="text-xs text-amber-600">
|
||||
This will reinstall the current version. Useful if the binary was rebuilt or corrupted.
|
||||
</p>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-gray-600">
|
||||
The agent will be temporarily offline during reinstallation.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-4 text-gray-600">
|
||||
Update agent from <strong>{currentVersion}</strong> to <strong>{availableVersion}</strong>?
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-gray-500">
|
||||
This will temporarily take the agent offline during the update process.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowConfirmDialog(false)}
|
||||
@@ -187,9 +211,14 @@ export function AgentUpdate({ agent, onUpdateComplete, className }: AgentUpdateP
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmUpdate}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded hover:bg-primary-700"
|
||||
className={cn(
|
||||
"px-4 py-2 rounded hover:bg-primary-700",
|
||||
currentVersion === availableVersion
|
||||
? "bg-amber-600 text-white hover:bg-amber-700"
|
||||
: "bg-primary-600 text-white"
|
||||
)}
|
||||
>
|
||||
Update Agent
|
||||
{currentVersion === availableVersion ? 'Reinstall Agent' : 'Update Agent'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Search,
|
||||
Upload,
|
||||
RefreshCw,
|
||||
Terminal,
|
||||
ChevronDown,
|
||||
@@ -15,7 +14,6 @@ import { updateApi, agentApi } from '@/lib/api';
|
||||
import toast from 'react-hot-toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { UpdatePackage } from '@/types';
|
||||
import { AgentUpdatesModal } from './AgentUpdatesModal';
|
||||
|
||||
interface AgentUpdatesEnhancedProps {
|
||||
agentId: string;
|
||||
@@ -50,7 +48,6 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
|
||||
const [selectedSeverity, setSelectedSeverity] = useState('all');
|
||||
const [showLogsModal, setShowLogsModal] = useState(false);
|
||||
const [logsData, setLogsData] = useState<LogResponse | null>(null);
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||
const [expandedUpdates, setExpandedUpdates] = useState<Set<string>>(new Set());
|
||||
const [selectedUpdates, setSelectedUpdates] = useState<string[]>([]);
|
||||
|
||||
@@ -319,14 +316,8 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Update Agent Button */}
|
||||
<button
|
||||
onClick={() => setShowUpdateModal(true)}
|
||||
className="text-sm text-primary-600 hover:text-primary-800 flex items-center space-x-1 border border-primary-300 px-2 py-1 rounded"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>Update Agent</span>
|
||||
</button>
|
||||
{/* Header-only view for Update packages - no agent update button here */}
|
||||
{/* Users should use Agent Health page for agent updates */}
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
@@ -571,17 +562,6 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Update Modal */}
|
||||
<AgentUpdatesModal
|
||||
isOpen={showUpdateModal}
|
||||
onClose={() => setShowUpdateModal(false)}
|
||||
selectedAgentIds={[agentId]}
|
||||
onAgentsUpdated={() => {
|
||||
setShowUpdateModal(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AlertTriangle, Info, Lock, Shield, CheckCircle } from 'lucide-react';
|
||||
import { SecurityCategorySectionProps, SecuritySetting } from '@/types/security';
|
||||
import SecuritySetting from './SecuritySetting';
|
||||
|
||||
const SecurityCategorySection: React.FC<SecurityCategorySectionProps> = ({
|
||||
title,
|
||||
description,
|
||||
settings,
|
||||
onSettingChange,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
error = null,
|
||||
}) => {
|
||||
const [expandedInfo, setExpandedInfo] = useState<string | null>(null);
|
||||
|
||||
// Group settings by type for better organization
|
||||
const groupedSettings = settings.reduce((acc, setting) => {
|
||||
const group = setting.type === 'toggle' ? 'main' : 'advanced';
|
||||
if (!acc[group]) acc[group] = [];
|
||||
acc[group].push(setting);
|
||||
return acc;
|
||||
}, {} as Record<string, SecuritySetting[]>);
|
||||
|
||||
const isSectionEnabled = settings.find(s => s.key === 'enabled')?.value ?? true;
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
|
||||
{isSectionEnabled ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<Lock className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600">{description}</p>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="ml-4 p-2 bg-red-50 border border-red-200 rounded-lg">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings Grid */}
|
||||
{!loading && (
|
||||
<div className="space-y-6">
|
||||
{/* Main Settings (Toggles) */}
|
||||
{groupedSettings.main && groupedSettings.main.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{groupedSettings.main.map((setting) => (
|
||||
<div key={setting.key} className="flex items-start gap-4 p-4 bg-gray-50 rounded-lg">
|
||||
<SecuritySetting
|
||||
setting={setting}
|
||||
onChange={(value) => onSettingChange(setting.key, value)}
|
||||
disabled={disabled || setting.disabled}
|
||||
error={null}
|
||||
/>
|
||||
{setting.description && (
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-gray-600">{setting.description}</p>
|
||||
{setting.key === 'enabled' && !setting.value && (
|
||||
<div className="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 flex-shrink-0" />
|
||||
<p className="text-sm text-yellow-800">
|
||||
Disabling this feature may reduce system security
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced Settings */}
|
||||
{groupedSettings.advanced && groupedSettings.advanced.length > 0 && (
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Shield className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-lg font-medium text-gray-900">Advanced Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{groupedSettings.advanced.map((setting) => (
|
||||
<div
|
||||
key={setting.key}
|
||||
className={`
|
||||
${setting.type === 'checkbox-group' ? 'md:col-span-2' : ''}
|
||||
${setting.type === 'json' ? 'md:col-span-2' : ''}
|
||||
`}
|
||||
>
|
||||
<SecuritySetting
|
||||
setting={setting}
|
||||
onChange={(value) => onSettingChange(setting.key, value)}
|
||||
disabled={disabled || setting.disabled || !isSectionEnabled}
|
||||
error={null}
|
||||
/>
|
||||
{setting.description && (
|
||||
<div className="mt-2 flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-gray-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-gray-600">{setting.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section Footer Info */}
|
||||
{!loading && !error && (
|
||||
<div className="mt-6 pt-4 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
<span>
|
||||
{isSectionEnabled ? 'Feature is active' : 'Feature is disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span>{settings.length} settings</span>
|
||||
{settings.filter(s => s.disabled).length > 0 && (
|
||||
<span className="text-amber-600">
|
||||
{settings.filter(s => s.disabled).length} disabled
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityCategorySection;
|
||||
590
aggregator-web/src/components/security/SecurityEvents.tsx
Normal file
590
aggregator-web/src/components/security/SecurityEvents.tsx
Normal file
@@ -0,0 +1,590 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Download,
|
||||
Filter,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Pause,
|
||||
Play,
|
||||
ChevronDown,
|
||||
Eye,
|
||||
Copy,
|
||||
Calendar,
|
||||
Server,
|
||||
User,
|
||||
Tag,
|
||||
FileText,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import { useSecurityEvents, useSecurityWebSocket } from '@/hooks/useSecuritySettings';
|
||||
import { SecurityEvent, EventFilters, SecurityEventsProps } from '@/types/security';
|
||||
|
||||
const SecurityEvents: React.FC = () => {
|
||||
const [filters, setFilters] = useState<EventFilters>({});
|
||||
const [selectedEvent, setSelectedEvent] = useState<SecurityEvent | null>(null);
|
||||
const [showFilterPanel, setShowFilterPanel] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 20;
|
||||
|
||||
// Fetch events
|
||||
const { data: eventsData, loading, error, refetch } = useSecurityEvents(
|
||||
currentPage,
|
||||
pageSize,
|
||||
filters
|
||||
);
|
||||
|
||||
// WebSocket for real-time updates
|
||||
const { events: liveEvents, connected, clearEvents } = useSecurityWebSocket();
|
||||
const [liveUpdates, setLiveUpdates] = useState(true);
|
||||
|
||||
// Combine live events with paginated events
|
||||
const allEvents = React.useMemo(() => {
|
||||
const staticEvents = eventsData?.events || [];
|
||||
if (liveUpdates && liveEvents.length > 0) {
|
||||
// Merge live events, avoiding duplicates
|
||||
const existingIds = new Set(staticEvents.map(e => e.id));
|
||||
const newLiveEvents = liveEvents.filter(e => !existingIds.has(e.id));
|
||||
return [...newLiveEvents, ...staticEvents].slice(0, pageSize);
|
||||
}
|
||||
return staticEvents;
|
||||
}, [eventsData, liveEvents, liveUpdates, pageSize]);
|
||||
|
||||
// Severity color mapping
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'text-red-600 bg-red-50 border-red-200';
|
||||
case 'error':
|
||||
return 'text-red-600 bg-red-50 border-red-200';
|
||||
case 'warn':
|
||||
return 'text-yellow-600 bg-yellow-50 border-yellow-200';
|
||||
case 'info':
|
||||
return 'text-blue-600 bg-blue-50 border-blue-200';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50 border-gray-200';
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
case 'error':
|
||||
return <XCircle className="w-4 h-4" />;
|
||||
case 'warn':
|
||||
return <AlertTriangle className="w-4 h-4" />;
|
||||
case 'info':
|
||||
return <Info className="w-4 h-4" />;
|
||||
default:
|
||||
return <Activity className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Format timestamp
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
// Copy event details to clipboard
|
||||
const copyEventDetails = (event: SecurityEvent) => {
|
||||
const details = JSON.stringify(event, null, 2);
|
||||
navigator.clipboard.writeText(details);
|
||||
};
|
||||
|
||||
// Export events
|
||||
const exportEvents = async (format: 'json' | 'csv') => {
|
||||
// Implementation would call API to export events
|
||||
console.log(`Exporting events as ${format}`);
|
||||
};
|
||||
|
||||
// Clear filters
|
||||
const clearFilters = () => {
|
||||
setFilters({});
|
||||
setSearchTerm('');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Apply filters
|
||||
const applyFilters = (newFilters: EventFilters) => {
|
||||
setFilters(newFilters);
|
||||
setCurrentPage(1);
|
||||
setShowFilterPanel(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Security Events</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{connected ? (
|
||||
<div className="flex items-center gap-1 text-sm text-green-600">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
Live
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-sm text-gray-500">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
Offline
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setLiveUpdates(!liveUpdates)}
|
||||
className={`flex items-center gap-2 px-3 py-2 text-sm rounded-lg border ${
|
||||
liveUpdates
|
||||
? 'bg-green-50 text-green-700 border-green-200'
|
||||
: 'bg-gray-50 text-gray-700 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{liveUpdates ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
{liveUpdates ? 'Pause Updates' : 'Resume Updates'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowFilterPanel(!showFilterPanel)}
|
||||
className={`flex items-center gap-2 px-3 py-2 text-sm rounded-lg border ${
|
||||
Object.keys(filters).length > 0
|
||||
? 'bg-blue-50 text-blue-700 border-blue-200'
|
||||
: 'bg-gray-50 text-gray-700 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Filter className="w-4 h-4" />
|
||||
Filters
|
||||
{Object.keys(filters).length > 0 && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
{Object.keys(filters).length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="relative group">
|
||||
<button className="flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100">
|
||||
<Download className="w-4 h-4" />
|
||||
Export
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
<div className="absolute right-0 mt-1 w-32 bg-white border border-gray-200 rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-10">
|
||||
<button
|
||||
onClick={() => exportEvents('json')}
|
||||
className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50 rounded-t-lg"
|
||||
>
|
||||
Export as JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={() => exportEvents('csv')}
|
||||
className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50 rounded-b-lg"
|
||||
>
|
||||
Export as CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search events by message, agent ID, or user..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
if (e.target.value) {
|
||||
applyFilters({ ...filters, search: e.target.value });
|
||||
} else {
|
||||
const newFilters = { ...filters };
|
||||
delete newFilters.search;
|
||||
applyFilters(newFilters);
|
||||
}
|
||||
}}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Panel */}
|
||||
{showFilterPanel && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Filter Events</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Severity Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Severity
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{['critical', 'error', 'warn', 'info'].map((severity) => (
|
||||
<label key={severity} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.severity?.includes(severity) || false}
|
||||
onChange={(e) => {
|
||||
const current = filters.severity || [];
|
||||
if (e.target.checked) {
|
||||
applyFilters({
|
||||
...filters,
|
||||
severity: [...current, severity],
|
||||
});
|
||||
} else {
|
||||
applyFilters({
|
||||
...filters,
|
||||
severity: current.filter(s => s !== severity),
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm capitalize">{severity}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Category
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
'command_signing',
|
||||
'update_security',
|
||||
'machine_binding',
|
||||
'key_management',
|
||||
'authentication',
|
||||
].map((category) => (
|
||||
<label key={category} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.category?.includes(category) || false}
|
||||
onChange={(e) => {
|
||||
const current = filters.category || [];
|
||||
if (e.target.checked) {
|
||||
applyFilters({
|
||||
...filters,
|
||||
category: [...current, category],
|
||||
});
|
||||
} else {
|
||||
applyFilters({
|
||||
...filters,
|
||||
category: current.filter(c => c !== category),
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{category.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Range Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Date Range
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={filters.date_range?.start || ''}
|
||||
onChange={(e) => {
|
||||
applyFilters({
|
||||
...filters,
|
||||
date_range: {
|
||||
...filters.date_range,
|
||||
start: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
/>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={filters.date_range?.end || ''}
|
||||
onChange={(e) => {
|
||||
applyFilters({
|
||||
...filters,
|
||||
date_range: {
|
||||
...filters.date_range,
|
||||
end: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent/User Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Agent / User
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Agent ID or User ID"
|
||||
value={filters.agent_id || filters.user_id || ''}
|
||||
onChange={(e) => {
|
||||
applyFilters({
|
||||
...filters,
|
||||
agent_id: e.target.value || undefined,
|
||||
user_id: e.target.value || undefined,
|
||||
});
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="px-4 py-2 text-sm text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowFilterPanel(false)}
|
||||
className="px-4 py-2 text-sm text-white bg-blue-600 rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Events List */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg">
|
||||
{loading && allEvents.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-6 text-center">
|
||||
<AlertTriangle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
||||
<p className="text-red-600">Failed to load security events</p>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="mt-2 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
) : allEvents.length === 0 ? (
|
||||
<div className="p-6 text-center">
|
||||
<Activity className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600">No security events found</p>
|
||||
{Object.keys(filters).length > 0 && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="mt-2 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{allEvents.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="p-4 hover:bg-gray-50 cursor-pointer"
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-lg border ${getSeverityColor(event.severity)}`}>
|
||||
{getSeverityIcon(event.severity)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 mb-1">
|
||||
{event.event_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{event.message}</p>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap ml-4">
|
||||
{formatTimestamp(event.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Tag className="w-3 h-3" />
|
||||
{event.category.replace('_', ' ')}
|
||||
</span>
|
||||
{event.agent_id && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Server className="w-3 h-3" />
|
||||
{event.agent_id}
|
||||
</span>
|
||||
)}
|
||||
{event.user_id && (
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="w-3 h-3" />
|
||||
{event.user_id}
|
||||
</span>
|
||||
)}
|
||||
{event.trace_id && (
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="w-3 h-3" />
|
||||
{event.trace_id.substring(0, 8)}...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{eventsData && eventsData.total > pageSize && (
|
||||
<div className="p-4 border-t border-gray-200 flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">
|
||||
Showing {(currentPage - 1) * pageSize + 1} to{' '}
|
||||
{Math.min(currentPage * pageSize, eventsData.total)} of {eventsData.total} events
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 text-sm border rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage * pageSize >= eventsData.total}
|
||||
className="px-3 py-1 text-sm border rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Event Detail Modal */}
|
||||
{selectedEvent && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Event Details</h3>
|
||||
<button
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Event Header */}
|
||||
<div className="flex items-start gap-4 pb-4 border-b">
|
||||
<div className={`p-2 rounded-lg border ${getSeverityColor(selectedEvent.severity)}`}>
|
||||
{getSeverityIcon(selectedEvent.severity)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900 mb-1">
|
||||
{selectedEvent.event_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{selectedEvent.message}</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
{formatTimestamp(selectedEvent.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Information */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Severity</p>
|
||||
<p className="capitalize">{selectedEvent.severity}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Category</p>
|
||||
<p className="capitalize">{selectedEvent.category.replace('_', ' ')}</p>
|
||||
</div>
|
||||
{selectedEvent.agent_id && (
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Agent ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.agent_id}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.user_id && (
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">User ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.user_id}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.trace_id && (
|
||||
<div className="col-span-2">
|
||||
<p className="font-medium text-gray-900">Trace ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.trace_id}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Event Details */}
|
||||
{Object.keys(selectedEvent.details).length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="font-medium text-gray-900">Additional Details</p>
|
||||
<button
|
||||
onClick={() => copyEventDetails(selectedEvent)}
|
||||
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 bg-gray-50 rounded border text-xs overflow-auto max-h-48">
|
||||
{JSON.stringify(selectedEvent.details, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityEvents;
|
||||
334
aggregator-web/src/components/security/SecuritySetting.tsx
Normal file
334
aggregator-web/src/components/security/SecuritySetting.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Check, X, Eye, EyeOff, AlertTriangle } from 'lucide-react';
|
||||
import { SecuritySettingProps, SecuritySetting } from '@/types/security';
|
||||
|
||||
const SecuritySetting: React.FC<SecuritySettingProps> = ({
|
||||
setting,
|
||||
onChange,
|
||||
disabled = false,
|
||||
error = null,
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = useState(setting.value);
|
||||
const [showValue, setShowValue] = useState(!setting.sensitive);
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
|
||||
// Validate input on change
|
||||
useEffect(() => {
|
||||
if (setting.validation && typeof setting.validation === 'function') {
|
||||
const validationError = setting.validation(localValue);
|
||||
setIsValid(!validationError);
|
||||
} else {
|
||||
// Built-in validations
|
||||
if (setting.type === 'number' || setting.type === 'slider') {
|
||||
const num = Number(localValue);
|
||||
if (setting.min !== undefined && num < setting.min) setIsValid(false);
|
||||
else if (setting.max !== undefined && num > setting.max) setIsValid(false);
|
||||
else setIsValid(true);
|
||||
}
|
||||
}
|
||||
}, [localValue, setting]);
|
||||
|
||||
// Handle value change
|
||||
const handleChange = (value: any) => {
|
||||
setLocalValue(value);
|
||||
|
||||
// For immediate updates (toggles), call onChange right away
|
||||
if (setting.type === 'toggle' || setting.type === 'checkbox') {
|
||||
onChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle blur for text-like inputs
|
||||
const handleBlur = () => {
|
||||
if (setting.type === 'toggle' || setting.type === 'checkbox') return;
|
||||
|
||||
if (isValid && localValue !== setting.value) {
|
||||
onChange(localValue);
|
||||
} else if (!isValid) {
|
||||
// Revert to original value on invalid
|
||||
setLocalValue(setting.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Render toggle switch
|
||||
const renderToggle = () => {
|
||||
const isEnabled = Boolean(localValue);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => handleChange(!isEnabled)}
|
||||
disabled={disabled}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
${isEnabled ? 'bg-blue-600' : 'bg-gray-200'}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0
|
||||
transition duration-200 ease-in-out
|
||||
${isEnabled ? 'translate-x-5' : 'translate-x-0'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// Render select dropdown
|
||||
const renderSelect = () => (
|
||||
<select
|
||||
value={localValue}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
onBlur={handleBlur}
|
||||
className={`
|
||||
w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
${disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}
|
||||
${error ? 'border-red-300' : 'border-gray-300'}
|
||||
`}
|
||||
>
|
||||
{setting.options?.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option.charAt(0).toUpperCase() + option.slice(1).replace(/_/g, ' ')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
// Render number input
|
||||
const renderNumber = () => (
|
||||
<input
|
||||
type="number"
|
||||
value={localValue}
|
||||
onChange={(e) => handleChange(Number(e.target.value))}
|
||||
disabled={disabled}
|
||||
onBlur={handleBlur}
|
||||
min={setting.min}
|
||||
max={setting.max}
|
||||
step={setting.step}
|
||||
className={`
|
||||
w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
${disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}
|
||||
${error ? 'border-red-300' : isValid ? 'border-gray-300' : 'border-red-300'}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
||||
// Render text input
|
||||
const renderText = () => (
|
||||
<div className="relative">
|
||||
<input
|
||||
type={setting.sensitive && !showValue ? 'password' : 'text'}
|
||||
value={localValue}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
onBlur={handleBlur}
|
||||
className={`
|
||||
w-full px-3 py-2 pr-10 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
${disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}
|
||||
${error ? 'border-red-300' : isValid ? 'border-gray-300' : 'border-red-300'}
|
||||
`}
|
||||
/>
|
||||
{setting.sensitive && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowValue(!showValue)}
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showValue ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render slider
|
||||
const renderSlider = () => {
|
||||
const min = setting.min || 0;
|
||||
const max = setting.max || 100;
|
||||
const percentage = ((Number(localValue) - min) / (max - min)) * 100;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm text-gray-600">
|
||||
<span>{min}</span>
|
||||
<span className="font-medium text-gray-900">{localValue}</span>
|
||||
<span>{max}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={setting.step || 1}
|
||||
value={localValue}
|
||||
onChange={(e) => handleChange(Number(e.target.value))}
|
||||
onMouseUp={handleBlur}
|
||||
disabled={disabled}
|
||||
className={`
|
||||
w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-4
|
||||
[&::-webkit-slider-thumb]:h-4
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-blue-600
|
||||
[&::-webkit-slider-thumb]:cursor-pointer
|
||||
[&::-moz-range-thumb]:w-4
|
||||
[&::-moz-range-thumb]:h-4
|
||||
[&::-moz-range-thumb]:rounded-full
|
||||
[&::-moz-range-thumb]:bg-blue-600
|
||||
[&::-moz-range-thumb]:cursor-pointer
|
||||
[&::-moz-range-thumb]:border-0
|
||||
`}
|
||||
style={{
|
||||
background: `linear-gradient(to right, #3B82F6 0%, #3B82F6 ${percentage}%, #E5E7EB ${percentage}%, #E5E7EB 100%)`
|
||||
}}
|
||||
/>
|
||||
{setting.step && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Step: {setting.step} {setting.min && setting.max && `(${setting.min} - ${setting.max})`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render checkbox group
|
||||
const renderCheckboxGroup = () => {
|
||||
const options = setting.options as Array<{ label: string; value: string }>;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.value}
|
||||
className={`
|
||||
flex items-center gap-2 cursor-pointer p-2 rounded-md
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(localValue[option.value])}
|
||||
onChange={(e) => {
|
||||
const newValue = {
|
||||
...localValue,
|
||||
[option.value]: e.target.checked,
|
||||
};
|
||||
handleChange(newValue);
|
||||
}}
|
||||
disabled={disabled}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render JSON editor
|
||||
const renderJSON = () => {
|
||||
const [tempValue, setTempValue] = useState(JSON.stringify(localValue, null, 2));
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setTempValue(JSON.stringify(localValue, null, 2));
|
||||
}, [localValue]);
|
||||
|
||||
const validateJSON = (value: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
setJsonError(null);
|
||||
handleChange(parsed);
|
||||
} catch (e) {
|
||||
setJsonError('Invalid JSON format');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<textarea
|
||||
value={tempValue}
|
||||
onChange={(e) => {
|
||||
setTempValue(e.target.value);
|
||||
if (jsonError) setJsonError(null);
|
||||
}}
|
||||
onBlur={() => validateJSON(tempValue)}
|
||||
disabled={disabled}
|
||||
rows={8}
|
||||
className={`
|
||||
w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm
|
||||
${disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}
|
||||
${error || jsonError ? 'border-red-300' : 'border-gray-300'}
|
||||
`}
|
||||
/>
|
||||
{jsonError && (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span>{jsonError}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render based on setting type
|
||||
const renderControl = () => {
|
||||
switch (setting.type) {
|
||||
case 'toggle':
|
||||
return renderToggle();
|
||||
case 'select':
|
||||
return renderSelect();
|
||||
case 'number':
|
||||
return renderNumber();
|
||||
case 'text':
|
||||
return renderText();
|
||||
case 'slider':
|
||||
return renderSlider();
|
||||
case 'checkbox-group':
|
||||
return renderCheckboxGroup();
|
||||
case 'json':
|
||||
return renderJSON();
|
||||
default:
|
||||
return <span className="text-gray-500">Unknown setting type</span>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{setting.label}
|
||||
{setting.required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
|
||||
{renderControl()}
|
||||
|
||||
{/* Validation Status */}
|
||||
{localValue !== setting.value && isValid && (
|
||||
<div className="flex items-center gap-1 text-xs text-green-600">
|
||||
<Check className="w-3 h-3" />
|
||||
<span>Changed</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isValid && (
|
||||
<div className="flex items-center gap-1 text-xs text-red-600">
|
||||
<X className="w-3 h-3" />
|
||||
<span>Invalid value</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-1 text-xs text-red-600">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySetting;
|
||||
231
aggregator-web/src/components/security/SecurityStatusCard.tsx
Normal file
231
aggregator-web/src/components/security/SecurityStatusCard.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Activity,
|
||||
Eye,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import { SecurityStatusCardProps } from '@/types/security';
|
||||
|
||||
const SecurityStatusCard: React.FC<SecurityStatusCardProps> = ({
|
||||
status,
|
||||
onRefresh,
|
||||
loading = false,
|
||||
}) => {
|
||||
const getStatusIcon = () => {
|
||||
switch (status.overall) {
|
||||
case 'healthy':
|
||||
return <ShieldCheck className="w-6 h-6 text-green-600" />;
|
||||
case 'warning':
|
||||
return <AlertTriangle className="w-6 h-6 text-yellow-600" />;
|
||||
case 'critical':
|
||||
return <XCircle className="w-6 h-6 text-red-600" />;
|
||||
default:
|
||||
return <Shield className="w-6 h-6 text-gray-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (status.overall) {
|
||||
case 'healthy':
|
||||
return 'bg-green-50 border-green-200 text-green-900';
|
||||
case 'warning':
|
||||
return 'bg-yellow-50 border-yellow-200 text-yellow-900';
|
||||
case 'critical':
|
||||
return 'bg-red-50 border-red-200 text-red-900';
|
||||
default:
|
||||
return 'bg-gray-50 border-gray-200 text-gray-900';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (status.overall) {
|
||||
case 'healthy':
|
||||
return 'All security features are operating normally';
|
||||
case 'warning':
|
||||
return 'Some security features require attention';
|
||||
case 'critical':
|
||||
return 'Critical security issues detected';
|
||||
default:
|
||||
return 'Security status unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const formatLastUpdate = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
|
||||
if (minutes < 1) return 'Just now';
|
||||
if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)} hour${Math.floor(minutes / 60) !== 1 ? 's' : ''} ago`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Main Status Card */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-3 rounded-lg border ${getStatusColor()}`}>
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Security Overview</h2>
|
||||
<p className="text-gray-600 mt-1">{getStatusText()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 text-sm rounded-lg transition-colors
|
||||
${loading
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
{loading ? 'Updating...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Feature Status Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{status.features.map((feature) => (
|
||||
<div
|
||||
key={feature.name}
|
||||
className="p-4 bg-gray-50 border border-gray-200 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{feature.name}
|
||||
</span>
|
||||
{feature.enabled ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-xs text-green-600 font-medium">
|
||||
{feature.status === 'healthy' ? 'Active' : feature.status}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<XCircle className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-xs text-gray-500">Disabled</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{feature.details && (
|
||||
<p className="text-xs text-gray-500">{feature.details}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-1 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{formatLastUpdate(feature.last_check)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Events Summary */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">Recent Events</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-1">{status.recent_events}</p>
|
||||
<p className="text-sm text-gray-600">Last 24 hours</p>
|
||||
</div>
|
||||
<Activity className="w-8 h-8 text-blue-600 opacity-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">Enabled Features</p>
|
||||
<p className="text-2xl font-bold text-green-600 mt-1">
|
||||
{status.features.filter(f => f.enabled).length}/{status.features.length}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">Active</p>
|
||||
</div>
|
||||
<Shield className="w-8 h-8 text-green-600 opacity-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">Last Updated</p>
|
||||
<p className="text-sm font-medium text-gray-700 mt-2">
|
||||
{formatLastUpdate(status.last_updated)}
|
||||
</p>
|
||||
</div>
|
||||
<Clock className="w-8 h-8 text-gray-600 opacity-20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alert Section */}
|
||||
{status.overall !== 'healthy' && (
|
||||
<div className={`border rounded-lg p-4 ${getStatusColor()}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
{status.overall === 'warning' ? (
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">
|
||||
{status.overall === 'warning' ? 'Security Warnings' : 'Security Alert'}
|
||||
</h3>
|
||||
<ul className="text-sm space-y-1">
|
||||
{status.features
|
||||
.filter(f => f.status !== 'healthy' && f.enabled)
|
||||
.map((feature, index) => (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-current opacity-60"></span>
|
||||
{feature.name}: {feature.details || feature.status}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<p className="text-sm font-medium text-gray-900 mb-3 flex items-center gap-2">
|
||||
<Info className="w-4 h-4" />
|
||||
Quick Actions
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded-lg hover:border-gray-400 flex items-center gap-2">
|
||||
<Eye className="w-3 h-3" />
|
||||
View Security Logs
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded-lg hover:border-gray-400 flex items-center gap-2">
|
||||
<Shield className="w-3 h-3" />
|
||||
Run Security Check
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded-lg hover:border-gray-400 flex items-center gap-2">
|
||||
<Activity className="w-3 h-3" />
|
||||
Monitor Events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityStatusCard;
|
||||
87
aggregator-web/src/hooks/useAgentEvents.ts
Normal file
87
aggregator-web/src/hooks/useAgentEvents.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { useRealtimeStore } from '@/lib/store';
|
||||
|
||||
// SystemEvent interface matching the backend model
|
||||
interface SystemEvent {
|
||||
id: string;
|
||||
agent_id: string;
|
||||
event_type: string;
|
||||
event_subtype: string;
|
||||
severity: 'info' | 'warning' | 'error' | 'critical';
|
||||
component: string;
|
||||
message: string;
|
||||
metadata?: Record<string, any>;
|
||||
created_at: string; // ISO timestamp string
|
||||
}
|
||||
|
||||
export interface UseAgentEventsOptions {
|
||||
severity?: string; // comma-separated: error,critical,warning,info
|
||||
limit?: number; // default 50, max 1000
|
||||
pollingInterval?: number; // milliseconds, default 30000 (30s)
|
||||
}
|
||||
|
||||
export const useAgentEvents = (
|
||||
agentId: string | null | undefined,
|
||||
options: UseAgentEventsOptions = {}
|
||||
) => {
|
||||
const { addNotification } = useRealtimeStore();
|
||||
const {
|
||||
severity = 'error,critical,warning',
|
||||
limit = 50,
|
||||
pollingInterval = 30000,
|
||||
} = options;
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['agent-events', agentId, severity, limit],
|
||||
queryFn: async () => {
|
||||
if (!agentId) {
|
||||
return { events: [] as SystemEvent[], total: 0 };
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (severity) params.append('severity', severity);
|
||||
if (limit) params.append('limit', limit.toString());
|
||||
|
||||
const response = await api.get(
|
||||
`/agents/${agentId}/events?${params.toString()}`
|
||||
);
|
||||
return response.data as { events: SystemEvent[]; total: number };
|
||||
},
|
||||
enabled: !!agentId,
|
||||
refetchInterval: pollingInterval,
|
||||
staleTime: pollingInterval / 2, // Consider data stale after half the polling interval
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.events && data.events.length > 0) {
|
||||
// Map system events to notification format and add to notification store
|
||||
data.events.forEach((event) => {
|
||||
// Map severity to notification type
|
||||
const type =
|
||||
event.severity === 'critical'
|
||||
? 'error'
|
||||
: event.severity === 'error'
|
||||
? 'error'
|
||||
: event.severity === 'warning'
|
||||
? 'warning'
|
||||
: 'info';
|
||||
|
||||
addNotification({
|
||||
type,
|
||||
title: `${event.component}: ${event.event_type}`,
|
||||
message: event.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [data?.events, addNotification]);
|
||||
|
||||
return {
|
||||
events: data?.events ?? [],
|
||||
total: data?.total ?? 0,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
490
aggregator-web/src/hooks/useSecuritySettings.ts
Normal file
490
aggregator-web/src/hooks/useSecuritySettings.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api, securityApi } from '@/lib/api';
|
||||
import {
|
||||
SecuritySettings,
|
||||
SecuritySettingsResponse,
|
||||
SecurityEventsResponse,
|
||||
SecurityEvent,
|
||||
AuditEntry,
|
||||
SecurityAuditResponse,
|
||||
EventFilters,
|
||||
SecuritySettingsState,
|
||||
KeyRotationRequest,
|
||||
KeyRotationResponse,
|
||||
MachineFingerprint
|
||||
} from '@/types/security';
|
||||
|
||||
// Default security settings
|
||||
const defaultSecuritySettings: SecuritySettings = {
|
||||
command_signing: {
|
||||
enabled: true,
|
||||
enforcement_mode: 'strict',
|
||||
algorithm: 'ed25519',
|
||||
},
|
||||
update_security: {
|
||||
enabled: true,
|
||||
enforcement_mode: 'strict',
|
||||
nonce_timeout_seconds: 300,
|
||||
require_signature_verification: true,
|
||||
allowed_algorithms: ['ed25519', 'rsa-2048', 'ecdsa-p256'],
|
||||
},
|
||||
machine_binding: {
|
||||
enabled: true,
|
||||
enforcement_mode: 'strict',
|
||||
binding_components: {
|
||||
hardware_id: true,
|
||||
bios_uuid: true,
|
||||
mac_addresses: true,
|
||||
cpu_id: false,
|
||||
disk_serial: false,
|
||||
},
|
||||
violation_action: 'block',
|
||||
binding_grace_period_minutes: 5,
|
||||
},
|
||||
logging: {
|
||||
log_level: 'info',
|
||||
retention_days: 30,
|
||||
log_failures: true,
|
||||
log_successes: false,
|
||||
log_to_file: true,
|
||||
log_to_console: true,
|
||||
export_format: 'json',
|
||||
},
|
||||
key_management: {
|
||||
current_key: {
|
||||
key_id: '',
|
||||
algorithm: 'ed25519',
|
||||
created_at: '',
|
||||
fingerprint: '',
|
||||
},
|
||||
auto_rotation: false,
|
||||
rotation_interval_days: 90,
|
||||
grace_period_days: 7,
|
||||
key_history: [],
|
||||
},
|
||||
};
|
||||
|
||||
// API calls
|
||||
const fetchSecuritySettings = async (): Promise<SecuritySettings> => {
|
||||
try {
|
||||
const response = await api.get('/security/settings');
|
||||
return response.data.settings || defaultSecuritySettings;
|
||||
} catch (error) {
|
||||
// Return defaults if API fails
|
||||
console.warn('Failed to fetch security settings, using defaults:', error);
|
||||
return defaultSecuritySettings;
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecuritySetting = async (category: string, key: string, value: any): Promise<void> => {
|
||||
await api.put(`/security/settings/${category}/${key}`, { value });
|
||||
};
|
||||
|
||||
const updateSecuritySettings = async (settings: Partial<SecuritySettings>): Promise<SecuritySettingsResponse> => {
|
||||
const response = await api.put('/security/settings', { settings });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const fetchSecurityAudit = async (page: number = 1, pageSize: number = 20): Promise<SecurityAuditResponse> => {
|
||||
const response = await api.get('/security/settings/audit', {
|
||||
params: { page, page_size: pageSize }
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const fetchSecurityEvents = async (
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
filters?: EventFilters
|
||||
): Promise<SecurityEventsResponse> => {
|
||||
const params: any = { page, page_size: pageSize };
|
||||
|
||||
if (filters) {
|
||||
if (filters.severity?.length) params.severity = filters.severity.join(',');
|
||||
if (filters.category?.length) params.category = filters.category.join(',');
|
||||
if (filters.date_range) {
|
||||
params.start_date = filters.date_range.start;
|
||||
params.end_date = filters.date_range.end;
|
||||
}
|
||||
if (filters.agent_id) params.agent_id = filters.agent_id;
|
||||
if (filters.user_id) params.user_id = filters.user_id;
|
||||
if (filters.search) params.search = filters.search;
|
||||
}
|
||||
|
||||
const response = await api.get('/security/events', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const rotateKey = async (request: KeyRotationRequest): Promise<KeyRotationResponse> => {
|
||||
const response = await api.post('/security/keys/rotate', request);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getMachineFingerprint = async (agentId: string): Promise<MachineFingerprint> => {
|
||||
const response = await api.get(`/security/machine-binding/fingerprint/${agentId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const exportSecuritySettings = async (): Promise<Blob> => {
|
||||
const response = await api.get('/security/settings/export', {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const importSecuritySettings = async (file: File): Promise<SecuritySettingsResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await api.post('/security/settings/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Main hook for security settings
|
||||
export const useSecuritySettings = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch security settings
|
||||
const {
|
||||
data: settings = defaultSecuritySettings,
|
||||
isLoading: loadingSettings,
|
||||
error: settingsError,
|
||||
refetch: refetchSettings,
|
||||
} = useQuery({
|
||||
queryKey: ['security', 'settings'],
|
||||
queryFn: fetchSecuritySettings,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
|
||||
// Fetch security overview/status
|
||||
const {
|
||||
data: securityOverview,
|
||||
isLoading: loadingOverview,
|
||||
refetch: refetchOverview,
|
||||
} = useQuery({
|
||||
queryKey: ['security', 'overview'],
|
||||
queryFn: () => securityApi.getOverview(),
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
refetchInterval: 60 * 1000, // Auto-refresh every minute
|
||||
});
|
||||
|
||||
// Update single setting mutation
|
||||
const updateSettingMutation = useMutation({
|
||||
mutationFn: ({ category, key, value }: { category: string; key: string; value: any }) =>
|
||||
updateSecuritySetting(category, key, value),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'settings'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'overview'] });
|
||||
},
|
||||
});
|
||||
|
||||
// Update all settings mutation
|
||||
const updateSettingsMutation = useMutation({
|
||||
mutationFn: updateSecuritySettings,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'settings'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'overview'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'audit'] });
|
||||
},
|
||||
});
|
||||
|
||||
// Key rotation mutation
|
||||
const rotateKeyMutation = useMutation({
|
||||
mutationFn: rotateKey,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'settings'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'overview'] });
|
||||
},
|
||||
});
|
||||
|
||||
// Export settings mutation
|
||||
const exportSettingsMutation = useMutation({
|
||||
mutationFn: exportSecuritySettings,
|
||||
});
|
||||
|
||||
// Import settings mutation
|
||||
const importSettingsMutation = useMutation({
|
||||
mutationFn: importSecuritySettings,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['security', 'settings'] });
|
||||
},
|
||||
});
|
||||
|
||||
// Update a single setting
|
||||
const updateSetting = async (category: string, key: string, value: any) => {
|
||||
try {
|
||||
await updateSettingMutation.mutateAsync({ category, key, value });
|
||||
} catch (error) {
|
||||
console.error(`Failed to update ${category}.${key}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Update multiple settings at once
|
||||
const updateSettings = async (newSettings: Partial<SecuritySettings>) => {
|
||||
try {
|
||||
await updateSettingsMutation.mutateAsync(newSettings);
|
||||
} catch (error) {
|
||||
console.error('Failed to update security settings:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Rotate security key
|
||||
const rotateSecurityKey = async (request: KeyRotationRequest) => {
|
||||
try {
|
||||
return await rotateKeyMutation.mutateAsync(request);
|
||||
} catch (error) {
|
||||
console.error('Failed to rotate security key:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Export settings to file
|
||||
const exportSettings = async () => {
|
||||
try {
|
||||
const blob = await exportSettingsMutation.mutateAsync();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = `redflag-security-settings-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error('Failed to export security settings:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Import settings from file
|
||||
const importSettings = async (file: File) => {
|
||||
try {
|
||||
return await importSettingsMutation.mutateAsync(file);
|
||||
} catch (error) {
|
||||
console.error('Failed to import security settings:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Reset settings to defaults
|
||||
const resetToDefaults = async () => {
|
||||
try {
|
||||
await updateSettings(defaultSecuritySettings);
|
||||
} catch (error) {
|
||||
console.error('Failed to reset security settings to defaults:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// Data
|
||||
settings,
|
||||
securityOverview,
|
||||
|
||||
// Loading states
|
||||
loading: loadingSettings || loadingOverview,
|
||||
saving: updateSettingMutation.isPending || updateSettingsMutation.isPending,
|
||||
|
||||
// Errors
|
||||
error: settingsError || updateSettingMutation.error || updateSettingsMutation.error,
|
||||
|
||||
// Actions
|
||||
updateSetting,
|
||||
updateSettings,
|
||||
rotateSecurityKey,
|
||||
exportSettings,
|
||||
importSettings,
|
||||
resetToDefaults,
|
||||
refetch: refetchSettings,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for security audit trail
|
||||
export const useSecurityAudit = (page: number = 1, pageSize: number = 20) => {
|
||||
return useQuery({
|
||||
queryKey: ['security', 'audit', page, pageSize],
|
||||
queryFn: () => fetchSecurityAudit(page, pageSize),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
};
|
||||
|
||||
// Hook for security events
|
||||
export const useSecurityEvents = (
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
filters?: EventFilters
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ['security', 'events', page, pageSize, filters],
|
||||
queryFn: () => fetchSecurityEvents(page, pageSize, filters),
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
});
|
||||
};
|
||||
|
||||
// Hook for machine fingerprint
|
||||
export const useMachineFingerprint = (agentId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['security', 'machine-fingerprint', agentId],
|
||||
queryFn: () => getMachineFingerprint(agentId),
|
||||
enabled: !!agentId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
// Hook for real-time security events (WebSocket)
|
||||
export const useSecurityWebSocket = () => {
|
||||
const [events, setEvents] = React.useState<SecurityEvent[]>([]);
|
||||
const [connected, setConnected] = React.useState(false);
|
||||
const ws = React.useRef<WebSocket | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Initialize WebSocket connection
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/api/v1/security/ws`;
|
||||
|
||||
ws.current = new WebSocket(wsUrl, [], {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
ws.current.onopen = () => {
|
||||
setConnected(true);
|
||||
console.log('Security WebSocket connected');
|
||||
};
|
||||
|
||||
ws.current.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'security_event') {
|
||||
setEvents(prev => [message.data, ...prev.slice(0, 999)]); // Keep last 1000 events
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.current.onerror = (error) => {
|
||||
console.error('Security WebSocket error:', error);
|
||||
setConnected(false);
|
||||
};
|
||||
|
||||
ws.current.onclose = () => {
|
||||
setConnected(false);
|
||||
console.log('Security WebSocket disconnected');
|
||||
|
||||
// Attempt to reconnect after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (!ws.current || ws.current.readyState === WebSocket.CLOSED) {
|
||||
// Re-initialize connection
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
events,
|
||||
connected,
|
||||
clearEvents: () => setEvents([]),
|
||||
};
|
||||
};
|
||||
|
||||
// Helper hook for form validation
|
||||
export const useSecurityValidation = () => {
|
||||
const validateSetting = (key: string, value: any): string | null => {
|
||||
switch (key) {
|
||||
case 'nonce_timeout_seconds':
|
||||
if (value < 60 || value > 3600) {
|
||||
return 'Nonce timeout must be between 60 and 3600 seconds';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'retention_days':
|
||||
if (value < 1 || value > 365) {
|
||||
return 'Retention period must be between 1 and 365 days';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rotation_interval_days':
|
||||
if (value < 7 || value > 365) {
|
||||
return 'Rotation interval must be between 7 and 365 days';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'binding_grace_period_minutes':
|
||||
if (value < 1 || value > 60) {
|
||||
return 'Grace period must be between 1 and 60 minutes';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateAll = (settings: SecuritySettings): Record<string, string> => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Validate command signing
|
||||
const cmdSigning = settings.command_signing;
|
||||
if (cmdSigning.enabled && !cmdSigning.algorithm) {
|
||||
errors['command_signing.algorithm'] = 'Algorithm is required when command signing is enabled';
|
||||
}
|
||||
|
||||
// Validate update security
|
||||
const updateSec = settings.update_security;
|
||||
if (updateSec.enabled) {
|
||||
const nonceError = validateSetting('nonce_timeout_seconds', updateSec.nonce_timeout_seconds);
|
||||
if (nonceError) errors['update_security.nonce_timeout_seconds'] = nonceError;
|
||||
}
|
||||
|
||||
// Validate machine binding
|
||||
const machineBinding = settings.machine_binding;
|
||||
if (machineBinding.enabled) {
|
||||
const hasAnyComponent = Object.values(machineBinding.binding_components).some(v => v);
|
||||
if (!hasAnyComponent) {
|
||||
errors['machine_binding.binding_components'] = 'At least one binding component must be selected';
|
||||
}
|
||||
|
||||
const graceError = validateSetting('binding_grace_period_minutes', machineBinding.binding_grace_period_minutes);
|
||||
if (graceError) errors['machine_binding.binding_grace_period_minutes'] = graceError;
|
||||
}
|
||||
|
||||
// Validate logging
|
||||
const logging = settings.logging;
|
||||
const retentionError = validateSetting('retention_days', logging.retention_days);
|
||||
if (retentionError) errors['logging.retention_days'] = retentionError;
|
||||
|
||||
// Validate key management
|
||||
const keyMgmt = settings.key_management;
|
||||
if (keyMgmt.auto_rotation) {
|
||||
const rotationError = validateSetting('rotation_interval_days', keyMgmt.rotation_interval_days);
|
||||
if (rotationError) errors['key_management.rotation_interval_days'] = rotationError;
|
||||
|
||||
const graceError = validateSetting('grace_period_days', keyMgmt.grace_period_days);
|
||||
if (graceError) errors['key_management.grace_period_days'] = graceError;
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
return { validateSetting, validateAll };
|
||||
};
|
||||
|
||||
// Import React for WebSocket hook
|
||||
import React from 'react';
|
||||
@@ -27,14 +27,13 @@ import {
|
||||
MonitorPlay,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { useAgents, useAgent, useScanAgent, useScanMultipleAgents, useUnregisterAgent } from '@/hooks/useAgents';
|
||||
import { useAgents, useAgent, useScanMultipleAgents, useUnregisterAgent } from '@/hooks/useAgents';
|
||||
import { useAgentUpdate } from '@/hooks/useAgentUpdate';
|
||||
import { useActiveCommands, useCancelCommand } from '@/hooks/useCommands';
|
||||
import { useHeartbeatStatus, useInvalidateHeartbeat, useHeartbeatAgentSync } from '@/hooks/useHeartbeat';
|
||||
import { agentApi } from '@/lib/api';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getStatusColor, formatRelativeTime, isOnline, formatBytes } from '@/lib/utils';
|
||||
import { AgentUpdate } from '@/components/AgentUpdate';
|
||||
import { cn } from '@/lib/utils';
|
||||
import toast from 'react-hot-toast';
|
||||
import { AgentSystemUpdates } from '@/components/AgentUpdates';
|
||||
@@ -62,6 +61,7 @@ const Agents: React.FC = () => {
|
||||
const [heartbeatLoading, setHeartbeatLoading] = useState(false); // Loading state for heartbeat toggle
|
||||
const [heartbeatCommandId, setHeartbeatCommandId] = useState<string | null>(null); // Track specific heartbeat command
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false); // Update modal state
|
||||
const [singleAgentUpdate, setSingleAgentUpdate] = useState<string | null>(null); // Single agent update modal
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
@@ -230,7 +230,6 @@ const Agents: React.FC = () => {
|
||||
// Fetch single agent if ID is provided
|
||||
const { data: selectedAgentData } = useAgent(id || '', !!id);
|
||||
|
||||
const scanAgentMutation = useScanAgent();
|
||||
const scanMultipleMutation = useScanMultipleAgents();
|
||||
const unregisterAgentMutation = useUnregisterAgent();
|
||||
|
||||
@@ -286,16 +285,6 @@ const Agents: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle scan operations
|
||||
const handleScanAgent = async (agentId: string) => {
|
||||
try {
|
||||
await scanAgentMutation.mutateAsync(agentId);
|
||||
toast.success('Scan triggered successfully');
|
||||
} catch (error) {
|
||||
// Error handling is done in the hook
|
||||
}
|
||||
};
|
||||
|
||||
const handleScanSelected = async () => {
|
||||
if (selectedAgents.length === 0) {
|
||||
toast.error('Please select at least one agent');
|
||||
@@ -498,19 +487,6 @@ const Agents: React.FC = () => {
|
||||
<span>Registered {formatRelativeTime(selectedAgent.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleScanAgent(selectedAgent.id)}
|
||||
disabled={scanAgentMutation.isPending}
|
||||
className="btn btn-primary sm:ml-4 w-full sm:w-auto"
|
||||
>
|
||||
{scanAgentMutation.isPending ? (
|
||||
<RefreshCw className="animate-spin h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Scan Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -838,22 +814,6 @@ const Agents: React.FC = () => {
|
||||
);
|
||||
})()
|
||||
)}
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-center mt-3 pt-3 border-t border-gray-200">
|
||||
<button
|
||||
onClick={() => handleScanAgent(selectedAgent.id)}
|
||||
disabled={scanAgentMutation.isPending}
|
||||
className="btn btn-primary w-full sm:w-auto text-sm"
|
||||
>
|
||||
{scanAgentMutation.isPending ? (
|
||||
<RefreshCw className="animate-spin h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Scan Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System info */}
|
||||
@@ -1326,10 +1286,19 @@ const Agents: React.FC = () => {
|
||||
{agent.current_version || 'Initial Registration'}
|
||||
</span>
|
||||
{agent.update_available === true && (
|
||||
<span className="flex items-center text-xs text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded-full">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Open update modal with this single agent
|
||||
setSingleAgentUpdate(agent.id);
|
||||
setShowUpdateModal(true);
|
||||
}}
|
||||
className="flex items-center text-xs text-amber-600 bg-amber-50 hover:bg-amber-100 px-1.5 py-0.5 rounded-full cursor-pointer transition-colors"
|
||||
title="Click to update agent"
|
||||
>
|
||||
<Download className="h-3 w-3 mr-1" />
|
||||
Update
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{agent.update_available === false && agent.current_version && (
|
||||
<span className="flex items-center text-xs text-green-600 bg-green-50 px-1.5 py-0.5 rounded-full">
|
||||
@@ -1373,14 +1342,6 @@ const Agents: React.FC = () => {
|
||||
</td>
|
||||
<td className="table-cell">
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handleScanAgent(agent.id)}
|
||||
disabled={scanAgentMutation.isPending}
|
||||
className="text-gray-400 hover:text-primary-600"
|
||||
title="Trigger scan"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedAgents([agent.id]);
|
||||
@@ -1395,13 +1356,6 @@ const Agents: React.FC = () => {
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
</button>
|
||||
{/* Agent Update with nonce security */}
|
||||
<AgentUpdate
|
||||
agent={agent}
|
||||
onUpdateComplete={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveAgent(agent.id, agent.hostname)}
|
||||
disabled={unregisterAgentMutation.isPending}
|
||||
@@ -1430,12 +1384,17 @@ const Agents: React.FC = () => {
|
||||
{/* Agent Updates Modal */}
|
||||
<AgentUpdatesModal
|
||||
isOpen={showUpdateModal}
|
||||
onClose={() => setShowUpdateModal(false)}
|
||||
selectedAgentIds={selectedAgents}
|
||||
onClose={() => {
|
||||
setShowUpdateModal(false);
|
||||
setSingleAgentUpdate(null);
|
||||
setSelectedAgents([]);
|
||||
}}
|
||||
selectedAgentIds={singleAgentUpdate ? [singleAgentUpdate] : selectedAgents}
|
||||
onAgentsUpdated={() => {
|
||||
// Refresh agents data after update
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
setSelectedAgents([]);
|
||||
setSingleAgentUpdate(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
738
aggregator-web/src/pages/SecuritySettings.tsx
Normal file
738
aggregator-web/src/pages/SecuritySettings.tsx
Normal file
@@ -0,0 +1,738 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Shield,
|
||||
Lock,
|
||||
Key,
|
||||
FileText,
|
||||
Settings as SettingsIcon,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
Download,
|
||||
Upload,
|
||||
Save,
|
||||
RotateCcw,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ChevronRight,
|
||||
Activity,
|
||||
History,
|
||||
Terminal,
|
||||
Server
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useSecuritySettings, useSecurityValidation } from '@/hooks/useSecuritySettings';
|
||||
import { SecuritySettings as SecuritySettingsType, SecuritySetting, ConfirmationDialogState } from '@/types/security';
|
||||
import SecurityStatusCard from '@/components/security/SecurityStatusCard';
|
||||
import SecurityCategorySection from '@/components/security/SecurityCategorySection';
|
||||
import SecurityEvents from '@/components/security/SecurityEvents';
|
||||
|
||||
const SecuritySettings: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { tab = 'overview' } = useParams();
|
||||
|
||||
const {
|
||||
settings,
|
||||
securityOverview,
|
||||
loading,
|
||||
saving,
|
||||
error,
|
||||
updateSetting,
|
||||
updateSettings,
|
||||
rotateSecurityKey,
|
||||
exportSettings,
|
||||
importSettings,
|
||||
resetToDefaults,
|
||||
refetch,
|
||||
} = useSecuritySettings();
|
||||
|
||||
const { validateAll } = useSecurityValidation();
|
||||
|
||||
// State management
|
||||
const [activeTab, setActiveTab] = useState(tab);
|
||||
const [localSettings, setLocalSettings] = useState<SecuritySettingsType | null>(null);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const [confirmationDialog, setConfirmationDialog] = useState<ConfirmationDialogState>({
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
severity: 'warning',
|
||||
requiresConfirmation: false,
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {},
|
||||
});
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
// Sync URL tab with state
|
||||
useEffect(() => {
|
||||
if (tab !== activeTab) {
|
||||
navigate(`/settings/security/${activeTab}`, { replace: true });
|
||||
}
|
||||
}, [activeTab, tab, navigate]);
|
||||
|
||||
// Initialize local settings
|
||||
useEffect(() => {
|
||||
if (settings && !localSettings) {
|
||||
setLocalSettings(JSON.parse(JSON.stringify(settings)));
|
||||
}
|
||||
}, [settings, localSettings]);
|
||||
|
||||
// Validate settings when they change
|
||||
useEffect(() => {
|
||||
if (localSettings) {
|
||||
const errors = validateAll(localSettings);
|
||||
setValidationErrors(errors);
|
||||
}
|
||||
}, [localSettings, validateAll]);
|
||||
|
||||
// Tab configuration
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview', icon: Shield },
|
||||
{ id: 'command-signing', label: 'Command Signing', icon: Terminal },
|
||||
{ id: 'update-security', label: 'Update Security', icon: Download },
|
||||
{ id: 'machine-binding', label: 'Machine Binding', icon: Server },
|
||||
{ id: 'logging', label: 'Logging', icon: FileText },
|
||||
{ id: 'key-management', label: 'Key Management', icon: Key },
|
||||
{ id: 'events', label: 'Security Events', icon: Activity },
|
||||
{ id: 'audit', label: 'Audit Trail', icon: History },
|
||||
];
|
||||
|
||||
// Command Signing Settings
|
||||
const commandSigningSettings: SecuritySetting[] = [
|
||||
{
|
||||
key: 'enabled',
|
||||
label: 'Enable Command Signing',
|
||||
type: 'toggle',
|
||||
value: localSettings?.command_signing?.enabled ?? false,
|
||||
description: 'Cryptographically sign all commands to prevent tampering',
|
||||
},
|
||||
{
|
||||
key: 'enforcement_mode',
|
||||
label: 'Enforcement Mode',
|
||||
type: 'select',
|
||||
value: localSettings?.command_signing?.enforcement_mode ?? 'strict',
|
||||
options: ['strict', 'warning', 'disabled'],
|
||||
description: 'How to handle unsigned commands',
|
||||
disabled: !localSettings?.command_signing?.enabled,
|
||||
},
|
||||
{
|
||||
key: 'algorithm',
|
||||
label: 'Signing Algorithm',
|
||||
type: 'select',
|
||||
value: localSettings?.command_signing?.algorithm ?? 'ed25519',
|
||||
options: ['ed25519', 'rsa-2048', 'ecdsa-p256'],
|
||||
description: 'Cryptographic algorithm for signing commands',
|
||||
disabled: !localSettings?.command_signing?.enabled,
|
||||
},
|
||||
];
|
||||
|
||||
// Update Security Settings
|
||||
const updateSecuritySettings: SecuritySetting[] = [
|
||||
{
|
||||
key: 'enabled',
|
||||
label: 'Enable Update Security',
|
||||
type: 'toggle',
|
||||
value: localSettings?.update_security?.enabled ?? false,
|
||||
description: 'Require signed updates and nonce validation',
|
||||
},
|
||||
{
|
||||
key: 'enforcement_mode',
|
||||
label: 'Enforcement Mode',
|
||||
type: 'select',
|
||||
value: localSettings?.update_security?.enforcement_mode ?? 'strict',
|
||||
options: ['strict', 'warning', 'disabled'],
|
||||
description: 'How to handle unsigned or invalid updates',
|
||||
disabled: !localSettings?.update_security?.enabled,
|
||||
},
|
||||
{
|
||||
key: 'nonce_timeout_seconds',
|
||||
label: 'Nonce Timeout',
|
||||
type: 'slider',
|
||||
value: localSettings?.update_security?.nonce_timeout_seconds ?? 300,
|
||||
min: 60,
|
||||
max: 3600,
|
||||
step: 60,
|
||||
description: 'How long a nonce is valid (in seconds)',
|
||||
disabled: !localSettings?.update_security?.enabled,
|
||||
},
|
||||
{
|
||||
key: 'require_signature_verification',
|
||||
label: 'Require Signature Verification',
|
||||
type: 'toggle',
|
||||
value: localSettings?.update_security?.require_signature_verification ?? true,
|
||||
description: 'Verify digital signatures on all updates',
|
||||
disabled: !localSettings?.update_security?.enabled,
|
||||
},
|
||||
];
|
||||
|
||||
// Machine Binding Settings
|
||||
const machineBindingSettings: SecuritySetting[] = [
|
||||
{
|
||||
key: 'enabled',
|
||||
label: 'Enable Machine Binding',
|
||||
type: 'toggle',
|
||||
value: localSettings?.machine_binding?.enabled ?? false,
|
||||
description: 'Bind agents to specific machine fingerprint',
|
||||
},
|
||||
{
|
||||
key: 'enforcement_mode',
|
||||
label: 'Enforcement Mode',
|
||||
type: 'select',
|
||||
value: localSettings?.machine_binding?.enforcement_mode ?? 'strict',
|
||||
options: ['strict', 'warning', 'disabled'],
|
||||
description: 'How to handle machine binding violations',
|
||||
disabled: !localSettings?.machine_binding?.enabled,
|
||||
},
|
||||
{
|
||||
key: 'binding_grace_period_minutes',
|
||||
label: 'Grace Period',
|
||||
type: 'slider',
|
||||
value: localSettings?.machine_binding?.binding_grace_period_minutes ?? 5,
|
||||
min: 1,
|
||||
max: 60,
|
||||
step: 1,
|
||||
description: 'Minutes to allow before enforcing binding',
|
||||
disabled: !localSettings?.machine_binding?.enabled,
|
||||
},
|
||||
{
|
||||
key: 'binding_components',
|
||||
label: 'Binding Components',
|
||||
type: 'checkbox-group',
|
||||
value: localSettings?.machine_binding?.binding_components ?? {},
|
||||
options: [
|
||||
{ label: 'Hardware ID', value: 'hardware_id' },
|
||||
{ label: 'BIOS UUID', value: 'bios_uuid' },
|
||||
{ label: 'MAC Addresses', value: 'mac_addresses' },
|
||||
{ label: 'CPU ID', value: 'cpu_id' },
|
||||
{ label: 'Disk Serial', value: 'disk_serial' },
|
||||
],
|
||||
description: 'Machine components to bind against',
|
||||
disabled: !localSettings?.machine_binding?.enabled,
|
||||
},
|
||||
{
|
||||
key: 'violation_action',
|
||||
label: 'Violation Action',
|
||||
type: 'select',
|
||||
value: localSettings?.machine_binding?.violation_action ?? 'block',
|
||||
options: ['block', 'warn', 'log_only'],
|
||||
description: 'Action to take on binding violations',
|
||||
disabled: !localSettings?.machine_binding?.enabled,
|
||||
},
|
||||
];
|
||||
|
||||
// Logging Settings
|
||||
const loggingSettings: SecuritySetting[] = [
|
||||
{
|
||||
key: 'log_level',
|
||||
label: 'Log Level',
|
||||
type: 'select',
|
||||
value: localSettings?.logging?.log_level ?? 'info',
|
||||
options: ['debug', 'info', 'warn', 'error'],
|
||||
description: 'Minimum severity level to log',
|
||||
},
|
||||
{
|
||||
key: 'retention_days',
|
||||
label: 'Retention Period',
|
||||
type: 'number',
|
||||
value: localSettings?.logging?.retention_days ?? 30,
|
||||
min: 1,
|
||||
max: 365,
|
||||
description: 'Days to retain security logs (1-365)',
|
||||
},
|
||||
{
|
||||
key: 'log_failures',
|
||||
label: 'Log Security Failures',
|
||||
type: 'toggle',
|
||||
value: localSettings?.logging?.log_failures ?? true,
|
||||
description: 'Record all security failures and violations',
|
||||
},
|
||||
{
|
||||
key: 'log_successes',
|
||||
label: 'Log Security Successes',
|
||||
type: 'toggle',
|
||||
value: localSettings?.logging?.log_successes ?? false,
|
||||
description: 'Record successful security operations',
|
||||
},
|
||||
{
|
||||
key: 'export_format',
|
||||
label: 'Export Format',
|
||||
type: 'select',
|
||||
value: localSettings?.logging?.export_format ?? 'json',
|
||||
options: ['json', 'csv', 'syslog'],
|
||||
description: 'Default format for log exports',
|
||||
},
|
||||
];
|
||||
|
||||
// Key Management Settings
|
||||
const keyManagementSettings: SecuritySetting[] = [
|
||||
{
|
||||
key: 'current_key_info',
|
||||
label: 'Current Key',
|
||||
type: 'text',
|
||||
value: localSettings?.key_management?.current_key?.key_id ?? 'No key configured',
|
||||
description: 'Currently active signing key',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'auto_rotation',
|
||||
label: 'Auto-Rotation',
|
||||
type: 'toggle',
|
||||
value: localSettings?.key_management?.auto_rotation ?? false,
|
||||
description: 'Automatically rotate signing keys on schedule',
|
||||
},
|
||||
{
|
||||
key: 'rotation_interval_days',
|
||||
label: 'Rotation Interval',
|
||||
type: 'number',
|
||||
value: localSettings?.key_management?.rotation_interval_days ?? 90,
|
||||
min: 7,
|
||||
max: 365,
|
||||
description: 'Days between automatic key rotations',
|
||||
disabled: !localSettings?.key_management?.auto_rotation,
|
||||
},
|
||||
{
|
||||
key: 'grace_period_days',
|
||||
label: 'Grace Period',
|
||||
type: 'number',
|
||||
value: localSettings?.key_management?.grace_period_days ?? 7,
|
||||
min: 1,
|
||||
max: 30,
|
||||
description: 'Days to accept old key after rotation',
|
||||
disabled: !localSettings?.key_management?.auto_rotation,
|
||||
},
|
||||
];
|
||||
|
||||
// Handle settings change
|
||||
const handleSettingChange = async (category: string, key: string, value: any) => {
|
||||
if (!localSettings) return;
|
||||
|
||||
const newSettings = {
|
||||
...localSettings,
|
||||
[category]: {
|
||||
...localSettings[category as keyof SecuritySettingsType],
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
|
||||
setLocalSettings(newSettings);
|
||||
setHasChanges(true);
|
||||
|
||||
// Auto-save for simple toggles
|
||||
if (typeof value === 'boolean') {
|
||||
try {
|
||||
await updateSetting(category, key, value);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
// Revert on error
|
||||
setLocalSettings(settings);
|
||||
setHasChanges(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Save all changes
|
||||
const handleSaveChanges = async () => {
|
||||
if (!localSettings || !hasChanges) return;
|
||||
|
||||
try {
|
||||
await updateSettings(localSettings);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Show confirmation dialog
|
||||
const showConfirmation = (
|
||||
title: string,
|
||||
message: string,
|
||||
onConfirm: () => void,
|
||||
requiresConfirmation: boolean = false
|
||||
) => {
|
||||
setConfirmationDialog({
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
severity: 'danger',
|
||||
requiresConfirmation,
|
||||
onConfirm: () => {
|
||||
onConfirm();
|
||||
setConfirmationDialog(prev => ({ ...prev, isOpen: false }));
|
||||
},
|
||||
onCancel: () => setConfirmationDialog(prev => ({ ...prev, isOpen: false })),
|
||||
});
|
||||
};
|
||||
|
||||
// Handle key rotation
|
||||
const handleRotateKey = () => {
|
||||
showConfirmation(
|
||||
'Rotate Security Key',
|
||||
'Rotating the security key will invalidate all existing agent connections. Agents will need to reconnect with the new key. This action cannot be undone.',
|
||||
async () => {
|
||||
await rotateSecurityKey({ reason: 'manual' });
|
||||
refetch();
|
||||
},
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
// Handle reset to defaults
|
||||
const handleResetDefaults = () => {
|
||||
showConfirmation(
|
||||
'Reset to Defaults',
|
||||
'This will reset all security settings to their default values. This may affect your system security. Type "RESET" to confirm.',
|
||||
async () => {
|
||||
await resetToDefaults();
|
||||
setLocalSettings(null);
|
||||
setHasChanges(false);
|
||||
},
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
// Render tab content
|
||||
const renderTabContent = () => {
|
||||
if (!localSettings) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (activeTab) {
|
||||
case 'overview':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{securityOverview && (
|
||||
<SecurityStatusCard
|
||||
status={{
|
||||
overall: securityOverview.overall_status,
|
||||
features: securityOverview.subsystems ? Object.entries(securityOverview.subsystems).map(([name, data]: [string, any]) => ({
|
||||
name: name.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||
enabled: data.enabled,
|
||||
status: data.status === 'healthy' ? 'healthy' : data.status === 'warning' ? 'warning' : 'error',
|
||||
last_check: new Date().toISOString(),
|
||||
details: data.status,
|
||||
})) : [],
|
||||
recent_events: securityOverview.alerts?.length || 0,
|
||||
last_updated: new Date().toISOString(),
|
||||
}}
|
||||
onRefresh={refetch}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={handleSaveChanges}
|
||||
disabled={!hasChanges || saving || Object.keys(validationErrors).length > 0}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={exportSettings}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Export Settings
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => document.getElementById('import-file')?.click()}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
Import Settings
|
||||
</button>
|
||||
<input
|
||||
id="import-file"
|
||||
type="file"
|
||||
accept=".json"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
await importSettings(file);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error);
|
||||
}
|
||||
}
|
||||
e.target.value = '';
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleResetDefaults}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status Summary */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Security Status</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ name: 'Command Signing', status: localSettings.command_signing.enabled },
|
||||
{ name: 'Update Security', status: localSettings.update_security.enabled },
|
||||
{ name: 'Machine Binding', status: localSettings.machine_binding.enabled },
|
||||
{ name: 'Security Logging', status: true },
|
||||
].map((feature) => (
|
||||
<div key={feature.name} className="flex items-center space-x-2">
|
||||
{feature.status ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900">{feature.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'command-signing':
|
||||
return (
|
||||
<SecurityCategorySection
|
||||
title="Command Signing"
|
||||
description="Configure cryptographic signing of all commands to prevent tampering and ensure authenticity"
|
||||
settings={commandSigningSettings}
|
||||
onSettingChange={(key, value) => handleSettingChange('command_signing', key, value)}
|
||||
disabled={loading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'update-security':
|
||||
return (
|
||||
<SecurityCategorySection
|
||||
title="Update Security"
|
||||
description="Configure security measures for agent updates including signature verification and nonce validation"
|
||||
settings={updateSecuritySettings}
|
||||
onSettingChange={(key, value) => handleSettingChange('update_security', key, value)}
|
||||
disabled={loading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'machine-binding':
|
||||
return (
|
||||
<SecurityCategorySection
|
||||
title="Machine Binding"
|
||||
description="Bind agents to specific machine fingerprints to prevent unauthorized access"
|
||||
settings={machineBindingSettings}
|
||||
onSettingChange={(key, value) => handleSettingChange('machine_binding', key, value)}
|
||||
disabled={loading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'logging':
|
||||
return (
|
||||
<SecurityCategorySection
|
||||
title="Security Logging"
|
||||
description="Configure logging of security events, failures, and audit trails"
|
||||
settings={loggingSettings}
|
||||
onSettingChange={(key, value) => handleSettingChange('logging', key, value)}
|
||||
disabled={loading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'key-management':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SecurityCategorySection
|
||||
title="Key Management"
|
||||
description="Manage cryptographic keys used for signing and verification"
|
||||
settings={keyManagementSettings}
|
||||
onSettingChange={(key, value) => handleSettingChange('key_management', key, value)}
|
||||
disabled={loading}
|
||||
error={error}
|
||||
/>
|
||||
|
||||
{/* Key Actions */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Key Actions</h3>
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handleRotateKey}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Rotate Security Key
|
||||
</button>
|
||||
<p className="text-sm text-gray-600">
|
||||
Generate a new signing key. The old key will remain valid during the grace period.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'events':
|
||||
return <SecurityEvents />;
|
||||
|
||||
case 'audit':
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Audit Trail</h2>
|
||||
<p className="text-gray-600">Audit trail implementation coming soon...</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-6 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<Shield className="w-8 h-8 text-blue-600" />
|
||||
Security Settings
|
||||
</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Configure security features to protect your RedFlag deployment
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
{showAdvanced ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
{showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||
<span className="text-red-800">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation Errors */}
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600" />
|
||||
<span className="font-medium text-yellow-800">Validation Errors</span>
|
||||
</div>
|
||||
<ul className="text-sm text-yellow-700 space-y-1">
|
||||
{Object.entries(validationErrors).map(([key, error]) => (
|
||||
<li key={key}>• {error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<nav className="-mb-px flex space-x-8 overflow-x-auto">
|
||||
{tabs.map((tabItem) => (
|
||||
<button
|
||||
key={tabItem.id}
|
||||
onClick={() => setActiveTab(tabItem.id)}
|
||||
className={`flex items-center gap-2 py-3 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === tabItem.id
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<tabItem.icon className="w-4 h-4" />
|
||||
{tabItem.label}
|
||||
{tabItem.id === 'events' && securityOverview?.alerts?.length > 0 && (
|
||||
<span className="ml-1 px-2 py-0.5 text-xs bg-red-100 text-red-800 rounded-full">
|
||||
{securityOverview.alerts.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="min-h-[600px]">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
{confirmationDialog.isOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{confirmationDialog.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{confirmationDialog.message}
|
||||
</p>
|
||||
{confirmationDialog.requiresConfirmation && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Type "{confirmationDialog.title === 'Rotate Security Key' ? 'CONFIRM' : 'RESET'}" to proceed
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
onChange={(e) => {
|
||||
const expected = confirmationDialog.title === 'Rotate Security Key' ? 'CONFIRM' : 'RESET';
|
||||
if (e.target.value === expected) {
|
||||
e.target.classList.remove('border-red-300');
|
||||
e.target.classList.add('border-green-300');
|
||||
} else {
|
||||
e.target.classList.remove('border-green-300');
|
||||
e.target.classList.add('border-red-300');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={confirmationDialog.onCancel}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmationDialog.onConfirm}
|
||||
className={`px-4 py-2 rounded-lg text-white ${
|
||||
confirmationDialog.severity === 'danger'
|
||||
? 'bg-red-600 hover:bg-red-700'
|
||||
: 'bg-blue-600 hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySettings;
|
||||
@@ -36,6 +36,7 @@ const Setup: React.FC = () => {
|
||||
const [showDbPassword, setShowDbPassword] = useState(false);
|
||||
const [signingKeys, setSigningKeys] = useState<SigningKeys | null>(null);
|
||||
const [generatingKeys, setGeneratingKeys] = useState(false);
|
||||
const [configType, setConfigType] = useState<'env' | 'swarm'>('env');
|
||||
|
||||
const [formData, setFormData] = useState<SetupFormData>({
|
||||
adminUser: 'admin',
|
||||
@@ -144,13 +145,14 @@ const Setup: React.FC = () => {
|
||||
try {
|
||||
const result = await setupApi.configure(formData);
|
||||
|
||||
// Add signing keys to env content if generated
|
||||
let finalEnvContent = result.envContent || '';
|
||||
if (signingKeys && finalEnvContent) {
|
||||
finalEnvContent += `\n# Ed25519 Signing Keys (for agent updates)\nREDFLAG_SIGNING_PRIVATE_KEY=${signingKeys.private_key}\n`;
|
||||
let configContent = '';
|
||||
if (configType === 'env') {
|
||||
configContent = generateEnvContent(result, signingKeys);
|
||||
} else {
|
||||
configContent = generateDockerSecretCommands(result, signingKeys);
|
||||
}
|
||||
|
||||
setEnvContent(finalEnvContent || null);
|
||||
setEnvContent(configContent || null);
|
||||
setShowSuccess(true);
|
||||
toast.success(result.message || 'Configuration saved successfully!');
|
||||
|
||||
@@ -164,6 +166,62 @@ const Setup: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const generateEnvContent = (result: any, keys: SigningKeys | null): string => {
|
||||
if (!result.envContent) return '';
|
||||
|
||||
let envContent = result.envContent;
|
||||
|
||||
if (keys) {
|
||||
envContent += `\n# Ed25519 Signing Keys (for agent updates)\nREDFLAG_SIGNING_PRIVATE_KEY=${keys.private_key}\n`;
|
||||
}
|
||||
|
||||
return envContent;
|
||||
};
|
||||
|
||||
const generateDockerSecretCommands = (result: any, keys: SigningKeys | null): string => {
|
||||
if (!result.envContent) return '';
|
||||
|
||||
// Parse the envContent to extract values
|
||||
const envLines = result.envContent.split('\n');
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
envLines.forEach(line => {
|
||||
const match = line.match(/^([^#=]+)=(.+)$/);
|
||||
if (match) {
|
||||
envVars[match[1].trim()] = match[2].trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Add signing keys if available
|
||||
if (keys) {
|
||||
envVars['REDFLAG_SIGNING_PRIVATE_KEY'] = keys.private_key;
|
||||
}
|
||||
|
||||
// Generate Docker secret commands
|
||||
const commands = [
|
||||
'# RedFlag Docker Secrets Configuration',
|
||||
'# Generated by web setup on 2025-12-13',
|
||||
'# [WARNING] SECURITY CRITICAL: Backup the signing key or you will lose access to all agents',
|
||||
'#',
|
||||
'# Run these commands on your Docker host to create the secrets:',
|
||||
'#',
|
||||
`printf '%s' '${envVars['REDFLAG_ADMIN_PASSWORD'] || ''}' | docker secret create redflag_admin_password -`,
|
||||
`printf '%s' '${envVars['REDFLAG_JWT_SECRET'] || ''}' | docker secret create redflag_jwt_secret -`,
|
||||
`printf '%s' '${envVars['REDFLAG_DB_PASSWORD'] || ''}' | docker secret create redflag_db_password -`,
|
||||
`printf '%s' '${envVars['REDFLAG_SIGNING_PRIVATE_KEY'] || ''}' | docker secret create redflag_signing_private_key -`,
|
||||
'',
|
||||
'# After creating the secrets, restart your RedFlag server:',
|
||||
'# docker compose down && docker compose up -d',
|
||||
'',
|
||||
'# Optional: Save these values securely (password manager, encrypted storage)',
|
||||
`# Admin Password: ${envVars['REDFLAG_ADMIN_PASSWORD'] || ''}`,
|
||||
`# JWT Secret: ${envVars['REDFLAG_JWT_SECRET'] || ''}`,
|
||||
`# DB Password: ${envVars['REDFLAG_DB_PASSWORD'] || ''}`,
|
||||
].join('\n');
|
||||
|
||||
return commands;
|
||||
};
|
||||
|
||||
// Success screen with configuration display
|
||||
if (showSuccess && envContent) {
|
||||
return (
|
||||
@@ -211,7 +269,22 @@ const Setup: React.FC = () => {
|
||||
{/* Configuration Content Section */}
|
||||
{envContent && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">Configuration File Content</h3>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{configType === 'env' ? 'Environment Configuration (.env)' : 'Docker Swarm Secrets'}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">.env</span>
|
||||
<button
|
||||
onClick={() => setConfigType(configType === 'env' ? 'swarm' : 'env')}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full ${configType === 'swarm' ? 'bg-indigo-600' : 'bg-gray-200'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition ${configType === 'swarm' ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
<span className="text-sm text-gray-600">Swarm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-md p-4">
|
||||
<textarea
|
||||
readOnly
|
||||
@@ -219,33 +292,76 @@ const Setup: React.FC = () => {
|
||||
className="w-full h-64 p-3 text-xs font-mono text-gray-800 bg-white border border-gray-300 rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(envContent);
|
||||
toast.success('Configuration content copied to clipboard!');
|
||||
}}
|
||||
className="mt-3 w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
Copy Configuration Content
|
||||
</button>
|
||||
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Important:</strong> Copy this configuration content and save it to <code className="bg-blue-100 px-1 rounded">./config/.env</code>, then run <code className="bg-blue-100 px-1 rounded">docker-compose down && docker-compose up -d</code> to apply the configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{configType === 'env' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(envContent);
|
||||
toast.success('.env content copied to clipboard!');
|
||||
}}
|
||||
className="mt-3 w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
Copy .env Content
|
||||
</button>
|
||||
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Next Steps:</strong> Save this content to <code className="bg-blue-100 px-1 rounded">config/.env</code> and run <code className="bg-blue-100 px-1 rounded">docker compose down && docker compose up -d</code> to apply the configuration.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Security Note:</strong> The <code className="bg-yellow-100 px-1 rounded">config/.env</code> file contains sensitive credentials. Ensure it has restricted permissions (<code className="bg-yellow-100 px-1 rounded">chmod 600</code>) and is excluded from version control.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(envContent);
|
||||
toast.success('Docker secret commands copied to clipboard!');
|
||||
}}
|
||||
className="mt-3 w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
Copy Docker Secret Commands
|
||||
</button>
|
||||
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Requirements:</strong> Docker Swarm mode is required. Run <code className="bg-blue-100 px-1 rounded">docker swarm init</code> on your Docker host before creating secrets.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Next Steps:</strong> Run the copied commands on your Docker host, then update <code className="bg-yellow-100 px-1 rounded">docker-compose.yml</code> to mount the secrets and restart.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* Next Steps */}
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">Next Steps</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-600">
|
||||
<li>Copy the configuration content using the green button above</li>
|
||||
<li>Save it to <code className="bg-gray-100 px-1 rounded">./config/.env</code></li>
|
||||
<li>Run <code className="bg-gray-100 px-1 rounded">docker-compose down && docker-compose up -d</code></li>
|
||||
<li>Login to the dashboard with your admin username and password</li>
|
||||
</ol>
|
||||
{configType === 'env' ? (
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-600">
|
||||
<li>Copy the .env content using the green button above</li>
|
||||
<li>Save it to <code className="bg-gray-100 px-1 rounded">config/.env</code></li>
|
||||
<li>Run <code className="bg-gray-100 px-1 rounded">docker compose down && docker compose up -d</code></li>
|
||||
<li>Login to the dashboard with your admin username and password</li>
|
||||
</ol>
|
||||
) : (
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-600">
|
||||
<li>Initialize Docker Swarm: <code className="bg-gray-100 px-1 rounded">docker swarm init</code></li>
|
||||
<li>Copy the Docker secret commands using the green button above</li>
|
||||
<li>Run the commands on your Docker host to create the secrets</li>
|
||||
<li>Update <code className="bg-gray-100 px-1 rounded">docker-compose.yml</code> to mount the secrets</li>
|
||||
<li>Restart RedFlag with <code className="bg-gray-100 px-1 rounded">docker compose down && docker compose up -d</code></li>
|
||||
<li>Login to the dashboard with your admin username and password</li>
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 space-y-3">
|
||||
|
||||
@@ -110,13 +110,13 @@ const TokenManagement: React.FC = () => {
|
||||
|
||||
const copyInstallCommand = async (token: string) => {
|
||||
const serverUrl = getServerUrl();
|
||||
const command = `curl -sfL ${serverUrl}/api/v1/install/linux | bash -s -- ${token}`;
|
||||
const command = `curl -sfL "${serverUrl}/api/v1/install/linux?token=${token}" | sudo bash`;
|
||||
await navigator.clipboard.writeText(command);
|
||||
};
|
||||
|
||||
const generateInstallCommand = (token: string) => {
|
||||
const serverUrl = getServerUrl();
|
||||
return `curl -sfL ${serverUrl}/api/v1/install/linux | bash -s -- ${token}`;
|
||||
return `curl -sfL "${serverUrl}/api/v1/install/linux?token=${token}" | sudo bash`;
|
||||
};
|
||||
|
||||
const getStatusColor = (token: RegistrationToken) => {
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
RefreshCw,
|
||||
Code,
|
||||
FileText,
|
||||
Package
|
||||
Package,
|
||||
Key
|
||||
} from 'lucide-react';
|
||||
import { useRegistrationTokens } from '@/hooks/useRegistrationTokens';
|
||||
import { toast } from 'react-hot-toast';
|
||||
@@ -73,15 +74,15 @@ const AgentManagement: React.FC = () => {
|
||||
|
||||
if (platform.id === 'linux') {
|
||||
if (token !== 'YOUR_REGISTRATION_TOKEN') {
|
||||
return `curl -sfL ${serverUrl}${platform.installScript} | sudo bash -s -- ${token}`;
|
||||
return `curl -sfL "${serverUrl}${platform.installScript}?token=${token}" | sudo bash`;
|
||||
} else {
|
||||
return `curl -sfL ${serverUrl}${platform.installScript} | sudo bash`;
|
||||
return `curl -sfL "${serverUrl}${platform.installScript}" | sudo bash`;
|
||||
}
|
||||
} else if (platform.id === 'windows') {
|
||||
if (token !== 'YOUR_REGISTRATION_TOKEN') {
|
||||
return `iwr ${serverUrl}${platform.installScript} -OutFile install.bat; .\\install.bat ${token}`;
|
||||
return `iwr "${serverUrl}${platform.installScript}?token=${token}" -OutFile install.bat; .\\install.bat`;
|
||||
} else {
|
||||
return `iwr ${serverUrl}${platform.installScript} -OutFile install.bat; .\\install.bat`;
|
||||
return `iwr "${serverUrl}${platform.installScript}" -OutFile install.bat; .\\install.bat`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
@@ -93,15 +94,15 @@ const AgentManagement: React.FC = () => {
|
||||
|
||||
if (platform.id === 'windows') {
|
||||
if (token !== 'YOUR_REGISTRATION_TOKEN') {
|
||||
return `# Download and run as Administrator with token\niwr ${serverUrl}${platform.installScript} -OutFile install.bat\n.\\install.bat ${token}`;
|
||||
return `# Download and run as Administrator with token\niwr "${serverUrl}${platform.installScript}?token=${token}" -OutFile install.bat\n.\\install.bat`;
|
||||
} else {
|
||||
return `# Download and run as Administrator\niwr ${serverUrl}${platform.installScript} -OutFile install.bat\n.\\install.bat`;
|
||||
return `# Download and run as Administrator\niwr "${serverUrl}${platform.installScript}" -OutFile install.bat\n.\\install.bat`;
|
||||
}
|
||||
} else {
|
||||
if (token !== 'YOUR_REGISTRATION_TOKEN') {
|
||||
return `# Download and run as root with token\ncurl -sfL ${serverUrl}${platform.installScript} | sudo bash -s -- ${token}`;
|
||||
return `# Download and run as root with token\ncurl -sfL "${serverUrl}${platform.installScript}?token=${token}" | sudo bash`;
|
||||
} else {
|
||||
return `# Download and run as root\ncurl -sfL ${serverUrl}${platform.installScript} | sudo bash`;
|
||||
return `# Download and run as root\ncurl -sfL "${serverUrl}${platform.installScript}" | sudo bash`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
314
aggregator-web/src/types/security.ts
Normal file
314
aggregator-web/src/types/security.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
// Security Settings Types for RedFlag
|
||||
|
||||
export interface SecuritySettings {
|
||||
command_signing: CommandSigningSettings;
|
||||
update_security: UpdateSecuritySettings;
|
||||
machine_binding: MachineBindingSettings;
|
||||
logging: LoggingSettings;
|
||||
key_management: KeyManagementSettings;
|
||||
}
|
||||
|
||||
export interface CommandSigningSettings {
|
||||
enabled: boolean;
|
||||
enforcement_mode: 'strict' | 'warning' | 'disabled';
|
||||
algorithm: 'ed25519' | 'rsa' | 'ecdsa';
|
||||
key_id?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSecuritySettings {
|
||||
enabled: boolean;
|
||||
enforcement_mode: 'strict' | 'warning' | 'disabled';
|
||||
nonce_timeout_seconds: number;
|
||||
require_signature_verification: boolean;
|
||||
allowed_algorithms: string[];
|
||||
}
|
||||
|
||||
export interface MachineBindingSettings {
|
||||
enabled: boolean;
|
||||
enforcement_mode: 'strict' | 'warning' | 'disabled';
|
||||
binding_components: {
|
||||
hardware_id: boolean;
|
||||
bios_uuid: boolean;
|
||||
mac_addresses: boolean;
|
||||
cpu_id: boolean;
|
||||
disk_serial: boolean;
|
||||
};
|
||||
violation_action: 'block' | 'warn' | 'log_only';
|
||||
binding_grace_period_minutes: number;
|
||||
}
|
||||
|
||||
export interface LoggingSettings {
|
||||
log_level: 'debug' | 'info' | 'warn' | 'error';
|
||||
retention_days: number;
|
||||
log_failures: boolean;
|
||||
log_successes: boolean;
|
||||
log_to_file: boolean;
|
||||
log_to_console: boolean;
|
||||
export_format: 'json' | 'csv' | 'syslog';
|
||||
}
|
||||
|
||||
export interface KeyManagementSettings {
|
||||
current_key: {
|
||||
key_id: string;
|
||||
algorithm: string;
|
||||
created_at: string;
|
||||
expires_at?: string;
|
||||
fingerprint: string;
|
||||
};
|
||||
auto_rotation: boolean;
|
||||
rotation_interval_days: number;
|
||||
grace_period_days: number;
|
||||
key_history: KeyHistoryEntry[];
|
||||
}
|
||||
|
||||
export interface KeyHistoryEntry {
|
||||
key_id: string;
|
||||
algorithm: string;
|
||||
created_at: string;
|
||||
retired_at: string;
|
||||
reason: 'rotation' | 'compromise' | 'manual';
|
||||
}
|
||||
|
||||
// UI Component Types
|
||||
export type SecuritySettingType =
|
||||
| 'toggle'
|
||||
| 'select'
|
||||
| 'number'
|
||||
| 'text'
|
||||
| 'json'
|
||||
| 'slider'
|
||||
| 'checkbox-group';
|
||||
|
||||
export interface SecuritySetting {
|
||||
key: string;
|
||||
label: string;
|
||||
type: SecuritySettingType;
|
||||
value: any;
|
||||
default?: any;
|
||||
description?: string;
|
||||
options?: string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
validation?: (value: any) => string | null;
|
||||
disabled?: boolean;
|
||||
sensitive?: boolean;
|
||||
}
|
||||
|
||||
export interface SecurityCategorySectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
settings: SecuritySetting[];
|
||||
onSettingChange: (key: string, value: any) => void;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface SecuritySettingProps {
|
||||
setting: SecuritySetting;
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface SecurityStatus {
|
||||
overall: 'healthy' | 'warning' | 'critical';
|
||||
features: SecurityFeatureStatus[];
|
||||
recent_events: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
export interface SecurityFeatureStatus {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
status: 'healthy' | 'warning' | 'error';
|
||||
last_check: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
export interface SecurityStatusCardProps {
|
||||
status: SecurityStatus;
|
||||
onRefresh?: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
// Security Events and Audit Trail
|
||||
export interface SecurityEvent {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
severity: 'info' | 'warn' | 'error' | 'critical';
|
||||
category: 'command_signing' | 'update_security' | 'machine_binding' | 'key_management' | 'authentication';
|
||||
event_type: string;
|
||||
agent_id?: string;
|
||||
user_id?: string;
|
||||
message: string;
|
||||
details: Record<string, any>;
|
||||
trace_id?: string;
|
||||
correlation_id?: string;
|
||||
}
|
||||
|
||||
export interface AuditEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
user_id: string;
|
||||
user_name: string;
|
||||
action: string;
|
||||
category: string;
|
||||
setting_key: string;
|
||||
old_value: any;
|
||||
new_value: any;
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface SecurityEventsState {
|
||||
events: SecurityEvent[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
filters: EventFilters;
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
};
|
||||
liveUpdates: boolean;
|
||||
}
|
||||
|
||||
export interface EventFilters {
|
||||
severity?: string[];
|
||||
category?: string[];
|
||||
date_range?: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
agent_id?: string;
|
||||
user_id?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface SecurityEventsProps {
|
||||
events: SecurityEvent[];
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
filters: EventFilters;
|
||||
onFiltersChange: (filters: EventFilters) => void;
|
||||
onEventSelect?: (event: SecurityEvent) => void;
|
||||
onExport?: (format: 'json' | 'csv') => void;
|
||||
pagination?: any;
|
||||
liveUpdates?: boolean;
|
||||
onToggleLiveUpdates?: () => void;
|
||||
}
|
||||
|
||||
// Confirmation Dialog Types
|
||||
export interface ConfirmationDialogState {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
details?: string;
|
||||
severity: 'warning' | 'danger';
|
||||
requiresConfirmation: boolean;
|
||||
confirmationText?: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
// Security Settings State Management
|
||||
export interface SecuritySettingsState {
|
||||
settings: SecuritySettings | null;
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
errors: Record<string, string>;
|
||||
hasChanges: boolean;
|
||||
validationStatus: 'valid' | 'invalid' | 'pending';
|
||||
lastSaved: string | null;
|
||||
}
|
||||
|
||||
// API Response Types
|
||||
export interface SecuritySettingsResponse {
|
||||
settings: SecuritySettings;
|
||||
updated_at: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface SecurityAuditResponse {
|
||||
audit_entries: AuditEntry[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface SecurityEventsResponse {
|
||||
events: SecurityEvent[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
// Validation Rules
|
||||
export interface ValidationRule {
|
||||
pattern?: RegExp;
|
||||
min?: number;
|
||||
max?: number;
|
||||
required?: boolean;
|
||||
custom?: (value: any) => string | null;
|
||||
}
|
||||
|
||||
export interface SecurityValidationRules {
|
||||
[key: string]: ValidationRule;
|
||||
}
|
||||
|
||||
// WebSocket Types for Real-time Updates
|
||||
export interface SecurityWebSocketMessage {
|
||||
type: 'security_event' | 'setting_changed' | 'status_updated';
|
||||
data: any;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Export and Import Types
|
||||
export interface SecurityExport {
|
||||
timestamp: string;
|
||||
version: string;
|
||||
settings: SecuritySettings;
|
||||
audit_trail: AuditEntry[];
|
||||
metadata: {
|
||||
exported_by: string;
|
||||
export_reason: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Machine Binding Detail Types
|
||||
export interface MachineFingerprint {
|
||||
hardware_id: string;
|
||||
bios_uuid: string;
|
||||
mac_addresses: string[];
|
||||
cpu_id: string;
|
||||
disk_serials: string[];
|
||||
hostname: string;
|
||||
os_info: {
|
||||
platform: string;
|
||||
version: string;
|
||||
architecture: string;
|
||||
};
|
||||
generated_at: string;
|
||||
fingerprint_hash: string;
|
||||
}
|
||||
|
||||
// Key Rotation Types
|
||||
export interface KeyRotationRequest {
|
||||
reason: 'scheduled' | 'compromise' | 'manual';
|
||||
grace_period_days?: number;
|
||||
immediate?: boolean;
|
||||
}
|
||||
|
||||
export interface KeyRotationResponse {
|
||||
new_key_id: string;
|
||||
old_key_id: string;
|
||||
grace_period_ends: string;
|
||||
rotation_complete: boolean;
|
||||
affected_agents: number;
|
||||
}
|
||||
Reference in New Issue
Block a user