regent_sdk/state/attribute/package/
apt.rs1use crate::error::RegentError;
52use crate::hosts::managed_host::InternalApiCallOutcome;
53use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
54use crate::hosts::properties::{HostProperties, LinuxFlavor, LinuxSpecifics, OsKind};
55use crate::secrets::SecretProvidersPool;
56use crate::state::Check;
57use crate::state::attribute::HostHandler;
58use crate::state::attribute::Privilege;
59use crate::state::attribute::Remediation;
60use crate::state::attribute::RemediationsList;
61use crate::state::attribute::package::PackageExpectedState;
62use crate::state::compliance::AttributeComplianceAssessment;
63use serde::{Deserialize, Serialize};
64use std::time::Duration;
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "PascalCase")]
68pub enum AptModuleInternalApiCall {
69 Install(String),
70 Remove(String),
71 Upgrade,
72}
73
74impl std::fmt::Display for AptModuleInternalApiCall {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 AptModuleInternalApiCall::Install(package) => write!(f, "install {}", package),
78 AptModuleInternalApiCall::Remove(package) => write!(f, "remove {}", package),
79 AptModuleInternalApiCall::Upgrade => write!(f, "upgrade"),
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all_fields = "PascalCase")]
86#[serde(deny_unknown_fields)]
87pub enum AptExpectedState {
88 SystemUpToDate,
90 #[serde(untagged)]
91 PackageState {
92 package: String,
93 state: PackageExpectedState,
94 },
95}
96
97impl Timeout for AptExpectedState {
98 fn default_timeout(&self) -> Duration {
99 match self {
100 Self::SystemUpToDate => Duration::from_secs(300),
101 Self::PackageState {
102 package: _,
103 state: _,
104 } => Duration::from_secs(60),
105 }
106 }
107}
108
109impl AptExpectedState {
110 pub fn full_system_upgrade() -> AptExpectedState {
111 AptExpectedState::SystemUpToDate
112 }
113
114 pub fn package_state(package: &str, state: PackageExpectedState) -> AptExpectedState {
115 AptExpectedState::PackageState {
116 package: package.to_string(),
117 state,
118 }
119 }
120
121 pub fn package_present(package: &str) -> AptExpectedState {
122 AptExpectedState::PackageState {
123 package: package.to_string(),
124 state: PackageExpectedState::Present,
125 }
126 }
127
128 pub fn package_absent(package: &str) -> AptExpectedState {
129 AptExpectedState::PackageState {
130 package: package.to_string(),
131 state: PackageExpectedState::Absent,
132 }
133 }
134}
135
136impl Check for AptExpectedState {
137 fn check(&self) -> Result<(), RegentError> {
138 Ok(())
139 }
140
141 fn check_host_compatibility(
142 &self,
143 host_properties: &HostProperties,
144 ) -> Result<(), RegentError> {
145 match host_properties.os_kind() {
146 OsKind::Linux(LinuxSpecifics {
147 linux_flavor: LinuxFlavor::Debian,
148 ..
149 }) => Ok(()),
150 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
151 "Host is {:?} but APT is only supported on Debian-based Linux distributions",
152 incompatible_os_kind
153 ))),
154 }
155 }
156}
157
158impl<Handler: HostHandler> AssessCompliance<Handler> for AptExpectedState {
159 async fn assess_compliance(
160 &self,
161 host_handler: &mut Handler,
162 host_properties: &Option<HostProperties>,
163 privilege: &Privilege,
164 _optional_secret_provider: &Option<SecretProvidersPool>,
165 ) -> Result<AttributeComplianceAssessment, RegentError> {
166 if let Some(props) = host_properties {
167 self.check_host_compatibility(props)?;
168 }
169
170 let apt_available = match host_handler
171 .is_this_command_available("apt-get", &Privilege::None)
172 .await
173 {
174 Ok(availability) => availability,
175 Err(details) => {
176 return Err(RegentError::FailedDryRunEvaluation(format!(
177 "{:?}",
178 details
179 )));
180 }
181 };
182 let dpkg_available = match host_handler
183 .is_this_command_available("dpkg", &Privilege::None)
184 .await
185 {
186 Ok(availability) => availability,
187 Err(details) => {
188 return Err(RegentError::FailedDryRunEvaluation(format!(
189 "{:?}",
190 details
191 )));
192 }
193 };
194
195 if !apt_available || !dpkg_available {
196 return Err(RegentError::FailedDryRunEvaluation(
197 "APT not working on this host. Is this really a debian-flavored linux distribution ?"
198 .to_string(),
199 ));
200 }
201
202 let mut remediations: Vec<Remediation> = Vec::new();
203
204 match &self {
205 Self::SystemUpToDate => {
206 match host_handler
209 .run_command(
210 "apt-get update -y && apt-get -s -u upgrade",
211 &Privilege::None,
212 )
213 .await
214 {
215 Ok(r) if r.return_code == 0 => {
216 if r.stdout.lines().any(|l| l.starts_with("Inst")) {
217 remediations.push(Remediation::Apt(AptApiCall::from(
218 AptModuleInternalApiCall::Upgrade,
219 privilege.clone(),
220 )));
221 }
222 }
223 Ok(r) => {
224 return Err(RegentError::FailedDryRunEvaluation(format!(
225 "Unable to check available updates: {:?}",
226 r
227 )));
228 }
229 Err(e) => {
230 return Err(RegentError::FailedDryRunEvaluation(format!(
231 "Unable to check available updates: {:?}",
232 e
233 )));
234 }
235 }
236 }
237 Self::PackageState {
238 package,
239 state: expected_state,
240 } => {
241 let package_is_currently_installed =
242 is_package_installed(host_handler, package).await;
243
244 match (package_is_currently_installed, expected_state) {
245 (true, PackageExpectedState::Present) => {}
246 (true, PackageExpectedState::Absent) => {
247 remediations.push(Remediation::Apt(AptApiCall::from(
248 AptModuleInternalApiCall::Remove(package.clone()),
249 privilege.clone(),
250 )));
251 }
252 (false, PackageExpectedState::Present) => {
253 remediations.push(Remediation::Apt(AptApiCall::from(
254 AptModuleInternalApiCall::Install(package.clone()),
255 privilege.clone(),
256 )));
257 }
258 (false, PackageExpectedState::Absent) => {}
259 }
260 }
261 }
262
263 if remediations.is_empty() {
264 Ok(AttributeComplianceAssessment::Compliant)
265 } else {
266 Ok(AttributeComplianceAssessment::NonCompliant(
267 RemediationsList::from(remediations)?,
268 ))
269 }
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub struct AptApiCall {
275 pub api_call: AptModuleInternalApiCall,
276 privilege: Privilege,
277}
278
279impl Check for AptApiCall {
280 fn check(&self) -> Result<(), RegentError> {
281 Ok(())
282 }
283
284 fn check_host_compatibility(
285 &self,
286 host_properties: &HostProperties,
287 ) -> Result<(), RegentError> {
288 match host_properties.os_kind() {
289 OsKind::Linux(LinuxSpecifics {
290 linux_flavor: LinuxFlavor::Debian,
291 ..
292 }) => Ok(()),
293 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
294 "Host is {:?} but APT is only supported on Debian-based Linux distributions",
295 incompatible_os_kind
296 ))),
297 }
298 }
299}
300
301impl AptApiCall {
302 pub fn display(&self) -> String {
303 match &self.api_call {
304 AptModuleInternalApiCall::Install(package_name) => {
305 format!("Install - {}", package_name)
306 }
307 AptModuleInternalApiCall::Remove(package_name) => {
308 format!("Remove - {}", package_name)
309 }
310 AptModuleInternalApiCall::Upgrade => String::from("Upgrade"),
311 }
312 }
313}
314
315impl<Handler: HostHandler> ReachCompliance<Handler> for AptApiCall {
316 async fn call(
317 &self,
318 host_handler: &mut Handler,
319 host_properties: &Option<HostProperties>,
320 _optional_secret_provider: &Option<SecretProvidersPool>,
321 ) -> Result<InternalApiCallOutcome, RegentError> {
322 if let Some(props) = host_properties {
323 self.check_host_compatibility(props)?;
324 }
325
326 let (cmd, privilege, package_name) = match &self.api_call {
327 AptModuleInternalApiCall::Install(package_name) => (
328 format!(
329 "DEBIAN_FRONTEND=noninteractive apt-get install -y {}",
330 package_name
331 ),
332 &self.privilege,
333 Some(package_name.clone()),
334 ),
335 AptModuleInternalApiCall::Remove(package_name) => (
336 format!(
337 "DEBIAN_FRONTEND=noninteractive apt-get remove --purge -y {}",
338 package_name
339 ),
340 &self.privilege,
341 Some(package_name.clone()),
342 ),
343 AptModuleInternalApiCall::Upgrade => (
344 "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get upgrade -y".to_string(),
345 &self.privilege,
346 None,
347 ),
348 };
349
350 let cmd_result = host_handler
351 .run_command(cmd.as_str(), privilege)
352 .await
353 .unwrap();
354
355 if cmd_result.return_code == 0 {
356 if let Some(pkg_name) = &package_name {
357 let verification_result = match &self.api_call {
358 AptModuleInternalApiCall::Install(_) => {
359 is_package_installed(host_handler, pkg_name).await
360 }
361 AptModuleInternalApiCall::Remove(_) => {
362 !is_package_installed(host_handler, pkg_name).await
363 }
364 AptModuleInternalApiCall::Upgrade => {
365 true
367 }
368 };
369
370 if verification_result {
371 Ok(InternalApiCallOutcome::Success(None))
372 } else {
373 Ok(InternalApiCallOutcome::Failure(format!(
374 "Command succeeded but post-verification failed: package {} state does not match expected",
375 pkg_name
376 )))
377 }
378 } else {
379 Ok(InternalApiCallOutcome::Success(None))
380 }
381 } else {
382 Ok(InternalApiCallOutcome::Failure(format!(
383 "RC : {}, STDOUT : {}, STDERR : {}",
384 cmd_result.return_code, cmd_result.stdout, cmd_result.stderr
385 )))
386 }
387 }
388}
389
390impl AptApiCall {
391 fn from(api_call: AptModuleInternalApiCall, privilege: Privilege) -> AptApiCall {
392 AptApiCall {
393 api_call,
394 privilege,
395 }
396 }
397}
398
399async fn is_package_installed<Handler: HostHandler>(
400 host_handler: &mut Handler,
401 package: &str,
402) -> bool {
403 let test = host_handler
404 .run_command(format!("dpkg -s {}", package).as_str(), &Privilege::None)
405 .await
406 .unwrap();
407
408 test.return_code == 0
409}
410
411#[cfg(test)]
412mod tests {
413
414 use super::*;
415
416 #[test]
417 fn parsing_apt_module_block_from_yaml_str() {
418 let raw_attributes = "---
419- SystemUpToDate
420
421- Package: apache2
422 State: Present
423
424- Package: apache2
425 State: Absent
426 ";
427
428 let attributes: Vec<AptExpectedState> = yaml_serde::from_str(raw_attributes).unwrap();
429
430 assert_eq!(attributes[0], AptExpectedState::SystemUpToDate);
431
432 assert_eq!(
433 attributes[1],
434 AptExpectedState::PackageState {
435 package: "apache2".to_string(),
436 state: PackageExpectedState::Present
437 }
438 );
439
440 assert_eq!(
441 attributes[2],
442 AptExpectedState::PackageState {
443 package: "apache2".to_string(),
444 state: PackageExpectedState::Absent
445 }
446 );
447 }
448
449 #[test]
450 fn rejecting_incorrect_apt_module_block_from_yaml_str() {
451 let raw_attribute = "---
452Package: apache2
453 ";
454 assert!(yaml_serde::from_str::<AptExpectedState>(raw_attribute).is_err());
455
456 let raw_attribute = "---
457Package:
458State: Absent
459 ";
460 assert!(yaml_serde::from_str::<AptExpectedState>(raw_attribute).is_err());
461
462 let raw_attribute = "---
463Package: apache2
464State: Absent
465unknown_key: unknown_value
466 ";
467 assert!(yaml_serde::from_str::<AptExpectedState>(raw_attribute).is_err());
468 }
469}