max / makenotwork
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] |