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>
121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'react-hot-toast';
|
|
import { adminApi } from '@/lib/api';
|
|
import {
|
|
RegistrationToken,
|
|
CreateRegistrationTokenRequest,
|
|
RegistrationTokenStats
|
|
} from '@/types';
|
|
|
|
// Query keys
|
|
export const registrationTokenKeys = {
|
|
all: ['registration-tokens'] as const,
|
|
lists: () => [...registrationTokenKeys.all, 'list'] as const,
|
|
list: (params: any) => [...registrationTokenKeys.lists(), params] as const,
|
|
details: () => [...registrationTokenKeys.all, 'detail'] as const,
|
|
detail: (id: string) => [...registrationTokenKeys.details(), id] as const,
|
|
stats: () => [...registrationTokenKeys.all, 'stats'] as const,
|
|
};
|
|
|
|
// Hooks
|
|
export const useRegistrationTokens = (params?: {
|
|
page?: number;
|
|
page_size?: number;
|
|
is_active?: boolean;
|
|
label?: string;
|
|
}) => {
|
|
return useQuery({
|
|
queryKey: registrationTokenKeys.list(params),
|
|
queryFn: () => adminApi.tokens.getTokens(params),
|
|
staleTime: 1000 * 60, // 1 minute
|
|
});
|
|
};
|
|
|
|
export const useRegistrationToken = (id: string) => {
|
|
return useQuery({
|
|
queryKey: registrationTokenKeys.detail(id),
|
|
queryFn: () => adminApi.tokens.getToken(id),
|
|
enabled: !!id,
|
|
staleTime: 1000 * 60, // 1 minute
|
|
});
|
|
};
|
|
|
|
export const useRegistrationTokenStats = () => {
|
|
return useQuery({
|
|
queryKey: registrationTokenKeys.stats(),
|
|
queryFn: () => adminApi.tokens.getStats(),
|
|
staleTime: 1000 * 60, // 1 minute
|
|
refetchInterval: 1000 * 60 * 5, // Refresh every 5 minutes
|
|
});
|
|
};
|
|
|
|
export const useCreateRegistrationToken = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: CreateRegistrationTokenRequest) =>
|
|
adminApi.tokens.createToken(data),
|
|
onSuccess: (newToken) => {
|
|
toast.success(`Registration token "${newToken.label}" created successfully`);
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.lists() });
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.stats() });
|
|
},
|
|
onError: (error: any) => {
|
|
console.error('Failed to create registration token:', error);
|
|
toast.error(error.response?.data?.message || 'Failed to create registration token');
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useRevokeRegistrationToken = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) => adminApi.tokens.revokeToken(id),
|
|
onSuccess: (_, tokenId) => {
|
|
toast.success('Registration token revoked successfully');
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.lists() });
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.detail(tokenId) });
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.stats() });
|
|
},
|
|
onError: (error: any) => {
|
|
console.error('Failed to revoke registration token:', error);
|
|
toast.error(error.response?.data?.message || 'Failed to revoke registration token');
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useDeleteRegistrationToken = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) => adminApi.tokens.deleteToken(id),
|
|
onSuccess: (_, tokenId) => {
|
|
toast.success('Registration token deleted successfully');
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.lists() });
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.detail(tokenId) });
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.stats() });
|
|
},
|
|
onError: (error: any) => {
|
|
console.error('Failed to delete registration token:', error);
|
|
toast.error(error.response?.data?.message || 'Failed to delete registration token');
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useCleanupRegistrationTokens = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: () => adminApi.tokens.cleanup(),
|
|
onSuccess: (result) => {
|
|
toast.success(`Cleaned up ${result.cleaned} expired tokens`);
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.lists() });
|
|
queryClient.invalidateQueries({ queryKey: registrationTokenKeys.stats() });
|
|
},
|
|
onError: (error: any) => {
|
|
console.error('Failed to cleanup registration tokens:', error);
|
|
toast.error(error.response?.data?.message || 'Failed to cleanup registration tokens');
|
|
},
|
|
});
|
|
}; |