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 +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +13 -13
- data/USAGE.md +154 -0
- data/lib/yara/ffi.rb +31 -0
- data/lib/yara/rule.rb +287 -0
- data/lib/yara/scanner.rb +52 -0
- data/lib/yara/version.rb +1 -1
- data/lib/yara.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e278e7deb04454bfa05c54feb3763d282073abe392ddbd142cef55afa749637
|
|
4
|
+
data.tar.gz: ec29557182c143b5d8a696d7d494bbf7e737fdd115cddaf637538322902863fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2bcfc0f2f49fbbb2faf7c0bd221c4b6c8b41ff7c3724feca975e4ae4c89f4d3528966efcb5123d315aafc95decde239ccd7d27a1c0327ce578990f84db7e131a
|
|
7
|
+
data.tar.gz: fb704aebc592c80b63012acbd060b17608f9a6c43250a560379c39b11e6e813825df67ddc4b5daaf676fa18da383fe3f73f66bfcc87421b906c21f33a721dfcf
|
data/.github/workflows/ruby.yml
CHANGED
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.
|
|
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.
|
|
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.
|
|
21
|
+
minitest (5.26.1)
|
|
22
22
|
parallel (1.27.0)
|
|
23
|
-
parser (3.3.
|
|
23
|
+
parser (3.3.10.0)
|
|
24
24
|
ast (~> 2.4.1)
|
|
25
25
|
racc
|
|
26
|
-
prism (1.
|
|
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.
|
|
33
|
-
regexp_parser (2.11.
|
|
34
|
-
rubocop (1.
|
|
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.
|
|
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.
|
|
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.
|
|
50
|
-
unicode-emoji (~> 4.
|
|
51
|
-
unicode-emoji (4.0
|
|
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
data/lib/yara.rb
CHANGED
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.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
|