yara-ffi 4.0.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
@@ -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.
@@ -172,7 +239,8 @@ module Yara
172
239
  rule_name = identifier_ptr.read_string(identifier_len)
173
240
 
174
241
  # Create a result with the rule source for metadata/string parsing
175
- result = ScanResult.new(rule_name, rule_ptr, true, @rule_source)
242
+ # and the scanned data for pattern match extraction
243
+ result = ScanResult.new(rule_name, rule_ptr, true, @rule_source, test_string)
176
244
  results << result
177
245
 
178
246
  yield result if block_given?
@@ -200,6 +268,139 @@ module Yara
200
268
  block_given? ? nil : results
201
269
  end
202
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
281
+
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}"
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
371
+
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
402
+ end
403
+
203
404
  # Public: Free all resources associated with this scanner.
204
405
  #
205
406
  # This method releases memory allocated by YARA-X for the compiled rules
@@ -219,7 +420,9 @@ module Yara
219
420
  # Returns nothing.
220
421
  def close
221
422
  Yara::FFI.yrx_scanner_destroy(@scanner_pointer) if @scanner_pointer
222
- Yara::FFI.yrx_rules_destroy(@rules_pointer) if @rules_pointer
423
+ if @rules_pointer && instance_variable_defined?(:@owns_rules) && @owns_rules
424
+ Yara::FFI.yrx_rules_destroy(@rules_pointer)
425
+ end
223
426
  @scanner_pointer = nil
224
427
  @rules_pointer = nil
225
428
  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.0"
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.0
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