Architecture rework and matrix output
This commit is contained in:
parent
d8ebb16486
commit
2df8a9351f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
|
||||
config.yaml
|
||||
|
1140
Cargo.lock
generated
1140
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,10 +8,14 @@ edition = "2018"
|
||||
libloading = "0.5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.8"
|
||||
serde_json = "1.0"
|
||||
getopts = "0.2"
|
||||
blurz = "0.4.0"
|
||||
regex = "1"
|
||||
chrono = "0.4"
|
||||
globset = "0.3"
|
||||
log = "0.4"
|
||||
simplelog = "0.5"
|
||||
simplelog = "0.5"
|
||||
reqwest = "0.9"
|
||||
percent-encoding = "1.0"
|
||||
rand = "0.6"
|
67
config.example.yaml
Normal file
67
config.example.yaml
Normal file
@ -0,0 +1,67 @@
|
||||
monitors:
|
||||
# - type: wifi_availability
|
||||
# config:
|
||||
# iface: lo
|
||||
# ssid: ""
|
||||
# essid: []
|
||||
# ping_targets: ["127.0.0.1"]
|
||||
# - type: dummy_sender
|
||||
# config:
|
||||
# level: Issue
|
||||
# period: 15.0
|
||||
- type: dummy_sender
|
||||
config:
|
||||
level: Critical
|
||||
period: 10.0
|
||||
# - type: dummy_sender
|
||||
# config:
|
||||
# level: Anomaly
|
||||
# period: 15.0
|
||||
- type: dhcp_leases
|
||||
config:
|
||||
hello: world
|
||||
path: /home/crom/GitProjects/rnetmon/leases.example.txt
|
||||
mac_rules:
|
||||
d4:c9:ef:50:aa:c8: # laptop eth0
|
||||
- 'binding state free'
|
||||
- 'uid "\001@\210\005\355`\205"'
|
||||
- 'set vendor-class-identifier = "android-dhcp-7.1.2"'
|
||||
# 3c:a9:f4:4c:a3:38: # laptop wlan0
|
||||
a0:1b:29:7f:37:9e:
|
||||
- uid "\001\240\033)\1777\236";
|
||||
- set vendor-class-identifier = "udhcp 1.20.2";
|
||||
|
||||
|
||||
outputs:
|
||||
- type: stdout
|
||||
# - type: espeak
|
||||
# - type: light_beewi_bbl227
|
||||
# filter:
|
||||
# include_types: ["*"]
|
||||
# config:
|
||||
# mac:
|
||||
# # ? "84:EB:18:7C:22:68" # Bedroom lightbulb
|
||||
# ? "84:EB:18:7C:1C:F7" # Testing lightbulb
|
||||
|
||||
# msg_types:
|
||||
# "*":
|
||||
# animation: Smooth
|
||||
# repeat: 2
|
||||
# speed: 1.5
|
||||
# color: 0x808080
|
||||
# # wifi.intrusion.*:
|
||||
# # color: 0xff0000
|
||||
# # pattern: blink
|
||||
# # duration: 5.0
|
||||
# # speed: 1.0
|
||||
# # ping.*:
|
||||
# # color: 0xff8000
|
||||
# levels:
|
||||
# Critical:
|
||||
# animation: Blink
|
||||
# color: 0xFF0000
|
||||
# Anomaly:
|
||||
# animation: Bounce
|
||||
# color: 0x00FF00
|
||||
|
||||
|
22
config.yaml
22
config.yaml
@ -1,22 +0,0 @@
|
||||
monitors:
|
||||
# - name: wifi_availability
|
||||
# config:
|
||||
# iface: lo
|
||||
# ssid: ""
|
||||
# essid: []
|
||||
# ping_targets: ["127.0.0.1"]
|
||||
# - name: tester
|
||||
- name: dhcp_leases
|
||||
config:
|
||||
path: /home/crom/GitProjects/rnetmon/leases.example.txt
|
||||
mac_rules:
|
||||
d4:c9:ef:50:aa:c8: # laptop eth0
|
||||
- binding state free
|
||||
- uid "\001@\210\005\355`\205"
|
||||
- set vendor-class-identifier = "android-dhcp-7.1.2"
|
||||
3c:a9:f4:4c:a3:38 # laptop wlan0
|
||||
|
||||
|
||||
outputs:
|
||||
# - name: stdout
|
||||
- name: espeak
|
@ -2,8 +2,6 @@ use globset::{Glob, GlobMatcher};
|
||||
|
||||
pub use std::collections::HashMap;
|
||||
|
||||
use log::{info, trace, warn};
|
||||
|
||||
use crate::message::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -14,13 +12,8 @@ pub struct Filter {
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
|
||||
let filter_node = config
|
||||
.get("filter")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
|
||||
let include_types = match filter_node.get("include_types") {
|
||||
pub fn new(config: &serde_yaml::Value) -> Self {
|
||||
let include_types = match config.get("include_types") {
|
||||
Some(v) => v
|
||||
.as_sequence()
|
||||
.expect("include_types must be a list of strings")
|
||||
@ -33,7 +26,7 @@ impl Filter {
|
||||
.collect(),
|
||||
None => vec![],
|
||||
};
|
||||
let exclude_types = match filter_node.get("exclude_types") {
|
||||
let exclude_types = match config.get("exclude_types") {
|
||||
Some(v) => v
|
||||
.as_sequence()
|
||||
.expect("exclude_types must be a list of strings")
|
||||
@ -47,7 +40,7 @@ impl Filter {
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let level = match filter_node.get("level") {
|
||||
let level = match config.get("level") {
|
||||
Some(l) => Level::try_from(l.as_str().expect("Level must be a string"))
|
||||
.expect("Unknown level name"),
|
||||
None => Level::Notice,
|
||||
|
46
src/main.rs
46
src/main.rs
@ -102,24 +102,30 @@ fn main() {
|
||||
|
||||
// Instantiate monitor threads and structs
|
||||
let mut mon_threads = vec![];
|
||||
for mon_config in config.monitors {
|
||||
let mon_type = mon_config
|
||||
for mon_node in config.monitors {
|
||||
let mon_type = mon_node
|
||||
.get("type")
|
||||
.expect("Missing `type` key for monitor")
|
||||
.as_str()
|
||||
.expect("Key `type` for monitor is not a string")
|
||||
.to_owned();
|
||||
let mon_config = mon_node
|
||||
.get("config")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
|
||||
let snd = mon_sender.clone();
|
||||
let bar = barrier.clone();
|
||||
|
||||
mon_threads.push(thread::spawn(move || {
|
||||
log::info!("+> monitor: {}", mon_type);
|
||||
match monitors::factory(&mon_type, &mon_config) {
|
||||
Ok(mut mon) => loop {
|
||||
match monitors::factory(&mon_type, mon_config) {
|
||||
Ok(mut mon) => {
|
||||
bar.wait();
|
||||
mon.run(&snd);
|
||||
},
|
||||
loop {
|
||||
mon.run(&snd);
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("Cannot instantiate monitor {}: {}", mon_type, e),
|
||||
}
|
||||
}));
|
||||
@ -128,30 +134,40 @@ fn main() {
|
||||
// Instantiate output threads and structs
|
||||
let mut output_senders: Vec<mpsc::Sender<Message>> = vec![];
|
||||
let mut output_threads = vec![];
|
||||
for out_config in config.outputs {
|
||||
let out_type = out_config
|
||||
for out_node in config.outputs {
|
||||
let out_type = out_node
|
||||
.get("type")
|
||||
.expect("Missing `type` key for output")
|
||||
.as_str()
|
||||
.expect("Key `type` for output is not a string")
|
||||
.to_owned();
|
||||
let out_config = out_node
|
||||
.get("config")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
let out_filter = out_node
|
||||
.get("filter")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
|
||||
let (out_sender, out_receiver) = mpsc::channel();
|
||||
output_senders.push(out_sender);
|
||||
|
||||
let filter = Filter::new(&out_config);
|
||||
let filter = Filter::new(&out_filter);
|
||||
let bar = barrier.clone();
|
||||
|
||||
output_threads.push(thread::spawn(move || {
|
||||
log::info!("+> output: {}", out_type);
|
||||
match outputs::factory(&out_type, &out_config) {
|
||||
Ok(mut output) => loop {
|
||||
match outputs::factory(&out_type, out_config) {
|
||||
Ok(mut output) => {
|
||||
bar.wait();
|
||||
let message = out_receiver.recv().unwrap();
|
||||
if filter.is_message_allowed(&message) {
|
||||
output.process_message(message);
|
||||
loop {
|
||||
let message = out_receiver.recv().unwrap();
|
||||
if filter.is_message_allowed(&message) {
|
||||
output.process_message(message);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => log::error!("Cannot instantiate output {}: {}", out_type, e),
|
||||
}
|
||||
}));
|
||||
|
@ -4,7 +4,7 @@ pub use std::collections::HashMap;
|
||||
pub use std::sync::mpsc;
|
||||
|
||||
pub trait Monitor {
|
||||
fn new(config: &HashMap<String, serde_yaml::Value>) -> Result<Self, Box<dyn std::error::Error>>
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>>
|
||||
where
|
||||
Self: Sized;
|
||||
fn run(&mut self, sender: &mpsc::Sender<Message>);
|
||||
|
@ -5,7 +5,7 @@ use serde::Deserialize;
|
||||
extern crate regex;
|
||||
use regex::Regex;
|
||||
extern crate chrono;
|
||||
use chrono::{DateTime, Local};
|
||||
use chrono::Local;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DHCPLeases {
|
||||
@ -21,17 +21,13 @@ struct DHCPLeasesConfig {
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
mac_rules: HashMap<String, Option<Vec<String>>>,
|
||||
#[serde(default)]
|
||||
period: Option<f64>,
|
||||
}
|
||||
|
||||
impl Monitor for DHCPLeases {
|
||||
fn new(
|
||||
config: &HashMap<String, serde_yaml::Value>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config_node = config
|
||||
.get("config")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
let config = serde_yaml::from_value(config_node)?;
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config = serde_yaml::from_value(config)?;
|
||||
|
||||
// Regex compilation
|
||||
let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap();
|
||||
@ -67,77 +63,76 @@ impl Monitor for DHCPLeases {
|
||||
let mut unauthorized_macs: Vec<String> = vec![];
|
||||
|
||||
for cap in self.rgx_lease.captures_iter(&config_content) {
|
||||
let _ip = cap.get(1).unwrap().as_str();
|
||||
let ip = cap.get(1).unwrap().as_str();
|
||||
let content = cap.get(2).unwrap().as_str();
|
||||
|
||||
let mac = self
|
||||
.rgx_mac
|
||||
.captures(content)
|
||||
.expect(&format!(
|
||||
"No 'hardware ethernet' field found for MAC address in {}",
|
||||
content
|
||||
))
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let starts_str = self
|
||||
.rgx_date_start
|
||||
.captures(content)
|
||||
.expect("No 'starts' field found in lease")
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let starts =
|
||||
chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S")
|
||||
.expect(&format!("Bad date format: '{}'", starts_str));
|
||||
let ends_str = self
|
||||
.rgx_date_ends
|
||||
.captures(content)
|
||||
.expect("No 'ends' field found in lease")
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let ends = chrono::naive::NaiveDateTime::parse_from_str(ends_str, "%Y/%m/%d %H:%M:%S")
|
||||
.expect(&format!("Bad date format: '{}'", ends_str));
|
||||
if let Some(mac_cap) = self.rgx_mac.captures(content) {
|
||||
let mac = mac_cap.get(1).unwrap().as_str();
|
||||
|
||||
let now = Local::now().naive_local();
|
||||
if starts <= now && now < ends {
|
||||
// Lease is active
|
||||
if let Some(rules) = self.config.mac_rules.get(mac) {
|
||||
// Found rules
|
||||
if let Some(rules_list) = rules {
|
||||
// Rules contains one or more entries
|
||||
for rule in rules_list {
|
||||
if content.find(rule).is_none() {
|
||||
unauthorized_macs.push(mac.to_owned());
|
||||
let starts_str = self
|
||||
.rgx_date_start
|
||||
.captures(content)
|
||||
.expect("No 'starts' field found in lease")
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let starts =
|
||||
chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S")
|
||||
.expect(&format!("Bad date format: '{}'", starts_str));
|
||||
let ends_str = self
|
||||
.rgx_date_ends
|
||||
.captures(content)
|
||||
.expect("No 'ends' field found in lease")
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let ends =
|
||||
chrono::naive::NaiveDateTime::parse_from_str(ends_str, "%Y/%m/%d %H:%M:%S")
|
||||
.expect(&format!("Bad date format: '{}'", ends_str));
|
||||
|
||||
sender
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unauthorized_mac.rule".to_owned(),
|
||||
text: format!(
|
||||
"Mismatching rule '{}' for device {}",
|
||||
rule, mac
|
||||
),
|
||||
})
|
||||
.unwrap();
|
||||
break;
|
||||
let now = Local::now().naive_utc();
|
||||
if starts <= now && now < ends {
|
||||
log::debug!("Found an active DHCP lease for {}", ip);
|
||||
// Lease is active
|
||||
if let Some(rules) = self.config.mac_rules.get(mac) {
|
||||
// Found rules
|
||||
if let Some(rules_list) = rules {
|
||||
// Rules contains one or more entries
|
||||
for rule in rules_list {
|
||||
if content.find(rule).is_none() {
|
||||
unauthorized_macs.push(mac.to_owned());
|
||||
|
||||
sender
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unauthorized_mac.rule"
|
||||
.to_owned(),
|
||||
text: format!(
|
||||
"Mismatching rule '{}' for device {} at IP {}",
|
||||
rule, mac, ip
|
||||
),
|
||||
})
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unauthorized_macs.push(mac.to_owned());
|
||||
} else {
|
||||
unauthorized_macs.push(mac.to_owned());
|
||||
|
||||
sender
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
|
||||
text: format!("Unauthorized device on network: {}", mac),
|
||||
})
|
||||
.unwrap();
|
||||
sender
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
|
||||
text: format!("Unknown device {} using IP {}", mac, ip),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No 'hardware ethernet' field found for IP {}", ip);
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +141,7 @@ impl Monitor for DHCPLeases {
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unknown_mac".to_owned(),
|
||||
msg_type: "dhcp_leases.unknown_mac.sumup".to_owned(),
|
||||
text: format!(
|
||||
"The following macs are not allowed: {:?}",
|
||||
unauthorized_macs
|
||||
@ -155,23 +150,9 @@ impl Monitor for DHCPLeases {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// let leases: Vec<(&str, &str)> = lease_rgx
|
||||
// .captures_iter(&config_content)
|
||||
// .map(|c| (c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str()))
|
||||
// .collect();
|
||||
// println!("{:?}", leases);
|
||||
// let cap = lease_rgx.captures(config_content);
|
||||
|
||||
// sender
|
||||
// .send(Message {
|
||||
// emitter: "dhcp_leases".to_owned(),
|
||||
// level: 10,
|
||||
// msg_type: "string".to_owned(),
|
||||
// text: format!("frfr"),
|
||||
// })
|
||||
// .unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
(self.config.period.unwrap_or(60.0) * 1000.0) as u64,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,15 +21,9 @@ struct DummySenderConfig {
|
||||
}
|
||||
|
||||
impl Monitor for DummySender {
|
||||
fn new(
|
||||
config: &HashMap<String, serde_yaml::Value>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config_node = config
|
||||
.get("config")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config: DummySenderConfig =
|
||||
serde_yaml::from_value(config_node).expect("Invalid config for dummy_sender");
|
||||
serde_yaml::from_value(config).expect("Invalid config for dummy_sender");
|
||||
|
||||
Ok(DummySender {
|
||||
cnt: 0,
|
||||
|
@ -3,16 +3,15 @@ pub mod dummy_sender;
|
||||
pub mod wifi_availability;
|
||||
|
||||
use crate::monitor::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn factory(
|
||||
name: &str,
|
||||
config: &HashMap<String, serde_yaml::Value>,
|
||||
config: serde_yaml::Value,
|
||||
) -> Result<Box<Monitor>, Box<dyn std::error::Error>> {
|
||||
match name {
|
||||
"dummy_sender" => Ok(Box::new(dummy_sender::DummySender::new(&config)?)),
|
||||
"wifi_availability" => Ok(Box::new(wifi_availability::WifiAvailability::new(&config)?)),
|
||||
"dhcp_leases" => Ok(Box::new(dhcp_leases::DHCPLeases::new(&config)?)),
|
||||
"dummy_sender" => Ok(Box::new(dummy_sender::DummySender::new(config)?)),
|
||||
"wifi_availability" => Ok(Box::new(wifi_availability::WifiAvailability::new(config)?)),
|
||||
"dhcp_leases" => Ok(Box::new(dhcp_leases::DHCPLeases::new(config)?)),
|
||||
_ => panic!("Unknown monitor name: {}", name),
|
||||
}
|
||||
}
|
||||
|
180
src/monitors/nmap_scanner.rs
Normal file
180
src/monitors/nmap_scanner.rs
Normal file
@ -0,0 +1,180 @@
|
||||
pub use crate::message::*;
|
||||
pub use crate::monitor::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
extern crate regex;
|
||||
use regex::Regex;
|
||||
extern crate chrono;
|
||||
use chrono::Local;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NmapScanner {
|
||||
config: NmapScannerConfig,
|
||||
rgx_lease: Regex,
|
||||
rgx_mac: Regex,
|
||||
rgx_date_start: Regex,
|
||||
rgx_date_ends: Regex,
|
||||
}
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
struct NmapScannerConfig {
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
ip_range: Option<String>,
|
||||
}
|
||||
|
||||
impl Monitor for NmapScanner {
|
||||
fn new(
|
||||
config: serde_yaml::Value,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut config = serde_yaml::from_value(config)?;
|
||||
|
||||
if config.ip_range.is_none() {
|
||||
// Detect current subnet
|
||||
|
||||
config.ip_range = Some("192.168.0.1-254");
|
||||
}
|
||||
|
||||
// Regex compilation
|
||||
let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap();
|
||||
let rgx_mac =
|
||||
Regex::new(r"(?m)^\s*hardware\s+ethernet\s([a-f0-9]{2}(?::[a-f0-9]{2}){5})\s*;")
|
||||
.unwrap();
|
||||
let rgx_date_start = Regex::new(r"(?m)^\s*starts\s+\d+\s+(.*?)\s*;").unwrap();
|
||||
let rgx_date_ends = Regex::new(r"(?m)^\s*ends\s+\d+\s+(.*?)\s*;").unwrap();
|
||||
|
||||
Ok(NmapScanner {
|
||||
config: config,
|
||||
rgx_lease: rgx_lease,
|
||||
rgx_mac: rgx_mac,
|
||||
rgx_date_start: rgx_date_start,
|
||||
rgx_date_ends: rgx_date_ends,
|
||||
})
|
||||
}
|
||||
fn run(&mut self, sender: &mpsc::Sender<Message>) {
|
||||
|
||||
nmap -sV -T4 -O -F --version-light 192.168.0.1-254
|
||||
|
||||
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
|
||||
let config_file_path = match self.config.path.len() {
|
||||
0 => "/var/lib/dhcp/dhcpd.leases",
|
||||
_ => &self.config.path,
|
||||
};
|
||||
let mut config_file =
|
||||
File::open(config_file_path).expect("Could not open DHCP leases file");
|
||||
let mut config_content = String::new();
|
||||
config_file
|
||||
.read_to_string(&mut config_content)
|
||||
.expect("Could not read DHCP leases file");
|
||||
|
||||
let mut unauthorized_macs: Vec<String> = vec![];
|
||||
|
||||
for cap in self.rgx_lease.captures_iter(&config_content) {
|
||||
let ip = cap.get(1).unwrap().as_str();
|
||||
let content = cap.get(2).unwrap().as_str();
|
||||
|
||||
if let Some(mac_cap) = self.rgx_mac.captures(content) {
|
||||
let mac = mac_cap.get(1).unwrap().as_str();
|
||||
|
||||
let starts_str = self
|
||||
.rgx_date_start
|
||||
.captures(content)
|
||||
.expect("No 'starts' field found in lease")
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let starts =
|
||||
chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S")
|
||||
.expect(&format!("Bad date format: '{}'", starts_str));
|
||||
let ends_str = self
|
||||
.rgx_date_ends
|
||||
.captures(content)
|
||||
.expect("No 'ends' field found in lease")
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let ends =
|
||||
chrono::naive::NaiveDateTime::parse_from_str(ends_str, "%Y/%m/%d %H:%M:%S")
|
||||
.expect(&format!("Bad date format: '{}'", ends_str));
|
||||
|
||||
let now = Local::now().naive_utc();
|
||||
if starts <= now && now < ends {
|
||||
log::debug!("Found an active DHCP lease for {}", ip);
|
||||
// Lease is active
|
||||
if let Some(rules) = self.config.mac_rules.get(mac) {
|
||||
// Found rules
|
||||
if let Some(rules_list) = rules {
|
||||
// Rules contains one or more entries
|
||||
for rule in rules_list {
|
||||
if content.find(rule).is_none() {
|
||||
unauthorized_macs.push(mac.to_owned());
|
||||
|
||||
sender
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unauthorized_mac.rule"
|
||||
.to_owned(),
|
||||
text: format!(
|
||||
"Mismatching rule '{}' for device {} at IP {}",
|
||||
rule, mac, ip
|
||||
),
|
||||
})
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unauthorized_macs.push(mac.to_owned());
|
||||
|
||||
sender
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
|
||||
text: format!("Unknown device {} using IP {}", mac, ip),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No 'hardware ethernet' field found for IP {}", ip);
|
||||
}
|
||||
}
|
||||
|
||||
if unauthorized_macs.len() > 0 {
|
||||
sender
|
||||
.send(Message {
|
||||
emitter: "dhcp_leases".to_owned(),
|
||||
level: Level::Issue,
|
||||
msg_type: "dhcp_leases.unknown_mac.sumup".to_owned(),
|
||||
text: format!(
|
||||
"The following macs are not allowed: {:?}",
|
||||
unauthorized_macs
|
||||
),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
(self.config.period.unwrap_or(60.0) * 1000.0) as u64,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
lease 192.168.0.26 {
|
||||
starts 4 2018/08/16 22:31:22;
|
||||
ends 4 2018/08/16 23:01:22;
|
||||
tstp 4 2018/08/16 23:01:22;
|
||||
cltt 4 2018/08/16 22:31:22;
|
||||
binding state free;
|
||||
hardware ethernet 84:4b:f5:16:f4:94;
|
||||
}
|
||||
*/
|
@ -17,15 +17,8 @@ struct WifiAvailabilityConfig {
|
||||
}
|
||||
|
||||
impl Monitor for WifiAvailability {
|
||||
fn new(
|
||||
config: &HashMap<String, serde_yaml::Value>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config_node = config
|
||||
.get("config")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
let config =
|
||||
serde_yaml::from_value(config_node).expect("Invalid config for wifi_availability");
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config = serde_yaml::from_value(config).expect("Invalid config for wifi_availability");
|
||||
|
||||
let wa = WifiAvailability { config: config };
|
||||
// Activate iface
|
||||
|
@ -3,7 +3,7 @@ pub use std::collections::HashMap;
|
||||
use crate::message::*;
|
||||
|
||||
pub trait Output {
|
||||
fn new(config: &HashMap<String, serde_yaml::Value>) -> Result<Self, Box<dyn std::error::Error>>
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>>
|
||||
where
|
||||
Self: Sized;
|
||||
fn process_message(&mut self, message: Message);
|
||||
|
@ -15,14 +15,8 @@ struct EspeakConfig {
|
||||
}
|
||||
|
||||
impl Output for Espeak {
|
||||
fn new(
|
||||
config: &HashMap<String, serde_yaml::Value>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let node = config
|
||||
.get("config")
|
||||
.unwrap_or(&serde_yaml::Value::Null)
|
||||
.clone();
|
||||
let cfg = serde_yaml::from_value(node).expect("Invalid config for Espeak");
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let cfg = serde_yaml::from_value(config).expect("Invalid config for Espeak");
|
||||
|
||||
Ok(Espeak { config: cfg })
|
||||
}
|
||||
|
@ -99,14 +99,8 @@ impl BluetoothLightbulb {
|
||||
}
|
||||
|
||||
impl Output for BluetoothLightbulb {
|
||||
fn new(
|
||||
config: &HashMap<String, serde_yaml::Value>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config_node = config
|
||||
.get("config")
|
||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||
.clone();
|
||||
let config: BluetoothLightbulbConfig = serde_yaml::from_value(config_node)?;
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config: BluetoothLightbulbConfig = serde_yaml::from_value(config)?;
|
||||
|
||||
let msg_types = config
|
||||
.msg_types
|
||||
@ -244,7 +238,7 @@ impl Output for BluetoothLightbulb {
|
||||
let speed = cfg.speed.unwrap();
|
||||
|
||||
// Play animation
|
||||
log::debug!("Playing {:?} {:?}", anim, color);
|
||||
log::debug!("Playing {:?} {:X}", anim, color);
|
||||
|
||||
for _ in 0..cfg.repeat.unwrap() {
|
||||
match anim {
|
||||
|
132
src/outputs/matrix.rs
Normal file
132
src/outputs/matrix.rs
Normal file
@ -0,0 +1,132 @@
|
||||
extern crate rand;
|
||||
|
||||
use crate::message::*;
|
||||
pub use crate::output::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::Rng;
|
||||
use reqwest::header;
|
||||
use reqwest::{Client, Url};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MatrixClient {
|
||||
http_client: Client,
|
||||
homeserver_uri: String,
|
||||
token: String,
|
||||
}
|
||||
impl MatrixClient {
|
||||
fn new(homeserver_uri: &str, token: &str) -> Self {
|
||||
Self {
|
||||
http_client: Client::new(),
|
||||
homeserver_uri: homeserver_uri.to_owned(),
|
||||
token: token.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
// fn get(
|
||||
// &self,
|
||||
// path: &str,
|
||||
// params: &HashMap<&str, String>,
|
||||
// ) -> Result<reqwest::Response, reqwest::Error> {
|
||||
// let mut path = self.homeserver_uri.clone() + path;
|
||||
|
||||
// let mut first = true;
|
||||
// for (k, v) in params {
|
||||
// path += &format!(
|
||||
// "{}{}={}",
|
||||
// if first { "?" } else { "&" },
|
||||
// utf8_percent_encode(k.as_ref(), DEFAULT_ENCODE_SET),
|
||||
// utf8_percent_encode(v.as_ref(), DEFAULT_ENCODE_SET)
|
||||
// );
|
||||
// first = false;
|
||||
// }
|
||||
|
||||
// self.http_client
|
||||
// .get(Url::parse(&path).unwrap())
|
||||
// .header(header::AUTHORIZATION, format!("Bearer {}", self.token))
|
||||
// .send()
|
||||
// }
|
||||
fn put(&self, path: &str, body: String) -> reqwest::Response {
|
||||
let path = self.homeserver_uri.clone() + path;
|
||||
|
||||
self.http_client
|
||||
.put(Url::parse(&path).expect(&format!("Invalid path: {}", path)))
|
||||
.header(header::AUTHORIZATION, format!("Bearer {}", self.token))
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(body)
|
||||
.send()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Matrix {
|
||||
client: MatrixClient,
|
||||
config: MatrixConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MatrixConfig {
|
||||
homeserver_url: String,
|
||||
token: String,
|
||||
room_id: String,
|
||||
}
|
||||
|
||||
impl Output for Matrix {
|
||||
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config: MatrixConfig = serde_yaml::from_value(config)?;
|
||||
|
||||
Ok(Matrix {
|
||||
client: MatrixClient::new(&config.homeserver_url, &config.token),
|
||||
config: config,
|
||||
})
|
||||
}
|
||||
fn process_message(&mut self, message: Message) {
|
||||
use serde_json::json;
|
||||
|
||||
let msg_type = match message.level {
|
||||
Level::Debug | Level::Notice => "m.notice",
|
||||
_ => "m.text",
|
||||
};
|
||||
|
||||
let decorator = match message.level {
|
||||
Level::Debug | Level::Notice => ("", ""),
|
||||
Level::Anomaly => ("<font color=\"#FFFF00\">", "</font>"),
|
||||
Level::Issue => ("<font color=\"#FF0000\">", "</font>"),
|
||||
Level::Critical => ("<strong><font color=\"#FF0000\">", "</font></strong>"),
|
||||
};
|
||||
|
||||
let text = match message.level {
|
||||
Level::Critical => format!("@room {}", message.text),
|
||||
_ => message.text,
|
||||
};
|
||||
|
||||
let body = json!({
|
||||
"body": format!("[{}->{}]: {}", message.emitter, message.msg_type, text),
|
||||
"formatted_body": format!("[{}->{}]: {}{}{}", message.emitter, message.msg_type, decorator.0, text, decorator.1),
|
||||
"msgtype": msg_type,
|
||||
"format": "org.matrix.custom.html",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mut res = self.client.put(
|
||||
&format!(
|
||||
"/_matrix/client/r0/rooms/{}/send/m.room.message/{}",
|
||||
utf8_percent_encode(&self.config.room_id, DEFAULT_ENCODE_SET),
|
||||
utf8_percent_encode(
|
||||
&rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(32)
|
||||
.collect::<String>(),
|
||||
DEFAULT_ENCODE_SET
|
||||
),
|
||||
),
|
||||
body,
|
||||
);
|
||||
if res.status().as_u16() != 200 {
|
||||
log::warn!("Could not send Matrix message: {}", res.text().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,20 @@
|
||||
pub mod espeak;
|
||||
pub mod light_beewi_bbl227;
|
||||
pub mod matrix;
|
||||
pub mod stdout;
|
||||
|
||||
use crate::output::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn factory(
|
||||
name: &str,
|
||||
config: &HashMap<String, serde_yaml::Value>,
|
||||
config: serde_yaml::Value,
|
||||
) -> Result<Box<Output>, Box<dyn std::error::Error>> {
|
||||
match name {
|
||||
"stdout" => Ok(Box::new(stdout::Stdout::new(&config)?)),
|
||||
"espeak" => Ok(Box::new(espeak::Espeak::new(&config)?)),
|
||||
"stdout" => Ok(Box::new(stdout::Stdout::new(config)?)),
|
||||
"espeak" => Ok(Box::new(espeak::Espeak::new(config)?)),
|
||||
"matrix" => Ok(Box::new(matrix::Matrix::new(config)?)),
|
||||
"light_beewi_bbl227" => Ok(Box::new(light_beewi_bbl227::BluetoothLightbulb::new(
|
||||
&config,
|
||||
config,
|
||||
)?)),
|
||||
_ => panic!("Unknown monitor name: {}", name),
|
||||
}
|
||||
|
@ -5,9 +5,7 @@ pub use crate::output::*;
|
||||
pub struct Stdout {}
|
||||
|
||||
impl Output for Stdout {
|
||||
fn new(
|
||||
_config: &HashMap<String, serde_yaml::Value>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
fn new(_config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Ok(Stdout {})
|
||||
}
|
||||
fn process_message(&mut self, message: Message) {
|
||||
|
Loading…
Reference in New Issue
Block a user