yara_x/scanner/mod.rs
1/*! This module implements the YARA scanner.
2
3The scanner takes the rules produces by the compiler and scans data with them.
4*/
5use std::collections::{BTreeMap, HashMap};
6use std::fmt::{Debug, Formatter};
7use std::fs;
8use std::io::Read;
9use std::mem::transmute;
10use std::ops::Range;
11use std::path::{Path, PathBuf};
12use std::pin::Pin;
13use std::slice::Iter;
14use std::sync::Once;
15use std::sync::atomic::AtomicU64;
16use std::time::Duration;
17
18use bitvec::prelude::*;
19#[cfg(unix)]
20use memmap2::Advice;
21use memmap2::{Mmap, MmapOptions};
22use protobuf::{CodedInputStream, MessageDyn};
23use thiserror::Error;
24
25use crate::Variable;
26use crate::compiler::{RuleId, Rules};
27use crate::models::Rule;
28use crate::modules::{
29 ModuleContext, ModuleError, RegisteredModule, module_by_name,
30};
31pub(crate) use crate::scanner::context::RuntimeObject;
32pub(crate) use crate::scanner::context::RuntimeObjectHandle;
33pub(crate) use crate::scanner::context::ScanContext;
34pub(crate) use crate::scanner::context::ScanState;
35use crate::scanner::context::create_wasm_store_and_ctx;
36pub(crate) use crate::scanner::matches::Match;
37use crate::types::{Struct, TypeValue};
38use crate::variables::VariableError;
39use crate::wasm::MATCHING_RULES_BITMAP_BASE;
40use crate::wasm::runtime::Store;
41
42mod context;
43mod matches;
44
45pub mod blocks;
46
47#[cfg(test)]
48mod tests;
49
50/// Error returned when a scan operation fails.
51#[derive(Error, Debug)]
52#[non_exhaustive]
53pub enum ScanError {
54 /// The scan was aborted after the timeout period.
55 #[error("timeout")]
56 Timeout,
57 /// Could not open the scanned file.
58 #[error("can not open `{path}`: {err}")]
59 OpenError {
60 /// Path of the file being scanned.
61 path: PathBuf,
62 /// Error that occurred.
63 err: std::io::Error,
64 },
65 /// Could not map the scanned file into memory.
66 #[error("can not map `{path}`: {err}")]
67 MapError {
68 /// Path of the file being scanned.
69 path: PathBuf,
70 /// Error that occurred.
71 err: std::io::Error,
72 },
73 /// Could not deserialize the protobuf message for some YARA module.
74 #[error(
75 "can not deserialize protobuf message for YARA module `{module}`: {err}"
76 )]
77 ProtoError {
78 /// Module name.
79 module: String,
80 /// Error that occurred.
81 err: protobuf::Error,
82 },
83 /// The module is unknown.
84 #[error("unknown module `{module}`")]
85 UnknownModule {
86 /// Module name.
87 module: String,
88 },
89 /// Some module produced an error when it was invoked.
90 #[error("error in module `{module}`: {err}")]
91 ModuleError {
92 /// Module name.
93 module: String,
94 /// Error that occurred.
95 err: ModuleError,
96 },
97}
98
99/// Global counter that gets incremented every 1 second by a dedicated thread.
100///
101/// This counter is used for determining when a scan operation has timed out.
102static HEARTBEAT_COUNTER: AtomicU64 = AtomicU64::new(0);
103
104/// Used for spawning the thread that increments `HEARTBEAT_COUNTER`.
105static INIT_HEARTBEAT: Once = Once::new();
106
107/// Represents the data being scanned.
108///
109/// The scanned data can be backed by a slice owned by someone else, or a
110/// vector or memory-mapped file owned by `ScannedData` itself.
111pub enum ScannedData<'d> {
112 Slice(&'d [u8]),
113 Vec(Vec<u8>),
114 Mmap { mmap: Mmap, len: usize },
115}
116
117impl AsRef<[u8]> for ScannedData<'_> {
118 fn as_ref(&self) -> &[u8] {
119 match self {
120 ScannedData::Slice(s) => s,
121 ScannedData::Vec(v) => v.as_ref(),
122 ScannedData::Mmap { mmap, len } => &mmap.as_ref()[..*len],
123 }
124 }
125}
126
127impl ScannedData<'_> {
128 #[inline]
129 fn len(&self) -> usize {
130 self.as_ref().len()
131 }
132}
133
134impl<'d> TryInto<ScannedData<'d>> for &'d [u8] {
135 type Error = ScanError;
136 fn try_into(self) -> Result<ScannedData<'d>, Self::Error> {
137 Ok(ScannedData::Slice(self))
138 }
139}
140
141impl<'d, const N: usize> TryInto<ScannedData<'d>> for &'d [u8; N] {
142 type Error = ScanError;
143 fn try_into(self) -> Result<ScannedData<'d>, Self::Error> {
144 Ok(ScannedData::Slice(self))
145 }
146}
147
148/// Contains information about the time spent on a rule.
149#[cfg(feature = "rules-profiling")]
150pub struct ProfilingData<'r> {
151 /// Rule namespace.
152 pub namespace: &'r str,
153 /// Rule name.
154 pub rule: &'r str,
155 /// Time spent executing the rule's condition.
156 pub condition_exec_time: Duration,
157 /// Time spent matching the rule's patterns.
158 pub pattern_matching_time: Duration,
159}
160
161/// Optional information for the scan operation.
162#[derive(Debug, Default)]
163pub struct ScanOptions<'a> {
164 module_metadata: HashMap<&'a str, &'a [u8]>,
165}
166
167impl<'a> ScanOptions<'a> {
168 /// Creates a new instance of `ScanOptions` with no additional information
169 /// for the scan operation.
170 ///
171 /// Use other methods to add additional information.
172 pub fn new() -> Self {
173 Self { module_metadata: Default::default() }
174 }
175
176 /// Adds metadata for a YARA module.
177 pub fn set_module_metadata(
178 mut self,
179 module_name: &'a str,
180 metadata: &'a [u8],
181 ) -> Self {
182 self.module_metadata.insert(module_name, metadata);
183 self
184 }
185}
186
187/// Scans data with already compiled YARA rules.
188///
189/// The scanner receives a set of compiled [`Rules`] and scans data with those
190/// rules. The same scanner can be used for scanning multiple files or
191/// in-memory data sequentially, but you need multiple scanners for scanning in
192/// parallel.
193pub struct Scanner<'r> {
194 _rules: &'r Rules,
195 wasm_store: Pin<Box<Store<ScanContext<'static, 'static>>>>,
196 use_mmap: bool,
197 max_scan_size: Option<usize>,
198}
199
200impl<'r> Scanner<'r> {
201 /// Creates a new scanner.
202 pub fn new(rules: &'r Rules) -> Self {
203 let wasm_store = create_wasm_store_and_ctx(rules);
204 Self { _rules: rules, wasm_store, use_mmap: true, max_scan_size: None }
205 }
206
207 /// Sets a timeout for scan operations.
208 ///
209 /// The scan functions will return an [ScanError::Timeout] once the
210 /// provided timeout duration has elapsed. The scanner will make every
211 /// effort to stop promptly after the designated timeout duration. However,
212 /// in some cases, particularly with rules containing only a few patterns,
213 /// the scanner could potentially continue running for a longer period than
214 /// the specified timeout.
215 pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
216 self.scan_context_mut().set_timeout(timeout);
217 self
218 }
219
220 /// Sets the maximum number of matches per pattern.
221 ///
222 /// When some pattern reaches the maximum number of patterns it won't
223 /// produce more matches.
224 pub fn max_matches_per_pattern(&mut self, n: usize) -> &mut Self {
225 self.scan_context_mut()
226 .tracker
227 .pattern_matches
228 .max_matches_per_pattern(n);
229 self
230 }
231
232 /// Enables or disables fast scan mode.
233 ///
234 /// During rule compilation, the compiler analyzes rule conditions to
235 /// identify patterns that are only ever used in simple boolean existence
236 /// checks (e.g., `$a` in YARA). If a pattern is never queried for its match
237 /// count (`#a`), specific match offset (`@a`), match length (`!a`), or
238 /// evaluated inside a loop, it is classified as a fast-scan pattern.
239 ///
240 /// In fast scan mode, the scanner optimizes scans by stopping the search
241 /// and match tracking for these fast-scan patterns once their **very first
242 /// match** is found. Subsequent occurrences in the input data are ignored,
243 /// preventing redundant Aho-Corasick scans, regex evaluations, and match
244 /// memory allocations.
245 ///
246 /// Note that using fast scan mode implies that not all matches will be
247 /// reported. For instance, when iterating matches using [`ScanResults`],
248 /// you won't get all occurrences of the pattern in the file, only the first
249 /// one.
250 ///
251 /// ### Example
252 ///
253 /// ```
254 /// # use yara_x::{Compiler, Scanner};
255 /// let mut compiler = Compiler::new();
256 /// compiler.add_source(r#"
257 /// rule test {
258 /// strings:
259 /// $a = "abc"
260 /// condition:
261 /// $a
262 /// }
263 /// "#).unwrap();
264 ///
265 /// let rules = compiler.build();
266 /// let mut scanner = Scanner::new(&rules);
267 ///
268 /// // Enable fast scan mode.
269 /// scanner.fast_scan(true);
270 ///
271 /// // The haystack contains two matches of "abc".
272 /// let results = scanner.scan(b"abc...abc").unwrap();
273 ///
274 /// // Find the matching rule.
275 /// let matching_rule = results.matching_rules().next().unwrap();
276 ///
277 /// // Only a single match is returned for pattern $a.
278 /// let pattern = matching_rule.patterns().next().unwrap();
279 /// let mut matches = pattern.matches();
280 /// assert_eq!(matches.next().unwrap().range().start, 0); // The first match
281 /// assert!(matches.next().is_none()); // No other matches are returned
282 /// ```
283 pub fn fast_scan(&mut self, yes: bool) -> &mut Self {
284 self.scan_context_mut().tracker.fast_scan = yes;
285 self
286 }
287
288 /// Specifies whether [`Scanner::scan_file`] and [`Scanner::scan_file_with_options`]
289 /// may use memory-mapped files to read input.
290 ///
291 /// By default, the scanner uses memory mapping for very large files, as this
292 /// is typically faster than copying file contents into memory. However, this
293 /// approach has a drawback: if another process truncates the file during
294 /// scanning, a `SIGBUS` signal may occur.
295 ///
296 /// Setting this option disables memory mapping and forces the scanner to
297 /// always read files into an in-memory buffer instead. This method is slower,
298 /// but safer.
299 pub fn use_mmap(&mut self, yes: bool) -> &mut Self {
300 self.use_mmap = yes;
301 self
302 }
303
304 /// Sets the maximum size of the data that will be scanned.
305 ///
306 /// If the scanned data (either a file or an in-memory buffer) is larger
307 /// than this value, it will be truncated to the given size.
308 ///
309 /// The value returned by `filesize` will be also limited to the given
310 /// size.
311 ///
312 /// Also notice that some modules (pe, elf, macho, etc) may be unable
313 /// to properly parse truncated files.
314 pub fn max_scan_size(&mut self, size: usize) -> &mut Self {
315 self.max_scan_size = Some(size);
316 self
317 }
318
319 /// Sets a callback that is invoked every time a YARA rule calls the
320 /// `console` module.
321 ///
322 /// The `callback` function is invoked with a string representing the
323 /// message being logged. The function can print the message to stdout,
324 /// append it to a file, etc. If no callback is set these messages are
325 /// ignored.
326 pub fn console_log<F>(&mut self, callback: F) -> &mut Self
327 where
328 F: FnMut(String) + 'r,
329 {
330 self.scan_context_mut().console_log = Some(Box::new(callback));
331 self
332 }
333
334 /// Sets the context size for matches.
335 ///
336 /// This specifies how many bytes at the left and right of each match will
337 /// be reported by [`crate::Match::data_with_context`]. By default, the
338 /// match context size is 0, which means that [`crate::Match::data_with_context`]
339 /// will return exactly the same data as [`crate::Match::data`].
340 pub fn match_context_size(&mut self, size: usize) -> &mut Self {
341 self.scan_context_mut().match_context_size = size;
342 self
343 }
344
345 /// Scans in-memory data.
346 pub fn scan<'a>(
347 &'a mut self,
348 data: &'a [u8],
349 ) -> Result<ScanResults<'a, 'r>, ScanError> {
350 let mut data = data;
351 if let Some(max) = self.max_scan_size
352 && data.len() > max
353 {
354 data = &data[..max];
355 }
356 self.scan_impl(ScannedData::Slice(data), None)
357 }
358
359 /// Scans a file.
360 pub fn scan_file<'a, P>(
361 &'a mut self,
362 target: P,
363 ) -> Result<ScanResults<'a, 'r>, ScanError>
364 where
365 P: AsRef<Path>,
366 {
367 self.scan_impl(self.load_file(target.as_ref())?, None)
368 }
369
370 /// Like [`Scanner::scan`], but allows to specify additional scan options.
371 pub fn scan_with_options<'a, 'opts>(
372 &'a mut self,
373 data: &'a [u8],
374 options: ScanOptions<'opts>,
375 ) -> Result<ScanResults<'a, 'r>, ScanError> {
376 let mut data = data;
377 if let Some(max) = self.max_scan_size
378 && data.len() > max
379 {
380 data = &data[..max];
381 }
382 self.scan_impl(ScannedData::Slice(data), Some(options))
383 }
384
385 /// Like [`Scanner::scan_file`], but allows to specify additional scan
386 /// options.
387 pub fn scan_file_with_options<'opts, P>(
388 &mut self,
389 target: P,
390 options: ScanOptions<'opts>,
391 ) -> Result<ScanResults<'_, 'r>, ScanError>
392 where
393 P: AsRef<Path>,
394 {
395 self.scan_impl(self.load_file(target.as_ref())?, Some(options))
396 }
397
398 /// Sets the value of a global variable.
399 ///
400 /// The variable must has been previously defined by calling
401 /// [`crate::Compiler::define_global`], and the type it has during the
402 /// definition must match the type of the new value (`T`).
403 ///
404 /// The variable will retain the new value in subsequent scans, unless this
405 /// function is called again for setting a new value.
406 pub fn set_global<T: TryInto<Variable>>(
407 &mut self,
408 ident: &str,
409 value: T,
410 ) -> Result<&mut Self, VariableError>
411 where
412 VariableError: From<<T as TryInto<Variable>>::Error>,
413 {
414 self.scan_context_mut().set_global(ident, value)?;
415 Ok(self)
416 }
417
418 /// Sets the output data for a YARA module.
419 ///
420 /// Each YARA module generates an output consisting of a data structure that
421 /// contains information about the scanned file. This data structure is
422 /// represented by a Protocol Buffer message. Typically, you won't need to
423 /// provide this data yourself, as the YARA module automatically generates
424 /// different outputs for each file it scans.
425 ///
426 /// However, there are two scenarios in which you may want to provide the
427 /// output for a module yourself:
428 ///
429 /// 1) When the module does not produce any output on its own.
430 /// 2) When you already know the output of the module for the upcoming file
431 /// to be scanned, and you prefer to reuse this data instead of generating
432 /// it again.
433 ///
434 /// Case 1) applies to certain modules lacking a main function, thus
435 /// incapable of producing any output on their own. For such modules, you
436 /// must set the output before scanning the associated data. Since the
437 /// module's output typically varies with each scanned file, you need to
438 /// call [`Scanner::set_module_output`] prior to each invocation of
439 /// [`Scanner::scan`]. Once [`Scanner::scan`] is executed, the module's
440 /// output is consumed and will be empty unless set again before the
441 /// subsequent call.
442 ///
443 /// Case 2) applies when you have previously stored the module's output for
444 /// certain scanned data. In such cases, when rescanning the data, you can
445 /// utilize this function to supply the module's output, thereby preventing
446 /// redundant computation by the module. This optimization enhances
447 /// performance by eliminating the need for the module to reparse the
448 /// scanned data.
449 ///
450 /// <br>
451 ///
452 /// The `data` argument must be a Protocol Buffer message corresponding
453 /// to any of the existing YARA modules.
454 pub fn set_module_output(
455 &mut self,
456 data: Box<dyn MessageDyn>,
457 ) -> Result<&mut Self, ScanError> {
458 let descriptor = data.descriptor_dyn();
459 let full_name = descriptor.full_name();
460
461 // Check if the protobuf message passed to this function corresponds
462 // with any of the existing modules.
463 if !crate::modules::registered_modules()
464 .any(|m| m.root_descriptor().full_name() == full_name)
465 {
466 return Err(ScanError::UnknownModule {
467 module: full_name.to_string(),
468 });
469 }
470
471 self.scan_context_mut()
472 .user_provided_module_outputs
473 .insert(full_name.to_string(), data);
474
475 Ok(self)
476 }
477
478 /// Similar to [`Scanner::set_module_output`], but receives a module name
479 /// and the protobuf message as raw data.
480 ///
481 /// `name` can be either the YARA module name (i.e: "pe", "elf", "dotnet",
482 /// etc.) or the fully-qualified name for the protobuf message associated
483 /// to the module (i.e: "pe.PE", "elf.ELF", "dotnet.Dotnet", etc.).
484 pub fn set_module_output_raw(
485 &mut self,
486 name: &str,
487 data: &[u8],
488 ) -> Result<&mut Self, ScanError> {
489 // Try to find the module by name first, if not found, then try
490 // to find a module where the fully-qualified name for its protobuf
491 // message matches the `name` arguments.
492 let descriptor = module_by_name(name)
493 .map(|module| module.root_descriptor())
494 .or_else(|| {
495 crate::modules::registered_modules()
496 .find(|module| {
497 module.root_descriptor().full_name() == name
498 })
499 .map(|module| module.root_descriptor())
500 });
501
502 if descriptor.is_none() {
503 return Err(ScanError::UnknownModule { module: name.to_string() });
504 }
505
506 let mut is = CodedInputStream::from_bytes(data);
507
508 // Default recursion limit is 100, that's not enough for some deeply
509 // nested structures like the process tree in the `vt` module.
510 is.set_recursion_limit(500);
511
512 self.set_module_output(
513 descriptor.unwrap().parse_from(&mut is).map_err(|err| {
514 ScanError::ProtoError { module: name.to_string(), err }
515 })?,
516 )
517 }
518
519 /// Returns profiling data for the slowest N rules.
520 ///
521 /// The profiling data reflects the cumulative execution time of each rule
522 /// across all scanned files. This information is useful for identifying
523 /// performance bottlenecks. To reset the profiling data and start fresh
524 /// for subsequent scans, use [`Scanner::clear_profiling_data`].
525 #[cfg(feature = "rules-profiling")]
526 pub fn slowest_rules(&self, n: usize) -> Vec<ProfilingData<'_>> {
527 self.scan_context().slowest_rules(n)
528 }
529
530 /// Clears all accumulated profiling data.
531 ///
532 /// This method resets the profiling data collected during rule execution
533 /// across scanned files. Use this to start a new profiling session, ensuring
534 /// the results reflect only the data gathered after this method is called.
535 #[cfg(feature = "rules-profiling")]
536 pub fn clear_profiling_data(&mut self) {
537 self.scan_context_mut().clear_profiling_data()
538 }
539}
540
541impl<'r> Scanner<'r> {
542 #[cfg(feature = "rules-profiling")]
543 #[inline]
544 fn scan_context<'a>(&self) -> &ScanContext<'r, 'a> {
545 unsafe {
546 transmute::<&ScanContext<'static, 'static>, &ScanContext<'r, '_>>(
547 self.wasm_store.data(),
548 )
549 }
550 }
551 #[inline]
552 fn scan_context_mut<'a>(&mut self) -> &mut ScanContext<'r, 'a> {
553 unsafe {
554 transmute::<
555 &mut ScanContext<'static, 'static>,
556 &mut ScanContext<'r, '_>,
557 >(self.wasm_store.data_mut())
558 }
559 }
560
561 fn load_file<'a>(
562 &self,
563 path: &Path,
564 ) -> Result<ScannedData<'a>, ScanError> {
565 let file = fs::File::open(path).map_err(|err| {
566 ScanError::OpenError { path: path.to_path_buf(), err }
567 })?;
568
569 let mut size = file.metadata().map(|m| m.len()).unwrap_or(0) as usize;
570
571 if let Some(max_scan_size) = self.max_scan_size {
572 size = std::cmp::min(size, max_scan_size);
573 }
574
575 // For files smaller than ~500MB reading the whole file is faster than
576 // using a memory-mapped file.
577 let data = if self.use_mmap && size > 500_000_000 {
578 let mapped_file = unsafe {
579 MmapOptions::new().map_copy_read_only(&file).map_err(|err| {
580 ScanError::MapError { path: path.to_path_buf(), err }
581 })
582 }?;
583 #[cfg(unix)]
584 mapped_file.advise(Advice::Sequential).map_err(|err| {
585 ScanError::MapError { path: path.to_path_buf(), err }
586 })?;
587 ScannedData::Mmap { mmap: mapped_file, len: size }
588 } else {
589 let mut buffered_file = Vec::with_capacity(size);
590 (&file)
591 .take(size as u64)
592 .read_to_end(&mut buffered_file)
593 .map_err(|err| ScanError::OpenError {
594 path: path.to_path_buf(),
595 err,
596 })?;
597 ScannedData::Vec(buffered_file)
598 };
599
600 Ok(data)
601 }
602
603 fn scan_impl<'a, 'opts>(
604 &'a mut self,
605 data: ScannedData<'a>,
606 options: Option<ScanOptions<'opts>>,
607 ) -> Result<ScanResults<'a, 'r>, ScanError> {
608 let ctx = self.scan_context_mut();
609
610 // Clear information about matches found in a previous scan, if any.
611 ctx.reset();
612
613 // Set the global variable `filesize` to the size of the scanned data.
614 ctx.set_filesize(data.len() as i64);
615
616 // Create the context that will be passed to the main function of each
617 // module.
618 let mut mod_ctx = ModuleContext::default();
619
620 // For each item in options.module_metadata, check if the module name
621 // is actually a registered module, and then add the corresponding
622 // metadata to mod_ctx.
623 for (module, meta) in options
624 .map(|options| options.module_metadata)
625 .into_iter()
626 .flatten()
627 .filter_map(|(name, meta)| Some((module_by_name(name)?, meta)))
628 {
629 mod_ctx.set_module_metadata(module.name(), meta);
630 }
631
632 // Indicate that the scanner is currently scanning the given data.
633 ctx.scan_state = ScanState::ScanningData(data);
634
635 let data = match &ctx.scan_state {
636 ScanState::ScanningData(data) => data.as_ref(),
637 _ => unreachable!(),
638 };
639
640 for module_name in ctx.compiled_rules.imports() {
641 // Look up the module in the module registry.
642 let module = module_by_name(module_name)
643 .unwrap_or_else(|| panic!("module `{module_name}` not found"));
644
645 let module_root_descriptor = module.root_descriptor();
646 let root_struct_name = module_root_descriptor.full_name();
647
648 let module_output;
649 // If the user already provided some output for the module by
650 // calling `Scanner::set_module_output`, use that output. If not,
651 // call the module's main function (if the module has a main
652 // function) for getting its output.
653 if let Some(output) =
654 ctx.user_provided_module_outputs.remove(root_struct_name)
655 {
656 module_output = Some(output);
657 } else {
658 if let Some(main_res) = module.main_fn(&mut mod_ctx, data) {
659 module_output = Some(main_res.map_err(|err| {
660 ScanError::ModuleError {
661 module: module_name.to_string(),
662 err,
663 }
664 })?);
665 } else {
666 module_output = None;
667 }
668 }
669
670 if let Some(module_output) = &module_output {
671 // Make sure that the module is returning a protobuf message of
672 // the expected type.
673 debug_assert_eq!(
674 module_output.descriptor_dyn().full_name(),
675 root_struct_name,
676 "main function of module `{}` must return `{}`, but returned `{}`",
677 module_name,
678 root_struct_name,
679 module_output.descriptor_dyn().full_name(),
680 );
681
682 // Make sure that the module is returning a protobuf message
683 // where all required fields are initialized. This only applies
684 // to proto2, proto3 doesn't have "required" fields, all fields
685 // are optional.
686 debug_assert!(
687 module_output.is_initialized_dyn(),
688 "module `{}` returned a protobuf `{}` where some required fields are not initialized ",
689 module_name,
690 root_struct_name
691 );
692 }
693
694 // When constant folding is enabled we don't need to generate
695 // structure fields for enums. This is because during the
696 // optimization process symbols like MyEnum.ENUM_ITEM are resolved
697 // to their constant values at compile time. In other words, the
698 // compiler determines that MyEnum.ENUM_ITEM is equal to some value
699 // X, and uses that value in the generated code.
700 //
701 // However, without constant folding, enums are treated as any
702 // other field in a struct, and their values are determined at scan
703 // time. For that reason these fields must be generated for enums
704 // when constant folding is disabled.
705 let generate_fields_for_enums =
706 !cfg!(feature = "constant-folding");
707
708 let module_struct = Struct::from_proto_descriptor_and_msg(
709 &module_root_descriptor,
710 module_output.as_deref(),
711 generate_fields_for_enums,
712 false,
713 );
714
715 if let Some(module_output) = module_output {
716 ctx.module_outputs
717 .insert(root_struct_name.to_string(), module_output);
718 }
719
720 // The data structure obtained from the module is added to the
721 // root structure. Any data from previous scans will be replaced
722 // with the new data structure.
723 ctx.root_struct
724 .add_field(module_name, TypeValue::Struct(module_struct));
725 }
726
727 // The user provided module outputs are not needed anymore. Let's
728 // clear any remaining entry in the hash map (which can happen if
729 // the user has set outputs for modules that are not even imported
730 // by the rules.
731 ctx.user_provided_module_outputs.clear();
732
733 // Clear the flag that indicates that the search phase was done.
734 ctx.set_pattern_search_done(false);
735
736 // Evaluate the conditions of every rule, this will call
737 // `ScanContext::search_for_patterns` if necessary.
738 ctx.eval_conditions()?;
739
740 let data = match ctx.scan_state.take() {
741 ScanState::ScanningData(data) => data,
742 _ => unreachable!(),
743 };
744
745 ctx.scan_state = ScanState::Finished(DataSnippets::SingleBlock(data));
746
747 Ok(ScanResults::new(ctx))
748 }
749}
750
751/// Helper type that exposes the data matched during a scan operation.
752///
753/// Matching data can be accessed through the [`Match::data`] method. Normally,
754/// this data can be retrieved by slicing directly into the scanned input.
755/// However, that requires the original input to remain valid until the scan
756/// results are processed. This works fine for a single contiguous block of
757/// memory, but is impractical when scanning multiple blocks, since holding
758/// onto all of them until the end would consume excessive memory.
759///
760/// To handle this, two strategies are used:
761///
762/// - **Single-block scans**: Data is accessed directly from the input slice.
763/// - **Multi-block scans**: Matching fragments are copied and retained in a
764/// BTreeMap until the results are processed. The keys in the btree are
765/// the offsets where the snippets start and the values are vectors with
766/// the snippet's data.
767///
768/// Each strategy corresponds to a variant in this enum.
769pub(crate) enum DataSnippets<'d> {
770 SingleBlock(ScannedData<'d>),
771 MultiBlock(BTreeMap<usize, Vec<u8>>),
772}
773
774impl DataSnippets<'_> {
775 pub(crate) fn get(&self, range: Range<usize>) -> Option<&[u8]> {
776 self.get_with_context(range, 0).map(|(data, _)| data)
777 }
778
779 /// Gets the data for the given `range`, but adding `context_size` additional
780 /// bytes to the left and right.
781 ///
782 /// Returns a tuple where the first item is the data slice with context,
783 /// and the second item is a range relative to the slice indicating where
784 /// the `range` part is located.
785 ///
786 /// The result will be `None` only if the data for `range` can't be found.
787 /// The additional bytes at the left and right will be added if possible,
788 /// but otherwise won't affect the result.
789 pub(crate) fn get_with_context(
790 &self,
791 range: Range<usize>,
792 context_size: usize,
793 ) -> Option<(&[u8], Range<usize>)> {
794 match self {
795 Self::SingleBlock(data) => {
796 let start = range.start.saturating_sub(context_size);
797 let end = range.end.saturating_add(context_size);
798 let end = std::cmp::min(end, data.len());
799
800 let slice = data.as_ref().get(start..end)?;
801 let rel_start = range.start - start;
802 let rel_end = range.end - start;
803
804 Some((slice, rel_start..rel_end))
805 }
806 Self::MultiBlock(btree) => {
807 for (snippet_offset, snippet_data) in
808 btree.range(..=range.start).rev()
809 {
810 // Calculate the start and end of the slice within the snippet.
811 let start = range.start.saturating_sub(*snippet_offset);
812 let end = range.end.saturating_sub(*snippet_offset);
813
814 if end > snippet_data.len() {
815 continue;
816 }
817
818 let start = start.saturating_sub(context_size);
819 let end = end.saturating_add(context_size);
820 let end = std::cmp::min(end, snippet_data.len());
821
822 match snippet_data.get(start..end) {
823 Some(data) if !data.is_empty() => {
824 let rel_start =
825 range.start - (*snippet_offset + start);
826 let rel_end =
827 range.end - (*snippet_offset + start);
828 return Some((data, rel_start..rel_end));
829 }
830 _ => continue,
831 }
832 }
833
834 None
835 }
836 }
837 }
838}
839
840/// Results of a scan operation.
841///
842/// Allows iterating over both the matching and non-matching rules.
843pub struct ScanResults<'a, 'r> {
844 ctx: &'a ScanContext<'r, 'a>,
845}
846
847impl Debug for ScanResults<'_, '_> {
848 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
849 f.write_str("ScanResults")
850 }
851}
852
853impl<'a, 'r> ScanResults<'a, 'r> {
854 fn new(ctx: &'a ScanContext<'r, 'a>) -> Self {
855 Self { ctx }
856 }
857
858 /// Returns an iterator that yields the matching rules in arbitrary order.
859 pub fn matching_rules(&self) -> MatchingRules<'_, 'r> {
860 MatchingRules::new(self.ctx)
861 }
862
863 /// Returns an iterator that yields the non-matching rules in arbitrary
864 /// order.
865 pub fn non_matching_rules(&self) -> NonMatchingRules<'_, 'r> {
866 NonMatchingRules::new(self.ctx)
867 }
868
869 /// Returns the protobuf produced by a YARA module after processing the
870 /// data.
871 ///
872 /// The result will be `None` if the module doesn't exist or didn't
873 /// produce any output.
874 pub fn module_output(
875 &self,
876 module_name: &str,
877 ) -> Option<&'a dyn MessageDyn> {
878 let module_descriptor =
879 module_by_name(module_name).map(|m| m.root_descriptor())?;
880 let module_output = self
881 .ctx
882 .module_outputs
883 .get(module_descriptor.full_name())?
884 .as_ref();
885 Some(module_output)
886 }
887
888 /// Returns an iterator that yields tuples composed of a YARA module name
889 /// and the protobuf produced by that module.
890 ///
891 /// Only returns the modules that produced some output.
892 pub fn module_outputs(&self) -> ModuleOutputs<'a, 'r> {
893 ModuleOutputs::new(self.ctx)
894 }
895}
896
897/// Iterator that yields the rules that matched during a scan.
898///
899/// Private rules are not included by default, use
900/// [`MatchingRules::include_private`] for changing this behaviour.
901pub struct MatchingRules<'a, 'r> {
902 ctx: &'a ScanContext<'r, 'a>,
903 iterator: Iter<'a, RuleId>,
904 len_non_private: usize,
905 len_private: usize,
906 include_private: bool,
907}
908
909impl<'a, 'r> MatchingRules<'a, 'r> {
910 fn new(ctx: &'a ScanContext<'r, 'a>) -> Self {
911 Self {
912 ctx,
913 iterator: ctx.matching_rules.iter(),
914 include_private: false,
915 len_non_private: ctx.matching_rules.len()
916 - ctx.num_matching_private_rules,
917 len_private: ctx.num_matching_private_rules,
918 }
919 }
920
921 /// Specifies whether the iterator should yield private rules.
922 ///
923 /// This does not reset the iterator to its initial state, the iterator will
924 /// continue from its current position.
925 pub fn include_private(mut self, yes: bool) -> Self {
926 self.include_private = yes;
927 self
928 }
929}
930
931impl<'a, 'r> Iterator for MatchingRules<'a, 'r> {
932 type Item = Rule<'a, 'r>;
933
934 fn next(&mut self) -> Option<Self::Item> {
935 let rules = self.ctx.compiled_rules;
936 loop {
937 let rule_id = *self.iterator.next()?;
938 let rule_info = rules.get(rule_id);
939 if rule_info.is_private {
940 self.len_private -= 1;
941 } else {
942 self.len_non_private -= 1;
943 }
944 if self.include_private || !rule_info.is_private {
945 return Some(Rule { ctx: Some(self.ctx), rule_info, rules });
946 }
947 }
948 }
949}
950
951impl ExactSizeIterator for MatchingRules<'_, '_> {
952 #[inline]
953 fn len(&self) -> usize {
954 if self.include_private {
955 self.len_non_private + self.len_private
956 } else {
957 self.len_non_private
958 }
959 }
960}
961
962/// Iterator that yields the rules that didn't match during a scan.
963///
964/// Private rules are not included by default, use
965/// [`NonMatchingRules::include_private`] for changing this behaviour.
966pub struct NonMatchingRules<'a, 'r> {
967 ctx: &'a ScanContext<'r, 'a>,
968 iterator: bitvec::slice::IterZeros<'a, u8, Lsb0>,
969 include_private: bool,
970 len_private: usize,
971 len_non_private: usize,
972}
973
974impl<'a, 'r> NonMatchingRules<'a, 'r> {
975 fn new(ctx: &'a ScanContext<'r, 'a>) -> Self {
976 let num_rules = ctx.compiled_rules.num_rules();
977 let main_memory = ctx
978 .wasm
979 .main_memory
980 .unwrap()
981 .data(unsafe { ctx.wasm.store.as_ref() });
982
983 let base = MATCHING_RULES_BITMAP_BASE as usize;
984
985 // Create a BitSlice that covers the region of main memory containing
986 // the bitmap that tells which rules matched and which did not.
987 let matching_rules_bitmap = BitSlice::<_, Lsb0>::from_slice(
988 &main_memory[base..base + num_rules / 8 + 1],
989 );
990
991 // The BitSlice will cover more bits than necessary, for example, if
992 // there are 3 rules the BitSlice will have 8 bits because it is
993 // created from a u8 slice that has 1 byte. Here we make sure that
994 // the BitSlice has exactly as many bits as existing rules.
995 let matching_rules_bitmap = &matching_rules_bitmap[0..num_rules];
996
997 Self {
998 ctx,
999 iterator: matching_rules_bitmap.iter_zeros(),
1000 include_private: false,
1001 len_non_private: ctx.compiled_rules.num_rules()
1002 - ctx.matching_rules.len()
1003 - ctx.num_non_matching_private_rules,
1004 len_private: ctx.num_non_matching_private_rules,
1005 }
1006 }
1007
1008 /// Specifies whether the iterator should yield private rules.
1009 ///
1010 /// This does not reset the iterator to its initial state, the iterator will
1011 /// continue from its current position.
1012 pub fn include_private(mut self, yes: bool) -> Self {
1013 self.include_private = yes;
1014 self
1015 }
1016}
1017
1018impl<'a, 'r> Iterator for NonMatchingRules<'a, 'r> {
1019 type Item = Rule<'a, 'r>;
1020
1021 fn next(&mut self) -> Option<Self::Item> {
1022 let rules = self.ctx.compiled_rules;
1023
1024 loop {
1025 let rule_id = RuleId::from(self.iterator.next()?);
1026 let rule_info = rules.get(rule_id);
1027
1028 if rule_info.is_private {
1029 self.len_private -= 1;
1030 } else {
1031 self.len_non_private -= 1;
1032 }
1033
1034 if self.include_private || !rule_info.is_private {
1035 return Some(Rule { ctx: Some(self.ctx), rule_info, rules });
1036 }
1037 }
1038 }
1039}
1040
1041impl ExactSizeIterator for NonMatchingRules<'_, '_> {
1042 #[inline]
1043 fn len(&self) -> usize {
1044 if self.include_private {
1045 self.len_non_private + self.len_private
1046 } else {
1047 self.len_non_private
1048 }
1049 }
1050}
1051
1052/// Iterator that returns the outputs produced by YARA modules.
1053pub struct ModuleOutputs<'a, 'r> {
1054 ctx: &'a ScanContext<'r, 'a>,
1055 len: usize,
1056 iterator: Box<dyn Iterator<Item = &'static dyn RegisteredModule> + 'a>,
1057}
1058
1059impl<'a, 'r> ModuleOutputs<'a, 'r> {
1060 fn new(ctx: &'a ScanContext<'r, 'a>) -> Self {
1061 Self {
1062 ctx,
1063 len: ctx.module_outputs.len(),
1064 iterator: Box::new(crate::modules::registered_modules()),
1065 }
1066 }
1067}
1068
1069impl ExactSizeIterator for ModuleOutputs<'_, '_> {
1070 #[inline]
1071 fn len(&self) -> usize {
1072 self.len
1073 }
1074}
1075
1076impl<'a> Iterator for ModuleOutputs<'a, '_> {
1077 type Item = (&'a str, &'a dyn MessageDyn);
1078
1079 fn next(&mut self) -> Option<Self::Item> {
1080 loop {
1081 let module = self.iterator.next()?;
1082 if let Some(module_output) = self
1083 .ctx
1084 .module_outputs
1085 .get(module.root_descriptor().full_name())
1086 {
1087 return Some((module.name(), module_output.as_ref()));
1088 }
1089 }
1090 }
1091}
1092
1093#[cfg(test)]
1094mod snippet_tests {
1095 use super::DataSnippets;
1096 use std::collections::BTreeMap;
1097
1098 #[test]
1099 fn snippets_multiblock() {
1100 let mut btree_map = BTreeMap::new();
1101
1102 btree_map.insert(0, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
1103 btree_map.insert(50, vec![50, 51, 52, 53, 54]);
1104 btree_map.insert(52, vec![52, 53]);
1105 btree_map.insert(100, vec![100, 101, 102, 103]);
1106
1107 let snippets = DataSnippets::MultiBlock(btree_map);
1108
1109 assert_eq!(snippets.get(0..2), Some([0, 1].as_slice()));
1110 assert_eq!(snippets.get(1..3), Some([1, 2].as_slice()));
1111 assert_eq!(snippets.get(8..9), Some([8].as_slice()));
1112 assert_eq!(snippets.get(10..11), None);
1113 assert_eq!(snippets.get(50..51), Some([50].as_slice()));
1114 assert_eq!(snippets.get(51..53), Some([51, 52].as_slice()));
1115 assert_eq!(snippets.get(50..54), Some([50, 51, 52, 53].as_slice()));
1116 assert_eq!(snippets.get(52..54), Some([52, 53].as_slice()));
1117 assert_eq!(snippets.get(52..55), Some([52, 53, 54].as_slice()));
1118 assert_eq!(snippets.get(52..53), Some([52].as_slice()));
1119 assert_eq!(snippets.get(50..56), None);
1120 assert_eq!(snippets.get(100..101), Some([100].as_slice()));
1121 assert_eq!(snippets.get(101..103), Some([101, 102].as_slice()));
1122
1123 assert_eq!(
1124 snippets.get_with_context(0..2, 1),
1125 Some(([0, 1, 2].as_slice(), 0..2))
1126 );
1127
1128 assert_eq!(
1129 snippets.get_with_context(0..2, 2),
1130 Some(([0, 1, 2, 3].as_slice(), 0..2))
1131 );
1132
1133 assert_eq!(
1134 snippets.get_with_context(2..4, 2),
1135 Some(([0, 1, 2, 3, 4, 5].as_slice(), 2..4))
1136 );
1137
1138 assert_eq!(
1139 snippets.get_with_context(51..52, 3),
1140 Some(([50, 51, 52, 53, 54].as_slice(), 1..2))
1141 );
1142
1143 assert_eq!(
1144 snippets.get_with_context(102..103, 3),
1145 Some(([100, 101, 102, 103].as_slice(), 2..3))
1146 );
1147 }
1148
1149 #[test]
1150 fn snippets_singleblock() {
1151 let data = b"Lorem ipsum dolor sit amet".to_vec();
1152 let scanned_data = super::ScannedData::Vec(data);
1153 let snippets = DataSnippets::SingleBlock(scanned_data);
1154
1155 // Test get
1156 assert_eq!(snippets.get(6..11), Some(b"ipsum".as_slice()));
1157 assert_eq!(snippets.get(0..5), Some(b"Lorem".as_slice()));
1158 assert_eq!(snippets.get(20..26), Some(b"t amet".as_slice()));
1159 assert_eq!(snippets.get(27..30), None);
1160
1161 // Test get_with_context
1162 // context_size = 5
1163 assert_eq!(
1164 snippets.get_with_context(6..11, 5),
1165 Some((b"orem ipsum dolo".as_slice(), 5..10))
1166 );
1167 assert_eq!(
1168 snippets.get_with_context(0..5, 5),
1169 Some((b"Lorem ipsu".as_slice(), 0..5))
1170 );
1171 assert_eq!(
1172 snippets.get_with_context(20..26, 5),
1173 Some((b"or sit amet".as_slice(), 5..11))
1174 );
1175 assert_eq!(snippets.get_with_context(32..35, 5), None);
1176 }
1177}