1use crate::error::RegentError;
73use crate::hosts::managed_host::InternalApiCallOutcome;
74use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
75use crate::hosts::properties::{HostProperties, InitSystem, LinuxFlavor, LinuxSpecifics, OsKind};
76use crate::secrets::SecretProvidersPool;
77use crate::state::Check;
78use crate::state::attribute::HostHandler;
79use crate::state::attribute::Privilege;
80use crate::state::attribute::Remediation;
81use crate::state::attribute::RemediationsList;
82use crate::state::compliance::AttributeComplianceAssessment;
83use serde::{Deserialize, Serialize};
84use std::time::Duration;
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(rename_all = "PascalCase")]
97pub enum ServiceExpectedState {
98 Started,
100 Stopped,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114#[serde(rename_all = "PascalCase")]
115pub enum ServiceAction {
116 Restarted,
118 Reloaded,
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all_fields = "PascalCase")]
156#[serde(untagged)]
157pub enum ServiceBlockExpectedState {
158 State {
163 name: String,
165 #[serde(default)]
167 state: Option<ServiceExpectedState>,
168 enabled: bool,
170 },
171 Action {
173 name: String,
175 action: ServiceAction,
177 },
178}
179
180impl Timeout for ServiceBlockExpectedState {
181 fn default_timeout(&self) -> Duration {
182 Duration::from_secs(10)
183 }
184}
185
186impl ServiceBlockExpectedState {
187 pub fn state(
189 name: &str,
190 state: ServiceExpectedState,
191 enabled: bool,
192 ) -> ServiceBlockExpectedState {
193 ServiceBlockExpectedState::State {
194 name: name.to_string(),
195 state: Some(state),
196 enabled,
197 }
198 }
199
200 pub fn enabled(name: &str, enabled: bool) -> ServiceBlockExpectedState {
202 ServiceBlockExpectedState::State {
203 name: name.to_string(),
204 state: None,
205 enabled,
206 }
207 }
208
209 pub fn restarted(name: &str) -> ServiceBlockExpectedState {
211 ServiceBlockExpectedState::Action {
212 name: name.to_string(),
213 action: ServiceAction::Restarted,
214 }
215 }
216
217 pub fn reloaded(name: &str) -> ServiceBlockExpectedState {
219 ServiceBlockExpectedState::Action {
220 name: name.to_string(),
221 action: ServiceAction::Reloaded,
222 }
223 }
224}
225
226impl Check for ServiceBlockExpectedState {
227 fn check(&self) -> Result<(), RegentError> {
228 Ok(())
234 }
235
236 fn check_host_compatibility(
237 &self,
238 host_properties: &HostProperties,
239 ) -> Result<(), RegentError> {
240 use crate::hosts::properties::InitSystem;
241 match host_properties.os_kind() {
242 OsKind::Linux(linux_specifics) => match linux_specifics.init_system {
243 InitSystem::Systemd => Ok(()),
244 InitSystem::Unknown => Err(RegentError::IncompatibleHost(
245 "systemctl requires systemd but init system could not be detected".to_string(),
246 )),
247 },
248 #[cfg(feature = "windows")]
249 OsKind::Windows(_) => Ok(()),
250 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
251 "Host is {:?} but service management is only supported on Linux with systemd or Windows (with windows feature)",
252 incompatible_os_kind
253 ))),
254 }
255 }
256}
257
258impl<Handler: HostHandler> AssessCompliance<Handler> for ServiceBlockExpectedState {
259 async fn assess_compliance(
260 &self,
261 host_handler: &mut Handler,
262 host_properties: &Option<HostProperties>,
263 privilege: &Privilege,
264 _optional_secret_provider: &Option<SecretProvidersPool>,
265 ) -> Result<AttributeComplianceAssessment, RegentError> {
266 if let Some(props) = host_properties {
268 self.check_host_compatibility(props)?;
269 }
270
271 let os_kind = host_properties
273 .as_ref()
274 .map(|props| props.os_kind())
275 .unwrap_or(&OsKind::Linux(LinuxSpecifics {
276 linux_flavor: LinuxFlavor::Debian,
277 init_system: InitSystem::Systemd,
278 }));
279
280 match os_kind {
282 #[cfg(feature = "windows")]
283 OsKind::Windows(_) => {
284 let command_available = host_handler
286 .is_this_command_available("sc", privilege)
287 .await
288 .unwrap_or(false);
289
290 if !command_available {
291 return Err(RegentError::FailedDryRunEvaluation(
292 "Service management commands (sc) are not available on this Windows host"
293 .to_string(),
294 ));
295 }
296 }
297 OsKind::Linux(_) => {
298 let command_available = host_handler
300 .is_this_command_available("systemctl", privilege)
301 .await
302 .unwrap_or(false);
303
304 if !command_available {
305 return Err(RegentError::FailedDryRunEvaluation(
306 "Service management commands (systemctl) are not available on this Linux host".to_string(),
307 ));
308 }
309 }
310 OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {}
311 }
312
313 let mut remediations: Vec<Remediation> = Vec::new();
315
316 match &self {
317 Self::State {
318 name,
319 state,
320 enabled,
321 } => {
322 match os_kind {
324 #[cfg(feature = "windows")]
325 OsKind::Windows(_) => {
326 if let Some(state) = state {
328 match state {
329 ServiceExpectedState::Started => {
330 let active = windows_service_is_active(host_handler, &name)
331 .await
332 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
333 if !active {
334 remediations.push(Remediation::Service(
335 ServiceApiCall::from(
336 ServiceModuleInternalApiCall::Start(name.clone()),
337 privilege.clone(),
338 ),
339 ));
340 }
341 }
342 ServiceExpectedState::Stopped => {
343 let active = windows_service_is_active(host_handler, &name)
344 .await
345 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
346 if active {
347 remediations.push(Remediation::Service(
348 ServiceApiCall::from(
349 ServiceModuleInternalApiCall::Stop(name.clone()),
350 privilege.clone(),
351 ),
352 ));
353 }
354 }
355 }
356 }
357 if *enabled {
359 let is_enabled = windows_service_is_enabled(host_handler, &name)
360 .await
361 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
362 if !is_enabled {
363 remediations.push(Remediation::Service(ServiceApiCall::from(
364 ServiceModuleInternalApiCall::Enable(name.clone()),
365 privilege.clone(),
366 )));
367 }
368 } else {
369 let is_enabled = windows_service_is_enabled(host_handler, &name)
370 .await
371 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
372 if is_enabled {
373 remediations.push(Remediation::Service(ServiceApiCall::from(
374 ServiceModuleInternalApiCall::Disable(name.clone()),
375 privilege.clone(),
376 )));
377 }
378 }
379 }
380 OsKind::Linux(_) => {
381 if let Some(state) = state {
383 match state {
384 ServiceExpectedState::Started => {
385 let active = service_is_active(host_handler, &name)
386 .await
387 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
388 if !active {
389 remediations.push(Remediation::Service(
390 ServiceApiCall::from(
391 ServiceModuleInternalApiCall::Start(name.clone()),
392 privilege.clone(),
393 ),
394 ));
395 }
396 }
397 ServiceExpectedState::Stopped => {
398 let active = service_is_active(host_handler, &name)
399 .await
400 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
401 if active {
402 remediations.push(Remediation::Service(
403 ServiceApiCall::from(
404 ServiceModuleInternalApiCall::Stop(name.clone()),
405 privilege.clone(),
406 ),
407 ));
408 }
409 }
410 }
411 }
412 if *enabled {
414 let is_enabled = service_is_enabled(host_handler, &name)
415 .await
416 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
417 if !is_enabled {
418 remediations.push(Remediation::Service(ServiceApiCall::from(
419 ServiceModuleInternalApiCall::Enable(name.clone()),
420 privilege.clone(),
421 )));
422 }
423 } else {
424 let is_enabled = service_is_enabled(host_handler, &name)
425 .await
426 .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
427 if is_enabled {
428 remediations.push(Remediation::Service(ServiceApiCall::from(
429 ServiceModuleInternalApiCall::Disable(name.clone()),
430 privilege.clone(),
431 )));
432 }
433 }
434 }
435 OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
436 return Err(RegentError::FailedDryRunEvaluation(format!(
437 "Service management is not supported on {:?}",
438 os_kind
439 )));
440 }
441 }
442 }
443 Self::Action { name, action } => {
444 match os_kind {
446 #[cfg(feature = "windows")]
447 OsKind::Windows(_) => {
448 match &action {
449 ServiceAction::Restarted => {
450 remediations.push(Remediation::Service(ServiceApiCall::from(
452 ServiceModuleInternalApiCall::Restart(name.clone()),
453 privilege.clone(),
454 )));
455 }
456 ServiceAction::Reloaded => {
457 remediations.push(Remediation::Service(ServiceApiCall::from(
459 ServiceModuleInternalApiCall::Reload(name.clone()),
460 privilege.clone(),
461 )));
462 }
463 }
464 }
465 OsKind::Linux(_) => {
466 match &action {
467 ServiceAction::Restarted => {
468 remediations.push(Remediation::Service(ServiceApiCall::from(
470 ServiceModuleInternalApiCall::Restart(name.clone()),
471 privilege.clone(),
472 )));
473 }
474 ServiceAction::Reloaded => {
475 remediations.push(Remediation::Service(ServiceApiCall::from(
477 ServiceModuleInternalApiCall::Reload(name.clone()),
478 privilege.clone(),
479 )));
480 }
481 }
482 }
483 OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
484 return Err(RegentError::FailedDryRunEvaluation(format!(
485 "Service management is not supported on {:?}",
486 os_kind
487 )));
488 }
489 }
490 }
491 }
492
493 if remediations.is_empty() {
494 Ok(AttributeComplianceAssessment::Compliant)
495 } else {
496 Ok(AttributeComplianceAssessment::NonCompliant(
497 RemediationsList::from(remediations)?,
498 ))
499 }
500 }
501}
502
503#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
504#[serde(rename_all = "PascalCase")]
505pub enum ServiceModuleInternalApiCall {
506 Start(String),
507 Stop(String),
508 Restart(String),
509 Reload(String),
510 Enable(String),
511 Disable(String),
512}
513
514impl std::fmt::Display for ServiceModuleInternalApiCall {
515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516 match self {
517 ServiceModuleInternalApiCall::Start(s) => write!(f, "start {}", s),
518 ServiceModuleInternalApiCall::Stop(s) => write!(f, "stop {}", s),
519 ServiceModuleInternalApiCall::Restart(s) => write!(f, "restart {}", s),
520 ServiceModuleInternalApiCall::Reload(s) => write!(f, "reload {}", s),
521 ServiceModuleInternalApiCall::Enable(s) => write!(f, "enable {}", s),
522 ServiceModuleInternalApiCall::Disable(s) => write!(f, "disable {}", s),
523 }
524 }
525}
526
527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
528pub struct ServiceApiCall {
529 pub api_call: ServiceModuleInternalApiCall,
530 privilege: Privilege,
531}
532
533impl ServiceApiCall {
534 pub fn display(&self) -> String {
535 match &self.api_call {
536 ServiceModuleInternalApiCall::Start(s) => format!("Start service {}", s),
537 ServiceModuleInternalApiCall::Stop(s) => format!("Stop service {}", s),
538 ServiceModuleInternalApiCall::Restart(s) => format!("Restart service {}", s),
539 ServiceModuleInternalApiCall::Reload(s) => format!("Reload service {}", s),
540 ServiceModuleInternalApiCall::Enable(s) => format!("Enable service {}", s),
541 ServiceModuleInternalApiCall::Disable(s) => format!("Disable service {}", s),
542 }
543 }
544
545 fn from(api_call: ServiceModuleInternalApiCall, privilege: Privilege) -> ServiceApiCall {
546 ServiceApiCall {
547 api_call,
548 privilege,
549 }
550 }
551}
552
553impl Check for ServiceApiCall {
554 fn check(&self) -> Result<(), RegentError> {
555 Ok(())
556 }
557
558 fn check_host_compatibility(
559 &self,
560 host_properties: &HostProperties,
561 ) -> Result<(), RegentError> {
562 use crate::hosts::properties::InitSystem;
563 match host_properties.os_kind() {
564 OsKind::Linux(linux_specifics) => match linux_specifics.init_system {
565 InitSystem::Systemd => Ok(()),
566 InitSystem::Unknown => Err(RegentError::IncompatibleHost(
567 "systemctl requires systemd but init system could not be detected".to_string(),
568 )),
569 },
570 #[cfg(feature = "windows")]
571 OsKind::Windows(_) => Ok(()),
572 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
573 "Host is {:?} but service management is only supported on Linux with systemd or Windows (with windows feature)",
574 incompatible_os_kind
575 ))),
576 }
577 }
578}
579
580impl<Handler: HostHandler> ReachCompliance<Handler> for ServiceApiCall {
581 async fn call(
582 &self,
583 host_handler: &mut Handler,
584 host_properties: &Option<HostProperties>,
585 _optional_secret_provider: &Option<SecretProvidersPool>,
586 ) -> Result<InternalApiCallOutcome, RegentError> {
587 if let Some(props) = host_properties {
589 self.check_host_compatibility(props)?;
590 }
591
592 let os_kind = host_properties
594 .as_ref()
595 .map(|props| props.os_kind())
596 .unwrap_or(&OsKind::Linux(LinuxSpecifics {
597 linux_flavor: LinuxFlavor::Debian,
598 init_system: InitSystem::Systemd,
599 }));
600
601 match os_kind {
603 #[cfg(feature = "windows")]
604 OsKind::Windows(_) => {
605 let cmd = match &self.api_call {
607 ServiceModuleInternalApiCall::Start(s) => format!("net start {}", s),
608 ServiceModuleInternalApiCall::Stop(s) => format!("net stop {}", s),
609 ServiceModuleInternalApiCall::Restart(s) => {
610 format!("net stop {} && net start {}", s, s)
612 }
613 ServiceModuleInternalApiCall::Reload(s) => {
614 format!("sc control {} 128", s) }
618 ServiceModuleInternalApiCall::Enable(s) => {
619 format!("sc config {} start= auto", s)
620 }
621 ServiceModuleInternalApiCall::Disable(s) => {
622 format!("sc config {} start= disabled", s)
623 }
624 };
625
626 let result = host_handler.run_windows_command(&cmd).await;
628
629 match result {
630 Ok(result) => {
631 if result.return_code == 0 {
632 Ok(InternalApiCallOutcome::Success(None))
633 } else {
634 Ok(InternalApiCallOutcome::Failure(format!(
635 "RC: {}, STDOUT: {}, STDERR: {}",
636 result.return_code, result.stdout, result.stderr
637 )))
638 }
639 }
640 Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
641 "Command execution failed: {:?}",
642 e
643 ))),
644 }
645 }
646 OsKind::Linux(_) => {
647 let cmd = match &self.api_call {
649 ServiceModuleInternalApiCall::Start(s) => format!("systemctl start {}", s),
650 ServiceModuleInternalApiCall::Stop(s) => format!("systemctl stop {}", s),
651 ServiceModuleInternalApiCall::Restart(s) => format!("systemctl restart {}", s),
652 ServiceModuleInternalApiCall::Reload(s) => format!("systemctl reload {}", s),
653 ServiceModuleInternalApiCall::Enable(s) => format!("systemctl enable {}", s),
654 ServiceModuleInternalApiCall::Disable(s) => format!("systemctl disable {}", s),
655 };
656
657 let result = host_handler.run_command(&cmd, &self.privilege).await;
659
660 match result {
661 Ok(result) => {
662 if result.return_code == 0 {
663 Ok(InternalApiCallOutcome::Success(None))
664 } else {
665 Ok(InternalApiCallOutcome::Failure(format!(
666 "RC: {}, STDOUT: {}, STDERR: {}",
667 result.return_code, result.stdout, result.stderr
668 )))
669 }
670 }
671 Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
672 "Command execution failed: {:?}",
673 e
674 ))),
675 }
676 }
677 OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
678 Err(RegentError::FailedDryRunEvaluation(format!(
679 "Service management is not supported on {:?}",
680 os_kind
681 )))
682 }
683 }
684 }
685}
686
687async fn service_is_active<Handler: HostHandler>(
688 host_handler: &mut Handler,
689 name: &str,
690) -> Result<bool, String> {
691 match host_handler
692 .run_command(&format!("systemctl is-active {}", name), &Privilege::None)
693 .await
694 {
695 Ok(r) => match r.return_code {
696 0 => Ok(true),
697 3 => Ok(false),
698 4 => Err(format!("Service not found: {}", name)),
699 _ => Ok(false), },
701 Err(e) => Err(format!("Unable to check active state of {}: {:?}", name, e)),
702 }
703}
704
705async fn service_is_enabled<Handler: HostHandler>(
706 host_handler: &mut Handler,
707 name: &str,
708) -> Result<bool, String> {
709 match host_handler
710 .run_command(&format!("systemctl is-enabled {}", name), &Privilege::None)
711 .await
712 {
713 Ok(r) => match r.return_code {
714 0 => Ok(true),
715 1 | 3 => Ok(false),
716 4 => Err(format!("Service not found: {}", name)),
717 _ => Ok(false),
718 },
719 Err(e) => Err(format!(
720 "Unable to check enabled state of {}: {:?}",
721 name, e
722 )),
723 }
724}
725
726#[cfg(feature = "windows")]
727async fn windows_service_is_active<Handler: HostHandler>(
728 host_handler: &mut Handler,
729 name: &str,
730) -> Result<bool, String> {
731 match host_handler
732 .run_windows_command(&format!("sc query {}", name))
733 .await
734 {
735 Ok(r) => {
736 if r.return_code != 0 {
739 if r.stdout.contains("does not exist") || r.stderr.contains("does not exist") {
741 return Err(format!("Service not found: {}", name));
742 }
743 return Ok(false);
744 }
745
746 let output = r.stdout.to_lowercase();
749 if output.contains("running") {
750 Ok(true)
751 } else if output.contains("stopped") || output.contains("pending") {
752 Ok(false)
753 } else {
754 Ok(false)
756 }
757 }
758 Err(e) => Err(format!("Unable to check active state of {}: {:?}", name, e)),
759 }
760}
761
762#[cfg(feature = "windows")]
763async fn windows_service_is_enabled<Handler: HostHandler>(
764 host_handler: &mut Handler,
765 name: &str,
766) -> Result<bool, String> {
767 match host_handler
768 .run_windows_command(&format!("sc qc {}", name))
769 .await
770 {
771 Ok(r) => {
772 if r.return_code != 0 {
775 if r.stdout.contains("does not exist") || r.stderr.contains("does not exist") {
776 return Err(format!("Service not found: {}", name));
777 }
778 return Ok(false);
779 }
780
781 let output = r.stdout.to_lowercase();
784 if output.contains("auto_start") || output.contains("2") {
785 Ok(true)
786 } else if output.contains("disabled") || output.contains("3") || output.contains("4") {
787 Ok(false)
788 } else {
789 Ok(false)
791 }
792 }
793 Err(e) => Err(format!(
794 "Unable to check enabled state of {}: {:?}",
795 name, e
796 )),
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 #[test]
805 fn parsing_service_module_block_from_yaml_str() {
806 let raw = "---
807- Name: nginx
808 State: Started
809 Enabled: true
810
811- Name: nginx
812 State: Stopped
813 Enabled: false
814
815- Name: nginx
816 Action: Restarted
817
818- Name: nginx
819 Action: Reloaded
820
821- Name: nginx
822 Enabled: true
823 ";
824 let _: Vec<ServiceBlockExpectedState> = yaml_serde::from_str(raw).unwrap();
825 }
826}