-
Notifications
You must be signed in to change notification settings - Fork 1
Rust API Reference
Function-level reference for every native (Rust) crate in HydraDragonAV Mobile. The native engine is compiled into libhydradragonandroid.so (one per ABI under app/src/main/jniLibs/) and called from Java via JNI through com.hydradragon.antivirus.engine.NativeScanner.
The workspace contains five crates:
| Crate | Path | Role |
|---|---|---|
hydradragonandroid |
hydradragonandroid/ |
JNI bridge + full scan pipeline + on-device metadata build |
hydradragonclamav |
hydradragonclamav/ |
Pure-Rust ClamAV-compatible signature engine + YARA-X bridge |
hydradragonml |
hydradragonml/ |
tract-onnx binary APK classifier |
hydradragonextractor |
hydradragonextractor/ |
Recursive archive/APK extraction + zip-bomb detection |
hydradragonxorfilter |
hydradragonxorfilter/ |
Binary-Fuse xor filter format shared by writer + on-device reader |
Cross-references: YARA-X-Modules for the androguard/hydradragon module schemas; Detection-Engines for how these crates combine; Developer-Guide for build instructions.
The crate root is hydradragonandroid/src/. It exposes native methods on the Java class com.hydradragon.antivirus.engine.NativeScanner, loads every scan asset (ClamAV DB, compiled YARA .yrc rulesets, ONNX ML model, TLSH digests, NSRL xor-filter whitelist, package whitelist, MinHash benign signatures, URL/IP xor filters) from the APK assets/ via AAssetManager, runs the full per-file scan pipeline, and returns JSON verdicts to Java.
Source files: asset_reader.rs, benign_db.rs, dex_scan.rs, elf.rs, emulate.rs, ip_scan.rs, lib.rs, url_scan.rs.
Thin FFI wrapper around Android's AAssetManager C API. Stores a native AAssetManager* in a process-global AtomicPtr so background threads (which do not hold a JNI env) can read bundled asset bytes by relative path.
| Signature | Behavior |
|---|---|
pub fn from_java(env: *mut c_void, asset_manager: *mut c_void) -> *mut c_void |
Wraps AAssetManager_fromJava. Input: JNI env pointer + Java AssetManager jobject (both raw c_void). Output: native AAssetManager* or null on FFI failure. Call once on the JNI thread, then pass the result to init. (asset_reader.rs:34) |
pub fn init(mgr: *mut c_void) |
Stores mgr into the static AASSET_MANAGER AtomicPtr (Relaxed). No-op return. Must be called before any read. (asset_reader.rs:43) |
pub fn read_file_bytes(relative_path: &str) -> Option<Vec<u8>> |
Opens relative_path under the stored manager in buffer mode (AASSET_MODE_BUFFER = 3), reads its full length, closes the asset. Returns None if the manager is null, the path has an interior NUL, the asset doesn't exist, or AAsset_read returns negative. Truncates to the actually-read count if fewer bytes than AAsset_getLength were read. (asset_reader.rs:47) |
pub fn list_assets(asset_dir: &str) -> Option<Vec<String>> |
Opens asset_dir as an AAssetDir and iterates AAssetDir_getNextFileName, returning all non-empty filenames. None if manager null / dir open fails / path has a NUL. (asset_reader.rs:72) |
pub fn read_all_assets(asset_dir: &str) -> HashMap<String, Vec<u8>> |
Lists asset_dir, then read_file_bytes of every "{asset_dir}/{name}", returning a map keyed by filename. Empty map on failure. Used at init to slurp the whole assets/ tree into memory. (asset_reader.rs:99) |
Content-based benign-APK whitelist. At init it parses benign_signatures.bin; at scan time it computes a 64-value MinHash signature from an APK's token set and declares KNOWN_BENIGN if any stored signature for that package has estimated Jaccard similarity ≥ 0.85, so heavy scanning (ClamAV/ML/TLSH) can be skipped. This is the MinHash usage in the codebase — it is a benign-content skip, not the ML anomaly detector.
| Item | Signature | Behavior |
|---|---|---|
| const | pub const K: usize = 64 |
Number of MinHash permutations; must match gen_benign_signatures.py. (benign_db.rs:25) |
| const | pub const THRESHOLD: f32 = 0.85 |
Jaccard similarity threshold for KNOWN_BENIGN. (benign_db.rs:28) |
| type | pub type Sig = [u64; K] |
A single 64-value MinHash signature. (benign_db.rs:33) |
| struct | pub struct BenignDb { sigs: HashMap<String, Vec<Sig>> } |
In-memory DB: package name → list of known-benign signatures. Fields private. (benign_db.rs:36) |
| method | pub fn load(data: &[u8]) -> Option<Self> |
Parses the binary format: u32 package count, then per package u8 name length + UTF-8 name + u32 sig count + that many 64×u64 sigs (all little-endian). None on short read / UTF-8 / parse error. Duplicate packages merged via or_default().extend. (benign_db.rs:43) |
| method | pub fn is_known_benign(&self, package_name: &str, tokens: &HashSet<u64>) -> bool |
false if the package isn't in the DB. Otherwise computes compute_sig(tokens) and returns true if jaccard(&query, s) >= THRESHOLD for any stored s. Used in run_scan's skip_heavy construction. (benign_db.rs:73) |
| method | pub fn package_count(&self) -> usize |
Number of packages in the DB. (benign_db.rs:88) |
| method | pub fn signature_count(&self) -> usize |
Total signatures across all packages. (benign_db.rs:93) |
| fn | pub fn compute_sig(tokens: &HashSet<u64>) -> Sig |
MinHash: sig = [u64::MAX; K]; for each token t and permutation i, h = (t ^ i).wrapping_mul(FNV_PRIME) (FNV_PRIME = 0x0000_0100_0000_01b3), keeping the minimum per slot. (benign_db.rs:100) |
| fn | pub fn jaccard(a: &Sig, b: &Sig) -> f32 |
Estimated Jaccard similarity = ` |
DEX string + static-analysis extraction using the dex-analysis/dex-core crates (FossRust dex-parser-analyzer). Produces the decoded string pool, flattened static-analysis findings, and a unique API-call histogram that feeds the hydradragon.api_call() YARA-X module function.
| Item | Signature | Behavior |
|---|---|---|
| struct | pub struct DexScan { pub text: String, pub findings: Vec<DexFinding>, pub api_calls: Vec<String> } |
text = decoded string pool (strings + method/class/field names), \n-joined, capped at MAX_TEXT = 8 MiB. findings = static-analysis findings (any severity), capped at MAX_FINDINGS = 64. api_calls = unique API call signatures in Lpkg/Cls;->method(params)return format, serialized as "sig\tcount", capped at MAX_API_CALLS = 4096. (dex_scan.rs:12) |
| struct | pub struct DexFinding { severity, kind: String, class_descriptor: String, message: String } |
Flattened from dex_analysis::Finding; message is "{id}: {message}". (dex_scan.rs:25) |
| fn | pub fn scan(bytes: &[u8]) -> Option<DexScan> |
Parses the DEX via dex_core::parse_dex, builds the string pool (breaking at MAX_TEXT), runs dex_analysis::analyze_dex with AnalysisConfig::default() taking the first MAX_FINDINGS, then builds the API-call histogram via graphs::build_xrefs + semantics::pretty_method for each callee in xrefs.method_calls, accumulating counts per signature. The whole body is wrapped in catch_unwind(AssertUnwindSafe(...)) so a malformed DEX returns None instead of panicking. (dex_scan.rs:34) |
| fn | pub fn is_severe(sev: Severity) -> bool |
matches!(sev, Severity::Critical) — only Critical findings count toward a malicious verdict; High and below are too false-positive-prone. (dex_scan.rs:91) |
Minimal, dependency-free ELF32/ELF64 (little-endian only) parser — just enough to drive Unicorn emulation: machine type, entry point, PT_LOAD segments, plus .dynsym/.dynstr linear scans to locate an exported symbol by name (e.g. JNI_OnLoad) and to enumerate imported (undefined) symbols with their GOT patch addresses.
| Item | Signature | Behavior |
|---|---|---|
| const |
pub const EM_ARM: u16 = 40, EM_AARCH64: u16 = 183, EM_386: u16 = 3, EM_X86_64: u16 = 62
|
ELF e_machine constants. (elf.rs:8-11) |
| struct | #[derive(Clone, Debug)] pub struct Segment { pub vaddr: u64, pub offset: u64, pub filesz: u64, pub memsz: u64 } |
One PT_LOAD segment. (elf.rs:16) |
| struct | #[derive(Clone, Debug)] pub struct ElfInfo { pub is_64: bool, pub machine: u16, pub entry: u64, pub segments: Vec<Segment> } |
is_64 kept for API completeness; emulate.rs derives everything from machine. (elf.rs:24) |
| fn | pub fn parse_elf(data: &[u8]) -> Option<ElfInfo> |
Validates \x7fELF magic, requires ei_data == 1 (little-endian; rejects big-endian as "not an Android target"), reads e_machine at offset 18, and depending on ei_class (1=32, 2=64) reads e_entry, e_phoff, e_phentsize, e_phnum. Iterates up to min(phnum, 4096) program headers, keeping only PT_LOAD (type 1) segments with 0 < memsz < 0x1000_0000. None for anything malformed. (elf.rs:52) |
| fn | pub fn find_dynsym(data: &[u8], want: &str) -> Option<u64> |
Locates .dynsym via the section-header table (using e_shstrndx), resolves its paired .dynstr via sh_link, then linearly scans symbols (capped at 65536). Returns the virtual address (st_value) of the first symbol whose name equals want and whose st_value != 0 (exported/defined). None if malformed/big-endian/.dynsym absent/name not found. (elf.rs:125) |
| struct | #[derive(Clone, Debug)] pub struct Import { pub name: String, pub got_addr: u64 } |
An undefined (imported) dynamic symbol and the relocation address (r_offset) that should hold its resolved pointer. (elf.rs:224) |
| fn | pub fn find_imports(data: &[u8]) -> Vec<Import> |
Walks all SHT_REL (9) / SHT_RELA (4) relocation sections (capped at 1,000,000 entries each), extracts r_offset and r_info, computes sym_idx = r_info >> 32 (64-bit) or r_info >> 8 (32-bit), and for each nonzero sym_idx whose dynsym entry is undefined (st_value == 0) emits Import { name, got_addr: r_offset }. Rel vs Rela addend distinction intentionally ignored. Not handled: Android's packed .android.rela (APS2/LEB128) format. Empty vec for malformed/big-endian ELF or missing .dynsym. (elf.rs:234) |
Pure-CPU Unicorn-Engine emulation of native .so libraries to reveal runtime-decoded strings (rolling-XOR/RC4/custom-cipher decode loops) and traced calls into a curated set of suspicious imported APIs (network/file/exec/anti-debug/fingerprinting). No syscalls, no JNI, no libc are emulated; execution halts on the first real unmodeled call. Bounded by both instruction count and a wall-clock timeout, plus a watchdog that force-stops a hung emu_start via a direct uc_emu_stop FFI call to work around a known ARM64-host JIT hang.
Constants: MAX_INSN = 200_000, TIMEOUT_US = 500_000, STACK_BASE = 0x7000_0000, STUB_BASE = 0x6000_0000, HARD_DEADLINE = 600 ms, MIN_STRING_LEN = 6, MAX_CONCURRENT_EMULATIONS = 2, MAX_HARD_TIMEOUTS = 1.
| Item | Signature | Behavior |
|---|---|---|
| fn | pub fn probe_emulation() -> bool |
One-shot startup probe (cached in EMULATION_PROBE: AtomicI8, 0=untested/1=available/-1=unavailable). Spawns a thread running a 4-instruction ARM64 snippet (mov x0,#42; add x0,x0,#1; mov x1,#0; ret) under a 10 ms timeout; caller waits via rx.recv_timeout(300 ms). true iff it succeeded. Subsequent calls return the cached result. (emulate.rs:73) |
| fn | pub fn emulation_available() -> bool |
Reads EMULATION_PROBE; if 0 (not yet probed) assumes true (the scan path has a per-call HARD_DEADLINE safety net), otherwise returns v > 0. (emulate.rs:121) |
| fn | pub fn host_arch() -> &'static str |
Compile-time cfg!(target_arch=...) → "ARM64 (AArch64)" / "ARMv7 (32-bit)" / "x86_64" / "x86 (32-bit)" / "unknown". (emulate.rs:137) |
| fn | pub fn unsupported_reason() -> &'static str |
Constant diagnostic: "ARM64 JIT backend hang detected — Unicorn emulation disabled for this session". Read by nativeEmulationReason. (emulate.rs:155) |
| struct | #[derive(Clone, Debug)] pub struct ApiCall { pub name: String } |
One observed call into a tracked imported API. (emulate.rs:245) |
| struct | #[derive(Clone, Debug, Default)] pub struct EmulationResult { pub strings: Vec<String>, pub api_calls: Vec<ApiCall> } |
Result of emulating one buffer. (emulate.rs:254) |
| fn | pub fn emulate(so_bytes: &[u8]) -> EmulationResult |
Entry point called from lib.rs (wrapped in catch_unwind). Fast-path returns Default if emulation_available() is false, parse_elf returns None, no segments, machine isn't ARM/AArch64/x86/x86_64, or find_dynsym(.., "JNI_OnLoad") (falling back to info.entry) is 0. Then claims one of 2 process-wide slots via a non-blocking CAS on EMULATION_SLOTS (skips immediately if both busy — never waits). Spawns a worker thread running emulate_body, sharing a stop_handle: Arc<AtomicUsize> that the worker stores the raw uc_engine handle into before emu_start. Caller waits via rx.recv_timeout(HARD_DEADLINE = 600 ms). On timeout it calls uc_emu_stop(handle) through C FFI, increments HARD_TIMEOUT_COUNT, and at MAX_HARD_TIMEOUTS = 1 permanently disables emulation (EMULATION_PROBE = -1). Returns the worker's result, or default on timeout. (emulate.rs:278) |
Internal emulate_body (emulate.rs:363): selects (arch, mode, sp_reg, pc_reg) and lr_reg (None on x86) from info.machine. Maps every PT_LOAD segment page-aligned RWX (skipping segments > 64 MiB), copies file bytes in, maps a dedicated stack at 0x7000_0000 (size 4 * PAGE), sets SP near the top. Maps a stub page at STUB_BASE = 0x6000_0000; for every tracked import from elf::find_imports, patches that import's GOT slot to a unique stub address and records stub_addr -> name in a shared Rc<RefCell<HashMap>>. Adds a code hook over the stub page that, on hit, records the ApiCall (capped at 256), then "returns" from the fake call by setting PC to LR (ARM/AArch64) or popping the return address off the stack (x86/x86_64); if the return target is 0 it calls emu_stop. Arms stop_handle with uc.get_handle() before uc.emu_start(entry, 0, TIMEOUT_US = 500_000, MAX_INSN = 200_000), clears it after. Then harvests every mapped region's live memory and extracts new printable ASCII strings (length ≥ 6, capped at 64) that do NOT already occur verbatim in the original file bytes (extract_new_ascii_strings, emulate.rs:539).
TRACKED_APIS (emulate.rs:228): socket, connect, send, sendto, recv, recvfrom, gethostbyname, getaddrinfo, inet_addr, inet_pton, open, open64, fopen, creat, unlink, remove, rename, system, popen, execve, execl, execvp, fork, vfork, dlopen, dlsym, ptrace, kill, __system_property_get.
Native malicious-IP exact-membership lookup against per-category Binary-Fuse (xor) blocklists (the non-CIDR entries of the allips set). No subnet/CIDR matching.
| Item | Signature | Behavior |
|---|---|---|
| struct | pub struct IpScanner { filters: Vec<CatFilter> } |
Private CatFilter { category: &'static str, filter: XorFilter }. (ip_scan.rs:11) |
| fn | pub fn from_bytes_map(files: &HashMap<String, Vec<u8>>) -> Option<IpScanner> |
For each (stem, category) in the priority-ordered CATS table (ipmalware→MALWARE_IP, ipphishing→PHISHING_IP, ipbruteforce→BRUTEFORCE_IP, ipddos→DDOS_IP, ipspam→SPAM_IP), loads {stem}.xf via XorFilter::from_bytes. None if zero filters loaded. Order = severity priority. (ip_scan.rs:26) |
| fn | pub fn scan(&self, ip: &str) -> Option<&'static str> |
Trims input; None if empty. Iterates filters in priority order and returns Some(category) for the first filter.contains(ip) hit, else None. Exact match on the trimmed canonical textual IP. (ip_scan.rs:44) |
Native URL/domain threat lookup — the Rust port of Java's UrlThreatScanner. All membership is Binary-Fuse (xor) filters loaded from .xf assets. Mirrors Java: an http(s) URL with a real path → URL scan against the *_URL xor filters (full scheme-less URL); otherwise → domain scan against the domain xor filters (registrable main domain via the public-suffix list).
| Item | Signature | Behavior |
|---|---|---|
| struct | pub struct UrlScanner { filters: Vec<CatFilter>, suffixes: HashSet<String> } |
Private CatFilter { category: &'static str, is_url: bool, filter: XorFilter }. (url_scan.rs:24) |
| fn | pub fn load_from_assets(files: &HashMap<String, Vec<u8>>) -> Option<UrlScanner> |
For each (stem, category, is_url) in CATS (malwareurl→MALWARE_URL/true, phishingurl→PHISHING_URL/true, phishing→PHISHING/false, malicious→MALICIOUS/false, malicious_mail→MAIL/false, abuse→ABUSE/false, spam→SPAM/false, mining→MINING/false), loads {stem}.xf. None if zero filters loaded. Also parses public_suffixes.txt into suffixes (lowercased, skipping blank and //-comment lines). (url_scan.rs:43) |
| fn | pub fn scan(&self, url: &str) -> Option<&'static str> |
Lowercases + trims; None if not http:// / https://. Strips the scheme to get norm (host[:port]/path). Computes has_path (a slash that isn't the last char). Strips any :port from the host. If has_path → URL scan: first is_url filter whose filter.contains(norm) matches (http and https to the same host+path normalize to the same key). Else → domain scan: main_domain(host) against the non-URL filters. Some(category) or None. (url_scan.rs:96) |
| fn (private) | fn main_domain<'a>(&self, host: &'a str) -> String |
PSL registrable-domain derivation: mc.yandex.ru → yandex.ru; a host whose suffix is listed (e.g. com.tk) returns p[i-1..].join("."). Falls back to last-two-labels. (url_scan.rs:73) |
The JNI bridge exposing native methods on com.hydradragon.antivirus.engine.NativeScanner, plus the whole per-file scan pipeline: extract → metadata build → YARA-x/ClamAV → ML → TLSH → result aggregation → JSON verdict. Manages the Engine (RwLock-guarded, OnceLock'd), the batch/deferred queue, hot rule learning, VPN rules, and all user-configurable atomic toggles.
| Item | Location | Notes |
|---|---|---|
const YRC_FILES: &[&str] |
lib.rs:83 |
Rulesets loaded at init: clean_rules_filtered_verified.yrc, valhalla-rules_filtered_verified.yrc, machine_learning_apk.yrc, androguard.yrc, hips_rules_filtered_verified.yrc. |
const VPN_YRC_FILES: &[&str] = ["emerging-all.yrc"] |
lib.rs:92 |
13 MB network-threat rules, loaded lazily on nativeEnableVpnScan(true). |
const MODEL_ONNX: &str = "model.onnx" |
lib.rs:95 |
|
const TLSH_DB: &str = "malware_tlsh.txt" |
lib.rs:98 |
|
const WHITELIST_XF: &str = "whitelist.xf" |
lib.rs:103 |
|
const WHITELIST_PACKAGES_DB: &str = "whitelist_packages.db" |
lib.rs:112 |
|
const BENIGN_SIGNATURES: &str = "benign_signatures.bin" |
lib.rs:113 |
|
static TLSH_THRESHOLD: AtomicI32 = 40 |
lib.rs:120 |
Mutable so the Settings slider takes effect immediately. Clamped 1–200 by the setter. |
static NATIVE_EMULATION_ENABLED: AtomicBool = true |
lib.rs:157 |
|
static MAX_SCAN_SIZE_MB: AtomicU32 = 650 |
lib.rs:163 |
|
static SCAN_RELEVANT_ONLY: AtomicBool = true |
lib.rs:170 |
|
static VPN_RULES_LOADED / VPN_SCAN_ENABLED: AtomicBool |
lib.rs:175 / 180 |
|
static BATCH_MODE / BATCH_ABORT: AtomicBool |
lib.rs:231 / 238 |
|
static JAVA_VM: OnceLock<jni::JavaVM> |
lib.rs:145 |
Lets Rust background threads call back into Java. |
static ENGINE: OnceLock<RwLock<Engine>> |
lib.rs:151 |
|
static ASSET_FILES: OnceLock<HashMap<String, Vec<u8>>> |
lib.rs:185 |
|
static INIT_DIR: OnceLock<String> |
lib.rs:190 |
Writable path for generated_rules/. |
static INIT_STARTED: AtomicBool |
lib.rs:194 |
|
static LAST_PANIC: Mutex<Option<String>> |
lib.rs:199 |
|
static INIT_STATUS: Mutex<String> |
lib.rs:340 |
|
static SCAN_SERIAL: Mutex<()> |
lib.rs:224 |
SCAN_SERIAL_MAX_WAIT = 5s (lib.rs:225). |
static DEFERRED_QUEUE: OnceLock<Mutex<Vec<DeferredItem>>> |
lib.rs:243 |
struct Engine {
clamav: Option<ClamavEngine>,
model: Option<Model>,
tlsh_db: Vec<tlsh_rs::TlshDigest>,
whitelist: Option<XorFilter>,
package_whitelist: HashMap<String, String>,
benign_db: Option<benign_db::BenignDb>,
url_scanner: Option<url_scan::UrlScanner>,
ip_scanner: Option<ip_scan::IpScanner>,
}All live on Java class com.hydradragon.antivirus.engine.NativeScanner. Each uses env.with_env(...).resolve::<LogErrorAndDefault>() so a pending JNI exception is logged and a default value returned rather than aborting. The Rust function names follow Java_com_hydradragon_antivirus_engine_NativeScanner_<methodName>.
| Rust fn (file:line) | Java signature | Returns to Java |
|---|---|---|
nativeInit (853) |
boolean nativeInit(String assetDir, boolean loadAutoRules, Object assetManager, String filesDir) |
true only if the engine was already loaded; otherwise spawns the ~70 s background init thread, sets INIT_STARTED, returns false. Stores the JVM in JAVA_VM, converts the Java AssetManager via asset_reader::from_java, reads all assets, builds the engine via do_init_from_assets, sets ENGINE if clamav or model loaded. Lowers its own scheduler priority to ANDROID_PRIORITY_BACKGROUND (nice 10) so ~8 s of cold-start CPU doesn't starve the UI thread. |
nativeIsReady (949) |
boolean nativeIsReady() |
ENGINE.get().is_some() — true once the async init finished. |
nativeSetEmulationEnabled (960) |
void nativeSetEmulationEnabled(boolean enabled) |
Stores into NATIVE_EMULATION_ENABLED. |
nativeIsEmulationAvailable (972) |
boolean nativeIsEmulationAvailable() |
emulate::probe_emulation() — cached; Java shows R.string.unicorn_unsupported when false. |
nativeEmulationReason (982) |
String nativeEmulationReason() |
emulate::unsupported_reason(). |
nativeHostArch (994) |
String nativeHostArch() |
emulate::host_arch(). |
nativeSetMaxScanSizeMb (1008) |
void nativeSetMaxScanSizeMb(int maxMb) |
Stores max_mb.max(1) into MAX_SCAN_SIZE_MB. |
nativeSetDetectZipBomb (1021) |
void nativeSetDetectZipBomb(boolean enabled) |
Calls hydradragonextractor::set_bomb_detection_enabled. |
nativeSetScanRelevantOnly (1033) |
void nativeSetScanRelevantOnly(boolean on) |
Stores into SCAN_RELEVANT_ONLY. |
nativeLearnRule (1048) |
boolean nativeLearnRule(String yarPath) |
Hot-loads one .yar into the live ClamAV engine via a brief write lock on ENGINE; true if add_yara_source_file returned Ok(Some(_)). |
nativeStatus (1070) |
String nativeStatus() |
The INIT_STATUS report string (what loaded/failed during init). |
nativeIsHashWhitelisted (1084) |
boolean nativeIsHashWhitelisted(String md5) |
whitelist.contains(md5) against the NSRL xor filter. |
nativeIsHashWhitelistedForFile (1107) |
boolean nativeIsHashWhitelistedForFile(String path, String md5) |
Same, but first reads the file's first 2 bytes and requires PK\x04\x03 (ZIP magic) so non-APKs are never whitelisted. |
nativeScanUrl (1145) |
String nativeScanUrl(String url) |
The malicious category (e.g. "PHISHING") from url_scanner.scan, or "" if clean/not a URL. |
nativeScanIp (1168) |
String nativeScanIp(String ip) |
The category (e.g. "MALWARE_IP") from ip_scanner.scan, or "". |
nativeScanText (1194) |
String nativeScanText(String text) |
Comma-joined matched rule/sig names from scan_text, or "". |
nativeScanHips (1214) |
String nativeScanHips(String hipsJson) |
JSON `{"malicious":..,"matches":[..],"suggestion":"uninstall" |
nativeEnableVpnScan (1323) |
void nativeEnableVpnScan(boolean enable) |
On true: load_vpn_rules() then sets VPN_SCAN_ENABLED; on false: clears VPN_SCAN_ENABLED (rules stay loaded). |
nativeScanPackets (1377) |
String nativeScanPackets(String packetsJson) |
JSON {"malicious":..,"matches":[..]} from scan_packets; {"malicious":false} no-op if VPN scan disabled. |
nativeScanApk (1397) |
String nativeScanApk(String path, String hydradragonJson, String fileMd5, boolean zeroTrust) |
The full scan verdict JSON from scan_apk → run_scan (or {"status":"deferred","path":...} in batch mode). |
nativeBeginBatchScan (1466) |
void nativeBeginBatchScan() |
Sets BATCH_MODE=true, BATCH_ABORT=false, clears the deferred queue. |
nativeAbortBatchScan (1480) |
void nativeAbortBatchScan() |
Sets BATCH_ABORT=true so the flush loop bails early. |
nativeEndBatchScan (1488) |
String nativeEndBatchScan() |
Clears BATCH_MODE, drains the queue, runs run_deferred_item for each (honoring BATCH_ABORT), returns "[v1,v2,...]" JSON array of verdicts. |
nativeSetTlshThreshold (1879) |
void nativeSetTlshThreshold(int threshold) |
Stores threshold.clamp(1,200) into TLSH_THRESHOLD. |
nativeTlshDiff (1892) |
int nativeTlshDiff(String tlsh1, String tlsh2) |
d1.diff(&d2), or -1 on parse/error. Used by the Anti-FP cache. |
| Signature | Behavior |
|---|---|
fn do_init_from_assets(files: &HashMap<String, Vec<u8>>, load_auto_rules: bool) -> Engine (lib.rs:348) |
Parallel init via std::thread::scope spawning 8 threads: ClamAV (+ all YRC_FILES compiled in parallel + optional generated_rules/*.yar from INIT_DIR), ONNX model, TLSH DB, NSRL whitelist, package whitelist, URL scanner, IP scanner, benign DB. Each wrapped in catch_unwind and producing a status-report fragment; set_status publishes the concatenated report. Returns the populated Engine. |
fn load_package_whitelist_from_bytes(bytes: &[u8]) -> HashMap<String, String> (lib.rs:607) |
Writes bytes to a temp file hydra_wl_pkg.db (rusqlite needs a path), opens it read-only, runs SELECT key, md5 FROM whitelist_package WHERE md5 IS NOT NULL, lowercases the md5, removes the temp file. One 17 MB write per process lifetime. |
fn load_vpn_rules() (lib.rs:1231) |
Idempotent (guarded by VPN_RULES_LOADED); under a write lock on ENGINE, compiles each VPN_YRC_FILES entry from ASSET_FILES and add_compiled_yara into the live ClamAV engine. |
fn scan_hips(hips_json: &str) -> String (lib.rs:1254) |
Early-validates JSON; builds module_meta = [("hydradragon", hips_json)]; scans the JSON bytes named "hips_behavior"; returns `{"malicious":..,"matches":[..],"suggestion":"uninstall" |
fn scan_text(text: &str) -> String (lib.rs:1293) |
Caps text at 8192 bytes; builds module_meta = [("hydradragon", r#"{"screen_text":"..."}"#)]; scans named "screen_text"; returns comma-joined match names. |
fn scan_packets(packets_json: &str) -> String (lib.rs:1339) |
No-op {"malicious":false} if VPN_SCAN_ENABLED false; validates JSON; builds module_meta = [("hydradragon", r#"{"network":{"packets":...}}"#)]; scans named "vpn_traffic"; returns {"malicious":..,"matches":[..]}. |
fn scan_apk(env, path, hydradragon_json, file_md5, zero_trust) -> String (lib.rs:1411) |
Reads the file, takes a read lock on ENGINE, calls run_scan on on_big_stack (64 MB stack). On a thread panic returns {"error":"scan panicked: ...","malicious":false} using LAST_PANIC. |
fn run_deferred_item(engine: &Engine, item: DeferredItem) -> String (lib.rs:1532) |
The Phase-3 runner for batch mode: rebuilds module_meta from the stored androguard_json/hydradragon_meta, runs emulation (same MAX_EMULATED_BUFFERS=8 cap + dedupe), rescan_buffers_parallel for ClamAV/YARA, ML model, severe DEX findings, TLSH, then assembles the same verdict JSON as run_scan. skip_heavy is rebuilt as all-false because only non-whitelisted buffers were stored. |
fn rescan_buffers_parallel(clamav, engine, buffers, skip_heavy, dex_scans, emulated, emulated_strings, module_meta, path, opts, max_dets, scan_timing) -> Vec<(String, String, Vec<String>)> (lib.rs:2032) |
The full-module-meta ClamAV/YARA pass over all buffers, parallelized across available_parallelism().clamp(1,4) scoped workers using an atomic-counter work-stealing scheme. Each worker: claims an index; skips by size / by SCAN_RELEVANT_ONLY (except top-level i==0); scans the raw buffer (scan_bytes_named_with_breakdown, panic-isolated per buffer); if Base64_Encoded_URL matched, runs extract_decode_base64_urls against url_scanner; scans the DEX string pool ({name}#dex); scans emulated strings ({name}#emulated) and runs extract_and_scan_urls; emits Behavior.Native: {api} dets for each unique emulated API call. Capped at max_dets (64). Merges per-worker timing via TimingBreakdown::accumulate. |
fn run_scan(engine, bytes, path, hydradragon, file_md5, zero_trust) -> String (lib.rs:2239) |
The master pipeline (see data flow below). Returns the verdict JSON. |
fn generate_yara_rule(file_hash, packages, detections, dex_scans, only_index) -> Option<String> (lib.rs:2838) |
Builds a yarGen-style YARA rule named auto_{file_hash}, import "androguard" + import "hydradragon". Strings: up to 40 DEX string-pool entries (length 8–128, no control chars, deduped), scoped to only_index if given. Condition groups: package-name ORs via androguard.package_name(...), N of them (N = strings.len().min(6).max(1)), androguard.rootkit_behavior() == 1, plus hydradragon.api_call(/.../) > 0 for launcher-hijack / suspicious-API patterns and hydradragon.dex_severe_finding_count() > 0 when severe findings exist. Also emits a HIPS runtime branch (OR'd) using hydradragon.ui_spam / notification_spam / clickjack / ransomware / strandhogg / removal_resistance / launcher_change / network_connections with a package regex. None if both strings and packages are empty. |
fn parse_manifest(data: &[u8]) -> Option<Manifest> (lib.rs:3370) |
Full binary-AXML walker producing Manifest { package, app_name, permissions, activities, services, receivers, main_activity, min_sdk, max_sdk, target_sdk }. Tracks an activity_stack and in_intent_filter/has_action_main/has_category_launcher to find the enabled MAIN/LAUNCHER activity (the icon-hiding enabled="false" + kept intent-filter trick is handled). Guard-bounded (200k chunks, 256 attrs, 64-deep activity stack, 4096 components). |
fn build_androguard_json(buffers: &[Buf]) -> Option<String> (lib.rs:3570) |
Finds the first parseable manifest, collects URLs, extracts the certificate, emits the androguard JSON (see module metadata contract). |
fn collect_urls(buffers: &[Buf]) -> Vec<String> (lib.rs:3525) |
Sweeps all buffers for http:// / https:// substrings, extending until whitespace/quote/<>/backslash/control/>=0x80; keeps URLs 10–2048 bytes, deduped, capped at 4096. |
fn extract_certificate(buffers: &[Buf]) -> Option<CertInfo> (lib.rs:3627) |
Finds the `META-INF/*.RSA |
fn collect_buffers(data, top_md5, path) -> (Vec<Buf>, Vec<(String,String,Vec<String>)>) (lib.rs:4007) |
Phase 1 extractor. Parallel work-stealing over a shared Mutex<Vec<WorkItem>> stack with an outstanding counter for termination; available_parallelism().clamp(1,4) workers; each wrapped in catch_unwind (a panicking worker sets capped=true to force-stop the rest). Caps: depth < 16, 4096 buffers, 2 GB total decompressed. Zip-bomb errors (hydradragonextractor::is_bomb_error) become HDR.Bomb.Decompression detections. Each Buf carries its apk_lineage (MD5s of ancestor zips, including its own when it is a zip) and entry_name. |
fn merge_dex_findings(hydradragon: Option<&[u8]>, dex_scans: &[Option<DexScan>]) -> Option<Vec<u8>> (lib.rs:3234) |
Folds DEX findings (any severity) into a dex_findings array and aggregates per-API-signature invocation counts into an api_calls array ("sig\tcount", sorted, capped 4096), merged on top of Java's hydradragon JSON (or an empty object). None if the result is an empty object. |
fn max_dangerous_perms(buffers: &[Buf]) -> usize (lib.rs:3085) |
Counts distinct DANGEROUS_PERMS (lib.rs:3018, ~37 entries) present in any buffer ≤ 4 MiB, checking both UTF-8 and UTF-16LE encodings; returns the max over buffers. |
fn collect_apk_hashes(buffers, top_md5) -> Vec<String> (lib.rs:3934) |
Lowercase MD5 of every zip/APK buffer (reuses top_md5 for the top-level), deduped, capped 64. |
fn collect_packages(buffers: &[Buf]) -> Vec<String> (lib.rs:3957) |
axml_package of each buffer, deduped, capped 64. |
fn tlsh_nearest(engine: &Engine, buf: &[u8]) -> Option<i32> (lib.rs:807) |
tlsh_rs::hash_bytes(buf); min diff over engine.tlsh_db; returns Some(best) only if best <= TLSH_THRESHOLD, else None. Empty DB → None. |
fn extract_and_scan_urls(engine: &Engine, decoded: &[u8]) -> Vec<String> (lib.rs:1926) |
Splits decoded emulation strings on \n, keeps lines starting http(s)://, scans each via url_scanner, emits "URL.{cat}: {url}" (capped 16). |
fn extract_decode_base64_urls(data, scanner) -> Vec<String> (lib.rs:1970) |
Scans for the B64_URL_PREFIXES (lib.rs:1956: standard + UTF-16LE base64 of http:///https://), extends over base64 chars + up to 2 = (≤100 bytes), decodes, scans decoded URLs via scanner. Emits "URL.{cat}: {s}" (capped 16). |
| helpers |
md5_hex (3921), sha1_hex/sha1_hash (3845/3855, hand-rolled RFC 3174), json_escape (320), is_text_like (657), is_obfuscated_xml (694), is_resource_path (700), is_relevant_buffer (716), has_network_indicators/has_base64/has_embedded_data (749/780/796), skip_by_size (642, ≤12 || > MAX_SCAN_SIZE_MB MB), tlsh_relevant (648, zip/ELF/DEX), on_big_stack (831, 64 MB stack thread), acquire_scan_serial_bounded (277, 5 s budget then proceeds without the lock), install_panic_hook (298), last_panic (4190), android_log/rust_timing_log! (62/75), deferred_queue (246). |
The Buf struct (lib.rs:3985): { data: Vec<u8>, apk_lineage: Vec<String>, entry_name: Option<String> }. DeferredItem (lib.rs:254): { path, zero_trust, buffers, dex_scans, androguard_json, hydradragon_meta, bomb_dets, perm_count, packages, hashes, extract_ms, dex_ms }.
Entry: nativeScanApk (lib.rs:1397) → scan_apk (lib.rs:1411) → on_big_stack(|| run_scan(...)) (lib.rs:2239). The SCAN_SERIAL mutex is acquired with a 5 s bounded wait first (so one stuck file can't freeze every other scan).
-
Phase 0 — serial gate.
acquire_scan_serial_bounded()(lib.rs:2248). -
Phase 1 — extract (
collect_buffers,lib.rs:4007, called atlib.rs:2272). Recursively unpacks the file and all nested archives (zip/gz/tar/xz/lzma/7z/rar viahydradragonextractor) intoVec<Buf>withapk_lineageMD5 chains. Zip-bomb errors becomeHDR.Bomb.Decompressiondetections returned alongside. Capped at 4096 buffers / 2 GB / depth 16. Timed asextract_ms. -
Phase 2 — metadata + fast passes + skip computation (
lib.rs:2283). All run before any heavy scan:max_dangerous_perms→perm_count;collect_packages(AXML package names);collect_apk_hashes(reuses Java's MD5 for the top-level);build_androguard_json(manifest + URLs + cert); whitelistskip_heavyconstruction (NSRL hash, MinHash benign, ancestor-lineage hash/package checks); DEX static analysis (dex_scan::scanon each non-skippeddex\nbuffer, timed asdex_ms);merge_dex_findings→hydradragon_meta;module_metabuilt (("androguard", …)+("hydradragon", …)); native emulation (emulate::emulateon each non-skipped\x7fELFbuffer, deduped by MD5, capped atMAX_EMULATED_BUFFERS = 8, timed asemulate_ms). -
Batch deferral (
lib.rs:2465). IfBATCH_MODE, store only non-whitelisted buffers + their Phase-2 data in aDeferredItem, push toDEFERRED_QUEUE, return{"status":"deferred","path":...}. Phase 3 runs later innativeEndBatchScanviarun_deferred_item. -
All-whitelisted short-circuit (
lib.rs:2500). If everyskip_heavy[i]is true, skip all Phase 3 and return a clean verdict (onlybomb_detscan make it malicious) withfile_tlsh. -
Phase 3a — ML model (
lib.rs:2528).model.scan(&b.data)on every zip buffer (no per-buffer skip; either all were whitelisted or none), trackingml_malicious, bestconfidence(ml_jaccard/ml_anomaly),ml_nearest, andml_lineages(object_path → apk_lineage). Timed asml_ms. -
Phase 3b — ClamAV + YARA (
rescan_buffers_parallel,lib.rs:2579). Fullmodule_metapass over all buffers withno_skip = vec![false; len], panic-isolated per buffer, plus DEX-string-pool scans, emulated-string scans, base64-URL decoding, emulation-API behavior signals. Returns(name, object_path, lineage)tuples, capped at 64. Producesclamav_msand per-engineyara_total_ms. -
Phase 3c — detection aggregation (
lib.rs:2613).detections = bomb_dets + yara_dets + ml_lineages + severe DEX findings (DEX/{sev}: {msg}) + TLSH (TLSH.Malware/dist={d}). -
Phase 3d — TLSH (
lib.rs:2637).tlsh_neareston eachtlsh_relevantbuffer; timed astlsh_ms. -
Phase 4 — generated rule + JSON assembly (
lib.rs:2701).malicious = !detections.is_empty(). Computesscoped_entry_idx(when all detections share the sameouter!/entrysuffix, scope the generated rule to that entry).generate_yara_ruleis built iffmalicious || zero_trust. Builds per-entryentry_md5s/entry_tlshsmaps (capped 1024) and top-levelfile_tlshfor the Anti-FP cache. Emits the per-stage timing breakdown to logcat (HydraDragon-RustTiming). Final JSON shape:
{"malicious":..,"matches":[..],"detections":[{"name","object_path","hashes"}],
"permissions":N,"packages":[..],"hashes":[..],"md5":"..",
"file_tlsh":"..",
"ml":{"malicious":..,"jaccard":..,"anomaly":..,"nearest":..|null},
"generated_rule":".."|null,"entry_md5s":{..},"entry_tlshs":{..}[,"error":".."]}(The batch-path run_deferred_item JSON additionally includes a leading "path":"..." and omits the optional error field.)
The YARA-X module metadata is passed as module_meta: &[(&str, &[u8])] to hydradragonclamav::ClamavEngine::scan_bytes_named[_with_breakdown]. That engine forwards these to yara-x's set_module_metadata(name, bytes) for each named module before scanning. Two modules are populated for APK scans:
androguard (built by build_androguard_json, lib.rs:3570):
{
"package_name": String|null,
"app_name": String|null,
"main_activity": String|null,
"activities": [String],
"services": [String],
"receivers": [String],
"permissions": [String],
"new_permissions": [String],
"urls": [String],
"min_sdk_version": String|null,
"max_sdk_version": String|null,
"target_sdk_version": String|null,
"certificate": { "subjectDN": String|null, "IssuerDN": String|null, "sha1": String|null }
}Source: parse_manifest for everything except urls (collect_urls) and certificate.* (extract_certificate → CertInfo). build_androguard_json returns None (no androguard entry pushed) if no buffer parses as a binary AXML manifest. certificate.* is parsed from the PKCS#7 signature block (META-INF/*.RSA|.DSA|.EC); subject/issuer are OpenSSL-style /key=value/... DNs and sha1 is the SHA-1 of the full certificate DER.
hydradragon (built by merge_dex_findings, lib.rs:3234): starts from Java's live-network/HIPS hydradragon JSON (the hydradragonJson arg to nativeScanApk, parsed if it's a JSON object) or an empty {}, then merges:
{
"dex_findings": [ { "severity": "Critical"|..., "kind": "...", "class_descriptor": "...", "message": "..." } ],
"api_calls": [ "Lpkg/Cls;->method(params)return\t<count>", ... ]
}dex_findings is added only if non-empty; api_calls is the aggregated invocation-count histogram across all DEX buffers (each entry "sig\tcount", the count being the sum across buffers). merge_dex_findings returns None (no hydradragon entry pushed) if the merged object is empty.
Special-purpose scans build their own single-entry module_meta: scan_hips → [("hydradragon", hips_json)] named "hips_behavior"; scan_text → [("hydradragon", r#"{"screen_text":"..."}"#)] named "screen_text"; scan_packets → [("hydradragon", r#"{"network":{"packets":...}}"#)] named "vpn_traffic".
The full schema of both modules (every field the YARA-X side reads) is in YARA-X-Modules.
Detailed function-level reference for this crate is being added below. See ClamAV-Integration for the conceptual overview in the meantime.
Detailed function-level reference for this crate is being added below. See AI-ML-Models for the conceptual overview in the meantime.
Detailed function-level reference for this crate is being added below.
Detailed function-level reference for this crate is being added below. See NSRL-Whitelisting for the conceptual overview in the meantime.