CORS: - Restrict CORS to DASHBOARD_URL environment variable - Default to production dashboard domain Authentication: - Add AuthUser requirement to all agent management endpoints - Add AuthUser requirement to all command endpoints - Add AuthUser requirement to all metrics endpoints - Add audit logging for command execution (user_id tracked) Agent Security: - Replace Unicode characters with ASCII markers [OK]/[ERROR]/[WARNING] - Add certificate pinning for update downloads (allowlist domains) - Fix insecure temp file creation (use /var/run/gururmm with 0700 perms) - Fix rollback script backgrounding (use setsid instead of literal &) Dashboard Security: - Move token storage from localStorage to sessionStorage - Add proper TypeScript types (remove 'any' from error handlers) - Centralize token management functions Legacy Agent: - Add -AllowInsecureTLS parameter (opt-in required) - Add Windows Event Log audit trail when insecure mode used - Update documentation with security warnings Closes: Phase 1 items in issue #1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
143 lines
4.2 KiB
Rust
143 lines
4.2 KiB
Rust
//! Commands API endpoints
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
Json,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
use crate::auth::AuthUser;
|
|
use crate::db::{self, Command};
|
|
use crate::ws::{CommandPayload, ServerMessage};
|
|
use crate::AppState;
|
|
|
|
/// Request to send a command to an agent
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SendCommandRequest {
|
|
/// Command type (shell, powershell, python, script)
|
|
pub command_type: String,
|
|
|
|
/// Command text to execute
|
|
pub command: String,
|
|
|
|
/// Timeout in seconds (optional, default 300)
|
|
pub timeout_seconds: Option<u64>,
|
|
|
|
/// Run as elevated/admin (optional, default false)
|
|
pub elevated: Option<bool>,
|
|
}
|
|
|
|
/// Response after sending a command
|
|
#[derive(Debug, Serialize)]
|
|
pub struct SendCommandResponse {
|
|
pub command_id: Uuid,
|
|
pub status: String,
|
|
pub message: String,
|
|
}
|
|
|
|
/// Query parameters for listing commands
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CommandsQuery {
|
|
pub limit: Option<i64>,
|
|
}
|
|
|
|
/// Send a command to an agent
|
|
/// Requires authentication. Logs the user who sent the command for audit trail.
|
|
pub async fn send_command(
|
|
State(state): State<AppState>,
|
|
user: AuthUser,
|
|
Path(agent_id): Path<Uuid>,
|
|
Json(req): Json<SendCommandRequest>,
|
|
) -> Result<Json<SendCommandResponse>, (StatusCode, String)> {
|
|
// Log the command being sent for audit trail
|
|
tracing::info!(
|
|
user_id = %user.user_id,
|
|
agent_id = %agent_id,
|
|
command_type = %req.command_type,
|
|
"Command sent by user"
|
|
);
|
|
|
|
// Verify agent exists
|
|
let _agent = db::get_agent_by_id(&state.db, agent_id)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
|
.ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?;
|
|
|
|
// Create command record with user ID for audit trail
|
|
let create = db::CreateCommand {
|
|
agent_id,
|
|
command_type: req.command_type.clone(),
|
|
command_text: req.command.clone(),
|
|
created_by: Some(user.user_id),
|
|
};
|
|
|
|
let command = db::create_command(&state.db, create)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
// Check if agent is connected
|
|
let agents = state.agents.read().await;
|
|
if agents.is_connected(&agent_id) {
|
|
// Send command via WebSocket
|
|
let cmd_msg = ServerMessage::Command(CommandPayload {
|
|
id: command.id,
|
|
command_type: req.command_type,
|
|
command: req.command,
|
|
timeout_seconds: req.timeout_seconds,
|
|
elevated: req.elevated.unwrap_or(false),
|
|
});
|
|
|
|
if agents.send_to(&agent_id, cmd_msg).await {
|
|
// Mark as running
|
|
let _ = db::mark_command_running(&state.db, command.id).await;
|
|
|
|
return Ok(Json(SendCommandResponse {
|
|
command_id: command.id,
|
|
status: "running".to_string(),
|
|
message: "Command sent to agent".to_string(),
|
|
}));
|
|
}
|
|
}
|
|
|
|
// Agent not connected or send failed - command is queued
|
|
Ok(Json(SendCommandResponse {
|
|
command_id: command.id,
|
|
status: "pending".to_string(),
|
|
message: "Agent is offline. Command queued for execution when agent reconnects."
|
|
.to_string(),
|
|
}))
|
|
}
|
|
|
|
/// List recent commands
|
|
/// Requires authentication.
|
|
pub async fn list_commands(
|
|
State(state): State<AppState>,
|
|
_user: AuthUser,
|
|
Query(query): Query<CommandsQuery>,
|
|
) -> Result<Json<Vec<Command>>, (StatusCode, String)> {
|
|
let limit = query.limit.unwrap_or(50).min(500);
|
|
|
|
let commands = db::get_recent_commands(&state.db, limit)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
Ok(Json(commands))
|
|
}
|
|
|
|
/// Get a specific command by ID
|
|
/// Requires authentication.
|
|
pub async fn get_command(
|
|
State(state): State<AppState>,
|
|
_user: AuthUser,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<Command>, (StatusCode, String)> {
|
|
let command = db::get_command_by_id(&state.db, id)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
|
.ok_or((StatusCode::NOT_FOUND, "Command not found".to_string()))?;
|
|
|
|
Ok(Json(command))
|
|
}
|