#![forbid(unsafe_code)]
use forensicnomicon::report::{Category, Finding, Observation, Severity, Source};
use lnk_core::{drive_type, ShellLink};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LnkAnomaly {
RemovableMediaTarget {
drive_type: u32,
drive_serial: u32,
path: Option<String>,
},
NetworkTarget {
net_name: Option<String>,
},
TrackerMachine {
machine_id: String,
},
}
impl LnkAnomaly {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::RemovableMediaTarget { .. } => "LNK-REMOVABLE-MEDIA-TARGET",
Self::NetworkTarget { .. } => "LNK-NETWORK-TARGET",
Self::TrackerMachine { .. } => "LNK-TRACKER-MACHINE",
}
}
}
impl Observation for LnkAnomaly {
fn severity(&self) -> Option<Severity> {
Some(match self {
Self::RemovableMediaTarget { .. } => Severity::Medium,
Self::NetworkTarget { .. } => Severity::Low,
Self::TrackerMachine { .. } => Severity::Info,
})
}
fn code(&self) -> &'static str {
LnkAnomaly::code(self)
}
fn category(&self) -> Category {
match self {
Self::RemovableMediaTarget { .. } | Self::NetworkTarget { .. } => Category::Threat,
Self::TrackerMachine { .. } => Category::Provenance,
}
}
fn mitre(&self) -> &'static [&'static str] {
match self {
Self::RemovableMediaTarget { .. } => &["T1052.001", "T1091"],
Self::NetworkTarget { .. } => &["T1021"],
Self::TrackerMachine { .. } => &[],
}
}
fn note(&self) -> String {
match self {
Self::RemovableMediaTarget {
drive_type,
drive_serial,
path,
} => format!(
"the link target resolves to a removable/external volume \
(drive_type {drive_type}, drive serial {drive_serial:#010X}{}); consistent with a \
file opened from external media (MITRE T1052.001 / T1091). The volume serial is \
the join key to a peripheral device connection",
path.as_deref()
.map_or_else(String::new, |p| format!(", path {p:?}"))
),
Self::NetworkTarget { net_name } => format!(
"the link carries a network relative link{}; consistent with a file opened from a \
network share (MITRE T1021)",
net_name
.as_deref()
.map_or_else(String::new, |n| format!(" to {n}"))
),
Self::TrackerMachine { machine_id } => format!(
"the tracker block records the origin machine {machine_id:?}; consistent with the \
link having been authored on that machine (attribution)"
),
}
}
}
#[must_use]
pub fn audit(link: &ShellLink) -> Vec<LnkAnomaly> {
let mut out = Vec::new();
if let Some(info) = &link.link_info {
if let Some(vol) = &info.volume_id {
if is_removable_volume(vol.drive_type, vol.drive_serial_number) {
out.push(LnkAnomaly::RemovableMediaTarget {
drive_type: vol.drive_type,
drive_serial: vol.drive_serial_number,
path: info.local_base_path.clone(),
});
}
}
if let Some(cnrl) = &info.common_network_relative_link {
out.push(LnkAnomaly::NetworkTarget {
net_name: cnrl.net_name.clone(),
});
}
}
if let Some(tracker) = &link.tracker {
if !tracker.machine_id.is_empty() {
out.push(LnkAnomaly::TrackerMachine {
machine_id: tracker.machine_id.clone(),
});
}
}
out
}
fn is_removable_volume(drive_type: u32, _drive_serial: u32) -> bool {
drive_type == drive_type::REMOVABLE
}
#[must_use]
pub fn audit_findings(link: &ShellLink, scope: impl Into<String>) -> Vec<Finding> {
let src = source(scope);
audit(link)
.iter()
.map(|a| a.to_finding(src.clone()))
.collect()
}
#[must_use]
pub fn source(scope: impl Into<String>) -> Source {
Source {
analyzer: "lnk-forensic".to_string(),
scope: scope.into(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}
}
#[cfg(test)]
mod tests {
include!("tests.rs");
}