feat: bump to v0.1.23 with security metrics and UI improvements

- Bump agent and server versions to 0.1.23
- Implement security metrics collection (bound agents, command processing, version compliance)
- Add dismiss button for timed out commands in agent status
- Add config sync endpoint for server->agent configuration updates
- Add ignored updates workflow in AgentUpdatesEnhanced (approve/reject workflow)
- Swap AgentScanners layout (subsystems top, security bottom)
- Replace placeholder security data with database metrics
- Add backpressure detection based on pending command ratios
This commit is contained in:
Fimeg
2025-11-04 09:41:27 -05:00
parent 38894f64d3
commit 95f70bd9bb
12 changed files with 511 additions and 244 deletions

View File

@@ -228,209 +228,6 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
return (
<div className="space-y-6">
{/* Compact Summary */}
<div className="card">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-6">
<div>
<span className="text-gray-600">Enabled:</span>
<span className="ml-2 font-medium text-green-600">{enabledCount}/{subsystems.length}</span>
</div>
<div>
<span className="text-gray-600">Auto-Run:</span>
<span className="ml-2 font-medium text-blue-600">{autoRunCount}</span>
</div>
</div>
<button
onClick={() => refetch()}
disabled={isLoading}
className="flex items-center space-x-1 px-3 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded transition-colors"
>
<RefreshCw className={cn('h-3 w-3', isLoading && 'animate-spin')} />
<span>Refresh</span>
</button>
</div>
</div>
{/* Security Health */}
<div className="bg-white/90 backdrop-blur-md rounded-lg border border-gray-200/50 shadow-sm">
<div className="flex items-center justify-between p-4 border-b border-gray-200/50">
<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>
</div>
<button
onClick={() => queryClient.invalidateQueries({ queryKey: ['security-overview'] })}
disabled={securityLoading}
className="flex items-center space-x-1 px-3 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-50/50 rounded-md transition-colors"
>
<RefreshCw className={cn('h-3 w-3', securityLoading && 'animate-spin')} />
<span>Refresh</span>
</button>
</div>
{securityLoading ? (
<div className="flex items-center justify-center py-8">
<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={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()}
</div>
</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':
return `Commands processed: ${Math.floor(Math.random() * 50)}. Failures: 0 (last 24h).`;
case 'ed25519_signing':
return `Fingerprint: ${Math.random().toString(36).substring(2, 18)}. Algorithm: Ed25519. Valid since: ${new Date().toLocaleDateString()}.`;
case 'machine_binding':
return `Bound agents: ${Math.floor(Math.random() * 100)}. Violations (24h): 0. Enforcement: Hardware fingerprint.`;
case 'nonce_validation':
return `Max age: 5min. Replays blocked (24h): 0. Format: UUID:Timestamp.`;
default:
return `Status: ${status}. Enabled: ${subsystem.enabled}`;
}
};
const getEnhancedSubtitle = (subsystemType: string, status: string) => {
switch (subsystemType) {
case 'command_validation':
return 'Operational - 0 failures';
case 'ed25519_signing':
return status === 'healthy' ? 'Enabled - Key valid' : 'Disabled - Invalid key';
case 'machine_binding':
return status === 'healthy' ? 'Enforced - 0 violations' : 'Violations detected';
case 'nonce_validation':
return 'Enabled - 5min window';
default:
return `${subsystem.enabled ? 'Enabled' : 'Disabled'} - ${status}`;
}
};
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">
{getSecurityIcon(key)}
</div>
</div>
<div>
<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>
</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 === '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 === 'degraded' && <AlertCircle className="w-3 h-3" />}
{subsystem.status === 'unhealthy' && <XCircle className="w-3 h-3" />}
{subsystem.status.toUpperCase()}
</div>
</div>
);
})}
</div>
</div>
{/* Security Alerts - Frosted Glass Style */}
{(securityOverview.alerts.length > 0 || securityOverview.recommendations.length > 0) && (
<div className="p-4 space-y-3">
{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>
))}
</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>
))}
</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()}
</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>
)}
</div>
{/* Subsystem Configuration Table */}
<div className="card">
<div className="flex items-center justify-between mb-3">
@@ -583,6 +380,229 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
<div className="text-xs text-gray-500">
Subsystems report specific metrics to the server on scheduled intervals. Enable auto-run to schedule automatic scans, or trigger manual scans as needed.
</div>
{/* Compact Summary */}
<div className="card">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-6">
<div>
<span className="text-gray-600">Enabled:</span>
<span className="ml-2 font-medium text-green-600">{enabledCount}/{subsystems.length}</span>
</div>
<div>
<span className="text-gray-600">Auto-Run:</span>
<span className="ml-2 font-medium text-blue-600">{autoRunCount}</span>
</div>
</div>
<button
onClick={() => refetch()}
disabled={isLoading}
className="flex items-center space-x-1 px-3 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded transition-colors"
>
<RefreshCw className={cn('h-3 w-3', isLoading && 'animate-spin')} />
<span>Refresh</span>
</button>
</div>
</div>
{/* Security Health */}
<div className="bg-white/90 backdrop-blur-md rounded-lg border border-gray-200/50 shadow-sm">
<div className="flex items-center justify-between p-4 border-b border-gray-200/50">
<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>
</div>
<button
onClick={() => queryClient.invalidateQueries({ queryKey: ['security-overview'] })}
disabled={securityLoading}
className="flex items-center space-x-1 px-3 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-50/50 rounded-md transition-colors"
>
<RefreshCw className={cn('h-3 w-3', securityLoading && 'animate-spin')} />
<span>Refresh</span>
</button>
</div>
{securityLoading ? (
<div className="flex items-center justify-center py-8">
<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={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()}
</div>
</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}`;
}
};
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}`;
}
};
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">
{getSecurityIcon(key)}
</div>
</div>
<div>
<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>
</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 === '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 === 'degraded' && <AlertCircle className="w-3 h-3" />}
{subsystem.status === 'unhealthy' && <XCircle className="w-3 h-3" />}
{subsystem.status.toUpperCase()}
</div>
</div>
);
})}
</div>
</div>
{/* Security Alerts - Frosted Glass Style */}
{(securityOverview.alerts.length > 0 || securityOverview.recommendations.length > 0) && (
<div className="p-4 space-y-3">
{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>
))}
</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>
))}
</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()}
</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>
)}
</div>
</div>
);
}

View File

@@ -40,7 +40,7 @@ interface LogResponse {
result: string;
}
type StatusTab = 'pending' | 'approved' | 'installing' | 'installed';
type StatusTab = 'pending' | 'approved' | 'installing' | 'installed' | 'ignored';
export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
const [activeStatus, setActiveStatus] = useState<StatusTab>('pending');
@@ -123,6 +123,21 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
},
});
const rejectMutation = useMutation({
mutationFn: async (updateId: string) => {
const response = await updateApi.rejectUpdate(updateId);
return response;
},
onSuccess: () => {
toast.success('Update rejected');
refetch();
queryClient.invalidateQueries({ queryKey: ['agent-updates'] });
},
onError: (error: any) => {
toast.error(`Failed to reject: ${error.message || 'Unknown error'}`);
},
});
const getLogsMutation = useMutation({
mutationFn: async (commandId: string) => {
setIsLoadingLogs(true);
@@ -182,6 +197,10 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
installMutation.mutate(updateId);
};
const handleReject = async (updateId: string) => {
rejectMutation.mutate(updateId);
};
const handleBulkApprove = async () => {
if (selectedUpdates.length === 0) {
toast.error('Select at least one update');
@@ -241,6 +260,7 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
{ key: 'approved', label: 'Approved' },
{ key: 'installing', label: 'Installing' },
{ key: 'installed', label: 'Installed' },
{ key: 'ignored', label: 'Ignored' },
].map((tab) => (
<button
key={tab.key}
@@ -387,12 +407,20 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
<div className="flex items-center space-x-2 flex-shrink-0">
{activeStatus === 'pending' && (
<button
onClick={(e) => { e.stopPropagation(); handleApprove(update.id); }}
className="text-xs text-gray-600 hover:text-gray-900 px-2 py-1"
>
Approve
</button>
<>
<button
onClick={(e) => { e.stopPropagation(); handleApprove(update.id); }}
className="text-xs text-gray-600 hover:text-gray-900 px-2 py-1"
>
Approve
</button>
<button
onClick={(e) => { e.stopPropagation(); handleReject(update.id); }}
className="text-xs text-red-600 hover:text-red-800 px-2 py-1"
>
Reject
</button>
</>
)}
{activeStatus === 'approved' && (
<button
@@ -402,6 +430,11 @@ export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) {
Install
</button>
)}
{activeStatus === 'ignored' && (
<span className="text-xs text-gray-500 px-2 py-1">
Rejected
</span>
)}
{update.recent_command_id && (
<button
onClick={(e) => { e.stopPropagation(); handleViewLogs(update); }}

View File

@@ -793,6 +793,15 @@ const Agents: React.FC = () => {
Cancel
</button>
)}
{command.status === 'timed_out' && (
<button
onClick={() => handleCancelCommand(command.id)}
disabled={cancelCommandMutation.isPending}
className="text-xs text-gray-600 hover:text-gray-800 disabled:opacity-50"
>
Dismiss
</button>
)}
</div>
</div>
</div>