Skip to main content

gossan_classify/
rules.rs

1//! TOML-defined service classification rules.
2//!
3//! Each rule matches a banner pattern and extracts service metadata.
4//! Rules are loaded from `rules/` directory, user-extensible.
5
6use serde::Deserialize;
7use std::collections::HashMap;
8use std::path::Path;
9
10/// A single service classification rule.
11#[derive(Debug, Clone, Deserialize)]
12pub struct ServiceRule {
13    /// Unique rule identifier (e.g., "http-apache").
14    pub id: String,
15    /// Human-readable service name (e.g., "Apache HTTP Server").
16    pub service: String,
17    /// Protocol this rule applies to (tcp/udp).
18    pub protocol: String,
19    /// Ports this rule commonly matches (hint, not filter).
20    pub common_ports: Vec<u16>,
21    /// Banner patterns to match (any match triggers the rule).
22    pub patterns: Vec<String>,
23    /// Regex for version extraction (capture group 1 = version).
24    pub version_pattern: Option<String>,
25    /// Security signals this service may emit.
26    pub security_signals: Vec<String>,
27    /// Priority (higher = preferred when multiple rules match).
28    pub priority: u8,
29}
30
31#[derive(Debug, Deserialize)]
32struct ServiceRulesFile {
33    #[serde(default)]
34    rule: Vec<ServiceRule>,
35}
36
37/// Load custom service-classification rules from a TOML file.
38///
39/// Schema: a `[[rule]]` array with each entry shaped like
40/// [`ServiceRule`]. Returns the parsed rules or an `io::Error` /
41/// `toml::de::Error` wrapped in `anyhow::Error`.
42///
43/// ```toml
44/// [[rule]]
45/// id = "custom-service"
46/// service = "MyService"
47/// protocol = "tcp"
48/// common_ports = [9999]
49/// patterns = ["MYSERVICE/"]
50/// version_pattern = "MYSERVICE/(\\d+\\.\\d+)"
51/// security_signals = []
52/// priority = 5
53/// ```
54pub fn load_from_toml<P: AsRef<Path>>(path: P) -> Result<Vec<ServiceRule>, anyhow::Error> {
55    let body = std::fs::read_to_string(path.as_ref())?;
56    let parsed: ServiceRulesFile = toml::from_str(&body)?;
57    Ok(parsed.rule)
58}
59
60/// Load custom rules + the built-in set, in that order. Custom
61/// rules take precedence (appear first) so a higher-priority custom
62/// rule can override a built-in.
63pub fn builtin_plus<P: AsRef<Path>>(custom_path: P) -> Vec<ServiceRule> {
64    let mut out = load_from_toml(custom_path).unwrap_or_default();
65    out.extend(builtin_rules());
66    out
67}
68
69/// Result of classifying a banner.
70#[derive(Debug, Clone)]
71pub struct ServiceMatch {
72    /// The rule that matched.
73    pub rule_id: String,
74    /// Identified service name.
75    pub service: String,
76    /// Extracted version, if available.
77    pub version: Option<String>,
78    /// Confidence score (0.0 - 1.0).
79    pub confidence: f32,
80    /// Security signals detected in the banner.
81    pub signals: Vec<String>,
82    /// Additional metadata extracted.
83    pub metadata: HashMap<String, String>,
84}
85
86/// Built-in rule definitions.
87///
88/// These cover the most common services. Users extend via TOML files
89/// in a `rules/` directory.
90pub fn builtin_rules() -> Vec<ServiceRule> {
91    let mut out = vec![
92        // ── HTTP servers ──────────────────────────────────────────────
93        ServiceRule {
94            id: "http-apache".into(),
95            service: "Apache HTTP Server".into(),
96            protocol: "tcp".into(),
97            common_ports: vec![80, 443, 8080, 8443],
98            patterns: vec!["Apache/".into(), "Server: Apache".into()],
99            version_pattern: Some(r"Apache/(\d+\.\d+\.\d+)".into()),
100            security_signals: vec!["server-version-disclosure".into()],
101            priority: 10,
102        },
103        ServiceRule {
104            id: "http-nginx".into(),
105            service: "nginx".into(),
106            protocol: "tcp".into(),
107            common_ports: vec![80, 443, 8080, 8443],
108            patterns: vec!["nginx/".into(), "Server: nginx".into()],
109            version_pattern: Some(r"nginx/(\d+\.\d+\.\d+)".into()),
110            security_signals: vec!["server-version-disclosure".into()],
111            priority: 10,
112        },
113        ServiceRule {
114            id: "http-iis".into(),
115            service: "Microsoft IIS".into(),
116            protocol: "tcp".into(),
117            common_ports: vec![80, 443, 8080],
118            patterns: vec!["Microsoft-IIS/".into(), "Server: Microsoft-IIS".into()],
119            version_pattern: Some(r"Microsoft-IIS/(\d+\.\d+)".into()),
120            security_signals: vec!["server-version-disclosure".into()],
121            priority: 10,
122        },
123        // ── SSH ───────────────────────────────────────────────────────
124        ServiceRule {
125            id: "ssh-openssh".into(),
126            service: "OpenSSH".into(),
127            protocol: "tcp".into(),
128            common_ports: vec![22, 2222],
129            patterns: vec!["SSH-2.0-OpenSSH".into(), "SSH-1.99-OpenSSH".into()],
130            version_pattern: Some(r"OpenSSH[_\s](\d+\.\d+\S*)".into()),
131            security_signals: vec!["ssh-version-disclosure".into()],
132            priority: 10,
133        },
134        ServiceRule {
135            id: "ssh-dropbear".into(),
136            service: "Dropbear SSH".into(),
137            protocol: "tcp".into(),
138            common_ports: vec![22, 2222],
139            patterns: vec!["dropbear".into()],
140            version_pattern: Some(r"dropbear[_\s](\d+\.\d+)".into()),
141            security_signals: vec!["ssh-version-disclosure".into()],
142            priority: 8,
143        },
144        // ── FTP ───────────────────────────────────────────────────────
145        ServiceRule {
146            id: "ftp-vsftpd".into(),
147            service: "vsftpd".into(),
148            protocol: "tcp".into(),
149            common_ports: vec![21],
150            patterns: vec!["vsftpd".into(), "vsFTPd".into()],
151            version_pattern: Some(r"vsftpd\s+(\d+\.\d+\.\d+)".into()),
152            security_signals: vec!["ftp-version-disclosure".into()],
153            priority: 10,
154        },
155        ServiceRule {
156            id: "ftp-proftpd".into(),
157            service: "ProFTPD".into(),
158            protocol: "tcp".into(),
159            common_ports: vec![21],
160            patterns: vec!["ProFTPD".into()],
161            version_pattern: Some(r"ProFTPD\s+(\d+\.\d+\.\d+\S*)".into()),
162            security_signals: vec!["ftp-version-disclosure".into()],
163            priority: 10,
164        },
165        // ── Databases ─────────────────────────────────────────────────
166        ServiceRule {
167            id: "mysql".into(),
168            service: "MySQL".into(),
169            protocol: "tcp".into(),
170            common_ports: vec![3306],
171            patterns: vec![
172                "mysql_native_password".into(),
173                "MariaDB".into(),
174                "\x00\x00\x00\x0a".into(), // MySQL greeting
175            ],
176            version_pattern: Some(r"(\d+\.\d+\.\d+)".into()),
177            security_signals: vec!["database-exposed".into(), "version-disclosure".into()],
178            priority: 10,
179        },
180        ServiceRule {
181            id: "postgresql".into(),
182            service: "PostgreSQL".into(),
183            protocol: "tcp".into(),
184            common_ports: vec![5432],
185            patterns: vec!["PostgreSQL".into()],
186            version_pattern: Some(r"PostgreSQL\s+(\d+\.\d+)".into()),
187            security_signals: vec!["database-exposed".into()],
188            priority: 10,
189        },
190        ServiceRule {
191            id: "redis".into(),
192            service: "Redis".into(),
193            protocol: "tcp".into(),
194            common_ports: vec![6379],
195            patterns: vec![
196                "-ERR".into(),
197                "+PONG".into(),
198                "$".into(),
199                "redis_version:".into(),
200            ],
201            version_pattern: Some(r"redis_version:(\d+\.\d+\.\d+)".into()),
202            security_signals: vec!["redis-unauthenticated".into(), "database-exposed".into()],
203            priority: 10,
204        },
205        ServiceRule {
206            id: "mongodb".into(),
207            service: "MongoDB".into(),
208            protocol: "tcp".into(),
209            common_ports: vec![27017],
210            patterns: vec!["ismaster".into(), "MongoDB".into()],
211            version_pattern: Some(r"(\d+\.\d+\.\d+)".into()),
212            security_signals: vec!["database-exposed".into()],
213            priority: 10,
214        },
215        // ── Mail ──────────────────────────────────────────────────────
216        ServiceRule {
217            id: "smtp".into(),
218            service: "SMTP".into(),
219            protocol: "tcp".into(),
220            common_ports: vec![25, 465, 587],
221            patterns: vec![
222                "220 ".into(),
223                "ESMTP".into(),
224                "Postfix".into(),
225                "Exim".into(),
226            ],
227            version_pattern: Some(r"(?:Postfix|Exim|Sendmail)\s*(\d+\.\d+\S*)".into()),
228            security_signals: vec!["smtp-open-relay-check".into()],
229            priority: 8,
230        },
231        // ── Elastic / Kibana ──────────────────────────────────────────
232        ServiceRule {
233            id: "elasticsearch".into(),
234            service: "Elasticsearch".into(),
235            protocol: "tcp".into(),
236            common_ports: vec![9200, 9300],
237            patterns: vec![
238                "\"cluster_name\"".into(),
239                "\"tagline\" : \"You Know, for Search\"".into(),
240            ],
241            version_pattern: Some(r#""number"\s*:\s*"(\d+\.\d+\.\d+)""#.into()),
242            security_signals: vec![
243                "elasticsearch-unauthenticated".into(),
244                "search-engine-exposed".into(),
245            ],
246            priority: 10,
247        },
248        // ── RDP ───────────────────────────────────────────────────────
249        ServiceRule {
250            id: "rdp".into(),
251            service: "RDP".into(),
252            protocol: "tcp".into(),
253            common_ports: vec![3389],
254            patterns: vec![
255                "\x03\x00".into(), // RDP negotiation response
256            ],
257            version_pattern: None,
258            security_signals: vec!["rdp-exposed".into()],
259            priority: 10,
260        },
261    ];
262    // Append the curated top-100 expansion below to keep diffs reviewable.
263    out.extend(extended_rules());
264    out
265}
266
267fn extended_rules() -> Vec<ServiceRule> {
268    let r = |id: &str,
269             service: &str,
270             protocol: &str,
271             ports: Vec<u16>,
272             patterns: Vec<&str>,
273             version: Option<&str>,
274             signals: Vec<&str>,
275             priority: u8|
276     -> ServiceRule {
277        ServiceRule {
278            id: id.into(),
279            service: service.into(),
280            protocol: protocol.into(),
281            common_ports: ports,
282            patterns: patterns.into_iter().map(String::from).collect(),
283            version_pattern: version.map(String::from),
284            security_signals: signals.into_iter().map(String::from).collect(),
285            priority,
286        }
287    };
288    vec![
289        // ── HTTP servers (additional) ─────────────────────────────────
290        r(
291            "http-haproxy",
292            "HAProxy",
293            "tcp",
294            vec![80, 443, 8080, 8443],
295            vec!["Server: HAProxy", "HAProxy", "X-Haproxy"],
296            Some(r"HAProxy[/\s]+(\d+\.\d+(?:\.\d+)?)"),
297            vec!["server-version-disclosure"],
298            9,
299        ),
300        r(
301            "http-caddy",
302            "Caddy",
303            "tcp",
304            vec![80, 443, 2015, 2019],
305            vec!["Server: Caddy"],
306            Some(r"Caddy/(\d+\.\d+\.\d+)"),
307            vec!["server-version-disclosure"],
308            10,
309        ),
310        r(
311            "http-litespeed",
312            "LiteSpeed",
313            "tcp",
314            vec![80, 443, 7080],
315            vec!["Server: LiteSpeed", "LiteSpeed/"],
316            Some(r"LiteSpeed[/\s](\d+\.\d+\.\d+)"),
317            vec!["server-version-disclosure"],
318            9,
319        ),
320        r(
321            "http-tomcat",
322            "Apache Tomcat",
323            "tcp",
324            vec![8080, 8443, 8005, 8009],
325            vec!["Apache-Coyote/", "Apache Tomcat"],
326            Some(r"Apache Tomcat/?(\d+\.\d+\.\d+)"),
327            vec!["servlet-container-exposed", "server-version-disclosure"],
328            10,
329        ),
330        r(
331            "http-jetty",
332            "Eclipse Jetty",
333            "tcp",
334            vec![8080, 8443],
335            vec!["Server: Jetty", "Jetty("],
336            Some(r"Jetty\(?(\d+\.\d+\.\d+)"),
337            vec!["server-version-disclosure"],
338            9,
339        ),
340        r(
341            "http-tornado",
342            "Tornado",
343            "tcp",
344            vec![8888, 8080],
345            vec!["Server: TornadoServer"],
346            Some(r"TornadoServer/(\d+\.\d+\.\d+)"),
347            vec!["server-version-disclosure"],
348            8,
349        ),
350        r(
351            "http-gunicorn",
352            "Gunicorn",
353            "tcp",
354            vec![8000, 8080],
355            vec!["Server: gunicorn"],
356            Some(r"gunicorn/(\d+\.\d+\.\d+)"),
357            vec!["server-version-disclosure"],
358            8,
359        ),
360        r(
361            "http-werkzeug",
362            "Werkzeug",
363            "tcp",
364            vec![5000, 8000],
365            vec!["Werkzeug/"],
366            Some(r"Werkzeug/(\d+\.\d+\.\d+)"),
367            vec!["debug-server-exposed", "server-version-disclosure"],
368            9,
369        ),
370        r(
371            "http-uvicorn",
372            "Uvicorn",
373            "tcp",
374            vec![8000, 8080],
375            vec!["Server: uvicorn"],
376            Some(r"uvicorn"),
377            vec!["server-version-disclosure"],
378            7,
379        ),
380        r(
381            "http-puma",
382            "Puma",
383            "tcp",
384            vec![3000, 9292],
385            vec!["Server: puma"],
386            Some(r"puma\s+(\d+\.\d+\.\d+)"),
387            vec!["server-version-disclosure"],
388            8,
389        ),
390        r(
391            "http-unicorn",
392            "Unicorn",
393            "tcp",
394            vec![3000, 8080],
395            vec!["Server: Unicorn"],
396            Some(r"Unicorn\s+(\d+\.\d+\.\d+)"),
397            vec!["server-version-disclosure"],
398            7,
399        ),
400        r(
401            "http-node-express",
402            "Express (Node)",
403            "tcp",
404            vec![3000, 8080],
405            vec!["X-Powered-By: Express"],
406            None,
407            vec!["framework-disclosure"],
408            8,
409        ),
410        r(
411            "http-aws-elb",
412            "AWS ELB",
413            "tcp",
414            vec![80, 443],
415            vec!["Server: awselb"],
416            Some(r"awselb/(\d+\.\d+)"),
417            vec![],
418            7,
419        ),
420        r(
421            "http-cloudflare",
422            "Cloudflare",
423            "tcp",
424            vec![80, 443],
425            vec!["Server: cloudflare", "CF-Ray:"],
426            None,
427            vec!["cdn-fronted"],
428            6,
429        ),
430        r(
431            "http-akamai",
432            "Akamai",
433            "tcp",
434            vec![80, 443],
435            vec!["Server: AkamaiGHost", "AkamaiGHost"],
436            None,
437            vec!["cdn-fronted"],
438            6,
439        ),
440        r(
441            "http-fastly",
442            "Fastly",
443            "tcp",
444            vec![80, 443],
445            vec!["X-Served-By: cache-", "Via: 1.1 varnish", "Fastly-Debug"],
446            None,
447            vec!["cdn-fronted"],
448            5,
449        ),
450        r(
451            "http-varnish",
452            "Varnish",
453            "tcp",
454            vec![80, 443, 6081, 6082],
455            vec!["Via: 1.1 varnish", "X-Varnish:"],
456            Some(r"varnish\s+v?(\d+\.\d+\.\d+)"),
457            vec![],
458            7,
459        ),
460        r(
461            "http-traefik",
462            "Traefik",
463            "tcp",
464            vec![80, 443, 8080],
465            vec!["Server: Traefik"],
466            Some(r"Traefik/(\d+\.\d+\.\d+)"),
467            vec!["server-version-disclosure"],
468            8,
469        ),
470        r(
471            "http-envoy",
472            "Envoy",
473            "tcp",
474            vec![80, 443, 9901],
475            vec!["Server: envoy", "X-Envoy-Upstream-Service-Time"],
476            None,
477            vec![],
478            7,
479        ),
480        // ── Caches / KV ───────────────────────────────────────────────
481        r(
482            "memcached",
483            "Memcached",
484            "tcp",
485            vec![11211],
486            vec!["VERSION ", "STAT pid"],
487            Some(r"VERSION\s+(\d+\.\d+\.\d+)"),
488            vec!["memcached-unauthenticated", "database-exposed"],
489            10,
490        ),
491        r(
492            "etcd",
493            "etcd",
494            "tcp",
495            vec![2379, 2380],
496            vec!["etcdserver", "/v2/keys", "/version"],
497            Some(r#""etcdserver"\s*:\s*"(\d+\.\d+\.\d+)""#),
498            vec!["etcd-exposed"],
499            10,
500        ),
501        r(
502            "consul",
503            "Consul",
504            "tcp",
505            vec![8500, 8501, 8600],
506            vec!["X-Consul-Index", "Consul Agent"],
507            Some(r"Consul/(\d+\.\d+\.\d+)"),
508            vec!["consul-exposed"],
509            10,
510        ),
511        r(
512            "vault",
513            "HashiCorp Vault",
514            "tcp",
515            vec![8200, 8201],
516            vec!["X-Vault-", "vault/"],
517            Some(r"vault/(\d+\.\d+\.\d+)"),
518            vec!["vault-api-exposed"],
519            10,
520        ),
521        // ── Document / column DBs ─────────────────────────────────────
522        r(
523            "couchdb",
524            "CouchDB",
525            "tcp",
526            vec![5984, 6984],
527            vec!["Welcome to Apache CouchDB", "couchdb"],
528            Some(r#""version"\s*:\s*"(\d+\.\d+\.\d+)""#),
529            vec!["database-exposed"],
530            10,
531        ),
532        r(
533            "cassandra",
534            "Cassandra",
535            "tcp",
536            vec![9042, 7000, 7199],
537            vec!["Apache Cassandra"],
538            Some(r"Cassandra\s+(\d+\.\d+\.\d+)"),
539            vec!["database-exposed"],
540            9,
541        ),
542        r(
543            "clickhouse",
544            "ClickHouse",
545            "tcp",
546            vec![8123, 9000, 9009],
547            vec!["X-ClickHouse-Server-Display-Name"],
548            Some(r"ClickHouse/(\d+\.\d+\.\d+)"),
549            vec!["database-exposed"],
550            9,
551        ),
552        r(
553            "influxdb",
554            "InfluxDB",
555            "tcp",
556            vec![8086, 8088],
557            vec!["X-Influxdb-Version"],
558            Some(r"X-Influxdb-Version:\s*(\d+\.\d+\.\d+)"),
559            vec!["database-exposed"],
560            9,
561        ),
562        // ── Message brokers / streams ─────────────────────────────────
563        r(
564            "rabbitmq",
565            "RabbitMQ",
566            "tcp",
567            vec![5672, 15672, 25672],
568            vec!["RabbitMQ", "AMQP\x00\x00"],
569            Some(r"RabbitMQ[/\s](\d+\.\d+\.\d+)"),
570            vec!["message-broker-exposed"],
571            10,
572        ),
573        r(
574            "rabbitmq-mgmt",
575            "RabbitMQ Management",
576            "tcp",
577            vec![15672],
578            vec!["RabbitMQ Management"],
579            Some(r"RabbitMQ\s+(\d+\.\d+\.\d+)"),
580            vec!["mgmt-ui-exposed"],
581            9,
582        ),
583        r(
584            "kafka",
585            "Kafka",
586            "tcp",
587            vec![9092, 9093, 9094],
588            vec!["org.apache.kafka", "broker.id"],
589            None,
590            vec!["broker-exposed"],
591            9,
592        ),
593        r(
594            "zookeeper",
595            "ZooKeeper",
596            "tcp",
597            vec![2181, 2888, 3888],
598            vec!["zookeeper.version", "ZooKeeper"],
599            Some(r"version=(\d+\.\d+\.\d+)"),
600            vec!["zookeeper-exposed"],
601            10,
602        ),
603        r(
604            "nats",
605            "NATS",
606            "tcp",
607            vec![4222, 6222, 8222],
608            vec!["INFO {", "\"server_name\""],
609            Some(r#""version"\s*:\s*"(\d+\.\d+\.\d+)""#),
610            vec!["nats-exposed"],
611            9,
612        ),
613        r(
614            "mosquitto",
615            "Mosquitto MQTT",
616            "tcp",
617            vec![1883, 8883, 9001],
618            vec!["mosquitto"],
619            Some(r"mosquitto\s+version\s+(\d+\.\d+\.\d+)"),
620            vec!["mqtt-exposed"],
621            9,
622        ),
623        r(
624            "pulsar",
625            "Apache Pulsar",
626            "tcp",
627            vec![6650, 8080, 6651],
628            vec!["pulsar://", "Pulsar"],
629            Some(r"Pulsar\s+(\d+\.\d+\.\d+)"),
630            vec!["broker-exposed"],
631            8,
632        ),
633        // ── Container & orchestration ────────────────────────────────
634        r(
635            "docker-daemon",
636            "Docker daemon",
637            "tcp",
638            vec![2375, 2376],
639            vec!["Docker/", "ApiVersion", "\"DockerRootDir\""],
640            Some(r"Docker/(\d+\.\d+\.\d+)"),
641            vec!["docker-api-exposed", "rce-on-unauth"],
642            10,
643        ),
644        r(
645            "kubernetes-api",
646            "Kubernetes API",
647            "tcp",
648            vec![6443, 8443, 10250],
649            vec!["k8s.io", "/api/v1", "\"kind\":\"APIVersions\""],
650            Some(r#""gitVersion"\s*:\s*"v(\d+\.\d+\.\d+)""#),
651            vec!["k8s-api-exposed"],
652            10,
653        ),
654        r(
655            "portainer",
656            "Portainer",
657            "tcp",
658            vec![9000, 9443],
659            vec!["Portainer", "X-Portainer"],
660            Some(r"Portainer-(\d+\.\d+\.\d+)"),
661            vec!["mgmt-ui-exposed"],
662            9,
663        ),
664        r(
665            "rancher",
666            "Rancher",
667            "tcp",
668            vec![8443, 80, 443],
669            vec!["X-Rancher-", "rancher"],
670            Some(r"rancher/(\d+\.\d+\.\d+)"),
671            vec!["mgmt-ui-exposed"],
672            8,
673        ),
674        r(
675            "kubelet",
676            "kubelet",
677            "tcp",
678            vec![10250, 10255, 10256],
679            vec!["/runningpods", "/healthz", "kubelet"],
680            None,
681            vec!["kubelet-exposed", "rce-on-unauth"],
682            10,
683        ),
684        // ── CI/CD ────────────────────────────────────────────────────
685        r(
686            "jenkins",
687            "Jenkins",
688            "tcp",
689            vec![8080, 50000, 8443],
690            vec!["X-Jenkins:", "Jenkins-Version"],
691            Some(r"X-Jenkins:\s*(\d+\.\d+(?:\.\d+)?)"),
692            vec!["ci-server-exposed"],
693            10,
694        ),
695        r(
696            "gitlab",
697            "GitLab",
698            "tcp",
699            vec![80, 443, 8081],
700            vec!["GitLab", "X-Gitlab-Meta"],
701            Some(r"GitLab/(\d+\.\d+\.\d+)"),
702            vec!["scm-server-exposed"],
703            9,
704        ),
705        r(
706            "gitea",
707            "Gitea",
708            "tcp",
709            vec![3000, 80, 443],
710            vec!["Gitea", "X-Gitea-"],
711            Some(r"Gitea/(\d+\.\d+\.\d+)"),
712            vec!["scm-server-exposed"],
713            9,
714        ),
715        r(
716            "gerrit",
717            "Gerrit",
718            "tcp",
719            vec![8080, 29418],
720            vec!["Gerrit-", "X-Gerrit-"],
721            Some(r"Gerrit/(\d+\.\d+\.\d+)"),
722            vec!["scm-server-exposed"],
723            8,
724        ),
725        r(
726            "sonarqube",
727            "SonarQube",
728            "tcp",
729            vec![9000, 9001],
730            vec!["SonarQube", "x-sonar-"],
731            Some(r"SonarQube/(\d+\.\d+\.\d+)"),
732            vec!["mgmt-ui-exposed"],
733            8,
734        ),
735        r(
736            "nexus",
737            "Sonatype Nexus",
738            "tcp",
739            vec![8081, 8443],
740            vec!["X-Nexus-", "Server: Nexus"],
741            Some(r"Nexus/(\d+\.\d+\.\d+)"),
742            vec!["registry-exposed"],
743            9,
744        ),
745        r(
746            "artifactory",
747            "JFrog Artifactory",
748            "tcp",
749            vec![8081, 8082],
750            vec!["X-JFrog-Art-", "X-Artifactory-"],
751            Some(r"Artifactory/(\d+\.\d+\.\d+)"),
752            vec!["registry-exposed"],
753            9,
754        ),
755        r(
756            "teamcity",
757            "JetBrains TeamCity",
758            "tcp",
759            vec![8111, 80, 443],
760            vec!["TeamCity", "X-TeamCity-"],
761            Some(r"TeamCity (\d+\.\d+\.\d+)"),
762            vec!["ci-server-exposed"],
763            8,
764        ),
765        r(
766            "concourse",
767            "Concourse CI",
768            "tcp",
769            vec![8080],
770            vec!["X-Concourse-", "concourse-version"],
771            Some(r"v(\d+\.\d+\.\d+)"),
772            vec!["ci-server-exposed"],
773            7,
774        ),
775        r(
776            "drone-ci",
777            "Drone CI",
778            "tcp",
779            vec![80, 443, 8000],
780            vec!["X-Drone", "drone/"],
781            None,
782            vec!["ci-server-exposed"],
783            7,
784        ),
785        r(
786            "argocd",
787            "ArgoCD",
788            "tcp",
789            vec![80, 443, 8080],
790            vec!["X-ArgoCD-", "Argo CD"],
791            None,
792            vec!["mgmt-ui-exposed"],
793            8,
794        ),
795        r(
796            "spinnaker",
797            "Spinnaker",
798            "tcp",
799            vec![8084, 9000],
800            vec!["X-Spinnaker-"],
801            None,
802            vec!["mgmt-ui-exposed"],
803            7,
804        ),
805        r(
806            "harbor",
807            "Harbor",
808            "tcp",
809            vec![80, 443, 4443],
810            vec!["X-Harbor-Version", "Harbor-Api-Version"],
811            Some(r"X-Harbor-Version:\s*v?(\d+\.\d+\.\d+)"),
812            vec!["registry-exposed"],
813            9,
814        ),
815        r(
816            "quay",
817            "Quay",
818            "tcp",
819            vec![80, 443, 8443],
820            vec!["X-Quay-", "quay-version"],
821            None,
822            vec!["registry-exposed"],
823            8,
824        ),
825        // ── Atlassian ────────────────────────────────────────────────
826        r(
827            "bamboo",
828            "Atlassian Bamboo",
829            "tcp",
830            vec![8085, 80, 443],
831            vec!["X-AUSERNAME", "Bamboo"],
832            None,
833            vec!["ci-server-exposed"],
834            7,
835        ),
836        r(
837            "bitbucket",
838            "Atlassian Bitbucket",
839            "tcp",
840            vec![7990, 7999],
841            vec!["X-AUSERNAME", "atl_bitbucket_session"],
842            None,
843            vec!["scm-server-exposed"],
844            7,
845        ),
846        r(
847            "confluence",
848            "Confluence",
849            "tcp",
850            vec![8090, 80, 443],
851            vec!["X-Confluence-Request-Time"],
852            None,
853            vec!["wiki-exposed"],
854            7,
855        ),
856        r(
857            "jira",
858            "Jira",
859            "tcp",
860            vec![8080, 80, 443],
861            vec!["X-AREQUESTID", "JSESSIONID", "atlassian.xsrf.token"],
862            None,
863            vec!["issue-tracker-exposed"],
864            7,
865        ),
866        // ── Observability ────────────────────────────────────────────
867        r(
868            "grafana",
869            "Grafana",
870            "tcp",
871            vec![3000, 80, 443],
872            vec!["grafana_session", "X-Grafana-"],
873            Some(r"Grafana v(\d+\.\d+\.\d+)"),
874            vec!["mgmt-ui-exposed"],
875            9,
876        ),
877        r(
878            "prometheus",
879            "Prometheus",
880            "tcp",
881            vec![9090, 9093, 9100],
882            vec!["prometheus_build_info", "/api/v1/query", "Prometheus"],
883            Some(r#"version="(\d+\.\d+\.\d+)""#),
884            vec!["metrics-exposed"],
885            9,
886        ),
887        r(
888            "alertmanager",
889            "Alertmanager",
890            "tcp",
891            vec![9093, 9094],
892            vec!["Alertmanager"],
893            Some(r"Alertmanager/(\d+\.\d+\.\d+)"),
894            vec!["mgmt-ui-exposed"],
895            7,
896        ),
897        r(
898            "loki",
899            "Grafana Loki",
900            "tcp",
901            vec![3100],
902            vec!["loki", "/loki/api/v1"],
903            Some(r"loki/(\d+\.\d+\.\d+)"),
904            vec!["log-server-exposed"],
905            7,
906        ),
907        r(
908            "tempo",
909            "Grafana Tempo",
910            "tcp",
911            vec![3200, 4317, 9411],
912            vec!["tempo", "/api/traces"],
913            None,
914            vec!["trace-server-exposed"],
915            6,
916        ),
917        r(
918            "kibana",
919            "Kibana",
920            "tcp",
921            vec![5601],
922            vec!["kbn-name", "kibana"],
923            Some(r#""version"\s*:\s*"(\d+\.\d+\.\d+)""#),
924            vec!["mgmt-ui-exposed"],
925            9,
926        ),
927        r(
928            "fluentd",
929            "Fluentd",
930            "tcp",
931            vec![24224, 9880],
932            vec!["fluentd"],
933            Some(r"fluentd[/-](\d+\.\d+\.\d+)"),
934            vec!["log-server-exposed"],
935            7,
936        ),
937        r(
938            "logstash",
939            "Logstash",
940            "tcp",
941            vec![5044, 9600],
942            vec!["logstash"],
943            Some(r"logstash[/-](\d+\.\d+\.\d+)"),
944            vec!["log-server-exposed"],
945            7,
946        ),
947        // ── Auth ────────────────────────────────────────────────────
948        r(
949            "keycloak",
950            "Keycloak",
951            "tcp",
952            vec![8080, 8443],
953            vec!["X-Keycloak-", "kc_restart", "keycloak"],
954            Some(r"Keycloak/(\d+\.\d+\.\d+)"),
955            vec!["auth-server-exposed"],
956            8,
957        ),
958        r(
959            "openldap",
960            "OpenLDAP",
961            "tcp",
962            vec![389, 636],
963            vec!["LDAP", "objectClass"],
964            None,
965            vec!["directory-exposed"],
966            8,
967        ),
968        r(
969            "freeradius",
970            "FreeRADIUS",
971            "udp",
972            vec![1812, 1813],
973            vec!["FreeRADIUS"],
974            Some(r"FreeRADIUS\s+Version\s+(\d+\.\d+\.\d+)"),
975            vec!["radius-exposed"],
976            7,
977        ),
978        // ── Mail (more) ──────────────────────────────────────────────
979        r(
980            "dovecot",
981            "Dovecot",
982            "tcp",
983            vec![110, 143, 993, 995],
984            vec!["Dovecot ready", "+OK Dovecot"],
985            Some(r"Dovecot\s+\(?(\d+\.\d+\.\d+)"),
986            vec!["mail-version-disclosure"],
987            9,
988        ),
989        r(
990            "courier-imap",
991            "Courier IMAP",
992            "tcp",
993            vec![143, 993],
994            vec!["Courier-IMAP"],
995            Some(r"Courier-IMAP\s+(\d+\.\d+\.\d+)"),
996            vec!["mail-version-disclosure"],
997            7,
998        ),
999        r(
1000            "sendmail",
1001            "Sendmail",
1002            "tcp",
1003            vec![25, 587],
1004            vec!["Sendmail"],
1005            Some(r"Sendmail\s+(\d+\.\d+\.\d+)"),
1006            vec!["mail-version-disclosure"],
1007            7,
1008        ),
1009        // ── File transfer ────────────────────────────────────────────
1010        r(
1011            "filezilla-server",
1012            "FileZilla Server",
1013            "tcp",
1014            vec![21, 990],
1015            vec!["FileZilla Server"],
1016            Some(r"FileZilla Server\s+(\d+\.\d+\.\d+)"),
1017            vec!["ftp-version-disclosure"],
1018            8,
1019        ),
1020        r(
1021            "pure-ftpd",
1022            "Pure-FTPd",
1023            "tcp",
1024            vec![21],
1025            vec!["Pure-FTPd"],
1026            Some(r"Pure-FTPd\s+(\d+\.\d+\S*)"),
1027            vec!["ftp-version-disclosure"],
1028            8,
1029        ),
1030        r(
1031            "samba",
1032            "Samba",
1033            "tcp",
1034            vec![139, 445],
1035            vec!["Samba", "SMB"],
1036            Some(r"Samba\s+(\d+\.\d+\.\d+)"),
1037            vec!["smb-exposed"],
1038            9,
1039        ),
1040        r(
1041            "nfs",
1042            "NFS",
1043            "tcp",
1044            vec![2049],
1045            vec!["NFS_PROGRAM"],
1046            None,
1047            vec!["nfs-exposed"],
1048            8,
1049        ),
1050        // ── Remote management ────────────────────────────────────────
1051        r(
1052            "ipmi",
1053            "IPMI",
1054            "udp",
1055            vec![623],
1056            vec!["IPMI"],
1057            None,
1058            vec!["ipmi-exposed", "remote-mgmt-exposed"],
1059            10,
1060        ),
1061        r(
1062            "ilo",
1063            "HP iLO",
1064            "tcp",
1065            vec![80, 443, 17988],
1066            vec!["Server: HP-iLO", "iLO"],
1067            Some(r"HP-iLO/(\d+\.\d+)"),
1068            vec!["remote-mgmt-exposed"],
1069            10,
1070        ),
1071        r(
1072            "idrac",
1073            "Dell iDRAC",
1074            "tcp",
1075            vec![80, 443, 5900],
1076            vec!["iDRAC", "Dell Inc."],
1077            Some(r"iDRAC[/\s](\d+)"),
1078            vec!["remote-mgmt-exposed"],
1079            10,
1080        ),
1081        r(
1082            "supermicro-ipmi",
1083            "Supermicro BMC",
1084            "tcp",
1085            vec![80, 443],
1086            vec!["ATEN International", "Supermicro"],
1087            None,
1088            vec!["remote-mgmt-exposed"],
1089            9,
1090        ),
1091        // ── Printing ─────────────────────────────────────────────────
1092        r(
1093            "ipp-cups",
1094            "IPP/CUPS",
1095            "tcp",
1096            vec![631],
1097            vec!["IPP/", "CUPS"],
1098            Some(r"CUPS/(\d+\.\d+\.\d+)"),
1099            vec!["printer-exposed"],
1100            7,
1101        ),
1102        r(
1103            "jetdirect",
1104            "HP JetDirect",
1105            "tcp",
1106            vec![9100],
1107            vec!["@PJL", "HP JetDirect"],
1108            None,
1109            vec!["printer-exposed"],
1110            7,
1111        ),
1112        // ── Legacy / clear-text ──────────────────────────────────────
1113        // Telnet IAC bytes 0xFF/0xFB/0xFD are valid in raw bytes but
1114        // not in a UTF-8 str literal, so we match the post-IAC "login:"
1115        // / "Password:" prompt that almost every telnet daemon emits.
1116        r(
1117            "telnet",
1118            "Telnet",
1119            "tcp",
1120            vec![23, 2323],
1121            vec!["login: ", "Password: ", "Welcome to "],
1122            None,
1123            vec!["cleartext-protocol", "telnet-exposed"],
1124            10,
1125        ),
1126        r(
1127            "snmp",
1128            "SNMP",
1129            "udp",
1130            vec![161, 162],
1131            vec!["public", "private"],
1132            None,
1133            vec!["snmp-exposed"],
1134            9,
1135        ),
1136        r(
1137            "rpcbind",
1138            "rpcbind/portmap",
1139            "tcp",
1140            vec![111],
1141            vec!["rpcbind", "program vers"],
1142            None,
1143            vec!["rpc-exposed"],
1144            7,
1145        ),
1146        // ── DNS servers ──────────────────────────────────────────────
1147        r(
1148            "powerdns",
1149            "PowerDNS",
1150            "udp",
1151            vec![53, 8081],
1152            vec!["PowerDNS"],
1153            Some(r"PowerDNS\s+Authoritative\s+Server\s+(\d+\.\d+\.\d+)"),
1154            vec!["dns-version-disclosure"],
1155            9,
1156        ),
1157        r(
1158            "nsd",
1159            "NLnet NSD",
1160            "udp",
1161            vec![53],
1162            vec!["NSD "],
1163            Some(r"NSD\s+(\d+\.\d+\.\d+)"),
1164            vec!["dns-version-disclosure"],
1165            8,
1166        ),
1167        r(
1168            "knot-dns",
1169            "Knot DNS",
1170            "udp",
1171            vec![53],
1172            vec!["Knot DNS"],
1173            Some(r"Knot DNS\s+(\d+\.\d+\.\d+)"),
1174            vec!["dns-version-disclosure"],
1175            8,
1176        ),
1177        r(
1178            "unbound",
1179            "Unbound",
1180            "udp",
1181            vec![53],
1182            vec!["unbound"],
1183            Some(r"unbound\s+(\d+\.\d+\.\d+)"),
1184            vec!["dns-version-disclosure"],
1185            7,
1186        ),
1187        r(
1188            "dnsdist",
1189            "dnsdist",
1190            "tcp",
1191            vec![53, 5199],
1192            vec!["dnsdist"],
1193            Some(r"dnsdist\s+(\d+\.\d+\.\d+)"),
1194            vec!["dns-version-disclosure"],
1195            7,
1196        ),
1197        // ── Time / NTP / NTS ─────────────────────────────────────────
1198        r(
1199            "ntp",
1200            "NTP",
1201            "udp",
1202            vec![123],
1203            vec!["\x1c"],
1204            None,
1205            vec![],
1206            6,
1207        ),
1208        // ── Misc network services ────────────────────────────────────
1209        r(
1210            "openvpn",
1211            "OpenVPN",
1212            "udp",
1213            vec![1194, 443],
1214            vec!["P_CONTROL", "OpenVPN"],
1215            None,
1216            vec!["vpn-exposed"],
1217            8,
1218        ),
1219        r(
1220            "wireguard",
1221            "WireGuard",
1222            "udp",
1223            vec![51820, 443],
1224            vec!["WireGuard"],
1225            None,
1226            vec!["vpn-exposed"],
1227            7,
1228        ),
1229        r(
1230            "pptp",
1231            "PPTP",
1232            "tcp",
1233            vec![1723],
1234            vec!["pptp"],
1235            None,
1236            vec!["legacy-vpn-exposed"],
1237            8,
1238        ),
1239        r(
1240            "strongswan",
1241            "strongSwan IPsec",
1242            "udp",
1243            vec![500, 4500],
1244            vec!["strongSwan"],
1245            Some(r"strongSwan\s+(\d+\.\d+\.\d+)"),
1246            vec!["vpn-exposed"],
1247            7,
1248        ),
1249        r(
1250            "xrdp",
1251            "xrdp",
1252            "tcp",
1253            vec![3389],
1254            vec!["xrdp"],
1255            Some(r"xrdp\s+(\d+\.\d+\.\d+)"),
1256            vec!["rdp-exposed"],
1257            8,
1258        ),
1259        r(
1260            "vnc",
1261            "VNC",
1262            "tcp",
1263            vec![5900, 5901, 5902],
1264            vec!["RFB 003.", "RFB"],
1265            Some(r"RFB 003\.(\d+)"),
1266            vec!["vnc-exposed", "remote-desktop-exposed"],
1267            9,
1268        ),
1269        // ── Game / streaming / chat ──────────────────────────────────
1270        r(
1271            "minecraft",
1272            "Minecraft Server",
1273            "tcp",
1274            vec![25565],
1275            vec![
1276                "Minecraft Server",
1277                "minecraft.net",
1278                "Vanilla 1.",
1279                "Bukkit on",
1280            ],
1281            None,
1282            vec!["game-server-exposed"],
1283            5,
1284        ),
1285        r(
1286            "ts3",
1287            "TeamSpeak 3",
1288            "udp",
1289            vec![9987, 10011, 30033],
1290            vec!["TS3", "TeaSpeak"],
1291            None,
1292            vec![],
1293            5,
1294        ),
1295        r(
1296            "plex",
1297            "Plex Media Server",
1298            "tcp",
1299            vec![32400],
1300            vec!["X-Plex-", "Plex Media Server"],
1301            Some(r"X-Plex-Version:\s*(\d+\.\d+\.\d+)"),
1302            vec!["media-server-exposed"],
1303            6,
1304        ),
1305        r(
1306            "jellyfin",
1307            "Jellyfin",
1308            "tcp",
1309            vec![8096, 8920],
1310            vec!["X-Application", "Jellyfin"],
1311            Some(r"Jellyfin/(\d+\.\d+\.\d+)"),
1312            vec!["media-server-exposed"],
1313            6,
1314        ),
1315        // ── Industrial / IoT (banner-detectable) ─────────────────────
1316        // Modbus MBAP header is 7 bytes; the ProtocolID at offset 2 is
1317        // always 0x0000 0x0000 plus a 2-byte length and unit ID. A naked
1318        // run of zero bytes is too generic — gate on the typical "len=6,
1319        // unit=1" prefix which catches most server responses without
1320        // matching arbitrary zero-padded buffers.
1321        r(
1322            "modbus",
1323            "Modbus TCP",
1324            "tcp",
1325            vec![502],
1326            vec!["\u{0}\u{0}\u{0}\u{0}\u{0}\u{6}\u{1}"],
1327            None,
1328            vec!["ics-exposed", "modbus-exposed"],
1329            10,
1330        ),
1331        r(
1332            "siemens-s7",
1333            "Siemens S7",
1334            "tcp",
1335            vec![102],
1336            vec!["S7", "ISO-on-TCP"],
1337            None,
1338            vec!["ics-exposed"],
1339            10,
1340        ),
1341        // ── Storage protocols ────────────────────────────────────────
1342        r(
1343            "iscsi",
1344            "iSCSI",
1345            "tcp",
1346            vec![3260],
1347            vec!["iSCSI", "iqn."],
1348            None,
1349            vec!["storage-exposed"],
1350            8,
1351        ),
1352        r(
1353            "minio",
1354            "MinIO",
1355            "tcp",
1356            vec![9000, 9001],
1357            vec!["X-Amz-Bucket-Region", "MinIO", "MinIO Console"],
1358            Some(r"MinIO/(\S+)"),
1359            vec!["object-storage-exposed"],
1360            9,
1361        ),
1362        // ── Search / vector / AI infra ───────────────────────────────
1363        r(
1364            "solr",
1365            "Apache Solr",
1366            "tcp",
1367            vec![8983, 7574],
1368            vec!["Solr", "X-Solr-"],
1369            Some(r"Solr/(\d+\.\d+\.\d+)"),
1370            vec!["search-engine-exposed"],
1371            9,
1372        ),
1373        r(
1374            "opensearch",
1375            "OpenSearch",
1376            "tcp",
1377            vec![9200],
1378            vec!["opensearch", "OpenSearch"],
1379            Some(r#""number"\s*:\s*"(\d+\.\d+\.\d+)""#),
1380            vec!["search-engine-exposed"],
1381            9,
1382        ),
1383        r(
1384            "milvus",
1385            "Milvus",
1386            "tcp",
1387            vec![19530, 9091],
1388            vec!["milvus"],
1389            Some(r"milvus[/\s](\d+\.\d+\.\d+)"),
1390            vec!["vector-db-exposed"],
1391            7,
1392        ),
1393        r(
1394            "qdrant",
1395            "Qdrant",
1396            "tcp",
1397            vec![6333, 6334],
1398            vec!["qdrant", "Qdrant"],
1399            Some(r"Qdrant/(\d+\.\d+\.\d+)"),
1400            vec!["vector-db-exposed"],
1401            7,
1402        ),
1403        r(
1404            "weaviate",
1405            "Weaviate",
1406            "tcp",
1407            vec![8080, 50051],
1408            vec!["weaviate"],
1409            Some(r"weaviate[/\s](\d+\.\d+\.\d+)"),
1410            vec!["vector-db-exposed"],
1411            7,
1412        ),
1413        // ── Misc legacy headline ports ───────────────────────────────
1414        r(
1415            "smb1",
1416            "SMBv1",
1417            "tcp",
1418            vec![139, 445],
1419            vec!["SMB 1.0"],
1420            None,
1421            vec!["smbv1-exposed", "deprecated-protocol"],
1422            10,
1423        ),
1424        r(
1425            "rsync",
1426            "rsync",
1427            "tcp",
1428            vec![873],
1429            vec!["@RSYNCD:"],
1430            Some(r"@RSYNCD:\s*(\d+\.\d+)"),
1431            vec!["rsync-exposed"],
1432            8,
1433        ),
1434    ]
1435}
1436
1437#[cfg(test)]
1438mod tests {
1439    use super::*;
1440
1441    #[test]
1442    fn builtin_rules_not_empty() {
1443        let rules = builtin_rules();
1444        assert!(rules.len() > 10, "should have substantial rule coverage");
1445    }
1446
1447    #[test]
1448    fn all_rules_have_unique_ids() {
1449        let rules = builtin_rules();
1450        let mut seen = std::collections::HashSet::new();
1451        for rule in &rules {
1452            assert!(seen.insert(&rule.id), "duplicate rule id: {}", rule.id);
1453        }
1454    }
1455
1456    #[test]
1457    fn all_rules_have_patterns() {
1458        for rule in builtin_rules() {
1459            assert!(
1460                !rule.patterns.is_empty(),
1461                "rule {} has no patterns",
1462                rule.id
1463            );
1464        }
1465    }
1466}