yara-ffi 4.0.0 → 4.1.1

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
@@ -63,6 +63,61 @@ module Yara
63
63
  @rule_source = ""
64
64
  end
65
65
 
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
85
+
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)
102
+ #
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
119
+ end
120
+
66
121
  # Public: Add a YARA rule to the scanner for later compilation.
67
122
  #
68
123
  # Rules are accumulated as source code and compiled together when compile()
@@ -134,6 +189,10 @@ module Yara
134
189
  # information about any matches found. When a block is provided, each
135
190
  # matching rule is yielded immediately as it's discovered during scanning.
136
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
+ #
137
196
  # Scanning treats the input as binary data regardless of content type.
138
197
  # String encoding is preserved but pattern matching occurs at the byte level.
139
198
  #
@@ -142,13 +201,21 @@ module Yara
142
201
  #
143
202
  # Examples
144
203
  #
145
- # # Collect all results
204
+ # # Collect all results with detailed pattern matches
146
205
  # results = scanner.scan("data to scan")
147
- # results.each { |match| puts match.rule_name }
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
148
215
  #
149
- # # Process matches immediately
216
+ # # Process matches immediately with pattern details
150
217
  # scanner.scan("data to scan") do |match|
151
- # puts "Found: #{match.rule_name}"
218
+ # puts "Found: #{match.rule_name} (#{match.total_matches} matches)"
152
219
  # end
153
220
  #
154
221
  # Returns a ScanResults object containing matches when no block given.
@@ -158,6 +225,9 @@ module Yara
158
225
  def scan(test_string)
159
226
  raise NotCompiledError, "Rules not compiled. Call compile() first." unless @scanner_pointer
160
227
 
228
+ # Handle nil input by treating it as empty string
229
+ test_string = "" if test_string.nil?
230
+
161
231
  results = ScanResults.new
162
232
 
163
233
  # Set up callback for matching rules
@@ -172,7 +242,8 @@ module Yara
172
242
  rule_name = identifier_ptr.read_string(identifier_len)
173
243
 
174
244
  # Create a result with the rule source for metadata/string parsing
175
- result = ScanResult.new(rule_name, rule_ptr, true, @rule_source)
245
+ # and the scanned data for pattern match extraction
246
+ result = ScanResult.new(rule_name, rule_ptr, true, @rule_source, test_string)
176
247
  results << result
177
248
 
178
249
  yield result if block_given?
@@ -200,6 +271,139 @@ module Yara
200
271
  block_given? ? nil : results
201
272
  end
202
273
 
274
+ # Public: Set a timeout for scanning operations on this scanner (milliseconds).
275
+ #
276
+ # This method configures the scanner to abort scans that take longer than
277
+ # the given timeout value. The timeout is specified in milliseconds.
278
+ #
279
+ # timeout_ms - Integer milliseconds to use as timeout
280
+ #
281
+ # Returns nothing. Raises ScanError on failure to set the timeout.
282
+ def set_timeout(timeout_ms)
283
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
284
+
285
+ result = Yara::FFI.yrx_scanner_set_timeout(@scanner_pointer, timeout_ms)
286
+ if result != Yara::FFI::YRX_SUCCESS
287
+ error_msg = Yara::FFI.yrx_last_error
288
+ raise ScanError, "Failed to set timeout: #{error_msg}"
289
+ end
290
+ nil
291
+ end
292
+
293
+ # Public: Set a global String variable for this scanner.
294
+ def set_global_str(ident, value)
295
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
296
+
297
+ result = Yara::FFI.yrx_scanner_set_global_str(@scanner_pointer, ident, value)
298
+ if result != Yara::FFI::YRX_SUCCESS
299
+ error_msg = Yara::FFI.yrx_last_error
300
+ raise ScanError, "Failed to set global string #{ident}: #{error_msg}"
301
+ end
302
+ nil
303
+ end
304
+
305
+ # Public: Set a global Boolean variable for this scanner.
306
+ def set_global_bool(ident, value)
307
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
308
+
309
+ result = Yara::FFI.yrx_scanner_set_global_bool(@scanner_pointer, ident, !!value)
310
+ if result != Yara::FFI::YRX_SUCCESS
311
+ error_msg = Yara::FFI.yrx_last_error
312
+ raise ScanError, "Failed to set global bool #{ident}: #{error_msg}"
313
+ end
314
+ nil
315
+ end
316
+
317
+ # Public: Set a global Integer variable for this scanner.
318
+ def set_global_int(ident, value)
319
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
320
+
321
+ result = Yara::FFI.yrx_scanner_set_global_int(@scanner_pointer, ident, value)
322
+ if result != Yara::FFI::YRX_SUCCESS
323
+ error_msg = Yara::FFI.yrx_last_error
324
+ raise ScanError, "Failed to set global int #{ident}: #{error_msg}"
325
+ end
326
+ nil
327
+ end
328
+
329
+ # Public: Set a global Float variable for this scanner.
330
+ def set_global_float(ident, value)
331
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
332
+
333
+ result = Yara::FFI.yrx_scanner_set_global_float(@scanner_pointer, ident, value)
334
+ if result != Yara::FFI::YRX_SUCCESS
335
+ error_msg = Yara::FFI.yrx_last_error
336
+ raise ScanError, "Failed to set global float #{ident}: #{error_msg}"
337
+ end
338
+ nil
339
+ end
340
+
341
+ # Public: Set multiple global variables at once from a hash.
342
+ #
343
+ # This convenience method allows setting multiple global variables in a single
344
+ # call, automatically detecting the appropriate type for each value. Supports
345
+ # String, Boolean (true/false), Integer, and Float values.
346
+ #
347
+ # globals - A Hash where keys are global variable names (String) and values
348
+ # are the global variable values (String, Boolean, Integer, or Float)
349
+ # strict - A Boolean indicating error handling mode:
350
+ # * true: raise ScanError on any failure (default)
351
+ # * false: ignore errors and continue setting other globals
352
+ #
353
+ # Examples
354
+ #
355
+ # # Set multiple globals with strict error handling
356
+ # scanner.set_globals({
357
+ # "ENV" => "production",
358
+ # "DEBUG" => false,
359
+ # "RETRIES" => 3,
360
+ # "THRESHOLD" => 0.95
361
+ # })
362
+ #
363
+ # # Set globals with lenient error handling
364
+ # scanner.set_globals({
365
+ # "DEFINED_VAR" => "value",
366
+ # "UNDEFINED_VAR" => "ignored" # Won't raise if undefined
367
+ # }, strict: false)
368
+ #
369
+ # Returns nothing.
370
+ # Raises NotCompiledError if scanner not initialized.
371
+ # Raises ScanError on any global setting failure when strict=true.
372
+ def set_globals(globals, strict: true)
373
+ raise NotCompiledError, "Scanner not initialized" unless @scanner_pointer
374
+
375
+ globals.each do |ident, value|
376
+ begin
377
+ case value
378
+ when String
379
+ set_global_str(ident, value)
380
+ when TrueClass, FalseClass
381
+ set_global_bool(ident, value)
382
+ when Integer
383
+ set_global_int(ident, value)
384
+ when Float
385
+ set_global_float(ident, value)
386
+ else
387
+ error_msg = "Unsupported global variable type for '#{ident}': #{value.class}"
388
+ if strict
389
+ raise ScanError, error_msg
390
+ else
391
+ # In non-strict mode, skip unsupported types silently
392
+ next
393
+ end
394
+ end
395
+ rescue ScanError => e
396
+ if strict
397
+ raise e
398
+ else
399
+ # In non-strict mode, continue with remaining globals
400
+ next
401
+ end
402
+ end
403
+ end
404
+ nil
405
+ end
406
+
203
407
  # Public: Free all resources associated with this scanner.
204
408
  #
205
409
  # This method releases memory allocated by YARA-X for the compiled rules
@@ -219,7 +423,9 @@ module Yara
219
423
  # Returns nothing.
220
424
  def close
221
425
  Yara::FFI.yrx_scanner_destroy(@scanner_pointer) if @scanner_pointer
222
- Yara::FFI.yrx_rules_destroy(@rules_pointer) if @rules_pointer
426
+ if @rules_pointer && instance_variable_defined?(:@owns_rules) && @owns_rules
427
+ Yara::FFI.yrx_rules_destroy(@rules_pointer)
428
+ end
223
429
  @scanner_pointer = nil
224
430
  @rules_pointer = nil
225
431
  end
data/lib/yara/version.rb CHANGED
@@ -5,5 +5,5 @@ module Yara
5
5
  #
6
6
  # This constant holds the current version of the Ruby gem, not the underlying
7
7
  # YARA-X library version. The gem version follows semantic versioning.
8
- VERSION = "4.0.0"
8
+ VERSION = "4.1.1"
9
9
  end
data/lib/yara.rb CHANGED
@@ -1,11 +1,14 @@
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"
7
9
  require_relative "yara/scan_results"
8
10
  require_relative "yara/scanner"
11
+ require_relative "yara/compiler"
9
12
  require_relative "yara/version"
10
13
 
11
14
  # Public: Main module providing Ruby FFI bindings to YARA-X for pattern
data/yara-ffi.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
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)/}) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yara-ffi
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Hoyt
@@ -43,10 +43,13 @@ files:
43
43
  - LICENSE.txt
44
44
  - README.md
45
45
  - Rakefile
46
+ - USAGE.md
46
47
  - bin/console
47
48
  - bin/setup
48
49
  - lib/yara.rb
50
+ - lib/yara/compiler.rb
49
51
  - lib/yara/ffi.rb
52
+ - lib/yara/pattern_match.rb
50
53
  - lib/yara/scan_result.rb
51
54
  - lib/yara/scan_results.rb
52
55
  - lib/yara/scanner.rb
@@ -60,7 +63,7 @@ licenses:
60
63
  metadata:
61
64
  homepage_uri: https://github.com/jonmagic/yara-ffi
62
65
  source_code_uri: https://github.com/jonmagic/yara-ffi
63
- changelog_uri: https://github.com/jonmagic/yara-ffi/main/CHANGELOG.md,
66
+ changelog_uri: https://github.com/jonmagic/yara-ffi/blob/main/CHANGELOG.md
64
67
  rdoc_options: []
65
68
  require_paths:
66
69
  - lib