regent_sdk/state/attribute/utilities/
lineinfile.rs1use crate::error::RegentError;
2use crate::hosts::managed_host::InternalApiCallOutcome;
3use crate::hosts::managed_host::{AssessCompliance, ReachCompliance};
4use crate::hosts::properties::HostProperties;
5use crate::secrets::SecretProvidersPool;
6use crate::state::Check;
7use crate::state::attribute::HostHandler;
8use crate::state::attribute::Privilege;
9use crate::state::attribute::Remediation;
10use crate::state::compliance::AttributeComplianceAssessment;
11use crate::state::expected_state::Parameter;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "PascalCase")]
16pub enum LineInFileModuleInternalApiCall {
17 Add(LineExpectedPosition),
18 Delete(Vec<u64>),
19}
20
21impl std::fmt::Display for LineInFileModuleInternalApiCall {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 LineInFileModuleInternalApiCall::Add(position) => {
25 write!(f, "add line at position {:?}", position)
26 }
27 LineInFileModuleInternalApiCall::Delete(line_numbers) => {
28 write!(f, "delete lines {:?}", line_numbers)
29 }
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "PascalCase")]
36enum LineExpectedState {
37 Present,
38 Absent,
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "PascalCase")]
43pub enum LineExpectedPosition {
44 Top,
45 Bottom,
46 Anywhere,
47 LineNumber(u64),
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53#[serde(rename_all = "PascalCase")]
54pub struct LineInFileBlockExpectedState {
55 file_path: String,
56 line: Option<Parameter<String>>,
57 state: LineExpectedState,
58 position: Option<LineExpectedPosition>, line_number: Option<u64>, }
67
68impl Check for LineInFileBlockExpectedState {
69 fn check(&self) -> Result<(), RegentError> {
70 if let (None, None) = (&self.line, &self.position) {
71 return Err(RegentError::IncoherentExpectedState(format!(
72 "Both 'line' and 'position' are unset. What is the expected state of this file ({}) ?",
73 self.file_path
74 )));
75 }
76 Ok(())
77 }
78}
79
80impl<Handler: HostHandler> AssessCompliance<Handler> for LineInFileBlockExpectedState {
81 async fn assess_compliance(
82 &self,
83 host_handler: &mut Handler,
84 _host_properties: &Option<HostProperties>,
85 privilege: &Privilege,
86 optional_secret_provider: &Option<SecretProvidersPool>,
87 ) -> Result<AttributeComplianceAssessment, RegentError> {
88 if !host_handler
89 .is_this_command_available("sed", &privilege)
90 .unwrap()
91 {
92 return Err(RegentError::FailedDryRunEvaluation(
93 "Sed command not available on this host".to_string(),
94 ));
95 }
96
97 let privilege = privilege.clone();
98
99 let file_exists_check = host_handler
100 .run_command(format!("test -f {}", self.file_path).as_str(), &privilege)
101 .unwrap();
102
103 if file_exists_check.return_code != 0 {
104 return Err(RegentError::FailedDryRunEvaluation(format!(
105 "{} not found, access denied or not a regular file (directory or device ?)",
106 self.file_path
107 )));
108 }
109
110 let line_content: Option<String> = match self.line.clone() {
111 Some(parameter) => Some(parameter.inner_raw(optional_secret_provider).await.unwrap()),
112 None => None,
113 };
114
115 let remediation = match &self.state {
116 LineExpectedState::Present => {
117 let filenumberoflines = host_handler
118 .run_command(
119 format!("wc -l {} | cut -f 1 -d ' '", self.file_path).as_str(),
120 &privilege,
121 )
122 .unwrap()
123 .stdout
124 .trim()
125 .parse::<u64>()
126 .unwrap();
127
128 let parsed_expected_position = match self.line_number {
130 Some(line_number) => Some(LineExpectedPosition::LineNumber(line_number)),
131 None => self.position.clone(),
132 };
133
134 if let Some(LineExpectedPosition::LineNumber(expected_line_number)) =
135 parsed_expected_position
136 {
137 if expected_line_number > filenumberoflines {
138 return Err(RegentError::FailedDryRunEvaluation(
139 "Position value out of range (use \"bottom\" instead)".to_string(),
140 ));
141 }
142 }
143
144 let file_actual_compliance = is_line_present(
145 host_handler,
146 &line_content.clone().unwrap(),
147 &self.file_path,
148 &privilege,
149 );
150
151 match file_actual_compliance {
152 Some(actual_line_numbers) => {
153 match &parsed_expected_position {
155 Some(expected_position) => {
156 match expected_position {
157 LineExpectedPosition::Top => {
158 if actual_line_numbers.contains(&1) {
159 Remediation::None(String::from(
161 "Line already present at expected place",
162 ))
163 } else {
164 Remediation::LineInFile(LineInFileApiCall {
165 api_call: LineInFileModuleInternalApiCall::Add(
166 LineExpectedPosition::Top,
167 ),
168 line_content: self.line.clone(),
169 file_path: self.file_path.clone(),
170 privilege,
171 })
172 }
173 }
174 LineExpectedPosition::Bottom => {
175 if actual_line_numbers.contains(&filenumberoflines) {
176 Remediation::None(String::from(
178 "Line already present at expected place",
179 ))
180 } else {
181 Remediation::LineInFile(LineInFileApiCall {
182 api_call: LineInFileModuleInternalApiCall::Add(
183 LineExpectedPosition::Bottom,
184 ),
185 line_content: self.line.clone(),
186 file_path: self.file_path.clone(),
187 privilege,
188 })
189 }
190 }
191 LineExpectedPosition::LineNumber(specific_line_number) => {
192 if actual_line_numbers.contains(&specific_line_number) {
193 Remediation::None(String::from(
195 "Line already present at expected place",
196 ))
197 } else {
198 Remediation::LineInFile(LineInFileApiCall {
199 api_call: LineInFileModuleInternalApiCall::Add(
200 LineExpectedPosition::LineNumber(
201 *specific_line_number,
202 ),
203 ),
204 line_content: self.line.clone(),
205 file_path: self.file_path.clone(),
206 privilege,
207 })
208 }
209 }
210 LineExpectedPosition::Anywhere => {
211 if actual_line_numbers.len() != 0 {
212 Remediation::None(String::from(
214 "Line already present in the file",
215 ))
216 } else {
217 Remediation::LineInFile(LineInFileApiCall {
218 api_call: LineInFileModuleInternalApiCall::Add(
219 LineExpectedPosition::Bottom,
220 ),
221 line_content: self.line.clone(),
222 file_path: self.file_path.clone(),
223 privilege,
224 })
225 }
226 }
227 }
228 }
229 None => {
230 Remediation::None(format!(
232 "Line already present {:?}",
233 actual_line_numbers
234 ))
235 }
236 }
237 }
238 None => {
239 match &parsed_expected_position {
241 Some(expected_position) => Remediation::LineInFile(LineInFileApiCall {
242 api_call: LineInFileModuleInternalApiCall::Add(
243 expected_position.clone(),
244 ),
245 line_content: self.line.clone(),
246 file_path: self.file_path.clone(),
247 privilege,
248 }),
249 None => {
250 Remediation::LineInFile(LineInFileApiCall {
252 api_call: LineInFileModuleInternalApiCall::Add(
253 LineExpectedPosition::Bottom,
254 ),
255 line_content: self.line.clone(),
256 file_path: self.file_path.clone(),
257 privilege,
258 })
259 }
260 }
261 }
262 }
263 }
264 LineExpectedState::Absent => {
265 match is_line_present(
267 host_handler,
268 &line_content.clone().unwrap(),
269 &self.file_path,
270 &privilege,
271 ) {
272 Some(line_numbers) => Remediation::LineInFile(LineInFileApiCall {
273 api_call: LineInFileModuleInternalApiCall::Delete(line_numbers),
274 line_content: self.line.clone(),
275 file_path: self.file_path.clone(),
276 privilege,
277 }),
278 None => {
279 Remediation::None(String::from("Line already absent"))
281 }
282 }
283 }
284 };
285
286 if let Remediation::None(_message) = remediation {
287 Ok(AttributeComplianceAssessment::Compliant)
288 } else {
289 Ok(AttributeComplianceAssessment::NonCompliant(Vec::from([
290 remediation,
291 ])))
292 }
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct LineInFileApiCall {
298 file_path: String,
299 line_content: Option<Parameter<String>>,
300 pub api_call: LineInFileModuleInternalApiCall,
301 privilege: Privilege,
302}
303
304impl LineInFileApiCall {
305 pub fn display(&self) -> String {
306 match &self.api_call {
307 LineInFileModuleInternalApiCall::Add(line_expected_position) => {
308 return format!(
309 "Line missing -> needs to be added here {:?}",
310 line_expected_position
311 );
312 }
313 LineInFileModuleInternalApiCall::Delete(line_numbers) => {
314 return format!("Line present {:?} -> needs to be removed", line_numbers);
315 }
316 }
317 }
318}
319
320impl<Handler: HostHandler> ReachCompliance<Handler> for LineInFileApiCall {
321 async fn call(
322 &self,
323 host_handler: &mut Handler,
324 _host_properties: &Option<HostProperties>,
325 optional_secret_provider: &Option<SecretProvidersPool>,
326 ) -> Result<InternalApiCallOutcome, RegentError> {
327 match &self.api_call {
328 LineInFileModuleInternalApiCall::Add(line_expected_position) => {
329 let filenumberoflines = host_handler
330 .run_command(
331 format!("wc -l {} | cut -f 1 -d ' '", self.file_path).as_str(),
332 &self.privilege,
333 )
334 .unwrap()
335 .stdout
336 .trim()
337 .parse::<u64>()
338 .unwrap();
339
340 let line_content: Option<String> = match self.line_content.clone() {
347 Some(parameter) => {
348 Some(parameter.inner_raw(optional_secret_provider).await.unwrap())
349 }
350 None => None,
351 };
352
353 let cmd: String;
355 if filenumberoflines == 0 {
356 match line_expected_position {
358 LineExpectedPosition::Top
359 | LineExpectedPosition::Bottom
360 | LineExpectedPosition::Anywhere => {
361 cmd =
362 format!("echo \'{}\' >> {}", line_content.unwrap(), self.file_path);
363 }
364 LineExpectedPosition::LineNumber(1) => {
365 cmd =
366 format!("echo \'{}\' >> {}", line_content.unwrap(), self.file_path);
367 }
368 LineExpectedPosition::LineNumber(_any_other_line_number) => {
369 return Ok(InternalApiCallOutcome::Failure(String::from(
371 "Position value out of range (use \"bottom\" instead)",
372 )));
373 }
374 }
375 } else {
376 let future_line_number = match line_expected_position {
378 LineExpectedPosition::Top => 1,
379 LineExpectedPosition::Bottom | LineExpectedPosition::Anywhere => {
380 filenumberoflines
381 }
382 LineExpectedPosition::LineNumber(specific_line_number) => {
383 *specific_line_number
384 }
385 };
386 cmd = format!(
387 "sed -i \'{} i {}\' {}",
388 future_line_number,
389 line_content.unwrap(),
390 self.file_path
391 );
392 }
393
394 let cmd_result = host_handler
395 .run_command(cmd.as_str(), &self.privilege)
396 .unwrap();
397
398 if cmd_result.return_code == 0 {
399 return Ok(InternalApiCallOutcome::Success(None));
400 } else {
401 return Ok(InternalApiCallOutcome::Failure(format!(
402 "Failed to add line. RC : {}, STDOUT : {}, STDERR : {}",
403 cmd_result.return_code, cmd_result.stdout, cmd_result.stderr
404 )));
405 }
406 }
407 LineInFileModuleInternalApiCall::Delete(line_numbers) => {
408 let formatted_line_numbers = line_numbers
411 .clone()
412 .into_iter()
413 .map(|i| format!("{}d;", i))
414 .collect::<String>();
415 let formatted_line_numbers = formatted_line_numbers
416 .split_at(formatted_line_numbers.len() - 1)
417 .0; let cmd = format!("sed -i \'{}\' {}", formatted_line_numbers, self.file_path);
420 let cmd_result = host_handler
421 .run_command(cmd.as_str(), &self.privilege)
422 .unwrap();
423
424 if cmd_result.return_code == 0 {
425 return Ok(InternalApiCallOutcome::Success(None));
426 } else {
427 return Ok(InternalApiCallOutcome::Failure(format!(
428 "Failed to remove line. RC : {}, STDOUT : {}, STDERR : {}",
429 cmd_result.return_code, cmd_result.stdout, cmd_result.stderr
430 )));
431 }
432 }
433 }
434 }
435}
436
437fn is_line_present<Handler: HostHandler>(
439 host_handler: &mut Handler,
440 line: &String,
441 file_path: &String,
442 privilege: &Privilege,
443) -> Option<Vec<u64>> {
444 let test = host_handler
445 .run_command(
446 format!("grep -n -F -w \'{}\' {}", line, file_path).as_str(), &privilege,
448 )
449 .unwrap();
450
451 if test.return_code == 0 {
452 let mut line_numbers: Vec<u64> = Vec::new();
453 for line in test.stdout.lines() {
454 line_numbers.push(line.split(':').next().unwrap().parse::<u64>().unwrap());
455 }
456 return Some(line_numbers);
457 } else {
458 return None;
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn parsing_lineinfile_module_block_from_yaml_str() {
468 let raw_attributes = "---
469
470- FilePath: /path/to/my/file
471 Line: the first line
472 State: !Present
473 Position: !Top
474
475- FilePath: /path/to/my/file
476 Line: 2nd line
477 State: !Present
478 LineNumber: 2
479
480- FilePath: /path/to/my/file
481 Line: the last line
482 State: !Present
483 Position: !Bottom
484
485- FilePath: /path/to/my/file
486 Line: the content expected not to be present at all
487 State: !Absent
488 ";
489
490 let _attributes: Vec<LineInFileBlockExpectedState> =
491 yaml_serde::from_str(raw_attributes).unwrap();
492 }
493}