-
Notifications
You must be signed in to change notification settings - Fork 1
YARA X Usage
HydraDragonAV Mobile integrates a custom YARA-X fork (v1.19.0, based on VirusTotal/yara-x) compiled into the native Rust scan engine (libhydradragonandroid.so). The fork adds one custom metadata-consuming module — hydradragon (which absorbs the former separate androguard module) — on top of the standard yara-x dex module, and is built with the pulley portable-interpreter backend so a single compiled .yrc loads across all ABIs and on 16 KB-page Android devices.
For the complete function-level module reference (every export function with arguments/return types, the full JSON schemas, every rule file, and the .yar → .yrc build chain), see YARA-X-Modules. This page is the usage-level overview: how to drive the yr CLI standalone, how to use the Rust API, what the custom modules expose, where the rules live, how auto-generation works, and how the offline compile chain ships exactly six .yrc.
The yara-x engine is a git dependency (github.com/HydraDragonAntivirus/yara-x), not a submodule. The local yara-x/ directory at the repo root holds the project's rule files (.yar) and helper curation scripts, not the engine source. Clone the engine fork separately (e.g. C:\...\GitHub\yara-x) if you need to modify the modules.
The fork registers one custom module via register_module! (source: lib/src/modules/hydradragon/mod.rs + schema.rs + protos/hydradragon.proto). It consumes external JSON reports handed to the scanner as module metadata (like yara-x's cuckoo) — it does not parse the scanned file itself. The standard dex module does parse the scanned file (see below).
The project's unified Android-analysis module. It consumes two external JSON reports:
-
Dynamic HIPS/network (built on-device by
merge_dex_findingsinlib.rs, starting from Java's live-network/HIPS report viaHipsMonitor.buildReportJsonand folding in the manifest report, DEX static-analysis findings + the API-call histogram), fed as metadata keyhydradragon. -
Static APK analysis (Koodous-style, built on-device by
build_hydradragon_jsonfrom a parsed binaryAndroidManifest.xml+ a URL sweep + the PKCS#7 signing certificate), fed as metadata keyhydradragon. This is what the former standaloneandroguardmodule provided — it is now part of thehydradragonmodule. There is noandroguardmetadata key anymore.
Gated by the hydradragon-module Cargo feature. The proto message Hydradragon {} is empty — all surface area is #[module_export] functions reading the thread-local JSON.
The exported functions are flat names (hydradragon.ui_spam(...), not hydradragon.network.connections). Full list with exact signatures, score computations, and the 17-field HydradragonJson schema in YARA-X-Modules#exported-yara-functions--hips-behavioral:
| Export | Returns | Source |
|---|---|---|
hydradragon.ui_spam(package_re) |
score | HIPS UI spam events |
hydradragon.notification_spam(package_re) |
score | notification spam events |
hydradragon.clickjack(package_re) |
score | clickjacking events |
hydradragon.ransomware_behavior(package_re) |
score | ransomware rename-burst events |
hydradragon.canary_triggered(package_re) |
1/0 | decoy-file trap hits |
hydradragon.network_connections(package_re) |
score | per-app connection summaries |
hydradragon.strandhogg(package_re) |
score | task-hijack events |
hydradragon.removal_resistance(package_re) |
score | uninstall/device-admin kick events |
hydradragon.launcher_change(package_re) |
score | default-home hijack attempts (newest export) |
hydradragon.behavior_flagged(package_re) |
count | per-package flag arrays |
hydradragon.foreground_package(package_re) |
1/0 | current foreground package |
hydradragon.observed_packages(package_re) |
count | observed packages |
hydradragon.system_package(package_re) |
1/0 | device/system state package |
hydradragon.rooted() |
score |
system.is_rooted + is_self_protection_triggered
|
hydradragon.debug_mode() |
1/0 | system.is_debug_mode |
hydradragon.url(re) / url(str)
|
count | live-observed URLs |
hydradragon.screen_text(re) |
1/0 | OCR'd screen text |
hydradragon.api_call(re) |
sum | DEX API-call invocation counts |
hydradragon.dex_finding(re) |
count | DEX static-analysis findings |
hydradragon.dex_severe_finding_count() |
count | High/Critical DEX findings |
hydradragon.network.dns_lookup(re) |
count | DNS-resolved domains |
hydradragon.network.host(re) |
count | DNS-resolved IPs |
hydradragon.network.payload_hex(hexstr) |
count | Suricata-style packet payload byte match |
hydradragon.network.http_request(re) / http_get(re) / http_post(re) / http_user_agent(re)
|
count | HTTP request parsing from captured packets |
hydradragon.network.tcp(re) / udp(re)
|
count | TCP/UDP packet dest matching |
Most behavioral functions take a package_re: RegexId and return a summed score (not a boolean) — they filter events whose package_name matches the regex, then accumulate a score from the event's numeric fields with bonuses for short time windows (0 < time_window_seconds < 60) and is_malicious. The /./ regex (matches any package) is the common "any app" form. Score computation pattern: events.iter().filter(|e| e.package_name matches package_re).map(score_fn).sum().
launcher_change — the newest export (fork commit 5a33aebb). Signature: fn launcher_change(ctx, package_re: RegexId) -> i64. Per matching launcher_change_events[] entry: start at 1, +3 if changed == true, ×2 if is_suspicious == true. So an attempt=1, an actual change=4 (1+3), a suspicious actual change=8. The JSON event is produced by HipsMonitor.reportLauncherChange (Java) and the trigger is ScanEngine.checkDefaultLauncher, which resolves Intent.CATEGORY_HOME at the start of every scanAllApps() sweep. No rule in hips_rules_filtered_verified.yar currently calls it — the export is exercised through the app's auto-generated dynamic rules (hydradragonandroid/src/lib.rs:3715-3721). The infrastructure is ready; a static rule is the missing piece.
Example rules:
import "hydradragon"
// Flag an app that changed the default launcher
rule launcher_hijack_detected {
condition:
hydradragon.launcher_change(/.*/) > 2
}
// Flag apps invoking launcher-hijacking APIs (static DEX analysis)
rule launcher_hijack_static {
condition:
hydradragon.api_call(/clearPackagePreferredActivities|addPreferredActivity|createRequestRoleIntent/) > 0
}
The Koodous androguard YARA module (Apache-2.0, "The Koodous Authors") was ported to Rust and folded into the hydradragon module — there is no separate androguard module anymore; rules import "hydradragon" and call the functions below with the hydradragon. prefix. It consumes the Koodous-style JSON report (built on-device by build_hydradragon_json from a parsed binary AndroidManifest.xml + a URL sweep + the PKCS#7 signing certificate) and exposes functions to query it. The rule file is named hydradragon.yar (after its Koodous origin, which lives in yara-x/hydradragon.yar), and its only imports are hydradragon and math.
Every "search" export has two overloads with the same name: a regex form (RegexId, matched via ctx.regexp_matches) and a string form (RuntimeString, case-insensitive equality via eq_ignore_ascii_case, mirroring the C module's strcasecmp). List queries return 1/0 for "any element matches"; single-value queries check the one value. certificate.sha1 is string-form only (no regex overload).
| Export (regex + string overload each) | Checks |
|---|---|
hydradragon.package_name(re/str) |
app package |
hydradragon.app_name(re/str) |
displayed app name |
hydradragon.main_activity(re/str) |
the MAIN/LAUNCHER activity |
hydradragon.activity(re/str) |
any declared activity |
hydradragon.service(re/str) |
any declared service |
hydradragon.receiver(re/str) |
any broadcast receiver |
hydradragon.permission(re/str) |
any permission in permissions or new_permissions
|
hydradragon.url(re/str) |
any URL found in the APK |
hydradragon.certificate.subject(re/str) |
cert subject DN |
hydradragon.certificate.issuer(re/str) |
cert issuer DN |
hydradragon.certificate.sha1(str) |
cert SHA-1 (string form only) |
hydradragon.min_sdk / max_sdk / target_sdk
|
int64 fields |
hydradragon.permissions_number |
int64 field — count of declared permissions |
hydradragon.rootkit_behavior() |
1/0 — no MAIN/LAUNCHER activity AND a suspicious permission |
rootkit_behavior() is the composite "stealth rootkit" signal: the app can't be opened from the home screen/app drawer at all (no enabled MAIN/LAUNCHER activity, collapsed into main_activity being absent/empty) AND it requests at least one high-privilege or persistence permission (BIND_DEVICE_ADMIN, BIND_ACCESSIBILITY_SERVICE, SYSTEM_ALERT_WINDOW, REQUEST_INSTALL_PACKAGES, RECEIVE_BOOT_COMPLETED, QUERY_ALL_PACKAGES, WRITE_SECURE_SETTINGS, BIND_NOTIFICATION_LISTENER_SERVICE, PACKAGE_USAGE_STATS). This list mirrors the host app's Java heuristic ScanEngine.ROOTKIT_SUSPICIOUS_PERMS. The rule hidden_icon_rootkit at the end of hydradragon.yar calls hydradragon.rootkit_behavior() == 1.
Certificate population note: the on-device producer (
extract_certificateinlib.rs:4357) navigates the PKCS#7SignedDatainMETA-INF/*.RSA|.DSA|.ECto the first X.509 certificate, computes its SHA-1 (sha1_hex, a hand-rolled RFC 3174 impl), and parses issuer/subject DNs (parse_dn, OpenSSL-style/key=value/...). Socertificate.subject/issuer/sha1are populated and rules using them will match. (An older code comment suggested they were emitted empty — that is stale; the parsing is wired.)
The upstream yara-x DEX module parses Dalvik Executable Format files directly (main(_ctx, data) calls parser::Dex::parse(data); on failure returns Dex::new() with set_is_dex(false)). Gated by the dex-module feature. Exports: dex.checksum, dex.signature, dex.contains_string(str), dex.contains_method(str), dex.contains_class(str). The on-device scanner also does its own DEX parsing via the separate dex-core/dex-analysis crates (FossRust dex-parser-analyzer); the dex module lets YARA rules query the parsed DEX directly.
The fork ships the standard yara-x CLI, binary yr (cli/ crate). This is the easiest way to test rules and module metadata offline without touching the Android app. Build it from the fork:
cd <path-to-yara-x-fork>
cargo build --release --bin yr # default features
cargo build --release --features pulley # match the on-device pulley backend
cargo build --features debug-cmd # enable the `yr debug` subcommands (incl. module listing)The CI workflow (.github/workflows/yara-x-validate.yml) builds yr --release with RUSTFLAGS="-A dead_code" and runs yr check on every yara-x/**/*.yar.
| Command | Purpose |
|---|---|
yr scan |
Scan a file/dir against rules (the main command) |
yr compile |
Compile .yar → serialized .yrc
|
yr check |
Validate rule syntax only (hidden; exit 1=errors, 2=warnings-only) |
yr dump |
Show module-produced data for a file |
yr fmt |
Format rule source files |
yr fix |
fix encoding (→ UTF-8) / fix warnings (auto-fix) |
yr deps |
Show rule→rule and rule→module dependency tree |
yr completion <shell> |
Emit shell completions |
yr debug ... |
ast / cst / ir / wasm / modules (needs --features debug-cmd) |
There is no
yr list_modules— module listing isyr debug modules(printsyara_x::mods::module_names()), which requires building the CLI with--features debug-cmd.
yr scan [OPTIONS] [NAMESPACE:]RULES_PATH... <TARGET_PATH>
Key flags (from cli/src/commands/scan.rs):
| Flag | Effect |
|---|---|
-C / --compiled-rules |
RULES_PATH is a compiled .yrc instead of source .yar
|
-f / --fast-scan |
fast-scan mode (scanner.fast_scan(true)) — same flag the on-device engine uses |
-x / --module-data MODULE=FILE |
pass FILE's contents as module metadata to MODULE (repeatable) — this is how hydradragon JSON reports are fed from the CLI
|
-r / --recursive [MAX_DEPTH] |
recurse into directories |
-p / --threads <N> |
parallel scan threads |
-o / --output-format <text|ndjson|json> |
output format |
-c / --count |
only match count per file |
-a / --timeout <SECS> |
per-file timeout |
-z / --skip-larger <BYTES> |
skip files larger than this |
-t / --tag <TAG> |
only rules with this tag |
-m / --print-meta, -s / --print-strings, -g / --print-tags, -e / --print-namespace
|
enrich output |
-n / --negate |
print non-satisfied rules |
Shared compile args (cli/src/commands/mod.rs): -d/--define VAR=VALUE, -w/--disable-warnings [ID], --ignore-invalid-rules, -I/--ignore-module <MODULE>, --include-dir <PATH>, --path-as-namespace, --relaxed-re-syntax.
# Scan a single file with source rules
yr scan rules.yar sample.bin
# Scan a directory recursively, fast-scan, JSON output, 4 threads
yr scan -r -f -p 4 -o ndjson rules.yar ./samples/
# Scan with pre-compiled rules (.yrc) — fastest, no on-device compile
yr scan -C compiled.yrc sample.bin
# Feed a static APK-analysis report to the hydradragon module (the Koodous/androguard-style report
# is fed under the "hydradragon" metadata key)
yr scan -x hydradragon=report.json hydradragon.yar app.apk
# Feed the dynamic HIPS report (the on-device pattern)
yr scan -x hydradragon=hd.json hips_rules_filtered_verified.yar app.apk
# Count-only recursive scan
yr scan -c -r rules_dir/ ./samples/The -x module=file flag wires into ScanOptions::set_module_metadata(module_name, file_bytes) before scanner.scan_file_with_options — exactly the API the on-device engine uses (see the Rust API section below).
# Compile source rules to a serialized .yrc (default output "output.yarc")
yr compile rules.yar -o output.yrc
yr compile ns1:rules1.yar ns2:rules2.yar -o combined.yrc
yr compile --path-as-namespace rules_dir/ -o out.yarc
# Validate syntax only (no .yrc written) — used by CI
yr check rules.yar
yr check -r rules_dir/
# Show module-produced data for a file (does NOT work for hydradragon —
# it consumes external metadata, not the scanned file)
yr dump --module pe SOMEFILE
yr dump --module pe --module dex --module olecf -o json SOMEFILEyr dump's SupportedModules are Lnk, Macho, Elf, Pe, Dotnet, Olecf, Vba, Crx, Dex — hydradragon/cuckoo are intentionally absent (they consume external metadata, not the scanned file).
The fork's public API (from lib/src/scanner/mod.rs and lib/src/compiler/context.rs):
// Compile source → Rules
let mut compiler = yara_x::Compiler::new();
compiler.add_source(source)?; // or add_source(SourceCode::from(...).with_origin(...))
let rules: yara_x::Rules = compiler.build();
// Serialize / deserialize (the .yrc format)
let bytes: Vec<u8> = rules.serialize()?;
let rules = yara_x::Rules::deserialize(&bytes)?;
// Scan — the on-device pattern
let mut scanner = yara_x::Scanner::new(&rules);
scanner.fast_scan(true); // same as `yr scan -f`
let results = if module_meta.is_empty() {
scanner.scan(data)?
} else {
let mut opts = yara_x::ScanOptions::new();
for (name, meta) in module_meta { // &[(&str, &[u8])]
opts = opts.set_module_metadata(name, meta);
}
scanner.scan_with_options(data, opts)?
};
for rule in results.matching_rules() {
let id = rule.identifier();
// rule.namespace(), .metadata(), .tags(), .patterns()
}Other Scanner knobs: scanner.scan_file(path), scanner.use_mmap(bool), scanner.max_scan_size(usize), scanner.set_timeout(Duration), scanner.max_matches_per_pattern(usize), scanner.set_global(ident, value), scanner.console_log(F). Rules::deserialize_from(reader) and rules.serialize_into(writer) are also available.
This is the copy-pasteable pattern the mobile repo uses to load compiled .yrc, cache one Scanner per thread (because Scanner::new instantiates a WASM runtime per call), and feed module metadata:
// Load pre-compiled .yrc (fast — no on-device compilation of 50k+ rules)
pub fn from_compiled(bytes: &[u8], name: String) -> Option<Self> {
let rules = yara_x::Rules::deserialize(bytes).ok()?;
Some(Self::new(rules, name))
}
// Compile from source (used for hot-loaded auto rules)
let mut compiler = yara_x::Compiler::new();
compiler.add_source(source).ok()?;
Some(Self::new(compiler.build(), name))
// Scan with module metadata + fast_scan
let mut scanner = yara_x::Scanner::new(rules_static);
scanner.fast_scan(true);
let results = if module_meta.is_empty() {
scanner.scan(data)?
} else {
let mut opts = yara_x::ScanOptions::new();
for (name, meta) in module_meta { // &[(&str, &[u8])]
opts = opts.set_module_metadata(name, meta);
}
scanner.scan_with_options(data, opts)?
};
for rule in results.matching_rules() { /* rule.identifier() ... */ }module_meta is &[(&str, &[u8])] — e.g. [("hydradragon", hd_json_bytes)]. The single hydradragon key feeds the hydradragon module (the static APK report and the DEX/network report are merged into one JSON). The YaraEngine boxes yara_x::Rules so its heap address is stable, then unsafely promotes &Rules to &'static Rules to cache Scanner in a thread_local — documented as an optimization. Matches are emitted as YARA-X.<identifier>.
The fork ships examples/custom-module/ — a standalone crate that registers a module via inventory::submit! at link time. It's the template the in-tree hydradragon module generalizes (that one instead reads external JSON metadata rather than the scanned bytes):
// examples/custom-module/src/lib.rs
fn foobar_main(_ctx: &mut ModuleContext, data: &[u8]) -> Result<Foobar, ModuleError> {
let mut out = Foobar::new();
out.count = Some(data.len() as u64);
out.label = Some("foobar".to_owned());
Ok(out)
}
#[module_export]
pub fn add(_ctx: &ScanContext, a: i64, b: i64) -> i64 { a + b }
register_module!("foobar", Foobar, foobar_main);Rules then import "foobar" and read foobar.count, foobar.label, foobar.tags, call foobar.add(a, b). Callers can override output per-scan via Scanner::set_module_output before Scanner::scan.
Java HipsMonitor.buildReportJson() ──┐
(ui_spam/notification_spam/ │
clickjack/ransomware/canary/ │ JNI
strandhogg/removal_resistance/ ▼
launcher_change/network/system/ hydradragonandroid/lib.rs
behavior_flags/behavior_state) ──► merge_dex_findings (folds manifest report + DEX findings + api_calls)
build_hydradragon_json (AXML + URLs + cert)
│
▼
YaraEngine::scan(data, object_path,
&[("hydradragon", hd_json)])
│
▼
ScanOptions::set_module_metadata(...)
│
▼
Scanner::scan_with_options(data, opts)
│
▼
module main() reads it via ctx.get_module_metadata("hydradragon")
hydradragonandroid/Cargo.toml does not depend on yara-x directly — it pulls it transitively through hydradragonclamav (path dep), which is the only crate with the yara-x = { git = "...", features = ["pulley", "hydradragon-module", "dex-module"] } line.
YARA-X source rules are stored in the yara-x/ directory at the project root (a vendored working tree of the fork's rule set, not the engine source):
yara-x/
├── hydradragon.yar # 116 rules — APK manifest analysis (project's own, Koodous-derived)
├── hips_rules_filtered_verified.yar # 46 rules — HIPS behavioral rules (project's own)
├── machine_learning_apk.yar # 330 rules — auto-generated APK signatures (project's own)
├── clean_rules_filtered_verified.yar # 317 rules — ELF/Linux malware (third-party, filtered+verified)
├── valhalla-rules_filtered_verified.yar # 94 rules — VALHALLA demo (third-party, filtered+verified)
└── emerging-all.yar # 49,364 rules — Emerging Threats Suricata-converted (third-party)
Rule counts use anchored ^rule (verified). Committed but not shipped (dropped by the compile tool): clean_rules_filtered_unverified.yar, valhalla-rules_filtered_unverified.yar, the raw clean_rules.yar / valhalla-rules.yar source archives, and AndroidOS.yar (each has a filtered sibling). See YARA-X-Modules#rule-files for per-file details, representative condition lines, and authorship.
hydradragon.yar:
rule BaDoink : official android {
condition:
hydradragon.app_name("BaDoink") or $type_a_1 or all of ($type_b*)
}
rule assd_developer : official android {
condition:
hydradragon.certificate.sha1("ED9A1CE1F18A1097DCCC5C0CB005E3861DA9C34A")
}
rule fake_facebook : fake android {
condition:
hydradragon.app_name("Facebook")
and not hydradragon.certificate.sha1("A0E980408030C669BCEB38FEFEC9527BE6C3DDD0")
}
hips_rules_filtered_verified.yar:
rule HIPS_UI_Spam { condition: hydradragon.ui_spam(/./) >= 30 }
rule HIPS_Ransomware { condition: hydradragon.ransomware_behavior(/./) >= 5 }
rule HIPS_Foreground_Threat {
condition: hydradragon.foreground_package(/./) >= 1 and hydradragon.behavior_flagged(/./) >= 1
}
rule HIPS_Malicious_URL {
condition: hydradragon.url(/(?i)(tor2web|\.onion\/|bitcoin:|malware|exploit|shell|backdoor|rat\b|crypt)/) >= 1
}
rule HIPS_HTTP_Data_Exfil {
condition: hydradragon.network.http_post(/(?i)(upload|send|data|log|report|collect|sync|submit|gate)/) >= 1
and hydradragon.behavior_flagged(/./) >= 1
}
rule HIPS_TCP_Suspicious_Port {
condition: hydradragon.network.tcp(/^(4444|5555|6666|6667|1337|8080|8443|9000)$/) >= 1
}
rule HIPS_DEX_Severe_Finding { condition: hydradragon.dex_severe_finding_count() >= 1 }
emerging-all.yar (the only file using network.payload_hex):
// each rule ≈ hydradragon.network.payload_hex("<hex>") >= 1
// where <hex> is the original Suricata `content` byte sequence
Compiled .yrc files are deployed to app/src/main/assets/scan/ and loaded by the native engine at cold start. Five rulesets load at init (YRC_FILES: clean_rules_filtered_verified, valhalla-rules_filtered_verified, machine_learning_apk, hydradragon, hips_rules_filtered_verified); emerging-all.yrc (the 13 MB network-threat ruleset) loads lazily when VPN full-capture mode is enabled (nativeEnableVpnScan(true) → load_vpn_rules()). See YARA-X-Modules#the-yar--to-yrc-build-chain for the compile/filter logic.
When the native scanner detects a malicious or zero-trust (unknown/unmatched) sample, it generates a YARA rule automatically (generate_yara_rule in lib.rs:3582). These auto-generated rules:
- Are named
auto_<file_md5>,import "hydradragon". - Meta:
generator,sample_md5,based_on_detections(or"none (Zero Trust...)"). - Strings: up to 40 DEX string-pool entries (length 8–128, no control chars, deduped), scoped to a single nested entry's MD5 + DEX strings if all detections share one
outer!/entry. - Combine package-name matching via
hydradragon.package_name(...)(OR'd if multiple), a DEX string-pool threshold (N of them, N =min(6, count)),hydradragon.rootkit_behavior() == 1, plushydradragon.api_call(/.../) > 0for launcher-hijack/suspicious-API patterns andhydradragon.dex_severe_finding_count() > 0when severe DEX findings exist. - Use AND-based multi-condition logic — all signals must align to match, reducing FPs.
- Include an OR'd HIPS runtime branch using
hydradragon.ui_spam/notification_spam/clickjack/ransomware/strandhogg/removal_resistance/launcher_change/network_connectionspredicates with a package regex, so a rerun of the same family is caught on package-name/network signals too, not just literal strings. - Are hot-loaded into the live engine via
nativeLearnRule(→clamav.add_yara_source_fileunder a write lock) so the current session benefits immediately, and (ifSaveAutoRulesis on, default true) persisted tofilesDir/hydra-scan/generated_rules/*.yarso they survive restarts (reloaded at the nextdo_init).
The manual, user-requested variant is ScanEngine.generateRuleForApp(apkPath, packageName) (forces zero_trust=true).
Users can paste their own YARA rules directly in the app — no PC, no yr CLI, no .yrc rebuild needed:
-
Where: Settings → Auto-Generated Rules → "➕ Add Custom Rule" button, or the "+ Add Custom Rule" row at the top of the auto-rules manager dialog (SettingsFragment:
showCustomRuleAddDialog). - Workflow: paste a YARA rule into the monospace editor → Save.
-
Validation: the rule text is written to a temp
.yarand compiled immediately by the live engine viaNativeScanner.learnRule(→clamav.add_yara_source_fileunder a write lock — the same hot-load path used for auto-generated rules). If compilation fails (bad syntax, missing import, invalid condition), the rule is rejected on the spot and nothing is written — the user sees a "Rule rejected — it could not be compiled" toast. An empty rule and a still-loading engine are also rejected with their own messages. -
On success the rule is:
- activated for the current session instantly (it is compiled into the live YARA engine right away), and
- persisted as
filesDir/hydra-scan/generated_rules/custom_<timestamp>.yar— the same directory the native engine reloads at every init (do_init_from_assets), so it survives restarts and applies to all future scans.
- Management: saved custom rules appear in the Auto-Rules manager list (tap to view the raw source, ⭳ to export via SAF, ✕ to delete). Deleting removes the file from disk; the engine stops matching it on the next restart.
-
Strings: fully localized — the six
custom_rule_*resources exist in all 19 shipped locales.
Because a valid rule is hot-loaded into the live engine for the rest of the session, an overly broad custom rule can cause immediate false positives across all scans — the on-device engine gives no per-rule time window. Validation checks compilability only, not behavior; keep custom rules as specific as the auto-generated ones (anchored package/API/DEX-string signals).
Rules are compiled offline by the hydradragon_yara_x_compile dev tool (dev-tools/hydradragon_yara_x_compile/, src/main.rs 186 lines — same yara-x features as the on-device crate, so the serialized .yrc is backend-compatible):
cargo run --release --manifest-path dev-tools/hydradragon_yara_x_compile/Cargo.toml -- yara-x/ app/src/main/assets/scan/CLI: hydradragon_yara_x_compile [--check] [--filtered] <source_dir> [<output_dir>]
-
--check— validate compilation only, do not write.yrc. -
--filtered— compile only*_filtered.yarfiles. -
<source_dir>— scanned recursively for.yarfiles. -
<output_dir>— defaults to<source_dir>/../app/src/main/assets/scanwhen omitted.
Filtering logic (the reason only six .yrc ship):
-
Drop
_unverified— any filename ending_unverified.yaris removed (they reference undeclared private rules and fail to compile). -
Drop raw rulesets that have a filtered sibling — a raw
<base>.yaris kept only if no<base>_filtered*.yarexists. Soclean_rules.yar,valhalla-rules.yar,AndroidOS.yarare skipped. -
--filteredfurther restricts to filenames ending_filtered.yar. -
Incremental — skips a
.yrcif it exists and its mtime ≥ the source.yarmtime; otherwiseCompiler::new()→add_source→build()→rules.serialize()→fs::write. On any compile error the file is skipped and counted; the tool exits non-zero if any file failed.
| File | Bytes |
|---|---|
hydradragon.yrc |
104,552 |
clean_rules_filtered_verified.yrc |
406,942 |
emerging-all.yrc |
13,378,439 |
hips_rules_filtered_verified.yrc |
19,873 |
machine_learning_apk.yrc |
388,207 |
valhalla-rules_filtered_verified.yrc |
83,105 |
These six .yrc correspond exactly to the six .yar that survived the filter rules. The .yrc format is the pulley-backend serialized ruleset that hydradragonclamav loads on-device via the matching pulley feature (YaraEngine::from_compiled → Rules::deserialize). The yara-x crate version pinned in hydradragonclamav/Cargo.lock is 1.19.0 from the fork.
# After editing yara-x/*.yar:
cargo run --release --manifest-path dev-tools/hydradragon_yara_x_compile/Cargo.toml -- yara-x/ app/src/main/assets/scan/
# Then rebuild the .so:
cd hydradragonandroid && build-android.cmdIf you change the yara-x fork itself (add/rename a module export), commit + push the fork, then update the pin in all three dependent crates so the offline compiler and the on-device engine stay on the same rev (the serialized .yrc is backend-specific):
cd hydradragonandroid; cargo update -p yara-x # builds the .so (transitively via hydradragonclamav)
cd ../hydradragonclamav; cargo update -p yara-x
cd ../dev-tools/hydradragon_yara_x_compile; cargo update -p yara-xAll three must match — the serialized .yrc is backend-specific. Then recompile the .yrc files (compiler) and rebuild the .so (the module metadata contract in hydradragonandroid/src/lib.rs must match the fork's schema).
YARA-X is written in Rust and is significantly faster than the original YARA. The native engine:
- Loads pre-compiled
.yrcrulesets (no on-device compilation) — deserialized directly viayara_x::Rules::deserialize. - Caches one
yara_x::Scannerper engine per thread (amortizingScanner::new's WASM runtime instantiation). - Uses
scanner.fast_scan(true). - Forwards module metadata (hydradragon JSON, static + dynamic) per scan via
ScanOptions::set_module_metadataso module-gated rules fire. - Matches against raw APK/file bytes without temporary files.
The pulley feature tells wasmtime to interpret rule-condition WebAssembly using the Pulley portable bytecode interpreter instead of the default Cranelift JIT. This is required on Android for two reasons (documented in hydradragonclamav/Cargo.toml):
- 16 KB-page devices crash with JIT ("changing of protections isn't page-aligned" in wasmtime mmap), and Android restricts W^X JIT anyway.
- Pulley bytecode is architecture-independent, so one compiled
.yrcworks across all ABIs (arm64-v8a,armeabi-v7a,x86_64,x86).
The backend tag is embedded in the serialized rules, so the compile tool and the on-device scanner MUST both use pulley (or both not) — a mismatch makes .yrc fail to load. The .yrc shipped in app/src/main/assets/scan/ are pulley-compiled.
- YARA-X-Modules — complete function-level module + rules + build-chain reference
- Detection-Engines — Overview of all engines
-
Behavioral-Protection — Runtime behavioral detectors that feed
hydradragon - Rust-API-Reference — the Rust side of the module-metadata pipeline
- ClamAV-Integration — ClamAV signature engine
- Data-Pipeline — Signature generation workflow