cypat/
engine.rs

1/*  
2*   SPDX-License-Identifier: GPL-3.0-only
3*   A cyberpatriots scoring engine library
4*   Copyright (C) 2023 Teresa Maria Rivera
5*/
6
7//! The Engine Structure itself
8//! 
9//! This is the main module of this library.
10//! It contains the [`Engine`] type, and some supporting structs and enums, [`AppData`], and [`InstallMethod`].
11//! 
12//! ## Examples
13//! 
14//! Here's an example of a stupidly simple scoring engine.
15//! ```rust
16//! fn main() {
17//!     let mut engine = cypat::Engine::new();
18//!     engine.add_file_vuln("world.txt", move |e, x| -> bool {
19//!         match x {
20//!             Some(file) => {
21//!                 let mut string: std::string::String;
22//!                 std::io::BufReader::new(file.clone()).read_line(&mut string);
23//! 
24//!                 if string == "Hello World" {
25//!                     e.add_score_entry(0, 50, "Wrote Hello World.".to_string());
26//!                     true
27//!                 } else {
28//!                     false
29//!                 }
30//!             },
31//!             None => false,
32//!         }
33//!     });
34//! 
35//!     engine.add_hook(|x| {
36//!         if x.entry_exists(0) {
37//!             x.stop(false);
38//!         }
39//!     });
40//! 
41//!     engine.set_freq(2);
42//!     engine.set_completed_freq(10);
43//!     engine.enter();
44//! }
45//! ```
46
47use std::{
48    fs::File, 
49    string::String, 
50    sync::{
51        atomic::{AtomicBool, AtomicU64, Ordering}, 
52        Arc, 
53        Mutex
54    }, 
55    thread::sleep, 
56    time::Duration
57};
58
59/// Contains package install method.
60#[derive(Clone, Copy)]
61pub enum InstallMethod {
62    Default,
63    PackageManager,
64    #[cfg(target_os = "windows")]
65    WinGet,
66    #[cfg(target_os = "linux")]
67    Snap,
68    #[cfg(target_os = "linux")]
69    Flatpak,
70    ManualInstall,
71}
72
73/// Contains some simple data regarding applications or packages
74/// 
75/// Contains some basic information regarding applications or packages.
76/// Somewhat useful, particularly for looking up package information on Linux.
77#[derive(Clone)]
78pub struct AppData {
79    pub install_method: InstallMethod,
80    pub name: String,
81}
82
83#[derive(Clone)]
84pub(crate) struct UserData {
85    pub(crate) name: String,
86}
87
88pub(crate) enum Condition {
89    FileVuln(String, Box<dyn FnMut(&mut Engine, Option<&mut File>) -> bool + Send + Sync>),
90    AppVuln(AppData, Box<dyn FnMut(&mut Engine, AppData) -> bool + Send + Sync>),
91    UserVuln(UserData, Box<dyn FnMut(&mut Engine, &str) -> bool + Send + Sync>),
92    CustomVuln(Box<dyn FnMut(&mut Engine) -> bool + Send + Sync>),
93}
94
95pub struct Engine {
96    is_running: AtomicBool,
97    score: Arc<Mutex<Vec<(u64, i32, String)>>>,
98    vulns: Arc<Mutex<Vec<(Condition, bool)>>>,
99    incomplete_freq: AtomicU64,
100    complete_freq: AtomicU64,
101    in_execution: AtomicBool,
102    step_iter: AtomicU64,
103}
104
105impl Engine {
106    /// Create a new engine
107    /// 
108    /// Create a new engine, using default values, and no scores or vulnerabilities.
109    pub fn new() -> Engine {
110        Engine {
111            is_running: AtomicBool::new(false),
112            score: Arc::new(Mutex::new(Vec::new())),
113            vulns: Arc::new(Mutex::new(Vec::new())),
114            incomplete_freq: AtomicU64::new(5),
115            complete_freq: AtomicU64::new(10),
116            in_execution: AtomicBool::new(false),
117            step_iter: AtomicU64::new(0),
118        }
119    }
120
121    pub(crate) fn add_vuln(&mut self, vuln: Condition) {
122        match self.vulns.lock() {
123            Ok(mut g) => g.push((vuln, false)),
124            Err(g) => panic!("{}", g),
125        }
126    }
127
128    /// Register a file vulnerability
129    /// 
130    /// Register a file vulnerability.
131    /// This takes the form of a function/closure that takes an [`&mut Engine`][`Engine`], and a [`Option<&mut File>`], and returns a [`bool`].
132    /// 
133    /// If the closure returns true, the vulnerability is interpreted as being completed, it is incomplete.
134    /// More on that in [`Engine::update`] and [`Engine::enter`]
135    pub fn add_file_vuln<F, S>(&mut self, name: S, f: F)
136    where 
137        F: FnMut(&mut Self, Option<&mut File>) -> bool + Send + Sync + 'static, // Whiny ass compiler
138        S: ToString,
139    {
140        self.add_vuln(Condition::FileVuln(name.to_string(), Box::new(f) as Box<dyn FnMut(&mut Self, Option<&mut File>) -> bool + Send + Sync>));
141    }
142
143    /// Register a package/app vulnerability
144    /// 
145    /// Register a package/app vulnerability.
146    /// This takes the form of a function/closure that takes an [`&mut Engine`][`Engine`], and an [`AppData`], and returns a [`bool`].
147    /// 
148    /// If the closure returns true, the vulnerability is interpreted as being completed, it is incomplete.
149    /// More on that in [`Engine::update`] and [`Engine::enter`]    
150    pub fn add_app_vuln<F, S>(&mut self, name: S, install_method: InstallMethod, f: F)
151    where 
152        F: FnMut(&mut Self, AppData) -> bool + Send + Sync + 'static, // Whiny ass compiler
153        S: ToString,
154    {
155        let ad = AppData {
156            name: name.to_string(),
157            install_method: install_method,
158        };
159
160        self.add_vuln(Condition::AppVuln(ad, Box::new(f) as Box<dyn FnMut(&mut Self, AppData) -> bool + Send + Sync>));
161    }
162
163    /// Register a user vulnerability
164    /// 
165    /// Register a user vulnerability.
166    /// This takes the form of a function/closure that takes a [`&mut Engine`][`Engine`], and a [`str`], and returns a [`bool`].
167    /// 
168    /// If the closure returns true, the vulnerability is interpreted as being completed, it is incomplete.
169    /// More on that in [`Engine::update`] and [`Engine::enter`]
170    pub fn add_user_vuln<F, S>(&mut self, name: S, f: F)
171    where 
172        F: FnMut(&mut Self, &str) -> bool + Send + Sync + 'static, // Whiny ass compiler
173        S: ToString,
174    {
175        let ud = UserData {
176            name: name.to_string(),
177        };
178
179        self.add_vuln(Condition::UserVuln(ud, Box::new(f) as Box<dyn FnMut(&mut Self, &str) -> bool + Send + Sync>));
180    }
181
182    /// Register a miscellaneous vulnerability
183    /// 
184    /// Register a miscellaneous vulnerability.
185    /// This takes the form of a function/closure that takes only a [`&mut Engine`][`Engine`], and returns a [`bool`].
186    /// 
187    /// If the closure returns true, the vulnerability is interpreted as being completed, it is incomplete.
188    /// More on that in [`Engine::update`] and [`Engine::enter`]
189    pub fn add_misc_vuln<F>(&mut self, f: F)
190    where
191        F: FnMut(&mut Self) -> bool + Send + Sync + 'static,
192    {
193        self.add_vuln(Condition::CustomVuln(Box::new(f) as Box<dyn FnMut(&mut Self) -> bool + Send + Sync>));
194    }
195
196    /// Register a hook vulnerability
197    /// 
198    /// Register a hook vulnerability, which takes the form of a closure that takes a [`&mut Engine`][`Engine`] as it's only parameter.
199    /// In reality this registers a miscellaneous vulnerability (see [`Engine::add_misc_vuln`]).
200    /// This miscellaneous vulnerability is literally just a call to the hook that discards it's return, and returns false.
201    pub fn add_hook<F, T>(&mut self, f: F)
202    where
203        F: FnMut(&mut Self) -> T + Send + Sync + 'static,
204    {
205        let mut boxed_f = Box::new(f);
206        self.add_misc_vuln(move |x: &mut Engine| {
207            let _ = boxed_f(x);
208            false
209        })
210    }
211
212    /// Sets the frequency in seconds at which the engine is updated.
213    /// 
214    /// Sets the frequency in seconds at which [`Engine::update`] is called, if using [`Engine::enter`].
215    /// 
216    /// This is handled as a private variable called [`incomplete_freq`][`Engine::set_freq`]
217    pub fn set_freq(&mut self, frequency: u64) {
218        self.incomplete_freq.store(frequency, Ordering::SeqCst);
219    }
220
221    /// Sets the frequency in iterations of engine updates that completed vulnerabilities are reviewed.
222    /// 
223    /// Sets the frequency in iterations of engine updates that completed vulnerabilities are re-executed.
224    /// This value is important even if you don't use [`Engine::enter`] because of the way it is interpreted by [`Engine::update`]
225    /// 
226    /// Internally this is handled as a variable called [`complete_freq`][`Engine::set_completed_freq`]
227    pub fn set_completed_freq(&mut self, frequency: u64) {
228        self.complete_freq.store(frequency, Ordering::SeqCst);
229    }
230
231    /// Adds an entry to the score report, with an ID, a score value, and an explanation
232    /// 
233    /// Adds an entry to the score report, with an ID, a score value, and an explanation.
234    /// If an entry exists with the same ID, it instead changes the score and explanation
235    pub fn add_score(&mut self, id: u64, add: i32, reason: String) {
236        match self.score.lock() {
237            Ok(mut g) => { 
238                for s in g.iter_mut() {
239                    if s.0 == id {
240                        s.1 = add;
241                        s.2 = reason;
242                        return;
243                    }
244                }
245
246                g.push((id, add, reason));
247            },
248            Err(g) => panic!("{}", g),
249        }
250    }
251
252    /// Removes the entry identified
253    pub fn remove_score(&mut self, id: u64) -> Result<(), ()> {
254        match self.score.lock() {
255            Ok(mut g) => {
256                for (idx, (id_of_val, _, _)) in (*g).clone().into_iter().enumerate() {
257                    if id_of_val == id {
258                        (*g).remove(idx);
259                        return Ok(());
260                    }
261                }
262
263                Err(())
264            },
265            Err(g) => panic!("{}", g),
266        }
267    }
268
269    /// Generates a list of score entries
270    /// Generates a vector containing the explanation and value of each score entry in order
271    pub fn generate_score_report(&mut self) -> Vec<(String, i32)> {
272        match self.score.lock() {
273            Ok(g) => {
274                let mut report = Vec::with_capacity((*g).len());
275
276                for (_, value, reason) in g.iter() {
277                    report.push((reason.clone(), *value));
278                }
279
280                report
281            },
282            Err(g) => panic!("{}", g),
283        }
284    }
285
286    fn handle_vulnerability(&mut self, vuln: &mut (Condition, bool)) {
287        match &mut vuln.0 {
288            Condition::FileVuln(d, f) => {
289                let pf = File::open(d.clone()).ok();
290
291                match pf {
292                    Some(mut file) => vuln.1 = f(self, Some(&mut file)),
293                    None => vuln.1 = f(self, None),
294                }
295            },
296            Condition::AppVuln(a, f) => {
297                vuln.1 = f(self, a.clone());
298            },
299            Condition::UserVuln(u, f) => {
300                vuln.1 = f(self, u.name.as_str());
301            },
302            Condition::CustomVuln(f) => {
303                vuln.1 = f(self);
304            },
305        }
306    }
307
308    /// Executes vulnerabilites
309    ///
310    /// Incomplete vulnerabilites are excuted each time the function is executed.
311    /// Complete vulnerabilites are excuted only if the number of iterations mod [`complete_freq`][`Engine::set_completed_freq`] is 0
312    pub fn update(&mut self) -> () {
313        self.in_execution.store(true, Ordering::SeqCst);
314        let tmp_vulns = Arc::clone(&self.vulns); 
315        
316        // Neat trick to get out of immutable borrow complaints
317        match tmp_vulns.lock() {
318            Ok(mut vulns) => {
319                for vuln in vulns.iter_mut() {
320                    if self.step_iter.load(Ordering::SeqCst) % self.complete_freq.load(Ordering::SeqCst) == 0 && vuln.1 {
321                        self.handle_vulnerability(vuln);
322                    } else {
323                        self.handle_vulnerability(vuln);
324                    }
325                }
326            },
327            Err(g) => panic!("{}",g)
328        };
329
330        self.in_execution.store(false, Ordering::SeqCst);
331    }
332
333    /// Start engine execution on this thread
334    /// 
335    /// This enters an loop that calls [`Engine::update`] [`incomplete_freq`][`Engine::set_freq`] times per second.
336    /// 
337    /// This state of execution only takes control of one thread, and other threads can generally continue without issue,
338    /// however, new vulnerabilities cannot be added.
339    pub fn enter(&mut self) -> () {
340
341        self.is_running.store(true, Ordering::SeqCst);
342        // TODO: init
343    
344        while self.is_running.load(Ordering::SeqCst) {
345            self.update();
346
347            sleep(Duration::from_secs_f32(1.0/(self.incomplete_freq.load(Ordering::SeqCst) as f32)));
348        }
349    }
350
351    /// Tells the engine to exit.
352    /// 
353    /// This stops engine execution if [`Engine::enter`] was called.
354    /// Otherwise does nothing, unless if `blocking` is set to true.
355    /// If `blocking` is set, it will wait until the current running update stops to return.
356    pub fn stop(&mut self, blocking: bool) -> () {
357        self.is_running.store(false, Ordering::SeqCst);
358
359        while blocking && self.in_execution.load(Ordering::SeqCst) {
360            std::hint::spin_loop(); // TODO: Optimize this shit
361        }
362    }
363
364    /// Calculate a total score
365    /// 
366    /// Calculate the total score for the current engine.
367    pub fn calc_total_score(&self) -> i32 {
368        match self.score.lock() {
369            Ok(guard) => guard.iter().fold(0, |acc, (_, i, _)| acc + i),
370            Err(g) => panic!("{}", g),
371        }
372    }
373
374    /// Get the entry identified by id, if it exists.
375    pub fn get_entry(&self, id: u64) -> Option<(u64, i32, String)> {
376        match self.score.lock() {
377            Ok(guard) => {
378                for i in guard.iter() {
379                    if id == i.0 {
380                        return Some(i.clone())
381                    }
382                }
383
384                None
385            },
386            Err(g) => panic!("{}", g),
387        }
388    }
389
390    /// Checks if the entry identified by id exists
391    pub fn entry_exists(&self, id: u64) -> bool {
392        match self.score.lock() {
393            Ok(guard) => {
394                for i in guard.iter() {
395                    if id == i.0 {
396                        return true;
397                    }
398                }
399
400                false
401            },
402            Err(g) => panic!("{}", g),
403        }
404    }
405}