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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +127 -9
- data/CHANGELOG.md +37 -0
- data/Gemfile.lock +1 -1
- data/README.md +30 -125
- data/USAGE.md +747 -0
- data/lib/yara/compiler.rb +161 -0
- data/lib/yara/ffi.rb +200 -0
- data/lib/yara/pattern_match.rb +178 -0
- data/lib/yara/scan_result.rb +428 -15
- data/lib/yara/scanner.rb +212 -6
- data/lib/yara/version.rb +1 -1
- data/lib/yara.rb +3 -0
- data/yara-ffi.gemspec +1 -1
- metadata +5 -2
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
|
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
|
-
|
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
|
-
|
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
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.
|
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
|