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}