use crate::event::{EventValue, SecurityEvent};
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}
#[derive(Clone, Debug)]
pub struct LogLevelEnforcer {
min_level: LogLevel,
}
impl LogLevelEnforcer {
#[must_use]
pub fn release() -> Self {
Self {
min_level: LogLevel::Info,
}
}
#[must_use]
pub fn debug() -> Self {
Self {
min_level: LogLevel::Trace,
}
}
#[must_use]
pub fn with_min_level(min_level: LogLevel) -> Self {
Self { min_level }
}
#[must_use]
pub fn should_emit(&self, level: LogLevel) -> bool {
level >= self.min_level
}
}
const NON_DEVICE_UUID_KEYS: &[&str] = &[
"event_id",
"parent_event_id",
"request_id",
"trace_id",
"correlation_id",
"session_id",
"transaction_id",
"span_id",
];
#[derive(Clone, Debug)]
pub struct MobileRedactionEngine {
_private: (),
}
impl MobileRedactionEngine {
#[must_use]
pub fn new() -> Self {
Self { _private: () }
}
#[must_use]
pub fn scrub_event(&self, mut event: SecurityEvent) -> SecurityEvent {
let mut new_labels = BTreeMap::new();
for (key, value) in event.labels {
match value {
EventValue::Classified {
value: v,
classification,
} => {
let scrubbed = self.scrub_value(&key, &v);
new_labels.insert(
key,
EventValue::Classified {
value: scrubbed,
classification,
},
);
}
}
}
event.labels = new_labels;
event
}
fn scrub_value(&self, key: &str, value: &str) -> String {
let trimmed = value.trim();
if is_imei(trimmed) {
return "[DEVICE_ID_REDACTED]".to_string();
}
if is_mac_address(trimmed) {
return "[DEVICE_ID_REDACTED]".to_string();
}
if is_gps_coordinates(trimmed) {
return "[LOCATION_REDACTED]".to_string();
}
if is_uuid(trimmed) && is_device_id_key(key) {
return if is_advertising_id_key(key) {
"[AD_ID_REDACTED]".to_string()
} else {
"[DEVICE_ID_REDACTED]".to_string()
};
}
value.to_string()
}
}
impl Default for MobileRedactionEngine {
fn default() -> Self {
Self::new()
}
}
fn is_imei(s: &str) -> bool {
s.len() == 15 && s.bytes().all(|b| b.is_ascii_digit())
}
fn is_mac_address(s: &str) -> bool {
if s.len() != 17 {
return false;
}
let bytes = s.as_bytes();
let separator = bytes[2];
if separator != b':' && separator != b'-' {
return false;
}
for (i, &b) in bytes.iter().enumerate() {
let pos_in_group = i % 3;
if pos_in_group == 2 {
if i == 17 - 1 {
if !b.is_ascii_hexdigit() {
return false;
}
} else if b != separator {
return false;
}
} else if !b.is_ascii_hexdigit() {
return false;
}
}
true
}
fn is_gps_coordinates(s: &str) -> bool {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 2 {
return false;
}
is_decimal_coordinate(parts[0].trim()) && is_decimal_coordinate(parts[1].trim())
}
fn is_decimal_coordinate(s: &str) -> bool {
let s = s.strip_prefix('-').unwrap_or(s);
let dot_parts: Vec<&str> = s.split('.').collect();
if dot_parts.len() != 2 {
return false;
}
let integer = dot_parts[0];
let fraction = dot_parts[1];
if integer.is_empty() || fraction.is_empty() {
return false;
}
if integer.len() > 3 || integer.is_empty() {
return false;
}
integer.bytes().all(|b| b.is_ascii_digit()) && fraction.bytes().all(|b| b.is_ascii_digit())
}
fn is_uuid(s: &str) -> bool {
if s.len() != 36 {
return false;
}
let bytes = s.as_bytes();
if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
return false;
}
for (i, &b) in bytes.iter().enumerate() {
if i == 8 || i == 13 || i == 18 || i == 23 {
continue;
}
if !b.is_ascii_hexdigit() {
return false;
}
}
true
}
fn is_device_id_key(key: &str) -> bool {
let lower = key.to_ascii_lowercase();
if NON_DEVICE_UUID_KEYS.iter().any(|&k| lower == k) {
return false;
}
lower.contains("idfv")
|| lower.contains("idfa")
|| lower.contains("gaid")
|| lower.contains("ad_id")
|| lower.contains("advertising")
|| lower.contains("device")
|| lower.contains("vendor")
|| lower.contains("hardware")
}
fn is_advertising_id_key(key: &str) -> bool {
let lower = key.to_ascii_lowercase();
lower.contains("ad_id")
|| lower.contains("idfa")
|| lower.contains("gaid")
|| lower.contains("advertising")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_imei() {
assert!(is_imei("353456789012345"));
assert!(!is_imei("12345678901234")); assert!(!is_imei("1234567890123456")); assert!(!is_imei("35345678901234a")); }
#[test]
fn test_is_mac_address() {
assert!(is_mac_address("AA:BB:CC:DD:EE:FF"));
assert!(is_mac_address("aa:bb:cc:dd:ee:ff"));
assert!(is_mac_address("AA-BB-CC-DD-EE-FF"));
assert!(!is_mac_address("AA:BB:CC:DD:EE")); assert!(!is_mac_address("AABBCCDDEEFF")); assert!(!is_mac_address("GG:BB:CC:DD:EE:FF")); }
#[test]
fn test_is_gps_coordinates() {
assert!(is_gps_coordinates("37.7749, -122.4194"));
assert!(is_gps_coordinates("-33.8688, 151.2093"));
assert!(is_gps_coordinates("0.0, 0.0"));
assert!(!is_gps_coordinates("hello, world"));
assert!(!is_gps_coordinates("37.7749"));
assert!(!is_gps_coordinates(""));
}
#[test]
fn test_is_uuid() {
assert!(is_uuid("E621E1F8-C36C-495A-93FC-0C247A3E6E5F"));
assert!(is_uuid("38400000-8cf0-11bd-b23e-10b96e40000d"));
assert!(!is_uuid("not-a-uuid"));
assert!(!is_uuid("E621E1F8C36C495A93FC0C247A3E6E5F")); }
#[test]
fn test_log_level_ordering() {
assert!(LogLevel::Error > LogLevel::Warn);
assert!(LogLevel::Warn > LogLevel::Info);
assert!(LogLevel::Info > LogLevel::Debug);
assert!(LogLevel::Debug > LogLevel::Trace);
}
}