verify,cli: jadx decompile-quality metric with analyze --decompile
This commit is contained in:
parent
2655f83d3c
commit
e9d5e44b9e
4 changed files with 222 additions and 1 deletions
|
|
@ -27,6 +27,96 @@ pub fn java_available() -> bool {
|
||||||
.is_ok_and(|out| out.status.success())
|
.is_ok_and(|out| out.status.success())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `jadx` is available for decompile checks.
|
||||||
|
pub fn jadx_available() -> bool {
|
||||||
|
Command::new("jadx")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.is_ok_and(|out| out.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decompile quality of one jar: how much of the output jadx itself flags.
|
||||||
|
///
|
||||||
|
/// `JADX WARN` markers mean recovered-but-dubious structures; `Failed`
|
||||||
|
/// markers mean unrecovered ones. Obfuscation wins when these climb while
|
||||||
|
/// `java -Xverify:all` stays green: the bytecode runs, but readers drown.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct DecompileReport {
|
||||||
|
/// `.java` files emitted.
|
||||||
|
pub files: usize,
|
||||||
|
/// Files with at least one `JADX WARN`.
|
||||||
|
pub files_with_warn: usize,
|
||||||
|
/// Total `JADX WARN` markers.
|
||||||
|
pub warns: usize,
|
||||||
|
/// `JADX WARN: Failed...` markers (unrecovered structures).
|
||||||
|
pub failed: usize,
|
||||||
|
/// Warn markers by kind (first word after the prefix).
|
||||||
|
pub kinds: std::collections::BTreeMap<String, usize>,
|
||||||
|
/// jadx exit code.
|
||||||
|
pub exit_code: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decompile a jar with jadx into a scratch dir and grade the output.
|
||||||
|
/// `None` when jadx is missing or refuses to start.
|
||||||
|
pub fn decompile_report(jar: &Path) -> Option<DecompileReport> {
|
||||||
|
if !jadx_available() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let scratch = std::env::temp_dir().join(format!("noxis-jadx-rep-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
let output = Command::new("jadx")
|
||||||
|
.arg("-q")
|
||||||
|
.arg("-d")
|
||||||
|
.arg(&scratch)
|
||||||
|
.arg(jar)
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
let mut report = DecompileReport {
|
||||||
|
exit_code: output.status.code(),
|
||||||
|
..DecompileReport::default()
|
||||||
|
};
|
||||||
|
let mut stack = vec![scratch.clone()];
|
||||||
|
while let Some(dir) = stack.pop() {
|
||||||
|
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
stack.push(path);
|
||||||
|
} else if path.extension().is_some_and(|ext| ext == "java") {
|
||||||
|
report.files += 1;
|
||||||
|
let text = std::fs::read(&path)
|
||||||
|
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut file_warns = 0;
|
||||||
|
for line in text.lines() {
|
||||||
|
if let Some(rest) = line.find("JADX WARN:").map(|i| &line[i + 10..]) {
|
||||||
|
let kind: String = rest
|
||||||
|
.trim_start_matches([' ', '\'', '"'])
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("?")
|
||||||
|
.trim_matches(['\'', '"', ':'])
|
||||||
|
.to_string();
|
||||||
|
*report.kinds.entry(kind.clone()).or_insert(0) += 1;
|
||||||
|
if kind == "Failed" {
|
||||||
|
report.failed += 1;
|
||||||
|
}
|
||||||
|
report.warns += 1;
|
||||||
|
file_warns += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if file_warns > 0 {
|
||||||
|
report.files_with_warn += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
Some(report)
|
||||||
|
}
|
||||||
|
|
||||||
/// Проверить байты одного класса настоящим верификатором.
|
/// Проверить байты одного класса настоящим верификатором.
|
||||||
pub fn verify_class_bytes(internal_name: &str, bytes: &[u8]) -> JavaVerdict {
|
pub fn verify_class_bytes(internal_name: &str, bytes: &[u8]) -> JavaVerdict {
|
||||||
if !java_available() {
|
if !java_available() {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ pub mod java;
|
||||||
pub mod leaks;
|
pub mod leaks;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
|
|
||||||
pub use java::{JavaVerdict, java_available, verify_class_bytes, verify_jar};
|
pub use java::{
|
||||||
|
DecompileReport, JavaVerdict, decompile_report, jadx_available, java_available,
|
||||||
|
verify_class_bytes, verify_jar,
|
||||||
|
};
|
||||||
pub use leaks::{RED_PATTERNS, SecretHit, check_patterns, collect_names, find_leaks, scan_secrets};
|
pub use leaks::{RED_PATTERNS, SecretHit, check_patterns, collect_names, find_leaks, scan_secrets};
|
||||||
pub use metrics::{
|
pub use metrics::{
|
||||||
ProgramStats, check_inflation, inflation_ratio, mappings_changed, program_stats,
|
ProgramStats, check_inflation, inflation_ratio, mappings_changed, program_stats,
|
||||||
|
|
|
||||||
108
crates/noxis-verify/tests/decompile.rs
Normal file
108
crates/noxis-verify/tests/decompile.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
//! Decompile-quality metric: jadx must chew a clean jar without warnings.
|
||||||
|
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use noxis_classfile::{ClassBuilder, Code, Insn, Version};
|
||||||
|
use noxis_verify::{decompile_report, jadx_available};
|
||||||
|
|
||||||
|
/// Two classes, one with a branchy method: enough structure for jadx to
|
||||||
|
/// prove it can read the jar, small enough to run in milliseconds.
|
||||||
|
fn sample_jar(path: &std::path::Path) {
|
||||||
|
let loop_code = Code {
|
||||||
|
max_stack: 2,
|
||||||
|
max_locals: 2,
|
||||||
|
insns: vec![
|
||||||
|
Insn::Const(noxis_classfile::Const::Int(0)),
|
||||||
|
Insn::Store {
|
||||||
|
slot: 0,
|
||||||
|
kind: noxis_classfile::SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Mark(0),
|
||||||
|
Insn::Load {
|
||||||
|
slot: 0,
|
||||||
|
kind: noxis_classfile::SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Const(noxis_classfile::Const::Int(10)),
|
||||||
|
Insn::Branch(
|
||||||
|
noxis_classfile::BranchCond::Int(noxis_classfile::Cond::Ge),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
Insn::Load {
|
||||||
|
slot: 0,
|
||||||
|
kind: noxis_classfile::SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Const(noxis_classfile::Const::Int(1)),
|
||||||
|
Insn::Binary {
|
||||||
|
prim: noxis_classfile::Prim::Int,
|
||||||
|
op: noxis_classfile::Arith::Add,
|
||||||
|
},
|
||||||
|
Insn::Store {
|
||||||
|
slot: 0,
|
||||||
|
kind: noxis_classfile::SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Goto(0),
|
||||||
|
Insn::Mark(1),
|
||||||
|
Insn::Load {
|
||||||
|
slot: 0,
|
||||||
|
kind: noxis_classfile::SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Return(Some(noxis_classfile::Prim::Int)),
|
||||||
|
],
|
||||||
|
exception_table: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut program = noxis_classfile::Program {
|
||||||
|
classes: vec![
|
||||||
|
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||||
|
.version(Version::JAVA_21)
|
||||||
|
.method_with_code(0x0009, "run", "()I", loop_code)
|
||||||
|
.build(),
|
||||||
|
ClassBuilder::new("demo/Help", Some("java/lang/Object"))
|
||||||
|
.version(Version::JAVA_21)
|
||||||
|
.build(),
|
||||||
|
],
|
||||||
|
resources: Vec::new(),
|
||||||
|
};
|
||||||
|
// Real frames: jadx grades structure, not just parsing.
|
||||||
|
let hierarchy = noxis_analysis::ClassHierarchy::build(&program);
|
||||||
|
for class in &mut program.classes {
|
||||||
|
for method in &mut class.methods {
|
||||||
|
if method.code.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let snapshot = method.clone();
|
||||||
|
let frames = noxis_classfile::compute_frames(&class.this_class, &snapshot, &hierarchy)
|
||||||
|
.expect("frames");
|
||||||
|
if let Some(code) = method.code.as_mut() {
|
||||||
|
code.frames = frames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let entries = noxis_jar::save(&program).expect("save");
|
||||||
|
noxis_jar::write_file(path, &entries).expect("write jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clean_jar_decompiles_without_warnings() {
|
||||||
|
if !jadx_available() {
|
||||||
|
eprintln!("SKIP: no jadx");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dir = std::env::temp_dir().join(format!("noxis-jadx-test-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).expect("mkdir");
|
||||||
|
let jar = dir.join("in.jar");
|
||||||
|
sample_jar(&jar);
|
||||||
|
let report = decompile_report(&jar).expect("jadx runs");
|
||||||
|
assert!(
|
||||||
|
report.files >= 2,
|
||||||
|
"expected 2 classes, got {}",
|
||||||
|
report.files
|
||||||
|
);
|
||||||
|
assert_eq!(report.failed, 0, "clean code must not fail: {report:?}");
|
||||||
|
assert_eq!(
|
||||||
|
report.files_with_warn, 0,
|
||||||
|
"clean code must not warn: {report:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,9 @@ pub struct Args {
|
||||||
/// TOML-конфиг с keep-паттернами (без него - пустой keep).
|
/// TOML-конфиг с keep-паттернами (без него - пустой keep).
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub config: Option<PathBuf>,
|
pub config: Option<PathBuf>,
|
||||||
|
/// Декомпилировать jadx и показать долю распознанных структур.
|
||||||
|
#[arg(long)]
|
||||||
|
pub decompile: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Показать классы, entrypoint'ы и keep-правила.
|
/// Показать классы, entrypoint'ы и keep-правила.
|
||||||
|
|
@ -48,6 +51,22 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
analysis.entrypoints.reflective_names.len()
|
analysis.entrypoints.reflective_names.len()
|
||||||
);
|
);
|
||||||
println!("keep-правил: {}", analysis.keep.len());
|
println!("keep-правил: {}", analysis.keep.len());
|
||||||
|
if args.decompile {
|
||||||
|
match noxis_verify::decompile_report(&args.input) {
|
||||||
|
None => println!("decompile: нет jadx"),
|
||||||
|
Some(report) => {
|
||||||
|
println!(
|
||||||
|
"decompile: файлов {}, с WARN {}, WARN всего {}, Failed {}",
|
||||||
|
report.files, report.files_with_warn, report.warns, report.failed
|
||||||
|
);
|
||||||
|
let mut kinds: Vec<(&String, &usize)> = report.kinds.iter().collect();
|
||||||
|
kinds.sort_by(|a, b| b.1.cmp(a.1));
|
||||||
|
for (kind, count) in kinds.iter().take(8) {
|
||||||
|
println!(" {kind}: {count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +81,7 @@ mod tests {
|
||||||
let args = Args {
|
let args = Args {
|
||||||
input: PathBuf::from("definitely-not-here.jar"),
|
input: PathBuf::from("definitely-not-here.jar"),
|
||||||
config: None,
|
config: None,
|
||||||
|
decompile: false,
|
||||||
};
|
};
|
||||||
assert!(run(args).is_err());
|
assert!(run(args).is_err());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue