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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +266 -0
- data/.github/workflows/ruby.yml +69 -17
- data/CHANGELOG.md +90 -1
- data/DEVELOPMENT.md +188 -0
- data/Dockerfile +19 -11
- data/Gemfile.lock +38 -23
- data/README.md +56 -36
- data/USAGE.md +747 -0
- data/lib/yara/compiler.rb +161 -0
- data/lib/yara/ffi.rb +500 -111
- data/lib/yara/pattern_match.rb +178 -0
- data/lib/yara/scan_result.rb +573 -71
- data/lib/yara/scan_results.rb +224 -0
- data/lib/yara/scanner.rb +436 -45
- data/lib/yara/version.rb +5 -1
- data/lib/yara.rb +73 -15
- data/yara-ffi.gemspec +4 -4
- metadata +13 -15
- data/lib/yara/user_data.rb +0 -5
- data/lib/yara/yr_meta.rb +0 -10
- data/lib/yara/yr_namespace.rb +0 -5
- data/lib/yara/yr_rule.rb +0 -11
- data/lib/yara/yr_string.rb +0 -15
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
|
-
|
6
|
-
|
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
|
-
|
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
|
-
|
12
|
-
|
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
|
-
#
|
15
|
-
#
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
Yara::FFI
|
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:
|
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
|
-
#
|
29
|
-
#
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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.
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
12
|
-
spec.description = "Use
|
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.
|
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)/}) }
|