Files
Redflag/aggregator-web/src/pages/History.tsx
jpetree331 f97d4845af feat(security): A-1 Ed25519 key rotation + A-2 replay attack fixes
Complete RedFlag codebase with two major security audit implementations.

== A-1: Ed25519 Key Rotation Support ==

Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management

Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing

== A-2: Replay Attack Fixes (F-1 through F-7) ==

F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH     - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH     - Migration 026: expires_at column with partial index
F-6 HIGH     - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH     - Agent-side executedIDs dedup map with cleanup
F-4 HIGH     - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt

Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.

All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:25:47 -04:00

93 lines
3.1 KiB
TypeScript

import React, { useState } from 'react';
import {
History,
Search,
RefreshCw,
} from 'lucide-react';
import ChatTimeline from '@/components/ChatTimeline';
import { useQuery } from '@tanstack/react-query';
import { logApi } from '@/lib/api';
import toast from 'react-hot-toast';
import { cn } from '@/lib/utils';
const HistoryPage: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
// Debounce search query
React.useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 300);
return () => {
clearTimeout(timer);
};
}, [searchQuery]);
const { data: historyData, isLoading, refetch, isFetching } = useQuery({
queryKey: ['history', { search: debouncedSearchQuery }],
queryFn: async () => {
try {
const params: any = {
page: 1,
page_size: 50,
};
if (debouncedSearchQuery) {
params.search = debouncedSearchQuery;
}
const response = await logApi.getAllLogs(params);
return response;
} catch (error) {
console.error('Failed to fetch history:', error);
toast.error('Failed to fetch history');
return { logs: [], total: 0, page: 1, page_size: 50 };
}
},
refetchInterval: 30000,
});
return (
<div className="px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-3">
<History className="h-8 w-8 text-indigo-600" />
<h1 className="text-2xl font-bold text-gray-900">History & Audit Log</h1>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search events..."
className="pl-10 pr-4 py-2 w-64 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<button
onClick={() => refetch()}
disabled={isFetching}
className="flex items-center space-x-2 px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm font-medium transition-colors disabled:opacity-50"
>
<RefreshCw className={cn("h-4 w-4", isFetching && "animate-spin")} />
<span>Refresh</span>
</button>
</div>
</div>
<p className="text-gray-600">
Complete chronological timeline of all system activities across all agents
</p>
</div>
{/* Timeline */}
<ChatTimeline isScopedView={false} externalSearch={debouncedSearchQuery} />
</div>
);
};
export default HistoryPage;