Skip to main content

memf_linux/
types.rs

1//! Output types for Linux forensic walkers.
2
3use std::fmt;
4
5/// State of a Linux process.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ProcessState {
8    /// TASK_RUNNING (0).
9    Running,
10    /// TASK_INTERRUPTIBLE (1).
11    Sleeping,
12    /// TASK_UNINTERRUPTIBLE (2).
13    DiskSleep,
14    /// __TASK_STOPPED (4).
15    Stopped,
16    /// __TASK_TRACED (8).
17    Traced,
18    /// EXIT_ZOMBIE (32).
19    Zombie,
20    /// EXIT_DEAD (16).
21    Dead,
22    /// Unknown or unrecognized state value.
23    Unknown(i64),
24}
25
26impl ProcessState {
27    /// Parse a Linux task state value.
28    pub fn from_raw(value: i64) -> Self {
29        match value {
30            0 => Self::Running,
31            1 => Self::Sleeping,
32            2 => Self::DiskSleep,
33            4 => Self::Stopped,
34            8 => Self::Traced,
35            16 => Self::Dead,
36            32 => Self::Zombie,
37            _ => Self::Unknown(value),
38        }
39    }
40}
41
42impl fmt::Display for ProcessState {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Running => write!(f, "R (running)"),
46            Self::Sleeping => write!(f, "S (sleeping)"),
47            Self::DiskSleep => write!(f, "D (disk sleep)"),
48            Self::Stopped => write!(f, "T (stopped)"),
49            Self::Traced => write!(f, "t (traced)"),
50            Self::Zombie => write!(f, "Z (zombie)"),
51            Self::Dead => write!(f, "X (dead)"),
52            Self::Unknown(v) => write!(f, "? ({v})"),
53        }
54    }
55}
56
57/// Information about a Linux process extracted from `task_struct`.
58#[derive(Debug, Clone)]
59pub struct ProcessInfo {
60    /// Process ID.
61    pub pid: u64,
62    /// Parent process ID.
63    pub ppid: u64,
64    /// Process command name (`task_struct.comm`, max 16 chars).
65    pub comm: String,
66    /// Process state.
67    pub state: ProcessState,
68    /// Virtual address of the `task_struct`.
69    pub vaddr: u64,
70    /// Page table root (CR3) from `mm->pgd`, if available.
71    pub cr3: Option<u64>,
72    /// Process start time in nanoseconds since boot.
73    /// Prefers `real_start_time` (CLOCK_BOOTTIME, includes suspend) for
74    /// timeline accuracy; falls back to `start_time` (CLOCK_MONOTONIC) on
75    /// older kernels. Zero if neither field is in the profile.
76    pub start_time: u64,
77}
78
79/// Network protocol.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Protocol {
82    /// TCP (IPv4).
83    Tcp,
84    /// UDP (IPv4).
85    Udp,
86    /// TCP (IPv6).
87    Tcp6,
88    /// UDP (IPv6).
89    Udp6,
90    /// Unix domain socket.
91    Unix,
92    /// Raw socket.
93    Raw,
94}
95
96impl fmt::Display for Protocol {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            Self::Tcp => write!(f, "TCP"),
100            Self::Udp => write!(f, "UDP"),
101            Self::Tcp6 => write!(f, "TCP6"),
102            Self::Udp6 => write!(f, "UDP6"),
103            Self::Unix => write!(f, "UNIX"),
104            Self::Raw => write!(f, "RAW"),
105        }
106    }
107}
108
109/// TCP connection state.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum ConnectionState {
112    /// TCP_ESTABLISHED (1).
113    Established,
114    /// TCP_SYN_SENT (2).
115    SynSent,
116    /// TCP_SYN_RECV (3).
117    SynRecv,
118    /// TCP_FIN_WAIT1 (4).
119    FinWait1,
120    /// TCP_FIN_WAIT2 (5).
121    FinWait2,
122    /// TCP_TIME_WAIT (6).
123    TimeWait,
124    /// TCP_CLOSE (7).
125    Close,
126    /// TCP_CLOSE_WAIT (8).
127    CloseWait,
128    /// TCP_LAST_ACK (9).
129    LastAck,
130    /// TCP_LISTEN (10).
131    Listen,
132    /// TCP_CLOSING (11).
133    Closing,
134    /// Unknown state.
135    Unknown(u8),
136}
137
138impl ConnectionState {
139    /// Parse a raw TCP state value.
140    pub fn from_raw(value: u8) -> Self {
141        match value {
142            1 => Self::Established,
143            2 => Self::SynSent,
144            3 => Self::SynRecv,
145            4 => Self::FinWait1,
146            5 => Self::FinWait2,
147            6 => Self::TimeWait,
148            7 => Self::Close,
149            8 => Self::CloseWait,
150            9 => Self::LastAck,
151            10 => Self::Listen,
152            11 => Self::Closing,
153            _ => Self::Unknown(value),
154        }
155    }
156}
157
158impl fmt::Display for ConnectionState {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            Self::Established => write!(f, "ESTABLISHED"),
162            Self::SynSent => write!(f, "SYN_SENT"),
163            Self::SynRecv => write!(f, "SYN_RECV"),
164            Self::FinWait1 => write!(f, "FIN_WAIT1"),
165            Self::FinWait2 => write!(f, "FIN_WAIT2"),
166            Self::TimeWait => write!(f, "TIME_WAIT"),
167            Self::Close => write!(f, "CLOSE"),
168            Self::CloseWait => write!(f, "CLOSE_WAIT"),
169            Self::LastAck => write!(f, "LAST_ACK"),
170            Self::Listen => write!(f, "LISTEN"),
171            Self::Closing => write!(f, "CLOSING"),
172            Self::Unknown(v) => write!(f, "UNKNOWN({v})"),
173        }
174    }
175}
176
177/// Information about a network connection extracted from kernel memory.
178#[derive(Debug, Clone)]
179pub struct ConnectionInfo {
180    /// Network protocol.
181    pub protocol: Protocol,
182    /// Local IP address as string.
183    pub local_addr: String,
184    /// Local port.
185    pub local_port: u16,
186    /// Remote IP address as string.
187    pub remote_addr: String,
188    /// Remote port.
189    pub remote_port: u16,
190    /// Connection state (TCP only).
191    pub state: ConnectionState,
192    /// PID of the owning process, if determinable.
193    pub pid: Option<u64>,
194}
195
196/// State of a kernel module.
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum ModuleState {
199    /// MODULE_STATE_LIVE.
200    Live,
201    /// MODULE_STATE_COMING.
202    Coming,
203    /// MODULE_STATE_GOING.
204    Going,
205    /// MODULE_STATE_UNFORMED.
206    Unformed,
207    /// Unknown state.
208    Unknown(u32),
209}
210
211impl ModuleState {
212    /// Parse a raw module state value.
213    pub fn from_raw(value: u32) -> Self {
214        match value {
215            0 => Self::Live,
216            1 => Self::Coming,
217            2 => Self::Going,
218            3 => Self::Unformed,
219            _ => Self::Unknown(value),
220        }
221    }
222}
223
224impl fmt::Display for ModuleState {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            Self::Live => write!(f, "Live"),
228            Self::Coming => write!(f, "Coming"),
229            Self::Going => write!(f, "Going"),
230            Self::Unformed => write!(f, "Unformed"),
231            Self::Unknown(v) => write!(f, "Unknown({v})"),
232        }
233    }
234}
235
236/// Information about a loaded kernel module.
237#[derive(Debug, Clone)]
238pub struct ModuleInfo {
239    /// Module name.
240    pub name: String,
241    /// Base virtual address of the module's core section.
242    pub base_addr: u64,
243    /// Size of the module's core section in bytes.
244    pub size: u64,
245    /// Module state.
246    pub state: ModuleState,
247}
248
249// ---------------------------------------------------------------------------
250// Process tree types
251// ---------------------------------------------------------------------------
252
253/// A process tree entry with depth annotation for display.
254///
255/// Used by [`crate::process::build_pstree`] to produce a flat, depth-annotated
256/// list from a process list, suitable for rendering as an indented tree.
257#[derive(Debug, Clone)]
258pub struct PsTreeEntry {
259    /// The process information.
260    pub process: ProcessInfo,
261    /// Tree depth (0 = root, 1 = child of root, etc.).
262    pub depth: u32,
263}
264
265// ---------------------------------------------------------------------------
266// Thread types
267// ---------------------------------------------------------------------------
268
269/// Information about a Linux thread extracted from `task_struct`.
270///
271/// In Linux, threads are `task_struct` entries linked via the `thread_group`
272/// list. Each thread has its own PID (acting as TID) while sharing the
273/// same `tgid` (thread group ID, i.e. the process PID).
274#[derive(Debug, Clone)]
275pub struct ThreadInfo {
276    /// Thread group ID (the process PID, from `task_struct.tgid`).
277    pub tgid: u64,
278    /// Thread ID (the thread's own PID, from `task_struct.pid`).
279    pub tid: u64,
280    /// Thread command name (`task_struct.comm`).
281    pub comm: String,
282    /// Thread state.
283    pub state: ProcessState,
284}
285
286// ---------------------------------------------------------------------------
287// VMA / memory map types
288// ---------------------------------------------------------------------------
289
290/// Permission flags for a virtual memory area.
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292#[allow(clippy::struct_excessive_bools)]
293pub struct VmaFlags {
294    /// VM_READ (0x1).
295    pub read: bool,
296    /// VM_WRITE (0x2).
297    pub write: bool,
298    /// VM_EXEC (0x4).
299    pub exec: bool,
300    /// VM_SHARED (0x8).
301    pub shared: bool,
302}
303
304impl VmaFlags {
305    /// Parse Linux `vm_flags` bitmask.
306    pub fn from_raw(flags: u64) -> Self {
307        Self {
308            read: flags & 0x1 != 0,
309            write: flags & 0x2 != 0,
310            exec: flags & 0x4 != 0,
311            shared: flags & 0x8 != 0,
312        }
313    }
314}
315
316impl fmt::Display for VmaFlags {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        write!(
319            f,
320            "{}{}{}{}",
321            if self.read { 'r' } else { '-' },
322            if self.write { 'w' } else { '-' },
323            if self.exec { 'x' } else { '-' },
324            if self.shared { 's' } else { 'p' },
325        )
326    }
327}
328
329/// Information about a process virtual memory area.
330#[derive(Debug, Clone)]
331pub struct VmaInfo {
332    /// PID of the owning process.
333    pub pid: u64,
334    /// Process name.
335    pub comm: String,
336    /// VMA start virtual address.
337    pub start: u64,
338    /// VMA end virtual address.
339    pub end: u64,
340    /// Permission flags.
341    pub flags: VmaFlags,
342    /// File page offset (`vm_pgoff`).
343    pub pgoff: u64,
344    /// Whether the VMA is file-backed.
345    pub file_backed: bool,
346}
347
348// ---------------------------------------------------------------------------
349// File descriptor types
350// ---------------------------------------------------------------------------
351
352/// Information about an open file descriptor.
353#[derive(Debug, Clone)]
354pub struct FileDescriptorInfo {
355    /// PID of the owning process.
356    pub pid: u64,
357    /// Process name.
358    pub comm: String,
359    /// File descriptor number.
360    pub fd: u32,
361    /// File path (from dentry, if resolvable).
362    pub path: String,
363    /// Inode number, if available.
364    pub inode: Option<u64>,
365    /// File position (f_pos).
366    pub pos: u64,
367}
368
369// ---------------------------------------------------------------------------
370// Environment variable types
371// ---------------------------------------------------------------------------
372
373/// A single environment variable from a process.
374#[derive(Debug, Clone, PartialEq, Eq)]
375pub struct EnvVarInfo {
376    /// PID of the owning process.
377    pub pid: u64,
378    /// Process name.
379    pub comm: String,
380    /// Variable name (e.g. "PATH").
381    pub key: String,
382    /// Variable value.
383    pub value: String,
384}
385
386// ---------------------------------------------------------------------------
387// Command line types
388// ---------------------------------------------------------------------------
389
390/// Process command line extracted from `mm_struct.arg_start`..`arg_end`.
391///
392/// The kernel stores argv as null-separated strings in the process's
393/// address space. This struct holds the reconstructed full command line
394/// with arguments joined by spaces.
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct CmdlineInfo {
397    /// Process ID.
398    pub pid: u64,
399    /// Process name (`task_struct.comm`).
400    pub comm: String,
401    /// Full command line (argv entries joined with spaces).
402    pub cmdline: String,
403}
404
405// ---------------------------------------------------------------------------
406// Malfind types
407// ---------------------------------------------------------------------------
408
409/// A suspicious memory region detected by malfind analysis.
410#[derive(Debug, Clone)]
411pub struct MalfindInfo {
412    /// PID of the owning process.
413    pub pid: u64,
414    /// Process name.
415    pub comm: String,
416    /// VMA start address.
417    pub start: u64,
418    /// VMA end address.
419    pub end: u64,
420    /// VMA permission flags.
421    pub flags: VmaFlags,
422    /// Why this region is suspicious.
423    pub reason: String,
424    /// First 64 bytes of the region (hex dump).
425    pub header_bytes: Vec<u8>,
426}
427
428// ---------------------------------------------------------------------------
429// Mount / filesystem types
430// ---------------------------------------------------------------------------
431
432/// Information about a mounted filesystem.
433#[derive(Debug, Clone)]
434pub struct MountInfo {
435    /// Device name or source.
436    pub dev_name: String,
437    /// Mount point path.
438    pub mount_point: String,
439    /// Filesystem type (e.g. "ext4", "tmpfs").
440    pub fs_type: String,
441}
442
443// ---------------------------------------------------------------------------
444// Syscall table types
445// ---------------------------------------------------------------------------
446
447// ---------------------------------------------------------------------------
448// Bash history types
449// ---------------------------------------------------------------------------
450
451/// A recovered bash command history entry.
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct BashHistoryInfo {
454    /// PID of the bash process.
455    pub pid: u64,
456    /// Process name (usually "bash").
457    pub comm: String,
458    /// The command text.
459    pub command: String,
460    /// Unix timestamp when the command was recorded, if available.
461    pub timestamp: Option<i64>,
462    /// Index of this entry in the history.
463    pub index: u64,
464}
465
466// ---------------------------------------------------------------------------
467// Process cross-view types (psxview)
468// ---------------------------------------------------------------------------
469
470/// Cross-view process visibility information for DKOM detection.
471#[derive(Debug, Clone)]
472pub struct PsxViewInfo {
473    /// Process ID.
474    pub pid: u64,
475    /// Process name.
476    pub comm: String,
477    /// Whether the process was found in the task_struct linked list.
478    pub in_task_list: bool,
479    /// Whether the process was found in the PID hash table.
480    pub in_pid_hash: bool,
481}
482
483// ---------------------------------------------------------------------------
484// TTY check types
485// ---------------------------------------------------------------------------
486
487/// Information about a TTY operations function pointer check.
488#[derive(Debug, Clone)]
489pub struct TtyCheckInfo {
490    /// TTY device name.
491    pub name: String,
492    /// Operation name (e.g. "write", "ioctl").
493    pub operation: String,
494    /// Handler function address.
495    pub handler: u64,
496    /// Whether this handler appears hooked (outside kernel text).
497    pub hooked: bool,
498}
499
500// ---------------------------------------------------------------------------
501// Kernel inline hook types
502// ---------------------------------------------------------------------------
503
504/// Information about a potential inline kernel function hook.
505#[derive(Debug, Clone)]
506pub struct KernelHookInfo {
507    /// Symbol name of the checked function.
508    pub symbol: String,
509    /// Function address.
510    pub address: u64,
511    /// Type of hook detected (e.g. "jmp", "call", "none").
512    pub hook_type: String,
513    /// Target address of the hook, if determinable.
514    pub target: Option<u64>,
515    /// Whether this appears suspicious.
516    pub suspicious: bool,
517}
518
519// ---------------------------------------------------------------------------
520// ELF info types
521// ---------------------------------------------------------------------------
522
523/// ELF object type from the ELF header `e_type` field.
524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum ElfType {
526    /// ET_NONE (0).
527    None,
528    /// ET_REL (1).
529    Relocatable,
530    /// ET_EXEC (2).
531    Executable,
532    /// ET_DYN (3) — shared object / PIE executable.
533    SharedObject,
534    /// ET_CORE (4).
535    Core,
536    /// Unknown type.
537    Unknown(u16),
538}
539
540impl ElfType {
541    /// Parse ELF `e_type` value.
542    pub fn from_raw(value: u16) -> Self {
543        match value {
544            0 => Self::None,
545            1 => Self::Relocatable,
546            2 => Self::Executable,
547            3 => Self::SharedObject,
548            4 => Self::Core,
549            _ => Self::Unknown(value),
550        }
551    }
552}
553
554impl fmt::Display for ElfType {
555    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556        match self {
557            Self::None => write!(f, "NONE"),
558            Self::Relocatable => write!(f, "REL"),
559            Self::Executable => write!(f, "EXEC"),
560            Self::SharedObject => write!(f, "DYN"),
561            Self::Core => write!(f, "CORE"),
562            Self::Unknown(v) => write!(f, "UNKNOWN({v})"),
563        }
564    }
565}
566
567/// Information about an ELF binary found in process memory.
568#[derive(Debug, Clone)]
569pub struct ElfInfo {
570    /// PID of the process.
571    pub pid: u64,
572    /// Process name.
573    pub comm: String,
574    /// VMA start address where ELF was found.
575    pub vma_start: u64,
576    /// ELF type.
577    pub elf_type: ElfType,
578    /// Machine architecture (e.g. EM_X86_64 = 62).
579    pub machine: u16,
580    /// Entry point address.
581    pub entry_point: u64,
582}
583
584// ---------------------------------------------------------------------------
585// Hidden module types
586// ---------------------------------------------------------------------------
587
588/// Information about a potentially hidden kernel module.
589#[derive(Debug, Clone)]
590pub struct HiddenModuleInfo {
591    /// Module name.
592    pub name: String,
593    /// Base virtual address.
594    pub base_addr: u64,
595    /// Module size in bytes.
596    pub size: u64,
597    /// Whether found in the modules linked list.
598    pub in_modules_list: bool,
599    /// Whether found via kset/sysfs walk.
600    pub in_sysfs: bool,
601}
602
603// ---------------------------------------------------------------------------
604// Syscall table types
605// ---------------------------------------------------------------------------
606
607/// Information about a syscall table entry.
608#[derive(Debug, Clone)]
609pub struct SyscallInfo {
610    /// Syscall number.
611    pub number: u64,
612    /// Address of the handler function.
613    pub handler: u64,
614    /// Whether this entry appears hooked (doesn't match known symbol).
615    pub hooked: bool,
616    /// Name of the expected handler, if known.
617    pub expected_name: Option<String>,
618}
619
620// ---------------------------------------------------------------------------
621// Boot time estimation
622// ---------------------------------------------------------------------------
623
624/// Source of a boot time estimate.
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
626pub enum BootTimeSource {
627    /// Derived from kernel timekeeper (wall_to_monotonic + offs_boot).
628    Timekeeper,
629    /// User-provided via --btime flag (e.g., from /proc/stat btime).
630    UserProvided,
631}
632
633impl std::fmt::Display for BootTimeSource {
634    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
635        match self {
636            Self::Timekeeper => write!(f, "timekeeper"),
637            Self::UserProvided => write!(f, "user-provided"),
638        }
639    }
640}
641
642/// A single boot time estimate from a specific source.
643#[derive(Debug, Clone)]
644pub struct BootTimeEstimate {
645    /// Where this estimate came from.
646    pub source: BootTimeSource,
647    /// Unix epoch seconds of the estimated boot time.
648    pub boot_epoch_secs: i64,
649}
650
651/// Aggregated boot time information from multiple sources.
652///
653/// Holds all collected estimates and detects inconsistencies between
654/// them (clock manipulation indicator in DFIR). The `best_estimate`
655/// is the first (highest-priority) source's epoch value.
656#[derive(Debug, Clone)]
657pub struct BootTimeInfo {
658    /// Best estimated boot epoch (Unix seconds), if any source was available.
659    pub best_estimate: Option<i64>,
660    /// All collected estimates for cross-validation.
661    pub estimates: Vec<BootTimeEstimate>,
662    /// Whether sources disagree beyond the drift threshold (60s).
663    pub inconsistent: bool,
664    /// Maximum drift between any two sources, in seconds.
665    pub max_drift_secs: i64,
666}
667
668/// Drift threshold (seconds) for boot time inconsistency detection.
669const BOOT_TIME_DRIFT_THRESHOLD: i64 = 60;
670
671impl BootTimeInfo {
672    /// Build from a collection of estimates.
673    ///
674    /// The first estimate is treated as highest-priority ("best").
675    /// Inconsistency is flagged when any pair of estimates differs
676    /// by more than 60 seconds.
677    pub fn from_estimates(estimates: Vec<BootTimeEstimate>) -> Self {
678        let best_estimate = estimates.first().map(|e| e.boot_epoch_secs);
679
680        let mut max_drift: i64 = 0;
681        for i in 0..estimates.len() {
682            for j in (i + 1)..estimates.len() {
683                let drift = (estimates[i].boot_epoch_secs - estimates[j].boot_epoch_secs).abs();
684                if drift > max_drift {
685                    max_drift = drift;
686                }
687            }
688        }
689
690        Self {
691            best_estimate,
692            estimates,
693            inconsistent: max_drift > BOOT_TIME_DRIFT_THRESHOLD,
694            max_drift_secs: max_drift,
695        }
696    }
697
698    /// Convert boot-relative nanoseconds to absolute Unix epoch seconds.
699    ///
700    /// Returns `None` if no boot time estimate is available.
701    pub fn absolute_secs(&self, boot_ns: u64) -> Option<i64> {
702        self.best_estimate.map(|epoch| {
703            let boot_secs = i64::try_from(boot_ns / 1_000_000_000).unwrap_or(i64::MAX);
704            epoch + boot_secs
705        })
706    }
707}
708
709// ---------------------------------------------------------------------------
710// ARP / neighbour table types
711// ---------------------------------------------------------------------------
712
713/// NUD (Neighbour Unreachability Detection) state.
714#[derive(Debug, Clone, Copy, PartialEq, Eq)]
715pub enum NeighState {
716    /// NUD_INCOMPLETE (0x01).
717    Incomplete,
718    /// NUD_REACHABLE (0x02).
719    Reachable,
720    /// NUD_STALE (0x04).
721    Stale,
722    /// NUD_DELAY (0x08).
723    Delay,
724    /// NUD_PROBE (0x10).
725    Probe,
726    /// NUD_FAILED (0x20).
727    Failed,
728    /// NUD_PERMANENT (0x80).
729    Permanent,
730    /// Unknown/combined flags.
731    Unknown(u8),
732}
733
734impl NeighState {
735    /// Parse a raw NUD state value.
736    pub fn from_raw(value: u8) -> Self {
737        match value {
738            0x01 => Self::Incomplete,
739            0x02 => Self::Reachable,
740            0x04 => Self::Stale,
741            0x08 => Self::Delay,
742            0x10 => Self::Probe,
743            0x20 => Self::Failed,
744            0x80 => Self::Permanent,
745            _ => Self::Unknown(value),
746        }
747    }
748}
749
750impl std::fmt::Display for NeighState {
751    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
752        match self {
753            Self::Incomplete => write!(f, "INCOMPLETE"),
754            Self::Reachable => write!(f, "REACHABLE"),
755            Self::Stale => write!(f, "STALE"),
756            Self::Delay => write!(f, "DELAY"),
757            Self::Probe => write!(f, "PROBE"),
758            Self::Failed => write!(f, "FAILED"),
759            Self::Permanent => write!(f, "PERMANENT"),
760            Self::Unknown(v) => write!(f, "UNKNOWN(0x{v:02x})"),
761        }
762    }
763}
764
765/// An ARP cache entry from the kernel neighbour table.
766#[derive(Debug, Clone)]
767pub struct ArpEntryInfo {
768    /// IPv4 address of the neighbour.
769    pub ip_addr: String,
770    /// MAC address (6 bytes as colon-separated hex).
771    pub mac_addr: String,
772    /// Network device name (e.g. "eth0").
773    pub dev_name: String,
774    /// NUD state.
775    pub state: NeighState,
776}
777
778// ---------------------------------------------------------------------------
779// Netfilter / iptables types
780// ---------------------------------------------------------------------------
781
782/// An iptables/nftables rule recovered from kernel memory.
783#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
784pub struct NetfilterRuleInfo {
785    /// Table name (e.g. "filter", "nat", "mangle").
786    pub table: String,
787    /// Chain name (e.g. "INPUT", "OUTPUT", "FORWARD").
788    pub chain: String,
789    /// Rule target (e.g. "ACCEPT", "DROP", "REJECT").
790    pub target: String,
791    /// Protocol (e.g. "tcp", "udp", "all").
792    pub protocol: String,
793    /// Source address/mask, if specified.
794    pub source: Option<String>,
795    /// Destination address/mask, if specified.
796    pub destination: Option<String>,
797}
798
799// ---------------------------------------------------------------------------
800// Crontab / scheduled task types
801// ---------------------------------------------------------------------------
802
803/// A crontab entry recovered from process memory.
804#[derive(Debug, Clone, PartialEq, Eq)]
805pub struct CrontabEntry {
806    /// PID of the process where the entry was found.
807    pub pid: u64,
808    /// Process name.
809    pub comm: String,
810    /// The raw crontab line (e.g. "0 * * * * /usr/bin/backup.sh").
811    pub line: String,
812}
813
814// ---------------------------------------------------------------------------
815// SSH key types
816// ---------------------------------------------------------------------------
817
818/// Type of SSH key found in memory.
819#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
820pub enum SshKeyType {
821    /// RSA key (`ssh-rsa`).
822    Rsa,
823    /// Ed25519 key (`ssh-ed25519`).
824    Ed25519,
825    /// DSA key (`ssh-dss`).
826    Dsa,
827    /// ECDSA NIST P-256 key (`ecdsa-sha2-nistp256`).
828    Ecdsa256,
829    /// ECDSA NIST P-384 key (`ecdsa-sha2-nistp384`).
830    Ecdsa384,
831    /// ECDSA NIST P-521 key (`ecdsa-sha2-nistp521`).
832    Ecdsa521,
833    /// Unrecognized key type.
834    Unknown,
835}
836
837impl SshKeyType {
838    /// Parse an SSH key type from its prefix string.
839    ///
840    /// Returns `Unknown` if the prefix is not recognized.
841    pub fn from_prefix(prefix: &str) -> Self {
842        match prefix {
843            "ssh-rsa" => Self::Rsa,
844            "ssh-ed25519" => Self::Ed25519,
845            "ssh-dss" => Self::Dsa,
846            "ecdsa-sha2-nistp256" => Self::Ecdsa256,
847            "ecdsa-sha2-nistp384" => Self::Ecdsa384,
848            "ecdsa-sha2-nistp521" => Self::Ecdsa521,
849            _ => Self::Unknown,
850        }
851    }
852}
853
854impl fmt::Display for SshKeyType {
855    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
856        match self {
857            Self::Rsa => write!(f, "ssh-rsa"),
858            Self::Ed25519 => write!(f, "ssh-ed25519"),
859            Self::Dsa => write!(f, "ssh-dss"),
860            Self::Ecdsa256 => write!(f, "ecdsa-sha2-nistp256"),
861            Self::Ecdsa384 => write!(f, "ecdsa-sha2-nistp384"),
862            Self::Ecdsa521 => write!(f, "ecdsa-sha2-nistp521"),
863            Self::Unknown => write!(f, "unknown"),
864        }
865    }
866}
867
868/// An SSH key artifact found in sshd process memory.
869#[derive(Debug, Clone, serde::Serialize)]
870pub struct SshKeyInfo {
871    /// PID of the sshd process where the key was found.
872    pub pid: u64,
873    /// Type of SSH key.
874    pub key_type: SshKeyType,
875    /// The raw key string (e.g., "ssh-rsa AAAA...").
876    pub key_data: String,
877    /// Comment field if present (e.g., "user@host").
878    pub comment: String,
879}
880
881// ---------------------------------------------------------------------------
882// Batch 1 walker types
883// ---------------------------------------------------------------------------
884
885/// PID namespace vs task list discrepancy — hidden process detection.
886#[derive(Debug, Clone, serde::Serialize)]
887pub struct HiddenProcessInfo {
888    /// Process ID.
889    pub pid: u64,
890    /// Process command name.
891    pub comm: String,
892    /// Whether the process was found in the PID namespace.
893    pub present_in_pid_ns: bool,
894    /// Whether the process was found in the task list.
895    pub present_in_task_list: bool,
896    /// Whether the process was found in the PID hash table.
897    pub present_in_pid_hash: bool,
898}
899
900/// vDSO tampering detection info.
901#[derive(Debug, Clone, serde::Serialize)]
902pub struct VdsoTamperInfo {
903    /// Process ID.
904    pub pid: u64,
905    /// Process command name.
906    pub comm: String,
907    /// Base address of the vDSO mapping.
908    pub vdso_base: u64,
909    /// Size of the vDSO mapping.
910    pub vdso_size: u64,
911    /// Whether the vDSO differs from the canonical kernel copy.
912    pub differs_from_canonical: bool,
913    /// Number of bytes that differ.
914    pub diff_byte_count: usize,
915}
916
917/// User namespace escalation detection info.
918#[derive(Debug, Clone, serde::Serialize)]
919pub struct UserNsEscalationInfo {
920    /// Process ID.
921    pub pid: u64,
922    /// Process command name.
923    pub comm: String,
924    /// Nesting depth of the user namespace.
925    pub ns_depth: u32,
926    /// UID that owns the user namespace.
927    pub owner_uid: u32,
928    /// UID of the process.
929    pub process_uid: u32,
930    /// Whether CAP_SYS_ADMIN is mapped in this namespace.
931    pub has_cap_sys_admin: bool,
932    /// Whether this namespace configuration is suspicious.
933    pub is_suspicious: bool,
934}
935
936/// Audit rule suppression / netlink audit tamper info.
937#[derive(Debug, Clone, serde::Serialize)]
938pub struct AuditTamperInfo {
939    /// Whether the audit subsystem is enabled.
940    pub audit_enabled: bool,
941    /// Audit backlog limit.
942    pub backlog_limit: u32,
943    /// PIDs excluded from auditing.
944    pub suppressed_pids: Vec<u64>,
945    /// UIDs excluded from auditing.
946    pub suppressed_uids: Vec<u32>,
947    /// Whether auditing is globally disabled.
948    pub audit_globally_disabled: bool,
949}
950
951/// CPU affinity / cryptominer detection info.
952#[derive(Debug, Clone, serde::Serialize)]
953pub struct CpuPinningInfo {
954    /// Process ID.
955    pub pid: u64,
956    /// Process command name.
957    pub comm: String,
958    /// Number of CPUs this process is restricted to.
959    pub pinned_cpu_count: u32,
960    /// Total number of CPUs on the system.
961    pub total_cpu_count: u32,
962    /// Scheduling policy (SCHED_NORMAL=0, SCHED_BATCH=3, SCHED_IDLE=5).
963    pub sched_policy: u32,
964    /// CPU time in nanoseconds (utime + stime).
965    pub cpu_time_ns: u64,
966}
967
968// ---------------------------------------------------------------------------
969// Batch 2 walker types
970// ---------------------------------------------------------------------------
971
972/// Container escape / breakout detection info.
973#[allow(clippy::struct_excessive_bools)]
974#[derive(Debug, Clone, serde::Serialize)]
975pub struct ContainerEscapeCorrelateInfo {
976    /// Process ID.
977    pub pid: u64,
978    /// Process command name.
979    pub comm: String,
980    /// Whether the PID namespace differs from the cgroup namespace.
981    pub pid_ns_differs_from_cgroup_ns: bool,
982    /// Whether the process has host filesystem mounts visible.
983    pub has_host_mounts: bool,
984    /// Whether the process has CAP_SYS_ADMIN.
985    pub cap_sys_admin: bool,
986    /// Whether the process has CAP_SYS_PTRACE.
987    pub cap_sys_ptrace: bool,
988    /// Whether the process is in a non-init PID namespace.
989    pub in_non_init_pid_ns: bool,
990}
991
992/// Timer/signal FD abuse type.
993#[derive(Debug, Clone, PartialEq, serde::Serialize)]
994pub enum FdAbuseType {
995    /// timerfd.
996    TimerFd,
997    /// signalfd.
998    SignalFd,
999    /// eventfd.
1000    EventFd,
1001}
1002
1003/// Timer/signal FD abuse info.
1004#[derive(Debug, Clone, serde::Serialize)]
1005pub struct FdAbuseInfo {
1006    /// Process ID.
1007    pub pid: u64,
1008    /// Process command name.
1009    pub comm: String,
1010    /// Type of file descriptor being abused.
1011    pub fd_type: FdAbuseType,
1012    /// For signalfd: bitmask of intercepted signals.
1013    pub signal_mask: u64,
1014    /// For timerfd: repeat interval in nanoseconds.
1015    pub interval_ns: u64,
1016    /// Whether the fd is shared across processes.
1017    pub is_cross_process_shared: bool,
1018}
1019
1020/// Shared memory anomaly info.
1021#[allow(clippy::struct_excessive_bools)]
1022#[derive(Debug, Clone, serde::Serialize)]
1023pub struct SharedMemAnomalyInfo {
1024    /// Process ID.
1025    pub pid: u64,
1026    /// Process command name.
1027    pub comm: String,
1028    /// Base address of the shared memory region.
1029    pub shm_base: u64,
1030    /// Size of the shared memory region.
1031    pub shm_size: u64,
1032    /// Whether the region was created with memfd_create.
1033    pub is_memfd: bool,
1034    /// Whether the region is executable.
1035    pub is_executable: bool,
1036    /// Whether the region is shared between processes with different UIDs.
1037    pub is_cross_uid: bool,
1038    /// Whether the region contains an ELF magic header.
1039    pub has_elf_header: bool,
1040}
1041
1042/// FUSE filesystem abuse info.
1043#[derive(Debug, Clone, serde::Serialize)]
1044pub struct FuseAbuseInfo {
1045    /// PID of the FUSE daemon.
1046    pub pid: u64,
1047    /// FUSE daemon command name.
1048    pub comm: String,
1049    /// Mount point path.
1050    pub mount_point: String,
1051    /// Whether the FUSE filesystem is mounted over a sensitive path (/proc, /sys, /etc).
1052    pub is_over_sensitive_path: bool,
1053    /// Whether the FUSE daemon runs as root.
1054    pub daemon_is_root: bool,
1055    /// Whether the `allow_other` mount option is set.
1056    pub allow_other: bool,
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061    use super::*;
1062
1063    #[test]
1064    fn neigh_state_from_raw() {
1065        assert_eq!(NeighState::from_raw(0x01), NeighState::Incomplete);
1066        assert_eq!(NeighState::from_raw(0x02), NeighState::Reachable);
1067        assert_eq!(NeighState::from_raw(0x04), NeighState::Stale);
1068        assert_eq!(NeighState::from_raw(0x08), NeighState::Delay);
1069        assert_eq!(NeighState::from_raw(0x10), NeighState::Probe);
1070        assert_eq!(NeighState::from_raw(0x20), NeighState::Failed);
1071        assert_eq!(NeighState::from_raw(0x80), NeighState::Permanent);
1072        assert!(matches!(
1073            NeighState::from_raw(0xFF),
1074            NeighState::Unknown(0xFF)
1075        ));
1076    }
1077
1078    #[test]
1079    fn neigh_state_display() {
1080        assert_eq!(NeighState::Reachable.to_string(), "REACHABLE");
1081        assert_eq!(NeighState::Stale.to_string(), "STALE");
1082        assert_eq!(NeighState::Permanent.to_string(), "PERMANENT");
1083        assert_eq!(NeighState::Unknown(0x42).to_string(), "UNKNOWN(0x42)");
1084    }
1085
1086    #[test]
1087    fn vma_flags_from_raw() {
1088        let f = VmaFlags::from_raw(0x5); // read + exec
1089        assert!(f.read);
1090        assert!(!f.write);
1091        assert!(f.exec);
1092        assert!(!f.shared);
1093    }
1094
1095    #[test]
1096    fn vma_flags_display() {
1097        assert_eq!(VmaFlags::from_raw(0x7).to_string(), "rwxp"); // r+w+x, private
1098        assert_eq!(VmaFlags::from_raw(0x1).to_string(), "r--p");
1099        assert_eq!(VmaFlags::from_raw(0xF).to_string(), "rwxs"); // shared
1100        assert_eq!(VmaFlags::from_raw(0x0).to_string(), "---p");
1101    }
1102
1103    #[test]
1104    fn process_state_from_raw() {
1105        assert_eq!(ProcessState::from_raw(0), ProcessState::Running);
1106        assert_eq!(ProcessState::from_raw(1), ProcessState::Sleeping);
1107        assert_eq!(ProcessState::from_raw(2), ProcessState::DiskSleep);
1108        assert_eq!(ProcessState::from_raw(4), ProcessState::Stopped);
1109        assert_eq!(ProcessState::from_raw(8), ProcessState::Traced);
1110        assert_eq!(ProcessState::from_raw(16), ProcessState::Dead);
1111        assert_eq!(ProcessState::from_raw(32), ProcessState::Zombie);
1112        assert!(matches!(
1113            ProcessState::from_raw(99),
1114            ProcessState::Unknown(99)
1115        ));
1116    }
1117
1118    #[test]
1119    fn process_state_display() {
1120        assert_eq!(ProcessState::Running.to_string(), "R (running)");
1121        assert_eq!(ProcessState::Sleeping.to_string(), "S (sleeping)");
1122        assert_eq!(ProcessState::DiskSleep.to_string(), "D (disk sleep)");
1123        assert_eq!(ProcessState::Stopped.to_string(), "T (stopped)");
1124        assert_eq!(ProcessState::Traced.to_string(), "t (traced)");
1125        assert_eq!(ProcessState::Dead.to_string(), "X (dead)");
1126        assert_eq!(ProcessState::Zombie.to_string(), "Z (zombie)");
1127        assert_eq!(ProcessState::Unknown(42).to_string(), "? (42)");
1128    }
1129
1130    #[test]
1131    fn connection_state_from_raw() {
1132        assert_eq!(ConnectionState::from_raw(1), ConnectionState::Established);
1133        assert_eq!(ConnectionState::from_raw(2), ConnectionState::SynSent);
1134        assert_eq!(ConnectionState::from_raw(3), ConnectionState::SynRecv);
1135        assert_eq!(ConnectionState::from_raw(4), ConnectionState::FinWait1);
1136        assert_eq!(ConnectionState::from_raw(5), ConnectionState::FinWait2);
1137        assert_eq!(ConnectionState::from_raw(6), ConnectionState::TimeWait);
1138        assert_eq!(ConnectionState::from_raw(7), ConnectionState::Close);
1139        assert_eq!(ConnectionState::from_raw(8), ConnectionState::CloseWait);
1140        assert_eq!(ConnectionState::from_raw(9), ConnectionState::LastAck);
1141        assert_eq!(ConnectionState::from_raw(10), ConnectionState::Listen);
1142        assert_eq!(ConnectionState::from_raw(11), ConnectionState::Closing);
1143        assert!(matches!(
1144            ConnectionState::from_raw(99),
1145            ConnectionState::Unknown(99)
1146        ));
1147    }
1148
1149    #[test]
1150    fn connection_state_display() {
1151        assert_eq!(ConnectionState::Established.to_string(), "ESTABLISHED");
1152        assert_eq!(ConnectionState::SynSent.to_string(), "SYN_SENT");
1153        assert_eq!(ConnectionState::SynRecv.to_string(), "SYN_RECV");
1154        assert_eq!(ConnectionState::FinWait1.to_string(), "FIN_WAIT1");
1155        assert_eq!(ConnectionState::FinWait2.to_string(), "FIN_WAIT2");
1156        assert_eq!(ConnectionState::TimeWait.to_string(), "TIME_WAIT");
1157        assert_eq!(ConnectionState::Close.to_string(), "CLOSE");
1158        assert_eq!(ConnectionState::CloseWait.to_string(), "CLOSE_WAIT");
1159        assert_eq!(ConnectionState::LastAck.to_string(), "LAST_ACK");
1160        assert_eq!(ConnectionState::Listen.to_string(), "LISTEN");
1161        assert_eq!(ConnectionState::Closing.to_string(), "CLOSING");
1162        assert_eq!(ConnectionState::Unknown(42).to_string(), "UNKNOWN(42)");
1163    }
1164
1165    #[test]
1166    fn module_state_from_raw() {
1167        assert_eq!(ModuleState::from_raw(0), ModuleState::Live);
1168        assert_eq!(ModuleState::from_raw(1), ModuleState::Coming);
1169        assert_eq!(ModuleState::from_raw(2), ModuleState::Going);
1170        assert_eq!(ModuleState::from_raw(3), ModuleState::Unformed);
1171        assert!(matches!(
1172            ModuleState::from_raw(99),
1173            ModuleState::Unknown(99)
1174        ));
1175    }
1176
1177    #[test]
1178    fn module_state_display() {
1179        assert_eq!(ModuleState::Live.to_string(), "Live");
1180        assert_eq!(ModuleState::Coming.to_string(), "Coming");
1181        assert_eq!(ModuleState::Going.to_string(), "Going");
1182        assert_eq!(ModuleState::Unformed.to_string(), "Unformed");
1183        assert_eq!(ModuleState::Unknown(42).to_string(), "Unknown(42)");
1184    }
1185
1186    #[test]
1187    fn protocol_display() {
1188        assert_eq!(Protocol::Tcp.to_string(), "TCP");
1189        assert_eq!(Protocol::Udp.to_string(), "UDP");
1190        assert_eq!(Protocol::Tcp6.to_string(), "TCP6");
1191        assert_eq!(Protocol::Udp6.to_string(), "UDP6");
1192        assert_eq!(Protocol::Unix.to_string(), "UNIX");
1193        assert_eq!(Protocol::Raw.to_string(), "RAW");
1194    }
1195
1196    #[test]
1197    fn elf_type_from_raw() {
1198        assert_eq!(ElfType::from_raw(0), ElfType::None);
1199        assert_eq!(ElfType::from_raw(1), ElfType::Relocatable);
1200        assert_eq!(ElfType::from_raw(2), ElfType::Executable);
1201        assert_eq!(ElfType::from_raw(3), ElfType::SharedObject);
1202        assert_eq!(ElfType::from_raw(4), ElfType::Core);
1203        assert!(matches!(ElfType::from_raw(99), ElfType::Unknown(99)));
1204    }
1205
1206    #[test]
1207    fn elf_type_display() {
1208        assert_eq!(ElfType::None.to_string(), "NONE");
1209        assert_eq!(ElfType::Relocatable.to_string(), "REL");
1210        assert_eq!(ElfType::Executable.to_string(), "EXEC");
1211        assert_eq!(ElfType::SharedObject.to_string(), "DYN");
1212        assert_eq!(ElfType::Core.to_string(), "CORE");
1213        assert_eq!(ElfType::Unknown(42).to_string(), "UNKNOWN(42)");
1214    }
1215
1216    // --- Boot time types ---
1217
1218    #[test]
1219    fn boot_time_source_display() {
1220        assert_eq!(BootTimeSource::Timekeeper.to_string(), "timekeeper");
1221        assert_eq!(BootTimeSource::UserProvided.to_string(), "user-provided");
1222    }
1223
1224    #[test]
1225    fn from_estimates_empty_has_no_best() {
1226        let info = BootTimeInfo::from_estimates(vec![]);
1227        assert_eq!(info.best_estimate, None);
1228        assert!(!info.inconsistent);
1229        assert_eq!(info.max_drift_secs, 0);
1230    }
1231
1232    #[test]
1233    fn from_estimates_single_source() {
1234        let info = BootTimeInfo::from_estimates(vec![BootTimeEstimate {
1235            source: BootTimeSource::Timekeeper,
1236            boot_epoch_secs: 1_712_000_000,
1237        }]);
1238        assert_eq!(info.best_estimate, Some(1_712_000_000));
1239        assert!(!info.inconsistent);
1240        assert_eq!(info.max_drift_secs, 0);
1241    }
1242
1243    #[test]
1244    fn from_estimates_consistent_sources() {
1245        let info = BootTimeInfo::from_estimates(vec![
1246            BootTimeEstimate {
1247                source: BootTimeSource::Timekeeper,
1248                boot_epoch_secs: 1_712_000_000,
1249            },
1250            BootTimeEstimate {
1251                source: BootTimeSource::UserProvided,
1252                boot_epoch_secs: 1_712_000_030, // 30s drift (< 60s threshold)
1253            },
1254        ]);
1255        assert_eq!(info.best_estimate, Some(1_712_000_000));
1256        assert!(!info.inconsistent);
1257        assert_eq!(info.max_drift_secs, 30);
1258    }
1259
1260    #[test]
1261    fn from_estimates_inconsistent_sources() {
1262        let info = BootTimeInfo::from_estimates(vec![
1263            BootTimeEstimate {
1264                source: BootTimeSource::Timekeeper,
1265                boot_epoch_secs: 1_712_000_000,
1266            },
1267            BootTimeEstimate {
1268                source: BootTimeSource::UserProvided,
1269                boot_epoch_secs: 1_712_000_120, // 120s drift (> 60s threshold)
1270            },
1271        ]);
1272        assert_eq!(info.best_estimate, Some(1_712_000_000));
1273        assert!(info.inconsistent);
1274        assert_eq!(info.max_drift_secs, 120);
1275    }
1276
1277    #[test]
1278    fn absolute_secs_with_boot_epoch() {
1279        let info = BootTimeInfo::from_estimates(vec![BootTimeEstimate {
1280            source: BootTimeSource::UserProvided,
1281            boot_epoch_secs: 1_712_000_000,
1282        }]);
1283        // 500ms after boot → epoch + 0 (sub-second truncates)
1284        assert_eq!(info.absolute_secs(500_000_000), Some(1_712_000_000));
1285        // 3600s after boot
1286        assert_eq!(info.absolute_secs(3_600_000_000_000), Some(1_712_003_600));
1287    }
1288
1289    #[test]
1290    fn absolute_secs_without_boot_epoch() {
1291        let info = BootTimeInfo::from_estimates(vec![]);
1292        assert_eq!(info.absolute_secs(500_000_000), None);
1293    }
1294
1295    // --- SSH key types ---
1296
1297    #[test]
1298    fn ssh_key_type_from_prefix() {
1299        assert_eq!(SshKeyType::from_prefix("ssh-rsa"), SshKeyType::Rsa);
1300        assert_eq!(SshKeyType::from_prefix("ssh-ed25519"), SshKeyType::Ed25519);
1301        assert_eq!(SshKeyType::from_prefix("ssh-dss"), SshKeyType::Dsa);
1302        assert_eq!(
1303            SshKeyType::from_prefix("ecdsa-sha2-nistp256"),
1304            SshKeyType::Ecdsa256
1305        );
1306        assert_eq!(
1307            SshKeyType::from_prefix("ecdsa-sha2-nistp384"),
1308            SshKeyType::Ecdsa384
1309        );
1310        assert_eq!(
1311            SshKeyType::from_prefix("ecdsa-sha2-nistp521"),
1312            SshKeyType::Ecdsa521
1313        );
1314        assert_eq!(SshKeyType::from_prefix("bogus"), SshKeyType::Unknown);
1315        assert_eq!(SshKeyType::from_prefix(""), SshKeyType::Unknown);
1316    }
1317
1318    #[test]
1319    fn ssh_key_type_display() {
1320        assert_eq!(SshKeyType::Rsa.to_string(), "ssh-rsa");
1321        assert_eq!(SshKeyType::Ed25519.to_string(), "ssh-ed25519");
1322        assert_eq!(SshKeyType::Dsa.to_string(), "ssh-dss");
1323        assert_eq!(SshKeyType::Ecdsa256.to_string(), "ecdsa-sha2-nistp256");
1324        assert_eq!(SshKeyType::Ecdsa384.to_string(), "ecdsa-sha2-nistp384");
1325        assert_eq!(SshKeyType::Ecdsa521.to_string(), "ecdsa-sha2-nistp521");
1326        assert_eq!(SshKeyType::Unknown.to_string(), "unknown");
1327    }
1328}