Skip to main content

memf_strings/
lib.rs

1#![deny(unsafe_code)]
2#![warn(missing_docs)]
3//! String extraction and IoC classification for memory forensics.
4
5pub mod classify;
6pub mod extract;
7pub mod from_file;
8pub mod regex_classifier;
9pub mod yara_classifier;
10pub mod yara_scanner;
11
12/// A string extracted from memory, classified into zero or more categories.
13#[derive(Debug, Clone)]
14pub struct ClassifiedString {
15    /// The extracted string value.
16    pub value: String,
17    /// Physical offset in the memory dump (0 if from a file).
18    pub physical_offset: u64,
19    /// How this string was encoded in memory.
20    pub encoding: StringEncoding,
21    /// Classification results (may be empty for uncategorized strings).
22    pub categories: Vec<(StringCategory, f32)>,
23}
24
25/// String encoding as found in memory.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StringEncoding {
28    /// ASCII (printable bytes 0x20-0x7E).
29    Ascii,
30    /// UTF-8.
31    Utf8,
32    /// UTF-16 Little Endian.
33    Utf16Le,
34}
35
36/// Classification category for an extracted string.
37#[derive(Debug, Clone, PartialEq)]
38pub enum StringCategory {
39    /// URL (http, https, ftp, file, data).
40    Url,
41    /// IPv4 address.
42    IpV4,
43    /// IPv6 address.
44    IpV6,
45    /// Email address.
46    Email,
47    /// Unix file path.
48    UnixPath,
49    /// Windows file path.
50    WindowsPath,
51    /// Windows registry key path.
52    RegistryKey,
53    /// Domain name.
54    DomainName,
55    /// Cryptocurrency address (Bitcoin, Ethereum, Monero).
56    CryptoAddress,
57    /// Private key material (PEM, SSH, etc.).
58    PrivateKey,
59    /// Base64-encoded blob (20+ chars).
60    Base64Blob,
61    /// Shell command or reverse shell indicator.
62    ShellCommand,
63    /// YARA rule match (rule name stored in string).
64    YaraMatch(String),
65}
66
67/// Error type for memf-strings operations.
68#[derive(Debug, thiserror::Error)]
69pub enum Error {
70    /// I/O error.
71    #[error("I/O error: {0}")]
72    Io(#[from] std::io::Error),
73
74    /// Error from the format crate.
75    #[error("format error: {0}")]
76    Format(#[from] memf_format::Error),
77
78    /// YARA compilation error.
79    #[error("YARA error: {0}")]
80    Yara(String),
81}
82
83/// Result alias for memf-strings.
84pub type Result<T> = std::result::Result<T, Error>;
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn classified_string_basic() {
92        let cs = ClassifiedString {
93            value: "https://example.com".into(),
94            physical_offset: 0x1234,
95            encoding: StringEncoding::Ascii,
96            categories: vec![(StringCategory::Url, 0.95)],
97        };
98        assert_eq!(cs.value, "https://example.com");
99        assert_eq!(cs.physical_offset, 0x1234);
100        assert_eq!(cs.categories.len(), 1);
101    }
102
103    #[test]
104    fn error_from_io() {
105        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
106        let err: Error = Error::from(io_err);
107        assert!(matches!(err, Error::Io(_)));
108        assert!(err.to_string().contains("missing"));
109    }
110
111    #[test]
112    fn error_from_format() {
113        let fmt_err = memf_format::Error::UnknownFormat;
114        let err: Error = Error::from(fmt_err);
115        assert!(matches!(err, Error::Format(_)));
116        assert!(err.to_string().contains("unknown dump format"));
117    }
118
119    #[test]
120    fn error_yara_display() {
121        let err = Error::Yara("compilation failed".into());
122        assert!(err.to_string().contains("compilation failed"));
123    }
124
125    #[test]
126    fn string_encoding_variants() {
127        assert_ne!(StringEncoding::Ascii, StringEncoding::Utf16Le);
128        assert_ne!(StringEncoding::Ascii, StringEncoding::Utf8);
129        assert_ne!(StringEncoding::Utf8, StringEncoding::Utf16Le);
130    }
131
132    #[test]
133    fn string_category_equality() {
134        assert_eq!(StringCategory::Url, StringCategory::Url);
135        assert_ne!(StringCategory::Url, StringCategory::Email);
136        assert_eq!(
137            StringCategory::YaraMatch("test".into()),
138            StringCategory::YaraMatch("test".into())
139        );
140        assert_ne!(
141            StringCategory::YaraMatch("a".into()),
142            StringCategory::YaraMatch("b".into())
143        );
144    }
145}