yara-ffi 4.1.0 → 4.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f964b7eb475719ea6ceb456125b6ead06be8d9263a8839267ee96cdbe072b470
4
- data.tar.gz: 36cc2f79374b0d00f245329f5d3e18423b6f16740e92f893c07d662f2183ce67
3
+ metadata.gz: 3e278e7deb04454bfa05c54feb3763d282073abe392ddbd142cef55afa749637
4
+ data.tar.gz: ec29557182c143b5d8a696d7d494bbf7e737fdd115cddaf637538322902863fc
5
5
  SHA512:
6
- metadata.gz: 5636bffbece91a2b3d48568e7bcac3e75d0f7713231c1a0abfbcbf83a0436b1d950ca5dccb5dae98b82c6ab3686521f61b561a2802f1c26145f887ea64fccf37
7
- data.tar.gz: b10394734d3c00d986a5f12a6a2682087a7641e2abb62108728fc699f1e79f395f4bf2c5d94842c440fb15c960690773760907be56b4f14edbe5c86c3589aa3c
6
+ metadata.gz: 2bcfc0f2f49fbbb2faf7c0bd221c4b6c8b41ff7c3724feca975e4ae4c89f4d3528966efcb5123d315aafc95decde239ccd7d27a1c0327ce578990f84db7e131a
7
+ data.tar.gz: fb704aebc592c80b63012acbd060b17608f9a6c43250a560379c39b11e6e813825df67ddc4b5daaf676fa18da383fe3f73f66bfcc87421b906c21f33a721dfcf
@@ -18,7 +18,7 @@ jobs:
18
18
  strategy:
19
19
  fail-fast: false
20
20
  matrix:
21
- ruby-version: ['3.2', '3.3']
21
+ ruby-version: ['3.2', '3.3', '3.4']
22
22
 
23
23
  steps:
24
24
  - name: Checkout code
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [4.2.0] - 2025-11-13
4
+
5
+ - **NEW**: Added rule iteration API for inspecting compiled rules without scanning
6
+ - `Scanner#each_rule` - Iterate through all compiled rules (returns Enumerator)
7
+ - `Yara::Rule` class for accessing rule properties
8
+ - `Rule#identifier` - Get rule name
9
+ - `Rule#namespace` - Get rule namespace
10
+ - `Rule#metadata` - Access rule metadata as hash
11
+ - `Rule#tags` - Get rule tags as array
12
+ - Enables building rule catalogs and introspection without scanning data
13
+ - Works with compiled rules from `Scanner`, `Compiler`, or deserialized rules
14
+ - **IMPROVED**: Enhanced FFI struct handling for better memory safety with unions
15
+
16
+ ## [4.1.1] - 2025-08-20
17
+
18
+ - **FIXED**: Fixed crash when `Yara.test` or `Yara.scan` receive `nil` as the test string parameter ([#15](https://github.com/jonmagic/yara-ffi/issues/15))
19
+ - `nil` values are now treated as empty strings instead of causing `NoMethodError`
20
+ - Both `Yara.test(rule, nil)` and `Yara.scan(rule, nil)` now return empty `ScanResults` objects
21
+
3
22
  ## [4.1.0] - 2025-08-20
4
23
 
5
24
  - **NEW**: Added advanced `Yara::Compiler` API for complex rule compilation scenarios
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- yara-ffi (4.1.0)
4
+ yara-ffi (4.2.0)
5
5
  ffi
6
6
 
7
7
  GEM
@@ -14,24 +14,24 @@ GEM
14
14
  ffi (1.17.2-arm64-darwin)
15
15
  ffi (1.17.2-x86_64-darwin)
16
16
  ffi (1.17.2-x86_64-linux-gnu)
17
- json (2.13.2)
17
+ json (2.16.0)
18
18
  language_server-protocol (3.17.0.5)
19
19
  lint_roller (1.1.0)
20
20
  method_source (1.1.0)
21
- minitest (5.25.5)
21
+ minitest (5.26.1)
22
22
  parallel (1.27.0)
23
- parser (3.3.9.0)
23
+ parser (3.3.10.0)
24
24
  ast (~> 2.4.1)
25
25
  racc
26
- prism (1.4.0)
26
+ prism (1.6.0)
27
27
  pry (0.15.2)
28
28
  coderay (~> 1.1)
29
29
  method_source (~> 1.0)
30
30
  racc (1.8.1)
31
31
  rainbow (3.1.1)
32
- rake (13.3.0)
33
- regexp_parser (2.11.2)
34
- rubocop (1.79.2)
32
+ rake (13.3.1)
33
+ regexp_parser (2.11.3)
34
+ rubocop (1.81.7)
35
35
  json (~> 2.3)
36
36
  language_server-protocol (~> 3.17.0.2)
37
37
  lint_roller (~> 1.1.0)
@@ -39,16 +39,16 @@ GEM
39
39
  parser (>= 3.3.0.2)
40
40
  rainbow (>= 2.2.2, < 4.0)
41
41
  regexp_parser (>= 2.9.3, < 3.0)
42
- rubocop-ast (>= 1.46.0, < 2.0)
42
+ rubocop-ast (>= 1.47.1, < 2.0)
43
43
  ruby-progressbar (~> 1.7)
44
44
  unicode-display_width (>= 2.4.0, < 4.0)
45
- rubocop-ast (1.46.0)
45
+ rubocop-ast (1.48.0)
46
46
  parser (>= 3.3.7.2)
47
47
  prism (~> 1.4)
48
48
  ruby-progressbar (1.13.0)
49
- unicode-display_width (3.1.5)
50
- unicode-emoji (~> 4.0, >= 4.0.4)
51
- unicode-emoji (4.0.4)
49
+ unicode-display_width (3.2.0)
50
+ unicode-emoji (~> 4.1)
51
+ unicode-emoji (4.1.0)
52
52
 
53
53
  PLATFORMS
54
54
  aarch64-linux
data/USAGE.md CHANGED
@@ -69,6 +69,25 @@ result.tags # => ["malware", "trojan"]
69
69
  result.has_tag?("malware") # => true
70
70
  ```
71
71
 
72
+ ### Rule Iteration (Without Scanning)
73
+
74
+ ```ruby
75
+ # Inspect compiled rules without scanning data
76
+ scanner.compile
77
+ scanner.each_rule do |rule|
78
+ puts "Rule: #{rule.identifier}"
79
+ puts "Namespace: #{rule.namespace}"
80
+ puts "Tags: #{rule.tags.join(', ')}"
81
+
82
+ # Access metadata
83
+ rule.metadata.each { |k, v| puts " #{k}: #{v}" }
84
+
85
+ # Type-safe metadata access
86
+ author = rule.metadata_string(:author)
87
+ severity = rule.metadata_int(:severity)
88
+ end
89
+ ```
90
+
72
91
  ### Global Variables
73
92
 
74
93
  ```ruby
@@ -284,6 +303,141 @@ results2 = scanner.scan(data2)
284
303
  scanner.close
285
304
  ```
286
305
 
306
+ ### Iterating Rules Without Scanning
307
+
308
+ Extract rule information, metadata, and tags without scanning any data:
309
+
310
+ ```ruby
311
+ # Setup: compile multiple rules
312
+ scanner = Yara::Scanner.new
313
+ scanner.add_rule(rule1)
314
+ scanner.add_rule(rule2, namespace: "malware")
315
+ scanner.compile
316
+
317
+ # Iterate through all compiled rules
318
+ scanner.each_rule do |rule|
319
+ puts "Rule: #{rule.identifier}"
320
+ puts "Namespace: #{rule.namespace || 'default'}"
321
+ puts "Qualified Name: #{rule.qualified_name}"
322
+
323
+ # Access metadata
324
+ puts "\nMetadata:"
325
+ rule.metadata.each do |key, value|
326
+ puts " #{key}: #{value}"
327
+ end
328
+
329
+ # Access tags
330
+ if rule.tags.any?
331
+ puts "\nTags: #{rule.tags.join(', ')}"
332
+ end
333
+
334
+ # Type-safe metadata access
335
+ if author = rule.metadata_string(:author)
336
+ puts "Author: #{author}"
337
+ end
338
+
339
+ if severity = rule.metadata_int(:severity)
340
+ puts "Severity: #{severity}/10"
341
+ end
342
+
343
+ # Check for specific tags
344
+ if rule.has_tag?("trojan")
345
+ puts "⚠️ Trojan detection rule"
346
+ end
347
+ end
348
+
349
+ # Use as an Enumerator
350
+ rules = scanner.each_rule.to_a
351
+ puts "Total rules: #{rules.size}"
352
+
353
+ # Filter rules by criteria
354
+ high_severity = scanner.each_rule.select do |rule|
355
+ (rule.metadata_int(:severity) || 0) >= 8
356
+ end
357
+
358
+ malware_rules = scanner.each_rule.select do |rule|
359
+ rule.has_tag?("malware")
360
+ end
361
+
362
+ scanner.close
363
+ ```
364
+
365
+ **Example Rule with Metadata:**
366
+
367
+ ```ruby
368
+ rule = <<-RULE
369
+ rule SuspiciousActivity : malware trojan
370
+ {
371
+ meta:
372
+ author = "Security Team"
373
+ description = "Detects suspicious API calls"
374
+ severity = 8
375
+ date = "2024-01-15"
376
+ is_active = true
377
+ confidence = 0.95
378
+
379
+ strings:
380
+ $api1 = "VirtualAlloc"
381
+ $api2 = "WriteProcessMemory"
382
+
383
+ condition:
384
+ all of them
385
+ }
386
+ RULE
387
+
388
+ scanner = Yara::Scanner.new
389
+ scanner.add_rule(rule, namespace: "detection")
390
+ scanner.compile
391
+
392
+ scanner.each_rule do |rule|
393
+ puts "Rule: #{rule.identifier}" # => "SuspiciousActivity"
394
+ puts "Namespace: #{rule.namespace}" # => "detection"
395
+ puts "Full name: #{rule.qualified_name}" # => "detection.SuspiciousActivity"
396
+
397
+ # Access metadata with type safety
398
+ puts "Author: #{rule.metadata_string(:author)}" # => "Security Team"
399
+ puts "Severity: #{rule.metadata_int(:severity)}" # => 8
400
+ puts "Active: #{rule.metadata_bool(:is_active)}" # => true
401
+ puts "Confidence: #{rule.metadata_float(:confidence)}" # => 0.95
402
+
403
+ # Check tags
404
+ puts "Is malware rule: #{rule.has_tag?('malware')}" # => true
405
+ puts "Tags: #{rule.tags.join(', ')}" # => "malware, trojan"
406
+ end
407
+
408
+ scanner.close
409
+ ```
410
+
411
+ **Use Case: Building a Rule Catalog**
412
+
413
+ ```ruby
414
+ # Create a catalog of all rules without scanning
415
+ def build_rule_catalog(scanner)
416
+ catalog = {}
417
+
418
+ scanner.each_rule do |rule|
419
+ catalog[rule.identifier] = {
420
+ namespace: rule.namespace,
421
+ description: rule.metadata_string(:description),
422
+ author: rule.metadata_string(:author),
423
+ severity: rule.metadata_int(:severity),
424
+ tags: rule.tags,
425
+ active: rule.metadata_bool(:is_active) != false
426
+ }
427
+ end
428
+
429
+ catalog
430
+ end
431
+
432
+ scanner.compile
433
+ catalog = build_rule_catalog(scanner)
434
+
435
+ # Query the catalog
436
+ catalog.each do |name, info|
437
+ puts "#{name}: #{info[:description]}" if info[:active]
438
+ end
439
+ ```
440
+
287
441
  ## Pattern Matching Analysis
288
442
 
289
443
  ### Detailed Pattern Match Information
data/lib/yara/ffi.rb CHANGED
@@ -139,6 +139,17 @@ module Yara
139
139
  # C Signature: typedef void (*YRX_ON_MATCHING_RULE)(const struct YRX_RULE *rule, void *user_data)
140
140
  callback :matching_rule_callback, [:pointer, :pointer], :void
141
141
 
142
+ # Internal: Callback function type for rule iteration.
143
+ #
144
+ # This callback is invoked for each rule during rule iteration.
145
+ # The callback receives pointers to the rule and optional user data.
146
+ #
147
+ # rule - A Pointer to the YRX_RULE structure
148
+ # user_data - A Pointer to optional user-provided data
149
+ #
150
+ # C Signature: typedef void (*YRX_RULE_CALLBACK)(const struct YRX_RULE *rule, void *user_data)
151
+ callback :rule_callback, [:pointer, :pointer], :void
152
+
142
153
  # Public: Set callback for handling rule matches during scanning.
143
154
  #
144
155
  # This function registers a callback that will be invoked each time a rule
@@ -158,6 +169,26 @@ module Yara
158
169
  # C Signature: enum YRX_RESULT yrx_scanner_on_matching_rule(struct YRX_SCANNER *scanner, YRX_ON_MATCHING_RULE callback, void *user_data)
159
170
  attach_function :yrx_scanner_on_matching_rule, [:pointer, :matching_rule_callback, :pointer], :int
160
171
 
172
+ # Public: Iterate through all compiled rules without scanning.
173
+ #
174
+ # This function calls the provided callback for each rule in the compiled
175
+ # rules object, regardless of whether they would match any data. This is
176
+ # useful for inspecting rule metadata, tags, and patterns without performing
177
+ # an actual scan.
178
+ #
179
+ # rules - A Pointer to the YRX_RULES structure
180
+ # callback - A Proc matching the rule_callback signature
181
+ # user_data - A Pointer to optional data passed to callback (can be nil)
182
+ #
183
+ # Examples
184
+ #
185
+ # callback = proc { |rule_ptr, user_data| puts "Found rule" }
186
+ # result = Yara::FFI.yrx_rules_iter(rules_ptr, callback, nil)
187
+ #
188
+ # Returns an Integer result code (YRX_SUCCESS on success).
189
+ # C Signature: enum YRX_RESULT yrx_rules_iter(const struct YRX_RULES *rules, YRX_RULE_CALLBACK callback, void *user_data)
190
+ attach_function :yrx_rules_iter, [:pointer, :rule_callback, :pointer], :int
191
+
161
192
  # Public: Scan data using the configured scanner and rules.
162
193
  #
163
194
  # This function performs pattern matching against the provided data using
data/lib/yara/rule.rb ADDED
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yara
4
+ # Public: Represents a YARA rule from compiled rules.
5
+ #
6
+ # A Rule provides access to a YARA rule's metadata, tags, and patterns
7
+ # without needing to scan any data. This is useful for inspecting compiled
8
+ # rules, extracting metadata, and understanding rule structure.
9
+ #
10
+ # Examples
11
+ #
12
+ # # Typically created by Scanner#each_rule
13
+ # scanner.each_rule do |rule|
14
+ # puts "Rule: #{rule.identifier}"
15
+ # puts "Namespace: #{rule.namespace}"
16
+ # puts "Tags: #{rule.tags.join(', ')}"
17
+ # rule.metadata.each { |k, v| puts " #{k}: #{v}" }
18
+ # end
19
+ class Rule
20
+ # Public: The identifier (name) of the rule.
21
+ attr_reader :identifier
22
+
23
+ # Public: The namespace of the rule.
24
+ attr_reader :namespace
25
+
26
+ # Public: FFI pointer to the underlying YRX_RULE structure.
27
+ attr_reader :rule_ptr
28
+
29
+ # Public: Initialize a new Rule from a YRX_RULE pointer.
30
+ #
31
+ # This constructor extracts the rule identifier, namespace, metadata,
32
+ # and tags using the YARA-X C API.
33
+ #
34
+ # rule_ptr - An FFI Pointer to the YRX_RULE structure
35
+ #
36
+ # Examples
37
+ #
38
+ # # Typically created internally by Scanner#each_rule
39
+ # rule = Rule.new(rule_ptr)
40
+ def initialize(rule_ptr)
41
+ @rule_ptr = rule_ptr
42
+ @identifier = extract_identifier
43
+ @namespace = extract_namespace
44
+ @metadata_cache = nil
45
+ @tags_cache = nil
46
+ end
47
+
48
+ # Public: Get the rule's metadata as a Hash.
49
+ #
50
+ # Metadata is extracted from the rule's meta section and includes
51
+ # various types: strings, integers, floats, booleans, and bytes.
52
+ #
53
+ # Returns a Hash mapping metadata keys (Symbols) to their values.
54
+ #
55
+ # Examples
56
+ #
57
+ # metadata = rule.metadata
58
+ # # => { author: "test", severity: 5, is_malware: true }
59
+ def metadata
60
+ @metadata_cache ||= extract_metadata
61
+ end
62
+
63
+ # Public: Get the rule's tags as an Array.
64
+ #
65
+ # Tags are labels used to categorize and organize rules, defined
66
+ # after the rule name in the rule definition.
67
+ #
68
+ # Returns an Array of Strings containing the rule's tags.
69
+ #
70
+ # Examples
71
+ #
72
+ # tags = rule.tags
73
+ # # => ["malware", "trojan"]
74
+ def tags
75
+ @tags_cache ||= extract_tags
76
+ end
77
+
78
+ # Public: Get a qualified name combining namespace and identifier.
79
+ #
80
+ # Returns a String in the format "namespace.identifier".
81
+ #
82
+ # Examples
83
+ #
84
+ # rule.qualified_name
85
+ # # => "malware.trojan_detector"
86
+ def qualified_name
87
+ return identifier if namespace.nil? || namespace.empty?
88
+
89
+ "#{namespace}.#{identifier}"
90
+ end
91
+
92
+ # Public: Check if the rule has a specific tag.
93
+ #
94
+ # tag - A String or Symbol representing the tag to check
95
+ #
96
+ # Returns true if the tag exists, false otherwise.
97
+ #
98
+ # Examples
99
+ #
100
+ # rule.has_tag?("malware")
101
+ # # => true
102
+ def has_tag?(tag)
103
+ tags.include?(tag.to_s)
104
+ end
105
+
106
+ # Public: Get a metadata value by key with type checking.
107
+ #
108
+ # key - A Symbol or String representing the metadata key
109
+ #
110
+ # Returns the metadata value if found, nil otherwise.
111
+ #
112
+ # Examples
113
+ #
114
+ # rule.metadata_value(:author)
115
+ # # => "test_author"
116
+ def metadata_value(key)
117
+ metadata[key.to_sym]
118
+ end
119
+
120
+ # Public: Get a String metadata value with type validation.
121
+ #
122
+ # key - A Symbol or String representing the metadata key
123
+ #
124
+ # Returns the String value if found and is a String, nil otherwise.
125
+ #
126
+ # Examples
127
+ #
128
+ # rule.metadata_string(:author)
129
+ # # => "test_author"
130
+ def metadata_string(key)
131
+ value = metadata_value(key)
132
+ value.is_a?(String) ? value : nil
133
+ end
134
+
135
+ # Public: Get an Integer metadata value with type validation.
136
+ #
137
+ # key - A Symbol or String representing the metadata key
138
+ #
139
+ # Returns the Integer value if found and is an Integer, nil otherwise.
140
+ #
141
+ # Examples
142
+ #
143
+ # rule.metadata_int(:severity)
144
+ # # => 5
145
+ def metadata_int(key)
146
+ value = metadata_value(key)
147
+ value.is_a?(Integer) ? value : nil
148
+ end
149
+
150
+ # Public: Get a Boolean metadata value with type validation.
151
+ #
152
+ # key - A Symbol or String representing the metadata key
153
+ #
154
+ # Returns the Boolean value if found and is a Boolean, nil otherwise.
155
+ #
156
+ # Examples
157
+ #
158
+ # rule.metadata_bool(:is_malware)
159
+ # # => true
160
+ def metadata_bool(key)
161
+ value = metadata_value(key)
162
+ [true, false].include?(value) ? value : nil
163
+ end
164
+
165
+ # Public: Get a Float metadata value with type validation.
166
+ #
167
+ # key - A Symbol or String representing the metadata key
168
+ #
169
+ # Returns the Float value if found and is a Float, nil otherwise.
170
+ #
171
+ # Examples
172
+ #
173
+ # rule.metadata_float(:confidence)
174
+ # # => 0.95
175
+ def metadata_float(key)
176
+ value = metadata_value(key)
177
+ value.is_a?(Float) ? value : nil
178
+ end
179
+
180
+ # Internal: Extract the rule identifier using YARA-X API.
181
+ #
182
+ # Returns a String containing the rule name.
183
+ def extract_identifier
184
+ ident_ptr = ::FFI::MemoryPointer.new(:pointer)
185
+ len_ptr = ::FFI::MemoryPointer.new(:size_t)
186
+
187
+ result = Yara::FFI.yrx_rule_identifier(@rule_ptr, ident_ptr, len_ptr)
188
+ if result != Yara::FFI::YRX_SUCCESS
189
+ raise "Failed to extract rule identifier: #{Yara::FFI.yrx_last_error}"
190
+ end
191
+
192
+ ident = ident_ptr.read_pointer
193
+ length = len_ptr.read(:size_t)
194
+ ident.read_bytes(length).force_encoding("UTF-8")
195
+ end
196
+
197
+ # Internal: Extract the rule namespace using YARA-X API.
198
+ #
199
+ # Returns a String containing the namespace, or nil if default namespace.
200
+ def extract_namespace
201
+ ns_ptr = ::FFI::MemoryPointer.new(:pointer)
202
+ len_ptr = ::FFI::MemoryPointer.new(:size_t)
203
+
204
+ result = Yara::FFI.yrx_rule_namespace(@rule_ptr, ns_ptr, len_ptr)
205
+ if result != Yara::FFI::YRX_SUCCESS
206
+ raise "Failed to extract rule namespace: #{Yara::FFI.yrx_last_error}"
207
+ end
208
+
209
+ ns = ns_ptr.read_pointer
210
+ length = len_ptr.read(:size_t)
211
+ namespace_str = ns.read_bytes(length).force_encoding("UTF-8")
212
+
213
+ # Return nil for default namespace
214
+ namespace_str.empty? ? nil : namespace_str
215
+ end
216
+
217
+ # Internal: Extract metadata from the rule using YARA-X API.
218
+ #
219
+ # Returns a Hash mapping metadata keys to their values.
220
+ def extract_metadata
221
+ metadata = {}
222
+
223
+ metadata_callback = proc do |metadata_ptr, _user_data|
224
+ begin
225
+ # Read identifier (first field, pointer at offset 0)
226
+ identifier_ptr = metadata_ptr.get_pointer(0)
227
+ next if identifier_ptr.null?
228
+ identifier = identifier_ptr.read_string.to_sym
229
+
230
+ # Read value_type (int at offset 8, after the 8-byte pointer)
231
+ value_type = metadata_ptr.get_int32(8)
232
+
233
+ # The value union starts at offset 16 (pointer:8 + int:4 + padding:4)
234
+ # This is due to struct alignment requirements
235
+ value_offset = 16
236
+
237
+ value = case value_type
238
+ when Yara::FFI::YRX_METADATA_TYPE_I64
239
+ metadata_ptr.get_int64(value_offset)
240
+ when Yara::FFI::YRX_METADATA_TYPE_F64
241
+ metadata_ptr.get_double(value_offset)
242
+ when Yara::FFI::YRX_METADATA_TYPE_BOOLEAN
243
+ metadata_ptr.get_uint8(value_offset) != 0
244
+ when Yara::FFI::YRX_METADATA_TYPE_STRING
245
+ str_ptr = metadata_ptr.get_pointer(value_offset)
246
+ str_ptr.null? ? nil : str_ptr.read_string
247
+ when Yara::FFI::YRX_METADATA_TYPE_BYTES
248
+ length = metadata_ptr.get_size_t(value_offset)
249
+ data_ptr = metadata_ptr.get_pointer(value_offset + 8)
250
+ (data_ptr.null? || length == 0) ? nil : data_ptr.read_bytes(length)
251
+ else
252
+ nil
253
+ end
254
+
255
+ metadata[identifier] = value unless value.nil?
256
+ rescue => e
257
+ # Skip problematic metadata entries to ensure partial extraction works
258
+ end
259
+ end
260
+
261
+ result = Yara::FFI.yrx_rule_iter_metadata(@rule_ptr, metadata_callback, nil)
262
+ if result != Yara::FFI::YRX_SUCCESS
263
+ raise "Failed to iterate rule metadata: #{Yara::FFI.yrx_last_error}"
264
+ end
265
+
266
+ metadata
267
+ end
268
+
269
+ # Internal: Extract tags from the rule using YARA-X API.
270
+ #
271
+ # Returns an Array of Strings containing the rule's tags.
272
+ def extract_tags
273
+ tags = []
274
+
275
+ tag_callback = proc do |tag_ptr, _user_data|
276
+ tags << tag_ptr.read_string unless tag_ptr.null?
277
+ end
278
+
279
+ result = Yara::FFI.yrx_rule_iter_tags(@rule_ptr, tag_callback, nil)
280
+ if result != Yara::FFI::YRX_SUCCESS
281
+ raise "Failed to iterate rule tags: #{Yara::FFI.yrx_last_error}"
282
+ end
283
+
284
+ tags
285
+ end
286
+ end
287
+ end
data/lib/yara/scanner.rb CHANGED
@@ -225,6 +225,9 @@ module Yara
225
225
  def scan(test_string)
226
226
  raise NotCompiledError, "Rules not compiled. Call compile() first." unless @scanner_pointer
227
227
 
228
+ # Handle nil input by treating it as empty string
229
+ test_string = "" if test_string.nil?
230
+
228
231
  results = ScanResults.new
229
232
 
230
233
  # Set up callback for matching rules
@@ -268,6 +271,55 @@ module Yara
268
271
  block_given? ? nil : results
269
272
  end
270
273
 
274
+ # Public: Iterate through all compiled rules without scanning.
275
+ #
276
+ # This method iterates through all rules in the compiled ruleset, yielding
277
+ # each as a Rule object. Unlike scan(), this does not require any data and
278
+ # provides access to rule metadata, tags, and other information for inspection.
279
+ #
280
+ # This is useful for:
281
+ # - Inspecting compiled rules without scanning
282
+ # - Extracting metadata from rule sets
283
+ # - Validating rule compilation
284
+ # - Building rule catalogs or indexes
285
+ #
286
+ # If no block is given, returns an Enumerator.
287
+ #
288
+ # Examples
289
+ #
290
+ # # Iterate with a block
291
+ # scanner.each_rule do |rule|
292
+ # puts "Rule: #{rule.identifier}"
293
+ # puts "Namespace: #{rule.namespace}"
294
+ # puts "Tags: #{rule.tags.join(', ')}"
295
+ # rule.metadata.each { |k, v| puts " #{k}: #{v}" }
296
+ # end
297
+ #
298
+ # # Or use as an Enumerator
299
+ # rules = scanner.each_rule.to_a
300
+ #
301
+ # Yields each Rule object.
302
+ # Returns nil when block given, Enumerator when no block given.
303
+ # Raises NotCompiledError if rules haven't been compiled yet.
304
+ def each_rule
305
+ raise NotCompiledError, "Rules must be compiled before iterating" unless @rules_pointer
306
+
307
+ return enum_for(:each_rule) unless block_given?
308
+
309
+ rule_callback = proc do |rule_ptr, _user_data|
310
+ rule = Rule.new(rule_ptr)
311
+ yield rule
312
+ end
313
+
314
+ result = Yara::FFI.yrx_rules_iter(@rules_pointer, rule_callback, nil)
315
+ if result != Yara::FFI::YRX_SUCCESS
316
+ error_msg = Yara::FFI.yrx_last_error
317
+ raise ScanError, "Failed to iterate rules: #{error_msg}"
318
+ end
319
+
320
+ nil
321
+ end
322
+
271
323
  # Public: Set a timeout for scanning operations on this scanner (milliseconds).
272
324
  #
273
325
  # This method configures the scanner to abort scans that take longer than
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.1.0"
8
+ VERSION = "4.2.0"
9
9
  end
data/lib/yara.rb CHANGED
@@ -5,6 +5,7 @@ require "json"
5
5
  require "pry"
6
6
  require_relative "yara/ffi"
7
7
  require_relative "yara/pattern_match"
8
+ require_relative "yara/rule"
8
9
  require_relative "yara/scan_result"
9
10
  require_relative "yara/scan_results"
10
11
  require_relative "yara/scanner"
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.1.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Hoyt
@@ -50,6 +50,7 @@ files:
50
50
  - lib/yara/compiler.rb
51
51
  - lib/yara/ffi.rb
52
52
  - lib/yara/pattern_match.rb
53
+ - lib/yara/rule.rb
53
54
  - lib/yara/scan_result.rb
54
55
  - lib/yara/scan_results.rb
55
56
  - lib/yara/scanner.rb