1use regex::Regex;
19use std::sync::LazyLock;
20pub mod boot_chain;
21pub mod os_hardening;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Finding {
30 pub label: String,
31 pub value: String,
32 pub detail: Option<String>,
33 pub source: Option<String>,
34 pub line_number: Option<usize>,
40}
41
42impl Finding {
43 fn new(label: &str, value: impl Into<String>) -> Self {
44 Self {
45 label: label.to_string(),
46 value: value.into(),
47 detail: None,
48 source: None,
49 line_number: None,
50 }
51 }
52 fn detail(mut self, d: impl Into<String>) -> Self {
53 self.detail = Some(d.into());
54 self
55 }
56 fn source(mut self, s: impl Into<String>) -> Self {
57 self.source = Some(s.into());
58 self
59 }
60}
61
62pub fn analyze(log: &str) -> Vec<Finding> {
96 let original = split_lines(log);
97 let normalized_owned: Vec<String> = original.iter().map(|l| normalize_line(l)).collect();
98 let normalized: Vec<&str> = normalized_owned.iter().map(String::as_str).collect();
99 let norm_log = normalized.join("\n");
100 let orig_log = original.join("\n");
101 ALL_DETECTORS
102 .iter()
103 .filter_map(|d| {
104 let (joined, lines) = if ORIGINAL_INPUT_LABELS.contains(&d.label) {
105 (&orig_log, &original)
106 } else {
107 (&norm_log, &normalized)
108 };
109 let finding = (d.run)(joined)?;
110 Some(attach_evidence(d, finding, &original, lines))
111 })
112 .collect()
113}
114
115const ORIGINAL_INPUT_LABELS: &[&str] = &["Telnet exposure", "Autoboot interruptable"];
118
119fn split_lines(log: &str) -> Vec<&str> {
124 let mut out = Vec::new();
125 let bytes = log.as_bytes();
126 let mut start = 0usize;
127 let mut i = 0usize;
128 while i < bytes.len() {
129 match bytes[i] {
130 b'\n' => {
131 out.push(&log[start..i]);
132 i += 1;
133 start = i;
134 }
135 b'\r' => {
136 out.push(&log[start..i]);
137 i += if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
139 2
140 } else {
141 1
142 };
143 start = i;
144 }
145 _ => i += 1,
146 }
147 }
148 out.push(&log[start..]);
149 out
150}
151
152static RE_ANSI_CSI: LazyLock<Regex> =
156 LazyLock::new(|| Regex::new(r"\x1b\[[0-?]*[ -/]*[@-~]").unwrap());
157
158static RE_LEADING_TS: LazyLock<Regex> = LazyLock::new(|| {
164 Regex::new(
165 r"^\s*\[(?:\d{2}:\d{2}:\d{2}(?:\.\d+)?|\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?|\s*\d+\.\d+)\]\s*",
166 )
167 .unwrap()
168});
169
170fn normalize_line(line: &str) -> String {
176 let mut s = RE_ANSI_CSI.replace_all(line, "").into_owned();
177 for _ in 0..8 {
180 match RE_LEADING_TS.find(&s) {
181 Some(m) if m.end() > 0 => {
182 s = s[m.end()..].to_string();
183 }
184 _ => break,
185 }
186 }
187 s
188}
189
190fn attach_evidence(d: &Detector, mut f: Finding, original: &[&str], lines: &[&str]) -> Finding {
212 if let Some(i) = lines
213 .iter()
214 .position(|line| (d.run)(line).is_some_and(|c| c.value == f.value && c.detail == f.detail))
215 {
216 f.source = Some(original[i].to_string());
217 f.line_number = Some(i + 1);
218 }
219 f
220}
221
222pub fn detector_labels() -> Vec<&'static str> {
225 ALL_DETECTORS.iter().map(|d| d.label).collect()
226}
227
228pub const SAMPLE: &str = "U-Boot 2020.10 (Sep 17 2023 - 11:38:21 +0000)
233Model: TP-Link Archer C7 v5
234DRAM: 128 MiB
235NAND: ONFI device found
236Hit any key to stop autoboot: 3
237[ 0.000000] Linux version 5.15.137 (builder@buildhost) (mips-openwrt-linux-musl-gcc (OpenWrt GCC 11.2.0 r19685-512e76967f), GNU ld (GNU Binutils) 2.37) #0 SMP Tue Nov 14 19:23:42 2023
238[ 0.000000] CPU0 revision is: 00019374 (MIPS 74Kc)
239[ 0.000000] Determined physical RAM map:
240[ 0.000000] memory: 08000000 @ 00000000 (usable)
241[ 1.234567] mtd: device 0 (boot)
242[ 2.456789] eth0: PHY found at 0x00 (Atheros AR8327)
243[ 3.123456] procd: - early -
244[ 3.789012] procd: - init -
245[ 4.012345] hotplug2: Bootup detected
246[ 4.234567] dropbear[1234]: Not backgrounding
247[ 4.345678] uhttpd[1235]: Listening on 0.0.0.0:80 0.0.0.0:443
248[ 4.456789] dnsmasq[1236]: started, version 2.86 cachesize 150
249[ 5.012345] DHCP client bound to address 192.168.1.42";
250
251pub const CRITICAL_LABELS: &[&str] = &["Autoboot interruptable", "Telnet exposure"];
257
258struct Detector {
264 label: &'static str,
265 run: fn(&str) -> Option<Finding>,
266}
267
268static ALL_DETECTORS: &[Detector] = &[
269 Detector {
270 label: "Bootloader",
271 run: run_bootloader,
272 },
273 Detector {
274 label: "Runtime firmware",
275 run: run_runtime_firmware,
276 },
277 Detector {
278 label: "ROM identifier",
279 run: run_rom_identifier,
280 },
281 Detector {
282 label: "Firmware SDK",
283 run: run_firmware_sdk,
284 },
285 Detector {
286 label: "Kernel",
287 run: run_kernel,
288 },
289 Detector {
290 label: "CPU / Arch",
291 run: run_cpu_arch,
292 },
293 Detector {
294 label: "Userland",
295 run: run_userland,
296 },
297 Detector {
298 label: "Flash layout",
299 run: run_flash_layout,
300 },
301 Detector {
302 label: "Init system",
303 run: run_init_system,
304 },
305 Detector {
306 label: "Device family",
307 run: run_device_family,
308 },
309 Detector {
310 label: "Network",
311 run: run_network,
312 },
313 Detector {
314 label: "Web admin",
315 run: run_web_admin,
316 },
317 Detector {
318 label: "Telnet exposure",
319 run: run_telnet_exposure,
320 },
321 Detector {
322 label: "Autoboot interruptable",
323 run: run_autoboot_interruptable,
324 },
325];
326
327static RE_UBOOT: LazyLock<Regex> =
332 LazyLock::new(|| Regex::new(r"(?m)^U-Boot\s+(\S+(?:[-+][\w.+\-]+)?)\s+\(([^)]+)\)").unwrap());
333static RE_COREBOOT: LazyLock<Regex> =
334 LazyLock::new(|| Regex::new(r"(?m)^coreboot-([\w.\-]+)").unwrap());
335static RE_GRUB: LazyLock<Regex> =
336 LazyLock::new(|| Regex::new(r"(?m)^GRUB\s+(?:version\s+)?([\d.]+)").unwrap());
337static RE_ESP_ROM: LazyLock<Regex> =
338 LazyLock::new(|| Regex::new(r"(?i)(rst:0x1\s+\(POWERON_RESET\)|esp_image:|chip is)").unwrap());
339
340fn run_bootloader(log: &str) -> Option<Finding> {
341 if let Some(m) = RE_UBOOT.captures(log) {
342 let ver = m.get(1)?.as_str();
343 let build = m.get(2)?.as_str();
344 let source = m.get(0)?.as_str();
345 return Some(
346 Finding::new("Bootloader", format!("U-Boot {ver}"))
347 .detail(build)
348 .source(source),
349 );
350 }
351 if let Some(m) = RE_COREBOOT.captures(log) {
352 let ver = m.get(1)?.as_str();
353 let source = m.get(0)?.as_str();
354 return Some(Finding::new("Bootloader", format!("coreboot {ver}")).source(source));
355 }
356 if let Some(m) = RE_GRUB.captures(log) {
357 let ver = m.get(1)?.as_str();
358 let source = m.get(0)?.as_str();
359 return Some(Finding::new("Bootloader", format!("GRUB {ver}")).source(source));
360 }
361 if let Some(m) = RE_ESP_ROM.captures(log) {
362 let source = m.get(0)?.as_str();
363 return Some(
364 Finding::new("Bootloader", "Espressif ROM bootloader")
365 .detail("ESP8266/ESP32")
366 .source(source),
367 );
368 }
369 None
370}
371
372static RE_OPENSBI: LazyLock<Regex> = LazyLock::new(|| {
380 Regex::new(r"(?m)^\s*OpenSBI\s+v?(\d+(?:\.\d+)+(?:[-+][A-Za-z0-9_.+\-]+)?)").unwrap()
381});
382
383fn run_runtime_firmware(log: &str) -> Option<Finding> {
384 let m = RE_OPENSBI.captures(log)?;
385 let ver = m.get(1)?.as_str();
386 Some(Finding::new("Runtime firmware", format!("OpenSBI {ver}")))
387}
388
389static RE_ESP_ROM_ID: LazyLock<Regex> =
397 LazyLock::new(|| Regex::new(r"(?m)^\s*ESP-ROM:([A-Za-z0-9._+\-]+)").unwrap());
398
399fn run_rom_identifier(log: &str) -> Option<Finding> {
400 let m = RE_ESP_ROM_ID.captures(log)?;
401 let id = m.get(1)?.as_str();
402 Some(Finding::new(
403 "ROM identifier",
404 format!("Espressif ROM {id}"),
405 ))
406}
407
408static RE_ESP_IDF: LazyLock<Regex> = LazyLock::new(|| {
415 Regex::new(r"\bboot: ESP-IDF\s+([A-Za-z0-9._+\-]+)\s+2nd stage bootloader\b").unwrap()
416});
417
418fn run_firmware_sdk(log: &str) -> Option<Finding> {
419 let m = RE_ESP_IDF.captures(log)?;
420 let ver = m.get(1)?.as_str();
421 Some(Finding::new("Firmware SDK", format!("ESP-IDF {ver}")))
422}
423
424static RE_LINUX: LazyLock<Regex> = LazyLock::new(|| {
430 Regex::new(r"Linux version[ \t]+(\S+)(?:[ \t]+\([^\r\n)]+\)[ \t]+\(([^\r\n)]+)\))?").unwrap()
431});
432static RE_DARWIN: LazyLock<Regex> =
433 LazyLock::new(|| Regex::new(r"Darwin Kernel Version\s+([^:]+):").unwrap());
434static RE_FREERTOS: LazyLock<Regex> =
435 LazyLock::new(|| Regex::new(r"(?i)FreeRTOS\s+(?:Kernel\s+)?V?([\d.]+)").unwrap());
436static RE_ZEPHYR: LazyLock<Regex> =
437 LazyLock::new(|| Regex::new(r"\*\*\*\s+Booting Zephyr OS build\s+([\w.\-]+)").unwrap());
438
439fn run_kernel(log: &str) -> Option<Finding> {
440 if let Some(m) = RE_LINUX.captures(log) {
441 let ver = m.get(1)?.as_str();
442 let source_full = m.get(0)?.as_str();
443 let source: String = source_full.chars().take(200).collect();
445 let mut f = Finding::new("Kernel", format!("Linux {ver}")).source(source);
446 if let Some(toolchain) = m.get(2) {
447 f = f.detail(toolchain.as_str().chars().take(80).collect::<String>());
448 }
449 return Some(f);
450 }
451 if let Some(m) = RE_DARWIN.captures(log) {
452 let ver = m.get(1)?.as_str().trim();
453 let source = m.get(0)?.as_str();
454 return Some(Finding::new("Kernel", format!("Darwin {ver}")).source(source));
455 }
456 if let Some(m) = RE_FREERTOS.captures(log) {
457 let ver = m.get(1)?.as_str();
458 let source = m.get(0)?.as_str();
459 return Some(Finding::new("Kernel", format!("FreeRTOS {ver}")).source(source));
460 }
461 if let Some(m) = RE_ZEPHYR.captures(log) {
462 let ver = m.get(1)?.as_str();
463 let source = m.get(0)?.as_str();
464 return Some(Finding::new("Kernel", format!("Zephyr {ver}")).source(source));
465 }
466 None
467}
468
469static RE_MIPS: LazyLock<Regex> = LazyLock::new(|| {
472 Regex::new(r"(?i)CPU\s*\d?\s+revision is:\s+\w+\s+\((MIPS\s+[\w\-]+)\)").unwrap()
473});
474static RE_ARM64: LazyLock<Regex> = LazyLock::new(|| {
475 Regex::new(r"(?i)Booting Linux on physical CPU.*aarch64|Linux version.*aarch64").unwrap()
476});
477static RE_ARMV7: LazyLock<Regex> =
478 LazyLock::new(|| Regex::new(r"(?i)CPU:\s+ARMv7|Linux version.*\barmv7l\b").unwrap());
479static RE_RISCV: LazyLock<Regex> =
480 LazyLock::new(|| Regex::new(r"(?i)Linux version.*riscv|hart\s+\d+:\s+running").unwrap());
481static RE_X86: LazyLock<Regex> =
482 LazyLock::new(|| Regex::new(r"(?i)Linux version.*x86_64").unwrap());
483static RE_XTENSA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)\bxtensa\b").unwrap());
487
488fn run_cpu_arch(log: &str) -> Option<Finding> {
489 if let Some(m) = RE_MIPS.captures(log) {
490 return Some(Finding::new("CPU / Arch", m.get(1)?.as_str().to_string()));
491 }
492 if RE_ARM64.is_match(log) {
493 return Some(Finding::new("CPU / Arch", "ARM64 (aarch64)"));
494 }
495 if RE_ARMV7.is_match(log) {
496 return Some(Finding::new("CPU / Arch", "ARMv7 (32-bit)"));
497 }
498 if RE_RISCV.is_match(log) {
499 return Some(Finding::new("CPU / Arch", "RISC-V"));
500 }
501 if RE_X86.is_match(log) {
502 return Some(Finding::new("CPU / Arch", "x86_64"));
503 }
504 if RE_XTENSA.is_match(log) {
505 return Some(Finding::new("CPU / Arch", "Xtensa (ESP)"));
506 }
507 None
508}
509
510static RE_BUSYBOX: LazyLock<Regex> =
517 LazyLock::new(|| Regex::new(r"(?i)BusyBox\s+v?(\d[\d.]*)").unwrap());
518
519fn run_userland(log: &str) -> Option<Finding> {
520 let m = RE_BUSYBOX.captures(log)?;
521 let ver = m.get(1)?.as_str();
522 Some(Finding::new("Userland", format!("BusyBox {ver}")))
523}
524
525static RE_MTD_PART: LazyLock<Regex> = LazyLock::new(|| {
536 Regex::new(r#"(?i)0x0*([0-9a-f]+)-0x0*([0-9a-f]+)\s*:\s*"([A-Za-z0-9_/.\-]+)""#).unwrap()
540});
541
542fn run_flash_layout(log: &str) -> Option<Finding> {
543 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
544 let mut named: Vec<String> = Vec::new();
545 for m in RE_MTD_PART.captures_iter(log) {
546 let (Some(start), Some(end), Some(name)) = (m.get(1), m.get(2), m.get(3)) else {
547 continue;
548 };
549 if !seen.insert(format!(
555 "{}-{}-{}",
556 start.as_str(),
557 end.as_str(),
558 name.as_str()
559 )) {
560 continue;
561 }
562 let size = hex_to_f64(end.as_str()) - hex_to_f64(start.as_str());
563 named.push(format!("{} ({})", name.as_str(), human_size(size)));
564 }
565 if named.is_empty() {
566 return None;
567 }
568 let plural = if named.len() == 1 { "" } else { "s" };
569 Some(
570 Finding::new("Flash layout", format!("{} partition{plural}", named.len()))
571 .detail(named.join(", ")),
572 )
573}
574
575fn hex_to_f64(hex: &str) -> f64 {
578 hex.chars().fold(0.0, |acc, c| {
579 acc * 16.0 + f64::from(c.to_digit(16).unwrap_or(0))
580 })
581}
582
583fn human_size(size: f64) -> String {
594 let kb = (size / 1024.0).round();
595 let kb = if kb == 0.0 { 0.0 } else { kb };
597 if kb < 1024.0 {
598 return format!("{kb}K");
599 }
600 let whole = kb as i128;
601 if whole % 1024 == 0 {
602 return format!("{}M", whole / 1024);
603 }
604 match whole
608 .checked_mul(20)
609 .and_then(|v| v.checked_add(1024))
610 .map(|v| v / 2048)
611 {
612 Some(tenths) => format!("{}.{}M", tenths / 10, tenths % 10),
613 None => format!("{:.1}M", kb / 1024.0),
614 }
615}
616
617static RE_PROCD_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)^procd:").unwrap());
620static RE_PROCD_INIT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"procd:\s+-\s+init").unwrap());
621static RE_SYSTEMD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)systemd\[1\]:").unwrap());
622static RE_SYSV: LazyLock<Regex> =
623 LazyLock::new(|| Regex::new(r"(?i)INIT:\s+version\s+([\d.]+)").unwrap());
624static RE_RUNIT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"runit:|runit-init").unwrap());
625
626fn run_init_system(log: &str) -> Option<Finding> {
627 if RE_PROCD_START.is_match(log) || RE_PROCD_INIT.is_match(log) {
628 return Some(Finding::new("Init system", "procd").detail("OpenWrt-family"));
629 }
630 if RE_SYSTEMD.is_match(log) {
631 return Some(Finding::new("Init system", "systemd"));
632 }
633 if let Some(m) = RE_SYSV.captures(log) {
634 let ver = m.get(1)?.as_str();
635 return Some(Finding::new("Init system", format!("SysV init {ver}")));
636 }
637 if RE_RUNIT.is_match(log) {
638 return Some(Finding::new("Init system", "runit"));
639 }
640 None
641}
642
643static RE_OPENWRT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)openwrt").unwrap());
646static RE_OPENWRT_GCC: LazyLock<Regex> =
647 LazyLock::new(|| Regex::new(r"(?i)OpenWrt GCC[^)]*\d{4}-\w+").unwrap());
648static RE_OPENWRT_R: LazyLock<Regex> =
649 LazyLock::new(|| Regex::new(r"(?i)OpenWrt\s+(r\d+[\-\w]+)").unwrap());
650static RE_RPI: LazyLock<Regex> =
651 LazyLock::new(|| Regex::new(r"(?i)Raspberry\s*Pi|bcm27\d{2}|bcm28\d{2}").unwrap());
652static RE_BUILDROOT: LazyLock<Regex> =
653 LazyLock::new(|| Regex::new(r"(?i)buildroot|br-\d{4}").unwrap());
654static RE_YOCTO: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)yocto|poky-\w+").unwrap());
655static RE_ESP_FAM: LazyLock<Regex> =
656 LazyLock::new(|| Regex::new(r"(?i)(esp32|esp8266|esp_image)").unwrap());
657static RE_MEDIATEK: LazyLock<Regex> =
658 LazyLock::new(|| Regex::new(r"(?i)Mediatek|MT76\d{2}|MT79\d{2}").unwrap());
659static RE_QUALCOMM: LazyLock<Regex> =
660 LazyLock::new(|| Regex::new(r"(?i)Qualcomm|IPQ\d{4}").unwrap());
661static RE_ALLWINNER: LazyLock<Regex> =
662 LazyLock::new(|| Regex::new(r"(?i)(Allwinner|sunxi|H\d{1,3}\s+SoC)").unwrap());
663
664fn run_device_family(log: &str) -> Option<Finding> {
665 if RE_OPENWRT.is_match(log) {
666 let detail = RE_OPENWRT_GCC
668 .find(log)
669 .or_else(|| RE_OPENWRT_R.find(log))
670 .map(|m| m.as_str().chars().take(80).collect::<String>());
671 let mut f = Finding::new("Device family", "OpenWrt");
672 if let Some(d) = detail {
673 f = f.detail(d);
674 }
675 return Some(f);
676 }
677 if RE_RPI.is_match(log) {
678 return Some(Finding::new("Device family", "Raspberry Pi family"));
679 }
680 if RE_BUILDROOT.is_match(log) {
681 return Some(Finding::new("Device family", "Buildroot"));
682 }
683 if RE_YOCTO.is_match(log) {
684 return Some(Finding::new("Device family", "Yocto / Poky"));
685 }
686 if RE_ESP_FAM.is_match(log) {
687 return Some(Finding::new("Device family", "Espressif (ESP)"));
688 }
689 if RE_MEDIATEK.is_match(log) {
690 return Some(Finding::new("Device family", "MediaTek SoC"));
691 }
692 if RE_QUALCOMM.is_match(log) {
693 return Some(Finding::new("Device family", "Qualcomm IPQ"));
694 }
695 if RE_ALLWINNER.is_match(log) {
696 return Some(Finding::new("Device family", "Allwinner sunxi"));
697 }
698 None
699}
700
701static RE_DHCP: LazyLock<Regex> =
704 LazyLock::new(|| Regex::new(r"(?i)DHCP\s+(?:client\s+)?bound to address\s+([\d.]+)").unwrap());
705static RE_DNSMASQ: LazyLock<Regex> =
706 LazyLock::new(|| Regex::new(r"(?i)dnsmasq\[\d+\]:\s+started,\s+version\s+([\d.]+)").unwrap());
707static RE_DROPBEAR: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)dropbear\[\d+\]").unwrap());
708
709fn run_network(log: &str) -> Option<Finding> {
710 if let Some(m) = RE_DHCP.captures(log) {
711 let ip = m.get(1)?.as_str();
712 let source = m.get(0)?.as_str();
713 return Some(
714 Finding::new("Network", "DHCP client active")
715 .detail(format!("Lease: {ip}"))
716 .source(source),
717 );
718 }
719 if let Some(m) = RE_DNSMASQ.captures(log) {
720 let ver = m.get(1)?.as_str();
721 let source = m.get(0)?.as_str();
722 return Some(Finding::new("Network", format!("dnsmasq {ver} active")).source(source));
723 }
724 if let Some(m) = RE_DROPBEAR.find(log) {
725 return Some(Finding::new("Network", "Dropbear SSH started").source(m.as_str()));
726 }
727 None
728}
729
730static RE_UHTTPD: LazyLock<Regex> =
733 LazyLock::new(|| Regex::new(r"(?i)uhttpd\[\d+\]:\s+Listening on\s+([\d.:]+)").unwrap());
734static RE_LIGHTTPD: LazyLock<Regex> =
735 LazyLock::new(|| Regex::new(r"(?i)lighttpd/([\d.]+)").unwrap());
736static RE_NGINX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)nginx/([\d.]+)").unwrap());
737
738fn run_web_admin(log: &str) -> Option<Finding> {
739 if let Some(m) = RE_UHTTPD.captures(log) {
740 let listen = m.get(1)?.as_str();
741 let source = m.get(0)?.as_str();
742 return Some(
743 Finding::new("Web admin", "uhttpd active")
744 .detail(format!("Listen: {listen}"))
745 .source(source),
746 );
747 }
748 if let Some(m) = RE_LIGHTTPD.captures(log) {
749 let ver = m.get(1)?.as_str();
750 let source = m.get(0)?.as_str();
751 return Some(Finding::new("Web admin", format!("lighttpd {ver}")).source(source));
752 }
753 if let Some(m) = RE_NGINX.captures(log) {
754 let ver = m.get(1)?.as_str();
755 let source = m.get(0)?.as_str();
756 return Some(Finding::new("Web admin", format!("nginx {ver}")).source(source));
757 }
758 None
759}
760
761static RE_TELNET: LazyLock<Regex> = LazyLock::new(|| {
764 Regex::new(r"(?i)telnetd?\[\d+\]?[^\n]*?(?:listening|started|on\s+\d)").unwrap()
765});
766
767fn run_telnet_exposure(log: &str) -> Option<Finding> {
768 if let Some(m) = RE_TELNET.find(log) {
769 return Some(
770 Finding::new("Telnet exposure", "Telnet service started")
771 .detail("Clear-text, review immediately")
772 .source(m.as_str()),
773 );
774 }
775 None
776}
777
778static RE_AUTOBOOT_ANY: LazyLock<Regex> =
781 LazyLock::new(|| Regex::new(r"Hit any key to stop autoboot:\s*[1-9]").unwrap());
782static RE_AUTOBOOT_CAP: LazyLock<Regex> =
783 LazyLock::new(|| Regex::new(r"Hit any key to stop autoboot:\s*\d+").unwrap());
784
785fn run_autoboot_interruptable(log: &str) -> Option<Finding> {
786 if RE_AUTOBOOT_ANY.is_match(log) {
787 if let Some(m) = RE_AUTOBOOT_CAP.find(log) {
791 return Some(
792 Finding::new("Autoboot interruptable", "Yes")
793 .detail("U-Boot will accept any key during the countdown")
794 .source(m.as_str()),
795 );
796 }
797 }
798 None
799}