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())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if !java_available() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ pub mod java;
|
|||
pub mod leaks;
|
||||
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 metrics::{
|
||||
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).
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Декомпилировать jadx и показать долю распознанных структур.
|
||||
#[arg(long)]
|
||||
pub decompile: bool,
|
||||
}
|
||||
|
||||
/// Показать классы, entrypoint'ы и keep-правила.
|
||||
|
|
@ -48,6 +51,22 @@ pub fn run(args: Args) -> Result<()> {
|
|||
analysis.entrypoints.reflective_names.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(())
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +81,7 @@ mod tests {
|
|||
let args = Args {
|
||||
input: PathBuf::from("definitely-not-here.jar"),
|
||||
config: None,
|
||||
decompile: false,
|
||||
};
|
||||
assert!(run(args).is_err());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue