use std::collections::HashMap;
pub mod discovery;
pub mod security;
pub mod service;
pub mod set;
pub use discovery::{Discovery, ScanResponse};
pub use security::{CertificateInfo, Security};
pub use service::Service;
pub use set::{PortSet, PortSetParseError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol {
Tcp,
Udp,
Sctp,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PortState {
ClosedFiltered,
Filtered,
Unfiltered,
Closed,
OpenFiltered,
Open,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScriptOutput {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
List(Vec<ScriptOutput>),
Map(HashMap<String, ScriptOutput>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Port {
number: u16,
protocol: Protocol,
state: PortState,
service: Option<Service>,
security: Option<Security>,
discovery: Option<Discovery>,
scripts: Option<HashMap<String, ScriptOutput>>,
}
impl Port {
pub fn new(number: u16, protocol: Protocol, state: PortState) -> Self {
Self {
number,
protocol,
state,
service: None,
security: None,
discovery: None,
scripts: None,
}
}
pub fn number(&self) -> u16 {
self.number
}
pub fn protocol(&self) -> Protocol {
self.protocol
}
pub fn state(&self) -> PortState {
self.state
}
pub fn set_state(&mut self, state: PortState) {
self.state = state;
}
pub fn service(&self) -> Option<&Service> {
self.service.as_ref()
}
pub fn service_name(&self) -> Option<&str> {
self.service.as_ref().map(|s| s.name())
}
pub fn set_service(&mut self, service: Service) {
self.service = Some(service);
}
pub fn security(&self) -> Option<&Security> {
self.security.as_ref()
}
pub fn set_security(&mut self, security: Security) {
self.security = Some(security);
}
pub fn discovery(&self) -> Option<&Discovery> {
self.discovery.as_ref()
}
pub fn scripts(&self) -> Option<&HashMap<String, ScriptOutput>> {
self.scripts.as_ref()
}
pub fn with_service(mut self, service: Service) -> Self {
self.service = Some(service);
self
}
pub fn with_security(mut self, security: Security) -> Self {
self.security = Some(security);
self
}
pub fn with_discovery(mut self, discovery: Discovery) -> Self {
self.discovery = Some(discovery);
self
}
pub fn add_script(mut self, key: impl Into<String>, output: ScriptOutput) -> Self {
let scripts = self.scripts.get_or_insert_with(HashMap::new);
scripts.insert(key.into(), output);
self
}
pub fn merge(&mut self, mut other: Port) {
self.state = std::cmp::max(self.state, other.state);
if let Some(other_service) = other.service {
if let Some(ref mut self_service) = self.service {
self_service.merge(other_service);
} else {
self.service = Some(other_service);
}
}
if let Some(other_security) = other.security {
if let Some(ref mut self_security) = self.security {
self_security.merge(other_security);
} else {
self.security = Some(other_security);
}
}
if other.state >= self.state && other.discovery.is_some() {
self.discovery = other.discovery;
}
if let Some(other_scripts) = other.scripts.take() {
let self_scripts = self.scripts.get_or_insert_with(HashMap::new);
for (key, value) in other_scripts {
self_scripts.insert(key, value);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn port_state_ordering_upgrades_correctly() {
let mut p1 = Port::new(80, Protocol::Tcp, PortState::Filtered);
p1.merge(Port::new(80, Protocol::Tcp, PortState::Open));
assert_eq!(p1.state(), PortState::Open);
let mut p2 = Port::new(53, Protocol::Udp, PortState::OpenFiltered);
p2.merge(Port::new(53, Protocol::Udp, PortState::Open));
assert_eq!(p2.state(), PortState::Open);
let mut p3 = Port::new(443, Protocol::Tcp, PortState::Unfiltered);
p3.merge(Port::new(443, Protocol::Tcp, PortState::Closed));
assert_eq!(p3.state(), PortState::Closed);
}
#[test]
fn structured_scripts_merge_correctly() {
let mut port = Port::new(80, Protocol::Tcp, PortState::Open)
.add_script("http-title", ScriptOutput::String("Index".into()));
let mut ssh_keys = HashMap::new();
ssh_keys.insert("rsa".into(), ScriptOutput::Integer(2048));
ssh_keys.insert("ed25519".into(), ScriptOutput::Integer(256));
let other = Port::new(80, Protocol::Tcp, PortState::Open)
.add_script("ssh-hostkey", ScriptOutput::Map(ssh_keys));
port.merge(other);
let scripts = port.scripts.as_ref().unwrap();
assert_eq!(scripts.len(), 2);
assert!(matches!(
scripts.get("http-title"),
Some(ScriptOutput::String(_))
));
assert!(matches!(
scripts.get("ssh-hostkey"),
Some(ScriptOutput::Map(_))
));
}
#[test]
fn discovery_telemetry_upgrades_on_better_state() {
let disc_filtered = Discovery::new(ScanResponse::NoResponse);
let mut p_filtered =
Port::new(22, Protocol::Tcp, PortState::Filtered).with_discovery(disc_filtered.clone());
let disc_open = Discovery::new(ScanResponse::TcpSynAck);
let p_open =
Port::new(22, Protocol::Tcp, PortState::Open).with_discovery(disc_open.clone());
p_filtered.merge(p_open);
assert_eq!(p_filtered.state(), PortState::Open);
assert_eq!(
p_filtered.discovery().unwrap().reason(),
&ScanResponse::TcpSynAck
);
}
}