Proven by paranoid runs over lorep (17 classes, fully green), LoVisual (4081 classes) and oc2r (1278 classes) with per-class java -Xverify:all, each fix first reproduced as a red test: - merge: Top absorbs instead of Top-wins (javac declares Top for slots unwritten on some inflow; precise types reject those edges). Regression: loop_head_keeps_top_for_loop_local. - emit: collapse [Long|Double, Top] pairs on operand stacks (javac writes one entry; the pair is rejected). Regression: wide_stack_join_emits_single_entries. - xfer ArrayLoad: push the wide pair, fixing lcmp underflow. Regression: array_load_wide_pushes_pair. - xfer <init>: convert only the receiver's own allocation site instead of every Uninitialized (nested construction clobbered the outer object). Regression: nested_construction_keeps_outer. - xfer ANewArray: array operands gain one dimension ([B] -> [[B) instead of an illegal L-wrap ([L[B;). Covered by the same test. - jar load: duplicate class names (multi-release module-info twins) keep the first occurrence instead of crashing save. Test: duplicate_class_names_keep_first. - strings/constants: skip methods past MAX_METHOD_INSNS (8000) so giants cannot overflow the 64KB writer ceiling. Tests: oversized_methods_are_skipped in both passes. - merge errors now name both types being merged. Also: em-dash removed repo-wide (724 spots, rule added to todo.md), allow-lints only in tests (scratch example deleted), code comments migrated to English in every file this change touches.
146 lines
5.3 KiB
Rust
146 lines
5.3 KiB
Rust
//! Утечки имён и секреты открытым текстом.
|
||
//!
|
||
//! Два независимых вопроса: (1) пережило ли исходное имя обфускацию
|
||
//! (сверка эталона до/после с учётом keep); (2) остались ли в строках
|
||
//! секреты, которые обязан был съесть пасс `strings`.
|
||
|
||
use std::collections::BTreeSet;
|
||
|
||
use noxis_analysis::KeepRules;
|
||
use noxis_classfile::{Const, Insn, Program};
|
||
|
||
/// Красный список: паттерны секретов, которых не должно быть в выходе.
|
||
///
|
||
/// Это generic-признаки (пароли, ключи, URL, приватные ключи), а не имена
|
||
/// конкретного проекта: проектные секреты ловятся тем, что строка вообще
|
||
/// осталась незашифрованной (см. `scan_secrets` + keep-список пасса).
|
||
pub const RED_PATTERNS: &[&str] = &[
|
||
r"(?i)password\s*=\s*[^\s;]+",
|
||
r"(?i)\bapi[_-]?key\b",
|
||
r#"jdbc:[a-z]+://[^\s"']+"#,
|
||
r"-----BEGIN [A-Z ]*PRIVATE KEY-----",
|
||
r#"https?://[^\s"']+"#,
|
||
];
|
||
|
||
/// Все идентификаторы программы: классы, члены, строки кода.
|
||
///
|
||
/// Члены - по простому имени (без владельца): сверка строгая и дешёвая,
|
||
/// для точечной проверки есть `find_leaks` с keep-фильтром.
|
||
pub fn collect_names(program: &Program) -> BTreeSet<String> {
|
||
let mut names = BTreeSet::new();
|
||
for class in &program.classes {
|
||
names.insert(class.this_class.clone());
|
||
for field in &class.fields {
|
||
names.insert(field.name.clone());
|
||
}
|
||
for method in &class.methods {
|
||
names.insert(method.name.clone());
|
||
}
|
||
}
|
||
names
|
||
}
|
||
|
||
/// Имена из эталона, пережившие обфускацию (минус keep).
|
||
///
|
||
/// Пустой результат - норма; непустой - тест падает с этим списком.
|
||
pub fn find_leaks(before: &BTreeSet<String>, after: &Program, keep: &KeepRules) -> Vec<String> {
|
||
let names = collect_names(after);
|
||
before
|
||
.iter()
|
||
.filter(|name| names.contains(*name))
|
||
.filter(|name| !keep.keeps_class(name) && !keep.keeps_member(name))
|
||
.cloned()
|
||
.collect()
|
||
}
|
||
|
||
/// Какие красные паттерны сработали на тексте.
|
||
pub fn check_patterns(text: &str) -> Vec<&'static str> {
|
||
use std::sync::OnceLock;
|
||
|
||
static COMPILED: OnceLock<Vec<(&'static str, regex::Regex)>> = OnceLock::new();
|
||
let compiled = COMPILED.get_or_init(|| {
|
||
RED_PATTERNS
|
||
.iter()
|
||
.filter_map(|pattern| {
|
||
regex::Regex::new(pattern)
|
||
.ok()
|
||
.map(|regex| (*pattern, regex))
|
||
})
|
||
.collect()
|
||
});
|
||
compiled
|
||
.iter()
|
||
.filter(|(_, regex)| regex.is_match(text))
|
||
.map(|(pattern, _)| *pattern)
|
||
.collect()
|
||
}
|
||
|
||
/// Секрет открытым текстом: где (`класс/метод` или `ресурс:путь`) и что.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct SecretHit {
|
||
pub location: String,
|
||
pub pattern: &'static str,
|
||
pub sample: String,
|
||
}
|
||
|
||
/// Все строки кода и текстовых ресурсов против красного списка.
|
||
pub fn scan_secrets(program: &Program) -> Vec<SecretHit> {
|
||
let mut hits = Vec::new();
|
||
for class in &program.classes {
|
||
for method in &class.methods {
|
||
let Some(code) = method.code.as_ref() else {
|
||
continue;
|
||
};
|
||
for insn in &code.insns {
|
||
if let Insn::Const(Const::String(text)) = insn {
|
||
let location = format!("{}/{}", class.this_class, method.name);
|
||
hits.extend(check_text(&location, text));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for resource in &program.resources {
|
||
if let Ok(text) = std::str::from_utf8(&resource.data) {
|
||
hits.extend(check_text(&format!("ресурс:{}", resource.path), text));
|
||
}
|
||
}
|
||
hits
|
||
}
|
||
|
||
fn check_text(location: &str, text: &str) -> Vec<SecretHit> {
|
||
check_patterns(text)
|
||
.into_iter()
|
||
.map(|pattern| SecretHit {
|
||
location: location.to_string(),
|
||
pattern,
|
||
sample: text.chars().take(48).collect(),
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn red_patterns_all_compile() {
|
||
assert_eq!(check_patterns("password=hunter2").len(), 1);
|
||
assert_eq!(RED_PATTERNS.len(), 5);
|
||
}
|
||
|
||
#[test]
|
||
fn red_patterns_catch_obvious_secrets() {
|
||
assert!(!check_patterns("password=hunter2").is_empty());
|
||
assert!(!check_patterns("api_key=abc").is_empty());
|
||
assert!(!check_patterns("jdbc:mysql://host/db").is_empty());
|
||
assert!(!check_patterns("https://example.com/hook").is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn clean_text_passes() {
|
||
assert!(check_patterns("hello world").is_empty());
|
||
assert!(check_patterns("aB3_foO").is_empty());
|
||
}
|
||
}
|