WIP: Save current state - security subsystems, migrations, logging

This commit is contained in:
Fimeg
2025-12-16 14:19:59 -05:00
parent f792ab23c7
commit f7c8d23c5d
89 changed files with 8884 additions and 1394 deletions

View File

@@ -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>
);
}

View File

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

View File

@@ -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>
);
}

View File

@@ -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;

View 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;

View 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;

View 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;

View 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,
};
};

View 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';

View File

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

View 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;

View File

@@ -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">

View File

@@ -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) => {

View File

@@ -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`;
}
}
};

View 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;
}