use std::{collections::BTreeSet, sync::Arc};
pub const MAX_CPES_PER_OS: usize = 50;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OsFingerprint {
pub name: Arc<str>,
pub family: Option<Arc<str>>,
pub generation: Option<Arc<str>>,
pub vendor: Option<Arc<str>>,
pub accuracy: u8,
cpe: BTreeSet<Arc<str>>,
}
impl OsFingerprint {
pub fn new(name: impl Into<Arc<str>>, accuracy: u8) -> Self {
Self {
name: name.into(),
family: None,
generation: None,
vendor: None,
accuracy: accuracy.min(100),
cpe: BTreeSet::new(),
}
}
pub fn add_cpe(&mut self, cpe: impl Into<Arc<str>>) {
if self.cpe.len() < MAX_CPES_PER_OS {
self.cpe.insert(cpe.into());
}
}
pub fn cpes(&self) -> &BTreeSet<Arc<str>> {
&self.cpe
}
pub fn is_highly_confident(&self) -> bool {
self.accuracy >= 85
}
pub fn merge(&mut self, other: OsFingerprint) {
if other.accuracy > self.accuracy {
*self = other;
} else if other.accuracy == self.accuracy {
if self.family.is_none() {
self.family = other.family;
}
if self.generation.is_none() {
self.generation = other.generation;
}
if self.vendor.is_none() {
self.vendor = other.vendor;
}
for cpe in other.cpe {
if self.cpe.len() >= MAX_CPES_PER_OS {
break;
}
self.cpe.insert(cpe);
}
}
}
}
impl std::fmt::Display for OsFingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)?;
if let Some(ref generation) = self.generation {
write!(f, " {}", generation)?;
}
write!(f, " [{}%]", self.accuracy)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn os_accuracy_clamping() {
let os = OsFingerprint::new("Linux", 200);
assert_eq!(os.accuracy, 100);
}
#[test]
fn test_is_highly_confident() {
assert!(OsFingerprint::new("Linux", 85).is_highly_confident());
assert!(!OsFingerprint::new("Linux", 84).is_highly_confident());
}
#[test]
fn os_merge_accuracy_priority() {
let mut os1 = OsFingerprint::new("Linux", 50);
let os2 = OsFingerprint::new("Ubuntu", 90);
os1.merge(os2);
assert_eq!(&*os1.name, "Ubuntu");
}
#[test]
fn os_merge_equal_accuracy_collision() {
let mut os1 = OsFingerprint::new("Linux", 80);
os1.family = Some(Arc::from("Old Family"));
let mut os2 = OsFingerprint::new("Linux", 80);
os2.family = Some(Arc::from("New Family"));
os2.generation = Some(Arc::from("New Gen"));
os1.merge(os2);
assert_eq!(os1.family.as_deref(), Some("Old Family"));
assert_eq!(os1.generation.as_deref(), Some("New Gen"));
}
#[test]
fn cpe_cap_enforcement() {
let mut os = OsFingerprint::new("Windows", 100);
for i in 0..100 {
os.add_cpe(format!("cpe:/o:ident:{}", i));
}
assert_eq!(os.cpes().len(), MAX_CPES_PER_OS);
}
}