Node.js bindings for YARA-X, the Rust rewrite of YARA, built with napi-rs. Scan buffers and files with YARA rules from JavaScript, with no YARA installation and no JavaScript runtime dependency.
compile()andfromFile()return a scanner that reuses its compiled state, so a rule set is parsed once and scanned many times.scanAsync()andscanFileAsync()run on a libuv worker thread, so a large scan does not block the event loop.serialize()anddeserialize()move compiled rules between processes and worker threads.emitWasmFile()writes the compiled rule conditions as a WebAssembly module for inspection and sandboxing.- Six prebuilt targets (macOS, Linux, Windows — see Platform support). Nothing is compiled at install time and there is no
postinstallscript.
npm install @litko/yara-xpnpm add @litko/yara-x
yarn add @litko/yara-xRequires Node.js 20 or newer. The matching prebuilt binary is installed automatically through an optional platform package; if no binary exists for your platform, see Troubleshooting.
Both module systems work:
// ES modules
import { compile } from "@litko/yara-x";
// CommonJS
const { compile } = require("@litko/yara-x");Type definitions ship with the package, so TypeScript picks them up with no extra dependency.
Compile a rule from source, then scan a buffer:
import { compile } from "@litko/yara-x";
const rules = compile(`
rule HelloWorld {
strings:
$greeting = "hello world"
condition:
$greeting
}
`);
const matches = rules.scan(Buffer.from("This is a test with hello world in it"));
for (const match of matches) {
console.log(`${match.ruleIdentifier} matched at ${match.matches[0].offset}`);
}Rule syntax is documented upstream: Anatomy of a rule and differences with YARA.
fromFile() compiles rules from a file, and scanFile() reads the target from disk without copying it into the Node.js heap first:
import { fromFile } from "@litko/yara-x";
const rules = fromFile("./rules/malware_rules.yar");
const matches = rules.scanFile("./samples/suspicious_file.exe");
console.log(`Found ${matches.length} matching rule(s).`);scanAsync() and scanFileAsync() run the scan on a worker thread and return a promise:
import { compile } from "@litko/yara-x";
const rules = compile(`
rule LargeFileDetection {
strings:
$pattern = "sensitive payload"
condition:
$pattern
}
`);
const matches = await rules.scanFileAsync("./samples/large_file.bin");
console.log(`Found ${matches.length} matching rule(s).`);Global variables can be defined at compile time and overridden per scan:
import { compile } from "@litko/yara-x";
const rules = compile(
`
rule VariableRule {
condition:
string_var contains "secret" and int_var > 10 and bool_var
}
`,
{
defineVariables: {
string_var: "this is a secret message",
int_var: 20,
bool_var: true,
},
},
);
// Uses the compile-time defaults.
console.log(rules.scan(Buffer.from("test payload")).length);
// Overrides them for this scan only.
console.log(
rules.scan(Buffer.from("test payload"), {
string_var: "no secrets here",
int_var: 5,
bool_var: false,
}).length,
);Incremental scanners use defineVariable() instead:
const scanner = create();
scanner.defineVariable("threshold", 42);Namespaces keep rule identifiers unique across rule sets:
import { compile, create } from "@litko/yara-x";
const rules = compile(
`
rule NamespacedRule {
strings:
$pattern = "namespace indicator"
condition:
$pattern
}
`,
{ namespace: "alpha" },
);
const [match] = rules.scan(Buffer.from("namespace indicator"));
console.log(match.namespace); // "alpha"
const scanner = create();
scanner.addRuleSource('rule SharedName { strings: $a = "one" condition: $a }', "first_ns");
scanner.addRuleSource('rule SharedName { strings: $a = "two" condition: $a }', "second_ns");create() returns an empty scanner that accepts rule sources one at a time:
import { create } from "@litko/yara-x";
const scanner = create();
scanner.addRuleSource(`
rule FirstRule {
strings:
$pattern = "first pattern"
condition:
$pattern
}
`);
scanner.addRuleFile("./rules/more_rules.yar", "custom_namespace");
scanner.addRuleSources([
{ source: "rule BatchRule1 { condition: true }", namespace: "batch" },
{ source: "rule BatchRule2 { condition: true }" },
]);
const matches = scanner.scan(Buffer.from("test data with first pattern"));
console.log(`Found ${matches.length} matching rule(s).`);Each addRuleSource() recompiles the accumulated rule set. When adding more than a couple of sources, prefer addRuleSources(), which compiles them in one pass.
serialize() returns a self-contained Buffer that another process or worker thread can restore with deserialize(), skipping rule parsing and pattern compilation:
import { compile, deserialize } from "@litko/yara-x";
import { readFile, writeFile } from "node:fs/promises";
const rules = compile(`
rule ProductionIndicator {
strings:
$target = "malicious payload"
condition:
$target
}
`);
await writeFile("./rules.yarx", rules.serialize());
const restored = deserialize(await readFile("./rules.yarx"));
const matches = restored.scan(Buffer.from("malicious payload"));
console.log(`Matches from restored rules: ${matches.length}`);Restored rules behave exactly like freshly compiled ones. Conditions still have to be recompiled for the local platform when the blob is loaded: measured at ~1.7 ms per process for the two-rule set in __test__/performance-regression.mjs.
Note
The producer and consumer must run the same YARA-X version (1.21.x) with the same module feature set. Serialized blobs contain compiled bytecode and pattern tables, and they do not obfuscate pattern strings.
validate() checks syntax and semantics without building a scanner:
import { validate } from "@litko/yara-x";
const result = validate(`
rule ValidationExample {
strings:
$pattern = "valid pattern"
condition:
$pattern
}
`);
if (result.errors.length > 0) {
for (const error of result.errors) {
console.error(`[${error.code}] line ${error.line}, col ${error.column}: ${error.message}`);
}
}getWarnings() returns the warnings emitted while compiling, and the compiler can be told to report fewer of them:
import { compile } from "@litko/yara-x";
const rules = compile(`
rule WarningRule {
strings:
$unused = "unused string"
condition:
true
}
`);
for (const warning of rules.getWarnings()) {
console.log(`[${warning.code}] ${warning.message}`);
}
const quietRules = compile(sourceText, {
maxWarnings: 10,
disableWarnings: ["slow_pattern", "duplicate_pattern_value"],
// enableAllWarnings: false, // suppress all compiler warnings
});ignoreInvalidRules keeps a large corpus compiling when a few rules are broken:
import { compile } from "@litko/yara-x";
const rules = compile(mixedCorpus, { ignoreInvalidRules: true });
// Rules that were skipped, and why.
for (const rule of rules.getIgnoredRules()) {
console.log(`Skipped "${rule.name}" (${rule.reason}): ${rule.detail}`);
}
// Errors that could not be attributed to a single rule.
for (const error of rules.getCompilationErrors()) {
console.log(`[${error.code}] ${error.message}`);
}include statements are resolved against includeDirectories:
import { compile } from "@litko/yara-x";
const rules = compile(
`
include "common/strings.yar"
include "malware/patterns.yar"
rule MainDetection {
condition:
common_string_rule or malware_rule
}
`,
{
enableIncludes: true,
includeDirectories: ["./rules", "./rules/common", "./rules/malware"],
},
);These controls apply to the scanner instance and protect against pathological rules and inputs.
// Collect at most 1,000 matches per pattern.
rules.setMaxMatchesPerPattern(1000);
const matches = rules.scan(Buffer.from("pattern ".repeat(10000)));
console.log(matches[0].matches.length); // 1000File scans use memory-mapped I/O by default. Turning it off falls back to streaming reads, which is safer when the file could be modified while it is being scanned:
rules.setUseMmap(false);
const matches = rules.scanFile("./untrusted_sample.bin");rules.setTimeout(5000); // milliseconds
try {
const matches = rules.scan(largeBuffer);
} catch (error) {
console.error(`Scan aborted: ${error.message}`);
}setMatchContextSize() includes surrounding bytes with every match, which is usually what you want for triage output:
rules.setMatchContextSize(10);
const matches = rules.scan(Buffer.from("header data confidential payload trailer info"));
for (const match of matches[0]?.matches ?? []) {
console.log(`Match: "${match.data}"`);
console.log(`Context: "${match.contextData}" (offset ${match.contextMatchOffset})`);
}YARA-X compiles rule conditions to WebAssembly. compileToWasm() and compileFileToWasm() write that module to disk, and an existing scanner can emit its own module with emitWasmFile():
import { compile, compileToWasm } from "@litko/yara-x";
compileToWasm(ruleString, "./output/rules.wasm");
const rules = compile(ruleString);
rules.emitWasmFile("./output/instance.wasm");
await rules.emitWasmFileAsync("./output/async_instance.wasm");Note
The output is a raw WebAssembly module containing the compiled conditions — useful for inspection with tools such as wasm2wat, or for running the conditions in a sandbox that accepts WebAssembly. To move compiled rules between machines, use serialize() and deserialize() instead.
Releases are built on GitHub-hosted runners, published to npm with trusted publishing and provenance, and the native binaries carry GitHub artifact attestations. Release assets are additionally signed keyless with Sigstore.
# npm registry signature and provenance attestations
npm audit signatures
# GitHub artifact attestation for a downloaded binary
gh attestation verify path/to/yara-x.*.node -R cawalch/node-yara-x
# Sigstore bundle attached to the GitHub release
cosign verify-blob --bundle yara-x.<target>.node.sigstore yara-x.<target>.node \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe numbers below come from the repository's committed baseline, __test__/baseline-performance.json, and can be regenerated with pnpm profile:baseline.
- Machine: Apple Silicon (darwin/arm64), 14 cores, 36 GB RAM
- Node: v26.10.0, release build with LTO
- Recorded: 2026-10-04
- Reproduce:
pnpm profile:baseline, thenpnpm profile:compare __test__/baseline-performance.jsonto check a change for regressions
The payloads are synthetic and highly repetitive (for example, 100 KB filled with a single byte plus three planted patterns), so the throughput column is an upper bound for a warm-cache microbenchmark rather than a prediction for real files.
| Rule shape | mean | p50 | p95 | p99 |
|---|---|---|---|---|
2 strings, any of them |
1.86 ms | 1.81 ms | 2.18 ms | 2.43 ms |
10 strings, 2 of ($a*) and 1 of ($b*) |
1.87 ms | 1.85 ms | 2.05 ms | 2.13 ms |
| 4 regular expressions | 10.21 ms | 10.19 ms | 10.45 ms | 11.11 ms |
| 5 separate rules | 2.16 ms | 2.14 ms | 2.28 ms | 2.62 ms |
| Hex patterns, regex and anchored strings | 2.27 ms | 2.25 ms | 2.41 ms | 2.85 ms |
50 iterations per rule shape.
Two-string rule, any of them, 100 iterations (10 for the 5 MB payload):
| Payload | mean | p50 | p95 | p99 | Throughput |
|---|---|---|---|---|---|
| 58 B | 3.7 µs | 3.5 µs | 5.3 µs | 11.4 µs | — |
| 100 KB | 14.8 µs | 14.0 µs | 17.8 µs | 19.8 µs | ~6.9 GB/s |
| 5 MB | 520 µs | 519 µs | 599 µs | 599 µs | ~10 GB/s |
| 50 KB with ~512 matches | 221 µs | 216 µs | 289 µs | 342 µs | ~0.23 GB/s |
Matches are what cost time: the 50 KB payload holds roughly 512 occurrences of the pattern, and scanning it takes longer than the 5 MB payload with three occurrences.
100 KB payload, 50 iterations per rule shape:
| Rule shape | mean | p50 | p95 | p99 |
|---|---|---|---|---|
| 2 strings | 13.8 µs | 12.5 µs | 19.2 µs | 44.5 µs |
| 10 strings | 12.5 µs | 11.9 µs | 14.9 µs | 28.1 µs |
| 4 regular expressions | 65.6 µs | 63.6 µs | 78.1 µs | 79.9 µs |
| 5 separate rules | 15.9 µs | 14.4 µs | 19.6 µs | 22.0 µs |
| Hex patterns, regex and anchored strings | 8.9 µs | 8.4 µs | 10.6 µs | 17.5 µs |
The baseline also records memory, CPU, and GC statistics for each of these runs; see the JSON for the full set.
The generated type definitions in index.d.ts are the authoritative reference for signatures; it is what TypeScript resolves when you import the package.
| Function | Description |
|---|---|
compile(ruleSource, options?) |
Compiles YARA rules from a string and returns a YaraX scanner. |
fromFile(rulePath, options?) |
Compiles YARA rules from a file and returns a YaraX scanner. |
create() |
Returns an empty YaraX scanner for incremental rule compilation. |
deserialize(data) |
Restores a YaraX scanner from a serialized Buffer. |
validate(ruleSource, options?) |
Validates rules without creating a scanner. |
compileToWasm(ruleSource, outputPath, options?) |
Compiles rules from a string and writes the WebAssembly module to outputPath. |
compileFileToWasm(rulesPath, outputPath, options?) |
Compiles rules from a file and writes the WebAssembly module to outputPath. |
| Method | Description |
|---|---|
scan(data, variables?) |
Scans a Buffer synchronously and returns the matching rules. |
scanFile(filePath, variables?) |
Scans a file on disk synchronously. |
scanAsync(data, variables?) |
Scans a Buffer on a worker thread. |
scanFileAsync(filePath, variables?) |
Scans a file on disk on a worker thread. |
serialize() |
Serializes compiled rules into a portable Buffer. |
addRuleSource(ruleSource, namespace?) |
Adds one rule source to an incremental scanner. |
addRuleSources(ruleSources) |
Adds several rule sources in a single compilation pass. |
addRuleFile(filePath, namespace?) |
Adds rules from a file to an incremental scanner. |
defineVariable(name, value) |
Defines a global variable on an incremental scanner. |
getWarnings() |
Returns compiler warnings from the last compilation. |
getIgnoredRules() |
Returns rules skipped by tolerant compilation or ignored modules. |
getCompilationErrors() |
Returns source-level errors collected when ignoreInvalidRules is set. |
setMaxMatchesPerPattern(maxMatches) |
Caps the matches collected per pattern. |
setUseMmap(useMmap) |
Enables or disables memory-mapped file scanning. |
setTimeout(timeoutMs) |
Sets the scan timeout in milliseconds. |
setMatchContextSize(size) |
Sets how many context bytes are returned with each match. |
emitWasmFile(outputPath) |
Writes the compiled WebAssembly condition module to disk synchronously. |
emitWasmFileAsync(outputPath) |
Writes the compiled WebAssembly condition module to disk asynchronously. |
| Option | Type | Description |
|---|---|---|
namespace |
string |
Target namespace for the compiled rules. |
defineVariables |
Record<string, string | number | boolean> |
Global variables defined on the compiler. |
ignoreInvalidRules |
boolean |
Skips invalid rules instead of failing compilation. |
ignoreModules |
string[] |
Module names to ignore during compilation. |
bannedModules |
BannedModule[] |
Modules that raise a compilation error when imported. |
features |
string[] |
Feature flags to enable. |
relaxedReSyntax |
boolean |
Enables relaxed regular expression syntax. |
conditionOptimization |
boolean |
Enables rule condition optimization. |
errorOnSlowPattern |
boolean |
Promotes slow pattern warnings to errors. |
errorOnSlowLoop |
boolean |
Promotes slow loop warnings to errors. |
maxWarnings |
number |
Maximum number of warnings reported. |
disableWarnings |
string[] |
Warning codes to suppress. |
enableAllWarnings |
boolean |
Enables or disables all compiler warnings (default true). |
includeDirectories |
string[] |
Directories searched when resolving include statements. |
enableIncludes |
boolean |
Enables or disables include processing. |
BannedModule, CompileResult, CompilerError, CompilerWarning, IgnoredRule, MatchData, RuleMatch, RuleSource, and the YaraX class are all exported and documented in index.d.ts.
| Platform | Architectures | Binary package |
|---|---|---|
| macOS | x64, arm64 | @litko/yara-x-darwin-x64, @litko/yara-x-darwin-arm64 |
| Linux (glibc) | x64, arm64 | @litko/yara-x-linux-x64-gnu, @litko/yara-x-linux-arm64-gnu |
| Windows | x64, arm64 | @litko/yara-x-win32-x64-msvc, @litko/yara-x-win32-arm64-msvc |
These packages are installed automatically as optional dependencies — do not depend on them directly.
Cannot find module '@litko/yara-x-<platform>' — there is no prebuilt binary for your platform and the optional dependency was skipped. The Linux builds target glibc, so musl-based images such as Alpine are not supported: use a glibc-based image such as node:22-bookworm-slim. On other platforms, open an issue with your process.platform, process.arch and Node.js version.
Unsupported platform on an unusual architecture — the six targets above are the full supported set. armv7, 32-bit x86, and s390x are not built.
Node.js 20 or newer required — the package declares "engines": { "node": ">= 20" } and uses napi-rs v3, which requires Node 20+.
Deserialized rules fail to scan — the producing and consuming environments must use the same YARA-X version and module features. See Serialize compiled rules.
- YARA-X rule syntax and differences from YARA
- Release process
- Releases and changelogs
- Security policy — please report vulnerabilities privately
This package is distributed under two licenses:
- MIT — the Node.js bindings and project code. See
LICENSE-MIT. - BSD-3-Clause — the bundled YARA-X library code, which is Copyright (c) VirusTotal. See
LICENSE-BSD-3-Clause.