Skip to content

YARA X Usage

Emirhan Uçan edited this page Aug 5, 2026 · 7 revisions

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.

Custom 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).

hydradragon Module

The project's unified Android-analysis module. It consumes two external JSON reports:

  • Dynamic HIPS/network (built on-device by merge_dex_findings in lib.rs, starting from Java's live-network/HIPS report via HipsMonitor.buildReportJson and folding in the manifest report, DEX static-analysis findings + the API-call histogram), fed as metadata key hydradragon.
  • Static APK analysis (Koodous-style, built on-device by build_hydradragon_json from a parsed binary AndroidManifest.xml + a URL sweep + the PKCS#7 signing certificate), fed as metadata key hydradragon. This is what the former standalone androguard module provided — it is now part of the hydradragon module. There is no androguard metadata 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
}

Static APK analysis (the former androguard module, now in hydradragon)

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_certificate in lib.rs:4357) navigates the PKCS#7 SignedData in META-INF/*.RSA|.DSA|.EC to 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/...). So certificate.subject/issuer/sha1 are populated and rules using them will match. (An older code comment suggested they were emitted empty — that is stale; the parsing is wired.)

dex Module (standard yara-x, feature enabled)

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.

Driving the yr CLI (standalone)

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.

Subcommands

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 is yr debug modules (prints yara_x::mods::module_names()), which requires building the CLI with --features debug-cmd.

yr scan — flags that matter

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.

Real invocations

# 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).

yr compile / yr check / yr dump

# 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 SOMEFILE

yr dump's SupportedModules are Lnk, Macho, Elf, Pe, Dotnet, Olecf, Vba, Crx, Dexhydradragon/cuckoo are intentionally absent (they consume external metadata, not the scanned file).

Rust API Usage

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.

The canonical on-device example — hydradragonclamav/src/yara_scan.rs

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>.

Writing a custom module (the examples/custom-module reference)

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.

Module-Metadata Pipeline (end-to-end)

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.

Rule Location

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.

Representative rule conditions (real text)

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.

Auto-Generated Rules

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, plus hydradragon.api_call(/.../) > 0 for launcher-hijack/suspicious-API patterns and hydradragon.dex_severe_finding_count() > 0 when 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_connections predicates 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_file under a write lock) so the current session benefits immediately, and (if SaveAutoRules is on, default true) persisted to filesDir/hydra-scan/generated_rules/*.yar so they survive restarts (reloaded at the next do_init).

The manual, user-requested variant is ScanEngine.generateRuleForApp(apkPath, packageName) (forces zero_trust=true).

Custom Rules (in-app editor)

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 .yar and compiled immediately by the live engine via NativeScanner.learnRule (→ clamav.add_yara_source_file under 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).

Rule Validation & Compilation

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.yar files.
  • <source_dir> — scanned recursively for .yar files.
  • <output_dir> — defaults to <source_dir>/../app/src/main/assets/scan when omitted.

Filtering logic (the reason only six .yrc ship):

  1. Drop _unverified — any filename ending _unverified.yar is removed (they reference undeclared private rules and fail to compile).
  2. Drop raw rulesets that have a filtered sibling — a raw <base>.yar is kept only if no <base>_filtered*.yar exists. So clean_rules.yar, valhalla-rules.yar, AndroidOS.yar are skipped.
  3. --filtered further restricts to filenames ending _filtered.yar.
  4. Incremental — skips a .yrc if it exists and its mtime ≥ the source .yar mtime; otherwise Compiler::new()add_sourcebuild()rules.serialize()fs::write. On any compile error the file is skipped and counted; the tool exits non-zero if any file failed.

Shipped .yrc set (app/src/main/assets/scan/)

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_compiledRules::deserialize). The yara-x crate version pinned in hydradragonclamav/Cargo.lock is 1.19.0 from the fork.

Recompiling after a rule or module change

# 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.cmd

If 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-x

All 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).

Performance

YARA-X is written in Rust and is significantly faster than the original YARA. The native engine:

  • Loads pre-compiled .yrc rulesets (no on-device compilation) — deserialized directly via yara_x::Rules::deserialize.
  • Caches one yara_x::Scanner per engine per thread (amortizing Scanner::new's WASM runtime instantiation).
  • Uses scanner.fast_scan(true).
  • Forwards module metadata (hydradragon JSON, static + dynamic) per scan via ScanOptions::set_module_metadata so module-gated rules fire.
  • Matches against raw APK/file bytes without temporary files.

The pulley backend (load-bearing)

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 .yrc works 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.

See Also

Clone this wiki locally