Skip to main content

regent_sdk/state/attribute/shell/
command.rs

1//! Shell command execution attribute
2//!
3//! This module provides the `CommandBlockExpectedState` type for executing arbitrary
4//! shell commands on managed hosts.
5//!
6//! **Compatible OS:**
7//! - All POSIX systems (Linux, macOS, FreeBSD) - uses `run_command`
8//! - Windows (when `windows` feature is enabled) - uses `run_windows_command`
9//!
10//! # Examples
11//!
12//! ## Rust API
13//!
14//! ```no_run
15//! use regent_sdk::state::attribute::shell::command::CommandBlockExpectedState;
16//! use regent_sdk::{Attribute, ExpectedState, Privilege};
17//!
18//! // Execute a simple command
19//! let echo = CommandBlockExpectedState::builder("echo 'Hello, World!'")
20//!     .build()
21//!     .unwrap();
22//!
23//! let expected_state = ExpectedState::new()
24//!     .with_attribute(Attribute::command(echo, Privilege::None, None))
25//!     .build();
26//! ```
27//!
28//! ## YAML API
29//!
30//! ```yaml
31//! Attributes:
32//!   - Name: Hello world must be printed
33//!     Privilege: !None
34//!     Detail: !Command
35//!       Cmd: "echo 'Hello, World!'"
36//! ```
37
38use crate::error::RegentError;
39use crate::hosts::managed_host::InternalApiCallOutcome;
40use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
41use crate::hosts::properties::{HostProperties, InitSystem, LinuxFlavor, LinuxSpecifics, OsKind};
42use crate::secrets::{SecretProvidersPool, SecretReference};
43use crate::state::Check;
44use crate::state::attribute::HostHandler;
45use crate::state::attribute::Privilege;
46use crate::state::attribute::Remediation;
47use crate::state::attribute::RemediationsList;
48use crate::state::compliance::AttributeComplianceAssessment;
49use crate::state::expected_state::Parameter;
50use serde::{Deserialize, Serialize};
51use std::time::Duration;
52
53/// Configuration for a shell command to execute
54///
55/// The command will be executed each time compliance is assessed. Use this for
56/// idempotent commands or one-time setup tasks.
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59#[serde(rename_all = "PascalCase")]
60pub struct CommandBlockExpectedState {
61    /// Command to execute. Can be a clear text string or a secret reference.
62    cmd: Parameter<String>,
63}
64
65impl Timeout for CommandBlockExpectedState {
66    fn default_timeout(&self) -> Duration {
67        Duration::from_secs(10)
68    }
69}
70
71impl CommandBlockExpectedState {
72    pub fn new(cmd: &str) -> CommandBlockExpectedState {
73        CommandBlockExpectedState {
74            cmd: Parameter::Clear(cmd.to_string()),
75        }
76    }
77
78    pub fn new_from_secret(sec_ref: SecretReference) -> CommandBlockExpectedState {
79        CommandBlockExpectedState {
80            cmd: Parameter::Secret(sec_ref),
81        }
82    }
83}
84
85impl Check for CommandBlockExpectedState {
86    fn check(&self) -> Result<(), RegentError> {
87        Ok(())
88    }
89
90    fn check_host_compatibility(
91        &self,
92        _host_properties: &HostProperties,
93    ) -> Result<(), RegentError> {
94        // Shell commands are cross-platform compatible
95        Ok(())
96    }
97}
98
99impl<Handler: HostHandler> AssessCompliance<Handler> for CommandBlockExpectedState {
100    async fn assess_compliance(
101        &self,
102        _host_handler: &mut Handler,
103        host_properties: &Option<HostProperties>,
104        privilege: &Privilege,
105        _optional_secret_provider: &Option<SecretProvidersPool>,
106    ) -> Result<AttributeComplianceAssessment, RegentError> {
107        // Early check: verify host compatibility (always passes for commands)
108        if let Some(props) = host_properties {
109            self.check_host_compatibility(props)?;
110        }
111
112        // Determine the effective OS kind - assume Linux if HostProperties is None
113        let os_kind = host_properties
114            .as_ref()
115            .map(|props| props.os_kind())
116            .unwrap_or(&OsKind::Linux(LinuxSpecifics {
117                linux_flavor: LinuxFlavor::Debian,
118                init_system: InitSystem::Systemd,
119            }));
120
121        // Match on OS kind - commands are supported on all platforms
122        match os_kind {
123            #[cfg(feature = "windows")]
124            OsKind::Windows(_) => {}
125            OsKind::Linux(_) => {}
126            OsKind::FreeBsd(_) => {}
127            OsKind::MacOs(_) => {}
128            OsKind::Unknown => {}
129        }
130
131        let mut remediations: Vec<Remediation> = Vec::new();
132
133        let privilege = privilege.clone();
134
135        remediations.push(Remediation::Command(CommandApiCall {
136            cmd: self.cmd.clone(),
137            privilege,
138        }));
139
140        return Ok(AttributeComplianceAssessment::NonCompliant(
141            RemediationsList::from(remediations)?,
142        ));
143    }
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct CommandApiCall {
148    pub cmd: Parameter<String>,
149    privilege: Privilege,
150}
151
152impl CommandApiCall {
153    pub fn display(&self) -> String {
154        return format!("Run command : {}", self.cmd);
155    }
156}
157
158impl Check for CommandApiCall {
159    fn check(&self) -> Result<(), RegentError> {
160        Ok(())
161    }
162
163    fn check_host_compatibility(
164        &self,
165        _host_properties: &HostProperties,
166    ) -> Result<(), RegentError> {
167        // Shell commands are cross-platform compatible
168        Ok(())
169    }
170}
171
172impl<Handler: HostHandler> ReachCompliance<Handler> for CommandApiCall {
173    async fn call(
174        &self,
175        host_handler: &mut Handler,
176        host_properties: &Option<HostProperties>,
177        optional_secret_provider: &Option<SecretProvidersPool>,
178    ) -> Result<InternalApiCallOutcome, RegentError> {
179        // Early check: verify host compatibility (always passes for commands)
180        if let Some(props) = host_properties {
181            self.check_host_compatibility(props)?;
182        }
183
184        // Determine the effective OS kind - assume Linux if HostProperties is None
185        let os_kind = host_properties
186            .as_ref()
187            .map(|props| props.os_kind())
188            .unwrap_or(&OsKind::Linux(LinuxSpecifics {
189                linux_flavor: LinuxFlavor::Debian,
190                init_system: InitSystem::Systemd,
191            }));
192
193        // Get the raw command string
194        let cmd_string = self
195            .cmd
196            .clone()
197            .inner_raw(optional_secret_provider)
198            .await
199            .unwrap();
200
201        // Match on OS kind to use the appropriate command execution method
202        match os_kind {
203            #[cfg(feature = "windows")]
204            OsKind::Windows(_) => {
205                // Execute command on Windows using run_windows_command
206                let cmd_result = host_handler.run_windows_command(&cmd_string).await;
207
208                match cmd_result {
209                    Ok(result) => {
210                        if result.return_code == 0 {
211                            Ok(InternalApiCallOutcome::Success(Some(result.stdout)))
212                        } else {
213                            Ok(InternalApiCallOutcome::Failure(format!(
214                                "RC : {}, STDOUT : {}, STDERR : {}",
215                                result.return_code, result.stdout, result.stderr
216                            )))
217                        }
218                    }
219                    Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
220                        "Command execution failed: {:?}",
221                        e
222                    ))),
223                }
224            }
225            OsKind::Linux(_) | OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
226                // Execute command on POSIX systems using run_command
227                let cmd_result = host_handler.run_command(&cmd_string, &self.privilege).await;
228
229                match cmd_result {
230                    Ok(result) => {
231                        if result.return_code == 0 {
232                            Ok(InternalApiCallOutcome::Success(Some(result.stdout)))
233                        } else {
234                            Ok(InternalApiCallOutcome::Failure(format!(
235                                "RC : {}, STDOUT : {}, STDERR : {}",
236                                result.return_code, result.stdout, result.stderr
237                            )))
238                        }
239                    }
240                    Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
241                        "Command execution failed: {:?}",
242                        e
243                    ))),
244                }
245            }
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn parsing_service_module_block_from_yaml_str() {
256        let raw_attributes = "---
257- Cmd: ls -ltrh";
258
259        let _attributes: Vec<CommandBlockExpectedState> =
260            yaml_serde::from_str(raw_attributes).unwrap();
261    }
262}