Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
517 changes: 377 additions & 140 deletions src/ascii_charts.rs

Large diffs are not rendered by default.

1,731 changes: 1,152 additions & 579 deletions src/behavior_analyzer.rs

Large diffs are not rendered by default.

78 changes: 52 additions & 26 deletions src/deobfuscator.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use regex::Regex;
/// JavaScript Deobfuscator - Rename variables using heuristics
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use regex::Regex;

/// Variable usage context for determining meaningful names
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -112,7 +112,10 @@ impl JSDeobfuscator {
let result = self.apply_renames();

if self.verbose {
println!("[+] Deobfuscation complete! Renamed {} variables", self.renamed_vars.len());
println!(
"[+] Deobfuscation complete! Renamed {} variables",
self.renamed_vars.len()
);
}
result
}
Expand All @@ -122,13 +125,13 @@ impl JSDeobfuscator {
// Find all single-letter, dollar sign, and short minified variable names
// Matches: a, x, H, $, a1, x2, H0, _a, _H, GaD (3 chars with caps)
let patterns = vec![
r"\b([a-z])\b", // Single lowercase: a, x
r"\b([A-Z])\b", // Single uppercase: H, A, L
r"\$", // Dollar sign: $
r"\b([a-z][0-9])\b", // Lowercase + digit: a1, x2
r"\b([A-Z][0-9])\b", // Uppercase + digit: H0, A1
r"\b(_[a-zA-Z])\b", // Underscore + letter: _a, _H
r"\b([A-Z][a-z]+[A-Z][a-z]*)\b", // CamelCase minified: GaD, KaD, UaD
r"\b([a-z])\b", // Single lowercase: a, x
r"\b([A-Z])\b", // Single uppercase: H, A, L
r"\$", // Dollar sign: $
r"\b([a-z][0-9])\b", // Lowercase + digit: a1, x2
r"\b([A-Z][0-9])\b", // Uppercase + digit: H0, A1
r"\b(_[a-zA-Z])\b", // Underscore + letter: _a, _H
r"\b([A-Z][a-z]+[A-Z][a-z]*)\b", // CamelCase minified: GaD, KaD, UaD
];

for pattern in patterns {
Expand Down Expand Up @@ -183,7 +186,10 @@ impl JSDeobfuscator {
ctx.has_query_selector = true;
ctx.is_dom_element = true;
}
if self.code.contains(&format!("{}.addEventListener(", var_name)) {
if self
.code
.contains(&format!("{}.addEventListener(", var_name))
{
ctx.has_add_event_listener = true;
ctx.is_dom_element = true;
}
Expand All @@ -198,20 +204,23 @@ impl JSDeobfuscator {
if self.code.contains(&format!("return {}", var_name)) {
ctx.used_in_return = true;
}
if self.code.contains(&format!("if({})", var_name)) ||
self.code.contains(&format!("if ({})", var_name)) {
if self.code.contains(&format!("if({})", var_name))
|| self.code.contains(&format!("if ({})", var_name))
{
ctx.used_in_condition = true;
}

// Check for error patterns
if self.code.contains(&format!("catch({})", var_name)) ||
self.code.contains(&format!("catch ({})", var_name)) {
if self.code.contains(&format!("catch({})", var_name))
|| self.code.contains(&format!("catch ({})", var_name))
{
ctx.is_error = true;
}

// Check for event patterns
if self.code.contains(&format!("function({}", var_name)) &&
(self.code.contains("event") || self.code.contains("Event")) {
if self.code.contains(&format!("function({}", var_name))
&& (self.code.contains("event") || self.code.contains("Event"))
{
ctx.is_event = true;
}

Expand All @@ -233,7 +242,8 @@ impl JSDeobfuscator {
}

// Check if it's a callback (function passed as argument)
let callback_re = Regex::new(&format!(r"\w+\([^)]*function\s*\([^)]*{}\s*\)", var_name)).unwrap();
let callback_re =
Regex::new(&format!(r"\w+\([^)]*function\s*\([^)]*{}\s*\)", var_name)).unwrap();
if callback_re.is_match(&self.code) {
ctx.is_callback = true;
}
Expand Down Expand Up @@ -350,13 +360,27 @@ impl JSDeobfuscator {
fn get_context_hints(&self, ctx: &VariableContext) -> String {
let mut hints = Vec::new();

if ctx.is_array_like { hints.push("array"); }
if ctx.is_promise { hints.push("promise"); }
if ctx.is_dom_element { hints.push("DOM"); }
if ctx.is_error { hints.push("error"); }
if ctx.is_callback { hints.push("callback"); }
if ctx.has_map_call { hints.push("mapped"); }
if ctx.has_then_call { hints.push("async"); }
if ctx.is_array_like {
hints.push("array");
}
if ctx.is_promise {
hints.push("promise");
}
if ctx.is_dom_element {
hints.push("DOM");
}
if ctx.is_error {
hints.push("error");
}
if ctx.is_callback {
hints.push("callback");
}
if ctx.has_map_call {
hints.push("mapped");
}
if ctx.has_then_call {
hints.push("async");
}

if hints.is_empty() {
"inferred".to_string()
Expand All @@ -379,7 +403,8 @@ mod tests {
a.map(x => x * 2);
return a;
}
"#.to_string();
"#
.to_string();

let mut deobf = JSDeobfuscator::new(code);
deobf.deobfuscate();
Expand All @@ -393,7 +418,8 @@ mod tests {
let code = r#"
var p = fetch(url);
p.then(r => r.json());
"#.to_string();
"#
.to_string();

let mut deobf = JSDeobfuscator::new(code);
deobf.deobfuscate();
Expand Down
15 changes: 15 additions & 0 deletions src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ impl LogDiscovery {

// Known AI tool directories
let search_patterns = vec![
// OpenCode (XDG-compliant paths)
".local/share/opencode", // Data: storage/, log/, snapshot/, tool-output/
".config/opencode", // Config: opencode.json
".local/state/opencode", // State: prompt-history.jsonl, kv.json
".cache/opencode", // Cache
// Claude Code
".claude",
"Library/Application Support/Claude",
Expand Down Expand Up @@ -224,6 +229,16 @@ impl LogDiscovery {
("commandEmbeddings.json", LogType::Cache), // Copilot embeddings
("settingEmbeddings.json", LogType::Cache), // Copilot embeddings
("toolEmbeddingsCache.bin", LogType::Cache), // Copilot embeddings
// OpenCode-specific directories (XDG data dir: ~/.local/share/opencode/)
("storage", LogType::Session), // Session/message/part JSON files
("log", LogType::Debug), // Application logs
("snapshot", LogType::FileHistory), // Snapshots
("tool-output", LogType::Debug), // Tool outputs
// OpenCode state files (XDG state dir: ~/.local/state/opencode/)
("prompt-history.jsonl", LogType::History), // Prompt history
("kv.json", LogType::Cache), // Key-value store
("model.json", LogType::Cache), // Model preferences
("frecency.jsonl", LogType::Cache), // Frecency data
];

for (subdir_name, log_type) in subdirs {
Expand Down
Loading
Loading