Files
Redflag/aggregator-web/src/lib/api.ts

879 lines
27 KiB
TypeScript

import axios, { AxiosResponse } from 'axios';
import {
Agent,
UpdatePackage,
AgentUpdatePackage,
DashboardStats,
AgentListResponse,
UpdateListResponse,
UpdateApprovalRequest,
ScanRequest,
ListQueryParams,
ApiError,
DockerContainer,
DockerImage,
DockerContainerListResponse,
DockerStats,
DockerUpdateRequest,
BulkDockerUpdateRequest,
RegistrationToken,
CreateRegistrationTokenRequest,
RegistrationTokenStats,
RateLimitConfig,
RateLimitStats,
RateLimitUsage,
RateLimitSummary,
AgentSubsystem,
SubsystemConfig,
SubsystemStats
} from '@/types';
// Base URL for API - use nginx proxy
export const API_BASE_URL = '/api/v1';
// Create axios instance
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor to handle errors
api.interceptors.response.use(
(response: AxiosResponse) => response,
async (error) => {
if (error.response?.status === 401) {
const { useAuthStore } = await import('./store');
useAuthStore.getState().logout();
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// API endpoints
export const agentApi = {
// Get all agents
getAgents: async (params?: ListQueryParams): Promise<AgentListResponse> => {
const response = await api.get('/agents', { params });
return response.data;
},
// Get single agent
getAgent: async (id: string): Promise<Agent> => {
const response = await api.get(`/agents/${id}`);
return response.data;
},
// Trigger scan on agents
triggerScan: async (request: ScanRequest): Promise<void> => {
await api.post('/agents/scan', request);
},
// Trigger scan on single agent
scanAgent: async (id: string): Promise<void> => {
await api.post(`/agents/${id}/scan`);
},
// Trigger heartbeat toggle on single agent
toggleHeartbeat: async (id: string, enabled: boolean, durationMinutes: number = 10): Promise<{ message: string; command_id: string; enabled: boolean }> => {
const response = await api.post(`/agents/${id}/heartbeat`, {
enabled: enabled,
duration_minutes: durationMinutes,
});
return response.data;
},
// Get heartbeat status for single agent
getHeartbeatStatus: async (id: string): Promise<{ enabled: boolean; until: string | null; active: boolean; duration_minutes: number }> => {
const response = await api.get(`/agents/${id}/heartbeat`);
return response.data;
},
// Trigger agent reboot
rebootAgent: async (id: string, delayMinutes: number = 1, message?: string): Promise<void> => {
await api.post(`/agents/${id}/reboot`, {
delay_minutes: delayMinutes,
message: message || 'System reboot requested by RedFlag'
});
},
// Unregister/remove agent
unregisterAgent: async (id: string): Promise<void> => {
await api.delete(`/agents/${id}`);
},
// Subsystem Management
getSubsystems: async (agentId: string): Promise<AgentSubsystem[]> => {
const response = await api.get(`/agents/${agentId}/subsystems`);
return response.data;
},
getSubsystem: async (agentId: string, subsystem: string): Promise<AgentSubsystem> => {
const response = await api.get(`/agents/${agentId}/subsystems/${subsystem}`);
return response.data;
},
updateSubsystem: async (agentId: string, subsystem: string, config: SubsystemConfig): Promise<{ message: string }> => {
const response = await api.patch(`/agents/${agentId}/subsystems/${subsystem}`, config);
return response.data;
},
enableSubsystem: async (agentId: string, subsystem: string): Promise<{ message: string }> => {
const response = await api.post(`/agents/${agentId}/subsystems/${subsystem}/enable`);
return response.data;
},
disableSubsystem: async (agentId: string, subsystem: string): Promise<{ message: string }> => {
const response = await api.post(`/agents/${agentId}/subsystems/${subsystem}/disable`);
return response.data;
},
triggerSubsystem: async (agentId: string, subsystem: string): Promise<{ message: string; command_id: string }> => {
const response = await api.post(`/agents/${agentId}/subsystems/${subsystem}/trigger`);
return response.data;
},
getSubsystemStats: async (agentId: string, subsystem: string): Promise<SubsystemStats> => {
const response = await api.get(`/agents/${agentId}/subsystems/${subsystem}/stats`);
return response.data;
},
setSubsystemAutoRun: async (agentId: string, subsystem: string, autoRun: boolean): Promise<{ message: string }> => {
const response = await api.post(`/agents/${agentId}/subsystems/${subsystem}/auto-run`, { auto_run: autoRun });
return response.data;
},
setSubsystemInterval: async (agentId: string, subsystem: string, intervalMinutes: number): Promise<{ message: string }> => {
const response = await api.post(`/agents/${agentId}/subsystems/${subsystem}/interval`, { interval_minutes: intervalMinutes });
return response.data;
},
// Update single agent
updateAgent: async (agentId: string, updateData: {
version: string;
platform: string;
scheduled?: string;
}): Promise<{ message: string; update_id: string; download_url: string; signature: string; checksum: string; file_size: number; estimated_time: number }> => {
const response = await api.post(`/agents/${agentId}/update`, updateData);
return response.data;
},
// Check if update available for agent
checkForUpdateAvailable: async (agentId: string): Promise<{ hasUpdate: boolean; currentVersion: string; latestVersion?: string; reason?: string; platform?: string }> => {
const response = await api.get(`/agents/${agentId}/updates/available`);
return response.data;
},
// Get update status for agent
getUpdateStatus: async (agentId: string): Promise<{ status: string; progress?: number; error?: string }> => {
const response = await api.get(`/agents/${agentId}/updates/status`);
return response.data;
},
// Generate update nonce for agent (new security feature)
generateUpdateNonce: async (agentId: string, targetVersion: string): Promise<{ agent_id: string; target_version: string; update_nonce: string; expires_at: number; expires_in_seconds: number }> => {
const response = await api.post(`/agents/${agentId}/update-nonce?target_version=${targetVersion}`);
return response.data;
},
// Get agent metrics
getAgentMetrics: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/metrics`);
return response.data;
},
// Get agent storage metrics
getAgentStorageMetrics: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/metrics/storage`);
return response.data;
},
// Get agent system metrics
getAgentSystemMetrics: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/metrics/system`);
return response.data;
},
// Get agent Docker images
getAgentDockerImages: async (agentId: string, params?: any): Promise<any> => {
const response = await api.get(`/agents/${agentId}/docker-images`, { params });
return response.data;
},
// Get agent Docker info
getAgentDockerInfo: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/docker-info`);
return response.data;
},
// Update multiple agents (bulk)
updateMultipleAgents: async (updateData: {
agent_ids: string[];
version: string;
platform: string;
scheduled?: string;
}): Promise<{ message: string; updated: Array<{ agent_id: string; hostname: string; update_id: string; status: string }>; failed: string[]; total_agents: number; package_info: any }> => {
const response = await api.post('/agents/bulk-update', updateData);
return response.data;
},
};
export const updateApi = {
// Get all updates
getUpdates: async (params?: ListQueryParams): Promise<UpdateListResponse> => {
const response = await api.get('/updates', { params });
return response.data;
},
// Get single update
getUpdate: async (id: string): Promise<UpdatePackage> => {
const response = await api.get(`/updates/${id}`);
return response.data;
},
// Approve updates
approveUpdates: async (request: UpdateApprovalRequest): Promise<void> => {
await api.post('/updates/approve', request);
},
// Approve single update
approveUpdate: async (id: string, scheduledAt?: string): Promise<void> => {
await api.post(`/updates/${id}/approve`, { scheduled_at: scheduledAt });
},
// Approve multiple updates
approveMultiple: async (updateIds: string[]): Promise<void> => {
await api.post('/updates/approve', { update_ids: updateIds });
},
// Reject/cancel update
rejectUpdate: async (id: string): Promise<void> => {
await api.post(`/updates/${id}/reject`);
},
// Install update immediately
installUpdate: async (id: string): Promise<void> => {
await api.post(`/updates/${id}/install`);
},
// Get update logs
getUpdateLogs: async (id: string, limit?: number): Promise<{ logs: any[]; count: number }> => {
const response = await api.get(`/updates/${id}/logs`, {
params: limit ? { limit } : undefined
});
return response.data;
},
// Retry a failed, timed_out, or cancelled command
retryCommand: async (commandId: string): Promise<{ message: string; command_id: string; new_id: string }> => {
const response = await api.post(`/commands/${commandId}/retry`);
return response.data;
},
// Cancel a pending or sent command
cancelCommand: async (commandId: string): Promise<{ message: string }> => {
const response = await api.post(`/commands/${commandId}/cancel`);
return response.data;
},
// Get active commands for live command control
getActiveCommands: async (): Promise<{ commands: any[]; count: number }> => {
const response = await api.get('/commands/active');
return response.data;
},
// Get recent commands for retry functionality
getRecentCommands: async (limit?: number): Promise<{ commands: any[]; count: number; limit: number }> => {
const response = await api.get('/commands/recent', {
params: limit ? { limit } : undefined
});
return response.data;
},
// Clear failed commands with filtering options
clearFailedCommands: async (options?: {
olderThanDays?: number;
onlyRetried?: boolean;
allFailed?: boolean;
}): Promise<{ message: string; count: number; cheeky_warning?: string }> => {
const params = new URLSearchParams();
if (options?.olderThanDays !== undefined) {
params.append('older_than_days', options.olderThanDays.toString());
}
if (options?.onlyRetried) {
params.append('only_retried', 'true');
}
if (options?.allFailed) {
params.append('all_failed', 'true');
}
const response = await api.delete(`/commands/failed${params.toString() ? '?' + params.toString() : ''}`);
return response.data;
},
// Get available update packages
getPackages: async (params?: {
version?: string;
platform?: string;
limit?: number;
offset?: number;
}): Promise<{ packages: AgentUpdatePackage[]; total: number; limit: number; offset: number }> => {
const response = await api.get('/updates/packages', { params });
return response.data;
},
// Sign new update package
signPackage: async (packageData: {
version: string;
platform: string;
architecture: string;
binary_path: string;
}): Promise<{ message: string; package: UpdatePackage }> => {
const response = await api.post('/updates/packages/sign', packageData);
return response.data;
},
};
export const statsApi = {
// Get dashboard statistics
getDashboardStats: async (): Promise<DashboardStats> => {
const response = await api.get('/stats/summary');
return response.data;
},
};
export const logApi = {
// Get all logs with filtering for universal log view
getAllLogs: async (params?: {
page?: number;
page_size?: number;
agent_id?: string;
action?: string;
result?: string;
since?: string;
}): Promise<{ logs: any[]; total: number; page: number; page_size: number }> => {
const response = await api.get('/logs', { params });
return response.data;
},
// Get active operations for live status view
getActiveOperations: async (): Promise<{ operations: any[]; count: number }> => {
const response = await api.get('/logs/active');
return response.data;
},
// Get active commands for live command control
getActiveCommands: async (): Promise<{ commands: any[]; count: number }> => {
const response = await api.get('/commands/active');
return response.data;
},
// Get recent commands for retry functionality
getRecentCommands: async (limit?: number): Promise<{ commands: any[]; count: number; limit: number }> => {
const response = await api.get('/commands/recent', {
params: limit ? { limit } : undefined
});
return response.data;
},
};
export const authApi = {
// Login with username and password
login: async (credentials: { username: string; password: string }): Promise<{ token: string; user: any }> => {
const response = await api.post('/auth/login', credentials);
return response.data;
},
// Verify token
verifyToken: async (): Promise<{ valid: boolean }> => {
const response = await api.get('/auth/verify');
return response.data;
},
// Logout
logout: async (): Promise<void> => {
await api.post('/auth/logout');
},
};
// Setup API for server configuration (uses nginx proxy)
const setupApiInstance = axios.create({
baseURL: '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
export const setupApi = {
// Check server health and status
checkHealth: async (): Promise<{ status: string }> => {
const response = await setupApiInstance.get('/health');
return response.data;
},
// Submit server configuration
configure: async (config: {
adminUser: string;
adminPassword: string;
dbHost: string;
dbPort: string;
dbName: string;
dbUser: string;
dbPassword: string;
serverHost: string;
serverPort: string;
maxSeats: string;
}): Promise<{ message: string; jwtSecret?: string; envContent?: string; manualRestartRequired?: boolean; manualRestartCommand?: string; configFilePath?: string }> => {
const response = await setupApiInstance.post('/setup/configure', config);
return response.data;
},
};
// Utility functions
export const createQueryString = (params: Record<string, any>): string => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
if (Array.isArray(value)) {
value.forEach(v => searchParams.append(key, v));
} else {
searchParams.append(key, value.toString());
}
}
});
return searchParams.toString();
};
// Error handling utility
export const handleApiError = (error: any): ApiError => {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const data = error.response?.data;
if (status === 401) {
return {
message: 'Authentication required. Please log in.',
code: 'UNAUTHORIZED',
};
}
if (status === 403) {
return {
message: 'Access denied. You do not have permission to perform this action.',
code: 'FORBIDDEN',
};
}
if (status === 404) {
return {
message: 'The requested resource was not found.',
code: 'NOT_FOUND',
};
}
if (status === 429) {
return {
message: 'Too many requests. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
};
}
if (status && status >= 500) {
return {
message: 'Server error. Please try again later.',
code: 'SERVER_ERROR',
};
}
return {
message: data?.message || error.message || 'An error occurred',
code: data?.code || 'UNKNOWN_ERROR',
details: data?.details,
};
}
return {
message: error.message || 'An unexpected error occurred',
code: 'UNKNOWN_ERROR',
};
};
// Docker-specific API endpoints
export const dockerApi = {
// Get all Docker containers and images across all agents
getContainers: async (params?: {
page?: number;
page_size?: number;
agent?: string;
status?: string;
search?: string;
}): Promise<DockerContainerListResponse> => {
const response = await api.get('/docker/containers', { params });
return response.data;
},
// Get Docker containers for a specific agent
getAgentContainers: async (agentId: string, params?: {
page?: number;
page_size?: number;
status?: string;
search?: string;
}): Promise<DockerContainerListResponse> => {
const response = await api.get(`/agents/${agentId}/docker`, { params });
return response.data;
},
// Get Docker statistics
getStats: async (): Promise<DockerStats> => {
const response = await api.get('/docker/stats');
return response.data;
},
// Approve Docker image update
approveUpdate: async (containerId: string, imageId: string, scheduledAt?: string): Promise<void> => {
await api.post(`/docker/containers/${containerId}/images/${imageId}/approve`, {
scheduled_at: scheduledAt,
});
},
// Reject Docker image update
rejectUpdate: async (containerId: string, imageId: string): Promise<void> => {
await api.post(`/docker/containers/${containerId}/images/${imageId}/reject`);
},
// Install Docker image update
installUpdate: async (containerId: string, imageId: string): Promise<void> => {
await api.post(`/docker/containers/${containerId}/images/${imageId}/install`);
},
// Bulk approve Docker updates
bulkApproveUpdates: async (updates: Array<{ containerId: string; imageId: string }>, scheduledAt?: string): Promise<{ approved: number }> => {
const response = await api.post('/docker/updates/bulk-approve', {
updates,
scheduled_at: scheduledAt,
});
return response.data;
},
// Bulk reject Docker updates
bulkRejectUpdates: async (updates: Array<{ containerId: string; imageId: string }>): Promise<{ rejected: number }> => {
const response = await api.post('/docker/updates/bulk-reject', {
updates,
});
return response.data;
},
// Trigger Docker scan on agents
triggerScan: async (agentIds?: string[]): Promise<void> => {
await api.post('/docker/scan', { agent_ids: agentIds });
},
};
// Admin API endpoints
export const adminApi = {
// Registration Token Management
tokens: {
// Get all registration tokens
getTokens: async (params?: {
page?: number;
page_size?: number;
is_active?: boolean;
label?: string;
}): Promise<{ tokens: RegistrationToken[]; total: number; page: number; page_size: number }> => {
const response = await api.get('/admin/registration-tokens', { params });
return response.data;
},
// Get single registration token
getToken: async (id: string): Promise<RegistrationToken> => {
const response = await api.get(`/admin/registration-tokens/${id}`);
return response.data;
},
// Create new registration token
createToken: async (request: CreateRegistrationTokenRequest): Promise<RegistrationToken> => {
const response = await api.post('/admin/registration-tokens', request);
return response.data;
},
// Revoke registration token (soft delete)
revokeToken: async (id: string): Promise<void> => {
await api.delete(`/admin/registration-tokens/${id}`);
},
// Delete registration token (hard delete)
deleteToken: async (id: string): Promise<void> => {
await api.delete(`/admin/registration-tokens/delete/${id}`);
},
// Get registration token statistics
getStats: async (): Promise<RegistrationTokenStats> => {
const response = await api.get('/admin/registration-tokens/stats');
return response.data;
},
// Cleanup expired tokens
cleanup: async (): Promise<{ cleaned: number }> => {
const response = await api.post('/admin/registration-tokens/cleanup');
return response.data;
},
},
// Rate Limiting Management
rateLimits: {
// Get all rate limit configurations
getConfigs: async (): Promise<RateLimitConfig[]> => {
const response = await api.get('/admin/rate-limits');
// Backend returns { settings: {...}, updated_at: "..." }
// Transform settings object to array format expected by frontend
const settings = response.data.settings || {};
const configs: RateLimitConfig[] = Object.entries(settings).map(([endpoint, config]: [string, any]) => ({
...config,
endpoint,
updated_at: response.data.updated_at, // Preserve update timestamp
}));
return configs;
},
// Update rate limit configuration
updateConfig: async (endpoint: string, config: Partial<RateLimitConfig>): Promise<RateLimitConfig> => {
const response = await api.put(`/admin/rate-limits/${endpoint}`, config);
return response.data;
},
// Update all rate limit configurations
updateAllConfigs: async (configs: RateLimitConfig[]): Promise<RateLimitConfig[]> => {
const response = await api.put('/admin/rate-limits', { configs });
return response.data;
},
// Reset rate limit configurations to defaults
resetConfigs: async (): Promise<RateLimitConfig[]> => {
const response = await api.post('/admin/rate-limits/reset');
return response.data;
},
// Get rate limit statistics
getStats: async (): Promise<RateLimitStats[]> => {
const response = await api.get('/admin/rate-limits/stats');
return response.data;
},
// Get rate limit usage
getUsage: async (): Promise<RateLimitUsage[]> => {
const response = await api.get('/admin/rate-limits/usage');
return response.data;
},
// Get rate limit summary
getSummary: async (): Promise<RateLimitSummary> => {
const response = await api.get('/admin/rate-limits/summary');
return response.data;
},
// Cleanup expired rate limit data
cleanup: async (): Promise<{ cleaned: number }> => {
const response = await api.post('/admin/rate-limits/cleanup');
return response.data;
},
},
// System Administration
system: {
// Get system health and status
getHealth: async (): Promise<{
status: 'healthy' | 'degraded' | 'unhealthy';
uptime: number;
version: string;
database_status: 'connected' | 'disconnected';
active_agents: number;
active_tokens: number;
rate_limits_enabled: boolean;
}> => {
const response = await api.get('/admin/system/health');
return response.data;
},
// Get active agents
getActiveAgents: async (): Promise<{
agents: Array<{
id: string;
hostname: string;
last_seen: string;
status: string;
}>;
count: number;
}> => {
const response = await api.get('/admin/system/active-agents');
return response.data;
},
// Get system configuration
getConfig: async (): Promise<Record<string, any>> => {
const response = await api.get('/admin/system/config');
return response.data;
},
// Update system configuration
updateConfig: async (config: Record<string, any>): Promise<Record<string, any>> => {
const response = await api.put('/admin/system/config', config);
return response.data;
},
},
};
// Security API endpoints
export const securityApi = {
// Get comprehensive security overview
getOverview: async (): Promise<{
timestamp: string;
overall_status: 'healthy' | 'degraded' | 'unhealthy';
subsystems: {
ed25519_signing: { status: string; enabled: boolean };
nonce_validation: { status: string; enabled: boolean };
machine_binding: { status: string; enabled: boolean };
command_validation: { status: string; enabled: boolean };
};
alerts: string[];
recommendations: string[];
}> => {
const response = await api.get('/security/overview');
return response.data;
},
// Get Ed25519 signing service status
getSigningStatus: async (): Promise<{
status: string;
timestamp: string;
checks: {
service_initialized: boolean;
public_key_available: boolean;
signing_operational: boolean;
};
public_key_fingerprint?: string;
algorithm?: string;
}> => {
const response = await api.get('/security/signing');
return response.data;
},
// Get nonce validation status
getNonceStatus: async (): Promise<{
status: string;
timestamp: string;
checks: {
validation_enabled: boolean;
max_age_minutes: number;
recent_validations: number;
validation_failures: number;
};
details: {
nonce_format: string;
signature_algorithm: string;
replay_protection: string;
};
}> => {
const response = await api.get('/security/nonce');
return response.data;
},
// Get machine binding status
getMachineBindingStatus: async (): Promise<{
status: string;
timestamp: string;
checks: {
binding_enforced: boolean;
min_agent_version: string;
fingerprint_required: boolean;
recent_violations: number;
};
details: {
enforcement_method: string;
binding_scope: string;
violation_action: string;
};
}> => {
const response = await api.get('/security/machine-binding');
return response.data;
},
// Get command validation status
getCommandValidationStatus: async (): Promise<{
status: string;
timestamp: string;
metrics: {
total_pending_commands: number;
agents_with_pending: number;
commands_last_hour: number;
commands_last_24h: number;
};
checks: {
command_processing: string;
backpressure_active: boolean;
agent_responsive: string;
};
}> => {
const response = await api.get('/security/commands');
return response.data;
},
// Get detailed security metrics
getMetrics: async (): Promise<{
timestamp: string;
signing: {
public_key_fingerprint: string;
algorithm: string;
key_size: number;
configured: boolean;
};
nonce: {
max_age_seconds: number;
format: string;
};
machine_binding: {
min_version: string;
enforcement: string;
};
command_processing: {
backpressure_threshold: number;
rate_limit_per_second: number;
};
}> => {
const response = await api.get('/security/metrics');
return response.data;
},
};
// Storage Metrics API
export const storageMetricsApi = {
// Report storage metrics (agent only)
async reportStorageMetrics(agentID: string, data: any): Promise<void> {
await api.post(`/agents/${agentID}/storage-metrics`, data);
},
// Get storage metrics for an agent
async getStorageMetrics(agentID: string): Promise<any> {
const response = await api.get(`/agents/${agentID}/storage-metrics`);
return response.data;
},
};
export default api;