yara-ffi 3.1.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/yara/scanner.rb CHANGED
@@ -1,81 +1,472 @@
1
1
  module Yara
2
+ # Public: High-level interface for compiling YARA rules and scanning data.
3
+ #
4
+ # The Scanner class provides a Ruby-friendly interface to YARA-X functionality.
5
+ # It manages the complete lifecycle of rule compilation, scanner creation, data
6
+ # scanning, and proper resource cleanup. Use this class for all normal YARA
7
+ # operations rather than the low-level FFI bindings.
8
+ #
9
+ # The scanner follows a compile-then-scan workflow:
10
+ # 1. Create scanner and add rules with add_rule()
11
+ # 2. Compile rules with compile()
12
+ # 3. Scan data with scan()
13
+ # 4. Clean up resources with close() or use block syntax for automatic cleanup
14
+ #
15
+ # Examples
16
+ #
17
+ # # Automatic resource management (recommended)
18
+ # rule = 'rule test { strings: $a = "hello" condition: $a }'
19
+ # Scanner.open(rule) do |scanner|
20
+ # scanner.compile
21
+ # results = scanner.scan("hello world")
22
+ # end
23
+ #
24
+ # # Manual resource management
25
+ # scanner = Scanner.new
26
+ # scanner.add_rule(rule)
27
+ # scanner.compile
28
+ # results = scanner.scan(data)
29
+ # scanner.close # Required to prevent memory leaks
2
30
  class Scanner
31
+ # Public: Raised when YARA rule compilation fails.
32
+ #
33
+ # This exception indicates syntax errors, undefined variables, or other
34
+ # issues that prevent successful rule compilation. The message includes
35
+ # details from YARA-X about what went wrong.
36
+ class CompilationError < StandardError; end
37
+
38
+ # Public: Raised when scanning operations fail.
39
+ #
40
+ # This exception indicates runtime errors during data scanning, such as
41
+ # I/O errors, memory issues, or internal YARA-X failures.
42
+ class ScanError < StandardError; end
43
+
44
+ # Public: Raised when attempting to scan before compiling rules.
45
+ #
46
+ # This exception indicates a programming error where scan() was called
47
+ # before compile(). Rules must be compiled before scanning can occur.
3
48
  class NotCompiledError < StandardError; end
4
49
 
5
- ERROR_CALLBACK = proc do |error_level, file_name, line_number, rule, message, user_data|
6
- # noop
50
+ # Public: Initialize a new Scanner instance.
51
+ #
52
+ # Creates a new scanner in an empty state. Rules must be added with
53
+ # add_rule() and compiled with compile() before scanning can occur.
54
+ #
55
+ # Examples
56
+ #
57
+ # scanner = Scanner.new
58
+ # scanner.add_rule('rule test { condition: true }')
59
+ # scanner.compile
60
+ def initialize
61
+ @rules_pointer = nil
62
+ @scanner_pointer = nil
63
+ @rule_source = ""
7
64
  end
8
65
 
9
- SCAN_FINISHED = 3
66
+ # Create a Scanner instance from a pre-built YRX_RULES pointer.
67
+ #
68
+ # rules_ptr - FFI::Pointer returned by Compiler#build (YRX_RULES*)
69
+ # owns_rules - Boolean indicating whether this Scanner should destroy
70
+ # the rules when closed. Default: false (caller destroys).
71
+ #
72
+ # Returns a Scanner instance.
73
+ def self.from_rules(rules_ptr, owns_rules: false)
74
+ scanner = new
75
+ scanner.instance_variable_set(:@rules_pointer, rules_ptr)
76
+ scanner.instance_variable_set(:@owns_rules, owns_rules)
77
+
78
+ # Create underlying scanner
79
+ scanner_holder = ::FFI::MemoryPointer.new(:pointer)
80
+ result = Yara::FFI.yrx_scanner_create(rules_ptr, scanner_holder)
81
+ if result != Yara::FFI::YRX_SUCCESS
82
+ error_msg = Yara::FFI.yrx_last_error
83
+ raise CompilationError, "Failed to create scanner from rules: #{error_msg}"
84
+ end
10
85
 
11
- # Public: Initializes instance of scanner. Under the hood this creates a pointer,
12
- # then calls yr_compiler_create with that pointer.
86
+ scanner.instance_variable_set(:@scanner_pointer, scanner_holder.get_pointer(0))
87
+ scanner
88
+ end
89
+
90
+ # Public: Create a Scanner from a serialized rules blob.
91
+ #
92
+ # Creates a new Scanner by deserializing a previously serialized YARA-X
93
+ # rules blob. This is useful when you have precompiled rules that were
94
+ # produced by Yara::Compiler#build_serialized or shipped between processes
95
+ # or persisted to disk.
96
+ #
97
+ # bytes - A String containing the binary serialized representation of YRX_RULES
98
+ # owns_rules - A Boolean indicating if the returned Scanner will take ownership
99
+ # of the underlying rules and destroy them when close is called.
100
+ # If false, the caller is responsible for freeing the rules with
101
+ # Yara::FFI.yrx_rules_destroy (default: true)
13
102
  #
14
- # error_callback: (optional) Proc to be called when an error occurs.
15
- # user_data: (optional) Instance of UserData to store and pass information.
16
- def initialize(error_callback: ERROR_CALLBACK, user_data: UserData.new)
17
- @error_callback = error_callback
18
- @user_data = user_data
19
- @compiler_pointer = ::FFI::MemoryPointer.new(:pointer)
20
- Yara::FFI.yr_compiler_create(@compiler_pointer)
21
- @compiler_pointer = @compiler_pointer.get_pointer(0)
22
- Yara::FFI.yr_compiler_set_callback(@compiler_pointer, error_callback, user_data)
103
+ # Returns a Scanner instance that is ready to use (no additional compile step required).
104
+ # Raises CompilationError if deserialization fails.
105
+ def self.from_serialized(bytes, owns_rules: true)
106
+ data_ptr = ::FFI::MemoryPointer.from_string(bytes)
107
+ data_len = bytes.bytesize
108
+
109
+ rules_ptr_holder = ::FFI::MemoryPointer.new(:pointer)
110
+ result = Yara::FFI.yrx_rules_deserialize(data_ptr, data_len, rules_ptr_holder)
111
+ if result != Yara::FFI::YRX_SUCCESS
112
+ error_msg = Yara::FFI.yrx_last_error
113
+ raise CompilationError, "Failed to deserialize rules: #{error_msg}"
114
+ end
115
+
116
+ rules_ptr = rules_ptr_holder.get_pointer(0)
117
+ scanner = from_rules(rules_ptr, owns_rules: owns_rules)
118
+ scanner
23
119
  end
24
120
 
25
- # Public: Adds a rule to the scanner and returns the namespace value. If a namespace
26
- # is not provided it will default to nil and use the global namespace.
121
+ # Public: Add a YARA rule to the scanner for later compilation.
27
122
  #
28
- # rule_string - String containing the Yara rule to be added.
29
- # namespace: (optional) String containing the namespace to be used for the rule.
123
+ # Rules are accumulated as source code and compiled together when compile()
124
+ # is called. Multiple rules can be added to create rule sets. Optional
125
+ # namespacing allows logical grouping of related rules.
126
+ #
127
+ # rule_string - A String containing a complete YARA rule definition
128
+ # namespace - An optional String namespace to contain the rule
129
+ #
130
+ # Examples
131
+ #
132
+ # scanner.add_rule('rule test1 { condition: true }')
133
+ # scanner.add_rule('rule test2 { condition: false }', namespace: 'testing')
134
+ #
135
+ # Returns nothing.
30
136
  def add_rule(rule_string, namespace: nil)
31
- Yara::FFI.yr_compiler_add_string(@compiler_pointer, rule_string, namespace)
137
+ # For now, we'll just store the rule source and compile later
138
+ # yara-x doesn't have separate add_rule like libyara
139
+ if namespace
140
+ @rule_source += "\nnamespace #{namespace} {\n#{rule_string}\n}\n"
141
+ else
142
+ @rule_source += "\n#{rule_string}\n"
143
+ end
32
144
  end
33
145
 
146
+ # Public: Compile all added rules into an executable scanner.
147
+ #
148
+ # This method compiles all rules added via add_rule() into an optimized
149
+ # form suitable for scanning. Compilation must succeed before any scanning
150
+ # operations can be performed. The compiled rules are used to create an
151
+ # internal scanner object for efficient data processing.
152
+ #
153
+ # Examples
154
+ #
155
+ # scanner = Scanner.new
156
+ # scanner.add_rule('rule test { condition: true }')
157
+ # scanner.compile
158
+ #
159
+ # Returns nothing.
160
+ # Raises CompilationError if rule compilation fails.
34
161
  def compile
162
+ raise CompilationError, "No rules added" if @rule_source.empty?
163
+
35
164
  @rules_pointer = ::FFI::MemoryPointer.new(:pointer)
36
- Yara::FFI.yr_compiler_get_rules(@compiler_pointer, @rules_pointer)
165
+ result = Yara::FFI.yrx_compile(@rule_source, @rules_pointer)
166
+
167
+ if result != Yara::FFI::YRX_SUCCESS
168
+ error_msg = Yara::FFI.yrx_last_error
169
+ raise CompilationError, "Failed to compile rules: #{error_msg}"
170
+ end
171
+
37
172
  @rules_pointer = @rules_pointer.get_pointer(0)
38
- Yara::FFI.yr_compiler_destroy(@compiler_pointer)
173
+
174
+ # Create scanner
175
+ @scanner_pointer_holder = ::FFI::MemoryPointer.new(:pointer)
176
+ result = Yara::FFI.yrx_scanner_create(@rules_pointer, @scanner_pointer_holder)
177
+
178
+ if result != Yara::FFI::YRX_SUCCESS
179
+ error_msg = Yara::FFI.yrx_last_error
180
+ raise CompilationError, "Failed to create scanner: #{error_msg}"
181
+ end
182
+
183
+ @scanner_pointer = @scanner_pointer_holder.get_pointer(0)
39
184
  end
40
185
 
41
- def call(test_string)
42
- raise NotCompiledError unless @rules_pointer
43
-
44
- results = []
45
- scanning = true
46
- result_callback = proc do |context_ptr, callback_type, rule, user_data|
47
- if callback_type == SCAN_FINISHED
48
- scanning = false
49
- else
50
- result = ScanResult.new(callback_type, rule, user_data)
51
- results << result if result.rule_outcome?
186
+ # Public: Scan data against compiled rules.
187
+ #
188
+ # This method scans the provided data using all compiled rules, returning
189
+ # information about any matches found. When a block is provided, each
190
+ # matching rule is yielded immediately as it's discovered during scanning.
191
+ #
192
+ # The enhanced version provides detailed pattern match information including
193
+ # exact offsets and lengths for each pattern match, enabling precise forensic
194
+ # analysis and data extraction.
195
+ #
196
+ # Scanning treats the input as binary data regardless of content type.
197
+ # String encoding is preserved but pattern matching occurs at the byte level.
198
+ #
199
+ # test_string - A String containing the data to scan
200
+ # block - Optional block that receives each ScanResult as found
201
+ #
202
+ # Examples
203
+ #
204
+ # # Collect all results with detailed pattern matches
205
+ # results = scanner.scan("data to scan")
206
+ # results.each do |match|
207
+ # puts "Rule: #{match.rule_name}"
208
+ # match.pattern_matches.each do |pattern_name, matches|
209
+ # puts " Pattern #{pattern_name}: #{matches.size} matches"
210
+ # matches.each do |m|
211
+ # puts " At offset #{m.offset}: '#{m.matched_data(test_string)}'"
212
+ # end
213
+ # end
214
+ # end
215
+ #
216
+ # # Process matches immediately with pattern details
217
+ # scanner.scan("data to scan") do |match|
218
+ # puts "Found: #{match.rule_name} (#{match.total_matches} matches)"
219
+ # end
220
+ #
221
+ # Returns a ScanResults object containing matches when no block given.
222
+ # Returns nil when a block is provided (matches are yielded instead).
223
+ # Raises NotCompiledError if compile() has not been called.
224
+ # Raises ScanError if scanning fails.
225
+ def scan(test_string)
226
+ raise NotCompiledError, "Rules not compiled. Call compile() first." unless @scanner_pointer
227
+
228
+ results = ScanResults.new
229
+
230
+ # Set up callback for matching rules
231
+ callback = proc do |rule_ptr, user_data|
232
+ # Extract rule identifier
233
+ ident_ptr = ::FFI::MemoryPointer.new(:pointer)
234
+ len_ptr = ::FFI::MemoryPointer.new(:size_t)
235
+
236
+ if Yara::FFI.yrx_rule_identifier(rule_ptr, ident_ptr, len_ptr) == Yara::FFI::YRX_SUCCESS
237
+ identifier_ptr = ident_ptr.get_pointer(0)
238
+ identifier_len = len_ptr.get_ulong(0)
239
+ rule_name = identifier_ptr.read_string(identifier_len)
240
+
241
+ # Create a result with the rule source for metadata/string parsing
242
+ # and the scanned data for pattern match extraction
243
+ result = ScanResult.new(rule_name, rule_ptr, true, @rule_source, test_string)
244
+ results << result
245
+
246
+ yield result if block_given?
52
247
  end
248
+ end
53
249
 
54
- 0 # ERROR_SUCCESS
250
+ # Set the callback
251
+ result = Yara::FFI.yrx_scanner_on_matching_rule(@scanner_pointer, callback, nil)
252
+ if result != Yara::FFI::YRX_SUCCESS
253
+ error_msg = Yara::FFI.yrx_last_error
254
+ raise ScanError, "Failed to set callback: #{error_msg}"
55
255
  end
56
256
 
257
+ # Scan the data
57
258
  test_string_bytesize = test_string.bytesize
58
259
  test_string_pointer = ::FFI::MemoryPointer.new(:char, test_string_bytesize)
59
260
  test_string_pointer.put_bytes(0, test_string)
60
261
 
61
- Yara::FFI.yr_rules_scan_mem(
62
- @rules_pointer,
63
- test_string_pointer,
64
- test_string_bytesize,
65
- 0,
66
- result_callback,
67
- @user_data,
68
- 1,
69
- )
262
+ result = Yara::FFI.yrx_scanner_scan(@scanner_pointer, test_string_pointer, test_string_bytesize)
263
+ if result != Yara::FFI::YRX_SUCCESS
264
+ error_msg = Yara::FFI.yrx_last_error
265
+ raise ScanError, "Scan failed: #{error_msg}"
266
+ end
267
+
268
+ block_given? ? nil : results
269
+ end
270
+
271
+ # Public: Set a timeout for scanning operations on this scanner (milliseconds).
272
+ #
273
+ # This method configures the scanner to abort scans that take longer than
274
+ # the given timeout value. The timeout is specified in milliseconds.
275
+ #
276
+ # timeout_ms - Integer milliseconds to use as timeout
277
+ #
278
+ # Returns nothing. Raises ScanError on failure to set the timeout.
279
+ def set_timeout(timeout_ms)
280
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
70
281
 
71
- while scanning do
282
+ result = Yara::FFI.yrx_scanner_set_timeout(@scanner_pointer, timeout_ms)
283
+ if result != Yara::FFI::YRX_SUCCESS
284
+ error_msg = Yara::FFI.yrx_last_error
285
+ raise ScanError, "Failed to set timeout: #{error_msg}"
72
286
  end
287
+ nil
288
+ end
289
+
290
+ # Public: Set a global String variable for this scanner.
291
+ def set_global_str(ident, value)
292
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
293
+
294
+ result = Yara::FFI.yrx_scanner_set_global_str(@scanner_pointer, ident, value)
295
+ if result != Yara::FFI::YRX_SUCCESS
296
+ error_msg = Yara::FFI.yrx_last_error
297
+ raise ScanError, "Failed to set global string #{ident}: #{error_msg}"
298
+ end
299
+ nil
300
+ end
301
+
302
+ # Public: Set a global Boolean variable for this scanner.
303
+ def set_global_bool(ident, value)
304
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
305
+
306
+ result = Yara::FFI.yrx_scanner_set_global_bool(@scanner_pointer, ident, !!value)
307
+ if result != Yara::FFI::YRX_SUCCESS
308
+ error_msg = Yara::FFI.yrx_last_error
309
+ raise ScanError, "Failed to set global bool #{ident}: #{error_msg}"
310
+ end
311
+ nil
312
+ end
313
+
314
+ # Public: Set a global Integer variable for this scanner.
315
+ def set_global_int(ident, value)
316
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
317
+
318
+ result = Yara::FFI.yrx_scanner_set_global_int(@scanner_pointer, ident, value)
319
+ if result != Yara::FFI::YRX_SUCCESS
320
+ error_msg = Yara::FFI.yrx_last_error
321
+ raise ScanError, "Failed to set global int #{ident}: #{error_msg}"
322
+ end
323
+ nil
324
+ end
325
+
326
+ # Public: Set a global Float variable for this scanner.
327
+ def set_global_float(ident, value)
328
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
329
+
330
+ result = Yara::FFI.yrx_scanner_set_global_float(@scanner_pointer, ident, value)
331
+ if result != Yara::FFI::YRX_SUCCESS
332
+ error_msg = Yara::FFI.yrx_last_error
333
+ raise ScanError, "Failed to set global float #{ident}: #{error_msg}"
334
+ end
335
+ nil
336
+ end
337
+
338
+ # Public: Set multiple global variables at once from a hash.
339
+ #
340
+ # This convenience method allows setting multiple global variables in a single
341
+ # call, automatically detecting the appropriate type for each value. Supports
342
+ # String, Boolean (true/false), Integer, and Float values.
343
+ #
344
+ # globals - A Hash where keys are global variable names (String) and values
345
+ # are the global variable values (String, Boolean, Integer, or Float)
346
+ # strict - A Boolean indicating error handling mode:
347
+ # * true: raise ScanError on any failure (default)
348
+ # * false: ignore errors and continue setting other globals
349
+ #
350
+ # Examples
351
+ #
352
+ # # Set multiple globals with strict error handling
353
+ # scanner.set_globals({
354
+ # "ENV" => "production",
355
+ # "DEBUG" => false,
356
+ # "RETRIES" => 3,
357
+ # "THRESHOLD" => 0.95
358
+ # })
359
+ #
360
+ # # Set globals with lenient error handling
361
+ # scanner.set_globals({
362
+ # "DEFINED_VAR" => "value",
363
+ # "UNDEFINED_VAR" => "ignored" # Won't raise if undefined
364
+ # }, strict: false)
365
+ #
366
+ # Returns nothing.
367
+ # Raises NotCompiledError if scanner not initialized.
368
+ # Raises ScanError on any global setting failure when strict=true.
369
+ def set_globals(globals, strict: true)
370
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
73
371
 
74
- results
372
+ globals.each do |ident, value|
373
+ begin
374
+ case value
375
+ when String
376
+ set_global_str(ident, value)
377
+ when TrueClass, FalseClass
378
+ set_global_bool(ident, value)
379
+ when Integer
380
+ set_global_int(ident, value)
381
+ when Float
382
+ set_global_float(ident, value)
383
+ else
384
+ error_msg = "Unsupported global variable type for '#{ident}': #{value.class}"
385
+ if strict
386
+ raise ScanError, error_msg
387
+ else
388
+ # In non-strict mode, skip unsupported types silently
389
+ next
390
+ end
391
+ end
392
+ rescue ScanError => e
393
+ if strict
394
+ raise e
395
+ else
396
+ # In non-strict mode, continue with remaining globals
397
+ next
398
+ end
399
+ end
400
+ end
401
+ nil
75
402
  end
76
403
 
404
+ # Public: Free all resources associated with this scanner.
405
+ #
406
+ # This method releases memory allocated by YARA-X for the compiled rules
407
+ # and scanner objects. It must be called to prevent memory leaks when
408
+ # using manual resource management. After calling close(), the scanner
409
+ # cannot be used for further operations.
410
+ #
411
+ # The open() class method with a block automatically calls close() to
412
+ # ensure proper cleanup even if exceptions occur.
413
+ #
414
+ # Examples
415
+ #
416
+ # scanner = Scanner.new
417
+ # # ... use scanner
418
+ # scanner.close # Required for cleanup
419
+ #
420
+ # Returns nothing.
77
421
  def close
78
- Yara::FFI.yr_rules_destroy(@rules_pointer)
422
+ Yara::FFI.yrx_scanner_destroy(@scanner_pointer) if @scanner_pointer
423
+ if @rules_pointer && instance_variable_defined?(:@owns_rules) && @owns_rules
424
+ Yara::FFI.yrx_rules_destroy(@rules_pointer)
425
+ end
426
+ @scanner_pointer = nil
427
+ @rules_pointer = nil
428
+ end
429
+
430
+ # Public: Create a scanner with automatic resource management.
431
+ #
432
+ # This class method creates a Scanner instance and optionally adds an
433
+ # initial rule. When used with a block, it ensures proper resource cleanup
434
+ # by automatically calling close() even if exceptions occur during scanning.
435
+ # This is the recommended way to use Scanner for most applications.
436
+ #
437
+ # rule_string - An optional String containing a YARA rule definition
438
+ # namespace - An optional String namespace for the initial rule
439
+ # block - Block that receives the scanner instance
440
+ #
441
+ # Examples
442
+ #
443
+ # # Block syntax with automatic cleanup (recommended)
444
+ # Scanner.open(rule) do |scanner|
445
+ # scanner.compile
446
+ # results = scanner.scan(data)
447
+ # end
448
+ #
449
+ # # Without block (manual cleanup required)
450
+ # scanner = Scanner.open(rule)
451
+ # scanner.compile
452
+ # # ... use scanner
453
+ # scanner.close
454
+ #
455
+ # Returns the result of the block when block given.
456
+ # Returns a new Scanner instance when no block given.
457
+ def self.open(rule_string = nil, namespace: nil)
458
+ scanner = new
459
+ scanner.add_rule(rule_string, namespace: namespace) if rule_string
460
+
461
+ if block_given?
462
+ begin
463
+ yield scanner
464
+ ensure
465
+ scanner.close
466
+ end
467
+ else
468
+ scanner
469
+ end
79
470
  end
80
471
  end
81
472
  end
data/lib/yara/version.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yara
4
- VERSION = "3.1.0"
4
+ # Public: Version information for the yara-ffi gem.
5
+ #
6
+ # This constant holds the current version of the Ruby gem, not the underlying
7
+ # YARA-X library version. The gem version follows semantic versioning.
8
+ VERSION = "4.1.0"
5
9
  end
data/lib/yara.rb CHANGED
@@ -1,29 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ffi"
4
+ require "json"
4
5
  require "pry"
5
6
  require_relative "yara/ffi"
7
+ require_relative "yara/pattern_match"
6
8
  require_relative "yara/scan_result"
9
+ require_relative "yara/scan_results"
7
10
  require_relative "yara/scanner"
11
+ require_relative "yara/compiler"
8
12
  require_relative "yara/version"
9
13
 
14
+ # Public: Main module providing Ruby FFI bindings to YARA-X for pattern
15
+ # matching and malware detection.
16
+ #
17
+ # This gem provides a Ruby interface to the YARA-X library (Rust-based YARA
18
+ # implementation) for scanning files, strings, and binary data using YARA rules.
19
+ # It offers both high-level convenience methods and low-level scanner control.
20
+ #
21
+ # Examples
22
+ #
23
+ # # Quick scanning with automatic resource cleanup
24
+ # rule = 'rule test { strings: $a = "hello" condition: $a }'
25
+ # results = Yara.scan(rule, "hello world")
26
+ #
27
+ # # Manual scanner control for advanced use cases
28
+ # Yara::Scanner.open(rule) do |scanner|
29
+ # scanner.compile
30
+ # results = scanner.scan(data)
31
+ # end
10
32
  module Yara
11
- def self.start
12
- Yara::FFI.yr_initialize
13
- end
14
-
15
- def self.stop
16
- Yara::FFI.yr_finalize
33
+ # Public: Test a YARA rule against data with automatic cleanup.
34
+ #
35
+ # This is a convenience method that handles the complete scan lifecycle:
36
+ # rule compilation, scanning, and resource cleanup. Use this for simple
37
+ # one-off scans where you don't need fine-grained control.
38
+ #
39
+ # rule_string - A String containing the YARA rule definition
40
+ # test_string - A String containing the data to scan
41
+ #
42
+ # Examples
43
+ #
44
+ # rule = 'rule test { strings: $a = "malware" condition: $a }'
45
+ # results = Yara.test(rule, "potential malware signature")
46
+ # # => #<Yara::ScanResults:0x... @results=[...]>
47
+ #
48
+ # Returns a Yara::ScanResults object containing any matching rules.
49
+ # Raises Yara::Scanner::CompilationError if the rule is invalid.
50
+ # Raises Yara::Scanner::ScanError if scanning fails.
51
+ def self.test(rule_string, test_string)
52
+ Scanner.open(rule_string) do |scanner|
53
+ scanner.compile
54
+ scanner.scan(test_string)
55
+ end
17
56
  end
18
57
 
19
- def self.test(rule_string, test_string)
20
- start
21
- scanner = Yara::Scanner.new
22
- scanner.add_rule(rule_string)
23
- scanner.compile
24
- scanner.call(test_string)
25
- ensure
26
- scanner.close
27
- stop
58
+ # Public: Scan data with a YARA rule, optionally yielding each match.
59
+ #
60
+ # This is a convenience method for scanning with optional block-based
61
+ # processing of results. When a block is provided, each matching rule
62
+ # is yielded as it's found during scanning.
63
+ #
64
+ # rule_string - A String containing the YARA rule definition
65
+ # data - A String containing the data to scan
66
+ # block - Optional block that receives each ScanResult as found
67
+ #
68
+ # Examples
69
+ #
70
+ # # Collect all results
71
+ # results = Yara.scan(rule, data)
72
+ #
73
+ # # Process matches as they're found
74
+ # Yara.scan(rule, data) do |match|
75
+ # puts "Found: #{match.rule_name}"
76
+ # end
77
+ #
78
+ # Returns a Yara::ScanResults object when no block given, nil when block given.
79
+ # Raises Yara::Scanner::CompilationError if the rule is invalid.
80
+ # Raises Yara::Scanner::ScanError if scanning fails.
81
+ def self.scan(rule_string, data, &block)
82
+ Scanner.open(rule_string) do |scanner|
83
+ scanner.compile
84
+ scanner.scan(data, &block)
85
+ end
28
86
  end
29
87
  end
data/yara-ffi.gemspec CHANGED
@@ -8,15 +8,15 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Jonathan Hoyt"]
9
9
  spec.email = ["jonmagic@gmail.com"]
10
10
 
11
- spec.summary = "A Ruby API to libyara."
12
- spec.description = "Use libyara from Ruby via ffi bindings."
11
+ spec.summary = "A Ruby API to YARA-X."
12
+ spec.description = "Use YARA-X from Ruby via FFI bindings."
13
13
  spec.homepage = "https://github.com/jonmagic/yara-ffi"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = "https://github.com/jonmagic/yara-ffi"
19
- spec.metadata["changelog_uri"] = "https://github.com/jonmagic/yara-ffi/main/CHANGELOG.md,"
19
+ spec.metadata["changelog_uri"] = "https://github.com/jonmagic/yara-ffi/blob/main/CHANGELOG.md"
20
20
 
21
21
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
22
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }