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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +127 -9
- data/CHANGELOG.md +31 -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 +209 -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.
|
@@ -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
|
-
|
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
|
-
|
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
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.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
|