use core::fmt;
pub use forensicnomicon::report::Severity;
impl forensicnomicon::report::Observation for Anomaly {
fn severity(&self) -> Option<Severity> {
Some(self.severity)
}
fn code(&self) -> &'static str {
self.code
}
fn note(&self) -> String {
self.note.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum AnomalyKind {
IncompleteCatalog {
entries_recovered: usize,
},
UnsupportedCodec {
path: String,
codec: char,
},
AbsolutePath {
path: String,
},
ParentTraversal {
path: String,
},
DuplicatePath {
path: String,
},
FutureTimestamp {
path: String,
field: &'static str,
epoch_secs: i64,
},
ControlCharsInName {
path: String,
},
}
impl AnomalyKind {
#[must_use]
pub fn severity(&self) -> Severity {
match self {
AnomalyKind::IncompleteCatalog { .. } => Severity::High,
AnomalyKind::UnsupportedCodec { .. }
| AnomalyKind::AbsolutePath { .. }
| AnomalyKind::ParentTraversal { .. } => Severity::Medium,
AnomalyKind::DuplicatePath { .. }
| AnomalyKind::FutureTimestamp { .. }
| AnomalyKind::ControlCharsInName { .. } => Severity::Low,
}
}
#[must_use]
pub fn code(&self) -> &'static str {
match self {
AnomalyKind::IncompleteCatalog { .. } => "DAR-CATALOG-INCOMPLETE",
AnomalyKind::UnsupportedCodec { .. } => "DAR-CODEC-UNSUPPORTED",
AnomalyKind::AbsolutePath { .. } => "DAR-PATH-ABSOLUTE",
AnomalyKind::ParentTraversal { .. } => "DAR-PATH-TRAVERSAL",
AnomalyKind::DuplicatePath { .. } => "DAR-PATH-DUPLICATE",
AnomalyKind::FutureTimestamp { .. } => "DAR-TIME-FUTURE",
AnomalyKind::ControlCharsInName { .. } => "DAR-NAME-CONTROL",
}
}
#[must_use]
pub fn note(&self) -> String {
match self {
AnomalyKind::IncompleteCatalog { entries_recovered } => format!(
"catalogue parsing stopped after recovering {entries_recovered} entries, before a \
clean root end-of-directory — the listing may be truncated; consistent with a \
partial or damaged archive, or an entry type this reader does not model"
),
AnomalyKind::UnsupportedCodec { path, codec } => format!(
"entry `{path}` is compressed with the `{codec}` codec, which this reader recognises \
but cannot decode — the entry is listed, but its data cannot be extracted here"
),
AnomalyKind::AbsolutePath { path } => format!(
"entry `{path}` has an absolute path — DAR stores paths relative to the archive \
root, so on naive extraction this would write outside the destination directory; \
consistent with an archive crafted to overwrite system paths"
),
AnomalyKind::ParentTraversal { path } => format!(
"entry `{path}` contains a `..` parent-directory component — on naive extraction \
this could escape the destination directory (a path-traversal / 'zip-slip' vector)"
),
AnomalyKind::DuplicatePath { path } => format!(
"path `{path}` is recorded by more than one catalogue entry — consistent with a \
crafted archive in which a later entry shadows an earlier one on extraction"
),
AnomalyKind::FutureTimestamp { path, field, epoch_secs } => format!(
"entry `{path}` {field} is {epoch_secs} (beyond the year 2100) — implausibly far in \
the future; consistent with a misconfigured clock or timestamp tampering"
),
AnomalyKind::ControlCharsInName { path } => format!(
"entry `{path}` contains non-printable control byte(s) in its name — consistent \
with an attempt to obscure the true filename in a terminal listing"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Anomaly {
pub severity: Severity,
pub code: &'static str,
pub kind: AnomalyKind,
pub note: String,
}
impl Anomaly {
#[must_use]
pub fn new(kind: AnomalyKind) -> Self {
Anomaly {
severity: kind.severity(),
code: kind.code(),
note: kind.note(),
kind,
}
}
}
impl fmt::Display for Anomaly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}: {}", self.severity, self.code, self.note)
}
}