#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Service {
name: String,
confidence: u8,
product: Option<String>,
version: Option<String>,
extrainfo: Option<String>,
cpe: Vec<String>,
}
impl Service {
pub fn new(name: impl Into<String>, confidence: u8) -> Self {
Self {
name: name.into(),
confidence: confidence.min(100),
product: None,
version: None,
extrainfo: None,
cpe: Vec::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn confidence(&self) -> u8 {
self.confidence
}
pub fn product(&self) -> Option<&str> {
self.product.as_deref()
}
pub fn version(&self) -> Option<&str> {
self.version.as_deref()
}
pub fn extrainfo(&self) -> Option<&str> {
self.extrainfo.as_deref()
}
pub fn cpe(&self) -> &[String] {
&self.cpe
}
pub fn with_product(mut self, product: impl Into<String>) -> Self {
self.product = Some(product.into());
self
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn add_cpe(mut self, cpe: impl Into<String>) -> Self {
let cpe_str = cpe.into();
if !self.cpe.contains(&cpe_str) {
self.cpe.push(cpe_str);
}
self
}
pub fn merge(&mut self, other: Service) {
let higher_confidence = other.confidence > self.confidence;
if higher_confidence {
self.name = other.name;
self.confidence = other.confidence;
if other.product.is_some() {
self.product = other.product;
}
if other.version.is_some() {
self.version = other.version;
}
if other.extrainfo.is_some() {
self.extrainfo = other.extrainfo;
}
} else {
if self.product.is_none() {
self.product = other.product;
}
if self.version.is_none() {
self.version = other.version;
}
if self.extrainfo.is_none() {
self.extrainfo = other.extrainfo;
}
}
for c in other.cpe {
if !self.cpe.contains(&c) {
self.cpe.push(c);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_builder_pattern() {
let srv = Service::new("http", 85)
.with_product("nginx")
.with_version("1.21.0")
.add_cpe("cpe:/a:igor_sysoev:nginx:1.21.0");
assert_eq!(srv.name(), "http");
assert_eq!(srv.confidence(), 85);
assert_eq!(srv.product(), Some("nginx"));
assert_eq!(srv.version(), Some("1.21.0"));
assert_eq!(srv.cpe().len(), 1);
}
#[test]
fn service_confidence_is_clamped_to_100() {
let srv = Service::new("ssh", 101);
assert_eq!(srv.confidence(), 100);
}
#[test]
fn service_merge_lower_confidence_does_not_overwrite_identity() {
let mut srv1 = Service::new("http", 85).with_product("nginx");
let srv2 = Service::new("unknown", 10).with_version("2.0");
srv1.merge(srv2);
assert_eq!(srv1.name(), "http");
assert_eq!(srv1.confidence(), 85);
assert_eq!(srv1.product(), Some("nginx"));
assert_eq!(srv1.version(), Some("2.0"));
}
#[test]
fn service_merge_higher_confidence_overwrites_identity() {
let mut srv1 = Service::new("http", 50).with_product("nginx");
let srv2 = Service::new("http", 100)
.with_product("Apache")
.with_version("2.4");
srv1.merge(srv2);
assert_eq!(srv1.name(), "http");
assert_eq!(srv1.confidence(), 100);
assert_eq!(srv1.product(), Some("Apache"));
assert_eq!(srv1.version(), Some("2.4"));
}
#[test]
fn service_merge_deduplicates_cpes() {
let mut srv1 = Service::new("ssh", 100).add_cpe("cpe:/a:openbsd:openssh");
let srv2 = Service::new("ssh", 100).add_cpe("cpe:/o:linux:linux_kernel");
srv1.merge(srv2);
assert_eq!(srv1.cpe().len(), 2);
}
}