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
|
/target
|
||||||
**/*.rs.bk
|
**/*.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"
|
libloading = "0.5.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
|
serde_json = "1.0"
|
||||||
getopts = "0.2"
|
getopts = "0.2"
|
||||||
blurz = "0.4.0"
|
blurz = "0.4.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
globset = "0.3"
|
globset = "0.3"
|
||||||
log = "0.4"
|
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;
|
pub use std::collections::HashMap;
|
||||||
|
|
||||||
use log::{info, trace, warn};
|
|
||||||
|
|
||||||
use crate::message::*;
|
use crate::message::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -14,13 +12,8 @@ pub struct Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Filter {
|
impl Filter {
|
||||||
pub fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
|
pub fn new(config: &serde_yaml::Value) -> Self {
|
||||||
let filter_node = config
|
let include_types = match config.get("include_types") {
|
||||||
.get("filter")
|
|
||||||
.unwrap_or(&serde_yaml::Mapping::new().into())
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let include_types = match filter_node.get("include_types") {
|
|
||||||
Some(v) => v
|
Some(v) => v
|
||||||
.as_sequence()
|
.as_sequence()
|
||||||
.expect("include_types must be a list of strings")
|
.expect("include_types must be a list of strings")
|
||||||
@ -33,7 +26,7 @@ impl Filter {
|
|||||||
.collect(),
|
.collect(),
|
||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
let exclude_types = match filter_node.get("exclude_types") {
|
let exclude_types = match config.get("exclude_types") {
|
||||||
Some(v) => v
|
Some(v) => v
|
||||||
.as_sequence()
|
.as_sequence()
|
||||||
.expect("exclude_types must be a list of strings")
|
.expect("exclude_types must be a list of strings")
|
||||||
@ -47,7 +40,7 @@ impl Filter {
|
|||||||
None => vec![],
|
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"))
|
Some(l) => Level::try_from(l.as_str().expect("Level must be a string"))
|
||||||
.expect("Unknown level name"),
|
.expect("Unknown level name"),
|
||||||
None => Level::Notice,
|
None => Level::Notice,
|
||||||
|
46
src/main.rs
46
src/main.rs
@ -102,24 +102,30 @@ fn main() {
|
|||||||
|
|
||||||
// Instantiate monitor threads and structs
|
// Instantiate monitor threads and structs
|
||||||
let mut mon_threads = vec![];
|
let mut mon_threads = vec![];
|
||||||
for mon_config in config.monitors {
|
for mon_node in config.monitors {
|
||||||
let mon_type = mon_config
|
let mon_type = mon_node
|
||||||
.get("type")
|
.get("type")
|
||||||
.expect("Missing `type` key for monitor")
|
.expect("Missing `type` key for monitor")
|
||||||
.as_str()
|
.as_str()
|
||||||
.expect("Key `type` for monitor is not a string")
|
.expect("Key `type` for monitor is not a string")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
let mon_config = mon_node
|
||||||
|
.get("config")
|
||||||
|
.unwrap_or(&serde_yaml::Mapping::new().into())
|
||||||
|
.clone();
|
||||||
|
|
||||||
let snd = mon_sender.clone();
|
let snd = mon_sender.clone();
|
||||||
let bar = barrier.clone();
|
let bar = barrier.clone();
|
||||||
|
|
||||||
mon_threads.push(thread::spawn(move || {
|
mon_threads.push(thread::spawn(move || {
|
||||||
log::info!("+> monitor: {}", mon_type);
|
log::info!("+> monitor: {}", mon_type);
|
||||||
match monitors::factory(&mon_type, &mon_config) {
|
match monitors::factory(&mon_type, mon_config) {
|
||||||
Ok(mut mon) => loop {
|
Ok(mut mon) => {
|
||||||
bar.wait();
|
bar.wait();
|
||||||
mon.run(&snd);
|
loop {
|
||||||
},
|
mon.run(&snd);
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => log::error!("Cannot instantiate monitor {}: {}", mon_type, e),
|
Err(e) => log::error!("Cannot instantiate monitor {}: {}", mon_type, e),
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -128,30 +134,40 @@ fn main() {
|
|||||||
// Instantiate output threads and structs
|
// Instantiate output threads and structs
|
||||||
let mut output_senders: Vec<mpsc::Sender<Message>> = vec![];
|
let mut output_senders: Vec<mpsc::Sender<Message>> = vec![];
|
||||||
let mut output_threads = vec![];
|
let mut output_threads = vec![];
|
||||||
for out_config in config.outputs {
|
for out_node in config.outputs {
|
||||||
let out_type = out_config
|
let out_type = out_node
|
||||||
.get("type")
|
.get("type")
|
||||||
.expect("Missing `type` key for output")
|
.expect("Missing `type` key for output")
|
||||||
.as_str()
|
.as_str()
|
||||||
.expect("Key `type` for output is not a string")
|
.expect("Key `type` for output is not a string")
|
||||||
.to_owned();
|
.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();
|
let (out_sender, out_receiver) = mpsc::channel();
|
||||||
output_senders.push(out_sender);
|
output_senders.push(out_sender);
|
||||||
|
|
||||||
let filter = Filter::new(&out_config);
|
let filter = Filter::new(&out_filter);
|
||||||
let bar = barrier.clone();
|
let bar = barrier.clone();
|
||||||
|
|
||||||
output_threads.push(thread::spawn(move || {
|
output_threads.push(thread::spawn(move || {
|
||||||
log::info!("+> output: {}", out_type);
|
log::info!("+> output: {}", out_type);
|
||||||
match outputs::factory(&out_type, &out_config) {
|
match outputs::factory(&out_type, out_config) {
|
||||||
Ok(mut output) => loop {
|
Ok(mut output) => {
|
||||||
bar.wait();
|
bar.wait();
|
||||||
let message = out_receiver.recv().unwrap();
|
loop {
|
||||||
if filter.is_message_allowed(&message) {
|
let message = out_receiver.recv().unwrap();
|
||||||
output.process_message(message);
|
if filter.is_message_allowed(&message) {
|
||||||
|
output.process_message(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => log::error!("Cannot instantiate output {}: {}", out_type, e),
|
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 use std::sync::mpsc;
|
||||||
|
|
||||||
pub trait Monitor {
|
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
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn run(&mut self, sender: &mpsc::Sender<Message>);
|
fn run(&mut self, sender: &mpsc::Sender<Message>);
|
||||||
|
@ -5,7 +5,7 @@ use serde::Deserialize;
|
|||||||
extern crate regex;
|
extern crate regex;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
use chrono::{DateTime, Local};
|
use chrono::Local;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DHCPLeases {
|
pub struct DHCPLeases {
|
||||||
@ -21,17 +21,13 @@ struct DHCPLeasesConfig {
|
|||||||
path: String,
|
path: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
mac_rules: HashMap<String, Option<Vec<String>>>,
|
mac_rules: HashMap<String, Option<Vec<String>>>,
|
||||||
|
#[serde(default)]
|
||||||
|
period: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Monitor for DHCPLeases {
|
impl Monitor for DHCPLeases {
|
||||||
fn new(
|
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
config: &HashMap<String, serde_yaml::Value>,
|
let config = serde_yaml::from_value(config)?;
|
||||||
) -> 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)?;
|
|
||||||
|
|
||||||
// Regex compilation
|
// Regex compilation
|
||||||
let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap();
|
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![];
|
let mut unauthorized_macs: Vec<String> = vec![];
|
||||||
|
|
||||||
for cap in self.rgx_lease.captures_iter(&config_content) {
|
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 content = cap.get(2).unwrap().as_str();
|
||||||
|
|
||||||
let mac = self
|
if let Some(mac_cap) = self.rgx_mac.captures(content) {
|
||||||
.rgx_mac
|
let mac = mac_cap.get(1).unwrap().as_str();
|
||||||
.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));
|
|
||||||
|
|
||||||
let now = Local::now().naive_local();
|
let starts_str = self
|
||||||
if starts <= now && now < ends {
|
.rgx_date_start
|
||||||
// Lease is active
|
.captures(content)
|
||||||
if let Some(rules) = self.config.mac_rules.get(mac) {
|
.expect("No 'starts' field found in lease")
|
||||||
// Found rules
|
.get(1)
|
||||||
if let Some(rules_list) = rules {
|
.unwrap()
|
||||||
// Rules contains one or more entries
|
.as_str();
|
||||||
for rule in rules_list {
|
let starts =
|
||||||
if content.find(rule).is_none() {
|
chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S")
|
||||||
unauthorized_macs.push(mac.to_owned());
|
.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
|
let now = Local::now().naive_utc();
|
||||||
.send(Message {
|
if starts <= now && now < ends {
|
||||||
emitter: "dhcp_leases".to_owned(),
|
log::debug!("Found an active DHCP lease for {}", ip);
|
||||||
level: Level::Issue,
|
// Lease is active
|
||||||
msg_type: "dhcp_leases.unauthorized_mac.rule".to_owned(),
|
if let Some(rules) = self.config.mac_rules.get(mac) {
|
||||||
text: format!(
|
// Found rules
|
||||||
"Mismatching rule '{}' for device {}",
|
if let Some(rules_list) = rules {
|
||||||
rule, mac
|
// Rules contains one or more entries
|
||||||
),
|
for rule in rules_list {
|
||||||
})
|
if content.find(rule).is_none() {
|
||||||
.unwrap();
|
unauthorized_macs.push(mac.to_owned());
|
||||||
break;
|
|
||||||
|
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 {
|
||||||
} else {
|
unauthorized_macs.push(mac.to_owned());
|
||||||
unauthorized_macs.push(mac.to_owned());
|
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(Message {
|
.send(Message {
|
||||||
emitter: "dhcp_leases".to_owned(),
|
emitter: "dhcp_leases".to_owned(),
|
||||||
level: Level::Issue,
|
level: Level::Issue,
|
||||||
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
|
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
|
||||||
text: format!("Unauthorized device on network: {}", mac),
|
text: format!("Unknown device {} using IP {}", mac, ip),
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("No 'hardware ethernet' field found for IP {}", ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +141,7 @@ impl Monitor for DHCPLeases {
|
|||||||
.send(Message {
|
.send(Message {
|
||||||
emitter: "dhcp_leases".to_owned(),
|
emitter: "dhcp_leases".to_owned(),
|
||||||
level: Level::Issue,
|
level: Level::Issue,
|
||||||
msg_type: "dhcp_leases.unknown_mac".to_owned(),
|
msg_type: "dhcp_leases.unknown_mac.sumup".to_owned(),
|
||||||
text: format!(
|
text: format!(
|
||||||
"The following macs are not allowed: {:?}",
|
"The following macs are not allowed: {:?}",
|
||||||
unauthorized_macs
|
unauthorized_macs
|
||||||
@ -155,23 +150,9 @@ impl Monitor for DHCPLeases {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// let leases: Vec<(&str, &str)> = lease_rgx
|
std::thread::sleep(std::time::Duration::from_millis(
|
||||||
// .captures_iter(&config_content)
|
(self.config.period.unwrap_or(60.0) * 1000.0) as u64,
|
||||||
// .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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,15 +21,9 @@ struct DummySenderConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Monitor for DummySender {
|
impl Monitor for DummySender {
|
||||||
fn new(
|
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
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: DummySenderConfig =
|
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 {
|
Ok(DummySender {
|
||||||
cnt: 0,
|
cnt: 0,
|
||||||
|
@ -3,16 +3,15 @@ pub mod dummy_sender;
|
|||||||
pub mod wifi_availability;
|
pub mod wifi_availability;
|
||||||
|
|
||||||
use crate::monitor::*;
|
use crate::monitor::*;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub fn factory(
|
pub fn factory(
|
||||||
name: &str,
|
name: &str,
|
||||||
config: &HashMap<String, serde_yaml::Value>,
|
config: serde_yaml::Value,
|
||||||
) -> Result<Box<Monitor>, Box<dyn std::error::Error>> {
|
) -> Result<Box<Monitor>, Box<dyn std::error::Error>> {
|
||||||
match name {
|
match name {
|
||||||
"dummy_sender" => Ok(Box::new(dummy_sender::DummySender::new(&config)?)),
|
"dummy_sender" => Ok(Box::new(dummy_sender::DummySender::new(config)?)),
|
||||||
"wifi_availability" => Ok(Box::new(wifi_availability::WifiAvailability::new(&config)?)),
|
"wifi_availability" => Ok(Box::new(wifi_availability::WifiAvailability::new(config)?)),
|
||||||
"dhcp_leases" => Ok(Box::new(dhcp_leases::DHCPLeases::new(&config)?)),
|
"dhcp_leases" => Ok(Box::new(dhcp_leases::DHCPLeases::new(config)?)),
|
||||||
_ => panic!("Unknown monitor name: {}", name),
|
_ => 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 {
|
impl Monitor for WifiAvailability {
|
||||||
fn new(
|
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
config: &HashMap<String, serde_yaml::Value>,
|
let config = serde_yaml::from_value(config).expect("Invalid config for wifi_availability");
|
||||||
) -> 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");
|
|
||||||
|
|
||||||
let wa = WifiAvailability { config: config };
|
let wa = WifiAvailability { config: config };
|
||||||
// Activate iface
|
// Activate iface
|
||||||
|
@ -3,7 +3,7 @@ pub use std::collections::HashMap;
|
|||||||
use crate::message::*;
|
use crate::message::*;
|
||||||
|
|
||||||
pub trait Output {
|
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
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn process_message(&mut self, message: Message);
|
fn process_message(&mut self, message: Message);
|
||||||
|
@ -15,14 +15,8 @@ struct EspeakConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Output for Espeak {
|
impl Output for Espeak {
|
||||||
fn new(
|
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
config: &HashMap<String, serde_yaml::Value>,
|
let cfg = serde_yaml::from_value(config).expect("Invalid config for Espeak");
|
||||||
) -> 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");
|
|
||||||
|
|
||||||
Ok(Espeak { config: cfg })
|
Ok(Espeak { config: cfg })
|
||||||
}
|
}
|
||||||
|
@ -99,14 +99,8 @@ impl BluetoothLightbulb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Output for BluetoothLightbulb {
|
impl Output for BluetoothLightbulb {
|
||||||
fn new(
|
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
config: &HashMap<String, serde_yaml::Value>,
|
let config: BluetoothLightbulbConfig = serde_yaml::from_value(config)?;
|
||||||
) -> 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)?;
|
|
||||||
|
|
||||||
let msg_types = config
|
let msg_types = config
|
||||||
.msg_types
|
.msg_types
|
||||||
@ -244,7 +238,7 @@ impl Output for BluetoothLightbulb {
|
|||||||
let speed = cfg.speed.unwrap();
|
let speed = cfg.speed.unwrap();
|
||||||
|
|
||||||
// Play animation
|
// Play animation
|
||||||
log::debug!("Playing {:?} {:?}", anim, color);
|
log::debug!("Playing {:?} {:X}", anim, color);
|
||||||
|
|
||||||
for _ in 0..cfg.repeat.unwrap() {
|
for _ in 0..cfg.repeat.unwrap() {
|
||||||
match anim {
|
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 espeak;
|
||||||
pub mod light_beewi_bbl227;
|
pub mod light_beewi_bbl227;
|
||||||
|
pub mod matrix;
|
||||||
pub mod stdout;
|
pub mod stdout;
|
||||||
|
|
||||||
use crate::output::*;
|
use crate::output::*;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub fn factory(
|
pub fn factory(
|
||||||
name: &str,
|
name: &str,
|
||||||
config: &HashMap<String, serde_yaml::Value>,
|
config: serde_yaml::Value,
|
||||||
) -> Result<Box<Output>, Box<dyn std::error::Error>> {
|
) -> Result<Box<Output>, Box<dyn std::error::Error>> {
|
||||||
match name {
|
match name {
|
||||||
"stdout" => Ok(Box::new(stdout::Stdout::new(&config)?)),
|
"stdout" => Ok(Box::new(stdout::Stdout::new(config)?)),
|
||||||
"espeak" => Ok(Box::new(espeak::Espeak::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(
|
"light_beewi_bbl227" => Ok(Box::new(light_beewi_bbl227::BluetoothLightbulb::new(
|
||||||
&config,
|
config,
|
||||||
)?)),
|
)?)),
|
||||||
_ => panic!("Unknown monitor name: {}", name),
|
_ => panic!("Unknown monitor name: {}", name),
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,7 @@ pub use crate::output::*;
|
|||||||
pub struct Stdout {}
|
pub struct Stdout {}
|
||||||
|
|
||||||
impl Output for Stdout {
|
impl Output for Stdout {
|
||||||
fn new(
|
fn new(_config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
_config: &HashMap<String, serde_yaml::Value>,
|
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
|
||||||
Ok(Stdout {})
|
Ok(Stdout {})
|
||||||
}
|
}
|
||||||
fn process_message(&mut self, message: Message) {
|
fn process_message(&mut self, message: Message) {
|
||||||
|
Loading…
Reference in New Issue
Block a user