use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Deserialize)]
pub struct ServiceRule {
pub id: String,
pub service: String,
pub protocol: String,
pub common_ports: Vec<u16>,
pub patterns: Vec<String>,
pub version_pattern: Option<String>,
pub security_signals: Vec<String>,
pub priority: u8,
}
#[derive(Debug, Deserialize)]
struct ServiceRulesFile {
#[serde(default)]
rule: Vec<ServiceRule>,
}
pub fn load_from_toml<P: AsRef<Path>>(path: P) -> Result<Vec<ServiceRule>, anyhow::Error> {
let body = std::fs::read_to_string(path.as_ref())?;
let parsed: ServiceRulesFile = toml::from_str(&body)?;
Ok(parsed.rule)
}
pub fn builtin_plus<P: AsRef<Path>>(custom_path: P) -> Vec<ServiceRule> {
let mut out = load_from_toml(custom_path).unwrap_or_default();
out.extend(builtin_rules());
out
}
#[derive(Debug, Clone)]
pub struct ServiceMatch {
pub rule_id: String,
pub service: String,
pub version: Option<String>,
pub confidence: f32,
pub signals: Vec<String>,
pub metadata: HashMap<String, String>,
}
pub fn builtin_rules() -> Vec<ServiceRule> {
let mut out = vec![
ServiceRule {
id: "http-apache".into(),
service: "Apache HTTP Server".into(),
protocol: "tcp".into(),
common_ports: vec![80, 443, 8080, 8443],
patterns: vec![
"Apache/".into(),
"Server: Apache".into(),
],
version_pattern: Some(r"Apache/(\d+\.\d+\.\d+)".into()),
security_signals: vec![
"server-version-disclosure".into(),
],
priority: 10,
},
ServiceRule {
id: "http-nginx".into(),
service: "nginx".into(),
protocol: "tcp".into(),
common_ports: vec![80, 443, 8080, 8443],
patterns: vec![
"nginx/".into(),
"Server: nginx".into(),
],
version_pattern: Some(r"nginx/(\d+\.\d+\.\d+)".into()),
security_signals: vec![
"server-version-disclosure".into(),
],
priority: 10,
},
ServiceRule {
id: "http-iis".into(),
service: "Microsoft IIS".into(),
protocol: "tcp".into(),
common_ports: vec![80, 443, 8080],
patterns: vec![
"Microsoft-IIS/".into(),
"Server: Microsoft-IIS".into(),
],
version_pattern: Some(r"Microsoft-IIS/(\d+\.\d+)".into()),
security_signals: vec![
"server-version-disclosure".into(),
],
priority: 10,
},
ServiceRule {
id: "ssh-openssh".into(),
service: "OpenSSH".into(),
protocol: "tcp".into(),
common_ports: vec![22, 2222],
patterns: vec![
"SSH-2.0-OpenSSH".into(),
"SSH-1.99-OpenSSH".into(),
],
version_pattern: Some(r"OpenSSH[_\s](\d+\.\d+\S*)".into()),
security_signals: vec![
"ssh-version-disclosure".into(),
],
priority: 10,
},
ServiceRule {
id: "ssh-dropbear".into(),
service: "Dropbear SSH".into(),
protocol: "tcp".into(),
common_ports: vec![22, 2222],
patterns: vec!["dropbear".into()],
version_pattern: Some(r"dropbear[_\s](\d+\.\d+)".into()),
security_signals: vec!["ssh-version-disclosure".into()],
priority: 8,
},
ServiceRule {
id: "ftp-vsftpd".into(),
service: "vsftpd".into(),
protocol: "tcp".into(),
common_ports: vec![21],
patterns: vec!["vsftpd".into(), "vsFTPd".into()],
version_pattern: Some(r"vsftpd\s+(\d+\.\d+\.\d+)".into()),
security_signals: vec!["ftp-version-disclosure".into()],
priority: 10,
},
ServiceRule {
id: "ftp-proftpd".into(),
service: "ProFTPD".into(),
protocol: "tcp".into(),
common_ports: vec![21],
patterns: vec!["ProFTPD".into()],
version_pattern: Some(r"ProFTPD\s+(\d+\.\d+\.\d+\S*)".into()),
security_signals: vec!["ftp-version-disclosure".into()],
priority: 10,
},
ServiceRule {
id: "mysql".into(),
service: "MySQL".into(),
protocol: "tcp".into(),
common_ports: vec![3306],
patterns: vec![
"mysql_native_password".into(),
"MariaDB".into(),
"\x00\x00\x00\x0a".into(), ],
version_pattern: Some(r"(\d+\.\d+\.\d+)".into()),
security_signals: vec![
"database-exposed".into(),
"version-disclosure".into(),
],
priority: 10,
},
ServiceRule {
id: "postgresql".into(),
service: "PostgreSQL".into(),
protocol: "tcp".into(),
common_ports: vec![5432],
patterns: vec!["PostgreSQL".into()],
version_pattern: Some(r"PostgreSQL\s+(\d+\.\d+)".into()),
security_signals: vec!["database-exposed".into()],
priority: 10,
},
ServiceRule {
id: "redis".into(),
service: "Redis".into(),
protocol: "tcp".into(),
common_ports: vec![6379],
patterns: vec![
"-ERR".into(),
"+PONG".into(),
"$".into(),
"redis_version:".into(),
],
version_pattern: Some(r"redis_version:(\d+\.\d+\.\d+)".into()),
security_signals: vec![
"redis-unauthenticated".into(),
"database-exposed".into(),
],
priority: 10,
},
ServiceRule {
id: "mongodb".into(),
service: "MongoDB".into(),
protocol: "tcp".into(),
common_ports: vec![27017],
patterns: vec!["ismaster".into(), "MongoDB".into()],
version_pattern: Some(r"(\d+\.\d+\.\d+)".into()),
security_signals: vec!["database-exposed".into()],
priority: 10,
},
ServiceRule {
id: "smtp".into(),
service: "SMTP".into(),
protocol: "tcp".into(),
common_ports: vec![25, 465, 587],
patterns: vec![
"220 ".into(),
"ESMTP".into(),
"Postfix".into(),
"Exim".into(),
],
version_pattern: Some(r"(?:Postfix|Exim|Sendmail)\s*(\d+\.\d+\S*)".into()),
security_signals: vec!["smtp-open-relay-check".into()],
priority: 8,
},
ServiceRule {
id: "elasticsearch".into(),
service: "Elasticsearch".into(),
protocol: "tcp".into(),
common_ports: vec![9200, 9300],
patterns: vec![
"\"cluster_name\"".into(),
"\"tagline\" : \"You Know, for Search\"".into(),
],
version_pattern: Some(r#""number"\s*:\s*"(\d+\.\d+\.\d+)""#.into()),
security_signals: vec![
"elasticsearch-unauthenticated".into(),
"search-engine-exposed".into(),
],
priority: 10,
},
ServiceRule {
id: "rdp".into(),
service: "RDP".into(),
protocol: "tcp".into(),
common_ports: vec![3389],
patterns: vec![
"\x03\x00".into(), ],
version_pattern: None,
security_signals: vec!["rdp-exposed".into()],
priority: 10,
},
];
out.extend(extended_rules());
out
}
fn extended_rules() -> Vec<ServiceRule> {
let r = |id: &str, service: &str, protocol: &str, ports: Vec<u16>,
patterns: Vec<&str>, version: Option<&str>,
signals: Vec<&str>, priority: u8| -> ServiceRule {
ServiceRule {
id: id.into(),
service: service.into(),
protocol: protocol.into(),
common_ports: ports,
patterns: patterns.into_iter().map(String::from).collect(),
version_pattern: version.map(String::from),
security_signals: signals.into_iter().map(String::from).collect(),
priority,
}
};
vec![
r("http-haproxy", "HAProxy", "tcp", vec![80, 443, 8080, 8443],
vec!["Server: HAProxy", "HAProxy", "X-Haproxy"],
Some(r"HAProxy[/\s]+(\d+\.\d+(?:\.\d+)?)"),
vec!["server-version-disclosure"], 9),
r("http-caddy", "Caddy", "tcp", vec![80, 443, 2015, 2019],
vec!["Server: Caddy"],
Some(r"Caddy/(\d+\.\d+\.\d+)"),
vec!["server-version-disclosure"], 10),
r("http-litespeed", "LiteSpeed", "tcp", vec![80, 443, 7080],
vec!["Server: LiteSpeed", "LiteSpeed/"],
Some(r"LiteSpeed[/\s](\d+\.\d+\.\d+)"),
vec!["server-version-disclosure"], 9),
r("http-tomcat", "Apache Tomcat", "tcp", vec![8080, 8443, 8005, 8009],
vec!["Apache-Coyote/", "Apache Tomcat"],
Some(r"Apache Tomcat/?(\d+\.\d+\.\d+)"),
vec!["servlet-container-exposed", "server-version-disclosure"], 10),
r("http-jetty", "Eclipse Jetty", "tcp", vec![8080, 8443],
vec!["Server: Jetty", "Jetty("],
Some(r"Jetty\(?(\d+\.\d+\.\d+)"),
vec!["server-version-disclosure"], 9),
r("http-tornado", "Tornado", "tcp", vec![8888, 8080],
vec!["Server: TornadoServer"],
Some(r"TornadoServer/(\d+\.\d+\.\d+)"),
vec!["server-version-disclosure"], 8),
r("http-gunicorn", "Gunicorn", "tcp", vec![8000, 8080],
vec!["Server: gunicorn"],
Some(r"gunicorn/(\d+\.\d+\.\d+)"),
vec!["server-version-disclosure"], 8),
r("http-werkzeug", "Werkzeug", "tcp", vec![5000, 8000],
vec!["Werkzeug/"],
Some(r"Werkzeug/(\d+\.\d+\.\d+)"),
vec!["debug-server-exposed", "server-version-disclosure"], 9),
r("http-uvicorn", "Uvicorn", "tcp", vec![8000, 8080],
vec!["Server: uvicorn"],
Some(r"uvicorn"),
vec!["server-version-disclosure"], 7),
r("http-puma", "Puma", "tcp", vec![3000, 9292],
vec!["Server: puma"],
Some(r"puma\s+(\d+\.\d+\.\d+)"),
vec!["server-version-disclosure"], 8),
r("http-unicorn", "Unicorn", "tcp", vec![3000, 8080],
vec!["Server: Unicorn"],
Some(r"Unicorn\s+(\d+\.\d+\.\d+)"),
vec!["server-version-disclosure"], 7),
r("http-node-express", "Express (Node)", "tcp", vec![3000, 8080],
vec!["X-Powered-By: Express"],
None, vec!["framework-disclosure"], 8),
r("http-aws-elb", "AWS ELB", "tcp", vec![80, 443],
vec!["Server: awselb"],
Some(r"awselb/(\d+\.\d+)"), vec![], 7),
r("http-cloudflare", "Cloudflare", "tcp", vec![80, 443],
vec!["Server: cloudflare", "CF-Ray:"],
None, vec!["cdn-fronted"], 6),
r("http-akamai", "Akamai", "tcp", vec![80, 443],
vec!["Server: AkamaiGHost", "AkamaiGHost"],
None, vec!["cdn-fronted"], 6),
r("http-fastly", "Fastly", "tcp", vec![80, 443],
vec!["X-Served-By: cache-", "Via: 1.1 varnish", "Fastly-Debug"],
None, vec!["cdn-fronted"], 5),
r("http-varnish", "Varnish", "tcp", vec![80, 443, 6081, 6082],
vec!["Via: 1.1 varnish", "X-Varnish:"],
Some(r"varnish\s+v?(\d+\.\d+\.\d+)"), vec![], 7),
r("http-traefik", "Traefik", "tcp", vec![80, 443, 8080],
vec!["Server: Traefik"],
Some(r"Traefik/(\d+\.\d+\.\d+)"), vec!["server-version-disclosure"], 8),
r("http-envoy", "Envoy", "tcp", vec![80, 443, 9901],
vec!["Server: envoy", "X-Envoy-Upstream-Service-Time"],
None, vec![], 7),
r("memcached", "Memcached", "tcp", vec![11211],
vec!["VERSION ", "STAT pid"],
Some(r"VERSION\s+(\d+\.\d+\.\d+)"),
vec!["memcached-unauthenticated", "database-exposed"], 10),
r("etcd", "etcd", "tcp", vec![2379, 2380],
vec!["etcdserver", "/v2/keys", "/version"],
Some(r#""etcdserver"\s*:\s*"(\d+\.\d+\.\d+)""#),
vec!["etcd-exposed"], 10),
r("consul", "Consul", "tcp", vec![8500, 8501, 8600],
vec!["X-Consul-Index", "Consul Agent"],
Some(r"Consul/(\d+\.\d+\.\d+)"), vec!["consul-exposed"], 10),
r("vault", "HashiCorp Vault", "tcp", vec![8200, 8201],
vec!["X-Vault-", "vault/"],
Some(r"vault/(\d+\.\d+\.\d+)"), vec!["vault-api-exposed"], 10),
r("couchdb", "CouchDB", "tcp", vec![5984, 6984],
vec!["Welcome to Apache CouchDB", "couchdb"],
Some(r#""version"\s*:\s*"(\d+\.\d+\.\d+)""#),
vec!["database-exposed"], 10),
r("cassandra", "Cassandra", "tcp", vec![9042, 7000, 7199],
vec!["Apache Cassandra"],
Some(r"Cassandra\s+(\d+\.\d+\.\d+)"),
vec!["database-exposed"], 9),
r("clickhouse", "ClickHouse", "tcp", vec![8123, 9000, 9009],
vec!["X-ClickHouse-Server-Display-Name"],
Some(r"ClickHouse/(\d+\.\d+\.\d+)"),
vec!["database-exposed"], 9),
r("influxdb", "InfluxDB", "tcp", vec![8086, 8088],
vec!["X-Influxdb-Version"],
Some(r"X-Influxdb-Version:\s*(\d+\.\d+\.\d+)"),
vec!["database-exposed"], 9),
r("rabbitmq", "RabbitMQ", "tcp", vec![5672, 15672, 25672],
vec!["RabbitMQ", "AMQP\x00\x00"],
Some(r"RabbitMQ[/\s](\d+\.\d+\.\d+)"),
vec!["message-broker-exposed"], 10),
r("rabbitmq-mgmt", "RabbitMQ Management", "tcp", vec![15672],
vec!["RabbitMQ Management"],
Some(r"RabbitMQ\s+(\d+\.\d+\.\d+)"),
vec!["mgmt-ui-exposed"], 9),
r("kafka", "Kafka", "tcp", vec![9092, 9093, 9094],
vec!["org.apache.kafka", "broker.id"],
None, vec!["broker-exposed"], 9),
r("zookeeper", "ZooKeeper", "tcp", vec![2181, 2888, 3888],
vec!["zookeeper.version", "ZooKeeper"],
Some(r"version=(\d+\.\d+\.\d+)"),
vec!["zookeeper-exposed"], 10),
r("nats", "NATS", "tcp", vec![4222, 6222, 8222],
vec!["INFO {", "\"server_name\""],
Some(r#""version"\s*:\s*"(\d+\.\d+\.\d+)""#),
vec!["nats-exposed"], 9),
r("mosquitto", "Mosquitto MQTT", "tcp", vec![1883, 8883, 9001],
vec!["mosquitto"],
Some(r"mosquitto\s+version\s+(\d+\.\d+\.\d+)"),
vec!["mqtt-exposed"], 9),
r("pulsar", "Apache Pulsar", "tcp", vec![6650, 8080, 6651],
vec!["pulsar://", "Pulsar"],
Some(r"Pulsar\s+(\d+\.\d+\.\d+)"),
vec!["broker-exposed"], 8),
r("docker-daemon", "Docker daemon", "tcp", vec![2375, 2376],
vec!["Docker/", "ApiVersion", "\"DockerRootDir\""],
Some(r"Docker/(\d+\.\d+\.\d+)"),
vec!["docker-api-exposed", "rce-on-unauth"], 10),
r("kubernetes-api", "Kubernetes API", "tcp", vec![6443, 8443, 10250],
vec!["k8s.io", "/api/v1", "\"kind\":\"APIVersions\""],
Some(r#""gitVersion"\s*:\s*"v(\d+\.\d+\.\d+)""#),
vec!["k8s-api-exposed"], 10),
r("portainer", "Portainer", "tcp", vec![9000, 9443],
vec!["Portainer", "X-Portainer"],
Some(r"Portainer-(\d+\.\d+\.\d+)"), vec!["mgmt-ui-exposed"], 9),
r("rancher", "Rancher", "tcp", vec![8443, 80, 443],
vec!["X-Rancher-", "rancher"],
Some(r"rancher/(\d+\.\d+\.\d+)"), vec!["mgmt-ui-exposed"], 8),
r("kubelet", "kubelet", "tcp", vec![10250, 10255, 10256],
vec!["/runningpods", "/healthz", "kubelet"],
None, vec!["kubelet-exposed", "rce-on-unauth"], 10),
r("jenkins", "Jenkins", "tcp", vec![8080, 50000, 8443],
vec!["X-Jenkins:", "Jenkins-Version"],
Some(r"X-Jenkins:\s*(\d+\.\d+(?:\.\d+)?)"),
vec!["ci-server-exposed"], 10),
r("gitlab", "GitLab", "tcp", vec![80, 443, 8081],
vec!["GitLab", "X-Gitlab-Meta"],
Some(r"GitLab/(\d+\.\d+\.\d+)"),
vec!["scm-server-exposed"], 9),
r("gitea", "Gitea", "tcp", vec![3000, 80, 443],
vec!["Gitea", "X-Gitea-"],
Some(r"Gitea/(\d+\.\d+\.\d+)"),
vec!["scm-server-exposed"], 9),
r("gerrit", "Gerrit", "tcp", vec![8080, 29418],
vec!["Gerrit-", "X-Gerrit-"],
Some(r"Gerrit/(\d+\.\d+\.\d+)"),
vec!["scm-server-exposed"], 8),
r("sonarqube", "SonarQube", "tcp", vec![9000, 9001],
vec!["SonarQube", "x-sonar-"],
Some(r"SonarQube/(\d+\.\d+\.\d+)"),
vec!["mgmt-ui-exposed"], 8),
r("nexus", "Sonatype Nexus", "tcp", vec![8081, 8443],
vec!["X-Nexus-", "Server: Nexus"],
Some(r"Nexus/(\d+\.\d+\.\d+)"),
vec!["registry-exposed"], 9),
r("artifactory", "JFrog Artifactory", "tcp", vec![8081, 8082],
vec!["X-JFrog-Art-", "X-Artifactory-"],
Some(r"Artifactory/(\d+\.\d+\.\d+)"),
vec!["registry-exposed"], 9),
r("teamcity", "JetBrains TeamCity", "tcp", vec![8111, 80, 443],
vec!["TeamCity", "X-TeamCity-"],
Some(r"TeamCity (\d+\.\d+\.\d+)"),
vec!["ci-server-exposed"], 8),
r("concourse", "Concourse CI", "tcp", vec![8080],
vec!["X-Concourse-", "concourse-version"],
Some(r"v(\d+\.\d+\.\d+)"), vec!["ci-server-exposed"], 7),
r("drone-ci", "Drone CI", "tcp", vec![80, 443, 8000],
vec!["X-Drone", "drone/"], None,
vec!["ci-server-exposed"], 7),
r("argocd", "ArgoCD", "tcp", vec![80, 443, 8080],
vec!["X-ArgoCD-", "Argo CD"], None,
vec!["mgmt-ui-exposed"], 8),
r("spinnaker", "Spinnaker", "tcp", vec![8084, 9000],
vec!["X-Spinnaker-"], None,
vec!["mgmt-ui-exposed"], 7),
r("harbor", "Harbor", "tcp", vec![80, 443, 4443],
vec!["X-Harbor-Version", "Harbor-Api-Version"],
Some(r"X-Harbor-Version:\s*v?(\d+\.\d+\.\d+)"),
vec!["registry-exposed"], 9),
r("quay", "Quay", "tcp", vec![80, 443, 8443],
vec!["X-Quay-", "quay-version"], None,
vec!["registry-exposed"], 8),
r("bamboo", "Atlassian Bamboo", "tcp", vec![8085, 80, 443],
vec!["X-AUSERNAME", "Bamboo"], None,
vec!["ci-server-exposed"], 7),
r("bitbucket", "Atlassian Bitbucket", "tcp", vec![7990, 7999],
vec!["X-AUSERNAME", "atl_bitbucket_session"], None,
vec!["scm-server-exposed"], 7),
r("confluence", "Confluence", "tcp", vec![8090, 80, 443],
vec!["X-Confluence-Request-Time"], None,
vec!["wiki-exposed"], 7),
r("jira", "Jira", "tcp", vec![8080, 80, 443],
vec!["X-AREQUESTID", "JSESSIONID", "atlassian.xsrf.token"], None,
vec!["issue-tracker-exposed"], 7),
r("grafana", "Grafana", "tcp", vec![3000, 80, 443],
vec!["grafana_session", "X-Grafana-"],
Some(r"Grafana v(\d+\.\d+\.\d+)"),
vec!["mgmt-ui-exposed"], 9),
r("prometheus", "Prometheus", "tcp", vec![9090, 9093, 9100],
vec!["prometheus_build_info", "/api/v1/query", "Prometheus"],
Some(r#"version="(\d+\.\d+\.\d+)""#),
vec!["metrics-exposed"], 9),
r("alertmanager", "Alertmanager", "tcp", vec![9093, 9094],
vec!["Alertmanager"], Some(r"Alertmanager/(\d+\.\d+\.\d+)"),
vec!["mgmt-ui-exposed"], 7),
r("loki", "Grafana Loki", "tcp", vec![3100],
vec!["loki", "/loki/api/v1"], Some(r"loki/(\d+\.\d+\.\d+)"),
vec!["log-server-exposed"], 7),
r("tempo", "Grafana Tempo", "tcp", vec![3200, 4317, 9411],
vec!["tempo", "/api/traces"], None,
vec!["trace-server-exposed"], 6),
r("kibana", "Kibana", "tcp", vec![5601],
vec!["kbn-name", "kibana"],
Some(r#""version"\s*:\s*"(\d+\.\d+\.\d+)""#),
vec!["mgmt-ui-exposed"], 9),
r("fluentd", "Fluentd", "tcp", vec![24224, 9880],
vec!["fluentd"], Some(r"fluentd[/-](\d+\.\d+\.\d+)"),
vec!["log-server-exposed"], 7),
r("logstash", "Logstash", "tcp", vec![5044, 9600],
vec!["logstash"], Some(r"logstash[/-](\d+\.\d+\.\d+)"),
vec!["log-server-exposed"], 7),
r("keycloak", "Keycloak", "tcp", vec![8080, 8443],
vec!["X-Keycloak-", "kc_restart", "keycloak"],
Some(r"Keycloak/(\d+\.\d+\.\d+)"),
vec!["auth-server-exposed"], 8),
r("openldap", "OpenLDAP", "tcp", vec![389, 636],
vec!["LDAP", "objectClass"], None,
vec!["directory-exposed"], 8),
r("freeradius", "FreeRADIUS", "udp", vec![1812, 1813],
vec!["FreeRADIUS"], Some(r"FreeRADIUS\s+Version\s+(\d+\.\d+\.\d+)"),
vec!["radius-exposed"], 7),
r("dovecot", "Dovecot", "tcp", vec![110, 143, 993, 995],
vec!["Dovecot ready", "+OK Dovecot"],
Some(r"Dovecot\s+\(?(\d+\.\d+\.\d+)"),
vec!["mail-version-disclosure"], 9),
r("courier-imap", "Courier IMAP", "tcp", vec![143, 993],
vec!["Courier-IMAP"], Some(r"Courier-IMAP\s+(\d+\.\d+\.\d+)"),
vec!["mail-version-disclosure"], 7),
r("sendmail", "Sendmail", "tcp", vec![25, 587],
vec!["Sendmail"], Some(r"Sendmail\s+(\d+\.\d+\.\d+)"),
vec!["mail-version-disclosure"], 7),
r("filezilla-server", "FileZilla Server", "tcp", vec![21, 990],
vec!["FileZilla Server"],
Some(r"FileZilla Server\s+(\d+\.\d+\.\d+)"),
vec!["ftp-version-disclosure"], 8),
r("pure-ftpd", "Pure-FTPd", "tcp", vec![21],
vec!["Pure-FTPd"], Some(r"Pure-FTPd\s+(\d+\.\d+\S*)"),
vec!["ftp-version-disclosure"], 8),
r("samba", "Samba", "tcp", vec![139, 445],
vec!["Samba", "SMB"], Some(r"Samba\s+(\d+\.\d+\.\d+)"),
vec!["smb-exposed"], 9),
r("nfs", "NFS", "tcp", vec![2049],
vec!["NFS_PROGRAM"], None,
vec!["nfs-exposed"], 8),
r("ipmi", "IPMI", "udp", vec![623],
vec!["IPMI"], None,
vec!["ipmi-exposed", "remote-mgmt-exposed"], 10),
r("ilo", "HP iLO", "tcp", vec![80, 443, 17988],
vec!["Server: HP-iLO", "iLO"],
Some(r"HP-iLO/(\d+\.\d+)"),
vec!["remote-mgmt-exposed"], 10),
r("idrac", "Dell iDRAC", "tcp", vec![80, 443, 5900],
vec!["iDRAC", "Dell Inc."],
Some(r"iDRAC[/\s](\d+)"),
vec!["remote-mgmt-exposed"], 10),
r("supermicro-ipmi", "Supermicro BMC", "tcp", vec![80, 443],
vec!["ATEN International", "Supermicro"], None,
vec!["remote-mgmt-exposed"], 9),
r("ipp-cups", "IPP/CUPS", "tcp", vec![631],
vec!["IPP/", "CUPS"], Some(r"CUPS/(\d+\.\d+\.\d+)"),
vec!["printer-exposed"], 7),
r("jetdirect", "HP JetDirect", "tcp", vec![9100],
vec!["@PJL", "HP JetDirect"], None,
vec!["printer-exposed"], 7),
r("telnet", "Telnet", "tcp", vec![23, 2323],
vec!["login: ", "Password: ", "Welcome to "],
None, vec!["cleartext-protocol", "telnet-exposed"], 10),
r("snmp", "SNMP", "udp", vec![161, 162],
vec!["public", "private"], None,
vec!["snmp-exposed"], 9),
r("rpcbind", "rpcbind/portmap", "tcp", vec![111],
vec!["rpcbind", "program vers"], None,
vec!["rpc-exposed"], 7),
r("powerdns", "PowerDNS", "udp", vec![53, 8081],
vec!["PowerDNS"], Some(r"PowerDNS\s+Authoritative\s+Server\s+(\d+\.\d+\.\d+)"),
vec!["dns-version-disclosure"], 9),
r("nsd", "NLnet NSD", "udp", vec![53],
vec!["NSD "], Some(r"NSD\s+(\d+\.\d+\.\d+)"),
vec!["dns-version-disclosure"], 8),
r("knot-dns", "Knot DNS", "udp", vec![53],
vec!["Knot DNS"], Some(r"Knot DNS\s+(\d+\.\d+\.\d+)"),
vec!["dns-version-disclosure"], 8),
r("unbound", "Unbound", "udp", vec![53],
vec!["unbound"], Some(r"unbound\s+(\d+\.\d+\.\d+)"),
vec!["dns-version-disclosure"], 7),
r("dnsdist", "dnsdist", "tcp", vec![53, 5199],
vec!["dnsdist"], Some(r"dnsdist\s+(\d+\.\d+\.\d+)"),
vec!["dns-version-disclosure"], 7),
r("ntp", "NTP", "udp", vec![123],
vec!["\x1c"], None, vec![], 6),
r("openvpn", "OpenVPN", "udp", vec![1194, 443],
vec!["P_CONTROL", "OpenVPN"], None,
vec!["vpn-exposed"], 8),
r("wireguard", "WireGuard", "udp", vec![51820, 443],
vec!["WireGuard"], None, vec!["vpn-exposed"], 7),
r("pptp", "PPTP", "tcp", vec![1723],
vec!["pptp"], None, vec!["legacy-vpn-exposed"], 8),
r("strongswan", "strongSwan IPsec", "udp", vec![500, 4500],
vec!["strongSwan"], Some(r"strongSwan\s+(\d+\.\d+\.\d+)"),
vec!["vpn-exposed"], 7),
r("xrdp", "xrdp", "tcp", vec![3389],
vec!["xrdp"], Some(r"xrdp\s+(\d+\.\d+\.\d+)"),
vec!["rdp-exposed"], 8),
r("vnc", "VNC", "tcp", vec![5900, 5901, 5902],
vec!["RFB 003.", "RFB"],
Some(r"RFB 003\.(\d+)"),
vec!["vnc-exposed", "remote-desktop-exposed"], 9),
r("minecraft", "Minecraft Server", "tcp", vec![25565],
vec!["Minecraft Server", "minecraft.net", "Vanilla 1.", "Bukkit on"],
None, vec!["game-server-exposed"], 5),
r("ts3", "TeamSpeak 3", "udp", vec![9987, 10011, 30033],
vec!["TS3", "TeaSpeak"], None, vec![], 5),
r("plex", "Plex Media Server", "tcp", vec![32400],
vec!["X-Plex-", "Plex Media Server"],
Some(r"X-Plex-Version:\s*(\d+\.\d+\.\d+)"),
vec!["media-server-exposed"], 6),
r("jellyfin", "Jellyfin", "tcp", vec![8096, 8920],
vec!["X-Application", "Jellyfin"],
Some(r"Jellyfin/(\d+\.\d+\.\d+)"),
vec!["media-server-exposed"], 6),
r("modbus", "Modbus TCP", "tcp", vec![502],
vec!["\u{0}\u{0}\u{0}\u{0}\u{0}\u{6}\u{1}"],
None, vec!["ics-exposed", "modbus-exposed"], 10),
r("siemens-s7", "Siemens S7", "tcp", vec![102],
vec!["S7", "ISO-on-TCP"], None,
vec!["ics-exposed"], 10),
r("iscsi", "iSCSI", "tcp", vec![3260],
vec!["iSCSI", "iqn."], None,
vec!["storage-exposed"], 8),
r("minio", "MinIO", "tcp", vec![9000, 9001],
vec!["X-Amz-Bucket-Region", "MinIO", "MinIO Console"],
Some(r"MinIO/(\S+)"), vec!["object-storage-exposed"], 9),
r("solr", "Apache Solr", "tcp", vec![8983, 7574],
vec!["Solr", "X-Solr-"],
Some(r"Solr/(\d+\.\d+\.\d+)"),
vec!["search-engine-exposed"], 9),
r("opensearch", "OpenSearch", "tcp", vec![9200],
vec!["opensearch", "OpenSearch"],
Some(r#""number"\s*:\s*"(\d+\.\d+\.\d+)""#),
vec!["search-engine-exposed"], 9),
r("milvus", "Milvus", "tcp", vec![19530, 9091],
vec!["milvus"], Some(r"milvus[/\s](\d+\.\d+\.\d+)"),
vec!["vector-db-exposed"], 7),
r("qdrant", "Qdrant", "tcp", vec![6333, 6334],
vec!["qdrant", "Qdrant"], Some(r"Qdrant/(\d+\.\d+\.\d+)"),
vec!["vector-db-exposed"], 7),
r("weaviate", "Weaviate", "tcp", vec![8080, 50051],
vec!["weaviate"], Some(r"weaviate[/\s](\d+\.\d+\.\d+)"),
vec!["vector-db-exposed"], 7),
r("smb1", "SMBv1", "tcp", vec![139, 445],
vec!["SMB 1.0"], None,
vec!["smbv1-exposed", "deprecated-protocol"], 10),
r("rsync", "rsync", "tcp", vec![873],
vec!["@RSYNCD:"], Some(r"@RSYNCD:\s*(\d+\.\d+)"),
vec!["rsync-exposed"], 8),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_rules_not_empty() {
let rules = builtin_rules();
assert!(rules.len() > 10, "should have substantial rule coverage");
}
#[test]
fn all_rules_have_unique_ids() {
let rules = builtin_rules();
let mut seen = std::collections::HashSet::new();
for rule in &rules {
assert!(seen.insert(&rule.id), "duplicate rule id: {}", rule.id);
}
}
#[test]
fn all_rules_have_patterns() {
for rule in builtin_rules() {
assert!(!rule.patterns.is_empty(), "rule {} has no patterns", rule.id);
}
}
}