use ssukka::config::JsStringEncoding;
use ssukka::Obfuscator;
use std::io::{self, Read, Write};
use std::process;
fn main() {
let args: Vec<String> = std::env::args().collect();
match parse_args(&args) {
Ok(opts) => {
if opts.help {
print_usage();
return;
}
if let Err(e) = run(opts) {
eprintln!("ssukka: {e}");
process::exit(1);
}
},
Err(msg) => {
eprintln!("ssukka: {msg}");
eprintln!("Try 'ssukka --help' for more information.");
process::exit(1);
},
}
}
struct CliOptions {
input: Option<String>,
output: Option<String>,
no_rename: bool,
no_minify_css: bool,
no_minify_js: bool,
no_encode_entities: bool,
no_shuffle_attrs: bool,
no_randomize_case: bool,
seed: Option<u64>,
honeypots: Option<usize>,
structural: bool,
polymorphic: bool,
js_encoding: Option<JsStringEncoding>,
js_ast: bool,
mangle: bool,
cff: bool,
dead_code: bool,
dead_code_threshold: Option<f32>,
inline_local: bool,
base_dir: Option<String>,
help: bool,
}
fn parse_args(args: &[String]) -> std::result::Result<CliOptions, String> {
let mut opts = CliOptions {
input: None,
output: None,
no_rename: false,
no_minify_css: false,
no_minify_js: false,
no_encode_entities: false,
no_shuffle_attrs: false,
no_randomize_case: false,
seed: None,
honeypots: None,
structural: false,
polymorphic: false,
js_encoding: None,
js_ast: false,
mangle: false,
cff: false,
dead_code: false,
dead_code_threshold: None,
inline_local: false,
base_dir: None,
help: false,
};
let mut i = 1; while i < args.len() {
match args[i].as_str() {
"-h" | "--help" => {
opts.help = true;
return Ok(opts);
},
"-i" | "--input" => {
i += 1;
if i >= args.len() {
return Err("missing argument for -i".into());
}
opts.input = Some(args[i].clone());
},
"-o" | "--output" => {
i += 1;
if i >= args.len() {
return Err("missing argument for -o".into());
}
opts.output = Some(args[i].clone());
},
"--seed" => {
i += 1;
if i >= args.len() {
return Err("missing argument for --seed".into());
}
opts.seed = Some(
args[i]
.parse::<u64>()
.map_err(|_| format!("invalid seed: {}", args[i]))?,
);
},
"--no-rename" => opts.no_rename = true,
"--no-minify-css" => opts.no_minify_css = true,
"--no-minify-js" => opts.no_minify_js = true,
"--no-encode-entities" => opts.no_encode_entities = true,
"--no-shuffle-attrs" => opts.no_shuffle_attrs = true,
"--no-randomize-case" => opts.no_randomize_case = true,
"--honeypots" => {
i += 1;
if i >= args.len() {
return Err("missing argument for --honeypots".into());
}
let n = args[i]
.parse::<usize>()
.map_err(|_| format!("invalid honeypot count: {}", args[i]))?;
opts.honeypots = Some(n);
},
"--structural" => opts.structural = true,
"--polymorphic" => opts.polymorphic = true,
"--js-string-encoding" => {
i += 1;
if i >= args.len() {
return Err("missing argument for --js-string-encoding".into());
}
opts.js_encoding = Some(match args[i].as_str() {
"none" => JsStringEncoding::None,
"escapes" => JsStringEncoding::Escapes,
"array" => JsStringEncoding::Array,
other => return Err(format!("invalid encoding: {other} (none|escapes|array)")),
});
},
"--js-ast" => opts.js_ast = true,
"--mangle" => opts.mangle = true,
"--cff" => opts.cff = true,
"--dead-code" => opts.dead_code = true,
"--dead-code-threshold" => {
i += 1;
if i >= args.len() {
return Err("missing argument for --dead-code-threshold".into());
}
opts.dead_code_threshold = Some(
args[i]
.parse::<f32>()
.map_err(|_| format!("invalid threshold: {}", args[i]))?,
);
},
"--inline-local-resources" => opts.inline_local = true,
"--base-dir" => {
i += 1;
if i >= args.len() {
return Err("missing argument for --base-dir".into());
}
opts.base_dir = Some(args[i].clone());
},
other => {
return Err(format!("unknown option: {other}"));
},
}
i += 1;
}
Ok(opts)
}
fn run(opts: CliOptions) -> std::result::Result<(), Box<dyn std::error::Error>> {
let html = match &opts.input {
Some(path) => std::fs::read_to_string(path)?,
None => {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
buf
},
};
let mut builder = Obfuscator::builder();
if opts.no_rename {
builder = builder.rename_classes(false).rename_ids(false);
}
if opts.no_minify_css {
builder = builder.minify_css(false);
}
if opts.no_minify_js {
builder = builder.minify_js(false);
}
if opts.no_encode_entities {
builder = builder
.encode_text_entities(false)
.encode_attr_entities(false)
.encode_js_strings(false);
}
if opts.no_shuffle_attrs {
builder = builder.shuffle_attributes(false);
}
if opts.no_randomize_case {
builder = builder.randomize_tag_case(false);
}
if let Some(n) = opts.honeypots {
builder = builder.inject_honeypots(true).honeypot_count(n);
}
if opts.structural {
builder = builder.structural_obfuscation(true);
}
if opts.polymorphic {
builder = builder.polymorphic(true);
}
if let Some(enc) = opts.js_encoding {
builder = builder.js_string_encoding(enc);
}
if opts.js_ast {
builder = builder.js_ast(true);
}
if opts.mangle {
builder = builder.js_ast(true).mangle_identifiers(true);
}
if opts.cff {
builder = builder.js_ast(true).control_flow_flattening(true);
}
if opts.dead_code {
builder = builder.js_ast(true).dead_code_injection(true);
}
if let Some(t) = opts.dead_code_threshold {
builder = builder.dead_code_threshold(t);
}
if opts.inline_local {
builder = builder.inline_local_resources(true);
}
if let Some(dir) = &opts.base_dir {
builder = builder.base_dir(dir.clone());
} else if opts.inline_local {
if let Some(parent) = opts.input.as_deref().and_then(|p| std::path::Path::new(p).parent()) {
if !parent.as_os_str().is_empty() {
builder = builder.base_dir(parent);
}
}
}
if let Some(seed) = opts.seed {
builder = builder.seed(seed);
}
let obfuscator = builder.build();
let result = obfuscator.obfuscate(&html)?;
match &opts.output {
Some(path) => std::fs::write(path, &result)?,
None => {
let stdout = io::stdout();
let mut handle = stdout.lock();
handle.write_all(result.as_bytes())?;
},
}
Ok(())
}
fn print_usage() {
println!(
"ssukka - HTML obfuscation tool
USAGE:
ssukka [OPTIONS]
ssukka -i input.html -o output.html
cat input.html | ssukka > output.html
OPTIONS:
-i, --input <FILE> Input HTML file (default: stdin)
-o, --output <FILE> Output file (default: stdout)
--seed <N> Seed for deterministic output
Cosmetic (on by default):
--no-rename Disable class/ID renaming
--no-minify-css Disable CSS minification
--no-minify-js Disable JS minification
--no-encode-entities Disable entity encoding
--no-shuffle-attrs Disable attribute order shuffling
--no-randomize-case Disable tag case randomization
--js-string-encoding <none|escapes|array>
JS string-literal strategy (default: escapes)
Advanced (opt-in - change DOM/size/runtime; see README threat model):
--honeypots <N> Inject N invisible decoy nodes (scraper traps)
--structural Move text into encoded attrs, restore client-side
--polymorphic Randomize transforms per run (no fixed seed)
--js-ast Use the oxc AST engine for <script> JS
--mangle Scope-aware local identifier renaming (implies --js-ast)
--cff Control-flow flattening (implies --js-ast)
--dead-code Opaque-predicate dead code injection (implies --js-ast)
--dead-code-threshold <0..1> Fraction of sites that get dead code
--inline-local-resources Inline local <link>/<script src> (offline only)
--base-dir <DIR> Base directory for resolving local resources
-h, --help Print this help message"
);
}