oletools_rs 0.1.0

Rust port of oletools — analysis tools for Microsoft Office files (VBA macros, DDE, OLE objects, RTF exploits)
Documentation
//! DDE field parser — common types and utilities.
//!
//! Provides `DdeField`, QUOTE decoding, and the safe-field blocklist.

use std::sync::LazyLock;

use regex::Regex;

/// A detected DDE field.
#[derive(Debug, Clone)]
pub struct DdeField {
    /// The raw field instruction text.
    pub field_instruction: String,
    /// The DDE command extracted from the field.
    pub command: String,
    /// DDE source application (if identifiable).
    pub source: String,
    /// QUOTE-decoded version of the command (if applicable).
    pub quote_decoded: Option<String>,
}

/// QUOTE field pattern: `QUOTE <decimal_bytes...>`
static RE_QUOTE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"(?i)^\s*QUOTE\s+([\d\s]+)\s*$").unwrap()
});

/// Safe field keywords that are NOT DDE (should be ignored).
const SAFE_FIELDS: &[&str] = &[
    "ADDIN", "ADVANCE", "ASK", "AUTHOR", "AUTONUM", "AUTONUMLGL",
    "AUTONUMOUT", "AUTOTEXT", "AUTOTEXTLIST", "BARCODE", "BIBLIOGRAPHY",
    "BIDIOUTLINE", "BOOKMARK", "CITATION", "COMMENTS", "COMPARE",
    "CREATEDATE", "DATABASE", "DATE", "DISPLAYBARCODE", "DOCPROPERTY",
    "DOCVARIABLE", "EDITTIME", "EQ", "FILENAME", "FILESIZE", "FILLIN",
    "FORMCHECKBOX", "FORMDROPDOWN", "FORMTEXT", "GLOSSARY", "GOTOBUTTON",
    "GREETINGLINE", "HYPERLINK", "IF", "IMPORT", "INCLUDE",
    "INCLUDEPICTURE", "INCLUDETEXT", "INDEX", "INFO", "KEYWORDS",
    "LASTSAVEDBY", "LISTNUM", "MACROBUTTON", "MERGEBARCODE",
    "MERGEFIELD", "MERGEFORMAT", "MERGEREC", "MERGESEQ", "NEXT",
    "NEXTIF", "NOTEREF", "NUMCHARS", "NUMPAGES", "NUMWORDS", "PAGE",
    "PAGEREF", "PRINT", "PRINTDATE", "PRIVATE", "RD", "REF",
    "REVNUM", "SAVEDATE", "SECTION", "SECTIONPAGES", "SEQ", "SET",
    "SHAPE", "SKIPIF", "STYLEREF", "SUBJECT", "SYMBOL", "TA",
    "TEMPLATE", "TIME", "TITLE", "TOA", "TOC", "USERADDRESS",
    "USERINITIALS", "USERNAME", "XE",
];

/// Check if a field instruction is a DDE field (not a safe field).
pub fn is_dde_field(instruction: &str) -> bool {
    let trimmed = instruction.trim();
    if trimmed.is_empty() {
        return false;
    }

    // Extract the first word (field type)
    let first_word = trimmed.split_whitespace().next().unwrap_or("");
    let upper = first_word.to_uppercase();

    // Check against safe fields
    if SAFE_FIELDS.iter().any(|&s| s == upper) {
        return false;
    }

    // Check for explicit DDE/DDEAUTO
    if upper == "DDE" || upper == "DDEAUTO" {
        return true;
    }

    // Unknown field types are potentially suspicious
    // but we only flag explicit DDE
    false
}

/// Parse a DDE field instruction into a `DdeField`.
pub fn parse_dde_field(instruction: &str) -> Option<DdeField> {
    let trimmed = instruction.trim();
    if trimmed.is_empty() {
        return None;
    }

    let first_word = trimmed
        .split_whitespace()
        .next()
        .unwrap_or("")
        .to_uppercase();

    if first_word != "DDE" && first_word != "DDEAUTO" {
        return None;
    }

    // Extract command (everything after the field type)
    let command = trimmed
        .strip_prefix(&first_word)
        .or_else(|| {
            // Case-insensitive strip
            let lower = trimmed.to_lowercase();
            let prefix = first_word.to_lowercase();
            if lower.starts_with(&prefix) {
                Some(&trimmed[prefix.len()..])
            } else {
                None
            }
        })
        .unwrap_or("")
        .trim()
        .to_string();

    // Try to extract source application
    let source = command
        .split_whitespace()
        .next()
        .unwrap_or("")
        .trim_matches('"')
        .to_string();

    Some(DdeField {
        field_instruction: trimmed.to_string(),
        command: command.clone(),
        source,
        quote_decoded: None,
    })
}

/// Decode a QUOTE field: `QUOTE 67 68 69` -> "CDE".
///
/// Each number is interpreted as an ASCII/Unicode code point.
pub fn decode_quote_field(instruction: &str) -> Option<String> {
    let caps = RE_QUOTE.captures(instruction)?;
    let numbers_str = caps.get(1)?.as_str();

    let decoded: String = numbers_str
        .split_whitespace()
        .filter_map(|n| {
            n.parse::<u32>()
                .ok()
                .and_then(char::from_u32)
        })
        .collect();

    if decoded.is_empty() {
        None
    } else {
        Some(decoded)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_dde_field() {
        assert!(is_dde_field("DDE Excel test"));
        assert!(is_dde_field("DDEAUTO cmd.exe /c calc"));
        assert!(!is_dde_field("DATE"));
        assert!(!is_dde_field("PAGE"));
        assert!(!is_dde_field("HYPERLINK http://example.com"));
        assert!(!is_dde_field(""));
    }

    #[test]
    fn test_parse_dde_field() {
        let field = parse_dde_field("DDEAUTO cmd.exe /c calc").unwrap();
        assert_eq!(field.field_instruction, "DDEAUTO cmd.exe /c calc");
        assert_eq!(field.command, "cmd.exe /c calc");
        assert_eq!(field.source, "cmd.exe");
    }

    #[test]
    fn test_parse_dde_field_with_quotes() {
        let field = parse_dde_field(r#"DDE "Excel" "Sheet1!R1C1""#).unwrap();
        assert_eq!(field.source, "Excel");
    }

    #[test]
    fn test_parse_non_dde() {
        assert!(parse_dde_field("DATE").is_none());
        assert!(parse_dde_field("").is_none());
    }

    #[test]
    fn test_decode_quote_field() {
        let result = decode_quote_field("QUOTE 67 68 69").unwrap();
        assert_eq!(result, "CDE");
    }

    #[test]
    fn test_decode_quote_hello() {
        let result = decode_quote_field("QUOTE 72 101 108 108 111").unwrap();
        assert_eq!(result, "Hello");
    }

    #[test]
    fn test_decode_quote_empty() {
        assert!(decode_quote_field("QUOTE").is_none());
        assert!(decode_quote_field("NOT A QUOTE").is_none());
    }

    #[test]
    fn test_safe_fields_coverage() {
        for &field in SAFE_FIELDS {
            assert!(!is_dde_field(field), "{field} should be safe");
        }
    }
}