Skip to main content

max / makenotwork

server: yara per-file fail-open on compile error The yara-x crate's pure-Rust engine has incomplete coverage of upstream YARA's built-in identifiers (filename, filepath, extension, etc.). Third-party rule corpora like Florian Roth's signature-base include rules that rely on those identifiers; a single such rule was taking down the entire scanner with a global compile error. compile_rules_from_dir now skips uncompilable files individually and logs each one, letting the rest of the corpus compile cleanly. On the live signature-base (746 files), one rule (gen_fake_amsi_dll using \`filename\`) skips and the other 745 load fine. New test covers the mixed-valid-and-invalid case; existing "invalid-rule-is-error" test renamed to reflect the new policy.
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-24 21:15 UTC
Commit: ca3516c58391f86910fe8c32e780b8452502df82
Parent: 39c6876
1 file changed, +55 insertions, -10 deletions
@@ -26,6 +26,7 @@ pub fn compile_rules_from_dir(dir: &str) -> Result<Option<yara_x::Rules>, String
26 26
27 27 let mut compiler = yara_x::Compiler::new();
28 28 let mut rule_count = 0;
29 + let mut skipped_count = 0;
29 30
30 31 let entries = std::fs::read_dir(path)
31 32 .map_err(|e| format!("Failed to read YARA rules directory: {}", e))?;
@@ -35,18 +36,42 @@ pub fn compile_rules_from_dir(dir: &str) -> Result<Option<yara_x::Rules>, String
35 36 let file_path = entry.path();
36 37
37 38 if file_path.extension().is_some_and(|ext| ext == "yar" || ext == "yara") {
38 - let source = std::fs::read_to_string(&file_path)
39 - .map_err(|e| format!("Failed to read {}: {}", file_path.display(), e))?;
40 -
41 - compiler
42 - .add_source(source.as_str())
43 - .map_err(|e| format!("Failed to compile {}: {}", file_path.display(), e))?;
39 + let source = match std::fs::read_to_string(&file_path) {
40 + Ok(s) => s,
41 + Err(e) => {
42 + tracing::warn!(file = %file_path.display(), error = %e, "skipping unreadable YARA rule file");
43 + skipped_count += 1;
44 + continue;
45 + }
46 + };
44 47
45 - rule_count += 1;
46 - tracing::debug!(file = %file_path.display(), "Loaded YARA rule file");
48 + // Per-file fail-open. Third-party rule corpora (e.g. Florian Roth's
49 + // signature-base) include rules that exercise built-in identifiers
50 + // (`filename`, `filepath`, `extension`, ...) which yara-x's pure-Rust
51 + // engine does not yet implement. A single such rule must not take
52 + // down the whole scanner — skip the file and log the rule path so
53 + // operators can audit coverage gaps.
54 + match compiler.add_source(source.as_str()) {
55 + Ok(_) => {
56 + rule_count += 1;
57 + tracing::debug!(file = %file_path.display(), "Loaded YARA rule file");
58 + }
59 + Err(e) => {
60 + tracing::warn!(
61 + file = %file_path.display(),
62 + error = %e,
63 + "skipping YARA rule file that yara-x cannot compile"
64 + );
65 + skipped_count += 1;
66 + }
67 + }
47 68 }
48 69 }
49 70
71 + if skipped_count > 0 {
72 + tracing::info!(skipped_count, "YARA rule files skipped due to unsupported features");
73 + }
74 +
50 75 if rule_count == 0 {
51 76 tracing::info!(dir = %dir, "No YARA rule files found");
52 77 return Ok(None);
@@ -330,12 +355,32 @@ mod tests {
330 355 }
331 356
332 357 #[test]
333 - fn invalid_yara_rule_returns_error() {
358 + fn invalid_yara_rule_is_skipped_not_fatal() {
359 + // Per-file fail-open: a single uncompilable rule (e.g. one using a
360 + // yara-x-unsupported built-in identifier from a third-party corpus)
361 + // must not abort the entire scanner. The file is logged and skipped.
334 362 let dir = tempfile::tempdir().unwrap();
335 363 std::fs::write(dir.path().join("bad.yar"), "this is not valid YARA syntax").unwrap();
336 364
337 365 let result = compile_rules_from_dir(dir.path().to_str().unwrap());
338 - assert!(result.is_err());
366 + assert!(result.is_ok(), "skipped-on-error, not aborted");
367 + // No valid rules in the dir, so the function returns Ok(None) — the
368 + // pipeline interprets None as "yara not configured" → Skip verdict.
369 + assert!(result.unwrap().is_none());
370 + }
371 +
372 + #[test]
373 + fn mixed_valid_and_invalid_rules_keeps_valid() {
374 + let dir = tempfile::tempdir().unwrap();
375 + std::fs::write(
376 + dir.path().join("good.yar"),
377 + r#"rule clean_test { strings: $s = "marker" condition: $s }"#,
378 + ).unwrap();
379 + std::fs::write(dir.path().join("bad.yar"), "not yara at all").unwrap();
380 +
381 + let result = compile_rules_from_dir(dir.path().to_str().unwrap());
382 + assert!(result.is_ok());
383 + assert!(result.unwrap().is_some(), "valid rules still compile when bad files are present");
339 384 }
340 385
341 386 #[test]