Files
claudetools/projects/msp-tools/guru-rmm/server/src/api/commands.rs
azcomputerguru 48a177c61a fix(security): Implement Phase 1 critical security fixes
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>
2026-01-20 21:16:24 -07:00

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))
}