Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,8 @@
**Vulnerability:** The `CronModule` in `src/modules/cron.rs` was vulnerable to CRLF injection because it did not validate that the `name` (and other parameters like `job`) of a cron job were free of newline characters. This allowed an attacker to inject arbitrary lines into the crontab file, potentially creating malicious cron jobs running as the target user.
**Learning:** When generating configuration files that are line-based (like crontabs), always validate user input for newline characters to prevent injection of new entries.
**Prevention:** Implement strict validation for all parameters that are written to line-based configuration files. Use helper functions like `validate_no_newlines` to enforce this constraint consistently.

## 2025-06-03 - Windows Command Injection via % and ^
**Vulnerability:** The `validate_command_args` utility was too permissive for Windows environments. It allowed `%` (variable expansion) and `^` (shell escape). This enabled Information Disclosure (reading environment variables) and command obfuscation/filter bypass on Windows systems where commands are executed via `cmd.exe`.
**Learning:** Shell metacharacters vary significantly by platform. A validation logic that works for POSIX shells is insufficient for Windows `cmd.exe`, which has its own set of special characters (`%`, `^`).
**Prevention:** Explicitly block Windows-specific shell metacharacters (`%`, `^`) in validation routines intended to be cross-platform or Windows-compatible.
1 change: 0 additions & 1 deletion src/modules/cloud/aws/ec2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ pub enum InstanceState {
Rebooted,
}


impl InstanceState {
fn from_str(s: &str) -> ModuleResult<Self> {
match s.to_lowercase().as_str() {
Expand Down
6 changes: 1 addition & 5 deletions src/modules/cloud/aws/s3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,7 @@ pub enum ServerSideEncryption {

impl ServerSideEncryption {
fn from_str(s: &str) -> ModuleResult<Self> {
match s
.to_uppercase()
.replace(['-', ':'], "_")
.as_str()
{
match s.to_uppercase().replace(['-', ':'], "_").as_str() {
"AES256" | "AES_256" | "SSE_S3" => Ok(ServerSideEncryption::Aes256),
"AWS_KMS" | "AWSKMS" | "KMS" | "SSE_KMS" => Ok(ServerSideEncryption::AwsKms),
"SSE_C" | "CUSTOMER" | "CUSTOMER_PROVIDED" => {
Expand Down
1 change: 0 additions & 1 deletion src/modules/cloud/azure/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ pub enum VmState {
Restarted,
}


impl VmState {
fn from_str(s: &str) -> ModuleResult<Self> {
match s.to_lowercase().as_str() {
Expand Down
1 change: 0 additions & 1 deletion src/modules/cloud/gcp/compute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ pub enum InstanceState {
Reset,
}


impl InstanceState {
fn from_str(s: &str) -> ModuleResult<Self> {
match s.to_lowercase().as_str() {
Expand Down
4 changes: 2 additions & 2 deletions src/modules/cloud/kubernetes/secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
//! ```

use crate::modules::{
Diff, Module, ModuleClassification, ModuleContext, ModuleOutput, ModuleParams,
ModuleResult, ParallelizationHint, ParamExt,
Diff, Module, ModuleClassification, ModuleContext, ModuleOutput, ModuleParams, ModuleResult,
ParallelizationHint, ParamExt,
};
use serde_json::json;
use std::collections::BTreeMap;
Expand Down
4 changes: 2 additions & 2 deletions src/modules/docker/docker_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@

#[cfg(feature = "docker")]
use bollard::container::{
Config, CreateContainerOptions, RemoveContainerOptions,
RestartContainerOptions, StartContainerOptions, StopContainerOptions,
Config, CreateContainerOptions, RemoveContainerOptions, RestartContainerOptions,
StartContainerOptions, StopContainerOptions,
};
#[cfg(feature = "docker")]
use bollard::models::{
Expand Down
5 changes: 1 addition & 4 deletions src/modules/docker/docker_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
//! - `repository`: Registry repository for push

#[cfg(feature = "docker")]
use bollard::image::{
BuildImageOptions, CreateImageOptions, RemoveImageOptions, TagImageOptions,
};
use bollard::image::{BuildImageOptions, CreateImageOptions, RemoveImageOptions, TagImageOptions};
#[cfg(feature = "docker")]
use bollard::Docker;
#[cfg(feature = "docker")]
Expand Down Expand Up @@ -273,7 +271,6 @@ impl DockerImageModule {

/// Build image from Dockerfile
async fn build_image(docker: &Docker, config: &ImageConfig) -> ModuleResult<()> {

use tar::Builder;

let build_path = config.build.path.as_ref().ok_or_else(|| {
Expand Down
4 changes: 1 addition & 3 deletions src/modules/docker/docker_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
#[cfg(feature = "docker")]
use bollard::models::{Ipam, IpamConfig};
#[cfg(feature = "docker")]
use bollard::network::{
ConnectNetworkOptions, CreateNetworkOptions, DisconnectNetworkOptions,
};
use bollard::network::{ConnectNetworkOptions, CreateNetworkOptions, DisconnectNetworkOptions};
#[cfg(feature = "docker")]
use bollard::Docker;

Expand Down
15 changes: 6 additions & 9 deletions src/modules/get_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,9 @@ impl Module for GetUrlModule {
}
}

let bytes = rt
.block_on(async { response.bytes().await })
.map_err(|e| {
ModuleError::ExecutionFailed(format!("Failed to read response body: {}", e))
})?;
let bytes = rt.block_on(async { response.bytes().await }).map_err(|e| {
ModuleError::ExecutionFailed(format!("Failed to read response body: {}", e))
})?;

// Verify checksum if provided
if let Some(ref cksum) = checksum {
Expand Down Expand Up @@ -220,10 +218,9 @@ impl Module for GetUrlModule {
dest,
bytes.len()
));
output.data.insert(
"dest".to_string(),
serde_json::Value::String(dest.clone()),
);
output
.data
.insert("dest".to_string(), serde_json::Value::String(dest.clone()));
output
.data
.insert("url".to_string(), serde_json::Value::String(url));
Expand Down
68 changes: 18 additions & 50 deletions src/modules/hpc/dcgm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,7 @@ fn parse_dcgmi_diag(output: &str, level: u32) -> DcgmDiagResult {
if parts.len() >= 3 {
let test_name = parts[1].trim();
let result = parts[2].trim();
if test_name.is_empty()
|| test_name.starts_with('-')
|| test_name.starts_with('=')
{
if test_name.is_empty() || test_name.starts_with('-') || test_name.starts_with('=') {
continue;
}
if result.to_lowercase().contains("fail") {
Expand Down Expand Up @@ -250,9 +247,7 @@ impl Module for DcgmModule {
let health_watches = params.get_bool_or("health_watches", false);
let diag_level = params.get_u32("diag_level")?;
let exporter = params.get_bool_or("exporter", false);
let exporter_port = params
.get_u32("exporter_port")?
.unwrap_or(9400);
let exporter_port = params.get_u32("exporter_port")?.unwrap_or(9400);
let exporter_counters = params.get_string("exporter_counters")?;

validate_operation_mode(&operation_mode)?;
Expand All @@ -262,11 +257,8 @@ impl Module for DcgmModule {

// -- state=absent --
if state == "absent" {
let (installed, _, _) = run_cmd(
connection,
"command -v dcgmi >/dev/null 2>&1",
context,
)?;
let (installed, _, _) =
run_cmd(connection, "command -v dcgmi >/dev/null 2>&1", context)?;

if !installed {
return Ok(ModuleOutput::ok("DCGM is not installed"));
Expand All @@ -276,16 +268,8 @@ impl Module for DcgmModule {
return Ok(ModuleOutput::changed("Would remove DCGM packages"));
}

let _ = run_cmd(
connection,
"systemctl stop nvidia-dcgm.service",
context,
);
let _ = run_cmd(
connection,
"systemctl disable nvidia-dcgm.service",
context,
);
let _ = run_cmd(connection, "systemctl stop nvidia-dcgm.service", context);
let _ = run_cmd(connection, "systemctl disable nvidia-dcgm.service", context);

let remove_cmd = match os_family {
"rhel" => "dnf remove -y datacenter-gpu-manager dcgm-exporter",
Expand All @@ -299,11 +283,8 @@ impl Module for DcgmModule {
// -- state=present --

// Step 1: Install DCGM
let (dcgm_installed, _, _) = run_cmd(
connection,
"command -v dcgmi >/dev/null 2>&1",
context,
)?;
let (dcgm_installed, _, _) =
run_cmd(connection, "command -v dcgmi >/dev/null 2>&1", context)?;

if !dcgm_installed {
if context.check_mode {
Expand Down Expand Up @@ -355,11 +336,7 @@ impl Module for DcgmModule {
context,
)?;
if svc_active {
run_cmd_ok(
connection,
"systemctl stop nvidia-dcgm.service",
context,
)?;
run_cmd_ok(connection, "systemctl stop nvidia-dcgm.service", context)?;
changed = true;
changes.push("Stopped nvidia-dcgm.service for embedded mode".to_string());
}
Expand All @@ -386,11 +363,8 @@ impl Module for DcgmModule {
changes.push(format!("Would run DCGM diagnostics level {}", level));
None
} else {
let diag_stdout = run_cmd_ok(
connection,
&format!("dcgmi diag -r {}", level),
context,
)?;
let diag_stdout =
run_cmd_ok(connection, &format!("dcgmi diag -r {}", level), context)?;
let result = parse_dcgmi_diag(&diag_stdout, level);
if !result.passed {
changes.push(format!(
Expand Down Expand Up @@ -430,17 +404,11 @@ impl Module for DcgmModule {

// Configure exporter port via environment override
if !context.check_mode {
let env_line = format!(
"DCGM_EXPORTER_LISTEN=:{}",
exporter_port
);
let env_line = format!("DCGM_EXPORTER_LISTEN=:{}", exporter_port);
let env_file = "/etc/default/dcgm-exporter";
let mut env_content = env_line.clone();
if let Some(ref counters) = exporter_counters {
env_content.push_str(&format!(
"\nDCGM_EXPORTER_COLLECTORS={}",
counters
));
env_content.push_str(&format!("\nDCGM_EXPORTER_COLLECTORS={}", counters));
}
run_cmd_ok(
connection,
Expand Down Expand Up @@ -630,7 +598,10 @@ mod tests {

#[test]
fn test_detect_os_family() {
assert_eq!(detect_os_family("ID_LIKE=\"rhel centos fedora\""), Some("rhel"));
assert_eq!(
detect_os_family("ID_LIKE=\"rhel centos fedora\""),
Some("rhel")
);
assert_eq!(detect_os_family("ID=ubuntu\nVERSION=22.04"), Some("debian"));
assert_eq!(detect_os_family("ID=freebsd"), None);
}
Expand All @@ -655,9 +626,6 @@ mod tests {
};
let json = serde_json::to_value(&config).unwrap();
assert_eq!(json["port"], 9400);
assert_eq!(
json["counters_file"],
"/etc/dcgm-exporter/counters.csv"
);
assert_eq!(json["counters_file"], "/etc/dcgm-exporter/counters.csv");
}
}
25 changes: 7 additions & 18 deletions src/modules/hpc/fabric_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,7 @@ fn parse_driver_version(nvidia_smi_output: &str) -> Option<String> {
return None;
}
// Take only the first line in case of multi-GPU output
Some(
version
.lines()
.next()
.unwrap_or("")
.trim()
.to_string(),
)
Some(version.lines().next().unwrap_or("").trim().to_string())
}

/// Validate fabric mode parameter.
Expand Down Expand Up @@ -297,11 +290,7 @@ impl Module for FabricManagerModule {
let config_content = generate_fm_config(&fabric_mode, fault_tolerance, log_level);

if !context.check_mode {
run_cmd_ok(
connection,
"mkdir -p /etc/nvidia-fabricmanager",
context,
)?;
run_cmd_ok(connection, "mkdir -p /etc/nvidia-fabricmanager", context)?;

// Check if config differs
let (exists, current_content, _) = run_cmd(
Expand Down Expand Up @@ -375,10 +364,7 @@ impl Module for FabricManagerModule {
};

let mut output = if changed {
ModuleOutput::changed(format!(
"Applied {} Fabric Manager changes",
changes.len()
))
ModuleOutput::changed(format!("Applied {} Fabric Manager changes", changes.len()))
} else {
ModuleOutput::ok("Fabric Manager is installed and configured")
};
Expand Down Expand Up @@ -467,7 +453,10 @@ mod tests {

#[test]
fn test_detect_os_family() {
assert_eq!(detect_os_family("ID_LIKE=\"rhel centos fedora\""), Some("rhel"));
assert_eq!(
detect_os_family("ID_LIKE=\"rhel centos fedora\""),
Some("rhel")
);
assert_eq!(detect_os_family("ID=ubuntu\nVERSION=22.04"), Some("debian"));
assert_eq!(detect_os_family("ID=freebsd"), None);
}
Expand Down
17 changes: 7 additions & 10 deletions src/modules/hpc/gdrcopy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,7 @@ impl Module for GdrcopyModule {
if context.check_mode {
changes.push("Would remove /etc/modules-load.d/gdrdrv.conf".to_string());
} else {
run_cmd_ok(
connection,
"rm -f /etc/modules-load.d/gdrdrv.conf",
context,
)?;
run_cmd_ok(connection, "rm -f /etc/modules-load.d/gdrdrv.conf", context)?;
changes.push("Removed /etc/modules-load.d/gdrdrv.conf".to_string());
}
}
Expand All @@ -237,10 +233,8 @@ impl Module for GdrcopyModule {
.with_data("changes", serde_json::json!(changes)));
}

return Ok(
ModuleOutput::changed("Removed GDRCopy")
.with_data("changes", serde_json::json!(changes)),
);
return Ok(ModuleOutput::changed("Removed GDRCopy")
.with_data("changes", serde_json::json!(changes)));
}

// -- state=present --
Expand Down Expand Up @@ -443,7 +437,10 @@ gdrcopy_sanity: PASSED

#[test]
fn test_detect_os_family() {
assert_eq!(detect_os_family("ID_LIKE=\"rhel centos fedora\""), Some("rhel"));
assert_eq!(
detect_os_family("ID_LIKE=\"rhel centos fedora\""),
Some("rhel")
);
assert_eq!(detect_os_family("ID=ubuntu\nVERSION=22.04"), Some("debian"));
assert_eq!(detect_os_family("ID=freebsd"), None);
}
Expand Down
Loading
Loading