Skip to main content

regent_sdk/state/attribute/shell/
command.rs

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