use std::cmp::Ordering;
use std::fmt;
use std::sync::{
atomic::{AtomicUsize, Ordering as AtomicOrdering},
Arc,
};
use crate::{deobfuscation::obfuscators::Obfuscator, metadata::token::Token};
pub struct DetectionScore {
score: AtomicUsize,
evidence: boxcar::Vec<DetectionEvidence>,
}
impl DetectionScore {
#[must_use]
pub fn new() -> Self {
Self {
score: AtomicUsize::new(0),
evidence: boxcar::Vec::new(),
}
}
#[must_use]
pub fn with_score(score: usize) -> Self {
Self {
score: AtomicUsize::new(score),
evidence: boxcar::Vec::new(),
}
}
#[must_use]
pub fn score(&self) -> usize {
self.score.load(AtomicOrdering::Relaxed)
}
pub fn evidence(&self) -> impl Iterator<Item = &DetectionEvidence> {
(0..self.evidence.count()).filter_map(|i| self.evidence.get(i))
}
pub fn add(&self, evidence: DetectionEvidence) {
self.score
.fetch_add(evidence.confidence(), AtomicOrdering::Relaxed);
self.evidence.push(evidence);
}
pub fn add_evidence(&self, evidence: DetectionEvidence) {
self.evidence.push(evidence);
}
pub fn set_score(&self, score: usize) {
self.score.store(score, AtomicOrdering::Relaxed);
}
#[must_use]
pub fn meets_threshold(&self, threshold: usize) -> bool {
self.score() >= threshold
}
#[must_use]
pub fn is_confident(&self) -> bool {
self.score() >= 50
}
#[must_use]
pub fn is_high_confidence(&self) -> bool {
self.score() >= 75
}
pub fn merge(&self, other: &DetectionScore) {
self.score.fetch_add(other.score(), AtomicOrdering::Relaxed);
for i in 0..other.evidence.count() {
if let Some(ev) = other.evidence.get(i) {
self.evidence.push(ev.clone());
}
}
}
#[must_use]
pub fn evidence_summary(&self) -> String {
if self.evidence.count() == 0 {
return "no evidence".to_string();
}
self.evidence()
.map(DetectionEvidence::short_description)
.collect::<Vec<_>>()
.join(", ")
}
}
impl Default for DetectionScore {
fn default() -> Self {
Self::new()
}
}
impl Clone for DetectionScore {
fn clone(&self) -> Self {
let new_score = Self::with_score(self.score());
for i in 0..self.evidence.count() {
if let Some(ev) = self.evidence.get(i) {
new_score.evidence.push(ev.clone());
}
}
new_score
}
}
impl fmt::Debug for DetectionScore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DetectionScore")
.field("score", &self.score())
.field("evidence_count", &self.evidence.count())
.finish()
}
}
impl PartialEq for DetectionScore {
fn eq(&self, other: &Self) -> bool {
self.score() == other.score()
}
}
impl Eq for DetectionScore {}
impl PartialOrd for DetectionScore {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DetectionScore {
fn cmp(&self, other: &Self) -> Ordering {
self.score().cmp(&other.score())
}
}
impl fmt::Display for DetectionScore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "score={} ({})", self.score(), self.evidence_summary())
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)] pub enum DetectionEvidence {
Attribute {
name: String,
confidence: usize,
},
TypePattern {
pattern: String,
match_count: usize,
confidence_per_match: usize,
},
BytecodePattern {
name: String,
locations: boxcar::Vec<Token>,
confidence: usize,
},
MetadataPattern {
name: String,
locations: boxcar::Vec<Token>,
confidence: usize,
},
Resource {
name: String,
signature: Vec<u8>,
confidence: usize,
},
Version {
obfuscator: String,
version: String,
confidence: usize,
},
MetadataString {
value: String,
location: String,
confidence: usize,
},
StructuralPattern {
description: String,
confidence: usize,
},
Contradiction {
description: String,
confidence_reduction: usize,
},
EncryptedMethodBodies {
count: usize,
confidence: usize,
},
ArtifactSections {
sections: Vec<String>,
confidence: usize,
},
ConstantDataFields {
field_count: usize,
type_count: usize,
confidence: usize,
},
ProtectionInfrastructure {
description: String,
count: usize,
confidence: usize,
},
}
impl DetectionEvidence {
#[must_use]
pub fn confidence(&self) -> usize {
match self {
Self::TypePattern {
match_count,
confidence_per_match,
..
} => (*match_count * confidence_per_match).min(50), Self::Attribute { confidence, .. }
| Self::BytecodePattern { confidence, .. }
| Self::MetadataPattern { confidence, .. }
| Self::Resource { confidence, .. }
| Self::Version { confidence, .. }
| Self::MetadataString { confidence, .. }
| Self::StructuralPattern { confidence, .. }
| Self::EncryptedMethodBodies { confidence, .. }
| Self::ArtifactSections { confidence, .. }
| Self::ConstantDataFields { confidence, .. }
| Self::ProtectionInfrastructure { confidence, .. } => *confidence,
Self::Contradiction {
confidence_reduction,
..
} => 0_usize.saturating_sub(*confidence_reduction), }
}
#[must_use]
pub fn short_description(&self) -> String {
match self {
Self::Attribute { name, .. } => format!("attr:{name}"),
Self::TypePattern {
pattern,
match_count,
..
} => format!("types:{pattern}x{match_count}"),
Self::BytecodePattern {
name, locations, ..
} => {
format!("bytecode:{}x{}", name, locations.count())
}
Self::MetadataPattern {
name, locations, ..
} => {
format!("metadata:{}x{}", name, locations.count())
}
Self::Resource { name, .. } => format!("resource:{name}"),
Self::Version {
obfuscator,
version,
..
} => format!("version:{obfuscator}@{version}"),
Self::MetadataString { value, .. } => format!("string:{value}"),
Self::StructuralPattern { description, .. } => {
format!("structure:{description}")
}
Self::Contradiction { description, .. } => {
format!("contra:{description}")
}
Self::EncryptedMethodBodies { count, .. } => {
format!("encrypted:{count} methods")
}
Self::ArtifactSections { sections, .. } => {
format!("artifact sections:{}", sections.len())
}
Self::ConstantDataFields {
field_count,
type_count,
..
} => {
format!("constant data:{field_count} fields, {type_count} types")
}
Self::ProtectionInfrastructure {
description, count, ..
} => {
format!("{description}:{count}")
}
}
}
}
#[derive(Clone, Default)]
pub struct DetectionResult {
primary_obfuscator: Option<Arc<dyn Obfuscator>>,
all_detected: Vec<(String, DetectionScore)>,
threshold: usize,
}
impl DetectionResult {
#[must_use]
pub fn empty(threshold: usize) -> Self {
Self {
primary_obfuscator: None,
all_detected: Vec::new(),
threshold,
}
}
#[must_use]
pub fn new(
primary_obfuscator: Option<Arc<dyn Obfuscator>>,
all_detected: Vec<(String, DetectionScore)>,
threshold: usize,
) -> Self {
Self {
primary_obfuscator,
all_detected,
threshold,
}
}
#[must_use]
pub fn detected(&self) -> bool {
self.primary_obfuscator.is_some()
}
#[must_use]
pub fn primary(&self) -> Option<&Arc<dyn Obfuscator>> {
self.primary_obfuscator.as_ref()
}
#[must_use]
pub fn threshold(&self) -> usize {
self.threshold
}
#[must_use]
pub fn all(&self) -> &[(String, DetectionScore)] {
&self.all_detected
}
#[must_use]
pub fn has(&self, obfuscator_id: &str) -> bool {
self.all_detected.iter().any(|(id, _)| id == obfuscator_id)
}
#[must_use]
pub fn summary(&self) -> String {
if let Some(obfuscator) = &self.primary_obfuscator {
let name = obfuscator.name();
let score = self
.all_detected
.iter()
.find(|(oname, _)| *oname == name)
.map_or(0, |(_, s)| s.score());
format!(
"Detected: {} (score={}), {} total candidates",
obfuscator.name(),
score,
self.all_detected.len()
)
} else {
"No obfuscator detected".to_string()
}
}
}
impl fmt::Display for DetectionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary())
}
}
impl fmt::Debug for DetectionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DetectionResult")
.field(
"primary",
&self.primary_obfuscator.as_ref().map(|o| o.name()),
)
.field("all_detected", &self.all_detected)
.field("threshold", &self.threshold)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detection_score_basic() {
let score = DetectionScore::new();
assert_eq!(score.score(), 0);
assert!(!score.is_confident());
score.add(DetectionEvidence::Attribute {
name: "TestAttribute".to_string(),
confidence: 30,
});
assert_eq!(score.score(), 30);
assert!(!score.is_confident());
score.add(DetectionEvidence::TypePattern {
pattern: "*Test*".to_string(),
match_count: 3,
confidence_per_match: 10,
});
assert_eq!(score.score(), 60); assert!(score.is_confident());
}
#[test]
fn test_detection_score_comparison() {
let score1 = DetectionScore::with_score(50);
let score2 = DetectionScore::with_score(75);
let score3 = DetectionScore::with_score(50);
assert!(score2 > score1);
assert!(score1 < score2);
assert_eq!(score1, score3);
}
#[test]
fn test_type_pattern_cap() {
let score = DetectionScore::new();
score.add(DetectionEvidence::TypePattern {
pattern: "*".to_string(),
match_count: 100,
confidence_per_match: 10,
});
assert_eq!(score.score(), 50);
}
#[test]
fn test_detection_result() {
let result = DetectionResult::new(
None,
vec![
("confuserex".to_string(), DetectionScore::with_score(80)),
("dotfuscator".to_string(), DetectionScore::with_score(30)),
],
50,
);
assert!(!result.detected());
assert!(result.primary().is_none());
assert!(result.has("confuserex"));
assert!(result.has("dotfuscator"));
assert!(!result.has("unknown"));
}
#[test]
fn test_evidence_summary() {
let score = DetectionScore::new();
score.add(DetectionEvidence::Attribute {
name: "PoweredBy".to_string(),
confidence: 20,
});
score.add(DetectionEvidence::Version {
obfuscator: "Test".to_string(),
version: "1.0".to_string(),
confidence: 30,
});
let summary = score.evidence_summary();
assert!(summary.contains("attr:PoweredBy"));
assert!(summary.contains("version:Test@1.0"));
}
}