first commit

This commit is contained in:
2025-09-17 12:13:02 -04:00
commit e3a5efe56f
25 changed files with 2276 additions and 0 deletions

21
src/arguments.rs Normal file
View File

@@ -0,0 +1,21 @@
use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
#[derive(Subcommand, Deserialize, Serialize, Debug)]
pub enum Mode {
Standard
}
#[derive(Parser, Deserialize, Serialize, Debug)]
#[command(version)]
pub struct Arguments {
#[clap(subcommand)]
pub command: Option<Mode>,
#[arg(short, long)]
pub config_path: Option<String>,
#[arg(short, long)]
pub webhook_url: Option<String>,
#[arg(short, long)]
pub service: Option<String>,
}

59
src/config/linux.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::{fs, process};
pub fn configure_linux_service() {
write_service_file("/etc/systemd/system/watchman.service");
if let Ok(o) = process::Command::new("systemctl")
.arg("daemon-reload")
.output()
{
println!("{}", String::from_utf8_lossy(&o.stdout));
if let Ok(o) = process::Command::new("systemctl")
.arg("enable")
.arg("watchman")
.output()
{
println!("{}", String::from_utf8_lossy(&o.stdout));
if let Ok(o) = process::Command::new("systemctl")
.arg("restart")
.arg("watchman")
.output()
{
println!("{}", String::from_utf8_lossy(&o.stdout));
}
}
}
}
pub fn write_service_file<T: ToString>(path: T) {
if whoami::username().eq_ignore_ascii_case("root") {
if fs::write(path.to_string(), service_file()).is_ok() {
println!(
"{}\n ~~~~~~~ SERVICE CONFIG CREATED ~~~~~~~",
service_file()
);
} else {
println!("{}", "COULD NOT WRITE SERVICE CONFIG TO FILE");
}
} else {
println!(
"{}",
"PLEASE RUN AS ROOT - COULD NOT WRITE SERVICE CONFIG TO FILE"
);
}
}
fn service_file() -> &'static str {
r#"
[Unit]
Description=Watchman
[Service]
Type=simple
User=root
Group=root
ExecStart=/root/.cargo/bin/watchman
[Install]
WantedBy=multi-user.target
"#
}

85
src/config/macos.rs Normal file
View File

@@ -0,0 +1,85 @@
use std::{fs, process};
pub fn configure_macos_service() {
write_launch_file("/Library/LaunchDaemons/com.helloimalemur.watchman.plist");
if let Ok(o) = process::Command::new("launchctl")
.arg("load")
.arg("/Library/LaunchDaemons/com.helloimalemur.watchman.plist")
.output()
{
println!("{}", String::from_utf8_lossy(&o.stdout));
if let Ok(o) = process::Command::new("launchctl")
.arg("start")
.arg("com.helloimalemur.watchman")
.output()
{
println!("{}", String::from_utf8_lossy(&o.stdout));
// if let Ok(o) = process::Command::new("systemctl")
// .arg("restart")
// .arg("watchman")
// .output()
// {
// println!("{}", String::from_utf8_lossy(&o.stdout));
// }
}
}
}
pub fn write_launch_file<T: ToString>(path: T) {
if whoami::username().eq_ignore_ascii_case("root") {
if fs::write(path.to_string(), launch_file()).is_ok() {
println!(
"{}\n ~~~~~~~ SERVICE CONFIG CREATED ~~~~~~~",
launch_file()
);
// sudo chown root:wheel /Library/LaunchDaemons/com.yourname.yourapp.plist
// sudo chmod 644 /Library/LaunchDaemons/com.yourname.yourapp.plist
let _ = process::Command::new("chown")
.arg("root:wheel")
.arg(path.to_string())
.spawn();
let _ = process::Command::new("chmod")
.arg("644")
.arg(path.to_string())
.spawn();
} else {
println!("{}", "COULD NOT WRITE SERVICE CONFIG TO FILE");
}
} else {
println!(
"{}",
"PLEASE RUN AS ROOT - COULD NOT WRITE SERVICE CONFIG TO FILE"
);
}
}
fn launch_file() -> &'static str {
r#"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.helloimalemur.watchman</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/watchman</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/watchman.log</string>
<key>StandardErrorPath</key>
<string>/var/log/watchman_errors.log</string>
</dict>
</plist>
"#
}

21
src/config/mod.rs Normal file
View File

@@ -0,0 +1,21 @@
use std::env::consts::OS;
use std::process;
use crate::config::linux::{configure_linux_service, write_service_file};
use crate::config::macos::configure_macos_service;
mod linux;
mod macos;
pub fn setup_service() {
match OS {
"linux" => {
configure_linux_service();
}
"macos" => {
configure_macos_service();
}
_ => {
println!("WARNING: not implemented for {}", OS);
}
}
}

193
src/fortiwatch.rs Normal file
View File

@@ -0,0 +1,193 @@
use crate::monitors::devices::USBMon;
use crate::monitors::filechanges::FileChanges;
// use crate::monitors::network::NETMon;
use crate::monitors::ssh_burn_file::SSHBurnMon;
use crate::monitors::clients::NewClientMon;
use config::Config;
use std::path::Path;
use std::process::exit;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use crate::arguments::Arguments;
use crate::config::setup_service;
use crate::util::{get_cache_dir, get_config_dir, write_default_blacklist, write_default_config};
pub trait EventMonitor {
async fn check(&mut self);
}
#[derive(Debug)]
pub enum Monitors {
USBMon(USBMon),
// NetMon(NETMon),
SSHBurnMon(SSHBurnMon),
FileChanges(FileChanges),
NewClients(NewClientMon),
}
pub struct AppState {
pub mon_usb: bool,
pub detection_triggered: bool,
pub monitors: Arc<Mutex<Vec<Monitors>>>,
pub settings_map: Config,
}
impl AppState {
pub fn new() -> Self {
AppState {
mon_usb: true,
detection_triggered: false,
monitors: Arc::new(Mutex::new(vec![])),
settings_map: Config::default(),
}
}
pub fn config(&mut self, args: Arguments) {
// check for config file if it doesn't exist write default config
println!("Configuring..");
let cache_dir = get_cache_dir();
let config_dir = get_config_dir();
let mut settings_file_path = format!("{}config/settings.toml", config_dir);
let mut webhook = String::new();
if let Some(wh) = args.webhook_url {
webhook = wh
}
if let Some(service_command) = args.service {
match service_command.as_str() {
"install" => {
println!("Installing..");
setup_service();
if !Path::new(settings_file_path.as_str()).exists() {
println!("Settings.toml does not exist");
crate::util::write_default_config(settings_file_path.clone(), webhook.clone());
exit(0);
}
}
_ => {}
}
}
if let Some(config_path) = args.config_path {
settings_file_path = config_path.to_string();
}
if !Path::new(settings_file_path.as_str()).exists() {
println!("Settings.toml does not exist");
write_default_config(settings_file_path.clone(), webhook);
}
let blacklist_file_path = format!("{}config/file_mon_blacklist", config_dir);
if !Path::new(blacklist_file_path.as_str()).exists() {
println!("file_mon_blacklist does not exist");
write_default_blacklist(blacklist_file_path.clone());
}
let config = Config::builder();
let settings = config
.add_source(config::File::with_name(settings_file_path.as_str()))
.build()
.expect("Configuration error: unable to parse settings");
// initialize monitoring modules
let mut monitors: Vec<Monitors> = vec![];
if settings
.get::<String>("usb_mon_enabled")
.unwrap()
.eq_ignore_ascii_case("true")
{
monitors.push(Monitors::USBMon(USBMon::new(settings.clone())));
}
// if settings
// .get::<String>("net_mon_enabled")
// .unwrap()
// .eq_ignore_ascii_case("true")
// {
// monitors.push(Monitors::NetMon(NETMon::new(settings.clone())));
// }
if settings
.get::<String>("burn_file_mon_enabled")
.unwrap()
.eq_ignore_ascii_case("true")
{
monitors.push(Monitors::SSHBurnMon(SSHBurnMon::new(settings.clone())));
}
if settings
.get::<String>("fs_mon_enabled")
.unwrap()
.eq_ignore_ascii_case("true")
{
monitors.push(Monitors::FileChanges(FileChanges::new(
settings.clone(),
blacklist_file_path,
cache_dir,
)));
}
// Enable new client scanning monitor if configured
if settings
.get::<String>("client_scan_enabled")
.unwrap_or("false".to_string())
.eq_ignore_ascii_case("true")
{
monitors.push(Monitors::NewClients(NewClientMon::new(settings.clone())));
}
self.monitors.clone_from(&Arc::new(Mutex::new(monitors)));
self.settings_map.clone_from(&settings)
}
pub async fn run(&mut self) {
let fs_check_tick = self
.settings_map
.get::<String>("fs_tick_delay_seconds")
.expect("tick_delay_seconds not found in Settings.toml");
let n_fs_check_tick = fs_check_tick
.parse::<u64>()
.expect("unable to parse fs_tick_delay_seconds");
let tick = self
.settings_map
.get::<String>("tick_delay_seconds")
.expect("tick_delay_seconds not found in Settings.toml");
let n_tick = tick
.parse::<u64>()
.expect("unable to parse tick_delay_seconds");
let mut last = SystemTime::now();
loop {
let mut binding = self.monitors.lock();
let bind = binding.as_mut().unwrap();
for i in bind.iter_mut() {
match i {
Monitors::USBMon(e) => {
// println!("{:#?}", e);
e.check().await;
}
// Monitors::NetMon(e) => {
// // println!("{:#?}", e);
// e.check().await;
// }
Monitors::SSHBurnMon(e) => {
// println!("{:#?}", e);
e.check().await;
}
Monitors::FileChanges(e) => {
// println!("{:#?}", e);
let now = SystemTime::now();
let dur_since = now.duration_since(last).unwrap();
if dur_since.as_secs() > n_fs_check_tick {
// println!("fs_tick");
last = SystemTime::now();
e.check().await;
}
}
Monitors::NewClients(e) => {
// println!("{:#?}", e);
e.check().await;
}
}
tokio::time::sleep(Duration::new(n_tick, 0)).await;
// println!("tick");
}
}
}
}

23
src/main.rs Normal file
View File

@@ -0,0 +1,23 @@
use std::env::consts::OS;
use clap::Parser;
use crate::arguments::Arguments;
use crate::fortiwatch::AppState;
use crate::prometheus::Prometheus;
mod fortiwatch;
mod monitors;
mod prometheus;
mod arguments;
mod config;
mod util;
#[tokio::main]
async fn main() {
println!("OS: {}", OS);
let arguments = Arguments::parse();
let mut prom = Prometheus::new();
prom.start().await;
let mut app = AppState::new();
app.config(arguments);
app.run().await;
}

View File

@@ -0,0 +1,82 @@
use std::env::consts::OS;
use crate::monitors::notify::send_discord;
use config::Config;
use std::process::Command;
use std::thread;
use std::time::Duration;
pub async fn reboot_system(settings_map: Config) {
let _res = send_discord("System rebooting", settings_map, &"".to_string()).await;
if let Ok(reboot) = Command::new("reboot").output() {
println!("{:#?}", reboot)
}
}
pub async fn unmount_encrypted_volumes() {
match OS {
"linux" => {
if let Ok(output) = Command::new("dmsetup").arg("ls").output() {
let encrypted_vols = String::from_utf8(output.stdout.to_vec()).unwrap();
for ea in encrypted_vols.lines() {
let vol_split: Vec<_> = ea.split_ascii_whitespace().collect();
let luks_vol = vol_split.first().unwrap().to_string();
// println!("{}", luks_vol);
// println!("{}", command_str);
if let Ok(res) = Command::new("cat").arg("/proc/mounts").output() {
let mount_file_results = String::from_utf8(res.stdout.to_vec()).unwrap();
for mount in mount_file_results.lines() {
let luks_vol_str = luks_vol.as_str();
if mount.contains(luks_vol_str) {
let mount_split: Vec<&str> = mount.split_ascii_whitespace().collect();
let vol_mount_path = mount_split.get(1).unwrap();
// println!("{}", vol_mount_path);
commit_umount_volume(vol_mount_path);
thread::sleep(Duration::new(1, 0));
commit_luks_close(luks_vol_str);
}
}
}
}
}
}
_ => {}
}
}
fn commit_umount_volume(vol_mount_path: &str) {
match OS {
"linux" => {
if let Ok(res) = Command::new("umount").arg(vol_mount_path).output() {
println!("{}", String::from_utf8(res.stdout.to_vec()).unwrap())
}
}
_ => {}
}
}
fn commit_luks_close(luks_vol: &str) {
match OS {
"linux" => {
println!("close luks");
if let Ok(res) = Command::new("cryptsetup")
.arg("luksClose")
.arg(luks_vol)
.output()
{
println!("{}", String::from_utf8(res.stdout.to_vec()).unwrap())
}
}
_ => {}
}
}
// #[cfg(test)]
// mod tests {
// use crate::monitors::actions::unmount_encrypted_volumes;
//
// #[test]
// fn test_unmount_encrypted() {
// let rt = tokio::runtime::Runtime::new();
// rt.unwrap().block_on(unmount_encrypted_volumes());
// }
// }

296
src/monitors/clients.rs Normal file
View File

@@ -0,0 +1,296 @@
use std::collections::{HashMap, HashSet};
use std::process::Command;
use std::time::{Duration, SystemTime};
use std::env::consts::OS;
use crate::fortiwatch::EventMonitor;
use crate::monitors::actions::reboot_system;
use crate::monitors::notify::send_discord;
use anyhow::anyhow;
use config::Config;
use lazy_static::lazy_static;
use prometheus::{register_int_counter, IntCounter};
lazy_static! {
pub static ref SCANS_TOTAL: IntCounter = register_int_counter!(
"watchman_scans_total",
"Total client scans run"
)
.unwrap();
pub static ref FINDINGS_TOTAL: IntCounter = register_int_counter!(
"watchman_findings_total",
"Total scan findings"
)
.unwrap();
pub static ref ACTIONS_TOTAL: IntCounter = register_int_counter!(
"watchman_actions_total",
"Total actions taken on findings"
)
.unwrap();
pub static ref SCANNER_ERRORS_TOTAL: IntCounter = register_int_counter!(
"watchman_scanner_errors_total",
"Total scanner errors"
)
.unwrap();
}
#[derive(Debug)]
pub struct NewClientMon {
settings_map: Config,
known_clients: HashSet<String>,
last_seen: HashMap<String, SystemTime>,
debounce_secs: u64,
}
impl NewClientMon {
pub fn new(settings_map: Config) -> Self {
NewClientMon {
settings_map,
known_clients: HashSet::new(),
last_seen: HashMap::new(),
debounce_secs: 600, // 10 minutes debounce
}
}
}
impl EventMonitor for NewClientMon {
async fn check(&mut self) {
let discovery_method = self
.settings_map
.get::<String>("client_discovery_method")
.unwrap_or("arp".to_string());
let exclusions: Vec<String> = self
.settings_map
.get::<Vec<String>>("client_scan_exclusions")
.unwrap_or(vec![]);
let cur_clients = match discovery_method.as_str() {
"arp" => discover_via_arp(),
// Future: add ping sweep
_ => discover_via_arp(),
};
if cur_clients.is_empty() {
return;
}
let now = SystemTime::now();
for ip in cur_clients {
if exclusions.iter().any(|e| e == &ip) {
continue;
}
let recently_seen = self
.last_seen
.get(&ip)
.and_then(|t| now.duration_since(*t).ok())
.map(|d| d.as_secs() < self.debounce_secs)
.unwrap_or(false);
if !self.known_clients.contains(&ip) || !recently_seen {
self.known_clients.insert(ip.clone());
self.last_seen.insert(ip.clone(), now);
// Trigger scan on add if enabled
let scan_on_add = self
.settings_map
.get::<String>("scan_on_add")
.unwrap_or("true".to_string())
.eq_ignore_ascii_case("true");
if scan_on_add {
if let Err(e) = scan_and_evaluate(ip.clone(), self.settings_map.clone()).await {
eprintln!("scan error: {:?}", e);
SCANNER_ERRORS_TOTAL.inc();
}
}
}
}
}
}
fn discover_via_arp() -> Vec<String> {
let mut ips: Vec<String> = vec![];
match OS {
"windows" => {
if let Ok(out) = Command::new("arp").arg("-a").output() {
let s = String::from_utf8_lossy(&out.stdout).to_string();
for line in s.lines() {
if let Some(ip) = extract_ip(line) {
if !ips.contains(&ip) {
ips.push(ip);
}
}
}
}
}
"macos" => {
if let Ok(out) = Command::new("arp").arg("-an").output() {
let s = String::from_utf8_lossy(&out.stdout).to_string();
for line in s.lines() {
if let Some(ip) = extract_ip(line) {
if !ips.contains(&ip) {
ips.push(ip);
}
}
}
} else if let Ok(out) = Command::new("arp").arg("-a").output() {
let s = String::from_utf8_lossy(&out.stdout).to_string();
for line in s.lines() {
if let Some(ip) = extract_ip(line) {
if !ips.contains(&ip) {
ips.push(ip);
}
}
}
}
}
_ => {
if let Ok(out) = Command::new("ip").arg("neigh").output() {
let s = String::from_utf8_lossy(&out.stdout).to_string();
for line in s.lines() {
if let Some(ip) = extract_ip(line) {
if !ips.contains(&ip) {
ips.push(ip);
}
}
}
} else if let Ok(out) = Command::new("arp").arg("-an").output() {
let s = String::from_utf8_lossy(&out.stdout).to_string();
for line in s.lines() {
if let Some(ip) = extract_ip(line) {
if !ips.contains(&ip) {
ips.push(ip);
}
}
}
}
}
}
ips
}
fn extract_ip(line: &str) -> Option<String> {
// naive parse for dotted IPv4
let mut cur = String::new();
let mut found = None;
for tok in line.split(|c: char| c.is_whitespace() || c == '(' || c == ')' ) {
if tok.chars().filter(|c| *c == '.').count() == 3 {
// simple check
cur = tok.to_string();
if cur.chars().all(|c| c.is_ascii_digit() || c == '.') {
found = Some(cur.clone());
break;
}
}
}
found
}
async fn scan_and_evaluate(ip: String, settings_map: Config) -> Result<(), anyhow::Error> {
SCANS_TOTAL.inc();
let nmap_path = settings_map
.get::<String>("nmap_path")
.unwrap_or("nmap".to_string());
let nmap_profile = settings_map
.get::<String>("nmap_profile")
.unwrap_or("-sV -T4".to_string());
// Build command: nmap [profile] -oX - <ip> without using a shell for cross-platform compatibility
let mut cmd = Command::new(nmap_path);
for arg in nmap_profile.split_whitespace() {
if !arg.is_empty() { cmd.arg(arg); }
}
cmd.arg("-oX").arg("-").arg(&ip);
let output = cmd.output()?;
if !output.status.success() {
SCANNER_ERRORS_TOTAL.inc();
return Err(anyhow!("nmap failed for {}", ip));
}
let xml = String::from_utf8_lossy(&output.stdout).to_string();
let open_ports = parse_open_ports_from_nmap_xml(&xml);
let critical_ports: Vec<String> = settings_map
.get::<Vec<String>>("findings_critical_ports")
.unwrap_or(vec!["23".to_string(), "2323".to_string(), "3389".to_string(), "5900".to_string()]);
let mut critical_hits: Vec<String> = vec![];
for p in &open_ports {
if critical_ports.iter().any(|c| c == p) {
critical_hits.push(p.clone());
}
}
let notify = settings_map
.get::<String>("notify_on_findings")
.unwrap_or("false".to_string())
.eq_ignore_ascii_case("true");
if !open_ports.is_empty() {
FINDINGS_TOTAL.inc();
let summary = format!(
"New client {} scanned. Open ports: {}. Critical: {}",
ip,
open_ports.join(","),
if critical_hits.is_empty() {
"none".to_string()
} else {
critical_hits.join(",")
}
);
if notify {
let _ = send_discord("Client scan findings", settings_map.clone(), &summary).await;
}
let action = settings_map
.get::<String>("action_on_findings")
.unwrap_or("none".to_string())
.to_lowercase();
match action.as_str() {
"reboot" => {
ACTIONS_TOTAL.inc();
reboot_system(settings_map.clone()).await;
}
_ => {}
}
}
Ok(())
}
fn parse_open_ports_from_nmap_xml(xml: &str) -> Vec<String> {
// Very naive XML parsing: look for <port portid="NNN"> ... <state state="open" .../>
let mut ports: Vec<String> = vec![];
let bytes = xml.as_bytes();
let mut i = 0;
while let Some(port_idx) = find_sub(bytes, i, b"<port ") {
if let Some(portid_idx) = find_sub(bytes, port_idx, b"portid=\"") {
let start = portid_idx + 8;
if let Some(end) = find_quote(bytes, start) {
let portid = &xml[start..end];
// search for state="open" before closing port tag
if let Some(state_idx) = find_sub(bytes, portid_idx, b"state=\"open\"") {
if let Some(close_idx) = find_sub(bytes, portid_idx, b"</port>") {
if state_idx < close_idx {
ports.push(portid.to_string());
}
}
}
i = end;
continue;
}
}
i = port_idx + 6;
}
ports
}
fn find_sub(hay: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
hay[from..].windows(needle.len()).position(|w| w == needle).map(|p| p + from)
}
fn find_quote(hay: &[u8], from: usize) -> Option<usize> {
for i in from..hay.len() {
if hay[i] == b'"' { return Some(i); }
}
None
}

182
src/monitors/devices.rs Normal file
View File

@@ -0,0 +1,182 @@
use std::env::consts::OS;
use crate::fortiwatch::EventMonitor;
use crate::monitors::actions::reboot_system;
use crate::monitors::notify::send_discord;
use chrono::Local;
use config::Config;
use std::process::{exit, Command};
use std::fs;
#[derive(Debug)]
pub struct USBMon {
triggered: bool,
devices: Vec<String>,
total_devices: usize,
last_check: usize,
settings_map: Config,
}
impl USBMon {
pub fn new(settings_map: Config) -> Self {
USBMon {
triggered: false,
devices: vec![],
total_devices: 0,
last_check: 0,
settings_map,
}
}
}
impl EventMonitor for USBMon {
async fn check(&mut self) {
let mut dev_change = String::new();
let mut new_devices: Vec<String> = vec![];
let n = get_usb_devices_physical().await;
n.iter().for_each(|r| new_devices.push(r.to_string()));
let d = get_usb_devices().await;
d.iter().for_each(|r| new_devices.push(r.to_string()));
if self.last_check != 0 && self.last_check != new_devices.len() {
self.total_devices = self.devices.len();
match self.last_check < new_devices.len() {
true => {
for entry in new_devices.iter() {
if !self.devices.contains(&entry) {
dev_change = entry.clone();
}
}
self.triggered = true;
println!(
"{} :: Total USB devices INCREASED: {} :: {}",
Local::now(),
self.total_devices,
&dev_change
);
}
false => {
for entry in self.devices.iter() {
if !new_devices.contains(&entry) {
dev_change = entry.clone();
}
}
println!(
"{} :: Total USB devices DECREASED: {} :: {}",
Local::now(),
self.total_devices,
&dev_change
);
}
}
self.devices.clone_from(&new_devices);
self.last_check = self.total_devices;
} else if self.last_check == 0 {
self.devices.clone_from(&new_devices);
self.total_devices = new_devices.len();
self.last_check = self.total_devices;
println!("Starting..");
println!(
"{} :: Total USB devices: {}",
Local::now(),
self.total_devices
);
}
// println!(
// "check usb: {}, count: {}",
// self.triggered, self.total_devices
// );
if self.triggered {
println!("ALERT USB");
usb_triggered(self.settings_map.clone(), &dev_change).await;
println!(
"{} :: USB count: {} :: {}",
Local::now(),
self.total_devices,
&dev_change
);
self.triggered = false;
}
}
}
async fn get_usb_devices() -> Vec<String> {
let mut devices: Vec<String> = vec![];
match OS {
"linux" => {
if let Ok(res) = Command::new("lsusb").output() {
if let Ok(result) = String::from_utf8(res.stdout.to_vec()) {
for r in result.lines() {
if !r.trim().is_empty() {
devices.push(r.trim().to_string());
}
}
}
}
}
"macos" => {
if let Ok(res) = Command::new("system_profiler").arg("SPUSBDataType").output() {
if let Ok(result) = String::from_utf8(res.stdout.to_vec()) {
for r in result.lines() {
if !r.trim().is_empty() {
devices.push(r.trim().to_string());
}
}
}
}
}
_ => {
// Unsupported platform (e.g., Windows): return empty list for safety
}
}
devices
}
async fn get_usb_devices_physical() -> Vec<String> {
let mut devices: Vec<String> = vec![];
match OS {
"linux" => {
if let Ok(contents) = fs::read_to_string("/proc/bus/input/devices") {
for line in contents.lines() {
if line.trim_start().starts_with("S:") {
devices.push(line.to_string());
}
}
}
}
"macos" => {
if let Ok(res) = Command::new("ioreg").args(["-n", "IOHIDSystem", "-r"]).output() {
if let Ok(result) = String::from_utf8(res.stdout.to_vec()) {
for r in result.lines() {
if !r.trim().is_empty() {
devices.push(r.trim().to_string());
}
}
}
}
}
_ => {
// Unsupported platform (e.g., Windows): return empty list for safety
}
}
devices
}
async fn usb_triggered(settings_map: Config, dev_change: &String) {
if settings_map
.get::<String>("reboot_on_increase_of_usb_devices")
.unwrap_or("false".to_string())
.eq_ignore_ascii_case("true")
{
reboot_system(settings_map.clone()).await;
}
if settings_map
.get::<String>("notify_on_increase_of_usb_devices")
.unwrap_or("false".to_string())
.eq_ignore_ascii_case("true")
{
let _ = send_discord("USB triggered", settings_map.clone(), &dev_change).await;
}
}

327
src/monitors/filechanges.rs Normal file
View File

@@ -0,0 +1,327 @@
use crate::fortiwatch::EventMonitor;
use crate::monitors::notify::send_discord;
use chrono::Local;
use config::Config;
use filesystem_hashing::hasher::HashType;
use filesystem_hashing::snapshot::Snapshot;
use filesystem_hashing::{compare_snapshots, create_snapshot};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::{env, fs};
#[allow(unused)]
#[derive(Debug)]
pub struct FileChanges {
triggered: bool,
step: u16,
monitored_directories: Vec<String>,
snapshots: Vec<Snapshot>,
hash_type: HashType,
settings_map: Config,
black_list: Vec<String>,
app_cache_path: String,
}
impl FileChanges {
pub fn new(settings_map: Config, blacklist_file_path: String, app_cache_path: String) -> Self {
let mut file_changes = FileChanges {
triggered: false,
step: 0,
monitored_directories: load_directories(settings_map.clone()),
snapshots: vec![],
hash_type: get_hash_type(settings_map.clone()),
settings_map,
black_list: load_blacklist(blacklist_file_path),
app_cache_path,
};
// load and push blacklisted directories
println!("Blacklisted: {:?}", file_changes.black_list);
// file_changes.load_state();
if file_changes.snapshots.is_empty() {
if let Ok(b) = file_changes
.settings_map
.get::<bool>("fs_mon_path_variable")
{
if b {
let path = env::var("PATH").unwrap_or_else(|_| String::new());
let split = path.split(':').collect::<Vec<&str>>();
for s in split {
if !file_changes.black_list.contains(&s.to_string()) {
if let Ok(snapshot) = create_snapshot(
s,
HashType::BLAKE3,
file_changes.black_list.clone(),
false,
) {
file_changes.snapshots.push(snapshot);
}
}
}
}
}
for dir in &file_changes.monitored_directories {
if !file_changes.black_list.contains(dir) {
if let Ok(snapshot) = create_snapshot(
dir.as_str(),
HashType::BLAKE3,
file_changes.black_list.clone(),
false,
) {
file_changes.snapshots.push(snapshot);
}
}
}
let mut count = 0;
println!("File Count:");
file_changes.snapshots.iter().for_each(|s| {
let s_len = s.file_hashes.lock().unwrap().len();
println!("{} ---- {}", s_len, s.root_path);
count += s_len
});
// println!("{:#?}", file_changes.snapshots);
let message = format!(
"{} :: Filesystem Snapshot Creation Successful\n\nTotal files: {}\n",
Local::now(),
count
);
println!("{}", message);
}
file_changes.save_state();
file_changes
}
fn save_state(&mut self) {
let snapshots_path = format!("{}snapshots/", self.app_cache_path);
// println!("{}", snapshots_path);
let _ = fs::create_dir_all(Path::new(snapshots_path.as_str()));
self.snapshots.iter().for_each(|snapshot: &Snapshot| {
let root_path_hash = blake3::hash(snapshot.root_path.as_bytes()).to_string();
// println!("Exporting: {}", root_path_hash);
let path = format!("{}/snapshots/{}", self.app_cache_path, root_path_hash);
// println!("{:?}", snapshot);
let sn = Snapshot {
file_hashes: Arc::new(Mutex::new(snapshot.file_hashes.lock().unwrap().clone())),
black_list: snapshot.black_list.clone(),
root_path: snapshot.root_path.clone(),
hash_type: HashType::BLAKE3,
uuid: snapshot.uuid.clone(),
date_created: snapshot.date_created.clone(),
};
if filesystem_hashing::export_snapshot(sn, path, true, false).is_err() {
println!("WARNING: could not save state")
}
});
}
fn load_state(&mut self) {
let snapshots_path = format!("{}snapshots/", self.app_cache_path);
let mut snapshots: Vec<Snapshot> = vec![];
let mut count = 0;
if Path::new(snapshots_path.as_str()).exists() {
let dir_vec = walkdir::WalkDir::new(snapshots_path)
.contents_first(true)
.into_iter()
.map(|e| e.unwrap().path().to_str().unwrap().to_string())
.filter(|a| Path::new(a).is_file())
.collect::<Vec<String>>();
// println!("{:?}", dir_vec);
dir_vec.iter().for_each(|dir| {
println!("{}", dir);
let import = filesystem_hashing::import_snapshot(dir.to_string(), false).unwrap();
// println!("{:?}", import);
count += import.file_hashes.lock().unwrap().len();
snapshots.push(import)
});
self.snapshots.clone_from(&snapshots);
println!("State Loaded..{} files", count);
// drop(snapshots)
// println!("{:?}", dir_vec);
}
}
}
impl EventMonitor for FileChanges {
async fn check(&mut self) {
match compare_all_snapshots(self, self.settings_map.clone(), self.black_list.clone()).await
{
None => {
println!("...");
}
Some(e) => match e.0 {
SnapshotChangeType::None => {
// let message = format!("{} :: File System Unchanged",Local::now());
println!(".. no changes ..");
}
SnapshotChangeType::Created => {
// println!("{} :: File Created Alert!\n{:#?}", Local::now(), e.1);
let message = format!(
"{} :: File Creation Detected: {:?}",
Local::now(),
e.1.created
);
fs_changes_alert(message, self.settings_map.clone()).await
}
SnapshotChangeType::Deleted => {
// println!("{} :: File Deleted Alert!\n{:#?}", Local::now(), e.1);
let message = format!(
"{} :: File Deletion Detected: {:?}",
Local::now(),
e.1.deleted
);
fs_changes_alert(message, self.settings_map.clone()).await
}
SnapshotChangeType::Changed => {
// println!("{} :: File Change Alert!\n{:#?}", Local::now(), e.1);
let message = format!(
"{} :: File Change Detected: {:?}",
Local::now(),
e.1.changed
);
fs_changes_alert(message, self.settings_map.clone()).await
}
},
}
self.triggered = false;
self.save_state()
}
}
fn load_directories(settings_map: Config) -> Vec<String> {
let mut dirs: Vec<String> = vec![];
let mon_dirs = settings_map.get::<Vec<String>>("fs_mon_dir").unwrap();
for i in mon_dirs.iter() {
if i.contains('$') {
let env_var = i.replace('$', "");
let env_ret = env::var(env_var).unwrap();
let split = env_ret.split(':').collect::<Vec<&str>>();
split.iter().for_each(|e| dirs.push(e.to_string()))
} else {
dirs.push(i.to_string())
}
}
println!("Monitoring Directories: {:#?}", dirs);
dirs
}
fn get_hash_type(settings_map: Config) -> HashType {
match settings_map
.get::<String>("fs_mon_hash_type")
.unwrap()
.as_str()
{
"blake3" => HashType::BLAKE3,
"SHA3" => HashType::SHA3,
"MD5" => HashType::MD5,
_ => HashType::BLAKE3,
}
}
fn load_blacklist(blacklist_file_path: String) -> Vec<String> {
let mut black_list: Vec<String> = vec![];
if let Ok(file) = fs::read_to_string(Path::new(&blacklist_file_path)) {
for line in file.lines() {
if !line.is_empty() {
black_list.push(line.to_string());
}
}
}
black_list
}
enum SnapshotChangeType {
None,
Created,
Deleted,
Changed,
}
#[derive(Debug)]
pub struct SnapshotCompareResult {
pub created: Vec<String>,
pub deleted: Vec<String>,
pub changed: Vec<String>,
}
async fn compare_all_snapshots(
file_changes: &mut FileChanges,
_settings_map: Config,
black_list: Vec<String>,
) -> Option<(SnapshotChangeType, SnapshotCompareResult)> {
let mut created: Vec<String> = vec![];
let mut deleted: Vec<String> = vec![];
let mut changed: Vec<String> = vec![];
let mut to_remove: Vec<usize> = vec![];
let mut new_sn: Vec<Snapshot> = vec![];
for (ind, i) in file_changes.snapshots.iter().enumerate() {
// println!("{:#?}", black_list);
if let Ok(rehash) =
Snapshot::new(i.root_path.as_ref(), i.hash_type, black_list.clone(), false)
{
if let Some(res) = compare_snapshots(i.clone(), rehash.clone(), false) {
// println!("{}", i.root_path);
for c in res.1.created {
created.push(c)
}
for d in res.1.deleted {
deleted.push(d)
}
for ch in res.1.changed {
changed.push(ch)
}
to_remove.push(ind);
new_sn.push(rehash.clone());
}
}
}
file_changes.snapshots.clear();
file_changes.snapshots = new_sn;
let mut return_type = SnapshotChangeType::None;
if !created.is_empty() {
return_type = SnapshotChangeType::Created;
}
if !deleted.is_empty() {
return_type = SnapshotChangeType::Deleted;
}
if !changed.is_empty() {
return_type = SnapshotChangeType::Changed;
}
Some((
return_type,
SnapshotCompareResult {
created,
deleted,
changed,
},
))
}
async fn fs_changes_alert(message: String, settings_map: Config) {
println!("{}", message);
let _ = send_discord(message.as_str(), settings_map, &"".to_string()).await;
}
#[cfg(test)]
mod tests {
use crate::monitors::filechanges::load_blacklist;
#[test]
fn test_load_blacklist() {
let x = load_blacklist("".to_string());
println!("{:#?}", x);
}
}

7
src/monitors/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod actions;
pub mod devices;
pub mod filechanges;
pub mod network;
pub mod notify;
pub mod ssh_burn_file;
pub mod clients;

67
src/monitors/network.rs Normal file
View File

@@ -0,0 +1,67 @@
use std::sync::Arc;
use crate::fortiwatch::EventMonitor;
use crate::monitors::actions::reboot_system;
use chrono::Local;
use config::Config;
use netstat::{get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo, SocketInfo};
use tokio::sync::Mutex;
#[allow(unused)]
#[derive(Debug)]
pub struct NETMon {
triggered: bool,
interfaces: Vec<String>,
settings_map: Config,
state: Arc<Mutex<Vec<SocketInfo>>>
}
impl NETMon {
pub fn new(settings_map: Config) -> Self {
let af_flags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
let proto_flags = ProtocolFlags::TCP | ProtocolFlags::UDP;
let sockets_info = get_sockets_info(af_flags, proto_flags).unwrap();
let moved_sockets = sockets_info.clone();
for si in moved_sockets {
match si.protocol_socket_info {
ProtocolSocketInfo::Tcp(tcp_si) => println!(
"TCP {}:{} -> {}:{} {:?} - {}",
tcp_si.local_addr,
tcp_si.local_port,
tcp_si.remote_addr,
tcp_si.remote_port,
si.associated_pids,
tcp_si.state
),
ProtocolSocketInfo::Udp(udp_si) => println!(
"UDP {}:{} -> *:* {:?}",
udp_si.local_addr, udp_si.local_port, si.associated_pids
),
}
}
NETMon {
triggered: false,
interfaces: vec![],
settings_map,
state: Arc::new(Mutex::new(sockets_info.clone()))
}
}
}
impl EventMonitor for NETMon {
async fn check(&mut self) {
if let Ok(check) = httping::ping("koonts.net", "", "https", 443).await {
self.triggered = !check
}
if self.triggered {
println!("{} :: ALERT NET", Local::now());
net_alert(self.settings_map.clone()).await;
}
// println!("check net: {}", self.triggered);
}
}
async fn net_alert(settings_map: Config) {
reboot_system(settings_map).await;
}

View File

@@ -0,0 +1,87 @@
use std::env::consts::OS;
use config::Config;
use std::process::Command;
use anyhow::anyhow;
use discord_webhook_lib::DiscordMessage;
pub async fn send_discord(
message: &str,
settings_map: Config,
append: &String,
) -> Result<(), anyhow::Error> {
#[allow(unused)]
let mut final_message = String::new();
final_message = match OS {
"linux" => {
if let Ok(output) = Command::new("hostnamectl").arg("hostname").output() {
if let Ok(hostname) = String::from_utf8(output.stdout) {
format!("{} Hostname: {} :: {}", message, hostname.trim(), append)
} else {
format!("{} Hostname: unknown :: {}", message, append)
}
} else {
format!("{} Hostname: unknown :: {}", message, append)
}
},
"macos" => {
if let Ok(output) = Command::new("hostname").output() {
if let Ok(hostname) = String::from_utf8(output.stdout) {
format!("{} Hostname: {} :: {}", message, hostname.trim(), append)
} else {
format!("{} Hostname: unknown :: {}", message, append)
}
} else {
format!("{} Hostname: unknown :: {}", message, append)
}
}
_ => {
if let Ok(output) = Command::new("hostname").output() {
if let Ok(hostname) = String::from_utf8(output.stdout) {
format!("{} Hostname: {} :: {}", message, hostname.trim(), append)
} else {
format!("{} Hostname: unknown :: {}", message, append)
}
} else {
format!("{} Hostname: unknown :: {}", message, append)
}
}
};
// Default to no-op if webhook is not provided
let discord_webhook_url = settings_map
.get::<String>("discord_webhook_url")
.unwrap_or_default();
if discord_webhook_url.trim().is_empty() {
return Ok(()); // Disabled by default if not configured
}
let discord_webhook_avatar_name = settings_map
.get::<String>("discord_webhook_avatar_name")
.unwrap_or_else(|_| "Watchman".to_string());
let mut message = DiscordMessage::builder(discord_webhook_url);
message.add_message(final_message);
message.add_field("username", discord_webhook_avatar_name);
let final_message = message.build();
if final_message.send().await.is_ok() {
Ok(())
} else {
Err(anyhow!("Failed to send discord message"))
}
}
// pub async fn send_email() {}
// #[cfg(test)]
// mod tests {
// use discord_webhook_lib::send_discord;
//
// #[test]
// fn test_send_wh() {
// let rt = tokio::runtime::Runtime::new();
// rt.unwrap().block_on(send_discord(
// "",
// "Hello World",
// "Lazarus"
// )).unwrap();
// }
// }

View File

@@ -0,0 +1,84 @@
use crate::fortiwatch::EventMonitor;
use crate::monitors::actions::unmount_encrypted_volumes;
use chrono::{DateTime, Utc};
use config::Config;
use std::process::Command;
#[allow(unused)]
#[derive(Debug)]
pub struct SSHBurnMon {
triggered: bool,
settings_map: Config,
last_check: DateTime<Utc>,
}
impl SSHBurnMon {
pub fn new(settings_map: Config) -> Self {
SSHBurnMon {
triggered: false,
settings_map,
last_check: Utc::now(),
}
}
async fn ssh_burn_triggered(&self) {
if self
.settings_map
.get::<String>("unmount_crypt_on_file_burn")
.unwrap()
.eq_ignore_ascii_case("true")
{
unmount_encrypted_volumes().await;
}
}
}
impl EventMonitor for SSHBurnMon {
async fn check(&mut self) {
let ssh_check_burn_check_interval = self
.settings_map
.get::<String>("ssh_check_burn_check_interval")
.unwrap()
.parse::<i64>()
.unwrap();
if Utc::now()
.signed_duration_since(self.last_check)
.num_seconds()
> ssh_check_burn_check_interval
{
let ssh_check_burn_host = self
.settings_map
.get::<String>("ssh_check_burn_host")
.unwrap();
let ssh_check_burn_user = self
.settings_map
.get::<String>("ssh_check_burn_user")
.unwrap();
let ssh_check_burn_key = self
.settings_map
.get::<String>("ssh_check_burn_key")
.unwrap();
let ssh_check_burn_path = self
.settings_map
.get::<String>("ssh_check_burn_path")
.unwrap();
let addr = format!("{}@{}", ssh_check_burn_user, ssh_check_burn_host);
let command_str = format!(
"if [ -f {} ]; then cat {}; fi",
ssh_check_burn_path, ssh_check_burn_path
);
if let Ok(result) = Command::new("ssh")
.arg("-i")
.arg(ssh_check_burn_key)
.arg(addr)
.arg(command_str)
.output()
{
let burn_contents = String::from_utf8(result.stdout).unwrap();
if burn_contents.eq_ignore_ascii_case("burn") {
self.ssh_burn_triggered().await;
}
}
self.last_check = Utc::now();
}
}
}

45
src/prometheus/mod.rs Normal file
View File

@@ -0,0 +1,45 @@
use actix_web::http::header;
use actix_web::{get, web, App, Error, HttpResponse, HttpServer, Responder};
use prometheus::{Encoder, TextEncoder};
use std::thread;
pub struct Prometheus {}
impl Prometheus {
pub fn new() -> Self {
Prometheus {}
}
pub async fn start(&self) {
let _handle = thread::spawn(|| {
let rt = tokio::runtime::Runtime::new();
let _ = rt.unwrap().block_on(
HttpServer::new(|| App::new().service(metrics))
.bind(("127.0.0.1", 8323))
.unwrap()
.run(),
);
});
// let _ = handle.join();
}
}
#[get("/metrics")]
async fn metrics() -> Result<HttpResponse, Error> {
let encoder = TextEncoder::new();
// HTTP_COUNTER.inc();
// let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["all"]).start_timer();
let metric_families = prometheus::gather();
let mut buffer = vec![];
encoder.encode(&metric_families, &mut buffer).unwrap();
// HTTP_BODY_GAUGE.set(buffer.len() as f64);
let response = String::from_utf8(buffer.clone()).unwrap();
buffer.clear();
// timer.observe_duration();
Ok(HttpResponse::Ok()
.insert_header(header::ContentType(mime::TEXT_PLAIN))
.body(response))
}

224
src/util/mod.rs Normal file
View File

@@ -0,0 +1,224 @@
use std::env::consts::OS;
use std::fs;
use std::path::Path;
pub fn get_cache_dir() -> String {
#[allow(unused)]
let mut cache_dir = String::new();
let cur_user = whoami::username();
match OS {
"linux" => {
if cur_user.eq_ignore_ascii_case("root") {
let _ = fs::create_dir_all(Path::new("/root/.config/watchman/config/"));
"/root/.config/watchman/".to_string()
} else {
let create_dir = format!("/home/{}/.config/watchman/config/", cur_user);
let _ = fs::create_dir_all(Path::new(create_dir.as_str()));
format!("/home/{}/.config/watchman/", cur_user)
}
}
"macos" => {
// /var/root/Library/Caches/com.helloimalemur.watchman/
if cur_user.eq_ignore_ascii_case("root") {
let _ = fs::create_dir_all(Path::new("/var/root/Library/Caches/com.helloimalemur.watchman/config/"));
"/var/root/Library/Caches/com.helloimalemur.watchman/".to_string()
} else {
let create_dir = format!("/Users/{}/Library/Caches/com.helloimalemur.watchman/config/", cur_user);
let _ = fs::create_dir_all(Path::new(create_dir.as_str()));
format!("/Users/{}/Library/Caches/com.helloimalemur.watchman/", cur_user)
}
}
"windows" => {
// Prefer LOCALAPPDATA for cache
if let Ok(local_appdata) = std::env::var("LOCALAPPDATA") {
let base = format!("{}\\Watchman\\", local_appdata);
let _ = fs::create_dir_all(Path::new(format!("{}config\\", base).as_str()));
base
} else {
let base = format!("C:\\Users\\{}\\AppData\\Local\\Watchman\\", cur_user);
let _ = fs::create_dir_all(Path::new(format!("{}config\\", base).as_str()));
base
}
}
_ => {
// Fallback to current directory for unknown OS
let _ = fs::create_dir_all(Path::new("./config/"));
"./".to_string()
}
}
}
pub fn get_config_dir() -> String {
#[allow(unused)]
let mut cache_dir = String::new();
let cur_user = whoami::username();
match OS {
"linux" => {
if cur_user.eq_ignore_ascii_case("root") {
let _ = fs::create_dir_all(Path::new("/root/.config/watchman/config/"));
"/root/.config/watchman/".to_string()
} else {
let create_dir = format!("/home/{}/.config/watchman/config/", cur_user);
let _ = fs::create_dir_all(Path::new(create_dir.as_str()));
format!("/home/{}/.config/watchman/", cur_user)
}
}
"macos" => {
// /var/root/Library/Application\ Support/com.helloimalemur.watchman/
if cur_user.eq_ignore_ascii_case("root") {
let _ = fs::create_dir_all(Path::new("/var/root/Library/Application\\ Support/com.helloimalemur.watchman/config/"));
"/var/root/Library/Application\\ Support/com.helloimalemur.watchman/".to_string()
} else {
let create_dir = format!("/Users/{}/Library/Application\\ Support/com.helloimalemur.watchman/config/", cur_user);
let _ = fs::create_dir_all(Path::new(create_dir.as_str()));
format!("/Users/{}/Library/Application\\ Support/com.helloimalemur.watchman/", cur_user)
}
}
"windows" => {
// Prefer APPDATA (Roaming) for config
if let Ok(appdata) = std::env::var("APPDATA") {
let base = format!("{}\\Watchman\\", appdata);
let _ = fs::create_dir_all(Path::new(format!("{}config\\", base).as_str()));
base
} else {
let base = format!("C:\\Users\\{}\\AppData\\Roaming\\Watchman\\", cur_user);
let _ = fs::create_dir_all(Path::new(format!("{}config\\", base).as_str()));
base
}
}
_ => {
// Fallback to current directory for unknown OS
let _ = fs::create_dir_all(Path::new("./config/"));
"./".to_string()
}
}
}
pub fn write_default_blacklist<T: ToString>(path: T) {
let _ = fs::write(path.to_string(), default_blacklist());
}
fn default_blacklist() -> &'static str {
r#"
/etc/mtab
/etc/cups
"#
}
pub fn write_default_config<T: ToString>(path: T, webhook: String) {
if fs::write(
path.to_string(),
default_config().replace("https://discord.com/api/webhooks/", webhook.as_str()),
)
.is_ok()
{
println!(
"{}\n ~~~~~~~ UNABLE TO LOCATE CONFIG FILE - DEFAULT CONFIG CREATED ~~~~~~~",
default_config().replace("https://discord.com/api/webhooks/", webhook.as_str())
);
}
}
fn default_config() -> &'static str {
match OS {
"linux" => {
r#"
## General settings
tick_delay_seconds = "5"
fs_tick_delay_seconds = "300"
### File System Integrity
fs_mon_path_variable = true
fs_mon_enabled = "true"
fs_mon_dir = ["/etc", "/bin", "$PATH"]
fs_mon_hash_type = "blake3"
### USB Monitor
usb_mon_enabled = "true"
reboot_on_increase_of_usb_devices = "false"
notify_on_increase_of_usb_devices = "false"
unmount_crypt_on_increase_of_usb_devices = "true"
### Burn File Monitor
burn_file_mon_enabled = "false"
unmount_crypt_on_file_burn = "true"
ssh_check_burn_host = "hostname"
ssh_check_burn_user = "root"
ssh_check_burn_key = "/home/user/.ssh/id_rsa"
ssh_check_burn_path = "/root/.config/burn"
ssh_check_burn_check_interval = "30"
burn_path_1 = "/root/test/"
### Network Monitor
net_mon_enabled = "false"
### Client Discovery & Scan
client_scan_enabled = "false"
client_discovery_method = "arp" # arp | ping
client_scan_exclusions = ["127.0.0.1", "localhost"]
scan_on_add = "true"
nmap_path = "nmap"
nmap_profile = "-sV -T4"
findings_critical_ports = ["23", "2323", "3389", "5900"]
notify_on_findings = "false"
action_on_findings = "none" # none | reboot
######## Notification settings
discord_webhook_url = "https://discord.com/api/webhooks/"
discord_webhook_avatar_name = "Lazarus"
"#
}
"macos" => {
r#"
## General settings
tick_delay_seconds = "5"
fs_tick_delay_seconds = "300"
### File System Integrity
fs_mon_path_variable = true
fs_mon_enabled = "true"
fs_mon_dir = ["/etc", "/bin", "$PATH"]
fs_mon_hash_type = "blake3"
### USB Monitor
usb_mon_enabled = "true"
reboot_on_increase_of_usb_devices = "false"
notify_on_increase_of_usb_devices = "true"
unmount_crypt_on_increase_of_usb_devices = "true"
### Burn File Monitor
burn_file_mon_enabled = "false"
unmount_crypt_on_file_burn = "true"
ssh_check_burn_host = "hostname"
ssh_check_burn_user = "root"
ssh_check_burn_key = "/User/user/.ssh/id_rsa"
ssh_check_burn_path = "/var/root/.config/burn"
ssh_check_burn_check_interval = "30"
burn_path_1 = "/var/root/test/"
### Network Monitor
net_mon_enabled = "false"
### Client Discovery & Scan
client_scan_enabled = "false"
client_discovery_method = "arp" # arp | ping
client_scan_exclusions = ["127.0.0.1", "localhost"]
scan_on_add = "true"
nmap_path = "nmap"
nmap_profile = "-sV -T4"
findings_critical_ports = ["23", "2323", "3389", "5900"]
notify_on_findings = "true"
action_on_findings = "none" # none | reboot
######## Notification settings
discord_webhook_url = "https://discord.com/api/webhooks/"
discord_webhook_avatar_name = "Lazarus"
"#
}
_ => {
panic!("Unsupported OS: {}", OS);
}
}
}