mod containers_parsing;
mod decode;
pub mod types;
pub use types::{
ArtifactDescriptor, ArtifactLocation, ArtifactParsingProfile, ArtifactQuery, ArtifactRecord,
ArtifactValue, BinaryField, BinaryFieldType, ContainerProfile, ContainerSignature, DataScope,
DecodeError, Decoder, FieldSchema, ForensicCatalog, HiveTarget, OsScope, Platform,
PlatformMask, RecordSignature, TriagePriority, ValueType,
};
pub use containers_parsing::{
all_container_profiles, all_container_signatures, all_parsing_profiles, all_record_signatures,
container_profile, container_signature, parsing_profile, record_signatures_for_container,
};
#[doc(hidden)]
pub use decode::{filetime_to_iso8601, rot13};
impl ForensicCatalog {
pub fn parsing_profile(&self, id: &str) -> Option<&'static ArtifactParsingProfile> {
containers_parsing::parsing_profile(id)
}
pub fn container_profile(&self, id: &str) -> Option<&'static ContainerProfile> {
if let Some(binding) = containers_parsing::artifact_container_bindings()
.iter()
.find(|b| b.artifact_id.eq_ignore_ascii_case(id))
{
return containers_parsing::container_profile(binding.container_id);
}
let desc = self.by_id(id)?;
containers_parsing::infer_container_profile_pub(desc)
}
pub fn container_signature(&self, id: &str) -> Option<&'static ContainerSignature> {
let profile = self.container_profile(id)?;
containers_parsing::container_signature(profile.id)
}
pub fn record_signatures(&self, id: &str) -> Vec<&'static RecordSignature> {
let direct: Vec<&'static RecordSignature> = containers_parsing::all_record_signatures()
.iter()
.filter(|sig| {
sig.artifact_id
.is_some_and(|artifact_id| artifact_id.eq_ignore_ascii_case(id))
})
.collect();
if !direct.is_empty() {
return direct;
}
if let Some(profile) = self.container_profile(id) {
return containers_parsing::record_signatures_for_container(profile.id);
}
Vec::new()
}
pub fn decode(
&self,
descriptor: &ArtifactDescriptor,
name: &str,
raw: &[u8],
) -> Result<ArtifactRecord, DecodeError> {
decode::decode_artifact(descriptor, name, raw)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use types::{DataScope, OsScope, TriagePriority};
fn desc(
id: &'static str,
artifact_type: ArtifactLocation,
hive: Option<HiveTarget>,
) -> ArtifactDescriptor {
ArtifactDescriptor {
id,
name: "n",
artifact_type,
hive,
key_path: "",
value_name: None,
file_path: None,
scope: DataScope::System,
os_scope: OsScope::All,
decoder: Decoder::Identity,
meaning: "m",
mitre_techniques: &[],
fields: &[],
retention: None,
triage_priority: TriagePriority::Medium,
related_artifacts: &[],
sources: &[],
evidence_strength: None,
evidence_caveats: &[],
volatility: None,
volatility_rationale: "",
}
}
static ENTRIES: &[ArtifactDescriptor] = &[
ArtifactDescriptor {
id: "userassist_exe",
name: "UserAssist",
artifact_type: ArtifactLocation::RegistryValue,
hive: Some(HiveTarget::NtUser),
key_path: "",
value_name: None,
file_path: None,
scope: DataScope::User,
os_scope: OsScope::Win7Plus,
decoder: Decoder::Identity,
meaning: "m",
mitre_techniques: &[],
fields: &[],
retention: None,
triage_priority: TriagePriority::High,
related_artifacts: &[],
sources: &[],
evidence_strength: None,
evidence_caveats: &[],
volatility: None,
volatility_rationale: "",
},
ArtifactDescriptor {
id: "reg_only",
name: "RegOnly",
artifact_type: ArtifactLocation::RegistryValue,
hive: Some(HiveTarget::HklmSystem),
key_path: "",
value_name: None,
file_path: None,
scope: DataScope::System,
os_scope: OsScope::All,
decoder: Decoder::Identity,
meaning: "m",
mitre_techniques: &[],
fields: &[],
retention: None,
triage_priority: TriagePriority::Low,
related_artifacts: &[],
sources: &[],
evidence_strength: None,
evidence_caveats: &[],
volatility: None,
volatility_rationale: "",
},
];
fn cat() -> ForensicCatalog {
ForensicCatalog::new(ENTRIES)
}
#[test]
fn parsing_profile_delegates() {
let cat = cat();
assert_eq!(
cat.parsing_profile("userassist_exe").unwrap().artifact_id,
"userassist_exe"
);
assert!(cat.parsing_profile("nope").is_none());
}
#[test]
fn container_profile_binding_infer_and_none() {
let cat = cat();
assert_eq!(
cat.container_profile("windows_timeline").unwrap().id,
"sqlite_database"
);
assert_eq!(
cat.container_profile("reg_only").unwrap().id,
"windows_registry_hive"
);
assert!(cat.container_profile("ghost").is_none());
}
#[test]
fn container_signature_resolves_via_profile() {
let cat = cat();
assert_eq!(
cat.container_signature("windows_timeline")
.unwrap()
.container_id,
"sqlite_database"
);
assert!(cat.container_signature("ghost").is_none());
}
#[test]
fn record_signatures_direct_container_and_empty() {
let cat = cat();
let direct = cat.record_signatures("userassist_exe");
assert_eq!(direct.len(), 1);
assert_eq!(direct[0].id, "userassist_count_payload");
let via_container = cat.record_signatures("windows_timeline");
assert!(via_container.iter().any(|s| s.id == "sqlite_btree_page"));
assert!(cat.record_signatures("ghost").is_empty());
}
#[test]
fn decode_delegates_to_decoder() {
let cat = cat();
let d = desc(
"x",
ArtifactLocation::RegistryValue,
Some(HiveTarget::NtUser),
);
let rec = cat.decode(&d, "val", b"hi").unwrap();
assert_eq!(rec.fields[0], ("value", ArtifactValue::Text("hi".into())));
}
}