yara-ffi 3.1.0 → 4.0.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.
data/lib/yara/scanner.rb CHANGED
@@ -1,81 +1,269 @@
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
3
- class NotCompiledError < StandardError; end
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
4
37
 
5
- ERROR_CALLBACK = proc do |error_level, file_name, line_number, rule, message, user_data|
6
- # noop
7
- end
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
8
43
 
9
- SCAN_FINISHED = 3
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.
48
+ class NotCompiledError < StandardError; end
10
49
 
11
- # Public: Initializes instance of scanner. Under the hood this creates a pointer,
12
- # then calls yr_compiler_create with that pointer.
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.
13
54
  #
14
- # error_callback: (optional) Proc to be called when an error occurs.
15
- # user_data: (optional) Instance of UserData to store and pass information.
16
- def initialize(error_callback: ERROR_CALLBACK, user_data: UserData.new)
17
- @error_callback = error_callback
18
- @user_data = user_data
19
- @compiler_pointer = ::FFI::MemoryPointer.new(:pointer)
20
- Yara::FFI.yr_compiler_create(@compiler_pointer)
21
- @compiler_pointer = @compiler_pointer.get_pointer(0)
22
- Yara::FFI.yr_compiler_set_callback(@compiler_pointer, error_callback, user_data)
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 = ""
23
64
  end
24
65
 
25
- # Public: Adds a rule to the scanner and returns the namespace value. If a namespace
26
- # is not provided it will default to nil and use the global namespace.
66
+ # Public: Add a YARA rule to the scanner for later compilation.
67
+ #
68
+ # Rules are accumulated as source code and compiled together when compile()
69
+ # is called. Multiple rules can be added to create rule sets. Optional
70
+ # namespacing allows logical grouping of related rules.
71
+ #
72
+ # rule_string - A String containing a complete YARA rule definition
73
+ # namespace - An optional String namespace to contain the rule
74
+ #
75
+ # Examples
27
76
  #
28
- # rule_string - String containing the Yara rule to be added.
29
- # namespace: (optional) String containing the namespace to be used for the rule.
77
+ # scanner.add_rule('rule test1 { condition: true }')
78
+ # scanner.add_rule('rule test2 { condition: false }', namespace: 'testing')
79
+ #
80
+ # Returns nothing.
30
81
  def add_rule(rule_string, namespace: nil)
31
- Yara::FFI.yr_compiler_add_string(@compiler_pointer, rule_string, namespace)
82
+ # For now, we'll just store the rule source and compile later
83
+ # yara-x doesn't have separate add_rule like libyara
84
+ if namespace
85
+ @rule_source += "\nnamespace #{namespace} {\n#{rule_string}\n}\n"
86
+ else
87
+ @rule_source += "\n#{rule_string}\n"
88
+ end
32
89
  end
33
90
 
91
+ # Public: Compile all added rules into an executable scanner.
92
+ #
93
+ # This method compiles all rules added via add_rule() into an optimized
94
+ # form suitable for scanning. Compilation must succeed before any scanning
95
+ # operations can be performed. The compiled rules are used to create an
96
+ # internal scanner object for efficient data processing.
97
+ #
98
+ # Examples
99
+ #
100
+ # scanner = Scanner.new
101
+ # scanner.add_rule('rule test { condition: true }')
102
+ # scanner.compile
103
+ #
104
+ # Returns nothing.
105
+ # Raises CompilationError if rule compilation fails.
34
106
  def compile
107
+ raise CompilationError, "No rules added" if @rule_source.empty?
108
+
35
109
  @rules_pointer = ::FFI::MemoryPointer.new(:pointer)
36
- Yara::FFI.yr_compiler_get_rules(@compiler_pointer, @rules_pointer)
110
+ result = Yara::FFI.yrx_compile(@rule_source, @rules_pointer)
111
+
112
+ if result != Yara::FFI::YRX_SUCCESS
113
+ error_msg = Yara::FFI.yrx_last_error
114
+ raise CompilationError, "Failed to compile rules: #{error_msg}"
115
+ end
116
+
37
117
  @rules_pointer = @rules_pointer.get_pointer(0)
38
- Yara::FFI.yr_compiler_destroy(@compiler_pointer)
118
+
119
+ # Create scanner
120
+ @scanner_pointer_holder = ::FFI::MemoryPointer.new(:pointer)
121
+ result = Yara::FFI.yrx_scanner_create(@rules_pointer, @scanner_pointer_holder)
122
+
123
+ if result != Yara::FFI::YRX_SUCCESS
124
+ error_msg = Yara::FFI.yrx_last_error
125
+ raise CompilationError, "Failed to create scanner: #{error_msg}"
126
+ end
127
+
128
+ @scanner_pointer = @scanner_pointer_holder.get_pointer(0)
39
129
  end
40
130
 
41
- def call(test_string)
42
- raise NotCompiledError unless @rules_pointer
43
-
44
- results = []
45
- scanning = true
46
- result_callback = proc do |context_ptr, callback_type, rule, user_data|
47
- if callback_type == SCAN_FINISHED
48
- scanning = false
49
- else
50
- result = ScanResult.new(callback_type, rule, user_data)
51
- results << result if result.rule_outcome?
131
+ # Public: Scan data against compiled rules.
132
+ #
133
+ # This method scans the provided data using all compiled rules, returning
134
+ # information about any matches found. When a block is provided, each
135
+ # matching rule is yielded immediately as it's discovered during scanning.
136
+ #
137
+ # Scanning treats the input as binary data regardless of content type.
138
+ # String encoding is preserved but pattern matching occurs at the byte level.
139
+ #
140
+ # test_string - A String containing the data to scan
141
+ # block - Optional block that receives each ScanResult as found
142
+ #
143
+ # Examples
144
+ #
145
+ # # Collect all results
146
+ # results = scanner.scan("data to scan")
147
+ # results.each { |match| puts match.rule_name }
148
+ #
149
+ # # Process matches immediately
150
+ # scanner.scan("data to scan") do |match|
151
+ # puts "Found: #{match.rule_name}"
152
+ # end
153
+ #
154
+ # Returns a ScanResults object containing matches when no block given.
155
+ # Returns nil when a block is provided (matches are yielded instead).
156
+ # Raises NotCompiledError if compile() has not been called.
157
+ # Raises ScanError if scanning fails.
158
+ def scan(test_string)
159
+ raise NotCompiledError, "Rules not compiled. Call compile() first." unless @scanner_pointer
160
+
161
+ results = ScanResults.new
162
+
163
+ # Set up callback for matching rules
164
+ callback = proc do |rule_ptr, user_data|
165
+ # Extract rule identifier
166
+ ident_ptr = ::FFI::MemoryPointer.new(:pointer)
167
+ len_ptr = ::FFI::MemoryPointer.new(:size_t)
168
+
169
+ if Yara::FFI.yrx_rule_identifier(rule_ptr, ident_ptr, len_ptr) == Yara::FFI::YRX_SUCCESS
170
+ identifier_ptr = ident_ptr.get_pointer(0)
171
+ identifier_len = len_ptr.get_ulong(0)
172
+ rule_name = identifier_ptr.read_string(identifier_len)
173
+
174
+ # Create a result with the rule source for metadata/string parsing
175
+ result = ScanResult.new(rule_name, rule_ptr, true, @rule_source)
176
+ results << result
177
+
178
+ yield result if block_given?
52
179
  end
180
+ end
53
181
 
54
- 0 # ERROR_SUCCESS
182
+ # Set the callback
183
+ result = Yara::FFI.yrx_scanner_on_matching_rule(@scanner_pointer, callback, nil)
184
+ if result != Yara::FFI::YRX_SUCCESS
185
+ error_msg = Yara::FFI.yrx_last_error
186
+ raise ScanError, "Failed to set callback: #{error_msg}"
55
187
  end
56
188
 
189
+ # Scan the data
57
190
  test_string_bytesize = test_string.bytesize
58
191
  test_string_pointer = ::FFI::MemoryPointer.new(:char, test_string_bytesize)
59
192
  test_string_pointer.put_bytes(0, test_string)
60
193
 
61
- Yara::FFI.yr_rules_scan_mem(
62
- @rules_pointer,
63
- test_string_pointer,
64
- test_string_bytesize,
65
- 0,
66
- result_callback,
67
- @user_data,
68
- 1,
69
- )
70
-
71
- while scanning do
194
+ result = Yara::FFI.yrx_scanner_scan(@scanner_pointer, test_string_pointer, test_string_bytesize)
195
+ if result != Yara::FFI::YRX_SUCCESS
196
+ error_msg = Yara::FFI.yrx_last_error
197
+ raise ScanError, "Scan failed: #{error_msg}"
72
198
  end
73
199
 
74
- results
200
+ block_given? ? nil : results
75
201
  end
76
202
 
203
+ # Public: Free all resources associated with this scanner.
204
+ #
205
+ # This method releases memory allocated by YARA-X for the compiled rules
206
+ # and scanner objects. It must be called to prevent memory leaks when
207
+ # using manual resource management. After calling close(), the scanner
208
+ # cannot be used for further operations.
209
+ #
210
+ # The open() class method with a block automatically calls close() to
211
+ # ensure proper cleanup even if exceptions occur.
212
+ #
213
+ # Examples
214
+ #
215
+ # scanner = Scanner.new
216
+ # # ... use scanner
217
+ # scanner.close # Required for cleanup
218
+ #
219
+ # Returns nothing.
77
220
  def close
78
- Yara::FFI.yr_rules_destroy(@rules_pointer)
221
+ Yara::FFI.yrx_scanner_destroy(@scanner_pointer) if @scanner_pointer
222
+ Yara::FFI.yrx_rules_destroy(@rules_pointer) if @rules_pointer
223
+ @scanner_pointer = nil
224
+ @rules_pointer = nil
225
+ end
226
+
227
+ # Public: Create a scanner with automatic resource management.
228
+ #
229
+ # This class method creates a Scanner instance and optionally adds an
230
+ # initial rule. When used with a block, it ensures proper resource cleanup
231
+ # by automatically calling close() even if exceptions occur during scanning.
232
+ # This is the recommended way to use Scanner for most applications.
233
+ #
234
+ # rule_string - An optional String containing a YARA rule definition
235
+ # namespace - An optional String namespace for the initial rule
236
+ # block - Block that receives the scanner instance
237
+ #
238
+ # Examples
239
+ #
240
+ # # Block syntax with automatic cleanup (recommended)
241
+ # Scanner.open(rule) do |scanner|
242
+ # scanner.compile
243
+ # results = scanner.scan(data)
244
+ # end
245
+ #
246
+ # # Without block (manual cleanup required)
247
+ # scanner = Scanner.open(rule)
248
+ # scanner.compile
249
+ # # ... use scanner
250
+ # scanner.close
251
+ #
252
+ # Returns the result of the block when block given.
253
+ # Returns a new Scanner instance when no block given.
254
+ def self.open(rule_string = nil, namespace: nil)
255
+ scanner = new
256
+ scanner.add_rule(rule_string, namespace: namespace) if rule_string
257
+
258
+ if block_given?
259
+ begin
260
+ yield scanner
261
+ ensure
262
+ scanner.close
263
+ end
264
+ else
265
+ scanner
266
+ end
79
267
  end
80
268
  end
81
269
  end
data/lib/yara/version.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yara
4
- VERSION = "3.1.0"
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.0.0"
5
9
  end
data/lib/yara.rb CHANGED
@@ -4,26 +4,81 @@ require "ffi"
4
4
  require "pry"
5
5
  require_relative "yara/ffi"
6
6
  require_relative "yara/scan_result"
7
+ require_relative "yara/scan_results"
7
8
  require_relative "yara/scanner"
8
9
  require_relative "yara/version"
9
10
 
11
+ # Public: Main module providing Ruby FFI bindings to YARA-X for pattern
12
+ # matching and malware detection.
13
+ #
14
+ # This gem provides a Ruby interface to the YARA-X library (Rust-based YARA
15
+ # implementation) for scanning files, strings, and binary data using YARA rules.
16
+ # It offers both high-level convenience methods and low-level scanner control.
17
+ #
18
+ # Examples
19
+ #
20
+ # # Quick scanning with automatic resource cleanup
21
+ # rule = 'rule test { strings: $a = "hello" condition: $a }'
22
+ # results = Yara.scan(rule, "hello world")
23
+ #
24
+ # # Manual scanner control for advanced use cases
25
+ # Yara::Scanner.open(rule) do |scanner|
26
+ # scanner.compile
27
+ # results = scanner.scan(data)
28
+ # end
10
29
  module Yara
11
- def self.start
12
- Yara::FFI.yr_initialize
13
- end
14
-
15
- def self.stop
16
- Yara::FFI.yr_finalize
30
+ # Public: Test a YARA rule against data with automatic cleanup.
31
+ #
32
+ # This is a convenience method that handles the complete scan lifecycle:
33
+ # rule compilation, scanning, and resource cleanup. Use this for simple
34
+ # one-off scans where you don't need fine-grained control.
35
+ #
36
+ # rule_string - A String containing the YARA rule definition
37
+ # test_string - A String containing the data to scan
38
+ #
39
+ # Examples
40
+ #
41
+ # rule = 'rule test { strings: $a = "malware" condition: $a }'
42
+ # results = Yara.test(rule, "potential malware signature")
43
+ # # => #<Yara::ScanResults:0x... @results=[...]>
44
+ #
45
+ # Returns a Yara::ScanResults object containing any matching rules.
46
+ # Raises Yara::Scanner::CompilationError if the rule is invalid.
47
+ # Raises Yara::Scanner::ScanError if scanning fails.
48
+ def self.test(rule_string, test_string)
49
+ Scanner.open(rule_string) do |scanner|
50
+ scanner.compile
51
+ scanner.scan(test_string)
52
+ end
17
53
  end
18
54
 
19
- def self.test(rule_string, test_string)
20
- start
21
- scanner = Yara::Scanner.new
22
- scanner.add_rule(rule_string)
23
- scanner.compile
24
- scanner.call(test_string)
25
- ensure
26
- scanner.close
27
- stop
55
+ # Public: Scan data with a YARA rule, optionally yielding each match.
56
+ #
57
+ # This is a convenience method for scanning with optional block-based
58
+ # processing of results. When a block is provided, each matching rule
59
+ # is yielded as it's found during scanning.
60
+ #
61
+ # rule_string - A String containing the YARA rule definition
62
+ # data - A String containing the data to scan
63
+ # block - Optional block that receives each ScanResult as found
64
+ #
65
+ # Examples
66
+ #
67
+ # # Collect all results
68
+ # results = Yara.scan(rule, data)
69
+ #
70
+ # # Process matches as they're found
71
+ # Yara.scan(rule, data) do |match|
72
+ # puts "Found: #{match.rule_name}"
73
+ # end
74
+ #
75
+ # Returns a Yara::ScanResults object when no block given, nil when block given.
76
+ # Raises Yara::Scanner::CompilationError if the rule is invalid.
77
+ # Raises Yara::Scanner::ScanError if scanning fails.
78
+ def self.scan(rule_string, data, &block)
79
+ Scanner.open(rule_string) do |scanner|
80
+ scanner.compile
81
+ scanner.scan(data, &block)
82
+ end
28
83
  end
29
84
  end
data/yara-ffi.gemspec CHANGED
@@ -8,11 +8,11 @@ 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 libyara."
12
- spec.description = "Use libyara from Ruby via ffi bindings."
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.4.0")
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"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yara-ffi
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Hoyt
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-04-18 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ffi
@@ -24,18 +23,20 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
- description: Use libyara from Ruby via ffi bindings.
26
+ description: Use YARA-X from Ruby via FFI bindings.
28
27
  email:
29
28
  - jonmagic@gmail.com
30
29
  executables: []
31
30
  extensions: []
32
31
  extra_rdoc_files: []
33
32
  files:
33
+ - ".github/copilot-instructions.md"
34
34
  - ".github/workflows/ruby.yml"
35
35
  - ".gitignore"
36
36
  - ".rubocop.yml"
37
37
  - CHANGELOG.md
38
38
  - CODE_OF_CONDUCT.md
39
+ - DEVELOPMENT.md
39
40
  - Dockerfile
40
41
  - Gemfile
41
42
  - Gemfile.lock
@@ -47,13 +48,9 @@ files:
47
48
  - lib/yara.rb
48
49
  - lib/yara/ffi.rb
49
50
  - lib/yara/scan_result.rb
51
+ - lib/yara/scan_results.rb
50
52
  - lib/yara/scanner.rb
51
- - lib/yara/user_data.rb
52
53
  - lib/yara/version.rb
53
- - lib/yara/yr_meta.rb
54
- - lib/yara/yr_namespace.rb
55
- - lib/yara/yr_rule.rb
56
- - lib/yara/yr_string.rb
57
54
  - script/bootstrap
58
55
  - script/test
59
56
  - yara-ffi.gemspec
@@ -64,7 +61,6 @@ metadata:
64
61
  homepage_uri: https://github.com/jonmagic/yara-ffi
65
62
  source_code_uri: https://github.com/jonmagic/yara-ffi
66
63
  changelog_uri: https://github.com/jonmagic/yara-ffi/main/CHANGELOG.md,
67
- post_install_message:
68
64
  rdoc_options: []
69
65
  require_paths:
70
66
  - lib
@@ -72,15 +68,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
72
68
  requirements:
73
69
  - - ">="
74
70
  - !ruby/object:Gem::Version
75
- version: 2.4.0
71
+ version: 3.2.0
76
72
  required_rubygems_version: !ruby/object:Gem::Requirement
77
73
  requirements:
78
74
  - - ">="
79
75
  - !ruby/object:Gem::Version
80
76
  version: '0'
81
77
  requirements: []
82
- rubygems_version: 3.3.3
83
- signing_key:
78
+ rubygems_version: 3.6.9
84
79
  specification_version: 4
85
- summary: A Ruby API to libyara.
80
+ summary: A Ruby API to YARA-X.
86
81
  test_files: []
@@ -1,5 +0,0 @@
1
- module Yara
2
- class UserData < FFI::Struct
3
- layout :number, :int
4
- end
5
- end
data/lib/yara/yr_meta.rb DELETED
@@ -1,10 +0,0 @@
1
- module Yara
2
- class YrMeta < FFI::Struct
3
- layout \
4
- :identifier, :string,
5
- :string, :string,
6
- :integer, :ulong_long,
7
- :type, :int,
8
- :flags, :int
9
- end
10
- end
@@ -1,5 +0,0 @@
1
- module Yara
2
- class YrNamespace < FFI::Struct
3
- layout :name, :string
4
- end
5
- end
data/lib/yara/yr_rule.rb DELETED
@@ -1,11 +0,0 @@
1
- module Yara
2
- class YrRule < FFI::Struct
3
- layout \
4
- :flags, :int,
5
- :identifier, :string,
6
- :tags, :string,
7
- :metas, :pointer,
8
- :strings, :pointer,
9
- :ns, YrNamespace.ptr
10
- end
11
- end
@@ -1,15 +0,0 @@
1
- module Yara
2
- class YrString < FFI::Struct
3
- layout \
4
- :flags, :uint,
5
- :idx, :uint,
6
- :fixed_offset, :ulong_long,
7
- :rule_idx, :uint,
8
- :length, :uint,
9
- :string, :pointer,
10
- :chained_to, :pointer,
11
- :chain_gap_min, :uint,
12
- :chain_gap_max, :uint,
13
- :identifier, :string
14
- end
15
- end